@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
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { $ } from "bun";
|
|
4
4
|
|
|
5
5
|
import { CLI_PATH } from "../cli-path";
|
|
6
|
+
import { assertGuardrail } from "../guardrail";
|
|
6
7
|
import type { Prd } from "../../scaffold/schemas/tmpl_prd";
|
|
7
8
|
import { exists, readState, writeState, FLOW_REL_DIR } from "../state";
|
|
8
9
|
|
|
@@ -154,17 +155,75 @@ function parsePrd(markdown: string): Prd {
|
|
|
154
155
|
// Main command
|
|
155
156
|
// ---------------------------------------------------------------------------
|
|
156
157
|
|
|
157
|
-
|
|
158
|
+
interface WriteJsonResult {
|
|
159
|
+
exitCode: number;
|
|
160
|
+
stderr: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface ApproveRequirementDeps {
|
|
164
|
+
existsFn: (path: string) => Promise<boolean>;
|
|
165
|
+
invokeWriteJsonFn: (
|
|
166
|
+
projectRoot: string,
|
|
167
|
+
schemaName: string,
|
|
168
|
+
outPath: string,
|
|
169
|
+
data: string,
|
|
170
|
+
) => Promise<WriteJsonResult>;
|
|
171
|
+
nowFn: () => Date;
|
|
172
|
+
readFileFn: typeof readFile;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function runWriteJsonCommand(
|
|
176
|
+
projectRoot: string,
|
|
177
|
+
schemaName: string,
|
|
178
|
+
outPath: string,
|
|
179
|
+
data: string,
|
|
180
|
+
): Promise<WriteJsonResult> {
|
|
181
|
+
const result =
|
|
182
|
+
await $`bun ${CLI_PATH} write-json --schema ${schemaName} --out ${outPath} --data ${data}`
|
|
183
|
+
.cwd(projectRoot)
|
|
184
|
+
.nothrow()
|
|
185
|
+
.quiet();
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
exitCode: result.exitCode,
|
|
189
|
+
stderr: result.stderr.toString().trim(),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const defaultDeps: ApproveRequirementDeps = {
|
|
194
|
+
existsFn: exists,
|
|
195
|
+
invokeWriteJsonFn: runWriteJsonCommand,
|
|
196
|
+
nowFn: () => new Date(),
|
|
197
|
+
readFileFn: readFile,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export async function runApproveRequirement(
|
|
201
|
+
optsOrDeps: { force?: boolean } | Partial<ApproveRequirementDeps> = {},
|
|
202
|
+
maybeDeps: Partial<ApproveRequirementDeps> = {},
|
|
203
|
+
): Promise<void> {
|
|
204
|
+
const isDepsArg =
|
|
205
|
+
typeof optsOrDeps === "object"
|
|
206
|
+
&& optsOrDeps !== null
|
|
207
|
+
&& (
|
|
208
|
+
"existsFn" in optsOrDeps
|
|
209
|
+
|| "invokeWriteJsonFn" in optsOrDeps
|
|
210
|
+
|| "nowFn" in optsOrDeps
|
|
211
|
+
|| "readFileFn" in optsOrDeps
|
|
212
|
+
);
|
|
213
|
+
const force = isDepsArg ? false : ((optsOrDeps as { force?: boolean }).force ?? false);
|
|
158
214
|
const projectRoot = process.cwd();
|
|
159
215
|
const state = await readState(projectRoot);
|
|
216
|
+
const deps = isDepsArg ? optsOrDeps : maybeDeps;
|
|
217
|
+
const mergedDeps: ApproveRequirementDeps = { ...defaultDeps, ...deps };
|
|
160
218
|
|
|
161
219
|
// --- US-001: Validate status ---
|
|
162
220
|
const requirementDefinition = state.phases.define.requirement_definition;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
221
|
+
await assertGuardrail(
|
|
222
|
+
state,
|
|
223
|
+
requirementDefinition.status !== "in_progress",
|
|
224
|
+
`Cannot approve requirement from status '${requirementDefinition.status}'. Expected in_progress.`,
|
|
225
|
+
{ force },
|
|
226
|
+
);
|
|
168
227
|
|
|
169
228
|
const requirementFile = requirementDefinition.file;
|
|
170
229
|
if (!requirementFile) {
|
|
@@ -172,26 +231,26 @@ export async function runApproveRequirement(): Promise<void> {
|
|
|
172
231
|
}
|
|
173
232
|
|
|
174
233
|
const requirementPath = join(projectRoot, FLOW_REL_DIR, requirementFile);
|
|
175
|
-
if (!(await
|
|
234
|
+
if (!(await mergedDeps.existsFn(requirementPath))) {
|
|
176
235
|
throw new Error(`Cannot approve requirement: file not found at ${requirementPath}`);
|
|
177
236
|
}
|
|
178
237
|
|
|
179
238
|
// --- US-002: Parse PRD markdown and generate JSON ---
|
|
180
|
-
const markdown = await
|
|
239
|
+
const markdown = await mergedDeps.readFileFn(requirementPath, "utf-8");
|
|
181
240
|
const prdData = parsePrd(markdown);
|
|
182
241
|
const prdJsonFileName = `it_${state.current_iteration}_PRD.json`;
|
|
183
242
|
const prdJsonRelPath = join(FLOW_REL_DIR, prdJsonFileName);
|
|
184
243
|
|
|
185
244
|
// Invoke write-json CLI to validate and write the PRD JSON
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
245
|
+
const result = await mergedDeps.invokeWriteJsonFn(
|
|
246
|
+
projectRoot,
|
|
247
|
+
"prd",
|
|
248
|
+
prdJsonRelPath,
|
|
249
|
+
JSON.stringify(prdData),
|
|
250
|
+
);
|
|
192
251
|
|
|
193
252
|
if (result.exitCode !== 0) {
|
|
194
|
-
const stderr = result.stderr.
|
|
253
|
+
const stderr = result.stderr.trim();
|
|
195
254
|
console.error("PRD JSON generation failed. Requirement remains in_progress.");
|
|
196
255
|
if (stderr) {
|
|
197
256
|
console.error(stderr);
|
|
@@ -207,7 +266,7 @@ export async function runApproveRequirement(): Promise<void> {
|
|
|
207
266
|
state.phases.define.prd_generation.status = "completed";
|
|
208
267
|
state.phases.define.prd_generation.file = prdJsonFileName;
|
|
209
268
|
|
|
210
|
-
state.last_updated =
|
|
269
|
+
state.last_updated = mergedDeps.nowFn().toISOString();
|
|
211
270
|
state.updated_by = "nvst:approve-requirement";
|
|
212
271
|
|
|
213
272
|
await writeState(projectRoot, state);
|
|
@@ -3,7 +3,7 @@ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
-
import { TestPlanSchema } from "../../schemas/
|
|
6
|
+
import { TestPlanSchema } from "../../scaffold/schemas/tmpl_test-plan";
|
|
7
7
|
import { readState, writeState } from "../state";
|
|
8
8
|
import { parseTestPlan, runApproveTestPlan } from "./approve-test-plan";
|
|
9
9
|
|
|
@@ -68,7 +68,7 @@ describe("approve test-plan command", () => {
|
|
|
68
68
|
|
|
69
69
|
expect(source).toContain('import { runApproveTestPlan } from "./commands/approve-test-plan";');
|
|
70
70
|
expect(source).toContain('if (subcommand === "test-plan") {');
|
|
71
|
-
expect(source).toContain("await runApproveTestPlan();");
|
|
71
|
+
expect(source).toContain("await runApproveTestPlan({ force });");
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
test("requires test_plan.status to be pending_approval", async () => {
|
|
@@ -3,7 +3,8 @@ import { join } from "node:path";
|
|
|
3
3
|
import { $ } from "bun";
|
|
4
4
|
|
|
5
5
|
import { CLI_PATH } from "../cli-path";
|
|
6
|
-
import
|
|
6
|
+
import { assertGuardrail } from "../guardrail";
|
|
7
|
+
import type { TestPlan } from "../../scaffold/schemas/tmpl_test-plan";
|
|
7
8
|
import { exists, FLOW_REL_DIR, readState, writeState } from "../state";
|
|
8
9
|
|
|
9
10
|
interface WriteJsonResult {
|
|
@@ -145,18 +146,31 @@ async function runWriteJsonCommand(
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
export async function runApproveTestPlan(
|
|
148
|
-
|
|
149
|
+
optsOrDeps: { force?: boolean } | Partial<ApproveTestPlanDeps> = {},
|
|
150
|
+
maybeDeps: Partial<ApproveTestPlanDeps> = {},
|
|
149
151
|
): Promise<void> {
|
|
152
|
+
const isDepsArg =
|
|
153
|
+
typeof optsOrDeps === "object"
|
|
154
|
+
&& optsOrDeps !== null
|
|
155
|
+
&& (
|
|
156
|
+
"existsFn" in optsOrDeps
|
|
157
|
+
|| "invokeWriteJsonFn" in optsOrDeps
|
|
158
|
+
|| "nowFn" in optsOrDeps
|
|
159
|
+
|| "readFileFn" in optsOrDeps
|
|
160
|
+
);
|
|
161
|
+
const force = isDepsArg ? false : ((optsOrDeps as { force?: boolean }).force ?? false);
|
|
150
162
|
const projectRoot = process.cwd();
|
|
151
163
|
const state = await readState(projectRoot);
|
|
164
|
+
const deps = isDepsArg ? optsOrDeps : maybeDeps;
|
|
152
165
|
const mergedDeps: ApproveTestPlanDeps = { ...defaultDeps, ...deps };
|
|
153
166
|
|
|
154
167
|
const testPlan = state.phases.prototype.test_plan;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
await assertGuardrail(
|
|
169
|
+
state,
|
|
170
|
+
testPlan.status !== "pending_approval",
|
|
171
|
+
`Cannot approve test plan from status '${testPlan.status}'. Expected pending_approval.`,
|
|
172
|
+
{ force },
|
|
173
|
+
);
|
|
160
174
|
|
|
161
175
|
const testPlanFile = testPlan.file;
|
|
162
176
|
if (!testPlanFile) {
|
|
@@ -420,8 +420,8 @@ describe("create issue --test-execution-report CLI integration", () => {
|
|
|
420
420
|
const exitCode = await proc.exited;
|
|
421
421
|
const stderr = await new Response(proc.stderr).text();
|
|
422
422
|
expect(stderr).not.toContain("Missing --agent");
|
|
423
|
-
if (exitCode !== 0) {
|
|
424
|
-
expect(stderr).
|
|
423
|
+
if (exitCode !== 0 && stderr) {
|
|
424
|
+
expect(stderr).toMatch(/test-execution-results|not found|Test execution results/);
|
|
425
425
|
}
|
|
426
426
|
});
|
|
427
427
|
|
|
@@ -7,44 +7,50 @@ import {
|
|
|
7
7
|
loadSkill,
|
|
8
8
|
type AgentProvider,
|
|
9
9
|
} from "../agent";
|
|
10
|
+
import { assertGuardrail } from "../guardrail";
|
|
10
11
|
import { exists, readState, writeState } from "../state";
|
|
11
12
|
|
|
12
13
|
export interface CreateProjectContextOptions {
|
|
13
14
|
provider: AgentProvider;
|
|
14
15
|
mode: "strict" | "yolo";
|
|
16
|
+
force?: boolean;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export async function runCreateProjectContext(opts: CreateProjectContextOptions): Promise<void> {
|
|
18
|
-
const { provider, mode } = opts;
|
|
20
|
+
const { provider, mode, force = false } = opts;
|
|
19
21
|
const projectRoot = process.cwd();
|
|
20
22
|
const state = await readState(projectRoot);
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
await assertGuardrail(
|
|
25
|
+
state,
|
|
26
|
+
state.phases.define.prd_generation.status !== "completed",
|
|
27
|
+
"Cannot create project context: define.prd_generation must be completed first.",
|
|
28
|
+
{ force },
|
|
29
|
+
);
|
|
27
30
|
|
|
28
31
|
const projectContext = state.phases.prototype.project_context;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
32
|
+
await assertGuardrail(
|
|
33
|
+
state,
|
|
34
|
+
projectContext.status === "pending_approval",
|
|
35
|
+
"Cannot create project context: project context is pending approval. " +
|
|
36
|
+
"Run `bun nvst approve project-context` or `bun nvst refine project-context` first.",
|
|
37
|
+
{ force },
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
await assertGuardrail(
|
|
41
|
+
state,
|
|
42
|
+
projectContext.status === "created",
|
|
43
|
+
"Cannot create project context: project context already exists. " +
|
|
44
|
+
"Use `bun nvst refine project-context` to iterate on it.",
|
|
45
|
+
{ force },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
await assertGuardrail(
|
|
49
|
+
state,
|
|
50
|
+
projectContext.status !== "pending",
|
|
51
|
+
`Cannot create project context from status '${projectContext.status}'. Expected pending.`,
|
|
52
|
+
{ force },
|
|
53
|
+
);
|
|
48
54
|
|
|
49
55
|
const prdFile = state.phases.define.prd_generation.file;
|
|
50
56
|
if (!prdFile) {
|
|
@@ -63,6 +63,20 @@ async function seedState(projectRoot: string, state: State): Promise<void> {
|
|
|
63
63
|
await writeState(projectRoot, state);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
const MINIMAL_PRD = {
|
|
67
|
+
goals: [] as string[],
|
|
68
|
+
userStories: [{ id: "US-001", title: "T", description: "D", acceptanceCriteria: [{ id: "AC1", text: "T" }] }],
|
|
69
|
+
functionalRequirements: [] as Array<{ id?: string; description: string }>,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
async function seedPrd(projectRoot: string, iteration: string): Promise<void> {
|
|
73
|
+
await writeFile(
|
|
74
|
+
join(projectRoot, ".agents", "flow", `it_${iteration}_PRD.json`),
|
|
75
|
+
JSON.stringify(MINIMAL_PRD),
|
|
76
|
+
"utf8",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
66
80
|
const createdRoots: string[] = [];
|
|
67
81
|
|
|
68
82
|
afterEach(async () => {
|
|
@@ -73,7 +87,9 @@ describe("create prototype phase validation", () => {
|
|
|
73
87
|
test("throws when current_phase is define and PRD is not completed", async () => {
|
|
74
88
|
const root = await createProjectRoot();
|
|
75
89
|
createdRoots.push(root);
|
|
76
|
-
|
|
90
|
+
const iteration = "000009";
|
|
91
|
+
await seedState(root, makeState({ currentPhase: "define", prdStatus: "pending", projectContextStatus: "pending", iteration }));
|
|
92
|
+
await seedPrd(root, iteration);
|
|
77
93
|
|
|
78
94
|
await withCwd(root, async () => {
|
|
79
95
|
await expect(runCreatePrototype({ provider: "claude" })).rejects.toThrow(
|
|
@@ -85,7 +101,9 @@ describe("create prototype phase validation", () => {
|
|
|
85
101
|
test("throws when current_phase is define and project_context is not created", async () => {
|
|
86
102
|
const root = await createProjectRoot();
|
|
87
103
|
createdRoots.push(root);
|
|
88
|
-
|
|
104
|
+
const iteration = "000009";
|
|
105
|
+
await seedState(root, makeState({ currentPhase: "define", prdStatus: "completed", projectContextStatus: "pending", iteration }));
|
|
106
|
+
await seedPrd(root, iteration);
|
|
89
107
|
|
|
90
108
|
await withCwd(root, async () => {
|
|
91
109
|
await expect(runCreatePrototype({ provider: "claude" })).rejects.toThrow(
|
|
@@ -99,29 +117,27 @@ describe("create prototype phase validation", () => {
|
|
|
99
117
|
createdRoots.push(root);
|
|
100
118
|
const iteration = "000009";
|
|
101
119
|
await seedState(root, makeState({ currentPhase: "define", prdStatus: "completed", projectContextStatus: "created", iteration }));
|
|
120
|
+
await seedPrd(root, iteration);
|
|
102
121
|
|
|
103
|
-
|
|
104
|
-
goals: ["Test"],
|
|
105
|
-
userStories: [
|
|
106
|
-
{ id: "US-001", title: "One", description: "D", acceptanceCriteria: [{ id: "AC1", text: "T" }] },
|
|
107
|
-
],
|
|
108
|
-
functionalRequirements: [{ id: "FR-001", description: "F" }],
|
|
109
|
-
};
|
|
122
|
+
await mkdir(join(root, ".agents"), { recursive: true });
|
|
110
123
|
await writeFile(
|
|
111
|
-
join(root, ".agents", "
|
|
112
|
-
|
|
124
|
+
join(root, ".agents", "PROJECT_CONTEXT.md"),
|
|
125
|
+
"# Project\n## Testing Strategy\n### Quality Checks\n```\nbun test\n```\n",
|
|
113
126
|
"utf8",
|
|
114
127
|
);
|
|
115
128
|
|
|
116
129
|
const { $ } = await import("bun");
|
|
117
130
|
await $`git init`.cwd(root).nothrow().quiet();
|
|
131
|
+
await $`git config user.email "test@test" && git config user.name "Test"`.cwd(root).nothrow().quiet();
|
|
132
|
+
await $`git add -A && git commit -m init`.cwd(root).nothrow().quiet();
|
|
118
133
|
|
|
119
134
|
await withCwd(root, async () => {
|
|
120
135
|
await expect(runCreatePrototype({ provider: "claude" })).rejects.toThrow(
|
|
121
|
-
"
|
|
136
|
+
"Git working tree is dirty",
|
|
122
137
|
);
|
|
123
138
|
});
|
|
124
139
|
|
|
140
|
+
// Transition writes state before the git check; phase is prototype
|
|
125
141
|
const updatedState = await readState(root);
|
|
126
142
|
expect(updatedState.current_phase).toBe("prototype");
|
|
127
143
|
});
|
|
@@ -129,7 +145,9 @@ describe("create prototype phase validation", () => {
|
|
|
129
145
|
test("throws when current_phase is refactor", async () => {
|
|
130
146
|
const root = await createProjectRoot();
|
|
131
147
|
createdRoots.push(root);
|
|
132
|
-
|
|
148
|
+
const iteration = "000009";
|
|
149
|
+
await seedState(root, makeState({ currentPhase: "refactor", iteration }));
|
|
150
|
+
await seedPrd(root, iteration);
|
|
133
151
|
|
|
134
152
|
await withCwd(root, async () => {
|
|
135
153
|
await expect(runCreatePrototype({ provider: "claude" })).rejects.toThrow(
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
loadSkill,
|
|
11
11
|
type AgentProvider,
|
|
12
12
|
} from "../agent";
|
|
13
|
+
import { assertGuardrail } from "../guardrail";
|
|
13
14
|
import { exists, FLOW_REL_DIR, readState, writeState } from "../state";
|
|
14
15
|
|
|
15
16
|
export interface CreatePrototypeOptions {
|
|
@@ -17,6 +18,7 @@ export interface CreatePrototypeOptions {
|
|
|
17
18
|
iterations?: number;
|
|
18
19
|
retryOnFail?: number;
|
|
19
20
|
stopOnCritical?: boolean;
|
|
21
|
+
force?: boolean;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
const ProgressEntrySchema = z.object({
|
|
@@ -106,6 +108,7 @@ function parseQualityChecks(projectContextContent: string): string[] {
|
|
|
106
108
|
export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<void> {
|
|
107
109
|
const projectRoot = process.cwd();
|
|
108
110
|
const state = await readState(projectRoot);
|
|
111
|
+
const force = opts.force ?? false;
|
|
109
112
|
|
|
110
113
|
if (opts.iterations !== undefined && (!Number.isInteger(opts.iterations) || opts.iterations < 1)) {
|
|
111
114
|
throw new Error(
|
|
@@ -167,21 +170,28 @@ export async function runCreatePrototype(opts: CreatePrototypeOptions): Promise<
|
|
|
167
170
|
state.current_phase = "prototype";
|
|
168
171
|
await writeState(projectRoot, state);
|
|
169
172
|
} else {
|
|
170
|
-
|
|
173
|
+
await assertGuardrail(
|
|
174
|
+
state,
|
|
175
|
+
true,
|
|
171
176
|
"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.",
|
|
177
|
+
{ force },
|
|
172
178
|
);
|
|
173
179
|
}
|
|
174
180
|
} else if (state.current_phase !== "prototype") {
|
|
175
|
-
|
|
181
|
+
await assertGuardrail(
|
|
182
|
+
state,
|
|
183
|
+
true,
|
|
176
184
|
"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.",
|
|
185
|
+
{ force },
|
|
177
186
|
);
|
|
178
187
|
}
|
|
179
188
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
189
|
+
await assertGuardrail(
|
|
190
|
+
state,
|
|
191
|
+
state.phases.prototype.project_context.status !== "created",
|
|
192
|
+
"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.",
|
|
193
|
+
{ force },
|
|
194
|
+
);
|
|
185
195
|
|
|
186
196
|
const workingTreeAfterPhase = await dollar`git status --porcelain`.cwd(projectRoot).nothrow().quiet();
|
|
187
197
|
if (workingTreeAfterPhase.exitCode !== 0) {
|
|
@@ -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`;
|