Merge remote-tracking branch 'origin/main' into garrytan/askuserquestion-split-on-overflow

This commit is contained in:
Garry Tan
2026-05-26 22:27:54 -07:00
107 changed files with 10060 additions and 3885 deletions
+14 -3
View File
@@ -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,
+3 -3
View File
@@ -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.
`;
}
+49
View File
@@ -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 };
}