tdd-enforcer 0.2.6 → 0.2.8

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.
@@ -1,375 +1,371 @@
1
- import { join } from "node:path";
2
- import { Type } from "typebox";
3
1
  import { exec } from "node:child_process";
2
+ import { join } from "node:path";
4
3
  import { promisify } from "node:util";
4
+ import { Type } from "typebox";
5
5
 
6
6
  const asyncExec = promisify(exec);
7
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+
8
+ import type {
9
+ ExtensionAPI,
10
+ ExtensionContext,
11
+ } from "@earendil-works/pi-coding-agent";
12
+ import type { Phase, TestRunner } from "../../engine/index.js";
8
13
  import {
9
- savePhaseState,
10
- nextPhase,
11
- checkGate,
12
- getDisallowedChanges,
13
- snapshot,
14
- hasParent,
15
- resetHard,
16
- undoLastCommit,
17
- headMessage,
14
+ checkGate,
15
+ getDisallowedChanges,
16
+ hasParent,
17
+ headMessage,
18
+ nextPhase,
19
+ resetHard,
20
+ savePhaseState,
21
+ snapshot,
22
+ undoLastCommit,
18
23
  } from "../../engine/index.js";
19
- import type { TestRunner, Phase } from "../../engine/index.js";
20
- import { getNudgePrompt } from "./prompts.js";
21
24
  import { loadTddState } from "./helpers.js";
22
25
  import { tddLog } from "./log.js";
26
+ import { getNudgePrompt } from "./prompts.js";
23
27
 
24
28
  // ── De dependency types ─────────────────────────────────────────────────────
25
29
 
26
30
  export interface NextPhaseDeps {
27
- loadTddState: typeof loadTddState;
28
- nextPhase: typeof nextPhase;
29
- getDisallowedChanges: typeof getDisallowedChanges;
30
- checkGate: typeof checkGate;
31
- snapshot: typeof snapshot;
32
- savePhaseState: typeof savePhaseState;
33
- getNudgePrompt: typeof getNudgePrompt;
34
- asyncExec: (command: string, options?: { cwd?: string; timeout?: number }) => Promise<{ stdout: string; stderr: string }>;
35
- tddLog: typeof tddLog;
31
+ loadTddState: typeof loadTddState;
32
+ nextPhase: typeof nextPhase;
33
+ getDisallowedChanges: typeof getDisallowedChanges;
34
+ checkGate: typeof checkGate;
35
+ snapshot: typeof snapshot;
36
+ savePhaseState: typeof savePhaseState;
37
+ getNudgePrompt: typeof getNudgePrompt;
38
+ asyncExec: (
39
+ command: string,
40
+ options?: { cwd?: string; timeout?: number },
41
+ ) => Promise<{ stdout: string; stderr: string }>;
42
+ tddLog: typeof tddLog;
36
43
  }
37
44
 
38
45
  export interface PreviousPhaseDeps {
39
- loadTddState: typeof loadTddState;
40
- hasParent: typeof hasParent;
41
- headMessage: typeof headMessage;
42
- resetHard: typeof resetHard;
43
- undoLastCommit: typeof undoLastCommit;
44
- savePhaseState: typeof savePhaseState;
45
- tddLog: typeof tddLog;
46
+ loadTddState: typeof loadTddState;
47
+ hasParent: typeof hasParent;
48
+ headMessage: typeof headMessage;
49
+ resetHard: typeof resetHard;
50
+ undoLastCommit: typeof undoLastCommit;
51
+ savePhaseState: typeof savePhaseState;
52
+ tddLog: typeof tddLog;
46
53
  }
47
54
 
48
55
  export interface TddStatusDeps {
49
- loadTddState: typeof loadTddState;
50
- tddLog: typeof tddLog;
56
+ loadTddState: typeof loadTddState;
57
+ tddLog: typeof tddLog;
51
58
  }
52
59
 
53
60
  // ── Default deps ────────────────────────────────────────────────────────────
54
61
 
