@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/dist/nax.js
CHANGED
|
@@ -3024,796 +3024,6 @@ var init_logger2 = __esm(() => {
|
|
|
3024
3024
|
init_formatters();
|
|
3025
3025
|
});
|
|
3026
3026
|
|
|
3027
|
-
// src/execution/pid-registry.ts
|
|
3028
|
-
import { existsSync } from "fs";
|
|
3029
|
-
|
|
3030
|
-
class PidRegistry {
|
|
3031
|
-
workdir;
|
|
3032
|
-
pidsFilePath;
|
|
3033
|
-
pids = new Set;
|
|
3034
|
-
platform;
|
|
3035
|
-
constructor(workdir, platform) {
|
|
3036
|
-
this.workdir = workdir;
|
|
3037
|
-
this.pidsFilePath = `${workdir}/${PID_REGISTRY_FILE}`;
|
|
3038
|
-
this.platform = platform ?? process.platform;
|
|
3039
|
-
}
|
|
3040
|
-
async register(pid) {
|
|
3041
|
-
const logger = getSafeLogger();
|
|
3042
|
-
this.pids.add(pid);
|
|
3043
|
-
const entry = {
|
|
3044
|
-
pid,
|
|
3045
|
-
spawnedAt: new Date().toISOString(),
|
|
3046
|
-
workdir: this.workdir
|
|
3047
|
-
};
|
|
3048
|
-
try {
|
|
3049
|
-
let existingContent = "";
|
|
3050
|
-
if (existsSync(this.pidsFilePath)) {
|
|
3051
|
-
existingContent = await Bun.file(this.pidsFilePath).text();
|
|
3052
|
-
}
|
|
3053
|
-
const line = `${JSON.stringify(entry)}
|
|
3054
|
-
`;
|
|
3055
|
-
await Bun.write(this.pidsFilePath, existingContent + line);
|
|
3056
|
-
logger?.debug("pid-registry", `Registered PID ${pid}`, { pid });
|
|
3057
|
-
} catch (err) {
|
|
3058
|
-
logger?.warn("pid-registry", `Failed to write PID ${pid} to registry`, {
|
|
3059
|
-
error: err.message
|
|
3060
|
-
});
|
|
3061
|
-
}
|
|
3062
|
-
}
|
|
3063
|
-
async unregister(pid) {
|
|
3064
|
-
const logger = getSafeLogger();
|
|
3065
|
-
this.pids.delete(pid);
|
|
3066
|
-
try {
|
|
3067
|
-
await this.writePidsFile();
|
|
3068
|
-
logger?.debug("pid-registry", `Unregistered PID ${pid}`, { pid });
|
|
3069
|
-
} catch (err) {
|
|
3070
|
-
logger?.warn("pid-registry", `Failed to unregister PID ${pid}`, {
|
|
3071
|
-
error: err.message
|
|
3072
|
-
});
|
|
3073
|
-
}
|
|
3074
|
-
}
|
|
3075
|
-
async killAll() {
|
|
3076
|
-
const logger = getSafeLogger();
|
|
3077
|
-
const pids = Array.from(this.pids);
|
|
3078
|
-
if (pids.length === 0) {
|
|
3079
|
-
logger?.debug("pid-registry", "No PIDs to kill");
|
|
3080
|
-
return;
|
|
3081
|
-
}
|
|
3082
|
-
logger?.info("pid-registry", `Killing ${pids.length} registered processes`, { pids });
|
|
3083
|
-
const killPromises = pids.map((pid) => this.killPid(pid));
|
|
3084
|
-
await Promise.allSettled(killPromises);
|
|
3085
|
-
try {
|
|
3086
|
-
await Bun.write(this.pidsFilePath, "");
|
|
3087
|
-
this.pids.clear();
|
|
3088
|
-
logger?.info("pid-registry", "All registered PIDs killed and registry cleared");
|
|
3089
|
-
} catch (err) {
|
|
3090
|
-
logger?.warn("pid-registry", "Failed to clear registry file", {
|
|
3091
|
-
error: err.message
|
|
3092
|
-
});
|
|
3093
|
-
}
|
|
3094
|
-
}
|
|
3095
|
-
async cleanupStale() {
|
|
3096
|
-
const logger = getSafeLogger();
|
|
3097
|
-
if (!existsSync(this.pidsFilePath)) {
|
|
3098
|
-
logger?.debug("pid-registry", "No stale PIDs file found");
|
|
3099
|
-
return;
|
|
3100
|
-
}
|
|
3101
|
-
try {
|
|
3102
|
-
const content = await Bun.file(this.pidsFilePath).text();
|
|
3103
|
-
const lines = content.split(`
|
|
3104
|
-
`).filter((line) => line.trim()).map((line) => {
|
|
3105
|
-
try {
|
|
3106
|
-
return JSON.parse(line);
|
|
3107
|
-
} catch {
|
|
3108
|
-
return null;
|
|
3109
|
-
}
|
|
3110
|
-
}).filter((entry) => entry !== null);
|
|
3111
|
-
if (lines.length === 0) {
|
|
3112
|
-
logger?.debug("pid-registry", "No stale PIDs to cleanup");
|
|
3113
|
-
await Bun.write(this.pidsFilePath, "");
|
|
3114
|
-
return;
|
|
3115
|
-
}
|
|
3116
|
-
const stalePids = lines.map((entry) => entry.pid);
|
|
3117
|
-
logger?.info("pid-registry", `Cleaning up ${stalePids.length} stale PIDs from previous run`, {
|
|
3118
|
-
pids: stalePids
|
|
3119
|
-
});
|
|
3120
|
-
const killPromises = stalePids.map((pid) => this.killPid(pid));
|
|
3121
|
-
await Promise.allSettled(killPromises);
|
|
3122
|
-
await Bun.write(this.pidsFilePath, "");
|
|
3123
|
-
logger?.info("pid-registry", "Stale PIDs cleanup completed");
|
|
3124
|
-
} catch (err) {
|
|
3125
|
-
logger?.warn("pid-registry", "Failed to cleanup stale PIDs", {
|
|
3126
|
-
error: err.message
|
|
3127
|
-
});
|
|
3128
|
-
}
|
|
3129
|
-
}
|
|
3130
|
-
async killPid(pid) {
|
|
3131
|
-
const logger = getSafeLogger();
|
|
3132
|
-
try {
|
|
3133
|
-
const checkProc = Bun.spawn(["kill", "-0", String(pid)], {
|
|
3134
|
-
stdout: "pipe",
|
|
3135
|
-
stderr: "pipe"
|
|
3136
|
-
});
|
|
3137
|
-
const checkCode = await checkProc.exited;
|
|
3138
|
-
if (checkCode !== 0) {
|
|
3139
|
-
logger?.debug("pid-registry", `PID ${pid} not found (already exited)`, { pid });
|
|
3140
|
-
return;
|
|
3141
|
-
}
|
|
3142
|
-
const killArgs = this.platform === "linux" ? ["kill", "-TERM", `-${pid}`] : ["kill", "-TERM", String(pid)];
|
|
3143
|
-
const killProc = Bun.spawn(killArgs, {
|
|
3144
|
-
stdout: "pipe",
|
|
3145
|
-
stderr: "pipe"
|
|
3146
|
-
});
|
|
3147
|
-
const killCode = await killProc.exited;
|
|
3148
|
-
if (killCode === 0) {
|
|
3149
|
-
logger?.debug("pid-registry", `Killed PID ${pid}`, { pid });
|
|
3150
|
-
} else {
|
|
3151
|
-
const stderr = await new Response(killProc.stderr).text();
|
|
3152
|
-
logger?.warn("pid-registry", `Failed to kill PID ${pid}`, {
|
|
3153
|
-
pid,
|
|
3154
|
-
exitCode: killCode,
|
|
3155
|
-
stderr: stderr.trim()
|
|
3156
|
-
});
|
|
3157
|
-
}
|
|
3158
|
-
} catch (err) {
|
|
3159
|
-
logger?.warn("pid-registry", `Error killing PID ${pid}`, {
|
|
3160
|
-
pid,
|
|
3161
|
-
error: err.message
|
|
3162
|
-
});
|
|
3163
|
-
}
|
|
3164
|
-
}
|
|
3165
|
-
async writePidsFile() {
|
|
3166
|
-
const entries = Array.from(this.pids).map((pid) => ({
|
|
3167
|
-
pid,
|
|
3168
|
-
spawnedAt: new Date().toISOString(),
|
|
3169
|
-
workdir: this.workdir
|
|
3170
|
-
}));
|
|
3171
|
-
const content = entries.map((entry) => JSON.stringify(entry)).join(`
|
|
3172
|
-
`);
|
|
3173
|
-
await Bun.write(this.pidsFilePath, content ? `${content}
|
|
3174
|
-
` : "");
|
|
3175
|
-
}
|
|
3176
|
-
getPids() {
|
|
3177
|
-
return Array.from(this.pids);
|
|
3178
|
-
}
|
|
3179
|
-
}
|
|
3180
|
-
var PID_REGISTRY_FILE = ".nax-pids";
|
|
3181
|
-
var init_pid_registry = __esm(() => {
|
|
3182
|
-
init_logger2();
|
|
3183
|
-
});
|
|
3184
|
-
|
|
3185
|
-
// src/agents/claude-decompose.ts
|
|
3186
|
-
function buildDecomposePrompt(options) {
|
|
3187
|
-
return `You are a requirements analyst. Break down the following feature specification into user stories and classify each story's complexity.
|
|
3188
|
-
|
|
3189
|
-
CODEBASE CONTEXT:
|
|
3190
|
-
${options.codebaseContext}
|
|
3191
|
-
|
|
3192
|
-
FEATURE SPECIFICATION:
|
|
3193
|
-
${options.specContent}
|
|
3194
|
-
|
|
3195
|
-
Decompose this spec into user stories. For each story, provide:
|
|
3196
|
-
1. id: Story ID (e.g., "US-001")
|
|
3197
|
-
2. title: Concise story title
|
|
3198
|
-
3. description: What needs to be implemented
|
|
3199
|
-
4. acceptanceCriteria: Array of testable criteria
|
|
3200
|
-
5. tags: Array of routing tags (e.g., ["security", "api"])
|
|
3201
|
-
6. dependencies: Array of story IDs this depends on (e.g., ["US-001"])
|
|
3202
|
-
7. complexity: "simple" | "medium" | "complex" | "expert"
|
|
3203
|
-
8. contextFiles: Array of file paths to inject into agent prompt before execution
|
|
3204
|
-
9. reasoning: Why this complexity level
|
|
3205
|
-
10. estimatedLOC: Estimated lines of code to change
|
|
3206
|
-
11. risks: Array of implementation risks
|
|
3207
|
-
12. testStrategy: "three-session-tdd" | "test-after"
|
|
3208
|
-
|
|
3209
|
-
testStrategy rules:
|
|
3210
|
-
- "three-session-tdd": ONLY for complex/expert tasks that are security-critical (auth, encryption, tokens, credentials) or define public API contracts consumers depend on
|
|
3211
|
-
- "test-after": for all other tasks including simple/medium complexity
|
|
3212
|
-
- A "simple" complexity task should almost never be "three-session-tdd"
|
|
3213
|
-
|
|
3214
|
-
Complexity classification rules:
|
|
3215
|
-
- simple: 1-3 files, <100 LOC, straightforward implementation, existing patterns
|
|
3216
|
-
- medium: 3-6 files, 100-300 LOC, moderate logic, some new patterns
|
|
3217
|
-
- complex: 6+ files, 300-800 LOC, architectural changes, cross-cutting concerns
|
|
3218
|
-
- expert: Security/crypto/real-time/distributed systems, >800 LOC, new infrastructure
|
|
3219
|
-
|
|
3220
|
-
Grouping Guidelines:
|
|
3221
|
-
- Combine small, related tasks (e.g., multiple utility functions, interfaces) into a single "simple" or "medium" story.
|
|
3222
|
-
- Do NOT create separate stories for every single file or function unless complex.
|
|
3223
|
-
- Aim for coherent units of value (e.g., "Implement User Authentication" vs "Create User Interface", "Create Login Service").
|
|
3224
|
-
- Maximum recommended stories: 10-15 per feature. Group aggressively if list grows too long.
|
|
3225
|
-
|
|
3226
|
-
Consider:
|
|
3227
|
-
1. Does infrastructure exist? (e.g., "add caching" when no cache layer exists = complex)
|
|
3228
|
-
2. How many files will be touched?
|
|
3229
|
-
3. Are there cross-cutting concerns (auth, validation, error handling)?
|
|
3230
|
-
4. Does it require new dependencies or architectural decisions?
|
|
3231
|
-
|
|
3232
|
-
Respond with ONLY a JSON array (no markdown code fences):
|
|
3233
|
-
[{
|
|
3234
|
-
"id": "US-001",
|
|
3235
|
-
"title": "Story title",
|
|
3236
|
-
"description": "Story description",
|
|
3237
|
-
"acceptanceCriteria": ["Criterion 1", "Criterion 2"],
|
|
3238
|
-
"tags": ["tag1"],
|
|
3239
|
-
"dependencies": [],
|
|
3240
|
-
"complexity": "medium",
|
|
3241
|
-
"contextFiles": ["src/path/to/file.ts"],
|
|
3242
|
-
"reasoning": "Why this complexity level",
|
|
3243
|
-
"estimatedLOC": 150,
|
|
3244
|
-
"risks": ["Risk 1"],
|
|
3245
|
-
"testStrategy": "test-after"
|
|
3246
|
-
}]`;
|
|
3247
|
-
}
|
|
3248
|
-
function parseDecomposeOutput(output) {
|
|
3249
|
-
const jsonMatch = output.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
|
|
3250
|
-
let jsonText = jsonMatch ? jsonMatch[1] : output;
|
|
3251
|
-
if (!jsonMatch) {
|
|
3252
|
-
const arrayMatch = output.match(/\[[\s\S]*\]/);
|
|
3253
|
-
if (arrayMatch) {
|
|
3254
|
-
jsonText = arrayMatch[0];
|
|
3255
|
-
}
|
|
3256
|
-
}
|
|
3257
|
-
let parsed;
|
|
3258
|
-
try {
|
|
3259
|
-
parsed = JSON.parse(jsonText.trim());
|
|
3260
|
-
} catch (error) {
|
|
3261
|
-
throw new Error(`Failed to parse decompose output as JSON: ${error.message}
|
|
3262
|
-
|
|
3263
|
-
Output:
|
|
3264
|
-
${output.slice(0, 500)}`);
|
|
3265
|
-
}
|
|
3266
|
-
if (!Array.isArray(parsed)) {
|
|
3267
|
-
throw new Error("Decompose output is not an array");
|
|
3268
|
-
}
|
|
3269
|
-
const stories = parsed.map((item, index) => {
|
|
3270
|
-
if (typeof item !== "object" || item === null) {
|
|
3271
|
-
throw new Error(`Story at index ${index} is not an object`);
|
|
3272
|
-
}
|
|
3273
|
-
const record = item;
|
|
3274
|
-
if (!record.id || typeof record.id !== "string") {
|
|
3275
|
-
throw new Error(`Story at index ${index} missing valid 'id' field`);
|
|
3276
|
-
}
|
|
3277
|
-
if (!record.title || typeof record.title !== "string") {
|
|
3278
|
-
throw new Error(`Story ${record.id} missing valid 'title' field`);
|
|
3279
|
-
}
|
|
3280
|
-
return {
|
|
3281
|
-
id: record.id,
|
|
3282
|
-
title: record.title,
|
|
3283
|
-
description: String(record.description || record.title),
|
|
3284
|
-
acceptanceCriteria: Array.isArray(record.acceptanceCriteria) ? record.acceptanceCriteria : ["Implementation complete"],
|
|
3285
|
-
tags: Array.isArray(record.tags) ? record.tags : [],
|
|
3286
|
-
dependencies: Array.isArray(record.dependencies) ? record.dependencies : [],
|
|
3287
|
-
complexity: validateComplexity(record.complexity),
|
|
3288
|
-
contextFiles: Array.isArray(record.contextFiles) ? record.contextFiles : Array.isArray(record.relevantFiles) ? record.relevantFiles : [],
|
|
3289
|
-
relevantFiles: Array.isArray(record.relevantFiles) ? record.relevantFiles : [],
|
|
3290
|
-
reasoning: String(record.reasoning || "No reasoning provided"),
|
|
3291
|
-
estimatedLOC: Number(record.estimatedLOC) || 0,
|
|
3292
|
-
risks: Array.isArray(record.risks) ? record.risks : [],
|
|
3293
|
-
testStrategy: record.testStrategy === "three-session-tdd" ? "three-session-tdd" : record.testStrategy === "test-after" ? "test-after" : undefined
|
|
3294
|
-
};
|
|
3295
|
-
});
|
|
3296
|
-
if (stories.length === 0) {
|
|
3297
|
-
throw new Error("Decompose returned empty story array");
|
|
3298
|
-
}
|
|
3299
|
-
return stories;
|
|
3300
|
-
}
|
|
3301
|
-
function validateComplexity(value) {
|
|
3302
|
-
if (value === "simple" || value === "medium" || value === "complex" || value === "expert") {
|
|
3303
|
-
return value;
|
|
3304
|
-
}
|
|
3305
|
-
return "medium";
|
|
3306
|
-
}
|
|
3307
|
-
|
|
3308
|
-
// src/agents/claude-plan.ts
|
|
3309
|
-
import { mkdtempSync, rmSync } from "fs";
|
|
3310
|
-
import { tmpdir } from "os";
|
|
3311
|
-
import { join } from "path";
|
|
3312
|
-
function buildPlanCommand(binary, options) {
|
|
3313
|
-
const cmd = [binary, "--permission-mode", "plan"];
|
|
3314
|
-
if (options.modelDef) {
|
|
3315
|
-
cmd.push("--model", options.modelDef.model);
|
|
3316
|
-
}
|
|
3317
|
-
cmd.push("--dangerously-skip-permissions");
|
|
3318
|
-
let fullPrompt = options.prompt;
|
|
3319
|
-
if (options.codebaseContext) {
|
|
3320
|
-
fullPrompt = `${options.codebaseContext}
|
|
3321
|
-
|
|
3322
|
-
${options.prompt}`;
|
|
3323
|
-
}
|
|
3324
|
-
if (options.inputFile) {
|
|
3325
|
-
try {
|
|
3326
|
-
const inputContent = __require("fs").readFileSync(__require("path").resolve(options.workdir, options.inputFile), "utf-8");
|
|
3327
|
-
fullPrompt = `${fullPrompt}
|
|
3328
|
-
|
|
3329
|
-
## Input Requirements
|
|
3330
|
-
|
|
3331
|
-
${inputContent}`;
|
|
3332
|
-
} catch (error) {
|
|
3333
|
-
throw new Error(`Failed to read input file ${options.inputFile}: ${error.message}`);
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
if (!options.interactive) {
|
|
3337
|
-
cmd.push("-p", fullPrompt);
|
|
3338
|
-
} else {
|
|
3339
|
-
cmd.push("-p", fullPrompt);
|
|
3340
|
-
}
|
|
3341
|
-
return cmd;
|
|
3342
|
-
}
|
|
3343
|
-
async function runPlan(binary, options, pidRegistry, buildAllowedEnv) {
|
|
3344
|
-
const cmd = buildPlanCommand(binary, options);
|
|
3345
|
-
const envOptions = {
|
|
3346
|
-
workdir: options.workdir,
|
|
3347
|
-
modelDef: options.modelDef || { provider: "anthropic", model: "claude-sonnet-4-5", env: {} },
|
|
3348
|
-
prompt: "",
|
|
3349
|
-
modelTier: "balanced",
|
|
3350
|
-
timeoutSeconds: 600
|
|
3351
|
-
};
|
|
3352
|
-
if (options.interactive) {
|
|
3353
|
-
const proc = Bun.spawn(cmd, {
|
|
3354
|
-
cwd: options.workdir,
|
|
3355
|
-
stdin: "inherit",
|
|
3356
|
-
stdout: "inherit",
|
|
3357
|
-
stderr: "inherit",
|
|
3358
|
-
env: buildAllowedEnv(envOptions)
|
|
3359
|
-
});
|
|
3360
|
-
await pidRegistry.register(proc.pid);
|
|
3361
|
-
const exitCode = await proc.exited;
|
|
3362
|
-
await pidRegistry.unregister(proc.pid);
|
|
3363
|
-
if (exitCode !== 0) {
|
|
3364
|
-
throw new Error(`Plan mode failed with exit code ${exitCode}`);
|
|
3365
|
-
}
|
|
3366
|
-
return { specContent: "", conversationLog: "" };
|
|
3367
|
-
}
|
|
3368
|
-
const tempDir = mkdtempSync(join(tmpdir(), "nax-plan-"));
|
|
3369
|
-
const outFile = join(tempDir, "stdout.txt");
|
|
3370
|
-
const errFile = join(tempDir, "stderr.txt");
|
|
3371
|
-
try {
|
|
3372
|
-
const proc = Bun.spawn(cmd, {
|
|
3373
|
-
cwd: options.workdir,
|
|
3374
|
-
stdin: "ignore",
|
|
3375
|
-
stdout: Bun.file(outFile),
|
|
3376
|
-
stderr: Bun.file(errFile),
|
|
3377
|
-
env: buildAllowedEnv(envOptions)
|
|
3378
|
-
});
|
|
3379
|
-
await pidRegistry.register(proc.pid);
|
|
3380
|
-
const exitCode = await proc.exited;
|
|
3381
|
-
await pidRegistry.unregister(proc.pid);
|
|
3382
|
-
const specContent = await Bun.file(outFile).text();
|
|
3383
|
-
const conversationLog = await Bun.file(errFile).text();
|
|
3384
|
-
if (exitCode !== 0) {
|
|
3385
|
-
throw new Error(`Plan mode failed with exit code ${exitCode}: ${conversationLog || "unknown error"}`);
|
|
3386
|
-
}
|
|
3387
|
-
return { specContent, conversationLog };
|
|
3388
|
-
} finally {
|
|
3389
|
-
try {
|
|
3390
|
-
rmSync(tempDir, { recursive: true });
|
|
3391
|
-
} catch (error) {
|
|
3392
|
-
const logger = getLogger();
|
|
3393
|
-
logger?.debug("agent", "Failed to clean up temp directory", { error, tempDir });
|
|
3394
|
-
}
|
|
3395
|
-
}
|
|
3396
|
-
}
|
|
3397
|
-
var init_claude_plan = __esm(() => {
|
|
3398
|
-
init_logger2();
|
|
3399
|
-
});
|
|
3400
|
-
|
|
3401
|
-
// src/agents/cost.ts
|
|
3402
|
-
function parseTokenUsage(output) {
|
|
3403
|
-
try {
|
|
3404
|
-
const jsonMatch = output.match(/\{[^}]*"usage"\s*:\s*\{[^}]*"input_tokens"\s*:\s*(\d+)[^}]*"output_tokens"\s*:\s*(\d+)[^}]*\}[^}]*\}/);
|
|
3405
|
-
if (jsonMatch) {
|
|
3406
|
-
return {
|
|
3407
|
-
inputTokens: Number.parseInt(jsonMatch[1], 10),
|
|
3408
|
-
outputTokens: Number.parseInt(jsonMatch[2], 10),
|
|
3409
|
-
confidence: "exact"
|
|
3410
|
-
};
|
|
3411
|
-
}
|
|
3412
|
-
const lines = output.split(`
|
|
3413
|
-
`);
|
|
3414
|
-
for (const line of lines) {
|
|
3415
|
-
if (line.trim().startsWith("{")) {
|
|
3416
|
-
try {
|
|
3417
|
-
const parsed = JSON.parse(line);
|
|
3418
|
-
if (parsed.usage?.input_tokens && parsed.usage?.output_tokens) {
|
|
3419
|
-
return {
|
|
3420
|
-
inputTokens: parsed.usage.input_tokens,
|
|
3421
|
-
outputTokens: parsed.usage.output_tokens,
|
|
3422
|
-
confidence: "exact"
|
|
3423
|
-
};
|
|
3424
|
-
}
|
|
3425
|
-
} catch {}
|
|
3426
|
-
}
|
|
3427
|
-
}
|
|
3428
|
-
} catch {}
|
|
3429
|
-
const inputMatch = output.match(/\b(?:input|input_tokens)\s*:\s*(\d{2,})|(?:input)\s+(?:tokens?)\s*:\s*(\d{2,})/i);
|
|
3430
|
-
const outputMatch = output.match(/\b(?:output|output_tokens)\s*:\s*(\d{2,})|(?:output)\s+(?:tokens?)\s*:\s*(\d{2,})/i);
|
|
3431
|
-
if (inputMatch && outputMatch) {
|
|
3432
|
-
const inputTokens = Number.parseInt(inputMatch[1] || inputMatch[2], 10);
|
|
3433
|
-
const outputTokens = Number.parseInt(outputMatch[1] || outputMatch[2], 10);
|
|
3434
|
-
if (inputTokens > 1e6 || outputTokens > 1e6) {
|
|
3435
|
-
return null;
|
|
3436
|
-
}
|
|
3437
|
-
return {
|
|
3438
|
-
inputTokens,
|
|
3439
|
-
outputTokens,
|
|
3440
|
-
confidence: "estimated"
|
|
3441
|
-
};
|
|
3442
|
-
}
|
|
3443
|
-
return null;
|
|
3444
|
-
}
|
|
3445
|
-
function estimateCost(modelTier, inputTokens, outputTokens, customRates) {
|
|
3446
|
-
const rates = customRates ?? COST_RATES[modelTier];
|
|
3447
|
-
const inputCost = inputTokens / 1e6 * rates.inputPer1M;
|
|
3448
|
-
const outputCost = outputTokens / 1e6 * rates.outputPer1M;
|
|
3449
|
-
return inputCost + outputCost;
|
|
3450
|
-
}
|
|
3451
|
-
function estimateCostFromOutput(modelTier, output) {
|
|
3452
|
-
const usage = parseTokenUsage(output);
|
|
3453
|
-
if (!usage) {
|
|
3454
|
-
return null;
|
|
3455
|
-
}
|
|
3456
|
-
const cost = estimateCost(modelTier, usage.inputTokens, usage.outputTokens);
|
|
3457
|
-
return {
|
|
3458
|
-
cost,
|
|
3459
|
-
confidence: usage.confidence
|
|
3460
|
-
};
|
|
3461
|
-
}
|
|
3462
|
-
function estimateCostByDuration(modelTier, durationMs) {
|
|
3463
|
-
const costPerMinute = {
|
|
3464
|
-
fast: 0.01,
|
|
3465
|
-
balanced: 0.05,
|
|
3466
|
-
powerful: 0.15
|
|
3467
|
-
};
|
|
3468
|
-
const minutes = durationMs / 60000;
|
|
3469
|
-
const cost = minutes * costPerMinute[modelTier];
|
|
3470
|
-
return {
|
|
3471
|
-
cost,
|
|
3472
|
-
confidence: "fallback"
|
|
3473
|
-
};
|
|
3474
|
-
}
|
|
3475
|
-
var COST_RATES;
|
|
3476
|
-
var init_cost = __esm(() => {
|
|
3477
|
-
COST_RATES = {
|
|
3478
|
-
fast: {
|
|
3479
|
-
inputPer1M: 0.8,
|
|
3480
|
-
outputPer1M: 4
|
|
3481
|
-
},
|
|
3482
|
-
balanced: {
|
|
3483
|
-
inputPer1M: 3,
|
|
3484
|
-
outputPer1M: 15
|
|
3485
|
-
},
|
|
3486
|
-
powerful: {
|
|
3487
|
-
inputPer1M: 15,
|
|
3488
|
-
outputPer1M: 75
|
|
3489
|
-
}
|
|
3490
|
-
};
|
|
3491
|
-
});
|
|
3492
|
-
|
|
3493
|
-
// src/agents/claude.ts
|
|
3494
|
-
class ClaudeCodeAdapter {
|
|
3495
|
-
name = "claude";
|
|
3496
|
-
displayName = "Claude Code";
|
|
3497
|
-
binary = "claude";
|
|
3498
|
-
capabilities = {
|
|
3499
|
-
supportedTiers: ["fast", "balanced", "powerful"],
|
|
3500
|
-
maxContextTokens: 200000,
|
|
3501
|
-
features: new Set(["tdd", "review", "refactor", "batch"])
|
|
3502
|
-
};
|
|
3503
|
-
pidRegistries = new Map;
|
|
3504
|
-
getPidRegistry(workdir) {
|
|
3505
|
-
if (!this.pidRegistries.has(workdir)) {
|
|
3506
|
-
this.pidRegistries.set(workdir, new PidRegistry(workdir));
|
|
3507
|
-
}
|
|
3508
|
-
const registry = this.pidRegistries.get(workdir);
|
|
3509
|
-
if (!registry) {
|
|
3510
|
-
throw new Error(`PidRegistry not found for workdir: ${workdir}`);
|
|
3511
|
-
}
|
|
3512
|
-
return registry;
|
|
3513
|
-
}
|
|
3514
|
-
async isInstalled() {
|
|
3515
|
-
try {
|
|
3516
|
-
const proc = Bun.spawn(["which", this.binary], { stdout: "pipe", stderr: "pipe" });
|
|
3517
|
-
const code = await proc.exited;
|
|
3518
|
-
return code === 0;
|
|
3519
|
-
} catch (error) {
|
|
3520
|
-
const logger = getLogger();
|
|
3521
|
-
logger?.debug("agent", "Failed to check if agent is installed", { error });
|
|
3522
|
-
return false;
|
|
3523
|
-
}
|
|
3524
|
-
}
|
|
3525
|
-
buildCommand(options) {
|
|
3526
|
-
const model = options.modelDef.model;
|
|
3527
|
-
const skipPermissions = options.dangerouslySkipPermissions ?? true;
|
|
3528
|
-
const permArgs = skipPermissions ? ["--dangerously-skip-permissions"] : [];
|
|
3529
|
-
return [this.binary, "--model", model, ...permArgs, "-p", options.prompt];
|
|
3530
|
-
}
|
|
3531
|
-
async run(options) {
|
|
3532
|
-
const maxRetries = 3;
|
|
3533
|
-
let lastError = null;
|
|
3534
|
-
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
3535
|
-
try {
|
|
3536
|
-
const result = await this.runOnce(options, attempt);
|
|
3537
|
-
if (result.rateLimited && attempt < maxRetries) {
|
|
3538
|
-
const backoffMs = 2 ** attempt * 1000;
|
|
3539
|
-
const logger = getLogger();
|
|
3540
|
-
logger.warn("agent", "Rate limited, retrying", { backoffSeconds: backoffMs / 1000, attempt, maxRetries });
|
|
3541
|
-
await Bun.sleep(backoffMs);
|
|
3542
|
-
continue;
|
|
3543
|
-
}
|
|
3544
|
-
return result;
|
|
3545
|
-
} catch (error) {
|
|
3546
|
-
lastError = error;
|
|
3547
|
-
const isSpawnError = lastError.message.includes("spawn") || lastError.message.includes("ENOENT");
|
|
3548
|
-
if (isSpawnError && attempt < maxRetries) {
|
|
3549
|
-
const backoffMs = 2 ** attempt * 1000;
|
|
3550
|
-
const logger = getLogger();
|
|
3551
|
-
logger.warn("agent", "Agent spawn error, retrying", {
|
|
3552
|
-
error: lastError.message,
|
|
3553
|
-
backoffSeconds: backoffMs / 1000,
|
|
3554
|
-
attempt,
|
|
3555
|
-
maxRetries
|
|
3556
|
-
});
|
|
3557
|
-
await Bun.sleep(backoffMs);
|
|
3558
|
-
continue;
|
|
3559
|
-
}
|
|
3560
|
-
throw lastError;
|
|
3561
|
-
}
|
|
3562
|
-
}
|
|
3563
|
-
throw lastError || new Error("Agent execution failed after all retries");
|
|
3564
|
-
}
|
|
3565
|
-
buildAllowedEnv(options) {
|
|
3566
|
-
const allowed = {};
|
|
3567
|
-
const essentialVars = ["PATH", "HOME", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
|
|
3568
|
-
for (const varName of essentialVars) {
|
|
3569
|
-
if (process.env[varName]) {
|
|
3570
|
-
allowed[varName] = process.env[varName];
|
|
3571
|
-
}
|
|
3572
|
-
}
|
|
3573
|
-
const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"];
|
|
3574
|
-
for (const varName of apiKeyVars) {
|
|
3575
|
-
if (process.env[varName]) {
|
|
3576
|
-
allowed[varName] = process.env[varName];
|
|
3577
|
-
}
|
|
3578
|
-
}
|
|
3579
|
-
const allowedPrefixes = ["CLAUDE_", "NAX_", "CLAW_", "TURBO_"];
|
|
3580
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
3581
|
-
if (allowedPrefixes.some((prefix) => key.startsWith(prefix))) {
|
|
3582
|
-
allowed[key] = value;
|
|
3583
|
-
}
|
|
3584
|
-
}
|
|
3585
|
-
if (options.modelDef.env) {
|
|
3586
|
-
Object.assign(allowed, options.modelDef.env);
|
|
3587
|
-
}
|
|
3588
|
-
if (options.env) {
|
|
3589
|
-
Object.assign(allowed, options.env);
|
|
3590
|
-
}
|
|
3591
|
-
return allowed;
|
|
3592
|
-
}
|
|
3593
|
-
async runOnce(options, _attempt) {
|
|
3594
|
-
const cmd = this.buildCommand(options);
|
|
3595
|
-
const startTime = Date.now();
|
|
3596
|
-
const proc = Bun.spawn(cmd, {
|
|
3597
|
-
cwd: options.workdir,
|
|
3598
|
-
stdout: "pipe",
|
|
3599
|
-
stderr: "inherit",
|
|
3600
|
-
env: this.buildAllowedEnv(options)
|
|
3601
|
-
});
|
|
3602
|
-
const processPid = proc.pid;
|
|
3603
|
-
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
3604
|
-
await pidRegistry.register(processPid);
|
|
3605
|
-
let timedOut = false;
|
|
3606
|
-
const timeoutId = setTimeout(() => {
|
|
3607
|
-
timedOut = true;
|
|
3608
|
-
try {
|
|
3609
|
-
_runOnceDeps.killProc(proc, "SIGTERM");
|
|
3610
|
-
} catch {}
|
|
3611
|
-
setTimeout(() => {
|
|
3612
|
-
try {
|
|
3613
|
-
_runOnceDeps.killProc(proc, "SIGKILL");
|
|
3614
|
-
} catch {}
|
|
3615
|
-
}, SIGKILL_GRACE_PERIOD_MS);
|
|
3616
|
-
}, options.timeoutSeconds * 1000);
|
|
3617
|
-
let exitCode;
|
|
3618
|
-
try {
|
|
3619
|
-
const hardDeadlineMs = options.timeoutSeconds * 1000 + SIGKILL_GRACE_PERIOD_MS + 3000;
|
|
3620
|
-
exitCode = await Promise.race([
|
|
3621
|
-
proc.exited,
|
|
3622
|
-
new Promise((resolve) => setTimeout(() => resolve(-1), hardDeadlineMs))
|
|
3623
|
-
]);
|
|
3624
|
-
if (exitCode === -1) {
|
|
3625
|
-
try {
|
|
3626
|
-
process.kill(processPid, "SIGKILL");
|
|
3627
|
-
} catch {}
|
|
3628
|
-
try {
|
|
3629
|
-
process.kill(-processPid, "SIGKILL");
|
|
3630
|
-
} catch {}
|
|
3631
|
-
}
|
|
3632
|
-
} finally {
|
|
3633
|
-
clearTimeout(timeoutId);
|
|
3634
|
-
await pidRegistry.unregister(processPid);
|
|
3635
|
-
}
|
|
3636
|
-
const stdout = await Promise.race([
|
|
3637
|
-
new Response(proc.stdout).text(),
|
|
3638
|
-
new Promise((resolve) => setTimeout(() => resolve(""), 5000))
|
|
3639
|
-
]);
|
|
3640
|
-
const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
|
|
3641
|
-
const durationMs = Date.now() - startTime;
|
|
3642
|
-
const fullOutput = stdout + stderr;
|
|
3643
|
-
const rateLimited = fullOutput.toLowerCase().includes("rate limit") || fullOutput.includes("429") || fullOutput.toLowerCase().includes("too many requests");
|
|
3644
|
-
let costEstimate = estimateCostFromOutput(options.modelTier, fullOutput);
|
|
3645
|
-
const logger = getLogger();
|
|
3646
|
-
if (!costEstimate) {
|
|
3647
|
-
const fallbackEstimate = estimateCostByDuration(options.modelTier, durationMs);
|
|
3648
|
-
costEstimate = {
|
|
3649
|
-
cost: fallbackEstimate.cost * 1.5,
|
|
3650
|
-
confidence: "fallback"
|
|
3651
|
-
};
|
|
3652
|
-
logger.warn("agent", "Cost estimation fallback (duration-based)", {
|
|
3653
|
-
modelTier: options.modelTier,
|
|
3654
|
-
cost: costEstimate.cost
|
|
3655
|
-
});
|
|
3656
|
-
} else if (costEstimate.confidence === "estimated") {
|
|
3657
|
-
logger.warn("agent", "Cost estimation using regex parsing (estimated confidence)", { cost: costEstimate.cost });
|
|
3658
|
-
}
|
|
3659
|
-
const cost = costEstimate.cost;
|
|
3660
|
-
const actualExitCode = timedOut ? 124 : exitCode;
|
|
3661
|
-
return {
|
|
3662
|
-
success: exitCode === 0 && !timedOut,
|
|
3663
|
-
exitCode: actualExitCode,
|
|
3664
|
-
output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS),
|
|
3665
|
-
stderr: stderr.slice(-MAX_AGENT_STDERR_CHARS),
|
|
3666
|
-
rateLimited,
|
|
3667
|
-
durationMs,
|
|
3668
|
-
estimatedCost: cost,
|
|
3669
|
-
pid: processPid
|
|
3670
|
-
};
|
|
3671
|
-
}
|
|
3672
|
-
async plan(options) {
|
|
3673
|
-
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
3674
|
-
return runPlan(this.binary, options, pidRegistry, this.buildAllowedEnv.bind(this));
|
|
3675
|
-
}
|
|
3676
|
-
async decompose(options) {
|
|
3677
|
-
const prompt = buildDecomposePrompt(options);
|
|
3678
|
-
const cmd = [
|
|
3679
|
-
this.binary,
|
|
3680
|
-
"--model",
|
|
3681
|
-
options.modelDef?.model || "claude-sonnet-4-5",
|
|
3682
|
-
"--dangerously-skip-permissions",
|
|
3683
|
-
"-p",
|
|
3684
|
-
prompt
|
|
3685
|
-
];
|
|
3686
|
-
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
3687
|
-
const proc = Bun.spawn(cmd, {
|
|
3688
|
-
cwd: options.workdir,
|
|
3689
|
-
stdout: "pipe",
|
|
3690
|
-
stderr: "inherit",
|
|
3691
|
-
env: this.buildAllowedEnv({
|
|
3692
|
-
workdir: options.workdir,
|
|
3693
|
-
modelDef: options.modelDef || { provider: "anthropic", model: "claude-sonnet-4-5", env: {} },
|
|
3694
|
-
prompt: "",
|
|
3695
|
-
modelTier: "balanced",
|
|
3696
|
-
timeoutSeconds: 600
|
|
3697
|
-
})
|
|
3698
|
-
});
|
|
3699
|
-
await pidRegistry.register(proc.pid);
|
|
3700
|
-
const DECOMPOSE_TIMEOUT_MS = 300000;
|
|
3701
|
-
let timedOut = false;
|
|
3702
|
-
const decomposeTimerId = setTimeout(() => {
|
|
3703
|
-
timedOut = true;
|
|
3704
|
-
try {
|
|
3705
|
-
proc.kill("SIGTERM");
|
|
3706
|
-
} catch {}
|
|
3707
|
-
setTimeout(() => {
|
|
3708
|
-
try {
|
|
3709
|
-
proc.kill("SIGKILL");
|
|
3710
|
-
} catch {}
|
|
3711
|
-
}, 5000);
|
|
3712
|
-
}, DECOMPOSE_TIMEOUT_MS);
|
|
3713
|
-
let exitCode;
|
|
3714
|
-
try {
|
|
3715
|
-
exitCode = await proc.exited;
|
|
3716
|
-
} finally {
|
|
3717
|
-
clearTimeout(decomposeTimerId);
|
|
3718
|
-
await pidRegistry.unregister(proc.pid);
|
|
3719
|
-
}
|
|
3720
|
-
if (timedOut) {
|
|
3721
|
-
throw new Error(`Decompose timed out after ${DECOMPOSE_TIMEOUT_MS / 1000}s`);
|
|
3722
|
-
}
|
|
3723
|
-
const stdout = await Promise.race([
|
|
3724
|
-
new Response(proc.stdout).text(),
|
|
3725
|
-
new Promise((resolve) => setTimeout(() => resolve(""), 5000))
|
|
3726
|
-
]);
|
|
3727
|
-
const stderr = await new Response(proc.stderr).text();
|
|
3728
|
-
if (exitCode !== 0) {
|
|
3729
|
-
throw new Error(`Decompose failed with exit code ${exitCode}: ${stderr}`);
|
|
3730
|
-
}
|
|
3731
|
-
const stories = parseDecomposeOutput(stdout);
|
|
3732
|
-
return { stories };
|
|
3733
|
-
}
|
|
3734
|
-
runInteractive(options) {
|
|
3735
|
-
const model = options.modelDef.model;
|
|
3736
|
-
const cmd = [this.binary, "--model", model, options.prompt];
|
|
3737
|
-
const proc = Bun.spawn(cmd, {
|
|
3738
|
-
cwd: options.workdir,
|
|
3739
|
-
env: { ...this.buildAllowedEnv(options), TERM: "xterm-256color", FORCE_COLOR: "1" },
|
|
3740
|
-
stdin: "pipe",
|
|
3741
|
-
stdout: "pipe",
|
|
3742
|
-
stderr: "inherit"
|
|
3743
|
-
});
|
|
3744
|
-
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
3745
|
-
pidRegistry.register(proc.pid).catch(() => {});
|
|
3746
|
-
(async () => {
|
|
3747
|
-
try {
|
|
3748
|
-
for await (const chunk of proc.stdout) {
|
|
3749
|
-
options.onOutput(Buffer.from(chunk));
|
|
3750
|
-
}
|
|
3751
|
-
} catch (err) {
|
|
3752
|
-
getLogger()?.error("agent", "runInteractive stdout error", { err });
|
|
3753
|
-
}
|
|
3754
|
-
})();
|
|
3755
|
-
proc.exited.then((code) => {
|
|
3756
|
-
pidRegistry.unregister(proc.pid).catch(() => {});
|
|
3757
|
-
options.onExit(code ?? 1);
|
|
3758
|
-
}).catch((err) => {
|
|
3759
|
-
getLogger()?.error("agent", "runInteractive exit error", { err });
|
|
3760
|
-
});
|
|
3761
|
-
return {
|
|
3762
|
-
write: (data) => {
|
|
3763
|
-
proc.stdin.write(data);
|
|
3764
|
-
},
|
|
3765
|
-
resize: (_cols, _rows) => {},
|
|
3766
|
-
kill: () => {
|
|
3767
|
-
proc.kill();
|
|
3768
|
-
},
|
|
3769
|
-
pid: proc.pid
|
|
3770
|
-
};
|
|
3771
|
-
}
|
|
3772
|
-
}
|
|
3773
|
-
var MAX_AGENT_OUTPUT_CHARS = 5000, MAX_AGENT_STDERR_CHARS = 1000, SIGKILL_GRACE_PERIOD_MS = 5000, _runOnceDeps;
|
|
3774
|
-
var init_claude = __esm(() => {
|
|
3775
|
-
init_pid_registry();
|
|
3776
|
-
init_logger2();
|
|
3777
|
-
init_claude_plan();
|
|
3778
|
-
init_cost();
|
|
3779
|
-
_runOnceDeps = {
|
|
3780
|
-
killProc(proc, signal) {
|
|
3781
|
-
proc.kill(signal);
|
|
3782
|
-
}
|
|
3783
|
-
};
|
|
3784
|
-
});
|
|
3785
|
-
|
|
3786
|
-
// src/agents/registry.ts
|
|
3787
|
-
function getAgent(name) {
|
|
3788
|
-
return ALL_AGENTS.find((a) => a.name === name);
|
|
3789
|
-
}
|
|
3790
|
-
async function checkAgentHealth() {
|
|
3791
|
-
return Promise.all(ALL_AGENTS.map(async (agent) => ({
|
|
3792
|
-
name: agent.name,
|
|
3793
|
-
displayName: agent.displayName,
|
|
3794
|
-
installed: await agent.isInstalled()
|
|
3795
|
-
})));
|
|
3796
|
-
}
|
|
3797
|
-
var ALL_AGENTS;
|
|
3798
|
-
var init_registry = __esm(() => {
|
|
3799
|
-
init_claude();
|
|
3800
|
-
ALL_AGENTS = [
|
|
3801
|
-
new ClaudeCodeAdapter
|
|
3802
|
-
];
|
|
3803
|
-
});
|
|
3804
|
-
|
|
3805
|
-
// src/agents/validation.ts
|
|
3806
|
-
function validateAgentForTier(agent, tier) {
|
|
3807
|
-
return agent.capabilities.supportedTiers.includes(tier);
|
|
3808
|
-
}
|
|
3809
|
-
|
|
3810
|
-
// src/agents/index.ts
|
|
3811
|
-
var init_agents = __esm(() => {
|
|
3812
|
-
init_claude();
|
|
3813
|
-
init_registry();
|
|
3814
|
-
init_cost();
|
|
3815
|
-
});
|
|
3816
|
-
|
|
3817
3027
|
// src/acceptance/generator.ts
|
|
3818
3028
|
function parseAcceptanceCriteria(specContent) {
|
|
3819
3029
|
const criteria = [];
|
|
@@ -4117,6 +3327,614 @@ var init_acceptance = __esm(() => {
|
|
|
4117
3327
|
init_fix_generator();
|
|
4118
3328
|
});
|
|
4119
3329
|
|
|
3330
|
+
// src/agents/types.ts
|
|
3331
|
+
var CompleteError;
|
|
3332
|
+
var init_types2 = __esm(() => {
|
|
3333
|
+
CompleteError = class CompleteError extends Error {
|
|
3334
|
+
exitCode;
|
|
3335
|
+
constructor(message, exitCode) {
|
|
3336
|
+
super(message);
|
|
3337
|
+
this.exitCode = exitCode;
|
|
3338
|
+
this.name = "CompleteError";
|
|
3339
|
+
}
|
|
3340
|
+
};
|
|
3341
|
+
});
|
|
3342
|
+
|
|
3343
|
+
// src/agents/adapters/aider.ts
|
|
3344
|
+
class AiderAdapter {
|
|
3345
|
+
name = "aider";
|
|
3346
|
+
displayName = "Aider";
|
|
3347
|
+
binary = "aider";
|
|
3348
|
+
capabilities = {
|
|
3349
|
+
supportedTiers: ["balanced"],
|
|
3350
|
+
maxContextTokens: 20000,
|
|
3351
|
+
features: new Set(["refactor"])
|
|
3352
|
+
};
|
|
3353
|
+
async isInstalled() {
|
|
3354
|
+
const path = _aiderCompleteDeps.which("aider");
|
|
3355
|
+
return path !== null;
|
|
3356
|
+
}
|
|
3357
|
+
buildCommand(options) {
|
|
3358
|
+
return ["aider", "--message", options.prompt, "--yes"];
|
|
3359
|
+
}
|
|
3360
|
+
async run(options) {
|
|
3361
|
+
const cmd = this.buildCommand(options);
|
|
3362
|
+
const startTime = Date.now();
|
|
3363
|
+
const proc = _aiderCompleteDeps.spawn(cmd, {
|
|
3364
|
+
stdout: "pipe",
|
|
3365
|
+
stderr: "inherit"
|
|
3366
|
+
});
|
|
3367
|
+
const exitCode = await proc.exited;
|
|
3368
|
+
const stdout = await new Response(proc.stdout).text();
|
|
3369
|
+
const durationMs = Date.now() - startTime;
|
|
3370
|
+
return {
|
|
3371
|
+
success: exitCode === 0,
|
|
3372
|
+
exitCode,
|
|
3373
|
+
output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS),
|
|
3374
|
+
rateLimited: false,
|
|
3375
|
+
durationMs,
|
|
3376
|
+
estimatedCost: 0,
|
|
3377
|
+
pid: proc.pid
|
|
3378
|
+
};
|
|
3379
|
+
}
|
|
3380
|
+
async complete(prompt, options) {
|
|
3381
|
+
const cmd = ["aider", "--message", prompt, "--yes"];
|
|
3382
|
+
if (options?.model) {
|
|
3383
|
+
cmd.push("--model", options.model);
|
|
3384
|
+
}
|
|
3385
|
+
const proc = _aiderCompleteDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
3386
|
+
const exitCode = await proc.exited;
|
|
3387
|
+
const stdout = await new Response(proc.stdout).text();
|
|
3388
|
+
const stderr = await new Response(proc.stderr).text();
|
|
3389
|
+
const trimmed = stdout.trim();
|
|
3390
|
+
if (exitCode !== 0) {
|
|
3391
|
+
const errorDetails = stderr.trim() || trimmed;
|
|
3392
|
+
const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
|
|
3393
|
+
throw new CompleteError(errorMessage, exitCode);
|
|
3394
|
+
}
|
|
3395
|
+
if (!trimmed) {
|
|
3396
|
+
throw new CompleteError("complete() returned empty output");
|
|
3397
|
+
}
|
|
3398
|
+
return trimmed;
|
|
3399
|
+
}
|
|
3400
|
+
async plan(_options) {
|
|
3401
|
+
throw new Error("AiderAdapter.plan() not implemented");
|
|
3402
|
+
}
|
|
3403
|
+
async decompose(_options) {
|
|
3404
|
+
throw new Error("AiderAdapter.decompose() not implemented");
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
var _aiderCompleteDeps, MAX_AGENT_OUTPUT_CHARS = 5000;
|
|
3408
|
+
var init_aider = __esm(() => {
|
|
3409
|
+
init_types2();
|
|
3410
|
+
_aiderCompleteDeps = {
|
|
3411
|
+
which(name) {
|
|
3412
|
+
return Bun.which(name);
|
|
3413
|
+
},
|
|
3414
|
+
spawn(cmd, opts) {
|
|
3415
|
+
return Bun.spawn(cmd, opts);
|
|
3416
|
+
}
|
|
3417
|
+
};
|
|
3418
|
+
});
|
|
3419
|
+
|
|
3420
|
+
// src/agents/adapters/codex.ts
|
|
3421
|
+
class CodexAdapter {
|
|
3422
|
+
name = "codex";
|
|
3423
|
+
displayName = "Codex";
|
|
3424
|
+
binary = "codex";
|
|
3425
|
+
capabilities = {
|
|
3426
|
+
supportedTiers: ["fast", "balanced"],
|
|
3427
|
+
maxContextTokens: 8000,
|
|
3428
|
+
features: new Set(["tdd", "refactor"])
|
|
3429
|
+
};
|
|
3430
|
+
async isInstalled() {
|
|
3431
|
+
const path = _codexRunDeps.which("codex");
|
|
3432
|
+
return path !== null;
|
|
3433
|
+
}
|
|
3434
|
+
buildCommand(options) {
|
|
3435
|
+
return ["codex", "-q", "--prompt", options.prompt];
|
|
3436
|
+
}
|
|
3437
|
+
async run(options) {
|
|
3438
|
+
const cmd = this.buildCommand(options);
|
|
3439
|
+
const startTime = Date.now();
|
|
3440
|
+
const proc = _codexRunDeps.spawn(cmd, {
|
|
3441
|
+
cwd: options.workdir,
|
|
3442
|
+
stdout: "pipe",
|
|
3443
|
+
stderr: "inherit"
|
|
3444
|
+
});
|
|
3445
|
+
const exitCode = await proc.exited;
|
|
3446
|
+
const stdout = await new Response(proc.stdout).text();
|
|
3447
|
+
const durationMs = Date.now() - startTime;
|
|
3448
|
+
return {
|
|
3449
|
+
success: exitCode === 0,
|
|
3450
|
+
exitCode,
|
|
3451
|
+
output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS2),
|
|
3452
|
+
rateLimited: false,
|
|
3453
|
+
durationMs,
|
|
3454
|
+
estimatedCost: 0,
|
|
3455
|
+
pid: proc.pid
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
async complete(prompt, _options) {
|
|
3459
|
+
const cmd = ["codex", "-q", "--prompt", prompt];
|
|
3460
|
+
const proc = _codexCompleteDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
3461
|
+
const exitCode = await proc.exited;
|
|
3462
|
+
const stdout = await new Response(proc.stdout).text();
|
|
3463
|
+
const stderr = await new Response(proc.stderr).text();
|
|
3464
|
+
const trimmed = stdout.trim();
|
|
3465
|
+
if (exitCode !== 0) {
|
|
3466
|
+
const errorDetails = stderr.trim() || trimmed;
|
|
3467
|
+
const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
|
|
3468
|
+
throw new CompleteError(errorMessage, exitCode);
|
|
3469
|
+
}
|
|
3470
|
+
if (!trimmed) {
|
|
3471
|
+
throw new CompleteError("complete() returned empty output");
|
|
3472
|
+
}
|
|
3473
|
+
return trimmed;
|
|
3474
|
+
}
|
|
3475
|
+
async plan(_options) {
|
|
3476
|
+
throw new Error("CodexAdapter.plan() not implemented");
|
|
3477
|
+
}
|
|
3478
|
+
async decompose(_options) {
|
|
3479
|
+
throw new Error("CodexAdapter.decompose() not implemented");
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
var _codexRunDeps, _codexCompleteDeps, MAX_AGENT_OUTPUT_CHARS2 = 5000;
|
|
3483
|
+
var init_codex = __esm(() => {
|
|
3484
|
+
init_types2();
|
|
3485
|
+
_codexRunDeps = {
|
|
3486
|
+
which(name) {
|
|
3487
|
+
return Bun.which(name);
|
|
3488
|
+
},
|
|
3489
|
+
spawn(cmd, opts) {
|
|
3490
|
+
return Bun.spawn(cmd, opts);
|
|
3491
|
+
}
|
|
3492
|
+
};
|
|
3493
|
+
_codexCompleteDeps = {
|
|
3494
|
+
spawn(cmd, opts) {
|
|
3495
|
+
return Bun.spawn(cmd, opts);
|
|
3496
|
+
}
|
|
3497
|
+
};
|
|
3498
|
+
});
|
|
3499
|
+
|
|
3500
|
+
// src/agents/adapters/gemini.ts
|
|
3501
|
+
class GeminiAdapter {
|
|
3502
|
+
name = "gemini";
|
|
3503
|
+
displayName = "Gemini CLI";
|
|
3504
|
+
binary = "gemini";
|
|
3505
|
+
capabilities = {
|
|
3506
|
+
supportedTiers: ["fast", "balanced", "powerful"],
|
|
3507
|
+
maxContextTokens: 1e6,
|
|
3508
|
+
features: new Set(["tdd", "review", "refactor"])
|
|
3509
|
+
};
|
|
3510
|
+
async isInstalled() {
|
|
3511
|
+
const path = _geminiRunDeps.which("gemini");
|
|
3512
|
+
if (path === null) {
|
|
3513
|
+
return false;
|
|
3514
|
+
}
|
|
3515
|
+
try {
|
|
3516
|
+
const proc = _geminiRunDeps.spawn(["gemini", "--version"], {
|
|
3517
|
+
stdout: "pipe",
|
|
3518
|
+
stderr: "pipe"
|
|
3519
|
+
});
|
|
3520
|
+
const exitCode = await proc.exited;
|
|
3521
|
+
if (exitCode !== 0) {
|
|
3522
|
+
return false;
|
|
3523
|
+
}
|
|
3524
|
+
const stdout = await new Response(proc.stdout).text();
|
|
3525
|
+
const lowerOut = stdout.toLowerCase();
|
|
3526
|
+
if (lowerOut.includes("not logged in")) {
|
|
3527
|
+
return false;
|
|
3528
|
+
}
|
|
3529
|
+
return true;
|
|
3530
|
+
} catch {
|
|
3531
|
+
return false;
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
buildCommand(options) {
|
|
3535
|
+
return ["gemini", "-p", options.prompt];
|
|
3536
|
+
}
|
|
3537
|
+
async run(options) {
|
|
3538
|
+
const cmd = this.buildCommand(options);
|
|
3539
|
+
const startTime = Date.now();
|
|
3540
|
+
const proc = _geminiRunDeps.spawn(cmd, {
|
|
3541
|
+
cwd: options.workdir,
|
|
3542
|
+
stdout: "pipe",
|
|
3543
|
+
stderr: "inherit"
|
|
3544
|
+
});
|
|
3545
|
+
const exitCode = await proc.exited;
|
|
3546
|
+
const stdout = await new Response(proc.stdout).text();
|
|
3547
|
+
const durationMs = Date.now() - startTime;
|
|
3548
|
+
return {
|
|
3549
|
+
success: exitCode === 0,
|
|
3550
|
+
exitCode,
|
|
3551
|
+
output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS3),
|
|
3552
|
+
rateLimited: false,
|
|
3553
|
+
durationMs,
|
|
3554
|
+
estimatedCost: 0,
|
|
3555
|
+
pid: proc.pid
|
|
3556
|
+
};
|
|
3557
|
+
}
|
|
3558
|
+
async complete(prompt, _options) {
|
|
3559
|
+
const cmd = ["gemini", "-p", prompt];
|
|
3560
|
+
const proc = _geminiCompleteDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
3561
|
+
const exitCode = await proc.exited;
|
|
3562
|
+
const stdout = await new Response(proc.stdout).text();
|
|
3563
|
+
const stderr = await new Response(proc.stderr).text();
|
|
3564
|
+
const trimmed = stdout.trim();
|
|
3565
|
+
if (exitCode !== 0) {
|
|
3566
|
+
const errorDetails = stderr.trim() || trimmed;
|
|
3567
|
+
const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
|
|
3568
|
+
throw new CompleteError(errorMessage, exitCode);
|
|
3569
|
+
}
|
|
3570
|
+
if (!trimmed) {
|
|
3571
|
+
throw new CompleteError("complete() returned empty output");
|
|
3572
|
+
}
|
|
3573
|
+
return trimmed;
|
|
3574
|
+
}
|
|
3575
|
+
async plan(_options) {
|
|
3576
|
+
throw new Error("GeminiAdapter.plan() not implemented");
|
|
3577
|
+
}
|
|
3578
|
+
async decompose(_options) {
|
|
3579
|
+
throw new Error("GeminiAdapter.decompose() not implemented");
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
var _geminiRunDeps, _geminiCompleteDeps, MAX_AGENT_OUTPUT_CHARS3 = 5000;
|
|
3583
|
+
var init_gemini = __esm(() => {
|
|
3584
|
+
init_types2();
|
|
3585
|
+
_geminiRunDeps = {
|
|
3586
|
+
which(name) {
|
|
3587
|
+
return Bun.which(name);
|
|
3588
|
+
},
|
|
3589
|
+
spawn(cmd, opts) {
|
|
3590
|
+
return Bun.spawn(cmd, opts);
|
|
3591
|
+
}
|
|
3592
|
+
};
|
|
3593
|
+
_geminiCompleteDeps = {
|
|
3594
|
+
spawn(cmd, opts) {
|
|
3595
|
+
return Bun.spawn(cmd, opts);
|
|
3596
|
+
}
|
|
3597
|
+
};
|
|
3598
|
+
});
|
|
3599
|
+
|
|
3600
|
+
// src/agents/adapters/opencode.ts
|
|
3601
|
+
class OpenCodeAdapter {
|
|
3602
|
+
name = "opencode";
|
|
3603
|
+
displayName = "OpenCode";
|
|
3604
|
+
binary = "opencode";
|
|
3605
|
+
capabilities = {
|
|
3606
|
+
supportedTiers: ["fast", "balanced"],
|
|
3607
|
+
maxContextTokens: 8000,
|
|
3608
|
+
features: new Set(["tdd", "refactor"])
|
|
3609
|
+
};
|
|
3610
|
+
async isInstalled() {
|
|
3611
|
+
const path = _opencodeCompleteDeps.which("opencode");
|
|
3612
|
+
return path !== null;
|
|
3613
|
+
}
|
|
3614
|
+
buildCommand(_options) {
|
|
3615
|
+
throw new Error("OpenCodeAdapter.buildCommand() not implemented");
|
|
3616
|
+
}
|
|
3617
|
+
async run(_options) {
|
|
3618
|
+
throw new Error("OpenCodeAdapter.run() not implemented");
|
|
3619
|
+
}
|
|
3620
|
+
async complete(prompt, _options) {
|
|
3621
|
+
const cmd = ["opencode", "--prompt", prompt];
|
|
3622
|
+
const proc = _opencodeCompleteDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
3623
|
+
const exitCode = await proc.exited;
|
|
3624
|
+
const stdout = await new Response(proc.stdout).text();
|
|
3625
|
+
const stderr = await new Response(proc.stderr).text();
|
|
3626
|
+
const trimmed = stdout.trim();
|
|
3627
|
+
if (exitCode !== 0) {
|
|
3628
|
+
const errorDetails = stderr.trim() || trimmed;
|
|
3629
|
+
const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
|
|
3630
|
+
throw new CompleteError(errorMessage, exitCode);
|
|
3631
|
+
}
|
|
3632
|
+
if (!trimmed) {
|
|
3633
|
+
throw new CompleteError("complete() returned empty output");
|
|
3634
|
+
}
|
|
3635
|
+
return trimmed;
|
|
3636
|
+
}
|
|
3637
|
+
async plan(_options) {
|
|
3638
|
+
throw new Error("OpenCodeAdapter.plan() not implemented");
|
|
3639
|
+
}
|
|
3640
|
+
async decompose(_options) {
|
|
3641
|
+
throw new Error("OpenCodeAdapter.decompose() not implemented");
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
var _opencodeCompleteDeps;
|
|
3645
|
+
var init_opencode = __esm(() => {
|
|
3646
|
+
init_types2();
|
|
3647
|
+
_opencodeCompleteDeps = {
|
|
3648
|
+
which(name) {
|
|
3649
|
+
return Bun.which(name);
|
|
3650
|
+
},
|
|
3651
|
+
spawn(cmd, opts) {
|
|
3652
|
+
return Bun.spawn(cmd, opts);
|
|
3653
|
+
}
|
|
3654
|
+
};
|
|
3655
|
+
});
|
|
3656
|
+
|
|
3657
|
+
// src/execution/pid-registry.ts
|
|
3658
|
+
import { existsSync } from "fs";
|
|
3659
|
+
|
|
3660
|
+
class PidRegistry {
|
|
3661
|
+
workdir;
|
|
3662
|
+
pidsFilePath;
|
|
3663
|
+
pids = new Set;
|
|
3664
|
+
platform;
|
|
3665
|
+
constructor(workdir, platform) {
|
|
3666
|
+
this.workdir = workdir;
|
|
3667
|
+
this.pidsFilePath = `${workdir}/${PID_REGISTRY_FILE}`;
|
|
3668
|
+
this.platform = platform ?? process.platform;
|
|
3669
|
+
}
|
|
3670
|
+
async register(pid) {
|
|
3671
|
+
const logger = getSafeLogger();
|
|
3672
|
+
this.pids.add(pid);
|
|
3673
|
+
const entry = {
|
|
3674
|
+
pid,
|
|
3675
|
+
spawnedAt: new Date().toISOString(),
|
|
3676
|
+
workdir: this.workdir
|
|
3677
|
+
};
|
|
3678
|
+
try {
|
|
3679
|
+
let existingContent = "";
|
|
3680
|
+
if (existsSync(this.pidsFilePath)) {
|
|
3681
|
+
existingContent = await Bun.file(this.pidsFilePath).text();
|
|
3682
|
+
}
|
|
3683
|
+
const line = `${JSON.stringify(entry)}
|
|
3684
|
+
`;
|
|
3685
|
+
await Bun.write(this.pidsFilePath, existingContent + line);
|
|
3686
|
+
logger?.debug("pid-registry", `Registered PID ${pid}`, { pid });
|
|
3687
|
+
} catch (err) {
|
|
3688
|
+
logger?.warn("pid-registry", `Failed to write PID ${pid} to registry`, {
|
|
3689
|
+
error: err.message
|
|
3690
|
+
});
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
async unregister(pid) {
|
|
3694
|
+
const logger = getSafeLogger();
|
|
3695
|
+
this.pids.delete(pid);
|
|
3696
|
+
try {
|
|
3697
|
+
await this.writePidsFile();
|
|
3698
|
+
logger?.debug("pid-registry", `Unregistered PID ${pid}`, { pid });
|
|
3699
|
+
} catch (err) {
|
|
3700
|
+
logger?.warn("pid-registry", `Failed to unregister PID ${pid}`, {
|
|
3701
|
+
error: err.message
|
|
3702
|
+
});
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
async killAll() {
|
|
3706
|
+
const logger = getSafeLogger();
|
|
3707
|
+
const pids = Array.from(this.pids);
|
|
3708
|
+
if (pids.length === 0) {
|
|
3709
|
+
logger?.debug("pid-registry", "No PIDs to kill");
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
logger?.info("pid-registry", `Killing ${pids.length} registered processes`, { pids });
|
|
3713
|
+
const killPromises = pids.map((pid) => this.killPid(pid));
|
|
3714
|
+
await Promise.allSettled(killPromises);
|
|
3715
|
+
try {
|
|
3716
|
+
await Bun.write(this.pidsFilePath, "");
|
|
3717
|
+
this.pids.clear();
|
|
3718
|
+
logger?.info("pid-registry", "All registered PIDs killed and registry cleared");
|
|
3719
|
+
} catch (err) {
|
|
3720
|
+
logger?.warn("pid-registry", "Failed to clear registry file", {
|
|
3721
|
+
error: err.message
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
async cleanupStale() {
|
|
3726
|
+
const logger = getSafeLogger();
|
|
3727
|
+
if (!existsSync(this.pidsFilePath)) {
|
|
3728
|
+
logger?.debug("pid-registry", "No stale PIDs file found");
|
|
3729
|
+
return;
|
|
3730
|
+
}
|
|
3731
|
+
try {
|
|
3732
|
+
const content = await Bun.file(this.pidsFilePath).text();
|
|
3733
|
+
const lines = content.split(`
|
|
3734
|
+
`).filter((line) => line.trim()).map((line) => {
|
|
3735
|
+
try {
|
|
3736
|
+
return JSON.parse(line);
|
|
3737
|
+
} catch {
|
|
3738
|
+
return null;
|
|
3739
|
+
}
|
|
3740
|
+
}).filter((entry) => entry !== null);
|
|
3741
|
+
if (lines.length === 0) {
|
|
3742
|
+
logger?.debug("pid-registry", "No stale PIDs to cleanup");
|
|
3743
|
+
await Bun.write(this.pidsFilePath, "");
|
|
3744
|
+
return;
|
|
3745
|
+
}
|
|
3746
|
+
const stalePids = lines.map((entry) => entry.pid);
|
|
3747
|
+
logger?.info("pid-registry", `Cleaning up ${stalePids.length} stale PIDs from previous run`, {
|
|
3748
|
+
pids: stalePids
|
|
3749
|
+
});
|
|
3750
|
+
const killPromises = stalePids.map((pid) => this.killPid(pid));
|
|
3751
|
+
await Promise.allSettled(killPromises);
|
|
3752
|
+
await Bun.write(this.pidsFilePath, "");
|
|
3753
|
+
logger?.info("pid-registry", "Stale PIDs cleanup completed");
|
|
3754
|
+
} catch (err) {
|
|
3755
|
+
logger?.warn("pid-registry", "Failed to cleanup stale PIDs", {
|
|
3756
|
+
error: err.message
|
|
3757
|
+
});
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
async killPid(pid) {
|
|
3761
|
+
const logger = getSafeLogger();
|
|
3762
|
+
try {
|
|
3763
|
+
const checkProc = Bun.spawn(["kill", "-0", String(pid)], {
|
|
3764
|
+
stdout: "pipe",
|
|
3765
|
+
stderr: "pipe"
|
|
3766
|
+
});
|
|
3767
|
+
const checkCode = await checkProc.exited;
|
|
3768
|
+
if (checkCode !== 0) {
|
|
3769
|
+
logger?.debug("pid-registry", `PID ${pid} not found (already exited)`, { pid });
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3772
|
+
const killArgs = this.platform === "linux" ? ["kill", "-TERM", `-${pid}`] : ["kill", "-TERM", String(pid)];
|
|
3773
|
+
const killProc = Bun.spawn(killArgs, {
|
|
3774
|
+
stdout: "pipe",
|
|
3775
|
+
stderr: "pipe"
|
|
3776
|
+
});
|
|
3777
|
+
const killCode = await killProc.exited;
|
|
3778
|
+
if (killCode === 0) {
|
|
3779
|
+
logger?.debug("pid-registry", `Killed PID ${pid}`, { pid });
|
|
3780
|
+
} else {
|
|
3781
|
+
const stderr = await new Response(killProc.stderr).text();
|
|
3782
|
+
logger?.warn("pid-registry", `Failed to kill PID ${pid}`, {
|
|
3783
|
+
pid,
|
|
3784
|
+
exitCode: killCode,
|
|
3785
|
+
stderr: stderr.trim()
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
} catch (err) {
|
|
3789
|
+
logger?.warn("pid-registry", `Error killing PID ${pid}`, {
|
|
3790
|
+
pid,
|
|
3791
|
+
error: err.message
|
|
3792
|
+
});
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
async writePidsFile() {
|
|
3796
|
+
const entries = Array.from(this.pids).map((pid) => ({
|
|
3797
|
+
pid,
|
|
3798
|
+
spawnedAt: new Date().toISOString(),
|
|
3799
|
+
workdir: this.workdir
|
|
3800
|
+
}));
|
|
3801
|
+
const content = entries.map((entry) => JSON.stringify(entry)).join(`
|
|
3802
|
+
`);
|
|
3803
|
+
await Bun.write(this.pidsFilePath, content ? `${content}
|
|
3804
|
+
` : "");
|
|
3805
|
+
}
|
|
3806
|
+
getPids() {
|
|
3807
|
+
return Array.from(this.pids);
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
var PID_REGISTRY_FILE = ".nax-pids";
|
|
3811
|
+
var init_pid_registry = __esm(() => {
|
|
3812
|
+
init_logger2();
|
|
3813
|
+
});
|
|
3814
|
+
|
|
3815
|
+
// src/agents/claude-decompose.ts
|
|
3816
|
+
function buildDecomposePrompt(options) {
|
|
3817
|
+
return `You are a requirements analyst. Break down the following feature specification into user stories and classify each story's complexity.
|
|
3818
|
+
|
|
3819
|
+
CODEBASE CONTEXT:
|
|
3820
|
+
${options.codebaseContext}
|
|
3821
|
+
|
|
3822
|
+
FEATURE SPECIFICATION:
|
|
3823
|
+
${options.specContent}
|
|
3824
|
+
|
|
3825
|
+
Decompose this spec into user stories. For each story, provide:
|
|
3826
|
+
1. id: Story ID (e.g., "US-001")
|
|
3827
|
+
2. title: Concise story title
|
|
3828
|
+
3. description: What needs to be implemented
|
|
3829
|
+
4. acceptanceCriteria: Array of testable criteria
|
|
3830
|
+
5. tags: Array of routing tags (e.g., ["security", "api"])
|
|
3831
|
+
6. dependencies: Array of story IDs this depends on (e.g., ["US-001"])
|
|
3832
|
+
7. complexity: "simple" | "medium" | "complex" | "expert"
|
|
3833
|
+
8. contextFiles: Array of file paths to inject into agent prompt before execution
|
|
3834
|
+
9. reasoning: Why this complexity level
|
|
3835
|
+
10. estimatedLOC: Estimated lines of code to change
|
|
3836
|
+
11. risks: Array of implementation risks
|
|
3837
|
+
12. testStrategy: "three-session-tdd" | "test-after"
|
|
3838
|
+
|
|
3839
|
+
testStrategy rules:
|
|
3840
|
+
- "three-session-tdd": ONLY for complex/expert tasks that are security-critical (auth, encryption, tokens, credentials) or define public API contracts consumers depend on
|
|
3841
|
+
- "test-after": for all other tasks including simple/medium complexity
|
|
3842
|
+
- A "simple" complexity task should almost never be "three-session-tdd"
|
|
3843
|
+
|
|
3844
|
+
Complexity classification rules:
|
|
3845
|
+
- simple: 1-3 files, <100 LOC, straightforward implementation, existing patterns
|
|
3846
|
+
- medium: 3-6 files, 100-300 LOC, moderate logic, some new patterns
|
|
3847
|
+
- complex: 6+ files, 300-800 LOC, architectural changes, cross-cutting concerns
|
|
3848
|
+
- expert: Security/crypto/real-time/distributed systems, >800 LOC, new infrastructure
|
|
3849
|
+
|
|
3850
|
+
Grouping Guidelines:
|
|
3851
|
+
- Combine small, related tasks (e.g., multiple utility functions, interfaces) into a single "simple" or "medium" story.
|
|
3852
|
+
- Do NOT create separate stories for every single file or function unless complex.
|
|
3853
|
+
- Aim for coherent units of value (e.g., "Implement User Authentication" vs "Create User Interface", "Create Login Service").
|
|
3854
|
+
- Maximum recommended stories: 10-15 per feature. Group aggressively if list grows too long.
|
|
3855
|
+
|
|
3856
|
+
Consider:
|
|
3857
|
+
1. Does infrastructure exist? (e.g., "add caching" when no cache layer exists = complex)
|
|
3858
|
+
2. How many files will be touched?
|
|
3859
|
+
3. Are there cross-cutting concerns (auth, validation, error handling)?
|
|
3860
|
+
4. Does it require new dependencies or architectural decisions?
|
|
3861
|
+
|
|
3862
|
+
Respond with ONLY a JSON array (no markdown code fences):
|
|
3863
|
+
[{
|
|
3864
|
+
"id": "US-001",
|
|
3865
|
+
"title": "Story title",
|
|
3866
|
+
"description": "Story description",
|
|
3867
|
+
"acceptanceCriteria": ["Criterion 1", "Criterion 2"],
|
|
3868
|
+
"tags": ["tag1"],
|
|
3869
|
+
"dependencies": [],
|
|
3870
|
+
"complexity": "medium",
|
|
3871
|
+
"contextFiles": ["src/path/to/file.ts"],
|
|
3872
|
+
"reasoning": "Why this complexity level",
|
|
3873
|
+
"estimatedLOC": 150,
|
|
3874
|
+
"risks": ["Risk 1"],
|
|
3875
|
+
"testStrategy": "test-after"
|
|
3876
|
+
}]`;
|
|
3877
|
+
}
|
|
3878
|
+
function parseDecomposeOutput(output) {
|
|
3879
|
+
const jsonMatch = output.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
|
|
3880
|
+
let jsonText = jsonMatch ? jsonMatch[1] : output;
|
|
3881
|
+
if (!jsonMatch) {
|
|
3882
|
+
const arrayMatch = output.match(/\[[\s\S]*\]/);
|
|
3883
|
+
if (arrayMatch) {
|
|
3884
|
+
jsonText = arrayMatch[0];
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
let parsed;
|
|
3888
|
+
try {
|
|
3889
|
+
parsed = JSON.parse(jsonText.trim());
|
|
3890
|
+
} catch (error) {
|
|
3891
|
+
throw new Error(`Failed to parse decompose output as JSON: ${error.message}
|
|
3892
|
+
|
|
3893
|
+
Output:
|
|
3894
|
+
${output.slice(0, 500)}`);
|
|
3895
|
+
}
|
|
3896
|
+
if (!Array.isArray(parsed)) {
|
|
3897
|
+
throw new Error("Decompose output is not an array");
|
|
3898
|
+
}
|
|
3899
|
+
const stories = parsed.map((item, index) => {
|
|
3900
|
+
if (typeof item !== "object" || item === null) {
|
|
3901
|
+
throw new Error(`Story at index ${index} is not an object`);
|
|
3902
|
+
}
|
|
3903
|
+
const record = item;
|
|
3904
|
+
if (!record.id || typeof record.id !== "string") {
|
|
3905
|
+
throw new Error(`Story at index ${index} missing valid 'id' field`);
|
|
3906
|
+
}
|
|
3907
|
+
if (!record.title || typeof record.title !== "string") {
|
|
3908
|
+
throw new Error(`Story ${record.id} missing valid 'title' field`);
|
|
3909
|
+
}
|
|
3910
|
+
return {
|
|
3911
|
+
id: record.id,
|
|
3912
|
+
title: record.title,
|
|
3913
|
+
description: String(record.description || record.title),
|
|
3914
|
+
acceptanceCriteria: Array.isArray(record.acceptanceCriteria) ? record.acceptanceCriteria : ["Implementation complete"],
|
|
3915
|
+
tags: Array.isArray(record.tags) ? record.tags : [],
|
|
3916
|
+
dependencies: Array.isArray(record.dependencies) ? record.dependencies : [],
|
|
3917
|
+
complexity: validateComplexity(record.complexity),
|
|
3918
|
+
contextFiles: Array.isArray(record.contextFiles) ? record.contextFiles : Array.isArray(record.relevantFiles) ? record.relevantFiles : [],
|
|
3919
|
+
relevantFiles: Array.isArray(record.relevantFiles) ? record.relevantFiles : [],
|
|
3920
|
+
reasoning: String(record.reasoning || "No reasoning provided"),
|
|
3921
|
+
estimatedLOC: Number(record.estimatedLOC) || 0,
|
|
3922
|
+
risks: Array.isArray(record.risks) ? record.risks : [],
|
|
3923
|
+
testStrategy: record.testStrategy === "three-session-tdd" ? "three-session-tdd" : record.testStrategy === "test-after" ? "test-after" : undefined
|
|
3924
|
+
};
|
|
3925
|
+
});
|
|
3926
|
+
if (stories.length === 0) {
|
|
3927
|
+
throw new Error("Decompose returned empty story array");
|
|
3928
|
+
}
|
|
3929
|
+
return stories;
|
|
3930
|
+
}
|
|
3931
|
+
function validateComplexity(value) {
|
|
3932
|
+
if (value === "simple" || value === "medium" || value === "complex" || value === "expert") {
|
|
3933
|
+
return value;
|
|
3934
|
+
}
|
|
3935
|
+
return "medium";
|
|
3936
|
+
}
|
|
3937
|
+
|
|
4120
3938
|
// src/config/types.ts
|
|
4121
3939
|
function resolveModel(entry) {
|
|
4122
3940
|
if (typeof entry === "string") {
|
|
@@ -18266,179 +18084,774 @@ var init_schemas3 = __esm(() => {
|
|
|
18266
18084
|
});
|
|
18267
18085
|
});
|
|
18268
18086
|
|
|
18269
|
-
// src/config/defaults.ts
|
|
18270
|
-
var DEFAULT_CONFIG;
|
|
18271
|
-
var init_defaults = __esm(() => {
|
|
18272
|
-
DEFAULT_CONFIG = {
|
|
18273
|
-
version: 1,
|
|
18274
|
-
models: {
|
|
18275
|
-
fast: { provider: "anthropic", model: "haiku" },
|
|
18276
|
-
balanced: { provider: "anthropic", model: "sonnet" },
|
|
18277
|
-
powerful: { provider: "anthropic", model: "opus" }
|
|
18278
|
-
},
|
|
18279
|
-
autoMode: {
|
|
18280
|
-
enabled: true,
|
|
18281
|
-
defaultAgent: "claude",
|
|
18282
|
-
fallbackOrder: ["claude", "codex", "opencode", "gemini"],
|
|
18283
|
-
complexityRouting: {
|
|
18284
|
-
simple: "fast",
|
|
18285
|
-
medium: "balanced",
|
|
18286
|
-
complex: "powerful",
|
|
18287
|
-
expert: "powerful"
|
|
18288
|
-
},
|
|
18289
|
-
escalation: {
|
|
18290
|
-
enabled: true,
|
|
18291
|
-
tierOrder: [
|
|
18292
|
-
{ tier: "fast", attempts: 5 },
|
|
18293
|
-
{ tier: "balanced", attempts: 3 },
|
|
18294
|
-
{ tier: "powerful", attempts: 2 }
|
|
18295
|
-
],
|
|
18296
|
-
escalateEntireBatch: true
|
|
18087
|
+
// src/config/defaults.ts
|
|
18088
|
+
var DEFAULT_CONFIG;
|
|
18089
|
+
var init_defaults = __esm(() => {
|
|
18090
|
+
DEFAULT_CONFIG = {
|
|
18091
|
+
version: 1,
|
|
18092
|
+
models: {
|
|
18093
|
+
fast: { provider: "anthropic", model: "haiku" },
|
|
18094
|
+
balanced: { provider: "anthropic", model: "sonnet" },
|
|
18095
|
+
powerful: { provider: "anthropic", model: "opus" }
|
|
18096
|
+
},
|
|
18097
|
+
autoMode: {
|
|
18098
|
+
enabled: true,
|
|
18099
|
+
defaultAgent: "claude",
|
|
18100
|
+
fallbackOrder: ["claude", "codex", "opencode", "gemini"],
|
|
18101
|
+
complexityRouting: {
|
|
18102
|
+
simple: "fast",
|
|
18103
|
+
medium: "balanced",
|
|
18104
|
+
complex: "powerful",
|
|
18105
|
+
expert: "powerful"
|
|
18106
|
+
},
|
|
18107
|
+
escalation: {
|
|
18108
|
+
enabled: true,
|
|
18109
|
+
tierOrder: [
|
|
18110
|
+
{ tier: "fast", attempts: 5 },
|
|
18111
|
+
{ tier: "balanced", attempts: 3 },
|
|
18112
|
+
{ tier: "powerful", attempts: 2 }
|
|
18113
|
+
],
|
|
18114
|
+
escalateEntireBatch: true
|
|
18115
|
+
}
|
|
18116
|
+
},
|
|
18117
|
+
routing: {
|
|
18118
|
+
strategy: "keyword",
|
|
18119
|
+
adaptive: {
|
|
18120
|
+
minSamples: 10,
|
|
18121
|
+
costThreshold: 0.8,
|
|
18122
|
+
fallbackStrategy: "llm"
|
|
18123
|
+
},
|
|
18124
|
+
llm: {
|
|
18125
|
+
model: "fast",
|
|
18126
|
+
fallbackToKeywords: true,
|
|
18127
|
+
cacheDecisions: true,
|
|
18128
|
+
mode: "hybrid",
|
|
18129
|
+
timeoutMs: 15000
|
|
18130
|
+
}
|
|
18131
|
+
},
|
|
18132
|
+
execution: {
|
|
18133
|
+
maxIterations: 10,
|
|
18134
|
+
iterationDelayMs: 2000,
|
|
18135
|
+
costLimit: 5,
|
|
18136
|
+
sessionTimeoutSeconds: 600,
|
|
18137
|
+
verificationTimeoutSeconds: 300,
|
|
18138
|
+
maxStoriesPerFeature: 500,
|
|
18139
|
+
rectification: {
|
|
18140
|
+
enabled: true,
|
|
18141
|
+
maxRetries: 2,
|
|
18142
|
+
fullSuiteTimeoutSeconds: 120,
|
|
18143
|
+
maxFailureSummaryChars: 2000,
|
|
18144
|
+
abortOnIncreasingFailures: true
|
|
18145
|
+
},
|
|
18146
|
+
regressionGate: {
|
|
18147
|
+
enabled: true,
|
|
18148
|
+
timeoutSeconds: 120,
|
|
18149
|
+
acceptOnTimeout: true,
|
|
18150
|
+
maxRectificationAttempts: 2
|
|
18151
|
+
},
|
|
18152
|
+
contextProviderTokenBudget: 2000,
|
|
18153
|
+
smartTestRunner: true
|
|
18154
|
+
},
|
|
18155
|
+
quality: {
|
|
18156
|
+
requireTypecheck: true,
|
|
18157
|
+
requireLint: true,
|
|
18158
|
+
requireTests: true,
|
|
18159
|
+
commands: {},
|
|
18160
|
+
forceExit: false,
|
|
18161
|
+
detectOpenHandles: true,
|
|
18162
|
+
detectOpenHandlesRetries: 1,
|
|
18163
|
+
gracePeriodMs: 5000,
|
|
18164
|
+
dangerouslySkipPermissions: true,
|
|
18165
|
+
drainTimeoutMs: 2000,
|
|
18166
|
+
shell: "/bin/sh",
|
|
18167
|
+
stripEnvVars: ["CLAUDECODE", "REPL_ID", "AGENT"],
|
|
18168
|
+
environmentalEscalationDivisor: 2
|
|
18169
|
+
},
|
|
18170
|
+
tdd: {
|
|
18171
|
+
maxRetries: 2,
|
|
18172
|
+
autoVerifyIsolation: true,
|
|
18173
|
+
autoApproveVerifier: true,
|
|
18174
|
+
strategy: "auto",
|
|
18175
|
+
sessionTiers: {
|
|
18176
|
+
testWriter: "balanced",
|
|
18177
|
+
verifier: "fast"
|
|
18178
|
+
},
|
|
18179
|
+
testWriterAllowedPaths: ["src/index.ts", "src/**/index.ts"],
|
|
18180
|
+
rollbackOnFailure: true,
|
|
18181
|
+
greenfieldDetection: true
|
|
18182
|
+
},
|
|
18183
|
+
constitution: {
|
|
18184
|
+
enabled: true,
|
|
18185
|
+
path: "constitution.md",
|
|
18186
|
+
maxTokens: 2000
|
|
18187
|
+
},
|
|
18188
|
+
analyze: {
|
|
18189
|
+
llmEnhanced: true,
|
|
18190
|
+
model: "balanced",
|
|
18191
|
+
fallbackToKeywords: true,
|
|
18192
|
+
maxCodebaseSummaryTokens: 5000
|
|
18193
|
+
},
|
|
18194
|
+
review: {
|
|
18195
|
+
enabled: true,
|
|
18196
|
+
checks: ["typecheck", "lint"],
|
|
18197
|
+
commands: {}
|
|
18198
|
+
},
|
|
18199
|
+
plan: {
|
|
18200
|
+
model: "balanced",
|
|
18201
|
+
outputPath: "spec.md"
|
|
18202
|
+
},
|
|
18203
|
+
acceptance: {
|
|
18204
|
+
enabled: true,
|
|
18205
|
+
maxRetries: 2,
|
|
18206
|
+
generateTests: true,
|
|
18207
|
+
testPath: "acceptance.test.ts"
|
|
18208
|
+
},
|
|
18209
|
+
context: {
|
|
18210
|
+
fileInjection: "disabled",
|
|
18211
|
+
testCoverage: {
|
|
18212
|
+
enabled: true,
|
|
18213
|
+
detail: "names-and-counts",
|
|
18214
|
+
maxTokens: 500,
|
|
18215
|
+
testPattern: "**/*.test.{ts,js,tsx,jsx}",
|
|
18216
|
+
scopeToStory: true
|
|
18217
|
+
},
|
|
18218
|
+
autoDetect: {
|
|
18219
|
+
enabled: true,
|
|
18220
|
+
maxFiles: 5,
|
|
18221
|
+
traceImports: false
|
|
18222
|
+
}
|
|
18223
|
+
},
|
|
18224
|
+
interaction: {
|
|
18225
|
+
plugin: "cli",
|
|
18226
|
+
config: {},
|
|
18227
|
+
defaults: {
|
|
18228
|
+
timeout: 600000,
|
|
18229
|
+
fallback: "escalate"
|
|
18230
|
+
},
|
|
18231
|
+
triggers: {
|
|
18232
|
+
"security-review": true,
|
|
18233
|
+
"cost-warning": true
|
|
18234
|
+
}
|
|
18235
|
+
},
|
|
18236
|
+
precheck: {
|
|
18237
|
+
storySizeGate: {
|
|
18238
|
+
enabled: true,
|
|
18239
|
+
maxAcCount: 6,
|
|
18240
|
+
maxDescriptionLength: 2000,
|
|
18241
|
+
maxBulletPoints: 8
|
|
18242
|
+
}
|
|
18243
|
+
},
|
|
18244
|
+
prompts: {},
|
|
18245
|
+
decompose: {
|
|
18246
|
+
trigger: "auto",
|
|
18247
|
+
maxAcceptanceCriteria: 6,
|
|
18248
|
+
maxSubstories: 5,
|
|
18249
|
+
maxSubstoryComplexity: "medium",
|
|
18250
|
+
maxRetries: 2,
|
|
18251
|
+
model: "balanced"
|
|
18252
|
+
}
|
|
18253
|
+
};
|
|
18254
|
+
});
|
|
18255
|
+
|
|
18256
|
+
// src/config/schema.ts
|
|
18257
|
+
var init_schema = __esm(() => {
|
|
18258
|
+
init_schemas3();
|
|
18259
|
+
init_defaults();
|
|
18260
|
+
});
|
|
18261
|
+
|
|
18262
|
+
// src/agents/model-resolution.ts
|
|
18263
|
+
var exports_model_resolution = {};
|
|
18264
|
+
__export(exports_model_resolution, {
|
|
18265
|
+
resolveBalancedModelDef: () => resolveBalancedModelDef
|
|
18266
|
+
});
|
|
18267
|
+
function resolveBalancedModelDef(config2, adapterDefault) {
|
|
18268
|
+
const configWithModels = config2;
|
|
18269
|
+
const models = configWithModels.models;
|
|
18270
|
+
const balancedEntry = models?.balanced;
|
|
18271
|
+
if (balancedEntry) {
|
|
18272
|
+
return resolveModel(balancedEntry);
|
|
18273
|
+
}
|
|
18274
|
+
if (adapterDefault) {
|
|
18275
|
+
return adapterDefault;
|
|
18276
|
+
}
|
|
18277
|
+
throw new Error("No balanced model configured in config.models.balanced and no adapter default provided");
|
|
18278
|
+
}
|
|
18279
|
+
var init_model_resolution = __esm(() => {
|
|
18280
|
+
init_schema();
|
|
18281
|
+
});
|
|
18282
|
+
|
|
18283
|
+
// src/agents/claude-plan.ts
|
|
18284
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
18285
|
+
import { tmpdir } from "os";
|
|
18286
|
+
import { join } from "path";
|
|
18287
|
+
function buildPlanCommand(binary, options) {
|
|
18288
|
+
const cmd = [binary, "--permission-mode", "plan"];
|
|
18289
|
+
let modelDef = options.modelDef;
|
|
18290
|
+
if (!modelDef && options.config) {
|
|
18291
|
+
modelDef = resolveBalancedModelDef(options.config);
|
|
18292
|
+
}
|
|
18293
|
+
if (modelDef) {
|
|
18294
|
+
cmd.push("--model", modelDef.model);
|
|
18295
|
+
}
|
|
18296
|
+
cmd.push("--dangerously-skip-permissions");
|
|
18297
|
+
let fullPrompt = options.prompt;
|
|
18298
|
+
if (options.codebaseContext) {
|
|
18299
|
+
fullPrompt = `${options.codebaseContext}
|
|
18300
|
+
|
|
18301
|
+
${options.prompt}`;
|
|
18302
|
+
}
|
|
18303
|
+
if (options.inputFile) {
|
|
18304
|
+
try {
|
|
18305
|
+
const inputContent = __require("fs").readFileSync(__require("path").resolve(options.workdir, options.inputFile), "utf-8");
|
|
18306
|
+
fullPrompt = `${fullPrompt}
|
|
18307
|
+
|
|
18308
|
+
## Input Requirements
|
|
18309
|
+
|
|
18310
|
+
${inputContent}`;
|
|
18311
|
+
} catch (error48) {
|
|
18312
|
+
throw new Error(`Failed to read input file ${options.inputFile}: ${error48.message}`);
|
|
18313
|
+
}
|
|
18314
|
+
}
|
|
18315
|
+
if (!options.interactive) {
|
|
18316
|
+
cmd.push("-p", fullPrompt);
|
|
18317
|
+
} else {
|
|
18318
|
+
cmd.push("-p", fullPrompt);
|
|
18319
|
+
}
|
|
18320
|
+
return cmd;
|
|
18321
|
+
}
|
|
18322
|
+
async function runPlan(binary, options, pidRegistry, buildAllowedEnv) {
|
|
18323
|
+
const { resolveBalancedModelDef: resolveBalancedModelDef2 } = await Promise.resolve().then(() => (init_model_resolution(), exports_model_resolution));
|
|
18324
|
+
const cmd = buildPlanCommand(binary, options);
|
|
18325
|
+
let modelDef = options.modelDef;
|
|
18326
|
+
if (!modelDef) {
|
|
18327
|
+
if (!options.config) {
|
|
18328
|
+
throw new Error("runPlan() requires either modelDef or config with models.balanced configured");
|
|
18329
|
+
}
|
|
18330
|
+
modelDef = resolveBalancedModelDef2(options.config);
|
|
18331
|
+
}
|
|
18332
|
+
const envOptions = {
|
|
18333
|
+
workdir: options.workdir,
|
|
18334
|
+
modelDef,
|
|
18335
|
+
prompt: "",
|
|
18336
|
+
modelTier: options.modelTier || "balanced",
|
|
18337
|
+
timeoutSeconds: 600
|
|
18338
|
+
};
|
|
18339
|
+
if (options.interactive) {
|
|
18340
|
+
const proc = Bun.spawn(cmd, {
|
|
18341
|
+
cwd: options.workdir,
|
|
18342
|
+
stdin: "inherit",
|
|
18343
|
+
stdout: "inherit",
|
|
18344
|
+
stderr: "inherit",
|
|
18345
|
+
env: buildAllowedEnv(envOptions)
|
|
18346
|
+
});
|
|
18347
|
+
await pidRegistry.register(proc.pid);
|
|
18348
|
+
const exitCode = await proc.exited;
|
|
18349
|
+
await pidRegistry.unregister(proc.pid);
|
|
18350
|
+
if (exitCode !== 0) {
|
|
18351
|
+
throw new Error(`Plan mode failed with exit code ${exitCode}`);
|
|
18352
|
+
}
|
|
18353
|
+
return { specContent: "", conversationLog: "" };
|
|
18354
|
+
}
|
|
18355
|
+
const tempDir = mkdtempSync(join(tmpdir(), "nax-plan-"));
|
|
18356
|
+
const outFile = join(tempDir, "stdout.txt");
|
|
18357
|
+
const errFile = join(tempDir, "stderr.txt");
|
|
18358
|
+
try {
|
|
18359
|
+
const proc = Bun.spawn(cmd, {
|
|
18360
|
+
cwd: options.workdir,
|
|
18361
|
+
stdin: "ignore",
|
|
18362
|
+
stdout: Bun.file(outFile),
|
|
18363
|
+
stderr: Bun.file(errFile),
|
|
18364
|
+
env: buildAllowedEnv(envOptions)
|
|
18365
|
+
});
|
|
18366
|
+
await pidRegistry.register(proc.pid);
|
|
18367
|
+
const exitCode = await proc.exited;
|
|
18368
|
+
await pidRegistry.unregister(proc.pid);
|
|
18369
|
+
const specContent = await Bun.file(outFile).text();
|
|
18370
|
+
const conversationLog = await Bun.file(errFile).text();
|
|
18371
|
+
if (exitCode !== 0) {
|
|
18372
|
+
throw new Error(`Plan mode failed with exit code ${exitCode}: ${conversationLog || "unknown error"}`);
|
|
18373
|
+
}
|
|
18374
|
+
return { specContent, conversationLog };
|
|
18375
|
+
} finally {
|
|
18376
|
+
try {
|
|
18377
|
+
rmSync(tempDir, { recursive: true });
|
|
18378
|
+
} catch (error48) {
|
|
18379
|
+
const logger = getLogger();
|
|
18380
|
+
logger?.debug("agent", "Failed to clean up temp directory", { error: error48, tempDir });
|
|
18381
|
+
}
|
|
18382
|
+
}
|
|
18383
|
+
}
|
|
18384
|
+
var init_claude_plan = __esm(() => {
|
|
18385
|
+
init_logger2();
|
|
18386
|
+
init_model_resolution();
|
|
18387
|
+
});
|
|
18388
|
+
|
|
18389
|
+
// src/agents/cost.ts
|
|
18390
|
+
function parseTokenUsage(output) {
|
|
18391
|
+
try {
|
|
18392
|
+
const jsonMatch = output.match(/\{[^}]*"usage"\s*:\s*\{[^}]*"input_tokens"\s*:\s*(\d+)[^}]*"output_tokens"\s*:\s*(\d+)[^}]*\}[^}]*\}/);
|
|
18393
|
+
if (jsonMatch) {
|
|
18394
|
+
return {
|
|
18395
|
+
inputTokens: Number.parseInt(jsonMatch[1], 10),
|
|
18396
|
+
outputTokens: Number.parseInt(jsonMatch[2], 10),
|
|
18397
|
+
confidence: "exact"
|
|
18398
|
+
};
|
|
18399
|
+
}
|
|
18400
|
+
const lines = output.split(`
|
|
18401
|
+
`);
|
|
18402
|
+
for (const line of lines) {
|
|
18403
|
+
if (line.trim().startsWith("{")) {
|
|
18404
|
+
try {
|
|
18405
|
+
const parsed = JSON.parse(line);
|
|
18406
|
+
if (parsed.usage?.input_tokens && parsed.usage?.output_tokens) {
|
|
18407
|
+
return {
|
|
18408
|
+
inputTokens: parsed.usage.input_tokens,
|
|
18409
|
+
outputTokens: parsed.usage.output_tokens,
|
|
18410
|
+
confidence: "exact"
|
|
18411
|
+
};
|
|
18412
|
+
}
|
|
18413
|
+
} catch {}
|
|
18414
|
+
}
|
|
18415
|
+
}
|
|
18416
|
+
} catch {}
|
|
18417
|
+
const inputMatch = output.match(/\b(?:input|input_tokens)\s*:\s*(\d{2,})|(?:input)\s+(?:tokens?)\s*:\s*(\d{2,})/i);
|
|
18418
|
+
const outputMatch = output.match(/\b(?:output|output_tokens)\s*:\s*(\d{2,})|(?:output)\s+(?:tokens?)\s*:\s*(\d{2,})/i);
|
|
18419
|
+
if (inputMatch && outputMatch) {
|
|
18420
|
+
const inputTokens = Number.parseInt(inputMatch[1] || inputMatch[2], 10);
|
|
18421
|
+
const outputTokens = Number.parseInt(outputMatch[1] || outputMatch[2], 10);
|
|
18422
|
+
if (inputTokens > 1e6 || outputTokens > 1e6) {
|
|
18423
|
+
return null;
|
|
18424
|
+
}
|
|
18425
|
+
return {
|
|
18426
|
+
inputTokens,
|
|
18427
|
+
outputTokens,
|
|
18428
|
+
confidence: "estimated"
|
|
18429
|
+
};
|
|
18430
|
+
}
|
|
18431
|
+
return null;
|
|
18432
|
+
}
|
|
18433
|
+
function estimateCost(modelTier, inputTokens, outputTokens, customRates) {
|
|
18434
|
+
const rates = customRates ?? COST_RATES[modelTier];
|
|
18435
|
+
const inputCost = inputTokens / 1e6 * rates.inputPer1M;
|
|
18436
|
+
const outputCost = outputTokens / 1e6 * rates.outputPer1M;
|
|
18437
|
+
return inputCost + outputCost;
|
|
18438
|
+
}
|
|
18439
|
+
function estimateCostFromOutput(modelTier, output) {
|
|
18440
|
+
const usage = parseTokenUsage(output);
|
|
18441
|
+
if (!usage) {
|
|
18442
|
+
return null;
|
|
18443
|
+
}
|
|
18444
|
+
const cost = estimateCost(modelTier, usage.inputTokens, usage.outputTokens);
|
|
18445
|
+
return {
|
|
18446
|
+
cost,
|
|
18447
|
+
confidence: usage.confidence
|
|
18448
|
+
};
|
|
18449
|
+
}
|
|
18450
|
+
function estimateCostByDuration(modelTier, durationMs) {
|
|
18451
|
+
const costPerMinute = {
|
|
18452
|
+
fast: 0.01,
|
|
18453
|
+
balanced: 0.05,
|
|
18454
|
+
powerful: 0.15
|
|
18455
|
+
};
|
|
18456
|
+
const minutes = durationMs / 60000;
|
|
18457
|
+
const cost = minutes * costPerMinute[modelTier];
|
|
18458
|
+
return {
|
|
18459
|
+
cost,
|
|
18460
|
+
confidence: "fallback"
|
|
18461
|
+
};
|
|
18462
|
+
}
|
|
18463
|
+
var COST_RATES;
|
|
18464
|
+
var init_cost = __esm(() => {
|
|
18465
|
+
COST_RATES = {
|
|
18466
|
+
fast: {
|
|
18467
|
+
inputPer1M: 0.8,
|
|
18468
|
+
outputPer1M: 4
|
|
18469
|
+
},
|
|
18470
|
+
balanced: {
|
|
18471
|
+
inputPer1M: 3,
|
|
18472
|
+
outputPer1M: 15
|
|
18473
|
+
},
|
|
18474
|
+
powerful: {
|
|
18475
|
+
inputPer1M: 15,
|
|
18476
|
+
outputPer1M: 75
|
|
18477
|
+
}
|
|
18478
|
+
};
|
|
18479
|
+
});
|
|
18480
|
+
|
|
18481
|
+
// src/agents/claude.ts
|
|
18482
|
+
class ClaudeCodeAdapter {
|
|
18483
|
+
name = "claude";
|
|
18484
|
+
displayName = "Claude Code";
|
|
18485
|
+
binary = "claude";
|
|
18486
|
+
capabilities = {
|
|
18487
|
+
supportedTiers: ["fast", "balanced", "powerful"],
|
|
18488
|
+
maxContextTokens: 200000,
|
|
18489
|
+
features: new Set(["tdd", "review", "refactor", "batch"])
|
|
18490
|
+
};
|
|
18491
|
+
pidRegistries = new Map;
|
|
18492
|
+
getPidRegistry(workdir) {
|
|
18493
|
+
if (!this.pidRegistries.has(workdir)) {
|
|
18494
|
+
this.pidRegistries.set(workdir, new PidRegistry(workdir));
|
|
18495
|
+
}
|
|
18496
|
+
const registry2 = this.pidRegistries.get(workdir);
|
|
18497
|
+
if (!registry2) {
|
|
18498
|
+
throw new Error(`PidRegistry not found for workdir: ${workdir}`);
|
|
18499
|
+
}
|
|
18500
|
+
return registry2;
|
|
18501
|
+
}
|
|
18502
|
+
async isInstalled() {
|
|
18503
|
+
try {
|
|
18504
|
+
const proc = Bun.spawn(["which", this.binary], { stdout: "pipe", stderr: "pipe" });
|
|
18505
|
+
const code = await proc.exited;
|
|
18506
|
+
return code === 0;
|
|
18507
|
+
} catch (error48) {
|
|
18508
|
+
const logger = getLogger();
|
|
18509
|
+
logger?.debug("agent", "Failed to check if agent is installed", { error: error48 });
|
|
18510
|
+
return false;
|
|
18511
|
+
}
|
|
18512
|
+
}
|
|
18513
|
+
buildCommand(options) {
|
|
18514
|
+
const model = options.modelDef.model;
|
|
18515
|
+
const skipPermissions = options.dangerouslySkipPermissions ?? true;
|
|
18516
|
+
const permArgs = skipPermissions ? ["--dangerously-skip-permissions"] : [];
|
|
18517
|
+
return [this.binary, "--model", model, ...permArgs, "-p", options.prompt];
|
|
18518
|
+
}
|
|
18519
|
+
async run(options) {
|
|
18520
|
+
const maxRetries = 3;
|
|
18521
|
+
let lastError = null;
|
|
18522
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
18523
|
+
try {
|
|
18524
|
+
const result = await this.runOnce(options, attempt);
|
|
18525
|
+
if (result.rateLimited && attempt < maxRetries) {
|
|
18526
|
+
const backoffMs = 2 ** attempt * 1000;
|
|
18527
|
+
const logger = getLogger();
|
|
18528
|
+
logger.warn("agent", "Rate limited, retrying", { backoffSeconds: backoffMs / 1000, attempt, maxRetries });
|
|
18529
|
+
await Bun.sleep(backoffMs);
|
|
18530
|
+
continue;
|
|
18531
|
+
}
|
|
18532
|
+
return result;
|
|
18533
|
+
} catch (error48) {
|
|
18534
|
+
lastError = error48;
|
|
18535
|
+
const isSpawnError = lastError.message.includes("spawn") || lastError.message.includes("ENOENT");
|
|
18536
|
+
if (isSpawnError && attempt < maxRetries) {
|
|
18537
|
+
const backoffMs = 2 ** attempt * 1000;
|
|
18538
|
+
const logger = getLogger();
|
|
18539
|
+
logger.warn("agent", "Agent spawn error, retrying", {
|
|
18540
|
+
error: lastError.message,
|
|
18541
|
+
backoffSeconds: backoffMs / 1000,
|
|
18542
|
+
attempt,
|
|
18543
|
+
maxRetries
|
|
18544
|
+
});
|
|
18545
|
+
await Bun.sleep(backoffMs);
|
|
18546
|
+
continue;
|
|
18547
|
+
}
|
|
18548
|
+
throw lastError;
|
|
18297
18549
|
}
|
|
18298
|
-
}
|
|
18299
|
-
|
|
18300
|
-
|
|
18301
|
-
|
|
18302
|
-
|
|
18303
|
-
|
|
18304
|
-
|
|
18305
|
-
|
|
18306
|
-
|
|
18307
|
-
model: "fast",
|
|
18308
|
-
fallbackToKeywords: true,
|
|
18309
|
-
cacheDecisions: true,
|
|
18310
|
-
mode: "hybrid",
|
|
18311
|
-
timeoutMs: 15000
|
|
18550
|
+
}
|
|
18551
|
+
throw lastError || new Error("Agent execution failed after all retries");
|
|
18552
|
+
}
|
|
18553
|
+
buildAllowedEnv(options) {
|
|
18554
|
+
const allowed = {};
|
|
18555
|
+
const essentialVars = ["PATH", "HOME", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
|
|
18556
|
+
for (const varName of essentialVars) {
|
|
18557
|
+
if (process.env[varName]) {
|
|
18558
|
+
allowed[varName] = process.env[varName];
|
|
18312
18559
|
}
|
|
18313
|
-
}
|
|
18314
|
-
|
|
18315
|
-
|
|
18316
|
-
|
|
18317
|
-
|
|
18318
|
-
sessionTimeoutSeconds: 600,
|
|
18319
|
-
verificationTimeoutSeconds: 300,
|
|
18320
|
-
maxStoriesPerFeature: 500,
|
|
18321
|
-
rectification: {
|
|
18322
|
-
enabled: true,
|
|
18323
|
-
maxRetries: 2,
|
|
18324
|
-
fullSuiteTimeoutSeconds: 120,
|
|
18325
|
-
maxFailureSummaryChars: 2000,
|
|
18326
|
-
abortOnIncreasingFailures: true
|
|
18327
|
-
},
|
|
18328
|
-
regressionGate: {
|
|
18329
|
-
enabled: true,
|
|
18330
|
-
timeoutSeconds: 120,
|
|
18331
|
-
acceptOnTimeout: true,
|
|
18332
|
-
maxRectificationAttempts: 2
|
|
18333
|
-
},
|
|
18334
|
-
contextProviderTokenBudget: 2000,
|
|
18335
|
-
smartTestRunner: true
|
|
18336
|
-
},
|
|
18337
|
-
quality: {
|
|
18338
|
-
requireTypecheck: true,
|
|
18339
|
-
requireLint: true,
|
|
18340
|
-
requireTests: true,
|
|
18341
|
-
commands: {},
|
|
18342
|
-
forceExit: false,
|
|
18343
|
-
detectOpenHandles: true,
|
|
18344
|
-
detectOpenHandlesRetries: 1,
|
|
18345
|
-
gracePeriodMs: 5000,
|
|
18346
|
-
dangerouslySkipPermissions: true,
|
|
18347
|
-
drainTimeoutMs: 2000,
|
|
18348
|
-
shell: "/bin/sh",
|
|
18349
|
-
stripEnvVars: ["CLAUDECODE", "REPL_ID", "AGENT"],
|
|
18350
|
-
environmentalEscalationDivisor: 2
|
|
18351
|
-
},
|
|
18352
|
-
tdd: {
|
|
18353
|
-
maxRetries: 2,
|
|
18354
|
-
autoVerifyIsolation: true,
|
|
18355
|
-
autoApproveVerifier: true,
|
|
18356
|
-
strategy: "auto",
|
|
18357
|
-
sessionTiers: {
|
|
18358
|
-
testWriter: "balanced",
|
|
18359
|
-
verifier: "fast"
|
|
18360
|
-
},
|
|
18361
|
-
testWriterAllowedPaths: ["src/index.ts", "src/**/index.ts"],
|
|
18362
|
-
rollbackOnFailure: true,
|
|
18363
|
-
greenfieldDetection: true
|
|
18364
|
-
},
|
|
18365
|
-
constitution: {
|
|
18366
|
-
enabled: true,
|
|
18367
|
-
path: "constitution.md",
|
|
18368
|
-
maxTokens: 2000
|
|
18369
|
-
},
|
|
18370
|
-
analyze: {
|
|
18371
|
-
llmEnhanced: true,
|
|
18372
|
-
model: "balanced",
|
|
18373
|
-
fallbackToKeywords: true,
|
|
18374
|
-
maxCodebaseSummaryTokens: 5000
|
|
18375
|
-
},
|
|
18376
|
-
review: {
|
|
18377
|
-
enabled: true,
|
|
18378
|
-
checks: ["typecheck", "lint"],
|
|
18379
|
-
commands: {}
|
|
18380
|
-
},
|
|
18381
|
-
plan: {
|
|
18382
|
-
model: "balanced",
|
|
18383
|
-
outputPath: "spec.md"
|
|
18384
|
-
},
|
|
18385
|
-
acceptance: {
|
|
18386
|
-
enabled: true,
|
|
18387
|
-
maxRetries: 2,
|
|
18388
|
-
generateTests: true,
|
|
18389
|
-
testPath: "acceptance.test.ts"
|
|
18390
|
-
},
|
|
18391
|
-
context: {
|
|
18392
|
-
fileInjection: "disabled",
|
|
18393
|
-
testCoverage: {
|
|
18394
|
-
enabled: true,
|
|
18395
|
-
detail: "names-and-counts",
|
|
18396
|
-
maxTokens: 500,
|
|
18397
|
-
testPattern: "**/*.test.{ts,js,tsx,jsx}",
|
|
18398
|
-
scopeToStory: true
|
|
18399
|
-
},
|
|
18400
|
-
autoDetect: {
|
|
18401
|
-
enabled: true,
|
|
18402
|
-
maxFiles: 5,
|
|
18403
|
-
traceImports: false
|
|
18560
|
+
}
|
|
18561
|
+
const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"];
|
|
18562
|
+
for (const varName of apiKeyVars) {
|
|
18563
|
+
if (process.env[varName]) {
|
|
18564
|
+
allowed[varName] = process.env[varName];
|
|
18404
18565
|
}
|
|
18405
|
-
}
|
|
18406
|
-
|
|
18407
|
-
|
|
18408
|
-
|
|
18409
|
-
|
|
18410
|
-
timeout: 600000,
|
|
18411
|
-
fallback: "escalate"
|
|
18412
|
-
},
|
|
18413
|
-
triggers: {
|
|
18414
|
-
"security-review": true,
|
|
18415
|
-
"cost-warning": true
|
|
18566
|
+
}
|
|
18567
|
+
const allowedPrefixes = ["CLAUDE_", "NAX_", "CLAW_", "TURBO_"];
|
|
18568
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
18569
|
+
if (allowedPrefixes.some((prefix) => key.startsWith(prefix))) {
|
|
18570
|
+
allowed[key] = value;
|
|
18416
18571
|
}
|
|
18417
|
-
}
|
|
18418
|
-
|
|
18419
|
-
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18423
|
-
|
|
18572
|
+
}
|
|
18573
|
+
if (options.modelDef.env) {
|
|
18574
|
+
Object.assign(allowed, options.modelDef.env);
|
|
18575
|
+
}
|
|
18576
|
+
if (options.env) {
|
|
18577
|
+
Object.assign(allowed, options.env);
|
|
18578
|
+
}
|
|
18579
|
+
return allowed;
|
|
18580
|
+
}
|
|
18581
|
+
async runOnce(options, _attempt) {
|
|
18582
|
+
const cmd = this.buildCommand(options);
|
|
18583
|
+
const startTime = Date.now();
|
|
18584
|
+
const proc = Bun.spawn(cmd, {
|
|
18585
|
+
cwd: options.workdir,
|
|
18586
|
+
stdout: "pipe",
|
|
18587
|
+
stderr: "inherit",
|
|
18588
|
+
env: this.buildAllowedEnv(options)
|
|
18589
|
+
});
|
|
18590
|
+
const processPid = proc.pid;
|
|
18591
|
+
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
18592
|
+
await pidRegistry.register(processPid);
|
|
18593
|
+
let timedOut = false;
|
|
18594
|
+
const timeoutId = setTimeout(() => {
|
|
18595
|
+
timedOut = true;
|
|
18596
|
+
try {
|
|
18597
|
+
_runOnceDeps.killProc(proc, "SIGTERM");
|
|
18598
|
+
} catch {}
|
|
18599
|
+
setTimeout(() => {
|
|
18600
|
+
try {
|
|
18601
|
+
_runOnceDeps.killProc(proc, "SIGKILL");
|
|
18602
|
+
} catch {}
|
|
18603
|
+
}, SIGKILL_GRACE_PERIOD_MS);
|
|
18604
|
+
}, options.timeoutSeconds * 1000);
|
|
18605
|
+
let exitCode;
|
|
18606
|
+
try {
|
|
18607
|
+
const hardDeadlineMs = options.timeoutSeconds * 1000 + SIGKILL_GRACE_PERIOD_MS + 3000;
|
|
18608
|
+
exitCode = await Promise.race([
|
|
18609
|
+
proc.exited,
|
|
18610
|
+
new Promise((resolve) => setTimeout(() => resolve(-1), hardDeadlineMs))
|
|
18611
|
+
]);
|
|
18612
|
+
if (exitCode === -1) {
|
|
18613
|
+
try {
|
|
18614
|
+
process.kill(processPid, "SIGKILL");
|
|
18615
|
+
} catch {}
|
|
18616
|
+
try {
|
|
18617
|
+
process.kill(-processPid, "SIGKILL");
|
|
18618
|
+
} catch {}
|
|
18424
18619
|
}
|
|
18425
|
-
}
|
|
18426
|
-
|
|
18427
|
-
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18432
|
-
|
|
18433
|
-
|
|
18620
|
+
} finally {
|
|
18621
|
+
clearTimeout(timeoutId);
|
|
18622
|
+
await pidRegistry.unregister(processPid);
|
|
18623
|
+
}
|
|
18624
|
+
const stdout = await Promise.race([
|
|
18625
|
+
new Response(proc.stdout).text(),
|
|
18626
|
+
new Promise((resolve) => setTimeout(() => resolve(""), 5000))
|
|
18627
|
+
]);
|
|
18628
|
+
const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
|
|
18629
|
+
const durationMs = Date.now() - startTime;
|
|
18630
|
+
const fullOutput = stdout + stderr;
|
|
18631
|
+
const rateLimited = fullOutput.toLowerCase().includes("rate limit") || fullOutput.includes("429") || fullOutput.toLowerCase().includes("too many requests");
|
|
18632
|
+
let costEstimate = estimateCostFromOutput(options.modelTier, fullOutput);
|
|
18633
|
+
const logger = getLogger();
|
|
18634
|
+
if (!costEstimate) {
|
|
18635
|
+
const fallbackEstimate = estimateCostByDuration(options.modelTier, durationMs);
|
|
18636
|
+
costEstimate = {
|
|
18637
|
+
cost: fallbackEstimate.cost * 1.5,
|
|
18638
|
+
confidence: "fallback"
|
|
18639
|
+
};
|
|
18640
|
+
logger.warn("agent", "Cost estimation fallback (duration-based)", {
|
|
18641
|
+
modelTier: options.modelTier,
|
|
18642
|
+
cost: costEstimate.cost
|
|
18643
|
+
});
|
|
18644
|
+
} else if (costEstimate.confidence === "estimated") {
|
|
18645
|
+
logger.warn("agent", "Cost estimation using regex parsing (estimated confidence)", { cost: costEstimate.cost });
|
|
18646
|
+
}
|
|
18647
|
+
const cost = costEstimate.cost;
|
|
18648
|
+
const actualExitCode = timedOut ? 124 : exitCode;
|
|
18649
|
+
return {
|
|
18650
|
+
success: exitCode === 0 && !timedOut,
|
|
18651
|
+
exitCode: actualExitCode,
|
|
18652
|
+
output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS4),
|
|
18653
|
+
stderr: stderr.slice(-MAX_AGENT_STDERR_CHARS),
|
|
18654
|
+
rateLimited,
|
|
18655
|
+
durationMs,
|
|
18656
|
+
estimatedCost: cost,
|
|
18657
|
+
pid: processPid
|
|
18658
|
+
};
|
|
18659
|
+
}
|
|
18660
|
+
async complete(prompt, options) {
|
|
18661
|
+
const cmd = ["claude", "-p", prompt];
|
|
18662
|
+
if (options?.model) {
|
|
18663
|
+
cmd.push("--model", options.model);
|
|
18664
|
+
}
|
|
18665
|
+
if (options?.maxTokens !== undefined) {
|
|
18666
|
+
cmd.push("--max-tokens", String(options.maxTokens));
|
|
18667
|
+
}
|
|
18668
|
+
if (options?.jsonMode) {
|
|
18669
|
+
cmd.push("--output-format", "json");
|
|
18670
|
+
}
|
|
18671
|
+
const proc = _completeDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
18672
|
+
const exitCode = await proc.exited;
|
|
18673
|
+
const stdout = await new Response(proc.stdout).text();
|
|
18674
|
+
const stderr = await new Response(proc.stderr).text();
|
|
18675
|
+
const trimmed = stdout.trim();
|
|
18676
|
+
if (exitCode !== 0) {
|
|
18677
|
+
const errorDetails = stderr.trim() || trimmed;
|
|
18678
|
+
const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
|
|
18679
|
+
throw new CompleteError(errorMessage, exitCode);
|
|
18680
|
+
}
|
|
18681
|
+
if (!trimmed) {
|
|
18682
|
+
throw new CompleteError("complete() returned empty output");
|
|
18683
|
+
}
|
|
18684
|
+
return trimmed;
|
|
18685
|
+
}
|
|
18686
|
+
async plan(options) {
|
|
18687
|
+
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
18688
|
+
return runPlan(this.binary, options, pidRegistry, this.buildAllowedEnv.bind(this));
|
|
18689
|
+
}
|
|
18690
|
+
async decompose(options) {
|
|
18691
|
+
const { resolveBalancedModelDef: resolveBalancedModelDef2 } = await Promise.resolve().then(() => (init_model_resolution(), exports_model_resolution));
|
|
18692
|
+
const prompt = buildDecomposePrompt(options);
|
|
18693
|
+
let modelDef = options.modelDef;
|
|
18694
|
+
if (!modelDef) {
|
|
18695
|
+
if (!options.config) {
|
|
18696
|
+
throw new Error("decompose() requires either modelDef or config with models.balanced configured");
|
|
18697
|
+
}
|
|
18698
|
+
modelDef = resolveBalancedModelDef2(options.config);
|
|
18699
|
+
}
|
|
18700
|
+
const cmd = [this.binary, "--model", modelDef.model, "--dangerously-skip-permissions", "-p", prompt];
|
|
18701
|
+
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
18702
|
+
const proc = _decomposeDeps.spawn(cmd, {
|
|
18703
|
+
cwd: options.workdir,
|
|
18704
|
+
stdout: "pipe",
|
|
18705
|
+
stderr: "inherit",
|
|
18706
|
+
env: this.buildAllowedEnv({
|
|
18707
|
+
workdir: options.workdir,
|
|
18708
|
+
modelDef,
|
|
18709
|
+
prompt: "",
|
|
18710
|
+
modelTier: options.modelTier || "balanced",
|
|
18711
|
+
timeoutSeconds: 600
|
|
18712
|
+
})
|
|
18713
|
+
});
|
|
18714
|
+
await pidRegistry.register(proc.pid);
|
|
18715
|
+
const DECOMPOSE_TIMEOUT_MS = 300000;
|
|
18716
|
+
let timedOut = false;
|
|
18717
|
+
const decomposeTimerId = setTimeout(() => {
|
|
18718
|
+
timedOut = true;
|
|
18719
|
+
try {
|
|
18720
|
+
proc.kill("SIGTERM");
|
|
18721
|
+
} catch {}
|
|
18722
|
+
setTimeout(() => {
|
|
18723
|
+
try {
|
|
18724
|
+
proc.kill("SIGKILL");
|
|
18725
|
+
} catch {}
|
|
18726
|
+
}, 5000);
|
|
18727
|
+
}, DECOMPOSE_TIMEOUT_MS);
|
|
18728
|
+
let exitCode;
|
|
18729
|
+
try {
|
|
18730
|
+
exitCode = await proc.exited;
|
|
18731
|
+
} finally {
|
|
18732
|
+
clearTimeout(decomposeTimerId);
|
|
18733
|
+
await pidRegistry.unregister(proc.pid);
|
|
18734
|
+
}
|
|
18735
|
+
if (timedOut) {
|
|
18736
|
+
throw new Error(`Decompose timed out after ${DECOMPOSE_TIMEOUT_MS / 1000}s`);
|
|
18737
|
+
}
|
|
18738
|
+
const stdout = await Promise.race([
|
|
18739
|
+
new Response(proc.stdout).text(),
|
|
18740
|
+
new Promise((resolve) => setTimeout(() => resolve(""), 5000))
|
|
18741
|
+
]);
|
|
18742
|
+
const stderr = await new Response(proc.stderr).text();
|
|
18743
|
+
if (exitCode !== 0) {
|
|
18744
|
+
throw new Error(`Decompose failed with exit code ${exitCode}: ${stderr}`);
|
|
18745
|
+
}
|
|
18746
|
+
const stories = parseDecomposeOutput(stdout);
|
|
18747
|
+
return { stories };
|
|
18748
|
+
}
|
|
18749
|
+
runInteractive(options) {
|
|
18750
|
+
const model = options.modelDef.model;
|
|
18751
|
+
const cmd = [this.binary, "--model", model, options.prompt];
|
|
18752
|
+
const proc = Bun.spawn(cmd, {
|
|
18753
|
+
cwd: options.workdir,
|
|
18754
|
+
env: { ...this.buildAllowedEnv(options), TERM: "xterm-256color", FORCE_COLOR: "1" },
|
|
18755
|
+
stdin: "pipe",
|
|
18756
|
+
stdout: "pipe",
|
|
18757
|
+
stderr: "inherit"
|
|
18758
|
+
});
|
|
18759
|
+
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
18760
|
+
pidRegistry.register(proc.pid).catch(() => {});
|
|
18761
|
+
(async () => {
|
|
18762
|
+
try {
|
|
18763
|
+
for await (const chunk of proc.stdout) {
|
|
18764
|
+
options.onOutput(Buffer.from(chunk));
|
|
18765
|
+
}
|
|
18766
|
+
} catch (err) {
|
|
18767
|
+
getLogger()?.error("agent", "runInteractive stdout error", { err });
|
|
18768
|
+
}
|
|
18769
|
+
})();
|
|
18770
|
+
proc.exited.then((code) => {
|
|
18771
|
+
pidRegistry.unregister(proc.pid).catch(() => {});
|
|
18772
|
+
options.onExit(code ?? 1);
|
|
18773
|
+
}).catch((err) => {
|
|
18774
|
+
getLogger()?.error("agent", "runInteractive exit error", { err });
|
|
18775
|
+
});
|
|
18776
|
+
return {
|
|
18777
|
+
write: (data) => {
|
|
18778
|
+
proc.stdin.write(data);
|
|
18779
|
+
},
|
|
18780
|
+
resize: (_cols, _rows) => {},
|
|
18781
|
+
kill: () => {
|
|
18782
|
+
proc.kill();
|
|
18783
|
+
},
|
|
18784
|
+
pid: proc.pid
|
|
18785
|
+
};
|
|
18786
|
+
}
|
|
18787
|
+
}
|
|
18788
|
+
var MAX_AGENT_OUTPUT_CHARS4 = 5000, MAX_AGENT_STDERR_CHARS = 1000, SIGKILL_GRACE_PERIOD_MS = 5000, _completeDeps, _decomposeDeps, _runOnceDeps;
|
|
18789
|
+
var init_claude = __esm(() => {
|
|
18790
|
+
init_pid_registry();
|
|
18791
|
+
init_logger2();
|
|
18792
|
+
init_claude_plan();
|
|
18793
|
+
init_cost();
|
|
18794
|
+
init_types2();
|
|
18795
|
+
_completeDeps = {
|
|
18796
|
+
spawn(cmd, opts) {
|
|
18797
|
+
return Bun.spawn(cmd, opts);
|
|
18798
|
+
}
|
|
18799
|
+
};
|
|
18800
|
+
_decomposeDeps = {
|
|
18801
|
+
spawn(cmd, opts) {
|
|
18802
|
+
return Bun.spawn(cmd, opts);
|
|
18803
|
+
}
|
|
18804
|
+
};
|
|
18805
|
+
_runOnceDeps = {
|
|
18806
|
+
killProc(proc, signal) {
|
|
18807
|
+
proc.kill(signal);
|
|
18434
18808
|
}
|
|
18435
18809
|
};
|
|
18436
18810
|
});
|
|
18437
18811
|
|
|
18438
|
-
// src/
|
|
18439
|
-
var
|
|
18440
|
-
|
|
18441
|
-
|
|
18812
|
+
// src/agents/registry.ts
|
|
18813
|
+
var exports_registry = {};
|
|
18814
|
+
__export(exports_registry, {
|
|
18815
|
+
getInstalledAgents: () => getInstalledAgents,
|
|
18816
|
+
getAllAgentNames: () => getAllAgentNames,
|
|
18817
|
+
getAgent: () => getAgent,
|
|
18818
|
+
checkAgentHealth: () => checkAgentHealth,
|
|
18819
|
+
ALL_AGENTS: () => ALL_AGENTS
|
|
18820
|
+
});
|
|
18821
|
+
function getAllAgentNames() {
|
|
18822
|
+
return ALL_AGENTS.map((a) => a.name);
|
|
18823
|
+
}
|
|
18824
|
+
function getAgent(name) {
|
|
18825
|
+
return ALL_AGENTS.find((a) => a.name === name);
|
|
18826
|
+
}
|
|
18827
|
+
async function getInstalledAgents() {
|
|
18828
|
+
const results = await Promise.all(ALL_AGENTS.map(async (agent) => ({
|
|
18829
|
+
agent,
|
|
18830
|
+
installed: await agent.isInstalled()
|
|
18831
|
+
})));
|
|
18832
|
+
return results.filter((r) => r.installed).map((r) => r.agent);
|
|
18833
|
+
}
|
|
18834
|
+
async function checkAgentHealth() {
|
|
18835
|
+
return Promise.all(ALL_AGENTS.map(async (agent) => ({
|
|
18836
|
+
name: agent.name,
|
|
18837
|
+
displayName: agent.displayName,
|
|
18838
|
+
installed: await agent.isInstalled()
|
|
18839
|
+
})));
|
|
18840
|
+
}
|
|
18841
|
+
var ALL_AGENTS;
|
|
18842
|
+
var init_registry = __esm(() => {
|
|
18843
|
+
init_aider();
|
|
18844
|
+
init_codex();
|
|
18845
|
+
init_gemini();
|
|
18846
|
+
init_opencode();
|
|
18847
|
+
init_claude();
|
|
18848
|
+
ALL_AGENTS = [
|
|
18849
|
+
new ClaudeCodeAdapter,
|
|
18850
|
+
new CodexAdapter,
|
|
18851
|
+
new OpenCodeAdapter,
|
|
18852
|
+
new GeminiAdapter,
|
|
18853
|
+
new AiderAdapter
|
|
18854
|
+
];
|
|
18442
18855
|
});
|
|
18443
18856
|
|
|
18444
18857
|
// src/decompose/apply.ts
|
|
@@ -19180,17 +19593,13 @@ function evictOldest() {
|
|
|
19180
19593
|
cachedDecisions.delete(firstKey);
|
|
19181
19594
|
}
|
|
19182
19595
|
}
|
|
19183
|
-
async function callLlmOnce(modelTier, prompt, config2, timeoutMs) {
|
|
19596
|
+
async function callLlmOnce(adapter, modelTier, prompt, config2, timeoutMs) {
|
|
19184
19597
|
const modelEntry = config2.models[modelTier];
|
|
19185
19598
|
if (!modelEntry) {
|
|
19186
19599
|
throw new Error(`Model tier "${modelTier}" not found in config.models`);
|
|
19187
19600
|
}
|
|
19188
19601
|
const modelDef = resolveModel(modelEntry);
|
|
19189
19602
|
const modelArg = modelDef.model;
|
|
19190
|
-
const proc = _deps.spawn(["claude", "-p", prompt, "--model", modelArg], {
|
|
19191
|
-
stdout: "pipe",
|
|
19192
|
-
stderr: "pipe"
|
|
19193
|
-
});
|
|
19194
19603
|
let timeoutId;
|
|
19195
19604
|
const timeoutPromise = new Promise((_, reject) => {
|
|
19196
19605
|
timeoutId = setTimeout(() => {
|
|
@@ -19198,14 +19607,7 @@ async function callLlmOnce(modelTier, prompt, config2, timeoutMs) {
|
|
|
19198
19607
|
}, timeoutMs);
|
|
19199
19608
|
});
|
|
19200
19609
|
timeoutPromise.catch(() => {});
|
|
19201
|
-
const outputPromise = (
|
|
19202
|
-
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
|
19203
|
-
const exitCode = await proc.exited;
|
|
19204
|
-
if (exitCode !== 0) {
|
|
19205
|
-
throw new Error(`claude CLI failed with exit code ${exitCode}: ${stderr}`);
|
|
19206
|
-
}
|
|
19207
|
-
return stdout.trim();
|
|
19208
|
-
})();
|
|
19610
|
+
const outputPromise = adapter.complete(prompt, { model: modelArg });
|
|
19209
19611
|
try {
|
|
19210
19612
|
const result = await Promise.race([outputPromise, timeoutPromise]);
|
|
19211
19613
|
clearTimeout(timeoutId);
|
|
@@ -19213,11 +19615,10 @@ async function callLlmOnce(modelTier, prompt, config2, timeoutMs) {
|
|
|
19213
19615
|
} catch (err) {
|
|
19214
19616
|
clearTimeout(timeoutId);
|
|
19215
19617
|
outputPromise.catch(() => {});
|
|
19216
|
-
proc.kill();
|
|
19217
19618
|
throw err;
|
|
19218
19619
|
}
|
|
19219
19620
|
}
|
|
19220
|
-
async function callLlm(modelTier, prompt, config2) {
|
|
19621
|
+
async function callLlm(adapter, modelTier, prompt, config2) {
|
|
19221
19622
|
const llmConfig = config2.routing.llm;
|
|
19222
19623
|
const timeoutMs = llmConfig?.timeoutMs ?? 30000;
|
|
19223
19624
|
const maxRetries = llmConfig?.retries ?? 1;
|
|
@@ -19225,7 +19626,7 @@ async function callLlm(modelTier, prompt, config2) {
|
|
|
19225
19626
|
let lastError;
|
|
19226
19627
|
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
19227
19628
|
try {
|
|
19228
|
-
return await callLlmOnce(modelTier, prompt, config2, timeoutMs);
|
|
19629
|
+
return await callLlmOnce(adapter, modelTier, prompt, config2, timeoutMs);
|
|
19229
19630
|
} catch (err) {
|
|
19230
19631
|
lastError = err;
|
|
19231
19632
|
if (attempt < maxRetries) {
|
|
@@ -19245,10 +19646,14 @@ async function routeBatch(stories, context) {
|
|
|
19245
19646
|
if (!llmConfig) {
|
|
19246
19647
|
throw new Error("LLM routing config not found");
|
|
19247
19648
|
}
|
|
19649
|
+
const adapter = context.adapter ?? _deps.adapter;
|
|
19650
|
+
if (!adapter) {
|
|
19651
|
+
throw new Error("No agent adapter available for batch routing (AA-003)");
|
|
19652
|
+
}
|
|
19248
19653
|
const modelTier = llmConfig.model ?? "fast";
|
|
19249
19654
|
const prompt = buildBatchPrompt(stories, config2);
|
|
19250
19655
|
try {
|
|
19251
|
-
const output = await callLlm(modelTier, prompt, config2);
|
|
19656
|
+
const output = await callLlm(adapter, modelTier, prompt, config2);
|
|
19252
19657
|
const decisions = parseBatchResponse(output, stories, config2);
|
|
19253
19658
|
if (llmConfig.cacheDecisions) {
|
|
19254
19659
|
for (const [storyId, decision] of decisions.entries()) {
|
|
@@ -19267,12 +19672,14 @@ var cachedDecisions, MAX_CACHE_SIZE = 100, _deps, llmStrategy;
|
|
|
19267
19672
|
var init_llm = __esm(() => {
|
|
19268
19673
|
init_config();
|
|
19269
19674
|
init_logger2();
|
|
19675
|
+
init_router();
|
|
19270
19676
|
init_keyword();
|
|
19271
19677
|
init_llm_prompts();
|
|
19272
19678
|
init_llm_prompts();
|
|
19273
19679
|
cachedDecisions = new Map;
|
|
19274
19680
|
_deps = {
|
|
19275
|
-
spawn: (cmd, opts) => Bun.spawn(cmd, opts)
|
|
19681
|
+
spawn: (cmd, opts) => Bun.spawn(cmd, opts),
|
|
19682
|
+
adapter: undefined
|
|
19276
19683
|
};
|
|
19277
19684
|
llmStrategy = {
|
|
19278
19685
|
name: "llm",
|
|
@@ -19288,14 +19695,16 @@ var init_llm = __esm(() => {
|
|
|
19288
19695
|
if (!cached2) {
|
|
19289
19696
|
throw new Error(`Cached decision not found for story: ${story.id}`);
|
|
19290
19697
|
}
|
|
19698
|
+
const tddStrategy = config2.tdd?.strategy ?? "auto";
|
|
19699
|
+
const freshTestStrategy = determineTestStrategy2(cached2.complexity, story.title, story.description, story.tags, tddStrategy);
|
|
19291
19700
|
const logger = getLogger();
|
|
19292
19701
|
logger.debug("routing", "LLM cache hit", {
|
|
19293
19702
|
storyId: story.id,
|
|
19294
19703
|
complexity: cached2.complexity,
|
|
19295
19704
|
modelTier: cached2.modelTier,
|
|
19296
|
-
testStrategy:
|
|
19705
|
+
testStrategy: freshTestStrategy
|
|
19297
19706
|
});
|
|
19298
|
-
return cached2;
|
|
19707
|
+
return { ...cached2, testStrategy: freshTestStrategy };
|
|
19299
19708
|
}
|
|
19300
19709
|
if (mode === "one-shot") {
|
|
19301
19710
|
const logger = getLogger();
|
|
@@ -19305,9 +19714,13 @@ var init_llm = __esm(() => {
|
|
|
19305
19714
|
return keywordStrategy.route(story, context);
|
|
19306
19715
|
}
|
|
19307
19716
|
try {
|
|
19717
|
+
const adapter = context.adapter ?? _deps.adapter;
|
|
19718
|
+
if (!adapter) {
|
|
19719
|
+
throw new Error("No agent adapter available for LLM routing (AA-003)");
|
|
19720
|
+
}
|
|
19308
19721
|
const modelTier = llmConfig.model ?? "fast";
|
|
19309
19722
|
const prompt = buildRoutingPrompt(story, config2);
|
|
19310
|
-
const output = await callLlm(modelTier, prompt, config2);
|
|
19723
|
+
const output = await callLlm(adapter, modelTier, prompt, config2);
|
|
19311
19724
|
const decision = parseRoutingResponse(output, story, config2);
|
|
19312
19725
|
if (llmConfig.cacheDecisions) {
|
|
19313
19726
|
if (cachedDecisions.size >= MAX_CACHE_SIZE) {
|
|
@@ -19503,6 +19916,8 @@ function determineTestStrategy2(complexity, title, description, tags = [], tddSt
|
|
|
19503
19916
|
return "three-session-tdd";
|
|
19504
19917
|
if (tddStrategy === "lite")
|
|
19505
19918
|
return "three-session-tdd-lite";
|
|
19919
|
+
if (tddStrategy === "simple")
|
|
19920
|
+
return "tdd-simple";
|
|
19506
19921
|
if (tddStrategy === "off")
|
|
19507
19922
|
return "test-after";
|
|
19508
19923
|
const text = [title, description, ...tags ?? []].join(" ").toLowerCase();
|
|
@@ -20242,7 +20657,7 @@ var package_default;
|
|
|
20242
20657
|
var init_package = __esm(() => {
|
|
20243
20658
|
package_default = {
|
|
20244
20659
|
name: "@nathapp/nax",
|
|
20245
|
-
version: "0.
|
|
20660
|
+
version: "0.36.0",
|
|
20246
20661
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
20247
20662
|
type: "module",
|
|
20248
20663
|
bin: {
|
|
@@ -20262,7 +20677,6 @@ var init_package = __esm(() => {
|
|
|
20262
20677
|
prepublishOnly: "bun run build"
|
|
20263
20678
|
},
|
|
20264
20679
|
dependencies: {
|
|
20265
|
-
"@anthropic-ai/sdk": "^0.74.0",
|
|
20266
20680
|
"@types/react": "^19.2.14",
|
|
20267
20681
|
chalk: "^5.6.2",
|
|
20268
20682
|
commander: "^13.1.0",
|
|
@@ -20304,8 +20718,8 @@ var init_version = __esm(() => {
|
|
|
20304
20718
|
NAX_VERSION = package_default.version;
|
|
20305
20719
|
NAX_COMMIT = (() => {
|
|
20306
20720
|
try {
|
|
20307
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
20308
|
-
return "
|
|
20721
|
+
if (/^[0-9a-f]{6,10}$/.test("78b52b0"))
|
|
20722
|
+
return "78b52b0";
|
|
20309
20723
|
} catch {}
|
|
20310
20724
|
try {
|
|
20311
20725
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -20581,7 +20995,7 @@ var init_metrics = __esm(() => {
|
|
|
20581
20995
|
|
|
20582
20996
|
// src/interaction/types.ts
|
|
20583
20997
|
var TRIGGER_METADATA;
|
|
20584
|
-
var
|
|
20998
|
+
var init_types3 = __esm(() => {
|
|
20585
20999
|
TRIGGER_METADATA = {
|
|
20586
21000
|
"security-review": {
|
|
20587
21001
|
defaultFallback: "abort",
|
|
@@ -21475,7 +21889,7 @@ class AutoInteractionPlugin {
|
|
|
21475
21889
|
}
|
|
21476
21890
|
async destroy() {}
|
|
21477
21891
|
async send(request) {}
|
|
21478
|
-
async receive(
|
|
21892
|
+
async receive(_requestId, _timeout = 60000) {
|
|
21479
21893
|
throw new Error("Auto plugin requires full request context (not just requestId)");
|
|
21480
21894
|
}
|
|
21481
21895
|
async decide(request) {
|
|
@@ -21483,8 +21897,20 @@ class AutoInteractionPlugin {
|
|
|
21483
21897
|
return;
|
|
21484
21898
|
}
|
|
21485
21899
|
try {
|
|
21486
|
-
|
|
21487
|
-
|
|
21900
|
+
if (_deps2.callLlm) {
|
|
21901
|
+
const decision2 = await _deps2.callLlm(request);
|
|
21902
|
+
if (decision2.confidence < (this.config.confidenceThreshold ?? 0.7)) {
|
|
21903
|
+
return;
|
|
21904
|
+
}
|
|
21905
|
+
return {
|
|
21906
|
+
requestId: request.id,
|
|
21907
|
+
action: decision2.action,
|
|
21908
|
+
value: decision2.value,
|
|
21909
|
+
respondedBy: "auto-ai",
|
|
21910
|
+
respondedAt: Date.now()
|
|
21911
|
+
};
|
|
21912
|
+
}
|
|
21913
|
+
const decision = await this.callLlm(request);
|
|
21488
21914
|
if (decision.confidence < (this.config.confidenceThreshold ?? 0.7)) {
|
|
21489
21915
|
return;
|
|
21490
21916
|
}
|
|
@@ -21501,26 +21927,24 @@ class AutoInteractionPlugin {
|
|
|
21501
21927
|
}
|
|
21502
21928
|
async callLlm(request) {
|
|
21503
21929
|
const prompt = this.buildPrompt(request);
|
|
21504
|
-
const
|
|
21505
|
-
if (!
|
|
21506
|
-
throw new Error("Auto plugin requires
|
|
21507
|
-
}
|
|
21508
|
-
|
|
21509
|
-
if (
|
|
21510
|
-
|
|
21511
|
-
|
|
21512
|
-
|
|
21513
|
-
|
|
21514
|
-
|
|
21515
|
-
|
|
21516
|
-
|
|
21930
|
+
const adapter = _deps2.adapter;
|
|
21931
|
+
if (!adapter) {
|
|
21932
|
+
throw new Error("Auto plugin requires adapter to be injected via _deps.adapter");
|
|
21933
|
+
}
|
|
21934
|
+
let modelArg;
|
|
21935
|
+
if (this.config.naxConfig) {
|
|
21936
|
+
const modelTier = this.config.model ?? "fast";
|
|
21937
|
+
const modelEntry = this.config.naxConfig.models[modelTier];
|
|
21938
|
+
if (!modelEntry) {
|
|
21939
|
+
throw new Error(`Model tier "${modelTier}" not found in config.models`);
|
|
21940
|
+
}
|
|
21941
|
+
const modelDef = resolveModel(modelEntry);
|
|
21942
|
+
modelArg = modelDef.model;
|
|
21943
|
+
}
|
|
21944
|
+
const output = await adapter.complete(prompt, {
|
|
21945
|
+
...modelArg && { model: modelArg },
|
|
21946
|
+
jsonMode: true
|
|
21517
21947
|
});
|
|
21518
|
-
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
|
21519
|
-
const exitCode = await proc.exited;
|
|
21520
|
-
if (exitCode !== 0) {
|
|
21521
|
-
throw new Error(`claude CLI failed with exit code ${exitCode}: ${stderr}`);
|
|
21522
|
-
}
|
|
21523
|
-
const output = stdout.trim();
|
|
21524
21948
|
return this.parseResponse(output);
|
|
21525
21949
|
}
|
|
21526
21950
|
buildPrompt(request) {
|
|
@@ -21602,6 +22026,7 @@ var init_auto = __esm(() => {
|
|
|
21602
22026
|
naxConfig: exports_external.any().optional()
|
|
21603
22027
|
});
|
|
21604
22028
|
_deps2 = {
|
|
22029
|
+
adapter: null,
|
|
21605
22030
|
callLlm: null
|
|
21606
22031
|
};
|
|
21607
22032
|
});
|
|
@@ -21726,7 +22151,7 @@ async function checkStoryOversized(context, config2, chain) {
|
|
|
21726
22151
|
}
|
|
21727
22152
|
}
|
|
21728
22153
|
var init_triggers = __esm(() => {
|
|
21729
|
-
|
|
22154
|
+
init_types3();
|
|
21730
22155
|
});
|
|
21731
22156
|
|
|
21732
22157
|
// src/interaction/init.ts
|
|
@@ -21746,19 +22171,19 @@ function createInteractionPlugin(pluginName) {
|
|
|
21746
22171
|
}
|
|
21747
22172
|
async function initInteractionChain(config2, headless) {
|
|
21748
22173
|
const logger = getSafeLogger();
|
|
21749
|
-
if (headless) {
|
|
21750
|
-
logger?.debug("interaction", "Headless mode - skipping interaction system");
|
|
21751
|
-
return null;
|
|
21752
|
-
}
|
|
21753
22174
|
if (!config2.interaction) {
|
|
21754
22175
|
logger?.debug("interaction", "No interaction config - skipping interaction system");
|
|
21755
22176
|
return null;
|
|
21756
22177
|
}
|
|
22178
|
+
const pluginName = config2.interaction.plugin;
|
|
22179
|
+
if (headless && pluginName === "cli") {
|
|
22180
|
+
logger?.debug("interaction", "Headless mode with CLI plugin - skipping interaction system (stdin unavailable)");
|
|
22181
|
+
return null;
|
|
22182
|
+
}
|
|
21757
22183
|
const chain = new InteractionChain({
|
|
21758
22184
|
defaultTimeout: config2.interaction.defaults.timeout,
|
|
21759
22185
|
defaultFallback: config2.interaction.defaults.fallback
|
|
21760
22186
|
});
|
|
21761
|
-
const pluginName = config2.interaction.plugin;
|
|
21762
22187
|
try {
|
|
21763
22188
|
const plugin = createInteractionPlugin(pluginName);
|
|
21764
22189
|
chain.register(plugin, 100);
|
|
@@ -21785,7 +22210,7 @@ var init_init = __esm(() => {
|
|
|
21785
22210
|
|
|
21786
22211
|
// src/interaction/index.ts
|
|
21787
22212
|
var init_interaction = __esm(() => {
|
|
21788
|
-
|
|
22213
|
+
init_types3();
|
|
21789
22214
|
init_state();
|
|
21790
22215
|
init_cli();
|
|
21791
22216
|
init_telegram();
|
|
@@ -23598,6 +24023,68 @@ ${pluginMarkdown}` : pluginMarkdown;
|
|
|
23598
24023
|
};
|
|
23599
24024
|
});
|
|
23600
24025
|
|
|
24026
|
+
// src/agents/validation.ts
|
|
24027
|
+
function validateAgentForTier(agent, tier) {
|
|
24028
|
+
return agent.capabilities.supportedTiers.includes(tier);
|
|
24029
|
+
}
|
|
24030
|
+
|
|
24031
|
+
// src/agents/version-detection.ts
|
|
24032
|
+
async function getAgentVersion(binaryName) {
|
|
24033
|
+
try {
|
|
24034
|
+
const proc = _versionDetectionDeps.spawn([binaryName, "--version"], {
|
|
24035
|
+
stdout: "pipe",
|
|
24036
|
+
stderr: "pipe"
|
|
24037
|
+
});
|
|
24038
|
+
const exitCode = await proc.exited;
|
|
24039
|
+
if (exitCode !== 0) {
|
|
24040
|
+
return null;
|
|
24041
|
+
}
|
|
24042
|
+
const stdout = await new Response(proc.stdout).text();
|
|
24043
|
+
const versionLine = stdout.trim().split(`
|
|
24044
|
+
`)[0];
|
|
24045
|
+
const versionMatch = versionLine.match(/v?(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
|
|
24046
|
+
if (versionMatch) {
|
|
24047
|
+
return versionMatch[0];
|
|
24048
|
+
}
|
|
24049
|
+
return versionLine || null;
|
|
24050
|
+
} catch {
|
|
24051
|
+
return null;
|
|
24052
|
+
}
|
|
24053
|
+
}
|
|
24054
|
+
async function getAgentVersions() {
|
|
24055
|
+
const agents = await getInstalledAgents();
|
|
24056
|
+
const agentsByName = new Map(agents.map((a) => [a.name, a]));
|
|
24057
|
+
const { ALL_AGENTS: ALL_AGENTS2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
|
|
24058
|
+
const versions2 = await Promise.all(ALL_AGENTS2.map(async (agent) => {
|
|
24059
|
+
const version2 = agentsByName.has(agent.name) ? await getAgentVersion(agent.binary) : null;
|
|
24060
|
+
return {
|
|
24061
|
+
name: agent.name,
|
|
24062
|
+
displayName: agent.displayName,
|
|
24063
|
+
version: version2,
|
|
24064
|
+
installed: agentsByName.has(agent.name)
|
|
24065
|
+
};
|
|
24066
|
+
}));
|
|
24067
|
+
return versions2;
|
|
24068
|
+
}
|
|
24069
|
+
var _versionDetectionDeps;
|
|
24070
|
+
var init_version_detection = __esm(() => {
|
|
24071
|
+
init_registry();
|
|
24072
|
+
_versionDetectionDeps = {
|
|
24073
|
+
spawn(cmd, opts) {
|
|
24074
|
+
return Bun.spawn(cmd, opts);
|
|
24075
|
+
}
|
|
24076
|
+
};
|
|
24077
|
+
});
|
|
24078
|
+
|
|
24079
|
+
// src/agents/index.ts
|
|
24080
|
+
var init_agents = __esm(() => {
|
|
24081
|
+
init_types2();
|
|
24082
|
+
init_claude();
|
|
24083
|
+
init_registry();
|
|
24084
|
+
init_cost();
|
|
24085
|
+
init_version_detection();
|
|
24086
|
+
});
|
|
24087
|
+
|
|
23601
24088
|
// src/tdd/isolation.ts
|
|
23602
24089
|
function isTestFile(filePath) {
|
|
23603
24090
|
return TEST_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
@@ -23755,7 +24242,38 @@ async function hasCommitsForStory(workdir, storyId, maxCommits = 20) {
|
|
|
23755
24242
|
function detectMergeConflict(output) {
|
|
23756
24243
|
return output.includes("CONFLICT") || output.includes("conflict");
|
|
23757
24244
|
}
|
|
24245
|
+
async function autoCommitIfDirty(workdir, stage, role, storyId) {
|
|
24246
|
+
const logger = getSafeLogger();
|
|
24247
|
+
try {
|
|
24248
|
+
const statusProc = Bun.spawn(["git", "status", "--porcelain"], {
|
|
24249
|
+
cwd: workdir,
|
|
24250
|
+
stdout: "pipe",
|
|
24251
|
+
stderr: "pipe"
|
|
24252
|
+
});
|
|
24253
|
+
const statusOutput = await new Response(statusProc.stdout).text();
|
|
24254
|
+
await statusProc.exited;
|
|
24255
|
+
if (!statusOutput.trim())
|
|
24256
|
+
return;
|
|
24257
|
+
logger?.warn(stage, `Agent did not commit after ${role} session \u2014 auto-committing`, {
|
|
24258
|
+
role,
|
|
24259
|
+
storyId,
|
|
24260
|
+
dirtyFiles: statusOutput.trim().split(`
|
|
24261
|
+
`).length
|
|
24262
|
+
});
|
|
24263
|
+
const addProc = Bun.spawn(["git", "add", "-A"], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
|
|
24264
|
+
await addProc.exited;
|
|
24265
|
+
const commitProc = Bun.spawn(["git", "commit", "-m", `chore(${storyId}): auto-commit after ${role} session`], {
|
|
24266
|
+
cwd: workdir,
|
|
24267
|
+
stdout: "pipe",
|
|
24268
|
+
stderr: "pipe"
|
|
24269
|
+
});
|
|
24270
|
+
await commitProc.exited;
|
|
24271
|
+
} catch {}
|
|
24272
|
+
}
|
|
23758
24273
|
var GIT_TIMEOUT_MS = 1e4;
|
|
24274
|
+
var init_git = __esm(() => {
|
|
24275
|
+
init_logger2();
|
|
24276
|
+
});
|
|
23759
24277
|
// src/verification/executor.ts
|
|
23760
24278
|
async function drainWithDeadline(proc, deadlineMs) {
|
|
23761
24279
|
const EMPTY = Symbol("timeout");
|
|
@@ -24319,6 +24837,20 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
24319
24837
|
if (!rectifyResult.success && rectifyResult.pid) {
|
|
24320
24838
|
await cleanupProcessTree(rectifyResult.pid);
|
|
24321
24839
|
}
|
|
24840
|
+
if (rectifyResult.success) {
|
|
24841
|
+
logger.info("tdd", "Rectification agent session complete", {
|
|
24842
|
+
storyId: story.id,
|
|
24843
|
+
attempt: rectificationState.attempt,
|
|
24844
|
+
cost: rectifyResult.estimatedCost
|
|
24845
|
+
});
|
|
24846
|
+
} else {
|
|
24847
|
+
logger.warn("tdd", "Rectification agent session failed", {
|
|
24848
|
+
storyId: story.id,
|
|
24849
|
+
attempt: rectificationState.attempt,
|
|
24850
|
+
exitCode: rectifyResult.exitCode
|
|
24851
|
+
});
|
|
24852
|
+
}
|
|
24853
|
+
await autoCommitIfDirty(workdir, "tdd", "rectification", story.id);
|
|
24322
24854
|
const rectifyIsolation = lite ? undefined : await verifyImplementerIsolation(workdir, rectifyBeforeRef);
|
|
24323
24855
|
if (rectifyIsolation && !rectifyIsolation.passed) {
|
|
24324
24856
|
logger.error("tdd", "Rectification violated isolation", {
|
|
@@ -24344,6 +24876,11 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
24344
24876
|
testSummary.failed = newTestSummary.failed;
|
|
24345
24877
|
testSummary.passed = newTestSummary.passed;
|
|
24346
24878
|
}
|
|
24879
|
+
logger.warn("tdd", "Full suite still failing after rectification attempt", {
|
|
24880
|
+
storyId: story.id,
|
|
24881
|
+
attempt: rectificationState.attempt,
|
|
24882
|
+
remainingFailures: rectificationState.currentFailures
|
|
24883
|
+
});
|
|
24347
24884
|
}
|
|
24348
24885
|
const finalFullSuite = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, { cwd: workdir });
|
|
24349
24886
|
const finalSuitePassed = finalFullSuite.success && finalFullSuite.exitCode === 0;
|
|
@@ -24360,6 +24897,7 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
24360
24897
|
}
|
|
24361
24898
|
var init_rectification_gate = __esm(() => {
|
|
24362
24899
|
init_config();
|
|
24900
|
+
init_git();
|
|
24363
24901
|
init_verification();
|
|
24364
24902
|
init_cleanup();
|
|
24365
24903
|
init_isolation();
|
|
@@ -24698,7 +25236,22 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
|
|
|
24698
25236
|
if (!result.success && result.pid) {
|
|
24699
25237
|
await cleanupProcessTree(result.pid);
|
|
24700
25238
|
}
|
|
24701
|
-
|
|
25239
|
+
if (result.success) {
|
|
25240
|
+
logger.info("tdd", `Session complete: ${role}`, {
|
|
25241
|
+
role,
|
|
25242
|
+
storyId: story.id,
|
|
25243
|
+
durationMs: Date.now() - startTime,
|
|
25244
|
+
cost: result.estimatedCost
|
|
25245
|
+
});
|
|
25246
|
+
} else {
|
|
25247
|
+
logger.warn("tdd", `Session failed: ${role}`, {
|
|
25248
|
+
role,
|
|
25249
|
+
storyId: story.id,
|
|
25250
|
+
durationMs: Date.now() - startTime,
|
|
25251
|
+
exitCode: result.exitCode
|
|
25252
|
+
});
|
|
25253
|
+
}
|
|
25254
|
+
await autoCommitIfDirty(workdir, "tdd", role, story.id);
|
|
24702
25255
|
let isolation;
|
|
24703
25256
|
if (!skipIsolation) {
|
|
24704
25257
|
if (role === "test-writer") {
|
|
@@ -24745,42 +25298,11 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
|
|
|
24745
25298
|
estimatedCost: result.estimatedCost
|
|
24746
25299
|
};
|
|
24747
25300
|
}
|
|
24748
|
-
async function autoCommitIfDirty(workdir, role, storyId) {
|
|
24749
|
-
const logger = getLogger();
|
|
24750
|
-
try {
|
|
24751
|
-
const statusProc = Bun.spawn(["git", "status", "--porcelain"], {
|
|
24752
|
-
cwd: workdir,
|
|
24753
|
-
stdout: "pipe",
|
|
24754
|
-
stderr: "pipe"
|
|
24755
|
-
});
|
|
24756
|
-
const statusOutput = await new Response(statusProc.stdout).text();
|
|
24757
|
-
await statusProc.exited;
|
|
24758
|
-
if (!statusOutput.trim())
|
|
24759
|
-
return;
|
|
24760
|
-
logger.warn("tdd", `Agent did not commit after ${role} session \u2014 auto-committing`, {
|
|
24761
|
-
role,
|
|
24762
|
-
storyId,
|
|
24763
|
-
dirtyFiles: statusOutput.trim().split(`
|
|
24764
|
-
`).length
|
|
24765
|
-
});
|
|
24766
|
-
const addProc = Bun.spawn(["git", "add", "-A"], {
|
|
24767
|
-
cwd: workdir,
|
|
24768
|
-
stdout: "pipe",
|
|
24769
|
-
stderr: "pipe"
|
|
24770
|
-
});
|
|
24771
|
-
await addProc.exited;
|
|
24772
|
-
const commitProc = Bun.spawn(["git", "commit", "-m", `chore(${storyId}): auto-commit after ${role} session`], {
|
|
24773
|
-
cwd: workdir,
|
|
24774
|
-
stdout: "pipe",
|
|
24775
|
-
stderr: "pipe"
|
|
24776
|
-
});
|
|
24777
|
-
await commitProc.exited;
|
|
24778
|
-
} catch {}
|
|
24779
|
-
}
|
|
24780
25301
|
var init_session_runner = __esm(() => {
|
|
24781
25302
|
init_config();
|
|
24782
25303
|
init_logger2();
|
|
24783
25304
|
init_prompts2();
|
|
25305
|
+
init_git();
|
|
24784
25306
|
init_cleanup();
|
|
24785
25307
|
init_isolation();
|
|
24786
25308
|
});
|
|
@@ -24836,6 +25358,95 @@ function isValidVerdict(obj) {
|
|
|
24836
25358
|
return false;
|
|
24837
25359
|
return true;
|
|
24838
25360
|
}
|
|
25361
|
+
function coerceVerdict(obj) {
|
|
25362
|
+
try {
|
|
25363
|
+
const verdictStr = String(obj.verdict ?? "").toUpperCase();
|
|
25364
|
+
const approved = verdictStr === "PASS" || verdictStr === "APPROVED" || obj.approved === true;
|
|
25365
|
+
let passCount = 0;
|
|
25366
|
+
let failCount = 0;
|
|
25367
|
+
let allPassing = approved;
|
|
25368
|
+
const summary = obj.verification_summary;
|
|
25369
|
+
if (summary?.test_results && typeof summary.test_results === "string") {
|
|
25370
|
+
const match = summary.test_results.match(/(\d+)\/(\d+)/);
|
|
25371
|
+
if (match) {
|
|
25372
|
+
passCount = Number.parseInt(match[1], 10);
|
|
25373
|
+
const total = Number.parseInt(match[2], 10);
|
|
25374
|
+
failCount = total - passCount;
|
|
25375
|
+
allPassing = failCount === 0;
|
|
25376
|
+
}
|
|
25377
|
+
}
|
|
25378
|
+
if (obj.tests && typeof obj.tests === "object") {
|
|
25379
|
+
const t = obj.tests;
|
|
25380
|
+
if (typeof t.passCount === "number")
|
|
25381
|
+
passCount = t.passCount;
|
|
25382
|
+
if (typeof t.failCount === "number")
|
|
25383
|
+
failCount = t.failCount;
|
|
25384
|
+
if (typeof t.allPassing === "boolean")
|
|
25385
|
+
allPassing = t.allPassing;
|
|
25386
|
+
}
|
|
25387
|
+
const criteria = [];
|
|
25388
|
+
let allMet = approved;
|
|
25389
|
+
const acReview = obj.acceptance_criteria_review;
|
|
25390
|
+
if (acReview) {
|
|
25391
|
+
for (const [key, val] of Object.entries(acReview)) {
|
|
25392
|
+
if (key.startsWith("criterion") && val && typeof val === "object") {
|
|
25393
|
+
const c = val;
|
|
25394
|
+
const met = String(c.status ?? "").toUpperCase() === "SATISFIED" || c.met === true;
|
|
25395
|
+
criteria.push({
|
|
25396
|
+
criterion: String(c.name ?? c.criterion ?? key),
|
|
25397
|
+
met,
|
|
25398
|
+
note: c.evidence ? String(c.evidence).slice(0, 200) : undefined
|
|
25399
|
+
});
|
|
25400
|
+
if (!met)
|
|
25401
|
+
allMet = false;
|
|
25402
|
+
}
|
|
25403
|
+
}
|
|
25404
|
+
}
|
|
25405
|
+
if (obj.acceptanceCriteria && typeof obj.acceptanceCriteria === "object") {
|
|
25406
|
+
const ac = obj.acceptanceCriteria;
|
|
25407
|
+
if (typeof ac.allMet === "boolean")
|
|
25408
|
+
allMet = ac.allMet;
|
|
25409
|
+
if (Array.isArray(ac.criteria)) {
|
|
25410
|
+
for (const c of ac.criteria) {
|
|
25411
|
+
if (c && typeof c === "object") {
|
|
25412
|
+
criteria.push(c);
|
|
25413
|
+
}
|
|
25414
|
+
}
|
|
25415
|
+
}
|
|
25416
|
+
}
|
|
25417
|
+
if (criteria.length === 0 && summary?.acceptance_criteria && typeof summary.acceptance_criteria === "string") {
|
|
25418
|
+
const acMatch = summary.acceptance_criteria.match(/(\d+)\/(\d+)/);
|
|
25419
|
+
if (acMatch) {
|
|
25420
|
+
const met = Number.parseInt(acMatch[1], 10);
|
|
25421
|
+
const total = Number.parseInt(acMatch[2], 10);
|
|
25422
|
+
allMet = met === total;
|
|
25423
|
+
}
|
|
25424
|
+
}
|
|
25425
|
+
let rating = "acceptable";
|
|
25426
|
+
const qualityStr = summary?.code_quality ? String(summary.code_quality).toLowerCase() : obj.quality && typeof obj.quality === "object" ? String(obj.quality.rating ?? "acceptable").toLowerCase() : "acceptable";
|
|
25427
|
+
if (qualityStr === "high" || qualityStr === "good")
|
|
25428
|
+
rating = "good";
|
|
25429
|
+
else if (qualityStr === "low" || qualityStr === "poor")
|
|
25430
|
+
rating = "poor";
|
|
25431
|
+
return {
|
|
25432
|
+
version: 1,
|
|
25433
|
+
approved,
|
|
25434
|
+
tests: { allPassing, passCount, failCount },
|
|
25435
|
+
testModifications: {
|
|
25436
|
+
detected: false,
|
|
25437
|
+
files: [],
|
|
25438
|
+
legitimate: true,
|
|
25439
|
+
reasoning: "Not assessed in free-form verdict"
|
|
25440
|
+
},
|
|
25441
|
+
acceptanceCriteria: { allMet, criteria },
|
|
25442
|
+
quality: { rating, issues: [] },
|
|
25443
|
+
fixes: Array.isArray(obj.fixes) ? obj.fixes : [],
|
|
25444
|
+
reasoning: typeof obj.reasoning === "string" ? obj.reasoning : typeof obj.overall_status === "string" ? obj.overall_status : summary?.overall_status ? String(summary.overall_status) : `Coerced from free-form verdict: ${verdictStr}`
|
|
25445
|
+
};
|
|
25446
|
+
} catch {
|
|
25447
|
+
return null;
|
|
25448
|
+
}
|
|
25449
|
+
}
|
|
24839
25450
|
async function readVerdict(workdir) {
|
|
24840
25451
|
const logger = getLogger();
|
|
24841
25452
|
const verdictPath = path8.join(workdir, VERDICT_FILE);
|
|
@@ -24855,13 +25466,26 @@ async function readVerdict(workdir) {
|
|
|
24855
25466
|
});
|
|
24856
25467
|
return null;
|
|
24857
25468
|
}
|
|
24858
|
-
if (
|
|
24859
|
-
|
|
24860
|
-
|
|
24861
|
-
|
|
24862
|
-
|
|
25469
|
+
if (isValidVerdict(parsed)) {
|
|
25470
|
+
return parsed;
|
|
25471
|
+
}
|
|
25472
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
25473
|
+
const coerced = coerceVerdict(parsed);
|
|
25474
|
+
if (coerced) {
|
|
25475
|
+
logger.info("tdd", "Coerced free-form verdict to structured format", {
|
|
25476
|
+
path: verdictPath,
|
|
25477
|
+
approved: coerced.approved,
|
|
25478
|
+
passCount: coerced.tests.passCount,
|
|
25479
|
+
failCount: coerced.tests.failCount
|
|
25480
|
+
});
|
|
25481
|
+
return coerced;
|
|
25482
|
+
}
|
|
24863
25483
|
}
|
|
24864
|
-
|
|
25484
|
+
logger.warn("tdd", "Verifier verdict file missing required fields and coercion failed \u2014 ignoring", {
|
|
25485
|
+
path: verdictPath,
|
|
25486
|
+
content: JSON.stringify(parsed).slice(0, 500)
|
|
25487
|
+
});
|
|
25488
|
+
return null;
|
|
24865
25489
|
} catch (err) {
|
|
24866
25490
|
logger.warn("tdd", "Failed to read verifier verdict file \u2014 ignoring", {
|
|
24867
25491
|
path: verdictPath,
|
|
@@ -25182,6 +25806,7 @@ var init_orchestrator2 = __esm(() => {
|
|
|
25182
25806
|
init_config();
|
|
25183
25807
|
init_greenfield();
|
|
25184
25808
|
init_logger2();
|
|
25809
|
+
init_git();
|
|
25185
25810
|
init_verification();
|
|
25186
25811
|
init_rectification_gate();
|
|
25187
25812
|
init_session_runner();
|
|
@@ -25227,34 +25852,6 @@ function routeTddFailure(failureCategory, isLiteMode, ctx, reviewReason) {
|
|
|
25227
25852
|
reason: reviewReason || "Three-session TDD requires review"
|
|
25228
25853
|
};
|
|
25229
25854
|
}
|
|
25230
|
-
async function autoCommitIfDirty2(workdir, role, storyId) {
|
|
25231
|
-
try {
|
|
25232
|
-
const statusProc = Bun.spawn(["git", "status", "--porcelain"], {
|
|
25233
|
-
cwd: workdir,
|
|
25234
|
-
stdout: "pipe",
|
|
25235
|
-
stderr: "pipe"
|
|
25236
|
-
});
|
|
25237
|
-
const statusOutput = await new Response(statusProc.stdout).text();
|
|
25238
|
-
await statusProc.exited;
|
|
25239
|
-
if (!statusOutput.trim())
|
|
25240
|
-
return;
|
|
25241
|
-
const logger = getLogger();
|
|
25242
|
-
logger.warn("execution", `Agent did not commit after ${role} session \u2014 auto-committing`, {
|
|
25243
|
-
role,
|
|
25244
|
-
storyId,
|
|
25245
|
-
dirtyFiles: statusOutput.trim().split(`
|
|
25246
|
-
`).length
|
|
25247
|
-
});
|
|
25248
|
-
const addProc = Bun.spawn(["git", "add", "-A"], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
|
|
25249
|
-
await addProc.exited;
|
|
25250
|
-
const commitProc = Bun.spawn(["git", "commit", "-m", `chore(${storyId}): auto-commit after ${role} session`], {
|
|
25251
|
-
cwd: workdir,
|
|
25252
|
-
stdout: "pipe",
|
|
25253
|
-
stderr: "pipe"
|
|
25254
|
-
});
|
|
25255
|
-
await commitProc.exited;
|
|
25256
|
-
} catch {}
|
|
25257
|
-
}
|
|
25258
25855
|
var executionStage, _executionDeps;
|
|
25259
25856
|
var init_execution = __esm(() => {
|
|
25260
25857
|
init_agents();
|
|
@@ -25262,6 +25859,7 @@ var init_execution = __esm(() => {
|
|
|
25262
25859
|
init_triggers();
|
|
25263
25860
|
init_logger2();
|
|
25264
25861
|
init_tdd();
|
|
25862
|
+
init_git();
|
|
25265
25863
|
executionStage = {
|
|
25266
25864
|
name: "execution",
|
|
25267
25865
|
enabled: () => true,
|
|
@@ -25336,7 +25934,7 @@ var init_execution = __esm(() => {
|
|
|
25336
25934
|
dangerouslySkipPermissions: ctx.config.execution.dangerouslySkipPermissions
|
|
25337
25935
|
});
|
|
25338
25936
|
ctx.agentResult = result;
|
|
25339
|
-
await
|
|
25937
|
+
await autoCommitIfDirty(ctx.workdir, "execution", "single-session", ctx.story.id);
|
|
25340
25938
|
const combinedOutput = (result.output ?? "") + (result.stderr ?? "");
|
|
25341
25939
|
if (_executionDeps.detectMergeConflict(combinedOutput) && ctx.interaction && isTriggerEnabled("merge-conflict", ctx.config)) {
|
|
25342
25940
|
const shouldProceed = await _executionDeps.checkMergeConflict({ featureName: ctx.prd.feature, storyId: ctx.story.id }, ctx.config, ctx.interaction);
|
|
@@ -25903,10 +26501,17 @@ ${rectificationPrompt}`;
|
|
|
25903
26501
|
timeoutSeconds: config2.execution.sessionTimeoutSeconds,
|
|
25904
26502
|
dangerouslySkipPermissions: config2.execution.dangerouslySkipPermissions
|
|
25905
26503
|
});
|
|
25906
|
-
if (
|
|
26504
|
+
if (agentResult.success) {
|
|
26505
|
+
logger?.info("rectification", `Agent ${label} session complete`, {
|
|
26506
|
+
storyId: story.id,
|
|
26507
|
+
attempt: rectificationState.attempt,
|
|
26508
|
+
cost: agentResult.estimatedCost
|
|
26509
|
+
});
|
|
26510
|
+
} else {
|
|
25907
26511
|
logger?.warn("rectification", `Agent ${label} session failed`, {
|
|
25908
26512
|
storyId: story.id,
|
|
25909
|
-
attempt: rectificationState.attempt
|
|
26513
|
+
attempt: rectificationState.attempt,
|
|
26514
|
+
exitCode: agentResult.exitCode
|
|
25910
26515
|
});
|
|
25911
26516
|
}
|
|
25912
26517
|
const retryVerification = await fullSuite({
|
|
@@ -25938,6 +26543,11 @@ ${rectificationPrompt}`;
|
|
|
25938
26543
|
testSummary.failed = newTestSummary.failed;
|
|
25939
26544
|
testSummary.passed = newTestSummary.passed;
|
|
25940
26545
|
}
|
|
26546
|
+
logger?.warn("rectification", `${label} still failing after attempt`, {
|
|
26547
|
+
storyId: story.id,
|
|
26548
|
+
attempt: rectificationState.attempt,
|
|
26549
|
+
remainingFailures: rectificationState.currentFailures
|
|
26550
|
+
});
|
|
25941
26551
|
}
|
|
25942
26552
|
if (rectificationState.attempt >= rectificationConfig.maxRetries) {
|
|
25943
26553
|
logger?.warn("rectification", `${label} exhausted max retries`, {
|
|
@@ -26362,6 +26972,7 @@ function reverseMapTestToSource(testFiles, workdir) {
|
|
|
26362
26972
|
}
|
|
26363
26973
|
var _smartRunnerDeps;
|
|
26364
26974
|
var init_smart_runner = __esm(() => {
|
|
26975
|
+
init_git();
|
|
26365
26976
|
_smartRunnerDeps = {
|
|
26366
26977
|
getChangedSourceFiles,
|
|
26367
26978
|
mapSourceToTests,
|
|
@@ -26597,6 +27208,7 @@ async function runDecompose(story, prd, config2, _workdir) {
|
|
|
26597
27208
|
}
|
|
26598
27209
|
var routingStage, _routingDeps;
|
|
26599
27210
|
var init_routing2 = __esm(() => {
|
|
27211
|
+
init_registry();
|
|
26600
27212
|
init_greenfield();
|
|
26601
27213
|
init_builder2();
|
|
26602
27214
|
init_triggers();
|
|
@@ -26609,6 +27221,8 @@ var init_routing2 = __esm(() => {
|
|
|
26609
27221
|
enabled: () => true,
|
|
26610
27222
|
async execute(ctx) {
|
|
26611
27223
|
const logger = getLogger();
|
|
27224
|
+
const agentName = ctx.config.execution?.agent ?? "claude";
|
|
27225
|
+
const adapter = _routingDeps.getAgent(agentName);
|
|
26612
27226
|
const hasExistingRouting = ctx.story.routing !== undefined;
|
|
26613
27227
|
const hasContentHash = ctx.story.routing?.contentHash !== undefined;
|
|
26614
27228
|
let currentHash;
|
|
@@ -26620,10 +27234,10 @@ var init_routing2 = __esm(() => {
|
|
|
26620
27234
|
const isCacheHit = hasExistingRouting && (!hasContentHash || hashMatch);
|
|
26621
27235
|
let routing;
|
|
26622
27236
|
if (isCacheHit) {
|
|
26623
|
-
routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config }, ctx.workdir, ctx.plugins);
|
|
27237
|
+
routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config, adapter }, ctx.workdir, ctx.plugins);
|
|
26624
27238
|
if (ctx.story.routing?.complexity)
|
|
26625
27239
|
routing.complexity = ctx.story.routing.complexity;
|
|
26626
|
-
if (ctx.story.routing?.testStrategy)
|
|
27240
|
+
if (!hasContentHash && ctx.story.routing?.testStrategy)
|
|
26627
27241
|
routing.testStrategy = ctx.story.routing.testStrategy;
|
|
26628
27242
|
if (ctx.story.routing?.modelTier) {
|
|
26629
27243
|
routing.modelTier = ctx.story.routing.modelTier;
|
|
@@ -26631,7 +27245,7 @@ var init_routing2 = __esm(() => {
|
|
|
26631
27245
|
routing.modelTier = _routingDeps.complexityToModelTier(routing.complexity, ctx.config);
|
|
26632
27246
|
}
|
|
26633
27247
|
} else {
|
|
26634
|
-
routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config }, ctx.workdir, ctx.plugins);
|
|
27248
|
+
routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config, adapter }, ctx.workdir, ctx.plugins);
|
|
26635
27249
|
currentHash = currentHash ?? _routingDeps.computeStoryContentHash(ctx.story);
|
|
26636
27250
|
ctx.story.routing = {
|
|
26637
27251
|
...ctx.story.routing ?? {},
|
|
@@ -26720,7 +27334,8 @@ var init_routing2 = __esm(() => {
|
|
|
26720
27334
|
computeStoryContentHash,
|
|
26721
27335
|
applyDecomposition,
|
|
26722
27336
|
runDecompose,
|
|
26723
|
-
checkStoryOversized
|
|
27337
|
+
checkStoryOversized,
|
|
27338
|
+
getAgent
|
|
26724
27339
|
};
|
|
26725
27340
|
});
|
|
26726
27341
|
|
|
@@ -26911,6 +27526,28 @@ var init_stages = __esm(() => {
|
|
|
26911
27526
|
postRunPipeline = [acceptanceStage];
|
|
26912
27527
|
});
|
|
26913
27528
|
|
|
27529
|
+
// src/plugins/plugin-logger.ts
|
|
27530
|
+
function createPluginLogger(pluginName) {
|
|
27531
|
+
const stage = `plugin:${pluginName}`;
|
|
27532
|
+
return {
|
|
27533
|
+
error(message, data) {
|
|
27534
|
+
getSafeLogger()?.error(stage, message, data);
|
|
27535
|
+
},
|
|
27536
|
+
warn(message, data) {
|
|
27537
|
+
getSafeLogger()?.warn(stage, message, data);
|
|
27538
|
+
},
|
|
27539
|
+
info(message, data) {
|
|
27540
|
+
getSafeLogger()?.info(stage, message, data);
|
|
27541
|
+
},
|
|
27542
|
+
debug(message, data) {
|
|
27543
|
+
getSafeLogger()?.debug(stage, message, data);
|
|
27544
|
+
}
|
|
27545
|
+
};
|
|
27546
|
+
}
|
|
27547
|
+
var init_plugin_logger = __esm(() => {
|
|
27548
|
+
init_logger2();
|
|
27549
|
+
});
|
|
27550
|
+
|
|
26914
27551
|
// src/plugins/registry.ts
|
|
26915
27552
|
class PluginRegistry {
|
|
26916
27553
|
plugins;
|
|
@@ -27322,7 +27959,8 @@ async function loadAndValidatePlugin(initialModulePath, config2, allowedRoots =
|
|
|
27322
27959
|
}
|
|
27323
27960
|
if (validated.setup) {
|
|
27324
27961
|
try {
|
|
27325
|
-
|
|
27962
|
+
const pluginLogger = createPluginLogger(validated.name);
|
|
27963
|
+
await validated.setup(config2, pluginLogger);
|
|
27326
27964
|
} catch (error48) {
|
|
27327
27965
|
const logger = getSafeLogger6();
|
|
27328
27966
|
logger?.error("plugins", `Plugin '${validated.name}' setup failed`, { error: error48 });
|
|
@@ -27353,6 +27991,7 @@ var _pluginErrorSink = (...args) => console.error(...args);
|
|
|
27353
27991
|
var init_loader5 = __esm(() => {
|
|
27354
27992
|
init_logger2();
|
|
27355
27993
|
init_path_security();
|
|
27994
|
+
init_plugin_logger();
|
|
27356
27995
|
init_registry2();
|
|
27357
27996
|
init_validator();
|
|
27358
27997
|
});
|
|
@@ -27480,26 +28119,27 @@ async function checkPRDValid(prd) {
|
|
|
27480
28119
|
message: passed ? "PRD structure is valid" : errors3.join("; ")
|
|
27481
28120
|
};
|
|
27482
28121
|
}
|
|
27483
|
-
async function
|
|
28122
|
+
async function checkAgentCLI(config2) {
|
|
28123
|
+
const agent = config2.execution?.agent || "claude";
|
|
27484
28124
|
try {
|
|
27485
|
-
const proc =
|
|
28125
|
+
const proc = _deps6.spawn([agent, "--version"], {
|
|
27486
28126
|
stdout: "pipe",
|
|
27487
28127
|
stderr: "pipe"
|
|
27488
28128
|
});
|
|
27489
28129
|
const exitCode = await proc.exited;
|
|
27490
28130
|
const passed = exitCode === 0;
|
|
27491
28131
|
return {
|
|
27492
|
-
name: "
|
|
28132
|
+
name: "agent-cli-available",
|
|
27493
28133
|
tier: "blocker",
|
|
27494
28134
|
passed,
|
|
27495
|
-
message: passed ?
|
|
28135
|
+
message: passed ? `${agent} CLI is available` : `${agent} CLI not found. Install the ${agent} binary.`
|
|
27496
28136
|
};
|
|
27497
28137
|
} catch {
|
|
27498
28138
|
return {
|
|
27499
|
-
name: "
|
|
28139
|
+
name: "agent-cli-available",
|
|
27500
28140
|
tier: "blocker",
|
|
27501
28141
|
passed: false,
|
|
27502
|
-
message:
|
|
28142
|
+
message: `${agent} CLI not found in PATH. Install the ${agent} binary.`
|
|
27503
28143
|
};
|
|
27504
28144
|
}
|
|
27505
28145
|
}
|
|
@@ -27653,7 +28293,12 @@ async function checkGitUserConfigured(workdir) {
|
|
|
27653
28293
|
message: passed ? "Git user is configured" : !hasName && !hasEmail ? "Git user.name and user.email not configured" : !hasName ? "Git user.name not configured" : "Git user.email not configured"
|
|
27654
28294
|
};
|
|
27655
28295
|
}
|
|
27656
|
-
var
|
|
28296
|
+
var _deps6;
|
|
28297
|
+
var init_checks_blockers = __esm(() => {
|
|
28298
|
+
_deps6 = {
|
|
28299
|
+
spawn: Bun.spawn
|
|
28300
|
+
};
|
|
28301
|
+
});
|
|
27657
28302
|
|
|
27658
28303
|
// src/precheck/checks-warnings.ts
|
|
27659
28304
|
import { existsSync as existsSync23 } from "fs";
|
|
@@ -27781,10 +28426,55 @@ async function checkPromptOverrideFiles(config2, workdir) {
|
|
|
27781
28426
|
}
|
|
27782
28427
|
var init_checks_warnings = () => {};
|
|
27783
28428
|
|
|
28429
|
+
// src/precheck/checks-agents.ts
|
|
28430
|
+
async function checkMultiAgentHealth() {
|
|
28431
|
+
try {
|
|
28432
|
+
const versions2 = await getAgentVersions();
|
|
28433
|
+
const installed = versions2.filter((v) => v.installed);
|
|
28434
|
+
const notInstalled = versions2.filter((v) => !v.installed);
|
|
28435
|
+
const lines = [];
|
|
28436
|
+
if (installed.length > 0) {
|
|
28437
|
+
lines.push(`Installed agents (${installed.length}):`);
|
|
28438
|
+
for (const agent of installed) {
|
|
28439
|
+
const versionStr = agent.version ? ` v${agent.version}` : " (version unknown)";
|
|
28440
|
+
lines.push(` \u2022 ${agent.displayName}${versionStr}`);
|
|
28441
|
+
}
|
|
28442
|
+
} else {
|
|
28443
|
+
lines.push("No additional agents detected (using default configured agent)");
|
|
28444
|
+
}
|
|
28445
|
+
if (notInstalled.length > 0) {
|
|
28446
|
+
lines.push(`
|
|
28447
|
+
Available but not installed (${notInstalled.length}):`);
|
|
28448
|
+
for (const agent of notInstalled) {
|
|
28449
|
+
lines.push(` \u2022 ${agent.displayName}`);
|
|
28450
|
+
}
|
|
28451
|
+
}
|
|
28452
|
+
const message = lines.join(`
|
|
28453
|
+
`);
|
|
28454
|
+
return {
|
|
28455
|
+
name: "multi-agent-health",
|
|
28456
|
+
tier: "warning",
|
|
28457
|
+
passed: true,
|
|
28458
|
+
message
|
|
28459
|
+
};
|
|
28460
|
+
} catch (error48) {
|
|
28461
|
+
return {
|
|
28462
|
+
name: "multi-agent-health",
|
|
28463
|
+
tier: "warning",
|
|
28464
|
+
passed: true,
|
|
28465
|
+
message: `Agent detection: ${error48 instanceof Error ? error48.message : "Unknown error"}`
|
|
28466
|
+
};
|
|
28467
|
+
}
|
|
28468
|
+
}
|
|
28469
|
+
var init_checks_agents = __esm(() => {
|
|
28470
|
+
init_version_detection();
|
|
28471
|
+
});
|
|
28472
|
+
|
|
27784
28473
|
// src/precheck/checks.ts
|
|
27785
28474
|
var init_checks3 = __esm(() => {
|
|
27786
28475
|
init_checks_blockers();
|
|
27787
28476
|
init_checks_warnings();
|
|
28477
|
+
init_checks_agents();
|
|
27788
28478
|
});
|
|
27789
28479
|
|
|
27790
28480
|
// src/precheck/story-size-gate.ts
|
|
@@ -27903,7 +28593,7 @@ async function runPrecheck(config2, prd, options) {
|
|
|
27903
28593
|
() => checkWorkingTreeClean(workdir),
|
|
27904
28594
|
() => checkStaleLock(workdir),
|
|
27905
28595
|
() => checkPRDValid(prd),
|
|
27906
|
-
() =>
|
|
28596
|
+
() => checkAgentCLI(config2),
|
|
27907
28597
|
() => checkDependenciesInstalled(workdir),
|
|
27908
28598
|
() => checkTestCommand(config2),
|
|
27909
28599
|
() => checkLintCommand(config2),
|
|
@@ -27930,7 +28620,8 @@ async function runPrecheck(config2, prd, options) {
|
|
|
27930
28620
|
() => checkPendingStories(prd),
|
|
27931
28621
|
() => checkOptionalCommands(config2, workdir),
|
|
27932
28622
|
() => checkGitignoreCoversNax(workdir),
|
|
27933
|
-
() => checkPromptOverrideFiles(config2, workdir)
|
|
28623
|
+
() => checkPromptOverrideFiles(config2, workdir),
|
|
28624
|
+
() => checkMultiAgentHealth()
|
|
27934
28625
|
];
|
|
27935
28626
|
for (const checkFn of tier2Checks) {
|
|
27936
28627
|
const result = await checkFn();
|
|
@@ -29080,6 +29771,7 @@ var init_run_initialization = __esm(() => {
|
|
|
29080
29771
|
init_errors3();
|
|
29081
29772
|
init_logger2();
|
|
29082
29773
|
init_prd();
|
|
29774
|
+
init_git();
|
|
29083
29775
|
});
|
|
29084
29776
|
|
|
29085
29777
|
// src/execution/lifecycle/run-setup.ts
|
|
@@ -30663,6 +31355,7 @@ var init_iteration_runner = __esm(() => {
|
|
|
30663
31355
|
init_logger2();
|
|
30664
31356
|
init_runner();
|
|
30665
31357
|
init_stages();
|
|
31358
|
+
init_git();
|
|
30666
31359
|
init_dry_run();
|
|
30667
31360
|
init_pipeline_result_handler();
|
|
30668
31361
|
});
|
|
@@ -31231,6 +31924,7 @@ var _regressionDeps;
|
|
|
31231
31924
|
var init_run_regression = __esm(() => {
|
|
31232
31925
|
init_logger2();
|
|
31233
31926
|
init_prd();
|
|
31927
|
+
init_git();
|
|
31234
31928
|
init_verification();
|
|
31235
31929
|
init_rectification_loop();
|
|
31236
31930
|
init_runners();
|
|
@@ -62310,9 +63004,6 @@ var {
|
|
|
62310
63004
|
Help
|
|
62311
63005
|
} = import__.default;
|
|
62312
63006
|
|
|
62313
|
-
// bin/nax.ts
|
|
62314
|
-
init_agents();
|
|
62315
|
-
|
|
62316
63007
|
// src/cli/analyze.ts
|
|
62317
63008
|
init_acceptance();
|
|
62318
63009
|
init_registry();
|
|
@@ -62601,7 +63292,14 @@ async function reclassifyWithLLM(story, storySpec, workdir, codebaseContext, con
|
|
|
62601
63292
|
throw new Error(`Agent "${agentName}" not found`);
|
|
62602
63293
|
const modelTier = config2.analyze.model;
|
|
62603
63294
|
const modelDef = resolveModel(config2.models[modelTier]);
|
|
62604
|
-
const result = await adapter.decompose({
|
|
63295
|
+
const result = await adapter.decompose({
|
|
63296
|
+
specContent: storySpec,
|
|
63297
|
+
workdir,
|
|
63298
|
+
codebaseContext,
|
|
63299
|
+
modelTier,
|
|
63300
|
+
modelDef,
|
|
63301
|
+
config: config2
|
|
63302
|
+
});
|
|
62605
63303
|
if (result.stories.length === 0)
|
|
62606
63304
|
return story;
|
|
62607
63305
|
const ds = result.stories[0];
|
|
@@ -62702,7 +63400,7 @@ async function decomposeLLM(specContent, workdir, config2, logger) {
|
|
|
62702
63400
|
throw new Error(`Agent "${agentName}" not found`);
|
|
62703
63401
|
const modelTier = config2.analyze.model;
|
|
62704
63402
|
const modelDef = resolveModel(config2.models[modelTier]);
|
|
62705
|
-
const result = await adapter.decompose({ specContent, workdir, codebaseContext, modelTier, modelDef });
|
|
63403
|
+
const result = await adapter.decompose({ specContent, workdir, codebaseContext, modelTier, modelDef, config: config2 });
|
|
62706
63404
|
logger.info("cli", "[OK] Agent decompose complete", { storiesCount: result.stories.length });
|
|
62707
63405
|
return result.stories.map((ds) => {
|
|
62708
63406
|
let testStrategy;
|
|
@@ -62824,7 +63522,8 @@ async function planCommand(prompt, workdir, config2, options = {}) {
|
|
|
62824
63522
|
codebaseContext,
|
|
62825
63523
|
inputFile: options.from,
|
|
62826
63524
|
modelTier,
|
|
62827
|
-
modelDef
|
|
63525
|
+
modelDef,
|
|
63526
|
+
config: config2
|
|
62828
63527
|
};
|
|
62829
63528
|
const adapter = new ClaudeCodeAdapter;
|
|
62830
63529
|
logger.info("cli", interactive ? "Starting interactive planning session..." : `Reading from ${options.from}...`, {
|
|
@@ -64425,6 +65124,25 @@ var claudeGenerator = {
|
|
|
64425
65124
|
generate: generateClaudeConfig
|
|
64426
65125
|
};
|
|
64427
65126
|
|
|
65127
|
+
// src/context/generators/codex.ts
|
|
65128
|
+
function generateCodexConfig(context) {
|
|
65129
|
+
const header = `# Codex Instructions
|
|
65130
|
+
|
|
65131
|
+
This file is auto-generated from \`nax/context.md\`.
|
|
65132
|
+
DO NOT EDIT MANUALLY \u2014 run \`nax generate\` to regenerate.
|
|
65133
|
+
|
|
65134
|
+
---
|
|
65135
|
+
|
|
65136
|
+
`;
|
|
65137
|
+
const metaSection = context.metadata ? formatMetadataSection(context.metadata) : "";
|
|
65138
|
+
return header + metaSection + context.markdown;
|
|
65139
|
+
}
|
|
65140
|
+
var codexGenerator = {
|
|
65141
|
+
name: "codex",
|
|
65142
|
+
outputFile: "codex.md",
|
|
65143
|
+
generate: generateCodexConfig
|
|
65144
|
+
};
|
|
65145
|
+
|
|
64428
65146
|
// src/context/generators/cursor.ts
|
|
64429
65147
|
function generateCursorRules(context) {
|
|
64430
65148
|
const header = `# Project Rules
|
|
@@ -64444,6 +65162,25 @@ var cursorGenerator = {
|
|
|
64444
65162
|
generate: generateCursorRules
|
|
64445
65163
|
};
|
|
64446
65164
|
|
|
65165
|
+
// src/context/generators/gemini.ts
|
|
65166
|
+
function generateGeminiConfig(context) {
|
|
65167
|
+
const header = `# Gemini CLI Context
|
|
65168
|
+
|
|
65169
|
+
This file is auto-generated from \`nax/context.md\`.
|
|
65170
|
+
DO NOT EDIT MANUALLY \u2014 run \`nax generate\` to regenerate.
|
|
65171
|
+
|
|
65172
|
+
---
|
|
65173
|
+
|
|
65174
|
+
`;
|
|
65175
|
+
const metaSection = context.metadata ? formatMetadataSection(context.metadata) : "";
|
|
65176
|
+
return header + metaSection + context.markdown;
|
|
65177
|
+
}
|
|
65178
|
+
var geminiGenerator = {
|
|
65179
|
+
name: "gemini",
|
|
65180
|
+
outputFile: "GEMINI.md",
|
|
65181
|
+
generate: generateGeminiConfig
|
|
65182
|
+
};
|
|
65183
|
+
|
|
64447
65184
|
// src/context/generators/opencode.ts
|
|
64448
65185
|
function generateOpencodeConfig(context) {
|
|
64449
65186
|
const header = `# Agent Instructions
|
|
@@ -64487,10 +65224,12 @@ var windsurfGenerator = {
|
|
|
64487
65224
|
// src/context/generator.ts
|
|
64488
65225
|
var GENERATORS = {
|
|
64489
65226
|
claude: claudeGenerator,
|
|
65227
|
+
codex: codexGenerator,
|
|
64490
65228
|
opencode: opencodeGenerator,
|
|
64491
65229
|
cursor: cursorGenerator,
|
|
64492
65230
|
windsurf: windsurfGenerator,
|
|
64493
|
-
aider: aiderGenerator
|
|
65231
|
+
aider: aiderGenerator,
|
|
65232
|
+
gemini: geminiGenerator
|
|
64494
65233
|
};
|
|
64495
65234
|
async function loadContextContent(options, config2) {
|
|
64496
65235
|
if (!existsSync18(options.contextPath)) {
|
|
@@ -64542,7 +65281,7 @@ async function generateAll(options, config2) {
|
|
|
64542
65281
|
}
|
|
64543
65282
|
|
|
64544
65283
|
// src/cli/generate.ts
|
|
64545
|
-
var VALID_AGENTS = ["claude", "opencode", "cursor", "windsurf", "aider"];
|
|
65284
|
+
var VALID_AGENTS = ["claude", "codex", "opencode", "cursor", "windsurf", "aider", "gemini"];
|
|
64546
65285
|
async function generateCommand(options) {
|
|
64547
65286
|
const workdir = process.cwd();
|
|
64548
65287
|
const contextPath = options.context ? join24(workdir, options.context) : join24(workdir, "nax/context.md");
|
|
@@ -64630,19 +65369,19 @@ var FIELD_DESCRIPTIONS = {
|
|
|
64630
65369
|
"models.fast": "Fast model for lightweight tasks (e.g., haiku)",
|
|
64631
65370
|
"models.balanced": "Balanced model for general coding (e.g., sonnet)",
|
|
64632
65371
|
"models.powerful": "Powerful model for complex tasks (e.g., opus)",
|
|
64633
|
-
autoMode: "Auto mode configuration for agent orchestration",
|
|
65372
|
+
autoMode: "Auto mode configuration for agent orchestration. Enables multi-agent routing with model tier selection per task complexity and escalation on failures.",
|
|
64634
65373
|
"autoMode.enabled": "Enable automatic agent selection and escalation",
|
|
64635
|
-
"autoMode.defaultAgent": "Default agent to use
|
|
64636
|
-
"autoMode.fallbackOrder":
|
|
64637
|
-
"autoMode.complexityRouting": "Model tier
|
|
64638
|
-
"autoMode.complexityRouting.simple": "Model tier for simple tasks",
|
|
64639
|
-
"autoMode.complexityRouting.medium": "Model tier for medium tasks",
|
|
64640
|
-
"autoMode.complexityRouting.complex": "Model tier for complex tasks",
|
|
64641
|
-
"autoMode.complexityRouting.expert": "Model tier for expert tasks",
|
|
64642
|
-
"autoMode.escalation": "Escalation settings for failed stories",
|
|
65374
|
+
"autoMode.defaultAgent": "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.",
|
|
65375
|
+
"autoMode.fallbackOrder": '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.',
|
|
65376
|
+
"autoMode.complexityRouting": "Model tier routing rules mapped to story complexity levels. Determines which model (fast/balanced/powerful) to use based on task complexity: simple \u2192 fast, medium \u2192 balanced, complex \u2192 powerful, expert \u2192 powerful.",
|
|
65377
|
+
"autoMode.complexityRouting.simple": "Model tier for simple tasks (low complexity, straightforward changes)",
|
|
65378
|
+
"autoMode.complexityRouting.medium": "Model tier for medium tasks (moderate complexity, multi-file changes)",
|
|
65379
|
+
"autoMode.complexityRouting.complex": "Model tier for complex tasks (high complexity, architectural decisions)",
|
|
65380
|
+
"autoMode.complexityRouting.expert": "Model tier for expert tasks (highest complexity, novel problems, design patterns)",
|
|
65381
|
+
"autoMode.escalation": "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.",
|
|
64643
65382
|
"autoMode.escalation.enabled": "Enable tier escalation on failure",
|
|
64644
|
-
"autoMode.escalation.tierOrder":
|
|
64645
|
-
"autoMode.escalation.escalateEntireBatch": "
|
|
65383
|
+
"autoMode.escalation.tierOrder": '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.',
|
|
65384
|
+
"autoMode.escalation.escalateEntireBatch": "When enabled, escalate all stories in a batch if one fails. When disabled, only the failing story escalates (allows parallel attempts at different tiers).",
|
|
64646
65385
|
routing: "Model routing strategy configuration",
|
|
64647
65386
|
"routing.strategy": "Routing strategy: keyword | llm | manual | adaptive | custom",
|
|
64648
65387
|
"routing.customStrategyPath": "Path to custom routing strategy (if strategy=custom)",
|
|
@@ -64951,8 +65690,11 @@ function displayConfigWithDescriptions(obj, path13, sources, indent = 0) {
|
|
|
64951
65690
|
const currentPathStr = currentPath.join(".");
|
|
64952
65691
|
const description = FIELD_DESCRIPTIONS[currentPathStr];
|
|
64953
65692
|
if (description) {
|
|
64954
|
-
const
|
|
64955
|
-
const
|
|
65693
|
+
const pathParts = currentPathStr.split(".");
|
|
65694
|
+
const isDirectSubsection = pathParts.length === 2;
|
|
65695
|
+
const isKeySection = ["prompts", "autoMode", "models", "routing"].includes(pathParts[0]);
|
|
65696
|
+
const shouldIncludePath = isKeySection && isDirectSubsection;
|
|
65697
|
+
const comment = shouldIncludePath ? `${currentPathStr}: ${description}` : description;
|
|
64956
65698
|
console.log(`${indentStr}# ${comment}`);
|
|
64957
65699
|
}
|
|
64958
65700
|
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
@@ -65020,6 +65762,55 @@ function formatValueForTable(value) {
|
|
|
65020
65762
|
}
|
|
65021
65763
|
return String(value);
|
|
65022
65764
|
}
|
|
65765
|
+
// src/cli/agents.ts
|
|
65766
|
+
init_registry();
|
|
65767
|
+
init_version_detection();
|
|
65768
|
+
async function agentsListCommand(config2, _workdir) {
|
|
65769
|
+
const agentVersions = await Promise.all(ALL_AGENTS.map(async (agent) => ({
|
|
65770
|
+
name: agent.name,
|
|
65771
|
+
displayName: agent.displayName,
|
|
65772
|
+
binary: agent.binary,
|
|
65773
|
+
version: await getAgentVersion(agent.binary),
|
|
65774
|
+
installed: await agent.isInstalled(),
|
|
65775
|
+
capabilities: agent.capabilities,
|
|
65776
|
+
isDefault: config2.autoMode.defaultAgent === agent.name
|
|
65777
|
+
})));
|
|
65778
|
+
const rows = agentVersions.map((info) => {
|
|
65779
|
+
const status = info.installed ? "installed" : "unavailable";
|
|
65780
|
+
const versionStr = info.version || "-";
|
|
65781
|
+
const defaultMarker = info.isDefault ? " (default)" : "";
|
|
65782
|
+
return {
|
|
65783
|
+
name: info.displayName + defaultMarker,
|
|
65784
|
+
status,
|
|
65785
|
+
version: versionStr,
|
|
65786
|
+
binary: info.binary,
|
|
65787
|
+
tiers: info.capabilities.supportedTiers.join(", ")
|
|
65788
|
+
};
|
|
65789
|
+
});
|
|
65790
|
+
if (rows.length === 0) {
|
|
65791
|
+
console.log("No agents available.");
|
|
65792
|
+
return;
|
|
65793
|
+
}
|
|
65794
|
+
const widths = {
|
|
65795
|
+
name: Math.max(5, ...rows.map((r) => r.name.length)),
|
|
65796
|
+
status: Math.max(6, ...rows.map((r) => r.status.length)),
|
|
65797
|
+
version: Math.max(7, ...rows.map((r) => r.version.length)),
|
|
65798
|
+
binary: Math.max(6, ...rows.map((r) => r.binary.length)),
|
|
65799
|
+
tiers: Math.max(5, ...rows.map((r) => r.tiers.length))
|
|
65800
|
+
};
|
|
65801
|
+
console.log(`
|
|
65802
|
+
Available Agents:
|
|
65803
|
+
`);
|
|
65804
|
+
console.log(`${pad2("Agent", widths.name)} ${pad2("Status", widths.status)} ${pad2("Version", widths.version)} ${pad2("Binary", widths.binary)} ${pad2("Tiers", widths.tiers)}`);
|
|
65805
|
+
console.log(`${"-".repeat(widths.name)} ${"-".repeat(widths.status)} ${"-".repeat(widths.version)} ${"-".repeat(widths.binary)} ${"-".repeat(widths.tiers)}`);
|
|
65806
|
+
for (const row of rows) {
|
|
65807
|
+
console.log(`${pad2(row.name, widths.name)} ${pad2(row.status, widths.status)} ${pad2(row.version, widths.version)} ${pad2(row.binary, widths.binary)} ${pad2(row.tiers, widths.tiers)}`);
|
|
65808
|
+
}
|
|
65809
|
+
console.log();
|
|
65810
|
+
}
|
|
65811
|
+
function pad2(str, width) {
|
|
65812
|
+
return str.padEnd(width);
|
|
65813
|
+
}
|
|
65023
65814
|
// src/commands/diagnose.ts
|
|
65024
65815
|
async function diagnose(options) {
|
|
65025
65816
|
await diagnoseCommand(options);
|
|
@@ -65358,7 +66149,7 @@ import { readdir as readdir4 } from "fs/promises";
|
|
|
65358
66149
|
import { homedir as homedir4 } from "os";
|
|
65359
66150
|
import { join as join28 } from "path";
|
|
65360
66151
|
var DEFAULT_LIMIT = 20;
|
|
65361
|
-
var
|
|
66152
|
+
var _deps7 = {
|
|
65362
66153
|
getRunsDir: () => join28(homedir4(), ".nax", "runs")
|
|
65363
66154
|
};
|
|
65364
66155
|
function formatDuration3(ms) {
|
|
@@ -65396,12 +66187,12 @@ var ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
|
65396
66187
|
function visibleLength(str) {
|
|
65397
66188
|
return str.replace(ANSI_RE, "").length;
|
|
65398
66189
|
}
|
|
65399
|
-
function
|
|
66190
|
+
function pad3(str, width) {
|
|
65400
66191
|
const padding = Math.max(0, width - visibleLength(str));
|
|
65401
66192
|
return str + " ".repeat(padding);
|
|
65402
66193
|
}
|
|
65403
66194
|
async function runsCommand(options = {}) {
|
|
65404
|
-
const runsDir =
|
|
66195
|
+
const runsDir = _deps7.getRunsDir();
|
|
65405
66196
|
let entries;
|
|
65406
66197
|
try {
|
|
65407
66198
|
entries = await readdir4(runsDir);
|
|
@@ -65455,12 +66246,12 @@ async function runsCommand(options = {}) {
|
|
|
65455
66246
|
date: 11
|
|
65456
66247
|
};
|
|
65457
66248
|
const header = [
|
|
65458
|
-
|
|
65459
|
-
|
|
65460
|
-
|
|
65461
|
-
|
|
65462
|
-
|
|
65463
|
-
|
|
66249
|
+
pad3(source_default.bold("RUN ID"), COL.runId),
|
|
66250
|
+
pad3(source_default.bold("PROJECT"), COL.project),
|
|
66251
|
+
pad3(source_default.bold("FEATURE"), COL.feature),
|
|
66252
|
+
pad3(source_default.bold("STATUS"), COL.status),
|
|
66253
|
+
pad3(source_default.bold("STORIES"), COL.stories),
|
|
66254
|
+
pad3(source_default.bold("DURATION"), COL.duration),
|
|
65464
66255
|
source_default.bold("DATE")
|
|
65465
66256
|
].join(" ");
|
|
65466
66257
|
console.log();
|
|
@@ -65469,12 +66260,12 @@ async function runsCommand(options = {}) {
|
|
|
65469
66260
|
for (const row of displayed) {
|
|
65470
66261
|
const colored = colorStatus(row.status);
|
|
65471
66262
|
const line = [
|
|
65472
|
-
|
|
65473
|
-
|
|
65474
|
-
|
|
65475
|
-
|
|
65476
|
-
|
|
65477
|
-
|
|
66263
|
+
pad3(row.runId, COL.runId),
|
|
66264
|
+
pad3(row.project, COL.project),
|
|
66265
|
+
pad3(row.feature, COL.feature),
|
|
66266
|
+
pad3(colored, COL.status + (colored.length - visibleLength(colored))),
|
|
66267
|
+
pad3(`${row.passed}/${row.total}`, COL.stories),
|
|
66268
|
+
pad3(formatDuration3(row.durationMs), COL.duration),
|
|
65478
66269
|
formatDate(row.registeredAt)
|
|
65479
66270
|
].join(" ");
|
|
65480
66271
|
console.log(line);
|
|
@@ -73547,16 +74338,21 @@ program2.command("analyze").description("Parse spec.md into prd.json via agent d
|
|
|
73547
74338
|
process.exit(1);
|
|
73548
74339
|
}
|
|
73549
74340
|
});
|
|
73550
|
-
program2.command("agents").description("
|
|
73551
|
-
|
|
73552
|
-
|
|
73553
|
-
|
|
73554
|
-
|
|
73555
|
-
|
|
73556
|
-
|
|
73557
|
-
|
|
74341
|
+
program2.command("agents").description("List available coding agents with status and capabilities").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
|
|
74342
|
+
let workdir;
|
|
74343
|
+
try {
|
|
74344
|
+
workdir = validateDirectory(options.dir);
|
|
74345
|
+
} catch (err) {
|
|
74346
|
+
console.error(source_default.red(`Invalid directory: ${err.message}`));
|
|
74347
|
+
process.exit(1);
|
|
74348
|
+
}
|
|
74349
|
+
try {
|
|
74350
|
+
const config2 = await loadConfig(workdir);
|
|
74351
|
+
await agentsListCommand(config2, workdir);
|
|
74352
|
+
} catch (err) {
|
|
74353
|
+
console.error(source_default.red(`Error: ${err.message}`));
|
|
74354
|
+
process.exit(1);
|
|
73558
74355
|
}
|
|
73559
|
-
console.log();
|
|
73560
74356
|
});
|
|
73561
74357
|
program2.command("config").description("Display effective merged configuration").option("--explain", "Show detailed field descriptions", false).option("--diff", "Show only fields where project overrides global", false).action(async (options) => {
|
|
73562
74358
|
try {
|