@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.
Files changed (48) hide show
  1. package/bin/nax.ts +18 -9
  2. package/dist/nax.js +1934 -1138
  3. package/package.json +1 -2
  4. package/src/agents/adapters/aider.ts +135 -0
  5. package/src/agents/adapters/codex.ts +153 -0
  6. package/src/agents/adapters/gemini.ts +177 -0
  7. package/src/agents/adapters/opencode.ts +106 -0
  8. package/src/agents/claude-plan.ts +22 -5
  9. package/src/agents/claude.ts +102 -11
  10. package/src/agents/index.ts +4 -1
  11. package/src/agents/model-resolution.ts +43 -0
  12. package/src/agents/registry.ts +8 -3
  13. package/src/agents/types-extended.ts +5 -1
  14. package/src/agents/types.ts +31 -0
  15. package/src/agents/version-detection.ts +109 -0
  16. package/src/analyze/classifier.ts +30 -50
  17. package/src/cli/agents.ts +87 -0
  18. package/src/cli/analyze-parser.ts +8 -1
  19. package/src/cli/analyze.ts +1 -1
  20. package/src/cli/config.ts +28 -14
  21. package/src/cli/generate.ts +1 -1
  22. package/src/cli/index.ts +1 -0
  23. package/src/cli/plan.ts +1 -0
  24. package/src/config/types.ts +3 -1
  25. package/src/context/generator.ts +4 -0
  26. package/src/context/generators/codex.ts +28 -0
  27. package/src/context/generators/gemini.ts +28 -0
  28. package/src/context/types.ts +1 -1
  29. package/src/interaction/init.ts +8 -7
  30. package/src/interaction/plugins/auto.ts +41 -25
  31. package/src/pipeline/stages/execution.ts +2 -39
  32. package/src/pipeline/stages/routing.ts +12 -3
  33. package/src/plugins/index.ts +2 -0
  34. package/src/plugins/loader.ts +4 -2
  35. package/src/plugins/plugin-logger.ts +41 -0
  36. package/src/plugins/types.ts +50 -1
  37. package/src/precheck/checks-agents.ts +63 -0
  38. package/src/precheck/checks-blockers.ts +37 -1
  39. package/src/precheck/checks.ts +4 -0
  40. package/src/precheck/index.ts +4 -2
  41. package/src/routing/router.ts +1 -0
  42. package/src/routing/strategies/llm.ts +53 -36
  43. package/src/routing/strategy.ts +3 -0
  44. package/src/tdd/rectification-gate.ts +25 -1
  45. package/src/tdd/session-runner.ts +18 -49
  46. package/src/tdd/verdict.ts +135 -7
  47. package/src/utils/git.ts +49 -0
  48. 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
