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 <dgrant@users.noreply.github.com>
This commit is contained in:
garrytananddgrant committed 2026-10-06 21:06:15 +00:00
1 parent 9613ff8f17
commit f14e2f5b19
9 files changed
+34 -25

No files matched your search

+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -68,7 +68,7 @@ For non-plan consult prompts (user typed `/codex <question>`), 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.
+1 -1
View File
@@ -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.
+9 -15
View File
@@ -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}: <feature name>"\`, \`tags: [${meta.tag}, <feature-slug>]\`,
then the output in markdown), then:
\`\`\`bash
gbrain put "${meta.slugPrefix}/<feature-slug>" --content "$(cat <<'EOF'
---
title: "${meta.title}: <feature name>"
tags: [${meta.tag}, <feature-slug>]
---
<skill output in markdown>
EOF
)"
gbrain put "${meta.slugPrefix}/<feature-slug>" < "<page-file>" && rm -f "<page-file>"
\`\`\`
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.`;
}
// ────────────────────────────────────────────────────────────────────
+2 -2
View File
@@ -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.
+1 -1
View File
@@ -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.
+11 -1
View File
@@ -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);
+7 -2
View File
@@ -51,7 +51,6 @@ export const IDENTIFIER_PLACEHOLDERS: Record<string, IdentifierGrammar> = {
'<check-number>': { grammar: NUM, what: 'canary check number' },
'<run-id>': { grammar: NUM, what: 'GitHub Actions run id' },
'<PID>': { grammar: NUM, what: 'process id printed by an earlier block' },
'<session-id>': { grammar: ID, what: 'Codex session id printed by an earlier call' },
'<retained-owner-token>': { grammar: ID, what: 'freeze owner token printed by acquire' },
'<RUN_ID>': { grammar: ID, what: 'run id printed by Setup' },
'<RUN_ID from Setup>': { grammar: ID, what: 'run id printed by Setup' },
@@ -96,7 +95,6 @@ export const IDENTIFIER_PLACEHOLDERS: Record<string, IdentifierGrammar> = {
'<reply-file-name>': { grammar: ID, what: 'basename of the mktemp reply file printed by the previous block' },
'<redact-file-name>': { grammar: ID, what: 'basename of the mktemp draft file printed by the free-text block' },
'<approach-file-name>': { grammar: ID, what: 'basename of the mktemp approach file printed by the free-text block' },
'<text-file-name>': { grammar: ID, what: 'basename of the mktemp text file printed by the previous block' },
'<greptile-dir>': { grammar: PATH, what: 'mktemp directory printed by the Greptile fetch block' },
'<SNAPSHOT_TOOL>': { grammar: QUOTED, what: 'snapshot tool path printed by autoplan', quoted: true },
'<SOURCE_PLAN>': { grammar: QUOTED, what: 'plan path', quoted: true },
@@ -159,6 +157,12 @@ export const IDENTIFIER_PLACEHOLDERS: Record<string, IdentifierGrammar> = {
'<tests>': { grammar: QUOTED, what: 'test command', quoted: true },
'<vitest>': { grammar: QUOTED, what: 'test command', quoted: true },
// External-host renders: Claude Code outside voice and the gbrain page save.
'<gstack-runtime-root>': { grammar: QUOTED, what: 'installed runtime path', quoted: true },
'<fresh-or-resume>': { grammar: oneOf('fresh', 'resume'), what: 'Claude Code session mode' },
'<feature-slug>': { grammar: ID, what: 'feature slug for the gbrain page' },
'<page-file>': { 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.
'<prompt-file-name>': { grammar: ID, what: 'basename of the mktemp prompt file (Aside, Codex, benchmark) printed by the previous block' },
'<brief-file-name>': { 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<string, IdentifierGrammar> = {
'<user-args>': {
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,
},
};