fix(#4264): wire both TDD dispatch backends to phase.tdd-applicable (#4284)

This commit is contained in:
Tom Boucher
2026-09-04 16:08:42 -04:00
committed by GitHub
parent 580059251a
commit 2f4f7538e9
27 changed files with 297 additions and 2 deletions

View File

@@ -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)

View File

@@ -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",

View File

@@ -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`'}
</execution_context>

View File

@@ -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='<objective>
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.

View File

@@ -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).

View File

@@ -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",

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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)`);
}
});
});