@nathapp/nax 0.77.3 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/nax.js CHANGED
@@ -19356,18 +19356,36 @@ function findProjectDir(startDir = process.cwd()) {
19356
19356
  }
19357
19357
  return null;
19358
19358
  }
19359
+ function defaultConfigWarn(msg) {
19360
+ try {
19361
+ getLogger().warn("config", msg);
19362
+ } catch {}
19363
+ }
19359
19364
  function applyRemovedStrategyCompat(conf) {
19360
19365
  const routing = conf.routing;
19361
19366
  const strategy = routing?.strategy;
19362
19367
  const REMOVED_STRATEGIES = ["manual", "adaptive", "custom"];
19363
19368
  if (typeof strategy === "string" && REMOVED_STRATEGIES.includes(strategy)) {
19364
- try {
19365
- getLogger().warn("config", `routing.strategy="${strategy}" was removed in ROUTE-001 and is no longer supported. Falling back to "keyword". Update your config to use "keyword" or "llm".`);
19366
- } catch {}
19369
+ defaultConfigWarn(`routing.strategy="${strategy}" was removed in ROUTE-001 and is no longer supported. Falling back to "keyword". Update your config to use "keyword" or "llm".`);
19367
19370
  return { ...conf, routing: { ...routing, strategy: "keyword" } };
19368
19371
  }
19369
19372
  return conf;
19370
19373
  }
