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,222 +1,306 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
1
  import { join } from "node:path";
3
- import { savePhaseState, resetGit, snapshot } from "../../engine/index.js";
4
- import { registerTools } from "./tools.js";
5
- import { registerHooks } from "./hooks.js";
2
+ import type {
3
+ ExtensionAPI,
4
+ ExtensionContext,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { resetGit, savePhaseState, snapshot } from "../../engine/index.js";
6
7
  import { loadTddState } from "./helpers.js";
8
+ import { registerHooks } from "./hooks.js";
7
9
  import { tddLog } from "./log.js";
10
+ import { registerTools } from "./tools.js";
8
11
 
9
12
  export async function handleTddOn(
10
- ctx: ExtensionContext,
11
- deps: {
12
- loadTddState: typeof loadTddState;
13
- snapshot: typeof snapshot;
14
- savePhaseState: typeof savePhaseState;
15
- tddLog: typeof tddLog;
16
- } = {
17
- loadTddState,
18
- snapshot,
19
- savePhaseState,
20
- tddLog,
21
- },
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
+ },
22
25
  ): Promise<void> {
23
- const root = ctx.cwd;
24
- const tddDir = join(root, ".pi", "tdd");
25
-
26
- deps.tddLog(tddDir, "INFO", "tdd:on: starting");
27
-
28
- const setup = deps.loadTddState(root);
29
- if (!setup.ok) {
30
- deps.tddLog(tddDir, "WARN", "tdd:on: setup invalid", { reason: setup.reason });
31
- ctx.ui.notify(setup.reason, "error");
32
- return;
33
- }
34
-
35
- const { state } = setup;
36
-
37
- if (state.enabled) {
38
- deps.tddLog(tddDir, "INFO", "tdd:on: already enabled", {
39
- phase: state.current,
40
- });
41
- ctx.ui.notify(`TDD already enabled — ${state.current.toUpperCase()} phase`, "info");
42
- return;
43
- }
44
-
45
- // Snapshot working tree so stale baseline doesn't nuke user changes
46
- deps.snapshot(root, state.current);
47
- deps.tddLog(tddDir, "INFO", "tdd:on: snapshot taken", {
48
- phase: state.current,
49
- });
50
-
51
- state.enabled = true;
52
- deps.savePhaseState(root, state);
53
- deps.tddLog(tddDir, "INFO", "tdd:on: enabled", {
54
- phase: state.current,
55
- });
56
- 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");
57
65
  }
58
66
 
59
67
  export async function handleTddOff(
60
- ctx: ExtensionContext,
61
- deps: {
62
- loadTddState: typeof loadTddState;
63
- savePhaseState: typeof savePhaseState;
64
- tddLog: typeof tddLog;
65
- } = {
66
- loadTddState,
67
- savePhaseState,
68
- tddLog,
69
- },
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
+ },
70
78
  ): Promise<void> {
71
- const root = ctx.cwd;
72
- const tddDir = join(root, ".pi", "tdd");
73
-
74
- const setup = deps.loadTddState(root);
75
- if (!setup.ok) {
76
- deps.tddLog(tddDir, "WARN", "tdd:off: setup invalid", { reason: setup.reason });
77
- ctx.ui.notify(setup.reason, "error");
78
- return;
79
- }
80
-
81
- const { state } = setup;
82
-
83
- if (!state.enabled) {
84
- deps.tddLog(tddDir, "INFO", "tdd:off: already disabled");
85
- ctx.ui.notify("TDD already disabled", "info");
86
- return;
87
- }
88
-
89
- state.enabled = false;
90
- deps.savePhaseState(root, state);
91
- deps.tddLog(tddDir, "INFO", "tdd:off: disabled", {
92
- was: state.current,
93
- });
94
- 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");
95
105
  }
96
106
 
