* test(#4011): TDD gate keys on TDD_MODE alone, not the MVP intersection Contract updates: no shipped line may conjoin MVP_MODE with TDD_MODE as a gate condition, the end-of-phase escalation must not require MVP, the executor agent's gate section triggers on TDD_MODE alone, and the gate semantics reference loads without MVP_MODE. * fix(#4011): key the TDD runtime gate on TDD_MODE alone The RED-commit gate shipped as #76's MVP slice kept the paired invocation's conjunct, so workflow.tdd_mode=true was silently inert on every non-MVP phase, contradicting references/tdd.md's own contract. Drops the MVP conjunct from the per-task gate and the end-of-phase review escalation; rescopes execute-mvp-tdd.md's load condition, gsd-executor's gate section, and mvp-concepts' intersection claim. MVP remains free to imply TDD; the file is not renamed (stated assumption in the PR body). * test(#4011): scope no-conjunct detector to shell conditions; clean stale MVP+TDD phrasing Review follow-ups: the detector now only inspects if/[ condition lines so explanatory prose mentioning both flags cannot trip it; remaining 'under/outside MVP+TDD' phrases in execute-phase.md, the gate reference, and docs/INVENTORY.md now describe TDD-mode semantics. Emitted-Drift-Ack-Growth: execute-phase.md — TDD-gate decoupling comment + escalation rescoping (#4011) Emitted-Drift-Ack-Growth: gsd-executor.md — gate section trigger rescoped to TDD_MODE alone (#4011) * chore(#4011): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/clever-elks-sing.md
Normal file
5
.changeset/clever-elks-sing.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 4180
|
||||
---
|
||||
**`workflow.tdd_mode: true` now actually enforces TDD** — the RED-commit runtime gate no longer requires MVP mode, so the obvious TDD opt-in stops being silently inert on non-MVP phases; the end-of-phase TDD review escalation follows the same decoupling. (#4011)
|
||||
@@ -423,7 +423,7 @@ If RED or GREEN gate commits are missing, add a warning to SUMMARY.md under a `#
|
||||
|
||||
## MVP+TDD Gate
|
||||
|
||||
**When the orchestrator passes both `MVP_MODE=true` and `TDD_MODE=true`:** Before running the implementation step of any task with `tdd="true"`, run the runtime gate from `~/.claude/gsd-core/references/execute-mvp-tdd.md` (Read it). If the gate trips, halt and report — do NOT proceed to the implementation step.
|
||||
**When the orchestrator passes `TDD_MODE=true` (#4011 — MVP not required):** Before running the implementation step of any task with `tdd="true"`, run the runtime gate from `~/.claude/gsd-core/references/execute-mvp-tdd.md` (Read it). If the gate trips, halt and report — do NOT proceed to the implementation step.
|
||||
|
||||
**Halt-and-report protocol:**
|
||||
|
||||
|
||||
@@ -381,7 +381,7 @@ Full roster at `gsd-core/references/*.md`. References are shared knowledge docum
|
||||
| `executor-examples.md` | Worked examples for the gsd-executor agent. |
|
||||
| `plan-checker-examples.md` | Worked example for the gsd-plan-checker agent (Scope Exceeded), moved out of the inline `<examples>` block to keep worked examples structurally separate from the agent contract, matching the other agents' reference layout. `@`-inlined at load (eager), so this costs ~0.6 KB of runtime context versus keeping it inline — a readability trade, not a size-cap remedy (#3724). |
|
||||
| `doc-conflict-engine.md` | Shared conflict-detection contract for ingest/import workflows. |
|
||||
| `execute-mvp-tdd.md` | Runtime gate semantics for execute-phase under MVP+TDD — pre-task failing-test verification, end-of-phase blocking review. |
|
||||
| `execute-mvp-tdd.md` | Runtime gate semantics for execute-phase under TDD mode — pre-task failing-test verification, end-of-phase blocking review. |
|
||||
| `mvp-concepts.md` | Cross-reference index for the six MVP-related reference files; maps each file to its purpose and which workflow loads it. |
|
||||
| `verify-mvp-mode.md` | UAT framing rules for MVP-mode phases — user-flow-first ordering, deferred technical checks, user-story-format guard. |
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# Execute-Phase — MVP+TDD Gate (Runtime Enforcement)
|
||||
# Execute-Phase — TDD Gate (Runtime Enforcement)
|
||||
|
||||
> Loaded by `execute-phase` workflow and `gsd-executor` agent only when **both** `MVP_MODE=true` AND `TDD_MODE=true` for the phase. Defines the runtime gate that blocks behavior-adding tasks until a failing-test commit exists.
|
||||
> Loaded by `execute-phase` workflow and `gsd-executor` agent when `TDD_MODE=true` for the phase (#4011 — the gate no longer requires MVP mode; MVP may imply TDD, but TDD never requires MVP). Defines the runtime gate that blocks behavior-adding tasks until a failing-test commit exists.
|
||||
|
||||
## When this gate fires
|
||||
|
||||
- `MVP_MODE` is `true` (resolved from CLI flag → ROADMAP `**Mode:**` field → config; see `gsd-core/references/planner-mvp-mode.md`).
|
||||
- `TDD_MODE` is `true` (resolved from `--tdd` flag → `workflow.tdd_mode` config).
|
||||
- `TDD_MODE` is `true` (resolved from `--tdd` flag → `workflow.tdd_mode` config). MVP mode is NOT required (#4011).
|
||||
- The current task being executed has `tdd="true"` in its `<task>` frontmatter (set by the planner per Phase 1).
|
||||
- The task's `<behavior>` block lists at least one expected behavior.
|
||||
|
||||
@@ -13,7 +12,7 @@ If any of these is false, the gate is inactive — execution proceeds normally.
|
||||
|
||||
## What the gate checks
|
||||
|
||||
For each task gated by MVP+TDD, the executor MUST verify (before running the implementation step):
|
||||
For each task gated by TDD, the executor MUST verify (before running the implementation step):
|
||||
|
||||
1. **A failing-test commit exists.** Search git log on the current branch for a commit matching `test({phase}-{plan})` whose subject mentions the same plan as the current task. The commit must touch a test file (`*.test.*`, `*.spec.*`, `tests/**`).
|
||||
2. **The test was actually red.** The commit message body or the executor's recent shell history must show the test failed when first run. Acceptable evidence:
|
||||
@@ -40,7 +39,7 @@ The executor MUST:
|
||||
2. Emit a structured halt report:
|
||||
|
||||
```
|
||||
### MVP+TDD GATE TRIPPED — Plan {plan_id}, Task {task_id}
|
||||
### TDD GATE TRIPPED — Plan {plan_id}, Task {task_id}
|
||||
|
||||
Reason: {missing_red_commit | red_commit_not_failing | feat_before_test}
|
||||
|
||||
@@ -56,14 +55,14 @@ The executor MUST:
|
||||
3. Exit the current execution wave cleanly. Do NOT roll back any prior commits in the same wave.
|
||||
4. Update `STATE.md` with `last_gate_trip: {plan_id}/{task_id}` so the user can resume after writing the test.
|
||||
|
||||
## Escalation: end-of-phase TDD review under MVP+TDD
|
||||
## Escalation: end-of-phase TDD review under TDD
|
||||
|
||||
The existing end-of-phase TDD review (in `workflows/execute-phase.md`'s `tdd_review_checkpoint` step) is normally **advisory** — it surfaces gate violations but does not block phase completion.
|
||||
|
||||
Under MVP+TDD, escalate this to **blocking**:
|
||||
Under TDD mode, escalate this to **blocking**:
|
||||
- If any TDD plan is missing a RED or GREEN commit, the executor MUST refuse to mark the phase complete.
|
||||
- The user is shown the same review table, but the verdict line reads:
|
||||
> "Phase blocked: {N} TDD plan(s) violate the RED→GREEN gate sequence under MVP+TDD. Resolve and re-run /gsd execute-phase, or override with `/gsd execute-phase {phase} --force-mvp-gate` to ship anyway."
|
||||
> "Phase blocked: {N} TDD plan(s) violate the RED→GREEN gate sequence under TDD. Resolve and re-run /gsd execute-phase, or override with `/gsd execute-phase {phase} --force-mvp-gate` to ship anyway."
|
||||
|
||||
The `--force-mvp-gate` flag is documented but not introduced by this plan — it is the escape hatch the spec mentions; if the user later builds it, the workflow already references the contract.
|
||||
|
||||
@@ -76,4 +75,4 @@ The `--force-mvp-gate` flag is documented but not introduced by this plan — it
|
||||
|
||||
## Compatibility with existing TDD discipline
|
||||
|
||||
This gate is additive to `gsd-core/references/tdd.md`. Tasks not under MVP+TDD continue to use the existing advisory TDD discipline (RED/GREEN/REFACTOR commits with end-of-phase review checkpoint). Only the runtime gate and the blocking escalation are new.
|
||||
This gate is additive to `gsd-core/references/tdd.md`. Tasks not under TDD mode continue to use the existing advisory TDD discipline (RED/GREEN/REFACTOR commits with end-of-phase review checkpoint). Only the runtime gate and the blocking escalation are new.
|
||||
|
||||
@@ -12,7 +12,7 @@ Canonical domain terms for the concepts named below live in [CONTEXT.md](../../C
|
||||
| `gsd-core/references/skeleton-template.md` | **Template.** Shape of `SKELETON.md` for new-project Phase 1 under `--mvp`. | `gsd-planner` agent when the Walking Skeleton gate fires |
|
||||
| `gsd-core/references/user-story-template.md` | **Template.** Format and slot definitions for `As a / I want to / So that`. | `gsd-mvp-phase` workflow during interactive prompting; `gsd-planner` when emitting the `## Phase Goal` header |
|
||||
| `gsd-core/references/spidr-splitting.md` | **Splitting discipline.** Five-axis decomposition (Spike, Paths, Interfaces, Data, Rules) for stories too large for one phase. | `gsd-mvp-phase` workflow when the user story exceeds size threshold |
|
||||
| `gsd-core/references/execute-mvp-tdd.md` | **Gate.** MVP+TDD runtime gate semantics: when it fires, what it checks, halt-and-report protocol, end-of-phase blocking escalation, Behavior-Adding Task definition. | `gsd-executor` agent when `MVP_MODE=true && TDD_MODE=true` |
|
||||
| `gsd-core/references/execute-mvp-tdd.md` | **Gate.** TDD runtime gate semantics: when it fires, what it checks, halt-and-report protocol, end-of-phase blocking escalation, Behavior-Adding Task definition. | `gsd-executor` agent when `TDD_MODE=true` (#4011) |
|
||||
| `gsd-core/references/verify-mvp-mode.md` | **UAT framing.** Three-section UAT structure (user-flow → technical → coverage), anti-patterns, `User Flow Coverage` section in VERIFICATION.md. | `gsd-verifier` agent when the phase under verification has `mode: mvp` |
|
||||
|
||||
## Concept-to-file map
|
||||
@@ -33,7 +33,7 @@ If you're looking for the canonical statement of a concept, this is where to fin
|
||||
|
||||
- **`--mvp` and `--prd <file>` together on Phase 1.** Both paths converge at the planner spawn. The PRD express path creates `CONTEXT.md` from the PRD file and continues to the research step; the Walking Skeleton gate fires independently when Phase 1 + new project + `--mvp`. The planner therefore receives both `WALKING_SKELETON=true` and PRD-derived context. This is intentional: the PRD informs what the skeleton should prove.
|
||||
- **`MVP_MODE` is all-or-nothing per phase, not per task.** A phase is either MVP-mode or standard. Mixed-mode phases are not supported (PRD #2826 Q1).
|
||||
- **`TDD_MODE` is independent of `MVP_MODE`.** TDD can be on without MVP, MVP can be on without TDD. Only the *intersection* (both true) activates the MVP+TDD Gate.
|
||||
- **`TDD_MODE` is independent of `MVP_MODE`.** TDD can be on without MVP, MVP can be on without TDD. The TDD runtime gate activates on `TDD_MODE` alone (#4011); MVP mode remains free to imply TDD without being required by it.
|
||||
- **The `gsd-roadmapper` agent makes the MVP/standard decision once at project init** based on `PROJECT_MODE`. Per-phase opt-in/out happens later via `/gsd:mvp-phase` or `/gsd-edit-phase`.
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -194,15 +194,15 @@ Offer these recovery options:
|
||||
- `mark-and-skip` — record the anomaly and move on only with explicit confirmation.
|
||||
</step>
|
||||
|
||||
**MVP+TDD gate.** Task-scoped enforcement runs inside plan execution (immediately before each implementation step), where `TASK_FILE`, `PLAN_ID`, and `TASK_ID` are defined. Keep the same predicate and RED-commit contract:
|
||||
**TDD gate.** Task-scoped enforcement runs inside plan execution (immediately before each implementation step), where `TASK_FILE`, `PLAN_ID`, and `TASK_ID` are defined. #4011: the gate keys on `TDD_MODE` ALONE — a discipline gate coupled to the product-scope `MVP_MODE` flag was silently inert on every non-MVP phase, contradicting `gsd-core/references/tdd.md`'s contract that `workflow.tdd_mode` binds for all `type: tdd` plans. MVP mode remains free to imply TDD; it is no longer required by it. Keep the same predicate and RED-commit contract:
|
||||
```bash
|
||||
if [ "$MVP_MODE" = "true" ] && [ "$TDD_MODE" = "true" ]; then
|
||||
if [ "$TDD_MODE" = "true" ]; then
|
||||
IS_BEHAVIOR_ADDING=$(gsd_run query task.is-behavior-adding "$TASK_FILE" --pick is_behavior_adding)
|
||||
if [ "$IS_BEHAVIOR_ADDING" = "true" ]; then
|
||||
RED_COMMIT=$(git log --oneline --grep="^test(${PHASE_NUMBER}-${PLAN_ID}):" -- "**/*.test.*" "**/*.spec.*" "tests/" | head -1)
|
||||
if [ -z "$RED_COMMIT" ]; then
|
||||
gsd_run query state.update last_gate_trip "${PLAN_ID}/${TASK_ID}" || true
|
||||
echo "MVP+TDD GATE TRIPPED: missing RED commit for ${PLAN_ID}/${TASK_ID}"
|
||||
echo "TDD GATE TRIPPED: missing RED commit for ${PLAN_ID}/${TASK_ID}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -1245,16 +1245,16 @@ CHECK_EXIT=$?
|
||||
|
||||
**Gate evaluation** uses the same two-step contract as `execute:wave:post` above.
|
||||
|
||||
**TDD review escalation (overrides the advisory default for the `tdd.review-checkpoint` gate only).** The tdd `execute:post` gate is declared `blocking: false`, so by the generic contract above it displays its `message`/table and continues. There is ONE documented exception (see `~/.claude/gsd-core/references/execute-mvp-tdd.md`): when `MVP_MODE=true` AND `TDD_MODE=true` AND `GATE_RESULT.block == true` (one or more TDD plans miss a RED or GREEN gate commit), the end-of-phase TDD review escalates from advisory to **blocking under MVP+TDD** — refuse to mark the phase complete and present:
|
||||
**TDD review escalation (overrides the advisory default for the `tdd.review-checkpoint` gate only).** The tdd `execute:post` gate is declared `blocking: false`, so by the generic contract above it displays its `message`/table and continues. There is ONE documented exception (see `~/.claude/gsd-core/references/execute-mvp-tdd.md`): when `TDD_MODE=true` AND `GATE_RESULT.block == true` (one or more TDD plans miss a RED or GREEN gate commit; #4011 — no MVP condition), the end-of-phase TDD review escalates from advisory to **blocking under TDD** — refuse to mark the phase complete and present:
|
||||
|
||||
```
|
||||
Phase blocked: {N} TDD plan(s) violate the RED→GREEN gate sequence under MVP+TDD.
|
||||
Phase blocked: {N} TDD plan(s) violate the RED→GREEN gate sequence under TDD.
|
||||
Resolve and re-run /gsd execute-phase, or override with /gsd execute-phase {phase} --force-mvp-gate to ship anyway.
|
||||
```
|
||||
|
||||
(`--force-mvp-gate` is the documented, not-yet-implemented escape hatch.) Outside MVP+TDD, TDD-review violations remain advisory (table shown, execution continues).
|
||||
(`--force-mvp-gate` is the documented, not-yet-implemented escape hatch.) Outside TDD mode, TDD-review violations remain advisory (table shown, execution continues).
|
||||
|
||||
**Proceed rule:** If `MVP_MODE && TDD_MODE && GATE_RESULT.block == true` for `tdd.review-checkpoint`: STOP — do NOT proceed to `close_parent_artifacts`, `regression_gate`, `verify_phase_goal`, or `phase.complete`. Otherwise proceed normally.
|
||||
**Proceed rule:** If `TDD_MODE && GATE_RESULT.block == true` for `tdd.review-checkpoint`: STOP — do NOT proceed to `close_parent_artifacts`, `regression_gate`, `verify_phase_goal`, or `phase.complete`. Otherwise proceed normally.
|
||||
</step>
|
||||
|
||||
<!-- gsd:section id="gap-closure-artifacts" when="state:gap-closure-phase" -->
|
||||
|
||||
@@ -34,14 +34,21 @@ function parseGateContract(content) {
|
||||
return {
|
||||
hasMvpModeVariable: lowerLines.some(line => line.includes('mvp_mode')),
|
||||
hasRoadmapModeResolution: lowerLines.some(line => line.includes('phase.mvp-mode') || line.includes('roadmap') && line.includes('mode')),
|
||||
hasDualGateCondition: lowerLines.some(line => line.includes('mvp_mode') && line.includes('tdd_mode')),
|
||||
hasGateLabel: lowerLines.some(line => line.includes('mvp+tdd gate') || line.includes('mvp-tdd gate')),
|
||||
// #4011: the gate keys on TDD_MODE ALONE. Any line conjoining MVP_MODE and
|
||||
// TDD_MODE re-couples a discipline gate to a product-scope flag and makes
|
||||
// workflow.tdd_mode=true silently inert on non-MVP phases.
|
||||
hasTddOnlyGateCondition: lines.some(line => /\$TDD_MODE.{0,40}=.{0,20}"true"/.test(line) && !/MVP_MODE/.test(line)),
|
||||
// Scoped to shell-condition lines only — explanatory prose legitimately
|
||||
// mentions both flags on one line.
|
||||
hasNoMvpTddConjunct: !lines.some(line =>
|
||||
(/if \[|\] &&|&& \[/.test(line)) && /MVP_MODE/.test(line) && /TDD_MODE/.test(line)),
|
||||
hasTddOnlyEscalation: !/mvp_mode\s*(=|&&|and)/i.test((content.split('tdd review escalation')[1] || '') ),
|
||||
hasGateLabel: lowerLines.some(line => line.includes('mvp+tdd gate') || line.includes('mvp-tdd gate') || line.includes('tdd gate')),
|
||||
hasRedCommitRule: lowerLines.some(line => line.includes('failing-test commit') || line.includes('missing red commit') || line.includes('test(')),
|
||||
// Must assert the REAL refusal semantics, not merely the words "blocking" + "mvp+tdd"
|
||||
// (which "advisory (blocking: false) ... under MVP+TDD" would satisfy as a false green).
|
||||
hasBlockingEscalation:
|
||||
content.toLowerCase().includes('mvp+tdd')
|
||||
&& (content.toLowerCase().includes('refuse to mark the phase complete')
|
||||
(content.toLowerCase().includes('refuse to mark the phase complete')
|
||||
|| content.toLowerCase().includes('phase blocked')),
|
||||
hasReferenceDoc: lowerLines.some(line => line.includes('execute-mvp-tdd.md')),
|
||||
// Must NOT have an unconditional "proceed regardless of gate results" that overrides the block.
|
||||
@@ -58,8 +65,10 @@ describe('execute-phase — MVP+TDD gate', () => {
|
||||
assert.ok(contract.hasRoadmapModeResolution, 'must consult phase mode from roadmap');
|
||||
});
|
||||
|
||||
test('gate fires when both MVP_MODE and TDD_MODE are true', () => {
|
||||
assert.ok(contract.hasDualGateCondition, 'workflow must combine MVP_MODE and TDD_MODE for the gate');
|
||||
test('gate fires on TDD_MODE alone — no MVP conjunct (#4011)', () => {
|
||||
assert.ok(contract.hasTddOnlyGateCondition, 'the per-task gate must key on TDD_MODE alone');
|
||||
assert.ok(contract.hasNoMvpTddConjunct,
|
||||
'no shipped line may conjoin MVP_MODE with TDD_MODE as a gate condition — workflow.tdd_mode=true must enforce TDD on non-MVP phases too (#4011)');
|
||||
});
|
||||
|
||||
test('per-task gate is documented before behavior-adding task execution', () => {
|
||||
@@ -67,8 +76,9 @@ describe('execute-phase — MVP+TDD gate', () => {
|
||||
assert.ok(contract.hasRedCommitRule, 'must reference failing-test commit check');
|
||||
});
|
||||
|
||||
test('end-of-phase TDD review escalates to blocking under MVP+TDD', () => {
|
||||
test('end-of-phase TDD review escalates to blocking under TDD (#4011)', () => {
|
||||
assert.ok(contract.hasBlockingEscalation, 'must escalate end-of-phase review to blocking');
|
||||
assert.ok(contract.hasTddOnlyEscalation, 'the escalation must not require MVP_MODE (#4011)');
|
||||
});
|
||||
|
||||
test('proceed past TDD escalation is conditional — not an unconditional override', () => {
|
||||
@@ -115,3 +125,17 @@ describe('execute-phase MVP+TDD — resolution chain integration', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #4011: the gate semantics reference loads on TDD_MODE alone ─────────────
|
||||
|
||||
describe('execute-mvp-tdd.md — load condition decoupled from MVP (#4011)', () => {
|
||||
const REF = path.join(__dirname, '..', 'gsd-core', 'references', 'execute-mvp-tdd.md');
|
||||
const content = fs.readFileSync(REF, 'utf-8');
|
||||
|
||||
test('reference no longer requires MVP_MODE to load', () => {
|
||||
const head = content.slice(0, 1200);
|
||||
assert.ok(!/MVP_MODE\s*=\s*true\s*(AND|&&|and)\s*TDD_MODE|both.{0,20}MVP_MODE\s*=\s*true\s*AND/i.test(head),
|
||||
'execute-mvp-tdd.md must load on TDD_MODE alone (#4011)');
|
||||
assert.ok(/TDD_MODE\s*=\s*true/i.test(head), 'load condition must still name TDD_MODE');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,16 @@ const REF = path.join(__dirname, '..', 'gsd-core', 'references', 'execute-mvp-td
|
||||
describe('gsd-executor — MVP+TDD gate section', () => {
|
||||
const content = fs.readFileSync(AGENT, 'utf-8');
|
||||
|
||||
test('agent defines an MVP+TDD Gate section', () => {
|
||||
assert.match(content, /MVP\+TDD\s*Gate|MVP[\s-]?TDD[\s-]?gate/i, 'must label the gate');
|
||||
test('agent defines a TDD Gate section keyed on TDD_MODE alone (#4011)', () => {
|
||||
assert.match(content, /MVP\+TDD\s*Gate|MVP[\s-]?TDD[\s-]?gate|TDD\s*Gate/i, 'must label the gate');
|
||||
// The gate's trigger must not require MVP_MODE (#4011): a discipline gate
|
||||
// keyed to a product-scope flag is silently inert on non-MVP phases.
|
||||
const gateSection = content.slice(
|
||||
content.search(/## (?:MVP\+TDD )?TDD Gate/i),
|
||||
content.indexOf('##', content.search(/## (?:MVP\+TDD )?TDD Gate/i) + 3),
|
||||
);
|
||||
assert.ok(!/MVP_MODE\s*=\s*"?"?true"?.{0,80}TDD_MODE|both .MVP_MODE.= true and .TDD_MODE.= true/i.test(gateSection),
|
||||
'the executor gate section must trigger on TDD_MODE alone, not the MVP intersection (#4011)');
|
||||
});
|
||||
|
||||
test('agent instructs halt-and-report when gate trips', () => {
|
||||
|
||||
Reference in New Issue
Block a user