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.
- package/.github/workflows/ci.yml +28 -0
- package/adapters/pi/helpers.test.ts +246 -244
- package/adapters/pi/helpers.ts +117 -91
- package/adapters/pi/hooks.test.ts +528 -425
- package/adapters/pi/hooks.ts +297 -230
- package/adapters/pi/index.test.ts +388 -272
- package/adapters/pi/index.ts +284 -200
- package/adapters/pi/log.test.ts +91 -91
- package/adapters/pi/log.ts +39 -27
- package/adapters/pi/prompts.test.ts +25 -25
- package/adapters/pi/prompts.ts +29 -30
- package/adapters/pi/tools.test.ts +387 -331
- package/adapters/pi/tools.ts +321 -325
- package/behaviour.md +15 -1
- package/biome.json +37 -0
- package/engine/config.test.ts +157 -157
- package/engine/config.ts +22 -19
- package/engine/enforce.test.ts +155 -145
- package/engine/enforce.ts +31 -23
- package/engine/git.test.ts +529 -507
- package/engine/git.ts +240 -114
- package/engine/index.ts +27 -4
- package/engine/state.test.ts +69 -69
- package/engine/state.ts +22 -20
- package/engine/transition.test.ts +229 -177
- package/engine/transition.ts +55 -52
- package/engine/types.ts +9 -9
- package/package.json +30 -23
- package/tsconfig.json +10 -10
package/adapters/pi/index.ts
CHANGED
|
@@ -1,222 +1,306 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
}
|