97
107
  export async function handleTddStatus(
98
- ctx: ExtensionContext,
99
- deps: {
100
- loadTddState: typeof loadTddState;
101
- tddLog: typeof tddLog;
102
- } = {
103
- loadTddState,
104
- tddLog,
105
- },
108
+ ctx: ExtensionContext,
109
+ deps: {
110
+ loadTddState: typeof loadTddState;
111
+ tddLog: typeof tddLog;
112
+ } = {
113
+ loadTddState,
114
+ tddLog,
115
+ },
106
116
  ): Promise<void> {
107
- const root = ctx.cwd;
108
- const tddDir = join(root, ".pi", "tdd");
109
- const result = deps.loadTddState(root);
110
-
111
- if (!result.ok) {
112
- deps.tddLog(tddDir, "WARN", "tdd:status: setup invalid", {
113
- reason: result.reason,
114
- });
115
- ctx.ui.notify(`TDD: ${result.reason}`, "error");
116
- return;
117
- }
118
-
119
- const { state, config } = result;
120
- const enabledStr = state.enabled ? "enabled" : "disabled";
121
- const phaseStr = state.current.toUpperCase();
122
- const redBlk = config.blockedInRed.join(", ") || "(none)";
123
- const greenBlk = config.blockedInGreen.join(", ") || "(none)";
124
- const commands = config.testCommands.join(", ") || "(none)";
125
-
126
- deps.tddLog(tddDir, "INFO", "tdd:status: queried", {
127
- enabled: state.enabled,
128
- phase: state.current,
129
- });
130
-
131
- ctx.ui.notify(
132
- `TDD enforcer ${enabledStr}\n` +
133
- `Current phase: ${phaseStr}\n` +
134
- `Blocked in RED: ${redBlk}\n` +
135
- `Blocked in GREEN: ${greenBlk}\n` +
136
- `Test commands: ${commands}`,
137
- "info",
138
- );
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
+ }
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");
139
201
  }
140
202
 
141
203
  export async function handleTddReset(
142
- ctx: ExtensionContext,
143
- deps: {
144
- loadTddState: typeof loadTddState;
145
- resetGit: typeof resetGit;
146
- snapshot: typeof snapshot;
147
- savePhaseState: typeof savePhaseState;
148
- tddLog: typeof tddLog;
149
- } = {
150
- loadTddState,
151
- resetGit,
152
- snapshot,
153
- savePhaseState,
154
- tddLog,
155
- },
204
+ ctx: ExtensionContext,
205
+ deps: {
206
+ loadTddState: typeof loadTddState;
207
+ resetGit: typeof resetGit;
208
+ snapshot: typeof snapshot;
209
+ savePhaseState: typeof savePhaseState;
210
+ tddLog: typeof tddLog;
211
+ } = {
212
+ loadTddState,
213
+ resetGit,
214
+ snapshot,
215
+ savePhaseState,
216
+ tddLog,
217
+ },
156
218
  ): Promise<void> {
157
- const root = ctx.cwd;
158
- const tddDir = join(root, ".pi", "tdd");
159
-
160
- deps.tddLog(tddDir, "INFO", "tdd:reset: starting");
161
-
162
- const setup = deps.loadTddState(root);
163
- if (!setup.ok) {
164
- deps.tddLog(tddDir, "WARN", "tdd:reset: setup invalid", { reason: setup.reason });
165
- ctx.ui.notify(setup.reason, "error");
166
- return;
167
- }
168
-
169
- // Nuke git history and re-init
170
- try {
171
- deps.resetGit(root);
172
- deps.tddLog(tddDir, "INFO", "tdd:reset: git reset and re-initialised");
173
- } catch (e) {
174
- deps.tddLog(tddDir, "ERROR", "tdd:reset: git reset failed", {
175
- error: (e as Error).message,
176
- });
177
- ctx.ui.notify("Failed to reset private git repo.", "error");
178
- return;
179
- }
180
-
181
- // Snapshot current working tree
182
- deps.snapshot(root, "red");
183
- deps.tddLog(tddDir, "INFO", "tdd:reset: snapshot taken");
184
-
185
- // Reset state to RED (disabled, user must run /tdd:on)
186
- deps.savePhaseState(root, { enabled: false, current: "red" });
187
- deps.tddLog(tddDir, "INFO", "tdd:reset: complete");
188
-
189
- ctx.ui.notify(
190
- "TDD snapshot history reset. Run /tdd:on to re-enable enforcement.",
191
- "warning",
192
- );
219
+ const root = ctx.cwd;
220
+ const tddDir = join(root, ".pi", "tdd");
221
+
222
+ deps.tddLog(tddDir, "INFO", "tdd:reset: starting");
223
+
224
+ const setup = deps.loadTddState(root);
225
+ if (!setup.ok) {
226
+ deps.tddLog(tddDir, "WARN", "tdd:reset: setup invalid", {
227
+ reason: setup.reason,
228
+ });
229
+ ctx.ui.notify(setup.reason, "error");
230
+ return;
231
+ }
232
+
233
+ // Nuke git history and re-init
234
+ try {
235
+ deps.resetGit(root);
236
+ deps.tddLog(tddDir, "INFO", "tdd:reset: git reset and re-initialised");
237
+ } catch (e) {
238
+ deps.tddLog(tddDir, "ERROR", "tdd:reset: git reset failed", {
239
+ error: (e as Error).message,
240
+ });
241
+ ctx.ui.notify("Failed to reset private git repo.", "error");
242
+ return;
243
+ }
244
+
245
+ // Snapshot current working tree
246
+ deps.snapshot(root, "red");
247
+ deps.tddLog(tddDir, "INFO", "tdd:reset: snapshot taken");
248
+
249
+ // Reset state to RED (disabled, user must run /tdd:on)
250
+ deps.savePhaseState(root, { enabled: false, current: "red" });
251
+ deps.tddLog(tddDir, "INFO", "tdd:reset: complete");
252
+
253
+ ctx.ui.notify(
254
+ "TDD snapshot history reset. Run /tdd:on to re-enable enforcement.",
255
+ "warning",
256
+ );
193
257
  }
