tdd-enforcer 0.2.7 → 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.
- package/adapters/pi/index.test.ts +106 -0
- package/adapters/pi/index.ts +62 -0
- package/adapters/pi/prompts.ts +2 -1
- package/adapters/pi/tools.test.ts +44 -48
- package/adapters/pi/tools.ts +21 -68
- package/package.json +30 -30
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import {
|
|
3
|
+
handleTddJump,
|
|
3
4
|
handleTddOff,
|
|
4
5
|
handleTddOn,
|
|
5
6
|
handleTddReset,
|
|
@@ -310,3 +311,108 @@ describe("handleTddReset", () => {
|
|
|
310
311
|
expect(ctx.notifications[0].type).toBe("error");
|
|
311
312
|
});
|
|
312
313
|
});
|
|
314
|
+
|
|
315
|
+
// ── handleTddJump ───────────────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
describe("handleTddJump", () => {
|
|
318
|
+
let mockLoadTddState: ReturnType<typeof vi.fn>;
|
|
319
|
+
let mockSnapshot: ReturnType<typeof vi.fn>;
|
|
320
|
+
let mockSavePhaseState: ReturnType<typeof vi.fn>;
|
|
321
|
+
let mockTddLog: ReturnType<typeof vi.fn>;
|
|
322
|
+
|
|
323
|
+
function makeDeps(overrides = {}) {
|
|
324
|
+
return {
|
|
325
|
+
loadTddState: mockLoadTddState,
|
|
326
|
+
snapshot: mockSnapshot,
|
|
327
|
+
savePhaseState: mockSavePhaseState,
|
|
328
|
+
tddLog: mockTddLog,
|
|
329
|
+
...overrides,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
beforeEach(() => {
|
|
334
|
+
vi.clearAllMocks();
|
|
335
|
+
mockLoadTddState = vi.fn();
|
|
336
|
+
mockSnapshot = vi.fn().mockReturnValue("snap123");
|
|
337
|
+
mockSavePhaseState = vi.fn();
|
|
338
|
+
mockTddLog = vi.fn();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
function tddOk(overrides?: { current?: string; enabled?: boolean }) {
|
|
342
|
+
return {
|
|
343
|
+
ok: true as const,
|
|
344
|
+
state: {
|
|
345
|
+
enabled: overrides?.enabled ?? true,
|
|
346
|
+
current: overrides?.current ?? "red",
|
|
347
|
+
},
|
|
348
|
+
config,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
it("shows error when TDD not setup", async () => {
|
|
353
|
+
mockLoadTddState.mockReturnValue({ ok: false, reason: "Missing .pi/tdd/" });
|
|
354
|
+
const ctx = makeCtx();
|
|
355
|
+
await handleTddJump("green", ctx, makeDeps());
|
|
356
|
+
expect(ctx.notifications[0].message).toContain("Missing");
|
|
357
|
+
expect(ctx.notifications[0].type).toBe("error");
|
|
358
|
+
expect(mockSavePhaseState).not.toHaveBeenCalled();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("notifies no-op when already in target phase", async () => {
|
|
362
|
+
mockLoadTddState.mockReturnValue(tddOk({ current: "green" }));
|
|
363
|
+
const ctx = makeCtx();
|
|
364
|
+
await handleTddJump("green", ctx, makeDeps());
|
|
365
|
+
expect(ctx.notifications[0].message).toContain("already in GREEN");
|
|
366
|
+
expect(ctx.notifications[0].type).toBe("info");
|
|
367
|
+
expect(mockSnapshot).not.toHaveBeenCalled();
|
|
368
|
+
expect(mockSavePhaseState).not.toHaveBeenCalled();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("snapshots, auto-enables, jumps phase, notifies", async () => {
|
|
372
|
+
mockLoadTddState.mockReturnValue(tddOk({ current: "red", enabled: true }));
|
|
373
|
+
const ctx = makeCtx();
|
|
374
|
+
await handleTddJump("green", ctx, makeDeps());
|
|
375
|
+
|
|
376
|
+
expect(mockSnapshot).toHaveBeenCalledWith("/test", "red");
|
|
377
|
+
expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
|
|
378
|
+
enabled: true,
|
|
379
|
+
current: "green",
|
|
380
|
+
});
|
|
381
|
+
expect(ctx.notifications[0].message).toContain("Skipped to GREEN phase");
|
|
382
|
+
expect(ctx.notifications[0].type).toBe("info");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("auto-enables when TDD disabled", async () => {
|
|
386
|
+
mockLoadTddState.mockReturnValue(tddOk({ current: "red", enabled: false }));
|
|
387
|
+
const ctx = makeCtx();
|
|
388
|
+
await handleTddJump("green", ctx, makeDeps());
|
|
389
|
+
|
|
390
|
+
expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
|
|
391
|
+
enabled: true,
|
|
392
|
+
current: "green",
|
|
393
|
+
});
|
|
394
|
+
expect(ctx.notifications[0].message).toContain("Skipped to GREEN phase");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("works for refactor from green", async () => {
|
|
398
|
+
mockLoadTddState.mockReturnValue(tddOk({ current: "green" }));
|
|
399
|
+
const ctx = makeCtx();
|
|
400
|
+
await handleTddJump("refactor", ctx, makeDeps());
|
|
401
|
+
expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
|
|
402
|
+
enabled: true,
|
|
403
|
+
current: "refactor",
|
|
404
|
+
});
|
|
405
|
+
expect(ctx.notifications[0].message).toContain("Skipped to REFACTOR phase");
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("works for red from green", async () => {
|
|
409
|
+
mockLoadTddState.mockReturnValue(tddOk({ current: "green" }));
|
|
410
|
+
const ctx = makeCtx();
|
|
411
|
+
await handleTddJump("red", ctx, makeDeps());
|
|
412
|
+
expect(mockSavePhaseState).toHaveBeenCalledWith("/test", {
|
|
413
|
+
enabled: true,
|
|
414
|
+
current: "red",
|
|
415
|
+
});
|
|
416
|
+
expect(ctx.notifications[0].message).toContain("Skipped to RED phase");
|
|
417
|
+
});
|
|
418
|
+
});
|
package/adapters/pi/index.ts
CHANGED
|
@@ -148,6 +148,58 @@ export async function handleTddStatus(
|
|
|
148
148
|
);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
export async function handleTddJump(
|
|
152
|
+
phase: "red" | "green" | "refactor",
|
|
153
|
+
ctx: ExtensionContext,
|
|
154
|
+
deps: {
|
|
155
|
+
loadTddState: typeof loadTddState;
|
|
156
|
+
snapshot: typeof snapshot;
|
|
157
|
+
savePhaseState: typeof savePhaseState;
|
|
158
|
+
tddLog: typeof tddLog;
|
|
159
|
+
} = {
|
|
160
|
+
loadTddState,
|
|
161
|
+
snapshot,
|
|
162
|
+
savePhaseState,
|
|
163
|
+
tddLog,
|
|
164
|
+
},
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
const root = ctx.cwd;
|
|
167
|
+
const tddDir = join(root, ".pi", "tdd");
|
|
168
|
+
|
|
169
|
+
const setup = deps.loadTddState(root);
|
|
170
|
+
if (!setup.ok) {
|
|
171
|
+
deps.tddLog(tddDir, "WARN", `tdd:${phase}: setup invalid`, {
|
|
172
|
+
reason: setup.reason,
|
|
173
|
+
});
|
|
174
|
+
ctx.ui.notify(setup.reason, "error");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const { state } = setup;
|
|
179
|
+
|
|
180
|
+
if (state.current === phase) {
|
|
181
|
+
deps.tddLog(tddDir, "INFO", `tdd:${phase}: already in ${phase}`, {
|
|
182
|
+
phase,
|
|
183
|
+
});
|
|
184
|
+
ctx.ui.notify(`TDD: already in ${phase.toUpperCase()} phase.`, "info");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Snapshot the current phase's work before jumping
|
|
189
|
+
deps.snapshot(root, state.current);
|
|
190
|
+
deps.tddLog(tddDir, "INFO", `tdd:${phase}: snapshot taken`, {
|
|
191
|
+
from: state.current,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Auto-enable if disabled, set phase
|
|
195
|
+
state.enabled = true;
|
|
196
|
+
state.current = phase;
|
|
197
|
+
deps.savePhaseState(root, state);
|
|
198
|
+
|
|
199
|
+
deps.tddLog(tddDir, "INFO", `tdd:${phase}: jumped`);
|
|
200
|
+
ctx.ui.notify(`Skipped to ${phase.toUpperCase()} phase.`, "info");
|
|
201
|
+
}
|
|
202
|
+
|
|
151
203
|
export async function handleTddReset(
|
|
152
204
|
ctx: ExtensionContext,
|
|
153
205
|
deps: {
|
|
@@ -239,6 +291,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
239
291
|
handleTddReset(ctx, { ...defaultDeps, resetGit }),
|
|
240
292
|
});
|
|
241
293
|
|
|
294
|
+
for (const phase of ["red", "green", "refactor"] as const) {
|
|
295
|
+
pi.registerCommand(`tdd:${phase}`, {
|
|
296
|
+
description:
|
|
297
|
+
`Skip to ${phase.toUpperCase()} phase. ` +
|
|
298
|
+
"Snapshot working tree, auto-enable TDD, set phase. No gate checks.",
|
|
299
|
+
handler: (_args: string, ctx: ExtensionContext) =>
|
|
300
|
+
handleTddJump(phase, ctx, defaultDeps),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
242
304
|
registerTools(pi);
|
|
243
305
|
registerHooks(pi);
|
|
244
306
|
}
|
package/adapters/pi/prompts.ts
CHANGED
|
@@ -11,7 +11,8 @@ export function getNudgePrompt(phase: Phase, config: Config): string {
|
|
|
11
11
|
`Blocked files: ${redBlock}\n` +
|
|
12
12
|
"All other files are free to modify. Call `next_tdd_phase` to proceed to GREEN.\n" +
|
|
13
13
|
"Think about what could go wrong and test for it — don't just verify the happy path, " +
|
|
14
|
-
"cover unhappy paths and edge cases too
|
|
14
|
+
"cover unhappy paths and edge cases too.\n" +
|
|
15
|
+
"Keep cycles small so reverting is cheap."
|
|
15
16
|
);
|
|
16
17
|
case "green":
|
|
17
18
|
return (
|
|
@@ -72,24 +72,26 @@ describe("executeNextPhase", () => {
|
|
|
72
72
|
mockAsyncExec.mockResolvedValue({ stdout: "", stderr: "" });
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
it("
|
|
75
|
+
it("throws when TDD not setup", async () => {
|
|
76
76
|
mockLoadTddState.mockReturnValue({
|
|
77
77
|
ok: false,
|
|
78
78
|
reason:
|
|
79
79
|
"Missing .pi/tdd/ directory. See the tdd-enforcer skill to learn how to set up TDD configs.",
|
|
80
80
|
});
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
await expect(
|
|
82
|
+
executeNextPhase({ cwd: "/test" } as any, makeDeps()),
|
|
83
|
+
).rejects.toThrow("Missing .pi/tdd/");
|
|
83
84
|
});
|
|
84
85
|
|
|
85
|
-
it("
|
|
86
|
+
it("throws when TDD disabled", async () => {
|
|
86
87
|
mockLoadTddState.mockReturnValue({
|
|
87
88
|
ok: true,
|
|
88
89
|
state: { enabled: false, current: "red" },
|
|
89
90
|
config: CONFIG,
|
|
90
91
|
});
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
await expect(
|
|
93
|
+
executeNextPhase({ cwd: "/test" } as any, makeDeps()),
|
|
94
|
+
).rejects.toThrow("not enabled");
|
|
93
95
|
});
|
|
94
96
|
|
|
95
97
|
it("blocks when allowlist violations exist", async () => {
|
|
@@ -99,9 +101,9 @@ describe("executeNextPhase", () => {
|
|
|
99
101
|
config: CONFIG,
|
|
100
102
|
});
|
|
101
103
|
mockGetDisallowedChanges.mockReturnValue(["src/violation.ts"]);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
await expect(
|
|
105
|
+
executeNextPhase({ cwd: "/test" } as any, makeDeps()),
|
|
106
|
+
).rejects.toThrow("BLOCKED");
|
|
105
107
|
});
|
|
106
108
|
|
|
107
109
|
it("blocks red→green when tests pass (need failing test)", async () => {
|
|
@@ -115,9 +117,9 @@ describe("executeNextPhase", () => {
|
|
|
115
117
|
message:
|
|
116
118
|
"Tests passed. Add a failing test before transitioning to GREEN.",
|
|
117
119
|
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
await expect(
|
|
121
|
+
executeNextPhase({ cwd: "/test" } as any, makeDeps()),
|
|
122
|
+
).rejects.toThrow("Add a failing test");
|
|
121
123
|
});
|
|
122
124
|
|
|
123
125
|
it("blocks green→refactor when tests fail", async () => {
|
|
@@ -130,9 +132,9 @@ describe("executeNextPhase", () => {
|
|
|
130
132
|
passed: false,
|
|
131
133
|
message: "Tests failed. Fix them before transitioning to REFACTOR.",
|
|
132
134
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
await expect(
|
|
136
|
+
executeNextPhase({ cwd: "/test" } as any, makeDeps()),
|
|
137
|
+
).rejects.toThrow("failed");
|
|
136
138
|
});
|
|
137
139
|
|
|
138
140
|
it("advances red→green when tests fail", async () => {
|
|
@@ -185,9 +187,9 @@ describe("executeNextPhase", () => {
|
|
|
185
187
|
passed: false,
|
|
186
188
|
message: "Tests failed. Fix them before transitioning to RED.",
|
|
187
189
|
});
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
await expect(
|
|
191
|
+
executeNextPhase({ cwd: "/test" } as any, makeDeps()),
|
|
192
|
+
).rejects.toThrow("failed");
|
|
191
193
|
expect(mockSavePhaseState).not.toHaveBeenCalled();
|
|
192
194
|
});
|
|
193
195
|
|
|
@@ -251,57 +253,49 @@ describe("executePreviousPhase", () => {
|
|
|
251
253
|
mockHasParent.mockReturnValue(true);
|
|
252
254
|
});
|
|
253
255
|
|
|
254
|
-
it("
|
|
256
|
+
it("throws when TDD not setup", async () => {
|
|
255
257
|
mockLoadTddState.mockReturnValue({
|
|
256
258
|
ok: false,
|
|
257
259
|
reason: "Missing .pi/tdd/ directory.",
|
|
258
260
|
});
|
|
259
|
-
|
|
260
|
-
{ cwd: "/test" } as any,
|
|
261
|
-
|
|
262
|
-
);
|
|
263
|
-
expect(result.content[0].text).toContain("Missing .pi/tdd/");
|
|
261
|
+
await expect(
|
|
262
|
+
executePreviousPhase({ cwd: "/test" } as any, makeDeps()),
|
|
263
|
+
).rejects.toThrow("Missing .pi/tdd/");
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
-
it("
|
|
266
|
+
it("throws when TDD disabled", async () => {
|
|
267
267
|
mockLoadTddState.mockReturnValue({
|
|
268
268
|
ok: true,
|
|
269
269
|
state: { enabled: false, current: "red" },
|
|
270
270
|
config: CONFIG,
|
|
271
271
|
});
|
|
272
|
-
|
|
273
|
-
{ cwd: "/test" } as any,
|
|
274
|
-
|
|
275
|
-
);
|
|
276
|
-
expect(result.content[0].text).toContain("not enabled");
|
|
272
|
+
await expect(
|
|
273
|
+
executePreviousPhase({ cwd: "/test" } as any, makeDeps()),
|
|
274
|
+
).rejects.toThrow("not enabled");
|
|
277
275
|
});
|
|
278
276
|
|
|
279
|
-
it("
|
|
277
|
+
it("throws when no parent commit", async () => {
|
|
280
278
|
mockLoadTddState.mockReturnValue({
|
|
281
279
|
ok: true,
|
|
282
280
|
state: { enabled: true, current: "red" },
|
|
283
281
|
config: CONFIG,
|
|
284
282
|
});
|
|
285
283
|
mockHasParent.mockReturnValue(false);
|
|
286
|
-
|
|
287
|
-
{ cwd: "/test" } as any,
|
|
288
|
-
|
|
289
|
-
);
|
|
290
|
-
expect(result.content[0].text).toContain("No previous phase");
|
|
284
|
+
await expect(
|
|
285
|
+
executePreviousPhase({ cwd: "/test" } as any, makeDeps()),
|
|
286
|
+
).rejects.toThrow("No previous phase");
|
|
291
287
|
});
|
|
292
288
|
|
|
293
|
-
it("
|
|
289
|
+
it("throws when HEAD message is not a TDD snapshot", async () => {
|
|
294
290
|
mockLoadTddState.mockReturnValue({
|
|
295
291
|
ok: true,
|
|
296
292
|
state: { enabled: true, current: "red" },
|
|
297
293
|
config: CONFIG,
|
|
298
294
|
});
|
|
299
295
|
mockHeadMessage.mockReturnValue("garbage");
|
|
300
|
-
|
|
301
|
-
{ cwd: "/test" } as any,
|
|
302
|
-
|
|
303
|
-
);
|
|
304
|
-
expect(result.content[0].text).toContain("not a TDD snapshot");
|
|
296
|
+
await expect(
|
|
297
|
+
executePreviousPhase({ cwd: "/test" } as any, makeDeps()),
|
|
298
|
+
).rejects.toThrow("not a TDD snapshot");
|
|
305
299
|
});
|
|
306
300
|
|
|
307
301
|
it("reverts to previous phase on success", async () => {
|
|
@@ -363,23 +357,25 @@ describe("executeTddStatus", () => {
|
|
|
363
357
|
mockTddLog = vi.fn();
|
|
364
358
|
});
|
|
365
359
|
|
|
366
|
-
it("
|
|
360
|
+
it("throws when TDD not setup", async () => {
|
|
367
361
|
mockLoadTddState.mockReturnValue({
|
|
368
362
|
ok: false,
|
|
369
363
|
reason: "Missing .pi/tdd/ directory.",
|
|
370
364
|
});
|
|
371
|
-
|
|
372
|
-
|
|
365
|
+
await expect(
|
|
366
|
+
executeTddStatus({ cwd: "/test" } as any, makeDeps()),
|
|
367
|
+
).rejects.toThrow("Missing .pi/tdd/");
|
|
373
368
|
});
|
|
374
369
|
|
|
375
|
-
it("
|
|
370
|
+
it("throws when TDD disabled", async () => {
|
|
376
371
|
mockLoadTddState.mockReturnValue({
|
|
377
372
|
ok: true,
|
|
378
373
|
state: { enabled: false, current: "red" },
|
|
379
374
|
config: CONFIG,
|
|
380
375
|
});
|
|
381
|
-
|
|
382
|
-
|
|
376
|
+
await expect(
|
|
377
|
+
executeTddStatus({ cwd: "/test" } as any, makeDeps()),
|
|
378
|
+
).rejects.toThrow("not enabled");
|
|
383
379
|
});
|
|
384
380
|
|
|
385
381
|
it("returns status details when TDD enabled", async () => {
|
package/adapters/pi/tools.ts
CHANGED
|
@@ -102,19 +102,11 @@ export async function executeNextPhase(
|
|
|
102
102
|
deps.tddLog(tddDir, "WARN", "next_tdd_phase: TDD not active", {
|
|
103
103
|
reason: tdd.reason,
|
|
104
104
|
});
|
|
105
|
-
|
|
106
|
-
content: [{ type: "text", text: `TDD: ${tdd.reason}` }],
|
|
107
|
-
details: {},
|
|
108
|
-
};
|
|
105
|
+
throw new Error(`TDD: ${tdd.reason}`);
|
|
109
106
|
}
|
|
110
107
|
if (!tdd.state.enabled) {
|
|
111
108
|
deps.tddLog(tddDir, "WARN", "next_tdd_phase: TDD disabled");
|
|
112
|
-
|
|
113
|
-
content: [
|
|
114
|
-
{ type: "text", text: "TDD is not enabled. Run /tdd:on to enable it." },
|
|
115
|
-
],
|
|
116
|
-
details: {},
|
|
117
|
-
};
|
|
109
|
+
throw new Error("TDD is not enabled. Run /tdd:on to enable it.");
|
|
118
110
|
}
|
|
119
111
|
|
|
120
112
|
const { state, config } = tdd;
|
|
@@ -130,18 +122,11 @@ export async function executeNextPhase(
|
|
|
130
122
|
from,
|
|
131
123
|
violations,
|
|
132
124
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
{
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
};
|
|
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
|
+
);
|
|
145
130
|
}
|
|
146
131
|
|
|
147
132
|
// 2. Gate check
|
|
@@ -176,7 +161,7 @@ export async function executeNextPhase(
|
|
|
176
161
|
});
|
|
177
162
|
|
|
178
163
|
if (!gate.passed) {
|
|
179
|
-
|
|
164
|
+
throw new Error(gate.message);
|
|
180
165
|
}
|
|
181
166
|
|
|
182
167
|
// 3. Snapshot — label with the phase the work was done in
|
|
@@ -214,19 +199,11 @@ export async function executePreviousPhase(
|
|
|
214
199
|
deps.tddLog(tddDir, "WARN", "previous_tdd_phase: TDD not active", {
|
|
215
200
|
reason: tdd.reason,
|
|
216
201
|
});
|
|
217
|
-
|
|
218
|
-
content: [{ type: "text", text: `TDD: ${tdd.reason}` }],
|
|
219
|
-
details: {},
|
|
220
|
-
};
|
|
202
|
+
throw new Error(`TDD: ${tdd.reason}`);
|
|
221
203
|
}
|
|
222
204
|
if (!tdd.state.enabled) {
|
|
223
205
|
deps.tddLog(tddDir, "WARN", "previous_tdd_phase: TDD disabled");
|
|
224
|
-
|
|
225
|
-
content: [
|
|
226
|
-
{ type: "text", text: "TDD is not enabled. Run /tdd:on to enable it." },
|
|
227
|
-
],
|
|
228
|
-
details: {},
|
|
229
|
-
};
|
|
206
|
+
throw new Error("TDD is not enabled. Run /tdd:on to enable it.");
|
|
230
207
|
}
|
|
231
208
|
|
|
232
209
|
const { state } = tdd;
|
|
@@ -235,10 +212,7 @@ export async function executePreviousPhase(
|
|
|
235
212
|
deps.tddLog(tddDir, "WARN", "previous_tdd_phase: no parent commit", {
|
|
236
213
|
phase: state.current,
|
|
237
214
|
});
|
|
238
|
-
|
|
239
|
-
content: [{ type: "text", text: "No previous phase to revert to." }],
|
|
240
|
-
details: {},
|
|
241
|
-
};
|
|
215
|
+
throw new Error("No previous phase to revert to.");
|
|
242
216
|
}
|
|
243
217
|
|
|
244
218
|
// Read phase from HEAD snapshot commit message (source of truth).
|
|
@@ -250,18 +224,11 @@ export async function executePreviousPhase(
|
|
|
250
224
|
deps.tddLog(tddDir, "ERROR", "previous_tdd_phase: invalid HEAD message", {
|
|
251
225
|
headMsg,
|
|
252
226
|
});
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
};
|
|
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
|
+
);
|
|
265
232
|
}
|
|
266
233
|
const label = phaseMatch[1];
|
|
267
234
|
if (label !== "red" && label !== "green" && label !== "refactor") {
|
|
@@ -269,15 +236,9 @@ export async function executePreviousPhase(
|
|
|
269
236
|
headMsg,
|
|
270
237
|
label,
|
|
271
238
|
});
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
type: "text",
|
|
276
|
-
text: `HEAD commit "${headMsg}" has unexpected label. Cannot determine previous phase.`,
|
|
277
|
-
},
|
|
278
|
-
],
|
|
279
|
-
details: {},
|
|
280
|
-
};
|
|
239
|
+
throw new Error(
|
|
240
|
+
`HEAD commit "${headMsg}" has unexpected label. Cannot determine previous phase.`,
|
|
241
|
+
);
|
|
281
242
|
}
|
|
282
243
|
const prevPhase: Phase = label;
|
|
283
244
|
deps.tddLog(tddDir, "INFO", "previous_tdd_phase: reverting", {
|
|
@@ -329,19 +290,11 @@ export async function executeTddStatus(
|
|
|
329
290
|
deps.tddLog(tddDir, "WARN", "tdd_status: TDD not active", {
|
|
330
291
|
reason: result.reason,
|
|
331
292
|
});
|
|
332
|
-
|
|
333
|
-
content: [{ type: "text", text: `TDD: ${result.reason}` }],
|
|
334
|
-
details: {},
|
|
335
|
-
};
|
|
293
|
+
throw new Error(`TDD: ${result.reason}`);
|
|
336
294
|
}
|
|
337
295
|
if (!result.state.enabled) {
|
|
338
296
|
deps.tddLog(tddDir, "WARN", "tdd_status: TDD disabled");
|
|
339
|
-
|
|
340
|
-
content: [
|
|
341
|
-
{ type: "text", text: "TDD is not enabled. Run /tdd:on to enable it." },
|
|
342
|
-
],
|
|
343
|
-
details: {},
|
|
344
|
-
};
|
|
297
|
+
throw new Error("TDD is not enabled. Run /tdd:on to enable it.");
|
|
345
298
|
}
|
|
346
299
|
|
|
347
300
|
const { state, config } = result;
|
package/package.json
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
2
|
+
"name": "tdd-enforcer",
|
|
3
|
+
"version": "0.2.8",
|
|
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
|
}
|