@nathapp/nax 0.88.0-canary.2 → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/nax.js +1052 -565
  2. package/package.json +3 -3
package/dist/nax.js CHANGED
@@ -12889,6 +12889,212 @@ function hasNaxDirNear(ctx) {
12889
12889
  var MAX_NESTED_DEPTH = 3, NEGATIVE_TTL_MS = 1000;
12890
12890
  var init_nax_dirs = () => {};
12891
12891
 
12892
+ // ../nax-agent/src/tools/bash-canonical.ts
12893
+ function refused(reason) {
12894
+ return { kind: "refused", reason: `Bash canonical analysis refused: ${reason}` };
12895
+ }
12896
+ function optionWidth(word, options) {
12897
+ if (options.flags.includes(word))
12898
+ return 1;
12899
+ if (options.values.includes(word))
12900
+ return 2;
12901
+ if (options.values.some((flag) => flag.startsWith("--") ? word.startsWith(`${flag}=`) : word.startsWith(flag) && word.length > flag.length))
12902
+ return 1;
12903
+ if (/^-[A-Za-z0-9]+$/.test(word) && [...word.slice(1)].every((letter) => options.flags.includes(`-${letter}`)))
12904
+ return 1;
12905
+ return;
12906
+ }
12907
+ function afterOptions(tokens, start, options) {
12908
+ let i = start;
12909
+ while (tokens[i]?.text.startsWith("-")) {
12910
+ const token = tokens[i];
12911
+ if (token.opaque)
12912
+ return "opaque wrapper option";
12913
+ if (token.text === "--")
12914
+ return i + 1;
12915
+ const width = optionWidth(token.text, options);
12916
+ if (width === undefined)
12917
+ return `unknown option ${token.text}`;
12918
+ if (width === 2 && tokens[i + 1] === undefined)
12919
+ return `missing value for ${token.text}`;
12920
+ i += width;
12921
+ }
12922
+ return i;
12923
+ }
12924
+ function shellPayload(tokens) {
12925
+ for (let i = 1;i < tokens.length; i += 1) {
12926
+ const token = tokens[i];
12927
+ const word = token.text;
12928
+ if (!word.startsWith("-") && !word.startsWith("+") || word === "--")
12929
+ return;
12930
+ if (token.opaque)
12931
+ return "opaque shell option";
12932
+ if (word === "-o" || word === "+o" || word === "-O" || word === "--rcfile" || word === "--init-file") {
12933
+ i += 1;
12934
+ continue;
12935
+ }
12936
+ if (/^-[A-Za-z]*c[A-Za-z]*$/.test(word))
12937
+ return tokens[i + 1] ?? "missing shell command string";
12938
+ }
12939
+ return;
12940
+ }
12941
+ function stripPrefixes(input) {
12942
+ let tokens = input;
12943
+ for (;; ) {
12944
+ const head = tokens[0];
12945
+ if (head === undefined)
12946
+ return [];
12947
+ if (head.opaque)
12948
+ return "opaque command head";
12949
+ if (KEYWORDS.has(head.text) || ASSIGNMENT3.test(head.text)) {
12950
+ tokens = tokens.slice(1);
12951
+ continue;
12952
+ }
12953
+ const wrapper = Object.hasOwn(WRAPPERS, head.text) ? WRAPPERS[head.text] : undefined;
12954
+ if (wrapper === undefined)
12955
+ break;
12956
+ const next = afterOptions(tokens, 1, wrapper);
12957
+ if (typeof next === "string")
12958
+ return `${head.text}: ${next}`;
12959
+ const command = head.text === "timeout" ? next + 1 : next;
12960
+ if (head.text === "timeout" && tokens[next] === undefined)
12961
+ return "missing timeout duration";
12962
+ tokens = tokens.slice(command);
12963
+ }
12964
+ return tokens;
12965
+ }
12966
+ function expandShell(payload, depth) {
12967
+ if (depth >= 3)
12968
+ return refused("shell command strings nest deeper than 3 levels");
12969
+ if (payload.opaque)
12970
+ return refused("opaque shell command string");
12971
+ const lexed = lexBashCommand(payload.text);
12972
+ if (lexed.kind === "refused")
12973
+ return refused(`shell command string contains ${lexed.construct}`);
12974
+ const segments = [];
12975
+ for (const nested of lexed.segments) {
12976
+ const result = canonicalise2(nested, depth + 1);
12977
+ if (result.kind === "refused")
12978
+ return result;
12979
+ segments.push(...result.segments);
12980
+ }
12981
+ return { kind: "ok", segments };
12982
+ }
12983
+ function canonicalise2(segment, depth) {
12984
+ const stripped = stripPrefixes(segment.tokens);
12985
+ if (typeof stripped === "string")
12986
+ return refused(stripped);
12987
+ let tokens = stripped;
12988
+ if (tokens.length === 0)
12989
+ return { kind: "ok", segments: [] };
12990
+ if (tokens.length === 1 && CLOSERS.has(tokens[0]?.text ?? ""))
12991
+ return { kind: "ok", segments: [] };
12992
+ if (tokens[0]?.text === "git") {
12993
+ const next = afterOptions(tokens, 1, GIT_OPTIONS);
12994
+ if (typeof next === "string")
12995
+ return refused(`git: ${next}`);
12996
+ tokens = [tokens[0], ...tokens.slice(next)];
12997
+ }
12998
+ const head = tokens[0]?.text.split("/").pop() ?? "";
12999
+ if (SHELLS.has(head)) {
13000
+ const payload = shellPayload(tokens);
13001
+ if (typeof payload === "string")
13002
+ return refused(payload);
13003
+ if (payload !== undefined)
13004
+ return expandShell(payload, depth);
13005
+ }
13006
+ return { kind: "ok", segments: [{ ...segment, tokens }] };
13007
+ }
13008
+ function canonicalSegments(segment) {
13009
+ return canonicalise2(segment, 0);
13010
+ }
13011
+ var WRAPPERS, GIT_OPTIONS, KEYWORDS, CLOSERS, SHELLS, ASSIGNMENT3;
13012
+ var init_bash_canonical = __esm(() => {
13013
+ init_permissions();
13014
+ WRAPPERS = {
13015
+ env: { flags: ["-i", "--ignore-environment"], values: ["-u", "--unset"] },
13016
+ command: { flags: ["-p"], values: [] },
13017
+ exec: { flags: ["-c", "-l"], values: ["-a"] },
13018
+ time: { flags: ["-p"], values: [] },
13019
+ nohup: { flags: [], values: [] },
13020
+ nice: { flags: [], values: ["-n", "--adjustment"] },
13021
+ timeout: {
13022
+ flags: ["--foreground", "--preserve-status", "-v", "--verbose"],
13023
+ values: ["-k", "--kill-after", "-s", "--signal"]
13024
+ },
13025
+ stdbuf: { flags: [], values: ["-i", "-o", "-e", "--input", "--output", "--error"] },
13026
+ xargs: {
13027
+ flags: ["-0", "--null", "-r", "--no-run-if-empty", "-t", "--verbose", "-p", "--interactive", "-x", "--exit"],
13028
+ values: [
13029
+ "-I",
13030
+ "-n",
13031
+ "-P",
13032
+ "-d",
13033
+ "-E",
13034
+ "-L",
13035
+ "-s",
13036
+ "-a",
13037
+ "--replace",
13038
+ "--max-args",
13039
+ "--max-procs",
13040
+ "--delimiter",
13041
+ "--eof",
13042
+ "--max-lines",
13043
+ "--max-chars",
13044
+ "--arg-file"
13045
+ ]
13046
+ },
13047
+ busybox: { flags: [], values: [] },
13048
+ sudo: {
13049
+ flags: [
13050
+ "-A",
13051
+ "-b",
13052
+ "-E",
13053
+ "-H",
13054
+ "-K",
13055
+ "-k",
13056
+ "-n",
13057
+ "-S",
13058
+ "--askpass",
13059
+ "--background",
13060
+ "--preserve-env",
13061
+ "--set-home",
13062
+ "--non-interactive",
13063
+ "--stdin"
13064
+ ],
13065
+ values: [
13066
+ "-u",
13067
+ "-g",
13068
+ "-h",
13069
+ "-D",
13070
+ "-C",
13071
+ "-p",
13072
+ "-r",
13073
+ "-t",
13074
+ "-U",
13075
+ "--user",
13076
+ "--group",
13077
+ "--host",
13078
+ "--chdir",
13079
+ "--close-from",
13080
+ "--prompt",
13081
+ "--role",
13082
+ "--type",
13083
+ "--other-user"
13084
+ ]
13085
+ },
13086
+ doas: { flags: ["-n", "-s", "-L"], values: ["-u", "-C"] }
13087
+ };
13088
+ GIT_OPTIONS = {
13089
+ flags: ["--no-pager", "-p", "--paginate", "--bare", "--literal-pathspecs", "--no-replace-objects"],
13090
+ values: ["-C", "-c", "--git-dir", "--work-tree", "--namespace"]
13091
+ };
13092
+ KEYWORDS = new Set(["if", "then", "else", "elif", "do", "while", "until", "{"]);
13093
+ CLOSERS = new Set(["fi", "done", "}"]);
13094
+ SHELLS = new Set(["sh", "bash", "dash", "zsh", "ksh"]);
13095
+ ASSIGNMENT3 = /^[A-Za-z_][A-Za-z0-9_]*=/;
13096
+ });
13097
+
12892
13098
  // ../nax-agent/src/tools/bash-cwd.ts
12893
13099
  function cdTargetsFor(segment, cwd, resolvePath) {
12894
13100
  const words = segment.tokens.map((token) => token.text);
@@ -13366,10 +13572,23 @@ function pathIsClean(spec, ctx) {
13366
13572
  function stashIsExempt(args, ctx) {
13367
13573
  if (args[0] === "pop" || args[0] === "apply")
13368
13574
  return restoreIsExempt(args.slice(1), ctx);
13575
+ if (args[0] !== "push")
13576
+ return false;
13369
13577
  const separator = args.indexOf("--");
13370
- if (args[0] !== "push" || separator === -1)
13578
+ const options = args.slice(1, separator === -1 ? args.length : separator);
13579
+ if (options.some((word) => word === "--pathspec-from-file" || word.startsWith("--pathspec-from-file="))) {
13371
13580
  return false;
13372
- const specs = args.slice(separator + 1);
13581
+ }
13582
+ const specs = [];
13583
+ for (let i = 0;i < options.length; i += 1) {
13584
+ const word = options[i];
13585
+ if (word === "-m" || word === "--message")
13586
+ i += 1;
13587
+ else if (!word.startsWith("-"))
13588
+ specs.push(word);
13589
+ }
13590
+ if (separator !== -1)
13591
+ specs.push(...args.slice(separator + 1));
13373
13592
  if (specs.some((spec) => isFullNaxExclusion(spec, ctx)))
13374
13593
  return true;
13375
13594
  return specs.length > 0 && specs.every((spec) => pathIsClean(spec, ctx));
@@ -13393,6 +13612,41 @@ var init_nax_stash_restore = __esm(() => {
13393
13612
  };
13394
13613
  });
13395
13614
 
13615
+ // ../nax-agent/src/tools/nax-verb-gaps.ts
13616
+ function verbGapsRefusal(verb, args, ctx) {
13617
+ if (!PATH_VERBS.has(verb) && !AUTOSTASH_VERBS.has(verb) && verb !== "sparse-checkout")
13618
+ return;
13619
+ const nax = findNaxDirs(ctx);
13620
+ if (nax.length === 0)
13621
+ return;
13622
+ if (AUTOSTASH_VERBS.has(verb)) {
13623
+ const flags = splitOperands(verb, args).flags;
13624
+ return flags.includes("--autostash") ? NAX_OWNED_RUN_STATE_EXPLANATION : undefined;
13625
+ }
13626
+ if (verb === "sparse-checkout") {
13627
+ return args[0] === "list" ? undefined : NAX_OWNED_RUN_STATE_EXPLANATION;
13628
+ }
13629
+ const ops = splitOperands(verb, args);
13630
+ const specs = [...ops.before, ...ops.after].filter((spec) => !isExclusionSpec(spec));
13631
+ if (specs.some((spec) => /[*?[]/.test(spec))) {
13632
+ return `${NAX_OWNED_RUN_STATE_EXPLANATION} Glob pathspecs may also select files under \`.nax/\`; name literal paths instead.`;
13633
+ }
13634
+ if (verb === "rm" || verb === "reset") {
13635
+ if (specs.some((spec) => operandTouchesNax(spec, ctx) || coversNax(spec, ctx, nax))) {
13636
+ return NAX_OWNED_RUN_STATE_EXPLANATION;
13637
+ }
13638
+ }
13639
+ return;
13640
+ }
13641
+ var PATH_VERBS, AUTOSTASH_VERBS;
13642
+ var init_nax_verb_gaps = __esm(() => {
13643
+ init_denial_redirect();
13644
+ init_nax_dirs();
13645
+ init_nax_revert();
13646
+ PATH_VERBS = new Set(["checkout", "restore", "rm", "reset"]);
13647
+ AUTOSTASH_VERBS = new Set(["rebase", "pull", "merge"]);
13648
+ });
13649
+
13396
13650
  // ../nax-agent/src/tools/nax-state-guard.ts
13397
13651
  import { resolve as resolve15 } from "path";
13398
13652
  function withGitDirs(ctx, dirs) {
@@ -13422,7 +13676,7 @@ function stashRefusal(args, ctx) {
13422
13676
  function shellCommandString(words) {
13423
13677
  const head = commandHeadIndex(words);
13424
13678
  const shell = words[head]?.split("/").pop();
13425
- if (shell === undefined || !SHELLS.has(shell))
13679
+ if (shell === undefined || !SHELLS2.has(shell))
13426
13680
  return;
13427
13681
  const flag = shell === "eval" ? head : words.findIndex((word, i) => i > head && /^-[A-Za-z]*c[A-Za-z]*$/.test(word));
13428
13682
  if (flag === -1 || flag + 1 >= words.length)
@@ -13430,6 +13684,9 @@ function shellCommandString(words) {
13430
13684
  return words.slice(flag + 1).join(" ").replace(/^\s*['"]|['"]\s*$/g, "");
13431
13685
  }
13432
13686
  function verbRefusal(git, segment, where) {
13687
+ const gaps = verbGapsRefusal(git.subcommand, git.args, where);
13688
+ if (gaps !== undefined)
13689
+ return gaps;
13433
13690
  if (git.literalPathspecs && EXCLUDING_VERBS.has(git.subcommand) && git.args.some((a) => a.startsWith(":"))) {
13434
13691
  if (hasNaxDirNear(where))
13435
13692
  return LITERAL_PATHSPECS_REFUSAL;
@@ -13595,14 +13852,14 @@ function stashIsSafeWord(words) {
13595
13852
  function mayRunItsStrings(words) {
13596
13853
  const head = baseName(words[commandHeadIndex(words)]);
13597
13854
  const lead = baseName(leadWord(words));
13598
- return [head, lead].some((n) => SHELLS.has(n) || EXEC_STYLE_HEADS.has(n) || isInterpreterName(n));
13855
+ return [head, lead].some((n) => SHELLS2.has(n) || EXEC_STYLE_HEADS.has(n) || isInterpreterName(n));
13599
13856
  }
13600
13857
  function nestedRefusal(words, ctx) {
13601
13858
  const git = locateGit(words);
13602
13859
  const readableGit = git !== undefined && !git.uncertain;
13603
13860
  if (!runsNestedShell(words) && !mayRunItsStrings(words))
13604
13861
  return;
13605
- const shellHead = SHELLS.has(baseName(words[commandHeadIndex(words)]));
13862
+ const shellHead = SHELLS2.has(baseName(words[commandHeadIndex(words)]));
13606
13863
  if (!shellHead && (readableGit || !mentionsGuardedVerb(words.join(" "))))
13607
13864
  return;
13608
13865
  for (const word of words) {
@@ -13632,7 +13889,7 @@ function naxStateRefusalInText(command, ctx) {
13632
13889
  }
13633
13890
  return;
13634
13891
  }
13635
- var STASH_SAFE_SUBCOMMANDS, INDEX_HIDING_FLAG, SHELLS, UPDATE_INDEX_REFUSAL, LITERAL_PATHSPECS_REFUSAL, STASH_REFUSAL, ALIAS_REFUSAL, DISCARD_VERBS, EXCLUDING_VERBS, HEREDOC_MARKER, baseName = (word) => (word ?? "").split("/").pop(), EXEC_STYLE_HEADS, isInterpreterName = (name) => /^(?:python[\d.]*|lua[\d.]*)$/.test(name), leadWord = (words) => words.find((w) => !/^[A-Za-z_]\w*=/.test(w)), runsNestedShell = (words) => words.some((word) => SHELLS.has(baseName(word)) || /^-[A-Za-z]*c[A-Za-z]*$/.test(word));
13892
+ var STASH_SAFE_SUBCOMMANDS, INDEX_HIDING_FLAG, SHELLS2, UPDATE_INDEX_REFUSAL, LITERAL_PATHSPECS_REFUSAL, STASH_REFUSAL, ALIAS_REFUSAL, DISCARD_VERBS, EXCLUDING_VERBS, HEREDOC_MARKER, baseName = (word) => (word ?? "").split("/").pop(), EXEC_STYLE_HEADS, isInterpreterName = (name) => /^(?:python[\d.]*|lua[\d.]*)$/.test(name), leadWord = (words) => words.find((w) => !/^[A-Za-z_]\w*=/.test(w)), runsNestedShell = (words) => words.some((word) => SHELLS2.has(baseName(word)) || /^-[A-Za-z]*c[A-Za-z]*$/.test(word));
13636
13893
  var init_nax_state_guard = __esm(() => {
13637
13894
  init_denial_redirect();
13638
13895
  init_locate();
@@ -13640,10 +13897,11 @@ var init_nax_state_guard = __esm(() => {
13640
13897
  init_nax_evasion();
13641
13898
  init_nax_revert();
13642
13899
  init_nax_stash_restore();
13900
+ init_nax_verb_gaps();
13643
13901
  init_nax_dirs();
13644
13902
  STASH_SAFE_SUBCOMMANDS = new Set(["list", "show", "create", "store", "drop", "clear"]);
13645
13903
  INDEX_HIDING_FLAG = /^--(?:no-)?(?:sk|as)/;
13646
- SHELLS = new Set(["bash", "sh", "zsh", "dash", "ksh", "eval"]);
13904
+ SHELLS2 = new Set(["bash", "sh", "zsh", "dash", "ksh", "eval"]);
13647
13905
  UPDATE_INDEX_REFUSAL = `${NAX_OWNED_RUN_STATE_EXPLANATION} ` + "Marking it with `git update-index --skip-worktree` or `--assume-unchanged` hides it from git and from nax's own commits, " + "and the flag outlives the run. To see whether something pre-existed, use `git diff HEAD -- <file>` or `git show HEAD:<file>`.";
13648
13906
  LITERAL_PATHSPECS_REFUSAL = "`--literal-pathspecs` / `GIT_LITERAL_PATHSPECS` turn every `:(...)` and `:!` exclusion into a literal path, " + "so nothing keeps nax's run state under `.nax/` out of this command. Drop the literal-pathspec setting.";
13649
13907
  STASH_REFUSAL = "`git stash` is refused while the repository holds nax's run state under `.nax/`: the stash would capture " + "nax's uncommitted state and a later pop would replay stale copies over what nax has written since. " + "To check whether something pre-existed, use `git diff HEAD -- <file>` or `git show HEAD:<file>`. " + `A \`git stash push -- . ${COVERING_EXCLUSION}\` is allowed. ` + "`push` of files or directories outside `.nax` is allowed, and so is `pop` or `apply` of a stash that holds no `.nax` path (quote the ref, e.g. `'stash@{1}'`).";
@@ -13809,6 +14067,33 @@ function lexicalDenial(lexed) {
13809
14067
  return;
13810
14068
  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);
13811
14069
  }
14070
+ function matchingDenial(args, segments) {
14071
+ const entry = args.denyEntry;
14072
+ if (entry === undefined)
14073
+ return;
14074
+ for (const segment of segments) {
14075
+ if (segment.tokens[0]?.text === "eval")
14076
+ return deny("Bash canonical analysis refused: eval cannot be analysed");
14077
+ if (matchesSegment(entry, segment)) {
14078
+ return deny(`${args.tool} segment "${render2(segment)}" is denied for this stage by rule ${ruleExpr(args.tool, entry, segment)}`);
14079
+ }
14080
+ }
14081
+ return;
14082
+ }
14083
+ function ruleSegments(segments, refuseUnreadable) {
14084
+ const views = [];
14085
+ for (const segment of segments) {
14086
+ views.push(segment);
14087
+ const canonical = canonicalSegments(segment);
14088
+ if (canonical.kind === "refused") {
14089
+ if (refuseUnreadable)
14090
+ return canonical;
14091
+ } else {
14092
+ views.push(...canonical.segments);
14093
+ }
14094
+ }
14095
+ return { kind: "ok", segments: views };
14096
+ }
13812
14097
  function checkBashCommand(args) {
13813
14098
  const { command, tool } = args;
13814
14099
  const invalid = invalidCommandDenial(command);
@@ -13819,11 +14104,13 @@ function checkBashCommand(args) {
13819
14104
  return state;
13820
14105
  const lexed = lexBashCommand(command);
13821
14106
  const segments = lexed.kind === "ok" ? lexed.segments : lexed.prefix;
13822
- for (const segment of segments) {
13823
- if (args.denyEntry !== undefined && matchesSegment(args.denyEntry, segment)) {
13824
- return deny(`${tool} segment "${render2(segment)}" is denied for this stage by rule ${ruleExpr(tool, args.denyEntry, segment)}`);
13825
- }
13826
- }
14107
+ const views = ruleSegments(segments, args.denyEntry !== undefined);
14108
+ if (views.kind === "refused")
14109
+ return deny(views.reason);
14110
+ const matchingSegments = views.segments;
14111
+ const denied = matchingDenial(args, matchingSegments);
14112
+ if (denied !== undefined)
14113
+ return denied;
13827
14114
  let cwd = [args.initialPath];
13828
14115
  for (const segment of segments) {
13829
14116
  const result = checkPayload(args, segment, cwd);
@@ -13841,7 +14128,7 @@ function checkBashCommand(args) {
13841
14128
  const alternatives = granted === "" ? "no command forms are granted for this stage" : `granted forms: ${granted}`;
13842
14129
  return deny(`${tool} is not granted "${render2(segment)}" -- ${alternatives}`, false, true);
13843
14130
  }
13844
- for (const segment of segments) {
14131
+ for (const segment of matchingSegments) {
13845
14132
  if (args.askEntry !== undefined && matchesSegment(args.askEntry, segment)) {
13846
14133
  return { kind: "ask", rule: ruleExpr(tool, args.askEntry, segment) };
13847
14134
  }
@@ -13850,6 +14137,7 @@ function checkBashCommand(args) {
13850
14137
  }
13851
14138
  var init_policy_bash = __esm(() => {
13852
14139
  init_permissions();
14140
+ init_bash_canonical();
13853
14141
  init_exec_guard();
13854
14142
  init_nax_revert();
13855
14143
  init_nax_state_guard();
@@ -17415,24 +17703,39 @@ function turnRetryFitsBudget(delayMs, remainingMs) {
17415
17703
  return remainingMs === undefined || delayMs <= remainingMs;
17416
17704
  }
17417
17705
  function abortError(signal) {
17418
- return signal.reason ?? new DOMException("Aborted", "AbortError");
17706
+ return signal?.reason ?? new DOMException("Aborted", "AbortError");
17419
17707
  }
17420
17708
  function abortableSleep(ms, sleep, signal) {
17421
17709
  if (signal === undefined)
17422
- return sleep(ms);
17710
+ return sleep(ms, signal);
17423
17711
  if (signal.aborted)
17424
17712
  return Promise.reject(abortError(signal));
17425
17713
  return new Promise((resolve, reject) => {
17426
17714
  const onAbort = () => reject(abortError(signal));
17427
17715
  signal.addEventListener("abort", onAbort, { once: true });
17428
- sleep(ms).then(() => {
17716
+ Promise.resolve().then(() => sleep(ms, signal)).then(() => {
17429
17717
  signal.removeEventListener("abort", onAbort);
17430
17718
  resolve();
17431
- }, reject);
17719
+ }, (error) => {
17720
+ signal.removeEventListener("abort", onAbort);
17721
+ reject(error);
17722
+ });
17432
17723
  });
17433
17724
  }
17434
- function realSleep(ms) {
17435
- return new Promise((resolve) => setTimeout(resolve, ms));
17725
+ function realSleep(ms, signal) {
17726
+ if (signal?.aborted)
17727
+ return Promise.reject(abortError(signal));
17728
+ return new Promise((resolve, reject) => {
17729
+ const onAbort = () => {
17730
+ clearTimeout(timer);
17731
+ reject(abortError(signal));
17732
+ };
17733
+ const timer = setTimeout(() => {
17734
+ signal?.removeEventListener("abort", onAbort);
17735
+ resolve();
17736
+ }, ms);
17737
+ signal?.addEventListener("abort", onAbort, { once: true });
17738
+ });
17436
17739
  }
17437
17740
  async function retryTransportFault(firstError, deps) {
17438
17741
  const { sleep } = deps;
@@ -22714,6 +23017,25 @@ class SinkRegistry {
22714
23017
  // src/logger/logger.ts
22715
23018
  import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
22716
23019
  import { appendFile as appendFile3 } from "fs/promises";
23020
+ function flushDeferredWarnings() {
23021
+ for (const { stage, message } of deferredWarnings.splice(0)) {
23022
+ _loggerDeps.writeStderr(`nax warning (${stage}): ${message}
23023
+ `);
23024
+ }
23025
+ }
23026
+ function warnOrDefer(stage, message) {
23027
+ if (instance !== null) {
23028
+ instance.warn(stage, message);
23029
+ return;
23030
+ }
23031
+ if (deferredWarnings.some((item) => item.stage === stage && item.message === message))
23032
+ return;
23033
+ deferredWarnings.push({ stage, message });
23034
+ if (!warningExitRegistered) {
23035
+ warningExitRegistered = true;
23036
+ _loggerDeps.onExit(flushDeferredWarnings);
23037
+ }
23038
+ }
22717
23039
 
22718
23040
  class Logger {
22719
23041
  level;
@@ -22898,6 +23220,9 @@ function initLogger(options = { level: "silent" }) {
22898
23220
  }
22899
23221
  instance = new Logger(options);
22900
23222
  setAgentLogger(instance);
23223
+ for (const { stage, message } of deferredWarnings.splice(0)) {
23224
+ instance.warn(stage, message);
23225
+ }
22901
23226
  if (options.filePath && !exitFlushRegistered) {
22902
23227
  exitFlushRegistered = true;
22903
23228
  process.on("exit", () => instance?.flushSync());
@@ -22924,7 +23249,7 @@ function resetLogger() {
22924
23249
  instance = null;
22925
23250
  setAgentLogger(null);
22926
23251
  }
22927
- var LOG_LEVEL_PRIORITY, MAX_BATCH_BYTES, instance = null, exitFlushRegistered = false, noopLogger2;
23252
+ var LOG_LEVEL_PRIORITY, MAX_BATCH_BYTES, instance = null, exitFlushRegistered = false, _loggerDeps, deferredWarnings, warningExitRegistered = false, noopLogger2;
22928
23253
  var init_logger = __esm(() => {
22929
23254
  init_internal();
22930
23255
  init_errors4();
@@ -22938,6 +23263,15 @@ var init_logger = __esm(() => {
22938
23263
  debug: 3
22939
23264
  };
22940
23265
  MAX_BATCH_BYTES = 64 * 1024;
23266
+ _loggerDeps = {
23267
+ onExit: (handler) => {
23268
+ process.once("exit", handler);
23269
+ },
23270
+ writeStderr: (text) => {
23271
+ process.stderr.write(text);
23272
+ }
23273
+ };
23274
+ deferredWarnings = [];
22941
23275
  noopLogger2 = new Logger({ level: "silent", useChalk: false, headless: false });
22942
23276
  });
22943
23277
 
@@ -23396,9 +23730,7 @@ function migrateLegacyNonBlockingFix(raw, logger) {
23396
23730
 
23397
23731
  // src/config/compat-shims.ts
23398
23732
  function defaultConfigWarn(msg) {
23399
- try {
23400
- getLogger2().warn("config", msg);
23401
- } catch {}
23733
+ warnOrDefer("config", msg);
23402
23734
  }
23403
23735
  function createConfigWarnDedupe(sink = defaultConfigWarn) {
23404
23736
  const seen = new Set;
@@ -24985,7 +25317,7 @@ function resolveWorkflowConfig(config) {
24985
25317
  return config?.workflow ?? WORKFLOW_DEFAULTS;
24986
25318
  }
24987
25319
  function isWorkflowMode(config) {
24988
- return config?.execution?.mode === "workflow";
25320
+ return config !== undefined && (config.execution?.mode ?? "workflow") === "workflow";
24989
25321
  }
24990
25322
  var ExecutionModeSchema, WorkflowConfigObjectSchema, WORKFLOW_DEFAULTS;
24991
25323
  var init_schemas_workflow = __esm(() => {
@@ -48222,7 +48554,7 @@ var package_default;
48222
48554
  var init_package = __esm(() => {
48223
48555
  package_default = {
48224
48556
  name: "@nathapp/nax",
48225
- version: "0.88.0-canary.2",
48557
+ version: "0.88.0",
48226
48558
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
48227
48559
  type: "module",
48228
48560
  bin: {
@@ -48278,7 +48610,7 @@ var init_package = __esm(() => {
48278
48610
  "test:integration": "bun test ./test/integration/ --timeout=60000",
48279
48611
  "test:ui": "bun test ./test/ui/ --timeout=60000",
48280
48612
  "test:e2e": "bun run scripts/run-tests.ts --e2e",
48281
- "test:confined": "bun ../nax-agent/scripts/run-confined-tests.ts --package=. --timeout=5000 ./test/unit/ && bun ../nax-agent/scripts/run-confined-tests.ts --package=. --timeout=5000 ./test/integration/",
48613
+ "test:confined": "bun ../nax-agent/scripts/run-confined-tests.ts --package=. --timeout=5000 --parallel=4 ./test/unit/ && bun ../nax-agent/scripts/run-confined-tests.ts --package=. --timeout=5000 --parallel=4 ./test/integration/",
48282
48614
  "test:coverage": "bun run ../repo-tooling/scripts/check-coverage.ts --require-all-files",
48283
48615
  "test:coverage:report": "bun run ../repo-tooling/scripts/check-coverage.ts --require-all-files --report",
48284
48616
  "test:coverage:update": "bun run ../repo-tooling/scripts/check-coverage.ts --require-all-files --update-baseline",
@@ -48308,7 +48640,7 @@ var init_package = __esm(() => {
48308
48640
  "@agentclientprotocol/sdk": "~1.7.0",
48309
48641
  "@anthropic-ai/sandbox-runtime": "0.0.77",
48310
48642
  "@modelcontextprotocol/sdk": "^1.32.1",
48311
- "@nathapp/nax-ai": "0.88.0-canary.2",
48643
+ "@nathapp/nax-ai": "0.88.0",
48312
48644
  chalk: "^5.6.2",
48313
48645
  commander: "^13.1.0",
48314
48646
  ink: "^6.7.0",
@@ -55578,7 +55910,8 @@ function editTargets(toolCall) {
55578
55910
  return [];
55579
55911
  const fromLocations = Array.isArray(toolCall.locations) ? toolCall.locations.flatMap((l) => isRecord5(l) && typeof l.path === "string" && l.path !== "" ? [l.path] : []) : [];
55580
55912
  const raw = isRecord5(toolCall.rawInput) ? toolCall.rawInput : {};
55581
- const fromRaw = RAW_PATH_KEYS.flatMap((k) => typeof raw[k] === "string" && raw[k] !== "" ? [raw[k]] : []);
55913
+ const keys = typeof toolCall.kind === "string" && WRITE_KINDS.has(toolCall.kind) ? WRITE_PATH_KEYS : RAW_PATH_KEYS;
55914
+ const fromRaw = keys.flatMap((k) => typeof raw[k] === "string" && raw[k] !== "" ? [raw[k]] : []);
55582
55915
  return [...new Set([...fromLocations, ...fromRaw])];
55583
55916
  }
55584
55917
  function expand(path, cwd) {
@@ -55649,10 +55982,12 @@ async function firstOutsidePolicy(targets, policy, cwd) {
55649
55982
  }
55650
55983
  return;
55651
55984
  }
55652
- var OUTSIDE_WRITE_ROOTS_REASON = "the edit target is outside the sandbox write roots", RAW_PATH_KEYS, _editTargetsDeps, SEPARATORS, hasDotDotSegment = (path) => path.split(SEPARATORS).includes(".."), MAX_SYMLINK_HOPS = 40, canonicalPolicyPath = async (path) => await canonical3(path) ?? path, nfc = (path) => path.normalize("NFC"), foldCase = (path) => _editTargetsDeps.platform() === "darwin" || _editTargetsDeps.platform() === "win32" ? path.toLowerCase() : path, contains = (path, root) => path === root || path.startsWith(root.endsWith(sep18) ? root : `${root}${sep18}`), withinRoot = (path, root) => contains(nfc(path), nfc(root)), withinDenied = (path, denied) => contains(foldCase(nfc(path)), foldCase(nfc(denied)));
55985
+ var OUTSIDE_WRITE_ROOTS_REASON = "the edit target is outside the sandbox write roots", RAW_PATH_KEYS, WRITE_PATH_KEYS, WRITE_KINDS, _editTargetsDeps, SEPARATORS, hasDotDotSegment = (path) => path.split(SEPARATORS).includes(".."), MAX_SYMLINK_HOPS = 40, canonicalPolicyPath = async (path) => await canonical3(path) ?? path, nfc = (path) => path.normalize("NFC"), foldCase = (path) => _editTargetsDeps.platform() === "darwin" || _editTargetsDeps.platform() === "win32" ? path.toLowerCase() : path, contains = (path, root) => path === root || path.startsWith(root.endsWith(sep18) ? root : `${root}${sep18}`), withinRoot = (path, root) => contains(nfc(path), nfc(root)), withinDenied = (path, denied) => contains(foldCase(nfc(path)), foldCase(nfc(denied)));
55653
55986
  var init_edit_targets = __esm(() => {
55654
55987
  init_text();
55655
55988
  RAW_PATH_KEYS = ["file_path", "notebook_path"];
55989
+ WRITE_PATH_KEYS = [...RAW_PATH_KEYS, "path", "filePath", "old_path", "new_path"];
55990
+ WRITE_KINDS = new Set(["edit", "delete", "move"]);
55656
55991
  _editTargetsDeps = { platform: () => process.platform };
55657
55992
  SEPARATORS = sep18 === "/" ? /\// : /[/\\]/;
55658
55993
  });
@@ -55735,8 +56070,10 @@ async function decidePermission(request, ctx) {
55735
56070
  async function decideFull(request, display, offered, auto, ctx) {
55736
56071
  if (offered.allowOnce === undefined)
55737
56072
  return denyNoAllowOnce(offered, auto);
55738
- const outside = ctx.sandbox !== undefined && display.tool !== "execute" && !READ_ONLY_KINDS.has(display.tool) ? await firstOutsidePolicy(editTargets(request.toolCall), ctx.sandbox, ctx.cwd) : undefined;
55739
- if (outside !== undefined) {
56073
+ const targets = editTargets(request.toolCall);
56074
+ const unknownWrite = ctx.sandbox !== undefined && WRITE_KINDS.has(display.tool) && targets.length === 0;
56075
+ const outside = ctx.sandbox !== undefined && display.tool !== "execute" && !READ_ONLY_KINDS.has(display.tool) ? await firstOutsidePolicy(targets, ctx.sandbox, ctx.cwd) : undefined;
56076
+ if (unknownWrite || outside !== undefined) {
55740
56077
  auto(OUTSIDE_WRITE_ROOTS_REASON, "deny");
55741
56078
  return rejectWith(offered);
55742
56079
  }
@@ -55976,8 +56313,12 @@ function startedLaunch(child, stderr, maxMessageBytes) {
55976
56313
  gone = true;
55977
56314
  });
55978
56315
  const kill = () => {
55979
- if (!gone && pid !== undefined)
55980
- killProcessGroup(pid, "SIGKILL");
56316
+ if (pid === undefined)
56317
+ return;
56318
+ if (gone)
56319
+ _launchDeps.killProcessGroup(pid, "SIGKILL", { singleProcessFallback: false });
56320
+ else
56321
+ _launchDeps.killProcessGroup(pid, "SIGKILL");
55981
56322
  };
55982
56323
  return {
55983
56324
  target: { kind: "stream", stream },
@@ -55993,14 +56334,23 @@ function startedLaunch(child, stderr, maxMessageBytes) {
55993
56334
  return exit.value;
55994
56335
  },
55995
56336
  async terminate(graceMs) {
55996
- if (!gone && pid !== undefined) {
56337
+ if (pid === undefined) {
56338
+ await exited;
56339
+ return;
56340
+ }
56341
+ if (!gone)
55997
56342
  child.stdin.end();
55998
- killProcessGroup(pid, "SIGTERM");
55999
- const timer = setTimeout(kill, graceMs);
56343
+ if (gone)
56344
+ _launchDeps.killProcessGroup(pid, "SIGTERM", { singleProcessFallback: false });
56345
+ else
56346
+ _launchDeps.killProcessGroup(pid, "SIGTERM");
56347
+ const timer = setTimeout(kill, graceMs);
56348
+ try {
56000
56349
  await exited;
56350
+ kill();
56351
+ } finally {
56001
56352
  clearTimeout(timer);
56002
56353
  }
56003
- await exited;
56004
56354
  }
56005
56355
  };
56006
56356
  }
@@ -56020,11 +56370,12 @@ async function agentGoneError(during, launched, secrets) {
56020
56370
  const exitDetails = exit === undefined ? {} : { exitCode: exit.code, signal: exit.signal, ...exit.spawnError === undefined ? {} : { spawnError: true } };
56021
56371
  return backendUnavailable(stderr === "" ? what : `${what}; stderr: ${stderr}`, { during, ...exitDetails, stderr });
56022
56372
  }
56023
- var GONE_WAIT_MS = 1000, STDERR_DRAIN_MS = 200;
56373
+ var GONE_WAIT_MS = 1000, _launchDeps, STDERR_DRAIN_MS = 200;
56024
56374
  var init_launch = __esm(() => {
56025
56375
  init_acp();
56026
56376
  init_src();
56027
56377
  init_errors6();
56378
+ _launchDeps = { killProcessGroup };
56028
56379
  });
56029
56380
 
56030
56381
  // ../nax-agent-acp/src/client/capabilities.ts
@@ -72379,8 +72730,8 @@ var init_version = __esm(() => {
72379
72730
  NAX_AI_VERSION = CATALOG_VERSION;
72380
72731
  NAX_COMMIT = (() => {
72381
72732
  try {
72382
- if (/^[0-9a-f]{6,10}$/.test("9c41694"))
72383
- return "9c41694";
72733
+ if (/^[0-9a-f]{6,10}$/.test("33be270"))
72734
+ return "33be270";
72384
72735
  } catch {}
72385
72736
  try {
72386
72737
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -77156,6 +77507,18 @@ async function connectMcp(config, opts) {
77156
77507
  try {
77157
77508
  if (signal.aborted)
77158
77509
  throw new McpConnectError("MCP connect failed: aborted");
77510
+ if (config.kind === "stdio" && opts.onSpawn !== undefined) {
77511
+ const start = handle.transport.start;
77512
+ let notified = false;
77513
+ handle.transport.start = async function() {
77514
+ await start.call(this);
77515
+ const pid = handle.pid();
77516
+ if (pid !== null && !notified) {
77517
+ notified = true;
77518
+ opts.onSpawn?.(pid);
77519
+ }
77520
+ };
77521
+ }
77159
77522
  await client.connect(handle.transport, { signal, timeout: opts.timeoutMs });
77160
77523
  const tools = await listAll(client, signal, opts.timeoutMs);
77161
77524
  return liveConnection(config, client, handle, tools, {
@@ -77251,7 +77614,8 @@ async function connectMcpServer(args) {
77251
77614
  cwd: args.workdir
77252
77615
  };
77253
77616
  const opts = {
77254
- signal: new AbortController().signal,
77617
+ signal: args.signal ?? new AbortController().signal,
77618
+ onSpawn: args.onSpawn,
77255
77619
  timeoutMs: args.connectTimeoutMs,
77256
77620
  clientInfo: { name: "nax", version: "1" }
77257
77621
  };
@@ -77349,6 +77713,8 @@ var init_lock = __esm(() => {
77349
77713
  // src/mcp/pool.ts
77350
77714
  function createMcpPool(opts) {
77351
77715
  const retry = opts.retry ?? MCP_DEFAULT_RETRY;
77716
+ const controller = new AbortController;
77717
+ const { signal } = controller;
77352
77718
  const keyOf = (serverId, workdir) => JSON.stringify([serverId, workdir]);
77353
77719
  const entries = new Map;
77354
77720
  const events = [];
@@ -77361,8 +77727,10 @@ function createMcpPool(opts) {
77361
77727
  if (server === undefined || !server.enabled)
77362
77728
  return;
77363
77729
  await assertTrusted(workdir, "mcp");
77364
- for (let attempt = 1;attempt <= retry.maxAttempts; attempt++) {
77730
+ for (let attempt = 1;attempt <= retry.maxAttempts && !signal.aborted; attempt++) {
77365
77731
  let connection;
77732
+ let spawnedPid;
77733
+ let registration;
77366
77734
  try {
77367
77735
  connection = await connectMcpServer({
77368
77736
  serverId,
@@ -77370,11 +77738,16 @@ function createMcpPool(opts) {
77370
77738
  command: server.command,
77371
77739
  args: server.args,
77372
77740
  env: server.env,
77373
- connectTimeoutMs: MCP_CONNECT_TIMEOUT_MS
77741
+ connectTimeoutMs: MCP_CONNECT_TIMEOUT_MS,
77742
+ signal,
77743
+ onSpawn(pid) {
77744
+ spawnedPid = pid;
77745
+ registration = opts.pidRegistry?.register(pid).catch(() => {});
77746
+ }
77374
77747
  });
77748
+ signal.throwIfAborted();
77749
+ await registration;
77375
77750
  const pid = connection.pid;
77376
- if (pid !== null)
77377
- await opts.pidRegistry?.register(pid).catch(() => {});
77378
77751
  const tools = await connection.listTools();
77379
77752
  record({ kind: "connected", serverId, workdir, at: new Date().toISOString(), pid, toolCount: tools.length });
77380
77753
  getSafeLogger2()?.info("mcp", `[pool] ${serverId} connected`, {
@@ -77386,12 +77759,11 @@ function createMcpPool(opts) {
77386
77759
  });
77387
77760
  return { connection, tools };
77388
77761
  } catch (error) {
77389
- if (connection !== undefined) {
77390
- const pid = connection.pid;
77762
+ if (connection !== undefined)
77391
77763
  await connection.close().catch(() => {});
77392
- if (pid !== null)
77393
- await opts.pidRegistry?.unregister(pid).catch(() => {});
77394
- }
77764
+ await registration;
77765
+ if (spawnedPid !== undefined)
77766
+ await opts.pidRegistry?.unregister(spawnedPid).catch(() => {});
77395
77767
  const reason = error instanceof Error ? error.message : String(error);
77396
77768
  record({ kind: "connect-failed", serverId, workdir, at: new Date().toISOString(), reason, attempt });
77397
77769
  getSafeLogger2()?.warn("mcp", `[pool] ${serverId} connect failed`, {
@@ -77401,8 +77773,8 @@ function createMcpPool(opts) {
77401
77773
  attempt,
77402
77774
  error: reason
77403
77775
  });
77404
- if (attempt < retry.maxAttempts && retry.baseDelayMs > 0) {
77405
- await Bun.sleep(retry.baseDelayMs * attempt);
77776
+ if (attempt < retry.maxAttempts && retry.baseDelayMs > 0 && !signal.aborted) {
77777
+ await cancellableDelay(retry.baseDelayMs * attempt, signal).catch(() => {});
77406
77778
  }
77407
77779
  }
77408
77780
  }
@@ -77459,6 +77831,7 @@ function createMcpPool(opts) {
77459
77831
  if (closed)
77460
77832
  return;
77461
77833
  closed = true;
77834
+ controller.abort();
77462
77835
  const pending = [...entries.values()];
77463
77836
  entries.clear();
77464
77837
  const settled = await Promise.allSettled(pending);
@@ -78445,7 +78818,15 @@ var init_deps = __esm(() => {
78445
78818
  init_errors4();
78446
78819
  init_tree_snapshot();
78447
78820
  init_hardened_git();
78448
- _snapshotDeps = { rm: rm8, lstat: lstat4, snapshotWorkingTree, changedPathsBetween, git: runHardenedGit };
78821
+ _snapshotDeps = {
78822
+ rm: rm8,
78823
+ lstat: lstat4,
78824
+ snapshotWorkingTree,
78825
+ changedPathsBetween,
78826
+ git: runHardenedGit,
78827
+ sleep: (ms) => Bun.sleep(ms),
78828
+ now: Date.now
78829
+ };
78449
78830
  });
78450
78831
 
78451
78832
  // src/review/reviewer-guard/snapshot-ignore-rules.ts
@@ -78586,9 +78967,9 @@ var init_snapshot_refs = __esm(() => {
78586
78967
  });
78587
78968
 
78588
78969
  // src/review/reviewer-guard/snapshot.ts
78589
- import { join as join70 } from "path";
78970
+ import { join as join70, resolve as resolve39 } from "path";
78590
78971
  function isKeeperOwnedPath(toplevel, rel) {
78591
- return classifyRunStatePath(join70(toplevel, rel)) !== null;
78972
+ return activeKeepers().some((keeper) => keeper.get(join70(toplevel, rel)) !== undefined);
78592
78973
  }
78593
78974
  async function takeRepoSnapshot(workdir) {
78594
78975
  const toplevel = await git2(["rev-parse", "--show-toplevel"], workdir);
@@ -78636,6 +79017,38 @@ async function snapshotEntries(snap) {
78636
79017
  }
78637
79018
  return entries;
78638
79019
  }
79020
+ async function waitForIndexLock(snap, failures) {
79021
+ const lock = resolve39(snap.toplevel, await git2(["rev-parse", "--git-path", "index.lock"], snap.toplevel));
79022
+ const deadline = _snapshotDeps.now() + INDEX_LOCK_WAIT_MS;
79023
+ while (true) {
79024
+ try {
79025
+ await _snapshotDeps.lstat(lock);
79026
+ } catch (err) {
79027
+ if (isAbsent(err))
79028
+ return;
79029
+ throw err;
79030
+ }
79031
+ const remaining = deadline - _snapshotDeps.now();
79032
+ if (remaining <= 0) {
79033
+ failures.push("index.lock held");
79034
+ return;
79035
+ }
79036
+ await _snapshotDeps.sleep(Math.min(INDEX_LOCK_POLL_MS, remaining));
79037
+ }
79038
+ }
79039
+ async function checkoutPaths(snap, paths, failures) {
79040
+ let restored = false;
79041
+ for (let i = 0;i < paths.length; i += CHECKOUT_CHUNK) {
79042
+ const chunk = paths.slice(i, i + CHECKOUT_CHUNK);
79043
+ try {
79044
+ await git2(["checkout", snap.tree, "--", ...chunk.map((p) => `:(top,literal)${p}`)], snap.toplevel);
79045
+ restored = true;
79046
+ } catch (err) {
79047
+ failures.push(`checkout ${chunk.length} path(s): ${errorMessage(err)}`);
79048
+ }
79049
+ }
79050
+ return restored;
79051
+ }
78639
79052
  async function restoreTree(snap, failures) {
78640
79053
  const entries = await snapshotEntries(snap);
78641
79054
  const ruleChanges = await restoreIgnoreRules({
@@ -78646,8 +79059,15 @@ async function restoreTree(snap, failures) {
78646
79059
  skip: (p) => isKeeperOwnedPath(snap.toplevel, p),
78647
79060
  remove: removePath
78648
79061
  }, failures);
78649
- const current = await _snapshotDeps.snapshotWorkingTree(snap.toplevel);
78650
- const diffed = (await _snapshotDeps.changedPathsBetween(snap.toplevel, snap.tree, current)).filter((p) => !isKeeperOwnedPath(snap.toplevel, p));
79062
+ let diffed;
79063
+ try {
79064
+ const current = await _snapshotDeps.snapshotWorkingTree(snap.toplevel);
79065
+ diffed = (await _snapshotDeps.changedPathsBetween(snap.toplevel, snap.tree, current)).filter((p) => !isKeeperOwnedPath(snap.toplevel, p));
79066
+ } catch (err) {
79067
+ failures.push(`tree: ${errorMessage(err)}`);
79068
+ const paths = [...entries].filter(([p, entry]) => entry.type !== "commit" && !isKeeperOwnedPath(snap.toplevel, p)).map(([p]) => p);
79069
+ return { paths, restored: await checkoutPaths(snap, paths, failures), fallback: "tracked-only" };
79070
+ }
78651
79071
  const changed = [...new Set([...ruleChanges, ...diffed])];
78652
79072
  if (changed.length === 0)
78653
79073
  return { paths: [], restored: false };
@@ -78656,10 +79076,7 @@ async function restoreTree(snap, failures) {
78656
79076
  for (const p of remove) {
78657
79077
  await removePath(join70(snap.toplevel, p)).catch((err) => failures.push(`remove ${p}: ${errorMessage(err)}`));
78658
79078
  }
78659
- for (let i = 0;i < restore.length; i += CHECKOUT_CHUNK) {
78660
- const chunk = restore.slice(i, i + CHECKOUT_CHUNK);
78661
- await git2(["checkout", snap.tree, "--", ...chunk.map((p) => `:(top,literal)${p}`)], snap.toplevel).catch((err) => failures.push(`checkout ${chunk.length} path(s): ${errorMessage(err)}`));
78662
- }
79079
+ await checkoutPaths(snap, restore, failures);
78663
79080
  return { paths: changed, restored: restore.length > 0 || ruleChanges.length > 0 };
78664
79081
  }
78665
79082
  async function restoreIndex(snap, force) {
@@ -78687,10 +79104,12 @@ async function restoreRepoSnapshot(snap) {
78687
79104
  const replaceRemoved = await phase("replace-refs", false, () => removeAddedReplaceRefs(snap, snap.toplevel));
78688
79105
  const refsLeft = await phase("refs-report", [], () => diffRefsLeft(snap, snap.toplevel));
78689
79106
  const stashLeft = await phase("stash-report", [], () => diffStashesLeft(snap.stashes, snap.toplevel));
79107
+ await phase("index-lock", undefined, () => waitForIndexLock(snap, failures));
78690
79108
  const tree = await phase("tree", { paths: [], restored: false }, () => restoreTree(snap, failures));
78691
79109
  const indexRestored = await phase("index", false, () => restoreIndex(snap, tree.restored));
78692
79110
  return {
78693
79111
  paths: tree.paths,
79112
+ ...tree.fallback === undefined ? {} : { fallback: tree.fallback },
78694
79113
  indexRestored,
78695
79114
  headRestored: headMoved || replaceRemoved,
78696
79115
  refsLeft,
@@ -78698,7 +79117,7 @@ async function restoreRepoSnapshot(snap) {
78698
79117
  failures
78699
79118
  };
78700
79119
  }
78701
- var CHECKOUT_CHUNK = 500;
79120
+ var CHECKOUT_CHUNK = 500, INDEX_LOCK_WAIT_MS = 5000, INDEX_LOCK_POLL_MS = 100;
78702
79121
  var init_snapshot = __esm(() => {
78703
79122
  init_internal();
78704
79123
  init_store();
@@ -80752,7 +81171,7 @@ var init_session_run_hop = __esm(() => {
80752
81171
  });
80753
81172
 
80754
81173
  // src/runtime/index.ts
80755
- import { basename as basename12, isAbsolute as isAbsolute26, join as join74, resolve as resolve39 } from "path";
81174
+ import { basename as basename12, isAbsolute as isAbsolute26, join as join74, resolve as resolve40 } from "path";
80756
81175
  function createRuntime(config, workdir, opts) {
80757
81176
  const runId = crypto.randomUUID();
80758
81177
  const controller = new AbortController;
@@ -80783,7 +81202,7 @@ function createRuntime(config, workdir, opts) {
80783
81202
  const reviewAuditor = opts?.reviewAuditor ?? (config.review?.audit?.enabled ? new ReviewAuditor(runId, outputDir) : createNoOpReviewAuditor());
80784
81203
  const usageEnabled = config.agent?.usageAudit?.enabled ?? false;
80785
81204
  const configuredUsageDir = config.agent?.usageAudit?.dir;
80786
- const usageDir = configuredUsageDir === undefined ? join74(outputDir, "usage") : isAbsolute26(configuredUsageDir) ? configuredUsageDir : resolve39(workdir, configuredUsageDir);
81205
+ const usageDir = configuredUsageDir === undefined ? join74(outputDir, "usage") : isAbsolute26(configuredUsageDir) ? configuredUsageDir : resolve40(workdir, configuredUsageDir);
80787
81206
  let usageAuditor;
80788
81207
  if (opts?.usageAuditor) {
80789
81208
  usageAuditor = opts.usageAuditor;
@@ -80986,12 +81405,12 @@ var init_runtime3 = __esm(() => {
80986
81405
 
80987
81406
  // src/context/engine/stage-assembler.ts
80988
81407
  import { readdir as readdir4 } from "fs/promises";
80989
- import { isAbsolute as isAbsolute27, join as join75, resolve as resolve40 } from "path";
81408
+ import { isAbsolute as isAbsolute27, join as join75, resolve as resolve41 } from "path";
80990
81409
  function dedupeScratchDirs(dirs) {
80991
81410
  return [...new Set(dirs.filter((dir) => Boolean(dir)))];
80992
81411
  }
80993
81412
  function toAbsolutePath2(projectDir, pathValue) {
80994
- return isAbsolute27(pathValue) ? pathValue : resolve40(projectDir, pathValue);
81413
+ return isAbsolute27(pathValue) ? pathValue : resolve41(projectDir, pathValue);
80995
81414
  }
80996
81415
  async function discoverSessionScratchDirsOnDisk(projectDir, featureName, storyId, ttlMs) {
80997
81416
  const logger = getLogger2();
@@ -90526,19 +90945,24 @@ var init_workflow_branch_fix = __esm(() => {
90526
90945
  });
90527
90946
 
90528
90947
  // src/operations/workflow-review-coverage.ts
90529
- function normalize6(entry) {
90530
- return entry.startsWith("./") ? entry.slice(2) : entry;
90948
+ import { isAbsolute as isAbsolute30, relative as relative29 } from "path";
90949
+ function normalize6(entry, repoRoot) {
90950
+ const path = isAbsolute30(entry) ? relative29(repoRoot, entry) : entry;
90951
+ if (path === ".." || path.startsWith("../") || isAbsolute30(path))
90952
+ return;
90953
+ return path.startsWith("./") ? path.slice(2) : path;
90531
90954
  }
90532
90955
  function covers2(entry, path) {
90533
- return entry === path || entry.endsWith(`/${path}`);
90956
+ return entry !== undefined && entry === path;
90534
90957
  }
90535
- function unreadFiles(required, inspected) {
90536
- const entries = inspected.map(normalize6);
90958
+ function unreadFiles(required, inspected, repoRoot) {
90959
+ const entries = inspected.map((entry) => normalize6(entry, repoRoot));
90537
90960
  return required.filter((path) => !entries.some((entry) => covers2(entry, path)));
90538
90961
  }
90539
- function isCompleteBranchVerdict(verdict, requiredFiles) {
90540
- return unreadFiles(requiredFiles, verdict.inspectedFiles).length === 0;
90962
+ function isCompleteBranchVerdict(verdict, requiredFiles, repoRoot) {
90963
+ return unreadFiles(requiredFiles, verdict.inspectedFiles, repoRoot).length === 0;
90541
90964
  }
90965
+ var init_workflow_review_coverage = () => {};
90542
90966
 
90543
90967
  // src/operations/workflow-story-review.ts
90544
90968
  var REVIEWER_VERIFY_TOOLS, WORKFLOW_REVIEW_TOOLS, workflowStoryReviewOp;
@@ -90584,6 +91008,7 @@ var init_workflow_branch_review = __esm(() => {
90584
91008
  init_config();
90585
91009
  init_prompts();
90586
91010
  init_workflow_replies();
91011
+ init_workflow_review_coverage();
90587
91012
  init_workflow_story_review();
90588
91013
  workflowBranchReviewOp = {
90589
91014
  kind: "run",
@@ -90605,12 +91030,12 @@ var init_workflow_branch_review = __esm(() => {
90605
91030
  retry: (input) => {
90606
91031
  const invalid = (lastOutput) => {
90607
91032
  const verdict = parseBranchReviewReply(lastOutput, input.minorIds);
90608
- const missing = verdict ? unreadFiles(input.requiredFiles, verdict.inspectedFiles) : [];
91033
+ const missing = verdict ? unreadFiles(input.requiredFiles, verdict.inspectedFiles, input.repoRoot) : [];
90609
91034
  return missing.length > 0 ? buildWorkflowBranchCoverageRetry(input.range, missing) : buildWorkflowReviewJsonRetry();
90610
91035
  };
90611
91036
  return makeParseRetryStrategy({
90612
91037
  parse: (output) => parseBranchReviewReply(output, input.minorIds),
90613
- validate: (parsed) => parsed !== null && isCompleteBranchVerdict(parsed, input.requiredFiles),
91038
+ validate: (parsed) => parsed !== null && isCompleteBranchVerdict(parsed, input.requiredFiles, input.repoRoot),
90614
91039
  reviewerKind: "workflow-branch-review",
90615
91040
  maxAttempts: 2,
90616
91041
  prompts: { invalid, truncated: buildWorkflowReviewJsonRetryCondensed },
@@ -90776,6 +91201,7 @@ var init_operations = __esm(() => {
90776
91201
  init_workflow_implement();
90777
91202
  init_workflow_re_review();
90778
91203
  init_workflow_replies();
91204
+ init_workflow_review_coverage();
90779
91205
  init_workflow_story_review();
90780
91206
  init_write_test();
90781
91207
  });
@@ -91913,7 +92339,7 @@ var init_failed_criteria = __esm(() => {
91913
92339
  });
91914
92340
 
91915
92341
  // src/acceptance/import-resolution.ts
91916
- import { resolve as resolve41, sep as sep20 } from "path";
92342
+ import { resolve as resolve42, sep as sep20 } from "path";
91917
92343
  function languageFromExtension(testFilePath) {
91918
92344
  if (!testFilePath)
91919
92345
  return;
@@ -91935,8 +92361,8 @@ async function resolveLanguage(opts) {
91935
92361
  return "typescript";
91936
92362
  }
91937
92363
  async function readCapped2(relPath, packageDir) {
91938
- const resolvedPackageDir = resolve41(packageDir);
91939
- const fullPath = resolve41(resolvedPackageDir, relPath);
92364
+ const resolvedPackageDir = resolve42(packageDir);
92365
+ const fullPath = resolve42(resolvedPackageDir, relPath);
91940
92366
  if (fullPath !== resolvedPackageDir && !fullPath.startsWith(resolvedPackageDir + sep20)) {
91941
92367
  return null;
91942
92368
  }
@@ -92361,9 +92787,9 @@ var init_hardening = __esm(() => {
92361
92787
  });
92362
92788
 
92363
92789
  // src/acceptance/override-keys.ts
92364
- import { relative as relative29 } from "path";
92790
+ import { relative as relative30 } from "path";
92365
92791
  function scopedOverrideKey(workdir, packageDir, acId) {
92366
- return `${relative29(workdir, packageDir) || "."}${OVERRIDE_SCOPE_SEPARATOR}${acId}`;
92792
+ return `${relative30(workdir, packageDir) || "."}${OVERRIDE_SCOPE_SEPARATOR}${acId}`;
92367
92793
  }
92368
92794
  function definingPackageCount(acId, counts) {
92369
92795
  const n = Number(NUMBERED_AC.exec(acId)?.[1] ?? 0);
@@ -92412,7 +92838,7 @@ var init_acceptance2 = __esm(() => {
92412
92838
 
92413
92839
  // src/cli/features-acceptance.ts
92414
92840
  import { existsSync as existsSync14 } from "fs";
92415
- import { join as join89, relative as relative30 } from "path";
92841
+ import { join as join89, relative as relative31 } from "path";
92416
92842
  async function resolveFeatureAcceptance(featureName, workdir) {
92417
92843
  let enabled = true;
92418
92844
  try {
@@ -92433,11 +92859,11 @@ async function resolveFeatureAcceptance(featureName, workdir) {
92433
92859
  const prd = await loadPRD(prdPath);
92434
92860
  const testGroups = await groupStoriesByPackage(prd, repoRoot, featureName, config.acceptance?.testPath, config.project?.language);
92435
92861
  const groups = await Promise.all(testGroups.map(async (g) => {
92436
- const packageDir = relative30(repoRoot, g.packageDir);
92862
+ const packageDir = relative31(repoRoot, g.packageDir);
92437
92863
  const command = await resolveGroupCommand(repoRoot, packageDir, config.acceptance?.command);
92438
92864
  return {
92439
92865
  packageDir,
92440
- testPath: relative30(repoRoot, g.testPath),
92866
+ testPath: relative31(repoRoot, g.testPath),
92441
92867
  exists: await Bun.file(g.testPath).exists(),
92442
92868
  command,
92443
92869
  cwd: packageDir,
@@ -92469,7 +92895,7 @@ var init_features_acceptance = __esm(() => {
92469
92895
 
92470
92896
  // src/cli/features-resolve.ts
92471
92897
  import { existsSync as existsSync15, readdirSync as readdirSync3 } from "fs";
92472
- import { join as join90, relative as relative31 } from "path";
92898
+ import { join as join90, relative as relative32 } from "path";
92473
92899
  async function resolveTestPatterns(workdir) {
92474
92900
  try {
92475
92901
  const config = await loadConfig(workdir);
@@ -92492,17 +92918,17 @@ async function searchSpecSource(naxDir, repoRoot, name) {
92492
92918
  ];
92493
92919
  const docsSpecExact = join90(repoRoot, "docs", "specs", `SPEC-${name}.md`);
92494
92920
  candidates.push({ abs: docsSpecExact, kind: "markdown" });
92495
- const checked = candidates.map((c) => relative31(repoRoot, c.abs));
92921
+ const checked = candidates.map((c) => relative32(repoRoot, c.abs));
92496
92922
  for (const { abs, kind } of candidates.slice(0, 2)) {
92497
92923
  if (kind === "markdown") {
92498
92924
  const nonEmpty = await isNonEmptyFile(abs);
92499
92925
  if (nonEmpty) {
92500
- return { source: { kind, path: relative31(repoRoot, abs) }, checked };
92926
+ return { source: { kind, path: relative32(repoRoot, abs) }, checked };
92501
92927
  }
92502
92928
  }
92503
92929
  }
92504
92930
  if (await isNonEmptyFile(docsSpecExact)) {
92505
- return { source: { kind: "markdown", path: relative31(repoRoot, docsSpecExact) }, checked };
92931
+ return { source: { kind: "markdown", path: relative32(repoRoot, docsSpecExact) }, checked };
92506
92932
  }
92507
92933
  const docsSpecsDir = join90(repoRoot, "docs", "specs");
92508
92934
  if (existsSync15(docsSpecsDir)) {
@@ -92510,7 +92936,7 @@ async function searchSpecSource(naxDir, repoRoot, name) {
92510
92936
  for (const match of glob.scanSync({ cwd: docsSpecsDir, absolute: false })) {
92511
92937
  const abs = join90(docsSpecsDir, match);
92512
92938
  if (await isNonEmptyFile(abs)) {
92513
- const relPath = relative31(repoRoot, abs);
92939
+ const relPath = relative32(repoRoot, abs);
92514
92940
  if (!checked.includes(relPath))
92515
92941
  checked.push(relPath);
92516
92942
  return { source: { kind: "markdown", path: relPath }, checked };
@@ -92518,7 +92944,7 @@ async function searchSpecSource(naxDir, repoRoot, name) {
92518
92944
  }
92519
92945
  }
92520
92946
  const prdAbs = join90(naxDir, "features", name, "prd.json");
92521
- const prdRel = relative31(repoRoot, prdAbs);
92947
+ const prdRel = relative32(repoRoot, prdAbs);
92522
92948
  if (!checked.includes(prdRel))
92523
92949
  checked.push(prdRel);
92524
92950
  if (existsSync15(prdAbs)) {
@@ -92570,8 +92996,8 @@ async function resolveFeatureSpec(name, workdir) {
92570
92996
  return {
92571
92997
  status: "ok",
92572
92998
  featureName: null,
92573
- specSource: { kind: "markdown", path: relative31(repoRoot, abs) },
92574
- message: `resolved spec: ${relative31(repoRoot, abs)}`
92999
+ specSource: { kind: "markdown", path: relative32(repoRoot, abs) },
93000
+ message: `resolved spec: ${relative32(repoRoot, abs)}`
92575
93001
  };
92576
93002
  }
92577
93003
  if (name !== undefined && name.trim() !== "") {
@@ -97561,7 +97987,7 @@ var init_windsurf = __esm(() => {
97561
97987
 
97562
97988
  // src/context/generator/index.ts
97563
97989
  import { existsSync as existsSync21 } from "fs";
97564
- import { join as join98, relative as relative32 } from "path";
97990
+ import { join as join98, relative as relative33 } from "path";
97565
97991
  async function loadContextContent(options, config) {
97566
97992
  if (!_generatorDeps.existsSync(options.contextPath)) {
97567
97993
  throw new NaxError(`Context file not found: ${options.contextPath}`, "CONTEXT_FILE_NOT_FOUND", {
@@ -97692,7 +98118,7 @@ async function discoverWorkspacePackages2(repoRoot) {
97692
98118
  }
97693
98119
  async function generateForPackage(packageDir, config, dryRun = false, repoRoot) {
97694
98120
  const resolvedRepoRoot = repoRoot ?? packageDir;
97695
- const relativePkgPath = relative32(resolvedRepoRoot, packageDir);
98121
+ const relativePkgPath = relative33(resolvedRepoRoot, packageDir);
97696
98122
  const contextPath = join98(resolvedRepoRoot, ".nax", "mono", relativePkgPath, "context.md");
97697
98123
  if (!_generatorDeps.existsSync(contextPath)) {
97698
98124
  return [
@@ -97975,10 +98401,10 @@ var init_generate = __esm(() => {
97975
98401
  });
97976
98402
 
97977
98403
  // src/utils/ignored-nax-paths.ts
97978
- import { relative as relative33 } from "path";
98404
+ import { relative as relative34 } from "path";
97979
98405
  function naxTrackedPaths(projectDir) {
97980
98406
  const dir = featureDir(projectDir, PROBE_FEATURE);
97981
- return [ledgerPath(projectDir, PROBE_FEATURE), ...COMMITTED_FEATURE_FILES.map((name) => `${dir}/${name}`)].map((absolute) => relative33(projectDir, absolute));
98407
+ return [ledgerPath(projectDir, PROBE_FEATURE), ...COMMITTED_FEATURE_FILES.map((name) => `${dir}/${name}`)].map((absolute) => relative34(projectDir, absolute));
97982
98408
  }
97983
98409
  function parseCheckIgnore(stdout) {
97984
98410
  const out = [];
@@ -98033,7 +98459,7 @@ var init_ignored_nax_paths = __esm(() => {
98033
98459
 
98034
98460
  // src/cli/init-context.ts
98035
98461
  import { mkdir as mkdir30, readdir as readdir9 } from "fs/promises";
98036
- import { basename as basename17, join as join100, relative as relative34, sep as sep21 } from "path";
98462
+ import { basename as basename17, join as join100, relative as relative35, sep as sep21 } from "path";
98037
98463
  async function bunFileExists(path) {
98038
98464
  return Bun.file(path).exists();
98039
98465
  }
@@ -98060,7 +98486,7 @@ async function walkDir(root, current, out, maxFiles) {
98060
98486
  continue;
98061
98487
  await walkDir(root, join100(current, entry.name), out, maxFiles);
98062
98488
  } else if (entry.isFile()) {
98063
- out.push(relative34(root, join100(current, entry.name)).replaceAll(sep21, "/"));
98489
+ out.push(relative35(root, join100(current, entry.name)).replaceAll(sep21, "/"));
98064
98490
  }
98065
98491
  }
98066
98492
  }
@@ -99421,11 +99847,11 @@ var init_rollback = __esm(() => {
99421
99847
  });
99422
99848
 
99423
99849
  // src/utils/paths.ts
99424
- import { join as join106, relative as relative35, sep as sep22 } from "path";
99850
+ import { join as join106, relative as relative36, sep as sep22 } from "path";
99425
99851
  function packageDirRelative(projectDir, workdir) {
99426
99852
  if (!projectDir || !workdir || workdir === projectDir)
99427
99853
  return;
99428
- const rel = relative35(projectDir, workdir);
99854
+ const rel = relative36(projectDir, workdir);
99429
99855
  if (rel === ".." || rel.startsWith(`..${sep22}`))
99430
99856
  return;
99431
99857
  return rel && rel !== "." ? rel : undefined;
@@ -100593,7 +101019,7 @@ function countOscillationOutcomes(iterations) {
100593
101019
  }
100594
101020
 
100595
101021
  // src/execution/story-orchestrator/give-up-advice.ts
100596
- import { join as join107, relative as relative36 } from "path";
101022
+ import { join as join107, relative as relative37 } from "path";
100597
101023
  function worstSeverity(findings) {
100598
101024
  return [...findings].sort((a, b) => compareSeverity(b.severity, a.severity))[0]?.severity;
100599
101025
  }
@@ -100666,7 +101092,7 @@ function buildGiveUpHook(args) {
100666
101092
  outputDir: ctx.runtime.outputDir,
100667
101093
  feature,
100668
101094
  runId: ctx.runtime.runId,
100669
- specPath: relative36(repoRoot, join107(featureDir(repoRoot, feature), "spec.md")),
101095
+ specPath: relative37(repoRoot, join107(featureDir(repoRoot, feature), "spec.md")),
100670
101096
  workdir: ctx.packageDir,
100671
101097
  queueHeadsUp: (s, text) => ctx.runtime.advisorHeadsUps.push(s, text)
100672
101098
  });
@@ -102770,7 +103196,7 @@ var init_fix_review_strategy = __esm(() => {
102770
103196
  });
102771
103197
 
102772
103198
  // src/execution/build-plan-for-strategy.ts
102773
- import { join as join108, normalize as normalize7, resolve as resolve42, sep as sep23 } from "path";
103199
+ import { join as join108, normalize as normalize7, resolve as resolve43, sep as sep23 } from "path";
102774
103200
  function requiresInitialRefCapture(strategy) {
102775
103201
  return isThreeSessionStrategy(strategy);
102776
103202
  }
@@ -102785,7 +103211,7 @@ function resolveStoryPathAnchors(ctxPackageDir, storyWorkdir) {
102785
103211
  if (segments.length === 0) {
102786
103212
  return { repoRoot: base, packageDir: base };
102787
103213
  }
102788
- const candidateRoot = resolve42(base, ...Array(segments.length).fill(".."));
103214
+ const candidateRoot = resolve43(base, ...Array(segments.length).fill(".."));
102789
103215
  if (join108(candidateRoot, normalizedRel) === base) {
102790
103216
  return { repoRoot: candidateRoot, packageDir: base };
102791
103217
  }
@@ -105006,7 +105432,7 @@ var init_worktree_id = __esm(() => {
105006
105432
  // src/worktree/manager.ts
105007
105433
  import { existsSync as existsSync25, symlinkSync } from "fs";
105008
105434
  import { mkdir as mkdir34, stat as stat10 } from "fs/promises";
105009
- import { join as join114, resolve as resolve43 } from "path";
105435
+ import { join as join114, resolve as resolve44 } from "path";
105010
105436
  async function resolveGitInfoDir(projectRoot) {
105011
105437
  const dotGit = join114(projectRoot, ".git");
105012
105438
  const isPointerFile = await stat10(dotGit).then((s) => s.isFile(), () => false);
@@ -105021,7 +105447,7 @@ async function resolveGitInfoDir(projectRoot) {
105021
105447
  stderr
105022
105448
  });
105023
105449
  }
105024
- return join114(resolve43(projectRoot, commonDir), "info");
105450
+ return join114(resolve44(projectRoot, commonDir), "info");
105025
105451
  }
105026
105452
  function dropRetiredLines(content, retired) {
105027
105453
  const lines = content.split(`
@@ -107977,6 +108403,204 @@ var init_approval = __esm(() => {
107977
108403
  init_advisor3();
107978
108404
  });
107979
108405
 
108406
+ // src/workflow/run-log.ts
108407
+ function logWorkflow(storyId, message, data = {}) {
108408
+ getSafeLogger2()?.info("workflow", message, { storyId, ...data });
108409
+ }
108410
+ function mirrorLedger(storyId, text) {
108411
+ const capped = text.length > MAX_MIRROR_CHARS ? `${text.slice(0, MAX_MIRROR_CHARS - 3)}...` : text;
108412
+ logWorkflow(storyId, storyId === FEATURE_SCOPE ? capped : `${storyId}: ${capped}`, { ledger: true });
108413
+ }
108414
+ function shortIssue(issue) {
108415
+ const flat = issue.replace(/\s+/g, " ").trim();
108416
+ return flat.length > MAX_ISSUE_CHARS ? `${flat.slice(0, MAX_ISSUE_CHARS - 3)}...` : flat;
108417
+ }
108418
+ function describeFindings(findings) {
108419
+ if (findings.length === 0)
108420
+ return "no findings";
108421
+ const counts = SEVERITY_ORDER2.map((sev) => [sev, findings.filter((f) => f.severity === sev).length]).filter(([, n]) => n > 0).map(([sev, n]) => `${n} ${sev}`);
108422
+ const items = findings.map((f) => f.id ? `${f.id} ${shortIssue(f.issue)}` : shortIssue(f.issue));
108423
+ return `${counts.join(", ")} (${items.join(", ")})`;
108424
+ }
108425
+ function startStep(storyId, label, options = {}) {
108426
+ const now = options.now ?? Date.now;
108427
+ const prefix = storyId === FEATURE_SCOPE ? "" : `${storyId} `;
108428
+ const startedAt = now();
108429
+ if (options.announce !== false) {
108430
+ const model = options.model ? { agentName: options.model.agentName, model: options.model.model } : {};
108431
+ logWorkflow(storyId, `${prefix}${label} start`, { step: label, ...model });
108432
+ }
108433
+ return {
108434
+ done(text, data = {}) {
108435
+ const durationMs = now() - startedAt;
108436
+ const joiner = options.bare ? " " : ": ";
108437
+ logWorkflow(storyId, `${prefix}${label}${joiner}${text} ${formatDuration(durationMs)}`, {
108438
+ step: label,
108439
+ durationMs,
108440
+ ...data
108441
+ });
108442
+ return durationMs;
108443
+ }
108444
+ };
108445
+ }
108446
+ var MAX_ISSUE_CHARS = 60, MAX_MIRROR_CHARS = 200, SEVERITY_ORDER2;
108447
+ var init_run_log = __esm(() => {
108448
+ init_log_format();
108449
+ init_logger2();
108450
+ init_advisor_live();
108451
+ SEVERITY_ORDER2 = ["critical", "important", "minor"];
108452
+ });
108453
+
108454
+ // src/workflow/ledger.ts
108455
+ import { appendFile as appendFile9, mkdir as mkdir35 } from "fs/promises";
108456
+ import { join as join118 } from "path";
108457
+ function ledgerFilePath(runDir) {
108458
+ return join118(runDir, "ledger.md");
108459
+ }
108460
+ function oneLine2(text) {
108461
+ return text.replace(/\s+/g, " ").trim();
108462
+ }
108463
+ async function ensureLedger(runDir, runId, feature) {
108464
+ const path = ledgerFilePath(runDir);
108465
+ if (await Bun.file(path).exists())
108466
+ return;
108467
+ await _ledgerDeps2.mkdir(runDir, { recursive: true });
108468
+ const header = `# Workflow ledger \u2014 run: ${runId} \u2014 feature: ${feature}
108469
+ `;
108470
+ await runStateWrite(path, { content: header }, async () => {
108471
+ await Bun.write(path, header);
108472
+ });
108473
+ }
108474
+ async function appendLedger(runDir, storyId, text, options = {}) {
108475
+ const path = ledgerFilePath(runDir);
108476
+ const line = `${storyId}: ${text}
108477
+ `;
108478
+ await runStateWrite(path, { append: line }, () => _ledgerDeps2.appendFile(path, line));
108479
+ if (options.mirror !== false)
108480
+ mirrorLedger(storyId, text);
108481
+ }
108482
+ var _ledgerDeps2, ledgerText;
108483
+ var init_ledger2 = __esm(() => {
108484
+ init_store();
108485
+ init_run_log();
108486
+ _ledgerDeps2 = { appendFile: appendFile9, mkdir: mkdir35 };
108487
+ ledgerText = {
108488
+ fixRound: (round, max, summary) => `fix round ${round}/${max} (${oneLine2(summary)})`,
108489
+ deferredMinor: (issue) => `deferred-minor: ${oneLine2(issue)}`,
108490
+ ruling: (r) => `Ruling: ${oneLine2(r.text)} \u2014 ${oneLine2(r.why)} \u2014 ${oneLine2(r.costIfWrong)}${r.coercedFrom ? " (coerced from defer: no later review step)" : ""}`,
108491
+ end: (state, summary) => `${state} (${oneLine2(summary)})`,
108492
+ reviewSkipped: (what) => `review-skipped (${oneLine2(what)})`,
108493
+ note: (text) => oneLine2(text)
108494
+ };
108495
+ });
108496
+
108497
+ // src/workflow/preflight.ts
108498
+ import { rename as rename10 } from "fs/promises";
108499
+ import { join as join119 } from "path";
108500
+ function preflightPath(runDir) {
108501
+ return join119(runDir, "preflight.json");
108502
+ }
108503
+ function what(d) {
108504
+ return d.action.type === "supersede" ? `supersede ${describeTarget(d.action.target)}` : d.action.type;
108505
+ }
108506
+ function isRecordObject(v) {
108507
+ return typeof v === "object" && v !== null && !Array.isArray(v);
108508
+ }
108509
+ function isStringArray2(v) {
108510
+ return Array.isArray(v) && v.every((s) => typeof s === "string");
108511
+ }
108512
+ function isOverlapEntry(v) {
108513
+ return isRecordObject(v) && typeof v.storyId === "string" && isStringArray2(v.specSections);
108514
+ }
108515
+ function isConflictRecord(v) {
108516
+ return isRecordObject(v) && typeof v.questionId === "string" && isStringArray2(v.storyIds) && typeof v.summary === "string" && (v.decisionId === null || typeof v.decisionId === "string") && typeof v.action === "string";
108517
+ }
108518
+ function isReusable(v) {
108519
+ if (!isRecordObject(v) || v.schemaVersion !== 1 || !isStringArray2(v.storyIds) || v.fallbackReason)
108520
+ return false;
108521
+ return Array.isArray(v.overlap) && v.overlap.every(isOverlapEntry) && Array.isArray(v.conflicts) && v.conflicts.every(isConflictRecord);
108522
+ }
108523
+ async function readExisting(path) {
108524
+ const file = Bun.file(path);
108525
+ if (!await file.exists())
108526
+ return null;
108527
+ try {
108528
+ const parsed = JSON.parse(await file.text());
108529
+ if (isReusable(parsed))
108530
+ return parsed;
108531
+ getLogger2().warn("workflow", "preflight.json is unusable (wrong shape or failed pre-flight) - re-running", {
108532
+ storyId: "_run",
108533
+ reason: "wrong-shape-or-fallback"
108534
+ });
108535
+ } catch (err) {
108536
+ getLogger2().warn("workflow", "preflight.json is unreadable - re-running pre-flight", {
108537
+ storyId: "_run",
108538
+ reason: err instanceof Error ? err.message : String(err)
108539
+ });
108540
+ }
108541
+ return null;
108542
+ }
108543
+ async function runPreflight(input) {
108544
+ const path = preflightPath(input.runDir);
108545
+ const existing = await readExisting(path);
108546
+ if (existing)
108547
+ return existing;
108548
+ const result = await input.advisor.adviseBatch({
108549
+ kind: "preflight",
108550
+ summary: `Pre-flight for ${input.storyIds.length} stories.`,
108551
+ storyIds: [...input.storyIds]
108552
+ });
108553
+ const items = result.kind === "preflight" ? result.items : [];
108554
+ const conflicts = items.map((item) => ({
108555
+ questionId: item.question.id,
108556
+ storyIds: item.decision?.appliesTo ?? (item.question.storyId ? [item.question.storyId] : []),
108557
+ summary: item.question.summary,
108558
+ decisionId: item.decision?.id ?? null,
108559
+ action: item.decision?.action.type ?? "hold",
108560
+ ...item.fallbackReason ? { fallbackReason: item.fallbackReason } : {}
108561
+ }));
108562
+ const file = {
108563
+ schemaVersion: 1,
108564
+ storyIds: [...input.storyIds],
108565
+ overlap: result.kind === "preflight" ? result.overlap : [],
108566
+ conflicts,
108567
+ ...result.kind === "preflight" && result.fallbackReason ? { fallbackReason: result.fallbackReason } : {}
108568
+ };
108569
+ const tmp = `${path}.tmp`;
108570
+ await Bun.write(tmp, `${JSON.stringify(file, null, 2)}
108571
+ `);
108572
+ await rename10(tmp, path);
108573
+ for (const [i, item] of items.entries()) {
108574
+ const record = conflicts[i];
108575
+ if (!record)
108576
+ continue;
108577
+ const text = item.decision ? ledgerText.ruling({
108578
+ action: "hold",
108579
+ text: `pre-flight ${item.decision.id}: ${what(item.decision)}`,
108580
+ why: item.decision.rationale,
108581
+ costIfWrong: costIfWrongOf(item.decision),
108582
+ source: "advisor"
108583
+ }) : ledgerText.ruling({
108584
+ action: "hold",
108585
+ text: "pre-flight hold (default)",
108586
+ why: item.fallbackReason ?? "no advisor ruling",
108587
+ costIfWrong: "the story runs under the spec as written",
108588
+ source: "default"
108589
+ });
108590
+ for (const storyId of record.storyIds)
108591
+ await input.ledger(storyId, text);
108592
+ }
108593
+ return file;
108594
+ }
108595
+ var isReusablePreflight;
108596
+ var init_preflight = __esm(() => {
108597
+ init_advisor();
108598
+ init_logger2();
108599
+ init_advisor_live();
108600
+ init_ledger2();
108601
+ isReusablePreflight = isReusable;
108602
+ });
108603
+
107980
108604
  // src/workflow/open-items.ts
107981
108605
  function findingItem(f, r) {
107982
108606
  return {
@@ -108024,8 +108648,8 @@ var init_open_items = __esm(() => {
108024
108648
  });
108025
108649
 
108026
108650
  // src/workflow/result.ts
108027
- import { mkdir as mkdir35, readdir as readdir10, rename as rename10, stat as stat11 } from "fs/promises";
108028
- import { basename as basename18, join as join118 } from "path";
108651
+ import { mkdir as mkdir36, readdir as readdir10, rename as rename11, stat as stat11 } from "fs/promises";
108652
+ import { basename as basename18, join as join120 } from "path";
108029
108653
  function newStoryRecord(storyId, maxRounds, now) {
108030
108654
  return {
108031
108655
  storyId,
@@ -108044,7 +108668,7 @@ function newStoryRecord(storyId, maxRounds, now) {
108044
108668
  };
108045
108669
  }
108046
108670
  async function readResult(runDir) {
108047
- const file = Bun.file(join118(runDir, RESULT_FILE));
108671
+ const file = Bun.file(join120(runDir, RESULT_FILE));
108048
108672
  if (!await file.exists())
108049
108673
  return null;
108050
108674
  try {
@@ -108080,7 +108704,7 @@ async function writePreflightTokens(runDir, header, byRole) {
108080
108704
  }
108081
108705
  async function writeResult(runDir, next) {
108082
108706
  await _resultDeps.mkdir(runDir, { recursive: true });
108083
- const path = join118(runDir, RESULT_FILE);
108707
+ const path = join120(runDir, RESULT_FILE);
108084
108708
  const tmp = `${path}.tmp`;
108085
108709
  const text = `${JSON.stringify(next, null, 2)}
108086
108710
  `;
@@ -108109,9 +108733,9 @@ async function runDirsNewestFirst(featureDir) {
108109
108733
  }
108110
108734
  const dated = [];
108111
108735
  for (const name of entries) {
108112
- const runDir = join118(featureDir, name);
108736
+ const runDir = join120(featureDir, name);
108113
108737
  try {
108114
- dated.push({ runDir, mtimeMs: (await _resultDeps.stat(join118(runDir, RESULT_FILE))).mtimeMs });
108738
+ dated.push({ runDir, mtimeMs: (await _resultDeps.stat(join120(runDir, RESULT_FILE))).mtimeMs });
108115
108739
  } catch {}
108116
108740
  }
108117
108741
  return [...dated].sort((a, b) => b.mtimeMs - a.mtimeMs).map((d) => d.runDir);
@@ -108133,7 +108757,120 @@ var _resultDeps, RESULT_FILE = "result.json";
108133
108757
  var init_result = __esm(() => {
108134
108758
  init_store();
108135
108759
  init_open_items();
108136
- _resultDeps = { mkdir: mkdir35, readdir: readdir10, rename: rename10, stat: stat11 };
108760
+ _resultDeps = { mkdir: mkdir36, readdir: readdir10, rename: rename11, stat: stat11 };
108761
+ });
108762
+
108763
+ // src/workflow/feature-record.ts
108764
+ import { basename as basename19 } from "path";
108765
+ function newFeatureRecord(input) {
108766
+ return {
108767
+ step: "repo-gates",
108768
+ startedAt: input.now,
108769
+ ...input.head ? { startHead: input.head } : {},
108770
+ seed: input.seed,
108771
+ gateFindings: [],
108772
+ reviewRan: false,
108773
+ reviewOpen: [],
108774
+ minors: input.minors,
108775
+ newMinors: [],
108776
+ nextFindingId: 1,
108777
+ fixRan: false,
108778
+ rulings: [],
108779
+ reasons: [],
108780
+ timings: {},
108781
+ tokensByRole: {}
108782
+ };
108783
+ }
108784
+ function minorsFrom(deferred) {
108785
+ return deferred.map((m, i) => ({ id: `M${i + 1}`, storyId: m.storyId, text: m.text, triage: "leave" }));
108786
+ }
108787
+ async function collectStoryRecords(featureDir) {
108788
+ const found = new Map;
108789
+ for (const runDir of await runDirsNewestFirst(featureDir)) {
108790
+ const stories = (await readResult(runDir))?.stories ?? {};
108791
+ for (const [id, record] of Object.entries(stories)) {
108792
+ if (!found.has(id))
108793
+ found.set(id, { record, runDir });
108794
+ }
108795
+ }
108796
+ return Object.fromEntries(found);
108797
+ }
108798
+ function isFeatureRecord(v) {
108799
+ if (typeof v !== "object" || v === null)
108800
+ return false;
108801
+ const o = v;
108802
+ return FEATURE_STEPS.includes(o.step) && typeof o.nextFindingId === "number" && Array.isArray(o.gateFindings) && Array.isArray(o.reviewOpen) && Array.isArray(o.minors) && Array.isArray(o.rulings) && Array.isArray(o.reasons);
108803
+ }
108804
+ async function loadPriorFeature(featureDir, currentRunId) {
108805
+ for (const runDir of await runDirsNewestFirst(featureDir)) {
108806
+ if (basename19(runDir) === currentRunId)
108807
+ continue;
108808
+ const feature = (await readResult(runDir))?.featureWorkflow;
108809
+ if (feature === undefined)
108810
+ continue;
108811
+ if (isFeatureRecord(feature))
108812
+ return feature;
108813
+ getSafeLogger2()?.warn("workflow", "Prior feature record is malformed \u2014 ignored", { storyId: "_run", runDir });
108814
+ }
108815
+ return null;
108816
+ }
108817
+ function fromRecord(id, title, located) {
108818
+ const r = located.record;
108819
+ if (!r.state)
108820
+ return null;
108821
+ return {
108822
+ id,
108823
+ title,
108824
+ state: r.state,
108825
+ ...r.reason ? { reason: r.reason } : {},
108826
+ rounds: r.round,
108827
+ unreviewed: r.unreviewed,
108828
+ flagged: r.flagged ?? 0,
108829
+ runDir: located.runDir
108830
+ };
108831
+ }
108832
+ function summariseStories(prd, records) {
108833
+ return prd.userStories.map((s) => {
108834
+ const located = records[s.id];
108835
+ const closed = located ? fromRecord(s.id, s.title, located) : null;
108836
+ if (closed)
108837
+ return closed;
108838
+ const base = { id: s.id, title: s.title, unreviewed: false, flagged: 0 };
108839
+ if (s.passes || s.status === "passed")
108840
+ return { ...base, state: "complete", reason: PASSED_EARLIER, rounds: 0 };
108841
+ return { ...base, state: "not-run", rounds: located?.record.round ?? 0 };
108842
+ });
108843
+ }
108844
+ async function newestPreflight(featureDir) {
108845
+ for (const runDir of await runDirsNewestFirst(featureDir)) {
108846
+ const path = preflightPath(runDir);
108847
+ const file = Bun.file(path);
108848
+ if (!await file.exists())
108849
+ continue;
108850
+ try {
108851
+ const parsed = JSON.parse(await file.text());
108852
+ if (isReusablePreflight(parsed))
108853
+ return { path, file: parsed };
108854
+ } catch {}
108855
+ }
108856
+ return null;
108857
+ }
108858
+ var FEATURE_STEPS, PASSED_EARLIER = "passed earlier, no workflow record";
108859
+ var init_feature_record = __esm(() => {
108860
+ init_logger2();
108861
+ init_preflight();
108862
+ init_result();
108863
+ FEATURE_STEPS = [
108864
+ "repo-gates",
108865
+ "branch-review",
108866
+ "branch-fix",
108867
+ "branch-rereview",
108868
+ "adjudicate",
108869
+ "final-gates",
108870
+ "gate-repair",
108871
+ "publish",
108872
+ "done"
108873
+ ];
108137
108874
  });
108138
108875
 
108139
108876
  // src/workflow/rulings.ts
@@ -108180,8 +108917,8 @@ var init_step_context = __esm(() => {
108180
108917
  });
108181
108918
 
108182
108919
  // src/workflow/brief.ts
108183
- import { mkdir as mkdir36 } from "fs/promises";
108184
- import { isAbsolute as isAbsolute30, join as join119 } from "path";
108920
+ import { mkdir as mkdir37 } from "fs/promises";
108921
+ import { dirname as dirname30, isAbsolute as isAbsolute31, join as join121 } from "path";
108185
108922
  function specExcerpts(specText, storyId) {
108186
108923
  const sections = specText.split(/^(?=#{1,6} )/m).map((s) => s.trim()).filter(Boolean);
108187
108924
  const hits = sections.filter((s) => s.includes(storyId));
@@ -108208,7 +108945,7 @@ async function loadSpec(sc) {
108208
108945
  const rel = resolved.specSource?.path;
108209
108946
  if (!rel)
108210
108947
  return { excerpts: [] };
108211
- const abs = isAbsolute30(rel) ? rel : join119(sc.projectDir, rel);
108948
+ const abs = isAbsolute31(rel) ? rel : join121(sc.projectDir, rel);
108212
108949
  const file = Bun.file(abs);
108213
108950
  return { path: rel, excerpts: await file.exists() ? specExcerpts(await file.text(), sc.story.id) : [] };
108214
108951
  } catch {
@@ -108216,8 +108953,8 @@ async function loadSpec(sc) {
108216
108953
  }
108217
108954
  }
108218
108955
  async function redBaseDependencies(sc) {
108219
- const result = await readResult(sc.runDir);
108220
- return (sc.story.dependencies ?? []).map((id) => result?.stories[id]).filter((r) => r?.state === "complete-gate-open").map((r) => ({ storyId: r?.storyId ?? "", reason: r?.reason ?? "gate red" }));
108956
+ const records = await collectStoryRecords(dirname30(sc.runDir));
108957
+ return (sc.story.dependencies ?? []).map((id) => records[id]?.record).filter((r) => r?.state === "complete-gate-open").map((r) => ({ storyId: r?.storyId ?? "", reason: r?.reason ?? "gate red" }));
108221
108958
  }
108222
108959
  function contextFilePaths(sc) {
108223
108960
  return (sc.story.contextFiles ?? []).map((entry) => typeof entry === "string" ? entry : entry.path);
@@ -108251,35 +108988,35 @@ async function writeBrief(sc) {
108251
108988
  redBaseDependencies: redBase,
108252
108989
  reportPath: sc.reportPath
108253
108990
  });
108254
- await mkdir36(sc.storyDir, { recursive: true });
108991
+ await mkdir37(sc.storyDir, { recursive: true });
108255
108992
  await Bun.write(sc.briefPath, text);
108256
108993
  }
108257
108994
  var MAX_EXCERPT_CHARS = 12000;
108258
108995
  var init_brief = __esm(() => {
108259
108996
  init_logger2();
108260
108997
  init_prompts();
108261
- init_result();
108998
+ init_feature_record();
108262
108999
  init_rulings();
108263
109000
  init_step_context();
108264
109001
  });
108265
109002
 
108266
109003
  // src/workflow/paths.ts
108267
- import { join as join120 } from "path";
109004
+ import { join as join122 } from "path";
108268
109005
  function workflowFeatureDir(projectDir, feature) {
108269
- return join120(featureDir(projectDir, feature), "workflow");
109006
+ return join122(featureDir(projectDir, feature), "workflow");
108270
109007
  }
108271
109008
  function workflowRunDir(projectDir, feature, runId) {
108272
- return join120(workflowFeatureDir(projectDir, feature), runId);
109009
+ return join122(workflowFeatureDir(projectDir, feature), runId);
108273
109010
  }
108274
109011
  function workflowStoryDir(runDir, storyId) {
108275
- return join120(runDir, storyId);
109012
+ return join122(runDir, storyId);
108276
109013
  }
108277
109014
  var init_paths5 = __esm(() => {
108278
109015
  init_config();
108279
109016
  });
108280
109017
 
108281
109018
  // src/workflow/feature-context.ts
108282
- import { join as join121 } from "path";
109019
+ import { join as join123 } from "path";
108283
109020
  async function buildFeatureContext(input) {
108284
109021
  const storyPackageDirs = input.prd.userStories.map(storyPackageDir);
108285
109022
  const dispatchAsk = await _featureContextDeps2.buildRunDispatchAskWiring({
@@ -108320,8 +109057,8 @@ async function buildFeatureContext(input) {
108320
109057
  outputDir: input.runtime.outputDir,
108321
109058
  runDir,
108322
109059
  featureDir: workflowFeatureDir(input.workdir, input.feature),
108323
- prdPath: join121(featureDir(input.workdir, input.feature), "prd.json"),
108324
- reportPath: join121(runDir, "feature-report.md"),
109060
+ prdPath: join123(featureDir(input.workdir, input.feature), "prd.json"),
109061
+ reportPath: join123(runDir, "feature-report.md"),
108325
109062
  packageDirs: [...new Set(storyPackageDirs.filter((d) => typeof d === "string" && d !== ""))],
108326
109063
  ...input.abortSignal ? { runSignal: input.abortSignal } : {},
108327
109064
  dryRun: input.runtime.dryRun === true
@@ -108337,97 +109074,6 @@ var init_feature_context3 = __esm(() => {
108337
109074
  _featureContextDeps2 = { buildRunDispatchAskWiring };
108338
109075
  });
108339
109076
 
108340
- // src/workflow/run-log.ts
108341
- function logWorkflow(storyId, message, data = {}) {
108342
- getSafeLogger2()?.info("workflow", message, { storyId, ...data });
108343
- }
108344
- function mirrorLedger(storyId, text) {
108345
- const capped = text.length > MAX_MIRROR_CHARS ? `${text.slice(0, MAX_MIRROR_CHARS - 3)}...` : text;
108346
- logWorkflow(storyId, storyId === FEATURE_SCOPE ? capped : `${storyId}: ${capped}`, { ledger: true });
108347
- }
108348
- function shortIssue(issue) {
108349
- const flat = issue.replace(/\s+/g, " ").trim();
108350
- return flat.length > MAX_ISSUE_CHARS ? `${flat.slice(0, MAX_ISSUE_CHARS - 3)}...` : flat;
108351
- }
108352
- function describeFindings(findings) {
108353
- if (findings.length === 0)
108354
- return "no findings";
108355
- const counts = SEVERITY_ORDER2.map((sev) => [sev, findings.filter((f) => f.severity === sev).length]).filter(([, n]) => n > 0).map(([sev, n]) => `${n} ${sev}`);
108356
- const items = findings.map((f) => f.id ? `${f.id} ${shortIssue(f.issue)}` : shortIssue(f.issue));
108357
- return `${counts.join(", ")} (${items.join(", ")})`;
108358
- }
108359
- function startStep(storyId, label, options = {}) {
108360
- const now = options.now ?? Date.now;
108361
- const prefix = storyId === FEATURE_SCOPE ? "" : `${storyId} `;
108362
- const startedAt = now();
108363
- if (options.announce !== false) {
108364
- const model = options.model ? { agentName: options.model.agentName, model: options.model.model } : {};
108365
- logWorkflow(storyId, `${prefix}${label} start`, { step: label, ...model });
108366
- }
108367
- return {
108368
- done(text, data = {}) {
108369
- const durationMs = now() - startedAt;
108370
- const joiner = options.bare ? " " : ": ";
108371
- logWorkflow(storyId, `${prefix}${label}${joiner}${text} ${formatDuration(durationMs)}`, {
108372
- step: label,
108373
- durationMs,
108374
- ...data
108375
- });
108376
- return durationMs;
108377
- }
108378
- };
108379
- }
108380
- var MAX_ISSUE_CHARS = 60, MAX_MIRROR_CHARS = 200, SEVERITY_ORDER2;
108381
- var init_run_log = __esm(() => {
108382
- init_log_format();
108383
- init_logger2();
108384
- init_advisor_live();
108385
- SEVERITY_ORDER2 = ["critical", "important", "minor"];
108386
- });
108387
-
108388
- // src/workflow/ledger.ts
108389
- import { appendFile as appendFile9, mkdir as mkdir37 } from "fs/promises";
108390
- import { join as join122 } from "path";
108391
- function ledgerFilePath(runDir) {
108392
- return join122(runDir, "ledger.md");
108393
- }
108394
- function oneLine2(text) {
108395
- return text.replace(/\s+/g, " ").trim();
108396
- }
108397
- async function ensureLedger(runDir, runId, feature) {
108398
- const path = ledgerFilePath(runDir);
108399
- if (await Bun.file(path).exists())
108400
- return;
108401
- await _ledgerDeps2.mkdir(runDir, { recursive: true });
108402
- const header = `# Workflow ledger \u2014 run: ${runId} \u2014 feature: ${feature}
108403
- `;
108404
- await runStateWrite(path, { content: header }, async () => {
108405
- await Bun.write(path, header);
108406
- });
108407
- }
108408
- async function appendLedger(runDir, storyId, text, options = {}) {
108409
- const path = ledgerFilePath(runDir);
108410
- const line = `${storyId}: ${text}
108411
- `;
108412
- await runStateWrite(path, { append: line }, () => _ledgerDeps2.appendFile(path, line));
108413
- if (options.mirror !== false)
108414
- mirrorLedger(storyId, text);
108415
- }
108416
- var _ledgerDeps2, ledgerText;
108417
- var init_ledger2 = __esm(() => {
108418
- init_store();
108419
- init_run_log();
108420
- _ledgerDeps2 = { appendFile: appendFile9, mkdir: mkdir37 };
108421
- ledgerText = {
108422
- fixRound: (round, max, summary) => `fix round ${round}/${max} (${oneLine2(summary)})`,
108423
- deferredMinor: (issue) => `deferred-minor: ${oneLine2(issue)}`,
108424
- ruling: (r) => `Ruling: ${oneLine2(r.text)} \u2014 ${oneLine2(r.why)} \u2014 ${oneLine2(r.costIfWrong)}${r.coercedFrom ? " (coerced from defer: no later review step)" : ""}`,
108425
- end: (state, summary) => `${state} (${oneLine2(summary)})`,
108426
- reviewSkipped: (what) => `review-skipped (${oneLine2(what)})`,
108427
- note: (text) => oneLine2(text)
108428
- };
108429
- });
108430
-
108431
109077
  // src/workflow/adjudicate.ts
108432
109078
  async function adjudicateResidual(advisor, req, ledger) {
108433
109079
  let answer = { rulings: [], diagnoses: {} };
@@ -108480,7 +109126,7 @@ var init_adjudicate = __esm(() => {
108480
109126
 
108481
109127
  // src/workflow/git.ts
108482
109128
  import { mkdir as mkdir38 } from "fs/promises";
108483
- import { dirname as dirname30, relative as relative37 } from "path";
109129
+ import { dirname as dirname31, relative as relative38 } from "path";
108484
109130
  function ledgerCommitMessage(feature) {
108485
109131
  return `chore(${feature}): advisor decisions`;
108486
109132
  }
@@ -108493,7 +109139,7 @@ function ledgerGitError(path, step, result) {
108493
109139
  });
108494
109140
  }
108495
109141
  async function commitLedger(projectDir, feature) {
108496
- const path = relative37(projectDir, ledgerPath(projectDir, feature));
109142
+ const path = relative38(projectDir, ledgerPath(projectDir, feature));
108497
109143
  if (!await Bun.file(ledgerPath(projectDir, feature)).exists())
108498
109144
  return false;
108499
109145
  const added = await _workflowDeps.git(["add", "-f", "--", path], projectDir);
@@ -108590,7 +109236,7 @@ async function writeBranchReviewPackage(projectDir, from, path) {
108590
109236
  const changedSection = `## Changed files (${records.length})${lines.length > 0 ? `
108591
109237
  ${lines.join(`
108592
109238
  `)}` : ""}`;
108593
- await mkdir38(dirname30(path), { recursive: true });
109239
+ await mkdir38(dirname31(path), { recursive: true });
108594
109240
  await Bun.write(path, `## Commits (${range})
108595
109241
  ${log.stdout.trim() || "(none)"}
108596
109242
 
@@ -108619,7 +109265,7 @@ async function writeReviewPackage(projectDir, from, path) {
108619
109265
  }
108620
109266
  const body = truncateDiff(diff.stdout, stat.stdout);
108621
109267
  const omitted = body === diff.stdout ? "" : omittedFilesSection(names.stdout, body);
108622
- await mkdir38(dirname30(path), { recursive: true });
109268
+ await mkdir38(dirname31(path), { recursive: true });
108623
109269
  await Bun.write(path, `## Commits (${range})
108624
109270
  ${log.stdout.trim() || "(none)"}
108625
109271
 
@@ -108630,6 +109276,16 @@ ${stat.stdout.trim() || "(no changes)"}
108630
109276
  ${body}
108631
109277
  ${omitted}`);
108632
109278
  }
109279
+ async function readDefaultBranch(projectDir) {
109280
+ try {
109281
+ const result = await _workflowDeps.git(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], projectDir);
109282
+ if (result.exitCode !== 0)
109283
+ return;
109284
+ return result.stdout.trim().replace(/^origin\//, "") || undefined;
109285
+ } catch {
109286
+ return;
109287
+ }
109288
+ }
108633
109289
  var DIFF_CONTEXT_LINES = 10;
108634
109290
  var init_git3 = __esm(() => {
108635
109291
  init_advisor();
@@ -108702,12 +109358,12 @@ var init_branch_fix = __esm(() => {
108702
109358
  });
108703
109359
 
108704
109360
  // src/workflow/review-coverage.ts
108705
- function basename19(path) {
109361
+ function basename20(path) {
108706
109362
  const cut = path.lastIndexOf("/");
108707
109363
  return cut === -1 ? path : path.slice(cut + 1);
108708
109364
  }
108709
109365
  function isNonSource(path) {
108710
- const base = basename19(path).toLowerCase();
109366
+ const base = basename20(path).toLowerCase();
108711
109367
  return LOCKFILE_BASENAMES2.has(base) || NON_SOURCE_SUFFIXES.some((suffix) => base.endsWith(suffix));
108712
109368
  }
108713
109369
  function owningPackage(path, packageDirs) {
@@ -108776,7 +109432,7 @@ var init_review_coverage = __esm(() => {
108776
109432
  });
108777
109433
 
108778
109434
  // src/workflow/story-review.ts
108779
- import { join as join123 } from "path";
109435
+ import { join as join124 } from "path";
108780
109436
  async function underReviewLimit(sc, writePackage, run) {
108781
109437
  try {
108782
109438
  await writePackage();
@@ -108800,7 +109456,7 @@ async function underReviewLimit(sc, writePackage, run) {
108800
109456
  }
108801
109457
  async function writeVerdictSafe(sc, file, verdict) {
108802
109458
  try {
108803
- await Bun.write(join123(sc.storyDir, file), `${JSON.stringify(verdict, null, 2)}
109459
+ await Bun.write(join124(sc.storyDir, file), `${JSON.stringify(verdict, null, 2)}
108804
109460
  `);
108805
109461
  } catch (err) {
108806
109462
  getLogger2().warn("workflow", `Could not write ${file}, continuing`, {
@@ -108810,7 +109466,7 @@ async function writeVerdictSafe(sc, file, verdict) {
108810
109466
  }
108811
109467
  }
108812
109468
  async function runStoryReview(sc, concerns, gates = {}) {
108813
- const packagePath = join123(sc.storyDir, "package-0.diff");
109469
+ const packagePath = join124(sc.storyDir, "package-0.diff");
108814
109470
  const prompt = buildWorkflowStoryReviewPrompt({
108815
109471
  briefPath: sc.briefPath,
108816
109472
  reportPath: sc.reportPath,
@@ -108837,10 +109493,10 @@ var init_story_review = __esm(() => {
108837
109493
  });
108838
109494
 
108839
109495
  // src/workflow/branch-review.ts
108840
- import { join as join124 } from "path";
109496
+ import { join as join125 } from "path";
108841
109497
  async function writeFeatureArtifact(fc, name, value) {
108842
109498
  try {
108843
- await Bun.write(join124(fc.runDir, name), `${JSON.stringify(value, null, 2)}
109499
+ await Bun.write(join125(fc.runDir, name), `${JSON.stringify(value, null, 2)}
108844
109500
  `);
108845
109501
  } catch (err) {
108846
109502
  getLogger2().warn("workflow", `Could not write ${name}, continuing`, { storyId: "_run", error: errorMessage(err) });
@@ -108850,7 +109506,7 @@ function branchReviewModel(fc) {
108850
109506
  return fc.workflow.models.branchReview ?? fc.config.finish?.reviewers?.quality ?? undefined;
108851
109507
  }
108852
109508
  async function runBranchReview(fc, input) {
108853
- const packagePath = join124(fc.runDir, "package-branch.diff");
109509
+ const packagePath = join125(fc.runDir, "package-branch.diff");
108854
109510
  const range = `${input.seed}..HEAD`;
108855
109511
  const prompt = buildWorkflowBranchReviewPrompt({
108856
109512
  ...input.specPath ? { specPath: input.specPath } : {},
@@ -108869,15 +109525,16 @@ async function runBranchReview(fc, input) {
108869
109525
  timeoutMs: fc.workflow.timeouts.reviewMs,
108870
109526
  minorIds: input.facts.minors.map((m) => m.id),
108871
109527
  requiredFiles,
109528
+ repoRoot: fc.projectDir,
108872
109529
  range
108873
109530
  }));
108874
109531
  if (!outcome.ran)
108875
109532
  return outcome;
108876
- const unread = unreadFiles(requiredFiles, outcome.verdict.inspectedFiles ?? []);
109533
+ const unread = unreadFiles(requiredFiles, outcome.verdict.inspectedFiles ?? [], fc.projectDir);
108877
109534
  return unread.length > 0 ? { ran: true, verdict: { ...outcome.verdict, unreadFiles: unread } } : outcome;
108878
109535
  }
108879
109536
  async function runBranchReReview(fc, input) {
108880
- const packagePath = join124(fc.runDir, "package-branch-fix.diff");
109537
+ const packagePath = join125(fc.runDir, "package-branch-fix.diff");
108881
109538
  const prompt = buildWorkflowBranchReReviewPrompt({
108882
109539
  ...input.specPath ? { specPath: input.specPath } : {},
108883
109540
  reportPath: fc.reportPath,
@@ -108913,12 +109570,12 @@ var init_cost_limit = __esm(() => {
108913
109570
  });
108914
109571
 
108915
109572
  // src/finish/a3-hints.ts
108916
- import { join as join125 } from "path";
109573
+ import { join as join126 } from "path";
108917
109574
  async function listJson(dir) {
108918
109575
  const glob = new Bun.Glob("*.json");
108919
109576
  const out = [];
108920
109577
  for await (const name of glob.scan({ cwd: dir, absolute: false }))
108921
- out.push(join125(dir, name));
109578
+ out.push(join126(dir, name));
108922
109579
  return out.sort((a, b) => a.localeCompare(b));
108923
109580
  }
108924
109581
  function hintsFromRecord(raw) {
@@ -108946,7 +109603,7 @@ function hintsFromRecord(raw) {
108946
109603
  return out;
108947
109604
  }
108948
109605
  async function readA3Hints(outputDir, feature) {
108949
- const dir = join125(outputDir, "nbf-worth-check", feature);
109606
+ const dir = join126(outputDir, "nbf-worth-check", feature);
108950
109607
  let files;
108951
109608
  try {
108952
109609
  files = await _a3HintsDeps.listJson(dir);
@@ -108973,7 +109630,7 @@ var init_a3_hints = __esm(() => {
108973
109630
  });
108974
109631
 
108975
109632
  // src/finish/acceptance-evidence.ts
108976
- import { basename as basename20, extname, join as join126 } from "path";
109633
+ import { basename as basename21, extname, join as join127 } from "path";
108977
109634
  function splitAcBlocks(text) {
108978
109635
  const lines = text.split(`
108979
109636
  `);
@@ -109001,7 +109658,7 @@ ${f.problem}`.matchAll(BACKTICK))
109001
109658
  for (const m of f.title.matchAll(CODE_WORD))
109002
109659
  ids.add(m[0]);
109003
109660
  if (f.file) {
109004
- const base = basename20(f.file, extname(f.file));
109661
+ const base = basename21(f.file, extname(f.file));
109005
109662
  if (!GENERIC_BASENAMES.has(base))
109006
109663
  ids.add(base);
109007
109664
  }
@@ -109022,7 +109679,7 @@ function findPinnedAcs(files, f) {
109022
109679
  return scored.filter((p) => p.score >= 1).sort((a, b) => b.score - a.score).slice(0, MAX_PINNED_BLOCKS);
109023
109680
  }
109024
109681
  async function loadAcceptanceFiles(workdir, paths) {
109025
- const texts = await Promise.all(paths.map((p) => _acceptanceEvidenceDeps.readText(join126(workdir, p))));
109682
+ const texts = await Promise.all(paths.map((p) => _acceptanceEvidenceDeps.readText(join127(workdir, p))));
109026
109683
  return paths.flatMap((path, i) => {
109027
109684
  const text = texts[i];
109028
109685
  return typeof text === "string" ? [{ path, text }] : [];
@@ -109041,17 +109698,17 @@ var init_acceptance_evidence = __esm(() => {
109041
109698
 
109042
109699
  // src/finish/safe-path.ts
109043
109700
  import { realpath as realpath4 } from "fs/promises";
109044
- import { basename as basename21, dirname as dirname31, join as join127, relative as relative38, sep as sep24 } from "path";
109701
+ import { basename as basename22, dirname as dirname32, join as join128, relative as relative39, sep as sep24 } from "path";
109045
109702
  async function insideRoot(root, target) {
109046
109703
  const realRoot = await realpath4(root).catch(() => null);
109047
109704
  if (realRoot === null)
109048
109705
  return false;
109049
- let dir = dirname31(target);
109706
+ let dir = dirname32(target);
109050
109707
  for (;; ) {
109051
109708
  const real = await realpath4(dir).catch(() => null);
109052
109709
  if (real !== null)
109053
109710
  return real === realRoot || real.startsWith(realRoot + sep24);
109054
- const up = dirname31(dir);
109711
+ const up = dirname32(dir);
109055
109712
  if (up === dir)
109056
109713
  return false;
109057
109714
  dir = up;
@@ -109061,12 +109718,12 @@ async function realpathLoose(target) {
109061
109718
  const real = await realpath4(target).catch(() => null);
109062
109719
  if (real !== null)
109063
109720
  return real;
109064
- const up = dirname31(target);
109065
- return up === target ? target : join127(await realpathLoose(up), basename21(target));
109721
+ const up = dirname32(target);
109722
+ return up === target ? target : join128(await realpathLoose(up), basename22(target));
109066
109723
  }
109067
109724
  async function relativeInsideRoot(root, dir) {
109068
109725
  const [realRoot, realDir] = await Promise.all([realpathLoose(root), realpathLoose(dir)]);
109069
- const rel = relative38(realRoot, realDir);
109726
+ const rel = relative39(realRoot, realDir);
109070
109727
  if (rel === "" || rel.startsWith("..") || rel.startsWith(sep24))
109071
109728
  return null;
109072
109729
  return rel.split(sep24).join("/");
@@ -109214,7 +109871,7 @@ var init_set_aside_git = __esm(() => {
109214
109871
  });
109215
109872
 
109216
109873
  // src/finish/acceptance-guard.ts
109217
- import { join as join128 } from "path";
109874
+ import { join as join129 } from "path";
109218
109875
  function acceptancePathsOf(context) {
109219
109876
  if (context.acceptanceStatus === "disabled")
109220
109877
  return [];
@@ -109243,7 +109900,7 @@ function changedAcs(before, after) {
109243
109900
  }
109244
109901
  async function acsChangedIn(root, path) {
109245
109902
  const head = await gitLiteral(["show", `HEAD:${path}`], root);
109246
- const after = await _guardDeps.readText(join128(root, path));
109903
+ const after = await _guardDeps.readText(join129(root, path));
109247
109904
  return changedAcs(head.exitCode === 0 ? head.stdout : "", after ?? "");
109248
109905
  }
109249
109906
  async function restoreFromHead(root, paths) {
@@ -109821,7 +110478,7 @@ function draftReason(f) {
109821
110478
  parts.push(`${f.unreviewedFixCommits.length} unreviewed fix commit(s)`);
109822
110479
  return parts.join("; ");
109823
110480
  }
109824
- async function resolve44(args) {
110481
+ async function resolve45(args) {
109825
110482
  const { facts } = args;
109826
110483
  if (isClean(facts)) {
109827
110484
  if ((facts.restoreFailed?.length ?? 0) > 0)
@@ -109858,7 +110515,7 @@ var init_resolve = __esm(() => {
109858
110515
  });
109859
110516
 
109860
110517
  // src/finish/advise.ts
109861
- import { relative as relative39 } from "path";
110518
+ import { relative as relative40 } from "path";
109862
110519
  async function reusablePrior(deps, f, key) {
109863
110520
  if (BLOCKING3.has(f.severity))
109864
110521
  return;
@@ -109993,7 +110650,7 @@ function createFinishAdvisor(deps) {
109993
110650
  return { action, rationale: decision.rationale };
109994
110651
  },
109995
110652
  async commitLedger(state) {
109996
- const path = relative39(state.workdir, ledgerPath(deps.repoRoot, deps.feature));
110653
+ const path = relative40(state.workdir, ledgerPath(deps.repoRoot, deps.feature));
109997
110654
  const added = await git(["add", "-f", "--", path], state.workdir);
109998
110655
  if (added.exitCode !== 0)
109999
110656
  throw ledgerGitError2("add", added);
@@ -110047,15 +110704,15 @@ var init_advise2 = __esm(() => {
110047
110704
 
110048
110705
  // src/finish/audit.ts
110049
110706
  import { appendFile as appendFile10, mkdir as mkdir39, readFile as readFile10, writeFile as writeFile12 } from "fs/promises";
110050
- import { join as join129 } from "path";
110707
+ import { join as join130 } from "path";
110051
110708
  function roundsPath(t) {
110052
- return join129(t.auditDir, `${t.runId}.jsonl`);
110709
+ return join130(t.auditDir, `${t.runId}.jsonl`);
110053
110710
  }
110054
110711
  function resultPath(t) {
110055
- return join129(t.auditDir, `${t.runId}.result.json`);
110712
+ return join130(t.auditDir, `${t.runId}.result.json`);
110056
110713
  }
110057
110714
  function ledgerPath2(auditDir) {
110058
- return join129(auditDir, "last.json");
110715
+ return join130(auditDir, "last.json");
110059
110716
  }
110060
110717
  async function updateLedger(t, result) {
110061
110718
  if (!result.headSha || !result.branch)
@@ -110213,14 +110870,14 @@ var init_state = __esm(() => {
110213
110870
  });
110214
110871
 
110215
110872
  // src/finish/checkpoint.ts
110216
- import { mkdir as mkdir40, rename as rename11, rm as rm11, writeFile as writeFile13 } from "fs/promises";
110217
- import { join as join130 } from "path";
110873
+ import { mkdir as mkdir40, rename as rename12, rm as rm11, writeFile as writeFile13 } from "fs/promises";
110874
+ import { join as join131 } from "path";
110218
110875
  function checkpointPath(audit) {
110219
- return join130(audit.auditDir, `${audit.runId}.checkpoint.json`);
110876
+ return join131(audit.auditDir, `${audit.runId}.checkpoint.json`);
110220
110877
  }
110221
110878
  async function writeCheckpoint(audit, state) {
110222
110879
  const target = checkpointPath(audit);
110223
- const tmp = join130(audit.auditDir, `${audit.runId}.checkpoint.${_checkpointDeps.uniqueId()}.tmp`);
110880
+ const tmp = join131(audit.auditDir, `${audit.runId}.checkpoint.${_checkpointDeps.uniqueId()}.tmp`);
110224
110881
  try {
110225
110882
  const text = serializeFinishState(state);
110226
110883
  await _checkpointDeps.mkdir(audit.auditDir);
@@ -110246,7 +110903,7 @@ var init_checkpoint2 = __esm(() => {
110246
110903
  await mkdir40(path, { recursive: true });
110247
110904
  },
110248
110905
  writeFile: (path, text) => writeFile13(path, text, "utf8"),
110249
- rename: (from, to) => rename11(from, to),
110906
+ rename: (from, to) => rename12(from, to),
110250
110907
  remove: (path) => rm11(path, { force: true }),
110251
110908
  uniqueId: () => crypto.randomUUID()
110252
110909
  };
@@ -110967,7 +111624,7 @@ var init_acceptance4 = __esm(() => {
110967
111624
  });
110968
111625
 
110969
111626
  // src/finish/gates/quality.ts
110970
- import { join as join132 } from "path";
111627
+ import { join as join133 } from "path";
110971
111628
  function overlayCommandsOf(override) {
110972
111629
  const commands = override?.quality?.commands;
110973
111630
  return commands !== undefined && Object.keys(commands).length > 0 ? commands : undefined;
@@ -110976,7 +111633,7 @@ async function resolveGateCommands(repoRoot, packageDirs) {
110976
111633
  const seen = new Set;
110977
111634
  const out = [];
110978
111635
  const add = (name, command, cwd, setup) => {
110979
- if (command === undefined)
111636
+ if (command === undefined || !(typeof command === "string" ? [command] : command).some((part) => part.trim()))
110980
111637
  return;
110981
111638
  const key = `${cwd}::${JSON.stringify(command)}::${JSON.stringify(setup ?? null)}`;
110982
111639
  if (seen.has(key))
@@ -110997,11 +111654,12 @@ async function resolveGateCommands(repoRoot, packageDirs) {
110997
111654
  const overlayCommands = overlayCommandsOf(override);
110998
111655
  if (!overlayCommands)
110999
111656
  continue;
111000
- const packageCwd = join132(repoRoot, packageDir);
111657
+ const packageCwd = join133(repoRoot, packageDir);
111001
111658
  for (const gate of GATE_ORDER) {
111002
111659
  if (overlayCommands[gate] === undefined)
111003
111660
  continue;
111004
- add(`${gate}@${packageDir}`, overlayCommands[gate], packageCwd, setupFor(overlayCommands, gate));
111661
+ const setupCommands = Object.hasOwn(overlayCommands, "e2eSetup") ? overlayCommands : { ...overlayCommands, e2eSetup: rootCommands.e2eSetup };
111662
+ add(`${gate}@${packageDir}`, overlayCommands[gate], packageCwd, setupFor(setupCommands, gate));
111005
111663
  }
111006
111664
  }
111007
111665
  return out;
@@ -111048,7 +111706,7 @@ ${res.output}`);
111048
111706
  passed: false,
111049
111707
  ran,
111050
111708
  failing: [],
111051
- output: `[quality] no quality.commands configured in ${join132(repoRoot, PROJECT_NAX_DIR, "config.json")} \u2014 nothing was verified`
111709
+ output: `[quality] no quality.commands configured in ${join133(repoRoot, PROJECT_NAX_DIR, "config.json")} \u2014 nothing was verified`
111052
111710
  };
111053
111711
  }
111054
111712
  return { passed: failing.length === 0, ran, failing, output: chunks.join(`
@@ -111075,12 +111733,12 @@ var restorePending = (h) => !h.restored && h.settledBy === undefined, restorePen
111075
111733
 
111076
111734
  // src/finish/untracked-snapshot.ts
111077
111735
  import { chmod, copyFile, lstat as lstat5, mkdir as mkdir41, realpath as realpath5, rm as rm12, unlink as unlink8 } from "fs/promises";
111078
- import { dirname as dirname32, join as join133, relative as relative40, resolve as resolve45, sep as sep25 } from "path";
111736
+ import { dirname as dirname33, join as join134, relative as relative41, resolve as resolve46, sep as sep25 } from "path";
111079
111737
  async function copyFileInto(src, dest, ownerOnly = false) {
111080
111738
  try {
111081
111739
  if (!(await lstat5(src)).isFile())
111082
111740
  return false;
111083
- await mkdir41(dirname32(dest), ownerOnly ? { recursive: true, mode: PRIVATE_DIR } : { recursive: true });
111741
+ await mkdir41(dirname33(dest), ownerOnly ? { recursive: true, mode: PRIVATE_DIR } : { recursive: true });
111084
111742
  await copyFile(src, dest);
111085
111743
  if (ownerOnly)
111086
111744
  await chmod(dest, PRIVATE_FILE);
@@ -111091,17 +111749,17 @@ async function copyFileInto(src, dest, ownerOnly = false) {
111091
111749
  }
111092
111750
  async function privateDirs(top, dir) {
111093
111751
  await mkdir41(dir, { recursive: true, mode: PRIVATE_DIR });
111094
- for (let d = dir;; d = dirname32(d)) {
111752
+ for (let d = dir;; d = dirname33(d)) {
111095
111753
  await chmod(d, PRIVATE_DIR);
111096
- if (d === top || dirname32(d) === d)
111754
+ if (d === top || dirname33(d) === d)
111097
111755
  return;
111098
111756
  }
111099
111757
  }
111100
111758
  async function removeSnapshotDir(auditDir, snapshot) {
111101
111759
  if (!snapshot)
111102
111760
  return;
111103
- const rel = relative40(resolve45(auditDir), resolve45(snapshot.dir));
111104
- if (rel === "" || rel.startsWith("..") || resolve45(rel) === rel)
111761
+ const rel = relative41(resolve46(auditDir), resolve46(snapshot.dir));
111762
+ if (rel === "" || rel.startsWith("..") || resolve46(rel) === rel)
111105
111763
  return;
111106
111764
  await rm12(snapshot.dir, { recursive: true, force: true }).catch(() => {
111107
111765
  return;
@@ -111120,16 +111778,16 @@ async function snapshotUntracked(workdir, paths, dir) {
111120
111778
  await rm12(dir, { recursive: true, force: true }).catch(() => {
111121
111779
  return;
111122
111780
  });
111123
- await privateDirs(dirname32(dir), dir);
111781
+ await privateDirs(dirname33(dir), dir);
111124
111782
  const { fileMax, totalMax } = _setAsideDeps.limits;
111125
111783
  const files = [];
111126
111784
  let total = 0;
111127
111785
  for (const path of paths) {
111128
- const st = await lstat5(join133(root, path)).catch(() => null);
111786
+ const st = await lstat5(join134(root, path)).catch(() => null);
111129
111787
  if (!st?.isFile())
111130
111788
  continue;
111131
111789
  const fits = st.size <= fileMax && total + st.size <= totalMax;
111132
- const saved = fits && await copyFileInto(join133(root, path), join133(dir, path), true);
111790
+ const saved = fits && await copyFileInto(join134(root, path), join134(dir, path), true);
111133
111791
  if (saved)
111134
111792
  total += st.size;
111135
111793
  files.push({ path, size: st.size, mtimeMs: st.mtimeMs, mode: st.mode & 511, saved });
@@ -111158,7 +111816,7 @@ async function restoreUntracked(workdir, snapshot, handoffPath) {
111158
111816
  const out = { restored: [], movedAside: [], failed: [] };
111159
111817
  const root = await repoRoot(workdir);
111160
111818
  for (const e of snapshot?.files ?? []) {
111161
- const file = join133(root, e.path);
111819
+ const file = join134(root, e.path);
111162
111820
  if (!await insideRoot(root, file)) {
111163
111821
  out.failed.push(e.path);
111164
111822
  continue;
@@ -111166,10 +111824,10 @@ async function restoreUntracked(workdir, snapshot, handoffPath) {
111166
111824
  const st = await lstat5(file).catch(() => null);
111167
111825
  if (unchanged(st, e))
111168
111826
  continue;
111169
- const kept = !st?.isFile() || await copyFileInto(file, join133(handoffPath, "untracked-modified", e.path), true);
111827
+ const kept = !st?.isFile() || await copyFileInto(file, join134(handoffPath, "untracked-modified", e.path), true);
111170
111828
  if (!kept) {
111171
111829
  out.failed.push(e.path);
111172
- } else if (e.saved && snapshot && await putBack(root, join133(snapshot.dir, e.path), file, e.mode)) {
111830
+ } else if (e.saved && snapshot && await putBack(root, join134(snapshot.dir, e.path), file, e.mode)) {
111173
111831
  out.restored.push(e.path);
111174
111832
  } else if (!e.saved && st) {
111175
111833
  const removed = await _setAsideDeps.remove(file).then(() => true, () => false);
@@ -111180,7 +111838,7 @@ async function restoreUntracked(workdir, snapshot, handoffPath) {
111180
111838
  }
111181
111839
  return out;
111182
111840
  }
111183
- var UNTRACKED_FILE_MAX = 1048576, UNTRACKED_TOTAL_MAX = 20971520, _setAsideDeps, PRIVATE_DIR = 448, PRIVATE_FILE = 384, realOrResolved = (path) => realpath5(path).catch(() => resolve45(path));
111841
+ var UNTRACKED_FILE_MAX = 1048576, UNTRACKED_TOTAL_MAX = 20971520, _setAsideDeps, PRIVATE_DIR = 448, PRIVATE_FILE = 384, realOrResolved = (path) => realpath5(path).catch(() => resolve46(path));
111184
111842
  var init_untracked_snapshot = __esm(() => {
111185
111843
  init_errors4();
111186
111844
  init_safe_path();
@@ -111193,9 +111851,9 @@ var init_untracked_snapshot = __esm(() => {
111193
111851
 
111194
111852
  // src/finish/set-aside.ts
111195
111853
  import { lstat as lstat6, mkdir as mkdir42, readFile as readFile11, writeFile as writeFile14 } from "fs/promises";
111196
- import { join as join134 } from "path";
111854
+ import { join as join135 } from "path";
111197
111855
  function handoffDir(audit, n) {
111198
- return join134(audit.auditDir, `${audit.runId}.handoff-${n}`);
111856
+ return join135(audit.auditDir, `${audit.runId}.handoff-${n}`);
111199
111857
  }
111200
111858
  function warn2(message, err) {
111201
111859
  getSafeLogger2()?.warn("finish", message, { storyId: "_run", error: errorMessage(err) });
@@ -111233,7 +111891,7 @@ async function dropIntent(root, created) {
111233
111891
  async function savePatch(root, dir, baseSha, created) {
111234
111892
  await addIntent(root, created);
111235
111893
  try {
111236
- const diff = await git3(["diff", "--binary", "--no-renames", `--output=${join134(dir, PATCH)}`, baseSha], root);
111894
+ const diff = await git3(["diff", "--binary", "--no-renames", `--output=${join135(dir, PATCH)}`, baseSha], root);
111237
111895
  return diff.exitCode === 0;
111238
111896
  } finally {
111239
111897
  await dropIntent(root, created).catch(() => {
@@ -111250,7 +111908,7 @@ async function restoreKnown(root, baseSha, inBase) {
111250
111908
  return ok;
111251
111909
  }
111252
111910
  async function removeEntry(root, rel) {
111253
- const file = join134(root, rel);
111911
+ const file = join135(root, rel);
111254
111912
  if (!await insideRoot(root, file))
111255
111913
  return false;
111256
111914
  if (!await lstat6(file).catch(() => null))
@@ -111273,7 +111931,7 @@ async function restorePaths(root, base, tracked, created) {
111273
111931
  return ok;
111274
111932
  }
111275
111933
  async function writeMeta(record) {
111276
- await writeFile14(join134(record.dir, META), `${JSON.stringify(record, null, 2)}
111934
+ await writeFile14(join135(record.dir, META), `${JSON.stringify(record, null, 2)}
111277
111935
  `, "utf8");
111278
111936
  }
111279
111937
  function newRecord(args, dir, created, changedPaths) {
@@ -111302,7 +111960,7 @@ async function saveWork(args, root, record) {
111302
111960
  try {
111303
111961
  await mkdir42(record.dir, { recursive: true });
111304
111962
  if (args.note) {
111305
- await writeFile14(join134(record.dir, NOTE), `${JSON.stringify(args.note, null, 2)}
111963
+ await writeFile14(join135(record.dir, NOTE), `${JSON.stringify(args.note, null, 2)}
111306
111964
  `, "utf8");
111307
111965
  record.hasNote = true;
111308
111966
  }
@@ -111356,7 +112014,7 @@ async function setAside(input) {
111356
112014
  async function readHandoffNote(record) {
111357
112015
  if (!record.hasNote)
111358
112016
  return;
111359
- const text = await readFile11(join134(record.dir, NOTE), "utf8").catch(() => null);
112017
+ const text = await readFile11(join135(record.dir, NOTE), "utf8").catch(() => null);
111360
112018
  const parsed = text ? tryParseLLMJson(text) : null;
111361
112019
  return isHandoffNote(parsed) ? parsed : undefined;
111362
112020
  }
@@ -111364,7 +112022,7 @@ async function reapplyHandoff(workdir, record) {
111364
112022
  if (!record.patchSaved)
111365
112023
  return { applied: false, files: [] };
111366
112024
  const root = await repoRoot(workdir);
111367
- const patch = join134(record.dir, PATCH);
112025
+ const patch = join135(record.dir, PATCH);
111368
112026
  const check = await git3(["apply", "--check", "--whitespace=nowarn", patch], root);
111369
112027
  if (check.exitCode !== 0)
111370
112028
  return { applied: false, files: [] };
@@ -111461,7 +112119,7 @@ var init_finalize_dirt = __esm(() => {
111461
112119
  });
111462
112120
 
111463
112121
  // src/finish/fix-step.ts
111464
- import { join as join135 } from "path";
112122
+ import { join as join136 } from "path";
111465
112123
  function findingIdsOf(findings) {
111466
112124
  return (findings ?? []).map(findingId);
111467
112125
  }
@@ -111532,7 +112190,7 @@ async function captureBase(state, deps) {
111532
112190
  if (!captured) {
111533
112191
  return { error: new NaxError("Cannot read the worktree base", "FINISH_FIX_BASE", { stage: "finish" }) };
111534
112192
  }
111535
- const snapshotDir = join135(deps.audit.auditDir, `${deps.audit.runId}.fix-base-${state.handoffs.length + 1}`);
112193
+ const snapshotDir = join136(deps.audit.auditDir, `${deps.audit.runId}.fix-base-${state.handoffs.length + 1}`);
111536
112194
  return {
111537
112195
  ...captured,
111538
112196
  untrackedSnapshot: await snapshotUntracked(state.workdir, captured.untrackedBefore, snapshotDir)
@@ -112308,7 +112966,7 @@ var init_gates_loop = __esm(() => {
112308
112966
  });
112309
112967
 
112310
112968
  // src/finish/review-inputs.ts
112311
- import { join as join136, relative as relative41 } from "path";
112969
+ import { join as join137, relative as relative42 } from "path";
112312
112970
  function isRecord12(value) {
112313
112971
  return typeof value === "object" && value !== null;
112314
112972
  }
@@ -112327,13 +112985,13 @@ function storiesFromText(text) {
112327
112985
  return rows.flatMap((row) => isRecord12(row) && typeof row.id === "string" && typeof row.title === "string" ? [{ id: row.id, title: row.title }] : []);
112328
112986
  }
112329
112987
  async function loadReviewInputs(state, outputDir) {
112330
- const prdAbs = join136(featureDir(state.workdir, state.feature), "prd.json");
112988
+ const prdAbs = join137(featureDir(state.workdir, state.feature), "prd.json");
112331
112989
  const [changed, hints, prdText] = await Promise.all([
112332
112990
  changedFiles(state.workdir, state.base),
112333
112991
  outputDir ? readA3Hints(outputDir, state.feature) : Promise.resolve([]),
112334
112992
  _reviewInputsDeps.readText(prdAbs)
112335
112993
  ]);
112336
- return { changed, hints, stories: storiesFromText(prdText), prdPath: relative41(state.workdir, prdAbs) };
112994
+ return { changed, hints, stories: storiesFromText(prdText), prdPath: relative42(state.workdir, prdAbs) };
112337
112995
  }
112338
112996
  var _reviewInputsDeps;
112339
112997
  var init_review_inputs = __esm(() => {
@@ -112455,7 +113113,7 @@ async function decide3(state, deps, rdeps) {
112455
113113
  const advise = approvalAdvisor(deps.advise, asked);
112456
113114
  const run = async () => {
112457
113115
  const facts = currentFacts(state, rdeps.inputs.changed);
112458
- return { facts, res: await resolve44({ facts, advise, reReviewUsed: state.review.reReviewUsed }) };
113116
+ return { facts, res: await resolve45({ facts, advise, reReviewUsed: state.review.reReviewUsed }) };
112459
113117
  };
112460
113118
  let out = await run();
112461
113119
  if (out.res.outcome === "re-review") {
@@ -113233,7 +113891,7 @@ var init_narrative = __esm(() => {
113233
113891
 
113234
113892
  // src/finish/pr/context.ts
113235
113893
  import { readFile as readFile12 } from "fs/promises";
113236
- import { join as join137 } from "path";
113894
+ import { join as join138 } from "path";
113237
113895
  async function readJson(path) {
113238
113896
  let text;
113239
113897
  try {
@@ -113320,8 +113978,8 @@ function fromResolution(state) {
113320
113978
  async function loadFinishPrContext(args) {
113321
113979
  const featureDirPath = featureDir(args.state.workdir, args.state.feature);
113322
113980
  const [prd, status, rounds, stat, template, specSummary, advisorDecisions] = await Promise.all([
113323
- readJson(join137(featureDirPath, "prd.json")),
113324
- readJson(join137(featureDirPath, "status.json")),
113981
+ readJson(join138(featureDirPath, "prd.json")),
113982
+ readJson(join138(featureDirPath, "status.json")),
113325
113983
  readRounds(args.audit),
113326
113984
  runDiffstat(args.state.workdir, args.state.base),
113327
113985
  loadTemplate(args.state.workdir, args.forge),
@@ -114315,6 +114973,12 @@ function classifyGh(r) {
114315
114973
  return { state: "failed", summary };
114316
114974
  return checks.every((c) => GH_OK.has(c.bucket)) ? { state: "green", summary } : { state: "waiting", summary };
114317
114975
  }
114976
+ function glabPipelineSha(mr) {
114977
+ const head = mr.head_pipeline;
114978
+ const fallback = mr.pipeline;
114979
+ const sha = (isRecord13(head) ? head.sha : undefined) ?? (isRecord13(fallback) ? fallback.sha : undefined);
114980
+ return typeof sha === "string" ? sha : undefined;
114981
+ }
114318
114982
  function classifyGlab(r) {
114319
114983
  if (r.exitCode !== 0)
114320
114984
  return cliError(r);
@@ -114327,9 +114991,13 @@ function classifyGlab(r) {
114327
114991
  if (status === undefined)
114328
114992
  return glabWaiting(parsed.value);
114329
114993
  const summary = `CI pipeline: ${status}`;
114330
- if (status === "success") {
114994
+ if (status === "manual" || status === "scheduled") {
114995
+ return { state: "unavailable", detail: `pipeline is waiting on a ${status} job` };
114996
+ }
114997
+ if (status === "success" || status === "skipped") {
114331
114998
  const waiting = glabWaiting(parsed.value, summary);
114332
- return waiting.state === "conflicting" ? waiting : { state: "green", summary };
114999
+ const sha = glabPipelineSha(parsed.value);
115000
+ return waiting.state === "conflicting" ? waiting : { state: "green", summary, sha };
114333
115001
  }
114334
115002
  return GL_FAILED.has(status) ? { state: "failed", summary } : glabWaiting(parsed.value, summary);
114335
115003
  }
@@ -114344,7 +115012,7 @@ async function githubWaiting(poll, prRef, cwd) {
114344
115012
  if (poll.state !== "waiting" && poll.state !== "green")
114345
115013
  return poll;
114346
115014
  try {
114347
- const r = await _ciWaitDeps.run(["gh", "pr", "view", prRef, "--json", "mergeable,mergeStateStatus,baseRefName"], {
115015
+ const r = await _ciWaitDeps.run(["gh", "pr", "view", prRef, "--json", "mergeable,mergeStateStatus,baseRefName,headRefOid"], {
114348
115016
  cwd
114349
115017
  });
114350
115018
  if (r.exitCode !== 0)
@@ -114353,7 +115021,9 @@ async function githubWaiting(poll, prRef, cwd) {
114353
115021
  if (!parsed.ok || !isRecord13(parsed.value))
114354
115022
  return poll;
114355
115023
  const mr = parsed.value;
114356
- return mr.mergeable === "CONFLICTING" || mr.mergeStateStatus === "DIRTY" ? conflictPoll(mr.baseRefName) : poll;
115024
+ if (mr.mergeable === "CONFLICTING" || mr.mergeStateStatus === "DIRTY")
115025
+ return conflictPoll(mr.baseRefName);
115026
+ return typeof mr.headRefOid === "string" ? { ...poll, sha: mr.headRefOid } : poll;
114357
115027
  } catch {
114358
115028
  return poll;
114359
115029
  }
@@ -114410,7 +115080,18 @@ async function waitWithHeartbeat(ms, startedAt, progress) {
114410
115080
  progress.heartbeatIfDue(startedAt);
114411
115081
  }
114412
115082
  }
115083
+ function bindToHead(poll, expectedSha) {
115084
+ if (poll.state !== "green" || !expectedSha || !poll.sha || poll.sha === expectedSha)
115085
+ return poll;
115086
+ return {
115087
+ ...poll,
115088
+ state: "waiting",
115089
+ mismatch: `checks report ${poll.sha.slice(0, 8)} but HEAD is ${expectedSha.slice(0, 8)}`
115090
+ };
115091
+ }
114413
115092
  function terminalPoll(poll) {
115093
+ if (poll.state === "unavailable")
115094
+ return { outcome: "unavailable", detail: poll.detail };
114414
115095
  if (poll.state === "green")
114415
115096
  return { outcome: "green", detail: "all checks passed" };
114416
115097
  if (poll.state === "failed")
@@ -114482,7 +115163,7 @@ async function waitForChecks(forge, prRef, opts) {
114482
115163
  for (;; ) {
114483
115164
  if (opts.signal?.aborted)
114484
115165
  return end("pending", "aborted");
114485
- const poll = await pollOnce(forge, prRef, opts.cwd);
115166
+ const poll = bindToHead(await pollOnce(forge, prRef, opts.cwd), opts.expectedSha);
114486
115167
  history.note(poll);
114487
115168
  if ("summary" in poll)
114488
115169
  progress.note(poll.summary);
@@ -114490,6 +115171,8 @@ async function waitForChecks(forge, prRef, opts) {
114490
115171
  if (terminal)
114491
115172
  return end(terminal.outcome, terminal.detail);
114492
115173
  if (_ciWaitDeps.now() + opts.pollMs > deadline) {
115174
+ if ("mismatch" in poll && poll.mismatch)
115175
+ return end("unavailable", poll.mismatch);
114493
115176
  const timeout = errors.timeout() ?? history.timeout(opts.timeoutMs);
114494
115177
  return end(timeout.outcome, timeout.detail);
114495
115178
  }
@@ -114717,9 +115400,9 @@ var init_pr_body = __esm(() => {
114717
115400
  });
114718
115401
 
114719
115402
  // src/workflow/finalize.ts
114720
- import { isAbsolute as isAbsolute31, join as join138 } from "path";
114721
- function isPublishableBranch(branch) {
114722
- return !PROTECTED_BRANCHES.has(branch);
115403
+ import { isAbsolute as isAbsolute32, join as join139 } from "path";
115404
+ function isPublishableBranch(branch, defaultBranch) {
115405
+ return !PROTECTED_BRANCHES.has(branch) && branch !== defaultBranch;
114723
115406
  }
114724
115407
  function workflowPrTitle(fc) {
114725
115408
  const raw = `feat(${fc.feature}): ${fc.prd.userStories.length} stories (nax workflow)`;
@@ -114728,7 +115411,8 @@ function workflowPrTitle(fc) {
114728
115411
  async function publishTarget(fc) {
114729
115412
  if (fc.dryRun)
114730
115413
  return { reason: "dry run" };
114731
- if (!isPublishableBranch(fc.branch))
115414
+ const defaultBranch = await readDefaultBranch(fc.projectDir);
115415
+ if (!isPublishableBranch(fc.branch, defaultBranch))
114732
115416
  return { reason: `branch "${fc.branch || "(empty)"}" is not a feature branch` };
114733
115417
  const forge = await _featureDeps.detectForge(fc.projectDir);
114734
115418
  return forge ? { forge } : { reason: "no forge detected (origin is not GitHub or GitLab)" };
@@ -114766,7 +115450,7 @@ async function loadAssumptions(stories) {
114766
115450
  if (!s.runDir)
114767
115451
  continue;
114768
115452
  try {
114769
- const file = Bun.file(join138(workflowStoryDir(s.runDir, s.id), "report.md"));
115453
+ const file = Bun.file(join139(workflowStoryDir(s.runDir, s.id), "report.md"));
114770
115454
  const text = await file.exists() ? extractAssumptions(await file.text()) : "";
114771
115455
  if (text)
114772
115456
  out.push({ storyId: s.id, text });
@@ -114779,7 +115463,7 @@ async function specClosingIssues(fc) {
114779
115463
  const rel = (await _workflowDeps.resolveFeatureSpec(fc.feature, fc.projectDir)).specSource?.path;
114780
115464
  if (!rel)
114781
115465
  return;
114782
- const abs = isAbsolute31(rel) ? rel : join138(fc.projectDir, rel);
115466
+ const abs = isAbsolute32(rel) ? rel : join139(fc.projectDir, rel);
114783
115467
  const closes = parseClosingIssues(await Bun.file(abs).text());
114784
115468
  return closes.length > 0 ? closes : undefined;
114785
115469
  } catch {
@@ -114905,9 +115589,19 @@ async function pushLedger(fc) {
114905
115589
  });
114906
115590
  }
114907
115591
  }
114908
- function waitForCi(fc, forge) {
115592
+ async function waitForCi(fc, forge) {
115593
+ let expectedSha;
115594
+ try {
115595
+ expectedSha = await readHead(fc.projectDir) || undefined;
115596
+ } catch (err) {
115597
+ getSafeLogger2()?.warn("workflow", "Could not read pushed HEAD \u2014 CI SHA comparison unavailable", {
115598
+ storyId: "_run",
115599
+ error: errorMessage(err)
115600
+ });
115601
+ }
114909
115602
  return _featureDeps.waitForChecks(forge, fc.branch, {
114910
115603
  cwd: fc.projectDir,
115604
+ ...expectedSha ? { expectedSha } : {},
114911
115605
  timeoutMs: fc.workflow.timeouts.ciWaitMs,
114912
115606
  pollMs: fc.workflow.timeouts.ciPollMs,
114913
115607
  ...fc.runSignal ? { signal: fc.runSignal } : {}
@@ -115229,6 +115923,7 @@ function featureModelFor(fc) {
115229
115923
  timeoutMs: NO_TIMEOUT,
115230
115924
  minorIds: [],
115231
115925
  requiredFiles: [],
115926
+ repoRoot: fc.projectDir,
115232
115927
  range: ""
115233
115928
  });
115234
115929
  }
@@ -115299,7 +115994,7 @@ var init_tokens = __esm(() => {
115299
115994
 
115300
115995
  // src/workflow/tool-error-outcome.ts
115301
115996
  import { readdir as readdir11, readFile as readFile13 } from "fs/promises";
115302
- import { join as join139 } from "path";
115997
+ import { join as join140 } from "path";
115303
115998
  function targetOf(row) {
115304
115999
  for (const field of TARGET_FIELDS) {
115305
116000
  const value = row.input?.[field];
@@ -115356,7 +116051,7 @@ async function readToolEntriesSince(scope, sinceMs) {
115356
116051
  const entries = [];
115357
116052
  for (const { name, stamp } of stamped) {
115358
116053
  try {
115359
- const body = JSON.parse(await readFile13(join139(dir, name), "utf8"));
116054
+ const body = JSON.parse(await readFile13(join140(dir, name), "utf8"));
115360
116055
  for (const [index, row] of (body.calls ?? []).entries()) {
115361
116056
  entries.push({ key: `${name}#${index}`, row, fileStamp: stamp });
115362
116057
  }
@@ -115941,225 +116636,6 @@ var init_feature_machine = __esm(() => {
115941
116636
  };
115942
116637
  });
115943
116638
 
115944
- // src/workflow/preflight.ts
115945
- import { rename as rename12 } from "fs/promises";
115946
- import { join as join140 } from "path";
115947
- function preflightPath(runDir) {
115948
- return join140(runDir, "preflight.json");
115949
- }
115950
- function what(d) {
115951
- return d.action.type === "supersede" ? `supersede ${describeTarget(d.action.target)}` : d.action.type;
115952
- }
115953
- function isRecordObject(v) {
115954
- return typeof v === "object" && v !== null && !Array.isArray(v);
115955
- }
115956
- function isStringArray2(v) {
115957
- return Array.isArray(v) && v.every((s) => typeof s === "string");
115958
- }
115959
- function isOverlapEntry(v) {
115960
- return isRecordObject(v) && typeof v.storyId === "string" && isStringArray2(v.specSections);
115961
- }
115962
- function isConflictRecord(v) {
115963
- return isRecordObject(v) && typeof v.questionId === "string" && isStringArray2(v.storyIds) && typeof v.summary === "string" && (v.decisionId === null || typeof v.decisionId === "string") && typeof v.action === "string";
115964
- }
115965
- function isReusable(v) {
115966
- if (!isRecordObject(v) || v.schemaVersion !== 1 || v.fallbackReason)
115967
- return false;
115968
- return Array.isArray(v.overlap) && v.overlap.every(isOverlapEntry) && Array.isArray(v.conflicts) && v.conflicts.every(isConflictRecord);
115969
- }
115970
- async function readExisting(path) {
115971
- const file = Bun.file(path);
115972
- if (!await file.exists())
115973
- return null;
115974
- try {
115975
- const parsed = JSON.parse(await file.text());
115976
- if (isReusable(parsed))
115977
- return parsed;
115978
- getLogger2().warn("workflow", "preflight.json is unusable (wrong shape or failed pre-flight) - re-running", {
115979
- storyId: "_run",
115980
- reason: "wrong-shape-or-fallback"
115981
- });
115982
- } catch (err) {
115983
- getLogger2().warn("workflow", "preflight.json is unreadable - re-running pre-flight", {
115984
- storyId: "_run",
115985
- reason: err instanceof Error ? err.message : String(err)
115986
- });
115987
- }
115988
- return null;
115989
- }
115990
- async function runPreflight(input) {
115991
- const path = preflightPath(input.runDir);
115992
- const existing = await readExisting(path);
115993
- if (existing)
115994
- return existing;
115995
- const result = await input.advisor.adviseBatch({
115996
- kind: "preflight",
115997
- summary: `Pre-flight for ${input.storyIds.length} stories.`,
115998
- storyIds: [...input.storyIds]
115999
- });
116000
- const items = result.kind === "preflight" ? result.items : [];
116001
- const conflicts = items.map((item) => ({
116002
- questionId: item.question.id,
116003
- storyIds: item.decision?.appliesTo ?? (item.question.storyId ? [item.question.storyId] : []),
116004
- summary: item.question.summary,
116005
- decisionId: item.decision?.id ?? null,
116006
- action: item.decision?.action.type ?? "hold",
116007
- ...item.fallbackReason ? { fallbackReason: item.fallbackReason } : {}
116008
- }));
116009
- const file = {
116010
- schemaVersion: 1,
116011
- overlap: result.kind === "preflight" ? result.overlap : [],
116012
- conflicts,
116013
- ...result.kind === "preflight" && result.fallbackReason ? { fallbackReason: result.fallbackReason } : {}
116014
- };
116015
- const tmp = `${path}.tmp`;
116016
- await Bun.write(tmp, `${JSON.stringify(file, null, 2)}
116017
- `);
116018
- await rename12(tmp, path);
116019
- for (const [i, item] of items.entries()) {
116020
- const record = conflicts[i];
116021
- if (!record)
116022
- continue;
116023
- const text = item.decision ? ledgerText.ruling({
116024
- action: "hold",
116025
- text: `pre-flight ${item.decision.id}: ${what(item.decision)}`,
116026
- why: item.decision.rationale,
116027
- costIfWrong: costIfWrongOf(item.decision),
116028
- source: "advisor"
116029
- }) : ledgerText.ruling({
116030
- action: "hold",
116031
- text: "pre-flight hold (default)",
116032
- why: item.fallbackReason ?? "no advisor ruling",
116033
- costIfWrong: "the story runs under the spec as written",
116034
- source: "default"
116035
- });
116036
- for (const storyId of record.storyIds)
116037
- await input.ledger(storyId, text);
116038
- }
116039
- return file;
116040
- }
116041
- var isReusablePreflight;
116042
- var init_preflight = __esm(() => {
116043
- init_advisor();
116044
- init_logger2();
116045
- init_advisor_live();
116046
- init_ledger2();
116047
- isReusablePreflight = isReusable;
116048
- });
116049
-
116050
- // src/workflow/feature-record.ts
116051
- import { basename as basename22 } from "path";
116052
- function newFeatureRecord(input) {
116053
- return {
116054
- step: "repo-gates",
116055
- startedAt: input.now,
116056
- ...input.head ? { startHead: input.head } : {},
116057
- seed: input.seed,
116058
- gateFindings: [],
116059
- reviewRan: false,
116060
- reviewOpen: [],
116061
- minors: input.minors,
116062
- newMinors: [],
116063
- nextFindingId: 1,
116064
- fixRan: false,
116065
- rulings: [],
116066
- reasons: [],
116067
- timings: {},
116068
- tokensByRole: {}
116069
- };
116070
- }
116071
- function minorsFrom(deferred) {
116072
- return deferred.map((m, i) => ({ id: `M${i + 1}`, storyId: m.storyId, text: m.text, triage: "leave" }));
116073
- }
116074
- async function collectStoryRecords(featureDir) {
116075
- const found = new Map;
116076
- for (const runDir of await runDirsNewestFirst(featureDir)) {
116077
- const stories = (await readResult(runDir))?.stories ?? {};
116078
- for (const [id, record] of Object.entries(stories)) {
116079
- if (!found.has(id))
116080
- found.set(id, { record, runDir });
116081
- }
116082
- }
116083
- return Object.fromEntries(found);
116084
- }
116085
- function isFeatureRecord(v) {
116086
- if (typeof v !== "object" || v === null)
116087
- return false;
116088
- const o = v;
116089
- return FEATURE_STEPS.includes(o.step) && typeof o.nextFindingId === "number" && Array.isArray(o.gateFindings) && Array.isArray(o.reviewOpen) && Array.isArray(o.minors) && Array.isArray(o.rulings) && Array.isArray(o.reasons);
116090
- }
116091
- async function loadPriorFeature(featureDir, currentRunId) {
116092
- for (const runDir of await runDirsNewestFirst(featureDir)) {
116093
- if (basename22(runDir) === currentRunId)
116094
- continue;
116095
- const feature = (await readResult(runDir))?.featureWorkflow;
116096
- if (feature === undefined)
116097
- continue;
116098
- if (isFeatureRecord(feature))
116099
- return feature;
116100
- getSafeLogger2()?.warn("workflow", "Prior feature record is malformed \u2014 ignored", { storyId: "_run", runDir });
116101
- }
116102
- return null;
116103
- }
116104
- function fromRecord(id, title, located) {
116105
- const r = located.record;
116106
- if (!r.state)
116107
- return null;
116108
- return {
116109
- id,
116110
- title,
116111
- state: r.state,
116112
- ...r.reason ? { reason: r.reason } : {},
116113
- rounds: r.round,
116114
- unreviewed: r.unreviewed,
116115
- flagged: r.flagged ?? 0,
116116
- runDir: located.runDir
116117
- };
116118
- }
116119
- function summariseStories(prd, records) {
116120
- return prd.userStories.map((s) => {
116121
- const located = records[s.id];
116122
- const closed = located ? fromRecord(s.id, s.title, located) : null;
116123
- if (closed)
116124
- return closed;
116125
- const base = { id: s.id, title: s.title, unreviewed: false, flagged: 0 };
116126
- if (s.passes || s.status === "passed")
116127
- return { ...base, state: "complete", reason: PASSED_EARLIER, rounds: 0 };
116128
- return { ...base, state: "not-run", rounds: located?.record.round ?? 0 };
116129
- });
116130
- }
116131
- async function newestPreflight(featureDir) {
116132
- for (const runDir of await runDirsNewestFirst(featureDir)) {
116133
- const path = preflightPath(runDir);
116134
- const file = Bun.file(path);
116135
- if (!await file.exists())
116136
- continue;
116137
- try {
116138
- const parsed = JSON.parse(await file.text());
116139
- if (isReusablePreflight(parsed))
116140
- return { path, file: parsed };
116141
- } catch {}
116142
- }
116143
- return null;
116144
- }
116145
- var FEATURE_STEPS, PASSED_EARLIER = "passed earlier, no workflow record";
116146
- var init_feature_record = __esm(() => {
116147
- init_logger2();
116148
- init_preflight();
116149
- init_result();
116150
- FEATURE_STEPS = [
116151
- "repo-gates",
116152
- "branch-review",
116153
- "branch-fix",
116154
- "branch-rereview",
116155
- "adjudicate",
116156
- "final-gates",
116157
- "gate-repair",
116158
- "publish",
116159
- "done"
116160
- ];
116161
- });
116162
-
116163
116639
  // src/workflow/ledger-items.ts
116164
116640
  function parseLine(line) {
116165
116641
  const m = LINE.exec(line);
@@ -116295,6 +116771,8 @@ async function resumeOrRestart(fc, prior, head) {
116295
116771
  };
116296
116772
  }
116297
116773
  async function startRecord(fc, prior, head) {
116774
+ if (isFinishedAtHead(prior, head) && isTransientDraft(prior))
116775
+ return { ...prior, step: "publish" };
116298
116776
  return prior && prior.step !== "done" ? resumeOrRestart(fc, prior, head) : freshRecord(fc, head);
116299
116777
  }
116300
116778
  function logComplete(stories, rec) {
@@ -116312,6 +116790,9 @@ function logHandOff(stories, storiesDurationMs) {
116312
116790
  const took = storiesDurationMs === undefined ? "" : ` (${formatDuration(storiesDurationMs)})`;
116313
116791
  logWorkflow(FEATURE_SCOPE, `stories done ${ran}/${stories.length}${took} - feature workflow start`);
116314
116792
  }
116793
+ function isTransientDraft(prior) {
116794
+ return prior.outcome === "draft" && (prior.ci === "unavailable" || prior.reasons.some((reason) => reason.startsWith("PR update failed:")));
116795
+ }
116315
116796
  function isFinishedAtHead(prior, head) {
116316
116797
  const done = prior?.outcome === "ready" || prior?.outcome === "draft" || prior?.outcome === "pushed";
116317
116798
  return prior?.step === "done" && head !== undefined && head !== "" && prior.head === head && done;
@@ -116321,7 +116802,7 @@ async function runInContext(fc, input, meter) {
116321
116802
  const stories = summariseStories(fc.prd, await collectStoryRecords(fc.featureDir));
116322
116803
  const prior = await loadPriorFeature(fc.featureDir, fc.runId);
116323
116804
  const head = await currentHead(fc);
116324
- if (isFinishedAtHead(prior, head)) {
116805
+ if (isFinishedAtHead(prior, head) && !isTransientDraft(prior)) {
116325
116806
  const current = {
116326
116807
  ...prior,
116327
116808
  passed: featurePassed({ ...prior.finalGates ? { finalGates: prior.finalGates } : {}, stories })
@@ -116389,12 +116870,16 @@ async function storePreflightTokens(meter, where) {
116389
116870
  });
116390
116871
  }
116391
116872
  }
116392
- async function adoptPriorPreflight(featureDir, runDir) {
116873
+ async function adoptPriorPreflight(featureDir, runDir, storyIds) {
116393
116874
  if (await Bun.file(preflightPath(runDir)).exists())
116394
116875
  return false;
116395
116876
  const prior = await newestPreflight(featureDir);
116396
116877
  if (!prior)
116397
116878
  return false;
116879
+ const covered = new Set(prior.file.storyIds);
116880
+ const pending = new Set(storyIds);
116881
+ if (covered.size !== pending.size || [...pending].some((id) => !covered.has(id)))
116882
+ return false;
116398
116883
  await mkdir43(runDir, { recursive: true });
116399
116884
  await Bun.write(preflightPath(runDir), `${JSON.stringify(prior.file, null, 2)}
116400
116885
  `);
@@ -116415,7 +116900,7 @@ async function runWorkflowPreflight(input) {
116415
116900
  const { fc } = built;
116416
116901
  here = { runDir: fc.runDir, header: { runId: fc.runId, feature: fc.feature } };
116417
116902
  await ensureLedger(fc.runDir, fc.runId, fc.feature);
116418
- if (await adoptPriorPreflight(fc.featureDir, fc.runDir)) {
116903
+ if (await adoptPriorPreflight(fc.featureDir, fc.runDir, storyIds)) {
116419
116904
  getSafeLogger2()?.info("workflow", "Reusing the pre-flight of an earlier run", { storyId: "_run" });
116420
116905
  }
116421
116906
  const advisor = _workflowDeps.createAdvisor({
@@ -116854,6 +117339,7 @@ class LoopState {
116854
117339
  suiteThisIteration = false;
116855
117340
  costStop = false;
116856
117341
  constructor(resume) {
117342
+ this.fixBase = resume?.fixBase;
116857
117343
  this.round = resume?.round ?? 0;
116858
117344
  this.reviewed = resume?.reviewed ?? false;
116859
117345
  this.unreviewed = resume?.unreviewed ?? false;
@@ -117004,6 +117490,7 @@ async function runStoryLoop(env) {
117004
117490
  s.pendingSession = [];
117005
117491
  await env.sink.update({
117006
117492
  openFindings: open,
117493
+ fixBase: s.fixBase,
117007
117494
  reviewed: s.reviewed,
117008
117495
  unreviewed: s.unreviewed,
117009
117496
  nextFindingId: s.nextId,
@@ -117029,7 +117516,7 @@ async function runStoryLoop(env) {
117029
117516
  if (s.reviewOpen.length > 0 && s.fixBase === undefined)
117030
117517
  s.fixBase = await env.steps.head();
117031
117518
  await env.sink.ledger(ledgerText.fixRound(s.round, env.maxFixRounds, `${open.length} open: ${open.map((f) => f.id).join(", ")}${fresh ? ", fresh" : ""}`));
117032
- await env.sink.update({ phase: "fix", round: s.round });
117519
+ await env.sink.update({ phase: "fix", round: s.round, fixBase: s.fixBase });
117033
117520
  const turn = {
117034
117521
  kind: "fix",
117035
117522
  round: s.round,
@@ -120977,7 +121464,7 @@ var init_rtk = __esm(() => {
120977
121464
 
120978
121465
  // src/execution/status-file.ts
120979
121466
  import { rename as rename13, unlink as unlink9 } from "fs/promises";
120980
- import { resolve as resolve46 } from "path";
121467
+ import { resolve as resolve47 } from "path";
120981
121468
  function countProgress(prd) {
120982
121469
  const stories = prd.userStories;
120983
121470
  const passed = stories.filter((s) => s.status === "passed").length;
@@ -121037,7 +121524,7 @@ function buildStatusSnapshot(state) {
121037
121524
  return snapshot;
121038
121525
  }
121039
121526
  async function writeStatusFile(filePath, status) {
121040
- const resolvedPath = resolve46(filePath);
121527
+ const resolvedPath = resolve47(filePath);
121041
121528
  if (filePath.includes("../") || filePath.includes("..\\")) {
121042
121529
  throw new NaxError("Invalid status file path: path traversal detected", "STATUS_FILE_PATH_TRAVERSAL", {
121043
121530
  stage: "execution",
@@ -124710,7 +125197,7 @@ var init_language_commands = __esm(() => {
124710
125197
  });
124711
125198
 
124712
125199
  // src/review/scoped-lint.ts
124713
- import { join as join151, relative as relative43 } from "path";
125200
+ import { join as join151, relative as relative44 } from "path";
124714
125201
  function normalizePath3(path) {
124715
125202
  return path.replaceAll("\\", "/").replace(/^\.\//, "");
124716
125203
  }
@@ -124736,7 +125223,7 @@ async function listChangedFiles(workdir, baseRef) {
124736
125223
  function inferActivePackageDir(workdir, projectDir) {
124737
125224
  if (!projectDir)
124738
125225
  return;
124739
- const rel = normalizePath3(relative43(projectDir, workdir));
125226
+ const rel = normalizePath3(relative44(projectDir, workdir));
124740
125227
  if (!rel || rel === "." || rel.startsWith(".."))
124741
125228
  return;
124742
125229
  return rel;
@@ -125504,14 +125991,14 @@ var init_run_setup_init = __esm(() => {
125504
125991
 
125505
125992
  // src/commands/common.ts
125506
125993
  import { existsSync as existsSync29, readdirSync as readdirSync5, realpathSync as realpathSync6 } from "fs";
125507
- import { join as join152, resolve as resolve48 } from "path";
125994
+ import { join as join152, resolve as resolve49 } from "path";
125508
125995
  function resolveProject(options = {}) {
125509
125996
  const { dir, feature } = options;
125510
125997
  let projectRoot;
125511
125998
  let naxDir;
125512
125999
  let configPath;
125513
126000
  if (dir) {
125514
- projectRoot = realpathSync6(resolve48(dir));
126001
+ projectRoot = realpathSync6(resolve49(dir));
125515
126002
  naxDir = join152(projectRoot, ".nax");
125516
126003
  if (!existsSync29(naxDir)) {
125517
126004
  throw new NaxError(`Directory does not contain a nax project: ${projectRoot}
@@ -125589,7 +126076,7 @@ async function resolveProjectAsync(options = {}) {
125589
126076
  if (!dir) {
125590
126077
  return resolveProject(options);
125591
126078
  }
125592
- if (existsSync29(resolve48(dir))) {
126079
+ if (existsSync29(resolve49(dir))) {
125593
126080
  return resolveProject(options);
125594
126081
  }
125595
126082
  const isPlainName = !dir.includes("/") && !dir.includes("\\");
@@ -125605,17 +126092,17 @@ async function resolveProjectAsync(options = {}) {
125605
126092
  } catch {}
125606
126093
  }
125607
126094
  throw new NaxError(`No project found for name or path: "${dir}"
125608
- Checked filesystem path: ${resolve48(dir)}
126095
+ Checked filesystem path: ${resolve49(dir)}
125609
126096
  Checked identity registry: ${registryIdentityPath}
125610
- Tip: use an absolute or relative path, or run "nax init" in your project directory first.`, "PROJECT_NOT_FOUND", { dir, resolvedPath: resolve48(dir), registryIdentityPath });
126097
+ Tip: use an absolute or relative path, or run "nax init" in your project directory first.`, "PROJECT_NOT_FOUND", { dir, resolvedPath: resolve49(dir), registryIdentityPath });
125611
126098
  }
125612
126099
  try {
125613
126100
  return resolveProject(options);
125614
126101
  } catch (err) {
125615
126102
  if (err instanceof Error && "code" in err && err.code === "ENOENT") {
125616
- throw new NaxError(`Path does not exist: ${resolve48(dir)}`, "PROJECT_NOT_FOUND", {
126103
+ throw new NaxError(`Path does not exist: ${resolve49(dir)}`, "PROJECT_NOT_FOUND", {
125617
126104
  dir,
125618
- resolvedPath: resolve48(dir),
126105
+ resolvedPath: resolve49(dir),
125619
126106
  cause: err
125620
126107
  });
125621
126108
  }
@@ -125623,7 +126110,7 @@ Tip: use an absolute or relative path, or run "nax init" in your project directo
125623
126110
  }
125624
126111
  }
125625
126112
  function findProjectRoot(startDir) {
125626
- let current = resolve48(startDir);
126113
+ let current = resolve49(startDir);
125627
126114
  let depth = 0;
125628
126115
  while (depth < MAX_DIRECTORY_DEPTH) {
125629
126116
  const naxDir = join152(current, ".nax");
@@ -125650,7 +126137,7 @@ var init_common = __esm(() => {
125650
126137
  // src/commands/curator.ts
125651
126138
  import { readdirSync as readdirSync6 } from "fs";
125652
126139
  import { unlink as unlink11 } from "fs/promises";
125653
- import { join as join153, resolve as resolve49, sep as sep26 } from "path";
126140
+ import { join as join153, resolve as resolve50, sep as sep26 } from "path";
125654
126141
  function openInEditor(filePath) {
125655
126142
  const [editor, ...args] = parseCommandToArgv(process.env.EDITOR ?? process.env.VISUAL ?? "vi");
125656
126143
  if (!editor)
@@ -125794,15 +126281,15 @@ async function curatorStatus(options) {
125794
126281
  }
125795
126282
  }
125796
126283
  function resolveCanonicalTargetPath(projectDir, canonicalFile) {
125797
- const target = resolve49(projectDir, canonicalFile);
125798
- const root = resolve49(projectDir);
126284
+ const target = resolve50(projectDir, canonicalFile);
126285
+ const root = resolve50(projectDir);
125799
126286
  if (target !== root && !target.startsWith(root + sep26))
125800
126287
  return null;
125801
126288
  const relative = target.slice(root.length + 1).replaceAll(sep26, "/");
125802
126289
  return CURATOR_TARGET_SHAPES.some((shape) => shape.test(relative)) ? target : null;
125803
126290
  }
125804
126291
  function isWithinCanonicalRulesDir(projectDir, targetPath) {
125805
- const rulesRoot = resolve49(projectDir, CANONICAL_RULES_DIR);
126292
+ const rulesRoot = resolve50(projectDir, CANONICAL_RULES_DIR);
125806
126293
  return targetPath === rulesRoot || targetPath.startsWith(rulesRoot + sep26);
125807
126294
  }
125808
126295
  async function curatorCommit(options) {
@@ -127069,7 +127556,7 @@ function renderReport(timeline, options = {}) {
127069
127556
 
127070
127557
  // src/commands/replay.ts
127071
127558
  import { existsSync as existsSync34 } from "fs";
127072
- import { dirname as dirname36 } from "path";
127559
+ import { dirname as dirname37 } from "path";
127073
127560
  async function readJsonlLenient(path) {
127074
127561
  if (!existsSync34(path))
127075
127562
  return [];
@@ -127098,7 +127585,7 @@ async function readJsonOrUndefined(path) {
127098
127585
  }
127099
127586
  async function readMetricsFromProject(meta) {
127100
127587
  await Promise.resolve().then(() => init_tracker());
127101
- const outputDir = dirname36(dirname36(dirname36(meta.eventsDir)));
127588
+ const outputDir = dirname37(dirname37(dirname37(meta.eventsDir)));
127102
127589
  const all = await loadRunMetrics(outputDir);
127103
127590
  return all.find((m) => m.runId === meta.runId);
127104
127591
  }
@@ -129255,7 +129742,7 @@ var init_tdd_failure_category = __esm(() => {
129255
129742
  });
129256
129743
 
129257
129744
  // src/execution/uncategorised-advice.ts
129258
- import { join as join162, relative as relative44 } from "path";
129745
+ import { join as join162, relative as relative45 } from "path";
129259
129746
  function callContextFor(ctx) {
129260
129747
  return {
129261
129748
  runtime: ctx.runtime,
@@ -129306,7 +129793,7 @@ async function adviseUncategorised(frame, pauseReason) {
129306
129793
  outputDir: ctx.runtime.outputDir,
129307
129794
  feature,
129308
129795
  runId: ctx.runtime.runId,
129309
- specPath: relative44(repoRoot, join162(featureDir(repoRoot, feature), "spec.md")),
129796
+ specPath: relative45(repoRoot, join162(featureDir(repoRoot, feature), "spec.md")),
129310
129797
  workdir: ctx.workdir,
129311
129798
  ...headsUpChannel(ctx)
129312
129799
  });
@@ -133130,7 +133617,7 @@ var init_checks_native_credentials = __esm(() => {
133130
133617
 
133131
133618
  // src/precheck/checks-warnings.ts
133132
133619
  import { existsSync as existsSync39 } from "fs";
133133
- import { isAbsolute as isAbsolute34 } from "path";
133620
+ import { isAbsolute as isAbsolute35 } from "path";
133134
133621
  async function checkClaudeMdExists(workdir) {
133135
133622
  const claudeMdPath = `${workdir}/CLAUDE.md`;
133136
133623
  const passed = existsSync39(claudeMdPath);
@@ -133299,7 +133786,7 @@ async function checkPromptOverrideFiles(config, workdir) {
133299
133786
  }
133300
133787
  async function checkHomeEnvValid() {
133301
133788
  const home = process.env.HOME ?? "";
133302
- const passed = home !== "" && isAbsolute34(home);
133789
+ const passed = home !== "" && isAbsolute35(home);
133303
133790
  return {
133304
133791
  name: "home-env-valid",
133305
133792
  tier: "warning",
@@ -134257,12 +134744,12 @@ var init_finalize_routing = __esm(() => {
134257
134744
 
134258
134745
  // src/plan/strategies/persist-prd.ts
134259
134746
  import { existsSync as defaultExistsSync, renameSync as defaultRenameSync } from "fs";
134260
- import { dirname as dirname37, join as join169 } from "path";
134747
+ import { dirname as dirname38, join as join169 } from "path";
134261
134748
  function unverifiedStampedStoryIds(prd) {
134262
134749
  return prd.userStories.filter((story) => story.workdirSource !== undefined && (story.contextFiles?.length ?? 0) + (story.expectedFiles?.length ?? 0) + (story.modifiedFiles?.length ?? 0) > 0).map((story) => story.id);
134263
134750
  }
134264
134751
  function moveDraftAside(outputPath) {
134265
- const rejectedPath = join169(dirname37(outputPath), "prd.rejected.json");
134752
+ const rejectedPath = join169(dirname38(outputPath), "prd.rejected.json");
134266
134753
  if (!_persistPrdDeps.existsSync(outputPath)) {
134267
134754
  return { phrase: `no draft was on disk at ${outputPath}` };
134268
134755
  }
@@ -135528,13 +136015,13 @@ var init_rules_cli_deps = __esm(() => {
135528
136015
  });
135529
136016
 
135530
136017
  // src/cli/rules-migrate-plan.ts
135531
- import { resolve as resolve50, sep as sep27 } from "path";
136018
+ import { resolve as resolve51, sep as sep27 } from "path";
135532
136019
  async function planMigration(sources, options) {
135533
136020
  const writes = [];
135534
136021
  const skips = [];
135535
- const resolvedTargetDir = resolve50(options.targetDir);
136022
+ const resolvedTargetDir = resolve51(options.targetDir);
135536
136023
  for (const source of sources) {
135537
- const resolvedTargetPath = resolve50(source.targetPath);
136024
+ const resolvedTargetPath = resolve51(source.targetPath);
135538
136025
  if (!resolvedTargetPath.startsWith(`${resolvedTargetDir}${sep27}`) && resolvedTargetPath !== resolvedTargetDir) {
135539
136026
  throw new NaxError(`Migration target escapes ${options.targetDir}: ${source.targetFileName} -> ${source.targetPath}`, "RULES_MIGRATE_TARGET_ESCAPE", { stage: "rules-migrate-plan", targetDir: options.targetDir, entry: source.targetPath });
135540
136027
  }
@@ -135677,7 +136164,7 @@ var init_rules_migrate = __esm(() => {
135677
136164
  });
135678
136165
 
135679
136166
  // src/cli/rules.ts
135680
- import { join as join178, resolve as resolve51, sep as sep28 } from "path";
136167
+ import { join as join178, resolve as resolve52, sep as sep28 } from "path";
135681
136168
  async function rulesLintCommand2(options) {
135682
136169
  return rulesLintCommand(options, {
135683
136170
  globCanonicalRuleFiles: _rulesCLIDeps.globCanonicalRuleFiles,
@@ -135728,8 +136215,8 @@ async function exportRuleDirectory(input) {
135728
136215
  const drifted = [];
135729
136216
  for (const rule of rules) {
135730
136217
  const rel = rule.path ?? rule.fileName;
135731
- const target = resolve51(workdir, ruleDir, rel);
135732
- if (!target.startsWith(`${resolve51(workdir, ruleDir)}${sep28}`)) {
136218
+ const target = resolve52(workdir, ruleDir, rel);
136219
+ if (!target.startsWith(`${resolve52(workdir, ruleDir)}${sep28}`)) {
135733
136220
  throw new NaxError(`Rule path escapes ${ruleDir}: ${rel}`, "RULES_EXPORT_PATH_ESCAPE", {
135734
136221
  stage: "rules-export",
135735
136222
  rule: rel
@@ -135749,9 +136236,9 @@ async function exportRuleDirectory(input) {
135749
136236
  }
135750
136237
  await _rulesCLIDeps.writeFile(target, content);
135751
136238
  }
135752
- const expected = new Set(rules.map((r) => resolve51(workdir, ruleDir, r.path ?? r.fileName)));
136239
+ const expected = new Set(rules.map((r) => resolve52(workdir, ruleDir, r.path ?? r.fileName)));
135753
136240
  for (const existing of _rulesCLIDeps.globInDir(join178(workdir, ruleDir))) {
135754
- if (!expected.has(resolve51(existing))) {
136241
+ if (!expected.has(resolve52(existing))) {
135755
136242
  _rulesCLIDeps.getLogger().warn("rules-export", "Generated rules dir contains a file with no canonical source", {
135756
136243
  file: existing,
135757
136244
  hint: `Delete it, or add the rule to ${CANONICAL_RULES_DIR}/`
@@ -136656,7 +137143,7 @@ var init_status_features_details = __esm(() => {
136656
137143
 
136657
137144
  // src/cli/status-features.ts
136658
137145
  import { existsSync as existsSync46, readdirSync as readdirSync10 } from "fs";
136659
- import { basename as basename34, join as join185, resolve as resolve52 } from "path";
137146
+ import { basename as basename34, join as join185, resolve as resolve53 } from "path";
136660
137147
  async function loadStatusFile(featureDir) {
136661
137148
  const statusPath = join185(featureDir, "status.json");
136662
137149
  if (!existsSync46(statusPath)) {
@@ -136883,7 +137370,7 @@ async function displayFeatureDetails(featureName, featureDir) {
136883
137370
  }
136884
137371
  }
136885
137372
  function resolveRootFromDir(dir) {
136886
- const resolved = resolve52(dir);
137373
+ const resolved = resolve53(dir);
136887
137374
  const naxDir = _statusFeaturesDeps.findProjectDir(resolved);
136888
137375
  return naxDir ? join185(naxDir, "..") : resolved;
136889
137376
  }
@@ -137025,12 +137512,12 @@ var init_status_dispatch = __esm(() => {
137025
137512
 
137026
137513
  // src/cli/trust.ts
137027
137514
  import { homedir as osHomedir2 } from "os";
137028
- import { resolve as resolve53 } from "path";
137515
+ import { resolve as resolve54 } from "path";
137029
137516
  function unreadableStoreLine(reason) {
137030
137517
  return trustStoreUnreadableError(trustStorePath(), reason).message;
137031
137518
  }
137032
137519
  function resolveAgainstCwd(path, deps) {
137033
- return path === undefined ? deps.cwd() : resolve53(deps.cwd(), path);
137520
+ return path === undefined ? deps.cwd() : resolve54(deps.cwd(), path);
137034
137521
  }
137035
137522
  function foldersOf(read) {
137036
137523
  return read.state === "ok" ? read.file.folders : [];