@nathapp/nax 0.82.1 → 0.82.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +75 -42
  2. package/dist/nax.js +1059 -594
  3. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -2668,7 +2668,7 @@ var package_default;
2668
2668
  var init_package = __esm(() => {
2669
2669
  package_default = {
2670
2670
  name: "@nathapp/nax",
2671
- version: "0.82.1",
2671
+ version: "0.82.3",
2672
2672
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
2673
2673
  type: "module",
2674
2674
  bin: {
@@ -18398,10 +18398,26 @@ function isNaxConfigFile(root, resolved) {
18398
18398
  return segments.length === 2 || segments.length >= 4 && segments[1] === "mono";
18399
18399
  }
18400
18400
  function isNaxOwnedWritePath(rel) {
18401
+ return naxOwnedKind(rel) !== undefined;
18402
+ }
18403
+ function naxOwnedKind(rel) {
18401
18404
  const segments = rel.split("/");
18402
18405
  if (segments.length === 1 && QUEUE_CONTROL_FILES.has(segments[0] ?? ""))
18403
- return true;
18404
- return segments[0] === ".nax" && segments[1] === "features" && segments[segments.length - 1] === "prd.json";
18406
+ return "queue";
18407
+ if (segments[0] === ".nax" && segments[1] === "features" && segments[segments.length - 1] === "prd.json") {
18408
+ return "prd";
18409
+ }
18410
+ return;
18411
+ }
18412
+ function naxOwnedBashRefusal(tool, kind, hit, verb) {
18413
+ switch (kind) {
18414
+ case "prd":
18415
+ return `${tool} command ${verb} "${hit}", which holds this story's acceptance criteria. ` + "nax updates it itself during the run, so it shows as modified. " + "Bash commands naming it are refused, reads included -- leave it as is. " + "To view it, use the `Read` tool.";
18416
+ case "queue":
18417
+ return `${tool} command ${verb} "${hit}", which is nax's run-control queue. ` + "Bash commands naming it are refused, reads included -- change the run through the queue command.";
18418
+ case "config":
18419
+ return `${tool} command ${verb} "${hit}", which is nax configuration. ` + "Bash commands naming it are refused, reads included -- nax configuration is not changed from inside a run.";
18420
+ }
18405
18421
  }
18406
18422
  function naxOwnedWriteRefusal(tool, rel, exemptRel) {
18407
18423
  if (!NAX_OWNED_WRITE_TOOLS.has(tool))
@@ -19566,7 +19582,7 @@ function capitalize(text) {
19566
19582
  return text.charAt(0).toUpperCase() + text.slice(1);
19567
19583
  }
19568
19584
  function escalateDescription(shell, patterns) {
19569
- return `Run one shell command string under ${shell}. ${PREFER_STRUCTURED_TOOLS_SENTENCE}` + `${capitalize(describeGrants(patterns))}. A command whose every segment matches a granted form is checked further: paths ` + "and redirect targets must stay inside the repository root, and `.git/` access, denied flags, unexpanded " + "`$VAR`, glob or brace characters, `~`, and a bare or option-shaped `cd` are refused without asking. A command " + "outside the granted forms, or one using a construct that cannot be analysed (e.g. command or process " + "substitution, backticks, here-documents, subshells, `2>&1`, `#` comments), is not refused: it is sent to a " + "human for approval (unless an identical command was already approved and remembered) and, if they allow it, " + "runs exactly as written; it is refused if they deny it or do not answer in time, so prefer the granted forms. A command matching a deny rule is refused without asking unless it cannot " + "be analysed. Each segment of a `&&`/`||`/`;`/`|` chain is checked separately. " + BACKGROUND_PROCESSES_KILLED_SENTENCE;
19585
+ return `Run one shell command string under ${shell}. ${PREFER_STRUCTURED_TOOLS_SENTENCE}` + `${capitalize(describeGrants(patterns))}. Every command, granted or not, is checked first: paths ` + "and redirect targets must stay inside the repository root, and `.git/` access, denied flags, unexpanded " + "`$VAR`, glob or brace characters, `~`, a bare `cd`, and a command matching a deny rule are " + "refused without asking. For a command using a construct that cannot be analysed (e.g. command or process " + "substitution, backticks, here-documents, subshells, `2>&1`, `#` comments, an option-shaped `cd`), these " + "checks cover the part before that construct. A command outside the granted forms, or one using a construct " + "that cannot be analysed, is not refused: it is sent to a human for approval (unless an identical command " + "was already approved and remembered) and, if they allow it, runs exactly as written; it is refused if they " + "deny it or do not answer in time, so prefer the granted forms. Each segment of a `&&`/`||`/`;`/`|` chain is " + "checked separately. " + BACKGROUND_PROCESSES_KILLED_SENTENCE;
19570
19586
  }
19571
19587
  function rawDescription(shell, containment = RAW_UNCONTAINED) {
19572
19588
  return `Run one shell command string under ${shell}. ${PREFER_STRUCTURED_TOOLS_SENTENCE}` + "This stage runs under raw mode (ADR-030): pipes, redirects, command substitution ($(...), backticks), " + "process substitution, here-documents and subshells all work here -- nothing is refused for being unparseable, " + "and Bash allow/deny/ask rules configured for this stage are NOT consulted. " + containment + "The only refusal is a command the lexer CAN parse that names or redirects into one of the exact file " + "paths nax owns -- .nax/config.json, .nax/mono/*/config.json, " + ".nax/features/**/prd.json, or the root queue-control files -- change those through nax rather than by " + "writing them directly. That screen is advisory, not a boundary: it matches exact file paths only, so a " + "command using command substitution, a directory target (cp x .nax/), a glob, a nested shell (sh -c '...'), " + "tar -C or dd of=, or a symlink alias all skip it; use the sandbox for a boundary. " + BACKGROUND_PROCESSES_KILLED_SENTENCE;
@@ -19779,6 +19795,46 @@ var init_delete = __esm(() => {
19779
19795
  };
19780
19796
  });
19781
19797
 
19798
+ // src/tools/edit-region.ts
19799
+ function countNewlines(text) {
19800
+ let count = 0;
19801
+ for (let i = 0;i < text.length; i += 1) {
19802
+ if (text[i] === `
19803
+ `)
19804
+ count += 1;
19805
+ }
19806
+ return count;
19807
+ }
19808
+ function composeEditRegion({ updated, matchIndex, newStringLength }) {
19809
+ const lines = splitModelLines(updated);
19810
+ const total = lines.length;
19811
+ if (total === 0)
19812
+ return "[file is now empty]";
19813
+ const start = Math.min(total, 1 + countNewlines(updated.slice(0, matchIndex)));
19814
+ const end = newStringLength > 0 ? Math.min(total, 1 + countNewlines(updated.slice(0, matchIndex + newStringLength - 1))) : start;
19815
+ const a = Math.max(1, start - CONTEXT_LINES);
19816
+ const b = Math.min(total, end + CONTEXT_LINES);
19817
+ const header = `[lines ${a}-${b} of ${total}]`;
19818
+ const selected = end - start + 1 > MAX_REGION_LINES_SHOWN ? [
19819
+ ...lines.slice(a - 1, start + 2),
19820
+ `[... lines ${start + 3}-${end - 3} not shown ...]`,
19821
+ ...lines.slice(end - 3, b)
19822
+ ] : lines.slice(a - 1, b);
19823
+ let view = [header, ...selected].join(`
19824
+ `);
19825
+ while (view.endsWith(`
19826
+ `))
19827
+ view = view.slice(0, -1);
19828
+ return view;
19829
+ }
19830
+ function replaceUniqueLiteral(source, oldString, newString, matchIndex) {
19831
+ return source.slice(0, matchIndex) + newString + source.slice(matchIndex + oldString.length);
19832
+ }
19833
+ var CONTEXT_LINES = 3, MAX_REGION_LINES_SHOWN = 8;
19834
+ var init_edit_region = __esm(() => {
19835
+ init_truncate();
19836
+ });
19837
+
19782
19838
  // src/tools/edit.ts
19783
19839
  import { readFile, stat, writeFile } from "fs/promises";
19784
19840
  function countOccurrences(haystack, needle) {
@@ -19792,11 +19848,17 @@ function countOccurrences(haystack, needle) {
19792
19848
  }
19793
19849
  return count;
19794
19850
  }
19795
- var editTool;
19851
+ var _editDeps, editTool;
19796
19852
  var init_edit = __esm(() => {
19853
+ init_edit_region();
19854
+ _editDeps = {
19855
+ stat: (path) => stat(path),
19856
+ readFile: (path, encoding) => readFile(path, encoding),
19857
+ writeFile: (path, data, encoding) => writeFile(path, data, encoding)
19858
+ };
19797
19859
  editTool = {
19798
19860
  name: "Edit",
19799
- description: "Replace one exact occurrence of old_string with new_string in a repository file. Fails if the match is absent or ambiguous.",
19861
+ description: "Replace one exact occurrence of old_string with new_string in a repository file. Fails if the match is absent or ambiguous. On success the result shows the edited lines with up to 3 lines of context and their line range, so you do not need to Read the file again to check the edit.",
19800
19862
  inputSchema: {
19801
19863
  type: "object",
19802
19864
  properties: {
@@ -19817,7 +19879,7 @@ var init_edit = __esm(() => {
19817
19879
  return { content: "old_string and new_string must be strings", isError: true };
19818
19880
  }
19819
19881
  try {
19820
- const { size } = await stat(target);
19882
+ const { size } = await _editDeps.stat(target);
19821
19883
  if (size > ctx.maxFileBytes) {
19822
19884
  return {
19823
19885
  content: `the file is ${size} bytes, which exceeds the ${ctx.maxFileBytes}-byte file ceiling -- refusing to edit ${target}`,
@@ -19829,7 +19891,7 @@ var init_edit = __esm(() => {
19829
19891
  }
19830
19892
  let source;
19831
19893
  try {
19832
- source = await readFile(target, "utf8");
19894
+ source = await _editDeps.readFile(target, "utf8");
19833
19895
  } catch (err) {
19834
19896
  return { content: err instanceof Error ? err.message : String(err), isError: true };
19835
19897
  }
@@ -19844,8 +19906,12 @@ var init_edit = __esm(() => {
19844
19906
  };
19845
19907
  }
19846
19908
  try {
19847
- await writeFile(target, source.replace(oldString, newString), "utf8");
19848
- return { content: `edited ${target}` };
19909
+ const matchIndex = source.indexOf(oldString);
19910
+ const updated = replaceUniqueLiteral(source, oldString, newString, matchIndex);
19911
+ await _editDeps.writeFile(target, updated, "utf8");
19912
+ const region = composeEditRegion({ updated, matchIndex, newStringLength: newString.length });
19913
+ return { content: `edited ${target}
19914
+ ${region}` };
19849
19915
  } catch (err) {
19850
19916
  return { content: err instanceof Error ? err.message : String(err), isError: true };
19851
19917
  }
@@ -20927,9 +20993,6 @@ var ASK_NO_CHANNEL_REASON = "matched an ask rule requiring human approval; no ap
20927
20993
  var init_ask = () => {};
20928
20994
 
20929
20995
  // src/permissions/bash-lex.ts
20930
- function refused(construct) {
20931
- return { kind: "refused", construct };
20932
- }
20933
20996
  function doubleQuoteEnd(command, from) {
20934
20997
  for (let i = from;i < command.length; i += 1) {
20935
20998
  const char = command[i];
@@ -20944,7 +21007,7 @@ function doubleQuoteEnd(command, from) {
20944
21007
  }
20945
21008
  function lexBashCommand(command) {
20946
21009
  if (command.trim() === "")
20947
- return refused("an empty command");
21010
+ return { kind: "refused", construct: "an empty command", prefix: [] };
20948
21011
  const segments = [];
20949
21012
  let tokens = [];
20950
21013
  let redirects = [];
@@ -20952,6 +21015,10 @@ function lexBashCommand(command) {
20952
21015
  let opaque = false;
20953
21016
  let started = false;
20954
21017
  let pendingRedirect;
21018
+ function refusedHere(construct) {
21019
+ const prefix = tokens.length === 0 && redirects.length === 0 ? [...segments] : [...segments, { tokens, redirects }];
21020
+ return { kind: "refused", construct, prefix };
21021
+ }
20955
21022
  function flushWord() {
20956
21023
  if (!started)
20957
21024
  return;
@@ -20983,7 +21050,7 @@ function lexBashCommand(command) {
20983
21050
  if (char === "'") {
20984
21051
  const end = command.indexOf("'", i + 1);
20985
21052
  if (end === -1)
20986
- return refused("an unbalanced single quote");
21053
+ return refusedHere("an unbalanced single quote");
20987
21054
  word += command.slice(i + 1, end);
20988
21055
  started = true;
20989
21056
  i = end + 1;
@@ -20992,12 +21059,12 @@ function lexBashCommand(command) {
20992
21059
  if (char === '"') {
20993
21060
  const end = doubleQuoteEnd(command, i + 1);
20994
21061
  if (end === -1)
20995
- return refused("an unbalanced double quote");
21062
+ return refusedHere("an unbalanced double quote");
20996
21063
  const inner = command.slice(i + 1, end);
20997
21064
  if (inner.includes("$("))
20998
- return refused("a command substitution `$(...)`");
21065
+ return refusedHere("a command substitution `$(...)`");
20999
21066
  if (inner.includes("`"))
21000
- return refused("a backtick command substitution");
21067
+ return refusedHere("a backtick command substitution");
21001
21068
  if (inner.includes("$"))
21002
21069
  opaque = true;
21003
21070
  word += inner;
@@ -21007,32 +21074,32 @@ function lexBashCommand(command) {
21007
21074
  }
21008
21075
  if (char === "\\") {
21009
21076
  if (next === undefined)
21010
- return refused("a trailing backslash");
21077
+ return refusedHere("a trailing backslash");
21011
21078
  word += next;
21012
21079
  started = true;
21013
21080
  i += 2;
21014
21081
  continue;
21015
21082
  }
21016
21083
  if (char === "(" || char === ")")
21017
- return refused("a subshell `( ... )`");
21084
+ return refusedHere("a subshell `( ... )`");
21018
21085
  if (char === "!" && !started)
21019
- return refused("a `!` negation");
21086
+ return refusedHere("a `!` negation");
21020
21087
  if (char === "#" && !started)
21021
- return refused("a `#` comment");
21088
+ return refusedHere("a `#` comment");
21022
21089
  if (char === "$" && next === "(")
21023
- return refused("a command substitution `$(...)`");
21090
+ return refusedHere("a command substitution `$(...)`");
21024
21091
  if (char === "`")
21025
- return refused("a backtick command substitution");
21092
+ return refusedHere("a backtick command substitution");
21026
21093
  if ((char === "<" || char === ">") && next === "(") {
21027
- return refused("a process substitution `<(...)` / `>(...)`");
21094
+ return refusedHere("a process substitution `<(...)` / `>(...)`");
21028
21095
  }
21029
21096
  if (char === "<" && next === "<")
21030
- return refused("a here-document `<<`");
21097
+ return refusedHere("a here-document `<<`");
21031
21098
  if ((char === "<" || char === ">") && next === "&") {
21032
- return refused("file-descriptor duplication (`2>&1`)");
21099
+ return refusedHere("file-descriptor duplication (`2>&1`)");
21033
21100
  }
21034
21101
  if (char === "&" && next === ">")
21035
- return refused("the `&>` redirection form");
21102
+ return refusedHere("the `&>` redirection form");
21036
21103
  if (char === "$") {
21037
21104
  opaque = true;
21038
21105
  word += char;
@@ -21049,21 +21116,21 @@ function lexBashCommand(command) {
21049
21116
  ` || char === ";") {
21050
21117
  const error49 = flushSegment(";");
21051
21118
  if (error49 !== undefined)
21052
- return refused(error49);
21119
+ return refusedHere(error49);
21053
21120
  i += 1;
21054
21121
  continue;
21055
21122
  }
21056
21123
  if (char === "&" && next === "&" || char === "|" && next === "|") {
21057
21124
  const error49 = flushSegment(char === "&" ? "&&" : "||");
21058
21125
  if (error49 !== undefined)
21059
- return refused(error49);
21126
+ return refusedHere(error49);
21060
21127
  i += 2;
21061
21128
  continue;
21062
21129
  }
21063
21130
  if (char === "|" || char === "&") {
21064
21131
  const error49 = flushSegment(char);
21065
21132
  if (error49 !== undefined)
21066
- return refused(error49);
21133
+ return refusedHere(error49);
21067
21134
  i += 1;
21068
21135
  continue;
21069
21136
  }
@@ -21088,7 +21155,7 @@ function lexBashCommand(command) {
21088
21155
  }
21089
21156
  const error48 = flushSegment();
21090
21157
  if (error48 !== undefined)
21091
- return refused(error48);
21158
+ return refusedHere(error48);
21092
21159
  return { kind: "ok", segments };
21093
21160
  }
21094
21161
 
@@ -21219,7 +21286,9 @@ function checkPayload(args, segment, cwd) {
21219
21286
  case "no-target":
21220
21287
  return { refusal: deny("`cd` with no target is refused") };
21221
21288
  case "option-shaped":
21222
- return { refusal: deny(`cd target "${cdResult.text}" is option-shaped, and this gate does not model it`) };
21289
+ return {
21290
+ refusal: deny(`cd target "${cdResult.text}" is option-shaped, and this gate does not model it`, false, true)
21291
+ };
21223
21292
  case "opaque":
21224
21293
  case "unresolved":
21225
21294
  return { refusal: deny(`cd target "${cdResult.text}" is not inside the permitted root`, true) };
@@ -21256,29 +21325,30 @@ function checkBashCommand(args) {
21256
21325
  if (command.trim() === "")
21257
21326
  return deny(`"command" must not be empty`);
21258
21327
  const lexed = lexBashCommand(command);
21259
- if (lexed.kind === "refused") {
21260
- return deny(`command contains ${lexed.construct}, which cannot be analysed and is therefore refused -- ` + "rewrite it without that construct, or use a structured tool", false, true);
21261
- }
21262
- for (const segment of lexed.segments) {
21328
+ const segments = lexed.kind === "ok" ? lexed.segments : lexed.prefix;
21329
+ for (const segment of segments) {
21263
21330
  if (args.denyEntry !== undefined && matchesSegment(args.denyEntry, segment)) {
21264
21331
  return deny(`${tool} segment "${render(segment)}" is denied for this stage by rule ${ruleExpr(tool, args.denyEntry, segment)}`);
21265
21332
  }
21266
21333
  }
21267
- for (const segment of lexed.segments) {
21268
- if (args.grant.unconditional || matchesSegment(args.grant, segment))
21269
- continue;
21270
- const granted = args.grant.raw.filter((pattern) => pattern !== "*").join(", ");
21271
- const alternatives = granted === "" ? "no command forms are granted for this stage" : `granted forms: ${granted}`;
21272
- return deny(`${tool} is not granted "${render(segment)}" -- ${alternatives}`, false, true);
21273
- }
21274
21334
  let cwd = [args.initialPath];
21275
- for (const segment of lexed.segments) {
21335
+ for (const segment of segments) {
21276
21336
  const result = checkPayload(args, segment, cwd);
21277
21337
  if (result.refusal !== undefined)
21278
21338
  return result.refusal;
21279
21339
  cwd = nextWorkingDirectories(segment, cwd, result.cdTargets);
21280
21340
  }
21281
- for (const segment of lexed.segments) {
21341
+ if (lexed.kind === "refused") {
21342
+ return deny(`command contains ${lexed.construct}, which cannot be analysed and is therefore refused -- ` + "rewrite it without that construct, or use a structured tool", false, true);
21343
+ }
21344
+ for (const segment of segments) {
21345
+ if (args.grant.unconditional || matchesSegment(args.grant, segment))
21346
+ continue;
21347
+ const granted = args.grant.raw.filter((pattern) => pattern !== "*").join(", ");
21348
+ const alternatives = granted === "" ? "no command forms are granted for this stage" : `granted forms: ${granted}`;
21349
+ return deny(`${tool} is not granted "${render(segment)}" -- ${alternatives}`, false, true);
21350
+ }
21351
+ for (const segment of segments) {
21282
21352
  if (args.askEntry !== undefined && matchesSegment(args.askEntry, segment)) {
21283
21353
  return { kind: "ask", rule: ruleExpr(tool, args.askEntry, segment) };
21284
21354
  }
@@ -21299,15 +21369,16 @@ function protectedHit(args, candidate, cwd) {
21299
21369
  for (const directory of cwd) {
21300
21370
  const lexical = realOrRaw(resolve10(directory, candidate));
21301
21371
  if (isNaxConfigFile(args.root, lexical))
21302
- return candidate;
21372
+ return { kind: "config", hit: candidate };
21303
21373
  const resolved = args.resolvePath(candidate, directory);
21304
21374
  if (resolved === null)
21305
21375
  continue;
21306
21376
  const rel = relative5(args.root, resolved).split(sep4).join("/");
21307
21377
  if (rel.startsWith(".."))
21308
21378
  continue;
21309
- if (isNaxOwnedWritePath(rel))
21310
- return candidate;
21379
+ const kind = naxOwnedKind(rel);
21380
+ if (kind !== undefined)
21381
+ return { kind, hit: candidate };
21311
21382
  }
21312
21383
  return;
21313
21384
  }
@@ -21327,7 +21398,7 @@ function screenRawBashCommand(args) {
21327
21398
  continue;
21328
21399
  const hit = protectedHit(args, token.text, cwd);
21329
21400
  if (hit !== undefined) {
21330
- return deny2(`${tool} command names "${hit}", which nax owns and no tool may modify -- ` + "change it through nax rather than by writing its file");
21401
+ return deny2(naxOwnedBashRefusal(tool, hit.kind, hit.hit, "names"));
21331
21402
  }
21332
21403
  }
21333
21404
  for (const redirect of segment.redirects) {
@@ -21335,7 +21406,7 @@ function screenRawBashCommand(args) {
21335
21406
  continue;
21336
21407
  const hit = protectedHit(args, redirect.target, cwd);
21337
21408
  if (hit !== undefined) {
21338
- return deny2(`${tool} command redirects into "${hit}", which nax owns and no tool may modify -- ` + "change it through nax rather than by writing its file");
21409
+ return deny2(naxOwnedBashRefusal(tool, hit.kind, hit.hit, "redirects into"));
21339
21410
  }
21340
21411
  }
21341
21412
  const cdResult = cdTargetsFor(segment, cwd, args.resolvePath);
@@ -22593,6 +22664,47 @@ var init_provider_advertise = __esm(() => {
22593
22664
  init_provider_types();
22594
22665
  });
22595
22666
 
22667
+ // src/tools/read-continuation.ts
22668
+ function limitStopFooter(input) {
22669
+ const { nextOffset, totalIsFloor, endLine, totalLines } = input;
22670
+ const remaining = Math.max(0, totalLines - endLine);
22671
+ const countLabel = totalIsFloor ? `${remaining}+` : `${remaining}`;
22672
+ return `[${countLabel} more lines in file. Use offset=${nextOffset} to continue.]`;
22673
+ }
22674
+ function shouldAppendLimitStopFooter(hasLimit, endLine, totalLines) {
22675
+ return hasLimit && endLine < totalLines;
22676
+ }
22677
+ function capFooter(input) {
22678
+ const { firstLine: firstLine2, lastLine, totalLabel } = input;
22679
+ return `[Showing lines ${firstLine2}-${lastLine} of ${totalLabel}. Use offset=${lastLine + 1} to continue.]`;
22680
+ }
22681
+ function applyCapCut(input) {
22682
+ const { header, lines, firstLine: firstLine2, totalLabel, limitStopFooter: limitStopFooter2, unshapedBody, maxBytes, maxLines } = input;
22683
+ const candidate = limitStopFooter2 === "" ? unshapedBody : `${unshapedBody}
22684
+ ${limitStopFooter2}`;
22685
+ if (splitModelLines(candidate).length <= maxLines && Buffer.byteLength(candidate, "utf8") <= maxBytes) {
22686
+ return { content: candidate };
22687
+ }
22688
+ for (let k = Math.min(lines.length, maxLines - 2);k >= 1; k -= 1) {
22689
+ if (1 + k + 1 > maxLines)
22690
+ continue;
22691
+ const lastLine = firstLine2 + k - 1;
22692
+ const footer = capFooter({ firstLine: firstLine2, lastLine, totalLabel });
22693
+ const bodyJoined = lines.slice(0, k).join(`
22694
+ `);
22695
+ const cut = `${header}
22696
+ ${bodyJoined}
22697
+ ${footer}`;
22698
+ if (Buffer.byteLength(cut, "utf8") <= maxBytes) {
22699
+ return { content: cut };
22700
+ }
22701
+ }
22702
+ return { content: candidate };
22703
+ }
22704
+ var init_read_continuation = __esm(() => {
22705
+ init_truncate();
22706
+ });
22707
+
22596
22708
  // src/tools/read.ts
22597
22709
  function parsePositiveInt(value, field) {
22598
22710
  if (typeof value !== "number" || !Number.isInteger(value))
@@ -22612,11 +22724,12 @@ function countLines(prefix) {
22612
22724
  var UNSUPPORTED_RANGE_ALIASES, readTool;
22613
22725
  var init_read = __esm(() => {
22614
22726
  init_bounded_io();
22727
+ init_read_continuation();
22615
22728
  init_truncate();
22616
22729
  UNSUPPORTED_RANGE_ALIASES = ["start_line", "end_line", "start", "end", "line", "lineEnd", "size"];
22617
22730
  readTool = {
22618
22731
  name: "Read",
22619
- description: "Read a UTF-8 text file from the repository. Paths are relative to the repository root. " + "Optionally pass offset (1-based line number to start from) and/or limit (maximum number of " + "lines to return) to read a slice instead of the whole file.",
22732
+ description: "Read a UTF-8 text file from the repository. Paths are relative to the repository root. " + "Optionally pass offset (1-based line number to start from) and/or limit (maximum number of " + "lines to return) to read a slice instead of the whole file. " + "Use Read to examine files instead of cat, sed, head, tail or awk in Bash. " + "A read that stops before the end of the file ends with a line naming the offset to continue from. " + "For a large file, read the part you need with offset/limit; when you need the whole file, continue with offset until complete. " + `Output is capped at ${MODEL_MAX_LINES} lines or ${MODEL_MAX_BYTES} bytes.`,
22620
22733
  inputSchema: {
22621
22734
  type: "object",
22622
22735
  properties: {
@@ -22646,9 +22759,21 @@ var init_read = __esm(() => {
22646
22759
  const prefix = await readPrefix(target, readCeiling);
22647
22760
  const bounded2 = Buffer.byteLength(prefix, "utf8") > readCeiling;
22648
22761
  const lineCount = countLines(prefix);
22649
- const header2 = `[${bounded2 ? `${lineCount}+` : `${lineCount}`} lines]`;
22650
- return { content: prefix === "" ? header2 : `${header2}
22651
- ${prefix}` };
22762
+ const totalLabel2 = bounded2 ? `${lineCount}+` : `${lineCount}`;
22763
+ const header = `[${totalLabel2} lines]`;
22764
+ const unshaped = prefix === "" ? header : `${header}
22765
+ ${prefix}`;
22766
+ const result2 = applyCapCut({
22767
+ header,
22768
+ lines: splitModelLines(prefix),
22769
+ firstLine: 1,
22770
+ totalLabel: totalLabel2,
22771
+ limitStopFooter: "",
22772
+ unshapedBody: unshaped,
22773
+ maxBytes: ctx.maxBytes,
22774
+ maxLines: MODEL_MAX_LINES
22775
+ });
22776
+ return { content: result2.content };
22652
22777
  }
22653
22778
  let offset = 1;
22654
22779
  if (hasOffset) {
@@ -22666,25 +22791,38 @@ ${prefix}` };
22666
22791
  }
22667
22792
  const body = await readPrefix(target, ctx.maxFileBytes);
22668
22793
  const bounded = Buffer.byteLength(body, "utf8") > ctx.maxFileBytes;
22669
- const trailingNewline = body.endsWith(`
22670
- `);
22671
- const lines = trailingNewline ? body.slice(0, -1).split(`
22672
- `) : body.split(`
22673
- `);
22674
- const totalLines = lines.length;
22794
+ const totalLines = countLines(body);
22675
22795
  const totalLabel = bounded ? `${totalLines}+` : `${totalLines}`;
22676
22796
  if (offset > totalLines) {
22677
22797
  return {
22678
22798
  content: bounded ? `offset ${offset} is past the first ${totalLines} lines, which is all that could be read within the ${ctx.maxFileBytes}-byte ceiling` : `offset ${offset} is past the end of the file -- it has ${totalLines} lines`
22679
22799
  };
22680
22800
  }
22801
+ const lines = splitModelLines(body);
22681
22802
  const startIndex = offset - 1;
22682
22803
  const endLine = limit === undefined ? totalLines : Math.min(startIndex + limit, totalLines);
22683
22804
  const selected = lines.slice(startIndex, endLine).join(`
22684
22805
  `);
22685
- const header = `[lines ${offset}-${endLine} of ${totalLabel}]
22686
- `;
22687
- return { content: `${header}${selected}` };
22806
+ const headerLine = `[lines ${offset}-${endLine} of ${totalLabel}]`;
22807
+ const limitStop = shouldAppendLimitStopFooter(limit !== undefined, endLine, totalLines) ? limitStopFooter({
22808
+ nextOffset: endLine + 1,
22809
+ totalIsFloor: bounded,
22810
+ endLine,
22811
+ totalLines
22812
+ }) : "";
22813
+ const unshapedBody = `${headerLine}
22814
+ ${selected}`;
22815
+ const result = applyCapCut({
22816
+ header: headerLine,
22817
+ lines: lines.slice(startIndex, endLine),
22818
+ firstLine: offset,
22819
+ totalLabel,
22820
+ limitStopFooter: limitStop,
22821
+ unshapedBody,
22822
+ maxBytes: ctx.maxBytes,
22823
+ maxLines: MODEL_MAX_LINES
22824
+ });
22825
+ return { content: result.content };
22688
22826
  } catch (err) {
22689
22827
  return { content: err instanceof Error ? err.message : String(err), isError: true };
22690
22828
  }
@@ -24199,7 +24337,7 @@ var init_scratchpad = __esm(() => {
24199
24337
  init_truncate();
24200
24338
  scratchpadWriteTool = {
24201
24339
  name: "ScratchpadWrite",
24202
- description: "Write a throwaway file to your scratchpad at .nax/scratchpad/. Use it for notes to yourself, command output you want to re-read, or intermediate lists. It is never committed and is wiped when a run finishes (a failed run's scratchpad is retained for inspection until the next run starts and clears it). Paths are relative to the scratchpad and cannot reach the repository.",
24340
+ description: "Write a throwaway file to your scratchpad at .nax/scratchpad/. Use it for notes to yourself, command output you want to re-read, intermediate lists, or a probe script to run against the project's code. It is never committed and is wiped when a run finishes (a failed run's scratchpad is retained for inspection until the next run starts and clears it). Paths are relative to the scratchpad and cannot reach the repository.",
24203
24341
  inputSchema: {
24204
24342
  type: "object",
24205
24343
  properties: {
@@ -24441,33 +24579,97 @@ var init_spill = __esm(() => {
24441
24579
  // src/tools/tool-audit.ts
24442
24580
  import { mkdir as mkdir7, writeFile as writeFile5 } from "fs/promises";
24443
24581
  import { join as join15 } from "path";
24582
+ function registerToolAuditSink(runId, sink) {
24583
+ const sinks = openToolAuditSinks.get(runId);
24584
+ if (sinks === undefined) {
24585
+ openToolAuditSinks.set(runId, new Set([sink]));
24586
+ return;
24587
+ }
24588
+ sinks.add(sink);
24589
+ }
24590
+ function unregisterToolAuditSink(runId, sink) {
24591
+ const sinks = openToolAuditSinks.get(runId);
24592
+ if (sinks === undefined)
24593
+ return;
24594
+ sinks.delete(sink);
24595
+ if (sinks.size === 0)
24596
+ openToolAuditSinks.delete(runId);
24597
+ }
24598
+ async function flushOpenToolAuditSinks(runId) {
24599
+ const sinks = openToolAuditSinks.get(runId);
24600
+ if (sinks === undefined)
24601
+ return;
24602
+ openToolAuditSinks.delete(runId);
24603
+ await Promise.all([...sinks].map(async (sink) => {
24604
+ try {
24605
+ await sink.flushPartial();
24606
+ } catch (error48) {
24607
+ getSafeLogger()?.warn("tools", "tool-audit partial flush failed", { runId, error: errorMessage(error48) });
24608
+ }
24609
+ }));
24610
+ }
24444
24611
  function createNoOpToolAuditSink() {
24445
24612
  return { record() {}, async flush() {} };
24446
24613
  }
24614
+ function nextFileStamp() {
24615
+ const now = Date.now();
24616
+ lastFileStamp = now > lastFileStamp ? now : lastFileStamp + 1;
24617
+ return lastFileStamp;
24618
+ }
24447
24619
  function createToolAuditSink(opts) {
24448
24620
  const calls = [];
24449
- return {
24621
+ const runId = opts.header?.runId;
24622
+ let partialFlushed = false;
24623
+ const write = async (partial2) => {
24624
+ if (calls.length === 0)
24625
+ return;
24626
+ await mkdir7(opts.dir, { recursive: true });
24627
+ const body = JSON.stringify({
24628
+ schemaVersion: TOOL_AUDIT_SCHEMA_VERSION,
24629
+ ...opts.header ?? {},
24630
+ sessionName: opts.sessionName,
24631
+ ...partial2 ? { partial: true } : {},
24632
+ calls: redactRowStrings(calls)
24633
+ }, null, 2);
24634
+ const prefix = runId !== undefined ? `${runId}-` : "";
24635
+ await writeFile5(join15(opts.dir, `${prefix}${nextFileStamp()}-${opts.sessionName}.json`), body);
24636
+ };
24637
+ const sink = {
24450
24638
  record(entry) {
24639
+ if (partialFlushed) {
24640
+ getSafeLogger()?.warn("tools", "tool-audit row recorded after the partial flush was dropped", {
24641
+ runId,
24642
+ tool: entry.tool
24643
+ });
24644
+ return;
24645
+ }
24451
24646
  calls.push(entry);
24452
24647
  },
24648
+ async flushPartial() {
24649
+ if (partialFlushed)
24650
+ return;
24651
+ await write(true);
24652
+ partialFlushed = true;
24653
+ if (runId !== undefined)
24654
+ unregisterToolAuditSink(runId, sink);
24655
+ },
24453
24656
  async flush() {
24454
- if (calls.length === 0)
24657
+ if (partialFlushed)
24455
24658
  return;
24456
- await mkdir7(opts.dir, { recursive: true });
24457
- const body = JSON.stringify({
24458
- schemaVersion: TOOL_AUDIT_SCHEMA_VERSION,
24459
- ...opts.header ?? {},
24460
- sessionName: opts.sessionName,
24461
- calls: redactRowStrings(calls)
24462
- }, null, 2);
24463
- const prefix = opts.header?.runId !== undefined ? `${opts.header.runId}-` : "";
24464
- await writeFile5(join15(opts.dir, `${prefix}${Date.now()}-${opts.sessionName}.json`), body);
24659
+ if (runId !== undefined)
24660
+ unregisterToolAuditSink(runId, sink);
24661
+ await write(false);
24465
24662
  }
24466
24663
  };
24664
+ if (runId !== undefined)
24665
+ registerToolAuditSink(runId, sink);
24666
+ return sink;
24467
24667
  }
24468
- var TOOL_AUDIT_SCHEMA_VERSION = 1;
24668
+ var openToolAuditSinks, TOOL_AUDIT_SCHEMA_VERSION = 1, lastFileStamp = 0;
24469
24669
  var init_tool_audit = __esm(() => {
24670
+ init_logger2();
24470
24671
  init_permissions();
24672
+ openToolAuditSinks = new Map;
24471
24673
  });
24472
24674
 
24473
24675
  // src/tools/write.ts
@@ -24677,6 +24879,7 @@ function createCodingToolRuntime(opts) {
24677
24879
  tool: policyIdentity,
24678
24880
  stage: opts.pipelineStage ?? "unknown",
24679
24881
  rule: verdict.rule ?? verdict.reason,
24882
+ ...verdict.rule !== undefined ? { matchedRule: verdict.rule } : {},
24680
24883
  summary: ask.summary,
24681
24884
  ...ask.unshowable ? { unshowable: true } : {},
24682
24885
  ...typeof input[tool.scope.commandField ?? ""] === "string" ? { command: input[tool.scope.commandField] } : {},
@@ -25605,7 +25808,9 @@ var init_config_guards = __esm(() => {
25605
25808
  "review.gateLLMChecksOnMechanicalPass": "LLM review checks are sequenced by the story orchestrator; this key never had an effect after #1859",
25606
25809
  debate: "the multi-agent debate subsystem was removed; a config that enabled debate.stages.plan without setting plan.mode was resolving to the debate plan strategy and now plans with `single` \u2014 set plan.mode explicitly to `single` or `refine`",
25607
25810
  "plan.citationThreshold": "this key only fed the removed pipeline plan mode",
25608
- "plan.criticModel": "this key only fed the removed pipeline plan mode"
25811
+ "plan.criticModel": "this key only fed the removed pipeline plan mode",
25812
+ "quality.autofix.enforceTestWriterIsolation": "the mock-structure handoff path this key guarded was removed in #1084; the key has had no effect since",
25813
+ "prompts.overrides.single-session": "the single-session prompt role is not used by any run since fb3cfad3e; test-after stories use the tdd-simple role, so override tdd-simple instead"
25609
25814
  };
25610
25815
  });
25611
25816
 
@@ -26775,12 +26980,10 @@ var init_schemas_execution = __esm(() => {
26775
26980
  }).default({ format: "auto" }),
26776
26981
  autofix: exports_external.object({
26777
26982
  enabled: exports_external.boolean().default(true),
26778
- maxAttempts: exports_external.number().int().min(1).default(3),
26779
- enforceTestWriterIsolation: exports_external.boolean().default(true)
26983
+ maxAttempts: exports_external.number().int().min(1).default(3)
26780
26984
  }).default({
26781
26985
  enabled: true,
26782
- maxAttempts: 3,
26783
- enforceTestWriterIsolation: true
26986
+ maxAttempts: 3
26784
26987
  }),
26785
26988
  forceExit: exports_external.boolean().default(false),
26786
26989
  detectOpenHandles: exports_external.boolean().default(true),
@@ -26832,6 +27035,7 @@ var init_schemas_execution = __esm(() => {
26832
27035
  }).default({ testWriter: "fast", verifier: "fast" }),
26833
27036
  verifierTimeoutSeconds: exports_external.number().int().min(60).max(7200).default(1800),
26834
27037
  testWriterAllowedPaths: exports_external.array(exports_external.string()).optional(),
27038
+ testWriterCommitHooks: exports_external.enum(["skip", "run"]).optional(),
26835
27039
  rollbackOnFailure: exports_external.boolean().optional(),
26836
27040
  greenfieldDetection: exports_external.boolean().optional()
26837
27041
  });
@@ -26847,7 +27051,7 @@ var init_schemas_execution = __esm(() => {
26847
27051
  });
26848
27052
 
26849
27053
  // src/config/schemas-infra.ts
26850
- var PlanConfigSchema, AcceptanceFixConfigSchema, AcceptanceConfigSchema, LlmRoutingConfigSchema, AgentRoutingProfileSchema, AgentRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, PromptAuditConfigSchema, UsageAuditConfigSchema, FallbackTargetSchema, AgentFallbackConfigSchema, DEFAULT_AGENT_IDLE_WATCHDOG_CONFIG, AgentIdleWatchdogConfigSchema, DEFAULT_AGENT_SPIN_BREAKER_CONFIG, AgentSpinBreakerConfigSchema, AgentAcpConfigSchema, AgentNativeTransportRetryConfigSchema, AgentNativeConfigSchema, AgentTimeoutRetryConfigSchema, DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, ProjectProfileSchema, VALID_AGENT_TYPES, GenerateConfigSchema, CuratorThresholdsSchema, CuratorRetentionConfigSchema, CuratorConfigSchema;
27054
+ var PlanConfigSchema, AcceptanceFixConfigSchema, AcceptanceConfigSchema, LlmRoutingConfigSchema, AgentRoutingProfileSchema, AgentRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, PromptAuditConfigSchema, UsageAuditConfigSchema, FallbackTargetSchema, AgentFallbackConfigSchema, DEFAULT_AGENT_IDLE_WATCHDOG_CONFIG, AgentIdleWatchdogConfigSchema, DEFAULT_AGENT_SPIN_BREAKER_CONFIG, AgentSpinBreakerConfigSchema, AgentAcpConfigSchema, AgentNativeTransportRetryConfigSchema, AgentNativeConfigSchema, AgentTimeoutRetryConfigSchema, DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG, AgentConfigSchema, PrecheckConfigSchema, PROMPT_OVERRIDE_ROLES, PromptsConfigSchema, ProjectProfileSchema, VALID_AGENT_TYPES, GenerateConfigSchema, CuratorThresholdsSchema, CuratorRetentionConfigSchema, CuratorConfigSchema;
26851
27055
  var init_schemas_infra = __esm(() => {
26852
27056
  init_zod();
26853
27057
  init_agent_defaults();
@@ -27098,10 +27302,19 @@ var init_schemas_infra = __esm(() => {
27098
27302
  PrecheckConfigSchema = exports_external.object({
27099
27303
  storySizeGate: StorySizeGateConfigSchema
27100
27304
  });
27305
+ PROMPT_OVERRIDE_ROLES = ["no-test", "test-writer", "implementer", "verifier", "tdd-simple"];
27101
27306
  PromptsConfigSchema = exports_external.object({
27102
- overrides: exports_external.record(exports_external.string().refine((key) => ["no-test", "test-writer", "implementer", "verifier", "single-session", "tdd-simple"].includes(key), {
27103
- message: "Role must be one of: no-test, test-writer, implementer, verifier, single-session, tdd-simple"
27104
- }), exports_external.string().min(1, "Override path must be non-empty")).optional(),
27307
+ overrides: exports_external.record(exports_external.string(), exports_external.string().min(1, "Override path must be non-empty")).superRefine((overrides, ctx) => {
27308
+ for (const key of Object.keys(overrides)) {
27309
+ if (!PROMPT_OVERRIDE_ROLES.includes(key)) {
27310
+ ctx.addIssue({
27311
+ code: "custom",
27312
+ message: `Role must be one of: ${PROMPT_OVERRIDE_ROLES.join(", ")}`,
27313
+ path: [key]
27314
+ });
27315
+ }
27316
+ }
27317
+ }).optional(),
27105
27318
  behavioralGuardrails: exports_external.enum(["off", "lite", "strict"]).default("lite")
27106
27319
  });
27107
27320
  ProjectProfileSchema = exports_external.object({
@@ -27477,7 +27690,7 @@ var init_schemas3 = __esm(() => {
27477
27690
  agents: { enabled: true, strategy: "off", profiles: [] }
27478
27691
  }),
27479
27692
  execution: ExecutionConfigSchema.default({
27480
- maxIterations: 10,
27693
+ maxIterations: 20,
27481
27694
  iterationDelayMs: 2000,
27482
27695
  costLimit: 30,
27483
27696
  sessionTimeoutSeconds: 3600,
@@ -27522,8 +27735,7 @@ var init_schemas3 = __esm(() => {
27522
27735
  },
27523
27736
  autofix: {
27524
27737
  enabled: true,
27525
- maxAttempts: 3,
27526
- enforceTestWriterIsolation: true
27738
+ maxAttempts: 3
27527
27739
  },
27528
27740
  forceExit: false,
27529
27741
  detectOpenHandles: true,
@@ -40501,23 +40713,6 @@ function lintDiagnosticToFinding(d, workdir, tool) {
40501
40713
  var init_lint = __esm(() => {
40502
40714
  init_path_utils();
40503
40715
  });
40504
- // src/findings/adapters/semantic-review.ts
40505
- function reviewFindingToFinding(f) {
40506
- return {
40507
- source: "semantic-review",
40508
- severity: f.severity,
40509
- category: f.category ?? "",
40510
- rule: f.ruleId,
40511
- file: f.file,
40512
- line: f.line,
40513
- column: f.column,
40514
- endLine: f.endLine,
40515
- endColumn: f.endColumn,
40516
- message: f.message,
40517
- fixTarget: "source"
40518
- };
40519
- }
40520
-
40521
40716
  // src/findings/adapters/test-failure.ts
40522
40717
  function testFailureToFinding(failure) {
40523
40718
  const frames = (failure.stackTrace ?? []).slice(0, MAX_FRAMES_IN_MESSAGE);
@@ -41362,7 +41557,7 @@ function buildBehavioralGuardrailsSection(role, level, _variant, _isolation) {
41362
41557
  if (role === "test-writer") {
41363
41558
  return buildTestWriterGuardrails(level);
41364
41559
  }
41365
- if (role === "single-session" || role === "tdd-simple" || role === "batch") {
41560
+ if (role === "tdd-simple" || role === "batch") {
41366
41561
  return buildCombinedGuardrails(level);
41367
41562
  }
41368
41563
  return buildImplementerGuardrails(level);
@@ -41486,7 +41681,7 @@ ${body}`;
41486
41681
  }
41487
41682
  var HERMETIC_ROLES, LANGUAGE_GUIDANCE;
41488
41683
  var init_hermetic = __esm(() => {
41489
- HERMETIC_ROLES = new Set(["test-writer", "implementer", "tdd-simple", "batch", "single-session"]);
41684
+ HERMETIC_ROLES = new Set(["test-writer", "implementer", "tdd-simple", "batch"]);
41490
41685
  LANGUAGE_GUIDANCE = {
41491
41686
  go: "Define interfaces for external dependencies. Use constructor injection. Test with interface mocks \u2014 no real I/O in tests.",
41492
41687
  rust: "Use trait objects or generics for external deps. Mock with the mockall crate. Use #[cfg(test)] modules.",
@@ -41682,11 +41877,6 @@ isolation scope: Implement source code in src/ to make tests pass. Do not modify
41682
41877
  return `${header}
41683
41878
 
41684
41879
  isolation scope: Read-only TDD integrity inspection. Review story-scoped test results and test-file modifications. Do NOT apply source or test fixes. You MAY write only the verdict file (.nax-verifier-verdict.json).${footer}`;
41685
- }
41686
- if (role === "single-session") {
41687
- return `${header}
41688
-
41689
- isolation scope: Create test files in test/ directory, then implement source code in src/ to make tests pass. Both directories are in scope for this session.${footer}`;
41690
41880
  }
41691
41881
  return `${header}
41692
41882
 
@@ -41727,7 +41917,11 @@ freely. Every other path under \`.nax/\` stays off limits.
41727
41917
  - A test under \`.nax/\` is NOT a reason to skip writing source-tree tests. \`.nax/\` is generated
41728
41918
  scaffolding, not real coverage of the package's code.
41729
41919
  - A source-tree test is NOT a reason to remove a test under \`.nax/\`. The two serve different
41730
- purposes and must coexist.`;
41920
+ purposes and must coexist.
41921
+
41922
+ nax updates \`.nax/features/<feature>/prd.json\` itself during a run, so it shows as modified in
41923
+ \`git status\` \u2014 do not diff or revert it: this story's criteria are already in this prompt, and if you
41924
+ need its contents, read it with the \`Read\` tool; shell commands that name it may be refused.`;
41731
41925
  }
41732
41926
 
41733
41927
  // src/prompts/sections/out-of-scope.ts
@@ -41851,6 +42045,7 @@ Workflow:
41851
42045
 
41852
42046
  Rules:
41853
42047
  - Stubs are NOT implementations. The implementer in the next session writes real logic.
42048
+ - Do not commit. When your session ends, nax commits the files you changed as the RED state.
41854
42049
  - Each test name describes ONE behavior. Use AC IDs in test names when available (e.g. \`it('AC4: throws Division by zero when b === 0')\`).
41855
42050
  - Assert on observable outputs.
41856
42051
  - ${frameworkHint}
@@ -41867,10 +42062,12 @@ Workflow:
41867
42062
  2. Break the work into small tasks before writing: treat each AC as one task and note the test name(s) you will write (success + boundary) and which file they belong in. This per-AC list is your checklist.
41868
42063
  3. Create test files in the location the project uses for tests (project context names it).
41869
42064
  4. For each AC: write at least one test for the success path AND at least one for a boundary/failure path (zero, empty, negative, missing, throws). ACs worded as "throws X" require a test asserting the throw.
41870
- 5. Run the new test files. Confirm every test fails with an ASSERTION failure \u2014 NOT an import error, compile error, or runtime crash before assertion. A test that errors before reaching its assertion does not prove the behavior is missing.
42065
+ 5. Run the new test files. Confirm every test fails with an ASSERTION failure, not an import error or a runtime crash before the assertion. A test that errors before reaching its assertion does not prove the behavior is missing.
41871
42066
 
41872
42067
  Rules:
41873
42068
  - Do NOT create or modify any source files. Read source for types/interfaces only.
42069
+ - A type-check error that exists only because the implementer has not yet added a field, parameter or export the acceptance criteria require is the expected RED state. Do not work around it with type casts, type-checker suppression comments, allow-list tags or throwaway type-probe scripts; type each test as the finished code will be.
42070
+ - Do not commit. When your session ends, nax commits the files you changed as the RED state.
41874
42071
  - Each test name describes ONE behavior; each test asserts ONE behavior. When the AC has a number or ID, prefix the test name (e.g. \`it('AC4: throws Division by zero when b === 0')\`).
41875
42072
  - Assert on observable outputs (return values, thrown errors, file contents, log output, boundary state). Do not assert on private helpers, internal call counts, or implementation-level mocks unless the AC requires it.
41876
42073
  - ${frameworkHint}
@@ -41891,25 +42088,6 @@ Instructions:
41891
42088
  - Do NOT perform semantic acceptance review; semantic/adversarial review stages own acceptance criteria and broad code-quality findings
41892
42089
  - Write a detailed verdict with reasoning
41893
42090
  - Goal: verify story-scoped tests pass and test integrity was preserved`;
41894
- }
41895
- if (role === "single-session") {
41896
- return `# Role: Single-Session
41897
-
41898
- Your task: write tests AND implement the feature in one session.
41899
-
41900
- Workflow:
41901
- 1. Read the acceptance criteria. For each AC, plan one success-path test and one boundary/failure test.
41902
- 2. Create test files in the location the project uses for tests. Cover every AC.
41903
- 3. Run the tests to confirm they fail with ASSERTION failures \u2014 NOT import errors or compile errors. A test that errors before reaching its assertion does not prove the behavior is missing.
41904
- 4. Implement source code in the package's source location to make the tests pass.
41905
- 5. After each meaningful change, re-run only the scoped test files \u2014 never the full suite.
41906
- 6. When all scoped tests pass, stage and commit ALL changed files: \`${gitCommitInstruction(commitMsg)}\`.
41907
-
41908
- Rules:
41909
- - Each test name describes ONE behavior; use AC IDs when available.
41910
- - Assert on observable outputs.
41911
- - ${frameworkHint}
41912
- - Goal: every AC has at least one passing test; all changes committed.`;
41913
42091
  }
41914
42092
  if (role === "batch") {
41915
42093
  const verifyCmdLine = testCmd ? ` - Re-run only the scoped test files after each meaningful change: ${testCmd}` : " - Re-run only the scoped test files after each meaningful change";
@@ -41969,6 +42147,11 @@ or is read by another step. A failed run's scratchpad is retained for inspection
41969
42147
  before failing outlive that run, and are cleared at the next run's start. Treat every file as disposable,
41970
42148
  and overwrite freely.
41971
42149
 
42150
+ To try a snippet that imports project code or dependencies, write it under \`${dir}\` with
42151
+ \`ScratchpadWrite\` and run it from there: relative imports resolve from \`${dir}\`, so prefer the
42152
+ project's package names or path aliases where it has them. A script written outside the repository,
42153
+ such as in \`/tmp\`, cannot resolve the project's modules, and the file tools cannot write there.
42154
+
41972
42155
  \`${dir}\` is the one directory under \`.nax/\` you may write to. Every other path under \`.nax/\`
41973
42156
  must still never be moved, renamed, or deleted.`;
41974
42157
  }
@@ -42199,7 +42382,7 @@ An adversarial reviewer will audit your tests after implementation and BLOCK the
42199
42382
  }
42200
42383
  var AUTHORING_ROLES;
42201
42384
  var init_test_quality = __esm(() => {
42202
- AUTHORING_ROLES = new Set(["test-writer", "single-session", "tdd-simple", "batch"]);
42385
+ AUTHORING_ROLES = new Set(["test-writer", "tdd-simple", "batch"]);
42203
42386
  });
42204
42387
 
42205
42388
  // src/prompts/sections/verdict.ts
@@ -42399,7 +42582,7 @@ ACCEPTANCE TEST FILE: ${p.acceptanceTestPath}
42399
42582
 
42400
42583
  SOURCE FILES (auto-detected from imports, up to ${p.maxFileLines} lines each):
42401
42584
  ${p.sourceFilesSection}
42402
- ${p.verdictSection}
42585
+
42403
42586
  Respond with ONLY a JSON object in this exact format (no markdown, no extra text):
42404
42587
  ${responseSchema}`;
42405
42588
  }
@@ -42478,16 +42661,10 @@ ${f.content}
42478
42661
  \`\`\``).join(`
42479
42662
 
42480
42663
  `) : "(No source files could be resolved from imports)";
42481
- const verdictSection = p.semanticVerdicts && p.semanticVerdicts.length > 0 ? `
42482
- SEMANTIC VERDICTS:
42483
- ${p.semanticVerdicts.map((v) => `- ${v.storyId}: ${v.passed ? "likely test bug (semantic review confirmed AC implementation)" : "unconfirmed"}`).join(`
42484
- `)}
42485
- ` : "";
42486
42664
  return this.buildDiagnosisPromptTemplate({
42487
42665
  truncatedOutput,
42488
42666
  acceptanceTestPath: p.acceptanceTestPath ?? "(path unavailable \u2014 inspect test output for file references)",
42489
42667
  sourceFilesSection,
42490
- verdictSection,
42491
42668
  maxFileLines: MAX_FILE_LINES
42492
42669
  });
42493
42670
  }
@@ -53204,8 +53381,8 @@ var init_version = __esm(() => {
53204
53381
  NAX_AI_VERSION = CATALOG_VERSION;
53205
53382
  NAX_COMMIT = (() => {
53206
53383
  try {
53207
- if (/^[0-9a-f]{6,10}$/.test("3e4ec026"))
53208
- return "3e4ec026";
53384
+ if (/^[0-9a-f]{6,10}$/.test("db5bc9ae"))
53385
+ return "db5bc9ae";
53209
53386
  } catch {}
53210
53387
  try {
53211
53388
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -53764,7 +53941,7 @@ function attachCostSubscriber(bus, aggregator, runId, projectKey) {
53764
53941
  offCompleted();
53765
53942
  };
53766
53943
  }
53767
- var _costSubscriberDeps, COST_ROW_SCHEMA_VERSION = 6;
53944
+ var _costSubscriberDeps, COST_ROW_SCHEMA_VERSION = 7;
53768
53945
  var init_cost2 = __esm(() => {
53769
53946
  init_agents();
53770
53947
  init_version();
@@ -53886,8 +54063,33 @@ function toUsageEntry(event, runId) {
53886
54063
  costUsd: event.costUsd
53887
54064
  };
53888
54065
  }
53889
- function attachUsageAuditSubscriber(bus, auditor, runId) {
53890
- return bus.onAgentStream((event) => {
54066
+ function toOneShotEntry(event, runId) {
54067
+ if (event.kind !== "complete")
54068
+ return null;
54069
+ const tu = event.tokenUsage;
54070
+ const wireExact = event.exactCostUsd;
54071
+ const costUsd = typeof wireExact === "number" && Number.isFinite(wireExact) ? wireExact : event.estimatedCostUsd;
54072
+ if (!tu && (costUsd ?? 0) === 0)
54073
+ return null;
54074
+ return {
54075
+ ts: event.timestamp,
54076
+ runId,
54077
+ scopeId: event.scopeId,
54078
+ streamCallId: event.callId ?? "one-shot",
54079
+ sessionName: event.sessionName,
54080
+ storyId: event.storyId,
54081
+ stage: event.stage,
54082
+ agentName: event.agentName,
54083
+ cadence: "one-shot",
54084
+ input: tu?.inputTokens,
54085
+ output: tu?.outputTokens,
54086
+ cacheRead: tu?.cacheReadInputTokens,
54087
+ cacheWrite: tu?.cacheCreationInputTokens,
54088
+ costUsd
54089
+ };
54090
+ }
54091
+ function attachUsageAuditSubscriber(bus, dispatchEvents, auditor, runId) {
54092
+ const offStream = bus.onAgentStream((event) => {
53891
54093
  switch (event.kind) {
53892
54094
  case "agent.usage_update":
53893
54095
  auditor.record(toUsageEntry(event, runId));
@@ -53896,6 +54098,15 @@ function attachUsageAuditSubscriber(bus, auditor, runId) {
53896
54098
  break;
53897
54099
  }
53898
54100
  });
54101
+ const offDispatch = dispatchEvents.onDispatch((event) => {
54102
+ const entry = toOneShotEntry(event, runId);
54103
+ if (entry)
54104
+ auditor.record(entry);
54105
+ });
54106
+ return () => {
54107
+ offStream();
54108
+ offDispatch();
54109
+ };
53899
54110
  }
53900
54111
 
53901
54112
  // src/runtime/middleware/index.ts
@@ -53907,15 +54118,255 @@ var init_middleware = __esm(() => {
53907
54118
  init_logging();
53908
54119
  });
53909
54120
 
54121
+ // src/runtime/usage-auditor.ts
54122
+ import { appendFileSync as appendFileSync2 } from "fs";
54123
+ import { mkdir as mkdir16 } from "fs/promises";
54124
+ import { join as join45 } from "path";
54125
+ function createNoOpUsageAuditor() {
54126
+ return {
54127
+ record() {},
54128
+ async flush() {}
54129
+ };
54130
+ }
54131
+ function deriveSessionRole(sessionName) {
54132
+ for (const role of ROLES_BY_DESCENDING_LENGTH) {
54133
+ if (sessionName === role || sessionName.endsWith(`-${role}`))
54134
+ return role;
54135
+ }
54136
+ return;
54137
+ }
54138
+
54139
+ class UsageAuditor {
54140
+ _queue = Promise.resolve();
54141
+ _dirCreated = false;
54142
+ _dir;
54143
+ _jsonlPath;
54144
+ constructor(runId, dir) {
54145
+ this._dir = dir;
54146
+ this._jsonlPath = join45(dir, `${runId}.jsonl`);
54147
+ }
54148
+ record(entry) {
54149
+ this._queue = this._queue.then(() => this._writeEntry(entry)).catch((err) => {
54150
+ const sysErr = err;
54151
+ getSafeLogger()?.warn("audit", "usage-audit write failed", {
54152
+ path: this._jsonlPath,
54153
+ error: errorMessage(err),
54154
+ code: sysErr?.code,
54155
+ errno: sysErr?.errno,
54156
+ syscall: sysErr?.syscall,
54157
+ ts: entry.ts,
54158
+ storyId: entry.storyId,
54159
+ sessionName: entry.sessionName,
54160
+ agentName: entry.agentName,
54161
+ stage: entry.stage,
54162
+ streamCallId: entry.streamCallId
54163
+ });
54164
+ });
54165
+ }
54166
+ async _writeEntry(entry) {
54167
+ if (!this._dirCreated) {
54168
+ await mkdir16(this._dir, { recursive: true });
54169
+ this._dirCreated = true;
54170
+ }
54171
+ const row = { ...entry, sessionRole: deriveSessionRole(entry.sessionName) };
54172
+ await _usageAuditorDeps.appendLine(this._jsonlPath, `${JSON.stringify(row)}
54173
+ `);
54174
+ }
54175
+ async flush() {
54176
+ await this._queue;
54177
+ }
54178
+ }
54179
+ var _usageAuditorDeps, ROLES_BY_DESCENDING_LENGTH;
54180
+ var init_usage_auditor = __esm(() => {
54181
+ init_logger2();
54182
+ init_session_role();
54183
+ _usageAuditorDeps = {
54184
+ appendLine: async (path6, data) => {
54185
+ appendFileSync2(path6, data, "utf8");
54186
+ }
54187
+ };
54188
+ ROLES_BY_DESCENDING_LENGTH = [...KNOWN_SESSION_ROLES].sort((a, b) => b.length - a.length);
54189
+ });
54190
+
54191
+ // src/runtime/in-flight-usage.ts
54192
+ function hasSpend(entry) {
54193
+ const { input, output, cacheRead, cacheWrite } = entry.tokens;
54194
+ return entry.costUsd > 0 || input + output + cacheRead + cacheWrite > 0;
54195
+ }
54196
+ function attachInFlightUsageTracker(streamBus, dispatchEvents) {
54197
+ const entries = new Map;
54198
+ const latestEndedBySession = new Map;
54199
+ let endedSeq = 0;
54200
+ const entryFor = (event) => {
54201
+ let entry = entries.get(event.callId);
54202
+ if (entry === undefined) {
54203
+ entry = {
54204
+ streamCallId: event.callId,
54205
+ agentName: event.agentName,
54206
+ model: "unknown",
54207
+ sessionName: event.sessionName,
54208
+ tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
54209
+ costUsd: 0,
54210
+ roundTrips: 0
54211
+ };
54212
+ entries.set(event.callId, entry);
54213
+ }
54214
+ entry.agentName = event.agentName;
54215
+ entry.sessionName = event.sessionName;
54216
+ if (event.storyId !== undefined)
54217
+ entry.storyId = event.storyId;
54218
+ if (event.stage !== undefined)
54219
+ entry.stage = event.stage;
54220
+ if (event.scopeId !== undefined)
54221
+ entry.scopeId = event.scopeId;
54222
+ return entry;
54223
+ };
54224
+ const onUsageUpdate = (event) => {
54225
+ if (event.cadence !== "round-trip")
54226
+ return;
54227
+ const entry = entryFor(event);
54228
+ entry.tokens.input += event.inputTokens ?? 0;
54229
+ entry.tokens.output += event.outputTokens ?? 0;
54230
+ entry.tokens.cacheRead += event.cacheRead ?? 0;
54231
+ entry.tokens.cacheWrite += event.cacheWrite ?? 0;
54232
+ entry.costUsd += event.costUsd ?? 0;
54233
+ if (event.roundTrip !== undefined)
54234
+ entry.roundTrips += 1;
54235
+ };
54236
+ const onCallStarted = (event) => {
54237
+ const entry = entryFor(event);
54238
+ entry.model = event.model === "" ? "unknown" : event.model;
54239
+ };
54240
+ const forget = (callId) => {
54241
+ entries.delete(callId);
54242
+ for (const [sessionName, remembered] of latestEndedBySession) {
54243
+ if (remembered === callId)
54244
+ latestEndedBySession.delete(sessionName);
54245
+ }
54246
+ };
54247
+ const onCallEnded = (event) => {
54248
+ latestEndedBySession.set(event.sessionName, event.callId);
54249
+ if (event.status === "success" || event.status === "timeout") {
54250
+ forget(event.callId);
54251
+ return;
54252
+ }
54253
+ const existing2 = entries.get(event.callId);
54254
+ if (existing2 === undefined)
54255
+ return;
54256
+ const entry = entryFor(event);
54257
+ entry.endedStatus = event.status;
54258
+ endedSeq += 1;
54259
+ entry.endedSeq = endedSeq;
54260
+ };
54261
+ const onDispatch = (event) => {
54262
+ if (event.kind !== "session-turn")
54263
+ return;
54264
+ const callId = latestEndedBySession.get(event.sessionName);
54265
+ if (callId === undefined)
54266
+ return;
54267
+ const entry = entries.get(callId);
54268
+ if (entry !== undefined && entry.endedStatus === "cancelled")
54269
+ forget(callId);
54270
+ };
54271
+ const matchesScope = (entry, event) => {
54272
+ if (entry.scopeId === undefined)
54273
+ return false;
54274
+ return entry.scopeId === event.scopeId || entry.scopeId === event.callId;
54275
+ };
54276
+ const onDispatchError = (event) => {
54277
+ const hasUsage = event.tokenUsage !== undefined;
54278
+ const costUsd = event.exactCostUsd ?? event.estimatedCostUsd ?? 0;
54279
+ if (!hasUsage && !(Number.isFinite(costUsd) && costUsd > 0))
54280
+ return;
54281
+ let target;
54282
+ for (const entry of entries.values()) {
54283
+ if (entry.endedStatus === undefined || !matchesScope(entry, event))
54284
+ continue;
54285
+ if (target === undefined || (entry.endedSeq ?? 0) > (target.endedSeq ?? 0))
54286
+ target = entry;
54287
+ }
54288
+ if (target !== undefined)
54289
+ forget(target.streamCallId);
54290
+ };
54291
+ const onStream = (event) => {
54292
+ if (event.kind === "agent.usage_update")
54293
+ onUsageUpdate(event);
54294
+ else if (event.kind === "agent.call_started")
54295
+ onCallStarted(event);
54296
+ else if (event.kind === "agent.call_ended")
54297
+ onCallEnded(event);
54298
+ };
54299
+ const residuals = () => {
54300
+ const out = [];
54301
+ for (const entry of entries.values()) {
54302
+ if (!hasSpend(entry))
54303
+ continue;
54304
+ out.push({
54305
+ streamCallId: entry.streamCallId,
54306
+ agentName: entry.agentName,
54307
+ model: entry.model,
54308
+ sessionName: entry.sessionName,
54309
+ ...entry.storyId !== undefined ? { storyId: entry.storyId } : {},
54310
+ ...entry.stage !== undefined ? { stage: entry.stage } : {},
54311
+ ...entry.scopeId !== undefined ? { scopeId: entry.scopeId } : {},
54312
+ tokens: { ...entry.tokens },
54313
+ costUsd: entry.costUsd,
54314
+ roundTrips: entry.roundTrips
54315
+ });
54316
+ }
54317
+ return out;
54318
+ };
54319
+ const offStream = streamBus.onAgentStream(onStream);
54320
+ const offDispatch = dispatchEvents.onDispatch(onDispatch);
54321
+ const offError = dispatchEvents.onDispatchError(onDispatchError);
54322
+ return {
54323
+ tracker: { residuals },
54324
+ off: () => {
54325
+ offStream();
54326
+ offDispatch();
54327
+ offError();
54328
+ }
54329
+ };
54330
+ }
54331
+ function toPartialCostEvent(residual, runId, projectKey) {
54332
+ const sessionRole = deriveSessionRole(residual.sessionName);
54333
+ return {
54334
+ ts: Date.now(),
54335
+ runId,
54336
+ ...projectKey !== undefined ? { projectKey } : {},
54337
+ schemaVersion: COST_ROW_SCHEMA_VERSION,
54338
+ partial: true,
54339
+ agentName: residual.agentName,
54340
+ model: residual.model,
54341
+ ...sessionRole !== undefined ? { sessionRole } : {},
54342
+ ...residual.stage !== undefined ? { stage: residual.stage } : {},
54343
+ ...residual.storyId !== undefined ? { storyId: residual.storyId } : {},
54344
+ ...residual.scopeId !== undefined ? { scopeId: residual.scopeId } : {},
54345
+ callId: residual.streamCallId,
54346
+ tokens: { ...residual.tokens },
54347
+ roundTrips: residual.roundTrips,
54348
+ roundTripUnit: "model-call",
54349
+ costUsd: residual.costUsd,
54350
+ estimatedCostUsd: residual.costUsd,
54351
+ exactCostUsd: residual.costUsd,
54352
+ confidence: "estimated",
54353
+ durationMs: 0
54354
+ };
54355
+ }
54356
+ var init_in_flight_usage = __esm(() => {
54357
+ init_middleware();
54358
+ init_usage_auditor();
54359
+ });
54360
+
53910
54361
  // src/runtime/packages.ts
53911
- import { isAbsolute as isAbsolute15, join as join45, relative as relative17 } from "path";
54362
+ import { isAbsolute as isAbsolute15, join as join46, relative as relative17 } from "path";
53912
54363
  function packageWorkdir(view) {
53913
54364
  const { packageDir, repoRoot } = view;
53914
54365
  if (!packageDir)
53915
54366
  return repoRoot;
53916
54367
  if (!repoRoot || isAbsolute15(packageDir))
53917
54368
  return packageDir;
53918
- return join45(repoRoot, packageDir);
54369
+ return join46(repoRoot, packageDir);
53919
54370
  }
53920
54371
  function createPackageView(config2, packageDir, repoRoot, hasOverride, overlay) {
53921
54372
  const memo = new Map;
@@ -54028,7 +54479,7 @@ function storyExecRoot(view) {
54028
54479
  const segments = packageDir.split("/");
54029
54480
  if (segments[0] !== ".nax-wt" || segments.length < 2)
54030
54481
  return repoRoot;
54031
- return join45(repoRoot, segments[0], segments[1]);
54482
+ return join46(repoRoot, segments[0], segments[1]);
54032
54483
  }
54033
54484
  var _packagesDeps;
54034
54485
  var init_packages = __esm(() => {
@@ -54161,9 +54612,9 @@ var init_paths2 = __esm(() => {
54161
54612
  });
54162
54613
 
54163
54614
  // src/runtime/prompt-auditor.ts
54164
- import { appendFileSync as appendFileSync2 } from "fs";
54165
- import { mkdir as mkdir16 } from "fs/promises";
54166
- import { join as join46 } from "path";
54615
+ import { appendFileSync as appendFileSync3 } from "fs";
54616
+ import { mkdir as mkdir17 } from "fs/promises";
54617
+ import { join as join47 } from "path";
54167
54618
  function createNoOpPromptAuditor() {
54168
54619
  return {
54169
54620
  record() {},
@@ -54243,8 +54694,8 @@ class PromptAuditor {
54243
54694
  _featureDir;
54244
54695
  _turnOrdinals = new Map;
54245
54696
  constructor(runId, flushDir, featureName) {
54246
- this._featureDir = join46(flushDir, featureName);
54247
- this._jsonlPath = join46(this._featureDir, `${runId}.jsonl`);
54697
+ this._featureDir = join47(flushDir, featureName);
54698
+ this._jsonlPath = join47(this._featureDir, `${runId}.jsonl`);
54248
54699
  }
54249
54700
  record(entry) {
54250
54701
  this._enqueue(entry.callType === "run" ? { ...entry, turn: this._nextTurn(entry) } : entry);
@@ -54282,7 +54733,7 @@ class PromptAuditor {
54282
54733
  async _writeEntry(entry) {
54283
54734
  if (!this._dirCreated) {
54284
54735
  try {
54285
- await mkdir16(this._featureDir, { recursive: true });
54736
+ await mkdir17(this._featureDir, { recursive: true });
54286
54737
  } catch (err) {
54287
54738
  throw tagAuditError(err, "jsonl");
54288
54739
  }
@@ -54299,7 +54750,7 @@ class PromptAuditor {
54299
54750
  return;
54300
54751
  const filename = deriveTxtFilename(entry);
54301
54752
  try {
54302
- await _promptAuditorDeps.write(join46(this._featureDir, filename), buildTxtContent(safeEntry));
54753
+ await _promptAuditorDeps.write(join47(this._featureDir, filename), buildTxtContent(safeEntry));
54303
54754
  } catch (err) {
54304
54755
  throw tagAuditError(err, "txt");
54305
54756
  }
@@ -54313,80 +54764,10 @@ var init_prompt_auditor = __esm(() => {
54313
54764
  init_logger2();
54314
54765
  _promptAuditorDeps = {
54315
54766
  write: (path7, data) => Bun.write(path7, data),
54316
- appendLine: async (path7, data) => {
54317
- appendFileSync2(path7, data, "utf8");
54318
- }
54319
- };
54320
- });
54321
-
54322
- // src/runtime/usage-auditor.ts
54323
- import { appendFileSync as appendFileSync3 } from "fs";
54324
- import { mkdir as mkdir17 } from "fs/promises";
54325
- import { join as join47 } from "path";
54326
- function createNoOpUsageAuditor() {
54327
- return {
54328
- record() {},
54329
- async flush() {}
54330
- };
54331
- }
54332
- function deriveSessionRole(sessionName) {
54333
- for (const role of ROLES_BY_DESCENDING_LENGTH) {
54334
- if (sessionName === role || sessionName.endsWith(`-${role}`))
54335
- return role;
54336
- }
54337
- return;
54338
- }
54339
-
54340
- class UsageAuditor {
54341
- _queue = Promise.resolve();
54342
- _dirCreated = false;
54343
- _dir;
54344
- _jsonlPath;
54345
- constructor(runId, dir) {
54346
- this._dir = dir;
54347
- this._jsonlPath = join47(dir, `${runId}.jsonl`);
54348
- }
54349
- record(entry) {
54350
- this._queue = this._queue.then(() => this._writeEntry(entry)).catch((err) => {
54351
- const sysErr = err;
54352
- getSafeLogger()?.warn("audit", "usage-audit write failed", {
54353
- path: this._jsonlPath,
54354
- error: errorMessage(err),
54355
- code: sysErr?.code,
54356
- errno: sysErr?.errno,
54357
- syscall: sysErr?.syscall,
54358
- ts: entry.ts,
54359
- storyId: entry.storyId,
54360
- sessionName: entry.sessionName,
54361
- agentName: entry.agentName,
54362
- stage: entry.stage,
54363
- streamCallId: entry.streamCallId
54364
- });
54365
- });
54366
- }
54367
- async _writeEntry(entry) {
54368
- if (!this._dirCreated) {
54369
- await mkdir17(this._dir, { recursive: true });
54370
- this._dirCreated = true;
54371
- }
54372
- const row = { ...entry, sessionRole: deriveSessionRole(entry.sessionName) };
54373
- await _usageAuditorDeps.appendLine(this._jsonlPath, `${JSON.stringify(row)}
54374
- `);
54375
- }
54376
- async flush() {
54377
- await this._queue;
54378
- }
54379
- }
54380
- var _usageAuditorDeps, ROLES_BY_DESCENDING_LENGTH;
54381
- var init_usage_auditor = __esm(() => {
54382
- init_logger2();
54383
- init_session_role();
54384
- _usageAuditorDeps = {
54385
54767
  appendLine: async (path7, data) => {
54386
54768
  appendFileSync3(path7, data, "utf8");
54387
54769
  }
54388
54770
  };
54389
- ROLES_BY_DESCENDING_LENGTH = [...KNOWN_SESSION_ROLES].sort((a, b) => b.length - a.length);
54390
54771
  });
54391
54772
 
54392
54773
  // src/agents/factory.ts
@@ -66183,6 +66564,7 @@ __export(exports_runtime, {
66183
66564
  _usageAuditorDeps: () => _usageAuditorDeps,
66184
66565
  attachAgentIdleWatchdog: () => attachAgentIdleWatchdog,
66185
66566
  attachAgentStreamLogging: () => attachAgentStreamLogging,
66567
+ attachInFlightUsageTracker: () => attachInFlightUsageTracker,
66186
66568
  attachUsageAuditSubscriber: () => attachUsageAuditSubscriber,
66187
66569
  claimProjectIdentity: () => claimProjectIdentity,
66188
66570
  createNoOpCostAggregator: () => createNoOpCostAggregator,
@@ -66205,6 +66587,7 @@ __export(exports_runtime, {
66205
66587
  spinTerminalNotice: () => spinTerminalNotice,
66206
66588
  storyExecRoot: () => storyExecRoot,
66207
66589
  storySpendUsd: () => storySpendUsd,
66590
+ toPartialCostEvent: () => toPartialCostEvent,
66208
66591
  totalSpendUsd: () => totalSpendUsd,
66209
66592
  writeProjectIdentity: () => writeProjectIdentity
66210
66593
  });
@@ -66296,9 +66679,10 @@ function createRuntime(config2, workdir, opts) {
66296
66679
  const offCost = attachCostSubscriber(dispatchEvents, costAggregator, runId, getProjectKey(config2, workdir));
66297
66680
  const offAudit = attachAuditSubscriber(dispatchEvents, promptAuditor, runId);
66298
66681
  const offReviewAudit = attachReviewAuditSubscriber(dispatchEvents, reviewAuditor, runId);
66299
- const offUsageAudit = attachUsageAuditSubscriber(agentStreamEvents, usageAuditor, runId);
66682
+ const offUsageAudit = attachUsageAuditSubscriber(agentStreamEvents, dispatchEvents, usageAuditor, runId);
66300
66683
  const offAgentStreamLogging = attachAgentStreamLogging(agentStreamEvents, runId);
66301
66684
  const offWatchdog = attachAgentIdleWatchdog(agentStreamEvents, watchdogControllerRegistry, config2);
66685
+ const { tracker: inFlightTracker, off: offInFlightUsage } = attachInFlightUsageTracker(agentStreamEvents, dispatchEvents);
66302
66686
  const packages = createPackageRegistry(configLoader, workdir);
66303
66687
  const logger = getLogger();
66304
66688
  const quarantineMemo = createQuarantineMemo();
@@ -66366,6 +66750,7 @@ function createRuntime(config2, workdir, opts) {
66366
66750
  offUsageAudit();
66367
66751
  offAgentStreamLogging();
66368
66752
  offWatchdog();
66753
+ offInFlightUsage();
66369
66754
  if (opts?.parentSignal && parentAbortHandler) {
66370
66755
  opts.parentSignal.removeEventListener("abort", parentAbortHandler);
66371
66756
  }
@@ -66375,6 +66760,10 @@ function createRuntime(config2, workdir, opts) {
66375
66760
  sessionManager.close();
66376
66761
  await mcpPool.close();
66377
66762
  await writeMcpRollup(outputDir, buildMcpRollup({ runId, events: mcpPool.events(), withheld: mcpWithheld })).catch((error48) => logger.warn("runtime", "mcp rollup write failed", { error: String(error48) }));
66763
+ for (const residual of inFlightTracker.residuals()) {
66764
+ costAggregator.record(toPartialCostEvent(residual, runId, projectKey));
66765
+ }
66766
+ await flushOpenToolAuditSinks(runId);
66378
66767
  const results = await Promise.allSettled([
66379
66768
  promptAuditor.flush(),
66380
66769
  usageAuditor.flush(),
@@ -66394,6 +66783,7 @@ var init_runtime2 = __esm(() => {
66394
66783
  init_agent_stream_events();
66395
66784
  init_cost_aggregator();
66396
66785
  init_dispatch_events();
66786
+ init_in_flight_usage();
66397
66787
  init_middleware();
66398
66788
  init_packages();
66399
66789
  init_paths2();
@@ -66402,6 +66792,7 @@ var init_runtime2 = __esm(() => {
66402
66792
  init_session_role();
66403
66793
  init_spin_breaker();
66404
66794
  init_usage_auditor();
66795
+ init_tools();
66405
66796
  init_factory();
66406
66797
  init_manager2();
66407
66798
  init_config();
@@ -66415,6 +66806,7 @@ var init_runtime2 = __esm(() => {
66415
66806
  init_agent_stream_events();
66416
66807
  init_cost_aggregator();
66417
66808
  init_dispatch_events();
66809
+ init_in_flight_usage();
66418
66810
  init_middleware();
66419
66811
  init_packages();
66420
66812
  init_paths2();
@@ -66826,7 +67218,6 @@ var init_feature_context_filter = __esm(() => {
66826
67218
  implementer: ["all", "implementer"],
66827
67219
  "test-writer": ["all", "test-writer"],
66828
67220
  verifier: ["all", "verifier"],
66829
- "single-session": ["all", "implementer", "test-writer"],
66830
67221
  "tdd-simple": ["all", "implementer", "test-writer"],
66831
67222
  "no-test": ["all", "implementer"],
66832
67223
  batch: ["all", "implementer", "test-writer"]
@@ -68073,8 +68464,7 @@ var init_acceptance_diagnose = __esm(() => {
68073
68464
  testOutput: input.testOutput,
68074
68465
  testFileContent: input.testFileContent,
68075
68466
  acceptanceTestPath: input.acceptanceTestPath,
68076
- sourceFiles: input.sourceFiles,
68077
- semanticVerdicts: input.semanticVerdicts
68467
+ sourceFiles: input.sourceFiles
68078
68468
  });
68079
68469
  return {
68080
68470
  role: { id: "role", content: "", overridable: false },
@@ -72703,8 +73093,8 @@ async function getChangedFiles(workdir, fromRef = "HEAD") {
72703
73093
  { stdout: output, stderr, exitCode },
72704
73094
  { stdout: statusOutput, stderr: statusStderr, exitCode: statusExitCode }
72705
73095
  ] = await Promise.all([
72706
- runGitBounded(["diff", "--name-only", fromRef], workdir),
72707
- runGitBounded(["status", "--porcelain"], workdir)
73096
+ runGitBounded(["diff", "--name-only", "-z", fromRef], workdir),
73097
+ runGitBounded(["status", "--porcelain", "-z"], workdir)
72708
73098
  ]);
72709
73099
  if (exitCode !== 0) {
72710
73100
  throw new NaxError(`git diff --name-only ${fromRef} failed (exit ${exitCode}): ${stderr.trim()}`, "GIT_DIFF_FAILED", {
@@ -72720,10 +73110,8 @@ async function getChangedFiles(workdir, fromRef = "HEAD") {
72720
73110
  exitCode: statusExitCode
72721
73111
  });
72722
73112
  }
72723
- const diffFiles = output.trim().split(`
72724
- `).filter(Boolean);
72725
- const untrackedFiles = statusOutput.split(`
72726
- `).filter((line) => line.startsWith("??")).map((line) => line.slice(2).trim()).filter(Boolean);
73113
+ const diffFiles = output.split("\x00").filter(Boolean);
73114
+ const untrackedFiles = statusOutput.split("\x00").filter((line) => line.startsWith("??")).map((line) => line.slice(3)).filter(Boolean);
72727
73115
  return [...new Set([...diffFiles, ...untrackedFiles])];
72728
73116
  }
72729
73117
  async function getAddedLinesPerFile(workdir, fromRef = "HEAD") {
@@ -76079,20 +76467,7 @@ var init_write_test = __esm(() => {
76079
76467
  stage: "run",
76080
76468
  session: { role: "test-writer", lifetime: "warm" },
76081
76469
  config: tddConfigSelector,
76082
- tools: [
76083
- "Read",
76084
- "Glob",
76085
- "Grep",
76086
- "Write",
76087
- "Edit",
76088
- "Delete",
76089
- "Git",
76090
- "RunCommand",
76091
- "GitCommit",
76092
- "Exec",
76093
- "Bash",
76094
- "RequestCapability"
76095
- ],
76470
+ tools: ["Read", "Glob", "Grep", "Write", "Edit", "Delete", "Git", "RunCommand", "Exec", "Bash", "RequestCapability"],
76096
76471
  model: (_input, ctx) => ctx.config.tdd?.sessionTiers?.testWriter,
76097
76472
  keepOpen: (_input, ctx) => shouldKeepSessionOpen(ctx.config, "test-writer"),
76098
76473
  build(input, _ctx) {
@@ -79097,7 +79472,7 @@ var init_config_descriptions = __esm(() => {
79097
79472
  "routing.llm.mode": "Routing mode: one-shot | per-story | hybrid",
79098
79473
  "routing.llm.timeoutMs": "Timeout for LLM routing call in milliseconds",
79099
79474
  execution: "Execution limits and timeouts",
79100
- "execution.maxIterations": "Max iterations per feature run (auto-calculated if not set)",
79475
+ "execution.maxIterations": "Max iterations per feature run \u2014 each story attempt, parallel batch and the final completion pass counts one",
79101
79476
  "execution.iterationDelayMs": "Delay between iterations in milliseconds",
79102
79477
  "execution.costLimit": "Max cost in USD before pausing execution (override per run with `nax run --max-cost`)",
79103
79478
  "execution.sessionTimeoutSeconds": "Timeout per agent coding session in seconds",
@@ -79176,6 +79551,7 @@ var init_config_descriptions = __esm(() => {
79176
79551
  "tdd.sessionTiers.verifier": "Model tier for verifier session",
79177
79552
  "tdd.verifierTimeoutSeconds": "Wall-clock budget for one verifier turn in seconds (default: 1800). Its own knob, not execution.sessionTimeoutSeconds",
79178
79553
  "tdd.testWriterAllowedPaths": "Glob patterns for files test-writer can modify",
79554
+ "tdd.testWriterCommitHooks": 'Git hooks on the RED commit nax makes after the test-writer phase: "skip" (default, --no-verify) | "run"',
79179
79555
  "tdd.rollbackOnFailure": "Rollback git changes when TDD fails",
79180
79556
  "tdd.greenfieldDetection": "Force tdd-simple on projects with no test files",
79181
79557
  constitution: "Constitution settings (core rules and constraints)",
@@ -79253,7 +79629,6 @@ var init_config_descriptions = __esm(() => {
79253
79629
  "prompts.overrides.test-writer": 'Path to custom test-writer prompt (e.g., ".nax/prompts/test-writer.md")',
79254
79630
  "prompts.overrides.implementer": 'Path to custom implementer prompt (e.g., ".nax/prompts/implementer.md")',
79255
79631
  "prompts.overrides.verifier": 'Path to custom verifier prompt (e.g., ".nax/prompts/verifier.md")',
79256
- "prompts.overrides.single-session": 'Path to custom single-session prompt (e.g., ".nax/prompts/single-session.md")',
79257
79632
  agent: "Agent protocol configuration (ACP-003)",
79258
79633
  "agent.protocol": "Protocol for agent communication: 'acp' (default) | 'native' | 'hybrid'",
79259
79634
  "agent.native.catalogOverrides": "Native only. Explicit catalog entries for model ids newer than the bundled pi-ai snapshot, plus optional provider-wide baseUrl/headers.",
@@ -79556,7 +79931,7 @@ function displayConfigWithDescriptions(obj, path10, sources, indent = 0) {
79556
79931
  if (description) {
79557
79932
  console.log(`${indentStr}# prompts.overrides: ${description}`);
79558
79933
  }
79559
- const roles = ["test-writer", "implementer", "verifier", "single-session"];
79934
+ const roles = ["test-writer", "implementer", "verifier"];
79560
79935
  console.log(`${indentStr}overrides:`);
79561
79936
  for (const role of roles) {
79562
79937
  const roleDesc = FIELD_DESCRIPTIONS[`prompts.overrides.${role}`];
@@ -79601,7 +79976,7 @@ function displayConfigWithDescriptions(obj, path10, sources, indent = 0) {
79601
79976
  if (description) {
79602
79977
  console.log(`# prompts.overrides: ${description}`);
79603
79978
  }
79604
- const roles = ["test-writer", "implementer", "verifier", "single-session"];
79979
+ const roles = ["test-writer", "implementer", "verifier"];
79605
79980
  console.log("overrides:");
79606
79981
  for (const role of roles) {
79607
79982
  const roleDesc = FIELD_DESCRIPTIONS[`prompts.overrides.${role}`];
@@ -80630,75 +81005,6 @@ var init_hardening = __esm(() => {
80630
81005
  };
80631
81006
  });
80632
81007
 
80633
- // src/acceptance/semantic-verdict.ts
80634
- import path11 from "path";
80635
- async function persistSemanticVerdict(featureDir2, storyId, verdict) {
80636
- const dir = path11.join(featureDir2, "semantic-verdicts");
80637
- await _semanticVerdictDeps.mkdirp(dir);
80638
- const filePath = path11.join(dir, `${storyId}.json`);
80639
- await _semanticVerdictDeps.writeFile(filePath, JSON.stringify(verdict, null, 2));
80640
- }
80641
- function migrateSemanticVerdict(verdict) {
80642
- if (!verdict.findings?.length)
80643
- return verdict;
80644
- const first = verdict.findings[0];
80645
- if ("source" in first)
80646
- return verdict;
80647
- return {
80648
- ...verdict,
80649
- findings: verdict.findings.map((f) => reviewFindingToFinding(f))
80650
- };
80651
- }
80652
- async function loadSemanticVerdicts(featureDir2) {
80653
- const dir = path11.join(featureDir2, "semantic-verdicts");
80654
- let files;
80655
- try {
80656
- files = await _semanticVerdictDeps.readdir(dir);
80657
- } catch (err) {
80658
- if (err.code === "ENOENT")
80659
- return [];
80660
- throw err;
80661
- }
80662
- const results = [];
80663
- for (const file2 of files) {
80664
- if (!file2.endsWith(".json"))
80665
- continue;
80666
- const filePath = path11.join(dir, file2);
80667
- const content = await _semanticVerdictDeps.readFile(filePath);
80668
- try {
80669
- const parsed = JSON.parse(content);
80670
- results.push(migrateSemanticVerdict(parsed));
80671
- } catch {
80672
- _semanticVerdictDeps.logDebug(`Skipping invalid JSON in semantic-verdicts/${file2}`);
80673
- }
80674
- }
80675
- return results;
80676
- }
80677
- var _semanticVerdictDeps;
80678
- var init_semantic_verdict = __esm(() => {
80679
- init_findings();
80680
- init_logger2();
80681
- _semanticVerdictDeps = {
80682
- mkdirp: async (dir) => {
80683
- const { mkdir: mkdir21 } = await import("fs/promises");
80684
- await mkdir21(dir, { recursive: true });
80685
- },
80686
- writeFile: async (filePath, content) => {
80687
- await Bun.write(filePath, content);
80688
- },
80689
- readdir: async (dir) => {
80690
- const { readdir: readdir7 } = await import("fs/promises");
80691
- return readdir7(dir);
80692
- },
80693
- readFile: async (filePath) => {
80694
- return Bun.file(filePath).text();
80695
- },
80696
- logDebug: (msg) => {
80697
- getLogger()?.debug("semantic-verdict", msg);
80698
- }
80699
- };
80700
- });
80701
-
80702
81008
  // src/acceptance/index.ts
80703
81009
  var exports_acceptance = {};
80704
81010
  __export(exports_acceptance, {
@@ -80711,11 +81017,9 @@ __export(exports_acceptance, {
80711
81017
  groupStoriesByPackage: () => groupStoriesByPackage,
80712
81018
  isStubTestContent: () => isStubTestContent,
80713
81019
  loadAcceptanceTestContent: () => loadAcceptanceTestContent,
80714
- loadSemanticVerdicts: () => loadSemanticVerdicts,
80715
81020
  loadSourceFilesForDiagnosis: () => loadSourceFilesForDiagnosis,
80716
81021
  parseAcceptanceCriteria: () => parseAcceptanceCriteria,
80717
81022
  parseRefinementResponse: () => parseRefinementResponse,
80718
- persistSemanticVerdict: () => persistSemanticVerdict,
80719
81023
  refinementWouldFallback: () => refinementWouldFallback,
80720
81024
  resolveAcceptanceFeatureTestPath: () => resolveAcceptanceFeatureTestPath,
80721
81025
  resolveSuggestedPackageFeatureTestPath: () => resolveSuggestedPackageFeatureTestPath,
@@ -80729,7 +81033,6 @@ var init_acceptance2 = __esm(() => {
80729
81033
  init_generator();
80730
81034
  init_hardening();
80731
81035
  init_refinement();
80732
- init_semantic_verdict();
80733
81036
  init_test_path();
80734
81037
  });
80735
81038
 
@@ -81725,11 +82028,11 @@ var init_generate = __esm(() => {
81725
82028
  // src/cli/init-context.ts
81726
82029
  import { mkdir as mkdir21, readdir as readdir7 } from "fs/promises";
81727
82030
  import { basename as basename11, join as join74, relative as relative23, sep as sep12 } from "path";
81728
- async function bunFileExists(path12) {
81729
- return Bun.file(path12).exists();
82031
+ async function bunFileExists(path11) {
82032
+ return Bun.file(path11).exists();
81730
82033
  }
81731
- async function mkdirp(path12) {
81732
- await mkdir21(path12, { recursive: true });
82034
+ async function mkdirp(path11) {
82035
+ await mkdir21(path11, { recursive: true });
81733
82036
  }
81734
82037
  async function findFiles(dir, maxFiles = 200) {
81735
82038
  const files = [];
@@ -81792,8 +82095,8 @@ async function detectEntryPoints(projectRoot) {
81792
82095
  const candidates = ["src/index.ts", "src/main.ts", "main.go", "src/lib.rs"];
81793
82096
  const found = [];
81794
82097
  for (const candidate of candidates) {
81795
- const path12 = join74(projectRoot, candidate);
81796
- if (await bunFileExists(path12)) {
82098
+ const path11 = join74(projectRoot, candidate);
82099
+ if (await bunFileExists(path11)) {
81797
82100
  found.push(candidate);
81798
82101
  }
81799
82102
  }
@@ -81803,8 +82106,8 @@ async function detectConfigFiles(projectRoot) {
81803
82106
  const candidates = ["tsconfig.json", "biome.json", "turbo.json", ".env.example"];
81804
82107
  const found = [];
81805
82108
  for (const candidate of candidates) {
81806
- const path12 = join74(projectRoot, candidate);
81807
- if (await bunFileExists(path12)) {
82109
+ const path11 = join74(projectRoot, candidate);
82110
+ if (await bunFileExists(path11)) {
81808
82111
  found.push(candidate);
81809
82112
  }
81810
82113
  }
@@ -82633,9 +82936,9 @@ var init_scanner = __esm(() => {
82633
82936
  _scannerDeps = {
82634
82937
  discoverWorkspacePackages: (workdir) => discoverWorkspacePackages2(workdir),
82635
82938
  detectLanguage: (pkgDir) => detectLanguage(pkgDir),
82636
- readPackageJson: async (path12) => {
82939
+ readPackageJson: async (path11) => {
82637
82940
  try {
82638
- const file2 = Bun.file(path12);
82941
+ const file2 = Bun.file(path11);
82639
82942
  if (!await file2.exists())
82640
82943
  return null;
82641
82944
  return await file2.json();
@@ -82656,6 +82959,7 @@ var init_analyze = __esm(() => {
82656
82959
  function createHumanAskLink(opts) {
82657
82960
  let queue = Promise.resolve();
82658
82961
  let activeId;
82962
+ const canRemember = (req) => opts.onRemember !== undefined && req.command !== undefined;
82659
82963
  const deny3 = (decidedBy) => ({
82660
82964
  decision: "deny",
82661
82965
  decidedBy
@@ -82793,7 +83097,7 @@ function createHumanAskLink(opts) {
82793
83097
  `stage: ${req.stage}`
82794
83098
  ].join(`
82795
83099
  `),
82796
- options: OPTIONS,
83100
+ options: canRemember(req) ? [ALLOW_ONCE, ALLOW_REMEMBER, DENY] : [ALLOW_ONCE, DENY],
82797
83101
  timeout: opts.timeoutMs,
82798
83102
  fallback: "abort",
82799
83103
  createdAt: Date.now(),
@@ -82811,7 +83115,7 @@ function createHumanAskLink(opts) {
82811
83115
  if (!PERMITS.has(action)) {
82812
83116
  outcome = deny3("human");
82813
83117
  } else {
82814
- if (action === "allow-remember" && opts.onRemember) {
83118
+ if (action === "allow-remember" && canRemember(req) && opts.onRemember) {
82815
83119
  try {
82816
83120
  await opts.onRemember(req);
82817
83121
  } catch (err) {
@@ -82937,7 +83241,7 @@ function createHumanAskLink(opts) {
82937
83241
  async function cancelPendingAsk(link) {
82938
83242
  await link.cancel();
82939
83243
  }
82940
- var MAX_COMMAND_CHARS = 3500, maskedFooter = (count) => `${count} secret value(s) masked; the approved command contains them`, _askLinkDeps, OPTIONS, PERMITS;
83244
+ var MAX_COMMAND_CHARS = 3500, maskedFooter = (count) => `${count} secret value(s) masked; the approved command contains them`, _askLinkDeps, ALLOW_ONCE, ALLOW_REMEMBER, DENY, PERMITS;
82941
83245
  var init_ask_link = __esm(() => {
82942
83246
  init_permissions();
82943
83247
  init_logger2();
@@ -82946,11 +83250,9 @@ var init_ask_link = __esm(() => {
82946
83250
  clearTimeout: (id) => clearTimeout(id),
82947
83251
  ASK_KEEPALIVE_MS: 60000
82948
83252
  };
82949
- OPTIONS = [
82950
- { key: "allow", label: "Allow once" },
82951
- { key: "allow-remember", label: "Allow + remember" },
82952
- { key: "deny", label: "Deny" }
82953
- ];
83253
+ ALLOW_ONCE = { key: "allow", label: "Allow once" };
83254
+ ALLOW_REMEMBER = { key: "allow-remember", label: "Allow + remember" };
83255
+ DENY = { key: "deny", label: "Deny" };
82954
83256
  PERMITS = new Set(["allow", "allow-remember"]);
82955
83257
  });
82956
83258
 
@@ -83124,10 +83426,10 @@ async function buildDispatchAskWiring(opts, deps = _dispatchAskDeps) {
83124
83426
  stage: req.stage,
83125
83427
  command: req.command ?? "",
83126
83428
  root: req.root ?? opts.repoRoot,
83127
- origin: "escalate",
83128
- matchedRule: null,
83429
+ origin: req.matchedRule !== undefined ? "askRule" : "escalate",
83430
+ matchedRule: req.matchedRule ?? null,
83129
83431
  approvedAt: new Date().toISOString(),
83130
- approvedBy: "telegram",
83432
+ approvedBy: opts.config.interaction?.plugin ?? "unknown",
83131
83433
  naxCommit: NAX_COMMIT
83132
83434
  })
83133
83435
  });
@@ -83205,6 +83507,25 @@ async function collectEffectiveRunStageModes(opts, deps = _dispatchAskDeps) {
83205
83507
  })));
83206
83508
  return collectRunStageModes([opts.rootConfig, ...opts.extraConfigs ?? [], ...packageConfigs]);
83207
83509
  }
83510
+ async function buildApprovalsSeal(opts, deps = _dispatchAskDeps) {
83511
+ const stageModes = await collectEffectiveRunStageModes({ projectDir: opts.projectDir, rootConfig: opts.rootConfig, packageDirs: opts.packageDirs }, deps);
83512
+ const forgeCapable = isForgeCapable(stageModes, opts.rootConfig.execution?.sandbox?.enabled === true);
83513
+ if (!forgeCapable)
83514
+ return async () => {};
83515
+ const approvalsFile = approvalsPath(opts.outputDir);
83516
+ const runId = opts.runId;
83517
+ return async () => {
83518
+ try {
83519
+ await deps.prepareApprovalsStore({ approvalsFile, runId, forgeCapable: true });
83520
+ } catch (error48) {
83521
+ getSafeLogger()?.warn("permissions", "[approvals] could not update the store's taint marker", {
83522
+ approvalsFile,
83523
+ forgeCapable: true,
83524
+ error: error48
83525
+ });
83526
+ }
83527
+ };
83528
+ }
83208
83529
  async function buildRunDispatchAskWiring(opts, deps = _dispatchAskDeps) {
83209
83530
  const stageModes = await collectEffectiveRunStageModes({
83210
83531
  projectDir: opts.projectDir,
@@ -83218,6 +83539,7 @@ var DEFAULT_APPROVAL_TIMEOUT_MS = 600000, APPROVAL_AUDIT_DIR = "approval-audit",
83218
83539
  var init_dispatch_ask = __esm(() => {
83219
83540
  init_command_safety();
83220
83541
  init_config();
83542
+ init_logger2();
83221
83543
  init_permissions();
83222
83544
  init_version();
83223
83545
  init_ask_link();
@@ -86076,12 +86398,108 @@ var init_cleanup = __esm(() => {
86076
86398
  };
86077
86399
  });
86078
86400
 
86401
+ // src/tdd/red-commit.ts
86402
+ function redCommitMessage(storyId) {
86403
+ return `chore(${storyId}): auto-commit after test-writer session (RED)`;
86404
+ }
86405
+ async function commitRedState(opts, deps = _redCommitDeps) {
86406
+ if (opts.dryRun)
86407
+ return { status: "skipped", reason: "dry-run" };
86408
+ try {
86409
+ return await commitFromRoot(opts, deps);
86410
+ } catch (err) {
86411
+ return { status: "failed", reason: errorMessage(err) };
86412
+ }
86413
+ }
86414
+ async function commitFromRoot(opts, deps) {
86415
+ const top = await deps.git(["rev-parse", "--show-toplevel"], opts.workdir, RED_COMMIT_GIT_TIMEOUT_MS);
86416
+ if (top.exitCode !== 0)
86417
+ return { status: "failed", reason: `git rev-parse failed: ${top.stderr.trim()}` };
86418
+ const gitRoot = top.stdout.trim();
86419
+ if (isBlocked2(gitRoot, opts))
86420
+ return { status: "skipped", reason: "blocked-worktree" };
86421
+ const changed = await deps.getChangedFiles(opts.workdir, opts.beforeRef);
86422
+ const files = await expandUntrackedDirs(gitRoot, changed, deps);
86423
+ const { kept } = await deps.partitionNaxOwnedPaths(gitRoot, files);
86424
+ if (kept.length === 0)
86425
+ return NOTHING;
86426
+ const addOpts = { pathspecs: kept, timeoutMs: RED_COMMIT_GIT_TIMEOUT_MS };
86427
+ const added = await gitlinkSafeAdd(deps.git, gitRoot, addOpts);
86428
+ if (added.exitCode !== 0)
86429
+ return { status: "failed", reason: `git add failed: ${added.stderr.trim()}` };
86430
+ const staged = await deps.git(["diff", "--cached", "--quiet", "--", ...kept], gitRoot, RED_COMMIT_GIT_TIMEOUT_MS);
86431
+ if (staged.timedOut || staged.exitCode !== 0 && staged.exitCode !== 1) {
86432
+ return { status: "failed", reason: `git diff --cached failed: ${staged.stderr.trim()}` };
86433
+ }
86434
+ if (staged.exitCode === 0)
86435
+ return NOTHING;
86436
+ const noVerify = opts.hooks === "skip" ? ["--no-verify"] : [];
86437
+ const argv = ["commit", "--only", "-m", redCommitMessage(opts.storyId), ...noVerify, "--", ...kept];
86438
+ const committed = await deps.git(argv, gitRoot, RED_COMMIT_GIT_TIMEOUT_MS);
86439
+ if (committed.exitCode !== 0) {
86440
+ const detail = committed.stderr.trim() || `exit ${committed.exitCode}`;
86441
+ return { status: "failed", reason: `git commit failed: ${detail}` };
86442
+ }
86443
+ return { status: "committed", files: kept, hooksSkipped: opts.hooks === "skip" };
86444
+ }
86445
+ function isBlocked2(gitRoot, opts) {
86446
+ if (!opts.blockedWorktrees?.size)
86447
+ return false;
86448
+ const root = realOrRaw(gitRoot);
86449
+ const blocked = [...opts.blockedWorktrees].filter((tree) => realOrRaw(tree) === root);
86450
+ if (blocked.length === 0)
86451
+ return false;
86452
+ const message = "Refusing to commit the RED state \u2014 working tree may still hold an unreverted mutation";
86453
+ getSafeLogger()?.error("tdd", message, {
86454
+ storyId: opts.storyId,
86455
+ workdir: opts.workdir,
86456
+ blocked,
86457
+ hint: "Check the mutation-check log for the file and line, restore it, then commit manually."
86458
+ });
86459
+ return true;
86460
+ }
86461
+ async function expandUntrackedDirs(gitRoot, paths, deps) {
86462
+ const out = [];
86463
+ for (const path11 of paths) {
86464
+ if (!path11.endsWith("/")) {
86465
+ out.push(path11);
86466
+ continue;
86467
+ }
86468
+ const args = ["ls-files", "--others", "--exclude-standard", "-z", "--", path11];
86469
+ const listed = await deps.git(args, gitRoot, RED_COMMIT_GIT_TIMEOUT_MS);
86470
+ if (listed.exitCode !== 0) {
86471
+ throw new NaxError(`git ls-files failed: ${listed.stderr.trim()}`, "GIT_LS_FILES_FAILED", {
86472
+ stage: "tdd-red-commit",
86473
+ path: path11
86474
+ });
86475
+ }
86476
+ out.push(...listed.stdout.split("\x00").filter(Boolean));
86477
+ }
86478
+ return out;
86479
+ }
86480
+ var RED_COMMIT_GIT_TIMEOUT_MS = 30000, _redCommitDeps, NOTHING;
86481
+ var init_red_commit = __esm(() => {
86482
+ init_errors();
86483
+ init_logger2();
86484
+ init_tools();
86485
+ init_git();
86486
+ init_realpath();
86487
+ init_isolation2();
86488
+ _redCommitDeps = {
86489
+ git: (args, cwd, timeoutMs) => gitWithTimeout(args, cwd, timeoutMs),
86490
+ getChangedFiles,
86491
+ partitionNaxOwnedPaths
86492
+ };
86493
+ NOTHING = { status: "skipped", reason: "nothing-to-commit" };
86494
+ });
86495
+
86079
86496
  // src/tdd/index.ts
86080
86497
  var init_tdd = __esm(() => {
86081
86498
  init_operations();
86082
86499
  init_test_runners();
86083
86500
  init_cleanup();
86084
86501
  init_isolation2();
86502
+ init_red_commit();
86085
86503
  init_rollback();
86086
86504
  init_verdict();
86087
86505
  });
@@ -86805,6 +87223,9 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
86805
87223
  }
86806
87224
  }
86807
87225
  }
87226
+ if (isTddPhase && opName === "test-writer" && !inRectification && beforeRef && outcome === "passed") {
87227
+ await commitTestWriterRedState(ctx, beforeRef);
87228
+ }
86808
87229
  return output;
86809
87230
  } catch (err) {
86810
87231
  const noDispatch = toNoDispatchCheckResult(opName, err, Date.now() - phaseStartedAt);
@@ -86905,6 +87326,32 @@ function derivePhaseOutcome(output) {
86905
87326
  return "skipped";
86906
87327
  return "failed";
86907
87328
  }
87329
+ async function commitTestWriterRedState(ctx, beforeRef) {
87330
+ const config2 = ctx.config ?? ctx.runtime.configLoader.current();
87331
+ const result = await _storyOrchestratorDeps.commitRedState({
87332
+ workdir: ctx.packageDir,
87333
+ beforeRef,
87334
+ storyId: ctx.storyId ?? "story",
87335
+ hooks: config2.tdd?.testWriterCommitHooks ?? "skip",
87336
+ dryRun: ctx.runtime.dryRun,
87337
+ blockedWorktrees: ctx.runtime.dirtyWorktrees
87338
+ });
87339
+ logRedCommit(ctx.storyId, result);
87340
+ }
87341
+ function logRedCommit(storyId, result) {
87342
+ const logger = getSafeLogger();
87343
+ if (result.status === "committed") {
87344
+ logger?.info("tdd", "RED state committed", {
87345
+ storyId,
87346
+ files: result.files.length,
87347
+ hooksSkipped: result.hooksSkipped
87348
+ });
87349
+ } else if (result.status === "skipped") {
87350
+ logger?.debug("tdd", "RED state commit skipped", { storyId, reason: result.reason });
87351
+ } else {
87352
+ logger?.warn("tdd", "RED state not committed", { storyId, reason: result.reason });
87353
+ }
87354
+ }
86908
87355
  function withIncreasingFailuresBail(strategies, enabled, consecutiveIncreases) {
86909
87356
  if (!enabled)
86910
87357
  return strategies;
@@ -86951,6 +87398,7 @@ var init_run_phase = __esm(() => {
86951
87398
  callOp,
86952
87399
  runFixCycle,
86953
87400
  captureGitRef,
87401
+ commitRedState,
86954
87402
  cleanupVerdict,
86955
87403
  prepareSemanticReviewInput,
86956
87404
  prepareAdversarialReviewInput,
@@ -88334,6 +88782,11 @@ async function performTeardown(ctx) {
88334
88782
  if (ctx.pidRegistry) {
88335
88783
  await ctx.pidRegistry.killAll();
88336
88784
  }
88785
+ if (ctx.sealApprovals) {
88786
+ await ctx.sealApprovals().catch(() => {
88787
+ return;
88788
+ });
88789
+ }
88337
88790
  }
88338
88791
  function getSignalNumber(signal) {
88339
88792
  const signalMap = {
@@ -88490,7 +88943,7 @@ var init_crash_recovery = __esm(() => {
88490
88943
  });
88491
88944
 
88492
88945
  // src/execution/ensure-package-dirs.ts
88493
- import path12 from "path";
88946
+ import path11 from "path";
88494
88947
  async function ensureStoryPackageDirs(prd, workdir, deps = _ensurePackageDirsDeps) {
88495
88948
  const logger = getSafeLogger();
88496
88949
  const relToStoryId = new Map;
@@ -88503,8 +88956,8 @@ async function ensureStoryPackageDirs(prd, workdir, deps = _ensurePackageDirsDep
88503
88956
  }
88504
88957
  const created = [];
88505
88958
  for (const [rel, storyId] of relToStoryId) {
88506
- const abs = path12.resolve(workdir, rel);
88507
- const rootWithSep = workdir.endsWith(path12.sep) ? workdir : workdir + path12.sep;
88959
+ const abs = path11.resolve(workdir, rel);
88960
+ const rootWithSep = workdir.endsWith(path11.sep) ? workdir : workdir + path11.sep;
88508
88961
  if (abs !== workdir && !abs.startsWith(rootWithSep)) {
88509
88962
  logger?.warn("execution", "Skipping story workdir outside repo root", {
88510
88963
  storyId,
@@ -88655,8 +89108,8 @@ async function verifyQuoteTriple(triple, workdir, deps = _quoteIntegrityDeps) {
88655
89108
  return false;
88656
89109
  const lines = content.split(`
88657
89110
  `);
88658
- const start = Math.max(0, triple.line - 1 - CONTEXT_LINES);
88659
- const end = Math.min(lines.length, triple.line + CONTEXT_LINES);
89111
+ const start = Math.max(0, triple.line - 1 - CONTEXT_LINES2);
89112
+ const end = Math.min(lines.length, triple.line + CONTEXT_LINES2);
88660
89113
  const window2 = lines.slice(start, end).join(`
88661
89114
  `);
88662
89115
  return normalizeWs2(window2).toLowerCase().includes(normalizeWs2(triple.quote).toLowerCase());
@@ -88681,14 +89134,14 @@ async function verifyEscalationQuotes(reason, workdir, storyId, deps = _quoteInt
88681
89134
  }
88682
89135
  return verified;
88683
89136
  }
88684
- var _quoteIntegrityDeps, CONTEXT_LINES = 3;
89137
+ var _quoteIntegrityDeps, CONTEXT_LINES2 = 3;
88685
89138
  var init_quote_integrity = __esm(() => {
88686
89139
  init_logger2();
88687
89140
  init_path_security2();
88688
89141
  _quoteIntegrityDeps = {
88689
- readFile: async (path13) => {
89142
+ readFile: async (path12) => {
88690
89143
  try {
88691
- return await Bun.file(path13).text();
89144
+ return await Bun.file(path12).text();
88692
89145
  } catch {
88693
89146
  return null;
88694
89147
  }
@@ -89128,7 +89581,7 @@ var init_escalation = __esm(() => {
89128
89581
  import { randomUUID as randomUUID10 } from "crypto";
89129
89582
  import { rename as rename5, unlink as unlink5 } from "fs/promises";
89130
89583
  import { hostname as hostname3 } from "os";
89131
- import path13 from "path";
89584
+ import path12 from "path";
89132
89585
  function getSafeLogger3() {
89133
89586
  try {
89134
89587
  return getLogger();
@@ -89208,7 +89661,7 @@ async function claimReclaimableLock(lockPath, observedContent, lockData) {
89208
89661
  return { action: "discard" };
89209
89662
  }
89210
89663
  async function acquireLock(workdir) {
89211
- const lockPath = path13.join(workdir, "nax.lock");
89664
+ const lockPath = path12.join(workdir, "nax.lock");
89212
89665
  const lockFile = Bun.file(lockPath);
89213
89666
  try {
89214
89667
  const exists2 = await lockFile.exists();
@@ -89261,7 +89714,7 @@ async function acquireLock(workdir) {
89261
89714
  }
89262
89715
  }
89263
89716
  async function releaseLock(workdir) {
89264
- const lockPath = path13.join(workdir, "nax.lock");
89717
+ const lockPath = path12.join(workdir, "nax.lock");
89265
89718
  try {
89266
89719
  await unlink5(lockPath);
89267
89720
  } catch (error48) {
@@ -89286,7 +89739,7 @@ var init_lock2 = __esm(() => {
89286
89739
  // src/execution/feature-lock.ts
89287
89740
  import { mkdir as mkdir24, rename as rename6, unlink as unlink6 } from "fs/promises";
89288
89741
  import { hostname as hostname4 } from "os";
89289
- import path14 from "path";
89742
+ import path13 from "path";
89290
89743
  function validateFeatureId2(featureId) {
89291
89744
  if (!featureId || featureId.length === 0) {
89292
89745
  throw new NaxError("Feature ID cannot be empty", "INVALID_FEATURE_ID", { stage: "feature-lock" });
@@ -89316,7 +89769,7 @@ function getSafeLogger4() {
89316
89769
  }
89317
89770
  function featureLockPath(outputDir, feature) {
89318
89771
  validateFeatureId2(feature);
89319
- return path14.join(outputDir, "features", feature, "nax.lock");
89772
+ return path13.join(outputDir, "features", feature, "nax.lock");
89320
89773
  }
89321
89774
  function lockHost() {
89322
89775
  return _featureLockDeps.host();
@@ -89377,7 +89830,7 @@ function emptyHolder(feature) {
89377
89830
  }
89378
89831
  async function acquireFeatureLock(args) {
89379
89832
  const lockPath = _featureLockDeps.featureLockPath(args.outputDir, args.feature);
89380
- const featureDir2 = path14.dirname(lockPath);
89833
+ const featureDir2 = path13.dirname(lockPath);
89381
89834
  await mkdir24(featureDir2, { recursive: true });
89382
89835
  const lockFile = Bun.file(lockPath);
89383
89836
  if (await lockFile.exists()) {
@@ -90834,7 +91287,7 @@ var init_acceptance3 = __esm(() => {
90834
91287
  });
90835
91288
 
90836
91289
  // src/pipeline/stages/acceptance-setup.ts
90837
- import path15 from "path";
91290
+ import path14 from "path";
90838
91291
  function computeACFingerprint(criteria) {
90839
91292
  const sorted = [...criteria].sort().join(`
90840
91293
  `);
@@ -90844,7 +91297,7 @@ function computeACFingerprint(criteria) {
90844
91297
  }
90845
91298
  function computeAcceptanceLayoutFingerprint(workdir, groups) {
90846
91299
  const layout = groups.map(({ testPath, stories }) => ({
90847
- testPath: path15.relative(workdir, testPath).replaceAll(path15.sep, "/"),
91300
+ testPath: path14.relative(workdir, testPath).replaceAll(path14.sep, "/"),
90848
91301
  storyIds: stories.map((story) => story.id).sort()
90849
91302
  })).sort((a, b) => a.testPath.localeCompare(b.testPath));
90850
91303
  const hasher = new Bun.CryptoHasher("sha256");
@@ -90854,14 +91307,14 @@ function computeAcceptanceLayoutFingerprint(workdir, groups) {
90854
91307
  async function runAcceptanceSetup(ctx, featureDir2, phaseStartTime) {
90855
91308
  const language = ctx.config.project?.language;
90856
91309
  const testPathConfig = ctx.config.acceptance.testPath;
90857
- const metaPath = path15.join(featureDir2, "acceptance-meta.json");
91310
+ const metaPath = path14.join(featureDir2, "acceptance-meta.json");
90858
91311
  const allCriteria = ctx.prd.userStories.filter(isInAcceptanceScope).flatMap((s) => s.acceptanceCriteria);
90859
91312
  const featureName = ctx.prd.feature ?? ctx.prd.featureName;
90860
91313
  const groups = await groupStoriesByPackage(ctx.prd, ctx.workdir, featureName, testPathConfig, language);
90861
91314
  const nonFixStories = groups.flatMap((g) => g.stories);
90862
91315
  const groupConfigs = new Map;
90863
91316
  for (const group of groups) {
90864
- const relativeWorkdir = path15.relative(ctx.projectDir, group.packageDir);
91317
+ const relativeWorkdir = path14.relative(ctx.projectDir, group.packageDir);
90865
91318
  let config2 = ctx.config;
90866
91319
  if (relativeWorkdir && relativeWorkdir !== ".") {
90867
91320
  try {
@@ -90906,7 +91359,6 @@ async function runAcceptanceSetup(ctx, featureDir2, phaseStartTime) {
90906
91359
  await _acceptanceSetupDeps.deleteFile(testPath);
90907
91360
  }
90908
91361
  }
90909
- await _acceptanceSetupDeps.deleteSemanticVerdicts(featureDir2);
90910
91362
  shouldGenerate = true;
90911
91363
  regenerated = true;
90912
91364
  } else {
@@ -91014,7 +91466,7 @@ async function runAcceptanceSetup(ctx, featureDir2, phaseStartTime) {
91014
91466
  testable: c.testable,
91015
91467
  storyId: c.storyId
91016
91468
  })), null, 2);
91017
- await _acceptanceSetupDeps.writeFile(path15.join(featureDir2, "acceptance-refined.json"), refinedJsonContent);
91469
+ await _acceptanceSetupDeps.writeFile(path14.join(featureDir2, "acceptance-refined.json"), refinedJsonContent);
91018
91470
  }
91019
91471
  if (sawDispatchFailure) {
91020
91472
  getSafeLogger()?.warn("acceptance-setup", "generation dispatch failed; not recording acceptance meta so the next run regenerates", { storyId: ctx.story?.id, metaPath });
@@ -91126,21 +91578,6 @@ var init_acceptance_setup = __esm(() => {
91126
91578
  throw err;
91127
91579
  }
91128
91580
  },
91129
- deleteSemanticVerdicts: async (featureDir2) => {
91130
- const dir = `${featureDir2}/semantic-verdicts`;
91131
- const { readdir: readdir8, unlink: unlink7 } = await import("fs/promises");
91132
- let files;
91133
- try {
91134
- files = await readdir8(dir);
91135
- } catch (err) {
91136
- if (err.code === "ENOENT")
91137
- return;
91138
- throw err;
91139
- }
91140
- for (const file2 of files) {
91141
- await unlink7(`${dir}/${file2}`);
91142
- }
91143
- },
91144
91581
  readMeta: async (metaPath) => {
91145
91582
  const f = Bun.file(metaPath);
91146
91583
  if (!await f.exists())
@@ -91345,7 +91782,6 @@ async function getDiffFilePaths(workdir, baseRef) {
91345
91782
  }
91346
91783
  var MAX_DIFF_TEXT_CHARS = 8000, HIGH_MEMORY_TELEMETRY_BYTES, STREAM_DRAIN_DEADLINE_MS = 2000, completionStage, _completionDeps;
91347
91784
  var init_completion = __esm(() => {
91348
- init_acceptance2();
91349
91785
  init_config();
91350
91786
  init_engine();
91351
91787
  init_fragments();
@@ -91465,7 +91901,6 @@ var init_completion = __esm(() => {
91465
91901
  };
91466
91902
  _completionDeps = {
91467
91903
  checkReviewGate,
91468
- persistSemanticVerdict,
91469
91904
  savePRD,
91470
91905
  getDiffText,
91471
91906
  getDiffFilePaths,
@@ -92241,9 +92676,9 @@ var init_prompt2 = __esm(() => {
92241
92676
  });
92242
92677
 
92243
92678
  // src/pipeline/stages/queue-check.ts
92244
- import path16 from "path";
92679
+ import path15 from "path";
92245
92680
  function resolvePrdPath(ctx) {
92246
- return path16.join(ctx.featureDir ?? featureDir(ctx.workdir, "unknown"), "prd.json");
92681
+ return path15.join(ctx.featureDir ?? featureDir(ctx.workdir, "unknown"), "prd.json");
92247
92682
  }
92248
92683
  function logDroppedCommands(logger, ctx, queueCommands, currentIndex) {
92249
92684
  const dropped = queueCommands.slice(currentIndex + 1);
@@ -92294,10 +92729,10 @@ async function processQueueCommands(ctx, logger, queueCommands) {
92294
92729
  }
92295
92730
  if (cmd.type === "INJECT") {
92296
92731
  try {
92297
- if (path16.isAbsolute(cmd.storyFile)) {
92732
+ if (path15.isAbsolute(cmd.storyFile)) {
92298
92733
  throw new NaxError(`INJECT storyFile must be a relative path within the workspace: ${cmd.storyFile}`, "INJECT_PATH_ABSOLUTE", { stage: "queue-check", storyId: ctx.story?.id ?? "unknown", storyFile: cmd.storyFile });
92299
92734
  }
92300
- const storyFilePath = validateFilePath(path16.join(ctx.workdir, cmd.storyFile), ctx.workdir);
92735
+ const storyFilePath = validateFilePath(path15.join(ctx.workdir, cmd.storyFile), ctx.workdir);
92301
92736
  const raw = await Bun.file(storyFilePath).json();
92302
92737
  const existingIds = new Set(ctx.prd.userStories.map((s) => s.id));
92303
92738
  const story = validateInjectedStory(raw, existingIds);
@@ -93675,7 +94110,7 @@ var init_hooks = __esm(() => {
93675
94110
  });
93676
94111
 
93677
94112
  // src/execution/lifecycle/acceptance-helpers.ts
93678
- import path17 from "path";
94113
+ import path16 from "path";
93679
94114
  function resolveAcceptanceFixTarget(acceptanceTestPaths, failedPackage, config2) {
93680
94115
  const matchedEntry = failedPackage ? acceptanceTestPaths?.find((entry) => entry.testPath === failedPackage.testPath || entry.packageDir === failedPackage.packageDir) : undefined;
93681
94116
  const selectedPathEntry = matchedEntry ?? acceptanceTestPaths?.[0];
@@ -93698,10 +94133,7 @@ function resolveAcceptanceFixTarget(acceptanceTestPaths, failedPackage, config2)
93698
94133
  function isStubTestFile(content) {
93699
94134
  return isStubTestContent(content);
93700
94135
  }
93701
- function isTestLevelFailure(failedACs, totalACs, semanticVerdicts) {
93702
- if (semanticVerdicts && semanticVerdicts.length > 0 && semanticVerdicts.every((v) => v.passed)) {
93703
- return true;
93704
- }
94136
+ function isTestLevelFailure(failedACs, totalACs) {
93705
94137
  const failedCount = typeof failedACs === "number" ? failedACs : failedACs.length;
93706
94138
  const hasACError = Array.isArray(failedACs) && failedACs.includes("AC-ERROR");
93707
94139
  if (hasACError)
@@ -93713,7 +94145,7 @@ function isTestLevelFailure(failedACs, totalACs, semanticVerdicts) {
93713
94145
  async function loadSpecContent(featureDir2) {
93714
94146
  if (!featureDir2)
93715
94147
  return "";
93716
- const specPath = path17.join(featureDir2, "spec.md");
94148
+ const specPath = path16.join(featureDir2, "spec.md");
93717
94149
  const specFile = Bun.file(specPath);
93718
94150
  return await specFile.exists() ? await specFile.text() : "";
93719
94151
  }
@@ -93733,7 +94165,7 @@ async function loadAcceptanceTestContent2(featureDir2, testPaths, configuredTest
93733
94165
  }
93734
94166
  if (!configuredTestPath)
93735
94167
  return [];
93736
- const resolvedPath = path17.join(featureDir2, configuredTestPath);
94168
+ const resolvedPath = path16.join(featureDir2, configuredTestPath);
93737
94169
  const testFile = Bun.file(resolvedPath);
93738
94170
  const content = await testFile.exists() ? await testFile.text() : "";
93739
94171
  return [{ content, path: resolvedPath }];
@@ -93763,7 +94195,7 @@ async function regenerateAcceptanceTest(testPath, acceptanceContext) {
93763
94195
  const { unlink: unlink7 } = await import("fs/promises");
93764
94196
  await unlink7(testPath);
93765
94197
  if (acceptanceContext.featureDir) {
93766
- const metaPath = path17.join(acceptanceContext.featureDir, "acceptance-meta.json");
94198
+ const metaPath = path16.join(acceptanceContext.featureDir, "acceptance-meta.json");
93767
94199
  try {
93768
94200
  await unlink7(metaPath);
93769
94201
  } catch {}
@@ -93779,7 +94211,7 @@ async function regenerateAcceptanceTest(testPath, acceptanceContext) {
93779
94211
  const diffOutput = await _regenerateDeps.spawnGitDiff(repoRoot, storyGitRef, pathspec);
93780
94212
  const changedFilesRaw = diffOutput.split(`
93781
94213
  `).map((f) => f.trim()).filter((f) => f.length > 0);
93782
- const packageDir = storyPkg && acceptanceContext.projectDir ? path17.join(acceptanceContext.projectDir, storyPkg) : undefined;
94214
+ const packageDir = storyPkg && acceptanceContext.projectDir ? path16.join(acceptanceContext.projectDir, storyPkg) : undefined;
93783
94215
  const ignoreMatchers = acceptanceContext.naxIgnoreIndex?.getMatchers(packageDir) ?? await resolveNaxIgnorePatterns(repoRoot, packageDir);
93784
94216
  const changedFiles2 = filterNaxInternalPaths(changedFilesRaw, ignoreMatchers);
93785
94217
  const MAX_BYTES = 51200;
@@ -93791,7 +94223,7 @@ async function regenerateAcceptanceTest(testPath, acceptanceContext) {
93791
94223
  for (const file2 of changedFiles2) {
93792
94224
  if (totalBytes >= MAX_BYTES)
93793
94225
  break;
93794
- const filePath = path17.join(repoRoot, file2);
94226
+ const filePath = path16.join(repoRoot, file2);
93795
94227
  try {
93796
94228
  const fileContent = await _regenerateDeps.readFile(filePath);
93797
94229
  const remaining = MAX_BYTES - totalBytes;
@@ -93878,7 +94310,7 @@ function fixCallCtx(ctx, packageDir, config2) {
93878
94310
  }
93879
94311
  async function resolveAcceptanceDiagnosis(opts) {
93880
94312
  const logger = getSafeLogger();
93881
- const { ctx, failures, totalACs, strategy, semanticVerdicts, diagnosisOpts } = opts;
94313
+ const { ctx, failures, totalACs, strategy, diagnosisOpts } = opts;
93882
94314
  const storyId = diagnosisOpts.storyId;
93883
94315
  if (strategy === "implement-only") {
93884
94316
  logger?.info("acceptance.diagnosis", "Fast path: implement-only strategy \u2192 source_bug", { storyId });
@@ -93888,19 +94320,6 @@ async function resolveAcceptanceDiagnosis(opts) {
93888
94320
  confidence: 1
93889
94321
  };
93890
94322
  }
93891
- const SENTINELS = ["AC-ERROR", "AC-HOOK"];
93892
- const hasOnlySentinels = failures.failedACs.length > 0 && failures.failedACs.every((ac) => SENTINELS.includes(ac));
93893
- if (!hasOnlySentinels && semanticVerdicts.length > 0 && semanticVerdicts.every((v) => v.passed)) {
93894
- logger?.info("acceptance.diagnosis", "Fast path: all semantic verdicts passed \u2192 test_bug", {
93895
- storyId,
93896
- verdictCount: semanticVerdicts.length
93897
- });
93898
- return {
93899
- verdict: "test_bug",
93900
- reasoning: `Semantic review confirmed all ${semanticVerdicts.length} ACs are implemented \u2014 failure is a test generation issue`,
93901
- confidence: 1
93902
- };
93903
- }
93904
94323
  if (isTestLevelFailure(failures.failedACs, totalACs)) {
93905
94324
  logger?.info("acceptance.diagnosis", "Fast path: test-level failure heuristic \u2192 test_bug", {
93906
94325
  storyId,
@@ -93922,8 +94341,7 @@ async function resolveAcceptanceDiagnosis(opts) {
93922
94341
  testOutput: diagnosisOpts.testOutput,
93923
94342
  testFileContent: diagnosisOpts.testFileContent,
93924
94343
  acceptanceTestPath: diagnosisOpts.acceptanceTestPath,
93925
- sourceFiles,
93926
- semanticVerdicts
94344
+ sourceFiles
93927
94345
  });
93928
94346
  }
93929
94347
  var _diagnosisDeps;
@@ -94211,7 +94629,6 @@ async function runAcceptanceLoop(ctx) {
94211
94629
  continue;
94212
94630
  }
94213
94631
  }
94214
- const semanticVerdicts = ctx.featureDir ? await _acceptanceLoopDeps.loadSemanticVerdicts(ctx.featureDir) : [];
94215
94632
  const totalACs = prd.userStories.filter((s) => !isLegacyFixStory(s)).flatMap((s) => s.acceptanceCriteria).length;
94216
94633
  if (!ctx.runtime) {
94217
94634
  logger?.error("acceptance", "Runtime not found for diagnosis", { storyId: firstStory?.id });
@@ -94234,7 +94651,6 @@ async function runAcceptanceLoop(ctx) {
94234
94651
  failures: pkgFailures,
94235
94652
  totalACs,
94236
94653
  strategy,
94237
- semanticVerdicts,
94238
94654
  diagnosisOpts: {
94239
94655
  testOutput: pkg.output,
94240
94656
  testFileContent,
@@ -94280,7 +94696,6 @@ var init_acceptance_loop = __esm(() => {
94280
94696
  init_acceptance_helpers();
94281
94697
  init_acceptance_helpers();
94282
94698
  _acceptanceLoopDeps = {
94283
- loadSemanticVerdicts,
94284
94699
  loadAcceptanceTestContent
94285
94700
  };
94286
94701
  _acceptanceFixCycleDeps = {
@@ -94509,7 +94924,7 @@ var init_scratchpad_wipe = __esm(() => {
94509
94924
  init_logger2();
94510
94925
  init_tools();
94511
94926
  _scratchpadWipeDeps = {
94512
- remove: (path18) => rm7(path18, { recursive: true, force: true })
94927
+ remove: (path17) => rm7(path17, { recursive: true, force: true })
94513
94928
  };
94514
94929
  });
94515
94930
 
@@ -94661,6 +95076,13 @@ async function cleanupRun(options) {
94661
95076
  } catch (error48) {
94662
95077
  logger?.warn("plugins", "Plugin teardown failed", { error: error48 });
94663
95078
  }
95079
+ if (options.sealApprovals) {
95080
+ try {
95081
+ await options.sealApprovals();
95082
+ } catch (error48) {
95083
+ logger?.warn("permissions", "End-of-run approvals seal failed \u2014 continuing teardown", { error: error48 });
95084
+ }
95085
+ }
94664
95086
  if (interactionChain) {
94665
95087
  try {
94666
95088
  await interactionChain.destroy();
@@ -97098,8 +97520,8 @@ async function defaultRun(cmd, opts) {
97098
97520
  clearTimeout(timer);
97099
97521
  }
97100
97522
  }
97101
- async function defaultReadText(path18) {
97102
- const file2 = Bun.file(path18);
97523
+ async function defaultReadText(path17) {
97524
+ const file2 = Bun.file(path17);
97103
97525
  if (!await file2.exists())
97104
97526
  return null;
97105
97527
  return file2.text();
@@ -97199,10 +97621,10 @@ var init_pr = __esm(() => {
97199
97621
  });
97200
97622
 
97201
97623
  // src/forge/template.ts
97202
- import * as path18 from "path";
97624
+ import * as path17 from "path";
97203
97625
  async function firstExisting(workdir, deps, paths) {
97204
97626
  for (const relPath of paths) {
97205
- const content = await deps.readText(path18.join(workdir, relPath));
97627
+ const content = await deps.readText(path17.join(workdir, relPath));
97206
97628
  if (content !== null)
97207
97629
  return content;
97208
97630
  }
@@ -97417,7 +97839,7 @@ var init_pr_body = __esm(() => {
97417
97839
  });
97418
97840
 
97419
97841
  // src/plugins/builtin/auto-pr/index.ts
97420
- import * as path19 from "path";
97842
+ import * as path18 from "path";
97421
97843
  async function defaultRun2(cmd, opts) {
97422
97844
  const argv = cmd[0] === "git" ? hardenedGitArgv(cmd) : cmd;
97423
97845
  const proc = Bun.spawn(argv, { cwd: opts.cwd, env: gitSpawnEnv(), stdout: "pipe", stderr: "pipe" });
@@ -97443,8 +97865,8 @@ async function defaultRun2(cmd, opts) {
97443
97865
  clearTimeout(timer);
97444
97866
  }
97445
97867
  }
97446
- async function defaultReadText2(path20) {
97447
- const file2 = Bun.file(path20);
97868
+ async function defaultReadText2(path19) {
97869
+ const file2 = Bun.file(path19);
97448
97870
  if (!await file2.exists())
97449
97871
  return null;
97450
97872
  return file2.text();
@@ -97474,8 +97896,8 @@ function getStorySummary(context) {
97474
97896
  function relativePrdPath(workdir, prdPath) {
97475
97897
  if (!prdPath)
97476
97898
  return prdPath;
97477
- const rel = path19.relative(workdir, prdPath);
97478
- return rel && !rel.startsWith("..") && !path19.isAbsolute(rel) ? rel : prdPath;
97899
+ const rel = path18.relative(workdir, prdPath);
97900
+ return rel && !rel.startsWith("..") && !path18.isAbsolute(rel) ? rel : prdPath;
97479
97901
  }
97480
97902
  function toPrBodyContext(context) {
97481
97903
  const summary = getStorySummary(context);
@@ -97764,7 +98186,7 @@ var init_auto_prune = __esm(() => {
97764
98186
  });
97765
98187
 
97766
98188
  // src/plugins/builtin/curator/collect.ts
97767
- import * as path20 from "path";
98189
+ import * as path19 from "path";
97768
98190
  function now() {
97769
98191
  return new Date().toISOString();
97770
98192
  }
@@ -97822,7 +98244,7 @@ function tokenCount(story) {
97822
98244
  }
97823
98245
  async function collectFromMetrics(context) {
97824
98246
  const observations = [];
97825
- const metricsPath = path20.join(context.outputDir, "metrics.json");
98247
+ const metricsPath = path19.join(context.outputDir, "metrics.json");
97826
98248
  try {
97827
98249
  const data = await readJsonFile(metricsPath);
97828
98250
  const runs = Array.isArray(data) ? data : [data];
@@ -97898,11 +98320,11 @@ function findingMessage(finding) {
97898
98320
  }
97899
98321
  async function collectFromReviewAudit(context) {
97900
98322
  const observations = [];
97901
- const auditDir = path20.join(context.outputDir, "review-audit");
98323
+ const auditDir = path19.join(context.outputDir, "review-audit");
97902
98324
  try {
97903
98325
  const glob = new Bun.Glob("**/*.json");
97904
98326
  for await (const file2 of glob.scan({ cwd: auditDir, absolute: false })) {
97905
- const fullPath = path20.join(auditDir, file2);
98327
+ const fullPath = path19.join(auditDir, file2);
97906
98328
  try {
97907
98329
  const audit = asRecord3(await readJsonFile(fullPath));
97908
98330
  if (!audit)
@@ -97953,7 +98375,7 @@ async function collectFromContextManifests(context) {
97953
98375
  try {
97954
98376
  const glob = new Bun.Glob("*/stories/*/context-manifest-*.json");
97955
98377
  for await (const file2 of glob.scan({ cwd: featuresRoot, absolute: false })) {
97956
- const fullPath = path20.join(featuresRoot, file2);
98378
+ const fullPath = path19.join(featuresRoot, file2);
97957
98379
  try {
97958
98380
  const parts = file2.split("/");
97959
98381
  const featureId = parts[0] ?? context.feature;
@@ -98560,9 +98982,9 @@ function renderProposals(proposals, runId, observationCount, provenance) {
98560
98982
 
98561
98983
  // src/plugins/builtin/curator/rollup.ts
98562
98984
  import { appendFile as appendFile7, mkdir as mkdir26, writeFile as writeFile11 } from "fs/promises";
98563
- import * as path21 from "path";
98985
+ import * as path20 from "path";
98564
98986
  async function appendToRollup(observations, rollupPath) {
98565
- const dir = path21.dirname(rollupPath);
98987
+ const dir = path20.dirname(rollupPath);
98566
98988
  await mkdir26(dir, { recursive: true }).catch(() => {});
98567
98989
  const { withPathFileLock: withPathFileLock2 } = await Promise.resolve().then(() => (init_path_file_lock(), exports_path_file_lock));
98568
98990
  await withPathFileLock2(rollupPath, () => appendToRollupUnlocked(observations, rollupPath)).catch(() => {});
@@ -98651,7 +99073,7 @@ var init_rollup2 = __esm(() => {
98651
99073
 
98652
99074
  // src/plugins/builtin/curator/index.ts
98653
99075
  import { mkdir as mkdir27 } from "fs/promises";
98654
- import * as path22 from "path";
99076
+ import * as path21 from "path";
98655
99077
  function getCuratorEnabled(context) {
98656
99078
  const cfg = context.config;
98657
99079
  if (!cfg)
@@ -98727,7 +99149,7 @@ var init_curator = __esm(() => {
98727
99149
  const observations = await collectObservations(curatorContext);
98728
99150
  if (context.outputDir) {
98729
99151
  const { observationsPath, rollupPath } = resolveCuratorOutputs(curatorContext);
98730
- const runDir = path22.dirname(observationsPath);
99152
+ const runDir = path21.dirname(observationsPath);
98731
99153
  await mkdir27(runDir, { recursive: true });
98732
99154
  await Bun.write(observationsPath, observations.map((o) => JSON.stringify(o)).join(`
98733
99155
  `) + (observations.length > 0 ? `
@@ -98765,7 +99187,7 @@ var init_curator = __esm(() => {
98765
99187
  const windowHasObservations = window2.observations.length > 0;
98766
99188
  const provenance = windowHasObservations ? { runCount: window2.runIds.length, observationCount: window2.observations.length } : { runCount: 1, observationCount: observations.length };
98767
99189
  const markdown = renderProposals(proposals, context.runId, observations.length, provenance);
98768
- const proposalsMdPath = path22.join(runDir, "curator-proposals.md");
99190
+ const proposalsMdPath = path21.join(runDir, "curator-proposals.md");
98769
99191
  await Bun.write(proposalsMdPath, markdown);
98770
99192
  }
98771
99193
  return {
@@ -99148,14 +99570,14 @@ var init_validator = __esm(() => {
99148
99570
 
99149
99571
  // src/plugins/loader.ts
99150
99572
  import * as fs from "fs/promises";
99151
- import * as path23 from "path";
99573
+ import * as path22 from "path";
99152
99574
  function getSafeLogger7() {
99153
99575
  return getSafeLogger();
99154
99576
  }
99155
99577
  function extractPluginName(pluginPath) {
99156
- const basename13 = path23.basename(pluginPath);
99578
+ const basename13 = path22.basename(pluginPath);
99157
99579
  if (basename13 === "index.ts" || basename13 === "index.js" || basename13 === "index.mjs") {
99158
- return path23.basename(path23.dirname(pluginPath));
99580
+ return path22.basename(path22.dirname(pluginPath));
99159
99581
  }
99160
99582
  return basename13.replace(/\.(ts|js|mjs)$/, "");
99161
99583
  }
@@ -99302,7 +99724,7 @@ async function discoverPlugins(dir, isTestFileFn) {
99302
99724
  try {
99303
99725
  const entries = await fs.readdir(dir, { withFileTypes: true });
99304
99726
  for (const entry of entries) {
99305
- const fullPath = path23.join(dir, entry.name);
99727
+ const fullPath = path22.join(dir, entry.name);
99306
99728
  if (entry.isFile()) {
99307
99729
  if (isPluginFile(entry.name, isTestFileFn)) {
99308
99730
  discovered.push({ path: fullPath });
@@ -99310,7 +99732,7 @@ async function discoverPlugins(dir, isTestFileFn) {
99310
99732
  } else if (entry.isDirectory()) {
99311
99733
  const indexPaths = ["index.ts", "index.js", "index.mjs"];
99312
99734
  for (const indexFile of indexPaths) {
99313
- const indexPath = path23.join(fullPath, indexFile);
99735
+ const indexPath = path22.join(fullPath, indexFile);
99314
99736
  try {
99315
99737
  await fs.access(indexPath);
99316
99738
  discovered.push({ path: indexPath });
@@ -99335,13 +99757,13 @@ function isPluginFile(filename, isTestFileFn) {
99335
99757
  return !FALLBACK_TEST_FILE_RE.test(filename);
99336
99758
  }
99337
99759
  function resolveModulePath(modulePath, projectRoot) {
99338
- if (path23.isAbsolute(modulePath) || !modulePath.startsWith("./") && !modulePath.startsWith("../")) {
99760
+ if (path22.isAbsolute(modulePath) || !modulePath.startsWith("./") && !modulePath.startsWith("../")) {
99339
99761
  return modulePath;
99340
99762
  }
99341
99763
  if (projectRoot) {
99342
- return path23.resolve(projectRoot, modulePath);
99764
+ return path22.resolve(projectRoot, modulePath);
99343
99765
  }
99344
- return path23.resolve(modulePath);
99766
+ return path22.resolve(modulePath);
99345
99767
  }
99346
99768
  async function loadAndValidatePlugin(initialModulePath, config2, allowedRoots, originalPath) {
99347
99769
  let attemptedPath = initialModulePath;
@@ -99563,8 +99985,8 @@ var init_language_commands = __esm(() => {
99563
99985
 
99564
99986
  // src/review/scoped-lint.ts
99565
99987
  import { join as join100, relative as relative26 } from "path";
99566
- function normalizePath3(path24) {
99567
- return path24.replaceAll("\\", "/").replace(/^\.\//, "");
99988
+ function normalizePath3(path23) {
99989
+ return path23.replaceAll("\\", "/").replace(/^\.\//, "");
99568
99990
  }
99569
99991
  function isSupportedDerivedScopedCommand(command) {
99570
99992
  const commands = normalizeCommandSpec(command);
@@ -99799,7 +100221,7 @@ var init_scoped_lint = __esm(() => {
99799
100221
  listChangedFiles,
99800
100222
  findPackageDir,
99801
100223
  runLintCommand,
99802
- fileExists: (path24) => Bun.file(path24).exists()
100224
+ fileExists: (path23) => Bun.file(path23).exists()
99803
100225
  };
99804
100226
  });
99805
100227
 
@@ -100260,7 +100682,7 @@ var init_paused_story_prompts = __esm(() => {
100260
100682
  });
100261
100683
 
100262
100684
  // src/execution/lifecycle/run-setup-init.ts
100263
- import path24 from "path";
100685
+ import path23 from "path";
100264
100686
  async function initializeAfterLock(options) {
100265
100687
  const logger = getSafeLogger();
100266
100688
  const { config: config2, workdir, feature, dryRun, runtime, interactionChain, runId, agentGetFn, statusWriter, deps } = options;
@@ -100292,8 +100714,8 @@ async function initializeAfterLock(options) {
100292
100714
  explicit: Object.fromEntries(explicitFields.map((f) => [f, existingProjectConfig[f]])),
100293
100715
  detected: Object.fromEntries(autodetectedFields.map((f) => [f, detectedProfile[f]]))
100294
100716
  });
100295
- const globalPluginsDir = path24.join(globalConfigDir(), "plugins");
100296
- const projectPluginsDir = path24.join(workdir, ".nax", "plugins");
100717
+ const globalPluginsDir = path23.join(globalConfigDir(), "plugins");
100718
+ const projectPluginsDir = path23.join(workdir, ".nax", "plugins");
100297
100719
  const configPlugins = config2.plugins || [];
100298
100720
  const resolvedPatterns = await resolveTestFilePatterns(config2, workdir);
100299
100721
  const isTestFileFn = (filename) => resolvedPatterns.regex.some((re) => re.test(filename));
@@ -101252,7 +101674,7 @@ __export(exports_migrate, {
101252
101674
  });
101253
101675
  import { existsSync as existsSync30 } from "fs";
101254
101676
  import { mkdir as mkdir28, readdir as readdir10, rename as rename9 } from "fs/promises";
101255
- import path25 from "path";
101677
+ import path24 from "path";
101256
101678
  async function detectGeneratedContent(naxDir) {
101257
101679
  if (!existsSync30(naxDir))
101258
101680
  return [];
@@ -101265,17 +101687,17 @@ async function detectGeneratedContent(naxDir) {
101265
101687
  }
101266
101688
  for (const entry of entries) {
101267
101689
  if (GENERATED_NAMES.has(entry)) {
101268
- candidates.push({ name: entry, srcPath: path25.join(naxDir, entry) });
101690
+ candidates.push({ name: entry, srcPath: path24.join(naxDir, entry) });
101269
101691
  }
101270
101692
  }
101271
- const featuresDir2 = path25.join(naxDir, "features");
101693
+ const featuresDir2 = path24.join(naxDir, "features");
101272
101694
  if (existsSync30(featuresDir2)) {
101273
101695
  let featureDirs = [];
101274
101696
  try {
101275
101697
  featureDirs = await readdir10(featuresDir2);
101276
101698
  } catch {}
101277
101699
  for (const fid of featureDirs) {
101278
- const featureDir2 = path25.join(featuresDir2, fid);
101700
+ const featureDir2 = path24.join(featuresDir2, fid);
101279
101701
  let subEntries = [];
101280
101702
  try {
101281
101703
  subEntries = await readdir10(featureDir2);
@@ -101285,12 +101707,12 @@ async function detectGeneratedContent(naxDir) {
101285
101707
  for (const sub of subEntries) {
101286
101708
  if (GENERATED_FEATURE_SUBNAMES.has(sub)) {
101287
101709
  candidates.push({
101288
- name: path25.join("features", fid, sub),
101289
- srcPath: path25.join(featureDir2, sub)
101710
+ name: path24.join("features", fid, sub),
101711
+ srcPath: path24.join(featureDir2, sub)
101290
101712
  });
101291
101713
  }
101292
101714
  if (sub === "stories") {
101293
- const storiesDir = path25.join(featureDir2, "stories");
101715
+ const storiesDir = path24.join(featureDir2, "stories");
101294
101716
  let storyDirs = [];
101295
101717
  try {
101296
101718
  storyDirs = await readdir10(storiesDir);
@@ -101298,7 +101720,7 @@ async function detectGeneratedContent(naxDir) {
101298
101720
  continue;
101299
101721
  }
101300
101722
  for (const sid of storyDirs) {
101301
- const storyDir = path25.join(storiesDir, sid);
101723
+ const storyDir = path24.join(storiesDir, sid);
101302
101724
  let storyEntries = [];
101303
101725
  try {
101304
101726
  storyEntries = await readdir10(storyDir);
@@ -101308,8 +101730,8 @@ async function detectGeneratedContent(naxDir) {
101308
101730
  for (const se of storyEntries) {
101309
101731
  if (se.startsWith("context-manifest-") && se.endsWith(".json")) {
101310
101732
  candidates.push({
101311
- name: path25.join("features", fid, "stories", sid, se),
101312
- srcPath: path25.join(storyDir, se)
101733
+ name: path24.join("features", fid, "stories", sid, se),
101734
+ srcPath: path24.join(storyDir, se)
101313
101735
  });
101314
101736
  }
101315
101737
  }
@@ -101330,15 +101752,15 @@ async function migrateCommand(options) {
101330
101752
  name: options.reclaim
101331
101753
  });
101332
101754
  }
101333
- const src = path25.join(globalConfigDir(), options.reclaim);
101755
+ const src = path24.join(globalConfigDir(), options.reclaim);
101334
101756
  if (!existsSync30(src)) {
101335
101757
  throw new NaxError(`Nothing to reclaim: ~/.nax/${options.reclaim} does not exist`, "MIGRATE_RECLAIM_NOT_FOUND", {
101336
101758
  stage: "migrate",
101337
101759
  name: options.reclaim
101338
101760
  });
101339
101761
  }
101340
- const archiveBase = path25.join(globalConfigDir(), "_archive");
101341
- const archiveDest = path25.join(archiveBase, `${options.reclaim}-${Date.now()}`);
101762
+ const archiveBase = path24.join(globalConfigDir(), "_archive");
101763
+ const archiveDest = path24.join(archiveBase, `${options.reclaim}-${Date.now()}`);
101342
101764
  await mkdir28(archiveBase, { recursive: true });
101343
101765
  await rename9(src, archiveDest);
101344
101766
  logger.info("migrate", `Reclaimed: archived to ${archiveDest}`, { storyId: "_migrate" });
@@ -101378,8 +101800,8 @@ async function migrateCommand(options) {
101378
101800
  logger.info("migrate", `Merged: identity for "${options.merge}" updated`, { storyId: "_migrate" });
101379
101801
  return;
101380
101802
  }
101381
- const naxDir = path25.join(options.workdir, ".nax");
101382
- const configPath = path25.join(naxDir, "config.json");
101803
+ const naxDir = path24.join(options.workdir, ".nax");
101804
+ const configPath = path24.join(naxDir, "config.json");
101383
101805
  if (!existsSync30(configPath)) {
101384
101806
  throw new NaxError("No .nax/config.json found \u2014 run nax init first", "MIGRATE_NO_CONFIG", {
101385
101807
  stage: "migrate",
@@ -101395,7 +101817,7 @@ async function migrateCommand(options) {
101395
101817
  cause: e
101396
101818
  });
101397
101819
  }
101398
- const projectKey = config2.name?.trim() || path25.basename(options.workdir);
101820
+ const projectKey = config2.name?.trim() || path24.basename(options.workdir);
101399
101821
  const destBase = projectOutputDir(projectKey, config2.outputDir);
101400
101822
  const candidates = await detectGeneratedContent(naxDir);
101401
101823
  if (candidates.length === 0) {
@@ -101404,7 +101826,7 @@ async function migrateCommand(options) {
101404
101826
  }
101405
101827
  if (options.dryRun) {
101406
101828
  for (const c of candidates) {
101407
- logger.info("migrate", `[dry-run] Would move: ${c.srcPath} -> ${path25.join(destBase, c.name)}`, {
101829
+ logger.info("migrate", `[dry-run] Would move: ${c.srcPath} -> ${path24.join(destBase, c.name)}`, {
101408
101830
  storyId: "_migrate"
101409
101831
  });
101410
101832
  }
@@ -101413,8 +101835,8 @@ async function migrateCommand(options) {
101413
101835
  await mkdir28(destBase, { recursive: true });
101414
101836
  let moved = 0;
101415
101837
  for (const candidate of candidates) {
101416
- const dest = path25.join(destBase, candidate.name);
101417
- await mkdir28(path25.dirname(dest), { recursive: true });
101838
+ const dest = path24.join(destBase, candidate.name);
101839
+ await mkdir28(path24.dirname(dest), { recursive: true });
101418
101840
  if (existsSync30(dest)) {
101419
101841
  throw new NaxError(`Migration conflict: destination already exists.
101420
101842
  Source: ${candidate.srcPath}
@@ -101444,7 +101866,7 @@ async function migrateCommand(options) {
101444
101866
  moved++;
101445
101867
  logger.info("migrate", `Moved: ${candidate.name}`, { storyId: "_migrate" });
101446
101868
  }
101447
- await Bun.write(path25.join(destBase, ".migrated-from"), JSON.stringify({ from: options.workdir, migratedAt: new Date().toISOString() }, null, 2));
101869
+ await Bun.write(path24.join(destBase, ".migrated-from"), JSON.stringify({ from: options.workdir, migratedAt: new Date().toISOString() }, null, 2));
101448
101870
  logger.info("migrate", `Migration complete: ${moved} entries moved`, {
101449
101871
  storyId: "_migrate",
101450
101872
  destBase
@@ -101815,10 +102237,10 @@ function renderReport(timeline, options = {}) {
101815
102237
  // src/commands/replay.ts
101816
102238
  import { existsSync as existsSync32 } from "fs";
101817
102239
  import { dirname as dirname20 } from "path";
101818
- async function readJsonlLenient(path26) {
101819
- if (!existsSync32(path26))
102240
+ async function readJsonlLenient(path25) {
102241
+ if (!existsSync32(path25))
101820
102242
  return [];
101821
- const content = await Bun.file(path26).text();
102243
+ const content = await Bun.file(path25).text();
101822
102244
  const lines = content.split(`
101823
102245
  `);
101824
102246
  const entries = [];
@@ -101832,11 +102254,11 @@ async function readJsonlLenient(path26) {
101832
102254
  }
101833
102255
  return entries;
101834
102256
  }
101835
- async function readJsonOrUndefined(path26) {
101836
- if (!existsSync32(path26))
102257
+ async function readJsonOrUndefined(path25) {
102258
+ if (!existsSync32(path25))
101837
102259
  return;
101838
102260
  try {
101839
- return await Bun.file(path26).json();
102261
+ return await Bun.file(path25).json();
101840
102262
  } catch {
101841
102263
  return;
101842
102264
  }
@@ -102549,7 +102971,7 @@ __export(exports_precheck_runner, {
102549
102971
  runPrecheckValidation: () => runPrecheckValidation
102550
102972
  });
102551
102973
  import { mkdirSync as mkdirSync5 } from "fs";
102552
- import path26 from "path";
102974
+ import path25 from "path";
102553
102975
  async function runPrecheckValidation(ctx) {
102554
102976
  const logger = getSafeLogger();
102555
102977
  if (process.env.NAX_PRECHECK !== "1") {
@@ -102558,7 +102980,7 @@ async function runPrecheckValidation(ctx) {
102558
102980
  }
102559
102981
  logger?.info("precheck", "Running precheck validations...");
102560
102982
  const { runPrecheck: runPrecheck3 } = await Promise.resolve().then(() => (init_precheck2(), exports_precheck));
102561
- const projectKey = ctx.config.name?.trim() || path26.basename(ctx.workdir);
102983
+ const projectKey = ctx.config.name?.trim() || path25.basename(ctx.workdir);
102562
102984
  const outputDir = projectOutputDir(projectKey, ctx.config.outputDir);
102563
102985
  const featureLock = ctx.featureName !== undefined ? { outputDir, feature: ctx.featureName } : undefined;
102564
102986
  const precheckResult = await runPrecheck3(ctx.config, ctx.prd, {
@@ -102568,7 +102990,7 @@ async function runPrecheckValidation(ctx) {
102568
102990
  ...featureLock !== undefined ? { featureLock } : {}
102569
102991
  });
102570
102992
  if (ctx.logFilePath) {
102571
- mkdirSync5(path26.dirname(ctx.logFilePath), { recursive: true });
102993
+ mkdirSync5(path25.dirname(ctx.logFilePath), { recursive: true });
102572
102994
  const precheckLog = {
102573
102995
  type: "precheck",
102574
102996
  timestamp: new Date().toISOString(),
@@ -102666,7 +103088,7 @@ __export(exports_run_setup, {
102666
103088
  warnInertBashStages: () => warnInertBashStages,
102667
103089
  warnProfileMismatch: () => warnProfileMismatch
102668
103090
  });
102669
- import path27 from "path";
103091
+ import path26 from "path";
102670
103092
  async function setupRun(options) {
102671
103093
  const logger = getSafeLogger();
102672
103094
  warnFallbackMisconfiguration(options.config, options.agentGetFn, logger);
@@ -102731,6 +103153,7 @@ async function setupRun(options) {
102731
103153
  }
102732
103154
  await runtime.pidRegistry.cleanupStale();
102733
103155
  let cleanupCrashHandlers;
103156
+ let sealApprovals;
102734
103157
  try {
102735
103158
  cleanupCrashHandlers = _runSetupDeps.installCrashHandlers({
102736
103159
  statusWriter,
@@ -102748,6 +103171,9 @@ async function setupRun(options) {
102748
103171
  emitError: (reason) => {
102749
103172
  pipelineEventBus.emit({ type: "run:errored", reason, feature: options.feature });
102750
103173
  },
103174
+ sealApprovals: async () => {
103175
+ await sealApprovals?.();
103176
+ },
102751
103177
  onShutdown: async (abortSignal) => {
102752
103178
  await closeAllRunSessions(sessionManager, options.agentGetFn, { force: true, signal: abortSignal });
102753
103179
  await runtime.close().catch((err) => {
@@ -102760,7 +103186,7 @@ async function setupRun(options) {
102760
103186
  statusWriter.setPrd(prd);
102761
103187
  {
102762
103188
  const { detectGeneratedContent: detectGeneratedContent2, migrateCommand: migrateCommand2 } = await Promise.resolve().then(() => (init_commands(), exports_commands));
102763
- const naxDir = path27.join(workdir, ".nax");
103189
+ const naxDir = path26.join(workdir, ".nax");
102764
103190
  const candidates = await detectGeneratedContent2(naxDir).catch(() => []);
102765
103191
  if (candidates.length > 0) {
102766
103192
  logger?.info("setup", "Found generated content under .nax/ \u2014 migrating to output dir", {
@@ -102787,7 +103213,7 @@ async function setupRun(options) {
102787
103213
  remoteUrl = new TextDecoder().decode(gitResult.stdout).trim() || null;
102788
103214
  }
102789
103215
  } catch {}
102790
- const projectKey = config2.name?.trim() || path27.basename(workdir);
103216
+ const projectKey = config2.name?.trim() || path26.basename(workdir);
102791
103217
  await claimProjectIdentity2(projectKey, workdir, remoteUrl).catch((err) => {
102792
103218
  if (err instanceof NaxError && err.code === "RUN_NAME_COLLISION") {
102793
103219
  throw err;
@@ -102871,6 +103297,19 @@ async function setupRun(options) {
102871
103297
  sweepFeatureTranscripts: _runSetupDeps.sweepFeatureTranscripts
102872
103298
  }
102873
103299
  });
103300
+ try {
103301
+ sealApprovals = await _runSetupDeps.buildApprovalsSeal({
103302
+ projectDir: options.workdir,
103303
+ rootConfig: options.config,
103304
+ packageDirs: initResult.prd.userStories.map(storyPackageDir),
103305
+ outputDir: runtime.outputDir,
103306
+ runId: options.runId
103307
+ });
103308
+ } catch (error48) {
103309
+ await releaseFeatureLock({ outputDir: runtime.outputDir, feature, runId });
103310
+ await releaseLock(workdir);
103311
+ throw error48;
103312
+ }
102874
103313
  return {
102875
103314
  statusWriter,
102876
103315
  sessionManager,
@@ -102880,7 +103319,8 @@ async function setupRun(options) {
102880
103319
  storyCounts: initResult.storyCounts,
102881
103320
  interactionChain: initResult.interactionChain,
102882
103321
  shutdownController,
102883
- runtime
103322
+ runtime,
103323
+ sealApprovals
102884
103324
  };
102885
103325
  } catch (error48) {
102886
103326
  cleanupCrashHandlers?.();
@@ -102909,6 +103349,7 @@ var init_run_setup = __esm(() => {
102909
103349
  init_test_runners();
102910
103350
  init_tools();
102911
103351
  init_git_env();
103352
+ init_path_frame();
102912
103353
  init_crash_recovery();
102913
103354
  init_feature_lock();
102914
103355
  init_helpers();
@@ -102921,6 +103362,7 @@ var init_run_setup = __esm(() => {
102921
103362
  createRuntime,
102922
103363
  installCrashHandlers,
102923
103364
  sweepFeatureTranscripts,
103365
+ buildApprovalsSeal,
102924
103366
  acquireLock,
102925
103367
  acquireFeatureLock
102926
103368
  };
@@ -104177,7 +104619,7 @@ var init_queue_file_lock = __esm(() => {
104177
104619
 
104178
104620
  // src/execution/queue-handler.ts
104179
104621
  import { rename as rename10, unlink as unlink11 } from "fs/promises";
104180
- import path28 from "path";
104622
+ import path27 from "path";
104181
104623
  function getSafeLogger8() {
104182
104624
  try {
104183
104625
  return getLogger();
@@ -104209,8 +104651,8 @@ async function claimCommandsLocked(queuePath, processingPath, logger) {
104209
104651
  return result.commands;
104210
104652
  }
104211
104653
  async function readQueueFile(workdir) {
104212
- const queuePath = path28.join(workdir, ".queue.txt");
104213
- const processingPath = path28.join(workdir, ".queue.txt.processing");
104654
+ const queuePath = path27.join(workdir, ".queue.txt");
104655
+ const processingPath = path27.join(workdir, ".queue.txt.processing");
104214
104656
  const logger = getSafeLogger8();
104215
104657
  try {
104216
104658
  return await withQueueFileLock(queuePath, async () => {
@@ -104225,8 +104667,8 @@ async function readQueueFile(workdir) {
104225
104667
  }
104226
104668
  }
104227
104669
  async function processQueueFile(workdir, processor) {
104228
- const queuePath = path28.join(workdir, ".queue.txt");
104229
- const processingPath = path28.join(workdir, ".queue.txt.processing");
104670
+ const queuePath = path27.join(workdir, ".queue.txt");
104671
+ const processingPath = path27.join(workdir, ".queue.txt.processing");
104230
104672
  const logger = getSafeLogger8();
104231
104673
  try {
104232
104674
  return await withQueueFileLock(queuePath, async () => {
@@ -104251,7 +104693,7 @@ async function processQueueFile(workdir, processor) {
104251
104693
  }
104252
104694
  }
104253
104695
  async function drainQueueAtBatchBoundary(workdir, prd) {
104254
- if (!await Bun.file(path28.join(workdir, ".queue.txt")).exists()) {
104696
+ if (!await Bun.file(path27.join(workdir, ".queue.txt")).exists()) {
104255
104697
  return { paused: false };
104256
104698
  }
104257
104699
  const logger = getSafeLogger8();
@@ -104307,10 +104749,10 @@ async function applyBatchBoundaryCommands(workdir, prd, commands, logger) {
104307
104749
  }
104308
104750
  if (cmd.type === "INJECT") {
104309
104751
  try {
104310
- if (path28.isAbsolute(cmd.storyFile)) {
104752
+ if (path27.isAbsolute(cmd.storyFile)) {
104311
104753
  throw new NaxError(`INJECT storyFile must be a relative path within the workspace: ${cmd.storyFile}`, "INJECT_PATH_ABSOLUTE", { stage: "queue-batch-boundary", storyFile: cmd.storyFile });
104312
104754
  }
104313
- const storyFilePath = validateFilePath(path28.join(workdir, cmd.storyFile), workdir);
104755
+ const storyFilePath = validateFilePath(path27.join(workdir, cmd.storyFile), workdir);
104314
104756
  const raw = await Bun.file(storyFilePath).json();
104315
104757
  const existingIds = new Set(prd.userStories.map((s) => s.id));
104316
104758
  const story = validateInjectedStory(raw, existingIds);
@@ -104330,8 +104772,8 @@ async function applyBatchBoundaryCommands(workdir, prd, commands, logger) {
104330
104772
  return { paused: false };
104331
104773
  }
104332
104774
  async function clearQueueFile(workdir) {
104333
- const queuePath = path28.join(workdir, ".queue.txt");
104334
- const processingPath = path28.join(workdir, ".queue.txt.processing");
104775
+ const queuePath = path27.join(workdir, ".queue.txt");
104776
+ const processingPath = path27.join(workdir, ".queue.txt.processing");
104335
104777
  const logger = getSafeLogger8();
104336
104778
  try {
104337
104779
  await withQueueFileLock(queuePath, async () => {
@@ -105750,12 +106192,12 @@ var init_body = __esm(() => {
105750
106192
  // src/finish/pr/context.ts
105751
106193
  import { readFile as readFile8 } from "fs/promises";
105752
106194
  import { join as join113 } from "path";
105753
- async function readJson(path29) {
106195
+ async function readJson(path28) {
105754
106196
  let text;
105755
106197
  try {
105756
- text = await _finishPrDeps.readText(path29);
106198
+ text = await _finishPrDeps.readText(path28);
105757
106199
  } catch (error48) {
105758
- _finishPrDeps.warn("[finish-pr] Failed to read PR context artifact", { path: path29, error: error48 });
106200
+ _finishPrDeps.warn("[finish-pr] Failed to read PR context artifact", { path: path28, error: error48 });
105759
106201
  return;
105760
106202
  }
105761
106203
  if (text === null)
@@ -105763,7 +106205,7 @@ async function readJson(path29) {
105763
106205
  try {
105764
106206
  return JSON.parse(text);
105765
106207
  } catch (error48) {
105766
- _finishPrDeps.warn("[finish-pr] Failed to parse PR context artifact", { path: path29, error: error48 });
106208
+ _finishPrDeps.warn("[finish-pr] Failed to parse PR context artifact", { path: path28, error: error48 });
105767
106209
  return;
105768
106210
  }
105769
106211
  }
@@ -105847,9 +106289,9 @@ var init_context5 = __esm(() => {
105847
106289
  init_pr_title();
105848
106290
  _finishPrDeps = {
105849
106291
  run: defaultForgeDeps.run,
105850
- readText: async (path29) => {
106292
+ readText: async (path28) => {
105851
106293
  try {
105852
- return await readFile8(path29, "utf8");
106294
+ return await readFile8(path28, "utf8");
105853
106295
  } catch (err) {
105854
106296
  if (err.code === "ENOENT")
105855
106297
  return null;
@@ -106738,7 +107180,7 @@ var init_finish = __esm(() => {
106738
107180
  });
106739
107181
 
106740
107182
  // src/execution/runner-completion.ts
106741
- import path29 from "path";
107183
+ import path28 from "path";
106742
107184
  async function runCompletionPhase(options) {
106743
107185
  const logger = getSafeLogger();
106744
107186
  logger?.debug("execution", "Completion phase started", {
@@ -106759,7 +107201,7 @@ async function runCompletionPhase(options) {
106759
107201
  const acceptanceStartTime = Date.now();
106760
107202
  pipelineEventBus.emit({ type: "postrun:phase:started", phase: "acceptance" });
106761
107203
  const acceptanceTestPaths = options.featureDir ? await Promise.all((await groupStoriesByPackage(options.prd, options.workdir, options.feature, options.config.acceptance.testPath, options.config.project?.language)).map(async (g) => {
106762
- const relativeWorkdir = path29.relative(options.workdir, g.packageDir);
107204
+ const relativeWorkdir = path28.relative(options.workdir, g.packageDir);
106763
107205
  let groupConfig = options.config;
106764
107206
  if (relativeWorkdir && relativeWorkdir !== ".") {
106765
107207
  try {
@@ -106890,7 +107332,7 @@ async function runCompletionPhase(options) {
106890
107332
  exitReason: options.exitReason,
106891
107333
  runtime: options.runtime,
106892
107334
  abortSignal: options.abortSignal,
106893
- isSequential: options.parallel === undefined,
107335
+ isSequential: options.parallel === undefined || !(options.parallel > 1),
106894
107336
  interactionChain: options.interactionChain
106895
107337
  });
106896
107338
  const { durationMs, runCompletedAt, finalCounts, reportedTotal, pluginGateFailed } = completionResult;
@@ -107496,7 +107938,7 @@ __export(exports_parallel_batch, {
107496
107938
  _parallelBatchDeps: () => _parallelBatchDeps,
107497
107939
  runParallelBatch: () => runParallelBatch
107498
107940
  });
107499
- import path30 from "path";
107941
+ import path29 from "path";
107500
107942
  async function runParallelBatch(options) {
107501
107943
  const { stories, ctx, prd } = options;
107502
107944
  const { workdir, config: config2, maxConcurrency, pipelineContext, eventEmitter, agentGetFn, hooks, pluginRegistry } = ctx;
@@ -107531,7 +107973,7 @@ async function runParallelBatch(options) {
107531
107973
  }
107532
107974
  worktreePaths.set(story.id, storyWorktreePath(workdir, worktreeId));
107533
107975
  }
107534
- const rootConfigPath = path30.join(workdir, ".nax", "config.json");
107976
+ const rootConfigPath = path29.join(workdir, ".nax", "config.json");
107535
107977
  const profileOverride = profileOverrideFromConfig(config2);
107536
107978
  const storyEffectiveConfigs = new Map;
107537
107979
  const configResults = await Promise.allSettled(stories.filter((story) => storyPackageDir(story)).map(async (story) => {
@@ -108514,7 +108956,8 @@ async function run(options) {
108514
108956
  pluginRegistry,
108515
108957
  interactionChain,
108516
108958
  shutdownController,
108517
- runtime
108959
+ runtime,
108960
+ sealApprovals
108518
108961
  } = setupResult;
108519
108962
  prd = setupResult.prd;
108520
108963
  const agentManager = runtime.agentManager;
@@ -108636,7 +109079,8 @@ async function run(options) {
108636
109079
  projectKey: runtime.projectKey,
108637
109080
  curatorRollupPath: runtime.curatorRollupPath,
108638
109081
  logFilePath,
108639
- config: config2
109082
+ config: config2,
109083
+ sealApprovals
108640
109084
  });
108641
109085
  logger?.debug("execution", "Runner finally \u2014 cleanupRun done, run() returning");
108642
109086
  } finally {
@@ -109101,12 +109545,12 @@ async function checkDependenciesInstalled(workdir) {
109101
109545
  { path: "vendor" }
109102
109546
  ];
109103
109547
  const found = [];
109104
- for (const { path: path31 } of depPaths) {
109105
- const fullPath = `${workdir}/${path31}`;
109548
+ for (const { path: path30 } of depPaths) {
109549
+ const fullPath = `${workdir}/${path30}`;
109106
109550
  if (existsSync36(fullPath)) {
109107
109551
  const stats = statSync6(fullPath);
109108
109552
  if (stats.isDirectory()) {
109109
- found.push(path31);
109553
+ found.push(path30);
109110
109554
  }
109111
109555
  }
109112
109556
  }
@@ -110535,8 +110979,8 @@ var init_plan_runtime = __esm(() => {
110535
110979
  init_git_env();
110536
110980
  init_plan_helpers();
110537
110981
  _planDeps = {
110538
- readFile: (path31) => Bun.file(path31).text(),
110539
- writeFile: (path31, content) => Bun.write(path31, content).then(() => {}),
110982
+ readFile: (path30) => Bun.file(path30).text(),
110983
+ writeFile: (path30, content) => Bun.write(path30, content).then(() => {}),
110540
110984
  scanSourceRoots: (workdir) => scanSourceRoots(workdir),
110541
110985
  createRuntime: (cfg, wd, featureName) => createRuntime(cfg, wd, { featureName }),
110542
110986
  claimProjectIdentity,
@@ -110545,10 +110989,10 @@ var init_plan_runtime = __esm(() => {
110545
110989
  const result = Bun.spawnSync(cmd, opts ? { cwd: opts.cwd, ...opts.env ? { env: opts.env } : {} } : {});
110546
110990
  return { stdout: result.stdout, exitCode: result.exitCode };
110547
110991
  },
110548
- mkdirp: (path31) => Bun.spawn(["mkdir", "-p", path31]).exited.then(() => {}),
110549
- existsSync: (path31) => existsSync38(path31),
110992
+ mkdirp: (path30) => Bun.spawn(["mkdir", "-p", path30]).exited.then(() => {}),
110993
+ existsSync: (path30) => existsSync38(path30),
110550
110994
  discoverWorkspacePackages: (repoRoot) => discoverWorkspacePackages2(repoRoot),
110551
- readPackageJsonAt: (path31) => Bun.file(path31).json().catch(() => null),
110995
+ readPackageJsonAt: (path30) => Bun.file(path30).json().catch(() => null),
110552
110996
  createInteractionBridge: () => createCliInteractionBridge(),
110553
110997
  initInteractionChain: (cfg, headless) => initInteractionChain(cfg, headless),
110554
110998
  runPrecheck: async (config2, prd, opts) => {
@@ -110569,7 +111013,7 @@ function assertSpecLintClean(specContent, options) {
110569
111013
  return [];
110570
111014
  const findings = lintSpecContent(specContent, {
110571
111015
  maxAcCount: options.maxAcCount,
110572
- fileExists: (path31) => existsSync39(join118(options.workdir, path31))
111016
+ fileExists: (path30) => existsSync39(join118(options.workdir, path30))
110573
111017
  });
110574
111018
  const blocking = findings.filter((finding) => BLOCKING_SPEC_LINT_CODES.has(finding.code));
110575
111019
  if (blocking.length > 0) {
@@ -110629,7 +111073,7 @@ async function buildPlanModeContext(workdir, fullConfig, options, deps) {
110629
111073
  }));
110630
111074
  const codebaseContext = buildSourceRootsSection(normalizedRoots);
110631
111075
  const relativePackages = [
110632
- ...new Set(sourceRoots.map((root) => root.path).filter((path31) => path31 !== ".").map((path31) => path31.startsWith("/") ? path31.replace(`${workdir}/`, "") : path31))
111076
+ ...new Set(sourceRoots.map((root) => root.path).filter((path30) => path30 !== ".").map((path30) => path30.startsWith("/") ? path30.replace(`${workdir}/`, "") : path30))
110633
111077
  ];
110634
111078
  const packageDetails = relativePackages.length === 0 ? [] : await Promise.all(relativePackages.map(async (relativePath) => {
110635
111079
  const packageJson = await deps.readPackageJsonAt(join119(workdir, relativePath, "package.json"));
@@ -110772,7 +111216,7 @@ var init_persist_prd = __esm(() => {
110772
111216
  init_prd();
110773
111217
  init_finalize_routing();
110774
111218
  _persistPrdDeps = {
110775
- existsSync: (path31) => defaultExistsSync(path31),
111219
+ existsSync: (path30) => defaultExistsSync(path30),
110776
111220
  discoverWorkspacePackages: (repoRoot) => discoverWorkspacePackages2(repoRoot)
110777
111221
  };
110778
111222
  });
@@ -111035,10 +111479,10 @@ var init_plan2 = __esm(() => {
111035
111479
  });
111036
111480
 
111037
111481
  // src/cli/plugins.ts
111038
- import * as path31 from "path";
111482
+ import * as path30 from "path";
111039
111483
  async function pluginsListCommand(config2, workdir, overrideGlobalPluginsDir) {
111040
- const globalPluginsDir = overrideGlobalPluginsDir ?? path31.join(globalConfigDir(), "plugins");
111041
- const projectPluginsDir = path31.join(workdir, ".nax", "plugins");
111484
+ const globalPluginsDir = overrideGlobalPluginsDir ?? path30.join(globalConfigDir(), "plugins");
111485
+ const projectPluginsDir = path30.join(workdir, ".nax", "plugins");
111042
111486
  const configPlugins = config2.plugins || [];
111043
111487
  const registry4 = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, workdir, config2.disabledPlugins);
111044
111488
  const plugins = registry4.plugins;
@@ -111088,10 +111532,10 @@ function formatSource(type, sourcePath) {
111088
111532
  return `built-in (${sourcePath})`;
111089
111533
  }
111090
111534
  if (type === "global") {
111091
- return `global (${path31.basename(sourcePath)})`;
111535
+ return `global (${path30.basename(sourcePath)})`;
111092
111536
  }
111093
111537
  if (type === "project") {
111094
- return `project (${path31.basename(sourcePath)})`;
111538
+ return `project (${path30.basename(sourcePath)})`;
111095
111539
  }
111096
111540
  return `config (${sourcePath})`;
111097
111541
  }
@@ -111133,7 +111577,7 @@ async function exportPromptCommand(options) {
111133
111577
  var VALID_EXPORT_ROLES;
111134
111578
  var init_prompts_export = __esm(() => {
111135
111579
  init_prompts();
111136
- VALID_EXPORT_ROLES = ["test-writer", "implementer", "verifier", "single-session", "tdd-simple"];
111580
+ VALID_EXPORT_ROLES = ["test-writer", "implementer", "verifier", "tdd-simple"];
111137
111581
  });
111138
111582
 
111139
111583
  // src/cli/prompts-init.ts
@@ -111175,7 +111619,6 @@ async function autoWirePromptsConfig(workdir) {
111175
111619
  "test-writer": ".nax/templates/test-writer.md",
111176
111620
  implementer: ".nax/templates/implementer.md",
111177
111621
  verifier: ".nax/templates/verifier.md",
111178
- "single-session": ".nax/templates/single-session.md",
111179
111622
  "tdd-simple": ".nax/templates/tdd-simple.md"
111180
111623
  }
111181
111624
  }
@@ -111196,7 +111639,6 @@ ${exampleConfig}`);
111196
111639
  "test-writer": ".nax/templates/test-writer.md",
111197
111640
  implementer: ".nax/templates/implementer.md",
111198
111641
  verifier: ".nax/templates/verifier.md",
111199
- "single-session": ".nax/templates/single-session.md",
111200
111642
  "tdd-simple": ".nax/templates/tdd-simple.md"
111201
111643
  };
111202
111644
  prompts.overrides = overrides;
@@ -111255,7 +111697,6 @@ var init_prompts_init = __esm(() => {
111255
111697
  { file: "test-writer.md", role: "test-writer" },
111256
111698
  { file: "implementer.md", role: "implementer", variant: "standard" },
111257
111699
  { file: "verifier.md", role: "verifier" },
111258
- { file: "single-session.md", role: "single-session" },
111259
111700
  { file: "tdd-simple.md", role: "tdd-simple" }
111260
111701
  ];
111261
111702
  });
@@ -111493,8 +111934,8 @@ async function resolveRunProfileOverride(opts) {
111493
111934
  return cliChain;
111494
111935
  if (opts.envProfile)
111495
111936
  return;
111496
- const readJson2 = opts._readJson ?? (async (path32) => {
111497
- const file2 = Bun.file(path32);
111937
+ const readJson2 = opts._readJson ?? (async (path31) => {
111938
+ const file2 = Bun.file(path31);
111498
111939
  if (!await file2.exists())
111499
111940
  return;
111500
111941
  return file2.json();
@@ -111874,11 +112315,11 @@ var init_rules_cli_deps = __esm(() => {
111874
112315
  init_logger2();
111875
112316
  init_rules_lint();
111876
112317
  _rulesCLIDeps = {
111877
- readFile: async (path32) => Bun.file(path32).text(),
111878
- writeFile: async (path32, content) => {
111879
- await Bun.write(path32, content);
112318
+ readFile: async (path31) => Bun.file(path31).text(),
112319
+ writeFile: async (path31, content) => {
112320
+ await Bun.write(path31, content);
111880
112321
  },
111881
- fileExists: async (path32) => Bun.file(path32).exists(),
112322
+ fileExists: async (path31) => Bun.file(path31).exists(),
111882
112323
  globInDir: (dir) => {
111883
112324
  try {
111884
112325
  return [...new Bun.Glob("*.md").scanSync({ cwd: dir })].sort().map((f) => join127(dir, f));
@@ -111886,8 +112327,8 @@ var init_rules_cli_deps = __esm(() => {
111886
112327
  return [];
111887
112328
  }
111888
112329
  },
111889
- mkdir: async (path32) => {
111890
- await mkdir33(path32, { recursive: true });
112330
+ mkdir: async (path31) => {
112331
+ await mkdir33(path31, { recursive: true });
111891
112332
  },
111892
112333
  globCanonicalRuleFiles: (workdir) => _rulesLintDeps.globCanonicalRuleFiles(workdir),
111893
112334
  globHasMatch: (pattern, cwd) => _rulesLintDeps.globHasMatch(pattern, cwd),
@@ -112404,10 +112845,10 @@ var init_setup_analyze = __esm(() => {
112404
112845
  init_detect();
112405
112846
  CANONICAL_SCRIPTS = ["build", "test", "lint", "type-check", "lint:fix"];
112406
112847
  _analyzeRepoDeps = {
112407
- fileExists: async (path32) => Bun.file(path32).exists(),
112408
- readJson: async (path32) => {
112848
+ fileExists: async (path31) => Bun.file(path31).exists(),
112849
+ readJson: async (path31) => {
112409
112850
  try {
112410
- const f = Bun.file(path32);
112851
+ const f = Bun.file(path31);
112411
112852
  if (!await f.exists())
112412
112853
  return null;
112413
112854
  return JSON.parse(await f.text());
@@ -112462,9 +112903,9 @@ async function fillScripts(workdir, analysis) {
112462
112903
  var TYPE_CHECK_KEY = "type-check", TYPE_CHECK_SCRIPT = "tsc --noEmit -p tsconfig.json", TYPE_CHECK_TURBO_PASSTHROUGH = "turbo run type-check", _fillScriptsDeps;
112463
112904
  var init_setup_fill = __esm(() => {
112464
112905
  _fillScriptsDeps = {
112465
- readJson: async (path32) => {
112906
+ readJson: async (path31) => {
112466
112907
  try {
112467
- const f = Bun.file(path32);
112908
+ const f = Bun.file(path31);
112468
112909
  if (!await f.exists())
112469
112910
  return null;
112470
112911
  return JSON.parse(await f.text());
@@ -112472,8 +112913,8 @@ var init_setup_fill = __esm(() => {
112472
112913
  return null;
112473
112914
  }
112474
112915
  },
112475
- writeFile: async (path32, content) => {
112476
- await Bun.write(path32, content);
112916
+ writeFile: async (path31, content) => {
112917
+ await Bun.write(path31, content);
112477
112918
  }
112478
112919
  };
112479
112920
  });
@@ -112531,9 +112972,9 @@ async function writeSetupConfig(workdir, config2, monoConfigs, _opts, deps = _wr
112531
112972
  var _writeSetupDeps;
112532
112973
  var init_setup_write = __esm(() => {
112533
112974
  _writeSetupDeps = {
112534
- writeFile: (path32, content) => Bun.write(path32, content).then(() => {}),
112535
- mkdir: async (path32) => {
112536
- const proc = Bun.spawn(["mkdir", "-p", path32]);
112975
+ writeFile: (path31, content) => Bun.write(path31, content).then(() => {}),
112976
+ mkdir: async (path31) => {
112977
+ const proc = Bun.spawn(["mkdir", "-p", path31]);
112537
112978
  await proc.exited;
112538
112979
  }
112539
112980
  };
@@ -112617,7 +113058,7 @@ var init_setup = __esm(() => {
112617
113058
  },
112618
113059
  generateSetupPlan: (ctx, analysis) => generateSetupPlan(ctx, analysis),
112619
113060
  runGate: (workdir, config2) => runSetupGate(workdir, config2),
112620
- fileExists: (path32) => Bun.file(path32).exists(),
113061
+ fileExists: (path31) => Bun.file(path31).exists(),
112621
113062
  writeSetupConfig: (workdir, config2, monoConfigs, opts) => writeSetupConfig(workdir, config2, monoConfigs, opts),
112622
113063
  stdout: (msg) => {
112623
113064
  process.stdout.write(`${msg}
@@ -112634,7 +113075,7 @@ var init_setup = __esm(() => {
112634
113075
  import { existsSync as existsSync43 } from "fs";
112635
113076
  import { join as join135 } from "path";
112636
113077
  async function resolveSpecPaths(options, deps) {
112637
- const explicit = (options.paths ?? []).filter((path32) => path32.trim().length > 0);
113078
+ const explicit = (options.paths ?? []).filter((path31) => path31.trim().length > 0);
112638
113079
  if (explicit.length > 0)
112639
113080
  return [...explicit];
112640
113081
  if (!options.feature)
@@ -112662,7 +113103,7 @@ async function lintOne(specPath, options, deps) {
112662
113103
  }
112663
113104
  const findings = lintSpecContent(content, {
112664
113105
  maxAcCount: options.maxAcCount,
112665
- fileExists: (path32) => deps.fileExists(join135(options.dir, path32))
113106
+ fileExists: (path31) => deps.fileExists(join135(options.dir, path31))
112666
113107
  });
112667
113108
  return {
112668
113109
  specPath,
@@ -112728,7 +113169,7 @@ var init_spec_lint_command = __esm(() => {
112728
113169
  init_prd();
112729
113170
  init_features_resolve();
112730
113171
  _specLintCommandDeps = {
112731
- readFile: async (path32) => Bun.file(path32).text(),
113172
+ readFile: async (path31) => Bun.file(path31).text(),
112732
113173
  fileExists: existsSync43,
112733
113174
  write: (line) => {
112734
113175
  console.log(line);
@@ -114648,11 +115089,11 @@ var require_react_reconciler_development = __commonJS(function(exports, module)
114648
115089
  fiber = fiber.next, id--;
114649
115090
  return fiber;
114650
115091
  }
114651
- function copyWithSetImpl(obj, path32, index, value) {
114652
- if (index >= path32.length)
115092
+ function copyWithSetImpl(obj, path31, index, value) {
115093
+ if (index >= path31.length)
114653
115094
  return value;
114654
- var key = path32[index], updated = isArrayImpl(obj) ? obj.slice() : assign2({}, obj);
114655
- updated[key] = copyWithSetImpl(obj[key], path32, index + 1, value);
115095
+ var key = path31[index], updated = isArrayImpl(obj) ? obj.slice() : assign2({}, obj);
115096
+ updated[key] = copyWithSetImpl(obj[key], path31, index + 1, value);
114656
115097
  return updated;
114657
115098
  }
114658
115099
  function copyWithRename(obj, oldPath, newPath) {
@@ -114672,11 +115113,11 @@ var require_react_reconciler_development = __commonJS(function(exports, module)
114672
115113
  index + 1 === oldPath.length ? (updated[newPath[index]] = updated[oldKey], isArrayImpl(updated) ? updated.splice(oldKey, 1) : delete updated[oldKey]) : updated[oldKey] = copyWithRenameImpl(obj[oldKey], oldPath, newPath, index + 1);
114673
115114
  return updated;
114674
115115
  }
114675
- function copyWithDeleteImpl(obj, path32, index) {
114676
- var key = path32[index], updated = isArrayImpl(obj) ? obj.slice() : assign2({}, obj);
114677
- if (index + 1 === path32.length)
115116
+ function copyWithDeleteImpl(obj, path31, index) {
115117
+ var key = path31[index], updated = isArrayImpl(obj) ? obj.slice() : assign2({}, obj);
115118
+ if (index + 1 === path31.length)
114678
115119
  return isArrayImpl(updated) ? updated.splice(key, 1) : delete updated[key], updated;
114679
- updated[key] = copyWithDeleteImpl(obj[key], path32, index + 1);
115120
+ updated[key] = copyWithDeleteImpl(obj[key], path31, index + 1);
114680
115121
  return updated;
114681
115122
  }
114682
115123
  function shouldSuspendImpl() {
@@ -124700,29 +125141,29 @@ Check the top-level render call using <` + componentName2 + ">.");
124700
125141
  var didWarnAboutNestedUpdates = false;
124701
125142
  var didWarnAboutFindNodeInStrictMode = {};
124702
125143
  var overrideHookState = null, overrideHookStateDeletePath = null, overrideHookStateRenamePath = null, overrideProps = null, overridePropsDeletePath = null, overridePropsRenamePath = null, scheduleUpdate = null, scheduleRetry = null, setErrorHandler = null, setSuspenseHandler = null;
124703
- overrideHookState = function(fiber, id, path32, value) {
125144
+ overrideHookState = function(fiber, id, path31, value) {
124704
125145
  id = findHook(fiber, id);
124705
- id !== null && (path32 = copyWithSetImpl(id.memoizedState, path32, 0, value), id.memoizedState = path32, id.baseState = path32, fiber.memoizedProps = assign2({}, fiber.memoizedProps), path32 = enqueueConcurrentRenderForLane(fiber, 2), path32 !== null && scheduleUpdateOnFiber(path32, fiber, 2));
125146
+ id !== null && (path31 = copyWithSetImpl(id.memoizedState, path31, 0, value), id.memoizedState = path31, id.baseState = path31, fiber.memoizedProps = assign2({}, fiber.memoizedProps), path31 = enqueueConcurrentRenderForLane(fiber, 2), path31 !== null && scheduleUpdateOnFiber(path31, fiber, 2));
124706
125147
  };
124707
- overrideHookStateDeletePath = function(fiber, id, path32) {
125148
+ overrideHookStateDeletePath = function(fiber, id, path31) {
124708
125149
  id = findHook(fiber, id);
124709
- id !== null && (path32 = copyWithDeleteImpl(id.memoizedState, path32, 0), id.memoizedState = path32, id.baseState = path32, fiber.memoizedProps = assign2({}, fiber.memoizedProps), path32 = enqueueConcurrentRenderForLane(fiber, 2), path32 !== null && scheduleUpdateOnFiber(path32, fiber, 2));
125150
+ id !== null && (path31 = copyWithDeleteImpl(id.memoizedState, path31, 0), id.memoizedState = path31, id.baseState = path31, fiber.memoizedProps = assign2({}, fiber.memoizedProps), path31 = enqueueConcurrentRenderForLane(fiber, 2), path31 !== null && scheduleUpdateOnFiber(path31, fiber, 2));
124710
125151
  };
124711
125152
  overrideHookStateRenamePath = function(fiber, id, oldPath, newPath) {
124712
125153
  id = findHook(fiber, id);
124713
125154
  id !== null && (oldPath = copyWithRename(id.memoizedState, oldPath, newPath), id.memoizedState = oldPath, id.baseState = oldPath, fiber.memoizedProps = assign2({}, fiber.memoizedProps), oldPath = enqueueConcurrentRenderForLane(fiber, 2), oldPath !== null && scheduleUpdateOnFiber(oldPath, fiber, 2));
124714
125155
  };
124715
- overrideProps = function(fiber, path32, value) {
124716
- fiber.pendingProps = copyWithSetImpl(fiber.memoizedProps, path32, 0, value);
125156
+ overrideProps = function(fiber, path31, value) {
125157
+ fiber.pendingProps = copyWithSetImpl(fiber.memoizedProps, path31, 0, value);
124717
125158
  fiber.alternate && (fiber.alternate.pendingProps = fiber.pendingProps);
124718
- path32 = enqueueConcurrentRenderForLane(fiber, 2);
124719
- path32 !== null && scheduleUpdateOnFiber(path32, fiber, 2);
125159
+ path31 = enqueueConcurrentRenderForLane(fiber, 2);
125160
+ path31 !== null && scheduleUpdateOnFiber(path31, fiber, 2);
124720
125161
  };
124721
- overridePropsDeletePath = function(fiber, path32) {
124722
- fiber.pendingProps = copyWithDeleteImpl(fiber.memoizedProps, path32, 0);
125162
+ overridePropsDeletePath = function(fiber, path31) {
125163
+ fiber.pendingProps = copyWithDeleteImpl(fiber.memoizedProps, path31, 0);
124723
125164
  fiber.alternate && (fiber.alternate.pendingProps = fiber.pendingProps);
124724
- path32 = enqueueConcurrentRenderForLane(fiber, 2);
124725
- path32 !== null && scheduleUpdateOnFiber(path32, fiber, 2);
125165
+ path31 = enqueueConcurrentRenderForLane(fiber, 2);
125166
+ path31 !== null && scheduleUpdateOnFiber(path31, fiber, 2);
124726
125167
  };
124727
125168
  overridePropsRenamePath = function(fiber, oldPath, newPath) {
124728
125169
  fiber.pendingProps = copyWithRename(fiber.memoizedProps, oldPath, newPath);
@@ -126361,11 +126802,38 @@ init_config_profile();
126361
126802
  init_features_resolve();
126362
126803
  init_generate();
126363
126804
 
126805
+ // src/cli/run-max-iterations.ts
126806
+ function parseMaxIterationsFlag(raw) {
126807
+ if (raw === undefined)
126808
+ return { ok: true, value: undefined };
126809
+ const value = Number.parseInt(raw, 10);
126810
+ if (!Number.isFinite(value) || value < 1) {
126811
+ return { ok: false, message: "--max-iterations must be a positive integer" };
126812
+ }
126813
+ return { ok: true, value };
126814
+ }
126815
+ function applyMaxIterationsFlag(config2, flag) {
126816
+ if (flag === undefined)
126817
+ return { ...config2, execution: { ...config2.execution } };
126818
+ return { ...config2, execution: { ...config2.execution, maxIterations: flag } };
126819
+ }
126820
+
126364
126821
  // src/cli/run-mode.ts
126365
126822
  function resolveUseHeadless(input) {
126366
126823
  return !input.isTTY || input.headlessFlag || input.headlessEnv || input.formatterMode === "json";
126367
126824
  }
126368
126825
 
126826
+ // src/cli/run-parallel.ts
126827
+ function parseParallelFlag(raw) {
126828
+ if (raw === undefined)
126829
+ return { ok: true, value: undefined };
126830
+ const value = Number.parseInt(raw, 10);
126831
+ if (!Number.isFinite(value) || value < 1) {
126832
+ return { ok: false, message: "--parallel must be a positive integer (omit it to run sequentially)" };
126833
+ }
126834
+ return { ok: true, value };
126835
+ }
126836
+
126369
126837
  // bin/nax.ts
126370
126838
  init_status_dispatch();
126371
126839
 
@@ -126384,14 +126852,14 @@ function resolveEffective(detected, configPatterns) {
126384
126852
  return "detected";
126385
126853
  return "none";
126386
126854
  }
126387
- async function loadRawConfig(path32) {
126388
- const f = Bun.file(path32);
126855
+ async function loadRawConfig(path31) {
126856
+ const f = Bun.file(path31);
126389
126857
  if (!await f.exists())
126390
126858
  return {};
126391
126859
  return JSON.parse(await f.text());
126392
126860
  }
126393
- async function writeRawConfig(path32, data) {
126394
- await Bun.write(path32, `${JSON.stringify(data, null, 2)}
126861
+ async function writeRawConfig(path31, data) {
126862
+ await Bun.write(path31, `${JSON.stringify(data, null, 2)}
126395
126863
  `);
126396
126864
  }
126397
126865
  function deepSet(obj, keyPath, value) {
@@ -131475,8 +131943,8 @@ function Text({ color, backgroundColor, dimColor = false, bold = false, italic =
131475
131943
  }
131476
131944
 
131477
131945
  // node_modules/ink/build/components/ErrorOverview.js
131478
- var cleanupPath = (path32) => {
131479
- return path32?.replace(`file://${cwd()}/`, "");
131946
+ var cleanupPath = (path31) => {
131947
+ return path31?.replace(`file://${cwd()}/`, "");
131480
131948
  };
131481
131949
  var stackUtils = new import_stack_utils.default({
131482
131950
  cwd: cwd(),
@@ -134517,7 +134985,7 @@ program2.command("setup").description("Analyze repo and generate .nax/config.jso
134517
134985
  });
134518
134986
  process.exit(exitCode);
134519
134987
  });
134520
- program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--max-cost <usd>", "Override cost limit (USD) for this run \u2014 aborts execution when exceeded").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--no-spec-lint", "Plan even when the spec declares sections that extract to nothing").option("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false).option("--force", "Force overwrite existing prd.json when using --plan", false).option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).option("--profile <name>", "Profile(s) to overlay (comma-separated or repeated; later overrides earlier)", collectProfile, []).option("--schedule <when>", "Defer run start until <when> (e.g. 30m, 1h30m, 17:00, 2026-07-02T02:00)").option("--compare <agents>", "Bake-off mode: comma-separated list of contestant agents (e.g. claude,codex)").option("--fresh", "Ignore any existing checkpoint.jsonl and re-run every incomplete story from scratch", false).addOption(new Option("--no-resume", "Alias for --fresh: never auto-resume from a prior checkpoint").default("__UNSET__")).action(async (options) => {
134988
+ program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations").option("--max-cost <usd>", "Override cost limit (USD) for this run \u2014 aborts execution when exceeded").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (omit = sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--no-spec-lint", "Plan even when the spec declares sections that extract to nothing").option("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false).option("--force", "Force overwrite existing prd.json when using --plan", false).option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).option("--profile <name>", "Profile(s) to overlay (comma-separated or repeated; later overrides earlier)", collectProfile, []).option("--schedule <when>", "Defer run start until <when> (e.g. 30m, 1h30m, 17:00, 2026-07-02T02:00)").option("--compare <agents>", "Bake-off mode: comma-separated list of contestant agents (e.g. claude,codex)").option("--fresh", "Ignore any existing checkpoint.jsonl and re-run every incomplete story from scratch", false).addOption(new Option("--no-resume", "Alias for --fresh: never auto-resume from a prior checkpoint").default("__UNSET__")).action(async (options) => {
134521
134989
  try {
134522
134990
  validateFeatureName(options.feature);
134523
134991
  } catch (err) {
@@ -134531,6 +134999,17 @@ program2.command("run").description("Run the orchestration loop for a feature").
134531
134999
  console.error(source_default.red(`Invalid directory: ${err.message}`));
134532
135000
  process.exit(1);
134533
135001
  }
135002
+ const maxIterationsFlag = parseMaxIterationsFlag(options.maxIterations);
135003
+ if (!maxIterationsFlag.ok) {
135004
+ console.error(source_default.red(maxIterationsFlag.message));
135005
+ process.exit(1);
135006
+ }
135007
+ const parallelFlag = parseParallelFlag(options.parallel);
135008
+ if (!parallelFlag.ok) {
135009
+ console.error(source_default.red(parallelFlag.message));
135010
+ process.exit(1);
135011
+ }
135012
+ const parallel = parallelFlag.value;
134534
135013
  try {
134535
135014
  const { assertCompareAgentExclusive: assertCompareAgentExclusive2 } = await Promise.resolve().then(() => (init_preflight(), exports_preflight));
134536
135015
  assertCompareAgentExclusive2({ compare: options.compare, agent: options.agent });
@@ -134720,12 +135199,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
134720
135199
  config2.agent ??= {};
134721
135200
  config2.agent.default = options.agent;
134722
135201
  }
134723
- const maxIterations = Number.parseInt(options.maxIterations, 10);
134724
- if (!Number.isFinite(maxIterations) || maxIterations < 1) {
134725
- console.error(source_default.red("--max-iterations must be a positive integer"));
134726
- process.exit(1);
134727
- }
134728
- config2.execution.maxIterations = maxIterations;
135202
+ config2 = applyMaxIterationsFlag(config2, maxIterationsFlag.value);
134729
135203
  if (options.maxCost !== undefined) {
134730
135204
  const maxCost = Number(options.maxCost);
134731
135205
  if (!Number.isFinite(maxCost) || maxCost <= 0) {
@@ -134765,15 +135239,6 @@ program2.command("run").description("Run the orchestration loop for a feature").
134765
135239
  console.log(source_default.dim(" [Headless mode \u2014 pipe output]"));
134766
135240
  }
134767
135241
  const statusFilePath = join142(outputDir, "status.json");
134768
- let parallel;
134769
- if (options.parallel !== undefined) {
134770
- parallel = Number.parseInt(options.parallel, 10);
134771
- if (Number.isNaN(parallel) || parallel < 0) {
134772
- tuiInstance?.unmount();
134773
- console.error(source_default.red("--parallel must be a non-negative integer"));
134774
- process.exit(1);
134775
- }
134776
- }
134777
135242
  if (scheduleGate.target) {
134778
135243
  const scheduleController = new AbortController;
134779
135244
  const onSigint = () => scheduleController.abort();
@@ -135205,8 +135670,8 @@ configProfileCmd.command("current").description("Show the currently active profi
135205
135670
  });
135206
135671
  configProfileCmd.command("create <name>").description("Create a new empty profile").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (name, options) => {
135207
135672
  try {
135208
- const path32 = await profileCreateCommand(name, options.dir);
135209
- console.log(`Created profile at: ${path32}`);
135673
+ const path31 = await profileCreateCommand(name, options.dir);
135674
+ console.log(`Created profile at: ${path31}`);
135210
135675
  } catch (err) {
135211
135676
  console.error(source_default.red(`Error: ${err.message}`));
135212
135677
  process.exit(1);