@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.
Files changed (50) hide show
  1. package/README.md +1 -1
  2. package/package.json +13 -4
  3. package/scaffold/.agents/skills/execute-refactor-item/tmpl_SKILL.md +59 -0
  4. package/scaffold/.agents/skills/plan-refactor/tmpl_SKILL.md +89 -9
  5. package/scaffold/.agents/skills/refine-refactor-plan/tmpl_SKILL.md +30 -0
  6. package/scaffold/.agents/tmpl_state_rules.md +0 -1
  7. package/scaffold/schemas/tmpl_refactor-execution-progress.ts +16 -0
  8. package/scaffold/schemas/tmpl_refactor-prd.ts +14 -0
  9. package/scaffold/schemas/tmpl_state.ts +1 -0
  10. package/schemas/refactor-execution-progress.ts +16 -0
  11. package/schemas/refactor-prd.ts +14 -0
  12. package/schemas/state.test.ts +58 -0
  13. package/schemas/state.ts +1 -0
  14. package/schemas/test-plan.test.ts +1 -1
  15. package/src/cli.test.ts +57 -0
  16. package/src/cli.ts +180 -56
  17. package/src/commands/approve-project-context.ts +13 -6
  18. package/src/commands/approve-refactor-plan.test.ts +254 -0
  19. package/src/commands/approve-refactor-plan.ts +200 -0
  20. package/src/commands/approve-requirement.test.ts +224 -0
  21. package/src/commands/approve-requirement.ts +75 -16
  22. package/src/commands/approve-test-plan.test.ts +2 -2
  23. package/src/commands/approve-test-plan.ts +21 -7
  24. package/src/commands/create-issue.test.ts +2 -2
  25. package/src/commands/create-project-context.ts +31 -25
  26. package/src/commands/create-prototype.test.ts +31 -13
  27. package/src/commands/create-prototype.ts +17 -7
  28. package/src/commands/create-test-plan.ts +8 -6
  29. package/src/commands/define-refactor-plan.test.ts +208 -0
  30. package/src/commands/define-refactor-plan.ts +96 -0
  31. package/src/commands/define-requirement.ts +15 -9
  32. package/src/commands/execute-refactor.test.ts +954 -0
  33. package/src/commands/execute-refactor.ts +336 -0
  34. package/src/commands/execute-test-plan.test.ts +9 -2
  35. package/src/commands/execute-test-plan.ts +13 -6
  36. package/src/commands/refine-project-context.ts +9 -7
  37. package/src/commands/refine-refactor-plan.test.ts +210 -0
  38. package/src/commands/refine-refactor-plan.ts +95 -0
  39. package/src/commands/refine-requirement.ts +9 -6
  40. package/src/commands/refine-test-plan.test.ts +2 -2
  41. package/src/commands/refine-test-plan.ts +9 -6
  42. package/src/commands/write-json.ts +102 -97
  43. package/src/force-flag.test.ts +144 -0
  44. package/src/guardrail.test.ts +411 -0
  45. package/src/guardrail.ts +104 -0
  46. package/src/install.test.ts +7 -5
  47. package/src/pack.test.ts +2 -1
  48. package/scaffold/.agents/flow/tmpl_README.md +0 -7
  49. package/scaffold/.agents/flow/tmpl_iteration_close_checklist.example.md +0 -11
  50. package/schemas/test-plan.ts +0 -20
