@nathapp/nax 0.35.0 → 0.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/bin/nax.ts +18 -9
  2. package/dist/nax.js +1283 -662
  3. package/package.json +1 -1
  4. package/src/agents/adapters/aider.ts +135 -0
  5. package/src/agents/adapters/gemini.ts +177 -0
  6. package/src/agents/adapters/opencode.ts +106 -0
  7. package/src/agents/claude-decompose.ts +3 -3
  8. package/src/agents/index.ts +2 -0
  9. package/src/agents/registry.ts +6 -2
  10. package/src/agents/version-detection.ts +109 -0
  11. package/src/cli/agents.ts +87 -0
  12. package/src/cli/config.ts +28 -14
  13. package/src/cli/constitution.ts +0 -92
  14. package/src/cli/generate.ts +1 -1
  15. package/src/cli/index.ts +1 -0
  16. package/src/constitution/generator.ts +0 -33
  17. package/src/constitution/index.ts +2 -1
  18. package/src/constitution/loader.ts +1 -13
  19. package/src/context/builder.ts +1 -2
  20. package/src/context/elements.ts +1 -12
  21. package/src/context/generator.ts +4 -0
  22. package/src/context/generators/codex.ts +28 -0
  23. package/src/context/generators/gemini.ts +28 -0
  24. package/src/context/index.ts +2 -1
  25. package/src/context/test-scanner.ts +1 -1
  26. package/src/context/types.ts +1 -1
  27. package/src/interaction/chain.ts +17 -1
  28. package/src/pipeline/stages/execution.ts +25 -40
  29. package/src/pipeline/stages/routing.ts +8 -2
  30. package/src/precheck/checks-agents.ts +63 -0
  31. package/src/precheck/checks.ts +3 -0
  32. package/src/precheck/index.ts +2 -0
  33. package/src/prompts/builder.ts +13 -6
  34. package/src/prompts/sections/conventions.ts +5 -7
  35. package/src/prompts/sections/isolation.ts +7 -7
  36. package/src/prompts/sections/role-task.ts +64 -64
  37. package/src/review/orchestrator.ts +11 -1
  38. package/src/routing/strategies/llm-prompts.ts +1 -1
  39. package/src/routing/strategies/llm.ts +3 -3
  40. package/src/tdd/index.ts +2 -3
  41. package/src/tdd/isolation.ts +0 -13
  42. package/src/tdd/orchestrator.ts +5 -0
  43. package/src/tdd/prompts.ts +1 -231
  44. package/src/tdd/rectification-gate.ts +2 -46
  45. package/src/tdd/session-runner.ts +4 -49
  46. package/src/tdd/verdict.ts +154 -9
  47. package/src/utils/git.ts +49 -0
  48. package/src/verification/parser.ts +0 -10
  49. package/src/verification/rectification-loop.ts +2 -51
  50. package/src/worktree/dispatcher.ts +0 -59
package/dist/nax.js CHANGED
@@ -2552,22 +2552,9 @@ var require_commander = __commonJS((exports) => {
2552
2552
  exports.InvalidOptionArgumentError = InvalidArgumentError;
2553
2553
  });
2554
2554
 
2555
- // src/agents/types.ts
2556
- var CompleteError;
2557
- var init_types = __esm(() => {
2558
- CompleteError = class CompleteError extends Error {
2559
- exitCode;
2560
- constructor(message, exitCode) {
2561
- super(message);
2562
- this.exitCode = exitCode;
2563
- this.name = "CompleteError";
2564
- }
2565
- };
2566
- });
2567
-
2568
2555
  // src/logging/types.ts
2569
2556
  var EMOJI;
