tdd-enforcer 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,9 +38,6 @@ function recoverState(
38
38
  if (label === "init") {
39
39
  return { enabled: false, current: "red" };
40
40
  }
41
- if (label !== "red" && label !== "green" && label !== "refactor") {
42
- return { enabled: false, current: "red" };
43
- }
44
41
  const next = deps.nextPhase(label);
45
42
  return { enabled: true, current: next ?? "red" };
46
43
  }
@@ -382,15 +382,20 @@ describe("executeTddStatus", () => {
382
382
  ).rejects.toThrow("Missing .pi/tdd/");
383
383
  });
384
384
 
385
- it("throws when TDD disabled", async () => {
385
+ it("returns status details when TDD disabled", async () => {
386
386
  mockLoadTddState.mockReturnValue({
387
387
  ok: true,
388
388
  state: { enabled: false, current: "red" },
389
389
  config: CONFIG,
390
390
  });
391
- await expect(
392
- executeTddStatus({ cwd: "/test" } as any, makeDeps()),
393
- ).rejects.toThrow("not enabled");
391
+ const result = await executeTddStatus({ cwd: "/test" } as any, makeDeps());
392
+ expect(result.content[0].text).toContain("disabled");
393
+ expect(result.content[0].text).toContain("RED");
394
+ expect(result.content[0].text).toContain("tests/**/*.test.ts");
395
+ expect(result.content[0].text).toContain("src/**/*.ts");
396
+ expect(result.details).toBeDefined();
397
+ expect(result.details?.enabled).toBe(false);
398
+ expect(result.details?.phase).toBe("red");
394
399
  });
395
400
 
396
401
  it("returns status details when TDD enabled", async () => {
@@ -292,18 +292,15 @@ export async function executeTddStatus(
292
292
  });
293
293
  throw new Error(`TDD: ${result.reason}`);
294
294
  }
295
- if (!result.state.enabled) {
296
- deps.tddLog(tddDir, "WARN", "tdd_status: TDD disabled");
297
- throw new Error("TDD is not enabled. Run /tdd:on to enable it.");
298
- }
299
-
300
295
  const { state, config } = result;
296
+ const enabledStr = state.enabled ? "enabled" : "disabled";
301
297
  const phaseStr = state.current.toUpperCase();
302
298
  const redBlk = config.blockedInRed.join(", ") || "(none)";
303
299
  const greenBlk = config.blockedInGreen.join(", ") || "(none)";
304
300
  const commands = config.testCommands.join(", ") || "(none)";
305
301
 
306
302
  deps.tddLog(tddDir, "INFO", "tdd_status: queried", {
303
+ enabled: state.enabled,
307
304
  phase: state.current,
308
305
  });
309
306
 
@@ -312,7 +309,7 @@ export async function executeTddStatus(
312
309
  {
313
310
  type: "text",
314
311
  text:
315
- `\nTDD enforcer enabled\n` +
312
+ `\nTDD enforcer ${enabledStr}\n` +
316
313
  `Current phase: ${phaseStr}\n` +
317
314
  `Blocked in RED: ${redBlk}\n` +
318
315
  `Blocked in GREEN: ${greenBlk}\n` +
@@ -320,7 +317,7 @@ export async function executeTddStatus(
320
317
  },
321
318
  ],
322
319
  details: {
323
- enabled: true,
320
+ enabled: state.enabled,
324
321
  phase: state.current,
325
322
  blockedInRed: config.blockedInRed,
326
323
  blockedInGreen: config.blockedInGreen,
@@ -61,6 +61,24 @@ describe("loadConfig", () => {
61
61
  });
62
62
  });
63
63
 
64
+ it("defaults timeoutSeconds to 120 when omitted", () => {
65
+ withTempDir((dir) => {
66
+ const tddDir = join(dir, ".pi", "tdd");
67
+ mkdirSync(tddDir, { recursive: true });
68
+ writeFileSync(
69
+ join(tddDir, "rules.json"),
70
+ JSON.stringify({
71
+ blockedInRed: ["tests/**/*.test.ts"],
72
+ blockedInGreen: ["src/**/*.ts"],
73
+ testCommands: ["npm test"],
74
+ }),
75
+ "utf-8",
76
+ );
77
+ const config = loadConfig(dir);
78
+ expect(config.timeoutSeconds).toBe(120);
79
+ });
80
+ });
81
+
64
82
  describe("validation — throws on invalid content", () => {
65
83
  it("throws when blockedInRed is not an array", () => {
66
84
  withTempDir((dir) => {
@@ -172,5 +190,56 @@ describe("loadConfig", () => {
172
190
  expect(() => loadConfig(dir)).toThrow();
173
191
  });
174
192
  });
193
+
194
+ it("throws when blockedInRed contains non-strings", () => {
195
+ withTempDir((dir) => {
196
+ const tddDir = join(dir, ".pi", "tdd");
197
+ mkdirSync(tddDir, { recursive: true });
198
+ writeFileSync(
199
+ join(tddDir, "rules.json"),
200
+ JSON.stringify({
201
+ blockedInRed: [123],
202
+ blockedInGreen: ["src/**/*.ts"],
203
+ testCommands: ["npm test"],
204
+ }),
205
+ "utf-8",
206
+ );
207
+ expect(() => loadConfig(dir)).toThrow();
208
+ });
209
+ });
210
+
211
+ it("throws when blockedInGreen contains non-strings", () => {
212
+ withTempDir((dir) => {
213
+ const tddDir = join(dir, ".pi", "tdd");
214
+ mkdirSync(tddDir, { recursive: true });
215
+ writeFileSync(
216
+ join(tddDir, "rules.json"),
217
+ JSON.stringify({
218
+ blockedInRed: ["tests/**/*.test.ts"],
219
+ blockedInGreen: [null],
220
+ testCommands: ["npm test"],
221
+ }),
222
+ "utf-8",
223
+ );
224
+ expect(() => loadConfig(dir)).toThrow();
225
+ });
226
+ });
227
+
228
+ it("throws when testCommands contains non-strings", () => {
229
+ withTempDir((dir) => {
230
+ const tddDir = join(dir, ".pi", "tdd");
231
+ mkdirSync(tddDir, { recursive: true });
232
+ writeFileSync(
233
+ join(tddDir, "rules.json"),
234
+ JSON.stringify({
235
+ blockedInRed: ["tests/**/*.test.ts"],
236
+ blockedInGreen: ["src/**/*.ts"],
237
+ testCommands: ["npm test", 456],
238
+ }),
239
+ "utf-8",
240
+ );
241
+ expect(() => loadConfig(dir)).toThrow();
242
+ });
243
+ });
175
244
  });
176
245
  });
package/engine/config.ts CHANGED
@@ -16,15 +16,24 @@ export function loadConfig(projectRoot: string): Config {
16
16
  if (!Array.isArray(parsed.blockedInRed) || parsed.blockedInRed.length === 0) {
17
17
  throw new Error("rules.json: blockedInRed must be a non-empty array");
18
18
  }
19
+ if (!parsed.blockedInRed.every((p: unknown) => typeof p === "string")) {
20
+ throw new Error("rules.json: blockedInRed must contain only strings");
21
+ }
19
22
  if (
20
23
  !Array.isArray(parsed.blockedInGreen) ||
21
24
  parsed.blockedInGreen.length === 0
22
25
  ) {
23
26
  throw new Error("rules.json: blockedInGreen must be a non-empty array");
24
27
  }
28
+ if (!parsed.blockedInGreen.every((p: unknown) => typeof p === "string")) {
29
+ throw new Error("rules.json: blockedInGreen must contain only strings");
30
+ }
25
31
  if (!Array.isArray(parsed.testCommands) || parsed.testCommands.length === 0) {
26
32
  throw new Error("rules.json: testCommands must be a non-empty array");
27
33
  }
34
+ if (!parsed.testCommands.every((p: unknown) => typeof p === "string")) {
35
+ throw new Error("rules.json: testCommands must contain only strings");
36
+ }
28
37
 
29
38
  return {
30
39
  blockedInRed: parsed.blockedInRed,
package/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "tdd-enforcer",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "scripts": {
6
- "fmt": "biome format --write .",
7
6
  "lint": "biome check .",
8
7
  "lint:fix": "biome check --write .",
9
8
  "test": "vitest run"
@@ -42,8 +42,10 @@ It locks files per phase — only test files in RED, only implementation files i
42
42
  - `blockedInRed` — globs the agent **cannot** modify in RED phase (implementation files)
43
43
  - `blockedInGreen` — globs the agent **cannot** modify in GREEN phase (test files)
44
44
  - `!` exclusion prefix — optional, carves out subsets from a block list at init time. E.g. `!src/**/*.test.ts` excludes co-located test files from `blockedInRed` so the agent can write them in RED phase
45
- - `testCommands` — shell commands to run tests
46
- - `timeoutSeconds` — test timeout per command
45
+ - `testCommands` — determines if a phase transition passes. Exit 0 passes, non-zero blocks. **Runs in parallel** — all entries are started concurrently. Use `&&` inside a single string entry to chain multiple commands in one step (e.g. `"npm run build && npm test"`). Do not rely on array ordering for dependency chains; put dependent commands in the same string entry with `&&`.
46
+
47
+ **Prefer auto-fix commands** that apply fixes (formatting, linting, etc.) before reporting remaining violations. Without auto-fix, formatting or lint issues in phase-locked files (e.g. test files in GREEN) will block the gate with no way to fix them — forcing `previous_tdd_phase` and losing all progress. Auto-fix commands avoid this deadlock by fixing locked files before the check runs.
48
+ - `timeoutSeconds` — test timeout per command (default: 120)
47
49
 
48
50
  2. **User** runs `/tdd:on` to enable enforcement.
49
51
 
package/DESIGN.md DELETED
@@ -1,183 +0,0 @@
1
- # TDD Enforcer — pi Extension Design
2
-
3
- ## Concept
4
-
5
- A pi extension that enforces the Red-Green-Refactor TDD cycle by:
6
- - Tracking current phase (RED / GREEN / REFACTOR)
7
- - Restricting which files the agent can modify per phase
8
- - Running tests on phase transitions to enforce red/green gate
9
- - Nudging the agent with phase-appropriate prompts
10
-
11
- ## Tools
12
-
13
- ### `next_tdd_phase`
14
-
15
- Advances the cycle:
16
-
17
- ```
18
- RED ──(tests fail)──► GREEN ──(tests pass)──► REFACTOR ──(tests pass)──► RED
19
- ```
20
-
21
- ### `previous_tdd_phase`
22
-
23
- Reverts to the previous snapshot by parsing the phase label from the last git snapshot commit. Restores working tree to exact prior state. No gate checks — just revert. Must have clear warning in the tool schema that this will revert all changes in the current state.
24
-
25
- ---
26
-
27
- ## Phase Rules
28
-
29
- | Phase | `allowedRedPhaseFiles` | `allowedGreenPhaseFiles` | Everything else |
30
- |-------|----------------------|------------------------|-----------------|
31
- | RED | ✅ Allowed | ❌ Locked | ✅ Free |
32
- | GREEN | ❌ Locked | ✅ Allowed | ✅ Free |
33
- | REFACTOR | ✅ Allowed | ✅ Allowed | ✅ Free |
34
-
35
- ### Transition Gates
36
-
37
- - **RED → GREEN**: Tests must **fail**. If tests pass, tool returns error — agent must break a test first.
38
- - **GREEN → REFACTOR**: Tests must **pass** (all exit codes zero).
39
- - **REFACTOR → RED**: Tests must **pass** (all exit codes zero).
40
-
41
- ### Nudging Prompts
42
-
43
- Each successful transition returns a message guiding the agent:
44
-
45
- - **→ RED**: *"You are now in **RED** phase. Write failing tests matching `allowedRedPhaseFiles` patterns. Only these files can be modified. Once tests fail, call `next_tdd_phase` to proceed to GREEN."* (list matched files)
46
- - **→ GREEN**: *"You are now in **GREEN** phase. Files matching `allowedRedPhaseFiles` are locked. Implement features in `allowedGreenPhaseFiles` to make tests pass. Call `next_tdd_phase` to proceed to REFACTOR."*
47
- - **→ REFACTOR**: *"You are now in **REFACTOR** phase. Both `allowedRedPhaseFiles` and `allowedGreenPhaseFiles` are free to modify. Refactor without changing behavior. Call `next_tdd_phase` to start a new RED cycle."*
48
-
49
- ---
50
-
51
- ## File Enforcement: Private Git + `tool_call` Fast-Feedback
52
-
53
- ### Source of truth: private git repo
54
-
55
- A separate git repository at `.pi/tdd/.git/` that tracks the project root as its working tree. The user's `.git/` is never touched.
56
-
57
- ```
58
- .pi/tdd/
59
- ├── .gitignore # private git — excludes file patterns from snapshots
60
- ├── state.json # {current: "red", enabled: true}
61
- ├── rules.json # user config
62
- └── .git/ # private git — init with --git-dir
63
- ```
64
-
65
- **Setup:** `git init` with `--git-dir=.pi/tdd/.git --work-tree=<project-root>`.
66
-
67
- **On phase entry (snapshot):**
68
- `git add -A && git commit -m "tdd: <phase> <ts>"` — captures entire working tree state.
69
-
70
- **On `next_tdd_phase` / `previous_tdd_phase` (allowlist check):**
71
- - `git diff --name-only HEAD` against previous snapshot commit
72
- - Cross-reference each changed file against phase allowlist
73
- - Violations → BLOCK with list of disallowed files
74
- - Also check for untracked files (`git ls-files --others --exclude-standard`)
75
-
76
- **On `previous_tdd_phase` (revert):**
77
- - `git restore --source=<prev-commit> --worktree -- .` — restores project to exact prior snapshot
78
- - Pop the last snapshot commit in the private git repo
79
-
80
- ### Benefits of private git
81
-
82
- - Catches ALL modifications — write, edit, bash, sed, python, C, anything — because it diffs the working tree, not tool calls
83
- - Cross-platform (git is everywhere)
84
- - No shell parsing, no fragile regexes, no edge cases
85
- - Zero interference with user's git — different `.git/`, no shared refs, no hooks, no global config
86
- - `.pi/tdd/` is disposable — user can nuke it anytime
87
- - Free diff, merge, partial restore, binary handling — no custom engine to write
88
-
89
- ### Fast feedback: per-tool enforcement
90
-
91
- The transition-time check catches everything, but it's wasteful to let the agent work on wrong files for a full phase. We enforce per-tool:
92
-
93
- #### `write` / `edit` — pre-execution block
94
-
95
- The file path is a direct parameter. In `tool_call`:
96
- - If path is disallowed in current phase → `{ block: true, reason: "..." }`
97
- - Otherwise → allow
98
-
99
- #### `bash` — post-execution detect-and-revert
100
-
101
- Bash can modify files indirectly (redirects, `sed -i`, scripts, compilers). Parsing command strings to predict targets is fragile. Instead:
102
-
103
- 1. **Let bash run** — no pre-check
104
- 2. **In `tool_result`:** `git diff --name-only HEAD` + `git ls-files --others --exclude-standard` to get all changes since phase snapshot
105
- 3. For each file: if it's disallowed in current phase → `git restore <filepath>` and append warning
106
-
107
- No in-memory tracking needed. The check is the same for every file regardless of how it was modified — write/edit changes that passed pre-check naturally match the allowed globs, violations get reverted.
108
-
109
- #### Why not regex bash parsing?
110
-
111
- Everyone else does it (pi-proof, pi-superteam, tdd-guard). It's fragile — misses `$(dynamic paths)`, glob expansion, scripts calling other scripts, piped commands, heredocs with variables. Our git-based post-check catches everything regex misses, with zero false negatives.
112
-
113
- Regex pre-check is optional (could catch obvious cases for better UX) but the git post-check is the reliable enforcer.
114
-
115
- ---
116
-
117
- ### `.gitignore`
118
-
119
- The private git's work-tree is the project root, so it respects the project's `.gitignore` automatically — no copy needed, no separate file needed.
120
-
121
- If the user wants to exclude additional files from TDD snapshots only, they can create `.pi/tdd/.gitignore` with those patterns. Git checks `.gitignore` starting from the work-tree root, so a file there is picked up naturally.
122
-
123
- ---
124
-
125
- ## Config: `.pi/tdd/rules.json`
126
-
127
- ```json
128
- {
129
- "allowedRedPhaseFiles": ["tests/**/*.test.ts", "specs/**/*.spec.ts"],
130
- "allowedGreenPhaseFiles": ["src/**/*.ts"],
131
- "testCommands": ["npm run test:unit", "npm run test:integration"],
132
- "timeoutSeconds": 120
133
- }
134
- ```
135
-
136
- - `allowedRedPhaseFiles`: Glob patterns for files allowed in RED phase (typically test files).
137
- - `allowedGreenPhaseFiles`: Glob patterns for files allowed in GREEN phase (typically implementation files).
138
- - Files matching neither set are free in all phases.
139
- - `testCommands`: `string` (shell-chained with `&&` for sequential) or `string[]` (run in parallel). Must be non-interactive.
140
- - `timeoutSeconds`: Per-command timeout. Extension passes it as pi's `bash` tool timeout param, so we don't rely on system `timeout` binary.
141
-
142
- ---
143
-
144
-
145
-
146
- ---
147
-
148
- ## Phase State Persistence
149
-
150
- Stored in `.pi/tdd/state.json`:
151
-
152
- ```json
153
- {
154
- "current": "green",
155
- "enabled": true
156
- }
157
- ```
158
-
159
- The snapshot history lives in the private git repo's commit log — no explicit stack array needed. `previous_tdd_phase` reads the phase label from the HEAD commit message and restores to the parent. Survives session restarts and extension reloads.
160
-
161
- ---
162
-
163
- ## Open Questions
164
-
165
- ### Initial state
166
-
167
- How does a project start?
168
-
169
- - **Auto RED**: Start in RED unconditionally. If tests already pass, agent sees a warning that there's no failing test yet — it needs to write one or break one.
170
- - **Auto-detect**: Run tests on startup. If failing → RED. If passing → GREEN (agent is already in the "make it pass" phase).
171
- - **Prompt**: Ask the user what phase to start in.
172
-
173
- ### Enabling/disabling
174
-
175
- Should we have `/tdd:on` and `/tdd:off` commands to toggle without unloading the extension?
176
-
177
- ### Config location
178
-
179
- Only `.pi/tdd/rules.json` in project root? Or support nested configs?
180
-
181
- ### Multiple projects / monorepos
182
-
183
- If the project root has sub-projects with different test commands, does rules.json support multiple entries keyed by directory?
package/behaviour.md DELETED
@@ -1,175 +0,0 @@
1
- # TDD Enforcer — Behaviour Spec
2
-
3
- ## Entry Gate — `loadTddState(root)` (all surfaces)
4
-
5
- Every surface (commands, hooks, tools) hits this first:
6
-
7
- ```
8
- loadTddState(root):
9
-
10
- ├── .pi/tdd/ exists? NO → "Missing .pi/tdd/. See the tdd-enforcer skill."
11
-
12
- ├── rules.json exists? NO → "Missing .pi/tdd/rules.json. See the tdd-enforcer skill."
13
-
14
- ├── rules.json valid? NO → "Invalid .pi/tdd/rules.json. See the tdd-enforcer skill."
15
-
16
- ├── .pi/tdd/.git/ exists? NO → initGit(root)
17
- │ FAIL → "Failed to initialise private git repo."
18
-
19
- ├── state.json exists? NO → recoverState()
20
- │ │
21
- │ └── recoverState():
22
- │ ├── .git exists? + headMessage readable?
23
- │ │ ├── "tdd: init" → { enabled: false, current: "red" }
24
- │ │ ├── "tdd: red" → { enabled: true, current: "green" }
25
- │ │ ├── "tdd: green" → { enabled: true, current: "refactor" }
26
- │ │ ├── "tdd: refactor" → { enabled: true, current: "red" }
27
- │ │ └── no match / error → { enabled: false, current: "red" }
28
- │ └── no .git / no commits → { enabled: false, current: "red" }
29
-
30
- ├── state.json valid? NO → recoverState() (same as above)
31
-
32
- └── return { ok: true, state, config }
33
- ```
34
-
35
- After this gate, every surface has `state` + `config`. Then branches on `state.enabled`.
36
-
37
- ---
38
-
39
- ## Commands (user only)
40
-
41
- | Command | Gate check | After gate |
42
- |---------|-----------|------------|
43
- | `tdd:on` | Setup valid? | If `enabled` → error "already on". Init git (if missing), snapshot, set `enabled: true`. |
44
- | `tdd:off` | Setup valid? | If `disabled` → error "already off". Set `enabled: false`. |
45
- | `tdd:status` | Setup valid? | Show state + config regardless of `enabled`. |
46
- | `tdd:reset` | Setup valid? | Nuke private git, re-init, snapshot, set `enabled: false`. |
47
- | `tdd:red` | Setup valid? | If already in RED → no-op. Snapshot working tree, auto-enable if disabled, set `current: "red"`. Notify "Skipped to RED phase". |
48
- | `tdd:green` | Setup valid? | If already in GREEN → no-op. Snapshot working tree, auto-enable if disabled, set `current: "green"`. Notify "Skipped to GREEN phase". |
49
- | `tdd:refactor` | Setup valid? | If already in REFACTOR → no-op. Snapshot working tree, auto-enable if disabled, set `current: "refactor"`. Notify "Skipped to REFACTOR phase". |
50
-
51
- All errors reference the tdd-enforcer skill.
52
-
53
- ### Phase Jump Commands — Usage
54
-
55
- These let the user skip phases they don't need for small changes:
56
-
57
- | Scenario | Command | Why |
58
- |----------|---------|-----|
59
- | "This is just an implementation change, no test needed" | `/tdd:green` | Skip RED, go straight to GREEN. Test files stay locked, implementation files unlocked. |
60
- | "This is just cleanup, not new behaviour" | `/tdd:refactor` | Skip RED + GREEN. All files unlocked. |
61
- | "Done refactoring, start next cycle" | `/tdd:red` | Same as `next_tdd_phase` from REFACTOR but direct — no gate check needed. |
62
-
63
- Unlike `next_tdd_phase`, these commands do NOT run transition gate checks. The user is explicitly choosing to skip a phase — they own the consequences.
64
-
65
- ---
66
-
67
- ## Hooks (agent — automatic)
68
-
69
- | Hook | Gate + enabled | Behaviour |
70
- |------|---------------|-----------|
71
- | `tool_call` (write/edit) | Setup broken → pass through. Disabled → pass through. **Enabled** → block if `relPath` starts with `.pi/tdd/` OR phase-locked. | |
72
- | `tool_result` (bash) | Setup broken → pass through. Disabled → pass through. **Enabled** → revert if path starts with `.pi/tdd/` OR phase-locked. RETOOL revert paths to `.pi/tdd/` prefix check. | |
73
-
74
- ---
75
-
76
- ## Tools (agent — callable)
77
-
78
- | Tool | Gate + enabled | Behaviour |
79
- |------|---------------|-----------|
80
- | `next_tdd_phase` | Disabled → error. Enabled → run gate + snapshot + transition. | |
81
- | `previous_tdd_phase` | Disabled → error. Enabled → revert to previous snapshot. | |
82
- | `tdd_status` | Disabled → error. Enabled → show state + config. | |
83
-
84
- ---
85
-
86
- ## Flow Diagrams
87
-
88
- ### Initial Setup Flow
89
-
90
- ```
91
- User creates .pi/tdd/rules.json
92
-
93
-
94
- User runs /tdd:on
95
-
96
-
97
- Gate: .pi/tdd/ exists? ──NO──► error (agent creates it)
98
-
99
- YES
100
-
101
-
102
- Gate: rules.json exists? ──NO──► error (agent creates it)
103
-
104
- YES
105
-
106
-
107
- Gate: rules.json valid? ──NO──► error (agent fixes it)
108
-
109
- YES
110
-
111
-
112
- Gate: state.json exists? ──NO──► auto-create
113
-
114
- YES/auto-created
115
-
116
-
117
- Init git (if missing) ──► snapshot ──► enabled: true
118
-
119
-
120
- TDD active — RED phase, enforcement on
121
- ```
122
-
123
- ### Phase Cycle Flow
124
-
125
- ```
126
- RED: write tests
127
- │ tests fail? ──NO──► "Tests passed. Add a failing test before transitioning."
128
- │ YES
129
- ├─► next_tdd_phase ──► GREEN
130
-
131
- GREEN: implement features
132
- │ tests pass? ──NO──► "Tests failed. Fix them before transitioning."
133
- │ YES
134
- ├─► next_tdd_phase ──► REFACTOR
135
-
136
- REFACTOR: refactor freely
137
- │ tests pass? ──NO──► "Tests failed. Fix them before transitioning."
138
- │ YES
139
- ├─► next_tdd_phase ──► RED (new cycle)
140
- ```
141
-
142
- ### Bash Enforcement Flow
143
-
144
- ```
145
- Agent runs bash command
146
-
147
-
148
- tool_call hook: stash pre-command state
149
-
150
-
151
- Bash executes (modifies files)
152
-
153
-
154
- tool_result hook: diff against stash
155
-
156
-
157
- For each changed file:
158
- ├── path starts with ".pi/tdd/"? ──YES──► revert, flag as violation
159
- ├── locked in current phase? ──YES──► revert, flag as violation
160
- └── no violations ──► keep changes
161
-
162
-
163
- Return warning listing reverted vs retained files
164
- ```
165
-
166
- ---
167
-
168
- ## Protection Summary
169
-
170
- | Surface | `.pi/tdd/` locked? | Phase-locked files? |
171
- |---------|-------------------|-------------------|
172
- | write/edit (TDD enabled) | ✅ Block | ✅ Block |
173
- | write/edit (TDD disabled) | ❌ Free | ❌ Free |
174
- | bash (TDD enabled) | ✅ Reverted | ✅ Reverted |
175
- | bash (TDD disabled) | ❌ Free | ❌ Free |