55
62
  const defaultNextPhaseDeps: NextPhaseDeps = {
56
- loadTddState,
57
- nextPhase,
58
- getDisallowedChanges,
59
- checkGate,
60
- snapshot,
61
- savePhaseState,
62
- getNudgePrompt,
63
- asyncExec,
64
- tddLog,
63
+ loadTddState,
64
+ nextPhase,
65
+ getDisallowedChanges,
66
+ checkGate,
67
+ snapshot,
68
+ savePhaseState,
69
+ getNudgePrompt,
70
+ asyncExec,
71
+ tddLog,
65
72
  };
66
73
 
67
74
  const defaultPreviousPhaseDeps: PreviousPhaseDeps = {
68
- loadTddState,
69
- hasParent,
70
- headMessage,
71
- resetHard,
72
- undoLastCommit,
73
- savePhaseState,
74
- tddLog,
75
+ loadTddState,
76
+ hasParent,
77
+ headMessage,
78
+ resetHard,
79
+ undoLastCommit,
80
+ savePhaseState,
81
+ tddLog,
75
82
  };
76
83
 
77
84
  const defaultTddStatusDeps: TddStatusDeps = {
78
- loadTddState,
79
- tddLog,
85
+ loadTddState,
86
+ tddLog,
80
87
  };
81
88
 
82
89
  // ── executeNextPhase ────────────────────────────────────────────────────────
83
90
 
