@nathapp/nax 0.40.0 → 0.41.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 (49) hide show
  1. package/dist/nax.js +1166 -277
  2. package/package.json +2 -2
  3. package/src/acceptance/fix-generator.ts +4 -35
  4. package/src/acceptance/generator.ts +27 -28
  5. package/src/acceptance/refinement.ts +72 -5
  6. package/src/acceptance/templates/cli.ts +47 -0
  7. package/src/acceptance/templates/component.ts +78 -0
  8. package/src/acceptance/templates/e2e.ts +43 -0
  9. package/src/acceptance/templates/index.ts +21 -0
  10. package/src/acceptance/templates/snapshot.ts +50 -0
  11. package/src/acceptance/templates/unit.ts +48 -0
  12. package/src/acceptance/types.ts +9 -1
  13. package/src/agents/acp/adapter.ts +644 -0
  14. package/src/agents/acp/cost.ts +79 -0
  15. package/src/agents/acp/index.ts +9 -0
  16. package/src/agents/acp/interaction-bridge.ts +126 -0
  17. package/src/agents/acp/parser.ts +166 -0
  18. package/src/agents/acp/spawn-client.ts +309 -0
  19. package/src/agents/acp/types.ts +22 -0
  20. package/src/agents/claude-complete.ts +3 -3
  21. package/src/agents/registry.ts +83 -0
  22. package/src/agents/types-extended.ts +23 -0
  23. package/src/agents/types.ts +17 -0
  24. package/src/cli/analyze.ts +6 -2
  25. package/src/cli/init-detect.ts +94 -8
  26. package/src/cli/init.ts +2 -2
  27. package/src/cli/plan.ts +23 -0
  28. package/src/config/defaults.ts +1 -0
  29. package/src/config/index.ts +1 -1
  30. package/src/config/runtime-types.ts +17 -0
  31. package/src/config/schema.ts +3 -1
  32. package/src/config/schemas.ts +9 -1
  33. package/src/config/types.ts +2 -0
  34. package/src/execution/executor-types.ts +6 -0
  35. package/src/execution/iteration-runner.ts +2 -0
  36. package/src/execution/lifecycle/acceptance-loop.ts +5 -2
  37. package/src/execution/lifecycle/run-initialization.ts +16 -4
  38. package/src/execution/lifecycle/run-setup.ts +4 -0
  39. package/src/execution/runner-completion.ts +11 -1
  40. package/src/execution/runner-execution.ts +8 -0
  41. package/src/execution/runner-setup.ts +4 -0
  42. package/src/execution/runner.ts +10 -0
  43. package/src/pipeline/stages/acceptance-setup.ts +4 -0
  44. package/src/pipeline/stages/execution.ts +33 -1
  45. package/src/pipeline/stages/routing.ts +18 -7
  46. package/src/pipeline/types.ts +10 -0
  47. package/src/tdd/orchestrator.ts +7 -0
  48. package/src/tdd/rectification-gate.ts +6 -0
  49. package/src/tdd/session-runner.ts +4 -0
@@ -0,0 +1,22 @@
1
+ /**
2
+ * ACP Adapter Types
3
+ *
4
+ * Type definitions for the ACP (Agent Communication Protocol) adapter.
5
+ * The adapter shells out to `acpx` CLI — no in-process ACP client needed.
6
+ */
7
+
8
+ import type { ModelTier } from "../../config/schema";
9
+
10
+ /**
11
+ * Maps agent names to their acpx registry entries and capabilities.
12
+ */
13
+ export interface AgentRegistryEntry {
14
+ /** Agent name in acpx's built-in registry (e.g., 'claude', 'codex', 'gemini') */
15
+ binary: string;
16
+ /** Human-readable display name */
17
+ displayName: string;
18
+ /** Model tiers this agent supports */
19
+ supportedTiers: readonly ModelTier[];
20
+ /** Max context window in tokens */
21
+ maxContextTokens: number;
22
+ }
@@ -43,9 +43,9 @@ export async function executeComplete(binary: string, prompt: string, options?:
43
43
  cmd.push("--model", options.model);
44
44
  }
45
45
 
46
- if (options?.maxTokens !== undefined) {
47
- cmd.push("--max-tokens", String(options.maxTokens));
48
- }
46
+ // Note: Claude Code CLI does not support --max-tokens; the option is accepted in
47
+ // CompleteOptions for future use or non-Claude adapters, but is intentionally not
48
+ // forwarded to the claude binary here.
49
49
 
