@nathapp/nax 0.35.0 → 0.36.1
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 +1283 -662
- package/package.json +1 -1
- package/src/agents/adapters/aider.ts +135 -0
- package/src/agents/adapters/gemini.ts +177 -0
- package/src/agents/adapters/opencode.ts +106 -0
- package/src/agents/claude-decompose.ts +3 -3
- package/src/agents/index.ts +2 -0
- package/src/agents/registry.ts +6 -2
- package/src/agents/version-detection.ts +109 -0
- package/src/cli/agents.ts +87 -0
- package/src/cli/config.ts +28 -14
- package/src/cli/constitution.ts +0 -92
- package/src/cli/generate.ts +1 -1
- package/src/cli/index.ts +1 -0
- package/src/constitution/generator.ts +0 -33
- package/src/constitution/index.ts +2 -1
- package/src/constitution/loader.ts +1 -13
- package/src/context/builder.ts +1 -2
- package/src/context/elements.ts +1 -12
- 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/index.ts +2 -1
- package/src/context/test-scanner.ts +1 -1
- package/src/context/types.ts +1 -1
- package/src/interaction/chain.ts +17 -1
- package/src/pipeline/stages/execution.ts +25 -40
- package/src/pipeline/stages/routing.ts +8 -2
- package/src/precheck/checks-agents.ts +63 -0
- package/src/precheck/checks.ts +3 -0
- package/src/precheck/index.ts +2 -0
- package/src/prompts/builder.ts +13 -6
- package/src/prompts/sections/conventions.ts +5 -7
- package/src/prompts/sections/isolation.ts +7 -7
- package/src/prompts/sections/role-task.ts +64 -64
- package/src/review/orchestrator.ts +11 -1
- package/src/routing/strategies/llm-prompts.ts +1 -1
- package/src/routing/strategies/llm.ts +3 -3
- package/src/tdd/index.ts +2 -3
- package/src/tdd/isolation.ts +0 -13
- package/src/tdd/orchestrator.ts +5 -0
- package/src/tdd/prompts.ts +1 -231
- package/src/tdd/rectification-gate.ts +2 -46
- package/src/tdd/session-runner.ts +4 -49
- package/src/tdd/verdict.ts +154 -9
- package/src/utils/git.ts +49 -0
- package/src/verification/parser.ts +0 -10
- package/src/verification/rectification-loop.ts +2 -51
- package/src/worktree/dispatcher.ts +0 -59
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/constitution.ts
CHANGED
|
@@ -4,12 +4,6 @@
|
|
|
4
4
|
* Generates agent-specific config files from nax/constitution.md.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import chalk from "chalk";
|
|
10
|
-
import { generateAll, generateFor } from "../constitution/generator";
|
|
11
|
-
import type { AgentType } from "../constitution/generators/types";
|
|
12
|
-
|
|
13
7
|
/** Constitution generate options */
|
|
14
8
|
export interface ConstitutionGenerateOptions {
|
|
15
9
|
/** Path to constitution file (default: nax/constitution.md) */
|
|
@@ -21,89 +15,3 @@ export interface ConstitutionGenerateOptions {
|
|
|
21
15
|
/** Dry run mode (don't write files) */
|
|
22
16
|
dryRun?: boolean;
|
|
23
17
|
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Constitution generate command
|
|
27
|
-
*/
|
|
28
|
-
export async function constitutionGenerateCommand(options: ConstitutionGenerateOptions): Promise<void> {
|
|
29
|
-
const workdir = process.cwd();
|
|
30
|
-
const constitutionPath = options.constitution
|
|
31
|
-
? join(workdir, options.constitution)
|
|
32
|
-
: join(workdir, "nax/constitution.md");
|
|
33
|
-
const outputDir = options.output ? join(workdir, options.output) : workdir;
|
|
34
|
-
|
|
35
|
-
// Validate constitution file exists
|
|
36
|
-
if (!existsSync(constitutionPath)) {
|
|
37
|
-
console.error(chalk.red(`[FAIL] Constitution file not found: ${constitutionPath}`));
|
|
38
|
-
console.error(chalk.yellow(`Create ${constitutionPath} first or use --constitution to specify a different path.`));
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
console.log(chalk.blue(`[OK] Loading constitution from ${constitutionPath}`));
|
|
43
|
-
|
|
44
|
-
// Validate agent type if specified
|
|
45
|
-
const validAgents: AgentType[] = ["claude", "opencode", "cursor", "windsurf", "aider"];
|
|
46
|
-
if (options.agent && !validAgents.includes(options.agent as AgentType)) {
|
|
47
|
-
console.error(chalk.red(`[FAIL] Unknown agent type: ${options.agent}`));
|
|
48
|
-
console.error(chalk.yellow(`Valid agents: ${validAgents.join(", ")}`));
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const dryRun = options.dryRun ?? false;
|
|
53
|
-
|
|
54
|
-
if (dryRun) {
|
|
55
|
-
console.log(chalk.yellow("[DRY RUN] No files will be written"));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
// Generate for specific agent or all agents
|
|
60
|
-
if (options.agent) {
|
|
61
|
-
const agent = options.agent as AgentType;
|
|
62
|
-
console.log(chalk.blue(`-> Generating config for ${agent}...`));
|
|
63
|
-
|
|
64
|
-
const result = await generateFor(agent, constitutionPath, outputDir, dryRun);
|
|
65
|
-
|
|
66
|
-
if (result.error) {
|
|
67
|
-
console.error(chalk.red(`[FAIL] ${agent}: ${result.error}`));
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (dryRun) {
|
|
72
|
-
console.log(chalk.green(`[OK] ${agent} -> ${result.outputFile} (${result.content.length} bytes, dry run)`));
|
|
73
|
-
} else {
|
|
74
|
-
console.log(chalk.green(`[OK] ${agent} -> ${result.outputFile} (${result.content.length} bytes)`));
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
77
|
-
console.log(chalk.blue("-> Generating configs for all agents..."));
|
|
78
|
-
|
|
79
|
-
const results = await generateAll(constitutionPath, outputDir, dryRun);
|
|
80
|
-
|
|
81
|
-
let errorCount = 0;
|
|
82
|
-
for (const result of results) {
|
|
83
|
-
if (result.error) {
|
|
84
|
-
console.error(chalk.red(`[FAIL] ${result.agent}: ${result.error}`));
|
|
85
|
-
errorCount++;
|
|
86
|
-
} else if (dryRun) {
|
|
87
|
-
console.log(
|
|
88
|
-
chalk.green(`[OK] ${result.agent} -> ${result.outputFile} (${result.content.length} bytes, dry run)`),
|
|
89
|
-
);
|
|
90
|
-
} else {
|
|
91
|
-
console.log(chalk.green(`[OK] ${result.agent} -> ${result.outputFile} (${result.content.length} bytes)`));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (errorCount > 0) {
|
|
96
|
-
console.error(chalk.red(`[FAIL] ${errorCount} generation(s) failed`));
|
|
97
|
-
process.exit(1);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!dryRun) {
|
|
102
|
-
console.log(chalk.green(`\n[OK] Constitution config(s) generated in ${outputDir}`));
|
|
103
|
-
}
|
|
104
|
-
} catch (err) {
|
|
105
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
106
|
-
console.error(chalk.red(`[FAIL] Generation failed: ${error}`));
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
}
|
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
|
@@ -156,36 +156,3 @@ export async function generateAll(
|
|
|
156
156
|
|
|
157
157
|
return results;
|
|
158
158
|
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Check if constitution file is newer than generated configs
|
|
162
|
-
*/
|
|
163
|
-
export function isConstitutionNewer(constitutionPath: string, outputDir: string): boolean {
|
|
164
|
-
if (!existsSync(constitutionPath)) {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const constitutionStat = Bun.file(constitutionPath);
|
|
169
|
-
const constitutionMtime = constitutionStat.lastModified;
|
|
170
|
-
|
|
171
|
-
// Check all generated files
|
|
172
|
-
const agents: AgentType[] = ["claude", "opencode", "cursor", "windsurf", "aider"];
|
|
173
|
-
for (const agent of agents) {
|
|
174
|
-
const outputFile = GENERATORS[agent].outputFile;
|
|
175
|
-
const outputPath = join(outputDir, outputFile);
|
|
176
|
-
|
|
177
|
-
if (!existsSync(outputPath)) {
|
|
178
|
-
// If any config file is missing, consider constitution newer
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const outputStat = Bun.file(outputPath);
|
|
183
|
-
const outputMtime = outputStat.lastModified;
|
|
184
|
-
|
|
185
|
-
if (constitutionMtime > outputMtime) {
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
@@ -7,4 +7,5 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export type { ConstitutionConfig, ConstitutionResult } from "./types";
|
|
10
|
-
export { loadConstitution,
|
|
10
|
+
export { loadConstitution, truncateToTokens } from "./loader";
|
|
11
|
+
export { estimateTokens } from "../optimizer/types";
|
|
@@ -8,21 +8,9 @@ import { existsSync } from "node:fs";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { validateFilePath } from "../config/path-security";
|
|
10
10
|
import { globalConfigDir } from "../config/paths";
|
|
11
|
+
import { estimateTokens } from "../optimizer/types";
|
|
11
12
|
import type { ConstitutionConfig, ConstitutionResult } from "./types";
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
* Estimate token count for text
|
|
15
|
-
*
|
|
16
|
-
* Uses simple heuristic: 1 token ≈ 3 characters (conservative estimate)
|
|
17
|
-
* This is a rough approximation sufficient for quota management.
|
|
18
|
-
*
|
|
19
|
-
* @param text - Text to estimate tokens for
|
|
20
|
-
* @returns Estimated token count
|
|
21
|
-
*/
|
|
22
|
-
export function estimateTokens(text: string): number {
|
|
23
|
-
return Math.ceil(text.length / 3);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
14
|
/**
|
|
27
15
|
* Truncate text to fit within token limit
|
|
28
16
|
*
|
package/src/context/builder.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { NaxConfig } from "../config";
|
|
9
9
|
import { getLogger } from "../logger";
|
|
10
|
+
import { estimateTokens } from "../optimizer/types";
|
|
10
11
|
import type { UserStory } from "../prd";
|
|
11
12
|
import { countStories, getContextFiles } from "../prd";
|
|
12
13
|
import { autoDetectContextFiles } from "./auto-detect";
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
createProgressContext,
|
|
19
20
|
createStoryContext,
|
|
20
21
|
createTestCoverageContext,
|
|
21
|
-
estimateTokens,
|
|
22
22
|
} from "./elements";
|
|
23
23
|
import { generateTestCoverageSummary } from "./test-scanner";
|
|
24
24
|
import type { BuiltContext, ContextBudget, ContextElement, StoryContext } from "./types";
|
|
@@ -30,7 +30,6 @@ export const _deps = {
|
|
|
30
30
|
|
|
31
31
|
// Re-export for backward compatibility
|
|
32
32
|
export {
|
|
33
|
-
estimateTokens,
|
|
34
33
|
createStoryContext,
|
|
35
34
|
createDependencyContext,
|
|
36
35
|
createErrorContext,
|
package/src/context/elements.ts
CHANGED
|
@@ -5,21 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { getLogger } from "../logger";
|
|
8
|
+
import { estimateTokens } from "../optimizer/types";
|
|
8
9
|
import type { StructuredFailure, UserStory } from "../prd";
|
|
9
10
|
import type { ContextElement } from "./types";
|
|
10
11
|
|
|
11
|
-
/**
|
|
12
|
-
* Approximate character-to-token ratio for token estimation.
|
|
13
|
-
* Value of 3 is a middle ground optimized for mixed content (prose + code + markdown).
|
|
14
|
-
* Slightly overestimates tokens, preventing budget overflow.
|
|
15
|
-
*/
|
|
16
|
-
const CHARS_PER_TOKEN = 3;
|
|
17
|
-
|
|
18
|
-
/** Estimate token count for text using character-to-token ratio. */
|
|
19
|
-
export function estimateTokens(text: string): number {
|
|
20
|
-
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
12
|
/** Create context element from current story */
|
|
24
13
|
export function createStoryContext(story: UserStory, priority: number): ContextElement {
|
|
25
14
|
const content = formatStoryAsText(story);
|
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/index.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
export type { ContextElement, ContextBudget, StoryContext, BuiltContext } from "./types";
|
|
6
6
|
|
|
7
7
|
export {
|
|
8
|
-
estimateTokens,
|
|
9
8
|
createStoryContext,
|
|
10
9
|
createDependencyContext,
|
|
11
10
|
createErrorContext,
|
|
@@ -17,6 +16,8 @@ export {
|
|
|
17
16
|
formatContextAsMarkdown,
|
|
18
17
|
} from "./builder";
|
|
19
18
|
|
|
19
|
+
export { estimateTokens } from "../optimizer/types";
|
|
20
|
+
|
|
20
21
|
export {
|
|
21
22
|
generateTestCoverageSummary,
|
|
22
23
|
scanTestFiles,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { Glob } from "bun";
|
|
11
11
|
import { getLogger } from "../logger";
|
|
12
|
-
import { estimateTokens } from "
|
|
12
|
+
import { estimateTokens } from "../optimizer/types";
|
|
13
13
|
|
|
14
14
|
// ============================================================================
|
|
15
15
|
// Types
|
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/chain.ts
CHANGED
|
@@ -86,11 +86,27 @@ export class InteractionChain {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* Send and receive in one call (convenience method)
|
|
89
|
+
* Send and receive in one call (convenience method).
|
|
90
|
+
*
|
|
91
|
+
* Normalizes "choose" type responses: when the plugin returns
|
|
92
|
+
* `action: "choose"` + `value: "<key>"`, remaps action to the selected
|
|
93
|
+
* option key so all consumers can switch on action directly without
|
|
94
|
+
* needing to inspect value themselves.
|
|
90
95
|
*/
|
|
91
96
|
async prompt(request: InteractionRequest): Promise<InteractionResponse> {
|
|
92
97
|
await this.send(request);
|
|
93
98
|
const response = await this.receive(request.id, request.timeout);
|
|
99
|
+
|
|
100
|
+
// Normalize choose responses: action="choose" means the user picked an option;
|
|
101
|
+
// the actual selection is in value. Remap to the selected key if it matches
|
|
102
|
+
// one of the declared options.
|
|
103
|
+
if (response.action === "choose" && response.value && request.options) {
|
|
104
|
+
const matched = request.options.find((o) => o.key === response.value);
|
|
105
|
+
if (matched) {
|
|
106
|
+
return { ...response, action: matched.key as InteractionResponse["action"] };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
return response;
|
|
95
111
|
}
|
|
96
112
|
|
|
@@ -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
|
/**
|
|
@@ -134,6 +134,7 @@ export const executionStage: PipelineStage = {
|
|
|
134
134
|
workdir: ctx.workdir,
|
|
135
135
|
modelTier: ctx.routing.modelTier,
|
|
136
136
|
contextMarkdown: ctx.contextMarkdown,
|
|
137
|
+
constitution: ctx.constitution?.content,
|
|
137
138
|
dryRun: false,
|
|
138
139
|
lite: isLiteMode,
|
|
139
140
|
});
|
|
@@ -156,7 +157,7 @@ export const executionStage: PipelineStage = {
|
|
|
156
157
|
// Store failure category in context for runner to use at max-attempts decision
|
|
157
158
|
ctx.tddFailureCategory = tddResult.failureCategory;
|
|
158
159
|
|
|
159
|
-
// Log
|
|
160
|
+
// Log and notify when human review is needed
|
|
160
161
|
if (tddResult.needsHumanReview) {
|
|
161
162
|
logger.warn("execution", "Human review needed", {
|
|
162
163
|
storyId: ctx.story.id,
|
|
@@ -164,6 +165,27 @@ export const executionStage: PipelineStage = {
|
|
|
164
165
|
lite: tddResult.lite,
|
|
165
166
|
failureCategory: tddResult.failureCategory,
|
|
166
167
|
});
|
|
168
|
+
// Send notification via interaction chain (Telegram in headless mode)
|
|
169
|
+
if (ctx.interaction) {
|
|
170
|
+
try {
|
|
171
|
+
await ctx.interaction.send({
|
|
172
|
+
id: `human-review-${ctx.story.id}-${Date.now()}`,
|
|
173
|
+
type: "notify",
|
|
174
|
+
featureName: ctx.featureDir ? (ctx.featureDir.split("/").pop() ?? "unknown") : "unknown",
|
|
175
|
+
storyId: ctx.story.id,
|
|
176
|
+
stage: "execution",
|
|
177
|
+
summary: `⚠️ Human review needed: ${ctx.story.id}`,
|
|
178
|
+
detail: `Story: ${ctx.story.title}\nReason: ${tddResult.reviewReason ?? "No reason provided"}\nCategory: ${tddResult.failureCategory ?? "unknown"}`,
|
|
179
|
+
fallback: "continue",
|
|
180
|
+
createdAt: Date.now(),
|
|
181
|
+
});
|
|
182
|
+
} catch (notifyErr) {
|
|
183
|
+
logger.warn("execution", "Failed to send human review notification", {
|
|
184
|
+
storyId: ctx.story.id,
|
|
185
|
+
error: String(notifyErr),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
167
189
|
}
|
|
168
190
|
|
|
169
191
|
return routeTddFailure(tddResult.failureCategory, isLiteMode, ctx, tddResult.reviewReason);
|
|
@@ -200,7 +222,7 @@ export const executionStage: PipelineStage = {
|
|
|
200
222
|
ctx.agentResult = result;
|
|
201
223
|
|
|
202
224
|
// BUG-058: Auto-commit if agent left uncommitted changes (single-session/test-after)
|
|
203
|
-
await autoCommitIfDirty(ctx.workdir, "single-session", ctx.story.id);
|
|
225
|
+
await autoCommitIfDirty(ctx.workdir, "execution", "single-session", ctx.story.id);
|
|
204
226
|
|
|
205
227
|
// merge-conflict trigger: detect CONFLICT markers in agent output
|
|
206
228
|
const combinedOutput = (result.output ?? "") + (result.stderr ?? "");
|
|
@@ -270,40 +292,3 @@ export const _executionDeps = {
|
|
|
270
292
|
isAmbiguousOutput,
|
|
271
293
|
checkStoryAmbiguity,
|
|
272
294
|
};
|
|
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,7 +92,7 @@ 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).
|
|
@@ -106,7 +111,7 @@ export const routingStage: PipelineStage = {
|
|
|
106
111
|
}
|
|
107
112
|
} else {
|
|
108
113
|
// Cache miss: no routing, or contentHash present but mismatched — fresh classification
|
|
109
|
-
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);
|
|
110
115
|
// currentHash already computed if a mismatch was detected; compute now if starting fresh
|
|
111
116
|
currentHash = currentHash ?? _routingDeps.computeStoryContentHash(ctx.story);
|
|
112
117
|
ctx.story.routing = {
|
|
@@ -223,4 +228,5 @@ export const _routingDeps = {
|
|
|
223
228
|
applyDecomposition,
|
|
224
229
|
runDecompose,
|
|
225
230
|
checkStoryOversized,
|
|
231
|
+
getAgent,
|
|
226
232
|
};
|
|
@@ -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
|
+
}
|