tdd-enforcer 0.2.8 → 0.2.9

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.
@@ -120,7 +120,7 @@ export async function handleToolCall(
120
120
  return {
121
121
  block: true,
122
122
  reason:
123
- "TDD: Config files are locked. No bypassing TDD allowed. If bypassing is justified, ask the user: turn TDD off (/tdd:off), reset (/tdd:reset), or change phase via /tdd commands.",
123
+ "TDD: Config files are locked. No bypassing TDD allowed. If bypassing is justified, ask the user: turn TDD off (/tdd:off), reset (/tdd:reset), or change phase via /tdd commands.\n\nIf TDD reverts too much of your progress, reduce the scope of each TDD cycle to minimise lost progress.",
124
124
  };
125
125
  }
126
126
 
@@ -267,6 +267,9 @@ function formatWarning(
267
267
  .join("");
268
268
  let warning = `\n\n⛔ ${phase.toUpperCase()}: reverted locked files modified by bash:`;
269
269
  for (const f of cmdViolations) warning += `\n - ${f}`;
270
+ if (cmdViolations.some((f) => f.startsWith(".pi/tdd/"))) {
271
+ warning += `\n\nIf TDD reverts too much of your progress, reduce the scope of each TDD cycle to minimise lost progress.`;
272
+ }
270
273
  if (cmdAllowed.length > 0) {
271
274
  warning += `\n\nAllowed changes retained:`;
272
275
  for (const f of cmdAllowed) warning += `\n - ${f}`;
@@ -1,5 +1,6 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import {
3
+ handleBeforeAgentStart,
3
4
  handleTddJump,
4
5
  handleTddOff,
5
6
  handleTddOn,
@@ -416,3 +417,90 @@ describe("handleTddJump", () => {
416
417
  expect(ctx.notifications[0].message).toContain("Skipped to RED phase");
417
418
  });
418
419
  });
420
+
421
+ // ── handleBeforeAgentStart ──────────────────────────────────────────────────
422
+
423
+ describe("handleBeforeAgentStart", () => {
424
+ let mockLoadTddState: ReturnType<typeof vi.fn>;
425
+
426
+ function makeDeps(overrides = {}) {
427
+ return {
428
+ loadTddState: mockLoadTddState,
429
+ ...overrides,
430
+ };
431
+ }
432
+
433
+ function makeEvent(): {
434
+ systemPromptOptions: { promptGuidelines: string[] };
435
+ } {
436
+ return { systemPromptOptions: { promptGuidelines: [] } };
437
+ }
438
+
439
+ beforeEach(() => {
440
+ vi.clearAllMocks();
441
+ mockLoadTddState = vi.fn();
442
+ });
443
+
444
+ it("pushes guidelines when TDD enabled", async () => {
445
+ mockLoadTddState.mockReturnValue({
446
+ ok: true,
447
+ state: { enabled: true, current: "red" },
448
+ config: {
449
+ blockedInRed: [],
450
+ blockedInGreen: [],
451
+ testCommands: [],
452
+ timeoutSeconds: 30,
453
+ },
454
+ });
455
+ const event = makeEvent();
456
+ await handleBeforeAgentStart(
457
+ event as any,
458
+ { cwd: "/test" } as any,
459
+ makeDeps(),
460
+ );
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
+ );
471
+ });
472
+
473
+ it("does not push guidelines when TDD not setup", async () => {
474
+ mockLoadTddState.mockReturnValue({
475
+ ok: false,
476
+ reason: "Missing .pi/tdd/",
477
+ });
478
+ const event = makeEvent();
479
+ await handleBeforeAgentStart(
480
+ event as any,
481
+ { cwd: "/test" } as any,
482
+ makeDeps(),
483
+ );
484
+ expect(event.systemPromptOptions.promptGuidelines).toHaveLength(0);
485
+ });
486
+
487
+ it("does not push guidelines when TDD disabled", async () => {
488
+ mockLoadTddState.mockReturnValue({
489
+ ok: true,
490
+ state: { enabled: false, current: "red" },
491
+ config: {
492
+ blockedInRed: [],
493
+ blockedInGreen: [],
494
+ testCommands: [],
495
+ timeoutSeconds: 30,
496
+ },
497
+ });
498
+ const event = makeEvent();
499
+ await handleBeforeAgentStart(
500
+ event as any,
501
+ { cwd: "/test" } as any,
502
+ makeDeps(),
503
+ );
504
+ expect(event.systemPromptOptions.promptGuidelines).toHaveLength(0);
505
+ });
506
+ });
@@ -148,6 +148,25 @@ export async function handleTddStatus(
148
148
  );
