From 2bead6ca1d0bb2771828a166e5f83433f9e98b71 Mon Sep 17 00:00:00 2001 From: sim Date: Fri, 7 Aug 2026 09:30:27 -0400 Subject: [PATCH] feat(#3149): add dedicated init.debug entry point for /gsd:debug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /gsd:debug was one of the last workflows with no cmdInit* of its own: its Step 0 made three separate round-trips (state.load, resolve-model gsd-debugger, config-get workflow.tdd_mode) to assemble one context. Because no debug-scoped fact was computed at any entry point, ADR-1671 admission gate (2) could never be satisfied for debug — an applicability atom naming such a fact would evaluate FALSE forever and silently exclude its section. Adds cmdInitDebug (init.debug), registers it in the init router and the command-alias table, and collapses debug.md Step 0 to one call. Every field resolves through the same primitive the call it replaces used: loadConfig for commit_docs, withProjectRoot for response_language (#2402), planningPaths for debug_dir, resolveModelInternal for debugger_model, and the existing Boolean(workflow.tdd_mode) idiom for tdd_mode. PlanningPaths gains a debug field so state.load and init.debug share ONE debug-directory expression rather than two kept in sync by hand. state.load keeps emitting debug_dir: it is a shipped query surface with its own test anchor, so narrowing it would break unseen consumers for no gain. No WHEN_VOCABULARY atom and no gsd:section marker: gate (1), a consuming section of at least 400 bytes, belongs to the change that adds the section. Closes #3149 Co-Authored-By: Claude Opus 5 --- .changeset/gallant-geese-wave.md | 5 ++ docs/reference/workflow-fragments.md | 11 +++ gsd-core/workflows/debug.md | 21 ++--- scripts/lint-test-file-count.allowlist.json | 9 ++ src/command-aliases.cts | 8 ++ src/init-command-router.cts | 5 ++ src/init.cts | 63 ++++++++++++++ src/planning-workspace.cts | 5 ++ src/state.cts | 10 ++- .../3149-init-debug-entry-point.json | 6 ++ tests/init-debug-workflow-contract.test.cjs | 87 +++++++++++++++++++ 11 files changed, 214 insertions(+), 16 deletions(-) create mode 100644 .changeset/gallant-geese-wave.md create mode 100644 tests/emitted-drift-acks/3149-init-debug-entry-point.json create mode 100644 tests/init-debug-workflow-contract.test.cjs diff --git a/.changeset/gallant-geese-wave.md b/.changeset/gallant-geese-wave.md new file mode 100644 index 000000000..10bfbd4d7 --- /dev/null +++ b/.changeset/gallant-geese-wave.md @@ -0,0 +1,5 @@ +--- +type: Changed +pr: 0 +--- +**`/gsd:debug` now initializes in one round-trip instead of three** — the workflow previously made three separate `gsd-tools` calls to assemble its context (`state.load`, `resolve-model`, and `config-get workflow.tdd_mode`); it now makes a single `init.debug` call carrying the same resolved values. (#3149) diff --git a/docs/reference/workflow-fragments.md b/docs/reference/workflow-fragments.md index 3e570754a..c4f4df3ab 100644 --- a/docs/reference/workflow-fragments.md +++ b/docs/reference/workflow-fragments.md @@ -159,6 +159,17 @@ own dedicated `cmdInit*` entry points (`cmdInitReview`, (`cmdInitDocsUpdate`, `cmdInitUpdate`, `cmdInitTransition`) plus an extension of the pre-existing `cmdInitNewMilestone`. +An entry point can also land **ahead of** the atom it will unblock. `#3149` +gives `debug` a dedicated `cmdInitDebug` (`init.debug`) with no vocabulary +change at all: `/gsd:debug` previously made three separate `gsd_run` +round-trips and had no `cmdInit*` of its own, so gate (2) could never be +satisfied for any debug-scoped fact. Shipping the entry point first satisfies +gate (2) on its own schedule and leaves gate (1) — a consuming section of at +least 400 bytes — to the change that actually adds the section. `debug` has no +`` markers yet, so it contributes no key to +`section-manifest.json` and `init.debug`'s `section_manifest` field degrades to +`null` (read everything) until it does. + ### Compound conditions are resolved in the fact, never the grammar `state:chunked-mode` looks, at the section-body level, like it should be a diff --git a/gsd-core/workflows/debug.md b/gsd-core/workflows/debug.md index d84d8866b..2fb1effaf 100644 --- a/gsd-core/workflows/debug.md +++ b/gsd-core/workflows/debug.md @@ -17,24 +17,21 @@ Valid GSD subagent types (use exact names — do not fall back to 'general-purpo ```bash _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi -INIT=$(gsd_run query state.load) +INIT=$(gsd_run query init.debug) if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` -Extract `commit_docs` and `config.response_language` from init JSON. Extract `debug_dir` from init JSON — an absolute path anchored on `project_root` (#2376: `debug_file_path` values handed to the spawned `gsd-debug-session-manager` must resolve regardless of that subagent's own cwd, which may differ from the orchestrator's — build them as `{debug_dir}/{slug}.md`, never a bare `.planning/debug/...` literal). +One round-trip carries everything this workflow needs (#3149 — this call replaces the former `state.load` + `resolve-model` + `config-get` trio). Extract from init JSON: + +- `commit_docs` — whether planning docs are committed. +- `response_language` — TOP-LEVEL field, present ONLY when configured. Absent means English; absence is not a degraded read. +- `debug_dir` — an absolute path anchored on `project_root` (#2376: `debug_file_path` values handed to the spawned `gsd-debug-session-manager` must resolve regardless of that subagent's own cwd, which may differ from the orchestrator's — build them as `{debug_dir}/{slug}.md`, never a bare `.planning/debug/...` literal). +- `debugger_model` — the resolved model for `gsd-debugger` spawns; used as `{debugger_model}` below and governed by the model-omission rule in step 2. +- `tdd_mode` — used as `{TDD_MODE}` in the session parameter blocks below. +- `section_manifest` — `null` today, because this workflow carries no `` markers. **When it is `null`, read this workflow in full.** When it is present, read only the files named in its `read` array. `null` and an empty `included` array are NOT the same: `null` means "no manifest for this workflow", an empty `included` means "nothing applies". **If `response_language` is set:** All user-facing questions, prompts, and explanations in this workflow MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. -Resolve debugger model: -```bash -debugger_model=$(gsd_run query resolve-model gsd-debugger --pick model 2>/dev/null || true) -``` - -Read TDD mode from config: -```bash -TDD_MODE=$(gsd_run query config-get workflow.tdd_mode --raw 2>/dev/null || echo "false") -``` - ## 1a. LIST subcommand When SUBCMD=list: diff --git a/scripts/lint-test-file-count.allowlist.json b/scripts/lint-test-file-count.allowlist.json index 15c2ede9d..358e4a13f 100644 --- a/scripts/lint-test-file-count.allowlist.json +++ b/scripts/lint-test-file-count.allowlist.json @@ -22,6 +22,15 @@ ], "issue": "622" }, + "init": { + "files": [ + "init-debug-workflow-contract.test.cjs", + "init-debug.test.cjs", + "init-manager.test.cjs", + "init.test.cjs" + ], + "issue": "3149" + }, "milestone": { "files": [ "milestone-archive.test.cjs", diff --git a/src/command-aliases.cts b/src/command-aliases.cts index d95785ec9..4babab2bc 100644 --- a/src/command-aliases.cts +++ b/src/command-aliases.cts @@ -448,6 +448,14 @@ export const INIT_COMMAND_ALIASES: CommandAlias[] = [ "subcommand": "transition", "mutation": false }, + { + "canonical": "init.debug", + "aliases": [ + "init debug" + ], + "subcommand": "debug", + "mutation": false + }, { "canonical": "init.new-workspace", "aliases": [ diff --git a/src/init-command-router.cts b/src/init-command-router.cts index 03f578266..2aee51368 100644 --- a/src/init-command-router.cts +++ b/src/init-command-router.cts @@ -47,6 +47,7 @@ interface InitModule { cmdInitDocsUpdate(cwd: string, raw: boolean, options?: Record): void; cmdInitUpdate(cwd: string, raw: boolean, options?: Record): void; cmdInitTransition(cwd: string, raw: boolean, options?: Record): void; + cmdInitDebug(cwd: string, raw: boolean, options?: Record): void; cmdInitNewWorkspace(cwd: string, raw: boolean): void; cmdInitListWorkspaces(cwd: string, raw: boolean): void; cmdInitRemoveWorkspace(cwd: string, name: string | undefined, raw: boolean): void; @@ -171,6 +172,10 @@ function routeInitCommand({ init, args, cwd, raw, error }: RouteInitCommandOptio init.cmdInitUpdate(cwd, raw, { next: namedArgs['next'], rc: namedArgs['rc'] }); }, transition: () => init.cmdInitTransition(cwd, raw, {}), + debug: () => { + const namedArgs = parseNamedArgs(args, [], ['diagnose']); + init.cmdInitDebug(cwd, raw, { diagnose: namedArgs['diagnose'] }); + }, 'new-workspace': () => init.cmdInitNewWorkspace(cwd, raw), 'list-workspaces': () => init.cmdInitListWorkspaces(cwd, raw), 'remove-workspace': () => init.cmdInitRemoveWorkspace(cwd, args[2], raw), diff --git a/src/init.cts b/src/init.cts index b642fb690..bd3f45fe0 100644 --- a/src/init.cts +++ b/src/init.cts @@ -2690,6 +2690,68 @@ function cmdInitTransition(cwd: string, raw: boolean, options: Record = {}): void { + const config = loadConfig(cwd); + const wf = (config.workflow ?? {}) as Record; + + const result: Record = { + commit_docs: config.commit_docs, + // #2376: absolute — debug.md builds `debug_file_path` as + // `{debug_dir}/{slug}.md` for its gsd-debug-session-manager spawns, whose + // own cwd may differ from the orchestrator's. + debug_dir: toPosixPath(planningPaths(cwd).debug), + debugger_model: resolveModelInternal(cwd, 'gsd-debugger'), + tdd_mode: Boolean(wf['tdd_mode']), + diagnose: options['diagnose'] === true, + }; + + // Additive, optional field — degrades to null while `debug` has no key in + // `gsd-core/workflows/section-manifest.json` (it has no `gsd:section` markers + // until #3128). null means "read everything", which is NOT the same as a + // computed empty selection. + result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'debug', {}); + + output(withProjectRoot(cwd, result), raw); +} + function cmdInitProgress(cwd: string, raw: boolean, options: Record = {}): void { try { (pruneOrphanedWorktrees as (cwd: string) => void)(cwd); @@ -3708,6 +3770,7 @@ export = { cmdInitDocsUpdate, cmdInitUpdate, cmdInitTransition, + cmdInitDebug, cmdInitNewWorkspace, cmdInitListWorkspaces, cmdInitRemoveWorkspace, diff --git a/src/planning-workspace.cts b/src/planning-workspace.cts index b64ab47fd..ef3502883 100644 --- a/src/planning-workspace.cts +++ b/src/planning-workspace.cts @@ -166,6 +166,7 @@ interface PlanningPaths { config: string; phases: string; requirements: string; + debug: string; } function planningPaths(cwd: string, ws?: string | null): PlanningPaths { @@ -178,6 +179,10 @@ function planningPaths(cwd: string, ws?: string | null): PlanningPaths { config: path.join(base, 'config.json'), phases: path.join(base, 'phases'), requirements: path.join(base, 'REQUIREMENTS.md'), + // #3149: the debug-session directory. Single source for both `state.load`'s + // `debug_dir` field and `init.debug`'s — previously each composed its own + // `path.join(planning, 'debug')` (DEFECT.GENERATIVE-FIX). + debug: path.join(base, 'debug'), }; } diff --git a/src/state.cts b/src/state.cts index b8e84bb3d..92509a4aa 100644 --- a/src/state.cts +++ b/src/state.cts @@ -351,10 +351,12 @@ function cmdStateLoad(cwd: string, raw: boolean): void { config_exists: configExists, // #2376: absolute (anchored on cwd), not orchestrator-cwd-relative — a // spawned subagent's own cwd may differ from the orchestrator's. - // debug.md has no init.* call of its own; it reads this field from - // `state load` to build debug_file_path for its gsd-debug-session-manager - // spawns instead of hardcoding '.planning/debug/{slug}.md'. - debug_dir: toPosixPath(path.join(planDir, 'debug')), + // #3149: debug.md now has its own `init.debug` entry point and reads this + // field from there, not from `state load`. This stays on the state.load + // bundle regardless: it is a shipped query surface with its own test anchor + // (tests/state.test.cjs), so narrowing it would break unseen consumers for + // no gain (Hyrum's Law). Both emit the SAME `planningPaths(cwd).debug`. + debug_dir: toPosixPath(planningPaths(cwd).debug), }; // For --raw, output a condensed key=value format diff --git a/tests/emitted-drift-acks/3149-init-debug-entry-point.json b/tests/emitted-drift-acks/3149-init-debug-entry-point.json new file mode 100644 index 000000000..46a939cab --- /dev/null +++ b/tests/emitted-drift-acks/3149-init-debug-entry-point.json @@ -0,0 +1,6 @@ +{ + "version": 1, + "paths": { + "debug.md": "#3149 (prerequisite for #3128, ADR-1671 admission gate 2): debug.md gains a dedicated `init.debug` entry point (cmdInitDebug) and its Step 0 collapses THREE separate `gsd_run` round-trips into one. Removed: `gsd_run query state.load` (line 20, replaced in place), the `resolve-model gsd-debugger --pick model` block, and the `config-get workflow.tdd_mode --raw` block — 2 prose lead-ins and 2 fenced code blocks in total. Added: a 6-bullet extraction list documenting the bundle's fields (`commit_docs`, the now TOP-LEVEL `response_language`, `debug_dir`, `debugger_model`, `tdd_mode`, `section_manifest`) plus the `section_manifest: null` -> read-everything rule and the null-vs-empty-included distinction. Net SOURCE growth is +607 bytes (20,555 -> 21,162): the bullets that document one bundle cost more bytes than the two shell round-trips they replace, which is the intended trade — the round-trips cost three subprocess spawns at RUN time on every /gsd:debug invocation. No `` marker is added and WHEN_VOCABULARY is unchanged at 29, so the composeWorkflow emission path is byte-identical in shape to before; only this file's own content moved. The `{TDD_MODE}` and `{debugger_model}` placeholders in the session-parameter blocks (lines ~137-145, ~226-234) are deliberately left byte-identical — they now resolve from the init bundle instead of shell variables, and rewording them would ripple into tests/fix-2257-debug-nonterminal-resume.test.cjs and tests/debug-session-manager-commit.test.cjs for no behavioral gain." + } +} diff --git a/tests/init-debug-workflow-contract.test.cjs b/tests/init-debug-workflow-contract.test.cjs new file mode 100644 index 000000000..4436deead --- /dev/null +++ b/tests/init-debug-workflow-contract.test.cjs @@ -0,0 +1,87 @@ +// allow-test-rule: source-text-is-the-product (see #3149) +// gsd-core/workflows/debug.md is shipped prompt content: the text IS what the +// runtime loads, so its Step 0 contract can only be asserted against the text. +// The behavioral half of this change lives in tests/init-debug.test.cjs, which +// carries no exemption. + +'use strict'; + +/** + * `debug.md` Step 0 contract after the `init.debug` consolidation (#3149). + * + * Matrix: `.gsd/phase/feat-3149-cmdinitdebug/50-test-matrix.md` group F. + * + * Guards the four ways this consolidation can silently regress: + * F1/F2 — a replaced round-trip creeping back, or the new one being lost. + * F3 — reading `config.response_language` (the nested state.load shape) + * instead of the flat top-level field withProjectRoot injects. This + * is the #2402 defect class: the workflow silently stays English. + * F4 — losing the `@file:` unwrap, which large init payloads still need. + * F5 — dropping the `section_manifest: null` -> read-everything rule, + * without which a null manifest reads as "read nothing". + */ + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const WORKFLOW_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'debug.md'); +const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + +/** Every `gsd_run query ` invocation in the workflow, in document order. */ +function queryInvocations(text) { + return [...text.matchAll(/gsd_run query ([\w.-]+)/g)].map((m) => m[1]); +} + +describe('debug.md Step 0 init contract (#3149, matrix §F)', () => { + test('calls init.debug exactly once (row F1)', () => { + const initDebugCalls = queryInvocations(workflow).filter((q) => q === 'init.debug'); + assert.equal(initDebugCalls.length, 1, 'exactly one init.debug round-trip'); + }); + + test('no longer makes the three replaced calls (row F2)', () => { + const queries = queryInvocations(workflow); + + assert.equal(queries.includes('state.load'), false, 'state.load is replaced by init.debug'); + assert.equal( + workflow.includes('resolve-model gsd-debugger'), + false, + 'debugger_model now rides the init bundle' + ); + assert.equal( + workflow.includes('config-get workflow.tdd_mode'), + false, + 'tdd_mode now rides the init bundle' + ); + }); + + test('reads the flat response_language, not the nested state.load shape (row F3)', () => { + assert.equal( + workflow.includes('config.response_language'), + false, + 'withProjectRoot injects response_language at the TOP level; reading config.response_language ' + + 'against an init bundle resolves undefined and silently drops translated output (#2402)' + ); + assert.ok( + workflow.includes('`response_language`'), + 'the field is still documented, just at its new location' + ); + }); + + test('still unwraps an @file: payload (row F4)', () => { + assert.ok( + workflow.includes('@file:'), + 'init payloads can spill to a file; dropping the unwrap leaves INIT holding a path, not JSON' + ); + }); + + test('documents the null-manifest read-everything fallback (row F5)', () => { + assert.ok(workflow.includes('section_manifest'), 'the field is documented'); + assert.ok( + /`null`[^\r\n]*read this workflow in full/i.test(workflow), + 'a null section_manifest must be documented as "read everything" — without the rule, ' + + 'a null manifest reads as an empty selection and the workflow reads nothing' + ); + }); +});