@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.
- package/dist/nax.js +1072 -268
- package/package.json +2 -2
- package/src/acceptance/fix-generator.ts +4 -35
- package/src/acceptance/generator.ts +4 -27
- package/src/agents/acp/adapter.ts +644 -0
- package/src/agents/acp/cost.ts +79 -0
- package/src/agents/acp/index.ts +9 -0
- package/src/agents/acp/interaction-bridge.ts +126 -0
- package/src/agents/acp/parser.ts +166 -0
- package/src/agents/acp/spawn-client.ts +309 -0
- package/src/agents/acp/types.ts +22 -0
- package/src/agents/claude-complete.ts +3 -3
- package/src/agents/registry.ts +83 -0
- package/src/agents/types-extended.ts +23 -0
- package/src/agents/types.ts +17 -0
- package/src/cli/analyze.ts +6 -2
- package/src/cli/plan.ts +23 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/runtime-types.ts +10 -0
- package/src/config/schema.ts +1 -0
- package/src/config/schemas.ts +6 -0
- package/src/config/types.ts +1 -0
- package/src/execution/executor-types.ts +6 -0
- package/src/execution/iteration-runner.ts +2 -0
- package/src/execution/lifecycle/acceptance-loop.ts +5 -2
- package/src/execution/lifecycle/run-initialization.ts +16 -4
- package/src/execution/lifecycle/run-setup.ts +4 -0
- package/src/execution/runner-completion.ts +11 -1
- package/src/execution/runner-execution.ts +8 -0
- package/src/execution/runner-setup.ts +4 -0
- package/src/execution/runner.ts +10 -0
- package/src/pipeline/stages/execution.ts +33 -1
- package/src/pipeline/stages/routing.ts +18 -7
- package/src/pipeline/types.ts +10 -0
- package/src/tdd/orchestrator.ts +7 -0
- package/src/tdd/rectification-gate.ts +6 -0
- 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
|
-
|
|
47
|
-
|
|
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");
|
package/src/agents/registry.ts
CHANGED
|
@@ -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
|
/**
|
package/src/agents/types.ts
CHANGED
|
@@ -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
|
/**
|
package/src/cli/analyze.ts
CHANGED
|
@@ -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(
|
|
228
|
-
|
|
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
|
package/src/config/defaults.ts
CHANGED
|
@@ -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
|
}
|
package/src/config/schema.ts
CHANGED
package/src/config/schemas.ts
CHANGED
|
@@ -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(),
|
package/src/config/types.ts
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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;
|
package/src/execution/runner.ts
CHANGED
|
@@ -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;
|