@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,85 @@
1
+ import { access, mkdir, readdir } from "node:fs/promises";
2
+ import { basename, dirname, join, relative } from "node:path";
3
+
4
+ const TEMPLATE_PREFIX = "tmpl_";
5
+ const SCAFFOLD_ROOT = join(import.meta.dir, "..", "..", "scaffold");
6
+
7
+ export interface ScaffoldEntry {
8
+ sourcePath: string;
9
+ destinationPath: string;
10
+ relativeDestinationPath: string;
11
+ }
12
+
13
+ function stripTemplatePrefix(fileName: string): string {
14
+ return fileName.startsWith(TEMPLATE_PREFIX) ? fileName.slice(TEMPLATE_PREFIX.length) : fileName;
15
+ }
16
+
17
+ async function walkFiles(directoryPath: string): Promise<string[]> {
18
+ const entries = await readdir(directoryPath, { withFileTypes: true });
19
+ const filePaths: string[] = [];
20
+
21
+ for (const entry of entries) {
22
+ const entryPath = join(directoryPath, entry.name);
23
+ if (entry.isDirectory()) {
24
+ filePaths.push(...(await walkFiles(entryPath)));
25
+ continue;
26
+ }
27
+ if (entry.isFile()) {
28
+ filePaths.push(entryPath);
29
+ }
30
+ }
31
+
32
+ return filePaths;
33
+ }
34
+
35
+ export async function getScaffoldEntries(projectRoot: string): Promise<ScaffoldEntry[]> {
36
+ const sourceFiles = await walkFiles(SCAFFOLD_ROOT);
37
+
38
+ return sourceFiles.map((sourcePath) => {
39
+ const relativeFromScaffold = relative(SCAFFOLD_ROOT, sourcePath);
40
+ const sourceDir = dirname(relativeFromScaffold);
41
+ const targetFileName = stripTemplatePrefix(basename(relativeFromScaffold));
42
+ const relativeDestinationPath =
43
+ sourceDir === "." ? targetFileName : join(sourceDir, targetFileName);
44
+
45
+ return {
46
+ sourcePath,
47
+ destinationPath: join(projectRoot, relativeDestinationPath),
48
+ relativeDestinationPath,
49
+ };
50
+ });
51
+ }
52
+
53
+ async function exists(filePath: string): Promise<boolean> {
54
+ try {
55
+ await access(filePath);
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ export async function runInit(): Promise<void> {
63
+ const projectRoot = process.cwd();
64
+ const entries = await getScaffoldEntries(projectRoot);
65
+
66
+ const created: string[] = [];
67
+ const skipped: string[] = [];
68
+
69
+ for (const entry of entries) {
70
+ if (await exists(entry.destinationPath)) {
71
+ console.warn(`Skipping existing file: ${entry.relativeDestinationPath}`);
72
+ skipped.push(entry.relativeDestinationPath);
73
+ continue;
74
+ }
75
+
76
+ await mkdir(dirname(entry.destinationPath), { recursive: true });
77
+ await Bun.write(entry.destinationPath, Bun.file(entry.sourcePath));
78
+ created.push(entry.relativeDestinationPath);
79
+ console.log(`Created: ${entry.relativeDestinationPath}`);
80
+ }
81
+
82
+ console.log(
83
+ `\nInit complete. Created ${created.length} file(s), skipped ${skipped.length} existing file(s).`,
84
+ );
85
+ }
@@ -0,0 +1,74 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import { buildPrompt, invokeAgent, loadSkill, type AgentProvider } from "../agent";
5
+ import { exists, readState, writeState } from "../state";
6
+
7
+ export interface RefineProjectContextOptions {
8
+ provider: AgentProvider;
9
+ challenge: boolean;
10
+ }
11
+
12
+ export async function runRefineProjectContext(opts: RefineProjectContextOptions): Promise<void> {
13
+ const { provider, challenge } = opts;
14
+ const projectRoot = process.cwd();
15
+ const state = await readState(projectRoot);
16
+
17
+ // US-003-AC01: Validate status is pending_approval or created
18
+ const projectContext = state.phases.prototype.project_context;
19
+ if (projectContext.status !== "pending_approval" && projectContext.status !== "created") {
20
+ throw new Error(
21
+ `Cannot refine project context from status '${projectContext.status}'. ` +
22
+ "Expected pending_approval or created.",
23
+ );
24
+ }
25
+
26
+ // Validate file reference exists in state
27
+ const contextFile = projectContext.file;
28
+ if (!contextFile) {
29
+ throw new Error("Cannot refine project context: project_context.file is missing.");
30
+ }
31
+
32
+ const contextPath = join(projectRoot, contextFile);
33
+ if (!(await exists(contextPath))) {
34
+ throw new Error(`Cannot refine project context: file not found at ${contextPath}`);
35
+ }
36
+
37
+ // US-003-AC03: Challenge mode uses a dedicated skill section
38
+ const skillName = challenge ? "refine-project-context" : "refine-project-context";
39
+ const skillBody = await loadSkill(projectRoot, skillName);
40
+ const contextContent = await readFile(contextPath, "utf8");
41
+
42
+ const context: Record<string, string> = {
43
+ current_iteration: state.current_iteration,
44
+ project_context_file: contextFile,
45
+ project_context_content: contextContent,
46
+ };
47
+
48
+ if (challenge) {
49
+ context.mode = "challenger";
50
+ }
51
+
52
+ const prompt = buildPrompt(skillBody, context);
53
+ const result = await invokeAgent({
54
+ provider,
55
+ prompt,
56
+ cwd: projectRoot,
57
+ interactive: true,
58
+ });
59
+
60
+ if (result.exitCode !== 0) {
61
+ throw new Error(`Agent invocation failed with exit code ${result.exitCode}.`);
62
+ }
63
+
64
+ // US-003-AC04: After refinement, set status back to pending_approval
65
+ projectContext.status = "pending_approval";
66
+ state.last_updated = new Date().toISOString();
67
+ state.updated_by = challenge
68
+ ? "nvst:refine-project-context:challenge"
69
+ : "nvst:refine-project-context";
70
+
71
+ await writeState(projectRoot, state);
72
+
73
+ console.log("Project context refined and marked as pending approval.");
74
+ }
@@ -0,0 +1,60 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import { buildPrompt, invokeAgent, loadSkill, type AgentProvider } from "../agent";
5
+ import { exists, readState, FLOW_REL_DIR } from "../state";
6
+
7
+ export interface RefineRequirementOptions {
8
+ provider: AgentProvider;
9
+ challenge: boolean;
10
+ }
11
+
12
+ export async function runRefineRequirement(opts: RefineRequirementOptions): Promise<void> {
13
+ const { provider, challenge } = opts;
14
+ const projectRoot = process.cwd();
15
+ const state = await readState(projectRoot);
16
+
17
+ const requirementDefinition = state.phases.define.requirement_definition;
18
+ if (requirementDefinition.status !== "in_progress") {
19
+ throw new Error(
20
+ `Cannot refine requirement from status '${requirementDefinition.status}'. Expected in_progress.`,
21
+ );
22
+ }
23
+
24
+ const requirementFile = requirementDefinition.file;
25
+ if (!requirementFile) {
26
+ throw new Error("Cannot refine requirement: define.requirement_definition.file is missing.");
27
+ }
28
+
29
+ const requirementPath = join(projectRoot, FLOW_REL_DIR, requirementFile);
30
+ if (!(await exists(requirementPath))) {
31
+ throw new Error(`Cannot refine requirement: file not found at ${requirementPath}`);
32
+ }
33
+
34
+ const skillBody = await loadSkill(projectRoot, "refine-pr-document");
35
+ const requirementContent = await readFile(requirementPath, "utf8");
36
+
37
+ const context: Record<string, string> = {
38
+ current_iteration: state.current_iteration,
39
+ requirement_file: requirementFile,
40
+ requirement_content: requirementContent,
41
+ };
42
+
43
+ if (challenge) {
44
+ context.mode = "challenger";
45
+ }
46
+
47
+ const prompt = buildPrompt(skillBody, context);
48
+ const result = await invokeAgent({
49
+ provider,
50
+ prompt,
51
+ cwd: projectRoot,
52
+ interactive: true,
53
+ });
54
+
55
+ if (result.exitCode !== 0) {
56
+ throw new Error(`Agent invocation failed with exit code ${result.exitCode}.`);
57
+ }
58
+
59
+ console.log("Requirement refined.");
60
+ }
@@ -0,0 +1,200 @@
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 { runRefineTestPlan } from "./refine-test-plan";
9
+
10
+ async function createProjectRoot(): Promise<string> {
11
+ return mkdtemp(join(tmpdir(), "nvst-refine-test-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" | "created",
27
+ file: string | null,
28
+ ) {
29
+ await mkdir(join(projectRoot, ".agents", "flow"), { recursive: true });
30
+
31
+ await writeState(projectRoot, {
32
+ current_iteration: "000003",
33
+ current_phase: "prototype",
34
+ phases: {
35
+ define: {
36
+ requirement_definition: { status: "approved", file: "it_000003_product-requirement-document.md" },
37
+ prd_generation: { status: "completed", file: "it_000003_PRD.json" },
38
+ },
39
+ prototype: {
40
+ project_context: { status: "created", file: ".agents/PROJECT_CONTEXT.md" },
41
+ test_plan: { status, file },
42
+ tp_generation: { status: "pending", file: null },
43
+ prototype_build: { status: "pending", file: null },
44
+ test_execution: { status: "pending", file: null },
45
+ prototype_approved: false,
46
+ },
47
+ refactor: {
48
+ evaluation_report: { status: "pending", file: null },
49
+ refactor_plan: { status: "pending", file: null },
50
+ refactor_execution: { status: "pending", file: null },
51
+ changelog: { status: "pending", file: null },
52
+ },
53
+ },
54
+ last_updated: "2026-02-21T00: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 test-plan command", () => {
67
+ test("registers refine test-plan command in CLI dispatch", async () => {
68
+ const source = await readFile(join(process.cwd(), "src", "cli.ts"), "utf8");
69
+
70
+ expect(source).toContain('import { runRefineTestPlan } from "./commands/refine-test-plan";');
71
+ expect(source).toContain('if (subcommand === "test-plan") {');
72
+ expect(source).toContain('const challenge = postAgentArgs.includes("--challenge");');
73
+ expect(source).toContain("await runRefineTestPlan({ provider, challenge });");
74
+ });
75
+
76
+ test("loads refine-test-plan skill, reads test plan file context, invokes agent interactively, and does not update state", async () => {
77
+ const projectRoot = await createProjectRoot();
78
+ createdRoots.push(projectRoot);
79
+ await seedState(projectRoot, "pending_approval", "it_000003_test-plan.md");
80
+
81
+ const testPlanPath = join(projectRoot, ".agents", "flow", "it_000003_test-plan.md");
82
+ await writeFile(testPlanPath, "# Existing Test Plan\n- Case A\n", "utf8");
83
+
84
+ let loadedSkill = "";
85
+ let invocation: { interactive: boolean | undefined; prompt: string } | undefined;
86
+ const stateBefore = JSON.stringify(await readState(projectRoot));
87
+
88
+ await withCwd(projectRoot, async () => {
89
+ await runRefineTestPlan(
90
+ { provider: "codex", challenge: false },
91
+ {
92
+ loadSkillFn: async (_root, skillName) => {
93
+ loadedSkill = skillName;
94
+ return "Refine test plan skill";
95
+ },
96
+ invokeAgentFn: async (options): Promise<AgentResult> => {
97
+ invocation = {
98
+ interactive: options.interactive,
99
+ prompt: options.prompt,
100
+ };
101
+ return { exitCode: 0, stdout: "", stderr: "" };
102
+ },
103
+ },
104
+ );
105
+ });
106
+
107
+ expect(loadedSkill).toBe("refine-test-plan");
108
+ if (invocation === undefined) {
109
+ throw new Error("Agent invocation was not captured");
110
+ }
111
+
112
+ expect(invocation.interactive).toBe(true);
113
+ expect(invocation.prompt).toContain("### current_iteration");
114
+ expect(invocation.prompt).toContain("000003");
115
+ expect(invocation.prompt).toContain("### test_plan_file");
116
+ expect(invocation.prompt).toContain("it_000003_test-plan.md");
117
+ expect(invocation.prompt).toContain("### test_plan_content");
118
+ expect(invocation.prompt).toContain("# Existing Test Plan");
119
+
120
+ const stateAfter = JSON.stringify(await readState(projectRoot));
121
+ expect(stateAfter).toBe(stateBefore);
122
+ });
123
+
124
+ test("passes mode=challenger in prompt context when challenge mode is enabled without updating state", async () => {
125
+ const projectRoot = await createProjectRoot();
126
+ createdRoots.push(projectRoot);
127
+ await seedState(projectRoot, "pending_approval", "it_000003_test-plan.md");
128
+
129
+ const testPlanPath = join(projectRoot, ".agents", "flow", "it_000003_test-plan.md");
130
+ await writeFile(testPlanPath, "# Existing Test Plan\n- Case B\n", "utf8");
131
+
132
+ let invocationPrompt = "";
133
+ const stateBefore = JSON.stringify(await readState(projectRoot));
134
+
135
+ await withCwd(projectRoot, async () => {
136
+ await runRefineTestPlan(
137
+ { provider: "codex", challenge: true },
138
+ {
139
+ loadSkillFn: async () => "Refine test plan skill",
140
+ invokeAgentFn: async (options): Promise<AgentResult> => {
141
+ invocationPrompt = options.prompt;
142
+ return { exitCode: 0, stdout: "", stderr: "" };
143
+ },
144
+ },
145
+ );
146
+ });
147
+
148
+ expect(invocationPrompt).toContain("### mode");
149
+ expect(invocationPrompt).toContain("challenger");
150
+
151
+ const stateAfter = JSON.stringify(await readState(projectRoot));
152
+ expect(stateAfter).toBe(stateBefore);
153
+ });
154
+
155
+ test("requires test_plan.status to be pending_approval", async () => {
156
+ const projectRoot = await createProjectRoot();
157
+ createdRoots.push(projectRoot);
158
+ await seedState(projectRoot, "pending", "it_000003_test-plan.md");
159
+
160
+ await withCwd(projectRoot, async () => {
161
+ await expect(
162
+ runRefineTestPlan(
163
+ { provider: "codex", challenge: false },
164
+ {
165
+ loadSkillFn: async () => "unused",
166
+ invokeAgentFn: async () => ({ exitCode: 0, stdout: "", stderr: "" }),
167
+ },
168
+ ),
169
+ ).rejects.toThrow("Cannot refine test plan from status 'pending'. Expected pending_approval.");
170
+ });
171
+ });
172
+ });
173
+
174
+ describe("refine-test-plan skill definition", () => {
175
+ test("has YAML frontmatter and required editor/challenger guidance", async () => {
176
+ const skillPath = join(
177
+ process.cwd(),
178
+ ".agents",
179
+ "skills",
180
+ "refine-test-plan",
181
+ "SKILL.md",
182
+ );
183
+ const source = await readFile(skillPath, "utf8");
184
+
185
+ expect(source.startsWith("---\n")).toBe(true);
186
+ expect(source).toContain("name: refine-test-plan");
187
+ expect(source).toContain("description:");
188
+ expect(source).toContain("user-invocable: true");
189
+ expect(source).toContain("Editor mode");
190
+ expect(source).toContain("default");
191
+ expect(source).toContain("Preserve the existing section structure");
192
+ expect(source).toContain("Challenger mode");
193
+ expect(source).toContain("`mode = \"challenger\"`");
194
+ expect(source).toContain("Coverage gaps");
195
+ expect(source).toContain("Weak or non-verifiable assertions");
196
+ expect(source).toContain("Over-reliance on manual testing");
197
+ expect(source).toContain("Update `it_{current_iteration}_test-plan.md` in place.");
198
+ expect(source).toContain("Same output file path is preserved");
199
+ });
200
+ });
@@ -0,0 +1,93 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import {
5
+ buildPrompt,
6
+ invokeAgent,
7
+ loadSkill,
8
+ type AgentInvokeOptions,
9
+ type AgentProvider,
10
+ type AgentResult,
11
+ } from "../agent";
12
+ import { exists, FLOW_REL_DIR, readState } from "../state";
13
+
14
+ export interface RefineTestPlanOptions {
15
+ provider: AgentProvider;
16
+ challenge: boolean;
17
+ }
18
+
19
+ interface RefineTestPlanDeps {
20
+ existsFn: (path: string) => Promise<boolean>;
21
+ invokeAgentFn: (options: AgentInvokeOptions) => Promise<AgentResult>;
22
+ loadSkillFn: (projectRoot: string, skillName: string) => Promise<string>;
23
+ readFileFn: typeof readFile;
24
+ }
25
+
26
+ const defaultDeps: RefineTestPlanDeps = {
27
+ existsFn: exists,
28
+ invokeAgentFn: invokeAgent,
29
+ loadSkillFn: loadSkill,
30
+ readFileFn: readFile,
31
+ };
32
+
33
+ export async function runRefineTestPlan(
34
+ opts: RefineTestPlanOptions,
35
+ deps: Partial<RefineTestPlanDeps> = {},
36
+ ): Promise<void> {
37
+ const { provider, challenge } = opts;
38
+ const projectRoot = process.cwd();
39
+ const state = await readState(projectRoot);
40
+ const mergedDeps: RefineTestPlanDeps = { ...defaultDeps, ...deps };
41
+
42
+ const testPlan = state.phases.prototype.test_plan;
43
+ if (testPlan.status !== "pending_approval") {
44
+ throw new Error(
45
+ `Cannot refine test plan from status '${testPlan.status}'. Expected pending_approval.`,
46
+ );
47
+ }
48
+
49
+ const testPlanFile = testPlan.file;
50
+ if (!testPlanFile) {
51
+ throw new Error("Cannot refine test plan: prototype.test_plan.file is missing.");
52
+ }
53
+
54
+ const testPlanPath = join(projectRoot, FLOW_REL_DIR, testPlanFile);
55
+ if (!(await mergedDeps.existsFn(testPlanPath))) {
56
+ throw new Error(`Cannot refine test plan: file not found at ${testPlanPath}`);
57
+ }
58
+
59
+ let skillBody: string;
60
+ try {
61
+ skillBody = await mergedDeps.loadSkillFn(projectRoot, "refine-test-plan");
62
+ } catch {
63
+ throw new Error(
64
+ "Required skill missing: expected .agents/skills/refine-test-plan/SKILL.md.",
65
+ );
66
+ }
67
+
68
+ const testPlanContent = await mergedDeps.readFileFn(testPlanPath, "utf8");
69
+ const context: Record<string, string> = {
70
+ current_iteration: state.current_iteration,
71
+ test_plan_file: testPlanFile,
72
+ test_plan_content: testPlanContent,
73
+ };
74
+
75
+ if (challenge) {
76
+ context.mode = "challenger";
77
+ }
78
+
79
+ const prompt = buildPrompt(skillBody, context);
80
+
81
+ const result = await mergedDeps.invokeAgentFn({
82
+ provider,
83
+ prompt,
84
+ cwd: projectRoot,
85
+ interactive: true,
86
+ });
87
+
88
+ if (result.exitCode !== 0) {
89
+ throw new Error(`Agent invocation failed with exit code ${result.exitCode}.`);
90
+ }
91
+
92
+ console.log("Test plan refined.");
93
+ }
@@ -0,0 +1,144 @@
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 { readState } from "../state";
7
+ import { runStartIteration } from "./start-iteration";
8
+
9
+ async function createProjectRoot(): Promise<string> {
10
+ return mkdtemp(join(tmpdir(), "nvst-start-iteration-"));
11
+ }
12
+
13
+ async function withCwd<T>(cwd: string, fn: () => Promise<T>): Promise<T> {
14
+ const previous = process.cwd();
15
+ process.chdir(cwd);
16
+ try {
17
+ return await fn();
18
+ } finally {
19
+ process.chdir(previous);
20
+ }
21
+ }
22
+
23
+ const createdRoots: string[] = [];
24
+
25
+ afterEach(async () => {
26
+ await Promise.all(createdRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
27
+ });
28
+
29
+ describe("start-iteration command", () => {
30
+ test("registers start iteration in CLI dispatch", async () => {
31
+ const source = await readFile(join(process.cwd(), "src", "cli.ts"), "utf8");
32
+ expect(source).toContain('import { runStartIteration } from "./commands/start-iteration";');
33
+ expect(source).toContain("await runStartIteration();");
34
+ });
35
+
36
+ test("preserves project_context when already created (immutable across iterations)", async () => {
37
+ const projectRoot = await createProjectRoot();
38
+ createdRoots.push(projectRoot);
39
+ const flowDir = join(projectRoot, ".agents", "flow");
40
+ await mkdir(flowDir, { recursive: true });
41
+
42
+ // Seed state with project_context already created
43
+ await writeFile(
44
+ join(projectRoot, ".agents", "state.json"),
45
+ JSON.stringify(
46
+ {
47
+ current_iteration: "000008",
48
+ current_phase: "refactor",
49
+ phases: {
50
+ define: {
51
+ requirement_definition: { status: "approved", file: "it_000008_product-requirement-document.md" },
52
+ prd_generation: { status: "completed", file: "it_000008_PRD.json" },
53
+ },
54
+ prototype: {
55
+ project_context: { status: "created", file: ".agents/PROJECT_CONTEXT.md" },
56
+ test_plan: { status: "created", file: "it_000008_TP.json" },
57
+ tp_generation: { status: "created", file: "it_000008_TP.json" },
58
+ prototype_build: { status: "created", file: null },
59
+ test_execution: { status: "completed", file: null },
60
+ prototype_approved: true,
61
+ },
62
+ refactor: {
63
+ evaluation_report: { status: "created", file: null },
64
+ refactor_plan: { status: "approved", file: null },
65
+ refactor_execution: { status: "completed", file: null },
66
+ changelog: { status: "created", file: null },
67
+ },
68
+ },
69
+ last_updated: "2026-02-22T20:00:00.000Z",
70
+ history: [{ iteration: "000007", archived_at: "2026-02-22T19:00:00.000Z", archived_path: ".agents/flow/archived/000007" }],
71
+ },
72
+ null,
73
+ 2,
74
+ ),
75
+ );
76
+
77
+ // Create an iteration file to archive
78
+ await writeFile(join(flowDir, "it_000008_ISSUES.json"), "[]");
79
+
80
+ await withCwd(projectRoot, async () => {
81
+ await runStartIteration();
82
+ });
83
+
84
+ const state = await readState(projectRoot);
85
+ expect(state.current_iteration).toBe("000009");
86
+ expect(state.phases.prototype.project_context).toEqual({
87
+ status: "created",
88
+ file: ".agents/PROJECT_CONTEXT.md",
89
+ });
90
+ });
91
+
92
+ test("does not preserve project_context when it was pending", async () => {
93
+ const projectRoot = await createProjectRoot();
94
+ createdRoots.push(projectRoot);
95
+ const flowDir = join(projectRoot, ".agents", "flow");
96
+ await mkdir(flowDir, { recursive: true });
97
+
98
+ await writeFile(
99
+ join(projectRoot, ".agents", "state.json"),
100
+ JSON.stringify(
101
+ {
102
+ current_iteration: "000002",
103
+ current_phase: "define",
104
+ phases: {
105
+ define: {
106
+ requirement_definition: { status: "pending", file: null },
107
+ prd_generation: { status: "pending", file: null },
108
+ },
109
+ prototype: {
110
+ project_context: { status: "pending", file: null },
111
+ test_plan: { status: "pending", file: null },
112
+ tp_generation: { status: "pending", file: null },
113
+ prototype_build: { status: "pending", file: null },
114
+ test_execution: { status: "pending", file: null },
115
+ prototype_approved: false,
116
+ },
117
+ refactor: {
118
+ evaluation_report: { status: "pending", file: null },
119
+ refactor_plan: { status: "pending", file: null },
120
+ refactor_execution: { status: "pending", file: null },
121
+ changelog: { status: "pending", file: null },
122
+ },
123
+ },
124
+ last_updated: "2026-02-22T20:00:00.000Z",
125
+ history: [],
126
+ },
127
+ null,
128
+ 2,
129
+ ),
130
+ );
131
+
132
+ await writeFile(join(flowDir, "it_000002_PRD.json"), "{}");
133
+
134
+ await withCwd(projectRoot, async () => {
135
+ await runStartIteration();
136
+ });
137
+
138
+ const state = await readState(projectRoot);
139
+ expect(state.phases.prototype.project_context).toEqual({
140
+ status: "pending",
141
+ file: null,
142
+ });
143
+ });
144
+ });