tdd-enforcer 0.2.9 → 0.3.0

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.
@@ -430,10 +430,8 @@ describe("handleBeforeAgentStart", () => {
430
430
  };
431
431
  }
432
432
 
433
- function makeEvent(): {
434
- systemPromptOptions: { promptGuidelines: string[] };
435
- } {
436
- return { systemPromptOptions: { promptGuidelines: [] } };
433
+ function makeEvent(): { systemPrompt: string } {
434
+ return { systemPrompt: "" };
437
435
  }
438
436
 
439
437
  beforeEach(() => {
@@ -441,7 +439,7 @@ describe("handleBeforeAgentStart", () => {
441
439
  mockLoadTddState = vi.fn();
442
440
  });
443
441
 
444
- it("pushes guidelines when TDD enabled", async () => {
442
+ it("appends TDD instructions when TDD enabled", async () => {
445
443
  mockLoadTddState.mockReturnValue({
446
444
  ok: true,
447
445
  state: { enabled: true, current: "red" },
@@ -458,19 +456,13 @@ describe("handleBeforeAgentStart", () => {
458
456
  { cwd: "/test" } as any,
459
457
  makeDeps(),
460
458
  );
461
- expect(event.systemPromptOptions.promptGuidelines).toHaveLength(3);
462
- expect(event.systemPromptOptions.promptGuidelines[0]).toContain(
463
- "locked files will be blocked",
464
- );
465
- expect(event.systemPromptOptions.promptGuidelines[1]).toContain(
466
- "next_tdd_phase",
467
- );
468
- expect(event.systemPromptOptions.promptGuidelines[2]).toContain(
469
- "cycle so reverting is cheap",
470
- );
459
+ expect(event.systemPrompt).toContain("TDD enforcement");
460
+ expect(event.systemPrompt).toContain("locked files will be blocked");
461
+ expect(event.systemPrompt).toContain("cycle so reverting is cheap");
462
+ expect(event.systemPrompt).not.toContain("next_tdd_phase");
471
463
  });
472
464
 
473
- it("does not push guidelines when TDD not setup", async () => {
465
+ it("does not modify systemPrompt when TDD not setup", async () => {
474
466
  mockLoadTddState.mockReturnValue({
475
467
  ok: false,
476
468
  reason: "Missing .pi/tdd/",
@@ -481,10 +473,10 @@ describe("handleBeforeAgentStart", () => {
481
473
  { cwd: "/test" } as any,
482
474
  makeDeps(),
483
475
  );
484
- expect(event.systemPromptOptions.promptGuidelines).toHaveLength(0);
476
+ expect(event.systemPrompt).toBe("");
485
477
  });
486
478
 
487
- it("does not push guidelines when TDD disabled", async () => {
479
+ it("appends disabled message when TDD was disabled", async () => {
488
480
  mockLoadTddState.mockReturnValue({
489
481
  ok: true,
490
482
  state: { enabled: false, current: "red" },
@@ -501,6 +493,6 @@ describe("handleBeforeAgentStart", () => {
501
493
  { cwd: "/test" } as any,
502
494
  makeDeps(),
503
495
  );
504
- expect(event.systemPromptOptions.promptGuidelines).toHaveLength(0);
496
+ expect(event.systemPrompt).toContain("was disabled");
505
497
  });
506
498
  });
@@ -1,7 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import type {
3
- ExtensionAPI,
4
- ExtensionContext,
3
+ ExtensionAPI,
4
+ ExtensionContext,
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
  import { resetGit, savePhaseState, snapshot } from "../../engine/index.js";
7
7
  import { loadTddState } from "./helpers.js";
@@ -10,320 +10,321 @@ import { tddLog } from "./log.js";
10
10
  import { registerTools } from "./tools.js";
11
11
 
12
12
  export async function handleTddOn(
13
- ctx: ExtensionContext,
14
- deps: {
15
- loadTddState: typeof loadTddState;
16
- snapshot: typeof snapshot;
17
- savePhaseState: typeof savePhaseState;
18
- tddLog: typeof tddLog;
19
- } = {
20
- loadTddState,
21
- snapshot,
22
- savePhaseState,
23
- tddLog,
24
- },
13
+ ctx: ExtensionContext,
14
+ deps: {
15
+ loadTddState: typeof loadTddState;
16
+ snapshot: typeof snapshot;
17
+ savePhaseState: typeof savePhaseState;
18
+ tddLog: typeof tddLog;
19
+ } = {
20
+ loadTddState,
21
+ snapshot,
22
+ savePhaseState,
23
+ tddLog,
24
+ },
25
25
  ): Promise<void> {
26
- const root = ctx.cwd;
27
- const tddDir = join(root, ".pi", "tdd");
28
-
29
- deps.tddLog(tddDir, "INFO", "tdd:on: starting");
30
-
31
- const setup = deps.loadTddState(root);
32
- if (!setup.ok) {
33
- deps.tddLog(tddDir, "WARN", "tdd:on: setup invalid", {
34
- reason: setup.reason,
35
- });
36
- ctx.ui.notify(setup.reason, "error");
37
- return;
38
- }
39
-
40
- const { state } = setup;
41
-
42
- if (state.enabled) {
43
- deps.tddLog(tddDir, "INFO", "tdd:on: already enabled", {
44
- phase: state.current,
45
- });
46
- ctx.ui.notify(
47
- `TDD already enabled — ${state.current.toUpperCase()} phase`,
48
- "info",
49
- );
50
- return;
51
- }
52
-
53
- // Snapshot working tree so stale baseline doesn't nuke user changes
54
- deps.snapshot(root, state.current);
55
- deps.tddLog(tddDir, "INFO", "tdd:on: snapshot taken", {
56
- phase: state.current,
57
- });
58
-
59
- state.enabled = true;
60
- deps.savePhaseState(root, state);
61
- deps.tddLog(tddDir, "INFO", "tdd:on: enabled", {
62
- phase: state.current,
63
- });
64
- ctx.ui.notify(`TDD enabled — ${state.current.toUpperCase()} phase`, "info");
26
+ const root = ctx.cwd;
27
+ const tddDir = join(root, ".pi", "tdd");
28
+
29
+ deps.tddLog(tddDir, "INFO", "tdd:on: starting");
30
+
31
+ const setup = deps.loadTddState(root);
32
+ if (!setup.ok) {
33
+ deps.tddLog(tddDir, "WARN", "tdd:on: setup invalid", {
34
+ reason: setup.reason,
35
+ });
36
+ ctx.ui.notify(setup.reason, "error");
37
+ return;
38
+ }
39
+
40
+ const { state } = setup;
41
+
42
+ if (state.enabled) {
43
+ deps.tddLog(tddDir, "INFO", "tdd:on: already enabled", {
44
+ phase: state.current,
45
+ });
46
+ ctx.ui.notify(
47
+ `TDD already enabled — ${state.current.toUpperCase()} phase`,
48
+ "info",
49
+ );
50
+ return;
51
+ }
52
+
53
+ // Snapshot working tree so stale baseline doesn't nuke user changes
54
+ deps.snapshot(root, state.current);
55
+ deps.tddLog(tddDir, "INFO", "tdd:on: snapshot taken", {
56
+ phase: state.current,
57
+ });
58
+
59
+ state.enabled = true;
60
+ deps.savePhaseState(root, state);
61
+ deps.tddLog(tddDir, "INFO", "tdd:on: enabled", {
62
+ phase: state.current,
63
+ });
64
+ ctx.ui.notify(`TDD enabled — ${state.current.toUpperCase()} phase`, "info");
65
65
  }
66
66
 
67
67
  export async function handleTddOff(
68
- ctx: ExtensionContext,
69
- deps: {
70
- loadTddState: typeof loadTddState;
71
- savePhaseState: typeof savePhaseState;
72
- tddLog: typeof tddLog;
73
- } = {
74
- loadTddState,
75
- savePhaseState,
76
- tddLog,
77
- },
68
+ ctx: ExtensionContext,
69
+ deps: {
70
+ loadTddState: typeof loadTddState;
71
+ savePhaseState: typeof savePhaseState;
72
+ tddLog: typeof tddLog;
73
+ } = {
74
+ loadTddState,
75
+ savePhaseState,
76
+ tddLog,
77
+ },
78
78
  ): Promise<void> {
79
- const root = ctx.cwd;
80
- const tddDir = join(root, ".pi", "tdd");
81
-
82
- const setup = deps.loadTddState(root);
83
- if (!setup.ok) {
84
- deps.tddLog(tddDir, "WARN", "tdd:off: setup invalid", {
85
- reason: setup.reason,
86
- });
87
- ctx.ui.notify(setup.reason, "error");
88
- return;
89
- }
90
-
91
- const { state } = setup;
92
-
93
- if (!state.enabled) {
94
- deps.tddLog(tddDir, "INFO", "tdd:off: already disabled");
95
- ctx.ui.notify("TDD already disabled", "info");
96
- return;
97
- }
98
-
99
- state.enabled = false;
100
- deps.savePhaseState(root, state);
101
- deps.tddLog(tddDir, "INFO", "tdd:off: disabled", {
102
- was: state.current,
103
- });
104
- ctx.ui.notify("TDD disabled", "info");
79
+ const root = ctx.cwd;
80
+ const tddDir = join(root, ".pi", "tdd");
81
+
82
+ const setup = deps.loadTddState(root);
83
+ if (!setup.ok) {
84
+ deps.tddLog(tddDir, "WARN", "tdd:off: setup invalid", {
85
+ reason: setup.reason,
86
+ });
87
+ ctx.ui.notify(setup.reason, "error");
88
+ return;
89
+ }
90
+
91
+ const { state } = setup;
92
+
93
+ if (!state.enabled) {
94
+ deps.tddLog(tddDir, "INFO", "tdd:off: already disabled");
95
+ ctx.ui.notify("TDD already disabled", "info");
96
+ return;
97
+ }
98
+
99
+ state.enabled = false;
100
+ deps.savePhaseState(root, state);
101
+ deps.tddLog(tddDir, "INFO", "tdd:off: disabled", {
102
+ was: state.current,
103
+ });
104
+ ctx.ui.notify("TDD disabled", "info");
105
105
  }
106
106
 
107
107
  export async function handleTddStatus(
108
- ctx: ExtensionContext,
109
- deps: {
110
- loadTddState: typeof loadTddState;
111
- tddLog: typeof tddLog;
112
- } = {
113
- loadTddState,
114
- tddLog,
115
- },
108
+ ctx: ExtensionContext,
109
+ deps: {
110
+ loadTddState: typeof loadTddState;
111
+ tddLog: typeof tddLog;
112
+ } = {
113
+ loadTddState,
114
+ tddLog,
115
+ },
116
116
  ): Promise<void> {
117
- const root = ctx.cwd;
118
- const tddDir = join(root, ".pi", "tdd");
119
- const result = deps.loadTddState(root);
120
-
121
- if (!result.ok) {
122
- deps.tddLog(tddDir, "WARN", "tdd:status: setup invalid", {
123
- reason: result.reason,
124
- });
125
- ctx.ui.notify(`TDD: ${result.reason}`, "error");
126
- return;
127
- }
128
-
129
- const { state, config } = result;
130
- const enabledStr = state.enabled ? "enabled" : "disabled";
131
- const phaseStr = state.current.toUpperCase();
132
- const redBlk = config.blockedInRed.join(", ") || "(none)";
133
- const greenBlk = config.blockedInGreen.join(", ") || "(none)";
134
- const commands = config.testCommands.join(", ") || "(none)";
135
-
136
- deps.tddLog(tddDir, "INFO", "tdd:status: queried", {
137
- enabled: state.enabled,
138
- phase: state.current,
139
- });
140
-
141
- ctx.ui.notify(
142
- `TDD enforcer ${enabledStr}\n` +
143
- `Current phase: ${phaseStr}\n` +
144
- `Blocked in RED: ${redBlk}\n` +
145
- `Blocked in GREEN: ${greenBlk}\n` +
146
- `Test commands: ${commands}`,
147
- "info",
148
- );
117
+ const root = ctx.cwd;
118
+ const tddDir = join(root, ".pi", "tdd");
119
+ const result = deps.loadTddState(root);
120
+
121
+ if (!result.ok) {
122
+ deps.tddLog(tddDir, "WARN", "tdd:status: setup invalid", {
123
+ reason: result.reason,
124
+ });
125
+ ctx.ui.notify(`TDD: ${result.reason}`, "error");
126
+ return;
127
+ }
128
+
129
+ const { state, config } = result;
130
+ const enabledStr = state.enabled ? "enabled" : "disabled";
131
+ const phaseStr = state.current.toUpperCase();
132
+ const redBlk = config.blockedInRed.join(", ") || "(none)";
133
+ const greenBlk = config.blockedInGreen.join(", ") || "(none)";
134
+ const commands = config.testCommands.join(", ") || "(none)";
135
+
136
+ deps.tddLog(tddDir, "INFO", "tdd:status: queried", {
137
+ enabled: state.enabled,
138
+ phase: state.current,
139
+ });
140
+
141
+ ctx.ui.notify(
142
+ `TDD enforcer ${enabledStr}\n` +
143
+ `Current phase: ${phaseStr}\n` +
144
+ `Blocked in RED: ${redBlk}\n` +
145
+ `Blocked in GREEN: ${greenBlk}\n` +
146
+ `Test commands: ${commands}`,
147
+ "info",
148
+ );
149
149
  }
150
150
 
151
151
  export async function handleBeforeAgentStart(
152
- event: {
153
- systemPromptOptions: { promptGuidelines: string[] };
154
- },
155
- ctx: { cwd: string },
156
- deps: {
157
- loadTddState: typeof loadTddState;
158
- },
152
+ event: { systemPrompt: string },
153
+ ctx: { cwd: string },
154
+ deps: {
155
+ loadTddState: typeof loadTddState;
156
+ },
159
157
  ): Promise<void> {
160
- const tdd = deps.loadTddState(ctx.cwd);
161
- if (!tdd.ok || !tdd.state.enabled) return;
162
-
163
- event.systemPromptOptions.promptGuidelines.push(
164
- "You are working under TDD enforcement. Each phase restricts which files you can modify — locked files will be blocked automatically.",
165
- "Use `next_tdd_phase` to advance through the cycle, `previous_tdd_phase` to revert a phase, `tdd_status` to check current phase and blocked file rules.",
166
- "Minimise the scope of each TDD cycle so reverting is cheap.",
167
- );
158
+ const tdd = deps.loadTddState(ctx.cwd);
159
+ if (!tdd.ok) return;
160
+
161
+ if (tdd.state.enabled) {
162
+ event.systemPrompt +=
163
+ "\n\nYou are working under TDD enforcement. Each phase restricts which files you can modify locked files will be blocked automatically.\n" +
164
+ "Minimise the scope of each TDD cycle so reverting is cheap.";
165
+ } else {
166
+ event.systemPrompt +=
167
+ "\n\nTDD enforcement was disabled. File restrictions are no longer enforced.";
168
+ }
168
169
  }
169
170
 
170
171
  export async function handleTddJump(
171
- phase: "red" | "green" | "refactor",
172
- ctx: ExtensionContext,
173
- deps: {
174
- loadTddState: typeof loadTddState;
175
- snapshot: typeof snapshot;
176
- savePhaseState: typeof savePhaseState;
177
- tddLog: typeof tddLog;
178
- } = {
179
- loadTddState,
180
- snapshot,
181
- savePhaseState,
182
- tddLog,
183
- },
172
+ phase: "red" | "green" | "refactor",
173
+ ctx: ExtensionContext,
174
+ deps: {
175
+ loadTddState: typeof loadTddState;
176
+ snapshot: typeof snapshot;
177
+ savePhaseState: typeof savePhaseState;
178
+ tddLog: typeof tddLog;
179
+ } = {
180
+ loadTddState,
181
+ snapshot,
182
+ savePhaseState,
183
+ tddLog,
184
+ },
184
185
  ): Promise<void> {
185
- const root = ctx.cwd;
186
- const tddDir = join(root, ".pi", "tdd");
187
-
188
- const setup = deps.loadTddState(root);
189
- if (!setup.ok) {
190
- deps.tddLog(tddDir, "WARN", `tdd:${phase}: setup invalid`, {
191
- reason: setup.reason,
192
- });
193
- ctx.ui.notify(setup.reason, "error");
194
- return;
195
- }
196
-
197
- const { state } = setup;
198
-
199
- if (state.current === phase) {
200
- deps.tddLog(tddDir, "INFO", `tdd:${phase}: already in ${phase}`, {
201
- phase,
202
- });
203
- ctx.ui.notify(`TDD: already in ${phase.toUpperCase()} phase.`, "info");
204
- return;
205
- }
206
-
207
- // Snapshot the current phase's work before jumping
208
- deps.snapshot(root, state.current);
209
- deps.tddLog(tddDir, "INFO", `tdd:${phase}: snapshot taken`, {
210
- from: state.current,
211
- });
212
-
213
- // Auto-enable if disabled, set phase
214
- state.enabled = true;
215
- state.current = phase;
216
- deps.savePhaseState(root, state);
217
-
218
- deps.tddLog(tddDir, "INFO", `tdd:${phase}: jumped`);
219
- ctx.ui.notify(`Skipped to ${phase.toUpperCase()} phase.`, "info");
186
+ const root = ctx.cwd;
187
+ const tddDir = join(root, ".pi", "tdd");
188
+
189
+ const setup = deps.loadTddState(root);
190
+ if (!setup.ok) {
191
+ deps.tddLog(tddDir, "WARN", `tdd:${phase}: setup invalid`, {
192
+ reason: setup.reason,
193
+ });
194
+ ctx.ui.notify(setup.reason, "error");
195
+ return;
196
+ }
197
+
198
+ const { state } = setup;
199
+
200
+ if (state.current === phase) {
201
+ deps.tddLog(tddDir, "INFO", `tdd:${phase}: already in ${phase}`, {
202
+ phase,
203
+ });
204
+ ctx.ui.notify(`TDD: already in ${phase.toUpperCase()} phase.`, "info");
205
+ return;
206
+ }
207
+
208
+ // Snapshot the current phase's work before jumping
209
+ deps.snapshot(root, state.current);
210
+ deps.tddLog(tddDir, "INFO", `tdd:${phase}: snapshot taken`, {
211
+ from: state.current,
212
+ });
213
+
214
+ // Auto-enable if disabled, set phase
215
+ state.enabled = true;
216
+ state.current = phase;
217
+ deps.savePhaseState(root, state);
218
+
219
+ deps.tddLog(tddDir, "INFO", `tdd:${phase}: jumped`);
220
+ ctx.ui.notify(`Skipped to ${phase.toUpperCase()} phase.`, "info");
220
221
  }
221
222
 
222
223
  export async function handleTddReset(
223
- ctx: ExtensionContext,
224
- deps: {
225
- loadTddState: typeof loadTddState;
226
- resetGit: typeof resetGit;
227
- snapshot: typeof snapshot;
228
- savePhaseState: typeof savePhaseState;
229
- tddLog: typeof tddLog;
230
- } = {
231
- loadTddState,
232
- resetGit,
233
- snapshot,
234
- savePhaseState,
235
- tddLog,
236
- },
224
+ ctx: ExtensionContext,
225
+ deps: {
226
+ loadTddState: typeof loadTddState;
227
+ resetGit: typeof resetGit;
228
+ snapshot: typeof snapshot;
229
+ savePhaseState: typeof savePhaseState;
230
+ tddLog: typeof tddLog;
231
+ } = {
232
+ loadTddState,
233
+ resetGit,
234
+ snapshot,
235
+ savePhaseState,
236
+ tddLog,
237
+ },
237
238
  ): Promise<void> {
238
- const root = ctx.cwd;
239
- const tddDir = join(root, ".pi", "tdd");
240
-
241
- deps.tddLog(tddDir, "INFO", "tdd:reset: starting");
242
-
243
- const setup = deps.loadTddState(root);
244
- if (!setup.ok) {
245
- deps.tddLog(tddDir, "WARN", "tdd:reset: setup invalid", {
246
- reason: setup.reason,
247
- });
248
- ctx.ui.notify(setup.reason, "error");
249
- return;
250
- }
251
-
252
- // Nuke git history and re-init
253
- try {
254
- deps.resetGit(root);
255
- deps.tddLog(tddDir, "INFO", "tdd:reset: git reset and re-initialised");
256
- } catch (e) {
257
- deps.tddLog(tddDir, "ERROR", "tdd:reset: git reset failed", {
258
- error: (e as Error).message,
259
- });
260
- ctx.ui.notify("Failed to reset private git repo.", "error");
261
- return;
262
- }
263
-
264
- // Snapshot current working tree
265
- deps.snapshot(root, "red");
266
- deps.tddLog(tddDir, "INFO", "tdd:reset: snapshot taken");
267
-
268
- // Reset state to RED (disabled, user must run /tdd:on)
269
- deps.savePhaseState(root, { enabled: false, current: "red" });
270
- deps.tddLog(tddDir, "INFO", "tdd:reset: complete");
271
-
272
- ctx.ui.notify(
273
- "TDD snapshot history reset. Run /tdd:on to re-enable enforcement.",
274
- "warning",
275
- );
239
+ const root = ctx.cwd;
240
+ const tddDir = join(root, ".pi", "tdd");
241
+
242
+ deps.tddLog(tddDir, "INFO", "tdd:reset: starting");
243
+
244
+ const setup = deps.loadTddState(root);
245
+ if (!setup.ok) {
246
+ deps.tddLog(tddDir, "WARN", "tdd:reset: setup invalid", {
247
+ reason: setup.reason,
248
+ });
249
+ ctx.ui.notify(setup.reason, "error");
250
+ return;
251
+ }
252
+
253
+ // Nuke git history and re-init
254
+ try {
255
+ deps.resetGit(root);
256
+ deps.tddLog(tddDir, "INFO", "tdd:reset: git reset and re-initialised");
257
+ } catch (e) {
258
+ deps.tddLog(tddDir, "ERROR", "tdd:reset: git reset failed", {
259
+ error: (e as Error).message,
260
+ });
261
+ ctx.ui.notify("Failed to reset private git repo.", "error");
262
+ return;
263
+ }
264
+
265
+ // Snapshot current working tree
266
+ deps.snapshot(root, "red");
267
+ deps.tddLog(tddDir, "INFO", "tdd:reset: snapshot taken");
268
+
269
+ // Reset state to RED (disabled, user must run /tdd:on)
270
+ deps.savePhaseState(root, { enabled: false, current: "red" });
271
+ deps.tddLog(tddDir, "INFO", "tdd:reset: complete");
272
+
273
+ ctx.ui.notify(
274
+ "TDD snapshot history reset. Run /tdd:on to re-enable enforcement.",
275
+ "warning",
276
+ );
276
277
  }
277
278
 
278
279
  export default function (pi: ExtensionAPI) {
279
- const defaultDeps = {
280
- loadTddState,
281
- snapshot,
282
- savePhaseState,
283
- tddLog,
284
- resetGit,
285
- };
286
-
287
- pi.registerCommand("tdd:on", {
288
- description: "Enable TDD enforcement",
289
- handler: (_args: string, ctx: ExtensionContext) =>
290
- handleTddOn(ctx, defaultDeps),
291
- });
292
-
293
- pi.registerCommand("tdd:off", {
294
- description: "Disable TDD enforcement",
295
- handler: (_args: string, ctx: ExtensionContext) =>
296
- handleTddOff(ctx, defaultDeps),
297
- });
298
-
299
- pi.registerCommand("tdd:status", {
300
- description: "Show TDD enforcement status",
301
- handler: (_args: string, ctx: ExtensionContext) =>
302
- handleTddStatus(ctx, defaultDeps),
303
- });
304
-
305
- pi.registerCommand("tdd:reset", {
306
- description:
307
- "WARNING: Destroys ALL TDD snapshot history and resets to RED phase. " +
308
- "Working tree is preserved. Run /tdd:on to re-enable after reset.",
309
- handler: (_args: string, ctx: ExtensionContext) =>
310
- handleTddReset(ctx, { ...defaultDeps, resetGit }),
311
- });
312
-
313
- for (const phase of ["red", "green", "refactor"] as const) {
314
- pi.registerCommand(`tdd:${phase}`, {
315
- description:
316
- `Skip to ${phase.toUpperCase()} phase. ` +
317
- "Snapshot working tree, auto-enable TDD, set phase. No gate checks.",
318
- handler: (_args: string, ctx: ExtensionContext) =>
319
- handleTddJump(phase, ctx, defaultDeps),
320
- });
321
- }
322
-
323
- registerTools(pi);
324
- registerHooks(pi);
325
-
326
- pi.on("before_agent_start", (event, ctx) =>
327
- handleBeforeAgentStart(event, ctx, { loadTddState }),
328
- );
280
+ const defaultDeps = {
281
+ loadTddState,
282
+ snapshot,
283
+ savePhaseState,
284
+ tddLog,
285
+ resetGit,
286
+ };
287
+
288
+ pi.registerCommand("tdd:on", {
289
+ description: "Enable TDD enforcement",
290
+ handler: (_args: string, ctx: ExtensionContext) =>
291
+ handleTddOn(ctx, defaultDeps),
292
+ });
293
+
294
+ pi.registerCommand("tdd:off", {
295
+ description: "Disable TDD enforcement",
296
+ handler: (_args: string, ctx: ExtensionContext) =>
297
+ handleTddOff(ctx, defaultDeps),
298
+ });
299
+
300
+ pi.registerCommand("tdd:status", {
301
+ description: "Show TDD enforcement status",
302
+ handler: (_args: string, ctx: ExtensionContext) =>
303
+ handleTddStatus(ctx, defaultDeps),
304
+ });
305
+
306
+ pi.registerCommand("tdd:reset", {
307
+ description:
308
+ "WARNING: Destroys ALL TDD snapshot history and resets to RED phase. " +
309
+ "Working tree is preserved. Run /tdd:on to re-enable after reset.",
310
+ handler: (_args: string, ctx: ExtensionContext) =>
311
+ handleTddReset(ctx, { ...defaultDeps, resetGit }),
312
+ });
313
+
314
+ for (const phase of ["red", "green", "refactor"] as const) {
315
+ pi.registerCommand(`tdd:${phase}`, {
316
+ description:
317
+ `Skip to ${phase.toUpperCase()} phase. ` +
318
+ "Snapshot working tree, auto-enable TDD, set phase. No gate checks.",
319
+ handler: (_args: string, ctx: ExtensionContext) =>
320
+ handleTddJump(phase, ctx, defaultDeps),
321
+ });
322
+ }
323
+
324
+ registerTools(pi);
325
+ registerHooks(pi);
326
+
327
+ pi.on("before_agent_start", (event, ctx) =>
328
+ handleBeforeAgentStart(event, ctx, { loadTddState }),
329
+ );
329
330
  }
package/package.json CHANGED
@@ -1,32 +1,32 @@
1
1
  {
2
- "name": "tdd-enforcer",
3
- "version": "0.2.9",
4
- "type": "module",
5
- "scripts": {
6
- "fmt": "biome format --write .",
7
- "lint": "biome check .",
8
- "lint:fix": "biome check --write .",
9
- "test": "vitest run"
10
- },
11
- "keywords": [
12
- "pi-package"
13
- ],
14
- "dependencies": {
15
- "picomatch": "^4.0.4"
16
- },
17
- "devDependencies": {
18
- "@biomejs/biome": "^2.5.0",
19
- "@earendil-works/pi-coding-agent": "^0.79.6",
20
- "@types/node": "^25.9.3",
21
- "typebox": "^1.2.16",
22
- "vitest": "^3"
23
- },
24
- "pi": {
25
- "extensions": [
26
- "./adapters/pi/index.ts"
27
- ],
28
- "skills": [
29
- "./skills"
30
- ]
31
- }
2
+ "name": "tdd-enforcer",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "fmt": "biome format --write .",
7
+ "lint": "biome check .",
8
+ "lint:fix": "biome check --write .",
9
+ "test": "vitest run"
10
+ },
11
+ "keywords": [
12
+ "pi-package"
13
+ ],
14
+ "dependencies": {
15
+ "picomatch": "^4.0.4"
16
+ },
17
+ "devDependencies": {
18
+ "@biomejs/biome": "^2.5.0",
19
+ "@earendil-works/pi-coding-agent": "^0.79.6",
20
+ "@types/node": "^25.9.3",
21
+ "typebox": "^1.2.16",
22
+ "vitest": "^3"
23
+ },
24
+ "pi": {
25
+ "extensions": [
26
+ "./adapters/pi/index.ts"
27
+ ],
28
+ "skills": [
29
+ "./skills"
30
+ ]
31
+ }
32
32
  }