tdd-enforcer 0.2.6 → 0.2.7

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,418 @@
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
+ return {
106
+ content: [{ type: "text", text: `TDD: ${tdd.reason}` }],
107
+ details: {},
108
+ };
109
+ }
110
+ if (!tdd.state.enabled) {
111
+ deps.tddLog(tddDir, "WARN", "next_tdd_phase: TDD disabled");
112
+ return {
113
+ content: [
114
+ { type: "text", text: "TDD is not enabled. Run /tdd:on to enable it." },
115
+ ],
116
+ details: {},
117
+ };
118
+ }
119
+
120
+ const { state, config } = tdd;
121
+ const from = state.current;
122
+ const to = deps.nextPhase(from) as Phase;
123
+
124
+ deps.tddLog(tddDir, "INFO", "next_tdd_phase: starting", { from, to });
125
+
126
+ // 1. Allowlist check
127
+ const violations = deps.getDisallowedChanges(root, from, config);
128
+ if (violations.length > 0) {
129
+ deps.tddLog(tddDir, "WARN", "next_tdd_phase: blocked by allowlist", {
130
+ from,
131
+ violations,
132
+ });
133
+ return {
134
+ content: [
135
+ {
136
+ type: "text",
137
+ text:
138
+ `BLOCKED: files not allowed in ${from.toUpperCase()} phase:\n` +
139
+ violations.map((f) => ` - ${f}`).join("\n") +
140
+ `\nRevert or remove them before proceeding.\n\nInspect with: cd .pi/tdd && git diff HEAD -- ${violations[0]}`,
141
+ },
142
+ ],
143
+ details: {},
144
+ };
145
+ }
146
+
147
+ // 2. Gate check
148
+ const testRunner: TestRunner = async (commands, timeout) => {
149
+ const results = await Promise.all(
150
+ commands.map(async (cmd) => {
151
+ try {
152
+ await deps.asyncExec(cmd, { cwd: root, timeout: timeout * 1000 });
153
+ return { command: cmd, passed: true };
154
+ } catch {
155
+ return { command: cmd, passed: false };
156
+ }
157
+ }),
158
+ );
159
+
160
+ const failed = results.filter((r) => !r.passed);
161
+ if (failed.length > 0) {
162
+ return {
163
+ passed: false,
164
+ message: `Tests failed:\n${failed.map((f) => ` - ${f.command}`).join("\n")}`,
165
+ };
166
+ }
167
+ return { passed: true, message: "All tests passed." };
168
+ };
169
+
170
+ const gate = await deps.checkGate(from, to, testRunner, config);
171
+ deps.tddLog(tddDir, "DEBUG", "next_tdd_phase: gate result", {
172
+ from,
173
+ to,
174
+ passed: gate.passed,
175
+ message: gate.message,
176
+ });
177
+
178
+ if (!gate.passed) {
179
+ return { content: [{ type: "text", text: gate.message }], details: {} };
180
+ }
181
+
182
+ // 3. Snapshot — label with the phase the work was done in
183
+ const hash = deps.snapshot(root, from);
184
+ deps.tddLog(tddDir, "INFO", "next_tdd_phase: snapshot created", {
185
+ from,
186
+ to,
187
+ hash,
188
+ });
189
+
190
+ // 4. Save state
191
+ state.current = to;
192
+ deps.savePhaseState(root, state);
193
+ deps.tddLog(tddDir, "INFO", "next_tdd_phase: complete", { from, to });
194
+
195
+ return {
196
+ content: [{ type: "text", text: deps.getNudgePrompt(to, config) }],
197
+ details: {},
198
+ };
179
199
  }
180
200
 
181
201
  // ── executePreviousPhase ────────────────────────────────────────────────────
182
202
 
183
203
  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
