@nathapp/nax 0.40.1 → 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 (37) hide show
  1. package/dist/nax.js +1072 -268
  2. package/package.json +2 -2
  3. package/src/acceptance/fix-generator.ts +4 -35
  4. package/src/acceptance/generator.ts +4 -27
  5. package/src/agents/acp/adapter.ts +644 -0
  6. package/src/agents/acp/cost.ts +79 -0
  7. package/src/agents/acp/index.ts +9 -0
  8. package/src/agents/acp/interaction-bridge.ts +126 -0
  9. package/src/agents/acp/parser.ts +166 -0
  10. package/src/agents/acp/spawn-client.ts +309 -0
  11. package/src/agents/acp/types.ts +22 -0
  12. package/src/agents/claude-complete.ts +3 -3
  13. package/src/agents/registry.ts +83 -0
  14. package/src/agents/types-extended.ts +23 -0
  15. package/src/agents/types.ts +17 -0
  16. package/src/cli/analyze.ts +6 -2
  17. package/src/cli/plan.ts +23 -0
  18. package/src/config/defaults.ts +1 -0
  19. package/src/config/runtime-types.ts +10 -0
  20. package/src/config/schema.ts +1 -0
  21. package/src/config/schemas.ts +6 -0
  22. package/src/config/types.ts +1 -0
  23. package/src/execution/executor-types.ts +6 -0
  24. package/src/execution/iteration-runner.ts +2 -0
  25. package/src/execution/lifecycle/acceptance-loop.ts +5 -2
  26. package/src/execution/lifecycle/run-initialization.ts +16 -4
  27. package/src/execution/lifecycle/run-setup.ts +4 -0
  28. package/src/execution/runner-completion.ts +11 -1
  29. package/src/execution/runner-execution.ts +8 -0
  30. package/src/execution/runner-setup.ts +4 -0
  31. package/src/execution/runner.ts +10 -0
  32. package/src/pipeline/stages/execution.ts +33 -1
  33. package/src/pipeline/stages/routing.ts +18 -7
  34. package/src/pipeline/types.ts +10 -0
  35. package/src/tdd/orchestrator.ts +7 -0
  36. package/src/tdd/rectification-gate.ts +6 -0
  37. 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);
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,
@@ -465,4 +465,14 @@ export interface NaxConfig {
465
465
  prompts?: PromptsConfig;
466
466
  /** Decompose settings (SD-003) */
467
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;
468
478
  }
@@ -48,6 +48,7 @@ export type {
48
48
  SmartTestRunnerConfig,
49
49
  DecomposeConfig,
50
50
  NaxConfig,
51
+ AgentConfig,
51
52
  } from "./types";
52
53
 
53
54
  export { resolveModel } from "./types";
@@ -326,6 +326,11 @@ const StorySizeGateConfigSchema = z.object({
326
326
  maxBulletPoints: z.number().int().min(1).max(100).default(8),
327
327
  });
328
328
 
329
+ const AgentConfigSchema = z.object({
330
+ protocol: z.enum(["acp", "cli"]).default("acp"),
331
+ acpPermissionMode: z.string().optional(),
332
+ });
333
+
329
334
  const PrecheckConfigSchema = z.object({
330
335
  storySizeGate: StorySizeGateConfigSchema,
331
336
  });
@@ -372,6 +377,7 @@ export const NaxConfigSchema = z
372
377
  disabledPlugins: z.array(z.string()).optional(),
373
378
  hooks: HooksConfigSchema.optional(),
374
379
  interaction: InteractionConfigSchema.optional(),
380
+ agent: AgentConfigSchema.optional(),
375
381
  precheck: PrecheckConfigSchema.optional(),
376
382
  prompts: PromptsConfigSchema.optional(),
377
383
  decompose: DecomposeConfigSchema.optional(),
@@ -52,4 +52,5 @@ export type {
52
52
  TddConfig,
53
53
  TestCoverageConfig,
54
54
  AdaptiveRoutingConfig,
55
+ AgentConfig,
55
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);
@@ -22,6 +22,7 @@ import type { InteractionChain } from "../../interaction";
22
22
  import { initInteractionChain } from "../../interaction";
23
23
  import { getSafeLogger } from "../../logger";
24
24
  import { pipelineEventBus } from "../../pipeline/event-bus";
25
+ import type { AgentGetFn } from "../../pipeline/types";
25
26
  import { loadPlugins } from "../../plugins/loader";
