@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,95 @@
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 { assertGuardrail } from "../guardrail";
13
+ import { exists, FLOW_REL_DIR, readState } from "../state";
14
+
15
+ export interface RefineRefactorPlanOptions {
16
+ provider: AgentProvider;
17
+ challenge: boolean;
18
+ force?: boolean;
19
+ }
20
+
21
+ interface RefineRefactorPlanDeps {
22
+ existsFn: (path: string) => Promise<boolean>;
23
+ invokeAgentFn: (options: AgentInvokeOptions) => Promise<AgentResult>;
24
+ loadSkillFn: (projectRoot: string, skillName: string) => Promise<string>;
25
+ readFileFn: typeof readFile;
26
+ }
27
+
28
+ const defaultDeps: RefineRefactorPlanDeps = {
29
+ existsFn: exists,
30
+ invokeAgentFn: invokeAgent,
31
+ loadSkillFn: loadSkill,
32
+ readFileFn: readFile,
33
+ };
34
+
35
+ export async function runRefineRefactorPlan(
36
+ opts: RefineRefactorPlanOptions,
37
+ deps: Partial<RefineRefactorPlanDeps> = {},
38
+ ): Promise<void> {
39
+ const { provider, challenge, force = false } = opts;
40
+ const projectRoot = process.cwd();
41
+ const state = await readState(projectRoot);
42
+ const mergedDeps: RefineRefactorPlanDeps = { ...defaultDeps, ...deps };
43
+
44
+ const refactorPlan = state.phases.refactor.refactor_plan;
45
+ await assertGuardrail(
46
+ state,
47
+ refactorPlan.status !== "pending_approval",
48
+ `Cannot refine refactor plan from status '${refactorPlan.status}'. Expected pending_approval.`,
49
+ { force },
50
+ );
51
+
52
+ const refactorPlanFile = refactorPlan.file;
53
+ if (!refactorPlanFile) {
54
+ throw new Error("Cannot refine refactor plan: refactor.refactor_plan.file is missing.");
55
+ }
56
+
57
+ const refactorPlanPath = join(projectRoot, FLOW_REL_DIR, refactorPlanFile);
58
+ if (!(await mergedDeps.existsFn(refactorPlanPath))) {
59
+ throw new Error(`Cannot refine refactor plan: file not found at ${refactorPlanPath}`);
60
+ }
61
+
62
+ let skillBody: string;
63
+ try {
64
+ skillBody = await mergedDeps.loadSkillFn(projectRoot, "refine-refactor-plan");
65
+ } catch {
66
+ throw new Error(
67
+ "Required skill missing: expected .agents/skills/refine-refactor-plan/SKILL.md.",
68
+ );
69
+ }
70
+
71
+ const refactorPlanContent = await mergedDeps.readFileFn(refactorPlanPath, "utf8");
72
+ const context: Record<string, string> = {
73
+ current_iteration: state.current_iteration,
74
+ refactor_plan_file: refactorPlanFile,
75
+ refactor_plan_content: refactorPlanContent,
76
+ };
77
+
78
+ if (challenge) {
79
+ context.mode = "challenger";
80
+ }
81
+
82
+ const prompt = buildPrompt(skillBody, context);
83
+ const result = await mergedDeps.invokeAgentFn({
84
+ provider,
85
+ prompt,
86
+ cwd: projectRoot,
87
+ interactive: true,
88
+ });
89
+
90
+ if (result.exitCode !== 0) {
91
+ throw new Error(`Agent invocation failed with exit code ${result.exitCode}.`);
92
+ }
93
+
94
+ console.log("Refactor plan refined.");
95
+ }
@@ -2,24 +2,27 @@ 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, FLOW_REL_DIR } from "../state";
6
7
 
7
8
  export interface RefineRequirementOptions {
8
9
  provider: AgentProvider;
9
10
  challenge: boolean;
11
+ force?: boolean;
10
12
  }
11
13
 
12
14
  export async function runRefineRequirement(opts: RefineRequirementOptions): 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
  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
- }
20
+ await assertGuardrail(
21
+ state,
22
+ requirementDefinition.status !== "in_progress",
23
+ `Cannot refine requirement from status '${requirementDefinition.status}'. Expected in_progress.`,
24
+ { force },
25
+ );
23
26
 
24
27
  const requirementFile = requirementDefinition.file;
25
28
  if (!requirementFile) {
@@ -69,8 +69,8 @@ describe("refine test-plan command", () => {
69
69
 
70
70
  expect(source).toContain('import { runRefineTestPlan } from "./commands/refine-test-plan";');
71
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 });");
72
+ expect(source).toContain('const challenge = postForceArgs.includes("--challenge");');
73
+ expect(source).toContain("await runRefineTestPlan({ provider, challenge, force });");
74
74
  });
