mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-18 07:40:09 +02:00
Merge remote-tracking branch 'origin/main' into garrytan/askuserquestion-split-on-overflow
This commit is contained in:
@@ -1,9 +1,20 @@
|
||||
/**
|
||||
* RESOLVERS record — maps {{PLACEHOLDER}} names to generator functions.
|
||||
* RESOLVERS record — maps {{PLACEHOLDER}} names to generator functions
|
||||
* or gated entries.
|
||||
*
|
||||
* Each resolver takes a TemplateContext and returns the replacement string.
|
||||
* Resolvers may be either a bare function (always fires) or a gated entry
|
||||
* ({ resolve, appliesTo }) where appliesTo can return false to skip the
|
||||
* resolver for a given skill. See ./types.ts: ResolverEntry.
|
||||
*
|
||||
* Most resolvers don't need a gate — the {{NAME}} placeholder system is
|
||||
* already conditional at the template level (the resolver only fires for
|
||||
* skills that reference it). Use a gate when you want a structural
|
||||
* guardrail that says "this placeholder is meaningful only in skills X, Y, Z"
|
||||
* even if someone later adds {{NAME}} to skill W.
|
||||
*/
|
||||
|
||||
import type { TemplateContext, ResolverFn } from './types';
|
||||
import type { TemplateContext, ResolverFn, ResolverValue } from './types';
|
||||
|
||||
// Domain modules
|
||||
import { generatePreamble } from './preamble';
|
||||
@@ -24,7 +35,7 @@ import { generateQuestionPreferenceCheck, generateQuestionLog, generateInlineTun
|
||||
import { generateMakePdfSetup } from './make-pdf';
|
||||
import { generateTasksSectionEmit, generateTasksSectionAggregate } from './tasks-section';
|
||||
|
||||
export const RESOLVERS: Record<string, ResolverFn> = {
|
||||
export const RESOLVERS: Record<string, ResolverValue> = {
|
||||
SLUG_EVAL: generateSlugEval,
|
||||
SLUG_SETUP: generateSlugSetup,
|
||||
COMMAND_REFERENCE: generateCommandReference,
|
||||
|
||||
@@ -109,10 +109,10 @@ export function generatePreamble(ctx: TemplateContext): string {
|
||||
...(tier >= 2 ? [
|
||||
generateContextRecovery(ctx),
|
||||
generateWritingStyle(ctx),
|
||||
generateCompletenessSection(),
|
||||
generateConfusionProtocol(),
|
||||
generateCompletenessSection(ctx),
|
||||
generateConfusionProtocol(ctx),
|
||||
generateContinuousCheckpoint(),
|
||||
generateContextHealth(),
|
||||
generateContextHealth(ctx),
|
||||
generateQuestionTuning(ctx),
|
||||
] : []),
|
||||
...(tier >= 3 ? [generateRepoModeSection(), generateSearchBeforeBuildingSection(ctx)] : []),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TemplateContext } from '../types';
|
||||
|
||||
|
||||
export function generateCompletenessSection(): string {
|
||||
export function generateCompletenessSection(ctx?: TemplateContext): string {
|
||||
if (ctx?.explainLevel === 'terse') return '';
|
||||
return `## Completeness Principle — Boil the Lake
|
||||
|
||||
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export function generateConfusionProtocol(): string {
|
||||
import type { TemplateContext } from '../types';
|
||||
|
||||
export function generateConfusionProtocol(ctx?: TemplateContext): string {
|
||||
if (ctx?.explainLevel === 'terse') return '';
|
||||
return `## Confusion Protocol
|
||||
|
||||
For high-stakes ambiguity (architecture, data model, destructive scope, missing context), STOP. Name it in one sentence, present 2-3 options with tradeoffs, and ask. Do not use for routine coding or obvious changes.`;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TemplateContext } from '../types';
|
||||
|
||||
|
||||
export function generateContextHealth(): string {
|
||||
export function generateContextHealth(ctx?: TemplateContext): string {
|
||||
if (ctx?.explainLevel === 'terse') return '';
|
||||
return `## Context Health (soft directive)
|
||||
|
||||
During long-running skill sessions, periodically write a brief \`[PROGRESS]\` summary: done, next, surprises.
|
||||
|
||||
@@ -90,6 +90,19 @@ _CHECKPOINT_MODE=$(${ctx.paths.binDir}/gstack-config get checkpoint_mode 2>/dev/
|
||||
_CHECKPOINT_PUSH=$(${ctx.paths.binDir}/gstack-config get checkpoint_push 2>/dev/null || echo "false")
|
||||
echo "CHECKPOINT_MODE: $_CHECKPOINT_MODE"
|
||||
echo "CHECKPOINT_PUSH: $_CHECKPOINT_PUSH"
|
||||
# Plan-mode hint for skills like /spec that branch behavior on plan-mode state.
|
||||
# Claude Code exposes plan mode via system reminders; we detect best-effort
|
||||
# from CLAUDE_PLAN_FILE (set by the harness when plan mode is active) and
|
||||
# fall back to "inactive". Codex hosts and Claude execution mode both end up
|
||||
# inactive, which is the safe default (defaults to file+execute pipeline).
|
||||
if [ -n "\${CLAUDE_PLAN_FILE:-}\${GSTACK_PLAN_MODE_FORCE:-}" ]; then
|
||||
export GSTACK_PLAN_MODE="active"
|
||||
elif [ "\${GSTACK_PLAN_MODE:-}" = "active" ]; then
|
||||
export GSTACK_PLAN_MODE="active"
|
||||
else
|
||||
export GSTACK_PLAN_MODE="inactive"
|
||||
fi
|
||||
echo "GSTACK_PLAN_MODE: $GSTACK_PLAN_MODE"
|
||||
[ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true${ctx.host === 'gbrain' || ctx.host === 'hermes' ? `
|
||||
if command -v gbrain &>/dev/null; then
|
||||
_BRAIN_JSON=$(gbrain doctor --fast --json 2>/dev/null || echo '{}')
|
||||
|
||||
@@ -33,6 +33,7 @@ Key routing rules:
|
||||
- Ship/deploy/PR → invoke /ship or /land-and-deploy
|
||||
- Save progress → invoke /context-save
|
||||
- Resume context → invoke /context-restore
|
||||
- Author a backlog-ready spec/issue → invoke /spec
|
||||
\`\`\`
|
||||
|
||||
Then commit the change: \`git add CLAUDE.md && git commit -m "chore: add gstack skill routing rules to CLAUDE.md"\`
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { TemplateContext } from '../types';
|
||||
|
||||
function loadJargonList(): string[] {
|
||||
const jargonPath = path.join(__dirname, '..', '..', 'jargon-list.json');
|
||||
try {
|
||||
const raw = fs.readFileSync(jargonPath, 'utf-8');
|
||||
const data = JSON.parse(raw);
|
||||
if (Array.isArray(data?.terms)) return data.terms.filter((t: unknown): t is string => typeof t === 'string');
|
||||
} catch {
|
||||
// Missing or malformed: fall back to empty list. Writing Style block still fires,
|
||||
// but with no terms to gloss — graceful degradation.
|
||||
/**
|
||||
* Writing Style preamble section.
|
||||
*
|
||||
* v1.45.0.0 changes (T3):
|
||||
* - Jargon list is referenced by path, not inlined. The 80-term list was
|
||||
* duplicated into every tier-2+ skill (~1.5-2 KB × 48 skills = ~80 KB
|
||||
* across the corpus). The pointer asks the agent to Read the JSON on
|
||||
* first jargon term encountered — one extra Read per session, but the
|
||||
* per-corpus payload is ~30 bytes.
|
||||
* - When `ctx.explainLevel === 'terse'`, the entire section is replaced
|
||||
* with a one-line pointer. Saves ~1.5 KB per tier-2+ skill in the
|
||||
* opt-in terse build.
|
||||
*/
|
||||
export function generateWritingStyle(ctx: TemplateContext): string {
|
||||
if (ctx.explainLevel === 'terse') {
|
||||
return `## Writing Style\n\nTerse mode (build-time): skip jargon glossing, outcome-framing layer, and decision-impact closers. Lead with the answer.\n`;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function generateWritingStyle(_ctx: TemplateContext): string {
|
||||
const terms = loadJargonList();
|
||||
const jargonBlock = terms.length > 0
|
||||
? `Jargon list, gloss on first use if the term appears:\n${terms.map(t => `- ${t}`).join('\n')}`
|
||||
: `Jargon list unavailable. Skip jargon glossing until \`scripts/jargon-list.json\` is restored.`;
|
||||
const jargonPath = `${ctx.paths.skillRoot}/scripts/jargon-list.json`;
|
||||
|
||||
return `## Writing Style (skip entirely if \`EXPLAIN_LEVEL: terse\` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||
|
||||
@@ -32,6 +31,6 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
|
||||
- User-turn override wins: if the current message asks for terse / no explanations / just the answer, skip this section.
|
||||
- Terse mode (EXPLAIN_LEVEL: terse): no glosses, no outcome-framing layer, shorter responses.
|
||||
|
||||
${jargonBlock}
|
||||
Curated jargon list lives at \`${jargonPath}\` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the \`terms\` array as the canonical list. The list is repo-owned and may grow between releases.
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,56 @@ export interface TemplateContext {
|
||||
preambleTier?: number; // 1-4, controls which preamble sections are included
|
||||
model?: Model; // model family for behavioral overlay. Omitted/undefined → no overlay.
|
||||
interactive?: boolean; // true → emit plan-mode handshake in preamble. Generator-only, not written to SKILL.md.
|
||||
/**
|
||||
* Build-time compression mode. Defaults to 'default'.
|
||||
*
|
||||
* - 'default': full preamble prose ships as today (writing style, completeness,
|
||||
* confusion protocol, context health are all present).
|
||||
* - 'terse': writing-style + completeness + confusion-protocol + context-health
|
||||
* sections are compressed to a one-line pointer at gen time. Saves ~3-5 KB
|
||||
* per tier-2+ skill. Opt-in via `--explain-level=terse` build flag for
|
||||
* users who want shipped skills to match their runtime preference and
|
||||
* avoid the per-session terse-mode prose.
|
||||
*
|
||||
* Default builds keep the runtime-conditional behavior intact (Writing Style
|
||||
* section says "skip entirely if EXPLAIN_LEVEL: terse appears in preamble echo").
|
||||
* Terse builds make the compression structural — bytes never ship in the first place.
|
||||
*/
|
||||
explainLevel?: 'default' | 'terse';
|
||||
}
|
||||
|
||||
/** Resolver function signature. args is populated for parameterized placeholders like {{INVOKE_SKILL:name}}. */
|
||||
export type ResolverFn = (ctx: TemplateContext, args?: string[]) => string;
|
||||
|
||||
/**
|
||||
* Optional gated resolver. When the gate returns false, the resolver is
|
||||
* skipped (substituted with empty string) — same effect as the placeholder
|
||||
* not being referenced. Use when a resolver's output is only meaningful for
|
||||
* a known subset of skills, so future template authors get a structural
|
||||
* guardrail instead of relying on social knowledge.
|
||||
*
|
||||
* Most resolvers don't need this — the {{NAME}} placeholder system is
|
||||
* already conditional at the template level. Use only when a resolver
|
||||
* lives inside another resolver (e.g. via preamble composition) AND must
|
||||
* be conditionalized, or when a top-level resolver has a small, well-defined
|
||||
* audience.
|
||||
*/
|
||||
export interface ResolverEntry {
|
||||
resolve: ResolverFn;
|
||||
appliesTo?: (ctx: TemplateContext) => boolean;
|
||||
}
|
||||
|
||||
/** Anything the RESOLVERS map accepts — either a bare function or a gated entry. */
|
||||
export type ResolverValue = ResolverFn | ResolverEntry;
|
||||
|
||||
/**
|
||||
* Type-narrowing helper for the gen-skill-docs lookup.
|
||||
* Returns (resolverFn, gate) so callers can do gate?.(ctx) before invoking.
|
||||
*/
|
||||
export function unwrapResolver(entry: ResolverValue): {
|
||||
resolve: ResolverFn;
|
||||
appliesTo?: (ctx: TemplateContext) => boolean;
|
||||
} {
|
||||
if (typeof entry === 'function') return { resolve: entry };
|
||||
return { resolve: entry.resolve, appliesTo: entry.appliesTo };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user