diff --git a/gsd-core/workflows/onboard.md b/gsd-core/workflows/onboard.md index 625372e3b..dbcd7c147 100644 --- a/gsd-core/workflows/onboard.md +++ b/gsd-core/workflows/onboard.md @@ -33,7 +33,7 @@ Parse JSON fields from `INIT`: - `commit_docs`, `text_mode`, `has_git`, `git_worktree_root`, `in_nested_subdir` Set: -- `TEXT_MODE=true` if `--text` is present or `text_mode` is true. +- `TEXT_MODE=true` if `--text` is present or `text_mode` is true. When `TEXT_MODE` is active, replace every `AskUserQuestion` call below with a plain-text numbered list and ask the user to type their choice number — required for non-Claude runtimes (OpenAI Codex, Gemini CLI, etc.) where `AskUserQuestion` is not available. - `ONBOARDING_ROOT={git_worktree_root || _GSD_RUNTIME_ROOT}`. If `has_git` and `in_nested_subdir` are true, warn that onboarding artifacts belong to the outer worktree at `git_worktree_root`. Do not run `git init`. diff --git a/tests/runtime-launcher-parity.test.cjs b/tests/runtime-launcher-parity.test.cjs index 8fada64b1..2f2448b7c 100644 --- a/tests/runtime-launcher-parity.test.cjs +++ b/tests/runtime-launcher-parity.test.cjs @@ -136,6 +136,22 @@ function collectAgentFiles() { return results; } +/** + * A workflow/agent file "delegates to the shared resolver" when it pulls the + * canonical gsd_run preamble in from gsd-core/references/gsd-run-resolver.md via + * an @-include instead of inlining the snippet (see onboard.md / issue #1990). + * + * Such files are exempt from the inline-preamble parity checks (B / G / H and the + * runtime-home propagation checks): they intentionally do NOT inline the preamble + * — onboard-command.test.cjs even asserts the absence of the inline form. Their + * resolver correctness is guaranteed transitively by: + * (1) onboard-command.test.cjs asserting the @-include is present, and + * (2) the "resolver reference stays byte-equal to the snippet" guard (B2) below. + */ +function delegatesToResolverReference(content) { + return content.includes('references/gsd-run-resolver.md'); +} + describe('runtime-launcher-parity (#373)', () => { // ─── (A) No retired GSD_SDK token ──────────────────────────────────────── test('(A) no GSD_SDK token in any workflow .md file', () => { @@ -170,6 +186,9 @@ describe('runtime-launcher-parity (#373)', () => { for (const f of files) { const rel = path.relative(WORKFLOWS_DIR, f); const content = fs.readFileSync(f, 'utf8'); + // Files that delegate to the shared resolver reference (@-include) do not + // inline the preamble — exempt them (see delegatesToResolverReference / (B2)). + if (delegatesToResolverReference(content)) continue; const blocks = extractShellBlocks(content); // Collect all block lines in document order for flat analysis @@ -226,6 +245,30 @@ describe('runtime-launcher-parity (#373)', () => { ); }); + // ─── (B2) Shared resolver reference stays byte-equal to the snippet ─────── + // Workflows may delegate to gsd-core/references/gsd-run-resolver.md instead of + // inlining the preamble (see delegatesToResolverReference). That delegation is + // only safe if the reference's bash block is byte-equal to the canonical + // snippet — otherwise a delegating workflow (e.g. onboard.md) would silently + // ship a drifted resolver. This guard replaces the inline-preamble checks for + // those files. + test('(B2) references/gsd-run-resolver.md preamble is byte-equal to the canonical snippet', () => { + const preambleStr = expectedPreamble().join('\n'); + const refPath = path.join(__dirname, '..', 'gsd-core', 'references', 'gsd-run-resolver.md'); + const refContent = fs.readFileSync(refPath, 'utf8'); + const refPreamble = extractShellBlocks(refContent) + .map((b) => b.lines.join('\n')) + .join('\n') + .trim(); + assert.equal( + refPreamble, + preambleStr, + 'gsd-core/references/gsd-run-resolver.md must contain the canonical gsd_run preamble ' + + 'byte-equal to _runtime-launcher.snippet.sh. Re-copy the snippet into the reference so ' + + 'workflows that delegate to it via @-include ship the current resolver.', + ); + }); + // ─── (C) Space-safe behavioral test ────────────────────────────────────── test('(C) gsd_run works with a RUNTIME_DIR path containing spaces', () => { // Create temp dir whose path contains a space @@ -402,6 +445,7 @@ describe('runtime-launcher-parity (#373)', () => { const content = fs.readFileSync(f, 'utf8'); const blocks = extractShellBlocks(content); const allBlockLines = blocks.flatMap((b) => b.lines); + if (delegatesToResolverReference(content)) continue; const fileHasGsdRun = allBlockLines.some((l) => /\bgsd_run\b/.test(l)); if (!fileHasGsdRun) continue; const allContent = allBlockLines.join('\n'); @@ -433,6 +477,7 @@ describe('runtime-launcher-parity (#373)', () => { const content = fs.readFileSync(f, 'utf8'); const blocks = extractShellBlocks(content); const allBlockLines = blocks.flatMap((b) => b.lines); + if (delegatesToResolverReference(content)) continue; const fileHasGsdRun = allBlockLines.some((l) => /\bgsd_run\b/.test(l)); if (!fileHasGsdRun) continue; if (!allBlockLines.join('\n').includes(CODEX_HOME_PROBE)) { @@ -1346,6 +1391,7 @@ describe('bug-891: non-Claude runtime home fallback arms', () => { const content = fs.readFileSync(f, 'utf8'); const blocks = extractShellBlocks(content); const allBlockLines = blocks.flatMap((b) => b.lines); + if (delegatesToResolverReference(content)) continue; const fileHasGsdRun = allBlockLines.some((l) => /\bgsd_run\b/.test(l)); if (!fileHasGsdRun) continue; const allContent = allBlockLines.join('\n');