26
27
  import type { PluginRegistry } from "../../plugins/registry";
27
28
  import type { PRD } from "../../prd";
@@ -53,6 +54,8 @@ export interface RunSetupOptions {
53
54
  // BUG-017: Additional getters for run.complete event on SIGTERM
54
55
  getStoriesCompleted: () => number;
55
56
  getTotalStories: () => number;
57
+ /** Protocol-aware agent resolver — passed from runner.ts registry */
58
+ agentGetFn?: AgentGetFn;
56
59
  }
57
60
 
58
61
  export interface RunSetupResult {
@@ -206,6 +209,7 @@ export async function setupRun(options: RunSetupOptions): Promise<RunSetupResult
206
209
  prdPath,
207
210
  workdir,
208
211
  dryRun,
212
+ agentGetFn: options.agentGetFn,
209
213
  });
210
214
  prd = initResult.prd;
211
215
  const counts = initResult.storyCounts;
@@ -11,9 +11,11 @@ import { fireHook } from "../hooks";
11
11
  import { getSafeLogger } from "../logger";
12
12
  import type { StoryMetrics } from "../metrics";
13
13
  import type { PipelineEventEmitter } from "../pipeline/events";
14
+ import type { AgentGetFn } from "../pipeline/types";
14
15
  import type { PluginRegistry } from "../plugins/registry";
15
16
  import { isComplete } from "../prd";
16
17
  import type { PRD } from "../prd";
18
+ import { autoCommitIfDirty } from "../utils/git";
17
19
  import { stopHeartbeat, writeExitSummary } from "./crash-recovery";
18
20
  import { hookCtx } from "./story-context";
19
21
 
@@ -42,6 +44,10 @@ export interface RunnerCompletionOptions {
42
44
  statusWriter: any;
43
45
  pluginRegistry: PluginRegistry;
44
46
  eventEmitter?: PipelineEventEmitter;
47
+ /** Protocol-aware agent resolver */
48
+ agentGetFn?: AgentGetFn;
49
+ /** Path to prd.json — required for acceptance fix story writes */
50
+ prdPath: string;
45
51
  }
46
52
 
47
53
  /**
@@ -67,7 +73,7 @@ export async function runCompletionPhase(options: RunnerCompletionOptions): Prom
67
73
  const acceptanceResult = await runAcceptanceLoop({
68
74
  config: options.config,
69
75
  prd: options.prd,
70
- prdPath: "", // Not needed for this extraction
76
+ prdPath: options.prdPath,
71
77
  workdir: options.workdir,
72
78
  featureDir: options.featureDir,
73
79
  hooks: options.hooks,
@@ -79,6 +85,7 @@ export async function runCompletionPhase(options: RunnerCompletionOptions): Prom
79
85
  pluginRegistry: options.pluginRegistry,
80
86
  eventEmitter: options.eventEmitter,
81
87
  statusWriter: options.statusWriter,
88
+ agentGetFn: options.agentGetFn,
82
89
  });
83
90
 
84
91
  Object.assign(options, {
@@ -153,6 +160,9 @@ export async function runCompletionPhase(options: RunnerCompletionOptions): Prom
153
160
  durationMs,
154
161
  );
155
162
 
163
+ // Commit status.json and any other nax runtime files left dirty at run end
164
+ await autoCommitIfDirty(options.workdir, "run.complete", "run-summary", options.feature);
165
+
156
166
  return {
157
167
  durationMs,
158
168
  runCompletedAt,
@@ -10,6 +10,7 @@ import type { LoadedHooksConfig } from "../hooks";
10
10
  import { getSafeLogger } from "../logger";
11
11
  import type { StoryMetrics } from "../metrics";
12
12
  import type { PipelineEventEmitter } from "../pipeline/events";
13
+ import type { AgentGetFn } from "../pipeline/types";
13
14
  import type { PluginRegistry } from "../plugins/registry";
14
15
  import type { PRD } from "../prd";
15
16
  import { tryLlmBatchRoute } from "../routing/batch-route";
@@ -17,6 +18,7 @@ import { clearCache as clearLlmCache, routeBatch as llmRouteBatch } from "../rou
17
18
  import { precomputeBatchPlan } from "./batching";
18
19
  import { getAllReadyStories } from "./helpers";
19
20
  import type { ParallelExecutorOptions, ParallelExecutorResult } from "./parallel-executor";
21
+ import type { PidRegistry } from "./pid-registry";
20
22
 
21
23
  /**
22
24
  * Options for the execution phase.
@@ -42,6 +44,10 @@ export interface RunnerExecutionOptions {
42
44
  headless: boolean;
43
45
  parallel?: number;
44
46
  runParallelExecution?: (options: ParallelExecutorOptions, prd: PRD) => Promise<ParallelExecutorResult>;
47
+ /** Protocol-aware agent resolver — created once in runner.ts from createAgentRegistry(config) */
48
+ agentGetFn?: AgentGetFn;
49
+ /** PID registry for crash recovery — passed to agent.run() to register child processes. */
50
+ pidRegistry?: PidRegistry;
45
51
  }