19374
+ function _applyRemovedRoutingKeysShim(conf, warn = defaultConfigWarn) {
19375
+ const routing = conf.routing;
19376
+ if (!routing || typeof routing !== "object")
19377
+ return conf;
19378
+ const REMOVED_ROUTING_KEYS = ["customStrategyPath", "adaptive"];
19379
+ let newRouting = routing;
19380
+ for (const key of REMOVED_ROUTING_KEYS) {
19381
+ if (key in newRouting) {
19382
+ warn(`routing.${key} was removed in ROUTE-001 along with the "custom"/"adaptive" strategies and has no effect. Remove it from your config.`);
19383
+ const { [key]: _removed, ...rest } = newRouting;
19384
+ newRouting = rest;
19385
+ }
19386
+ }
19387
+ return newRouting === routing ? conf : { ...conf, routing: newRouting };
19388
+ }
19371
19389
  function applyBatchModeCompat(conf) {
19372
19390
  const routing = conf.routing;
19373
19391
  const llm = routing?.llm;
@@ -19375,9 +19393,7 @@ function applyBatchModeCompat(conf) {
19375
19393
  const batchMode = llm.batchMode;
19376
19394
  if (typeof batchMode === "boolean") {
19377
19395
  const mappedMode = batchMode ? "one-shot" : "per-story";
19378
- try {
19379
- getLogger().warn("config", `routing.llm.batchMode is deprecated and will be removed in v1.0. Mapped to mode="${mappedMode}". Update your config to use routing.llm.mode instead.`);
19380
- } catch {}
19396
+ defaultConfigWarn(`routing.llm.batchMode is deprecated and will be removed in v1.0. Mapped to mode="${mappedMode}". Update your config to use routing.llm.mode instead.`);
19381
19397
  return {
19382
19398
  ...conf,
19383
19399
  routing: {
@@ -19389,11 +19405,7 @@ function applyBatchModeCompat(conf) {
19389
19405
  }
19390
19406
  return conf;
19391
19407
  }
19392
- function _applyLegacyReviewExecutionShim(conf, warn = (msg) => {
19393
- try {
19394
- getLogger().warn("config", msg);
19395
- } catch {}
19396
- }) {
19408
+ function _applyLegacyReviewExecutionShim(conf, warn = defaultConfigWarn) {
19397
19409
  let result = conf;
19398
19410
  const execution = conf.execution;
19399
19411
  if (execution && typeof execution === "object" && "inlineReview" in execution) {
@@ -19420,11 +19432,7 @@ function _applyLegacyReviewExecutionShim(conf, warn = (msg) => {
19420
19432
  }
19421
19433
  return result;
19422
19434
  }
19423
- function applyRoutingRetryDeprecationWarning(conf, warn = (msg) => {
19424
- try {
19425
- getLogger().warn("config", msg);
19426
- } catch {}
19427
- }) {
19435
+ function applyRoutingRetryDeprecationWarning(conf, warn = defaultConfigWarn) {
19428
19436
  const routing = conf.routing;
19429
19437
  const llm = routing?.llm;
19430
19438
  if (!llm)
@@ -19450,14 +19458,14 @@ async function loadConfig(startDir, cliOverrides) {
19450
19458
  } catch {}
19451
19459
  if (globalConfRaw) {
19452
19460
  const { profile: _gProfile, ...globalConfStripped } = globalConfRaw;
19453
- const globalConf = _applyLegacyReviewExecutionShim(applyRoutingRetryDeprecationWarning(applyBatchModeCompat(applyRemovedStrategyCompat(migrateLegacyReviewModelKey(migrateLegacyTestPattern(globalConfStripped, logger), logger)))));
19461
+ const globalConf = _applyLegacyReviewExecutionShim(_applyRemovedRoutingKeysShim(applyRoutingRetryDeprecationWarning(applyBatchModeCompat(applyRemovedStrategyCompat(migrateLegacyReviewModelKey(migrateLegacyTestPattern(globalConfStripped, logger), logger))))));
19454
19462
  rawConfig = deepMergeConfig(rawConfig, globalConf);
19455
19463
  }
19456
19464
  if (projDir) {
19457
19465
  const projConf = await loadJsonFile(join3(projDir, "config.json"), "config");
19458
19466
  if (projConf) {
19459
19467
  const { profile: _pProfile, ...projConfStripped } = projConf;
19460
- const resolvedProjConf = _applyLegacyReviewExecutionShim(applyRoutingRetryDeprecationWarning(applyBatchModeCompat(applyRemovedStrategyCompat(migrateLegacyReviewModelKey(migrateLegacyTestPattern(projConfStripped, logger), logger)))));
19468
+ const resolvedProjConf = _applyLegacyReviewExecutionShim(_applyRemovedRoutingKeysShim(applyRoutingRetryDeprecationWarning(applyBatchModeCompat(applyRemovedStrategyCompat(migrateLegacyReviewModelKey(migrateLegacyTestPattern(projConfStripped, logger), logger))))));
19461
19469
  rawConfig = deepMergeConfig(rawConfig, resolvedProjConf);
19462
19470
  }
19463
19471
  }
@@ -20033,6 +20041,7 @@ __export(exports_config, {
20033
20041
  TEST_STRATEGY_GUIDE: () => TEST_STRATEGY_GUIDE,
20034
20042
  SPEC_ANCHOR_RULES: () => SPEC_ANCHOR_RULES,
20035
20043
  SINGLE_SESSION_TEST_OWNING_STRATEGIES: () => SINGLE_SESSION_TEST_OWNING_STRATEGIES,
20044
+ RoutingConfigSchema: () => RoutingConfigSchema,
20036
20045
  ReviewConfigSchema: () => ReviewConfigSchema,
20037
20046
  PlanConfigSchema: () => PlanConfigSchema,
20038
20047
  NaxConfigSchema: () => NaxConfigSchema,
@@ -24339,17 +24348,25 @@ function buildManifest(inputs) {
24339
24348
  ...staleChunkIds.length > 0 && { staleChunks: staleChunkIds }
24340
24349
  };
24341
24350
  }
24342
- function rebuildUsedTokens(prior, packed, newPriorStageDigest) {
24343
- const priorChunksTokens = prior.chunks.reduce((sum, c) => sum + c.tokens, 0);
24344
- const extraTokens = packed.filter((c) => !prior.chunks.some((pc) => pc.id === c.id)).reduce((sum, c) => sum + c.tokens, 0);
24345
- const packedTokens = priorChunksTokens + extraTokens;
24346
- const newDigestContent = newPriorStageDigest?.trim();
24347
- const newDigestContribution = newDigestContent ? Math.ceil(newDigestContent.length / 4) : 0;
24348
- return Math.max(0, packedTokens + newDigestContribution);
24349
- }
24350
24351
  var CHUNK_SUMMARY_CHARS = 300;
24351
24352
 
24352
24353
  // src/context/engine/orchestrator-rebuild-helpers.ts
24354
+ function toContextChunk(packed) {
24355
+ const providerId = packed.providerId ?? packed.id.split(":")[0] ?? "unknown";
24356
+ return {
24357
+ id: packed.id,
24358
+ providerId,
24359
+ kind: packed.kind,
24360
+ scope: packed.scope,
24361
+ role: packed.role,
24362
+ content: packed.content,
24363
+ tokens: packed.tokens,
24364
+ rawScore: packed.rawScore,
24365
+ score: packed.score,
24366
+ reason: packed.reason,
24367
+ ...packed.staleCandidate && { staleCandidate: true }
24368
+ };
24369
+ }
24353
24370
  function buildFailureNoteChunk(priorAgentId, newAgentId, failure) {
24354
24371
  const lines = [
24355
24372
  "## Agent swap (availability fallback)",
@@ -24385,17 +24402,62 @@ var DEFAULT_REBUILD_AGENT_ID = "claude";
24385
24402
  function scoreDensity(chunk) {
24386
24403
  return chunk.tokens > 0 ? chunk.score / chunk.tokens : Number.POSITIVE_INFINITY;
24387
24404
  }
24405
+ function greedyNonFloor(nonFloor, remainingBudget) {
24406
+ const sorted = [...nonFloor].sort((a, b) => scoreDensity(b) - scoreDensity(a));
24407
+ const selected = [];
24408
+ const excludedIds = [];
24409
+ let used = 0;
24410
+ for (const chunk of sorted) {
24411
+ if (used + chunk.tokens <= remainingBudget) {
24412
+ selected.push(chunk);
24413
+ used += chunk.tokens;
24414
+ } else {
24415
+ excludedIds.push(chunk.id);
24416
+ }
24417
+ }
24418
+ return { selected, excludedIds };
24419
+ }
24420
+ function largestSingleItem(nonFloor, remainingBudget) {
24421
+ let best = null;
24422
+ for (const chunk of nonFloor) {
24423
+ if (chunk.tokens > remainingBudget)
24424
+ continue;
24425
+ if (best === null || chunk.score > best.score)
24426
+ best = chunk;
24427
+ }
24428
+ return best ? [best] : [];
24429
+ }
24430
+ function repairNonFloor(nonFloor, remainingBudget) {
24431
+ const greedy = greedyNonFloor(nonFloor, remainingBudget);
24432
+ const greedyScore = greedy.selected.reduce((s, c) => s + c.score, 0);
24433
+ const largest = largestSingleItem(nonFloor, remainingBudget);
24434
+ const largestScore = largest.reduce((s, c) => s + c.score, 0);
24435
+ const candidates = [
24436
+ { selected: greedy.selected, totalScore: greedyScore },
24437
+ { selected: largest, totalScore: largestScore }
24438
+ ];
24439
+ let best = candidates[0];
24440
+ for (const candidate of candidates) {
24441
+ if (candidate.totalScore > best.totalScore) {
24442
+ best = candidate;
24443
+ }
24444
+ }
24445
+ const winnerIds = new Set(best.selected.map((c) => c.id));
24446
+ const excludedIds = nonFloor.filter((c) => !winnerIds.has(c.id)).map((c) => c.id);
24447
+ return { selected: best.selected, excludedIds };
24448
+ }
24388
24449
  function packChunks(chunks, budgetTokens, availableBudgetTokens) {
24389
24450
  const effectiveBudget = availableBudgetTokens !== undefined ? Math.min(budgetTokens, availableBudgetTokens) : budgetTokens;
24390
24451
  const floorChunks = chunks.filter((c) => FLOOR_KINDS.includes(c.kind));
24391
- const nonFloorChunks = chunks.filter((c) => !FLOOR_KINDS.includes(c.kind)).sort((a, b) => scoreDensity(b) - scoreDensity(a));
24452
+ const nonFloorChunks = chunks.filter((c) => !FLOOR_KINDS.includes(c.kind));
24392
24453
  const packed = [];
24393
- const budgetExcludedIds = [];
24394
24454
  const floorPackedIds = [];
24395
24455
  const floorOverageIds = [];
24396
24456
  let usedTokens = 0;
24457
+ const totalFloorTokens = floorChunks.reduce((sum, c) => sum + c.tokens, 0);
24458
+ const floorCollectivelyOverflows = totalFloorTokens > effectiveBudget;
24397
24459
  for (const chunk of floorChunks) {
24398
- const overflows = usedTokens + chunk.tokens > effectiveBudget;
24460
+ const overflows = floorCollectivelyOverflows || usedTokens + chunk.tokens > effectiveBudget;
24399
24461
  const packedChunk = { ...chunk };
24400
24462
  if (overflows) {
24401
24463
  packedChunk.reason = "budget-exceeded-by-floor";
@@ -24405,15 +24467,20 @@ function packChunks(chunks, budgetTokens, availableBudgetTokens) {
24405
24467
  packed.push(packedChunk);
24406
24468
  usedTokens += chunk.tokens;
24407
24469
  }
24408
- for (const chunk of nonFloorChunks) {
24409
- if (usedTokens + chunk.tokens <= effectiveBudget) {
24410
- packed.push({ ...chunk });
24411
- usedTokens += chunk.tokens;
24412
- } else {
24413
- budgetExcludedIds.push(chunk.id);
24414
- }
24470
+ const remainingBudget = Math.max(0, effectiveBudget - usedTokens);
24471
+ const { selected, excludedIds } = repairNonFloor(nonFloorChunks, remainingBudget);
24472
+ for (const chunk of selected) {
24473
+ packed.push({ ...chunk });
24474
+ usedTokens += chunk.tokens;
24415
24475
  }
24416
- return { packed, budgetExcludedIds, usedTokens, effectiveBudget, floorPackedIds, floorOverageIds };
24476
+ return {
24477
+ packed,
24478
+ budgetExcludedIds: excludedIds,
24479
+ usedTokens,
24480
+ effectiveBudget,
24481
+ floorPackedIds,
24482
+ floorOverageIds
24483
+ };
24417
24484
  }
24418
24485
  var FLOOR_KINDS;
24419
24486
  var init_packing = __esm(() => {
@@ -28491,6 +28558,147 @@ var init_render = __esm(() => {
28491
28558
  FIXED_RENDER_OVERHEAD_TOKENS = Math.ceil(FIXED_RENDER_OVERHEAD_CHARS / 4);
28492
28559
  });
28493
28560
 
28561
+ // src/context/engine/scratch-neutralizer.ts
28562
+ function neutralizeForAgent(content, sourceAgent, targetAgent) {
28563
+ if (!content || !sourceAgent || !targetAgent || sourceAgent === targetAgent)
28564
+ return content;
28565
+ if (sourceAgent.toLowerCase() !== "claude")
28566
+ return content;
28567
+ let result = content;
28568
+ for (const [pattern, replacement] of CLAUDE_TOOL_SUBSTITUTIONS) {
28569
+ result = result.replace(pattern, replacement);
28570
+ }
28571
+ return result;
28572
+ }
28573
+ var CLAUDE_TOOL_SUBSTITUTIONS;
28574
+ var init_scratch_neutralizer = __esm(() => {
28575
+ CLAUDE_TOOL_SUBSTITUTIONS = [
28576
+ [/\bthe\s+Read\s+tool\b/gi, "a file read"],
28577
+ [/\bthe\s+Edit\s+tool\b/gi, "a file edit"],
28578
+ [/\bthe\s+Write\s+tool\b/gi, "a file write"],
28579
+ [/\bthe\s+Bash\s+tool\b/gi, "a shell command"],
28580
+ [/\bthe\s+Grep\s+tool\b/gi, "a code search"],
28581
+ [/\bthe\s+Glob\s+tool\b/gi, "a file search"],
28582
+ [/\bthe\s+Agent\s+tool\b/gi, "a sub-agent"],
28583
+ [/\bthe\s+Task\s+tool\b/gi, "a sub-agent"]
28584
+ ];
28585
+ });
28586
+
28587
+ // src/context/engine/rebuild.ts
28588
+ import { randomUUID as randomUUID2 } from "crypto";
28589
+ function toPackedChunks(prior, newAgentId, targetAgentId) {
28590
+ const priorAgentForNeutralize = prior.agentId ?? "";
28591
+ return prior.chunks.map((c) => {
28592
+ const content = newAgentId && newAgentId !== priorAgentForNeutralize && c.kind === "session" ? neutralizeForAgent(c.content, priorAgentForNeutralize, targetAgentId) : c.content;
28593
+ return { ...c, content, rawScore: c.score, roleFiltered: false, belowMinScore: false };
28594
+ });
28595
+ }
28596
+ function rebuild(prior, options = {}, deps = DEFAULT_REBUILD_DEPS) {
28597
+ const { newAgentId, failure, priorStageDigest, storyId } = options;
28598
+ const targetAgentId = newAgentId ?? prior.agentId ?? DEFAULT_REBUILD_AGENT_ID;
28599
+ const logger = deps.getLogger();
28600
+ if (newAgentId && !AGENT_PROFILES[newAgentId]) {
28601
+ logger.warn("context-v2", "rebuildForAgent: unknown agent id \u2014 using conservative defaults", {
28602
+ ...storyId && { storyId },
28603
+ stage: prior.manifest.stage,
28604
+ agentId: newAgentId
28605
+ });
28606
+ }
28607
+ const priorChunkIds = prior.chunks.map((c) => c.id);
28608
+ const packedChunks = toPackedChunks(prior, newAgentId, targetAgentId);
28609
+ const targetProfile = getAgentProfile(targetAgentId).profile;
28610
+ const rebuiltPullTools = targetProfile.caps.supportsToolCalls ? prior.pullTools : [];
28611
+ const effectiveBudget = Math.min(prior.manifest.effectiveBudget ?? Number.POSITIVE_INFINITY, targetProfile.caps.preferredPromptTokens);
28612
+ let failureNoteChunk;
28613
+ if (failure && newAgentId && !prior.manifest.rebuildInfo) {
28614
+ failureNoteChunk = buildFailureNoteChunk(prior.agentId || "unknown", newAgentId, failure);
28615
+ packedChunks.push(failureNoteChunk);
28616
+ }
28617
+ let packResult = packChunks(packedChunks, effectiveBudget);
28618
+ if (failureNoteChunk && !packResult.packed.some((c) => c.id === failureNoteChunk.id)) {
28619
+ packResult = {
28620
+ ...packResult,
28621
+ packed: [...packResult.packed, failureNoteChunk],
28622
+ usedTokens: packResult.usedTokens + failureNoteChunk.tokens
28623
+ };
28624
+ }
28625
+ const chunkById = new Map(packResult.packed.map((c) => [c.id, c]));
28626
+ const orderedChunks = [];
28627
+ for (const priorId of priorChunkIds) {
28628
+ const chunk = chunkById.get(priorId);
28629
+ if (chunk)
28630
+ orderedChunks.push(chunk);
28631
+ }
28632
+ if (failureNoteChunk && chunkById.has(failureNoteChunk.id)) {
28633
+ orderedChunks.push(failureNoteChunk);
28634
+ }
28635
+ const pushMarkdown = newAgentId ? renderForAgent(orderedChunks, targetAgentId, { priorStageDigest }) : renderChunks(orderedChunks, { priorStageDigest });
28636
+ const digest = buildDigest(orderedChunks);
28637
+ const dTokens = digestTokens(digest);
28638
+ const newDigestContent = priorStageDigest?.trim();
28639
+ const digestContribution = newDigestContent ? Math.ceil(newDigestContent.length / 4) : 0;
28640
+ const usedTokens = packResult.usedTokens + digestContribution;
28641
+ const rebuildInfo = failure && newAgentId ? {
28642
+ priorAgentId: prior.agentId ?? "unknown",
28643
+ newAgentId: targetAgentId,
28644
+ failureCategory: failure.category,
28645
+ failureOutcome: failure.outcome,
28646
+ priorChunkIds,
28647
+ newChunkIds: orderedChunks.map((c) => c.id),
28648
+ chunkIdMap: [
28649
+ ...priorChunkIds.map((priorId) => {
28650
+ const entry = chunkById.get(priorId);
28651
+ return entry ? { priorChunkId: priorId, newChunkId: priorId } : null;
28652
+ }).filter((entry) => entry !== null),
28653
+ ...failureNoteChunk ? [{ priorChunkId: failureNoteChunk.id, newChunkId: failureNoteChunk.id }] : []
28654
+ ]
28655
+ } : undefined;
28656
+ const includedChunkIds = new Set(orderedChunks.map((c) => c.id));
28657
+ const chunkSummaries = Object.fromEntries(orderedChunks.map((c) => [c.id, c.content.slice(0, CHUNK_SUMMARY_CHARS)]));
28658
+ const chunkEffectiveness = prior.manifest.chunkEffectiveness ? Object.fromEntries(Object.entries(prior.manifest.chunkEffectiveness).filter(([id]) => includedChunkIds.has(id))) : undefined;
28659
+ const excludedChunks = packResult.budgetExcludedIds.filter((id) => !includedChunkIds.has(id)).map((id) => ({ id, reason: "budget" }));
28660
+ const manifest = {
28661
+ ...prior.manifest,
28662
+ requestId: deps.uuid(),
28663
+ includedChunks: orderedChunks.map((c) => c.id),
28664
+ excludedChunks,
28665
+ chunkTokens: Object.fromEntries(orderedChunks.map((c) => [c.id, c.tokens])),
28666
+ usedTokens,
28667
+ digestTokens: dTokens,
28668
+ buildMs: 0,
28669
+ rebuildInfo,
28670
+ effectiveBudget,
28671
+ floorItems: packResult.floorPackedIds,
28672
+ floorOverageItems: packResult.floorOverageIds.length > 0 ? packResult.floorOverageIds : undefined,
28673
+ chunkSummaries: Object.keys(chunkSummaries).length > 0 ? chunkSummaries : undefined,
28674
+ staleChunks: orderedChunks.some((c) => c.staleCandidate) ? orderedChunks.filter((c) => c.staleCandidate).map((c) => c.id) : undefined,
28675
+ chunkEffectiveness: chunkEffectiveness && Object.keys(chunkEffectiveness).length > 0 ? chunkEffectiveness : undefined
28676
+ };
28677
+ const rebuiltChunks = orderedChunks.map(toContextChunk);
28678
+ return {
28679
+ pushMarkdown,
28680
+ pullTools: rebuiltPullTools,
28681
+ digest,
28682
+ manifest,
28683
+ chunks: rebuiltChunks,
28684
+ agentId: targetAgentId
28685
+ };
28686
+ }
28687
+ var DEFAULT_REBUILD_DEPS;
28688
+ var init_rebuild = __esm(() => {
28689
+ init_logger2();
28690
+ init_agent_profiles();
28691
+ init_agent_renderer();
28692
+ init_digest();
28693
+ init_packing();
28694
+ init_render();
28695
+ init_scratch_neutralizer();
28696
+ DEFAULT_REBUILD_DEPS = {
28697
+ uuid: () => randomUUID2(),
28698
+ getLogger: () => getLogger()
28699
+ };
28700
+ });
28701
+
28494
28702
  // src/context/engine/scoring.ts
28495
28703
  function roleMultiplier(chunkRoles, callerRole) {
28496
28704
  if (chunkRoles.includes(callerRole))
@@ -28527,34 +28735,8 @@ var init_scoring = __esm(() => {
28527
28735
  };
28528
28736
  });
28529
28737
 
28530
- // src/context/engine/scratch-neutralizer.ts
28531
- function neutralizeForAgent(content, sourceAgent, targetAgent) {
28532
- if (!content || !sourceAgent || !targetAgent || sourceAgent === targetAgent)
28533
- return content;
28534
- if (sourceAgent.toLowerCase() !== "claude")
28535
- return content;
28536
- let result = content;
28537
- for (const [pattern, replacement] of CLAUDE_TOOL_SUBSTITUTIONS) {
28538
- result = result.replace(pattern, replacement);
28539
- }
28540
- return result;
28541
- }
28542
- var CLAUDE_TOOL_SUBSTITUTIONS;
28543
- var init_scratch_neutralizer = __esm(() => {
28544
- CLAUDE_TOOL_SUBSTITUTIONS = [
28545
- [/\bthe\s+Read\s+tool\b/gi, "a file read"],
28546
- [/\bthe\s+Edit\s+tool\b/gi, "a file edit"],
28547
- [/\bthe\s+Write\s+tool\b/gi, "a file write"],
28548
- [/\bthe\s+Bash\s+tool\b/gi, "a shell command"],
28549
- [/\bthe\s+Grep\s+tool\b/gi, "a code search"],
28550
- [/\bthe\s+Glob\s+tool\b/gi, "a file search"],
28551
- [/\bthe\s+Agent\s+tool\b/gi, "a sub-agent"],
28552
- [/\bthe\s+Task\s+tool\b/gi, "a sub-agent"]
28553
- ];
28554
- });
28555
-
28556
28738
  // src/context/engine/orchestrator.ts
28557
- import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
28739
+ import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
28558
28740
  function buildPullToolDescriptors(stageToolNames, pullConfig) {
28559
28741
  if (!pullConfig?.enabled || stageToolNames.length === 0)
28560
28742
  return [];
@@ -28584,22 +28766,6 @@ async function fetchWithTimeout(provider, request, timeoutMs = PROVIDER_FETCH_TI
28584
28766
  clearTimeout(handle);
28585
28767
  }
28586
28768
  }
28587
- function toContextChunk(packed) {
28588
- const providerId = packed.providerId ?? packed.id.split(":")[0] ?? "unknown";
28589
- return {
28590
- id: packed.id,
28591
- providerId,
28592
- kind: packed.kind,
28593
- scope: packed.scope,
28594
- role: packed.role,
28595
- content: packed.content,
28596
- tokens: packed.tokens,
28597
- rawScore: packed.rawScore,
28598
- score: packed.score,
28599
- reason: packed.reason,
28600
- ...packed.staleCandidate && { staleCandidate: true }
28601
- };
28602
- }
28603
28769
  function enrichRaw(chunk, providerId) {
28604
28770
  return { ...chunk, providerId };
28605
28771
  }
@@ -28760,9 +28926,8 @@ class ContextOrchestrator {
28760
28926
  excludedNonFloorChunkCount: budgetExcludedIds.length
28761
28927
  });
28762
28928
  }
28763
- const pushMarkdown = renderChunks(packed, {
28764
- priorStageDigest: request.priorStageDigest
28765
- });
28929
+ const renderOptions = { priorStageDigest: request.priorStageDigest };
28930
+ const pushMarkdown = request.agentId !== undefined ? renderForAgent(packed, request.agentId, renderOptions) : renderChunks(packed, renderOptions);
28766
28931
  const digest = buildDigest(packed);
28767
28932
  const dTokens = digestTokens(digest);
28768
28933
  const buildMs = _orchestratorDeps.now() - startMs;
@@ -28799,62 +28964,10 @@ class ContextOrchestrator {
28799
28964
  };
28800
28965
  }
28801
28966
  rebuildForAgent(prior, options = {}) {
28802
- const { newAgentId, failure, priorStageDigest, storyId } = options;
28803
- const targetAgentId = newAgentId ?? prior.agentId ?? DEFAULT_REBUILD_AGENT_ID;
28804
- const logger = _orchestratorDeps.getLogger();
28805
- if (newAgentId && !AGENT_PROFILES[newAgentId]) {
28806
- logger.warn("context-v2", "rebuildForAgent: unknown agent id \u2014 using conservative defaults", {
28807
- ...storyId && { storyId },
28808
- stage: prior.manifest.stage,
28809
- agentId: newAgentId
28810
- });
28811
- }
28812
- const priorChunkIds = prior.chunks.map((c) => c.id);
28813
- const priorAgentForNeutralize = prior.agentId ?? "";
28814
- const packedChunks = prior.chunks.map((c) => {
28815
- const content = newAgentId && newAgentId !== priorAgentForNeutralize && c.kind === "session" ? neutralizeForAgent(c.content, priorAgentForNeutralize, targetAgentId) : c.content;
28816
- return { ...c, content, rawScore: c.score, roleFiltered: false, belowMinScore: false };
28967
+ return _orchestratorDeps.rebuild(prior, options, {
28968
+ uuid: _orchestratorDeps.uuid,
28969
+ getLogger: _orchestratorDeps.getLogger
28817
28970
  });
28818
- if (failure && newAgentId) {
28819
- packedChunks.push(buildFailureNoteChunk(priorAgentForNeutralize || "unknown", newAgentId, failure));
28820
- }
28821
- const pushMarkdown = newAgentId ? renderForAgent(packedChunks, targetAgentId, { priorStageDigest }) : renderChunks(packedChunks, { priorStageDigest });
28822
- const digest = buildDigest(packedChunks);
28823
- const dTokens = digestTokens(digest);
28824
- const rebuildInfo = failure && newAgentId ? {
28825
- priorAgentId: prior.agentId ?? "unknown",
28826
- newAgentId: targetAgentId,
28827
- failureCategory: failure.category,
28828
- failureOutcome: failure.outcome,
28829
- priorChunkIds,
28830
- newChunkIds: packedChunks.map((c) => c.id),
28831
- chunkIdMap: priorChunkIds.map((priorChunkId, index) => {
28832
- const newChunkId = packedChunks[index]?.id;
28833
- return newChunkId ? { priorChunkId, newChunkId } : null;
28834
- }).filter((entry) => entry !== null)
28835
- } : undefined;
28836
- const usedTokens = rebuildUsedTokens(prior, packedChunks, priorStageDigest);
28837
- const targetProfile = getAgentProfile(targetAgentId).profile;
28838
- const rebuiltPullTools = targetProfile.caps.supportsToolCalls ? prior.pullTools : [];
28839
- const manifest = {
28840
- ...prior.manifest,
28841
- requestId: _orchestratorDeps.uuid(),
28842
- includedChunks: packedChunks.map((c) => c.id),
28843
- chunkTokens: Object.fromEntries(packedChunks.map((c) => [c.id, c.tokens])),
28844
- usedTokens,
28845
- digestTokens: dTokens,
28846
- buildMs: 0,
28847
- rebuildInfo,
28848
- effectiveBudget: Math.min(prior.manifest.effectiveBudget ?? Number.POSITIVE_INFINITY, targetProfile.caps.preferredPromptTokens)
28849
- };
28850
- return {
28851
- pushMarkdown,
28852
- pullTools: rebuiltPullTools,
28853
- digest,
28854
- manifest,
28855
- chunks: packedChunks.map(toContextChunk),
28856
- agentId: targetAgentId
28857
- };
28858
28971
  }
28859
28972
  }
28860
28973
  var _orchestratorDeps, PROVIDER_FETCH_TIMEOUT_MS = 5000;
@@ -28867,14 +28980,15 @@ var init_orchestrator = __esm(() => {
28867
28980
  init_digest();
28868
28981
  init_packing();
28869
28982
  init_pull_tools();
28983
+ init_rebuild();
28870
28984
  init_render();
28871
28985
  init_scoring();
28872
- init_scratch_neutralizer();
28873
28986
  init_stage_config();
28874
28987
  _orchestratorDeps = {
28875
28988
  now: () => Date.now(),
28876
- uuid: () => randomUUID2(),
28877
- getLogger
28989
+ uuid: () => randomUUID3(),
28990
+ getLogger,
28991
+ rebuild
28878
28992
  };
28879
28993
  });
28880
28994
 
@@ -30432,9 +30546,10 @@ function resetFailedStoriesToPending(prd, opts = {}) {
30432
30546
  }
30433
30547
  function markStorySkipped(prd, storyId) {
30434
30548
  const story = prd.userStories.find((s) => s.id === storyId);
30435
- if (story) {
30436
- story.status = "skipped";
30437
- }
30549
+ if (!story)
30550
+ return false;
30551
+ story.status = "skipped";
30552
+ return true;
30438
30553
  }
30439
30554
  function resetStoryToPending(prd, storyId) {
30440
30555
  const story = prd.userStories.find((s) => s.id === storyId);
@@ -32804,6 +32919,7 @@ var init_engine = __esm(() => {
32804
32919
  init_packing();
32805
32920
  init_render();
32806
32921
  init_digest();
32922
+ init_rebuild();
32807
32923
  init_available_budget();
32808
32924
  init_stage_config();
32809
32925
  init_static_rules();
@@ -33384,15 +33500,23 @@ Set \`approved: false\` when ANY of these conditions are true:
33384
33500
  - The implementer loosened test assertions to mask bugs
33385
33501
  - The implementer made illegitimate test changes
33386
33502
 
33503
+ When tests fail but the implementation satisfies every acceptance criterion and
33504
+ a specific test assertion contradicts the specification, set
33505
+ \`testFailureDiagnosis.cause\` to \`"test-incorrect"\` and identify each
33506
+ assertion with its file, test name, and reasoning. Do not use this diagnosis if
33507
+ the implementer modified tests or any acceptance criterion is unmet.
33508
+
33387
33509
  **JSON schema** (fill in all fields with real values):
33388
33510
 
33389
33511
  \`\`\`json
33390
- {"version":1,"approved":true,"tests":{"allPassing":true,"passCount":42,"failCount":0},"testModifications":{"detected":false,"files":[],"legitimate":true,"reasoning":"..."},"acceptanceCriteria":{"allMet":true,"criteria":[{"criterion":"...","met":true}]},"quality":{"rating":"good","issues":[]},"fixes":[],"reasoning":"..."}
33512
+ {"version":1,"approved":true,"tests":{"allPassing":true,"passCount":42,"failCount":0},"testModifications":{"detected":false,"files":[],"legitimate":true,"reasoning":"..."},"testFailureDiagnosis":null,"acceptanceCriteria":{"allMet":true,"criteria":[{"criterion":"...","met":true}]},"quality":{"rating":"good","issues":[]},"fixes":[],"reasoning":"..."}
33391
33513
  \`\`\`
33392
33514
 
33393
33515
  **Field notes:**
33394
33516
  - \`quality.rating\` must be one of: \`"good"\`, \`"acceptable"\`, \`"poor"\`
33395
33517
  - \`testModifications.files\` \u2014 list any test files the implementer changed
33518
+ - \`testFailureDiagnosis\` \u2014 normally \`null\`; for a concrete incorrect-test
33519
+ diagnosis use \`{"cause":"test-incorrect","assertions":[{"file":"...","testName":"...","reasoning":"..."}]}\`
33396
33520
  - \`acceptanceCriteria\` and \`quality\` are advisory in this TDD verifier verdict; do not use them to reject semantic correctness
33397
33521
  - \`fixes\` \u2014 keep this empty; the verifier must not apply code or test fixes
33398
33522
  - \`reasoning\` \u2014 brief summary of your overall assessment
@@ -33833,7 +33957,8 @@ class TddPromptBuilder {
33833
33957
  ` + `Re-emit the verdict as the FINAL content of your reply.
33834
33958
  ` + `Output ONLY the JSON object \u2014 no markdown fences, no explanation, no prose.
33835
33959
  ` + `The reply must start with { and end with } on its own line.
33836
- ` + "Required top-level fields: version, approved, tests, testModifications, acceptanceCriteria, quality, fixes, reasoning.";
33960
+ ` + `Required top-level fields: version, approved, tests, testModifications, acceptanceCriteria, quality, fixes, reasoning.
33961
+ ` + 'Optional testFailureDiagnosis: null, or {"cause":"test-incorrect","assertions":[{"file":"...","testName":"...","reasoning":"..."}]}.';
33837
33962
  }
33838
33963
  static verdictRetryCondensed() {
33839
33964
  return `Your previous reply was truncated and could not be parsed as valid JSON.
@@ -33844,7 +33969,7 @@ class TddPromptBuilder {
33844
33969
  ` + `- Set reasoning to a single sentence.
33845
33970
  ` + `Output ONLY the JSON object \u2014 no markdown fences, no prose.
33846
33971
  ` + `Schema (minimal):
33847
- ` + `{"version":1,"approved":boolean,"tests":{"allPassing":boolean,"passCount":number,"failCount":number},"testModifications":{"detected":boolean,"files":[],"legitimate":boolean,"reasoning":"..."},"acceptanceCriteria":{"allMet":boolean,"criteria":[]},"quality":{"rating":"good"|"acceptable"|"poor","issues":[]},"fixes":[],"reasoning":"..."}`;
33972
+ ` + `{"version":1,"approved":boolean,"tests":{"allPassing":boolean,"passCount":number,"failCount":number},"testModifications":{"detected":boolean,"files":[],"legitimate":boolean,"reasoning":"..."},"testFailureDiagnosis":null,"acceptanceCriteria":{"allMet":boolean,"criteria":[]},"quality":{"rating":"good"|"acceptable"|"poor","issues":[]},"fixes":[],"reasoning":"..."}`;
33848
33973
  }
33849
33974
  s(id, content) {
33850
33975
  return { id, content, overridable: false };
@@ -39861,6 +39986,21 @@ var init_implement = __esm(() => {
39861
39986
  // src/tdd/verdict-reader.ts
39862
39987
  import { unlink } from "fs/promises";
39863
39988
  import path4 from "path";
39989
+ function isValidTestFailureDiagnosis(value) {
39990
+ if (!value || typeof value !== "object")
39991
+ return false;
39992
+ const diagnosis = value;
39993
+ if (!["implementation", "test-incorrect", "unknown"].includes(diagnosis.cause))
39994
+ return false;
39995
+ if (!Array.isArray(diagnosis.assertions))
39996
+ return false;
39997
+ return diagnosis.assertions.every((assertion) => {
39998
+ if (!assertion || typeof assertion !== "object")
39999
+ return false;
40000
+ const item = assertion;
40001
+ return typeof item.file === "string" && typeof item.reasoning === "string" && (item.testName === undefined || typeof item.testName === "string");
40002
+ });
40003
+ }
39864
40004
  function isValidVerdict(obj) {
39865
40005
  if (!obj || typeof obj !== "object")
39866
40006
  return false;
@@ -39889,6 +40029,9 @@ function isValidVerdict(obj) {
39889
40029
  return false;
39890
40030
  if (typeof mods.reasoning !== "string")
39891
40031
  return false;
40032
+ if (v.testFailureDiagnosis !== undefined && v.testFailureDiagnosis !== null && !isValidTestFailureDiagnosis(v.testFailureDiagnosis)) {
40033
+ return false;
40034
+ }
39892
40035
  if (!v.acceptanceCriteria || typeof v.acceptanceCriteria !== "object")
39893
40036
  return false;
39894
40037
  const ac = v.acceptanceCriteria;
@@ -39989,6 +40132,7 @@ function coerceVerdict(obj) {
39989
40132
  legitimate: true,
39990
40133
  reasoning: "Not assessed in free-form verdict"
39991
40134
  },
40135
+ ...isValidTestFailureDiagnosis(obj.testFailureDiagnosis) ? { testFailureDiagnosis: obj.testFailureDiagnosis } : {},
39992
40136
  acceptanceCriteria: { allMet, criteria },
39993
40137
  quality: { rating, issues: [] },
39994
40138
  fixes: Array.isArray(obj.fixes) ? obj.fixes : [],
@@ -40088,9 +40232,6 @@ function categorizeVerdict(verdict, testsPass) {
40088
40232
  reviewReason: "Tests failing after all sessions (no verdict file)"
40089
40233
  };
40090
40234
  }
40091
- if (verdict.approved) {
40092
- return { success: true };
40093
- }
40094
40235
  if (verdict.testModifications.detected && !verdict.testModifications.legitimate) {
40095
40236
  const files = verdict.testModifications.files.join(", ") || "unknown files";
40096
40237
  return {
@@ -40100,6 +40241,14 @@ function categorizeVerdict(verdict, testsPass) {
40100
40241
  };
40101
40242
  }
40102
40243
  if (!verdict.tests.allPassing) {
40244
+ if (hasAdmissibleIncorrectTestDiagnosis(verdict)) {
40245
+ const files = verdict.testFailureDiagnosis?.assertions.map((assertion) => assertion.file).join(", ");
40246
+ return {
40247
+ success: false,
40248
+ failureCategory: "test-incorrect",
40249
+ reviewReason: `Verifier identified incorrect test assertion(s) in ${files}; human review required before changing tests or source.`
40250
+ };
40251
+ }
40103
40252
  return {
40104
40253
  success: false,
40105
40254
  failureCategory: "tests-failing",
@@ -40108,6 +40257,10 @@ function categorizeVerdict(verdict, testsPass) {
40108
40257
  }
40109
40258
  return { success: true };
40110
40259
  }
40260
+ function hasAdmissibleIncorrectTestDiagnosis(verdict) {
40261
+ const diagnosis = verdict.testFailureDiagnosis;
40262
+ return verdict.approved === false && verdict.testModifications.detected === false && verdict.acceptanceCriteria.allMet === true && diagnosis?.cause === "test-incorrect" && diagnosis.assertions.length > 0 && diagnosis.assertions.every((assertion) => assertion.file.trim() !== "" && assertion.reasoning.trim() !== "");
40263
+ }
40111
40264
  var init_verdict = __esm(() => {
40112
40265
  init_verdict_reader();
40113
40266
  });
@@ -40149,6 +40302,23 @@ function buildVerifierFindings(verdict, categorization) {
40149
40302
  }
40150
40303
  ];
40151
40304
  }
40305
+ case "test-incorrect": {
40306
+ const assertions = verdict.testFailureDiagnosis?.assertions ?? [];
40307
+ const files = assertions.map((assertion) => assertion.file);
40308
+ return [
40309
+ {
40310
+ source: "tdd-verifier",
40311
+ severity: "error",
40312
+ category: "incorrect-test-assertion",
40313
+ fixTarget: "test",
40314
+ message: `Verifier identified incorrect test assertion(s) in ${files.join(", ")}; human review required`,
40315
+ meta: {
40316
+ assertions,
40317
+ reasoning: verdict.reasoning
40318
+ }
40319
+ }
40320
+ ];
40321
+ }
40152
40322
  default:
40153
40323
  return [];
40154
40324
  }
@@ -44093,6 +44263,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
44093
44263
  const now = _deps.now ?? _cycleDeps.now;
44094
44264
  const storyId = ctx.storyId;
44095
44265
  const packageDir = ctx.packageDir;
44266
+ const logCtx = { storyId, packageDir, cycleName };
44096
44267
  let totalCostUsd = 0;
44097
44268
  const declines = createDeclineLedger(_deps.declineBacking);
44098
44269
  let unresolvedDetail;
@@ -44108,9 +44279,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
44108
44279
  const orphanSources = [...new Set(cycle.findings.map((f) => f.source))];
44109
44280
  const retiredStrategies = declines.retiredNames(cycle.strategies, cycle.findings);
44110
44281
  logger?.warn("findings.cycle", "cycle exited \u2014 no matching strategy (orphaned findings)", {
44111
- storyId,
44112
- packageDir,
44113
- cycleName,
44282
+ ...logCtx,
44114
44283
  reason: "no-strategy",
44115
44284
  findingsCount: cycle.findings.length,
44116
44285
  orphanSources,
@@ -44127,9 +44296,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
44127
44296
  if (uncappedActive.length === 0) {
44128
44297
  const exhaustedStrategy = active.find((s) => countStrategyAttempts(history, s.name) >= s.maxAttempts);
44129
44298
  logger?.info("findings.cycle", "cycle exited \u2014 all active strategies exhausted", {
44130
- storyId,
44131
- packageDir,
44132
- cycleName,
44299
+ ...logCtx,
44133
44300
  reason: "max-attempts-per-strategy",
44134
44301
  exhaustedStrategy: exhaustedStrategy?.name
44135
44302
  });
@@ -44144,9 +44311,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
44144
44311
  const totalAttempts = countTotalAttempts(history);
44145
44312
  if (totalAttempts >= cycle.config.maxAttemptsTotal) {
44146
44313
  logger?.info("findings.cycle", "cycle exited \u2014 total attempt cap reached", {
44147
- storyId,
44148
- packageDir,
44149
- cycleName,
44314
+ ...logCtx,
44150
44315
  reason: "max-attempts-total",
44151
44316
  totalAttempts,
44152
44317
  maxAttemptsTotal: cycle.config.maxAttemptsTotal
@@ -44161,13 +44326,14 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
44161
44326
  for (const strategy of uncappedActive) {
44162
44327
  const bailReason = strategy.bailWhen?.(history) ?? null;
44163
44328
  if (bailReason !== null) {
44329
+ const inheritedIterations = cycle.priorIterations?.length ?? 0;
44164
44330
  logger?.info("findings.cycle", "cycle exited \u2014 bail predicate fired", {
44165
- storyId,
44166
- packageDir,
44167
- cycleName,
44331
+ ...logCtx,
44168
44332
  reason: "bail-when",
44169
44333
  strategyName: strategy.name,
44170
- bailDetail: bailReason
44334
+ bailDetail: bailReason,
44335
+ cycleIterations: cycle.iterations.length,
44336
+ ...inheritedIterations > 0 ? { inheritedIterations } : {}
44171
44337
  });
44172
44338
  return finish({
44173
44339
  iterations: cycle.iterations,
@@ -44222,9 +44388,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
44222
44388
  }, { storyId, packageDir, cycleName }, logger);
44223
44389
  totalCostUsd += fixesApplied.reduce((sum, fa) => sum + (fa.costUsd ?? 0), 0);
44224
44390
  logger?.info("findings.cycle", "cycle exited \u2014 agent gave up", {
44225
- storyId,
44226
- packageDir,
44227
- cycleName,
44391
+ ...logCtx,
44228
44392
  reason: "agent-gave-up",
44229
44393
  strategyName: firstUnresolved.strategyName,
44230
44394
  unresolvedDetail: firstUnresolved.unresolved
@@ -44238,9 +44402,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
44238
44402
  });
44239
44403
  }
44240
44404
  logger?.info("findings.cycle", "strategy gave up \u2014 retired, continuing with co-run siblings", {
44241
- storyId,
44242
- packageDir,
44243
- cycleName,
44405
+ ...logCtx,
44244
44406
  strategyName: firstUnresolved.strategyName,
44245
44407
  unresolvedDetail: firstUnresolved.unresolved,
44246
44408
  ranWithoutGivingUp: fixesApplied.filter((fa) => !fa.unresolved).map((fa) => fa.strategyName)
@@ -44271,9 +44433,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
44271
44433
  finishedAt: finishedAt3
44272
44434
  }, { storyId, packageDir, cycleName }, logger);
44273
44435
  logger?.warn("findings.cycle", "lite validate failed on terminal exhausted branch", {
44274
- storyId,
44275
- packageDir,
44276
- cycleName,
44436
+ ...logCtx,
44277
44437
  error: errorMessage(err)
44278
44438
  });
44279
44439
  return finish({
@@ -44297,9 +44457,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
44297
44457
  cycle.findings = liteFindingsAfter;
44298
44458
  if (liteFindingsAfter.length === 0 && !liteShortCircuited) {
44299
44459
  logger?.info("findings.cycle", "cycle exited \u2014 resolved after terminal lite validate", {
44300
- storyId,
44301
- packageDir,
44302
- cycleName,
44460
+ ...logCtx,
44303
44461
  reason: "resolved"
44304
44462
  });
44305
44463
  return finish({
@@ -44313,18 +44471,14 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
44313
44471
  const companions = uncappedActive.filter((s) => !group.includes(s));
44314
44472
  if (companions.length > 0) {
44315
44473
  logger?.info("findings.cycle", "exclusive strategy exhausted \u2014 continuing to companion strategies", {
44316
- storyId,
44317
- packageDir,
44318
- cycleName,
44474
+ ...logCtx,
44319
44475
  exhaustedStrategies: group.map((s) => s.name),
44320
44476
  remainingStrategies: companions.map((s) => s.name)
44321
44477
  });
44322
44478
  continue;
44323
44479
  }
44324
44480
  logger?.info("findings.cycle", "cycle exited \u2014 validate short-circuited", {
44325
- storyId,
44326
- packageDir,
44327
- cycleName,
44481
+ ...logCtx,
44328
44482
  reason: "validate-short-circuit",
44329
44483
  liteFindingsAfterCount: liteFindingsAfter.length
44330
44484
  });
@@ -44336,9 +44490,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
44336
44490
  });
44337
44491
  }
44338
44492
  logger?.info("findings.cycle", "cycle exited \u2014 strategy attempt cap reached (lite validate)", {
44339
- storyId,
44340
- packageDir,
44341
- cycleName,
44493
+ ...logCtx,
44342
44494
  reason: "max-attempts-per-strategy",
44343
44495
  exhaustedStrategy: group[0]?.name,
44344
44496
  liteFindingsAfterCount: liteFindingsAfter.length
@@ -44375,9 +44527,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
44375
44527
  });
44376
44528
  }
44377
44529
  logger?.warn("findings.cycle", "validator retry", {
44378
- storyId,
44379
- packageDir,
44380
- cycleName,
44530
+ ...logCtx,
44381
44531
  attempt: validatorAttempt + 1,
44382
44532
  error: errorMessage(err)
44383
44533
  });
@@ -44431,8 +44581,8 @@ var NAX_BAIL_WRAPPER = "__naxBailWrapper";
44431
44581
  function createStoryFixHistory() {
44432
44582
  return new Map;
44433
44583
  }
44434
- function storyFixKey(storyId, tier) {
44435
- return `${storyId}::${tier ?? "default"}`;
44584
+ function storyFixKey(storyId, tier, agent) {
44585
+ return `${storyId}::${tier ?? "default"}::${agent ?? "default"}`;
44436
44586
  }
44437
44587
  function getStoryFixState(store, key) {
44438
44588
  let existing = store.get(key);
@@ -44958,7 +45108,7 @@ var package_default;
44958
45108
  var init_package = __esm(() => {
44959
45109
  package_default = {
44960
45110
  name: "@nathapp/nax",
44961
- version: "0.77.3",
45111
+ version: "0.78.0",
44962
45112
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
44963
45113
  type: "module",
44964
45114
  bin: {
@@ -44997,13 +45147,17 @@ var init_package = __esm(() => {
44997
45147
  "test:e2e": "timeout -k 5s 180s bun test test/e2e/ --timeout=60000",
44998
45148
  "test:coverage": "bun run scripts/check-coverage.ts",
44999
45149
  "test:coverage:report": "bun run scripts/check-coverage.ts --report",
45000
- "check-test-overlap": "bun run scripts/check-test-overlap.ts",
45001
- "check-dead-tests": "bun run scripts/check-dead-tests.ts",
45002
- "check:test-sizes": "bun run scripts/check-test-sizes.ts",
45150
+ "report:test-overlap": "bun run scripts/report-test-overlap.ts",
45151
+ "report:dead-tests": "bun run scripts/report-dead-tests.ts",
45003
45152
  "check:test-mocks": "bun scripts/check-inline-test-mocks.ts --strict",
45004
45153
  "check:process-cwd": "bash scripts/check-process-cwd.sh",
45005
45154
  "check:no-adapter-wrap": "bash scripts/check-no-adapter-wrap.sh",
45006
45155
  "check:dispatch-context": "bash scripts/check-dispatch-context.sh",
45156
+ "check:naxconfig-cast": "bash scripts/check-no-silent-naxconfig-cast.sh",
45157
+ "check:runtime-cleanup": "bash scripts/check-runtime-cleanup.sh",
45158
+ "check:adapter-no-config-import": "bash scripts/check-adapter-no-config-import.sh",
45159
+ "check:gate-reachability": "bun run scripts/check-gate-reachability.ts",
45160
+ "check:all": "bun run lint && bun run check:test-mocks && bun run check:process-cwd && bun run check:no-adapter-wrap && bun run check:dispatch-context && bun run check:naxconfig-cast && bun run check:runtime-cleanup && bun run check:adapter-no-config-import && bun run check:gate-reachability",
45007
45161
  prepublishOnly: "bun run build",
45008
45162
  "test:full": "FULL=1 NAX_PRECHECK=1 bun test test/ --timeout=60000"
45009
45163
  },
@@ -45062,8 +45216,8 @@ var init_version = __esm(() => {
45062
45216
  NAX_VERSION = package_default.version;
45063
45217
  NAX_COMMIT = (() => {
45064
45218
  try {
45065
- if (/^[0-9a-f]{6,10}$/.test("e42ce962"))
45066
- return "e42ce962";
45219
+ if (/^[0-9a-f]{6,10}$/.test("af86acb4"))
45220
+ return "af86acb4";
45067
45221
  } catch {}
45068
45222
  try {
45069
45223
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -51129,7 +51283,7 @@ var init_pid_registry = __esm(() => {
51129
51283
  });
51130
51284
 
51131
51285
  // src/session/manager-deps.ts
51132
- import { randomUUID as randomUUID3 } from "crypto";
51286
+ import { randomUUID as randomUUID4 } from "crypto";
51133
51287
  import { mkdir as mkdir5 } from "fs/promises";
51134
51288
  import { isAbsolute as isAbsolute11, join as join32, relative as relative12, sep as sep3 } from "path";
51135
51289
  function resolveProjectDirFromScratchDir(scratchDir) {
@@ -51151,7 +51305,7 @@ var init_manager_deps = __esm(() => {
51151
51305
  _sessionManagerDeps = {
51152
51306
  now: () => new Date().toISOString(),
51153
51307
  nowMs: () => Date.now(),
51154
- uuid: () => randomUUID3(),
51308
+ uuid: () => randomUUID4(),
51155
51309
  sessionScratchDir: (projectDir, featureName, sessionId) => join32(projectDir, ".nax", "features", featureName, "sessions", sessionId),
51156
51310
  writeDescriptor: async (scratchDir, descriptor, projectDir) => {
51157
51311
  await mkdir5(scratchDir, { recursive: true });
@@ -60272,7 +60426,7 @@ var init_completion = __esm(() => {
60272
60426
  const isBatch = ctx.stories.length > 1;
60273
60427
  const sessionCost = ctx.runtime.costAggregator.byStory()[ctx.story.id]?.totalCostUsd ?? 0;
60274
60428
  const persistPrd2 = ctx.skipPrdPersistence !== true;
60275
- const prdPath = ctx.prdPath ?? (ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`);
60429
+ const prdPath = ctx.prdPath ?? (ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/.nax/features/unknown/prd.json`);
60276
60430
  const storyStartTime = ctx.storyStartTime || new Date().toISOString();
60277
60431
  if (isBatch) {
60278
60432
  ctx.storyMetrics = collectBatchMetrics(ctx, storyStartTime);
@@ -60719,7 +60873,7 @@ var init_helpers = __esm(() => {
60719
60873
  });
60720
60874
 
60721
60875
  // src/pipeline/stages/context.ts
60722
- import { randomUUID as randomUUID4 } from "crypto";
60876
+ import { randomUUID as randomUUID5 } from "crypto";
60723
60877
  async function runV2Path(ctx) {
60724
60878
  const logger = getLogger();
60725
60879
  const agentName = ctx.routing.agent ?? ctx.agentManager?.getDefault() ?? "claude";
@@ -60943,7 +61097,7 @@ var init_context2 = __esm(() => {
60943
61097
  createOrchestrator: createDefaultOrchestrator,
60944
61098
  loadPlugins: loadPluginProviders,
60945
61099
  v1FeatureProvider: () => new FeatureContextProvider,
60946
- uuid: () => randomUUID4(),
61100
+ uuid: () => randomUUID5(),
60947
61101
  readDigest: readDigestFile,
60948
61102
  writeDigest: writeDigestFile
60949
61103
  };
@@ -60991,6 +61145,23 @@ async function spawnWithTimeout(proc, timeoutMs) {
60991
61145
  function captureFailureSentinel() {
60992
61146
  return `__capture_failed__:${Date.now()}:${Math.random().toString(36).slice(2)}`;
60993
61147
  }
61148
+ async function untrackedDigestInput(deps, workdir) {
61149
+ const listed = await spawnWithTimeout(spawnGit(deps, ["ls-files", "--others", "--exclude-standard", "-z"], workdir), TREE_CAPTURE_TIMEOUT_MS);
61150
+ if (listed.exitCode !== 0)
61151
+ return null;
61152
+ const paths = listed.stdout.split("\x00").filter((p) => p !== "");
61153
+ if (paths.length === 0)
61154
+ return "0";
61155
+ paths.sort();
61156
+ const capped = paths.slice(0, MAX_UNTRACKED_HASHED);
61157
+ const hashed = await spawnWithTimeout(spawnGit(deps, ["hash-object", "--", ...capped], workdir), UNTRACKED_HASH_TIMEOUT_MS);
61158
+ if (hashed.exitCode !== 0)
61159
+ return null;
61160
+ return `${paths.length}
61161
+ ${capped.join(`
61162
+ `)}
61163
+ ${hashed.stdout.trim()}`;
61164
+ }
60994
61165
  async function captureTreeState(workdir, options) {
60995
61166
  let headSha = "";
60996
61167
  let dirtyDigest = "";
@@ -61002,14 +61173,29 @@ async function captureTreeState(workdir, options) {
61002
61173
  headSha = captureFailureSentinel();
61003
61174
  }
61004
61175
  try {
61005
- const proc = spawnGit(options._deps, ["status", "--porcelain"], workdir);
61006
- const { stdout, exitCode } = await spawnWithTimeout(proc, TREE_CAPTURE_TIMEOUT_MS);
61007
- if (exitCode === 0) {
61008
- const trimmed = stdout.trim();
61009
- if (trimmed) {
61010
- const hasher = new Bun.CryptoHasher("sha256");
61011
- hasher.update(trimmed);
61012
- dirtyDigest = hasher.digest("hex");
61176
+ const statusProc = spawnGit(options._deps, ["status", "--porcelain"], workdir);
61177
+ const status = await spawnWithTimeout(statusProc, TREE_CAPTURE_TIMEOUT_MS);
61178
+ if (status.exitCode === 0) {
61179
+ const trimmedStatus = status.stdout.trim();
61180
+ if (trimmedStatus) {
61181
+ const diffProc = spawnGit(options._deps, ["diff"], workdir);
61182
+ const diff = await spawnWithTimeout(diffProc, TREE_CAPTURE_TIMEOUT_MS);
61183
+ const cachedDiffProc = spawnGit(options._deps, ["diff", "--cached"], workdir);
61184
+ const cachedDiff = await spawnWithTimeout(cachedDiffProc, TREE_CAPTURE_TIMEOUT_MS);
61185
+ const untracked = await untrackedDigestInput(options._deps, workdir);
61186
+ if (diff.exitCode === 0 && cachedDiff.exitCode === 0 && untracked !== null) {
61187
+ const hasher = new Bun.CryptoHasher("sha256");
61188
+ hasher.update(trimmedStatus);
61189
+ hasher.update("\x00");
61190
+ hasher.update(diff.stdout);
61191
+ hasher.update("\x00");
61192
+ hasher.update(cachedDiff.stdout);
61193
+ hasher.update("\x00");
61194
+ hasher.update(untracked);
61195
+ dirtyDigest = hasher.digest("hex");
61196
+ } else {
61197
+ dirtyDigest = captureFailureSentinel();
61198
+ }
61013
61199
  }
61014
61200
  } else {
61015
61201
  dirtyDigest = captureFailureSentinel();
@@ -61028,7 +61214,7 @@ function buildCheckpointLogData(meta3) {
61028
61214
  const { storyId, ...rest } = meta3;
61029
61215
  return { storyId, ...rest };
61030
61216
  }
61031
- var TREE_CAPTURE_TIMEOUT_MS = 75;
61217
+ var TREE_CAPTURE_TIMEOUT_MS = 75, MAX_UNTRACKED_HASHED = 500, UNTRACKED_HASH_TIMEOUT_MS = 250;
61032
61218
 
61033
61219
  // src/tdd/rollback.ts
61034
61220
  async function rollbackToRef(workdir, ref) {
@@ -62350,12 +62536,19 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides)
62350
62536
  if (initialFindings.length === 0) {
62351
62537
  return {};
62352
62538
  }
62539
+ if (initialFindings.some((finding) => finding.category === "incorrect-test-assertion")) {
62540
+ getSafeLogger()?.warn("story-orchestrator", "Incorrect test diagnosis requires human review", {
62541
+ storyId: ctx.storyId,
62542
+ findingCount: initialFindings.length
62543
+ });
62544
+ return { terminalReviewRequired: true, unfixedFindings: initialFindings };
62545
+ }
62353
62546
  if (!ctx.storyId) {
62354
62547
  return {};
62355
62548
  }
62356
62549
  const storyFixBudgetEnabled = !nbfPath && ctx.runtime.configLoader.current().execution?.rectification?.storyScopedFixBudget === true;
62357
62550
  const store = ctx.runtime.storyFixHistory;
62358
- const fixKey = storyFixBudgetEnabled ? storyFixKey(ctx.storyId, ctx.phaseTelemetry?.tier) : undefined;
62551
+ const fixKey = storyFixBudgetEnabled ? storyFixKey(ctx.storyId, ctx.phaseTelemetry?.tier, ctx.agentName) : undefined;
62359
62552
  const fixState = fixKey !== undefined && store ? getStoryFixState(store, fixKey) : undefined;
62360
62553
  const priorIterationCount = fixState?.iterations.length ?? 0;
62361
62554
  const declineSnapshot = fixState ? new Map([...fixState.declines].map(([name, keys]) => [name, new Set(keys)])) : undefined;
@@ -62570,7 +62763,7 @@ class ExecutionPlan {
62570
62763
  const rectResult = await runRectification(this.ctx, this.state, phaseCosts, phaseOutputs, {
62571
62764
  gateBaselineKeys: preRectGateFailureKeys
62572
62765
  });
62573
- if (this.state.rectification && (!rectResult.rectificationExhausted || rectResult.liteScopeIncomplete)) {
62766
+ if (this.state.rectification && !rectResult.terminalReviewRequired && (!rectResult.rectificationExhausted || rectResult.liteScopeIncomplete)) {
62574
62767
  let resumeRectifyUsed = false;
62575
62768
  for (const phase of collectOrderedPhases(this.state)) {
62576
62769
  const name = phase.slot.op.name;
@@ -63342,6 +63535,8 @@ function routeTddFailure(failureCategory, isLiteMode, ctx, reviewReason, failure
63342
63535
  };
63343
63536
  case "dependency-prep":
63344
63537
  return pauseFallback;
63538
+ case "test-incorrect":
63539
+ return pauseFallback;
63345
63540
  default:
63346
63541
  return pauseFallback;
63347
63542
  }
@@ -64220,6 +64415,7 @@ function parseQueueFile(content) {
64220
64415
  var init_queue = () => {};
64221
64416
 
64222
64417
  // src/execution/queue-handler.ts
64418
+ import { rename, unlink as unlink3 } from "fs/promises";
64223
64419
  import path15 from "path";
64224
64420
  function getSafeLogger4() {
64225
64421
  try {
@@ -64239,8 +64435,11 @@ async function readQueueFile(workdir) {
64239
64435
  return [];
64240
64436
  }
64241
64437
  try {
64242
- await Bun.spawn(["mv", queuePath, processingPath], { stdout: "pipe" }).exited;
64438
+ await rename(queuePath, processingPath);
64243
64439
  } catch (error48) {
64440
+ logger?.warn("queue", "Failed to rename queue file for processing", {
64441
+ error: error48.message
64442
+ });
64244
64443
  return [];
64245
64444
  }
64246
64445
  const processingFile = Bun.file(processingPath);
@@ -64261,7 +64460,7 @@ async function clearQueueFile(workdir) {
64261
64460
  const file3 = Bun.file(processingPath);
64262
64461
  const exists = await file3.exists();
64263
64462
  if (exists) {
64264
- await Bun.spawn(["rm", processingPath], { stdout: "pipe" }).exited;
64463
+ await unlink3(processingPath);
64265
64464
  }
64266
64465
  } catch (error48) {
64267
64466
  logger?.warn("queue", "Failed to clear queue file", {
@@ -64276,6 +64475,20 @@ var init_queue_handler = __esm(() => {
64276
64475
 
64277
64476
  // src/pipeline/stages/queue-check.ts
64278
64477
  import path16 from "path";
64478
+ function resolvePrdPath(ctx) {
64479
+ return ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/.nax/features/unknown/prd.json`;
64480
+ }
64481
+ function logDroppedCommands(logger, ctx, queueCommands, currentIndex) {
64482
+ const dropped = queueCommands.slice(currentIndex + 1);
64483
+ if (dropped.length === 0) {
64484
+ return;
64485
+ }
64486
+ logger.warn("queue", "Dropped unprocessed queue commands", {
64487
+ storyId: ctx.story?.id ?? "unknown",
64488
+ droppedCount: dropped.length,
64489
+ droppedTypes: dropped.map((c) => c.type)
64490
+ });
64491
+ }
64279
64492
  var queueCheckStage;
64280
64493
  var init_queue_check = __esm(() => {
64281
64494
  init_config();
@@ -64292,9 +64505,10 @@ var init_queue_check = __esm(() => {
64292
64505
  if (queueCommands.length === 0) {
64293
64506
  return { action: "continue" };
64294
64507
  }
64295
- for (const cmd of queueCommands) {
64508
+ for (const [index, cmd] of queueCommands.entries()) {
64296
64509
  if (cmd.type === "PAUSE") {
64297
64510
  logger.warn("queue", "Paused by user", { storyId: ctx.story?.id ?? "unknown", command: "PAUSE" });
64511
+ logDroppedCommands(logger, ctx, queueCommands, index);
64298
64512
  await clearQueueFile(ctx.workdir);
64299
64513
  return { action: "pause", reason: "User requested pause via .queue.txt" };
64300
64514
  }
@@ -64305,16 +64519,15 @@ var init_queue_check = __esm(() => {
64305
64519
  markStorySkipped(ctx.prd, s.id);
64306
64520
  }
64307
64521
  }
64308
- const prdPath = ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`;
64309
- await savePRD(ctx.prd, prdPath);
64522
+ await savePRD(ctx.prd, resolvePrdPath(ctx));
64523
+ logDroppedCommands(logger, ctx, queueCommands, index);
64310
64524
  await clearQueueFile(ctx.workdir);
64311
64525
  return { action: "pause", reason: "User requested abort" };
64312
64526
  }
64313
64527
  if (cmd.type === "RETRY") {
64314
64528
  logger.warn("queue", "Retrying story by user request", { storyId: cmd.storyId });
64315
64529
  resetStoryToPending(ctx.prd, cmd.storyId);
64316
- const prdPath = ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`;
64317
- await savePRD(ctx.prd, prdPath);
64530
+ await savePRD(ctx.prd, resolvePrdPath(ctx));
64318
64531
  continue;
64319
64532
  }
64320
64533
  if (cmd.type === "PRIORITY") {
@@ -64323,8 +64536,7 @@ var init_queue_check = __esm(() => {
64323
64536
  priority: cmd.value
64324
64537
  });
64325
64538
  setStoryPriority(ctx.prd, cmd.storyId, cmd.value);
64326
- const prdPath = ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`;
64327
- await savePRD(ctx.prd, prdPath);
64539
+ await savePRD(ctx.prd, resolvePrdPath(ctx));
64328
64540
  continue;
64329
64541
  }
64330
64542
  if (cmd.type === "INJECT") {
@@ -64342,8 +64554,7 @@ var init_queue_check = __esm(() => {
64342
64554
  injectedStoryId: story.id,
64343
64555
  storyFile: cmd.storyFile
64344
64556
  });
64345
- const prdPath = ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`;
64346
- await savePRD(ctx.prd, prdPath);
64557
+ await savePRD(ctx.prd, resolvePrdPath(ctx));
64347
64558
  } catch (err) {
64348
64559
  logger.error("queue", "Failed to inject story \u2014 skipping INJECT command", {
64349
64560
  storyId: ctx.story?.id ?? "unknown",
@@ -64354,16 +64565,19 @@ var init_queue_check = __esm(() => {
64354
64565
  continue;
64355
64566
  }
64356
64567
  if (cmd.type === "SKIP") {
64357
- const isTargeted = ctx.stories.some((s) => s.id === cmd.storyId);
64358
- if (isTargeted) {
64359
- logger.warn("queue", "Skipping story by user request", {
64568
+ if (markStorySkipped(ctx.prd, cmd.storyId)) {
64569
+ logger.warn("queue", "Skipping story by user request", { storyId: cmd.storyId });
64570
+ await savePRD(ctx.prd, resolvePrdPath(ctx));
64571
+ } else {
64572
+ logger.warn("queue", "SKIP names a story that is not in the PRD \u2014 ignoring", {
64360
64573
  storyId: cmd.storyId
64361
64574
  });
64362
- markStorySkipped(ctx.prd, cmd.storyId);
64363
- const prdPath = ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`;
64364
- await savePRD(ctx.prd, prdPath);
64575
+ }
64576
+ const isTargeted = ctx.stories.some((s) => s.id === cmd.storyId);
64577
+ if (isTargeted) {
64365
64578
  ctx.stories = ctx.stories.filter((s) => s.id !== cmd.storyId);
64366
64579
  if (ctx.stories.length === 0) {
64580
+ logDroppedCommands(logger, ctx, queueCommands, index);
64367
64581
  await clearQueueFile(ctx.workdir);
64368
64582
  return { action: "skip", reason: "All stories in batch were skipped" };
64369
64583
  }
@@ -64409,7 +64623,7 @@ var init_routing2 = __esm(() => {
64409
64623
  candidateTier
64410
64624
  });
64411
64625
  }
64412
- const isEscalated = previousTier !== undefined && (previousRank !== undefined && candidateRank !== undefined ? previousRank > candidateRank : hasEscalationRecords);
64626
+ const isEscalated = previousTier !== undefined && (hasEscalationRecords || previousRank !== undefined && candidateRank !== undefined && previousRank > candidateRank);
64413
64627
  const modelTier = isEscalated ? previousTier : candidateTier;
64414
64628
  const routing = { ...decision, modelTier, agent: ctx.story.routing?.agent ?? decision.agent };
64415
64629
  const neverEscalated = !hasEscalationRecords;
@@ -69779,7 +69993,7 @@ var init_writer = __esm(() => {
69779
69993
  });
69780
69994
 
69781
69995
  // src/execution/checkpoint/reader.ts
69782
- import { join as join85 } from "path";
69996
+ import { join as join86 } from "path";
69783
69997
  function isValidRecord(value) {
69784
69998
  if (!value || typeof value !== "object")
69785
69999
  return false;
@@ -69812,7 +70026,7 @@ async function defaultRead(filePath) {
69812
70026
  return file3.text();
69813
70027
  }
69814
70028
  async function loadCheckpoints(featureDir, options = { _deps: { read: defaultRead } }) {
69815
- const filePath = join85(featureDir, "checkpoint.jsonl");
70029
+ const filePath = join86(featureDir, "checkpoint.jsonl");
69816
70030
  const deps = options._deps ?? { read: defaultRead };
69817
70031
  let content;
69818
70032
  try {
@@ -69882,7 +70096,7 @@ var init_reader = __esm(() => {
69882
70096
  });
69883
70097
 
69884
70098
  // src/execution/checkpoint/resume-cli.ts
69885
- import { join as join86 } from "path";
70099
+ import { join as join87 } from "path";
69886
70100
  function applyResumeModeDeps(featureDir, mode = "auto") {
69887
70101
  if (mode === "fresh" || mode === "no-resume") {
69888
70102
  _storyOrchestratorDeps.loadCheckpoints = async (_fd) => new Map;
@@ -69892,7 +70106,7 @@ function applyResumeModeDeps(featureDir, mode = "auto") {
69892
70106
  _storyOrchestratorDeps.loadCheckpoints = async (_fd) => loadCheckpoints(target);
69893
70107
  }
69894
70108
  function applyRecordGreenDeps(featureDir, runId) {
69895
- const writer = createCheckpointWriter(join86(featureDir, "checkpoint.jsonl"), runId);
70109
+ const writer = createCheckpointWriter(join87(featureDir, "checkpoint.jsonl"), runId);
69896
70110
  _storyOrchestratorDeps.recordGreen = (storyId, phase, tree) => writer.recordGreen(storyId, phase, tree);
69897
70111
  }
69898
70112
  var init_resume_cli = __esm(() => {
@@ -69941,11 +70155,11 @@ var init_types10 = __esm(() => {
69941
70155
  });
69942
70156
 
69943
70157
  // src/hooks/runner.ts
69944
- import { join as join87 } from "path";
70158
+ import { join as join88 } from "path";
69945
70159
  function createDrainDeadline2(deadlineMs) {
69946
70160
  let timeoutId;
69947
- const promise2 = new Promise((resolve20) => {
69948
- timeoutId = setTimeout(() => resolve20(""), deadlineMs);
70161
+ const promise2 = new Promise((resolve21) => {
70162
+ timeoutId = setTimeout(() => resolve21(""), deadlineMs);
69949
70163
  });
69950
70164
  return {
69951
70165
  promise: promise2,
@@ -69960,14 +70174,14 @@ async function loadHooksConfig(projectDir, globalDir) {
69960
70174
  let globalHooks = { hooks: {} };
69961
70175
  let projectHooks = { hooks: {} };
69962
70176
  let skipGlobal = false;
69963
- const projectPath = join87(projectDir, "hooks.json");
70177
+ const projectPath = join88(projectDir, "hooks.json");
69964
70178
  const projectData = await loadJsonFile(projectPath, "hooks");
69965
70179
  if (projectData) {
69966
70180
  projectHooks = projectData;
69967
70181
  skipGlobal = projectData.skipGlobal ?? false;
69968
70182
  }
69969
70183
  if (!skipGlobal && globalDir) {
69970
- const globalPath = join87(globalDir, "hooks.json");
70184
+ const globalPath = join88(globalDir, "hooks.json");
69971
70185
  const globalData = await loadJsonFile(globalPath, "hooks");
69972
70186
  if (globalData) {
69973
70187
  globalHooks = globalData;
@@ -70483,7 +70697,7 @@ var init_crash_recovery = __esm(() => {
70483
70697
  });
70484
70698
 
70485
70699
  // src/acceptance/import-resolution.ts
70486
- import { resolve as resolve20, sep as sep8 } from "path";
70700
+ import { resolve as resolve21, sep as sep9 } from "path";
70487
70701
  function languageFromExtension(testFilePath) {
70488
70702
  if (!testFilePath)
70489
70703
  return;
@@ -70505,9 +70719,9 @@ async function resolveLanguage(opts) {
70505
70719
  return "typescript";
70506
70720
  }
70507
70721
  async function readCapped(relPath, packageDir) {
70508
- const resolvedPackageDir = resolve20(packageDir);
70509
- const fullPath = resolve20(resolvedPackageDir, relPath);
70510
- if (fullPath !== resolvedPackageDir && !fullPath.startsWith(resolvedPackageDir + sep8)) {
70722
+ const resolvedPackageDir = resolve21(packageDir);
70723
+ const fullPath = resolve21(resolvedPackageDir, relPath);
70724
+ if (fullPath !== resolvedPackageDir && !fullPath.startsWith(resolvedPackageDir + sep9)) {
70511
70725
  return null;
70512
70726
  }
70513
70727
  try {
@@ -70754,12 +70968,12 @@ async function regenerateAcceptanceTest(testPath, acceptanceContext) {
70754
70968
  const content = await Bun.file(testPath).text();
70755
70969
  await Bun.write(bakPath, content);
70756
70970
  logger?.info("acceptance", `Backed up acceptance test -> ${bakPath}`);
70757
- const { unlink: unlink3 } = await import("fs/promises");
70758
- await unlink3(testPath);
70971
+ const { unlink: unlink4 } = await import("fs/promises");
70972
+ await unlink4(testPath);
70759
70973
  if (acceptanceContext.featureDir) {
70760
70974
  const metaPath = path25.join(acceptanceContext.featureDir, "acceptance-meta.json");
70761
70975
  try {
70762
- await unlink3(metaPath);
70976
+ await unlink4(metaPath);
70763
70977
  } catch {}
70764
70978
  }
70765
70979
  let implementationContext;
@@ -71226,16 +71440,16 @@ var init_acceptance_loop = __esm(() => {
71226
71440
  });
71227
71441
 
71228
71442
  // src/session/scratch-purge.ts
71229
- import { mkdir as mkdir12, rename, rm } from "fs/promises";
71230
- import { dirname as dirname15, join as join88 } from "path";
71443
+ import { mkdir as mkdir12, rename as rename2, rm } from "fs/promises";
71444
+ import { dirname as dirname15, join as join89 } from "path";
71231
71445
  async function purgeStaleScratch(projectDir, featureName, retentionDays, archiveInsteadOfDelete = false) {
71232
- const sessionsDir = join88(projectDir, ".nax", "features", featureName, "sessions");
71446
+ const sessionsDir = join89(projectDir, ".nax", "features", featureName, "sessions");
71233
71447
  const sessionIds = await _scratchPurgeDeps.listSessionDirs(sessionsDir);
71234
71448
  const cutoffMs = _scratchPurgeDeps.now() - retentionDays * 86400000;
71235
71449
  let purged = 0;
71236
71450
  for (const sessionId of sessionIds) {
71237
- const sessionDir = join88(sessionsDir, sessionId);
71238
- const descriptorPath = join88(sessionDir, "descriptor.json");
71451
+ const sessionDir = join89(sessionsDir, sessionId);
71452
+ const descriptorPath = join89(sessionDir, "descriptor.json");
71239
71453
  if (!await _scratchPurgeDeps.fileExists(descriptorPath))
71240
71454
  continue;
71241
71455
  let lastActivityAt;
@@ -71251,7 +71465,7 @@ async function purgeStaleScratch(projectDir, featureName, retentionDays, archive
71251
71465
  if (new Date(lastActivityAt).getTime() >= cutoffMs)
71252
71466
  continue;
71253
71467
  if (archiveInsteadOfDelete) {
71254
- const archiveDest = join88(projectDir, ".nax", "features", featureName, "_archive", "sessions", sessionId);
71468
+ const archiveDest = join89(projectDir, ".nax", "features", featureName, "_archive", "sessions", sessionId);
71255
71469
  await _scratchPurgeDeps.move(sessionDir, archiveDest);
71256
71470
  } else {
71257
71471
  await _scratchPurgeDeps.remove(sessionDir);
@@ -71279,7 +71493,7 @@ var init_scratch_purge = __esm(() => {
71279
71493
  remove: (path26) => rm(path26, { recursive: true, force: true }),
71280
71494
  move: async (src, dest) => {
71281
71495
  await mkdir12(dirname15(dest), { recursive: true });
71282
- await rename(src, dest);
71496
+ await rename2(src, dest);
71283
71497
  },
71284
71498
  now: () => Date.now()
71285
71499
  };
@@ -71432,18 +71646,6 @@ ${rawOutput.slice(0, SYNTHETIC_FINDING_OUTPUT_LIMIT)}`,
71432
71646
  }
71433
71647
  ];
71434
71648
  }
71435
- async function findResponsibleStory(testFile, workdir, passedStories) {
71436
- const logger = getSafeLogger();
71437
- for (let i = passedStories.length - 1;i >= 0; i--) {
71438
- const story = passedStories[i];
71439
- const hasCommits = await hasCommitsForStory(workdir, story.id, 50);
71440
- if (hasCommits) {
71441
- logger?.info("regression", `Mapped test to story ${story.id}`, { testFile });
71442
- return story;
71443
- }
71444
- }
71445
- return;
71446
- }
71447
71649
  function findResponsibleStoryByTransition(testFile, snapshots) {
71448
71650
  const ordered = [...snapshots].sort((a, b) => a.completedAt.localeCompare(b.completedAt) || a.storyId.localeCompare(b.storyId));
71449
71651
  for (const snap of ordered) {
@@ -71587,32 +71789,25 @@ async function runDeferredRegression(options) {
71587
71789
  const affectedStoriesObjs = new Map;
71588
71790
  if (testFilesInFailures.size === 0) {
71589
71791
  logger?.warn("regression", "No test files found in failures (unmapped)");
71590
- for (const story of passedStories) {
71591
- affectedStories.add(story.id);
71592
- affectedStoriesObjs.set(story.id, story);
71593
- }
71594
71792
  } else {
71595
71793
  const testFilesArray = Array.from(testFilesInFailures);
71596
71794
  const snapshots = options.storyMetrics ?? [];
71597
71795
  const passedById = new Map(passedStories.map((s) => [s.id, s]));
71598
71796
  for (const testFile of testFilesArray) {
71599
- let responsibleStory;
71600
71797
  const transitionId = findResponsibleStoryByTransition(testFile, snapshots);
71601
- if (transitionId && passedById.has(transitionId)) {
71602
- responsibleStory = passedById.get(transitionId);
71798
+ const responsibleStory = transitionId ? passedById.get(transitionId) : undefined;
71799
+ if (responsibleStory) {
71603
71800
  logger?.info("regression", "Mapped test to story via gate transition", {
71604
71801
  storyId: transitionId,
71605
71802
  testFile
71606
71803
  });
71607
- }
71608
- if (!responsibleStory) {
71609
- responsibleStory = await findResponsibleStory(testFile, workdir, passedStories);
71610
- }
71611
- if (responsibleStory) {
71612
71804
  affectedStories.add(responsibleStory.id);
71613
71805
  affectedStoriesObjs.set(responsibleStory.id, responsibleStory);
71614
71806
  } else {
71615
- logger?.warn("regression", "Could not map test file to story", { testFile });
71807
+ logger?.warn("regression", "Could not safely map test file to a passed story", {
71808
+ testFile,
71809
+ ...transitionId ? { transitionStoryId: transitionId } : {}
71810
+ });
71616
71811
  }
71617
71812
  }
71618
71813
  }
@@ -71756,7 +71951,6 @@ var init_run_regression = __esm(() => {
71756
71951
  init_pipeline();
71757
71952
  init_prd();
71758
71953
  init_test_runners();
71759
- init_git();
71760
71954
  init_verification();
71761
71955
  init_run_regression_triage();
71762
71956
  _regressionDeps = {
@@ -72316,7 +72510,8 @@ async function runCompletionPhase(options) {
72316
72510
  deferredReviewStartedAt: options.deferredReviewStartedAt,
72317
72511
  exitReason: options.exitReason,
72318
72512
  runtime: options.runtime,
72319
- abortSignal: options.abortSignal
72513
+ abortSignal: options.abortSignal,
72514
+ isSequential: options.parallel === undefined
72320
72515
  });
72321
72516
  const { durationMs, runCompletedAt, finalCounts, reportedTotal, pluginGateFailed } = completionResult;
72322
72517
  if (options.featureDir) {
@@ -72528,12 +72723,12 @@ var init_ensure_package_dirs = __esm(() => {
72528
72723
 
72529
72724
  // src/pipeline/subscribers/events-writer.ts
72530
72725
  import { appendFile as appendFile5, mkdir as mkdir13 } from "fs/promises";
72531
- import { basename as basename17, join as join89 } from "path";
72726
+ import { basename as basename17, join as join90 } from "path";
72532
72727
  function wireEventsWriter(bus, feature, runId, workdir) {
72533
72728
  const logger = getSafeLogger();
72534
72729
  const project = basename17(workdir);
72535
- const eventsDir = join89(getEventsRootDir(), project);
72536
- const eventsFile = join89(eventsDir, "events.jsonl");
72730
+ const eventsDir = join90(getEventsRootDir(), project);
72731
+ const eventsFile = join90(eventsDir, "events.jsonl");
72537
72732
  let dirReady = false;
72538
72733
  const write = (line) => {
72539
72734
  return (async () => {
@@ -72714,12 +72909,12 @@ var init_interaction2 = __esm(() => {
72714
72909
 
72715
72910
  // src/pipeline/subscribers/registry.ts
72716
72911
  import { mkdir as mkdir14, writeFile as writeFile2 } from "fs/promises";
72717
- import { basename as basename18, join as join90 } from "path";
72912
+ import { basename as basename18, join as join91 } from "path";
72718
72913
  function wireRegistry(bus, feature, runId, workdir, outputDir) {
72719
72914
  const logger = getSafeLogger();
72720
72915
  const project = basename18(workdir);
72721
- const runDir = join90(getRunsDir(), `${project}-${feature}-${runId}`);
72722
- const metaFile = join90(runDir, "meta.json");
72916
+ const runDir = join91(getRunsDir(), `${project}-${feature}-${runId}`);
72917
+ const metaFile = join91(runDir, "meta.json");
72723
72918
  const unsub = bus.on("run:started", (_ev) => {
72724
72919
  return (async () => {
72725
72920
  try {
@@ -72729,8 +72924,8 @@ function wireRegistry(bus, feature, runId, workdir, outputDir) {
72729
72924
  project,
72730
72925
  feature,
72731
72926
  workdir,
72732
- statusPath: join90(outputDir, "features", feature, "status.json"),
72733
- eventsDir: join90(outputDir, "features", feature, "runs"),
72927
+ statusPath: join91(outputDir, "features", feature, "status.json"),
72928
+ eventsDir: join91(outputDir, "features", feature, "runs"),
72734
72929
  registeredAt: new Date().toISOString()
72735
72930
  };
72736
72931
  await writeFile2(metaFile, JSON.stringify(meta3, null, 2));
@@ -72851,7 +73046,7 @@ var init_types11 = __esm(() => {
72851
73046
 
72852
73047
  // src/worktree/dependencies.ts
72853
73048
  import { existsSync as existsSync33 } from "fs";
72854
- import { join as join91 } from "path";
73049
+ import { join as join92 } from "path";
72855
73050
  async function prepareWorktreeDependencies(options) {
72856
73051
  const mode = options.config.execution.worktreeDependencies.mode;
72857
73052
  const resolvedCwd = resolveDependencyCwd(options);
@@ -72865,7 +73060,7 @@ async function prepareWorktreeDependencies(options) {
72865
73060
  }
72866
73061
  }
72867
73062
  function resolveDependencyCwd(options) {
72868
- return options.storyWorkdir ? join91(options.worktreeRoot, options.storyWorkdir) : options.worktreeRoot;
73063
+ return options.storyWorkdir ? join92(options.worktreeRoot, options.storyWorkdir) : options.worktreeRoot;
72869
73064
  }
72870
73065
  function resolveInheritedDependencies(options, resolvedCwd) {
72871
73066
  if (hasDependencyManifests(options.worktreeRoot, resolvedCwd)) {
@@ -72875,7 +73070,7 @@ function resolveInheritedDependencies(options, resolvedCwd) {
72875
73070
  }
72876
73071
  function hasDependencyManifests(worktreeRoot, resolvedCwd) {
72877
73072
  const directories = resolvedCwd === worktreeRoot ? [worktreeRoot] : [worktreeRoot, resolvedCwd];
72878
- return directories.some((directory) => PHASE_ONE_INHERIT_UNSUPPORTED_FILES.some((filename) => _worktreeDependencyDeps.existsSync(join91(directory, filename))));
73073
+ return directories.some((directory) => PHASE_ONE_INHERIT_UNSUPPORTED_FILES.some((filename) => _worktreeDependencyDeps.existsSync(join92(directory, filename))));
72879
73074
  }
72880
73075
  async function provisionDependencies(config2, worktreeRoot, resolvedCwd) {
72881
73076
  const setupCommand2 = config2.execution.worktreeDependencies.setupCommand;
@@ -72932,20 +73127,15 @@ var init_dependencies = __esm(() => {
72932
73127
  });
72933
73128
 
72934
73129
  // src/worktree/manager.ts
72935
- var exports_manager = {};
72936
- __export(exports_manager, {
72937
- _managerDeps: () => _managerDeps,
72938
- WorktreeManager: () => WorktreeManager
72939
- });
72940
73130
  import { existsSync as existsSync34, symlinkSync } from "fs";
72941
73131
  import { mkdir as mkdir15 } from "fs/promises";
72942
- import { join as join92 } from "path";
73132
+ import { join as join93 } from "path";
72943
73133
 
72944
73134
  class WorktreeManager {
72945
73135
  async ensureGitExcludes(projectRoot) {
72946
73136
  const logger = getSafeLogger();
72947
- const infoDir = join92(projectRoot, ".git", "info");
72948
- const excludePath = join92(infoDir, "exclude");
73137
+ const infoDir = join93(projectRoot, ".git", "info");
73138
+ const excludePath = join93(infoDir, "exclude");
72949
73139
  try {
72950
73140
  await mkdir15(infoDir, { recursive: true });
72951
73141
  let existing = "";
@@ -72972,7 +73162,7 @@ ${missing.join(`
72972
73162
  }
72973
73163
  async create(projectRoot, storyId) {
72974
73164
  validateStoryId(storyId);
72975
- const worktreePath = join92(projectRoot, ".nax-wt", storyId);
73165
+ const worktreePath = join93(projectRoot, ".nax-wt", storyId);
72976
73166
  const branchName = `nax/${storyId}`;
72977
73167
  try {
72978
73168
  const pruneProc = _managerDeps.spawn(["git", "worktree", "prune"], {
@@ -73033,9 +73223,9 @@ ${missing.join(`
73033
73223
  projectRoot
73034
73224
  });
73035
73225
  }
73036
- const envSource = join92(projectRoot, ".env");
73226
+ const envSource = join93(projectRoot, ".env");
73037
73227
  if (existsSync34(envSource)) {
73038
- const envTarget = join92(worktreePath, ".env");
73228
+ const envTarget = join93(worktreePath, ".env");
73039
73229
  try {
73040
73230
  symlinkSync(envSource, envTarget, "file");
73041
73231
  } catch (error48) {
@@ -73051,7 +73241,7 @@ ${missing.join(`
73051
73241
  }
73052
73242
  async remove(projectRoot, storyId) {
73053
73243
  validateStoryId(storyId);
73054
- const worktreePath = join92(projectRoot, ".nax-wt", storyId);
73244
+ const worktreePath = join93(projectRoot, ".nax-wt", storyId);
73055
73245
  const branchName = `nax/${storyId}`;
73056
73246
  try {
73057
73247
  const proc = _managerDeps.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
@@ -73222,20 +73412,30 @@ var init_dry_run = __esm(() => {
73222
73412
  });
73223
73413
 
73224
73414
  // src/worktree/merge.ts
73225
- var exports_merge = {};
73226
- __export(exports_merge, {
73227
- _mergeDeps: () => _mergeDeps,
73228
- MergeEngine: () => MergeEngine
73229
- });
73230
-
73231
73415
  class MergeEngine {
73232
73416
  worktreeManager;
73233
73417
  constructor(worktreeManager) {
73234
73418
  this.worktreeManager = worktreeManager;
73235
73419
  }
73420
+ async isMidMerge(projectRoot) {
73421
+ const proc = _mergeDeps.spawn(["git", "rev-parse", "-q", "--verify", "MERGE_HEAD"], {
73422
+ cwd: projectRoot,
73423
+ stdout: "pipe",
73424
+ stderr: "pipe"
73425
+ });
73426
+ return await proc.exited === 0;
73427
+ }
73236
73428
  async merge(projectRoot, storyId) {
73237
73429
  const branchName = `nax/${storyId}`;
73238
73430
  try {
73431
+ if (await this.isMidMerge(projectRoot)) {
73432
+ const error48 = `Repository has an unresolved merge in progress; refusing to merge ${branchName}`;
73433
+ getSafeLogger()?.error("worktree", "Refusing to merge into a mid-merge repository", {
73434
+ storyId,
73435
+ projectRoot
73436
+ });
73437
+ return { success: false, failureKind: "error", error: error48 };
73438
+ }
73239
73439
  const mergeProc = _mergeDeps.spawn(["git", "merge", "--no-ff", branchName, "-m", `Merge branch '${branchName}'`], {
73240
73440
  cwd: projectRoot,
73241
73441
  stdout: "pipe",
@@ -73257,24 +73457,38 @@ class MergeEngine {
73257
73457
  }
73258
73458
  return { success: true };
73259
73459
  }
73260
- const output = `${stdout}
73261
- ${stderr}`;
73262
- if (output.includes("CONFLICT") || output.includes("conflict") || output.includes("Automatic merge failed")) {
73263
- const conflictFiles = await this.getConflictFiles(projectRoot);
73264
- await this.abortMerge(projectRoot);
73265
- return {
73266
- success: false,
73267
- conflictFiles
73268
- };
73269
- }
73270
- throw new Error(`Merge failed: ${stderr || stdout || "unknown error"}`);
73460
+ return await this.classifyMergeFailure(projectRoot, storyId, `${stdout}
73461
+ ${stderr}`);
73271
73462
  } catch (error48) {
73272
- if (error48 instanceof Error) {
73273
- throw error48;
73274
- }
73275
- throw new Error(`Failed to merge branch ${branchName}: ${String(error48)}`);
73463
+ getSafeLogger()?.error("worktree", "Merge failed before git could report", {
73464
+ storyId,
73465
+ error: errorMessage(error48)
73466
+ });
73467
+ return { success: false, failureKind: "error", error: errorMessage(error48) };
73276
73468
  }
73277
73469
  }
73470
+ async classifyMergeFailure(projectRoot, storyId, output) {
73471
+ const logger = getSafeLogger();
73472
+ const conflictFiles = await this.getConflictFiles(projectRoot);
73473
+ const midMerge = await this.isMidMerge(projectRoot);
73474
+ if (!midMerge && conflictFiles.length === 0) {
73475
+ const error48 = output.trim() || "unknown error";
73476
+ logger?.error("worktree", "Merge failed for a non-conflict reason", {
73477
+ storyId,
73478
+ error: error48
73479
+ });
73480
+ return { success: false, failureKind: "error", error: error48 };
73481
+ }
73482
+ if (!await this.abortMerge(projectRoot)) {
73483
+ const error48 = `Merge conflict in ${storyId} could not be aborted; repository left mid-merge`;
73484
+ logger?.error("worktree", "git merge --abort failed \u2014 repository left mid-merge", {
73485
+ storyId,
73486
+ conflictFiles
73487
+ });
73488
+ return { success: false, failureKind: "error", conflictFiles, error: error48 };
73489
+ }
73490
+ return { success: false, failureKind: "conflict", conflictFiles };
73491
+ }
73278
73492
  async mergeAll(projectRoot, storyIds, dependencies) {
73279
73493
  const orderedStories = this.topologicalSort(storyIds, dependencies);
73280
73494
  const results = [];
@@ -73286,13 +73500,15 @@ ${stderr}`;
73286
73500
  results.push({
73287
73501
  success: false,
73288
73502
  storyId,
73289
- conflictFiles: []
73503
+ conflictFiles: [],
73504
+ failureKind: "error",
73505
+ error: `Skipped: depends on a story that failed to merge (${deps.filter((d) => failedStories.has(d)).join(", ")})`
73290
73506
  });
73291
73507
  failedStories.add(storyId);
73292
73508
  continue;
73293
73509
  }
73294
73510
  let result = await this.merge(projectRoot, storyId);
73295
- if (!result.success && result.conflictFiles) {
73511
+ if (result.failureKind === "conflict") {
73296
73512
  try {
73297
73513
  await this.rebaseWorktree(projectRoot, storyId);
73298
73514
  result = await this.merge(projectRoot, storyId);
@@ -73301,7 +73517,9 @@ ${stderr}`;
73301
73517
  success: false,
73302
73518
  storyId,
73303
73519
  conflictFiles: result.conflictFiles,
73304
- retryCount: 1
73520
+ retryCount: 1,
73521
+ failureKind: result.failureKind ?? "error",
73522
+ error: result.error
73305
73523
  });
73306
73524
  failedStories.add(storyId);
73307
73525
  continue;
@@ -73316,7 +73534,9 @@ ${stderr}`;
73316
73534
  success: false,
73317
73535
  storyId,
73318
73536
  conflictFiles: result.conflictFiles,
73319
- retryCount: 1
73537
+ retryCount: 1,
73538
+ failureKind: "error",
73539
+ error: errorMessage(error48)
73320
73540
  });
73321
73541
  failedStories.add(storyId);
73322
73542
  }
@@ -73330,7 +73550,9 @@ ${stderr}`;
73330
73550
  results.push({
73331
73551
  success: false,
73332
73552
  storyId,
73333
- retryCount: 0
73553
+ retryCount: 0,
73554
+ failureKind: result.failureKind ?? "error",
73555
+ error: result.error
73334
73556
  });
73335
73557
  failedStories.add(storyId);
73336
73558
  }
@@ -73430,12 +73652,20 @@ ${stderr}`;
73430
73652
  stdout: "pipe",
73431
73653
  stderr: "pipe"
73432
73654
  });
73433
- await proc.exited;
73655
+ const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
73656
+ if (exitCode !== 0) {
73657
+ getSafeLogger()?.error("worktree", "Failed to abort merge", {
73658
+ exitCode,
73659
+ stderr: stderr.trim()
73660
+ });
73661
+ return false;
73662
+ }
73663
+ return true;
73434
73664
  } catch (error48) {
73435
- const logger = getSafeLogger();
73436
- logger?.warn("worktree", "Failed to abort merge", {
73665
+ getSafeLogger()?.error("worktree", "Failed to abort merge", {
73437
73666
  error: errorMessage(error48)
73438
73667
  });
73668
+ return false;
73439
73669
  }
73440
73670
  }
73441
73671
  }
@@ -73448,6 +73678,21 @@ var init_merge = __esm(() => {
73448
73678
  };
73449
73679
  });
73450
73680
 
73681
+ // src/worktree/index.ts
73682
+ var exports_worktree = {};
73683
+ __export(exports_worktree, {
73684
+ prepareWorktreeDependencies: () => prepareWorktreeDependencies,
73685
+ _mergeDeps: () => _mergeDeps,
73686
+ WorktreeManager: () => WorktreeManager,
73687
+ WorktreeDependencyPreparationError: () => WorktreeDependencyPreparationError,
73688
+ MergeEngine: () => MergeEngine
73689
+ });
73690
+ var init_worktree = __esm(() => {
73691
+ init_manager3();
73692
+ init_merge();
73693
+ init_dependencies();
73694
+ });
73695
+
73451
73696
  // src/execution/escalation/escalation.ts
73452
73697
  function escalateTier(currentRung, tierOrder) {
73453
73698
  const i = currentRung.agent !== undefined ? tierOrder.findIndex((t) => t.tier === currentRung.tier && t.agent === currentRung.agent) : tierOrder.findIndex((t) => t.tier === currentRung.tier);
@@ -73659,6 +73904,7 @@ function resolveMaxAttemptsOutcome(failureCategory) {
73659
73904
  case "verifier-rejected":
73660
73905
  case "greenfield-no-tests":
73661
73906
  case "no-tests-authored":
73907
+ case "test-incorrect":
73662
73908
  return "pause";
73663
73909
  case "runtime-crash":
73664
73910
  return "pause";
@@ -73809,6 +74055,7 @@ var init_escalation = __esm(() => {
73809
74055
  // src/execution/merge-conflict-rectify.ts
73810
74056
  var exports_merge_conflict_rectify = {};
73811
74057
  __export(exports_merge_conflict_rectify, {
74058
+ rectifyMergeFailure: () => rectifyMergeFailure,
73812
74059
  rectifyConflictedStory: () => rectifyConflictedStory
73813
74060
  });
73814
74061
  import path28 from "path";
@@ -73822,13 +74069,22 @@ async function closeStaleAcpSession(worktreePath, sessionName) {
73822
74069
  await proc.exited;
73823
74070
  } catch {}
73824
74071
  }
74072
+ function rectifyMergeFailure(storyId, cost, mergeResult) {
74073
+ return {
74074
+ success: false,
74075
+ storyId,
74076
+ cost,
74077
+ finalConflict: mergeResult?.failureKind !== "error",
74078
+ conflictFiles: mergeResult?.conflictFiles ?? []
74079
+ };
74080
+ }
73825
74081
  async function rectifyConflictedStory(options) {
73826
74082
  const { storyId, workdir, config: config2, hooks, pluginRegistry, prd, eventEmitter, agentGetFn } = options;
73827
74083
  const logger = getSafeLogger();
73828
74084
  logger?.info("parallel", "Rectifying story on updated base", { storyId, attempt: "rectification" });
73829
74085
  try {
73830
- const { WorktreeManager: WorktreeManager2 } = await Promise.resolve().then(() => (init_manager3(), exports_manager));
73831
- const { MergeEngine: MergeEngine2 } = await Promise.resolve().then(() => (init_merge(), exports_merge));
74086
+ const { WorktreeManager: WorktreeManager2 } = await Promise.resolve().then(() => (init_worktree(), exports_worktree));
74087
+ const { MergeEngine: MergeEngine2 } = await Promise.resolve().then(() => (init_worktree(), exports_worktree));
73832
74088
  const { runPipeline: runPipeline2 } = await Promise.resolve().then(() => (init_runner4(), exports_runner));
73833
74089
  const { defaultPipeline: defaultPipeline2 } = await Promise.resolve().then(() => (init_stages(), exports_stages));
73834
74090
  const { routeTask: routeTask2 } = await Promise.resolve().then(() => (init_routing(), exports_routing));
@@ -73880,9 +74136,12 @@ async function rectifyConflictedStory(options) {
73880
74136
  const mergeResults2 = await mergeEngine.mergeAll(workdir, [storyId], { [storyId]: [] });
73881
74137
  const mergeResult = mergeResults2[0];
73882
74138
  if (!mergeResult || !mergeResult.success) {
73883
- const conflictFiles = mergeResult?.conflictFiles ?? [];
73884
- logger?.info("parallel", "Rectification failed - preserving worktree", { storyId });
73885
- return { success: false, storyId, cost, finalConflict: true, conflictFiles };
74139
+ logger?.info("parallel", "Rectification failed - preserving worktree", {
74140
+ storyId,
74141
+ failureKind: mergeResult?.failureKind,
74142
+ error: mergeResult?.error
74143
+ });
74144
+ return rectifyMergeFailure(storyId, cost, mergeResult);
73886
74145
  }
73887
74146
  logger?.info("parallel", "Rectification succeeded - story merged", {
73888
74147
  storyId,
@@ -73903,10 +74162,10 @@ var init_merge_conflict_rectify = __esm(() => {
73903
74162
  });
73904
74163
 
73905
74164
  // src/execution/pipeline-result-handler.ts
73906
- import { join as join93 } from "path";
74165
+ import { join as join94 } from "path";
73907
74166
  async function removeWorktreeDirectory(projectRoot, storyId) {
73908
74167
  const logger = getSafeLogger();
73909
- const worktreePath = join93(projectRoot, ".nax-wt", storyId);
74168
+ const worktreePath = join94(projectRoot, ".nax-wt", storyId);
73910
74169
  try {
73911
74170
  const proc = _resultHandlerDeps.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
73912
74171
  cwd: projectRoot,
@@ -73922,6 +74181,23 @@ async function removeWorktreeDirectory(projectRoot, storyId) {
73922
74181
  });
73923
74182
  }
73924
74183
  }
74184
+ async function failStoryAfterMerge(ctx, prd, reason) {
74185
+ markStoryFailed(prd, ctx.story.id, undefined, undefined, ctx.statusWriter);
74186
+ await savePRD(prd, ctx.prdPath);
74187
+ if (ctx.featureDir) {
74188
+ await appendProgress(ctx.featureDir, ctx.story.id, "failed", `${ctx.story.title} \u2014 ${reason}`);
74189
+ }
74190
+ pipelineEventBus.emit({
74191
+ type: "story:failed",
74192
+ storyId: ctx.story.id,
74193
+ story: { id: ctx.story.id, title: ctx.story.title, status: ctx.story.status, attempts: ctx.story.attempts },
74194
+ reason,
74195
+ countsTowardEscalation: false,
74196
+ feature: ctx.feature,
74197
+ attempts: ctx.story.attempts,
74198
+ cost: ctx.runtime.costAggregator.byStory()[ctx.story.id]?.totalCostUsd ?? ctx.totalCost
74199
+ });
74200
+ }
73925
74201
  function filterOutputFiles(files) {
73926
74202
  const NOISE = [
73927
74203
  /\.test\.(ts|js|tsx|jsx)$/,
@@ -73974,6 +74250,16 @@ async function handlePipelineSuccess(ctx, pipelineResult) {
73974
74250
  if (ctx.config.execution.storyIsolation === "worktree") {
73975
74251
  const story = ctx.story;
73976
74252
  const mergeResult = await _resultHandlerDeps.mergeEngine.merge(ctx.workdir, story.id);
74253
+ if (!mergeResult.success && mergeResult.failureKind === "error") {
74254
+ const reason = `Merge failed for a non-conflict reason: ${mergeResult.error ?? "unknown error"}`;
74255
+ logger?.error("worktree", "Merge failed for a non-conflict reason \u2014 marking story as failed", {
74256
+ storyId: story.id,
74257
+ error: mergeResult.error
74258
+ });
74259
+ await failStoryAfterMerge(ctx, prd, reason);
74260
+ await removeWorktreeDirectory(ctx.workdir, story.id);
74261
+ return { storiesCompletedDelta: 0, costDelta, prd, prdDirty: true };
74262
+ }
73977
74263
  if (!mergeResult.success) {
73978
74264
  const { rectifyConflictedStory: rectifyConflictedStory2 } = await Promise.resolve().then(() => (init_merge_conflict_rectify(), exports_merge_conflict_rectify));
73979
74265
  const rectifyResult = await rectifyConflictedStory2({
@@ -73995,7 +74281,9 @@ async function handlePipelineSuccess(ctx, pipelineResult) {
73995
74281
  storyId: story.id,
73996
74282
  conflictFiles: mergeResult.conflictFiles
73997
74283
  });
73998
- return { storiesCompletedDelta: 0, costDelta, prd, prdDirty: false };
74284
+ const files = (mergeResult.conflictFiles ?? []).join(", ");
74285
+ await failStoryAfterMerge(ctx, prd, `Merge conflict could not be rectified${files ? ` (${files})` : ""} \u2014 the branch did not land`);
74286
+ return { storiesCompletedDelta: 0, costDelta, prd, prdDirty: true };
73999
74287
  }
74000
74288
  }
74001
74289
  logger?.info("worktree", "Merged story to main", { storyId: story.id });
@@ -74110,8 +74398,7 @@ var init_pipeline_result_handler = __esm(() => {
74110
74398
  init_prd();
74111
74399
  init_bun_deps();
74112
74400
  init_git();
74113
- init_manager3();
74114
- init_merge();
74401
+ init_worktree();
74115
74402
  init_escalation();
74116
74403
  init_progress();
74117
74404
  _resultHandlerDeps = {
@@ -74123,7 +74410,7 @@ var init_pipeline_result_handler = __esm(() => {
74123
74410
 
74124
74411
  // src/execution/iteration-runner.ts
74125
74412
  import { existsSync as existsSync35 } from "fs";
74126
- import { join as join94 } from "path";
74413
+ import { join as join95 } from "path";
74127
74414
  function releaseHeavyPipelineContext(ctx) {
74128
74415
  ctx.agentResult = undefined;
74129
74416
  ctx.prompt = undefined;
@@ -74162,7 +74449,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost2, allStor
74162
74449
  const storyStartTime = Date.now();
74163
74450
  let effectiveWorkdir = ctx.workdir;
74164
74451
  if (ctx.config.execution.storyIsolation === "worktree") {
74165
- const worktreePath = join94(ctx.workdir, ".nax-wt", story.id);
74452
+ const worktreePath = join95(ctx.workdir, ".nax-wt", story.id);
74166
74453
  const worktreeExists = _iterationRunnerDeps.existsSync(worktreePath);
74167
74454
  if (!worktreeExists) {
74168
74455
  await _iterationRunnerDeps.worktreeManager.ensureGitExcludes(ctx.workdir);
@@ -74182,7 +74469,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost2, allStor
74182
74469
  }
74183
74470
  const accumulatedAttemptCost = (story.priorFailures || []).reduce((sum, f) => sum + (f.cost || 0), 0);
74184
74471
  const profileOverride = profileOverrideFromConfig(ctx.config);
74185
- const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join94(ctx.workdir, ".nax", "config.json"), story.workdir, profileOverride) : ctx.config;
74472
+ const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join95(ctx.workdir, ".nax", "config.json"), story.workdir, profileOverride) : ctx.config;
74186
74473
  let dependencyContext;
74187
74474
  if (ctx.config.execution.storyIsolation === "worktree") {
74188
74475
  try {
@@ -74209,7 +74496,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost2, allStor
74209
74496
  };
74210
74497
  }
74211
74498
  }
74212
- const resolvedWorkdir = dependencyContext?.cwd ? dependencyContext.cwd : ctx.config.execution.storyIsolation === "worktree" ? story.workdir ? join94(effectiveWorkdir, story.workdir) : effectiveWorkdir : story.workdir ? join94(ctx.workdir, story.workdir) : ctx.workdir;
74499
+ const resolvedWorkdir = dependencyContext?.cwd ? dependencyContext.cwd : ctx.config.execution.storyIsolation === "worktree" ? story.workdir ? join95(effectiveWorkdir, story.workdir) : effectiveWorkdir : story.workdir ? join95(ctx.workdir, story.workdir) : ctx.workdir;
74213
74500
  const pipelineContext = {
74214
74501
  config: effectiveConfig,
74215
74502
  rootConfig: ctx.config,
@@ -74408,7 +74695,7 @@ __export(exports_parallel_worker, {
74408
74695
  buildWorktreePipelineContext: () => buildWorktreePipelineContext,
74409
74696
  _parallelWorkerDeps: () => _parallelWorkerDeps
74410
74697
  });
74411
- import { join as join95 } from "path";
74698
+ import { join as join96 } from "path";
74412
74699
  function buildWorktreePipelineContext(base, _story) {
74413
74700
  return { ...base, prd: structuredClone(base.prd) };
74414
74701
  }
@@ -74431,7 +74718,7 @@ async function executeStoryInWorktree(story, worktreePath, dependencyContext, co
74431
74718
  story,
74432
74719
  stories: [story],
74433
74720
  projectDir: context.projectDir,
74434
- workdir: dependencyContext.cwd ?? (story.workdir ? join95(worktreePath, story.workdir) : worktreePath),
74721
+ workdir: dependencyContext.cwd ?? (story.workdir ? join96(worktreePath, story.workdir) : worktreePath),
74435
74722
  worktreeDependencyContext: dependencyContext,
74436
74723
  routing,
74437
74724
  storyGitRef: storyGitRef ?? undefined
@@ -74644,6 +74931,12 @@ async function runParallelBatch(options) {
74644
74931
  logger?.info("parallel-batch", "Story merged successfully", {
74645
74932
  storyId: mergeResult.storyId
74646
74933
  });
74934
+ } else if (mergeResult.failureKind === "error") {
74935
+ workerResult.failed.push({ story, error: mergeResult.error ?? "merge failed" });
74936
+ logger?.error("parallel-batch", "Merge failed for a non-conflict reason", {
74937
+ storyId: mergeResult.storyId,
74938
+ error: mergeResult.error
74939
+ });
74647
74940
  } else {
74648
74941
  workerResult.mergeConflicts.push({
74649
74942
  storyId: mergeResult.storyId,
@@ -74731,11 +75024,11 @@ var init_parallel_batch = __esm(() => {
74731
75024
  return executeParallelBatch2(_stories, _projectRoot, _config, _context, _worktreePaths, _dependencyContexts, _maxConcurrency, _eventEmitter, _storyEffectiveConfigs);
74732
75025
  },
74733
75026
  createWorktreeManager: async () => {
74734
- const { WorktreeManager: WorktreeManager2 } = await Promise.resolve().then(() => (init_manager3(), exports_manager));
75027
+ const { WorktreeManager: WorktreeManager2 } = await Promise.resolve().then(() => (init_worktree(), exports_worktree));
74735
75028
  return new WorktreeManager2;
74736
75029
  },
74737
75030
  createMergeEngine: async (worktreeManager) => {
74738
- const { MergeEngine: MergeEngine2 } = await Promise.resolve().then(() => (init_merge(), exports_merge));
75031
+ const { MergeEngine: MergeEngine2 } = await Promise.resolve().then(() => (init_worktree(), exports_worktree));
74739
75032
  return new MergeEngine2(worktreeManager);
74740
75033
  },
74741
75034
  rectifyConflictedStory: async (opts) => {
@@ -75401,8 +75694,8 @@ var init_runner_execution = __esm(() => {
75401
75694
  });
75402
75695
 
75403
75696
  // src/execution/status-file.ts
75404
- import { rename as rename2, unlink as unlink3 } from "fs/promises";
75405
- import { resolve as resolve21 } from "path";
75697
+ import { rename as rename3, unlink as unlink4 } from "fs/promises";
75698
+ import { resolve as resolve22 } from "path";
75406
75699
  function countProgress(prd) {
75407
75700
  const stories = prd.userStories;
75408
75701
  const passed = stories.filter((s) => s.status === "passed").length;
@@ -75447,21 +75740,21 @@ function buildStatusSnapshot(state) {
75447
75740
  return snapshot;
75448
75741
  }
75449
75742
  async function writeStatusFile(filePath, status) {
75450
- const resolvedPath = resolve21(filePath);
75743
+ const resolvedPath = resolve22(filePath);
75451
75744
  if (filePath.includes("../") || filePath.includes("..\\")) {
75452
75745
  throw new Error("Invalid status file path: path traversal detected");
75453
75746
  }
75454
75747
  const tmpPath = `${resolvedPath}.tmp`;
75455
75748
  try {
75456
- await unlink3(tmpPath);
75749
+ await unlink4(tmpPath);
75457
75750
  } catch {}
75458
75751
  await Bun.write(tmpPath, JSON.stringify(status, null, 2));
75459
- await rename2(tmpPath, resolvedPath);
75752
+ await rename3(tmpPath, resolvedPath);
75460
75753
  }
75461
75754
  var init_status_file = () => {};
75462
75755
 
75463
75756
  // src/execution/status-writer.ts
75464
- import { join as join96 } from "path";
75757
+ import { join as join97 } from "path";
75465
75758
 
75466
75759
  class StatusWriter {
75467
75760
  statusFile;
@@ -75580,7 +75873,7 @@ class StatusWriter {
75580
75873
  if (!this._prd)
75581
75874
  return;
75582
75875
  const safeLogger = getSafeLogger();
75583
- const featureStatusPath = join96(featureDir, "status.json");
75876
+ const featureStatusPath = join97(featureDir, "status.json");
75584
75877
  const write = async () => {
75585
75878
  try {
75586
75879
  const base = this.getSnapshot(totalCost2, iterations);
@@ -75612,7 +75905,7 @@ __export(exports_migrate, {
75612
75905
  detectGeneratedContent: () => detectGeneratedContent
75613
75906
  });
75614
75907
  import { existsSync as existsSync36 } from "fs";
75615
- import { mkdir as mkdir16, readdir as readdir5, rename as rename3 } from "fs/promises";
75908
+ import { mkdir as mkdir16, readdir as readdir5, rename as rename4 } from "fs/promises";
75616
75909
  import path30 from "path";
75617
75910
  async function detectGeneratedContent(naxDir) {
75618
75911
  if (!existsSync36(naxDir))
@@ -75701,7 +75994,7 @@ async function migrateCommand(options) {
75701
75994
  const archiveBase = path30.join(globalConfigDir(), "_archive");
75702
75995
  const archiveDest = path30.join(archiveBase, `${options.reclaim}-${Date.now()}`);
75703
75996
  await mkdir16(archiveBase, { recursive: true });
75704
- await rename3(src, archiveDest);
75997
+ await rename4(src, archiveDest);
75705
75998
  logger.info("migrate", `Reclaimed: archived to ${archiveDest}`, { storyId: "_migrate" });
75706
75999
  return;
75707
76000
  }
@@ -75780,7 +76073,7 @@ async function migrateCommand(options) {
75780
76073
  Remove the destination or run nax migrate --dry-run to inspect.`, "MIGRATE_CONFLICT", { stage: "migrate", src: candidate.srcPath, dest });
75781
76074
  }
75782
76075
  try {
75783
- await rename3(candidate.srcPath, dest);
76076
+ await rename4(candidate.srcPath, dest);
75784
76077
  } catch (err) {
75785
76078
  const isXdev = err instanceof Error && "code" in err && err.code === "EXDEV";
75786
76079
  if (isXdev) {
@@ -76024,7 +76317,7 @@ __export(exports_run_initialization, {
76024
76317
  initializeRun: () => initializeRun,
76025
76318
  _reconcileDeps: () => _reconcileDeps
76026
76319
  });
76027
- import { join as join97 } from "path";
76320
+ import { join as join98 } from "path";
76028
76321
  async function reconcileState(prd, prdPath, workdir, config2) {
76029
76322
  const logger = getSafeLogger();
76030
76323
  let reconciledCount = 0;
@@ -76041,7 +76334,7 @@ async function reconcileState(prd, prdPath, workdir, config2) {
76041
76334
  });
76042
76335
  continue;
76043
76336
  }
76044
- const effectiveWorkdir = story.workdir ? join97(workdir, story.workdir) : workdir;
76337
+ const effectiveWorkdir = story.workdir ? join98(workdir, story.workdir) : workdir;
76045
76338
  try {
76046
76339
  const reviewResult = await _reconcileDeps.runReview(config2.review, effectiveWorkdir, config2.execution);
76047
76340
  if (!reviewResult.success) {
@@ -76857,6 +77150,7 @@ async function run(options) {
76857
77150
  hooks,
76858
77151
  feature,
76859
77152
  workdir,
77153
+ parallel,
76860
77154
  prdPath,
76861
77155
  statusFile,
76862
77156
  logFilePath,
@@ -77446,14 +77740,14 @@ See https://react.dev/link/invalid-hook-call for tips about how to debug and fix
77446
77740
  prevActScopeDepth !== actScopeDepth - 1 && console.error("You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one. ");
77447
77741
  actScopeDepth = prevActScopeDepth;
77448
77742
  }
77449
- function recursivelyFlushAsyncActWork(returnValue, resolve22, reject) {
77743
+ function recursivelyFlushAsyncActWork(returnValue, resolve23, reject) {
77450
77744
  var queue = ReactSharedInternals.actQueue;
77451
77745
  if (queue !== null)
77452
77746
  if (queue.length !== 0)
77453
77747
  try {
77454
77748
  flushActQueue(queue);
77455
77749
  enqueueTask(function() {
77456
- return recursivelyFlushAsyncActWork(returnValue, resolve22, reject);
77750
+ return recursivelyFlushAsyncActWork(returnValue, resolve23, reject);
77457
77751
  });
77458
77752
  return;
77459
77753
  } catch (error48) {
@@ -77461,7 +77755,7 @@ See https://react.dev/link/invalid-hook-call for tips about how to debug and fix
77461
77755
  }
77462
77756
  else
77463
77757
  ReactSharedInternals.actQueue = null;
77464
- 0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) : resolve22(returnValue);
77758
+ 0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) : resolve23(returnValue);
77465
77759
  }
77466
77760
  function flushActQueue(queue) {
77467
77761
  if (!isFlushing) {
@@ -77637,14 +77931,14 @@ See https://react.dev/link/invalid-hook-call for tips about how to debug and fix
77637
77931
  didAwaitActCall || didWarnNoAwaitAct || (didWarnNoAwaitAct = true, console.error("You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"));
77638
77932
  });
77639
77933
  return {
77640
- then: function(resolve22, reject) {
77934
+ then: function(resolve23, reject) {
77641
77935
  didAwaitActCall = true;
77642
77936
  thenable.then(function(returnValue) {
77643
77937
  popActScope(prevActQueue, prevActScopeDepth);
77644
77938
  if (prevActScopeDepth === 0) {
77645
77939
  try {
77646
77940
  flushActQueue(queue), enqueueTask(function() {
77647
- return recursivelyFlushAsyncActWork(returnValue, resolve22, reject);
77941
+ return recursivelyFlushAsyncActWork(returnValue, resolve23, reject);
77648
77942
  });
77649
77943
  } catch (error$0) {
77650
77944
  ReactSharedInternals.thrownErrors.push(error$0);
@@ -77655,7 +77949,7 @@ See https://react.dev/link/invalid-hook-call for tips about how to debug and fix
77655
77949
  reject(_thrownError);
77656
77950
  }
77657
77951
  } else
77658
- resolve22(returnValue);
77952
+ resolve23(returnValue);
77659
77953
  }, function(error48) {
77660
77954
  popActScope(prevActQueue, prevActScopeDepth);
77661
77955
  0 < ReactSharedInternals.thrownErrors.length ? (error48 = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(error48)) : reject(error48);
@@ -77671,11 +77965,11 @@ See https://react.dev/link/invalid-hook-call for tips about how to debug and fix
77671
77965
  if (0 < ReactSharedInternals.thrownErrors.length)
77672
77966
  throw callback = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, callback;
77673
77967
  return {
77674
- then: function(resolve22, reject) {
77968
+ then: function(resolve23, reject) {
77675
77969
  didAwaitActCall = true;
77676
77970
  prevActScopeDepth === 0 ? (ReactSharedInternals.actQueue = queue, enqueueTask(function() {
77677
- return recursivelyFlushAsyncActWork(returnValue$jscomp$0, resolve22, reject);
77678
- })) : resolve22(returnValue$jscomp$0);
77971
+ return recursivelyFlushAsyncActWork(returnValue$jscomp$0, resolve23, reject);
77972
+ })) : resolve23(returnValue$jscomp$0);
77679
77973
  }
77680
77974
  };
77681
77975
  };
@@ -80517,8 +80811,8 @@ It can also happen if the client has a browser extension installed which messes
80517
80811
  currentEntangledActionThenable = {
80518
80812
  status: "pending",
80519
80813
  value: undefined,
80520
- then: function(resolve22) {
80521
- entangledListeners.push(resolve22);
80814
+ then: function(resolve23) {
80815
+ entangledListeners.push(resolve23);
80522
80816
  }
80523
80817
  };
80524
80818
  }
@@ -80542,8 +80836,8 @@ It can also happen if the client has a browser extension installed which messes
80542
80836
  status: "pending",
80543
80837
  value: null,
80544
80838
  reason: null,
80545
- then: function(resolve22) {
80546
- listeners.push(resolve22);
80839
+ then: function(resolve23) {
80840
+ listeners.push(resolve23);
80547
80841
  }
80548
80842
  };
80549
80843
  thenable.then(function() {
@@ -106442,9 +106736,9 @@ var init_ranking = __esm(() => {
106442
106736
  });
106443
106737
 
106444
106738
  // src/bakeoff/coordinator.ts
106445
- import { join as join102 } from "path";
106739
+ import { join as join103 } from "path";
106446
106740
  async function persistBakeoffResult(result2, outputDir) {
106447
- const filePath = join102(outputDir, "bakeoff.json");
106741
+ const filePath = join103(outputDir, "bakeoff.json");
106448
106742
  await Bun.write(filePath, JSON.stringify(result2, null, 2));
106449
106743
  }
106450
106744
  async function runBakeoff(options, deps = {}) {
@@ -106622,7 +106916,7 @@ var init_bakeoff = __esm(() => {
106622
106916
  });
106623
106917
 
106624
106918
  // src/plugins/builtin/curator/rollup-prune.ts
106625
- import { rename as rename4, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
106919
+ import { rename as rename5, unlink as unlink5, writeFile as writeFile3 } from "fs/promises";
106626
106920
  import { appendFile as appendFile6 } from "fs/promises";
106627
106921
  async function scanProjectRunIds(rollupPath, projectKey) {
106628
106922
  const maxTsByRunId = new Map;
@@ -106691,9 +106985,9 @@ async function pruneRollup(input) {
106691
106985
  await flush();
106692
106986
  }
106693
106987
  await flush();
106694
- await rename4(tmpPath, rollupPath);
106988
+ await rename5(tmpPath, rollupPath);
106695
106989
  } catch (err) {
106696
- await unlink4(tmpPath).catch(() => {});
106990
+ await unlink5(tmpPath).catch(() => {});
106697
106991
  throw err;
106698
106992
  }
106699
106993
  return result2;
@@ -106714,8 +107008,8 @@ __export(exports_curator, {
106714
107008
  _curatorCmdDeps: () => _curatorCmdDeps
106715
107009
  });
106716
107010
  import { readdirSync as readdirSync9 } from "fs";
106717
- import { unlink as unlink5 } from "fs/promises";
106718
- import { join as join103, resolve as resolve22, sep as sep9 } from "path";
107011
+ import { unlink as unlink6 } from "fs/promises";
107012
+ import { join as join104, resolve as resolve23, sep as sep10 } from "path";
106719
107013
  function listRunIds(runsDir) {
106720
107014
  try {
106721
107015
  return readdirSync9(runsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
@@ -106795,7 +107089,7 @@ async function curatorStatus(options) {
106795
107089
  const config2 = await _curatorCmdDeps.loadConfig(resolved.projectDir);
106796
107090
  const projectKey = getProjectKey(config2, resolved.projectDir);
106797
107091
  const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
106798
- const runsDir = join103(outputDir, "runs");
107092
+ const runsDir = join104(outputDir, "runs");
106799
107093
  const runIds = listRunIds(runsDir);
106800
107094
  let runId;
106801
107095
  if (options.run) {
@@ -106812,8 +107106,8 @@ async function curatorStatus(options) {
106812
107106
  runId = runIds[runIds.length - 1];
106813
107107
  }
106814
107108
  console.log(`Run: ${runId}`);
106815
- const runDir = join103(runsDir, runId);
106816
- const observationsPath = join103(runDir, "observations.jsonl");
107109
+ const runDir = join104(runsDir, runId);
107110
+ const observationsPath = join104(runDir, "observations.jsonl");
106817
107111
  const observations = await parseObservations(observationsPath);
106818
107112
  const counts = new Map;
106819
107113
  for (const obs of observations) {
@@ -106823,7 +107117,7 @@ async function curatorStatus(options) {
106823
107117
  for (const [kind, count] of counts.entries()) {
106824
107118
  console.log(` ${kind}: ${count}`);
106825
107119
  }
106826
- const proposalsPath = join103(runDir, "curator-proposals.md");
107120
+ const proposalsPath = join104(runDir, "curator-proposals.md");
106827
107121
  const proposalText = await _curatorCmdDeps.readFile(proposalsPath).catch(() => null);
106828
107122
  if (proposalText !== null) {
106829
107123
  console.log("");
@@ -106833,24 +107127,24 @@ async function curatorStatus(options) {
106833
107127
  }
106834
107128
  }
106835
107129
  function resolveCanonicalTargetPath(projectDir, canonicalFile) {
106836
- const target = resolve22(projectDir, canonicalFile);
106837
- const root = resolve22(projectDir);
106838
- if (target !== root && !target.startsWith(root + sep9))
107130
+ const target = resolve23(projectDir, canonicalFile);
107131
+ const root = resolve23(projectDir);
107132
+ if (target !== root && !target.startsWith(root + sep10))
106839
107133
  return null;
106840
- const relative18 = target.slice(root.length + 1).replaceAll(sep9, "/");
107134
+ const relative18 = target.slice(root.length + 1).replaceAll(sep10, "/");
106841
107135
  return CURATOR_TARGET_SHAPES.some((shape) => shape.test(relative18)) ? target : null;
106842
107136
  }
106843
107137
  function isWithinCanonicalRulesDir(projectDir, targetPath) {
106844
- const rulesRoot = resolve22(projectDir, CANONICAL_RULES_DIR);
106845
- return targetPath === rulesRoot || targetPath.startsWith(rulesRoot + sep9);
107138
+ const rulesRoot = resolve23(projectDir, CANONICAL_RULES_DIR);
107139
+ return targetPath === rulesRoot || targetPath.startsWith(rulesRoot + sep10);
106846
107140
  }
106847
107141
  async function curatorCommit(options) {
106848
107142
  const resolved = await _curatorCmdDeps.resolveProject({ dir: options.project });
106849
107143
  const config2 = await _curatorCmdDeps.loadConfig(resolved.projectDir);
106850
107144
  const projectKey = getProjectKey(config2, resolved.projectDir);
106851
107145
  const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
106852
- const runDir = join103(outputDir, "runs", options.runId);
106853
- const proposalsPath = join103(runDir, "curator-proposals.md");
107146
+ const runDir = join104(outputDir, "runs", options.runId);
107147
+ const proposalsPath = join104(runDir, "curator-proposals.md");
106854
107148
  const proposalText = await _curatorCmdDeps.readFile(proposalsPath).catch(() => null);
106855
107149
  if (proposalText === null) {
106856
107150
  console.log(`curator-proposals.md not found for run ${options.runId}.`);
@@ -106977,7 +107271,7 @@ async function curatorDryrun(options) {
106977
107271
  const config2 = await _curatorCmdDeps.loadConfig(resolved.projectDir);
106978
107272
  const projectKey = getProjectKey(config2, resolved.projectDir);
106979
107273
  const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
106980
- const runsDir = join103(outputDir, "runs");
107274
+ const runsDir = join104(outputDir, "runs");
106981
107275
  const runIds = listRunIds(runsDir);
106982
107276
  if (runIds.length === 0) {
106983
107277
  console.log("No runs found.");
@@ -106988,7 +107282,7 @@ async function curatorDryrun(options) {
106988
107282
  console.log(`Run ${options.run} not found in ${runsDir}.`);
106989
107283
  return;
106990
107284
  }
106991
- const observationsPath = join103(runsDir, runId, "observations.jsonl");
107285
+ const observationsPath = join104(runsDir, runId, "observations.jsonl");
106992
107286
  const observations = await parseObservations(observationsPath);
106993
107287
  const thresholds = getThresholds(config2);
106994
107288
  const proposals = runHeuristics(observations, thresholds);
@@ -107020,12 +107314,12 @@ async function curatorGc(options) {
107020
107314
  dropUnattributed: sweep
107021
107315
  });
107022
107316
  const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
107023
- const perRunsDir = join103(outputDir, "runs");
107317
+ const perRunsDir = join104(outputDir, "runs");
107024
107318
  for (const runId of uniqueRunIds) {
107025
107319
  if (!keepSet.has(runId)) {
107026
- const runDir = join103(perRunsDir, runId);
107027
- await _curatorCmdDeps.removeFile(join103(runDir, "observations.jsonl"));
107028
- await _curatorCmdDeps.removeFile(join103(runDir, "curator-proposals.md"));
107320
+ const runDir = join104(perRunsDir, runId);
107321
+ await _curatorCmdDeps.removeFile(join104(runDir, "observations.jsonl"));
107322
+ await _curatorCmdDeps.removeFile(join104(runDir, "curator-proposals.md"));
107029
107323
  }
107030
107324
  }
107031
107325
  const droppedRuns = Math.max(0, uniqueRunIds.length - keepSet.size);
@@ -107065,7 +107359,7 @@ var init_curator2 = __esm(() => {
107065
107359
  },
107066
107360
  removeFile: async (p) => {
107067
107361
  try {
107068
- await unlink5(p);
107362
+ await unlink6(p);
107069
107363
  } catch {}
107070
107364
  },
107071
107365
  openInEditor: async (filePath) => {
@@ -107084,7 +107378,7 @@ var init_curator2 = __esm(() => {
107084
107378
  init_source();
107085
107379
  import { existsSync as existsSync39, mkdirSync as mkdirSync8 } from "fs";
107086
107380
  import { homedir as homedir3 } from "os";
107087
- import { basename as basename21, join as join104 } from "path";
107381
+ import { basename as basename21, join as join105 } from "path";
107088
107382
 
107089
107383
  // node_modules/commander/esm.mjs
107090
107384
  var import__ = __toESM(require_commander(), 1);
@@ -108139,12 +108433,7 @@ var FIELD_DESCRIPTIONS = {
108139
108433
  "autoMode.escalation.tierOrder": 'Ordered tier escalation chain with per-tier attempt budgets. Format: [{"tier": "fast", "attempts": 2}, {"tier": "balanced", "attempts": 2}, {"tier": "powerful", "attempts": 1}]. Allows each tier to attempt fixes before escalating to the next.',
108140
108434
  "autoMode.escalation.escalateEntireBatch": "When enabled, escalate all stories in a batch if one fails. When disabled, only the failing story escalates (allows parallel attempts at different tiers).",
108141
108435
  routing: "Model routing strategy configuration",
108142
- "routing.strategy": "Routing strategy: keyword | llm | manual | adaptive | custom",
108143
- "routing.customStrategyPath": "Path to custom routing strategy (if strategy=custom)",
108144
- "routing.adaptive": "Adaptive routing settings",
108145
- "routing.adaptive.minSamples": "Minimum samples before adaptive routing activates",
108146
- "routing.adaptive.costThreshold": "Cost threshold for strategy switching (0-1)",
108147
- "routing.adaptive.fallbackStrategy": "Fallback strategy if adaptive fails",
108436
+ "routing.strategy": "Routing strategy: keyword | llm",
108148
108437
  "routing.llm": "LLM-based routing settings",
108149
108438
  "routing.llm.model": 'Model selector for routing decisions. Accepts a tier string (for example "fast") or an explicit object like { agent: "codex", model: "gpt-5.4" }.',
108150
108439
  "routing.llm.fallbackToKeywords": "Fall back to keyword routing on LLM failure",
@@ -108809,11 +109098,12 @@ init_canonical_loader();
108809
109098
  init_errors();
108810
109099
  init_logger2();
108811
109100
  import { mkdir as mkdir11 } from "fs/promises";
108812
- import { basename as basename15, join as join75, resolve as resolve19, sep as sep7 } from "path";
109101
+ import { join as join76, resolve as resolve20, sep as sep8 } from "path";
108813
109102
 
108814
109103
  // src/cli/rules-lint.ts
108815
109104
  init_engine();
108816
109105
  init_canonical_loader();
109106
+ init_errors();
108817
109107
  init_logger2();
108818
109108
  init_test_runners();
108819
109109
  import { join as join74 } from "path";
@@ -108894,8 +109184,15 @@ async function rulesLintCommand(options, deps = _rulesLintDeps) {
108894
109184
  }
108895
109185
  let totalRuleFiles = 0;
108896
109186
  let warningCount = 0;
109187
+ const failedRoots = [];
108897
109188
  for (const root of roots) {
108898
- const rules = await deps.loadCanonicalRules(root);
109189
+ let rules;
109190
+ try {
109191
+ rules = await deps.loadCanonicalRules(root);
109192
+ } catch (err) {
109193
+ failedRoots.push({ root, cause: err });
109194
+ continue;
109195
+ }
108899
109196
  totalRuleFiles += rules.length;
108900
109197
  for (const rule of rules) {
108901
109198
  for (const warning of rule.warnings ?? []) {
@@ -108927,6 +109224,18 @@ async function rulesLintCommand(options, deps = _rulesLintDeps) {
108927
109224
  }
108928
109225
  }
108929
109226
  }
109227
+ if (totalRuleFiles === 0 && failedRoots.length === 0) {
109228
+ warningCount++;
109229
+ logger.warn("rules-lint", "Canonical rules store is empty \u2014 no rule files found across any rule root. Run `nax rules migrate` to seed the store.", { code: "EMPTY_STORE", roots: roots.length });
109230
+ }
109231
+ if (failedRoots.length > 0) {
109232
+ const failedRootPaths = failedRoots.map((f) => f.root);
109233
+ throw new NaxError(`Failed to load canonical rules from ${failedRootPaths.length} rule root(s): ${failedRootPaths.join(", ")}`, "RULES_LINT_ROOT_FAILED", {
109234
+ stage: "rules-lint",
109235
+ failedRoots: failedRootPaths,
109236
+ causes: failedRoots.map((f) => f.cause)
109237
+ });
109238
+ }
108930
109239
  const scopeLabel = roots.length === 1 ? "repo root" : `${roots.length} rule roots`;
108931
109240
  if (warningCount > 0) {
108932
109241
  console.log(`[WARN] Canonical rules lint completed with ${warningCount} warning(s) (${totalRuleFiles} file(s) across ${scopeLabel}).`);
@@ -108934,6 +109243,150 @@ async function rulesLintCommand(options, deps = _rulesLintDeps) {
108934
109243
  console.log(`[OK] Canonical rules lint passed (${totalRuleFiles} file(s) across ${scopeLabel}).`);
108935
109244
  }
108936
109245
  }
109246
+ // src/cli/rules-migrate.ts
109247
+ init_canonical_loader();
109248
+ init_errors();
109249
+ import { basename as basename15, join as join75 } from "path";
109250
+
109251
+ // src/cli/rules-migrate-plan.ts
109252
+ init_errors();
109253
+ import { resolve as resolve19, sep as sep7 } from "path";
109254
+ async function planMigration(sources, options) {
109255
+ const writes = [];
109256
+ const skips = [];
109257
+ const resolvedTargetDir = resolve19(options.targetDir);
109258
+ for (const source of sources) {
109259
+ const resolvedTargetPath = resolve19(source.targetPath);
109260
+ if (!resolvedTargetPath.startsWith(`${resolvedTargetDir}${sep7}`) && resolvedTargetPath !== resolvedTargetDir) {
109261
+ throw new NaxError(`Migration target escapes ${options.targetDir}: ${source.targetFileName} -> ${source.targetPath}`, "RULES_MIGRATE_TARGET_ESCAPE", { stage: "rules-migrate-plan", targetDir: options.targetDir, entry: source.targetPath });
109262
+ }
109263
+ const exists = await options.fileExists(source.targetPath);
109264
+ if (exists && !options.force) {
109265
+ skips.push(source);
109266
+ } else {
109267
+ writes.push(source);
109268
+ }
109269
+ }
109270
+ return { writes, skips };
109271
+ }
109272
+
109273
+ // src/cli/rules-migrate.ts
109274
+ function neutralizeContent(content) {
109275
+ let result = content;
109276
+ let replacements = 0;
109277
+ for (const rule of NEUTRALITY_RULES) {
109278
+ for (const { pattern, replacement } of rule.neutralizeSteps ?? []) {
109279
+ const matches = [...result.matchAll(pattern)].length;
109280
+ if (matches > 0) {
109281
+ result = result.replace(pattern, replacement);
109282
+ replacements += matches;
109283
+ }
109284
+ }
109285
+ }
109286
+ return { content: result.trim(), replacements };
109287
+ }
109288
+ function toYamlListLiteral(scalar) {
109289
+ const trimmed = scalar.trim();
109290
+ const unquoted = trimmed.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
109291
+ return `[${JSON.stringify(unquoted)}]`;
109292
+ }
109293
+ function translateLegacyFrontmatter(content) {
109294
+ const fm = /^---(\r?\n)([\s\S]*?)\r?\n---\r?\n/.exec(content);
109295
+ if (!fm?.[2])
109296
+ return { content, translated: false };
109297
+ const eol = fm[1] ?? `
109298
+ `;
109299
+ const block = fm[2];
109300
+ if (!/^paths:/m.test(block) || /^appliesTo:/m.test(block))
109301
+ return { content, translated: false };
109302
+ const scalarMatch = /^paths:[ \t]*(\S.*)$/m.exec(block);
109303
+ const isInlineList = scalarMatch?.[1]?.trim().startsWith("[") ?? false;
109304
+ const rewritten = scalarMatch && !isInlineList ? block.replace(/^paths:[ \t]*(\S.*)$/m, `appliesTo: ${toYamlListLiteral(scalarMatch[1])}`) : block.replace(/^paths:/m, "appliesTo:");
109305
+ const head = content.slice(0, fm.index);
109306
+ const tail = content.slice(fm.index + fm[0].length);
109307
+ return { content: `${head}---${eol}${rewritten}${eol}---${eol}${tail}`, translated: true };
109308
+ }
109309
+ function withReviewNotice(content, replacements) {
109310
+ if (replacements <= 0)
109311
+ return content;
109312
+ const notice = `<!-- NOTE: ${replacements} neutralization(s) applied \u2014 review before committing -->
109313
+
109314
+ `;
109315
+ const fm = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(content);
109316
+ if (!fm)
109317
+ return notice + content;
109318
+ return content.slice(0, fm[0].length) + notice + content.slice(fm[0].length).replace(/^(?:\r?\n)+/, "");
109319
+ }
109320
+ async function collectMigrationSources(workdir) {
109321
+ const sources = [];
109322
+ const rulesDir = join75(workdir, ".claude", "rules");
109323
+ const ruleFiles = _rulesCLIDeps.globInDir(rulesDir);
109324
+ for (const filePath of ruleFiles) {
109325
+ try {
109326
+ const content = await _rulesCLIDeps.readFile(filePath);
109327
+ if (content.trim()) {
109328
+ sources.push({ sourcePath: filePath, targetFileName: basename15(filePath), content });
109329
+ }
109330
+ } catch {}
109331
+ }
109332
+ return sources;
109333
+ }
109334
+ async function rulesMigrateCommand(options) {
109335
+ const workdir = options.dir ?? process.cwd();
109336
+ const force = options.force ?? false;
109337
+ const dryRun = options.dryRun === true;
109338
+ const sources = await collectMigrationSources(workdir);
109339
+ if (sources.length === 0) {
109340
+ console.log("[WARN] No source files found (checked .claude/rules/*.md). Nothing to migrate.");
109341
+ console.log("[WARN] Root CLAUDE.md is not a migration source. Move the rules you want migrated into .claude/rules/, or author .nax/rules/ directly.");
109342
+ return { written: [], skipped: [] };
109343
+ }
109344
+ const targetDir = join75(workdir, CANONICAL_RULES_DIR);
109345
+ const planEntries = sources.map((source) => ({
109346
+ sourcePath: source.sourcePath,
109347
+ targetFileName: source.targetFileName,
109348
+ targetPath: join75(targetDir, source.targetFileName),
109349
+ content: source.content
109350
+ }));
109351
+ const plan = await planMigration(planEntries, {
109352
+ targetDir,
109353
+ force,
109354
+ fileExists: _rulesCLIDeps.fileExists
109355
+ });
109356
+ if (!dryRun) {
109357
+ try {
109358
+ await _rulesCLIDeps.mkdir(targetDir);
109359
+ } catch (err) {
109360
+ throw new NaxError(`Failed to create ${CANONICAL_RULES_DIR}: ${errorMessage(err)}`, "RULES_MIGRATE_MKDIR_FAILED", { stage: "rules-migrate", targetDir });
109361
+ }
109362
+ }
109363
+ const written = [];
109364
+ const skipped = [];
109365
+ for (const entry of plan.writes) {
109366
+ const { content: scoped2 } = translateLegacyFrontmatter(entry.content);
109367
+ const { content: neutralized, replacements } = neutralizeContent(scoped2);
109368
+ const output = withReviewNotice(neutralized, replacements);
109369
+ if (dryRun) {
109370
+ console.log(`[dry-run] Would write ${entry.targetFileName} from ${entry.sourcePath} (${replacements} replacements)`);
109371
+ } else {
109372
+ await _rulesCLIDeps.writeFile(entry.targetPath, output);
109373
+ console.log(`[OK] ${entry.targetFileName} <- ${entry.sourcePath}${replacements > 0 ? ` (${replacements} replacements)` : ""}`);
109374
+ }
109375
+ written.push(entry.targetFileName);
109376
+ }
109377
+ for (const entry of plan.skips) {
109378
+ console.log(`[skip] ${entry.targetFileName} already exists (use --force to overwrite)`);
109379
+ skipped.push(entry.targetFileName);
109380
+ }
109381
+ console.log(dryRun ? `
109382
+ Dry run: ${written.length} file(s) would be written, ${skipped.length} skipped.` : `
109383
+ Migration complete: ${written.length} file(s) written, ${skipped.length} skipped.`);
109384
+ if (!dryRun && written.length > 0) {
109385
+ console.log(`Review ${CANONICAL_RULES_DIR}/ before committing. Run \`nax rules export --agent=claude\` to regenerate CLAUDE.md.`);
109386
+ }
109387
+ return { written, skipped };
109388
+ }
109389
+
108937
109390
  // src/cli/rules.ts
108938
109391
  var _rulesCLIDeps = {
108939
109392
  readFile: async (path25) => Bun.file(path25).text(),
@@ -108943,7 +109396,7 @@ var _rulesCLIDeps = {
108943
109396
  fileExists: async (path25) => Bun.file(path25).exists(),
108944
109397
  globInDir: (dir) => {
108945
109398
  try {
108946
- return [...new Bun.Glob("*.md").scanSync({ cwd: dir })].sort().map((f) => join75(dir, f));
109399
+ return [...new Bun.Glob("*.md").scanSync({ cwd: dir })].sort().map((f) => join76(dir, f));
108947
109400
  } catch {
108948
109401
  return [];
108949
109402
  }
@@ -109016,8 +109469,8 @@ async function exportRuleDirectory(input) {
109016
109469
  const drifted = [];
109017
109470
  for (const rule of rules) {
109018
109471
  const rel = rule.path ?? rule.fileName;
109019
- const target = resolve19(workdir, ruleDir, rel);
109020
- if (!target.startsWith(`${resolve19(workdir, ruleDir)}${sep7}`)) {
109472
+ const target = resolve20(workdir, ruleDir, rel);
109473
+ if (!target.startsWith(`${resolve20(workdir, ruleDir)}${sep8}`)) {
109021
109474
  throw new NaxError(`Rule path escapes ${ruleDir}: ${rel}`, "RULES_EXPORT_PATH_ESCAPE", {
109022
109475
  stage: "rules-export",
109023
109476
  rule: rel
@@ -109037,9 +109490,9 @@ async function exportRuleDirectory(input) {
109037
109490
  }
109038
109491
  await _rulesCLIDeps.writeFile(target, content);
109039
109492
  }
109040
- const expected = new Set(rules.map((r) => resolve19(workdir, ruleDir, r.path ?? r.fileName)));
109041
- for (const existing of _rulesCLIDeps.globInDir(join75(workdir, ruleDir))) {
109042
- if (!expected.has(resolve19(existing))) {
109493
+ const expected = new Set(rules.map((r) => resolve20(workdir, ruleDir, r.path ?? r.fileName)));
109494
+ for (const existing of _rulesCLIDeps.globInDir(join76(workdir, ruleDir))) {
109495
+ if (!expected.has(resolve20(existing))) {
109043
109496
  _rulesCLIDeps.getLogger().warn("rules-export", "Generated rules dir contains a file with no canonical source", {
109044
109497
  file: existing,
109045
109498
  hint: `Delete it, or add the rule to ${CANONICAL_RULES_DIR}/`
@@ -109094,7 +109547,7 @@ ${r.content}`).join(`
109094
109547
  `);
109095
109548
  const shimContent = `${header + body}
109096
109549
  `;
109097
- const shimPath = join75(workdir, shimFileName);
109550
+ const shimPath = join76(workdir, shimFileName);
109098
109551
  if (options.dryRun) {
109099
109552
  console.log(`[dry-run] Would write ${shimPath} (${shimContent.length} bytes)`);
109100
109553
  return;
@@ -109102,117 +109555,6 @@ ${r.content}`).join(`
109102
109555
  await _rulesCLIDeps.writeFile(shimPath, shimContent);
109103
109556
  console.log(`[OK] Wrote ${shimFileName} (${rules.length} rule file(s) from ${CANONICAL_RULES_DIR}/)`);
109104
109557
  }
109105
- function neutralizeContent(content) {
109106
- let result = content;
109107
- let replacements = 0;
109108
- for (const rule of NEUTRALITY_RULES) {
109109
- for (const { pattern, replacement } of rule.neutralizeSteps ?? []) {
109110
- const matches = [...result.matchAll(pattern)].length;
109111
- if (matches > 0) {
109112
- result = result.replace(pattern, replacement);
109113
- replacements += matches;
109114
- }
109115
- }
109116
- }
109117
- return { content: result.trim(), replacements };
109118
- }
109119
- function toYamlListLiteral(scalar) {
109120
- const trimmed = scalar.trim();
109121
- const unquoted = trimmed.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
109122
- return `[${JSON.stringify(unquoted)}]`;
109123
- }
109124
- function translateLegacyFrontmatter(content) {
109125
- const fm = /^---(\r?\n)([\s\S]*?)\r?\n---\r?\n/.exec(content);
109126
- if (!fm?.[2])
109127
- return { content, translated: false };
109128
- const eol = fm[1] ?? `
109129
- `;
109130
- const block = fm[2];
109131
- if (!/^paths:/m.test(block) || /^appliesTo:/m.test(block))
109132
- return { content, translated: false };
109133
- const scalarMatch = /^paths:[ \t]*(\S.*)$/m.exec(block);
109134
- const isInlineList = scalarMatch?.[1]?.trim().startsWith("[") ?? false;
109135
- const rewritten = scalarMatch && !isInlineList ? block.replace(/^paths:[ \t]*(\S.*)$/m, `appliesTo: ${toYamlListLiteral(scalarMatch[1])}`) : block.replace(/^paths:/m, "appliesTo:");
109136
- const head = content.slice(0, fm.index);
109137
- const tail = content.slice(fm.index + fm[0].length);
109138
- return { content: `${head}---${eol}${rewritten}${eol}---${eol}${tail}`, translated: true };
109139
- }
109140
- function withReviewNotice(content, replacements) {
109141
- if (replacements <= 0)
109142
- return content;
109143
- const notice = `<!-- NOTE: ${replacements} neutralization(s) applied \u2014 review before committing -->
109144
-
109145
- `;
109146
- const fm = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(content);
109147
- if (!fm)
109148
- return notice + content;
109149
- return content.slice(0, fm[0].length) + notice + content.slice(fm[0].length).replace(/^(?:\r?\n)+/, "");
109150
- }
109151
- async function collectMigrationSources(workdir) {
109152
- const sources = [];
109153
- const claudeMdPath = join75(workdir, "CLAUDE.md");
109154
- if (await _rulesCLIDeps.fileExists(claudeMdPath)) {
109155
- const content = await _rulesCLIDeps.readFile(claudeMdPath);
109156
- if (content.trim()) {
109157
- sources.push({ sourcePath: claudeMdPath, targetFileName: "project-conventions.md", content });
109158
- }
109159
- }
109160
- const rulesDir = join75(workdir, ".claude", "rules");
109161
- const ruleFiles = _rulesCLIDeps.globInDir(rulesDir);
109162
- for (const filePath of ruleFiles) {
109163
- try {
109164
- const content = await _rulesCLIDeps.readFile(filePath);
109165
- if (content.trim()) {
109166
- sources.push({ sourcePath: filePath, targetFileName: basename15(filePath), content });
109167
- }
109168
- } catch {}
109169
- }
109170
- return sources;
109171
- }
109172
- async function rulesMigrateCommand(options) {
109173
- const workdir = options.dir ?? process.cwd();
109174
- const force = options.force ?? false;
109175
- const sources = await collectMigrationSources(workdir);
109176
- if (sources.length === 0) {
109177
- console.log("[WARN] No source files found (checked CLAUDE.md and .claude/rules/*.md). Nothing to migrate.");
109178
- return;
109179
- }
109180
- const targetDir = join75(workdir, CANONICAL_RULES_DIR);
109181
- if (!options.dryRun) {
109182
- try {
109183
- await _rulesCLIDeps.mkdir(targetDir);
109184
- } catch (err) {
109185
- throw new NaxError(`Failed to create ${CANONICAL_RULES_DIR}: ${errorMessage(err)}`, "RULES_MIGRATE_MKDIR_FAILED", { stage: "rules-migrate", targetDir });
109186
- }
109187
- }
109188
- let written = 0;
109189
- let skipped = 0;
109190
- for (const { sourcePath, targetFileName, content } of sources) {
109191
- const targetPath = join75(targetDir, targetFileName);
109192
- if (!force && !options.dryRun && await _rulesCLIDeps.fileExists(targetPath)) {
109193
- console.log(`[skip] ${targetFileName} already exists (use --force to overwrite)`);
109194
- skipped++;
109195
- continue;
109196
- }
109197
- const { content: scoped2 } = translateLegacyFrontmatter(content);
109198
- const { content: neutralized, replacements } = neutralizeContent(scoped2);
109199
- const output = withReviewNotice(neutralized, replacements);
109200
- if (options.dryRun) {
109201
- console.log(`[dry-run] Would write ${targetFileName} from ${sourcePath} (${replacements} replacements)`);
109202
- } else {
109203
- await _rulesCLIDeps.writeFile(targetPath, output);
109204
- console.log(`[OK] ${targetFileName} <- ${sourcePath}${replacements > 0 ? ` (${replacements} replacements)` : ""}`);
109205
- }
109206
- written++;
109207
- }
109208
- if (!options.dryRun) {
109209
- console.log(`
109210
- Migration complete: ${written} file(s) written, ${skipped} skipped.`);
109211
- if (written > 0) {
109212
- console.log(`Review ${CANONICAL_RULES_DIR}/ before committing. Run \`nax rules export --agent=claude\` to regenerate CLAUDE.md.`);
109213
- }
109214
- }
109215
- }
109216
109558
  // src/cli/resolve-run-profile.ts
109217
109559
  init_config();
109218
109560
  init_logger2();
@@ -109247,7 +109589,7 @@ async function resolveRunProfileOverride(opts) {
109247
109589
  init_config();
109248
109590
  init_test_runners();
109249
109591
  import { existsSync as existsSync28, readdirSync as readdirSync6 } from "fs";
109250
- import { join as join77, relative as relative17 } from "path";
109592
+ import { join as join78, relative as relative17 } from "path";
109251
109593
 
109252
109594
  // src/cli/features-acceptance.ts
109253
109595
  init_acceptance2();
@@ -109255,7 +109597,7 @@ init_config();
109255
109597
  init_logger2();
109256
109598
  init_prd();
109257
109599
  import { existsSync as existsSync27 } from "fs";
109258
- import { join as join76, relative as relative16 } from "path";
109600
+ import { join as join77, relative as relative16 } from "path";
109259
109601
  async function resolveFeatureAcceptance(featureName, workdir) {
109260
109602
  let enabled = true;
109261
109603
  try {
@@ -109263,13 +109605,13 @@ async function resolveFeatureAcceptance(featureName, workdir) {
109263
109605
  if (!naxDir) {
109264
109606
  return { status: "no-prd", enabled, groups: [] };
109265
109607
  }
109266
- const repoRoot = join76(naxDir, "..");
109608
+ const repoRoot = join77(naxDir, "..");
109267
109609
  const config2 = await loadConfig(workdir);
109268
109610
  enabled = config2.acceptance?.enabled ?? true;
109269
109611
  if (!enabled) {
109270
109612
  return { status: "disabled", enabled: false, groups: [] };
109271
109613
  }
109272
- const prdPath = join76(naxDir, "features", featureName, "prd.json");
109614
+ const prdPath = join77(naxDir, "features", featureName, "prd.json");
109273
109615
  if (!existsSync27(prdPath)) {
109274
109616
  return { status: "no-prd", enabled, groups: [] };
109275
109617
  }
@@ -109321,10 +109663,10 @@ async function isNonEmptyFile(absolutePath) {
109321
109663
  }
109322
109664
  async function searchSpecSource(naxDir, repoRoot, name) {
109323
109665
  const candidates = [
109324
- { abs: join77(naxDir, "features", name, "spec.md"), kind: "markdown" },
109325
- { abs: join77(naxDir, "specs", `${name}.md`), kind: "markdown" }
109666
+ { abs: join78(naxDir, "features", name, "spec.md"), kind: "markdown" },
109667
+ { abs: join78(naxDir, "specs", `${name}.md`), kind: "markdown" }
109326
109668
  ];
109327
- const docsSpecExact = join77(repoRoot, "docs", "specs", `SPEC-${name}.md`);
109669
+ const docsSpecExact = join78(repoRoot, "docs", "specs", `SPEC-${name}.md`);
109328
109670
  candidates.push({ abs: docsSpecExact, kind: "markdown" });
109329
109671
  const checked = candidates.map((c) => relative17(repoRoot, c.abs));
109330
109672
  for (const { abs, kind } of candidates.slice(0, 2)) {
@@ -109338,11 +109680,11 @@ async function searchSpecSource(naxDir, repoRoot, name) {
109338
109680
  if (await isNonEmptyFile(docsSpecExact)) {
109339
109681
  return { source: { kind: "markdown", path: relative17(repoRoot, docsSpecExact) }, checked };
109340
109682
  }
109341
- const docsSpecsDir = join77(repoRoot, "docs", "specs");
109683
+ const docsSpecsDir = join78(repoRoot, "docs", "specs");
109342
109684
  if (existsSync28(docsSpecsDir)) {
109343
109685
  const glob = new Bun.Glob(`*${name}*.md`);
109344
109686
  for (const match of glob.scanSync({ cwd: docsSpecsDir, absolute: false })) {
109345
- const abs = join77(docsSpecsDir, match);
109687
+ const abs = join78(docsSpecsDir, match);
109346
109688
  if (await isNonEmptyFile(abs)) {
109347
109689
  const relPath = relative17(repoRoot, abs);
109348
109690
  if (!checked.includes(relPath))
@@ -109351,7 +109693,7 @@ async function searchSpecSource(naxDir, repoRoot, name) {
109351
109693
  }
109352
109694
  }
109353
109695
  }
109354
- const prdAbs = join77(naxDir, "features", name, "prd.json");
109696
+ const prdAbs = join78(naxDir, "features", name, "prd.json");
109355
109697
  const prdRel = relative17(repoRoot, prdAbs);
109356
109698
  if (!checked.includes(prdRel))
109357
109699
  checked.push(prdRel);
@@ -109361,14 +109703,14 @@ async function searchSpecSource(naxDir, repoRoot, name) {
109361
109703
  return { source: null, checked };
109362
109704
  }
109363
109705
  function discoverCandidates(naxDir) {
109364
- const featuresDir = join77(naxDir, "features");
109706
+ const featuresDir = join78(naxDir, "features");
109365
109707
  if (!existsSync28(featuresDir))
109366
109708
  return [];
109367
109709
  return readdirSync6(featuresDir, { withFileTypes: true }).filter((e) => {
109368
109710
  if (!e.isDirectory())
109369
109711
  return false;
109370
- const dir = join77(featuresDir, e.name);
109371
- return existsSync28(join77(dir, "prd.json")) || existsSync28(join77(dir, "spec.md"));
109712
+ const dir = join78(featuresDir, e.name);
109713
+ return existsSync28(join78(dir, "prd.json")) || existsSync28(join78(dir, "spec.md"));
109372
109714
  }).map((e) => e.name).sort();
109373
109715
  }
109374
109716
  async function resolveFeatureSpec(name, workdir) {
@@ -109379,9 +109721,9 @@ async function resolveFeatureSpec(name, workdir) {
109379
109721
  message: `not a nax repo: no .nax/config.json found from ${workdir}`
109380
109722
  };
109381
109723
  }
109382
- const repoRoot = join77(naxDir, "..");
109724
+ const repoRoot = join78(naxDir, "..");
109383
109725
  if (name !== undefined && (name.startsWith("./") || name.startsWith("/") || name.endsWith(".md"))) {
109384
- const abs = name.startsWith("/") ? name : join77(workdir, name);
109726
+ const abs = name.startsWith("/") ? name : join78(workdir, name);
109385
109727
  if (!existsSync28(abs)) {
109386
109728
  return {
109387
109729
  status: "missing",
@@ -109421,7 +109763,7 @@ async function resolveFeatureSpec(name, workdir) {
109421
109763
  message: `resolved spec: ${source2.path}`
109422
109764
  };
109423
109765
  }
109424
- const featureDir = join77(naxDir, "features", name);
109766
+ const featureDir = join78(naxDir, "features", name);
109425
109767
  if (existsSync28(featureDir)) {
109426
109768
  return {
109427
109769
  status: "missing",
@@ -109484,14 +109826,14 @@ init_runtime();
109484
109826
  init_json_file();
109485
109827
  init_routing();
109486
109828
  import { mkdirSync as mkdirSync6 } from "fs";
109487
- import { basename as basename16, join as join78 } from "path";
109829
+ import { basename as basename16, join as join79 } from "path";
109488
109830
  var _routingCalibrateDeps = {
109489
109831
  loadRunMetrics: (outputDir) => loadRunMetrics(outputDir),
109490
109832
  readConfig: (workdir) => loadConfig(workdir),
109491
109833
  writeConfig: async (workdir, config2) => {
109492
109834
  const dir = projectInputDir(workdir);
109493
109835
  mkdirSync6(dir, { recursive: true });
109494
- const filePath = join78(dir, "config.json");
109836
+ const filePath = join79(dir, "config.json");
109495
109837
  await saveJsonFile(filePath, config2, "routing-calibrate");
109496
109838
  },
109497
109839
  stdout: (msg) => {
@@ -109618,7 +109960,7 @@ init_logger2();
109618
109960
  init_detect2();
109619
109961
  init_workspace();
109620
109962
  init_common();
109621
- import { join as join79 } from "path";
109963
+ import { join as join80 } from "path";
109622
109964
  function resolveEffective(detected, configPatterns) {
109623
109965
  if (configPatterns !== undefined)
109624
109966
  return "config";
@@ -109703,7 +110045,7 @@ async function detectCommand(options) {
109703
110045
  const rootDetected = detectionMap[""] ?? { patterns: [], confidence: "empty", sources: [] };
109704
110046
  const pkgEntries = await Promise.all(packageDirs.map(async (dir) => {
109705
110047
  const det = detectionMap[dir] ?? { patterns: [], confidence: "empty", sources: [] };
109706
- const pkgConfigPath = join79(workdir, ".nax", "mono", dir, "config.json");
110048
+ const pkgConfigPath = join80(workdir, ".nax", "mono", dir, "config.json");
109707
110049
  const pkgRaw = await loadRawConfig(pkgConfigPath);
109708
110050
  const pkgPatterns = deepGet(pkgRaw, TEST_PATTERNS_KEY);
109709
110051
  const effective = Array.isArray(pkgPatterns) ? pkgPatterns : undefined;
@@ -109757,13 +110099,13 @@ async function detectCommand(options) {
109757
110099
  if (rootDetected.confidence === "empty") {
109758
110100
  console.log(source_default.yellow(" root: skipped (empty detection)"));
109759
110101
  } else {
109760
- const rootConfigPath = join79(workdir, ".nax", "config.json");
110102
+ const rootConfigPath = join80(workdir, ".nax", "config.json");
109761
110103
  try {
109762
110104
  const status = await applyToConfig(rootConfigPath, rootDetected.patterns, options.force ?? false);
109763
110105
  if (status === "skipped") {
109764
110106
  console.log(source_default.dim(" root: skipped (testFilePatterns already set; use --force to overwrite)"));
109765
110107
  } else {
109766
- console.log(source_default.green(` root: ${status} \u2192 ${join79(".nax", "config.json")}`));
110108
+ console.log(source_default.green(` root: ${status} \u2192 ${join80(".nax", "config.json")}`));
109767
110109
  }
109768
110110
  } catch (err) {
109769
110111
  console.error(source_default.red(` root: write failed \u2014 ${err.message}`));
@@ -109776,13 +110118,13 @@ async function detectCommand(options) {
109776
110118
  console.log(source_default.dim(` ${dir}: skipped (empty detection)`));
109777
110119
  continue;
109778
110120
  }
109779
- const pkgConfigPath = join79(workdir, ".nax", "mono", dir, "config.json");
110121
+ const pkgConfigPath = join80(workdir, ".nax", "mono", dir, "config.json");
109780
110122
  try {
109781
110123
  const status = await applyToConfig(pkgConfigPath, det.patterns, options.force ?? false);
109782
110124
  if (status === "skipped") {
109783
110125
  console.log(source_default.dim(` ${dir}: skipped (already set)`));
109784
110126
  } else {
109785
- console.log(source_default.green(` ${dir}: ${status} \u2192 ${join79(".nax", "mono", dir, "config.json")}`));
110127
+ console.log(source_default.green(` ${dir}: ${status} \u2192 ${join80(".nax", "mono", dir, "config.json")}`));
109786
110128
  }
109787
110129
  } catch (err) {
109788
110130
  console.error(source_default.red(` ${dir}: write failed \u2014 ${err.message}`));
@@ -109800,19 +110142,19 @@ async function detectCommand(options) {
109800
110142
  // src/commands/logs.ts
109801
110143
  init_common();
109802
110144
  import { existsSync as existsSync30 } from "fs";
109803
- import { join as join82 } from "path";
110145
+ import { join as join83 } from "path";
109804
110146
 
109805
110147
  // src/commands/logs-formatter.ts
109806
110148
  init_source();
109807
110149
  init_formatter();
109808
110150
  import { readdirSync as readdirSync8 } from "fs";
109809
- import { join as join81 } from "path";
110151
+ import { join as join82 } from "path";
109810
110152
 
109811
110153
  // src/commands/logs-reader.ts
109812
110154
  init_paths3();
109813
110155
  import { existsSync as existsSync29, readdirSync as readdirSync7 } from "fs";
109814
110156
  import { readdir as readdir3 } from "fs/promises";
109815
- import { join as join80 } from "path";
110157
+ import { join as join81 } from "path";
109816
110158
  var _logsReaderDeps = {
109817
110159
  getRunsDir
109818
110160
  };
@@ -109826,7 +110168,7 @@ async function resolveRunFileFromRegistry(runId) {
109826
110168
  }
109827
110169
  let matched = null;
109828
110170
  for (const entry of entries) {
109829
- const metaPath = join80(runsDir, entry, "meta.json");
110171
+ const metaPath = join81(runsDir, entry, "meta.json");
109830
110172
  try {
109831
110173
  const meta3 = await Bun.file(metaPath).json();
109832
110174
  if (meta3.runId === runId || meta3.runId.startsWith(runId)) {
@@ -109848,14 +110190,14 @@ async function resolveRunFileFromRegistry(runId) {
109848
110190
  return null;
109849
110191
  }
109850
110192
  const specificFile = files.find((f) => f === `${matched.runId}.jsonl`);
109851
- return join80(matched.eventsDir, specificFile ?? files[0]);
110193
+ return join81(matched.eventsDir, specificFile ?? files[0]);
109852
110194
  }
109853
110195
  async function selectRunFile(runsDir) {
109854
110196
  const files = readdirSync7(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
109855
110197
  if (files.length === 0) {
109856
110198
  return null;
109857
110199
  }
109858
- return join80(runsDir, files[0]);
110200
+ return join81(runsDir, files[0]);
109859
110201
  }
109860
110202
  async function extractRunSummary(filePath) {
109861
110203
  const file3 = Bun.file(filePath);
@@ -109941,7 +110283,7 @@ Runs:
109941
110283
  console.log(source_default.gray(" Timestamp Stories Duration Cost Status"));
109942
110284
  console.log(source_default.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
109943
110285
  for (const file3 of files) {
109944
- const filePath = join81(runsDir, file3);
110286
+ const filePath = join82(runsDir, file3);
109945
110287
  const summary = await extractRunSummary(filePath);
109946
110288
  const timestamp = file3.replace(".jsonl", "");
109947
110289
  const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
@@ -110055,7 +110397,7 @@ async function logsCommand(options) {
110055
110397
  return;
110056
110398
  }
110057
110399
  const resolved = resolveProject2({ dir: options.dir });
110058
- const naxDir = join82(resolved.projectDir, ".nax");
110400
+ const naxDir = join83(resolved.projectDir, ".nax");
110059
110401
  const configPath = resolved.configPath;
110060
110402
  const configFile = Bun.file(configPath);
110061
110403
  const config2 = await configFile.json();
@@ -110063,8 +110405,8 @@ async function logsCommand(options) {
110063
110405
  if (!featureName) {
110064
110406
  throw new Error("No feature specified in config.json");
110065
110407
  }
110066
- const featureDir = join82(naxDir, "features", featureName);
110067
- const runsDir = join82(featureDir, "runs");
110408
+ const featureDir = join83(naxDir, "features", featureName);
110409
+ const runsDir = join83(featureDir, "runs");
110068
110410
  if (!existsSync30(runsDir)) {
110069
110411
  throw new Error(`No runs directory found for feature: ${featureName}`);
110070
110412
  }
@@ -110090,7 +110432,7 @@ init_prd();
110090
110432
  init_precheck();
110091
110433
  init_common();
110092
110434
  import { existsSync as existsSync31 } from "fs";
110093
- import { join as join83 } from "path";
110435
+ import { join as join84 } from "path";
110094
110436
  async function precheckCommand(options) {
110095
110437
  const resolved = resolveProject2({
110096
110438
  dir: options.dir,
@@ -110112,9 +110454,9 @@ async function precheckCommand(options) {
110112
110454
  process.exit(1);
110113
110455
  }
110114
110456
  }
110115
- const naxDir = join83(resolved.projectDir, ".nax");
110116
- const featureDir = join83(naxDir, "features", featureName);
110117
- const prdPath = join83(featureDir, "prd.json");
110457
+ const naxDir = join84(resolved.projectDir, ".nax");
110458
+ const featureDir = join84(naxDir, "features", featureName);
110459
+ const prdPath = join84(featureDir, "prd.json");
110118
110460
  if (!existsSync31(featureDir)) {
110119
110461
  console.error(source_default.red(`Feature not found: ${featureName}`));
110120
110462
  process.exit(1);
@@ -110142,7 +110484,7 @@ import { dirname as dirname14 } from "path";
110142
110484
  init_errors();
110143
110485
  init_paths3();
110144
110486
  import { readdir as readdir4 } from "fs/promises";
110145
- import { join as join84 } from "path";
110487
+ import { join as join85 } from "path";
110146
110488
  var _discoveryDeps = {
110147
110489
  getRunsDir
110148
110490
  };
@@ -110156,7 +110498,7 @@ async function loadMetas(runsDir) {
110156
110498
  const metas = [];
110157
110499
  for (const entry of entries) {
110158
110500
  try {
110159
- const meta3 = await Bun.file(join84(runsDir, entry, "meta.json")).json();
110501
+ const meta3 = await Bun.file(join85(runsDir, entry, "meta.json")).json();
110160
110502
  metas.push(meta3);
110161
110503
  } catch {}
110162
110504
  }
@@ -110173,7 +110515,7 @@ async function discoverRun(query, depsArg = _discoveryDeps) {
110173
110515
  throw new NaxError("No runs registered", "RUN_NOT_FOUND", { runsDir });
110174
110516
  }
110175
110517
  const latest = metas.reduce((acc, m) => m.runId > acc.runId ? m : acc);
110176
- return { meta: latest, jsonlPath: join84(latest.eventsDir, `${latest.runId}.jsonl`) };
110518
+ return { meta: latest, jsonlPath: join85(latest.eventsDir, `${latest.runId}.jsonl`) };
110177
110519
  }
110178
110520
  const matched = metas.filter((m) => matches(m, query));
110179
110521
  if (matched.length === 0) {
@@ -110190,7 +110532,7 @@ async function discoverRun(query, depsArg = _discoveryDeps) {
110190
110532
  });
110191
110533
  }
110192
110534
  const meta3 = matched[0];
110193
- return { meta: meta3, jsonlPath: join84(meta3.eventsDir, `${meta3.runId}.jsonl`) };
110535
+ return { meta: meta3, jsonlPath: join85(meta3.eventsDir, `${meta3.runId}.jsonl`) };
110194
110536
  }
110195
110537
 
110196
110538
  // src/replay/json.ts
@@ -110528,11 +110870,11 @@ init_errors();
110528
110870
  init_checkpoint();
110529
110871
  init_runtime();
110530
110872
  import { existsSync as existsSync37 } from "fs";
110531
- import { basename as basename19, join as join98 } from "path";
110873
+ import { basename as basename19, join as join99 } from "path";
110532
110874
  async function defaultCheckpointExists(featureDir) {
110533
110875
  if (!featureDir || !existsSync37(featureDir))
110534
110876
  return false;
110535
- return existsSync37(join98(featureDir, "checkpoint.jsonl"));
110877
+ return existsSync37(join99(featureDir, "checkpoint.jsonl"));
110536
110878
  }
110537
110879
  async function defaultLoadCheckpoints(featureDir) {
110538
110880
  return loadCheckpoints(featureDir);
@@ -110592,12 +110934,12 @@ function registerResumeCommand(program2) {
110592
110934
  `);
110593
110935
  process.exit(1);
110594
110936
  }
110595
- const featureDir = join98(naxDir, "features", cmdOpts.feature);
110937
+ const featureDir = join99(naxDir, "features", cmdOpts.feature);
110596
110938
  const deps = {
110597
110939
  ..._resumeCmdDeps,
110598
110940
  runInvocation: async (feature, opts) => {
110599
110941
  const config2 = await loadConfig2(naxDir ?? undefined);
110600
- const prdPath = join98(opts.featureDir ?? "", "prd.json");
110942
+ const prdPath = join99(opts.featureDir ?? "", "prd.json");
110601
110943
  if (!existsSync38(prdPath)) {
110602
110944
  process.stderr.write(`Feature "${feature}" not found or missing prd.json
110603
110945
  `);
@@ -110609,7 +110951,7 @@ function registerResumeCommand(program2) {
110609
110951
  applyResumeModeDeps2(opts.featureDir ?? "", "auto");
110610
110952
  const projectKey = config2.name?.trim() || basename19(cmdOpts.dir);
110611
110953
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
110612
- const statusFilePath = join98(outputDir, "status.json");
110954
+ const statusFilePath = join99(outputDir, "status.json");
110613
110955
  const result = await run2({
110614
110956
  prdPath,
110615
110957
  workdir: cmdOpts.dir,
@@ -110644,7 +110986,7 @@ function registerResumeCommand(program2) {
110644
110986
  init_source();
110645
110987
  init_paths3();
110646
110988
  import { readdir as readdir6 } from "fs/promises";
110647
- import { join as join99 } from "path";
110989
+ import { join as join100 } from "path";
110648
110990
  var DEFAULT_LIMIT = 20;
110649
110991
  var _runsCmdDeps = {
110650
110992
  getRunsDir
@@ -110701,7 +111043,7 @@ async function runsCommand(options = {}) {
110701
111043
  }
110702
111044
  const rows = [];
110703
111045
  for (const entry of entries) {
110704
- const metaPath = join99(runsDir, entry, "meta.json");
111046
+ const metaPath = join100(runsDir, entry, "meta.json");
110705
111047
  let meta3;
110706
111048
  try {
110707
111049
  meta3 = await Bun.file(metaPath).json();
@@ -110778,7 +111120,7 @@ async function runsCommand(options = {}) {
110778
111120
 
110779
111121
  // src/commands/unlock.ts
110780
111122
  init_source();
110781
- import { join as join100 } from "path";
111123
+ import { join as join101 } from "path";
110782
111124
  function isProcessAlive2(pid) {
110783
111125
  try {
110784
111126
  process.kill(pid, 0);
@@ -110793,7 +111135,7 @@ function formatLockAge(ageMs) {
110793
111135
  }
110794
111136
  async function unlockCommand(options) {
110795
111137
  const workdir = options.dir ?? process.cwd();
110796
- const lockPath = join100(workdir, "nax.lock");
111138
+ const lockPath = join101(workdir, "nax.lock");
110797
111139
  const lockFile = Bun.file(lockPath);
110798
111140
  const exists = await lockFile.exists();
110799
111141
  if (!exists) {
@@ -116443,8 +116785,8 @@ class Ink {
116443
116785
  }
116444
116786
  }
116445
116787
  async waitUntilExit() {
116446
- this.exitPromise ||= new Promise((resolve22, reject2) => {
116447
- this.resolveExitPromise = resolve22;
116788
+ this.exitPromise ||= new Promise((resolve23, reject2) => {
116789
+ this.resolveExitPromise = resolve23;
116448
116790
  this.rejectExitPromise = reject2;
116449
116791
  });
116450
116792
  if (!this.beforeExitHandler) {
@@ -118709,7 +119051,7 @@ async function promptForConfirmation(question) {
118709
119051
  if (!process.stdin.isTTY) {
118710
119052
  return true;
118711
119053
  }
118712
- return new Promise((resolve23) => {
119054
+ return new Promise((resolve24) => {
118713
119055
  process.stdout.write(source_default.bold(`${question} [Y/n] `));
118714
119056
  process.stdin.setRawMode(true);
118715
119057
  process.stdin.resume();
@@ -118722,9 +119064,9 @@ async function promptForConfirmation(question) {
118722
119064
  process.stdout.write(`
118723
119065
  `);
118724
119066
  if (answer === "n") {
118725
- resolve23(false);
119067
+ resolve24(false);
118726
119068
  } else {
118727
- resolve23(true);
119069
+ resolve24(true);
118728
119070
  }
118729
119071
  };
118730
119072
  process.stdin.on("data", handler);
@@ -118753,7 +119095,7 @@ Next: nax generate --package ${options.package}`));
118753
119095
  }
118754
119096
  return;
118755
119097
  }
118756
- const naxDir = join104(workdir, ".nax");
119098
+ const naxDir = join105(workdir, ".nax");
118757
119099
  if (existsSync39(naxDir) && !options.force) {
118758
119100
  console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
118759
119101
  return;
@@ -118782,11 +119124,11 @@ Next: nax generate --package ${options.package}`));
118782
119124
  }
118783
119125
  }
118784
119126
  }
118785
- mkdirSync8(join104(naxDir, "features"), { recursive: true });
118786
- mkdirSync8(join104(naxDir, "hooks"), { recursive: true });
119127
+ mkdirSync8(join105(naxDir, "features"), { recursive: true });
119128
+ mkdirSync8(join105(naxDir, "hooks"), { recursive: true });
118787
119129
  const initConfig = options.name ? { ...DEFAULT_CONFIG, name: options.name } : DEFAULT_CONFIG;
118788
- await Bun.write(join104(naxDir, "config.json"), JSON.stringify(initConfig, null, 2));
118789
- await Bun.write(join104(naxDir, "hooks.json"), JSON.stringify({
119130
+ await Bun.write(join105(naxDir, "config.json"), JSON.stringify(initConfig, null, 2));
119131
+ await Bun.write(join105(naxDir, "hooks.json"), JSON.stringify({
118790
119132
  hooks: {
118791
119133
  "on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
118792
119134
  "on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
@@ -118794,12 +119136,12 @@ Next: nax generate --package ${options.package}`));
118794
119136
  "on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false }
118795
119137
  }
118796
119138
  }, null, 2));
118797
- await Bun.write(join104(naxDir, ".gitignore"), `# nax temp files
119139
+ await Bun.write(join105(naxDir, ".gitignore"), `# nax temp files
118798
119140
  *.tmp
118799
119141
  .paused.json
118800
119142
  .nax-verifier-verdict.json
118801
119143
  `);
118802
- await Bun.write(join104(naxDir, "context.md"), `# Project Context
119144
+ await Bun.write(join105(naxDir, "context.md"), `# Project Context
118803
119145
 
118804
119146
  This document defines coding standards, architectural decisions, and forbidden patterns for this project.
118805
119147
  Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
@@ -118983,7 +119325,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
118983
119325
  const cliOverrides = {};
118984
119326
  const cliProfiles = options.profile ?? [];
118985
119327
  const profileOverride = naxDir ? await resolveRunProfileOverride({
118986
- prdPath: join104(naxDir, "features", options.feature, "prd.json"),
119328
+ prdPath: join105(naxDir, "features", options.feature, "prd.json"),
118987
119329
  projectRoot: workdir,
118988
119330
  cliProfile: cliProfiles,
118989
119331
  envProfile: process.env.NAX_PROFILE
@@ -118996,8 +119338,8 @@ program2.command("run").description("Run the orchestration loop for a feature").
118996
119338
  console.error(source_default.red("nax not initialized. Run: nax init"));
118997
119339
  process.exit(1);
118998
119340
  }
118999
- const featureDir = join104(naxDir, "features", options.feature);
119000
- const prdPath = join104(featureDir, "prd.json");
119341
+ const featureDir = join105(naxDir, "features", options.feature);
119342
+ const prdPath = join105(featureDir, "prd.json");
119001
119343
  if (options.plan && options.from) {
119002
119344
  if (existsSync39(prdPath) && !options.force) {
119003
119345
  console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
@@ -119019,10 +119361,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
119019
119361
  }
119020
119362
  }
119021
119363
  try {
119022
- const planLogDir = join104(featureDir, "plan");
119364
+ const planLogDir = join105(featureDir, "plan");
119023
119365
  mkdirSync8(planLogDir, { recursive: true });
119024
119366
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
119025
- const planLogPath = join104(planLogDir, `${planLogId}.jsonl`);
119367
+ const planLogPath = join105(planLogDir, `${planLogId}.jsonl`);
119026
119368
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
119027
119369
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
119028
119370
  console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
@@ -119070,10 +119412,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
119070
119412
  resetLogger();
119071
119413
  const projectKey = config2.name?.trim() || basename21(workdir);
119072
119414
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
119073
- const runsDir = join104(outputDir, "features", options.feature, "runs");
119415
+ const runsDir = join105(outputDir, "features", options.feature, "runs");
119074
119416
  mkdirSync8(runsDir, { recursive: true });
119075
119417
  const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
119076
- const logFilePath = join104(runsDir, `${runId}.jsonl`);
119418
+ const logFilePath = join105(runsDir, `${runId}.jsonl`);
119077
119419
  const isTTY = process.stdout.isTTY ?? false;
119078
119420
  const headlessFlag = options.headless ?? false;
119079
119421
  const headlessEnv = process.env.NAX_HEADLESS === "1";
@@ -119099,7 +119441,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
119099
119441
  }
119100
119442
  config2.execution.costLimit = maxCost;
119101
119443
  }
119102
- const globalNaxDir = join104(homedir3(), ".nax");
119444
+ const globalNaxDir = join105(homedir3(), ".nax");
119103
119445
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
119104
119446
  const eventEmitter = new PipelineEventEmitter;
119105
119447
  const agentStreamEvents = useHeadless ? undefined : new AgentStreamEventBus;
@@ -119118,12 +119460,12 @@ program2.command("run").description("Run the orchestration loop for a feature").
119118
119460
  stories: initialStories,
119119
119461
  events: eventEmitter,
119120
119462
  agentStreamEvents,
119121
- queueFilePath: join104(workdir, ".queue.txt")
119463
+ queueFilePath: join105(workdir, ".queue.txt")
119122
119464
  });
119123
119465
  } else {
119124
119466
  console.log(source_default.dim(" [Headless mode \u2014 pipe output]"));
119125
119467
  }
119126
- const statusFilePath = join104(outputDir, "status.json");
119468
+ const statusFilePath = join105(outputDir, "status.json");
119127
119469
  let parallel;
119128
119470
  if (options.parallel !== undefined) {
119129
119471
  parallel = Number.parseInt(options.parallel, 10);
@@ -119186,7 +119528,7 @@ Scheduled run cancelled.`));
119186
119528
  agentStreamEvents,
119187
119529
  resumeMode: options.fresh === true || options.resume === false ? "fresh" : "auto"
119188
119530
  });
119189
- const latestSymlink = join104(runsDir, "latest.jsonl");
119531
+ const latestSymlink = join105(runsDir, "latest.jsonl");
119190
119532
  try {
119191
119533
  if (existsSync39(latestSymlink)) {
119192
119534
  Bun.spawnSync(["rm", latestSymlink]);
@@ -119283,9 +119625,9 @@ features.command("create <name>").description("Create a new feature").option("-d
119283
119625
  console.error(source_default.red("nax not initialized. Run: nax init"));
119284
119626
  process.exit(1);
119285
119627
  }
119286
- const featureDir = join104(naxDir, "features", name);
119628
+ const featureDir = join105(naxDir, "features", name);
119287
119629
  mkdirSync8(featureDir, { recursive: true });
119288
- await Bun.write(join104(featureDir, "spec.md"), `# Feature: ${name}
119630
+ await Bun.write(join105(featureDir, "spec.md"), `# Feature: ${name}
119289
119631
 
119290
119632
  ## Overview
119291
119633
 
@@ -119318,7 +119660,7 @@ features.command("create <name>").description("Create a new feature").option("-d
119318
119660
 
119319
119661
  <!-- What this feature explicitly does NOT cover. -->
119320
119662
  `);
119321
- await Bun.write(join104(featureDir, "progress.txt"), `# Progress: ${name}
119663
+ await Bun.write(join105(featureDir, "progress.txt"), `# Progress: ${name}
119322
119664
 
119323
119665
  Created: ${new Date().toISOString()}
119324
119666
 
@@ -119344,7 +119686,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
119344
119686
  console.error(source_default.red("nax not initialized."));
119345
119687
  process.exit(1);
119346
119688
  }
119347
- const featuresDir = join104(naxDir, "features");
119689
+ const featuresDir = join105(naxDir, "features");
119348
119690
  if (!existsSync39(featuresDir)) {
119349
119691
  console.log(source_default.dim("No features yet."));
119350
119692
  return;
@@ -119359,7 +119701,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
119359
119701
  Features:
119360
119702
  `));
119361
119703
  for (const name of entries) {
119362
- const prdPath = join104(featuresDir, name, "prd.json");
119704
+ const prdPath = join105(featuresDir, name, "prd.json");
119363
119705
  if (existsSync39(prdPath)) {
119364
119706
  const prd = await loadPRD(prdPath);
119365
119707
  const c = countStories(prd);
@@ -119432,10 +119774,10 @@ Use: nax plan -f <feature> --from <spec>`));
119432
119774
  cliOverrides.profile = cliProfiles;
119433
119775
  }
119434
119776
  const config2 = await loadConfig(workdir, cliOverrides);
119435
- const featureLogDir = join104(naxDir, "features", options.feature, "plan");
119777
+ const featureLogDir = join105(naxDir, "features", options.feature, "plan");
119436
119778
  mkdirSync8(featureLogDir, { recursive: true });
119437
119779
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
119438
- const planLogPath = join104(featureLogDir, `${planLogId}.jsonl`);
119780
+ const planLogPath = join105(featureLogDir, `${planLogId}.jsonl`);
119439
119781
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
119440
119782
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
119441
119783
  try {