diff --git a/.changeset/mellow-seals-climb.md b/.changeset/mellow-seals-climb.md new file mode 100644 index 000000000..46aa6ab77 --- /dev/null +++ b/.changeset/mellow-seals-climb.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 4284 +--- +**TDD dispatch now correctly embeds `tdd.md` only when a plan is actually TDD** — both executor dispatch backends previously referenced an unassigned `${TDD_APPLICABLE}` placeholder, so the RED/GREEN/REFACTOR procedure could silently be dropped for a real TDD plan or embedded for a non-TDD one with no error. Both backends now resolve TDD-applicability via the single `phase.tdd-applicable` predicate and fail closed if it cannot be resolved, rather than guessing. (#4264, #4265, #4266, #3800) diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index 34da9ed3a..5c8e69972 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -621,6 +621,7 @@ "execute-phase/steps/protected-branch.md", "execute-phase/steps/regression-gate-run.md", "execute-phase/steps/regression-gate.md", + "execute-phase/steps/tdd-applicability-resolution.md", "execute-phase/steps/wave-post-gate-hooks.md", "execute-phase/steps/worktree-recovery-policy.md", "new-milestone/steps/project-md-milestone-write.md", diff --git a/gsd-core/workflows/execute-phase.md b/gsd-core/workflows/execute-phase.md index 7918764b9..3b087c7a0 100644 --- a/gsd-core/workflows/execute-phase.md +++ b/gsd-core/workflows/execute-phase.md @@ -677,6 +677,8 @@ increases monotonically across waves. `{status}` is `complete` (success), **Executor routing (#1689/#3370).** Per plan, run `gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md` to set `EXECUTOR_TYPE` for `subagent_type="{EXECUTOR_TYPE}"` below. + **TDD-applicability resolution (#4266/#4272).** Run `gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md`. + **Worktree mode** (`USE_WORKTREES` and `USE_WORKTREES_FOR_PLAN` not `false`): Before spawning, capture the current HEAD: @@ -757,7 +759,7 @@ increases monotonically across waves. `{status}` is `complete` (success), - `~/.claude/gsd-core/workflows/execute-plan.md` - `~/.claude/gsd-core/templates/summary.md` - `~/.claude/gsd-core/references/checkpoints.md` - ${TDD_APPLICABLE ? '- `~/.claude/gsd-core/references/tdd.md`' : ''} # #3990: only when this dispatch is TDD (plan type: tdd, a tdd="true" task, or TDD_MODE=true) + ${TDD_APPLICABLE ? '- `~/.claude/gsd-core/references/tdd.md`' : ''} # #3990/#4265: type: tdd, tdd="true", or workflow.tdd_mode - `~/.claude/gsd-core/references/worktree-path-safety.md` ${CONTEXT_WINDOW < 200000 ? '' : '- `~/.claude/gsd-core/references/executor-examples.md`'} diff --git a/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md b/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md index e17d93311..8d8315fb2 100644 --- a/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +++ b/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md @@ -161,6 +161,18 @@ Assign the composed prompt to a shell variable so it can be passed as one argume # no host subagent machinery, so the role definition must ride the prompt # (#3637 acceptance: resolved agent instructions as launch-level # instructions + provenance of which role definition was used). +# Resolve TDD-applicability for THIS plan (#4266/#4272) — fail closed on +# command failure, mirroring the ISOLATION resolution above: an absent +# verdict must never silently resolve to "not TDD" (ADR-3473 §8.4), since +# that would silently drop a real TDD plan's RED/GREEN/REFACTOR procedure. +_TDD_APPLICABLE_RAW=$(gsd_run query phase.tdd-applicable "{phase_dir}/{plan_file}" --pick applicable 2>/dev/null) +_TDD_APPLICABLE_RC=$? +if [ $_TDD_APPLICABLE_RC -ne 0 ]; then + echo "FATAL: could not resolve TDD-applicability for plan {plan_number} — 'gsd_run query phase.tdd-applicable' failed. Refusing to guess whether this dispatch needs the TDD procedure. Halting." >&2 + exit 1 +fi +TDD_APPLICABLE="$_TDD_APPLICABLE_RAW" + EXECUTOR_PROMPT=' Execute plan {plan_number} of phase {phase_number}-{phase_name}. Commit each task atomically. Create SUMMARY.md. @@ -181,7 +193,7 @@ the executor workflow from repository search. SUMMARY commit semantics and the gitignored-planning skip contract) - summary.md template - checkpoints.md -${TDD_APPLICABLE ? "- tdd.md" : ""} # #3990: only when this dispatch is TDD (plan type: tdd, or TDD_MODE=true) +${TDD_APPLICABLE ? "- tdd.md" : ""} # #3990/#4265: only when this dispatch is TDD (plan type: tdd, a tdd="true" task, or workflow.tdd_mode config) - worktree-path-safety.md - agents/gsd-executor.md (the ROLE DEFINITION you are executing — its steps 0/0a/0b per-commit HEAD/cwd-drift/path-guard discipline applies to every @@ -257,6 +269,10 @@ printf '%s' "$EXECUTOR_PROMPT" | grep -q '\${AGENT_SKILLS}' && { echo "FATAL: executor prompt for plan {plan_number} still contains the un-substituted \${AGENT_SKILLS} marker — the role definition was not spliced in (#3637). Halting." >&2 exit 1 } +printf '%s' "$EXECUTOR_PROMPT" | grep -q '\${TDD_APPLICABLE' && { + echo "FATAL: executor prompt for plan {plan_number} still contains the un-substituted \${TDD_APPLICABLE marker — the TDD-applicability decision was not resolved into the prompt (#4266). Halting." >&2 + exit 1 +} ``` The prompt body must contain no single-quote character, since the assignment above is single-quoted; keep apostrophes out of it when editing. diff --git a/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md b/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md new file mode 100644 index 000000000..ffffcd386 --- /dev/null +++ b/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md @@ -0,0 +1,25 @@ +# TDD-applicability resolution (#4266/#4272) + +Run for each plan, immediately after executor routing and before composing +that plan's `Agent()` prompt in step 3. Resolves whether this dispatch is TDD +— fail closed, do not guess. + +## Resolution + +```bash +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif _gsd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd_run is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; case "$(gsd_run runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') GSD_IDENTITY_STATUS=ok;; esac; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +TDD_APPLICABLE_RAW=$(gsd_run query phase.tdd-applicable "{phase_dir}/{plan_file}" --pick applicable 2>/dev/null) +TDD_APPLICABLE_RC=$? +if [ $TDD_APPLICABLE_RC -ne 0 ]; then + echo "FATAL: could not resolve TDD-applicability for plan {plan_number} — 'gsd_run query phase.tdd-applicable' failed. Refusing to guess whether this dispatch needs the TDD procedure. Halting." >&2 + exit 1 +fi +TDD_APPLICABLE="$TDD_APPLICABLE_RAW" +``` + +## Pre-dispatch check (MANDATORY) + +Before calling Agent(), confirm every `${...}` conditional in the prompt below +(`TDD_APPLICABLE`, `CONTEXT_WINDOW`, `AGENT_SKILLS`) was resolved to concrete +text for THIS plan. If any marker's value was not computed, HALT — do not +dispatch a prompt containing literal `${...}` template syntax (#4266). diff --git a/scripts/lint-workflow-shellcheck-baseline.json b/scripts/lint-workflow-shellcheck-baseline.json index bea73f537..aef877e46 100644 --- a/scripts/lint-workflow-shellcheck-baseline.json +++ b/scripts/lint-workflow-shellcheck-baseline.json @@ -379,6 +379,11 @@ "code": "2016", "message": "Expressions don't expand in single quotes, use double quotes for that." }, + { + "file": "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", + "code": "2016", + "message": "Expressions don't expand in single quotes, use double quotes for that." + }, { "file": "gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md", "code": "2086", diff --git a/tests/executor-isolation-prompt-contract.test.cjs b/tests/executor-isolation-prompt-contract.test.cjs index ec8ffcd87..5dd317122 100644 --- a/tests/executor-isolation-prompt-contract.test.cjs +++ b/tests/executor-isolation-prompt-contract.test.cjs @@ -91,6 +91,17 @@ test('#3637: embed PERFORMANCE is gated — an un-embedded template halts before ); }); +test('#4266: embed PERFORMANCE is gated — an un-substituted TDD_APPLICABLE marker halts before spawn', () => { + // Matches the ${AGENT_SKILLS} gate immediately above: a third performance gate for + // the #4266/#4272 TDD-applicability predicate, halting before worktree creation + // when the marker survives composition. + assert.match( + content, + /grep -q '\\\$\{TDD_APPLICABLE'[\s\S]{0,300}exit 1/, + 'an un-substituted TDD_APPLICABLE marker means the TDD-applicability decision was not resolved into the prompt — halt', + ); +}); + test('#3637: the gsd-executor ROLE DEFINITION is a mandatory embed with provenance', () => { // The agent-skills query alone is conditional (its block is a skills list // whenever the project configures agent_skills; only unconfigured diff --git a/tests/fixtures/install-tree/antigravity.json b/tests/fixtures/install-tree/antigravity.json index a9022d3ab..a75e3a6c5 100644 --- a/tests/fixtures/install-tree/antigravity.json +++ b/tests/fixtures/install-tree/antigravity.json @@ -284,6 +284,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/augment.json b/tests/fixtures/install-tree/augment.json index a3f995123..5cb1ce8e3 100644 --- a/tests/fixtures/install-tree/augment.json +++ b/tests/fixtures/install-tree/augment.json @@ -356,6 +356,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/claude-local.json b/tests/fixtures/install-tree/claude-local.json index 138e9d4b7..3b4f0e576 100644 --- a/tests/fixtures/install-tree/claude-local.json +++ b/tests/fixtures/install-tree/claude-local.json @@ -356,6 +356,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/claude.json b/tests/fixtures/install-tree/claude.json index 9912c8f8e..6c156cfd2 100644 --- a/tests/fixtures/install-tree/claude.json +++ b/tests/fixtures/install-tree/claude.json @@ -284,6 +284,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/cline.json b/tests/fixtures/install-tree/cline.json index 43386f720..b50ceb0e3 100644 --- a/tests/fixtures/install-tree/cline.json +++ b/tests/fixtures/install-tree/cline.json @@ -286,6 +286,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/codebuddy.json b/tests/fixtures/install-tree/codebuddy.json index 8a1f56f66..045eb7794 100644 --- a/tests/fixtures/install-tree/codebuddy.json +++ b/tests/fixtures/install-tree/codebuddy.json @@ -356,6 +356,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/codex.json b/tests/fixtures/install-tree/codex.json index 406f8bead..9ebe80ce5 100644 --- a/tests/fixtures/install-tree/codex.json +++ b/tests/fixtures/install-tree/codex.json @@ -320,6 +320,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/copilot.json b/tests/fixtures/install-tree/copilot.json index 10244bd20..8f9e129de 100644 --- a/tests/fixtures/install-tree/copilot.json +++ b/tests/fixtures/install-tree/copilot.json @@ -285,6 +285,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/cursor.json b/tests/fixtures/install-tree/cursor.json index 7a36b7297..3482b8351 100644 --- a/tests/fixtures/install-tree/cursor.json +++ b/tests/fixtures/install-tree/cursor.json @@ -284,6 +284,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/hermes.json b/tests/fixtures/install-tree/hermes.json index 6f3776f78..b9788288f 100644 --- a/tests/fixtures/install-tree/hermes.json +++ b/tests/fixtures/install-tree/hermes.json @@ -284,6 +284,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/kilo.json b/tests/fixtures/install-tree/kilo.json index 01bc3a40c..2c629a91c 100644 --- a/tests/fixtures/install-tree/kilo.json +++ b/tests/fixtures/install-tree/kilo.json @@ -356,6 +356,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/kimi-code.json b/tests/fixtures/install-tree/kimi-code.json index 22c232aa7..91348e616 100644 --- a/tests/fixtures/install-tree/kimi-code.json +++ b/tests/fixtures/install-tree/kimi-code.json @@ -285,6 +285,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/kimi.json b/tests/fixtures/install-tree/kimi.json index 1a0ce7d45..081c8b1c5 100644 --- a/tests/fixtures/install-tree/kimi.json +++ b/tests/fixtures/install-tree/kimi.json @@ -321,6 +321,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/opencode.json b/tests/fixtures/install-tree/opencode.json index 6aa5d6027..7c27ed6f0 100644 --- a/tests/fixtures/install-tree/opencode.json +++ b/tests/fixtures/install-tree/opencode.json @@ -356,6 +356,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/pi.json b/tests/fixtures/install-tree/pi.json index 692cc3b3a..9f702e912 100644 --- a/tests/fixtures/install-tree/pi.json +++ b/tests/fixtures/install-tree/pi.json @@ -251,6 +251,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/qwen.json b/tests/fixtures/install-tree/qwen.json index 7a9c6f65a..8434d37de 100644 --- a/tests/fixtures/install-tree/qwen.json +++ b/tests/fixtures/install-tree/qwen.json @@ -284,6 +284,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/trae.json b/tests/fixtures/install-tree/trae.json index 37a2e5875..4d32fd53c 100644 --- a/tests/fixtures/install-tree/trae.json +++ b/tests/fixtures/install-tree/trae.json @@ -284,6 +284,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/windsurf.json b/tests/fixtures/install-tree/windsurf.json index 7609620bd..4e865e373 100644 --- a/tests/fixtures/install-tree/windsurf.json +++ b/tests/fixtures/install-tree/windsurf.json @@ -284,6 +284,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/fixtures/install-tree/zcode.json b/tests/fixtures/install-tree/zcode.json index 5c2ff4162..13133b274 100644 --- a/tests/fixtures/install-tree/zcode.json +++ b/tests/fixtures/install-tree/zcode.json @@ -356,6 +356,7 @@ "gsd-core/workflows/execute-phase/steps/protected-branch.md", "gsd-core/workflows/execute-phase/steps/regression-gate-run.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", + "gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md", "gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md", "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md", "gsd-core/workflows/execute-plan.md", diff --git a/tests/tdd-backend-wiring.test.cjs b/tests/tdd-backend-wiring.test.cjs new file mode 100644 index 000000000..097dca752 --- /dev/null +++ b/tests/tdd-backend-wiring.test.cjs @@ -0,0 +1,211 @@ +'use strict'; + +/** + * #4264/#4265/#4266 (epic #4272 Phase 2) — TDD_APPLICABLE is USED (as a + * `${TDD_APPLICABLE ? ... : ...}` ternary, per #3990) in both executor + * dispatch backends but was never ASSIGNED anywhere: `phase.tdd-applicable` + * (Phase 1, #4273) computed the predicate, but no workflow ever called it. + * That is a silent "absence resolves to false" defect (ADR-3473 §8.4) — an + * un-substituted `${TDD_APPLICABLE ...}` marker is JS-truthy as literal text, + * so a real TDD plan would have silently dropped its RED/GREEN/REFACTOR + * procedure while never producing an error. + * + * These are shape assertions on the raw workflow-markdown TEXT: the deployed + * text IS the runtime-loaded product (an LLM orchestrator reads it top to + * bottom and composes/executes it) — same rationale as + * tdd-single-statement.test.cjs's header comment. `readFileSync` here targets + * `.md` files only, never a `.cjs`/`.js`/`.ts` source path, so + * `local/no-source-grep` does not apply and no `allow-test-rule` marker is + * needed (confirmed against tdd-single-statement.test.cjs, which reads the + * same two files with no such marker). + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const ROOT = path.join(__dirname, '..'); +const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8'); + +const HARNESS_PATH = 'gsd-core/workflows/execute-phase.md'; +const WORKTREE_PATH = 'gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md'; +const TDD_STEP_FRAGMENT_PATH = 'gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md'; + +// #4272-phase-6 byte-ceiling extraction (two commits ago): the harness +// backend's TDD_APPLICABLE assignment + phase.tdd-applicable call moved OUT +// of execute-phase.md and into the tdd-applicability-resolution.md step +// fragment; execute-phase.md itself now carries only a one-line REFERENCE to +// that fragment. The worktree backend was not affected — it still carries +// everything inline in executor-isolation-dispatch.md. `referenceFile` is +// where the reader/orchestrator encounters the backend (and, for the +// harness, where the one-line pointer + the ${TDD_APPLICABLE...} USE live); +// `assignmentFile` is where TDD_APPLICABLE is actually ASSIGNED. +const BACKENDS = { + 'execute-phase.md': { referenceFile: HARNESS_PATH, assignmentFile: TDD_STEP_FRAGMENT_PATH }, + 'executor-isolation-dispatch.md': { referenceFile: WORKTREE_PATH, assignmentFile: WORKTREE_PATH }, +}; + +describe('#4266 — TDD_APPLICABLE is actually computed in both backends', () => { + // A backend may assign TDD_APPLICABLE directly from the `gsd_run query + // phase.tdd-applicable` call (harness: `TDD_APPLICABLE=$(gsd_run query + // phase.tdd-applicable ...)`) or via an intermediate raw/rc pair mirroring + // the existing ISOLATION resolution pattern (worktree: `_TDD_APPLICABLE_RAW + // =$(gsd_run query phase.tdd-applicable ...)` ... `TDD_APPLICABLE= + // "$_TDD_APPLICABLE_RAW"`) — either shape is fine as long as the assignment + // exists AND is fed by a real phase.tdd-applicable call within a small + // preceding window, not a bare/silent default. + function assertTddApplicableIsComputed(name, filePath) { + const text = read(filePath); + const lines = text.split('\n'); + const assignIdx = lines.findIndex((l) => /^\s*TDD_APPLICABLE=/.test(l)); + assert.ok(assignIdx !== -1, `${name} must assign TDD_APPLICABLE, not just reference it`); + const windowStart = Math.max(0, assignIdx - 8); + const window = lines.slice(windowStart, assignIdx + 1).join('\n'); + assert.ok( + window.includes('phase.tdd-applicable'), + `${name}'s TDD_APPLICABLE assignment must be fed by a phase.tdd-applicable call within a few lines above it, got window: ${window}`, + ); + } + + test('harness backend (execute-phase.md) assigns TDD_APPLICABLE via phase.tdd-applicable', () => { + // Assignment now lives in the extracted tdd-applicability-resolution.md + // step fragment, not in execute-phase.md itself (#4272-phase-6). + assertTddApplicableIsComputed('execute-phase.md', BACKENDS['execute-phase.md'].assignmentFile); + }); + + test('worktree backend (executor-isolation-dispatch.md) assigns TDD_APPLICABLE via phase.tdd-applicable', () => { + assertTddApplicableIsComputed('executor-isolation-dispatch.md', BACKENDS['executor-isolation-dispatch.md'].assignmentFile); + }); + + for (const [name, cfg] of Object.entries(BACKENDS)) { + test(`${name}'s phase.tdd-applicable call is plan-scoped, not phase-scoped`, () => { + const text = read(cfg.assignmentFile); + const callLine = text.split('\n').find((l) => l.includes('phase.tdd-applicable') && l.includes('gsd_run query')); + assert.ok(callLine, `${name} must carry a gsd_run query phase.tdd-applicable call`); + assert.ok( + callLine.includes('{phase_dir}') && callLine.includes('{plan_file}'), + `${name}'s phase.tdd-applicable call must pass the per-plan {phase_dir}/{plan_file} path, got: ${callLine.trim()}`, + ); + }); + } + + test('both backends assign TDD_APPLICABLE before every ${TDD_APPLICABLE...} use (document order)', () => { + // Worktree backend: assignment and every ${TDD_APPLICABLE...} use live + // in the same file — same-file ordering check. + { + const text = read(BACKENDS['executor-isolation-dispatch.md'].assignmentFile); + const lines = text.split('\n'); + const assignIdx = lines.findIndex((l) => /^\s*TDD_APPLICABLE=/.test(l)); + assert.ok(assignIdx !== -1, 'executor-isolation-dispatch.md has no TDD_APPLICABLE assignment'); + const useIndices = []; + lines.forEach((l, i) => { + if (/\$\{TDD_APPLICABLE\b/.test(l)) useIndices.push(i); + }); + assert.ok(useIndices.length > 0, 'executor-isolation-dispatch.md has no ${TDD_APPLICABLE...} use to check ordering against'); + for (const useIdx of useIndices) { + assert.ok( + assignIdx < useIdx, + `executor-isolation-dispatch.md: TDD_APPLICABLE is used at line ${useIdx + 1} before it is assigned at line ${assignIdx + 1}`, + ); + } + } + + // Harness backend: the assignment was extracted into + // tdd-applicability-resolution.md (#4272-phase-6); execute-phase.md no + // longer contains the assignment at all, only a one-line REFERENCE to + // where it's resolved followed (later) by the ${TDD_APPLICABLE...} USE. + // Confirm the fragment does assign it, and that in execute-phase.md the + // reference precedes every use. + { + const fragmentText = read(BACKENDS['execute-phase.md'].assignmentFile); + assert.ok( + /^\s*TDD_APPLICABLE=/m.test(fragmentText), + 'tdd-applicability-resolution.md has no TDD_APPLICABLE assignment', + ); + + const text = read(BACKENDS['execute-phase.md'].referenceFile); + const lines = text.split('\n'); + const refIdx = lines.findIndex( + (l) => l.includes('tdd-applicability-resolution.md') && l.includes('TDD-applicability resolution'), + ); + assert.ok(refIdx !== -1, 'execute-phase.md must carry a one-line reference to tdd-applicability-resolution.md'); + const useIndices = []; + lines.forEach((l, i) => { + if (/\$\{TDD_APPLICABLE\b/.test(l)) useIndices.push(i); + }); + assert.ok(useIndices.length > 0, 'execute-phase.md has no ${TDD_APPLICABLE...} use to check ordering against'); + for (const useIdx of useIndices) { + assert.ok( + refIdx < useIdx, + `execute-phase.md: TDD_APPLICABLE is used at line ${useIdx + 1} before the reference to its resolution at line ${refIdx + 1}`, + ); + } + } + }); + + test('both tdd.md embed-line comments describe the same three real sources (#4265)', () => { + for (const [name, cfg] of Object.entries(BACKENDS)) { + const text = read(cfg.referenceFile); + const line = text.split('\n').find((l) => /tdd\.md/.test(l) && /TDD_APPLICABLE \?/.test(l)); + assert.ok(line, `${name} still lists tdd.md as a conditional embed entry`); + assert.ok(line.includes('type: tdd'), `${name}'s tdd.md comment must mention plan type: tdd, got: ${line.trim()}`); + assert.ok(line.includes('tdd="true"'), `${name}'s tdd.md comment must mention the tdd="true" task attribute (#4265), got: ${line.trim()}`); + assert.ok( + line.includes('workflow.tdd_mode') || line.includes('config'), + `${name}'s tdd.md comment must mention the workflow.tdd_mode config default, got: ${line.trim()}`, + ); + } + }); + + test('worktree backend fail-closes on an un-substituted ${TDD_APPLICABLE marker, matching the ${AGENT_SKILLS} check shape', () => { + const text = read(WORKTREE_PATH); + const lines = text.split('\n'); + const skillsIdx = lines.findIndex((l) => l.includes("grep -q '\\${AGENT_SKILLS}'")); + assert.ok(skillsIdx !== -1, 'worktree backend must still carry the existing ${AGENT_SKILLS} fail-closed check'); + const tddCheckIdx = lines.findIndex((l) => l.includes("grep -q '\\${TDD_APPLICABLE")); + assert.ok(tddCheckIdx !== -1, 'worktree backend must carry a fail-closed grep check for an unresolved ${TDD_APPLICABLE marker'); + // Same shape: FATAL message + exit 1 within the following couple of lines. + const block = lines.slice(tddCheckIdx, tddCheckIdx + 4).join('\n'); + assert.ok(/FATAL:/.test(block), 'the ${TDD_APPLICABLE check must emit a FATAL message like the ${AGENT_SKILLS} check'); + assert.ok(/exit 1/.test(block), 'the ${TDD_APPLICABLE check must exit 1 like the ${AGENT_SKILLS} check'); + }); + + test('harness backend references the tdd-applicability-resolution step fragment before Agent() is called, and that fragment states a MANDATORY pre-dispatch halt covering TDD_APPLICABLE, CONTEXT_WINDOW, and AGENT_SKILLS', () => { + // #4272-phase-6 byte-ceiling extraction: the "MANDATORY pre-dispatch check" + // paragraph moved out of execute-phase.md into + // tdd-applicability-resolution.md (mirroring per-plan-executor-routing.md); + // the host file now carries only a one-line reference to it. + const text = read(HARNESS_PATH); + const lines = text.split('\n'); + // The literal executor dispatch call sits alone on its own line + // (` Agent(`), inside the per-plan spawn section (step 3) — distinct + // from the many inline `` `Agent(...)` `` prose mentions elsewhere in + // this file (e.g. the runtime-compatibility table at line ~22). + const agentCallIdx = lines.findIndex((l) => /^\s*Agent\($/.test(l)); + assert.ok(agentCallIdx !== -1, 'execute-phase.md must contain the literal Agent( dispatch-call line'); + const before = lines.slice(0, agentCallIdx).join('\n'); + assert.ok( + before.includes('execute-phase/steps/tdd-applicability-resolution.md'), + 'execute-phase.md must reference the tdd-applicability-resolution.md step fragment before the Agent() call', + ); + + const fragment = read(TDD_STEP_FRAGMENT_PATH); + const checkIdx = fragment.indexOf('MANDATORY'); + assert.ok(checkIdx !== -1, 'tdd-applicability-resolution.md must state a MANDATORY pre-dispatch check'); + const checkText = fragment.slice(checkIdx); + for (const marker of ['TDD_APPLICABLE', 'CONTEXT_WINDOW', 'AGENT_SKILLS']) { + assert.ok(checkText.includes(marker), `pre-dispatch check must name ${marker}`); + } + assert.ok(/HALT/i.test(checkText), 'pre-dispatch check must instruct an explicit halt, not a soft warning'); + }); + + test('regression: tdd-single-statement.test.cjs assertions still hold by inspection', () => { + for (const [name, cfg] of Object.entries(BACKENDS)) { + const text = read(cfg.referenceFile); + const line = text.split('\n').find((l) => /tdd\.md/.test(l) && /TDD_APPLICABLE/.test(l)); + assert.ok(line, `${name} still lists tdd.md as a conditional embed entry`); + assert.ok(/TDD_APPLICABLE \?/.test(line), `${name}'s tdd.md entry must stay conditional on TDD_APPLICABLE (#3990)`); + } + }); +});