@quinteroac/agents-coding-toolkit 0.1.0-preview → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +29 -15
  2. package/package.json +14 -4
  3. package/scaffold/.agents/flow/tmpl_it_000001_progress.example.json +20 -0
  4. package/scaffold/.agents/skills/execute-refactor-item/tmpl_SKILL.md +59 -0
  5. package/scaffold/.agents/skills/plan-refactor/tmpl_SKILL.md +89 -9
  6. package/scaffold/.agents/skills/refine-refactor-plan/tmpl_SKILL.md +30 -0
  7. package/scaffold/.agents/tmpl_state_rules.md +0 -1
  8. package/scaffold/schemas/tmpl_prototype-progress.ts +22 -0
  9. package/scaffold/schemas/tmpl_refactor-execution-progress.ts +16 -0
  10. package/scaffold/schemas/tmpl_refactor-prd.ts +14 -0
  11. package/scaffold/schemas/tmpl_state.ts +1 -0
  12. package/scaffold/schemas/tmpl_test-execution-progress.ts +17 -0
  13. package/schemas/issues.ts +19 -0
  14. package/schemas/prototype-progress.ts +22 -0
  15. package/schemas/refactor-execution-progress.ts +16 -0
  16. package/schemas/refactor-prd.ts +14 -0
  17. package/schemas/state.test.ts +58 -0
  18. package/schemas/state.ts +1 -0
  19. package/schemas/test-execution-progress.ts +17 -0
  20. package/schemas/test-plan.test.ts +1 -1
  21. package/schemas/validate-progress.ts +1 -1
  22. package/schemas/validate-state.ts +1 -1
  23. package/src/cli.test.ts +57 -0
  24. package/src/cli.ts +227 -58
  25. package/src/commands/approve-project-context.ts +13 -6
  26. package/src/commands/approve-prototype.test.ts +427 -0
  27. package/src/commands/approve-prototype.ts +185 -0
  28. package/src/commands/approve-refactor-plan.test.ts +254 -0
  29. package/src/commands/approve-refactor-plan.ts +200 -0
  30. package/src/commands/approve-requirement.test.ts +224 -0
  31. package/src/commands/approve-requirement.ts +75 -16
  32. package/src/commands/approve-test-plan.test.ts +2 -2
  33. package/src/commands/approve-test-plan.ts +21 -7
  34. package/src/commands/create-issue.test.ts +2 -2
  35. package/src/commands/create-project-context.ts +31 -25
  36. package/src/commands/create-prototype.test.ts +488 -18
  37. package/src/commands/create-prototype.ts +185 -63
  38. package/src/commands/create-test-plan.ts +8 -6
  39. package/src/commands/define-refactor-plan.test.ts +208 -0
  40. package/src/commands/define-refactor-plan.ts +96 -0
  41. package/src/commands/define-requirement.ts +15 -9
  42. package/src/commands/execute-automated-fix.test.ts +78 -33
  43. package/src/commands/execute-automated-fix.ts +34 -101
  44. package/src/commands/execute-refactor.test.ts +954 -0
  45. package/src/commands/execute-refactor.ts +332 -0
  46. package/src/commands/execute-test-plan.test.ts +24 -16
  47. package/src/commands/execute-test-plan.ts +29 -55
  48. package/src/commands/flow-config.ts +79 -0
  49. package/src/commands/flow.test.ts +755 -0
  50. package/src/commands/flow.ts +405 -0
  51. package/src/commands/refine-project-context.ts +9 -7
  52. package/src/commands/refine-refactor-plan.test.ts +210 -0
  53. package/src/commands/refine-refactor-plan.ts +95 -0
  54. package/src/commands/refine-requirement.ts +9 -6
  55. package/src/commands/refine-test-plan.test.ts +2 -2
  56. package/src/commands/refine-test-plan.ts +9 -6
  57. package/src/commands/start-iteration.test.ts +52 -0
  58. package/src/commands/start-iteration.ts +5 -0
  59. package/src/commands/write-json.ts +102 -97
  60. package/src/flow-cli.test.ts +18 -0
  61. package/src/force-flag.test.ts +144 -0
  62. package/src/guardrail.test.ts +411 -0
  63. package/src/guardrail.ts +82 -0
  64. package/src/install.test.ts +7 -5
  65. package/src/pack.test.ts +2 -1
  66. package/src/progress-utils.ts +34 -0
  67. package/src/readline.ts +23 -0
  68. package/src/write-json-artifact.ts +33 -0
  69. package/scaffold/.agents/flow/tmpl_README.md +0 -7
  70. package/scaffold/.agents/flow/tmpl_iteration_close_checklist.example.md +0 -11
  71. package/schemas/test-plan.ts +0 -20
