@quinteroac/agents-coding-toolkit 0.1.0-preview → 0.1.1-preview.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 +1 -1
- package/package.json +13 -4
- 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_refactor-execution-progress.ts +16 -0
- package/scaffold/schemas/tmpl_refactor-prd.ts +14 -0
- package/scaffold/schemas/tmpl_state.ts +1 -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-plan.test.ts +1 -1
- package/src/cli.test.ts +57 -0
- package/src/cli.ts +180 -56
- package/src/commands/approve-project-context.ts +13 -6
- 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 +31 -13
- package/src/commands/create-prototype.ts +17 -7
- 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-refactor.test.ts +954 -0
- package/src/commands/execute-refactor.ts +336 -0
- package/src/commands/execute-test-plan.test.ts +9 -2
- package/src/commands/execute-test-plan.ts +13 -6
- 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/write-json.ts +102 -97
- package/src/force-flag.test.ts +144 -0
- package/src/guardrail.test.ts +411 -0
- package/src/guardrail.ts +104 -0
- package/src/install.test.ts +7 -5
- package/src/pack.test.ts +2 -1
- 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,336 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { RefactorPrdSchema } from "../../scaffold/schemas/tmpl_refactor-prd";
|
|
6
|
+
import {
|
|
7
|
+
RefactorExecutionProgressSchema,
|
|
8
|
+
type RefactorExecutionProgress,
|
|
9
|
+
} from "../../scaffold/schemas/tmpl_refactor-execution-progress";
|
|
10
|
+
import {
|
|
11
|
+
buildPrompt,
|
|
12
|
+
invokeAgent,
|
|
13
|
+
loadSkill,
|
|
14
|
+
type AgentInvokeOptions,
|
|
15
|
+
type AgentProvider,
|
|
16
|
+
type AgentResult,
|
|
17
|
+
} from "../agent";
|
|
18
|
+
import { CLI_PATH } from "../cli-path";
|
|
19
|
+
import { assertGuardrail } from "../guardrail";
|
|
20
|
+
import { exists, FLOW_REL_DIR, readState, writeState } from "../state";
|
|
21
|
+
|
|
22
|
+
export interface ExecuteRefactorOptions {
|
|
23
|
+
provider: AgentProvider;
|
|
24
|
+
force?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { RefactorExecutionProgressSchema };
|
|
28
|
+
export type { RefactorExecutionProgress };
|
|
29
|
+
|
|
30
|
+
interface WriteJsonResult {
|
|
31
|
+
exitCode: number;
|
|
32
|
+
stderr: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ExecuteRefactorDeps {
|
|
36
|
+
existsFn: (path: string) => Promise<boolean>;
|
|
37
|
+
invokeAgentFn: (options: AgentInvokeOptions) => Promise<AgentResult>;
|
|
38
|
+
invokeWriteJsonFn: (
|
|
39
|
+
projectRoot: string,
|
|
40
|
+
schemaName: string,
|
|
41
|
+
outPath: string,
|
|
42
|
+
data: string,
|
|
43
|
+
) => Promise<WriteJsonResult>;
|
|
44
|
+
loadSkillFn: (projectRoot: string, skillName: string) => Promise<string>;
|
|
45
|
+
logFn: (message: string) => void;
|
|
46
|
+
nowFn: () => Date;
|
|
47
|
+
readFileFn: typeof readFile;
|
|
48
|
+
writeFileFn: typeof writeFile;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function runWriteJsonCommand(
|
|
52
|
+
projectRoot: string,
|
|
53
|
+
schemaName: string,
|
|
54
|
+
outPath: string,
|
|
55
|
+
data: string,
|
|
56
|
+
): Promise<WriteJsonResult> {
|
|
57
|
+
const result =
|
|
58
|
+
await $`bun ${CLI_PATH} write-json --schema ${schemaName} --out ${outPath} --data ${data}`
|
|
59
|
+
.cwd(projectRoot)
|
|
60
|
+
.nothrow()
|
|
61
|
+
.quiet();
|
|
62
|
+
return {
|
|
63
|
+
exitCode: result.exitCode,
|
|
64
|
+
stderr: result.stderr.toString().trim(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const defaultDeps: ExecuteRefactorDeps = {
|
|
69
|
+
existsFn: exists,
|
|
70
|
+
invokeAgentFn: invokeAgent,
|
|
71
|
+
invokeWriteJsonFn: runWriteJsonCommand,
|
|
72
|
+
loadSkillFn: loadSkill,
|
|
73
|
+
logFn: console.log,
|
|
74
|
+
nowFn: () => new Date(),
|
|
75
|
+
readFileFn: readFile,
|
|
76
|
+
writeFileFn: writeFile,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export async function runExecuteRefactor(
|
|
80
|
+
opts: ExecuteRefactorOptions,
|
|
81
|
+
deps: Partial<ExecuteRefactorDeps> = {},
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
const mergedDeps: ExecuteRefactorDeps = { ...defaultDeps, ...deps };
|
|
84
|
+
const force = opts.force ?? false;
|
|
85
|
+
const projectRoot = process.cwd();
|
|
86
|
+
const state = await readState(projectRoot);
|
|
87
|
+
|
|
88
|
+
// AC02: Reject if current_phase !== "refactor"
|
|
89
|
+
await assertGuardrail(
|
|
90
|
+
state,
|
|
91
|
+
state.current_phase !== "refactor",
|
|
92
|
+
`Cannot execute refactor: current_phase must be 'refactor'. Current phase: '${state.current_phase}'.`,
|
|
93
|
+
{ force },
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// AC03: Reject if refactor_plan.status !== "approved"
|
|
97
|
+
await assertGuardrail(
|
|
98
|
+
state,
|
|
99
|
+
state.phases.refactor.refactor_plan.status !== "approved",
|
|
100
|
+
`Cannot execute refactor: refactor_plan.status must be 'approved'. Current status: '${state.phases.refactor.refactor_plan.status}'. Run \`bun nvst approve refactor-plan\` first.`,
|
|
101
|
+
{ force },
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// AC04: Reject if refactor_execution.status is already "completed"
|
|
105
|
+
await assertGuardrail(
|
|
106
|
+
state,
|
|
107
|
+
state.phases.refactor.refactor_execution.status === "completed",
|
|
108
|
+
"Cannot execute refactor: refactor_execution.status is already 'completed'.",
|
|
109
|
+
{ force },
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// AC05: Read and validate refactor-prd.json
|
|
113
|
+
const iteration = state.current_iteration;
|
|
114
|
+
const refactorPrdFileName = `it_${iteration}_refactor-prd.json`;
|
|
115
|
+
const refactorPrdPath = join(projectRoot, FLOW_REL_DIR, refactorPrdFileName);
|
|
116
|
+
|
|
117
|
+
if (!(await mergedDeps.existsFn(refactorPrdPath))) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Refactor PRD file missing: expected ${join(FLOW_REL_DIR, refactorPrdFileName)}. Run \`bun nvst approve refactor-plan\` first.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let parsedPrd: unknown;
|
|
124
|
+
try {
|
|
125
|
+
parsedPrd = JSON.parse(await mergedDeps.readFileFn(refactorPrdPath, "utf8"));
|
|
126
|
+
} catch {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Invalid refactor PRD JSON in ${join(FLOW_REL_DIR, refactorPrdFileName)}.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const prdValidation = RefactorPrdSchema.safeParse(parsedPrd);
|
|
133
|
+
if (!prdValidation.success) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Refactor PRD schema mismatch in ${join(FLOW_REL_DIR, refactorPrdFileName)}.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const refactorItems = prdValidation.data.refactorItems;
|
|
140
|
+
|
|
141
|
+
// Load skill
|
|
142
|
+
let skillTemplate: string;
|
|
143
|
+
try {
|
|
144
|
+
skillTemplate = await mergedDeps.loadSkillFn(projectRoot, "execute-refactor-item");
|
|
145
|
+
} catch {
|
|
146
|
+
throw new Error(
|
|
147
|
+
"Required skill missing: expected .agents/skills/execute-refactor-item/SKILL.md.",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// AC13: Progress file name
|
|
152
|
+
const progressFileName = `it_${iteration}_refactor-execution-progress.json`;
|
|
153
|
+
const progressPath = join(projectRoot, FLOW_REL_DIR, progressFileName);
|
|
154
|
+
|
|
155
|
+
// AC06: Set refactor_execution.status = "in_progress" before processing
|
|
156
|
+
// AC13: Set refactor_execution.file
|
|
157
|
+
state.phases.refactor.refactor_execution.status = "in_progress";
|
|
158
|
+
state.phases.refactor.refactor_execution.file = progressFileName;
|
|
159
|
+
state.last_updated = mergedDeps.nowFn().toISOString();
|
|
160
|
+
state.updated_by = "nvst:execute-refactor";
|
|
161
|
+
await writeState(projectRoot, state);
|
|
162
|
+
|
|
163
|
+
// Initialize or load progress file
|
|
164
|
+
let progressData: RefactorExecutionProgress;
|
|
165
|
+
|
|
166
|
+
if (await mergedDeps.existsFn(progressPath)) {
|
|
167
|
+
let parsedProgress: unknown;
|
|
168
|
+
try {
|
|
169
|
+
parsedProgress = JSON.parse(await mergedDeps.readFileFn(progressPath, "utf8"));
|
|
170
|
+
} catch {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Invalid progress JSON in ${join(FLOW_REL_DIR, progressFileName)}.`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const progressValidation = RefactorExecutionProgressSchema.safeParse(parsedProgress);
|
|
177
|
+
if (!progressValidation.success) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Progress schema mismatch in ${join(FLOW_REL_DIR, progressFileName)}.`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// AC05: Verify progress item IDs match refactor PRD item IDs
|
|
184
|
+
const expectedIds = [...refactorItems.map((item) => item.id)].sort((a, b) => a.localeCompare(b));
|
|
185
|
+
const existingIds = [...progressValidation.data.entries.map((entry) => entry.id)].sort((a, b) => a.localeCompare(b));
|
|
186
|
+
if (
|
|
187
|
+
expectedIds.length !== existingIds.length ||
|
|
188
|
+
expectedIds.some((id, i) => id !== existingIds[i])
|
|
189
|
+
) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
"Refactor execution progress file out of sync: entry ids do not match refactor PRD item ids.",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
progressData = progressValidation.data;
|
|
196
|
+
} else {
|
|
197
|
+
const now = mergedDeps.nowFn().toISOString();
|
|
198
|
+
progressData = {
|
|
199
|
+
entries: refactorItems.map((item) => ({
|
|
200
|
+
id: item.id,
|
|
201
|
+
title: item.title,
|
|
202
|
+
status: "pending" as const,
|
|
203
|
+
attempt_count: 0,
|
|
204
|
+
last_agent_exit_code: null,
|
|
205
|
+
updated_at: now,
|
|
206
|
+
})),
|
|
207
|
+
};
|
|
208
|
+
const writeResult = await mergedDeps.invokeWriteJsonFn(
|
|
209
|
+
projectRoot,
|
|
210
|
+
"refactor-execution-progress",
|
|
211
|
+
join(FLOW_REL_DIR, progressFileName),
|
|
212
|
+
JSON.stringify(progressData),
|
|
213
|
+
);
|
|
214
|
+
if (writeResult.exitCode !== 0) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Failed to write refactor execution progress: ${writeResult.stderr || "write-json exited non-zero"}.`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// AC07, AC08, AC09, AC10: Process each item in order
|
|
222
|
+
for (const item of refactorItems) {
|
|
223
|
+
const entry = progressData.entries.find((e) => e.id === item.id);
|
|
224
|
+
if (!entry || entry.status === "completed") {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Set current item to in_progress before invoking agent (FR-4; observability on interrupt)
|
|
229
|
+
entry.status = "in_progress";
|
|
230
|
+
entry.updated_at = mergedDeps.nowFn().toISOString();
|
|
231
|
+
const writeInProgressResult = await mergedDeps.invokeWriteJsonFn(
|
|
232
|
+
projectRoot,
|
|
233
|
+
"refactor-execution-progress",
|
|
234
|
+
join(FLOW_REL_DIR, progressFileName),
|
|
235
|
+
JSON.stringify(progressData),
|
|
236
|
+
);
|
|
237
|
+
if (writeInProgressResult.exitCode !== 0) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Failed to write refactor execution progress: ${writeInProgressResult.stderr || "write-json exited non-zero"}.`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// AC07: Build prompt with skill and item context (FR-6 variable names)
|
|
244
|
+
const prompt = buildPrompt(skillTemplate, {
|
|
245
|
+
current_iteration: iteration,
|
|
246
|
+
item_id: item.id,
|
|
247
|
+
item_title: item.title,
|
|
248
|
+
item_description: item.description,
|
|
249
|
+
item_rationale: item.rationale,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// AC08: Invoke agent in interactive mode
|
|
253
|
+
const agentResult = await mergedDeps.invokeAgentFn({
|
|
254
|
+
provider: opts.provider,
|
|
255
|
+
prompt,
|
|
256
|
+
cwd: projectRoot,
|
|
257
|
+
interactive: true,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// AC09 & AC10: Record result after each invocation, continue on failure
|
|
261
|
+
const succeeded = agentResult.exitCode === 0;
|
|
262
|
+
entry.status = succeeded ? "completed" : "failed";
|
|
263
|
+
entry.attempt_count = entry.attempt_count + 1;
|
|
264
|
+
entry.last_agent_exit_code = agentResult.exitCode;
|
|
265
|
+
entry.updated_at = mergedDeps.nowFn().toISOString();
|
|
266
|
+
|
|
267
|
+
const writeResult = await mergedDeps.invokeWriteJsonFn(
|
|
268
|
+
projectRoot,
|
|
269
|
+
"refactor-execution-progress",
|
|
270
|
+
join(FLOW_REL_DIR, progressFileName),
|
|
271
|
+
JSON.stringify(progressData),
|
|
272
|
+
);
|
|
273
|
+
if (writeResult.exitCode !== 0) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`Failed to write refactor execution progress: ${writeResult.stderr || "write-json exited non-zero"}.`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
mergedDeps.logFn(
|
|
280
|
+
`iteration=it_${iteration} item=${item.id} outcome=${entry.status}`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// US-003: Generate markdown execution report (written regardless of failures)
|
|
285
|
+
const reportFileName = `it_${iteration}_refactor-execution-report.md`;
|
|
286
|
+
const reportPath = join(projectRoot, FLOW_REL_DIR, reportFileName);
|
|
287
|
+
const reportContent = buildRefactorExecutionReport(iteration, progressData);
|
|
288
|
+
await mergedDeps.writeFileFn(reportPath, reportContent, "utf8");
|
|
289
|
+
|
|
290
|
+
// AC11 & AC12: Update state based on overall result
|
|
291
|
+
const allCompleted = progressData.entries.every((entry) => entry.status === "completed");
|
|
292
|
+
|
|
293
|
+
if (allCompleted) {
|
|
294
|
+
// AC11: All completed → set status to "completed"
|
|
295
|
+
state.phases.refactor.refactor_execution.status = "completed";
|
|
296
|
+
}
|
|
297
|
+
// AC12: Any failure → stays "in_progress" (already set above)
|
|
298
|
+
|
|
299
|
+
state.last_updated = mergedDeps.nowFn().toISOString();
|
|
300
|
+
state.updated_by = "nvst:execute-refactor";
|
|
301
|
+
await writeState(projectRoot, state);
|
|
302
|
+
|
|
303
|
+
if (allCompleted) {
|
|
304
|
+
mergedDeps.logFn("Refactor execution completed for all items.");
|
|
305
|
+
} else {
|
|
306
|
+
mergedDeps.logFn("Refactor execution paused with remaining pending or failed items.");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function buildRefactorExecutionReport(
|
|
311
|
+
iteration: string,
|
|
312
|
+
progress: RefactorExecutionProgress,
|
|
313
|
+
): string {
|
|
314
|
+
const total = progress.entries.length;
|
|
315
|
+
const completed = progress.entries.filter((e) => e.status === "completed").length;
|
|
316
|
+
const failed = progress.entries.filter((e) => e.status === "failed").length;
|
|
317
|
+
|
|
318
|
+
const tableRows = progress.entries
|
|
319
|
+
.map((e) => {
|
|
320
|
+
const exitCode = e.last_agent_exit_code === null ? "N/A" : String(e.last_agent_exit_code);
|
|
321
|
+
return `| ${e.id} | ${e.title} | ${e.status} | ${exitCode} |`;
|
|
322
|
+
})
|
|
323
|
+
.join("\n");
|
|
324
|
+
|
|
325
|
+
return `# Refactor Execution Report
|
|
326
|
+
|
|
327
|
+
**Iteration:** it_${iteration}
|
|
328
|
+
**Total:** ${total}
|
|
329
|
+
**Completed:** ${completed}
|
|
330
|
+
**Failed:** ${failed}
|
|
331
|
+
|
|
332
|
+
| RI ID | Title | Status | Agent Exit Code |
|
|
333
|
+
|-------|-------|--------|-----------------|
|
|
334
|
+
${tableRows}
|
|
335
|
+
`;
|
|
336
|
+
}
|
|
@@ -114,7 +114,7 @@ describe("execute test-plan command", () => {
|
|
|
114
114
|
expect(source).toContain("if (command === \"execute\") {");
|
|
115
115
|
expect(source).toContain('if (subcommand === "test-plan") {');
|
|
116
116
|
expect(source).toContain("const { provider, remainingArgs: postAgentArgs } = parseAgentArg(args.slice(1));");
|
|
117
|
-
expect(source).toContain("await runExecuteTestPlan({ provider });");
|
|
117
|
+
expect(source).toContain("await runExecuteTestPlan({ provider, force });");
|
|
118
118
|
expect(source).toContain("execute test-plan --agent <provider>");
|
|
119
119
|
});
|
|
120
120
|
|
|
@@ -324,6 +324,7 @@ describe("execute test-plan command", () => {
|
|
|
324
324
|
|
|
325
325
|
const state = await readState(projectRoot);
|
|
326
326
|
expect(state.phases.prototype.test_execution.status).toBe("completed");
|
|
327
|
+
expect(state.phases.prototype.prototype_approved).toBe(true);
|
|
327
328
|
expect(state.updated_by).toBe("nvst:execute-test-plan");
|
|
328
329
|
});
|
|
329
330
|
|
|
@@ -537,6 +538,10 @@ describe("execute test-plan command", () => {
|
|
|
537
538
|
expect(rerunBatchPrompt).not.toContain("TC-US001-01");
|
|
538
539
|
});
|
|
539
540
|
|
|
541
|
+
// After retry, all pass -> prototype approved
|
|
542
|
+
const stateAfterRetry = await readState(projectRoot);
|
|
543
|
+
expect(stateAfterRetry.phases.prototype.prototype_approved).toBe(true);
|
|
544
|
+
|
|
540
545
|
const progressRaw = await readFile(
|
|
541
546
|
join(projectRoot, ".agents", "flow", "it_000005_test-execution-progress.json"),
|
|
542
547
|
"utf8",
|
|
@@ -1736,10 +1741,11 @@ describe("US-004: preserve report and state tracking compatibility", () => {
|
|
|
1736
1741
|
expect(stateSnapshots[0]!.status).toBe("in_progress");
|
|
1737
1742
|
expect(stateSnapshots[0]!.file).toBe("it_000005_test-execution-progress.json");
|
|
1738
1743
|
|
|
1739
|
-
// After execution (all passed): completed
|
|
1744
|
+
// After execution (all passed): completed and prototype approved
|
|
1740
1745
|
const finalState = await readState(projectRoot);
|
|
1741
1746
|
expect(finalState.phases.prototype.test_execution.status).toBe("completed");
|
|
1742
1747
|
expect(finalState.phases.prototype.test_execution.file).toBe("it_000005_test-execution-progress.json");
|
|
1748
|
+
expect(finalState.phases.prototype.prototype_approved).toBe(true);
|
|
1743
1749
|
expect(finalState.updated_by).toBe("nvst:execute-test-plan");
|
|
1744
1750
|
});
|
|
1745
1751
|
|
|
@@ -1779,6 +1785,7 @@ describe("US-004: preserve report and state tracking compatibility", () => {
|
|
|
1779
1785
|
|
|
1780
1786
|
const finalState = await readState(projectRoot);
|
|
1781
1787
|
expect(finalState.phases.prototype.test_execution.status).toBe("failed");
|
|
1788
|
+
expect(finalState.phases.prototype.prototype_approved).toBe(false);
|
|
1782
1789
|
expect(finalState.phases.prototype.test_execution.file).toBe("it_000005_test-execution-progress.json");
|
|
1783
1790
|
expect(finalState.updated_by).toBe("nvst:execute-test-plan");
|
|
1784
1791
|
});
|
|
@@ -11,12 +11,14 @@ import {
|
|
|
11
11
|
type AgentProvider,
|
|
12
12
|
type AgentResult,
|
|
13
13
|
} from "../agent";
|
|
14
|
+
import { assertGuardrail } from "../guardrail";
|
|
14
15
|
import { exists, FLOW_REL_DIR, readState, writeState } from "../state";
|
|
15
|
-
import { TestPlanSchema, type TestPlan } from "../../schemas/
|
|
16
|
+
import { TestPlanSchema, type TestPlan } from "../../scaffold/schemas/tmpl_test-plan";
|
|
16
17
|
import { extractJson } from "./create-issue";
|
|
17
18
|
|
|
18
19
|
export interface ExecuteTestPlanOptions {
|
|
19
20
|
provider: AgentProvider;
|
|
21
|
+
force?: boolean;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
const ExecutionPayloadSchema = z.object({
|
|
@@ -321,13 +323,15 @@ export async function runExecuteTestPlan(
|
|
|
321
323
|
const projectRoot = process.cwd();
|
|
322
324
|
const mergedDeps: ExecuteTestPlanDeps = { ...defaultDeps, ...deps };
|
|
323
325
|
const state = await readState(projectRoot);
|
|
326
|
+
const force = opts.force ?? false;
|
|
324
327
|
|
|
325
328
|
const tpGeneration = state.phases.prototype.tp_generation;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
329
|
+
await assertGuardrail(
|
|
330
|
+
state,
|
|
331
|
+
tpGeneration.status !== "created",
|
|
332
|
+
`Cannot execute test plan: prototype.tp_generation.status must be created. Current status: '${tpGeneration.status}'. Run \`bun nvst approve test-plan\` first.`,
|
|
333
|
+
{ force },
|
|
334
|
+
);
|
|
331
335
|
|
|
332
336
|
if (!tpGeneration.file) {
|
|
333
337
|
throw new Error("Cannot execute test plan: prototype.tp_generation.file is missing.");
|
|
@@ -710,6 +714,9 @@ export async function runExecuteTestPlan(
|
|
|
710
714
|
const hasFailedTests = progress.entries.some((entry) => entry.status === "failed");
|
|
711
715
|
state.phases.prototype.test_execution.status = hasFailedTests ? "failed" : "completed";
|
|
712
716
|
state.phases.prototype.test_execution.file = progressFileName;
|
|
717
|
+
if (!hasFailedTests) {
|
|
718
|
+
state.phases.prototype.prototype_approved = true;
|
|
719
|
+
}
|
|
713
720
|
state.last_updated = mergedDeps.nowFn().toISOString();
|
|
714
721
|
state.updated_by = "nvst:execute-test-plan";
|
|
715
722
|
await writeState(projectRoot, state);
|
|
@@ -2,26 +2,28 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { buildPrompt, invokeAgent, loadSkill, type AgentProvider } from "../agent";
|
|
5
|
+
import { assertGuardrail } from "../guardrail";
|
|
5
6
|
import { exists, readState, writeState } from "../state";
|
|
6
7
|
|
|
7
8
|
export interface RefineProjectContextOptions {
|
|
8
9
|
provider: AgentProvider;
|
|
9
10
|
challenge: boolean;
|
|
11
|
+
force?: boolean;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export async function runRefineProjectContext(opts: RefineProjectContextOptions): Promise<void> {
|
|
13
|
-
const { provider, challenge } = opts;
|
|
15
|
+
const { provider, challenge, force = false } = opts;
|
|
14
16
|
const projectRoot = process.cwd();
|
|
15
17
|
const state = await readState(projectRoot);
|
|
16
18
|
|
|
17
19
|
// US-003-AC01: Validate status is pending_approval or created
|
|
18
20
|
const projectContext = state.phases.prototype.project_context;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
await assertGuardrail(
|
|
22
|
+
state,
|
|
23
|
+
projectContext.status !== "pending_approval" && projectContext.status !== "created",
|
|
24
|
+
`Cannot refine project context from status '${projectContext.status}'. Expected pending_approval or created.`,
|
|
25
|
+
{ force },
|
|
26
|
+
);
|
|
25
27
|
|
|
26
28
|
// Validate file reference exists in state
|
|
27
29
|
const contextFile = projectContext.file;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import type { AgentResult } from "../agent";
|
|
7
|
+
import { readState, writeState } from "../state";
|
|
8
|
+
import { runRefineRefactorPlan } from "./refine-refactor-plan";
|
|
9
|
+
|
|
10
|
+
async function createProjectRoot(): Promise<string> {
|
|
11
|
+
return mkdtemp(join(tmpdir(), "nvst-refine-refactor-plan-"));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function withCwd<T>(cwd: string, fn: () => Promise<T>): Promise<T> {
|
|
15
|
+
const previous = process.cwd();
|
|
16
|
+
process.chdir(cwd);
|
|
17
|
+
try {
|
|
18
|
+
return await fn();
|
|
19
|
+
} finally {
|
|
20
|
+
process.chdir(previous);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function seedState(
|
|
25
|
+
projectRoot: string,
|
|
26
|
+
status: "pending" | "pending_approval" | "approved",
|
|
27
|
+
file: string | null,
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
await mkdir(join(projectRoot, ".agents", "flow"), { recursive: true });
|
|
30
|
+
|
|
31
|
+
await writeState(projectRoot, {
|
|
32
|
+
current_iteration: "000013",
|
|
33
|
+
current_phase: "refactor",
|
|
34
|
+
phases: {
|
|
35
|
+
define: {
|
|
36
|
+
requirement_definition: { status: "approved", file: "it_000013_product-requirement-document.md" },
|
|
37
|
+
prd_generation: { status: "completed", file: "it_000013_PRD.json" },
|
|
38
|
+
},
|
|
39
|
+
prototype: {
|
|
40
|
+
project_context: { status: "created", file: ".agents/PROJECT_CONTEXT.md" },
|
|
41
|
+
test_plan: { status: "created", file: "it_000013_test-plan.md" },
|
|
42
|
+
tp_generation: { status: "created", file: "it_000013_TEST-PLAN.json" },
|
|
43
|
+
prototype_build: { status: "created", file: "it_000013_progress.json" },
|
|
44
|
+
test_execution: { status: "completed", file: "it_000013_test-execution-report.json" },
|
|
45
|
+
prototype_approved: true,
|
|
46
|
+
},
|
|
47
|
+
refactor: {
|
|
48
|
+
evaluation_report: { status: "created", file: "it_000013_evaluation-report.md" },
|
|
49
|
+
refactor_plan: { status, file },
|
|
50
|
+
refactor_execution: { status: "pending", file: null },
|
|
51
|
+
changelog: { status: "pending", file: null },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
last_updated: "2026-02-26T00:00:00.000Z",
|
|
55
|
+
updated_by: "seed",
|
|
56
|
+
history: [],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const createdRoots: string[] = [];
|
|
61
|
+
|
|
62
|
+
afterEach(async () => {
|
|
63
|
+
await Promise.all(createdRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("refine refactor-plan command", () => {
|
|
67
|
+
test("registers refine refactor-plan command in CLI dispatch", async () => {
|
|
68
|
+
const source = await readFile(join(process.cwd(), "src", "cli.ts"), "utf8");
|
|
69
|
+
|
|
70
|
+
expect(source).toContain('import { runRefineRefactorPlan } from "./commands/refine-refactor-plan";');
|
|
71
|
+
expect(source).toContain('if (subcommand === "refactor-plan") {');
|
|
72
|
+
expect(source).toContain('const challenge = postForceArgs.includes("--challenge");');
|
|
73
|
+
expect(source).toContain("await runRefineRefactorPlan({ provider, challenge, force });");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("requires refactor.refactor_plan.status to be pending_approval", async () => {
|
|
77
|
+
const projectRoot = await createProjectRoot();
|
|
78
|
+
createdRoots.push(projectRoot);
|
|
79
|
+
await seedState(projectRoot, "approved", "it_000013_refactor-plan.md");
|
|
80
|
+
|
|
81
|
+
await withCwd(projectRoot, async () => {
|
|
82
|
+
await expect(
|
|
83
|
+
runRefineRefactorPlan(
|
|
84
|
+
{ provider: "codex", challenge: false },
|
|
85
|
+
{
|
|
86
|
+
loadSkillFn: async () => "unused",
|
|
87
|
+
invokeAgentFn: async () => ({ exitCode: 0, stdout: "", stderr: "" }),
|
|
88
|
+
},
|
|
89
|
+
),
|
|
90
|
+
).rejects.toThrow(
|
|
91
|
+
"Cannot refine refactor plan from status 'approved'. Expected pending_approval.",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("rejects when refactor.refactor_plan.file is missing", async () => {
|
|
97
|
+
const projectRoot = await createProjectRoot();
|
|
98
|
+
createdRoots.push(projectRoot);
|
|
99
|
+
await seedState(projectRoot, "pending_approval", null);
|
|
100
|
+
|
|
101
|
+
await withCwd(projectRoot, async () => {
|
|
102
|
+
await expect(
|
|
103
|
+
runRefineRefactorPlan(
|
|
104
|
+
{ provider: "codex", challenge: false },
|
|
105
|
+
{
|
|
106
|
+
loadSkillFn: async () => "unused",
|
|
107
|
+
invokeAgentFn: async () => ({ exitCode: 0, stdout: "", stderr: "" }),
|
|
108
|
+
},
|
|
109
|
+
),
|
|
110
|
+
).rejects.toThrow("Cannot refine refactor plan: refactor.refactor_plan.file is missing.");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("rejects when refactor plan file does not exist on disk", async () => {
|
|
115
|
+
const projectRoot = await createProjectRoot();
|
|
116
|
+
createdRoots.push(projectRoot);
|
|
117
|
+
await seedState(projectRoot, "pending_approval", "it_000013_refactor-plan.md");
|
|
118
|
+
|
|
119
|
+
await withCwd(projectRoot, async () => {
|
|
120
|
+
await expect(
|
|
121
|
+
runRefineRefactorPlan(
|
|
122
|
+
{ provider: "codex", challenge: false },
|
|
123
|
+
{
|
|
124
|
+
loadSkillFn: async () => "unused",
|
|
125
|
+
invokeAgentFn: async () => ({ exitCode: 0, stdout: "", stderr: "" }),
|
|
126
|
+
},
|
|
127
|
+
),
|
|
128
|
+
).rejects.toThrow("Cannot refine refactor plan: file not found at");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("loads refine-refactor-plan skill, reads file context, invokes interactively, and does not mutate state", async () => {
|
|
133
|
+
const projectRoot = await createProjectRoot();
|
|
134
|
+
createdRoots.push(projectRoot);
|
|
135
|
+
await seedState(projectRoot, "pending_approval", "it_000013_refactor-plan.md");
|
|
136
|
+
|
|
137
|
+
const refactorPlanPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-plan.md");
|
|
138
|
+
await writeFile(refactorPlanPath, "# Current Refactor Plan\n- Refactor module A\n", "utf8");
|
|
139
|
+
|
|
140
|
+
let loadedSkill = "";
|
|
141
|
+
let invocation: { interactive: boolean | undefined; prompt: string } | undefined;
|
|
142
|
+
const stateBefore = JSON.stringify(await readState(projectRoot));
|
|
143
|
+
|
|
144
|
+
await withCwd(projectRoot, async () => {
|
|
145
|
+
await runRefineRefactorPlan(
|
|
146
|
+
{ provider: "codex", challenge: false },
|
|
147
|
+
{
|
|
148
|
+
loadSkillFn: async (_root, skillName) => {
|
|
149
|
+
loadedSkill = skillName;
|
|
150
|
+
return "Refine refactor plan skill";
|
|
151
|
+
},
|
|
152
|
+
invokeAgentFn: async (options): Promise<AgentResult> => {
|
|
153
|
+
invocation = {
|
|
154
|
+
interactive: options.interactive,
|
|
155
|
+
prompt: options.prompt,
|
|
156
|
+
};
|
|
157
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(loadedSkill).toBe("refine-refactor-plan");
|
|
164
|
+
if (invocation === undefined) {
|
|
165
|
+
throw new Error("Agent invocation was not captured");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
expect(invocation.interactive).toBe(true);
|
|
169
|
+
expect(invocation.prompt).toContain("### current_iteration");
|
|
170
|
+
expect(invocation.prompt).toContain("000013");
|
|
171
|
+
expect(invocation.prompt).toContain("### refactor_plan_file");
|
|
172
|
+
expect(invocation.prompt).toContain("it_000013_refactor-plan.md");
|
|
173
|
+
expect(invocation.prompt).toContain("### refactor_plan_content");
|
|
174
|
+
expect(invocation.prompt).toContain("# Current Refactor Plan");
|
|
175
|
+
|
|
176
|
+
const stateAfter = JSON.stringify(await readState(projectRoot));
|
|
177
|
+
expect(stateAfter).toBe(stateBefore);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("passes mode=challenger in prompt context when challenge mode is enabled", async () => {
|
|
181
|
+
const projectRoot = await createProjectRoot();
|
|
182
|
+
createdRoots.push(projectRoot);
|
|
183
|
+
await seedState(projectRoot, "pending_approval", "it_000013_refactor-plan.md");
|
|
184
|
+
|
|
185
|
+
const refactorPlanPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-plan.md");
|
|
186
|
+
await writeFile(refactorPlanPath, "# Current Refactor Plan\n- Refactor module B\n", "utf8");
|
|
187
|
+
|
|
188
|
+
let invocationPrompt = "";
|
|
189
|
+
const stateBefore = JSON.stringify(await readState(projectRoot));
|
|
190
|
+
|
|
191
|
+
await withCwd(projectRoot, async () => {
|
|
192
|
+
await runRefineRefactorPlan(
|
|
193
|
+
{ provider: "codex", challenge: true },
|
|
194
|
+
{
|
|
195
|
+
loadSkillFn: async () => "Refine refactor plan skill",
|
|
196
|
+
invokeAgentFn: async (options): Promise<AgentResult> => {
|
|
197
|
+
invocationPrompt = options.prompt;
|
|
198
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(invocationPrompt).toContain("### mode");
|
|
205
|
+
expect(invocationPrompt).toContain("challenger");
|
|
206
|
+
|
|
207
|
+
const stateAfter = JSON.stringify(await readState(projectRoot));
|
|
208
|
+
expect(stateAfter).toBe(stateBefore);
|
|
209
|
+
});
|
|
210
|
+
});
|