@quinteroac/agents-coding-toolkit 0.1.0-preview → 0.2.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.
- package/README.md +29 -15
- package/package.json +14 -4
- package/scaffold/.agents/flow/tmpl_it_000001_progress.example.json +20 -0
- package/scaffold/.agents/skills/execute-refactor-item/tmpl_SKILL.md +59 -0
- package/scaffold/.agents/skills/plan-refactor/tmpl_SKILL.md +89 -9
- package/scaffold/.agents/skills/refine-refactor-plan/tmpl_SKILL.md +30 -0
- package/scaffold/.agents/tmpl_state_rules.md +0 -1
- package/scaffold/schemas/tmpl_prototype-progress.ts +22 -0
- package/scaffold/schemas/tmpl_refactor-execution-progress.ts +16 -0
- package/scaffold/schemas/tmpl_refactor-prd.ts +14 -0
- package/scaffold/schemas/tmpl_state.ts +1 -0
- package/scaffold/schemas/tmpl_test-execution-progress.ts +17 -0
- package/schemas/issues.ts +19 -0
- package/schemas/prototype-progress.ts +22 -0
- package/schemas/refactor-execution-progress.ts +16 -0
- package/schemas/refactor-prd.ts +14 -0
- package/schemas/state.test.ts +58 -0
- package/schemas/state.ts +1 -0
- package/schemas/test-execution-progress.ts +17 -0
- package/schemas/test-plan.test.ts +1 -1
- package/schemas/validate-progress.ts +1 -1
- package/schemas/validate-state.ts +1 -1
- package/src/cli.test.ts +57 -0
- package/src/cli.ts +227 -58
- package/src/commands/approve-project-context.ts +13 -6
- package/src/commands/approve-prototype.test.ts +427 -0
- package/src/commands/approve-prototype.ts +185 -0
- package/src/commands/approve-refactor-plan.test.ts +254 -0
- package/src/commands/approve-refactor-plan.ts +200 -0
- package/src/commands/approve-requirement.test.ts +224 -0
- package/src/commands/approve-requirement.ts +75 -16
- package/src/commands/approve-test-plan.test.ts +2 -2
- package/src/commands/approve-test-plan.ts +21 -7
- package/src/commands/create-issue.test.ts +2 -2
- package/src/commands/create-project-context.ts +31 -25
- package/src/commands/create-prototype.test.ts +488 -18
- package/src/commands/create-prototype.ts +185 -63
- package/src/commands/create-test-plan.ts +8 -6
- package/src/commands/define-refactor-plan.test.ts +208 -0
- package/src/commands/define-refactor-plan.ts +96 -0
- package/src/commands/define-requirement.ts +15 -9
- package/src/commands/execute-automated-fix.test.ts +78 -33
- package/src/commands/execute-automated-fix.ts +34 -101
- package/src/commands/execute-refactor.test.ts +954 -0
- package/src/commands/execute-refactor.ts +332 -0
- package/src/commands/execute-test-plan.test.ts +24 -16
- package/src/commands/execute-test-plan.ts +29 -55
- package/src/commands/flow-config.ts +79 -0
- package/src/commands/flow.test.ts +755 -0
- package/src/commands/flow.ts +405 -0
- package/src/commands/refine-project-context.ts +9 -7
- package/src/commands/refine-refactor-plan.test.ts +210 -0
- package/src/commands/refine-refactor-plan.ts +95 -0
- package/src/commands/refine-requirement.ts +9 -6
- package/src/commands/refine-test-plan.test.ts +2 -2
- package/src/commands/refine-test-plan.ts +9 -6
- package/src/commands/start-iteration.test.ts +52 -0
- package/src/commands/start-iteration.ts +5 -0
- package/src/commands/write-json.ts +102 -97
- package/src/flow-cli.test.ts +18 -0
- package/src/force-flag.test.ts +144 -0
- package/src/guardrail.test.ts +411 -0
- package/src/guardrail.ts +82 -0
- package/src/install.test.ts +7 -5
- package/src/pack.test.ts +2 -1
- package/src/progress-utils.ts +34 -0
- package/src/readline.ts +23 -0
- package/src/write-json-artifact.ts +33 -0
- package/scaffold/.agents/flow/tmpl_README.md +0 -7
- package/scaffold/.agents/flow/tmpl_iteration_close_checklist.example.md +0 -11
- package/schemas/test-plan.ts +0 -20
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { State } from "../scaffold/schemas/tmpl_state";
|
|
4
|
+
import { assertGuardrail, GuardrailAbortError } from "./guardrail";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Test helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
function makeState(flow_guardrail?: "strict" | "relaxed"): State {
|
|
11
|
+
return {
|
|
12
|
+
current_iteration: "000001",
|
|
13
|
+
current_phase: "define",
|
|
14
|
+
flow_guardrail,
|
|
15
|
+
phases: {
|
|
16
|
+
define: {
|
|
17
|
+
requirement_definition: { status: "pending", file: null },
|
|
18
|
+
prd_generation: { status: "pending", file: null },
|
|
19
|
+
},
|
|
20
|
+
prototype: {
|
|
21
|
+
project_context: { status: "pending", file: null },
|
|
22
|
+
test_plan: { status: "pending", file: null },
|
|
23
|
+
tp_generation: { status: "pending", file: null },
|
|
24
|
+
prototype_build: { status: "pending", file: null },
|
|
25
|
+
test_execution: { status: "pending", file: null },
|
|
26
|
+
prototype_approved: false,
|
|
27
|
+
},
|
|
28
|
+
refactor: {
|
|
29
|
+
evaluation_report: { status: "pending", file: null },
|
|
30
|
+
refactor_plan: { status: "pending", file: null },
|
|
31
|
+
refactor_execution: { status: "pending", file: null },
|
|
32
|
+
changelog: { status: "pending", file: null },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
last_updated: "2026-01-01T00:00:00.000Z",
|
|
36
|
+
updated_by: "test",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Captures stderr writes without calling the real process.stderr.write. */
|
|
41
|
+
function makeStderrCapture(): { messages: string[]; fn: (msg: string) => void } {
|
|
42
|
+
const messages: string[] = [];
|
|
43
|
+
return {
|
|
44
|
+
messages,
|
|
45
|
+
fn: (msg: string) => messages.push(msg),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
// Reset exitCode after each test so tests don't bleed into each other
|
|
51
|
+
process.exitCode = 0;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// No violation — no-op
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
describe("assertGuardrail – no violation", () => {
|
|
59
|
+
test("returns immediately without side-effects when violated is false", async () => {
|
|
60
|
+
const stderr = makeStderrCapture();
|
|
61
|
+
const state = makeState("relaxed");
|
|
62
|
+
|
|
63
|
+
await assertGuardrail(state, false, "should not appear", {
|
|
64
|
+
readLineFn: async () => null,
|
|
65
|
+
stderrWriteFn: stderr.fn,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(stderr.messages).toHaveLength(0);
|
|
69
|
+
expect(process.exitCode).toBeFalsy();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Strict mode (default)
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
describe("assertGuardrail – strict mode", () => {
|
|
78
|
+
test("US-003-AC03: strict mode throws the same violation message and never prompts", async () => {
|
|
79
|
+
const state = makeState("strict");
|
|
80
|
+
let promptCalled = false;
|
|
81
|
+
|
|
82
|
+
await expect(
|
|
83
|
+
assertGuardrail(state, true, "current_phase is 'prototype' but 'define' is required.", {
|
|
84
|
+
readLineFn: async () => {
|
|
85
|
+
promptCalled = true;
|
|
86
|
+
return "y";
|
|
87
|
+
},
|
|
88
|
+
stderrWriteFn: () => {},
|
|
89
|
+
}),
|
|
90
|
+
).rejects.toThrow("current_phase is 'prototype' but 'define' is required.");
|
|
91
|
+
expect(promptCalled).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("throws Error (not GuardrailAbortError) in strict mode", async () => {
|
|
95
|
+
const state = makeState("strict");
|
|
96
|
+
|
|
97
|
+
await expect(
|
|
98
|
+
assertGuardrail(state, true, "some violation", {
|
|
99
|
+
stderrWriteFn: () => {},
|
|
100
|
+
}),
|
|
101
|
+
).rejects.not.toBeInstanceOf(GuardrailAbortError);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("US-003-AC04: absent flow_guardrail defaults to strict hard-error behavior", async () => {
|
|
105
|
+
const state = makeState(undefined);
|
|
106
|
+
let promptCalled = false;
|
|
107
|
+
|
|
108
|
+
await expect(
|
|
109
|
+
assertGuardrail(state, true, "strict default violation", {
|
|
110
|
+
readLineFn: async () => {
|
|
111
|
+
promptCalled = true;
|
|
112
|
+
return "y";
|
|
113
|
+
},
|
|
114
|
+
stderrWriteFn: () => {},
|
|
115
|
+
}),
|
|
116
|
+
).rejects.toThrow("strict default violation");
|
|
117
|
+
expect(promptCalled).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("does not print warning to stderr in strict mode without force", async () => {
|
|
121
|
+
const stderr = makeStderrCapture();
|
|
122
|
+
const state = makeState("strict");
|
|
123
|
+
|
|
124
|
+
await assertGuardrail(state, true, "msg", {
|
|
125
|
+
stderrWriteFn: stderr.fn,
|
|
126
|
+
}).catch(() => {});
|
|
127
|
+
|
|
128
|
+
expect(stderr.messages).toHaveLength(0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Relaxed mode — US-001-AC01, AC02, AC06
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
describe("assertGuardrail – relaxed mode, warning and prompt", () => {
|
|
137
|
+
// US-001-AC01: warning to stderr
|
|
138
|
+
test("US-001-AC01: prints 'Warning: <message>' to stderr first", async () => {
|
|
139
|
+
const stderr = makeStderrCapture();
|
|
140
|
+
const state = makeState("relaxed");
|
|
141
|
+
|
|
142
|
+
await assertGuardrail(
|
|
143
|
+
state,
|
|
144
|
+
true,
|
|
145
|
+
"current_phase is 'prototype' but 'define' is required.",
|
|
146
|
+
{
|
|
147
|
+
readLineFn: async () => "y",
|
|
148
|
+
stderrWriteFn: stderr.fn,
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(stderr.messages[0]).toBe(
|
|
153
|
+
"Warning: current_phase is 'prototype' but 'define' is required.",
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// US-001-AC02: confirmation prompt after warning
|
|
158
|
+
test("US-001-AC02: prints 'Proceed anyway? [y/N]' immediately after the warning", async () => {
|
|
159
|
+
const stderr = makeStderrCapture();
|
|
160
|
+
const state = makeState("relaxed");
|
|
161
|
+
|
|
162
|
+
await assertGuardrail(state, true, "violation msg", {
|
|
163
|
+
readLineFn: async () => "y",
|
|
164
|
+
stderrWriteFn: stderr.fn,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(stderr.messages[1]).toBe("Proceed anyway? [y/N]");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// US-001-AC06: output goes to stderr (tested implicitly via stderrWriteFn being the only channel)
|
|
171
|
+
test("US-001-AC06: warning and prompt are sent to stderrWriteFn (stderr), not stdout", async () => {
|
|
172
|
+
const stderr = makeStderrCapture();
|
|
173
|
+
const state = makeState("relaxed");
|
|
174
|
+
|
|
175
|
+
await assertGuardrail(state, true, "msg", {
|
|
176
|
+
readLineFn: async () => "y",
|
|
177
|
+
stderrWriteFn: stderr.fn,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// stderrWriteFn was called with warning and prompt
|
|
181
|
+
expect(stderr.messages).toContain("Warning: msg");
|
|
182
|
+
expect(stderr.messages).toContain("Proceed anyway? [y/N]");
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Relaxed mode — confirmed (US-001-AC03)
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
describe("assertGuardrail – relaxed mode, confirmed", () => {
|
|
191
|
+
test("US-001-AC03: resolves without error when user enters 'y'", async () => {
|
|
192
|
+
const state = makeState("relaxed");
|
|
193
|
+
|
|
194
|
+
await expect(
|
|
195
|
+
assertGuardrail(state, true, "msg", {
|
|
196
|
+
readLineFn: async () => "y",
|
|
197
|
+
stderrWriteFn: () => {},
|
|
198
|
+
}),
|
|
199
|
+
).resolves.toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("US-001-AC03: resolves without error when user enters 'Y'", async () => {
|
|
203
|
+
const state = makeState("relaxed");
|
|
204
|
+
|
|
205
|
+
await expect(
|
|
206
|
+
assertGuardrail(state, true, "msg", {
|
|
207
|
+
readLineFn: async () => "Y",
|
|
208
|
+
stderrWriteFn: () => {},
|
|
209
|
+
}),
|
|
210
|
+
).resolves.toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("US-001-AC03: does not print 'Aborted.' when confirmed", async () => {
|
|
214
|
+
const stderr = makeStderrCapture();
|
|
215
|
+
const state = makeState("relaxed");
|
|
216
|
+
|
|
217
|
+
await assertGuardrail(state, true, "msg", {
|
|
218
|
+
readLineFn: async () => "y",
|
|
219
|
+
stderrWriteFn: stderr.fn,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(stderr.messages).not.toContain("Aborted.");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("US-001-AC03: does not set process.exitCode when confirmed", async () => {
|
|
226
|
+
const state = makeState("relaxed");
|
|
227
|
+
|
|
228
|
+
await assertGuardrail(state, true, "msg", {
|
|
229
|
+
readLineFn: async () => "y",
|
|
230
|
+
stderrWriteFn: () => {},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(process.exitCode).toBeFalsy();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Relaxed mode — aborted (US-001-AC04)
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
describe("assertGuardrail – relaxed mode, aborted", () => {
|
|
242
|
+
test("US-001-AC04: throws GuardrailAbortError when user enters 'n'", async () => {
|
|
243
|
+
const state = makeState("relaxed");
|
|
244
|
+
|
|
245
|
+
await expect(
|
|
246
|
+
assertGuardrail(state, true, "msg", {
|
|
247
|
+
readLineFn: async () => "n",
|
|
248
|
+
stderrWriteFn: () => {},
|
|
249
|
+
}),
|
|
250
|
+
).rejects.toBeInstanceOf(GuardrailAbortError);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("US-001-AC04: throws GuardrailAbortError when user presses Enter (empty input)", async () => {
|
|
254
|
+
const state = makeState("relaxed");
|
|
255
|
+
|
|
256
|
+
await expect(
|
|
257
|
+
assertGuardrail(state, true, "msg", {
|
|
258
|
+
readLineFn: async () => "",
|
|
259
|
+
stderrWriteFn: () => {},
|
|
260
|
+
}),
|
|
261
|
+
).rejects.toBeInstanceOf(GuardrailAbortError);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("US-001-AC04: throws GuardrailAbortError for any non-y/Y input", async () => {
|
|
265
|
+
const state = makeState("relaxed");
|
|
266
|
+
|
|
267
|
+
for (const input of ["no", "yes", "1", " ", "N"]) {
|
|
268
|
+
await expect(
|
|
269
|
+
assertGuardrail(state, true, "msg", {
|
|
270
|
+
readLineFn: async () => input,
|
|
271
|
+
stderrWriteFn: () => {},
|
|
272
|
+
}),
|
|
273
|
+
).rejects.toBeInstanceOf(GuardrailAbortError);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("US-001-AC04: prints 'Aborted.' to stderr when user does not confirm", async () => {
|
|
278
|
+
const stderr = makeStderrCapture();
|
|
279
|
+
const state = makeState("relaxed");
|
|
280
|
+
|
|
281
|
+
await assertGuardrail(state, true, "msg", {
|
|
282
|
+
readLineFn: async () => "n",
|
|
283
|
+
stderrWriteFn: stderr.fn,
|
|
284
|
+
}).catch(() => {});
|
|
285
|
+
|
|
286
|
+
expect(stderr.messages).toContain("Aborted.");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("US-001-AC04: sets process.exitCode to 1 when user does not confirm", async () => {
|
|
290
|
+
const state = makeState("relaxed");
|
|
291
|
+
|
|
292
|
+
await assertGuardrail(state, true, "msg", {
|
|
293
|
+
readLineFn: async () => "n",
|
|
294
|
+
stderrWriteFn: () => {},
|
|
295
|
+
}).catch(() => {});
|
|
296
|
+
|
|
297
|
+
expect(process.exitCode).toBe(1);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Relaxed mode — stdin closed / not a TTY (US-001-AC05)
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
describe("assertGuardrail – relaxed mode, stdin closed", () => {
|
|
306
|
+
test("US-001-AC05: throws GuardrailAbortError when readLineFn returns null (stdin closed)", async () => {
|
|
307
|
+
const state = makeState("relaxed");
|
|
308
|
+
|
|
309
|
+
await expect(
|
|
310
|
+
assertGuardrail(state, true, "msg", {
|
|
311
|
+
readLineFn: async () => null,
|
|
312
|
+
stderrWriteFn: () => {},
|
|
313
|
+
}),
|
|
314
|
+
).rejects.toBeInstanceOf(GuardrailAbortError);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("US-001-AC05: prints 'Aborted.' to stderr when stdin is closed", async () => {
|
|
318
|
+
const stderr = makeStderrCapture();
|
|
319
|
+
const state = makeState("relaxed");
|
|
320
|
+
|
|
321
|
+
await assertGuardrail(state, true, "msg", {
|
|
322
|
+
readLineFn: async () => null,
|
|
323
|
+
stderrWriteFn: stderr.fn,
|
|
324
|
+
}).catch(() => {});
|
|
325
|
+
|
|
326
|
+
expect(stderr.messages).toContain("Aborted.");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("US-001-AC05: sets process.exitCode to 1 when stdin is closed", async () => {
|
|
330
|
+
const state = makeState("relaxed");
|
|
331
|
+
|
|
332
|
+
await assertGuardrail(state, true, "msg", {
|
|
333
|
+
readLineFn: async () => null,
|
|
334
|
+
stderrWriteFn: () => {},
|
|
335
|
+
}).catch(() => {});
|
|
336
|
+
|
|
337
|
+
expect(process.exitCode).toBe(1);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("US-001-AC05: treats readLineFn throwing as closed stdin (abort)", async () => {
|
|
341
|
+
const state = makeState("relaxed");
|
|
342
|
+
|
|
343
|
+
await expect(
|
|
344
|
+
assertGuardrail(state, true, "msg", {
|
|
345
|
+
readLineFn: async () => {
|
|
346
|
+
throw new Error("stdin read error");
|
|
347
|
+
},
|
|
348
|
+
stderrWriteFn: () => {},
|
|
349
|
+
}),
|
|
350
|
+
).rejects.toBeInstanceOf(GuardrailAbortError);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// force option — skips prompt (partial US-002 surface, verified by AC05 wording)
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
describe("assertGuardrail – force option", () => {
|
|
359
|
+
test("with force=true and relaxed: prints warning but does not prompt or abort", async () => {
|
|
360
|
+
const stderr = makeStderrCapture();
|
|
361
|
+
const state = makeState("relaxed");
|
|
362
|
+
|
|
363
|
+
await assertGuardrail(state, true, "forced violation", {
|
|
364
|
+
force: true,
|
|
365
|
+
readLineFn: async () => {
|
|
366
|
+
throw new Error("should not be called");
|
|
367
|
+
},
|
|
368
|
+
stderrWriteFn: stderr.fn,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(stderr.messages).toContain("Warning: forced violation");
|
|
372
|
+
expect(stderr.messages).not.toContain("Proceed anyway? [y/N]");
|
|
373
|
+
expect(stderr.messages).not.toContain("Aborted.");
|
|
374
|
+
expect(process.exitCode).toBeFalsy();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("with force=true and strict: prints warning and continues without throwing", async () => {
|
|
378
|
+
const stderr = makeStderrCapture();
|
|
379
|
+
const state = makeState("strict");
|
|
380
|
+
|
|
381
|
+
await expect(
|
|
382
|
+
assertGuardrail(state, true, "forced strict violation", {
|
|
383
|
+
force: true,
|
|
384
|
+
stderrWriteFn: stderr.fn,
|
|
385
|
+
}),
|
|
386
|
+
).resolves.toBeUndefined();
|
|
387
|
+
|
|
388
|
+
expect(stderr.messages).toContain("Warning: forced strict violation");
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
// GuardrailAbortError identity
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
describe("GuardrailAbortError", () => {
|
|
397
|
+
test("is an instance of Error", () => {
|
|
398
|
+
const err = new GuardrailAbortError();
|
|
399
|
+
expect(err).toBeInstanceOf(Error);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("has message 'Aborted.'", () => {
|
|
403
|
+
const err = new GuardrailAbortError();
|
|
404
|
+
expect(err.message).toBe("Aborted.");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("has name 'GuardrailAbortError'", () => {
|
|
408
|
+
const err = new GuardrailAbortError();
|
|
409
|
+
expect(err.name).toBe("GuardrailAbortError");
|
|
410
|
+
});
|
|
411
|
+
});
|
package/src/guardrail.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { State } from "../scaffold/schemas/tmpl_state";
|
|
2
|
+
|
|
3
|
+
import { defaultReadLine } from "./readline";
|
|
4
|
+
|
|
5
|
+
export class GuardrailAbortError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("Aborted.");
|
|
8
|
+
this.name = "GuardrailAbortError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ReadLineFn = () => Promise<string | null>;
|
|
13
|
+
export type StderrWriteFn = (message: string) => void;
|
|
14
|
+
|
|
15
|
+
export interface GuardrailOptions {
|
|
16
|
+
force?: boolean;
|
|
17
|
+
readLineFn?: ReadLineFn;
|
|
18
|
+
stderrWriteFn?: StderrWriteFn;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function defaultStderrWrite(message: string): void {
|
|
22
|
+
process.stderr.write(`${message}\n`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Enforces the flow guardrail for phase/status violations.
|
|
27
|
+
*
|
|
28
|
+
* - strict (default): throws Error with `message` when `violated` is true and `force` is false
|
|
29
|
+
* - relaxed: prints warning + confirmation prompt to stderr; aborts (exitCode=1) unless user confirms
|
|
30
|
+
* - force: prints warning but skips the confirmation prompt regardless of guardrail mode
|
|
31
|
+
*
|
|
32
|
+
* When not violated, returns immediately without side effects.
|
|
33
|
+
*/
|
|
34
|
+
export async function assertGuardrail(
|
|
35
|
+
state: State,
|
|
36
|
+
violated: boolean,
|
|
37
|
+
message: string,
|
|
38
|
+
opts: GuardrailOptions = {},
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
if (!violated) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const guardrail = state.flow_guardrail ?? "strict";
|
|
45
|
+
const {
|
|
46
|
+
force = false,
|
|
47
|
+
readLineFn = defaultReadLine,
|
|
48
|
+
stderrWriteFn = defaultStderrWrite,
|
|
49
|
+
} = opts;
|
|
50
|
+
|
|
51
|
+
if (guardrail === "strict" && !force) {
|
|
52
|
+
throw new Error(message);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Print warning to stderr (AC01, AC06)
|
|
56
|
+
stderrWriteFn(`Warning: ${message}`);
|
|
57
|
+
|
|
58
|
+
if (force) {
|
|
59
|
+
// With --force: warn but skip the confirmation prompt (US-002)
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Relaxed mode without --force: prompt for confirmation (AC02, AC06)
|
|
64
|
+
stderrWriteFn("Proceed anyway? [y/N]");
|
|
65
|
+
|
|
66
|
+
let line: string | null;
|
|
67
|
+
try {
|
|
68
|
+
line = await readLineFn();
|
|
69
|
+
} catch {
|
|
70
|
+
line = null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Only "y" or "Y" is treated as confirmation (AC03, AC04, AC05)
|
|
74
|
+
if (line !== null && (line.trim() === "y" || line.trim() === "Y")) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// User aborted or stdin was closed (AC04, AC05)
|
|
79
|
+
stderrWriteFn("Aborted.");
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
throw new GuardrailAbortError();
|
|
82
|
+
}
|
package/src/install.test.ts
CHANGED
|
@@ -5,9 +5,11 @@ import { join } from "node:path";
|
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
|
|
7
7
|
const PROJECT_ROOT = join(import.meta.dir, "..");
|
|
8
|
-
const PACKAGE_VERSION =
|
|
8
|
+
const PACKAGE_VERSION =
|
|
9
|
+
(await Bun.file(join(PROJECT_ROOT, "package.json")).json()) as { version?: string };
|
|
10
|
+
const PACKAGE_VERSION_STR = PACKAGE_VERSION?.version ?? "0.1.0";
|
|
9
11
|
// Scoped packages: npm pack produces scope-package-version.tgz
|
|
10
|
-
const TARBALL_BASENAME = `quinteroac-agents-coding-toolkit-${
|
|
12
|
+
const TARBALL_BASENAME = `quinteroac-agents-coding-toolkit-${PACKAGE_VERSION_STR}.tgz`;
|
|
11
13
|
const TARBALL_PATH = join(PROJECT_ROOT, TARBALL_BASENAME);
|
|
12
14
|
|
|
13
15
|
const tempProjectRoots: string[] = [];
|
|
@@ -70,7 +72,7 @@ describe("install package", () => {
|
|
|
70
72
|
encoding: "utf-8",
|
|
71
73
|
});
|
|
72
74
|
expect(result.status).toBe(0);
|
|
73
|
-
expect(result.stdout?.trim()).toBe(
|
|
75
|
+
expect(result.stdout?.trim()).toBe(PACKAGE_VERSION_STR);
|
|
74
76
|
});
|
|
75
77
|
|
|
76
78
|
test("US-002-AC01: user can install the package from the local file system or registry", async () => {
|
|
@@ -86,7 +88,7 @@ describe("install package", () => {
|
|
|
86
88
|
|
|
87
89
|
const pkgJsonPath = join(nodeModules, "package.json");
|
|
88
90
|
const pkg = (await Bun.file(pkgJsonPath).json()) as { version?: string; bin?: Record<string, string> };
|
|
89
|
-
expect(pkg.version).toBe(
|
|
91
|
+
expect(pkg.version).toBe(PACKAGE_VERSION_STR);
|
|
90
92
|
expect(pkg.bin).toBeDefined();
|
|
91
93
|
expect(pkg.bin?.nvst).toBeDefined();
|
|
92
94
|
});
|
|
@@ -119,6 +121,6 @@ describe("install package", () => {
|
|
|
119
121
|
|
|
120
122
|
const { exitCode, stdout } = runNvst(tempRoot, ["--version"]);
|
|
121
123
|
expect(exitCode).toBe(0);
|
|
122
|
-
expect(stdout.trim()).toBe(
|
|
124
|
+
expect(stdout.trim()).toBe(PACKAGE_VERSION_STR);
|
|
123
125
|
});
|
|
124
126
|
});
|
package/src/pack.test.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { join } from "node:path";
|
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
|
|
6
6
|
const PROJECT_ROOT = join(import.meta.dir, "..");
|
|
7
|
-
const
|
|
7
|
+
const pkg = (await Bun.file(join(PROJECT_ROOT, "package.json")).json()) as { version?: string };
|
|
8
|
+
const PACKAGE_VERSION = pkg?.version ?? "0.1.0";
|
|
8
9
|
// Scoped packages: npm pack produces scope-package-version.tgz
|
|
9
10
|
const TARBALL_BASENAME = `quinteroac-agents-coding-toolkit-${PACKAGE_VERSION}.tgz`;
|
|
10
11
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for ID matching and progress entry state updates.
|
|
3
|
+
*
|
|
4
|
+
* Centralises logic used across create-prototype, execute-test-plan, and
|
|
5
|
+
* execute-refactor so that matching rules and timestamp semantics stay
|
|
6
|
+
* consistent.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function sortedValues(values: string[]): string[] {
|
|
10
|
+
return [...values].sort((a, b) => a.localeCompare(b));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function idsMatchExactly(left: string[], right: string[]): boolean {
|
|
14
|
+
if (left.length !== right.length) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
19
|
+
if (left[i] !== right[i]) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function applyStatusUpdate<S extends string>(
|
|
28
|
+
entry: { status: S; updated_at: string },
|
|
29
|
+
status: S,
|
|
30
|
+
timestamp: string,
|
|
31
|
+
): void {
|
|
32
|
+
entry.status = status;
|
|
33
|
+
entry.updated_at = timestamp;
|
|
34
|
+
}
|
package/src/readline.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
|
|
3
|
+
export async function defaultReadLine(): Promise<string | null> {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
const rl = createInterface({
|
|
6
|
+
input: process.stdin,
|
|
7
|
+
terminal: false,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
let settled = false;
|
|
11
|
+
|
|
12
|
+
const settle = (value: string | null): void => {
|
|
13
|
+
if (!settled) {
|
|
14
|
+
settled = true;
|
|
15
|
+
rl.close();
|
|
16
|
+
resolve(value);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
rl.once("line", settle);
|
|
21
|
+
rl.once("close", () => settle(null));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import type { ZodSchema } from "zod";
|
|
4
|
+
|
|
5
|
+
export type WriteJsonArtifactFn = (
|
|
6
|
+
absolutePath: string,
|
|
7
|
+
schema: ZodSchema,
|
|
8
|
+
data: unknown,
|
|
9
|
+
) => Promise<void>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Schema-validated JSON artifact writer.
|
|
13
|
+
*
|
|
14
|
+
* Validates `data` against `schema` then writes pretty-printed JSON to
|
|
15
|
+
* `absolutePath`, creating parent directories as needed. This is the
|
|
16
|
+
* in-process equivalent of `nvst write-json` for commands that need to
|
|
17
|
+
* write iteration-scoped `.agents/flow/` artifacts without spawning a
|
|
18
|
+
* subprocess.
|
|
19
|
+
*/
|
|
20
|
+
export async function writeJsonArtifact(
|
|
21
|
+
absolutePath: string,
|
|
22
|
+
schema: ZodSchema,
|
|
23
|
+
data: unknown,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const result = schema.safeParse(data);
|
|
26
|
+
if (!result.success) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`writeJsonArtifact: schema validation failed for ${absolutePath}.\n${JSON.stringify(result.error.format(), null, 2)}`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
32
|
+
await writeFile(absolutePath, `${JSON.stringify(result.data, null, 2)}\n`, "utf-8");
|
|
33
|
+
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# Flow directory (scaffold)
|
|
2
|
-
|
|
3
|
-
<!-- TODO: Complete. All content in English. -->
|
|
4
|
-
|
|
5
|
-
- **What lives here:** TBD — Iteration artifacts `it_<6 digits>_*` (e.g. product-requirement-document.md, PRD.json, progress.json, test-plan.md, evaluation-report.md, refactor_plan.md).
|
|
6
|
-
- **Naming:** Prefix `it_` + 6-digit iteration number (e.g. `it_000001_`, `it_000042_`). No spaces.
|
|
7
|
-
- **What gets archived:** When starting the next iteration, current iteration files move to `.agents/flow/archived/<iteration>/` (e.g. `archived/000001/`).
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# Iteration close checklist (scaffold)
|
|
2
|
-
|
|
3
|
-
<!-- TODO: Complete. All content in English. Use when closing an iteration (before running start next iteration / archive). -->
|
|
4
|
-
|
|
5
|
-
- [ ] **prototype_approved** — Prototype has been approved (`phases.prototype.prototype_approved === true`).
|
|
6
|
-
- [ ] **Full tests** — Tests for all use cases of the iteration (original + refactor/regression) have been run and pass (or documented exceptions).
|
|
7
|
-
- [ ] **PROJECT_CONTEXT.md** — Updated with newly implemented features; summary mechanism applied so file does not exceed 250 lines.
|
|
8
|
-
- [ ] **CHANGELOG.md** — Iteration summary recorded (requirements, refactorings, fixes).
|
|
9
|
-
- [ ] **Archive** — Ready to run “start iteration” (move `it_<iteration>_*` to `.agents/flow/archived/<iteration>/`, update state.history, reset phase for next iteration).
|
|
10
|
-
|
|
11
|
-
TBD — Add any project-specific items.
|
package/schemas/test-plan.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
const TestStatusSchema = z.enum(["pending", "passed", "failed", "skipped"]);
|
|
4
|
-
|
|
5
|
-
const TestItemSchema = z.object({
|
|
6
|
-
id: z.string(),
|
|
7
|
-
description: z.string(),
|
|
8
|
-
status: TestStatusSchema,
|
|
9
|
-
correlatedRequirements: z.array(z.string()),
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export const TestPlanSchema = z.object({
|
|
13
|
-
overallStatus: TestStatusSchema,
|
|
14
|
-
scope: z.array(z.string()),
|
|
15
|
-
environmentData: z.array(z.string()),
|
|
16
|
-
automatedTests: z.array(TestItemSchema),
|
|
17
|
-
exploratoryManualTests: z.array(TestItemSchema),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
export type TestPlan = z.infer<typeof TestPlanSchema>;
|