@quinteroac/agents-coding-toolkit 0.1.0-preview

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.
Files changed (85) hide show
  1. package/AGENTS.md +7 -0
  2. package/README.md +127 -0
  3. package/package.json +34 -0
  4. package/scaffold/.agents/flow/archived/tmpl_.gitkeep +0 -0
  5. package/scaffold/.agents/flow/tmpl_README.md +7 -0
  6. package/scaffold/.agents/flow/tmpl_iteration_close_checklist.example.md +11 -0
  7. package/scaffold/.agents/skills/automated-fix/tmpl_SKILL.md +67 -0
  8. package/scaffold/.agents/skills/create-issue/tmpl_SKILL.md +68 -0
  9. package/scaffold/.agents/skills/create-pr-document/tmpl_SKILL.md +125 -0
  10. package/scaffold/.agents/skills/create-project-context/tmpl_SKILL.md +168 -0
  11. package/scaffold/.agents/skills/create-test-plan/tmpl_SKILL.md +86 -0
  12. package/scaffold/.agents/skills/debug/tmpl_SKILL.md +19 -0
  13. package/scaffold/.agents/skills/evaluate/tmpl_SKILL.md +19 -0
  14. package/scaffold/.agents/skills/execute-test-batch/tmpl_SKILL.md +49 -0
  15. package/scaffold/.agents/skills/execute-test-case/tmpl_SKILL.md +47 -0
  16. package/scaffold/.agents/skills/implement-user-story/tmpl_SKILL.md +68 -0
  17. package/scaffold/.agents/skills/plan-refactor/tmpl_SKILL.md +19 -0
  18. package/scaffold/.agents/skills/refactor-prd/tmpl_SKILL.md +19 -0
  19. package/scaffold/.agents/skills/refine-pr-document/tmpl_SKILL.md +108 -0
  20. package/scaffold/.agents/skills/refine-project-context/tmpl_SKILL.md +157 -0
  21. package/scaffold/.agents/skills/refine-test-plan/tmpl_SKILL.md +76 -0
  22. package/scaffold/.agents/tmpl_PROJECT_CONTEXT.md +3 -0
  23. package/scaffold/.agents/tmpl_state.example.json +26 -0
  24. package/scaffold/.agents/tmpl_state_rules.md +29 -0
  25. package/scaffold/docs/nvst-flow/templates/tmpl_CHANGELOG.md +18 -0
  26. package/scaffold/docs/nvst-flow/templates/tmpl_TECHNICAL_DEBT.md +11 -0
  27. package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_evaluation-report.md +19 -0
  28. package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_product-requirement-document.md +19 -0
  29. package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_refactor_plan.md +19 -0
  30. package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_test-plan.md +19 -0
  31. package/scaffold/docs/nvst-flow/tmpl_COMMANDS.md +0 -0
  32. package/scaffold/docs/nvst-flow/tmpl_QUICK_USE.md +0 -0
  33. package/scaffold/docs/tmpl_PLACEHOLDER.md +0 -0
  34. package/scaffold/schemas/node-shims.d.ts +15 -0
  35. package/scaffold/schemas/tmpl_issues.ts +19 -0
  36. package/scaffold/schemas/tmpl_prd.ts +26 -0
  37. package/scaffold/schemas/tmpl_progress.ts +39 -0
  38. package/scaffold/schemas/tmpl_state.ts +81 -0
  39. package/scaffold/schemas/tmpl_test-plan.ts +20 -0
  40. package/scaffold/schemas/tmpl_validate-progress.ts +13 -0
  41. package/scaffold/schemas/tmpl_validate-state.ts +13 -0
  42. package/scaffold/tmpl_AGENTS.md +7 -0
  43. package/schemas/prd.ts +26 -0
  44. package/schemas/progress.ts +39 -0
  45. package/schemas/state.ts +81 -0
  46. package/schemas/test-plan.test.ts +53 -0
  47. package/schemas/test-plan.ts +20 -0
  48. package/schemas/validate-progress.ts +13 -0
  49. package/schemas/validate-state.ts +13 -0
  50. package/src/agent.test.ts +37 -0
  51. package/src/agent.ts +225 -0
  52. package/src/cli-path.ts +4 -0
  53. package/src/cli.ts +578 -0
  54. package/src/commands/approve-project-context.ts +37 -0
  55. package/src/commands/approve-requirement.ts +217 -0
  56. package/src/commands/approve-test-plan.test.ts +193 -0
  57. package/src/commands/approve-test-plan.ts +202 -0
  58. package/src/commands/create-issue.test.ts +484 -0
  59. package/src/commands/create-issue.ts +371 -0
  60. package/src/commands/create-project-context.ts +96 -0
  61. package/src/commands/create-prototype.test.ts +153 -0
  62. package/src/commands/create-prototype.ts +425 -0
  63. package/src/commands/create-test-plan.test.ts +381 -0
  64. package/src/commands/create-test-plan.ts +248 -0
  65. package/src/commands/define-requirement.ts +47 -0
  66. package/src/commands/destroy.ts +113 -0
  67. package/src/commands/execute-automated-fix.test.ts +580 -0
  68. package/src/commands/execute-automated-fix.ts +363 -0
  69. package/src/commands/execute-manual-fix.test.ts +343 -0
  70. package/src/commands/execute-manual-fix.ts +203 -0
  71. package/src/commands/execute-test-plan.test.ts +1891 -0
  72. package/src/commands/execute-test-plan.ts +722 -0
  73. package/src/commands/init.ts +85 -0
  74. package/src/commands/refine-project-context.ts +74 -0
  75. package/src/commands/refine-requirement.ts +60 -0
  76. package/src/commands/refine-test-plan.test.ts +200 -0
  77. package/src/commands/refine-test-plan.ts +93 -0
  78. package/src/commands/start-iteration.test.ts +144 -0
  79. package/src/commands/start-iteration.ts +101 -0
  80. package/src/commands/write-json.ts +136 -0
  81. package/src/install.test.ts +124 -0
  82. package/src/pack.test.ts +103 -0
  83. package/src/state.test.ts +66 -0
  84. package/src/state.ts +52 -0
  85. package/tsconfig.json +15 -0