75
75
 
76
76
  test("loads refine-test-plan skill, reads test plan file context, invokes agent interactively, and does not update state", async () => {
@@ -9,11 +9,13 @@ import {
9
9
  type AgentProvider,
10
10
  type AgentResult,
11
11
  } from "../agent";
12
+ import { assertGuardrail } from "../guardrail";
12
13
  import { exists, FLOW_REL_DIR, readState } from "../state";
13
14
 
14
15
  export interface RefineTestPlanOptions {
15
16
  provider: AgentProvider;
16
17
  challenge: boolean;
18
+ force?: boolean;
17
19
  }
18
20
 
19
21
  interface RefineTestPlanDeps {
@@ -34,17 +36,18 @@ export async function runRefineTestPlan(
34
36
  opts: RefineTestPlanOptions,
35
37
  deps: Partial<RefineTestPlanDeps> = {},
36
38
  ): Promise<void> {
37
- const { provider, challenge } = opts;
39
+ const { provider, challenge, force = false } = opts;
38
40
  const projectRoot = process.cwd();
39
41
  const state = await readState(projectRoot);
40
42
  const mergedDeps: RefineTestPlanDeps = { ...defaultDeps, ...deps };
41
43
 
42
44
  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
- }
45
+ await assertGuardrail(
46
+ state,
47
+ testPlan.status !== "pending_approval",
48
+ `Cannot refine test plan from status '${testPlan.status}'. Expected pending_approval.`,
49
+ { force },
50
+ );
48
51
 
49
52
  const testPlanFile = testPlan.file;
50
53
  if (!testPlanFile) {
@@ -5,6 +5,8 @@ import type { ZodSchema } from "zod";
5
5
  import { StateSchema } from "../../scaffold/schemas/tmpl_state";
6
6
  import { ProgressSchema } from "../../scaffold/schemas/tmpl_progress";
7
7
  import { PrdSchema } from "../../scaffold/schemas/tmpl_prd";
8
+ import { RefactorPrdSchema } from "../../scaffold/schemas/tmpl_refactor-prd";
9
+ import { RefactorExecutionProgressSchema } from "../../scaffold/schemas/tmpl_refactor-execution-progress";
8
10
  import { TestPlanSchema } from "../../scaffold/schemas/tmpl_test-plan";
9
11
  import { IssuesSchema } from "../../scaffold/schemas/tmpl_issues";
10
12
 
@@ -12,11 +14,13 @@ import { IssuesSchema } from "../../scaffold/schemas/tmpl_issues";
12
14
  // Schema registry — maps CLI name → Zod schema
13
15
  // ---------------------------------------------------------------------------
14
16
  const SCHEMA_REGISTRY: Record<string, ZodSchema> = {
15
- state: StateSchema,
16
- progress: ProgressSchema,
17
- prd: PrdSchema,
18
- "test-plan": TestPlanSchema,
19
- issues: IssuesSchema,
17
+ state: StateSchema,
18
+ progress: ProgressSchema,
19
+ prd: PrdSchema,
20
+ "refactor-prd": RefactorPrdSchema,
21
+ "refactor-execution-progress": RefactorExecutionProgressSchema,
22
+ "test-plan": TestPlanSchema,
23
+ issues: IssuesSchema,
20
24
  };
21
25
 
22
26
  const SUPPORTED_SCHEMAS = Object.keys(SCHEMA_REGISTRY).join(", ");
@@ -25,112 +29,113 @@ const SUPPORTED_SCHEMAS = Object.keys(SCHEMA_REGISTRY).join(", ");
25
29
  // Argument parsing helpers
26
30
  // ---------------------------------------------------------------------------
27
31
  function extractFlag(args: string[], flag: string): { value: string | null; remaining: string[] } {
28
- const idx = args.indexOf(flag);
29
- if (idx === -1) return { value: null, remaining: args };
30
- if (idx + 1 >= args.length) {
31
- throw new Error(`Missing value for ${flag}`);
32
- }
33
- const value = args[idx + 1];
34
- const remaining = [...args.slice(0, idx), ...args.slice(idx + 2)];
35
- return { value, remaining };
32
+ const idx = args.indexOf(flag);
33
+ if (idx === -1) return { value: null, remaining: args };
34
+ if (idx + 1 >= args.length) {
35
+ throw new Error(`Missing value for ${flag}`);
36
+ }
37
+ const value = args[idx + 1];
38
+ const remaining = [...args.slice(0, idx), ...args.slice(idx + 2)];
39
+ return { value, remaining };
36
40
  }
37
41
 
38
42
  // ---------------------------------------------------------------------------
39
43
  // Read JSON payload from stdin (non-blocking, returns null if nothing piped)
40
44
  // ---------------------------------------------------------------------------
41
45
  async function readStdin(): Promise<string> {
42
- const chunks: Buffer[] = [];
43
- const reader = Bun.stdin.stream().getReader();
44
- while (true) {
45
- const { done, value } = await reader.read();
46
- if (done) break;
47
- chunks.push(Buffer.from(value));
48
- }
49
- return Buffer.concat(chunks).toString("utf-8");
46
+ const chunks: Buffer[] = [];
47
+ const reader = Bun.stdin.stream().getReader();
48
+ while (true) {
49
+ const { done, value } = await reader.read();
50
+ if (done) break;
51
+ chunks.push(Buffer.from(value));
52
+ }
53
+ return Buffer.concat(chunks).toString("utf-8");
50
54
  }
51
55
 
52
56
  // ---------------------------------------------------------------------------
53
57
  // Main entry point
54
58
  // ---------------------------------------------------------------------------
55
59
  export interface WriteJsonOptions {
56
- args: string[];
60
+ args: string[];
57
61
  }
58
62
 
59
63
  export async function runWriteJson({ args }: WriteJsonOptions): Promise<void> {
60
- // --- Parse --schema ---
61
- const { value: schemaName, remaining: afterSchema } = extractFlag(args, "--schema");
62
- if (!schemaName) {
63
- console.error("Error: --schema <name> is required.");
64
- console.error(`Supported schemas: ${SUPPORTED_SCHEMAS}`);
65
- process.exitCode = 1;
66
- return;
67
- }
68
-
69
- const schema = SCHEMA_REGISTRY[schemaName];
70
- if (!schema) {
71
- console.error(`Error: Unknown schema "${schemaName}".`);
72
- console.error(`Supported schemas: ${SUPPORTED_SCHEMAS}`);
73
- process.exitCode = 1;
74
- return;
75
- }
76
-
77
- // --- Parse --out ---
78
- const { value: outPath, remaining: afterOut } = extractFlag(afterSchema, "--out");
79
- if (!outPath) {
80
- console.error("Error: --out <path> is required.");
81
- process.exitCode = 1;
82
- return;
83
- }
84
-
85
- // --- Parse --data (optional) ---
86
- const { value: dataArg, remaining: afterData } = extractFlag(afterOut, "--data");
87
-
88
- // Reject unknown args
89
- if (afterData.length > 0) {
90
- console.error(`Error: Unknown option(s): ${afterData.join(" ")}`);
91
- process.exitCode = 1;
92
- return;
64
+ // --- Parse --schema ---
65
+ const { value: schemaName, remaining: afterSchema } = extractFlag(args, "--schema");
66
+ if (!schemaName) {
67
+ console.error("Error: --schema <name> is required.");
68
+ console.error(`Supported schemas: ${SUPPORTED_SCHEMAS}`);
69
+ process.exitCode = 1;
70
+ return;
71
+ }
72
+
73
+ const schema = SCHEMA_REGISTRY[schemaName];
74
+ if (!schema) {
75
+ console.error(`Error: Unknown schema "${schemaName}".`);
76
+ console.error(`Supported schemas: ${SUPPORTED_SCHEMAS}`);
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+
81
+ // --- Parse --out ---
82
+ const { value: outPath, remaining: afterOut } = extractFlag(afterSchema, "--out");
83
+ if (!outPath) {
84
+ console.error("Error: --out <path> is required.");
85
+ process.exitCode = 1;
86
+ return;
87
+ }
88
+
89
+ // --- Parse --data (optional) ---
90
+ const { value: dataArg, remaining: afterData } = extractFlag(afterOut, "--data");
91
+ const unknownArgs = afterData.filter((arg) => arg !== "--force");
92
+
93
+ // Reject unknown args
94
+ if (unknownArgs.length > 0) {
95
+ console.error(`Error: Unknown option(s): ${unknownArgs.join(" ")}`);
96
+ process.exitCode = 1;
97
+ return;
98
+ }
99
+
100
+ // --- Obtain JSON payload ---
101
+ let rawJson: string;
102
+ if (dataArg) {
103
+ rawJson = dataArg;
104
+ } else {
105
+ // Read from stdin
106
+ rawJson = await readStdin();
107
+ if (!rawJson.trim()) {
108
+ console.error("Error: No JSON payload provided. Use --data '<json>' or pipe via stdin.");
109
+ process.exitCode = 1;
110
+ return;
93
111
  }
94
-
95
- // --- Obtain JSON payload ---
96
- let rawJson: string;
97
- if (dataArg) {
98
- rawJson = dataArg;
99
- } else {
100
- // Read from stdin
101
- rawJson = await readStdin();
102
- if (!rawJson.trim()) {
103
- console.error("Error: No JSON payload provided. Use --data '<json>' or pipe via stdin.");
104
- process.exitCode = 1;
105
- return;
106
- }
107
- }
108
-
109
- // --- Parse JSON string ---
110
- let parsed: unknown;
111
- try {
112
- parsed = JSON.parse(rawJson);
113
- } catch (err) {
114
- console.error("Error: Invalid JSON input.");
115
- console.error((err as Error).message);
116
- process.exitCode = 1;
117
- return;
118
- }
119
-
120
- // --- Validate against schema ---
121
- const result = schema.safeParse(parsed);
122
- if (!result.success) {
123
- const formatted = result.error.format();
124
- console.error(JSON.stringify({ ok: false, schema: schemaName, errors: formatted }, null, 2));
125
- process.exitCode = 1;
126
- return;
127
- }
128
-
129
- // --- Write file ---
130
- const resolvedPath = resolve(process.cwd(), outPath);
131
- await mkdir(dirname(resolvedPath), { recursive: true });
132
- const content = `${JSON.stringify(result.data, null, 2)}\n`;
133
- await writeFile(resolvedPath, content, "utf-8");
134
-
135
- console.log(`Written: ${outPath}`);
112
+ }
113
+
114
+ // --- Parse JSON string ---
115
+ let parsed: unknown;
116
+ try {
117
+ parsed = JSON.parse(rawJson);
118
+ } catch (err) {
119
+ console.error("Error: Invalid JSON input.");
120
+ console.error((err as Error).message);
121
+ process.exitCode = 1;
122
+ return;
123
+ }
124
+
125
+ // --- Validate against schema ---
126
+ const result = schema.safeParse(parsed);
127
+ if (!result.success) {
128
+ const formatted = result.error.format();
129
+ console.error(JSON.stringify({ ok: false, schema: schemaName, errors: formatted }, null, 2));
130
+ process.exitCode = 1;
131
+ return;
132
+ }
133
+
134
+ // --- Write file ---
135
+ const resolvedPath = resolve(process.cwd(), outPath);
136
+ await mkdir(dirname(resolvedPath), { recursive: true });
137
+ const content = `${JSON.stringify(result.data, null, 2)}\n`;
138
+ await writeFile(resolvedPath, content, "utf-8");
139
+
140
+ console.log(`Written: ${outPath}`);
136
141
  }
@@ -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 type { State } from "../scaffold/schemas/tmpl_state";
7
+ import { runApproveProjectContext } from "./commands/approve-project-context";
8
+
9
+ const createdRoots: string[] = [];
10
+
11
+ function buildState(flow_guardrail: "strict" | "relaxed"): State {
12
+ return {
13
+ current_iteration: "000015",
14
+ current_phase: "prototype",
15
+ flow_guardrail,
16
+ phases: {
17
+ define: {
18
+ requirement_definition: { status: "approved", file: "it_000015_product-requirement-document.md" },
19
+ prd_generation: { status: "completed", file: "it_000015_PRD.json" },
20
+ },
21
+ prototype: {
22
+ project_context: { status: "pending", file: ".agents/PROJECT_CONTEXT.md" },
23
+ test_plan: { status: "pending", file: null },
24
+ tp_generation: { status: "pending", file: null },
25
+ prototype_build: { status: "pending", file: null },
26
+ test_execution: { status: "pending", file: null },
27
+ prototype_approved: false,
28
+ },
29
+ refactor: {
30
+ evaluation_report: { status: "pending", file: null },
31
+ refactor_plan: { status: "pending", file: null },
32
+ refactor_execution: { status: "pending", file: null },
33
+ changelog: { status: "pending", file: null },
34
+ },
35
+ },
36
+ last_updated: "2026-01-01T00:00:00.000Z",
37
+ updated_by: "test",
38
+ };
39
+ }
40
+
41
+ async function seedProject(flowGuardrail: "strict" | "relaxed"): Promise<string> {
42
+ const root = await mkdtemp(join(tmpdir(), "nvst-force-"));
43
+ createdRoots.push(root);
44
+ await mkdir(join(root, ".agents", "flow"), { recursive: true });
45
+ await writeFile(
46
+ join(root, ".agents", "state.json"),
47
+ `${JSON.stringify(buildState(flowGuardrail), null, 2)}\n`,
48
+ "utf8",
49
+ );
50
+ await writeFile(join(root, ".agents", "PROJECT_CONTEXT.md"), "# Project Context\n", "utf8");
51
+ return root;
52
+ }
53
+
54
+ afterEach(async () => {
55
+ await Promise.all(createdRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
56
+ });
57
+
58
+ describe("US-002 --force support", () => {
59
+ test("US-002-AC01: cli forwards --force to phase/status-validated command handlers", async () => {
60
+ const source = await readFile(join(import.meta.dir, "cli.ts"), "utf8");
61
+ expect(source).toContain("await runCreateProjectContext({ provider, mode, force })");
62
+ expect(source).toContain("await runDefineRequirement({ provider, force })");
63
+ expect(source).toContain("await runDefineRefactorPlan({ provider, force })");
64
+ expect(source).toContain("await runRefineRequirement({ provider, challenge, force })");
65
+ expect(source).toContain("await runRefineProjectContext({ provider, challenge, force })");
66
+ expect(source).toContain("await runRefineTestPlan({ provider, challenge, force })");
67
+ expect(source).toContain("await runRefineRefactorPlan({ provider, challenge, force })");
68
+ expect(source).toContain("await runApproveRequirement({ force })");
69
+ expect(source).toContain("await runApproveProjectContext({ force })");
70
+ expect(source).toContain("await runApproveTestPlan({ force })");
71
+ expect(source).toContain("await runApproveRefactorPlan({ force })");
72
+ expect(source).toContain("await runCreatePrototype({ provider, iterations, retryOnFail, stopOnCritical, force })");
73
+ expect(source).toContain("await runExecuteTestPlan({ provider, force })");
74
+ expect(source).toContain("await runExecuteRefactor({ provider, force })");
75
+ });
76
+
77
+ test("US-002-AC02/AC03: strict mode + --force warns and bypasses prompt", async () => {
78
+ const root = await seedProject("strict");
79
+ const previousCwd = process.cwd();
80
+ const messages: string[] = [];
81
+ const originalWrite = process.stderr.write.bind(process.stderr);
82
+ process.stderr.write = ((chunk: unknown) => {
83
+ messages.push(String(chunk));
84
+ return true;
85
+ }) as typeof process.stderr.write;
86
+
87
+ try {
88
+ process.chdir(root);
89
+ await runApproveProjectContext({ force: true });
90
+ } finally {
91
+ process.stderr.write = originalWrite;
92
+ process.chdir(previousCwd);
93
+ }
94
+
95
+ const state = JSON.parse(
96
+ await readFile(join(root, ".agents", "state.json"), "utf8"),
97
+ ) as State;
98
+ expect(state.phases.prototype.project_context.status).toBe("created");
99
+ expect(messages.join("")).toContain(
100
+ "Warning: Cannot approve project context from status 'pending'. Expected pending_approval.",
101
+ );
102
+ expect(messages.join("")).not.toContain("Proceed anyway? [y/N]");
103
+ });
104
+
105
+ test("US-002-AC03: relaxed mode + --force also bypasses prompt", async () => {
106
+ const root = await seedProject("relaxed");
107
+ const previousCwd = process.cwd();
108
+ const messages: string[] = [];
109
+ const originalWrite = process.stderr.write.bind(process.stderr);
110
+ process.stderr.write = ((chunk: unknown) => {
111
+ messages.push(String(chunk));
112
+ return true;
113
+ }) as typeof process.stderr.write;
114
+
115
+ try {
116
+ process.chdir(root);
117
+ await runApproveProjectContext({ force: true });
118
+ } finally {
119
+ process.stderr.write = originalWrite;
120
+ process.chdir(previousCwd);
121
+ }
122
+
123
+ expect(messages.join("")).toContain(
124
+ "Warning: Cannot approve project context from status 'pending'. Expected pending_approval.",
125
+ );
126
+ expect(messages.join("")).not.toContain("Proceed anyway? [y/N]");
127
+ });
128
+
129
+ test("US-002-AC04: commands without guardrail checks ignore --force when successful", async () => {
130
+ const root = await mkdtemp(join(tmpdir(), "nvst-force-cli-"));
131
+ createdRoots.push(root);
132
+ const cliPath = join(import.meta.dir, "cli.ts");
133
+ const proc = Bun.spawn(["bun", cliPath, "init", "--force"], {
134
+ cwd: root,
135
+ stdout: "pipe",
136
+ stderr: "pipe",
137
+ });
138
+ const exitCode = await proc.exited;
139
+ const stderrText = await new Response(proc.stderr).text();
140
+
141
+ expect(exitCode).toBe(0);
142
+ expect(stderrText).toBe("");
143
+ });
144
+ });