- routing: {
18300
- strategy: "keyword",
18301
- adaptive: {
18302
- minSamples: 10,
18303
- costThreshold: 0.8,
18304
- fallbackStrategy: "llm"
18305
- },
18306
- llm: {
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
- execution: {
18315
- maxIterations: 10,
18316
- iterationDelayMs: 2000,
18317
- costLimit: 5,
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
- interaction: {
18407
- plugin: "cli",
18408
- config: {},
18409
- defaults: {
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
- precheck: {
18419
- storySizeGate: {
18420
- enabled: true,
18421
- maxAcCount: 6,
18422
- maxDescriptionLength: 2000,
18423
- maxBulletPoints: 8
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
- prompts: {},
18427
- decompose: {
18428
- trigger: "auto",
18429
- maxAcceptanceCriteria: 6,
18430
- maxSubstories: 5,
18431
- maxSubstoryComplexity: "medium",
18432
- maxRetries: 2,
18433
- model: "balanced"
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/config/schema.ts
18439
- var init_schema = __esm(() => {
18440
- init_schemas3();
18441
- init_defaults();
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 = (async () => {
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: cached2.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.34.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("a679961"))
20308
- return "a679961";
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 init_types2 = __esm(() => {
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(requestId, timeout = 60000) {
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
- const callFn = _deps2.callLlm ?? this.callLlm.bind(this);
21487
- const decision = await callFn(request);
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 modelTier = this.config.model ?? "fast";
21505
- if (!this.config.naxConfig) {
21506
- throw new Error("Auto plugin requires naxConfig in init()");
21507
- }
21508
- const modelEntry = this.config.naxConfig.models[modelTier];
21509
- if (!modelEntry) {
21510
- throw new Error(`Model tier "${modelTier}" not found in config.models`);
21511
- }
21512
- const modelDef = resolveModel(modelEntry);
21513
- const modelArg = modelDef.model;
21514
- const proc = Bun.spawn(["claude", "-p", prompt, "--model", modelArg], {
21515
- stdout: "pipe",
21516
- stderr: "pipe"
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
- init_types2();
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
- init_types2();
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
- await autoCommitIfDirty(workdir, role, story.id);
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 (!isValidVerdict(parsed)) {
24859
- logger.warn("tdd", "Verifier verdict file missing required fields \u2014 ignoring", {
24860
- path: verdictPath
24861
- });
24862
- return null;
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
- return parsed;
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 autoCommitIfDirty2(ctx.workdir, "single-session", ctx.story.id);
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 (!agentResult.success) {
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
- await validated.setup(config2);
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 checkClaudeCLI() {
28122
+ async function checkAgentCLI(config2) {
28123
+ const agent = config2.execution?.agent || "claude";
27484
28124
  try {
27485
- const proc = Bun.spawn(["claude", "--version"], {
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: "claude-cli-available",
28132
+ name: "agent-cli-available",
27493
28133
  tier: "blocker",
27494
28134
  passed,
27495
- message: passed ? "Claude CLI is available" : "Claude CLI not found. Install from https://claude.ai/download"
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: "claude-cli-available",
28139
+ name: "agent-cli-available",
27500
28140
  tier: "blocker",
27501
28141
  passed: false,
27502
- message: "Claude CLI not found in PATH. Install from https://claude.ai/download"
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 init_checks_blockers = () => {};
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
- () => checkClaudeCLI(),
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({ specContent: storySpec, workdir, codebaseContext, modelTier, modelDef });
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 (e.g., claude, codex)",
64636
- "autoMode.fallbackOrder": "Fallback order when agent is rate-limited",
64637
- "autoMode.complexityRouting": "Model tier per complexity level",
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": "Ordered tier escalation with per-tier attempt budgets",
64645
- "autoMode.escalation.escalateEntireBatch": "Escalate all stories in batch when one fails",
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 isPromptsSubSection = currentPathStr.startsWith("prompts.");
64955
- const comment = isPromptsSubSection ? `${currentPathStr}: ${description}` : description;
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 _deps6 = {
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 pad2(str, width) {
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 = _deps6.getRunsDir();
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
- pad2(source_default.bold("RUN ID"), COL.runId),
65459
- pad2(source_default.bold("PROJECT"), COL.project),
65460
- pad2(source_default.bold("FEATURE"), COL.feature),
65461
- pad2(source_default.bold("STATUS"), COL.status),
65462
- pad2(source_default.bold("STORIES"), COL.stories),
65463
- pad2(source_default.bold("DURATION"), COL.duration),
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
- pad2(row.runId, COL.runId),
65473
- pad2(row.project, COL.project),
65474
- pad2(row.feature, COL.feature),
65475
- pad2(colored, COL.status + (colored.length - visibleLength(colored))),
65476
- pad2(`${row.passed}/${row.total}`, COL.stories),
65477
- pad2(formatDuration3(row.durationMs), COL.duration),
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("Check available coding agents").action(async () => {
73551
- const health = await checkAgentHealth();
73552
- console.log(source_default.bold(`
73553
- Coding Agents:
73554
- `));
73555
- for (const agent of health) {
73556
- const status = agent.installed ? source_default.green("\u2705 installed") : source_default.red("\u274C not found");
73557
- console.log(` ${agent.displayName.padEnd(15)} ${status}`);
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 {