@@ -0,0 +1,371 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+
5
+ import {
6
+ buildPrompt,
7
+ invokeAgent,
8
+ loadSkill,
9
+ type AgentProvider,
10
+ } from "../agent";
11
+ import { CLI_PATH } from "../cli-path";
12
+ import { readState, exists, FLOW_REL_DIR } from "../state";
13
+ import { IssuesSchema, type Issue } from "../../scaffold/schemas/tmpl_issues";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Agent output schema — agent produces title+description only
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const AgentIssueSchema = z.object({
20
+ title: z.string(),
21
+ description: z.string(),
22
+ });
23
+
24
+ const AgentOutputSchema = z.array(AgentIssueSchema);
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Options
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export interface CreateIssueOptions {
31
+ provider: AgentProvider;
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Command handler
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export async function runCreateIssue(opts: CreateIssueOptions): Promise<void> {
39
+ const { provider } = opts;
40
+ const projectRoot = process.cwd();
41
+ const state = await readState(projectRoot);
42
+
43
+ const iteration = state.current_iteration;
44
+
45
+ // Load skill and build prompt
46
+ const skillBody = await loadSkill(projectRoot, "create-issue");
47
+ const prompt = buildPrompt(skillBody, {
48
+ current_iteration: iteration,
49
+ });
50
+
51
+ // Invoke agent interactively
52
+ const result = await invokeAgent({
53
+ provider,
54
+ prompt,
55
+ cwd: projectRoot,
56
+ interactive: true,
57
+ });
58
+
59
+ if (result.exitCode !== 0) {
60
+ throw new Error(
61
+ `Agent invocation failed with exit code ${result.exitCode}.`,
62
+ );
63
+ }
64
+
65
+ // Agent writes the file via write-json (see create-issue skill). Check if file exists first.
66
+ const outputFileName = `it_${iteration}_ISSUES.json`;
67
+ const outputRelPath = join(FLOW_REL_DIR, outputFileName);
68
+ const outputAbsPath = join(projectRoot, outputRelPath);
69
+
70
+ if (await exists(outputAbsPath)) {
71
+ // Agent ran write-json; validate the file
72
+ const content = await readFile(outputAbsPath, "utf8");
73
+ const parsed = JSON.parse(content) as unknown;
74
+ const validationResult = IssuesSchema.safeParse(parsed);
75
+ if (!validationResult.success) {
76
+ const formatted = validationResult.error.format();
77
+ throw new Error(
78
+ `Agent wrote invalid issues file:\n${JSON.stringify(formatted, null, 2)}`,
79
+ );
80
+ }
81
+ console.log(`Issues file created: ${outputRelPath}`);
82
+ return;
83
+ }
84
+
85
+ // Fallback: parse stdout and call write-json (for agents that output JSON instead of running write-json)
86
+ const agentOutput = result.stdout.trim();
87
+ if (!agentOutput) {
88
+ throw new Error(
89
+ "Agent did not produce output. Expected the agent to run write-json to create the issues file, or output a JSON array to stdout.",
90
+ );
91
+ }
92
+
93
+ const jsonStr = extractJson(agentOutput);
94
+ let parsed: unknown;
95
+ try {
96
+ parsed = JSON.parse(jsonStr);
97
+ } catch {
98
+ const looksLikeConversation =
99
+ /describe|issue|please|what|how|tell me|help you/i.test(agentOutput) &&
100
+ !agentOutput.trimStart().startsWith("[");
101
+ if (looksLikeConversation) {
102
+ throw new Error(
103
+ "The agent started the interactive session but exited before completing. " +
104
+ "The output was a question, not the final issues. This can happen if the agent " +
105
+ "encountered an error (e.g. IDE companion connection failed) or exited unexpectedly. " +
106
+ "Try running again or use a different provider (claude, codex, cursor). " +
107
+ "The agent must run write-json when done; see .agents/skills/create-issue/SKILL.md.",
108
+ );
109
+ }
110
+ throw new Error(
111
+ `Failed to parse agent output as JSON. Raw output:\n${agentOutput}`,
112
+ );
113
+ }
114
+
115
+ const agentResult = AgentOutputSchema.safeParse(parsed);
116
+ if (!agentResult.success) {
117
+ const formatted = agentResult.error.format();
118
+ throw new Error(
119
+ `Agent output does not match expected format:\n${JSON.stringify(formatted, null, 2)}`,
120
+ );
121
+ }
122
+
123
+ const issues: Issue[] = agentResult.data.map((item, index) => ({
124
+ id: `ISSUE-${iteration}-${String(index + 1).padStart(3, "0")}`,
125
+ title: item.title,
126
+ description: item.description,
127
+ status: "open" as const,
128
+ }));
129
+
130
+ const validationResult = IssuesSchema.safeParse(issues);
131
+ if (!validationResult.success) {
132
+ const formatted = validationResult.error.format();
133
+ throw new Error(
134
+ `Generated issues failed schema validation:\n${JSON.stringify(formatted, null, 2)}`,
135
+ );
136
+ }
137
+
138
+ const dataStr = JSON.stringify(validationResult.data);
139
+ const proc = Bun.spawn(
140
+ [
141
+ "bun",
142
+ CLI_PATH,
143
+ "write-json",
144
+ "--schema",
145
+ "issues",
146
+ "--out",
147
+ outputRelPath,
148
+ "--data",
149
+ dataStr,
150
+ ],
151
+ { cwd: projectRoot, stdout: "pipe", stderr: "pipe" },
152
+ );
153
+
154
+ const writeExitCode = await proc.exited;
155
+ if (writeExitCode !== 0) {
156
+ const stderr = await new Response(proc.stderr).text();
157
+ throw new Error(`Failed to write issues file: ${stderr}`);
158
+ }
159
+
160
+ console.log(`Issues file created: ${outputRelPath}`);
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Helpers
165
+ // ---------------------------------------------------------------------------
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Test-execution-report → issues (US-002)
169
+ // ---------------------------------------------------------------------------
170
+
171
+ const TestResultPayloadSchema = z.object({
172
+ status: z.string(),
173
+ evidence: z.string().optional(),
174
+ notes: z.string().optional(),
175
+ });
176
+
177
+ const TestResultSchema = z.object({
178
+ testCaseId: z.string(),
179
+ description: z.string(),
180
+ correlatedRequirements: z.array(z.string()).optional(),
181
+ payload: TestResultPayloadSchema,
182
+ });
183
+
184
+ const TestExecutionResultsSchema = z.object({
185
+ iteration: z.string(),
186
+ results: z.array(TestResultSchema),
187
+ });
188
+
189
+ export type TestExecutionResults = z.infer<typeof TestExecutionResultsSchema>;
190
+
191
+ const ACTIONABLE_STATUSES = ["failed", "skipped", "invocation_failed"] as const;
192
+
193
+ export function isActionableStatus(status: string): boolean {
194
+ return (ACTIONABLE_STATUSES as readonly string[]).includes(status);
195
+ }
196
+
197
+ export function buildIssuesFromTestResults(
198
+ results: TestExecutionResults,
199
+ iteration: string,
200
+ ): Issue[] {
201
+ const actionable = results.results.filter((r) =>
202
+ isActionableStatus(r.payload.status),
203
+ );
204
+
205
+ return actionable.map((r, index) => ({
206
+ id: `ISSUE-${iteration}-${String(index + 1).padStart(3, "0")}`,
207
+ title: `[${r.payload.status}] ${r.testCaseId}: ${r.description}`,
208
+ description: [
209
+ `Test case ${r.testCaseId} resulted in status: ${r.payload.status}.`,
210
+ r.payload.notes ? `Notes: ${r.payload.notes}` : "",
211
+ r.payload.evidence ? `Evidence: ${r.payload.evidence}` : "",
212
+ r.correlatedRequirements?.length
213
+ ? `Correlated requirements: ${r.correlatedRequirements.join(", ")}`
214
+ : "",
215
+ ]
216
+ .filter(Boolean)
217
+ .join("\n"),
218
+ status: "open" as const,
219
+ }));
220
+ }
221
+
222
+ export async function runCreateIssueFromTestReport(): Promise<void> {
223
+ const projectRoot = process.cwd();
224
+ const state = await readState(projectRoot);
225
+ const iteration = state.current_iteration;
226
+
227
+ // AC01: Try reading from .agents/flow/ first, then archived path
228
+ const fileName = `it_${iteration}_test-execution-results.json`;
229
+ const flowPath = join(projectRoot, FLOW_REL_DIR, fileName);
230
+
231
+ let resultsPath: string | null = null;
232
+
233
+ if (await exists(flowPath)) {
234
+ resultsPath = flowPath;
235
+ } else {
236
+ // Check archived path from state.json history for current iteration
237
+ const historyEntry = state.history?.find(
238
+ (h) => h.iteration === iteration,
239
+ );
240
+ if (historyEntry) {
241
+ const archivedPath = join(
242
+ projectRoot,
243
+ historyEntry.archived_path,
244
+ fileName,
245
+ );
246
+ if (await exists(archivedPath)) {
247
+ resultsPath = archivedPath;
248
+ }
249
+ }
250
+ }
251
+
252
+ // AC05: Fail with clear error if not found in either location
253
+ if (!resultsPath) {
254
+ throw new Error(
255
+ `Test execution results file not found: looked for ${fileName} in ${FLOW_REL_DIR}/ and archived path for iteration ${iteration}.`,
256
+ );
257
+ }
258
+
259
+ // Read and validate the test execution results
260
+ const raw = await readFile(resultsPath, "utf8");
261
+ let parsed: unknown;
262
+ try {
263
+ parsed = JSON.parse(raw);
264
+ } catch {
265
+ throw new Error(
266
+ `Failed to parse test execution results as JSON: ${resultsPath}`,
267
+ );
268
+ }
269
+
270
+ const validationResult = TestExecutionResultsSchema.safeParse(parsed);
271
+ if (!validationResult.success) {
272
+ const formatted = validationResult.error.format();
273
+ throw new Error(
274
+ `Test execution results failed schema validation:\n${JSON.stringify(formatted, null, 2)}`,
275
+ );
276
+ }
277
+
278
+ // AC02: Convert actionable test results into issues
279
+ const issues = buildIssuesFromTestResults(validationResult.data, iteration);
280
+
281
+ // AC04: If all tests are passing, write empty array and exit with code 0
282
+ // (buildIssuesFromTestResults returns [] when no actionable results)
283
+
284
+ // AC06: Validate against ISSUES schema
285
+ const issuesValidation = IssuesSchema.safeParse(issues);
286
+ if (!issuesValidation.success) {
287
+ const formatted = issuesValidation.error.format();
288
+ throw new Error(
289
+ `Generated issues failed ISSUES schema validation:\n${JSON.stringify(formatted, null, 2)}`,
290
+ );
291
+ }
292
+
293
+ // AC03: Write output file via write-json
294
+ const outputFileName = `it_${iteration}_ISSUES.json`;
295
+ const outputRelPath = join(FLOW_REL_DIR, outputFileName);
296
+ const dataStr = JSON.stringify(issuesValidation.data);
297
+
298
+ const proc = Bun.spawn(
299
+ [
300
+ "bun",
301
+ CLI_PATH,
302
+ "write-json",
303
+ "--schema",
304
+ "issues",
305
+ "--out",
306
+ outputRelPath,
307
+ "--data",
308
+ dataStr,
309
+ ],
310
+ { cwd: projectRoot, stdout: "pipe", stderr: "pipe" },
311
+ );
312
+
313
+ const writeExitCode = await proc.exited;
314
+ if (writeExitCode !== 0) {
315
+ const stderr = await new Response(proc.stderr).text();
316
+ throw new Error(`Failed to write issues file: ${stderr}`);
317
+ }
318
+
319
+ if (issues.length === 0) {
320
+ console.log(
321
+ `All tests passing. Empty issues file created: ${outputRelPath}`,
322
+ );
323
+ } else {
324
+ console.log(
325
+ `${issues.length} issue(s) created from test execution results: ${outputRelPath}`,
326
+ );
327
+ }
328
+ }
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Helpers
332
+ // ---------------------------------------------------------------------------
333
+
334
+ /** Extract JSON array from text that may contain markdown fences or surrounding text. */
335
+ export function extractJson(text: string): string {
336
+ // 1. Prefer ```json block when multiple fences exist (REQ-FIX-01, REQ-FIX-02).
337
+ // Use lazy *? to stop at the FIRST closing ```, avoiding capture of later blocks (e.g. ```bash).
338
+ const jsonFenceMatch = text.match(/```json\s*\n([\s\S]*?)\n```\s*/);
339
+ if (jsonFenceMatch) {
340
+ return jsonFenceMatch[1].trim();
341
+ }
342
+
343
+ // 2. Try single-block case: ``` or ```json at line start, content until closing ``` at line end
344
+ const fenceMatch = text.match(/^```(?:json)?\s*\n([\s\S]*)\n```\s*$/m);
345
+ if (fenceMatch) {
346
+ return fenceMatch[1].trim();
347
+ }
348
+
349
+ // 3. Fallback: plain ``` fence (no json label) - find first ``` and match to its closing fence
350
+ // Only use when there is a single block (first and last ``` span the same block)
351
+ const openIdx = text.indexOf("```");
352
+ if (openIdx !== -1) {
353
+ const afterOpen = text.indexOf("\n", openIdx);
354
+ if (afterOpen !== -1) {
355
+ const rest = text.slice(afterOpen + 1);
356
+ const closeMatch = rest.match(/\n```\s*/);
357
+ if (closeMatch) {
358
+ return rest.slice(0, closeMatch.index).trim();
359
+ }
360
+ }
361
+ }
362
+
363
+ // 4. Try to find a JSON array directly (REQ-FIX-03)
364
+ const arrayMatch = text.match(/\[[\s\S]*\]/);
365
+ if (arrayMatch) {
366
+ return arrayMatch[0];
367
+ }
368
+
369
+ // Return as-is and let JSON.parse handle it
370
+ return text;
371
+ }
@@ -0,0 +1,96 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import {
5
+ buildPrompt,
6
+ invokeAgent,
7
+ loadSkill,
8
+ type AgentProvider,
9
+ } from "../agent";
10
+ import { exists, readState, writeState } from "../state";
11
+
12
+ export interface CreateProjectContextOptions {
13
+ provider: AgentProvider;
14
+ mode: "strict" | "yolo";
15
+ }
16
+
17
+ export async function runCreateProjectContext(opts: CreateProjectContextOptions): Promise<void> {
18
+ const { provider, mode } = opts;
19
+ const projectRoot = process.cwd();
20
+ const state = await readState(projectRoot);
21
+
22
+ if (state.phases.define.prd_generation.status !== "completed") {
23
+ throw new Error(
24
+ "Cannot create project context: define.prd_generation must be completed first.",
25
+ );
26
+ }
27
+
28
+ const projectContext = state.phases.prototype.project_context;
29
+ if (projectContext.status === "pending_approval") {
30
+ throw new Error(
31
+ "Cannot create project context: project context is pending approval. " +
32
+ "Run `bun nvst approve project-context` or `bun nvst refine project-context` first.",
33
+ );
34
+ }
35
+
36
+ if (projectContext.status === "created") {
37
+ throw new Error(
38
+ "Cannot create project context: project context already exists. " +
39
+ "Use `bun nvst refine project-context` to iterate on it.",
40
+ );
41
+ }
42
+
43
+ if (projectContext.status !== "pending") {
44
+ throw new Error(
45
+ `Cannot create project context from status '${projectContext.status}'. Expected pending.`,
46
+ );
47
+ }
48
+
49
+ const prdFile = state.phases.define.prd_generation.file;
50
+ if (!prdFile) {
51
+ throw new Error("Cannot create project context: define.prd_generation.file is missing.");
52
+ }
53
+
54
+ if (state.current_phase === "define") {
55
+ state.current_phase = "prototype";
56
+ }
57
+
58
+ const skillBody = await loadSkill(projectRoot, "create-project-context");
59
+ const prdPath = join(projectRoot, ".agents", "flow", prdFile);
60
+ const prdContent = await readFile(prdPath, "utf8");
61
+
62
+ const projectContextPath = join(projectRoot, ".agents", "PROJECT_CONTEXT.md");
63
+ const existingProjectContext =
64
+ (await exists(projectContextPath)) ? await readFile(projectContextPath, "utf8") : "";
65
+
66
+ const context: Record<string, string> = {
67
+ mode,
68
+ prd_file: prdFile,
69
+ prd_content: prdContent,
70
+ };
71
+
72
+ if (existingProjectContext) {
73
+ context.existing_project_context = existingProjectContext;
74
+ }
75
+
76
+ const prompt = buildPrompt(skillBody, context);
77
+ const result = await invokeAgent({
78
+ provider,
79
+ prompt,
80
+ cwd: projectRoot,
81
+ interactive: true,
82
+ });
83
+
84
+ if (result.exitCode !== 0) {
85
+ throw new Error(`Agent invocation failed with exit code ${result.exitCode}.`);
86
+ }
87
+
88
+ state.phases.prototype.project_context.status = "pending_approval";
89
+ state.phases.prototype.project_context.file = ".agents/PROJECT_CONTEXT.md";
90
+ state.last_updated = new Date().toISOString();
91
+ state.updated_by = "nvst:create-project-context";
92
+
93
+ await writeState(projectRoot, state);
94
+
95
+ console.log("Project context generated and marked as pending approval.");
96
+ }
@@ -0,0 +1,153 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdir, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { mkdtemp } from "node:fs/promises";
6
+
7
+ import { readState, writeState } from "../state";
8
+ import type { State } from "../../scaffold/schemas/tmpl_state";
9
+ import { runCreatePrototype } from "./create-prototype";
10
+
11
+ async function createProjectRoot(): Promise<string> {
12
+ return mkdtemp(join(tmpdir(), "nvst-create-prototype-"));
13
+ }
14
+
15
+ async function withCwd<T>(cwd: string, fn: () => Promise<T>): Promise<T> {
16
+ const previous = process.cwd();
17
+ process.chdir(cwd);
18
+ try {
19
+ return await fn();
20
+ } finally {
21
+ process.chdir(previous);
22
+ }
23
+ }
24
+
25
+ function makeState(overrides: {
26
+ currentPhase?: State["current_phase"];
27
+ prdStatus?: "pending" | "completed";
28
+ projectContextStatus?: "pending" | "pending_approval" | "created";
29
+ iteration?: string;
30
+ } = {}): State {
31
+ const iteration = overrides.iteration ?? "000009";
32
+ return {
33
+ current_iteration: iteration,
34
+ current_phase: overrides.currentPhase ?? "prototype",
35
+ phases: {
36
+ define: {
37
+ requirement_definition: { status: "approved", file: `it_${iteration}_product-requirement-document.md` },
38
+ prd_generation: { status: overrides.prdStatus ?? "completed", file: `it_${iteration}_PRD.json` },
39
+ },
40
+ prototype: {
41
+ project_context: { status: overrides.projectContextStatus ?? "created", file: ".agents/PROJECT_CONTEXT.md" },
42
+ test_plan: { status: "pending", file: null },
43
+ tp_generation: { status: "pending", file: null },
44
+ prototype_build: { status: "pending", file: null },
45
+ test_execution: { status: "pending", file: null },
46
+ prototype_approved: false,
47
+ },
48
+ refactor: {
49
+ evaluation_report: { status: "pending", file: null },
50
+ refactor_plan: { status: "pending", file: null },
51
+ refactor_execution: { status: "pending", file: null },
52
+ changelog: { status: "pending", file: null },
53
+ },
54
+ },
55
+ last_updated: "2026-02-22T00:00:00.000Z",
56
+ updated_by: "seed",
57
+ history: [],
58
+ };
59
+ }
60
+
61
+ async function seedState(projectRoot: string, state: State): Promise<void> {
62
+ await mkdir(join(projectRoot, ".agents", "flow"), { recursive: true });
63
+ await writeState(projectRoot, state);
64
+ }
65
+
66
+ const createdRoots: string[] = [];
67
+
68
+ afterEach(async () => {
69
+ await Promise.all(createdRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
70
+ });
71
+
72
+ describe("create prototype phase validation", () => {
73
+ test("throws when current_phase is define and PRD is not completed", async () => {
74
+ const root = await createProjectRoot();
75
+ createdRoots.push(root);
76
+ await seedState(root, makeState({ currentPhase: "define", prdStatus: "pending", projectContextStatus: "pending" }));
77
+
78
+ await withCwd(root, async () => {
79
+ await expect(runCreatePrototype({ provider: "claude" })).rejects.toThrow(
80
+ "Cannot create prototype: current_phase is define and prerequisites are not met.",
81
+ );
82
+ });
83
+ });
84
+
85
+ test("throws when current_phase is define and project_context is not created", async () => {
86
+ const root = await createProjectRoot();
87
+ createdRoots.push(root);
88
+ await seedState(root, makeState({ currentPhase: "define", prdStatus: "completed", projectContextStatus: "pending" }));
89
+
90
+ await withCwd(root, async () => {
91
+ await expect(runCreatePrototype({ provider: "claude" })).rejects.toThrow(
92
+ "Cannot create prototype: current_phase is define and prerequisites are not met.",
93
+ );
94
+ });
95
+ });
96
+
97
+ test("auto-transitions from define to prototype and starts build in same run when PRD and git are ready", async () => {
98
+ const root = await createProjectRoot();
99
+ createdRoots.push(root);
100
+ const iteration = "000009";
101
+ await seedState(root, makeState({ currentPhase: "define", prdStatus: "completed", projectContextStatus: "created", iteration }));
102
+
103
+ const prdContent = {
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
+ };
110
+ await writeFile(
111
+ join(root, ".agents", "flow", `it_${iteration}_PRD.json`),
112
+ JSON.stringify(prdContent),
113
+ "utf8",
114
+ );
115
+
116
+ const { $ } = await import("bun");
117
+ await $`git init`.cwd(root).nothrow().quiet();
118
+
119
+ await withCwd(root, async () => {
120
+ await expect(runCreatePrototype({ provider: "claude" })).rejects.toThrow(
121
+ "Required skill missing",
122
+ );
123
+ });
124
+
125
+ const updatedState = await readState(root);
126
+ expect(updatedState.current_phase).toBe("prototype");
127
+ });
128
+
129
+ test("throws when current_phase is refactor", async () => {
130
+ const root = await createProjectRoot();
131
+ createdRoots.push(root);
132
+ await seedState(root, makeState({ currentPhase: "refactor" }));
133
+
134
+ await withCwd(root, async () => {
135
+ await expect(runCreatePrototype({ provider: "claude" })).rejects.toThrow(
136
+ "Cannot create prototype: current_phase must be define (with approved PRD) or prototype.",
137
+ );
138
+ });
139
+ });
140
+
141
+ test("proceeds when current_phase is already prototype", async () => {
142
+ const root = await createProjectRoot();
143
+ createdRoots.push(root);
144
+ await seedState(root, makeState({ currentPhase: "prototype" }));
145
+
146
+ // Passes phase check, fails later at PRD file lookup.
147
+ await withCwd(root, async () => {
148
+ await expect(runCreatePrototype({ provider: "claude" })).rejects.toThrow(
149
+ "PRD source of truth missing",
150
+ );
151
+ });
152
+ });
153
+ });