@nathapp/nax 0.40.1 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/nax.js +1072 -268
  2. package/package.json +2 -2
  3. package/src/acceptance/fix-generator.ts +4 -35
  4. package/src/acceptance/generator.ts +4 -27
  5. package/src/agents/acp/adapter.ts +644 -0
  6. package/src/agents/acp/cost.ts +79 -0
  7. package/src/agents/acp/index.ts +9 -0
  8. package/src/agents/acp/interaction-bridge.ts +126 -0
  9. package/src/agents/acp/parser.ts +166 -0
  10. package/src/agents/acp/spawn-client.ts +309 -0
  11. package/src/agents/acp/types.ts +22 -0
  12. package/src/agents/claude-complete.ts +3 -3
  13. package/src/agents/registry.ts +83 -0
  14. package/src/agents/types-extended.ts +23 -0
  15. package/src/agents/types.ts +17 -0
  16. package/src/cli/analyze.ts +6 -2
  17. package/src/cli/plan.ts +23 -0
  18. package/src/config/defaults.ts +1 -0
  19. package/src/config/runtime-types.ts +10 -0
  20. package/src/config/schema.ts +1 -0
  21. package/src/config/schemas.ts +6 -0
  22. package/src/config/types.ts +1 -0
  23. package/src/execution/executor-types.ts +6 -0
  24. package/src/execution/iteration-runner.ts +2 -0
  25. package/src/execution/lifecycle/acceptance-loop.ts +5 -2
  26. package/src/execution/lifecycle/run-initialization.ts +16 -4
  27. package/src/execution/lifecycle/run-setup.ts +4 -0
  28. package/src/execution/runner-completion.ts +11 -1
  29. package/src/execution/runner-execution.ts +8 -0
  30. package/src/execution/runner-setup.ts +4 -0
  31. package/src/execution/runner.ts +10 -0
  32. package/src/pipeline/stages/execution.ts +33 -1
  33. package/src/pipeline/stages/routing.ts +18 -7
  34. package/src/pipeline/types.ts +10 -0
  35. package/src/tdd/orchestrator.ts +7 -0
  36. package/src/tdd/rectification-gate.ts +6 -0
  37. package/src/tdd/session-runner.ts +4 -0
package/dist/nax.js CHANGED
@@ -3241,9 +3241,6 @@ async function executeComplete(binary, prompt, options) {
3241
3241
  if (options?.model) {
3242
3242
  cmd.push("--model", options.model);
3243
3243
  }
3244
- if (options?.maxTokens !== undefined) {
3245
- cmd.push("--max-tokens", String(options.maxTokens));
3246
- }
3247
3244
  if (options?.jsonMode) {
3248
3245
  cmd.push("--output-format", "json");
3249
3246
  }
@@ -3469,6 +3466,17 @@ function estimateCostByDuration(modelTier, durationMs) {
3469
3466
  confidence: "fallback"
3470
3467
  };
3471
3468
  }
3469
+ function formatCostWithConfidence(estimate) {
3470
+ const formattedCost = `$${estimate.cost.toFixed(2)}`;
3471
+ switch (estimate.confidence) {
3472
+ case "exact":
3473
+ return formattedCost;
3474
+ case "estimated":
3475
+ return `~${formattedCost}`;
3476
+ case "fallback":
3477
+ return `~${formattedCost} (duration-based)`;
3478
+ }
3479
+ }
3472
3480
  var COST_RATES;