@@ -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
+ });
@@ -0,0 +1,96 @@
1
+ import {
2
+ buildPrompt,
3
+ invokeAgent,
4
+ loadSkill,
5
+ type AgentInvokeOptions,
6
+ type AgentProvider,
7
+ type AgentResult,
8
+ } from "../agent";
9
+ import { assertGuardrail } from "../guardrail";
10
+ import { readState, writeState } from "../state";
11
+
12
+ export interface DefineRefactorPlanOptions {
13
+ provider: AgentProvider;
14
+ force?: boolean;
15
+ }
16
+
17
+ interface DefineRefactorPlanDeps {
18
+ invokeAgentFn: (options: AgentInvokeOptions) => Promise<AgentResult>;
19
+ loadSkillFn: (projectRoot: string, skillName: string) => Promise<string>;
20
+ nowFn: () => Date;
21
+ }
22
+
23
+ const defaultDeps: DefineRefactorPlanDeps = {
24
+ invokeAgentFn: invokeAgent,
25
+ loadSkillFn: loadSkill,
26
+ nowFn: () => new Date(),
27
+ };
28
+
29
+ export async function runDefineRefactorPlan(
30
+ opts: DefineRefactorPlanOptions,
31
+ deps: Partial<DefineRefactorPlanDeps> = {},
32
+ ): Promise<void> {
33
+ const { provider, force = false } = opts;
34
+ const projectRoot = process.cwd();
35
+ const state = await readState(projectRoot);
36
+ const mergedDeps: DefineRefactorPlanDeps = { ...defaultDeps, ...deps };
37
+
38
+ await assertGuardrail(
39
+ state,
40
+ !state.phases.prototype.prototype_approved,
41
+ "Cannot define refactor plan: phases.prototype.prototype_approved must be true. Complete prototype (all tests passing) first.",
42
+ { force },
43
+ );
44
+
45
+ // Intentional auto-transition: if the user runs this command directly from
46
+ // the prototype phase (prototype_approved === true), we advance to "refactor"
47
+ // so they don't have to manually update the phase. US-001-AC01 says the phase
48
+ // must be "refactor", but accepting "prototype" here is a UX convenience that
49
+ // is safe because prototype_approved is already enforced above.
50
+ if (state.current_phase === "prototype") {
51
+ state.current_phase = "refactor";
52
+ } else if (state.current_phase !== "refactor") {
53
+ await assertGuardrail(
54
+ state,
55
+ true,
56
+ `Cannot define refactor plan: current_phase must be 'prototype' or 'refactor'. Current: '${state.current_phase}'.`,
57
+ { force },
58
+ );
59
+ }
60
+
61
+ const refactorPlan = state.phases.refactor.refactor_plan;
62
+ await assertGuardrail(
63
+ state,
64
+ refactorPlan.status !== "pending",
65
+ `Cannot define refactor plan from status '${refactorPlan.status}'. Expected pending.`,
66
+ { force },
67
+ );
68
+
69
+ const skillBody = await mergedDeps.loadSkillFn(projectRoot, "plan-refactor");
70
+ const prompt = buildPrompt(skillBody, {
71
+ current_iteration: state.current_iteration,
72
+ });
73
+ const result = await mergedDeps.invokeAgentFn({
74
+ provider,
75
+ prompt,
76
+ cwd: projectRoot,
77
+ interactive: true,
78
+ });
79
+
80
+ if (result.exitCode !== 0) {
81
+ throw new Error(`Agent invocation failed with exit code ${result.exitCode}.`);
82
+ }
83
+
84
+ state.phases.refactor.evaluation_report.status = "created";
85
+ state.phases.refactor.evaluation_report.file = `it_${state.current_iteration}_evaluation-report.md`;
86
+ refactorPlan.status = "pending_approval";
87
+ refactorPlan.file = `it_${state.current_iteration}_refactor-plan.md`;
88
+ state.last_updated = mergedDeps.nowFn().toISOString();
89
+ state.updated_by = "nvst:define-refactor-plan";
90
+
91
+ await writeState(projectRoot, state);
92
+
93
+ console.log(
94
+ "Evaluation report and refactor plan created. Refactor plan is pending approval.",
95
+ );
96
+ }
@@ -1,25 +1,31 @@
1
1
  import { buildPrompt, invokeAgent, loadSkill, type AgentProvider } from "../agent";
2
+ import { assertGuardrail } from "../guardrail";
2
3
  import { readState, writeState } from "../state";
3
4
 
4
5
  export interface DefineRequirementOptions {
5
6
  provider: AgentProvider;
7
+ force?: boolean;
6
8
  }
7
9
 
8
10
  export async function runDefineRequirement(opts: DefineRequirementOptions): Promise<void> {
9
- const { provider } = opts;
11
+ const { provider, force = false } = opts;
10
12
  const projectRoot = process.cwd();
11
13
  const state = await readState(projectRoot);
12
14
 
13
- if (state.current_phase !== "define") {
14
- throw new Error("Cannot define requirement: current_phase must be 'define'.");
15
- }
15
+ await assertGuardrail(
16
+ state,
17
+ state.current_phase !== "define",
18
+ "Cannot define requirement: current_phase must be 'define'.",
19
+ { force },
20
+ );
16
21
 
17
22
  const requirementDefinition = state.phases.define.requirement_definition;
18
- if (requirementDefinition.status !== "pending") {
19
- throw new Error(
20
- `Cannot define requirement from status '${requirementDefinition.status}'. Expected pending.`,
21
- );
22
- }
23
+ await assertGuardrail(
24
+ state,
25
+ requirementDefinition.status !== "pending",
26
+ `Cannot define requirement from status '${requirementDefinition.status}'. Expected pending.`,
27
+ { force },
28
+ );
23
29
 
24
30
  const skillBody = await loadSkill(projectRoot, "create-pr-document");
25
31
  const prompt = buildPrompt(skillBody, {