@nathapp/nax 0.80.0-canary.4 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/nax.js +805 -460
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -18428,19 +18428,23 @@ function redactValue(input, seen = new WeakSet, depth = 0) {
18428
18428
  }
18429
18429
  var SECRET_KEY_PATTERN, SECRET_VALUE_PATTERNS, REDACTED = "[REDACTED]", MAX_REDACT_DEPTH = 100, CIRCULAR_REF_MARKER = "[Circular]";
18430
18430
  var init_redact = __esm(() => {
18431
- SECRET_KEY_PATTERN = /(SECRET|TOKEN(?!s\b)|API_?KEY|PASSWORD|PRIVATE_?KEY|ACCESS_?KEY|WEBHOOK)/i;
18431
+ SECRET_KEY_PATTERN = /(SECRET|TOKEN(?!s\b)|API_?KEY|PASSWORD|PRIVATE_?KEY|ACCESS_?KEY|WEBHOOK|(?:\w+)?_URL|\w+_URI|\w+_DSN|CONNECTION\s*STRING)/i;
18432
18432
  SECRET_VALUE_PATTERNS = [
18433
18433
  /sk-[A-Za-z0-9_-]{16,}/g,
18434
18434
  /ghp_[A-Za-z0-9]{16,}/g,
18435
18435
  /gh[opsu]_[A-Za-z0-9]{16,}/g,
18436
+ /github_pat_[A-Za-z0-9_]{20,}/g,
18436
18437
  /npm_[A-Za-z0-9]{8,}/g,
18437
18438
  /AKIA[0-9A-Z]{16}/g,
18438
18439
  /xox[baprs]-[A-Za-z0-9-]{10,}/g,
18440
+ /\b\d{6,}:[A-Za-z0-9_-]{30,}\b/g,
18439
18441
  /(?:SECRET|TOKEN|API_?KEY|PASSWORD|PRIVATE_?KEY|ACCESS_?KEY|WEBHOOK)=[^\s"',]+/gi,
18440
18442
  /-----BEGIN [A-Z ]*(?:PRIVATE KEY|CERTIFICATE)(?: BLOCK)?-----[\s\S]{0,65536}?-----END [A-Z ]*(?:PRIVATE KEY|CERTIFICATE)(?: BLOCK)?-----/g,
18441
18443
  /eyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g,
18442
- /\b(?:Bearer|Basic)\s+(?=[A-Za-z0-9\-._~+/]*[0-9+/_-])[A-Za-z0-9\-._~+/]{8,}={0,2}/gi,
18443
- /(?:x-api-key|api[_-]?key)\s*[:=]\s*[^\s"',]+/gi
18444
+ /\bBearer\s+(?=[A-Za-z0-9\-._~+/]*[0-9+/_-])[A-Za-z0-9\-._~+/]{8,}={0,2}/gi,
18445
+ /\b[Bb][Aa][Ss][Ii][Cc]\s+(?=[A-Za-z0-9+/]*[A-Z])(?=[A-Za-z0-9+/]*[a-z])[A-Za-z0-9+/]{8,}={0,2}/g,
18446
+ /(?:x-api-key|api[_-]?key)\s*[:=]\s*[^\s"',]+/gi,
18447
+ /\b[a-z][a-z0-9+.-]*:\/\/(?:[^/\s@]*:[^/\s@]+)@/gi
18444
18448
  ];
18445
18449
  });
18446
18450
 
@@ -19122,23 +19126,66 @@ var init_config_guards = __esm(() => {
19122
19126
  });
19123
19127
 
19124
19128
  // src/config/dotenv.ts
19129
+ function parseQuotedValue(raw, quote) {
19130
+ let value = "";
19131
+ for (let i = 1;i < raw.length; i++) {
19132
+ const c = raw[i];
19133
+ if (quote === '"' && c === "\\" && i + 1 < raw.length) {
19134
+ const next = raw[i + 1];
19135
+ if (next === "n") {
19136
+ value += `
19137
+ `;
19138
+ i++;
19139
+ continue;
19140
+ }
19141
+ if (next === '"' || next === "\\") {
19142
+ value += next;
19143
+ i++;
19144
+ continue;
19145
+ }
19146
+ value += c;
19147
+ continue;
19148
+ }
19149
+ if (c === quote) {
19150
+ return value;
19151
+ }
19152
+ value += c;
19153
+ }
19154
+ return value;
19155
+ }
19156
+ function stripInlineComment(raw) {
19157
+ for (let i = 0;i < raw.length; i++) {
19158
+ if (raw[i] === "#" && (i === 0 || /\s/.test(raw[i - 1] ?? ""))) {
19159
+ return raw.slice(0, i);
19160
+ }
19161
+ }
19162
+ return raw;
19163
+ }
19125
19164
  function parseDotenv(content) {
19126
19165
  if (!content)
19127
19166
  return {};
19128
19167
  const result = {};
19129
19168
  for (const rawLine of content.split(`
19130
19169
  `)) {
19131
- const line = rawLine.trim();
19170
+ let line = rawLine.trim();
19132
19171
  if (!line || line.startsWith("#"))
19133
19172
  continue;
19134
- const stripped = line.startsWith("export ") ? line.slice(7).trim() : line;
19135
- const eqIndex = stripped.indexOf("=");
19173
+ if (line.startsWith("export ")) {
19174
+ line = line.slice(7).trim();
19175
+ if (line.length >= 2 && (line[0] === '"' || line[0] === "'") && line.endsWith(line[0])) {
19176
+ line = line.slice(1, -1);
19177
+ }
19178
+ }
19179
+ const eqIndex = line.indexOf("=");
19136
19180
  if (eqIndex === -1)
19137
19181
  continue;
19138
- const key = stripped.slice(0, eqIndex).trim();
19139
- let value = stripped.slice(eqIndex + 1).trim();
19140
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
19141
- value = value.slice(1, -1);
19182
+ const key = line.slice(0, eqIndex).trim();
19183
+ const rawValue = line.slice(eqIndex + 1).trim();
19184
+ let value;
19185
+ if (rawValue.startsWith('"') || rawValue.startsWith("'")) {
19186
+ value = parseQuotedValue(rawValue, rawValue[0]);
19187
+ } else {
19188
+ value = stripInlineComment(rawValue).trim();
19142
19189
  }
19143
19190
  result[key] = value;
19144
19191
  }
@@ -19161,12 +19208,13 @@ function resolveEnvVars(config2, env2, path = []) {
19161
19208
  return config2;
19162
19209
  }
19163
19210
  function resolveString(str, env2, path) {
19164
- 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) => {
19211
+ const resolveOne = (varName) => {
19165
19212
  if (!(varName in env2)) {
19166
19213
  throw new UnresolvedEnvVarError(varName, path);
19167
19214
  }
19168
19215
  return env2[varName];
19169
- }).replace(new RegExp(`${DOUBLE_DOLLAR_PLACEHOLDER}([A-Za-z_][A-Za-z0-9_]*)`, "g"), "$$$1");
19216
+ };
19217
+ return str.replace(/\$\$(\{[A-Za-z_][A-Za-z0-9_]*\}|[A-Za-z_][A-Za-z0-9_]*)/g, `${DOUBLE_DOLLAR_PLACEHOLDER}$1`).replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, varName) => resolveOne(varName)).replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, varName) => resolveOne(varName)).replace(new RegExp(`${DOUBLE_DOLLAR_PLACEHOLDER}(\\{[A-Za-z_][A-Za-z0-9_]*\\}|[A-Za-z_][A-Za-z0-9_]*)`, "g"), "$$$1");
19170
19218
  }
19171
19219
  var UnresolvedEnvVarError, DOUBLE_DOLLAR_PLACEHOLDER = "__DOLLAR_ESCAPE__";