46
52
 
47
53
  /**
@@ -198,6 +204,8 @@ export async function runExecutionPhase(
198
204
  runId: options.runId,
199
205
  startTime: options.startTime,
200
206
  batchPlan,
207
+ agentGetFn: options.agentGetFn,
208
+ pidRegistry: options.pidRegistry,
201
209
  },
202
210
  prd,
203
211
  );
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { NaxConfig } from "../config";
9
9
  import type { LoadedHooksConfig } from "../hooks";
10
+ import type { AgentGetFn } from "../pipeline/types";
10
11
 
11
12
  /**
12
13
  * Options for the setup phase.
@@ -31,6 +32,8 @@ export interface RunnerSetupOptions {
31
32
  getIterations: () => number;
32
33
  getStoriesCompleted: () => number;
33
34
  getTotalStories: () => number;
35
+ /** Protocol-aware agent resolver — created from createAgentRegistry(config) in runner.ts */
36
+ agentGetFn?: AgentGetFn;
34
37
  }
35
38
 
36
39
  /**
@@ -76,6 +79,7 @@ export async function runSetupPhase(options: RunnerSetupOptions): Promise<Runner
76
79
  // BUG-017: Pass getters for run.complete event on SIGTERM
77
80
  getStoriesCompleted: options.getStoriesCompleted,
78
81
  getTotalStories: options.getTotalStories,
82
+ agentGetFn: options.agentGetFn,
79
83
  });
80
84
 
81
85
  return setupResult;
@@ -13,6 +13,7 @@
13
13
  * - runner-completion.ts: Acceptance loop, hooks, metrics
14
14
  */
15
15
 
16
+ import { createAgentRegistry } from "../agents/registry";
16
17
  import type { NaxConfig } from "../config";
17
18
  import type { LoadedHooksConfig } from "../hooks";
18
19
  import { fireHook } from "../hooks";
@@ -116,6 +117,10 @@ export async function run(options: RunOptions): Promise<RunResult> {
116
117
 
117
118
  const logger = getSafeLogger();
118
119
 
120
+ // Create protocol-aware agent registry (ACP wiring — ACP-003/registry-wiring)
121
+ const registry = createAgentRegistry(config);
122
+ const agentGetFn = registry.getAgent.bind(registry);
123
+
119
124
  // Declare prd before crash handler setup to avoid TDZ if SIGTERM arrives during setup
120
125
  // biome-ignore lint/suspicious/noExplicitAny: PRD type initialized during setup
121
126
  let prd: any | undefined;
@@ -137,6 +142,7 @@ export async function run(options: RunOptions): Promise<RunResult> {
137
142
  skipPrecheck,
138
143
  headless,
139
144
  formatterMode,
145
+ agentGetFn,
140
146
  getTotalCost: () => totalCost,
141
147
  getIterations: () => iterations,
142
148
  // BUG-017: Pass getters for run.complete event on SIGTERM
@@ -170,6 +176,8 @@ export async function run(options: RunOptions): Promise<RunResult> {
170
176
  headless,
171
177
  parallel,
172
178
  runParallelExecution: _runnerDeps.runParallelExecution ?? undefined,
179
+ agentGetFn,
180
+ pidRegistry,
173
181
  },
174
182
  prd,
175
183
  pluginRegistry,
@@ -198,6 +206,7 @@ export async function run(options: RunOptions): Promise<RunResult> {
198
206
  hooks,
199
207
  feature,
200
208
  workdir,
209
+ prdPath,
201
210
  statusFile,
202
211
  logFilePath,
203
212
  runId,
@@ -214,6 +223,7 @@ export async function run(options: RunOptions): Promise<RunResult> {
214
223
  statusWriter,
215
224
  pluginRegistry,
216
225
  eventEmitter,
226
+ agentGetFn,
217
227
  });
218
228
 
219
229
  const { durationMs } = completionResult;