3473
3481
  var init_cost = __esm(() => {
3474
3482
  COST_RATES = {
@@ -17525,7 +17533,7 @@ var init_zod = __esm(() => {
17525
17533
  });
17526
17534
 
17527
17535
  // src/config/schemas.ts
17528
- var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, AdaptiveRoutingConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, DecomposeConfigSchema, NaxConfigSchema;
17536
+ var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, AdaptiveRoutingConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, DecomposeConfigSchema, NaxConfigSchema;
17529
17537
  var init_schemas3 = __esm(() => {
17530
17538
  init_zod();
17531
17539
  TokenPricingSchema = exports_external.object({
@@ -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()
@@ -18676,29 +18689,10 @@ async function generateAcceptanceTests(adapter, options) {
18676
18689
  logger.info("acceptance", "Found acceptance criteria", { count: criteria.length });
18677
18690
  const prompt = buildAcceptanceTestPrompt(criteria, options.featureName, options.codebaseContext);
18678
18691
  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
- }
18692
+ const output = await adapter.complete(prompt, {
18693
+ model: options.modelDef.model
18690
18694
  });
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);
18695
+ const testCode = extractTestCode(output);
18702
18696
  return {
18703
18697
  testCode,
18704
18698
  criteria
@@ -18801,7 +18795,7 @@ Requirements:
18801
18795
  Respond with ONLY the fix description (no JSON, no markdown, just the description text).`;
18802
18796
  }
18803
18797
  async function generateFixStories(adapter, options) {
18804
- const { failedACs, testOutput, prd, specContent, workdir, modelDef } = options;
18798
+ const { failedACs, testOutput, prd, specContent, modelDef } = options;
18805
18799
  const fixStories = [];
18806
18800
  const acTextMap = parseACTextFromSpec(specContent);
18807
18801
  const logger = getLogger();
@@ -18816,34 +18810,9 @@ async function generateFixStories(adapter, options) {
18816
18810
  }
18817
18811
  const prompt = buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd);
18818
18812
  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
- }
18813
+ const fixDescription = await adapter.complete(prompt, {
18814
+ model: modelDef.model
18830
18815
  });
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
18816
  fixStories.push({
18848
18817
  id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
18849
18818
  title: `Fix: ${failedAC} \u2014 ${acText.slice(0, 50)}`,
@@ -18910,6 +18879,679 @@ var init_acceptance = __esm(() => {
18910
18879
  init_fix_generator();
18911
18880
  });
18912
18881
 
18882
+ // src/agents/acp/parser.ts
18883
+ function parseAcpxJsonOutput(rawOutput) {
18884
+ const lines = rawOutput.split(`
18885
+ `).filter((l) => l.trim());
18886
+ let text = "";
18887
+ let tokenUsage;
18888
+ let stopReason;
18889
+ let error48;
18890
+ for (const line of lines) {
18891
+ try {
18892
+ const event = JSON.parse(line);
18893
+ if (event.content && typeof event.content === "string")
18894
+ text += event.content;
18895
+ if (event.text && typeof event.text === "string")
18896
+ text += event.text;
18897
+ if (event.result && typeof event.result === "string")
18898
+ text = event.result;
18899
+ if (event.cumulative_token_usage)
18900
+ tokenUsage = event.cumulative_token_usage;
18901
+ if (event.usage) {
18902
+ tokenUsage = {
18903
+ input_tokens: event.usage.input_tokens ?? event.usage.prompt_tokens ?? 0,
18904
+ output_tokens: event.usage.output_tokens ?? event.usage.completion_tokens ?? 0
18905
+ };
18906
+ }
18907
+ if (event.stopReason)
18908
+ stopReason = event.stopReason;
18909
+ if (event.stop_reason)
18910
+ stopReason = event.stop_reason;
18911
+ if (event.error) {
18912
+ error48 = typeof event.error === "string" ? event.error : event.error.message ?? JSON.stringify(event.error);
18913
+ }
18914
+ } catch {
18915
+ if (!text)
18916
+ text = line;
18917
+ }
18918
+ }
18919
+ return { text: text.trim(), tokenUsage, stopReason, error: error48 };
18920
+ }
18921
+
18922
+ // src/agents/acp/spawn-client.ts
18923
+ function buildAllowedEnv2(extraEnv) {
18924
+ const allowed = {};
18925
+ const essentialVars = ["PATH", "HOME", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
18926
+ for (const varName of essentialVars) {
18927
+ if (process.env[varName])
18928
+ allowed[varName] = process.env[varName];
18929
+ }
18930
+ const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "CLAUDE_API_KEY"];
18931
+ for (const varName of apiKeyVars) {
18932
+ if (process.env[varName])
18933
+ allowed[varName] = process.env[varName];
18934
+ }
18935
+ const allowedPrefixes = ["CLAUDE_", "NAX_", "CLAW_", "TURBO_", "ACPX_", "CODEX_", "GEMINI_"];
18936
+ for (const [key, value] of Object.entries(process.env)) {
18937
+ if (allowedPrefixes.some((prefix) => key.startsWith(prefix))) {
18938
+ allowed[key] = value;
18939
+ }
18940
+ }
18941
+ if (extraEnv)
18942
+ Object.assign(allowed, extraEnv);
18943
+ return allowed;
18944
+ }
18945
+
18946
+ class SpawnAcpSession {
18947
+ agentName;
18948
+ sessionName;
18949
+ cwd;
18950
+ model;
18951
+ timeoutSeconds;
18952
+ permissionMode;
18953
+ env;
18954
+ constructor(opts) {
18955
+ this.agentName = opts.agentName;
18956
+ this.sessionName = opts.sessionName;
18957
+ this.cwd = opts.cwd;
18958
+ this.model = opts.model;
18959
+ this.timeoutSeconds = opts.timeoutSeconds;
18960
+ this.permissionMode = opts.permissionMode;
18961
+ this.env = opts.env;
18962
+ }
18963
+ async prompt(text) {
18964
+ const cmd = [
18965
+ "acpx",
18966
+ "--cwd",
18967
+ this.cwd,
18968
+ ...this.permissionMode === "approve-all" ? ["--approve-all"] : [],
18969
+ "--format",
18970
+ "json",
18971
+ "--model",
18972
+ this.model,
18973
+ "--timeout",
18974
+ String(this.timeoutSeconds),
18975
+ this.agentName,
18976
+ "prompt",
18977
+ "-s",
18978
+ this.sessionName,
18979
+ "--file",
18980
+ "-"
18981
+ ];
18982
+ getSafeLogger()?.debug("acp-adapter", `Sending prompt to session: ${this.sessionName}`);
18983
+ const proc = _spawnClientDeps.spawn(cmd, {
18984
+ cwd: this.cwd,
18985
+ stdin: "pipe",
18986
+ stdout: "pipe",
18987
+ stderr: "pipe",
18988
+ env: this.env
18989
+ });
18990
+ proc.stdin.write(text);
18991
+ proc.stdin.end();
18992
+ const exitCode = await proc.exited;
18993
+ const stdout = await new Response(proc.stdout).text();
18994
+ const stderr = await new Response(proc.stderr).text();
18995
+ if (exitCode !== 0) {
18996
+ getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
18997
+ stderr: stderr.slice(0, 200)
18998
+ });
18999
+ return {
19000
+ messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
19001
+ stopReason: "error"
19002
+ };
19003
+ }
19004
+ try {
19005
+ const parsed = parseAcpxJsonOutput(stdout);
19006
+ return {
19007
+ messages: [{ role: "assistant", content: parsed.text || "" }],
19008
+ stopReason: "end_turn",
19009
+ cumulative_token_usage: parsed.tokenUsage
19010
+ };
19011
+ } catch (err) {
19012
+ getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
19013
+ stderr: stderr.slice(0, 200)
19014
+ });
19015
+ throw err;
19016
+ }
19017
+ }
19018
+ async close() {
19019
+ const cmd = ["acpx", this.agentName, "sessions", "close", this.sessionName];
19020
+ getSafeLogger()?.debug("acp-adapter", `Closing session: ${this.sessionName}`);
19021
+ const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
19022
+ const exitCode = await proc.exited;
19023
+ if (exitCode !== 0) {
19024
+ const stderr = await new Response(proc.stderr).text();
19025
+ getSafeLogger()?.warn("acp-adapter", "Failed to close session", {
19026
+ sessionName: this.sessionName,
19027
+ stderr: stderr.slice(0, 200)
19028
+ });
19029
+ }
19030
+ }
19031
+ async cancelActivePrompt() {
19032
+ const cmd = ["acpx", this.agentName, "cancel"];
19033
+ getSafeLogger()?.debug("acp-adapter", `Cancelling active prompt: ${this.sessionName}`);
19034
+ const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
19035
+ await proc.exited;
19036
+ }
19037
+ }
19038
+
19039
+ class SpawnAcpClient {
19040
+ agentName;
19041
+ model;
19042
+ cwd;
19043
+ timeoutSeconds;
19044
+ env;
19045
+ constructor(cmdStr, cwd, timeoutSeconds) {
19046
+ const parts = cmdStr.split(/\s+/);
19047
+ const modelIdx = parts.indexOf("--model");
19048
+ this.model = modelIdx >= 0 && parts[modelIdx + 1] ? parts[modelIdx + 1] : "default";
19049
+ this.agentName = parts[parts.length - 1] || "claude";
19050
+ this.cwd = cwd || process.cwd();
19051
+ this.timeoutSeconds = timeoutSeconds || 1800;
19052
+ this.env = buildAllowedEnv2();
19053
+ }
19054
+ async start() {}
19055
+ async createSession(opts) {
19056
+ const sessionName = opts.sessionName || `nax-${Date.now()}`;
19057
+ const cmd = ["acpx", opts.agentName, "sessions", "ensure", "--name", sessionName];
19058
+ getSafeLogger()?.debug("acp-adapter", `Ensuring session: ${sessionName}`);
19059
+ const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
19060
+ const exitCode = await proc.exited;
19061
+ if (exitCode !== 0) {
19062
+ const stderr = await new Response(proc.stderr).text();
19063
+ throw new Error(`[acp-adapter] Failed to create session: ${stderr || `exit code ${exitCode}`}`);
19064
+ }
19065
+ return new SpawnAcpSession({
19066
+ agentName: opts.agentName,
19067
+ sessionName,
19068
+ cwd: this.cwd,
19069
+ model: this.model,
19070
+ timeoutSeconds: this.timeoutSeconds,
19071
+ permissionMode: opts.permissionMode,
19072
+ env: this.env
19073
+ });
19074
+ }
19075
+ async loadSession(sessionName) {
19076
+ const cmd = ["acpx", this.agentName, "sessions", "ensure", "--name", sessionName];
19077
+ const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
19078
+ const exitCode = await proc.exited;
19079
+ if (exitCode !== 0) {
19080
+ return null;
19081
+ }
19082
+ return new SpawnAcpSession({
19083
+ agentName: this.agentName,
19084
+ sessionName,
19085
+ cwd: this.cwd,
19086
+ model: this.model,
19087
+ timeoutSeconds: this.timeoutSeconds,
19088
+ permissionMode: "approve-all",
19089
+ env: this.env
19090
+ });
19091
+ }
19092
+ async close() {}
19093
+ }
19094
+ function createSpawnAcpClient(cmdStr, cwd, timeoutSeconds) {
19095
+ return new SpawnAcpClient(cmdStr, cwd, timeoutSeconds);
19096
+ }
19097
+ var _spawnClientDeps;
19098
+ var init_spawn_client = __esm(() => {
19099
+ init_logger2();
19100
+ _spawnClientDeps = {
19101
+ spawn(cmd, opts) {
19102
+ return Bun.spawn(cmd, opts);
19103
+ }
19104
+ };
19105
+ });
19106
+
19107
+ // src/agents/acp/cost.ts
19108
+ function estimateCostFromTokenUsage(usage, model) {
19109
+ const pricing = MODEL_PRICING[model];
19110
+ if (!pricing) {
19111
+ const fallbackInputRate = 3 / 1e6;
19112
+ const fallbackOutputRate = 15 / 1e6;
19113
+ const inputCost2 = (usage.input_tokens ?? 0) * fallbackInputRate;
19114
+ const outputCost2 = (usage.output_tokens ?? 0) * fallbackOutputRate;
19115
+ const cacheReadCost2 = (usage.cache_read_input_tokens ?? 0) * (0.5 / 1e6);
19116
+ const cacheCreationCost2 = (usage.cache_creation_input_tokens ?? 0) * (2 / 1e6);
19117
+ return inputCost2 + outputCost2 + cacheReadCost2 + cacheCreationCost2;
19118
+ }
19119
+ const inputRate = pricing.input / 1e6;
19120
+ const outputRate = pricing.output / 1e6;
19121
+ const cacheReadRate = (pricing.cacheRead ?? pricing.input * 0.1) / 1e6;
19122
+ const cacheCreationRate = (pricing.cacheCreation ?? pricing.input * 0.33) / 1e6;
19123
+ const inputCost = (usage.input_tokens ?? 0) * inputRate;
19124
+ const outputCost = (usage.output_tokens ?? 0) * outputRate;
19125
+ const cacheReadCost = (usage.cache_read_input_tokens ?? 0) * cacheReadRate;
19126
+ const cacheCreationCost = (usage.cache_creation_input_tokens ?? 0) * cacheCreationRate;
19127
+ return inputCost + outputCost + cacheReadCost + cacheCreationCost;
19128
+ }
19129
+ var MODEL_PRICING;
19130
+ var init_cost2 = __esm(() => {
19131
+ MODEL_PRICING = {
19132
+ "claude-sonnet-4": { input: 3, output: 15 },
19133
+ "claude-sonnet-4-5": { input: 3, output: 15 },
19134
+ "claude-haiku": { input: 0.8, output: 4, cacheRead: 0.1, cacheCreation: 1 },
19135
+ "claude-haiku-4-5": { input: 0.8, output: 4, cacheRead: 0.1, cacheCreation: 1 },
19136
+ "claude-opus": { input: 15, output: 75 },
19137
+ "claude-opus-4": { input: 15, output: 75 },
19138
+ "gpt-4.1": { input: 10, output: 30 },
19139
+ "gpt-4": { input: 30, output: 60 },
19140
+ "gpt-3.5-turbo": { input: 0.5, output: 1.5 },
19141
+ "gemini-2.5-pro": { input: 0.075, output: 0.3 },
19142
+ "gemini-2-pro": { input: 0.075, output: 0.3 },
19143
+ codex: { input: 0.02, output: 0.06 },
19144
+ "code-davinci-002": { input: 0.02, output: 0.06 }
19145
+ };
19146
+ });
19147
+
19148
+ // src/agents/acp/adapter.ts
19149
+ import { createHash } from "crypto";
19150
+ import { join as join3 } from "path";
19151
+ function resolveRegistryEntry(agentName) {
19152
+ return AGENT_REGISTRY[agentName] ?? DEFAULT_ENTRY;
19153
+ }
19154
+ function isRateLimitError(err) {
19155
+ if (!(err instanceof Error))
19156
+ return false;
19157
+ const msg = err.message.toLowerCase();
19158
+ return msg.includes("rate limit") || msg.includes("rate_limit") || msg.includes("429");
19159
+ }
19160
+ function buildSessionName(workdir, featureName, storyId, sessionRole) {
19161
+ const hash2 = createHash("sha256").update(workdir).digest("hex").slice(0, 8);
19162
+ const sanitize = (s) => s.replace(/[^a-z0-9]+/gi, "-").toLowerCase().replace(/^-+|-+$/g, "");
19163
+ const parts = ["nax", hash2];
19164
+ if (featureName)
19165
+ parts.push(sanitize(featureName));
19166
+ if (storyId)
19167
+ parts.push(sanitize(storyId));
19168
+ if (sessionRole)
19169
+ parts.push(sanitize(sessionRole));
19170
+ return parts.join("-");
19171
+ }
19172
+ async function ensureAcpSession(client, sessionName, agentName, permissionMode) {
19173
+ if (client.loadSession) {
19174
+ try {
19175
+ const existing = await client.loadSession(sessionName);
19176
+ if (existing) {
19177
+ getSafeLogger()?.debug("acp-adapter", `Resumed existing session: ${sessionName}`);
19178
+ return existing;
19179
+ }
19180
+ } catch {}
19181
+ }
19182
+ getSafeLogger()?.debug("acp-adapter", `Creating new session: ${sessionName}`);
19183
+ return client.createSession({ agentName, permissionMode, sessionName });
19184
+ }
19185
+ async function runSessionPrompt(session, prompt, timeoutMs) {
19186
+ const promptPromise = session.prompt(prompt);
19187
+ const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve("timeout"), timeoutMs));
19188
+ const winner = await Promise.race([promptPromise, timeoutPromise]);
19189
+ if (winner === "timeout") {
19190
+ try {
19191
+ await session.cancelActivePrompt();
19192
+ } catch {
19193
+ await session.close().catch(() => {});
19194
+ }
19195
+ return { response: null, timedOut: true };
19196
+ }
19197
+ return { response: winner, timedOut: false };
19198
+ }
19199
+ async function closeAcpSession(session) {
19200
+ try {
19201
+ await session.close();
19202
+ } catch (err) {
19203
+ getSafeLogger()?.warn("acp-adapter", "Failed to close session", { error: String(err) });
19204
+ }
19205
+ }
19206
+ function acpSessionsPath(workdir, featureName) {
19207
+ return join3(workdir, "nax", "features", featureName, "acp-sessions.json");
19208
+ }
19209
+ async function saveAcpSession(workdir, featureName, storyId, sessionName) {
19210
+ try {
19211
+ const path = acpSessionsPath(workdir, featureName);
19212
+ let data = {};
19213
+ try {
19214
+ const existing = await Bun.file(path).text();
19215
+ data = JSON.parse(existing);
19216
+ } catch {}
19217
+ data[storyId] = sessionName;
19218
+ await Bun.write(path, JSON.stringify(data, null, 2));
19219
+ } catch (err) {
19220
+ getSafeLogger()?.warn("acp-adapter", "Failed to save session to sidecar", { error: String(err) });
19221
+ }
19222
+ }
19223
+ async function clearAcpSession(workdir, featureName, storyId) {
19224
+ try {
19225
+ const path = acpSessionsPath(workdir, featureName);
19226
+ let data = {};
19227
+ try {
19228
+ const existing = await Bun.file(path).text();
19229
+ data = JSON.parse(existing);
19230
+ } catch {
19231
+ return;
19232
+ }
19233
+ delete data[storyId];
19234
+ await Bun.write(path, JSON.stringify(data, null, 2));
19235
+ } catch (err) {
19236
+ getSafeLogger()?.warn("acp-adapter", "Failed to clear session from sidecar", { error: String(err) });
19237
+ }
19238
+ }
19239
+ async function readAcpSession(workdir, featureName, storyId) {
19240
+ try {
19241
+ const path = acpSessionsPath(workdir, featureName);
19242
+ const existing = await Bun.file(path).text();
19243
+ const data = JSON.parse(existing);
19244
+ return data[storyId] ?? null;
19245
+ } catch {
19246
+ return null;
19247
+ }
19248
+ }
19249
+ function extractOutput(response) {
19250
+ if (!response)
19251
+ return "";
19252
+ return response.messages.filter((m) => m.role === "assistant").map((m) => m.content).join(`
19253
+ `).trim();
19254
+ }
19255
+ function extractQuestion(output) {
19256
+ const text = output.trim();
19257
+ if (!text)
19258
+ return null;
19259
+ const sentences = text.split(/(?<=[.!?])\s+/);
19260
+ const questionSentences = sentences.filter((s) => s.trim().endsWith("?"));
19261
+ if (questionSentences.length > 0) {
19262
+ const q = questionSentences[questionSentences.length - 1].trim();
19263
+ if (q.length > 10)
19264
+ return q;
19265
+ }
19266
+ const lower = text.toLowerCase();
19267
+ const markers = [
19268
+ "please confirm",
19269
+ "please specify",
19270
+ "please provide",
19271
+ "which would you",
19272
+ "should i ",
19273
+ "do you want",
19274
+ "can you clarify"
19275
+ ];
19276
+ for (const marker of markers) {
19277
+ if (lower.includes(marker)) {
19278
+ return text.slice(-200).trim();
19279
+ }
19280
+ }
19281
+ return null;
19282
+ }
19283
+
19284
+ class AcpAgentAdapter {
19285
+ name;
19286
+ displayName;
19287
+ binary;
19288
+ capabilities;
19289
+ constructor(agentName) {
19290
+ const entry = resolveRegistryEntry(agentName);
19291
+ this.name = agentName;
19292
+ this.displayName = entry.displayName;
19293
+ this.binary = entry.binary;
19294
+ this.capabilities = {
19295
+ supportedTiers: entry.supportedTiers,
19296
+ maxContextTokens: entry.maxContextTokens,
19297
+ features: new Set(["tdd", "review", "refactor"])
19298
+ };
19299
+ }
19300
+ async isInstalled() {
19301
+ const path = _acpAdapterDeps.which(this.binary);
19302
+ return path !== null;
19303
+ }
19304
+ buildCommand(_options) {
19305
+ return ["acpx", this.name, "session"];
19306
+ }
19307
+ buildAllowedEnv(_options) {
19308
+ return {};
19309
+ }
19310
+ async run(options) {
19311
+ const startTime = Date.now();
19312
+ let lastError;
19313
+ getSafeLogger()?.debug("acp-adapter", `Starting run for ${this.name}`, {
19314
+ model: options.modelDef.model,
19315
+ workdir: options.workdir,
19316
+ featureName: options.featureName,
19317
+ storyId: options.storyId,
19318
+ sessionRole: options.sessionRole
19319
+ });
19320
+ for (let attempt = 0;attempt < MAX_RATE_LIMIT_RETRIES; attempt++) {
19321
+ try {
19322
+ const result = await this._runWithClient(options, startTime);
19323
+ if (!result.success) {
19324
+ getSafeLogger()?.warn("acp-adapter", `Run failed for ${this.name}`, { exitCode: result.exitCode });
19325
+ }
19326
+ return result;
19327
+ } catch (err) {
19328
+ const error48 = err instanceof Error ? err : new Error(String(err));
19329
+ lastError = error48;
19330
+ const shouldRetry = isRateLimitError(error48) && attempt < MAX_RATE_LIMIT_RETRIES - 1;
19331
+ if (!shouldRetry)
19332
+ break;
19333
+ const backoffMs = 2 ** (attempt + 1) * 1000;
19334
+ getSafeLogger()?.warn("acp-adapter", "Retrying after rate limit", {
19335
+ backoffSeconds: backoffMs / 1000,
19336
+ attempt: attempt + 1
19337
+ });
19338
+ await _acpAdapterDeps.sleep(backoffMs);
19339
+ }
19340
+ }
19341
+ const durationMs = Date.now() - startTime;
19342
+ return {
19343
+ success: false,
19344
+ exitCode: 1,
19345
+ output: lastError?.message ?? "Run failed",
19346
+ rateLimited: isRateLimitError(lastError),
19347
+ durationMs,
19348
+ estimatedCost: 0
19349
+ };
19350
+ }
19351
+ async _runWithClient(options, startTime) {
19352
+ const cmdStr = `acpx --model ${options.modelDef.model} ${this.name}`;
19353
+ const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds);
19354
+ await client.start();
19355
+ let sessionName = options.acpSessionName;
19356
+ if (!sessionName && options.featureName && options.storyId) {
19357
+ sessionName = await readAcpSession(options.workdir, options.featureName, options.storyId) ?? undefined;
19358
+ }
19359
+ sessionName ??= buildSessionName(options.workdir, options.featureName, options.storyId, options.sessionRole);
19360
+ const permissionMode = options.dangerouslySkipPermissions ? "approve-all" : "default";
19361
+ const session = await ensureAcpSession(client, sessionName, this.name, permissionMode);
19362
+ if (options.featureName && options.storyId) {
19363
+ await saveAcpSession(options.workdir, options.featureName, options.storyId, sessionName);
19364
+ }
19365
+ let lastResponse = null;
19366
+ let timedOut = false;
19367
+ const totalTokenUsage = { input_tokens: 0, output_tokens: 0 };
19368
+ try {
19369
+ let currentPrompt = options.prompt;
19370
+ let turnCount = 0;
19371
+ const MAX_TURNS = options.interactionBridge ? 10 : 1;
19372
+ while (turnCount < MAX_TURNS) {
19373
+ turnCount++;
19374
+ getSafeLogger()?.debug("acp-adapter", `Session turn ${turnCount}/${MAX_TURNS}`, { sessionName });
19375
+ const turnResult = await runSessionPrompt(session, currentPrompt, options.timeoutSeconds * 1000);
19376
+ if (turnResult.timedOut) {
19377
+ timedOut = true;
19378
+ break;
19379
+ }
19380
+ lastResponse = turnResult.response;
19381
+ if (!lastResponse)
19382
+ break;
19383
+ if (lastResponse.cumulative_token_usage) {
19384
+ totalTokenUsage.input_tokens += lastResponse.cumulative_token_usage.input_tokens ?? 0;
19385
+ totalTokenUsage.output_tokens += lastResponse.cumulative_token_usage.output_tokens ?? 0;
19386
+ }
19387
+ const outputText = extractOutput(lastResponse);
19388
+ const question = extractQuestion(outputText);
19389
+ if (!question || !options.interactionBridge)
19390
+ break;
19391
+ getSafeLogger()?.debug("acp-adapter", "Agent asked question, routing to interactionBridge", { question });
19392
+ try {
19393
+ const answer = await Promise.race([
19394
+ options.interactionBridge.onQuestionDetected(question),
19395
+ new Promise((_, reject) => setTimeout(() => reject(new Error("interaction timeout")), INTERACTION_TIMEOUT_MS))
19396
+ ]);
19397
+ currentPrompt = answer;
19398
+ } catch (err) {
19399
+ const msg = err instanceof Error ? err.message : String(err);
19400
+ getSafeLogger()?.warn("acp-adapter", `InteractionBridge failed: ${msg}`);
19401
+ break;
19402
+ }
19403
+ }
19404
+ if (turnCount >= MAX_TURNS) {
19405
+ getSafeLogger()?.warn("acp-adapter", "Reached max turns limit", { sessionName, maxTurns: MAX_TURNS });
19406
+ }
19407
+ } finally {
19408
+ await closeAcpSession(session);
19409
+ await client.close().catch(() => {});
19410
+ if (options.featureName && options.storyId) {
19411
+ await clearAcpSession(options.workdir, options.featureName, options.storyId);
19412
+ }
19413
+ }
19414
+ const durationMs = Date.now() - startTime;
19415
+ if (timedOut) {
19416
+ return {
19417
+ success: false,
19418
+ exitCode: 124,
19419
+ output: `Session timed out after ${options.timeoutSeconds}s`,
19420
+ rateLimited: false,
19421
+ durationMs,
19422
+ estimatedCost: 0
19423
+ };
19424
+ }
19425
+ const success2 = lastResponse?.stopReason === "end_turn";
19426
+ const output = extractOutput(lastResponse);
19427
+ const estimatedCost = totalTokenUsage.input_tokens > 0 || totalTokenUsage.output_tokens > 0 ? estimateCostFromTokenUsage(totalTokenUsage, options.modelDef.model) : 0;
19428
+ return {
19429
+ success: success2,
19430
+ exitCode: success2 ? 0 : 1,
19431
+ output: output.slice(-MAX_AGENT_OUTPUT_CHARS2),
19432
+ rateLimited: false,
19433
+ durationMs,
19434
+ estimatedCost
19435
+ };
19436
+ }
19437
+ async complete(prompt, _options) {
19438
+ const model = _options?.model ?? "default";
19439
+ const cmdStr = `acpx --model ${model} ${this.name}`;
19440
+ const client = _acpAdapterDeps.createClient(cmdStr);
19441
+ await client.start();
19442
+ const permissionMode = _options?.dangerouslySkipPermissions ? "approve-all" : "default";
19443
+ let session = null;
19444
+ try {
19445
+ session = await client.createSession({ agentName: this.name, permissionMode });
19446
+ const response = await session.prompt(prompt);
19447
+ if (response.stopReason === "error") {
19448
+ throw new CompleteError("complete() failed: stop reason is error");
19449
+ }
19450
+ const text = response.messages.filter((m) => m.role === "assistant").map((m) => m.content).join(`
19451
+ `).trim();
19452
+ if (!text) {
19453
+ throw new CompleteError("complete() returned empty output");
19454
+ }
19455
+ return text;
19456
+ } finally {
19457
+ if (session) {
19458
+ await session.close().catch(() => {});
19459
+ }
19460
+ await client.close().catch(() => {});
19461
+ }
19462
+ }
19463
+ async plan(options) {
19464
+ if (options.interactive) {
19465
+ throw new Error("[acp-adapter] plan() interactive mode is not yet supported via ACP");
19466
+ }
19467
+ const modelDef = options.modelDef ?? { provider: "anthropic", model: "default" };
19468
+ const timeoutSeconds = options.timeoutSeconds ?? options.config?.execution?.sessionTimeoutSeconds ?? 600;
19469
+ const result = await this.run({
19470
+ prompt: options.prompt,
19471
+ workdir: options.workdir,
19472
+ modelTier: options.modelTier ?? "balanced",
19473
+ modelDef,
19474
+ timeoutSeconds,
19475
+ dangerouslySkipPermissions: options.dangerouslySkipPermissions ?? false,
19476
+ interactionBridge: options.interactionBridge,
19477
+ featureName: options.featureName,
19478
+ storyId: options.storyId,
19479
+ sessionRole: options.sessionRole
19480
+ });
19481
+ if (!result.success) {
19482
+ throw new Error(`[acp-adapter] plan() failed: ${result.output}`);
19483
+ }
19484
+ const specContent = result.output.trim();
19485
+ if (!specContent) {
19486
+ throw new Error("[acp-adapter] plan() returned empty spec content");
19487
+ }
19488
+ return { specContent };
19489
+ }
19490
+ async decompose(options) {
19491
+ const model = options.modelDef?.model;
19492
+ const prompt = buildDecomposePrompt(options);
19493
+ let output;
19494
+ try {
19495
+ output = await this.complete(prompt, { model, jsonMode: true });
19496
+ } catch (err) {
19497
+ const msg = err instanceof Error ? err.message : String(err);
19498
+ throw new Error(`[acp-adapter] decompose() failed: ${msg}`, { cause: err });
19499
+ }
19500
+ let stories;
19501
+ try {
19502
+ stories = parseDecomposeOutput(output);
19503
+ } catch (err) {
19504
+ throw new Error(`[acp-adapter] decompose() failed to parse stories: ${err.message}`, { cause: err });
19505
+ }
19506
+ return { stories };
19507
+ }
19508
+ }
19509
+ var MAX_AGENT_OUTPUT_CHARS2 = 5000, MAX_RATE_LIMIT_RETRIES = 3, INTERACTION_TIMEOUT_MS, AGENT_REGISTRY, DEFAULT_ENTRY, _acpAdapterDeps;
19510
+ var init_adapter = __esm(() => {
19511
+ init_logger2();
19512
+ init_spawn_client();
19513
+ init_types2();
19514
+ init_cost2();
19515
+ INTERACTION_TIMEOUT_MS = 5 * 60 * 1000;
19516
+ AGENT_REGISTRY = {
19517
+ claude: {
19518
+ binary: "claude",
19519
+ displayName: "Claude Code (ACP)",
19520
+ supportedTiers: ["fast", "balanced", "powerful"],
19521
+ maxContextTokens: 200000
19522
+ },
19523
+ codex: {
19524
+ binary: "codex",
19525
+ displayName: "OpenAI Codex (ACP)",
19526
+ supportedTiers: ["fast", "balanced"],
19527
+ maxContextTokens: 128000
19528
+ },
19529
+ gemini: {
19530
+ binary: "gemini",
19531
+ displayName: "Gemini CLI (ACP)",
19532
+ supportedTiers: ["fast", "balanced", "powerful"],
19533
+ maxContextTokens: 1e6
19534
+ }
19535
+ };
19536
+ DEFAULT_ENTRY = {
19537
+ binary: "claude",
19538
+ displayName: "ACP Agent",
19539
+ supportedTiers: ["balanced"],
19540
+ maxContextTokens: 128000
19541
+ };
19542
+ _acpAdapterDeps = {
19543
+ which(name) {
19544
+ return Bun.which(name);
19545
+ },
19546
+ async sleep(ms) {
19547
+ await Bun.sleep(ms);
19548
+ },
19549
+ createClient(cmdStr, cwd, timeoutSeconds) {
19550
+ return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds);
19551
+ }
19552
+ };
19553
+ });
19554
+
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.41.0",
21119
21811
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
21120
21812
  type: "module",
21121
21813
  bin: {
@@ -21179,8 +21871,8 @@ var init_version = __esm(() => {
21179
21871
  NAX_VERSION = package_default.version;
21180
21872
  NAX_COMMIT = (() => {
21181
21873
  try {
21182
- if (/^[0-9a-f]{6,10}$/.test("ba3f634"))
21183
- return "ba3f634";
21874
+ if (/^[0-9a-f]{6,10}$/.test("0db1c5f"))
21875
+ return "0db1c5f";
21184
21876
  } catch {}
21185
21877
  try {
21186
21878
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -23460,10 +24152,10 @@ var init_autofix = __esm(() => {
23460
24152
 
23461
24153
  // src/execution/progress.ts
23462
24154
  import { mkdirSync as mkdirSync2 } from "fs";
23463
- import { join as join14 } from "path";
24155
+ import { join as join15 } from "path";
23464
24156
  async function appendProgress(featureDir, storyId, status, message) {
23465
24157
  mkdirSync2(featureDir, { recursive: true });
23466
- const progressPath = join14(featureDir, "progress.txt");
24158
+ const progressPath = join15(featureDir, "progress.txt");
23467
24159
  const timestamp = new Date().toISOString();
23468
24160
  const entry = `[${timestamp}] ${storyId} \u2014 ${status.toUpperCase()} \u2014 ${message}
23469
24161
  `;
@@ -23547,7 +24239,7 @@ function estimateTokens(text) {
23547
24239
 
23548
24240
  // src/constitution/loader.ts
23549
24241
  import { existsSync as existsSync13 } from "fs";
23550
- import { join as join15 } from "path";
24242
+ import { join as join16 } from "path";
23551
24243
  function truncateToTokens(text, maxTokens) {
23552
24244
  const maxChars = maxTokens * 3;
23553
24245
  if (text.length <= maxChars) {
@@ -23569,7 +24261,7 @@ async function loadConstitution(projectDir, config2) {
23569
24261
  }
23570
24262
  let combinedContent = "";
23571
24263
  if (!config2.skipGlobal) {
23572
- const globalPath = join15(globalConfigDir(), config2.path);
24264
+ const globalPath = join16(globalConfigDir(), config2.path);
23573
24265
  if (existsSync13(globalPath)) {
23574
24266
  const validatedPath = validateFilePath(globalPath, globalConfigDir());
23575
24267
  const globalFile = Bun.file(validatedPath);
@@ -23579,7 +24271,7 @@ async function loadConstitution(projectDir, config2) {
23579
24271
  }
23580
24272
  }
23581
24273
  }
23582
- const projectPath = join15(projectDir, config2.path);
24274
+ const projectPath = join16(projectDir, config2.path);
23583
24275
  if (existsSync13(projectPath)) {
23584
24276
  const validatedPath = validateFilePath(projectPath, projectDir);
23585
24277
  const projectFile = Bun.file(validatedPath);
@@ -24605,6 +25297,15 @@ ${pluginMarkdown}` : pluginMarkdown;
24605
25297
  function validateAgentForTier(agent, tier) {
24606
25298
  return agent.capabilities.supportedTiers.includes(tier);
24607
25299
  }
25300
+ function validateAgentFeature(agent, feature) {
25301
+ return agent.capabilities.features.has(feature);
25302
+ }
25303
+ function describeAgentCapabilities(agent) {
25304
+ const tiers = agent.capabilities.supportedTiers.join(",");
25305
+ const features = Array.from(agent.capabilities.features).join(",");
25306
+ const maxTokens = agent.capabilities.maxContextTokens;
25307
+ return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
25308
+ }
24608
25309
 
24609
25310
  // src/agents/version-detection.ts
24610
25311
  async function getAgentVersion(binaryName) {
@@ -24655,6 +25356,26 @@ var init_version_detection = __esm(() => {
24655
25356
  });
24656
25357
 
24657
25358
  // src/agents/index.ts
25359
+ var exports_agents = {};
25360
+ __export(exports_agents, {
25361
+ validateAgentForTier: () => validateAgentForTier,
25362
+ validateAgentFeature: () => validateAgentFeature,
25363
+ parseTokenUsage: () => parseTokenUsage,
25364
+ getInstalledAgents: () => getInstalledAgents,
25365
+ getAllAgentNames: () => getAllAgentNames,
25366
+ getAgentVersions: () => getAgentVersions,
25367
+ getAgentVersion: () => getAgentVersion,
25368
+ getAgent: () => getAgent,
25369
+ formatCostWithConfidence: () => formatCostWithConfidence,
25370
+ estimateCostFromOutput: () => estimateCostFromOutput,
25371
+ estimateCostByDuration: () => estimateCostByDuration,
25372
+ estimateCost: () => estimateCost,
25373
+ describeAgentCapabilities: () => describeAgentCapabilities,
25374
+ checkAgentHealth: () => checkAgentHealth,
25375
+ CompleteError: () => CompleteError,
25376
+ ClaudeCodeAdapter: () => ClaudeCodeAdapter,
25377
+ COST_RATES: () => COST_RATES
25378
+ });
24658
25379
  var init_agents = __esm(() => {
24659
25380
  init_types2();
24660
25381
  init_claude();
@@ -24732,14 +25453,14 @@ var init_isolation = __esm(() => {
24732
25453
 
24733
25454
  // src/context/greenfield.ts
24734
25455
  import { readdir } from "fs/promises";
24735
- import { join as join16 } from "path";
25456
+ import { join as join17 } from "path";
24736
25457
  async function scanForTestFiles(dir, testPattern, isRootCall = true) {
24737
25458
  const results = [];
24738
25459
  const ignoreDirs = new Set(["node_modules", "dist", "build", ".next", ".git"]);
24739
25460
  try {
24740
25461
  const entries = await readdir(dir, { withFileTypes: true });
24741
25462
  for (const entry of entries) {
24742
- const fullPath = join16(dir, entry.name);
25463
+ const fullPath = join17(dir, entry.name);
24743
25464
  if (entry.isDirectory()) {
24744
25465
  if (ignoreDirs.has(entry.name))
24745
25466
  continue;
@@ -25127,13 +25848,13 @@ function parseTestOutput(output, exitCode) {
25127
25848
 
25128
25849
  // src/verification/runners.ts
25129
25850
  import { existsSync as existsSync14 } from "fs";
25130
- import { join as join17 } from "path";
25851
+ import { join as join18 } from "path";
25131
25852
  async function verifyAssets(workingDirectory, expectedFiles) {
25132
25853
  if (!expectedFiles || expectedFiles.length === 0)
25133
25854
  return { success: true, missingFiles: [] };
25134
25855
  const missingFiles = [];
25135
25856
  for (const file2 of expectedFiles) {
25136
- if (!existsSync14(join17(workingDirectory, file2)))
25857
+ if (!existsSync14(join18(workingDirectory, file2)))
25137
25858
  missingFiles.push(file2);
25138
25859
  }
25139
25860
  if (missingFiles.length > 0) {
@@ -25345,7 +26066,7 @@ var init_prompts = __esm(() => {
25345
26066
  });
25346
26067
 
25347
26068
  // src/tdd/rectification-gate.ts
25348
- async function runFullSuiteGate(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger) {
26069
+ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, featureName) {
25349
26070
  const rectificationEnabled = config2.execution.rectification?.enabled ?? false;
25350
26071
  if (!rectificationEnabled)
25351
26072
  return false;
@@ -25361,7 +26082,7 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
25361
26082
  if (!fullSuitePassed && fullSuiteResult.output) {
25362
26083
  const testSummary = parseBunTestOutput(fullSuiteResult.output);
25363
26084
  if (testSummary.failed > 0) {
25364
- return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout);
26085
+ return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout, featureName);
25365
26086
  }
25366
26087
  if (testSummary.passed > 0) {
25367
26088
  logger.info("tdd", "Full suite gate passed (non-zero exit, 0 failures, tests detected)", {
@@ -25389,7 +26110,7 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
25389
26110
  });
25390
26111
  return false;
25391
26112
  }
25392
- async function runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout) {
26113
+ async function runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout, featureName) {
25393
26114
  const rectificationState = {
25394
26115
  attempt: 0,
25395
26116
  initialFailures: testSummary.failed,
@@ -25411,7 +26132,10 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
25411
26132
  modelTier: implementerTier,
25412
26133
  modelDef: resolveModel(config2.models[implementerTier]),
25413
26134
  timeoutSeconds: config2.execution.sessionTimeoutSeconds,
25414
- dangerouslySkipPermissions: config2.execution.dangerouslySkipPermissions
26135
+ dangerouslySkipPermissions: config2.execution.dangerouslySkipPermissions,
26136
+ featureName,
26137
+ storyId: story.id,
26138
+ sessionRole: "implementer"
25415
26139
  });
25416
26140
  if (!rectifyResult.success && rectifyResult.pid) {
25417
26141
  await cleanupProcessTree(rectifyResult.pid);
@@ -25808,13 +26532,13 @@ var exports_loader = {};
25808
26532
  __export(exports_loader, {
25809
26533
  loadOverride: () => loadOverride
25810
26534
  });
25811
- import { join as join18 } from "path";
26535
+ import { join as join19 } from "path";
25812
26536
  async function loadOverride(role, workdir, config2) {
25813
26537
  const overridePath = config2.prompts?.overrides?.[role];
25814
26538
  if (!overridePath) {
25815
26539
  return null;
25816
26540
  }
25817
- const absolutePath = join18(workdir, overridePath);
26541
+ const absolutePath = join19(workdir, overridePath);
25818
26542
  const file2 = Bun.file(absolutePath);
25819
26543
  if (!await file2.exists()) {
25820
26544
  return null;
@@ -25993,7 +26717,7 @@ async function rollbackToRef(workdir, ref) {
25993
26717
  }
25994
26718
  logger.info("tdd", "Successfully rolled back git changes", { ref });
25995
26719
  }
25996
- async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution) {
26720
+ async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution, featureName) {
25997
26721
  const startTime = Date.now();
25998
26722
  let prompt;
25999
26723
  switch (role) {
@@ -26015,7 +26739,10 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
26015
26739
  modelTier,
26016
26740
  modelDef: resolveModel(config2.models[modelTier]),
26017
26741
  timeoutSeconds: config2.execution.sessionTimeoutSeconds,
26018
- dangerouslySkipPermissions: config2.execution.dangerouslySkipPermissions
26742
+ dangerouslySkipPermissions: config2.execution.dangerouslySkipPermissions,
26743
+ featureName,
26744
+ storyId: story.id,
26745
+ sessionRole: role
26019
26746
  });
26020
26747
  if (!result.success && result.pid) {
26021
26748
  await cleanupProcessTree(result.pid);
@@ -26363,6 +27090,7 @@ async function runThreeSessionTdd(options) {
26363
27090
  config: config2,
26364
27091
  workdir,
26365
27092
  modelTier,
27093
+ featureName,
26366
27094
  contextMarkdown,
26367
27095
  constitution,
26368
27096
  dryRun = false,
@@ -26426,7 +27154,7 @@ async function runThreeSessionTdd(options) {
26426
27154
  let session1;
26427
27155
  if (!isRetry) {
26428
27156
  const testWriterTier = config2.tdd.sessionTiers?.testWriter ?? "balanced";
26429
- session1 = await runTddSession("test-writer", agent, story, config2, workdir, testWriterTier, session1Ref, contextMarkdown, lite, lite, constitution);
27157
+ session1 = await runTddSession("test-writer", agent, story, config2, workdir, testWriterTier, session1Ref, contextMarkdown, lite, lite, constitution, featureName);
26430
27158
  sessions.push(session1);
26431
27159
  }
26432
27160
  if (session1 && !session1.success) {
@@ -26488,7 +27216,7 @@ async function runThreeSessionTdd(options) {
26488
27216
  });
26489
27217
  const session2Ref = await captureGitRef(workdir) ?? "HEAD";
26490
27218
  const implementerTier = config2.tdd.sessionTiers?.implementer ?? modelTier;
26491
- const session2 = await runTddSession("implementer", agent, story, config2, workdir, implementerTier, session2Ref, contextMarkdown, lite, lite, constitution);
27219
+ const session2 = await runTddSession("implementer", agent, story, config2, workdir, implementerTier, session2Ref, contextMarkdown, lite, lite, constitution, featureName);
26492
27220
  sessions.push(session2);
26493
27221
  if (!session2.success) {
26494
27222
  needsHumanReview = true;
@@ -26504,10 +27232,10 @@ async function runThreeSessionTdd(options) {
26504
27232
  lite
26505
27233
  };
26506
27234
  }
26507
- const fullSuiteGatePassed = await runFullSuiteGate(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger);
27235
+ const fullSuiteGatePassed = await runFullSuiteGate(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, featureName);
26508
27236
  const session3Ref = await captureGitRef(workdir) ?? "HEAD";
26509
27237
  const verifierTier = config2.tdd.sessionTiers?.verifier ?? "fast";
26510
- const session3 = await runTddSession("verifier", agent, story, config2, workdir, verifierTier, session3Ref, undefined, false, false, constitution);
27238
+ const session3 = await runTddSession("verifier", agent, story, config2, workdir, verifierTier, session3Ref, undefined, false, false, constitution, featureName);
26511
27239
  sessions.push(session3);
26512
27240
  const verdict = await readVerdict(workdir);
26513
27241
  await cleanupVerdict(workdir);
@@ -26666,7 +27394,7 @@ var init_execution = __esm(() => {
26666
27394
  enabled: () => true,
26667
27395
  async execute(ctx) {
26668
27396
  const logger = getLogger();
26669
- const agent = _executionDeps.getAgent(ctx.config.autoMode.defaultAgent);
27397
+ const agent = (ctx.agentGetFn ?? _executionDeps.getAgent)(ctx.config.autoMode.defaultAgent);
26670
27398
  if (!agent) {
26671
27399
  return {
26672
27400
  action: "fail",
@@ -26686,6 +27414,7 @@ var init_execution = __esm(() => {
26686
27414
  config: ctx.config,
26687
27415
  workdir: ctx.workdir,
26688
27416
  modelTier: ctx.routing.modelTier,
27417
+ featureName: ctx.prd.feature,
26689
27418
  contextMarkdown: ctx.contextMarkdown,
26690
27419
  constitution: ctx.constitution?.content,
26691
27420
  dryRun: false,
@@ -26755,7 +27484,38 @@ Category: ${tddResult.failureCategory ?? "unknown"}`,
26755
27484
  modelTier: ctx.routing.modelTier,
26756
27485
  modelDef: resolveModel(ctx.config.models[ctx.routing.modelTier]),
26757
27486
  timeoutSeconds: ctx.config.execution.sessionTimeoutSeconds,
26758
- dangerouslySkipPermissions: ctx.config.execution.dangerouslySkipPermissions
27487
+ dangerouslySkipPermissions: ctx.config.execution.dangerouslySkipPermissions,
27488
+ pidRegistry: ctx.pidRegistry,
27489
+ featureName: ctx.prd.feature,
27490
+ storyId: ctx.story.id,
27491
+ interactionBridge: (() => {
27492
+ const plugin = ctx.interaction?.getPrimary();
27493
+ if (!plugin)
27494
+ return;
27495
+ const QUESTION_PATTERNS2 = [/\?/, /\bwhich\b/i, /\bshould i\b/i, /\bunclear\b/i, /\bplease clarify\b/i];
27496
+ return {
27497
+ detectQuestion: async (text) => QUESTION_PATTERNS2.some((p) => p.test(text)),
27498
+ onQuestionDetected: async (text) => {
27499
+ const requestId = `ix-acp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
27500
+ await plugin.send({
27501
+ id: requestId,
27502
+ type: "input",
27503
+ featureName: ctx.prd.feature,
27504
+ storyId: ctx.story.id,
27505
+ stage: "execution",
27506
+ summary: text,
27507
+ fallback: "continue",
27508
+ createdAt: Date.now()
27509
+ });
27510
+ try {
27511
+ const response = await plugin.receive(requestId, 120000);
27512
+ return response.value ?? "continue";
27513
+ } catch {
27514
+ return "continue";
27515
+ }
27516
+ }
27517
+ };
27518
+ })()
26759
27519
  });
26760
27520
  ctx.agentResult = result;
26761
27521
  await autoCommitIfDirty(ctx.workdir, "execution", "single-session", ctx.story.id);
@@ -27971,16 +28731,20 @@ var init_regression2 = __esm(() => {
27971
28731
  });
27972
28732
 
27973
28733
  // src/pipeline/stages/routing.ts
27974
- async function runDecompose(story, prd, config2, _workdir) {
28734
+ async function runDecompose(story, prd, config2, _workdir, agentGetFn) {
27975
28735
  const naxDecompose = config2.decompose;
27976
28736
  const builderConfig = {
27977
28737
  maxSubStories: naxDecompose?.maxSubstories ?? 5,
27978
28738
  maxComplexity: naxDecompose?.maxSubstoryComplexity ?? "medium",
27979
28739
  maxRetries: naxDecompose?.maxRetries ?? 2
27980
28740
  };
28741
+ const agent = (agentGetFn ?? getAgent)(config2.autoMode.defaultAgent);
28742
+ if (!agent) {
28743
+ throw new Error(`[decompose] Agent "${config2.autoMode.defaultAgent}" not found \u2014 cannot decompose`);
28744
+ }
27981
28745
  const adapter = {
27982
- async decompose(_prompt) {
27983
- throw new Error("[decompose] No LLM adapter configured for story decomposition");
28746
+ async decompose(prompt) {
28747
+ return agent.complete(prompt, { jsonMode: true });
27984
28748
  }
27985
28749
  };
27986
28750
  return DecomposeBuilder.for(story).prd(prd).config(builderConfig).decompose(adapter);
@@ -28001,7 +28765,7 @@ var init_routing2 = __esm(() => {
28001
28765
  async execute(ctx) {
28002
28766
  const logger = getLogger();
28003
28767
  const agentName = ctx.config.execution?.agent ?? "claude";
28004
- const adapter = _routingDeps.getAgent(agentName);
28768
+ const adapter = (ctx.agentGetFn ?? _routingDeps.getAgent)(agentName);
28005
28769
  const hasExistingRouting = ctx.story.routing !== undefined;
28006
28770
  const hasContentHash = ctx.story.routing?.contentHash !== undefined;
28007
28771
  let currentHash;
@@ -28070,7 +28834,7 @@ var init_routing2 = __esm(() => {
28070
28834
  if (decomposeConfig.trigger === "disabled") {
28071
28835
  logger.warn("routing", `Story ${ctx.story.id} is oversized (${acCount} ACs) but decompose is disabled \u2014 continuing with original`);
28072
28836
  } else if (decomposeConfig.trigger === "auto") {
28073
- const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir);
28837
+ const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir, ctx.agentGetFn);
28074
28838
  if (result.validation.valid) {
28075
28839
  _routingDeps.applyDecomposition(ctx.prd, result);
28076
28840
  if (ctx.prdPath) {
@@ -28085,7 +28849,7 @@ var init_routing2 = __esm(() => {
28085
28849
  } else if (decomposeConfig.trigger === "confirm") {
28086
28850
  const action = await _routingDeps.checkStoryOversized({ featureName: ctx.prd.feature, storyId: ctx.story.id, criteriaCount: acCount }, ctx.config, ctx.interaction);
28087
28851
  if (action === "decompose") {
28088
- const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir);
28852
+ const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir, ctx.agentGetFn);
28089
28853
  if (result.validation.valid) {
28090
28854
  _routingDeps.applyDecomposition(ctx.prd, result);
28091
28855
  if (ctx.prdPath) {
@@ -29573,19 +30337,19 @@ var init_precheck = __esm(() => {
29573
30337
  });
29574
30338
 
29575
30339
  // src/hooks/runner.ts
29576
- import { join as join36 } from "path";
30340
+ import { join as join37 } from "path";
29577
30341
  async function loadHooksConfig(projectDir, globalDir) {
29578
30342
  let globalHooks = { hooks: {} };
29579
30343
  let projectHooks = { hooks: {} };
29580
30344
  let skipGlobal = false;
29581
- const projectPath = join36(projectDir, "hooks.json");
30345
+ const projectPath = join37(projectDir, "hooks.json");
29582
30346
  const projectData = await loadJsonFile(projectPath, "hooks");
29583
30347
  if (projectData) {
29584
30348
  projectHooks = projectData;
29585
30349
  skipGlobal = projectData.skipGlobal ?? false;
29586
30350
  }
29587
30351
  if (!skipGlobal && globalDir) {
29588
- const globalPath = join36(globalDir, "hooks.json");
30352
+ const globalPath = join37(globalDir, "hooks.json");
29589
30353
  const globalData = await loadJsonFile(globalPath, "hooks");
29590
30354
  if (globalData) {
29591
30355
  globalHooks = globalData;
@@ -30026,7 +30790,8 @@ function buildResult(success2, prd, totalCost, iterations, storiesCompleted, prd
30026
30790
  }
30027
30791
  async function generateAndAddFixStories(ctx, failures, prd) {
30028
30792
  const logger = getSafeLogger();
30029
- const agent = getAgent(ctx.config.autoMode.defaultAgent);
30793
+ const { getAgent: getAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
30794
+ const agent = (ctx.agentGetFn ?? getAgent2)(ctx.config.autoMode.defaultAgent);
30030
30795
  if (!agent) {
30031
30796
  logger?.error("acceptance", "Agent not found, cannot generate fix stories");
30032
30797
  return null;
@@ -30172,7 +30937,6 @@ async function runAcceptanceLoop(ctx) {
30172
30937
  }
30173
30938
  var init_acceptance_loop = __esm(() => {
30174
30939
  init_acceptance();
30175
- init_agents();
30176
30940
  init_schema();
30177
30941
  init_hooks();
30178
30942
  init_logger2();
@@ -30622,12 +31386,12 @@ __export(exports_manager, {
30622
31386
  WorktreeManager: () => WorktreeManager
30623
31387
  });
30624
31388
  import { existsSync as existsSync30, symlinkSync } from "fs";
30625
- import { join as join37 } from "path";
31389
+ import { join as join38 } from "path";
30626
31390
 
30627
31391
  class WorktreeManager {
30628
31392
  async create(projectRoot, storyId) {
30629
31393
  validateStoryId(storyId);
30630
- const worktreePath = join37(projectRoot, ".nax-wt", storyId);
31394
+ const worktreePath = join38(projectRoot, ".nax-wt", storyId);
30631
31395
  const branchName = `nax/${storyId}`;
30632
31396
  try {
30633
31397
  const proc = Bun.spawn(["git", "worktree", "add", worktreePath, "-b", branchName], {
@@ -30652,9 +31416,9 @@ class WorktreeManager {
30652
31416
  }
30653
31417
  throw new Error(`Failed to create worktree: ${String(error48)}`);
30654
31418
  }
30655
- const nodeModulesSource = join37(projectRoot, "node_modules");
31419
+ const nodeModulesSource = join38(projectRoot, "node_modules");
30656
31420
  if (existsSync30(nodeModulesSource)) {
30657
- const nodeModulesTarget = join37(worktreePath, "node_modules");
31421
+ const nodeModulesTarget = join38(worktreePath, "node_modules");
30658
31422
  try {
30659
31423
  symlinkSync(nodeModulesSource, nodeModulesTarget, "dir");
30660
31424
  } catch (error48) {
@@ -30662,9 +31426,9 @@ class WorktreeManager {
30662
31426
  throw new Error(`Failed to symlink node_modules: ${errorMessage(error48)}`);
30663
31427
  }
30664
31428
  }
30665
- const envSource = join37(projectRoot, ".env");
31429
+ const envSource = join38(projectRoot, ".env");
30666
31430
  if (existsSync30(envSource)) {
30667
- const envTarget = join37(worktreePath, ".env");
31431
+ const envTarget = join38(worktreePath, ".env");
30668
31432
  try {
30669
31433
  symlinkSync(envSource, envTarget, "file");
30670
31434
  } catch (error48) {
@@ -30675,7 +31439,7 @@ class WorktreeManager {
30675
31439
  }
30676
31440
  async remove(projectRoot, storyId) {
30677
31441
  validateStoryId(storyId);
30678
- const worktreePath = join37(projectRoot, ".nax-wt", storyId);
31442
+ const worktreePath = join38(projectRoot, ".nax-wt", storyId);
30679
31443
  const branchName = `nax/${storyId}`;
30680
31444
  try {
30681
31445
  const proc = Bun.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
@@ -31065,7 +31829,7 @@ var init_parallel_worker = __esm(() => {
31065
31829
 
31066
31830
  // src/execution/parallel-coordinator.ts
31067
31831
  import os3 from "os";
31068
- import { join as join38 } from "path";
31832
+ import { join as join39 } from "path";
31069
31833
  function groupStoriesByDependencies(stories) {
31070
31834
  const batches = [];
31071
31835
  const processed = new Set;
@@ -31142,7 +31906,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
31142
31906
  };
31143
31907
  const worktreePaths = new Map;
31144
31908
  for (const story of batch) {
31145
- const worktreePath = join38(projectRoot, ".nax-wt", story.id);
31909
+ const worktreePath = join39(projectRoot, ".nax-wt", story.id);
31146
31910
  try {
31147
31911
  await worktreeManager.create(projectRoot, story.id);
31148
31912
  worktreePaths.set(story.id, worktreePath);
@@ -31191,7 +31955,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
31191
31955
  });
31192
31956
  logger?.warn("parallel", "Worktree preserved for manual conflict resolution", {
31193
31957
  storyId: mergeResult.storyId,
31194
- worktreePath: join38(projectRoot, ".nax-wt", mergeResult.storyId)
31958
+ worktreePath: join39(projectRoot, ".nax-wt", mergeResult.storyId)
31195
31959
  });
31196
31960
  }
31197
31961
  }
@@ -31650,12 +32414,12 @@ var init_parallel_executor = __esm(() => {
31650
32414
  // src/pipeline/subscribers/events-writer.ts
31651
32415
  import { appendFile as appendFile2, mkdir } from "fs/promises";
31652
32416
  import { homedir as homedir5 } from "os";
31653
- import { basename as basename3, join as join39 } from "path";
32417
+ import { basename as basename3, join as join40 } from "path";
31654
32418
  function wireEventsWriter(bus, feature, runId, workdir) {
31655
32419
  const logger = getSafeLogger();
31656
32420
  const project = basename3(workdir);
31657
- const eventsDir = join39(homedir5(), ".nax", "events", project);
31658
- const eventsFile = join39(eventsDir, "events.jsonl");
32421
+ const eventsDir = join40(homedir5(), ".nax", "events", project);
32422
+ const eventsFile = join40(eventsDir, "events.jsonl");
31659
32423
  let dirReady = false;
31660
32424
  const write = (line) => {
31661
32425
  (async () => {
@@ -31815,12 +32579,12 @@ var init_interaction2 = __esm(() => {
31815
32579
  // src/pipeline/subscribers/registry.ts
31816
32580
  import { mkdir as mkdir2, writeFile } from "fs/promises";
31817
32581
  import { homedir as homedir6 } from "os";
31818
- import { basename as basename4, join as join40 } from "path";
32582
+ import { basename as basename4, join as join41 } from "path";
31819
32583
  function wireRegistry(bus, feature, runId, workdir) {
31820
32584
  const logger = getSafeLogger();
31821
32585
  const project = basename4(workdir);
31822
- const runDir = join40(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
31823
- const metaFile = join40(runDir, "meta.json");
32586
+ const runDir = join41(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
32587
+ const metaFile = join41(runDir, "meta.json");
31824
32588
  const unsub = bus.on("run:started", (_ev) => {
31825
32589
  (async () => {
31826
32590
  try {
@@ -31830,8 +32594,8 @@ function wireRegistry(bus, feature, runId, workdir) {
31830
32594
  project,
31831
32595
  feature,
31832
32596
  workdir,
31833
- statusPath: join40(workdir, "nax", "features", feature, "status.json"),
31834
- eventsDir: join40(workdir, "nax", "features", feature, "runs"),
32597
+ statusPath: join41(workdir, "nax", "features", feature, "status.json"),
32598
+ eventsDir: join41(workdir, "nax", "features", feature, "runs"),
31835
32599
  registeredAt: new Date().toISOString()
31836
32600
  };
31837
32601
  await writeFile(metaFile, JSON.stringify(meta3, null, 2));
@@ -32499,6 +33263,8 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
32499
33263
  storyStartTime: new Date().toISOString(),
32500
33264
  storyGitRef: storyGitRef ?? undefined,
32501
33265
  interaction: ctx.interactionChain ?? undefined,
33266
+ agentGetFn: ctx.agentGetFn,
33267
+ pidRegistry: ctx.pidRegistry,
32502
33268
  accumulatedAttemptCost: accumulatedAttemptCost > 0 ? accumulatedAttemptCost : undefined
32503
33269
  };
32504
33270
  ctx.statusWriter.setPrd(prd);
@@ -32825,7 +33591,7 @@ async function writeStatusFile(filePath, status) {
32825
33591
  var init_status_file = () => {};
32826
33592
 
32827
33593
  // src/execution/status-writer.ts
32828
- import { join as join41 } from "path";
33594
+ import { join as join42 } from "path";
32829
33595
 
32830
33596
  class StatusWriter {
32831
33597
  statusFile;
@@ -32893,7 +33659,7 @@ class StatusWriter {
32893
33659
  if (!this._prd)
32894
33660
  return;
32895
33661
  const safeLogger = getSafeLogger();
32896
- const featureStatusPath = join41(featureDir, "status.json");
33662
+ const featureStatusPath = join42(featureDir, "status.json");
32897
33663
  try {
32898
33664
  const base = this.getSnapshot(totalCost, iterations);
32899
33665
  if (!base) {
@@ -33097,6 +33863,7 @@ var init_precheck_runner = __esm(() => {
33097
33863
  // src/execution/lifecycle/run-initialization.ts
33098
33864
  var exports_run_initialization = {};
33099
33865
  __export(exports_run_initialization, {
33866
+ logActiveProtocol: () => logActiveProtocol,
33100
33867
  initializeRun: () => initializeRun
33101
33868
  });
33102
33869
  async function reconcileState(prd, prdPath, workdir) {
@@ -33123,11 +33890,12 @@ async function reconcileState(prd, prdPath, workdir) {
33123
33890
  }
33124
33891
  return prd;
33125
33892
  }
33126
- async function checkAgentInstalled(config2, dryRun) {
33893
+ async function checkAgentInstalled(config2, dryRun, agentGetFn) {
33127
33894
  if (dryRun)
33128
33895
  return;
33129
33896
  const logger = getSafeLogger();
33130
- const agent = getAgent(config2.autoMode.defaultAgent);
33897
+ const { getAgent: getAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
33898
+ const agent = (agentGetFn ?? getAgent2)(config2.autoMode.defaultAgent);
33131
33899
  if (!agent) {
33132
33900
  logger?.error("execution", "Agent not found", {
33133
33901
  agent: config2.autoMode.defaultAgent
@@ -33155,9 +33923,14 @@ function validateStoryCount(counts, config2) {
33155
33923
  throw new StoryLimitExceededError(counts.total, config2.execution.maxStoriesPerFeature);
33156
33924
  }
33157
33925
  }
33926
+ function logActiveProtocol(config2) {
33927
+ const logger = getSafeLogger();
33928
+ const protocol = config2.agent?.protocol ?? "cli";
33929
+ logger?.info("run-initialization", `Agent protocol: ${protocol}`, { protocol });
33930
+ }
33158
33931
  async function initializeRun(ctx) {
33159
33932
  const logger = getSafeLogger();
33160
- await checkAgentInstalled(ctx.config, ctx.dryRun);
33933
+ await checkAgentInstalled(ctx.config, ctx.dryRun, ctx.agentGetFn);
33161
33934
  let prd = await loadPRD(ctx.prdPath);
33162
33935
  prd = await reconcileState(prd, ctx.prdPath, ctx.workdir);
33163
33936
  const counts = countStories(prd);
@@ -33170,7 +33943,6 @@ async function initializeRun(ctx) {
33170
33943
  return { prd, storyCounts: counts };
33171
33944
  }
33172
33945
  var init_run_initialization = __esm(() => {
33173
- init_agents();
33174
33946
  init_errors3();
33175
33947
  init_logger2();
33176
33948
  init_prd();
@@ -33279,7 +34051,8 @@ async function setupRun(options) {
33279
34051
  config: config2,
33280
34052
  prdPath,
33281
34053
  workdir,
33282
- dryRun
34054
+ dryRun,
34055
+ agentGetFn: options.agentGetFn
33283
34056
  });
33284
34057
  prd = initResult.prd;
33285
34058
  const counts = initResult.storyCounts;
@@ -64209,7 +64982,7 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
64209
64982
  init_source();
64210
64983
  import { existsSync as existsSync32, mkdirSync as mkdirSync6 } from "fs";
64211
64984
  import { homedir as homedir8 } from "os";
64212
- import { join as join42 } from "path";
64985
+ import { join as join43 } from "path";
64213
64986
 
64214
64987
  // node_modules/commander/esm.mjs
64215
64988
  var import__ = __toESM(require_commander(), 1);
@@ -64231,14 +65004,14 @@ var {
64231
65004
  init_acceptance();
64232
65005
  init_registry();
64233
65006
  import { existsSync as existsSync8 } from "fs";
64234
- import { join as join8 } from "path";
65007
+ import { join as join9 } from "path";
64235
65008
 
64236
65009
  // src/analyze/scanner.ts
64237
65010
  import { existsSync as existsSync2 } from "fs";
64238
- import { join as join3 } from "path";
65011
+ import { join as join4 } from "path";
64239
65012
  async function scanCodebase(workdir) {
64240
- const srcPath = join3(workdir, "src");
64241
- const packageJsonPath = join3(workdir, "package.json");
65013
+ const srcPath = join4(workdir, "src");
65014
+ const packageJsonPath = join4(workdir, "package.json");
64242
65015
  const fileTree = existsSync2(srcPath) ? await generateFileTree(srcPath, 3) : "No src/ directory";
64243
65016
  let dependencies = {};
64244
65017
  let devDependencies = {};
@@ -64278,7 +65051,7 @@ async function generateFileTree(dir, maxDepth, currentDepth = 0, prefix = "") {
64278
65051
  });
64279
65052
  for (let i = 0;i < dirEntries.length; i++) {
64280
65053
  const entry = dirEntries[i];
64281
- const fullPath = join3(dir, entry);
65054
+ const fullPath = join4(dir, entry);
64282
65055
  const isLast = i === dirEntries.length - 1;
64283
65056
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
64284
65057
  const childPrefix = isLast ? " " : "\u2502 ";
@@ -64310,16 +65083,16 @@ function detectTestPatterns(workdir, dependencies, devDependencies) {
64310
65083
  } else {
64311
65084
  patterns.push("Test framework: likely bun:test (no framework dependency)");
64312
65085
  }
64313
- if (existsSync2(join3(workdir, "test"))) {
65086
+ if (existsSync2(join4(workdir, "test"))) {
64314
65087
  patterns.push("Test directory: test/");
64315
65088
  }
64316
- if (existsSync2(join3(workdir, "__tests__"))) {
65089
+ if (existsSync2(join4(workdir, "__tests__"))) {
64317
65090
  patterns.push("Test directory: __tests__/");
64318
65091
  }
64319
- if (existsSync2(join3(workdir, "tests"))) {
65092
+ if (existsSync2(join4(workdir, "tests"))) {
64320
65093
  patterns.push("Test directory: tests/");
64321
65094
  }
64322
- const hasTestFiles = existsSync2(join3(workdir, "test")) || existsSync2(join3(workdir, "src"));
65095
+ const hasTestFiles = existsSync2(join4(workdir, "test")) || existsSync2(join4(workdir, "src"));
64323
65096
  if (hasTestFiles) {
64324
65097
  patterns.push("Test files: *.test.ts, *.spec.ts");
64325
65098
  }
@@ -64337,7 +65110,7 @@ init_version();
64337
65110
  // src/cli/analyze-parser.ts
64338
65111
  init_registry();
64339
65112
  import { existsSync as existsSync7 } from "fs";
64340
- import { join as join7 } from "path";
65113
+ import { join as join8 } from "path";
64341
65114
  init_schema();
64342
65115
  init_logger2();
64343
65116
  init_prd();
@@ -64467,7 +65240,7 @@ function estimateLOCFromComplexity(complexity) {
64467
65240
  }
64468
65241
  }
64469
65242
  async function reclassifyExistingPRD(featureDir, featureName, branchName, workdir, config2) {
64470
- const prdPath = join7(featureDir, "prd.json");
65243
+ const prdPath = join8(featureDir, "prd.json");
64471
65244
  if (!existsSync7(prdPath)) {
64472
65245
  throw new Error(`prd.json not found at ${prdPath}. Run analyze without --reclassify first.`);
64473
65246
  }
@@ -64568,11 +65341,11 @@ function reclassifyWithKeywords(story, config2) {
64568
65341
  // src/cli/analyze.ts
64569
65342
  async function analyzeFeature(options) {
64570
65343
  const { featureDir, featureName, branchName, config: config2, specPath: explicitSpecPath, reclassify = false } = options;
64571
- const workdir = join8(featureDir, "../..");
65344
+ const workdir = join9(featureDir, "../..");
64572
65345
  if (reclassify) {
64573
65346
  return await reclassifyExistingPRD(featureDir, featureName, branchName, workdir, config2);
64574
65347
  }
64575
- const specPath = explicitSpecPath || join8(featureDir, "spec.md");
65348
+ const specPath = explicitSpecPath || join9(featureDir, "spec.md");
64576
65349
  if (!existsSync8(specPath))
64577
65350
  throw new Error(`spec.md not found at ${specPath}`);
64578
65351
  const specContent = await Bun.file(specPath).text();
@@ -64689,7 +65462,7 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
64689
65462
  modelDef,
64690
65463
  config: config2
64691
65464
  });
64692
- const acceptanceTestPath = join8(featureDir, config2.acceptance.testPath);
65465
+ const acceptanceTestPath = join9(featureDir, config2.acceptance.testPath);
64693
65466
  await Bun.write(acceptanceTestPath, result.testCode);
64694
65467
  logger.info("cli", "[OK] Acceptance tests generated", {
64695
65468
  criteriaCount: result.criteria.length,
@@ -64702,9 +65475,25 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
64702
65475
  // src/cli/plan.ts
64703
65476
  init_claude();
64704
65477
  import { existsSync as existsSync9 } from "fs";
64705
- import { join as join9 } from "path";
65478
+ import { join as join10 } from "path";
65479
+ import { createInterface } from "readline";
64706
65480
  init_schema();
64707
65481
  init_logger2();
65482
+ var QUESTION_PATTERNS = [/\?[\s]*$/, /\bwhich\b/i, /\bshould i\b/i, /\bdo you want\b/i, /\bwould you like\b/i];
65483
+ async function detectQuestion(text) {
65484
+ return QUESTION_PATTERNS.some((p) => p.test(text.trim()));
65485
+ }
65486
+ async function askHuman(question) {
65487
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
65488
+ return new Promise((resolve6) => {
65489
+ rl.question(`
65490
+ [Agent asks]: ${question}
65491
+ Your answer: `, (answer) => {
65492
+ rl.close();
65493
+ resolve6(answer.trim());
65494
+ });
65495
+ });
65496
+ }
64708
65497
  var SPEC_TEMPLATE = `# Feature: [title]
64709
65498
 
64710
65499
  ## Problem
@@ -64725,8 +65514,8 @@ What this does NOT include.
64725
65514
  `;
64726
65515
  async function planCommand(prompt, workdir, config2, options = {}) {
64727
65516
  const interactive = options.interactive !== false;
64728
- const ngentDir = join9(workdir, "nax");
64729
- const outputPath = join9(ngentDir, config2.plan.outputPath);
65517
+ const ngentDir = join10(workdir, "nax");
65518
+ const outputPath = join10(ngentDir, config2.plan.outputPath);
64730
65519
  if (!existsSync9(ngentDir)) {
64731
65520
  throw new Error(`nax directory not found. Run 'nax init' first in ${workdir}`);
64732
65521
  }
@@ -64746,7 +65535,8 @@ async function planCommand(prompt, workdir, config2, options = {}) {
64746
65535
  inputFile: options.from,
64747
65536
  modelTier,
64748
65537
  modelDef,
64749
- config: config2
65538
+ config: config2,
65539
+ interactionBridge: interactive ? { detectQuestion, onQuestionDetected: askHuman } : undefined
64750
65540
  };
64751
65541
  const adapter = new ClaudeCodeAdapter;
64752
65542
  logger.info("cli", interactive ? "Starting interactive planning session..." : `Reading from ${options.from}...`, {
@@ -64964,13 +65754,13 @@ async function displayModelEfficiency(workdir) {
64964
65754
  // src/cli/status-features.ts
64965
65755
  init_source();
64966
65756
  import { existsSync as existsSync11, readdirSync as readdirSync2 } from "fs";
64967
- import { join as join12 } from "path";
65757
+ import { join as join13 } from "path";
64968
65758
 
64969
65759
  // src/commands/common.ts
64970
65760
  init_path_security2();
64971
65761
  init_errors3();
64972
65762
  import { existsSync as existsSync10, readdirSync, realpathSync as realpathSync2 } from "fs";
64973
- import { join as join10, resolve as resolve6 } from "path";
65763
+ import { join as join11, resolve as resolve6 } from "path";
64974
65764
  function resolveProject(options = {}) {
64975
65765
  const { dir, feature } = options;
64976
65766
  let projectRoot;
@@ -64978,12 +65768,12 @@ function resolveProject(options = {}) {
64978
65768
  let configPath;
64979
65769
  if (dir) {
64980
65770
  projectRoot = realpathSync2(resolve6(dir));
64981
- naxDir = join10(projectRoot, "nax");
65771
+ naxDir = join11(projectRoot, "nax");
64982
65772
  if (!existsSync10(naxDir)) {
64983
65773
  throw new NaxError(`Directory does not contain a nax project: ${projectRoot}
64984
65774
  Expected to find: ${naxDir}`, "NAX_DIR_NOT_FOUND", { projectRoot, naxDir });
64985
65775
  }
64986
- configPath = join10(naxDir, "config.json");
65776
+ configPath = join11(naxDir, "config.json");
64987
65777
  if (!existsSync10(configPath)) {
64988
65778
  throw new NaxError(`nax directory found but config.json is missing: ${naxDir}
64989
65779
  Expected to find: ${configPath}`, "CONFIG_NOT_FOUND", { naxDir, configPath });
@@ -64991,22 +65781,22 @@ Expected to find: ${configPath}`, "CONFIG_NOT_FOUND", { naxDir, configPath });
64991
65781
  } else {
64992
65782
  const found = findProjectRoot(process.cwd());
64993
65783
  if (!found) {
64994
- const cwdNaxDir = join10(process.cwd(), "nax");
65784
+ const cwdNaxDir = join11(process.cwd(), "nax");
64995
65785
  if (existsSync10(cwdNaxDir)) {
64996
- const cwdConfigPath = join10(cwdNaxDir, "config.json");
65786
+ const cwdConfigPath = join11(cwdNaxDir, "config.json");
64997
65787
  throw new NaxError(`nax directory found but config.json is missing: ${cwdNaxDir}
64998
65788
  Expected to find: ${cwdConfigPath}`, "CONFIG_NOT_FOUND", { naxDir: cwdNaxDir, configPath: cwdConfigPath });
64999
65789
  }
65000
65790
  throw new NaxError("No nax project found. Run this command from within a nax project directory, or use -d flag to specify the project path.", "PROJECT_NOT_FOUND", { cwd: process.cwd() });
65001
65791
  }
65002
65792
  projectRoot = found;
65003
- naxDir = join10(projectRoot, "nax");
65004
- configPath = join10(naxDir, "config.json");
65793
+ naxDir = join11(projectRoot, "nax");
65794
+ configPath = join11(naxDir, "config.json");
65005
65795
  }
65006
65796
  let featureDir;
65007
65797
  if (feature) {
65008
- const featuresDir = join10(naxDir, "features");
65009
- featureDir = join10(featuresDir, feature);
65798
+ const featuresDir = join11(naxDir, "features");
65799
+ featureDir = join11(featuresDir, feature);
65010
65800
  if (!existsSync10(featureDir)) {
65011
65801
  const availableFeatures = existsSync10(featuresDir) ? readdirSync(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
65012
65802
  const availableMsg = availableFeatures.length > 0 ? `
@@ -65033,12 +65823,12 @@ function findProjectRoot(startDir) {
65033
65823
  let current = resolve6(startDir);
65034
65824
  let depth = 0;
65035
65825
  while (depth < MAX_DIRECTORY_DEPTH) {
65036
- const naxDir = join10(current, "nax");
65037
- const configPath = join10(naxDir, "config.json");
65826
+ const naxDir = join11(current, "nax");
65827
+ const configPath = join11(naxDir, "config.json");
65038
65828
  if (existsSync10(configPath)) {
65039
65829
  return realpathSync2(current);
65040
65830
  }
65041
- const parent = join10(current, "..");
65831
+ const parent = join11(current, "..");
65042
65832
  if (parent === current) {
65043
65833
  break;
65044
65834
  }
@@ -65060,7 +65850,7 @@ function isPidAlive(pid) {
65060
65850
  }
65061
65851
  }
65062
65852
  async function loadStatusFile(featureDir) {
65063
- const statusPath = join12(featureDir, "status.json");
65853
+ const statusPath = join13(featureDir, "status.json");
65064
65854
  if (!existsSync11(statusPath)) {
65065
65855
  return null;
65066
65856
  }
@@ -65072,7 +65862,7 @@ async function loadStatusFile(featureDir) {
65072
65862
  }
65073
65863
  }
65074
65864
  async function loadProjectStatusFile(projectDir) {
65075
- const statusPath = join12(projectDir, "nax", "status.json");
65865
+ const statusPath = join13(projectDir, "nax", "status.json");
65076
65866
  if (!existsSync11(statusPath)) {
65077
65867
  return null;
65078
65868
  }
@@ -65084,7 +65874,7 @@ async function loadProjectStatusFile(projectDir) {
65084
65874
  }
65085
65875
  }
65086
65876
  async function getFeatureSummary(featureName, featureDir) {
65087
- const prdPath = join12(featureDir, "prd.json");
65877
+ const prdPath = join13(featureDir, "prd.json");
65088
65878
  const prd = await loadPRD(prdPath);
65089
65879
  const counts = countStories(prd);
65090
65880
  const summary = {
@@ -65118,7 +65908,7 @@ async function getFeatureSummary(featureName, featureDir) {
65118
65908
  };
65119
65909
  }
65120
65910
  }
65121
- const runsDir = join12(featureDir, "runs");
65911
+ const runsDir = join13(featureDir, "runs");
65122
65912
  if (existsSync11(runsDir)) {
65123
65913
  const runs = readdirSync2(runsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".jsonl") && e.name !== "latest.jsonl").map((e) => e.name).sort().reverse();
65124
65914
  if (runs.length > 0) {
@@ -65129,7 +65919,7 @@ async function getFeatureSummary(featureName, featureDir) {
65129
65919
  return summary;
65130
65920
  }
65131
65921
  async function displayAllFeatures(projectDir) {
65132
- const featuresDir = join12(projectDir, "nax", "features");
65922
+ const featuresDir = join13(projectDir, "nax", "features");
65133
65923
  if (!existsSync11(featuresDir)) {
65134
65924
  console.log(source_default.dim("No features found."));
65135
65925
  return;
@@ -65170,7 +65960,7 @@ async function displayAllFeatures(projectDir) {
65170
65960
  console.log();
65171
65961
  }
65172
65962
  }
65173
- const summaries = await Promise.all(features.map((name) => getFeatureSummary(name, join12(featuresDir, name))));
65963
+ const summaries = await Promise.all(features.map((name) => getFeatureSummary(name, join13(featuresDir, name))));
65174
65964
  console.log(source_default.bold(`\uD83D\uDCCA Features
65175
65965
  `));
65176
65966
  const header = ` ${"Feature".padEnd(25)} ${"Done".padEnd(6)} ${"Failed".padEnd(8)} ${"Pending".padEnd(9)} ${"Last Run".padEnd(22)} ${"Cost".padEnd(10)} Status`;
@@ -65196,7 +65986,7 @@ async function displayAllFeatures(projectDir) {
65196
65986
  console.log();
65197
65987
  }
65198
65988
  async function displayFeatureDetails(featureName, featureDir) {
65199
- const prdPath = join12(featureDir, "prd.json");
65989
+ const prdPath = join13(featureDir, "prd.json");
65200
65990
  const prd = await loadPRD(prdPath);
65201
65991
  const counts = countStories(prd);
65202
65992
  const status = await loadStatusFile(featureDir);
@@ -65311,7 +66101,7 @@ async function displayFeatureStatus(options = {}) {
65311
66101
  init_errors3();
65312
66102
  init_logger2();
65313
66103
  import { existsSync as existsSync12, readdirSync as readdirSync3 } from "fs";
65314
- import { join as join13 } from "path";
66104
+ import { join as join14 } from "path";
65315
66105
  async function parseRunLog(logPath) {
65316
66106
  const logger = getLogger();
65317
66107
  try {
@@ -65327,7 +66117,7 @@ async function parseRunLog(logPath) {
65327
66117
  async function runsListCommand(options) {
65328
66118
  const logger = getLogger();
65329
66119
  const { feature, workdir } = options;
65330
- const runsDir = join13(workdir, "nax", "features", feature, "runs");
66120
+ const runsDir = join14(workdir, "nax", "features", feature, "runs");
65331
66121
  if (!existsSync12(runsDir)) {
65332
66122
  logger.info("cli", "No runs found for feature", { feature, hint: `Directory not found: ${runsDir}` });
65333
66123
  return;
@@ -65339,7 +66129,7 @@ async function runsListCommand(options) {
65339
66129
  }
65340
66130
  logger.info("cli", `Runs for ${feature}`, { count: files.length });
65341
66131
  for (const file2 of files.sort().reverse()) {
65342
- const logPath = join13(runsDir, file2);
66132
+ const logPath = join14(runsDir, file2);
65343
66133
  const entries = await parseRunLog(logPath);
65344
66134
  const startEvent = entries.find((e) => e.message === "run.start");
65345
66135
  const completeEvent = entries.find((e) => e.message === "run.complete");
@@ -65365,7 +66155,7 @@ async function runsListCommand(options) {
65365
66155
  async function runsShowCommand(options) {
65366
66156
  const logger = getLogger();
65367
66157
  const { runId, feature, workdir } = options;
65368
- const logPath = join13(workdir, "nax", "features", feature, "runs", `${runId}.jsonl`);
66158
+ const logPath = join14(workdir, "nax", "features", feature, "runs", `${runId}.jsonl`);
65369
66159
  if (!existsSync12(logPath)) {
65370
66160
  logger.error("cli", "Run not found", { runId, feature, logPath });
65371
66161
  throw new NaxError("Run not found", "RUN_NOT_FOUND", { runId, feature, logPath });
@@ -65404,7 +66194,7 @@ async function runsShowCommand(options) {
65404
66194
  // src/cli/prompts-main.ts
65405
66195
  init_logger2();
65406
66196
  import { existsSync as existsSync15, mkdirSync as mkdirSync3 } from "fs";
65407
- import { join as join20 } from "path";
66197
+ import { join as join21 } from "path";
65408
66198
 
65409
66199
  // src/pipeline/index.ts
65410
66200
  init_runner();
@@ -65440,7 +66230,7 @@ init_prd();
65440
66230
 
65441
66231
  // src/cli/prompts-tdd.ts
65442
66232
  init_prompts2();
65443
- import { join as join19 } from "path";
66233
+ import { join as join20 } from "path";
65444
66234
  async function handleThreeSessionTddPrompts(story, ctx, outputDir, logger) {
65445
66235
  const [testWriterPrompt, implementerPrompt, verifierPrompt] = await Promise.all([
65446
66236
  PromptBuilder.for("test-writer", { isolation: "strict" }).withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(ctx.config.quality?.commands?.test).build(),
@@ -65459,7 +66249,7 @@ ${frontmatter}---
65459
66249
 
65460
66250
  ${session.prompt}`;
65461
66251
  if (outputDir) {
65462
- const promptFile = join19(outputDir, `${story.id}.${session.role}.md`);
66252
+ const promptFile = join20(outputDir, `${story.id}.${session.role}.md`);
65463
66253
  await Bun.write(promptFile, fullOutput);
65464
66254
  logger.info("cli", "Written TDD prompt file", {
65465
66255
  storyId: story.id,
@@ -65475,7 +66265,7 @@ ${"=".repeat(80)}`);
65475
66265
  }
65476
66266
  }
65477
66267
  if (outputDir && ctx.contextMarkdown) {
65478
- const contextFile = join19(outputDir, `${story.id}.context.md`);
66268
+ const contextFile = join20(outputDir, `${story.id}.context.md`);
65479
66269
  const frontmatter = buildFrontmatter(story, ctx);
65480
66270
  const contextOutput = `---
65481
66271
  ${frontmatter}---
@@ -65489,12 +66279,12 @@ ${ctx.contextMarkdown}`;
65489
66279
  async function promptsCommand(options) {
65490
66280
  const logger = getLogger();
65491
66281
  const { feature, workdir, config: config2, storyId, outputDir } = options;
65492
- const naxDir = join20(workdir, "nax");
66282
+ const naxDir = join21(workdir, "nax");
65493
66283
  if (!existsSync15(naxDir)) {
65494
66284
  throw new Error(`nax directory not found. Run 'nax init' first in ${workdir}`);
65495
66285
  }
65496
- const featureDir = join20(naxDir, "features", feature);
65497
- const prdPath = join20(featureDir, "prd.json");
66286
+ const featureDir = join21(naxDir, "features", feature);
66287
+ const prdPath = join21(featureDir, "prd.json");
65498
66288
  if (!existsSync15(prdPath)) {
65499
66289
  throw new Error(`Feature "${feature}" not found or missing prd.json`);
65500
66290
  }
@@ -65554,10 +66344,10 @@ ${frontmatter}---
65554
66344
 
65555
66345
  ${ctx.prompt}`;
65556
66346
  if (outputDir) {
65557
- const promptFile = join20(outputDir, `${story.id}.prompt.md`);
66347
+ const promptFile = join21(outputDir, `${story.id}.prompt.md`);
65558
66348
  await Bun.write(promptFile, fullOutput);
65559
66349
  if (ctx.contextMarkdown) {
65560
- const contextFile = join20(outputDir, `${story.id}.context.md`);
66350
+ const contextFile = join21(outputDir, `${story.id}.context.md`);
65561
66351
  const contextOutput = `---
65562
66352
  ${frontmatter}---
65563
66353
 
@@ -65621,7 +66411,7 @@ function buildFrontmatter(story, ctx, role) {
65621
66411
  }
65622
66412
  // src/cli/prompts-init.ts
65623
66413
  import { existsSync as existsSync16, mkdirSync as mkdirSync4 } from "fs";
65624
- import { join as join21 } from "path";
66414
+ import { join as join22 } from "path";
65625
66415
  var TEMPLATE_ROLES = [
65626
66416
  { file: "test-writer.md", role: "test-writer" },
65627
66417
  { file: "implementer.md", role: "implementer", variant: "standard" },
@@ -65645,9 +66435,9 @@ var TEMPLATE_HEADER = `<!--
65645
66435
  `;
65646
66436
  async function promptsInitCommand(options) {
65647
66437
  const { workdir, force = false, autoWireConfig = true } = options;
65648
- const templatesDir = join21(workdir, "nax", "templates");
66438
+ const templatesDir = join22(workdir, "nax", "templates");
65649
66439
  mkdirSync4(templatesDir, { recursive: true });
65650
- const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync16(join21(templatesDir, f)));
66440
+ const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync16(join22(templatesDir, f)));
65651
66441
  if (existingFiles.length > 0 && !force) {
65652
66442
  console.warn(`[WARN] nax/templates/ already contains files: ${existingFiles.join(", ")}. No files overwritten.
65653
66443
  Pass --force to overwrite existing templates.`);
@@ -65655,7 +66445,7 @@ async function promptsInitCommand(options) {
65655
66445
  }
65656
66446
  const written = [];
65657
66447
  for (const template of TEMPLATE_ROLES) {
65658
- const filePath = join21(templatesDir, template.file);
66448
+ const filePath = join22(templatesDir, template.file);
65659
66449
  const roleBody = template.role === "implementer" ? buildRoleTaskSection(template.role, template.variant) : buildRoleTaskSection(template.role);
65660
66450
  const content = TEMPLATE_HEADER + roleBody;
65661
66451
  await Bun.write(filePath, content);
@@ -65671,7 +66461,7 @@ async function promptsInitCommand(options) {
65671
66461
  return written;
65672
66462
  }
65673
66463
  async function autoWirePromptsConfig(workdir) {
65674
- const configPath = join21(workdir, "nax.config.json");
66464
+ const configPath = join22(workdir, "nax.config.json");
65675
66465
  if (!existsSync16(configPath)) {
65676
66466
  const exampleConfig = JSON.stringify({
65677
66467
  prompts: {
@@ -65838,7 +66628,7 @@ init_config();
65838
66628
  init_logger2();
65839
66629
  init_prd();
65840
66630
  import { existsSync as existsSync17, readdirSync as readdirSync4 } from "fs";
65841
- import { join as join24 } from "path";
66631
+ import { join as join25 } from "path";
65842
66632
 
65843
66633
  // src/cli/diagnose-analysis.ts
65844
66634
  function detectFailurePattern(story, prd, status) {
@@ -66037,7 +66827,7 @@ function isProcessAlive2(pid) {
66037
66827
  }
66038
66828
  }
66039
66829
  async function loadStatusFile2(workdir) {
66040
- const statusPath = join24(workdir, "nax", "status.json");
66830
+ const statusPath = join25(workdir, "nax", "status.json");
66041
66831
  if (!existsSync17(statusPath))
66042
66832
  return null;
66043
66833
  try {
@@ -66065,7 +66855,7 @@ async function countCommitsSince(workdir, since) {
66065
66855
  }
66066
66856
  }
66067
66857
  async function checkLock(workdir) {
66068
- const lockFile = Bun.file(join24(workdir, "nax.lock"));
66858
+ const lockFile = Bun.file(join25(workdir, "nax.lock"));
66069
66859
  if (!await lockFile.exists())
66070
66860
  return { lockPresent: false };
66071
66861
  try {
@@ -66083,8 +66873,8 @@ async function diagnoseCommand(options = {}) {
66083
66873
  const logger = getLogger();
66084
66874
  const workdir = options.workdir ?? process.cwd();
66085
66875
  const naxSubdir = findProjectDir(workdir);
66086
- let projectDir = naxSubdir ? join24(naxSubdir, "..") : null;
66087
- if (!projectDir && existsSync17(join24(workdir, "nax"))) {
66876
+ let projectDir = naxSubdir ? join25(naxSubdir, "..") : null;
66877
+ if (!projectDir && existsSync17(join25(workdir, "nax"))) {
66088
66878
  projectDir = workdir;
66089
66879
  }
66090
66880
  if (!projectDir)
@@ -66095,7 +66885,7 @@ async function diagnoseCommand(options = {}) {
66095
66885
  if (status2) {
66096
66886
  feature = status2.run.feature;
66097
66887
  } else {
66098
- const featuresDir = join24(projectDir, "nax", "features");
66888
+ const featuresDir = join25(projectDir, "nax", "features");
66099
66889
  if (!existsSync17(featuresDir))
66100
66890
  throw new Error("No features found in project");
66101
66891
  const features = readdirSync4(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
@@ -66105,8 +66895,8 @@ async function diagnoseCommand(options = {}) {
66105
66895
  logger.info("diagnose", "No feature specified, using first found", { feature });
66106
66896
  }
66107
66897
  }
66108
- const featureDir = join24(projectDir, "nax", "features", feature);
66109
- const prdPath = join24(featureDir, "prd.json");
66898
+ const featureDir = join25(projectDir, "nax", "features", feature);
66899
+ const prdPath = join25(featureDir, "prd.json");
66110
66900
  if (!existsSync17(prdPath))
66111
66901
  throw new Error(`Feature not found: ${feature}`);
66112
66902
  const prd = await loadPRD(prdPath);
@@ -66149,16 +66939,16 @@ init_interaction();
66149
66939
  init_source();
66150
66940
  init_loader2();
66151
66941
  import { existsSync as existsSync20 } from "fs";
66152
- import { join as join27 } from "path";
66942
+ import { join as join28 } from "path";
66153
66943
 
66154
66944
  // src/context/generator.ts
66155
66945
  init_path_security2();
66156
66946
  import { existsSync as existsSync19 } from "fs";
66157
- import { join as join26 } from "path";
66947
+ import { join as join27 } from "path";
66158
66948
 
66159
66949
  // src/context/injector.ts
66160
66950
  import { existsSync as existsSync18 } from "fs";
66161
- import { join as join25 } from "path";
66951
+ import { join as join26 } from "path";
66162
66952
  var NOTABLE_NODE_DEPS = [
66163
66953
  "@nestjs",
66164
66954
  "express",
@@ -66188,7 +66978,7 @@ var NOTABLE_NODE_DEPS = [
66188
66978
  "ioredis"
66189
66979
  ];
66190
66980
  async function detectNode(workdir) {
66191
- const pkgPath = join25(workdir, "package.json");
66981
+ const pkgPath = join26(workdir, "package.json");
66192
66982
  if (!existsSync18(pkgPath))
66193
66983
  return null;
66194
66984
  try {
@@ -66205,7 +66995,7 @@ async function detectNode(workdir) {
66205
66995
  }
66206
66996
  }
66207
66997
  async function detectGo(workdir) {
66208
- const goMod = join25(workdir, "go.mod");
66998
+ const goMod = join26(workdir, "go.mod");
66209
66999
  if (!existsSync18(goMod))
66210
67000
  return null;
66211
67001
  try {
@@ -66229,7 +67019,7 @@ async function detectGo(workdir) {
66229
67019
  }
66230
67020
  }
66231
67021
  async function detectRust(workdir) {
66232
- const cargoPath = join25(workdir, "Cargo.toml");
67022
+ const cargoPath = join26(workdir, "Cargo.toml");
66233
67023
  if (!existsSync18(cargoPath))
66234
67024
  return null;
66235
67025
  try {
@@ -66245,8 +67035,8 @@ async function detectRust(workdir) {
66245
67035
  }
66246
67036
  }
66247
67037
  async function detectPython(workdir) {
66248
- const pyproject = join25(workdir, "pyproject.toml");
66249
- const requirements = join25(workdir, "requirements.txt");
67038
+ const pyproject = join26(workdir, "pyproject.toml");
67039
+ const requirements = join26(workdir, "requirements.txt");
66250
67040
  if (!existsSync18(pyproject) && !existsSync18(requirements))
66251
67041
  return null;
66252
67042
  try {
@@ -66265,7 +67055,7 @@ async function detectPython(workdir) {
66265
67055
  }
66266
67056
  }
66267
67057
  async function detectPhp(workdir) {
66268
- const composerPath = join25(workdir, "composer.json");
67058
+ const composerPath = join26(workdir, "composer.json");
66269
67059
  if (!existsSync18(composerPath))
66270
67060
  return null;
66271
67061
  try {
@@ -66278,7 +67068,7 @@ async function detectPhp(workdir) {
66278
67068
  }
66279
67069
  }
66280
67070
  async function detectRuby(workdir) {
66281
- const gemfile = join25(workdir, "Gemfile");
67071
+ const gemfile = join26(workdir, "Gemfile");
66282
67072
  if (!existsSync18(gemfile))
66283
67073
  return null;
66284
67074
  try {
@@ -66290,9 +67080,9 @@ async function detectRuby(workdir) {
66290
67080
  }
66291
67081
  }
66292
67082
  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");
67083
+ const pom = join26(workdir, "pom.xml");
67084
+ const gradle = join26(workdir, "build.gradle");
67085
+ const gradleKts = join26(workdir, "build.gradle.kts");
66296
67086
  if (!existsSync18(pom) && !existsSync18(gradle) && !existsSync18(gradleKts))
66297
67087
  return null;
66298
67088
  try {
@@ -66300,7 +67090,7 @@ async function detectJvm(workdir) {
66300
67090
  const content2 = await Bun.file(pom).text();
66301
67091
  const nameMatch = content2.match(/<artifactId>([^<]+)<\/artifactId>/);
66302
67092
  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";
67093
+ const lang2 = existsSync18(join26(workdir, "src/main/kotlin")) ? "Kotlin" : "Java";
66304
67094
  return { name: nameMatch?.[1], lang: lang2, dependencies: deps2 };
66305
67095
  }
66306
67096
  const gradleFile = existsSync18(gradleKts) ? gradleKts : gradle;
@@ -66521,7 +67311,7 @@ async function generateFor(agent, options, config2) {
66521
67311
  try {
66522
67312
  const context = await loadContextContent(options, config2);
66523
67313
  const content = generator.generate(context);
66524
- const outputPath = join26(options.outputDir, generator.outputFile);
67314
+ const outputPath = join27(options.outputDir, generator.outputFile);
66525
67315
  validateFilePath(outputPath, options.outputDir);
66526
67316
  if (!options.dryRun) {
66527
67317
  await Bun.write(outputPath, content);
@@ -66538,7 +67328,7 @@ async function generateAll(options, config2) {
66538
67328
  for (const [agentKey, generator] of Object.entries(GENERATORS)) {
66539
67329
  try {
66540
67330
  const content = generator.generate(context);
66541
- const outputPath = join26(options.outputDir, generator.outputFile);
67331
+ const outputPath = join27(options.outputDir, generator.outputFile);
66542
67332
  validateFilePath(outputPath, options.outputDir);
66543
67333
  if (!options.dryRun) {
66544
67334
  await Bun.write(outputPath, content);
@@ -66556,8 +67346,8 @@ async function generateAll(options, config2) {
66556
67346
  var VALID_AGENTS = ["claude", "codex", "opencode", "cursor", "windsurf", "aider", "gemini"];
66557
67347
  async function generateCommand(options) {
66558
67348
  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;
67349
+ const contextPath = options.context ? join28(workdir, options.context) : join28(workdir, "nax/context.md");
67350
+ const outputDir = options.output ? join28(workdir, options.output) : workdir;
66561
67351
  const autoInject = !options.noAutoInject;
66562
67352
  const dryRun = options.dryRun ?? false;
66563
67353
  if (!existsSync20(contextPath)) {
@@ -66633,7 +67423,7 @@ async function generateCommand(options) {
66633
67423
  // src/cli/config-display.ts
66634
67424
  init_loader2();
66635
67425
  import { existsSync as existsSync22 } from "fs";
66636
- import { join as join29 } from "path";
67426
+ import { join as join30 } from "path";
66637
67427
 
66638
67428
  // src/cli/config-descriptions.ts
66639
67429
  var FIELD_DESCRIPTIONS = {
@@ -66839,7 +67629,7 @@ function deepEqual(a, b) {
66839
67629
  init_defaults();
66840
67630
  init_loader2();
66841
67631
  import { existsSync as existsSync21 } from "fs";
66842
- import { join as join28 } from "path";
67632
+ import { join as join29 } from "path";
66843
67633
  async function loadConfigFile(path14) {
66844
67634
  if (!existsSync21(path14))
66845
67635
  return null;
@@ -66861,7 +67651,7 @@ async function loadProjectConfig() {
66861
67651
  const projectDir = findProjectDir();
66862
67652
  if (!projectDir)
66863
67653
  return null;
66864
- const projectPath = join28(projectDir, "config.json");
67654
+ const projectPath = join29(projectDir, "config.json");
66865
67655
  return await loadConfigFile(projectPath);
66866
67656
  }
66867
67657
 
@@ -66921,7 +67711,7 @@ async function configCommand(config2, options = {}) {
66921
67711
  function determineConfigSources() {
66922
67712
  const globalPath = globalConfigPath();
66923
67713
  const projectDir = findProjectDir();
66924
- const projectPath = projectDir ? join29(projectDir, "config.json") : null;
67714
+ const projectPath = projectDir ? join30(projectDir, "config.json") : null;
66925
67715
  return {
66926
67716
  global: fileExists(globalPath) ? globalPath : null,
66927
67717
  project: projectPath && fileExists(projectPath) ? projectPath : null
@@ -67101,21 +67891,21 @@ async function diagnose(options) {
67101
67891
 
67102
67892
  // src/commands/logs.ts
67103
67893
  import { existsSync as existsSync24 } from "fs";
67104
- import { join as join32 } from "path";
67894
+ import { join as join33 } from "path";
67105
67895
 
67106
67896
  // src/commands/logs-formatter.ts
67107
67897
  init_source();
67108
67898
  init_formatter();
67109
67899
  import { readdirSync as readdirSync6 } from "fs";
67110
- import { join as join31 } from "path";
67900
+ import { join as join32 } from "path";
67111
67901
 
67112
67902
  // src/commands/logs-reader.ts
67113
67903
  import { existsSync as existsSync23, readdirSync as readdirSync5 } from "fs";
67114
67904
  import { readdir as readdir3 } from "fs/promises";
67115
67905
  import { homedir as homedir3 } from "os";
67116
- import { join as join30 } from "path";
67906
+ import { join as join31 } from "path";
67117
67907
  var _deps5 = {
67118
- getRunsDir: () => process.env.NAX_RUNS_DIR ?? join30(homedir3(), ".nax", "runs")
67908
+ getRunsDir: () => process.env.NAX_RUNS_DIR ?? join31(homedir3(), ".nax", "runs")
67119
67909
  };
67120
67910
  async function resolveRunFileFromRegistry(runId) {
67121
67911
  const runsDir = _deps5.getRunsDir();
@@ -67127,7 +67917,7 @@ async function resolveRunFileFromRegistry(runId) {
67127
67917
  }
67128
67918
  let matched = null;
67129
67919
  for (const entry of entries) {
67130
- const metaPath = join30(runsDir, entry, "meta.json");
67920
+ const metaPath = join31(runsDir, entry, "meta.json");
67131
67921
  try {
67132
67922
  const meta3 = await Bun.file(metaPath).json();
67133
67923
  if (meta3.runId === runId || meta3.runId.startsWith(runId)) {
@@ -67149,14 +67939,14 @@ async function resolveRunFileFromRegistry(runId) {
67149
67939
  return null;
67150
67940
  }
67151
67941
  const specificFile = files.find((f) => f === `${matched.runId}.jsonl`);
67152
- return join30(matched.eventsDir, specificFile ?? files[0]);
67942
+ return join31(matched.eventsDir, specificFile ?? files[0]);
67153
67943
  }
67154
67944
  async function selectRunFile(runsDir) {
67155
67945
  const files = readdirSync5(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
67156
67946
  if (files.length === 0) {
67157
67947
  return null;
67158
67948
  }
67159
- return join30(runsDir, files[0]);
67949
+ return join31(runsDir, files[0]);
67160
67950
  }
67161
67951
  async function extractRunSummary(filePath) {
67162
67952
  const file2 = Bun.file(filePath);
@@ -67241,7 +68031,7 @@ Runs:
67241
68031
  console.log(source_default.gray(" Timestamp Stories Duration Cost Status"));
67242
68032
  console.log(source_default.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
67243
68033
  for (const file2 of files) {
67244
- const filePath = join31(runsDir, file2);
68034
+ const filePath = join32(runsDir, file2);
67245
68035
  const summary = await extractRunSummary(filePath);
67246
68036
  const timestamp = file2.replace(".jsonl", "");
67247
68037
  const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
@@ -67366,7 +68156,7 @@ async function logsCommand(options) {
67366
68156
  return;
67367
68157
  }
67368
68158
  const resolved = resolveProject({ dir: options.dir });
67369
- const naxDir = join32(resolved.projectDir, "nax");
68159
+ const naxDir = join33(resolved.projectDir, "nax");
67370
68160
  const configPath = resolved.configPath;
67371
68161
  const configFile = Bun.file(configPath);
67372
68162
  const config2 = await configFile.json();
@@ -67374,8 +68164,8 @@ async function logsCommand(options) {
67374
68164
  if (!featureName) {
67375
68165
  throw new Error("No feature specified in config.json");
67376
68166
  }
67377
- const featureDir = join32(naxDir, "features", featureName);
67378
- const runsDir = join32(featureDir, "runs");
68167
+ const featureDir = join33(naxDir, "features", featureName);
68168
+ const runsDir = join33(featureDir, "runs");
67379
68169
  if (!existsSync24(runsDir)) {
67380
68170
  throw new Error(`No runs directory found for feature: ${featureName}`);
67381
68171
  }
@@ -67400,7 +68190,7 @@ init_config();
67400
68190
  init_prd();
67401
68191
  init_precheck();
67402
68192
  import { existsSync as existsSync29 } from "fs";
67403
- import { join as join33 } from "path";
68193
+ import { join as join34 } from "path";
67404
68194
  async function precheckCommand(options) {
67405
68195
  const resolved = resolveProject({
67406
68196
  dir: options.dir,
@@ -67416,9 +68206,9 @@ async function precheckCommand(options) {
67416
68206
  process.exit(1);
67417
68207
  }
67418
68208
  }
67419
- const naxDir = join33(resolved.projectDir, "nax");
67420
- const featureDir = join33(naxDir, "features", featureName);
67421
- const prdPath = join33(featureDir, "prd.json");
68209
+ const naxDir = join34(resolved.projectDir, "nax");
68210
+ const featureDir = join34(naxDir, "features", featureName);
68211
+ const prdPath = join34(featureDir, "prd.json");
67422
68212
  if (!existsSync29(featureDir)) {
67423
68213
  console.error(source_default.red(`Feature not found: ${featureName}`));
67424
68214
  process.exit(1);
@@ -67442,10 +68232,10 @@ async function precheckCommand(options) {
67442
68232
  init_source();
67443
68233
  import { readdir as readdir4 } from "fs/promises";
67444
68234
  import { homedir as homedir4 } from "os";
67445
- import { join as join34 } from "path";
68235
+ import { join as join35 } from "path";
67446
68236
  var DEFAULT_LIMIT = 20;
67447
68237
  var _deps7 = {
67448
- getRunsDir: () => join34(homedir4(), ".nax", "runs")
68238
+ getRunsDir: () => join35(homedir4(), ".nax", "runs")
67449
68239
  };
67450
68240
  function formatDuration3(ms) {
67451
68241
  if (ms <= 0)
@@ -67497,7 +68287,7 @@ async function runsCommand(options = {}) {
67497
68287
  }
67498
68288
  const rows = [];
67499
68289
  for (const entry of entries) {
67500
- const metaPath = join34(runsDir, entry, "meta.json");
68290
+ const metaPath = join35(runsDir, entry, "meta.json");
67501
68291
  let meta3;
67502
68292
  try {
67503
68293
  meta3 = await Bun.file(metaPath).json();
@@ -67574,7 +68364,7 @@ async function runsCommand(options = {}) {
67574
68364
 
67575
68365
  // src/commands/unlock.ts
67576
68366
  init_source();
67577
- import { join as join35 } from "path";
68367
+ import { join as join36 } from "path";
67578
68368
  function isProcessAlive3(pid) {
67579
68369
  try {
67580
68370
  process.kill(pid, 0);
@@ -67589,7 +68379,7 @@ function formatLockAge(ageMs) {
67589
68379
  }
67590
68380
  async function unlockCommand(options) {
67591
68381
  const workdir = options.dir ?? process.cwd();
67592
- const lockPath = join35(workdir, "nax.lock");
68382
+ const lockPath = join36(workdir, "nax.lock");
67593
68383
  const lockFile = Bun.file(lockPath);
67594
68384
  const exists = await lockFile.exists();
67595
68385
  if (!exists) {
@@ -67628,6 +68418,7 @@ async function unlockCommand(options) {
67628
68418
  init_config();
67629
68419
 
67630
68420
  // src/execution/runner.ts
68421
+ init_registry();
67631
68422
  init_hooks();
67632
68423
  init_logger2();
67633
68424
  init_prd();
@@ -67637,6 +68428,7 @@ init_crash_recovery();
67637
68428
  init_hooks();
67638
68429
  init_logger2();
67639
68430
  init_prd();
68431
+ init_git();
67640
68432
  init_crash_recovery();
67641
68433
  init_story_context();
67642
68434
  async function runCompletionPhase(options) {
@@ -67646,7 +68438,7 @@ async function runCompletionPhase(options) {
67646
68438
  const acceptanceResult = await runAcceptanceLoop2({
67647
68439
  config: options.config,
67648
68440
  prd: options.prd,
67649
- prdPath: "",
68441
+ prdPath: options.prdPath,
67650
68442
  workdir: options.workdir,
67651
68443
  featureDir: options.featureDir,
67652
68444
  hooks: options.hooks,
@@ -67657,7 +68449,8 @@ async function runCompletionPhase(options) {
67657
68449
  allStoryMetrics: options.allStoryMetrics,
67658
68450
  pluginRegistry: options.pluginRegistry,
67659
68451
  eventEmitter: options.eventEmitter,
67660
- statusWriter: options.statusWriter
68452
+ statusWriter: options.statusWriter,
68453
+ agentGetFn: options.agentGetFn
67661
68454
  });
67662
68455
  Object.assign(options, {
67663
68456
  prd: acceptanceResult.prd,
@@ -67708,6 +68501,7 @@ async function runCompletionPhase(options) {
67708
68501
  }
67709
68502
  stopHeartbeat();
67710
68503
  await writeExitSummary(options.logFilePath, options.totalCost, options.iterations, options.storiesCompleted, durationMs);
68504
+ await autoCommitIfDirty(options.workdir, "run.complete", "run-summary", options.feature);
67711
68505
  return {
67712
68506
  durationMs,
67713
68507
  runCompletedAt
@@ -67858,7 +68652,9 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
67858
68652
  logFilePath: options.logFilePath,
67859
68653
  runId: options.runId,
67860
68654
  startTime: options.startTime,
67861
- batchPlan
68655
+ batchPlan,
68656
+ agentGetFn: options.agentGetFn,
68657
+ pidRegistry: options.pidRegistry
67862
68658
  }, prd);
67863
68659
  prd = sequentialResult.prd;
67864
68660
  iterations = sequentialResult.iterations;
@@ -67896,7 +68692,8 @@ async function runSetupPhase(options) {
67896
68692
  getTotalCost: options.getTotalCost,
67897
68693
  getIterations: options.getIterations,
67898
68694
  getStoriesCompleted: options.getStoriesCompleted,
67899
- getTotalStories: options.getTotalStories
68695
+ getTotalStories: options.getTotalStories,
68696
+ agentGetFn: options.agentGetFn
67900
68697
  });
67901
68698
  return setupResult;
67902
68699
  }
@@ -67934,6 +68731,8 @@ async function run(options) {
67934
68731
  let totalCost = 0;
67935
68732
  const allStoryMetrics = [];
67936
68733
  const logger = getSafeLogger();
68734
+ const registry2 = createAgentRegistry(config2);
68735
+ const agentGetFn = registry2.getAgent.bind(registry2);
67937
68736
  let prd;
67938
68737
  const setupResult = await runSetupPhase({
67939
68738
  prdPath,
@@ -67951,6 +68750,7 @@ async function run(options) {
67951
68750
  skipPrecheck,
67952
68751
  headless,
67953
68752
  formatterMode,
68753
+ agentGetFn,
67954
68754
  getTotalCost: () => totalCost,
67955
68755
  getIterations: () => iterations,
67956
68756
  getStoriesCompleted: () => storiesCompleted,
@@ -67978,7 +68778,9 @@ async function run(options) {
67978
68778
  formatterMode,
67979
68779
  headless,
67980
68780
  parallel,
67981
- runParallelExecution: _runnerDeps.runParallelExecution ?? undefined
68781
+ runParallelExecution: _runnerDeps.runParallelExecution ?? undefined,
68782
+ agentGetFn,
68783
+ pidRegistry
67982
68784
  }, prd, pluginRegistry);
67983
68785
  prd = executionResult.prd;
67984
68786
  iterations = executionResult.iterations;
@@ -67999,6 +68801,7 @@ async function run(options) {
67999
68801
  hooks,
68000
68802
  feature,
68001
68803
  workdir,
68804
+ prdPath,
68002
68805
  statusFile,
68003
68806
  logFilePath,
68004
68807
  runId,
@@ -68014,7 +68817,8 @@ async function run(options) {
68014
68817
  iterations,
68015
68818
  statusWriter,
68016
68819
  pluginRegistry,
68017
- eventEmitter
68820
+ eventEmitter,
68821
+ agentGetFn
68018
68822
  });
68019
68823
  const { durationMs } = completionResult;
68020
68824
  return {
@@ -75357,15 +76161,15 @@ program2.command("init").description("Initialize nax in the current project").op
75357
76161
  console.error(source_default.red(`Invalid directory: ${err.message}`));
75358
76162
  process.exit(1);
75359
76163
  }
75360
- const naxDir = join42(workdir, "nax");
76164
+ const naxDir = join43(workdir, "nax");
75361
76165
  if (existsSync32(naxDir) && !options.force) {
75362
76166
  console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
75363
76167
  return;
75364
76168
  }
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({
76169
+ mkdirSync6(join43(naxDir, "features"), { recursive: true });
76170
+ mkdirSync6(join43(naxDir, "hooks"), { recursive: true });
76171
+ await Bun.write(join43(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
76172
+ await Bun.write(join43(naxDir, "hooks.json"), JSON.stringify({
75369
76173
  hooks: {
75370
76174
  "on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
75371
76175
  "on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
@@ -75373,12 +76177,12 @@ program2.command("init").description("Initialize nax in the current project").op
75373
76177
  "on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false }
75374
76178
  }
75375
76179
  }, null, 2));
75376
- await Bun.write(join42(naxDir, ".gitignore"), `# nax temp files
76180
+ await Bun.write(join43(naxDir, ".gitignore"), `# nax temp files
75377
76181
  *.tmp
75378
76182
  .paused.json
75379
76183
  .nax-verifier-verdict.json
75380
76184
  `);
75381
- await Bun.write(join42(naxDir, "context.md"), `# Project Context
76185
+ await Bun.write(join43(naxDir, "context.md"), `# Project Context
75382
76186
 
75383
76187
  This document defines coding standards, architectural decisions, and forbidden patterns for this project.
75384
76188
  Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
@@ -75496,16 +76300,16 @@ program2.command("run").description("Run the orchestration loop for a feature").
75496
76300
  console.error(source_default.red("nax not initialized. Run: nax init"));
75497
76301
  process.exit(1);
75498
76302
  }
75499
- const featureDir = join42(naxDir, "features", options.feature);
75500
- const prdPath = join42(featureDir, "prd.json");
76303
+ const featureDir = join43(naxDir, "features", options.feature);
76304
+ const prdPath = join43(featureDir, "prd.json");
75501
76305
  if (!existsSync32(prdPath)) {
75502
76306
  console.error(source_default.red(`Feature "${options.feature}" not found or missing prd.json`));
75503
76307
  process.exit(1);
75504
76308
  }
75505
- const runsDir = join42(featureDir, "runs");
76309
+ const runsDir = join43(featureDir, "runs");
75506
76310
  mkdirSync6(runsDir, { recursive: true });
75507
76311
  const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
75508
- const logFilePath = join42(runsDir, `${runId}.jsonl`);
76312
+ const logFilePath = join43(runsDir, `${runId}.jsonl`);
75509
76313
  const isTTY = process.stdout.isTTY ?? false;
75510
76314
  const headlessFlag = options.headless ?? false;
75511
76315
  const headlessEnv = process.env.NAX_HEADLESS === "1";
@@ -75521,7 +76325,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
75521
76325
  config2.autoMode.defaultAgent = options.agent;
75522
76326
  }
75523
76327
  config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
75524
- const globalNaxDir = join42(homedir8(), ".nax");
76328
+ const globalNaxDir = join43(homedir8(), ".nax");
75525
76329
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
75526
76330
  const eventEmitter = new PipelineEventEmitter;
75527
76331
  let tuiInstance;
@@ -75544,7 +76348,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
75544
76348
  } else {
75545
76349
  console.log(source_default.dim(" [Headless mode \u2014 pipe output]"));
75546
76350
  }
75547
- const statusFilePath = join42(workdir, "nax", "status.json");
76351
+ const statusFilePath = join43(workdir, "nax", "status.json");
75548
76352
  let parallel;
75549
76353
  if (options.parallel !== undefined) {
75550
76354
  parallel = Number.parseInt(options.parallel, 10);
@@ -75570,7 +76374,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
75570
76374
  headless: useHeadless,
75571
76375
  skipPrecheck: options.skipPrecheck ?? false
75572
76376
  });
75573
- const latestSymlink = join42(runsDir, "latest.jsonl");
76377
+ const latestSymlink = join43(runsDir, "latest.jsonl");
75574
76378
  try {
75575
76379
  if (existsSync32(latestSymlink)) {
75576
76380
  Bun.spawnSync(["rm", latestSymlink]);
@@ -75608,9 +76412,9 @@ features.command("create <name>").description("Create a new feature").option("-d
75608
76412
  console.error(source_default.red("nax not initialized. Run: nax init"));
75609
76413
  process.exit(1);
75610
76414
  }
75611
- const featureDir = join42(naxDir, "features", name);
76415
+ const featureDir = join43(naxDir, "features", name);
75612
76416
  mkdirSync6(featureDir, { recursive: true });
75613
- await Bun.write(join42(featureDir, "spec.md"), `# Feature: ${name}
76417
+ await Bun.write(join43(featureDir, "spec.md"), `# Feature: ${name}
75614
76418
 
75615
76419
  ## Overview
75616
76420
 
@@ -75618,7 +76422,7 @@ features.command("create <name>").description("Create a new feature").option("-d
75618
76422
 
75619
76423
  ## Acceptance Criteria
75620
76424
  `);
75621
- await Bun.write(join42(featureDir, "plan.md"), `# Plan: ${name}
76425
+ await Bun.write(join43(featureDir, "plan.md"), `# Plan: ${name}
75622
76426
 
75623
76427
  ## Architecture
75624
76428
 
@@ -75626,7 +76430,7 @@ features.command("create <name>").description("Create a new feature").option("-d
75626
76430
 
75627
76431
  ## Dependencies
75628
76432
  `);
75629
- await Bun.write(join42(featureDir, "tasks.md"), `# Tasks: ${name}
76433
+ await Bun.write(join43(featureDir, "tasks.md"), `# Tasks: ${name}
75630
76434
 
75631
76435
  ## US-001: [Title]
75632
76436
 
@@ -75635,7 +76439,7 @@ features.command("create <name>").description("Create a new feature").option("-d
75635
76439
  ### Acceptance Criteria
75636
76440
  - [ ] Criterion 1
75637
76441
  `);
75638
- await Bun.write(join42(featureDir, "progress.txt"), `# Progress: ${name}
76442
+ await Bun.write(join43(featureDir, "progress.txt"), `# Progress: ${name}
75639
76443
 
75640
76444
  Created: ${new Date().toISOString()}
75641
76445
 
@@ -75663,7 +76467,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
75663
76467
  console.error(source_default.red("nax not initialized."));
75664
76468
  process.exit(1);
75665
76469
  }
75666
- const featuresDir = join42(naxDir, "features");
76470
+ const featuresDir = join43(naxDir, "features");
75667
76471
  if (!existsSync32(featuresDir)) {
75668
76472
  console.log(source_default.dim("No features yet."));
75669
76473
  return;
@@ -75678,7 +76482,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
75678
76482
  Features:
75679
76483
  `));
75680
76484
  for (const name of entries) {
75681
- const prdPath = join42(featuresDir, name, "prd.json");
76485
+ const prdPath = join43(featuresDir, name, "prd.json");
75682
76486
  if (existsSync32(prdPath)) {
75683
76487
  const prd = await loadPRD(prdPath);
75684
76488
  const c = countStories(prd);
@@ -75731,7 +76535,7 @@ program2.command("analyze").description("Parse spec.md into prd.json via agent d
75731
76535
  console.error(source_default.red("nax not initialized. Run: nax init"));
75732
76536
  process.exit(1);
75733
76537
  }
75734
- const featureDir = join42(naxDir, "features", options.feature);
76538
+ const featureDir = join43(naxDir, "features", options.feature);
75735
76539
  if (!existsSync32(featureDir)) {
75736
76540
  console.error(source_default.red(`Feature "${options.feature}" not found.`));
75737
76541
  process.exit(1);
@@ -75747,7 +76551,7 @@ program2.command("analyze").description("Parse spec.md into prd.json via agent d
75747
76551
  specPath: options.from,
75748
76552
  reclassify: options.reclassify
75749
76553
  });
75750
- const prdPath = join42(featureDir, "prd.json");
76554
+ const prdPath = join43(featureDir, "prd.json");
75751
76555
  await Bun.write(prdPath, JSON.stringify(prd, null, 2));
75752
76556
  const c = countStories(prd);
75753
76557
  console.log(source_default.green(`