@nathapp/nax 0.34.0 → 0.36.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/bin/nax.ts +18 -9
- package/dist/nax.js +1934 -1138
- package/package.json +1 -2
- package/src/agents/adapters/aider.ts +135 -0
- package/src/agents/adapters/codex.ts +153 -0
- package/src/agents/adapters/gemini.ts +177 -0
- package/src/agents/adapters/opencode.ts +106 -0
- package/src/agents/claude-plan.ts +22 -5
- package/src/agents/claude.ts +102 -11
- package/src/agents/index.ts +4 -1
- package/src/agents/model-resolution.ts +43 -0
- package/src/agents/registry.ts +8 -3
- package/src/agents/types-extended.ts +5 -1
- package/src/agents/types.ts +31 -0
- package/src/agents/version-detection.ts +109 -0
- package/src/analyze/classifier.ts +30 -50
- package/src/cli/agents.ts +87 -0
- package/src/cli/analyze-parser.ts +8 -1
- package/src/cli/analyze.ts +1 -1
- package/src/cli/config.ts +28 -14
- package/src/cli/generate.ts +1 -1
- package/src/cli/index.ts +1 -0
- package/src/cli/plan.ts +1 -0
- package/src/config/types.ts +3 -1
- package/src/context/generator.ts +4 -0
- package/src/context/generators/codex.ts +28 -0
- package/src/context/generators/gemini.ts +28 -0
- package/src/context/types.ts +1 -1
- package/src/interaction/init.ts +8 -7
- package/src/interaction/plugins/auto.ts +41 -25
- package/src/pipeline/stages/execution.ts +2 -39
- package/src/pipeline/stages/routing.ts +12 -3
- package/src/plugins/index.ts +2 -0
- package/src/plugins/loader.ts +4 -2
- package/src/plugins/plugin-logger.ts +41 -0
- package/src/plugins/types.ts +50 -1
- package/src/precheck/checks-agents.ts +63 -0
- package/src/precheck/checks-blockers.ts +37 -1
- package/src/precheck/checks.ts +4 -0
- package/src/precheck/index.ts +4 -2
- package/src/routing/router.ts +1 -0
- package/src/routing/strategies/llm.ts +53 -36
- package/src/routing/strategy.ts +3 -0
- package/src/tdd/rectification-gate.ts +25 -1
- package/src/tdd/session-runner.ts +18 -49
- package/src/tdd/verdict.ts +135 -7
- package/src/utils/git.ts +49 -0
- package/src/verification/rectification-loop.ts +14 -1
package/src/cli/config.ts
CHANGED
|
@@ -23,19 +23,27 @@ const FIELD_DESCRIPTIONS: Record<string, string> = {
|
|
|
23
23
|
"models.powerful": "Powerful model for complex tasks (e.g., opus)",
|
|
24
24
|
|
|
25
25
|
// Auto mode
|
|
26
|
-
autoMode:
|
|
26
|
+
autoMode:
|
|
27
|
+
"Auto mode configuration for agent orchestration. Enables multi-agent routing with model tier selection per task complexity and escalation on failures.",
|
|
27
28
|
"autoMode.enabled": "Enable automatic agent selection and escalation",
|
|
28
|
-
"autoMode.defaultAgent":
|
|
29
|
-
|
|
30
|
-
"autoMode.
|
|
31
|
-
|
|
32
|
-
"autoMode.complexityRouting
|
|
33
|
-
|
|
34
|
-
"autoMode.complexityRouting.
|
|
35
|
-
"autoMode.
|
|
29
|
+
"autoMode.defaultAgent":
|
|
30
|
+
"Default agent to use when no specific agent is requested. Examples: 'claude' (Claude Code), 'codex' (GitHub Copilot), 'opencode' (OpenCode). The agent handles the main coding tasks.",
|
|
31
|
+
"autoMode.fallbackOrder":
|
|
32
|
+
'Fallback order for agent selection when the primary agent is rate-limited, unavailable, or fails. Tries each agent in sequence until one succeeds. Example: ["claude", "codex", "opencode"] means try Claude first, then Copilot, then OpenCode.',
|
|
33
|
+
"autoMode.complexityRouting":
|
|
34
|
+
"Model tier routing rules mapped to story complexity levels. Determines which model (fast/balanced/powerful) to use based on task complexity: simple → fast, medium → balanced, complex → powerful, expert → powerful.",
|
|
35
|
+
"autoMode.complexityRouting.simple": "Model tier for simple tasks (low complexity, straightforward changes)",
|
|
36
|
+
"autoMode.complexityRouting.medium": "Model tier for medium tasks (moderate complexity, multi-file changes)",
|
|
37
|
+
"autoMode.complexityRouting.complex": "Model tier for complex tasks (high complexity, architectural decisions)",
|
|
38
|
+
"autoMode.complexityRouting.expert":
|
|
39
|
+
"Model tier for expert tasks (highest complexity, novel problems, design patterns)",
|
|
40
|
+
"autoMode.escalation":
|
|
41
|
+
"Escalation settings for failed stories. When a story fails after max attempts at current tier, escalate to the next tier in tierOrder. Enables progressive use of more powerful models.",
|
|
36
42
|
"autoMode.escalation.enabled": "Enable tier escalation on failure",
|
|
37
|
-
"autoMode.escalation.tierOrder":
|
|
38
|
-
|
|
43
|
+
"autoMode.escalation.tierOrder":
|
|
44
|
+
'Ordered tier escalation chain with per-tier attempt budgets. Format: [{"tier": "fast", "attempts": 2}, {"tier": "balanced", "attempts": 2}, {"tier": "powerful", "attempts": 1}]. Allows each tier to attempt fixes before escalating to the next.',
|
|
45
|
+
"autoMode.escalation.escalateEntireBatch":
|
|
46
|
+
"When enabled, escalate all stories in a batch if one fails. When disabled, only the failing story escalates (allows parallel attempts at different tiers).",
|
|
39
47
|
|
|
40
48
|
// Routing
|
|
41
49
|
routing: "Model routing strategy configuration",
|
|
@@ -528,9 +536,15 @@ function displayConfigWithDescriptions(
|
|
|
528
536
|
|
|
529
537
|
// Display description comment if available
|
|
530
538
|
if (description) {
|
|
531
|
-
// Include path for
|
|
532
|
-
|
|
533
|
-
const
|
|
539
|
+
// Include path for direct subsections of key configuration sections
|
|
540
|
+
// (to improve clarity of important configs like multi-agent setup)
|
|
541
|
+
const pathParts = currentPathStr.split(".");
|
|
542
|
+
// Only show path for 2-level paths (e.g., "autoMode.enabled", "models.fast")
|
|
543
|
+
// to keep deeply nested descriptions concise
|
|
544
|
+
const isDirectSubsection = pathParts.length === 2;
|
|
545
|
+
const isKeySection = ["prompts", "autoMode", "models", "routing"].includes(pathParts[0]);
|
|
546
|
+
const shouldIncludePath = isKeySection && isDirectSubsection;
|
|
547
|
+
const comment = shouldIncludePath ? `${currentPathStr}: ${description}` : description;
|
|
534
548
|
console.log(`${indentStr}# ${comment}`);
|
|
535
549
|
}
|
|
536
550
|
|
package/src/cli/generate.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface GenerateCommandOptions {
|
|
|
26
26
|
noAutoInject?: boolean;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const VALID_AGENTS: AgentType[] = ["claude", "opencode", "cursor", "windsurf", "aider"];
|
|
29
|
+
const VALID_AGENTS: AgentType[] = ["claude", "codex", "opencode", "cursor", "windsurf", "aider", "gemini"];
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* `nax generate` command handler.
|
package/src/cli/index.ts
CHANGED
package/src/cli/plan.ts
CHANGED
package/src/config/types.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
export type Complexity = "simple" | "medium" | "complex" | "expert";
|
|
9
9
|
export type TestStrategy = "test-after" | "tdd-simple" | "three-session-tdd" | "three-session-tdd-lite";
|
|
10
|
-
export type TddStrategy = "auto" | "strict" | "lite" | "off";
|
|
10
|
+
export type TddStrategy = "auto" | "strict" | "lite" | "simple" | "off";
|
|
11
11
|
|
|
12
12
|
export interface EscalationEntry {
|
|
13
13
|
from: string;
|
|
@@ -125,6 +125,8 @@ export interface ExecutionConfig {
|
|
|
125
125
|
/** Enable smart test runner to scope test runs to changed files (default: true).
|
|
126
126
|
* Accepts boolean for backward compat or a SmartTestRunnerConfig object. */
|
|
127
127
|
smartTestRunner?: boolean | SmartTestRunnerConfig;
|
|
128
|
+
/** Configured agent binary: claude, codex, opencode, gemini, aider (default: claude) */
|
|
129
|
+
agent?: string;
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
/** Quality gate config */
|
package/src/context/generator.ts
CHANGED
|
@@ -11,7 +11,9 @@ import type { NaxConfig } from "../config";
|
|
|
11
11
|
import { validateFilePath } from "../config/path-security";
|
|
12
12
|
import { aiderGenerator } from "./generators/aider";
|
|
13
13
|
import { claudeGenerator } from "./generators/claude";
|
|
14
|
+
import { codexGenerator } from "./generators/codex";
|
|
14
15
|
import { cursorGenerator } from "./generators/cursor";
|
|
16
|
+
import { geminiGenerator } from "./generators/gemini";
|
|
15
17
|
import { opencodeGenerator } from "./generators/opencode";
|
|
16
18
|
import { windsurfGenerator } from "./generators/windsurf";
|
|
17
19
|
import { buildProjectMetadata } from "./injector";
|
|
@@ -20,10 +22,12 @@ import type { AgentContextGenerator, AgentType, ContextContent, GeneratorMap } f
|
|
|
20
22
|
/** Generator registry */
|
|
21
23
|
const GENERATORS: GeneratorMap = {
|
|
22
24
|
claude: claudeGenerator,
|
|
25
|
+
codex: codexGenerator,
|
|
23
26
|
opencode: opencodeGenerator,
|
|
24
27
|
cursor: cursorGenerator,
|
|
25
28
|
windsurf: windsurfGenerator,
|
|
26
29
|
aider: aiderGenerator,
|
|
30
|
+
gemini: geminiGenerator,
|
|
27
31
|
};
|
|
28
32
|
|
|
29
33
|
/** Generation result for a single agent */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Config Generator (v0.16.1)
|
|
3
|
+
*
|
|
4
|
+
* Generates codex.md from nax/context.md + auto-injected metadata.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { formatMetadataSection } from "../injector";
|
|
8
|
+
import type { AgentContextGenerator, ContextContent } from "../types";
|
|
9
|
+
|
|
10
|
+
function generateCodexConfig(context: ContextContent): string {
|
|
11
|
+
const header = `# Codex Instructions
|
|
12
|
+
|
|
13
|
+
This file is auto-generated from \`nax/context.md\`.
|
|
14
|
+
DO NOT EDIT MANUALLY — run \`nax generate\` to regenerate.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const metaSection = context.metadata ? formatMetadataSection(context.metadata) : "";
|
|
21
|
+
return header + metaSection + context.markdown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const codexGenerator: AgentContextGenerator = {
|
|
25
|
+
name: "codex",
|
|
26
|
+
outputFile: "codex.md",
|
|
27
|
+
generate: generateCodexConfig,
|
|
28
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI Config Generator (v0.16.1)
|
|
3
|
+
*
|
|
4
|
+
* Generates GEMINI.md from nax/context.md + auto-injected metadata.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { formatMetadataSection } from "../injector";
|
|
8
|
+
import type { AgentContextGenerator, ContextContent } from "../types";
|
|
9
|
+
|
|
10
|
+
function generateGeminiConfig(context: ContextContent): string {
|
|
11
|
+
const header = `# Gemini CLI Context
|
|
12
|
+
|
|
13
|
+
This file is auto-generated from \`nax/context.md\`.
|
|
14
|
+
DO NOT EDIT MANUALLY — run \`nax generate\` to regenerate.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const metaSection = context.metadata ? formatMetadataSection(context.metadata) : "";
|
|
21
|
+
return header + metaSection + context.markdown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const geminiGenerator: AgentContextGenerator = {
|
|
25
|
+
name: "gemini",
|
|
26
|
+
outputFile: "GEMINI.md",
|
|
27
|
+
generate: generateGeminiConfig,
|
|
28
|
+
};
|
package/src/context/types.ts
CHANGED
|
@@ -40,7 +40,7 @@ export interface AgentContextGenerator {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/** All available generator types */
|
|
43
|
-
export type AgentType = "claude" | "opencode" | "cursor" | "windsurf" | "aider";
|
|
43
|
+
export type AgentType = "claude" | "codex" | "opencode" | "cursor" | "windsurf" | "aider" | "gemini";
|
|
44
44
|
|
|
45
45
|
/** Generator registry map */
|
|
46
46
|
export type GeneratorMap = Record<AgentType, AgentContextGenerator>;
|
package/src/interaction/init.ts
CHANGED
|
@@ -41,18 +41,20 @@ function createInteractionPlugin(pluginName: string): InteractionPlugin {
|
|
|
41
41
|
export async function initInteractionChain(config: NaxConfig, headless: boolean): Promise<InteractionChain | null> {
|
|
42
42
|
const logger = getSafeLogger();
|
|
43
43
|
|
|
44
|
-
// If headless mode, skip interaction system
|
|
45
|
-
if (headless) {
|
|
46
|
-
logger?.debug("interaction", "Headless mode - skipping interaction system");
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
44
|
// If no interaction config, skip
|
|
51
45
|
if (!config.interaction) {
|
|
52
46
|
logger?.debug("interaction", "No interaction config - skipping interaction system");
|
|
53
47
|
return null;
|
|
54
48
|
}
|
|
55
49
|
|
|
50
|
+
// In headless mode, skip CLI plugin only — it requires stdin (TTY).
|
|
51
|
+
// Telegram and Webhook plugins work via HTTP and don't need a TTY.
|
|
52
|
+
const pluginName = config.interaction.plugin;
|
|
53
|
+
if (headless && pluginName === "cli") {
|
|
54
|
+
logger?.debug("interaction", "Headless mode with CLI plugin - skipping interaction system (stdin unavailable)");
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
56
58
|
// Create chain
|
|
57
59
|
const chain = new InteractionChain({
|
|
58
60
|
defaultTimeout: config.interaction.defaults.timeout,
|
|
@@ -60,7 +62,6 @@ export async function initInteractionChain(config: NaxConfig, headless: boolean)
|
|
|
60
62
|
});
|
|
61
63
|
|
|
62
64
|
// Create and register plugin
|
|
63
|
-
const pluginName = config.interaction.plugin;
|
|
64
65
|
try {
|
|
65
66
|
const plugin = createInteractionPlugin(pluginName);
|
|
66
67
|
chain.register(plugin, 100);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { z } from "zod";
|
|
9
|
+
import type { AgentAdapter } from "../../agents/types";
|
|
9
10
|
import type { NaxConfig } from "../../config";
|
|
10
11
|
import { resolveModel } from "../../config";
|
|
11
12
|
import type { InteractionPlugin, InteractionRequest, InteractionResponse } from "../types";
|
|
@@ -40,9 +41,12 @@ interface DecisionResponse {
|
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Module-level deps for testability (_deps pattern).
|
|
43
|
-
* Override
|
|
44
|
+
* Override adapter in tests to mock adapter.complete() without spawning the claude CLI.
|
|
45
|
+
*
|
|
46
|
+
* For backward compatibility, also supports _deps.callLlm (deprecated).
|
|
44
47
|
*/
|
|
45
48
|
export const _deps = {
|
|
49
|
+
adapter: null as AgentAdapter | null,
|
|
46
50
|
callLlm: null as ((request: InteractionRequest) => Promise<DecisionResponse>) | null,
|
|
47
51
|
};
|
|
48
52
|
|
|
@@ -71,7 +75,7 @@ export class AutoInteractionPlugin implements InteractionPlugin {
|
|
|
71
75
|
// No-op — in-process plugin
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
async receive(
|
|
78
|
+
async receive(_requestId: string, _timeout = 60000): Promise<InteractionResponse> {
|
|
75
79
|
// For auto plugin, we need to fetch the request from somewhere
|
|
76
80
|
// In practice, the chain should pass the request to us
|
|
77
81
|
// For now, throw an error since we need the full request
|
|
@@ -88,8 +92,23 @@ export class AutoInteractionPlugin implements InteractionPlugin {
|
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
// Use deprecated callLlm if provided (backward compatibility)
|
|
96
|
+
if (_deps.callLlm) {
|
|
97
|
+
const decision = await _deps.callLlm(request);
|
|
98
|
+
if (decision.confidence < (this.config.confidenceThreshold ?? 0.7)) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
requestId: request.id,
|
|
103
|
+
action: decision.action,
|
|
104
|
+
value: decision.value,
|
|
105
|
+
respondedBy: "auto-ai",
|
|
106
|
+
respondedAt: Date.now(),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Use new adapter-based path
|
|
111
|
+
const decision = await this.callLlm(request);
|
|
93
112
|
|
|
94
113
|
// Check confidence threshold
|
|
95
114
|
if (decision.confidence < (this.config.confidenceThreshold ?? 0.7)) {
|
|
@@ -114,34 +133,31 @@ export class AutoInteractionPlugin implements InteractionPlugin {
|
|
|
114
133
|
*/
|
|
115
134
|
private async callLlm(request: InteractionRequest): Promise<DecisionResponse> {
|
|
116
135
|
const prompt = this.buildPrompt(request);
|
|
117
|
-
const modelTier = this.config.model ?? "fast";
|
|
118
136
|
|
|
119
|
-
|
|
120
|
-
|
|
137
|
+
// Get adapter from dependency injection or throw
|
|
138
|
+
const adapter = _deps.adapter;
|
|
139
|
+
if (!adapter) {
|
|
140
|
+
throw new Error("Auto plugin requires adapter to be injected via _deps.adapter");
|
|
121
141
|
}
|
|
122
142
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
// Resolve model option if naxConfig is available
|
|
144
|
+
let modelArg: string | undefined;
|
|
145
|
+
if (this.config.naxConfig) {
|
|
146
|
+
const modelTier = this.config.model ?? "fast";
|
|
147
|
+
const modelEntry = this.config.naxConfig.models[modelTier];
|
|
148
|
+
if (!modelEntry) {
|
|
149
|
+
throw new Error(`Model tier "${modelTier}" not found in config.models`);
|
|
150
|
+
}
|
|
151
|
+
const modelDef = resolveModel(modelEntry);
|
|
152
|
+
modelArg = modelDef.model;
|
|
126
153
|
}
|
|
127
154
|
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const proc = Bun.spawn(["claude", "-p", prompt, "--model", modelArg], {
|
|
133
|
-
stdout: "pipe",
|
|
134
|
-
stderr: "pipe",
|
|
155
|
+
// Use adapter.complete() for one-shot LLM call
|
|
156
|
+
const output = await adapter.complete(prompt, {
|
|
157
|
+
...(modelArg && { model: modelArg }),
|
|
158
|
+
jsonMode: true,
|
|
135
159
|
});
|
|
136
160
|
|
|
137
|
-
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
|
138
|
-
|
|
139
|
-
const exitCode = await proc.exited;
|
|
140
|
-
if (exitCode !== 0) {
|
|
141
|
-
throw new Error(`claude CLI failed with exit code ${exitCode}: ${stderr}`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const output = stdout.trim();
|
|
145
161
|
return this.parseResponse(output);
|
|
146
162
|
}
|
|
147
163
|
|
|
@@ -36,7 +36,7 @@ import { checkMergeConflict, checkStoryAmbiguity, isTriggerEnabled } from "../..
|
|
|
36
36
|
import { getLogger } from "../../logger";
|
|
37
37
|
import type { FailureCategory } from "../../tdd";
|
|
38
38
|
import { runThreeSessionTdd } from "../../tdd";
|
|
39
|
-
import { detectMergeConflict } from "../../utils/git";
|
|
39
|
+
import { autoCommitIfDirty, detectMergeConflict } from "../../utils/git";
|
|
40
40
|
import type { PipelineContext, PipelineStage, StageResult } from "../types";
|
|
41
41
|
|
|
42
42
|
/**
|
|
@@ -200,7 +200,7 @@ export const executionStage: PipelineStage = {
|
|
|
200
200
|
ctx.agentResult = result;
|
|
201
201
|
|
|
202
202
|
// BUG-058: Auto-commit if agent left uncommitted changes (single-session/test-after)
|
|
203
|
-
await autoCommitIfDirty(ctx.workdir, "single-session", ctx.story.id);
|
|
203
|
+
await autoCommitIfDirty(ctx.workdir, "execution", "single-session", ctx.story.id);
|
|
204
204
|
|
|
205
205
|
// merge-conflict trigger: detect CONFLICT markers in agent output
|
|
206
206
|
const combinedOutput = (result.output ?? "") + (result.stderr ?? "");
|
|
@@ -270,40 +270,3 @@ export const _executionDeps = {
|
|
|
270
270
|
isAmbiguousOutput,
|
|
271
271
|
checkStoryAmbiguity,
|
|
272
272
|
};
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* BUG-058: Auto-commit safety net for single-session/test-after.
|
|
276
|
-
* Mirrors the same function in tdd/session-runner.ts for three-session TDD.
|
|
277
|
-
*/
|
|
278
|
-
async function autoCommitIfDirty(workdir: string, role: string, storyId: string): Promise<void> {
|
|
279
|
-
try {
|
|
280
|
-
const statusProc = Bun.spawn(["git", "status", "--porcelain"], {
|
|
281
|
-
cwd: workdir,
|
|
282
|
-
stdout: "pipe",
|
|
283
|
-
stderr: "pipe",
|
|
284
|
-
});
|
|
285
|
-
const statusOutput = await new Response(statusProc.stdout).text();
|
|
286
|
-
await statusProc.exited;
|
|
287
|
-
|
|
288
|
-
if (!statusOutput.trim()) return;
|
|
289
|
-
|
|
290
|
-
const logger = getLogger();
|
|
291
|
-
logger.warn("execution", `Agent did not commit after ${role} session — auto-committing`, {
|
|
292
|
-
role,
|
|
293
|
-
storyId,
|
|
294
|
-
dirtyFiles: statusOutput.trim().split("\n").length,
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
const addProc = Bun.spawn(["git", "add", "-A"], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
|
|
298
|
-
await addProc.exited;
|
|
299
|
-
|
|
300
|
-
const commitProc = Bun.spawn(["git", "commit", "-m", `chore(${storyId}): auto-commit after ${role} session`], {
|
|
301
|
-
cwd: workdir,
|
|
302
|
-
stdout: "pipe",
|
|
303
|
-
stderr: "pipe",
|
|
304
|
-
});
|
|
305
|
-
await commitProc.exited;
|
|
306
|
-
} catch {
|
|
307
|
-
// Silently ignore — auto-commit is best-effort
|
|
308
|
-
}
|
|
309
|
-
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
import { getAgent } from "../../agents/registry";
|
|
28
29
|
import type { NaxConfig } from "../../config";
|
|
29
30
|
import { isGreenfieldStory } from "../../context/greenfield";
|
|
30
31
|
import { applyDecomposition } from "../../decompose/apply";
|
|
@@ -68,6 +69,10 @@ export const routingStage: PipelineStage = {
|
|
|
68
69
|
async execute(ctx: PipelineContext): Promise<StageResult> {
|
|
69
70
|
const logger = getLogger();
|
|
70
71
|
|
|
72
|
+
// Resolve agent adapter for LLM routing (shared with execution)
|
|
73
|
+
const agentName = ctx.config.execution?.agent ?? "claude";
|
|
74
|
+
const adapter = _routingDeps.getAgent(agentName);
|
|
75
|
+
|
|
71
76
|
// Staleness detection (RRP-003):
|
|
72
77
|
// - story.routing absent → cache miss (no prior routing)
|
|
73
78
|
// - story.routing + no contentHash → legacy cache hit (manual / pre-RRP-003 routing, honor as-is)
|
|
@@ -87,10 +92,13 @@ export const routingStage: PipelineStage = {
|
|
|
87
92
|
|
|
88
93
|
if (isCacheHit) {
|
|
89
94
|
// Cache hit: legacy routing (no contentHash) or matching contentHash — use cached values
|
|
90
|
-
routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config }, ctx.workdir, ctx.plugins);
|
|
95
|
+
routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config, adapter }, ctx.workdir, ctx.plugins);
|
|
91
96
|
// Override with cached values only when they are actually set
|
|
92
97
|
if (ctx.story.routing?.complexity) routing.complexity = ctx.story.routing.complexity;
|
|
93
|
-
|
|
98
|
+
// BUG-062: Only honor stored testStrategy for legacy/manual routing (no contentHash).
|
|
99
|
+
// When contentHash exists, the LLM strategy layer already recomputes testStrategy
|
|
100
|
+
// fresh via determineTestStrategy() — don't clobber it with the stale PRD value.
|
|
101
|
+
if (!hasContentHash && ctx.story.routing?.testStrategy) routing.testStrategy = ctx.story.routing.testStrategy;
|
|
94
102
|
// BUG-032: Use escalated modelTier if explicitly set (by handleTierEscalation),
|
|
95
103
|
// otherwise derive from complexity + current config
|
|
96
104
|
if (ctx.story.routing?.modelTier) {
|
|
@@ -103,7 +111,7 @@ export const routingStage: PipelineStage = {
|
|
|
103
111
|
}
|
|
104
112
|
} else {
|
|
105
113
|
// Cache miss: no routing, or contentHash present but mismatched — fresh classification
|
|
106
|
-
routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config }, ctx.workdir, ctx.plugins);
|
|
114
|
+
routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config, adapter }, ctx.workdir, ctx.plugins);
|
|
107
115
|
// currentHash already computed if a mismatch was detected; compute now if starting fresh
|
|
108
116
|
currentHash = currentHash ?? _routingDeps.computeStoryContentHash(ctx.story);
|
|
109
117
|
ctx.story.routing = {
|
|
@@ -220,4 +228,5 @@ export const _routingDeps = {
|
|
|
220
228
|
applyDecomposition,
|
|
221
229
|
runDecompose,
|
|
222
230
|
checkStoryOversized,
|
|
231
|
+
getAgent,
|
|
223
232
|
};
|
package/src/plugins/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type {
|
|
|
9
9
|
PluginType,
|
|
10
10
|
PluginExtensions,
|
|
11
11
|
PluginConfigEntry,
|
|
12
|
+
PluginLogger,
|
|
12
13
|
IReviewPlugin,
|
|
13
14
|
ReviewCheckResult,
|
|
14
15
|
IContextProvider,
|
|
@@ -29,3 +30,4 @@ export type {
|
|
|
29
30
|
export { validatePlugin } from "./validator";
|
|
30
31
|
export { loadPlugins } from "./loader";
|
|
31
32
|
export { PluginRegistry } from "./registry";
|
|
33
|
+
export { createPluginLogger } from "./plugin-logger";
|
package/src/plugins/loader.ts
CHANGED
|
@@ -11,6 +11,7 @@ import * as fs from "node:fs/promises";
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import { getSafeLogger as _getSafeLoggerFromModule } from "../logger";
|
|
13
13
|
import { validateModulePath } from "../utils/path-security";
|
|
14
|
+
import { createPluginLogger } from "./plugin-logger";
|
|
14
15
|
import { PluginRegistry } from "./registry";
|
|
15
16
|
import type { NaxPlugin, PluginConfigEntry } from "./types";
|
|
16
17
|
import { validatePlugin } from "./validator";
|
|
@@ -272,10 +273,11 @@ async function loadAndValidatePlugin(
|
|
|
272
273
|
return null;
|
|
273
274
|
}
|
|
274
275
|
|
|
275
|
-
// Call setup() if defined
|
|
276
|
+
// Call setup() if defined — pass plugin-scoped logger
|
|
276
277
|
if (validated.setup) {
|
|
277
278
|
try {
|
|
278
|
-
|
|
279
|
+
const pluginLogger = createPluginLogger(validated.name);
|
|
280
|
+
await validated.setup(config, pluginLogger);
|
|
279
281
|
} catch (error) {
|
|
280
282
|
const logger = getSafeLogger();
|
|
281
283
|
logger?.error("plugins", `Plugin '${validated.name}' setup failed`, { error });
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Logger Factory
|
|
3
|
+
*
|
|
4
|
+
* Creates write-only, stage-prefixed loggers for plugins.
|
|
5
|
+
* Each logger auto-tags entries with `plugin:<name>` so plugin
|
|
6
|
+
* output is filterable and cannot impersonate core stages.
|
|
7
|
+
*
|
|
8
|
+
* @module plugins/plugin-logger
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getSafeLogger } from "../logger";
|
|
12
|
+
import type { PluginLogger } from "./types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a PluginLogger scoped to a plugin name.
|
|
16
|
+
*
|
|
17
|
+
* The returned logger delegates to the global nax Logger with
|
|
18
|
+
* `plugin:<pluginName>` as the stage. If the global logger is
|
|
19
|
+
* not initialized (e.g., during tests), calls are silently dropped.
|
|
20
|
+
*
|
|
21
|
+
* @param pluginName - Plugin name used as stage prefix
|
|
22
|
+
* @returns PluginLogger instance
|
|
23
|
+
*/
|
|
24
|
+
export function createPluginLogger(pluginName: string): PluginLogger {
|
|
25
|
+
const stage = `plugin:${pluginName}`;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
error(message: string, data?: Record<string, unknown>): void {
|
|
29
|
+
getSafeLogger()?.error(stage, message, data);
|
|
30
|
+
},
|
|
31
|
+
warn(message: string, data?: Record<string, unknown>): void {
|
|
32
|
+
getSafeLogger()?.warn(stage, message, data);
|
|
33
|
+
},
|
|
34
|
+
info(message: string, data?: Record<string, unknown>): void {
|
|
35
|
+
getSafeLogger()?.info(stage, message, data);
|
|
36
|
+
},
|
|
37
|
+
debug(message: string, data?: Record<string, unknown>): void {
|
|
38
|
+
getSafeLogger()?.debug(stage, message, data);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/plugins/types.ts
CHANGED
|
@@ -61,8 +61,9 @@ export interface NaxPlugin {
|
|
|
61
61
|
* validating config, establishing connections, etc.
|
|
62
62
|
*
|
|
63
63
|
* @param config - Plugin-specific config from nax config.json
|
|
64
|
+
* @param logger - Write-only logger scoped to this plugin (stage auto-prefixed as `plugin:<name>`)
|
|
64
65
|
*/
|
|
65
|
-
setup?(config: Record<string, unknown
|
|
66
|
+
setup?(config: Record<string, unknown>, logger: PluginLogger): Promise<void>;
|
|
66
67
|
|
|
67
68
|
/**
|
|
68
69
|
* Called when the nax run ends (success or failure).
|
|
@@ -333,6 +334,54 @@ export interface IReporter {
|
|
|
333
334
|
onRunEnd?(event: RunEndEvent): Promise<void>;
|
|
334
335
|
}
|
|
335
336
|
|
|
337
|
+
// ============================================================================
|
|
338
|
+
// Plugin Logger
|
|
339
|
+
// ============================================================================
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Write-only, level-gated logger provided to plugins via setup().
|
|
343
|
+
*
|
|
344
|
+
* All log entries are auto-prefixed with `plugin:<name>` as the stage,
|
|
345
|
+
* so plugins cannot impersonate core nax stages. The interface is
|
|
346
|
+
* intentionally minimal — plugins only need to emit messages, not
|
|
347
|
+
* configure log levels or access log files.
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```ts
|
|
351
|
+
* let log: PluginLogger;
|
|
352
|
+
*
|
|
353
|
+
* const myPlugin: NaxPlugin = {
|
|
354
|
+
* name: "my-plugin",
|
|
355
|
+
* version: "1.0.0",
|
|
356
|
+
* provides: ["reviewer"],
|
|
357
|
+
* async setup(config, logger) {
|
|
358
|
+
* log = logger;
|
|
359
|
+
* log.info("Initialized with config", { keys: Object.keys(config) });
|
|
360
|
+
* },
|
|
361
|
+
* extensions: {
|
|
362
|
+
* reviewer: {
|
|
363
|
+
* name: "my-check",
|
|
364
|
+
* description: "Custom check",
|
|
365
|
+
* async check(workdir, changedFiles) {
|
|
366
|
+
* log.debug("Scanning files", { count: changedFiles.length });
|
|
367
|
+
* // ...
|
|
368
|
+
* }
|
|
369
|
+
* }
|
|
370
|
+
* }
|
|
371
|
+
* };
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
export interface PluginLogger {
|
|
375
|
+
/** Log an error message */
|
|
376
|
+
error(message: string, data?: Record<string, unknown>): void;
|
|
377
|
+
/** Log a warning message */
|
|
378
|
+
warn(message: string, data?: Record<string, unknown>): void;
|
|
379
|
+
/** Log an informational message */
|
|
380
|
+
info(message: string, data?: Record<string, unknown>): void;
|
|
381
|
+
/** Log a debug message */
|
|
382
|
+
debug(message: string, data?: Record<string, unknown>): void;
|
|
383
|
+
}
|
|
384
|
+
|
|
336
385
|
// ============================================================================
|
|
337
386
|
// Plugin Config
|
|
338
387
|
// ============================================================================
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Precheck for multi-agent health
|
|
3
|
+
*
|
|
4
|
+
* Detects installed agents, reports version information,
|
|
5
|
+
* and checks health status for each configured agent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getAgentVersions } from "../agents/version-detection";
|
|
9
|
+
import type { Check } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check multi-agent health: installed agents and their versions
|
|
13
|
+
*
|
|
14
|
+
* This is a Tier 2 warning check. Reports which agents are available
|
|
15
|
+
* and their versions, but doesn't fail if no agents are installed
|
|
16
|
+
* (since the main configured agent is checked in Tier 1).
|
|
17
|
+
*/
|
|
18
|
+
export async function checkMultiAgentHealth(): Promise<Check> {
|
|
19
|
+
try {
|
|
20
|
+
const versions = await getAgentVersions();
|
|
21
|
+
|
|
22
|
+
// Separate installed from not installed
|
|
23
|
+
const installed = versions.filter((v) => v.installed);
|
|
24
|
+
const notInstalled = versions.filter((v) => !v.installed);
|
|
25
|
+
|
|
26
|
+
// Build message with agent status
|
|
27
|
+
const lines: string[] = [];
|
|
28
|
+
|
|
29
|
+
if (installed.length > 0) {
|
|
30
|
+
lines.push(`Installed agents (${installed.length}):`);
|
|
31
|
+
for (const agent of installed) {
|
|
32
|
+
const versionStr = agent.version ? ` v${agent.version}` : " (version unknown)";
|
|
33
|
+
lines.push(` • ${agent.displayName}${versionStr}`);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
lines.push("No additional agents detected (using default configured agent)");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (notInstalled.length > 0) {
|
|
40
|
+
lines.push(`\nAvailable but not installed (${notInstalled.length}):`);
|
|
41
|
+
for (const agent of notInstalled) {
|
|
42
|
+
lines.push(` • ${agent.displayName}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const message = lines.join("\n");
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
name: "multi-agent-health",
|
|
50
|
+
tier: "warning",
|
|
51
|
+
passed: true, // Always pass - this is informational
|
|
52
|
+
message,
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// If version detection fails, still pass but report error
|
|
56
|
+
return {
|
|
57
|
+
name: "multi-agent-health",
|
|
58
|
+
tier: "warning",
|
|
59
|
+
passed: true,
|
|
60
|
+
message: `Agent detection: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|