@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,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildPrompt,
|
|
3
|
+
invokeAgent,
|
|
4
|
+
loadSkill,
|
|
5
|
+
type AgentInvokeOptions,
|
|
6
|
+
type AgentProvider,
|
|
7
|
+
type AgentResult,
|
|
8
|
+
} from "../agent";
|
|
9
|
+
import { assertGuardrail } from "../guardrail";
|
|
10
|
+
import { readState, writeState } from "../state";
|
|
11
|
+
|
|
12
|
+
export interface DefineRefactorPlanOptions {
|
|
13
|
+
provider: AgentProvider;
|
|
14
|
+
force?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface DefineRefactorPlanDeps {
|
|
18
|
+
invokeAgentFn: (options: AgentInvokeOptions) => Promise<AgentResult>;
|
|
19
|
+
loadSkillFn: (projectRoot: string, skillName: string) => Promise<string>;
|
|
20
|
+
nowFn: () => Date;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const defaultDeps: DefineRefactorPlanDeps = {
|
|
24
|
+
invokeAgentFn: invokeAgent,
|
|
25
|
+
loadSkillFn: loadSkill,
|
|
26
|
+
nowFn: () => new Date(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export async function runDefineRefactorPlan(
|
|
30
|
+
opts: DefineRefactorPlanOptions,
|
|
31
|
+
deps: Partial<DefineRefactorPlanDeps> = {},
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const { provider, force = false } = opts;
|
|
34
|
+
const projectRoot = process.cwd();
|
|
35
|
+
const state = await readState(projectRoot);
|
|
36
|
+
const mergedDeps: DefineRefactorPlanDeps = { ...defaultDeps, ...deps };
|
|
37
|
+
|
|
38
|
+
await assertGuardrail(
|
|
39
|
+
state,
|
|
40
|
+
!state.phases.prototype.prototype_approved,
|
|
41
|
+
"Cannot define refactor plan: phases.prototype.prototype_approved must be true. Complete prototype (all tests passing) first.",
|
|
42
|
+
{ force },
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Intentional auto-transition: if the user runs this command directly from
|
|
46
|
+
// the prototype phase (prototype_approved === true), we advance to "refactor"
|
|
47
|
+
// so they don't have to manually update the phase. US-001-AC01 says the phase
|
|
48
|
+
// must be "refactor", but accepting "prototype" here is a UX convenience that
|
|
49
|
+
// is safe because prototype_approved is already enforced above.
|
|
50
|
+
if (state.current_phase === "prototype") {
|
|
51
|
+
state.current_phase = "refactor";
|
|
52
|
+
} else if (state.current_phase !== "refactor") {
|
|
53
|
+
await assertGuardrail(
|
|
54
|
+
state,
|
|
55
|
+
true,
|
|
56
|
+
`Cannot define refactor plan: current_phase must be 'prototype' or 'refactor'. Current: '${state.current_phase}'.`,
|
|
57
|
+
{ force },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const refactorPlan = state.phases.refactor.refactor_plan;
|
|
62
|
+
await assertGuardrail(
|
|
63
|
+
state,
|
|
64
|
+
refactorPlan.status !== "pending",
|
|
65
|
+
`Cannot define refactor plan from status '${refactorPlan.status}'. Expected pending.`,
|
|
66
|
+
{ force },
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const skillBody = await mergedDeps.loadSkillFn(projectRoot, "plan-refactor");
|
|
70
|
+
const prompt = buildPrompt(skillBody, {
|
|
71
|
+
current_iteration: state.current_iteration,
|
|
72
|
+
});
|
|
73
|
+
const result = await mergedDeps.invokeAgentFn({
|
|
74
|
+
provider,
|
|
75
|
+
prompt,
|
|
76
|
+
cwd: projectRoot,
|
|
77
|
+
interactive: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (result.exitCode !== 0) {
|
|
81
|
+
throw new Error(`Agent invocation failed with exit code ${result.exitCode}.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
state.phases.refactor.evaluation_report.status = "created";
|
|
85
|
+
state.phases.refactor.evaluation_report.file = `it_${state.current_iteration}_evaluation-report.md`;
|
|
86
|
+
refactorPlan.status = "pending_approval";
|
|
87
|
+
refactorPlan.file = `it_${state.current_iteration}_refactor-plan.md`;
|
|
88
|
+
state.last_updated = mergedDeps.nowFn().toISOString();
|
|
89
|
+
state.updated_by = "nvst:define-refactor-plan";
|
|
90
|
+
|
|
91
|
+
await writeState(projectRoot, state);
|
|
92
|
+
|
|
93
|
+
console.log(
|
|
94
|
+
"Evaluation report and refactor plan created. Refactor plan is pending approval.",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
import { buildPrompt, invokeAgent, loadSkill, type AgentProvider } from "../agent";
|
|
2
|
+
import { assertGuardrail } from "../guardrail";
|
|
2
3
|
import { readState, writeState } from "../state";
|
|
3
4
|
|
|
4
5
|
export interface DefineRequirementOptions {
|
|
5
6
|
provider: AgentProvider;
|
|
7
|
+
force?: boolean;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export async function runDefineRequirement(opts: DefineRequirementOptions): Promise<void> {
|
|
9
|
-
const { provider } = opts;
|
|
11
|
+
const { provider, force = false } = opts;
|
|
10
12
|
const projectRoot = process.cwd();
|
|
11
13
|
const state = await readState(projectRoot);
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
await assertGuardrail(
|
|
16
|
+
state,
|
|
17
|
+
state.current_phase !== "define",
|
|
18
|
+
"Cannot define requirement: current_phase must be 'define'.",
|
|
19
|
+
{ force },
|
|
20
|
+
);
|
|
16
21
|
|
|
17
22
|
const requirementDefinition = state.phases.define.requirement_definition;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
await assertGuardrail(
|
|
24
|
+
state,
|
|
25
|
+
requirementDefinition.status !== "pending",
|
|
26
|
+
`Cannot define requirement from status '${requirementDefinition.status}'. Expected pending.`,
|
|
27
|
+
{ force },
|
|
28
|
+
);
|
|
23
29
|
|
|
24
30
|
const skillBody = await loadSkill(projectRoot, "create-pr-document");
|
|
25
31
|
const prompt = buildPrompt(skillBody, {
|
|
@@ -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
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
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
4
|
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
type AgentResult,
|
|
12
12
|
} from "../agent";
|
|
13
13
|
import { exists, FLOW_REL_DIR, readState } from "../state";
|
|
14
|
-
import { type Issue } from "../../scaffold/schemas/tmpl_issues";
|
|
14
|
+
import { type Issue, IssuesSchema } from "../../scaffold/schemas/tmpl_issues";
|
|
15
|
+
import { writeJsonArtifact, type WriteJsonArtifactFn } from "../write-json-artifact";
|
|
15
16
|
|
|
16
17
|
export interface ExecuteAutomatedFixOptions {
|
|
17
18
|
provider: AgentProvider;
|
|
@@ -27,7 +28,7 @@ interface ExecuteAutomatedFixDeps {
|
|
|
27
28
|
nowFn: () => Date;
|
|
28
29
|
readFileFn: typeof readFile;
|
|
29
30
|
runCommitFn: (projectRoot: string, message: string) => Promise<number>;
|
|
30
|
-
|
|
31
|
+
writeJsonArtifactFn: WriteJsonArtifactFn;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
const defaultDeps: ExecuteAutomatedFixDeps = {
|
|
@@ -44,7 +45,7 @@ const defaultDeps: ExecuteAutomatedFixDeps = {
|
|
|
44
45
|
.quiet();
|
|
45
46
|
return result.exitCode;
|
|
46
47
|
},
|
|
47
|
-
|
|
48
|
+
writeJsonArtifactFn: writeJsonArtifact,
|
|
48
49
|
};
|
|
49
50
|
|
|
50
51
|
function isNetworkErrorText(text: string): boolean {
|
|
@@ -72,106 +73,12 @@ function sortIssuesById(issues: Issue[]): Issue[] {
|
|
|
72
73
|
return [...issues].sort((left, right) => left.id.localeCompare(right.id));
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
const ALLOWED_ISSUE_STATUSES: Set<Issue["status"]> = new Set([
|
|
76
|
-
"open",
|
|
77
|
-
"fixed",
|
|
78
|
-
"retry",
|
|
79
|
-
"manual-fix",
|
|
80
|
-
]);
|
|
81
|
-
|
|
82
|
-
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
83
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
return value as Record<string, unknown>;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function parseIssuesForProcessing(
|
|
90
|
-
raw: unknown,
|
|
91
|
-
flowRelativePath: string,
|
|
92
|
-
logFn: (message: string) => void,
|
|
93
|
-
): Issue[] {
|
|
94
|
-
if (!Array.isArray(raw)) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`Deterministic validation error: issues schema mismatch in ${flowRelativePath}.`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const parsedIssues: Issue[] = [];
|
|
101
|
-
const seenIds = new Set<string>();
|
|
102
|
-
|
|
103
|
-
for (const [index, item] of raw.entries()) {
|
|
104
|
-
const issue = asRecord(item);
|
|
105
|
-
if (!issue) {
|
|
106
|
-
logFn(
|
|
107
|
-
`Warning: Skipping invalid issue at index ${index} in ${flowRelativePath}: expected an object.`,
|
|
108
|
-
);
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const id = issue.id;
|
|
113
|
-
const title = issue.title;
|
|
114
|
-
const description = issue.description;
|
|
115
|
-
const status = issue.status;
|
|
116
|
-
|
|
117
|
-
const missingFields: string[] = [];
|
|
118
|
-
if (typeof id !== "string") {
|
|
119
|
-
missingFields.push("id");
|
|
120
|
-
}
|
|
121
|
-
if (typeof title !== "string") {
|
|
122
|
-
missingFields.push("title");
|
|
123
|
-
}
|
|
124
|
-
if (typeof description !== "string") {
|
|
125
|
-
missingFields.push("description");
|
|
126
|
-
}
|
|
127
|
-
if (typeof status !== "string") {
|
|
128
|
-
missingFields.push("status");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (missingFields.length > 0) {
|
|
132
|
-
logFn(
|
|
133
|
-
`Warning: Skipping issue at index ${index} in ${flowRelativePath}: missing required field(s): ${missingFields.join(", ")}.`,
|
|
134
|
-
);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const validId = id as string;
|
|
139
|
-
const validTitle = title as string;
|
|
140
|
-
const validDescription = description as string;
|
|
141
|
-
const validStatus = status as Issue["status"];
|
|
142
|
-
|
|
143
|
-
if (!ALLOWED_ISSUE_STATUSES.has(validStatus)) {
|
|
144
|
-
logFn(
|
|
145
|
-
`Warning: Skipping issue ${validId} in ${flowRelativePath}: invalid status '${status}'.`,
|
|
146
|
-
);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (seenIds.has(validId)) {
|
|
151
|
-
logFn(
|
|
152
|
-
`Warning: Skipping duplicate issue id '${validId}' in ${flowRelativePath}.`,
|
|
153
|
-
);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
seenIds.add(validId);
|
|
158
|
-
parsedIssues.push({
|
|
159
|
-
id: validId,
|
|
160
|
-
title: validTitle,
|
|
161
|
-
description: validDescription,
|
|
162
|
-
status: validStatus,
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return parsedIssues;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
76
|
async function writeIssuesFile(
|
|
170
77
|
issuesPath: string,
|
|
171
78
|
issues: Issue[],
|
|
172
79
|
deps: ExecuteAutomatedFixDeps,
|
|
173
80
|
): Promise<void> {
|
|
174
|
-
await deps.
|
|
81
|
+
await deps.writeJsonArtifactFn(issuesPath, IssuesSchema, issues);
|
|
175
82
|
}
|
|
176
83
|
|
|
177
84
|
async function commitIssueUpdate(
|
|
@@ -185,6 +92,25 @@ async function commitIssueUpdate(
|
|
|
185
92
|
return exitCode === 0;
|
|
186
93
|
}
|
|
187
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Guardrail policy: `execute-automated-fix` is an explicit exception to the
|
|
97
|
+
* phase-based guardrail system used by `execute-test-plan` and
|
|
98
|
+
* `execute-refactor`. Those commands assert `current_phase` and prerequisite
|
|
99
|
+
* status fields via `assertGuardrail` before running, because they depend on
|
|
100
|
+
* phase-specific state transitions being in place.
|
|
101
|
+
*
|
|
102
|
+
* `execute-automated-fix` is deliberately phase-independent: issues can exist
|
|
103
|
+
* and require automated remediation at any point in the workflow (prototype or
|
|
104
|
+
* refactor phases, or during reruns after partial fixes). Its sole
|
|
105
|
+
* prerequisite is the existence of a valid issues file for the current
|
|
106
|
+
* iteration, which is already enforced by a hard error below. Adding a
|
|
107
|
+
* phase-based guardrail here would prevent legitimate use cases (e.g. fixing
|
|
108
|
+
* issues discovered late in a refactor pass) without adding safety value.
|
|
109
|
+
*
|
|
110
|
+
* `--force` is therefore not applicable to this command and is not accepted as
|
|
111
|
+
* a flag (any unrecognised option, including `--force`, is rejected by the CLI
|
|
112
|
+
* router before reaching this function).
|
|
113
|
+
*/
|
|
188
114
|
export async function runExecuteAutomatedFix(
|
|
189
115
|
opts: ExecuteAutomatedFixOptions,
|
|
190
116
|
deps: Partial<ExecuteAutomatedFixDeps> = {},
|
|
@@ -224,7 +150,14 @@ export async function runExecuteAutomatedFix(
|
|
|
224
150
|
);
|
|
225
151
|
}
|
|
226
152
|
|
|
227
|
-
const
|
|
153
|
+
const issuesValidation = IssuesSchema.safeParse(parsedIssuesRaw);
|
|
154
|
+
if (!issuesValidation.success) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Deterministic validation error: issues schema mismatch in ${flowRelativePath}.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const issues = sortIssuesById(issuesValidation.data);
|
|
228
161
|
const openIssues = issues.filter((issue) => issue.status === "open");
|
|
229
162
|
|
|
230
163
|
if (openIssues.length === 0) {
|
|
@@ -233,7 +166,7 @@ export async function runExecuteAutomatedFix(
|
|
|
233
166
|
}
|
|
234
167
|
|
|
235
168
|
const skillTemplate = await mergedDeps.loadSkillFn(projectRoot, "automated-fix");
|
|
236
|
-
const maxIssuesToProcess = opts.iterations ??
|
|
169
|
+
const maxIssuesToProcess = opts.iterations ?? openIssues.length;
|
|
237
170
|
const issuesToProcess = openIssues.slice(0, maxIssuesToProcess);
|
|
238
171
|
const maxRetries = opts.retryOnFail ?? 0;
|
|
239
172
|
|