50
50
  if (options?.jsonMode) {
51
51
  cmd.push("--output-format", "json");
@@ -4,6 +4,9 @@
4
4
  * Discovers and manages available coding agents.
5
5
  */
6
6
 
7
+ import type { NaxConfig } from "../config/schema";
8
+ import { getLogger } from "../logger";
9
+ import { AcpAgentAdapter } from "./acp/adapter";
7
10
  import { AiderAdapter } from "./adapters/aider";
8
11
  import { CodexAdapter } from "./adapters/codex";
9
12
  import { GeminiAdapter } from "./adapters/gemini";
@@ -51,3 +54,83 @@ export async function checkAgentHealth(): Promise<Array<{ name: string; displayN
51
54
  })),
52
55
  );
53
56
  }
57
+
58
+ /** Protocol-aware agent registry returned by createAgentRegistry() */
59
+ export interface AgentRegistry {
60
+ /** Get a specific agent, respecting the configured protocol */
61
+ getAgent(name: string): AgentAdapter | undefined;
62
+ /** Get all installed agents */
63
+ getInstalledAgents(): Promise<AgentAdapter[]>;
64
+ /** Check health of all agents */
65
+ checkAgentHealth(): Promise<Array<{ name: string; displayName: string; installed: boolean }>>;
66
+ /** Active protocol ('acp' | 'cli') */
67
+ protocol: "acp" | "cli";
68
+ }
69
+
70
+ /**
71
+ * Create a protocol-aware agent registry.
72
+ *
73
+ * When config.agent.protocol is 'acp', returns AcpAgentAdapter instances.
74
+ * When 'cli' (or unset), returns legacy CLI adapters.
75
+ * AcpAgentAdapter instances are cached per agent name for the lifetime of the registry.
76
+ */
77
+ export function createAgentRegistry(config: NaxConfig): AgentRegistry {
78
+ const protocol: "acp" | "cli" = config.agent?.protocol ?? "cli";
79
+ const logger = getLogger();
80
+ const acpCache = new Map<string, AcpAgentAdapter>();
81
+
82
+ // Log which protocol is being used at startup
83
+ logger?.info("agents", `Agent protocol: ${protocol}`, { protocol, hasConfig: !!config.agent });
84
+
85
+ function getAgent(name: string): AgentAdapter | undefined {
86
+ if (protocol === "acp") {
87
+ const known = ALL_AGENTS.find((a) => a.name === name);
88
+ if (!known) return undefined;
89
+ if (!acpCache.has(name)) {
90
+ acpCache.set(name, new AcpAgentAdapter(name));
91
+ logger?.debug("agents", `Created AcpAgentAdapter for ${name}`, { name, protocol });
92
+ }
93
+ return acpCache.get(name);
94
+ }
95
+ const adapter = ALL_AGENTS.find((a) => a.name === name);
96
+ if (adapter) {
97
+ logger?.debug("agents", `Using CLI adapter for ${name}: ${adapter.constructor.name}`, { name });
98
+ }
99
+ return adapter;
100
+ }
101
+
102
+ async function getInstalledAgents(): Promise<AgentAdapter[]> {
103
+ const agents =
104
+ protocol === "acp"
105
+ ? ALL_AGENTS.map((a) => {
106
+ if (!acpCache.has(a.name)) {
107
+ acpCache.set(a.name, new AcpAgentAdapter(a.name));
108
+ }
109
+ return acpCache.get(a.name) as AcpAgentAdapter;
110
+ })
111
+ : ALL_AGENTS;
112
+ const results = await Promise.all(agents.map(async (agent) => ({ agent, installed: await agent.isInstalled() })));
113
+ return results.filter((r) => r.installed).map((r) => r.agent);
114
+ }
115
+
116
+ async function checkAgentHealth(): Promise<Array<{ name: string; displayName: string; installed: boolean }>> {
117
+ const agents =
118
+ protocol === "acp"
119
+ ? ALL_AGENTS.map((a) => {
120
+ if (!acpCache.has(a.name)) {
121
+ acpCache.set(a.name, new AcpAgentAdapter(a.name));
122
+ }
123
+ return acpCache.get(a.name) as AcpAgentAdapter;
124
+ })
125
+ : ALL_AGENTS;
126
+ return Promise.all(
127
+ agents.map(async (agent) => ({
128
+ name: agent.name,
129
+ displayName: agent.displayName,
130
+ installed: await agent.isInstalled(),
131
+ })),
132
+ );
133
+ }
134
+
135
+ return { getAgent, getInstalledAgents, checkAgentHealth, protocol };
136
+ }
@@ -30,6 +30,29 @@ export interface PlanOptions {
30
30
  modelDef?: ModelDef;
31
31
  /** Global config — used to resolve models.balanced when modelDef is absent */
32
32
  config?: Partial<NaxConfig>;
33
+ /**
34
+ * Interaction bridge for mid-session human Q&A (ACP only).
35
+ * If provided, the agent can pause and ask clarifying questions during planning.
36
+ */
37
+ interactionBridge?: {
38
+ detectQuestion: (text: string) => Promise<boolean>;
39
+ onQuestionDetected: (text: string) => Promise<string>;
40
+ };
41
+ /** Feature name for ACP session naming (plan→run continuity) */
42
+ featureName?: string;
43
+ /** Story ID for ACP session naming (plan→run continuity) */
44
+ storyId?: string;
45
+ /** Session role for TDD isolation (e.g. "test-writer" | "implementer" | "verifier") */
46
+ sessionRole?: string;
47
+ /** Timeout in seconds — inherited from config.execution.sessionTimeoutSeconds */
48
+ timeoutSeconds?: number;
49
+ /** Whether to skip permission prompts (maps to permissionMode in ACP) */
50
+ dangerouslySkipPermissions?: boolean;
51
+ /**
52
+ * Callback invoked with the ACP session name after the session is created.
53
+ * Used to persist the name to status.json for plan→run session continuity.
54
+ */
55
+ onAcpSessionCreated?: (sessionName: string) => Promise<void> | void;
33
56
  }
