From f14e2f5b194154422eb09432acb0a3bb2380fee1 Mon Sep 17 00:00:00 2001 From: garrytan Date: Tue, 6 Oct 2026 21:06:15 +0000 Subject: [PATCH] fix: the gbrain page save and external-host placeholders pass the free-text rule The gbrain save block passed the whole page through a quoted heredoc inside --content; the agent now writes it to a mktemp file and gbrain put reads it on stdin. The Claude Code outside-voice runtime root and session mode, and the feature slug, join the allowlist with grammars; unused entries are dropped, and a test keeps every allowlisted placeholder tied to a real use. Co-authored-by: dgrant --- benchmark-models/SKILL.md | 2 +- codex/sections/challenge-mode.md | 2 +- codex/sections/consult-mode.md | 2 +- codex/sections/review-mode.md | 2 +- scripts/resolvers/gbrain.ts | 24 +++++++++--------------- ship/sections/pr-body.md | 4 ++-- ship/sections/test-coverage.md | 2 +- test/env-host-fence-conformance.test.ts | 12 +++++++++++- test/helpers/placeholder-allowlist.ts | 9 +++++++-- 9 files changed, 34 insertions(+), 25 deletions(-) diff --git a/benchmark-models/SKILL.md b/benchmark-models/SKILL.md index 7eeb5e596..8491914ec 100644 --- a/benchmark-models/SKILL.md +++ b/benchmark-models/SKILL.md @@ -193,7 +193,7 @@ If B: ask the user for the inline prompt. It never goes into a shell command: cr _GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp" mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; } _EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; } -PROMPT_FILE=$(mktemp "$_GT/benchmark-prompt.XXXXXX") && echo "PROMPT_FILE: $PROMPT_FILE (name: ${PROMPT_FILE##*/})" +PROMPT_FILE=$(mktemp "$_GT/benchmark-prompt.XXXXXX") || { echo "Not sent: cannot create a file in $_GT." >&2; exit 1; }; echo "PROMPT_FILE: $PROMPT_FILE (name: ${PROMPT_FILE##*/})" ``` Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand. diff --git a/codex/sections/challenge-mode.md b/codex/sections/challenge-mode.md index d1609ff18..636567a79 100644 --- a/codex/sections/challenge-mode.md +++ b/codex/sections/challenge-mode.md @@ -25,7 +25,7 @@ Review the changes on this branch against the base branch. Run `git diff origin/ _GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp" mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; } _EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; } -PROMPT_FILE=$(mktemp "$_GT/codex-prompt.XXXXXX") && echo "PROMPT_FILE: $PROMPT_FILE (name: ${PROMPT_FILE##*/})" +PROMPT_FILE=$(mktemp "$_GT/codex-prompt.XXXXXX") || { echo "Not sent: cannot create a file in $_GT." >&2; exit 1; }; echo "PROMPT_FILE: $PROMPT_FILE (name: ${PROMPT_FILE##*/})" ``` Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand. diff --git a/codex/sections/consult-mode.md b/codex/sections/consult-mode.md index 17e1b2739..b03633a1f 100644 --- a/codex/sections/consult-mode.md +++ b/codex/sections/consult-mode.md @@ -68,7 +68,7 @@ For non-plan consult prompts (user typed `/codex `), still prepend the _GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp" mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; } _EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; } -PROMPT_FILE=$(mktemp "$_GT/codex-prompt.XXXXXX") && echo "PROMPT_FILE: $PROMPT_FILE (name: ${PROMPT_FILE##*/})" +PROMPT_FILE=$(mktemp "$_GT/codex-prompt.XXXXXX") || { echo "Not sent: cannot create a file in $_GT." >&2; exit 1; }; echo "PROMPT_FILE: $PROMPT_FILE (name: ${PROMPT_FILE##*/})" ``` Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand. diff --git a/codex/sections/review-mode.md b/codex/sections/review-mode.md index 413410544..86368857b 100644 --- a/codex/sections/review-mode.md +++ b/codex/sections/review-mode.md @@ -89,7 +89,7 @@ after `/codex review ` into it: _GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp" mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; } _EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; } -FOCUS_FILE=$(mktemp "$_GT/codex-focus.XXXXXX") && echo "FOCUS_FILE: $FOCUS_FILE (name: ${FOCUS_FILE##*/})" +FOCUS_FILE=$(mktemp "$_GT/codex-focus.XXXXXX") || { echo "Not sent: cannot create a file in $_GT." >&2; exit 1; }; echo "FOCUS_FILE: $FOCUS_FILE (name: ${FOCUS_FILE##*/})" ``` Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand. diff --git a/scripts/resolvers/gbrain.ts b/scripts/resolvers/gbrain.ts index 48fd52500..c23dd4159 100644 --- a/scripts/resolvers/gbrain.ts +++ b/scripts/resolvers/gbrain.ts @@ -97,25 +97,19 @@ handling): see \`docs/gbrain-write-surfaces.md\` §Save Template.`; **Skip this entire section if \`gbrain\` is not on PATH.** -After completing this skill, save the output: +Save the output: run \`mkdir -p .gstack/tmp && mktemp .gstack/tmp/page.XXXXXX\` at the +repo root, write the page into the printed file with your file-write tool +(frontmatter \`title: "${meta.title}: "\`, \`tags: [${meta.tag}, ]\`, +then the output in markdown), then: \`\`\`bash -gbrain put "${meta.slugPrefix}/" --content "$(cat <<'EOF' ---- -title: "${meta.title}: " -tags: [${meta.tag}, ] ---- - -EOF -)" +gbrain put "${meta.slugPrefix}/" < "" && rm -f "" \`\`\` -Read the saved page back before claiming persistence. Then extract -person/org entities and create stub pages for each one. -Throttle errors (exit 1 with "throttle"/"rate limit"/"busy") and any -other non-zero exit are transient — don't retry inline. Full entity-stub -template, throttle handling, and backlink protocol: -see \`docs/gbrain-write-surfaces.md\` §Save Template.`; +Read the saved page back before claiming persistence. Then create stub pages for +its person/org entities. Any non-zero exit (throttle included) is transient: don't +retry inline. Entity stubs, throttling and backlinks: see +\`docs/gbrain-write-surfaces.md\` §Save Template.`; } // ──────────────────────────────────────────────────────────────────── diff --git a/ship/sections/pr-body.md b/ship/sections/pr-body.md index 9b7ebcf18..6d5d34be7 100644 --- a/ship/sections/pr-body.md +++ b/ship/sections/pr-body.md @@ -120,8 +120,8 @@ Write the body from above into two private files: the first through the _GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp" mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; } _EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; } -BODY_TOP_FILE=$(mktemp "$_GT/pr-body-top.XXXXXX") && echo "BODY_TOP_FILE: $BODY_TOP_FILE (name: ${BODY_TOP_FILE##*/})" -BODY_REST_FILE=$(mktemp "$_GT/pr-body-rest.XXXXXX") && echo "BODY_REST_FILE: $BODY_REST_FILE (name: ${BODY_REST_FILE##*/})" +BODY_TOP_FILE=$(mktemp "$_GT/pr-body-top.XXXXXX") || { echo "Not sent: cannot create a file in $_GT." >&2; exit 1; }; echo "BODY_TOP_FILE: $BODY_TOP_FILE (name: ${BODY_TOP_FILE##*/})" +BODY_REST_FILE=$(mktemp "$_GT/pr-body-rest.XXXXXX") || { echo "Not sent: cannot create a file in $_GT." >&2; exit 1; }; echo "BODY_REST_FILE: $BODY_REST_FILE (name: ${BODY_REST_FILE##*/})" ``` Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand. diff --git a/ship/sections/test-coverage.md b/ship/sections/test-coverage.md index bbce19e9f..4cdfae8b7 100644 --- a/ship/sections/test-coverage.md +++ b/ship/sections/test-coverage.md @@ -343,7 +343,7 @@ After your analysis, output a single JSON object on the LAST LINE of your respon _GT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp" mkdir -p "$_GT" && chmod 700 "$_GT" || { echo "Not sent: cannot create $_GT for the text file." >&2; exit 1; } _EX=$(git rev-parse --git-path info/exclude 2>/dev/null) && mkdir -p "$(dirname "$_EX")" && { grep -qxF '/.gstack/tmp/' "$_EX" 2>/dev/null || echo '/.gstack/tmp/' >> "$_EX"; } -REJECTED_FILE=$(mktemp "$_GT/rejected-tests.XXXXXX") && echo "REJECTED_FILE: $REJECTED_FILE (name: ${REJECTED_FILE##*/})" +REJECTED_FILE=$(mktemp "$_GT/rejected-tests.XXXXXX") || { echo "Not sent: cannot create a file in $_GT." >&2; exit 1; }; echo "REJECTED_FILE: $REJECTED_FILE (name: ${REJECTED_FILE##*/})" ``` Write the text into each printed file with your file-write tool (Claude Code's Write tool needs a Read of the empty file first), exactly as it should appear. The text never goes into a shell command, heredoc or quoted argument. If a write fails or is refused, do not send: print the cause, the file path and the command below for sending by hand. diff --git a/test/env-host-fence-conformance.test.ts b/test/env-host-fence-conformance.test.ts index 7ea59454f..fc10a4798 100644 --- a/test/env-host-fence-conformance.test.ts +++ b/test/env-host-fence-conformance.test.ts @@ -19,7 +19,7 @@ import { runGeneration } from '../scripts/gen-skill-docs'; import { generateMakePdfSetup } from '../scripts/resolvers/make-pdf'; import { binaryAssignment, fencePrelude, insertRuntimePreludes, MAKE_PDF_OVERRIDE, PRELUDE_BYTE_BUDGET, runtimeRootPrelude } from '../scripts/resolvers/runtime-root'; import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types'; -import { lintFence, normalizePlaceholders } from './helpers/generated-bash-lint'; +import { IDENTIFIER_PLACEHOLDERS, lintFence, normalizePlaceholders } from './helpers/generated-bash-lint'; const ROOT = path.resolve(import.meta.dir, '..'); const ENV_HOSTS = ALL_HOST_CONFIGS.filter(h => h.usesEnvVars); @@ -288,6 +288,16 @@ describe('#3046: runtime reference docs that skills execute pass the lint', () = }); }); +describe('CEO-12: the identifier allowlist matches what the skills use', () => { + test('every allowlisted placeholder appears in some rendered fence or runtime doc fence', () => { + const docs = fs.readdirSync(ROOT, { withFileTypes: true }) + .filter(e => e.isDirectory() && fs.existsSync(path.join(ROOT, e.name, 'SKILL.md.tmpl'))) + .flatMap(e => fs.readdirSync(path.join(ROOT, e.name)).filter(f => f.endsWith('.md') && f !== 'SKILL.md').map(f => path.join(ROOT, e.name, f))); + const text = allFences().map(f => f.body).concat(docs.flatMap(d => bashFences(fs.readFileSync(d, 'utf8')).map(f => f.body))).join('\n'); + expect(Object.keys(IDENTIFIER_PLACEHOLDERS).filter(p => !text.includes(p))).toEqual([]); + }); +}); + describe('INV-3: negative controls (planted bad fences fail each check)', () => { test('lint rules', () => { const rules = (body: string) => lintFence(body).map(f => f.rule); diff --git a/test/helpers/placeholder-allowlist.ts b/test/helpers/placeholder-allowlist.ts index 28d30413c..903604eaa 100644 --- a/test/helpers/placeholder-allowlist.ts +++ b/test/helpers/placeholder-allowlist.ts @@ -51,7 +51,6 @@ export const IDENTIFIER_PLACEHOLDERS: Record = { '': { grammar: NUM, what: 'canary check number' }, '': { grammar: NUM, what: 'GitHub Actions run id' }, '': { grammar: NUM, what: 'process id printed by an earlier block' }, - '': { grammar: ID, what: 'Codex session id printed by an earlier call' }, '': { grammar: ID, what: 'freeze owner token printed by acquire' }, '': { grammar: ID, what: 'run id printed by Setup' }, '': { grammar: ID, what: 'run id printed by Setup' }, @@ -96,7 +95,6 @@ export const IDENTIFIER_PLACEHOLDERS: Record = { '': { grammar: ID, what: 'basename of the mktemp reply file printed by the previous block' }, '': { grammar: ID, what: 'basename of the mktemp draft file printed by the free-text block' }, '': { grammar: ID, what: 'basename of the mktemp approach file printed by the free-text block' }, - '': { grammar: ID, what: 'basename of the mktemp text file printed by the previous block' }, '': { grammar: PATH, what: 'mktemp directory printed by the Greptile fetch block' }, '': { grammar: QUOTED, what: 'snapshot tool path printed by autoplan', quoted: true }, '': { grammar: QUOTED, what: 'plan path', quoted: true }, @@ -159,6 +157,12 @@ export const IDENTIFIER_PLACEHOLDERS: Record = { '': { grammar: QUOTED, what: 'test command', quoted: true }, '': { grammar: QUOTED, what: 'test command', quoted: true }, + // External-host renders: Claude Code outside voice and the gbrain page save. + '': { grammar: QUOTED, what: 'installed runtime path', quoted: true }, + '': { grammar: oneOf('fresh', 'resume'), what: 'Claude Code session mode' }, + '': { grammar: ID, what: 'feature slug for the gbrain page' }, + '': { grammar: QUOTED, what: 'mktemp page file path printed by the previous command', quoted: true }, + // A3 group 1 (Aside prompts, design briefs, design approval feedback): mktemp basenames. '': { grammar: ID, what: 'basename of the mktemp prompt file (Aside, Codex, benchmark) printed by the previous block' }, '': { grammar: ID, what: 'basename of the mktemp design brief file printed by the free-text block' }, @@ -176,5 +180,6 @@ export const IDENTIFIER_PLACEHOLDERS: Record = { '': { grammar: /^(?:(?:--(?:incremental|full|dry-run|quiet|no-code|no-memory|no-brain-sync|code-only|dream|no-dream|allow-reclone|prune-gone-worktrees)|--sources [a-z][a-z0-9_,-]*)(?: (?=-)|$))*$/, what: 'gstack-gbrain-sync flags (space-separated; --sources takes one comma-separated type list)', + list: true, }, };