@@ -0,0 +1,405 @@
1
+ import type { AgentProvider } from "../agent";
2
+ import { parseProvider } from "../agent";
3
+ import { runCreateProjectContext } from "./create-project-context";
4
+ import { runCreatePrototype } from "./create-prototype";
5
+ import { runCreateTestPlan } from "./create-test-plan";
6
+ import { runDefineRefactorPlan } from "./define-refactor-plan";
7
+ import { runDefineRequirement } from "./define-requirement";
8
+ import { runExecuteRefactor } from "./execute-refactor";
9
+ import { runExecuteTestPlan } from "./execute-test-plan";
10
+ import { GuardrailAbortError } from "../guardrail";
11
+ import { defaultReadLine } from "../readline";
12
+ import { readState } from "../state";
13
+ import type { State } from "../../scaffold/schemas/tmpl_state";
14
+ import {
15
+ buildApprovalGateMessage,
16
+ FLOW_APPROVAL_TARGETS,
17
+ type FlowStep,
18
+ FLOW_STEPS,
19
+ type FlowHandlerKey,
20
+ } from "./flow-config";
21
+
22
+ export interface FlowOptions {
23
+ provider?: AgentProvider;
24
+ force?: boolean;
25
+ }
26
+
27
+ type FlowDecision =
28
+ | { kind: "step"; step: FlowStep }
29
+ | { kind: "approval_gate"; message: string }
30
+ | { kind: "complete"; message: string }
31
+ | { kind: "blocked"; message: string };
32
+
33
+ interface FlowDeps {
34
+ readLineFn: () => Promise<string | null>;
35
+ readStateFn: (projectRoot: string) => Promise<State>;
36
+ runCreateProjectContextFn: typeof runCreateProjectContext;
37
+ runCreatePrototypeFn: typeof runCreatePrototype;
38
+ runCreateTestPlanFn: typeof runCreateTestPlan;
39
+ runDefineRefactorPlanFn: typeof runDefineRefactorPlan;
40
+ runDefineRequirementFn: typeof runDefineRequirement;
41
+ runExecuteRefactorFn: typeof runExecuteRefactor;
42
+ runExecuteTestPlanFn: typeof runExecuteTestPlan;
43
+ stderrWriteFn: (message: string) => void;
44
+ stdoutWriteFn: (message: string) => void;
45
+ }
46
+
47
+ const defaultDeps: FlowDeps = {
48
+ readLineFn: defaultReadLine,
49
+ readStateFn: readState,
50
+ runCreateProjectContextFn: runCreateProjectContext,
51
+ runCreatePrototypeFn: runCreatePrototype,
52
+ runCreateTestPlanFn: runCreateTestPlan,
53
+ runDefineRefactorPlanFn: runDefineRefactorPlan,
54
+ runDefineRequirementFn: runDefineRequirement,
55
+ runExecuteRefactorFn: runExecuteRefactor,
56
+ runExecuteTestPlanFn: runExecuteTestPlan,
57
+ stderrWriteFn: (message: string) => process.stderr.write(`${message}\n`),
58
+ stdoutWriteFn: (message: string) => process.stdout.write(`${message}\n`),
59
+ };
60
+
61
+ // Status semantics in flow orchestration:
62
+ // - "in_progress" may represent either a resumable execution step or an approval wait state.
63
+ // - For requirement_definition, "in_progress" means content exists and approval is required.
64
+ // - For build/execution steps, "in_progress" means work was interrupted/partial and should be resumed.
65
+ function isResumableInProgressStatus(status: string): boolean {
66
+ return status === "in_progress";
67
+ }
68
+
69
+ function isApprovalGateInProgressStatus(status: string): boolean {
70
+ return status === "in_progress";
71
+ }
72
+
73
+ function isRunnablePendingOrResumable(status: string): boolean {
74
+ return status === "pending" || isResumableInProgressStatus(status);
75
+ }
76
+
77
+ function buildIterationCompleteMessage(iteration: string): string {
78
+ return `Iteration ${iteration} complete. All phases finished.`;
79
+ }
80
+
81
+ function buildUnsupportedStatusMessage(path: string, status: string): string {
82
+ return `Unsupported status '${status}' at phases.${path}.`;
83
+ }
84
+
85
+ function buildNoRunnableStepMessage(path: string): string {
86
+ return `No runnable flow step found for phases.${path}.`;
87
+ }
88
+
89
+ function resolveDefinePhaseDecision(state: State): FlowDecision | null {
90
+ const define = state.phases.define;
91
+
92
+ for (const key of Object.keys(define) as Array<keyof typeof define>) {
93
+ if (key === "requirement_definition") {
94
+ const status = define.requirement_definition.status;
95
+ if (status === "pending") {
96
+ return { kind: "step", step: FLOW_STEPS["define-requirement"] };
97
+ }
98
+ if (isApprovalGateInProgressStatus(status)) {
99
+ return {
100
+ kind: "approval_gate",
101
+ message: buildApprovalGateMessage(FLOW_APPROVAL_TARGETS.requirement),
102
+ };
103
+ }
104
+ if (status !== "approved") {
105
+ return {
106
+ kind: "blocked",
107
+ message: buildUnsupportedStatusMessage("define.requirement_definition", status),
108
+ };
109
+ }
110
+ continue;
111
+ }
112
+
113
+ const status = define.prd_generation.status;
114
+ if (status === "completed") {
115
+ continue;
116
+ }
117
+ if (status === "pending") {
118
+ return {
119
+ kind: "blocked",
120
+ message: buildNoRunnableStepMessage("define.prd_generation"),
121
+ };
122
+ }
123
+ return {
124
+ kind: "blocked",
125
+ message: buildUnsupportedStatusMessage("define.prd_generation", status),
126
+ };
127
+ }
128
+
129
+ return null;
130
+ }
131
+
132
+ function resolvePrototypePhaseDecision(state: State): FlowDecision | null {
133
+ const prototype = state.phases.prototype;
134
+
135
+ for (const key of Object.keys(prototype) as Array<keyof typeof prototype>) {
136
+ if (key === "project_context") {
137
+ const status = prototype.project_context.status;
138
+ if (status === "pending") {
139
+ return { kind: "step", step: FLOW_STEPS["create-project-context"] };
140
+ }
141
+ if (status === "pending_approval") {
142
+ return {
143
+ kind: "approval_gate",
144
+ message: buildApprovalGateMessage(FLOW_APPROVAL_TARGETS.projectContext),
145
+ };
146
+ }
147
+ if (status !== "created") {
148
+ return {
149
+ kind: "blocked",
150
+ message: buildUnsupportedStatusMessage("prototype.project_context", status),
151
+ };
152
+ }
153
+ continue;
154
+ }
155
+
156
+ if (key === "test_plan") {
157
+ const status = prototype.test_plan.status;
158
+ if (status === "pending") {
159
+ return { kind: "step", step: FLOW_STEPS["create-test-plan"] };
160
+ }
161
+ if (status === "pending_approval") {
162
+ return {
163
+ kind: "approval_gate",
164
+ message: buildApprovalGateMessage(FLOW_APPROVAL_TARGETS.testPlan),
165
+ };
166
+ }
167
+ if (status !== "created") {
168
+ return {
169
+ kind: "blocked",
170
+ message: buildUnsupportedStatusMessage("prototype.test_plan", status),
171
+ };
172
+ }
173
+ continue;
174
+ }
175
+
176
+ if (key === "tp_generation") {
177
+ const status = prototype.tp_generation.status;
178
+ if (status === "created") {
179
+ continue;
180
+ }
181
+ if (status === "pending") {
182
+ return {
183
+ kind: "blocked",
184
+ message: buildNoRunnableStepMessage("prototype.tp_generation"),
185
+ };
186
+ }
187
+ return {
188
+ kind: "blocked",
189
+ message: buildUnsupportedStatusMessage("prototype.tp_generation", status),
190
+ };
191
+ }
192
+
193
+ if (key === "prototype_build") {
194
+ const status = prototype.prototype_build.status;
195
+ if (isRunnablePendingOrResumable(status)) {
196
+ return { kind: "step", step: FLOW_STEPS["create-prototype"] };
197
+ }
198
+ if (status !== "created") {
199
+ return {
200
+ kind: "blocked",
201
+ message: buildUnsupportedStatusMessage("prototype.prototype_build", status),
202
+ };
203
+ }
204
+ continue;
205
+ }
206
+
207
+ if (key === "test_execution") {
208
+ const status = prototype.test_execution.status;
209
+ if (status === "pending" || status === "in_progress" || status === "failed") {
210
+ return { kind: "step", step: FLOW_STEPS["execute-test-plan"] };
211
+ }
212
+ if (status !== "completed") {
213
+ return {
214
+ kind: "blocked",
215
+ message: buildUnsupportedStatusMessage("prototype.test_execution", status),
216
+ };
217
+ }
218
+ continue;
219
+ }
220
+
221
+ if (key === "prototype_approved") {
222
+ if (!prototype.prototype_approved) {
223
+ return {
224
+ kind: "approval_gate",
225
+ message: buildApprovalGateMessage(FLOW_APPROVAL_TARGETS.prototype),
226
+ };
227
+ }
228
+ }
229
+ }
230
+
231
+ return null;
232
+ }
233
+
234
+ function resolveRefactorPhaseDecision(state: State): FlowDecision | null {
235
+ const refactor = state.phases.refactor;
236
+
237
+ for (const key of Object.keys(refactor) as Array<keyof typeof refactor>) {
238
+ if (key === "evaluation_report") {
239
+ const status = refactor.evaluation_report.status;
240
+ if (status === "created") {
241
+ continue;
242
+ }
243
+ if (status === "pending") {
244
+ if (refactor.refactor_plan.status === "pending") {
245
+ return { kind: "step", step: FLOW_STEPS["define-refactor-plan"] };
246
+ }
247
+ if (refactor.refactor_plan.status === "pending_approval") {
248
+ return {
249
+ kind: "approval_gate",
250
+ message: buildApprovalGateMessage(FLOW_APPROVAL_TARGETS.refactorPlan),
251
+ };
252
+ }
253
+ return {
254
+ kind: "blocked",
255
+ message: buildNoRunnableStepMessage("refactor.evaluation_report"),
256
+ };
257
+ }
258
+ return {
259
+ kind: "blocked",
260
+ message: buildUnsupportedStatusMessage("refactor.evaluation_report", status),
261
+ };
262
+ }
263
+
264
+ if (key === "refactor_plan") {
265
+ const status = refactor.refactor_plan.status;
266
+ if (status === "pending") {
267
+ return { kind: "step", step: FLOW_STEPS["define-refactor-plan"] };
268
+ }
269
+ if (status === "pending_approval") {
270
+ return {
271
+ kind: "approval_gate",
272
+ message: buildApprovalGateMessage(FLOW_APPROVAL_TARGETS.refactorPlan),
273
+ };
274
+ }
275
+ if (status !== "approved") {
276
+ return {
277
+ kind: "blocked",
278
+ message: buildUnsupportedStatusMessage("refactor.refactor_plan", status),
279
+ };
280
+ }
281
+ continue;
282
+ }
283
+
284
+ if (key === "refactor_execution") {
285
+ const status = refactor.refactor_execution.status;
286
+ if (isRunnablePendingOrResumable(status)) {
287
+ return { kind: "step", step: FLOW_STEPS["execute-refactor"] };
288
+ }
289
+ if (status !== "completed") {
290
+ return {
291
+ kind: "blocked",
292
+ message: buildUnsupportedStatusMessage("refactor.refactor_execution", status),
293
+ };
294
+ }
295
+ continue;
296
+ }
297
+
298
+ if (key === "changelog") {
299
+ const status = refactor.changelog.status;
300
+ if (status === "pending" || status === "created") {
301
+ continue;
302
+ }
303
+ return {
304
+ kind: "blocked",
305
+ message: buildUnsupportedStatusMessage("refactor.changelog", status),
306
+ };
307
+ }
308
+ }
309
+
310
+ return null;
311
+ }
312
+
313
+ export function detectNextFlowDecision(state: State): FlowDecision {
314
+ for (const phase of Object.keys(state.phases) as Array<keyof State["phases"]>) {
315
+ const decision = phase === "define"
316
+ ? resolveDefinePhaseDecision(state)
317
+ : phase === "prototype"
318
+ ? resolvePrototypePhaseDecision(state)
319
+ : resolveRefactorPhaseDecision(state);
320
+
321
+ if (decision !== null) {
322
+ return decision;
323
+ }
324
+ }
325
+
326
+ return { kind: "complete", message: buildIterationCompleteMessage(state.current_iteration) };
327
+ }
328
+
329
+ async function ensureProvider(
330
+ provider: AgentProvider | undefined,
331
+ deps: FlowDeps,
332
+ ): Promise<AgentProvider> {
333
+ if (provider) {
334
+ return provider;
335
+ }
336
+
337
+ deps.stdoutWriteFn("Enter agent provider:");
338
+
339
+ const value = await deps.readLineFn();
340
+ if (value === null) {
341
+ throw new Error("Missing agent provider from stdin.");
342
+ }
343
+
344
+ return parseProvider(value.trim());
345
+ }
346
+
347
+ export async function runFlow(
348
+ opts: FlowOptions = {},
349
+ deps: Partial<FlowDeps> = {},
350
+ ): Promise<void> {
351
+ const mergedDeps: FlowDeps = { ...defaultDeps, ...deps };
352
+ const projectRoot = process.cwd();
353
+ let provider = opts.provider;
354
+ const force = opts.force ?? false;
355
+
356
+ while (true) {
357
+ const state = await mergedDeps.readStateFn(projectRoot);
358
+ const decision = detectNextFlowDecision(state);
359
+
360
+ if (decision.kind === "complete") {
361
+ mergedDeps.stdoutWriteFn(decision.message);
362
+ return;
363
+ }
364
+
365
+ if (decision.kind === "approval_gate") {
366
+ mergedDeps.stdoutWriteFn(decision.message);
367
+ return;
368
+ }
369
+
370
+ if (decision.kind === "blocked") {
371
+ mergedDeps.stderrWriteFn(decision.message);
372
+ process.exitCode = 1;
373
+ return;
374
+ }
375
+
376
+ const { step } = decision;
377
+ try {
378
+ if (step.requiresAgent) {
379
+ provider = await ensureProvider(provider, mergedDeps);
380
+ }
381
+
382
+ mergedDeps.stdoutWriteFn(`Running: bun nvst ${step.label}`);
383
+ const handlers: Record<FlowHandlerKey, () => Promise<void>> = {
384
+ runDefineRequirementFn: () => mergedDeps.runDefineRequirementFn({ provider: provider!, force }),
385
+ runCreateProjectContextFn: () => mergedDeps.runCreateProjectContextFn({ provider: provider!, mode: "strict", force }),
386
+ runCreatePrototypeFn: () => mergedDeps.runCreatePrototypeFn({ provider: provider!, force }),
387
+ runCreateTestPlanFn: () => mergedDeps.runCreateTestPlanFn({ provider: provider!, force }),
388
+ runExecuteTestPlanFn: () => mergedDeps.runExecuteTestPlanFn({ provider: provider!, force }),
389
+ runDefineRefactorPlanFn: () => mergedDeps.runDefineRefactorPlanFn({ provider: provider!, force }),
390
+ runExecuteRefactorFn: () => mergedDeps.runExecuteRefactorFn({ provider: provider!, force }),
391
+ };
392
+
393
+ await handlers[step.handlerKey]();
394
+ continue;
395
+ } catch (error) {
396
+ if (error instanceof GuardrailAbortError) {
397
+ throw error;
398
+ }
399
+ const message = error instanceof Error ? error.message : String(error);
400
+ mergedDeps.stderrWriteFn(message);
401
+ process.exitCode = 1;
402
+ return;
403
+ }
404
+ }
405
+ }
@@ -2,26 +2,28 @@ import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
4
  import { buildPrompt, invokeAgent, loadSkill, type AgentProvider } from "../agent";
