@nathapp/nax 0.78.0 → 0.79.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.
package/dist/nax.js CHANGED
@@ -16640,7 +16640,10 @@ var init_schemas_context = __esm(() => {
16640
16640
  enabled: exports_external.boolean().default(true),
16641
16641
  maxStoryAge: exports_external.number().int().min(1).default(10),
16642
16642
  scoreMultiplier: exports_external.number().min(0).max(1).default(0.4)
16643
- }).default(() => ({ enabled: true, maxStoryAge: 10, scoreMultiplier: 0.4 }))
16643
+ }).default(() => ({ enabled: true, maxStoryAge: 10, scoreMultiplier: 0.4 })),
16644
+ manifest: exports_external.object({
16645
+ retentionDays: exports_external.number().int().min(1)
16646
+ }).optional()
16644
16647
  }).default(() => ({
16645
16648
  enabled: false,
16646
16649
  minScore: 0.1,
@@ -17984,9 +17987,9 @@ var init_severity = __esm(() => {
17984
17987
  info: 0,
17985
17988
  unverifiable: 0,
17986
17989
  low: 1,
17987
- warning: 1,
17988
- error: 2,
17989
- critical: 3
17990
+ warning: 2,
17991
+ error: 3,
17992
+ critical: 4
17990
17993
  };
17991
17994
  });
17992
17995
 
@@ -18681,6 +18684,17 @@ var init_logger = __esm(() => {
18681
18684
  });
18682
18685
 
18683
18686
  // src/logger/index.ts
18687
+ var exports_logger = {};
18688
+ __export(exports_logger, {
18689
+ resetLogger: () => resetLogger,
18690
+ initLogger: () => initLogger,
18691
+ getSafeLogger: () => getSafeLogger,
18692
+ getLogger: () => getLogger,
18693
+ formatJsonl: () => formatJsonl,
18694
+ formatConsole: () => formatConsole,
18695
+ addSink: () => addSink,
18696
+ Logger: () => Logger
18697
+ });
18684
18698
  var init_logger2 = __esm(() => {
18685
18699
  init_logger();
18686
18700
  init_formatters();
@@ -18836,6 +18850,55 @@ var init_config_guards = __esm(() => {
18836
18850
  };
18837
18851
  });
18838
18852
 
18853
+ // src/config/dotenv.ts
18854
+ function parseDotenv(content) {
18855
+ if (!content)
18856
+ return {};
18857
+ const result = {};
18858
+ for (const rawLine of content.split(`
18859
+ `)) {
18860
+ const line = rawLine.trim();
18861
+ if (!line || line.startsWith("#"))
18862
+ continue;
18863
+ const stripped = line.startsWith("export ") ? line.slice(7).trim() : line;
18864
+ const eqIndex = stripped.indexOf("=");
18865
+ if (eqIndex === -1)
18866
+ continue;
18867
+ const key = stripped.slice(0, eqIndex).trim();
18868
+ let value = stripped.slice(eqIndex + 1).trim();
18869
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
18870
+ value = value.slice(1, -1);
18871
+ }
18872
+ result[key] = value;
18873
+ }
18874
+ return result;
18875
+ }
18876
+ function resolveEnvVars(config2, env2) {
18877
+ if (typeof config2 === "string") {
18878
+ return resolveString(config2, env2);
18879
+ }
18880
+ if (Array.isArray(config2)) {
18881
+ return config2.map((item) => resolveEnvVars(item, env2));
18882
+ }
18883
+ if (config2 !== null && typeof config2 === "object") {
18884
+ const result = {};
18885
+ for (const [key, value] of Object.entries(config2)) {
18886
+ result[key] = resolveEnvVars(value, env2);
18887
+ }
18888
+ return result;
18889
+ }
18890
+ return config2;
18891
+ }
18892
+ function resolveString(str, env2) {
18893
+ return str.replace(/\$\$([A-Za-z_][A-Za-z0-9_]*)/g, `${DOUBLE_DOLLAR_PLACEHOLDER}$1`).replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, varName) => {
18894
+ if (!(varName in env2)) {
18895
+ throw new Error(`Environment variable $${varName} (${varName}) is not defined`);
18896
+ }
18897
+ return env2[varName];
18898
+ }).replace(new RegExp(`${DOUBLE_DOLLAR_PLACEHOLDER}([A-Za-z_][A-Za-z0-9_]*)`, "g"), "$$$1");
18899
+ }
18900
+ var DOUBLE_DOLLAR_PLACEHOLDER = "__DOLLAR_ESCAPE__";
18901
+
18839
18902
  // src/config/merge.ts
18840
18903
  function mergePackageConfig(root, packageOverride) {
18841
18904
  const hasAnyMergeableField = packageOverride.agent !== undefined || packageOverride.models !== undefined || packageOverride.routing !== undefined || packageOverride.execution !== undefined || packageOverride.review !== undefined || packageOverride.acceptance !== undefined || packageOverride.quality !== undefined || packageOverride.context !== undefined || packageOverride.project !== undefined;
@@ -18874,6 +18937,10 @@ function mergePackageConfig(root, packageOverride) {
18874
18937
  mutationCheck: {
18875
18938
  ...root.execution.mutationCheck,
18876
18939
  ...packageOverride.execution?.mutationCheck
18940
+ },
18941
+ rectification: {
18942
+ ...root.execution.rectification,
18943
+ ...packageOverride.execution?.rectification
18877
18944
  }
18878
18945
  },
18879
18946
  review: {
@@ -18910,11 +18977,13 @@ function mergePackageConfig(root, packageOverride) {
18910
18977
  },
18911
18978
  ...packageOverride.review?.commands
18912
18979
  },
18913
- semantic: packageOverride.review?.semantic !== undefined ? { ...root.review.semantic, ...packageOverride.review.semantic } : root.review.semantic
18980
+ semantic: packageOverride.review?.semantic !== undefined ? { ...root.review.semantic, ...packageOverride.review.semantic } : root.review.semantic,
18981
+ adversarial: packageOverride.review?.adversarial !== undefined ? { ...root.review.adversarial, ...packageOverride.review.adversarial } : root.review.adversarial
18914
18982
  },
18915
18983
  acceptance: {
18916
18984
  ...root.acceptance,
18917
- ...packageOverride.acceptance
18985
+ ...packageOverride.acceptance,
18986
+ fix: packageOverride.acceptance?.fix !== undefined ? { ...root.acceptance.fix, ...packageOverride.acceptance.fix } : root.acceptance.fix
18918
18987
  },
18919
18988
  quality: {
18920
18989
  ...root.quality,
@@ -18922,7 +18991,9 @@ function mergePackageConfig(root, packageOverride) {
18922
18991
  ...root.quality.commands,
18923
18992
  ...packageOverride.quality?.commands
18924
18993
  },
18925
- testing: packageOverride.quality?.testing !== undefined ? { ...root.quality.testing, ...packageOverride.quality.testing } : root.quality.testing
18994
+ testing: packageOverride.quality?.testing !== undefined ? { ...root.quality.testing, ...packageOverride.quality.testing } : root.quality.testing,
18995
+ autofix: packageOverride.quality?.autofix !== undefined ? { ...root.quality.autofix, ...packageOverride.quality.autofix } : root.quality.autofix,
18996
+ lintOutput: packageOverride.quality?.lintOutput !== undefined ? { ...root.quality.lintOutput, ...packageOverride.quality.lintOutput } : root.quality.lintOutput
18926
18997
  },
18927
18998
  context: {
18928
18999
  ...root.context,
@@ -18935,6 +19006,10 @@ function mergePackageConfig(root, packageOverride) {
18935
19006
  stages: {
18936
19007
  ...root.context.v2?.stages,
18937
19008
  ...packageOverride.context?.v2?.stages
19009
+ },
19010
+ rules: {
19011
+ ...root.context.v2?.rules,
19012
+ ...packageOverride.context?.v2?.rules
18938
19013
  }
18939
19014
  }
18940
19015
  },
@@ -19153,55 +19228,6 @@ function projectConfigDir(projectRoot) {
19153
19228
  var GLOBAL_CONFIG_DIR_ENV = "NAX_GLOBAL_CONFIG_DIR", PROJECT_NAX_DIR = ".nax";
19154
19229
  var init_paths = () => {};
19155
19230
 
19156
- // src/config/dotenv.ts
19157
- function parseDotenv(content) {
19158
- if (!content)
19159
- return {};
19160
- const result = {};
19161
- for (const rawLine of content.split(`
19162
- `)) {
19163
- const line = rawLine.trim();
19164
- if (!line || line.startsWith("#"))
19165
- continue;
19166
- const stripped = line.startsWith("export ") ? line.slice(7).trim() : line;
19167
- const eqIndex = stripped.indexOf("=");
19168
- if (eqIndex === -1)
19169
- continue;
19170
- const key = stripped.slice(0, eqIndex).trim();
19171
- let value = stripped.slice(eqIndex + 1).trim();
19172
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
19173
- value = value.slice(1, -1);
19174
- }
19175
- result[key] = value;
19176
- }
19177
- return result;
19178
- }
19179
- function resolveEnvVars(config2, env2) {
19180
- if (typeof config2 === "string") {
19181
- return resolveString(config2, env2);
19182
- }
19183
- if (Array.isArray(config2)) {
19184
- return config2.map((item) => resolveEnvVars(item, env2));
19185
- }
19186
- if (config2 !== null && typeof config2 === "object") {
19187
- const result = {};
19188
- for (const [key, value] of Object.entries(config2)) {
19189
- result[key] = resolveEnvVars(value, env2);
19190
- }
19191
- return result;
19192
- }
19193
- return config2;
19194
- }
19195
- function resolveString(str, env2) {
19196
- return str.replace(/\$\$([A-Za-z_][A-Za-z0-9_]*)/g, `${DOUBLE_DOLLAR_PLACEHOLDER}$1`).replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, varName) => {
19197
- if (!(varName in env2)) {
19198
- throw new Error(`Environment variable $${varName} (${varName}) is not defined`);
19199
- }
19200
- return env2[varName];
19201
- }).replace(new RegExp(`${DOUBLE_DOLLAR_PLACEHOLDER}([A-Za-z_][A-Za-z0-9_]*)`, "g"), "$$$1");
19202
- }
19203
- var DOUBLE_DOLLAR_PLACEHOLDER = "__DOLLAR_ESCAPE__";
19204
-
19205
19231
  // src/config/profile.ts
19206
19232
  import { readdirSync } from "fs";
19207
19233
  import { join as join2 } from "path";
@@ -19471,8 +19497,9 @@ async function loadConfig(startDir, cliOverrides) {
19471
19497
  }
19472
19498
  for (const name of overlayChain) {
19473
19499
  const profileData = await loadProfile(name, projectRoot);
19474
- rawConfig = deepMergeConfig(rawConfig, profileData);
19475
- await loadProfileEnv(name, projectRoot);
19500
+ const profileEnv = await loadProfileEnv(name, projectRoot);
19501
+ const resolvedProfileData = Object.keys(profileEnv).length > 0 ? resolveEnvVars(profileData, profileEnv) : profileData;
19502
+ rawConfig = deepMergeConfig(rawConfig, resolvedProfileData);
19476
19503
  }