34
57
 
35
58
  /**
@@ -59,6 +59,21 @@ export interface AgentRunOptions {
59
59
  env?: Record<string, string>;
60
60
  /** Use --dangerously-skip-permissions flag (default: true) */
61
61
  dangerouslySkipPermissions?: boolean;
62
+ /** Interaction bridge for mid-session human interaction (ACP) */
63
+ interactionBridge?: {
64
+ detectQuestion: (text: string) => Promise<boolean>;
65
+ onQuestionDetected: (text: string) => Promise<string>;
66
+ };
67
+ /** PID registry for cleanup on crash/SIGTERM */
68
+ pidRegistry?: import("../execution/pid-registry").PidRegistry;
69
+ /** ACP session name to resume for plan→run session continuity */
70
+ acpSessionName?: string;
71
+ /** Feature name for ACP session naming and logging */
72
+ featureName?: string;
73
+ /** Story ID for ACP session naming and logging */
74
+ storyId?: string;
75
+ /** Session role for TDD isolation (e.g. "test-writer" | "implementer" | "verifier") */
76
+ sessionRole?: string;
62
77
  }
63
78
 
64
79
  /**
@@ -83,6 +98,8 @@ export interface CompleteOptions {
83
98
  jsonMode?: boolean;
84
99
  /** Override the model (adds --model flag) */
85
100
  model?: string;
101
+ /** Whether to skip permission prompts (maps to permissionMode in ACP) */
102
+ dangerouslySkipPermissions?: boolean;
86
103
  }
87
104
 
88
105
  /**
@@ -223,9 +223,13 @@ async function runDecomposeDefault(
223
223
  maxComplexity: naxDecompose?.maxSubstoryComplexity ?? "medium",
224
224
  maxRetries: naxDecompose?.maxRetries ?? 2,
225
225
  };
226
+ const agent = getAgent(config.autoMode.defaultAgent);
227
+ if (!agent) {
228
+ throw new Error(`[decompose] Agent "${config.autoMode.defaultAgent}" not found — cannot decompose`);
229
+ }
226
230
  const adapter = {
227
- async decompose(_prompt: string): Promise<string> {
228
- throw new Error("[decompose] No LLM adapter configured for story decomposition");
231
+ async decompose(prompt: string): Promise<string> {
232
+ return agent.complete(prompt, { jsonMode: true });
229
233
  },
230
234
  };
231
235
  return DecomposeBuilder.for(story).prd(prd).config(builderConfig).decompose(adapter);
@@ -5,12 +5,74 @@
5
5
  * for nax/config.json.
6
6
  */