84
91
  export async function executeNextPhase(
85
- ctx: ExtensionContext,
86
- deps: NextPhaseDeps = defaultNextPhaseDeps,
87
- ): Promise<{ content: Array<{ type: string; text: string }>; details?: Record<string, unknown> }> {
88
- const root = ctx.cwd;
89
- const tddDir = join(root, ".pi", "tdd");
90
- const tdd = deps.loadTddState(root);
91
- if (!tdd.ok) {
92
- deps.tddLog(tddDir, "WARN", "next_tdd_phase: TDD not active", { reason: tdd.reason });
93
- return { content: [{ type: "text", text: `TDD: ${tdd.reason}` }], details: {} };
94
- }
95
- if (!tdd.state.enabled) {
96
- deps.tddLog(tddDir, "WARN", "next_tdd_phase: TDD disabled");
97
- return { content: [{ type: "text", text: "TDD is not enabled. Run /tdd:on to enable it." }], details: {} };
98
- }
99
-
100
- const { state, config } = tdd;
101
- const from = state.current;
102
- const to = deps.nextPhase(from) as Phase;
103
-
104
- deps.tddLog(tddDir, "INFO", "next_tdd_phase: starting", { from, to });
105
-
106
- // 1. Allowlist check
107
- const violations = deps.getDisallowedChanges(root, from, config);
108
- if (violations.length > 0) {
109
- deps.tddLog(tddDir, "WARN", "next_tdd_phase: blocked by allowlist", {
110
- from,
111
- violations,
112
- });
113
- return {
114
- content: [
115
- {
116
- type: "text",
117
- text:
118
- `BLOCKED: files not allowed in ${from.toUpperCase()} phase:\n` +
119
- violations.map((f) => ` - ${f}`).join("\n") +
120
- `\nRevert or remove them before proceeding.\n\nInspect with: cd .pi/tdd && git diff HEAD -- ${violations[0]}`,
121
- },
122
- ],
123
- details: {},
124
- };
125
- }
126
-
127
- // 2. Gate check
128
- const testRunner: TestRunner = async (commands, timeout) => {
129
- const results = await Promise.all(
130
- commands.map(async (cmd) => {
131
- try {
132
- await deps.asyncExec(cmd, { cwd: root, timeout: timeout * 1000 });
133
- return { command: cmd, passed: true };
134
- } catch {
135
- return { command: cmd, passed: false };
136
- }
137
- }),
138
- );
139
-
140
- const failed = results.filter((r) => !r.passed);
141
- if (failed.length > 0) {
142
- return {
143
- passed: false,
144
- message: "Tests failed:\n" + failed.map((f) => ` - ${f.command}`).join("\n"),
145
- };
146
- }
147
- return { passed: true, message: "All tests passed." };
148
- };
149
-
150
- const gate = await deps.checkGate(from, to, testRunner, config);
151
- deps.tddLog(tddDir, "DEBUG", "next_tdd_phase: gate result", {
152
- from,
153
- to,
154
- passed: gate.passed,
155
- message: gate.message,
156
- });
157
-
158
- if (!gate.passed) {
159
- return { content: [{ type: "text", text: gate.message }], details: {} };
160
- }
161
-
162
- // 3. Snapshot label with the phase the work was done in
163
- const hash = deps.snapshot(root, from);
164
- deps.tddLog(tddDir, "INFO", "next_tdd_phase: snapshot created", {
165
- from,
166
- to,
167
- hash,
168
- });
169
-
170
- // 4. Save state
171
- state.current = to;
172
- deps.savePhaseState(root, state);
173
- deps.tddLog(tddDir, "INFO", "next_tdd_phase: complete", { from, to });
174
-
175
- return {
176
- content: [{ type: "text", text: deps.getNudgePrompt(to, config) }],
177
- details: {},
178
- };
92
+ ctx: ExtensionContext,
93
+ deps: NextPhaseDeps = defaultNextPhaseDeps,
94
+ ): Promise<{
95
+ content: Array<{ type: string; text: string }>;
96
+ details?: Record<string, unknown>;
97
+ }> {
98
+ const root = ctx.cwd;
99
+ const tddDir = join(root, ".pi", "tdd");
100
+ const tdd = deps.loadTddState(root);
101
+ if (!tdd.ok) {
102
+ deps.tddLog(tddDir, "WARN", "next_tdd_phase: TDD not active", {
103
+ reason: tdd.reason,
104
+ });
105
+ throw new Error(`TDD: ${tdd.reason}`);
106
+ }
107
+ if (!tdd.state.enabled) {
108
+ deps.tddLog(tddDir, "WARN", "next_tdd_phase: TDD disabled");
109
+ throw new Error("TDD is not enabled. Run /tdd:on to enable it.");
110
+ }
111
+
112
+ const { state, config } = tdd;
113
+ const from = state.current;
114
+ const to = deps.nextPhase(from) as Phase;
115
+
116
+ deps.tddLog(tddDir, "INFO", "next_tdd_phase: starting", { from, to });
117
+
118
+ // 1. Allowlist check
119
+ const violations = deps.getDisallowedChanges(root, from, config);
120
+ if (violations.length > 0) {
121
+ deps.tddLog(tddDir, "WARN", "next_tdd_phase: blocked by allowlist", {
122
+ from,
123
+ violations,
124
+ });
125
+ throw new Error(
126
+ `BLOCKED: files not allowed in ${from.toUpperCase()} phase:\n` +
127
+ violations.map((f) => ` - ${f}`).join("\n") +
128
+ `\nRevert or remove them before proceeding.\n\nInspect with: cd .pi/tdd && git diff HEAD -- ${violations[0]}`,
129
+ );
130
+ }
131
+
132
+ // 2. Gate check
133
+ const testRunner: TestRunner = async (commands, timeout) => {
134
+ const results = await Promise.all(
135
+ commands.map(async (cmd) => {
136
+ try {
137
+ await deps.asyncExec(cmd, { cwd: root, timeout: timeout * 1000 });
138
+ return { command: cmd, passed: true };
139
+ } catch {
140
+ return { command: cmd, passed: false };
141
+ }
142
+ }),
143
+ );
144
+
145
+ const failed = results.filter((r) => !r.passed);
146
+ if (failed.length > 0) {
147
+ return {
148
+ passed: false,
149
+ message: `Tests failed:\n${failed.map((f) => ` - ${f.command}`).join("\n")}`,
150
+ };
151
+ }
152
+ return { passed: true, message: "All tests passed." };
153
+ };
154
+
155
+ const gate = await deps.checkGate(from, to, testRunner, config);
156
+ deps.tddLog(tddDir, "DEBUG", "next_tdd_phase: gate result", {
157
+ from,
158
+ to,
159
+ passed: gate.passed,
160
+ message: gate.message,
161
+ });
162
+
163
+ if (!gate.passed) {
164
+ throw new Error(gate.message);
165
+ }
166
+
167
+ // 3. Snapshot — label with the phase the work was done in
168
+ const hash = deps.snapshot(root, from);
169
+ deps.tddLog(tddDir, "INFO", "next_tdd_phase: snapshot created", {
170
+ from,
171
+ to,
172
+ hash,
173
+ });
174
+
175
+ // 4. Save state
176
+ state.current = to;
177
+ deps.savePhaseState(root, state);
178
+ deps.tddLog(tddDir, "INFO", "next_tdd_phase: complete", { from, to });
179
+
180
+ return {
181
+ content: [{ type: "text", text: deps.getNudgePrompt(to, config) }],
182
+ details: {},
183
+ };
179
184
  }