149
149
  }
150
150
 
151
+ export async function handleBeforeAgentStart(
152
+ event: {
153
+ systemPromptOptions: { promptGuidelines: string[] };
154
+ },
155
+ ctx: { cwd: string },
156
+ deps: {
157
+ loadTddState: typeof loadTddState;
158
+ },
159
+ ): 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
+ );
168
+ }
169
+
151
170
  export async function handleTddJump(
152
171
  phase: "red" | "green" | "refactor",
153
172
  ctx: ExtensionContext,
@@ -303,4 +322,8 @@ export default function (pi: ExtensionAPI) {
303
322
 
304
323
  registerTools(pi);
305
324
  registerHooks(pi);
325
+
326
+ pi.on("before_agent_start", (event, ctx) =>
327
+ handleBeforeAgentStart(event, ctx, { loadTddState }),
328
+ );
306
329
  }
@@ -12,7 +12,7 @@ export function getNudgePrompt(phase: Phase, config: Config): string {
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
14
  "cover unhappy paths and edge cases too.\n" +
15
- "Keep cycles small so reverting is cheap."
15
+ "Minimise the scope of each TDD cycle so reverting is cheap."
16
16
  );
17
17
  case "green":
18
18
  return (
@@ -137,6 +137,21 @@ describe("executeNextPhase", () => {
137
137
  ).rejects.toThrow("failed");
138
138
  });
139
139
 
140
+ it("result text has leading newline for spacing", async () => {
141
+ mockLoadTddState.mockReturnValue({
142
+ ok: true,
143
+ state: { enabled: true, current: "red" },
144
+ config: CONFIG,
145
+ });
146
+ mockCheckGate.mockResolvedValue({
147
+ passed: true,
148
+ message: "Tests fail — proceed to GREEN.",
149
+ });
150
+ mockGetNudgePrompt.mockReturnValue("content");
151
+ const result = await executeNextPhase({ cwd: "/test" } as any, makeDeps());
152
+ expect(result.content[0].text.startsWith("\n")).toBe(true);
153
+ });
154
+
140
155
  it("advances red→green when tests fail", async () => {
141
156
  mockLoadTddState.mockReturnValue({
142
157
  ok: true,
@@ -178,7 +178,7 @@ export async function executeNextPhase(
178
178
  deps.tddLog(tddDir, "INFO", "next_tdd_phase: complete", { from, to });
179
179
 
180
180
  return {
181
- content: [{ type: "text", text: deps.getNudgePrompt(to, config) }],
181
+ content: [{ type: "text", text: `\n${deps.getNudgePrompt(to, config)}` }],
182
182
  details: {},
183
183
  };
184
184
  }
@@ -266,7 +266,7 @@ export async function executePreviousPhase(
266
266
  content: [
267
267
  {
268
268
  type: "text",
269
- text: `Reverted to ${prevPhase.toUpperCase()}. Working tree has the previous snapshot content as unstaged changes.`,
269
+ text: `\nReverted to ${prevPhase.toUpperCase()}. Working tree has the previous snapshot content as unstaged changes.`,
270
270
  },
271
271
  ],
272
272
  details: {},
@@ -312,7 +312,7 @@ export async function executeTddStatus(
312
312
  {
313
313
  type: "text",
314
314
  text:
315
- `TDD enforcer enabled\n` +
315
+ `\nTDD enforcer enabled\n` +
316
316
  `Current phase: ${phaseStr}\n` +
317
317
  `Blocked in RED: ${redBlk}\n` +
318
318
  `Blocked in GREEN: ${greenBlk}\n` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tdd-enforcer",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "fmt": "biome format --write .",
@@ -54,7 +54,7 @@ It locks files per phase — only test files in RED, only implementation files i
54
54
  ### RED
55
55
  Files matching `blockedInRed` are locked — everything else is free.
56
56
 
57
- Write failing tests for one feature at a time. Think about what could go wrong and test for it — don't just verify the happy path, cover unhappy paths and edge cases too. Keep cycles small so reverting is cheap and safe if assumptions turn out wrong.
57
+ Write failing tests for one feature at a time. Think about what could go wrong and test for it — don't just verify the happy path, cover unhappy paths and edge cases too. Minimise the scope of each TDD cycle so reverting is cheap and safe if assumptions turn out wrong.
58
58
 
59
59
  Call `next_tdd_phase` once tests fail.
60
60