@nathapp/nax 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/nax.js +1166 -277
  2. package/package.json +2 -2
  3. package/src/acceptance/fix-generator.ts +4 -35
  4. package/src/acceptance/generator.ts +27 -28
  5. package/src/acceptance/refinement.ts +72 -5
  6. package/src/acceptance/templates/cli.ts +47 -0
  7. package/src/acceptance/templates/component.ts +78 -0
  8. package/src/acceptance/templates/e2e.ts +43 -0
  9. package/src/acceptance/templates/index.ts +21 -0
  10. package/src/acceptance/templates/snapshot.ts +50 -0
  11. package/src/acceptance/templates/unit.ts +48 -0
  12. package/src/acceptance/types.ts +9 -1
  13. package/src/agents/acp/adapter.ts +644 -0
  14. package/src/agents/acp/cost.ts +79 -0
  15. package/src/agents/acp/index.ts +9 -0
  16. package/src/agents/acp/interaction-bridge.ts +126 -0
  17. package/src/agents/acp/parser.ts +166 -0
  18. package/src/agents/acp/spawn-client.ts +309 -0
  19. package/src/agents/acp/types.ts +22 -0
  20. package/src/agents/claude-complete.ts +3 -3
  21. package/src/agents/registry.ts +83 -0
  22. package/src/agents/types-extended.ts +23 -0
  23. package/src/agents/types.ts +17 -0
  24. package/src/cli/analyze.ts +6 -2
  25. package/src/cli/init-detect.ts +94 -8
  26. package/src/cli/init.ts +2 -2
  27. package/src/cli/plan.ts +23 -0
  28. package/src/config/defaults.ts +1 -0
  29. package/src/config/index.ts +1 -1
  30. package/src/config/runtime-types.ts +17 -0
  31. package/src/config/schema.ts +3 -1
  32. package/src/config/schemas.ts +9 -1
  33. package/src/config/types.ts +2 -0
  34. package/src/execution/executor-types.ts +6 -0
  35. package/src/execution/iteration-runner.ts +2 -0
  36. package/src/execution/lifecycle/acceptance-loop.ts +5 -2
  37. package/src/execution/lifecycle/run-initialization.ts +16 -4
  38. package/src/execution/lifecycle/run-setup.ts +4 -0
  39. package/src/execution/runner-completion.ts +11 -1
  40. package/src/execution/runner-execution.ts +8 -0
  41. package/src/execution/runner-setup.ts +4 -0
  42. package/src/execution/runner.ts +10 -0
  43. package/src/pipeline/stages/acceptance-setup.ts +4 -0
  44. package/src/pipeline/stages/execution.ts +33 -1
  45. package/src/pipeline/stages/routing.ts +18 -7
  46. package/src/pipeline/types.ts +10 -0
  47. package/src/tdd/orchestrator.ts +7 -0
  48. package/src/tdd/rectification-gate.ts +6 -0
  49. package/src/tdd/session-runner.ts +4 -0
package/dist/nax.js CHANGED
@@ -3241,9 +3241,6 @@ async function executeComplete(binary, prompt, options) {
3241
3241
  if (options?.model) {
3242
3242
  cmd.push("--model", options.model);
3243
3243
  }
3244
- if (options?.maxTokens !== undefined) {
3245
- cmd.push("--max-tokens", String(options.maxTokens));
3246
- }
3247
3244
  if (options?.jsonMode) {
3248
3245
  cmd.push("--output-format", "json");
3249
3246
  }
@@ -3469,6 +3466,17 @@ function estimateCostByDuration(modelTier, durationMs) {
3469
3466
  confidence: "fallback"
3470
3467
  };
3471
3468
  }
3469
+ function formatCostWithConfidence(estimate) {
3470
+ const formattedCost = `$${estimate.cost.toFixed(2)}`;
3471
+ switch (estimate.confidence) {
3472
+ case "exact":
3473
+ return formattedCost;
3474
+ case "estimated":
3475
+ return `~${formattedCost}`;
3476
+ case "fallback":
3477
+ return `~${formattedCost} (duration-based)`;
3478
+ }
3479
+ }
3472
3480
  var COST_RATES;