19172
19220
  var init_dotenv = __esm(() => {
@@ -19526,6 +19574,14 @@ async function loadProfile(profileName, projectRoot) {
19526
19574
  }
19527
19575
  return base;
19528
19576
  }
19577
+ function sensitiveFilteredProcessEnv() {
19578
+ const filtered = {};
19579
+ for (const [key, value] of Object.entries(_profileDeps.env)) {
19580
+ if (value !== undefined && !SENSITIVE_ENV_KEY_PATTERN.test(key))
19581
+ filtered[key] = value;
19582
+ }
19583
+ return filtered;
19584
+ }
19529
19585
  async function loadProfileEnv(profileName, projectRoot) {
19530
19586
  validateProfileName(profileName);
19531
19587
  const globalPath = join2(globalConfigDir(), "profiles", `${profileName}.env`);
@@ -19533,11 +19589,7 @@ async function loadProfileEnv(profileName, projectRoot) {
19533
19589
  const globalFile = Bun.file(globalPath);
19534
19590
  const projectFile = Bun.file(projectPath);
19535
19591
  const [globalExists, projectExists] = await Promise.all([globalFile.exists(), projectFile.exists()]);
19536
- let merged = {};
19537
- for (const [key, value] of Object.entries(_profileDeps.env)) {
19538
- if (value !== undefined && !SENSITIVE_ENV_KEY_PATTERN.test(key))
19539
- merged[key] = value;
19540
- }
19592
+ let merged = sensitiveFilteredProcessEnv();
19541
19593
  if (!globalExists && !projectExists) {
19542
19594
  return merged;
19543
19595
  }
@@ -19645,6 +19697,20 @@ var init_profile = __esm(() => {
19645
19697
  // src/config/loader.ts
19646
19698
  import { existsSync as existsSync3 } from "fs";
19647
19699
  import { basename as basename2, dirname, join as join3, resolve as resolve3 } from "path";
19700
+ function resolveEnvVarsWarnOnFailure(config2, logger, layerName) {
19701
+ try {
19702
+ return resolveEnvVars(config2, sensitiveFilteredProcessEnv());
19703
+ } catch (err) {
19704
+ if (err instanceof UnresolvedEnvVarError) {
19705
+ logger?.warn("config", `${layerName} references undefined environment variable \u2014 left unresolved`, {
19706
+ varName: err.varName,
19707
+ path: err.path.join(".") || "(root)"
19708
+ });
19709
+ return config2;
19710
+ }
19711
+ throw err;
19712
+ }
19713
+ }
19648
19714
  function globalConfigPath() {
19649
19715
  return join3(globalConfigDir(), "config.json");
19650
19716
  }
@@ -19696,6 +19762,7 @@ async function loadConfig(startDir, cliOverrides) {
19696
19762
  });
19697
19763
  }
19698
19764
  }
19765
+ rawConfig = resolveEnvVarsWarnOnFailure(rawConfig, logger, "global/project config");
19699
19766
  for (const name of overlayChain) {
19700
19767
  const profileData = await loadProfile(name, projectRoot);
19701
19768
  const profileEnv = await loadProfileEnv(name, projectRoot);
@@ -19794,13 +19861,23 @@ async function loadConfigForWorkdir(rootConfigPath, packageDir, cliOverrides) {
19794
19861
  const { profile: packageProfile, ...packageFields } = packageOverride;
19795
19862
  let merged = mergePackageConfig(rootConfig, packageFields);
19796
19863
  merged = stripRemovedNoOpKeys(merged, defaultConfigWarn);
19864
+ const envResolvedMerged = resolveEnvVarsWarnOnFailure(merged, logger, `per-package config (${packageDir})`);
19797
19865
  const packageChain = parseProfileList(packageProfile).filter((name) => name && name !== "default");
19798
- let rawMerged = merged;
19866
+ let rawMerged = envResolvedMerged;
19799
19867
  if (packageChain.length > 0) {
19800
19868
  const packageRoot = join3(repoRoot, packageDir);
19801
19869
  for (const name of packageChain) {
19802
19870
  const profileData = await loadProfile(name, packageRoot);
19803
- rawMerged = deepMergeConfig(rawMerged, profileData);
19871
+ const profileEnv = await loadProfileEnv(name, packageRoot);
19872
+ let resolvedProfileData;
19873
+ try {
19874
+ resolvedProfileData = Object.keys(profileEnv).length > 0 ? resolveEnvVars(profileData, profileEnv) : profileData;
19875
+ } catch (err) {
19876
+ const varName = err instanceof UnresolvedEnvVarError ? err.varName : undefined;
19877
+ const path = err instanceof UnresolvedEnvVarError ? err.path.join(".") : undefined;
19878
+ throw new NaxError(`Per-package profile "${name}" (${packageDir}) references an undefined environment variable${varName ? ` $${varName}` : ""}${path ? ` at "${path}"` : ""}.`, "PROFILE_ENV_VAR_UNRESOLVED", { stage: "config", profileName: name, packageDir, varName, path, cause: err });
19879
+ }
19880
+ rawMerged = deepMergeConfig(rawMerged, resolvedProfileData);
19804
19881
  }
19805
19882
  rawMerged.profile = packageChain.join("+");
19806
19883
  rawMerged.profileChain = packageChain;
@@ -19809,6 +19886,7 @@ async function loadConfigForWorkdir(rootConfigPath, packageDir, cliOverrides) {
19809
19886
  rejectLegacyRectificationKeys(rawMerged);
19810
19887
  rejectDeadQualityFlags(rawMerged);
19811
19888
  rejectUnimplementedScopedProfile(rawMerged);
19889
+ rejectUnimplementedPermissionsBlock(rawMerged);
19812
19890
  rawMerged = stripRemovedNoOpKeys(rawMerged, defaultConfigWarn);
19813
19891
  const result = NaxConfigSchema.safeParse(rawMerged);
19814
19892
  if (!result.success) {
@@ -20250,6 +20328,7 @@ __export(exports_config, {
20250
20328
  trackedSpawnDeadlines: () => trackedSpawnDeadlines,
20251
20329
  testPatternConfigSelector: () => testPatternConfigSelector,
20252
20330
  tddConfigSelector: () => tddConfigSelector,
20331
+ sensitiveFilteredProcessEnv: () => sensitiveFilteredProcessEnv,
20253
20332
  routingConfigSelector: () => routingConfigSelector,
20254
20333
  reviewConfigSelector: () => reviewConfigSelector,
20255
20334
  resolveTestStrategy: () => resolveTestStrategy,
@@ -23503,10 +23582,10 @@ class AgentManager {
23503
23582
  const adapter = this._resolveRegistry().getAgent(currentAgent);
23504
23583
  if (!adapter) {
23505
23584
  _finalStatus = "error";
23506
- return {
23507
- result: { output: "", tokenUsage: { inputTokens: 0, outputTokens: 0 }, estimatedCostUsd: 0 },
23508
- fallbacks
23509
- };
23585
+ throw new NaxError(`Agent "${currentAgent}" not found in registry`, "AGENT_NOT_FOUND", {
23586
+ stage: "complete",
23587
+ agentName: currentAgent
23588
+ });
23510
23589
  }
23511
23590
  let result;
23512
23591
  try {
@@ -23655,7 +23734,7 @@ class AgentManager {
23655
23734
  throw new NaxError("AgentManager.runAsSession: _sendPrompt is not wired \u2014 pass sendPrompt at construction via NaxRuntime", "SEND_PROMPT_UNAVAILABLE", { stage: opts.pipelineStage ?? "run", agentName });
23656
23735
  }
23657
23736
  const stage = opts.pipelineStage ?? "run";
23658
- const resolvedPermissions = resolvePermissions(this._config, stage);
23737
+ const resolvedPermissions = resolvePermissions(opts.config ?? this._config, stage);
23659
23738
  const sessionRole = handle.role ?? opts.sessionRole ?? "main";
23660
23739
  const start = Date.now();
23661
23740
  try {
@@ -23697,7 +23776,7 @@ class AgentManager {
23697
23776
  }
23698
23777
  async completeAs(agentName, prompt, options) {
23699
23778
  const stage = options.pipelineStage ?? "complete";
23700
- const resolvedPermissions = resolvePermissions(this._config, stage);
23779
+ const resolvedPermissions = resolvePermissions(options.config ?? this._config, stage);
23701
23780
  const augmented = {
23702
23781
  ...options,
23703
23782
  resolvedPermissions,
@@ -25127,7 +25206,9 @@ function buildDigest(chunks) {
25127
25206
  const scopeRank = Object.fromEntries(SCOPE_ORDER2.map((s, i) => [s, i]));
25128
25207
  const sorted = [...chunks].sort((a, b) => {
25129
25208
  const scopeDiff = (scopeRank[a.scope] ?? 99) - (scopeRank[b.scope] ?? 99);
25130
- return scopeDiff !== 0 ? scopeDiff : a.id.localeCompare(b.id);
25209
+ if (scopeDiff !== 0)
25210
+ return scopeDiff;
25211
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
25131
25212
  });
25132
25213
  const lines = [];
25133
25214
  for (const chunk of sorted) {
@@ -25508,16 +25589,13 @@ var init_manager_run = __esm(() => {
25508
25589
  // src/session/manager-sweep.ts
25509
25590
  function sweepOrphansImpl(sessions, ttlMs) {
25510
25591
  const cutoff = _sessionManagerDeps.nowMs() - ttlMs;
25511
- const terminal = ["COMPLETED", "FAILED"];
25512
25592
  let removed = 0;
25513
25593
  for (const [id, session] of sessions.entries()) {
25514
- if (!terminal.includes(session.state))
25515
- continue;
25516
25594
  const lastActivityMs = session.lastActivityAt ? new Date(session.lastActivityAt).getTime() : Number.NaN;
25517
- if (!Number.isFinite(lastActivityMs) || lastActivityMs < cutoff) {
25518
- sessions.delete(id);
25519
- removed++;
25520
- }
25595
+ if (Number.isFinite(lastActivityMs) && lastActivityMs >= cutoff)
25596
+ continue;
25597
+ sessions.delete(id);
25598
+ removed++;
25521
25599
  }
25522
25600
  if (removed > 0) {
25523
25601
  getLogger().debug("session", "Swept orphan sessions", { removed });
@@ -25821,8 +25899,10 @@ class SessionManager {
25821
25899
  if (terminal.includes(session.state))
25822
25900
  continue;
25823
25901
  const updated = { ...session, state: "COMPLETED", lastActivityAt: now };
25824
- this._sessions.set(id, updated);
25825
25902
  this._persistDescriptor(updated);
25903
+ this._sessions.delete(id);
25904
+ if (updated.handle)
25905
+ this._liveHandles.delete(updated.handle);
25826
25906
  closed.push({ ...updated });
25827
25907
  getLogger().debug("session", "Session closed by closeStory", {
25828
25908
  storyId,
@@ -25869,7 +25949,7 @@ class SessionManager {
25869
25949
  if (!adapter) {
25870
25950
  throw new NaxError(`SessionManager.openSession: no adapter found for agent "${opts.agentName}"`, "ADAPTER_NOT_FOUND", { stage: "session", agentName: opts.agentName });
25871
25951
  }
25872
- const resolvedPermissions = resolvePermissions(this._config, opts.pipelineStage);
25952
+ const resolvedPermissions = resolvePermissions(opts.config ?? this._config, opts.pipelineStage);
25873
25953
  const existingDescriptor = this._findByName(name);
25874
25954
  const resume = existingDescriptor !== undefined;
25875
25955
  const handle = await adapter.openSession(name, {
@@ -28886,19 +28966,55 @@ function buildTestCandidates(sourceFile, workdir, packagePrefix, shapes, testDir
28886
28966
  const sourceAbs = `${workdir}/${sourceFile}`;
28887
28967
  return [...new Set(candidates)].filter((c) => c !== sourceAbs);
28888
28968
  }
28969
+ function tokenizeCommand(command) {
28970
+ const tokens = [];
28971
+ let current = "";
28972
+ let quote = null;
28973
+ for (const char of command.trim()) {
28974
+ if (quote) {
28975
+ current += char;
28976
+ if (char === quote)
28977
+ quote = null;
28978
+ continue;
28979
+ }
28980
+ if (char === '"' || char === "'") {
28981
+ quote = char;
28982
+ current += char;
28983
+ continue;
28984
+ }
28985
+ if (/\s/.test(char)) {
28986
+ if (current) {
28987
+ tokens.push(current);
28988
+ current = "";
28989
+ }
28990
+ continue;
28991
+ }
28992
+ current += char;
28993
+ }
28994
+ if (current)
28995
+ tokens.push(current);
28996
+ return tokens;
28997
+ }
28998
+ function unquote(token) {
28999
+ if (token.length >= 2 && (token[0] === '"' || token[0] === "'") && token[token.length - 1] === token[0]) {
29000
+ return token.slice(1, -1);
29001
+ }
29002
+ return token;
29003
+ }
28889
29004
  function buildSmartTestCommand(testFiles, baseCommand) {
28890
29005
  if (testFiles.length === 0) {
28891
29006
  return baseCommand;
28892
29007
  }
28893
29008
  const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
28894
29009
  const quotedTestFiles = testFiles.map(shellQuote);
28895
- const parts = baseCommand.trim().split(/\s+/);
29010
+ const parts = tokenizeCommand(baseCommand);
28896
29011
  let lastPathIndex = -1;
28897
29012
  for (let i = parts.length - 1;i >= 0; i--) {
28898
- if (!parts[i].includes("/"))
29013
+ const bare = unquote(parts[i]);
29014
+ if (!bare.includes("/"))
28899
29015
  continue;
28900
- const precededByPathFlag = i > 0 && PATH_TAKING_FLAGS.includes(parts[i - 1]);
28901
- const isCombinedFlagValue = PATH_TAKING_FLAGS.some((flag) => parts[i].startsWith(`${flag}=`));
29016
+ const precededByPathFlag = i > 0 && PATH_TAKING_FLAGS.includes(unquote(parts[i - 1]));
29017
+ const isCombinedFlagValue = PATH_TAKING_FLAGS.some((flag) => bare.startsWith(`${flag}=`));
28902
29018
  if (precededByPathFlag || isCombinedFlagValue)
28903
29019
  continue;
28904
29020
  lastPathIndex = i;
@@ -30742,8 +30858,22 @@ function validateInjectedStory(raw, existingIds) {
30742
30858
  } else {
30743
30859
  id = deriveNextStoryId(existingIds);
30744
30860
  }
30745
- const tags = Array.isArray(s.tags) ? s.tags.filter((t) => typeof t === "string") : [];
30746
- const dependencies = Array.isArray(s.dependencies) ? s.dependencies.filter((d) => typeof d === "string") : [];
30861
+ if (Array.isArray(s.tags)) {
30862
+ for (let i = 0;i < s.tags.length; i++) {
30863
+ if (typeof s.tags[i] !== "string") {
30864
+ throw new NaxError(`[queue] INJECT story.tags[${i}] must be a string (got ${typeof s.tags[i]})`, "SCHEMA_VALIDATION_FAILED", { stage: "queue", tagIndex: i, tagType: typeof s.tags[i] });
30865
+ }
30866
+ }
30867
+ }
30868
+ if (Array.isArray(s.dependencies)) {
30869
+ for (let i = 0;i < s.dependencies.length; i++) {
30870
+ if (typeof s.dependencies[i] !== "string") {
30871
+ throw new NaxError(`[queue] INJECT story.dependencies[${i}] must be a string (got ${typeof s.dependencies[i]})`, "SCHEMA_VALIDATION_FAILED", { stage: "queue", depIndex: i, depType: typeof s.dependencies[i] });
30872
+ }
30873
+ }
30874
+ }
30875
+ const tags = Array.isArray(s.tags) ? s.tags : [];
30876
+ const dependencies = Array.isArray(s.dependencies) ? s.dependencies : [];
30747
30877
  for (const dep of dependencies) {
30748
30878
  if (!existingIds.has(dep)) {
30749
30879
  throw new NaxError(`[queue] INJECT story.dependencies references unknown story ID "${dep}"`, "SCHEMA_VALIDATION_FAILED", { stage: "queue", dep });
@@ -30929,14 +31059,28 @@ function validateStory(raw, index, allIds, seenIds) {
30929
31059
  }
30930
31060
  const noTestJustification = typeof rawJustification === "string" && rawJustification.trim() !== "" ? rawJustification.trim() : undefined;
30931
31061
  const rawDeps = s.dependencies;
30932
- const dependencies = Array.isArray(rawDeps) ? Array.from(new Set(rawDeps.map((dep) => normalizeStoryId(dep)))) : [];
31062
+ const dependencies = Array.isArray(rawDeps) ? (() => {
31063
+ for (const [i, dep] of rawDeps.entries()) {
31064
+ if (typeof dep !== "string") {
31065
+ throw new NaxError(`[schema] story[${index}].dependencies[${i}] must be a string (got ${typeof dep})`, "SCHEMA_VALIDATION_FAILED", { stage: "schema", index, depIndex: i, depType: typeof dep });
31066
+ }
31067
+ }
31068
+ return Array.from(new Set(rawDeps.map((dep) => normalizeStoryId(dep))));
31069
+ })() : [];
30933
31070
  for (const dep of dependencies) {
30934
31071
  if (!allIds.has(normalizeStoryId(dep))) {
30935
31072
  throw new NaxError(`[schema] story[${index}].dependencies references unknown story ID "${dep}"`, "SCHEMA_VALIDATION_FAILED", { stage: "schema", index, dep });
30936
31073
  }
30937
31074
  }
30938
31075
  const rawTags = s.tags;
30939
- const tags = Array.isArray(rawTags) ? rawTags : [];
31076
+ const tags = Array.isArray(rawTags) ? (() => {
31077
+ for (const [i, tag] of rawTags.entries()) {
31078
+ if (typeof tag !== "string") {
31079
+ throw new NaxError(`[schema] story[${index}].tags[${i}] must be a string (got ${typeof tag})`, "SCHEMA_VALIDATION_FAILED", { stage: "schema", index, tagIndex: i, tagType: typeof tag });
31080
+ }
31081
+ }
31082
+ return rawTags;
31083
+ })() : [];
30940
31084
  const rawWorkdir = s.workdir;
30941
31085
  let workdir;
30942
31086
  if (rawWorkdir !== undefined && rawWorkdir !== null) {
@@ -33054,6 +33198,84 @@ function computePollutionMetrics(manifests) {
33054
33198
  };
33055
33199
  }
33056
33200
 
33201
+ // src/utils/path-file-lock.ts
33202
+ import { randomUUID as randomUUID5 } from "crypto";
33203
+ import { open, readdir, stat as stat2, unlink as unlink2 } from "fs/promises";
33204
+ import { basename as basename7, dirname as dirname8 } from "path";
33205
+ function buildCandidatePath(targetPath) {
33206
+ const time3 = _pathFileLockDeps.now().toString().padStart(LOCK_TIME_WIDTH, "0");
33207
+ return `${targetPath}.lock.${time3}.${process.pid}.${_pathFileLockDeps.randomUUID()}`;
33208
+ }
33209
+ function candidatePid(fileName) {
33210
+ const segments = fileName.split(".");
33211
+ const pid = Number(segments.at(-2));
33212
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
33213
+ }
33214
+ function candidateTime(fileName) {
33215
+ const segments = fileName.split(".");
33216
+ const timestamp = Number(segments.at(-3));
33217
+ return Number.isFinite(timestamp) ? timestamp : null;
33218
+ }
33219
+ async function listLiveCandidates(targetPath) {
33220
+ const directory = dirname8(targetPath);
33221
+ const prefix = `${basename7(targetPath)}.lock.`;
33222
+ const candidates = (await _pathFileLockDeps.readdir(directory).catch(() => [])).filter((name) => name.startsWith(prefix));
33223
+ const live = [];
33224
+ for (const candidate of candidates) {
33225
+ const pid = candidatePid(candidate);
33226
+ const createdAt = candidateTime(candidate);
33227
+ const candidatePath = `${directory}/${candidate}`;
33228
+ if (pid !== null && createdAt !== null && _pathFileLockDeps.isPidAlive(pid)) {
33229
+ const stats = await _pathFileLockDeps.stat(candidatePath).catch(() => null);
33230
+ if (stats)
33231
+ live.push({ name: candidate, createdAt: stats.birthtimeMs });
33232
+ } else {
33233
+ await _pathFileLockDeps.unlink(candidatePath).catch(() => {});
33234
+ }
33235
+ }
33236
+ return live.sort((a, b) => a.createdAt - b.createdAt || a.name.localeCompare(b.name)).map(({ name }) => name);
33237
+ }
33238
+ async function acquire(targetPath, retryMs, timeoutMs) {
33239
+ const candidatePath = buildCandidatePath(targetPath);
33240
+ const handle = await _pathFileLockDeps.open(candidatePath, "wx");
33241
+ await handle.close();
33242
+ const deadline = _pathFileLockDeps.now() + timeoutMs;
33243
+ while (_pathFileLockDeps.now() < deadline) {
33244
+ const candidates = await listLiveCandidates(targetPath);
33245
+ if (candidates[0] === basename7(candidatePath)) {
33246
+ return async () => {
33247
+ await _pathFileLockDeps.unlink(candidatePath).catch(() => {});
33248
+ };
33249
+ }
33250
+ await _pathFileLockDeps.sleep(retryMs);
33251
+ }
33252
+ await _pathFileLockDeps.unlink(candidatePath).catch(() => {});
33253
+ throw new Error(`[path-lock] Timed out acquiring path lock: ${targetPath}`);
33254
+ }
33255
+ async function withPathFileLock(targetPath, operation, options = {}) {
33256
+ const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
33257
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
33258
+ const release = await acquire(targetPath, retryMs, timeoutMs);
33259
+ try {
33260
+ return await operation();
33261
+ } finally {
33262
+ await release();
33263
+ }
33264
+ }
33265
+ var DEFAULT_RETRY_MS = 10, DEFAULT_TIMEOUT_MS = 5000, LOCK_TIME_WIDTH = 13, _pathFileLockDeps;
33266
+ var init_path_file_lock = __esm(() => {
33267
+ _pathFileLockDeps = {
33268
+ open,
33269
+ readdir,
33270
+ stat: stat2,
33271
+ unlink: unlink2,
33272
+ randomUUID: randomUUID5,
33273
+ now: () => Date.now(),
33274
+ sleep: (ms) => new Promise((resolve9) => setTimeout(resolve9, ms)),
33275
+ isPidAlive: isProcessAlive
33276
+ };
33277
+ });
33278
+
33057
33279
  // src/metrics/types.ts
33058
33280
  class TokenUsage {
33059
33281
  inputTokens;
@@ -33303,9 +33525,12 @@ async function saveRunMetrics(outputDir, runMetrics) {
33303
33525
  cacheCreationInputTokens: totalCacheCreationInputTokens
33304
33526
  })
33305
33527
  } : runMetrics;
33306
- const existing = await loadJsonFile(metricsPath, "metrics");
33307
- const allMetrics = Array.isArray(existing) ? existing : [];
33308
- allMetrics.push(finalMetrics);
33528
+ const allMetrics = await withPathFileLock(metricsPath, async () => {
33529
+ const existing = await loadJsonFile(metricsPath, "metrics");
33530
+ const base = Array.isArray(existing) ? existing : [];
33531
+ base.push(finalMetrics);
33532
+ return base;
33533
+ });
33309
33534
  const isTruncating = allMetrics.length > MAX_RETAINED_RUNS;
33310
33535
  const cappedMetrics = isTruncating ? allMetrics.slice(allMetrics.length - MAX_RETAINED_RUNS) : allMetrics;
33311
33536
  if (isTruncating && !hasWarnedAboutRunTruncation) {
@@ -33331,6 +33556,7 @@ var init_tracker = __esm(() => {
33331
33556
  init_manifest_store();
33332
33557
  init_logger2();
33333
33558
  init_json_file();
33559
+ init_path_file_lock();
33334
33560
  });
33335
33561
 
33336
33562
  // src/metrics/aggregator.ts
@@ -33521,12 +33747,12 @@ function buildLastRun(runs) {
33521
33747
  function buildModelEfficiency(metrics) {
33522
33748
  if (!metrics)
33523
33749
  return [];
33524
- return Object.entries(metrics.modelEfficiency).map(([model, stat2]) => ({
33750
+ return Object.entries(metrics.modelEfficiency).map(([model, stat3]) => ({
33525
33751
  model,
33526
- attempts: stat2.attempts,
33527
- passRate: stat2.passRate,
33528
- avgCost: stat2.avgCost,
33529
- totalCost: stat2.totalCost
33752
+ attempts: stat3.attempts,
33753
+ passRate: stat3.passRate,
33754
+ avgCost: stat3.avgCost,
33755
+ totalCost: stat3.totalCost
33530
33756
  })).sort((x, y) => y.totalCost - x.totalCost);
33531
33757
  }
33532
33758
  function stripInternalFields(report) {
@@ -33658,6 +33884,19 @@ var init_prior_run_failure = __esm(() => {
33658
33884
  };
33659
33885
  });
33660
33886
 
33887
+ // src/utils/jsonl-tail.ts
33888
+ async function readJsonlTail(path2, maxBytes = DEFAULT_JSONL_TAIL_BYTES) {
33889
+ const file3 = Bun.file(path2);
33890
+ const size = file3.size;
33891
+ if (size <= maxBytes)
33892
+ return file3.text();
33893
+ const tail = await file3.slice(size - maxBytes).text();
33894
+ const newlineIndex = tail.indexOf(`
33895
+ `);
33896
+ return newlineIndex === -1 ? "" : tail.slice(newlineIndex + 1);
33897
+ }
33898
+ var DEFAULT_JSONL_TAIL_BYTES = 65536;
33899
+
33661
33900
  // src/context/engine/providers/session-scratch.ts
33662
33901
  import { createHash as createHash9 } from "crypto";
33663
33902
  function contentHash86(content) {
@@ -33762,12 +34001,12 @@ class SessionScratchProvider {
33762
34001
  }
33763
34002
  var MAX_ENTRIES_PER_DIR = 20, MAX_CHUNK_TOKENS3 = 500, _sessionScratchDeps;
33764
34003
  var init_session_scratch = __esm(() => {
33765
- init_scratch_writer();
34004
+ init_session();
33766
34005
  init_path_filters();
33767
34006
  init_scratch_neutralizer();
33768
34007
  _sessionScratchDeps = {
33769
34008
  fileExists: (path2) => Bun.file(path2).exists(),
33770
- readFile: (path2) => Bun.file(path2).text()
34009
+ readFile: (path2) => readJsonlTail(path2)
33771
34010
  };
33772
34011
  });
33773
34012
 
@@ -33976,9 +34215,7 @@ async function autoDetectContextFiles(options) {
33976
34215
  "-I",
33977
34216
  "-E",
33978
34217
  "-e",
33979
- grepPattern,
33980
- "--",
33981
- "src/"
34218
+ grepPattern
33982
34219
  ];
33983
34220
  try {
33984
34221
  const proc = Bun.spawn(grepCommand, {
@@ -34001,6 +34238,9 @@ async function autoDetectContextFiles(options) {
34001
34238
  `).map((line) => line.trim()).filter((line) => line.length > 0);
34002
34239
  const filtered = allFiles.filter((filePath) => {
34003
34240
  const lower = filePath.toLowerCase();
34241
+ if (lower.includes("node_modules/") || lower.startsWith(".git/") || lower.includes("/.git/") || lower.startsWith(".nax/") || lower.includes("/.nax/")) {
34242
+ return false;
34243
+ }
34004
34244
  if (isTestFileByPatterns(filePath, testFilePatterns)) {
34005
34245
  return false;
34006
34246
  }
@@ -34201,6 +34441,7 @@ var init_parent_context = __esm(() => {
34201
34441
  });
34202
34442
 
34203
34443
  // src/context/test-scanner.ts
34444
+ import { stat as stat3 } from "fs/promises";
34204
34445
  import path2 from "path";
34205
34446
  var {Glob: Glob2 } = globalThis.Bun;
34206
34447
  function extractTestStructure(source) {
@@ -34248,8 +34489,8 @@ function deriveTestPatterns(contextFiles, resolvedGlobs) {
34248
34489
  const suffixes = resolvedGlobs ? extractGlobSuffixes(resolvedGlobs) : DEFAULT_TS_DERIVE_SUFFIXES;
34249
34490
  const effectiveSuffixes = suffixes.length > 0 ? suffixes : DEFAULT_TS_DERIVE_SUFFIXES;
34250
34491
  for (const filePath of contextFiles) {
34251
- const basename7 = path2.basename(filePath);
34252
- const basenameNoExt = basename7.replace(/\.[^.]+$/, "");
34492
+ const basename8 = path2.basename(filePath);
34493
+ const basenameNoExt = basename8.replace(/\.[^.]+$/, "");
34253
34494
  for (const suffix of effectiveSuffixes) {
34254
34495
  patterns.add(`${basenameNoExt}${suffix}`);
34255
34496
  }
@@ -34311,8 +34552,8 @@ async function scanTestFiles(options) {
34311
34552
  const files = [];
34312
34553
  for await (const filePath of glob.scan({ cwd: scanDir, absolute: false })) {
34313
34554
  if (allowedBasenames !== null) {
34314
- const basename7 = path2.basename(filePath);
34315
- if (!allowedBasenames.has(basename7)) {
34555
+ const basename8 = path2.basename(filePath);
34556
+ if (!allowedBasenames.has(basename8)) {
34316
34557
  continue;
34317
34558
  }
34318
34559
  }
@@ -34325,6 +34566,24 @@ async function scanTestFiles(options) {
34325
34566
  }
34326
34567
  const fullPath = path2.join(scanDir, filePath);
34327
34568
  try {
34569
+ try {
34570
+ const fileStat = await stat3(fullPath);
34571
+ if (fileStat.size > MAX_TEST_FILE_SIZE_BYTES) {
34572
+ getLogger().debug("test-scanner", "File exceeds size cap \u2014 skipped without read", {
34573
+ path: fullPath,
34574
+ sizeBytes: fileStat.size,
34575
+ capBytes: MAX_TEST_FILE_SIZE_BYTES
34576
+ });
34577
+ continue;
34578
+ }
34579
+ } catch (statErr) {
34580
+ if (statErr.code !== "ENOENT") {
34581
+ getLogger().debug("test-scanner", "stat failed \u2014 falling through to read", {
34582
+ path: fullPath,
34583
+ error: errorMessage(statErr)
34584
+ });
34585
+ }
34586
+ }
34328
34587
  const source = await Bun.file(fullPath).text();
34329
34588
  const { describes, testCount } = extractTestStructure(source);
34330
34589
  if (testCount > 0 || describes.length > 0) {
@@ -34424,10 +34683,11 @@ async function generateTestCoverageSummary(options) {
34424
34683
  const tokens = estimateTokens2(summary);
34425
34684
  return { files, totalTests, summary, tokens };
34426
34685
  }
34427
- var DEFAULT_MAX_SCAN_FILES = 200;
34686
+ var DEFAULT_MAX_SCAN_FILES = 200, MAX_TEST_FILE_SIZE_BYTES;
34428
34687
  var init_test_scanner = __esm(() => {
34429
34688
  init_logger2();
34430
34689
  init_conventions();
34690
+ MAX_TEST_FILE_SIZE_BYTES = 1 * 1024 * 1024;
34431
34691
  });
34432
34692
 
34433
34693
  // src/context/formatter.ts
@@ -34990,6 +35250,25 @@ var init_context = __esm(() => {
34990
35250
  init_fragments();
34991
35251
  });
34992
35252
 
35253
+ // src/context/engine/providers/canonical-rules-cache.ts
35254
+ function memoizedLoadCanonicalRules(workdir, options) {
35255
+ const cached2 = canonicalRulesCache.get(workdir);
35256
+ if (cached2)
35257
+ return cached2;
35258
+ const loaded = loadCanonicalRules(workdir, options);
35259
+ canonicalRulesCache.set(workdir, loaded);
35260
+ loaded.catch(() => canonicalRulesCache.delete(workdir));
35261
+ return loaded;
35262
+ }
35263
+ function _resetCanonicalRulesCache() {
35264
+ canonicalRulesCache.clear();
35265
+ }
35266
+ var canonicalRulesCache;
35267
+ var init_canonical_rules_cache = __esm(() => {
35268
+ init_canonical_loader();
35269
+ canonicalRulesCache = new Map;
35270
+ });
35271
+
34993
35272
  // src/context/engine/providers/static-rules.ts
34994
35273
  import { createHash as createHash10 } from "crypto";
34995
35274
  import { join as join20, relative as relative7 } from "path";
@@ -35018,7 +35297,7 @@ function globToRegex3(pattern) {
35018
35297
  const beforeSlash = i > 0 && pattern[i - 1] === "/";
35019
35298
  const afterSlash = pattern[i + 2] === "/";
35020
35299
  if (beforeSlash && afterSlash) {
35021
- regex = `${regex.slice(0, -1)}(?:.*\\/)?`;
35300
+ regex = `${regex}(?:.*\\/)?`;
35022
35301
  i += 3;
35023
35302
  } else if (afterSlash) {
35024
35303
  regex += "(?:.*\\/)?";
@@ -35125,7 +35404,14 @@ class StaticRulesProvider {
35125
35404
  mergedRules = [...merged.values()];
35126
35405
  }
35127
35406
  }
35128
- mergedRules.sort((a, b) => canonicalRulePriority(a) - canonicalRulePriority(b) || canonicalRuleId(a).localeCompare(canonicalRuleId(b)));
35407
+ mergedRules.sort((a, b) => {
35408
+ const priorityDiff = canonicalRulePriority(a) - canonicalRulePriority(b);
35409
+ if (priorityDiff !== 0)
35410
+ return priorityDiff;
35411
+ const idA = canonicalRuleId(a);
35412
+ const idB = canonicalRuleId(b);
35413
+ return idA < idB ? -1 : idA > idB ? 1 : 0;
35414
+ });
35129
35415
  if (mergedRules.length === 0 && (repoRulesAll.length > 0 || packageRulesCount > 0)) {
35130
35416
  logger.warn("static-rules", "Canonical rules found but none apply to this package context", {
35131
35417
  storyId: request.storyId,
@@ -35377,6 +35663,8 @@ var init_static_rules = __esm(() => {
35377
35663
  init_logger2();
35378
35664
  init_optimizer();
35379
35665
  init_canonical_loader();
35666
+ init_canonical_rules_cache();
35667
+ init_canonical_rules_cache();
35380
35668
  _staticRulesDeps = {
35381
35669
  readFile: async (path4) => Bun.file(path4).text(),
35382
35670
  fileExists: async (path4) => Bun.file(path4).exists(),
@@ -35387,7 +35675,7 @@ var init_static_rules = __esm(() => {
35387
35675
  return [];
35388
35676
  }
35389
35677
  },
35390
- loadCanonicalRules,
35678
+ loadCanonicalRules: memoizedLoadCanonicalRules,
35391
35679
  splitRuleIntoSections,
35392
35680
  applySectionBudget
35393
35681
  };
@@ -35527,12 +35815,15 @@ async function readDiagnosticsDir(scratchDir) {
35527
35815
  } catch {
35528
35816
  return null;
35529
35817
  }
35530
- const entries = parseToolDiagnosticsJsonl(raw);
35531
- if (entries.length === 0)
35818
+ const parsedEntries = parseToolDiagnosticsJsonl(raw);
35819
+ if (parsedEntries.length === 0)
35532
35820
  return null;
35533
- const content = renderEntries(entries);
35821
+ const entries = parsedEntries.slice(-MAX_ENTRIES_PER_DIR2);
35822
+ let content = renderEntries(entries);
35534
35823
  if (!content)
35535
35824
  return null;
35825
+ if (content.length > MAX_CHUNK_CHARS)
35826
+ content = content.slice(0, MAX_CHUNK_CHARS);
35536
35827
  const hash2 = contentHash89(content);
35537
35828
  const tokens = Math.ceil(content.length / 4);
35538
35829
  return {
@@ -35563,13 +35854,14 @@ class ToolDiagnosticsProvider {
35563
35854
  return { chunks, pullTools: [] };
35564
35855
  }
35565
35856
  }
35566
- var _toolDiagnosticsDeps;
35857
+ var _toolDiagnosticsDeps, MAX_ENTRIES_PER_DIR2 = 20, MAX_CHUNK_TOKENS4 = 500, MAX_CHUNK_CHARS;
35567
35858
  var init_tool_diagnostics = __esm(() => {
35568
35859
  init_session();
35569
35860
  _toolDiagnosticsDeps = {
35570
35861
  fileExists: (path4) => Bun.file(path4).exists(),
35571
- readFile: (path4) => Bun.file(path4).text()
35862
+ readFile: (path4) => readJsonlTail(path4)
35572
35863
  };
35864
+ MAX_CHUNK_CHARS = MAX_CHUNK_TOKENS4 * 4;
35573
35865
  });
35574
35866
 
35575
35867
  // src/context/engine/orchestrator-factory.ts
@@ -35760,17 +36052,19 @@ class PluginProviderCache {
35760
36052
  const hit = this.cache.get(key);
35761
36053
  if (hit)
35762
36054
  return hit;
35763
- const providers = await _pluginCacheDeps.loadProviders(enabled, workdir);
35764
- this.cache.set(key, providers);
35765
- return providers;
36055
+ const loading = _pluginCacheDeps.loadProviders(enabled, workdir);
36056
+ this.cache.set(key, loading);
36057
+ loading.catch(() => this.cache.delete(key));
36058
+ return loading;
35766
36059
  }
35767
36060
  async disposeAll() {
35768
36061
  if (this.disposed)
35769
36062
  return;
35770
36063
  this.disposed = true;
35771
36064
  const logger = getLogger();
36065
+ const allProviders = await Promise.all([...this.cache.values()].map((loading) => loading.catch(() => [])));
35772
36066
  const disposals = [];
35773
- for (const providers of this.cache.values()) {
36067
+ for (const providers of allProviders) {
35774
36068
  for (const provider of providers) {
35775
36069
  const initialisable = provider;
35776
36070
  if (typeof initialisable.dispose !== "function")
@@ -35862,17 +36156,22 @@ var init_provider_weights = __esm(() => {
35862
36156
  // src/context/engine/provider-weights-cache.ts
35863
36157
  class ProviderWeightsCache {
35864
36158
  cache = new Map;
36159
+ generations = new Map;
35865
36160
  async loadOrGet(featureId, projectDir) {
35866
36161
  const cached2 = this.cache.get(featureId);
35867
36162
  if (cached2)
35868
36163
  return cached2;
36164
+ const startGeneration = this.generations.get(featureId) ?? 0;
35869
36165
  const stored = await _providerWeightsCacheDeps.loadFeatureManifests({ featureId, projectDir });
35870
36166
  const weights = _providerWeightsCacheDeps.deriveProviderWeights(stored.map((s) => s.manifest));
35871
- this.cache.set(featureId, weights);
36167
+ if ((this.generations.get(featureId) ?? 0) === startGeneration) {
36168
+ this.cache.set(featureId, weights);
36169
+ }
35872
36170
  return weights;
35873
36171
  }
35874
36172
  invalidate(featureId) {
35875
36173
  this.cache.delete(featureId);
36174
+ this.generations.set(featureId, (this.generations.get(featureId) ?? 0) + 1);
35876
36175
  }
35877
36176
  }
35878
36177
  var _providerWeightsCacheDeps;
@@ -36003,7 +36302,7 @@ var init_query_scratch = __esm(() => {
36003
36302
  });
36004
36303
 
36005
36304
  // src/context/engine/stage-assembler.ts
36006
- import { readdir } from "fs/promises";
36305
+ import { readdir as readdir2 } from "fs/promises";
36007
36306
  import { isAbsolute as isAbsolute8, join as join22, resolve as resolve10 } from "path";
36008
36307
  function dedupeScratchDirs(dirs) {
36009
36308
  return [...new Set(dirs.filter((dir) => Boolean(dir)))];
@@ -36137,7 +36436,7 @@ var init_stage_assembler = __esm(() => {
36137
36436
  init_stage_config();
36138
36437
  DISK_DISCOVERY_TTL_MS = 4 * 60 * 60 * 1000;
36139
36438
  _stageAssemblerDeps = {
36140
- readdir: (path4) => readdir(path4),
36439
+ readdir: (path4) => readdir2(path4),
36141
36440
  readDescriptor: async (path4) => {
36142
36441
  const f = Bun.file(path4);
36143
36442
  if (!await f.exists())
@@ -36534,7 +36833,7 @@ var init_effectiveness = __esm(() => {
36534
36833
  });
36535
36834
 
36536
36835
  // src/context/engine/manifest-purge.ts
36537
- import { dirname as dirname8, resolve as resolve11 } from "path";
36836
+ import { dirname as dirname9, resolve as resolve11 } from "path";
36538
36837
  async function purgeStaleManifests(projectDir, retentionDays) {
36539
36838
  const allEntries = await _manifestPurgeDeps.scan(MANIFEST_PATTERN, projectDir, MAX_MANIFEST_SCAN);
36540
36839
  if (allEntries.length >= MAX_MANIFEST_SCAN) {
@@ -36557,7 +36856,7 @@ async function purgeStaleManifests(projectDir, retentionDays) {
36557
36856
  try {
36558
36857
  await _manifestPurgeDeps.unlink(absPath);
36559
36858
  deleted++;
36560
- touchedDirs.add(dirname8(absPath));
36859
+ touchedDirs.add(dirname9(absPath));
36561
36860
  } catch {}
36562
36861
  }
36563
36862
  for (const storyDir of touchedDirs) {
@@ -36586,8 +36885,8 @@ var init_manifest_purge = __esm(() => {
36586
36885
  const file3 = Bun.file(path4);
36587
36886
  if (!await file3.exists())
36588
36887
  throw new Error(`stat: file not found: ${path4}`);
36589
- const stat2 = await file3.stat();
36590
- return stat2.mtimeMs;
36888
+ const stat4 = await file3.stat();
36889
+ return stat4.mtimeMs;
36591
36890
  },
36592
36891
  unlink: async (path4) => {
36593
36892
  const { unlink: nodeUnlink } = await import("fs/promises");
@@ -38067,7 +38366,7 @@ if none of the proposals are acceptable and none can be reasonably synthesized i
38067
38366
  }
38068
38367
  function buildDebateDiffSection(ctx) {
38069
38368
  if (ctx.mode === "ref") {
38070
- const stat2 = ctx.stat ?? "(no stat available)";
38369
+ const stat4 = ctx.stat ?? "(no stat available)";
38071
38370
  const ref = ctx.storyGitRef;
38072
38371
  const excludes = [
38073
38372
  ...new Set([...ctx.productionExcludePatterns ?? [], ":!.nax/", ":!**/.nax/", ":!.nax-pids", ":!**/.nax-pids"])
@@ -38076,7 +38375,7 @@ function buildDebateDiffSection(ctx) {
38076
38375
  return [
38077
38376
  "## Changed Files",
38078
38377
  "```",
38079
- stat2,
38378
+ stat4,
38080
38379
  "```",
38081
38380
  "",
38082
38381
  `## Git Baseline: \`${ref}\``,
@@ -38209,7 +38508,7 @@ ${diff}\`\`\`
38209
38508
 
38210
38509
  `;
38211
38510
  }
38212
- function buildRefDiffSection(storyGitRef, stat2, excludePatterns) {
38511
+ function buildRefDiffSection(storyGitRef, stat4, excludePatterns) {
38213
38512
  const merged = [...new Set([...excludePatterns, ":!.nax/", ":!.nax-pids"])];
38214
38513
  const excludeArgs = merged.map((p) => `'${p}'`).join(" ");
38215
38514
  const productionDiffCmd = `git diff --unified=3 ${storyGitRef}..HEAD -- . ${excludeArgs}`;
@@ -38217,7 +38516,7 @@ function buildRefDiffSection(storyGitRef, stat2, excludePatterns) {
38217
38516
  const logCmd = `git log --oneline ${storyGitRef}..HEAD`;
38218
38517
  return `## Changed Files
38219
38518
  \`\`\`
38220
- ${stat2}
38519
+ ${stat4}
38221
38520
  \`\`\`
38222
38521
 
38223
38522
  ## Git Baseline: \`${storyGitRef}\`
@@ -38439,17 +38738,17 @@ The configured blocking threshold is \`"${threshold}"\`. Findings with severity
38439
38738
 
38440
38739
  `;
38441
38740
  }
38442
- function buildAdversarialRefDiffSection(storyGitRef, stat2, excludePatterns = [], testGlobs = [], refExcludePatterns = []) {
38741
+ function buildAdversarialRefDiffSection(storyGitRef, stat4, excludePatterns = [], testGlobs = [], refExcludePatterns = []) {
38443
38742
  const merged = [...new Set([...excludePatterns, ":!.nax/", ":!**/.nax/", ":!.nax-pids", ":!**/.nax-pids"])];
38444
38743
  const excludeArgs = merged.map((p) => `'${p}'`).join(" ");
38445
38744
  const productionExcludes = [
38446
38745
  ...new Set([...refExcludePatterns, ":!.nax/", ":!**/.nax/", ":!.nax-pids", ":!**/.nax-pids"])
38447
38746
  ];
38448
38747
  const productionExcludeArgs = productionExcludes.map((p) => `'${p}'`).join(" ");
38449
- const statBlock = stat2 ? `## Changed Files Summary
38748
+ const statBlock = stat4 ? `## Changed Files Summary
38450
38749
 
38451
38750
  \`\`\`
38452
- ${stat2}
38751
+ ${stat4}
38453
38752
  \`\`\`
38454
38753
 
38455
38754
  ` : "";
@@ -38662,7 +38961,7 @@ var init_adversarial_review_builder = __esm(() => {
38662
38961
  mode,
38663
38962
  diff,
38664
38963
  storyGitRef,
38665
- stat: stat2,
38964
+ stat: stat4,
38666
38965
  testInventory,
38667
38966
  excludePatterns,
38668
38967
  testGlobs,
@@ -38690,7 +38989,7 @@ ${config2.rules.map((r) => `- ${r}`).join(`
38690
38989
  ` : "";
38691
38990
  let diffBlock;
38692
38991
  if (mode === "ref" && storyGitRef) {
38693
- diffBlock = buildAdversarialRefDiffSection(storyGitRef, stat2, excludePatterns ?? [], testGlobs ?? [], refExcludePatterns ?? []);
38992
+ diffBlock = buildAdversarialRefDiffSection(storyGitRef, stat4, excludePatterns ?? [], testGlobs ?? [], refExcludePatterns ?? []);
38694
38993
  } else if (mode === "embedded" && diff) {
38695
38994
  diffBlock = buildAdversarialEmbeddedDiffSection(diff, testInventory);
38696
38995
  } else {
@@ -39150,8 +39449,8 @@ function stripMarkdownInline(s) {
39150
39449
  function extractLocusKeywords(finding) {
39151
39450
  const keywords = [];
39152
39451
  if (finding.file) {
39153
- const basename7 = finding.file.split("/").pop() ?? "";
39154
- const stem = basename7.replace(/\.[^.]+$/, "");
39452
+ const basename8 = finding.file.split("/").pop() ?? "";
39453
+ const stem = basename8.replace(/\.[^.]+$/, "");
39155
39454
  for (const part of stem.split(/[-_]/)) {
39156
39455
  if (part.length >= 3)
39157
39456
  keywords.push(part.toLowerCase());
@@ -40669,58 +40968,58 @@ function proposeAdjustments(bandStats, mapping, thresholds = {}) {
40669
40968
  const adjustments = [];
40670
40969
  const skipped = [];
40671
40970
  const hints = [];
40672
- for (const stat2 of bandStats) {
40673
- const currentTier = mapping[stat2.complexity];
40971
+ for (const stat4 of bandStats) {
40972
+ const currentTier = mapping[stat4.complexity];
40674
40973
  if (currentTier === undefined) {
40675
40974
  skipped.push({
40676
- complexity: stat2.complexity,
40975
+ complexity: stat4.complexity,
40677
40976
  reason: "missing-mapping"
40678
40977
  });
40679
40978
  continue;
40680
40979
  }
40681
- if (stat2.sampleCount < t.minSamples) {
40980
+ if (stat4.sampleCount < t.minSamples) {
40682
40981
  skipped.push({
40683
- complexity: stat2.complexity,
40982
+ complexity: stat4.complexity,
40684
40983
  reason: "insufficient-samples",
40685
- sampleCount: stat2.sampleCount,
40984
+ sampleCount: stat4.sampleCount,
40686
40985
  minSamples: t.minSamples
40687
40986
  });
40688
40987
  continue;
40689
40988
  }
40690
- const upgraded = stat2.escalationRate >= t.upgradeEscalationRate && stat2.mismatchRate >= t.upgradeMismatchRate;
40691
- const downgraded = stat2.firstPassRate >= t.downgradeFirstPassRate && stat2.escalationRate <= t.downgradeEscalationRate && stat2.mismatchRate > 0;
40989
+ const upgraded = stat4.escalationRate >= t.upgradeEscalationRate && stat4.mismatchRate >= t.upgradeMismatchRate;
40990
+ const downgraded = stat4.firstPassRate >= t.downgradeFirstPassRate && stat4.escalationRate <= t.downgradeEscalationRate && stat4.mismatchRate > 0;
40692
40991
  if (upgraded) {
40693
40992
  const to = nextTier(currentTier, "upgrade");
40694
40993
  if (to !== null) {
40695
40994
  adjustments.push({
40696
- band: stat2.complexity,
40697
- complexity: stat2.complexity,
40995
+ band: stat4.complexity,
40996
+ complexity: stat4.complexity,
40698
40997
  from: currentTier,
40699
40998
  to,
40700
40999
  fromTier: currentTier,
40701
41000
  toTier: to,
40702
41001
  direction: "upgrade",
40703
- rationale: `escalationRate=${stat2.escalationRate} \u2265 ${t.upgradeEscalationRate}, mismatchRate=${stat2.mismatchRate} \u2265 ${t.upgradeMismatchRate}`
41002
+ rationale: `escalationRate=${stat4.escalationRate} \u2265 ${t.upgradeEscalationRate}, mismatchRate=${stat4.mismatchRate} \u2265 ${t.upgradeMismatchRate}`
40704
41003
  });
40705
41004
  }
40706
41005
  } else if (downgraded) {
40707
41006
  const to = nextTier(currentTier, "downgrade");
40708
41007
  if (to !== null) {
40709
41008
  adjustments.push({
40710
- band: stat2.complexity,
40711
- complexity: stat2.complexity,
41009
+ band: stat4.complexity,
41010
+ complexity: stat4.complexity,
40712
41011
  from: currentTier,
40713
41012
  to,
40714
41013
  fromTier: currentTier,
40715
41014
  toTier: to,
40716
41015
  direction: "downgrade",
40717
- rationale: `firstPassRate=${stat2.firstPassRate} \u2265 ${t.downgradeFirstPassRate} and escalationRate=${stat2.escalationRate} \u2264 ${t.downgradeEscalationRate}`
41016
+ rationale: `firstPassRate=${stat4.firstPassRate} \u2265 ${t.downgradeFirstPassRate} and escalationRate=${stat4.escalationRate} \u2264 ${t.downgradeEscalationRate}`
40718
41017
  });
40719
41018
  }
40720
41019
  }
40721
- if (stat2.mismatchRate >= t.upgradeMismatchRate) {
41020
+ if (stat4.mismatchRate >= t.upgradeMismatchRate) {
40722
41021
  hints.push({
40723
- message: `classify.ts: high mismatch for band "${stat2.complexity}" (mismatchRate=${stat2.mismatchRate}) \u2014 review keyword classification.`
41022
+ message: `classify.ts: high mismatch for band "${stat4.complexity}" (mismatchRate=${stat4.mismatchRate}) \u2014 review keyword classification.`
40724
41023
  });
40725
41024
  }
40726
41025
  }
@@ -43799,7 +44098,7 @@ var init_implement = __esm(() => {
43799
44098
  });
43800
44099
 
43801
44100
  // src/tdd/verdict-reader.ts
43802
- import { unlink as unlink2 } from "fs/promises";
44101
+ import { unlink as unlink3 } from "fs/promises";
43803
44102
  import path5 from "path";
43804
44103
  function isValidTestFailureDiagnosis(value) {
43805
44104
  if (!value || typeof value !== "object")
@@ -44019,7 +44318,7 @@ async function readVerdict(workdir) {
44019
44318
  async function cleanupVerdict(workdir) {
44020
44319
  const verdictPath = path5.join(workdir, VERDICT_FILE);
44021
44320
  try {
44022
- await unlink2(verdictPath);
44321
+ await unlink3(verdictPath);
44023
44322
  } catch {}
44024
44323
  }
44025
44324
  var VERDICT_FILE = ".nax-verifier-verdict.json";
@@ -45075,11 +45374,11 @@ async function executeWithTimeout(command, timeoutSeconds, env2, options) {
45075
45374
  if (!exitedDuringGrace) {
45076
45375
  killProcessGroup(pid, "SIGKILL");
45077
45376
  }
45078
- const [out, err] = await Promise.all([
45377
+ const [out2, err2] = await Promise.all([
45079
45378
  raceWithDeadline(stdoutPromise, drainTimeoutMs),
45080
45379
  raceWithDeadline(stderrPromise, drainTimeoutMs)
45081
45380
  ]);
45082
- const parts = [out !== DRAIN_TIMEOUT ? out : "", err !== DRAIN_TIMEOUT ? err : ""].filter(Boolean);
45381
+ const parts = [out2 !== DRAIN_TIMEOUT ? out2 : "", err2 !== DRAIN_TIMEOUT ? err2 : ""].filter(Boolean);
45083
45382
  const partialOutput = parts.join(`
45084
45383
  `) || undefined;
45085
45384
  return {
@@ -45093,7 +45392,12 @@ async function executeWithTimeout(command, timeoutSeconds, env2, options) {
45093
45392
  };
45094
45393
  }
45095
45394
  const exitCode = typeof raceResult === "number" ? raceResult : 0;
45096
- const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
45395
+ const [out, err] = await Promise.all([
45396
+ raceWithDeadline(stdoutPromise, drainTimeoutMs),
45397
+ raceWithDeadline(stderrPromise, drainTimeoutMs)
45398
+ ]);
45399
+ const stdout = out !== DRAIN_TIMEOUT ? out : "";
45400
+ const stderr = err !== DRAIN_TIMEOUT ? err : "";
45097
45401
  const output = `${stdout}
45098
45402
  ${stderr}`;
45099
45403
  return {
@@ -45121,9 +45425,31 @@ function appendForceExitFlag(command) {
45121
45425
  return appendFlag(command, "--forceExit");
45122
45426
  }
45123
45427
  function appendFlag(command, flag) {
45124
- const pipeIndex = command.search(/[|>]/);
45125
- if (pipeIndex > 0) {
45126
- return `${command.slice(0, pipeIndex).trimEnd()} ${flag} ${command.slice(pipeIndex)}`;
45428
+ let quote = null;
45429
+ for (let i = 0;i < command.length; i++) {
45430
+ const char = command[i];
45431
+ if (quote) {
45432
+ if (char === quote)
45433
+ quote = null;
45434
+ continue;
45435
+ }
45436
+ if (char === '"' || char === "'") {
45437
+ quote = char;
45438
+ continue;
45439
+ }
45440
+ if (char === "|" || char === ">") {
45441
+ let splitIndex = i;
45442
+ if (char === ">") {
45443
+ let digitStart = splitIndex;
45444
+ while (digitStart > 0 && /[0-9]/.test(command[digitStart - 1] ?? ""))
45445
+ digitStart--;
45446
+ const charBeforeDigits = command[digitStart - 1];
45447
+ const isStandaloneFdToken = digitStart < splitIndex && (digitStart === 0 || /[\s|;&]/.test(charBeforeDigits ?? ""));
45448
+ if (isStandaloneFdToken)
45449
+ splitIndex = digitStart;
45450
+ }
45451
+ return `${command.slice(0, splitIndex).trimEnd()} ${flag} ${command.slice(splitIndex)}`;
45452
+ }
45127
45453
  }
45128
45454
  return `${command} ${flag}`;
45129
45455
  }
@@ -45933,7 +46259,7 @@ function createDrainDeadline(deadlineMs) {
45933
46259
  };
45934
46260
  }
45935
46261
  async function runQualityCommand(opts) {
45936
- const { commandName, command, workdir, storyId, timeoutMs = DEFAULT_TIMEOUT_MS, env: env2, stripEnvVars } = opts;
46262
+ const { commandName, command, workdir, storyId, timeoutMs = DEFAULT_TIMEOUT_MS2, env: env2, stripEnvVars } = opts;
45937
46263
  if (!command || command.trim() === "") {
45938
46264
  return {
45939
46265
  commandName,
@@ -46046,7 +46372,7 @@ async function runQualityCommand(opts) {
46046
46372
  };
46047
46373
  }
46048
46374
  }
46049
- var DEFAULT_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS = 5000, STREAM_DRAIN_TIMEOUT_MS = 2000, _qualityRunnerDeps;
46375
+ var DEFAULT_TIMEOUT_MS2 = 120000, SIGKILL_GRACE_PERIOD_MS = 5000, STREAM_DRAIN_TIMEOUT_MS = 2000, _qualityRunnerDeps;
46050
46376
  var init_runner = __esm(() => {
46051
46377
  init_logger2();
46052
46378
  _qualityRunnerDeps = {
@@ -47512,12 +47838,12 @@ function journalPathFor(repoRoot, storyId) {
47512
47838
  return join31(journalDir(repoRoot), journalFileName(storyId));
47513
47839
  }
47514
47840
  async function mayHaveJournal(candidateRoots) {
47515
- const { stat: stat2 } = await import("fs/promises");
47841
+ const { stat: stat4 } = await import("fs/promises");
47516
47842
  for (const root of candidateRoots) {
47517
47843
  if (!root)
47518
47844
  continue;
47519
47845
  try {
47520
- if ((await stat2(journalDir(root))).isDirectory())
47846
+ if ((await stat4(journalDir(root))).isDirectory())
47521
47847
  return true;
47522
47848
  } catch {}
47523
47849
  }
@@ -47529,8 +47855,8 @@ async function recordInFlight(repoRoot, entry) {
47529
47855
  await Bun.write(journalPathFor(repoRoot, entry.storyId), JSON.stringify(entry));
47530
47856
  }
47531
47857
  async function clearInFlight(repoRoot, storyId) {
47532
- const { unlink: unlink3 } = await import("fs/promises");
47533
- await unlink3(journalPathFor(repoRoot, storyId)).catch(() => {});
47858
+ const { unlink: unlink4 } = await import("fs/promises");
47859
+ await unlink4(journalPathFor(repoRoot, storyId)).catch(() => {});
47534
47860
  }
47535
47861
  async function readEntry(path7) {
47536
47862
  try {
@@ -47544,11 +47870,11 @@ async function readEntry(path7) {
47544
47870
  }
47545
47871
  }
47546
47872
  async function restoreInFlight(repoRoot) {
47547
- const { readdir: readdir2, unlink: unlink3 } = await import("fs/promises");
47873
+ const { readdir: readdir3, unlink: unlink4 } = await import("fs/promises");
47548
47874
  const dir = journalDir(repoRoot);
47549
47875
  let names;
47550
47876
  try {
47551
- names = await readdir2(dir);
47877
+ names = await readdir3(dir);
47552
47878
  } catch {
47553
47879
  return [];
47554
47880
  }
@@ -47559,13 +47885,13 @@ async function restoreInFlight(repoRoot) {
47559
47885
  const path7 = join31(dir, name);
47560
47886
  const entry = await readEntry(path7);
47561
47887
  if (!entry) {
47562
- await unlink3(path7).catch(() => {});
47888
+ await unlink4(path7).catch(() => {});
47563
47889
  continue;
47564
47890
  }
47565
47891
  if (!isInside(repoRoot, entry.file))
47566
47892
  continue;
47567
47893
  results.push(await restoreEntry(entry));
47568
- await unlink3(path7).catch(() => {});
47894
+ await unlink4(path7).catch(() => {});
47569
47895
  }
47570
47896
  return results;
47571
47897
  }
@@ -48486,8 +48812,12 @@ function recordReviewIteration(store, storyId, roundFindings) {
48486
48812
  startedAt: now,
48487
48813
  finishedAt: now
48488
48814
  };
48489
- store.set(storyId, [...prior, iteration]);
48815
+ const merged = [...prior, iteration];
48816
+ const trimmed = merged.length > MAX_ITERATIONS_PER_STORY ? merged.slice(-MAX_ITERATIONS_PER_STORY) : merged;
48817
+ const renumbered = trimmed.map((it, i) => ({ ...it, iterationNum: i + 1 }));
48818
+ store.set(storyId, renumbered);
48490
48819
  }
48820
+ var MAX_ITERATIONS_PER_STORY = 10;
48491
48821
  var init_review_iteration_store = __esm(() => {
48492
48822
  init_findings();
48493
48823
  });
@@ -48602,15 +48932,15 @@ async function collectDiffStat(workdir, storyGitRef, options) {
48602
48932
  ]);
48603
48933
  return exitCode === 0 ? stdout.trim() : "";
48604
48934
  }
48605
- function truncateDiff(diff, stat2) {
48935
+ function truncateDiff(diff, stat4) {
48606
48936
  if (diff.length <= DIFF_CAP_BYTES) {
48607
48937
  return diff;
48608
48938
  }
48609
48939
  const truncated = diff.slice(0, DIFF_CAP_BYTES);
48610
48940
  const visibleFiles = (truncated.match(/^diff --git/gm) ?? []).length;
48611
48941
  const totalFiles = (diff.match(/^diff --git/gm) ?? []).length;
48612
- const statPreamble = stat2 ? `## File Summary (all changed files)
48613
- ${stat2}
48942
+ const statPreamble = stat4 ? `## File Summary (all changed files)
48943
+ ${stat4}
48614
48944
 
48615
48945
  ## Diff (truncated \u2014 ${visibleFiles}/${totalFiles} files shown)
48616
48946
  ` : "";
@@ -48831,25 +49161,25 @@ async function prepareSemanticReviewInput(args) {
48831
49161
  };
48832
49162
  }
48833
49163
  const { packageDir, packageDirRelative } = derivePackageDirs(workdir, projectDir);
48834
- const stat2 = await collectDiffStat(workdir, effectiveRef, { naxIgnoreIndex, packageDir });
49164
+ const stat4 = await collectDiffStat(workdir, effectiveRef, { naxIgnoreIndex, packageDir });
48835
49165
  const resolved = args.resolvedTestPatterns ?? await resolveTestFilePatterns(config2 ?? reviewConfigSelector.select(DEFAULT_CONFIG), projectDir ?? workdir, packageDirRelative);
48836
49166
  const excludePatterns = [...resolveReviewExcludePatterns(semanticConfig.excludePatterns, resolved)];
48837
49167
  const diffMode = semanticConfig.diffMode ?? "ref";
48838
49168
  if (diffMode === "ref") {
48839
- if (!stat2) {
49169
+ if (!stat4) {
48840
49170
  return { effectiveRef, stat: "", diff: undefined, excludePatterns, skipReason: "no changes detected" };
48841
49171
  }
48842
- return { effectiveRef, stat: stat2, diff: undefined, excludePatterns };
49172
+ return { effectiveRef, stat: stat4, diff: undefined, excludePatterns };
48843
49173
  }
48844
49174
  const rawDiff = await collectDiff(workdir, effectiveRef, excludePatterns, { naxIgnoreIndex, packageDir });
48845
49175
  if (rawDiff === null) {
48846
- return { effectiveRef, stat: stat2, diff: undefined, excludePatterns, skipReason: "git diff failed" };
49176
+ return { effectiveRef, stat: stat4, diff: undefined, excludePatterns, skipReason: "git diff failed" };
48847
49177
  }
48848
- const diff = truncateDiff(rawDiff, rawDiff.length > DIFF_CAP_BYTES ? stat2 : undefined);
49178
+ const diff = truncateDiff(rawDiff, rawDiff.length > DIFF_CAP_BYTES ? stat4 : undefined);
48849
49179
  if (!diff) {
48850
- return { effectiveRef, stat: stat2, diff: undefined, excludePatterns, skipReason: "no production code changes" };
49180
+ return { effectiveRef, stat: stat4, diff: undefined, excludePatterns, skipReason: "no production code changes" };
48851
49181
  }
48852
- return { effectiveRef, stat: stat2, diff, excludePatterns };
49182
+ return { effectiveRef, stat: stat4, diff, excludePatterns };
48853
49183
  }
48854
49184
  async function prepareAdversarialReviewInput(args) {
48855
49185
  const { workdir, projectDir, storyId, storyGitRef, config: config2, naxIgnoreIndex, adversarialConfig } = args;
@@ -48867,9 +49197,9 @@ async function prepareAdversarialReviewInput(args) {
48867
49197
  };
48868
49198
  }
48869
49199
  const { packageDir, packageDirRelative } = derivePackageDirs(workdir, projectDir);
48870
- const stat2 = await collectDiffStat(workdir, effectiveRef, { naxIgnoreIndex, packageDir });
49200
+ const stat4 = await collectDiffStat(workdir, effectiveRef, { naxIgnoreIndex, packageDir });
48871
49201
  const diffMode = adversarialConfig.diffMode ?? "ref";
48872
- if (diffMode === "ref" && !stat2) {
49202
+ if (diffMode === "ref" && !stat4) {
48873
49203
  return {
48874
49204
  effectiveRef,
48875
49205
  stat: "",
@@ -48890,7 +49220,7 @@ async function prepareAdversarialReviewInput(args) {
48890
49220
  if (diffMode === "ref") {
48891
49221
  return {
48892
49222
  effectiveRef,
48893
- stat: stat2,
49223
+ stat: stat4,
48894
49224
  diff: undefined,
48895
49225
  testInventory: undefined,
48896
49226
  excludePatterns,
@@ -48902,7 +49232,7 @@ async function prepareAdversarialReviewInput(args) {
48902
49232
  if (!diff) {
48903
49233
  return {
48904
49234
  effectiveRef,
48905
- stat: stat2,
49235
+ stat: stat4,
48906
49236
  diff: undefined,
48907
49237
  testInventory: undefined,
48908
49238
  excludePatterns,
@@ -48915,7 +49245,7 @@ async function prepareAdversarialReviewInput(args) {
48915
49245
  naxIgnoreIndex,
48916
49246
  packageDir
48917
49247
  });
48918
- return { effectiveRef, stat: stat2, diff, testInventory, excludePatterns, testGlobs, refExcludePatterns };
49248
+ return { effectiveRef, stat: stat4, diff, testInventory, excludePatterns, testGlobs, refExcludePatterns };
48919
49249
  }
48920
49250
  var init_prepare_inputs = __esm(() => {
48921
49251
  init_config();
@@ -48924,14 +49254,14 @@ var init_prepare_inputs = __esm(() => {
48924
49254
  });
48925
49255
 
48926
49256
  // src/utils/nax-project-root.ts
48927
- import { dirname as dirname9, join as join33, resolve as resolve13 } from "path";
49257
+ import { dirname as dirname10, join as join33, resolve as resolve13 } from "path";
48928
49258
  async function findNaxProjectRoot(startDir) {
48929
49259
  let dir = resolve13(startDir);
48930
49260
  for (let depth = 0;depth < MAX_NAX_WALK_DEPTH; depth++) {
48931
49261
  if (await _naxProjectRootDeps.exists(join33(dir, ".nax", "config.json"))) {
48932
49262
  return dir;
48933
49263
  }
48934
- const parent = dirname9(dir);
49264
+ const parent = dirname10(dir);
48935
49265
  if (parent === dir)
48936
49266
  break;
48937
49267
  dir = parent;
@@ -48952,7 +49282,7 @@ var package_default;
48952
49282
  var init_package = __esm(() => {
48953
49283
  package_default = {
48954
49284
  name: "@nathapp/nax",
48955
- version: "0.80.0-canary.4",
49285
+ version: "0.80.0",
48956
49286
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
48957
49287
  type: "module",
48958
49288
  bin: {
@@ -49067,8 +49397,8 @@ var init_version = __esm(() => {
49067
49397
  NAX_VERSION = package_default.version;
49068
49398
  NAX_COMMIT = (() => {
49069
49399
  try {
49070
- if (/^[0-9a-f]{6,10}$/.test("3c02dbe9"))
49071
- return "3c02dbe9";
49400
+ if (/^[0-9a-f]{6,10}$/.test("b4df507e"))
49401
+ return "b4df507e";
49072
49402
  } catch {}
49073
49403
  try {
49074
49404
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -49309,7 +49639,7 @@ async function runAdversarialReview(opts) {
49309
49639
  };
49310
49640
  }
49311
49641
  const effectiveRef = prepared.effectiveRef;
49312
- const stat2 = prepared.stat;
49642
+ const stat4 = prepared.stat;
49313
49643
  const diff = prepared.diff;
49314
49644
  const testInventory = prepared.testInventory;
49315
49645
  const effectiveRefExcludePatterns = prepared.refExcludePatterns;
@@ -49370,7 +49700,7 @@ async function runAdversarialReview(opts) {
49370
49700
  mode: diffMode,
49371
49701
  diff,
49372
49702
  storyGitRef: effectiveRef,
49373
- stat: stat2,
49703
+ stat: stat4,
49374
49704
  testInventory,
49375
49705
  excludePatterns: adversarialConfig.excludePatterns,
49376
49706
  testGlobs,
@@ -50162,7 +50492,7 @@ async function runSemanticDebate(opts) {
50162
50492
  story,
50163
50493
  diffMode,
50164
50494
  diff,
50165
- stat: stat2,
50495
+ stat: stat4,
50166
50496
  semanticConfig,
50167
50497
  effectiveRef,
50168
50498
  startTime,
@@ -50428,7 +50758,7 @@ async function runSemanticReview(opts) {
50428
50758
  };
50429
50759
  }
50430
50760
  const effectiveRef = prepared.effectiveRef;
50431
- const stat2 = prepared.stat;
50761
+ const stat4 = prepared.stat;
50432
50762
  const diff = prepared.diff;
50433
50763
  const excludePatterns = prepared.excludePatterns;
50434
50764
  const effectiveAgentManager = runtime?.agentManager ?? agentManager;
@@ -50468,7 +50798,7 @@ async function runSemanticReview(opts) {
50468
50798
  mode: diffMode,
50469
50799
  diff,
50470
50800
  storyGitRef: effectiveRef,
50471
- stat: stat2,
50801
+ stat: stat4,
50472
50802
  priorSemanticIterations,
50473
50803
  excludePatterns: semanticConfig.excludePatterns
50474
50804
  });
@@ -50501,7 +50831,7 @@ async function runSemanticReview(opts) {
50501
50831
  story,
50502
50832
  diffMode,
50503
50833
  diff,
50504
- stat: stat2,
50834
+ stat: stat4,
50505
50835
  semanticConfig,
50506
50836
  effectiveRef,
50507
50837
  startTime,
@@ -50535,7 +50865,7 @@ async function runSemanticReview(opts) {
50535
50865
  mode: diffMode,
50536
50866
  diff,
50537
50867
  storyGitRef: effectiveRef,
50538
- stat: stat2,
50868
+ stat: stat4,
50539
50869
  priorSemanticIterations,
50540
50870
  excludePatterns,
50541
50871
  featureCtxBlock,
@@ -50841,18 +51171,11 @@ function normalizeMechanicalFindings(checkName, result, workdir) {
50841
51171
  }
50842
51172
  async function getUncommittedFilesImpl(workdir) {
50843
51173
  try {
50844
- const proc = Bun.spawn({
50845
- cmd: ["git", "diff", "--name-only", "HEAD"],
50846
- cwd: workdir,
50847
- stdout: "pipe",
50848
- stderr: "pipe"
50849
- });
50850
- const exitCode = await proc.exited;
51174
+ const { stdout, exitCode } = await gitWithTimeout(["diff", "--name-only", "HEAD"], workdir);
50851
51175
  if (exitCode !== 0) {
50852
51176
  return [];
50853
51177
  }
50854
- const output = await new Response(proc.stdout).text();
50855
- return output.trim().split(`
51178
+ return stdout.trim().split(`
50856
51179
  `).filter(Boolean);
50857
51180
  } catch {
50858
51181
  return [];
@@ -53127,6 +53450,7 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
53127
53450
  role: resolvedRunOptions.sessionRole ?? "implementer",
53128
53451
  workdir,
53129
53452
  pipelineStage: stage,
53453
+ config: config2,
53130
53454
  modelDef,
53131
53455
  ...resolvedRunOptions.modelDef !== undefined ? {} : { modelTier: effectiveTier },
53132
53456
  timeoutSeconds: resolvedRunOptions.timeoutSeconds ?? config2.execution?.sessionTimeoutSeconds ?? DEFAULT_CONFIG.execution.sessionTimeoutSeconds,
@@ -53143,6 +53467,7 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
53143
53467
  role: resolvedRunOptions.sessionRole ?? "implementer",
53144
53468
  workdir,
53145
53469
  pipelineStage: stage,
53470
+ config: config2,
53146
53471
  modelDef,
53147
53472
  ...pinned ? {} : { modelTier: effectiveTier },
53148
53473
  timeoutSeconds: resolvedRunOptions.timeoutSeconds ?? config2.execution?.sessionTimeoutSeconds ?? DEFAULT_CONFIG.execution.sessionTimeoutSeconds,
@@ -53159,6 +53484,7 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
53159
53484
  workdir,
53160
53485
  projectDir,
53161
53486
  pipelineStage: stage,
53487
+ config: config2,
53162
53488
  sessionRole: resolvedRunOptions.sessionRole,
53163
53489
  signal: resolvedRunOptions.abortSignal,
53164
53490
  contextPullTools,
@@ -55285,8 +55611,8 @@ async function triageFlakyFindings(input) {
55285
55611
  result.push({ ...f });
55286
55612
  return { findings: result, quarantineReport: { keys, reasons } };
55287
55613
  }
55288
- const changedTestSet = new Set(diff.changedTestFiles.map(basename7));
55289
- const mappedTestSet = new Set(diff.mappedTestFiles.map(basename7));
55614
+ const changedTestSet = new Set(diff.changedTestFiles.map(basename8));
55615
+ const mappedTestSet = new Set(diff.mappedTestFiles.map(basename8));
55290
55616
  const candidates = findings.filter((f) => isProbeCandidate(f, changedTestSet, mappedTestSet));
55291
55617
  if (candidates.length > flakeDetection.maxProbesPerGate) {
55292
55618
  logger?.info("flake-triage", `Skipping flake triage \u2014 ${candidates.length} candidates exceed maxProbesPerGate=${flakeDetection.maxProbesPerGate}`);
@@ -55356,7 +55682,7 @@ async function triageFlakyFindings(input) {
55356
55682
  }
55357
55683
  return { findings: result, quarantineReport: { keys, reasons } };
55358
55684
  }
55359
- function basename7(path8) {
55685
+ function basename8(path8) {
55360
55686
  const i = path8.lastIndexOf("/");
55361
55687
  return i === -1 ? path8 : path8.slice(i + 1);
55362
55688
  }
@@ -55367,7 +55693,7 @@ function isProbeCandidate(finding, changedTestSet, mappedTestSet) {
55367
55693
  return false;
55368
55694
  if (!finding.rule)
55369
55695
  return false;
55370
- const base = basename7(finding.file);
55696
+ const base = basename8(finding.file);
55371
55697
  if (changedTestSet.has(base) || changedTestSet.has(finding.file))
55372
55698
  return false;
55373
55699
  if (mappedTestSet.has(base) || mappedTestSet.has(finding.file))
@@ -55404,6 +55730,7 @@ function createSessionRunHop(sessionManager, getAgentManager) {
55404
55730
  role: options.sessionRole,
55405
55731
  workdir: options.workdir,
55406
55732
  pipelineStage: options.pipelineStage ?? "run",
55733
+ config: options.config,
55407
55734
  modelDef: options.modelDef,
55408
55735
  timeoutSeconds: options.timeoutSeconds,
55409
55736
  featureName: options.featureName,
@@ -55422,6 +55749,7 @@ function createSessionRunHop(sessionManager, getAgentManager) {
55422
55749
  workdir: options.workdir,
55423
55750
  projectDir: options.projectDir,
55424
55751
  pipelineStage: options.pipelineStage ?? "run",
55752
+ config: options.config,
55425
55753
  sessionRole: options.sessionRole,
55426
55754
  signal: options.abortSignal,
55427
55755
  interactionHandler,
@@ -55513,7 +55841,7 @@ __export(exports_runtime, {
55513
55841
  CostAggregator: () => CostAggregator,
55514
55842
  AgentStreamEventBus: () => AgentStreamEventBus
55515
55843
  });
55516
- import { basename as basename8, join as join38 } from "path";
55844
+ import { basename as basename9, join as join38 } from "path";
55517
55845
  function createRuntime(config2, workdir, opts) {
55518
55846
  const runId = crypto.randomUUID();
55519
55847
  const controller = new AbortController;
@@ -55525,7 +55853,7 @@ function createRuntime(config2, workdir, opts) {
55525
55853
  const configLoader = createConfigLoader(config2);
55526
55854
  const dispatchEvents = new DispatchEventBus;
55527
55855
  const agentStreamEvents = opts?.agentStreamEvents ?? new AgentStreamEventBus;
55528
- const projectKey = config2.name?.trim() || basename8(workdir);
55856
+ const projectKey = config2.name?.trim() || basename9(workdir);
55529
55857
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
55530
55858
  const globalDir = globalOutputDir();
55531
55859
  const curatorRollupPathValue = curatorRollupPath(globalDir, config2.curator?.rollupPath);
@@ -58000,11 +58328,28 @@ class InteractionChain {
58000
58328
  return this.plugins[0]?.plugin ?? null;
58001
58329
  }
58002
58330
  async send(request) {
58003
- const plugin = this.getPrimary();
58004
- if (!plugin) {
58005
- throw new NaxError("No interaction plugin registered", "INTERACTION_ERROR", { stage: "run" });
58331
+ if (this.plugins.length === 0) {
58332
+ throw new NaxError("No interaction plugin registered", "INTERACTION_ERROR", {
58333
+ stage: "run",
58334
+ requestId: request.id
58335
+ });
58336
+ }
58337
+ const errors3 = [];
58338
+ for (const entry of this.plugins) {
58339
+ try {
58340
+ await entry.plugin.send(request);
58341
+ return;
58342
+ } catch (err) {
58343
+ const error48 = err instanceof Error ? err : new Error(String(err));
58344
+ errors3.push(error48);
58345
+ }
58006
58346
  }
58007
- await plugin.send(request);
58347
+ const errorMessages = errors3.map((e) => e.message).join("; ");
58348
+ throw new NaxError(`All interaction plugins failed: ${errorMessages}`, "INTERACTION_ERROR", {
58349
+ stage: "run",
58350
+ requestId: request.id,
58351
+ pluginCount: this.plugins.length
58352
+ });
58008
58353
  }
58009
58354
  async receive(requestId, timeout) {
58010
58355
  if (this.plugins.length === 0) {
@@ -58393,10 +58738,10 @@ function buildHeader(request) {
58393
58738
  const emoji3 = getStageEmoji(request.stage);
58394
58739
  let text = `${emoji3} *${request.stage.toUpperCase()}*
58395
58740
  `;
58396
- text += `*Feature:* ${request.featureName}
58741
+ text += `*Feature:* ${sanitizeMarkdown(request.featureName)}
58397
58742
  `;
58398
58743
  if (request.storyId) {
58399
- text += `*Story:* ${request.storyId}
58744
+ text += `*Story:* ${sanitizeMarkdown(request.storyId)}
58400
58745
  `;
58401
58746
  }
58402
58747
  text += `
@@ -58603,16 +58948,24 @@ var init_telegram = __esm(() => {
58603
58948
  const partLabel = chunks.length > 1 ? `[${i + 1}/${chunks.length}] ` : "";
58604
58949
  const text = `${header}
58605
58950
  ${partLabel}${chunks[i]}`;
58606
- const response = await _telegramPluginDeps.fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`, {
58607
- method: "POST",
58608
- headers: { "Content-Type": "application/json" },
58609
- body: JSON.stringify({
58610
- chat_id: this.chatId,
58611
- text,
58612
- reply_markup: isLast && keyboard ? { inline_keyboard: keyboard } : undefined,
58613
- parse_mode: "Markdown"
58614
- })
58615
- });
58951
+ const controller = new AbortController;
58952
+ const timer = setTimeout(() => controller.abort(), 5000);
58953
+ let response;
58954
+ try {
58955
+ response = await _telegramPluginDeps.fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`, {
58956
+ method: "POST",
58957
+ headers: { "Content-Type": "application/json" },
58958
+ body: JSON.stringify({
58959
+ chat_id: this.chatId,
58960
+ text,
58961
+ reply_markup: isLast && keyboard ? { inline_keyboard: keyboard } : undefined,
58962
+ parse_mode: "Markdown"
58963
+ }),
58964
+ signal: controller.signal
58965
+ });
58966
+ } finally {
58967
+ clearTimeout(timer);
58968
+ }
58616
58969
  if (!response.ok) {
58617
58970
  const errorBody = await response.text().catch(() => "");
58618
58971
  throw new Error(`Telegram API error (${response.status}): ${errorBody || response.statusText}`);
@@ -58648,8 +59001,9 @@ ${partLabel}${chunks[i]}`;
58648
59001
  });
58649
59002
  }
58650
59003
  async cancel(requestId) {
58651
- await this.sendTimeoutMessage(requestId);
59004
+ const pending = this.pendingMessages.get(requestId);
58652
59005
  this.resolveReceiver(requestId, "skip", "timeout");
59006
+ this.sendTimeoutMessage(requestId, pending);
58653
59007
  }
58654
59008
  ensurePoller() {
58655
59009
  if (this.poller)
@@ -58709,8 +59063,9 @@ ${partLabel}${chunks[i]}`;
58709
59063
  });
58710
59064
  }
58711
59065
  async expireReceiver(requestId) {
58712
- await this.sendTimeoutMessage(requestId);
59066
+ const pending = this.pendingMessages.get(requestId);
58713
59067
  this.resolveReceiver(requestId, "skip", "timeout");
59068
+ this.sendTimeoutMessage(requestId, pending);
58714
59069
  }
58715
59070
  resolveReceiver(requestId, action, respondedBy) {
58716
59071
  this.resolveReceiverWithResponse(requestId, { requestId, action, respondedBy, respondedAt: Date.now() });
@@ -58862,24 +59217,31 @@ ${partLabel}${chunks[i]}`;
58862
59217
  }
58863
59218
  } catch {}
58864
59219
  }
58865
- async sendTimeoutMessage(requestId) {
58866
- const pending = this.pendingMessages.get(requestId);
59220
+ async sendTimeoutMessage(requestId, pendingArg) {
59221
+ const pending = pendingArg ?? this.pendingMessages.get(requestId);
58867
59222
  if (!pending || !this.botToken || !this.chatId) {
58868
59223
  this.pendingMessages.delete(requestId);
58869
59224
  return;
58870
59225
  }
58871
59226
  const lastId = pending.ids[pending.ids.length - 1];
58872
59227
  try {
58873
- await _telegramPluginDeps.fetch(`https://api.telegram.org/bot${this.botToken}/editMessageText`, {
58874
- method: "POST",
58875
- headers: { "Content-Type": "application/json" },
58876
- body: JSON.stringify({
58877
- chat_id: this.chatId,
58878
- message_id: lastId,
58879
- text: "\u23F1 EXPIRED \u2014 Interaction timed out",
58880
- reply_markup: { inline_keyboard: [] }
58881
- })
58882
- });
59228
+ const controller = new AbortController;
59229
+ const timer = setTimeout(() => controller.abort(), CALLBACK_API_TIMEOUT_MS);
59230
+ try {
59231
+ await _telegramPluginDeps.fetch(`https://api.telegram.org/bot${this.botToken}/editMessageText`, {
59232
+ method: "POST",
59233
+ headers: { "Content-Type": "application/json" },
59234
+ body: JSON.stringify({
59235
+ chat_id: this.chatId,
59236
+ message_id: lastId,
59237
+ text: "\u23F1 EXPIRED \u2014 Interaction timed out",
59238
+ reply_markup: { inline_keyboard: [] }
59239
+ }),
59240
+ signal: controller.signal
59241
+ });
59242
+ } finally {
59243
+ clearTimeout(timer);
59244
+ }
58883
59245
  } catch {} finally {
58884
59246
  this.pendingMessages.delete(requestId);
58885
59247
  }
@@ -58974,7 +59336,7 @@ function installServePortZeroCompat() {
58974
59336
  const request = input instanceof Request ? input : new Request(input instanceof URL ? input.toString() : input, init);
58975
59337
  const url2 = new URL(request.url);
58976
59338
  const port = Number.parseInt(url2.port, 10);
58977
- if ((url2.hostname === "localhost" || url2.hostname === "127.0.0.1") && inMemoryServers.has(port)) {
59339
+ if ((url2.hostname === "localhost" || url2.hostname === "127.0.0.1") && url2.pathname.startsWith(CALLBACK_PATH_PREFIX) && inMemoryServers.has(port)) {
58978
59340
  const server = inMemoryServers.get(port);
58979
59341
  if (!server) {
58980
59342
  return new Response("Not Found", { status: 404 });
@@ -58985,7 +59347,7 @@ function installServePortZeroCompat() {
58985
59347
  };
58986
59348
  servePortZeroCompatInstalled = true;
58987
59349
  }
58988
- var PORT_ZERO_COMPAT_BASE = 40000, PORT_ZERO_COMPAT_SPAN = 20000, servePortZeroCompatInstalled = false, servePortZeroCounter = 0, inMemoryServers;
59350
+ var PORT_ZERO_COMPAT_BASE = 40000, PORT_ZERO_COMPAT_SPAN = 20000, CALLBACK_PATH_PREFIX = "/nax/interact/", servePortZeroCompatInstalled = false, servePortZeroCounter = 0, inMemoryServers;
58989
59351
  var init_webhook_serve_compat = __esm(() => {
58990
59352
  init_errors();
58991
59353
  inMemoryServers = new Map;
@@ -59575,10 +59937,10 @@ function validateFeatureName(feature) {
59575
59937
 
59576
59938
  // src/plan/critic.ts
59577
59939
  import { mkdir as mkdir7 } from "fs/promises";
59578
- import { dirname as dirname10, join as join47 } from "path";
59940
+ import { dirname as dirname11, join as join47 } from "path";
59579
59941
  async function writeSpecDeltas(findings, workdir, runId, storyId, manifest) {
59580
59942
  const path8 = join47(workdir, ".nax", "runs", runId, "plan", storyId, "spec-deltas.md");
59581
- await mkdir7(dirname10(path8), { recursive: true });
59943
+ await mkdir7(dirname11(path8), { recursive: true });
59582
59944
  await Bun.write(path8, formatSpecDeltas(findings, manifest));
59583
59945
  return path8;
59584
59946
  }
@@ -59806,8 +60168,8 @@ async function checkStaleLock(workdir) {
59806
60168
  } else if (lockData.startedAt) {
59807
60169
  lockTimeMs = new Date(lockData.startedAt).getTime();
59808
60170
  } else {
59809
- const stat2 = statSync3(lockPath);
59810
- lockTimeMs = stat2.mtimeMs;
60171
+ const stat4 = statSync3(lockPath);
60172
+ lockTimeMs = stat4.mtimeMs;
59811
60173
  }
59812
60174
  const holderAlive = typeof lockData.pid === "number" && isProcessAlive(lockData.pid);
59813
60175
  const ageMs = Date.now() - lockTimeMs;
@@ -61075,10 +61437,10 @@ __export(exports_status_cost, {
61075
61437
  displayCostMetrics: () => displayCostMetrics,
61076
61438
  _costReportEmitDeps: () => _costReportEmitDeps
61077
61439
  });
61078
- import { basename as basename9 } from "path";
61440
+ import { basename as basename10 } from "path";
61079
61441
  async function resolveProject(workdir) {
61080
61442
  const config2 = await loadConfig(workdir).catch(() => null);
61081
- const project = config2?.name?.trim() || basename9(workdir);
61443
+ const project = config2?.name?.trim() || basename10(workdir);
61082
61444
  const outputDir = projectOutputDir(project, config2?.outputDir);
61083
61445
  return { project, outputDir };
61084
61446
  }
@@ -61359,7 +61721,7 @@ __export(exports_status_features, {
61359
61721
  _statusFeaturesDeps: () => _statusFeaturesDeps
61360
61722
  });
61361
61723
  import { existsSync as existsSync17, readdirSync as readdirSync3 } from "fs";
61362
- import { basename as basename10, join as join51, resolve as resolve15 } from "path";
61724
+ import { basename as basename11, join as join51, resolve as resolve15 } from "path";
61363
61725
  async function loadStatusFile(featureDir2) {
61364
61726
  const statusPath = join51(featureDir2, "status.json");
61365
61727
  if (!existsSync17(statusPath)) {
@@ -61374,7 +61736,7 @@ async function loadStatusFile(featureDir2) {
61374
61736
  }
61375
61737
  async function loadProjectStatusFile(projectDir) {
61376
61738
  const config2 = await _statusFeaturesDeps.loadConfig(projectDir).catch(() => null);
61377
- const projectKey = config2?.name?.trim() || basename10(projectDir);
61739
+ const projectKey = config2?.name?.trim() || basename11(projectDir);
61378
61740
  const outputDir = _statusFeaturesDeps.projectOutputDir(projectKey, config2?.outputDir);
61379
61741
  const statusPath = join51(outputDir, "status.json");
61380
61742
  if (!existsSync17(statusPath)) {
@@ -61448,7 +61810,7 @@ async function getFeatureSummary(featureName, featureDir2) {
61448
61810
  }
61449
61811
  async function displayAllFeatures(projectDir) {
61450
61812
  const config2 = await _statusFeaturesDeps.loadConfig(projectDir).catch(() => null);
61451
- const projectKey = config2?.name?.trim() || basename10(projectDir);
61813
+ const projectKey = config2?.name?.trim() || basename11(projectDir);
61452
61814
  const outputDir = _statusFeaturesDeps.projectOutputDir(projectKey, config2?.outputDir);
61453
61815
  const featuresDir2 = join51(outputDir, "features");
61454
61816
  if (!existsSync17(featuresDir2)) {
@@ -61643,7 +62005,7 @@ async function displayFeatureStatus(options = {}) {
61643
62005
  if (options.dir) {
61644
62006
  const projectDir = resolve15(options.dir);
61645
62007
  const config2 = await _statusFeaturesDeps.loadConfig(projectDir).catch(() => null);
61646
- const projectKey = config2?.name?.trim() || basename10(projectDir);
62008
+ const projectKey = config2?.name?.trim() || basename11(projectDir);
61647
62009
  const outputDir = _statusFeaturesDeps.projectOutputDir(projectKey, config2?.outputDir);
61648
62010
  featureDir2 = join51(outputDir, "features", options.feature);
61649
62011
  } else {
@@ -62025,8 +62387,8 @@ var init_semantic_verdict = __esm(() => {
62025
62387
  await Bun.write(filePath, content);
62026
62388
  },
62027
62389
  readdir: async (dir) => {
62028
- const { readdir: readdir2 } = await import("fs/promises");
62029
- return readdir2(dir);
62390
+ const { readdir: readdir3 } = await import("fs/promises");
62391
+ return readdir3(dir);
62030
62392
  },
62031
62393
  readFile: async (filePath) => {
62032
62394
  return Bun.file(filePath).text();
@@ -62944,9 +63306,9 @@ var init_acceptance_setup = __esm(() => {
62944
63306
  await Bun.write(dest, content);
62945
63307
  },
62946
63308
  deleteFile: async (filePath) => {
62947
- const { unlink: unlink3 } = await import("fs/promises");
63309
+ const { unlink: unlink4 } = await import("fs/promises");
62948
63310
  try {
62949
- await unlink3(filePath);
63311
+ await unlink4(filePath);
62950
63312
  } catch (err) {
62951
63313
  if (err.code !== "ENOENT")
62952
63314
  throw err;
@@ -62954,17 +63316,17 @@ var init_acceptance_setup = __esm(() => {
62954
63316
  },
62955
63317
  deleteSemanticVerdicts: async (featureDir2) => {
62956
63318
  const dir = `${featureDir2}/semantic-verdicts`;
62957
- const { readdir: readdir2, unlink: unlink3 } = await import("fs/promises");
63319
+ const { readdir: readdir3, unlink: unlink4 } = await import("fs/promises");
62958
63320
  let files;
62959
63321
  try {
62960
- files = await readdir2(dir);
63322
+ files = await readdir3(dir);
62961
63323
  } catch (err) {
62962
63324
  if (err.code === "ENOENT")
62963
63325
  return;
62964
63326
  throw err;
62965
63327
  }
62966
63328
  for (const file3 of files) {
62967
- await unlink3(`${dir}/${file3}`);
63329
+ await unlink4(`${dir}/${file3}`);
62968
63330
  }
62969
63331
  },
62970
63332
  readMeta: async (metaPath) => {
@@ -63405,7 +63767,7 @@ var init_constitution = __esm(() => {
63405
63767
  });
63406
63768
 
63407
63769
  // src/pipeline/stages/constitution.ts
63408
- import { dirname as dirname11 } from "path";
63770
+ import { dirname as dirname12 } from "path";
63409
63771
  var constitutionStage;
63410
63772
  var init_constitution2 = __esm(() => {
63411
63773
  init_constitution();
@@ -63415,7 +63777,7 @@ var init_constitution2 = __esm(() => {
63415
63777
  enabled: (ctx) => ctx.config.constitution.enabled,
63416
63778
  async execute(ctx) {
63417
63779
  const logger = getLogger();
63418
- const ngentDir = ctx.featureDir ? dirname11(dirname11(ctx.featureDir)) : `${ctx.workdir}/nax`;
63780
+ const ngentDir = ctx.featureDir ? dirname12(dirname12(ctx.featureDir)) : `${ctx.workdir}/nax`;
63419
63781
  const result = await loadConstitution(ngentDir, ctx.config.constitution);
63420
63782
  if (result) {
63421
63783
  ctx.constitution = result;
@@ -63594,7 +63956,7 @@ var init_story_context = __esm(() => {
63594
63956
  });
63595
63957
 
63596
63958
  // src/execution/lock.ts
63597
- import { rename as rename2, unlink as unlink3 } from "fs/promises";
63959
+ import { rename as rename2, unlink as unlink4 } from "fs/promises";
63598
63960
  import path13 from "path";
63599
63961
  function getSafeLogger4() {
63600
63962
  try {
@@ -63658,7 +64020,7 @@ async function acquireLock(workdir) {
63658
64020
  }
63659
64021
  if (claimedPid !== lockPid) {
63660
64022
  const restored = claimedContent !== null && await tryExclusiveCreate(lockPath, claimedContent);
63661
- await unlink3(tombstonePath).catch(() => {});
64023
+ await unlink4(tombstonePath).catch(() => {});
63662
64024
  if (!restored) {
63663
64025
  const logger2 = getSafeLogger4();
63664
64026
  logger2?.warn("execution", "Stolen lock could not be restored \u2014 a newer lock already exists", {
@@ -63671,7 +64033,7 @@ async function acquireLock(workdir) {
63671
64033
  logger?.warn("execution", "Removing stale lock", {
63672
64034
  pid: lockPid
63673
64035
  });
63674
- await unlink3(tombstonePath).catch(() => {});
64036
+ await unlink4(tombstonePath).catch(() => {});
63675
64037
  }
63676
64038
  }
63677
64039
  const lockData = {
@@ -63697,7 +64059,7 @@ async function acquireLock(workdir) {
63697
64059
  async function releaseLock(workdir) {
63698
64060
  const lockPath = path13.join(workdir, "nax.lock");
63699
64061
  try {
63700
- await unlink3(lockPath);
64062
+ await unlink4(lockPath);
63701
64063
  } catch (error48) {
63702
64064
  if (error48.code !== "ENOENT") {
63703
64065
  const logger = getSafeLogger4();
@@ -63718,7 +64080,7 @@ var init_helpers = __esm(() => {
63718
64080
  });
63719
64081
 
63720
64082
  // src/pipeline/stages/context.ts
63721
- import { randomUUID as randomUUID5 } from "crypto";
64083
+ import { randomUUID as randomUUID6 } from "crypto";
63722
64084
  import { join as join57 } from "path";
63723
64085
  async function runV2Path(ctx) {
63724
64086
  const logger = getLogger();
@@ -63957,7 +64319,7 @@ var init_context2 = __esm(() => {
63957
64319
  createOrchestrator: createDefaultOrchestrator,
63958
64320
  loadPlugins: loadPluginProviders,
63959
64321
  v1FeatureProvider: () => new FeatureContextProvider,
63960
- uuid: () => randomUUID5(),
64322
+ uuid: () => randomUUID6(),
63961
64323
  readDigest: readDigestFile,
63962
64324
  writeDigest: writeDigestFile,
63963
64325
  loadFeatureManifests,
@@ -63986,23 +64348,30 @@ function spawnGit(deps, args, workdir) {
63986
64348
  });
63987
64349
  }
63988
64350
  async function spawnWithTimeout(proc, timeoutMs) {
63989
- const result = await Promise.race([
63990
- (async () => {
63991
- const [exitCode, stdout] = await Promise.all([
63992
- proc.exited,
63993
- new Response(proc.stdout).text(),
63994
- new Response(proc.stderr).text()
63995
- ]);
63996
- return { stdout, exitCode };
63997
- })(),
63998
- new Promise((resolve16) => setTimeout(() => {
63999
- try {
64000
- proc.kill("SIGKILL");
64001
- } catch {}
64002
- resolve16({ stdout: "", exitCode: 1 });
64003
- }, timeoutMs))
64004
- ]);
64005
- return result;
64351
+ let timer;
64352
+ try {
64353
+ const result = await Promise.race([
64354
+ (async () => {
64355
+ const [exitCode, stdout] = await Promise.all([
64356
+ proc.exited,
64357
+ new Response(proc.stdout).text(),
64358
+ new Response(proc.stderr).text()
64359
+ ]);
64360
+ return { stdout, exitCode };
64361
+ })(),
64362
+ new Promise((resolve16) => {
64363
+ timer = setTimeout(() => {
64364
+ try {
64365
+ proc.kill("SIGKILL");
64366
+ } catch {}
64367
+ resolve16({ stdout: "", exitCode: 1 });
64368
+ }, timeoutMs);
64369
+ })
64370
+ ]);
64371
+ return result;
64372
+ } finally {
64373
+ clearTimeout(timer);
64374
+ }
64006
64375
  }
64007
64376
  function captureFailureSentinel() {
64008
64377
  return `__capture_failed__:${Date.now()}:${Math.random().toString(36).slice(2)}`;
@@ -64076,7 +64445,7 @@ function buildCheckpointLogData(meta3) {
64076
64445
  const { storyId, ...rest } = meta3;
64077
64446
  return { storyId, ...rest };
64078
64447
  }
64079
- var TREE_CAPTURE_TIMEOUT_MS = 75, MAX_UNTRACKED_HASHED = 500, UNTRACKED_HASH_TIMEOUT_MS = 250;
64448
+ var TREE_CAPTURE_TIMEOUT_MS = 1000, MAX_UNTRACKED_HASHED = 500, UNTRACKED_HASH_TIMEOUT_MS = 250;
64080
64449
 
64081
64450
  // src/tdd/rollback.ts
64082
64451
  import { rm as rm2 } from "fs/promises";
@@ -65605,7 +65974,19 @@ class ExecutionPlan {
65605
65974
  });
65606
65975
  } else {
65607
65976
  const currentTree = await _storyOrchestratorDeps.captureTreeState(this.ctx.packageDir);
65608
- await _storyOrchestratorDeps.recordGreen(this.ctx.storyId, name, currentTree);
65977
+ try {
65978
+ await _storyOrchestratorDeps.recordGreen(this.ctx.storyId, name, currentTree);
65979
+ } catch (error48) {
65980
+ if (error48 instanceof NaxError && error48.code === "CHECKPOINT_WRITE_FAILED") {
65981
+ logger?.warn("story-orchestrator", "recordGreen failed \u2014 resume checkpoint lost, story verdict unaffected", {
65982
+ storyId: this.ctx.storyId,
65983
+ phase: name,
65984
+ error: errorMessage(error48)
65985
+ });
65986
+ } else {
65987
+ throw error48;
65988
+ }
65989
+ }
65609
65990
  }
65610
65991
  }
65611
65992
  const gateName = this.state.fullSuiteGate?.slot.op.name;
@@ -65789,6 +66170,7 @@ class ExecutionPlan {
65789
66170
  }
65790
66171
  }
65791
66172
  var init_execution_plan = __esm(() => {
66173
+ init_errors();
65792
66174
  init_logger2();
65793
66175
  init_non_blocking_fix();
65794
66176
  init_phase_eval();
@@ -68221,12 +68603,12 @@ async function regenerateAcceptanceTest(testPath, acceptanceContext) {
68221
68603
  const content = await Bun.file(testPath).text();
68222
68604
  await Bun.write(bakPath, content);
68223
68605
  logger?.info("acceptance", `Backed up acceptance test -> ${bakPath}`);
68224
- const { unlink: unlink4 } = await import("fs/promises");
68225
- await unlink4(testPath);
68606
+ const { unlink: unlink5 } = await import("fs/promises");
68607
+ await unlink5(testPath);
68226
68608
  if (acceptanceContext.featureDir) {
68227
68609
  const metaPath = path14.join(acceptanceContext.featureDir, "acceptance-meta.json");
68228
68610
  try {
68229
- await unlink4(metaPath);
68611
+ await unlink5(metaPath);
68230
68612
  } catch {}
68231
68613
  }
68232
68614
  let implementationContext;
@@ -68694,7 +69076,7 @@ var init_acceptance_loop = __esm(() => {
68694
69076
 
68695
69077
  // src/session/scratch-purge.ts
68696
69078
  import { mkdir as mkdir9, rename as rename3, rm as rm3 } from "fs/promises";
68697
- import { dirname as dirname12, join as join63 } from "path";
69079
+ import { dirname as dirname13, join as join63 } from "path";
68698
69080
  async function purgeStaleScratch(projectDir, featureName, retentionDays, archiveInsteadOfDelete = false) {
68699
69081
  const sessionsDir = join63(featureDir(projectDir, featureName), "sessions");
68700
69082
  const sessionIds = await _scratchPurgeDeps.listSessionDirs(sessionsDir);
@@ -68746,7 +69128,7 @@ var init_scratch_purge = __esm(() => {
68746
69128
  readFile: (path15) => Bun.file(path15).text(),
68747
69129
  remove: (path15) => rm3(path15, { recursive: true, force: true }),
68748
69130
  move: async (src, dest) => {
68749
- await mkdir9(dirname12(dest), { recursive: true });
69131
+ await mkdir9(dirname13(dest), { recursive: true });
68750
69132
  await rename3(src, dest);
68751
69133
  },
68752
69134
  now: () => Date.now()
@@ -69433,6 +69815,7 @@ async function handleRunCompletion(options) {
69433
69815
  clearLanguageCache();
69434
69816
  clearWorkspaceCache();
69435
69817
  clearGitRootCache();
69818
+ _resetCanonicalRulesCache();
69436
69819
  const finalCounts = countStories(prd);
69437
69820
  const fallbackAggregate = deriveRunFallbackAggregates(allStoryMetrics);
69438
69821
  pipelineEventBus.emit({
@@ -69525,7 +69908,7 @@ async function handleRunCompletion(options) {
69525
69908
  });
69526
69909
  statusWriter.setPrd(prd);
69527
69910
  statusWriter.setCurrentStory(null);
69528
- statusWriter.setRunStatus(regressionGateFailed ? "failed" : exitReason === "cost-limit" ? "cost-limit" : isComplete(prd) ? "completed" : isStalled(prd, config2.execution.rectification?.maxAttemptsTotal) ? "stalled" : "running");
69911
+ statusWriter.setRunStatus(regressionGateFailed ? "failed" : exitReason === "cost-limit" ? "cost-limit" : isComplete(prd) ? "completed" : isStalled(prd, config2.execution.rectification?.maxAttemptsTotal) ? "stalled" : "aborted");
69529
69912
  await statusWriter.update(reportedTotal, iterations);
69530
69913
  return {
69531
69914
  durationMs,
@@ -69978,9 +70361,9 @@ var init_ensure_package_dirs = __esm(() => {
69978
70361
  init_logger2();
69979
70362
  _ensurePackageDirsDeps = {
69980
70363
  exists: async (p) => {
69981
- const { stat: stat2 } = await import("fs/promises");
70364
+ const { stat: stat4 } = await import("fs/promises");
69982
70365
  try {
69983
- return (await stat2(p)).isDirectory();
70366
+ return (await stat4(p)).isDirectory();
69984
70367
  } catch {
69985
70368
  return false;
69986
70369
  }
@@ -69994,10 +70377,10 @@ var init_ensure_package_dirs = __esm(() => {
69994
70377
 
69995
70378
  // src/pipeline/subscribers/events-writer.ts
69996
70379
  import { appendFile as appendFile4, mkdir as mkdir10 } from "fs/promises";
69997
- import { basename as basename12, join as join64 } from "path";
70380
+ import { basename as basename13, join as join64 } from "path";
69998
70381
  function wireEventsWriter(bus, feature, runId, workdir) {
69999
70382
  const logger = getSafeLogger();
70000
- const project = basename12(workdir);
70383
+ const project = basename13(workdir);
70001
70384
  const eventsDir = join64(getEventsRootDir(), project);
70002
70385
  const eventsFile = join64(eventsDir, "events.jsonl");
70003
70386
  let dirReady = false;
@@ -70180,10 +70563,10 @@ var init_interaction2 = __esm(() => {
70180
70563
 
70181
70564
  // src/pipeline/subscribers/registry.ts
70182
70565
  import { mkdir as mkdir11, writeFile } from "fs/promises";
70183
- import { basename as basename13, join as join65 } from "path";
70566
+ import { basename as basename14, join as join65 } from "path";
70184
70567
  function wireRegistry(bus, feature, runId, workdir, outputDir) {
70185
70568
  const logger = getSafeLogger();
70186
- const project = basename13(workdir);
70569
+ const project = basename14(workdir);
70187
70570
  const runDir = join65(getRunsDir(), `${project}-${feature}-${runId}`);
70188
70571
  const metaFile = join65(runDir, "meta.json");
70189
70572
  const unsub = bus.on("run:started", (_ev) => {
@@ -71188,12 +71571,7 @@ ${missing.join(`
71188
71571
  }
71189
71572
  async hasWorktreeRecord(projectRoot, branchName) {
71190
71573
  try {
71191
- const proc = _managerDeps.spawn(["git", "worktree", "list", "--porcelain"], {
71192
- cwd: projectRoot,
71193
- stdout: "pipe",
71194
- stderr: "pipe"
71195
- });
71196
- const [exitCode, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
71574
+ const { stdout, exitCode } = await gitWithTimeout(["worktree", "list", "--porcelain"], projectRoot);
71197
71575
  if (exitCode !== 0)
71198
71576
  return false;
71199
71577
  const targetBranch = `refs/heads/${branchName}`;
@@ -71209,12 +71587,7 @@ ${missing.join(`
71209
71587
  const branchName = `nax/${storyId}`;
71210
71588
  const hadWorktreeRecord = await this.hasWorktreeRecord(projectRoot, branchName);
71211
71589
  try {
71212
- const pruneProc = _managerDeps.spawn(["git", "worktree", "prune"], {
71213
- cwd: projectRoot,
71214
- stdout: "pipe",
71215
- stderr: "pipe"
71216
- });
71217
- await pruneProc.exited;
71590
+ await gitWithTimeout(["worktree", "prune"], projectRoot);
71218
71591
  } catch {}
71219
71592
  let removedLiveWorktree = false;
71220
71593
  try {
@@ -71223,21 +71596,11 @@ ${missing.join(`
71223
71596
  } catch {}
71224
71597
  if (!removedLiveWorktree && hadWorktreeRecord) {
71225
71598
  try {
71226
- const branchProc = _managerDeps.spawn(["git", "branch", "-D", branchName], {
71227
- cwd: projectRoot,
71228
- stdout: "pipe",
71229
- stderr: "pipe"
71230
- });
71231
- await branchProc.exited;
71599
+ await gitWithTimeout(["branch", "-D", branchName], projectRoot);
71232
71600
  } catch {}
71233
71601
  }
71234
71602
  try {
71235
- const proc = _managerDeps.spawn(["git", "worktree", "add", worktreePath, "-b", branchName], {
71236
- cwd: projectRoot,
71237
- stdout: "pipe",
71238
- stderr: "pipe"
71239
- });
71240
- const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
71603
+ const { exitCode, stderr } = await gitWithTimeout(["worktree", "add", worktreePath, "-b", branchName], projectRoot);
71241
71604
  if (exitCode !== 0) {
71242
71605
  throw new NaxError(`Failed to create worktree: ${stderr || "unknown error"}`, "WORKTREE_ERROR", {
71243
71606
  stage: "worktree",
@@ -71292,12 +71655,7 @@ ${missing.join(`
71292
71655
  const worktreePath = join67(projectRoot, ".nax-wt", storyId);
71293
71656
  const branchName = `nax/${storyId}`;
71294
71657
  try {
71295
- const proc = _managerDeps.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
71296
- cwd: projectRoot,
71297
- stdout: "pipe",
71298
- stderr: "pipe"
71299
- });
71300
- const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
71658
+ const { exitCode, stderr } = await gitWithTimeout(["worktree", "remove", worktreePath, "--force"], projectRoot);
71301
71659
  if (exitCode !== 0) {
71302
71660
  if (stderr.includes("not found") || stderr.includes("does not exist") || stderr.includes("no such worktree") || stderr.includes("is not a working tree")) {
71303
71661
  throw new NaxError(`Worktree not found: ${worktreePath}`, "WORKTREE_ERROR", {
@@ -71325,12 +71683,7 @@ ${missing.join(`
71325
71683
  });
71326
71684
  }
71327
71685
  try {
71328
- const proc = _managerDeps.spawn(["git", "branch", "-D", branchName], {
71329
- cwd: projectRoot,
71330
- stdout: "pipe",
71331
- stderr: "pipe"
71332
- });
71333
- const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
71686
+ const { exitCode, stderr } = await gitWithTimeout(["branch", "-D", branchName], projectRoot);
71334
71687
  if (exitCode !== 0) {
71335
71688
  if (!stderr.includes("not found")) {
71336
71689
  const logger = getSafeLogger();
@@ -71346,16 +71699,7 @@ ${missing.join(`
71346
71699
  }
71347
71700
  async list(projectRoot) {
71348
71701
  try {
71349
- const proc = _managerDeps.spawn(["git", "worktree", "list", "--porcelain"], {
71350
- cwd: projectRoot,
71351
- stdout: "pipe",
71352
- stderr: "pipe"
71353
- });
71354
- const [exitCode, stderr, stdout] = await Promise.all([
71355
- proc.exited,
71356
- new Response(proc.stderr).text(),
71357
- new Response(proc.stdout).text()
71358
- ]);
71702
+ const { stdout, stderr, exitCode } = await gitWithTimeout(["worktree", "list", "--porcelain"], projectRoot);
71359
71703
  if (exitCode !== 0) {
71360
71704
  throw new NaxError(`Failed to list worktrees: ${stderr || "unknown error"}`, "WORKTREE_ERROR", {
71361
71705
  stage: "worktree",
@@ -71398,16 +71742,12 @@ ${missing.join(`
71398
71742
  return worktrees;
71399
71743
  }
71400
71744
  }
71401
- var _managerDeps;
71402
71745
  var init_manager3 = __esm(() => {
71403
71746
  init_errors();
71404
71747
  init_logger2();
71405
71748
  init_validate();
71406
- init_bun_deps();
71749
+ init_git();
71407
71750
  init_gitignore();
71408
- _managerDeps = {
71409
- spawn
71410
- };
71411
71751
  });
71412
71752
 
71413
71753
  // src/execution/dry-run.ts
@@ -71467,12 +71807,8 @@ class MergeEngine {
71467
71807
  this.worktreeManager = worktreeManager;
71468
71808
  }
71469
71809
  async isMidMerge(projectRoot) {
71470
- const proc = _mergeDeps.spawn(["git", "rev-parse", "-q", "--verify", "MERGE_HEAD"], {
71471
- cwd: projectRoot,
71472
- stdout: "pipe",
71473
- stderr: "pipe"
71474
- });
71475
- return await proc.exited === 0;
71810
+ const { exitCode } = await gitWithTimeout(["rev-parse", "-q", "--verify", "MERGE_HEAD"], projectRoot);
71811
+ return exitCode === 0;
71476
71812
  }
71477
71813
  async merge(projectRoot, storyId) {
71478
71814
  const branchName = `nax/${storyId}`;
@@ -71485,16 +71821,7 @@ class MergeEngine {
71485
71821
  });
71486
71822
  return { success: false, failureKind: "error", error: error48 };
71487
71823
  }
71488
- const mergeProc = _mergeDeps.spawn(["git", "merge", "--no-ff", branchName, "-m", `Merge branch '${branchName}'`], {
71489
- cwd: projectRoot,
71490
- stdout: "pipe",
71491
- stderr: "pipe"
71492
- });
71493
- const [exitCode, stderr, stdout] = await Promise.all([
71494
- mergeProc.exited,
71495
- new Response(mergeProc.stderr).text(),
71496
- new Response(mergeProc.stdout).text()
71497
- ]);
71824
+ const { exitCode, stderr, stdout } = await gitWithTimeout(["merge", "--no-ff", branchName, "-m", `Merge branch '${branchName}'`], projectRoot);
71498
71825
  if (exitCode === 0) {
71499
71826
  try {
71500
71827
  await this.worktreeManager.remove(projectRoot, storyId);
@@ -71650,36 +71977,15 @@ ${stderr}`);
71650
71977
  async rebaseWorktree(projectRoot, storyId) {
71651
71978
  const worktreePath = `${projectRoot}/.nax-wt/${storyId}`;
71652
71979
  try {
71653
- const currentBranchProc = _mergeDeps.spawn(["git", "rev-parse", "--abbrev-ref", "HEAD"], {
71654
- cwd: projectRoot,
71655
- stdout: "pipe",
71656
- stderr: "pipe"
71657
- });
71658
- const [exitCode, currentBranchRaw] = await Promise.all([
71659
- currentBranchProc.exited,
71660
- new Response(currentBranchProc.stdout).text()
71661
- ]);
71980
+ const { exitCode, stdout: currentBranchRaw } = await gitWithTimeout(["rev-parse", "--abbrev-ref", "HEAD"], projectRoot);
71662
71981
  if (exitCode !== 0) {
71663
71982
  throw new Error("Failed to get current branch");
71664
71983
  }
71665
71984
  const currentBranch = currentBranchRaw.trim();
71666
- const rebaseProc = _mergeDeps.spawn(["git", "rebase", currentBranch], {
71667
- cwd: worktreePath,
71668
- stdout: "pipe",
71669
- stderr: "pipe"
71670
- });
71671
- const [rebaseExitCode, rebaseStderr] = await Promise.all([
71672
- rebaseProc.exited,
71673
- new Response(rebaseProc.stderr).text()
71674
- ]);
71985
+ const { exitCode: rebaseExitCode, stderr: rebaseStderr } = await gitWithTimeout(["rebase", currentBranch], worktreePath);
71675
71986
  if (rebaseExitCode !== 0) {
71676
71987
  const stderr = rebaseStderr;
71677
- const abortProc = _mergeDeps.spawn(["git", "rebase", "--abort"], {
71678
- cwd: worktreePath,
71679
- stdout: "pipe",
71680
- stderr: "pipe"
71681
- });
71682
- await abortProc.exited;
71988
+ await gitWithTimeout(["rebase", "--abort"], worktreePath);
71683
71989
  throw new Error(`Rebase failed: ${stderr || "unknown error"}`);
71684
71990
  }
71685
71991
  } catch (error48) {
@@ -71691,12 +71997,7 @@ ${stderr}`);
71691
71997
  }
71692
71998
  async getConflictFiles(projectRoot) {
71693
71999
  try {
71694
- const proc = _mergeDeps.spawn(["git", "diff", "--name-only", "--diff-filter=U"], {
71695
- cwd: projectRoot,
71696
- stdout: "pipe",
71697
- stderr: "pipe"
71698
- });
71699
- const [exitCode, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
72000
+ const { stdout, exitCode } = await gitWithTimeout(["diff", "--name-only", "--diff-filter=U"], projectRoot);
71700
72001
  if (exitCode !== 0) {
71701
72002
  return [];
71702
72003
  }
@@ -71708,12 +72009,7 @@ ${stderr}`);
71708
72009
  }
71709
72010
  async abortMerge(projectRoot) {
71710
72011
  try {
71711
- const proc = _mergeDeps.spawn(["git", "merge", "--abort"], {
71712
- cwd: projectRoot,
71713
- stdout: "pipe",
71714
- stderr: "pipe"
71715
- });
71716
- const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
72012
+ const { exitCode, stderr } = await gitWithTimeout(["merge", "--abort"], projectRoot);
71717
72013
  if (exitCode !== 0) {
71718
72014
  getSafeLogger()?.error("worktree", "Failed to abort merge", {
71719
72015
  exitCode,
@@ -71730,20 +72026,15 @@ ${stderr}`);
71730
72026
  }
71731
72027
  }
71732
72028
  }
71733
- var _mergeDeps;
71734
72029
  var init_merge = __esm(() => {
71735
72030
  init_logger2();
71736
- init_bun_deps();
71737
- _mergeDeps = {
71738
- spawn
71739
- };
72031
+ init_git();
71740
72032
  });
71741
72033
 
71742
72034
  // src/worktree/index.ts
71743
72035
  var exports_worktree = {};
71744
72036
  __export(exports_worktree, {
71745
72037
  prepareWorktreeDependencies: () => prepareWorktreeDependencies,
71746
- _mergeDeps: () => _mergeDeps,
71747
72038
  WorktreeManager: () => WorktreeManager,
71748
72039
  WorktreeDependencyPreparationError: () => WorktreeDependencyPreparationError,
71749
72040
  MergeEngine: () => MergeEngine
@@ -72018,7 +72309,7 @@ async function removeWorktreeDirectory(projectRoot, storyId) {
72018
72309
  stdout: "pipe",
72019
72310
  stderr: "pipe"
72020
72311
  });
72021
- const [exitCode, stderr] = await Promise.all([
72312
+ const [exitCode, stdout, stderr] = await Promise.all([
72022
72313
  proc.exited,
72023
72314
  new Response(proc.stdout).text().catch(() => ""),
72024
72315
  new Response(proc.stderr).text().catch(() => "")
@@ -73541,8 +73832,9 @@ function detectForge(remoteUrl) {
73541
73832
  async function hasOpenPr(forge, branch, deps, cwd) {
73542
73833
  const cmd = forge === "github" ? ["gh", "pr", "list", "--head", branch, "--state", "open", "--json", "number"] : ["glab", "mr", "list", "--source-branch", branch, "--state", "opened", "--output", "json"];
73543
73834
  const result = await deps.run(cmd, { cwd });
73544
- if (result.exitCode !== 0)
73545
- return false;
73835
+ if (result.exitCode !== 0) {
73836
+ throw new Error(`hasOpenPr: forge CLI exited with code ${result.exitCode}: ${result.stderr.trim()}`);
73837
+ }
73546
73838
  try {
73547
73839
  const parsed = JSON.parse(result.stdout);
73548
73840
  return Array.isArray(parsed) && parsed.length > 0;
@@ -73790,12 +74082,27 @@ var init_template = __esm(() => {
73790
74082
  import * as path20 from "path";
73791
74083
  async function defaultRun(cmd, opts) {
73792
74084
  const proc = Bun.spawn(cmd, { cwd: opts.cwd, stdout: "pipe", stderr: "pipe" });
73793
- const [exitCode, stdout, stderr] = await Promise.all([
73794
- proc.exited,
73795
- new Response(proc.stdout).text(),
73796
- new Response(proc.stderr).text()
73797
- ]);
73798
- return { exitCode, stdout, stderr };
74085
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_SUBPROCESS_TIMEOUT_MS;
74086
+ let timedOut = false;
74087
+ const timer = setTimeout(() => {
74088
+ timedOut = true;
74089
+ proc.kill();
74090
+ }, timeoutMs);
74091
+ try {
74092
+ const [exitCode, stdout, stderr] = await Promise.all([
74093
+ proc.exited,
74094
+ new Response(proc.stdout).text(),
74095
+ new Response(proc.stderr).text()
74096
+ ]);
74097
+ return timedOut ? {
74098
+ exitCode: exitCode === 0 ? 124 : exitCode,
74099
+ stdout,
74100
+ stderr: `${stderr}
74101
+ [auto-pr] command killed after ${timeoutMs}ms timeout`
74102
+ } : { exitCode, stdout, stderr };
74103
+ } finally {
74104
+ clearTimeout(timer);
74105
+ }
73799
74106
  }
73800
74107
  async function defaultReadText(path21) {
73801
74108
  const file3 = Bun.file(path21);
@@ -73845,7 +74152,7 @@ function toPrBodyContext(context) {
73845
74152
  stories: context.stories
73846
74153
  };
73847
74154
  }
73848
- var PLUGIN_NAME = "nax-auto-pr", PLUGIN_VERSION = "0.1.0", GIT_REMOTE_CMD, _autoPrDeps, autoPrAction, autoPrPlugin;
74155
+ var PLUGIN_NAME = "nax-auto-pr", PLUGIN_VERSION = "0.1.0", GIT_REMOTE_CMD, DEFAULT_SUBPROCESS_TIMEOUT_MS = 30000, _autoPrDeps, autoPrAction, autoPrPlugin;
73849
74156
  var init_auto_pr = __esm(() => {
73850
74157
  init_forge();
73851
74158
  init_pr_body();
@@ -73911,6 +74218,23 @@ var init_auto_pr = __esm(() => {
73911
74218
  const message = pushResult.stderr.trim() || `git push exited with code ${pushResult.exitCode}`;
73912
74219
  return { success: false, message: `Failed to push branch "${context.branch}" to origin: ${message}` };
73913
74220
  }
74221
+ let existsAfterPush = false;
74222
+ try {
74223
+ existsAfterPush = await _autoPrDeps.hasOpenPr(forge, context.branch, { run: _autoPrDeps.run, readText: _autoPrDeps.readText }, context.workdir);
74224
+ } catch (checkErr) {
74225
+ context.logger.warn("Auto-PR re-check inconclusive \u2014 skipping to avoid duplicate PR", {
74226
+ branch: context.branch,
74227
+ error: String(checkErr)
74228
+ });
74229
+ return { success: false, message: `Auto-PR skipped: re-check inconclusive (${String(checkErr)})` };
74230
+ }
74231
+ if (existsAfterPush) {
74232
+ context.logger.warn("Auto-PR skipped \u2014 open PR/MR appeared during push (concurrent run?)", {
74233
+ branch: context.branch,
74234
+ forge
74235
+ });
74236
+ return { success: false, message: `Open PR/MR already exists for branch "${context.branch}"` };
74237
+ }
73914
74238
  const template = await _autoPrDeps.findPrTemplate(context.workdir, forge, {
73915
74239
  run: _autoPrDeps.run,
73916
74240
  readText: _autoPrDeps.readText
@@ -74157,8 +74481,8 @@ async function writtenThisRun(filePath, runStartedAt) {
74157
74481
  if (runStartedAt === undefined)
74158
74482
  return true;
74159
74483
  try {
74160
- const stat2 = await Bun.file(filePath).stat();
74161
- return stat2.mtimeMs >= runStartedAt;
74484
+ const stat4 = await Bun.file(filePath).stat();
74485
+ return stat4.mtimeMs >= runStartedAt;
74162
74486
  } catch {
74163
74487
  return true;
74164
74488
  }
@@ -75168,14 +75492,23 @@ function buildEscalationMessage(feature, reason, findings) {
75168
75492
  \u2026and ${omitted} more` : ""}`;
75169
75493
  }
75170
75494
  async function sendTelegramNotify(cfg, text) {
75171
- const res = await _telegramDeps.fetch(`https://api.telegram.org/bot${cfg.token}/sendMessage`, {
75172
- method: "POST",
75173
- headers: { "content-type": "application/json" },
75174
- body: JSON.stringify({ chat_id: cfg.chatId, text })
75175
- });
75176
- return res.ok;
75495
+ const controller = new AbortController;
75496
+ const timer = setTimeout(() => controller.abort(), NOTIFY_FETCH_TIMEOUT_MS);
75497
+ try {
75498
+ const res = await _telegramDeps.fetch(`https://api.telegram.org/bot${cfg.token}/sendMessage`, {
75499
+ method: "POST",
75500
+ headers: { "content-type": "application/json" },
75501
+ body: JSON.stringify({ chat_id: cfg.chatId, text }),
75502
+ signal: controller.signal
75503
+ });
75504
+ return res.ok;
75505
+ } catch {
75506
+ return false;
75507
+ } finally {
75508
+ clearTimeout(timer);
75509
+ }
75177
75510
  }
75178
- var TELEGRAM_MAX_MESSAGE_CHARS = 4096, MAX_FINDING_TITLE_CHARS = 120, _telegramDeps;
75511
+ var TELEGRAM_MAX_MESSAGE_CHARS = 4096, MAX_FINDING_TITLE_CHARS = 120, _telegramDeps, NOTIFY_FETCH_TIMEOUT_MS = 5000;
75179
75512
  var init_telegram2 = __esm(() => {
75180
75513
  _telegramDeps = { fetch: (...a) => fetch(...a) };
75181
75514
  });
@@ -75410,6 +75743,8 @@ var PLUGIN_NAME4 = "nax-finish", PLUGIN_VERSION4 = "0.1.0", PACKAGE_ROOT_SEARCH_
75410
75743
  var init_nax_finish = __esm(() => {
75411
75744
  init_config2();
75412
75745
  init_telegram2();
75746
+ init_telegram2();
75747
+ init_telegram2();
75413
75748
  _naxFinishDeps = {
75414
75749
  run: defaultRun2,
75415
75750
  readResult: defaultReadResult,
@@ -75511,6 +75846,7 @@ function createBatchQueue(opts) {
75511
75846
  let overflowing = false;
75512
75847
  let tornDown = false;
75513
75848
  let timer;
75849
+ const inFlightSends = new Set;
75514
75850
  const armTimer = () => {
75515
75851
  timer = setTimeout(() => {
75516
75852
  doFlush();
@@ -75534,7 +75870,8 @@ function createBatchQueue(opts) {
75534
75870
  return Promise.resolve();
75535
75871
  const batch = queue;
75536
75872
  queue = [];
75537
- sendWithRetry(batch);
75873
+ const sendPromise = sendWithRetry(batch).finally(() => inFlightSends.delete(sendPromise));
75874
+ inFlightSends.add(sendPromise);
75538
75875
  return Promise.resolve();
75539
75876
  };
75540
75877
  const enqueue = (item) => {
@@ -75556,7 +75893,10 @@ function createBatchQueue(opts) {
75556
75893
  armTimer();
75557
75894
  return {
75558
75895
  enqueue,
75559
- flushNow: () => doFlush(),
75896
+ flushNow: async () => {
75897
+ await doFlush();
75898
+ await Promise.all(inFlightSends);
75899
+ },
75560
75900
  teardown: () => {
75561
75901
  tornDown = true;
75562
75902
  if (timer !== undefined)
@@ -76812,11 +77152,11 @@ function getSafeLogger6() {
76812
77152
  return getSafeLogger();
76813
77153
  }
76814
77154
  function extractPluginName(pluginPath) {
76815
- const basename15 = path25.basename(pluginPath);
76816
- if (basename15 === "index.ts" || basename15 === "index.js" || basename15 === "index.mjs") {
77155
+ const basename16 = path25.basename(pluginPath);
77156
+ if (basename16 === "index.ts" || basename16 === "index.js" || basename16 === "index.mjs") {
76817
77157
  return path25.basename(path25.dirname(pluginPath));
76818
77158
  }
76819
- return basename15.replace(/\.(ts|js|mjs)$/, "");
77159
+ return basename16.replace(/\.(ts|js|mjs)$/, "");
76820
77160
  }
76821
77161
  async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, disabledPlugins, isTestFileFn, reporters) {
76822
77162
  const loadedPlugins = [];
@@ -77087,7 +77427,7 @@ var init_loader4 = __esm(() => {
77087
77427
  });
77088
77428
 
77089
77429
  // src/execution/status-file.ts
77090
- import { rename as rename4, unlink as unlink4 } from "fs/promises";
77430
+ import { rename as rename4, unlink as unlink5 } from "fs/promises";
77091
77431
  import { resolve as resolve20 } from "path";
77092
77432
  function countProgress(prd) {
77093
77433
  const stories = prd.userStories;
@@ -77139,7 +77479,7 @@ async function writeStatusFile(filePath, status) {
77139
77479
  }
77140
77480
  const tmpPath = `${resolvedPath}.tmp`;
77141
77481
  try {
77142
- await unlink4(tmpPath);
77482
+ await unlink5(tmpPath);
77143
77483
  } catch {}
77144
77484
  await Bun.write(tmpPath, JSON.stringify(status, null, 2));
77145
77485
  await rename4(tmpPath, resolvedPath);
@@ -77300,7 +77640,7 @@ __export(exports_init_context, {
77300
77640
  generatePackageContextTemplate: () => generatePackageContextTemplate,
77301
77641
  generateContextTemplate: () => generateContextTemplate
77302
77642
  });
77303
- import { basename as basename15, join as join78 } from "path";
77643
+ import { basename as basename16, join as join78 } from "path";
77304
77644
  async function bunFileExists(path26) {
77305
77645
  return Bun.file(path26).exists();
77306
77646
  }
@@ -77395,7 +77735,7 @@ async function scanProject(projectRoot) {
77395
77735
  const readmeSnippet = await readReadmeSnippet(projectRoot);
77396
77736
  const entryPoints = await detectEntryPoints(projectRoot);
77397
77737
  const configFiles = await detectConfigFiles(projectRoot);
77398
- const projectName = packageManifest?.name || basename15(projectRoot);
77738
+ const projectName = packageManifest?.name || basename16(projectRoot);
77399
77739
  return {
77400
77740
  projectName,
77401
77741
  fileTree,
@@ -78059,7 +78399,7 @@ __export(exports_migrate, {
78059
78399
  detectGeneratedContent: () => detectGeneratedContent
78060
78400
  });
78061
78401
  import { existsSync as existsSync25 } from "fs";
78062
- import { mkdir as mkdir16, readdir as readdir3, rename as rename5 } from "fs/promises";
78402
+ import { mkdir as mkdir16, readdir as readdir4, rename as rename5 } from "fs/promises";
78063
78403
  import path26 from "path";
78064
78404
  async function detectGeneratedContent(naxDir) {
78065
78405
  if (!existsSync25(naxDir))
@@ -78067,7 +78407,7 @@ async function detectGeneratedContent(naxDir) {
78067
78407
  const candidates = [];
78068
78408
  let entries = [];
78069
78409
  try {
78070
- entries = await readdir3(naxDir);
78410
+ entries = await readdir4(naxDir);
78071
78411
  } catch {
78072
78412
  return [];
78073
78413
  }
@@ -78080,13 +78420,13 @@ async function detectGeneratedContent(naxDir) {
78080
78420
  if (existsSync25(featuresDir2)) {
78081
78421
  let featureDirs = [];
78082
78422
  try {
78083
- featureDirs = await readdir3(featuresDir2);
78423
+ featureDirs = await readdir4(featuresDir2);
78084
78424
  } catch {}
78085
78425
  for (const fid of featureDirs) {
78086
78426
  const featureDir2 = path26.join(featuresDir2, fid);
78087
78427
  let subEntries = [];
78088
78428
  try {
78089
- subEntries = await readdir3(featureDir2);
78429
+ subEntries = await readdir4(featureDir2);
78090
78430
  } catch {
78091
78431
  continue;
78092
78432
  }
@@ -78101,7 +78441,7 @@ async function detectGeneratedContent(naxDir) {
78101
78441
  const storiesDir = path26.join(featureDir2, "stories");
78102
78442
  let storyDirs = [];
78103
78443
  try {
78104
- storyDirs = await readdir3(storiesDir);
78444
+ storyDirs = await readdir4(storiesDir);
78105
78445
  } catch {
78106
78446
  continue;
78107
78447
  }
@@ -78109,7 +78449,7 @@ async function detectGeneratedContent(naxDir) {
78109
78449
  const storyDir = path26.join(storiesDir, sid);
78110
78450
  let storyEntries = [];
78111
78451
  try {
78112
- storyEntries = await readdir3(storyDir);
78452
+ storyEntries = await readdir4(storyDir);
78113
78453
  } catch {
78114
78454
  continue;
78115
78455
  }
@@ -78877,6 +79217,7 @@ async function setupRun(options) {
78877
79217
  if (!lockAcquired) {
78878
79218
  logger?.error("execution", "Another nax process is already running in this directory");
78879
79219
  logger?.error("execution", "If you believe this is an error, remove nax.lock manually");
79220
+ cleanupCrashHandlers();
78880
79221
  throw new LockAcquisitionError(workdir);
78881
79222
  }
78882
79223
  try {
@@ -79477,19 +79818,19 @@ function parseQueueFile(content) {
79477
79818
  var init_queue = () => {};
79478
79819
 
79479
79820
  // src/utils/queue-file-lock.ts
79480
- import { randomUUID as randomUUID6 } from "crypto";
79481
- import { open, readdir as readdir4, stat as stat2, unlink as unlink5 } from "fs/promises";
79482
- import { basename as basename16, dirname as dirname17 } from "path";
79483
- function buildCandidatePath(queuePath) {
79484
- const time3 = _queueLockDeps.now().toString().padStart(LOCK_TIME_WIDTH, "0");
79821
+ import { randomUUID as randomUUID7 } from "crypto";
79822
+ import { open as open2, readdir as readdir5, stat as stat4, unlink as unlink6 } from "fs/promises";
79823
+ import { basename as basename17, dirname as dirname18 } from "path";
79824
+ function buildCandidatePath2(queuePath) {
79825
+ const time3 = _queueLockDeps.now().toString().padStart(LOCK_TIME_WIDTH2, "0");
79485
79826
  return `${queuePath}.lock.${time3}.${process.pid}.${_queueLockDeps.randomUUID()}`;
79486
79827
  }
79487
- function candidatePid(fileName) {
79828
+ function candidatePid2(fileName) {
79488
79829
  const segments = fileName.split(".");
79489
79830
  const pid = Number(segments.at(-2));
79490
79831
  return Number.isInteger(pid) && pid > 0 ? pid : null;
79491
79832
  }
79492
- function candidateTime(fileName) {
79833
+ function candidateTime2(fileName) {
79493
79834
  const segments = fileName.split(".");
79494
79835
  try {
79495
79836
  const timestamp = Number(segments.at(-3));
@@ -79505,14 +79846,14 @@ function isLiveCandidate(pid, createdAt) {
79505
79846
  return false;
79506
79847
  return _queueLockDeps.isPidAlive(pid);
79507
79848
  }
79508
- async function listLiveCandidates(queuePath) {
79509
- const directory = dirname17(queuePath);
79510
- const prefix = `${basename16(queuePath)}.lock.`;
79849
+ async function listLiveCandidates2(queuePath) {
79850
+ const directory = dirname18(queuePath);
79851
+ const prefix = `${basename17(queuePath)}.lock.`;
79511
79852
  const candidates = (await _queueLockDeps.readdir(directory)).filter((name) => name.startsWith(prefix));
79512
79853
  const live = [];
79513
79854
  for (const candidate of candidates) {
79514
- const pid = candidatePid(candidate);
79515
- const createdAt = candidateTime(candidate);
79855
+ const pid = candidatePid2(candidate);
79856
+ const createdAt = candidateTime2(candidate);
79516
79857
  const candidatePath = `${directory}/${candidate}`;
79517
79858
  if (isLiveCandidate(pid, createdAt)) {
79518
79859
  const stats = await _queueLockDeps.stat(candidatePath).catch(() => null);
@@ -79523,14 +79864,14 @@ async function listLiveCandidates(queuePath) {
79523
79864
  }
79524
79865
  return live.sort((a, b) => a.createdAt - b.createdAt || a.name.localeCompare(b.name)).map(({ name }) => name);
79525
79866
  }
79526
- async function acquire(queuePath) {
79527
- const candidatePath = buildCandidatePath(queuePath);
79867
+ async function acquire2(queuePath) {
79868
+ const candidatePath = buildCandidatePath2(queuePath);
79528
79869
  const handle = await _queueLockDeps.open(candidatePath, "wx");
79529
79870
  await handle.close();
79530
79871
  const deadline = Date.now() + LOCK_TIMEOUT_MS;
79531
79872
  while (Date.now() < deadline) {
79532
- const candidates = await listLiveCandidates(queuePath);
79533
- if (candidates[0] === basename16(candidatePath)) {
79873
+ const candidates = await listLiveCandidates2(queuePath);
79874
+ if (candidates[0] === basename17(candidatePath)) {
79534
79875
  return () => _queueLockDeps.unlink(candidatePath).catch(() => {});
79535
79876
  }
79536
79877
  await _queueLockDeps.sleep(LOCK_RETRY_MS);
@@ -79539,21 +79880,21 @@ async function acquire(queuePath) {
79539
79880
  throw new Error(`[queue] Timed out acquiring queue lock: ${queuePath}`);
79540
79881
  }
79541
79882
  async function withQueueFileLock(queuePath, operation) {
79542
- const release = await acquire(queuePath);
79883
+ const release = await acquire2(queuePath);
79543
79884
  try {
79544
79885
  return await operation();
79545
79886
  } finally {
79546
79887
  await release();
79547
79888
  }
79548
79889
  }
79549
- var LOCK_RETRY_MS = 10, LOCK_TIMEOUT_MS = 5000, LOCK_TIME_WIDTH = 13, _queueLockDeps;
79890
+ var LOCK_RETRY_MS = 10, LOCK_TIMEOUT_MS = 5000, LOCK_TIME_WIDTH2 = 13, _queueLockDeps;
79550
79891
  var init_queue_file_lock = __esm(() => {
79551
79892
  _queueLockDeps = {
79552
- open,
79553
- readdir: readdir4,
79554
- stat: stat2,
79555
- unlink: unlink5,
79556
- randomUUID: randomUUID6,
79893
+ open: open2,
79894
+ readdir: readdir5,
79895
+ stat: stat4,
79896
+ unlink: unlink6,
79897
+ randomUUID: randomUUID7,
79557
79898
  now: () => Date.now(),
79558
79899
  sleep: (ms) => new Promise((resolve21) => setTimeout(resolve21, ms)),
79559
79900
  isPidAlive: isProcessAlive
@@ -79561,7 +79902,7 @@ var init_queue_file_lock = __esm(() => {
79561
79902
  });
79562
79903
 
79563
79904
  // src/execution/queue-handler.ts
79564
- import { rename as rename6, unlink as unlink6 } from "fs/promises";
79905
+ import { rename as rename6, unlink as unlink7 } from "fs/promises";
79565
79906
  import path29 from "path";
79566
79907
  function getSafeLogger7() {
79567
79908
  try {
@@ -79619,7 +79960,12 @@ async function processQueueFile(workdir, processor) {
79619
79960
  if (commands === null)
79620
79961
  return;
79621
79962
  const result = await processor(commands);
79622
- await unlink6(processingPath).catch(() => {});
79963
+ await unlink7(processingPath).catch((error48) => {
79964
+ logger?.warn("queue", "Failed to clear processed queue file \u2014 next run may re-apply this batch", {
79965
+ error: error48.message,
79966
+ processingPath
79967
+ });
79968
+ });
79623
79969
  return result;
79624
79970
  });
79625
79971
  } catch (error48) {
@@ -79638,7 +79984,7 @@ async function clearQueueFile(workdir) {
79638
79984
  await withQueueFileLock(queuePath, async () => {
79639
79985
  const file3 = Bun.file(processingPath);
79640
79986
  if (await file3.exists())
79641
- await unlink6(processingPath);
79987
+ await unlink7(processingPath);
79642
79988
  });
79643
79989
  } catch (error48) {
79644
79990
  logger?.warn("queue", "Failed to clear queue file", {
@@ -80652,18 +80998,17 @@ async function runSetupGate(workdir, config2) {
80652
80998
  return 0;
80653
80999
  }
80654
81000
  logger.info("setup-verify", "Running verification gate", { storyId: "setup", cmd: testCmd });
80655
- const parts = testCmd.trim().split(/\s+/).filter(Boolean);
80656
- if (parts.length === 0)
80657
- return 0;
80658
- const proc = _setupVerifyDeps.spawn(parts, { cwd: workdir });
80659
- return await proc.exited;
81001
+ const result = await runQualityCommand({
81002
+ commandName: "setup-verify",
81003
+ command: testCmd,
81004
+ workdir,
81005
+ storyId: "setup"
81006
+ });
81007
+ return result.exitCode;
80660
81008
  }
80661
- var _setupVerifyDeps;
80662
81009
  var init_setup_verify = __esm(() => {
80663
81010
  init_logger2();
80664
- _setupVerifyDeps = {
80665
- spawn: Bun.spawn.bind(Bun)
80666
- };
81011
+ init_runner();
80667
81012
  });
80668
81013
 
80669
81014
  // src/cli/setup-write.ts
@@ -110155,7 +110500,7 @@ var MAX_WORKTREE_ID_LENGTH = 64, HASH_SUFFIX_LENGTH = 8;
110155
110500
  var init_worktree_id = () => {};
110156
110501
 
110157
110502
  // src/bakeoff/contestant.ts
110158
- import { basename as basename21, join as join111 } from "path";
110503
+ import { basename as basename22, join as join111 } from "path";
110159
110504
  function aggregateTotals(metrics) {
110160
110505
  let costUsd = 0;
110161
110506
  let wallTimeMs = 0;
@@ -110185,7 +110530,7 @@ async function runContestant(agent, options, deps) {
110185
110530
  }
110186
110531
  };
110187
110532
  const worktree = join111(options.projectRoot, ".nax-wt", storyId);
110188
- const projectKey = options.config.name?.trim() || basename21(options.projectRoot);
110533
+ const projectKey = options.config.name?.trim() || basename22(options.projectRoot);
110189
110534
  const outputDir = join111(projectOutputDir(projectKey, options.outputDir), "bakeoff", feature, agent);
110190
110535
  const context = {
110191
110536
  profile: agent,
@@ -110512,7 +110857,7 @@ var init_bakeoff = __esm(() => {
110512
110857
  });
110513
110858
 
110514
110859
  // src/plugins/builtin/curator/rollup-prune.ts
110515
- import { rename as rename7, unlink as unlink8, writeFile as writeFile3 } from "fs/promises";
110860
+ import { rename as rename7, unlink as unlink9, writeFile as writeFile3 } from "fs/promises";
110516
110861
  import { appendFile as appendFile7 } from "fs/promises";
110517
110862
  async function scanProjectRunIds(rollupPath, projectKey) {
110518
110863
  const maxTsByRunId = new Map;
@@ -110583,7 +110928,7 @@ async function pruneRollup(input) {
110583
110928
  await flush();
110584
110929
  await rename7(tmpPath, rollupPath);
110585
110930
  } catch (err) {
110586
- await unlink8(tmpPath).catch(() => {});
110931
+ await unlink9(tmpPath).catch(() => {});
110587
110932
  throw err;
110588
110933
  }
110589
110934
  return result2;
@@ -110604,7 +110949,7 @@ __export(exports_curator, {
110604
110949
  _curatorCmdDeps: () => _curatorCmdDeps
110605
110950
  });
110606
110951
  import { readdirSync as readdirSync9 } from "fs";
110607
- import { unlink as unlink9 } from "fs/promises";
110952
+ import { unlink as unlink10 } from "fs/promises";
110608
110953
  import { join as join114, resolve as resolve23, sep as sep9 } from "path";
110609
110954
  function listRunIds(runsDir) {
110610
110955
  try {
@@ -110969,7 +111314,7 @@ var init_curator2 = __esm(() => {
110969
111314
  },
110970
111315
  removeFile: async (p) => {
110971
111316
  try {
110972
- await unlink9(p);
111317
+ await unlink10(p);
110973
111318
  } catch {}
110974
111319
  },
110975
111320
  openInEditor: async (filePath) => {
@@ -110988,7 +111333,7 @@ var init_curator2 = __esm(() => {
110988
111333
  init_source();
110989
111334
  import { existsSync as existsSync41, mkdirSync as mkdirSync8 } from "fs";
110990
111335
  import { homedir as homedir3 } from "os";
110991
- import { basename as basename23, join as join115 } from "path";
111336
+ import { basename as basename24, join as join115 } from "path";
110992
111337
 
110993
111338
  // node_modules/commander/esm.mjs
110994
111339
  var import__ = __toESM(require_commander(), 1);
@@ -111666,12 +112011,12 @@ init_errors();
111666
112011
  init_logger2();
111667
112012
  init_runtime();
111668
112013
  import { existsSync as existsSync18, readdirSync as readdirSync4 } from "fs";
111669
- import { basename as basename11, join as join52 } from "path";
112014
+ import { basename as basename12, join as join52 } from "path";
111670
112015
  async function resolveOutputDir(workdir, override) {
111671
112016
  if (override)
111672
112017
  return override;
111673
112018
  const config2 = await loadConfig(workdir).catch(() => null);
111674
- const projectKey = config2?.name?.trim() || basename11(workdir);
112019
+ const projectKey = config2?.name?.trim() || basename12(workdir);
111675
112020
  return projectOutputDir(projectKey, config2?.outputDir);
111676
112021
  }
111677
112022
  async function parseRunLog(logPath) {
@@ -113379,7 +113724,7 @@ async function rulesLintCommand(options, deps = _rulesLintDeps) {
113379
113724
  // src/cli/rules-migrate.ts
113380
113725
  init_canonical_loader();
113381
113726
  init_errors();
113382
- import { basename as basename18, join as join96 } from "path";
113727
+ import { basename as basename19, join as join96 } from "path";
113383
113728
 
113384
113729
  // src/cli/rules-migrate-plan.ts
113385
113730
  init_errors();
@@ -113458,7 +113803,7 @@ async function collectMigrationSources(workdir) {
113458
113803
  try {
113459
113804
  const content = await _rulesCLIDeps.readFile(filePath);
113460
113805
  if (content.trim()) {
113461
- sources.push({ sourcePath: filePath, targetFileName: basename18(filePath), content });
113806
+ sources.push({ sourcePath: filePath, targetFileName: basename19(filePath), content });
113462
113807
  }
113463
113808
  } catch {}
113464
113809
  }
@@ -113959,7 +114304,7 @@ init_runtime();
113959
114304
  init_json_file();
113960
114305
  init_routing();
113961
114306
  import { mkdirSync as mkdirSync7 } from "fs";
113962
- import { basename as basename19, join as join100 } from "path";
114307
+ import { basename as basename20, join as join100 } from "path";
113963
114308
  var _routingCalibrateDeps = {
113964
114309
  loadRunMetrics: (outputDir) => loadRunMetrics(outputDir),
113965
114310
  readConfig: (workdir) => loadConfig(workdir),
@@ -114017,7 +114362,7 @@ async function runRoutingCalibrateCli(options, deps = _routingCalibrateDeps) {
114017
114362
  function resolveOutputDir2(workdir, override, prior) {
114018
114363
  if (override)
114019
114364
  return override;
114020
- const key = prior?.name?.trim() || basename19(workdir);
114365
+ const key = prior?.name?.trim() || basename20(workdir);
114021
114366
  return projectOutputDir(key, prior?.outputDir);
114022
114367
  }
114023
114368
  function mergeComplexityRouting(prior, adjustments) {
@@ -114291,7 +114636,7 @@ import { join as join103 } from "path";
114291
114636
  // src/commands/logs-reader.ts
114292
114637
  init_paths3();
114293
114638
  import { existsSync as existsSync35, readdirSync as readdirSync7 } from "fs";
114294
- import { readdir as readdir5 } from "fs/promises";
114639
+ import { readdir as readdir6 } from "fs/promises";
114295
114640
  import { join as join102 } from "path";
114296
114641
  var _logsReaderDeps = {
114297
114642
  getRunsDir
@@ -114300,7 +114645,7 @@ async function resolveRunFileFromRegistry(runId) {
114300
114645
  const runsDir = _logsReaderDeps.getRunsDir();
114301
114646
  let entries;
114302
114647
  try {
114303
- entries = await readdir5(runsDir);
114648
+ entries = await readdir6(runsDir);
114304
114649
  } catch {
114305
114650
  throw new Error(`Run not found in registry: ${runId}`);
114306
114651
  }
@@ -114622,12 +114967,12 @@ async function precheckCommand(options) {
114622
114967
  init_errors();
114623
114968
  init_metrics();
114624
114969
  import { existsSync as existsSync38 } from "fs";
114625
- import { dirname as dirname18 } from "path";
114970
+ import { dirname as dirname19 } from "path";
114626
114971
 
114627
114972
  // src/replay/discovery.ts
114628
114973
  init_errors();
114629
114974
  init_paths3();
114630
- import { readdir as readdir6 } from "fs/promises";
114975
+ import { readdir as readdir7 } from "fs/promises";
114631
114976
  import { join as join106 } from "path";
114632
114977
  var _discoveryDeps = {
114633
114978
  getRunsDir
@@ -114635,7 +114980,7 @@ var _discoveryDeps = {
114635
114980
  async function loadMetas(runsDir) {
114636
114981
  let entries;
114637
114982
  try {
114638
- entries = await readdir6(runsDir);
114983
+ entries = await readdir7(runsDir);
114639
114984
  } catch {
114640
114985
  return [];
114641
114986
  }
@@ -114935,7 +115280,7 @@ async function readJsonOrUndefined(path32) {
114935
115280
  }
114936
115281
  async function readMetricsFromProject(meta3) {
114937
115282
  const { loadRunMetrics: loadRunMetrics2 } = await Promise.resolve().then(() => (init_tracker(), exports_tracker));
114938
- const outputDir = dirname18(dirname18(dirname18(meta3.eventsDir)));
115283
+ const outputDir = dirname19(dirname19(dirname19(meta3.eventsDir)));
114939
115284
  const all = await loadRunMetrics2(outputDir);
114940
115285
  return all.find((m) => m.runId === meta3.runId);
114941
115286
  }
@@ -115018,7 +115363,7 @@ init_errors();
115018
115363
  init_checkpoint();
115019
115364
  init_runtime();
115020
115365
  import { existsSync as existsSync39 } from "fs";
115021
- import { basename as basename20, join as join107 } from "path";
115366
+ import { basename as basename21, join as join107 } from "path";
115022
115367
  async function defaultCheckpointExists(featureDir2) {
115023
115368
  if (!featureDir2 || !existsSync39(featureDir2))
115024
115369
  return false;
@@ -115098,7 +115443,7 @@ function registerResumeCommand(program2) {
115098
115443
  const globalNaxDir = globalConfigDir();
115099
115444
  const hooks = await loadHooksConfig2(naxDir, globalNaxDir);
115100
115445
  applyResumeModeDeps2(opts.featureDir ?? "", "auto");
115101
- const projectKey = config2.name?.trim() || basename20(cmdOpts.dir);
115446
+ const projectKey = config2.name?.trim() || basename21(cmdOpts.dir);
115102
115447
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
115103
115448
  const statusFilePath = join107(outputDir, "status.json");
115104
115449
  const runsDir = join107(outputDir, "features", feature, "runs");
@@ -115139,7 +115484,7 @@ function registerResumeCommand(program2) {
115139
115484
  // src/commands/runs.ts
115140
115485
  init_source();
115141
115486
  init_paths3();
115142
- import { readdir as readdir7 } from "fs/promises";
115487
+ import { readdir as readdir8 } from "fs/promises";
115143
115488
  import { join as join108 } from "path";
115144
115489
  var DEFAULT_LIMIT = 20;
115145
115490
  var _runsCmdDeps = {
@@ -115190,7 +115535,7 @@ async function runsCommand(options = {}) {
115190
115535
  const runsDir = _runsCmdDeps.getRunsDir();
115191
115536
  let entries;
115192
115537
  try {
115193
- entries = await readdir7(runsDir);
115538
+ entries = await readdir8(runsDir);
115194
115539
  } catch {
115195
115540
  console.log("No runs found.");
115196
115541
  return;
@@ -115274,7 +115619,7 @@ async function runsCommand(options = {}) {
115274
115619
 
115275
115620
  // src/commands/unlock.ts
115276
115621
  init_source();
115277
- import { unlink as unlink7 } from "fs/promises";
115622
+ import { unlink as unlink8 } from "fs/promises";
115278
115623
  import { join as join109 } from "path";
115279
115624
  function formatLockAge(ageMs) {
115280
115625
  const minutes = Math.round(ageMs / (60 * 1000));
@@ -115321,7 +115666,7 @@ async function unlockCommand(options) {
115321
115666
  }
115322
115667
  }
115323
115668
  try {
115324
- await unlink7(lockPath);
115669
+ await unlink8(lockPath);
115325
115670
  } catch (error48) {
115326
115671
  console.error(source_default.red(`Failed to remove lock: ${error48 instanceof Error ? error48.message : String(error48)}`));
115327
115672
  process.exit(1);
@@ -123561,7 +123906,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
123561
123906
  process.exit(1);
123562
123907
  }
123563
123908
  resetLogger();
123564
- const projectKey = config2.name?.trim() || basename23(workdir);
123909
+ const projectKey = config2.name?.trim() || basename24(workdir);
123565
123910
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
123566
123911
  const runsDir = join115(outputDir, "features", options.feature, "runs");
123567
123912
  mkdirSync8(runsDir, { recursive: true });