180
185
 
181
186
  // ── executePreviousPhase ────────────────────────────────────────────────────
182
187
 
183
188
  export async function executePreviousPhase(
184
- ctx: ExtensionContext,
185
- deps: PreviousPhaseDeps = defaultPreviousPhaseDeps,
186
- ): Promise<{ content: Array<{ type: string; text: string }>; details?: Record<string, unknown> }> {
187
- const root = ctx.cwd;
188
- const tddDir = join(root, ".pi", "tdd");
189
- const tdd = deps.loadTddState(root);
190
- if (!tdd.ok) {
191
- deps.tddLog(tddDir, "WARN", "previous_tdd_phase: TDD not active", {
192
- reason: tdd.reason,
193
- });
194
- return { content: [{ type: "text", text: `TDD: ${tdd.reason}` }], details: {} };
195
- }
196
- if (!tdd.state.enabled) {
197
- deps.tddLog(tddDir, "WARN", "previous_tdd_phase: TDD disabled");
198
- return { content: [{ type: "text", text: "TDD is not enabled. Run /tdd:on to enable it." }], details: {} };
199
- }
200
-
201
- const { state } = tdd;
202
-
203
- if (!deps.hasParent(root)) {
204
- deps.tddLog(tddDir, "WARN", "previous_tdd_phase: no parent commit", {
205
- phase: state.current,
206
- });
207
- return {
208
- content: [{ type: "text", text: "No previous phase to revert to." }],
209
- details: {},
210
- };
211
- }
212
-
213
- // Read phase from HEAD snapshot commit message (source of truth).
214
- // Snapshot is labeled with the phase the work was done in, so we use
215
- // it directly — no hardcoded phase map needed.
216
- const headMsg = deps.headMessage(root);
217
- const phaseMatch = headMsg.match(/^tdd: (red|green|refactor)/);
218
- if (!phaseMatch) {
219
- deps.tddLog(tddDir, "ERROR", "previous_tdd_phase: invalid HEAD message", {
220
- headMsg,
221
- });
222
- return {
223
- content: [
224
- {
225
- type: "text",
226
- text: `HEAD commit "${headMsg}" is not a TDD snapshot. Cannot determine previous phase.\n` +
227
- `The private git repo at .pi/tdd must not be manually modified. ` +
228
- `Tampering with it will cause TDD state corruption.`,
229
- },
230
- ],
231
- details: {},
232
- };
233
- }
234
- const label = phaseMatch[1];
235
- if (label !== "red" && label !== "green" && label !== "refactor") {
236
- deps.tddLog(tddDir, "ERROR", "previous_tdd_phase: invalid phase label", {
237
- headMsg,
238
- label,
239
- });
240
- return {
241
- content: [
242
- {
243
- type: "text",
244
- text: `HEAD commit "${headMsg}" has unexpected label. Cannot determine previous phase.`,
245
- },
246
- ],
247
- details: {},
248
- };
249
- }
250
- const prevPhase: Phase = label;
251
- deps.tddLog(tddDir, "INFO", "previous_tdd_phase: reverting", {
252
- from: state.current,
253
- to: prevPhase,
254
- headMsg,
255
- });
256
-
257
- // 1. Nuke any uncommitted changes, WT matches HEAD
258
- deps.resetHard(root);
259
- deps.tddLog(tddDir, "DEBUG", "previous_tdd_phase: resetHard done");
260
-
261
- // 2. Pop last snapshot commit, keep its content as unstaged
262
- deps.undoLastCommit(root);
263
- deps.tddLog(tddDir, "DEBUG", "previous_tdd_phase: undoLastCommit done");
264
-
265
- // 3. Update phase label from the snapshot's own label
266
- state.current = prevPhase;
267
- deps.savePhaseState(root, state);
268
- deps.tddLog(tddDir, "INFO", "previous_tdd_phase: complete", {
269
- to: prevPhase,
270
- });
271
-
272
- return {
273
- content: [
274
- {
275
- type: "text",
276
- text: `Reverted to ${prevPhase.toUpperCase()}. Working tree has the previous snapshot content as unstaged changes.`,
277
- },
278
- ],
279
- details: {},
280
- };
189
+ ctx: ExtensionContext,
190
+ deps: PreviousPhaseDeps = defaultPreviousPhaseDeps,
191
+ ): Promise<{
192
+ content: Array<{ type: string; text: string }>;
193
+ details?: Record<string, unknown>;
194
+ }> {
195
+ const root = ctx.cwd;
196
+ const tddDir = join(root, ".pi", "tdd");
197
+ const tdd = deps.loadTddState(root);
198
+ if (!tdd.ok) {
199
+ deps.tddLog(tddDir, "WARN", "previous_tdd_phase: TDD not active", {
200
+ reason: tdd.reason,
201
+ });
202
+ throw new Error(`TDD: ${tdd.reason}`);
203
+ }
204
+ if (!tdd.state.enabled) {
205
+ deps.tddLog(tddDir, "WARN", "previous_tdd_phase: TDD disabled");
206
+ throw new Error("TDD is not enabled. Run /tdd:on to enable it.");
207
+ }
208
+
209
+ const { state } = tdd;
210
+
211
+ if (!deps.hasParent(root)) {
212
+ deps.tddLog(tddDir, "WARN", "previous_tdd_phase: no parent commit", {
213
+ phase: state.current,
214
+ });
215
+ throw new Error("No previous phase to revert to.");
216
+ }
217
+
218
+ // Read phase from HEAD snapshot commit message (source of truth).
219
+ // Snapshot is labeled with the phase the work was done in, so we use
220
+ // it directly — no hardcoded phase map needed.
221
+ const headMsg = deps.headMessage(root);
222
+ const phaseMatch = headMsg.match(/^tdd: (red|green|refactor)/);
223
+ if (!phaseMatch) {
224
+ deps.tddLog(tddDir, "ERROR", "previous_tdd_phase: invalid HEAD message", {
225
+ headMsg,
226
+ });
227
+ throw new Error(
228
+ `HEAD commit "${headMsg}" is not a TDD snapshot. Cannot determine previous phase.\n` +
229
+ `The private git repo at .pi/tdd must not be manually modified. ` +
230
+ `Tampering with it will cause TDD state corruption.`,
231
+ );
232
+ }
233
+ const label = phaseMatch[1];
234
+ if (label !== "red" && label !== "green" && label !== "refactor") {
235
+ deps.tddLog(tddDir, "ERROR", "previous_tdd_phase: invalid phase label", {
236
+ headMsg,
237
+ label,
238
+ });
239
+ throw new Error(
240
+ `HEAD commit "${headMsg}" has unexpected label. Cannot determine previous phase.`,
241
+ );
242
+ }
243
+ const prevPhase: Phase = label;
244
+ deps.tddLog(tddDir, "INFO", "previous_tdd_phase: reverting", {
245
+ from: state.current,
246
+ to: prevPhase,
247
+ headMsg,
248
+ });
249
+
250
+ // 1. Nuke any uncommitted changes, WT matches HEAD
251
+ deps.resetHard(root);
252
+ deps.tddLog(tddDir, "DEBUG", "previous_tdd_phase: resetHard done");
253
+
254
+ // 2. Pop last snapshot commit, keep its content as unstaged
255
+ deps.undoLastCommit(root);
256
+ deps.tddLog(tddDir, "DEBUG", "previous_tdd_phase: undoLastCommit done");
257
+
258
+ // 3. Update phase label from the snapshot's own label
259
+ state.current = prevPhase;
260
+ deps.savePhaseState(root, state);
261
+ deps.tddLog(tddDir, "INFO", "previous_tdd_phase: complete", {
262
+ to: prevPhase,
263
+ });
264
+
265
+ return {
266
+ content: [
267
+ {
268
+ type: "text",
269
+ text: `Reverted to ${prevPhase.toUpperCase()}. Working tree has the previous snapshot content as unstaged changes.`,
270
+ },
271
+ ],
272
+ details: {},
273
+ };
281
274
  }