194
258
 
195
259
  export default function (pi: ExtensionAPI) {
196
- const defaultDeps = { loadTddState, snapshot, savePhaseState, tddLog, resetGit };
197
-
198
- pi.registerCommand("tdd:on", {
199
- description: "Enable TDD enforcement",
200
- handler: (_args: string, ctx: ExtensionContext) => handleTddOn(ctx, defaultDeps),
201
- });
202
-
203
- pi.registerCommand("tdd:off", {
204
- description: "Disable TDD enforcement",
205
- handler: (_args: string, ctx: ExtensionContext) => handleTddOff(ctx, defaultDeps),
206
- });
207
-
208
- pi.registerCommand("tdd:status", {
209
- description: "Show TDD enforcement status",
210
- handler: (_args: string, ctx: ExtensionContext) => handleTddStatus(ctx, defaultDeps),
211
- });
212
-
213
- pi.registerCommand("tdd:reset", {
214
- description:
215
- "WARNING: Destroys ALL TDD snapshot history and resets to RED phase. " +
216
- "Working tree is preserved. Run /tdd:on to re-enable after reset.",
217
- handler: (_args: string, ctx: ExtensionContext) => handleTddReset(ctx, { ...defaultDeps, resetGit }),
218
- });
219
-
220
- registerTools(pi);
221
- registerHooks(pi);
260
+ const defaultDeps = {
261
+ loadTddState,
262
+ snapshot,
263
+ savePhaseState,
264
+ tddLog,
265
+ resetGit,
266
+ };
267
+
268
+ pi.registerCommand("tdd:on", {
269
+ description: "Enable TDD enforcement",
270
+ handler: (_args: string, ctx: ExtensionContext) =>
271
+ handleTddOn(ctx, defaultDeps),
272
+ });
273
+
274
+ pi.registerCommand("tdd:off", {
275
+ description: "Disable TDD enforcement",
276
+ handler: (_args: string, ctx: ExtensionContext) =>
277
+ handleTddOff(ctx, defaultDeps),
278
+ });
279
+
280
+ pi.registerCommand("tdd:status", {
281
+ description: "Show TDD enforcement status",
282
+ handler: (_args: string, ctx: ExtensionContext) =>
283
+ handleTddStatus(ctx, defaultDeps),
284
+ });
285
+
286
+ pi.registerCommand("tdd:reset", {
287
+ description:
288
+ "WARNING: Destroys ALL TDD snapshot history and resets to RED phase. " +
289
+ "Working tree is preserved. Run /tdd:on to re-enable after reset.",
290
+ handler: (_args: string, ctx: ExtensionContext) =>
291
+ handleTddReset(ctx, { ...defaultDeps, resetGit }),
292
+ });
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
+
304
+ registerTools(pi);
305
+ registerHooks(pi);
222
306
  }