- };
204
+ ctx: ExtensionContext,
205
+ deps: PreviousPhaseDeps = defaultPreviousPhaseDeps,
206
+ ): Promise<{
207
+ content: Array<{ type: string; text: string }>;
208
+ details?: Record<string, unknown>;
209
+ }> {
210
+ const root = ctx.cwd;
211
+ const tddDir = join(root, ".pi", "tdd");
212
+ const tdd = deps.loadTddState(root);
213
+ if (!tdd.ok) {
214
+ deps.tddLog(tddDir, "WARN", "previous_tdd_phase: TDD not active", {
215
+ reason: tdd.reason,
216
+ });
217
+ return {
218
+ content: [{ type: "text", text: `TDD: ${tdd.reason}` }],
219
+ details: {},
220
+ };
221
+ }
222
+ if (!tdd.state.enabled) {
223
+ deps.tddLog(tddDir, "WARN", "previous_tdd_phase: TDD disabled");
224
+ return {
225
+ content: [
226
+ { type: "text", text: "TDD is not enabled. Run /tdd:on to enable it." },
227
+ ],
228
+ details: {},
229
+ };
230
+ }
231
+
232
+ const { state } = tdd;
233
+
234
+ if (!deps.hasParent(root)) {
235
+ deps.tddLog(tddDir, "WARN", "previous_tdd_phase: no parent commit", {
236
+ phase: state.current,
237
+ });
238
+ return {
239
+ content: [{ type: "text", text: "No previous phase to revert to." }],
240
+ details: {},
241
+ };
242
+ }
243
+
244
+ // Read phase from HEAD snapshot commit message (source of truth).
245
+ // Snapshot is labeled with the phase the work was done in, so we use
246
+ // it directly no hardcoded phase map needed.
247
+ const headMsg = deps.headMessage(root);
248
+ const phaseMatch = headMsg.match(/^tdd: (red|green|refactor)/);
249
+ if (!phaseMatch) {
250
+ deps.tddLog(tddDir, "ERROR", "previous_tdd_phase: invalid HEAD message", {
251
+ headMsg,
252
+ });
253
+ return {
254
+ content: [
255
+ {
256
+ type: "text",
257
+ text:
258
+ `HEAD commit "${headMsg}" is not a TDD snapshot. Cannot determine previous phase.\n` +
259
+ `The private git repo at .pi/tdd must not be manually modified. ` +
260
+ `Tampering with it will cause TDD state corruption.`,
261
+ },
262
+ ],
263
+ details: {},
264
+ };
265
+ }
266
+ const label = phaseMatch[1];
267
+ if (label !== "red" && label !== "green" && label !== "refactor") {
268
+ deps.tddLog(tddDir, "ERROR", "previous_tdd_phase: invalid phase label", {
269
+ headMsg,
270
+ label,
271
+ });
272
+ return {
273
+ content: [
274
+ {
275
+ type: "text",
276
+ text: `HEAD commit "${headMsg}" has unexpected label. Cannot determine previous phase.`,
277
+ },
278
+ ],
279
+ details: {},
280
+ };
281
+ }
282
+ const prevPhase: Phase = label;
283
+ deps.tddLog(tddDir, "INFO", "previous_tdd_phase: reverting", {
284
+ from: state.current,
285
+ to: prevPhase,
286
+ headMsg,
287
+ });
288
+
289
+ // 1. Nuke any uncommitted changes, WT matches HEAD
290
+ deps.resetHard(root);
291
+ deps.tddLog(tddDir, "DEBUG", "previous_tdd_phase: resetHard done");
292
+
293
+ // 2. Pop last snapshot commit, keep its content as unstaged
294
+ deps.undoLastCommit(root);
295
+ deps.tddLog(tddDir, "DEBUG", "previous_tdd_phase: undoLastCommit done");
296
+
297
+ // 3. Update phase label from the snapshot's own label
298
+ state.current = prevPhase;
299
+ deps.savePhaseState(root, state);
300
+ deps.tddLog(tddDir, "INFO", "previous_tdd_phase: complete", {
301
+ to: prevPhase,
302
+ });
303
+
304
+ return {
305
+ content: [
306
+ {
307
+ type: "text",
308
+ text: `Reverted to ${prevPhase.toUpperCase()}. Working tree has the previous snapshot content as unstaged changes.`,
309
+ },
310
+ ],
311
+ details: {},
312
+ };
281
313
  }
