diff --git a/.changeset/daring-lynx-hum.md b/.changeset/daring-lynx-hum.md new file mode 100644 index 000000000..2ad35ea71 --- /dev/null +++ b/.changeset/daring-lynx-hum.md @@ -0,0 +1,5 @@ +--- +type: Changed +pr: 2558 +--- +**Every workflow now carries response-language coverage, and every directive names inter-tool narration** — previously uncovered workflows (including `/gsd-review` and lazy-loaded mode/step files) now apply a shared or inline directive, and the 44 workflows whose directive covered only "questions, prompts, and explanations" now name narration between tool calls, status updates, progress notes, and findings, so running commentary no longer stays in English beside translated answers. A CI lint (`lint:response-language`) prevents future workflows from shipping uncovered or with the weaker wording. (#2529) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5de478395..bd8e00ed8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1292,6 +1292,7 @@ The following checks run on every PR in addition to the test suite: | `Lint — ESLint` | No source-grep tests (see above), via the `local/no-source-grep` rule | Replace with `runGsdTools()` behavioral tests, or add `// allow-test-rule: ` | | `Lint — cross-platform portability` | Windows-portability defects in tests, via `local/no-path-literal-in-assert` (more rules land per [ADR-1703](docs/adr/1703-portability-enforcement-architecture.md)) — e.g. a path-returning call asserted against a hardcoded `/`-literal | Normalize the actual: `String(pathFn(...)).replace(/\\/g, '/')`, or structure platform-specific code behind a `process.platform !== 'win32'` guard. **No `eslint-disable`** — see [cross-platform-portability-rules.md](docs/contributing/cross-platform-portability-rules.md) | | `lint-docs-guard-registration.cjs` (via `npm run lint:ci`) | A test that reads shipped `docs/` content must be registered so it runs on the PR that changes those docs — otherwise it can only fail after merge | Register it in `scripts/docs-guard-registry.cjs`, mapping the test to the docs paths it reads, or mark it `// docs-guard-exempt: ` and list it in `scripts/lint-docs-guard-registration.exempt-baseline.cjs` — see [docs-guard-registration.md](docs/contributing/docs-guard-registration.md) | +| `lint-response-language-coverage.cjs` (via `npm run lint:ci`) | Every workflow file instructs the model to honour `response_language` in user-facing prose, and the directive names inter-tool narration rather than questions alone — a directive that omits the narration class leaves running commentary in English beside translated answers (#2529) | Give the file one of the four coverage forms: the eager `@`-reference, its own inline directive, the pinned line, or proven inheritance from the parent that dispatches it — see [response-language-coverage.md](docs/contributing/response-language-coverage.md) | Run locally before pushing: `npm run lint` (or `npx eslint .`) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 00d348b26..608ac63c1 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -189,7 +189,7 @@ project one is reported, since that is the file you are most likely able to fix. | `dynamic_routing.provider_escalation` | string[] | ordered model IDs | (none) | Opt-in fallback providers tried when a run dies on a quota / rate limit — see [provider escalation](#provider-escalation-on-quota-exceeded--added-in-v143). Added in v1.43 ([#2296](https://github.com/open-gsd/gsd-core/issues/2296)) | | `project_code` | string | any short string | (none) | Prefix for phase directory names (e.g., `"ABC"` produces `ABC-01-setup/`). Added in v1.31 | | `phase_id_convention` | enum | `"milestone-prefixed"`, `"bracket"`, `null` | `null` | Phase ID naming convention. `null` = legacy numeric IDs (`Phase 1`, `Phase 2`). `"milestone-prefixed"` = globally unique IDs that encode the enclosing milestone (`Phase 1-01`, `Phase 1-02`). Run `gsd-tools roadmap upgrade --convention milestone-prefixed` to migrate an existing ROADMAP.md. `"bracket"` = IDs that carry the milestone in a bracket ahead of the phase number — heading `### [GSD.02] 05: Name`, directory `GSD.02-05-name` — per [ADR-612](adr/612-bracket-phase-id-convention.md). **`"bracket"` currently affects the READ path only:** `roadmap analyze` / `roadmap get-phase`, the W005/W006/W007 phase checks, `validate health` (including an advisory W021 — a bracket phase's milestone disagreeing with its enclosing section, or a phase heading still spelled in legacy form that has not yet been migrated to bracket form), and both `total_phases` derivations recognise the bracket spelling once it is set. There is no bracket migrator and no bracket emit yet, so set it only on a project whose ROADMAP.md already uses that spelling; a project on any other value compiles the same patterns it did before and is unaffected. **What opting in costs:** on a bracket repo a heading whose bracket is followed directly by a digit is read as a phase heading, so shapes that are legal prose headings on any other convention — `### [RFC.2119] 5:`, `### [v1.0] 2024:`, `### [ADR.612] 3:` — are claimed as phases and will move `phase_count`, `total_phases` and W006. A bracket repo cedes that heading shape; that is the trade the opt-in buys, and it is why the widened read is selected at construction time from this value rather than applied everywhere ([#2761](https://github.com/open-gsd/gsd-core/issues/2761)). | -| `response_language` | string | language code | (none) | Language for agent responses (e.g., `"pt"`, `"ko"`, `"ja"`). Propagates to all spawned agents for cross-phase language consistency. Added in v1.32. UAT checkpoint frames (`/gsd-verify-work`) render a localized banner/instruction for English, Spanish, French, German, Portuguese, Japanese, Chinese, Korean, Italian, Dutch, Polish, Russian, Ukrainian, Turkish, Hindi, Arabic, Vietnamese, and Indonesian (endonyms and ISO codes also accepted); any other value falls back to the English frame. One deliberate exception: the `spec-phase` edge-completeness probe is fed an English translation of each requirement's text, because its shape cues are English-only — the SPEC itself stays in this language. See [Spec-Phase Edge-Completeness Probe](FEATURES.md#144-spec-phase-edge-completeness-probe). | +| `response_language` | string | language code | (none) | Language for agent responses (e.g., `"pt"`, `"ko"`, `"ja"`). Propagates to all spawned agents for cross-phase language consistency. Added in v1.32. UAT checkpoint frames (`/gsd-verify-work`) render a localized banner/instruction for English, Spanish, French, German, Portuguese, Japanese, Chinese, Korean, Italian, Dutch, Polish, Russian, Ukrainian, Turkish, Hindi, Arabic, Vietnamese, and Indonesian (endonyms and ISO codes also accepted); any other value falls back to the English frame. One deliberate exception: the `spec-phase` edge-completeness probe is fed an English translation of each requirement's text, because its shape cues are English-only — the SPEC itself stays in this language. See [Spec-Phase Edge-Completeness Probe](FEATURES.md#144-spec-phase-edge-completeness-probe). Every workflow is required to carry a directive honouring this setting, including for inter-tool narration; authors add or fix one per [response-language coverage](contributing/response-language-coverage.md), and `npm run lint:response-language` enforces it. | | `context_window` | number | any integer | `200000` | Context window size in tokens. Set `1000000` for 1M-context models (e.g., `claude-fable-5`). Values `>= 500000` enable adaptive context enrichment (full-body reads of prior SUMMARY.md, deeper anti-pattern reads). Configured via `/gsd-config --advanced`. | | `context_profile` | string | `dev`, `research`, `review` | (none) | Execution context preset that applies a pre-configured bundle of mode, model, and workflow settings for the current type of work. Added in v1.34 | | `claude_md_path` | string | any file path | `./.claude/CLAUDE.md` | Custom output path for the generated CLAUDE.md file. Useful for monorepos or projects that need CLAUDE.md in a non-root location. Defaults to `./.claude/CLAUDE.md` — a valid project-scoped memory location that keeps GSD-generated content from polluting a hand-crafted repo-root `CLAUDE.md` ([#1098](https://github.com/open-gsd/gsd-core/issues/1098)). An existing file without GSD markers is never overwritten unless `--force` is passed. Default changed from `./CLAUDE.md` in v1.5. Added in v1.36 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index dd0cc5d90..01c37004d 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -2108,6 +2108,8 @@ Test suite that scans all agent, workflow, and command files for embedded inject **Requirements:** - REQ-LANG-01: System MUST respect `response_language` setting across all phases and agents - REQ-LANG-02: Setting MUST propagate to all spawned agents for consistent language output +- REQ-LANG-03: Every workflow MUST carry response-language coverage — through an exact inline directive, a shared `@`-referenced directive (`gsd-core/references/response-language-directive.md`), or inheritance from the parent workflow that dispatches it; enforced in CI by `scripts/lint-response-language-coverage.cjs` (#2529) +- REQ-LANG-04: A covering directive MUST name inter-tool narration, not only the question/prompt surface. A directive names it by using the word "narration" or the phrase "between tool calls"; the class it denotes is the model's running commentary between tool calls, status updates, progress notes and findings included, and enumerating those items without naming the class does not satisfy the rule. A directive worded around questions and prompts alone leaves the model's running commentary in English beside translated answers, which is the defect #2529 reports; `scripts/lint-response-language-coverage.cjs` rejects it (#2529) **Config:** | Setting | Type | Default | Description | diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index c0db5d56e..283420f50 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -279,6 +279,7 @@ "research-documentation-lookup.md", "research-philosophy.md", "research-verification-protocol.md", + "response-language-directive.md", "reviewer-instances.md", "revision-loop.md", "runtime-aware-dispatch.md", diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index 0dc618e5e..d07b1ee42 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -351,6 +351,7 @@ Full roster at `gsd-core/references/*.md`. References are shared knowledge docum | `execute-phase-between-wave-reset.md` | Between-wave manifest reset and worktree base refresh for waves 2+, plus the pre-wave cross-plan key-links dependency check (#1369). | | `execute-phase-wave-guard.md` | Inter-wave worktree base re-check for wave N+1 — the harness caches the fork base, so a fresh worktree would otherwise be cut from the stale pre-wave base (#1369, #2652). | | `offer-next.md` | The `offer_next` step body extracted from `execute-phase.md` — auto-advance routing and the no-transition check (#2537). | +| `response-language-directive.md` | Shared response-language directive for workflow output, inter-tool narration, and translated report-template prose (#2529). | | `continuation-format.md` | Session continuation/resume format. | | `domain-probes.md` | Domain-specific probing questions for discuss-phase. | | `edge-probe.md` | Spec-phase edge-completeness probe — 8-category edge taxonomy, shape classification, and the `requirements → checks → verifier` resolution model (Step 5.5). | diff --git a/docs/contributing/response-language-coverage.md b/docs/contributing/response-language-coverage.md new file mode 100644 index 000000000..d281dc300 --- /dev/null +++ b/docs/contributing/response-language-coverage.md @@ -0,0 +1,89 @@ +# Response-language coverage + +Every workflow file must instruct the model to honour `response_language` in the prose a user +reads. `npm run lint:response-language` (chained into `npm run lint:ci`) enforces it across the +whole catalog. This page is the how-to and the rationale; the mechanics live in +[`scripts/lint-response-language-coverage.cjs`](../../scripts/lint-response-language-coverage.cjs) +and its suite, +[`tests/response-language-coverage.test.cjs`](../../tests/response-language-coverage.test.cjs). +The shipped requirements are REQ-LANG-01..04 in +[`docs/features/response-language-config.md`](../features/response-language-config.md). + +## The problem this gate exists for + +A directive worded around *questions, prompts, and explanations* covers the answer and leaves the +running commentary in English. The user then reads a translated answer wrapped in English status +updates, progress notes, and findings — which is the defect #2529 reports, not a stylistic +preference. Before this gate, 44 workflows carried exactly that wording and the older lint +certified it as coverage, so the gate was legitimising the bug it was meant to catch. + +Hence the discriminator: a directive counts only if it **names the narration class**. Naming means +the word `narration` or the phrase `between tool calls`. Enumerating members of the class — status +updates, progress notes, findings — without naming the class does not satisfy it, because the +enumeration reads as a closed list and the class is open (REQ-LANG-04). + +## The four coverage forms + +| Form | What it looks like | When it applies | +|---|---|---| +| Shared reference | `@~/.claude/gsd-core/references/response-language-directive.md` on its own line | The file is loaded eagerly — a top-level workflow. Preferred: one place to maintain the wording for the 42 files that take it. | +| Own inline directive | One line carrying the directive in the file's own words | The file already has one, or its prose needs local phrasing. Must pass all four predicates (below). | +| Pinned inline directive | The canonical line, byte for byte | A lazily-loaded mode/step/template that **cannot prove inheritance**. | +| Inherited | nothing in the file itself | A fragment whose parent workflow dispatches it from a read/execute context and is itself covered. | + +### Which form to use + +Answer in this order: + +1. **Is the file loaded eagerly?** Take the shared reference. An `@`-line is expanded when the file + is loaded, so this is the cheapest correct answer for a top-level workflow. +2. **Is it a fragment under `/{modes,steps,templates}/`?** Then check whether + `.md` dispatches this exact path from a read/execute context *and* is itself covered. + The catalog writes that stub two ways — rooted at `gsd-core/workflows/`, or relative to the + catalog — and either one counts; a path with no read/execute/run verb ahead of it on the same + line is a mention, not a dispatch, and proves nothing. + If both hold, the fragment **inherits** — add nothing. The parent's directive is already in the + loaded context by the time the fragment is read, so a second copy buys no coverage and gives the + wording somewhere to drift. +3. **Otherwise the fragment carries the pinned line**, and its path joins + `EXACT_INLINE_DIRECTIVE_WORKFLOWS` in the lint. Do not reach for the `@`-reference here: an + `@`-line inside a file that is itself read later is inert — it is text at that point, not an + import. + +Rule 2 decides the set in rule 3, and the suite enforces the boundary in both directions: a pinned +path that would have inherited fails +`a pinned workflow is one that could not have inherited instead`. + +### The pinned line + +``` +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. +``` + +Byte for byte, on its own line. A pinned file may also switch to the shared reference if it ever +becomes eagerly loaded — that is strictly better and the lint accepts it. + +## What an inline directive must contain + +A single line must satisfy four independent predicates: + +1. the token `response_language`, +2. an action verb — apply, use, present, translate, …, +3. a user-output noun — prose, output, questions, findings, …, +4. the narration class — `narration` or `between tool calls`. + +All four on **one** line. The check is textual, not semantic: it reads vocabulary, not polarity. + +## When the lint reds + +| Message | What to do | +|---|---| +| `N workflow(s) have no response-language coverage` | Pick a form from the table above for each listed path. | +| `N shared directive reference(s) no longer carry an actionable directive` | The reference itself was weakened. Fix the reference — one edit re-covers every file that imports it. | +| `cannot read the workflow directory` / `no workflow files found under` | Discovery failed. The lint fails closed on purpose: a run that inspected zero workflows cannot establish coverage. | + +Run it locally before pushing: + +``` +npm run lint:response-language +``` diff --git a/docs/features/response-language-config.md b/docs/features/response-language-config.md index ca22c43bf..eb703b353 100644 --- a/docs/features/response-language-config.md +++ b/docs/features/response-language-config.md @@ -11,6 +11,8 @@ group: v1.32 Features **Requirements:** - REQ-LANG-01: System MUST respect `response_language` setting across all phases and agents - REQ-LANG-02: Setting MUST propagate to all spawned agents for consistent language output +- REQ-LANG-03: Every workflow MUST carry response-language coverage — through an exact inline directive, a shared `@`-referenced directive (`gsd-core/references/response-language-directive.md`), or inheritance from the parent workflow that dispatches it; enforced in CI by `scripts/lint-response-language-coverage.cjs` (#2529) +- REQ-LANG-04: A covering directive MUST name inter-tool narration, not only the question/prompt surface. A directive names it by using the word "narration" or the phrase "between tool calls"; the class it denotes is the model's running commentary between tool calls, status updates, progress notes and findings included, and enumerating those items without naming the class does not satisfy the rule. A directive worded around questions and prompts alone leaves the model's running commentary in English beside translated answers, which is the defect #2529 reports; `scripts/lint-response-language-coverage.cjs` rejects it (#2529) **Config:** | Setting | Type | Default | Description | diff --git a/gsd-core/references/execute-phase-response-language.md b/gsd-core/references/execute-phase-response-language.md index 6020382d8..b530762cf 100644 --- a/gsd-core/references/execute-phase-response-language.md +++ b/gsd-core/references/execute-phase-response-language.md @@ -2,6 +2,12 @@ **If `response_language` is set:** User-facing orchestrator output (questions, narration, report-template prose) in `{response_language}`; technical terms, code, file paths, and subagent prompts stay in English. Pass `response_language: {value}` into every spawned subagent prompt so any user-facing output they produce stays in the configured language. +**The `gsd-verifier` subagent has no workflow file of its own (#2529):** the `verify_phase_goal` step reaches it by dispatch, not by reading a workflow, so there is no file in which to place a directive — the dispatch prompt is the only place its coverage can live. That prompt MUST carry this line verbatim, immediately after `Create VERIFICATION.md.`: + +`Use response_language {response_language} for all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code and paths.` + +It lives here rather than inline in `workflows/execute-phase.md` for the same reason the rest of this file does — that workflow is held under the frozen byte ceiling named below, and this `@-reference` is eager, so the orchestrator loads this instruction with the workflow either way. + The literal report templates embedded in this workflow (`## Execution Plan`, `## Phase {X}: {Name} Execution Complete`, `## ⚠ Phase {X}: {Name} — Gaps Found`, etc.) are a structural source, not literal output to copy verbatim — render their prose translated into `{response_language}` while keeping headings' structural markers, table columns, IDs, commands, and file paths unchanged. This directive was extracted from `workflows/execute-phase.md` to keep that file under the frozen pre-phase-6 byte ceiling (ADR-857 Phase 6 capstone, `tests/claude-orchestration.test.cjs`). The `@-reference` is eager, so the runtime still loads this content alongside the workflow — the extraction is purely a file-size discipline, not a lazy-load optimization. diff --git a/gsd-core/references/response-language-directive.md b/gsd-core/references/response-language-directive.md new file mode 100644 index 000000000..072b5ff50 --- /dev/null +++ b/gsd-core/references/response-language-directive.md @@ -0,0 +1,9 @@ +# Response-Language Directive (#2529) + +**If `response_language` is set** (in the init JSON this workflow parses, or in `.planning/config.json`): ALL user-facing output of this workflow MUST be in that language — narration between tool calls, status updates, progress notes, findings, banners, report prose, questions (AskUserQuestion or plain text), and summaries. Technical terms, code, file paths, commands, and identifiers stay in English. + +Literal English report/banner templates embedded in a workflow are a structural SOURCE, not literal output to copy verbatim — render their prose translated into `{response_language}` while keeping headings' structural markers, table columns, IDs, commands, and file paths unchanged. Exception: blocks a workflow explicitly requires to be emitted byte-for-byte (e.g. pre-rendered checkpoints) are output exactly as rendered. + +Pass `response_language: {value}` into every spawned subagent prompt so any user-facing output they produce stays in the configured language. + +Workflows take this contract in one of three forms (REQ-LANG-03): an `@`-reference to this file; their own inline directive naming the same narration class; or, for a fragment loaded by a covered parent, inheritance from that parent. Coverage is enforced by `scripts/lint-response-language-coverage.cjs` — a new workflow cannot ship without one of the three, and the lint checks this file's own wording too, so a weakened directive here uncovers every workflow that imports it rather than passing silently. Workflow-specific directives (e.g. `execute-phase-response-language.md`) take precedence where present. diff --git a/gsd-core/workflows/add-backlog.md b/gsd-core/workflows/add-backlog.md index 3b63283fa..21f5b38ea 100644 --- a/gsd-core/workflows/add-backlog.md +++ b/gsd-core/workflows/add-backlog.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + # Add Backlog Item Workflow Invoked by `/gsd:capture --backlog` (`commands/gsd/capture.md`). diff --git a/gsd-core/workflows/add-phase.md b/gsd-core/workflows/add-phase.md index cdef99f40..8b858cf05 100644 --- a/gsd-core/workflows/add-phase.md +++ b/gsd-core/workflows/add-phase.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Add a new integer phase to the end of the current milestone in the roadmap. Automatically calculates next phase number, creates phase directory, and updates roadmap structure. diff --git a/gsd-core/workflows/add-tests.md b/gsd-core/workflows/add-tests.md index 018eeb097..6ed3b581a 100644 --- a/gsd-core/workflows/add-tests.md +++ b/gsd-core/workflows/add-tests.md @@ -40,7 +40,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi Extract from init JSON: `phase_dir`, `phase_number`, `phase_name`, `response_language`. -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Verify the phase directory exists. If not: ``` diff --git a/gsd-core/workflows/add-todo.md b/gsd-core/workflows/add-todo.md index 16cd2061a..bc378b7ea 100644 --- a/gsd-core/workflows/add-todo.md +++ b/gsd-core/workflows/add-todo.md @@ -19,7 +19,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi Extract from init JSON: `commit_docs`, `date`, `timestamp`, `todo_count`, `todos`, `pending_dir`, `todos_dir_exists`, `response_language`. -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Ensure directories exist: ```bash diff --git a/gsd-core/workflows/ai-integration-phase.md b/gsd-core/workflows/ai-integration-phase.md index a2c5a81a4..eb086c2ee 100644 --- a/gsd-core/workflows/ai-integration-phase.md +++ b/gsd-core/workflows/ai-integration-phase.md @@ -27,7 +27,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi Parse JSON for: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_context`, `has_research`, `commit_docs`, `response_language`. -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. **File paths:** `state_path`, `roadmap_path`, `requirements_path`, `context_path`. diff --git a/gsd-core/workflows/analyze-dependencies.md b/gsd-core/workflows/analyze-dependencies.md index 376e31c9e..ee75d2b5a 100644 --- a/gsd-core/workflows/analyze-dependencies.md +++ b/gsd-core/workflows/analyze-dependencies.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Analyze ROADMAP.md phases for dependency relationships before execution. Detect file overlap between phases, semantic API/data-flow dependencies, and suggest `Depends on` entries to prevent merge conflicts during parallel execution by `/gsd:manager`. diff --git a/gsd-core/workflows/audit-fix.md b/gsd-core/workflows/audit-fix.md index 38ee2a702..96676a2ce 100644 --- a/gsd-core/workflows/audit-fix.md +++ b/gsd-core/workflows/audit-fix.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Autonomous audit-to-fix pipeline. Runs an audit, parses findings, classifies each as auto-fixable vs manual-only, spawns executor agents for fixable issues, runs tests diff --git a/gsd-core/workflows/audit-milestone.md b/gsd-core/workflows/audit-milestone.md index daa35ea8c..817439c45 100644 --- a/gsd-core/workflows/audit-milestone.md +++ b/gsd-core/workflows/audit-milestone.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Verify milestone achieved its definition of done by aggregating phase verifications, checking cross-phase integration, and assessing requirements coverage. Reads existing VERIFICATION.md files (phases already verified during execute-phase), aggregates tech debt and deferred gaps, then spawns integration checker for cross-phase wiring. diff --git a/gsd-core/workflows/audit-uat.md b/gsd-core/workflows/audit-uat.md index 10735339b..e30090402 100644 --- a/gsd-core/workflows/audit-uat.md +++ b/gsd-core/workflows/audit-uat.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Cross-phase audit of all UAT and verification files. Finds every outstanding item (pending, skipped, blocked, human_needed), optionally verifies against the codebase to detect stale docs, and produces a prioritized human test plan. diff --git a/gsd-core/workflows/autonomous.md b/gsd-core/workflows/autonomous.md index 3847c7185..4639a0004 100644 --- a/gsd-core/workflows/autonomous.md +++ b/gsd-core/workflows/autonomous.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Drive milestone phases autonomously — all remaining phases, a range via `--from N`/`--to N`, or a single phase via `--only N`. For each incomplete phase: discuss → plan → execute using Skill() flat invocations. When `--converge` or `--cross-ai` is set, route the planning step through plan-review convergence before execution. Pauses only for explicit user decisions (grey area acceptance, blockers, validation requests). Re-reads ROADMAP.md after each phase to catch dynamically inserted phases. diff --git a/gsd-core/workflows/check-todos.md b/gsd-core/workflows/check-todos.md index 06f06f75d..335d46ff9 100644 --- a/gsd-core/workflows/check-todos.md +++ b/gsd-core/workflows/check-todos.md @@ -19,7 +19,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi Extract from init JSON: `todo_count`, `todos`, `pending_dir`, `response_language`. -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. If `todo_count` is 0: ``` diff --git a/gsd-core/workflows/cleanup.md b/gsd-core/workflows/cleanup.md index fa22d4a5a..6f11e9790 100644 --- a/gsd-core/workflows/cleanup.md +++ b/gsd-core/workflows/cleanup.md @@ -19,7 +19,7 @@ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-pars RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default "" 2>/dev/null || echo "") ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. diff --git a/gsd-core/workflows/code-review-fix.md b/gsd-core/workflows/code-review-fix.md index 96ad7276e..e36ba55f5 100644 --- a/gsd-core/workflows/code-review-fix.md +++ b/gsd-core/workflows/code-review-fix.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Auto-fix issues from REVIEW.md. Validates phase, checks config gate, verifies REVIEW.md exists and has fixable issues, spawns gsd-code-fixer agent, handles --auto iteration loop (capped at 3), commits REVIEW-FIX.md once at the end, and presents results. diff --git a/gsd-core/workflows/code-review.md b/gsd-core/workflows/code-review.md index 6d33ce4ae..11da2b040 100644 --- a/gsd-core/workflows/code-review.md +++ b/gsd-core/workflows/code-review.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Review source files changed during a phase for bugs, security issues, and code quality problems. Computes file scope (--files override > SUMMARY.md > git diff fallback), checks config gate, spawns gsd-code-reviewer agent, commits REVIEW.md, and presents results to user. When --fix is passed, delegates to code-review-fix.md after review to auto-apply findings via gsd-code-fixer. diff --git a/gsd-core/workflows/complete-milestone.md b/gsd-core/workflows/complete-milestone.md index 466ea92f1..4d4d064d7 100644 --- a/gsd-core/workflows/complete-milestone.md +++ b/gsd-core/workflows/complete-milestone.md @@ -46,7 +46,7 @@ RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default " gsd_run query audit-open ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. If the output contains open items (any section with count > 0): diff --git a/gsd-core/workflows/debug.md b/gsd-core/workflows/debug.md index e3a0dc2a8..18fb6ff89 100644 --- a/gsd-core/workflows/debug.md +++ b/gsd-core/workflows/debug.md @@ -30,7 +30,7 @@ One round-trip carries everything this workflow needs (#3149 — this call repla - `tdd_mode` — used as `{TDD_MODE}` in the session parameter blocks below. - `section_manifest` — `null` today, because this workflow declares no applicability-section markers of its own. **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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. ## 1a. LIST subcommand diff --git a/gsd-core/workflows/diagnose-issues.md b/gsd-core/workflows/diagnose-issues.md index d78eba199..c2bf048dd 100644 --- a/gsd-core/workflows/diagnose-issues.md +++ b/gsd-core/workflows/diagnose-issues.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Orchestrate parallel debug agents to investigate UAT gaps and find root causes. diff --git a/gsd-core/workflows/discuss-phase-assumptions.md b/gsd-core/workflows/discuss-phase-assumptions.md index 8e3f77e7e..309f5a245 100644 --- a/gsd-core/workflows/discuss-phase-assumptions.md +++ b/gsd-core/workflows/discuss-phase-assumptions.md @@ -73,7 +73,7 @@ AGENT_SKILLS_ANALYZER=$(gsd_run query agent-skills gsd-assumptions-analyzer) ANALYZER_MODEL=$(gsd_run query resolve-model gsd-assumptions-analyzer --raw) ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Parse JSON for: `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_plans`, `has_verification`, diff --git a/gsd-core/workflows/discuss-phase-power.md b/gsd-core/workflows/discuss-phase-power.md index 0877ee6e3..410bacfd7 100644 --- a/gsd-core/workflows/discuss-phase-power.md +++ b/gsd-core/workflows/discuss-phase-power.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Power user mode for discuss-phase. Generates ALL questions upfront into a JSON state file and an HTML companion UI, then waits for the user to answer at their own pace. When the user signals readiness, processes all answers in one pass and generates CONTEXT.md. diff --git a/gsd-core/workflows/discuss-phase.md b/gsd-core/workflows/discuss-phase.md index 259d81e9c..4bd59fd30 100644 --- a/gsd-core/workflows/discuss-phase.md +++ b/gsd-core/workflows/discuss-phase.md @@ -122,7 +122,7 @@ AGENT_SKILLS_ADVISOR=$(gsd_run query agent-skills gsd-advisor-researcher) Parse JSON for: `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_plans`, `has_verification`, `plan_count`, `roadmap_exists`, `planning_exists`, `response_language`. -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. **If `phase_found` is false:** ``` diff --git a/gsd-core/workflows/discuss-phase/modes/advisor.md b/gsd-core/workflows/discuss-phase/modes/advisor.md index 240056622..9c56fc2ea 100644 --- a/gsd-core/workflows/discuss-phase/modes/advisor.md +++ b/gsd-core/workflows/discuss-phase/modes/advisor.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # Advisor mode — research-backed comparison tables > **Lazy-loaded and gated.** The parent `workflows/discuss-phase.md` Reads diff --git a/gsd-core/workflows/discuss-phase/modes/all.md b/gsd-core/workflows/discuss-phase/modes/all.md index 50fa9d066..7176409cf 100644 --- a/gsd-core/workflows/discuss-phase/modes/all.md +++ b/gsd-core/workflows/discuss-phase/modes/all.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # --all mode — auto-select ALL gray areas, discuss interactively > **Lazy-loaded.** Read this file from `workflows/discuss-phase.md` when diff --git a/gsd-core/workflows/discuss-phase/modes/analyze.md b/gsd-core/workflows/discuss-phase/modes/analyze.md index b373da116..d2b5a4a3d 100644 --- a/gsd-core/workflows/discuss-phase/modes/analyze.md +++ b/gsd-core/workflows/discuss-phase/modes/analyze.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # --analyze mode — trade-off tables before each question > **Lazy-loaded overlay.** Read this file from `workflows/discuss-phase.md` diff --git a/gsd-core/workflows/discuss-phase/modes/auto.md b/gsd-core/workflows/discuss-phase/modes/auto.md index fdd001eb3..638d5db98 100644 --- a/gsd-core/workflows/discuss-phase/modes/auto.md +++ b/gsd-core/workflows/discuss-phase/modes/auto.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # --auto mode — fully autonomous discuss-phase > **Lazy-loaded.** Read this file from `workflows/discuss-phase.md` when diff --git a/gsd-core/workflows/discuss-phase/modes/batch.md b/gsd-core/workflows/discuss-phase/modes/batch.md index c62b25d55..f99cfcf2e 100644 --- a/gsd-core/workflows/discuss-phase/modes/batch.md +++ b/gsd-core/workflows/discuss-phase/modes/batch.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # --batch mode — grouped question batches > **Lazy-loaded overlay.** Read this file from `workflows/discuss-phase.md` diff --git a/gsd-core/workflows/discuss-phase/modes/chain.md b/gsd-core/workflows/discuss-phase/modes/chain.md index e8b0218eb..085a68a91 100644 --- a/gsd-core/workflows/discuss-phase/modes/chain.md +++ b/gsd-core/workflows/discuss-phase/modes/chain.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # --chain mode — interactive discuss, then auto-advance > **Lazy-loaded.** Read this file from `workflows/discuss-phase.md` when diff --git a/gsd-core/workflows/discuss-phase/modes/default.md b/gsd-core/workflows/discuss-phase/modes/default.md index 910636dcc..68edf901c 100644 --- a/gsd-core/workflows/discuss-phase/modes/default.md +++ b/gsd-core/workflows/discuss-phase/modes/default.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # Default mode — interactive discuss-phase > **Lazy-loaded.** Read this file from `workflows/discuss-phase.md` when no diff --git a/gsd-core/workflows/discuss-phase/modes/power.md b/gsd-core/workflows/discuss-phase/modes/power.md index 69f603b89..4ac0dc29a 100644 --- a/gsd-core/workflows/discuss-phase/modes/power.md +++ b/gsd-core/workflows/discuss-phase/modes/power.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # --power mode — bulk question generation, async answering > **Lazy-loaded.** Read this file from `workflows/discuss-phase.md` when diff --git a/gsd-core/workflows/discuss-phase/modes/text.md b/gsd-core/workflows/discuss-phase/modes/text.md index 9208aae51..fc566e6fa 100644 --- a/gsd-core/workflows/discuss-phase/modes/text.md +++ b/gsd-core/workflows/discuss-phase/modes/text.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # --text mode — plain-text overlay (no AskUserQuestion) > **Lazy-loaded overlay.** Read this file from `workflows/discuss-phase.md` diff --git a/gsd-core/workflows/discuss-phase/templates/context.md b/gsd-core/workflows/discuss-phase/templates/context.md index 7e861370b..53c6a318c 100644 --- a/gsd-core/workflows/discuss-phase/templates/context.md +++ b/gsd-core/workflows/discuss-phase/templates/context.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # CONTEXT.md template — for discuss-phase write_context step > **Lazy-loaded.** Read this file only inside the `write_context` step of diff --git a/gsd-core/workflows/discuss-phase/templates/discussion-log.md b/gsd-core/workflows/discuss-phase/templates/discussion-log.md index 62a68684e..ef998dee8 100644 --- a/gsd-core/workflows/discuss-phase/templates/discussion-log.md +++ b/gsd-core/workflows/discuss-phase/templates/discussion-log.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # DISCUSSION-LOG.md template — for discuss-phase git_commit step > **Lazy-loaded.** Read this file only inside the `git_commit` step of diff --git a/gsd-core/workflows/do.md b/gsd-core/workflows/do.md index 79d154352..8c59482f0 100644 --- a/gsd-core/workflows/do.md +++ b/gsd-core/workflows/do.md @@ -13,7 +13,7 @@ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-pars RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default "" 2>/dev/null || echo "") ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. **Check for input.** diff --git a/gsd-core/workflows/docs-update.md b/gsd-core/workflows/docs-update.md index 040cb7ff3..cff09c920 100644 --- a/gsd-core/workflows/docs-update.md +++ b/gsd-core/workflows/docs-update.md @@ -34,7 +34,7 @@ Extract from init JSON: - `monorepo_workspaces` — array of workspace glob patterns (empty if not a monorepo) - `section_manifest` — parsed from `INIT_DOCS_UPDATE` (not `INIT`); gates the `dispatch-monorepo-packages` section below - `project_root` — absolute path to the project root -- `response_language` — if set, present all user-facing questions, prompts, and explanations in this workflow in that language; technical terms, code, file paths, and subagent prompts stay in English +- `response_language` — if set, present all user-facing output of this workflow in that language — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations; technical terms, code, file paths, and subagent prompts stay in English diff --git a/gsd-core/workflows/edit-phase.md b/gsd-core/workflows/edit-phase.md index 1d03a03ca..e0e7094f3 100644 --- a/gsd-core/workflows/edit-phase.md +++ b/gsd-core/workflows/edit-phase.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Edit any field of an existing phase in ROADMAP.md in place. The phase number and position are always preserved. Guarded against in-progress and completed phases unless --force is passed. Validates depends_on references before writing. Shows a diff and requests confirmation before writing. diff --git a/gsd-core/workflows/eval-review.md b/gsd-core/workflows/eval-review.md index 8d0619170..8fc0d5ff7 100644 --- a/gsd-core/workflows/eval-review.md +++ b/gsd-core/workflows/eval-review.md @@ -19,7 +19,7 @@ INIT=$(gsd_run query init.phase-op "${PHASE_ARG}") if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `commit_docs`. diff --git a/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md b/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md index 0f255755b..03a7dc02e 100644 --- a/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +++ b/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # Step: codebase_drift_gate Post-execution structural drift detection (#2003). Runs after the last wave diff --git a/gsd-core/workflows/execute-phase/steps/regression-gate-run.md b/gsd-core/workflows/execute-phase/steps/regression-gate-run.md index e93d781a9..d8575f98b 100644 --- a/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +++ b/gsd-core/workflows/execute-phase/steps/regression-gate-run.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # Step: regression_gate_run Run the resolved prior-phase test command one-shot, bounded by a timeout, so a diff --git a/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md b/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md index ccc992853..204d6ffeb 100644 --- a/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +++ b/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # Worktree Recovery Policy ## ORCHESTRATOR FAIL-CLOSED RULE (#48) diff --git a/gsd-core/workflows/execute-plan.md b/gsd-core/workflows/execute-plan.md index 56347cb2c..5648b6ba6 100644 --- a/gsd-core/workflows/execute-plan.md +++ b/gsd-core/workflows/execute-plan.md @@ -50,7 +50,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi Extract from init JSON: `executor_model`, `commit_docs`, `sub_repos`, `phase_dir`, `phase_number`, `plans`, `summaries`, `incomplete_plans`, `state_path`, `config_path`, `response_language`. -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. If `.planning/` missing: error. diff --git a/gsd-core/workflows/explore.md b/gsd-core/workflows/explore.md index f5f6b5215..674d0fc97 100644 --- a/gsd-core/workflows/explore.md +++ b/gsd-core/workflows/explore.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Socratic ideation workflow. Guides the developer through exploring an idea via probing questions, offers mid-conversation research when useful, then routes crystallized outputs to GSD artifacts. diff --git a/gsd-core/workflows/extract-learnings.md b/gsd-core/workflows/extract-learnings.md index d7abf51fe..b4c1b7a30 100644 --- a/gsd-core/workflows/extract-learnings.md +++ b/gsd-core/workflows/extract-learnings.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Extract decisions, lessons learned, patterns discovered, and surprises encountered from completed phase artifacts into a structured LEARNINGS.md file. Captures institutional knowledge that would otherwise be lost between phases. diff --git a/gsd-core/workflows/fast.md b/gsd-core/workflows/fast.md index 308ac913f..d0279f61f 100644 --- a/gsd-core/workflows/fast.md +++ b/gsd-core/workflows/fast.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Execute a trivial task inline without subagent overhead. No PLAN.md, no Task spawning, no research, no plan checking. Just: understand → do → commit → log. diff --git a/gsd-core/workflows/forensics.md b/gsd-core/workflows/forensics.md index 0e72d6210..7fbdfbfb8 100644 --- a/gsd-core/workflows/forensics.md +++ b/gsd-core/workflows/forensics.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + # Forensics Workflow Post-mortem investigation for failed or stuck GSD workflows. Analyzes git history, diff --git a/gsd-core/workflows/graduation.md b/gsd-core/workflows/graduation.md index db9e9e036..2d8ae5567 100644 --- a/gsd-core/workflows/graduation.md +++ b/gsd-core/workflows/graduation.md @@ -28,7 +28,7 @@ GRADUATION_WINDOW=$(gsd_run query config-get features.graduation_window --raw 2> GRADUATION_THRESHOLD=$(gsd_run query config-get features.graduation_threshold --raw 2>/dev/null || echo "3") ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. **Skip silently (print nothing) if:** - `features.graduation` is `false` diff --git a/gsd-core/workflows/health.md b/gsd-core/workflows/health.md index e3046d9f0..1dc489ee1 100644 --- a/gsd-core/workflows/health.md +++ b/gsd-core/workflows/health.md @@ -12,7 +12,7 @@ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-pars RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default "" 2>/dev/null || echo "") ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. **Parse arguments:** diff --git a/gsd-core/workflows/help.md b/gsd-core/workflows/help.md index b5bf35c2a..7902315f4 100644 --- a/gsd-core/workflows/help.md +++ b/gsd-core/workflows/help.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Display GSD command help at the tier the user asked for. Output ONLY the reference content of the chosen mode. Do NOT add project-specific analysis, git status, next-step suggestions, or any commentary beyond the reference. diff --git a/gsd-core/workflows/help/modes/brief.md b/gsd-core/workflows/help/modes/brief.md index 46a1cd41f..e4c3b1d0f 100644 --- a/gsd-core/workflows/help/modes/brief.md +++ b/gsd-core/workflows/help/modes/brief.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + One-liner refresher for returning users. Output ONLY the `` content below. No additions. diff --git a/gsd-core/workflows/help/modes/default.md b/gsd-core/workflows/help/modes/default.md index 86274d63b..6b0fe4a66 100644 --- a/gsd-core/workflows/help/modes/default.md +++ b/gsd-core/workflows/help/modes/default.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + One-page newcomer-oriented tour of GSD Core. Output ONLY the `` content below. No additions. diff --git a/gsd-core/workflows/help/modes/full.md b/gsd-core/workflows/help/modes/full.md index 496f59bc1..1df7fb437 100644 --- a/gsd-core/workflows/help/modes/full.md +++ b/gsd-core/workflows/help/modes/full.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + Display the complete GSD Core command reference. Output ONLY the reference content. Do NOT add project-specific analysis, git status, next-step suggestions, or any commentary beyond the reference. diff --git a/gsd-core/workflows/help/modes/topic.md b/gsd-core/workflows/help/modes/topic.md index 89b380263..07761879c 100644 --- a/gsd-core/workflows/help/modes/topic.md +++ b/gsd-core/workflows/help/modes/topic.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + Emit a section from the full reference for the topic in `$ARGUMENTS`. Read `workflows/help/modes/full.md`, resolve the topic alias to a section heading using the table below, and output the resolved-routing preamble plus the section content. Scope is controlled by a `--brief` flag in `$ARGUMENTS`: full scope (default) emits the entire section; compact scope (`--brief `) emits only the signature line + one-line summary for a compact scoped lookup. No additions, no surrounding chrome. diff --git a/gsd-core/workflows/import.md b/gsd-core/workflows/import.md index 0edba4189..a81df0b03 100644 --- a/gsd-core/workflows/import.md +++ b/gsd-core/workflows/import.md @@ -16,7 +16,7 @@ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-pars RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default "" 2>/dev/null || echo "") ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. ``` ### GSD ► IMPORT diff --git a/gsd-core/workflows/inbox.md b/gsd-core/workflows/inbox.md index e2ff3bbd2..8ac5bf0bf 100644 --- a/gsd-core/workflows/inbox.md +++ b/gsd-core/workflows/inbox.md @@ -22,7 +22,7 @@ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-pars RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default "" 2>/dev/null || echo "") ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Verify prerequisites: diff --git a/gsd-core/workflows/ingest-docs.md b/gsd-core/workflows/ingest-docs.md index 318206ea7..6c0de5e08 100644 --- a/gsd-core/workflows/ingest-docs.md +++ b/gsd-core/workflows/ingest-docs.md @@ -59,7 +59,7 @@ SYNTHESIZER_MODEL=$(gsd_run query resolve-model gsd-doc-synthesizer --raw) ROADMAPPER_MODEL=$(gsd_run query resolve-model gsd-roadmapper --raw) ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Parse `project_exists`, `planning_exists`, `has_git`, `git_worktree_root`, `in_nested_subdir`, `project_path` from INIT. diff --git a/gsd-core/workflows/insert-phase.md b/gsd-core/workflows/insert-phase.md index ed634c033..4846ed156 100644 --- a/gsd-core/workflows/insert-phase.md +++ b/gsd-core/workflows/insert-phase.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Insert a decimal phase for urgent work discovered mid-milestone between existing integer phases. Uses decimal numbering (72.1, 72.2, etc.) to preserve the logical sequence of planned phases while accommodating urgent insertions without renumbering the entire roadmap. diff --git a/gsd-core/workflows/list-phase-assumptions.md b/gsd-core/workflows/list-phase-assumptions.md index 255f133c2..96c5de2cb 100644 --- a/gsd-core/workflows/list-phase-assumptions.md +++ b/gsd-core/workflows/list-phase-assumptions.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Surface Claude's assumptions about a phase before planning, enabling users to correct misconceptions early. diff --git a/gsd-core/workflows/list-seeds.md b/gsd-core/workflows/list-seeds.md index f0848bf82..5e0519953 100644 --- a/gsd-core/workflows/list-seeds.md +++ b/gsd-core/workflows/list-seeds.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + List captured seeds for browsing and audit, with an optional status filter. Read-only — never mutates seeds. diff --git a/gsd-core/workflows/list-workspaces.md b/gsd-core/workflows/list-workspaces.md index f449a80eb..85e6fde16 100644 --- a/gsd-core/workflows/list-workspaces.md +++ b/gsd-core/workflows/list-workspaces.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + List all GSD workspaces found in ~/gsd-workspaces/ with their status. diff --git a/gsd-core/workflows/manager.md b/gsd-core/workflows/manager.md index d483aa702..108c56f2c 100644 --- a/gsd-core/workflows/manager.md +++ b/gsd-core/workflows/manager.md @@ -26,7 +26,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi Parse JSON for: `milestone_version`, `milestone_name`, `phase_count`, `completed_count`, `in_progress_count`, `phases`, `recommended_actions`, `all_complete`, `waiting_signal`, `manager_flags`, `response_language`, and the optional trio `queued_milestone_version`, `queued_milestone_name`, `queued_phases` (added in SDK fix `2495-2496-2497` — may be absent on older SDK versions, treat missing as empty). -**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. Subagent dispatches (discuss/plan/execute) stay in English at the prompt level; include `response_language` in their spawn args per the workflow being dispatched. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Subagent dispatches (discuss/plan/execute) stay in English at the prompt level; include `response_language` in their spawn args per the workflow being dispatched. `manager_flags` contains per-step passthrough flags from config: - `manager_flags.discuss` — appended to `/gsd:discuss-phase` args (e.g. `"--auto --analyze"`) diff --git a/gsd-core/workflows/map-codebase.md b/gsd-core/workflows/map-codebase.md index 6e0f2e988..f7057c6a9 100644 --- a/gsd-core/workflows/map-codebase.md +++ b/gsd-core/workflows/map-codebase.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Orchestrate parallel codebase mapper agents to analyze codebase and produce structured documents in .planning/codebase/ diff --git a/gsd-core/workflows/milestone-summary.md b/gsd-core/workflows/milestone-summary.md index 7ff0a45cd..81b939f1e 100644 --- a/gsd-core/workflows/milestone-summary.md +++ b/gsd-core/workflows/milestone-summary.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + # Milestone Summary Workflow Generate a comprehensive, human-friendly project summary from completed milestone artifacts. diff --git a/gsd-core/workflows/mvp-phase.md b/gsd-core/workflows/mvp-phase.md index 1e3ec9d82..9f938b9db 100644 --- a/gsd-core/workflows/mvp-phase.md +++ b/gsd-core/workflows/mvp-phase.md @@ -58,7 +58,7 @@ else fi ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. If `PHASE_FOUND` is `false`: error and exit. Suggest `/gsd add-phase` or `/gsd insert-phase` to create the phase first. diff --git a/gsd-core/workflows/new-milestone.md b/gsd-core/workflows/new-milestone.md index 737cc68c4..bc6ca4a0d 100644 --- a/gsd-core/workflows/new-milestone.md +++ b/gsd-core/workflows/new-milestone.md @@ -50,7 +50,7 @@ if [[ "$INIT_EARLY" == @file:* ]]; then INIT_EARLY=$(cat "${INIT_EARLY#@file:}") `GSD_WS` must chain to every downstream routing suggestion in this workflow (Step 4's shared-file guard, and the `/gsd:discuss-phase`/`/gsd:plan-phase` routing hints below) per the routing-propagation contract in `gsd-core/references/workstream-flag.md` — never let it silently drop. -**If `response_language` is set:** All user-facing questions, prompts, and explanations in this workflow (including the "What do you want to build next?" prompt and seed-selection questions below) MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations (including the "What do you want to build next?" prompt and seed-selection questions below) — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. - Read PROJECT.md (existing project, validated requirements, decisions) - Read MILESTONES.md (what shipped previously) diff --git a/gsd-core/workflows/new-project.md b/gsd-core/workflows/new-project.md index 7d79929d8..0fb56e70b 100644 --- a/gsd-core/workflows/new-project.md +++ b/gsd-core/workflows/new-project.md @@ -39,7 +39,7 @@ AGENT_SKILLS_ROADMAPPER=$(gsd_run query agent-skills gsd-roadmapper) Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `project_exists`, `has_codebase_map`, `planning_exists`, `has_existing_code`, `has_package_file`, `is_brownfield`, `needs_codebase_map`, `has_git`, `git_worktree_root`, `in_nested_subdir`, `project_path`, `agents_installed`, `missing_agents`, `agent_runtime`, `agents_dir`, `required_agents`, `required_agents_installed`, `missing_required_agents`, `agent_skill_payloads_available`, `agent_skill_payload_agents`, `requirements_exists`, `init_incomplete`, `requirements_path`, `roadmap_path`, `config_path`, `research_dir`, `response_language`. -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. **If `agents_installed` is false:** Display a warning before proceeding: ```text diff --git a/gsd-core/workflows/new-workspace.md b/gsd-core/workflows/new-workspace.md index 749d7ece7..58e4b5488 100644 --- a/gsd-core/workflows/new-workspace.md +++ b/gsd-core/workflows/new-workspace.md @@ -20,7 +20,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi Parse JSON for: `default_workspace_base`, `child_repos`, `child_repo_count`, `worktree_available`, `is_git_repo`, `cwd_repo_name`, `project_root`, `response_language`. -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. ## 2. Parse Arguments diff --git a/gsd-core/workflows/next.md b/gsd-core/workflows/next.md index 3ee9537a8..64654b178 100644 --- a/gsd-core/workflows/next.md +++ b/gsd-core/workflows/next.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Detect current project state and automatically advance to the next logical GSD workflow step. Reads project state to determine: discuss → plan → execute → verify → complete progression. diff --git a/gsd-core/workflows/node-repair.md b/gsd-core/workflows/node-repair.md index 7be3dbbcc..247e15e05 100644 --- a/gsd-core/workflows/node-repair.md +++ b/gsd-core/workflows/node-repair.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Autonomous repair operator for failed task verification. Invoked by execute-plan when a task fails its done-criteria. Proposes and attempts structured fixes before escalating to the user. diff --git a/gsd-core/workflows/note.md b/gsd-core/workflows/note.md index 8e701cd0c..2e71c851e 100644 --- a/gsd-core/workflows/note.md +++ b/gsd-core/workflows/note.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Zero-friction idea capture. One Write call, one confirmation line. No questions, no prompts. diff --git a/gsd-core/workflows/onboard.md b/gsd-core/workflows/onboard.md index eced3b0a7..34153404c 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` - `response_language` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Set: - `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. diff --git a/gsd-core/workflows/pause-work.md b/gsd-core/workflows/pause-work.md index 8d347a037..0c5744122 100644 --- a/gsd-core/workflows/pause-work.md +++ b/gsd-core/workflows/pause-work.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Create structured `.planning/HANDOFF.json` and `.continue-here.md` handoff files to preserve complete work state across sessions. The JSON provides machine-readable state for `/gsd:resume-work`; the markdown provides human-readable context. diff --git a/gsd-core/workflows/plan-phase.md b/gsd-core/workflows/plan-phase.md index 4858a9ccf..aece64ff7 100644 --- a/gsd-core/workflows/plan-phase.md +++ b/gsd-core/workflows/plan-phase.md @@ -100,7 +100,7 @@ Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_ **#2517:** omit the `model=` param from an `Agent()` call when its `researcher`/`planner`/`checker`_model is `"inherit"` or empty — passing `model=""` 404s on non-Claude runtimes; omitting inherits the orchestrator model (mirrors execute-phase). -**If `response_language` is set:** All user-facing orchestrator output MUST be in `{response_language}`; technical terms, code, paths, and subagent prompts stay in English. Pass `response_language: {value}` into every spawned subagent prompt. +**If `response_language` is set:** All user-facing orchestrator output — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be in `{response_language}`; technical terms, code, paths, and subagent prompts stay in English. Pass `response_language: {value}` into every spawned subagent prompt. **File paths (for blocks):** `state_path`, `roadmap_path`, `requirements_path`, `context_path`, `research_path`, `verification_path`, `uat_path`, `reviews_path`. These are null if files don't exist. diff --git a/gsd-core/workflows/plan-phase/steps/prd-express-path.md b/gsd-core/workflows/plan-phase/steps/prd-express-path.md index 5b762ac61..7f908f869 100644 --- a/gsd-core/workflows/plan-phase/steps/prd-express-path.md +++ b/gsd-core/workflows/plan-phase/steps/prd-express-path.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + # PRD Express Path — generate CONTEXT.md from a PRD Runs when `--prd ` is provided (§3.5 of `plan-phase.md`). diff --git a/gsd-core/workflows/plan-review-convergence.md b/gsd-core/workflows/plan-review-convergence.md index acc163fca..117a4d7b6 100644 --- a/gsd-core/workflows/plan-review-convergence.md +++ b/gsd-core/workflows/plan-review-convergence.md @@ -119,7 +119,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi Parse JSON for: `phase_dir`, `phase_number`, `padded_phase`, `phase_name`, `has_plans`, `plan_count`, `commit_docs`, `text_mode`, `response_language`. -**If `response_language` is set:** All user-facing output should be in `{response_language}`. +**If `response_language` is set:** All user-facing output — narration between tool calls, status updates, progress notes, findings, questions, and report prose — should be in `{response_language}`. Set `TEXT_MODE=true` if `--text` is present in $ARGUMENTS OR `text_mode` from init JSON is `true`. When `TEXT_MODE` is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. diff --git a/gsd-core/workflows/plant-seed.md b/gsd-core/workflows/plant-seed.md index ae0f2952c..706d2e081 100644 --- a/gsd-core/workflows/plant-seed.md +++ b/gsd-core/workflows/plant-seed.md @@ -140,7 +140,7 @@ RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default " gsd_run query commit "docs: plant seed — {$IDEA}" --files .planning/seeds/SEED-{PADDED}-{slug}.md ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. diff --git a/gsd-core/workflows/pr-branch.md b/gsd-core/workflows/pr-branch.md index 00a6b8a84..3ffa66ff0 100644 --- a/gsd-core/workflows/pr-branch.md +++ b/gsd-core/workflows/pr-branch.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Create a clean branch for pull requests by filtering .planning/ paths out of the cherry-picked history. Two modes, selected by the `planning.pr_strict` config key: diff --git a/gsd-core/workflows/profile-user.md b/gsd-core/workflows/profile-user.md index d69f501f3..489901c27 100644 --- a/gsd-core/workflows/profile-user.md +++ b/gsd-core/workflows/profile-user.md @@ -19,7 +19,7 @@ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-pars RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default "" 2>/dev/null || echo "") ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. ## 1. Initialize diff --git a/gsd-core/workflows/progress.md b/gsd-core/workflows/progress.md index b0d753e84..b3c09fd64 100644 --- a/gsd-core/workflows/progress.md +++ b/gsd-core/workflows/progress.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Check project progress, summarize recent work and what's ahead, then intelligently route to the next action — either executing an existing plan or creating the next one. Provides situational awareness before continuing work. diff --git a/gsd-core/workflows/quick-batch.md b/gsd-core/workflows/quick-batch.md index aa265ef53..9a97a4fa9 100644 --- a/gsd-core/workflows/quick-batch.md +++ b/gsd-core/workflows/quick-batch.md @@ -1,3 +1,4 @@ +@~/.claude/gsd-core/references/response-language-directive.md Batch several `/gsd:quick`-shaped tasks together (#3676, epic #3344, ADR-1239 "Quick-batch binding"). ONE coordinator (this workflow) owns every shared diff --git a/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md b/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md index 82d85e7b1..b83c130ce 100644 --- a/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +++ b/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + **Step 4.5: Plan-checker loop (only when `$VALIDATE_MODE`, called from planner-wave.md)** Runs once per DAG layer, for every item in that layer that produced a diff --git a/gsd-core/workflows/quick.md b/gsd-core/workflows/quick.md index 6f9b0d48b..651f94564 100644 --- a/gsd-core/workflows/quick.md +++ b/gsd-core/workflows/quick.md @@ -43,7 +43,7 @@ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-pars RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default "" 2>/dev/null || echo "") ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. If `$DESCRIPTION` is empty after parsing, prompt user interactively: diff --git a/gsd-core/workflows/reapply-patches.md b/gsd-core/workflows/reapply-patches.md index f585890ed..b4e6a493a 100644 --- a/gsd-core/workflows/reapply-patches.md +++ b/gsd-core/workflows/reapply-patches.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + # Reapply Local Patches Workflow Invoked by `/gsd:update --reapply` (`commands/gsd/update.md`). diff --git a/gsd-core/workflows/remove-phase.md b/gsd-core/workflows/remove-phase.md index 3c6e165a7..6246c7a5f 100644 --- a/gsd-core/workflows/remove-phase.md +++ b/gsd-core/workflows/remove-phase.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Remove an unstarted future phase from the project roadmap, delete its directory, renumber all subsequent phases to maintain a clean linear sequence, and commit the change. The git commit serves as the historical record of removal. diff --git a/gsd-core/workflows/remove-workspace.md b/gsd-core/workflows/remove-workspace.md index 919d95eba..1c21bfc9f 100644 --- a/gsd-core/workflows/remove-workspace.md +++ b/gsd-core/workflows/remove-workspace.md @@ -19,7 +19,7 @@ INIT=$(gsd_run query init.remove-workspace "$WORKSPACE_NAME") if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Parse JSON for: `workspace_name`, `workspace_path`, `has_manifest`, `strategy`, `repos`, `repo_count`, `dirty_repos`, `has_dirty_repos`. diff --git a/gsd-core/workflows/resume-project.md b/gsd-core/workflows/resume-project.md index d1fbe113c..a8a1060bf 100644 --- a/gsd-core/workflows/resume-project.md +++ b/gsd-core/workflows/resume-project.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Use this workflow when: - Starting a new session on an existing project diff --git a/gsd-core/workflows/review.md b/gsd-core/workflows/review.md index 93e292c28..0b5a4222d 100644 --- a/gsd-core/workflows/review.md +++ b/gsd-core/workflows/review.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Cross-AI peer review — invoke external AI CLIs to independently review phase plans. Each CLI gets the same prompt (PROJECT.md context, phase plans, requirements) and diff --git a/gsd-core/workflows/scan.md b/gsd-core/workflows/scan.md index 691ba877b..550880d1a 100644 --- a/gsd-core/workflows/scan.md +++ b/gsd-core/workflows/scan.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Lightweight codebase assessment. Spawns a single gsd-codebase-mapper agent for one focus area, producing targeted documents in `.planning/codebase/`. diff --git a/gsd-core/workflows/secure-phase.md b/gsd-core/workflows/secure-phase.md index 2d623b40a..c2d59d9f3 100644 --- a/gsd-core/workflows/secure-phase.md +++ b/gsd-core/workflows/secure-phase.md @@ -23,7 +23,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi AGENT_SKILLS_AUDITOR=$(gsd_run query agent-skills gsd-security-auditor) ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`. diff --git a/gsd-core/workflows/session-report.md b/gsd-core/workflows/session-report.md index 4676f2891..ab867a38f 100644 --- a/gsd-core/workflows/session-report.md +++ b/gsd-core/workflows/session-report.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Generate a post-session summary document capturing work performed, outcomes achieved, and estimated resource usage. Writes SESSION_REPORT.md to .planning/reports/ for human review and stakeholder sharing. diff --git a/gsd-core/workflows/settings-advanced.md b/gsd-core/workflows/settings-advanced.md index 1ad7b733f..f4c329a20 100644 --- a/gsd-core/workflows/settings-advanced.md +++ b/gsd-core/workflows/settings-advanced.md @@ -1,3 +1,5 @@ +Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers. + Interactive configuration of GSD power-user knobs — plan bounce, node repair, subagent timeouts, inline plan threshold, cross-AI execution, base branch, branch templates, response language, diff --git a/gsd-core/workflows/settings-integrations.md b/gsd-core/workflows/settings-integrations.md index 332ef60ba..e6840d714 100644 --- a/gsd-core/workflows/settings-integrations.md +++ b/gsd-core/workflows/settings-integrations.md @@ -60,7 +60,7 @@ if [[ -z "${GSD_CONFIG_PATH:-}" ]]; then fi ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Store `$GSD_CONFIG_PATH`. Every subsequent read/write uses it. diff --git a/gsd-core/workflows/settings.md b/gsd-core/workflows/settings.md index b6c554ac4..1eed59525 100644 --- a/gsd-core/workflows/settings.md +++ b/gsd-core/workflows/settings.md @@ -28,7 +28,7 @@ if [[ -z "${GSD_CONFIG_PATH:-}" ]]; then fi ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Creates `config.json` (at the resolved path) with defaults if missing. `INIT` still holds `state.load` output for any step that needs STATE fields. Store `$GSD_CONFIG_PATH` — all subsequent reads and writes use this path, not a hardcoded `.planning/config.json`, so active-workstream installs target the correct file (#2282). diff --git a/gsd-core/workflows/ship.md b/gsd-core/workflows/ship.md index 2cfde4b49..dc9d7d7f1 100644 --- a/gsd-core/workflows/ship.md +++ b/gsd-core/workflows/ship.md @@ -30,7 +30,7 @@ INIT=$(gsd_run query init.phase-op "${PHASE_ARG}") if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Parse from init JSON: `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`, `commit_docs`. diff --git a/gsd-core/workflows/sketch-wrap-up.md b/gsd-core/workflows/sketch-wrap-up.md index 18f5d9b2c..e4bb2a037 100644 --- a/gsd-core/workflows/sketch-wrap-up.md +++ b/gsd-core/workflows/sketch-wrap-up.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Curate sketch design findings and package them into a persistent project skill for future UI implementation. Reads from `.planning/sketches/`, writes skill to `./.claude/skills/sketch-findings-[project]/` diff --git a/gsd-core/workflows/sketch.md b/gsd-core/workflows/sketch.md index bb0e53574..4ce301632 100644 --- a/gsd-core/workflows/sketch.md +++ b/gsd-core/workflows/sketch.md @@ -102,7 +102,7 @@ RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default " COMMIT_DOCS=$(gsd_run query config-get commit_docs --raw 2>/dev/null || echo "true") ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. diff --git a/gsd-core/workflows/smart-entry.md b/gsd-core/workflows/smart-entry.md index 44e758244..e9bb4b2c5 100644 --- a/gsd-core/workflows/smart-entry.md +++ b/gsd-core/workflows/smart-entry.md @@ -34,7 +34,7 @@ RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default " SNAPSHOT=$(gsd_run smart-entry --json 2>/dev/null) ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Parse `SNAPSHOT` as JSON. It has the shape: diff --git a/gsd-core/workflows/spec-phase.md b/gsd-core/workflows/spec-phase.md index 423f9d075..3a5ecd29a 100644 --- a/gsd-core/workflows/spec-phase.md +++ b/gsd-core/workflows/spec-phase.md @@ -63,7 +63,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi Parse JSON for: `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `state_path`, `requirements_path`, `roadmap_path`, `planning_path`, `response_language`, `commit_docs`. -**If `response_language` is set:** All user-facing text in this workflow MUST be in `{response_language}`. Technical terms, code, and file paths stay in English. +**If `response_language` is set:** All user-facing text in this workflow — narration between tool calls, status updates, progress notes, findings, questions, and report prose — MUST be in `{response_language}`. Technical terms, code, and file paths stay in English. **If `phase_found` is false:** ``` diff --git a/gsd-core/workflows/spike-wrap-up.md b/gsd-core/workflows/spike-wrap-up.md index 2bae5e9ab..cd3421e21 100644 --- a/gsd-core/workflows/spike-wrap-up.md +++ b/gsd-core/workflows/spike-wrap-up.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Package spike experiment findings into a persistent project skill — an implementation blueprint for future build conversations. Reads from `.planning/spikes/`, writes skill to diff --git a/gsd-core/workflows/spike.md b/gsd-core/workflows/spike.md index f8015c9de..fb75263eb 100644 --- a/gsd-core/workflows/spike.md +++ b/gsd-core/workflows/spike.md @@ -18,7 +18,7 @@ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-pars RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default "" 2>/dev/null || echo "") ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. ``` diff --git a/gsd-core/workflows/stats.md b/gsd-core/workflows/stats.md index 8c89eb83d..97d04bd65 100644 --- a/gsd-core/workflows/stats.md +++ b/gsd-core/workflows/stats.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + Display comprehensive project statistics including phases, plans, requirements, git metrics, and timeline. diff --git a/gsd-core/workflows/sync-skills.md b/gsd-core/workflows/sync-skills.md index cf3e17368..032c0cd3f 100644 --- a/gsd-core/workflows/sync-skills.md +++ b/gsd-core/workflows/sync-skills.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + # sync-skills — Cross-Runtime GSD Skill Sync **Command:** `/gsd-sync-skills` diff --git a/gsd-core/workflows/thread.md b/gsd-core/workflows/thread.md index 7284106d6..6067bb285 100644 --- a/gsd-core/workflows/thread.md +++ b/gsd-core/workflows/thread.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + # Thread Workflow Invoked by `/gsd:thread` (`commands/gsd/thread.md`). diff --git a/gsd-core/workflows/transition.md b/gsd-core/workflows/transition.md index 74830401b..23901a761 100644 --- a/gsd-core/workflows/transition.md +++ b/gsd-core/workflows/transition.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + **This is an INTERNAL workflow — NOT a user-facing command.** diff --git a/gsd-core/workflows/ui-phase.md b/gsd-core/workflows/ui-phase.md index f94482fa6..a958698b1 100644 --- a/gsd-core/workflows/ui-phase.md +++ b/gsd-core/workflows/ui-phase.md @@ -28,7 +28,7 @@ AGENT_SKILLS_UI_CHECKER=$(gsd_run query agent-skills gsd-ui-checker) Parse JSON for: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_context`, `has_research`, `commit_docs`, `response_language`. -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. **File paths:** `state_path`, `roadmap_path`, `requirements_path`, `context_path`, `research_path`. diff --git a/gsd-core/workflows/ui-review.md b/gsd-core/workflows/ui-review.md index c27cb6f26..f935f5fff 100644 --- a/gsd-core/workflows/ui-review.md +++ b/gsd-core/workflows/ui-review.md @@ -23,7 +23,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi AGENT_SKILLS_UI_REVIEWER=$(gsd_run query agent-skills gsd-ui-auditor) ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `commit_docs`. diff --git a/gsd-core/workflows/ultraplan-phase.md b/gsd-core/workflows/ultraplan-phase.md index d065fdac9..99c1bf39b 100644 --- a/gsd-core/workflows/ultraplan-phase.md +++ b/gsd-core/workflows/ultraplan-phase.md @@ -1,3 +1,5 @@ +@~/.claude/gsd-core/references/response-language-directive.md + # Ultraplan Phase Workflow [BETA] Offload GSD's plan phase to Claude Code's ultraplan cloud infrastructure. diff --git a/gsd-core/workflows/undo.md b/gsd-core/workflows/undo.md index 71328f50b..1d1ee962c 100644 --- a/gsd-core/workflows/undo.md +++ b/gsd-core/workflows/undo.md @@ -13,7 +13,7 @@ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-pars RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default "" 2>/dev/null || echo "") ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Display the stage banner: diff --git a/gsd-core/workflows/update.md b/gsd-core/workflows/update.md index 569417430..83b510299 100644 --- a/gsd-core/workflows/update.md +++ b/gsd-core/workflows/update.md @@ -7,7 +7,7 @@ Read all files referenced by the invoking prompt's execution_context before star -**If `response_language` is configured:** All user-facing questions, prompts, and explanations in this workflow MUST be presented in that language. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. +**If `response_language` is configured:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in that language. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Detect the installed GSD version, scope, runtime, and config dir. diff --git a/gsd-core/workflows/validate-phase.md b/gsd-core/workflows/validate-phase.md index 288621589..f52bc91bc 100644 --- a/gsd-core/workflows/validate-phase.md +++ b/gsd-core/workflows/validate-phase.md @@ -23,7 +23,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi AGENT_SKILLS_AUDITOR=$(gsd_run query agent-skills gsd-nyquist-auditor) ``` -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`. diff --git a/gsd-core/workflows/verify-work.md b/gsd-core/workflows/verify-work.md index e87682e81..8805756a5 100644 --- a/gsd-core/workflows/verify-work.md +++ b/gsd-core/workflows/verify-work.md @@ -50,7 +50,7 @@ AGENT_SKILLS_CHECKER=$(gsd_run query agent-skills gsd-plan-checker) Parse JSON for: `planner_model`, `checker_model`, `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `has_verification`, `uat_path`, `state_path`, `roadmap_path`, `response_language`. -**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. +**If `response_language` is set:** All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. ```bash # MVP mode detection via the centralized phase.mvp-mode resolver. diff --git a/package.json b/package.json index a5fed3811..f29bb1242 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,8 @@ "lint:table-schema-drift": "node scripts/lint-table-schema-drift.cjs", "lint:frontmatter-scalar-broad-grep": "node scripts/lint-frontmatter-scalar-broad-grep.cjs", "lint:removed-but-needed": "node scripts/lint-removed-but-needed.cjs", - "lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/lint-portable-timeout.cjs && node scripts/lint-portable-grep.cjs && node scripts/validate-registry.cjs && node scripts/lint-table-schema-drift.cjs && node scripts/lint-fix-has-regression-tests.cjs && node scripts/lint-example-parser-parity.cjs && node scripts/lint-docs-command-form.cjs && node scripts/lint-plan-count-drift.cjs && node scripts/lint-milestone-window-drift.cjs && node scripts/lint-phase-enumeration-drift.cjs && node scripts/lint-planning-prompt-drift.cjs && node scripts/lint-unreachable-guard-drift.cjs && node scripts/lint-completion-ratio-drift.cjs && node scripts/lint-slug-derivation-drift.cjs && node scripts/lint-state-field-drift.cjs && node scripts/lint-state-write-path-drift.cjs && node scripts/lint-completion-predicate-drift.cjs && node scripts/lint-planning-snapshot-bypass-drift.cjs && node scripts/lint-health-diagnostic-rule-table.cjs && node scripts/lint-planning-artifact-writer-drift.cjs && node scripts/lint-frontmatter-scalar-broad-grep.cjs && node scripts/lint-removed-but-needed.cjs && node scripts/lint-no-adhoc-regex-escape.cjs && node scripts/lint-vendored-deps.cjs && node scripts/lint-docs-guard-registration.cjs && node scripts/lint-source-test-name-collision.cjs && npm run lint:hooks-runtime-build-seam && node scripts/check-contract-drift.cjs && node scripts/lint-mutation-test-derivation-drift.cjs && node scripts/lint-seam-enforcement.cjs && node scripts/lint-workflow-shellcheck.cjs", + "lint:response-language": "node scripts/lint-response-language-coverage.cjs", + "lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/lint-portable-timeout.cjs && node scripts/lint-portable-grep.cjs && node scripts/validate-registry.cjs && node scripts/lint-table-schema-drift.cjs && node scripts/lint-fix-has-regression-tests.cjs && node scripts/lint-example-parser-parity.cjs && node scripts/lint-docs-command-form.cjs && node scripts/lint-plan-count-drift.cjs && node scripts/lint-milestone-window-drift.cjs && node scripts/lint-phase-enumeration-drift.cjs && node scripts/lint-planning-prompt-drift.cjs && node scripts/lint-unreachable-guard-drift.cjs && node scripts/lint-completion-ratio-drift.cjs && node scripts/lint-slug-derivation-drift.cjs && node scripts/lint-state-field-drift.cjs && node scripts/lint-state-write-path-drift.cjs && node scripts/lint-completion-predicate-drift.cjs && node scripts/lint-planning-snapshot-bypass-drift.cjs && node scripts/lint-health-diagnostic-rule-table.cjs && node scripts/lint-planning-artifact-writer-drift.cjs && node scripts/lint-frontmatter-scalar-broad-grep.cjs && node scripts/lint-removed-but-needed.cjs && node scripts/lint-no-adhoc-regex-escape.cjs && node scripts/lint-vendored-deps.cjs && node scripts/lint-docs-guard-registration.cjs && node scripts/lint-source-test-name-collision.cjs && npm run lint:hooks-runtime-build-seam && node scripts/check-contract-drift.cjs && node scripts/lint-mutation-test-derivation-drift.cjs && node scripts/lint-seam-enforcement.cjs && node scripts/lint-workflow-shellcheck.cjs && npm run lint:response-language", "lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs", "lint:regression-names": "node scripts/lint-regression-test-names.cjs", "lint:descriptions": "node scripts/lint-descriptions.cjs", diff --git a/scripts/docs-guard-registry.cjs b/scripts/docs-guard-registry.cjs index c029fa2bc..ed7efbf09 100644 --- a/scripts/docs-guard-registry.cjs +++ b/scripts/docs-guard-registry.cjs @@ -335,6 +335,16 @@ const DOCS_GUARD_TESTS = { ], 'tests/progress-forensic.test.cjs': ['docs/COMMANDS.md'], 'tests/repo-layout.test.cjs': ['docs/contributing/bootstrap.md'], + // Reads REQ-LANG-04 and runs every form it offers an author through the + // matcher that enforces it, so a reword of the requirement alone is exactly + // the change this guard must run on (#2529). Both paths are named because + // #3840 made docs/FEATURES.md a generated projection: the requirement's + // source is the fragment, and an edit there that is not regenerated would + // otherwise reach this guard through neither path. + 'tests/response-language-coverage.test.cjs': [ + 'docs/FEATURES.md', + 'docs/features/response-language-config.md', + ], 'tests/reversibility-tagging.test.cjs': ['docs/reference/plan-md.md'], 'tests/reviewer-docs-parity.test.cjs': [ 'docs/COMMANDS.md', diff --git a/scripts/lint-response-language-coverage.cjs b/scripts/lint-response-language-coverage.cjs new file mode 100644 index 000000000..95e590d09 --- /dev/null +++ b/scripts/lint-response-language-coverage.cjs @@ -0,0 +1,524 @@ +#!/usr/bin/env node +/** + * lint-response-language-coverage.cjs + * + * Enforces #2529: every workflow must be covered by the response-language + * contract, so a workflow can never ship English-only when the user has + * configured `response_language`. + * + * A workflow file passes when it contains EITHER: + * - a reference to the shared directive + * (`references/response-language-directive.md`), OR + * - its own inline `response_language` directive (the ~half of the catalog + * that already carried one before #2529, plus workflow-specific extracts + * like `references/execute-phase-response-language.md`). + * + * A bare config-field mention is not coverage: the same line must direct how + * user-facing output is rendered, or the workflow must load a known directive. + * + * Nor is a directive that names only the question/answer surface. #2529's stated + * defect is that inter-tool NARRATION stays in English while the answers around + * it are translated, so a line saying "all user-facing questions, prompts, and + * explanations" describes the gap rather than closing it. Coverage therefore + * requires a narration-class token as well — see `NARRATION_CLASS_RE`. + * + * A workflow FRAGMENT (`//.md`, the shape + * the #1671 fragment epic extracts) additionally passes when its parent workflow + * names that exact fragment path and is itself covered — see + * `inheritsParentCoverage`, which proves the inheritance per file instead of + * granting it to a directory. + * + * Exit 0 only when workflows were actually found AND every one of them is + * covered; exit 1 with a per-file listing if not. "No violations" alone is not + * a pass: a run that inspected nothing has established nothing. + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.join(__dirname, '..'); +const WORKFLOWS_DIR = path.join(ROOT, 'gsd-core', 'workflows'); +// A reference resolves as a sibling of the workflow catalog, so a fixture tree +// and the real one resolve by one rule. +const REFERENCE_ROOT = path.join(ROOT, 'gsd-core'); +const DIRECTIVE_REFS = [ + 'references/response-language-directive.md', + 'references/execute-phase-response-language.md', +]; +// Names the narration class explicitly for the same reason the inline directives +// in the workflow bodies do (see NARRATION_CLASS_RE): "user-facing prose" alone +// reads, in practice, as the question/answer surface, which is the half of the +// output #2529 was never about. +const INLINE_RESPONSE_LANGUAGE_DIRECTIVE = + 'Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers.'; +// THE RULE THAT DECIDES THIS SET (#2529, restated in review round 29). +// A lazy-loaded mode/step/template carries its own directive only when it cannot +// PROVE inheritance -- no parent dispatches it from a read/execute context, or the +// parent is itself uncovered. Where inheritance is proven, the parent's directive is +// already in the loaded context by the time the fragment is read, so a second copy +// buys no coverage and adds a sentence that can drift. Pinning the exact wording is +// what makes the first case safe: these files cannot take the eager @-reference (an +// @-line inside a later Read is inert), so the sentence lives inline, and a partial +// typo or rewording would otherwise split the contract silently. +// enforces the rule in both directions: +// no member of this set may be one that would inherit anyway. +const EXACT_INLINE_DIRECTIVE_WORKFLOWS = new Set([ + 'discuss-phase/modes/advisor.md', + 'discuss-phase/modes/all.md', + 'discuss-phase/modes/analyze.md', + 'discuss-phase/modes/auto.md', + 'discuss-phase/modes/batch.md', + 'discuss-phase/modes/chain.md', + 'discuss-phase/modes/default.md', + 'discuss-phase/modes/power.md', + 'discuss-phase/modes/text.md', + 'discuss-phase/templates/context.md', + 'discuss-phase/templates/discussion-log.md', + 'execute-phase/steps/codebase-drift-gate.md', + 'execute-phase/steps/regression-gate-run.md', + 'execute-phase/steps/worktree-recovery-policy.md', + 'help/modes/brief.md', + 'help/modes/default.md', + 'help/modes/full.md', + 'help/modes/topic.md', + 'plan-phase/steps/prd-express-path.md', + 'quick-batch/steps/plan-checker-loop.md', + 'settings-advanced.md', +]); +// This lint once carried a third coverage form, for `verify-phase.md`: a +// workflow file nothing entered directly, covered instead by the directive +// execute-phase.md injects into its `gsd-verifier` dispatch prompt. `next` +// deleted that workflow in #3421 (an orphan that shipped ~40 KB to every runtime +// and was never loaded) and migrated its live gates into the verifier, so the +// form has no subject left and is gone with it. +// +// The dispatch contract itself is NOT gone — the verifier subagent still runs and +// still emits user-facing prose, so `references/execute-phase-response-language.md` +// still tells the orchestrator to carry the directive into that prompt. It is +// simply no longer a statement about any file in this catalog, which is all this +// lint reads. `tests/response-language-coverage.test.cjs` pins both ends of it +// (the reference's directive text and the `Create VERIFICATION.md.` anchor it +// positions against in execute-phase.md) directly against the real tree. +// Fragment directories produced by the workflow-fragment epic (#1671). A file +// under one of these is a SECTION of its parent workflow, never an entry point: +// it is reached by a `read and execute gsd-core/workflows/` stub, which +// fires with the parent — and therefore the parent's response-language +// directive — already loaded. +// +// The stub is USUALLY in the top-level parent, which is the only case +// `inheritsParentCoverage` can prove, and it deliberately proves no more. Four +// fragments arrive by a different route today, and none of them relies on this +// function: `execute-phase/steps/regression-gate-run.md`, +// `plan-phase/steps/prd-express-path.md` and +// `quick-batch/steps/plan-checker-loop.md` are dispatched by a SIBLING fragment +// (`regression-gate.md`, `prd-express-gate.md`, `planner-wave.md`), and +// `help/modes/topic.md` is routed from a table in `help.md` that names the path +// without a dispatch verb at all. All four carry the pinned inline directive and +// are listed in `EXACT_INLINE_DIRECTIVE_WORKFLOWS`, so `findViolations` settles +// them before inheritance is ever consulted. +// +// Left as one hop on purpose. Walking the sibling chain would let a fragment +// inherit through a file this lint has not proven reachable, trading a loud +// failure for a quiet assumption; as it stands, extracting a fragment-of-a- +// fragment without pinning it turns the lint RED, which is the correct answer +// and names the file to fix. +const FRAGMENT_DIRS = new Set(['modes', 'steps', 'templates']); +const DIRECTIVE_ACTION_RE = /\b(?:apply|present|render|respond|translate|use|write|must|should)\b/i; +const USER_OUTPUT_RE = /\b(?:explanations?|language|narration|outputs?|prompts?|prose|questions?|templates?|user-facing)\b/i; +// The defect #2529 reports is NARRATION, not the question/answer surface: a +// directive worded as "all user-facing questions, prompts, and explanations" is +// read as covering what the user is asked and told at a turn boundary, and the +// running commentary the model emits between tool calls stays in English beside +// it. A line that names only that surface therefore certifies the exact gap the +// issue exists to close, so coverage additionally requires a narration-class +// token — narration, status, progress, findings, or output "between tool calls". +// Round 21 tightening: the accepted forms are the two that NAME the class — the +// word "narration", or output described as running "between tool calls". The +// earlier list also accepted a bare "status", "progress" or "findings", which +// are words that merely APPEAR in the canonical phrasing: a line reading "Use +// response_language for all user-facing output; report status." passed while +// naming nothing about inter-tool narration, i.e. weaker than REQ-LANG-04 +// requires. Verified before tightening: all 79 catalog files that passed under +// the old list still pass under this one, so no workflow needed rewording — the +// dropped tokens were reachable only by directives nobody ships. +// Still deliberately not a phrase match: pinning one sentence would push authors +// toward copying wording instead of stating the rule (`tests/response-language-coverage.test.cjs` +// pins the old weak wording as a FAILING case so this cannot silently loosen). +const NARRATION_CLASS_RE = /\bnarration\b|\bbetween tool calls\b/i; +// The catalog's extensions, matched case-insensitively and by whole extension. +// `endsWith('.md')` failed in the one direction this lint cannot afford: it is +// silent UNDER-enforcement, the same vacuous pass `main()` refuses when +// discovery returns nothing. `SETTINGS.MD` is the same file to Windows and +// macOS and a different one to Linux, so a case-sensitive test exempts it on +// the only platform whose verdict gates the merge — a workflow certified by +// having gone unseen. +// `.mdx` stays out deliberately. Admitting an extension states what a workflow +// IS, and that claim has a second half: `inheritsParentCoverage` resolves a +// fragment's parent as `.md`. If the repo ever emits an `.mdx` +// workflow, the entry belongs here next to the parent resolution it must move +// with, not ahead of it. +const WORKFLOW_EXTENSIONS = new Set(['.md']); + +function isWorkflowFile(name) { + return WORKFLOW_EXTENSIONS.has(path.extname(name).toLowerCase()); +} + +/** + * Walk the catalog, FOLLOWING symlinked subtrees. + * + * `Dirent` uses lstat semantics, so a symlinked directory answers false to both + * `isDirectory()` and `isFile()`. Branching on those alone skipped a symlinked + * subtree in silence while `files.length > 0` kept the run green — the exact + * "reported OK while coverage regressed" outcome `main()` claims to make + * impossible. Each symlink is resolved with `statSync` and followed. + * + * Cycles are bounded by the `seen` set of resolved real paths: a link pointing + * at an ancestor is entered once and not re-entered. A broken link resolves to + * nothing and is skipped — it contributes no file to inspect, and no coverage + * claim rides on it. + */ +function findMarkdownFilesRecursive(dir, seen = new Set()) { + const files = []; + let real; + try { real = fs.realpathSync(dir); } catch { real = dir; } + if (seen.has(real)) return files; + seen.add(real); + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + let isDirectory = entry.isDirectory(); + let isFile = entry.isFile(); + if (entry.isSymbolicLink()) { + let stat; + try { stat = fs.statSync(full); } catch { continue; } + isDirectory = stat.isDirectory(); + isFile = stat.isFile(); + } + if (isDirectory) files.push(...findMarkdownFilesRecursive(full, seen)); + else if (isFile && isWorkflowFile(entry.name)) files.push(full); + } + return files.sort(); +} + +/** + * Does the file LOAD a shared directive, rather than merely name one? + * + * The distinction is the same one `namesFragmentAsEntryPoint` draws a level + * down, and it matters for the same reason: only a real `@`-import puts the + * directive in context. A changelog line ("#2529: added the shared + * response-language-directive.md reference"), a deprecation note, or any prose + * naming the path proves nothing about what loads — but a bare substring test + * cannot tell those apart, so such a workflow would be certified covered while + * shipping the very defect #2529 exists to close. + * + * The catalog emits the import exactly one way: an `@`-path occupying the whole + * line — line 1 for the 43 top-level workflows, line 96 for `execute-phase.md`. + * Both `@~/` and `@$HOME/` spellings are accepted because the repo writes both + * (see `scripts/strip-prose-atrefs.cjs`). + * + * LIMITATION, stated rather than papered over: like the fragment check, this + * reads Markdown as text. An import inside a fenced block quoted as an example + * still counts, and one whose line carries a trailing `` no longer + * does. Neither shape occurs in the catalog today, and both would be visible in + * review as a quoted or edited import. + */ +const DIRECTIVE_IMPORT_RES = DIRECTIVE_REFS.map( + (ref) => new RegExp(`^@(?:~|\\$HOME)/\\S*${ref.replaceAll('.', '\\.')}$`), +); + +function importsDirectiveReference(content) { + return importedDirectiveReference(content) !== null; +} + +/** Which known reference does this file import, if any? */ +function importedDirectiveReference(content) { + const lines = content.split(/\r?\n/).map((line) => line.trim()); + for (const [index, ref] of DIRECTIVE_REFS.entries()) { + if (lines.some((line) => DIRECTIVE_IMPORT_RES[index].test(line))) return ref; + } + return null; +} + +/** + * The four-predicate test for an actionable directive on a single line. + * + * LIMITATION (text, not AST): the four hits are independent, so the predicate + * reads vocabulary and not polarity. A line that negates the instruction — + * "Do not translate narration; apply response_language to code only." — satisfies + * all four and reads as coverage. Contrived rather than plausible: the wording is + * pinned byte-for-byte in the files that cannot take the reference, and the shared + * reference is validated against this same predicate, so a negated line would have + * to be authored deliberately in one of the 44 files that carry their own sentence. + * Named here because a predicate that cannot see "not" should say so. + */ +function carriesInlineDirective(content) { + return content.split(/\r?\n/).some((line) => + /\bresponse_language\b/i.test(line) && + DIRECTIVE_ACTION_RE.test(line) && + USER_OUTPUT_RE.test(line) && + NARRATION_CLASS_RE.test(line) + ); +} + +/** + * Does the SHARED FILE a workflow imports itself carry an actionable directive? + * + * Checking only that the `@`-import LINE exists is a hole the size of the + * bucket: 43 workflows take their entire coverage from one file, and nothing in + * the repo pinned that file's text. Rewriting it back to the pre-#2529 wording — + * the sentence this PR's own round-10 finding calls the DEFECT — left the lint + * green and every workflow "covered". That is the same "the gate certifies the + * defect" failure the round-10 fix closed, one level up. + * + * So an import is coverage only when the referenced file passes the very + * predicate an inline directive must pass. A missing, unreadable, or weakened + * reference covers nobody, and `main()` reports it as one systemic failure + * rather than as 43 identical per-file violations. + */ +// Keyed by identity AND version, not by path alone. A path-only key is correct +// for a one-shot CLI and for the tests, where every fixture gets a fresh mkdtemp +// root — but it makes the verdict for a path permanent within the process, so a +// caller that rewrites a reference and re-asks gets the stale answer. Stamping +// size and mtime into the key costs one stat and removes the class rather than +// documenting it. +const referenceDirectiveCache = new Map(); +function referenceCarriesDirective(ref, refRoot) { + const file = path.join(refRoot, ref); + let stamp = 'missing'; + try { + const stat = fs.statSync(file); + stamp = `${stat.size}:${stat.mtimeMs}`; + } catch { /* keep the missing stamp; the read below decides the verdict */ } + const key = [refRoot, ref, stamp].join('\u0000'); + if (!referenceDirectiveCache.has(key)) { + let ok = false; + try { ok = carriesInlineDirective(fs.readFileSync(file, 'utf8')); } + catch { ok = false; } + referenceDirectiveCache.set(key, ok); + } + return referenceDirectiveCache.get(key); +} + +/** + * Of the references these files actually import, which are missing, unreadable, + * or no longer actionable? + * + * Scoped to imported references on purpose: a reference nobody imports cannot + * uncover anybody, so failing on it would red a run over a file it never + * consulted. Every reference that IS imported is load-bearing for every workflow + * that imports it. + */ +function findBrokenDirectiveReferences(files, refRoot = REFERENCE_ROOT) { + const imported = new Set(); + for (const file of files) { + let content; + try { content = fs.readFileSync(file, 'utf8'); } catch { continue; } + const ref = importedDirectiveReference(content); + if (ref !== null) imported.add(ref); + } + return [...imported].filter((ref) => !referenceCarriesDirective(ref, refRoot)).sort(); +} + +function hasResponseLanguageCoverage(content, refRoot = REFERENCE_ROOT) { + const ref = importedDirectiveReference(content); + if (ref !== null) return referenceCarriesDirective(ref, refRoot); + + return carriesInlineDirective(content); +} + +/** + * How far a read/execute verb may sit from the fragment path it governs on the + * same line. Sized from the widest shape the catalog actually emits — + * ``read and execute `gsd-core/workflows/...` `` (14 characters between verb and + * path) — with room for a variant, and deliberately far short of a sentence, so + * a verb belonging to a different clause cannot reach across and vouch for a + * path it never dispatches. + */ +const ENTRY_POINT_VERB_WINDOW = 40; +const ENTRY_POINT_VERB_RE = new RegExp(`\\b(?:read|execute|run)\\b.{0,${ENTRY_POINT_VERB_WINDOW}}$`, 'i'); + +/** + * Does the parent name this fragment as a live ENTRY POINT, rather than merely + * mentioning its path? + * + * The distinction is the whole basis of inherited coverage: the inheritance is + * sound only because the fragment cannot be reached except through the parent's + * dispatch stub, which fires with the parent's directive already loaded. A path + * that appears in a changelog line, a deprecation note, or a comment proves + * nothing about how the fragment is reached — but a bare substring test cannot + * tell those apart, so before this check any mention at all granted coverage. + * + * The catalog emits exactly one shape, in five spellings: ``read and execute + * `` ``, ``Read+execute `` ``, ``Read `` if ``, + * ``run `` to `` (the spelling #1689's per-plan executor routing + * introduced), and the same stub written with the path RELATIVE to the catalog + * rather than rooted at `gsd-core/workflows/` (#3552's `branching_strategy: + * none` arm). A read/execute/run verb within `ENTRY_POINT_VERB_WINDOW` + * characters ahead of the path on the SAME LINE covers all five. + * + * LIMITATION, stated rather than papered over: this reads Markdown as text, not + * as a parsed document. A dispatch stub that a future edit comments out with + * ``, or moves inside a fenced block quoted as an example, still + * matches — the verb and the path are both still there. Distinguishing those + * needs a Markdown parser, and the cheap approximations (tracking fence state, + * skipping `` guard, and nothing here checks that the + * guard is satisfiable or that its `id` still matches the fragment it gates. + * Every such pairing lines up today. If one drifts, the fragment becomes + * unreachable while this function still calls it dispatched — an orphan the lint + * reports as covered. That is not the defect #2529 is about: a dispatch that + * never fires renders nothing, so no English prose reaches the user through it. + * The gap is a soundness one, worth naming rather than implying. + */ +function namesFragmentAsEntryPoint(parent, relative) { + // Two spellings of the same dispatch: catalog-rooted and catalog-RELATIVE. + // #3552's `"none"` arm introduced the second one — `Read and execute + // `execute-phase/steps/protected-branch.md`` — and a rooted-only needle read + // that live dispatch as no dispatch at all. + const needles = [`gsd-core/workflows/${relative}`, relative]; + return parent.split(/\r?\n/).some((line) => needles.some((needle) => { + const at = line.indexOf(needle); + // A path character immediately before the match means this is the TAIL of + // some longer path, not the fragment itself: `vendor/` must not + // grant `` coverage. The rooted needle carries its own prefix and + // is matched on its own, so nothing legitimate is lost here. + if (at === -1 || (at > 0 && /[\w/.-]/.test(line.charAt(at - 1)))) return false; + return ENTRY_POINT_VERB_RE.test(line.slice(0, at)); + })); +} + +/** + * A fragment inherits its parent workflow's coverage, but only when the + * inheritance is PROVEN per file rather than assumed from the directory: + * 1. the path is `//.md`, + * 2. `.md` exists and dispatches this exact fragment path from a + * read/execute context — i.e. the parent really is the way in, not just a + * file that happens to spell the name (see `namesFragmentAsEntryPoint`), and + * 3. the parent is itself covered. + * A fragment nobody dispatches, or one hanging off an uncovered parent, stays a + * violation. Without this the lint reds on every new fragment the #1671 epic + * extracts, even though the extraction moved prose that was already covered. + */ +function inheritsParentCoverage(workflowsDir, relative, refRoot = REFERENCE_ROOT) { + const segments = relative.split('/'); + if (segments.length !== 3 || !FRAGMENT_DIRS.has(segments[1])) return false; + const parentPath = path.join(workflowsDir, `${segments[0]}.md`); + if (!fs.existsSync(parentPath)) return false; + const parent = fs.readFileSync(parentPath, 'utf8'); + if (!namesFragmentAsEntryPoint(parent, relative)) return false; + return hasResponseLanguageCoverage(parent, refRoot); +} + +function findViolations(workflowsDir, refRoot = REFERENCE_ROOT) { + return findMarkdownFilesRecursive(workflowsDir).filter((file) => { + const relative = path.relative(workflowsDir, file).replaceAll(path.sep, '/'); + const content = fs.readFileSync(file, 'utf8'); + if (EXACT_INLINE_DIRECTIVE_WORKFLOWS.has(relative)) { + if (content.split(/\r?\n/).includes(INLINE_RESPONSE_LANGUAGE_DIRECTIVE)) return false; + // The pin exists because these files cannot take the eager @-reference, not + // because the reference is worse. If one ever CAN take it -- a fragment that + // becomes eagerly loaded -- that conversion is strictly better than the pin and + // must not read as a violation. Only the reference form is admitted here: its + // wording is validated in turn by findBrokenDirectiveReferences, so the contract + // survives the swap. An arbitrary reworded inline line stays a violation, which + // is the whole point of pinning. + return !importsDirectiveReference(content) || !hasResponseLanguageCoverage(content, refRoot); + } + if (hasResponseLanguageCoverage(content, refRoot)) return false; + return !inheritsParentCoverage(workflowsDir, relative, refRoot); + }); +} + +function main(workflowsDir = WORKFLOWS_DIR, io = console, refRoot = REFERENCE_ROOT) { + // The catalog is discovered, not declared, so an unreadable or empty + // directory yields an empty violation list — indistinguishable from full + // coverage if the only success condition is `violations.length === 0`. Both + // discovery failures below are therefore lint failures in their own right: + // a stripped install tree, a `__dirname`-relative path typo, or an + // unfollowed symlink must not be able to report OK while coverage silently + // regresses to the pre-#2529 state. + let files; + try { + files = findMarkdownFilesRecursive(workflowsDir); + } catch (error) { + if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error; + io.error( + `lint-response-language-coverage: cannot read the workflow directory ${workflowsDir} (${error.code}).\n` + + `Coverage cannot be established, so this is a failure and not a pass (#2529).`, + ); + return 1; + } + + if (files.length === 0) { + io.error( + `lint-response-language-coverage: no workflow files found under ${workflowsDir}.\n` + + `A run that inspected zero workflows cannot establish coverage (#2529).`, + ); + return 1; + } + + // The shared references are checked ONCE, before the per-file pass. 43 + // workflows hold no directive of their own, so a reference that has gone + // missing or been reworded back to the pre-#2529 sentence is a single + // systemic failure, not 43 identical ones — and reporting it as 43 would bury + // the cause under its symptoms. + const brokenReferences = findBrokenDirectiveReferences(files, refRoot); + if (brokenReferences.length > 0) { + io.error( + 'lint-response-language-coverage: ' + brokenReferences.length + + ' shared directive reference(s) no longer carry an actionable directive (#2529, REQ-LANG-04).\n' + + 'Workflows that @-import a reference take ALL of their coverage from it, so a missing,\n' + + 'unreadable, or reworded reference silently uncovers every one of them:\n\n' + + brokenReferences.map((ref) => ' - ' + ref).join('\n') + '\n\n' + + 'The file must itself name the narration class on the same line as response_language\n' + + '— the same rule an inline directive must satisfy.', + ); + return 1; + } + + const violations = findViolations(workflowsDir, refRoot); + + if (violations.length > 0) { + io.error( + `lint-response-language-coverage: ${violations.length} workflow(s) have no response-language coverage (#2529).\n` + + `Each workflow must either @-reference a recognized response-language directive\n` + + `or carry its own inline \`response_language\` directive (unless its parent dispatches it).\n` + + `An inline directive names the narration class with the word "narration" or the phrase\n` + + `"between tool calls" (REQ-LANG-04); enumerating status updates, progress notes or\n` + + `findings without naming the class does not satisfy it:\n\n` + + violations.map((file) => ` - ${path.relative(workflowsDir, file).replaceAll(path.sep, '/')}`).join('\n'), + ); + return 1; + } + + io.log(`lint-response-language-coverage: OK (${files.length} workflows covered)`); + return 0; +} + +if (require.main === module) process.exitCode = main(); + +module.exports = { + EXACT_INLINE_DIRECTIVE_WORKFLOWS, + INLINE_RESPONSE_LANGUAGE_DIRECTIVE, + NARRATION_CLASS_RE, + WORKFLOW_EXTENSIONS, + WORKFLOWS_DIR, + REFERENCE_ROOT, + carriesInlineDirective, + findBrokenDirectiveReferences, + findMarkdownFilesRecursive, + findViolations, + hasResponseLanguageCoverage, + importedDirectiveReference, + importsDirectiveReference, + inheritsParentCoverage, + namesFragmentAsEntryPoint, + main, +}; diff --git a/tests/fixtures/install-tree/antigravity.json b/tests/fixtures/install-tree/antigravity.json index a75e3a6c5..6d47f693d 100644 --- a/tests/fixtures/install-tree/antigravity.json +++ b/tests/fixtures/install-tree/antigravity.json @@ -145,6 +145,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/augment.json b/tests/fixtures/install-tree/augment.json index 5cb1ce8e3..eaa1d2c41 100644 --- a/tests/fixtures/install-tree/augment.json +++ b/tests/fixtures/install-tree/augment.json @@ -217,6 +217,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/claude-local.json b/tests/fixtures/install-tree/claude-local.json index 3b4f0e576..963b04b4c 100644 --- a/tests/fixtures/install-tree/claude-local.json +++ b/tests/fixtures/install-tree/claude-local.json @@ -217,6 +217,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/claude.json b/tests/fixtures/install-tree/claude.json index 6c156cfd2..5141ad8c7 100644 --- a/tests/fixtures/install-tree/claude.json +++ b/tests/fixtures/install-tree/claude.json @@ -145,6 +145,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/cline.json b/tests/fixtures/install-tree/cline.json index b50ceb0e3..3b6f1516e 100644 --- a/tests/fixtures/install-tree/cline.json +++ b/tests/fixtures/install-tree/cline.json @@ -147,6 +147,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/codebuddy.json b/tests/fixtures/install-tree/codebuddy.json index 045eb7794..0996e51fd 100644 --- a/tests/fixtures/install-tree/codebuddy.json +++ b/tests/fixtures/install-tree/codebuddy.json @@ -217,6 +217,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/codex.json b/tests/fixtures/install-tree/codex.json index 9ebe80ce5..33aa642b4 100644 --- a/tests/fixtures/install-tree/codex.json +++ b/tests/fixtures/install-tree/codex.json @@ -181,6 +181,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/copilot.json b/tests/fixtures/install-tree/copilot.json index 8f9e129de..9766eaea1 100644 --- a/tests/fixtures/install-tree/copilot.json +++ b/tests/fixtures/install-tree/copilot.json @@ -146,6 +146,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/cursor.json b/tests/fixtures/install-tree/cursor.json index 3482b8351..00bed0210 100644 --- a/tests/fixtures/install-tree/cursor.json +++ b/tests/fixtures/install-tree/cursor.json @@ -145,6 +145,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/hermes.json b/tests/fixtures/install-tree/hermes.json index b9788288f..831d29fc6 100644 --- a/tests/fixtures/install-tree/hermes.json +++ b/tests/fixtures/install-tree/hermes.json @@ -145,6 +145,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/kilo.json b/tests/fixtures/install-tree/kilo.json index 2c629a91c..f82469e54 100644 --- a/tests/fixtures/install-tree/kilo.json +++ b/tests/fixtures/install-tree/kilo.json @@ -217,6 +217,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/kimi-code.json b/tests/fixtures/install-tree/kimi-code.json index 91348e616..91a396923 100644 --- a/tests/fixtures/install-tree/kimi-code.json +++ b/tests/fixtures/install-tree/kimi-code.json @@ -146,6 +146,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/kimi.json b/tests/fixtures/install-tree/kimi.json index 081c8b1c5..6d7c605f9 100644 --- a/tests/fixtures/install-tree/kimi.json +++ b/tests/fixtures/install-tree/kimi.json @@ -182,6 +182,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/opencode.json b/tests/fixtures/install-tree/opencode.json index 7c27ed6f0..edd6a769d 100644 --- a/tests/fixtures/install-tree/opencode.json +++ b/tests/fixtures/install-tree/opencode.json @@ -217,6 +217,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/pi.json b/tests/fixtures/install-tree/pi.json index 9f702e912..e5d8a814e 100644 --- a/tests/fixtures/install-tree/pi.json +++ b/tests/fixtures/install-tree/pi.json @@ -112,6 +112,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/qwen.json b/tests/fixtures/install-tree/qwen.json index 8434d37de..d39fb7c88 100644 --- a/tests/fixtures/install-tree/qwen.json +++ b/tests/fixtures/install-tree/qwen.json @@ -145,6 +145,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/trae.json b/tests/fixtures/install-tree/trae.json index 4d32fd53c..26de4b764 100644 --- a/tests/fixtures/install-tree/trae.json +++ b/tests/fixtures/install-tree/trae.json @@ -145,6 +145,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/windsurf.json b/tests/fixtures/install-tree/windsurf.json index 4e865e373..df74944a9 100644 --- a/tests/fixtures/install-tree/windsurf.json +++ b/tests/fixtures/install-tree/windsurf.json @@ -145,6 +145,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/fixtures/install-tree/zcode.json b/tests/fixtures/install-tree/zcode.json index 13133b274..7dcc77b58 100644 --- a/tests/fixtures/install-tree/zcode.json +++ b/tests/fixtures/install-tree/zcode.json @@ -217,6 +217,7 @@ "gsd-core/references/research-documentation-lookup.md", "gsd-core/references/research-philosophy.md", "gsd-core/references/research-verification-protocol.md", + "gsd-core/references/response-language-directive.md", "gsd-core/references/reviewer-instances.md", "gsd-core/references/revision-loop.md", "gsd-core/references/runtime-aware-dispatch.md", diff --git a/tests/gsd-quick-batch-quick-regression.test.cjs b/tests/gsd-quick-batch-quick-regression.test.cjs index cd7cde53d..6ab47d9d6 100644 --- a/tests/gsd-quick-batch-quick-regression.test.cjs +++ b/tests/gsd-quick-batch-quick-regression.test.cjs @@ -22,10 +22,35 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const { + git, resolveBase, resolveChangedPaths, baseRefCandidates, } = require('./helpers/emitted-runtime.cjs'); +const { + INLINE_RESPONSE_LANGUAGE_DIRECTIVE, + importsDirectiveReference, +} = require('../scripts/lint-response-language-coverage.cjs'); + +/** + * Does this path's diff say anything beyond the shared response-language + * directive (#2529)? + * + * The two accepted forms are read from `scripts/lint-response-language-coverage.cjs` + * rather than restated here, so a reworded contract cannot leave this carve-out + * matching prose the lint itself no longer recognizes as coverage. A file the + * branch ADDED answers true, every line being new — which is what a real + * #3676-phase branch looks like. + */ +function editsBeyondSharedDirective(base, file) { + const diff = git(['diff', '--unified=0', `${base}...HEAD`, '--', file]); + return diff.split('\n').some((line) => { + if (!/^[+-]/.test(line) || line.startsWith('+++') || line.startsWith('---')) return false; + const body = line.slice(1).trim(); + if (body === '' || body === INLINE_RESPONSE_LANGUAGE_DIRECTIVE) return false; + return !importsDirectiveReference(body); + }); +} describe('quick-batch: /gsd:quick command + workflow stay byte-identical (row 48)', () => { test('commands/gsd/quick.md and gsd-core/workflows/quick.md are not in this branch\'s changed-path set', (t) => { @@ -51,25 +76,41 @@ describe('quick-batch: /gsd:quick command + workflow stay byte-identical (row 48 // branch's quick.md edit is none of this guard's business. // tests/ excluded: this guard file's own name matches, which would make // the scope check self-satisfying on every branch that edits it. - const touchesQuickBatch = changed.some((p) => /quick-batch/.test(p) && !p.startsWith('tests/')); + // #2529 review round 40: a branch can touch the whole quick-batch surface + // without being #3676 phase work. The response-language coverage sweep adds + // one shared directive line to EVERY workflow, quick-batch.md and its + // fragments included, which made this scope check true — and the row then + // read that branch's ordinary-quick edit, the same one directive line, as a + // phase violation. That is the false positive the #3730 note already scoped + // this row away from, arriving by the other door: not a branch that misses + // the surface, but one that touches all of it. So a path counts only when + // its diff says something other than the coverage contract. + const isPhaseWork = (p) => editsBeyondSharedDirective(resolved.ref, p); + const touchesQuickBatch = changed.some((p) => + /quick-batch/.test(p) && !p.startsWith('tests/') && isPhaseWork(p)); if (!touchesQuickBatch) { t.skip( - `branch does not touch the quick-batch surface (${changed.length} changed paths) — ` + + `branch does no quick-batch phase work (${changed.length} changed paths; any ` + + 'quick-batch path it touches carries only the shared response-language directive) — ' + 'row 48 governs #3676-phase branches only', ); return; } + // Same reading on both sides of the row: a directive-only edit is the + // coverage contract every workflow carries, not a quick-batch edit. assert.ok( - !changed.includes('commands/gsd/quick.md'), + !(changed.includes('commands/gsd/quick.md') && isPhaseWork('commands/gsd/quick.md')), 'commands/gsd/quick.md must stay untouched by the #3676 quick-batch phase', ); assert.ok( - !changed.includes('gsd-core/workflows/quick.md'), + !(changed.includes('gsd-core/workflows/quick.md') && isPhaseWork('gsd-core/workflows/quick.md')), 'gsd-core/workflows/quick.md must stay untouched by the #3676 quick-batch phase', ); // The step fragments under quick/steps/ are likewise untouched — quick-batch // has its own, separate quick-batch/steps/ tree. - const touchedQuickSteps = changed.filter((p) => p.startsWith('gsd-core/workflows/quick/steps/')); + const touchedQuickSteps = changed + .filter((p) => p.startsWith('gsd-core/workflows/quick/steps/')) + .filter(isPhaseWork); assert.deepEqual(touchedQuickSteps, [], `unexpected changes under gsd-core/workflows/quick/steps/: ${touchedQuickSteps.join(', ')}`); }); }); diff --git a/tests/response-language-coverage.test.cjs b/tests/response-language-coverage.test.cjs new file mode 100644 index 000000000..e4c5d988b --- /dev/null +++ b/tests/response-language-coverage.test.cjs @@ -0,0 +1,918 @@ +'use strict'; + +const { afterEach, describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { cleanup } = require('./helpers.cjs'); +const fc = require('./helpers/fast-check-setup.cjs'); + +const { + EXACT_INLINE_DIRECTIVE_WORKFLOWS, + INLINE_RESPONSE_LANGUAGE_DIRECTIVE, + REFERENCE_ROOT, + WORKFLOW_EXTENSIONS, + WORKFLOWS_DIR, + carriesInlineDirective, + findBrokenDirectiveReferences, + findMarkdownFilesRecursive, + findViolations, + hasResponseLanguageCoverage, + inheritsParentCoverage, + namesFragmentAsEntryPoint, + main, +} = require('../scripts/lint-response-language-coverage.cjs'); + +describe('response-language workflow coverage lint (#2529)', () => { + const tempDirs = []; + + afterEach(() => { + for (const dir of tempDirs.splice(0)) cleanup(dir); + }); + + function fixture() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-response-language-')); + tempDirs.push(root); + fs.mkdirSync(path.join(root, 'nested', 'modes'), { recursive: true }); + fs.writeFileSync( + path.join(root, 'covered-by-reference.md'), + '@~/.claude/gsd-core/references/response-language-directive.md\n', + ); + fs.writeFileSync( + path.join(root, 'nested', 'covered-inline.md'), + 'Use config.response_language for all prose, narration included.\n', + ); + fs.writeFileSync( + path.join(root, 'nested', 'mere-field-mention.md'), + 'Parse JSON for: phase_number, response_language.\n', + ); + fs.writeFileSync(path.join(root, 'nested', 'modes', 'uncovered.md'), '# English-only mode\n'); + fs.writeFileSync(path.join(root, 'nested', 'ignored.txt'), 'not a workflow'); + return root; + } + + test('walks nested workflow directories recursively and ignores non-Markdown files', () => { + const root = fixture(); + const relative = findMarkdownFilesRecursive(root) + .map((file) => path.relative(root, file).replaceAll(path.sep, '/')); + + assert.deepStrictEqual(relative, [ + 'covered-by-reference.md', + 'nested/covered-inline.md', + 'nested/mere-field-mention.md', + 'nested/modes/uncovered.md', + ]); + }); + + test('an uppercase extension is discovered, and is a violation rather than an exemption', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-response-language-case-')); + tempDirs.push(root); + fs.writeFileSync(path.join(root, 'SHOUTING.MD'), '# no directive\n'); + fs.writeFileSync(path.join(root, 'mixed.Md'), '# no directive\n'); + fs.writeFileSync(path.join(root, 'not-a-workflow.mdx'), '# a format the catalog does not emit\n'); + + const relative = findMarkdownFilesRecursive(root) + .map((file) => path.relative(root, file).replaceAll(path.sep, '/')); + assert.deepStrictEqual(relative, ['SHOUTING.MD', 'mixed.Md']); + + // The point is the direction of the old failure: a case-sensitive suffix + // test dropped these two on Linux alone, and a dropped file is a file this + // lint certifies by never having looked at it. Both must land as + // violations, the same as any lowercase sibling carrying no directive. + const violations = findViolations(root) + .map((file) => path.relative(root, file).replaceAll(path.sep, '/')); + assert.deepStrictEqual(violations, ['SHOUTING.MD', 'mixed.Md']); + }); + + test('every admitted extension is spelled so the case-insensitive match can reach it', () => { + // `isWorkflowFile` lowercases the extension before the lookup, so an entry + // carrying any uppercase would be unreachable — dead configuration that + // reads like coverage. The leading dot is the other half: `path.extname` + // returns one, and an entry without it matches nothing. + for (const extension of WORKFLOW_EXTENSIONS) { + assert.equal(extension, extension.toLowerCase(), `${extension} can never match`); + assert.ok(extension.startsWith('.'), `${extension} is not an extension path.extname returns`); + } + }); + + test('reports an uncovered nested workflow while accepting both coverage forms', () => { + const root = fixture(); + const violations = findViolations(root) + .map((file) => path.relative(root, file).replaceAll(path.sep, '/')); + + assert.deepStrictEqual(violations, [ + 'nested/mere-field-mention.md', + 'nested/modes/uncovered.md', + ]); + }); + + test('pins every shared inline directive site to one exact canonical line', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-response-language-parity-')); + tempDirs.push(root); + for (const relative of EXACT_INLINE_DIRECTIVE_WORKFLOWS) { + const file = path.join(root, relative); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, `${INLINE_RESPONSE_LANGUAGE_DIRECTIVE}\n`); + } + + // Not a count. The size of this set is a consequence of the rule enforced in + // "a pinned workflow is one that could not have inherited instead" — it moves + // whenever the catalog does, and a number here would only record when it last + // moved. What has to hold is that the set is non-empty (an empty set would make + // every assertion below vacuous) and that every member pins to the one line. + assert.ok(EXACT_INLINE_DIRECTIVE_WORKFLOWS.size > 0, "the pinned set is empty"); + assert.deepStrictEqual(findViolations(root), []); + + const drifted = path.join(root, 'discuss-phase', 'modes', 'advisor.md'); + fs.writeFileSync( + drifted, + 'Apply response_language to all user-facing prose; preserve code and paths.\n', + ); + assert.deepStrictEqual(findViolations(root), [drifted]); + }); + + // #1671 keeps extracting workflow prose into fragments. A fragment carries no + // directive of its own, so without inheritance every extraction reds this lint + // for prose that was already covered where it used to live. + function fragmentFixture({ parentCovered = true, parentNamesFragment = true } = {}) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-response-language-fragment-')); + tempDirs.push(root); + fs.mkdirSync(path.join(root, 'autonomous', 'steps'), { recursive: true }); + fs.writeFileSync( + path.join(root, 'autonomous.md'), + [ + parentCovered + ? '@~/.claude/gsd-core/references/response-language-directive.md' + : '# No directive here', + parentNamesFragment + ? 'read and execute `gsd-core/workflows/autonomous/steps/converge-banner.md`' + : 'read and execute `gsd-core/workflows/autonomous/steps/something-else.md`', + ].join('\n') + '\n', + ); + fs.writeFileSync( + path.join(root, 'autonomous', 'steps', 'converge-banner.md'), + 'Display: `Planning: convergence enabled`\n', + ); + return root; + } + + test('a fragment inherits coverage from the parent that names it', () => { + const root = fragmentFixture(); + + assert.strictEqual( + inheritsParentCoverage(root, 'autonomous/steps/converge-banner.md'), + true, + ); + assert.deepStrictEqual(findViolations(root), []); + }); + + test('inheritance is refused when the parent is uncovered or does not name the fragment', () => { + const uncoveredParent = fragmentFixture({ parentCovered: false }); + assert.deepStrictEqual( + findViolations(uncoveredParent).map((file) => path.relative(uncoveredParent, file).replaceAll(path.sep, '/')), + ['autonomous.md', 'autonomous/steps/converge-banner.md'], + ); + + const unreferenced = fragmentFixture({ parentNamesFragment: false }); + assert.deepStrictEqual( + findViolations(unreferenced).map((file) => path.relative(unreferenced, file).replaceAll(path.sep, '/')), + ['autonomous/steps/converge-banner.md'], + ); + }); + + // #2558 round 10, Minor D. `inheritsParentCoverage` used to prove the parent + // "is the way in" with a bare substring test, so any mention of the fragment + // path — a changelog line, a deprecation note, a sentence about the file — + // granted the fragment its parent's coverage. Inheritance is only sound when + // the parent DISPATCHES the fragment, since that is what guarantees the + // parent's directive is loaded when the fragment runs. + test('inheritance requires a dispatching read/execute context, not a bare mention', () => { + const dispatches = [ + 'If `section_manifest` is `null`: read and execute `gsd-core/workflows/a/steps/b.md`. Otherwise skip.', + 'Read and execute `gsd-core/workflows/a/steps/b.md`.', + 'Read+execute `gsd-core/workflows/a/steps/b.md` (defines the helpers).', + 'Read `gsd-core/workflows/a/steps/b.md` if planning freezes on Windows.', + // #1689's per-plan executor routing dispatches with `run`, not read/execute. + '**Executor routing.** Per plan, run `gsd-core/workflows/a/steps/b.md` to set `EXECUTOR_TYPE`.', + // #3552's `branching_strategy: none` arm dispatches with the path written + // RELATIVE to the catalog. A rooted-only needle read that live dispatch as + // no dispatch, and the fragment it reaches read as uncovered. + '**"none":** Read and execute `a/steps/b.md`.', + ]; + for (const line of dispatches) { + assert.strictEqual( + namesFragmentAsEntryPoint(`${line}\n`, 'a/steps/b.md'), + true, + `dispatch stub not recognized: ${line}`, + ); + } + + const mentions = [ + '- #1234: extracted the wave logic into `gsd-core/workflows/a/steps/b.md`.', + 'The prose below used to live in `gsd-core/workflows/a/steps/b.md`.', + '`gsd-core/workflows/a/steps/b.md` is deprecated and no longer dispatched.', + // The verb is on a different line: it governs nothing here. + 'Read and execute the step below.\nSee `gsd-core/workflows/a/steps/b.md`.', + // The verb is on the same line but a whole clause away, so it belongs to a + // different sentence — the window is what keeps it from vouching. + 'Read the roadmap first, then decide whether any of this still applies to `gsd-core/workflows/a/steps/b.md`.', + // The relative form matches on a path boundary, so a DIFFERENT file whose + // path merely ends with this one dispatches itself, not this fragment. + 'Read and execute `vendor/a/steps/b.md`.', + ]; + for (const line of mentions) { + assert.strictEqual( + namesFragmentAsEntryPoint(`${line}\n`, 'a/steps/b.md'), + false, + `bare mention accepted as a dispatch: ${line}`, + ); + } + }); + + test('a fragment mentioned but never dispatched does not inherit', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-response-language-mention-')); + tempDirs.push(root); + fs.mkdirSync(path.join(root, 'autonomous', 'steps'), { recursive: true }); + fs.writeFileSync( + path.join(root, 'autonomous.md'), + '@~/.claude/gsd-core/references/response-language-directive.md\n' + + '- #1671: the banner prose moved to `gsd-core/workflows/autonomous/steps/converge-banner.md`.\n', + ); + fs.writeFileSync( + path.join(root, 'autonomous', 'steps', 'converge-banner.md'), + 'Display: `Planning: convergence enabled`\n', + ); + + assert.strictEqual( + inheritsParentCoverage(root, 'autonomous/steps/converge-banner.md'), + false, + ); + assert.deepStrictEqual( + findViolations(root).map((file) => path.relative(root, file).replaceAll(path.sep, '/')), + ['autonomous/steps/converge-banner.md'], + ); + }); + + // #2558 round 10 (Minor C), narrowed in round 13. The `gsd-verifier` subagent + // emits user-facing prose but reads no workflow file of its own, so its + // coverage lives entirely in the dispatch prompt execute-phase.md builds: the + // reference tells the orchestrator to carry the directive "immediately after + // `Create VERIFICATION.md.`", and that anchor lives in the workflow. + // + // Round 13 removed the lint's side of this: it hung off `verify-phase.md`, a + // catalog file `next` deleted in #3421. The contract outlived the file — the + // verifier still runs — but nothing in the workflows tree carries it any more, + // so the lint cannot see it and this test is now the only thing holding the two + // halves together. Asserted against the REAL tree, not a fixture: a fixture + // would only prove the assertion can pass. + const VERIFIER_DISPATCH_CONTRACT = { + reference: '../references/execute-phase-response-language.md', + directive: 'Use response_language {response_language} for all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code and paths.', + anchor: 'Create VERIFICATION.md.', + anchorIn: 'execute-phase.md', + }; + + test('the gsd-verifier dispatch contract still has both of its halves', () => { + const { reference, directive, anchor, anchorIn } = VERIFIER_DISPATCH_CONTRACT; + + const referencePath = path.join(WORKFLOWS_DIR, reference); + assert.ok(fs.existsSync(referencePath), `reference is stale: ${reference}`); + const referenceFile = fs.readFileSync(referencePath, 'utf8'); + assert.ok( + referenceFile.includes(directive), + `${reference} no longer carries the directive the verifier dispatch must inject`, + ); + + const anchorPath = path.join(WORKFLOWS_DIR, anchorIn); + assert.ok(fs.existsSync(anchorPath), `anchor file is stale: ${anchorIn}`); + assert.ok( + fs.readFileSync(anchorPath, 'utf8').split(/\r?\n/).some((line) => line.includes(anchor)), + `${anchorIn} no longer carries the anchor "${anchor}" that the injected ` + + `response-language directive is positioned against. Re-anchor it in ${reference}.`, + ); + + // The reference must keep naming the same anchor, or the two halves drift + // apart while each stays individually true. + assert.ok( + referenceFile.includes(anchor), + `${reference} no longer names the anchor "${anchor}"`, + ); + }); + + test('inheritance reaches fragment directories only, never a nested workflow tree', () => { + const root = fragmentFixture(); + // Depth and directory name are both load-bearing: a two-segment path has no + // parent workflow, and a directory outside the fragment set is not a section. + assert.strictEqual(inheritsParentCoverage(root, 'autonomous.md'), false); + assert.strictEqual( + inheritsParentCoverage(root, 'autonomous/steps/nested/converge-banner.md'), + false, + ); + assert.strictEqual( + inheritsParentCoverage(root, 'autonomous/references/converge-banner.md'), + false, + ); + }); + + test('rejects a bare config mention and accepts an actionable inline directive', () => { + assert.strictEqual(hasResponseLanguageCoverage('response_language\n'), false); + assert.strictEqual( + hasResponseLanguageCoverage( + 'Apply response_language to all user-facing prose, narration included.\n', + ), + true, + ); + }); + + // The regression guard for the fix in #2558 round 13, and the sibling of the + // dispatch-vs-mention test above. Before it, the reference check was a bare + // `content.includes(ref)`: a workflow whose changelog merely NAMED the shared + // directive was certified covered while shipping no import at all — the same + // false-positive class, one level up. + test('coverage by reference requires an @-import, not a mention of the path', () => { + const imports = [ + '@~/.claude/gsd-core/references/response-language-directive.md', + '@$HOME/.claude/gsd-core/references/response-language-directive.md', + '@~/.claude/gsd-core/references/execute-phase-response-language.md', + // Leading/trailing whitespace is still an import line. + ' @~/.claude/gsd-core/references/response-language-directive.md ', + ]; + for (const line of imports) { + assert.strictEqual( + hasResponseLanguageCoverage(`${line}\n`), + true, + `import line not recognized: ${line}`, + ); + } + + const mentions = [ + '- #2529: added the shared references/response-language-directive.md reference.', + 'The directive lives in `gsd-core/references/response-language-directive.md`.', + 'references/response-language-directive.md is deprecated; do not import it.', + // An import that is only quoted as an example, mid-sentence, loads nothing. + 'Add `@~/.claude/gsd-core/references/response-language-directive.md` to new workflows.', + ]; + for (const line of mentions) { + assert.strictEqual( + hasResponseLanguageCoverage(`${line}\n`), + false, + `bare mention accepted as coverage: ${line}`, + ); + } + }); + + // The regression guard for the fix in #2558 round 10. Before it, the lint + // certified 45 workflows whose directive named only "questions, prompts, and + // explanations" — the exact wording #2529 filed as the DEFECT, because it + // leaves the model's between-tool-call narration in English while the answers + // around it are translated. Certifying that wording made the gate legitimise + // the bug it exists to catch, so the old sentence must now FAIL. + test('the pre-#2558 weak wording is no longer coverage; naming narration is', () => { + const weak = + '**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.\n'; + assert.strictEqual( + hasResponseLanguageCoverage(weak), + false, + 'a directive naming only the question/prompt surface must not count as coverage', + ); + + // Every narration-class form the lint accepts, each asserted on its own so a + // future edit to the alternation cannot silently drop one while the others + // keep the suite green. + for (const token of [ + 'narration between tool calls', + 'narration', + 'output between tool calls', + ]) { + assert.strictEqual( + hasResponseLanguageCoverage( + `Apply response_language to all user-facing output — ${token} included.\n`, + ), + true, + `"${token}" must satisfy the narration-class requirement`, + ); + } + + // Round 21: a word that merely APPEARS in the canonical phrasing does not + // name the class. REQ-LANG-04 requires the directive to name inter-tool + // narration; "report status" names a surface the model already reports in + // English and says nothing about the commentary between tool calls, so the + // earlier token list was weaker than the requirement it enforced. + for (const weakToken of ['status updates', 'progress notes', 'findings']) { + assert.strictEqual( + hasResponseLanguageCoverage( + `Apply response_language to all user-facing output — ${weakToken} included.\n`, + ), + false, + `"${weakToken}" alone must not satisfy the narration-class requirement`, + ); + } + + // The narration token alone is not a directive either: the line still has to + // name response_language and act on it, so the tightening did not swap one + // half of the predicate for the other. + assert.strictEqual( + hasResponseLanguageCoverage('Narration between tool calls is emitted here.\n'), + false, + ); + + // Both halves must land on the SAME line. A workflow that mentions narration + // in one paragraph and response_language in another has stated no rule. + assert.strictEqual( + hasResponseLanguageCoverage( + 'Apply response_language to all user-facing prose.\nNarration is emitted between tool calls.\n', + ), + false, + ); + }); + + // The wording migration is only real if it actually landed in the catalog: the + // lint could pass on a tree where every file still carried the weak sentence if + // the tightening above were ever reverted. Assert the catalog directly. + // Round 21 widened the scan from the workflow catalog to the references + // directory as well. The predicate check on imported references (above) is the + // durable guard; this is the cheap independent one, and the two fail for + // different reasons — the predicate asks whether a directive is present and + // actionable, this asks whether the specific sentence #2529 filed as the + // defect has come back. A shared reference carrying that sentence would be + // the single highest-blast-radius regression in the catalog: 43 workflows + // hold no directive of their own. + test('no shipped workflow or reference still carries the pre-#2558 weak directive sentence', () => { + const WEAK_SENTENCE = + 'All user-facing questions, prompts, and explanations in this workflow MUST be presented in'; + const workflows = findMarkdownFilesRecursive(WORKFLOWS_DIR); + const references = findMarkdownFilesRecursive(path.join(REFERENCE_ROOT, 'references')); + const scanned = [...workflows, ...references]; + const offenders = scanned + .filter((file) => fs.readFileSync(file, 'utf8').includes(WEAK_SENTENCE)) + .map((file) => path.relative(REFERENCE_ROOT, file).replaceAll(path.sep, '/')); + + assert.deepStrictEqual(offenders, []); + // A scan that inspected nothing proves nothing — the same rule main() applies. + // Stated as "each source produced files" rather than as a floor. A numeric + // floor here reads as the workflow count, is stale the moment the catalog + // moves, and would still pass a scan that lost one of the two directories + // entirely — the failure it exists to catch. + assert.ok(workflows.length > 0, 'the workflow catalog scan produced no files'); + assert.ok(references.length > 0, 'the reference directory scan produced no files'); + }); + + test('main returns a failure code and reports each violation', () => { + const root = fixture(); + const errors = []; + const logs = []; + const exitCode = main(root, { + error: (message) => errors.push(message), + log: (message) => logs.push(message), + }); + + assert.strictEqual(exitCode, 1); + assert.strictEqual(logs.length, 0); + assert.match(errors[0], /2 workflow\(s\) have no response-language coverage/); + assert.match(errors[0], /nested\/mere-field-mention\.md/); + assert.match(errors[0], /nested\/modes\/uncovered\.md/); + }); + + test('main returns success and emits the covered workflow count', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-response-language-ok-')); + tempDirs.push(root); + fs.writeFileSync( + path.join(root, 'covered.md'), + 'Apply response_language to all user-facing prose, narration included.\n', + ); + const errors = []; + const logs = []; + + assert.strictEqual(main(root, { + error: (message) => errors.push(message), + log: (message) => logs.push(message), + }), 0); + assert.deepStrictEqual(errors, []); + assert.deepStrictEqual(logs, [ + 'lint-response-language-coverage: OK (1 workflows covered)', + ]); + }); + + test('main fails instead of passing vacuously when discovery finds no workflow', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-response-language-empty-')); + tempDirs.push(root); + fs.mkdirSync(path.join(root, 'not-a-workflow'), { recursive: true }); + fs.writeFileSync(path.join(root, 'not-a-workflow', 'notes.txt'), 'not Markdown'); + const errors = []; + const logs = []; + + assert.strictEqual(main(root, { + error: (message) => errors.push(message), + log: (message) => logs.push(message), + }), 1); + assert.deepStrictEqual(logs, []); + assert.match(errors[0], /no workflow files found/); + }); + + test('main fails closed on an unreadable workflow directory rather than throwing', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-response-language-absent-')); + tempDirs.push(root); + fs.writeFileSync(path.join(root, 'file-not-dir.md'), 'Apply response_language to all prose.\n'); + const errors = []; + const logs = []; + const io = { + error: (message) => errors.push(message), + log: (message) => logs.push(message), + }; + + assert.strictEqual(main(path.join(root, 'does-not-exist'), io), 1); + assert.match(errors[0], /cannot read the workflow directory/); + assert.match(errors[0], /ENOENT/); + + assert.strictEqual(main(path.join(root, 'file-not-dir.md'), io), 1); + assert.match(errors[1], /cannot read the workflow directory/); + assert.deepStrictEqual(logs, []); + }); + + // #2558 round 21, Blocker. 43 workflows hold no directive of their own and take + // ALL of their coverage from one shared file. The lint checked only that the + // @-import LINE existed, so rewriting that file back to the pre-#2529 sentence + // left every one of them "covered" with the whole suite green — the same + // "the gate certifies the defect" failure the round-10 fix closed, one level up. + // These tests fail on the unfixed lint. + function referenceFixture(referenceText) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-response-language-ref-')); + tempDirs.push(root); + const workflows = path.join(root, 'workflows'); + fs.mkdirSync(path.join(root, 'references'), { recursive: true }); + fs.mkdirSync(workflows, { recursive: true }); + fs.writeFileSync( + path.join(workflows, 'takes-the-reference.md'), + '@~/.claude/gsd-core/references/response-language-directive.md\n', + ); + if (referenceText !== null) { + fs.writeFileSync( + path.join(root, 'references', 'response-language-directive.md'), + referenceText, + ); + } + return { root, workflows }; + } + + const STRONG_REFERENCE = + 'ALL user-facing output of this workflow MUST be in `response_language` — ' + + 'narration between tool calls, findings, and report prose.\n'; + const WEAK_REFERENCE = + '**If `response_language` is set:** All user-facing questions, prompts, and ' + + 'explanations in this workflow MUST be presented in `{response_language}`.\n'; + + test('an imported reference that no longer carries a directive uncovers its importers', () => { + const strong = referenceFixture(STRONG_REFERENCE); + assert.deepStrictEqual(findViolations(strong.workflows, strong.root), []); + assert.deepStrictEqual(findBrokenDirectiveReferences( + findMarkdownFilesRecursive(strong.workflows), strong.root, + ), []); + + // The reviewer's mutation: the shared file reworded back to the defect. + const weak = referenceFixture(WEAK_REFERENCE); + assert.deepStrictEqual( + findViolations(weak.workflows, weak.root).map((file) => path.basename(file)), + ['takes-the-reference.md'], + ); + + // ...and the same for a reference that is missing outright. + const missing = referenceFixture(null); + assert.deepStrictEqual( + findViolations(missing.workflows, missing.root).map((file) => path.basename(file)), + ['takes-the-reference.md'], + ); + }); + + test('main reports a weakened reference as one systemic failure, not per importer', () => { + const weak = referenceFixture(WEAK_REFERENCE); + const errors = []; + const logs = []; + const exitCode = main(weak.workflows, { + error: (message) => errors.push(message), + log: (message) => logs.push(message), + }, weak.root); + + assert.strictEqual(exitCode, 1); + assert.deepStrictEqual(logs, []); + assert.match(errors[0], /shared directive reference\(s\) no longer carry an actionable directive/); + assert.match(errors[0], /references\/response-language-directive\.md/); + // The cause, not its 43 symptoms. + assert.doesNotMatch(errors[0], /workflow\(s\) have no response-language coverage/); + }); + + test('every shipped directive reference carries an actionable directive', () => { + // The real tree, not a fixture: this is the assertion that would have caught + // the hole, and it holds for both references the catalog imports. + for (const ref of ['response-language-directive.md', 'execute-phase-response-language.md']) { + const file = path.join(REFERENCE_ROOT, 'references', ref); + assert.ok(fs.existsSync(file), `missing shipped reference: ${ref}`); + assert.strictEqual( + carriesInlineDirective(fs.readFileSync(file, 'utf8')), + true, + `${ref} must itself name the narration class alongside response_language`, + ); + } + assert.deepStrictEqual( + findBrokenDirectiveReferences(findMarkdownFilesRecursive(WORKFLOWS_DIR)), + [], + ); + }); + + // #2558 round 21, Minor 1. Dirent uses lstat semantics, so a symlinked + // directory answers false to both isDirectory() and isFile() — the walk used + // to skip such a subtree in silence while files.length > 0 kept the run green, + // which is precisely what main()'s comment claims cannot happen. + test('the walk follows a symlinked subtree instead of skipping it silently', (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-response-language-symlink-')); + tempDirs.push(root); + const workflows = path.join(root, 'workflows'); + const outside = path.join(root, 'outside'); + fs.mkdirSync(workflows, { recursive: true }); + fs.mkdirSync(outside, { recursive: true }); + fs.writeFileSync( + path.join(workflows, 'covered.md'), + 'Apply response_language to all user-facing prose, narration included.\n', + ); + fs.writeFileSync(path.join(outside, 'uncovered.md'), '# English-only mode\n'); + try { + fs.symlinkSync(outside, path.join(workflows, 'linked'), 'junction'); + } catch { + // Unprivileged Windows without Developer Mode cannot create links at all. + t.skip('symlink creation not permitted in this environment'); + return; + } + + assert.deepStrictEqual( + findMarkdownFilesRecursive(workflows) + .map((file) => path.relative(workflows, file).replaceAll(path.sep, '/')) + .sort(), + ['covered.md', 'linked/uncovered.md'], + ); + assert.deepStrictEqual( + findViolations(workflows).map((file) => path.basename(file)), + ['uncovered.md'], + ); + }); + + test('REQ-LANG-04 offers authors only forms the lint accepts', () => { + // Round 22: the requirement text is the shipped contract, so every form it + // hands an author must pass the lint that enforces it. The earlier wording + // enumerated four items joined by `or`, but only two of them name the + // narration class — an author copying `status updates` straight out of the + // requirement got a red lint for following it. Pin text and matcher + // together so the next reword of either cannot drift from the other. + const features = fs.readFileSync( + path.join(__dirname, '..', 'docs', 'FEATURES.md'), + 'utf8', + ); + const requirement = features + .split(/\r?\n/) + .find((line) => line.startsWith('- REQ-LANG-04:')); + assert.ok(requirement, 'REQ-LANG-04 must be present in docs/FEATURES.md'); + + // Round 23: scoped to the CLAUSE that states how the class is named, not to + // every quoted span on the line. Reading the whole line cannot tell an OFFER + // from a MENTION, so quoting the defective wording in order to warn against + // it — or quoting the class members descriptively — would have failed the + // document for offering the thing it warns against. That is the same + // mention-vs-claim confusion #3752 just repaired in the parity guard. + // Bounded quantifiers throughout: the scan runs over file content + // (local/no-unbounded-quantifier). + const offeredForms = (line) => { + const clause = line.match(/A directive names it by using ([^;.]{1,200})/); + return clause + ? [...clause[1].matchAll(/"([^"]{1,40})"/g)].map((match) => match[1]) + : []; + }; + + const offered = offeredForms(requirement); + assert.ok( + offered.length >= 2, + 'REQ-LANG-04 must state how a directive names the class, quoting each accepted form', + ); + for (const form of offered) { + assert.strictEqual( + hasResponseLanguageCoverage( + `Apply response_language to all user-facing output — ${form} included.\n`, + ), + true, + `REQ-LANG-04 offers "${form}", so the lint must accept it`, + ); + } + + // The class members it lists are described as insufficient alone, which is + // what NARRATION_CLASS_RE enforces and what the assertions above at the + // weak-token loop prove. + assert.match(requirement, /does not satisfy the rule/); + for (const member of ['status updates', 'progress notes', 'findings']) { + assert.ok( + !offered.includes(member), + `REQ-LANG-04 must not offer "${member}" as a standalone form`, + ); + } + + // The extraction reads the offering clause, so a quoted span elsewhere on + // the line is a mention and not an offer. Each addition below is a correct + // edit to the requirement that the round-22 whole-line scan rejected. + // Asserted last so a genuine drift in the line above fails on its own + // message rather than on this guard. + for (const mention of [ + ' A directive saying "questions, prompts, and explanations" is the defect.', + ' The class covers "status updates" and "progress notes" as members.', + ' See also "response-language coverage".', + ]) { + assert.deepStrictEqual( + offeredForms(requirement + mention), + offered, + `a mention must not read as an offer: ${mention.trim()}`, + ); + } + }); + + test('every pinned workflow path is live in the real catalog', () => { + // The pinned sets are enforced by exact path. A rename that leaves a stale + // entry behind does not fail the lint — the moved file quietly falls back + // to the loose coverage check, so the exact-line pin stops being enforced + // without anything going red. Assert the pins still resolve. + const discovered = new Set( + findMarkdownFilesRecursive(WORKFLOWS_DIR) + .map((file) => path.relative(WORKFLOWS_DIR, file).replaceAll(path.sep, '/')), + ); + const pinned = [...EXACT_INLINE_DIRECTIVE_WORKFLOWS].sort(); + + assert.deepStrictEqual(pinned.filter((relative) => !discovered.has(relative)), []); + }); + + test('a pinned workflow is one that could not have inherited instead', () => { + // The rule this set encodes: a lazy-loaded mode/step/template carries its own + // directive only where inheritance cannot be PROVEN for it — no parent + // dispatches it from a read/execute context, or the parent is uncovered. Where + // inheritance is proven, the pin is a second copy of one sentence with no + // coverage behind it, and the two forms then look arbitrary to the next author. + // This PR shipped 14 such pins before review measured them. Assert the rule + // rather than the count, so the set cannot re-grow the noise. + const redundant = [...EXACT_INLINE_DIRECTIVE_WORKFLOWS] + .filter((relative) => inheritsParentCoverage(WORKFLOWS_DIR, relative)) + .sort(); + + assert.deepStrictEqual(redundant, []); + }); + + test('a pinned workflow that converts to the shared reference is not a violation', () => { + // The pin means "this file cannot take the eager @-reference", not "the + // reference is worse than the pin". A fragment that becomes eagerly loaded and + // takes the reference is strictly better off, and an early return on the pinned + // path alone would red that improvement — a gate stricter than the contract it + // enforces, with no escape but editing the set. + const pinned = [...EXACT_INLINE_DIRECTIVE_WORKFLOWS].find((p) => !p.includes("/")); + assert.ok(pinned, "expected at least one top-level pinned workflow"); + + const converted = referenceFixture(STRONG_REFERENCE); + fs.writeFileSync( + path.join(converted.workflows, pinned), + '@~/.claude/gsd-core/references/response-language-directive.md\n', + ); + assert.deepStrictEqual(findViolations(converted.workflows, converted.root), []); + + // The pin still holds against everything else. A reworded inline line is a + // violation exactly as before, and so is the reference form when the shared + // file itself has been weakened — the swap inherits the reference's validation, + // it does not escape validation. + const reworded = referenceFixture(STRONG_REFERENCE); + fs.writeFileSync( + path.join(reworded.workflows, pinned), + 'Apply response_language to user-facing prose, narration included.\n', + ); + assert.deepStrictEqual( + findViolations(reworded.workflows, reworded.root).map((file) => path.basename(file)), + [pinned], + ); + + const weakened = referenceFixture(WEAK_REFERENCE); + fs.writeFileSync( + path.join(weakened.workflows, pinned), + '@~/.claude/gsd-core/references/response-language-directive.md\n', + ); + assert.deepStrictEqual( + findViolations(weakened.workflows, weakened.root).map((file) => path.basename(file)).sort(), + [pinned, 'takes-the-reference.md'].sort(), + ); + }); +}); + +/** + * Property tests for the four-predicate directive-line matcher. + * + * `carriesInlineDirective` accepts a document when ONE line carries all four + * signals at once: the config field, an action verb, a user-output term, and + * the narration class. The cases above pin particular phrasings; these pin the + * rule those phrasings are instances of. + * + * Per CONTRIBUTING.md "Fixture provenance (#2371)" the vocabulary below is + * written out here rather than read back from the script's own regexes. A + * generator seeded from the matcher can only re-derive what the matcher already + * believes — spelled out independently, these properties fail when a predicate + * is widened, dropped, or allowed to span lines. That independence paid for + * itself immediately: the plural forms below are what caught `output` being the + * one term in its class without an `s?`, so "translate all outputs, including + * narration between tool calls" read as uncovered. + */ +describe('response-language directive line: matcher properties (#2529)', () => { + const FIELD = 'response_language'; + const NARRATION = 'between tool calls'; + // The output pool deliberately EXCLUDES narration-class words. "narration" is + // in both classes, so one token would satisfy two predicates and the + // necessity property below could no longer tell them apart. + const ACTIONS = [ + 'apply', 'present', 'render', 'respond', 'translate', 'use', 'write', 'must', 'should', + ]; + const OUTPUTS = [ + 'explanation', 'explanations', 'language', 'output', 'outputs', 'prompt', 'prompts', + 'prose', 'question', 'questions', 'template', 'templates', 'user-facing', + ]; + const FILLER = ['the', 'and', 'of', 'in', 'for', 'each', 'step', 'file', 'then', 'this']; + + const cased = (word, mode) => { + if (mode === 'upper') return word.toUpperCase(); + if (mode === 'title') return word.replace(/\b[a-z]/g, (c) => c.toUpperCase()); + return word; + }; + + const partsArb = fc.record({ + action: fc.constantFrom(...ACTIONS), + output: fc.constantFrom(...OUTPUTS), + order: fc.shuffledSubarray([0, 1, 2, 3], { minLength: 4, maxLength: 4 }), + mode: fc.constantFrom('lower', 'upper', 'title'), + gaps: fc.array(fc.array(fc.constantFrom(...FILLER), { maxLength: 4 }), { + minLength: 5, maxLength: 5, + }), + }); + + const signals = ({ action, output }) => [FIELD, action, output, NARRATION]; + + // One line, the four signals in generated order, arbitrary neutral filler + // between them. `omit` drops exactly one signal for the necessity property. + const buildLine = (parts, omit = -1) => { + const tokens = signals(parts); + const words = []; + parts.order + .filter((index) => index !== omit) + .forEach((index, position) => { + words.push(...parts.gaps[position], cased(tokens[index], parts.mode)); + }); + words.push(...parts.gaps[4]); + return words.join(' ').trim(); + }; + + test('property: one line carrying all four signals is coverage, wherever it sits', () => { + fc.assert(fc.property( + partsArb, + fc.array(fc.constantFrom(...FILLER), { maxLength: 4 }), + fc.array(fc.constantFrom(...FILLER), { maxLength: 4 }), + (parts, before, after) => { + const line = buildLine(parts); + const document = [...before, line, ...after].join('\n'); + assert.equal( + carriesInlineDirective(document), true, + `read as uncovered: ${JSON.stringify(line)}`, + ); + }, + )); + }); + + test('property: dropping any one of the four signals is not coverage', () => { + fc.assert(fc.property(partsArb, fc.integer({ min: 0, max: 3 }), (parts, omit) => { + const line = buildLine(parts, omit); + assert.equal( + carriesInlineDirective(line), false, + `read as covered without ${JSON.stringify(signals(parts)[omit])}: ${JSON.stringify(line)}`, + ); + })); + }); + + test('property: the four signals spread across lines are not coverage', () => { + fc.assert(fc.property( + partsArb, + fc.array(fc.boolean(), { minLength: 3, maxLength: 3 }), + (parts, breaks) => { + // No break at all is the single-line case above, not this property. + fc.pre(breaks.some(Boolean)); + const tokens = signals(parts); + const document = parts.order.reduce( + (text, index, position) => (position === 0 + ? cased(tokens[index], parts.mode) + : text + (breaks[position - 1] ? '\n' : ' ') + cased(tokens[index], parts.mode)), + '', + ); + assert.equal( + carriesInlineDirective(document), false, + `read as covered across lines: ${JSON.stringify(document)}`, + ); + }, + )); + }); +}); diff --git a/tests/skill-frontmatter-contract.test.cjs b/tests/skill-frontmatter-contract.test.cjs index e272d0c98..db021ccf3 100644 --- a/tests/skill-frontmatter-contract.test.cjs +++ b/tests/skill-frontmatter-contract.test.cjs @@ -927,7 +927,12 @@ const DEFAULT_BUDGET = 60; // this FULL_BUDGET is a separate line-count budget for help/modes/full.md). // The size-budget test is non-recursive so full.md is not covered there; cap it here. // FULL ceiling lowered from 1500 → 844 (actualMax=784; #597 ratchet-down). -const FULL_BUDGET = 844; +// Raised 844 → 846 for #2529. #3676 (quick-batch, #4212) grew full.md 834 → 844, +// landing it exactly on the ceiling with zero slack; the two lines this file then +// takes — the pinned response-language directive and its blank separator — are a +// coverage contract every workflow carries, not content creep, which is what this +// budget guards. The ratchet rule is unchanged: actualMax 846, slack 0. +const FULL_BUDGET = 846; // Grace bands: // SMALL_GRACE — for the tiny brief/default/dispatcher files (≤ ~70 lines):