19477
19504
  if (cliOverrides) {
19478
19505
  rawConfig = deepMergeConfig(rawConfig, cliOverrides);
@@ -20052,6 +20079,7 @@ __export(exports_config, {
20052
20079
  DESCRIPTION_QUALITY_RULES: () => DESCRIPTION_QUALITY_RULES,
20053
20080
  DEFAULT_CONFIG: () => DEFAULT_CONFIG,
20054
20081
  DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG: () => DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG,
20082
+ ContextV2ConfigSchema: () => ContextV2ConfigSchema,
20055
20083
  ConfiguredModelSchema: () => ConfiguredModelSchema,
20056
20084
  COMPLEXITY_GUIDE: () => COMPLEXITY_GUIDE,
20057
20085
  AutoRouteConfigSchema: () => AutoRouteConfigSchema,
@@ -20097,9 +20125,11 @@ var CompleteError, SessionFailureError, SessionTurnError;
20097
20125
  var init_types3 = __esm(() => {
20098
20126
  CompleteError = class CompleteError extends Error {
20099
20127
  exitCode;
20100
- constructor(message, exitCode) {
20128
+ retryable;
20129
+ constructor(message, exitCode, retryable) {
20101
20130
  super(message);
20102
20131
  this.exitCode = exitCode;
20132
+ this.retryable = retryable;
20103
20133
  this.name = "CompleteError";
20104
20134
  }
20105
20135
  };
@@ -20276,6 +20306,21 @@ var init_bun_deps = __esm(() => {
20276
20306
  });
20277
20307
 
20278
20308
  // src/agents/acp/parse-agent-error.ts
20309
+ function classifyCompleteError(error48) {
20310
+ if (error48.retryable === undefined)
20311
+ return null;
20312
+ return {
20313
+ output: error48.message,
20314
+ tokenUsage: { inputTokens: 0, outputTokens: 0 },
20315
+ estimatedCostUsd: 0,
20316
+ adapterFailure: {
20317
+ category: "quality",
20318
+ outcome: "fail-adapter-error",
20319
+ retriable: error48.retryable,
20320
+ message: error48.message.slice(0, 500)
20321
+ }
20322
+ };
20323
+ }
20279
20324
  function parseAgentError(stderr) {
20280
20325
  if (!stderr) {
20281
20326
  return { type: "unknown" };
@@ -20603,42 +20648,55 @@ function parseSessionIds(stdout) {
20603
20648
  return { sessionId: undefined, recordId: undefined };
20604
20649
  }
20605
20650
 
20606
- // src/agents/acp/spawn-client.ts
20607
- import { randomUUID } from "crypto";
20608
- async function readAndParseLines(stream, state, onActivity) {
20651
+ // src/agents/acp/stdout-line-reader.ts
20652
+ function readAndParseLines(stream, state, onActivity) {
20609
20653
  const decoder = new TextDecoder;
20610
20654
  let remainder = "";
20611
20655
  const reader = stream.getReader();
20612
- try {
20613
- while (true) {
20614
- const { done, value } = await reader.read();
20615
- if (done)
20616
- break;
20617
- remainder += decoder.decode(value, { stream: true });
20618
- for (;; ) {
20619
- const nl = remainder.indexOf(`
20620
- `);
20621
- if (nl < 0)
20656
+ const promise2 = (async () => {
20657
+ try {
20658
+ while (true) {
20659
+ const { done, value } = await reader.read();
20660
+ if (done)
20622
20661
  break;
20623
- const line = remainder.slice(0, nl);
20624
- remainder = remainder.slice(nl + 1);
20625
- if (line.trim()) {
20626
- const activity = parseAcpxJsonLine(line, state);
20627
- if (activity && onActivity)
20628
- onActivity(activity);
20662
+ remainder += decoder.decode(value, { stream: true });
20663
+ for (;; ) {
20664
+ const nl = remainder.indexOf(`
20665
+ `);
20666
+ if (nl < 0)
20667
+ break;
20668
+ const line = remainder.slice(0, nl);
20669
+ remainder = remainder.slice(nl + 1);
20670
+ if (line.trim()) {
20671
+ const activity = parseAcpxJsonLine(line, state);
20672
+ if (activity && onActivity)
20673
+ onActivity(activity);
20674
+ }
20629
20675
  }
20630
20676
  }
20677
+ remainder += decoder.decode();
20678
+ if (remainder.trim()) {
20679
+ const activity = parseAcpxJsonLine(remainder.trim(), state);
20680
+ if (activity && onActivity)
20681
+ onActivity(activity);
20682
+ }
20683
+ } finally {
20684
+ reader.releaseLock();
20631
20685
  }
20632
- remainder += decoder.decode();
20633
- if (remainder.trim()) {
20634
- const activity = parseAcpxJsonLine(remainder.trim(), state);
20635
- if (activity && onActivity)
20636
- onActivity(activity);
20686
+ })();
20687
+ return {
20688
+ promise: promise2,
20689
+ cancel: () => {
20690
+ reader.cancel().catch(() => {});
20637
20691
  }
20638
- } finally {
20639
- reader.releaseLock();
20640
- }
20692
+ };
20641
20693
  }
20694
+ var init_stdout_line_reader = __esm(() => {
20695
+ init_agents();
20696
+ });
20697
+
20698
+ // src/agents/acp/spawn-client.ts
20699
+ import { randomUUID } from "crypto";
20642
20700
 
20643
20701
  class SpawnAcpSession {
20644
20702
  agentName;
@@ -20793,7 +20851,8 @@ class SpawnAcpSession {
20793
20851
  });
20794
20852
  }
20795
20853
  } : undefined;
20796
- const parsePromise = readAndParseLines(proc.stdout, parseState, onActivity).catch(() => {});
20854
+ const parseHandle = readAndParseLines(proc.stdout, parseState, onActivity);
20855
+ const parsePromise = parseHandle.promise.catch(() => {});
20797
20856
  const stderrPromise = new Response(proc.stderr).text().catch(() => "");
20798
20857
  const exitCode = await proc.exited;
20799
20858
  const makeDrain = (ms) => {
@@ -20805,10 +20864,16 @@ class SpawnAcpSession {
20805
20864
  };
20806
20865
  const drainA = makeDrain(_spawnClientDeps.streamDrainTimeoutMs);
20807
20866
  const drainB = makeDrain(_spawnClientDeps.streamDrainTimeoutMs);
20808
- const [, stderr] = await Promise.all([
20809
- Promise.race([parsePromise, drainA.promise]).finally(() => drainA.cancel()),
20867
+ const stdoutRaceResult = Promise.race([
20868
+ parsePromise.then(() => "parsed"),
20869
+ drainA.promise.then(() => "drain")
20870
+ ]).finally(() => drainA.cancel());
20871
+ const [stdoutWinner, stderr] = await Promise.all([
20872
+ stdoutRaceResult,
20810
20873
  Promise.race([stderrPromise, drainB.promise]).finally(() => drainB.cancel())
20811
20874
  ]);
20875
+ if (stdoutWinner === "drain")
20876
+ parseHandle.cancel();
20812
20877
  emit?.({ ...baseEvent, kind: "agent.process_update", status: "exited", exitCode, timestamp: now() });
20813
20878
  if (exitCode !== 0) {
20814
20879
  const parsedOnError = finalizeParseState(parseState);
@@ -21079,6 +21144,7 @@ var init_spawn_client = __esm(() => {
21079
21144
  init_env();
21080
21145
  init_model_spec();
21081
21146
  init_reasoning_effort();
21147
+ init_stdout_line_reader();
21082
21148
  _spawnClientDeps = {
21083
21149
  spawn: typedSpawn,
21084
21150
  streamDrainTimeoutMs: ACPX_STREAM_DRAIN_TIMEOUT_MS
@@ -21372,11 +21438,54 @@ var init_adapter_output = __esm(() => {
21372
21438
  CONTEXT_TOOL_CALL_PATTERN = /<nax_tool_call\s+name="([^"]+)">\s*([\s\S]*?)\s*<\/nax_tool_call>/i;
21373
21439
  });
21374
21440
 
21375
- // src/agents/acp/adapter.ts
21441
+ // src/agents/acp/agent-entries.ts
21376
21442
  function resolveRegistryEntry(agentName) {
21377
21443
  return AGENT_REGISTRY[agentName] ?? DEFAULT_ENTRY;
21378
21444
  }
21445
+ var AGENT_REGISTRY, DEFAULT_ENTRY, ACP_ADAPTER_NAMES;
21446
+ var init_agent_entries = __esm(() => {
21447
+ AGENT_REGISTRY = {
21448
+ claude: {
21449
+ binary: "claude",
21450
+ displayName: "Claude Code (ACP)",
21451
+ supportedTiers: ["fast", "balanced", "powerful"],
21452
+ maxContextTokens: 200000
21453
+ },
21454
+ codex: {
21455
+ binary: "codex",
21456
+ displayName: "OpenAI Codex (ACP)",
21457
+ supportedTiers: ["fast", "balanced"],
21458
+ maxContextTokens: 128000
21459
+ },
21460
+ gemini: {
21461
+ binary: "gemini",
21462
+ displayName: "Gemini CLI (ACP)",
21463
+ supportedTiers: ["fast", "balanced", "powerful"],
21464
+ maxContextTokens: 1e6
21465
+ },
21466
+ opencode: {
21467
+ binary: "opencode",
21468
+ displayName: "opencode (ACP)",
21469
+ supportedTiers: ["fast", "balanced", "powerful"],
21470
+ maxContextTokens: 128000
21471
+ },
21472
+ pi: {
21473
+ binary: "pi",
21474
+ displayName: "Pi Coding Agent (ACP)",
21475
+ supportedTiers: ["fast", "balanced", "powerful"],
21476
+ maxContextTokens: 128000
21477
+ }
21478
+ };
21479
+ DEFAULT_ENTRY = {
21480
+ binary: "claude",
21481
+ displayName: "ACP Agent",
21482
+ supportedTiers: ["balanced"],
21483
+ maxContextTokens: 128000
21484
+ };
21485
+ ACP_ADAPTER_NAMES = new Set(Object.keys(AGENT_REGISTRY));
21486
+ });
21379
21487
 
21488
+ // src/agents/acp/adapter.ts
21380
21489
  class AcpAgentAdapter {
21381
21490
  name;
21382
21491
  displayName;
@@ -21428,7 +21537,9 @@ class AcpAgentAdapter {
21428
21537
  timeoutPromise.catch(() => {});
21429
21538
  let response;
21430
21539
  try {
21431
- response = await Promise.race([session.prompt(prompt), timeoutPromise]);
21540
+ const promptPromise = session.prompt(prompt);
21541
+ promptPromise.catch(() => {});
21542
+ response = await Promise.race([promptPromise, timeoutPromise]);
21432
21543
  } finally {
21433
21544
  clearTimeout(timeoutId);
21434
21545
  }
@@ -21441,7 +21552,7 @@ class AcpAgentAdapter {
21441
21552
  cancelled: true
21442
21553
  };
21443
21554
  }
21444
- throw new CompleteError("complete() failed: stop reason is error");
21555
+ throw new CompleteError("complete() failed: stop reason is error", undefined, response.retryable);
21445
21556
  }
21446
21557
  const text = response.messages.filter((m) => m.role === "assistant").map((m) => m.content).join(`
21447
21558
  `).trim();
@@ -21456,7 +21567,7 @@ class AcpAgentAdapter {
21456
21567
  throw new CompleteError("complete() returned empty output");
21457
21568
  }
21458
21569
  const tokenUsage = response.cumulative_token_usage ? this._mapper.toInternal(response.cumulative_token_usage) : { inputTokens: 0, outputTokens: 0 };
21459
- const estimatedCostUsd = tokenUsage.inputTokens > 0 ? estimateCostFromTokenUsage(tokenUsage, _options.modelDef.model) : 0;
21570
+ const estimatedCostUsd = tokenUsage.inputTokens > 0 || tokenUsage.outputTokens > 0 ? estimateCostFromTokenUsage(tokenUsage, _options.modelDef.model) : 0;
21460
21571
  const exactCostUsd = response.exactCostUsd;
21461
21572
  if (exactCostUsd !== undefined) {
21462
21573
  getSafeLogger()?.info("acp-adapter", "complete() cost", {
@@ -21481,6 +21592,11 @@ class AcpAgentAdapter {
21481
21592
  return await tryOneAgent(this.name);
21482
21593
  } catch (err) {
21483
21594
  const error48 = err instanceof Error ? err : new Error(String(err));
21595
+ if (error48 instanceof CompleteError) {
21596
+ const classified = classifyCompleteError(error48);
21597
+ if (classified)
21598
+ return classified;
21599
+ }
21484
21600
  const parsed = _fallbackDeps.parseAgentError(error48.message);
21485
21601
  if (parsed.type === "auth") {
21486
21602
  return {
@@ -21747,7 +21863,7 @@ class AcpAgentAdapter {
21747
21863
  }
21748
21864
  }
21749
21865
  }
21750
- var INTERACTION_TIMEOUT_MS, AGENT_REGISTRY, DEFAULT_ENTRY, ACP_ADAPTER_NAMES;
21866
+ var INTERACTION_TIMEOUT_MS;
21751
21867
  var init_adapter = __esm(() => {
21752
21868
  init_errors();
21753
21869
  init_logger2();
@@ -21756,42 +21872,11 @@ var init_adapter = __esm(() => {
21756
21872
  init_token_mapper();
21757
21873
  init_adapter_lifecycle();
21758
21874
  init_adapter_output();
21875
+ init_agent_entries();
21759
21876
  init_adapter_lifecycle();
21760
21877
  init_adapter_output();
21878
+ init_agent_entries();
21761
21879
  INTERACTION_TIMEOUT_MS = 5 * 60 * 1000;
21762
- AGENT_REGISTRY = {
21763
- claude: {
21764
- binary: "claude",
21765
- displayName: "Claude Code (ACP)",
21766
- supportedTiers: ["fast", "balanced", "powerful"],
21767
- maxContextTokens: 200000
21768
- },
21769
- codex: {
21770
- binary: "codex",
21771
- displayName: "OpenAI Codex (ACP)",
21772
- supportedTiers: ["fast", "balanced"],
21773
- maxContextTokens: 128000
21774
- },
21775
- gemini: {
21776
- binary: "gemini",
21777
- displayName: "Gemini CLI (ACP)",
21778
- supportedTiers: ["fast", "balanced", "powerful"],
21779
- maxContextTokens: 1e6
21780
- },
21781
- opencode: {
21782
- binary: "opencode",
21783
- displayName: "opencode (ACP)",
21784
- supportedTiers: ["fast", "balanced", "powerful"],
21785
- maxContextTokens: 128000
21786
- }
21787
- };
21788
- DEFAULT_ENTRY = {
21789
- binary: "claude",
21790
- displayName: "ACP Agent",
21791
- supportedTiers: ["balanced"],
21792
- maxContextTokens: 128000
21793
- };
21794
- ACP_ADAPTER_NAMES = new Set(Object.keys(AGENT_REGISTRY));
21795
21880
  });
21796
21881
 
21797
21882
  // src/agents/acp/parser.ts
@@ -21802,12 +21887,17 @@ function createParseState() {
21802
21887
  exactCostUsd: undefined,
21803
21888
  stopReason: undefined,
21804
21889
  error: undefined,
21805
- retryable: false
21890
+ retryable: false,
21891
+ sawJsonLine: false
21806
21892
  };
21807
21893
  }
21808
21894
  function parseAcpxJsonLine(line, state) {
21809
21895
  try {
21810
21896
  const event = JSON.parse(line);
21897
+ if (!state.sawJsonLine) {
21898
+ state.text = "";
21899
+ state.sawJsonLine = true;
21900
+ }
21811
21901
  if (event.jsonrpc === "2.0") {
21812
21902
  if (event.method === "session/update" && event.params?.update) {
21813
21903
  const update = event.params.update;
@@ -21860,11 +21950,18 @@ function parseAcpxJsonLine(line, state) {
21860
21950
  state.stopReason = result.stop_reason;
21861
21951
  if (result.usage && typeof result.usage === "object") {
21862
21952
  const u = result.usage;
21953
+ const asNumber = (...values) => {
21954
+ for (const v of values) {
21955
+ if (typeof v === "number" && Number.isFinite(v))
21956
+ return v;
21957
+ }
21958
+ return 0;
21959
+ };
21863
21960
  state.tokenUsage = {
21864
- input_tokens: u.inputTokens ?? u.input_tokens ?? 0,
21865
- output_tokens: u.outputTokens ?? u.output_tokens ?? 0,
21866
- cache_read_input_tokens: u.cachedReadTokens ?? u.cache_read_input_tokens ?? 0,
21867
- cache_creation_input_tokens: u.cachedWriteTokens ?? u.cache_creation_input_tokens ?? 0
21961
+ input_tokens: asNumber(u.inputTokens, u.input_tokens),
21962
+ output_tokens: asNumber(u.outputTokens, u.output_tokens),
21963
+ cache_read_input_tokens: asNumber(u.cachedReadTokens, u.cache_read_input_tokens),
21964
+ cache_creation_input_tokens: asNumber(u.cachedWriteTokens, u.cache_creation_input_tokens)
21868
21965
  };
21869
21966
  }
21870
21967
  }
@@ -21907,7 +22004,7 @@ function parseAcpxJsonLine(line, state) {
21907
22004
  state.error = typeof event.error === "string" ? event.error : event.error.message ?? JSON.stringify(event.error);
21908
22005
  }
21909
22006
  } catch {
21910
- if (!state.text)
22007
+ if (!state.text && !state.sawJsonLine)
21911
22008
  state.text = line;
21912
22009
  }
21913
22010
  return;
@@ -21996,7 +22093,7 @@ var KNOWN_AGENT_NAMES, _registryTestAdapters;
21996
22093
  var init_registry = __esm(() => {
21997
22094
  init_logger2();
21998
22095
  init_adapter();
21999
- KNOWN_AGENT_NAMES = ["claude", "codex", "opencode", "gemini", "aider"];
22096
+ KNOWN_AGENT_NAMES = ["claude", "codex", "opencode", "gemini", "aider", "pi"];
22000
22097
  _registryTestAdapters = new Map;
22001
22098
  });
22002
22099
 
@@ -22336,7 +22433,7 @@ function trySameAgentRetry(result, state, deps) {
22336
22433
  outcome: "timeout-retry",
22337
22434
  timeoutRetryAttempts: newAttempts,
22338
22435
  kind: { kind: "timeout-retry", attempt: newAttempts },
22339
- currentRunOptions: resolveTimeoutRetryOptions(currentRunOptions, timeoutConfig, config2.execution),
22436
+ currentRunOptions: resolveTimeoutRetryOptions(currentRunOptions, timeoutConfig, config2.execution, requestRunOptions),
22340
22437
  fallbackRecord: {
22341
22438
  outcome: result.adapterFailure?.outcome ?? "fail-timeout",
22342
22439
  category: result.adapterFailure?.category ?? "quality",
@@ -22375,8 +22472,9 @@ function extractTimeoutRetryConfig(config2) {
22375
22472
  budgetMultiplier: fromConfig?.budgetMultiplier ?? DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG.budgetMultiplier
22376
22473
  };
22377
22474
  }
22378
- function resolveTimeoutRetryOptions(prev, timeoutConfig, executionConfig) {
22379
- const budget = prev.timeoutSeconds ?? executionConfig?.sessionTimeoutSeconds ?? DEFAULT_CONFIG.execution.sessionTimeoutSeconds;
22475
+ function resolveTimeoutRetryOptions(prev, timeoutConfig, executionConfig, baseRunOptions) {
22476
+ const baseBudget = baseRunOptions?.timeoutSeconds ?? prev.timeoutSeconds;
22477
+ const budget = baseBudget ?? executionConfig?.sessionTimeoutSeconds ?? DEFAULT_CONFIG.execution.sessionTimeoutSeconds;
22380
22478
  return { ...prev, timeoutSeconds: budget * timeoutConfig.budgetMultiplier };
22381
22479
  }
22382
22480
  function timeoutRetryShouldRetry(attempts, config2) {
@@ -30156,7 +30254,7 @@ function validateStory(raw, index, allIds) {
30156
30254
  const rawDeps = s.dependencies;
30157
30255
  const dependencies = Array.isArray(rawDeps) ? rawDeps : [];
30158
30256
  for (const dep of dependencies) {
30159
- if (!allIds.has(dep)) {
30257
+ if (!allIds.has(normalizeStoryId(dep))) {
30160
30258
  throw new NaxError(`[schema] story[${index}].dependencies references unknown story ID "${dep}"`, "SCHEMA_VALIDATION_FAILED", { stage: "schema", index, dep });
30161
30259
  }
30162
30260
  }
@@ -30418,6 +30516,12 @@ async function loadPRD(path) {
30418
30516
  throw new Error(`PRD file is too large (${sizeMB} MB exceeds ${limitMB} MB limit). Split this feature into smaller features or reduce story count.`);
30419
30517
  }
30420
30518
  const prd = await Bun.file(path).json();
30519
+ if (!Array.isArray(prd.userStories)) {
30520
+ throw new NaxError(`PRD file is missing or has a corrupt "userStories" array: ${path}`, "PRD_INVALID", {
30521
+ stage: "prd",
30522
+ path
30523
+ });
30524
+ }
30421
30525
  for (const story of prd.userStories) {
30422
30526
  story.attempts = story.attempts ?? 0;
30423
30527
  story.priorErrors = story.priorErrors ?? [];
@@ -30464,7 +30568,7 @@ function getNextStory(prd, currentStoryId, maxRetries) {
30464
30568
  return currentStory;
30465
30569
  }
30466
30570
  }
30467
- const eligible = prd.userStories.filter((s) => !s.passes && s.status !== "passed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "failed" && s.status !== "paused" && s.status !== "decomposed" && hasSatisfiedDependencies(s, storyIds, completedIds));
30571
+ const eligible = prd.userStories.filter((s) => !s.passes && s.status !== "passed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "failed" && s.status !== "paused" && s.status !== "decomposed" && s.status !== "regression-failed" && hasSatisfiedDependencies(s, storyIds, completedIds));
30468
30572
  if (eligible.length === 0)
30469
30573
  return null;
30470
30574
  return eligible.reduce((best, s) => (s.priority ?? 0) > (best.priority ?? 0) ? s : best);
@@ -30495,7 +30599,7 @@ function markStoryPassed(prd, storyId, _statusWriter) {
30495
30599
  const parent = prd.userStories.find((s) => s.id === parentId);
30496
30600
  if (parent && parent.status === "decomposed") {
30497
30601
  const siblings = prd.userStories.filter((s) => s.parentStoryId === parentId);
30498
- const allSiblingsPassed = siblings.length > 0 && siblings.every((s) => s.passes || s.status === "passed");
30602
+ const allSiblingsPassed = siblings.length > 0 && siblings.every((s) => s.passes || s.status === "passed" || s.status === "skipped");
30499
30603
  if (allSiblingsPassed) {
30500
30604
  parent.passes = true;
30501
30605
  parent.status = "passed";
@@ -31572,19 +31676,17 @@ ${newContextSection}`);
31572
31676
  result += sections.acceptanceCriteria;
31573
31677
  remainingChars -= sections.acceptanceCriteria.length;
31574
31678
  }
31575
- if (sections.context && remainingChars > 0) {
31576
- const reserveForMessage = sections.context.length > remainingChars ? trimmedMessage.length : 0;
31679
+ const trimmable = sections.context ?? sections.other;
31680
+ if (trimmable && remainingChars > 0) {
31681
+ const reserveForMessage = trimmable.length > remainingChars ? trimmedMessage.length : 0;
31577
31682
  const maxContextChars = Math.max(0, remainingChars - reserveForMessage);
31578
- const trimmedContext = sections.context.substring(0, maxContextChars);
31683
+ const trimmedContext = trimmable.substring(0, maxContextChars);
31579
31684
  result += trimmedContext;
31580
- if (trimmedContext.length < sections.context.length) {
31685
+ if (trimmedContext.length < trimmable.length) {
31581
31686
  result += trimmedMessage;
31582
31687
  }
31583
31688
  }
31584
- if (sections.other && remainingChars > sections.other.length) {
31585
- result += sections.other;
31586
- }
31587
- return result;
31689
+ return result.trim() ? result : prompt;
31588
31690
  }
31589
31691
  extractSections(prompt) {
31590
31692
  const sections = {};
@@ -32911,6 +33013,80 @@ var init_tool_runtime = __esm(() => {
32911
33013
  init_pull_tools();
32912
33014
  });
32913
33015
 
33016
+ // src/context/engine/manifest-purge.ts
33017
+ import { dirname as dirname7, resolve as resolve11 } from "path";
33018
+ async function purgeStaleManifests(projectDir, retentionDays) {
33019
+ const allEntries = await _manifestPurgeDeps.scan(MANIFEST_PATTERN, projectDir, MAX_MANIFEST_SCAN);
33020
+ if (allEntries.length >= MAX_MANIFEST_SCAN) {
33021
+ _manifestPurgeDeps.debugLog("manifest-purge", `Manifest scan reached MAX_MANIFEST_SCAN=${MAX_MANIFEST_SCAN}; stopping further examination`, { cap: MAX_MANIFEST_SCAN, projectDir });
33022
+ }
33023
+ const entries = allEntries.slice(0, MAX_MANIFEST_SCAN);
33024
+ const cutoffMs = _manifestPurgeDeps.now() - retentionDays * DAY_MS;
33025
+ let deleted = 0;
33026
+ const touchedDirs = new Set;
33027
+ for (const relPath of entries) {
33028
+ const absPath = resolve11(projectDir, relPath);
33029
+ let mtimeMs;
33030
+ try {
33031
+ mtimeMs = await _manifestPurgeDeps.statMtime(absPath);
33032
+ } catch {
33033
+ continue;
33034
+ }
33035
+ if (mtimeMs >= cutoffMs)
33036
+ continue;
33037
+ try {
33038
+ await _manifestPurgeDeps.unlink(absPath);
33039
+ deleted++;
33040
+ touchedDirs.add(dirname7(absPath));
33041
+ } catch {}
33042
+ }
33043
+ for (const storyDir of touchedDirs) {
33044
+ await _manifestPurgeDeps.rmdirIfEmpty(storyDir);
33045
+ }
33046
+ return deleted;
33047
+ }
33048
+ var MAX_MANIFEST_SCAN = 5000, DAY_MS = 86400000, MANIFEST_PATTERN = ".nax/features/*/stories/*/{context-manifest-*,rebuild-manifest}.json", _manifestPurgeDeps;
33049
+ var init_manifest_purge = __esm(() => {
33050
+ init_logger2();
33051
+ _manifestPurgeDeps = {
33052
+ now: () => Date.now(),
33053
+ scan: async (pattern, cwd, cap) => {
33054
+ const results = [];
33055
+ const g = new Bun.Glob(pattern);
33056
+ for (const entry of g.scanSync({ cwd, absolute: false, dot: true })) {
33057
+ if (results.length >= cap)
33058
+ break;
33059
+ results.push(entry);
33060
+ }
33061
+ return results;
33062
+ },
33063
+ statMtime: async (path3) => {
33064
+ const file3 = Bun.file(path3);
33065
+ if (!await file3.exists())
33066
+ throw new Error(`stat: file not found: ${path3}`);
33067
+ const stat = await file3.stat();
33068
+ return stat.mtimeMs;
33069
+ },
33070
+ unlink: async (path3) => {
33071
+ const { unlink: nodeUnlink } = await import("fs/promises");
33072
+ await nodeUnlink(path3);
33073
+ },
33074
+ rmdirIfEmpty: async (path3) => {
33075
+ const { rmdir } = await import("fs/promises");
33076
+ try {
33077
+ await rmdir(path3);
33078
+ return true;
33079
+ } catch {
33080
+ return false;
33081
+ }
33082
+ },
33083
+ debugLog: (stage, message, data) => {
33084
+ const logger = getLogger();
33085
+ logger?.debug(stage, message, data);
33086
+ }
33087
+ };
33088
+ });
33089
+
32914
33090
  // src/context/engine/index.ts
32915
33091
  var init_engine = __esm(() => {
32916
33092
  init_orchestrator();
@@ -32936,6 +33112,7 @@ var init_engine = __esm(() => {
32936
33112
  init_stage_assembler();
32937
33113
  init_tool_runtime();
32938
33114
  init_manifest_store();
33115
+ init_manifest_purge();
32939
33116
  });
32940
33117
 
32941
33118
  // src/prompts/core/section-accumulator.ts
@@ -35606,12 +35783,12 @@ function acceptanceDiagnoseRawArrayToFindings(raw) {
35606
35783
  }
35607
35784
 
35608
35785
  // src/findings/path-utils.ts
35609
- import { relative as relative8, resolve as resolve11 } from "path";
35786
+ import { relative as relative8, resolve as resolve12 } from "path";
35610
35787
  function rebaseToWorkdir(rawPath, cwd, workdir) {
35611
35788
  if (rawPath.startsWith("/")) {
35612
35789
  return relative8(workdir, rawPath);
35613
35790
  }
35614
- return relative8(workdir, resolve11(cwd, rawPath));
35791
+ return relative8(workdir, resolve12(cwd, rawPath));
35615
35792
  }
35616
35793
  var init_path_utils = () => {};
35617
35794
 
@@ -36549,7 +36726,7 @@ function validateRoutingDecision(parsed, config2, story) {
36549
36726
  throw new Error(`Invalid complexity: ${parsed.complexity}`);
36550
36727
  }
36551
36728
  const modelTier = parsed.modelTier;
36552
- const tierExistsInAnyAgent = Object.values(config2.models).some((agentTiers) => (modelTier in agentTiers));
36729
+ const tierExistsInAnyAgent = Object.values(config2.models).some((agentTiers) => typeof modelTier === "string" && Object.hasOwn(agentTiers, modelTier));
36553
36730
  if (!tierExistsInAnyAgent) {
36554
36731
  throw new Error(`Invalid modelTier: ${modelTier} (not found in any agent's tier map)`);
36555
36732
  }
@@ -37806,10 +37983,13 @@ function validateAdversarialShape(parsed) {
37806
37983
  const acks = extractAcks(obj.acks);
37807
37984
  return {
37808
37985
  passed: obj.passed,
37809
- findings: obj.findings,
37986
+ findings: obj.findings.filter(isAdversarialFindingShaped),
37810
37987
  ...acks.length > 0 && { acks }
37811
37988
  };
37812
37989
  }
37990
+ function isAdversarialFindingShaped(f) {
37991
+ return typeof f === "object" && f !== null && !Array.isArray(f);
37992
+ }
37813
37993
  function formatFindings(findings) {
37814
37994
  return findings.map((f) => `[${f.severity}][${f.category}] ${f.file}:${f.line} \u2014 ${f.issue}
37815
37995
  Suggestion: ${f.suggestion}`).join(`
@@ -39653,9 +39833,10 @@ var init_debate_hybrid = __esm(() => {
39653
39833
  return { ...proposal, output: `Agent "failed" during rebuttal`, estimatedCostUsd: totalCostUsd };
39654
39834
  }
39655
39835
  if (round < ctx.input.rounds) {
39656
- const settledRound = await raceAgainstAbort(Promise.all(ctx.input.rebutBarriers[round - 1].map((b) => b.promise)), ctx.input.signal, ctx.input.storyId);
39657
- priorRoundOutputs.push(settledRound);
39658
- roundInputs = settledRound;
39836
+ const settledRound = await raceAgainstAbort(Promise.allSettled(ctx.input.rebutBarriers[round - 1].map((b) => b.promise)), ctx.input.signal, ctx.input.storyId);
39837
+ const roundOutputs = settledRound.map((r) => r.status === "fulfilled" ? r.value : "");
39838
+ priorRoundOutputs.push(roundOutputs);
39839
+ roundInputs = roundOutputs;
39659
39840
  }
39660
39841
  }
39661
39842
  return { ...lastTurn, estimatedCostUsd: totalCostUsd };
@@ -39701,7 +39882,8 @@ var init_debate_plan = __esm(() => {
39701
39882
  return proposal;
39702
39883
  }
39703
39884
  }
39704
- const peerProposals = await raceAgainstAbort(Promise.all(ctx.input.proposalBarriers.map((barrier) => barrier.promise)), ctx.input.signal, ctx.input.storyId);
39885
+ const peerProposalsSettled = await raceAgainstAbort(Promise.allSettled(ctx.input.proposalBarriers.map((barrier) => barrier.promise)), ctx.input.signal, ctx.input.storyId);
39886
+ const peerProposals = peerProposalsSettled.map((r) => r.status === "fulfilled" ? r.value : "");
39705
39887
  const rebutResult = ctx.input.turnSemaphore ? await ctx.input.turnSemaphore.run(() => ctx.send(ctx.input.buildRebutPrompt(peerProposals))) : await ctx.send(ctx.input.buildRebutPrompt(peerProposals));
39706
39888
  ctx.input.rebuttalBarrier.resolve(rebutResult.output);
39707
39889
  const decision = await raceAgainstAbort(ctx.input.selectionSignal, ctx.input.signal, ctx.input.storyId);
@@ -39740,8 +39922,18 @@ async function getChangedFiles(workdir, fromRef = "HEAD") {
39740
39922
  stdout: "pipe",
39741
39923
  stderr: "pipe"
39742
39924
  });
39743
- const output = await Bun.readableStreamToText(proc.stdout);
39744
- await proc.exited;
39925
+ const [output, stderr, exitCode] = await Promise.all([
39926
+ Bun.readableStreamToText(proc.stdout),
39927
+ Bun.readableStreamToText(proc.stderr),
39928
+ proc.exited
39929
+ ]);
39930
+ if (exitCode !== 0) {
39931
+ throw new NaxError(`git diff --name-only ${fromRef} failed (exit ${exitCode}): ${stderr.trim()}`, "GIT_DIFF_FAILED", {
39932
+ stage: "tdd-isolation",
39933
+ fromRef,
39934
+ exitCode
39935
+ });
39936
+ }
39745
39937
  return output.trim().split(`
39746
39938
  `).filter(Boolean);
39747
39939
  }
@@ -39816,6 +40008,7 @@ async function verifyImplementerIsolation(workdir, beforeRef, testFilePatterns =
39816
40008
  }
39817
40009
  var _isolationDeps, SRC_PATTERNS, LITE_STUB_ADDED_LINES_CEILING = 20;
39818
40010
  var init_isolation = __esm(() => {
40011
+ init_errors();
39819
40012
  init_test_runners();
39820
40013
  init_bun_deps();
39821
40014
  _isolationDeps = { spawn };
@@ -41323,17 +41516,18 @@ async function executeWithTimeout(command, timeoutSeconds, env2, options) {
41323
41516
  stdout: "pipe",
41324
41517
  stderr: "pipe",
41325
41518
  env: env2 || normalizeEnvironment(process.env),
41326
- cwd: options?.cwd
41519
+ cwd: options?.cwd,
41520
+ detached: true
41327
41521
  });
41328
41522
  const stdoutPromise = new Response(proc.stdout).text().catch(() => "");
41329
41523
  const stderrPromise = new Response(proc.stderr).text().catch(() => "");
41330
41524
  const timeoutMs = timeoutSeconds * 1000;
41331
41525
  let timedOut = false;
41332
41526
  const timer = { id: undefined };
41333
- const timeoutPromise = new Promise((resolve12) => {
41527
+ const timeoutPromise = new Promise((resolve13) => {
41334
41528
  timer.id = setTimeout(() => {
41335
41529
  timedOut = true;
41336
- resolve12();
41530
+ resolve13();
41337
41531
  }, timeoutMs);
41338
41532
  });
41339
41533
  const processPromise = proc.exited;
@@ -42197,8 +42391,8 @@ function makeDeclarationSink() {
42197
42391
  var {spawn: spawn2 } = globalThis.Bun;
42198
42392
  function createDrainDeadline(deadlineMs) {
42199
42393
  let timeoutId;
42200
- const promise2 = new Promise((resolve12) => {
42201
- timeoutId = setTimeout(() => resolve12(""), deadlineMs);
42394
+ const promise2 = new Promise((resolve13) => {
42395
+ timeoutId = setTimeout(() => resolve13(""), deadlineMs);
42202
42396
  });
42203
42397
  return {
42204
42398
  promise: promise2,
@@ -45080,14 +45274,14 @@ var init_prepare_inputs = __esm(() => {
45080
45274
  });
45081
45275
 
45082
45276
  // src/utils/nax-project-root.ts
45083
- import { dirname as dirname7, join as join27, resolve as resolve12 } from "path";
45277
+ import { dirname as dirname8, join as join27, resolve as resolve13 } from "path";
45084
45278
  async function findNaxProjectRoot(startDir) {
45085
- let dir = resolve12(startDir);
45279
+ let dir = resolve13(startDir);
45086
45280
  for (let depth = 0;depth < MAX_NAX_WALK_DEPTH; depth++) {
45087
45281
  if (await _naxProjectRootDeps.exists(join27(dir, ".nax", "config.json"))) {
45088
45282
  return dir;
45089
45283
  }
45090
- const parent = dirname7(dir);
45284
+ const parent = dirname8(dir);
45091
45285
  if (parent === dir)
45092
45286
  break;
45093
45287
  dir = parent;
@@ -45108,7 +45302,7 @@ var package_default;
45108
45302
  var init_package = __esm(() => {
45109
45303
  package_default = {
45110
45304
  name: "@nathapp/nax",
45111
- version: "0.78.0",
45305
+ version: "0.79.0",
45112
45306
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
45113
45307
  type: "module",
45114
45308
  bin: {
@@ -45155,9 +45349,14 @@ var init_package = __esm(() => {
45155
45349
  "check:dispatch-context": "bash scripts/check-dispatch-context.sh",
45156
45350
  "check:naxconfig-cast": "bash scripts/check-no-silent-naxconfig-cast.sh",
45157
45351
  "check:runtime-cleanup": "bash scripts/check-runtime-cleanup.sh",
45352
+ "check:scripts": "bash scripts/check-scripts.sh",
45158
45353
  "check:adapter-no-config-import": "bash scripts/check-adapter-no-config-import.sh",
45354
+ "check:test-typecheck": "bun run scripts/check-test-typecheck.ts",
45355
+ "check:test-typecheck:update": "bun run scripts/check-test-typecheck.ts --update-baseline",
45356
+ "check:test-as-unknown-as": "bun run scripts/check-test-as-unknown-as.ts",
45357
+ "check:test-as-unknown-as:update": "bun run scripts/check-test-as-unknown-as.ts --update-baseline",
45159
45358
  "check:gate-reachability": "bun run scripts/check-gate-reachability.ts",
45160
- "check:all": "bun run lint && bun run check:test-mocks && bun run check:process-cwd && bun run check:no-adapter-wrap && bun run check:dispatch-context && bun run check:naxconfig-cast && bun run check:runtime-cleanup && bun run check:adapter-no-config-import && bun run check:gate-reachability",
45359
+ "check:all": "bun run lint && bun run check:test-mocks && bun run check:process-cwd && bun run check:no-adapter-wrap && bun run check:scripts && bun run check:dispatch-context && bun run check:naxconfig-cast && bun run check:runtime-cleanup && bun run check:adapter-no-config-import && bun run check:test-typecheck && bun run check:test-as-unknown-as && bun run check:gate-reachability",
45161
45360
  prepublishOnly: "bun run build",
45162
45361
  "test:full": "FULL=1 NAX_PRECHECK=1 bun test test/ --timeout=60000"
45163
45362
  },
@@ -45216,8 +45415,8 @@ var init_version = __esm(() => {
45216
45415
  NAX_VERSION = package_default.version;
45217
45416
  NAX_COMMIT = (() => {
45218
45417
  try {
45219
- if (/^[0-9a-f]{6,10}$/.test("af86acb4"))
45220
- return "af86acb4";
45418
+ if (/^[0-9a-f]{6,10}$/.test("fdfb7e0e"))
45419
+ return "fdfb7e0e";
45221
45420
  } catch {}
45222
45421
  try {
45223
45422
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -46054,7 +46253,7 @@ function appendFilesToCommand(command, files) {
46054
46253
  }
46055
46254
  async function listChangedFiles(workdir, baseRef) {
46056
46255
  const proc = Bun.spawn({
46057
- cmd: ["git", "diff", "--name-only", `${baseRef}..HEAD`],
46256
+ cmd: ["git", "diff", "--relative", "--name-only", `${baseRef}..HEAD`],
46058
46257
  cwd: workdir,
46059
46258
  stdout: "pipe",
46060
46259
  stderr: "pipe"
@@ -50239,7 +50438,7 @@ function createPackageRegistry(loader, repoRoot) {
50239
50438
  return "";
50240
50439
  return packageDir;
50241
50440
  }
50242
- function resolve13(packageDir) {
50441
+ function resolve14(packageDir) {
50243
50442
  const key = toRelativeKey(packageDir);
50244
50443
  const cached2 = cache.get(key);
50245
50444
  if (cached2 !== undefined) {
@@ -50276,9 +50475,9 @@ function createPackageRegistry(loader, repoRoot) {
50276
50475
  all() {
50277
50476
  return [...cache.values()];
50278
50477
  },
50279
- resolve: resolve13,
50478
+ resolve: resolve14,
50280
50479
  repo() {
50281
- return resolve13(undefined);
50480
+ return resolve14(undefined);
50282
50481
  },
50283
50482
  hydrate
50284
50483
  };
@@ -51956,6 +52155,11 @@ var init_session = __esm(() => {
51956
52155
  });
51957
52156
 
51958
52157
  // src/verification/flake-probe.ts
52158
+ function probeRanNoTests(output) {
52159
+ if (!output)
52160
+ return false;
52161
+ return NO_TESTS_EXECUTED_MARKERS.some((re) => re.test(output));
52162
+ }
51959
52163
  function escapeRegex2(input) {
51960
52164
  return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
51961
52165
  }
@@ -51998,7 +52202,7 @@ async function runFlakeProbe(input) {
51998
52202
  } catch {
51999
52203
  continue;
52000
52204
  }
52001
- if (result.success && result.countsTowardEscalation) {
52205
+ if (result.success && result.countsTowardEscalation && !probeRanNoTests(result.output)) {
52002
52206
  probePasses += 1;
52003
52207
  }
52004
52208
  }
@@ -52007,10 +52211,11 @@ async function runFlakeProbe(input) {
52007
52211
  }
52008
52212
  return { verdict: "consistent-failure", probeRuns };
52009
52213
  }
52010
- var _flakeProbeDeps;
52214
+ var NO_TESTS_EXECUTED_MARKERS, _flakeProbeDeps;
52011
52215
  var init_flake_probe = __esm(() => {
52012
52216
  init_errors();
52013
52217
  init_executor();
52218
+ NO_TESTS_EXECUTED_MARKERS = [/no tests? to run/i, /^ran 0 tests?/im];
52014
52219
  _flakeProbeDeps = {
52015
52220
  execute: executeWithTimeout
52016
52221
  };
@@ -52586,9 +52791,9 @@ async function buildProjectMetadata(workdir, config2) {
52586
52791
  name: detected?.name,
52587
52792
  language: detected?.lang,
52588
52793
  dependencies: detected?.dependencies ?? [],
52589
- testCommand: config2.execution?.testCommand ?? undefined,
52590
- lintCommand: config2.execution?.lintCommand ?? undefined,
52591
- typecheckCommand: config2.execution?.typecheckCommand ?? undefined
52794
+ testCommand: config2.quality?.commands?.test ?? undefined,
52795
+ lintCommand: config2.quality?.commands?.lint ?? undefined,
52796
+ typecheckCommand: config2.quality?.commands?.typecheck ?? undefined
52592
52797
  };
52593
52798
  }
52594
52799
  function formatMetadataSection(metadata) {
@@ -53892,7 +54097,9 @@ async function finalizePlanRun(ctx, opts, successful, rebuttalList, outputPaths,
53892
54097
  } : await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.callContext, ctx.storyId, resolverTimeoutMs, opts.workdir, opts.feature, buildPlanSynthesisSuffix(opts.specContent), finalizedProposals.map((p) => p.debater), agentManager);
53893
54098
  let finalOutcome = outcome.outcome;
53894
54099
  let winningOutput = outcome.output ?? finalizedProposals[0]?.output;
53895
- winningOutput = await readWinnerOutput(selectionSummary.winnerOutputPath ?? outputPaths[0], winningOutput);
54100
+ if (selectionSummary.winnerOutputPath) {
54101
+ winningOutput = await readWinnerOutput(selectionSummary.winnerOutputPath, winningOutput);
54102
+ }
53896
54103
  let runCostUsd = totalCostUsd;
53897
54104
  if (config2.postDebateVerifier && winningOutput) {
53898
54105
  const verifierCtx = {
@@ -54039,7 +54246,8 @@ async function runPlan(ctx, taskContext, outputFormat, opts) {
54039
54246
  const settled = await Promise.allSettled(callOpPromises);
54040
54247
  for (let i = 0;i < settled.length; i++) {
54041
54248
  const res = settled[i];
54042
- if (res.status === "fulfilled") {
54249
+ const succeeded = res.status === "fulfilled" && res.value.success;
54250
+ if (succeeded && res.status === "fulfilled") {
54043
54251
  successful.push({
54044
54252
  debater: resolved[i].debater,
54045
54253
  agentName: resolved[i].agentName,
@@ -54064,7 +54272,7 @@ async function runPlan(ctx, taskContext, outputFormat, opts) {
54064
54272
  stage: ctx.stage,
54065
54273
  debaterIndex: i,
54066
54274
  agent: resolved[i].debater.agent,
54067
- error: res.reason instanceof Error ? res.reason.message : String(res.reason)
54275
+ error: res.status === "rejected" ? res.reason instanceof Error ? res.reason.message : String(res.reason) : `debate op returned success:false \u2014 ${res.value.rebut}`
54068
54276
  });
54069
54277
  }
54070
54278
  }
@@ -54075,6 +54283,8 @@ async function runPlan(ctx, taskContext, outputFormat, opts) {
54075
54283
  resolver.resolve({});
54076
54284
  const proposalBarriers = resolved.map(() => Promise.withResolvers());
54077
54285
  const rebuttalBarriers = resolved.map(() => Promise.withResolvers());
54286
+ for (const barrier of proposalBarriers)
54287
+ barrier.promise.catch(() => {});
54078
54288
  const rebutBuilder = new DebatePromptBuilder({ taskContext, outputFormat: "", stage: "plan" }, { debaters: resolved.map((e) => e.debater), sessionMode: "stateful" });
54079
54289
  const callOpPromisesB = resolved.map(({ debater, agentName }, index) => {
54080
54290
  const debaterCtx = {
@@ -54100,6 +54310,7 @@ async function runPlan(ctx, taskContext, outputFormat, opts) {
54100
54310
  callOpPromisesB[i].then((result) => {
54101
54311
  rebuttalBarriers[i].resolve(result.rebut ?? "");
54102
54312
  }, (err) => {
54313
+ proposalBarriers[i].reject(err);
54103
54314
  rebuttalBarriers[i].reject(err);
54104
54315
  });
54105
54316
  }
@@ -54221,7 +54432,6 @@ async function runStateful(ctx, prompt) {
54221
54432
  const rebuttalBuilder = buildRebuttalPromptBuilder(ctx.stage, prompt, resolved.map((entry) => entry.debater));
54222
54433
  const signal = resolveStatefulSignal(ctx);
54223
54434
  const noopBuildRebutPrompt = () => "";
54224
- const localProposalBarrier = () => [Promise.withResolvers()];
54225
54435
  const debaterRole = (index) => `debate-${ctx.stage}-${index}`;
54226
54436
  const debaterCallContext = (agentName, index) => ({
54227
54437
  ...createDebaterCallContext(ctx, agentName),
@@ -54233,12 +54443,13 @@ async function runStateful(ctx, prompt) {
54233
54443
  throw new NaxError("[debate] Stateful debate aborted", "CALL_OP_ABORTED", { storyId: ctx.storyId });
54234
54444
  }
54235
54445
  };
54446
+ const proposalBarriers = resolved.map(() => Promise.withResolvers());
54236
54447
  const proposalSettled = await allSettledBounded(resolved.map(({ debater, agentName }, index) => () => callOp(debaterCallContext(agentName, index), statefulDebaterOp, {
54237
54448
  debater,
54238
54449
  index,
54239
54450
  proposePrompt: proposalBuilder.buildProposalPrompt(index),
54240
54451
  buildRebutPrompt: noopBuildRebutPrompt,
54241
- proposalBarriers: localProposalBarrier(),
54452
+ proposalBarriers,
54242
54453
  signal,
54243
54454
  storyId: ctx.storyId,
54244
54455
  skipRebuttal: true
@@ -54304,6 +54515,7 @@ async function runStateful(ctx, prompt) {
54304
54515
  });
54305
54516
  return buildFailedResult(ctx.storyId, ctx.stage, ctx.stageConfig, 0);
54306
54517
  }
54518
+ const rebuttalRoundBarriers = successfulProposals.map(() => Promise.withResolvers());
54307
54519
  const rebuttals = shouldRunRebuttal ? (await allSettledBounded(successfulProposals.map((proposal, index) => () => callOp(debaterCallContext(proposal.agentName, proposal.resolvedIndex), statefulDebaterOp, {
54308
54520
  debater: proposal.debater,
54309
54521
  index,
@@ -54312,7 +54524,7 @@ async function runStateful(ctx, prompt) {
54312
54524
  output: entry.output
54313
54525
  }))),
54314
54526
  buildRebutPrompt: noopBuildRebutPrompt,
54315
- proposalBarriers: localProposalBarrier(),
54527
+ proposalBarriers: rebuttalRoundBarriers,
54316
54528
  signal,
54317
54529
  storyId: ctx.storyId,
54318
54530
  skipRebuttal: true
@@ -54962,9 +55174,9 @@ ${request.summary}
54962
55174
  throw new Error("CLI plugin not initialized");
54963
55175
  }
54964
55176
  let timeoutId;
54965
- const timeoutPromise = new Promise((resolve14) => {
55177
+ const timeoutPromise = new Promise((resolve15) => {
54966
55178
  timeoutId = setTimeout(() => {
54967
- resolve14({
55179
+ resolve15({
54968
55180
  requestId: request.id,
54969
55181
  action: "skip",
54970
55182
  respondedBy: "timeout",
@@ -55120,9 +55332,9 @@ ${request.summary}
55120
55332
  if (!this.rl) {
55121
55333
  throw new Error("CLI plugin not initialized");
55122
55334
  }
55123
- return new Promise((resolve14) => {
55335
+ return new Promise((resolve15) => {
55124
55336
  this.rl?.question(prompt, (answer) => {
55125
- resolve14(answer);
55337
+ resolve15(answer);
55126
55338
  });
55127
55339
  });
55128
55340
  }
@@ -55480,9 +55692,9 @@ ${partLabel}${chunks[i]}`;
55480
55692
  parseUpdate(requestId, update) {
55481
55693
  if (update.callback_query) {
55482
55694
  const data = update.callback_query.data;
55483
- if (!data.startsWith(requestId))
55484
- return null;
55485
55695
  const parts = data.split(":");
55696
+ if (parts[0] !== requestId)
55697
+ return null;
55486
55698
  if (parts.length < 2)
55487
55699
  return null;
55488
55700
  const action = parts[1];
@@ -55609,11 +55821,11 @@ function installServePortZeroCompat() {
55609
55821
  const originalFetch = globalThis.fetch.bind(globalThis);
55610
55822
  const patchedServe = (options) => {
55611
55823
  const requestedPort = typeof options.port === "number" ? options.port : 0;
55612
- if (requestedPort !== 0 && !inMemoryServers.has(requestedPort)) {
55824
+ if (!inMemoryServers.has(requestedPort)) {
55613
55825
  try {
55614
55826
  return originalServe(options);
55615
55827
  } catch {
55616
- return createInMemoryServer(options, requestedPort);
55828
+ return createInMemoryServer(options, requestedPort === 0 ? nextCompatPort() : requestedPort);
55617
55829
  }
55618
55830
  }
55619
55831
  return createInMemoryServer(options, nextCompatPort());
@@ -55736,7 +55948,7 @@ class WebhookInteractionPlugin {
55736
55948
  this.registeredRequestIds.delete(requestId);
55737
55949
  return early;
55738
55950
  }
55739
- return new Promise((resolve14) => {
55951
+ return new Promise((resolve15) => {
55740
55952
  const existingCallback = this.receiveCallbacks.get(requestId);
55741
55953
  if (existingCallback) {
55742
55954
  this.clearReceiveTimer(requestId);
@@ -55751,7 +55963,7 @@ class WebhookInteractionPlugin {
55751
55963
  this.clearReceiveTimer(requestId);
55752
55964
  this.receiveCallbacks.delete(requestId);
55753
55965
  this.registeredRequestIds.delete(requestId);
55754
- resolve14({
55966
+ resolve15({
55755
55967
  requestId,
55756
55968
  action: "skip",
55757
55969
  respondedBy: "timeout",
@@ -55763,7 +55975,7 @@ class WebhookInteractionPlugin {
55763
55975
  this.clearReceiveTimer(requestId);
55764
55976
  this.receiveCallbacks.delete(requestId);
55765
55977
  this.registeredRequestIds.delete(requestId);
55766
- resolve14(response);
55978
+ resolve15(response);
55767
55979
  });
55768
55980
  });
55769
55981
  }
@@ -56039,8 +56251,8 @@ Options:`);
56039
56251
  stage: "run"
56040
56252
  });
56041
56253
  }
56042
- if (parsed.confidence < 0 || parsed.confidence > 1) {
56043
- throw new NaxError(`Invalid confidence: ${parsed.confidence} (must be 0-1)`, "AUTO_APPROVE_PARSE_FAILED", {
56254
+ if (typeof parsed.confidence !== "number" || !Number.isFinite(parsed.confidence) || parsed.confidence < 0 || parsed.confidence > 1) {
56255
+ throw new NaxError(`Invalid confidence: ${parsed.confidence} (must be a number 0-1)`, "AUTO_APPROVE_PARSE_FAILED", {
56044
56256
  stage: "run"
56045
56257
  });
56046
56258
  }
@@ -56136,7 +56348,7 @@ function createTriggerRequest(trigger, context, config2) {
56136
56348
  const metadata = TRIGGER_METADATA[trigger];
56137
56349
  const { fallback, timeout } = getTriggerConfig(trigger, config2);
56138
56350
  const summary = substituteTemplate(metadata.defaultSummary, context);
56139
- const id = `trigger-${trigger}-${Date.now()}`;
56351
+ const id = `trigger-${trigger}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
56140
56352
  return {
56141
56353
  id,
56142
56354
  type: "confirm",
@@ -56315,10 +56527,10 @@ function validateFeatureName(feature) {
56315
56527
 
56316
56528
  // src/plan/critic.ts
56317
56529
  import { mkdir as mkdir6 } from "fs/promises";
56318
- import { dirname as dirname8, join as join43 } from "path";
56530
+ import { dirname as dirname9, join as join43 } from "path";
56319
56531
  async function writeSpecDeltas(findings, workdir, runId, storyId, manifest) {
56320
56532
  const path8 = join43(workdir, ".nax", "runs", runId, "plan", storyId, "spec-deltas.md");
56321
- await mkdir6(dirname8(path8), { recursive: true });
56533
+ await mkdir6(dirname9(path8), { recursive: true });
56322
56534
  await Bun.write(path8, formatSpecDeltas(findings, manifest));
56323
56535
  return path8;
56324
56536
  }
@@ -56393,13 +56605,13 @@ function createCliInteractionBridge() {
56393
56605
  process.stdout.write(`
56394
56606
  \uD83E\uDD16 Agent: ${text}
56395
56607
  You: `);
56396
- return new Promise((resolve14) => {
56608
+ return new Promise((resolve15) => {
56397
56609
  const rl = createInterface2({ input: process.stdin, terminal: false });
56398
56610
  rl.once("line", (line) => {
56399
56611
  rl.close();
56400
- resolve14(line.trim());
56612
+ resolve15(line.trim());
56401
56613
  });
56402
- rl.once("close", () => resolve14(""));
56614
+ rl.once("close", () => resolve15(""));
56403
56615
  });
56404
56616
  }
56405
56617
  };
@@ -58463,14 +58675,14 @@ var init_status_cost = __esm(() => {
58463
58675
 
58464
58676
  // src/commands/common.ts
58465
58677
  import { existsSync as existsSync16, readdirSync as readdirSync2, realpathSync as realpathSync4 } from "fs";
58466
- import { join as join46, resolve as resolve14 } from "path";
58678
+ import { join as join46, resolve as resolve15 } from "path";
58467
58679
  function resolveProject2(options = {}) {
58468
58680
  const { dir, feature } = options;
58469
58681
  let projectRoot;
58470
58682
  let naxDir;
58471
58683
  let configPath;
58472
58684
  if (dir) {
58473
- projectRoot = realpathSync4(resolve14(dir));
58685
+ projectRoot = realpathSync4(resolve15(dir));
58474
58686
  naxDir = join46(projectRoot, ".nax");
58475
58687
  if (!existsSync16(naxDir)) {
58476
58688
  throw new NaxError(`Directory does not contain a nax project: ${projectRoot}
@@ -58527,12 +58739,28 @@ No features found in this project.`;
58527
58739
  featureDir
58528
58740
  };
58529
58741
  }
58742
+ function resolveSingleFeature(naxDir, remediationHint = "pass -f <name>") {
58743
+ const featuresDir = join46(naxDir, "features");
58744
+ const available = existsSync16(featuresDir) ? readdirSync2(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort() : [];
58745
+ if (available.length === 1)
58746
+ return available[0];
58747
+ if (available.length === 0) {
58748
+ throw new NaxError(`No feature specified and no features found \u2014 ${remediationHint}.`, "FEATURE_NOT_SPECIFIED", {
58749
+ featuresDir
58750
+ });
58751
+ }
58752
+ throw new NaxError(`No feature specified and multiple features exist \u2014 ${remediationHint}.
58753
+
58754
+ Available features:
58755
+ ${available.map((f) => ` - ${f}`).join(`
58756
+ `)}`, "FEATURE_AMBIGUOUS", { featuresDir, available });
58757
+ }
58530
58758
  async function resolveProjectAsync(options = {}) {
58531
58759
  const { dir } = options;
58532
58760
  if (!dir) {
58533
58761
  return resolveProject2(options);
58534
58762
  }
58535
- if (existsSync16(resolve14(dir))) {
58763
+ if (existsSync16(resolve15(dir))) {
58536
58764
  return resolveProject2(options);
58537
58765
  }
58538
58766
  const isPlainName = !dir.includes("/") && !dir.includes("\\");
@@ -58548,17 +58776,17 @@ async function resolveProjectAsync(options = {}) {
58548
58776
  } catch {}
58549
58777
  }
58550
58778
  throw new NaxError(`No project found for name or path: "${dir}"
58551
- Checked filesystem path: ${resolve14(dir)}
58779
+ Checked filesystem path: ${resolve15(dir)}
58552
58780
  Checked identity registry: ${registryIdentityPath}
58553
- Tip: use an absolute or relative path, or run "nax init" in your project directory first.`, "PROJECT_NOT_FOUND", { dir, resolvedPath: resolve14(dir), registryIdentityPath });
58781
+ Tip: use an absolute or relative path, or run "nax init" in your project directory first.`, "PROJECT_NOT_FOUND", { dir, resolvedPath: resolve15(dir), registryIdentityPath });
58554
58782
  }
58555
58783
  try {
58556
58784
  return resolveProject2(options);
58557
58785
  } catch (err) {
58558
58786
  if (err instanceof Error && "code" in err && err.code === "ENOENT") {
58559
- throw new NaxError(`Path does not exist: ${resolve14(dir)}`, "PROJECT_NOT_FOUND", {
58787
+ throw new NaxError(`Path does not exist: ${resolve15(dir)}`, "PROJECT_NOT_FOUND", {
58560
58788
  dir,
58561
- resolvedPath: resolve14(dir),
58789
+ resolvedPath: resolve15(dir),
58562
58790
  cause: err
58563
58791
  });
58564
58792
  }
@@ -58566,7 +58794,7 @@ Tip: use an absolute or relative path, or run "nax init" in your project directo
58566
58794
  }
58567
58795
  }
58568
58796
  function findProjectRoot(startDir) {
58569
- let current = resolve14(startDir);
58797
+ let current = resolve15(startDir);
58570
58798
  let depth = 0;
58571
58799
  while (depth < MAX_DIRECTORY_DEPTH) {
58572
58800
  const naxDir = join46(current, ".nax");
@@ -58596,7 +58824,7 @@ __export(exports_status_features, {
58596
58824
  _statusFeaturesDeps: () => _statusFeaturesDeps
58597
58825
  });
58598
58826
  import { existsSync as existsSync17, readdirSync as readdirSync3 } from "fs";
58599
- import { basename as basename10, join as join47, resolve as resolve15 } from "path";
58827
+ import { basename as basename10, join as join47, resolve as resolve16 } from "path";
58600
58828
  function isPidAlive(pid) {
58601
58829
  try {
58602
58830
  process.kill(pid, 0);
@@ -58911,7 +59139,7 @@ async function displayFeatureStatus(options = {}) {
58911
59139
  if (options.feature) {
58912
59140
  let featureDir;
58913
59141
  if (options.dir) {
58914
- const projectDir = resolve15(options.dir);
59142
+ const projectDir = resolve16(options.dir);
58915
59143
  const config2 = await _statusFeaturesDeps.loadConfig(projectDir).catch(() => null);
58916
59144
  const projectKey = config2?.name?.trim() || basename10(projectDir);
58917
59145
  const outputDir = _statusFeaturesDeps.projectOutputDir(projectKey, config2?.outputDir);
@@ -59150,9 +59378,11 @@ var init_events = () => {};
59150
59378
  // src/acceptance/fix-generator.ts
59151
59379
  function findRelatedStories(failedAC, prd) {
59152
59380
  const relatedStoryIds = [];
59381
+ const escapedAC = failedAC.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
59382
+ const acPattern = new RegExp(`(?<![\\w-])${escapedAC}(?!\\d)`);
59153
59383
  for (const story of prd.userStories) {
59154
59384
  for (const ac of story.acceptanceCriteria) {
59155
- if (ac.includes(failedAC)) {
59385
+ if (acPattern.test(ac)) {
59156
59386
  relatedStoryIds.push(story.id);
59157
59387
  break;
59158
59388
  }
@@ -59525,6 +59755,58 @@ function logTestOutput(logger, stage, output, opts = {}) {
59525
59755
  output: lines
59526
59756
  });
59527
59757
  }
59758
+ // src/verification/flake-baseline-diff.ts
59759
+ async function resolveFlakeBaselineDiff(config2, workdir, storyWorkdir) {
59760
+ try {
59761
+ const resolved = await resolveTestFilePatterns(config2, workdir, storyWorkdir);
59762
+ const baseRef = await getMergeBase(workdir);
59763
+ if (baseRef === undefined) {
59764
+ throw new NaxError("getMergeBase() found no usable ref (empty/detached repo)", "FLAKE_BASELINE_NO_MERGE_BASE", {
59765
+ stage: "flake-triage",
59766
+ workdir
59767
+ });
59768
+ }
59769
+ const preflight = await gitWithTimeout(["diff", "--name-only", baseRef], workdir);
59770
+ if (preflight.exitCode !== 0) {
59771
+ throw new NaxError(`git diff --name-only ${baseRef} failed (exit ${preflight.exitCode})`, "FLAKE_BASELINE_GIT_DIFF_FAILED", {
59772
+ stage: "flake-triage",
59773
+ baseRef,
59774
+ exitCode: preflight.exitCode
59775
+ });
59776
+ }
59777
+ const changedTestFiles = await getChangedTestFiles(workdir, workdir, baseRef, storyWorkdir, [...resolved.regex]);
59778
+ const changedNonTestFiles = await getChangedNonTestFiles(workdir, baseRef, storyWorkdir, [...resolved.regex], undefined, workdir);
59779
+ const mappedTestFiles = await mapSourceToTests(changedNonTestFiles, workdir, storyWorkdir, [...resolved.globs]);
59780
+ return { changedTestFiles, mappedTestFiles };
59781
+ } catch (err) {
59782
+ getSafeLogger()?.warn("flake-triage", "Baseline diff resolution failed \u2014 skipping triage this gate (fail closed)", {
59783
+ error: errorMessage(err)
59784
+ });
59785
+ return null;
59786
+ }
59787
+ }
59788
+ var init_flake_baseline_diff = __esm(() => {
59789
+ init_errors();
59790
+ init_logger2();
59791
+ init_test_runners();
59792
+ init_git();
59793
+ init_smart_runner();
59794
+ });
59795
+
59796
+ // src/verification/shell-quote.ts
59797
+ function shellQuoteArg(arg) {
59798
+ return `'${arg.replaceAll("'", `'\\''`)}'`;
59799
+ }
59800
+
59801
+ // src/verification/index.ts
59802
+ var init_verification = __esm(() => {
59803
+ init_executor();
59804
+ init_runners();
59805
+ init_flake_probe();
59806
+ init_flake_triage();
59807
+ init_flake_baseline_diff();
59808
+ init_mutation();
59809
+ });
59528
59810
 
59529
59811
  // src/pipeline/stages/acceptance.ts
59530
59812
  var exports_acceptance2 = {};
@@ -59546,6 +59828,7 @@ var init_acceptance3 = __esm(() => {
59546
59828
  init_logger2();
59547
59829
  init_prd();
59548
59830
  init_test_runners();
59831
+ init_verification();
59549
59832
  _acceptanceStageDeps = {
59550
59833
  runHardeningPass: async (ctx) => {
59551
59834
  const { runHardeningPass: runHardeningPass2 } = await Promise.resolve().then(() => (init_acceptance2(), exports_acceptance));
@@ -59624,18 +59907,9 @@ var init_acceptance3 = __esm(() => {
59624
59907
  cmd: testCmdParts.join(" "),
59625
59908
  packageDir
59626
59909
  });
59627
- const proc = Bun.spawn(testCmdParts, {
59628
- cwd: packageDir,
59629
- stdout: "pipe",
59630
- stderr: "pipe"
59631
- });
59632
- const [exitCode, stdout, stderr] = await Promise.all([
59633
- proc.exited,
59634
- new Response(proc.stdout).text(),
59635
- new Response(proc.stderr).text()
59636
- ]);
59637
- const output = `${stdout}
59638
- ${stderr}`;
59910
+ const execution = await executeWithTimeout(testCmdParts.map(shellQuoteArg).join(" "), Math.ceil(ctx.config.acceptance.timeoutMs / 1000), undefined, { cwd: packageDir });
59911
+ const exitCode = execution.exitCode ?? (execution.success ? 0 : 1);
59912
+ const output = execution.output ?? "";
59639
59913
  allOutputParts.push(output);
59640
59914
  const failedACs = parseTestFailures2(output);
59641
59915
  const overrides = ctx.prd.acceptanceOverrides ?? {};
@@ -60049,7 +60323,7 @@ async function runAcceptanceSetup(ctx, featureDir, phaseStartTime) {
60049
60323
  cmd: runCmd.join(" "),
60050
60324
  packageDir
60051
60325
  });
60052
- const { exitCode } = await _acceptanceSetupDeps.runTest(testPath, packageDir, runCmd);
60326
+ const { exitCode } = await _acceptanceSetupDeps.runTest(testPath, packageDir, runCmd, ctx.config.acceptance.timeoutMs);
60053
60327
  if (exitCode !== 0) {
60054
60328
  redFailCount++;
60055
60329
  }
@@ -60086,6 +60360,7 @@ var init_acceptance_setup = __esm(() => {
60086
60360
  init_logger2();
60087
60361
  init_operations();
60088
60362
  init_git();
60363
+ init_verification();
60089
60364
  init_event_bus();
60090
60365
  _acceptanceSetupDeps = {
60091
60366
  getAgent: (_name) => {
@@ -60146,20 +60421,14 @@ var init_acceptance_setup = __esm(() => {
60146
60421
  loadGroupConfig: async (projectDir, relativeWorkdir) => {
60147
60422
  return loadConfigForWorkdir(path13.join(projectDir, ".nax", "config.json"), relativeWorkdir || undefined);
60148
60423
  },
60149
- runTest: async (_testPath, _workdir, _cmd) => {
60150
- const cmd = _cmd;
60151
- const proc = Bun.spawn(cmd, {
60152
- cwd: _workdir,
60153
- stdout: "pipe",
60154
- stderr: "pipe"
60424
+ runTest: async (_testPath, _workdir, _cmd, timeoutMs = 1800000) => {
60425
+ const execution = await executeWithTimeout(_cmd.map(shellQuoteArg).join(" "), Math.ceil(timeoutMs / 1000), undefined, {
60426
+ cwd: _workdir
60155
60427
  });
60156
- const [exitCode, stdout, stderr] = await Promise.all([
60157
- proc.exited,
60158
- new Response(proc.stdout).text(),
60159
- new Response(proc.stderr).text()
60160
- ]);
60161
- return { exitCode, output: `${stdout}
60162
- ${stderr}` };
60428
+ return {
60429
+ exitCode: execution.exitCode ?? (execution.success ? 0 : 1),
60430
+ output: execution.output ?? ""
60431
+ };
60163
60432
  },
60164
60433
  callOp: async (pipelineCtx, packageDir, op, input, storyId) => {
60165
60434
  if (!pipelineCtx.runtime) {
@@ -60591,7 +60860,7 @@ var init_constitution = __esm(() => {
60591
60860
  });
60592
60861
 
60593
60862
  // src/pipeline/stages/constitution.ts
60594
- import { dirname as dirname9 } from "path";
60863
+ import { dirname as dirname10 } from "path";
60595
60864
  var constitutionStage;
60596
60865
  var init_constitution2 = __esm(() => {
60597
60866
  init_constitution();
@@ -60601,7 +60870,7 @@ var init_constitution2 = __esm(() => {
60601
60870
  enabled: (ctx) => ctx.config.constitution.enabled,
60602
60871
  async execute(ctx) {
60603
60872
  const logger = getLogger();
60604
- const ngentDir = ctx.featureDir ? dirname9(dirname9(ctx.featureDir)) : `${ctx.workdir}/nax`;
60873
+ const ngentDir = ctx.featureDir ? dirname10(dirname10(ctx.featureDir)) : `${ctx.workdir}/nax`;
60605
60874
  const result = await loadConstitution(ngentDir, ctx.config.constitution);
60606
60875
  if (result) {
60607
60876
  ctx.constitution = result;
@@ -61133,11 +61402,11 @@ async function spawnWithTimeout(proc, timeoutMs) {
61133
61402
  ]);
61134
61403
  return { stdout, exitCode };
61135
61404
  })(),
61136
- new Promise((resolve16) => setTimeout(() => {
61405
+ new Promise((resolve17) => setTimeout(() => {
61137
61406
  try {
61138
61407
  proc.kill("SIGKILL");
61139
61408
  } catch {}
61140
- resolve16({ stdout: "", exitCode: 1 });
61409
+ resolve17({ stdout: "", exitCode: 1 });
61141
61410
  }, timeoutMs))
61142
61411
  ]);
61143
61412
  return result;
@@ -61908,38 +62177,6 @@ var init_story_orchestrator_logging = __esm(() => {
61908
62177
  init_logger2();
61909
62178
  init_phase_eval();
61910
62179
  });
61911
- // src/verification/flake-baseline-diff.ts
61912
- async function resolveFlakeBaselineDiff(config2, workdir, storyWorkdir) {
61913
- try {
61914
- const resolved = await resolveTestFilePatterns(config2, workdir, storyWorkdir);
61915
- const baseRef = await getMergeBase(workdir);
61916
- const changedTestFiles = await getChangedTestFiles(workdir, workdir, baseRef, storyWorkdir, [...resolved.regex]);
61917
- const changedNonTestFiles = await getChangedNonTestFiles(workdir, baseRef, storyWorkdir, [...resolved.regex], undefined, workdir);
61918
- const mappedTestFiles = await mapSourceToTests(changedNonTestFiles, workdir, storyWorkdir, [...resolved.globs]);
61919
- return { changedTestFiles, mappedTestFiles };
61920
- } catch (err) {
61921
- getSafeLogger()?.warn("flake-triage", "Baseline diff resolution failed \u2014 skipping triage this gate (fail closed)", {
61922
- error: errorMessage(err)
61923
- });
61924
- return null;
61925
- }
61926
- }
61927
- var init_flake_baseline_diff = __esm(() => {
61928
- init_logger2();
61929
- init_test_runners();
61930
- init_git();
61931
- init_smart_runner();
61932
- });
61933
-
61934
- // src/verification/index.ts
61935
- var init_verification = __esm(() => {
61936
- init_executor();
61937
- init_runners();
61938
- init_flake_probe();
61939
- init_flake_triage();
61940
- init_flake_baseline_diff();
61941
- init_mutation();
61942
- });
61943
62180
 
61944
62181
  // src/execution/story-orchestrator/flake-triage-seam.ts
61945
62182
  var productionTriageSeam = async (gateFindings, { ctx, rawOutput, quarantineMemo }) => {
@@ -63036,7 +63273,7 @@ var init_story_orchestrator = __esm(() => {
63036
63273
  });
63037
63274
 
63038
63275
  // src/execution/build-plan-for-strategy.ts
63039
- import { join as join52, normalize as normalize5, resolve as resolve16, sep as sep6 } from "path";
63276
+ import { join as join52, normalize as normalize5, resolve as resolve17, sep as sep6 } from "path";
63040
63277
  function requiresInitialRefCapture(strategy) {
63041
63278
  return isThreeSessionStrategy(strategy);
63042
63279
  }
@@ -63051,7 +63288,7 @@ function resolveStoryPathAnchors(ctxPackageDir, storyWorkdir) {
63051
63288
  if (segments.length === 0) {
63052
63289
  return { repoRoot: base, packageDir: base };
63053
63290
  }
63054
- const candidateRoot = resolve16(base, ...Array(segments.length).fill(".."));
63291
+ const candidateRoot = resolve17(base, ...Array(segments.length).fill(".."));
63055
63292
  if (join52(candidateRoot, normalizedRel) === base) {
63056
63293
  return { repoRoot: candidateRoot, packageDir: base };
63057
63294
  }
@@ -67451,19 +67688,20 @@ function h2PullToolEmptyResult(observations, threshold) {
67451
67688
  const byKeyword = new Map;
67452
67689
  for (const obs of pulls) {
67453
67690
  const keyword = obs.payload.keyword;
67691
+ const site = `${obs.featureId}/${obs.storyId}`;
67454
67692
  const existing = byKeyword.get(keyword);
67455
67693
  if (existing) {
67456
- existing.storyIds.push(obs.storyId);
67694
+ existing.sites.push(site);
67457
67695
  } else {
67458
- byKeyword.set(keyword, { storyIds: [obs.storyId], featureId: obs.featureId });
67696
+ byKeyword.set(keyword, { sites: [site], featureId: obs.featureId });
67459
67697
  }
67460
67698
  }
67461
67699
  const proposals = [];
67462
67700
  for (const [keyword, data] of byKeyword.entries()) {
67463
- if (data.storyIds.length < threshold)
67701
+ if (data.sites.length < threshold)
67464
67702
  continue;
67465
- const count = data.storyIds.length;
67466
- const unique = uniqueStoryIds(data.storyIds);
67703
+ const count = data.sites.length;
67704
+ const unique = uniqueStoryIds(data.sites);
67467
67705
  proposals.push({
67468
67706
  id: "H2",
67469
67707
  severity: "MED",
@@ -67480,23 +67718,24 @@ function h3RepeatedRectification(observations, threshold) {
67480
67718
  const cycles = observations.filter((o) => o.kind === "rectify-cycle");
67481
67719
  const byStory = new Map;
67482
67720
  for (const obs of cycles) {
67483
- const existing = byStory.get(obs.storyId);
67721
+ const key = `${obs.featureId}/${obs.storyId}`;
67722
+ const existing = byStory.get(key);
67484
67723
  if (existing) {
67485
67724
  existing.count++;
67486
67725
  } else {
67487
- byStory.set(obs.storyId, { count: 1, featureId: obs.featureId });
67726
+ byStory.set(key, { count: 1, featureId: obs.featureId, storyId: obs.storyId });
67488
67727
  }
67489
67728
  }
67490
67729
  const proposals = [];
67491
- for (const [storyId, data] of byStory.entries()) {
67492
- if (data.count < threshold)
67730
+ for (const { count, featureId, storyId } of byStory.values()) {
67731
+ if (count < threshold)
67493
67732
  continue;
67494
67733
  proposals.push({
67495
67734
  id: "H3",
67496
67735
  severity: "HIGH",
67497
- target: { canonicalFile: `.nax/features/${data.featureId}/context.md`, action: "add" },
67498
- description: `Repeated rectification cycle: story ${storyId} required ${data.count} rectify attempts`,
67499
- evidence: `Story ${storyId} triggered ${data.count} rectify cycles`,
67736
+ target: { canonicalFile: `.nax/features/${featureId}/context.md`, action: "add" },
67737
+ description: `Repeated rectification cycle: story ${storyId} required ${count} rectify attempts`,
67738
+ evidence: `Story ${storyId} triggered ${count} rectify cycles`,
67500
67739
  sourceKinds: ["rectify-cycle"],
67501
67740
  storyIds: [storyId]
67502
67741
  });
@@ -67508,19 +67747,20 @@ function h4EscalationChain(observations, threshold) {
67508
67747
  const byPath = new Map;
67509
67748
  for (const obs of escalations) {
67510
67749
  const key = `${obs.payload.from}->${obs.payload.to}`;
67750
+ const site = `${obs.featureId}/${obs.storyId}`;
67511
67751
  const existing = byPath.get(key);
67512
67752
  if (existing) {
67513
- existing.storyIds.push(obs.storyId);
67753
+ existing.sites.push(site);
67514
67754
  } else {
67515
- byPath.set(key, { storyIds: [obs.storyId], featureId: obs.featureId });
67755
+ byPath.set(key, { sites: [site], featureId: obs.featureId });
67516
67756
  }
67517
67757
  }
67518
67758
  const proposals = [];
67519
67759
  for (const [escalationPath, data] of byPath.entries()) {
67520
- if (data.storyIds.length < threshold)
67760
+ if (data.sites.length < threshold)
67521
67761
  continue;
67522
- const count = data.storyIds.length;
67523
- const unique = uniqueStoryIds(data.storyIds);
67762
+ const count = data.sites.length;
67763
+ const unique = uniqueStoryIds(data.sites);
67524
67764
  proposals.push({
67525
67765
  id: "H4",
67526
67766
  severity: "MED",
@@ -67567,15 +67807,17 @@ function h6FixCycleUnchanged(observations, threshold) {
67567
67807
  const iterations = observations.filter((o) => o.kind === "fix-cycle-iteration");
67568
67808
  const byStory = new Map;
67569
67809
  for (const obs of iterations) {
67570
- const existing = byStory.get(obs.storyId);
67810
+ const key = `${obs.featureId}/${obs.storyId}`;
67811
+ const existing = byStory.get(key);
67571
67812
  if (existing) {
67572
67813
  existing.push(obs);
67573
67814
  } else {
67574
- byStory.set(obs.storyId, [obs]);
67815
+ byStory.set(key, [obs]);
67575
67816
  }
67576
67817
  }
67577
67818
  const proposals = [];
67578
- for (const [storyId, storyIterations] of byStory.entries()) {
67819
+ for (const storyIterations of byStory.values()) {
67820
+ const storyId = storyIterations[0].storyId;
67579
67821
  const ordered = [...storyIterations].sort((a, b) => (a.payload.iterationNum ?? a.payload.iteration) - (b.payload.iterationNum ?? b.payload.iteration));
67580
67822
  let currentStreak = 0;
67581
67823
  let maxStreak = 0;
@@ -69697,6 +69939,15 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, di
69697
69939
  const effectiveProjectRoot = projectRoot || projectDir;
69698
69940
  const pluginNames = new Set;
69699
69941
  const disabledSet = new Set(disabledPlugins ?? []);
69942
+ const registerLoadedPlugin = (entry) => {
69943
+ const existingIndex = loadedPlugins.findIndex((p) => p.plugin.name === entry.plugin.name);
69944
+ if (existingIndex >= 0) {
69945
+ loadedPlugins[existingIndex] = entry;
69946
+ } else {
69947
+ loadedPlugins.push(entry);
69948
+ }
69949
+ pluginNames.add(entry.plugin.name);
69950
+ };
69700
69951
  const logger = getSafeLogger6();
69701
69952
  if (!disabledSet.has(curatorPlugin.name)) {
69702
69953
  if (curatorPlugin.setup) {
@@ -69784,11 +70035,10 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, di
69784
70035
  if (pluginNames.has(validated.name)) {
69785
70036
  logger?.warn("plugins", `Plugin name collision: '${validated.name}' (global directory)`);
69786
70037
  }
69787
- loadedPlugins.push({
70038
+ registerLoadedPlugin({
69788
70039
  plugin: validated,
69789
70040
  source: { type: "global", path: plugin.path }
69790
70041
  });
69791
- pluginNames.add(validated.name);
69792
70042
  }
69793
70043
  }
69794
70044
  const projectPlugins = await discoverPlugins(projectDir, isTestFileFn);
@@ -69803,11 +70053,10 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, di
69803
70053
  if (pluginNames.has(validated.name)) {
69804
70054
  logger?.warn("plugins", `Plugin name collision: '${validated.name}' (project directory overrides global)`);
69805
70055
  }
69806
- loadedPlugins.push({
70056
+ registerLoadedPlugin({
69807
70057
  plugin: validated,
69808
70058
  source: { type: "project", path: plugin.path }
69809
70059
  });
69810
- pluginNames.add(validated.name);
69811
70060
  }
69812
70061
  }
69813
70062
  for (const entry of configPlugins) {
@@ -69821,11 +70070,10 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, di
69821
70070
  if (pluginNames.has(validated.name)) {
69822
70071
  logger?.warn("plugins", `Plugin name collision: '${validated.name}' (config entry overrides previous)`);
69823
70072
  }
69824
- loadedPlugins.push({
70073
+ registerLoadedPlugin({
69825
70074
  plugin: validated,
69826
70075
  source: { type: "config", path: entry.module }
69827
70076
  });
69828
- pluginNames.add(validated.name);
69829
70077
  }
69830
70078
  }
69831
70079
  return new PluginRegistry(loadedPlugins, builtinPostRunActions);
@@ -70158,8 +70406,8 @@ var init_types10 = __esm(() => {
70158
70406
  import { join as join88 } from "path";
70159
70407
  function createDrainDeadline2(deadlineMs) {
70160
70408
  let timeoutId;
70161
- const promise2 = new Promise((resolve21) => {
70162
- timeoutId = setTimeout(() => resolve21(""), deadlineMs);
70409
+ const promise2 = new Promise((resolve22) => {
70410
+ timeoutId = setTimeout(() => resolve22(""), deadlineMs);
70163
70411
  });
70164
70412
  return {
70165
70413
  promise: promise2,
@@ -70279,14 +70527,17 @@ async function executeHook(hookDef, ctx, workdir) {
70279
70527
  env: buildAllowedEnv({ env: env2 })
70280
70528
  });
70281
70529
  let timedOut = false;
70530
+ let killTimeoutId;
70282
70531
  const timeoutId = setTimeout(() => {
70283
70532
  timedOut = true;
70284
70533
  killProcessGroup(proc.pid, "SIGTERM");
70534
+ killTimeoutId = setTimeout(() => killProcessGroup(proc.pid, "SIGKILL"), HOOK_KILL_GRACE_MS);
70285
70535
  }, timeout);
70286
70536
  const stdoutPromise = new Response(proc.stdout).text().catch(() => "");
70287
70537
  const stderrPromise = new Response(proc.stderr).text().catch(() => "");
70288
70538
  const exitCode = await proc.exited;
70289
70539
  clearTimeout(timeoutId);
70540
+ clearTimeout(killTimeoutId);
70290
70541
  const [stdout, stderr] = timedOut ? await (async () => {
70291
70542
  const stdoutDrain = createDrainDeadline2(STREAM_DRAIN_TIMEOUT_MS2);
70292
70543
  const stderrDrain = createDrainDeadline2(STREAM_DRAIN_TIMEOUT_MS2);
@@ -70339,7 +70590,7 @@ async function fireHook(config2, event, ctx, workdir) {
70339
70590
  }
70340
70591
  }
70341
70592
  }
70342
- var DEFAULT_TIMEOUT = 5000, STREAM_DRAIN_TIMEOUT_MS2 = 2000;
70593
+ var DEFAULT_TIMEOUT = 5000, STREAM_DRAIN_TIMEOUT_MS2 = 2000, HOOK_KILL_GRACE_MS = 5000;
70343
70594
  var init_runner5 = __esm(() => {
70344
70595
  init_env();
70345
70596
  init_logger2();
@@ -70697,7 +70948,7 @@ var init_crash_recovery = __esm(() => {
70697
70948
  });
70698
70949
 
70699
70950
  // src/acceptance/import-resolution.ts
70700
- import { resolve as resolve21, sep as sep9 } from "path";
70951
+ import { resolve as resolve22, sep as sep9 } from "path";
70701
70952
  function languageFromExtension(testFilePath) {
70702
70953
  if (!testFilePath)
70703
70954
  return;
@@ -70719,8 +70970,8 @@ async function resolveLanguage(opts) {
70719
70970
  return "typescript";
70720
70971
  }
70721
70972
  async function readCapped(relPath, packageDir) {
70722
- const resolvedPackageDir = resolve21(packageDir);
70723
- const fullPath = resolve21(resolvedPackageDir, relPath);
70973
+ const resolvedPackageDir = resolve22(packageDir);
70974
+ const fullPath = resolve22(resolvedPackageDir, relPath);
70724
70975
  if (fullPath !== resolvedPackageDir && !fullPath.startsWith(resolvedPackageDir + sep9)) {
70725
70976
  return null;
70726
70977
  }
@@ -71306,7 +71557,7 @@ async function runAcceptanceLoop(ctx) {
71306
71557
  const prdDirty = false;
71307
71558
  logger?.info("acceptance", "All stories complete, running acceptance validation");
71308
71559
  const { acceptanceStage: acceptanceStage2 } = await _runAcceptanceTestsOnceDeps.importAcceptanceStage();
71309
- while (acceptanceRetries < maxRetries) {
71560
+ do {
71310
71561
  const attemptCtx = { ...ctx, acceptanceRetries };
71311
71562
  const firstStory = prd.userStories[0];
71312
71563
  const acceptanceContext = buildAcceptanceContext(attemptCtx, prd);
@@ -71412,7 +71663,7 @@ async function runAcceptanceLoop(ctx) {
71412
71663
  const success2 = finalCheck.passed && remainingFindings.length === 0;
71413
71664
  const failureMessages = !success2 ? finalCheck.failedACs.length > 0 ? finalCheck.failedACs : remainingFindings.length > 0 ? remainingFindings.map((f) => f.message) : ["acceptance validation failed (unknown cause)"] : undefined;
71414
71665
  return buildResult(success2, prd, totalCost2, iterations, storiesCompleted, prdDirty, failureMessages, acceptanceRetries + totalInternalIterations, finalCheck.missingTargets);
71415
- }
71666
+ } while (acceptanceRetries < maxRetries);
71416
71667
  return buildResult(false, prd, totalCost2, iterations, storiesCompleted, prdDirty);
71417
71668
  }
71418
71669
  var _acceptanceLoopDeps, _acceptanceFixCycleDeps, _runAcceptanceTestsOnceDeps, MAX_STUB_REGENS = 2;
@@ -71441,7 +71692,7 @@ var init_acceptance_loop = __esm(() => {
71441
71692
 
71442
71693
  // src/session/scratch-purge.ts
71443
71694
  import { mkdir as mkdir12, rename as rename2, rm } from "fs/promises";
71444
- import { dirname as dirname15, join as join89 } from "path";
71695
+ import { dirname as dirname16, join as join89 } from "path";
71445
71696
  async function purgeStaleScratch(projectDir, featureName, retentionDays, archiveInsteadOfDelete = false) {
71446
71697
  const sessionsDir = join89(projectDir, ".nax", "features", featureName, "sessions");
71447
71698
  const sessionIds = await _scratchPurgeDeps.listSessionDirs(sessionsDir);
@@ -71492,7 +71743,7 @@ var init_scratch_purge = __esm(() => {
71492
71743
  readFile: (path26) => Bun.file(path26).text(),
71493
71744
  remove: (path26) => rm(path26, { recursive: true, force: true }),
71494
71745
  move: async (src, dest) => {
71495
- await mkdir12(dirname15(dest), { recursive: true });
71746
+ await mkdir12(dirname16(dest), { recursive: true });
71496
71747
  await rename2(src, dest);
71497
71748
  },
71498
71749
  now: () => Date.now()
@@ -71566,10 +71817,11 @@ async function runRegressionFlakeTriage(params) {
71566
71817
  testCommand,
71567
71818
  quarantineMemo,
71568
71819
  triageFn,
71820
+ resolveBaselineDiffFn,
71569
71821
  flakeDetection
71570
71822
  } = params;
71571
71823
  const logger = getSafeLogger();
71572
- const baselineDiff = await resolveFlakeBaselineDiff(config2, workdir);
71824
+ const baselineDiff = await resolveBaselineDiffFn(config2, workdir);
71573
71825
  if (baselineDiff === null) {
71574
71826
  const untriaged = regressionFindings.filter((f) => f.category === "failed-test");
71575
71827
  const testFilesInFailures2 = new Set;
@@ -71625,7 +71877,6 @@ async function runRegressionFlakeTriage(params) {
71625
71877
  var init_run_regression_triage = __esm(() => {
71626
71878
  init_logger2();
71627
71879
  init_test_runners();
71628
- init_verification();
71629
71880
  });
71630
71881
 
71631
71882
  // src/execution/lifecycle/run-regression.ts
@@ -71779,6 +72030,7 @@ async function runDeferredRegression(options) {
71779
72030
  testCommand,
71780
72031
  quarantineMemo,
71781
72032
  triageFn: _regressionDeps.triageFlakyFindings,
72033
+ resolveBaselineDiffFn: _regressionDeps.resolveFlakeBaselineDiff,
71782
72034
  flakeDetection: config2.execution.flakeDetection
71783
72035
  });
71784
72036
  if (triageOutcome.shortCircuit) {
@@ -71957,7 +72209,8 @@ var init_run_regression = __esm(() => {
71957
72209
  runVerification: fullSuite,
71958
72210
  runFixCycle: (cycle, ctx, name) => runFixCycle(cycle, ctx, name),
71959
72211
  parseTestOutput,
71960
- triageFlakyFindings
72212
+ triageFlakyFindings,
72213
+ resolveFlakeBaselineDiff
71961
72214
  };
71962
72215
  });
71963
72216
 
@@ -72221,6 +72474,17 @@ async function handleRunCompletion(options) {
72221
72474
  logger?.warn("run.complete", "Failed to purge stale session scratch", { error: String(err) });
72222
72475
  }
72223
72476
  }
72477
+ const manifestCfg = config2.context?.v2?.manifest;
72478
+ if (manifestCfg?.retentionDays) {
72479
+ try {
72480
+ const purged = await _runCompletionDeps.purgeStaleManifests(effectiveProjectDir, manifestCfg.retentionDays);
72481
+ if (purged > 0) {
72482
+ logger?.info("run.complete", "Purged stale context manifests", { purged });
72483
+ }
72484
+ } catch (err) {
72485
+ logger?.warn("run.complete", "Failed to purge stale context manifests", { error: String(err) });
72486
+ }
72487
+ }
72224
72488
  const storyMetricsSummary = allStoryMetrics.map((sm) => ({
72225
72489
  storyId: sm.storyId,
72226
72490
  complexity: sm.complexity,
@@ -72274,6 +72538,7 @@ async function handleRunCompletion(options) {
72274
72538
  }
72275
72539
  var _runCompletionDeps;
72276
72540
  var init_run_completion = __esm(() => {
72541
+ init_engine();
72277
72542
  init_pipeline();
72278
72543
  init_agents();
72279
72544
  init_runner5();
@@ -72289,7 +72554,8 @@ var init_run_completion = __esm(() => {
72289
72554
  _runCompletionDeps = {
72290
72555
  runDeferredRegression,
72291
72556
  fireHook,
72292
- closeAllRunSessions
72557
+ closeAllRunSessions,
72558
+ purgeStaleManifests
72293
72559
  };
72294
72560
  });
72295
72561
 
@@ -74636,7 +74902,18 @@ function selectNextStories(prd, config2, batchPlan, currentBatchIndex, lastStory
74636
74902
  const batch = batchPlan[currentBatchIndex];
74637
74903
  const storiesToExecute = batch.stories.filter((s) => !s.passes && s.status !== "passed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "failed" && s.status !== "paused" && s.status !== "decomposed");
74638
74904
  if (storiesToExecute.length === 0) {
74639
- return null;
74905
+ const fallbackStory = getNextStory(prd, lastStoryId, config2.execution.rectification?.maxAttemptsTotal ?? 12);
74906
+ if (!fallbackStory)
74907
+ return null;
74908
+ return {
74909
+ selection: {
74910
+ story: fallbackStory,
74911
+ storiesToExecute: [fallbackStory],
74912
+ routing: buildPreviewRouting(fallbackStory, config2),
74913
+ isBatchExecution: false
74914
+ },
74915
+ nextBatchIndex: currentBatchIndex + 1
74916
+ };
74640
74917
  }
74641
74918
  const story2 = storiesToExecute[0];
74642
74919
  return {
@@ -74833,6 +75110,8 @@ async function runParallelBatch(options) {
74833
75110
  const worktreeManager = await _parallelBatchDeps.createWorktreeManager();
74834
75111
  const worktreePaths = new Map;
74835
75112
  const storyStartTimes = new Map;
75113
+ const preExecutionFailures = [];
75114
+ const preExecutionFailureEndTimes = new Map;
74836
75115
  for (const story of stories) {
74837
75116
  storyStartTimes.set(story.id, Date.now());
74838
75117
  try {
@@ -74842,7 +75121,18 @@ async function runParallelBatch(options) {
74842
75121
  storyId: story.id,
74843
75122
  error: error48 instanceof Error ? error48.message : String(error48)
74844
75123
  });
74845
- throw error48;
75124
+ preExecutionFailures.push({
75125
+ story,
75126
+ pipelineResult: {
75127
+ success: false,
75128
+ finalAction: "fail",
75129
+ reason: error48 instanceof Error ? error48.message : String(error48),
75130
+ stoppedAtStage: "worktree-create",
75131
+ context: { ...pipelineContext, story, stories: [story], workdir }
75132
+ }
75133
+ });
75134
+ preExecutionFailureEndTimes.set(story.id, Date.now());
75135
+ continue;
74846
75136
  }
74847
75137
  worktreePaths.set(story.id, path29.join(workdir, ".nax-wt", story.id));
74848
75138
  }
@@ -74872,7 +75162,6 @@ async function runParallelBatch(options) {
74872
75162
  }
74873
75163
  const dependencyContexts = new Map;
74874
75164
  const readyStories = [];
74875
- const preExecutionFailures = [];
74876
75165
  for (const story of stories) {
74877
75166
  const worktreeRoot = worktreePaths.get(story.id);
74878
75167
  if (!worktreeRoot)
@@ -74967,7 +75256,7 @@ async function runParallelBatch(options) {
74967
75256
  storyEndTimes.set(story.id, batchEndMs);
74968
75257
  }
74969
75258
  for (const { story } of failed) {
74970
- storyEndTimes.set(story.id, batchEndMs);
75259
+ storyEndTimes.set(story.id, preExecutionFailureEndTimes.get(story.id) ?? batchEndMs);
74971
75260
  }
74972
75261
  const mergeConflicts = [];
74973
75262
  for (const conflict of workerResult.mergeConflicts) {
@@ -75238,7 +75527,7 @@ async function executeUnified(ctx, initialPrd) {
75238
75527
  });
75239
75528
  for (const { story, pipelineResult } of batchResult.failed) {
75240
75529
  const storyRouting = prd.userStories.find((s) => s.id === story.id)?.routing;
75241
- await handlePipelineFailure({
75530
+ const failureResult = await handlePipelineFailure({
75242
75531
  config: ctx.config,
75243
75532
  prd,
75244
75533
  prdPath: ctx.prdPath,
@@ -75267,12 +75556,13 @@ async function executeUnified(ctx, initialPrd) {
75267
75556
  runtime: ctx.runtime,
75268
75557
  abortSignal: ctx.abortSignal
75269
75558
  }, pipelineResult);
75559
+ prd = failureResult.prd;
75270
75560
  }
75271
75561
  reconcileBatchOutcome(prd, batchResult);
75272
75562
  await savePRD(prd, ctx.prdPath);
75273
75563
  await pipelineEventBus.drain();
75274
75564
  totalCost2 += batchResult.totalCost;
75275
- storiesCompleted += batchResult.completed.length;
75565
+ storiesCompleted += batchResult.completed.length + batchResult.mergeConflicts.filter((c) => c.rectified).length;
75276
75566
  prdDirty = true;
75277
75567
  if (ctx.sessionManager) {
75278
75568
  for (const story of batchResult.completed) {
@@ -75695,7 +75985,7 @@ var init_runner_execution = __esm(() => {
75695
75985
 
75696
75986
  // src/execution/status-file.ts
75697
75987
  import { rename as rename3, unlink as unlink4 } from "fs/promises";
75698
- import { resolve as resolve22 } from "path";
75988
+ import { resolve as resolve23 } from "path";
75699
75989
  function countProgress(prd) {
75700
75990
  const stories = prd.userStories;
75701
75991
  const passed = stories.filter((s) => s.status === "passed").length;
@@ -75740,7 +76030,7 @@ function buildStatusSnapshot(state) {
75740
76030
  return snapshot;
75741
76031
  }
75742
76032
  async function writeStatusFile(filePath, status) {
75743
- const resolvedPath = resolve22(filePath);
76033
+ const resolvedPath = resolve23(filePath);
75744
76034
  if (filePath.includes("../") || filePath.includes("..\\")) {
75745
76035
  throw new Error("Invalid status file path: path traversal detected");
75746
76036
  }
@@ -77052,8 +77342,10 @@ async function run(options) {
77052
77342
  const runId = `run-${new Date().toISOString().replace(/[:.]/g, "-")}`;
77053
77343
  const origLoadCheckpoints = _storyOrchestratorDeps.loadCheckpoints;
77054
77344
  const origRecordGreen = _storyOrchestratorDeps.recordGreen;
77055
- applyResumeModeDeps(featureDir ?? "", resumeMode);
77056
- applyRecordGreenDeps(featureDir ?? "", runId);
77345
+ if (featureDir) {
77346
+ applyResumeModeDeps(featureDir, resumeMode);
77347
+ applyRecordGreenDeps(featureDir, runId);
77348
+ }
77057
77349
  let iterations = 0;
77058
77350
  let storiesCompleted = 0;
77059
77351
  let totalCost2 = 0;
@@ -77740,14 +78032,14 @@ See https://react.dev/link/invalid-hook-call for tips about how to debug and fix
77740
78032
  prevActScopeDepth !== actScopeDepth - 1 && console.error("You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one. ");
77741
78033
  actScopeDepth = prevActScopeDepth;
77742
78034
  }
77743
- function recursivelyFlushAsyncActWork(returnValue, resolve23, reject) {
78035
+ function recursivelyFlushAsyncActWork(returnValue, resolve24, reject) {
77744
78036
  var queue = ReactSharedInternals.actQueue;
77745
78037
  if (queue !== null)
77746
78038
  if (queue.length !== 0)
77747
78039
  try {
77748
78040
  flushActQueue(queue);
77749
78041
  enqueueTask(function() {
77750
- return recursivelyFlushAsyncActWork(returnValue, resolve23, reject);
78042
+ return recursivelyFlushAsyncActWork(returnValue, resolve24, reject);
77751
78043
  });
77752
78044
  return;
77753
78045
  } catch (error48) {
@@ -77755,7 +78047,7 @@ See https://react.dev/link/invalid-hook-call for tips about how to debug and fix
77755
78047
  }
77756
78048
  else
77757
78049
  ReactSharedInternals.actQueue = null;
77758
- 0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) : resolve23(returnValue);
78050
+ 0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) : resolve24(returnValue);
77759
78051
  }
77760
78052
  function flushActQueue(queue) {
77761
78053
  if (!isFlushing) {
@@ -77931,14 +78223,14 @@ See https://react.dev/link/invalid-hook-call for tips about how to debug and fix
77931
78223
  didAwaitActCall || didWarnNoAwaitAct || (didWarnNoAwaitAct = true, console.error("You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"));
77932
78224
  });
77933
78225
  return {
77934
- then: function(resolve23, reject) {
78226
+ then: function(resolve24, reject) {
77935
78227
  didAwaitActCall = true;
77936
78228
  thenable.then(function(returnValue) {
77937
78229
  popActScope(prevActQueue, prevActScopeDepth);
77938
78230
  if (prevActScopeDepth === 0) {
77939
78231
  try {
77940
78232
  flushActQueue(queue), enqueueTask(function() {
77941
- return recursivelyFlushAsyncActWork(returnValue, resolve23, reject);
78233
+ return recursivelyFlushAsyncActWork(returnValue, resolve24, reject);
77942
78234
  });
77943
78235
  } catch (error$0) {
77944
78236
  ReactSharedInternals.thrownErrors.push(error$0);
@@ -77949,7 +78241,7 @@ See https://react.dev/link/invalid-hook-call for tips about how to debug and fix
77949
78241
  reject(_thrownError);
77950
78242
  }
77951
78243
  } else
77952
- resolve23(returnValue);
78244
+ resolve24(returnValue);
77953
78245
  }, function(error48) {
77954
78246
  popActScope(prevActQueue, prevActScopeDepth);
77955
78247
  0 < ReactSharedInternals.thrownErrors.length ? (error48 = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(error48)) : reject(error48);
@@ -77965,11 +78257,11 @@ See https://react.dev/link/invalid-hook-call for tips about how to debug and fix
77965
78257
  if (0 < ReactSharedInternals.thrownErrors.length)
77966
78258
  throw callback = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, callback;
77967
78259
  return {
77968
- then: function(resolve23, reject) {
78260
+ then: function(resolve24, reject) {
77969
78261
  didAwaitActCall = true;
77970
78262
  prevActScopeDepth === 0 ? (ReactSharedInternals.actQueue = queue, enqueueTask(function() {
77971
- return recursivelyFlushAsyncActWork(returnValue$jscomp$0, resolve23, reject);
77972
- })) : resolve23(returnValue$jscomp$0);
78263
+ return recursivelyFlushAsyncActWork(returnValue$jscomp$0, resolve24, reject);
78264
+ })) : resolve24(returnValue$jscomp$0);
77973
78265
  }
77974
78266
  };
77975
78267
  };
@@ -80811,8 +81103,8 @@ It can also happen if the client has a browser extension installed which messes
80811
81103
  currentEntangledActionThenable = {
80812
81104
  status: "pending",
80813
81105
  value: undefined,
80814
- then: function(resolve23) {
80815
- entangledListeners.push(resolve23);
81106
+ then: function(resolve24) {
81107
+ entangledListeners.push(resolve24);
80816
81108
  }
80817
81109
  };
80818
81110
  }
@@ -80836,8 +81128,8 @@ It can also happen if the client has a browser extension installed which messes
80836
81128
  status: "pending",
80837
81129
  value: null,
80838
81130
  reason: null,
80839
- then: function(resolve23) {
80840
- listeners.push(resolve23);
81131
+ then: function(resolve24) {
81132
+ listeners.push(resolve24);
80841
81133
  }
80842
81134
  };
80843
81135
  thenable.then(function() {
@@ -107009,7 +107301,7 @@ __export(exports_curator, {
107009
107301
  });
107010
107302
  import { readdirSync as readdirSync9 } from "fs";
107011
107303
  import { unlink as unlink6 } from "fs/promises";
107012
- import { join as join104, resolve as resolve23, sep as sep10 } from "path";
107304
+ import { join as join104, resolve as resolve24, sep as sep10 } from "path";
107013
107305
  function listRunIds(runsDir) {
107014
107306
  try {
107015
107307
  return readdirSync9(runsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
@@ -107127,15 +107419,15 @@ async function curatorStatus(options) {
107127
107419
  }
107128
107420
  }
107129
107421
  function resolveCanonicalTargetPath(projectDir, canonicalFile) {
107130
- const target = resolve23(projectDir, canonicalFile);
107131
- const root = resolve23(projectDir);
107422
+ const target = resolve24(projectDir, canonicalFile);
107423
+ const root = resolve24(projectDir);
107132
107424
  if (target !== root && !target.startsWith(root + sep10))
107133
107425
  return null;
107134
107426
  const relative18 = target.slice(root.length + 1).replaceAll(sep10, "/");
107135
107427
  return CURATOR_TARGET_SHAPES.some((shape) => shape.test(relative18)) ? target : null;
107136
107428
  }
107137
107429
  function isWithinCanonicalRulesDir(projectDir, targetPath) {
107138
- const rulesRoot = resolve23(projectDir, CANONICAL_RULES_DIR);
107430
+ const rulesRoot = resolve24(projectDir, CANONICAL_RULES_DIR);
107139
107431
  return targetPath === rulesRoot || targetPath.startsWith(rulesRoot + sep10);
107140
107432
  }
107141
107433
  async function curatorCommit(options) {
@@ -108076,6 +108368,12 @@ async function parseRunLog(logPath) {
108076
108368
  return [];
108077
108369
  }
108078
108370
  }
108371
+ function findRunStart(entries) {
108372
+ return entries.find((e) => e.stage === "run.start");
108373
+ }
108374
+ function findRunSummary(entries) {
108375
+ return entries.find((e) => e.stage === "run.complete" && e.data?.totalStories !== undefined);
108376
+ }
108079
108377
  async function runsListCommand(options) {
108080
108378
  const logger = getLogger();
108081
108379
  const { feature, workdir } = options;
@@ -108094,8 +108392,8 @@ async function runsListCommand(options) {
108094
108392
  for (const file3 of files.sort().reverse()) {
108095
108393
  const logPath = join48(runsDir, file3);
108096
108394
  const entries = await parseRunLog(logPath);
108097
- const startEvent = entries.find((e) => e.message === "run.start");
108098
- const completeEvent = entries.find((e) => e.message === "run.complete");
108395
+ const startEvent = findRunStart(entries);
108396
+ const completeEvent = findRunSummary(entries);
108099
108397
  if (!startEvent) {
108100
108398
  logger.warn("cli", "Run log missing run.start event", { file: file3 });
108101
108399
  continue;
@@ -108125,8 +108423,8 @@ async function runsShowCommand(options) {
108125
108423
  throw new NaxError("Run not found", "RUN_NOT_FOUND", { runId, feature, logPath });
108126
108424
  }
108127
108425
  const entries = await parseRunLog(logPath);
108128
- const startEvent = entries.find((e) => e.message === "run.start");
108129
- const completeEvent = entries.find((e) => e.message === "run.complete");
108426
+ const startEvent = findRunStart(entries);
108427
+ const completeEvent = findRunSummary(entries);
108130
108428
  const storyEvents = entries.filter((e) => e.stage === "execution" && e.data?.storyId);
108131
108429
  if (!startEvent) {
108132
108430
  logger.error("cli", "Run log missing run.start event", { runId });
@@ -108966,6 +109264,7 @@ async function profileUseCommand(profileName, startDir) {
108966
109264
  await Bun.write(configPath, JSON.stringify(rest, null, 2));
108967
109265
  return "Profile reset to default.";
108968
109266
  }
109267
+ await loadProfile(profileName, startDir);
108969
109268
  const updated = { ...existing, profile: profileName };
108970
109269
  await Bun.write(configPath, JSON.stringify(updated, null, 2));
108971
109270
  return `Now using profile: ${profileName}`;
@@ -109098,7 +109397,7 @@ init_canonical_loader();
109098
109397
  init_errors();
109099
109398
  init_logger2();
109100
109399
  import { mkdir as mkdir11 } from "fs/promises";
109101
- import { join as join76, resolve as resolve20, sep as sep8 } from "path";
109400
+ import { join as join76, resolve as resolve21, sep as sep8 } from "path";
109102
109401
 
109103
109402
  // src/cli/rules-lint.ts
109104
109403
  init_engine();
@@ -109250,13 +109549,13 @@ import { basename as basename15, join as join75 } from "path";
109250
109549
 
109251
109550
  // src/cli/rules-migrate-plan.ts
109252
109551
  init_errors();
109253
- import { resolve as resolve19, sep as sep7 } from "path";
109552
+ import { resolve as resolve20, sep as sep7 } from "path";
109254
109553
  async function planMigration(sources, options) {
109255
109554
  const writes = [];
109256
109555
  const skips = [];
109257
- const resolvedTargetDir = resolve19(options.targetDir);
109556
+ const resolvedTargetDir = resolve20(options.targetDir);
109258
109557
  for (const source of sources) {
109259
- const resolvedTargetPath = resolve19(source.targetPath);
109558
+ const resolvedTargetPath = resolve20(source.targetPath);
109260
109559
  if (!resolvedTargetPath.startsWith(`${resolvedTargetDir}${sep7}`) && resolvedTargetPath !== resolvedTargetDir) {
109261
109560
  throw new NaxError(`Migration target escapes ${options.targetDir}: ${source.targetFileName} -> ${source.targetPath}`, "RULES_MIGRATE_TARGET_ESCAPE", { stage: "rules-migrate-plan", targetDir: options.targetDir, entry: source.targetPath });
109262
109561
  }
@@ -109469,8 +109768,8 @@ async function exportRuleDirectory(input) {
109469
109768
  const drifted = [];
109470
109769
  for (const rule of rules) {
109471
109770
  const rel = rule.path ?? rule.fileName;
109472
- const target = resolve20(workdir, ruleDir, rel);
109473
- if (!target.startsWith(`${resolve20(workdir, ruleDir)}${sep8}`)) {
109771
+ const target = resolve21(workdir, ruleDir, rel);
109772
+ if (!target.startsWith(`${resolve21(workdir, ruleDir)}${sep8}`)) {
109474
109773
  throw new NaxError(`Rule path escapes ${ruleDir}: ${rel}`, "RULES_EXPORT_PATH_ESCAPE", {
109475
109774
  stage: "rules-export",
109476
109775
  rule: rel
@@ -109490,9 +109789,9 @@ async function exportRuleDirectory(input) {
109490
109789
  }
109491
109790
  await _rulesCLIDeps.writeFile(target, content);
109492
109791
  }
109493
- const expected = new Set(rules.map((r) => resolve20(workdir, ruleDir, r.path ?? r.fileName)));
109792
+ const expected = new Set(rules.map((r) => resolve21(workdir, ruleDir, r.path ?? r.fileName)));
109494
109793
  for (const existing of _rulesCLIDeps.globInDir(join76(workdir, ruleDir))) {
109495
- if (!expected.has(resolve20(existing))) {
109794
+ if (!expected.has(resolve21(existing))) {
109496
109795
  _rulesCLIDeps.getLogger().warn("rules-export", "Generated rules dir contains a file with no canonical source", {
109497
109796
  file: existing,
109498
109797
  hint: `Delete it, or add the rule to ${CANONICAL_RULES_DIR}/`
@@ -110166,17 +110465,29 @@ async function resolveRunFileFromRegistry(runId) {
110166
110465
  } catch {
110167
110466
  throw new Error(`Run not found in registry: ${runId}`);
110168
110467
  }
110169
- let matched = null;
110468
+ let exactMatch = null;
110469
+ const prefixMatches = [];
110170
110470
  for (const entry of entries) {
110171
110471
  const metaPath = join81(runsDir, entry, "meta.json");
110172
110472
  try {
110173
110473
  const meta3 = await Bun.file(metaPath).json();
110174
- if (meta3.runId === runId || meta3.runId.startsWith(runId)) {
110175
- matched = meta3;
110474
+ if (meta3.runId === runId) {
110475
+ exactMatch = meta3;
110176
110476
  break;
110177
110477
  }
110478
+ if (meta3.runId.startsWith(runId)) {
110479
+ prefixMatches.push(meta3);
110480
+ }
110178
110481
  } catch {}
110179
110482
  }
110483
+ let matched = exactMatch;
110484
+ if (!matched) {
110485
+ if (prefixMatches.length > 1) {
110486
+ const candidates = prefixMatches.map((m) => m.runId).join(", ");
110487
+ throw new Error(`Ambiguous run ID "${runId}" matches multiple runs: ${candidates}`);
110488
+ }
110489
+ matched = prefixMatches[0] ?? null;
110490
+ }
110180
110491
  if (!matched) {
110181
110492
  throw new Error(`Run not found in registry: ${runId}`);
110182
110493
  }
@@ -110398,13 +110709,7 @@ async function logsCommand(options) {
110398
110709
  }
110399
110710
  const resolved = resolveProject2({ dir: options.dir });
110400
110711
  const naxDir = join83(resolved.projectDir, ".nax");
110401
- const configPath = resolved.configPath;
110402
- const configFile = Bun.file(configPath);
110403
- const config2 = await configFile.json();
110404
- const featureName = config2.feature;
110405
- if (!featureName) {
110406
- throw new Error("No feature specified in config.json");
110407
- }
110712
+ const featureName = resolveSingleFeature(naxDir, "pass -r <runId> (see `nax runs list`)");
110408
110713
  const featureDir = join83(naxDir, "features", featureName);
110409
110714
  const runsDir = join83(featureDir, "runs");
110410
110715
  if (!existsSync30(runsDir)) {
@@ -110444,17 +110749,16 @@ async function precheckCommand(options) {
110444
110749
  const result2 = await runEnvironmentPrecheck(config3, resolved.projectDir, { format });
110445
110750
  process.exit(result2.passed ? EXIT_CODES.SUCCESS : EXIT_CODES.BLOCKER);
110446
110751
  }
110752
+ const naxDir = join84(resolved.projectDir, ".nax");
110447
110753
  let featureName = options.feature;
110448
110754
  if (!featureName) {
110449
- const configFile = Bun.file(resolved.configPath);
110450
- const config3 = await configFile.json();
110451
- featureName = config3.feature;
110452
- if (!featureName) {
110453
- console.error(source_default.red("No feature specified. Use -f flag or set feature in config.json"));
110755
+ try {
110756
+ featureName = resolveSingleFeature(naxDir);
110757
+ } catch (err) {
110758
+ console.error(source_default.red(err instanceof Error ? err.message : String(err)));
110454
110759
  process.exit(1);
110455
110760
  }
110456
110761
  }
110457
- const naxDir = join84(resolved.projectDir, ".nax");
110458
110762
  const featureDir = join84(naxDir, "features", featureName);
110459
110763
  const prdPath = join84(featureDir, "prd.json");
110460
110764
  if (!existsSync31(featureDir)) {
@@ -110478,7 +110782,7 @@ async function precheckCommand(options) {
110478
110782
  // src/commands/replay.ts
110479
110783
  init_errors();
110480
110784
  import { existsSync as existsSync32 } from "fs";
110481
- import { dirname as dirname14 } from "path";
110785
+ import { dirname as dirname15 } from "path";
110482
110786
 
110483
110787
  // src/replay/discovery.ts
110484
110788
  init_errors();
@@ -110791,7 +111095,7 @@ async function readJsonOrUndefined(path25) {
110791
111095
  }
110792
111096
  async function readMetricsFromProject(meta3) {
110793
111097
  const { loadRunMetrics: loadRunMetrics2 } = await Promise.resolve().then(() => (init_tracker(), exports_tracker));
110794
- const outputDir = dirname14(dirname14(dirname14(meta3.eventsDir)));
111098
+ const outputDir = dirname15(dirname15(dirname15(meta3.eventsDir)));
110795
111099
  const all = await loadRunMetrics2(outputDir);
110796
111100
  return all.find((m) => m.runId === meta3.runId);
110797
111101
  }
@@ -110924,10 +111228,11 @@ function registerResumeCommand(program2) {
110924
111228
  const { findProjectDir: findProjectDir2 } = await Promise.resolve().then(() => (init_config(), exports_config));
110925
111229
  const { run: run2 } = await Promise.resolve().then(() => (init_execution2(), exports_execution));
110926
111230
  const { applyResumeModeDeps: applyResumeModeDeps2 } = await Promise.resolve().then(() => (init_checkpoint(), exports_checkpoint));
110927
- const { existsSync: existsSync38 } = await import("fs");
111231
+ const { existsSync: existsSync38, mkdirSync: mkdirSync8 } = await import("fs");
110928
111232
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
110929
111233
  const { loadPRD: loadPRD2 } = await Promise.resolve().then(() => (init_prd(), exports_prd));
110930
111234
  const { loadHooksConfig: loadHooksConfig2 } = await Promise.resolve().then(() => (init_hooks(), exports_hooks));
111235
+ const { initLogger: initLogger2 } = await Promise.resolve().then(() => (init_logger2(), exports_logger));
110931
111236
  const naxDir = findProjectDir2(cmdOpts.dir);
110932
111237
  if (!naxDir) {
110933
111238
  process.stderr.write(`nax not initialized. Run: nax init
@@ -110952,6 +111257,11 @@ function registerResumeCommand(program2) {
110952
111257
  const projectKey = config2.name?.trim() || basename19(cmdOpts.dir);
110953
111258
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
110954
111259
  const statusFilePath = join99(outputDir, "status.json");
111260
+ const runsDir = join99(outputDir, "features", feature, "runs");
111261
+ mkdirSync8(runsDir, { recursive: true });
111262
+ const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
111263
+ const logFilePath = join99(runsDir, `${runId}.jsonl`);
111264
+ initLogger2({ level: "info", filePath: logFilePath, useChalk: true, headless: true, suppressConsole: false });
110955
111265
  const result = await run2({
110956
111266
  prdPath,
110957
111267
  workdir: cmdOpts.dir,
@@ -110962,7 +111272,7 @@ function registerResumeCommand(program2) {
110962
111272
  dryRun: false,
110963
111273
  useBatch: true,
110964
111274
  statusFile: statusFilePath,
110965
- logFilePath: undefined,
111275
+ logFilePath,
110966
111276
  formatterMode: "normal",
110967
111277
  headless: true,
110968
111278
  skipPrecheck: false,
@@ -116785,8 +117095,8 @@ class Ink {
116785
117095
  }
116786
117096
  }
116787
117097
  async waitUntilExit() {
116788
- this.exitPromise ||= new Promise((resolve23, reject2) => {
116789
- this.resolveExitPromise = resolve23;
117098
+ this.exitPromise ||= new Promise((resolve24, reject2) => {
117099
+ this.resolveExitPromise = resolve24;
116790
117100
  this.rejectExitPromise = reject2;
116791
117101
  });
116792
117102
  if (!this.beforeExitHandler) {
@@ -118571,15 +118881,17 @@ function usePipelineBusEvents(initialStories) {
118571
118881
  });
118572
118882
  const unsubCompleted = pipelineEventBus.on("story:completed", (event) => {
118573
118883
  setState((prev) => {
118884
+ const prevStoryCost = prev.stories.find((s) => s.story.id === event.storyId)?.cost ?? 0;
118885
+ const costDelta = event.cost ?? 0;
118886
+ const storyCost = prevStoryCost + costDelta;
118887
+ const totalCost2 = prev.totalCost + costDelta;
118574
118888
  const newStories = prev.stories.map((s) => {
118575
118889
  if (s.story.id === event.storyId) {
118576
118890
  const status = event.passed ? "passed" : "failed";
118577
- const storyCost = event.cost ?? s.cost;
118578
118891
  return { ...s, status, cost: storyCost };
118579
118892
  }
118580
118893
  return s;
118581
118894
  });
118582
- const totalCost2 = newStories.reduce((sum2, s) => sum2 + (s.cost ?? 0), 0);
118583
118895
  const { [event.storyId]: _removed, ...remainingSteps } = prev.storySteps;
118584
118896
  const lastFailedStoryId = event.passed ? prev.lastFailedStoryId : event.storyId;
118585
118897
  return { ...prev, stories: newStories, totalCost: totalCost2, storySteps: remainingSteps, lastFailedStoryId };
@@ -118814,7 +119126,7 @@ function App2({ feature, version: version2, stories: initialStories, events, que
118814
119126
  break;
118815
119127
  }
118816
119128
  };
118817
- use_input_default((input) => {
119129
+ use_input_default((input, key) => {
118818
119130
  if (showQuitConfirm || showAbortConfirm) {
118819
119131
  const inputKey = input.toLowerCase();
118820
119132
  if (inputKey === "y") {
@@ -118824,7 +119136,7 @@ function App2({ feature, version: version2, stories: initialStories, events, que
118824
119136
  writeQueueCommand(queueFilePath, { type: "ABORT" });
118825
119137
  setShowAbortConfirm(false);
118826
119138
  }
118827
- } else if (inputKey === "n" || input === "\x1B") {
119139
+ } else if (inputKey === "n" || key.escape) {
118828
119140
  setShowQuitConfirm(false);
118829
119141
  setShowAbortConfirm(false);
118830
119142
  }
@@ -119051,7 +119363,7 @@ async function promptForConfirmation(question) {
119051
119363
  if (!process.stdin.isTTY) {
119052
119364
  return true;
119053
119365
  }
119054
- return new Promise((resolve24) => {
119366
+ return new Promise((resolve25) => {
119055
119367
  process.stdout.write(source_default.bold(`${question} [Y/n] `));
119056
119368
  process.stdin.setRawMode(true);
119057
119369
  process.stdin.resume();
@@ -119060,13 +119372,17 @@ async function promptForConfirmation(question) {
119060
119372
  process.stdin.setRawMode(false);
119061
119373
  process.stdin.pause();
119062
119374
  process.stdin.removeListener("data", handler);
119063
- const answer = char.toLowerCase();
119064
119375
  process.stdout.write(`
119065
119376
  `);
119377
+ if (char === "\x03") {
119378
+ resolve25(false);
119379
+ process.exit(130);
119380
+ }
119381
+ const answer = char.toLowerCase();
119066
119382
  if (answer === "n") {
119067
- resolve24(false);
119383
+ resolve25(false);
119068
119384
  } else {
119069
- resolve24(true);
119385
+ resolve25(true);
119070
119386
  }
119071
119387
  };
119072
119388
  process.stdin.on("data", handler);
@@ -119245,6 +119561,12 @@ program2.command("setup").description("Analyze repo and generate .nax/config.jso
119245
119561
  process.exit(exitCode);
119246
119562
  });
119247
119563
  program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--max-cost <usd>", "Override cost limit (USD) for this run \u2014 aborts execution when exceeded").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false).option("--force", "Force overwrite existing prd.json when using --plan", false).option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).option("--profile <name>", "Profile(s) to overlay (comma-separated or repeated; later overrides earlier)", collectProfile, []).option("--schedule <when>", "Defer run start until <when> (e.g. 30m, 1h30m, 17:00, 2026-07-02T02:00)").option("--compare <agents>", "Bake-off mode: comma-separated list of contestant agents (e.g. claude,codex)").option("--fresh", "Ignore any existing checkpoint.jsonl and re-run every incomplete story from scratch", false).addOption(new Option("--no-resume", "Alias for --fresh: never auto-resume from a prior checkpoint").default("__UNSET__")).action(async (options) => {
119564
+ try {
119565
+ validateFeatureName(options.feature);
119566
+ } catch (err) {
119567
+ console.error(source_default.red(`Invalid feature name: ${err.message}`));
119568
+ process.exit(1);
119569
+ }
119248
119570
  let workdir;
119249
119571
  try {
119250
119572
  workdir = validateDirectory(options.dir);
@@ -119338,6 +119660,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
119338
119660
  console.error(source_default.red("nax not initialized. Run: nax init"));
119339
119661
  process.exit(1);
119340
119662
  }
119663
+ const projectRoot = join105(naxDir, "..");
119341
119664
  const featureDir = join105(naxDir, "features", options.feature);
119342
119665
  const prdPath = join105(featureDir, "prd.json");
119343
119666
  if (options.plan && options.from) {
@@ -119368,7 +119691,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
119368
119691
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
119369
119692
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
119370
119693
  console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
119371
- const planResult = await planCommand(workdir, config2, {
119694
+ const planResult = await planCommand(projectRoot, config2, {
119372
119695
  from: options.from,
119373
119696
  feature: options.feature,
119374
119697
  auto: options.oneShot ?? false,
@@ -119432,7 +119755,12 @@ program2.command("run").description("Run the orchestration loop for a feature").
119432
119755
  config2.agent ??= {};
119433
119756
  config2.agent.default = options.agent;
119434
119757
  }
119435
- config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
119758
+ const maxIterations = Number.parseInt(options.maxIterations, 10);
119759
+ if (!Number.isFinite(maxIterations) || maxIterations < 1) {
119760
+ console.error(source_default.red("--max-iterations must be a positive integer"));
119761
+ process.exit(1);
119762
+ }
119763
+ config2.execution.maxIterations = maxIterations;
119436
119764
  if (options.maxCost !== undefined) {
119437
119765
  const maxCost = Number(options.maxCost);
119438
119766
  if (!Number.isFinite(maxCost) || maxCost <= 0) {
@@ -119509,25 +119837,32 @@ Scheduled run cancelled.`));
119509
119837
  const exitOutcome = typeof bakeoffResult?.outcome === "number" ? bakeoffResult.outcome : 0;
119510
119838
  process.exit(exitOutcome);
119511
119839
  }
119512
- const result2 = await run({
119513
- prdPath,
119514
- workdir,
119515
- config: config2,
119516
- hooks,
119517
- feature: options.feature,
119518
- featureDir,
119519
- dryRun: options.dryRun,
119520
- useBatch: options.batch ?? true,
119521
- parallel,
119522
- eventEmitter,
119523
- statusFile: statusFilePath,
119524
- logFilePath,
119525
- formatterMode: useHeadless ? formatterMode : undefined,
119526
- headless: useHeadless,
119527
- skipPrecheck: options.skipPrecheck ?? false,
119528
- agentStreamEvents,
119529
- resumeMode: options.fresh === true || options.resume === false ? "fresh" : "auto"
119530
- });
119840
+ let result2;
119841
+ try {
119842
+ result2 = await run({
119843
+ prdPath,
119844
+ workdir,
119845
+ config: config2,
119846
+ hooks,
119847
+ feature: options.feature,
119848
+ featureDir,
119849
+ dryRun: options.dryRun,
119850
+ useBatch: options.batch ?? true,
119851
+ parallel,
119852
+ eventEmitter,
119853
+ statusFile: statusFilePath,
119854
+ logFilePath,
119855
+ formatterMode: useHeadless ? formatterMode : undefined,
119856
+ headless: useHeadless,
119857
+ skipPrecheck: options.skipPrecheck ?? false,
119858
+ agentStreamEvents,
119859
+ resumeMode: options.fresh === true || options.resume === false ? "fresh" : "auto"
119860
+ });
119861
+ } finally {
119862
+ if (tuiInstance) {
119863
+ tuiInstance.unmount();
119864
+ }
119865
+ }
119531
119866
  const latestSymlink = join105(runsDir, "latest.jsonl");
119532
119867
  try {
119533
119868
  if (existsSync39(latestSymlink)) {
@@ -119539,9 +119874,6 @@ Scheduled run cancelled.`));
119539
119874
  } catch (error48) {
119540
119875
  console.error(source_default.yellow(`Warning: Failed to create latest.jsonl symlink: ${error48}`));
119541
119876
  }
119542
- if (tuiInstance) {
119543
- tuiInstance.unmount();
119544
- }
119545
119877
  if (useHeadless) {
119546
119878
  console.log(source_default.dim(`
119547
119879
  \u2500\u2500 Summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
@@ -119613,6 +119945,12 @@ function printHumanReadable(result2) {
119613
119945
  }
119614
119946
  var features = program2.command("features").description("Manage features");
119615
119947
  features.command("create <name>").description("Create a new feature").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (name, options) => {
119948
+ try {
119949
+ validateFeatureName(name);
119950
+ } catch (err) {
119951
+ console.error(source_default.red(`Invalid feature name: ${err.message}`));
119952
+ process.exit(1);
119953
+ }
119616
119954
  let workdir;
119617
119955
  try {
119618
119956
  workdir = validateDirectory(options.dir);
@@ -119768,12 +120106,13 @@ Use: nax plan -f <feature> --from <spec>`));
119768
120106
  console.error(source_default.red("nax not initialized. Run: nax init"));
119769
120107
  process.exit(1);
119770
120108
  }
120109
+ const projectRoot = join105(naxDir, "..");
119771
120110
  const cliOverrides = {};
119772
120111
  const cliProfiles = options.profile ?? [];
119773
120112
  if (cliProfiles.length > 0) {
119774
120113
  cliOverrides.profile = cliProfiles;
119775
120114
  }
119776
- const config2 = await loadConfig(workdir, cliOverrides);
120115
+ const config2 = await loadConfig(projectRoot, cliOverrides);
119777
120116
  const featureLogDir = join105(naxDir, "features", options.feature, "plan");
119778
120117
  mkdirSync8(featureLogDir, { recursive: true });
119779
120118
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
@@ -119782,7 +120121,7 @@ Use: nax plan -f <feature> --from <spec>`));
119782
120121
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
119783
120122
  try {
119784
120123
  if (options.decompose) {
119785
- await planDecomposeCommand(workdir, config2, {
120124
+ await planDecomposeCommand(projectRoot, config2, {
119786
120125
  feature: options.feature,
119787
120126
  storyId: options.decompose
119788
120127
  });
@@ -119794,7 +120133,7 @@ Use: nax plan -f <feature> --from <spec>`));
119794
120133
  console.error(source_default.red("Error: --from <spec-path> is required unless --decompose is used"));
119795
120134
  process.exit(1);
119796
120135
  }
119797
- const planResult = await planCommand(workdir, config2, {
120136
+ const planResult = await planCommand(projectRoot, config2, {
119798
120137
  from: options.from,
119799
120138
  feature: options.feature,
119800
120139
  auto: options.auto || options.oneShot,
@@ -119990,6 +120329,7 @@ runs.command("list").description("List all runs for a feature").requiredOption("
119990
120329
  console.error(source_default.red(`Invalid directory: ${err.message}`));
119991
120330
  process.exit(1);
119992
120331
  }
120332
+ initLogger({ level: "info", useChalk: true });
119993
120333
  await runsListCommand({ feature: options.feature, workdir });
119994
120334
  });
119995
120335
  runs.command("show <run-id>").description("Show detailed information for a specific run").requiredOption("-f, --feature <name>", "Feature name").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (runId, options) => {
@@ -120000,6 +120340,7 @@ runs.command("show <run-id>").description("Show detailed information for a speci
120000
120340
  console.error(source_default.red(`Invalid directory: ${err.message}`));
120001
120341
  process.exit(1);
120002
120342
  }
120343
+ initLogger({ level: "info", useChalk: true });
120003
120344
  await runsShowCommand({ runId, feature: options.feature, workdir });
120004
120345
  });
120005
120346
  registerReplayCommand(program2);