282
314
 
283
315
  // ── executeTddStatus ────────────────────────────────────────────────────────
284
316
 
285
317
  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
- };
318
+ ctx: ExtensionContext,
319
+ deps: TddStatusDeps = defaultTddStatusDeps,
320
+ ): Promise<{
321
+ content: Array<{ type: string; text: string }>;
322
+ details?: Record<string, unknown>;
323
+ }> {
324
+ const root = ctx.cwd;
325
+ const tddDir = join(root, ".pi", "tdd");
326
+ const result = deps.loadTddState(root);
327
+
328
+ if (!result.ok) {
329
+ deps.tddLog(tddDir, "WARN", "tdd_status: TDD not active", {
330
+ reason: result.reason,
331
+ });
332
+ return {
333
+ content: [{ type: "text", text: `TDD: ${result.reason}` }],
334
+ details: {},
335
+ };
336
+ }
337
+ if (!result.state.enabled) {
338
+ deps.tddLog(tddDir, "WARN", "tdd_status: TDD disabled");
339
+ return {
340
+ content: [
341
+ { type: "text", text: "TDD is not enabled. Run /tdd:on to enable it." },
342
+ ],
343
+ details: {},
344
+ };
345
+ }
346
+
347
+ const { state, config } = result;
348
+ const phaseStr = state.current.toUpperCase();
349
+ const redBlk = config.blockedInRed.join(", ") || "(none)";
350
+ const greenBlk = config.blockedInGreen.join(", ") || "(none)";
351
+ const commands = config.testCommands.join(", ") || "(none)";
352
+
353
+ deps.tddLog(tddDir, "INFO", "tdd_status: queried", {
354
+ phase: state.current,
355
+ });
356
+
357
+ return {
358
+ content: [
359
+ {
360
+ type: "text",
361
+ text:
362
+ `TDD enforcer enabled\n` +
363
+ `Current phase: ${phaseStr}\n` +
364
+ `Blocked in RED: ${redBlk}\n` +
365
+ `Blocked in GREEN: ${greenBlk}\n` +
366
+ `Test commands: ${commands}`,
367
+ },
368
+ ],
369
+ details: {
370
+ enabled: true,
371
+ phase: state.current,
372
+ blockedInRed: config.blockedInRed,
373
+ blockedInGreen: config.blockedInGreen,
374
+ testCommands: config.testCommands,
375
+ },
376
+ };
334
377
  }
335
378
 
336
379
  // ── registerTools ───────────────────────────────────────────────────────────
337
380
 
338
381
  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
- });
382
+ pi.registerTool({
383
+ name: "next_tdd_phase",
384
+ label: "Next TDD Phase",
385
+ description:
386
+ "Advance to the next TDD phase. Runs transition gates (test pass/fail checks) " +
387
+ "and allowlist validation (no forbidden files modified).",
388
+ parameters: Type.Object({}),
389
+ execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
390
+ return executeNextPhase(ctx, defaultNextPhaseDeps);
391
+ },
392
+ });
393
+
394
+ pi.registerTool({
395
+ name: "previous_tdd_phase",
396
+ label: "Previous TDD Phase",
397
+ description:
398
+ "WARNING: Discards ALL changes made in the current phase and reverts the working tree " +
399
+ "to what it was when the last phase ended. Use when the previous phase's work was wrong " +
400
+ "and this phase cannot proceed.",
401
+ parameters: Type.Object({}),
402
+ execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
403
+ return executePreviousPhase(ctx, defaultPreviousPhaseDeps);
404
+ },
405
+ });
406
+
407
+ pi.registerTool({
408
+ name: "tdd_status",
409
+ label: "TDD Status",
410
+ description:
411
+ "Show the current TDD enforcement status: enabled/disabled, current phase, " +
412
+ "blocked file globs per phase, and test commands.",
413
+ parameters: Type.Object({}),
414
+ execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
415
+ return executeTddStatus(ctx, defaultTddStatusDeps);
416
+ },
417
+ });
375
418
  }