5
+ import { assertGuardrail } from "../guardrail";
5
6
  import { exists, readState, writeState } from "../state";
6
7
 
7
8
  export interface RefineProjectContextOptions {
8
9
  provider: AgentProvider;
9
10
  challenge: boolean;
11
+ force?: boolean;
10
12
  }
11
13
 
12
14
  export async function runRefineProjectContext(opts: RefineProjectContextOptions): Promise<void> {
13
- const { provider, challenge } = opts;
15
+ const { provider, challenge, force = false } = opts;
14
16
  const projectRoot = process.cwd();
15
17
  const state = await readState(projectRoot);
16
18
 
17
19
  // US-003-AC01: Validate status is pending_approval or created
18
20
  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
- }
21
+ await assertGuardrail(
22
+ state,
23
+ projectContext.status !== "pending_approval" && projectContext.status !== "created",
24
+ `Cannot refine project context from status '${projectContext.status}'. Expected pending_approval or created.`,
25
+ { force },
26
+ );
25
27
 
26
28
  // Validate file reference exists in state
27
29
  const contextFile = projectContext.file;
@@ -0,0 +1,210 @@
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 { runRefineRefactorPlan } from "./refine-refactor-plan";
9
+
10
+ async function createProjectRoot(): Promise<string> {
11
+ return mkdtemp(join(tmpdir(), "nvst-refine-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
+ status: "pending" | "pending_approval" | "approved",
27
+ file: string | null,
28
+ ): Promise<void> {
29
+ await mkdir(join(projectRoot, ".agents", "flow"), { recursive: true });
30
+
31
+ await writeState(projectRoot, {
32
+ current_iteration: "000013",
33
+ current_phase: "refactor",
34
+ phases: {
35
+ define: {
36
+ requirement_definition: { status: "approved", file: "it_000013_product-requirement-document.md" },
37
+ prd_generation: { status: "completed", file: "it_000013_PRD.json" },
38
+ },
39
+ prototype: {
40
+ project_context: { status: "created", file: ".agents/PROJECT_CONTEXT.md" },
41
+ test_plan: { status: "created", file: "it_000013_test-plan.md" },
42
+ tp_generation: { status: "created", file: "it_000013_TEST-PLAN.json" },
43
+ prototype_build: { status: "created", file: "it_000013_progress.json" },
44
+ test_execution: { status: "completed", file: "it_000013_test-execution-report.json" },
45
+ prototype_approved: true,
46
+ },
47
+ refactor: {
48
+ evaluation_report: { status: "created", file: "it_000013_evaluation-report.md" },
49
+ refactor_plan: { status, file },
50
+ refactor_execution: { status: "pending", file: null },
51
+ changelog: { status: "pending", file: null },
52
+ },
53
+ },
54
+ last_updated: "2026-02-26T00: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 refactor-plan command", () => {
67
+ test("registers refine refactor-plan command in CLI dispatch", async () => {
68
+ const source = await readFile(join(process.cwd(), "src", "cli.ts"), "utf8");
69
+
70
+ expect(source).toContain('import { runRefineRefactorPlan } from "./commands/refine-refactor-plan";');
71
+ expect(source).toContain('if (subcommand === "refactor-plan") {');
72
+ expect(source).toContain('const challenge = postForceArgs.includes("--challenge");');
73
+ expect(source).toContain("await runRefineRefactorPlan({ provider, challenge, force });");
74
+ });
75
+
76
+ test("requires refactor.refactor_plan.status to be pending_approval", async () => {
77
+ const projectRoot = await createProjectRoot();
78
+ createdRoots.push(projectRoot);
79
+ await seedState(projectRoot, "approved", "it_000013_refactor-plan.md");
80
+
81
+ await withCwd(projectRoot, async () => {
82
+ await expect(
83
+ runRefineRefactorPlan(
84
+ { provider: "codex", challenge: false },
85
+ {
86
+ loadSkillFn: async () => "unused",
87
+ invokeAgentFn: async () => ({ exitCode: 0, stdout: "", stderr: "" }),
88
+ },
89
+ ),
90
+ ).rejects.toThrow(
91
+ "Cannot refine refactor plan from status 'approved'. Expected pending_approval.",
92
+ );
93
+ });
94
+ });
95
+
96
+ test("rejects when refactor.refactor_plan.file is missing", async () => {
97
+ const projectRoot = await createProjectRoot();
98
+ createdRoots.push(projectRoot);
99
+ await seedState(projectRoot, "pending_approval", null);
100
+
101
+ await withCwd(projectRoot, async () => {
102
+ await expect(
103
+ runRefineRefactorPlan(
104
+ { provider: "codex", challenge: false },
105
+ {
106
+ loadSkillFn: async () => "unused",
107
+ invokeAgentFn: async () => ({ exitCode: 0, stdout: "", stderr: "" }),
108
+ },
109
+ ),
110
+ ).rejects.toThrow("Cannot refine refactor plan: refactor.refactor_plan.file is missing.");
111
+ });
112
+ });
113
+
114
+ test("rejects when refactor plan file does not exist on disk", async () => {
115
+ const projectRoot = await createProjectRoot();
116
+ createdRoots.push(projectRoot);
117
+ await seedState(projectRoot, "pending_approval", "it_000013_refactor-plan.md");
118
+
119
+ await withCwd(projectRoot, async () => {
120
+ await expect(
121
+ runRefineRefactorPlan(
122
+ { provider: "codex", challenge: false },
123
+ {
124
+ loadSkillFn: async () => "unused",
125
+ invokeAgentFn: async () => ({ exitCode: 0, stdout: "", stderr: "" }),
126
+ },
127
+ ),
128
+ ).rejects.toThrow("Cannot refine refactor plan: file not found at");
129
+ });
130
+ });
131
+
132
+ test("loads refine-refactor-plan skill, reads file context, invokes interactively, and does not mutate state", async () => {
133
+ const projectRoot = await createProjectRoot();
134
+ createdRoots.push(projectRoot);
135
+ await seedState(projectRoot, "pending_approval", "it_000013_refactor-plan.md");
136
+
137
+ const refactorPlanPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-plan.md");
138
+ await writeFile(refactorPlanPath, "# Current Refactor Plan\n- Refactor module A\n", "utf8");
139
+
140
+ let loadedSkill = "";
141
+ let invocation: { interactive: boolean | undefined; prompt: string } | undefined;
142
+ const stateBefore = JSON.stringify(await readState(projectRoot));
143
+
144
+ await withCwd(projectRoot, async () => {
145
+ await runRefineRefactorPlan(
146
+ { provider: "codex", challenge: false },
147
+ {
148
+ loadSkillFn: async (_root, skillName) => {
149
+ loadedSkill = skillName;
150
+ return "Refine refactor plan skill";
151
+ },
152
+ invokeAgentFn: async (options): Promise<AgentResult> => {
153
+ invocation = {
154
+ interactive: options.interactive,
155
+ prompt: options.prompt,
156
+ };
157
+ return { exitCode: 0, stdout: "", stderr: "" };
158
+ },
159
+ },
160
+ );
161
+ });
162
+
163
+ expect(loadedSkill).toBe("refine-refactor-plan");
164
+ if (invocation === undefined) {
165
+ throw new Error("Agent invocation was not captured");
166
+ }
167
+
168
+ expect(invocation.interactive).toBe(true);
169
+ expect(invocation.prompt).toContain("### current_iteration");
170
+ expect(invocation.prompt).toContain("000013");
171
+ expect(invocation.prompt).toContain("### refactor_plan_file");
172
+ expect(invocation.prompt).toContain("it_000013_refactor-plan.md");
173
+ expect(invocation.prompt).toContain("### refactor_plan_content");
174
+ expect(invocation.prompt).toContain("# Current Refactor Plan");
175
+
176
+ const stateAfter = JSON.stringify(await readState(projectRoot));
177
+ expect(stateAfter).toBe(stateBefore);
178
+ });
179
+
180
+ test("passes mode=challenger in prompt context when challenge mode is enabled", async () => {
181
+ const projectRoot = await createProjectRoot();
182
+ createdRoots.push(projectRoot);
183
+ await seedState(projectRoot, "pending_approval", "it_000013_refactor-plan.md");
184
+
185
+ const refactorPlanPath = join(projectRoot, ".agents", "flow", "it_000013_refactor-plan.md");
186
+ await writeFile(refactorPlanPath, "# Current Refactor Plan\n- Refactor module B\n", "utf8");
187
+
188
+ let invocationPrompt = "";
189
+ const stateBefore = JSON.stringify(await readState(projectRoot));
190
+
191
+ await withCwd(projectRoot, async () => {
192
+ await runRefineRefactorPlan(
193
+ { provider: "codex", challenge: true },
194
+ {
195
+ loadSkillFn: async () => "Refine refactor plan skill",
196
+ invokeAgentFn: async (options): Promise<AgentResult> => {
197
+ invocationPrompt = options.prompt;
198
+ return { exitCode: 0, stdout: "", stderr: "" };
199
+ },
200
+ },
201
+ );
202
+ });
203
+
204
+ expect(invocationPrompt).toContain("### mode");
205
+ expect(invocationPrompt).toContain("challenger");
206
+
207
+ const stateAfter = JSON.stringify(await readState(projectRoot));
208
+ expect(stateAfter).toBe(stateBefore);
209
+ });
210
+ });