@nathapp/nax 0.40.1 → 0.42.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 (46) hide show
  1. package/README.md +1 -0
  2. package/bin/nax.ts +130 -11
  3. package/dist/nax.js +1520 -424
  4. package/package.json +8 -7
  5. package/src/acceptance/fix-generator.ts +4 -35
  6. package/src/acceptance/generator.ts +4 -27
  7. package/src/agents/acp/adapter.ts +642 -0
  8. package/src/agents/acp/cost.ts +79 -0
  9. package/src/agents/acp/index.ts +9 -0
  10. package/src/agents/acp/interaction-bridge.ts +126 -0
  11. package/src/agents/acp/parser.ts +166 -0
  12. package/src/agents/acp/spawn-client.ts +309 -0
  13. package/src/agents/acp/types.ts +22 -0
  14. package/src/agents/claude-complete.ts +3 -3
  15. package/src/agents/claude.ts +12 -2
  16. package/src/agents/registry.ts +83 -0
  17. package/src/agents/types-extended.ts +23 -0
  18. package/src/agents/types.ts +17 -0
  19. package/src/analyze/scanner.ts +16 -20
  20. package/src/cli/analyze.ts +6 -2
  21. package/src/cli/plan.ts +218 -129
  22. package/src/commands/precheck.ts +1 -1
  23. package/src/config/defaults.ts +1 -0
  24. package/src/config/runtime-types.ts +10 -0
  25. package/src/config/schema.ts +1 -0
  26. package/src/config/schemas.ts +6 -0
  27. package/src/config/types.ts +1 -0
  28. package/src/execution/executor-types.ts +6 -0
  29. package/src/execution/iteration-runner.ts +2 -0
  30. package/src/execution/lifecycle/acceptance-loop.ts +5 -2
  31. package/src/execution/lifecycle/run-initialization.ts +16 -4
  32. package/src/execution/lifecycle/run-setup.ts +4 -0
  33. package/src/execution/runner-completion.ts +11 -1
  34. package/src/execution/runner-execution.ts +8 -0
  35. package/src/execution/runner-setup.ts +4 -0
  36. package/src/execution/runner.ts +10 -0
  37. package/src/interaction/plugins/webhook.ts +10 -1
  38. package/src/pipeline/stages/execution.ts +33 -1
  39. package/src/pipeline/stages/routing.ts +18 -7
  40. package/src/pipeline/types.ts +10 -0
  41. package/src/prd/schema.ts +249 -0
  42. package/src/tdd/orchestrator.ts +7 -0
  43. package/src/tdd/rectification-gate.ts +6 -0
  44. package/src/tdd/session-runner.ts +15 -2
  45. package/src/utils/git.ts +30 -0
  46. package/src/verification/runners.ts +10 -1
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({
@@ -17794,6 +17802,10 @@ var init_schemas3 = __esm(() => {
17794
17802
  maxDescriptionLength: exports_external.number().int().min(100).max(1e4).default(2000),
17795
17803
  maxBulletPoints: exports_external.number().int().min(1).max(100).default(8)
17796
17804
  });
17805
+ AgentConfigSchema = exports_external.object({
17806
+ protocol: exports_external.enum(["acp", "cli"]).default("acp"),
17807
+ acpPermissionMode: exports_external.string().optional()
17808
+ });
17797
17809
  PrecheckConfigSchema = exports_external.object({
17798
17810
  storySizeGate: StorySizeGateConfigSchema
17799
17811
  });
@@ -17829,6 +17841,7 @@ var init_schemas3 = __esm(() => {
17829
17841
  disabledPlugins: exports_external.array(exports_external.string()).optional(),
17830
17842
  hooks: HooksConfigSchema.optional(),
17831
17843
  interaction: InteractionConfigSchema.optional(),
17844
+ agent: AgentConfigSchema.optional(),
17832
17845
  precheck: PrecheckConfigSchema.optional(),
17833
17846
  prompts: PromptsConfigSchema.optional(),
17834
17847
  decompose: DecomposeConfigSchema.optional()
@@ -18240,7 +18253,7 @@ class ClaudeCodeAdapter {
18240
18253
  const backoffMs = 2 ** attempt * 1000;
18241
18254
  const logger = getLogger();
18242
18255
  logger.warn("agent", "Rate limited, retrying", { backoffSeconds: backoffMs / 1000, attempt, maxRetries });
18243
- await Bun.sleep(backoffMs);
18256
+ await _claudeAdapterDeps.sleep(backoffMs);
18244
18257
  continue;
18245
18258
  }
18246
18259
  return result;
@@ -18256,7 +18269,7 @@ class ClaudeCodeAdapter {
18256
18269
  attempt,
18257
18270
  maxRetries
18258
18271
  });
18259
- await Bun.sleep(backoffMs);
18272
+ await _claudeAdapterDeps.sleep(backoffMs);
18260
18273
  continue;
18261
18274
  }
18262
18275
  throw lastError;
@@ -18336,7 +18349,7 @@ class ClaudeCodeAdapter {
18336
18349
  return runInteractiveMode(this.binary, options, pidRegistry);
18337
18350
  }
18338
18351
  }
18339
- var _decomposeDeps;
18352
+ var _decomposeDeps, _claudeAdapterDeps;
18340
18353
  var init_claude = __esm(() => {
18341
18354
  init_pid_registry();
18342
18355
  init_logger2();
@@ -18349,6 +18362,9 @@ var init_claude = __esm(() => {
18349
18362
  return Bun.spawn(cmd, opts);
18350
18363
  }
18351
18364
  };
18365
+ _claudeAdapterDeps = {
18366
+ sleep: (ms) => Bun.sleep(ms)
18367
+ };
18352
18368
  });
18353
18369
 
18354
18370
  // src/utils/errors.ts
@@ -18676,29 +18692,10 @@ async function generateAcceptanceTests(adapter, options) {
18676
18692
  logger.info("acceptance", "Found acceptance criteria", { count: criteria.length });
18677
18693
  const prompt = buildAcceptanceTestPrompt(criteria, options.featureName, options.codebaseContext);
18678
18694
  try {
18679
- const skipPerms = options.config.quality?.dangerouslySkipPermissions ?? true;
18680
- const permArgs = skipPerms ? ["--dangerously-skip-permissions"] : [];
18681
- const cmd = [adapter.binary, "--model", options.modelDef.model, ...permArgs, "-p", prompt];
18682
- const proc = Bun.spawn(cmd, {
18683
- cwd: options.workdir,
18684
- stdout: "pipe",
18685
- stderr: "pipe",
18686
- env: {
18687
- ...process.env,
18688
- ...options.modelDef.env || {}
18689
- }
18695
+ const output = await adapter.complete(prompt, {
18696
+ model: options.modelDef.model
18690
18697
  });
18691
- const exitCode = await proc.exited;
18692
- const stdout = await new Response(proc.stdout).text();
18693
- const stderr = await new Response(proc.stderr).text();
18694
- if (exitCode !== 0) {
18695
- logger.warn("acceptance", "\u26A0 Agent test generation failed", { stderr });
18696
- return {
18697
- testCode: generateSkeletonTests(options.featureName, criteria),
18698
- criteria
18699
- };
18700
- }
18701
- const testCode = extractTestCode(stdout);
18698
+ const testCode = extractTestCode(output);
18702
18699
  return {
18703
18700
  testCode,
18704
18701
  criteria
@@ -18801,7 +18798,7 @@ Requirements:
18801
18798
  Respond with ONLY the fix description (no JSON, no markdown, just the description text).`;
18802
18799
  }
18803
18800
  async function generateFixStories(adapter, options) {
18804
- const { failedACs, testOutput, prd, specContent, workdir, modelDef } = options;
18801
+ const { failedACs, testOutput, prd, specContent, modelDef } = options;
18805
18802
  const fixStories = [];
18806
18803
  const acTextMap = parseACTextFromSpec(specContent);
18807
18804
  const logger = getLogger();
@@ -18816,34 +18813,9 @@ async function generateFixStories(adapter, options) {
18816
18813
  }
18817
18814
  const prompt = buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd);
18818
18815
  try {
18819
- const skipPerms = options.config.quality?.dangerouslySkipPermissions ?? true;
18820
- const permArgs = skipPerms ? ["--dangerously-skip-permissions"] : [];
18821
- const cmd = [adapter.binary, "--model", modelDef.model, ...permArgs, "-p", prompt];
18822
- const proc = Bun.spawn(cmd, {
18823
- cwd: workdir,
18824
- stdout: "pipe",
18825
- stderr: "pipe",
18826
- env: {
18827
- ...process.env,
18828
- ...modelDef.env || {}
18829
- }
18816
+ const fixDescription = await adapter.complete(prompt, {
18817
+ model: modelDef.model
18830
18818
  });
18831
- const exitCode = await proc.exited;
18832
- const stdout = await new Response(proc.stdout).text();
18833
- const stderr = await new Response(proc.stderr).text();
18834
- if (exitCode !== 0) {
18835
- logger.warn("acceptance", "\u26A0 Agent fix generation failed", { failedAC, stderr });
18836
- fixStories.push({
18837
- id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
18838
- title: `Fix: ${failedAC}`,
18839
- failedAC,
18840
- testOutput,
18841
- relatedStories,
18842
- description: `Fix the implementation to make ${failedAC} pass. Related stories: ${relatedStories.join(", ")}.`
18843
- });
18844
- continue;
18845
- }
18846
- const fixDescription = stdout.trim();
18847
18819
  fixStories.push({
18848
18820
  id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
18849
18821
  title: `Fix: ${failedAC} \u2014 ${acText.slice(0, 50)}`,
@@ -18910,6 +18882,676 @@ var init_acceptance = __esm(() => {
18910
18882
  init_fix_generator();
18911
18883
  });
18912
18884
 
18885
+ // src/agents/acp/parser.ts
18886
+ function parseAcpxJsonOutput(rawOutput) {
18887
+ const lines = rawOutput.split(`
18888
+ `).filter((l) => l.trim());
18889
+ let text = "";
18890
+ let tokenUsage;
18891
+ let stopReason;
18892
+ let error48;
18893
+ for (const line of lines) {
18894
+ try {
18895
+ const event = JSON.parse(line);
18896
+ if (event.content && typeof event.content === "string")
18897
+ text += event.content;
18898
+ if (event.text && typeof event.text === "string")
18899
+ text += event.text;
18900
+ if (event.result && typeof event.result === "string")
18901
+ text = event.result;
18902
+ if (event.cumulative_token_usage)
18903
+ tokenUsage = event.cumulative_token_usage;
18904
+ if (event.usage) {
18905
+ tokenUsage = {
18906
+ input_tokens: event.usage.input_tokens ?? event.usage.prompt_tokens ?? 0,
18907
+ output_tokens: event.usage.output_tokens ?? event.usage.completion_tokens ?? 0
18908
+ };
18909
+ }
18910
+ if (event.stopReason)
18911
+ stopReason = event.stopReason;
18912
+ if (event.stop_reason)
18913
+ stopReason = event.stop_reason;
18914
+ if (event.error) {
18915
+ error48 = typeof event.error === "string" ? event.error : event.error.message ?? JSON.stringify(event.error);
18916
+ }
18917
+ } catch {
18918
+ if (!text)
18919
+ text = line;
18920
+ }
18921
+ }
18922
+ return { text: text.trim(), tokenUsage, stopReason, error: error48 };
18923
+ }
18924
+
18925
+ // src/agents/acp/spawn-client.ts
18926
+ function buildAllowedEnv2(extraEnv) {
18927
+ const allowed = {};
18928
+ const essentialVars = ["PATH", "HOME", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
18929
+ for (const varName of essentialVars) {
18930
+ if (process.env[varName])
18931
+ allowed[varName] = process.env[varName];
18932
+ }
18933
+ const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "CLAUDE_API_KEY"];
18934
+ for (const varName of apiKeyVars) {
18935
+ if (process.env[varName])
18936
+ allowed[varName] = process.env[varName];
18937
+ }
18938
+ const allowedPrefixes = ["CLAUDE_", "NAX_", "CLAW_", "TURBO_", "ACPX_", "CODEX_", "GEMINI_"];
18939
+ for (const [key, value] of Object.entries(process.env)) {
18940
+ if (allowedPrefixes.some((prefix) => key.startsWith(prefix))) {
18941
+ allowed[key] = value;
18942
+ }
18943
+ }
18944
+ if (extraEnv)
18945
+ Object.assign(allowed, extraEnv);
18946
+ return allowed;
18947
+ }
18948
+
18949
+ class SpawnAcpSession {
18950
+ agentName;
18951
+ sessionName;
18952
+ cwd;
18953
+ model;
18954
+ timeoutSeconds;
18955
+ permissionMode;
18956
+ env;
18957
+ constructor(opts) {
18958
+ this.agentName = opts.agentName;
18959
+ this.sessionName = opts.sessionName;
18960
+ this.cwd = opts.cwd;
18961
+ this.model = opts.model;
18962
+ this.timeoutSeconds = opts.timeoutSeconds;
18963
+ this.permissionMode = opts.permissionMode;
18964
+ this.env = opts.env;
18965
+ }
18966
+ async prompt(text) {
18967
+ const cmd = [
18968
+ "acpx",
18969
+ "--cwd",
18970
+ this.cwd,
18971
+ ...this.permissionMode === "approve-all" ? ["--approve-all"] : [],
18972
+ "--format",
18973
+ "json",
18974
+ "--model",
18975
+ this.model,
18976
+ "--timeout",
18977
+ String(this.timeoutSeconds),
18978
+ this.agentName,
18979
+ "prompt",
18980
+ "-s",
18981
+ this.sessionName,
18982
+ "--file",
18983
+ "-"
18984
+ ];
18985
+ getSafeLogger()?.debug("acp-adapter", `Sending prompt to session: ${this.sessionName}`);
18986
+ const proc = _spawnClientDeps.spawn(cmd, {
18987
+ cwd: this.cwd,
18988
+ stdin: "pipe",
18989
+ stdout: "pipe",
18990
+ stderr: "pipe",
18991
+ env: this.env
18992
+ });
18993
+ proc.stdin.write(text);
18994
+ proc.stdin.end();
18995
+ const exitCode = await proc.exited;
18996
+ const stdout = await new Response(proc.stdout).text();
18997
+ const stderr = await new Response(proc.stderr).text();
18998
+ if (exitCode !== 0) {
18999
+ getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
19000
+ stderr: stderr.slice(0, 200)
19001
+ });
19002
+ return {
19003
+ messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
19004
+ stopReason: "error"
19005
+ };
19006
+ }
19007
+ try {
19008
+ const parsed = parseAcpxJsonOutput(stdout);
19009
+ return {
19010
+ messages: [{ role: "assistant", content: parsed.text || "" }],
19011
+ stopReason: "end_turn",
19012
+ cumulative_token_usage: parsed.tokenUsage
19013
+ };
19014
+ } catch (err) {
19015
+ getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
19016
+ stderr: stderr.slice(0, 200)
19017
+ });
19018
+ throw err;
19019
+ }
19020
+ }
19021
+ async close() {
19022
+ const cmd = ["acpx", this.agentName, "sessions", "close", this.sessionName];
19023
+ getSafeLogger()?.debug("acp-adapter", `Closing session: ${this.sessionName}`);
19024
+ const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
19025
+ const exitCode = await proc.exited;
19026
+ if (exitCode !== 0) {
19027
+ const stderr = await new Response(proc.stderr).text();
19028
+ getSafeLogger()?.warn("acp-adapter", "Failed to close session", {
19029
+ sessionName: this.sessionName,
19030
+ stderr: stderr.slice(0, 200)
19031
+ });
19032
+ }
19033
+ }
19034
+ async cancelActivePrompt() {
19035
+ const cmd = ["acpx", this.agentName, "cancel"];
19036
+ getSafeLogger()?.debug("acp-adapter", `Cancelling active prompt: ${this.sessionName}`);
19037
+ const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
19038
+ await proc.exited;
19039
+ }
19040
+ }
19041
+
19042
+ class SpawnAcpClient {
19043
+ agentName;
19044
+ model;
19045
+ cwd;
19046
+ timeoutSeconds;
19047
+ env;
19048
+ constructor(cmdStr, cwd, timeoutSeconds) {
19049
+ const parts = cmdStr.split(/\s+/);
19050
+ const modelIdx = parts.indexOf("--model");
19051
+ this.model = modelIdx >= 0 && parts[modelIdx + 1] ? parts[modelIdx + 1] : "default";
19052
+ this.agentName = parts[parts.length - 1] || "claude";
19053
+ this.cwd = cwd || process.cwd();
19054
+ this.timeoutSeconds = timeoutSeconds || 1800;
19055
+ this.env = buildAllowedEnv2();
19056
+ }
19057
+ async start() {}
19058
+ async createSession(opts) {
19059
+ const sessionName = opts.sessionName || `nax-${Date.now()}`;
19060
+ const cmd = ["acpx", opts.agentName, "sessions", "ensure", "--name", sessionName];
19061
+ getSafeLogger()?.debug("acp-adapter", `Ensuring session: ${sessionName}`);
19062
+ const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
19063
+ const exitCode = await proc.exited;
19064
+ if (exitCode !== 0) {
19065
+ const stderr = await new Response(proc.stderr).text();
19066
+ throw new Error(`[acp-adapter] Failed to create session: ${stderr || `exit code ${exitCode}`}`);
19067
+ }
19068
+ return new SpawnAcpSession({
19069
+ agentName: opts.agentName,
19070
+ sessionName,
19071
+ cwd: this.cwd,
19072
+ model: this.model,
19073
+ timeoutSeconds: this.timeoutSeconds,
19074
+ permissionMode: opts.permissionMode,
19075
+ env: this.env
19076
+ });
19077
+ }
19078
+ async loadSession(sessionName) {
19079
+ const cmd = ["acpx", this.agentName, "sessions", "ensure", "--name", sessionName];
19080
+ const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
19081
+ const exitCode = await proc.exited;
19082
+ if (exitCode !== 0) {
19083
+ return null;
19084
+ }
19085
+ return new SpawnAcpSession({
19086
+ agentName: this.agentName,
19087
+ sessionName,
19088
+ cwd: this.cwd,
19089
+ model: this.model,
19090
+ timeoutSeconds: this.timeoutSeconds,
19091
+ permissionMode: "approve-all",
19092
+ env: this.env
19093
+ });
19094
+ }
19095
+ async close() {}
19096
+ }
19097
+ function createSpawnAcpClient(cmdStr, cwd, timeoutSeconds) {
19098
+ return new SpawnAcpClient(cmdStr, cwd, timeoutSeconds);
19099
+ }
19100
+ var _spawnClientDeps;
19101
+ var init_spawn_client = __esm(() => {
19102
+ init_logger2();
19103
+ _spawnClientDeps = {
19104
+ spawn(cmd, opts) {
19105
+ return Bun.spawn(cmd, opts);
19106
+ }
19107
+ };
19108
+ });
19109
+
19110
+ // src/agents/acp/cost.ts
19111
+ function estimateCostFromTokenUsage(usage, model) {
19112
+ const pricing = MODEL_PRICING[model];
19113
+ if (!pricing) {
19114
+ const fallbackInputRate = 3 / 1e6;
19115
+ const fallbackOutputRate = 15 / 1e6;
19116
+ const inputCost2 = (usage.input_tokens ?? 0) * fallbackInputRate;
19117
+ const outputCost2 = (usage.output_tokens ?? 0) * fallbackOutputRate;
19118
+ const cacheReadCost2 = (usage.cache_read_input_tokens ?? 0) * (0.5 / 1e6);
19119
+ const cacheCreationCost2 = (usage.cache_creation_input_tokens ?? 0) * (2 / 1e6);
19120
+ return inputCost2 + outputCost2 + cacheReadCost2 + cacheCreationCost2;
19121
+ }
19122
+ const inputRate = pricing.input / 1e6;
19123
+ const outputRate = pricing.output / 1e6;
19124
+ const cacheReadRate = (pricing.cacheRead ?? pricing.input * 0.1) / 1e6;
19125
+ const cacheCreationRate = (pricing.cacheCreation ?? pricing.input * 0.33) / 1e6;
19126
+ const inputCost = (usage.input_tokens ?? 0) * inputRate;
19127
+ const outputCost = (usage.output_tokens ?? 0) * outputRate;
19128
+ const cacheReadCost = (usage.cache_read_input_tokens ?? 0) * cacheReadRate;
19129
+ const cacheCreationCost = (usage.cache_creation_input_tokens ?? 0) * cacheCreationRate;
19130
+ return inputCost + outputCost + cacheReadCost + cacheCreationCost;
19131
+ }
19132
+ var MODEL_PRICING;
19133
+ var init_cost2 = __esm(() => {
19134
+ MODEL_PRICING = {
19135
+ "claude-sonnet-4": { input: 3, output: 15 },
19136
+ "claude-sonnet-4-5": { input: 3, output: 15 },
19137
+ "claude-haiku": { input: 0.8, output: 4, cacheRead: 0.1, cacheCreation: 1 },
19138
+ "claude-haiku-4-5": { input: 0.8, output: 4, cacheRead: 0.1, cacheCreation: 1 },
19139
+ "claude-opus": { input: 15, output: 75 },
19140
+ "claude-opus-4": { input: 15, output: 75 },
19141
+ "gpt-4.1": { input: 10, output: 30 },
19142
+ "gpt-4": { input: 30, output: 60 },
19143
+ "gpt-3.5-turbo": { input: 0.5, output: 1.5 },
19144
+ "gemini-2.5-pro": { input: 0.075, output: 0.3 },
19145
+ "gemini-2-pro": { input: 0.075, output: 0.3 },
19146
+ codex: { input: 0.02, output: 0.06 },
19147
+ "code-davinci-002": { input: 0.02, output: 0.06 }
19148
+ };
19149
+ });
19150
+
19151
+ // src/agents/acp/adapter.ts
19152
+ import { createHash } from "crypto";
19153
+ import { join as join3 } from "path";
19154
+ function resolveRegistryEntry(agentName) {
19155
+ return AGENT_REGISTRY[agentName] ?? DEFAULT_ENTRY;
19156
+ }
19157
+ function isRateLimitError(err) {
19158
+ if (!(err instanceof Error))
19159
+ return false;
19160
+ const msg = err.message.toLowerCase();
19161
+ return msg.includes("rate limit") || msg.includes("rate_limit") || msg.includes("429");
19162
+ }
19163
+ function buildSessionName(workdir, featureName, storyId, sessionRole) {
19164
+ const hash2 = createHash("sha256").update(workdir).digest("hex").slice(0, 8);
19165
+ const sanitize = (s) => s.replace(/[^a-z0-9]+/gi, "-").toLowerCase().replace(/^-+|-+$/g, "");
19166
+ const parts = ["nax", hash2];
19167
+ if (featureName)
19168
+ parts.push(sanitize(featureName));
19169
+ if (storyId)
19170
+ parts.push(sanitize(storyId));
19171
+ if (sessionRole)
19172
+ parts.push(sanitize(sessionRole));
19173
+ return parts.join("-");
19174
+ }
19175
+ async function ensureAcpSession(client, sessionName, agentName, permissionMode) {
19176
+ if (client.loadSession) {
19177
+ try {
19178
+ const existing = await client.loadSession(sessionName);
19179
+ if (existing) {
19180
+ getSafeLogger()?.debug("acp-adapter", `Resumed existing session: ${sessionName}`);
19181
+ return existing;
19182
+ }
19183
+ } catch {}
19184
+ }
19185
+ getSafeLogger()?.debug("acp-adapter", `Creating new session: ${sessionName}`);
19186
+ return client.createSession({ agentName, permissionMode, sessionName });
19187
+ }
19188
+ async function runSessionPrompt(session, prompt, timeoutMs) {
19189
+ const promptPromise = session.prompt(prompt);
19190
+ const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve("timeout"), timeoutMs));
19191
+ const winner = await Promise.race([promptPromise, timeoutPromise]);
19192
+ if (winner === "timeout") {
19193
+ try {
19194
+ await session.cancelActivePrompt();
19195
+ } catch {
19196
+ await session.close().catch(() => {});
19197
+ }
19198
+ return { response: null, timedOut: true };
19199
+ }
19200
+ return { response: winner, timedOut: false };
19201
+ }
19202
+ async function closeAcpSession(session) {
19203
+ try {
19204
+ await session.close();
19205
+ } catch (err) {
19206
+ getSafeLogger()?.warn("acp-adapter", "Failed to close session", { error: String(err) });
19207
+ }
19208
+ }
19209
+ function acpSessionsPath(workdir, featureName) {
19210
+ return join3(workdir, "nax", "features", featureName, "acp-sessions.json");
19211
+ }
19212
+ async function saveAcpSession(workdir, featureName, storyId, sessionName) {
19213
+ try {
19214
+ const path = acpSessionsPath(workdir, featureName);
19215
+ let data = {};
19216
+ try {
19217
+ const existing = await Bun.file(path).text();
19218
+ data = JSON.parse(existing);
19219
+ } catch {}
19220
+ data[storyId] = sessionName;
19221
+ await Bun.write(path, JSON.stringify(data, null, 2));
19222
+ } catch (err) {
19223
+ getSafeLogger()?.warn("acp-adapter", "Failed to save session to sidecar", { error: String(err) });
19224
+ }
19225
+ }
19226
+ async function clearAcpSession(workdir, featureName, storyId) {
19227
+ try {
19228
+ const path = acpSessionsPath(workdir, featureName);
19229
+ let data = {};
19230
+ try {
19231
+ const existing = await Bun.file(path).text();
19232
+ data = JSON.parse(existing);
19233
+ } catch {
19234
+ return;
19235
+ }
19236
+ delete data[storyId];
19237
+ await Bun.write(path, JSON.stringify(data, null, 2));
19238
+ } catch (err) {
19239
+ getSafeLogger()?.warn("acp-adapter", "Failed to clear session from sidecar", { error: String(err) });
19240
+ }
19241
+ }
19242
+ async function readAcpSession(workdir, featureName, storyId) {
19243
+ try {
19244
+ const path = acpSessionsPath(workdir, featureName);
19245
+ const existing = await Bun.file(path).text();
19246
+ const data = JSON.parse(existing);
19247
+ return data[storyId] ?? null;
19248
+ } catch {
19249
+ return null;
19250
+ }
19251
+ }
19252
+ function extractOutput(response) {
19253
+ if (!response)
19254
+ return "";
19255
+ return response.messages.filter((m) => m.role === "assistant").map((m) => m.content).join(`
19256
+ `).trim();
19257
+ }
19258
+ function extractQuestion(output) {
19259
+ const text = output.trim();
19260
+ if (!text)
19261
+ return null;
19262
+ const sentences = text.split(/(?<=[.!?])\s+/);
19263
+ const questionSentences = sentences.filter((s) => s.trim().endsWith("?"));
19264
+ if (questionSentences.length > 0) {
19265
+ const q = questionSentences[questionSentences.length - 1].trim();
19266
+ if (q.length > 10)
19267
+ return q;
19268
+ }
19269
+ const lower = text.toLowerCase();
19270
+ const markers = [
19271
+ "please confirm",
19272
+ "please specify",
19273
+ "please provide",
19274
+ "which would you",
19275
+ "should i ",
19276
+ "do you want",
19277
+ "can you clarify"
19278
+ ];
19279
+ for (const marker of markers) {
19280
+ if (lower.includes(marker)) {
19281
+ return text.slice(-200).trim();
19282
+ }
19283
+ }
19284
+ return null;
19285
+ }
19286
+
19287
+ class AcpAgentAdapter {
19288
+ name;
19289
+ displayName;
19290
+ binary;
19291
+ capabilities;
19292
+ constructor(agentName) {
19293
+ const entry = resolveRegistryEntry(agentName);
19294
+ this.name = agentName;
19295
+ this.displayName = entry.displayName;
19296
+ this.binary = entry.binary;
19297
+ this.capabilities = {
19298
+ supportedTiers: entry.supportedTiers,
19299
+ maxContextTokens: entry.maxContextTokens,
19300
+ features: new Set(["tdd", "review", "refactor"])
19301
+ };
19302
+ }
19303
+ async isInstalled() {
19304
+ const path = _acpAdapterDeps.which(this.binary);
19305
+ return path !== null;
19306
+ }
19307
+ buildCommand(_options) {
19308
+ return ["acpx", this.name, "session"];
19309
+ }
19310
+ buildAllowedEnv(_options) {
19311
+ return {};
19312
+ }
19313
+ async run(options) {
19314
+ const startTime = Date.now();
19315
+ let lastError;
19316
+ getSafeLogger()?.debug("acp-adapter", `Starting run for ${this.name}`, {
19317
+ model: options.modelDef.model,
19318
+ workdir: options.workdir,
19319
+ featureName: options.featureName,
19320
+ storyId: options.storyId,
19321
+ sessionRole: options.sessionRole
19322
+ });
19323
+ for (let attempt = 0;attempt < MAX_RATE_LIMIT_RETRIES; attempt++) {
19324
+ try {
19325
+ const result = await this._runWithClient(options, startTime);
19326
+ if (!result.success) {
19327
+ getSafeLogger()?.warn("acp-adapter", `Run failed for ${this.name}`, { exitCode: result.exitCode });
19328
+ }
19329
+ return result;
19330
+ } catch (err) {
19331
+ const error48 = err instanceof Error ? err : new Error(String(err));
19332
+ lastError = error48;
19333
+ const shouldRetry = isRateLimitError(error48) && attempt < MAX_RATE_LIMIT_RETRIES - 1;
19334
+ if (!shouldRetry)
19335
+ break;
19336
+ const backoffMs = 2 ** (attempt + 1) * 1000;
19337
+ getSafeLogger()?.warn("acp-adapter", "Retrying after rate limit", {
19338
+ backoffSeconds: backoffMs / 1000,
19339
+ attempt: attempt + 1
19340
+ });
19341
+ await _acpAdapterDeps.sleep(backoffMs);
19342
+ }
19343
+ }
19344
+ const durationMs = Date.now() - startTime;
19345
+ return {
19346
+ success: false,
19347
+ exitCode: 1,
19348
+ output: lastError?.message ?? "Run failed",
19349
+ rateLimited: isRateLimitError(lastError),
19350
+ durationMs,
19351
+ estimatedCost: 0
19352
+ };
19353
+ }
19354
+ async _runWithClient(options, startTime) {
19355
+ const cmdStr = `acpx --model ${options.modelDef.model} ${this.name}`;
19356
+ const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds);
19357
+ await client.start();
19358
+ let sessionName = options.acpSessionName;
19359
+ if (!sessionName && options.featureName && options.storyId) {
19360
+ sessionName = await readAcpSession(options.workdir, options.featureName, options.storyId) ?? undefined;
19361
+ }
19362
+ sessionName ??= buildSessionName(options.workdir, options.featureName, options.storyId, options.sessionRole);
19363
+ const permissionMode = options.dangerouslySkipPermissions ? "approve-all" : "default";
19364
+ const session = await ensureAcpSession(client, sessionName, this.name, permissionMode);
19365
+ if (options.featureName && options.storyId) {
19366
+ await saveAcpSession(options.workdir, options.featureName, options.storyId, sessionName);
19367
+ }
19368
+ let lastResponse = null;
19369
+ let timedOut = false;
19370
+ const totalTokenUsage = { input_tokens: 0, output_tokens: 0 };
19371
+ try {
19372
+ let currentPrompt = options.prompt;
19373
+ let turnCount = 0;
19374
+ const MAX_TURNS = options.interactionBridge ? 10 : 1;
19375
+ while (turnCount < MAX_TURNS) {
19376
+ turnCount++;
19377
+ getSafeLogger()?.debug("acp-adapter", `Session turn ${turnCount}/${MAX_TURNS}`, { sessionName });
19378
+ const turnResult = await runSessionPrompt(session, currentPrompt, options.timeoutSeconds * 1000);
19379
+ if (turnResult.timedOut) {
19380
+ timedOut = true;
19381
+ break;
19382
+ }
19383
+ lastResponse = turnResult.response;
19384
+ if (!lastResponse)
19385
+ break;
19386
+ if (lastResponse.cumulative_token_usage) {
19387
+ totalTokenUsage.input_tokens += lastResponse.cumulative_token_usage.input_tokens ?? 0;
19388
+ totalTokenUsage.output_tokens += lastResponse.cumulative_token_usage.output_tokens ?? 0;
19389
+ }
19390
+ const outputText = extractOutput(lastResponse);
19391
+ const question = extractQuestion(outputText);
19392
+ if (!question || !options.interactionBridge)
19393
+ break;
19394
+ getSafeLogger()?.debug("acp-adapter", "Agent asked question, routing to interactionBridge", { question });
19395
+ try {
19396
+ const answer = await Promise.race([
19397
+ options.interactionBridge.onQuestionDetected(question),
19398
+ new Promise((_, reject) => setTimeout(() => reject(new Error("interaction timeout")), INTERACTION_TIMEOUT_MS))
19399
+ ]);
19400
+ currentPrompt = answer;
19401
+ } catch (err) {
19402
+ const msg = err instanceof Error ? err.message : String(err);
19403
+ getSafeLogger()?.warn("acp-adapter", `InteractionBridge failed: ${msg}`);
19404
+ break;
19405
+ }
19406
+ }
19407
+ if (turnCount >= MAX_TURNS && options.interactionBridge) {
19408
+ getSafeLogger()?.warn("acp-adapter", "Reached max turns limit", { sessionName, maxTurns: MAX_TURNS });
19409
+ }
19410
+ } finally {
19411
+ await closeAcpSession(session);
19412
+ await client.close().catch(() => {});
19413
+ if (options.featureName && options.storyId) {
19414
+ await clearAcpSession(options.workdir, options.featureName, options.storyId);
19415
+ }
19416
+ }
19417
+ const durationMs = Date.now() - startTime;
19418
+ if (timedOut) {
19419
+ return {
19420
+ success: false,
19421
+ exitCode: 124,
19422
+ output: `Session timed out after ${options.timeoutSeconds}s`,
19423
+ rateLimited: false,
19424
+ durationMs,
19425
+ estimatedCost: 0
19426
+ };
19427
+ }
19428
+ const success2 = lastResponse?.stopReason === "end_turn";
19429
+ const output = extractOutput(lastResponse);
19430
+ const estimatedCost = totalTokenUsage.input_tokens > 0 || totalTokenUsage.output_tokens > 0 ? estimateCostFromTokenUsage(totalTokenUsage, options.modelDef.model) : 0;
19431
+ return {
19432
+ success: success2,
19433
+ exitCode: success2 ? 0 : 1,
19434
+ output: output.slice(-MAX_AGENT_OUTPUT_CHARS2),
19435
+ rateLimited: false,
19436
+ durationMs,
19437
+ estimatedCost
19438
+ };
19439
+ }
19440
+ async complete(prompt, _options) {
19441
+ const model = _options?.model ?? "default";
19442
+ const cmdStr = `acpx --model ${model} ${this.name}`;
19443
+ const client = _acpAdapterDeps.createClient(cmdStr);
19444
+ await client.start();
19445
+ const permissionMode = _options?.dangerouslySkipPermissions ? "approve-all" : "default";
19446
+ let session = null;
19447
+ try {
19448
+ session = await client.createSession({ agentName: this.name, permissionMode });
19449
+ const response = await session.prompt(prompt);
19450
+ if (response.stopReason === "error") {
19451
+ throw new CompleteError("complete() failed: stop reason is error");
19452
+ }
19453
+ const text = response.messages.filter((m) => m.role === "assistant").map((m) => m.content).join(`
19454
+ `).trim();
19455
+ if (!text) {
19456
+ throw new CompleteError("complete() returned empty output");
19457
+ }
19458
+ return text;
19459
+ } finally {
19460
+ if (session) {
19461
+ await session.close().catch(() => {});
19462
+ }
19463
+ await client.close().catch(() => {});
19464
+ }
19465
+ }
19466
+ async plan(options) {
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
+
18913
19555
  // src/agents/adapters/aider.ts
18914
19556
  class AiderAdapter {
18915
19557
  name = "aider";
@@ -18940,7 +19582,7 @@ class AiderAdapter {
18940
19582
  return {
18941
19583
  success: exitCode === 0,
18942
19584
  exitCode,
18943
- output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS2),
19585
+ output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS3),
18944
19586
  rateLimited: false,
18945
19587
  durationMs,
18946
19588
  estimatedCost: 0,
@@ -18974,7 +19616,7 @@ class AiderAdapter {
18974
19616
  throw new Error("AiderAdapter.decompose() not implemented");
18975
19617
  }
18976
19618
  }
18977
- var _aiderCompleteDeps, MAX_AGENT_OUTPUT_CHARS2 = 5000;
19619
+ var _aiderCompleteDeps, MAX_AGENT_OUTPUT_CHARS3 = 5000;
18978
19620
  var init_aider = __esm(() => {
18979
19621
  init_types2();
18980
19622
  _aiderCompleteDeps = {
@@ -19018,7 +19660,7 @@ class CodexAdapter {
19018
19660
  return {
19019
19661
  success: exitCode === 0,
19020
19662
  exitCode,
19021
- output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS3),
19663
+ output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS4),
19022
19664
  rateLimited: false,
19023
19665
  durationMs,
19024
19666
  estimatedCost: 0,
@@ -19049,7 +19691,7 @@ class CodexAdapter {
19049
19691
  throw new Error("CodexAdapter.decompose() not implemented");
19050
19692
  }
19051
19693
  }
19052
- var _codexRunDeps, _codexCompleteDeps, MAX_AGENT_OUTPUT_CHARS3 = 5000;
19694
+ var _codexRunDeps, _codexCompleteDeps, MAX_AGENT_OUTPUT_CHARS4 = 5000;
19053
19695
  var init_codex = __esm(() => {
19054
19696
  init_types2();
19055
19697
  _codexRunDeps = {
@@ -19118,7 +19760,7 @@ class GeminiAdapter {
19118
19760
  return {
19119
19761
  success: exitCode === 0,
19120
19762
  exitCode,
19121
- output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS4),
19763
+ output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS5),
19122
19764
  rateLimited: false,
19123
19765
  durationMs,
19124
19766
  estimatedCost: 0,
@@ -19149,7 +19791,7 @@ class GeminiAdapter {
19149
19791
  throw new Error("GeminiAdapter.decompose() not implemented");
19150
19792
  }
19151
19793
  }
19152
- var _geminiRunDeps, _geminiCompleteDeps, MAX_AGENT_OUTPUT_CHARS4 = 5000;
19794
+ var _geminiRunDeps, _geminiCompleteDeps, MAX_AGENT_OUTPUT_CHARS5 = 5000;
19153
19795
  var init_gemini = __esm(() => {
19154
19796
  init_types2();
19155
19797
  _geminiRunDeps = {
@@ -19230,6 +19872,7 @@ __export(exports_registry, {
19230
19872
  getInstalledAgents: () => getInstalledAgents,
19231
19873
  getAllAgentNames: () => getAllAgentNames,
19232
19874
  getAgent: () => getAgent,
19875
+ createAgentRegistry: () => createAgentRegistry,
19233
19876
  checkAgentHealth: () => checkAgentHealth,
19234
19877
  ALL_AGENTS: () => ALL_AGENTS
19235
19878
  });
@@ -19253,8 +19896,57 @@ async function checkAgentHealth() {
19253
19896
  installed: await agent.isInstalled()
19254
19897
  })));
19255
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
+ }
19256
19946
  var ALL_AGENTS;
19257
19947
  var init_registry = __esm(() => {
19948
+ init_logger2();
19949
+ init_adapter();
19258
19950
  init_aider();
19259
19951
  init_codex();
19260
19952
  init_gemini();
@@ -19416,7 +20108,7 @@ var init_chain = __esm(() => {
19416
20108
  });
19417
20109
 
19418
20110
  // src/utils/path-security.ts
19419
- import { isAbsolute, join as join4, normalize, resolve } from "path";
20111
+ import { isAbsolute, join as join5, normalize, resolve } from "path";
19420
20112
  function validateModulePath(modulePath, allowedRoots) {
19421
20113
  if (!modulePath) {
19422
20114
  return { valid: false, error: "Module path is empty" };
@@ -19432,7 +20124,7 @@ function validateModulePath(modulePath, allowedRoots) {
19432
20124
  }
19433
20125
  } else {
19434
20126
  for (const root of normalizedRoots) {
19435
- const absoluteTarget = resolve(join4(root, modulePath));
20127
+ const absoluteTarget = resolve(join5(root, modulePath));
19436
20128
  if (absoluteTarget.startsWith(`${root}/`) || absoluteTarget === root) {
19437
20129
  return { valid: true, absolutePath: absoluteTarget };
19438
20130
  }
@@ -19782,27 +20474,27 @@ var init_path_security2 = () => {};
19782
20474
 
19783
20475
  // src/config/paths.ts
19784
20476
  import { homedir } from "os";
19785
- import { join as join5, resolve as resolve4 } from "path";
20477
+ import { join as join6, resolve as resolve4 } from "path";
19786
20478
  function globalConfigDir() {
19787
- return join5(homedir(), ".nax");
20479
+ return join6(homedir(), ".nax");
19788
20480
  }
19789
20481
  var init_paths = () => {};
19790
20482
 
19791
20483
  // src/config/loader.ts
19792
20484
  import { existsSync as existsSync5 } from "fs";
19793
- import { join as join6, resolve as resolve5 } from "path";
20485
+ import { join as join7, resolve as resolve5 } from "path";
19794
20486
  function globalConfigPath() {
19795
- return join6(globalConfigDir(), "config.json");
20487
+ return join7(globalConfigDir(), "config.json");
19796
20488
  }
19797
20489
  function findProjectDir(startDir = process.cwd()) {
19798
20490
  let dir = resolve5(startDir);
19799
20491
  let depth = 0;
19800
20492
  while (depth < MAX_DIRECTORY_DEPTH) {
19801
- const candidate = join6(dir, "nax");
19802
- if (existsSync5(join6(candidate, "config.json"))) {
20493
+ const candidate = join7(dir, "nax");
20494
+ if (existsSync5(join7(candidate, "config.json"))) {
19803
20495
  return candidate;
19804
20496
  }
19805
- const parent = join6(dir, "..");
20497
+ const parent = join7(dir, "..");
19806
20498
  if (parent === dir)
19807
20499
  break;
19808
20500
  dir = parent;
@@ -19840,7 +20532,7 @@ async function loadConfig(projectDir, cliOverrides) {
19840
20532
  }
19841
20533
  const projDir = projectDir ?? findProjectDir();
19842
20534
  if (projDir) {
19843
- const projConf = await loadJsonFile(join6(projDir, "config.json"), "config");
20535
+ const projConf = await loadJsonFile(join7(projDir, "config.json"), "config");
19844
20536
  if (projConf) {
19845
20537
  const resolvedProjConf = applyBatchModeCompat(projConf);
19846
20538
  rawConfig = deepMergeConfig(rawConfig, resolvedProjConf);
@@ -21115,7 +21807,7 @@ var package_default;
21115
21807
  var init_package = __esm(() => {
21116
21808
  package_default = {
21117
21809
  name: "@nathapp/nax",
21118
- version: "0.40.1",
21810
+ version: "0.42.0",
21119
21811
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
21120
21812
  type: "module",
21121
21813
  bin: {
@@ -21127,11 +21819,12 @@ var init_package = __esm(() => {
21127
21819
  build: 'bun build bin/nax.ts --outdir dist --target bun --define "GIT_COMMIT=\\"$(git rev-parse --short HEAD)\\""',
21128
21820
  typecheck: "bun x tsc --noEmit",
21129
21821
  lint: "bun x biome check src/ bin/",
21130
- test: "NAX_SKIP_PRECHECK=1 bun test test/ --timeout=60000",
21131
- "test:watch": "bun test --watch",
21132
- "test:unit": "bun test ./test/unit/ --timeout=60000",
21133
- "test:integration": "bun test ./test/integration/ --timeout=60000",
21134
- "test:ui": "bun test ./test/ui/ --timeout=60000",
21822
+ test: "CI=1 NAX_SKIP_PRECHECK=1 bun test test/ --timeout=60000",
21823
+ "test:watch": "CI=1 bun test --watch",
21824
+ "test:unit": "CI=1 NAX_SKIP_PRECHECK=1 bun test ./test/unit/ --timeout=60000",
21825
+ "test:integration": "CI=1 NAX_SKIP_PRECHECK=1 bun test ./test/integration/ --timeout=60000",
21826
+ "test:ui": "CI=1 bun test ./test/ui/ --timeout=60000",
21827
+ "test:real": "NAX_SKIP_PRECHECK=1 bun test test/ --timeout=60000",
21135
21828
  "check-test-overlap": "bun run scripts/check-test-overlap.ts",
21136
21829
  "check-dead-tests": "bun run scripts/check-dead-tests.ts",
21137
21830
  "check:test-sizes": "bun run scripts/check-test-sizes.ts",
@@ -21179,8 +21872,8 @@ var init_version = __esm(() => {
21179
21872
  NAX_VERSION = package_default.version;
21180
21873
  NAX_COMMIT = (() => {
21181
21874
  try {
21182
- if (/^[0-9a-f]{6,10}$/.test("ba3f634"))
21183
- return "ba3f634";
21875
+ if (/^[0-9a-f]{6,10}$/.test("a59af3a"))
21876
+ return "a59af3a";
21184
21877
  } catch {}
21185
21878
  try {
21186
21879
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -21198,6 +21891,23 @@ var init_version = __esm(() => {
21198
21891
  NAX_BUILD_INFO = NAX_COMMIT === "dev" ? `v${NAX_VERSION}` : `v${NAX_VERSION} (${NAX_COMMIT})`;
21199
21892
  });
21200
21893
 
21894
+ // src/prd/validate.ts
21895
+ function validateStoryId(id) {
21896
+ if (!id || id.length === 0) {
21897
+ throw new Error("Story ID cannot be empty");
21898
+ }
21899
+ if (id.includes("..")) {
21900
+ throw new Error("Story ID cannot contain path traversal (..)");
21901
+ }
21902
+ if (id.startsWith("--")) {
21903
+ throw new Error("Story ID cannot start with git flags (--)");
21904
+ }
21905
+ const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
21906
+ if (!validPattern.test(id)) {
21907
+ throw new Error(`Story ID must match pattern [a-zA-Z0-9][a-zA-Z0-9._-]{0,63}. Got: ${id}`);
21908
+ }
21909
+ }
21910
+
21201
21911
  // src/errors.ts
21202
21912
  var NaxError, AgentNotFoundError, AgentNotInstalledError, StoryLimitExceededError, LockAcquisitionError;
21203
21913
  var init_errors3 = __esm(() => {
@@ -22206,7 +22916,7 @@ class WebhookInteractionPlugin {
22206
22916
  this.pendingResponses.delete(requestId);
22207
22917
  return response;
22208
22918
  }
22209
- await Bun.sleep(backoffMs);
22919
+ await _webhookPluginDeps.sleep(backoffMs);
22210
22920
  backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
22211
22921
  }
22212
22922
  return {
@@ -22305,9 +23015,12 @@ class WebhookInteractionPlugin {
22305
23015
  }
22306
23016
  }
22307
23017
  }
22308
- var WebhookConfigSchema, InteractionResponseSchema;
23018
+ var _webhookPluginDeps, WebhookConfigSchema, InteractionResponseSchema;
22309
23019
  var init_webhook = __esm(() => {
22310
23020
  init_zod();
23021
+ _webhookPluginDeps = {
23022
+ sleep: (ms) => Bun.sleep(ms)
23023
+ };
22311
23024
  WebhookConfigSchema = exports_external.object({
22312
23025
  url: exports_external.string().url().optional(),
22313
23026
  callbackPort: exports_external.number().int().min(1024).max(65535).optional(),
@@ -22346,8 +23059,8 @@ class AutoInteractionPlugin {
22346
23059
  return;
22347
23060
  }
22348
23061
  try {
22349
- if (_deps2.callLlm) {
22350
- const decision2 = await _deps2.callLlm(request);
23062
+ if (_deps3.callLlm) {
23063
+ const decision2 = await _deps3.callLlm(request);
22351
23064
  if (decision2.confidence < (this.config.confidenceThreshold ?? 0.7)) {
22352
23065
  return;
22353
23066
  }
@@ -22376,7 +23089,7 @@ class AutoInteractionPlugin {
22376
23089
  }
22377
23090
  async callLlm(request) {
22378
23091
  const prompt = this.buildPrompt(request);
22379
- const adapter = _deps2.adapter;
23092
+ const adapter = _deps3.adapter;
22380
23093
  if (!adapter) {
22381
23094
  throw new Error("Auto plugin requires adapter to be injected via _deps.adapter");
22382
23095
  }
@@ -22464,7 +23177,7 @@ Respond with ONLY this JSON (no markdown, no explanation):
22464
23177
  return parsed;
22465
23178
  }
22466
23179
  }
22467
- var AutoConfigSchema, _deps2;
23180
+ var AutoConfigSchema, _deps3;
22468
23181
  var init_auto = __esm(() => {
22469
23182
  init_zod();
22470
23183
  init_config();
@@ -22474,7 +23187,7 @@ var init_auto = __esm(() => {
22474
23187
  maxCostPerDecision: exports_external.number().positive().optional(),
22475
23188
  naxConfig: exports_external.any().optional()
22476
23189
  });
22477
- _deps2 = {
23190
+ _deps3 = {
22478
23191
  adapter: null,
22479
23192
  callLlm: null
22480
23193
  };
@@ -23155,7 +23868,7 @@ async function runReview(config2, workdir, executionConfig) {
23155
23868
  const logger = getSafeLogger();
23156
23869
  const checks3 = [];
23157
23870
  let firstFailure;
23158
- const allUncommittedFiles = await _deps3.getUncommittedFiles(workdir);
23871
+ const allUncommittedFiles = await _deps4.getUncommittedFiles(workdir);
23159
23872
  const NAX_RUNTIME_FILES = new Set(["nax/status.json", ".nax-verifier-verdict.json"]);
23160
23873
  const uncommittedFiles = allUncommittedFiles.filter((f) => !NAX_RUNTIME_FILES.has(f) && !f.match(/^nax\/features\/.+\/prd\.json$/));
23161
23874
  if (uncommittedFiles.length > 0) {
@@ -23195,11 +23908,11 @@ Stage and commit these files before running review.`
23195
23908
  failureReason: firstFailure
23196
23909
  };
23197
23910
  }
23198
- var _reviewRunnerDeps, REVIEW_CHECK_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS2 = 5000, _deps3;
23911
+ var _reviewRunnerDeps, REVIEW_CHECK_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS2 = 5000, _deps4;
23199
23912
  var init_runner2 = __esm(() => {
23200
23913
  init_logger2();
23201
23914
  _reviewRunnerDeps = { spawn, file: Bun.file };
23202
- _deps3 = {
23915
+ _deps4 = {
23203
23916
  getUncommittedFiles: getUncommittedFilesImpl
23204
23917
  };
23205
23918
  });
@@ -23460,10 +24173,10 @@ var init_autofix = __esm(() => {
23460
24173
 
23461
24174
  // src/execution/progress.ts
23462
24175
  import { mkdirSync as mkdirSync2 } from "fs";
23463
- import { join as join14 } from "path";
24176
+ import { join as join15 } from "path";
23464
24177
  async function appendProgress(featureDir, storyId, status, message) {
23465
24178
  mkdirSync2(featureDir, { recursive: true });
23466
- const progressPath = join14(featureDir, "progress.txt");
24179
+ const progressPath = join15(featureDir, "progress.txt");
23467
24180
  const timestamp = new Date().toISOString();
23468
24181
  const entry = `[${timestamp}] ${storyId} \u2014 ${status.toUpperCase()} \u2014 ${message}
23469
24182
  `;
@@ -23547,7 +24260,7 @@ function estimateTokens(text) {
23547
24260
 
23548
24261
  // src/constitution/loader.ts
23549
24262
  import { existsSync as existsSync13 } from "fs";
23550
- import { join as join15 } from "path";
24263
+ import { join as join16 } from "path";
23551
24264
  function truncateToTokens(text, maxTokens) {
23552
24265
  const maxChars = maxTokens * 3;
23553
24266
  if (text.length <= maxChars) {
@@ -23569,7 +24282,7 @@ async function loadConstitution(projectDir, config2) {
23569
24282
  }
23570
24283
  let combinedContent = "";
23571
24284
  if (!config2.skipGlobal) {
23572
- const globalPath = join15(globalConfigDir(), config2.path);
24285
+ const globalPath = join16(globalConfigDir(), config2.path);
23573
24286
  if (existsSync13(globalPath)) {
23574
24287
  const validatedPath = validateFilePath(globalPath, globalConfigDir());
23575
24288
  const globalFile = Bun.file(validatedPath);
@@ -23579,7 +24292,7 @@ async function loadConstitution(projectDir, config2) {
23579
24292
  }
23580
24293
  }
23581
24294
  }
23582
- const projectPath = join15(projectDir, config2.path);
24295
+ const projectPath = join16(projectDir, config2.path);
23583
24296
  if (existsSync13(projectPath)) {
23584
24297
  const validatedPath = validateFilePath(projectPath, projectDir);
23585
24298
  const projectFile = Bun.file(validatedPath);
@@ -24304,7 +25017,7 @@ async function addFileElements(elements, storyContext, story) {
24304
25017
  if (contextFiles.length === 0 && storyContext.config?.context?.autoDetect?.enabled !== false && storyContext.workdir) {
24305
25018
  const autoDetectConfig = storyContext.config?.context?.autoDetect;
24306
25019
  try {
24307
- const detected = await _deps4.autoDetectContextFiles({
25020
+ const detected = await _deps5.autoDetectContextFiles({
24308
25021
  workdir: storyContext.workdir,
24309
25022
  storyTitle: story.title,
24310
25023
  maxFiles: autoDetectConfig?.maxFiles ?? 5,
@@ -24362,7 +25075,7 @@ ${content}
24362
25075
  }
24363
25076
  }
24364
25077
  }
24365
- var _deps4;
25078
+ var _deps5;
24366
25079
  var init_builder3 = __esm(() => {
24367
25080
  init_logger2();
24368
25081
  init_prd();
@@ -24370,7 +25083,7 @@ var init_builder3 = __esm(() => {
24370
25083
  init_elements();
24371
25084
  init_test_scanner();
24372
25085
  init_elements();
24373
- _deps4 = {
25086
+ _deps5 = {
24374
25087
  autoDetectContextFiles
24375
25088
  };
24376
25089
  });
@@ -24605,6 +25318,15 @@ ${pluginMarkdown}` : pluginMarkdown;
24605
25318
  function validateAgentForTier(agent, tier) {
24606
25319
  return agent.capabilities.supportedTiers.includes(tier);
24607
25320
  }
25321
+ function validateAgentFeature(agent, feature) {
25322
+ return agent.capabilities.features.has(feature);
25323
+ }
25324
+ function describeAgentCapabilities(agent) {
25325
+ const tiers = agent.capabilities.supportedTiers.join(",");
25326
+ const features = Array.from(agent.capabilities.features).join(",");
25327
+ const maxTokens = agent.capabilities.maxContextTokens;
25328
+ return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
25329
+ }
24608
25330
 
24609
25331
  // src/agents/version-detection.ts
24610
25332
  async function getAgentVersion(binaryName) {
@@ -24655,6 +25377,26 @@ var init_version_detection = __esm(() => {
24655
25377
  });
24656
25378
 
24657
25379
  // src/agents/index.ts
25380
+ var exports_agents = {};
25381
+ __export(exports_agents, {
25382
+ validateAgentForTier: () => validateAgentForTier,
25383
+ validateAgentFeature: () => validateAgentFeature,
25384
+ parseTokenUsage: () => parseTokenUsage,
25385
+ getInstalledAgents: () => getInstalledAgents,
25386
+ getAllAgentNames: () => getAllAgentNames,
25387
+ getAgentVersions: () => getAgentVersions,
25388
+ getAgentVersion: () => getAgentVersion,
25389
+ getAgent: () => getAgent,
25390
+ formatCostWithConfidence: () => formatCostWithConfidence,
25391
+ estimateCostFromOutput: () => estimateCostFromOutput,
25392
+ estimateCostByDuration: () => estimateCostByDuration,
25393
+ estimateCost: () => estimateCost,
25394
+ describeAgentCapabilities: () => describeAgentCapabilities,
25395
+ checkAgentHealth: () => checkAgentHealth,
25396
+ CompleteError: () => CompleteError,
25397
+ ClaudeCodeAdapter: () => ClaudeCodeAdapter,
25398
+ COST_RATES: () => COST_RATES
25399
+ });
24658
25400
  var init_agents = __esm(() => {
24659
25401
  init_types2();
24660
25402
  init_claude();
@@ -24732,14 +25474,14 @@ var init_isolation = __esm(() => {
24732
25474
 
24733
25475
  // src/context/greenfield.ts
24734
25476
  import { readdir } from "fs/promises";
24735
- import { join as join16 } from "path";
25477
+ import { join as join17 } from "path";
24736
25478
  async function scanForTestFiles(dir, testPattern, isRootCall = true) {
24737
25479
  const results = [];
24738
25480
  const ignoreDirs = new Set(["node_modules", "dist", "build", ".next", ".git"]);
24739
25481
  try {
24740
25482
  const entries = await readdir(dir, { withFileTypes: true });
24741
25483
  for (const entry of entries) {
24742
- const fullPath = join16(dir, entry.name);
25484
+ const fullPath = join17(dir, entry.name);
24743
25485
  if (entry.isDirectory()) {
24744
25486
  if (ignoreDirs.has(entry.name))
24745
25487
  continue;
@@ -24823,6 +25565,30 @@ function detectMergeConflict(output) {
24823
25565
  async function autoCommitIfDirty(workdir, stage, role, storyId) {
24824
25566
  const logger = getSafeLogger();
24825
25567
  try {
25568
+ const topLevelProc = _gitDeps.spawn(["git", "rev-parse", "--show-toplevel"], {
25569
+ cwd: workdir,
25570
+ stdout: "pipe",
25571
+ stderr: "pipe"
25572
+ });
25573
+ const gitRoot = (await new Response(topLevelProc.stdout).text()).trim();
25574
+ await topLevelProc.exited;
25575
+ const { realpathSync: realpathSync3 } = await import("fs");
25576
+ const realWorkdir = (() => {
25577
+ try {
25578
+ return realpathSync3(workdir);
25579
+ } catch {
25580
+ return workdir;
25581
+ }
25582
+ })();
25583
+ const realGitRoot = (() => {
25584
+ try {
25585
+ return realpathSync3(gitRoot);
25586
+ } catch {
25587
+ return gitRoot;
25588
+ }
25589
+ })();
25590
+ if (realWorkdir !== realGitRoot)
25591
+ return;
24826
25592
  const statusProc = _gitDeps.spawn(["git", "status", "--porcelain"], {
24827
25593
  cwd: workdir,
24828
25594
  stdout: "pipe",
@@ -25127,13 +25893,13 @@ function parseTestOutput(output, exitCode) {
25127
25893
 
25128
25894
  // src/verification/runners.ts
25129
25895
  import { existsSync as existsSync14 } from "fs";
25130
- import { join as join17 } from "path";
25896
+ import { join as join18 } from "path";
25131
25897
  async function verifyAssets(workingDirectory, expectedFiles) {
25132
25898
  if (!expectedFiles || expectedFiles.length === 0)
25133
25899
  return { success: true, missingFiles: [] };
25134
25900
  const missingFiles = [];
25135
25901
  for (const file2 of expectedFiles) {
25136
- if (!existsSync14(join17(workingDirectory, file2)))
25902
+ if (!existsSync14(join18(workingDirectory, file2)))
25137
25903
  missingFiles.push(file2);
25138
25904
  }
25139
25905
  if (missingFiles.length > 0) {
@@ -25208,11 +25974,15 @@ async function fullSuite(options) {
25208
25974
  return runVerificationCore(options);
25209
25975
  }
25210
25976
  async function regression(options) {
25211
- await Bun.sleep(2000);
25977
+ await _regressionRunnerDeps.sleep(2000);
25212
25978
  return runVerificationCore({ ...options, expectedFiles: undefined });
25213
25979
  }
25980
+ var _regressionRunnerDeps;
25214
25981
  var init_runners = __esm(() => {
25215
25982
  init_executor();
25983
+ _regressionRunnerDeps = {
25984
+ sleep: (ms) => Bun.sleep(ms)
25985
+ };
25216
25986
  });
25217
25987
 
25218
25988
  // src/verification/rectification.ts
@@ -25345,7 +26115,7 @@ var init_prompts = __esm(() => {
25345
26115
  });
25346
26116
 
25347
26117
  // src/tdd/rectification-gate.ts
25348
- async function runFullSuiteGate(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger) {
26118
+ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, featureName) {
25349
26119
  const rectificationEnabled = config2.execution.rectification?.enabled ?? false;
25350
26120
  if (!rectificationEnabled)
25351
26121
  return false;
@@ -25361,7 +26131,7 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
25361
26131
  if (!fullSuitePassed && fullSuiteResult.output) {
25362
26132
  const testSummary = parseBunTestOutput(fullSuiteResult.output);
25363
26133
  if (testSummary.failed > 0) {
25364
- return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout);
26134
+ return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout, featureName);
25365
26135
  }
25366
26136
  if (testSummary.passed > 0) {
25367
26137
  logger.info("tdd", "Full suite gate passed (non-zero exit, 0 failures, tests detected)", {
@@ -25389,7 +26159,7 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
25389
26159
  });
25390
26160
  return false;
25391
26161
  }
25392
- async function runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout) {
26162
+ async function runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout, featureName) {
25393
26163
  const rectificationState = {
25394
26164
  attempt: 0,
25395
26165
  initialFailures: testSummary.failed,
@@ -25411,7 +26181,10 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
25411
26181
  modelTier: implementerTier,
25412
26182
  modelDef: resolveModel(config2.models[implementerTier]),
25413
26183
  timeoutSeconds: config2.execution.sessionTimeoutSeconds,
25414
- dangerouslySkipPermissions: config2.execution.dangerouslySkipPermissions
26184
+ dangerouslySkipPermissions: config2.execution.dangerouslySkipPermissions,
26185
+ featureName,
26186
+ storyId: story.id,
26187
+ sessionRole: "implementer"
25415
26188
  });
25416
26189
  if (!rectifyResult.success && rectifyResult.pid) {
25417
26190
  await cleanupProcessTree(rectifyResult.pid);
@@ -25808,13 +26581,13 @@ var exports_loader = {};
25808
26581
  __export(exports_loader, {
25809
26582
  loadOverride: () => loadOverride
25810
26583
  });
25811
- import { join as join18 } from "path";
26584
+ import { join as join19 } from "path";
25812
26585
  async function loadOverride(role, workdir, config2) {
25813
26586
  const overridePath = config2.prompts?.overrides?.[role];
25814
26587
  if (!overridePath) {
25815
26588
  return null;
25816
26589
  }
25817
- const absolutePath = join18(workdir, overridePath);
26590
+ const absolutePath = join19(workdir, overridePath);
25818
26591
  const file2 = Bun.file(absolutePath);
25819
26592
  if (!await file2.exists()) {
25820
26593
  return null;
@@ -25993,7 +26766,7 @@ async function rollbackToRef(workdir, ref) {
25993
26766
  }
25994
26767
  logger.info("tdd", "Successfully rolled back git changes", { ref });
25995
26768
  }
25996
- async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution) {
26769
+ async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution, featureName) {
25997
26770
  const startTime = Date.now();
25998
26771
  let prompt;
25999
26772
  switch (role) {
@@ -26015,7 +26788,10 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
26015
26788
  modelTier,
26016
26789
  modelDef: resolveModel(config2.models[modelTier]),
26017
26790
  timeoutSeconds: config2.execution.sessionTimeoutSeconds,
26018
- dangerouslySkipPermissions: config2.execution.dangerouslySkipPermissions
26791
+ dangerouslySkipPermissions: config2.execution.dangerouslySkipPermissions,
26792
+ featureName,
26793
+ storyId: story.id,
26794
+ sessionRole: role
26019
26795
  });
26020
26796
  if (!result.success && result.pid) {
26021
26797
  await cleanupProcessTree(result.pid);
@@ -26035,7 +26811,7 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
26035
26811
  exitCode: result.exitCode
26036
26812
  });
26037
26813
  }
26038
- await autoCommitIfDirty(workdir, "tdd", role, story.id);
26814
+ await _sessionRunnerDeps.autoCommitIfDirty(workdir, "tdd", role, story.id);
26039
26815
  let isolation;
26040
26816
  if (!skipIsolation) {
26041
26817
  if (role === "test-writer") {
@@ -26082,6 +26858,7 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
26082
26858
  estimatedCost: result.estimatedCost
26083
26859
  };
26084
26860
  }
26861
+ var _sessionRunnerDeps;
26085
26862
  var init_session_runner = __esm(() => {
26086
26863
  init_config();
26087
26864
  init_logger2();
@@ -26089,6 +26866,9 @@ var init_session_runner = __esm(() => {
26089
26866
  init_git();
26090
26867
  init_cleanup();
26091
26868
  init_isolation();
26869
+ _sessionRunnerDeps = {
26870
+ autoCommitIfDirty
26871
+ };
26092
26872
  });
26093
26873
 
26094
26874
  // src/tdd/verdict-reader.ts
@@ -26363,6 +27143,7 @@ async function runThreeSessionTdd(options) {
26363
27143
  config: config2,
26364
27144
  workdir,
26365
27145
  modelTier,
27146
+ featureName,
26366
27147
  contextMarkdown,
26367
27148
  constitution,
26368
27149
  dryRun = false,
@@ -26426,7 +27207,7 @@ async function runThreeSessionTdd(options) {
26426
27207
  let session1;
26427
27208
  if (!isRetry) {
26428
27209
  const testWriterTier = config2.tdd.sessionTiers?.testWriter ?? "balanced";
26429
- session1 = await runTddSession("test-writer", agent, story, config2, workdir, testWriterTier, session1Ref, contextMarkdown, lite, lite, constitution);
27210
+ session1 = await runTddSession("test-writer", agent, story, config2, workdir, testWriterTier, session1Ref, contextMarkdown, lite, lite, constitution, featureName);
26430
27211
  sessions.push(session1);
26431
27212
  }
26432
27213
  if (session1 && !session1.success) {
@@ -26488,7 +27269,7 @@ async function runThreeSessionTdd(options) {
26488
27269
  });
26489
27270
  const session2Ref = await captureGitRef(workdir) ?? "HEAD";
26490
27271
  const implementerTier = config2.tdd.sessionTiers?.implementer ?? modelTier;
26491
- const session2 = await runTddSession("implementer", agent, story, config2, workdir, implementerTier, session2Ref, contextMarkdown, lite, lite, constitution);
27272
+ const session2 = await runTddSession("implementer", agent, story, config2, workdir, implementerTier, session2Ref, contextMarkdown, lite, lite, constitution, featureName);
26492
27273
  sessions.push(session2);
26493
27274
  if (!session2.success) {
26494
27275
  needsHumanReview = true;
@@ -26504,10 +27285,10 @@ async function runThreeSessionTdd(options) {
26504
27285
  lite
26505
27286
  };
26506
27287
  }
26507
- const fullSuiteGatePassed = await runFullSuiteGate(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger);
27288
+ const fullSuiteGatePassed = await runFullSuiteGate(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, featureName);
26508
27289
  const session3Ref = await captureGitRef(workdir) ?? "HEAD";
26509
27290
  const verifierTier = config2.tdd.sessionTiers?.verifier ?? "fast";
26510
- const session3 = await runTddSession("verifier", agent, story, config2, workdir, verifierTier, session3Ref, undefined, false, false, constitution);
27291
+ const session3 = await runTddSession("verifier", agent, story, config2, workdir, verifierTier, session3Ref, undefined, false, false, constitution, featureName);
26511
27292
  sessions.push(session3);
26512
27293
  const verdict = await readVerdict(workdir);
26513
27294
  await cleanupVerdict(workdir);
@@ -26666,7 +27447,7 @@ var init_execution = __esm(() => {
26666
27447
  enabled: () => true,
26667
27448
  async execute(ctx) {
26668
27449
  const logger = getLogger();
26669
- const agent = _executionDeps.getAgent(ctx.config.autoMode.defaultAgent);
27450
+ const agent = (ctx.agentGetFn ?? _executionDeps.getAgent)(ctx.config.autoMode.defaultAgent);
26670
27451
  if (!agent) {
26671
27452
  return {
26672
27453
  action: "fail",
@@ -26686,6 +27467,7 @@ var init_execution = __esm(() => {
26686
27467
  config: ctx.config,
26687
27468
  workdir: ctx.workdir,
26688
27469
  modelTier: ctx.routing.modelTier,
27470
+ featureName: ctx.prd.feature,
26689
27471
  contextMarkdown: ctx.contextMarkdown,
26690
27472
  constitution: ctx.constitution?.content,
26691
27473
  dryRun: false,
@@ -26755,7 +27537,38 @@ Category: ${tddResult.failureCategory ?? "unknown"}`,
26755
27537
  modelTier: ctx.routing.modelTier,
26756
27538
  modelDef: resolveModel(ctx.config.models[ctx.routing.modelTier]),
26757
27539
  timeoutSeconds: ctx.config.execution.sessionTimeoutSeconds,
26758
- dangerouslySkipPermissions: ctx.config.execution.dangerouslySkipPermissions
27540
+ dangerouslySkipPermissions: ctx.config.execution.dangerouslySkipPermissions,
27541
+ pidRegistry: ctx.pidRegistry,
27542
+ featureName: ctx.prd.feature,
27543
+ storyId: ctx.story.id,
27544
+ interactionBridge: (() => {
27545
+ const plugin = ctx.interaction?.getPrimary();
27546
+ if (!plugin)
27547
+ return;
27548
+ const QUESTION_PATTERNS = [/\?/, /\bwhich\b/i, /\bshould i\b/i, /\bunclear\b/i, /\bplease clarify\b/i];
27549
+ return {
27550
+ detectQuestion: async (text) => QUESTION_PATTERNS.some((p) => p.test(text)),
27551
+ onQuestionDetected: async (text) => {
27552
+ const requestId = `ix-acp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
27553
+ await plugin.send({
27554
+ id: requestId,
27555
+ type: "input",
27556
+ featureName: ctx.prd.feature,
27557
+ storyId: ctx.story.id,
27558
+ stage: "execution",
27559
+ summary: text,
27560
+ fallback: "continue",
27561
+ createdAt: Date.now()
27562
+ });
27563
+ try {
27564
+ const response = await plugin.receive(requestId, 120000);
27565
+ return response.value ?? "continue";
27566
+ } catch {
27567
+ return "continue";
27568
+ }
27569
+ }
27570
+ };
27571
+ })()
26759
27572
  });
26760
27573
  ctx.agentResult = result;
26761
27574
  await autoCommitIfDirty(ctx.workdir, "execution", "single-session", ctx.story.id);
@@ -27971,16 +28784,20 @@ var init_regression2 = __esm(() => {
27971
28784
  });
27972
28785
 
27973
28786
  // src/pipeline/stages/routing.ts
27974
- async function runDecompose(story, prd, config2, _workdir) {
28787
+ async function runDecompose(story, prd, config2, _workdir, agentGetFn) {
27975
28788
  const naxDecompose = config2.decompose;
27976
28789
  const builderConfig = {
27977
28790
  maxSubStories: naxDecompose?.maxSubstories ?? 5,
27978
28791
  maxComplexity: naxDecompose?.maxSubstoryComplexity ?? "medium",
27979
28792
  maxRetries: naxDecompose?.maxRetries ?? 2
27980
28793
  };
28794
+ const agent = (agentGetFn ?? getAgent)(config2.autoMode.defaultAgent);
28795
+ if (!agent) {
28796
+ throw new Error(`[decompose] Agent "${config2.autoMode.defaultAgent}" not found \u2014 cannot decompose`);
28797
+ }
27981
28798
  const adapter = {
27982
- async decompose(_prompt) {
27983
- throw new Error("[decompose] No LLM adapter configured for story decomposition");
28799
+ async decompose(prompt) {
28800
+ return agent.complete(prompt, { jsonMode: true });
27984
28801
  }
27985
28802
  };
27986
28803
  return DecomposeBuilder.for(story).prd(prd).config(builderConfig).decompose(adapter);
@@ -28001,7 +28818,7 @@ var init_routing2 = __esm(() => {
28001
28818
  async execute(ctx) {
28002
28819
  const logger = getLogger();
28003
28820
  const agentName = ctx.config.execution?.agent ?? "claude";
28004
- const adapter = _routingDeps.getAgent(agentName);
28821
+ const adapter = (ctx.agentGetFn ?? _routingDeps.getAgent)(agentName);
28005
28822
  const hasExistingRouting = ctx.story.routing !== undefined;
28006
28823
  const hasContentHash = ctx.story.routing?.contentHash !== undefined;
28007
28824
  let currentHash;
@@ -28070,7 +28887,7 @@ var init_routing2 = __esm(() => {
28070
28887
  if (decomposeConfig.trigger === "disabled") {
28071
28888
  logger.warn("routing", `Story ${ctx.story.id} is oversized (${acCount} ACs) but decompose is disabled \u2014 continuing with original`);
28072
28889
  } else if (decomposeConfig.trigger === "auto") {
28073
- const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir);
28890
+ const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir, ctx.agentGetFn);
28074
28891
  if (result.validation.valid) {
28075
28892
  _routingDeps.applyDecomposition(ctx.prd, result);
28076
28893
  if (ctx.prdPath) {
@@ -28085,7 +28902,7 @@ var init_routing2 = __esm(() => {
28085
28902
  } else if (decomposeConfig.trigger === "confirm") {
28086
28903
  const action = await _routingDeps.checkStoryOversized({ featureName: ctx.prd.feature, storyId: ctx.story.id, criteriaCount: acCount }, ctx.config, ctx.interaction);
28087
28904
  if (action === "decompose") {
28088
- const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir);
28905
+ const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir, ctx.agentGetFn);
28089
28906
  if (result.validation.valid) {
28090
28907
  _routingDeps.applyDecomposition(ctx.prd, result);
28091
28908
  if (ctx.prdPath) {
@@ -28987,7 +29804,7 @@ var init_checks_config = () => {};
28987
29804
  async function checkAgentCLI(config2) {
28988
29805
  const agent = config2.execution?.agent || "claude";
28989
29806
  try {
28990
- const proc = _deps6.spawn([agent, "--version"], {
29807
+ const proc = _deps7.spawn([agent, "--version"], {
28991
29808
  stdout: "pipe",
28992
29809
  stderr: "pipe"
28993
29810
  });
@@ -29008,9 +29825,9 @@ async function checkAgentCLI(config2) {
29008
29825
  };
29009
29826
  }
29010
29827
  }
29011
- var _deps6;
29828
+ var _deps7;
29012
29829
  var init_checks_cli = __esm(() => {
29013
- _deps6 = {
29830
+ _deps7 = {
29014
29831
  spawn: Bun.spawn
29015
29832
  };
29016
29833
  });
@@ -29573,19 +30390,19 @@ var init_precheck = __esm(() => {
29573
30390
  });
29574
30391
 
29575
30392
  // src/hooks/runner.ts
29576
- import { join as join36 } from "path";
30393
+ import { join as join37 } from "path";
29577
30394
  async function loadHooksConfig(projectDir, globalDir) {
29578
30395
  let globalHooks = { hooks: {} };
29579
30396
  let projectHooks = { hooks: {} };
29580
30397
  let skipGlobal = false;
29581
- const projectPath = join36(projectDir, "hooks.json");
30398
+ const projectPath = join37(projectDir, "hooks.json");
29582
30399
  const projectData = await loadJsonFile(projectPath, "hooks");
29583
30400
  if (projectData) {
29584
30401
  projectHooks = projectData;
29585
30402
  skipGlobal = projectData.skipGlobal ?? false;
29586
30403
  }
29587
30404
  if (!skipGlobal && globalDir) {
29588
- const globalPath = join36(globalDir, "hooks.json");
30405
+ const globalPath = join37(globalDir, "hooks.json");
29589
30406
  const globalData = await loadJsonFile(globalPath, "hooks");
29590
30407
  if (globalData) {
29591
30408
  globalHooks = globalData;
@@ -30026,7 +30843,8 @@ function buildResult(success2, prd, totalCost, iterations, storiesCompleted, prd
30026
30843
  }
30027
30844
  async function generateAndAddFixStories(ctx, failures, prd) {
30028
30845
  const logger = getSafeLogger();
30029
- const agent = getAgent(ctx.config.autoMode.defaultAgent);
30846
+ const { getAgent: getAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
30847
+ const agent = (ctx.agentGetFn ?? getAgent2)(ctx.config.autoMode.defaultAgent);
30030
30848
  if (!agent) {
30031
30849
  logger?.error("acceptance", "Agent not found, cannot generate fix stories");
30032
30850
  return null;
@@ -30172,7 +30990,6 @@ async function runAcceptanceLoop(ctx) {
30172
30990
  }
30173
30991
  var init_acceptance_loop = __esm(() => {
30174
30992
  init_acceptance();
30175
- init_agents();
30176
30993
  init_schema();
30177
30994
  init_hooks();
30178
30995
  init_logger2();
@@ -30599,35 +31416,18 @@ var init_headless_formatter = __esm(() => {
30599
31416
  init_version();
30600
31417
  });
30601
31418
 
30602
- // src/prd/validate.ts
30603
- function validateStoryId(id) {
30604
- if (!id || id.length === 0) {
30605
- throw new Error("Story ID cannot be empty");
30606
- }
30607
- if (id.includes("..")) {
30608
- throw new Error("Story ID cannot contain path traversal (..)");
30609
- }
30610
- if (id.startsWith("--")) {
30611
- throw new Error("Story ID cannot start with git flags (--)");
30612
- }
30613
- const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
30614
- if (!validPattern.test(id)) {
30615
- throw new Error(`Story ID must match pattern [a-zA-Z0-9][a-zA-Z0-9._-]{0,63}. Got: ${id}`);
30616
- }
30617
- }
30618
-
30619
31419
  // src/worktree/manager.ts
30620
31420
  var exports_manager = {};
30621
31421
  __export(exports_manager, {
30622
31422
  WorktreeManager: () => WorktreeManager
30623
31423
  });
30624
31424
  import { existsSync as existsSync30, symlinkSync } from "fs";
30625
- import { join as join37 } from "path";
31425
+ import { join as join38 } from "path";
30626
31426
 
30627
31427
  class WorktreeManager {
30628
31428
  async create(projectRoot, storyId) {
30629
31429
  validateStoryId(storyId);
30630
- const worktreePath = join37(projectRoot, ".nax-wt", storyId);
31430
+ const worktreePath = join38(projectRoot, ".nax-wt", storyId);
30631
31431
  const branchName = `nax/${storyId}`;
30632
31432
  try {
30633
31433
  const proc = Bun.spawn(["git", "worktree", "add", worktreePath, "-b", branchName], {
@@ -30652,9 +31452,9 @@ class WorktreeManager {
30652
31452
  }
30653
31453
  throw new Error(`Failed to create worktree: ${String(error48)}`);
30654
31454
  }
30655
- const nodeModulesSource = join37(projectRoot, "node_modules");
31455
+ const nodeModulesSource = join38(projectRoot, "node_modules");
30656
31456
  if (existsSync30(nodeModulesSource)) {
30657
- const nodeModulesTarget = join37(worktreePath, "node_modules");
31457
+ const nodeModulesTarget = join38(worktreePath, "node_modules");
30658
31458
  try {
30659
31459
  symlinkSync(nodeModulesSource, nodeModulesTarget, "dir");
30660
31460
  } catch (error48) {
@@ -30662,9 +31462,9 @@ class WorktreeManager {
30662
31462
  throw new Error(`Failed to symlink node_modules: ${errorMessage(error48)}`);
30663
31463
  }
30664
31464
  }
30665
- const envSource = join37(projectRoot, ".env");
31465
+ const envSource = join38(projectRoot, ".env");
30666
31466
  if (existsSync30(envSource)) {
30667
- const envTarget = join37(worktreePath, ".env");
31467
+ const envTarget = join38(worktreePath, ".env");
30668
31468
  try {
30669
31469
  symlinkSync(envSource, envTarget, "file");
30670
31470
  } catch (error48) {
@@ -30675,7 +31475,7 @@ class WorktreeManager {
30675
31475
  }
30676
31476
  async remove(projectRoot, storyId) {
30677
31477
  validateStoryId(storyId);
30678
- const worktreePath = join37(projectRoot, ".nax-wt", storyId);
31478
+ const worktreePath = join38(projectRoot, ".nax-wt", storyId);
30679
31479
  const branchName = `nax/${storyId}`;
30680
31480
  try {
30681
31481
  const proc = Bun.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
@@ -31065,7 +31865,7 @@ var init_parallel_worker = __esm(() => {
31065
31865
 
31066
31866
  // src/execution/parallel-coordinator.ts
31067
31867
  import os3 from "os";
31068
- import { join as join38 } from "path";
31868
+ import { join as join39 } from "path";
31069
31869
  function groupStoriesByDependencies(stories) {
31070
31870
  const batches = [];
31071
31871
  const processed = new Set;
@@ -31142,7 +31942,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
31142
31942
  };
31143
31943
  const worktreePaths = new Map;
31144
31944
  for (const story of batch) {
31145
- const worktreePath = join38(projectRoot, ".nax-wt", story.id);
31945
+ const worktreePath = join39(projectRoot, ".nax-wt", story.id);
31146
31946
  try {
31147
31947
  await worktreeManager.create(projectRoot, story.id);
31148
31948
  worktreePaths.set(story.id, worktreePath);
@@ -31191,7 +31991,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
31191
31991
  });
31192
31992
  logger?.warn("parallel", "Worktree preserved for manual conflict resolution", {
31193
31993
  storyId: mergeResult.storyId,
31194
- worktreePath: join38(projectRoot, ".nax-wt", mergeResult.storyId)
31994
+ worktreePath: join39(projectRoot, ".nax-wt", mergeResult.storyId)
31195
31995
  });
31196
31996
  }
31197
31997
  }
@@ -31650,12 +32450,12 @@ var init_parallel_executor = __esm(() => {
31650
32450
  // src/pipeline/subscribers/events-writer.ts
31651
32451
  import { appendFile as appendFile2, mkdir } from "fs/promises";
31652
32452
  import { homedir as homedir5 } from "os";
31653
- import { basename as basename3, join as join39 } from "path";
32453
+ import { basename as basename3, join as join40 } from "path";
31654
32454
  function wireEventsWriter(bus, feature, runId, workdir) {
31655
32455
  const logger = getSafeLogger();
31656
32456
  const project = basename3(workdir);
31657
- const eventsDir = join39(homedir5(), ".nax", "events", project);
31658
- const eventsFile = join39(eventsDir, "events.jsonl");
32457
+ const eventsDir = join40(homedir5(), ".nax", "events", project);
32458
+ const eventsFile = join40(eventsDir, "events.jsonl");
31659
32459
  let dirReady = false;
31660
32460
  const write = (line) => {
31661
32461
  (async () => {
@@ -31815,12 +32615,12 @@ var init_interaction2 = __esm(() => {
31815
32615
  // src/pipeline/subscribers/registry.ts
31816
32616
  import { mkdir as mkdir2, writeFile } from "fs/promises";
31817
32617
  import { homedir as homedir6 } from "os";
31818
- import { basename as basename4, join as join40 } from "path";
32618
+ import { basename as basename4, join as join41 } from "path";
31819
32619
  function wireRegistry(bus, feature, runId, workdir) {
31820
32620
  const logger = getSafeLogger();
31821
32621
  const project = basename4(workdir);
31822
- const runDir = join40(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
31823
- const metaFile = join40(runDir, "meta.json");
32622
+ const runDir = join41(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
32623
+ const metaFile = join41(runDir, "meta.json");
31824
32624
  const unsub = bus.on("run:started", (_ev) => {
31825
32625
  (async () => {
31826
32626
  try {
@@ -31830,8 +32630,8 @@ function wireRegistry(bus, feature, runId, workdir) {
31830
32630
  project,
31831
32631
  feature,
31832
32632
  workdir,
31833
- statusPath: join40(workdir, "nax", "features", feature, "status.json"),
31834
- eventsDir: join40(workdir, "nax", "features", feature, "runs"),
32633
+ statusPath: join41(workdir, "nax", "features", feature, "status.json"),
32634
+ eventsDir: join41(workdir, "nax", "features", feature, "runs"),
31835
32635
  registeredAt: new Date().toISOString()
31836
32636
  };
31837
32637
  await writeFile(metaFile, JSON.stringify(meta3, null, 2));
@@ -32499,6 +33299,8 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
32499
33299
  storyStartTime: new Date().toISOString(),
32500
33300
  storyGitRef: storyGitRef ?? undefined,
32501
33301
  interaction: ctx.interactionChain ?? undefined,
33302
+ agentGetFn: ctx.agentGetFn,
33303
+ pidRegistry: ctx.pidRegistry,
32502
33304
  accumulatedAttemptCost: accumulatedAttemptCost > 0 ? accumulatedAttemptCost : undefined
32503
33305
  };
32504
33306
  ctx.statusWriter.setPrd(prd);
@@ -32825,7 +33627,7 @@ async function writeStatusFile(filePath, status) {
32825
33627
  var init_status_file = () => {};
32826
33628
 
32827
33629
  // src/execution/status-writer.ts
32828
- import { join as join41 } from "path";
33630
+ import { join as join42 } from "path";
32829
33631
 
32830
33632
  class StatusWriter {
32831
33633
  statusFile;
@@ -32893,7 +33695,7 @@ class StatusWriter {
32893
33695
  if (!this._prd)
32894
33696
  return;
32895
33697
  const safeLogger = getSafeLogger();
32896
- const featureStatusPath = join41(featureDir, "status.json");
33698
+ const featureStatusPath = join42(featureDir, "status.json");
32897
33699
  try {
32898
33700
  const base = this.getSnapshot(totalCost, iterations);
32899
33701
  if (!base) {
@@ -33097,6 +33899,7 @@ var init_precheck_runner = __esm(() => {
33097
33899
  // src/execution/lifecycle/run-initialization.ts
33098
33900
  var exports_run_initialization = {};
33099
33901
  __export(exports_run_initialization, {
33902
+ logActiveProtocol: () => logActiveProtocol,
33100
33903
  initializeRun: () => initializeRun
33101
33904
  });
33102
33905
  async function reconcileState(prd, prdPath, workdir) {
@@ -33123,11 +33926,12 @@ async function reconcileState(prd, prdPath, workdir) {
33123
33926
  }
33124
33927
  return prd;
33125
33928
  }
33126
- async function checkAgentInstalled(config2, dryRun) {
33929
+ async function checkAgentInstalled(config2, dryRun, agentGetFn) {
33127
33930
  if (dryRun)
33128
33931
  return;
33129
33932
  const logger = getSafeLogger();
33130
- const agent = getAgent(config2.autoMode.defaultAgent);
33933
+ const { getAgent: getAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
33934
+ const agent = (agentGetFn ?? getAgent2)(config2.autoMode.defaultAgent);
33131
33935
  if (!agent) {
33132
33936
  logger?.error("execution", "Agent not found", {
33133
33937
  agent: config2.autoMode.defaultAgent
@@ -33155,9 +33959,14 @@ function validateStoryCount(counts, config2) {
33155
33959
  throw new StoryLimitExceededError(counts.total, config2.execution.maxStoriesPerFeature);
33156
33960
  }
33157
33961
  }
33962
+ function logActiveProtocol(config2) {
33963
+ const logger = getSafeLogger();
33964
+ const protocol = config2.agent?.protocol ?? "cli";
33965
+ logger?.info("run-initialization", `Agent protocol: ${protocol}`, { protocol });
33966
+ }
33158
33967
  async function initializeRun(ctx) {
33159
33968
  const logger = getSafeLogger();
33160
- await checkAgentInstalled(ctx.config, ctx.dryRun);
33969
+ await checkAgentInstalled(ctx.config, ctx.dryRun, ctx.agentGetFn);
33161
33970
  let prd = await loadPRD(ctx.prdPath);
33162
33971
  prd = await reconcileState(prd, ctx.prdPath, ctx.workdir);
33163
33972
  const counts = countStories(prd);
@@ -33170,7 +33979,6 @@ async function initializeRun(ctx) {
33170
33979
  return { prd, storyCounts: counts };
33171
33980
  }
33172
33981
  var init_run_initialization = __esm(() => {
33173
- init_agents();
33174
33982
  init_errors3();
33175
33983
  init_logger2();
33176
33984
  init_prd();
@@ -33279,7 +34087,8 @@ async function setupRun(options) {
33279
34087
  config: config2,
33280
34088
  prdPath,
33281
34089
  workdir,
33282
- dryRun
34090
+ dryRun,
34091
+ agentGetFn: options.agentGetFn
33283
34092
  });
33284
34093
  prd = initResult.prd;
33285
34094
  const counts = initResult.storyCounts;
@@ -64209,7 +65018,7 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
64209
65018
  init_source();
64210
65019
  import { existsSync as existsSync32, mkdirSync as mkdirSync6 } from "fs";
64211
65020
  import { homedir as homedir8 } from "os";
64212
- import { join as join42 } from "path";
65021
+ import { join as join43 } from "path";
64213
65022
 
64214
65023
  // node_modules/commander/esm.mjs
64215
65024
  var import__ = __toESM(require_commander(), 1);
@@ -64231,14 +65040,14 @@ var {
64231
65040
  init_acceptance();
64232
65041
  init_registry();
64233
65042
  import { existsSync as existsSync8 } from "fs";
64234
- import { join as join8 } from "path";
65043
+ import { join as join9 } from "path";
64235
65044
 
64236
65045
  // src/analyze/scanner.ts
64237
- import { existsSync as existsSync2 } from "fs";
64238
- import { join as join3 } from "path";
65046
+ import { existsSync as existsSync2, readdirSync } from "fs";
65047
+ import { join as join4 } from "path";
64239
65048
  async function scanCodebase(workdir) {
64240
- const srcPath = join3(workdir, "src");
64241
- const packageJsonPath = join3(workdir, "package.json");
65049
+ const srcPath = join4(workdir, "src");
65050
+ const packageJsonPath = join4(workdir, "package.json");
64242
65051
  const fileTree = existsSync2(srcPath) ? await generateFileTree(srcPath, 3) : "No src/ directory";
64243
65052
  let dependencies = {};
64244
65053
  let devDependencies = {};
@@ -64263,30 +65072,23 @@ async function generateFileTree(dir, maxDepth, currentDepth = 0, prefix = "") {
64263
65072
  }
64264
65073
  const entries = [];
64265
65074
  try {
64266
- const dirEntries = Array.from(new Bun.Glob("*").scanSync({
64267
- cwd: dir,
64268
- onlyFiles: false
64269
- }));
65075
+ const dirEntries = readdirSync(dir, { withFileTypes: true });
64270
65076
  dirEntries.sort((a, b) => {
64271
- const aIsDir = !a.includes(".");
64272
- const bIsDir = !b.includes(".");
64273
- if (aIsDir && !bIsDir)
65077
+ if (a.isDirectory() && !b.isDirectory())
64274
65078
  return -1;
64275
- if (!aIsDir && bIsDir)
65079
+ if (!a.isDirectory() && b.isDirectory())
64276
65080
  return 1;
64277
- return a.localeCompare(b);
65081
+ return a.name.localeCompare(b.name);
64278
65082
  });
64279
65083
  for (let i = 0;i < dirEntries.length; i++) {
64280
- const entry = dirEntries[i];
64281
- const fullPath = join3(dir, entry);
65084
+ const dirent = dirEntries[i];
64282
65085
  const isLast = i === dirEntries.length - 1;
64283
65086
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
64284
65087
  const childPrefix = isLast ? " " : "\u2502 ";
64285
- const stat = await Bun.file(fullPath).stat();
64286
- const isDir = stat.isDirectory();
64287
- entries.push(`${prefix}${connector}${entry}${isDir ? "/" : ""}`);
65088
+ const isDir = dirent.isDirectory();
65089
+ entries.push(`${prefix}${connector}${dirent.name}${isDir ? "/" : ""}`);
64288
65090
  if (isDir) {
64289
- const subtree = await generateFileTree(fullPath, maxDepth, currentDepth + 1, prefix + childPrefix);
65091
+ const subtree = await generateFileTree(join4(dir, dirent.name), maxDepth, currentDepth + 1, prefix + childPrefix);
64290
65092
  if (subtree) {
64291
65093
  entries.push(subtree);
64292
65094
  }
@@ -64310,16 +65112,16 @@ function detectTestPatterns(workdir, dependencies, devDependencies) {
64310
65112
  } else {
64311
65113
  patterns.push("Test framework: likely bun:test (no framework dependency)");
64312
65114
  }
64313
- if (existsSync2(join3(workdir, "test"))) {
65115
+ if (existsSync2(join4(workdir, "test"))) {
64314
65116
  patterns.push("Test directory: test/");
64315
65117
  }
64316
- if (existsSync2(join3(workdir, "__tests__"))) {
65118
+ if (existsSync2(join4(workdir, "__tests__"))) {
64317
65119
  patterns.push("Test directory: __tests__/");
64318
65120
  }
64319
- if (existsSync2(join3(workdir, "tests"))) {
65121
+ if (existsSync2(join4(workdir, "tests"))) {
64320
65122
  patterns.push("Test directory: tests/");
64321
65123
  }
64322
- const hasTestFiles = existsSync2(join3(workdir, "test")) || existsSync2(join3(workdir, "src"));
65124
+ const hasTestFiles = existsSync2(join4(workdir, "test")) || existsSync2(join4(workdir, "src"));
64323
65125
  if (hasTestFiles) {
64324
65126
  patterns.push("Test files: *.test.ts, *.spec.ts");
64325
65127
  }
@@ -64337,7 +65139,7 @@ init_version();
64337
65139
  // src/cli/analyze-parser.ts
64338
65140
  init_registry();
64339
65141
  import { existsSync as existsSync7 } from "fs";
64340
- import { join as join7 } from "path";
65142
+ import { join as join8 } from "path";
64341
65143
  init_schema();
64342
65144
  init_logger2();
64343
65145
  init_prd();
@@ -64467,7 +65269,7 @@ function estimateLOCFromComplexity(complexity) {
64467
65269
  }
64468
65270
  }
64469
65271
  async function reclassifyExistingPRD(featureDir, featureName, branchName, workdir, config2) {
64470
- const prdPath = join7(featureDir, "prd.json");
65272
+ const prdPath = join8(featureDir, "prd.json");
64471
65273
  if (!existsSync7(prdPath)) {
64472
65274
  throw new Error(`prd.json not found at ${prdPath}. Run analyze without --reclassify first.`);
64473
65275
  }
@@ -64568,11 +65370,11 @@ function reclassifyWithKeywords(story, config2) {
64568
65370
  // src/cli/analyze.ts
64569
65371
  async function analyzeFeature(options) {
64570
65372
  const { featureDir, featureName, branchName, config: config2, specPath: explicitSpecPath, reclassify = false } = options;
64571
- const workdir = join8(featureDir, "../..");
65373
+ const workdir = join9(featureDir, "../..");
64572
65374
  if (reclassify) {
64573
65375
  return await reclassifyExistingPRD(featureDir, featureName, branchName, workdir, config2);
64574
65376
  }
64575
- const specPath = explicitSpecPath || join8(featureDir, "spec.md");
65377
+ const specPath = explicitSpecPath || join9(featureDir, "spec.md");
64576
65378
  if (!existsSync8(specPath))
64577
65379
  throw new Error(`spec.md not found at ${specPath}`);
64578
65380
  const specContent = await Bun.file(specPath).text();
@@ -64689,7 +65491,7 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
64689
65491
  modelDef,
64690
65492
  config: config2
64691
65493
  });
64692
- const acceptanceTestPath = join8(featureDir, config2.acceptance.testPath);
65494
+ const acceptanceTestPath = join9(featureDir, config2.acceptance.testPath);
64693
65495
  await Bun.write(acceptanceTestPath, result.testCode);
64694
65496
  logger.info("cli", "[OK] Acceptance tests generated", {
64695
65497
  criteriaCount: result.criteria.length,
@@ -64700,77 +65502,243 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
64700
65502
  }
64701
65503
  }
64702
65504
  // src/cli/plan.ts
64703
- init_claude();
65505
+ init_registry();
64704
65506
  import { existsSync as existsSync9 } from "fs";
64705
- import { join as join9 } from "path";
64706
- init_schema();
65507
+ import { join as join10 } from "path";
64707
65508
  init_logger2();
64708
- var SPEC_TEMPLATE = `# Feature: [title]
64709
-
64710
- ## Problem
64711
- Why this is needed.
64712
-
64713
- ## Requirements
64714
- - REQ-1: ...
64715
- - REQ-2: ...
64716
-
64717
- ## Acceptance Criteria
64718
- - AC-1: ...
64719
65509
 
64720
- ## Technical Notes
64721
- Architecture hints, constraints, dependencies.
65510
+ // src/prd/schema.ts
65511
+ var VALID_COMPLEXITY = ["simple", "medium", "complex", "expert"];
65512
+ var VALID_TEST_STRATEGIES = [
65513
+ "test-after",
65514
+ "tdd-simple",
65515
+ "three-session-tdd",
65516
+ "three-session-tdd-lite"
65517
+ ];
65518
+ var STORY_ID_NO_SEPARATOR = /^([A-Za-z]+)(\d+)$/;
65519
+ function extractJsonFromMarkdown(text) {
65520
+ const match = text.match(/```(?:json)?\s*\n([\s\S]*?)\n?\s*```/);
65521
+ if (match) {
65522
+ return match[1] ?? text;
65523
+ }
65524
+ return text;
65525
+ }
65526
+ function stripTrailingCommas(text) {
65527
+ return text.replace(/,\s*([}\]])/g, "$1");
65528
+ }
65529
+ function normalizeStoryId(id) {
65530
+ const match = id.match(STORY_ID_NO_SEPARATOR);
65531
+ if (match) {
65532
+ return `${match[1]}-${match[2]}`;
65533
+ }
65534
+ return id;
65535
+ }
65536
+ function normalizeComplexity2(raw) {
65537
+ const lower = raw.toLowerCase();
65538
+ if (VALID_COMPLEXITY.includes(lower)) {
65539
+ return lower;
65540
+ }
65541
+ return null;
65542
+ }
65543
+ function validateStory(raw, index, allIds) {
65544
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
65545
+ throw new Error(`[schema] story[${index}] must be an object`);
65546
+ }
65547
+ const s = raw;
65548
+ const rawId = s.id;
65549
+ if (rawId === undefined || rawId === null || rawId === "") {
65550
+ throw new Error(`[schema] story[${index}].id is required and must be non-empty`);
65551
+ }
65552
+ if (typeof rawId !== "string") {
65553
+ throw new Error(`[schema] story[${index}].id must be a string`);
65554
+ }
65555
+ const id = normalizeStoryId(rawId);
65556
+ validateStoryId(id);
65557
+ const title = s.title;
65558
+ if (!title || typeof title !== "string" || title.trim() === "") {
65559
+ throw new Error(`[schema] story[${index}].title is required and must be non-empty`);
65560
+ }
65561
+ const description = s.description;
65562
+ if (!description || typeof description !== "string" || description.trim() === "") {
65563
+ throw new Error(`[schema] story[${index}].description is required and must be non-empty`);
65564
+ }
65565
+ const ac = s.acceptanceCriteria;
65566
+ if (!Array.isArray(ac) || ac.length === 0) {
65567
+ throw new Error(`[schema] story[${index}].acceptanceCriteria is required and must be a non-empty array`);
65568
+ }
65569
+ for (let i = 0;i < ac.length; i++) {
65570
+ if (typeof ac[i] !== "string") {
65571
+ throw new Error(`[schema] story[${index}].acceptanceCriteria[${i}] must be a string`);
65572
+ }
65573
+ }
65574
+ const routing = typeof s.routing === "object" && s.routing !== null ? s.routing : {};
65575
+ const rawComplexity = routing.complexity ?? s.complexity;
65576
+ if (rawComplexity === undefined || rawComplexity === null) {
65577
+ throw new Error(`[schema] story[${index}] missing complexity. Set routing.complexity to one of: ${VALID_COMPLEXITY.join(", ")}`);
65578
+ }
65579
+ if (typeof rawComplexity !== "string") {
65580
+ throw new Error(`[schema] story[${index}].routing.complexity must be a string`);
65581
+ }
65582
+ const complexity = normalizeComplexity2(rawComplexity);
65583
+ if (complexity === null) {
65584
+ throw new Error(`[schema] story[${index}].routing.complexity "${rawComplexity}" is invalid. Valid values: ${VALID_COMPLEXITY.join(", ")}`);
65585
+ }
65586
+ const rawTestStrategy = routing.testStrategy ?? s.testStrategy;
65587
+ const testStrategy = rawTestStrategy !== undefined && VALID_TEST_STRATEGIES.includes(rawTestStrategy) ? rawTestStrategy : "tdd-simple";
65588
+ const rawDeps = s.dependencies;
65589
+ const dependencies = Array.isArray(rawDeps) ? rawDeps : [];
65590
+ for (const dep of dependencies) {
65591
+ if (!allIds.has(dep)) {
65592
+ throw new Error(`[schema] story[${index}].dependencies references unknown story ID "${dep}"`);
65593
+ }
65594
+ }
65595
+ const rawTags = s.tags;
65596
+ const tags = Array.isArray(rawTags) ? rawTags : [];
65597
+ return {
65598
+ id,
65599
+ title: title.trim(),
65600
+ description: description.trim(),
65601
+ acceptanceCriteria: ac,
65602
+ tags,
65603
+ dependencies,
65604
+ status: "pending",
65605
+ passes: false,
65606
+ attempts: 0,
65607
+ escalations: [],
65608
+ routing: {
65609
+ complexity,
65610
+ testStrategy,
65611
+ reasoning: "validated from LLM output"
65612
+ }
65613
+ };
65614
+ }
65615
+ function parseRawString(text) {
65616
+ const extracted = extractJsonFromMarkdown(text);
65617
+ const cleaned = stripTrailingCommas(extracted);
65618
+ try {
65619
+ return JSON.parse(cleaned);
65620
+ } catch (err) {
65621
+ const parseErr = err;
65622
+ throw new Error(`[schema] Failed to parse JSON: ${parseErr.message}`, { cause: parseErr });
65623
+ }
65624
+ }
65625
+ function validatePlanOutput(raw, feature, branch) {
65626
+ const parsed = typeof raw === "string" ? parseRawString(raw) : raw;
65627
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
65628
+ throw new Error("[schema] PRD output must be a JSON object");
65629
+ }
65630
+ const obj = parsed;
65631
+ const rawStories = obj.userStories;
65632
+ if (!Array.isArray(rawStories) || rawStories.length === 0) {
65633
+ throw new Error("[schema] userStories is required and must be a non-empty array");
65634
+ }
65635
+ const allIds = new Set;
65636
+ for (const story of rawStories) {
65637
+ if (typeof story === "object" && story !== null && !Array.isArray(story)) {
65638
+ const s = story;
65639
+ const rawId = s.id;
65640
+ if (typeof rawId === "string" && rawId !== "") {
65641
+ allIds.add(normalizeStoryId(rawId));
65642
+ }
65643
+ }
65644
+ }
65645
+ const userStories = rawStories.map((story, index) => validateStory(story, index, allIds));
65646
+ const now = new Date().toISOString();
65647
+ return {
65648
+ project: typeof obj.project === "string" && obj.project !== "" ? obj.project : feature,
65649
+ feature,
65650
+ branchName: branch,
65651
+ createdAt: typeof obj.createdAt === "string" ? obj.createdAt : now,
65652
+ updatedAt: now,
65653
+ userStories
65654
+ };
65655
+ }
64722
65656
 
64723
- ## Out of Scope
64724
- What this does NOT include.
64725
- `;
64726
- async function planCommand(prompt, workdir, config2, options = {}) {
64727
- const interactive = options.interactive !== false;
64728
- const ngentDir = join9(workdir, "nax");
64729
- const outputPath = join9(ngentDir, config2.plan.outputPath);
64730
- if (!existsSync9(ngentDir)) {
65657
+ // src/cli/plan.ts
65658
+ var _deps2 = {
65659
+ readFile: (path) => Bun.file(path).text(),
65660
+ writeFile: (path, content) => Bun.write(path, content).then(() => {}),
65661
+ scanCodebase: (workdir) => scanCodebase(workdir),
65662
+ getAgent: (name) => getAgent(name),
65663
+ readPackageJson: (workdir) => Bun.file(join10(workdir, "package.json")).json().catch(() => null),
65664
+ spawnSync: (cmd, opts) => {
65665
+ const result = Bun.spawnSync(cmd, opts ? { cwd: opts.cwd } : {});
65666
+ return { stdout: result.stdout, exitCode: result.exitCode };
65667
+ },
65668
+ mkdirp: (path) => Bun.spawn(["mkdir", "-p", path]).exited.then(() => {})
65669
+ };
65670
+ async function planCommand(workdir, config2, options) {
65671
+ const naxDir = join10(workdir, "nax");
65672
+ if (!existsSync9(naxDir)) {
64731
65673
  throw new Error(`nax directory not found. Run 'nax init' first in ${workdir}`);
64732
65674
  }
64733
65675
  const logger = getLogger();
64734
- logger.info("cli", "Scanning codebase...");
64735
- const scan = await scanCodebase(workdir);
65676
+ logger?.info("plan", "Reading spec", { from: options.from });
65677
+ const specContent = await _deps2.readFile(options.from);
65678
+ logger?.info("plan", "Scanning codebase...");
65679
+ const scan = await _deps2.scanCodebase(workdir);
64736
65680
  const codebaseContext = buildCodebaseContext2(scan);
64737
- const modelTier = config2.plan.model;
64738
- const modelEntry = config2.models[modelTier];
64739
- const modelDef = resolveModel(modelEntry);
64740
- const fullPrompt = buildPlanPrompt(prompt, SPEC_TEMPLATE);
64741
- const planOptions = {
64742
- prompt: fullPrompt,
64743
- workdir,
64744
- interactive,
64745
- codebaseContext,
64746
- inputFile: options.from,
64747
- modelTier,
64748
- modelDef,
64749
- config: config2
64750
- };
64751
- const adapter = new ClaudeCodeAdapter;
64752
- logger.info("cli", interactive ? "Starting interactive planning session..." : `Reading from ${options.from}...`, {
64753
- interactive,
64754
- from: options.from
64755
- });
64756
- const result = await adapter.plan(planOptions);
64757
- if (interactive) {
64758
- if (result.specContent) {
64759
- await Bun.write(outputPath, result.specContent);
64760
- } else {
64761
- if (!existsSync9(outputPath)) {
64762
- throw new Error(`Interactive planning completed but spec not found at ${outputPath}`);
64763
- }
64764
- }
65681
+ const pkg = await _deps2.readPackageJson(workdir);
65682
+ const projectName = detectProjectName(workdir, pkg);
65683
+ const branchName = options.branch ?? `feat/${options.feature}`;
65684
+ const prompt = buildPlanningPrompt(specContent, codebaseContext);
65685
+ const agentName = config2?.autoMode?.defaultAgent ?? "claude";
65686
+ const adapter = _deps2.getAgent(agentName);
65687
+ if (!adapter) {
65688
+ throw new Error(`[plan] No agent adapter found for '${agentName}'`);
65689
+ }
65690
+ const timeoutSeconds = config2?.execution?.sessionTimeoutSeconds ?? 600;
65691
+ let rawResponse;
65692
+ if (options.auto) {
65693
+ rawResponse = await adapter.complete(prompt, { jsonMode: true });
64765
65694
  } else {
64766
- if (!result.specContent) {
64767
- throw new Error("Agent did not produce specification content");
65695
+ const interactionBridge = createCliInteractionBridge();
65696
+ logger?.info("plan", "Starting interactive planning session...", { agent: agentName });
65697
+ try {
65698
+ const result = await adapter.plan({
65699
+ prompt,
65700
+ workdir,
65701
+ interactive: true,
65702
+ timeoutSeconds,
65703
+ interactionBridge
65704
+ });
65705
+ rawResponse = result.specContent;
65706
+ } finally {
65707
+ logger?.info("plan", "Interactive session ended");
64768
65708
  }
64769
- await Bun.write(outputPath, result.specContent);
64770
65709
  }
64771
- logger.info("cli", "\u2713 Specification written to output", { outputPath });
65710
+ const finalPrd = validatePlanOutput(rawResponse, options.feature, branchName);
65711
+ finalPrd.project = projectName;
65712
+ const outputDir = join10(naxDir, "features", options.feature);
65713
+ const outputPath = join10(outputDir, "prd.json");
65714
+ await _deps2.mkdirp(outputDir);
65715
+ await _deps2.writeFile(outputPath, JSON.stringify(finalPrd, null, 2));
65716
+ logger?.info("plan", "[OK] PRD written", { outputPath });
64772
65717
  return outputPath;
64773
65718
  }
65719
+ function createCliInteractionBridge() {
65720
+ return {
65721
+ async detectQuestion(text) {
65722
+ return text.includes("?");
65723
+ },
65724
+ async onQuestionDetected(text) {
65725
+ return text;
65726
+ }
65727
+ };
65728
+ }
65729
+ function detectProjectName(workdir, pkg) {
65730
+ if (pkg?.name && typeof pkg.name === "string") {
65731
+ return pkg.name;
65732
+ }
65733
+ const result = _deps2.spawnSync(["git", "remote", "get-url", "origin"], { cwd: workdir });
65734
+ if (result.exitCode === 0) {
65735
+ const url2 = result.stdout.toString().trim();
65736
+ const match = url2.match(/\/([^/]+?)(?:\.git)?$/);
65737
+ if (match?.[1])
65738
+ return match[1];
65739
+ }
65740
+ return "unknown";
65741
+ }
64774
65742
  function buildCodebaseContext2(scan) {
64775
65743
  const sections = [];
64776
65744
  sections.push(`## Codebase Structure
@@ -64797,24 +65765,62 @@ function buildCodebaseContext2(scan) {
64797
65765
  return sections.join(`
64798
65766
  `);
64799
65767
  }
64800
- function buildPlanPrompt(userPrompt, template) {
64801
- return `You are helping plan a new feature for this codebase.
65768
+ function buildPlanningPrompt(specContent, codebaseContext) {
65769
+ return `You are a senior software architect generating a product requirements document (PRD) as JSON.
65770
+
65771
+ ## Spec
65772
+
65773
+ ${specContent}
65774
+
65775
+ ## Codebase Context
65776
+
65777
+ ${codebaseContext}
65778
+
65779
+ ## Output Schema
65780
+
65781
+ Generate a JSON object with this exact structure (no markdown, no explanation \u2014 JSON only):
65782
+
65783
+ {
65784
+ "project": "string \u2014 project name",
65785
+ "feature": "string \u2014 feature name",
65786
+ "branchName": "string \u2014 git branch (e.g. feat/my-feature)",
65787
+ "createdAt": "ISO 8601 timestamp",
65788
+ "updatedAt": "ISO 8601 timestamp",
65789
+ "userStories": [
65790
+ {
65791
+ "id": "string \u2014 e.g. US-001",
65792
+ "title": "string \u2014 concise story title",
65793
+ "description": "string \u2014 detailed description of the story",
65794
+ "acceptanceCriteria": ["string \u2014 each AC line"],
65795
+ "tags": ["string \u2014 routing tags, e.g. feature, security, api"],
65796
+ "dependencies": ["string \u2014 story IDs this story depends on"],
65797
+ "status": "pending",
65798
+ "passes": false,
65799
+ "routing": {
65800
+ "complexity": "simple | medium | complex | expert",
65801
+ "testStrategy": "test-after | tdd-lite | three-session-tdd",
65802
+ "reasoning": "string \u2014 brief classification rationale"
65803
+ },
65804
+ "escalations": [],
65805
+ "attempts": 0
65806
+ }
65807
+ ]
65808
+ }
64802
65809
 
64803
- Task: ${userPrompt}
65810
+ ## Complexity Classification Guide
64804
65811
 
64805
- Please gather requirements and produce a structured specification following this template:
65812
+ - simple: \u226450 LOC, single-file change, purely additive, no new dependencies \u2192 test-after
65813
+ - medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 tdd-lite
65814
+ - complex: 200\u2013500 LOC, multiple modules, new abstractions or integrations \u2192 three-session-tdd
65815
+ - expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd
64806
65816
 
64807
- ${template}
65817
+ ## Test Strategy Guide
64808
65818
 
64809
- Ask clarifying questions as needed to ensure the spec is complete and unambiguous.
64810
- Focus on understanding:
64811
- - The problem being solved
64812
- - Specific requirements and constraints
64813
- - Acceptance criteria for success
64814
- - Technical approach and architecture
64815
- - What is explicitly out of scope
65819
+ - test-after: Simple changes with well-understood behavior. Write tests after implementation.
65820
+ - tdd-lite: Medium complexity. Write key tests first, implement, then fill coverage.
65821
+ - three-session-tdd: Complex/expert. Full TDD cycle with separate sessions for tests and implementation.
64816
65822
 
64817
- When done, output the complete specification in markdown format.`;
65823
+ Output ONLY the JSON object. Do not wrap in markdown code blocks.`;
64818
65824
  }
64819
65825
  // src/cli/accept.ts
64820
65826
  init_config();
@@ -64963,14 +65969,14 @@ async function displayModelEfficiency(workdir) {
64963
65969
  }
64964
65970
  // src/cli/status-features.ts
64965
65971
  init_source();
64966
- import { existsSync as existsSync11, readdirSync as readdirSync2 } from "fs";
64967
- import { join as join12 } from "path";
65972
+ import { existsSync as existsSync11, readdirSync as readdirSync3 } from "fs";
65973
+ import { join as join13 } from "path";
64968
65974
 
64969
65975
  // src/commands/common.ts
64970
65976
  init_path_security2();
64971
65977
  init_errors3();
64972
- import { existsSync as existsSync10, readdirSync, realpathSync as realpathSync2 } from "fs";
64973
- import { join as join10, resolve as resolve6 } from "path";
65978
+ import { existsSync as existsSync10, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
65979
+ import { join as join11, resolve as resolve6 } from "path";
64974
65980
  function resolveProject(options = {}) {
64975
65981
  const { dir, feature } = options;
64976
65982
  let projectRoot;
@@ -64978,12 +65984,12 @@ function resolveProject(options = {}) {
64978
65984
  let configPath;
64979
65985
  if (dir) {
64980
65986
  projectRoot = realpathSync2(resolve6(dir));
64981
- naxDir = join10(projectRoot, "nax");
65987
+ naxDir = join11(projectRoot, "nax");
64982
65988
  if (!existsSync10(naxDir)) {
64983
65989
  throw new NaxError(`Directory does not contain a nax project: ${projectRoot}
64984
65990
  Expected to find: ${naxDir}`, "NAX_DIR_NOT_FOUND", { projectRoot, naxDir });
64985
65991
  }
64986
- configPath = join10(naxDir, "config.json");
65992
+ configPath = join11(naxDir, "config.json");
64987
65993
  if (!existsSync10(configPath)) {
64988
65994
  throw new NaxError(`nax directory found but config.json is missing: ${naxDir}
64989
65995
  Expected to find: ${configPath}`, "CONFIG_NOT_FOUND", { naxDir, configPath });
@@ -64991,24 +65997,24 @@ Expected to find: ${configPath}`, "CONFIG_NOT_FOUND", { naxDir, configPath });
64991
65997
  } else {
64992
65998
  const found = findProjectRoot(process.cwd());
64993
65999
  if (!found) {
64994
- const cwdNaxDir = join10(process.cwd(), "nax");
66000
+ const cwdNaxDir = join11(process.cwd(), "nax");
64995
66001
  if (existsSync10(cwdNaxDir)) {
64996
- const cwdConfigPath = join10(cwdNaxDir, "config.json");
66002
+ const cwdConfigPath = join11(cwdNaxDir, "config.json");
64997
66003
  throw new NaxError(`nax directory found but config.json is missing: ${cwdNaxDir}
64998
66004
  Expected to find: ${cwdConfigPath}`, "CONFIG_NOT_FOUND", { naxDir: cwdNaxDir, configPath: cwdConfigPath });
64999
66005
  }
65000
66006
  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() });
65001
66007
  }
65002
66008
  projectRoot = found;
65003
- naxDir = join10(projectRoot, "nax");
65004
- configPath = join10(naxDir, "config.json");
66009
+ naxDir = join11(projectRoot, "nax");
66010
+ configPath = join11(naxDir, "config.json");
65005
66011
  }
65006
66012
  let featureDir;
65007
66013
  if (feature) {
65008
- const featuresDir = join10(naxDir, "features");
65009
- featureDir = join10(featuresDir, feature);
66014
+ const featuresDir = join11(naxDir, "features");
66015
+ featureDir = join11(featuresDir, feature);
65010
66016
  if (!existsSync10(featureDir)) {
65011
- const availableFeatures = existsSync10(featuresDir) ? readdirSync(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
66017
+ const availableFeatures = existsSync10(featuresDir) ? readdirSync2(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
65012
66018
  const availableMsg = availableFeatures.length > 0 ? `
65013
66019
 
65014
66020
  Available features:
@@ -65033,12 +66039,12 @@ function findProjectRoot(startDir) {
65033
66039
  let current = resolve6(startDir);
65034
66040
  let depth = 0;
65035
66041
  while (depth < MAX_DIRECTORY_DEPTH) {
65036
- const naxDir = join10(current, "nax");
65037
- const configPath = join10(naxDir, "config.json");
66042
+ const naxDir = join11(current, "nax");
66043
+ const configPath = join11(naxDir, "config.json");
65038
66044
  if (existsSync10(configPath)) {
65039
66045
  return realpathSync2(current);
65040
66046
  }
65041
- const parent = join10(current, "..");
66047
+ const parent = join11(current, "..");
65042
66048
  if (parent === current) {
65043
66049
  break;
65044
66050
  }
@@ -65060,7 +66066,7 @@ function isPidAlive(pid) {
65060
66066
  }
65061
66067
  }
65062
66068
  async function loadStatusFile(featureDir) {
65063
- const statusPath = join12(featureDir, "status.json");
66069
+ const statusPath = join13(featureDir, "status.json");
65064
66070
  if (!existsSync11(statusPath)) {
65065
66071
  return null;
65066
66072
  }
@@ -65072,7 +66078,7 @@ async function loadStatusFile(featureDir) {
65072
66078
  }
65073
66079
  }
65074
66080
  async function loadProjectStatusFile(projectDir) {
65075
- const statusPath = join12(projectDir, "nax", "status.json");
66081
+ const statusPath = join13(projectDir, "nax", "status.json");
65076
66082
  if (!existsSync11(statusPath)) {
65077
66083
  return null;
65078
66084
  }
@@ -65084,7 +66090,7 @@ async function loadProjectStatusFile(projectDir) {
65084
66090
  }
65085
66091
  }
65086
66092
  async function getFeatureSummary(featureName, featureDir) {
65087
- const prdPath = join12(featureDir, "prd.json");
66093
+ const prdPath = join13(featureDir, "prd.json");
65088
66094
  const prd = await loadPRD(prdPath);
65089
66095
  const counts = countStories(prd);
65090
66096
  const summary = {
@@ -65118,9 +66124,9 @@ async function getFeatureSummary(featureName, featureDir) {
65118
66124
  };
65119
66125
  }
65120
66126
  }
65121
- const runsDir = join12(featureDir, "runs");
66127
+ const runsDir = join13(featureDir, "runs");
65122
66128
  if (existsSync11(runsDir)) {
65123
- const runs = readdirSync2(runsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".jsonl") && e.name !== "latest.jsonl").map((e) => e.name).sort().reverse();
66129
+ const runs = readdirSync3(runsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".jsonl") && e.name !== "latest.jsonl").map((e) => e.name).sort().reverse();
65124
66130
  if (runs.length > 0) {
65125
66131
  const latestRun = runs[0].replace(".jsonl", "");
65126
66132
  summary.lastRun = latestRun;
@@ -65129,12 +66135,12 @@ async function getFeatureSummary(featureName, featureDir) {
65129
66135
  return summary;
65130
66136
  }
65131
66137
  async function displayAllFeatures(projectDir) {
65132
- const featuresDir = join12(projectDir, "nax", "features");
66138
+ const featuresDir = join13(projectDir, "nax", "features");
65133
66139
  if (!existsSync11(featuresDir)) {
65134
66140
  console.log(source_default.dim("No features found."));
65135
66141
  return;
65136
66142
  }
65137
- const features = readdirSync2(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
66143
+ const features = readdirSync3(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
65138
66144
  if (features.length === 0) {
65139
66145
  console.log(source_default.dim("No features found."));
65140
66146
  return;
@@ -65170,7 +66176,7 @@ async function displayAllFeatures(projectDir) {
65170
66176
  console.log();
65171
66177
  }
65172
66178
  }
65173
- const summaries = await Promise.all(features.map((name) => getFeatureSummary(name, join12(featuresDir, name))));
66179
+ const summaries = await Promise.all(features.map((name) => getFeatureSummary(name, join13(featuresDir, name))));
65174
66180
  console.log(source_default.bold(`\uD83D\uDCCA Features
65175
66181
  `));
65176
66182
  const header = ` ${"Feature".padEnd(25)} ${"Done".padEnd(6)} ${"Failed".padEnd(8)} ${"Pending".padEnd(9)} ${"Last Run".padEnd(22)} ${"Cost".padEnd(10)} Status`;
@@ -65196,7 +66202,7 @@ async function displayAllFeatures(projectDir) {
65196
66202
  console.log();
65197
66203
  }
65198
66204
  async function displayFeatureDetails(featureName, featureDir) {
65199
- const prdPath = join12(featureDir, "prd.json");
66205
+ const prdPath = join13(featureDir, "prd.json");
65200
66206
  const prd = await loadPRD(prdPath);
65201
66207
  const counts = countStories(prd);
65202
66208
  const status = await loadStatusFile(featureDir);
@@ -65310,8 +66316,8 @@ async function displayFeatureStatus(options = {}) {
65310
66316
  // src/cli/runs.ts
65311
66317
  init_errors3();
65312
66318
  init_logger2();
65313
- import { existsSync as existsSync12, readdirSync as readdirSync3 } from "fs";
65314
- import { join as join13 } from "path";
66319
+ import { existsSync as existsSync12, readdirSync as readdirSync4 } from "fs";
66320
+ import { join as join14 } from "path";
65315
66321
  async function parseRunLog(logPath) {
65316
66322
  const logger = getLogger();
65317
66323
  try {
@@ -65327,19 +66333,19 @@ async function parseRunLog(logPath) {
65327
66333
  async function runsListCommand(options) {
65328
66334
  const logger = getLogger();
65329
66335
  const { feature, workdir } = options;
65330
- const runsDir = join13(workdir, "nax", "features", feature, "runs");
66336
+ const runsDir = join14(workdir, "nax", "features", feature, "runs");
65331
66337
  if (!existsSync12(runsDir)) {
65332
66338
  logger.info("cli", "No runs found for feature", { feature, hint: `Directory not found: ${runsDir}` });
65333
66339
  return;
65334
66340
  }
65335
- const files = readdirSync3(runsDir).filter((f) => f.endsWith(".jsonl"));
66341
+ const files = readdirSync4(runsDir).filter((f) => f.endsWith(".jsonl"));
65336
66342
  if (files.length === 0) {
65337
66343
  logger.info("cli", "No runs found for feature", { feature });
65338
66344
  return;
65339
66345
  }
65340
66346
  logger.info("cli", `Runs for ${feature}`, { count: files.length });
65341
66347
  for (const file2 of files.sort().reverse()) {
65342
- const logPath = join13(runsDir, file2);
66348
+ const logPath = join14(runsDir, file2);
65343
66349
  const entries = await parseRunLog(logPath);
65344
66350
  const startEvent = entries.find((e) => e.message === "run.start");
65345
66351
  const completeEvent = entries.find((e) => e.message === "run.complete");
@@ -65365,7 +66371,7 @@ async function runsListCommand(options) {
65365
66371
  async function runsShowCommand(options) {
65366
66372
  const logger = getLogger();
65367
66373
  const { runId, feature, workdir } = options;
65368
- const logPath = join13(workdir, "nax", "features", feature, "runs", `${runId}.jsonl`);
66374
+ const logPath = join14(workdir, "nax", "features", feature, "runs", `${runId}.jsonl`);
65369
66375
  if (!existsSync12(logPath)) {
65370
66376
  logger.error("cli", "Run not found", { runId, feature, logPath });
65371
66377
  throw new NaxError("Run not found", "RUN_NOT_FOUND", { runId, feature, logPath });
@@ -65404,7 +66410,7 @@ async function runsShowCommand(options) {
65404
66410
  // src/cli/prompts-main.ts
65405
66411
  init_logger2();
65406
66412
  import { existsSync as existsSync15, mkdirSync as mkdirSync3 } from "fs";
65407
- import { join as join20 } from "path";
66413
+ import { join as join21 } from "path";
65408
66414
 
65409
66415
  // src/pipeline/index.ts
65410
66416
  init_runner();
@@ -65440,7 +66446,7 @@ init_prd();
65440
66446
 
65441
66447
  // src/cli/prompts-tdd.ts
65442
66448
  init_prompts2();
65443
- import { join as join19 } from "path";
66449
+ import { join as join20 } from "path";
65444
66450
  async function handleThreeSessionTddPrompts(story, ctx, outputDir, logger) {
65445
66451
  const [testWriterPrompt, implementerPrompt, verifierPrompt] = await Promise.all([
65446
66452
  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(),
@@ -65459,7 +66465,7 @@ ${frontmatter}---
65459
66465
 
65460
66466
  ${session.prompt}`;
65461
66467
  if (outputDir) {
65462
- const promptFile = join19(outputDir, `${story.id}.${session.role}.md`);
66468
+ const promptFile = join20(outputDir, `${story.id}.${session.role}.md`);
65463
66469
  await Bun.write(promptFile, fullOutput);
65464
66470
  logger.info("cli", "Written TDD prompt file", {
65465
66471
  storyId: story.id,
@@ -65475,7 +66481,7 @@ ${"=".repeat(80)}`);
65475
66481
  }
65476
66482
  }
65477
66483
  if (outputDir && ctx.contextMarkdown) {
65478
- const contextFile = join19(outputDir, `${story.id}.context.md`);
66484
+ const contextFile = join20(outputDir, `${story.id}.context.md`);
65479
66485
  const frontmatter = buildFrontmatter(story, ctx);
65480
66486
  const contextOutput = `---
65481
66487
  ${frontmatter}---
@@ -65489,12 +66495,12 @@ ${ctx.contextMarkdown}`;
65489
66495
  async function promptsCommand(options) {
65490
66496
  const logger = getLogger();
65491
66497
  const { feature, workdir, config: config2, storyId, outputDir } = options;
65492
- const naxDir = join20(workdir, "nax");
66498
+ const naxDir = join21(workdir, "nax");
65493
66499
  if (!existsSync15(naxDir)) {
65494
66500
  throw new Error(`nax directory not found. Run 'nax init' first in ${workdir}`);
65495
66501
  }
65496
- const featureDir = join20(naxDir, "features", feature);
65497
- const prdPath = join20(featureDir, "prd.json");
66502
+ const featureDir = join21(naxDir, "features", feature);
66503
+ const prdPath = join21(featureDir, "prd.json");
65498
66504
  if (!existsSync15(prdPath)) {
65499
66505
  throw new Error(`Feature "${feature}" not found or missing prd.json`);
65500
66506
  }
@@ -65554,10 +66560,10 @@ ${frontmatter}---
65554
66560
 
65555
66561
  ${ctx.prompt}`;
65556
66562
  if (outputDir) {
65557
- const promptFile = join20(outputDir, `${story.id}.prompt.md`);
66563
+ const promptFile = join21(outputDir, `${story.id}.prompt.md`);
65558
66564
  await Bun.write(promptFile, fullOutput);
65559
66565
  if (ctx.contextMarkdown) {
65560
- const contextFile = join20(outputDir, `${story.id}.context.md`);
66566
+ const contextFile = join21(outputDir, `${story.id}.context.md`);
65561
66567
  const contextOutput = `---
65562
66568
  ${frontmatter}---
65563
66569
 
@@ -65621,7 +66627,7 @@ function buildFrontmatter(story, ctx, role) {
65621
66627
  }
65622
66628
  // src/cli/prompts-init.ts
65623
66629
  import { existsSync as existsSync16, mkdirSync as mkdirSync4 } from "fs";
65624
- import { join as join21 } from "path";
66630
+ import { join as join22 } from "path";
65625
66631
  var TEMPLATE_ROLES = [
65626
66632
  { file: "test-writer.md", role: "test-writer" },
65627
66633
  { file: "implementer.md", role: "implementer", variant: "standard" },
@@ -65645,9 +66651,9 @@ var TEMPLATE_HEADER = `<!--
65645
66651
  `;
65646
66652
  async function promptsInitCommand(options) {
65647
66653
  const { workdir, force = false, autoWireConfig = true } = options;
65648
- const templatesDir = join21(workdir, "nax", "templates");
66654
+ const templatesDir = join22(workdir, "nax", "templates");
65649
66655
  mkdirSync4(templatesDir, { recursive: true });
65650
- const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync16(join21(templatesDir, f)));
66656
+ const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync16(join22(templatesDir, f)));
65651
66657
  if (existingFiles.length > 0 && !force) {
65652
66658
  console.warn(`[WARN] nax/templates/ already contains files: ${existingFiles.join(", ")}. No files overwritten.
65653
66659
  Pass --force to overwrite existing templates.`);
@@ -65655,7 +66661,7 @@ async function promptsInitCommand(options) {
65655
66661
  }
65656
66662
  const written = [];
65657
66663
  for (const template of TEMPLATE_ROLES) {
65658
- const filePath = join21(templatesDir, template.file);
66664
+ const filePath = join22(templatesDir, template.file);
65659
66665
  const roleBody = template.role === "implementer" ? buildRoleTaskSection(template.role, template.variant) : buildRoleTaskSection(template.role);
65660
66666
  const content = TEMPLATE_HEADER + roleBody;
65661
66667
  await Bun.write(filePath, content);
@@ -65671,7 +66677,7 @@ async function promptsInitCommand(options) {
65671
66677
  return written;
65672
66678
  }
65673
66679
  async function autoWirePromptsConfig(workdir) {
65674
- const configPath = join21(workdir, "nax.config.json");
66680
+ const configPath = join22(workdir, "nax.config.json");
65675
66681
  if (!existsSync16(configPath)) {
65676
66682
  const exampleConfig = JSON.stringify({
65677
66683
  prompts: {
@@ -65837,8 +66843,8 @@ function pad(str, width) {
65837
66843
  init_config();
65838
66844
  init_logger2();
65839
66845
  init_prd();
65840
- import { existsSync as existsSync17, readdirSync as readdirSync4 } from "fs";
65841
- import { join as join24 } from "path";
66846
+ import { existsSync as existsSync17, readdirSync as readdirSync5 } from "fs";
66847
+ import { join as join25 } from "path";
65842
66848
 
65843
66849
  // src/cli/diagnose-analysis.ts
65844
66850
  function detectFailurePattern(story, prd, status) {
@@ -66037,7 +67043,7 @@ function isProcessAlive2(pid) {
66037
67043
  }
66038
67044
  }
66039
67045
  async function loadStatusFile2(workdir) {
66040
- const statusPath = join24(workdir, "nax", "status.json");
67046
+ const statusPath = join25(workdir, "nax", "status.json");
66041
67047
  if (!existsSync17(statusPath))
66042
67048
  return null;
66043
67049
  try {
@@ -66065,7 +67071,7 @@ async function countCommitsSince(workdir, since) {
66065
67071
  }
66066
67072
  }
66067
67073
  async function checkLock(workdir) {
66068
- const lockFile = Bun.file(join24(workdir, "nax.lock"));
67074
+ const lockFile = Bun.file(join25(workdir, "nax.lock"));
66069
67075
  if (!await lockFile.exists())
66070
67076
  return { lockPresent: false };
66071
67077
  try {
@@ -66083,8 +67089,8 @@ async function diagnoseCommand(options = {}) {
66083
67089
  const logger = getLogger();
66084
67090
  const workdir = options.workdir ?? process.cwd();
66085
67091
  const naxSubdir = findProjectDir(workdir);
66086
- let projectDir = naxSubdir ? join24(naxSubdir, "..") : null;
66087
- if (!projectDir && existsSync17(join24(workdir, "nax"))) {
67092
+ let projectDir = naxSubdir ? join25(naxSubdir, "..") : null;
67093
+ if (!projectDir && existsSync17(join25(workdir, "nax"))) {
66088
67094
  projectDir = workdir;
66089
67095
  }
66090
67096
  if (!projectDir)
@@ -66095,18 +67101,18 @@ async function diagnoseCommand(options = {}) {
66095
67101
  if (status2) {
66096
67102
  feature = status2.run.feature;
66097
67103
  } else {
66098
- const featuresDir = join24(projectDir, "nax", "features");
67104
+ const featuresDir = join25(projectDir, "nax", "features");
66099
67105
  if (!existsSync17(featuresDir))
66100
67106
  throw new Error("No features found in project");
66101
- const features = readdirSync4(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
67107
+ const features = readdirSync5(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
66102
67108
  if (features.length === 0)
66103
67109
  throw new Error("No features found");
66104
67110
  feature = features[0];
66105
67111
  logger.info("diagnose", "No feature specified, using first found", { feature });
66106
67112
  }
66107
67113
  }
66108
- const featureDir = join24(projectDir, "nax", "features", feature);
66109
- const prdPath = join24(featureDir, "prd.json");
67114
+ const featureDir = join25(projectDir, "nax", "features", feature);
67115
+ const prdPath = join25(featureDir, "prd.json");
66110
67116
  if (!existsSync17(prdPath))
66111
67117
  throw new Error(`Feature not found: ${feature}`);
66112
67118
  const prd = await loadPRD(prdPath);
@@ -66149,16 +67155,16 @@ init_interaction();
66149
67155
  init_source();
66150
67156
  init_loader2();
66151
67157
  import { existsSync as existsSync20 } from "fs";
66152
- import { join as join27 } from "path";
67158
+ import { join as join28 } from "path";
66153
67159
 
66154
67160
  // src/context/generator.ts
66155
67161
  init_path_security2();
66156
67162
  import { existsSync as existsSync19 } from "fs";
66157
- import { join as join26 } from "path";
67163
+ import { join as join27 } from "path";
66158
67164
 
66159
67165
  // src/context/injector.ts
66160
67166
  import { existsSync as existsSync18 } from "fs";
66161
- import { join as join25 } from "path";
67167
+ import { join as join26 } from "path";
66162
67168
  var NOTABLE_NODE_DEPS = [
66163
67169
  "@nestjs",
66164
67170
  "express",
@@ -66188,7 +67194,7 @@ var NOTABLE_NODE_DEPS = [
66188
67194
  "ioredis"
66189
67195
  ];
66190
67196
  async function detectNode(workdir) {
66191
- const pkgPath = join25(workdir, "package.json");
67197
+ const pkgPath = join26(workdir, "package.json");
66192
67198
  if (!existsSync18(pkgPath))
66193
67199
  return null;
66194
67200
  try {
@@ -66205,7 +67211,7 @@ async function detectNode(workdir) {
66205
67211
  }
66206
67212
  }
66207
67213
  async function detectGo(workdir) {
66208
- const goMod = join25(workdir, "go.mod");
67214
+ const goMod = join26(workdir, "go.mod");
66209
67215
  if (!existsSync18(goMod))
66210
67216
  return null;
66211
67217
  try {
@@ -66229,7 +67235,7 @@ async function detectGo(workdir) {
66229
67235
  }
66230
67236
  }
66231
67237
  async function detectRust(workdir) {
66232
- const cargoPath = join25(workdir, "Cargo.toml");
67238
+ const cargoPath = join26(workdir, "Cargo.toml");
66233
67239
  if (!existsSync18(cargoPath))
66234
67240
  return null;
66235
67241
  try {
@@ -66245,8 +67251,8 @@ async function detectRust(workdir) {
66245
67251
  }
66246
67252
  }
66247
67253
  async function detectPython(workdir) {
66248
- const pyproject = join25(workdir, "pyproject.toml");
66249
- const requirements = join25(workdir, "requirements.txt");
67254
+ const pyproject = join26(workdir, "pyproject.toml");
67255
+ const requirements = join26(workdir, "requirements.txt");
66250
67256
  if (!existsSync18(pyproject) && !existsSync18(requirements))
66251
67257
  return null;
66252
67258
  try {
@@ -66265,7 +67271,7 @@ async function detectPython(workdir) {
66265
67271
  }
66266
67272
  }
66267
67273
  async function detectPhp(workdir) {
66268
- const composerPath = join25(workdir, "composer.json");
67274
+ const composerPath = join26(workdir, "composer.json");
66269
67275
  if (!existsSync18(composerPath))
66270
67276
  return null;
66271
67277
  try {
@@ -66278,7 +67284,7 @@ async function detectPhp(workdir) {
66278
67284
  }
66279
67285
  }
66280
67286
  async function detectRuby(workdir) {
66281
- const gemfile = join25(workdir, "Gemfile");
67287
+ const gemfile = join26(workdir, "Gemfile");
66282
67288
  if (!existsSync18(gemfile))
66283
67289
  return null;
66284
67290
  try {
@@ -66290,9 +67296,9 @@ async function detectRuby(workdir) {
66290
67296
  }
66291
67297
  }
66292
67298
  async function detectJvm(workdir) {
66293
- const pom = join25(workdir, "pom.xml");
66294
- const gradle = join25(workdir, "build.gradle");
66295
- const gradleKts = join25(workdir, "build.gradle.kts");
67299
+ const pom = join26(workdir, "pom.xml");
67300
+ const gradle = join26(workdir, "build.gradle");
67301
+ const gradleKts = join26(workdir, "build.gradle.kts");
66296
67302
  if (!existsSync18(pom) && !existsSync18(gradle) && !existsSync18(gradleKts))
66297
67303
  return null;
66298
67304
  try {
@@ -66300,7 +67306,7 @@ async function detectJvm(workdir) {
66300
67306
  const content2 = await Bun.file(pom).text();
66301
67307
  const nameMatch = content2.match(/<artifactId>([^<]+)<\/artifactId>/);
66302
67308
  const deps2 = [...content2.matchAll(/<artifactId>([^<]+)<\/artifactId>/g)].map((m) => m[1]).filter((d) => d !== nameMatch?.[1]).slice(0, 10);
66303
- const lang2 = existsSync18(join25(workdir, "src/main/kotlin")) ? "Kotlin" : "Java";
67309
+ const lang2 = existsSync18(join26(workdir, "src/main/kotlin")) ? "Kotlin" : "Java";
66304
67310
  return { name: nameMatch?.[1], lang: lang2, dependencies: deps2 };
66305
67311
  }
66306
67312
  const gradleFile = existsSync18(gradleKts) ? gradleKts : gradle;
@@ -66521,7 +67527,7 @@ async function generateFor(agent, options, config2) {
66521
67527
  try {
66522
67528
  const context = await loadContextContent(options, config2);
66523
67529
  const content = generator.generate(context);
66524
- const outputPath = join26(options.outputDir, generator.outputFile);
67530
+ const outputPath = join27(options.outputDir, generator.outputFile);
66525
67531
  validateFilePath(outputPath, options.outputDir);
66526
67532
  if (!options.dryRun) {
66527
67533
  await Bun.write(outputPath, content);
@@ -66538,7 +67544,7 @@ async function generateAll(options, config2) {
66538
67544
  for (const [agentKey, generator] of Object.entries(GENERATORS)) {
66539
67545
  try {
66540
67546
  const content = generator.generate(context);
66541
- const outputPath = join26(options.outputDir, generator.outputFile);
67547
+ const outputPath = join27(options.outputDir, generator.outputFile);
66542
67548
  validateFilePath(outputPath, options.outputDir);
66543
67549
  if (!options.dryRun) {
66544
67550
  await Bun.write(outputPath, content);
@@ -66556,8 +67562,8 @@ async function generateAll(options, config2) {
66556
67562
  var VALID_AGENTS = ["claude", "codex", "opencode", "cursor", "windsurf", "aider", "gemini"];
66557
67563
  async function generateCommand(options) {
66558
67564
  const workdir = process.cwd();
66559
- const contextPath = options.context ? join27(workdir, options.context) : join27(workdir, "nax/context.md");
66560
- const outputDir = options.output ? join27(workdir, options.output) : workdir;
67565
+ const contextPath = options.context ? join28(workdir, options.context) : join28(workdir, "nax/context.md");
67566
+ const outputDir = options.output ? join28(workdir, options.output) : workdir;
66561
67567
  const autoInject = !options.noAutoInject;
66562
67568
  const dryRun = options.dryRun ?? false;
66563
67569
  if (!existsSync20(contextPath)) {
@@ -66633,7 +67639,7 @@ async function generateCommand(options) {
66633
67639
  // src/cli/config-display.ts
66634
67640
  init_loader2();
66635
67641
  import { existsSync as existsSync22 } from "fs";
66636
- import { join as join29 } from "path";
67642
+ import { join as join30 } from "path";
66637
67643
 
66638
67644
  // src/cli/config-descriptions.ts
66639
67645
  var FIELD_DESCRIPTIONS = {
@@ -66839,7 +67845,7 @@ function deepEqual(a, b) {
66839
67845
  init_defaults();
66840
67846
  init_loader2();
66841
67847
  import { existsSync as existsSync21 } from "fs";
66842
- import { join as join28 } from "path";
67848
+ import { join as join29 } from "path";
66843
67849
  async function loadConfigFile(path14) {
66844
67850
  if (!existsSync21(path14))
66845
67851
  return null;
@@ -66861,7 +67867,7 @@ async function loadProjectConfig() {
66861
67867
  const projectDir = findProjectDir();
66862
67868
  if (!projectDir)
66863
67869
  return null;
66864
- const projectPath = join28(projectDir, "config.json");
67870
+ const projectPath = join29(projectDir, "config.json");
66865
67871
  return await loadConfigFile(projectPath);
66866
67872
  }
66867
67873
 
@@ -66921,7 +67927,7 @@ async function configCommand(config2, options = {}) {
66921
67927
  function determineConfigSources() {
66922
67928
  const globalPath = globalConfigPath();
66923
67929
  const projectDir = findProjectDir();
66924
- const projectPath = projectDir ? join29(projectDir, "config.json") : null;
67930
+ const projectPath = projectDir ? join30(projectDir, "config.json") : null;
66925
67931
  return {
66926
67932
  global: fileExists(globalPath) ? globalPath : null,
66927
67933
  project: projectPath && fileExists(projectPath) ? projectPath : null
@@ -67101,24 +68107,24 @@ async function diagnose(options) {
67101
68107
 
67102
68108
  // src/commands/logs.ts
67103
68109
  import { existsSync as existsSync24 } from "fs";
67104
- import { join as join32 } from "path";
68110
+ import { join as join33 } from "path";
67105
68111
 
67106
68112
  // src/commands/logs-formatter.ts
67107
68113
  init_source();
67108
68114
  init_formatter();
67109
- import { readdirSync as readdirSync6 } from "fs";
67110
- import { join as join31 } from "path";
68115
+ import { readdirSync as readdirSync7 } from "fs";
68116
+ import { join as join32 } from "path";
67111
68117
 
67112
68118
  // src/commands/logs-reader.ts
67113
- import { existsSync as existsSync23, readdirSync as readdirSync5 } from "fs";
68119
+ import { existsSync as existsSync23, readdirSync as readdirSync6 } from "fs";
67114
68120
  import { readdir as readdir3 } from "fs/promises";
67115
68121
  import { homedir as homedir3 } from "os";
67116
- import { join as join30 } from "path";
67117
- var _deps5 = {
67118
- getRunsDir: () => process.env.NAX_RUNS_DIR ?? join30(homedir3(), ".nax", "runs")
68122
+ import { join as join31 } from "path";
68123
+ var _deps6 = {
68124
+ getRunsDir: () => process.env.NAX_RUNS_DIR ?? join31(homedir3(), ".nax", "runs")
67119
68125
  };
67120
68126
  async function resolveRunFileFromRegistry(runId) {
67121
- const runsDir = _deps5.getRunsDir();
68127
+ const runsDir = _deps6.getRunsDir();
67122
68128
  let entries;
67123
68129
  try {
67124
68130
  entries = await readdir3(runsDir);
@@ -67127,7 +68133,7 @@ async function resolveRunFileFromRegistry(runId) {
67127
68133
  }
67128
68134
  let matched = null;
67129
68135
  for (const entry of entries) {
67130
- const metaPath = join30(runsDir, entry, "meta.json");
68136
+ const metaPath = join31(runsDir, entry, "meta.json");
67131
68137
  try {
67132
68138
  const meta3 = await Bun.file(metaPath).json();
67133
68139
  if (meta3.runId === runId || meta3.runId.startsWith(runId)) {
@@ -67143,20 +68149,20 @@ async function resolveRunFileFromRegistry(runId) {
67143
68149
  console.log(`Log directory unavailable for run: ${runId}`);
67144
68150
  return null;
67145
68151
  }
67146
- const files = readdirSync5(matched.eventsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
68152
+ const files = readdirSync6(matched.eventsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
67147
68153
  if (files.length === 0) {
67148
68154
  console.log(`No log files found for run: ${runId}`);
67149
68155
  return null;
67150
68156
  }
67151
68157
  const specificFile = files.find((f) => f === `${matched.runId}.jsonl`);
67152
- return join30(matched.eventsDir, specificFile ?? files[0]);
68158
+ return join31(matched.eventsDir, specificFile ?? files[0]);
67153
68159
  }
67154
68160
  async function selectRunFile(runsDir) {
67155
- const files = readdirSync5(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
68161
+ const files = readdirSync6(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
67156
68162
  if (files.length === 0) {
67157
68163
  return null;
67158
68164
  }
67159
- return join30(runsDir, files[0]);
68165
+ return join31(runsDir, files[0]);
67160
68166
  }
67161
68167
  async function extractRunSummary(filePath) {
67162
68168
  const file2 = Bun.file(filePath);
@@ -67230,7 +68236,7 @@ var LOG_LEVEL_PRIORITY2 = {
67230
68236
  error: 3
67231
68237
  };
67232
68238
  async function displayRunsList(runsDir) {
67233
- const files = readdirSync6(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
68239
+ const files = readdirSync7(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
67234
68240
  if (files.length === 0) {
67235
68241
  console.log(source_default.dim("No runs found"));
67236
68242
  return;
@@ -67241,7 +68247,7 @@ Runs:
67241
68247
  console.log(source_default.gray(" Timestamp Stories Duration Cost Status"));
67242
68248
  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"));
67243
68249
  for (const file2 of files) {
67244
- const filePath = join31(runsDir, file2);
68250
+ const filePath = join32(runsDir, file2);
67245
68251
  const summary = await extractRunSummary(filePath);
67246
68252
  const timestamp = file2.replace(".jsonl", "");
67247
68253
  const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
@@ -67366,7 +68372,7 @@ async function logsCommand(options) {
67366
68372
  return;
67367
68373
  }
67368
68374
  const resolved = resolveProject({ dir: options.dir });
67369
- const naxDir = join32(resolved.projectDir, "nax");
68375
+ const naxDir = join33(resolved.projectDir, "nax");
67370
68376
  const configPath = resolved.configPath;
67371
68377
  const configFile = Bun.file(configPath);
67372
68378
  const config2 = await configFile.json();
@@ -67374,8 +68380,8 @@ async function logsCommand(options) {
67374
68380
  if (!featureName) {
67375
68381
  throw new Error("No feature specified in config.json");
67376
68382
  }
67377
- const featureDir = join32(naxDir, "features", featureName);
67378
- const runsDir = join32(featureDir, "runs");
68383
+ const featureDir = join33(naxDir, "features", featureName);
68384
+ const runsDir = join33(featureDir, "runs");
67379
68385
  if (!existsSync24(runsDir)) {
67380
68386
  throw new Error(`No runs directory found for feature: ${featureName}`);
67381
68387
  }
@@ -67400,7 +68406,7 @@ init_config();
67400
68406
  init_prd();
67401
68407
  init_precheck();
67402
68408
  import { existsSync as existsSync29 } from "fs";
67403
- import { join as join33 } from "path";
68409
+ import { join as join34 } from "path";
67404
68410
  async function precheckCommand(options) {
67405
68411
  const resolved = resolveProject({
67406
68412
  dir: options.dir,
@@ -67416,16 +68422,16 @@ async function precheckCommand(options) {
67416
68422
  process.exit(1);
67417
68423
  }
67418
68424
  }
67419
- const naxDir = join33(resolved.projectDir, "nax");
67420
- const featureDir = join33(naxDir, "features", featureName);
67421
- const prdPath = join33(featureDir, "prd.json");
68425
+ const naxDir = join34(resolved.projectDir, "nax");
68426
+ const featureDir = join34(naxDir, "features", featureName);
68427
+ const prdPath = join34(featureDir, "prd.json");
67422
68428
  if (!existsSync29(featureDir)) {
67423
68429
  console.error(source_default.red(`Feature not found: ${featureName}`));
67424
68430
  process.exit(1);
67425
68431
  }
67426
68432
  if (!existsSync29(prdPath)) {
67427
68433
  console.error(source_default.red(`Missing prd.json for feature: ${featureName}`));
67428
- console.error(source_default.dim(`Run: nax analyze -f ${featureName}`));
68434
+ console.error(source_default.dim(`Run: nax plan -f ${featureName} --from spec.md --auto`));
67429
68435
  process.exit(EXIT_CODES.INVALID_PRD);
67430
68436
  }
67431
68437
  const config2 = await loadConfig(resolved.projectDir);
@@ -67442,10 +68448,10 @@ async function precheckCommand(options) {
67442
68448
  init_source();
67443
68449
  import { readdir as readdir4 } from "fs/promises";
67444
68450
  import { homedir as homedir4 } from "os";
67445
- import { join as join34 } from "path";
68451
+ import { join as join35 } from "path";
67446
68452
  var DEFAULT_LIMIT = 20;
67447
- var _deps7 = {
67448
- getRunsDir: () => join34(homedir4(), ".nax", "runs")
68453
+ var _deps8 = {
68454
+ getRunsDir: () => join35(homedir4(), ".nax", "runs")
67449
68455
  };
67450
68456
  function formatDuration3(ms) {
67451
68457
  if (ms <= 0)
@@ -67487,7 +68493,7 @@ function pad3(str, width) {
67487
68493
  return str + " ".repeat(padding);
67488
68494
  }
67489
68495
  async function runsCommand(options = {}) {
67490
- const runsDir = _deps7.getRunsDir();
68496
+ const runsDir = _deps8.getRunsDir();
67491
68497
  let entries;
67492
68498
  try {
67493
68499
  entries = await readdir4(runsDir);
@@ -67497,7 +68503,7 @@ async function runsCommand(options = {}) {
67497
68503
  }
67498
68504
  const rows = [];
67499
68505
  for (const entry of entries) {
67500
- const metaPath = join34(runsDir, entry, "meta.json");
68506
+ const metaPath = join35(runsDir, entry, "meta.json");
67501
68507
  let meta3;
67502
68508
  try {
67503
68509
  meta3 = await Bun.file(metaPath).json();
@@ -67574,7 +68580,7 @@ async function runsCommand(options = {}) {
67574
68580
 
67575
68581
  // src/commands/unlock.ts
67576
68582
  init_source();
67577
- import { join as join35 } from "path";
68583
+ import { join as join36 } from "path";
67578
68584
  function isProcessAlive3(pid) {
67579
68585
  try {
67580
68586
  process.kill(pid, 0);
@@ -67589,7 +68595,7 @@ function formatLockAge(ageMs) {
67589
68595
  }
67590
68596
  async function unlockCommand(options) {
67591
68597
  const workdir = options.dir ?? process.cwd();
67592
- const lockPath = join35(workdir, "nax.lock");
68598
+ const lockPath = join36(workdir, "nax.lock");
67593
68599
  const lockFile = Bun.file(lockPath);
67594
68600
  const exists = await lockFile.exists();
67595
68601
  if (!exists) {
@@ -67628,6 +68634,7 @@ async function unlockCommand(options) {
67628
68634
  init_config();
67629
68635
 
67630
68636
  // src/execution/runner.ts
68637
+ init_registry();
67631
68638
  init_hooks();
67632
68639
  init_logger2();
67633
68640
  init_prd();
@@ -67637,6 +68644,7 @@ init_crash_recovery();
67637
68644
  init_hooks();
67638
68645
  init_logger2();
67639
68646
  init_prd();
68647
+ init_git();
67640
68648
  init_crash_recovery();
67641
68649
  init_story_context();
67642
68650
  async function runCompletionPhase(options) {
@@ -67646,7 +68654,7 @@ async function runCompletionPhase(options) {
67646
68654
  const acceptanceResult = await runAcceptanceLoop2({
67647
68655
  config: options.config,
67648
68656
  prd: options.prd,
67649
- prdPath: "",
68657
+ prdPath: options.prdPath,
67650
68658
  workdir: options.workdir,
67651
68659
  featureDir: options.featureDir,
67652
68660
  hooks: options.hooks,
@@ -67657,7 +68665,8 @@ async function runCompletionPhase(options) {
67657
68665
  allStoryMetrics: options.allStoryMetrics,
67658
68666
  pluginRegistry: options.pluginRegistry,
67659
68667
  eventEmitter: options.eventEmitter,
67660
- statusWriter: options.statusWriter
68668
+ statusWriter: options.statusWriter,
68669
+ agentGetFn: options.agentGetFn
67661
68670
  });
67662
68671
  Object.assign(options, {
67663
68672
  prd: acceptanceResult.prd,
@@ -67708,6 +68717,7 @@ async function runCompletionPhase(options) {
67708
68717
  }
67709
68718
  stopHeartbeat();
67710
68719
  await writeExitSummary(options.logFilePath, options.totalCost, options.iterations, options.storiesCompleted, durationMs);
68720
+ await autoCommitIfDirty(options.workdir, "run.complete", "run-summary", options.feature);
67711
68721
  return {
67712
68722
  durationMs,
67713
68723
  runCompletedAt
@@ -67858,7 +68868,9 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
67858
68868
  logFilePath: options.logFilePath,
67859
68869
  runId: options.runId,
67860
68870
  startTime: options.startTime,
67861
- batchPlan
68871
+ batchPlan,
68872
+ agentGetFn: options.agentGetFn,
68873
+ pidRegistry: options.pidRegistry
67862
68874
  }, prd);
67863
68875
  prd = sequentialResult.prd;
67864
68876
  iterations = sequentialResult.iterations;
@@ -67896,7 +68908,8 @@ async function runSetupPhase(options) {
67896
68908
  getTotalCost: options.getTotalCost,
67897
68909
  getIterations: options.getIterations,
67898
68910
  getStoriesCompleted: options.getStoriesCompleted,
67899
- getTotalStories: options.getTotalStories
68911
+ getTotalStories: options.getTotalStories,
68912
+ agentGetFn: options.agentGetFn
67900
68913
  });
67901
68914
  return setupResult;
67902
68915
  }
@@ -67934,6 +68947,8 @@ async function run(options) {
67934
68947
  let totalCost = 0;
67935
68948
  const allStoryMetrics = [];
67936
68949
  const logger = getSafeLogger();
68950
+ const registry2 = createAgentRegistry(config2);
68951
+ const agentGetFn = registry2.getAgent.bind(registry2);
67937
68952
  let prd;
67938
68953
  const setupResult = await runSetupPhase({
67939
68954
  prdPath,
@@ -67951,6 +68966,7 @@ async function run(options) {
67951
68966
  skipPrecheck,
67952
68967
  headless,
67953
68968
  formatterMode,
68969
+ agentGetFn,
67954
68970
  getTotalCost: () => totalCost,
67955
68971
  getIterations: () => iterations,
67956
68972
  getStoriesCompleted: () => storiesCompleted,
@@ -67978,7 +68994,9 @@ async function run(options) {
67978
68994
  formatterMode,
67979
68995
  headless,
67980
68996
  parallel,
67981
- runParallelExecution: _runnerDeps.runParallelExecution ?? undefined
68997
+ runParallelExecution: _runnerDeps.runParallelExecution ?? undefined,
68998
+ agentGetFn,
68999
+ pidRegistry
67982
69000
  }, prd, pluginRegistry);
67983
69001
  prd = executionResult.prd;
67984
69002
  iterations = executionResult.iterations;
@@ -67999,6 +69017,7 @@ async function run(options) {
67999
69017
  hooks,
68000
69018
  feature,
68001
69019
  workdir,
69020
+ prdPath,
68002
69021
  statusFile,
68003
69022
  logFilePath,
68004
69023
  runId,
@@ -68014,7 +69033,8 @@ async function run(options) {
68014
69033
  iterations,
68015
69034
  statusWriter,
68016
69035
  pluginRegistry,
68017
- eventEmitter
69036
+ eventEmitter,
69037
+ agentGetFn
68018
69038
  });
68019
69039
  const { durationMs } = completionResult;
68020
69040
  return {
@@ -75349,6 +76369,31 @@ function renderTui(props) {
75349
76369
  init_version();
75350
76370
  var program2 = new Command;
75351
76371
  program2.name("nax").description("AI Coding Agent Orchestrator \u2014 loops until done").version(NAX_VERSION);
76372
+ async function promptForConfirmation(question) {
76373
+ if (!process.stdin.isTTY) {
76374
+ return true;
76375
+ }
76376
+ return new Promise((resolve9) => {
76377
+ process.stdout.write(source_default.bold(`${question} [Y/n] `));
76378
+ process.stdin.setRawMode(true);
76379
+ process.stdin.resume();
76380
+ process.stdin.setEncoding("utf8");
76381
+ const handler = (char) => {
76382
+ process.stdin.setRawMode(false);
76383
+ process.stdin.pause();
76384
+ process.stdin.removeListener("data", handler);
76385
+ const answer = char.toLowerCase();
76386
+ process.stdout.write(`
76387
+ `);
76388
+ if (answer === "n") {
76389
+ resolve9(false);
76390
+ } else {
76391
+ resolve9(true);
76392
+ }
76393
+ };
76394
+ process.stdin.on("data", handler);
76395
+ });
76396
+ }
75352
76397
  program2.command("init").description("Initialize nax in the current project").option("-d, --dir <path>", "Project directory", process.cwd()).option("-f, --force", "Force overwrite existing files", false).action(async (options) => {
75353
76398
  let workdir;
75354
76399
  try {
@@ -75357,15 +76402,15 @@ program2.command("init").description("Initialize nax in the current project").op
75357
76402
  console.error(source_default.red(`Invalid directory: ${err.message}`));
75358
76403
  process.exit(1);
75359
76404
  }
75360
- const naxDir = join42(workdir, "nax");
76405
+ const naxDir = join43(workdir, "nax");
75361
76406
  if (existsSync32(naxDir) && !options.force) {
75362
76407
  console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
75363
76408
  return;
75364
76409
  }
75365
- mkdirSync6(join42(naxDir, "features"), { recursive: true });
75366
- mkdirSync6(join42(naxDir, "hooks"), { recursive: true });
75367
- await Bun.write(join42(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
75368
- await Bun.write(join42(naxDir, "hooks.json"), JSON.stringify({
76410
+ mkdirSync6(join43(naxDir, "features"), { recursive: true });
76411
+ mkdirSync6(join43(naxDir, "hooks"), { recursive: true });
76412
+ await Bun.write(join43(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
76413
+ await Bun.write(join43(naxDir, "hooks.json"), JSON.stringify({
75369
76414
  hooks: {
75370
76415
  "on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
75371
76416
  "on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
@@ -75373,12 +76418,12 @@ program2.command("init").description("Initialize nax in the current project").op
75373
76418
  "on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false }
75374
76419
  }
75375
76420
  }, null, 2));
75376
- await Bun.write(join42(naxDir, ".gitignore"), `# nax temp files
76421
+ await Bun.write(join43(naxDir, ".gitignore"), `# nax temp files
75377
76422
  *.tmp
75378
76423
  .paused.json
75379
76424
  .nax-verifier-verdict.json
75380
76425
  `);
75381
- await Bun.write(join42(naxDir, "context.md"), `# Project Context
76426
+ await Bun.write(join43(naxDir, "context.md"), `# Project Context
75382
76427
 
75383
76428
  This document defines coding standards, architectural decisions, and forbidden patterns for this project.
75384
76429
  Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
@@ -75463,7 +76508,7 @@ Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cu
75463
76508
  console.log(source_default.dim(`
75464
76509
  Next: nax features create <name>`));
75465
76510
  });
75466
- program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).action(async (options) => {
76511
+ program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).action(async (options) => {
75467
76512
  let workdir;
75468
76513
  try {
75469
76514
  workdir = validateDirectory(options.dir);
@@ -75471,6 +76516,14 @@ program2.command("run").description("Run the orchestration loop for a feature").
75471
76516
  console.error(source_default.red(`Invalid directory: ${err.message}`));
75472
76517
  process.exit(1);
75473
76518
  }
76519
+ if (options.plan && !options.from) {
76520
+ console.error(source_default.red("Error: --plan requires --from <spec-path>"));
76521
+ process.exit(1);
76522
+ }
76523
+ if (options.from && !existsSync32(options.from)) {
76524
+ console.error(source_default.red(`Error: File not found: ${options.from} (required with --plan)`));
76525
+ process.exit(1);
76526
+ }
75474
76527
  let logLevel = "info";
75475
76528
  const envLevel = process.env.NAX_LOG_LEVEL?.toLowerCase();
75476
76529
  if (envLevel && ["error", "warn", "info", "debug"].includes(envLevel)) {
@@ -75496,16 +76549,48 @@ program2.command("run").description("Run the orchestration loop for a feature").
75496
76549
  console.error(source_default.red("nax not initialized. Run: nax init"));
75497
76550
  process.exit(1);
75498
76551
  }
75499
- const featureDir = join42(naxDir, "features", options.feature);
75500
- const prdPath = join42(featureDir, "prd.json");
76552
+ const featureDir = join43(naxDir, "features", options.feature);
76553
+ const prdPath = join43(featureDir, "prd.json");
76554
+ if (options.plan && options.from) {
76555
+ try {
76556
+ console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
76557
+ const generatedPrdPath = await planCommand(workdir, config2, {
76558
+ from: options.from,
76559
+ feature: options.feature,
76560
+ auto: true,
76561
+ branch: undefined
76562
+ });
76563
+ const generatedPrd = await loadPRD(generatedPrdPath);
76564
+ console.log(source_default.bold(`
76565
+ \u2500\u2500 Planning Summary \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`));
76566
+ console.log(source_default.dim(`Feature: ${generatedPrd.feature}`));
76567
+ console.log(source_default.dim(`Stories: ${generatedPrd.userStories.length}`));
76568
+ console.log();
76569
+ for (const story of generatedPrd.userStories) {
76570
+ const complexity = story.routing?.complexity || "unknown";
76571
+ console.log(source_default.dim(` ${story.id}: ${story.title} [${complexity}]`));
76572
+ }
76573
+ console.log();
76574
+ if (!options.headless) {
76575
+ const confirmationResult = await promptForConfirmation("Proceed with execution?");
76576
+ if (!confirmationResult) {
76577
+ console.log(source_default.yellow("Execution cancelled."));
76578
+ process.exit(0);
76579
+ }
76580
+ }
76581
+ } catch (err) {
76582
+ console.error(source_default.red(`Error during planning: ${err.message}`));
76583
+ process.exit(1);
76584
+ }
76585
+ }
75501
76586
  if (!existsSync32(prdPath)) {
75502
76587
  console.error(source_default.red(`Feature "${options.feature}" not found or missing prd.json`));
75503
76588
  process.exit(1);
75504
76589
  }
75505
- const runsDir = join42(featureDir, "runs");
76590
+ const runsDir = join43(featureDir, "runs");
75506
76591
  mkdirSync6(runsDir, { recursive: true });
75507
76592
  const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
75508
- const logFilePath = join42(runsDir, `${runId}.jsonl`);
76593
+ const logFilePath = join43(runsDir, `${runId}.jsonl`);
75509
76594
  const isTTY = process.stdout.isTTY ?? false;
75510
76595
  const headlessFlag = options.headless ?? false;
75511
76596
  const headlessEnv = process.env.NAX_HEADLESS === "1";
@@ -75521,7 +76606,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
75521
76606
  config2.autoMode.defaultAgent = options.agent;
75522
76607
  }
75523
76608
  config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
75524
- const globalNaxDir = join42(homedir8(), ".nax");
76609
+ const globalNaxDir = join43(homedir8(), ".nax");
75525
76610
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
75526
76611
  const eventEmitter = new PipelineEventEmitter;
75527
76612
  let tuiInstance;
@@ -75544,7 +76629,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
75544
76629
  } else {
75545
76630
  console.log(source_default.dim(" [Headless mode \u2014 pipe output]"));
75546
76631
  }
75547
- const statusFilePath = join42(workdir, "nax", "status.json");
76632
+ const statusFilePath = join43(workdir, "nax", "status.json");
75548
76633
  let parallel;
75549
76634
  if (options.parallel !== undefined) {
75550
76635
  parallel = Number.parseInt(options.parallel, 10);
@@ -75570,7 +76655,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
75570
76655
  headless: useHeadless,
75571
76656
  skipPrecheck: options.skipPrecheck ?? false
75572
76657
  });
75573
- const latestSymlink = join42(runsDir, "latest.jsonl");
76658
+ const latestSymlink = join43(runsDir, "latest.jsonl");
75574
76659
  try {
75575
76660
  if (existsSync32(latestSymlink)) {
75576
76661
  Bun.spawnSync(["rm", latestSymlink]);
@@ -75608,9 +76693,9 @@ features.command("create <name>").description("Create a new feature").option("-d
75608
76693
  console.error(source_default.red("nax not initialized. Run: nax init"));
75609
76694
  process.exit(1);
75610
76695
  }
75611
- const featureDir = join42(naxDir, "features", name);
76696
+ const featureDir = join43(naxDir, "features", name);
75612
76697
  mkdirSync6(featureDir, { recursive: true });
75613
- await Bun.write(join42(featureDir, "spec.md"), `# Feature: ${name}
76698
+ await Bun.write(join43(featureDir, "spec.md"), `# Feature: ${name}
75614
76699
 
75615
76700
  ## Overview
75616
76701
 
@@ -75618,7 +76703,7 @@ features.command("create <name>").description("Create a new feature").option("-d
75618
76703
 
75619
76704
  ## Acceptance Criteria
75620
76705
  `);
75621
- await Bun.write(join42(featureDir, "plan.md"), `# Plan: ${name}
76706
+ await Bun.write(join43(featureDir, "plan.md"), `# Plan: ${name}
75622
76707
 
75623
76708
  ## Architecture
75624
76709
 
@@ -75626,7 +76711,7 @@ features.command("create <name>").description("Create a new feature").option("-d
75626
76711
 
75627
76712
  ## Dependencies
75628
76713
  `);
75629
- await Bun.write(join42(featureDir, "tasks.md"), `# Tasks: ${name}
76714
+ await Bun.write(join43(featureDir, "tasks.md"), `# Tasks: ${name}
75630
76715
 
75631
76716
  ## US-001: [Title]
75632
76717
 
@@ -75635,7 +76720,7 @@ features.command("create <name>").description("Create a new feature").option("-d
75635
76720
  ### Acceptance Criteria
75636
76721
  - [ ] Criterion 1
75637
76722
  `);
75638
- await Bun.write(join42(featureDir, "progress.txt"), `# Progress: ${name}
76723
+ await Bun.write(join43(featureDir, "progress.txt"), `# Progress: ${name}
75639
76724
 
75640
76725
  Created: ${new Date().toISOString()}
75641
76726
 
@@ -75648,7 +76733,7 @@ Created: ${new Date().toISOString()}
75648
76733
  console.log(source_default.dim(" \u251C\u2500\u2500 tasks.md"));
75649
76734
  console.log(source_default.dim(" \u2514\u2500\u2500 progress.txt"));
75650
76735
  console.log(source_default.dim(`
75651
- Next: Edit spec.md and tasks.md, then: nax analyze --feature ${name}`));
76736
+ Next: Edit spec.md and tasks.md, then: nax plan -f ${name} --from spec.md --auto`));
75652
76737
  });
75653
76738
  features.command("list").description("List all features").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
75654
76739
  let workdir;
@@ -75663,13 +76748,13 @@ features.command("list").description("List all features").option("-d, --dir <pat
75663
76748
  console.error(source_default.red("nax not initialized."));
75664
76749
  process.exit(1);
75665
76750
  }
75666
- const featuresDir = join42(naxDir, "features");
76751
+ const featuresDir = join43(naxDir, "features");
75667
76752
  if (!existsSync32(featuresDir)) {
75668
76753
  console.log(source_default.dim("No features yet."));
75669
76754
  return;
75670
76755
  }
75671
- const { readdirSync: readdirSync7 } = await import("fs");
75672
- const entries = readdirSync7(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
76756
+ const { readdirSync: readdirSync8 } = await import("fs");
76757
+ const entries = readdirSync8(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
75673
76758
  if (entries.length === 0) {
75674
76759
  console.log(source_default.dim("No features yet."));
75675
76760
  return;
@@ -75678,7 +76763,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
75678
76763
  Features:
75679
76764
  `));
75680
76765
  for (const name of entries) {
75681
- const prdPath = join42(featuresDir, name, "prd.json");
76766
+ const prdPath = join43(featuresDir, name, "prd.json");
75682
76767
  if (existsSync32(prdPath)) {
75683
76768
  const prd = await loadPRD(prdPath);
75684
76769
  const c = countStories(prd);
@@ -75689,7 +76774,13 @@ Features:
75689
76774
  }
75690
76775
  console.log();
75691
76776
  });
75692
- program2.command("plan <description>").description("Interactive planning via agent plan mode").option("--from <file>", "Non-interactive mode: read from input file").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (description, options) => {
76777
+ program2.command("plan [description]").description("Generate prd.json from a spec file via LLM one-shot call (replaces deprecated 'nax analyze')").requiredOption("--from <spec-path>", "Path to spec file (required)").requiredOption("-f, --feature <name>", "Feature name (required)").option("--auto", "Run in auto (one-shot LLM) mode", false).option("-b, --branch <branch>", "Override default branch name").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (description, options) => {
76778
+ if (description) {
76779
+ console.error(source_default.red(`Error: Positional args removed in plan v2.
76780
+
76781
+ Use: nax plan -f <feature> --from <spec>`));
76782
+ process.exit(1);
76783
+ }
75693
76784
  let workdir;
75694
76785
  try {
75695
76786
  workdir = validateDirectory(options.dir);
@@ -75704,21 +76795,26 @@ program2.command("plan <description>").description("Interactive planning via age
75704
76795
  }
75705
76796
  const config2 = await loadConfig(workdir);
75706
76797
  try {
75707
- const specPath = await planCommand(description, workdir, config2, {
75708
- interactive: !options.from,
75709
- from: options.from
76798
+ const prdPath = await planCommand(workdir, config2, {
76799
+ from: options.from,
76800
+ feature: options.feature,
76801
+ auto: options.auto,
76802
+ branch: options.branch
75710
76803
  });
75711
76804
  console.log(source_default.green(`
75712
- \u2705 Planning complete`));
75713
- console.log(source_default.dim(` Spec: ${specPath}`));
76805
+ [OK] PRD generated`));
76806
+ console.log(source_default.dim(` PRD: ${prdPath}`));
75714
76807
  console.log(source_default.dim(`
75715
- Next: nax analyze -f <feature-name>`));
76808
+ Next: nax run -f ${options.feature}`));
75716
76809
  } catch (err) {
75717
76810
  console.error(source_default.red(`Error: ${err.message}`));
75718
76811
  process.exit(1);
75719
76812
  }
75720
76813
  });
75721
- program2.command("analyze").description("Parse spec.md into prd.json via agent decompose").requiredOption("-f, --feature <name>", "Feature name").option("-b, --branch <name>", "Branch name", "feat/<feature>").option("--from <path>", "Explicit spec path (overrides default spec.md)").option("--reclassify", "Re-classify existing prd.json without decompose", false).option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
76814
+ program2.command("analyze").description("(deprecated) Parse spec.md into prd.json via agent decompose \u2014 use 'nax plan' instead").requiredOption("-f, --feature <name>", "Feature name").option("-b, --branch <name>", "Branch name", "feat/<feature>").option("--from <path>", "Explicit spec path (overrides default spec.md)").option("--reclassify", "Re-classify existing prd.json without decompose", false).option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
76815
+ const deprecationMsg = "\u26A0\uFE0F 'nax analyze' is deprecated. Use 'nax plan -f <feature> --from <spec> --auto' instead.";
76816
+ process.stderr.write(`${source_default.yellow(deprecationMsg)}
76817
+ `);
75722
76818
  let workdir;
75723
76819
  try {
75724
76820
  workdir = validateDirectory(options.dir);
@@ -75731,7 +76827,7 @@ program2.command("analyze").description("Parse spec.md into prd.json via agent d
75731
76827
  console.error(source_default.red("nax not initialized. Run: nax init"));
75732
76828
  process.exit(1);
75733
76829
  }
75734
- const featureDir = join42(naxDir, "features", options.feature);
76830
+ const featureDir = join43(naxDir, "features", options.feature);
75735
76831
  if (!existsSync32(featureDir)) {
75736
76832
  console.error(source_default.red(`Feature "${options.feature}" not found.`));
75737
76833
  process.exit(1);
@@ -75747,7 +76843,7 @@ program2.command("analyze").description("Parse spec.md into prd.json via agent d
75747
76843
  specPath: options.from,
75748
76844
  reclassify: options.reclassify
75749
76845
  });
75750
- const prdPath = join42(featureDir, "prd.json");
76846
+ const prdPath = join43(featureDir, "prd.json");
75751
76847
  await Bun.write(prdPath, JSON.stringify(prd, null, 2));
75752
76848
  const c = countStories(prd);
75753
76849
  console.log(source_default.green(`