@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
|
@@ -1,59 +1,142 @@
|
|
|
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";
|
|
18
|
+
import { assertGuardrail } from "../guardrail";
|
|
19
|
+
import { defaultReadLine } from "../readline";
|
|
20
|
+
import { idsMatchExactly, sortedValues } from "../progress-utils";
|
|
13
21
|
import { exists, FLOW_REL_DIR, readState, writeState } from "../state";
|
|
22
|
+
import { writeJsonArtifact, type WriteJsonArtifactFn } from "../write-json-artifact";
|
|
14
23
|
|
|
15
24
|
export interface CreatePrototypeOptions {
|
|
16
25
|
provider: AgentProvider;
|
|
17
26
|
iterations?: number;
|
|
18
27
|
retryOnFail?: number;
|
|
19
28
|
stopOnCritical?: boolean;
|
|
29
|
+
force?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
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`);
|
|
20
85
|
}
|
|
21
86
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
status: z.enum(["pending", "failed", "completed"]),
|
|
25
|
-
attempt_count: z.number().int().nonnegative(),
|
|
26
|
-
last_agent_exit_code: z.number().int().nullable(),
|
|
27
|
-
quality_checks: z.array(
|
|
28
|
-
z.object({
|
|
29
|
-
command: z.string(),
|
|
30
|
-
exit_code: z.number().int(),
|
|
31
|
-
}),
|
|
32
|
-
),
|
|
33
|
-
last_error_summary: z.string(),
|
|
34
|
-
updated_at: z.string(),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
export const PrototypeProgressSchema = z.object({
|
|
38
|
-
entries: z.array(ProgressEntrySchema),
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
function sortedValues(values: string[]): string[] {
|
|
42
|
-
return [...values].sort((a, b) => a.localeCompare(b));
|
|
87
|
+
function defaultIsTTY(): boolean {
|
|
88
|
+
return process.stdin.isTTY === true;
|
|
43
89
|
}
|
|
44
90
|
|
|
45
|
-
function
|
|
46
|
-
|
|
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()) {
|
|
47
98
|
return false;
|
|
48
99
|
}
|
|
49
100
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
101
|
+
writeFn(question);
|
|
102
|
+
|
|
103
|
+
let line: string | null;
|
|
104
|
+
try {
|
|
105
|
+
line = await readLineFn();
|
|
106
|
+
} catch {
|
|
107
|
+
line = null;
|
|
108
|
+
}
|
|
109
|
+
|
|
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()}`);
|
|
54
117
|
}
|
|
55
118
|
|
|
56
|
-
|
|
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
|
+
}
|
|
57
140
|
}
|
|
58
141
|
|
|
59
142
|
function parseQualityChecks(projectContextContent: string): string[] {
|
|
@@ -103,9 +186,14 @@ function parseQualityChecks(projectContextContent: string): string[] {
|
|
|
103
186
|
.filter((line) => line.length > 0);
|
|
104
187
|
}
|
|
105
188
|
|
|
106
|
-
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 };
|
|
107
194
|
const projectRoot = process.cwd();
|
|
108
195
|
const state = await readState(projectRoot);
|
|
196
|
+
const force = opts.force ?? false;
|
|
109
197
|
|
|
110
198
|
if (opts.iterations !== undefined && (!Number.isInteger(opts.iterations) || opts.iterations < 1)) {
|
|
111
199
|
throw new Error(
|
|
@@ -148,6 +236,8 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
148
236
|
);
|
|
149
237
|
}
|
|
150
238
|
|
|
239
|
+
let prePrototypeCommitDone = false;
|
|
240
|
+
|
|
151
241
|
if (state.current_phase === "define") {
|
|
152
242
|
if (
|
|
153
243
|
state.phases.define.prd_generation.status === "completed" &&
|
|
@@ -160,39 +250,59 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
160
250
|
);
|
|
161
251
|
}
|
|
162
252
|
if (workingTreeBeforeTransition.stdout.toString().trim().length > 0) {
|
|
163
|
-
|
|
164
|
-
|
|
253
|
+
const shouldProceed = await mergedDeps.promptDirtyTreeCommitFn(
|
|
254
|
+
DIRTY_TREE_COMMIT_PROMPT,
|
|
165
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;
|
|
166
262
|
}
|
|
167
263
|
state.current_phase = "prototype";
|
|
168
264
|
await writeState(projectRoot, state);
|
|
169
265
|
} else {
|
|
170
|
-
|
|
266
|
+
await assertGuardrail(
|
|
267
|
+
state,
|
|
268
|
+
true,
|
|
171
269
|
"Cannot create prototype: current_phase is define and prerequisites are not met. Complete define phase and run `bun nvst create project-context --agent <provider>` then `bun nvst approve project-context` first.",
|
|
270
|
+
{ force },
|
|
172
271
|
);
|
|
173
272
|
}
|
|
174
273
|
} else if (state.current_phase !== "prototype") {
|
|
175
|
-
|
|
274
|
+
await assertGuardrail(
|
|
275
|
+
state,
|
|
276
|
+
true,
|
|
176
277
|
"Cannot create prototype: current_phase must be define (with approved PRD) or prototype. Complete define phase and run `bun nvst create project-context --agent <provider>` then `bun nvst approve project-context` first.",
|
|
278
|
+
{ force },
|
|
177
279
|
);
|
|
178
280
|
}
|
|
179
281
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
282
|
+
await assertGuardrail(
|
|
283
|
+
state,
|
|
284
|
+
state.phases.prototype.project_context.status !== "created",
|
|
285
|
+
"Cannot create prototype: prototype.project_context.status must be created. Run `bun nvst create project-context --agent <provider>` and `bun nvst approve project-context` first.",
|
|
286
|
+
{ force },
|
|
287
|
+
);
|
|
185
288
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
+
}
|
|
196
306
|
}
|
|
197
307
|
|
|
198
308
|
const branchName = `feature/it_${iteration}`;
|
|
@@ -220,7 +330,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
220
330
|
const progressFileName = `it_${iteration}_progress.json`;
|
|
221
331
|
const progressPath = join(projectRoot, FLOW_REL_DIR, progressFileName);
|
|
222
332
|
const storyIds = sortedValues(prdValidation.data.userStories.map((story) => story.id));
|
|
223
|
-
let progressData:
|
|
333
|
+
let progressData: PrototypeProgress;
|
|
224
334
|
|
|
225
335
|
if (await exists(progressPath)) {
|
|
226
336
|
let parsedProgress: unknown;
|
|
@@ -263,7 +373,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
263
373
|
})),
|
|
264
374
|
};
|
|
265
375
|
|
|
266
|
-
await
|
|
376
|
+
await mergedDeps.writeJsonArtifactFn(progressPath, PrototypeProgressSchema, progress);
|
|
267
377
|
progressData = progress;
|
|
268
378
|
}
|
|
269
379
|
|
|
@@ -273,7 +383,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
273
383
|
});
|
|
274
384
|
|
|
275
385
|
if (eligibleStories.length === 0) {
|
|
276
|
-
|
|
386
|
+
mergedDeps.logFn("No pending or failed user stories to implement. Exiting without changes.");
|
|
277
387
|
return;
|
|
278
388
|
}
|
|
279
389
|
|
|
@@ -285,7 +395,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
285
395
|
|
|
286
396
|
let skillTemplate: string;
|
|
287
397
|
try {
|
|
288
|
-
skillTemplate = await
|
|
398
|
+
skillTemplate = await mergedDeps.loadSkillFn(projectRoot, "implement-user-story");
|
|
289
399
|
} catch {
|
|
290
400
|
throw new Error(
|
|
291
401
|
"Required skill missing: expected .agents/skills/implement-user-story/SKILL.md.",
|
|
@@ -324,7 +434,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
324
434
|
user_story: JSON.stringify(story, null, 2),
|
|
325
435
|
});
|
|
326
436
|
|
|
327
|
-
const agentResult = await
|
|
437
|
+
const agentResult = await mergedDeps.invokeAgentFn({
|
|
328
438
|
provider: opts.provider,
|
|
329
439
|
prompt,
|
|
330
440
|
cwd: projectRoot,
|
|
@@ -358,7 +468,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
358
468
|
entry.last_error_summary = "Agent or quality check failed";
|
|
359
469
|
}
|
|
360
470
|
|
|
361
|
-
await
|
|
471
|
+
await mergedDeps.writeJsonArtifactFn(progressPath, PrototypeProgressSchema, progressData);
|
|
362
472
|
|
|
363
473
|
if (allPassed) {
|
|
364
474
|
const commitMessage = `feat: implement ${story.id} - ${story.title}`;
|
|
@@ -371,9 +481,9 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
371
481
|
entry.status = "failed";
|
|
372
482
|
entry.last_error_summary = "Git commit failed";
|
|
373
483
|
entry.updated_at = new Date().toISOString();
|
|
374
|
-
await
|
|
484
|
+
await mergedDeps.writeJsonArtifactFn(progressPath, PrototypeProgressSchema, progressData);
|
|
375
485
|
|
|
376
|
-
|
|
486
|
+
mergedDeps.logFn(
|
|
377
487
|
`iteration=it_${iteration} story=${story.id} attempt=${entry.attempt_count} outcome=commit_failed`,
|
|
378
488
|
);
|
|
379
489
|
|
|
@@ -381,7 +491,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
381
491
|
haltedByCritical = true;
|
|
382
492
|
}
|
|
383
493
|
} else {
|
|
384
|
-
|
|
494
|
+
mergedDeps.logFn(
|
|
385
495
|
`iteration=it_${iteration} story=${story.id} attempt=${entry.attempt_count} outcome=passed`,
|
|
386
496
|
);
|
|
387
497
|
}
|
|
@@ -389,7 +499,7 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
389
499
|
break;
|
|
390
500
|
}
|
|
391
501
|
|
|
392
|
-
|
|
502
|
+
mergedDeps.logFn(
|
|
393
503
|
`iteration=it_${iteration} story=${story.id} attempt=${entry.attempt_count} outcome=failed`,
|
|
394
504
|
);
|
|
395
505
|
|
|
@@ -413,13 +523,25 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
413
523
|
await writeState(projectRoot, state);
|
|
414
524
|
|
|
415
525
|
if (storiesAttempted === 0) {
|
|
416
|
-
|
|
526
|
+
mergedDeps.logFn("No user stories attempted.");
|
|
417
527
|
return;
|
|
418
528
|
}
|
|
419
529
|
|
|
420
530
|
if (allCompleted) {
|
|
421
|
-
|
|
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.");
|
|
422
544
|
} else {
|
|
423
|
-
|
|
545
|
+
mergedDeps.logFn("Prototype implementation paused with remaining pending or failed stories.");
|
|
424
546
|
}
|
|
425
547
|
}
|
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
type AgentProvider,
|
|
11
11
|
type AgentResult,
|
|
12
12
|
} from "../agent";
|
|
13
|
+
import { assertGuardrail } from "../guardrail";
|
|
13
14
|
import { exists, FLOW_REL_DIR, readState, writeState } from "../state";
|
|
14
|
-
import { TestPlanSchema, type TestPlan } from "../../schemas/
|
|
15
|
+
import { TestPlanSchema, type TestPlan } from "../../scaffold/schemas/tmpl_test-plan";
|
|
15
16
|
|
|
16
17
|
export interface CreateTestPlanOptions {
|
|
17
18
|
provider: AgentProvider;
|
|
@@ -155,11 +156,12 @@ export async function runCreateTestPlan(
|
|
|
155
156
|
const state = await readState(projectRoot);
|
|
156
157
|
const mergedDeps: CreateTestPlanDeps = { ...defaultDeps, ...deps };
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
await assertGuardrail(
|
|
160
|
+
state,
|
|
161
|
+
state.phases.prototype.project_context.status !== "created",
|
|
162
|
+
"Cannot create test plan: prototype.project_context.status must be created. Run `bun nvst approve project-context` first.",
|
|
163
|
+
{ force: opts.force },
|
|
164
|
+
);
|
|
163
165
|
|
|
164
166
|
const iteration = state.current_iteration;
|
|
165
167
|
const fileName = `it_${iteration}_test-plan.md`;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, mkdir, readFile, rm } 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 { runDefineRefactorPlan } from "./define-refactor-plan";
|
|
9
|
+
|
|
10
|
+
async function createProjectRoot(): Promise<string> {
|
|
11
|
+
return mkdtemp(join(tmpdir(), "nvst-define-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
|
+
stateOverrides: {
|
|
27
|
+
currentPhase?: "define" | "prototype" | "refactor";
|
|
28
|
+
refactorPlanStatus?: "pending" | "pending_approval" | "approved";
|
|
29
|
+
prototypeApproved?: boolean;
|
|
30
|
+
} = {},
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
const currentPhase = stateOverrides.currentPhase ?? "refactor";
|
|
33
|
+
const refactorPlanStatus = stateOverrides.refactorPlanStatus ?? "pending";
|
|
34
|
+
const prototypeApproved = stateOverrides.prototypeApproved ?? true;
|
|
35
|
+
|
|
36
|
+
await mkdir(join(projectRoot, ".agents", "flow"), { recursive: true });
|
|
37
|
+
|
|
38
|
+
await writeState(projectRoot, {
|
|
39
|
+
current_iteration: "000013",
|
|
40
|
+
current_phase: currentPhase,
|
|
41
|
+
phases: {
|
|
42
|
+
define: {
|
|
43
|
+
requirement_definition: { status: "approved", file: "it_000013_product-requirement-document.md" },
|
|
44
|
+
prd_generation: { status: "completed", file: "it_000013_PRD.json" },
|
|
45
|
+
},
|
|
46
|
+
prototype: {
|
|
47
|
+
project_context: { status: "created", file: ".agents/PROJECT_CONTEXT.md" },
|
|
48
|
+
test_plan: { status: "created", file: "it_000013_test-plan.md" },
|
|
49
|
+
tp_generation: { status: "created", file: "it_000013_TEST-PLAN.json" },
|
|
50
|
+
prototype_build: { status: "created", file: "it_000013_progress.json" },
|
|
51
|
+
test_execution: { status: "completed", file: "it_000013_test-execution-report.json" },
|
|
52
|
+
prototype_approved: prototypeApproved,
|
|
53
|
+
},
|
|
54
|
+
refactor: {
|
|
55
|
+
evaluation_report: { status: "pending", file: null },
|
|
56
|
+
refactor_plan: { status: refactorPlanStatus, file: null },
|
|
57
|
+
refactor_execution: { status: "pending", file: null },
|
|
58
|
+
changelog: { status: "pending", file: null },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
last_updated: "2026-02-26T00:00:00.000Z",
|
|
62
|
+
updated_by: "seed",
|
|
63
|
+
history: [],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const createdRoots: string[] = [];
|
|
68
|
+
|
|
69
|
+
afterEach(async () => {
|
|
70
|
+
await Promise.all(createdRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("define refactor-plan command", () => {
|
|
74
|
+
test("registers define refactor-plan command in CLI dispatch", async () => {
|
|
75
|
+
const source = await readFile(join(process.cwd(), "src", "cli.ts"), "utf8");
|
|
76
|
+
|
|
77
|
+
expect(source).toContain('import { runDefineRefactorPlan } from "./commands/define-refactor-plan";');
|
|
78
|
+
expect(source).toContain('if (subcommand === "refactor-plan") {');
|
|
79
|
+
expect(source).toContain("await runDefineRefactorPlan({ provider, force });");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("rejects when prototype_approved is false", async () => {
|
|
83
|
+
const projectRoot = await createProjectRoot();
|
|
84
|
+
createdRoots.push(projectRoot);
|
|
85
|
+
await seedState(projectRoot, { prototypeApproved: false });
|
|
86
|
+
|
|
87
|
+
await withCwd(projectRoot, async () => {
|
|
88
|
+
await expect(runDefineRefactorPlan({ provider: "codex" })).rejects.toThrow(
|
|
89
|
+
"Cannot define refactor plan: phases.prototype.prototype_approved must be true",
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("allows bypassing prototype_approved guard with force", async () => {
|
|
95
|
+
const projectRoot = await createProjectRoot();
|
|
96
|
+
createdRoots.push(projectRoot);
|
|
97
|
+
await seedState(projectRoot, { prototypeApproved: false });
|
|
98
|
+
|
|
99
|
+
await withCwd(projectRoot, async () => {
|
|
100
|
+
await runDefineRefactorPlan(
|
|
101
|
+
{ provider: "codex", force: true },
|
|
102
|
+
{
|
|
103
|
+
loadSkillFn: async () => "Refactor planning instructions",
|
|
104
|
+
invokeAgentFn: async () => ({ exitCode: 0, stdout: "", stderr: "" }),
|
|
105
|
+
nowFn: () => new Date("2026-02-26T10:00:00.000Z"),
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const state = await readState(projectRoot);
|
|
111
|
+
expect(state.phases.refactor.evaluation_report.status).toBe("created");
|
|
112
|
+
expect(state.phases.refactor.refactor_plan.status).toBe("pending_approval");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("rejects when current_phase is define", async () => {
|
|
116
|
+
const projectRoot = await createProjectRoot();
|
|
117
|
+
createdRoots.push(projectRoot);
|
|
118
|
+
await seedState(projectRoot, { currentPhase: "define", prototypeApproved: true });
|
|
119
|
+
|
|
120
|
+
await withCwd(projectRoot, async () => {
|
|
121
|
+
await expect(runDefineRefactorPlan({ provider: "codex" })).rejects.toThrow(
|
|
122
|
+
"Cannot define refactor plan: current_phase must be 'prototype' or 'refactor'",
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("accepts when current_phase is prototype and prototype_approved is true, transitions to refactor", async () => {
|
|
128
|
+
const projectRoot = await createProjectRoot();
|
|
129
|
+
createdRoots.push(projectRoot);
|
|
130
|
+
await seedState(projectRoot, { currentPhase: "prototype" });
|
|
131
|
+
|
|
132
|
+
await withCwd(projectRoot, async () => {
|
|
133
|
+
await runDefineRefactorPlan(
|
|
134
|
+
{ provider: "codex" },
|
|
135
|
+
{
|
|
136
|
+
loadSkillFn: async () => "Refactor planning instructions",
|
|
137
|
+
invokeAgentFn: async () => ({ exitCode: 0, stdout: "", stderr: "" }),
|
|
138
|
+
nowFn: () => new Date("2026-02-26T10:00:00.000Z"),
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const state = await readState(projectRoot);
|
|
144
|
+
expect(state.current_phase).toBe("refactor");
|
|
145
|
+
expect(state.phases.refactor.evaluation_report.status).toBe("created");
|
|
146
|
+
expect(state.phases.refactor.refactor_plan.status).toBe("pending_approval");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("rejects when refactor.refactor_plan.status is not pending", async () => {
|
|
150
|
+
const projectRoot = await createProjectRoot();
|
|
151
|
+
createdRoots.push(projectRoot);
|
|
152
|
+
await seedState(projectRoot, { refactorPlanStatus: "approved" });
|
|
153
|
+
|
|
154
|
+
await withCwd(projectRoot, async () => {
|
|
155
|
+
await expect(runDefineRefactorPlan({ provider: "codex" })).rejects.toThrow(
|
|
156
|
+
"Cannot define refactor plan from status 'approved'. Expected pending.",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("loads plan-refactor skill, invokes interactive agent, and persists pending_approval state", async () => {
|
|
162
|
+
const projectRoot = await createProjectRoot();
|
|
163
|
+
createdRoots.push(projectRoot);
|
|
164
|
+
await seedState(projectRoot);
|
|
165
|
+
|
|
166
|
+
let loadedSkill = "";
|
|
167
|
+
let invocation: { interactive: boolean | undefined; prompt: string } | undefined;
|
|
168
|
+
|
|
169
|
+
await withCwd(projectRoot, async () => {
|
|
170
|
+
await runDefineRefactorPlan(
|
|
171
|
+
{ provider: "codex" },
|
|
172
|
+
{
|
|
173
|
+
loadSkillFn: async (_root, skillName) => {
|
|
174
|
+
loadedSkill = skillName;
|
|
175
|
+
return "Refactor planning instructions from SKILL.md";
|
|
176
|
+
},
|
|
177
|
+
invokeAgentFn: async (options): Promise<AgentResult> => {
|
|
178
|
+
invocation = {
|
|
179
|
+
interactive: options.interactive,
|
|
180
|
+
prompt: options.prompt,
|
|
181
|
+
};
|
|
182
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
183
|
+
},
|
|
184
|
+
nowFn: () => new Date("2026-02-26T10:00:00.000Z"),
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(loadedSkill).toBe("plan-refactor");
|
|
190
|
+
if (invocation === undefined) {
|
|
191
|
+
throw new Error("Agent invocation was not captured");
|
|
192
|
+
}
|
|
193
|
+
expect(invocation.interactive).toBe(true);
|
|
194
|
+
expect(invocation.prompt).toContain("Refactor planning instructions from SKILL.md");
|
|
195
|
+
expect(invocation.prompt).toContain("### current_iteration");
|
|
196
|
+
expect(invocation.prompt).toContain("000013");
|
|
197
|
+
|
|
198
|
+
const state = await readState(projectRoot);
|
|
199
|
+
expect(state.phases.refactor.evaluation_report.status).toBe("created");
|
|
200
|
+
expect(state.phases.refactor.evaluation_report.file).toBe(
|
|
201
|
+
"it_000013_evaluation-report.md",
|
|
202
|
+
);
|
|
203
|
+
expect(state.phases.refactor.refactor_plan.status).toBe("pending_approval");
|
|
204
|
+
expect(state.phases.refactor.refactor_plan.file).toBe("it_000013_refactor-plan.md");
|
|
205
|
+
expect(state.last_updated).toBe("2026-02-26T10:00:00.000Z");
|
|
206
|
+
expect(state.updated_by).toBe("nvst:define-refactor-plan");
|
|
207
|
+
});
|
|
208
|
+
});
|