7
7
 
8
- import { existsSync } from "node:fs";
8
+ import { existsSync, readFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
 
11
11
  /** Detected project runtime */
12
12
  export type Runtime = "bun" | "node" | "unknown";
13
13
 
14
+ /** Detected UI framework */
15
+ export type UIFramework = "ink" | "react" | "vue" | "svelte";
16
+
17
+ /** Full stack info including UI framework and bin detection */
18
+ export interface StackInfo extends ProjectStack {
19
+ uiFramework?: UIFramework;
20
+ hasBin?: boolean;
21
+ }
22
+
23
+ /** Shape of a parsed package.json for detection purposes */
24
+ interface PackageJson {
25
+ dependencies?: Record<string, string>;
26
+ devDependencies?: Record<string, string>;
27
+ peerDependencies?: Record<string, string>;
28
+ bin?: Record<string, string> | string;
29
+ }
30
+
31
+ function readPackageJson(projectRoot: string): PackageJson | undefined {
32
+ const pkgPath = join(projectRoot, "package.json");
33
+ if (!existsSync(pkgPath)) return undefined;
34
+ try {
35
+ return JSON.parse(readFileSync(pkgPath, "utf-8")) as PackageJson;
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ }
40
+
41
+ function allDeps(pkg: PackageJson): Record<string, string> {
42
+ return {
43
+ ...pkg.dependencies,
44
+ ...pkg.devDependencies,
45
+ ...pkg.peerDependencies,
46
+ };
47
+ }
48
+
49
+ function detectUIFramework(pkg: PackageJson): UIFramework | undefined {
50
+ const deps = allDeps(pkg);
51
+ if ("ink" in deps) return "ink";
52
+ if ("react" in deps || "next" in deps) return "react";
53
+ if ("vue" in deps || "nuxt" in deps) return "vue";
54
+ if ("svelte" in deps || "@sveltejs/kit" in deps) return "svelte";
55
+ return undefined;
56
+ }
57
+
58
+ function detectHasBin(pkg: PackageJson): boolean {
59
+ return pkg.bin !== undefined;
60
+ }
61
+
62
+ /**
63
+ * Detect the project stack including UI framework from package.json.
64
+ */
65
+ export function detectStack(projectRoot: string): StackInfo {
66
+ const base = detectProjectStack(projectRoot);
67
+ const pkg = readPackageJson(projectRoot);
68
+ if (!pkg) return base;
69
+ return {
70
+ ...base,
71
+ uiFramework: detectUIFramework(pkg),
72
+ hasBin: detectHasBin(pkg) || undefined,
73
+ };
74
+ }
75
+
14
76
  /** Detected project language */
15
77
  export type Language = "typescript" | "python" | "rust" | "go" | "unknown";
16
78
 
@@ -146,24 +208,48 @@ function isStackDetected(stack: ProjectStack): boolean {
146
208
  return stack.runtime !== "unknown" || stack.language !== "unknown";
147
209
  }
148
210
 
211
+ /** Build the acceptance config section from StackInfo, or undefined if not applicable. */
212
+ function buildAcceptanceConfig(stack: StackInfo): { testStrategy: string; testFramework?: string } | undefined {
213
+ if (stack.uiFramework === "ink") {
214
+ return { testStrategy: "component", testFramework: "ink-testing-library" };
215
+ }
216
+ if (stack.uiFramework === "react") {
217
+ return { testStrategy: "component", testFramework: "@testing-library/react" };
218
+ }
219
+ if (stack.uiFramework === "vue") {
220
+ return { testStrategy: "component", testFramework: "@testing-library/vue" };
221
+ }
222
+ if (stack.uiFramework === "svelte") {
223
+ return { testStrategy: "component", testFramework: "@testing-library/svelte" };
224
+ }
225
+ if (stack.hasBin) {
226
+ const testFramework = stack.runtime === "bun" ? "bun:test" : "jest";
227
+ return { testStrategy: "cli", testFramework };
228
+ }
229
+ return undefined;
230
+ }
231
+
149
232
  /**
150
233
  * Build the full init config object from a detected project stack.
151
234
  * Falls back to minimal config when stack is undetected.
152
235
  */
153
- export function buildInitConfig(stack: ProjectStack): object {
236
+ export function buildInitConfig(stack: ProjectStack | StackInfo): object {
237
+ const stackInfo = stack as StackInfo;
238
+ const acceptance = buildAcceptanceConfig(stackInfo);
239
+
154
240
  if (!isStackDetected(stack)) {
155
- return { version: 1 };
241
+ return acceptance ? { version: 1, acceptance } : { version: 1 };
156
242
  }
157
243
 
158
244
  const commands = buildQualityCommands(stack);
159
245
  const hasCommands = Object.keys(commands).length > 0;
160
246
 
161
- if (!hasCommands) {
247
+ if (!hasCommands && !acceptance) {
162
248
  return { version: 1 };
163
249
  }
164
250
 
165
- return {
166
- version: 1,
167
- quality: { commands },
168
- };
251
+ const config: Record<string, unknown> = { version: 1 };
252
+ if (hasCommands) config.quality = { commands };
253
+ if (acceptance) config.acceptance = acceptance;
254
+ return config;
169
255
  }
package/src/cli/init.ts CHANGED
@@ -10,7 +10,7 @@ import { join } from "node:path";
10
10
  import { globalConfigDir, projectConfigDir } from "../config/paths";
11
11
  import { getLogger } from "../logger";
12
12
  import { initContext } from "./init-context";
13
- import { buildInitConfig, detectProjectStack } from "./init-detect";
13
+ import { buildInitConfig, detectStack } from "./init-detect";
14
14
  import type { ProjectStack } from "./init-detect";
15
15
  import { promptsInitCommand } from "./prompts";
16
16
 
@@ -183,7 +183,7 @@ export async function initProject(projectRoot: string, options?: InitProjectOpti
183
183
  }
184
184
 
185
185
  // Detect project stack and build config
186
- const stack = detectProjectStack(projectRoot);
186
+ const stack = detectStack(projectRoot);
187
187
  const projectConfig = buildInitConfig(stack);
188
188
  logger.info("init", "Detected project stack", {
189
189
  runtime: stack.runtime,
package/src/cli/plan.ts CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { existsSync } from "node:fs";
9
9
  import { join } from "node:path";
10
+ import { createInterface } from "node:readline";
10
11
  import { ClaudeCodeAdapter } from "../agents/claude";
11
12
  import type { PlanOptions } from "../agents/types";
12
13
  import { scanCodebase } from "../analyze/scanner";
@@ -14,6 +15,26 @@ import type { NaxConfig } from "../config";
14
15
  import { resolveModel } from "../config/schema";
15
16
  import { getLogger } from "../logger";
16
17
 
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+ // Question detection helpers for ACP interaction bridge
20
+ // ─────────────────────────────────────────────────────────────────────────────
21
+
22
+ const QUESTION_PATTERNS = [/\?[\s]*$/, /\bwhich\b/i, /\bshould i\b/i, /\bdo you want\b/i, /\bwould you like\b/i];
23
+
24
+ async function detectQuestion(text: string): Promise<boolean> {
25
+ return QUESTION_PATTERNS.some((p) => p.test(text.trim()));
26
+ }
27
+
28
+ async function askHuman(question: string): Promise<string> {
29
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
30
+ return new Promise((resolve) => {
31
+ rl.question(`\n[Agent asks]: ${question}\nYour answer: `, (answer) => {
32
+ rl.close();
33
+ resolve(answer.trim());
34
+ });
35
+ });
36
+ }
37
+
17
38
  /**
18
39
  * Template for structured specification output.
19
40
  *
@@ -92,6 +113,8 @@ export async function planCommand(
92
113
  modelTier,
93
114
  modelDef,
94
115
  config,
116
+ // Wire ACP interaction bridge for mid-session Q&A (only in interactive mode)
117
+ interactionBridge: interactive ? { detectQuestion, onQuestionDetected: askHuman } : undefined,
95
118
  };
96
119
 
97
120
  // Run agent in plan mode
@@ -203,6 +203,7 @@ export const DEFAULT_CONFIG: NaxConfig = {
203
203
  },
204
204
  },
205
205
  prompts: {},
206
+ // agent: intentionally omitted — cli is the default when no agent config is set
206
207
  decompose: {
207
208
  trigger: "auto",
208
209
  maxAcceptanceCriteria: 6,
@@ -14,7 +14,7 @@ export type {
14
14
  TierConfig,
15
15
  RectificationConfig,
16
16
  } from "./schema";
17
- export { DEFAULT_CONFIG, resolveModel, NaxConfigSchema } from "./schema";
17
+ export { DEFAULT_CONFIG, resolveModel, NaxConfigSchema, AcceptanceConfigSchema } from "./schema";
18
18
  export { loadConfig, findProjectDir, globalConfigPath } from "./loader";
19
19
  export { validateConfig, type ValidationResult } from "./validate"; // @deprecated: Use NaxConfigSchema.safeParse() instead
20
20
  export { validateDirectory, validateFilePath, isWithinDirectory, MAX_DIRECTORY_DEPTH } from "./path-security";
@@ -228,6 +228,9 @@ export interface PlanConfig {
228
228
  outputPath: string;
229
229
  }
230
230
 
231
+ /** Valid test strategy values for acceptance testing */
232
+ export type AcceptanceTestStrategy = "unit" | "component" | "cli" | "e2e" | "snapshot";
233
+
231
234
  /** Acceptance validation config */
232
235
  export interface AcceptanceConfig {
233
236
  /** Enable acceptance test generation and validation */
@@ -244,6 +247,10 @@ export interface AcceptanceConfig {
244
247
  refinement: boolean;
245
248
  /** Whether to run RED gate check after generating acceptance tests (default: true) */
246
249
  redGate: boolean;
250
+ /** Test strategy for acceptance tests (default: auto-detect) */
251
+ testStrategy?: AcceptanceTestStrategy;
252
+ /** Test framework for acceptance tests (default: auto-detect) */
253
+ testFramework?: string;
247
254
  }
248
255
 
249
256
  /** Optimizer config (v0.10) */
@@ -458,4 +465,14 @@ export interface NaxConfig {
458
465
  prompts?: PromptsConfig;
459
466
  /** Decompose settings (SD-003) */
460
467
  decompose?: DecomposeConfig;
468
+ /** Agent protocol settings (ACP-003) */
469
+ agent?: AgentConfig;
470
+ }
471
+
472
+ /** Agent protocol configuration (ACP-003) */
473
+ export interface AgentConfig {
474
+ /** Protocol to use for agent communication (default: 'acp') */
475
+ protocol?: "acp" | "cli";
476
+ /** ACP permission mode (default: 'approve-all') */
477
+ acpPermissionMode?: string;
461
478
  }
@@ -30,6 +30,7 @@ export type {
30
30
  ReviewConfig,
31
31
  PlanConfig,
32
32
  AcceptanceConfig,
33
+ AcceptanceTestStrategy,
33
34
  OptimizerConfig,
34
35
  PluginConfigEntry,
35
36
  HooksConfig,
@@ -47,12 +48,13 @@ export type {
47
48
  SmartTestRunnerConfig,
48
49
  DecomposeConfig,
49
50
  NaxConfig,
51
+ AgentConfig,
50
52
  } from "./types";
51
53
 
52
54
  export { resolveModel } from "./types";
53
55
 
54
56
  // Zod schemas
55
- export { NaxConfigSchema } from "./schemas";
57
+ export { NaxConfigSchema, AcceptanceConfigSchema } from "./schemas";
56
58
 
57
59
  // Default config
58
60
  export { DEFAULT_CONFIG } from "./defaults";
@@ -210,7 +210,7 @@ const PlanConfigSchema = z.object({
210
210
  outputPath: z.string().min(1, "plan.outputPath must be non-empty"),
211
211
  });
212
212
 
213
- const AcceptanceConfigSchema = z.object({
213
+ export const AcceptanceConfigSchema = z.object({
214
214
  enabled: z.boolean(),
215
215
  maxRetries: z.number().int().nonnegative(),
216
216
  generateTests: z.boolean(),
@@ -218,6 +218,8 @@ const AcceptanceConfigSchema = z.object({
218
218
  model: z.enum(["fast", "balanced", "powerful"]).default("fast"),
219
219
  refinement: z.boolean().default(true),
220
220
  redGate: z.boolean().default(true),
221
+ testStrategy: z.enum(["unit", "component", "cli", "e2e", "snapshot"]).optional(),
222
+ testFramework: z.string().min(1, "acceptance.testFramework must be non-empty").optional(),
221
223
  });
222
224
 
223
225
  const TestCoverageConfigSchema = z.object({
@@ -324,6 +326,11 @@ const StorySizeGateConfigSchema = z.object({
324
326
  maxBulletPoints: z.number().int().min(1).max(100).default(8),
325
327
  });
326
328
 
329
+ const AgentConfigSchema = z.object({
330
+ protocol: z.enum(["acp", "cli"]).default("acp"),
331
+ acpPermissionMode: z.string().optional(),
332
+ });
333
+
327
334
  const PrecheckConfigSchema = z.object({
328
335
  storySizeGate: StorySizeGateConfigSchema,
329
336
  });
@@ -370,6 +377,7 @@ export const NaxConfigSchema = z
370
377
  disabledPlugins: z.array(z.string()).optional(),
371
378
  hooks: HooksConfigSchema.optional(),
372
379
  interaction: InteractionConfigSchema.optional(),
380
+ agent: AgentConfigSchema.optional(),
373
381
  precheck: PrecheckConfigSchema.optional(),
374
382
  prompts: PromptsConfigSchema.optional(),
375
383
  decompose: DecomposeConfigSchema.optional(),
@@ -24,6 +24,7 @@ export { resolveModel } from "./schema-types";
24
24
  // Runtime types
25
25
  export type {
26
26
  AcceptanceConfig,
27
+ AcceptanceTestStrategy,
27
28
  AnalyzeConfig,
28
29
  AutoModeConfig,
29
30
  ConstitutionConfig,
@@ -51,4 +52,5 @@ export type {
51
52
  TddConfig,
52
53
  TestCoverageConfig,
53
54
  AdaptiveRoutingConfig,
55
+ AgentConfig,
54
56
  } from "./runtime-types";
@@ -10,10 +10,12 @@ import type { InteractionChain } from "../interaction/chain";
10
10
  import type { StoryMetrics } from "../metrics";
11
11
  import type { PipelineEventEmitter } from "../pipeline/events";
12
12
  import type { RoutingResult } from "../pipeline/types";
13
+ import type { AgentGetFn } from "../pipeline/types";
13
14
  import type { PluginRegistry } from "../plugins";
14
15
  import type { PRD, UserStory } from "../prd/types";
15
16
  import type { StoryBatch } from "./batching";
16
17
  import type { DeferredReviewResult } from "./deferred-review";
18
+ import type { PidRegistry } from "./pid-registry";
17
19
  import type { StatusWriter } from "./status-writer";
18
20
 
19
21
  export interface SequentialExecutionContext {
@@ -33,6 +35,10 @@ export interface SequentialExecutionContext {
33
35
  startTime: number;
34
36
  batchPlan: StoryBatch[];
35
37
  interactionChain?: InteractionChain | null;
38
+ /** Protocol-aware agent resolver (ACP wiring). Falls back to standalone getAgent when absent. */
39
+ agentGetFn?: AgentGetFn;
40
+ /** PID registry for crash recovery — register child PIDs so they can be killed on SIGTERM. */
41
+ pidRegistry?: PidRegistry;
36
42
  }
37
43
 
38
44
  export interface SequentialExecutionResult {
@@ -78,6 +78,8 @@ export async function runIteration(
78
78
  storyStartTime: new Date().toISOString(),
79
79
  storyGitRef: storyGitRef ?? undefined,
80
80
  interaction: ctx.interactionChain ?? undefined,
81
+ agentGetFn: ctx.agentGetFn,
82
+ pidRegistry: ctx.pidRegistry,
81
83
  accumulatedAttemptCost: accumulatedAttemptCost > 0 ? accumulatedAttemptCost : undefined,
82
84
  };
83
85
 
@@ -10,7 +10,6 @@
10
10
 
11
11
  import path from "node:path";
12
12
  import { type FixStory, convertFixStoryToUserStory, generateFixStories } from "../../acceptance";
13
- import { getAgent } from "../../agents";
14
13
  import type { NaxConfig } from "../../config";
15
14
  import { resolveModel } from "../../config/schema";
16
15
  import { type LoadedHooksConfig, fireHook } from "../../hooks";
@@ -19,6 +18,7 @@ import type { StoryMetrics } from "../../metrics";
19
18
  import type { PipelineEventEmitter } from "../../pipeline/events";
20
19
  import { runPipeline } from "../../pipeline/runner";
21
20
  import { defaultPipeline } from "../../pipeline/stages";
21
+ import type { AgentGetFn } from "../../pipeline/types";
22
22
  import type { PipelineContext, RoutingResult } from "../../pipeline/types";
23
23
  import type { PluginRegistry } from "../../plugins";
24
24
  import { loadPRD, savePRD } from "../../prd";
@@ -42,6 +42,8 @@ export interface AcceptanceLoopContext {
42
42
  pluginRegistry: PluginRegistry;
43
43
  eventEmitter?: PipelineEventEmitter;
44
44
  statusWriter: StatusWriter;
45
+ /** Protocol-aware agent resolver — passed from registry at run start */
46
+ agentGetFn?: AgentGetFn;
45
47
  }
46
48
 
47
49
  export interface AcceptanceLoopResult {
@@ -80,7 +82,8 @@ async function generateAndAddFixStories(
80
82
  prd: PRD,
81
83
  ): Promise<FixStory[] | null> {
82
84
  const logger = getSafeLogger();
83
- const agent = getAgent(ctx.config.autoMode.defaultAgent);
85
+ const { getAgent } = await import("../../agents");
86
+ const agent = (ctx.agentGetFn ?? getAgent)(ctx.config.autoMode.defaultAgent);
84
87
  if (!agent) {
85
88
  logger?.error("acceptance", "Agent not found, cannot generate fix stories");
86
89
  return null;
@@ -9,10 +9,10 @@
9
9
  */
10
10
 
11
11
  import chalk from "chalk";
12
- import { getAgent } from "../../agents";
13
12
  import type { NaxConfig } from "../../config";
14
13
  import { AgentNotFoundError, AgentNotInstalledError, StoryLimitExceededError } from "../../errors";
15
14
  import { getSafeLogger } from "../../logger";
15
+ import type { AgentGetFn } from "../../pipeline/types";
16
16
  import { countStories, loadPRD, markStoryPassed, savePRD } from "../../prd";
17
17
  import type { PRD } from "../../prd/types";
18
18
  import { hasCommitsForStory } from "../../utils/git";
@@ -22,6 +22,8 @@ export interface InitializationContext {
22
22
  prdPath: string;
23
23
  workdir: string;
24
24
  dryRun: boolean;
25
+ /** Protocol-aware agent resolver — passed from registry at run start */
26
+ agentGetFn?: AgentGetFn;
25
27
  }
26
28
 
27
29
  export interface InitializationResult {
@@ -74,11 +76,12 @@ async function reconcileState(prd: PRD, prdPath: string, workdir: string): Promi
74
76
  /**
75
77
  * Validate agent installation
76
78
  */
77
- async function checkAgentInstalled(config: NaxConfig, dryRun: boolean): Promise<void> {
79
+ async function checkAgentInstalled(config: NaxConfig, dryRun: boolean, agentGetFn?: AgentGetFn): Promise<void> {
78
80
  if (dryRun) return;
79
81
 
80
82
  const logger = getSafeLogger();
81
- const agent = getAgent(config.autoMode.defaultAgent);
83
+ const { getAgent } = await import("../../agents");
84
+ const agent = (agentGetFn ?? getAgent)(config.autoMode.defaultAgent);
82
85
 
83
86
  if (!agent) {
84
87
  logger?.error("execution", "Agent not found", {
@@ -114,6 +117,15 @@ function validateStoryCount(counts: ReturnType<typeof countStories>, config: Nax
114
117
  }
115
118
  }
116
119
 
120
+ /**
121
+ * Log the active agent protocol to aid debugging.
122
+ */
123
+ export function logActiveProtocol(config: NaxConfig): void {
124
+ const logger = getSafeLogger();
125
+ const protocol = config.agent?.protocol ?? "cli";
126
+ logger?.info("run-initialization", `Agent protocol: ${protocol}`, { protocol });
127
+ }
128
+
117
129
  /**
118
130
  * Initialize execution: validate agent, reconcile state, check limits
119
131
  */
@@ -121,7 +133,7 @@ export async function initializeRun(ctx: InitializationContext): Promise<Initial
121
133
  const logger = getSafeLogger();
122
134
 
123
135
  // Check agent installation
124
- await checkAgentInstalled(ctx.config, ctx.dryRun);
136
+ await checkAgentInstalled(ctx.config, ctx.dryRun, ctx.agentGetFn);
125
137
 
126
138
  // Load and reconcile PRD
127
139
  let prd = await loadPRD(ctx.prdPath);