282
275
 
283
276
  // ── executeTddStatus ────────────────────────────────────────────────────────
284
277
 
285
278
  export async function executeTddStatus(
286
- ctx: ExtensionContext,
287
- deps: TddStatusDeps = defaultTddStatusDeps,
288
- ): Promise<{ content: Array<{ type: string; text: string }>; details?: Record<string, unknown> }> {
289
- const root = ctx.cwd;
290
- const tddDir = join(root, ".pi", "tdd");
291
- const result = deps.loadTddState(root);
292
-
293
- if (!result.ok) {
294
- deps.tddLog(tddDir, "WARN", "tdd_status: TDD not active", {
295
- reason: result.reason,
296
- });
297
- return { content: [{ type: "text", text: `TDD: ${result.reason}` }], details: {} };
298
- }
299
- if (!result.state.enabled) {
300
- deps.tddLog(tddDir, "WARN", "tdd_status: TDD disabled");
301
- return { content: [{ type: "text", text: "TDD is not enabled. Run /tdd:on to enable it." }], details: {} };
302
- }
303
-
304
- const { state, config } = result;
305
- const phaseStr = state.current.toUpperCase();
306
- const redBlk = config.blockedInRed.join(", ") || "(none)";
307
- const greenBlk = config.blockedInGreen.join(", ") || "(none)";
308
- const commands = config.testCommands.join(", ") || "(none)";
309
-
310
- deps.tddLog(tddDir, "INFO", "tdd_status: queried", {
311
- phase: state.current,
312
- });
313
-
314
- return {
315
- content: [
316
- {
317
- type: "text",
318
- text:
319
- `TDD enforcer enabled\n` +
320
- `Current phase: ${phaseStr}\n` +
321
- `Blocked in RED: ${redBlk}\n` +
322
- `Blocked in GREEN: ${greenBlk}\n` +
323
- `Test commands: ${commands}`,
324
- },
325
- ],
326
- details: {
327
- enabled: true,
328
- phase: state.current,
329
- blockedInRed: config.blockedInRed,
330
- blockedInGreen: config.blockedInGreen,
331
- testCommands: config.testCommands,
332
- },
333
- };
279
+ ctx: ExtensionContext,
280
+ deps: TddStatusDeps = defaultTddStatusDeps,
281
+ ): Promise<{
282
+ content: Array<{ type: string; text: string }>;
283
+ details?: Record<string, unknown>;
284
+ }> {
285
+ const root = ctx.cwd;
286
+ const tddDir = join(root, ".pi", "tdd");
287
+ const result = deps.loadTddState(root);
288
+
289
+ if (!result.ok) {
290
+ deps.tddLog(tddDir, "WARN", "tdd_status: TDD not active", {
291
+ reason: result.reason,
292
+ });
293
+ throw new Error(`TDD: ${result.reason}`);
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
+ const { state, config } = result;
301
+ const phaseStr = state.current.toUpperCase();
302
+ const redBlk = config.blockedInRed.join(", ") || "(none)";
303
+ const greenBlk = config.blockedInGreen.join(", ") || "(none)";
304
+ const commands = config.testCommands.join(", ") || "(none)";
305
+
306
+ deps.tddLog(tddDir, "INFO", "tdd_status: queried", {
307
+ phase: state.current,
308
+ });
309
+
310
+ return {
311
+ content: [
312
+ {
313
+ type: "text",
314
+ text:
315
+ `TDD enforcer enabled\n` +
316
+ `Current phase: ${phaseStr}\n` +
317
+ `Blocked in RED: ${redBlk}\n` +
318
+ `Blocked in GREEN: ${greenBlk}\n` +
319
+ `Test commands: ${commands}`,
320
+ },
321
+ ],
322
+ details: {
323
+ enabled: true,
324
+ phase: state.current,
325
+ blockedInRed: config.blockedInRed,
326
+ blockedInGreen: config.blockedInGreen,
327
+ testCommands: config.testCommands,
328
+ },
329
+ };
334
330
  }
335
331
 
336
332
  // ── registerTools ───────────────────────────────────────────────────────────
337
333
 
338
334
  export function registerTools(pi: ExtensionAPI): void {
339
- pi.registerTool({
340
- name: "next_tdd_phase",
341
- label: "Next TDD Phase",
342
- description:
343
- "Advance to the next TDD phase. Runs transition gates (test pass/fail checks) " +
344
- "and allowlist validation (no forbidden files modified).",
345
- parameters: Type.Object({}),
346
- execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
347
- return executeNextPhase(ctx, defaultNextPhaseDeps);
348
- },
349
- });
350
-
351
- pi.registerTool({
352
- name: "previous_tdd_phase",
353
- label: "Previous TDD Phase",
354
- description:
355
- "WARNING: Discards ALL changes made in the current phase and reverts the working tree " +
356
- "to what it was when the last phase ended. Use when the previous phase's work was wrong " +
357
- "and this phase cannot proceed.",
358
- parameters: Type.Object({}),
359
- execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
360
- return executePreviousPhase(ctx, defaultPreviousPhaseDeps);
361
- },
362
- });
363
-
364
- pi.registerTool({
365
- name: "tdd_status",
366
- label: "TDD Status",
367
- description:
368
- "Show the current TDD enforcement status: enabled/disabled, current phase, " +
369
- "blocked file globs per phase, and test commands.",
370
- parameters: Type.Object({}),
371
- execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
372
- return executeTddStatus(ctx, defaultTddStatusDeps);
373
- },
374
- });
335
+ pi.registerTool({
336
+ name: "next_tdd_phase",
337
+ label: "Next TDD Phase",
338
+ description:
339
+ "Advance to the next TDD phase. Runs transition gates (test pass/fail checks) " +
340
+ "and allowlist validation (no forbidden files modified).",
341
+ parameters: Type.Object({}),
342
+ execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
343
+ return executeNextPhase(ctx, defaultNextPhaseDeps);
344
+ },
345
+ });
346
+
347
+ pi.registerTool({
348
+ name: "previous_tdd_phase",
349
+ label: "Previous TDD Phase",
350
+ description:
351
+ "WARNING: Discards ALL changes made in the current phase and reverts the working tree " +
352
+ "to what it was when the last phase ended. Use when the previous phase's work was wrong " +
353
+ "and this phase cannot proceed.",
354
+ parameters: Type.Object({}),
355
+ execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
356
+ return executePreviousPhase(ctx, defaultPreviousPhaseDeps);
357
+ },
358
+ });
359
+
360
+ pi.registerTool({
361
+ name: "tdd_status",
362
+ label: "TDD Status",
363
+ description:
364
+ "Show the current TDD enforcement status: enabled/disabled, current phase, " +
365
+ "blocked file globs per phase, and test commands.",
366
+ parameters: Type.Object({}),
367
+ execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
368
+ return executeTddStatus(ctx, defaultTddStatusDeps);
369
+ },
370
+ });
375
371
  }