@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.
- package/dist/nax.js +1166 -277
- package/package.json +2 -2
- package/src/acceptance/fix-generator.ts +4 -35
- package/src/acceptance/generator.ts +27 -28
- package/src/acceptance/refinement.ts +72 -5
- package/src/acceptance/templates/cli.ts +47 -0
- package/src/acceptance/templates/component.ts +78 -0
- package/src/acceptance/templates/e2e.ts +43 -0
- package/src/acceptance/templates/index.ts +21 -0
- package/src/acceptance/templates/snapshot.ts +50 -0
- package/src/acceptance/templates/unit.ts +48 -0
- package/src/acceptance/types.ts +9 -1
- 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/init-detect.ts +94 -8
- package/src/cli/init.ts +2 -2
- package/src/cli/plan.ts +23 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/index.ts +1 -1
- package/src/config/runtime-types.ts +17 -0
- package/src/config/schema.ts +3 -1
- package/src/config/schemas.ts +9 -1
- package/src/config/types.ts +2 -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/acceptance-setup.ts +4 -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/init-detect.ts
CHANGED
|
@@ -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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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,
|
|
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 =
|
|
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
|
package/src/config/defaults.ts
CHANGED
package/src/config/index.ts
CHANGED
|
@@ -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
|
}
|
package/src/config/schema.ts
CHANGED
|
@@ -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";
|
package/src/config/schemas.ts
CHANGED
|
@@ -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(),
|
package/src/config/types.ts
CHANGED
|
@@ -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
|
|
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);
|