2570
- var init_types2 = __esm(() => {
2557
+ var init_types = __esm(() => {
2571
2558
  EMOJI = {
2572
2559
  success: "\u2713",
2573
2560
  failure: "\u2717",
@@ -2820,13 +2807,13 @@ function createNoopChalk() {
2820
2807
  }
2821
2808
  var init_formatter = __esm(() => {
2822
2809
  init_source();
2823
- init_types2();
2810
+ init_types();
2824
2811
  });
2825
2812
 
2826
2813
  // src/logging/index.ts
2827
2814
  var init_logging = __esm(() => {
2828
2815
  init_formatter();
2829
- init_types2();
2816
+ init_types();
2830
2817
  });
2831
2818
 
2832
2819
  // src/logger/formatters.ts
@@ -3037,6 +3024,636 @@ var init_logger2 = __esm(() => {
3037
3024
  init_formatters();
3038
3025
  });
3039
3026
 
3027
+ // src/acceptance/generator.ts
3028
+ function parseAcceptanceCriteria(specContent) {
3029
+ const criteria = [];
3030
+ const lines = specContent.split(`
3031
+ `);
3032
+ for (let i = 0;i < lines.length; i++) {
3033
+ const line = lines[i];
3034
+ const lineNumber = i + 1;
3035
+ const acMatch = line.match(/^\s*-?\s*(?:\[.\])?\s*(AC-\d+):\s*(.+)$/i);
3036
+ if (acMatch) {
3037
+ const id = acMatch[1].toUpperCase();
3038
+ const text = acMatch[2].trim();
3039
+ criteria.push({
3040
+ id,
3041
+ text,
3042
+ lineNumber
3043
+ });
3044
+ }
3045
+ }
3046
+ return criteria;
3047
+ }
3048
+ function buildAcceptanceTestPrompt(criteria, featureName, codebaseContext) {
3049
+ const criteriaList = criteria.map((ac) => `${ac.id}: ${ac.text}`).join(`
3050
+ `);
3051
+ return `You are a test engineer. Generate acceptance tests for the "${featureName}" feature based on the acceptance criteria below.
3052
+
3053
+ CODEBASE CONTEXT:
3054
+ ${codebaseContext}
3055
+
3056
+ ACCEPTANCE CRITERIA:
3057
+ ${criteriaList}
3058
+
3059
+ Generate a complete acceptance.test.ts file using bun:test framework. Follow these rules:
3060
+
3061
+ 1. **One test per AC**: Each acceptance criterion maps to exactly one test
3062
+ 2. **Test observable behavior only**: No implementation details, only user-facing behavior
3063
+ 3. **Independent tests**: No shared state between tests
3064
+ 4. **Integration-level**: Tests should be runnable without mocking (use real implementations)
3065
+ 5. **Clear test names**: Use format "AC-N: <description>" for test names
3066
+ 6. **Async where needed**: Use async/await for operations that may be asynchronous
3067
+
3068
+ Use this structure:
3069
+
3070
+ \`\`\`typescript
3071
+ import { describe, test, expect } from "bun:test";
3072
+
3073
+ describe("${featureName} - Acceptance Tests", () => {
3074
+ test("AC-1: <description>", async () => {
3075
+ // Test implementation
3076
+ });
3077
+
3078
+ test("AC-2: <description>", async () => {
3079
+ // Test implementation
3080
+ });
3081
+ });
3082
+ \`\`\`
3083
+
3084
+ **Important**:
3085
+ - Import the feature code being tested
3086
+ - Set up any necessary test fixtures
3087
+ - Use expect() assertions to verify behavior
3088
+ - Clean up resources if needed (close connections, delete temp files)
3089
+
3090
+ Respond with ONLY the TypeScript test code (no markdown code fences, no explanation).`;
3091
+ }
3092
+ async function generateAcceptanceTests(adapter, options) {
3093
+ const logger = getLogger();
3094
+ const criteria = parseAcceptanceCriteria(options.specContent);
3095
+ if (criteria.length === 0) {
3096
+ logger.warn("acceptance", "\u26A0 No acceptance criteria found in spec.md");
3097
+ return {
3098
+ testCode: generateSkeletonTests(options.featureName, []),
3099
+ criteria: []
3100
+ };
3101
+ }
3102
+ logger.info("acceptance", "Found acceptance criteria", { count: criteria.length });
3103
+ const prompt = buildAcceptanceTestPrompt(criteria, options.featureName, options.codebaseContext);
3104
+ try {
3105
+ const skipPerms = options.config.quality?.dangerouslySkipPermissions ?? true;
3106
+ const permArgs = skipPerms ? ["--dangerously-skip-permissions"] : [];
3107
+ const cmd = [adapter.binary, "--model", options.modelDef.model, ...permArgs, "-p", prompt];
3108
+ const proc = Bun.spawn(cmd, {
3109
+ cwd: options.workdir,
3110
+ stdout: "pipe",
3111
+ stderr: "pipe",
3112
+ env: {
3113
+ ...process.env,
3114
+ ...options.modelDef.env || {}
3115
+ }
3116
+ });
3117
+ const exitCode = await proc.exited;
3118
+ const stdout = await new Response(proc.stdout).text();
3119
+ const stderr = await new Response(proc.stderr).text();
3120
+ if (exitCode !== 0) {
3121
+ logger.warn("acceptance", "\u26A0 Agent test generation failed", { stderr });
3122
+ return {
3123
+ testCode: generateSkeletonTests(options.featureName, criteria),
3124
+ criteria
3125
+ };
3126
+ }
3127
+ const testCode = extractTestCode(stdout);
3128
+ return {
3129
+ testCode,
3130
+ criteria
3131
+ };
3132
+ } catch (error) {
3133
+ logger.warn("acceptance", "\u26A0 Agent test generation error", { error: error.message });
3134
+ return {
3135
+ testCode: generateSkeletonTests(options.featureName, criteria),
3136
+ criteria
3137
+ };
3138
+ }
3139
+ }
3140
+ function extractTestCode(output) {
3141
+ const fenceMatch = output.match(/```(?:typescript|ts)?\s*([\s\S]*?)\s*```/);
3142
+ if (fenceMatch) {
3143
+ return fenceMatch[1].trim();
3144
+ }
3145
+ const importMatch = output.match(/import\s+{[\s\S]+/);
3146
+ if (importMatch) {
3147
+ return importMatch[0].trim();
3148
+ }
3149
+ return output.trim();
3150
+ }
3151
+ function generateSkeletonTests(featureName, criteria) {
3152
+ const tests = criteria.map((ac) => {
3153
+ return ` test("${ac.id}: ${ac.text}", async () => {
3154
+ // TODO: Implement acceptance test for ${ac.id}
3155
+ // ${ac.text}
3156
+ expect(true).toBe(false); // Replace with actual test
3157
+ });`;
3158
+ }).join(`
3159
+
3160
+ `);
3161
+ return `import { describe, test, expect } from "bun:test";
3162
+
3163
+ describe("${featureName} - Acceptance Tests", () => {
3164
+ ${tests || " // No acceptance criteria found"}
3165
+ });
3166
+ `;
3167
+ }
3168
+ var init_generator = __esm(() => {
3169
+ init_logger2();
3170
+ });
3171
+
3172
+ // src/acceptance/fix-generator.ts
3173
+ function findRelatedStories(failedAC, prd) {
3174
+ const relatedStoryIds = [];
3175
+ for (const story of prd.userStories) {
3176
+ for (const ac of story.acceptanceCriteria) {
3177
+ if (ac.includes(failedAC)) {
3178
+ relatedStoryIds.push(story.id);
3179
+ break;
3180
+ }
3181
+ }
3182
+ }
3183
+ if (relatedStoryIds.length > 0) {
3184
+ return relatedStoryIds;
3185
+ }
3186
+ const passedStories = prd.userStories.filter((s) => s.status === "passed").map((s) => s.id);
3187
+ return passedStories.slice(0, 5);
3188
+ }
3189
+ function buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd) {
3190
+ const relatedStoriesText = relatedStories.map((id) => {
3191
+ const story = prd.userStories.find((s) => s.id === id);
3192
+ if (!story)
3193
+ return "";
3194
+ return `${story.id}: ${story.title}
3195
+ ${story.description}`;
3196
+ }).filter(Boolean).join(`
3197
+
3198
+ `);
3199
+ return `You are a debugging expert. A feature acceptance test has failed.
3200
+
3201
+ FAILED ACCEPTANCE CRITERION:
3202
+ ${failedAC}: ${acText}
3203
+
3204
+ TEST FAILURE OUTPUT:
3205
+ ${testOutput}
3206
+
3207
+ RELATED STORIES (implemented this functionality):
3208
+ ${relatedStoriesText}
3209
+
3210
+ Your task: Generate a fix story description that will make the acceptance test pass.
3211
+
3212
+ Requirements:
3213
+ 1. Analyze the test failure to understand the root cause
3214
+ 2. Identify what needs to change in the code
3215
+ 3. Write a clear, actionable fix description (2-4 sentences)
3216
+ 4. Focus on the specific issue, not general improvements
3217
+ 5. Reference the relevant story IDs if needed
3218
+
3219
+ Respond with ONLY the fix description (no JSON, no markdown, just the description text).`;
3220
+ }
3221
+ async function generateFixStories(adapter, options) {
3222
+ const { failedACs, testOutput, prd, specContent, workdir, modelDef } = options;
3223
+ const fixStories = [];
3224
+ const acTextMap = parseACTextFromSpec(specContent);
3225
+ const logger = getLogger();
3226
+ for (let i = 0;i < failedACs.length; i++) {
3227
+ const failedAC = failedACs[i];
3228
+ const acText = acTextMap[failedAC] || "No description available";
3229
+ logger.info("acceptance", "Generating fix for failed AC", { failedAC });
3230
+ const relatedStories = findRelatedStories(failedAC, prd);
3231
+ if (relatedStories.length === 0) {
3232
+ logger.warn("acceptance", "\u26A0 No related stories found for failed AC \u2014 skipping", { failedAC });
3233
+ continue;
3234
+ }
3235
+ const prompt = buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd);
3236
+ try {
3237
+ const skipPerms = options.config.quality?.dangerouslySkipPermissions ?? true;
3238
+ const permArgs = skipPerms ? ["--dangerously-skip-permissions"] : [];
3239
+ const cmd = [adapter.binary, "--model", modelDef.model, ...permArgs, "-p", prompt];
3240
+ const proc = Bun.spawn(cmd, {
3241
+ cwd: workdir,
3242
+ stdout: "pipe",
3243
+ stderr: "pipe",
3244
+ env: {
3245
+ ...process.env,
3246
+ ...modelDef.env || {}
3247
+ }
3248
+ });
3249
+ const exitCode = await proc.exited;
3250
+ const stdout = await new Response(proc.stdout).text();
3251
+ const stderr = await new Response(proc.stderr).text();
3252
+ if (exitCode !== 0) {
3253
+ logger.warn("acceptance", "\u26A0 Agent fix generation failed", { failedAC, stderr });
3254
+ fixStories.push({
3255
+ id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
3256
+ title: `Fix: ${failedAC}`,
3257
+ failedAC,
3258
+ testOutput,
3259
+ relatedStories,
3260
+ description: `Fix the implementation to make ${failedAC} pass. Related stories: ${relatedStories.join(", ")}.`
3261
+ });
3262
+ continue;
3263
+ }
3264
+ const fixDescription = stdout.trim();
3265
+ fixStories.push({
3266
+ id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
3267
+ title: `Fix: ${failedAC} \u2014 ${acText.slice(0, 50)}`,
3268
+ failedAC,
3269
+ testOutput,
3270
+ relatedStories,
3271
+ description: fixDescription
3272
+ });
3273
+ logger.info("acceptance", "\u2713 Generated fix story", { storyId: fixStories[fixStories.length - 1].id });
3274
+ } catch (error) {
3275
+ logger.warn("acceptance", "\u26A0 Error generating fix", {
3276
+ failedAC,
3277
+ error: error.message
3278
+ });
3279
+ fixStories.push({
3280
+ id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
3281
+ title: `Fix: ${failedAC}`,
3282
+ failedAC,
3283
+ testOutput,
3284
+ relatedStories,
3285
+ description: `Fix the implementation to make ${failedAC} pass. Related stories: ${relatedStories.join(", ")}.`
3286
+ });
3287
+ }
3288
+ }
3289
+ return fixStories;
3290
+ }
3291
+ function parseACTextFromSpec(specContent) {
3292
+ const map = {};
3293
+ const lines = specContent.split(`
3294
+ `);
3295
+ for (const line of lines) {
3296
+ const acMatch = line.match(/^\s*-?\s*(?:\[.\])?\s*(AC-\d+):\s*(.+)$/i);
3297
+ if (acMatch) {
3298
+ const id = acMatch[1].toUpperCase();
3299
+ const text = acMatch[2].trim();
3300
+ map[id] = text;
3301
+ }
3302
+ }
3303
+ return map;
3304
+ }
3305
+ function convertFixStoryToUserStory(fixStory) {
3306
+ return {
3307
+ id: fixStory.id,
3308
+ title: fixStory.title,
3309
+ description: fixStory.description,
3310
+ acceptanceCriteria: [`Fix ${fixStory.failedAC}`],
3311
+ tags: ["fix", "acceptance-failure"],
3312
+ dependencies: fixStory.relatedStories,
3313
+ status: "pending",
3314
+ passes: false,
3315
+ escalations: [],
3316
+ attempts: 0,
3317
+ contextFiles: []
3318
+ };
3319
+ }
3320
+ var init_fix_generator = __esm(() => {
3321
+ init_logger2();
3322
+ });
3323
+
3324
+ // src/acceptance/index.ts
3325
+ var init_acceptance = __esm(() => {
3326
+ init_generator();
3327
+ init_fix_generator();
3328
+ });
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
+
3040
3657
  // src/execution/pid-registry.ts
3041
3658
  import { existsSync } from "fs";
3042
3659
 
@@ -3297,7 +3914,7 @@ ${output.slice(0, 500)}`);
3297
3914
  acceptanceCriteria: Array.isArray(record.acceptanceCriteria) ? record.acceptanceCriteria : ["Implementation complete"],
3298
3915
  tags: Array.isArray(record.tags) ? record.tags : [],
3299
3916
  dependencies: Array.isArray(record.dependencies) ? record.dependencies : [],
3300
- complexity: validateComplexity(record.complexity),
3917
+ complexity: coerceComplexity(record.complexity),
3301
3918
  contextFiles: Array.isArray(record.contextFiles) ? record.contextFiles : Array.isArray(record.relevantFiles) ? record.relevantFiles : [],
3302
3919
  relevantFiles: Array.isArray(record.relevantFiles) ? record.relevantFiles : [],
3303
3920
  reasoning: String(record.reasoning || "No reasoning provided"),
@@ -3311,7 +3928,7 @@ ${output.slice(0, 500)}`);
3311
3928
  }
3312
3929
  return stories;
3313
3930
  }
3314
- function validateComplexity(value) {
3931
+ function coerceComplexity(value) {
3315
3932
  if (value === "simple" || value === "medium" || value === "complex" || value === "expert") {
3316
3933
  return value;
3317
3934
  }
@@ -18032,7 +18649,7 @@ class ClaudeCodeAdapter {
18032
18649
  return {
18033
18650
  success: exitCode === 0 && !timedOut,
18034
18651
  exitCode: actualExitCode,
18035
- output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS),
18652
+ output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS4),
18036
18653
  stderr: stderr.slice(-MAX_AGENT_STDERR_CHARS),
18037
18654
  rateLimited,
18038
18655
  durationMs,
@@ -18168,13 +18785,13 @@ class ClaudeCodeAdapter {
18168
18785
  };
18169
18786
  }
18170
18787
  }
18171
- var MAX_AGENT_OUTPUT_CHARS = 5000, MAX_AGENT_STDERR_CHARS = 1000, SIGKILL_GRACE_PERIOD_MS = 5000, _completeDeps, _decomposeDeps, _runOnceDeps;
18788
+ var MAX_AGENT_OUTPUT_CHARS4 = 5000, MAX_AGENT_STDERR_CHARS = 1000, SIGKILL_GRACE_PERIOD_MS = 5000, _completeDeps, _decomposeDeps, _runOnceDeps;
18172
18789
  var init_claude = __esm(() => {
18173
18790
  init_pid_registry();
18174
18791
  init_logger2();
18175
18792
  init_claude_plan();
18176
18793
  init_cost();
18177
- init_types();
18794
+ init_types2();
18178
18795
  _completeDeps = {
18179
18796
  spawn(cmd, opts) {
18180
18797
  return Bun.spawn(cmd, opts);
@@ -18192,90 +18809,28 @@ var init_claude = __esm(() => {
18192
18809
  };
18193
18810
  });
18194
18811
 
18195
- // src/agents/adapters/codex.ts
18196
- class CodexAdapter {
18197
- name = "codex";
18198
- displayName = "Codex";
18199
- binary = "codex";
18200
- capabilities = {
18201
- supportedTiers: ["fast", "balanced"],
18202
- maxContextTokens: 8000,
18203
- features: new Set(["tdd", "refactor"])
18204
- };
18205
- async isInstalled() {
18206
- const path = _codexRunDeps.which("codex");
18207
- return path !== null;
18208
- }
18209
- buildCommand(options) {
18210
- return ["codex", "-q", "--prompt", options.prompt];
18211
- }
18212
- async run(options) {
18213
- const cmd = this.buildCommand(options);
18214
- const startTime = Date.now();
18215
- const proc = _codexRunDeps.spawn(cmd, {
18216
- cwd: options.workdir,
18217
- stdout: "pipe",
18218
- stderr: "inherit"
18219
- });
18220
- const exitCode = await proc.exited;
18221
- const stdout = await new Response(proc.stdout).text();
18222
- const durationMs = Date.now() - startTime;
18223
- return {
18224
- success: exitCode === 0,
18225
- exitCode,
18226
- output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS2),
18227
- rateLimited: false,
18228
- durationMs,
18229
- estimatedCost: 0,
18230
- pid: proc.pid
18231
- };
18232
- }
18233
- async complete(prompt, _options) {
18234
- const cmd = ["codex", "-q", "--prompt", prompt];
18235
- const proc = _codexCompleteDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
18236
- const exitCode = await proc.exited;
18237
- const stdout = await new Response(proc.stdout).text();
18238
- const stderr = await new Response(proc.stderr).text();
18239
- const trimmed = stdout.trim();
18240
- if (exitCode !== 0) {
18241
- const errorDetails = stderr.trim() || trimmed;
18242
- const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
18243
- throw new CompleteError(errorMessage, exitCode);
18244
- }
18245
- if (!trimmed) {
18246
- throw new CompleteError("complete() returned empty output");
18247
- }
18248
- return trimmed;
18249
- }
18250
- async plan(_options) {
18251
- throw new Error("CodexAdapter.plan() not implemented");
18252
- }
18253
- async decompose(_options) {
18254
- throw new Error("CodexAdapter.decompose() not implemented");
18255
- }
18256
- }
18257
- var _codexRunDeps, _codexCompleteDeps, MAX_AGENT_OUTPUT_CHARS2 = 5000;
18258
- var init_codex = __esm(() => {
18259
- init_types();
18260
- _codexRunDeps = {
18261
- which(name) {
18262
- return Bun.which(name);
18263
- },
18264
- spawn(cmd, opts) {
18265
- return Bun.spawn(cmd, opts);
18266
- }
18267
- };
18268
- _codexCompleteDeps = {
18269
- spawn(cmd, opts) {
18270
- return Bun.spawn(cmd, opts);
18271
- }
18272
- };
18273
- });
18274
-
18275
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
+ }
18276
18824
  function getAgent(name) {
18277
18825
  return ALL_AGENTS.find((a) => a.name === name);
18278
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
+ }
18279
18834
  async function checkAgentHealth() {
18280
18835
  return Promise.all(ALL_AGENTS.map(async (agent) => ({
18281
18836
  name: agent.name,
@@ -18285,330 +18840,20 @@ async function checkAgentHealth() {
18285
18840
  }
18286
18841
  var ALL_AGENTS;
18287
18842
  var init_registry = __esm(() => {
18843
+ init_aider();
18288
18844
  init_codex();
18845
+ init_gemini();
18846
+ init_opencode();
18289
18847
  init_claude();
18290
18848
  ALL_AGENTS = [
18291
18849
  new ClaudeCodeAdapter,
18292
- new CodexAdapter
18850
+ new CodexAdapter,
18851
+ new OpenCodeAdapter,
18852
+ new GeminiAdapter,
18853
+ new AiderAdapter
18293
18854
  ];
18294
18855
  });
18295
18856
 
18296
- // src/agents/validation.ts
18297
- function validateAgentForTier(agent, tier) {
18298
- return agent.capabilities.supportedTiers.includes(tier);
18299
- }
18300
-
18301
- // src/agents/index.ts
18302
- var init_agents = __esm(() => {
18303
- init_types();
18304
- init_claude();
18305
- init_registry();
18306
- init_cost();
18307
- });
18308
-
18309
- // src/acceptance/generator.ts
18310
- function parseAcceptanceCriteria(specContent) {
18311
- const criteria = [];
18312
- const lines = specContent.split(`
18313
- `);
18314
- for (let i = 0;i < lines.length; i++) {
18315
- const line = lines[i];
18316
- const lineNumber = i + 1;
18317
- const acMatch = line.match(/^\s*-?\s*(?:\[.\])?\s*(AC-\d+):\s*(.+)$/i);
18318
- if (acMatch) {
18319
- const id = acMatch[1].toUpperCase();
18320
- const text = acMatch[2].trim();
18321
- criteria.push({
18322
- id,
18323
- text,
18324
- lineNumber
18325
- });
18326
- }
18327
- }
18328
- return criteria;
18329
- }
18330
- function buildAcceptanceTestPrompt(criteria, featureName, codebaseContext) {
18331
- const criteriaList = criteria.map((ac) => `${ac.id}: ${ac.text}`).join(`
18332
- `);
18333
- return `You are a test engineer. Generate acceptance tests for the "${featureName}" feature based on the acceptance criteria below.
18334
-
18335
- CODEBASE CONTEXT:
18336
- ${codebaseContext}
18337
-
18338
- ACCEPTANCE CRITERIA:
18339
- ${criteriaList}
18340
-
18341
- Generate a complete acceptance.test.ts file using bun:test framework. Follow these rules:
18342
-
18343
- 1. **One test per AC**: Each acceptance criterion maps to exactly one test
18344
- 2. **Test observable behavior only**: No implementation details, only user-facing behavior
18345
- 3. **Independent tests**: No shared state between tests
18346
- 4. **Integration-level**: Tests should be runnable without mocking (use real implementations)
18347
- 5. **Clear test names**: Use format "AC-N: <description>" for test names
18348
- 6. **Async where needed**: Use async/await for operations that may be asynchronous
18349
-
18350
- Use this structure:
18351
-
18352
- \`\`\`typescript
18353
- import { describe, test, expect } from "bun:test";
18354
-
18355
- describe("${featureName} - Acceptance Tests", () => {
18356
- test("AC-1: <description>", async () => {
18357
- // Test implementation
18358
- });
18359
-
18360
- test("AC-2: <description>", async () => {
18361
- // Test implementation
18362
- });
18363
- });
18364
- \`\`\`
18365
-
18366
- **Important**:
18367
- - Import the feature code being tested
18368
- - Set up any necessary test fixtures
18369
- - Use expect() assertions to verify behavior
18370
- - Clean up resources if needed (close connections, delete temp files)
18371
-
18372
- Respond with ONLY the TypeScript test code (no markdown code fences, no explanation).`;
18373
- }
18374
- async function generateAcceptanceTests(adapter, options) {
18375
- const logger = getLogger();
18376
- const criteria = parseAcceptanceCriteria(options.specContent);
18377
- if (criteria.length === 0) {
18378
- logger.warn("acceptance", "\u26A0 No acceptance criteria found in spec.md");
18379
- return {
18380
- testCode: generateSkeletonTests(options.featureName, []),
18381
- criteria: []
18382
- };
18383
- }
18384
- logger.info("acceptance", "Found acceptance criteria", { count: criteria.length });
18385
- const prompt = buildAcceptanceTestPrompt(criteria, options.featureName, options.codebaseContext);
18386
- try {
18387
- const skipPerms = options.config.quality?.dangerouslySkipPermissions ?? true;
18388
- const permArgs = skipPerms ? ["--dangerously-skip-permissions"] : [];
18389
- const cmd = [adapter.binary, "--model", options.modelDef.model, ...permArgs, "-p", prompt];
18390
- const proc = Bun.spawn(cmd, {
18391
- cwd: options.workdir,
18392
- stdout: "pipe",
18393
- stderr: "pipe",
18394
- env: {
18395
- ...process.env,
18396
- ...options.modelDef.env || {}
18397
- }
18398
- });
18399
- const exitCode = await proc.exited;
18400
- const stdout = await new Response(proc.stdout).text();
18401
- const stderr = await new Response(proc.stderr).text();
18402
- if (exitCode !== 0) {
18403
- logger.warn("acceptance", "\u26A0 Agent test generation failed", { stderr });
18404
- return {
18405
- testCode: generateSkeletonTests(options.featureName, criteria),
18406
- criteria
18407
- };
18408
- }
18409
- const testCode = extractTestCode(stdout);
18410
- return {
18411
- testCode,
18412
- criteria
18413
- };
18414
- } catch (error48) {
18415
- logger.warn("acceptance", "\u26A0 Agent test generation error", { error: error48.message });
18416
- return {
18417
- testCode: generateSkeletonTests(options.featureName, criteria),
18418
- criteria
18419
- };
18420
- }
18421
- }
18422
- function extractTestCode(output) {
18423
- const fenceMatch = output.match(/```(?:typescript|ts)?\s*([\s\S]*?)\s*```/);
18424
- if (fenceMatch) {
18425
- return fenceMatch[1].trim();
18426
- }
18427
- const importMatch = output.match(/import\s+{[\s\S]+/);
18428
- if (importMatch) {
18429
- return importMatch[0].trim();
18430
- }
18431
- return output.trim();
18432
- }
18433
- function generateSkeletonTests(featureName, criteria) {
18434
- const tests = criteria.map((ac) => {
18435
- return ` test("${ac.id}: ${ac.text}", async () => {
18436
- // TODO: Implement acceptance test for ${ac.id}
18437
- // ${ac.text}
18438
- expect(true).toBe(false); // Replace with actual test
18439
- });`;
18440
- }).join(`
18441
-
18442
- `);
18443
- return `import { describe, test, expect } from "bun:test";
18444
-
18445
- describe("${featureName} - Acceptance Tests", () => {
18446
- ${tests || " // No acceptance criteria found"}
18447
- });
18448
- `;
18449
- }
18450
- var init_generator = __esm(() => {
18451
- init_logger2();
18452
- });
18453
-
18454
- // src/acceptance/fix-generator.ts
18455
- function findRelatedStories(failedAC, prd) {
18456
- const relatedStoryIds = [];
18457
- for (const story of prd.userStories) {
18458
- for (const ac of story.acceptanceCriteria) {
18459
- if (ac.includes(failedAC)) {
18460
- relatedStoryIds.push(story.id);
18461
- break;
18462
- }
18463
- }
18464
- }
18465
- if (relatedStoryIds.length > 0) {
18466
- return relatedStoryIds;
18467
- }
18468
- const passedStories = prd.userStories.filter((s) => s.status === "passed").map((s) => s.id);
18469
- return passedStories.slice(0, 5);
18470
- }
18471
- function buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd) {
18472
- const relatedStoriesText = relatedStories.map((id) => {
18473
- const story = prd.userStories.find((s) => s.id === id);
18474
- if (!story)
18475
- return "";
18476
- return `${story.id}: ${story.title}
18477
- ${story.description}`;
18478
- }).filter(Boolean).join(`
18479
-
18480
- `);
18481
- return `You are a debugging expert. A feature acceptance test has failed.
18482
-
18483
- FAILED ACCEPTANCE CRITERION:
18484
- ${failedAC}: ${acText}
18485
-
18486
- TEST FAILURE OUTPUT:
18487
- ${testOutput}
18488
-
18489
- RELATED STORIES (implemented this functionality):
18490
- ${relatedStoriesText}
18491
-
18492
- Your task: Generate a fix story description that will make the acceptance test pass.
18493
-
18494
- Requirements:
18495
- 1. Analyze the test failure to understand the root cause
18496
- 2. Identify what needs to change in the code
18497
- 3. Write a clear, actionable fix description (2-4 sentences)
18498
- 4. Focus on the specific issue, not general improvements
18499
- 5. Reference the relevant story IDs if needed
18500
-
18501
- Respond with ONLY the fix description (no JSON, no markdown, just the description text).`;
18502
- }
18503
- async function generateFixStories(adapter, options) {
18504
- const { failedACs, testOutput, prd, specContent, workdir, modelDef } = options;
18505
- const fixStories = [];
18506
- const acTextMap = parseACTextFromSpec(specContent);
18507
- const logger = getLogger();
18508
- for (let i = 0;i < failedACs.length; i++) {
18509
- const failedAC = failedACs[i];
18510
- const acText = acTextMap[failedAC] || "No description available";
18511
- logger.info("acceptance", "Generating fix for failed AC", { failedAC });
18512
- const relatedStories = findRelatedStories(failedAC, prd);
18513
- if (relatedStories.length === 0) {
18514
- logger.warn("acceptance", "\u26A0 No related stories found for failed AC \u2014 skipping", { failedAC });
18515
- continue;
18516
- }
18517
- const prompt = buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd);
18518
- try {
18519
- const skipPerms = options.config.quality?.dangerouslySkipPermissions ?? true;
18520
- const permArgs = skipPerms ? ["--dangerously-skip-permissions"] : [];
18521
- const cmd = [adapter.binary, "--model", modelDef.model, ...permArgs, "-p", prompt];
18522
- const proc = Bun.spawn(cmd, {
18523
- cwd: workdir,
18524
- stdout: "pipe",
18525
- stderr: "pipe",
18526
- env: {
18527
- ...process.env,
18528
- ...modelDef.env || {}
18529
- }
18530
- });
18531
- const exitCode = await proc.exited;
18532
- const stdout = await new Response(proc.stdout).text();
18533
- const stderr = await new Response(proc.stderr).text();
18534
- if (exitCode !== 0) {
18535
- logger.warn("acceptance", "\u26A0 Agent fix generation failed", { failedAC, stderr });
18536
- fixStories.push({
18537
- id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
18538
- title: `Fix: ${failedAC}`,
18539
- failedAC,
18540
- testOutput,
18541
- relatedStories,
18542
- description: `Fix the implementation to make ${failedAC} pass. Related stories: ${relatedStories.join(", ")}.`
18543
- });
18544
- continue;
18545
- }
18546
- const fixDescription = stdout.trim();
18547
- fixStories.push({
18548
- id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
18549
- title: `Fix: ${failedAC} \u2014 ${acText.slice(0, 50)}`,
18550
- failedAC,
18551
- testOutput,
18552
- relatedStories,
18553
- description: fixDescription
18554
- });
18555
- logger.info("acceptance", "\u2713 Generated fix story", { storyId: fixStories[fixStories.length - 1].id });
18556
- } catch (error48) {
18557
- logger.warn("acceptance", "\u26A0 Error generating fix", {
18558
- failedAC,
18559
- error: error48.message
18560
- });
18561
- fixStories.push({
18562
- id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
18563
- title: `Fix: ${failedAC}`,
18564
- failedAC,
18565
- testOutput,
18566
- relatedStories,
18567
- description: `Fix the implementation to make ${failedAC} pass. Related stories: ${relatedStories.join(", ")}.`
18568
- });
18569
- }
18570
- }
18571
- return fixStories;
18572
- }
18573
- function parseACTextFromSpec(specContent) {
18574
- const map2 = {};
18575
- const lines = specContent.split(`
18576
- `);
18577
- for (const line of lines) {
18578
- const acMatch = line.match(/^\s*-?\s*(?:\[.\])?\s*(AC-\d+):\s*(.+)$/i);
18579
- if (acMatch) {
18580
- const id = acMatch[1].toUpperCase();
18581
- const text = acMatch[2].trim();
18582
- map2[id] = text;
18583
- }
18584
- }
18585
- return map2;
18586
- }
18587
- function convertFixStoryToUserStory(fixStory) {
18588
- return {
18589
- id: fixStory.id,
18590
- title: fixStory.title,
18591
- description: fixStory.description,
18592
- acceptanceCriteria: [`Fix ${fixStory.failedAC}`],
18593
- tags: ["fix", "acceptance-failure"],
18594
- dependencies: fixStory.relatedStories,
18595
- status: "pending",
18596
- passes: false,
18597
- escalations: [],
18598
- attempts: 0,
18599
- contextFiles: []
18600
- };
18601
- }
18602
- var init_fix_generator = __esm(() => {
18603
- init_logger2();
18604
- });
18605
-
18606
- // src/acceptance/index.ts
18607
- var init_acceptance = __esm(() => {
18608
- init_generator();
18609
- init_fix_generator();
18610
- });
18611
-
18612
18857
  // src/decompose/apply.ts
18613
18858
  function applyDecomposition(prd, result) {
18614
18859
  const { subStories } = result;
@@ -19226,7 +19471,7 @@ Your complexity classification will determine the execution strategy:
19226
19471
  Respond with ONLY this JSON (no markdown, no explanation):
19227
19472
  {"complexity":"simple|medium|complex|expert","modelTier":"fast|balanced|powerful","reasoning":"<one line>"}`;
19228
19473
  }
19229
- function buildBatchPrompt(stories, config2) {
19474
+ function buildBatchRoutingPrompt(stories, config2) {
19230
19475
  const storyBlocks = stories.map((story, idx) => {
19231
19476
  const criteria = story.acceptanceCriteria.map((c, i) => ` ${i + 1}. ${c}`).join(`
19232
19477
  `);
@@ -19406,7 +19651,7 @@ async function routeBatch(stories, context) {
19406
19651
  throw new Error("No agent adapter available for batch routing (AA-003)");
19407
19652
  }
19408
19653
  const modelTier = llmConfig.model ?? "fast";
19409
- const prompt = buildBatchPrompt(stories, config2);
19654
+ const prompt = buildBatchRoutingPrompt(stories, config2);
19410
19655
  try {
19411
19656
  const output = await callLlm(adapter, modelTier, prompt, config2);
19412
19657
  const decisions = parseBatchResponse(output, stories, config2);
@@ -19828,7 +20073,7 @@ var init_routing = __esm(() => {
19828
20073
  });
19829
20074
 
19830
20075
  // src/decompose/validators/complexity.ts
19831
- function validateComplexity2(substories, maxComplexity) {
20076
+ function validateComplexity(substories, maxComplexity) {
19832
20077
  const errors3 = [];
19833
20078
  const warnings = [];
19834
20079
  const maxOrder = COMPLEXITY_ORDER[maxComplexity];
@@ -20140,7 +20385,7 @@ function runAllValidators(originalStory, substories, existingStories, config2) {
20140
20385
  const results = [
20141
20386
  validateOverlap(substories, existingStories),
20142
20387
  validateCoverage(originalStory, substories),
20143
- validateComplexity2(substories, maxComplexity),
20388
+ validateComplexity(substories, maxComplexity),
20144
20389
  validateDependencies(substories, existingIds)
20145
20390
  ];
20146
20391
  const errors3 = results.flatMap((r) => r.errors);
@@ -20412,7 +20657,7 @@ var package_default;
20412
20657
  var init_package = __esm(() => {
20413
20658
  package_default = {
20414
20659
  name: "@nathapp/nax",
20415
- version: "0.35.0",
20660
+ version: "0.36.1",
20416
20661
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
20417
20662
  type: "module",
20418
20663
  bin: {
@@ -20473,8 +20718,8 @@ var init_version = __esm(() => {
20473
20718
  NAX_VERSION = package_default.version;
20474
20719
  NAX_COMMIT = (() => {
20475
20720
  try {
20476
- if (/^[0-9a-f]{6,10}$/.test("a5e44ea"))
20477
- return "a5e44ea";
20721
+ if (/^[0-9a-f]{6,10}$/.test("b241bab"))
20722
+ return "b241bab";
20478
20723
  } catch {}
20479
20724
  try {
20480
20725
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -20847,6 +21092,12 @@ class InteractionChain {
20847
21092
  async prompt(request) {
20848
21093
  await this.send(request);
20849
21094
  const response = await this.receive(request.id, request.timeout);
21095
+ if (response.action === "choose" && response.value && request.options) {
21096
+ const matched = request.options.find((o) => o.key === response.value);
21097
+ if (matched) {
21098
+ return { ...response, action: matched.key };
21099
+ }
21100
+ }
20850
21101
  return response;
20851
21102
  }
20852
21103
  async cancel(requestId) {
@@ -22436,9 +22687,17 @@ class ReviewOrchestrator {
22436
22687
  const changedFiles = await getChangedFiles(workdir);
22437
22688
  const pluginResults = [];
22438
22689
  for (const reviewer of reviewers) {
22439
- logger?.info("review", `Running plugin reviewer: ${reviewer.name}`);
22690
+ logger?.info("review", `Running plugin reviewer: ${reviewer.name}`, {
22691
+ changedFiles: changedFiles.length
22692
+ });
22440
22693
  try {
22441
22694
  const result = await reviewer.check(workdir, changedFiles);
22695
+ logger?.info("review", `Plugin reviewer result: ${reviewer.name}`, {
22696
+ passed: result.passed,
22697
+ exitCode: result.exitCode,
22698
+ output: result.output?.slice(0, 500),
22699
+ findings: result.findings?.length ?? 0
22700
+ });
22442
22701
  pluginResults.push({
22443
22702
  name: reviewer.name,
22444
22703
  passed: result.passed,
@@ -22457,6 +22716,7 @@ class ReviewOrchestrator {
22457
22716
  }
22458
22717
  } catch (error48) {
22459
22718
  const errorMsg = error48 instanceof Error ? error48.message : String(error48);
22719
+ logger?.warn("review", `Plugin reviewer threw error: ${reviewer.name}`, { error: errorMsg });
22460
22720
  pluginResults.push({ name: reviewer.name, passed: false, output: "", error: errorMsg });
22461
22721
  builtIn.pluginReviewers = pluginResults;
22462
22722
  return {
@@ -22714,12 +22974,14 @@ var init_completion = __esm(() => {
22714
22974
  };
22715
22975
  });
22716
22976
 
22977
+ // src/optimizer/types.ts
22978
+ function estimateTokens(text) {
22979
+ return Math.ceil(text.length / 4);
22980
+ }
22981
+
22717
22982
  // src/constitution/loader.ts
22718
22983
  import { existsSync as existsSync13 } from "fs";
22719
22984
  import { join as join14 } from "path";
22720
- function estimateTokens(text) {
22721
- return Math.ceil(text.length / 3);
22722
- }
22723
22985
  function truncateToTokens(text, maxTokens) {
22724
22986
  const maxChars = maxTokens * 3;
22725
22987
  if (text.length <= maxChars) {
@@ -22969,32 +23231,29 @@ var init_auto_detect = __esm(() => {
22969
23231
  });
22970
23232
 
22971
23233
  // src/context/elements.ts
22972
- function estimateTokens2(text) {
22973
- return Math.ceil(text.length / CHARS_PER_TOKEN);
22974
- }
22975
23234
  function createStoryContext(story, priority) {
22976
23235
  const content = formatStoryAsText(story);
22977
- return { type: "story", storyId: story.id, content, priority, tokens: estimateTokens2(content) };
23236
+ return { type: "story", storyId: story.id, content, priority, tokens: estimateTokens(content) };
22978
23237
  }
22979
23238
  function createDependencyContext(story, priority) {
22980
23239
  const content = formatStoryAsText(story);
22981
- return { type: "dependency", storyId: story.id, content, priority, tokens: estimateTokens2(content) };
23240
+ return { type: "dependency", storyId: story.id, content, priority, tokens: estimateTokens(content) };
22982
23241
  }
22983
23242
  function createErrorContext(errorMessage, priority) {
22984
- return { type: "error", content: errorMessage, priority, tokens: estimateTokens2(errorMessage) };
23243
+ return { type: "error", content: errorMessage, priority, tokens: estimateTokens(errorMessage) };
22985
23244
  }
22986
23245
  function createProgressContext(progressText, priority) {
22987
- return { type: "progress", content: progressText, priority, tokens: estimateTokens2(progressText) };
23246
+ return { type: "progress", content: progressText, priority, tokens: estimateTokens(progressText) };
22988
23247
  }
22989
23248
  function createFileContext(filePath, content, priority) {
22990
- return { type: "file", filePath, content, priority, tokens: estimateTokens2(content) };
23249
+ return { type: "file", filePath, content, priority, tokens: estimateTokens(content) };
22991
23250
  }
22992
23251
  function createTestCoverageContext(content, tokens, priority) {
22993
23252
  return { type: "test-coverage", content, priority, tokens };
22994
23253
  }
22995
23254
  function createPriorFailuresContext(failures, priority) {
22996
23255
  const content = formatPriorFailures(failures);
22997
- return { type: "prior-failures", content, priority, tokens: estimateTokens2(content) };
23256
+ return { type: "prior-failures", content, priority, tokens: estimateTokens(content) };
22998
23257
  }
22999
23258
  function formatPriorFailures(failures) {
23000
23259
  if (!failures || failures.length === 0) {
@@ -23065,7 +23324,6 @@ function formatStoryAsText(story) {
23065
23324
  return parts.join(`
23066
23325
  `);
23067
23326
  }
23068
- var CHARS_PER_TOKEN = 3;
23069
23327
  var init_elements = __esm(() => {
23070
23328
  init_logger2();
23071
23329
  });
@@ -23222,7 +23480,7 @@ function truncateToTokenBudget(files, maxTokens, preferredDetail) {
23222
23480
  for (let i = startIndex;i < detailLevels.length; i++) {
23223
23481
  const detail = detailLevels[i];
23224
23482
  const summary = formatTestSummary(files, detail);
23225
- const tokens = estimateTokens2(summary);
23483
+ const tokens = estimateTokens(summary);
23226
23484
  if (tokens <= maxTokens) {
23227
23485
  return { summary, detail, truncated: i !== startIndex };
23228
23486
  }
@@ -23232,7 +23490,7 @@ function truncateToTokenBudget(files, maxTokens, preferredDetail) {
23232
23490
  truncatedFiles = truncatedFiles.slice(0, truncatedFiles.length - 1);
23233
23491
  const summary = `${formatTestSummary(truncatedFiles, "names-only")}
23234
23492
  ... and ${files.length - truncatedFiles.length} more test files`;
23235
- if (estimateTokens2(summary) <= maxTokens) {
23493
+ if (estimateTokens(summary) <= maxTokens) {
23236
23494
  return { summary, detail: "names-only", truncated: true };
23237
23495
  }
23238
23496
  }
@@ -23255,13 +23513,12 @@ async function generateTestCoverageSummary(options) {
23255
23513
  }
23256
23514
  const totalTests = files.reduce((sum, f) => sum + f.testCount, 0);
23257
23515
  const { summary } = truncateToTokenBudget(files, maxTokens, detail);
23258
- const tokens = estimateTokens2(summary);
23516
+ const tokens = estimateTokens(summary);
23259
23517
  return { files, totalTests, summary, tokens };
23260
23518
  }
23261
23519
  var COMMON_TEST_DIRS;
23262
23520
  var init_test_scanner = __esm(() => {
23263
23521
  init_logger2();
23264
- init_builder3();
23265
23522
  COMMON_TEST_DIRS = ["test", "tests", "__tests__", "src/__tests__", "spec"];
23266
23523
  });
23267
23524
 
@@ -23778,6 +24035,68 @@ ${pluginMarkdown}` : pluginMarkdown;
23778
24035
  };
23779
24036
  });
23780
24037
 
24038
+ // src/agents/validation.ts
24039
+ function validateAgentForTier(agent, tier) {
24040
+ return agent.capabilities.supportedTiers.includes(tier);
24041
+ }
24042
+
24043
+ // src/agents/version-detection.ts
24044
+ async function getAgentVersion(binaryName) {
24045
+ try {
24046
+ const proc = _versionDetectionDeps.spawn([binaryName, "--version"], {
24047
+ stdout: "pipe",
24048
+ stderr: "pipe"
24049
+ });
24050
+ const exitCode = await proc.exited;
24051
+ if (exitCode !== 0) {
24052
+ return null;
24053
+ }
24054
+ const stdout = await new Response(proc.stdout).text();
24055
+ const versionLine = stdout.trim().split(`
24056
+ `)[0];
24057
+ const versionMatch = versionLine.match(/v?(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
24058
+ if (versionMatch) {
24059
+ return versionMatch[0];
24060
+ }
24061
+ return versionLine || null;
24062
+ } catch {
24063
+ return null;
24064
+ }
24065
+ }
24066
+ async function getAgentVersions() {
24067
+ const agents = await getInstalledAgents();
24068
+ const agentsByName = new Map(agents.map((a) => [a.name, a]));
24069
+ const { ALL_AGENTS: ALL_AGENTS2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
24070
+ const versions2 = await Promise.all(ALL_AGENTS2.map(async (agent) => {
24071
+ const version2 = agentsByName.has(agent.name) ? await getAgentVersion(agent.binary) : null;
24072
+ return {
24073
+ name: agent.name,
24074
+ displayName: agent.displayName,
24075
+ version: version2,
24076
+ installed: agentsByName.has(agent.name)
24077
+ };
24078
+ }));
24079
+ return versions2;
24080
+ }
24081
+ var _versionDetectionDeps;
24082
+ var init_version_detection = __esm(() => {
24083
+ init_registry();
24084
+ _versionDetectionDeps = {
24085
+ spawn(cmd, opts) {
24086
+ return Bun.spawn(cmd, opts);
24087
+ }
24088
+ };
24089
+ });
24090
+
24091
+ // src/agents/index.ts
24092
+ var init_agents = __esm(() => {
24093
+ init_types2();
24094
+ init_claude();
24095
+ init_registry();
24096
+ init_cost();
24097
+ init_version_detection();
24098
+ });
24099
+
23781
24100
  // src/tdd/isolation.ts
23782
24101
  function isTestFile(filePath) {
23783
24102
  return TEST_PATTERNS.some((pattern) => pattern.test(filePath));
@@ -23935,7 +24254,38 @@ async function hasCommitsForStory(workdir, storyId, maxCommits = 20) {
23935
24254
  function detectMergeConflict(output) {
23936
24255
  return output.includes("CONFLICT") || output.includes("conflict");
23937
24256
  }
24257
+ async function autoCommitIfDirty(workdir, stage, role, storyId) {
24258
+ const logger = getSafeLogger();
24259
+ try {
24260
+ const statusProc = Bun.spawn(["git", "status", "--porcelain"], {
24261
+ cwd: workdir,
24262
+ stdout: "pipe",
24263
+ stderr: "pipe"
24264
+ });
24265
+ const statusOutput = await new Response(statusProc.stdout).text();
24266
+ await statusProc.exited;
24267
+ if (!statusOutput.trim())
24268
+ return;
24269
+ logger?.warn(stage, `Agent did not commit after ${role} session \u2014 auto-committing`, {
24270
+ role,
24271
+ storyId,
24272
+ dirtyFiles: statusOutput.trim().split(`
24273
+ `).length
24274
+ });
24275
+ const addProc = Bun.spawn(["git", "add", "-A"], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
24276
+ await addProc.exited;
24277
+ const commitProc = Bun.spawn(["git", "commit", "-m", `chore(${storyId}): auto-commit after ${role} session`], {
24278
+ cwd: workdir,
24279
+ stdout: "pipe",
24280
+ stderr: "pipe"
24281
+ });
24282
+ await commitProc.exited;
24283
+ } catch {}
24284
+ }
23938
24285
  var GIT_TIMEOUT_MS = 1e4;
24286
+ var init_git = __esm(() => {
24287
+ init_logger2();
24288
+ });
23939
24289
  // src/verification/executor.ts
23940
24290
  async function drainWithDeadline(proc, deadlineMs) {
23941
24291
  const EMPTY = Symbol("timeout");
@@ -24420,7 +24770,7 @@ var init_cleanup = __esm(() => {
24420
24770
  });
24421
24771
 
24422
24772
  // src/tdd/prompts.ts
24423
- function buildImplementerRectificationPrompt(failures, story, contextMarkdown, config2) {
24773
+ function buildImplementerRectificationPrompt(failures, story, _contextMarkdown, config2) {
24424
24774
  return createRectificationPrompt(failures, story, config2);
24425
24775
  }
24426
24776
  var init_prompts = __esm(() => {
@@ -24512,7 +24862,7 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
24512
24862
  exitCode: rectifyResult.exitCode
24513
24863
  });
24514
24864
  }
24515
- await autoCommitIfDirty(workdir, "rectification", story.id, logger);
24865
+ await autoCommitIfDirty(workdir, "tdd", "rectification", story.id);
24516
24866
  const rectifyIsolation = lite ? undefined : await verifyImplementerIsolation(workdir, rectifyBeforeRef);
24517
24867
  if (rectifyIsolation && !rectifyIsolation.passed) {
24518
24868
  logger.error("tdd", "Rectification violated isolation", {
@@ -24557,35 +24907,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
24557
24907
  logger.info("tdd", "Full suite gate passed", { storyId: story.id });
24558
24908
  return true;
24559
24909
  }
24560
- async function autoCommitIfDirty(workdir, role, storyId, logger) {
24561
- try {
24562
- const statusProc = Bun.spawn(["git", "status", "--porcelain"], {
24563
- cwd: workdir,
24564
- stdout: "pipe",
24565
- stderr: "pipe"
24566
- });
24567
- const statusOutput = await new Response(statusProc.stdout).text();
24568
- await statusProc.exited;
24569
- if (!statusOutput.trim())
24570
- return;
24571
- logger.warn("tdd", `Agent did not commit after ${role} session \u2014 auto-committing`, {
24572
- role,
24573
- storyId,
24574
- dirtyFiles: statusOutput.trim().split(`
24575
- `).length
24576
- });
24577
- const addProc = Bun.spawn(["git", "add", "-A"], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
24578
- await addProc.exited;
24579
- const commitProc = Bun.spawn(["git", "commit", "-m", `chore(${storyId}): auto-commit after ${role} session`], {
24580
- cwd: workdir,
24581
- stdout: "pipe",
24582
- stderr: "pipe"
24583
- });
24584
- await commitProc.exited;
24585
- } catch {}
24586
- }
24587
24910
  var init_rectification_gate = __esm(() => {
24588
24911
  init_config();
24912
+ init_git();
24589
24913
  init_verification();
24590
24914
  init_cleanup();
24591
24915
  init_isolation();
@@ -24596,9 +24920,9 @@ var init_rectification_gate = __esm(() => {
24596
24920
  function buildConventionsSection() {
24597
24921
  return `# Conventions
24598
24922
 
24599
- ` + `Follow existing code patterns and conventions. Write idiomatic, maintainable code.
24923
+ Follow existing code patterns and conventions. Write idiomatic, maintainable code.
24600
24924
 
24601
- ` + "When running tests, run ONLY test files related to your changes (e.g. `bun test ./test/specific.test.ts`). " + "NEVER run `bun test` without a file filter \u2014 full suite output will flood your context window and cause failures.\n\n" + "Commit your changes when done using conventional commit format (e.g. `feat:`, `fix:`, `test:`).";
24925
+ Commit your changes when done using conventional commit format (e.g. \`feat:\`, \`fix:\`, \`test:\`).`;
24602
24926
  }
24603
24927
 
24604
24928
  // src/prompts/sections/isolation.ts
@@ -24607,29 +24931,39 @@ function buildIsolationSection(roleOrMode, mode) {
24607
24931
  return buildIsolationSection("test-writer", roleOrMode);
24608
24932
  }
24609
24933
  const role = roleOrMode;
24610
- const header = `# Isolation Rules
24611
-
24612
- `;
24934
+ const header = "# Isolation Rules";
24613
24935
  const footer = `
24614
24936
 
24615
24937
  ${TEST_FILTER_RULE}`;
24616
24938
  if (role === "test-writer") {
24617
24939
  const m = mode ?? "strict";
24618
24940
  if (m === "strict") {
24619
- return `${header}isolation scope: Only create or modify files in the test/ directory. Tests must fail because the feature is not yet implemented. Do NOT modify any source files in src/.${footer}`;
24941
+ return `${header}
24942
+
24943
+ isolation scope: Only create or modify files in the test/ directory. Tests must fail because the feature is not yet implemented. Do NOT modify any source files in src/.${footer}`;
24620
24944
  }
24621
- return `${header}isolation scope: Create test files in test/. MAY read src/ files and MAY import from src/ to ensure correct types/interfaces. May create minimal stubs in src/ if needed to make imports work, but do NOT implement real logic.${footer}`;
24945
+ return `${header}
24946
+
24947
+ isolation scope: Create test files in test/. MAY read src/ files and MAY import from src/ to ensure correct types/interfaces. May create minimal stubs in src/ if needed to make imports work, but do NOT implement real logic.${footer}`;
24622
24948
  }
24623
24949
  if (role === "implementer") {
24624
- return `${header}isolation scope: Implement source code in src/ to make tests pass. Do not modify test files. Run tests frequently to track progress.${footer}`;
24950
+ return `${header}
24951
+
24952
+ isolation scope: Implement source code in src/ to make tests pass. Do not modify test files. Run tests frequently to track progress.${footer}`;
24625
24953
  }
24626
24954
  if (role === "verifier") {
24627
- return `${header}isolation scope: Read-only inspection. Review all test results, implementation code, and acceptance criteria compliance. You MAY write a verdict file (.nax-verifier-verdict.json) and apply legitimate fixes if needed.${footer}`;
24955
+ return `${header}
24956
+
24957
+ isolation scope: Read-only inspection. Review all test results, implementation code, and acceptance criteria compliance. You MAY write a verdict file (.nax-verifier-verdict.json) and apply legitimate fixes if needed.${footer}`;
24628
24958
  }
24629
24959
  if (role === "single-session") {
24630
- return `${header}isolation scope: Create test files in test/ directory, then implement source code in src/ to make tests pass. Both directories are in scope for this session.${footer}`;
24960
+ return `${header}
24961
+
24962
+ isolation scope: Create test files in test/ directory, then implement source code in src/ to make tests pass. Both directories are in scope for this session.${footer}`;
24631
24963
  }
24632
- return `${header}isolation scope: You may modify both src/ and test/ files. Write failing tests FIRST, then implement to make them pass.`;
24964
+ return `${header}
24965
+
24966
+ isolation scope: You may modify both src/ and test/ files. Write failing tests FIRST, then implement to make them pass.`;
24633
24967
  }
24634
24968
  var TEST_FILTER_RULE;
24635
24969
  var init_isolation2 = __esm(() => {
@@ -24647,76 +24981,76 @@ function buildRoleTaskSection(roleOrVariant, variant) {
24647
24981
  if (v === "standard") {
24648
24982
  return `# Role: Implementer
24649
24983
 
24650
- ` + `Your task: make failing tests pass.
24984
+ Your task: make failing tests pass.
24651
24985
 
24652
- ` + `Instructions:
24653
- ` + `- Implement source code in src/ to make tests pass
24654
- ` + `- Do NOT modify test files
24655
- ` + `- Run tests frequently to track progress
24656
- ` + `- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
24657
- ` + "- Goal: all tests green, all changes committed";
24986
+ Instructions:
24987
+ - Implement source code in src/ to make tests pass
24988
+ - Do NOT modify test files
24989
+ - Run tests frequently to track progress
24990
+ - When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
24991
+ - Goal: all tests green, all changes committed`;
24658
24992
  }
24659
24993
  return `# Role: Implementer (Lite)
24660
24994
 
24661
- ` + `Your task: Write tests AND implement the feature in a single session.
24995
+ Your task: Write tests AND implement the feature in a single session.
24662
24996
 
24663
- ` + `Instructions:
24664
- ` + `- Write tests first (test/ directory), then implement (src/ directory)
24665
- ` + `- All tests must pass by the end
24666
- ` + `- Use Bun test (describe/test/expect)
24667
- ` + `- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
24668
- ` + "- Goal: all tests green, all criteria met, all changes committed";
24997
+ Instructions:
24998
+ - Write tests first (test/ directory), then implement (src/ directory)
24999
+ - All tests must pass by the end
25000
+ - Use Bun test (describe/test/expect)
25001
+ - When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
25002
+ - Goal: all tests green, all criteria met, all changes committed`;
24669
25003
  }
24670
25004
  if (role === "test-writer") {
24671
25005
  return `# Role: Test-Writer
24672
25006
 
24673
- ` + `Your task: Write comprehensive failing tests for the feature.
25007
+ Your task: Write comprehensive failing tests for the feature.
24674
25008
 
24675
- ` + `Instructions:
24676
- ` + `- Create test files in test/ directory that cover acceptance criteria
24677
- ` + `- Tests must fail initially (RED phase) \u2014 the feature is not yet implemented
24678
- ` + `- Use Bun test (describe/test/expect)
24679
- ` + `- Write clear test names that document expected behavior
24680
- ` + `- Focus on behavior, not implementation details
24681
- ` + "- Goal: comprehensive test suite ready for implementation";
25009
+ Instructions:
25010
+ - Create test files in test/ directory that cover acceptance criteria
25011
+ - Tests must fail initially (RED phase) \u2014 the feature is not yet implemented
25012
+ - Use Bun test (describe/test/expect)
25013
+ - Write clear test names that document expected behavior
25014
+ - Focus on behavior, not implementation details
25015
+ - Goal: comprehensive test suite ready for implementation`;
24682
25016
  }
24683
25017
  if (role === "verifier") {
24684
25018
  return `# Role: Verifier
24685
25019
 
24686
- ` + `Your task: Review and verify the implementation against acceptance criteria.
25020
+ Your task: Review and verify the implementation against acceptance criteria.
24687
25021
 
24688
- ` + `Instructions:
24689
- ` + `- Review all test results \u2014 verify tests pass
24690
- ` + `- Check that implementation meets all acceptance criteria
24691
- ` + `- Inspect code quality, error handling, and edge cases
24692
- ` + `- Verify test modifications (if any) are legitimate fixes
24693
- ` + `- Write a detailed verdict with reasoning
24694
- ` + "- Goal: provide comprehensive verification and quality assurance";
25022
+ Instructions:
25023
+ - Review all test results \u2014 verify tests pass
25024
+ - Check that implementation meets all acceptance criteria
25025
+ - Inspect code quality, error handling, and edge cases
25026
+ - Verify test modifications (if any) are legitimate fixes
25027
+ - Write a detailed verdict with reasoning
25028
+ - Goal: provide comprehensive verification and quality assurance`;
24695
25029
  }
24696
25030
  if (role === "single-session") {
24697
25031
  return `# Role: Single-Session
24698
25032
 
24699
- ` + `Your task: Write tests AND implement the feature in a single focused session.
25033
+ Your task: Write tests AND implement the feature in a single focused session.
24700
25034
 
24701
- ` + `Instructions:
24702
- ` + `- Phase 1: Write comprehensive tests (test/ directory)
24703
- ` + `- Phase 2: Implement to make all tests pass (src/ directory)
24704
- ` + `- Use Bun test (describe/test/expect)
24705
- ` + `- Run tests frequently throughout implementation
24706
- ` + `- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
24707
- ` + "- Goal: all tests passing, all changes committed, full story complete";
25035
+ Instructions:
25036
+ - Phase 1: Write comprehensive tests (test/ directory)
25037
+ - Phase 2: Implement to make all tests pass (src/ directory)
25038
+ - Use Bun test (describe/test/expect)
25039
+ - Run tests frequently throughout implementation
25040
+ - When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
25041
+ - Goal: all tests passing, all changes committed, full story complete`;
24708
25042
  }
24709
25043
  return `# Role: TDD-Simple
24710
25044
 
24711
- ` + `Your task: Write failing tests FIRST, then implement to make them pass.
25045
+ Your task: Write failing tests FIRST, then implement to make them pass.
24712
25046
 
24713
- ` + `Instructions:
24714
- ` + `- RED phase: Write failing tests FIRST for the acceptance criteria
24715
- ` + `- RED phase: Run the tests to confirm they fail
24716
- ` + `- GREEN phase: Implement the minimum code to make tests pass
24717
- ` + `- REFACTOR phase: Refactor while keeping tests green
24718
- ` + `- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
24719
- ` + "- Goal: all tests passing, feature complete, all changes committed";
25047
+ Instructions:
25048
+ - RED phase: Write failing tests FIRST for the acceptance criteria
25049
+ - RED phase: Run the tests to confirm they fail
25050
+ - GREEN phase: Implement the minimum code to make tests pass
25051
+ - REFACTOR phase: Refactor while keeping tests green
25052
+ - When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
25053
+ - Goal: all tests passing, feature complete, all changes committed`;
24720
25054
  }
24721
25055
 
24722
25056
  // src/prompts/sections/story.ts
@@ -24734,6 +25068,69 @@ ${story.description}
24734
25068
  ${criteria}`;
24735
25069
  }
24736
25070
 
25071
+ // src/prompts/sections/verdict.ts
25072
+ function buildVerdictSection(story) {
25073
+ return `# Verdict Instructions
25074
+
25075
+ ## Write Verdict File
25076
+
25077
+ After completing your verification, you **MUST** write a verdict file at the **project root**:
25078
+
25079
+ **File:** \`.nax-verifier-verdict.json\`
25080
+
25081
+ Set \`approved: true\` when ALL of these conditions are met:
25082
+ - All tests pass
25083
+ - Implementation is clean and follows conventions
25084
+ - All acceptance criteria met
25085
+ - Any test modifications by implementer are legitimate fixes
25086
+
25087
+ Set \`approved: false\` when ANY of these conditions are true:
25088
+ - Tests are failing and you cannot fix them
25089
+ - The implementer loosened test assertions to mask bugs
25090
+ - Critical acceptance criteria are not met
25091
+ - Code quality is poor (security issues, severe bugs, etc.)
25092
+
25093
+ **Full JSON schema example** (fill in all fields with real values):
25094
+
25095
+ \`\`\`json
25096
+ {
25097
+ "version": 1,
25098
+ "approved": true,
25099
+ "tests": {
25100
+ "allPassing": true,
25101
+ "passCount": 42,
25102
+ "failCount": 0
25103
+ },
25104
+ "testModifications": {
25105
+ "detected": false,
25106
+ "files": [],
25107
+ "legitimate": true,
25108
+ "reasoning": "No test files were modified by the implementer"
25109
+ },
25110
+ "acceptanceCriteria": {
25111
+ "allMet": true,
25112
+ "criteria": [
25113
+ { "criterion": "Example criterion", "met": true }
25114
+ ]
25115
+ },
25116
+ "quality": {
25117
+ "rating": "good",
25118
+ "issues": []
25119
+ },
25120
+ "fixes": [],
25121
+ "reasoning": "All tests pass, implementation is clean, all acceptance criteria are met."
25122
+ }
25123
+ \`\`\`
25124
+
25125
+ **Field notes:**
25126
+ - \`quality.rating\` must be one of: \`"good"\`, \`"acceptable"\`, \`"poor"\`
25127
+ - \`testModifications.files\` \u2014 list any test files the implementer changed
25128
+ - \`fixes\` \u2014 list any fixes you applied yourself during this verification session
25129
+ - \`reasoning\` \u2014 brief summary of your overall assessment
25130
+
25131
+ When done, commit any fixes with message: "fix: verify and adjust ${story.title}"`;
25132
+ }
25133
+
24737
25134
  // src/prompts/loader.ts
24738
25135
  var exports_loader = {};
24739
25136
  __export(exports_loader, {
@@ -24809,6 +25206,9 @@ ${this._constitution}`);
24809
25206
  if (this._story) {
24810
25207
  sections.push(buildStorySection(this._story));
24811
25208
  }
25209
+ if (this._role === "verifier" && this._story) {
25210
+ sections.push(buildVerdictSection(this._story));
25211
+ }
24812
25212
  const isolation = this._options.isolation;
24813
25213
  sections.push(buildIsolationSection(this._role, isolation));
24814
25214
  if (this._contextMd) {
@@ -24897,7 +25297,7 @@ async function rollbackToRef(workdir, ref) {
24897
25297
  }
24898
25298
  logger.info("tdd", "Successfully rolled back git changes", { ref });
24899
25299
  }
24900
- async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false) {
25300
+ async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution) {
24901
25301
  const startTime = Date.now();
24902
25302
  let prompt;
24903
25303
  switch (role) {
@@ -24905,7 +25305,7 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
24905
25305
  prompt = await PromptBuilder.for("test-writer", { isolation: lite ? "lite" : "strict" }).withLoader(workdir, config2).story(story).context(contextMarkdown).build();
24906
25306
  break;
24907
25307
  case "implementer":
24908
- prompt = await PromptBuilder.for("implementer", { variant: lite ? "lite" : "standard" }).withLoader(workdir, config2).story(story).context(contextMarkdown).build();
25308
+ prompt = await PromptBuilder.for("implementer", { variant: lite ? "lite" : "standard" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).build();
24909
25309
  break;
24910
25310
  case "verifier":
24911
25311
  prompt = await PromptBuilder.for("verifier").withLoader(workdir, config2).story(story).context(contextMarkdown).build();
@@ -24939,7 +25339,7 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
24939
25339
  exitCode: result.exitCode
24940
25340
  });
24941
25341
  }
24942
- await autoCommitIfDirty2(workdir, role, story.id);
25342
+ await autoCommitIfDirty(workdir, "tdd", role, story.id);
24943
25343
  let isolation;
24944
25344
  if (!skipIsolation) {
24945
25345
  if (role === "test-writer") {
@@ -24986,42 +25386,11 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
24986
25386
  estimatedCost: result.estimatedCost
24987
25387
  };
24988
25388
  }
24989
- async function autoCommitIfDirty2(workdir, role, storyId) {
24990
- const logger = getLogger();
24991
- try {
24992
- const statusProc = Bun.spawn(["git", "status", "--porcelain"], {
24993
- cwd: workdir,
24994
- stdout: "pipe",
24995
- stderr: "pipe"
24996
- });
24997
- const statusOutput = await new Response(statusProc.stdout).text();
24998
- await statusProc.exited;
24999
- if (!statusOutput.trim())
25000
- return;
25001
- logger.warn("tdd", `Agent did not commit after ${role} session \u2014 auto-committing`, {
25002
- role,
25003
- storyId,
25004
- dirtyFiles: statusOutput.trim().split(`
25005
- `).length
25006
- });
25007
- const addProc = Bun.spawn(["git", "add", "-A"], {
25008
- cwd: workdir,
25009
- stdout: "pipe",
25010
- stderr: "pipe"
25011
- });
25012
- await addProc.exited;
25013
- const commitProc = Bun.spawn(["git", "commit", "-m", `chore(${storyId}): auto-commit after ${role} session`], {
25014
- cwd: workdir,
25015
- stdout: "pipe",
25016
- stderr: "pipe"
25017
- });
25018
- await commitProc.exited;
25019
- } catch {}
25020
- }
25021
25389
  var init_session_runner = __esm(() => {
25022
25390
  init_config();
25023
25391
  init_logger2();
25024
25392
  init_prompts2();
25393
+ init_git();
25025
25394
  init_cleanup();
25026
25395
  init_isolation();
25027
25396
  });
@@ -25077,6 +25446,95 @@ function isValidVerdict(obj) {
25077
25446
  return false;
25078
25447
  return true;
25079
25448
  }
25449
+ function coerceVerdict(obj) {
25450
+ try {
25451
+ const verdictStr = String(obj.verdict ?? "").toUpperCase();
25452
+ const approved = verdictStr === "PASS" || verdictStr === "APPROVED" || verdictStr.startsWith("VERIFIED") || verdictStr.includes("ALL ACCEPTANCE CRITERIA MET") || obj.approved === true;
25453
+ let passCount = 0;
25454
+ let failCount = 0;
25455
+ let allPassing = approved;
25456
+ const summary = obj.verification_summary;
25457
+ if (summary?.test_results && typeof summary.test_results === "string") {
25458
+ const match = summary.test_results.match(/(\d+)\/(\d+)/);
25459
+ if (match) {
25460
+ passCount = Number.parseInt(match[1], 10);
25461
+ const total = Number.parseInt(match[2], 10);
25462
+ failCount = total - passCount;
25463
+ allPassing = failCount === 0;
25464
+ }
25465
+ }
25466
+ if (obj.tests && typeof obj.tests === "object") {
25467
+ const t = obj.tests;
25468
+ if (typeof t.passCount === "number")
25469
+ passCount = t.passCount;
25470
+ if (typeof t.failCount === "number")
25471
+ failCount = t.failCount;
25472
+ if (typeof t.allPassing === "boolean")
25473
+ allPassing = t.allPassing;
25474
+ }
25475
+ const criteria = [];
25476
+ let allMet = approved;
25477
+ const acReview = obj.acceptance_criteria_review;
25478
+ if (acReview) {
25479
+ for (const [key, val] of Object.entries(acReview)) {
25480
+ if (key.startsWith("criterion") && val && typeof val === "object") {
25481
+ const c = val;
25482
+ const met = String(c.status ?? "").toUpperCase() === "SATISFIED" || c.met === true;
25483
+ criteria.push({
25484
+ criterion: String(c.name ?? c.criterion ?? key),
25485
+ met,
25486
+ note: c.evidence ? String(c.evidence).slice(0, 200) : undefined
25487
+ });
25488
+ if (!met)
25489
+ allMet = false;
25490
+ }
25491
+ }
25492
+ }
25493
+ if (obj.acceptanceCriteria && typeof obj.acceptanceCriteria === "object") {
25494
+ const ac = obj.acceptanceCriteria;
25495
+ if (typeof ac.allMet === "boolean")
25496
+ allMet = ac.allMet;
25497
+ if (Array.isArray(ac.criteria)) {
25498
+ for (const c of ac.criteria) {
25499
+ if (c && typeof c === "object") {
25500
+ criteria.push(c);
25501
+ }
25502
+ }
25503
+ }
25504
+ }
25505
+ if (criteria.length === 0 && summary?.acceptance_criteria && typeof summary.acceptance_criteria === "string") {
25506
+ const acMatch = summary.acceptance_criteria.match(/(\d+)\/(\d+)/);
25507
+ if (acMatch) {
25508
+ const met = Number.parseInt(acMatch[1], 10);
25509
+ const total = Number.parseInt(acMatch[2], 10);
25510
+ allMet = met === total;
25511
+ }
25512
+ }
25513
+ let rating = "acceptable";
25514
+ const qualityStr = summary?.code_quality ? String(summary.code_quality).toLowerCase() : obj.quality && typeof obj.quality === "object" ? String(obj.quality.rating ?? "acceptable").toLowerCase() : "acceptable";
25515
+ if (qualityStr === "high" || qualityStr === "good")
25516
+ rating = "good";
25517
+ else if (qualityStr === "low" || qualityStr === "poor")
25518
+ rating = "poor";
25519
+ return {
25520
+ version: 1,
25521
+ approved,
25522
+ tests: { allPassing, passCount, failCount },
25523
+ testModifications: {
25524
+ detected: false,
25525
+ files: [],
25526
+ legitimate: true,
25527
+ reasoning: "Not assessed in free-form verdict"
25528
+ },
25529
+ acceptanceCriteria: { allMet, criteria },
25530
+ quality: { rating, issues: [] },
25531
+ fixes: Array.isArray(obj.fixes) ? obj.fixes : [],
25532
+ 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}`
25533
+ };
25534
+ } catch {
25535
+ return null;
25536
+ }
25537
+ }
25080
25538
  async function readVerdict(workdir) {
25081
25539
  const logger = getLogger();
25082
25540
  const verdictPath = path8.join(workdir, VERDICT_FILE);
@@ -25086,24 +25544,47 @@ async function readVerdict(workdir) {
25086
25544
  if (!exists) {
25087
25545
  return null;
25088
25546
  }
25089
- let parsed;
25547
+ let rawText;
25090
25548
  try {
25091
- parsed = await file2.json();
25092
- } catch (parseErr) {
25093
- logger.warn("tdd", "Verifier verdict file is not valid JSON \u2014 ignoring", {
25549
+ rawText = await file2.text();
25550
+ } catch (readErr) {
25551
+ logger.warn("tdd", "Failed to read verifier verdict file", {
25094
25552
  path: verdictPath,
25095
- error: String(parseErr)
25553
+ error: String(readErr)
25096
25554
  });
25097
25555
  return null;
25098
25556
  }
25099
- if (!isValidVerdict(parsed)) {
25100
- logger.warn("tdd", "Verifier verdict file missing required fields \u2014 ignoring", {
25557
+ let parsed;
25558
+ try {
25559
+ parsed = JSON.parse(rawText);
25560
+ } catch (parseErr) {
25561
+ logger.warn("tdd", "Verifier verdict file is not valid JSON \u2014 ignoring", {
25101
25562
  path: verdictPath,
25102
- content: JSON.stringify(parsed).slice(0, 500)
25563
+ error: String(parseErr),
25564
+ rawContent: rawText.slice(0, 1000)
25103
25565
  });
25104
25566
  return null;
25105
25567
  }
25106
- return parsed;
25568
+ if (isValidVerdict(parsed)) {
25569
+ return parsed;
25570
+ }
25571
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
25572
+ const coerced = coerceVerdict(parsed);
25573
+ if (coerced) {
25574
+ logger.info("tdd", "Coerced free-form verdict to structured format", {
25575
+ path: verdictPath,
25576
+ approved: coerced.approved,
25577
+ passCount: coerced.tests.passCount,
25578
+ failCount: coerced.tests.failCount
25579
+ });
25580
+ return coerced;
25581
+ }
25582
+ }
25583
+ logger.warn("tdd", "Verifier verdict file missing required fields and coercion failed \u2014 ignoring", {
25584
+ path: verdictPath,
25585
+ content: JSON.stringify(parsed).slice(0, 500)
25586
+ });
25587
+ return null;
25107
25588
  } catch (err) {
25108
25589
  logger.warn("tdd", "Failed to read verifier verdict file \u2014 ignoring", {
25109
25590
  path: verdictPath,
@@ -25182,6 +25663,7 @@ async function runThreeSessionTdd(options) {
25182
25663
  workdir,
25183
25664
  modelTier,
25184
25665
  contextMarkdown,
25666
+ constitution,
25185
25667
  dryRun = false,
25186
25668
  lite = false,
25187
25669
  _recursionDepth = 0
@@ -25243,7 +25725,7 @@ async function runThreeSessionTdd(options) {
25243
25725
  let session1;
25244
25726
  if (!isRetry) {
25245
25727
  const testWriterTier = config2.tdd.sessionTiers?.testWriter ?? "balanced";
25246
- session1 = await runTddSession("test-writer", agent, story, config2, workdir, testWriterTier, session1Ref, contextMarkdown, lite, lite);
25728
+ session1 = await runTddSession("test-writer", agent, story, config2, workdir, testWriterTier, session1Ref, contextMarkdown, lite, lite, constitution);
25247
25729
  sessions.push(session1);
25248
25730
  }
25249
25731
  if (session1 && !session1.success) {
@@ -25305,7 +25787,7 @@ async function runThreeSessionTdd(options) {
25305
25787
  });
25306
25788
  const session2Ref = await captureGitRef(workdir) ?? "HEAD";
25307
25789
  const implementerTier = config2.tdd.sessionTiers?.implementer ?? modelTier;
25308
- const session2 = await runTddSession("implementer", agent, story, config2, workdir, implementerTier, session2Ref, contextMarkdown, lite, lite);
25790
+ const session2 = await runTddSession("implementer", agent, story, config2, workdir, implementerTier, session2Ref, contextMarkdown, lite, lite, constitution);
25309
25791
  sessions.push(session2);
25310
25792
  if (!session2.success) {
25311
25793
  needsHumanReview = true;
@@ -25324,7 +25806,7 @@ async function runThreeSessionTdd(options) {
25324
25806
  const fullSuiteGatePassed = await runFullSuiteGate(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger);
25325
25807
  const session3Ref = await captureGitRef(workdir) ?? "HEAD";
25326
25808
  const verifierTier = config2.tdd.sessionTiers?.verifier ?? "fast";
25327
- const session3 = await runTddSession("verifier", agent, story, config2, workdir, verifierTier, session3Ref, undefined, false, false);
25809
+ const session3 = await runTddSession("verifier", agent, story, config2, workdir, verifierTier, session3Ref, undefined, false, false, constitution);
25328
25810
  sessions.push(session3);
25329
25811
  const verdict = await readVerdict(workdir);
25330
25812
  await cleanupVerdict(workdir);
@@ -25424,6 +25906,7 @@ var init_orchestrator2 = __esm(() => {
25424
25906
  init_config();
25425
25907
  init_greenfield();
25426
25908
  init_logger2();
25909
+ init_git();
25427
25910
  init_verification();
25428
25911
  init_rectification_gate();
25429
25912
  init_session_runner();
@@ -25469,34 +25952,6 @@ function routeTddFailure(failureCategory, isLiteMode, ctx, reviewReason) {
25469
25952
  reason: reviewReason || "Three-session TDD requires review"
25470
25953
  };
25471
25954
  }
25472
- async function autoCommitIfDirty3(workdir, role, storyId) {
25473
- try {
25474
- const statusProc = Bun.spawn(["git", "status", "--porcelain"], {
25475
- cwd: workdir,
25476
- stdout: "pipe",
25477
- stderr: "pipe"
25478
- });
25479
- const statusOutput = await new Response(statusProc.stdout).text();
25480
- await statusProc.exited;
25481
- if (!statusOutput.trim())
25482
- return;
25483
- const logger = getLogger();
25484
- logger.warn("execution", `Agent did not commit after ${role} session \u2014 auto-committing`, {
25485
- role,
25486
- storyId,
25487
- dirtyFiles: statusOutput.trim().split(`
25488
- `).length
25489
- });
25490
- const addProc = Bun.spawn(["git", "add", "-A"], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
25491
- await addProc.exited;
25492
- const commitProc = Bun.spawn(["git", "commit", "-m", `chore(${storyId}): auto-commit after ${role} session`], {
25493
- cwd: workdir,
25494
- stdout: "pipe",
25495
- stderr: "pipe"
25496
- });
25497
- await commitProc.exited;
25498
- } catch {}
25499
- }
25500
25955
  var executionStage, _executionDeps;
25501
25956
  var init_execution = __esm(() => {
25502
25957
  init_agents();
@@ -25504,6 +25959,7 @@ var init_execution = __esm(() => {
25504
25959
  init_triggers();
25505
25960
  init_logger2();
25506
25961
  init_tdd();
25962
+ init_git();
25507
25963
  executionStage = {
25508
25964
  name: "execution",
25509
25965
  enabled: () => true,
@@ -25530,6 +25986,7 @@ var init_execution = __esm(() => {
25530
25986
  workdir: ctx.workdir,
25531
25987
  modelTier: ctx.routing.modelTier,
25532
25988
  contextMarkdown: ctx.contextMarkdown,
25989
+ constitution: ctx.constitution?.content,
25533
25990
  dryRun: false,
25534
25991
  lite: isLiteMode
25535
25992
  });
@@ -25553,6 +26010,28 @@ var init_execution = __esm(() => {
25553
26010
  lite: tddResult.lite,
25554
26011
  failureCategory: tddResult.failureCategory
25555
26012
  });
26013
+ if (ctx.interaction) {
26014
+ try {
26015
+ await ctx.interaction.send({
26016
+ id: `human-review-${ctx.story.id}-${Date.now()}`,
26017
+ type: "notify",
26018
+ featureName: ctx.featureDir ? ctx.featureDir.split("/").pop() ?? "unknown" : "unknown",
26019
+ storyId: ctx.story.id,
26020
+ stage: "execution",
26021
+ summary: `\u26A0\uFE0F Human review needed: ${ctx.story.id}`,
26022
+ detail: `Story: ${ctx.story.title}
26023
+ Reason: ${tddResult.reviewReason ?? "No reason provided"}
26024
+ Category: ${tddResult.failureCategory ?? "unknown"}`,
26025
+ fallback: "continue",
26026
+ createdAt: Date.now()
26027
+ });
26028
+ } catch (notifyErr) {
26029
+ logger.warn("execution", "Failed to send human review notification", {
26030
+ storyId: ctx.story.id,
26031
+ error: String(notifyErr)
26032
+ });
26033
+ }
26034
+ }
25556
26035
  }
25557
26036
  return routeTddFailure(tddResult.failureCategory, isLiteMode, ctx, tddResult.reviewReason);
25558
26037
  }
@@ -25578,7 +26057,7 @@ var init_execution = __esm(() => {
25578
26057
  dangerouslySkipPermissions: ctx.config.execution.dangerouslySkipPermissions
25579
26058
  });
25580
26059
  ctx.agentResult = result;
25581
- await autoCommitIfDirty3(ctx.workdir, "single-session", ctx.story.id);
26060
+ await autoCommitIfDirty(ctx.workdir, "execution", "single-session", ctx.story.id);
25582
26061
  const combinedOutput = (result.output ?? "") + (result.stderr ?? "");
25583
26062
  if (_executionDeps.detectMergeConflict(combinedOutput) && ctx.interaction && isTriggerEnabled("merge-conflict", ctx.config)) {
25584
26063
  const shouldProceed = await _executionDeps.checkMergeConflict({ featureName: ctx.prd.feature, storyId: ctx.story.id }, ctx.config, ctx.interaction);
@@ -25623,16 +26102,11 @@ var init_execution = __esm(() => {
25623
26102
  };
25624
26103
  });
25625
26104
 
25626
- // src/optimizer/types.ts
25627
- function estimateTokens4(text) {
25628
- return Math.ceil(text.length / 4);
25629
- }
25630
-
25631
26105
  // src/optimizer/noop.optimizer.ts
25632
26106
  class NoopOptimizer {
25633
26107
  name = "noop";
25634
26108
  async optimize(input) {
25635
- const tokens = estimateTokens4(input.prompt);
26109
+ const tokens = estimateTokens(input.prompt);
25636
26110
  return {
25637
26111
  prompt: input.prompt,
25638
26112
  originalTokens: tokens,
@@ -25648,7 +26122,7 @@ var init_noop_optimizer = () => {};
25648
26122
  class RuleBasedOptimizer {
25649
26123
  name = "rule-based";
25650
26124
  async optimize(input) {
25651
- const originalTokens = estimateTokens4(input.prompt);
26125
+ const originalTokens = estimateTokens(input.prompt);
25652
26126
  const appliedRules = [];
25653
26127
  let optimized = input.prompt;
25654
26128
  const config2 = {
@@ -25677,13 +26151,13 @@ class RuleBasedOptimizer {
25677
26151
  }
25678
26152
  }
25679
26153
  if (config2.maxPromptTokens) {
25680
- const currentTokens = estimateTokens4(optimized);
26154
+ const currentTokens = estimateTokens(optimized);
25681
26155
  if (currentTokens > config2.maxPromptTokens) {
25682
26156
  optimized = this.trimToMaxTokens(optimized, config2.maxPromptTokens);
25683
26157
  appliedRules.push("maxPromptTokens");
25684
26158
  }
25685
26159
  }
25686
- const optimizedTokens = estimateTokens4(optimized);
26160
+ const optimizedTokens = estimateTokens(optimized);
25687
26161
  const savings = originalTokens > 0 ? (originalTokens - optimizedTokens) / originalTokens : 0;
25688
26162
  return {
25689
26163
  prompt: optimized,
@@ -25726,7 +26200,7 @@ ${newContextSection}`);
25726
26200
  return prompt;
25727
26201
  }
25728
26202
  trimToMaxTokens(prompt, maxTokens) {
25729
- const currentTokens = estimateTokens4(prompt);
26203
+ const currentTokens = estimateTokens(prompt);
25730
26204
  if (currentTokens <= maxTokens) {
25731
26205
  return prompt;
25732
26206
  }
@@ -25865,7 +26339,7 @@ var init_optimizer2 = __esm(() => {
25865
26339
  });
25866
26340
 
25867
26341
  // src/execution/prompts.ts
25868
- function buildBatchPrompt2(stories, contextMarkdown, constitution) {
26342
+ function buildBatchPrompt(stories, contextMarkdown, constitution) {
25869
26343
  const storyPrompts = stories.map((story, idx) => {
25870
26344
  return `## Story ${idx + 1}: ${story.id} \u2014 ${story.title}
25871
26345
 
@@ -25923,7 +26397,7 @@ var init_prompt = __esm(() => {
25923
26397
  const isBatch = ctx.stories.length > 1;
25924
26398
  let prompt;
25925
26399
  if (isBatch) {
25926
- prompt = buildBatchPrompt2(ctx.stories, ctx.contextMarkdown, ctx.constitution);
26400
+ prompt = buildBatchPrompt(ctx.stories, ctx.contextMarkdown, ctx.constitution);
25927
26401
  } else {
25928
26402
  const role = ctx.routing.testStrategy === "tdd-simple" ? "tdd-simple" : "single-session";
25929
26403
  const builder = PromptBuilder.for(role).withLoader(ctx.workdir, ctx.config).story(ctx.story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content);
@@ -26211,7 +26685,6 @@ ${rectificationPrompt}`;
26211
26685
  var init_rectification_loop = __esm(() => {
26212
26686
  init_agents();
26213
26687
  init_config();
26214
- init_progress();
26215
26688
  init_test_output_parser();
26216
26689
  init_logger2();
26217
26690
  init_prd();
@@ -26616,6 +27089,7 @@ function reverseMapTestToSource(testFiles, workdir) {
26616
27089
  }
26617
27090
  var _smartRunnerDeps;
26618
27091
  var init_smart_runner = __esm(() => {
27092
+ init_git();
26619
27093
  _smartRunnerDeps = {
26620
27094
  getChangedSourceFiles,
26621
27095
  mapSourceToTests,
@@ -26851,6 +27325,7 @@ async function runDecompose(story, prd, config2, _workdir) {
26851
27325
  }
26852
27326
  var routingStage, _routingDeps;
26853
27327
  var init_routing2 = __esm(() => {
27328
+ init_registry();
26854
27329
  init_greenfield();
26855
27330
  init_builder2();
26856
27331
  init_triggers();
@@ -26863,6 +27338,8 @@ var init_routing2 = __esm(() => {
26863
27338
  enabled: () => true,
26864
27339
  async execute(ctx) {
26865
27340
  const logger = getLogger();
27341
+ const agentName = ctx.config.execution?.agent ?? "claude";
27342
+ const adapter = _routingDeps.getAgent(agentName);
26866
27343
  const hasExistingRouting = ctx.story.routing !== undefined;
26867
27344
  const hasContentHash = ctx.story.routing?.contentHash !== undefined;
26868
27345
  let currentHash;
@@ -26874,7 +27351,7 @@ var init_routing2 = __esm(() => {
26874
27351
  const isCacheHit = hasExistingRouting && (!hasContentHash || hashMatch);
26875
27352
  let routing;
26876
27353
  if (isCacheHit) {
26877
- routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config }, ctx.workdir, ctx.plugins);
27354
+ routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config, adapter }, ctx.workdir, ctx.plugins);
26878
27355
  if (ctx.story.routing?.complexity)
26879
27356
  routing.complexity = ctx.story.routing.complexity;
26880
27357
  if (!hasContentHash && ctx.story.routing?.testStrategy)
@@ -26885,7 +27362,7 @@ var init_routing2 = __esm(() => {
26885
27362
  routing.modelTier = _routingDeps.complexityToModelTier(routing.complexity, ctx.config);
26886
27363
  }
26887
27364
  } else {
26888
- routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config }, ctx.workdir, ctx.plugins);
27365
+ routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config, adapter }, ctx.workdir, ctx.plugins);
26889
27366
  currentHash = currentHash ?? _routingDeps.computeStoryContentHash(ctx.story);
26890
27367
  ctx.story.routing = {
26891
27368
  ...ctx.story.routing ?? {},
@@ -26974,7 +27451,8 @@ var init_routing2 = __esm(() => {
26974
27451
  computeStoryContentHash,
26975
27452
  applyDecomposition,
26976
27453
  runDecompose,
26977
- checkStoryOversized
27454
+ checkStoryOversized,
27455
+ getAgent
26978
27456
  };
26979
27457
  });
26980
27458
 
@@ -28065,10 +28543,55 @@ async function checkPromptOverrideFiles(config2, workdir) {
28065
28543
  }
28066
28544
  var init_checks_warnings = () => {};
28067
28545
 
28546
+ // src/precheck/checks-agents.ts
28547
+ async function checkMultiAgentHealth() {
28548
+ try {
28549
+ const versions2 = await getAgentVersions();
28550
+ const installed = versions2.filter((v) => v.installed);
28551
+ const notInstalled = versions2.filter((v) => !v.installed);
28552
+ const lines = [];
28553
+ if (installed.length > 0) {
28554
+ lines.push(`Installed agents (${installed.length}):`);
28555
+ for (const agent of installed) {
28556
+ const versionStr = agent.version ? ` v${agent.version}` : " (version unknown)";
28557
+ lines.push(` \u2022 ${agent.displayName}${versionStr}`);
28558
+ }
28559
+ } else {
28560
+ lines.push("No additional agents detected (using default configured agent)");
28561
+ }
28562
+ if (notInstalled.length > 0) {
28563
+ lines.push(`
28564
+ Available but not installed (${notInstalled.length}):`);
28565
+ for (const agent of notInstalled) {
28566
+ lines.push(` \u2022 ${agent.displayName}`);
28567
+ }
28568
+ }
28569
+ const message = lines.join(`
28570
+ `);
28571
+ return {
28572
+ name: "multi-agent-health",
28573
+ tier: "warning",
28574
+ passed: true,
28575
+ message
28576
+ };
28577
+ } catch (error48) {
28578
+ return {
28579
+ name: "multi-agent-health",
28580
+ tier: "warning",
28581
+ passed: true,
28582
+ message: `Agent detection: ${error48 instanceof Error ? error48.message : "Unknown error"}`
28583
+ };
28584
+ }
28585
+ }
28586
+ var init_checks_agents = __esm(() => {
28587
+ init_version_detection();
28588
+ });
28589
+
28068
28590
  // src/precheck/checks.ts
28069
28591
  var init_checks3 = __esm(() => {
28070
28592
  init_checks_blockers();
28071
28593
  init_checks_warnings();
28594
+ init_checks_agents();
28072
28595
  });
28073
28596
 
28074
28597
  // src/precheck/story-size-gate.ts
@@ -28214,7 +28737,8 @@ async function runPrecheck(config2, prd, options) {
28214
28737
  () => checkPendingStories(prd),
28215
28738
  () => checkOptionalCommands(config2, workdir),
28216
28739
  () => checkGitignoreCoversNax(workdir),
28217
- () => checkPromptOverrideFiles(config2, workdir)
28740
+ () => checkPromptOverrideFiles(config2, workdir),
28741
+ () => checkMultiAgentHealth()
28218
28742
  ];
28219
28743
  for (const checkFn of tier2Checks) {
28220
28744
  const result = await checkFn();
@@ -29364,6 +29888,7 @@ var init_run_initialization = __esm(() => {
29364
29888
  init_errors3();
29365
29889
  init_logger2();
29366
29890
  init_prd();
29891
+ init_git();
29367
29892
  });
29368
29893
 
29369
29894
  // src/execution/lifecycle/run-setup.ts
@@ -30947,6 +31472,7 @@ var init_iteration_runner = __esm(() => {
30947
31472
  init_logger2();
30948
31473
  init_runner();
30949
31474
  init_stages();
31475
+ init_git();
30950
31476
  init_dry_run();
30951
31477
  init_pipeline_result_handler();
30952
31478
  });
@@ -31515,6 +32041,7 @@ var _regressionDeps;
31515
32041
  var init_run_regression = __esm(() => {
31516
32042
  init_logger2();
31517
32043
  init_prd();
32044
+ init_git();
31518
32045
  init_verification();
31519
32046
  init_rectification_loop();
31520
32047
  init_runners();
@@ -62594,9 +63121,6 @@ var {
62594
63121
  Help
62595
63122
  } = import__.default;
62596
63123
 
62597
- // bin/nax.ts
62598
- init_agents();
62599
-
62600
63124
  // src/cli/analyze.ts
62601
63125
  init_acceptance();
62602
63126
  init_registry();
@@ -64717,6 +65241,25 @@ var claudeGenerator = {
64717
65241
  generate: generateClaudeConfig
64718
65242
  };
64719
65243
 
65244
+ // src/context/generators/codex.ts
65245
+ function generateCodexConfig(context) {
65246
+ const header = `# Codex Instructions
65247
+
65248
+ This file is auto-generated from \`nax/context.md\`.
65249
+ DO NOT EDIT MANUALLY \u2014 run \`nax generate\` to regenerate.
65250
+
65251
+ ---
65252
+
65253
+ `;
65254
+ const metaSection = context.metadata ? formatMetadataSection(context.metadata) : "";
65255
+ return header + metaSection + context.markdown;
65256
+ }
65257
+ var codexGenerator = {
65258
+ name: "codex",
65259
+ outputFile: "codex.md",
65260
+ generate: generateCodexConfig
65261
+ };
65262
+
64720
65263
  // src/context/generators/cursor.ts
64721
65264
  function generateCursorRules(context) {
64722
65265
  const header = `# Project Rules
@@ -64736,6 +65279,25 @@ var cursorGenerator = {
64736
65279
  generate: generateCursorRules
64737
65280
  };
64738
65281
 
65282
+ // src/context/generators/gemini.ts
65283
+ function generateGeminiConfig(context) {
65284
+ const header = `# Gemini CLI Context
65285
+
65286
+ This file is auto-generated from \`nax/context.md\`.
65287
+ DO NOT EDIT MANUALLY \u2014 run \`nax generate\` to regenerate.
65288
+
65289
+ ---
65290
+
65291
+ `;
65292
+ const metaSection = context.metadata ? formatMetadataSection(context.metadata) : "";
65293
+ return header + metaSection + context.markdown;
65294
+ }
65295
+ var geminiGenerator = {
65296
+ name: "gemini",
65297
+ outputFile: "GEMINI.md",
65298
+ generate: generateGeminiConfig
65299
+ };
65300
+
64739
65301
  // src/context/generators/opencode.ts
64740
65302
  function generateOpencodeConfig(context) {
64741
65303
  const header = `# Agent Instructions
@@ -64779,10 +65341,12 @@ var windsurfGenerator = {
64779
65341
  // src/context/generator.ts
64780
65342
  var GENERATORS = {
64781
65343
  claude: claudeGenerator,
65344
+ codex: codexGenerator,
64782
65345
  opencode: opencodeGenerator,
64783
65346
  cursor: cursorGenerator,
64784
65347
  windsurf: windsurfGenerator,
64785
- aider: aiderGenerator
65348
+ aider: aiderGenerator,
65349
+ gemini: geminiGenerator
64786
65350
  };
64787
65351
  async function loadContextContent(options, config2) {
64788
65352
  if (!existsSync18(options.contextPath)) {
@@ -64834,7 +65398,7 @@ async function generateAll(options, config2) {
64834
65398
  }
64835
65399
 
64836
65400
  // src/cli/generate.ts
64837
- var VALID_AGENTS = ["claude", "opencode", "cursor", "windsurf", "aider"];
65401
+ var VALID_AGENTS = ["claude", "codex", "opencode", "cursor", "windsurf", "aider", "gemini"];
64838
65402
  async function generateCommand(options) {
64839
65403
  const workdir = process.cwd();
64840
65404
  const contextPath = options.context ? join24(workdir, options.context) : join24(workdir, "nax/context.md");
@@ -64922,19 +65486,19 @@ var FIELD_DESCRIPTIONS = {
64922
65486
  "models.fast": "Fast model for lightweight tasks (e.g., haiku)",
64923
65487
  "models.balanced": "Balanced model for general coding (e.g., sonnet)",
64924
65488
  "models.powerful": "Powerful model for complex tasks (e.g., opus)",
64925
- autoMode: "Auto mode configuration for agent orchestration",
65489
+ autoMode: "Auto mode configuration for agent orchestration. Enables multi-agent routing with model tier selection per task complexity and escalation on failures.",
64926
65490
  "autoMode.enabled": "Enable automatic agent selection and escalation",
64927
- "autoMode.defaultAgent": "Default agent to use (e.g., claude, codex)",
64928
- "autoMode.fallbackOrder": "Fallback order when agent is rate-limited",
64929
- "autoMode.complexityRouting": "Model tier per complexity level",
64930
- "autoMode.complexityRouting.simple": "Model tier for simple tasks",
64931
- "autoMode.complexityRouting.medium": "Model tier for medium tasks",
64932
- "autoMode.complexityRouting.complex": "Model tier for complex tasks",
64933
- "autoMode.complexityRouting.expert": "Model tier for expert tasks",
64934
- "autoMode.escalation": "Escalation settings for failed stories",
65491
+ "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.",
65492
+ "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.',
65493
+ "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.",
65494
+ "autoMode.complexityRouting.simple": "Model tier for simple tasks (low complexity, straightforward changes)",
65495
+ "autoMode.complexityRouting.medium": "Model tier for medium tasks (moderate complexity, multi-file changes)",
65496
+ "autoMode.complexityRouting.complex": "Model tier for complex tasks (high complexity, architectural decisions)",
65497
+ "autoMode.complexityRouting.expert": "Model tier for expert tasks (highest complexity, novel problems, design patterns)",
65498
+ "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.",
64935
65499
  "autoMode.escalation.enabled": "Enable tier escalation on failure",
64936
- "autoMode.escalation.tierOrder": "Ordered tier escalation with per-tier attempt budgets",
64937
- "autoMode.escalation.escalateEntireBatch": "Escalate all stories in batch when one fails",
65500
+ "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.',
65501
+ "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).",
64938
65502
  routing: "Model routing strategy configuration",
64939
65503
  "routing.strategy": "Routing strategy: keyword | llm | manual | adaptive | custom",
64940
65504
  "routing.customStrategyPath": "Path to custom routing strategy (if strategy=custom)",
@@ -65243,8 +65807,11 @@ function displayConfigWithDescriptions(obj, path13, sources, indent = 0) {
65243
65807
  const currentPathStr = currentPath.join(".");
65244
65808
  const description = FIELD_DESCRIPTIONS[currentPathStr];
65245
65809
  if (description) {
65246
- const isPromptsSubSection = currentPathStr.startsWith("prompts.");
65247
- const comment = isPromptsSubSection ? `${currentPathStr}: ${description}` : description;
65810
+ const pathParts = currentPathStr.split(".");
65811
+ const isDirectSubsection = pathParts.length === 2;
65812
+ const isKeySection = ["prompts", "autoMode", "models", "routing"].includes(pathParts[0]);
65813
+ const shouldIncludePath = isKeySection && isDirectSubsection;
65814
+ const comment = shouldIncludePath ? `${currentPathStr}: ${description}` : description;
65248
65815
  console.log(`${indentStr}# ${comment}`);
65249
65816
  }
65250
65817
  if (value !== null && typeof value === "object" && !Array.isArray(value)) {
@@ -65312,6 +65879,55 @@ function formatValueForTable(value) {
65312
65879
  }
65313
65880
  return String(value);
65314
65881
  }
65882
+ // src/cli/agents.ts
65883
+ init_registry();
65884
+ init_version_detection();
65885
+ async function agentsListCommand(config2, _workdir) {
65886
+ const agentVersions = await Promise.all(ALL_AGENTS.map(async (agent) => ({
65887
+ name: agent.name,
65888
+ displayName: agent.displayName,
65889
+ binary: agent.binary,
65890
+ version: await getAgentVersion(agent.binary),
65891
+ installed: await agent.isInstalled(),
65892
+ capabilities: agent.capabilities,
65893
+ isDefault: config2.autoMode.defaultAgent === agent.name
65894
+ })));
65895
+ const rows = agentVersions.map((info) => {
65896
+ const status = info.installed ? "installed" : "unavailable";
65897
+ const versionStr = info.version || "-";
65898
+ const defaultMarker = info.isDefault ? " (default)" : "";
65899
+ return {
65900
+ name: info.displayName + defaultMarker,
65901
+ status,
65902
+ version: versionStr,
65903
+ binary: info.binary,
65904
+ tiers: info.capabilities.supportedTiers.join(", ")
65905
+ };
65906
+ });
65907
+ if (rows.length === 0) {
65908
+ console.log("No agents available.");
65909
+ return;
65910
+ }
65911
+ const widths = {
65912
+ name: Math.max(5, ...rows.map((r) => r.name.length)),
65913
+ status: Math.max(6, ...rows.map((r) => r.status.length)),
65914
+ version: Math.max(7, ...rows.map((r) => r.version.length)),
65915
+ binary: Math.max(6, ...rows.map((r) => r.binary.length)),
65916
+ tiers: Math.max(5, ...rows.map((r) => r.tiers.length))
65917
+ };
65918
+ console.log(`
65919
+ Available Agents:
65920
+ `);
65921
+ console.log(`${pad2("Agent", widths.name)} ${pad2("Status", widths.status)} ${pad2("Version", widths.version)} ${pad2("Binary", widths.binary)} ${pad2("Tiers", widths.tiers)}`);
65922
+ console.log(`${"-".repeat(widths.name)} ${"-".repeat(widths.status)} ${"-".repeat(widths.version)} ${"-".repeat(widths.binary)} ${"-".repeat(widths.tiers)}`);
65923
+ for (const row of rows) {
65924
+ 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)}`);
65925
+ }
65926
+ console.log();
65927
+ }
65928
+ function pad2(str, width) {
65929
+ return str.padEnd(width);
65930
+ }
65315
65931
  // src/commands/diagnose.ts
65316
65932
  async function diagnose(options) {
65317
65933
  await diagnoseCommand(options);
@@ -65688,7 +66304,7 @@ var ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
65688
66304
  function visibleLength(str) {
65689
66305
  return str.replace(ANSI_RE, "").length;
65690
66306
  }
65691
- function pad2(str, width) {
66307
+ function pad3(str, width) {
65692
66308
  const padding = Math.max(0, width - visibleLength(str));
65693
66309
  return str + " ".repeat(padding);
65694
66310
  }
@@ -65747,12 +66363,12 @@ async function runsCommand(options = {}) {
65747
66363
  date: 11
65748
66364
  };
65749
66365
  const header = [
65750
- pad2(source_default.bold("RUN ID"), COL.runId),
65751
- pad2(source_default.bold("PROJECT"), COL.project),
65752
- pad2(source_default.bold("FEATURE"), COL.feature),
65753
- pad2(source_default.bold("STATUS"), COL.status),
65754
- pad2(source_default.bold("STORIES"), COL.stories),
65755
- pad2(source_default.bold("DURATION"), COL.duration),
66366
+ pad3(source_default.bold("RUN ID"), COL.runId),
66367
+ pad3(source_default.bold("PROJECT"), COL.project),
66368
+ pad3(source_default.bold("FEATURE"), COL.feature),
66369
+ pad3(source_default.bold("STATUS"), COL.status),
66370
+ pad3(source_default.bold("STORIES"), COL.stories),
66371
+ pad3(source_default.bold("DURATION"), COL.duration),
65756
66372
  source_default.bold("DATE")
65757
66373
  ].join(" ");
65758
66374
  console.log();
@@ -65761,12 +66377,12 @@ async function runsCommand(options = {}) {
65761
66377
  for (const row of displayed) {
65762
66378
  const colored = colorStatus(row.status);
65763
66379
  const line = [
65764
- pad2(row.runId, COL.runId),
65765
- pad2(row.project, COL.project),
65766
- pad2(row.feature, COL.feature),
65767
- pad2(colored, COL.status + (colored.length - visibleLength(colored))),
65768
- pad2(`${row.passed}/${row.total}`, COL.stories),
65769
- pad2(formatDuration3(row.durationMs), COL.duration),
66380
+ pad3(row.runId, COL.runId),
66381
+ pad3(row.project, COL.project),
66382
+ pad3(row.feature, COL.feature),
66383
+ pad3(colored, COL.status + (colored.length - visibleLength(colored))),
66384
+ pad3(`${row.passed}/${row.total}`, COL.stories),
66385
+ pad3(formatDuration3(row.durationMs), COL.duration),
65770
66386
  formatDate(row.registeredAt)
65771
66387
  ].join(" ");
65772
66388
  console.log(line);
@@ -73839,16 +74455,21 @@ program2.command("analyze").description("Parse spec.md into prd.json via agent d
73839
74455
  process.exit(1);
73840
74456
  }
73841
74457
  });
73842
- program2.command("agents").description("Check available coding agents").action(async () => {
73843
- const health = await checkAgentHealth();
73844
- console.log(source_default.bold(`
73845
- Coding Agents:
73846
- `));
73847
- for (const agent of health) {
73848
- const status = agent.installed ? source_default.green("\u2705 installed") : source_default.red("\u274C not found");
73849
- console.log(` ${agent.displayName.padEnd(15)} ${status}`);
74458
+ program2.command("agents").description("List available coding agents with status and capabilities").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
74459
+ let workdir;
74460
+ try {
74461
+ workdir = validateDirectory(options.dir);
74462
+ } catch (err) {
74463
+ console.error(source_default.red(`Invalid directory: ${err.message}`));
74464
+ process.exit(1);
74465
+ }
74466
+ try {
74467
+ const config2 = await loadConfig(workdir);
74468
+ await agentsListCommand(config2, workdir);
74469
+ } catch (err) {
74470
+ console.error(source_default.red(`Error: ${err.message}`));
74471
+ process.exit(1);
73850
74472
  }
73851
- console.log();
73852
74473
  });
73853
74474
  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) => {
73854
74475
  try {