@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
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
import { readFile
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { $ as dollar } from "bun";
|
|
4
|
-
import { z } from "zod";
|
|
5
4
|
|
|
6
5
|
import { PrdSchema } from "../../scaffold/schemas/tmpl_prd";
|
|
6
|
+
import {
|
|
7
|
+
PrototypeProgressSchema,
|
|
8
|
+
type PrototypeProgress,
|
|
9
|
+
} from "../../scaffold/schemas/tmpl_prototype-progress";
|
|
7
10
|
import {
|
|
8
11
|
buildPrompt,
|
|
9
12
|
invokeAgent,
|
|
10
13
|
loadSkill,
|
|
14
|
+
type AgentInvokeOptions,
|
|
11
15
|
type AgentProvider,
|
|
16
|
+
type AgentResult,
|
|
12
17
|
} from "../agent";
|
|
13
18
|
import { assertGuardrail } from "../guardrail";
|
|
19
|
+
import { defaultReadLine } from "../readline";
|
|
20
|
+
import { idsMatchExactly, sortedValues } from "../progress-utils";
|
|
14
21
|
import { exists, FLOW_REL_DIR, readState, writeState } from "../state";
|
|
22
|
+
import { writeJsonArtifact, type WriteJsonArtifactFn } from "../write-json-artifact";
|
|
15
23
|
|
|
16
24
|
export interface CreatePrototypeOptions {
|
|
17
25
|
provider: AgentProvider;
|
|
@@ -21,41 +29,114 @@ export interface CreatePrototypeOptions {
|
|
|
21
29
|
force?: boolean;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
const DECLINE_DIRTY_TREE_ABORT_MESSAGE = "Aborted. Commit or discard your changes and re-run `bun nvst create prototype`.";
|
|
33
|
+
const DIRTY_TREE_COMMIT_PROMPT = "Working tree has uncommitted changes. Stage and commit them now to proceed? [y/N]";
|
|
34
|
+
|
|
35
|
+
interface CreatePrototypeDeps {
|
|
36
|
+
invokeAgentFn: (options: AgentInvokeOptions) => Promise<AgentResult>;
|
|
37
|
+
loadSkillFn: (projectRoot: string, skillName: string) => Promise<string>;
|
|
38
|
+
checkGhAvailableFn: (projectRoot: string) => Promise<boolean>;
|
|
39
|
+
createPullRequestFn: (
|
|
40
|
+
projectRoot: string,
|
|
41
|
+
title: string,
|
|
42
|
+
body: string,
|
|
43
|
+
) => Promise<{ exitCode: number; stderr: string }>;
|
|
44
|
+
logFn: (message: string) => void;
|
|
45
|
+
warnFn: (message: string) => void;
|
|
46
|
+
promptDirtyTreeCommitFn: (question: string) => Promise<boolean>;
|
|
47
|
+
gitAddAndCommitFn: (projectRoot: string, commitMessage: string) => Promise<void>;
|
|
48
|
+
writeJsonArtifactFn: WriteJsonArtifactFn;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const defaultDeps: CreatePrototypeDeps = {
|
|
52
|
+
invokeAgentFn: invokeAgent,
|
|
53
|
+
loadSkillFn: loadSkill,
|
|
54
|
+
checkGhAvailableFn: async (projectRoot) => {
|
|
55
|
+
const proc = Bun.spawn(["gh", "--version"], {
|
|
56
|
+
cwd: projectRoot,
|
|
57
|
+
stdout: "ignore",
|
|
58
|
+
stderr: "ignore",
|
|
59
|
+
});
|
|
60
|
+
return (await proc.exited) === 0;
|
|
61
|
+
},
|
|
62
|
+
createPullRequestFn: async (projectRoot, title, body) => {
|
|
63
|
+
const result = await dollar`gh pr create --title ${title} --body ${body}`
|
|
64
|
+
.cwd(projectRoot)
|
|
65
|
+
.nothrow()
|
|
66
|
+
.quiet();
|
|
67
|
+
return {
|
|
68
|
+
exitCode: result.exitCode,
|
|
69
|
+
stderr: result.stderr.toString().trim(),
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
logFn: console.log,
|
|
73
|
+
warnFn: console.warn,
|
|
74
|
+
promptDirtyTreeCommitFn: promptForDirtyTreeCommit,
|
|
75
|
+
gitAddAndCommitFn: runGitAddAndCommit,
|
|
76
|
+
writeJsonArtifactFn: writeJsonArtifact,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type ReadLineFn = () => Promise<string | null>;
|
|
80
|
+
type WriteFn = (message: string) => void;
|
|
81
|
+
type IsTTYFn = () => boolean;
|
|
82
|
+
|
|
83
|
+
function defaultWrite(message: string): void {
|
|
84
|
+
process.stdout.write(`${message}\n`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function defaultIsTTY(): boolean {
|
|
88
|
+
return process.stdin.isTTY === true;
|
|
45
89
|
}
|
|
46
90
|
|
|
47
|
-
function
|
|
48
|
-
|
|
91
|
+
export async function promptForDirtyTreeCommit(
|
|
92
|
+
question: string,
|
|
93
|
+
readLineFn: ReadLineFn = defaultReadLine,
|
|
94
|
+
writeFn: WriteFn = defaultWrite,
|
|
95
|
+
isTTYFn: IsTTYFn = defaultIsTTY,
|
|
96
|
+
): Promise<boolean> {
|
|
97
|
+
if (!isTTYFn()) {
|
|
49
98
|
return false;
|
|
50
99
|
}
|
|
51
100
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
101
|
+
writeFn(question);
|
|
102
|
+
|
|
103
|
+
let line: string | null;
|
|
104
|
+
try {
|
|
105
|
+
line = await readLineFn();
|
|
106
|
+
} catch {
|
|
107
|
+
line = null;
|
|
56
108
|
}
|
|
57
109
|
|
|
58
|
-
return
|
|
110
|
+
return line !== null && (line.trim() === "y" || line.trim() === "Y");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function runGitAddAndCommit(projectRoot: string, commitMessage: string): Promise<void> {
|
|
114
|
+
const addResult = await dollar`git add -A`.cwd(projectRoot).nothrow().quiet();
|
|
115
|
+
if (addResult.exitCode !== 0) {
|
|
116
|
+
throw new Error(`Pre-prototype commit failed:\n${addResult.stderr.toString().trim()}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const commitResult = await dollar`git commit -m ${commitMessage}`.cwd(projectRoot).nothrow().quiet();
|
|
120
|
+
if (commitResult.exitCode !== 0) {
|
|
121
|
+
throw new Error(`Pre-prototype commit failed:\n${commitResult.stderr.toString().trim()}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function runPrePrototypeCommit(
|
|
126
|
+
projectRoot: string,
|
|
127
|
+
iteration: string,
|
|
128
|
+
gitAddAndCommitFn: (projectRoot: string, commitMessage: string) => Promise<void> = runGitAddAndCommit,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
const commitMessage = `chore: pre-prototype commit it_${iteration}`;
|
|
131
|
+
try {
|
|
132
|
+
await gitAddAndCommitFn(projectRoot, commitMessage);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
135
|
+
if (reason.startsWith("Pre-prototype commit failed:\n")) {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`Pre-prototype commit failed:\n${reason}`);
|
|
139
|
+
}
|
|
59
140
|
}
|
|
60
141
|
|
|
61
142
|
function parseQualityChecks(projectContextContent: string): string[] {
|
|
@@ -105,7 +186,11 @@ function parseQualityChecks(projectContextContent: string): string[] {
|
|
|
105
186
|
.filter((line) => line.length > 0);
|
|
106
187
|
}
|
|
107
188
|
|
|
108
|
-
export async function runCreatePrototype(
|
|
189
|
+
export async function runCreatePrototype(
|
|
190
|
+
opts: CreatePrototypeOptions,
|
|
191
|
+
deps: Partial<CreatePrototypeDeps> = {},
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
const mergedDeps: CreatePrototypeDeps = { ...defaultDeps, ...deps };
|
|
109
194
|
const projectRoot = process.cwd();
|
|
110
195
|
const state = await readState(projectRoot);
|
|
111
196
|
const force = opts.force ?? false;
|
|
@@ -151,6 +236,8 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
151
236
|
);
|
|
152
237
|
}
|
|
153
238
|
|
|
239
|
+
let prePrototypeCommitDone = false;
|
|
240
|
+
|
|
154
241
|
if (state.current_phase === "define") {
|
|
155
242
|
if (
|
|
156
243
|
state.phases.define.prd_generation.status === "completed" &&
|
|
@@ -163,9 +250,15 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
163
250
|
);
|
|
164
251
|
}
|
|
165
252
|
if (workingTreeBeforeTransition.stdout.toString().trim().length > 0) {
|
|
166
|
-
|
|
167
|
-
|
|
253
|
+
const shouldProceed = await mergedDeps.promptDirtyTreeCommitFn(
|
|
254
|
+
DIRTY_TREE_COMMIT_PROMPT,
|
|
168
255
|
);
|
|
256
|
+
if (!shouldProceed) {
|
|
257
|
+
mergedDeps.logFn(DECLINE_DIRTY_TREE_ABORT_MESSAGE);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
await runPrePrototypeCommit(projectRoot, iteration, mergedDeps.gitAddAndCommitFn);
|
|
261
|
+
prePrototypeCommitDone = true;
|
|
169
262
|
}
|
|
170
263
|
state.current_phase = "prototype";
|
|
171
264
|
await writeState(projectRoot, state);
|
|
@@ -193,16 +286,23 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
193
286
|
{ force },
|
|
194
287
|
);
|
|
195
288
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
289
|
+
if (!prePrototypeCommitDone) {
|
|
290
|
+
const workingTreeAfterPhase = await dollar`git status --porcelain`.cwd(projectRoot).nothrow().quiet();
|
|
291
|
+
if (workingTreeAfterPhase.exitCode !== 0) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
"Unable to verify git working tree status. Ensure this directory is a git repository and git is installed.",
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (workingTreeAfterPhase.stdout.toString().trim().length > 0) {
|
|
297
|
+
const shouldProceed = await mergedDeps.promptDirtyTreeCommitFn(
|
|
298
|
+
DIRTY_TREE_COMMIT_PROMPT,
|
|
299
|
+
);
|
|
300
|
+
if (!shouldProceed) {
|
|
301
|
+
mergedDeps.logFn(DECLINE_DIRTY_TREE_ABORT_MESSAGE);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
await runPrePrototypeCommit(projectRoot, iteration, mergedDeps.gitAddAndCommitFn);
|
|
305
|
+
}
|
|
206
306
|
}
|
|
207
307
|
|
|
208
308
|
const branchName = `feature/it_${iteration}`;
|
|
@@ -230,7 +330,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
230
330
|
const progressFileName = `it_${iteration}_progress.json`;
|
|
231
331
|
const progressPath = join(projectRoot, FLOW_REL_DIR, progressFileName);
|
|
232
332
|
const storyIds = sortedValues(prdValidation.data.userStories.map((story) => story.id));
|
|
233
|
-
let progressData:
|
|
333
|
+
let progressData: PrototypeProgress;
|
|
234
334
|
|
|
235
335
|
if (await exists(progressPath)) {
|
|
236
336
|
let parsedProgress: unknown;
|
|
@@ -273,7 +373,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
273
373
|
})),
|
|
274
374
|
};
|
|
275
375
|
|
|
276
|
-
await
|
|
376
|
+
await mergedDeps.writeJsonArtifactFn(progressPath, PrototypeProgressSchema, progress);
|
|
277
377
|
progressData = progress;
|
|
278
378
|
}
|
|
279
379
|
|
|
@@ -283,7 +383,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
283
383
|
});
|
|
284
384
|
|
|
285
385
|
if (eligibleStories.length === 0) {
|
|
286
|
-
|
|
386
|
+
mergedDeps.logFn("No pending or failed user stories to implement. Exiting without changes.");
|
|
287
387
|
return;
|
|
288
388
|
}
|
|
289
389
|
|
|
@@ -295,7 +395,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
295
395
|
|
|
296
396
|
let skillTemplate: string;
|
|
297
397
|
try {
|
|
298
|
-
skillTemplate = await
|
|
398
|
+
skillTemplate = await mergedDeps.loadSkillFn(projectRoot, "implement-user-story");
|
|
299
399
|
} catch {
|
|
300
400
|
throw new Error(
|
|
301
401
|
"Required skill missing: expected .agents/skills/implement-user-story/SKILL.md.",
|
|
@@ -334,7 +434,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
334
434
|
user_story: JSON.stringify(story, null, 2),
|
|
335
435
|
});
|
|
336
436
|
|
|
337
|
-
const agentResult = await
|
|
437
|
+
const agentResult = await mergedDeps.invokeAgentFn({
|
|
338
438
|
provider: opts.provider,
|
|
339
439
|
prompt,
|
|
340
440
|
cwd: projectRoot,
|
|
@@ -368,7 +468,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
368
468
|
entry.last_error_summary = "Agent or quality check failed";
|
|
369
469
|
}
|
|
370
470
|
|
|
371
|
-
await
|
|
471
|
+
await mergedDeps.writeJsonArtifactFn(progressPath, PrototypeProgressSchema, progressData);
|
|
372
472
|
|
|
373
473
|
if (allPassed) {
|
|
374
474
|
const commitMessage = `feat: implement ${story.id} - ${story.title}`;
|
|
@@ -381,9 +481,9 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
381
481
|
entry.status = "failed";
|
|
382
482
|
entry.last_error_summary = "Git commit failed";
|
|
383
483
|
entry.updated_at = new Date().toISOString();
|
|
384
|
-
await
|
|
484
|
+
await mergedDeps.writeJsonArtifactFn(progressPath, PrototypeProgressSchema, progressData);
|
|
385
485
|
|
|
386
|
-
|
|
486
|
+
mergedDeps.logFn(
|
|
387
487
|
`iteration=it_${iteration} story=${story.id} attempt=${entry.attempt_count} outcome=commit_failed`,
|
|
388
488
|
);
|
|
389
489
|
|
|
@@ -391,7 +491,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
391
491
|
haltedByCritical = true;
|
|
392
492
|
}
|
|
393
493
|
} else {
|
|
394
|
-
|
|
494
|
+
mergedDeps.logFn(
|
|
395
495
|
`iteration=it_${iteration} story=${story.id} attempt=${entry.attempt_count} outcome=passed`,
|
|
396
496
|
);
|
|
397
497
|
}
|
|
@@ -399,7 +499,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
399
499
|
break;
|
|
400
500
|
}
|
|
401
501
|
|
|
402
|
-
|
|
502
|
+
mergedDeps.logFn(
|
|
403
503
|
`iteration=it_${iteration} story=${story.id} attempt=${entry.attempt_count} outcome=failed`,
|
|
404
504
|
);
|
|
405
505
|
|
|
@@ -423,13 +523,25 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
423
523
|
await writeState(projectRoot, state);
|
|
424
524
|
|
|
425
525
|
if (storiesAttempted === 0) {
|
|
426
|
-
|
|
526
|
+
mergedDeps.logFn("No user stories attempted.");
|
|
427
527
|
return;
|
|
428
528
|
}
|
|
429
529
|
|
|
430
530
|
if (allCompleted) {
|
|
431
|
-
|
|
531
|
+
const ghAvailable = await mergedDeps.checkGhAvailableFn(projectRoot);
|
|
532
|
+
if (!ghAvailable) {
|
|
533
|
+
mergedDeps.logFn("gh CLI not found — skipping PR creation");
|
|
534
|
+
} else {
|
|
535
|
+
const prTitle = `feat: prototype it_${iteration}`;
|
|
536
|
+
const prBody = `Prototype for iteration it_${iteration}`;
|
|
537
|
+
const prResult = await mergedDeps.createPullRequestFn(projectRoot, prTitle, prBody);
|
|
538
|
+
if (prResult.exitCode !== 0) {
|
|
539
|
+
const suffix = prResult.stderr.length > 0 ? `: ${prResult.stderr}` : "";
|
|
540
|
+
mergedDeps.warnFn(`gh pr create failed (non-fatal)${suffix}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
mergedDeps.logFn("Prototype implementation completed for all user stories.");
|
|
432
544
|
} else {
|
|
433
|
-
|
|
545
|
+
mergedDeps.logFn("Prototype implementation paused with remaining pending or failed stories.");
|
|
434
546
|
}
|
|
435
547
|
}
|
|
@@ -193,6 +193,66 @@ describe("execute automated-fix", () => {
|
|
|
193
193
|
expect(providersUsed).toEqual(["cursor"]);
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
+
// RI-005: execute-automated-fix is a phase-independent exception to the guardrail system.
|
|
197
|
+
// It must process issues regardless of current_phase because issues can arise and need
|
|
198
|
+
// fixing at any point in the workflow (prototype OR refactor phases).
|
|
199
|
+
test("RI-005: processes open issues regardless of current_phase (phase-independent guardrail exception)", async () => {
|
|
200
|
+
const projectRoot = await createProjectRoot();
|
|
201
|
+
createdRoots.push(projectRoot);
|
|
202
|
+
|
|
203
|
+
// Seed state with current_phase = "refactor", not the typical "prototype"
|
|
204
|
+
await mkdir(join(projectRoot, ".agents", "flow"), { recursive: true });
|
|
205
|
+
const { writeState } = await import("../state");
|
|
206
|
+
await writeState(projectRoot, {
|
|
207
|
+
current_iteration: "000009",
|
|
208
|
+
current_phase: "refactor",
|
|
209
|
+
phases: {
|
|
210
|
+
define: {
|
|
211
|
+
requirement_definition: { status: "approved", file: "it_000009_product-requirement-document.md" },
|
|
212
|
+
prd_generation: { status: "completed", file: "it_000009_PRD.json" },
|
|
213
|
+
},
|
|
214
|
+
prototype: {
|
|
215
|
+
project_context: { status: "created", file: ".agents/PROJECT_CONTEXT.md" },
|
|
216
|
+
test_plan: { status: "created", file: "it_000009_test-plan.md" },
|
|
217
|
+
tp_generation: { status: "created", file: "it_000009_test-plan.json" },
|
|
218
|
+
prototype_build: { status: "created", file: null },
|
|
219
|
+
test_execution: { status: "completed", file: null },
|
|
220
|
+
prototype_approved: true,
|
|
221
|
+
},
|
|
222
|
+
refactor: {
|
|
223
|
+
evaluation_report: { status: "created", file: null },
|
|
224
|
+
refactor_plan: { status: "approved", file: null },
|
|
225
|
+
refactor_execution: { status: "in_progress", file: null },
|
|
226
|
+
changelog: { status: "pending", file: null },
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
last_updated: "2026-02-22T00:00:00.000Z",
|
|
230
|
+
updated_by: "seed",
|
|
231
|
+
history: [],
|
|
232
|
+
});
|
|
233
|
+
await writeIssues(projectRoot, "000009", [
|
|
234
|
+
{ id: "ISSUE-000009-001", title: "Refactor-phase issue", description: "fix me", status: "open" },
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
let invokeCount = 0;
|
|
238
|
+
|
|
239
|
+
await withCwd(projectRoot, async () => {
|
|
240
|
+
await runExecuteAutomatedFix(
|
|
241
|
+
{ provider: "codex" },
|
|
242
|
+
{
|
|
243
|
+
loadSkillFn: async () => "debug workflow",
|
|
244
|
+
invokeAgentFn: async () => {
|
|
245
|
+
invokeCount += 1;
|
|
246
|
+
return { exitCode: 0, stdout: "ok", stderr: "" };
|
|
247
|
+
},
|
|
248
|
+
runCommitFn: async () => 0,
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(invokeCount).toBe(1);
|
|
254
|
+
});
|
|
255
|
+
|
|
196
256
|
test("logs informative message and exits without changes when zero open issues exist", async () => {
|
|
197
257
|
const projectRoot = await createProjectRoot();
|
|
198
258
|
createdRoots.push(projectRoot);
|
|
@@ -225,7 +285,8 @@ describe("execute automated-fix", () => {
|
|
|
225
285
|
expect(logs).toContain("No open issues to process. Exiting without changes.");
|
|
226
286
|
});
|
|
227
287
|
|
|
228
|
-
|
|
288
|
+
// US-001-AC01: When --iterations is not provided, all open issues are processed
|
|
289
|
+
test("US-001-AC01: processes all open issues when --iterations is not provided", async () => {
|
|
229
290
|
const projectRoot = await createProjectRoot();
|
|
230
291
|
createdRoots.push(projectRoot);
|
|
231
292
|
|
|
@@ -237,6 +298,7 @@ describe("execute automated-fix", () => {
|
|
|
237
298
|
]);
|
|
238
299
|
|
|
239
300
|
let invokeCount = 0;
|
|
301
|
+
const logs: string[] = [];
|
|
240
302
|
|
|
241
303
|
await withCwd(projectRoot, async () => {
|
|
242
304
|
await runExecuteAutomatedFix(
|
|
@@ -248,6 +310,8 @@ describe("execute automated-fix", () => {
|
|
|
248
310
|
return { exitCode: 0, stdout: "", stderr: "" };
|
|
249
311
|
},
|
|
250
312
|
runCommitFn: async () => 0,
|
|
313
|
+
logFn: (message) => logs.push(message),
|
|
314
|
+
nowFn: () => new Date("2026-02-22T12:00:00.000Z"),
|
|
251
315
|
},
|
|
252
316
|
);
|
|
253
317
|
});
|
|
@@ -255,10 +319,14 @@ describe("execute automated-fix", () => {
|
|
|
255
319
|
const issuesRaw = await readFile(join(projectRoot, ".agents", "flow", "it_000009_ISSUES.json"), "utf8");
|
|
256
320
|
const issues = JSON.parse(issuesRaw) as Array<{ id: string; status: string }>;
|
|
257
321
|
|
|
258
|
-
|
|
322
|
+
// All 3 open issues should be processed
|
|
323
|
+
expect(invokeCount).toBe(3);
|
|
259
324
|
expect(issues.find((issue) => issue.id === "ISSUE-000009-001")?.status).toBe("fixed");
|
|
260
|
-
expect(issues.find((issue) => issue.id === "ISSUE-000009-002")?.status).toBe("
|
|
261
|
-
expect(issues.find((issue) => issue.id === "ISSUE-000009-003")?.status).toBe("
|
|
325
|
+
expect(issues.find((issue) => issue.id === "ISSUE-000009-002")?.status).toBe("fixed");
|
|
326
|
+
expect(issues.find((issue) => issue.id === "ISSUE-000009-003")?.status).toBe("fixed");
|
|
327
|
+
// AC04: summary reflects all processed issues
|
|
328
|
+
expect(logs).toContain("Summary: Fixed=3 Failed=0");
|
|
329
|
+
expect(logs).toContain("Processed 3 open issue(s) at 2026-02-22T12:00:00.000Z");
|
|
262
330
|
});
|
|
263
331
|
|
|
264
332
|
test("processes only the first N open issues when --iterations is provided", async () => {
|
|
@@ -297,7 +365,7 @@ describe("execute automated-fix", () => {
|
|
|
297
365
|
expect(issues.find((issue) => issue.id === "ISSUE-000009-003")?.status).toBe("open");
|
|
298
366
|
});
|
|
299
367
|
|
|
300
|
-
test("
|
|
368
|
+
test("throws deterministic validation error when issues file contains entries with missing required fields", async () => {
|
|
301
369
|
const projectRoot = await createProjectRoot();
|
|
302
370
|
createdRoots.push(projectRoot);
|
|
303
371
|
|
|
@@ -308,34 +376,11 @@ describe("execute automated-fix", () => {
|
|
|
308
376
|
{ id: "ISSUE-000009-003", title: "Fixed", description: "skip", status: "fixed" },
|
|
309
377
|
]);
|
|
310
378
|
|
|
311
|
-
const logs: string[] = [];
|
|
312
|
-
const prompts: string[] = [];
|
|
313
|
-
|
|
314
379
|
await withCwd(projectRoot, async () => {
|
|
315
|
-
await runExecuteAutomatedFix(
|
|
316
|
-
|
|
317
|
-
{
|
|
318
|
-
loadSkillFn: async () => "debug workflow",
|
|
319
|
-
invokeAgentFn: async (options) => {
|
|
320
|
-
prompts.push(options.prompt);
|
|
321
|
-
return { exitCode: 0, stdout: "", stderr: "" };
|
|
322
|
-
},
|
|
323
|
-
runCommitFn: async () => 0,
|
|
324
|
-
logFn: (message) => logs.push(message),
|
|
325
|
-
},
|
|
380
|
+
await expect(runExecuteAutomatedFix({ provider: "codex" })).rejects.toThrow(
|
|
381
|
+
"Deterministic validation error: issues schema mismatch in .agents/flow/it_000009_ISSUES.json.",
|
|
326
382
|
);
|
|
327
383
|
});
|
|
328
|
-
|
|
329
|
-
expect(prompts).toHaveLength(1);
|
|
330
|
-
expect(prompts[0]).toContain('"id": "ISSUE-000009-001"');
|
|
331
|
-
expect(logs.some((line) => line.includes("Warning: Skipping issue at index 1"))).toBe(true);
|
|
332
|
-
|
|
333
|
-
const issuesRaw = await readFile(join(projectRoot, ".agents", "flow", "it_000009_ISSUES.json"), "utf8");
|
|
334
|
-
const issues = JSON.parse(issuesRaw) as Array<{ id: string; status: string }>;
|
|
335
|
-
|
|
336
|
-
expect(issues.find((issue) => issue.id === "ISSUE-000009-001")?.status).toBe("fixed");
|
|
337
|
-
expect(issues.find((issue) => issue.id === "ISSUE-000009-003")?.status).toBe("fixed");
|
|
338
|
-
expect(issues.some((issue) => issue.id === "ISSUE-000009-002")).toBe(false);
|
|
339
384
|
});
|
|
340
385
|
|
|
341
386
|
test("marks issue as retry when hypothesis is not confirmed and retries remain", async () => {
|
|
@@ -363,9 +408,9 @@ describe("execute automated-fix", () => {
|
|
|
363
408
|
return { exitCode: 0, stdout: "", stderr: "" };
|
|
364
409
|
},
|
|
365
410
|
runCommitFn: async () => 0,
|
|
366
|
-
|
|
367
|
-
writtenSnapshots.push(
|
|
368
|
-
|
|
411
|
+
writeJsonArtifactFn: async (path, _schema, data) => {
|
|
412
|
+
writtenSnapshots.push(JSON.stringify(data, null, 2));
|
|
413
|
+
await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
369
414
|
},
|
|
370
415
|
},
|
|
371
416
|
);
|