@quinteroac/agents-coding-toolkit 0.1.1-preview.0 → 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 +2 -1
- package/scaffold/.agents/flow/tmpl_it_000001_progress.example.json +20 -0
- package/scaffold/.agents/skills/execute-refactor-item/tmpl_SKILL.md +5 -5
- package/scaffold/schemas/tmpl_prototype-progress.ts +22 -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/test-execution-progress.ts +17 -0
- package/schemas/validate-progress.ts +1 -1
- package/schemas/validate-state.ts +1 -1
- package/src/cli.ts +51 -6
- package/src/commands/approve-prototype.test.ts +427 -0
- package/src/commands/approve-prototype.ts +185 -0
- package/src/commands/create-prototype.test.ts +459 -7
- package/src/commands/create-prototype.ts +168 -56
- 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 +3 -3
- package/src/commands/execute-refactor.ts +8 -12
- package/src/commands/execute-test-plan.test.ts +20 -19
- package/src/commands/execute-test-plan.ts +19 -52
- 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/start-iteration.test.ts +52 -0
- package/src/commands/start-iteration.ts +5 -0
- package/src/flow-cli.test.ts +18 -0
- package/src/guardrail.ts +2 -24
- package/src/progress-utils.ts +34 -0
- package/src/readline.ts +23 -0
- package/src/write-json-artifact.ts +33 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import type { AgentProvider } from "../agent";
|
|
2
|
+
import { parseProvider } from "../agent";
|
|
3
|
+
import { runCreateProjectContext } from "./create-project-context";
|
|
4
|
+
import { runCreatePrototype } from "./create-prototype";
|
|
5
|
+
import { runCreateTestPlan } from "./create-test-plan";
|
|
6
|
+
import { runDefineRefactorPlan } from "./define-refactor-plan";
|
|
7
|
+
import { runDefineRequirement } from "./define-requirement";
|
|
8
|
+
import { runExecuteRefactor } from "./execute-refactor";
|
|
9
|
+
import { runExecuteTestPlan } from "./execute-test-plan";
|
|
10
|
+
import { GuardrailAbortError } from "../guardrail";
|
|
11
|
+
import { defaultReadLine } from "../readline";
|
|
12
|
+
import { readState } from "../state";
|
|
13
|
+
import type { State } from "../../scaffold/schemas/tmpl_state";
|
|
14
|
+
import {
|
|
15
|
+
buildApprovalGateMessage,
|
|
16
|
+
FLOW_APPROVAL_TARGETS,
|
|
17
|
+
type FlowStep,
|
|
18
|
+
FLOW_STEPS,
|
|
19
|
+
type FlowHandlerKey,
|
|
20
|
+
} from "./flow-config";
|
|
21
|
+
|
|
22
|
+
export interface FlowOptions {
|
|
23
|
+
provider?: AgentProvider;
|
|
24
|
+
force?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type FlowDecision =
|
|
28
|
+
| { kind: "step"; step: FlowStep }
|
|
29
|
+
| { kind: "approval_gate"; message: string }
|
|
30
|
+
| { kind: "complete"; message: string }
|
|
31
|
+
| { kind: "blocked"; message: string };
|
|
32
|
+
|
|
33
|
+
interface FlowDeps {
|
|
34
|
+
readLineFn: () => Promise<string | null>;
|
|
35
|
+
readStateFn: (projectRoot: string) => Promise<State>;
|
|
36
|
+
runCreateProjectContextFn: typeof runCreateProjectContext;
|
|
37
|
+
runCreatePrototypeFn: typeof runCreatePrototype;
|
|
38
|
+
runCreateTestPlanFn: typeof runCreateTestPlan;
|
|
39
|
+
runDefineRefactorPlanFn: typeof runDefineRefactorPlan;
|
|
40
|
+
runDefineRequirementFn: typeof runDefineRequirement;
|
|
41
|
+
runExecuteRefactorFn: typeof runExecuteRefactor;
|
|
42
|
+
runExecuteTestPlanFn: typeof runExecuteTestPlan;
|
|
43
|
+
stderrWriteFn: (message: string) => void;
|
|
44
|
+
stdoutWriteFn: (message: string) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const defaultDeps: FlowDeps = {
|
|
48
|
+
readLineFn: defaultReadLine,
|
|
49
|
+
readStateFn: readState,
|
|
50
|
+
runCreateProjectContextFn: runCreateProjectContext,
|
|
51
|
+
runCreatePrototypeFn: runCreatePrototype,
|
|
52
|
+
runCreateTestPlanFn: runCreateTestPlan,
|
|
53
|
+
runDefineRefactorPlanFn: runDefineRefactorPlan,
|
|
54
|
+
runDefineRequirementFn: runDefineRequirement,
|
|
55
|
+
runExecuteRefactorFn: runExecuteRefactor,
|
|
56
|
+
runExecuteTestPlanFn: runExecuteTestPlan,
|
|
57
|
+
stderrWriteFn: (message: string) => process.stderr.write(`${message}\n`),
|
|
58
|
+
stdoutWriteFn: (message: string) => process.stdout.write(`${message}\n`),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Status semantics in flow orchestration:
|
|
62
|
+
// - "in_progress" may represent either a resumable execution step or an approval wait state.
|
|
63
|
+
// - For requirement_definition, "in_progress" means content exists and approval is required.
|
|
64
|
+
// - For build/execution steps, "in_progress" means work was interrupted/partial and should be resumed.
|
|
65
|
+
function isResumableInProgressStatus(status: string): boolean {
|
|
66
|
+
return status === "in_progress";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isApprovalGateInProgressStatus(status: string): boolean {
|
|
70
|
+
return status === "in_progress";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isRunnablePendingOrResumable(status: string): boolean {
|
|
74
|
+
return status === "pending" || isResumableInProgressStatus(status);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildIterationCompleteMessage(iteration: string): string {
|
|
78
|
+
return `Iteration ${iteration} complete. All phases finished.`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildUnsupportedStatusMessage(path: string, status: string): string {
|
|
82
|
+
return `Unsupported status '${status}' at phases.${path}.`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildNoRunnableStepMessage(path: string): string {
|
|
86
|
+
return `No runnable flow step found for phases.${path}.`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveDefinePhaseDecision(state: State): FlowDecision | null {
|
|
90
|
+
const define = state.phases.define;
|
|
91
|
+
|
|
92
|
+
for (const key of Object.keys(define) as Array<keyof typeof define>) {
|
|
93
|
+
if (key === "requirement_definition") {
|
|
94
|
+
const status = define.requirement_definition.status;
|
|
95
|
+
if (status === "pending") {
|
|
96
|
+
return { kind: "step", step: FLOW_STEPS["define-requirement"] };
|
|
97
|
+
}
|
|
98
|
+
if (isApprovalGateInProgressStatus(status)) {
|
|
99
|
+
return {
|
|
100
|
+
kind: "approval_gate",
|
|
101
|
+
message: buildApprovalGateMessage(FLOW_APPROVAL_TARGETS.requirement),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (status !== "approved") {
|
|
105
|
+
return {
|
|
106
|
+
kind: "blocked",
|
|
107
|
+
message: buildUnsupportedStatusMessage("define.requirement_definition", status),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const status = define.prd_generation.status;
|
|
114
|
+
if (status === "completed") {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (status === "pending") {
|
|
118
|
+
return {
|
|
119
|
+
kind: "blocked",
|
|
120
|
+
message: buildNoRunnableStepMessage("define.prd_generation"),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
kind: "blocked",
|
|
125
|
+
message: buildUnsupportedStatusMessage("define.prd_generation", status),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolvePrototypePhaseDecision(state: State): FlowDecision | null {
|
|
133
|
+
const prototype = state.phases.prototype;
|
|
134
|
+
|
|
135
|
+
for (const key of Object.keys(prototype) as Array<keyof typeof prototype>) {
|
|
136
|
+
if (key === "project_context") {
|
|
137
|
+
const status = prototype.project_context.status;
|
|
138
|
+
if (status === "pending") {
|
|
139
|
+
return { kind: "step", step: FLOW_STEPS["create-project-context"] };
|
|
140
|
+
}
|
|
141
|
+
if (status === "pending_approval") {
|
|
142
|
+
return {
|
|
143
|
+
kind: "approval_gate",
|
|
144
|
+
message: buildApprovalGateMessage(FLOW_APPROVAL_TARGETS.projectContext),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (status !== "created") {
|
|
148
|
+
return {
|
|
149
|
+
kind: "blocked",
|
|
150
|
+
message: buildUnsupportedStatusMessage("prototype.project_context", status),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (key === "test_plan") {
|
|
157
|
+
const status = prototype.test_plan.status;
|
|
158
|
+
if (status === "pending") {
|
|
159
|
+
return { kind: "step", step: FLOW_STEPS["create-test-plan"] };
|
|
160
|
+
}
|
|
161
|
+
if (status === "pending_approval") {
|
|
162
|
+
return {
|
|
163
|
+
kind: "approval_gate",
|
|
164
|
+
message: buildApprovalGateMessage(FLOW_APPROVAL_TARGETS.testPlan),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (status !== "created") {
|
|
168
|
+
return {
|
|
169
|
+
kind: "blocked",
|
|
170
|
+
message: buildUnsupportedStatusMessage("prototype.test_plan", status),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (key === "tp_generation") {
|
|
177
|
+
const status = prototype.tp_generation.status;
|
|
178
|
+
if (status === "created") {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (status === "pending") {
|
|
182
|
+
return {
|
|
183
|
+
kind: "blocked",
|
|
184
|
+
message: buildNoRunnableStepMessage("prototype.tp_generation"),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
kind: "blocked",
|
|
189
|
+
message: buildUnsupportedStatusMessage("prototype.tp_generation", status),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (key === "prototype_build") {
|
|
194
|
+
const status = prototype.prototype_build.status;
|
|
195
|
+
if (isRunnablePendingOrResumable(status)) {
|
|
196
|
+
return { kind: "step", step: FLOW_STEPS["create-prototype"] };
|
|
197
|
+
}
|
|
198
|
+
if (status !== "created") {
|
|
199
|
+
return {
|
|
200
|
+
kind: "blocked",
|
|
201
|
+
message: buildUnsupportedStatusMessage("prototype.prototype_build", status),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (key === "test_execution") {
|
|
208
|
+
const status = prototype.test_execution.status;
|
|
209
|
+
if (status === "pending" || status === "in_progress" || status === "failed") {
|
|
210
|
+
return { kind: "step", step: FLOW_STEPS["execute-test-plan"] };
|
|
211
|
+
}
|
|
212
|
+
if (status !== "completed") {
|
|
213
|
+
return {
|
|
214
|
+
kind: "blocked",
|
|
215
|
+
message: buildUnsupportedStatusMessage("prototype.test_execution", status),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (key === "prototype_approved") {
|
|
222
|
+
if (!prototype.prototype_approved) {
|
|
223
|
+
return {
|
|
224
|
+
kind: "approval_gate",
|
|
225
|
+
message: buildApprovalGateMessage(FLOW_APPROVAL_TARGETS.prototype),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function resolveRefactorPhaseDecision(state: State): FlowDecision | null {
|
|
235
|
+
const refactor = state.phases.refactor;
|
|
236
|
+
|
|
237
|
+
for (const key of Object.keys(refactor) as Array<keyof typeof refactor>) {
|
|
238
|
+
if (key === "evaluation_report") {
|
|
239
|
+
const status = refactor.evaluation_report.status;
|
|
240
|
+
if (status === "created") {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (status === "pending") {
|
|
244
|
+
if (refactor.refactor_plan.status === "pending") {
|
|
245
|
+
return { kind: "step", step: FLOW_STEPS["define-refactor-plan"] };
|
|
246
|
+
}
|
|
247
|
+
if (refactor.refactor_plan.status === "pending_approval") {
|
|
248
|
+
return {
|
|
249
|
+
kind: "approval_gate",
|
|
250
|
+
message: buildApprovalGateMessage(FLOW_APPROVAL_TARGETS.refactorPlan),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
kind: "blocked",
|
|
255
|
+
message: buildNoRunnableStepMessage("refactor.evaluation_report"),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
kind: "blocked",
|
|
260
|
+
message: buildUnsupportedStatusMessage("refactor.evaluation_report", status),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (key === "refactor_plan") {
|
|
265
|
+
const status = refactor.refactor_plan.status;
|
|
266
|
+
if (status === "pending") {
|
|
267
|
+
return { kind: "step", step: FLOW_STEPS["define-refactor-plan"] };
|
|
268
|
+
}
|
|
269
|
+
if (status === "pending_approval") {
|
|
270
|
+
return {
|
|
271
|
+
kind: "approval_gate",
|
|
272
|
+
message: buildApprovalGateMessage(FLOW_APPROVAL_TARGETS.refactorPlan),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (status !== "approved") {
|
|
276
|
+
return {
|
|
277
|
+
kind: "blocked",
|
|
278
|
+
message: buildUnsupportedStatusMessage("refactor.refactor_plan", status),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (key === "refactor_execution") {
|
|
285
|
+
const status = refactor.refactor_execution.status;
|
|
286
|
+
if (isRunnablePendingOrResumable(status)) {
|
|
287
|
+
return { kind: "step", step: FLOW_STEPS["execute-refactor"] };
|
|
288
|
+
}
|
|
289
|
+
if (status !== "completed") {
|
|
290
|
+
return {
|
|
291
|
+
kind: "blocked",
|
|
292
|
+
message: buildUnsupportedStatusMessage("refactor.refactor_execution", status),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (key === "changelog") {
|
|
299
|
+
const status = refactor.changelog.status;
|
|
300
|
+
if (status === "pending" || status === "created") {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
kind: "blocked",
|
|
305
|
+
message: buildUnsupportedStatusMessage("refactor.changelog", status),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function detectNextFlowDecision(state: State): FlowDecision {
|
|
314
|
+
for (const phase of Object.keys(state.phases) as Array<keyof State["phases"]>) {
|
|
315
|
+
const decision = phase === "define"
|
|
316
|
+
? resolveDefinePhaseDecision(state)
|
|
317
|
+
: phase === "prototype"
|
|
318
|
+
? resolvePrototypePhaseDecision(state)
|
|
319
|
+
: resolveRefactorPhaseDecision(state);
|
|
320
|
+
|
|
321
|
+
if (decision !== null) {
|
|
322
|
+
return decision;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { kind: "complete", message: buildIterationCompleteMessage(state.current_iteration) };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function ensureProvider(
|
|
330
|
+
provider: AgentProvider | undefined,
|
|
331
|
+
deps: FlowDeps,
|
|
332
|
+
): Promise<AgentProvider> {
|
|
333
|
+
if (provider) {
|
|
334
|
+
return provider;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
deps.stdoutWriteFn("Enter agent provider:");
|
|
338
|
+
|
|
339
|
+
const value = await deps.readLineFn();
|
|
340
|
+
if (value === null) {
|
|
341
|
+
throw new Error("Missing agent provider from stdin.");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return parseProvider(value.trim());
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function runFlow(
|
|
348
|
+
opts: FlowOptions = {},
|
|
349
|
+
deps: Partial<FlowDeps> = {},
|
|
350
|
+
): Promise<void> {
|
|
351
|
+
const mergedDeps: FlowDeps = { ...defaultDeps, ...deps };
|
|
352
|
+
const projectRoot = process.cwd();
|
|
353
|
+
let provider = opts.provider;
|
|
354
|
+
const force = opts.force ?? false;
|
|
355
|
+
|
|
356
|
+
while (true) {
|
|
357
|
+
const state = await mergedDeps.readStateFn(projectRoot);
|
|
358
|
+
const decision = detectNextFlowDecision(state);
|
|
359
|
+
|
|
360
|
+
if (decision.kind === "complete") {
|
|
361
|
+
mergedDeps.stdoutWriteFn(decision.message);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (decision.kind === "approval_gate") {
|
|
366
|
+
mergedDeps.stdoutWriteFn(decision.message);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (decision.kind === "blocked") {
|
|
371
|
+
mergedDeps.stderrWriteFn(decision.message);
|
|
372
|
+
process.exitCode = 1;
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const { step } = decision;
|
|
377
|
+
try {
|
|
378
|
+
if (step.requiresAgent) {
|
|
379
|
+
provider = await ensureProvider(provider, mergedDeps);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
mergedDeps.stdoutWriteFn(`Running: bun nvst ${step.label}`);
|
|
383
|
+
const handlers: Record<FlowHandlerKey, () => Promise<void>> = {
|
|
384
|
+
runDefineRequirementFn: () => mergedDeps.runDefineRequirementFn({ provider: provider!, force }),
|
|
385
|
+
runCreateProjectContextFn: () => mergedDeps.runCreateProjectContextFn({ provider: provider!, mode: "strict", force }),
|
|
386
|
+
runCreatePrototypeFn: () => mergedDeps.runCreatePrototypeFn({ provider: provider!, force }),
|
|
387
|
+
runCreateTestPlanFn: () => mergedDeps.runCreateTestPlanFn({ provider: provider!, force }),
|
|
388
|
+
runExecuteTestPlanFn: () => mergedDeps.runExecuteTestPlanFn({ provider: provider!, force }),
|
|
389
|
+
runDefineRefactorPlanFn: () => mergedDeps.runDefineRefactorPlanFn({ provider: provider!, force }),
|
|
390
|
+
runExecuteRefactorFn: () => mergedDeps.runExecuteRefactorFn({ provider: provider!, force }),
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
await handlers[step.handlerKey]();
|
|
394
|
+
continue;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error instanceof GuardrailAbortError) {
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
400
|
+
mergedDeps.stderrWriteFn(message);
|
|
401
|
+
process.exitCode = 1;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -89,6 +89,58 @@ describe("start-iteration command", () => {
|
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
test("preserves flow_guardrail when starting a new iteration", async () => {
|
|
93
|
+
const projectRoot = await createProjectRoot();
|
|
94
|
+
createdRoots.push(projectRoot);
|
|
95
|
+
const flowDir = join(projectRoot, ".agents", "flow");
|
|
96
|
+
await mkdir(flowDir, { recursive: true });
|
|
97
|
+
|
|
98
|
+
await writeFile(
|
|
99
|
+
join(projectRoot, ".agents", "state.json"),
|
|
100
|
+
JSON.stringify(
|
|
101
|
+
{
|
|
102
|
+
current_iteration: "000003",
|
|
103
|
+
current_phase: "define",
|
|
104
|
+
flow_guardrail: "relaxed",
|
|
105
|
+
phases: {
|
|
106
|
+
define: {
|
|
107
|
+
requirement_definition: { status: "pending", file: null },
|
|
108
|
+
prd_generation: { status: "pending", file: null },
|
|
109
|
+
},
|
|
110
|
+
prototype: {
|
|
111
|
+
project_context: { status: "pending", file: null },
|
|
112
|
+
test_plan: { status: "pending", file: null },
|
|
113
|
+
tp_generation: { status: "pending", file: null },
|
|
114
|
+
prototype_build: { status: "pending", file: null },
|
|
115
|
+
test_execution: { status: "pending", file: null },
|
|
116
|
+
prototype_approved: false,
|
|
117
|
+
},
|
|
118
|
+
refactor: {
|
|
119
|
+
evaluation_report: { status: "pending", file: null },
|
|
120
|
+
refactor_plan: { status: "pending", file: null },
|
|
121
|
+
refactor_execution: { status: "pending", file: null },
|
|
122
|
+
changelog: { status: "pending", file: null },
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
last_updated: "2026-02-22T20:00:00.000Z",
|
|
126
|
+
history: [],
|
|
127
|
+
},
|
|
128
|
+
null,
|
|
129
|
+
2,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
await writeFile(join(flowDir, "it_000003_PRD.json"), "{}");
|
|
134
|
+
|
|
135
|
+
await withCwd(projectRoot, async () => {
|
|
136
|
+
await runStartIteration();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const state = await readState(projectRoot);
|
|
140
|
+
expect(state.current_iteration).toBe("000004");
|
|
141
|
+
expect(state.flow_guardrail).toBe("relaxed");
|
|
142
|
+
});
|
|
143
|
+
|
|
92
144
|
test("does not preserve project_context when it was pending", async () => {
|
|
93
145
|
const projectRoot = await createProjectRoot();
|
|
94
146
|
createdRoots.push(projectRoot);
|
|
@@ -92,6 +92,11 @@ export async function runStartIteration(): Promise<void> {
|
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Preserve flow_guardrail so user configuration is not lost when starting an iteration
|
|
96
|
+
if (parsedState.flow_guardrail !== undefined) {
|
|
97
|
+
nextState.flow_guardrail = parsedState.flow_guardrail;
|
|
98
|
+
}
|
|
99
|
+
|
|
95
100
|
await writeState(projectRoot, nextState);
|
|
96
101
|
|
|
97
102
|
console.log(
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
describe("US-001: flow CLI routing and usage", () => {
|
|
6
|
+
test("cli.ts routes `flow` command to runFlow handler", async () => {
|
|
7
|
+
const source = await readFile(join(import.meta.dir, "cli.ts"), "utf8");
|
|
8
|
+
expect(source).toContain('import { runFlow } from "./commands/flow"');
|
|
9
|
+
expect(source).toContain('if (command === "flow")');
|
|
10
|
+
expect(source).toContain("await runFlow({ provider, force })");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("printUsage includes flow command help text", async () => {
|
|
14
|
+
const source = await readFile(join(import.meta.dir, "cli.ts"), "utf8");
|
|
15
|
+
expect(source).toContain("flow [--agent <provider>] [--force]");
|
|
16
|
+
expect(source).toContain("Run the next pending flow step(s) until an approval gate or completion");
|
|
17
|
+
});
|
|
18
|
+
});
|
package/src/guardrail.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createInterface } from "node:readline";
|
|
2
|
-
|
|
3
1
|
import type { State } from "../scaffold/schemas/tmpl_state";
|
|
4
2
|
|
|
3
|
+
import { defaultReadLine } from "./readline";
|
|
4
|
+
|
|
5
5
|
export class GuardrailAbortError extends Error {
|
|
6
6
|
constructor() {
|
|
7
7
|
super("Aborted.");
|
|
@@ -18,28 +18,6 @@ export interface GuardrailOptions {
|
|
|
18
18
|
stderrWriteFn?: StderrWriteFn;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
async function defaultReadLine(): Promise<string | null> {
|
|
22
|
-
return new Promise((resolve) => {
|
|
23
|
-
const rl = createInterface({
|
|
24
|
-
input: process.stdin,
|
|
25
|
-
terminal: false,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
let settled = false;
|
|
29
|
-
|
|
30
|
-
const settle = (value: string | null): void => {
|
|
31
|
-
if (!settled) {
|
|
32
|
-
settled = true;
|
|
33
|
-
rl.close();
|
|
34
|
-
resolve(value);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
rl.once("line", settle);
|
|
39
|
-
rl.once("close", () => settle(null));
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
21
|
function defaultStderrWrite(message: string): void {
|
|
44
22
|
process.stderr.write(`${message}\n`);
|
|
45
23
|
}
|
|
@@ -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
|
+
}
|