3473
3481
  var init_cost = __esm(() => {
3474
3482
  COST_RATES = {
@@ -17525,7 +17533,7 @@ var init_zod = __esm(() => {
17525
17533
  });
17526
17534
 
17527
17535
  // src/config/schemas.ts
17528
- var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, AdaptiveRoutingConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, DecomposeConfigSchema, NaxConfigSchema;
17536
+ var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, AdaptiveRoutingConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, DecomposeConfigSchema, NaxConfigSchema;
17529
17537
  var init_schemas3 = __esm(() => {
17530
17538
  init_zod();
17531
17539
  TokenPricingSchema = exports_external.object({
@@ -17708,7 +17716,9 @@ var init_schemas3 = __esm(() => {
17708
17716
  testPath: exports_external.string().min(1, "acceptance.testPath must be non-empty"),
17709
17717
  model: exports_external.enum(["fast", "balanced", "powerful"]).default("fast"),
17710
17718
  refinement: exports_external.boolean().default(true),
17711
- redGate: exports_external.boolean().default(true)
17719
+ redGate: exports_external.boolean().default(true),
17720
+ testStrategy: exports_external.enum(["unit", "component", "cli", "e2e", "snapshot"]).optional(),
17721
+ testFramework: exports_external.string().min(1, "acceptance.testFramework must be non-empty").optional()
17712
17722
  });
17713
17723
  TestCoverageConfigSchema = exports_external.object({
17714
17724
  enabled: exports_external.boolean().default(true),
@@ -17792,6 +17802,10 @@ var init_schemas3 = __esm(() => {
17792
17802
  maxDescriptionLength: exports_external.number().int().min(100).max(1e4).default(2000),
17793
17803
  maxBulletPoints: exports_external.number().int().min(1).max(100).default(8)
17794
17804
  });
17805
+ AgentConfigSchema = exports_external.object({
17806
+ protocol: exports_external.enum(["acp", "cli"]).default("acp"),
17807
+ acpPermissionMode: exports_external.string().optional()
17808
+ });
17795
17809
  PrecheckConfigSchema = exports_external.object({
17796
17810
  storySizeGate: StorySizeGateConfigSchema
17797
17811
  });
@@ -17827,6 +17841,7 @@ var init_schemas3 = __esm(() => {
17827
17841
  disabledPlugins: exports_external.array(exports_external.string()).optional(),
17828
17842
  hooks: HooksConfigSchema.optional(),
17829
17843
  interaction: InteractionConfigSchema.optional(),
17844
+ agent: AgentConfigSchema.optional(),
17830
17845
  precheck: PrecheckConfigSchema.optional(),
17831
17846
  prompts: PromptsConfigSchema.optional(),
17832
17847
  decompose: DecomposeConfigSchema.optional()
@@ -18362,14 +18377,16 @@ __export(exports_refinement, {
18362
18377
  buildRefinementPrompt: () => buildRefinementPrompt,
18363
18378
  _refineDeps: () => _refineDeps
18364
18379
  });
18365
- function buildRefinementPrompt(criteria, codebaseContext) {
18380
+ function buildRefinementPrompt(criteria, codebaseContext, options) {
18366
18381
  const criteriaList = criteria.map((c, i) => `${i + 1}. ${c}`).join(`
18367
18382
  `);
18383
+ const strategySection = buildStrategySection(options);
18384
+ const refinedExample = buildRefinedExample(options?.testStrategy);
18368
18385
  return `You are an acceptance criteria refinement assistant. Your task is to convert raw acceptance criteria into concrete, machine-verifiable assertions.
18369
18386
 
18370
18387
  CODEBASE CONTEXT:
18371
18388
  ${codebaseContext}
18372
-
18389
+ ${strategySection}
18373
18390
  ACCEPTANCE CRITERIA TO REFINE:
18374
18391
  ${criteriaList}
18375
18392
 
@@ -18384,11 +18401,53 @@ Respond with ONLY a JSON array (no markdown code fences):
18384
18401
 
18385
18402
  Rules:
18386
18403
  - "original" must match the input criterion text exactly
18387
- - "refined" must be a concrete assertion (e.g., "Function returns array of length N", "HTTP status 200 returned")
18404
+ - "refined" must be a concrete assertion (e.g., ${refinedExample})
18388
18405
  - "testable" is false only if the criterion cannot be automatically verified (e.g., "UX feels responsive", "design looks good")
18389
18406
  - "storyId" leave as empty string \u2014 it will be assigned by the caller
18390
18407
  - Respond with ONLY the JSON array`;
18391
18408
  }
18409
+ function buildStrategySection(options) {
18410
+ if (!options?.testStrategy) {
18411
+ return "";
18412
+ }
18413
+ const framework = options.testFramework ? ` Use ${options.testFramework} testing library syntax.` : "";
18414
+ switch (options.testStrategy) {
18415
+ case "component":
18416
+ return `
18417
+ TEST STRATEGY: component
18418
+ Focus assertions on rendered output visible on screen \u2014 text content, visible elements, and screen state.
18419
+ Assert what the user sees rendered in the component, not what internal functions produce.${framework}
18420
+ `;
18421
+ case "cli":
18422
+ return `
18423
+ TEST STRATEGY: cli
18424
+ Focus assertions on stdout and stderr text output from the CLI command.
18425
+ Assert about terminal output content, exit codes, and standard output/standard error streams.${framework}
18426
+ `;
18427
+ case "e2e":
18428
+ return `
18429
+ TEST STRATEGY: e2e
18430
+ Focus assertions on HTTP response content \u2014 status codes, response bodies, and endpoint behavior.
18431
+ Assert about HTTP responses, status codes, and API endpoint output.${framework}
18432
+ `;
18433
+ default:
18434
+ return framework ? `
18435
+ TEST FRAMEWORK: ${options.testFramework}
18436
+ ` : "";
18437
+ }
18438
+ }
18439
+ function buildRefinedExample(testStrategy) {
18440
+ switch (testStrategy) {
18441
+ case "component":
18442
+ return '"Text content visible on screen matches expected", "Rendered output contains expected element"';
18443
+ case "cli":
18444
+ return '"stdout contains expected text", "stderr is empty on success", "exit code is 0"';
18445
+ case "e2e":
18446
+ return '"HTTP status 200 returned", "Response body contains expected field", "Endpoint returns JSON"';
18447
+ default:
18448
+ return '"Array of length N returned", "HTTP status 200 returned"';
18449
+ }
18450
+ }
18392
18451
  function parseRefinementResponse(response, criteria) {
18393
18452
  if (!response || !response.trim()) {
18394
18453
  return fallbackCriteria(criteria);
@@ -18412,7 +18471,7 @@ async function refineAcceptanceCriteria(criteria, context) {
18412
18471
  if (criteria.length === 0) {
18413
18472
  return [];
18414
18473
  }
18415
- const { storyId, codebaseContext, config: config2 } = context;
18474
+ const { storyId, codebaseContext, config: config2, testStrategy, testFramework } = context;
18416
18475
  const logger = getLogger();
18417
18476
  const modelTier = config2.acceptance?.model ?? "fast";
18418
18477
  const modelEntry = config2.models[modelTier] ?? config2.models.fast;
@@ -18420,7 +18479,7 @@ async function refineAcceptanceCriteria(criteria, context) {
18420
18479
  throw new Error(`[refinement] config.models.${modelTier} not configured`);
18421
18480
  }
18422
18481
  const modelDef = resolveModel(modelEntry);
18423
- const prompt = buildRefinementPrompt(criteria, codebaseContext);
18482
+ const prompt = buildRefinementPrompt(criteria, codebaseContext, { testStrategy, testFramework });
18424
18483
  let response;
18425
18484
  try {
18426
18485
  response = await _refineDeps.adapter.complete(prompt, {
@@ -18483,6 +18542,7 @@ async function generateFromPRD(_stories, refinedCriteria, options) {
18483
18542
  }
18484
18543
  const criteriaList = refinedCriteria.map((c, i) => `AC-${i + 1}: ${c.refined}`).join(`
18485
18544
  `);
18545
+ const strategyInstructions = buildStrategyInstructions(options.testStrategy, options.testFramework);
18486
18546
  const prompt = `You are a test engineer. Generate acceptance tests for the "${options.featureName}" feature based on the refined acceptance criteria below.
18487
18547
 
18488
18548
  CODEBASE CONTEXT:
@@ -18491,7 +18551,7 @@ ${options.codebaseContext}
18491
18551
  ACCEPTANCE CRITERIA (refined):
18492
18552
  ${criteriaList}
18493
18553
 
18494
- Generate a complete acceptance.test.ts file using bun:test framework. Each AC maps to exactly one test named "AC-N: <description>".
18554
+ ${strategyInstructions}Generate a complete acceptance.test.ts file using bun:test framework. Each AC maps to exactly one test named "AC-N: <description>".
18495
18555
 
18496
18556
  Use this structure:
18497
18557
 
@@ -18518,6 +18578,40 @@ Respond with ONLY the TypeScript test code (no markdown code fences, no explanat
18518
18578
  await _generatorPRDDeps.writeFile(join2(options.workdir, "acceptance-refined.json"), refinedJsonContent);
18519
18579
  return { testCode, criteria };
18520
18580
  }
18581
+ function buildStrategyInstructions(strategy, framework) {
18582
+ switch (strategy) {
18583
+ case "component": {
18584
+ const fw = framework ?? "ink-testing-library";
18585
+ if (fw === "react") {
18586
+ return `TEST STRATEGY: component (react)
18587
+ Import render and screen from @testing-library/react. Render the component and use screen.getByText to assert on output.
18588
+
18589
+ `;
18590
+ }
18591
+ return `TEST STRATEGY: component (ink-testing-library)
18592
+ Import render from ink-testing-library. Render the component and use lastFrame() to assert on output.
18593
+
18594
+ `;
18595
+ }
18596
+ case "cli":
18597
+ return `TEST STRATEGY: cli
18598
+ Use Bun.spawn to run the binary. Read stdout and assert on the text output.
18599
+
18600
+ `;
18601
+ case "e2e":
18602
+ return `TEST STRATEGY: e2e
18603
+ Use fetch() against http://localhost to call the running service. Assert on response body using response.text() or response.json().
18604
+
18605
+ `;
18606
+ case "snapshot":
18607
+ return `TEST STRATEGY: snapshot
18608
+ Render the component and use toMatchSnapshot() to capture and compare snapshots.
18609
+
18610
+ `;
18611
+ default:
18612
+ return "";
18613
+ }
18614
+ }
18521
18615
  function parseAcceptanceCriteria(specContent) {
18522
18616
  const criteria = [];
18523
18617
  const lines = specContent.split(`
@@ -18595,29 +18689,10 @@ async function generateAcceptanceTests(adapter, options) {
18595
18689
  logger.info("acceptance", "Found acceptance criteria", { count: criteria.length });
18596
18690
  const prompt = buildAcceptanceTestPrompt(criteria, options.featureName, options.codebaseContext);
18597
18691
  try {
18598
- const skipPerms = options.config.quality?.dangerouslySkipPermissions ?? true;
18599
- const permArgs = skipPerms ? ["--dangerously-skip-permissions"] : [];
18600
- const cmd = [adapter.binary, "--model", options.modelDef.model, ...permArgs, "-p", prompt];
18601
- const proc = Bun.spawn(cmd, {
18602
- cwd: options.workdir,
18603
- stdout: "pipe",
18604
- stderr: "pipe",
18605
- env: {
18606
- ...process.env,
18607
- ...options.modelDef.env || {}
18608
- }
18692
+ const output = await adapter.complete(prompt, {
18693
+ model: options.modelDef.model
18609
18694
  });
18610
- const exitCode = await proc.exited;
18611
- const stdout = await new Response(proc.stdout).text();
18612
- const stderr = await new Response(proc.stderr).text();
18613
- if (exitCode !== 0) {
18614
- logger.warn("acceptance", "\u26A0 Agent test generation failed", { stderr });
18615
- return {
18616
- testCode: generateSkeletonTests(options.featureName, criteria),
18617
- criteria
18618
- };
18619
- }
18620
- const testCode = extractTestCode(stdout);
18695
+ const testCode = extractTestCode(output);
18621
18696
  return {
18622
18697
  testCode,
18623
18698
  criteria
@@ -18720,7 +18795,7 @@ Requirements:
18720
18795
  Respond with ONLY the fix description (no JSON, no markdown, just the description text).`;
18721
18796
  }
18722
18797
  async function generateFixStories(adapter, options) {
18723
- const { failedACs, testOutput, prd, specContent, workdir, modelDef } = options;
18798
+ const { failedACs, testOutput, prd, specContent, modelDef } = options;
18724
18799
  const fixStories = [];
18725
18800
  const acTextMap = parseACTextFromSpec(specContent);
18726
18801
  const logger = getLogger();
@@ -18735,34 +18810,9 @@ async function generateFixStories(adapter, options) {
18735
18810
  }
18736
18811
  const prompt = buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd);
18737
18812
  try {
18738
- const skipPerms = options.config.quality?.dangerouslySkipPermissions ?? true;
18739
- const permArgs = skipPerms ? ["--dangerously-skip-permissions"] : [];
18740
- const cmd = [adapter.binary, "--model", modelDef.model, ...permArgs, "-p", prompt];
18741
- const proc = Bun.spawn(cmd, {
18742
- cwd: workdir,
18743
- stdout: "pipe",
18744
- stderr: "pipe",
18745
- env: {
18746
- ...process.env,
18747
- ...modelDef.env || {}
18748
- }
18813
+ const fixDescription = await adapter.complete(prompt, {
18814
+ model: modelDef.model
18749
18815
  });
18750
- const exitCode = await proc.exited;
18751
- const stdout = await new Response(proc.stdout).text();
18752
- const stderr = await new Response(proc.stderr).text();
18753
- if (exitCode !== 0) {
18754
- logger.warn("acceptance", "\u26A0 Agent fix generation failed", { failedAC, stderr });
18755
- fixStories.push({
18756
- id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
18757
- title: `Fix: ${failedAC}`,
18758
- failedAC,
18759
- testOutput,
18760
- relatedStories,
18761
- description: `Fix the implementation to make ${failedAC} pass. Related stories: ${relatedStories.join(", ")}.`
18762
- });
18763
- continue;
18764
- }
18765
- const fixDescription = stdout.trim();
18766
18816
  fixStories.push({
18767
18817
  id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
18768
18818
  title: `Fix: ${failedAC} \u2014 ${acText.slice(0, 50)}`,
@@ -18829,6 +18879,679 @@ var init_acceptance = __esm(() => {
18829
18879
  init_fix_generator();
18830
18880
  });
18831
18881
 
18882
+ // src/agents/acp/parser.ts
18883
+ function parseAcpxJsonOutput(rawOutput) {
18884
+ const lines = rawOutput.split(`
18885
+ `).filter((l) => l.trim());
18886
+ let text = "";
18887
+ let tokenUsage;
18888
+ let stopReason;
18889
+ let error48;
18890
+ for (const line of lines) {
18891
+ try {
18892
+ const event = JSON.parse(line);
18893
+ if (event.content && typeof event.content === "string")
18894
+ text += event.content;
18895
+ if (event.text && typeof event.text === "string")
18896
+ text += event.text;
18897
+ if (event.result && typeof event.result === "string")
18898
+ text = event.result;
18899
+ if (event.cumulative_token_usage)
18900
+ tokenUsage = event.cumulative_token_usage;
18901
+ if (event.usage) {
18902
+ tokenUsage = {
18903
+ input_tokens: event.usage.input_tokens ?? event.usage.prompt_tokens ?? 0,
18904
+ output_tokens: event.usage.output_tokens ?? event.usage.completion_tokens ?? 0
18905
+ };
18906
+ }
18907
+ if (event.stopReason)
18908
+ stopReason = event.stopReason;
18909
+ if (event.stop_reason)
18910
+ stopReason = event.stop_reason;
18911
+ if (event.error) {
18912
+ error48 = typeof event.error === "string" ? event.error : event.error.message ?? JSON.stringify(event.error);
18913
+ }
18914
+ } catch {
18915
+ if (!text)
18916
+ text = line;
18917
+ }
18918
+ }
18919
+ return { text: text.trim(), tokenUsage, stopReason, error: error48 };
18920
+ }
18921
+
18922
+ // src/agents/acp/spawn-client.ts
18923
+ function buildAllowedEnv2(extraEnv) {
18924
+ const allowed = {};
18925
+ const essentialVars = ["PATH", "HOME", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
18926
+ for (const varName of essentialVars) {
18927
+ if (process.env[varName])
18928
+ allowed[varName] = process.env[varName];
18929
+ }
18930
+ const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "CLAUDE_API_KEY"];
18931
+ for (const varName of apiKeyVars) {
18932
+ if (process.env[varName])
18933
+ allowed[varName] = process.env[varName];
18934
+ }
18935
+ const allowedPrefixes = ["CLAUDE_", "NAX_", "CLAW_", "TURBO_", "ACPX_", "CODEX_", "GEMINI_"];
18936
+ for (const [key, value] of Object.entries(process.env)) {
18937
+ if (allowedPrefixes.some((prefix) => key.startsWith(prefix))) {
18938
+ allowed[key] = value;
18939
+ }
18940
+ }
18941
+ if (extraEnv)
18942
+ Object.assign(allowed, extraEnv);
18943
+ return allowed;
18944
+ }
18945
+
18946
+ class SpawnAcpSession {
18947
+ agentName;
18948
+ sessionName;
18949
+ cwd;
18950
+ model;
18951
+ timeoutSeconds;
18952
+ permissionMode;
18953
+ env;
18954
+ constructor(opts) {
18955
+ this.agentName = opts.agentName;
18956
+ this.sessionName = opts.sessionName;
18957
+ this.cwd = opts.cwd;
18958
+ this.model = opts.model;
18959
+ this.timeoutSeconds = opts.timeoutSeconds;
18960
+ this.permissionMode = opts.permissionMode;
18961
+ this.env = opts.env;
18962
+ }
18963
+ async prompt(text) {
18964
+ const cmd = [
18965
+ "acpx",
18966
+ "--cwd",
18967
+ this.cwd,
18968
+ ...this.permissionMode === "approve-all" ? ["--approve-all"] : [],
18969
+ "--format",
18970
+ "json",
18971
+ "--model",
18972
+ this.model,
18973
+ "--timeout",
18974
+ String(this.timeoutSeconds),
18975
+ this.agentName,
18976
+ "prompt",
18977
+ "-s",
18978
+ this.sessionName,
18979
+ "--file",
18980
+ "-"
18981
+ ];
18982
+ getSafeLogger()?.debug("acp-adapter", `Sending prompt to session: ${this.sessionName}`);
18983
+ const proc = _spawnClientDeps.spawn(cmd, {
18984
+ cwd: this.cwd,
18985
+ stdin: "pipe",
18986
+ stdout: "pipe",
18987
+ stderr: "pipe",
18988
+ env: this.env
18989
+ });
18990
+ proc.stdin.write(text);
18991
+ proc.stdin.end();
18992
+ const exitCode = await proc.exited;
18993
+ const stdout = await new Response(proc.stdout).text();
18994
+ const stderr = await new Response(proc.stderr).text();
18995
+ if (exitCode !== 0) {
18996
+ getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
18997
+ stderr: stderr.slice(0, 200)
18998
+ });
18999
+ return {
19000
+ messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
19001
+ stopReason: "error"
19002
+ };
19003
+ }
19004
+ try {
19005
+ const parsed = parseAcpxJsonOutput(stdout);
19006
+ return {
19007
+ messages: [{ role: "assistant", content: parsed.text || "" }],
19008
+ stopReason: "end_turn",
19009
+ cumulative_token_usage: parsed.tokenUsage
19010
+ };
19011
+ } catch (err) {
19012
+ getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
19013
+ stderr: stderr.slice(0, 200)
19014
+ });
19015
+ throw err;
19016
+ }
19017
+ }
19018
+ async close() {
19019
+ const cmd = ["acpx", this.agentName, "sessions", "close", this.sessionName];
19020
+ getSafeLogger()?.debug("acp-adapter", `Closing session: ${this.sessionName}`);
19021
+ const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
19022
+ const exitCode = await proc.exited;
19023
+ if (exitCode !== 0) {
19024
+ const stderr = await new Response(proc.stderr).text();
19025
+ getSafeLogger()?.warn("acp-adapter", "Failed to close session", {
19026
+ sessionName: this.sessionName,
19027
+ stderr: stderr.slice(0, 200)
19028
+ });
19029
+ }
19030
+ }
19031
+ async cancelActivePrompt() {
19032
+ const cmd = ["acpx", this.agentName, "cancel"];
19033
+ getSafeLogger()?.debug("acp-adapter", `Cancelling active prompt: ${this.sessionName}`);
19034
+ const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
19035
+ await proc.exited;
19036
+ }
19037
+ }
19038
+
19039
+ class SpawnAcpClient {
19040
+ agentName;
19041
+ model;
19042
+ cwd;
19043
+ timeoutSeconds;
19044
+ env;
19045
+ constructor(cmdStr, cwd, timeoutSeconds) {
19046
+ const parts = cmdStr.split(/\s+/);
19047
+ const modelIdx = parts.indexOf("--model");
19048
+ this.model = modelIdx >= 0 && parts[modelIdx + 1] ? parts[modelIdx + 1] : "default";
19049
+ this.agentName = parts[parts.length - 1] || "claude";
19050
+ this.cwd = cwd || process.cwd();
19051
+ this.timeoutSeconds = timeoutSeconds || 1800;
19052
+ this.env = buildAllowedEnv2();
19053
+ }
19054
+ async start() {}
19055
+ async createSession(opts) {
19056
+ const sessionName = opts.sessionName || `nax-${Date.now()}`;
19057
+ const cmd = ["acpx", opts.agentName, "sessions", "ensure", "--name", sessionName];
19058
+ getSafeLogger()?.debug("acp-adapter", `Ensuring session: ${sessionName}`);
19059
+ const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
19060
+ const exitCode = await proc.exited;
19061
+ if (exitCode !== 0) {
19062
+ const stderr = await new Response(proc.stderr).text();
19063
+ throw new Error(`[acp-adapter] Failed to create session: ${stderr || `exit code ${exitCode}`}`);
19064
+ }
19065
+ return new SpawnAcpSession({
19066
+ agentName: opts.agentName,
19067
+ sessionName,
19068
+ cwd: this.cwd,
19069
+ model: this.model,
19070
+ timeoutSeconds: this.timeoutSeconds,
19071
+ permissionMode: opts.permissionMode,
19072
+ env: this.env
19073
+ });
19074
+ }
19075
+ async loadSession(sessionName) {
19076
+ const cmd = ["acpx", this.agentName, "sessions", "ensure", "--name", sessionName];
19077
+ const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
19078
+ const exitCode = await proc.exited;
19079
+ if (exitCode !== 0) {
19080
+ return null;
19081
+ }
19082
+ return new SpawnAcpSession({
19083
+ agentName: this.agentName,
19084
+ sessionName,
19085
+ cwd: this.cwd,
19086
+ model: this.model,
19087
+ timeoutSeconds: this.timeoutSeconds,
19088
+ permissionMode: "approve-all",
19089
+ env: this.env
19090
+ });
19091
+ }
19092
+ async close() {}
19093
+ }
19094
+ function createSpawnAcpClient(cmdStr, cwd, timeoutSeconds) {
19095
+ return new SpawnAcpClient(cmdStr, cwd, timeoutSeconds);
19096
+ }
19097
+ var _spawnClientDeps;
19098
+ var init_spawn_client = __esm(() => {
19099
+ init_logger2();
19100
+ _spawnClientDeps = {
19101
+ spawn(cmd, opts) {
19102
+ return Bun.spawn(cmd, opts);
19103
+ }
19104
+ };
19105
+ });
19106
+
19107
+ // src/agents/acp/cost.ts
19108
+ function estimateCostFromTokenUsage(usage, model) {
19109
+ const pricing = MODEL_PRICING[model];
19110
+ if (!pricing) {
19111
+ const fallbackInputRate = 3 / 1e6;
19112
+ const fallbackOutputRate = 15 / 1e6;
19113
+ const inputCost2 = (usage.input_tokens ?? 0) * fallbackInputRate;
19114
+ const outputCost2 = (usage.output_tokens ?? 0) * fallbackOutputRate;
19115
+ const cacheReadCost2 = (usage.cache_read_input_tokens ?? 0) * (0.5 / 1e6);
19116
+ const cacheCreationCost2 = (usage.cache_creation_input_tokens ?? 0) * (2 / 1e6);
19117
+ return inputCost2 + outputCost2 + cacheReadCost2 + cacheCreationCost2;
19118
+ }
19119
+ const inputRate = pricing.input / 1e6;
19120
+ const outputRate = pricing.output / 1e6;
19121
+ const cacheReadRate = (pricing.cacheRead ?? pricing.input * 0.1) / 1e6;
19122
+ const cacheCreationRate = (pricing.cacheCreation ?? pricing.input * 0.33) / 1e6;
19123
+ const inputCost = (usage.input_tokens ?? 0) * inputRate;
19124
+ const outputCost = (usage.output_tokens ?? 0) * outputRate;
19125
+ const cacheReadCost = (usage.cache_read_input_tokens ?? 0) * cacheReadRate;
19126
+ const cacheCreationCost = (usage.cache_creation_input_tokens ?? 0) * cacheCreationRate;
19127
+ return inputCost + outputCost + cacheReadCost + cacheCreationCost;
19128
+ }
19129
+ var MODEL_PRICING;
19130
+ var init_cost2 = __esm(() => {
19131
+ MODEL_PRICING = {
19132
+ "claude-sonnet-4": { input: 3, output: 15 },
19133
+ "claude-sonnet-4-5": { input: 3, output: 15 },
19134
+ "claude-haiku": { input: 0.8, output: 4, cacheRead: 0.1, cacheCreation: 1 },
19135
+ "claude-haiku-4-5": { input: 0.8, output: 4, cacheRead: 0.1, cacheCreation: 1 },
19136
+ "claude-opus": { input: 15, output: 75 },
19137
+ "claude-opus-4": { input: 15, output: 75 },
19138
+ "gpt-4.1": { input: 10, output: 30 },
19139
+ "gpt-4": { input: 30, output: 60 },
19140
+ "gpt-3.5-turbo": { input: 0.5, output: 1.5 },
19141
+ "gemini-2.5-pro": { input: 0.075, output: 0.3 },
19142
+ "gemini-2-pro": { input: 0.075, output: 0.3 },
19143
+ codex: { input: 0.02, output: 0.06 },
19144
+ "code-davinci-002": { input: 0.02, output: 0.06 }
19145
+ };
19146
+ });
19147
+
19148
+ // src/agents/acp/adapter.ts
19149
+ import { createHash } from "crypto";
19150
+ import { join as join3 } from "path";
19151
+ function resolveRegistryEntry(agentName) {
19152
+ return AGENT_REGISTRY[agentName] ?? DEFAULT_ENTRY;
19153
+ }
19154
+ function isRateLimitError(err) {
19155
+ if (!(err instanceof Error))
19156
+ return false;
19157
+ const msg = err.message.toLowerCase();
19158
+ return msg.includes("rate limit") || msg.includes("rate_limit") || msg.includes("429");
19159
+ }
19160
+ function buildSessionName(workdir, featureName, storyId, sessionRole) {
19161
+ const hash2 = createHash("sha256").update(workdir).digest("hex").slice(0, 8);
19162
+ const sanitize = (s) => s.replace(/[^a-z0-9]+/gi, "-").toLowerCase().replace(/^-+|-+$/g, "");
19163
+ const parts = ["nax", hash2];
19164
+ if (featureName)
19165
+ parts.push(sanitize(featureName));
19166
+ if (storyId)
19167
+ parts.push(sanitize(storyId));
19168
+ if (sessionRole)
19169
+ parts.push(sanitize(sessionRole));
19170
+ return parts.join("-");
19171
+ }
19172
+ async function ensureAcpSession(client, sessionName, agentName, permissionMode) {
19173
+ if (client.loadSession) {
19174
+ try {
19175
+ const existing = await client.loadSession(sessionName);
19176
+ if (existing) {
19177
+ getSafeLogger()?.debug("acp-adapter", `Resumed existing session: ${sessionName}`);
19178
+ return existing;
19179
+ }
19180
+ } catch {}
19181
+ }
19182
+ getSafeLogger()?.debug("acp-adapter", `Creating new session: ${sessionName}`);
19183
+ return client.createSession({ agentName, permissionMode, sessionName });
19184
+ }
19185
+ async function runSessionPrompt(session, prompt, timeoutMs) {
19186
+ const promptPromise = session.prompt(prompt);
19187
+ const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve("timeout"), timeoutMs));
19188
+ const winner = await Promise.race([promptPromise, timeoutPromise]);
19189
+ if (winner === "timeout") {
19190
+ try {
19191
+ await session.cancelActivePrompt();
19192
+ } catch {
19193
+ await session.close().catch(() => {});
19194
+ }
19195
+ return { response: null, timedOut: true };
19196
+ }
19197
+ return { response: winner, timedOut: false };
19198
+ }
19199
+ async function closeAcpSession(session) {
19200
+ try {
19201
+ await session.close();
19202
+ } catch (err) {
19203
+ getSafeLogger()?.warn("acp-adapter", "Failed to close session", { error: String(err) });
19204
+ }
19205
+ }
19206
+ function acpSessionsPath(workdir, featureName) {
19207
+ return join3(workdir, "nax", "features", featureName, "acp-sessions.json");
19208
+ }
19209
+ async function saveAcpSession(workdir, featureName, storyId, sessionName) {
19210
+ try {
19211
+ const path = acpSessionsPath(workdir, featureName);
19212
+ let data = {};
19213
+ try {
19214
+ const existing = await Bun.file(path).text();
19215
+ data = JSON.parse(existing);
19216
+ } catch {}
19217
+ data[storyId] = sessionName;
19218
+ await Bun.write(path, JSON.stringify(data, null, 2));
19219
+ } catch (err) {
19220
+ getSafeLogger()?.warn("acp-adapter", "Failed to save session to sidecar", { error: String(err) });
19221
+ }
19222
+ }
19223
+ async function clearAcpSession(workdir, featureName, storyId) {
19224
+ try {
19225
+ const path = acpSessionsPath(workdir, featureName);
19226
+ let data = {};
19227
+ try {
19228
+ const existing = await Bun.file(path).text();
19229
+ data = JSON.parse(existing);
19230
+ } catch {
19231
+ return;
19232
+ }
19233
+ delete data[storyId];
19234
+ await Bun.write(path, JSON.stringify(data, null, 2));
19235
+ } catch (err) {
19236
+ getSafeLogger()?.warn("acp-adapter", "Failed to clear session from sidecar", { error: String(err) });
19237
+ }
19238
+ }
19239
+ async function readAcpSession(workdir, featureName, storyId) {
19240
+ try {
19241
+ const path = acpSessionsPath(workdir, featureName);
19242
+ const existing = await Bun.file(path).text();
19243
+ const data = JSON.parse(existing);
19244
+ return data[storyId] ?? null;
19245
+ } catch {
19246
+ return null;
19247
+ }
19248
+ }
19249
+ function extractOutput(response) {
19250
+ if (!response)
19251
+ return "";
19252
+ return response.messages.filter((m) => m.role === "assistant").map((m) => m.content).join(`
19253
+ `).trim();
19254
+ }
19255
+ function extractQuestion(output) {
19256
+ const text = output.trim();
19257
+ if (!text)
19258
+ return null;
19259
+ const sentences = text.split(/(?<=[.!?])\s+/);
19260
+ const questionSentences = sentences.filter((s) => s.trim().endsWith("?"));
19261
+ if (questionSentences.length > 0) {
19262
+ const q = questionSentences[questionSentences.length - 1].trim();
19263
+ if (q.length > 10)
19264
+ return q;
19265
+ }
19266
+ const lower = text.toLowerCase();
19267
+ const markers = [
19268
+ "please confirm",
19269
+ "please specify",
19270
+ "please provide",
19271
+ "which would you",
19272
+ "should i ",
19273
+ "do you want",
19274
+ "can you clarify"
19275
+ ];
19276
+ for (const marker of markers) {
19277
+ if (lower.includes(marker)) {
19278
+ return text.slice(-200).trim();
19279
+ }
19280
+ }
19281
+ return null;
19282
+ }
19283
+
19284
+ class AcpAgentAdapter {
19285
+ name;
19286
+ displayName;
19287
+ binary;
19288
+ capabilities;
19289
+ constructor(agentName) {
19290
+ const entry = resolveRegistryEntry(agentName);
19291
+ this.name = agentName;
19292
+ this.displayName = entry.displayName;
19293
+ this.binary = entry.binary;
19294
+ this.capabilities = {
19295
+ supportedTiers: entry.supportedTiers,
19296
+ maxContextTokens: entry.maxContextTokens,
19297
+ features: new Set(["tdd", "review", "refactor"])
19298
+ };
19299
+ }
19300
+ async isInstalled() {
19301
+ const path = _acpAdapterDeps.which(this.binary);
19302
+ return path !== null;
19303
+ }
19304
+ buildCommand(_options) {
19305
+ return ["acpx", this.name, "session"];
19306
+ }
19307
+ buildAllowedEnv(_options) {
19308
+ return {};
19309
+ }
19310
+ async run(options) {
19311
+ const startTime = Date.now();
19312
+ let lastError;
19313
+ getSafeLogger()?.debug("acp-adapter", `Starting run for ${this.name}`, {
19314
+ model: options.modelDef.model,
19315
+ workdir: options.workdir,
19316
+ featureName: options.featureName,
19317
+ storyId: options.storyId,
19318
+ sessionRole: options.sessionRole
19319
+ });
19320
+ for (let attempt = 0;attempt < MAX_RATE_LIMIT_RETRIES; attempt++) {
19321
+ try {
19322
+ const result = await this._runWithClient(options, startTime);
19323
+ if (!result.success) {
19324
+ getSafeLogger()?.warn("acp-adapter", `Run failed for ${this.name}`, { exitCode: result.exitCode });
19325
+ }
19326
+ return result;
19327
+ } catch (err) {
19328
+ const error48 = err instanceof Error ? err : new Error(String(err));
19329
+ lastError = error48;
19330
+ const shouldRetry = isRateLimitError(error48) && attempt < MAX_RATE_LIMIT_RETRIES - 1;
19331
+ if (!shouldRetry)
19332
+ break;
19333
+ const backoffMs = 2 ** (attempt + 1) * 1000;
19334
+ getSafeLogger()?.warn("acp-adapter", "Retrying after rate limit", {
19335
+ backoffSeconds: backoffMs / 1000,
19336
+ attempt: attempt + 1
19337
+ });
19338
+ await _acpAdapterDeps.sleep(backoffMs);
19339
+ }
19340
+ }
19341
+ const durationMs = Date.now() - startTime;
19342
+ return {
19343
+ success: false,
19344
+ exitCode: 1,
19345
+ output: lastError?.message ?? "Run failed",
19346
+ rateLimited: isRateLimitError(lastError),
19347
+ durationMs,
19348
+ estimatedCost: 0
19349
+ };
19350
+ }
19351
+ async _runWithClient(options, startTime) {
19352
+ const cmdStr = `acpx --model ${options.modelDef.model} ${this.name}`;
19353
+ const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds);
19354
+ await client.start();
19355
+ let sessionName = options.acpSessionName;
19356
+ if (!sessionName && options.featureName && options.storyId) {
19357
+ sessionName = await readAcpSession(options.workdir, options.featureName, options.storyId) ?? undefined;
19358
+ }
19359
+ sessionName ??= buildSessionName(options.workdir, options.featureName, options.storyId, options.sessionRole);
19360
+ const permissionMode = options.dangerouslySkipPermissions ? "approve-all" : "default";
19361
+ const session = await ensureAcpSession(client, sessionName, this.name, permissionMode);
19362
+ if (options.featureName && options.storyId) {
19363
+ await saveAcpSession(options.workdir, options.featureName, options.storyId, sessionName);
19364
+ }
19365
+ let lastResponse = null;
19366
+ let timedOut = false;
19367
+ const totalTokenUsage = { input_tokens: 0, output_tokens: 0 };
19368
+ try {
19369
+ let currentPrompt = options.prompt;
19370
+ let turnCount = 0;
19371
+ const MAX_TURNS = options.interactionBridge ? 10 : 1;
19372
+ while (turnCount < MAX_TURNS) {
19373
+ turnCount++;
19374
+ getSafeLogger()?.debug("acp-adapter", `Session turn ${turnCount}/${MAX_TURNS}`, { sessionName });
19375
+ const turnResult = await runSessionPrompt(session, currentPrompt, options.timeoutSeconds * 1000);
19376
+ if (turnResult.timedOut) {
19377
+ timedOut = true;
19378
+ break;
19379
+ }
19380
+ lastResponse = turnResult.response;
19381
+ if (!lastResponse)
19382
+ break;
19383
+ if (lastResponse.cumulative_token_usage) {
19384
+ totalTokenUsage.input_tokens += lastResponse.cumulative_token_usage.input_tokens ?? 0;
19385
+ totalTokenUsage.output_tokens += lastResponse.cumulative_token_usage.output_tokens ?? 0;
19386
+ }
19387
+ const outputText = extractOutput(lastResponse);
19388
+ const question = extractQuestion(outputText);
19389
+ if (!question || !options.interactionBridge)
19390
+ break;
19391
+ getSafeLogger()?.debug("acp-adapter", "Agent asked question, routing to interactionBridge", { question });
19392
+ try {
19393
+ const answer = await Promise.race([
19394
+ options.interactionBridge.onQuestionDetected(question),
19395
+ new Promise((_, reject) => setTimeout(() => reject(new Error("interaction timeout")), INTERACTION_TIMEOUT_MS))
19396
+ ]);
19397
+ currentPrompt = answer;
19398
+ } catch (err) {
19399
+ const msg = err instanceof Error ? err.message : String(err);
19400
+ getSafeLogger()?.warn("acp-adapter", `InteractionBridge failed: ${msg}`);
19401
+ break;
19402
+ }
19403
+ }
19404
+ if (turnCount >= MAX_TURNS) {
19405
+ getSafeLogger()?.warn("acp-adapter", "Reached max turns limit", { sessionName, maxTurns: MAX_TURNS });
19406
+ }
19407
+ } finally {
19408
+ await closeAcpSession(session);
19409
+ await client.close().catch(() => {});
19410
+ if (options.featureName && options.storyId) {
19411
+ await clearAcpSession(options.workdir, options.featureName, options.storyId);
19412
+ }
19413
+ }
19414
+ const durationMs = Date.now() - startTime;
19415
+ if (timedOut) {
19416
+ return {
19417
+ success: false,
19418
+ exitCode: 124,
19419
+ output: `Session timed out after ${options.timeoutSeconds}s`,
19420
+ rateLimited: false,
19421
+ durationMs,
19422
+ estimatedCost: 0
19423
+ };
19424
+ }
19425
+ const success2 = lastResponse?.stopReason === "end_turn";
19426
+ const output = extractOutput(lastResponse);
19427
+ const estimatedCost = totalTokenUsage.input_tokens > 0 || totalTokenUsage.output_tokens > 0 ? estimateCostFromTokenUsage(totalTokenUsage, options.modelDef.model) : 0;
19428
+ return {
19429
+ success: success2,
19430
+ exitCode: success2 ? 0 : 1,
19431
+ output: output.slice(-MAX_AGENT_OUTPUT_CHARS2),
19432
+ rateLimited: false,
19433
+ durationMs,
19434
+ estimatedCost
19435
+ };
19436
+ }
19437
+ async complete(prompt, _options) {
19438
+ const model = _options?.model ?? "default";
19439
+ const cmdStr = `acpx --model ${model} ${this.name}`;
19440
+ const client = _acpAdapterDeps.createClient(cmdStr);
19441
+ await client.start();
19442
+ const permissionMode = _options?.dangerouslySkipPermissions ? "approve-all" : "default";
19443
+ let session = null;
19444
+ try {
19445
+ session = await client.createSession({ agentName: this.name, permissionMode });
19446
+ const response = await session.prompt(prompt);
19447
+ if (response.stopReason === "error") {
19448
+ throw new CompleteError("complete() failed: stop reason is error");
19449
+ }
19450
+ const text = response.messages.filter((m) => m.role === "assistant").map((m) => m.content).join(`
19451
+ `).trim();
19452
+ if (!text) {
19453
+ throw new CompleteError("complete() returned empty output");
19454
+ }
19455
+ return text;
19456
+ } finally {
19457
+ if (session) {
19458
+ await session.close().catch(() => {});
19459
+ }
19460
+ await client.close().catch(() => {});
19461
+ }
19462
+ }
19463
+ async plan(options) {
19464
+ if (options.interactive) {
19465
+ throw new Error("[acp-adapter] plan() interactive mode is not yet supported via ACP");
19466
+ }
19467
+ const modelDef = options.modelDef ?? { provider: "anthropic", model: "default" };
19468
+ const timeoutSeconds = options.timeoutSeconds ?? options.config?.execution?.sessionTimeoutSeconds ?? 600;
19469
+ const result = await this.run({
19470
+ prompt: options.prompt,
19471
+ workdir: options.workdir,
19472
+ modelTier: options.modelTier ?? "balanced",
19473
+ modelDef,
19474
+ timeoutSeconds,
19475
+ dangerouslySkipPermissions: options.dangerouslySkipPermissions ?? false,
19476
+ interactionBridge: options.interactionBridge,
19477
+ featureName: options.featureName,
19478
+ storyId: options.storyId,
19479
+ sessionRole: options.sessionRole
19480
+ });
19481
+ if (!result.success) {
19482
+ throw new Error(`[acp-adapter] plan() failed: ${result.output}`);
19483
+ }
19484
+ const specContent = result.output.trim();
19485
+ if (!specContent) {
19486
+ throw new Error("[acp-adapter] plan() returned empty spec content");
19487
+ }
19488
+ return { specContent };
19489
+ }
19490
+ async decompose(options) {
19491
+ const model = options.modelDef?.model;
19492
+ const prompt = buildDecomposePrompt(options);
19493
+ let output;
19494
+ try {
19495
+ output = await this.complete(prompt, { model, jsonMode: true });
19496
+ } catch (err) {
19497
+ const msg = err instanceof Error ? err.message : String(err);
19498
+ throw new Error(`[acp-adapter] decompose() failed: ${msg}`, { cause: err });
19499
+ }
19500
+ let stories;
19501
+ try {
19502
+ stories = parseDecomposeOutput(output);
19503
+ } catch (err) {
19504
+ throw new Error(`[acp-adapter] decompose() failed to parse stories: ${err.message}`, { cause: err });
19505
+ }
19506
+ return { stories };
19507
+ }
19508
+ }
19509
+ var MAX_AGENT_OUTPUT_CHARS2 = 5000, MAX_RATE_LIMIT_RETRIES = 3, INTERACTION_TIMEOUT_MS, AGENT_REGISTRY, DEFAULT_ENTRY, _acpAdapterDeps;
19510
+ var init_adapter = __esm(() => {
19511
+ init_logger2();
19512
+ init_spawn_client();
19513
+ init_types2();
19514
+ init_cost2();
19515
+ INTERACTION_TIMEOUT_MS = 5 * 60 * 1000;
19516
+ AGENT_REGISTRY = {
19517
+ claude: {
19518
+ binary: "claude",
19519
+ displayName: "Claude Code (ACP)",
19520
+ supportedTiers: ["fast", "balanced", "powerful"],
19521
+ maxContextTokens: 200000
19522
+ },
19523
+ codex: {
19524
+ binary: "codex",
19525
+ displayName: "OpenAI Codex (ACP)",
19526
+ supportedTiers: ["fast", "balanced"],
19527
+ maxContextTokens: 128000
19528
+ },
19529
+ gemini: {
19530
+ binary: "gemini",
19531
+ displayName: "Gemini CLI (ACP)",
19532
+ supportedTiers: ["fast", "balanced", "powerful"],
19533
+ maxContextTokens: 1e6
19534
+ }
19535
+ };
19536
+ DEFAULT_ENTRY = {
19537
+ binary: "claude",
19538
+ displayName: "ACP Agent",
19539
+ supportedTiers: ["balanced"],
19540
+ maxContextTokens: 128000
19541
+ };
19542
+ _acpAdapterDeps = {
19543
+ which(name) {
19544
+ return Bun.which(name);
19545
+ },
19546
+ async sleep(ms) {
19547
+ await Bun.sleep(ms);
19548
+ },
19549
+ createClient(cmdStr, cwd, timeoutSeconds) {
19550
+ return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds);
19551
+ }
19552
+ };
19553
+ });
19554
+
18832
19555
  // src/agents/adapters/aider.ts
18833
19556
  class AiderAdapter {
18834
19557
  name = "aider";
@@ -18859,7 +19582,7 @@ class AiderAdapter {
18859
19582
  return {
18860
19583
  success: exitCode === 0,
18861
19584
  exitCode,
18862
- output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS2),
19585
+ output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS3),
18863
19586
  rateLimited: false,
18864
19587
  durationMs,
18865
19588
  estimatedCost: 0,
@@ -18893,7 +19616,7 @@ class AiderAdapter {
18893
19616
  throw new Error("AiderAdapter.decompose() not implemented");
18894
19617
  }
18895
19618
  }
18896
- var _aiderCompleteDeps, MAX_AGENT_OUTPUT_CHARS2 = 5000;
19619
+ var _aiderCompleteDeps, MAX_AGENT_OUTPUT_CHARS3 = 5000;
18897
19620
  var init_aider = __esm(() => {
18898
19621
  init_types2();
18899
19622
  _aiderCompleteDeps = {
@@ -18937,7 +19660,7 @@ class CodexAdapter {
18937
19660
  return {
18938
19661
  success: exitCode === 0,
18939
19662
  exitCode,
18940
- output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS3),
19663
+ output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS4),
18941
19664
  rateLimited: false,
18942
19665
  durationMs,
18943
19666
  estimatedCost: 0,
@@ -18968,7 +19691,7 @@ class CodexAdapter {
18968
19691
  throw new Error("CodexAdapter.decompose() not implemented");
18969
19692
  }
18970
19693
  }
18971
- var _codexRunDeps, _codexCompleteDeps, MAX_AGENT_OUTPUT_CHARS3 = 5000;
19694
+ var _codexRunDeps, _codexCompleteDeps, MAX_AGENT_OUTPUT_CHARS4 = 5000;
18972
19695
  var init_codex = __esm(() => {
18973
19696
  init_types2();
18974
19697
  _codexRunDeps = {
@@ -19037,7 +19760,7 @@ class GeminiAdapter {
19037
19760
  return {
19038
19761
  success: exitCode === 0,
19039
19762
  exitCode,
19040
- output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS4),
19763
+ output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS5),
19041
19764
  rateLimited: false,
19042
19765
  durationMs,
19043
19766
  estimatedCost: 0,
@@ -19068,7 +19791,7 @@ class GeminiAdapter {
19068
19791
  throw new Error("GeminiAdapter.decompose() not implemented");
19069
19792
  }
19070
19793
  }
19071
- var _geminiRunDeps, _geminiCompleteDeps, MAX_AGENT_OUTPUT_CHARS4 = 5000;
19794
+ var _geminiRunDeps, _geminiCompleteDeps, MAX_AGENT_OUTPUT_CHARS5 = 5000;
19072
19795
  var init_gemini = __esm(() => {
19073
19796
  init_types2();
19074
19797
  _geminiRunDeps = {
@@ -19149,6 +19872,7 @@ __export(exports_registry, {
19149
19872
  getInstalledAgents: () => getInstalledAgents,
19150
19873
  getAllAgentNames: () => getAllAgentNames,
19151
19874
  getAgent: () => getAgent,
19875
+ createAgentRegistry: () => createAgentRegistry,
19152
19876
  checkAgentHealth: () => checkAgentHealth,
19153
19877
  ALL_AGENTS: () => ALL_AGENTS
19154
19878
  });
@@ -19172,8 +19896,57 @@ async function checkAgentHealth() {
19172
19896
  installed: await agent.isInstalled()
19173
19897
  })));
19174
19898
  }
19899
+ function createAgentRegistry(config2) {
19900
+ const protocol = config2.agent?.protocol ?? "cli";
19901
+ const logger = getLogger();
19902
+ const acpCache = new Map;
19903
+ logger?.info("agents", `Agent protocol: ${protocol}`, { protocol, hasConfig: !!config2.agent });
19904
+ function getAgent2(name) {
19905
+ if (protocol === "acp") {
19906
+ const known = ALL_AGENTS.find((a) => a.name === name);
19907
+ if (!known)
19908
+ return;
19909
+ if (!acpCache.has(name)) {
19910
+ acpCache.set(name, new AcpAgentAdapter(name));
19911
+ logger?.debug("agents", `Created AcpAgentAdapter for ${name}`, { name, protocol });
19912
+ }
19913
+ return acpCache.get(name);
19914
+ }
19915
+ const adapter = ALL_AGENTS.find((a) => a.name === name);
19916
+ if (adapter) {
19917
+ logger?.debug("agents", `Using CLI adapter for ${name}: ${adapter.constructor.name}`, { name });
19918
+ }
19919
+ return adapter;
19920
+ }
19921
+ async function getInstalledAgents2() {
19922
+ const agents = protocol === "acp" ? ALL_AGENTS.map((a) => {
19923
+ if (!acpCache.has(a.name)) {
19924
+ acpCache.set(a.name, new AcpAgentAdapter(a.name));
19925
+ }
19926
+ return acpCache.get(a.name);
19927
+ }) : ALL_AGENTS;
19928
+ const results = await Promise.all(agents.map(async (agent) => ({ agent, installed: await agent.isInstalled() })));
19929
+ return results.filter((r) => r.installed).map((r) => r.agent);
19930
+ }
19931
+ async function checkAgentHealth2() {
19932
+ const agents = protocol === "acp" ? ALL_AGENTS.map((a) => {
19933
+ if (!acpCache.has(a.name)) {
19934
+ acpCache.set(a.name, new AcpAgentAdapter(a.name));
19935
+ }
19936
+ return acpCache.get(a.name);
19937
+ }) : ALL_AGENTS;
19938
+ return Promise.all(agents.map(async (agent) => ({
19939
+ name: agent.name,
19940
+ displayName: agent.displayName,
19941
+ installed: await agent.isInstalled()
19942
+ })));
19943
+ }
19944
+ return { getAgent: getAgent2, getInstalledAgents: getInstalledAgents2, checkAgentHealth: checkAgentHealth2, protocol };
19945
+ }
19175
19946
  var ALL_AGENTS;
19176
19947
  var init_registry = __esm(() => {
19948
+ init_logger2();
19949
+ init_adapter();
19177
19950
  init_aider();
19178
19951
  init_codex();
19179
19952
  init_gemini();
@@ -19335,7 +20108,7 @@ var init_chain = __esm(() => {
19335
20108
  });
19336
20109
 
19337
20110
  // src/utils/path-security.ts
19338
- import { isAbsolute, join as join4, normalize, resolve } from "path";
20111
+ import { isAbsolute, join as join5, normalize, resolve } from "path";
19339
20112
  function validateModulePath(modulePath, allowedRoots) {
19340
20113
  if (!modulePath) {
19341
20114
  return { valid: false, error: "Module path is empty" };
@@ -19351,7 +20124,7 @@ function validateModulePath(modulePath, allowedRoots) {
19351
20124
  }
19352
20125
  } else {
19353
20126
  for (const root of normalizedRoots) {
19354
- const absoluteTarget = resolve(join4(root, modulePath));
20127
+ const absoluteTarget = resolve(join5(root, modulePath));
19355
20128
  if (absoluteTarget.startsWith(`${root}/`) || absoluteTarget === root) {
19356
20129
  return { valid: true, absolutePath: absoluteTarget };
19357
20130
  }
@@ -19701,27 +20474,27 @@ var init_path_security2 = () => {};
19701
20474
 
19702
20475
  // src/config/paths.ts
19703
20476
  import { homedir } from "os";
19704
- import { join as join5, resolve as resolve4 } from "path";
20477
+ import { join as join6, resolve as resolve4 } from "path";
19705
20478
  function globalConfigDir() {
19706
- return join5(homedir(), ".nax");
20479
+ return join6(homedir(), ".nax");
19707
20480
  }
19708
20481
  var init_paths = () => {};
19709
20482
 
19710
20483
  // src/config/loader.ts
19711
20484
  import { existsSync as existsSync5 } from "fs";
19712
- import { join as join6, resolve as resolve5 } from "path";
20485
+ import { join as join7, resolve as resolve5 } from "path";
19713
20486
  function globalConfigPath() {
19714
- return join6(globalConfigDir(), "config.json");
20487
+ return join7(globalConfigDir(), "config.json");
19715
20488
  }
19716
20489
  function findProjectDir(startDir = process.cwd()) {
19717
20490
  let dir = resolve5(startDir);
19718
20491
  let depth = 0;
19719
20492
  while (depth < MAX_DIRECTORY_DEPTH) {
19720
- const candidate = join6(dir, "nax");
19721
- if (existsSync5(join6(candidate, "config.json"))) {
20493
+ const candidate = join7(dir, "nax");
20494
+ if (existsSync5(join7(candidate, "config.json"))) {
19722
20495
  return candidate;
19723
20496
  }
19724
- const parent = join6(dir, "..");
20497
+ const parent = join7(dir, "..");
19725
20498
  if (parent === dir)
19726
20499
  break;
19727
20500
  dir = parent;
@@ -19759,7 +20532,7 @@ async function loadConfig(projectDir, cliOverrides) {
19759
20532
  }
19760
20533
  const projDir = projectDir ?? findProjectDir();
19761
20534
  if (projDir) {
19762
- const projConf = await loadJsonFile(join6(projDir, "config.json"), "config");
20535
+ const projConf = await loadJsonFile(join7(projDir, "config.json"), "config");
19763
20536
  if (projConf) {
19764
20537
  const resolvedProjConf = applyBatchModeCompat(projConf);
19765
20538
  rawConfig = deepMergeConfig(rawConfig, resolvedProjConf);
@@ -21034,7 +21807,7 @@ var package_default;
21034
21807
  var init_package = __esm(() => {
21035
21808
  package_default = {
21036
21809
  name: "@nathapp/nax",
21037
- version: "0.40.0",
21810
+ version: "0.41.0",
21038
21811
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
21039
21812
  type: "module",
21040
21813
  bin: {
@@ -21098,8 +21871,8 @@ var init_version = __esm(() => {
21098
21871
  NAX_VERSION = package_default.version;
21099
21872
  NAX_COMMIT = (() => {
21100
21873
  try {
21101
- if (/^[0-9a-f]{6,10}$/.test("5b23ab2"))
21102
- return "5b23ab2";
21874
+ if (/^[0-9a-f]{6,10}$/.test("0db1c5f"))
21875
+ return "0db1c5f";
21103
21876
  } catch {}
21104
21877
  try {
21105
21878
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -22846,7 +23619,9 @@ ${stderr}` };
22846
23619
  refinedCriteria = await _acceptanceSetupDeps.refine(allCriteria, {
22847
23620
  storyId: ctx.prd.userStories[0]?.id ?? "US-001",
22848
23621
  codebaseContext: "",
22849
- config: ctx.config
23622
+ config: ctx.config,
23623
+ testStrategy: ctx.config.acceptance.testStrategy,
23624
+ testFramework: ctx.config.acceptance.testFramework
22850
23625
  });
22851
23626
  } else {
22852
23627
  refinedCriteria = allCriteria.map((c) => ({
@@ -22863,7 +23638,9 @@ ${stderr}` };
22863
23638
  codebaseContext: "",
22864
23639
  modelTier: ctx.config.acceptance.model ?? "fast",
22865
23640
  modelDef: resolveModel(ctx.config.models[ctx.config.acceptance.model ?? "fast"]),
22866
- config: ctx.config
23641
+ config: ctx.config,
23642
+ testStrategy: ctx.config.acceptance.testStrategy,
23643
+ testFramework: ctx.config.acceptance.testFramework
22867
23644
  });
22868
23645
  await _acceptanceSetupDeps.writeFile(testPath, result.testCode);
22869
23646
  }
@@ -23375,10 +24152,10 @@ var init_autofix = __esm(() => {
23375
24152
 
23376
24153
  // src/execution/progress.ts
23377
24154
  import { mkdirSync as mkdirSync2 } from "fs";
23378
- import { join as join14 } from "path";
24155
+ import { join as join15 } from "path";
23379
24156
  async function appendProgress(featureDir, storyId, status, message) {
23380
24157
  mkdirSync2(featureDir, { recursive: true });
23381
- const progressPath = join14(featureDir, "progress.txt");
24158
+ const progressPath = join15(featureDir, "progress.txt");
23382
24159
  const timestamp = new Date().toISOString();
23383
24160
  const entry = `[${timestamp}] ${storyId} \u2014 ${status.toUpperCase()} \u2014 ${message}
23384
24161
  `;
@@ -23462,7 +24239,7 @@ function estimateTokens(text) {
23462
24239
 
23463
24240
  // src/constitution/loader.ts
23464
24241
  import { existsSync as existsSync13 } from "fs";
23465
- import { join as join15 } from "path";
24242
+ import { join as join16 } from "path";
23466
24243
  function truncateToTokens(text, maxTokens) {
23467
24244
  const maxChars = maxTokens * 3;
23468
24245
  if (text.length <= maxChars) {
@@ -23484,7 +24261,7 @@ async function loadConstitution(projectDir, config2) {
23484
24261
  }
23485
24262
  let combinedContent = "";
23486
24263
  if (!config2.skipGlobal) {
23487
- const globalPath = join15(globalConfigDir(), config2.path);
24264
+ const globalPath = join16(globalConfigDir(), config2.path);
23488
24265
  if (existsSync13(globalPath)) {
23489
24266
  const validatedPath = validateFilePath(globalPath, globalConfigDir());
23490
24267
  const globalFile = Bun.file(validatedPath);
@@ -23494,7 +24271,7 @@ async function loadConstitution(projectDir, config2) {
23494
24271
  }
23495
24272
  }
23496
24273
  }
23497
- const projectPath = join15(projectDir, config2.path);
24274
+ const projectPath = join16(projectDir, config2.path);
23498
24275
  if (existsSync13(projectPath)) {
23499
24276
  const validatedPath = validateFilePath(projectPath, projectDir);
23500
24277
  const projectFile = Bun.file(validatedPath);
@@ -24520,6 +25297,15 @@ ${pluginMarkdown}` : pluginMarkdown;
24520
25297
  function validateAgentForTier(agent, tier) {
24521
25298
  return agent.capabilities.supportedTiers.includes(tier);
24522
25299
  }
25300
+ function validateAgentFeature(agent, feature) {
25301
+ return agent.capabilities.features.has(feature);
25302
+ }
25303
+ function describeAgentCapabilities(agent) {
25304
+ const tiers = agent.capabilities.supportedTiers.join(",");
25305
+ const features = Array.from(agent.capabilities.features).join(",");
25306
+ const maxTokens = agent.capabilities.maxContextTokens;
25307
+ return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
25308
+ }
24523
25309
 
24524
25310
  // src/agents/version-detection.ts
24525
25311
  async function getAgentVersion(binaryName) {
@@ -24570,6 +25356,26 @@ var init_version_detection = __esm(() => {
24570
25356
  });
24571
25357
 
24572
25358
  // src/agents/index.ts
25359
+ var exports_agents = {};
25360
+ __export(exports_agents, {
25361
+ validateAgentForTier: () => validateAgentForTier,
25362
+ validateAgentFeature: () => validateAgentFeature,
25363
+ parseTokenUsage: () => parseTokenUsage,
25364
+ getInstalledAgents: () => getInstalledAgents,
25365
+ getAllAgentNames: () => getAllAgentNames,
25366
+ getAgentVersions: () => getAgentVersions,
25367
+ getAgentVersion: () => getAgentVersion,
25368
+ getAgent: () => getAgent,
25369
+ formatCostWithConfidence: () => formatCostWithConfidence,
25370
+ estimateCostFromOutput: () => estimateCostFromOutput,
25371
+ estimateCostByDuration: () => estimateCostByDuration,
25372
+ estimateCost: () => estimateCost,
25373
+ describeAgentCapabilities: () => describeAgentCapabilities,
25374
+ checkAgentHealth: () => checkAgentHealth,
25375
+ CompleteError: () => CompleteError,
25376
+ ClaudeCodeAdapter: () => ClaudeCodeAdapter,
25377
+ COST_RATES: () => COST_RATES
25378
+ });
24573
25379
  var init_agents = __esm(() => {
24574
25380
  init_types2();
24575
25381
  init_claude();
@@ -24647,14 +25453,14 @@ var init_isolation = __esm(() => {
24647
25453
 
24648
25454
  // src/context/greenfield.ts
24649
25455
  import { readdir } from "fs/promises";
24650
- import { join as join16 } from "path";
25456
+ import { join as join17 } from "path";
24651
25457
  async function scanForTestFiles(dir, testPattern, isRootCall = true) {
24652
25458
  const results = [];
24653
25459
  const ignoreDirs = new Set(["node_modules", "dist", "build", ".next", ".git"]);
24654
25460
  try {
24655
25461
  const entries = await readdir(dir, { withFileTypes: true });
24656
25462
  for (const entry of entries) {
24657
- const fullPath = join16(dir, entry.name);
25463
+ const fullPath = join17(dir, entry.name);
24658
25464
  if (entry.isDirectory()) {
24659
25465
  if (ignoreDirs.has(entry.name))
24660
25466
  continue;
@@ -25042,13 +25848,13 @@ function parseTestOutput(output, exitCode) {
25042
25848
 
25043
25849
  // src/verification/runners.ts
25044
25850
  import { existsSync as existsSync14 } from "fs";
25045
- import { join as join17 } from "path";
25851
+ import { join as join18 } from "path";
25046
25852
  async function verifyAssets(workingDirectory, expectedFiles) {
25047
25853
  if (!expectedFiles || expectedFiles.length === 0)
25048
25854
  return { success: true, missingFiles: [] };
25049
25855
  const missingFiles = [];
25050
25856
  for (const file2 of expectedFiles) {
25051
- if (!existsSync14(join17(workingDirectory, file2)))
25857
+ if (!existsSync14(join18(workingDirectory, file2)))
25052
25858
  missingFiles.push(file2);
25053
25859
  }
25054
25860
  if (missingFiles.length > 0) {
@@ -25260,7 +26066,7 @@ var init_prompts = __esm(() => {
25260
26066
  });
25261
26067
 
25262
26068
  // src/tdd/rectification-gate.ts
25263
- async function runFullSuiteGate(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger) {
26069
+ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, featureName) {
25264
26070
  const rectificationEnabled = config2.execution.rectification?.enabled ?? false;
25265
26071
  if (!rectificationEnabled)
25266
26072
  return false;
@@ -25276,7 +26082,7 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
25276
26082
  if (!fullSuitePassed && fullSuiteResult.output) {
25277
26083
  const testSummary = parseBunTestOutput(fullSuiteResult.output);
25278
26084
  if (testSummary.failed > 0) {
25279
- return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout);
26085
+ return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout, featureName);
25280
26086
  }
25281
26087
  if (testSummary.passed > 0) {
25282
26088
  logger.info("tdd", "Full suite gate passed (non-zero exit, 0 failures, tests detected)", {
@@ -25304,7 +26110,7 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
25304
26110
  });
25305
26111
  return false;
25306
26112
  }
25307
- async function runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout) {
26113
+ async function runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout, featureName) {
25308
26114
  const rectificationState = {
25309
26115
  attempt: 0,
25310
26116
  initialFailures: testSummary.failed,
@@ -25326,7 +26132,10 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
25326
26132
  modelTier: implementerTier,
25327
26133
  modelDef: resolveModel(config2.models[implementerTier]),
25328
26134
  timeoutSeconds: config2.execution.sessionTimeoutSeconds,
25329
- dangerouslySkipPermissions: config2.execution.dangerouslySkipPermissions
26135
+ dangerouslySkipPermissions: config2.execution.dangerouslySkipPermissions,
26136
+ featureName,
26137
+ storyId: story.id,
26138
+ sessionRole: "implementer"
25330
26139
  });
25331
26140
  if (!rectifyResult.success && rectifyResult.pid) {
25332
26141
  await cleanupProcessTree(rectifyResult.pid);
@@ -25723,13 +26532,13 @@ var exports_loader = {};
25723
26532
  __export(exports_loader, {
25724
26533
  loadOverride: () => loadOverride
25725
26534
  });
25726
- import { join as join18 } from "path";
26535
+ import { join as join19 } from "path";
25727
26536
  async function loadOverride(role, workdir, config2) {
25728
26537
  const overridePath = config2.prompts?.overrides?.[role];
25729
26538
  if (!overridePath) {
25730
26539
  return null;
25731
26540
  }
25732
- const absolutePath = join18(workdir, overridePath);
26541
+ const absolutePath = join19(workdir, overridePath);
25733
26542
  const file2 = Bun.file(absolutePath);
25734
26543
  if (!await file2.exists()) {
25735
26544
  return null;
@@ -25908,7 +26717,7 @@ async function rollbackToRef(workdir, ref) {
25908
26717
  }
25909
26718
  logger.info("tdd", "Successfully rolled back git changes", { ref });
25910
26719
  }
25911
- async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution) {
26720
+ async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution, featureName) {
25912
26721
  const startTime = Date.now();
25913
26722
  let prompt;
25914
26723
  switch (role) {
@@ -25930,7 +26739,10 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
25930
26739
  modelTier,
25931
26740
  modelDef: resolveModel(config2.models[modelTier]),
25932
26741
  timeoutSeconds: config2.execution.sessionTimeoutSeconds,
25933
- dangerouslySkipPermissions: config2.execution.dangerouslySkipPermissions
26742
+ dangerouslySkipPermissions: config2.execution.dangerouslySkipPermissions,
26743
+ featureName,
26744
+ storyId: story.id,
26745
+ sessionRole: role
25934
26746
  });
25935
26747
  if (!result.success && result.pid) {
25936
26748
  await cleanupProcessTree(result.pid);
@@ -26278,6 +27090,7 @@ async function runThreeSessionTdd(options) {
26278
27090
  config: config2,
26279
27091
  workdir,
26280
27092
  modelTier,
27093
+ featureName,
26281
27094
  contextMarkdown,
26282
27095
  constitution,
26283
27096
  dryRun = false,
@@ -26341,7 +27154,7 @@ async function runThreeSessionTdd(options) {
26341
27154
  let session1;
26342
27155
  if (!isRetry) {
26343
27156
  const testWriterTier = config2.tdd.sessionTiers?.testWriter ?? "balanced";
26344
- session1 = await runTddSession("test-writer", agent, story, config2, workdir, testWriterTier, session1Ref, contextMarkdown, lite, lite, constitution);
27157
+ session1 = await runTddSession("test-writer", agent, story, config2, workdir, testWriterTier, session1Ref, contextMarkdown, lite, lite, constitution, featureName);
26345
27158
  sessions.push(session1);
26346
27159
  }
26347
27160
  if (session1 && !session1.success) {
@@ -26403,7 +27216,7 @@ async function runThreeSessionTdd(options) {
26403
27216
  });
26404
27217
  const session2Ref = await captureGitRef(workdir) ?? "HEAD";
26405
27218
  const implementerTier = config2.tdd.sessionTiers?.implementer ?? modelTier;
26406
- const session2 = await runTddSession("implementer", agent, story, config2, workdir, implementerTier, session2Ref, contextMarkdown, lite, lite, constitution);
27219
+ const session2 = await runTddSession("implementer", agent, story, config2, workdir, implementerTier, session2Ref, contextMarkdown, lite, lite, constitution, featureName);
26407
27220
  sessions.push(session2);
26408
27221
  if (!session2.success) {
26409
27222
  needsHumanReview = true;
@@ -26419,10 +27232,10 @@ async function runThreeSessionTdd(options) {
26419
27232
  lite
26420
27233
  };
26421
27234
  }
26422
- const fullSuiteGatePassed = await runFullSuiteGate(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger);
27235
+ const fullSuiteGatePassed = await runFullSuiteGate(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, featureName);
26423
27236
  const session3Ref = await captureGitRef(workdir) ?? "HEAD";
26424
27237
  const verifierTier = config2.tdd.sessionTiers?.verifier ?? "fast";
26425
- const session3 = await runTddSession("verifier", agent, story, config2, workdir, verifierTier, session3Ref, undefined, false, false, constitution);
27238
+ const session3 = await runTddSession("verifier", agent, story, config2, workdir, verifierTier, session3Ref, undefined, false, false, constitution, featureName);
26426
27239
  sessions.push(session3);
26427
27240
  const verdict = await readVerdict(workdir);
26428
27241
  await cleanupVerdict(workdir);
@@ -26581,7 +27394,7 @@ var init_execution = __esm(() => {
26581
27394
  enabled: () => true,
26582
27395
  async execute(ctx) {
26583
27396
  const logger = getLogger();
26584
- const agent = _executionDeps.getAgent(ctx.config.autoMode.defaultAgent);
27397
+ const agent = (ctx.agentGetFn ?? _executionDeps.getAgent)(ctx.config.autoMode.defaultAgent);
26585
27398
  if (!agent) {
26586
27399
  return {
26587
27400
  action: "fail",
@@ -26601,6 +27414,7 @@ var init_execution = __esm(() => {
26601
27414
  config: ctx.config,
26602
27415
  workdir: ctx.workdir,
26603
27416
  modelTier: ctx.routing.modelTier,
27417
+ featureName: ctx.prd.feature,
26604
27418
  contextMarkdown: ctx.contextMarkdown,
26605
27419
  constitution: ctx.constitution?.content,
26606
27420
  dryRun: false,
@@ -26670,7 +27484,38 @@ Category: ${tddResult.failureCategory ?? "unknown"}`,
26670
27484
  modelTier: ctx.routing.modelTier,
26671
27485
  modelDef: resolveModel(ctx.config.models[ctx.routing.modelTier]),
26672
27486
  timeoutSeconds: ctx.config.execution.sessionTimeoutSeconds,
26673
- dangerouslySkipPermissions: ctx.config.execution.dangerouslySkipPermissions
27487
+ dangerouslySkipPermissions: ctx.config.execution.dangerouslySkipPermissions,
27488
+ pidRegistry: ctx.pidRegistry,
27489
+ featureName: ctx.prd.feature,
27490
+ storyId: ctx.story.id,
27491
+ interactionBridge: (() => {
27492
+ const plugin = ctx.interaction?.getPrimary();
27493
+ if (!plugin)
27494
+ return;
27495
+ const QUESTION_PATTERNS2 = [/\?/, /\bwhich\b/i, /\bshould i\b/i, /\bunclear\b/i, /\bplease clarify\b/i];
27496
+ return {
27497
+ detectQuestion: async (text) => QUESTION_PATTERNS2.some((p) => p.test(text)),
27498
+ onQuestionDetected: async (text) => {
27499
+ const requestId = `ix-acp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
27500
+ await plugin.send({
27501
+ id: requestId,
27502
+ type: "input",
27503
+ featureName: ctx.prd.feature,
27504
+ storyId: ctx.story.id,
27505
+ stage: "execution",
27506
+ summary: text,
27507
+ fallback: "continue",
27508
+ createdAt: Date.now()
27509
+ });
27510
+ try {
27511
+ const response = await plugin.receive(requestId, 120000);
27512
+ return response.value ?? "continue";
27513
+ } catch {
27514
+ return "continue";
27515
+ }
27516
+ }
27517
+ };
27518
+ })()
26674
27519
  });
26675
27520
  ctx.agentResult = result;
26676
27521
  await autoCommitIfDirty(ctx.workdir, "execution", "single-session", ctx.story.id);
@@ -27886,16 +28731,20 @@ var init_regression2 = __esm(() => {
27886
28731
  });
27887
28732
 
27888
28733
  // src/pipeline/stages/routing.ts
27889
- async function runDecompose(story, prd, config2, _workdir) {
28734
+ async function runDecompose(story, prd, config2, _workdir, agentGetFn) {
27890
28735
  const naxDecompose = config2.decompose;
27891
28736
  const builderConfig = {
27892
28737
  maxSubStories: naxDecompose?.maxSubstories ?? 5,
27893
28738
  maxComplexity: naxDecompose?.maxSubstoryComplexity ?? "medium",
27894
28739
  maxRetries: naxDecompose?.maxRetries ?? 2
27895
28740
  };
28741
+ const agent = (agentGetFn ?? getAgent)(config2.autoMode.defaultAgent);
28742
+ if (!agent) {
28743
+ throw new Error(`[decompose] Agent "${config2.autoMode.defaultAgent}" not found \u2014 cannot decompose`);
28744
+ }
27896
28745
  const adapter = {
27897
- async decompose(_prompt) {
27898
- throw new Error("[decompose] No LLM adapter configured for story decomposition");
28746
+ async decompose(prompt) {
28747
+ return agent.complete(prompt, { jsonMode: true });
27899
28748
  }
27900
28749
  };
27901
28750
  return DecomposeBuilder.for(story).prd(prd).config(builderConfig).decompose(adapter);
@@ -27916,7 +28765,7 @@ var init_routing2 = __esm(() => {
27916
28765
  async execute(ctx) {
27917
28766
  const logger = getLogger();
27918
28767
  const agentName = ctx.config.execution?.agent ?? "claude";
27919
- const adapter = _routingDeps.getAgent(agentName);
28768
+ const adapter = (ctx.agentGetFn ?? _routingDeps.getAgent)(agentName);
27920
28769
  const hasExistingRouting = ctx.story.routing !== undefined;
27921
28770
  const hasContentHash = ctx.story.routing?.contentHash !== undefined;
27922
28771
  let currentHash;
@@ -27985,7 +28834,7 @@ var init_routing2 = __esm(() => {
27985
28834
  if (decomposeConfig.trigger === "disabled") {
27986
28835
  logger.warn("routing", `Story ${ctx.story.id} is oversized (${acCount} ACs) but decompose is disabled \u2014 continuing with original`);
27987
28836
  } else if (decomposeConfig.trigger === "auto") {
27988
- const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir);
28837
+ const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir, ctx.agentGetFn);
27989
28838
  if (result.validation.valid) {
27990
28839
  _routingDeps.applyDecomposition(ctx.prd, result);
27991
28840
  if (ctx.prdPath) {
@@ -28000,7 +28849,7 @@ var init_routing2 = __esm(() => {
28000
28849
  } else if (decomposeConfig.trigger === "confirm") {
28001
28850
  const action = await _routingDeps.checkStoryOversized({ featureName: ctx.prd.feature, storyId: ctx.story.id, criteriaCount: acCount }, ctx.config, ctx.interaction);
28002
28851
  if (action === "decompose") {
28003
- const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir);
28852
+ const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir, ctx.agentGetFn);
28004
28853
  if (result.validation.valid) {
28005
28854
  _routingDeps.applyDecomposition(ctx.prd, result);
28006
28855
  if (ctx.prdPath) {
@@ -29488,19 +30337,19 @@ var init_precheck = __esm(() => {
29488
30337
  });
29489
30338
 
29490
30339
  // src/hooks/runner.ts
29491
- import { join as join36 } from "path";
30340
+ import { join as join37 } from "path";
29492
30341
  async function loadHooksConfig(projectDir, globalDir) {
29493
30342
  let globalHooks = { hooks: {} };
29494
30343
  let projectHooks = { hooks: {} };
29495
30344
  let skipGlobal = false;
29496
- const projectPath = join36(projectDir, "hooks.json");
30345
+ const projectPath = join37(projectDir, "hooks.json");
29497
30346
  const projectData = await loadJsonFile(projectPath, "hooks");
29498
30347
  if (projectData) {
29499
30348
  projectHooks = projectData;
29500
30349
  skipGlobal = projectData.skipGlobal ?? false;
29501
30350
  }
29502
30351
  if (!skipGlobal && globalDir) {
29503
- const globalPath = join36(globalDir, "hooks.json");
30352
+ const globalPath = join37(globalDir, "hooks.json");
29504
30353
  const globalData = await loadJsonFile(globalPath, "hooks");
29505
30354
  if (globalData) {
29506
30355
  globalHooks = globalData;
@@ -29941,7 +30790,8 @@ function buildResult(success2, prd, totalCost, iterations, storiesCompleted, prd
29941
30790
  }
29942
30791
  async function generateAndAddFixStories(ctx, failures, prd) {
29943
30792
  const logger = getSafeLogger();
29944
- const agent = getAgent(ctx.config.autoMode.defaultAgent);
30793
+ const { getAgent: getAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
30794
+ const agent = (ctx.agentGetFn ?? getAgent2)(ctx.config.autoMode.defaultAgent);
29945
30795
  if (!agent) {
29946
30796
  logger?.error("acceptance", "Agent not found, cannot generate fix stories");
29947
30797
  return null;
@@ -30087,7 +30937,6 @@ async function runAcceptanceLoop(ctx) {
30087
30937
  }
30088
30938
  var init_acceptance_loop = __esm(() => {
30089
30939
  init_acceptance();
30090
- init_agents();
30091
30940
  init_schema();
30092
30941
  init_hooks();
30093
30942
  init_logger2();
@@ -30537,12 +31386,12 @@ __export(exports_manager, {
30537
31386
  WorktreeManager: () => WorktreeManager
30538
31387
  });
30539
31388
  import { existsSync as existsSync30, symlinkSync } from "fs";
30540
- import { join as join37 } from "path";
31389
+ import { join as join38 } from "path";
30541
31390
 
30542
31391
  class WorktreeManager {
30543
31392
  async create(projectRoot, storyId) {
30544
31393
  validateStoryId(storyId);
30545
- const worktreePath = join37(projectRoot, ".nax-wt", storyId);
31394
+ const worktreePath = join38(projectRoot, ".nax-wt", storyId);
30546
31395
  const branchName = `nax/${storyId}`;
30547
31396
  try {
30548
31397
  const proc = Bun.spawn(["git", "worktree", "add", worktreePath, "-b", branchName], {
@@ -30567,9 +31416,9 @@ class WorktreeManager {
30567
31416
  }
30568
31417
  throw new Error(`Failed to create worktree: ${String(error48)}`);
30569
31418
  }
30570
- const nodeModulesSource = join37(projectRoot, "node_modules");
31419
+ const nodeModulesSource = join38(projectRoot, "node_modules");
30571
31420
  if (existsSync30(nodeModulesSource)) {
30572
- const nodeModulesTarget = join37(worktreePath, "node_modules");
31421
+ const nodeModulesTarget = join38(worktreePath, "node_modules");
30573
31422
  try {
30574
31423
  symlinkSync(nodeModulesSource, nodeModulesTarget, "dir");
30575
31424
  } catch (error48) {
@@ -30577,9 +31426,9 @@ class WorktreeManager {
30577
31426
  throw new Error(`Failed to symlink node_modules: ${errorMessage(error48)}`);
30578
31427
  }
30579
31428
  }
30580
- const envSource = join37(projectRoot, ".env");
31429
+ const envSource = join38(projectRoot, ".env");
30581
31430
  if (existsSync30(envSource)) {
30582
- const envTarget = join37(worktreePath, ".env");
31431
+ const envTarget = join38(worktreePath, ".env");
30583
31432
  try {
30584
31433
  symlinkSync(envSource, envTarget, "file");
30585
31434
  } catch (error48) {
@@ -30590,7 +31439,7 @@ class WorktreeManager {
30590
31439
  }
30591
31440
  async remove(projectRoot, storyId) {
30592
31441
  validateStoryId(storyId);
30593
- const worktreePath = join37(projectRoot, ".nax-wt", storyId);
31442
+ const worktreePath = join38(projectRoot, ".nax-wt", storyId);
30594
31443
  const branchName = `nax/${storyId}`;
30595
31444
  try {
30596
31445
  const proc = Bun.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
@@ -30980,7 +31829,7 @@ var init_parallel_worker = __esm(() => {
30980
31829
 
30981
31830
  // src/execution/parallel-coordinator.ts
30982
31831
  import os3 from "os";
30983
- import { join as join38 } from "path";
31832
+ import { join as join39 } from "path";
30984
31833
  function groupStoriesByDependencies(stories) {
30985
31834
  const batches = [];
30986
31835
  const processed = new Set;
@@ -31057,7 +31906,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
31057
31906
  };
31058
31907
  const worktreePaths = new Map;
31059
31908
  for (const story of batch) {
31060
- const worktreePath = join38(projectRoot, ".nax-wt", story.id);
31909
+ const worktreePath = join39(projectRoot, ".nax-wt", story.id);
31061
31910
  try {
31062
31911
  await worktreeManager.create(projectRoot, story.id);
31063
31912
  worktreePaths.set(story.id, worktreePath);
@@ -31106,7 +31955,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
31106
31955
  });
31107
31956
  logger?.warn("parallel", "Worktree preserved for manual conflict resolution", {
31108
31957
  storyId: mergeResult.storyId,
31109
- worktreePath: join38(projectRoot, ".nax-wt", mergeResult.storyId)
31958
+ worktreePath: join39(projectRoot, ".nax-wt", mergeResult.storyId)
31110
31959
  });
31111
31960
  }
31112
31961
  }
@@ -31565,12 +32414,12 @@ var init_parallel_executor = __esm(() => {
31565
32414
  // src/pipeline/subscribers/events-writer.ts
31566
32415
  import { appendFile as appendFile2, mkdir } from "fs/promises";
31567
32416
  import { homedir as homedir5 } from "os";
31568
- import { basename as basename3, join as join39 } from "path";
32417
+ import { basename as basename3, join as join40 } from "path";
31569
32418
  function wireEventsWriter(bus, feature, runId, workdir) {
31570
32419
  const logger = getSafeLogger();
31571
32420
  const project = basename3(workdir);
31572
- const eventsDir = join39(homedir5(), ".nax", "events", project);
31573
- const eventsFile = join39(eventsDir, "events.jsonl");
32421
+ const eventsDir = join40(homedir5(), ".nax", "events", project);
32422
+ const eventsFile = join40(eventsDir, "events.jsonl");
31574
32423
  let dirReady = false;
31575
32424
  const write = (line) => {
31576
32425
  (async () => {
@@ -31730,12 +32579,12 @@ var init_interaction2 = __esm(() => {
31730
32579
  // src/pipeline/subscribers/registry.ts
31731
32580
  import { mkdir as mkdir2, writeFile } from "fs/promises";
31732
32581
  import { homedir as homedir6 } from "os";
31733
- import { basename as basename4, join as join40 } from "path";
32582
+ import { basename as basename4, join as join41 } from "path";
31734
32583
  function wireRegistry(bus, feature, runId, workdir) {
31735
32584
  const logger = getSafeLogger();
31736
32585
  const project = basename4(workdir);
31737
- const runDir = join40(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
31738
- const metaFile = join40(runDir, "meta.json");
32586
+ const runDir = join41(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
32587
+ const metaFile = join41(runDir, "meta.json");
31739
32588
  const unsub = bus.on("run:started", (_ev) => {
31740
32589
  (async () => {
31741
32590
  try {
@@ -31745,8 +32594,8 @@ function wireRegistry(bus, feature, runId, workdir) {
31745
32594
  project,
31746
32595
  feature,
31747
32596
  workdir,
31748
- statusPath: join40(workdir, "nax", "features", feature, "status.json"),
31749
- eventsDir: join40(workdir, "nax", "features", feature, "runs"),
32597
+ statusPath: join41(workdir, "nax", "features", feature, "status.json"),
32598
+ eventsDir: join41(workdir, "nax", "features", feature, "runs"),
31750
32599
  registeredAt: new Date().toISOString()
31751
32600
  };
31752
32601
  await writeFile(metaFile, JSON.stringify(meta3, null, 2));
@@ -32414,6 +33263,8 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
32414
33263
  storyStartTime: new Date().toISOString(),
32415
33264
  storyGitRef: storyGitRef ?? undefined,
32416
33265
  interaction: ctx.interactionChain ?? undefined,
33266
+ agentGetFn: ctx.agentGetFn,
33267
+ pidRegistry: ctx.pidRegistry,
32417
33268
  accumulatedAttemptCost: accumulatedAttemptCost > 0 ? accumulatedAttemptCost : undefined
32418
33269
  };
32419
33270
  ctx.statusWriter.setPrd(prd);
@@ -32740,7 +33591,7 @@ async function writeStatusFile(filePath, status) {
32740
33591
  var init_status_file = () => {};
32741
33592
 
32742
33593
  // src/execution/status-writer.ts
32743
- import { join as join41 } from "path";
33594
+ import { join as join42 } from "path";
32744
33595
 
32745
33596
  class StatusWriter {
32746
33597
  statusFile;
@@ -32808,7 +33659,7 @@ class StatusWriter {
32808
33659
  if (!this._prd)
32809
33660
  return;
32810
33661
  const safeLogger = getSafeLogger();
32811
- const featureStatusPath = join41(featureDir, "status.json");
33662
+ const featureStatusPath = join42(featureDir, "status.json");
32812
33663
  try {
32813
33664
  const base = this.getSnapshot(totalCost, iterations);
32814
33665
  if (!base) {
@@ -33012,6 +33863,7 @@ var init_precheck_runner = __esm(() => {
33012
33863
  // src/execution/lifecycle/run-initialization.ts
33013
33864
  var exports_run_initialization = {};
33014
33865
  __export(exports_run_initialization, {
33866
+ logActiveProtocol: () => logActiveProtocol,
33015
33867
  initializeRun: () => initializeRun
33016
33868
  });
33017
33869
  async function reconcileState(prd, prdPath, workdir) {
@@ -33038,11 +33890,12 @@ async function reconcileState(prd, prdPath, workdir) {
33038
33890
  }
33039
33891
  return prd;
33040
33892
  }
33041
- async function checkAgentInstalled(config2, dryRun) {
33893
+ async function checkAgentInstalled(config2, dryRun, agentGetFn) {
33042
33894
  if (dryRun)
33043
33895
  return;
33044
33896
  const logger = getSafeLogger();
33045
- const agent = getAgent(config2.autoMode.defaultAgent);
33897
+ const { getAgent: getAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
33898
+ const agent = (agentGetFn ?? getAgent2)(config2.autoMode.defaultAgent);
33046
33899
  if (!agent) {
33047
33900
  logger?.error("execution", "Agent not found", {
33048
33901
  agent: config2.autoMode.defaultAgent
@@ -33070,9 +33923,14 @@ function validateStoryCount(counts, config2) {
33070
33923
  throw new StoryLimitExceededError(counts.total, config2.execution.maxStoriesPerFeature);
33071
33924
  }
33072
33925
  }
33926
+ function logActiveProtocol(config2) {
33927
+ const logger = getSafeLogger();
33928
+ const protocol = config2.agent?.protocol ?? "cli";
33929
+ logger?.info("run-initialization", `Agent protocol: ${protocol}`, { protocol });
33930
+ }
33073
33931
  async function initializeRun(ctx) {
33074
33932
  const logger = getSafeLogger();
33075
- await checkAgentInstalled(ctx.config, ctx.dryRun);
33933
+ await checkAgentInstalled(ctx.config, ctx.dryRun, ctx.agentGetFn);
33076
33934
  let prd = await loadPRD(ctx.prdPath);
33077
33935
  prd = await reconcileState(prd, ctx.prdPath, ctx.workdir);
33078
33936
  const counts = countStories(prd);
@@ -33085,7 +33943,6 @@ async function initializeRun(ctx) {
33085
33943
  return { prd, storyCounts: counts };
33086
33944
  }
33087
33945
  var init_run_initialization = __esm(() => {
33088
- init_agents();
33089
33946
  init_errors3();
33090
33947
  init_logger2();
33091
33948
  init_prd();
@@ -33194,7 +34051,8 @@ async function setupRun(options) {
33194
34051
  config: config2,
33195
34052
  prdPath,
33196
34053
  workdir,
33197
- dryRun
34054
+ dryRun,
34055
+ agentGetFn: options.agentGetFn
33198
34056
  });
33199
34057
  prd = initResult.prd;
33200
34058
  const counts = initResult.storyCounts;
@@ -64124,7 +64982,7 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
64124
64982
  init_source();
64125
64983
  import { existsSync as existsSync32, mkdirSync as mkdirSync6 } from "fs";
64126
64984
  import { homedir as homedir8 } from "os";
64127
- import { join as join42 } from "path";
64985
+ import { join as join43 } from "path";
64128
64986
 
64129
64987
  // node_modules/commander/esm.mjs
64130
64988
  var import__ = __toESM(require_commander(), 1);
@@ -64146,14 +65004,14 @@ var {
64146
65004
  init_acceptance();
64147
65005
  init_registry();
64148
65006
  import { existsSync as existsSync8 } from "fs";
64149
- import { join as join8 } from "path";
65007
+ import { join as join9 } from "path";
64150
65008
 
64151
65009
  // src/analyze/scanner.ts
64152
65010
  import { existsSync as existsSync2 } from "fs";
64153
- import { join as join3 } from "path";
65011
+ import { join as join4 } from "path";
64154
65012
  async function scanCodebase(workdir) {
64155
- const srcPath = join3(workdir, "src");
64156
- const packageJsonPath = join3(workdir, "package.json");
65013
+ const srcPath = join4(workdir, "src");
65014
+ const packageJsonPath = join4(workdir, "package.json");
64157
65015
  const fileTree = existsSync2(srcPath) ? await generateFileTree(srcPath, 3) : "No src/ directory";
64158
65016
  let dependencies = {};
64159
65017
  let devDependencies = {};
@@ -64193,7 +65051,7 @@ async function generateFileTree(dir, maxDepth, currentDepth = 0, prefix = "") {
64193
65051
  });
64194
65052
  for (let i = 0;i < dirEntries.length; i++) {
64195
65053
  const entry = dirEntries[i];
64196
- const fullPath = join3(dir, entry);
65054
+ const fullPath = join4(dir, entry);
64197
65055
  const isLast = i === dirEntries.length - 1;
64198
65056
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
64199
65057
  const childPrefix = isLast ? " " : "\u2502 ";
@@ -64225,16 +65083,16 @@ function detectTestPatterns(workdir, dependencies, devDependencies) {
64225
65083
  } else {
64226
65084
  patterns.push("Test framework: likely bun:test (no framework dependency)");
64227
65085
  }
64228
- if (existsSync2(join3(workdir, "test"))) {
65086
+ if (existsSync2(join4(workdir, "test"))) {
64229
65087
  patterns.push("Test directory: test/");
64230
65088
  }
64231
- if (existsSync2(join3(workdir, "__tests__"))) {
65089
+ if (existsSync2(join4(workdir, "__tests__"))) {
64232
65090
  patterns.push("Test directory: __tests__/");
64233
65091
  }
64234
- if (existsSync2(join3(workdir, "tests"))) {
65092
+ if (existsSync2(join4(workdir, "tests"))) {
64235
65093
  patterns.push("Test directory: tests/");
64236
65094
  }
64237
- const hasTestFiles = existsSync2(join3(workdir, "test")) || existsSync2(join3(workdir, "src"));
65095
+ const hasTestFiles = existsSync2(join4(workdir, "test")) || existsSync2(join4(workdir, "src"));
64238
65096
  if (hasTestFiles) {
64239
65097
  patterns.push("Test files: *.test.ts, *.spec.ts");
64240
65098
  }
@@ -64252,7 +65110,7 @@ init_version();
64252
65110
  // src/cli/analyze-parser.ts
64253
65111
  init_registry();
64254
65112
  import { existsSync as existsSync7 } from "fs";
64255
- import { join as join7 } from "path";
65113
+ import { join as join8 } from "path";
64256
65114
  init_schema();
64257
65115
  init_logger2();
64258
65116
  init_prd();
@@ -64382,7 +65240,7 @@ function estimateLOCFromComplexity(complexity) {
64382
65240
  }
64383
65241
  }
64384
65242
  async function reclassifyExistingPRD(featureDir, featureName, branchName, workdir, config2) {
64385
- const prdPath = join7(featureDir, "prd.json");
65243
+ const prdPath = join8(featureDir, "prd.json");
64386
65244
  if (!existsSync7(prdPath)) {
64387
65245
  throw new Error(`prd.json not found at ${prdPath}. Run analyze without --reclassify first.`);
64388
65246
  }
@@ -64483,11 +65341,11 @@ function reclassifyWithKeywords(story, config2) {
64483
65341
  // src/cli/analyze.ts
64484
65342
  async function analyzeFeature(options) {
64485
65343
  const { featureDir, featureName, branchName, config: config2, specPath: explicitSpecPath, reclassify = false } = options;
64486
- const workdir = join8(featureDir, "../..");
65344
+ const workdir = join9(featureDir, "../..");
64487
65345
  if (reclassify) {
64488
65346
  return await reclassifyExistingPRD(featureDir, featureName, branchName, workdir, config2);
64489
65347
  }
64490
- const specPath = explicitSpecPath || join8(featureDir, "spec.md");
65348
+ const specPath = explicitSpecPath || join9(featureDir, "spec.md");
64491
65349
  if (!existsSync8(specPath))
64492
65350
  throw new Error(`spec.md not found at ${specPath}`);
64493
65351
  const specContent = await Bun.file(specPath).text();
@@ -64604,7 +65462,7 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
64604
65462
  modelDef,
64605
65463
  config: config2
64606
65464
  });
64607
- const acceptanceTestPath = join8(featureDir, config2.acceptance.testPath);
65465
+ const acceptanceTestPath = join9(featureDir, config2.acceptance.testPath);
64608
65466
  await Bun.write(acceptanceTestPath, result.testCode);
64609
65467
  logger.info("cli", "[OK] Acceptance tests generated", {
64610
65468
  criteriaCount: result.criteria.length,
@@ -64617,9 +65475,25 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
64617
65475
  // src/cli/plan.ts
64618
65476
  init_claude();
64619
65477
  import { existsSync as existsSync9 } from "fs";
64620
- import { join as join9 } from "path";
65478
+ import { join as join10 } from "path";
65479
+ import { createInterface } from "readline";
64621
65480
  init_schema();
64622
65481
  init_logger2();
65482
+ var QUESTION_PATTERNS = [/\?[\s]*$/, /\bwhich\b/i, /\bshould i\b/i, /\bdo you want\b/i, /\bwould you like\b/i];
65483
+ async function detectQuestion(text) {
65484
+ return QUESTION_PATTERNS.some((p) => p.test(text.trim()));
65485
+ }
65486
+ async function askHuman(question) {
65487
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
65488
+ return new Promise((resolve6) => {
65489
+ rl.question(`
65490
+ [Agent asks]: ${question}
65491
+ Your answer: `, (answer) => {
65492
+ rl.close();
65493
+ resolve6(answer.trim());
65494
+ });
65495
+ });
65496
+ }
64623
65497
  var SPEC_TEMPLATE = `# Feature: [title]
64624
65498
 
64625
65499
  ## Problem
@@ -64640,8 +65514,8 @@ What this does NOT include.
64640
65514
  `;
64641
65515
  async function planCommand(prompt, workdir, config2, options = {}) {
64642
65516
  const interactive = options.interactive !== false;
64643
- const ngentDir = join9(workdir, "nax");
64644
- const outputPath = join9(ngentDir, config2.plan.outputPath);
65517
+ const ngentDir = join10(workdir, "nax");
65518
+ const outputPath = join10(ngentDir, config2.plan.outputPath);
64645
65519
  if (!existsSync9(ngentDir)) {
64646
65520
  throw new Error(`nax directory not found. Run 'nax init' first in ${workdir}`);
64647
65521
  }
@@ -64661,7 +65535,8 @@ async function planCommand(prompt, workdir, config2, options = {}) {
64661
65535
  inputFile: options.from,
64662
65536
  modelTier,
64663
65537
  modelDef,
64664
- config: config2
65538
+ config: config2,
65539
+ interactionBridge: interactive ? { detectQuestion, onQuestionDetected: askHuman } : undefined
64665
65540
  };
64666
65541
  const adapter = new ClaudeCodeAdapter;
64667
65542
  logger.info("cli", interactive ? "Starting interactive planning session..." : `Reading from ${options.from}...`, {
@@ -64879,13 +65754,13 @@ async function displayModelEfficiency(workdir) {
64879
65754
  // src/cli/status-features.ts
64880
65755
  init_source();
64881
65756
  import { existsSync as existsSync11, readdirSync as readdirSync2 } from "fs";
64882
- import { join as join12 } from "path";
65757
+ import { join as join13 } from "path";
64883
65758
 
64884
65759
  // src/commands/common.ts
64885
65760
  init_path_security2();
64886
65761
  init_errors3();
64887
65762
  import { existsSync as existsSync10, readdirSync, realpathSync as realpathSync2 } from "fs";
64888
- import { join as join10, resolve as resolve6 } from "path";
65763
+ import { join as join11, resolve as resolve6 } from "path";
64889
65764
  function resolveProject(options = {}) {
64890
65765
  const { dir, feature } = options;
64891
65766
  let projectRoot;
@@ -64893,12 +65768,12 @@ function resolveProject(options = {}) {
64893
65768
  let configPath;
64894
65769
  if (dir) {
64895
65770
  projectRoot = realpathSync2(resolve6(dir));
64896
- naxDir = join10(projectRoot, "nax");
65771
+ naxDir = join11(projectRoot, "nax");
64897
65772
  if (!existsSync10(naxDir)) {
64898
65773
  throw new NaxError(`Directory does not contain a nax project: ${projectRoot}
64899
65774
  Expected to find: ${naxDir}`, "NAX_DIR_NOT_FOUND", { projectRoot, naxDir });
64900
65775
  }
64901
- configPath = join10(naxDir, "config.json");
65776
+ configPath = join11(naxDir, "config.json");
64902
65777
  if (!existsSync10(configPath)) {
64903
65778
  throw new NaxError(`nax directory found but config.json is missing: ${naxDir}
64904
65779
  Expected to find: ${configPath}`, "CONFIG_NOT_FOUND", { naxDir, configPath });
@@ -64906,22 +65781,22 @@ Expected to find: ${configPath}`, "CONFIG_NOT_FOUND", { naxDir, configPath });
64906
65781
  } else {
64907
65782
  const found = findProjectRoot(process.cwd());
64908
65783
  if (!found) {
64909
- const cwdNaxDir = join10(process.cwd(), "nax");
65784
+ const cwdNaxDir = join11(process.cwd(), "nax");
64910
65785
  if (existsSync10(cwdNaxDir)) {
64911
- const cwdConfigPath = join10(cwdNaxDir, "config.json");
65786
+ const cwdConfigPath = join11(cwdNaxDir, "config.json");
64912
65787
  throw new NaxError(`nax directory found but config.json is missing: ${cwdNaxDir}
64913
65788
  Expected to find: ${cwdConfigPath}`, "CONFIG_NOT_FOUND", { naxDir: cwdNaxDir, configPath: cwdConfigPath });
64914
65789
  }
64915
65790
  throw new NaxError("No nax project found. Run this command from within a nax project directory, or use -d flag to specify the project path.", "PROJECT_NOT_FOUND", { cwd: process.cwd() });
64916
65791
  }
64917
65792
  projectRoot = found;
64918
- naxDir = join10(projectRoot, "nax");
64919
- configPath = join10(naxDir, "config.json");
65793
+ naxDir = join11(projectRoot, "nax");
65794
+ configPath = join11(naxDir, "config.json");
64920
65795
  }
64921
65796
  let featureDir;
64922
65797
  if (feature) {
64923
- const featuresDir = join10(naxDir, "features");
64924
- featureDir = join10(featuresDir, feature);
65798
+ const featuresDir = join11(naxDir, "features");
65799
+ featureDir = join11(featuresDir, feature);
64925
65800
  if (!existsSync10(featureDir)) {
64926
65801
  const availableFeatures = existsSync10(featuresDir) ? readdirSync(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
64927
65802
  const availableMsg = availableFeatures.length > 0 ? `
@@ -64948,12 +65823,12 @@ function findProjectRoot(startDir) {
64948
65823
  let current = resolve6(startDir);
64949
65824
  let depth = 0;
64950
65825
  while (depth < MAX_DIRECTORY_DEPTH) {
64951
- const naxDir = join10(current, "nax");
64952
- const configPath = join10(naxDir, "config.json");
65826
+ const naxDir = join11(current, "nax");
65827
+ const configPath = join11(naxDir, "config.json");
64953
65828
  if (existsSync10(configPath)) {
64954
65829
  return realpathSync2(current);
64955
65830
  }
64956
- const parent = join10(current, "..");
65831
+ const parent = join11(current, "..");
64957
65832
  if (parent === current) {
64958
65833
  break;
64959
65834
  }
@@ -64975,7 +65850,7 @@ function isPidAlive(pid) {
64975
65850
  }
64976
65851
  }
64977
65852
  async function loadStatusFile(featureDir) {
64978
- const statusPath = join12(featureDir, "status.json");
65853
+ const statusPath = join13(featureDir, "status.json");
64979
65854
  if (!existsSync11(statusPath)) {
64980
65855
  return null;
64981
65856
  }
@@ -64987,7 +65862,7 @@ async function loadStatusFile(featureDir) {
64987
65862
  }
64988
65863
  }
64989
65864
  async function loadProjectStatusFile(projectDir) {
64990
- const statusPath = join12(projectDir, "nax", "status.json");
65865
+ const statusPath = join13(projectDir, "nax", "status.json");
64991
65866
  if (!existsSync11(statusPath)) {
64992
65867
  return null;
64993
65868
  }
@@ -64999,7 +65874,7 @@ async function loadProjectStatusFile(projectDir) {
64999
65874
  }
65000
65875
  }
65001
65876
  async function getFeatureSummary(featureName, featureDir) {
65002
- const prdPath = join12(featureDir, "prd.json");
65877
+ const prdPath = join13(featureDir, "prd.json");
65003
65878
  const prd = await loadPRD(prdPath);
65004
65879
  const counts = countStories(prd);
65005
65880
  const summary = {
@@ -65033,7 +65908,7 @@ async function getFeatureSummary(featureName, featureDir) {
65033
65908
  };
65034
65909
  }
65035
65910
  }
65036
- const runsDir = join12(featureDir, "runs");
65911
+ const runsDir = join13(featureDir, "runs");
65037
65912
  if (existsSync11(runsDir)) {
65038
65913
  const runs = readdirSync2(runsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".jsonl") && e.name !== "latest.jsonl").map((e) => e.name).sort().reverse();
65039
65914
  if (runs.length > 0) {
@@ -65044,7 +65919,7 @@ async function getFeatureSummary(featureName, featureDir) {
65044
65919
  return summary;
65045
65920
  }
65046
65921
  async function displayAllFeatures(projectDir) {
65047
- const featuresDir = join12(projectDir, "nax", "features");
65922
+ const featuresDir = join13(projectDir, "nax", "features");
65048
65923
  if (!existsSync11(featuresDir)) {
65049
65924
  console.log(source_default.dim("No features found."));
65050
65925
  return;
@@ -65085,7 +65960,7 @@ async function displayAllFeatures(projectDir) {
65085
65960
  console.log();
65086
65961
  }
65087
65962
  }
65088
- const summaries = await Promise.all(features.map((name) => getFeatureSummary(name, join12(featuresDir, name))));
65963
+ const summaries = await Promise.all(features.map((name) => getFeatureSummary(name, join13(featuresDir, name))));
65089
65964
  console.log(source_default.bold(`\uD83D\uDCCA Features
65090
65965
  `));
65091
65966
  const header = ` ${"Feature".padEnd(25)} ${"Done".padEnd(6)} ${"Failed".padEnd(8)} ${"Pending".padEnd(9)} ${"Last Run".padEnd(22)} ${"Cost".padEnd(10)} Status`;
@@ -65111,7 +65986,7 @@ async function displayAllFeatures(projectDir) {
65111
65986
  console.log();
65112
65987
  }
65113
65988
  async function displayFeatureDetails(featureName, featureDir) {
65114
- const prdPath = join12(featureDir, "prd.json");
65989
+ const prdPath = join13(featureDir, "prd.json");
65115
65990
  const prd = await loadPRD(prdPath);
65116
65991
  const counts = countStories(prd);
65117
65992
  const status = await loadStatusFile(featureDir);
@@ -65226,7 +66101,7 @@ async function displayFeatureStatus(options = {}) {
65226
66101
  init_errors3();
65227
66102
  init_logger2();
65228
66103
  import { existsSync as existsSync12, readdirSync as readdirSync3 } from "fs";
65229
- import { join as join13 } from "path";
66104
+ import { join as join14 } from "path";
65230
66105
  async function parseRunLog(logPath) {
65231
66106
  const logger = getLogger();
65232
66107
  try {
@@ -65242,7 +66117,7 @@ async function parseRunLog(logPath) {
65242
66117
  async function runsListCommand(options) {
65243
66118
  const logger = getLogger();
65244
66119
  const { feature, workdir } = options;
65245
- const runsDir = join13(workdir, "nax", "features", feature, "runs");
66120
+ const runsDir = join14(workdir, "nax", "features", feature, "runs");
65246
66121
  if (!existsSync12(runsDir)) {
65247
66122
  logger.info("cli", "No runs found for feature", { feature, hint: `Directory not found: ${runsDir}` });
65248
66123
  return;
@@ -65254,7 +66129,7 @@ async function runsListCommand(options) {
65254
66129
  }
65255
66130
  logger.info("cli", `Runs for ${feature}`, { count: files.length });
65256
66131
  for (const file2 of files.sort().reverse()) {
65257
- const logPath = join13(runsDir, file2);
66132
+ const logPath = join14(runsDir, file2);
65258
66133
  const entries = await parseRunLog(logPath);
65259
66134
  const startEvent = entries.find((e) => e.message === "run.start");
65260
66135
  const completeEvent = entries.find((e) => e.message === "run.complete");
@@ -65280,7 +66155,7 @@ async function runsListCommand(options) {
65280
66155
  async function runsShowCommand(options) {
65281
66156
  const logger = getLogger();
65282
66157
  const { runId, feature, workdir } = options;
65283
- const logPath = join13(workdir, "nax", "features", feature, "runs", `${runId}.jsonl`);
66158
+ const logPath = join14(workdir, "nax", "features", feature, "runs", `${runId}.jsonl`);
65284
66159
  if (!existsSync12(logPath)) {
65285
66160
  logger.error("cli", "Run not found", { runId, feature, logPath });
65286
66161
  throw new NaxError("Run not found", "RUN_NOT_FOUND", { runId, feature, logPath });
@@ -65319,7 +66194,7 @@ async function runsShowCommand(options) {
65319
66194
  // src/cli/prompts-main.ts
65320
66195
  init_logger2();
65321
66196
  import { existsSync as existsSync15, mkdirSync as mkdirSync3 } from "fs";
65322
- import { join as join20 } from "path";
66197
+ import { join as join21 } from "path";
65323
66198
 
65324
66199
  // src/pipeline/index.ts
65325
66200
  init_runner();
@@ -65355,7 +66230,7 @@ init_prd();
65355
66230
 
65356
66231
  // src/cli/prompts-tdd.ts
65357
66232
  init_prompts2();
65358
- import { join as join19 } from "path";
66233
+ import { join as join20 } from "path";
65359
66234
  async function handleThreeSessionTddPrompts(story, ctx, outputDir, logger) {
65360
66235
  const [testWriterPrompt, implementerPrompt, verifierPrompt] = await Promise.all([
65361
66236
  PromptBuilder.for("test-writer", { isolation: "strict" }).withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(ctx.config.quality?.commands?.test).build(),
@@ -65374,7 +66249,7 @@ ${frontmatter}---
65374
66249
 
65375
66250
  ${session.prompt}`;
65376
66251
  if (outputDir) {
65377
- const promptFile = join19(outputDir, `${story.id}.${session.role}.md`);
66252
+ const promptFile = join20(outputDir, `${story.id}.${session.role}.md`);
65378
66253
  await Bun.write(promptFile, fullOutput);
65379
66254
  logger.info("cli", "Written TDD prompt file", {
65380
66255
  storyId: story.id,
@@ -65390,7 +66265,7 @@ ${"=".repeat(80)}`);
65390
66265
  }
65391
66266
  }
65392
66267
  if (outputDir && ctx.contextMarkdown) {
65393
- const contextFile = join19(outputDir, `${story.id}.context.md`);
66268
+ const contextFile = join20(outputDir, `${story.id}.context.md`);
65394
66269
  const frontmatter = buildFrontmatter(story, ctx);
65395
66270
  const contextOutput = `---
65396
66271
  ${frontmatter}---
@@ -65404,12 +66279,12 @@ ${ctx.contextMarkdown}`;
65404
66279
  async function promptsCommand(options) {
65405
66280
  const logger = getLogger();
65406
66281
  const { feature, workdir, config: config2, storyId, outputDir } = options;
65407
- const naxDir = join20(workdir, "nax");
66282
+ const naxDir = join21(workdir, "nax");
65408
66283
  if (!existsSync15(naxDir)) {
65409
66284
  throw new Error(`nax directory not found. Run 'nax init' first in ${workdir}`);
65410
66285
  }
65411
- const featureDir = join20(naxDir, "features", feature);
65412
- const prdPath = join20(featureDir, "prd.json");
66286
+ const featureDir = join21(naxDir, "features", feature);
66287
+ const prdPath = join21(featureDir, "prd.json");
65413
66288
  if (!existsSync15(prdPath)) {
65414
66289
  throw new Error(`Feature "${feature}" not found or missing prd.json`);
65415
66290
  }
@@ -65469,10 +66344,10 @@ ${frontmatter}---
65469
66344
 
65470
66345
  ${ctx.prompt}`;
65471
66346
  if (outputDir) {
65472
- const promptFile = join20(outputDir, `${story.id}.prompt.md`);
66347
+ const promptFile = join21(outputDir, `${story.id}.prompt.md`);
65473
66348
  await Bun.write(promptFile, fullOutput);
65474
66349
  if (ctx.contextMarkdown) {
65475
- const contextFile = join20(outputDir, `${story.id}.context.md`);
66350
+ const contextFile = join21(outputDir, `${story.id}.context.md`);
65476
66351
  const contextOutput = `---
65477
66352
  ${frontmatter}---
65478
66353
 
@@ -65536,7 +66411,7 @@ function buildFrontmatter(story, ctx, role) {
65536
66411
  }
65537
66412
  // src/cli/prompts-init.ts
65538
66413
  import { existsSync as existsSync16, mkdirSync as mkdirSync4 } from "fs";
65539
- import { join as join21 } from "path";
66414
+ import { join as join22 } from "path";
65540
66415
  var TEMPLATE_ROLES = [
65541
66416
  { file: "test-writer.md", role: "test-writer" },
65542
66417
  { file: "implementer.md", role: "implementer", variant: "standard" },
@@ -65560,9 +66435,9 @@ var TEMPLATE_HEADER = `<!--
65560
66435
  `;
65561
66436
  async function promptsInitCommand(options) {
65562
66437
  const { workdir, force = false, autoWireConfig = true } = options;
65563
- const templatesDir = join21(workdir, "nax", "templates");
66438
+ const templatesDir = join22(workdir, "nax", "templates");
65564
66439
  mkdirSync4(templatesDir, { recursive: true });
65565
- const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync16(join21(templatesDir, f)));
66440
+ const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync16(join22(templatesDir, f)));
65566
66441
  if (existingFiles.length > 0 && !force) {
65567
66442
  console.warn(`[WARN] nax/templates/ already contains files: ${existingFiles.join(", ")}. No files overwritten.
65568
66443
  Pass --force to overwrite existing templates.`);
@@ -65570,7 +66445,7 @@ async function promptsInitCommand(options) {
65570
66445
  }
65571
66446
  const written = [];
65572
66447
  for (const template of TEMPLATE_ROLES) {
65573
- const filePath = join21(templatesDir, template.file);
66448
+ const filePath = join22(templatesDir, template.file);
65574
66449
  const roleBody = template.role === "implementer" ? buildRoleTaskSection(template.role, template.variant) : buildRoleTaskSection(template.role);
65575
66450
  const content = TEMPLATE_HEADER + roleBody;
65576
66451
  await Bun.write(filePath, content);
@@ -65586,7 +66461,7 @@ async function promptsInitCommand(options) {
65586
66461
  return written;
65587
66462
  }
65588
66463
  async function autoWirePromptsConfig(workdir) {
65589
- const configPath = join21(workdir, "nax.config.json");
66464
+ const configPath = join22(workdir, "nax.config.json");
65590
66465
  if (!existsSync16(configPath)) {
65591
66466
  const exampleConfig = JSON.stringify({
65592
66467
  prompts: {
@@ -65753,7 +66628,7 @@ init_config();
65753
66628
  init_logger2();
65754
66629
  init_prd();
65755
66630
  import { existsSync as existsSync17, readdirSync as readdirSync4 } from "fs";
65756
- import { join as join24 } from "path";
66631
+ import { join as join25 } from "path";
65757
66632
 
65758
66633
  // src/cli/diagnose-analysis.ts
65759
66634
  function detectFailurePattern(story, prd, status) {
@@ -65952,7 +66827,7 @@ function isProcessAlive2(pid) {
65952
66827
  }
65953
66828
  }
65954
66829
  async function loadStatusFile2(workdir) {
65955
- const statusPath = join24(workdir, "nax", "status.json");
66830
+ const statusPath = join25(workdir, "nax", "status.json");
65956
66831
  if (!existsSync17(statusPath))
65957
66832
  return null;
65958
66833
  try {
@@ -65980,7 +66855,7 @@ async function countCommitsSince(workdir, since) {
65980
66855
  }
65981
66856
  }
65982
66857
  async function checkLock(workdir) {
65983
- const lockFile = Bun.file(join24(workdir, "nax.lock"));
66858
+ const lockFile = Bun.file(join25(workdir, "nax.lock"));
65984
66859
  if (!await lockFile.exists())
65985
66860
  return { lockPresent: false };
65986
66861
  try {
@@ -65998,8 +66873,8 @@ async function diagnoseCommand(options = {}) {
65998
66873
  const logger = getLogger();
65999
66874
  const workdir = options.workdir ?? process.cwd();
66000
66875
  const naxSubdir = findProjectDir(workdir);
66001
- let projectDir = naxSubdir ? join24(naxSubdir, "..") : null;
66002
- if (!projectDir && existsSync17(join24(workdir, "nax"))) {
66876
+ let projectDir = naxSubdir ? join25(naxSubdir, "..") : null;
66877
+ if (!projectDir && existsSync17(join25(workdir, "nax"))) {
66003
66878
  projectDir = workdir;
66004
66879
  }
66005
66880
  if (!projectDir)
@@ -66010,7 +66885,7 @@ async function diagnoseCommand(options = {}) {
66010
66885
  if (status2) {
66011
66886
  feature = status2.run.feature;
66012
66887
  } else {
66013
- const featuresDir = join24(projectDir, "nax", "features");
66888
+ const featuresDir = join25(projectDir, "nax", "features");
66014
66889
  if (!existsSync17(featuresDir))
66015
66890
  throw new Error("No features found in project");
66016
66891
  const features = readdirSync4(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
@@ -66020,8 +66895,8 @@ async function diagnoseCommand(options = {}) {
66020
66895
  logger.info("diagnose", "No feature specified, using first found", { feature });
66021
66896
  }
66022
66897
  }
66023
- const featureDir = join24(projectDir, "nax", "features", feature);
66024
- const prdPath = join24(featureDir, "prd.json");
66898
+ const featureDir = join25(projectDir, "nax", "features", feature);
66899
+ const prdPath = join25(featureDir, "prd.json");
66025
66900
  if (!existsSync17(prdPath))
66026
66901
  throw new Error(`Feature not found: ${feature}`);
66027
66902
  const prd = await loadPRD(prdPath);
@@ -66064,16 +66939,16 @@ init_interaction();
66064
66939
  init_source();
66065
66940
  init_loader2();
66066
66941
  import { existsSync as existsSync20 } from "fs";
66067
- import { join as join27 } from "path";
66942
+ import { join as join28 } from "path";
66068
66943
 
66069
66944
  // src/context/generator.ts
66070
66945
  init_path_security2();
66071
66946
  import { existsSync as existsSync19 } from "fs";
66072
- import { join as join26 } from "path";
66947
+ import { join as join27 } from "path";
66073
66948
 
66074
66949
  // src/context/injector.ts
66075
66950
  import { existsSync as existsSync18 } from "fs";
66076
- import { join as join25 } from "path";
66951
+ import { join as join26 } from "path";
66077
66952
  var NOTABLE_NODE_DEPS = [
66078
66953
  "@nestjs",
66079
66954
  "express",
@@ -66103,7 +66978,7 @@ var NOTABLE_NODE_DEPS = [
66103
66978
  "ioredis"
66104
66979
  ];
66105
66980
  async function detectNode(workdir) {
66106
- const pkgPath = join25(workdir, "package.json");
66981
+ const pkgPath = join26(workdir, "package.json");
66107
66982
  if (!existsSync18(pkgPath))
66108
66983
  return null;
66109
66984
  try {
@@ -66120,7 +66995,7 @@ async function detectNode(workdir) {
66120
66995
  }
66121
66996
  }
66122
66997
  async function detectGo(workdir) {
66123
- const goMod = join25(workdir, "go.mod");
66998
+ const goMod = join26(workdir, "go.mod");
66124
66999
  if (!existsSync18(goMod))
66125
67000
  return null;
66126
67001
  try {
@@ -66144,7 +67019,7 @@ async function detectGo(workdir) {
66144
67019
  }
66145
67020
  }
66146
67021
  async function detectRust(workdir) {
66147
- const cargoPath = join25(workdir, "Cargo.toml");
67022
+ const cargoPath = join26(workdir, "Cargo.toml");
66148
67023
  if (!existsSync18(cargoPath))
66149
67024
  return null;
66150
67025
  try {
@@ -66160,8 +67035,8 @@ async function detectRust(workdir) {
66160
67035
  }
66161
67036
  }
66162
67037
  async function detectPython(workdir) {
66163
- const pyproject = join25(workdir, "pyproject.toml");
66164
- const requirements = join25(workdir, "requirements.txt");
67038
+ const pyproject = join26(workdir, "pyproject.toml");
67039
+ const requirements = join26(workdir, "requirements.txt");
66165
67040
  if (!existsSync18(pyproject) && !existsSync18(requirements))
66166
67041
  return null;
66167
67042
  try {
@@ -66180,7 +67055,7 @@ async function detectPython(workdir) {
66180
67055
  }
66181
67056
  }
66182
67057
  async function detectPhp(workdir) {
66183
- const composerPath = join25(workdir, "composer.json");
67058
+ const composerPath = join26(workdir, "composer.json");
66184
67059
  if (!existsSync18(composerPath))
66185
67060
  return null;
66186
67061
  try {
@@ -66193,7 +67068,7 @@ async function detectPhp(workdir) {
66193
67068
  }
66194
67069
  }
66195
67070
  async function detectRuby(workdir) {
66196
- const gemfile = join25(workdir, "Gemfile");
67071
+ const gemfile = join26(workdir, "Gemfile");
66197
67072
  if (!existsSync18(gemfile))
66198
67073
  return null;
66199
67074
  try {
@@ -66205,9 +67080,9 @@ async function detectRuby(workdir) {
66205
67080
  }
66206
67081
  }
66207
67082
  async function detectJvm(workdir) {
66208
- const pom = join25(workdir, "pom.xml");
66209
- const gradle = join25(workdir, "build.gradle");
66210
- const gradleKts = join25(workdir, "build.gradle.kts");
67083
+ const pom = join26(workdir, "pom.xml");
67084
+ const gradle = join26(workdir, "build.gradle");
67085
+ const gradleKts = join26(workdir, "build.gradle.kts");
66211
67086
  if (!existsSync18(pom) && !existsSync18(gradle) && !existsSync18(gradleKts))
66212
67087
  return null;
66213
67088
  try {
@@ -66215,7 +67090,7 @@ async function detectJvm(workdir) {
66215
67090
  const content2 = await Bun.file(pom).text();
66216
67091
  const nameMatch = content2.match(/<artifactId>([^<]+)<\/artifactId>/);
66217
67092
  const deps2 = [...content2.matchAll(/<artifactId>([^<]+)<\/artifactId>/g)].map((m) => m[1]).filter((d) => d !== nameMatch?.[1]).slice(0, 10);
66218
- const lang2 = existsSync18(join25(workdir, "src/main/kotlin")) ? "Kotlin" : "Java";
67093
+ const lang2 = existsSync18(join26(workdir, "src/main/kotlin")) ? "Kotlin" : "Java";
66219
67094
  return { name: nameMatch?.[1], lang: lang2, dependencies: deps2 };
66220
67095
  }
66221
67096
  const gradleFile = existsSync18(gradleKts) ? gradleKts : gradle;
@@ -66436,7 +67311,7 @@ async function generateFor(agent, options, config2) {
66436
67311
  try {
66437
67312
  const context = await loadContextContent(options, config2);
66438
67313
  const content = generator.generate(context);
66439
- const outputPath = join26(options.outputDir, generator.outputFile);
67314
+ const outputPath = join27(options.outputDir, generator.outputFile);
66440
67315
  validateFilePath(outputPath, options.outputDir);
66441
67316
  if (!options.dryRun) {
66442
67317
  await Bun.write(outputPath, content);
@@ -66453,7 +67328,7 @@ async function generateAll(options, config2) {
66453
67328
  for (const [agentKey, generator] of Object.entries(GENERATORS)) {
66454
67329
  try {
66455
67330
  const content = generator.generate(context);
66456
- const outputPath = join26(options.outputDir, generator.outputFile);
67331
+ const outputPath = join27(options.outputDir, generator.outputFile);
66457
67332
  validateFilePath(outputPath, options.outputDir);
66458
67333
  if (!options.dryRun) {
66459
67334
  await Bun.write(outputPath, content);
@@ -66471,8 +67346,8 @@ async function generateAll(options, config2) {
66471
67346
  var VALID_AGENTS = ["claude", "codex", "opencode", "cursor", "windsurf", "aider", "gemini"];
66472
67347
  async function generateCommand(options) {
66473
67348
  const workdir = process.cwd();
66474
- const contextPath = options.context ? join27(workdir, options.context) : join27(workdir, "nax/context.md");
66475
- const outputDir = options.output ? join27(workdir, options.output) : workdir;
67349
+ const contextPath = options.context ? join28(workdir, options.context) : join28(workdir, "nax/context.md");
67350
+ const outputDir = options.output ? join28(workdir, options.output) : workdir;
66476
67351
  const autoInject = !options.noAutoInject;
66477
67352
  const dryRun = options.dryRun ?? false;
66478
67353
  if (!existsSync20(contextPath)) {
@@ -66548,7 +67423,7 @@ async function generateCommand(options) {
66548
67423
  // src/cli/config-display.ts
66549
67424
  init_loader2();
66550
67425
  import { existsSync as existsSync22 } from "fs";
66551
- import { join as join29 } from "path";
67426
+ import { join as join30 } from "path";
66552
67427
 
66553
67428
  // src/cli/config-descriptions.ts
66554
67429
  var FIELD_DESCRIPTIONS = {
@@ -66754,7 +67629,7 @@ function deepEqual(a, b) {
66754
67629
  init_defaults();
66755
67630
  init_loader2();
66756
67631
  import { existsSync as existsSync21 } from "fs";
66757
- import { join as join28 } from "path";
67632
+ import { join as join29 } from "path";
66758
67633
  async function loadConfigFile(path14) {
66759
67634
  if (!existsSync21(path14))
66760
67635
  return null;
@@ -66776,7 +67651,7 @@ async function loadProjectConfig() {
66776
67651
  const projectDir = findProjectDir();
66777
67652
  if (!projectDir)
66778
67653
  return null;
66779
- const projectPath = join28(projectDir, "config.json");
67654
+ const projectPath = join29(projectDir, "config.json");
66780
67655
  return await loadConfigFile(projectPath);
66781
67656
  }
66782
67657
 
@@ -66836,7 +67711,7 @@ async function configCommand(config2, options = {}) {
66836
67711
  function determineConfigSources() {
66837
67712
  const globalPath = globalConfigPath();
66838
67713
  const projectDir = findProjectDir();
66839
- const projectPath = projectDir ? join29(projectDir, "config.json") : null;
67714
+ const projectPath = projectDir ? join30(projectDir, "config.json") : null;
66840
67715
  return {
66841
67716
  global: fileExists(globalPath) ? globalPath : null,
66842
67717
  project: projectPath && fileExists(projectPath) ? projectPath : null
@@ -67016,21 +67891,21 @@ async function diagnose(options) {
67016
67891
 
67017
67892
  // src/commands/logs.ts
67018
67893
  import { existsSync as existsSync24 } from "fs";
67019
- import { join as join32 } from "path";
67894
+ import { join as join33 } from "path";
67020
67895
 
67021
67896
  // src/commands/logs-formatter.ts
67022
67897
  init_source();
67023
67898
  init_formatter();
67024
67899
  import { readdirSync as readdirSync6 } from "fs";
67025
- import { join as join31 } from "path";
67900
+ import { join as join32 } from "path";
67026
67901
 
67027
67902
  // src/commands/logs-reader.ts
67028
67903
  import { existsSync as existsSync23, readdirSync as readdirSync5 } from "fs";
67029
67904
  import { readdir as readdir3 } from "fs/promises";
67030
67905
  import { homedir as homedir3 } from "os";
67031
- import { join as join30 } from "path";
67906
+ import { join as join31 } from "path";
67032
67907
  var _deps5 = {
67033
- getRunsDir: () => process.env.NAX_RUNS_DIR ?? join30(homedir3(), ".nax", "runs")
67908
+ getRunsDir: () => process.env.NAX_RUNS_DIR ?? join31(homedir3(), ".nax", "runs")
67034
67909
  };
67035
67910
  async function resolveRunFileFromRegistry(runId) {
67036
67911
  const runsDir = _deps5.getRunsDir();
@@ -67042,7 +67917,7 @@ async function resolveRunFileFromRegistry(runId) {
67042
67917
  }
67043
67918
  let matched = null;
67044
67919
  for (const entry of entries) {
67045
- const metaPath = join30(runsDir, entry, "meta.json");
67920
+ const metaPath = join31(runsDir, entry, "meta.json");
67046
67921
  try {
67047
67922
  const meta3 = await Bun.file(metaPath).json();
67048
67923
  if (meta3.runId === runId || meta3.runId.startsWith(runId)) {
@@ -67064,14 +67939,14 @@ async function resolveRunFileFromRegistry(runId) {
67064
67939
  return null;
67065
67940
  }
67066
67941
  const specificFile = files.find((f) => f === `${matched.runId}.jsonl`);
67067
- return join30(matched.eventsDir, specificFile ?? files[0]);
67942
+ return join31(matched.eventsDir, specificFile ?? files[0]);
67068
67943
  }
67069
67944
  async function selectRunFile(runsDir) {
67070
67945
  const files = readdirSync5(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
67071
67946
  if (files.length === 0) {
67072
67947
  return null;
67073
67948
  }
67074
- return join30(runsDir, files[0]);
67949
+ return join31(runsDir, files[0]);
67075
67950
  }
67076
67951
  async function extractRunSummary(filePath) {
67077
67952
  const file2 = Bun.file(filePath);
@@ -67156,7 +68031,7 @@ Runs:
67156
68031
  console.log(source_default.gray(" Timestamp Stories Duration Cost Status"));
67157
68032
  console.log(source_default.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
67158
68033
  for (const file2 of files) {
67159
- const filePath = join31(runsDir, file2);
68034
+ const filePath = join32(runsDir, file2);
67160
68035
  const summary = await extractRunSummary(filePath);
67161
68036
  const timestamp = file2.replace(".jsonl", "");
67162
68037
  const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
@@ -67281,7 +68156,7 @@ async function logsCommand(options) {
67281
68156
  return;
67282
68157
  }
67283
68158
  const resolved = resolveProject({ dir: options.dir });
67284
- const naxDir = join32(resolved.projectDir, "nax");
68159
+ const naxDir = join33(resolved.projectDir, "nax");
67285
68160
  const configPath = resolved.configPath;
67286
68161
  const configFile = Bun.file(configPath);
67287
68162
  const config2 = await configFile.json();
@@ -67289,8 +68164,8 @@ async function logsCommand(options) {
67289
68164
  if (!featureName) {
67290
68165
  throw new Error("No feature specified in config.json");
67291
68166
  }
67292
- const featureDir = join32(naxDir, "features", featureName);
67293
- const runsDir = join32(featureDir, "runs");
68167
+ const featureDir = join33(naxDir, "features", featureName);
68168
+ const runsDir = join33(featureDir, "runs");
67294
68169
  if (!existsSync24(runsDir)) {
67295
68170
  throw new Error(`No runs directory found for feature: ${featureName}`);
67296
68171
  }
@@ -67315,7 +68190,7 @@ init_config();
67315
68190
  init_prd();
67316
68191
  init_precheck();
67317
68192
  import { existsSync as existsSync29 } from "fs";
67318
- import { join as join33 } from "path";
68193
+ import { join as join34 } from "path";
67319
68194
  async function precheckCommand(options) {
67320
68195
  const resolved = resolveProject({
67321
68196
  dir: options.dir,
@@ -67331,9 +68206,9 @@ async function precheckCommand(options) {
67331
68206
  process.exit(1);
67332
68207
  }
67333
68208
  }
67334
- const naxDir = join33(resolved.projectDir, "nax");
67335
- const featureDir = join33(naxDir, "features", featureName);
67336
- const prdPath = join33(featureDir, "prd.json");
68209
+ const naxDir = join34(resolved.projectDir, "nax");
68210
+ const featureDir = join34(naxDir, "features", featureName);
68211
+ const prdPath = join34(featureDir, "prd.json");
67337
68212
  if (!existsSync29(featureDir)) {
67338
68213
  console.error(source_default.red(`Feature not found: ${featureName}`));
67339
68214
  process.exit(1);
@@ -67357,10 +68232,10 @@ async function precheckCommand(options) {
67357
68232
  init_source();
67358
68233
  import { readdir as readdir4 } from "fs/promises";
67359
68234
  import { homedir as homedir4 } from "os";
67360
- import { join as join34 } from "path";
68235
+ import { join as join35 } from "path";
67361
68236
  var DEFAULT_LIMIT = 20;
67362
68237
  var _deps7 = {
67363
- getRunsDir: () => join34(homedir4(), ".nax", "runs")
68238
+ getRunsDir: () => join35(homedir4(), ".nax", "runs")
67364
68239
  };
67365
68240
  function formatDuration3(ms) {
67366
68241
  if (ms <= 0)
@@ -67412,7 +68287,7 @@ async function runsCommand(options = {}) {
67412
68287
  }
67413
68288
  const rows = [];
67414
68289
  for (const entry of entries) {
67415
- const metaPath = join34(runsDir, entry, "meta.json");
68290
+ const metaPath = join35(runsDir, entry, "meta.json");
67416
68291
  let meta3;
67417
68292
  try {
67418
68293
  meta3 = await Bun.file(metaPath).json();
@@ -67489,7 +68364,7 @@ async function runsCommand(options = {}) {
67489
68364
 
67490
68365
  // src/commands/unlock.ts
67491
68366
  init_source();
67492
- import { join as join35 } from "path";
68367
+ import { join as join36 } from "path";
67493
68368
  function isProcessAlive3(pid) {
67494
68369
  try {
67495
68370
  process.kill(pid, 0);
@@ -67504,7 +68379,7 @@ function formatLockAge(ageMs) {
67504
68379
  }
67505
68380
  async function unlockCommand(options) {
67506
68381
  const workdir = options.dir ?? process.cwd();
67507
- const lockPath = join35(workdir, "nax.lock");
68382
+ const lockPath = join36(workdir, "nax.lock");
67508
68383
  const lockFile = Bun.file(lockPath);
67509
68384
  const exists = await lockFile.exists();
67510
68385
  if (!exists) {
@@ -67543,6 +68418,7 @@ async function unlockCommand(options) {
67543
68418
  init_config();
67544
68419
 
67545
68420
  // src/execution/runner.ts
68421
+ init_registry();
67546
68422
  init_hooks();
67547
68423
  init_logger2();
67548
68424
  init_prd();
@@ -67552,6 +68428,7 @@ init_crash_recovery();
67552
68428
  init_hooks();
67553
68429
  init_logger2();
67554
68430
  init_prd();
68431
+ init_git();
67555
68432
  init_crash_recovery();
67556
68433
  init_story_context();
67557
68434
  async function runCompletionPhase(options) {
@@ -67561,7 +68438,7 @@ async function runCompletionPhase(options) {
67561
68438
  const acceptanceResult = await runAcceptanceLoop2({
67562
68439
  config: options.config,
67563
68440
  prd: options.prd,
67564
- prdPath: "",
68441
+ prdPath: options.prdPath,
67565
68442
  workdir: options.workdir,
67566
68443
  featureDir: options.featureDir,
67567
68444
  hooks: options.hooks,
@@ -67572,7 +68449,8 @@ async function runCompletionPhase(options) {
67572
68449
  allStoryMetrics: options.allStoryMetrics,
67573
68450
  pluginRegistry: options.pluginRegistry,
67574
68451
  eventEmitter: options.eventEmitter,
67575
- statusWriter: options.statusWriter
68452
+ statusWriter: options.statusWriter,
68453
+ agentGetFn: options.agentGetFn
67576
68454
  });
67577
68455
  Object.assign(options, {
67578
68456
  prd: acceptanceResult.prd,
@@ -67623,6 +68501,7 @@ async function runCompletionPhase(options) {
67623
68501
  }
67624
68502
  stopHeartbeat();
67625
68503
  await writeExitSummary(options.logFilePath, options.totalCost, options.iterations, options.storiesCompleted, durationMs);
68504
+ await autoCommitIfDirty(options.workdir, "run.complete", "run-summary", options.feature);
67626
68505
  return {
67627
68506
  durationMs,
67628
68507
  runCompletedAt
@@ -67773,7 +68652,9 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
67773
68652
  logFilePath: options.logFilePath,
67774
68653
  runId: options.runId,
67775
68654
  startTime: options.startTime,
67776
- batchPlan
68655
+ batchPlan,
68656
+ agentGetFn: options.agentGetFn,
68657
+ pidRegistry: options.pidRegistry
67777
68658
  }, prd);
67778
68659
  prd = sequentialResult.prd;
67779
68660
  iterations = sequentialResult.iterations;
@@ -67811,7 +68692,8 @@ async function runSetupPhase(options) {
67811
68692
  getTotalCost: options.getTotalCost,
67812
68693
  getIterations: options.getIterations,
67813
68694
  getStoriesCompleted: options.getStoriesCompleted,
67814
- getTotalStories: options.getTotalStories
68695
+ getTotalStories: options.getTotalStories,
68696
+ agentGetFn: options.agentGetFn
67815
68697
  });
67816
68698
  return setupResult;
67817
68699
  }
@@ -67849,6 +68731,8 @@ async function run(options) {
67849
68731
  let totalCost = 0;
67850
68732
  const allStoryMetrics = [];
67851
68733
  const logger = getSafeLogger();
68734
+ const registry2 = createAgentRegistry(config2);
68735
+ const agentGetFn = registry2.getAgent.bind(registry2);
67852
68736
  let prd;
67853
68737
  const setupResult = await runSetupPhase({
67854
68738
  prdPath,
@@ -67866,6 +68750,7 @@ async function run(options) {
67866
68750
  skipPrecheck,
67867
68751
  headless,
67868
68752
  formatterMode,
68753
+ agentGetFn,
67869
68754
  getTotalCost: () => totalCost,
67870
68755
  getIterations: () => iterations,
67871
68756
  getStoriesCompleted: () => storiesCompleted,
@@ -67893,7 +68778,9 @@ async function run(options) {
67893
68778
  formatterMode,
67894
68779
  headless,
67895
68780
  parallel,
67896
- runParallelExecution: _runnerDeps.runParallelExecution ?? undefined
68781
+ runParallelExecution: _runnerDeps.runParallelExecution ?? undefined,
68782
+ agentGetFn,
68783
+ pidRegistry
67897
68784
  }, prd, pluginRegistry);
67898
68785
  prd = executionResult.prd;
67899
68786
  iterations = executionResult.iterations;
@@ -67914,6 +68801,7 @@ async function run(options) {
67914
68801
  hooks,
67915
68802
  feature,
67916
68803
  workdir,
68804
+ prdPath,
67917
68805
  statusFile,
67918
68806
  logFilePath,
67919
68807
  runId,
@@ -67929,7 +68817,8 @@ async function run(options) {
67929
68817
  iterations,
67930
68818
  statusWriter,
67931
68819
  pluginRegistry,
67932
- eventEmitter
68820
+ eventEmitter,
68821
+ agentGetFn
67933
68822
  });
67934
68823
  const { durationMs } = completionResult;
67935
68824
  return {
@@ -75272,15 +76161,15 @@ program2.command("init").description("Initialize nax in the current project").op
75272
76161
  console.error(source_default.red(`Invalid directory: ${err.message}`));
75273
76162
  process.exit(1);
75274
76163
  }
75275
- const naxDir = join42(workdir, "nax");
76164
+ const naxDir = join43(workdir, "nax");
75276
76165
  if (existsSync32(naxDir) && !options.force) {
75277
76166
  console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
75278
76167
  return;
75279
76168
  }
75280
- mkdirSync6(join42(naxDir, "features"), { recursive: true });
75281
- mkdirSync6(join42(naxDir, "hooks"), { recursive: true });
75282
- await Bun.write(join42(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
75283
- await Bun.write(join42(naxDir, "hooks.json"), JSON.stringify({
76169
+ mkdirSync6(join43(naxDir, "features"), { recursive: true });
76170
+ mkdirSync6(join43(naxDir, "hooks"), { recursive: true });
76171
+ await Bun.write(join43(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
76172
+ await Bun.write(join43(naxDir, "hooks.json"), JSON.stringify({
75284
76173
  hooks: {
75285
76174
  "on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
75286
76175
  "on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
@@ -75288,12 +76177,12 @@ program2.command("init").description("Initialize nax in the current project").op
75288
76177
  "on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false }
75289
76178
  }
75290
76179
  }, null, 2));
75291
- await Bun.write(join42(naxDir, ".gitignore"), `# nax temp files
76180
+ await Bun.write(join43(naxDir, ".gitignore"), `# nax temp files
75292
76181
  *.tmp
75293
76182
  .paused.json
75294
76183
  .nax-verifier-verdict.json
75295
76184
  `);
75296
- await Bun.write(join42(naxDir, "context.md"), `# Project Context
76185
+ await Bun.write(join43(naxDir, "context.md"), `# Project Context
75297
76186
 
75298
76187
  This document defines coding standards, architectural decisions, and forbidden patterns for this project.
75299
76188
  Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
@@ -75411,16 +76300,16 @@ program2.command("run").description("Run the orchestration loop for a feature").
75411
76300
  console.error(source_default.red("nax not initialized. Run: nax init"));
75412
76301
  process.exit(1);
75413
76302
  }
75414
- const featureDir = join42(naxDir, "features", options.feature);
75415
- const prdPath = join42(featureDir, "prd.json");
76303
+ const featureDir = join43(naxDir, "features", options.feature);
76304
+ const prdPath = join43(featureDir, "prd.json");
75416
76305
  if (!existsSync32(prdPath)) {
75417
76306
  console.error(source_default.red(`Feature "${options.feature}" not found or missing prd.json`));
75418
76307
  process.exit(1);
75419
76308
  }
75420
- const runsDir = join42(featureDir, "runs");
76309
+ const runsDir = join43(featureDir, "runs");
75421
76310
  mkdirSync6(runsDir, { recursive: true });
75422
76311
  const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
75423
- const logFilePath = join42(runsDir, `${runId}.jsonl`);
76312
+ const logFilePath = join43(runsDir, `${runId}.jsonl`);
75424
76313
  const isTTY = process.stdout.isTTY ?? false;
75425
76314
  const headlessFlag = options.headless ?? false;
75426
76315
  const headlessEnv = process.env.NAX_HEADLESS === "1";
@@ -75436,7 +76325,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
75436
76325
  config2.autoMode.defaultAgent = options.agent;
75437
76326
  }
75438
76327
  config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
75439
- const globalNaxDir = join42(homedir8(), ".nax");
76328
+ const globalNaxDir = join43(homedir8(), ".nax");
75440
76329
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
75441
76330
  const eventEmitter = new PipelineEventEmitter;
75442
76331
  let tuiInstance;
@@ -75459,7 +76348,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
75459
76348
  } else {
75460
76349
  console.log(source_default.dim(" [Headless mode \u2014 pipe output]"));
75461
76350
  }
75462
- const statusFilePath = join42(workdir, "nax", "status.json");
76351
+ const statusFilePath = join43(workdir, "nax", "status.json");
75463
76352
  let parallel;
75464
76353
  if (options.parallel !== undefined) {
75465
76354
  parallel = Number.parseInt(options.parallel, 10);
@@ -75485,7 +76374,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
75485
76374
  headless: useHeadless,
75486
76375
  skipPrecheck: options.skipPrecheck ?? false
75487
76376
  });
75488
- const latestSymlink = join42(runsDir, "latest.jsonl");
76377
+ const latestSymlink = join43(runsDir, "latest.jsonl");
75489
76378
  try {
75490
76379
  if (existsSync32(latestSymlink)) {
75491
76380
  Bun.spawnSync(["rm", latestSymlink]);
@@ -75523,9 +76412,9 @@ features.command("create <name>").description("Create a new feature").option("-d
75523
76412
  console.error(source_default.red("nax not initialized. Run: nax init"));
75524
76413
  process.exit(1);
75525
76414
  }
75526
- const featureDir = join42(naxDir, "features", name);
76415
+ const featureDir = join43(naxDir, "features", name);
75527
76416
  mkdirSync6(featureDir, { recursive: true });
75528
- await Bun.write(join42(featureDir, "spec.md"), `# Feature: ${name}
76417
+ await Bun.write(join43(featureDir, "spec.md"), `# Feature: ${name}
75529
76418
 
75530
76419
  ## Overview
75531
76420
 
@@ -75533,7 +76422,7 @@ features.command("create <name>").description("Create a new feature").option("-d
75533
76422
 
75534
76423
  ## Acceptance Criteria
75535
76424
  `);
75536
- await Bun.write(join42(featureDir, "plan.md"), `# Plan: ${name}
76425
+ await Bun.write(join43(featureDir, "plan.md"), `# Plan: ${name}
75537
76426
 
75538
76427
  ## Architecture
75539
76428
 
@@ -75541,7 +76430,7 @@ features.command("create <name>").description("Create a new feature").option("-d
75541
76430
 
75542
76431
  ## Dependencies
75543
76432
  `);
75544
- await Bun.write(join42(featureDir, "tasks.md"), `# Tasks: ${name}
76433
+ await Bun.write(join43(featureDir, "tasks.md"), `# Tasks: ${name}
75545
76434
 
75546
76435
  ## US-001: [Title]
75547
76436
 
@@ -75550,7 +76439,7 @@ features.command("create <name>").description("Create a new feature").option("-d
75550
76439
  ### Acceptance Criteria
75551
76440
  - [ ] Criterion 1
75552
76441
  `);
75553
- await Bun.write(join42(featureDir, "progress.txt"), `# Progress: ${name}
76442
+ await Bun.write(join43(featureDir, "progress.txt"), `# Progress: ${name}
75554
76443
 
75555
76444
  Created: ${new Date().toISOString()}
75556
76445
 
@@ -75578,7 +76467,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
75578
76467
  console.error(source_default.red("nax not initialized."));
75579
76468
  process.exit(1);
75580
76469
  }
75581
- const featuresDir = join42(naxDir, "features");
76470
+ const featuresDir = join43(naxDir, "features");
75582
76471
  if (!existsSync32(featuresDir)) {
75583
76472
  console.log(source_default.dim("No features yet."));
75584
76473
  return;
@@ -75593,7 +76482,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
75593
76482
  Features:
75594
76483
  `));
75595
76484
  for (const name of entries) {
75596
- const prdPath = join42(featuresDir, name, "prd.json");
76485
+ const prdPath = join43(featuresDir, name, "prd.json");
75597
76486
  if (existsSync32(prdPath)) {
75598
76487
  const prd = await loadPRD(prdPath);
75599
76488
  const c = countStories(prd);
@@ -75646,7 +76535,7 @@ program2.command("analyze").description("Parse spec.md into prd.json via agent d
75646
76535
  console.error(source_default.red("nax not initialized. Run: nax init"));
75647
76536
  process.exit(1);
75648
76537
  }
75649
- const featureDir = join42(naxDir, "features", options.feature);
76538
+ const featureDir = join43(naxDir, "features", options.feature);
75650
76539
  if (!existsSync32(featureDir)) {
75651
76540
  console.error(source_default.red(`Feature "${options.feature}" not found.`));
75652
76541
  process.exit(1);
@@ -75662,7 +76551,7 @@ program2.command("analyze").description("Parse spec.md into prd.json via agent d
75662
76551
  specPath: options.from,
75663
76552
  reclassify: options.reclassify
75664
76553
  });
75665
- const prdPath = join42(featureDir, "prd.json");
76554
+ const prdPath = join43(featureDir, "prd.json");
75666
76555
  await Bun.write(prdPath, JSON.stringify(prd, null, 2));
75667
76556
  const c = countStories(prd);
75668
76557
  console.log(source_default.green(`