@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.
- package/dist/nax.js +1052 -565
- 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
|
-
|
|
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
|
-
|
|
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 || !
|
|
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) =>
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
13823
|
-
|
|
13824
|
-
|
|
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
|
|
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
|
|
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
|
-
},
|
|
17719
|
+
}, (error) => {
|
|
17720
|
+
signal.removeEventListener("abort", onAbort);
|
|
17721
|
+
reject(error);
|
|
17722
|
+
});
|
|
17432
17723
|
});
|
|
17433
17724
|
}
|
|
17434
|
-
function realSleep(ms) {
|
|
17435
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
55739
|
-
|
|
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 (
|
|
55980
|
-
|
|
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 (
|
|
56337
|
+
if (pid === undefined) {
|
|
56338
|
+
await exited;
|
|
56339
|
+
return;
|
|
56340
|
+
}
|
|
56341
|
+
if (!gone)
|
|
55997
56342
|
child.stdin.end();
|
|
55998
|
-
|
|
55999
|
-
|
|
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("
|
|
72383
|
-
return "
|
|
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
|
-
|
|
77393
|
-
|
|
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
|
|
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 = {
|
|
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
|
|
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
|
-
|
|
78650
|
-
|
|
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
|
-
|
|
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
|
|
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 :
|
|
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
|
|
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 :
|
|
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
|
-
|
|
90530
|
-
|
|
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
|
|
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
|
|
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 =
|
|
91939
|
-
const fullPath =
|
|
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
|
|
92790
|
+
import { relative as relative30 } from "path";
|
|
92365
92791
|
function scopedOverrideKey(workdir, packageDir, acId) {
|
|
92366
|
-
return `${
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
|
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) =>
|
|
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:
|
|
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:
|
|
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 =
|
|
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 =
|
|
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:
|
|
92574
|
-
message: `resolved spec: ${
|
|
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
|
|
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 =
|
|
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
|
|
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) =>
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
108028
|
-
import { basename as basename18, join as
|
|
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(
|
|
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 =
|
|
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 =
|
|
108736
|
+
const runDir = join120(featureDir, name);
|
|
108113
108737
|
try {
|
|
108114
|
-
dated.push({ runDir, mtimeMs: (await _resultDeps.stat(
|
|
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:
|
|
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
|
|
108184
|
-
import { isAbsolute as
|
|
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 =
|
|
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
|
|
108220
|
-
return (sc.story.dependencies ?? []).map((id) =>
|
|
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
|
|
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
|
-
|
|
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
|
|
109004
|
+
import { join as join122 } from "path";
|
|
108268
109005
|
function workflowFeatureDir(projectDir, feature) {
|
|
108269
|
-
return
|
|
109006
|
+
return join122(featureDir(projectDir, feature), "workflow");
|
|
108270
109007
|
}
|
|
108271
109008
|
function workflowRunDir(projectDir, feature, runId) {
|
|
108272
|
-
return
|
|
109009
|
+
return join122(workflowFeatureDir(projectDir, feature), runId);
|
|
108273
109010
|
}
|
|
108274
109011
|
function workflowStoryDir(runDir, storyId) {
|
|
108275
|
-
return
|
|
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
|
|
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:
|
|
108324
|
-
reportPath:
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
109496
|
+
import { join as join125 } from "path";
|
|
108841
109497
|
async function writeFeatureArtifact(fc, name, value) {
|
|
108842
109498
|
try {
|
|
108843
|
-
await Bun.write(
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
109065
|
-
return up === target ? 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 =
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
110707
|
+
import { join as join130 } from "path";
|
|
110051
110708
|
function roundsPath(t) {
|
|
110052
|
-
return
|
|
110709
|
+
return join130(t.auditDir, `${t.runId}.jsonl`);
|
|
110053
110710
|
}
|
|
110054
110711
|
function resultPath(t) {
|
|
110055
|
-
return
|
|
110712
|
+
return join130(t.auditDir, `${t.runId}.result.json`);
|
|
110056
110713
|
}
|
|
110057
110714
|
function ledgerPath2(auditDir) {
|
|
110058
|
-
return
|
|
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
|
|
110217
|
-
import { join as
|
|
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
|
|
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 =
|
|
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) =>
|
|
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
|
|
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 =
|
|
111657
|
+
const packageCwd = join133(repoRoot, packageDir);
|
|
111001
111658
|
for (const gate of GATE_ORDER) {
|
|
111002
111659
|
if (overlayCommands[gate] === undefined)
|
|
111003
111660
|
continue;
|
|
111004
|
-
|
|
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 ${
|
|
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
|
|
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(
|
|
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 =
|
|
111752
|
+
for (let d = dir;; d = dirname33(d)) {
|
|
111095
111753
|
await chmod(d, PRIVATE_DIR);
|
|
111096
|
-
if (d === top ||
|
|
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 =
|
|
111104
|
-
if (rel === "" || rel.startsWith("..") ||
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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,
|
|
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,
|
|
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(() =>
|
|
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
|
|
111854
|
+
import { join as join135 } from "path";
|
|
111197
111855
|
function handoffDir(audit, n) {
|
|
111198
|
-
return
|
|
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=${
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
|
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(
|
|
113324
|
-
readJson(
|
|
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 === "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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: ${
|
|
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:
|
|
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: ${
|
|
126103
|
+
throw new NaxError(`Path does not exist: ${resolve49(dir)}`, "PROJECT_NOT_FOUND", {
|
|
125617
126104
|
dir,
|
|
125618
|
-
resolvedPath:
|
|
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 =
|
|
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
|
|
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 =
|
|
125798
|
-
const root =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
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 !== "" &&
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
136022
|
+
const resolvedTargetDir = resolve51(options.targetDir);
|
|
135536
136023
|
for (const source of sources) {
|
|
135537
|
-
const resolvedTargetPath =
|
|
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
|
|
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 =
|
|
135732
|
-
if (!target.startsWith(`${
|
|
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) =>
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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() :
|
|
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 : [];
|