@brainervirus/workit-mcp 8.24.0 → 8.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1625 -1491
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -17775,6 +17775,26 @@ function resolveRef(cwd, ref) {
17775
17775
  return null;
17776
17776
  return line(git(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]));
17777
17777
  }
17778
+ function originHeadRef(cwd) {
17779
+ return line(git(cwd, ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"]));
17780
+ }
17781
+ function ownChangeDrift(cwd, range) {
17782
+ const before = diffRaw(cwd, range.fromFork, range.from);
17783
+ const after = diffRaw(cwd, range.toFork, range.to);
17784
+ if (!before || !after)
17785
+ return null;
17786
+ const judged = new Map(before.map((entry) => [entry.path, entry]));
17787
+ const current = new Map(after.map((entry) => [entry.path, entry]));
17788
+ const same = (a, b) => a !== undefined && b !== undefined && a.oldMode === b.oldMode && a.oldBlob === b.oldBlob && a.newMode === b.newMode && a.newBlob === b.newBlob;
17789
+ return [...new Set([...judged.keys(), ...current.keys()])].filter((file) => !same(judged.get(file), current.get(file))).toSorted();
17790
+ }
17791
+ function sameOwnChange(cwd, range) {
17792
+ const fromFork = mergeBase(cwd, range.fromBase, range.from);
17793
+ const toFork = mergeBase(cwd, range.toBase, range.to);
17794
+ if (!fromFork || !toFork)
17795
+ return false;
17796
+ return ownChangeDrift(cwd, { from: range.from, to: range.to, fromFork, toFork })?.length === 0;
17797
+ }
17778
17798
  var LANE_DIFF = [
17779
17799
  "-c",
17780
17800
  "core.quotePath=false",
@@ -21401,9 +21421,9 @@ var workerBlocksTransition = (transition, state) => transition === "pause" ? fal
21401
21421
 
21402
21422
  // packages/workit-core/src/core/task-evaluation.ts
21403
21423
  import { createHash as createHash10 } from "node:crypto";
21404
- import * as fs14 from "node:fs";
21405
- import { spawnSync as spawnSync11 } from "node:child_process";
21406
- import path18 from "node:path";
21424
+ import * as fs13 from "node:fs";
21425
+ import { spawnSync as spawnSync10 } from "node:child_process";
21426
+ import path17 from "node:path";
21407
21427
 
21408
21428
  // packages/workit-core/src/check-config.ts
21409
21429
  import { spawnSync as spawnSync8 } from "node:child_process";
@@ -21624,68 +21644,20 @@ function checkCommand(args, version, onPath = workitAvailable()) {
21624
21644
 
21625
21645
  // packages/workit-core/src/core/policy/derive.ts
21626
21646
  import { existsSync as existsSync5 } from "node:fs";
21627
- import path17 from "node:path";
21647
+ import path16 from "node:path";
21628
21648
 
21629
21649
  // packages/workit-core/src/ledger.ts
21630
21650
  import { spawnSync as spawnSync9 } from "node:child_process";
21631
21651
  import { createHash as createHash9, randomBytes as randomBytes3 } from "node:crypto";
21632
21652
  import fs12 from "node:fs";
21653
+ import path15 from "node:path";
21654
+
21655
+ // packages/workit-core/src/core/policy/lane.ts
21656
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
21633
21657
  import path14 from "node:path";
21634
- var LEDGER_VERSION = 1;
21635
- var MAX_LINE_BYTES = 4096;
21636
- var PASSING = new Set(["verified", "tests-verified", "type-check-only"]);
21637
- var FAILING = new Set(["failed", "blocked"]);
21638
- var STRONG_RESULTS = new Set(["verified", "tests-verified"]);
21639
- var PR_ROW_TYPES = new Set([
21640
- "pr.created",
21641
- "pr.status",
21642
- "pr.merged",
21643
- "pr.linked"
21644
- ]);
21645
- var err = (code, error, unblock) => ({
21646
- ok: false,
21647
- code,
21648
- error,
21649
- ...unblock ? { unblock } : {}
21650
- });
21651
- var gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
21652
- const run = spawnSync9("git", args, {
21653
- cwd,
21654
- encoding: "utf8",
21655
- timeout: timeoutMs,
21656
- killSignal: "SIGKILL",
21657
- windowsHide: true,
21658
- maxBuffer: 256 * 1024 * 1024,
21659
- env: {
21660
- ...process.env,
21661
- GIT_TERMINAL_PROMPT: "0",
21662
- GIT_OPTIONAL_LOCKS: "0",
21663
- LC_ALL: "C",
21664
- LANGUAGE: "C"
21665
- }
21666
- });
21667
- return {
21668
- ok: run.status === 0,
21669
- stdout: run.stdout ?? "",
21670
- stderr: run.error ? String(run.error.message) : run.stderr ?? ""
21671
- };
21672
- };
21673
- var git4 = (cwd, args) => {
21674
- const run = gitRun2(cwd, args);
21675
- const value = run.ok ? run.stdout.trim() : "";
21676
- return value ? value : null;
21677
- };
21658
+
21659
+ // packages/workit-core/src/git/default-base.ts
21678
21660
  var safeRef = (value) => value.length > 0 && !value.startsWith("-");
21679
- function resolveCommit(cwd, ref) {
21680
- if (!safeRef(ref))
21681
- return null;
21682
- return git4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]);
21683
- }
21684
- function branchHead(cwd, branch) {
21685
- if (!safeRef(branch))
21686
- return null;
21687
- return resolveCommit(cwd, `refs/heads/${branch}`) ?? resolveCommit(cwd, `refs/remotes/origin/${branch}`);
21688
- }
21689
21661
  var trackTrunk = (cwd) => {
21690
21662
  try {
21691
21663
  const resolved = vcsConfig("resolve", cwd);
@@ -21701,11 +21673,11 @@ function defaultBase(cwd) {
21701
21673
  const trunk = trackTrunk(cwd);
21702
21674
  if (trunk) {
21703
21675
  for (const candidate of [`origin/${trunk}`, trunk])
21704
- if (resolveCommit(cwd, candidate))
21676
+ if (resolveRef(cwd, candidate))
21705
21677
  return candidate;
21706
21678
  }
21707
- const originHead = git4(cwd, ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"]);
21708
- if (originHead && resolveCommit(cwd, originHead))
21679
+ const originHead = originHeadRef(cwd);
21680
+ if (originHead && resolveRef(cwd, originHead))
21709
21681
  return originHead;
21710
21682
  for (const candidate of [
21711
21683
  "origin/main",
@@ -21715,1489 +21687,1611 @@ function defaultBase(cwd) {
21715
21687
  "origin/develop",
21716
21688
  "develop"
21717
21689
  ])
21718
- if (resolveCommit(cwd, candidate))
21690
+ if (resolveRef(cwd, candidate))
21719
21691
  return candidate;
21720
21692
  return null;
21721
21693
  }
21722
- function diffHash(cwd, base, head) {
21723
- if (!safeRef(base) || !safeRef(head))
21724
- return null;
21725
- const diff = gitRun2(cwd, [
21726
- "diff",
21727
- "-U3",
21728
- "--diff-algorithm=myers",
21729
- "--indent-heuristic",
21730
- "--no-color",
21731
- "--no-ext-diff",
21732
- "--no-textconv",
21733
- "--no-renames",
21734
- "--full-index",
21735
- "--binary",
21736
- "--src-prefix=a/",
21737
- "--dst-prefix=b/",
21738
- `${base}...${head}`,
21739
- "--"
21740
- ], GIT_TIMEOUTS.worktree);
21741
- if (!diff.ok || !diff.stdout)
21694
+
21695
+ // packages/workit-core/src/core/policy/attributes.ts
21696
+ function patternRegex(dir, pattern) {
21697
+ if (!pattern || pattern.endsWith("/") || pattern.startsWith('"') || pattern.startsWith("!"))
21742
21698
  return null;
21743
- const hash = createHash9("sha256");
21744
- for (const line of diff.stdout.split(`
21745
- `)) {
21746
- if (line.startsWith("index "))
21747
- continue;
21748
- hash.update(line.startsWith("@@ ") ? "@@" : line);
21749
- hash.update(`
21750
- `);
21751
- }
21752
- return `sha256:${hash.digest("hex")}`;
21753
- }
21754
- function storeRoot(cwd) {
21755
- const location = resolveStore(path14.resolve(cwd));
21756
- if (location instanceof Error)
21757
- return err("unavailable", location.message.replace(/; fix the repository.*$/u, ""), "fix the repository (git status should work here) or run from a non-git directory");
21758
- return { ok: true, value: { root: location.dir, shared: location.shared } };
21759
- }
21760
- function ledgerPath(cwd) {
21761
- const root = storeRoot(cwd);
21762
- return root.ok ? { ok: true, value: path14.join(root.value.root, "ledger", "ledger.jsonl") } : root;
21763
- }
21764
- var newId2 = () => `${Date.now().toString(36)}-${randomBytes3(6).toString("hex")}`;
21765
- var NETWORK_FS_TYPES = new Set([
21766
- 26985,
21767
- 20859,
21768
- 4266872130,
21769
- 4283649346,
21770
- 1702057286,
21771
- 1397113167,
21772
- 12805120,
21773
- 16914839,
21774
- 1196443219,
21775
- 198183888
21776
- ]);
21777
- var isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0);
21778
- var onNetworkFs = (dir) => {
21779
- if (process.platform !== "linux")
21780
- return false;
21781
- try {
21782
- return isNetworkFsType(fs12.statfsSync(dir).type);
21783
- } catch {
21784
- return false;
21699
+ const anchored = pattern.includes("/");
21700
+ const body = pattern.replace(/^\//u, "");
21701
+ let source = "";
21702
+ for (let index = 0;index < body.length; index += 1) {
21703
+ const char = body[index];
21704
+ if (char === "*" && body[index + 1] === "*") {
21705
+ const slash = body[index + 2] === "/";
21706
+ source += slash ? "(?:.*/)?" : ".*";
21707
+ index += slash ? 2 : 1;
21708
+ } else if (char === "*")
21709
+ source += "[^/]*";
21710
+ else if (char === "?")
21711
+ source += "[^/]";
21712
+ else if (char === "[") {
21713
+ const end = body.indexOf("]", index + 2);
21714
+ if (end < 0)
21715
+ return null;
21716
+ const set = body.slice(index + 1, end).replace(/^!/u, "^").replace(/\\/gu, "\\\\");
21717
+ source += `[${set}]`;
21718
+ index = end;
21719
+ } else if (char === "\\" && index + 1 < body.length) {
21720
+ source += body[index + 1].replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
21721
+ index += 1;
21722
+ } else
21723
+ source += char.replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
21785
21724
  }
21786
- };
21787
- var LOCK_WAIT_MS = 5000;
21788
- var LOCK_STALE_MS = 30000;
21789
- var LOCK_OWNER = "owner";
21790
- var lockToken = (lock) => {
21725
+ const prefix = dir ? `${dir.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}/` : "";
21791
21726
  try {
21792
- return fs12.readFileSync(path14.join(lock, LOCK_OWNER), "utf8");
21727
+ return new RegExp(anchored ? `^${prefix}${source}$` : `^${prefix}(?:.*/)?${source}$`, "u");
21793
21728
  } catch {
21794
21729
  return null;
21795
21730
  }
21796
- };
21797
- var lockAgeMs = (lock) => {
21798
- for (const target of [path14.join(lock, LOCK_OWNER), lock]) {
21799
- try {
21800
- return Date.now() - fs12.statSync(target).mtimeMs;
21801
- } catch {}
21802
- }
21803
- return null;
21804
- };
21805
- var ledgerLock = {
21806
- acquire(lock) {
21807
- try {
21808
- fs12.mkdirSync(lock);
21809
- } catch (error) {
21810
- if (error.code === "EEXIST")
21811
- return null;
21812
- throw error;
21813
- }
21814
- const token = `${process.pid}-${randomBytes3(8).toString("hex")}`;
21815
- fs12.writeFileSync(path14.join(lock, LOCK_OWNER), token);
21816
- return token;
21817
- },
21818
- release(lock, token) {
21819
- if (lockToken(lock) !== token)
21820
- return false;
21821
- const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
21822
- try {
21823
- fs12.renameSync(lock, tomb);
21824
- } catch {
21825
- return false;
21826
- }
21827
- if (lockToken(tomb) !== token) {
21828
- try {
21829
- fs12.renameSync(tomb, lock);
21830
- } catch {}
21831
- return false;
21832
- }
21833
- fs12.rmSync(tomb, { recursive: true, force: true });
21834
- return true;
21835
- },
21836
- token: lockToken,
21837
- reap(lock, observed) {
21838
- const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
21839
- try {
21840
- fs12.renameSync(lock, tomb);
21841
- } catch {
21842
- return false;
21843
- }
21844
- if (lockToken(tomb) === observed) {
21845
- fs12.rmSync(tomb, { recursive: true, force: true });
21846
- return true;
21847
- }
21848
- try {
21849
- fs12.renameSync(tomb, lock);
21850
- } catch {
21851
- fs12.rmSync(tomb, { recursive: true, force: true });
21731
+ }
21732
+ function rulesOf(dir, text) {
21733
+ const out = [];
21734
+ for (const raw of text.split(`
21735
+ `)) {
21736
+ const line = raw.trim();
21737
+ if (!line || line.startsWith("#") || line.startsWith("[attr]"))
21738
+ continue;
21739
+ const [pattern, ...attributes] = line.split(/\s+/u);
21740
+ let generated = null;
21741
+ for (const attribute of attributes) {
21742
+ if (attribute === "linguist-generated" || attribute === "linguist-generated=true")
21743
+ generated = true;
21744
+ else if (attribute === "-linguist-generated" || attribute === "!linguist-generated" || attribute.startsWith("linguist-generated="))
21745
+ generated = false;
21852
21746
  }
21853
- return false;
21854
- }
21855
- };
21856
- function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
21857
- const lock = `${file}.lock`;
21858
- const deadline = Date.now() + waitMs;
21859
- let token = null;
21860
- for (;; ) {
21861
- token = ledgerLock.acquire(lock);
21862
- if (token)
21863
- break;
21864
- const observed = lockToken(lock);
21865
- const age = lockAgeMs(lock);
21866
- if (age !== null && age > LOCK_STALE_MS && ledgerLock.reap(lock, observed))
21747
+ if (generated === null)
21867
21748
  continue;
21868
- if (Date.now() >= deadline)
21869
- return null;
21870
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
21749
+ const regex = patternRegex(dir, pattern);
21750
+ if (regex)
21751
+ out.push({ regex, generated });
21871
21752
  }
21872
- try {
21873
- return fn();
21874
- } finally {
21875
- ledgerLock.release(lock, token);
21753
+ return out;
21754
+ }
21755
+ function generatedByAttributes(files, paths) {
21756
+ const rules = [...files].map(([file, text]) => {
21757
+ const dir = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
21758
+ return {
21759
+ depth: dir ? dir.split("/").length : 0,
21760
+ rules: rulesOf(dir, text)
21761
+ };
21762
+ }).toSorted((left, right) => left.depth - right.depth).flatMap((entry) => entry.rules);
21763
+ const out = new Set;
21764
+ for (const file of paths) {
21765
+ const last = rules.findLast((rule) => rule.regex.test(file));
21766
+ if (last?.generated)
21767
+ out.add(file);
21876
21768
  }
21769
+ return out;
21877
21770
  }
21878
- var sha2562 = (text) => createHash9("sha256").update(text).digest("hex");
21879
- var chainHash = (prevHash, body) => sha2562(`${prevHash ?? ""}
21880
- ${body}`);
21881
- var TAIL_BYTES = 64 * 1024;
21882
- var lastLine = (fd, size) => {
21883
- for (let span = Math.min(size, TAIL_BYTES);span > 0; span = Math.min(size, span * 2)) {
21884
- const buffer = Buffer.alloc(span);
21885
- fs12.readSync(fd, buffer, 0, span, size - span);
21886
- const end = buffer.lastIndexOf(10);
21887
- if (end < 0) {
21888
- if (span === size)
21889
- return null;
21890
- continue;
21771
+
21772
+ // packages/workit-core/src/core/policy/substance.ts
21773
+ var SLASH_BLOCK = [["/*", "*/"]];
21774
+ var C_LIKE = { line: ["//"], block: SLASH_BLOCK, quotes: `"'` };
21775
+ var SYNTAXES = Object.fromEntries([
21776
+ ...["ts", "mts", "cts", "js", "mjs", "cjs"].map((ext) => [ext, { ...C_LIKE, js: true }]),
21777
+ ...["java", "kt", "kts", "scala", "swift", "dart", "groovy"].map((ext) => [
21778
+ ext,
21779
+ { ...C_LIKE, triple: true }
21780
+ ]),
21781
+ ["go", { ...C_LIKE, rawBacktick: true }],
21782
+ ...["css", "scss", "less"].map((ext) => [
21783
+ ext,
21784
+ { line: [], block: SLASH_BLOCK, quotes: `"'`, cssUrl: true }
21785
+ ]),
21786
+ ...["py", "pyi"].map((ext) => [
21787
+ ext,
21788
+ { line: ["#"], block: [], quotes: `"'`, triple: true, indented: true, fstring: true }
21789
+ ]),
21790
+ ["toml", { line: ["#"], block: [], quotes: `"`, literal: "'", triple: true }],
21791
+ ...["html", "htm", "xml", "svg", "xhtml"].map((ext) => [
21792
+ ext,
21793
+ { line: [], block: [["<!--", "-->"]], quotes: "", indented: true }
21794
+ ])
21795
+ ]);
21796
+ var DIRECTIVE = /@ts-|eslint|prettier|biome|oxlint|istanbul|\bc8\b|\bv8\b|@jsx|@flow|@vite|webpack|__PURE__|__NO_SIDE_EFFECTS__|source(?:Mapping)?URL|^\/\s*<|go:|\+build|nolint|noqa|type:|pyright|mypy|pylint|pragma|coding[:=]|fmt:|NOSONAR|@license|@preserve|@refresh|environment|language=|cython|ruff|isort|deno-|@vitest|@jest|swiftlint|ktlint|checkstyle|suppress|nullable|#(?:end)?region/iu;
21797
+ var extensionOf = (file) => {
21798
+ const name = file.slice(file.lastIndexOf("/") + 1);
21799
+ const dot = name.lastIndexOf(".");
21800
+ return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
21801
+ };
21802
+ var CI_PATH = /^\.github\/|(?:^|\/)(?:\.gitlab-ci\.ya?ml|Jenkinsfile|azure-pipelines\.ya?ml|\.pre-commit-config\.ya?ml|\.circleci\/.*)$/u;
21803
+ var syntaxOf = (file) => CI_PATH.test(file) ? null : SYNTAXES[extensionOf(file)] ?? null;
21804
+ var SPACE = /[ \t\r\f\v]/u;
21805
+ var WORD = /[\w$]/u;
21806
+ var NO_SPACE_AFTER = "([{,;";
21807
+ var NO_SPACE_BEFORE = ")]},;";
21808
+ var REGEX_AFTER = "(,=:[!&|?{};+-*%<>~^";
21809
+ var REGEX_KEYWORDS = new Set([
21810
+ "return",
21811
+ "typeof",
21812
+ "instanceof",
21813
+ "case",
21814
+ "do",
21815
+ "else",
21816
+ "in",
21817
+ "of",
21818
+ "new",
21819
+ "delete",
21820
+ "void",
21821
+ "throw",
21822
+ "yield",
21823
+ "await"
21824
+ ]);
21825
+ function substanceOf(file, text) {
21826
+ const syntax = syntaxOf(file);
21827
+ if (!syntax)
21828
+ return plainSubstance(text);
21829
+ const markup = syntax.block.some(([open]) => open === "<!--");
21830
+ if (markup && (/<(?:script|style)\b|<!\[CDATA\[|<\?/iu.test(text) || /[^>\s][ \t]*<!--/u.test(text)))
21831
+ return plainSubstance(text);
21832
+ return lex(syntax, text);
21833
+ }
21834
+ function plainSubstance(text) {
21835
+ return text.split(`
21836
+ `).map((line) => line.replace(/[ \t\r\f\v]+$/u, "")).filter(Boolean).join(`
21837
+ `);
21838
+ }
21839
+ function lex(syntax, text) {
21840
+ const collapse = !syntax.indented;
21841
+ let out = "";
21842
+ let lineStart = 0;
21843
+ let kept = 0;
21844
+ let pendingSpace = false;
21845
+ const frames = [{ kind: "code", braces: 0 }];
21846
+ let index = 0;
21847
+ if (text.startsWith("#!")) {
21848
+ const end = text.indexOf(`
21849
+ `);
21850
+ out = end < 0 ? text : text.slice(0, end);
21851
+ kept = out.length;
21852
+ index = end < 0 ? text.length : end;
21853
+ }
21854
+ const emitCode = (chunk) => {
21855
+ if (collapse && pendingSpace && out.length > lineStart) {
21856
+ const before = out[out.length - 1];
21857
+ if (!NO_SPACE_AFTER.includes(before) && !NO_SPACE_BEFORE.includes(chunk[0]))
21858
+ out += " ";
21891
21859
  }
21892
- let stop = end;
21893
- let text = "";
21894
- while (stop >= 0) {
21895
- const start = buffer.lastIndexOf(10, stop - 1);
21896
- text = buffer.toString("utf8", start + 1, stop).trim();
21897
- if (text) {
21898
- if (start >= 0 || span === size)
21899
- return text;
21900
- break;
21901
- }
21902
- if (start < 0) {
21903
- if (span === size)
21904
- return null;
21905
- break;
21906
- }
21907
- stop = start;
21860
+ pendingSpace = false;
21861
+ out += chunk;
21862
+ };
21863
+ const emitVerbatim = (chunk) => {
21864
+ emitCode(chunk);
21865
+ kept = out.length;
21866
+ };
21867
+ const newline = () => {
21868
+ if (!collapse) {
21869
+ let end = out.length;
21870
+ while (end > Math.max(lineStart, kept) && SPACE.test(out[end - 1]))
21871
+ end -= 1;
21872
+ out = out.slice(0, end);
21908
21873
  }
21909
- if (span === size)
21910
- return text || null;
21911
- }
21912
- return null;
21913
- };
21914
- var tighten = (fd) => {
21915
- try {
21916
- if ((fs12.fstatSync(fd).mode & 63) !== 0)
21917
- fs12.fchmodSync(fd, 384);
21918
- } catch {}
21919
- };
21920
- function appendLine(file, build, options) {
21921
- const write = () => {
21922
- const fd = fs12.openSync(file, "a+", 384);
21923
- try {
21924
- if (process.platform !== "win32")
21925
- tighten(fd);
21926
- const size = fs12.fstatSync(fd).size;
21927
- let fence = "";
21928
- if (size > 0) {
21929
- const last = Buffer.alloc(1);
21930
- fs12.readSync(fd, last, 0, 1, size - 1);
21931
- if (last[0] !== 10)
21932
- fence = `
21874
+ pendingSpace = false;
21875
+ if (out.length > lineStart) {
21876
+ out += `
21933
21877
  `;
21934
- }
21935
- const line = build(lastLine(fd, size));
21936
- if (!line.ok)
21937
- return line;
21938
- const bytes = Buffer.from(fence + line.value);
21939
- const written = fs12.writeSync(fd, bytes);
21940
- if (written !== bytes.length)
21941
- return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
21942
- return { ok: true, value: undefined };
21943
- } finally {
21944
- fs12.closeSync(fd);
21878
+ lineStart = out.length;
21945
21879
  }
21946
21880
  };
21947
- try {
21948
- fs12.mkdirSync(path14.dirname(file), { recursive: true });
21949
- if (!options.forceLock && !onNetworkFs(path14.dirname(file)))
21950
- return write();
21951
- const locked = withLedgerLock(file, write);
21952
- return locked ?? err("busy", `ledger lock ${file}.lock is held`, "retry; remove the .lock directory if no workit process is running");
21953
- } catch (error) {
21954
- return err("unavailable", `cannot append to ${file}: ${error.message}`);
21955
- }
21956
- }
21957
- var sealed = (row, id, at, previous) => {
21958
- const { prevHash: _prev, rowHash: _row, ...fields } = row;
21959
- const prevHash = previous === null ? null : sha2562(previous);
21960
- const body = { v: LEDGER_VERSION, id, at, ...fields, prevHash };
21961
- const full = { ...body, rowHash: chainHash(prevHash, JSON.stringify(body)) };
21962
- return { full, line: `${JSON.stringify(full)}
21963
- ` };
21964
- };
21965
- function appendRaw(cwd, row, options) {
21966
- const file = ledgerPath(cwd);
21967
- if (!file.ok)
21968
- return file;
21969
- const id = newId2();
21970
- const at = (options.now ?? new Date).toISOString();
21971
- let full = null;
21972
- const written = appendLine(file.value, (previous) => {
21973
- const next = sealed(row, id, at, previous);
21974
- const bytes = Buffer.byteLength(next.line);
21975
- if (bytes > MAX_LINE_BYTES)
21976
- return err("invalid_input", `ledger row is ${bytes} bytes; the limit is ${MAX_LINE_BYTES}. Shorten the text and pass details by --ref <path|url>`);
21977
- full = next.full;
21978
- return { ok: true, value: next.line };
21979
- }, options);
21980
- return written.ok ? { ok: true, value: full } : written;
21981
- }
21982
- function observedRowBytes(row) {
21983
- const sample = sealed({ ...row, observer: "workit_cli" }, newId2(), new Date().toISOString(), "");
21984
- return Buffer.byteLength(sample.line);
21985
- }
21986
- function appendObserved(cwd, row, options = {}) {
21987
- return appendRaw(cwd, { ...row, observer: "workit_cli" }, options);
21988
- }
21989
- var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
21990
- var str2 = (value) => typeof value === "string" ? value : null;
21991
- var normalizeActor = (value) => {
21992
- const actor = isRecord3(value) ? value : {};
21993
- return {
21994
- host: str2(actor.host) ?? "unknown",
21995
- session: str2(actor.session),
21996
- agentId: str2(actor.agentId)
21881
+ const regexAllowed = () => {
21882
+ let end = out.length;
21883
+ while (end > 0 && /\s/u.test(out[end - 1]))
21884
+ end -= 1;
21885
+ if (end === 0 || REGEX_AFTER.includes(out[end - 1]))
21886
+ return true;
21887
+ let start = end;
21888
+ while (start > 0 && WORD.test(out[start - 1]))
21889
+ start -= 1;
21890
+ return REGEX_KEYWORDS.has(out.slice(start, end));
21997
21891
  };
21998
- };
21999
- function supersedeAllowed(by, target, onBranch = (row) => (by.branch ?? null) === (row.branch ?? null)) {
22000
- if (by.type !== target.type)
22001
- return `a ${by.type} row cannot supersede a ${target.type} row`;
22002
- if (!by.actor.session || by.actor.session !== target.actor.session)
22003
- return "only the session that wrote a row can supersede it";
22004
- if (!onBranch(target))
22005
- return `the row is on another branch (${String(target.branch ?? "none")}); a row supersedes only one on its own branch`;
22006
- if (by.type === "verdict" && (by.kind ?? "review") !== (target.kind ?? "review"))
22007
- return `a ${String(by.kind ?? "review")} verdict cannot supersede a ${String(target.kind ?? "review")} verdict; record it with --kind ${String(target.kind ?? "review")}`;
22008
- if (by.type === "verdict" && by.self === true && target.self !== true)
22009
- return "a self verdict cannot replace an independent verdict";
22010
- return null;
22011
- }
22012
- function readLedger(cwd, options = {}) {
22013
- const file = ledgerPath(cwd);
22014
- if (!file.ok)
22015
- return file;
22016
- let text = "";
22017
- let stat = null;
22018
- try {
22019
- stat = fs12.statSync(file.value);
22020
- text = fs12.readFileSync(file.value, "utf8");
22021
- } catch (error) {
22022
- if (error.code === "ENOENT")
22023
- return {
22024
- ok: true,
22025
- value: {
22026
- path: file.value,
22027
- rows: [],
22028
- skipped: 0,
22029
- integrity: options.integrity ? { chained: 0, legacy: 0, unverified: [] } : null
22030
- }
22031
- };
22032
- return err("unavailable", `cannot read ${file.value}: ${error.message}`);
22033
- }
22034
- const rows = [];
22035
- let skipped = 0;
22036
- const integrity = { chained: 0, legacy: 0, unverified: [] };
22037
- const earlier = new Set([null]);
22038
- let chainStarted = false;
22039
- const ids = new Set;
22040
- for (const raw of text.split(`
22041
- `)) {
22042
- const trimmed = raw.trim();
22043
- if (!trimmed)
21892
+ const scanString = (quote, escapes, multiline) => {
21893
+ const prefix = text.slice(Math.max(0, index - 2), index).match(/[a-z]*$/iu)?.[0] ?? "";
21894
+ const fstring = syntax.fstring === true && /f/iu.test(prefix);
21895
+ let end = index + quote.length;
21896
+ while (end < text.length) {
21897
+ const char = text[end];
21898
+ if (escapes && char === "\\") {
21899
+ end += 2;
21900
+ continue;
21901
+ }
21902
+ if (text.startsWith(quote, end)) {
21903
+ if (fstring && unbalanced(text.slice(index + quote.length, end)))
21904
+ return false;
21905
+ emitVerbatim(text.slice(index, end + quote.length));
21906
+ index = end + quote.length;
21907
+ return true;
21908
+ }
21909
+ if (char === `
21910
+ ` && !multiline)
21911
+ return false;
21912
+ end += 1;
21913
+ }
21914
+ return false;
21915
+ };
21916
+ while (index < text.length) {
21917
+ const frame = frames[frames.length - 1];
21918
+ const char = text[index];
21919
+ if (frame.kind === "template") {
21920
+ if (char === "\\") {
21921
+ emitVerbatim(text.slice(index, index + 2));
21922
+ index += 2;
21923
+ } else if (char === "`") {
21924
+ emitVerbatim(char);
21925
+ frames.pop();
21926
+ index += 1;
21927
+ } else if (text.startsWith("${", index)) {
21928
+ emitVerbatim("${");
21929
+ frames.push({ kind: "code", braces: 0 });
21930
+ index += 2;
21931
+ } else {
21932
+ out += char;
21933
+ kept = out.length;
21934
+ index += 1;
21935
+ }
22044
21936
  continue;
22045
- const linked = (prevHash) => earlier.has(prevHash);
22046
- if (options.integrity)
22047
- earlier.add(sha2562(trimmed));
22048
- let parsed;
22049
- try {
22050
- parsed = JSON.parse(trimmed);
22051
- } catch {
22052
- skipped += 1;
21937
+ }
21938
+ if (char === `
21939
+ `) {
21940
+ newline();
21941
+ index += 1;
22053
21942
  continue;
22054
21943
  }
22055
- if (!isRecord3(parsed) || !str2(parsed.id) || !str2(parsed.type) || !str2(parsed.at)) {
22056
- skipped += 1;
21944
+ if (SPACE.test(char)) {
21945
+ if (collapse)
21946
+ pendingSpace = true;
21947
+ else
21948
+ out += char;
21949
+ index += 1;
22057
21950
  continue;
22058
21951
  }
22059
- let rowIntegrity = options.integrity ? "verified" : undefined;
22060
- let reason = null;
22061
- if (!options.integrity) {} else if (typeof parsed.rowHash === "string") {
22062
- chainStarted = true;
22063
- const { rowHash, ...body } = parsed;
22064
- const prevHash = str2(parsed.prevHash);
22065
- if (chainHash(prevHash, JSON.stringify(body)) !== rowHash)
22066
- reason = "hash_mismatch";
22067
- else if (!linked(prevHash))
22068
- reason = "broken_link";
22069
- } else if (chainStarted)
22070
- reason = "unsigned";
22071
- else
22072
- rowIntegrity = "legacy";
22073
- if (options.integrity && !reason && ids.has(parsed.id))
22074
- reason = "duplicate_id";
22075
- if (options.integrity)
22076
- ids.add(parsed.id);
22077
- if (reason)
22078
- rowIntegrity = "unverified";
22079
- const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
22080
- const row = {
22081
- ...parsed,
22082
- v: typeof parsed.v === "number" ? parsed.v : LEDGER_VERSION,
22083
- id: parsed.id,
22084
- at: parsed.at,
22085
- type: parsed.type,
22086
- actor: normalizeActor(parsed.actor),
22087
- branch: str2(parsed.branch),
22088
- head: str2(parsed.head),
22089
- base: str2(parsed.base),
22090
- baseSha: str2(parsed.baseSha),
22091
- patchId: str2(parsed.patchId),
22092
- diffHash: str2(parsed.diffHash),
22093
- tree: str2(parsed.tree),
22094
- dirty: typeof parsed.dirty === "boolean" ? parsed.dirty : null,
22095
- ...pr === undefined ? {} : { pr },
22096
- ...str2(parsed.supersedes) ? { supersedes: parsed.supersedes } : {},
22097
- seq: rows.length + 1,
22098
- superseded: false,
22099
- supersedeIgnored: false,
22100
- ...rowIntegrity ? { integrity: rowIntegrity } : {},
22101
- ...reason ? { integrityReason: reason } : {}
22102
- };
22103
- rows.push(row);
22104
- if (!options.integrity)
21952
+ const lineOpener = syntax.line.find((opener) => text.startsWith(opener, index));
21953
+ if (lineOpener) {
21954
+ const end = text.indexOf(`
21955
+ `, index);
21956
+ const comment = text.slice(index, end < 0 ? text.length : end);
21957
+ if (DIRECTIVE.test(comment.slice(lineOpener.length)))
21958
+ emitVerbatim(comment);
21959
+ else
21960
+ pendingSpace = true;
21961
+ index = end < 0 ? text.length : end;
22105
21962
  continue;
22106
- if (rowIntegrity === "verified")
22107
- integrity.chained += 1;
22108
- else if (rowIntegrity === "legacy")
22109
- integrity.legacy += 1;
22110
- else if (reason)
22111
- integrity.unverified.push({
22112
- seq: row.seq,
22113
- id: row.id,
22114
- type: row.type,
22115
- branch: row.branch,
22116
- reason
22117
- });
22118
- }
22119
- const scopes = new Map;
22120
- const scopeOf = (branch) => {
22121
- let scope = scopes.get(branch);
22122
- if (!scope)
22123
- scopes.set(branch, scope = branchScope(cwd, branch));
22124
- return scope;
22125
- };
22126
- const byId = new Map;
22127
- for (const row of rows) {
22128
- const target = row.supersedes ? byId.get(row.supersedes) : undefined;
22129
- if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
22130
- target.superseded = true;
22131
- else if (row.supersedes)
22132
- row.supersedeIgnored = true;
22133
- if (!byId.has(row.id))
22134
- byId.set(row.id, row);
21963
+ }
21964
+ const block = syntax.block.find(([open]) => text.startsWith(open, index));
21965
+ if (block) {
21966
+ const end = text.indexOf(block[1], index + block[0].length);
21967
+ if (end < 0)
21968
+ return null;
21969
+ const comment = text.slice(index, end + block[1].length);
21970
+ if (DIRECTIVE.test(comment.slice(block[0].length, -block[1].length)))
21971
+ emitVerbatim(comment);
21972
+ else
21973
+ pendingSpace = true;
21974
+ if (comment.includes(`
21975
+ `))
21976
+ newline();
21977
+ index = end + block[1].length;
21978
+ continue;
21979
+ }
21980
+ if (syntax.triple && (char === '"' || char === "'") && text.startsWith(char.repeat(3), index)) {
21981
+ if (!scanString(char.repeat(3), !syntax.literal?.includes(char), true))
21982
+ return null;
21983
+ continue;
21984
+ }
21985
+ if (syntax.quotes.includes(char)) {
21986
+ if (!scanString(char, true, Boolean(syntax.multiline)))
21987
+ return null;
21988
+ continue;
21989
+ }
21990
+ if (syntax.literal?.includes(char)) {
21991
+ if (!scanString(char, false, false))
21992
+ return null;
21993
+ continue;
21994
+ }
21995
+ if (syntax.rawBacktick && char === "`") {
21996
+ if (!scanString("`", false, true))
21997
+ return null;
21998
+ continue;
21999
+ }
22000
+ if (syntax.js && char === "`") {
22001
+ emitVerbatim(char);
22002
+ frames.push({ kind: "template" });
22003
+ index += 1;
22004
+ continue;
22005
+ }
22006
+ if (syntax.cssUrl && /^url\(\s*[^"')\s]/iu.test(text.slice(index, index + 64)))
22007
+ return null;
22008
+ if (syntax.js && char === "/" && !regexAllowed() && out.trimEnd().endsWith(")")) {
22009
+ const lineEnd = text.indexOf(`
22010
+ `, index);
22011
+ if (text.slice(index + 1, lineEnd < 0 ? text.length : lineEnd).includes("/"))
22012
+ return null;
22013
+ }
22014
+ if (syntax.js && char === "/" && regexAllowed()) {
22015
+ let end = index + 1;
22016
+ let inClass = false;
22017
+ while (end < text.length && (inClass || text[end] !== "/")) {
22018
+ if (text[end] === `
22019
+ `)
22020
+ return null;
22021
+ if (text[end] === "\\")
22022
+ end += 1;
22023
+ else if (text[end] === "[")
22024
+ inClass = true;
22025
+ else if (text[end] === "]")
22026
+ inClass = false;
22027
+ end += 1;
22028
+ }
22029
+ if (end >= text.length)
22030
+ return null;
22031
+ emitVerbatim(text.slice(index, end + 1));
22032
+ index = end + 1;
22033
+ continue;
22034
+ }
22035
+ if (frames.length > 1 && frame.kind === "code") {
22036
+ if (char === "{")
22037
+ frame.braces += 1;
22038
+ else if (char === "}") {
22039
+ if (frame.braces === 0) {
22040
+ emitVerbatim(char);
22041
+ frames.pop();
22042
+ index += 1;
22043
+ continue;
22044
+ }
22045
+ frame.braces -= 1;
22046
+ }
22047
+ }
22048
+ emitCode(char);
22049
+ index += 1;
22135
22050
  }
22136
- if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
22137
- tamperCache.set(file.value, {
22138
- size: stat.size,
22139
- stamp: fileStamp(stat),
22140
- breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
22141
- });
22142
- return {
22143
- ok: true,
22144
- value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
22145
- };
22051
+ if (frames.length !== 1)
22052
+ return null;
22053
+ newline();
22054
+ return out;
22146
22055
  }
22147
- var TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
22148
- var tamperCache = new Map;
22149
- var fileStamp = (stat) => `${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`;
22150
- function ledgerTampering(cwd) {
22151
- const file = ledgerPath(cwd);
22152
- if (!file.ok)
22153
- return [];
22154
- try {
22155
- const stat = fs12.statSync(file.value);
22156
- const cached = tamperCache.get(file.value);
22157
- if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
22158
- return cached.breaks;
22159
- } catch {
22160
- return [];
22056
+ function unbalanced(body) {
22057
+ let depth = 0;
22058
+ for (const part of body.replaceAll("{{", "").replaceAll("}}", "")) {
22059
+ if (part === "{")
22060
+ depth += 1;
22061
+ else if (part === "}")
22062
+ depth = Math.max(0, depth - 1);
22161
22063
  }
22162
- const read = readLedger(cwd, { integrity: true });
22163
- return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
22164
- }
22165
- function tamperedUnblock(cwd) {
22166
- const file = ledgerPath(cwd);
22167
- const where = file.ok ? file.value : "the ledger file";
22168
- return `workit ledger verify-integrity # lists the edited or missing rows; if the edit is understood, move ${where} aside (mv ${where} ${where}.tampered-<date>) and record the verdicts again`;
22169
- }
22170
- var tamperedWarning = (cwd, breaks) => `the ledger was edited, or a row was removed or moved (${breaks.slice(0, 3).map((row) => `${row.reason} at row ${row.seq}`).join(", ")}${breaks.length > 3 ? ", …" : ""}): no verdict is accepted until it is repaired. ${tamperedUnblock(cwd)}`;
22171
- function verdictBasis(verdict, current) {
22172
- if (verdict.head && verdict.head === current.head)
22173
- return "fresh";
22174
- if (verdict.patchId && verdict.patchId === current.patchId && verdict.diffHash && verdict.diffHash === current.diffHash)
22175
- return "carried";
22176
- return "stale";
22064
+ return depth > 0;
22177
22065
  }
22178
- var isLiveVerdict = (row, retired) => row.type === "verdict" && typeof row.result === "string" && !retired.has(row.id);
22179
- function trailerSessions(cwd, base, head) {
22180
- if (!base || !head || !safeRef(base) || !safeRef(head))
22181
- return [];
22182
- const out = git4(cwd, [
22183
- "log",
22184
- "--format=%(trailers:key=Workit-Session,valueonly,separator=%x00)",
22185
- `${base}..${head}`
22186
- ]);
22187
- return (out ?? "").split(/[\0\n]/u).map((value) => value.trim()).filter(Boolean);
22066
+ function hasOpaqueLineBreak(text) {
22067
+ return /\r(?!\n)|[\u2028\u2029]/u.test(text);
22188
22068
  }
22189
- var RENAMED = /^branch: renamed refs\/heads\/(.+) to refs\/heads\/(.+)$/iu;
22190
- function branchScope(cwd, branch) {
22191
- const renamedAt = new Map;
22192
- let since = Number.NEGATIVE_INFINITY;
22193
- if (safeRef(branch)) {
22194
- const log = git4(cwd, [
22195
- "log",
22196
- "-g",
22197
- "--date=unix",
22198
- "--format=%gd%x09%gs",
22199
- `refs/heads/${branch}`,
22200
- "--"
22201
- ]);
22202
- for (const line of (log ?? "").split(`
22203
- `)) {
22204
- const [selector = "", subject = ""] = line.split("\t");
22205
- const seconds = Number(/@\{(\d+)\}$/u.exec(selector)?.[1]);
22206
- if (!Number.isFinite(seconds))
22207
- continue;
22208
- since = seconds * 1000;
22209
- const match = RENAMED.exec(subject);
22210
- if (!match || match[1] === branch)
22211
- continue;
22212
- const until = (seconds + 1) * 1000;
22213
- renamedAt.set(match[1], Math.max(renamedAt.get(match[1]) ?? 0, until));
22214
- }
22069
+ var decoder = new TextDecoder("utf-8", { fatal: true });
22070
+ function textOf(bytes) {
22071
+ if (bytes.includes(0))
22072
+ return null;
22073
+ try {
22074
+ return decoder.decode(bytes);
22075
+ } catch {
22076
+ return null;
22215
22077
  }
22216
- return (row) => {
22217
- if (row.branch === branch)
22218
- return true;
22219
- const until = row.branch === null ? undefined : renamedAt.get(row.branch);
22220
- if (until === undefined)
22221
- return false;
22222
- const at = Date.parse(row.at);
22223
- return at >= since && at <= until;
22224
- };
22225
22078
  }
22226
- var renameAwareBranch = (scopeOf, branch) => {
22227
- return (target) => {
22228
- const own = typeof branch === "string" ? branch : null;
22229
- const theirs = typeof target.branch === "string" ? target.branch : null;
22230
- if (own === theirs)
22231
- return true;
22232
- if (own === null || theirs === null || typeof target.at !== "string")
22233
- return false;
22234
- return scopeOf(own)({ branch: theirs, at: target.at });
22235
- };
22236
- };
22237
- function authorSessions(cwd, rows, branch, range = { base: null, head: null }, scope = branchScope(cwd, branch)) {
22238
- const sessions = new Set;
22239
- for (const row of rows) {
22240
- if (!scope(row) || row.type !== "commit.recorded" && row.type !== "task.opened")
22241
- continue;
22242
- const session = str2(row.session) ?? row.actor.session;
22243
- if (session)
22244
- sessions.add(session);
22245
- }
22246
- for (const session of trailerSessions(cwd, range.base, range.head))
22247
- sessions.add(session);
22248
- return sessions;
22079
+ function cosmeticChange(file, before, after) {
22080
+ const left = textOf(before);
22081
+ const right = textOf(after);
22082
+ if (left === null || right === null)
22083
+ return false;
22084
+ if (hasOpaqueLineBreak(left) || hasOpaqueLineBreak(right))
22085
+ return false;
22086
+ const a = substanceOf(file, left);
22087
+ return a !== null && a === substanceOf(file, right);
22249
22088
  }
22250
- var derivesFrom = (derived, bare) => !bare.includes(":") && derived.endsWith(`:${bare}`);
22251
- var authoredBy = (authors, session) => {
22252
- if (authors.has(session))
22253
- return true;
22254
- for (const author of authors)
22255
- if (derivesFrom(author, session) || derivesFrom(session, author))
22256
- return true;
22257
- return false;
22089
+
22090
+ // packages/workit-core/src/core/policy/lane.ts
22091
+ var LANE_THRESHOLDS = {
22092
+ selfMaxLines: 100,
22093
+ selfMaxFiles: 5,
22094
+ fullMinLines: 500
22258
22095
  };
22259
- var TRUST_REASONS = new Set([
22260
- "unsigned",
22261
- "hash_mismatch",
22262
- "broken_link",
22263
- "duplicate_id",
22264
- "unissued_session",
22265
- "unbound_session"
22096
+ var RISK_TOKENS = new Set([
22097
+ "auth",
22098
+ "authn",
22099
+ "authz",
22100
+ "oauth",
22101
+ "security",
22102
+ "permission",
22103
+ "permissions",
22104
+ "secret",
22105
+ "secrets",
22106
+ "crypto",
22107
+ "gate",
22108
+ "gates",
22109
+ "policy",
22110
+ "policies",
22111
+ "hook",
22112
+ "hooks",
22113
+ "husky",
22114
+ "lefthook",
22115
+ "release",
22116
+ "releases",
22117
+ "migration",
22118
+ "migrations",
22119
+ "migrate",
22120
+ "workflows"
22266
22121
  ]);
22267
- var SHELL_VOUCHED_HOSTS = new Set([
22268
- "cli",
22269
- "opencode",
22270
- "pi",
22271
- "codex_cli",
22272
- "codex_desktop"
22122
+ var RISK_FILES = new Set([
22123
+ ".gitlab-ci.yml",
22124
+ "Jenkinsfile",
22125
+ "azure-pipelines.yml",
22126
+ ".pre-commit-config.yaml",
22127
+ ".workit/risk-paths"
22273
22128
  ]);
22274
- function sessionTrust(cwd, row) {
22275
- const session = row.actor.session;
22276
- if (!session || row.self === true)
22277
- return null;
22278
- const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
22279
- const host = row.actor.host;
22280
- const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
22281
- if (!issuedOnly)
22282
- return null;
22283
- const binding = issuedBinding(cwd, session);
22284
- if (binding === null)
22285
- return "unissued_session";
22286
- return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
22129
+ var RISK_NAMES = new Set([".gitattributes"]);
22130
+ var RISK_PATHS_FILE = ".workit/risk-paths";
22131
+ var LOCKFILES = new Set([
22132
+ "bun.lock",
22133
+ "bun.lockb",
22134
+ "package-lock.json",
22135
+ "npm-shrinkwrap.json",
22136
+ "yarn.lock",
22137
+ "pnpm-lock.yaml",
22138
+ "Cargo.lock",
22139
+ "go.sum",
22140
+ "poetry.lock",
22141
+ "uv.lock",
22142
+ "Pipfile.lock",
22143
+ "Gemfile.lock",
22144
+ "composer.lock"
22145
+ ]);
22146
+ var DOC_EXT = /\.(?:md|mdx|markdown|rst|adoc)$/iu;
22147
+ var DOC_NAMES = /^(?:license|licence|notice|authors|changelog|changes|contributors)$/iu;
22148
+ var INSTRUCTIONS = /(?:^|\/)(?:AGENTS|CLAUDE|GEMINI|SKILL)\.md$|(?:^|\/)(?:skills|agents|commands|rules|prompts)\/[^/]+/iu;
22149
+ var TEST_PATH = /(?:^|\/)(?:test|tests|__tests__)\/|\.(?:test|spec)\.[^/]+$|_test\.[^/]+$/iu;
22150
+ var basename = (file) => file.slice(file.lastIndexOf("/") + 1);
22151
+ var isDoc = (file) => !INSTRUCTIONS.test(file) && (DOC_EXT.test(file) || DOC_NAMES.test(basename(file)));
22152
+ function kindOf(file) {
22153
+ if (file.binary)
22154
+ return "behavior";
22155
+ if (file.generated)
22156
+ return "generated";
22157
+ if (LOCKFILES.has(basename(file.path)))
22158
+ return "lockfile";
22159
+ if (isDoc(file.path))
22160
+ return "doc";
22161
+ if (file.cosmetic)
22162
+ return "cosmetic";
22163
+ return TEST_PATH.test(file.path) ? "test" : "behavior";
22164
+ }
22165
+ var globCache = new Map;
22166
+ function globMatch(pattern, file) {
22167
+ let regex = globCache.get(pattern);
22168
+ if (!regex) {
22169
+ let source = "";
22170
+ for (let index = 0;index < pattern.length; index += 1) {
22171
+ const char = pattern[index];
22172
+ if (char === "*" && pattern[index + 1] === "*") {
22173
+ source += pattern[index + 2] === "/" ? "(?:[^/]*/)*" : ".*";
22174
+ index += pattern[index + 2] === "/" ? 2 : 1;
22175
+ } else if (char === "*")
22176
+ source += "[^/]*";
22177
+ else if (char === "?")
22178
+ source += "[^/]";
22179
+ else
22180
+ source += char.replace(/[.+^${}()|[\]\\]/gu, "\\$&");
22181
+ }
22182
+ regex = new RegExp(`^${source}$`, "u");
22183
+ globCache.set(pattern, regex);
22184
+ }
22185
+ return regex.test(file) || file.startsWith(`${pattern.replace(/\/$/u, "")}/`);
22287
22186
  }
22288
- function trustReasons(cwd, row) {
22289
- if (row.integrity === "unverified")
22290
- return [row.integrityReason ?? "unsigned"];
22291
- if (row.integrity !== "verified")
22292
- return ["unsigned"];
22293
- const session = sessionTrust(cwd, row);
22294
- return session ? [session] : [];
22187
+ function isRiskPath(file, extra = []) {
22188
+ if (extra.some((pattern) => globMatch(pattern, file)))
22189
+ return true;
22190
+ if (RISK_FILES.has(file) || RISK_NAMES.has(basename(file)))
22191
+ return true;
22192
+ if (TEST_PATH.test(file) || isDoc(file))
22193
+ return false;
22194
+ return file.split("/").map((segment, index, all) => index === all.length - 1 ? segment.replace(/\.[^.]+$/u, "") : segment).some((segment) => segment.toLowerCase().split(/[-_.]/u).some((token) => RISK_TOKENS.has(token)));
22295
22195
  }
22296
- function withIntegrity(cwd, rows) {
22297
- if (rows.every((row) => row.integrity !== undefined))
22298
- return rows;
22299
- const read = readLedger(cwd, { integrity: true });
22300
- const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
22301
- return rows.map((row) => {
22302
- if (row.integrity !== undefined)
22303
- return row;
22304
- const fresh = bySeq.get(row.seq);
22305
- return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
22196
+ var parseRiskPaths = (text) => text.split(`
22197
+ `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.replace(/^\//u, ""));
22198
+ function computeLane(input) {
22199
+ const riskFiles = input.files.filter((file) => isRiskPath(file.path, input.riskPaths)).map((file) => file.path);
22200
+ const risky = new Set(riskFiles);
22201
+ const kinds = input.files.map((file) => ({
22202
+ file,
22203
+ kind: risky.has(file.path) ? "behavior" : kindOf(file)
22204
+ }));
22205
+ const behavior = kinds.filter((entry) => entry.kind === "behavior" || entry.kind === "test" || entry.kind === "lockfile");
22206
+ const lines = behavior.filter((entry) => entry.kind !== "lockfile").reduce((sum, entry) => sum + entry.file.added + entry.file.deleted, 0);
22207
+ const opaque = behavior.filter((entry) => entry.kind === "lockfile" || entry.file.binary);
22208
+ const result = (lane, reasons) => ({
22209
+ lane,
22210
+ reasons,
22211
+ lines,
22212
+ files: behavior.length,
22213
+ riskFiles
22306
22214
  });
22215
+ const { selfMaxLines, selfMaxFiles, fullMinLines } = LANE_THRESHOLDS;
22216
+ if (input.riskTier === "high")
22217
+ return result("full", ["the task judgment records high risk"]);
22218
+ if (riskFiles.length)
22219
+ return result("full", [
22220
+ `touches a risk path: ${riskFiles.slice(0, 3).join(", ")}${riskFiles.length > 3 ? ", …" : ""}`
22221
+ ]);
22222
+ if (!behavior.length)
22223
+ return result("none", [
22224
+ input.files.length ? "only docs, comments, formatting or generated copies changed" : "no changes"
22225
+ ]);
22226
+ if (lines >= fullMinLines)
22227
+ return result("full", [`large diff: ${lines} lines (≥ ${fullMinLines})`]);
22228
+ if (lines > selfMaxLines)
22229
+ return result("focused", [`${lines} lines changed (> ${selfMaxLines})`]);
22230
+ if (behavior.length > selfMaxFiles)
22231
+ return result("focused", [`${behavior.length} behavior files changed (> ${selfMaxFiles})`]);
22232
+ if (opaque.length)
22233
+ return result("focused", [
22234
+ `a lockfile or binary changed: ${opaque.slice(0, 3).map((entry) => entry.file.path).join(", ")}${opaque.length > 3 ? ", …" : ""}`
22235
+ ]);
22236
+ return result("self", [
22237
+ `small change: ${lines} line${lines === 1 ? "" : "s"} in ${behavior.length} file${behavior.length === 1 ? "" : "s"}, no risk path`
22238
+ ]);
22307
22239
  }
22308
- var untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason));
22309
- var independenceReasons = (row, authors) => {
22310
- const reasons = [];
22311
- if (!row.actor.session)
22312
- reasons.push("no_session");
22313
- else if (authoredBy(authors, row.actor.session))
22314
- reasons.push("author_session");
22315
- if (row.self === true && !reasons.includes("no_session"))
22316
- reasons.push("self");
22317
- return reasons;
22318
- };
22319
- function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
22320
- let effective = null;
22321
- for (const row of rows) {
22322
- if (!effective) {
22323
- effective = row;
22324
- continue;
22325
- }
22326
- if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
22327
- continue;
22328
- const rowIndependent = reasonsOf(row).length === 0;
22329
- const effIndependent = reasonsOf(effective).length === 0;
22330
- if (effIndependent && !rowIndependent && isCurrent(effective))
22331
- continue;
22332
- if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
22333
- continue;
22334
- effective = row;
22240
+ function untrackedStat(cwd, file) {
22241
+ try {
22242
+ const bytes = readFileSync4(path14.join(cwd, file));
22243
+ if (bytes.includes(0))
22244
+ return { path: file, added: 0, deleted: 0, binary: true };
22245
+ const text = bytes.toString("utf8");
22246
+ if (hasOpaqueLineBreak(text))
22247
+ return { path: file, added: 0, deleted: 0, binary: true };
22248
+ const lines = text.split(`
22249
+ `).length - (text.endsWith(`
22250
+ `) ? 1 : 0);
22251
+ return { path: file, added: lines, deleted: 0 };
22252
+ } catch {
22253
+ return { path: file, added: 0, deleted: 0, binary: true };
22335
22254
  }
22336
- return effective;
22337
22255
  }
22338
- function restackCarries(rows, branch, from, to) {
22339
- if (!from || !to)
22340
- return false;
22341
- const edges = new Map;
22342
- for (const row of rows)
22343
- if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.integrity === "verified" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
22344
- edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
22345
- const seen = new Set([from]);
22346
- const queue = [from];
22347
- while (queue.length) {
22348
- const next = queue.shift();
22349
- if (next === to)
22350
- return true;
22351
- for (const target of edges.get(next) ?? [])
22352
- if (!seen.has(target)) {
22353
- seen.add(target);
22354
- queue.push(target);
22256
+ var REGULAR = new Set(["100644", "100755"]);
22257
+ var ABSENT = /^0+$/u;
22258
+ function cosmeticFiles(cwd, fork, head) {
22259
+ const raw = diffRaw(cwd, fork, head);
22260
+ if (!raw)
22261
+ return null;
22262
+ const candidates = raw.filter((entry) => REGULAR.has(entry.oldMode) && entry.oldMode === entry.newMode && !ABSENT.test(entry.oldBlob) && (head === null || !ABSENT.test(entry.newBlob)));
22263
+ const blobs = readBlobs(cwd, candidates.flatMap((entry) => [entry.oldBlob, entry.newBlob]));
22264
+ if (!blobs)
22265
+ return null;
22266
+ const out = new Set;
22267
+ const opaque = new Set;
22268
+ for (const entry of candidates) {
22269
+ const before = blobs.get(entry.oldBlob);
22270
+ let after = blobs.get(entry.newBlob);
22271
+ if (!after && head === null && ABSENT.test(entry.newBlob))
22272
+ try {
22273
+ after = readFileSync4(path14.join(cwd, entry.path));
22274
+ } catch {
22275
+ after = undefined;
22355
22276
  }
22277
+ if (before && after && cosmeticChange(entry.path, before, after))
22278
+ out.add(entry.path);
22279
+ else if ([before, after].some((bytes) => bytes && hasOpaqueLineBreak(bytes.toString("utf8"))))
22280
+ opaque.add(entry.path);
22356
22281
  }
22357
- return false;
22282
+ return { cosmetic: out, opaque };
22358
22283
  }
22359
- function checkVerdicts(cwd, branch, ledgerRows) {
22360
- const rows = withIntegrity(cwd, ledgerRows);
22361
- const head = branchHead(cwd, branch);
22362
- const fallbackBase = defaultBase(cwd);
22363
- const scope = branchScope(cwd, branch);
22364
- const keyCache = new Map;
22365
- const keysAt = (base, at) => {
22366
- if (!base || !at)
22367
- return { patchId: null, diffHash: null };
22368
- const cacheKey = `${base}\x00${at}`;
22369
- let key = keyCache.get(cacheKey);
22370
- if (!key) {
22371
- key = { patchId: patchId(cwd, base, at), diffHash: diffHash(cwd, base, at) };
22372
- keyCache.set(cacheKey, key);
22373
- }
22374
- return key;
22375
- };
22376
- const keyFor = (base) => keysAt(base, head);
22377
- const basisOf = (row) => {
22378
- const basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
22379
- return basis === "stale" && restackCarries(rows, branch, row.head, head) ? "carried" : basis;
22380
- };
22381
- const sameCode = (failed, row) => {
22382
- if (failed.head && failed.head === row.head)
22383
- return true;
22384
- const base = failed.base ?? fallbackBase;
22385
- const stored = (row.base ?? fallbackBase) === base && row.baseSha !== null && row.baseSha === failed.baseSha;
22386
- const keys = stored ? { patchId: row.patchId, diffHash: row.diffHash } : keysAt(base, row.head);
22387
- return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(rows, branch, failed.head, row.head);
22388
- };
22389
- const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
22390
- const failureAuthorCache = new Map;
22391
- const failureAuthors = (row) => {
22392
- let set = failureAuthorCache.get(row.seq);
22393
- if (!set) {
22394
- set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
22395
- failureAuthorCache.set(row.seq, set);
22396
- }
22397
- return set;
22398
- };
22399
- const reasonCache = new Map;
22400
- const reasonsOf = (row) => {
22401
- let reasons = reasonCache.get(row);
22402
- if (!reasons) {
22403
- const failing = FAILING.has(String(row.result));
22404
- reasons = [
22405
- ...trustReasons(cwd, row),
22406
- ...independenceReasons(row, failing ? failureAuthors(row) : authors)
22407
- ];
22408
- reasonCache.set(row, reasons);
22409
- }
22410
- return reasons;
22411
- };
22412
- const retired = new Set;
22413
- for (const row of rows)
22414
- if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
22415
- retired.add(row.supersedes);
22416
- const byKind = new Map;
22417
- for (const row of rows)
22418
- if (isLiveVerdict(row, retired) && scope(row)) {
22419
- const kind = typeof row.kind === "string" ? row.kind : "review";
22420
- byKind.set(kind, [...byKind.get(kind) ?? [], row]);
22421
- }
22422
- const verdicts = [];
22423
- const warnings = [];
22424
- for (const [kind, list] of byKind) {
22425
- const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
22426
- if (!row)
22427
- continue;
22428
- const basis = basisOf(row);
22429
- const current = basis !== "stale";
22430
- const independence = reasonsOf(row);
22431
- const reasons = [
22432
- ...current ? [] : ["stale"],
22433
- ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
22434
- ...independence
22435
- ];
22436
- verdicts.push({
22437
- kind,
22438
- basis,
22439
- current,
22440
- independent: independence.length === 0,
22441
- accepted: reasons.length === 0,
22442
- reasons,
22443
- verdict: row
22444
- });
22445
- if (reasons.length === 0) {
22446
- for (const later of list)
22447
- if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
22448
- warnings.push(`the author's own (self) ${String(later.result)} ${kind} verdict ${later.id} by ${later.actor.session ?? "no session"} on the current code is newer than the accepted pass ${row.id}: read it before merging`);
22449
- }
22284
+ function generatedAt(cwd, fork, paths) {
22285
+ const files = treeFilesNamed(cwd, fork, ".gitattributes");
22286
+ const blobs = files?.size ? readBlobs(cwd, [...files.values()]) : null;
22287
+ if (!files || !blobs)
22288
+ return new Set;
22289
+ const texts = new Map;
22290
+ for (const [file, id] of files) {
22291
+ const bytes = blobs.get(id);
22292
+ if (bytes)
22293
+ texts.set(file, bytes.toString("utf8"));
22450
22294
  }
22451
- for (const row of rows)
22452
- if (row.integrity === "unverified" && scope(row))
22453
- warnings.push(`ledger row ${row.id} (${row.type}) on ${row.branch ?? branch} is unverified: the workit CLI did not write it or it changed afterwards (workit ledger verify-integrity)`);
22454
- const tampered = ledgerTampering(cwd);
22455
- if (tampered.length) {
22456
- warnings.push(tamperedWarning(cwd, tampered));
22457
- for (const entry of verdicts) {
22458
- entry.accepted = false;
22459
- entry.reasons = [...entry.reasons, "ledger_tampered"];
22460
- }
22295
+ return generatedByAttributes(texts, paths);
22296
+ }
22297
+ var forkOf = (cwd, base, head) => mergeBase(cwd, base, head ?? "HEAD");
22298
+ function laneFiles(cwd, fork, head, attributesAt = fork) {
22299
+ const stats = diffNumstat(cwd, fork, head);
22300
+ const cosmetic = stats ? cosmeticFiles(cwd, fork, head) : null;
22301
+ if (!stats || !cosmetic)
22302
+ return null;
22303
+ const files = [
22304
+ ...stats.map((entry) => ({
22305
+ ...entry,
22306
+ ...cosmetic.opaque.has(entry.path) ? { binary: true } : {},
22307
+ cosmetic: !entry.binary && cosmetic.cosmetic.has(entry.path)
22308
+ })),
22309
+ ...head === null ? untrackedFiles(cwd).map((file) => untrackedStat(cwd, file)) : []
22310
+ ];
22311
+ const generated = generatedAt(cwd, attributesAt, files.map((entry) => entry.path));
22312
+ return files.map((entry) => ({
22313
+ ...entry,
22314
+ generated: generated.has(entry.path)
22315
+ }));
22316
+ }
22317
+ function driftFiles(cwd, range) {
22318
+ if (range.fromFork === range.toFork)
22319
+ return laneFiles(cwd, range.from, range.to, range.toFork);
22320
+ const drifted = ownChangeDrift(cwd, range);
22321
+ const stats = drifted ? diffNumstat(cwd, range.from, range.to) : null;
22322
+ if (!drifted || !stats)
22323
+ return null;
22324
+ const byPath = new Map(stats.map((entry) => [entry.path, entry]));
22325
+ const generated = generatedAt(cwd, range.toFork, drifted);
22326
+ return drifted.map((file) => ({
22327
+ path: file,
22328
+ added: byPath.get(file)?.added ?? 0,
22329
+ deleted: byPath.get(file)?.deleted ?? 0,
22330
+ ...byPath.get(file)?.binary ? { binary: true } : {},
22331
+ cosmetic: false,
22332
+ generated: generated.has(file)
22333
+ }));
22334
+ }
22335
+ function branchLane(cwd, options) {
22336
+ const candidates = options.base ? [`origin/${options.base}`, options.base] : [];
22337
+ const base = candidates.find((ref) => resolveRef(cwd, ref) !== null) ?? defaultBase(cwd) ?? null;
22338
+ const head = options.workingTree ? null : options.head;
22339
+ const fork = base && (options.workingTree || options.head) ? forkOf(cwd, base, head) : null;
22340
+ const files = fork ? laneFiles(cwd, fork, head) : null;
22341
+ const riskTier = options.judgment?.riskTier ?? null;
22342
+ if (!fork || !files) {
22343
+ const high = riskTier === "high";
22344
+ return {
22345
+ lane: high ? "full" : "focused",
22346
+ reasons: [
22347
+ `the diff of ${options.head?.slice(0, 12) ?? "(no head)"} against ${base ?? "(no base)"} cannot be read`,
22348
+ ...high ? ["the task judgment records high risk"] : []
22349
+ ],
22350
+ lines: 0,
22351
+ files: 0,
22352
+ riskFiles: [],
22353
+ base,
22354
+ head: options.head
22355
+ };
22461
22356
  }
22462
- verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
22463
- const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
22464
- const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
22465
- const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
22466
- const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
22467
- accepted: false,
22468
- verdict: failing?.verdict ?? null,
22469
- reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
22470
- };
22471
- const latestSelf = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && !untrusted(entry.reasons) && (STRONG_RESULTS.has(String(entry.verdict.result)) || FAILING.has(String(entry.verdict.result))));
22472
- const selfEntry = latestSelf && STRONG_RESULTS.has(String(latestSelf.verdict.result)) ? latestSelf : undefined;
22357
+ const repoRisk = parseRiskPaths(fileAt(cwd, fork, RISK_PATHS_FILE) ?? "");
22473
22358
  return {
22474
- branch,
22475
- head,
22476
- review: accepted.accepted ? "verified" : selfEntry ? "self-reviewed" : "unreviewed",
22477
- selfVerdict: accepted.accepted ? null : selfEntry?.verdict ?? null,
22478
- base: newestCurrent?.verdict.base ?? fallbackBase,
22479
- patchId: keyFor(newestCurrent?.verdict.base ?? fallbackBase).patchId,
22480
- current: { basis: newestCurrent?.basis ?? "none", verdict: newestCurrent?.verdict ?? null },
22481
- accepted,
22482
- verdicts,
22483
- authors: [...authors].toSorted(),
22484
- warnings
22359
+ ...computeLane({
22360
+ files,
22361
+ riskTier,
22362
+ riskPaths: [...repoRisk, ...options.riskPaths ?? []]
22363
+ }),
22364
+ base,
22365
+ head: options.head
22485
22366
  };
22486
22367
  }
22487
- var OBSERVED_ONLY = new Set([
22488
- "check",
22489
- "merge.unverified",
22490
- "policy.judged",
22491
- ...PR_ROW_TYPES
22492
- ]);
22493
-
22494
- // packages/workit-core/src/core/policy/lane.ts
22495
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
22496
- import path16 from "node:path";
22368
+ var laneNeed = (lane) => lane === "none" ? "none" : lane === "self" ? "self" : "non-author";
22497
22369
 
22498
- // packages/workit-core/src/core/pr-create.ts
22499
- import fs13 from "node:fs";
22500
- import path15 from "node:path";
22501
- import { spawnSync as spawnSync10 } from "node:child_process";
22502
- // packages/workit-core/src/forge/pr-body.ts
22503
- function parseGhIssue(value) {
22504
- const m = /issues\/(\d+)/.exec(value);
22505
- return m ? m[1] : String(value).trim().replace(/^#/, "");
22370
+ // packages/workit-core/src/core/policy/carry.ts
22371
+ var TEST_ONLY_KINDS = new Set(["doc", "cosmetic", "generated", "test"]);
22372
+ function verdictDrift(cwd, input) {
22373
+ const since = input.from;
22374
+ const stale = (reason, files = null, focused = false) => ({
22375
+ carries: false,
22376
+ since,
22377
+ files,
22378
+ focused,
22379
+ reason
22380
+ });
22381
+ const fromFork = mergeBase(cwd, input.fromBase, input.from);
22382
+ const toFork = mergeBase(cwd, input.toBase, input.to);
22383
+ const files = fromFork && toFork ? driftFiles(cwd, { from: input.from, to: input.to, fromFork, toFork }) : null;
22384
+ if (!fromFork || !toFork || !files)
22385
+ return stale(`the change since ${since.slice(0, 12)} cannot be read`);
22386
+ const riskPaths = [fromFork, toFork].flatMap((fork) => parseRiskPaths(fileAt(cwd, fork, RISK_PATHS_FILE) ?? ""));
22387
+ const lane = computeLane({ files, riskPaths });
22388
+ const paths = files.map((file) => file.path);
22389
+ if (lane.lane === "none")
22390
+ return { carries: true, rule: "lane-none", since, files: paths };
22391
+ if (input.kind === "unit" && lane.lane === "self" && files.every((file) => TEST_ONLY_KINDS.has(kindOf(file))))
22392
+ return { carries: true, rule: "tests-only", since, files: paths };
22393
+ return stale(lane.reasons[0] ?? `lane ${lane.lane}`, paths, lane.lane === "self");
22506
22394
  }
22507
22395
 
22508
- // packages/workit-core/src/core/pr-create.ts
22509
- function whichOnPath(tool) {
22510
- const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
22511
- for (const dir of (process.env.PATH ?? "").split(path15.delimiter)) {
22512
- if (!dir)
22513
- continue;
22514
- for (const name of names) {
22515
- const candidate = path15.join(dir, name);
22516
- try {
22517
- fs13.accessSync(candidate, fs13.constants.X_OK);
22518
- return candidate;
22519
- } catch {}
22520
- }
22521
- }
22522
- return null;
22523
- }
22524
- var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
22525
- var pushRemote = (cwd) => {
22526
- const result = spawnSync10("git", ["remote", "get-url", "--push", "--all", "origin"], {
22396
+ // packages/workit-core/src/ledger.ts
22397
+ var LEDGER_VERSION = 1;
22398
+ var MAX_LINE_BYTES = 4096;
22399
+ var PASSING = new Set(["verified", "tests-verified", "type-check-only"]);
22400
+ var FAILING = new Set(["failed", "blocked"]);
22401
+ var STRONG_RESULTS = new Set(["verified", "tests-verified"]);
22402
+ var PR_ROW_TYPES = new Set([
22403
+ "pr.created",
22404
+ "pr.status",
22405
+ "pr.merged",
22406
+ "pr.linked"
22407
+ ]);
22408
+ var err = (code, error, unblock) => ({
22409
+ ok: false,
22410
+ code,
22411
+ error,
22412
+ ...unblock ? { unblock } : {}
22413
+ });
22414
+ var gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
22415
+ const run = spawnSync9("git", args, {
22527
22416
  cwd,
22528
- encoding: "utf8"
22417
+ encoding: "utf8",
22418
+ timeout: timeoutMs,
22419
+ killSignal: "SIGKILL",
22420
+ windowsHide: true,
22421
+ maxBuffer: 256 * 1024 * 1024,
22422
+ env: {
22423
+ ...process.env,
22424
+ GIT_TERMINAL_PROMPT: "0",
22425
+ GIT_OPTIONAL_LOCKS: "0",
22426
+ LC_ALL: "C",
22427
+ LANGUAGE: "C"
22428
+ }
22529
22429
  });
22530
- if (result.status !== 0)
22531
- return null;
22532
- const urls = (result.stdout ?? "").split(/\r?\n/u).map((url) => url.trim()).filter(Boolean);
22533
- return urls.length === 1 ? urls[0] : null;
22430
+ return {
22431
+ ok: run.status === 0,
22432
+ stdout: run.stdout ?? "",
22433
+ stderr: run.error ? String(run.error.message) : run.stderr ?? ""
22434
+ };
22534
22435
  };
22535
-
22536
- // packages/workit-core/src/forge/redact.ts
22537
- var KEY = String.raw`[A-Za-z0-9_.-]*?(?:token|secret|password|passwd|pwd|api[_-]?key|access[_-]?key|account[_-]?key|private[_-]?key|client[_-]?secret|credentials?|authorization|cookie|signature)[A-Za-z0-9_.-]*`;
22538
- var ASSIGNMENT = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*["']?)((?:bearer|basic|token|digest)\s+)?([^\s"',;&]+)`, "giu");
22539
- var QUOTED_ASSIGNMENT = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*)(["'])([^"'\n]*[^\s"'][^"'\n]*)\2`, "giu");
22540
- var ANSI = new RegExp(String.raw`\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007]*\u0007`, "gu");
22541
-
22542
- // packages/workit-core/src/git/ops.ts
22543
- var COMMIT_TIMEOUT_MS = 10 * 60000;
22544
-
22545
- // packages/workit-core/src/core/policy/attributes.ts
22546
- function patternRegex(dir, pattern) {
22547
- if (!pattern || pattern.endsWith("/") || pattern.startsWith('"') || pattern.startsWith("!"))
22436
+ var git4 = (cwd, args) => {
22437
+ const run = gitRun2(cwd, args);
22438
+ const value = run.ok ? run.stdout.trim() : "";
22439
+ return value ? value : null;
22440
+ };
22441
+ var safeRef2 = (value) => value.length > 0 && !value.startsWith("-");
22442
+ function resolveCommit(cwd, ref) {
22443
+ if (!safeRef2(ref))
22548
22444
  return null;
22549
- const anchored = pattern.includes("/");
22550
- const body = pattern.replace(/^\//u, "");
22551
- let source = "";
22552
- for (let index = 0;index < body.length; index += 1) {
22553
- const char = body[index];
22554
- if (char === "*" && body[index + 1] === "*") {
22555
- const slash = body[index + 2] === "/";
22556
- source += slash ? "(?:.*/)?" : ".*";
22557
- index += slash ? 2 : 1;
22558
- } else if (char === "*")
22559
- source += "[^/]*";
22560
- else if (char === "?")
22561
- source += "[^/]";
22562
- else if (char === "[") {
22563
- const end = body.indexOf("]", index + 2);
22564
- if (end < 0)
22565
- return null;
22566
- const set = body.slice(index + 1, end).replace(/^!/u, "^").replace(/\\/gu, "\\\\");
22567
- source += `[${set}]`;
22568
- index = end;
22569
- } else if (char === "\\" && index + 1 < body.length) {
22570
- source += body[index + 1].replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
22571
- index += 1;
22572
- } else
22573
- source += char.replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
22445
+ return git4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]);
22446
+ }
22447
+ function branchHead(cwd, branch) {
22448
+ if (!safeRef2(branch))
22449
+ return null;
22450
+ return resolveCommit(cwd, `refs/heads/${branch}`) ?? resolveCommit(cwd, `refs/remotes/origin/${branch}`);
22451
+ }
22452
+ function diffHash(cwd, base, head) {
22453
+ if (!safeRef2(base) || !safeRef2(head))
22454
+ return null;
22455
+ const diff = gitRun2(cwd, [
22456
+ "diff",
22457
+ "-U3",
22458
+ "--diff-algorithm=myers",
22459
+ "--indent-heuristic",
22460
+ "--no-color",
22461
+ "--no-ext-diff",
22462
+ "--no-textconv",
22463
+ "--no-renames",
22464
+ "--full-index",
22465
+ "--binary",
22466
+ "--src-prefix=a/",
22467
+ "--dst-prefix=b/",
22468
+ `${base}...${head}`,
22469
+ "--"
22470
+ ], GIT_TIMEOUTS.worktree);
22471
+ if (!diff.ok || !diff.stdout)
22472
+ return null;
22473
+ const hash = createHash9("sha256");
22474
+ for (const line of diff.stdout.split(`
22475
+ `)) {
22476
+ if (line.startsWith("index "))
22477
+ continue;
22478
+ hash.update(line.startsWith("@@ ") ? "@@" : line);
22479
+ hash.update(`
22480
+ `);
22481
+ }
22482
+ return `sha256:${hash.digest("hex")}`;
22483
+ }
22484
+ function storeRoot(cwd) {
22485
+ const location = resolveStore(path15.resolve(cwd));
22486
+ if (location instanceof Error)
22487
+ return err("unavailable", location.message.replace(/; fix the repository.*$/u, ""), "fix the repository (git status should work here) or run from a non-git directory");
22488
+ return { ok: true, value: { root: location.dir, shared: location.shared } };
22489
+ }
22490
+ function ledgerPath(cwd) {
22491
+ const root = storeRoot(cwd);
22492
+ return root.ok ? { ok: true, value: path15.join(root.value.root, "ledger", "ledger.jsonl") } : root;
22493
+ }
22494
+ var newId2 = () => `${Date.now().toString(36)}-${randomBytes3(6).toString("hex")}`;
22495
+ var NETWORK_FS_TYPES = new Set([
22496
+ 26985,
22497
+ 20859,
22498
+ 4266872130,
22499
+ 4283649346,
22500
+ 1702057286,
22501
+ 1397113167,
22502
+ 12805120,
22503
+ 16914839,
22504
+ 1196443219,
22505
+ 198183888
22506
+ ]);
22507
+ var isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0);
22508
+ var onNetworkFs = (dir) => {
22509
+ if (process.platform !== "linux")
22510
+ return false;
22511
+ try {
22512
+ return isNetworkFsType(fs12.statfsSync(dir).type);
22513
+ } catch {
22514
+ return false;
22574
22515
  }
22575
- const prefix = dir ? `${dir.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}/` : "";
22516
+ };
22517
+ var LOCK_WAIT_MS = 5000;
22518
+ var LOCK_STALE_MS = 30000;
22519
+ var LOCK_OWNER = "owner";
22520
+ var lockToken = (lock) => {
22576
22521
  try {
22577
- return new RegExp(anchored ? `^${prefix}${source}$` : `^${prefix}(?:.*/)?${source}$`, "u");
22522
+ return fs12.readFileSync(path15.join(lock, LOCK_OWNER), "utf8");
22578
22523
  } catch {
22579
22524
  return null;
22580
22525
  }
22581
- }
22582
- function rulesOf(dir, text) {
22583
- const out = [];
22584
- for (const raw of text.split(`
22585
- `)) {
22586
- const line = raw.trim();
22587
- if (!line || line.startsWith("#") || line.startsWith("[attr]"))
22588
- continue;
22589
- const [pattern, ...attributes] = line.split(/\s+/u);
22590
- let generated = null;
22591
- for (const attribute of attributes) {
22592
- if (attribute === "linguist-generated" || attribute === "linguist-generated=true")
22593
- generated = true;
22594
- else if (attribute === "-linguist-generated" || attribute === "!linguist-generated" || attribute.startsWith("linguist-generated="))
22595
- generated = false;
22596
- }
22597
- if (generated === null)
22598
- continue;
22599
- const regex = patternRegex(dir, pattern);
22600
- if (regex)
22601
- out.push({ regex, generated });
22602
- }
22603
- return out;
22604
- }
22605
- function generatedByAttributes(files, paths) {
22606
- const rules = [...files].map(([file, text]) => {
22607
- const dir = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
22608
- return {
22609
- depth: dir ? dir.split("/").length : 0,
22610
- rules: rulesOf(dir, text)
22611
- };
22612
- }).toSorted((left, right) => left.depth - right.depth).flatMap((entry) => entry.rules);
22613
- const out = new Set;
22614
- for (const file of paths) {
22615
- const last = rules.findLast((rule) => rule.regex.test(file));
22616
- if (last?.generated)
22617
- out.add(file);
22618
- }
22619
- return out;
22620
- }
22621
-
22622
- // packages/workit-core/src/core/policy/substance.ts
22623
- var SLASH_BLOCK = [["/*", "*/"]];
22624
- var C_LIKE = { line: ["//"], block: SLASH_BLOCK, quotes: `"'` };
22625
- var SYNTAXES = Object.fromEntries([
22626
- ...["ts", "mts", "cts", "js", "mjs", "cjs"].map((ext) => [ext, { ...C_LIKE, js: true }]),
22627
- ...["java", "kt", "kts", "scala", "swift", "dart", "groovy"].map((ext) => [
22628
- ext,
22629
- { ...C_LIKE, triple: true }
22630
- ]),
22631
- ["go", { ...C_LIKE, rawBacktick: true }],
22632
- ...["css", "scss", "less"].map((ext) => [
22633
- ext,
22634
- { line: [], block: SLASH_BLOCK, quotes: `"'`, cssUrl: true }
22635
- ]),
22636
- ...["py", "pyi"].map((ext) => [
22637
- ext,
22638
- { line: ["#"], block: [], quotes: `"'`, triple: true, indented: true, fstring: true }
22639
- ]),
22640
- ["toml", { line: ["#"], block: [], quotes: `"`, literal: "'", triple: true }],
22641
- ...["html", "htm", "xml", "svg", "xhtml"].map((ext) => [
22642
- ext,
22643
- { line: [], block: [["<!--", "-->"]], quotes: "", indented: true }
22644
- ])
22645
- ]);
22646
- var DIRECTIVE = /@ts-|eslint|prettier|biome|oxlint|istanbul|\bc8\b|\bv8\b|@jsx|@flow|@vite|webpack|__PURE__|__NO_SIDE_EFFECTS__|source(?:Mapping)?URL|^\/\s*<|go:|\+build|nolint|noqa|type:|pyright|mypy|pylint|pragma|coding[:=]|fmt:|NOSONAR|@license|@preserve|@refresh|environment|language=|cython|ruff|isort|deno-|@vitest|@jest|swiftlint|ktlint|checkstyle|suppress|nullable|#(?:end)?region/iu;
22647
- var extensionOf = (file) => {
22648
- const name = file.slice(file.lastIndexOf("/") + 1);
22649
- const dot = name.lastIndexOf(".");
22650
- return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
22651
22526
  };
22652
- var CI_PATH = /^\.github\/|(?:^|\/)(?:\.gitlab-ci\.ya?ml|Jenkinsfile|azure-pipelines\.ya?ml|\.pre-commit-config\.ya?ml|\.circleci\/.*)$/u;
22653
- var syntaxOf = (file) => CI_PATH.test(file) ? null : SYNTAXES[extensionOf(file)] ?? null;
22654
- var SPACE = /[ \t\r\f\v]/u;
22655
- var WORD = /[\w$]/u;
22656
- var NO_SPACE_AFTER = "([{,;";
22657
- var NO_SPACE_BEFORE = ")]},;";
22658
- var REGEX_AFTER = "(,=:[!&|?{};+-*%<>~^";
22659
- var REGEX_KEYWORDS = new Set([
22660
- "return",
22661
- "typeof",
22662
- "instanceof",
22663
- "case",
22664
- "do",
22665
- "else",
22666
- "in",
22667
- "of",
22668
- "new",
22669
- "delete",
22670
- "void",
22671
- "throw",
22672
- "yield",
22673
- "await"
22674
- ]);
22675
- function substanceOf(file, text) {
22676
- const syntax = syntaxOf(file);
22677
- if (!syntax)
22678
- return plainSubstance(text);
22679
- const markup = syntax.block.some(([open]) => open === "<!--");
22680
- if (markup && (/<(?:script|style)\b|<!\[CDATA\[|<\?/iu.test(text) || /[^>\s][ \t]*<!--/u.test(text)))
22681
- return plainSubstance(text);
22682
- return lex(syntax, text);
22683
- }
22684
- function plainSubstance(text) {
22685
- return text.split(`
22686
- `).map((line) => line.replace(/[ \t\r\f\v]+$/u, "")).filter(Boolean).join(`
22687
- `);
22688
- }
22689
- function lex(syntax, text) {
22690
- const collapse = !syntax.indented;
22691
- let out = "";
22692
- let lineStart = 0;
22693
- let kept = 0;
22694
- let pendingSpace = false;
22695
- const frames = [{ kind: "code", braces: 0 }];
22696
- let index = 0;
22697
- if (text.startsWith("#!")) {
22698
- const end = text.indexOf(`
22699
- `);
22700
- out = end < 0 ? text : text.slice(0, end);
22701
- kept = out.length;
22702
- index = end < 0 ? text.length : end;
22527
+ var lockAgeMs = (lock) => {
22528
+ for (const target of [path15.join(lock, LOCK_OWNER), lock]) {
22529
+ try {
22530
+ return Date.now() - fs12.statSync(target).mtimeMs;
22531
+ } catch {}
22703
22532
  }
22704
- const emitCode = (chunk) => {
22705
- if (collapse && pendingSpace && out.length > lineStart) {
22706
- const before = out[out.length - 1];
22707
- if (!NO_SPACE_AFTER.includes(before) && !NO_SPACE_BEFORE.includes(chunk[0]))
22708
- out += " ";
22709
- }
22710
- pendingSpace = false;
22711
- out += chunk;
22712
- };
22713
- const emitVerbatim = (chunk) => {
22714
- emitCode(chunk);
22715
- kept = out.length;
22716
- };
22717
- const newline = () => {
22718
- if (!collapse) {
22719
- let end = out.length;
22720
- while (end > Math.max(lineStart, kept) && SPACE.test(out[end - 1]))
22721
- end -= 1;
22722
- out = out.slice(0, end);
22723
- }
22724
- pendingSpace = false;
22725
- if (out.length > lineStart) {
22726
- out += `
22727
- `;
22728
- lineStart = out.length;
22533
+ return null;
22534
+ };
22535
+ var ledgerLock = {
22536
+ acquire(lock) {
22537
+ try {
22538
+ fs12.mkdirSync(lock);
22539
+ } catch (error) {
22540
+ if (error.code === "EEXIST")
22541
+ return null;
22542
+ throw error;
22729
22543
  }
22730
- };
22731
- const regexAllowed = () => {
22732
- let end = out.length;
22733
- while (end > 0 && /\s/u.test(out[end - 1]))
22734
- end -= 1;
22735
- if (end === 0 || REGEX_AFTER.includes(out[end - 1]))
22736
- return true;
22737
- let start = end;
22738
- while (start > 0 && WORD.test(out[start - 1]))
22739
- start -= 1;
22740
- return REGEX_KEYWORDS.has(out.slice(start, end));
22741
- };
22742
- const scanString = (quote, escapes, multiline) => {
22743
- const prefix = text.slice(Math.max(0, index - 2), index).match(/[a-z]*$/iu)?.[0] ?? "";
22744
- const fstring = syntax.fstring === true && /f/iu.test(prefix);
22745
- let end = index + quote.length;
22746
- while (end < text.length) {
22747
- const char = text[end];
22748
- if (escapes && char === "\\") {
22749
- end += 2;
22750
- continue;
22751
- }
22752
- if (text.startsWith(quote, end)) {
22753
- if (fstring && unbalanced(text.slice(index + quote.length, end)))
22754
- return false;
22755
- emitVerbatim(text.slice(index, end + quote.length));
22756
- index = end + quote.length;
22757
- return true;
22758
- }
22759
- if (char === `
22760
- ` && !multiline)
22761
- return false;
22762
- end += 1;
22544
+ const token = `${process.pid}-${randomBytes3(8).toString("hex")}`;
22545
+ fs12.writeFileSync(path15.join(lock, LOCK_OWNER), token);
22546
+ return token;
22547
+ },
22548
+ release(lock, token) {
22549
+ if (lockToken(lock) !== token)
22550
+ return false;
22551
+ const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
22552
+ try {
22553
+ fs12.renameSync(lock, tomb);
22554
+ } catch {
22555
+ return false;
22763
22556
  }
22764
- return false;
22765
- };
22766
- while (index < text.length) {
22767
- const frame = frames[frames.length - 1];
22768
- const char = text[index];
22769
- if (frame.kind === "template") {
22770
- if (char === "\\") {
22771
- emitVerbatim(text.slice(index, index + 2));
22772
- index += 2;
22773
- } else if (char === "`") {
22774
- emitVerbatim(char);
22775
- frames.pop();
22776
- index += 1;
22777
- } else if (text.startsWith("${", index)) {
22778
- emitVerbatim("${");
22779
- frames.push({ kind: "code", braces: 0 });
22780
- index += 2;
22781
- } else {
22782
- out += char;
22783
- kept = out.length;
22784
- index += 1;
22785
- }
22786
- continue;
22557
+ if (lockToken(tomb) !== token) {
22558
+ try {
22559
+ fs12.renameSync(tomb, lock);
22560
+ } catch {}
22561
+ return false;
22787
22562
  }
22788
- if (char === `
22789
- `) {
22790
- newline();
22791
- index += 1;
22792
- continue;
22563
+ fs12.rmSync(tomb, { recursive: true, force: true });
22564
+ return true;
22565
+ },
22566
+ token: lockToken,
22567
+ reap(lock, observed) {
22568
+ const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
22569
+ try {
22570
+ fs12.renameSync(lock, tomb);
22571
+ } catch {
22572
+ return false;
22793
22573
  }
22794
- if (SPACE.test(char)) {
22795
- if (collapse)
22796
- pendingSpace = true;
22797
- else
22798
- out += char;
22799
- index += 1;
22800
- continue;
22574
+ if (lockToken(tomb) === observed) {
22575
+ fs12.rmSync(tomb, { recursive: true, force: true });
22576
+ return true;
22801
22577
  }
22802
- const lineOpener = syntax.line.find((opener) => text.startsWith(opener, index));
22803
- if (lineOpener) {
22804
- const end = text.indexOf(`
22805
- `, index);
22806
- const comment = text.slice(index, end < 0 ? text.length : end);
22807
- if (DIRECTIVE.test(comment.slice(lineOpener.length)))
22808
- emitVerbatim(comment);
22809
- else
22810
- pendingSpace = true;
22811
- index = end < 0 ? text.length : end;
22812
- continue;
22578
+ try {
22579
+ fs12.renameSync(tomb, lock);
22580
+ } catch {
22581
+ fs12.rmSync(tomb, { recursive: true, force: true });
22813
22582
  }
22814
- const block = syntax.block.find(([open]) => text.startsWith(open, index));
22815
- if (block) {
22816
- const end = text.indexOf(block[1], index + block[0].length);
22817
- if (end < 0)
22818
- return null;
22819
- const comment = text.slice(index, end + block[1].length);
22820
- if (DIRECTIVE.test(comment.slice(block[0].length, -block[1].length)))
22821
- emitVerbatim(comment);
22822
- else
22823
- pendingSpace = true;
22824
- if (comment.includes(`
22825
- `))
22826
- newline();
22827
- index = end + block[1].length;
22583
+ return false;
22584
+ }
22585
+ };
22586
+ function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
22587
+ const lock = `${file}.lock`;
22588
+ const deadline = Date.now() + waitMs;
22589
+ let token = null;
22590
+ for (;; ) {
22591
+ token = ledgerLock.acquire(lock);
22592
+ if (token)
22593
+ break;
22594
+ const observed = lockToken(lock);
22595
+ const age = lockAgeMs(lock);
22596
+ if (age !== null && age > LOCK_STALE_MS && ledgerLock.reap(lock, observed))
22828
22597
  continue;
22829
- }
22830
- if (syntax.triple && (char === '"' || char === "'") && text.startsWith(char.repeat(3), index)) {
22831
- if (!scanString(char.repeat(3), !syntax.literal?.includes(char), true))
22598
+ if (Date.now() >= deadline)
22599
+ return null;
22600
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
22601
+ }
22602
+ try {
22603
+ return fn();
22604
+ } finally {
22605
+ ledgerLock.release(lock, token);
22606
+ }
22607
+ }
22608
+ var sha2562 = (text) => createHash9("sha256").update(text).digest("hex");
22609
+ var chainHash = (prevHash, body) => sha2562(`${prevHash ?? ""}
22610
+ ${body}`);
22611
+ var TAIL_BYTES = 64 * 1024;
22612
+ var lastLine = (fd, size) => {
22613
+ for (let span = Math.min(size, TAIL_BYTES);span > 0; span = Math.min(size, span * 2)) {
22614
+ const buffer = Buffer.alloc(span);
22615
+ fs12.readSync(fd, buffer, 0, span, size - span);
22616
+ const end = buffer.lastIndexOf(10);
22617
+ if (end < 0) {
22618
+ if (span === size)
22832
22619
  return null;
22833
22620
  continue;
22834
22621
  }
22835
- if (syntax.quotes.includes(char)) {
22836
- if (!scanString(char, true, Boolean(syntax.multiline)))
22837
- return null;
22838
- continue;
22622
+ let stop = end;
22623
+ let text = "";
22624
+ while (stop >= 0) {
22625
+ const start = buffer.lastIndexOf(10, stop - 1);
22626
+ text = buffer.toString("utf8", start + 1, stop).trim();
22627
+ if (text) {
22628
+ if (start >= 0 || span === size)
22629
+ return text;
22630
+ break;
22631
+ }
22632
+ if (start < 0) {
22633
+ if (span === size)
22634
+ return null;
22635
+ break;
22636
+ }
22637
+ stop = start;
22839
22638
  }
22840
- if (syntax.literal?.includes(char)) {
22841
- if (!scanString(char, false, false))
22842
- return null;
22843
- continue;
22639
+ if (span === size)
22640
+ return text || null;
22641
+ }
22642
+ return null;
22643
+ };
22644
+ var tighten = (fd) => {
22645
+ try {
22646
+ if ((fs12.fstatSync(fd).mode & 63) !== 0)
22647
+ fs12.fchmodSync(fd, 384);
22648
+ } catch {}
22649
+ };
22650
+ function appendLine(file, build, options) {
22651
+ const write = () => {
22652
+ const fd = fs12.openSync(file, "a+", 384);
22653
+ try {
22654
+ if (process.platform !== "win32")
22655
+ tighten(fd);
22656
+ const size = fs12.fstatSync(fd).size;
22657
+ let fence = "";
22658
+ if (size > 0) {
22659
+ const last = Buffer.alloc(1);
22660
+ fs12.readSync(fd, last, 0, 1, size - 1);
22661
+ if (last[0] !== 10)
22662
+ fence = `
22663
+ `;
22664
+ }
22665
+ const line = build(lastLine(fd, size));
22666
+ if (!line.ok)
22667
+ return line;
22668
+ const bytes = Buffer.from(fence + line.value);
22669
+ const written = fs12.writeSync(fd, bytes);
22670
+ if (written !== bytes.length)
22671
+ return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
22672
+ return { ok: true, value: undefined };
22673
+ } finally {
22674
+ fs12.closeSync(fd);
22844
22675
  }
22845
- if (syntax.rawBacktick && char === "`") {
22846
- if (!scanString("`", false, true))
22847
- return null;
22676
+ };
22677
+ try {
22678
+ fs12.mkdirSync(path15.dirname(file), { recursive: true });
22679
+ if (!options.forceLock && !onNetworkFs(path15.dirname(file)))
22680
+ return write();
22681
+ const locked = withLedgerLock(file, write);
22682
+ return locked ?? err("busy", `ledger lock ${file}.lock is held`, "retry; remove the .lock directory if no workit process is running");
22683
+ } catch (error) {
22684
+ return err("unavailable", `cannot append to ${file}: ${error.message}`);
22685
+ }
22686
+ }
22687
+ var sealed = (row, id, at, previous) => {
22688
+ const { prevHash: _prev, rowHash: _row, ...fields } = row;
22689
+ const prevHash = previous === null ? null : sha2562(previous);
22690
+ const body = { v: LEDGER_VERSION, id, at, ...fields, prevHash };
22691
+ const full = { ...body, rowHash: chainHash(prevHash, JSON.stringify(body)) };
22692
+ return { full, line: `${JSON.stringify(full)}
22693
+ ` };
22694
+ };
22695
+ function appendRaw(cwd, row, options) {
22696
+ const file = ledgerPath(cwd);
22697
+ if (!file.ok)
22698
+ return file;
22699
+ const id = newId2();
22700
+ const at = (options.now ?? new Date).toISOString();
22701
+ let full = null;
22702
+ const written = appendLine(file.value, (previous) => {
22703
+ const next = sealed(row, id, at, previous);
22704
+ const bytes = Buffer.byteLength(next.line);
22705
+ if (bytes > MAX_LINE_BYTES)
22706
+ return err("invalid_input", `ledger row is ${bytes} bytes; the limit is ${MAX_LINE_BYTES}. Shorten the text and pass details by --ref <path|url>`);
22707
+ full = next.full;
22708
+ return { ok: true, value: next.line };
22709
+ }, options);
22710
+ return written.ok ? { ok: true, value: full } : written;
22711
+ }
22712
+ function observedRowBytes(row) {
22713
+ const sample = sealed({ ...row, observer: "workit_cli" }, newId2(), new Date().toISOString(), "");
22714
+ return Buffer.byteLength(sample.line);
22715
+ }
22716
+ function appendObserved(cwd, row, options = {}) {
22717
+ return appendRaw(cwd, { ...row, observer: "workit_cli" }, options);
22718
+ }
22719
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
22720
+ var str2 = (value) => typeof value === "string" ? value : null;
22721
+ var normalizeActor = (value) => {
22722
+ const actor = isRecord3(value) ? value : {};
22723
+ return {
22724
+ host: str2(actor.host) ?? "unknown",
22725
+ session: str2(actor.session),
22726
+ agentId: str2(actor.agentId)
22727
+ };
22728
+ };
22729
+ function supersedeAllowed(by, target, onBranch = (row) => (by.branch ?? null) === (row.branch ?? null)) {
22730
+ if (by.type !== target.type)
22731
+ return `a ${by.type} row cannot supersede a ${target.type} row`;
22732
+ if (!by.actor.session || by.actor.session !== target.actor.session)
22733
+ return "only the session that wrote a row can supersede it";
22734
+ if (!onBranch(target))
22735
+ return `the row is on another branch (${String(target.branch ?? "none")}); a row supersedes only one on its own branch`;
22736
+ if (by.type === "verdict" && (by.kind ?? "review") !== (target.kind ?? "review"))
22737
+ return `a ${String(by.kind ?? "review")} verdict cannot supersede a ${String(target.kind ?? "review")} verdict; record it with --kind ${String(target.kind ?? "review")}`;
22738
+ if (by.type === "verdict" && by.self === true && target.self !== true)
22739
+ return "a self verdict cannot replace an independent verdict";
22740
+ return null;
22741
+ }
22742
+ function readLedger(cwd, options = {}) {
22743
+ const file = ledgerPath(cwd);
22744
+ if (!file.ok)
22745
+ return file;
22746
+ let text = "";
22747
+ let stat = null;
22748
+ try {
22749
+ stat = fs12.statSync(file.value);
22750
+ text = fs12.readFileSync(file.value, "utf8");
22751
+ } catch (error) {
22752
+ if (error.code === "ENOENT")
22753
+ return {
22754
+ ok: true,
22755
+ value: {
22756
+ path: file.value,
22757
+ rows: [],
22758
+ skipped: 0,
22759
+ integrity: options.integrity ? { chained: 0, legacy: 0, unverified: [] } : null
22760
+ }
22761
+ };
22762
+ return err("unavailable", `cannot read ${file.value}: ${error.message}`);
22763
+ }
22764
+ const rows = [];
22765
+ let skipped = 0;
22766
+ const integrity = { chained: 0, legacy: 0, unverified: [] };
22767
+ const earlier = new Set([null]);
22768
+ let chainStarted = false;
22769
+ const ids = new Set;
22770
+ for (const raw of text.split(`
22771
+ `)) {
22772
+ const trimmed = raw.trim();
22773
+ if (!trimmed)
22848
22774
  continue;
22849
- }
22850
- if (syntax.js && char === "`") {
22851
- emitVerbatim(char);
22852
- frames.push({ kind: "template" });
22853
- index += 1;
22775
+ const linked = (prevHash) => earlier.has(prevHash);
22776
+ if (options.integrity)
22777
+ earlier.add(sha2562(trimmed));
22778
+ let parsed;
22779
+ try {
22780
+ parsed = JSON.parse(trimmed);
22781
+ } catch {
22782
+ skipped += 1;
22854
22783
  continue;
22855
22784
  }
22856
- if (syntax.cssUrl && /^url\(\s*[^"')\s]/iu.test(text.slice(index, index + 64)))
22857
- return null;
22858
- if (syntax.js && char === "/" && !regexAllowed() && out.trimEnd().endsWith(")")) {
22859
- const lineEnd = text.indexOf(`
22860
- `, index);
22861
- if (text.slice(index + 1, lineEnd < 0 ? text.length : lineEnd).includes("/"))
22862
- return null;
22863
- }
22864
- if (syntax.js && char === "/" && regexAllowed()) {
22865
- let end = index + 1;
22866
- let inClass = false;
22867
- while (end < text.length && (inClass || text[end] !== "/")) {
22868
- if (text[end] === `
22869
- `)
22870
- return null;
22871
- if (text[end] === "\\")
22872
- end += 1;
22873
- else if (text[end] === "[")
22874
- inClass = true;
22875
- else if (text[end] === "]")
22876
- inClass = false;
22877
- end += 1;
22878
- }
22879
- if (end >= text.length)
22880
- return null;
22881
- emitVerbatim(text.slice(index, end + 1));
22882
- index = end + 1;
22785
+ if (!isRecord3(parsed) || !str2(parsed.id) || !str2(parsed.type) || !str2(parsed.at)) {
22786
+ skipped += 1;
22883
22787
  continue;
22884
22788
  }
22885
- if (frames.length > 1 && frame.kind === "code") {
22886
- if (char === "{")
22887
- frame.braces += 1;
22888
- else if (char === "}") {
22889
- if (frame.braces === 0) {
22890
- emitVerbatim(char);
22891
- frames.pop();
22892
- index += 1;
22893
- continue;
22894
- }
22895
- frame.braces -= 1;
22896
- }
22897
- }
22898
- emitCode(char);
22899
- index += 1;
22789
+ let rowIntegrity = options.integrity ? "verified" : undefined;
22790
+ let reason = null;
22791
+ if (!options.integrity) {} else if (typeof parsed.rowHash === "string") {
22792
+ chainStarted = true;
22793
+ const { rowHash, ...body } = parsed;
22794
+ const prevHash = str2(parsed.prevHash);
22795
+ if (chainHash(prevHash, JSON.stringify(body)) !== rowHash)
22796
+ reason = "hash_mismatch";
22797
+ else if (!linked(prevHash))
22798
+ reason = "broken_link";
22799
+ } else if (chainStarted)
22800
+ reason = "unsigned";
22801
+ else
22802
+ rowIntegrity = "legacy";
22803
+ if (options.integrity && !reason && ids.has(parsed.id))
22804
+ reason = "duplicate_id";
22805
+ if (options.integrity)
22806
+ ids.add(parsed.id);
22807
+ if (reason)
22808
+ rowIntegrity = "unverified";
22809
+ const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
22810
+ const row = {
22811
+ ...parsed,
22812
+ v: typeof parsed.v === "number" ? parsed.v : LEDGER_VERSION,
22813
+ id: parsed.id,
22814
+ at: parsed.at,
22815
+ type: parsed.type,
22816
+ actor: normalizeActor(parsed.actor),
22817
+ branch: str2(parsed.branch),
22818
+ head: str2(parsed.head),
22819
+ base: str2(parsed.base),
22820
+ baseSha: str2(parsed.baseSha),
22821
+ patchId: str2(parsed.patchId),
22822
+ diffHash: str2(parsed.diffHash),
22823
+ tree: str2(parsed.tree),
22824
+ dirty: typeof parsed.dirty === "boolean" ? parsed.dirty : null,
22825
+ ...pr === undefined ? {} : { pr },
22826
+ ...str2(parsed.supersedes) ? { supersedes: parsed.supersedes } : {},
22827
+ seq: rows.length + 1,
22828
+ superseded: false,
22829
+ supersedeIgnored: false,
22830
+ ...rowIntegrity ? { integrity: rowIntegrity } : {},
22831
+ ...reason ? { integrityReason: reason } : {}
22832
+ };
22833
+ rows.push(row);
22834
+ if (!options.integrity)
22835
+ continue;
22836
+ if (rowIntegrity === "verified")
22837
+ integrity.chained += 1;
22838
+ else if (rowIntegrity === "legacy")
22839
+ integrity.legacy += 1;
22840
+ else if (reason)
22841
+ integrity.unverified.push({
22842
+ seq: row.seq,
22843
+ id: row.id,
22844
+ type: row.type,
22845
+ branch: row.branch,
22846
+ reason
22847
+ });
22900
22848
  }
22901
- if (frames.length !== 1)
22902
- return null;
22903
- newline();
22904
- return out;
22905
- }
22906
- function unbalanced(body) {
22907
- let depth = 0;
22908
- for (const part of body.replaceAll("{{", "").replaceAll("}}", "")) {
22909
- if (part === "{")
22910
- depth += 1;
22911
- else if (part === "}")
22912
- depth = Math.max(0, depth - 1);
22849
+ const scopes = new Map;
22850
+ const scopeOf = (branch) => {
22851
+ let scope = scopes.get(branch);
22852
+ if (!scope)
22853
+ scopes.set(branch, scope = branchScope(cwd, branch));
22854
+ return scope;
22855
+ };
22856
+ const byId = new Map;
22857
+ for (const row of rows) {
22858
+ const target = row.supersedes ? byId.get(row.supersedes) : undefined;
22859
+ if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
22860
+ target.superseded = true;
22861
+ else if (row.supersedes)
22862
+ row.supersedeIgnored = true;
22863
+ if (!byId.has(row.id))
22864
+ byId.set(row.id, row);
22913
22865
  }
22914
- return depth > 0;
22915
- }
22916
- function hasOpaqueLineBreak(text) {
22917
- return /\r(?!\n)|[\u2028\u2029]/u.test(text);
22866
+ if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
22867
+ tamperCache.set(file.value, {
22868
+ size: stat.size,
22869
+ stamp: fileStamp(stat),
22870
+ breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
22871
+ });
22872
+ return {
22873
+ ok: true,
22874
+ value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
22875
+ };
22918
22876
  }
22919
- var decoder = new TextDecoder("utf-8", { fatal: true });
22920
- function textOf(bytes) {
22921
- if (bytes.includes(0))
22922
- return null;
22877
+ var TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
22878
+ var tamperCache = new Map;
22879
+ var fileStamp = (stat) => `${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`;
22880
+ function ledgerTampering(cwd) {
22881
+ const file = ledgerPath(cwd);
22882
+ if (!file.ok)
22883
+ return [];
22923
22884
  try {
22924
- return decoder.decode(bytes);
22885
+ const stat = fs12.statSync(file.value);
22886
+ const cached = tamperCache.get(file.value);
22887
+ if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
22888
+ return cached.breaks;
22925
22889
  } catch {
22926
- return null;
22890
+ return [];
22927
22891
  }
22892
+ const read = readLedger(cwd, { integrity: true });
22893
+ return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
22928
22894
  }
22929
- function cosmeticChange(file, before, after) {
22930
- const left = textOf(before);
22931
- const right = textOf(after);
22932
- if (left === null || right === null)
22933
- return false;
22934
- if (hasOpaqueLineBreak(left) || hasOpaqueLineBreak(right))
22935
- return false;
22936
- const a = substanceOf(file, left);
22937
- return a !== null && a === substanceOf(file, right);
22895
+ function tamperedUnblock(cwd) {
22896
+ const file = ledgerPath(cwd);
22897
+ const where = file.ok ? file.value : "the ledger file";
22898
+ return `workit ledger verify-integrity # lists the edited or missing rows; if the edit is understood, move ${where} aside (mv ${where} ${where}.tampered-<date>) and record the verdicts again`;
22938
22899
  }
22939
-
22940
- // packages/workit-core/src/core/policy/lane.ts
22941
- var LANE_THRESHOLDS = {
22942
- selfMaxLines: 100,
22943
- selfMaxFiles: 5,
22944
- fullMinLines: 500
22945
- };
22946
- var RISK_TOKENS = new Set([
22947
- "auth",
22948
- "authn",
22949
- "authz",
22950
- "oauth",
22951
- "security",
22952
- "permission",
22953
- "permissions",
22954
- "secret",
22955
- "secrets",
22956
- "crypto",
22957
- "gate",
22958
- "gates",
22959
- "policy",
22960
- "policies",
22961
- "hook",
22962
- "hooks",
22963
- "husky",
22964
- "lefthook",
22965
- "release",
22966
- "releases",
22967
- "migration",
22968
- "migrations",
22969
- "migrate",
22970
- "workflows"
22971
- ]);
22972
- var RISK_FILES = new Set([
22973
- ".gitlab-ci.yml",
22974
- "Jenkinsfile",
22975
- "azure-pipelines.yml",
22976
- ".pre-commit-config.yaml",
22977
- ".workit/risk-paths"
22978
- ]);
22979
- var RISK_NAMES = new Set([".gitattributes"]);
22980
- var RISK_PATHS_FILE = ".workit/risk-paths";
22981
- var LOCKFILES = new Set([
22982
- "bun.lock",
22983
- "bun.lockb",
22984
- "package-lock.json",
22985
- "npm-shrinkwrap.json",
22986
- "yarn.lock",
22987
- "pnpm-lock.yaml",
22988
- "Cargo.lock",
22989
- "go.sum",
22990
- "poetry.lock",
22991
- "uv.lock",
22992
- "Pipfile.lock",
22993
- "Gemfile.lock",
22994
- "composer.lock"
22995
- ]);
22996
- var DOC_EXT = /\.(?:md|mdx|markdown|rst|adoc|txt)$/iu;
22997
- var DOC_NAMES = /^(?:license|licence|notice|authors|changelog|changes|contributors)$/iu;
22998
- var INSTRUCTIONS = /(?:^|\/)(?:AGENTS|CLAUDE|GEMINI|SKILL)\.md$|(?:^|\/)(?:skills|agents|commands|rules|prompts)\/[^/]+/iu;
22999
- var TEST_PATH = /(?:^|\/)(?:test|tests|__tests__|spec|specs|fixtures)\/|\.(?:test|spec)\.[^/]+$/iu;
23000
- var basename = (file) => file.slice(file.lastIndexOf("/") + 1);
23001
- var isDoc = (file) => !INSTRUCTIONS.test(file) && (DOC_EXT.test(file) || DOC_NAMES.test(basename(file)));
23002
- function kindOf(file) {
23003
- if (file.binary)
23004
- return "behavior";
23005
- if (file.generated)
23006
- return "generated";
23007
- if (LOCKFILES.has(basename(file.path)))
23008
- return "lockfile";
23009
- if (isDoc(file.path))
23010
- return "doc";
23011
- if (file.cosmetic)
23012
- return "cosmetic";
23013
- return TEST_PATH.test(file.path) ? "test" : "behavior";
22900
+ var tamperedWarning = (cwd, breaks) => `the ledger was edited, or a row was removed or moved (${breaks.slice(0, 3).map((row) => `${row.reason} at row ${row.seq}`).join(", ")}${breaks.length > 3 ? ", …" : ""}): no verdict is accepted until it is repaired. ${tamperedUnblock(cwd)}`;
22901
+ function verdictBasis(verdict, current) {
22902
+ if (verdict.head && verdict.head === current.head)
22903
+ return "fresh";
22904
+ if (verdict.patchId && verdict.patchId === current.patchId && verdict.diffHash && verdict.diffHash === current.diffHash)
22905
+ return "carried";
22906
+ return "stale";
23014
22907
  }
23015
- var globCache = new Map;
23016
- function globMatch(pattern, file) {
23017
- let regex = globCache.get(pattern);
23018
- if (!regex) {
23019
- let source = "";
23020
- for (let index = 0;index < pattern.length; index += 1) {
23021
- const char = pattern[index];
23022
- if (char === "*" && pattern[index + 1] === "*") {
23023
- source += pattern[index + 2] === "/" ? "(?:[^/]*/)*" : ".*";
23024
- index += pattern[index + 2] === "/" ? 2 : 1;
23025
- } else if (char === "*")
23026
- source += "[^/]*";
23027
- else if (char === "?")
23028
- source += "[^/]";
23029
- else
23030
- source += char.replace(/[.+^${}()|[\]\\]/gu, "\\$&");
22908
+ var isLiveVerdict = (row, retired) => row.type === "verdict" && typeof row.result === "string" && !retired.has(row.id);
22909
+ function trailerSessions(cwd, base, head) {
22910
+ if (!base || !head || !safeRef2(base) || !safeRef2(head))
22911
+ return [];
22912
+ const out = git4(cwd, [
22913
+ "log",
22914
+ "--format=%(trailers:key=Workit-Session,valueonly,separator=%x00)",
22915
+ `${base}..${head}`
22916
+ ]);
22917
+ return (out ?? "").split(/[\0\n]/u).map((value) => value.trim()).filter(Boolean);
22918
+ }
22919
+ var RENAMED = /^branch: renamed refs\/heads\/(.+) to refs\/heads\/(.+)$/iu;
22920
+ function branchScope(cwd, branch) {
22921
+ const renamedAt = new Map;
22922
+ let since = Number.NEGATIVE_INFINITY;
22923
+ if (safeRef2(branch)) {
22924
+ const log = git4(cwd, [
22925
+ "log",
22926
+ "-g",
22927
+ "--date=unix",
22928
+ "--format=%gd%x09%gs",
22929
+ `refs/heads/${branch}`,
22930
+ "--"
22931
+ ]);
22932
+ for (const line of (log ?? "").split(`
22933
+ `)) {
22934
+ const [selector = "", subject = ""] = line.split("\t");
22935
+ const seconds = Number(/@\{(\d+)\}$/u.exec(selector)?.[1]);
22936
+ if (!Number.isFinite(seconds))
22937
+ continue;
22938
+ since = seconds * 1000;
22939
+ const match = RENAMED.exec(subject);
22940
+ if (!match || match[1] === branch)
22941
+ continue;
22942
+ const until = (seconds + 1) * 1000;
22943
+ renamedAt.set(match[1], Math.max(renamedAt.get(match[1]) ?? 0, until));
23031
22944
  }
23032
- regex = new RegExp(`^${source}$`, "u");
23033
- globCache.set(pattern, regex);
23034
22945
  }
23035
- return regex.test(file) || file.startsWith(`${pattern.replace(/\/$/u, "")}/`);
22946
+ return (row) => {
22947
+ if (row.branch === branch)
22948
+ return true;
22949
+ const until = row.branch === null ? undefined : renamedAt.get(row.branch);
22950
+ if (until === undefined)
22951
+ return false;
22952
+ const at = Date.parse(row.at);
22953
+ return at >= since && at <= until;
22954
+ };
23036
22955
  }
23037
- function isRiskPath(file, extra = []) {
23038
- if (extra.some((pattern) => globMatch(pattern, file)))
23039
- return true;
23040
- if (RISK_FILES.has(file) || RISK_NAMES.has(basename(file)))
22956
+ var renameAwareBranch = (scopeOf, branch) => {
22957
+ return (target) => {
22958
+ const own = typeof branch === "string" ? branch : null;
22959
+ const theirs = typeof target.branch === "string" ? target.branch : null;
22960
+ if (own === theirs)
22961
+ return true;
22962
+ if (own === null || theirs === null || typeof target.at !== "string")
22963
+ return false;
22964
+ return scopeOf(own)({ branch: theirs, at: target.at });
22965
+ };
22966
+ };
22967
+ function authorSessions(cwd, rows, branch, range = { base: null, head: null }, scope = branchScope(cwd, branch)) {
22968
+ const sessions = new Set;
22969
+ for (const row of rows) {
22970
+ if (!scope(row) || row.type !== "commit.recorded" && row.type !== "task.opened")
22971
+ continue;
22972
+ const session = str2(row.session) ?? row.actor.session;
22973
+ if (session)
22974
+ sessions.add(session);
22975
+ }
22976
+ for (const session of trailerSessions(cwd, range.base, range.head))
22977
+ sessions.add(session);
22978
+ return sessions;
22979
+ }
22980
+ var derivesFrom = (derived, bare) => !bare.includes(":") && derived.endsWith(`:${bare}`);
22981
+ var authoredBy = (authors, session) => {
22982
+ if (authors.has(session))
23041
22983
  return true;
23042
- if (TEST_PATH.test(file) || isDoc(file))
23043
- return false;
23044
- return file.split("/").map((segment, index, all) => index === all.length - 1 ? segment.replace(/\.[^.]+$/u, "") : segment).some((segment) => segment.toLowerCase().split(/[-_.]/u).some((token) => RISK_TOKENS.has(token)));
22984
+ for (const author of authors)
22985
+ if (derivesFrom(author, session) || derivesFrom(session, author))
22986
+ return true;
22987
+ return false;
22988
+ };
22989
+ var TRUST_REASONS = new Set([
22990
+ "unsigned",
22991
+ "hash_mismatch",
22992
+ "broken_link",
22993
+ "duplicate_id",
22994
+ "unissued_session",
22995
+ "unbound_session"
22996
+ ]);
22997
+ var SHELL_VOUCHED_HOSTS = new Set([
22998
+ "cli",
22999
+ "opencode",
23000
+ "pi",
23001
+ "codex_cli",
23002
+ "codex_desktop"
23003
+ ]);
23004
+ function sessionTrust(cwd, row) {
23005
+ const session = row.actor.session;
23006
+ if (!session || row.self === true)
23007
+ return null;
23008
+ const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
23009
+ const host = row.actor.host;
23010
+ const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
23011
+ if (!issuedOnly)
23012
+ return null;
23013
+ const binding = issuedBinding(cwd, session);
23014
+ if (binding === null)
23015
+ return "unissued_session";
23016
+ return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
23045
23017
  }
23046
- var parseRiskPaths = (text) => text.split(`
23047
- `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.replace(/^\//u, ""));
23048
- function computeLane(input) {
23049
- const riskFiles = input.files.filter((file) => isRiskPath(file.path, input.riskPaths)).map((file) => file.path);
23050
- const risky = new Set(riskFiles);
23051
- const kinds = input.files.map((file) => ({
23052
- file,
23053
- kind: risky.has(file.path) ? "behavior" : kindOf(file)
23054
- }));
23055
- const behavior = kinds.filter((entry) => entry.kind === "behavior" || entry.kind === "test" || entry.kind === "lockfile");
23056
- const lines = behavior.filter((entry) => entry.kind !== "lockfile").reduce((sum, entry) => sum + entry.file.added + entry.file.deleted, 0);
23057
- const opaque = behavior.filter((entry) => entry.kind === "lockfile" || entry.file.binary);
23058
- const result = (lane, reasons) => ({
23059
- lane,
23060
- reasons,
23061
- lines,
23062
- files: behavior.length,
23063
- riskFiles
23018
+ function trustReasons(cwd, row) {
23019
+ if (row.integrity === "unverified")
23020
+ return [row.integrityReason ?? "unsigned"];
23021
+ if (row.integrity !== "verified")
23022
+ return ["unsigned"];
23023
+ const session = sessionTrust(cwd, row);
23024
+ return session ? [session] : [];
23025
+ }
23026
+ function withIntegrity(cwd, rows) {
23027
+ if (rows.every((row) => row.integrity !== undefined))
23028
+ return rows;
23029
+ const read = readLedger(cwd, { integrity: true });
23030
+ const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
23031
+ return rows.map((row) => {
23032
+ if (row.integrity !== undefined)
23033
+ return row;
23034
+ const fresh = bySeq.get(row.seq);
23035
+ return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
23064
23036
  });
23065
- const { selfMaxLines, selfMaxFiles, fullMinLines } = LANE_THRESHOLDS;
23066
- if (input.riskTier === "high")
23067
- return result("full", ["the task judgment records high risk"]);
23068
- if (riskFiles.length)
23069
- return result("full", [
23070
- `touches a risk path: ${riskFiles.slice(0, 3).join(", ")}${riskFiles.length > 3 ? ", …" : ""}`
23071
- ]);
23072
- if (!behavior.length)
23073
- return result("none", [
23074
- input.files.length ? "only docs, comments, formatting or generated copies changed" : "no changes"
23075
- ]);
23076
- if (lines >= fullMinLines)
23077
- return result("full", [`large diff: ${lines} lines (≥ ${fullMinLines})`]);
23078
- if (lines > selfMaxLines)
23079
- return result("focused", [`${lines} lines changed (> ${selfMaxLines})`]);
23080
- if (behavior.length > selfMaxFiles)
23081
- return result("focused", [`${behavior.length} behavior files changed (> ${selfMaxFiles})`]);
23082
- if (opaque.length)
23083
- return result("focused", [
23084
- `a lockfile or binary changed: ${opaque.slice(0, 3).map((entry) => entry.file.path).join(", ")}${opaque.length > 3 ? ", …" : ""}`
23085
- ]);
23086
- return result("self", [
23087
- `small change: ${lines} line${lines === 1 ? "" : "s"} in ${behavior.length} file${behavior.length === 1 ? "" : "s"}, no risk path`
23088
- ]);
23089
23037
  }
23090
- function untrackedStat(cwd, file) {
23091
- try {
23092
- const bytes = readFileSync4(path16.join(cwd, file));
23093
- if (bytes.includes(0))
23094
- return { path: file, added: 0, deleted: 0, binary: true };
23095
- const text = bytes.toString("utf8");
23096
- if (hasOpaqueLineBreak(text))
23097
- return { path: file, added: 0, deleted: 0, binary: true };
23098
- const lines = text.split(`
23099
- `).length - (text.endsWith(`
23100
- `) ? 1 : 0);
23101
- return { path: file, added: lines, deleted: 0 };
23102
- } catch {
23103
- return { path: file, added: 0, deleted: 0, binary: true };
23038
+ var untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason));
23039
+ var independenceReasons = (row, authors) => {
23040
+ const reasons = [];
23041
+ if (!row.actor.session)
23042
+ reasons.push("no_session");
23043
+ else if (authoredBy(authors, row.actor.session))
23044
+ reasons.push("author_session");
23045
+ if (row.self === true && !reasons.includes("no_session"))
23046
+ reasons.push("self");
23047
+ return reasons;
23048
+ };
23049
+ function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
23050
+ let effective = null;
23051
+ for (const row of rows) {
23052
+ if (!effective) {
23053
+ effective = row;
23054
+ continue;
23055
+ }
23056
+ if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
23057
+ continue;
23058
+ const rowIndependent = reasonsOf(row).length === 0;
23059
+ const effIndependent = reasonsOf(effective).length === 0;
23060
+ if (effIndependent && !rowIndependent && isCurrent(effective))
23061
+ continue;
23062
+ if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
23063
+ continue;
23064
+ effective = row;
23104
23065
  }
23066
+ return effective;
23105
23067
  }
23106
- var REGULAR = new Set(["100644", "100755"]);
23107
- var ABSENT = /^0+$/u;
23108
- function cosmeticFiles(cwd, fork, head) {
23109
- const raw = diffRaw(cwd, fork, head);
23110
- if (!raw)
23111
- return null;
23112
- const candidates = raw.filter((entry) => REGULAR.has(entry.oldMode) && entry.oldMode === entry.newMode && !ABSENT.test(entry.oldBlob) && (head === null || !ABSENT.test(entry.newBlob)));
23113
- const blobs = readBlobs(cwd, candidates.flatMap((entry) => [entry.oldBlob, entry.newBlob]));
23114
- if (!blobs)
23115
- return null;
23116
- const out = new Set;
23117
- const opaque = new Set;
23118
- for (const entry of candidates) {
23119
- const before = blobs.get(entry.oldBlob);
23120
- let after = blobs.get(entry.newBlob);
23121
- if (!after && head === null && ABSENT.test(entry.newBlob))
23122
- try {
23123
- after = readFileSync4(path16.join(cwd, entry.path));
23124
- } catch {
23125
- after = undefined;
23068
+ var restackExact = new WeakMap;
23069
+ function restackCarries(exactIn, rows, branch, from, to) {
23070
+ if (!from || !to)
23071
+ return false;
23072
+ const exact = (row, fromHead, head) => {
23073
+ if (exactIn === null)
23074
+ return true;
23075
+ const cwd = exactIn;
23076
+ let known = restackExact.get(row);
23077
+ if (known === undefined) {
23078
+ known = typeof row.fromBase === "string" && typeof row.toBase === "string" && sameOwnChange(cwd, {
23079
+ from: fromHead,
23080
+ to: head,
23081
+ fromBase: row.fromBase,
23082
+ toBase: row.toBase
23083
+ });
23084
+ restackExact.set(row, known);
23085
+ }
23086
+ return known;
23087
+ };
23088
+ const edges = new Map;
23089
+ for (const row of rows)
23090
+ if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.integrity === "verified" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head && exact(row, row.fromHead, row.head))
23091
+ edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
23092
+ const seen = new Set([from]);
23093
+ const queue = [from];
23094
+ while (queue.length) {
23095
+ const next = queue.shift();
23096
+ if (next === to)
23097
+ return true;
23098
+ for (const target of edges.get(next) ?? [])
23099
+ if (!seen.has(target)) {
23100
+ seen.add(target);
23101
+ queue.push(target);
23126
23102
  }
23127
- if (before && after && cosmeticChange(entry.path, before, after))
23128
- out.add(entry.path);
23129
- else if ([before, after].some((bytes) => bytes && hasOpaqueLineBreak(bytes.toString("utf8"))))
23130
- opaque.add(entry.path);
23131
23103
  }
23132
- return { cosmetic: out, opaque };
23104
+ return false;
23133
23105
  }
23134
- function generatedAt(cwd, fork, paths) {
23135
- const files = treeFilesNamed(cwd, fork, ".gitattributes");
23136
- const blobs = files?.size ? readBlobs(cwd, [...files.values()]) : null;
23137
- if (!files || !blobs)
23138
- return new Set;
23139
- const texts = new Map;
23140
- for (const [file, id] of files) {
23141
- const bytes = blobs.get(id);
23142
- if (bytes)
23143
- texts.set(file, bytes.toString("utf8"));
23106
+ function checkVerdicts(cwd, branch, ledgerRows) {
23107
+ const rows = withIntegrity(cwd, ledgerRows);
23108
+ const head = branchHead(cwd, branch);
23109
+ const fallbackBase = defaultBase(cwd);
23110
+ const scope = branchScope(cwd, branch);
23111
+ const keyCache = new Map;
23112
+ const keysAt = (base, at) => {
23113
+ if (!base || !at)
23114
+ return { patchId: null, diffHash: null };
23115
+ const cacheKey = `${base}\x00${at}`;
23116
+ let key = keyCache.get(cacheKey);
23117
+ if (!key) {
23118
+ key = { patchId: patchId(cwd, base, at), diffHash: diffHash(cwd, base, at) };
23119
+ keyCache.set(cacheKey, key);
23120
+ }
23121
+ return key;
23122
+ };
23123
+ const keyFor = (base) => keysAt(base, head);
23124
+ const driftCache = new Map;
23125
+ const driftOf = (row) => {
23126
+ if (driftCache.has(row))
23127
+ return driftCache.get(row) ?? null;
23128
+ let drift = null;
23129
+ const toBase = row.base ?? fallbackBase;
23130
+ const toBaseSha = toBase ? resolveCommit(cwd, toBase) : null;
23131
+ const fromBase = row.baseSha ?? toBaseSha;
23132
+ if (row.head && head && row.head !== head && toBaseSha && fromBase && !untrusted(reasonsOf(row)))
23133
+ drift = verdictDrift(cwd, {
23134
+ from: row.head,
23135
+ to: head,
23136
+ fromBase,
23137
+ toBase: toBaseSha,
23138
+ kind: typeof row.kind === "string" ? row.kind : "review"
23139
+ });
23140
+ driftCache.set(row, drift);
23141
+ return drift;
23142
+ };
23143
+ const exactCache = new Map;
23144
+ const exactCarry = (row) => {
23145
+ if (FAILING.has(String(row.result)))
23146
+ return true;
23147
+ let exact = exactCache.get(row);
23148
+ if (exact === undefined) {
23149
+ const toBase = row.base ?? fallbackBase;
23150
+ const toBaseSha = toBase ? resolveCommit(cwd, toBase) : null;
23151
+ const fromBase = row.baseSha ?? toBaseSha;
23152
+ exact = Boolean(row.head && head && toBaseSha && fromBase && sameOwnChange(cwd, { from: row.head, to: head, fromBase, toBase: toBaseSha }));
23153
+ exactCache.set(row, exact);
23154
+ }
23155
+ return exact;
23156
+ };
23157
+ const keyBasis = (row) => {
23158
+ const basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
23159
+ return basis === "carried" && !exactCarry(row) ? "stale" : basis;
23160
+ };
23161
+ const restacked = (row) => restackCarries(FAILING.has(String(row.result)) ? null : cwd, rows, branch, row.head, head);
23162
+ const carryOf = (row) => {
23163
+ if (keyBasis(row) !== "stale")
23164
+ return null;
23165
+ if (restacked(row))
23166
+ return null;
23167
+ const drift = driftOf(row);
23168
+ return drift?.carries ? { rule: drift.rule, since: drift.since, files: drift.files } : null;
23169
+ };
23170
+ const basisOf = (row) => {
23171
+ const basis = keyBasis(row);
23172
+ if (basis !== "stale")
23173
+ return basis;
23174
+ return restacked(row) || carryOf(row) ? "carried" : basis;
23175
+ };
23176
+ const sameCode = (failed, row) => {
23177
+ if (failed.head && failed.head === row.head)
23178
+ return true;
23179
+ const base = failed.base ?? fallbackBase;
23180
+ const stored = (row.base ?? fallbackBase) === base && row.baseSha !== null && row.baseSha === failed.baseSha;
23181
+ const keys = stored ? { patchId: row.patchId, diffHash: row.diffHash } : keysAt(base, row.head);
23182
+ return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(null, rows, branch, failed.head, row.head);
23183
+ };
23184
+ const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
23185
+ const failureAuthorCache = new Map;
23186
+ const failureAuthors = (row) => {
23187
+ let set = failureAuthorCache.get(row.seq);
23188
+ if (!set) {
23189
+ set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
23190
+ failureAuthorCache.set(row.seq, set);
23191
+ }
23192
+ return set;
23193
+ };
23194
+ const reasonCache = new Map;
23195
+ const reasonsOf = (row) => {
23196
+ let reasons = reasonCache.get(row);
23197
+ if (!reasons) {
23198
+ const failing = FAILING.has(String(row.result));
23199
+ reasons = [
23200
+ ...trustReasons(cwd, row),
23201
+ ...independenceReasons(row, failing ? failureAuthors(row) : authors)
23202
+ ];
23203
+ reasonCache.set(row, reasons);
23204
+ }
23205
+ return reasons;
23206
+ };
23207
+ const retired = new Set;
23208
+ for (const row of rows)
23209
+ if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
23210
+ retired.add(row.supersedes);
23211
+ const byKind = new Map;
23212
+ for (const row of rows)
23213
+ if (isLiveVerdict(row, retired) && scope(row)) {
23214
+ const kind = typeof row.kind === "string" ? row.kind : "review";
23215
+ byKind.set(kind, [...byKind.get(kind) ?? [], row]);
23216
+ }
23217
+ const verdicts = [];
23218
+ const warnings = [];
23219
+ for (const [kind, list] of byKind) {
23220
+ const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
23221
+ if (!row)
23222
+ continue;
23223
+ const basis = basisOf(row);
23224
+ const current = basis !== "stale";
23225
+ const independence = reasonsOf(row);
23226
+ const reasons = [
23227
+ ...current ? [] : ["stale"],
23228
+ ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
23229
+ ...independence
23230
+ ];
23231
+ verdicts.push({
23232
+ kind,
23233
+ basis,
23234
+ current,
23235
+ independent: independence.length === 0,
23236
+ accepted: reasons.length === 0,
23237
+ reasons,
23238
+ carry: basis === "carried" ? carryOf(row) : null,
23239
+ verdict: row
23240
+ });
23241
+ if (reasons.length === 0) {
23242
+ for (const later of list)
23243
+ if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
23244
+ warnings.push(`the author's own (self) ${String(later.result)} ${kind} verdict ${later.id} by ${later.actor.session ?? "no session"} on the current code is newer than the accepted pass ${row.id}: read it before merging`);
23245
+ }
23144
23246
  }
23145
- return generatedByAttributes(texts, paths);
23146
- }
23147
- var forkOf = (cwd, base, head) => mergeBase(cwd, base, head ?? "HEAD");
23148
- function laneFiles(cwd, fork, head) {
23149
- const stats = diffNumstat(cwd, fork, head);
23150
- const cosmetic = stats ? cosmeticFiles(cwd, fork, head) : null;
23151
- if (!stats || !cosmetic)
23152
- return null;
23153
- const files = [
23154
- ...stats.map((entry) => ({
23155
- ...entry,
23156
- ...cosmetic.opaque.has(entry.path) ? { binary: true } : {},
23157
- cosmetic: !entry.binary && cosmetic.cosmetic.has(entry.path)
23158
- })),
23159
- ...head === null ? untrackedFiles(cwd).map((file) => untrackedStat(cwd, file)) : []
23160
- ];
23161
- const generated = generatedAt(cwd, fork, files.map((entry) => entry.path));
23162
- return files.map((entry) => ({
23163
- ...entry,
23164
- generated: generated.has(entry.path)
23165
- }));
23166
- }
23167
- function branchLane(cwd, options) {
23168
- const candidates = options.base ? [`origin/${options.base}`, options.base] : [];
23169
- const base = candidates.find((ref) => resolveRef(cwd, ref) !== null) ?? defaultBase(cwd) ?? null;
23170
- const head = options.workingTree ? null : options.head;
23171
- const fork = base && (options.workingTree || options.head) ? forkOf(cwd, base, head) : null;
23172
- const files = fork ? laneFiles(cwd, fork, head) : null;
23173
- const riskTier = options.judgment?.riskTier ?? null;
23174
- if (!fork || !files) {
23175
- const high = riskTier === "high";
23176
- return {
23177
- lane: high ? "full" : "focused",
23178
- reasons: [
23179
- `the diff of ${options.head?.slice(0, 12) ?? "(no head)"} against ${base ?? "(no base)"} cannot be read`,
23180
- ...high ? ["the task judgment records high risk"] : []
23181
- ],
23182
- lines: 0,
23183
- files: 0,
23184
- riskFiles: [],
23185
- base,
23186
- head: options.head
23187
- };
23247
+ for (const row of rows)
23248
+ if (row.integrity === "unverified" && scope(row))
23249
+ warnings.push(`ledger row ${row.id} (${row.type}) on ${row.branch ?? branch} is unverified: the workit CLI did not write it or it changed afterwards (workit ledger verify-integrity)`);
23250
+ const tampered = ledgerTampering(cwd);
23251
+ if (tampered.length) {
23252
+ warnings.push(tamperedWarning(cwd, tampered));
23253
+ for (const entry of verdicts) {
23254
+ entry.accepted = false;
23255
+ entry.reasons = [...entry.reasons, "ledger_tampered"];
23256
+ }
23188
23257
  }
23189
- const repoRisk = parseRiskPaths(fileAt(cwd, fork, RISK_PATHS_FILE) ?? "");
23258
+ verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
23259
+ const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
23260
+ const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
23261
+ const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
23262
+ const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
23263
+ accepted: false,
23264
+ verdict: failing?.verdict ?? null,
23265
+ reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
23266
+ };
23267
+ const latestSelf = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && !untrusted(entry.reasons) && (STRONG_RESULTS.has(String(entry.verdict.result)) || FAILING.has(String(entry.verdict.result))));
23268
+ const selfEntry = latestSelf && STRONG_RESULTS.has(String(latestSelf.verdict.result)) ? latestSelf : undefined;
23269
+ const carryEntry = verdicts.find((entry) => entry.verdict === accepted.verdict && entry.accepted) ?? newestCurrent;
23270
+ const lastPass = accepted.accepted || failing || tampered.length ? undefined : verdicts.findLast((entry) => !entry.current && !untrusted(entry.reasons) && STRONG_RESULTS.has(String(entry.verdict.result)));
23271
+ const drift = lastPass ? driftOf(lastPass.verdict) : null;
23272
+ const reverify = drift && !drift.carries && drift.focused && drift.files ? { scope: "focused", since: drift.since, files: drift.files } : null;
23190
23273
  return {
23191
- ...computeLane({
23192
- files,
23193
- riskTier,
23194
- riskPaths: [...repoRisk, ...options.riskPaths ?? []]
23195
- }),
23196
- base,
23197
- head: options.head
23274
+ branch,
23275
+ head,
23276
+ review: accepted.accepted ? "verified" : selfEntry ? "self-reviewed" : "unreviewed",
23277
+ selfVerdict: accepted.accepted ? null : selfEntry?.verdict ?? null,
23278
+ base: newestCurrent?.verdict.base ?? fallbackBase,
23279
+ patchId: keyFor(newestCurrent?.verdict.base ?? fallbackBase).patchId,
23280
+ current: { basis: newestCurrent?.basis ?? "none", verdict: newestCurrent?.verdict ?? null },
23281
+ carry: carryEntry?.carry ?? null,
23282
+ reverify,
23283
+ accepted,
23284
+ verdicts,
23285
+ authors: [...authors].toSorted(),
23286
+ warnings
23198
23287
  };
23199
23288
  }
23200
- var laneNeed = (lane) => lane === "none" ? "none" : lane === "self" ? "self" : "non-author";
23289
+ var OBSERVED_ONLY = new Set([
23290
+ "check",
23291
+ "merge.unverified",
23292
+ "policy.judged",
23293
+ ...PR_ROW_TYPES
23294
+ ]);
23201
23295
 
23202
23296
  // packages/workit-core/src/core/policy/derive.ts
23203
23297
  var RULES = {
@@ -23352,7 +23446,7 @@ var productDecision = (task, ledger) => {
23352
23446
  };
23353
23447
  };
23354
23448
  var planRecorded = (task, root) => {
23355
- const cited = (latestJudgment(task)?.refs ?? []).find((ref) => ref.kind === "external" || ref.kind === "file" && existsSync5(path17.resolve(root, ref.path)));
23449
+ const cited = (latestJudgment(task)?.refs ?? []).find((ref) => ref.kind === "external" || ref.kind === "file" && existsSync5(path16.resolve(root, ref.path)));
23356
23450
  if (cited)
23357
23451
  return {
23358
23452
  met: true,
@@ -23419,14 +23513,14 @@ function derivedRuleStatus(task, requirement, root, ledger) {
23419
23513
 
23420
23514
  // packages/workit-core/src/core/task-evaluation.ts
23421
23515
  var digestBytes = (value) => createHash10("sha256").update(value).digest("hex");
23422
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path18.sep}`);
23423
- var relative = (root, candidate) => path18.relative(root, candidate).split(path18.sep).join("/") || ".";
23516
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path17.sep}`);
23517
+ var relative = (root, candidate) => path17.relative(root, candidate).split(path17.sep).join("/") || ".";
23424
23518
  var canonicalPath = (value) => {
23425
23519
  if (typeof value !== "string" || !value)
23426
23520
  return null;
23427
23521
  if (value.includes("\\") || value.split("/").includes(".."))
23428
23522
  return null;
23429
- const normalized = path18.posix.normalize(value);
23523
+ const normalized = path17.posix.normalize(value);
23430
23524
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
23431
23525
  return null;
23432
23526
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -23462,7 +23556,7 @@ var compareCodeUnits2 = (left, right) => {
23462
23556
  return left.length - right.length;
23463
23557
  };
23464
23558
  var gitPaths = (root) => {
23465
- const result = spawnSync11("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
23559
+ const result = spawnSync10("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
23466
23560
  cwd: root,
23467
23561
  encoding: "buffer"
23468
23562
  });
@@ -23474,21 +23568,21 @@ var gitPaths = (root) => {
23474
23568
  git: !/not a git repository/i.test(stderr)
23475
23569
  };
23476
23570
  }
23477
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path18.sep).join("/")) : [];
23478
- const stagedDeleted = spawnSync11("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
23571
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path17.sep).join("/")) : [];
23572
+ const stagedDeleted = spawnSync10("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
23479
23573
  if (stagedDeleted.status !== 0) {
23480
23574
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
23481
23575
  if (!/not a git repository/i.test(stderr))
23482
23576
  return { paths, uncertain: true, git: true };
23483
23577
  } else if (stagedDeleted.stdout) {
23484
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path18.sep).join("/")));
23578
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path17.sep).join("/")));
23485
23579
  }
23486
23580
  return { paths: [...new Set(paths)], uncertain: false, git: true };
23487
23581
  };
23488
23582
  var walk = (root, directory, output) => {
23489
23583
  let names;
23490
23584
  try {
23491
- names = fs14.readdirSync(directory);
23585
+ names = fs13.readdirSync(directory);
23492
23586
  } catch {
23493
23587
  return true;
23494
23588
  }
@@ -23496,10 +23590,10 @@ var walk = (root, directory, output) => {
23496
23590
  for (const name of names) {
23497
23591
  if (name === ".git" || name === ".workit")
23498
23592
  continue;
23499
- const target = path18.join(directory, name);
23593
+ const target = path17.join(directory, name);
23500
23594
  let stat;
23501
23595
  try {
23502
- stat = fs14.lstatSync(target);
23596
+ stat = fs13.lstatSync(target);
23503
23597
  } catch {
23504
23598
  uncertain = true;
23505
23599
  continue;
@@ -23514,14 +23608,14 @@ var walk = (root, directory, output) => {
23514
23608
  var scopeRoots = (root, scope) => {
23515
23609
  const roots = [];
23516
23610
  for (const item of scope.paths.length ? scope.paths : ["."]) {
23517
- const target = path18.resolve(root, item);
23611
+ const target = path17.resolve(root, item);
23518
23612
  if (!inside(root, target))
23519
23613
  return failure2("invalid_input", `candidate scope escapes checkout: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} is outside the checkout` }] });
23520
23614
  let ancestor = target;
23521
- while (!fs14.existsSync(ancestor) && ancestor !== root)
23522
- ancestor = path18.dirname(ancestor);
23615
+ while (!fs13.existsSync(ancestor) && ancestor !== root)
23616
+ ancestor = path17.dirname(ancestor);
23523
23617
  try {
23524
- if (!inside(root, fs14.realpathSync(ancestor)))
23618
+ if (!inside(root, fs13.realpathSync(ancestor)))
23525
23619
  return failure2("invalid_input", `candidate scope escapes checkout through a symlink: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} resolves outside the checkout` }] });
23526
23620
  } catch {
23527
23621
  return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
@@ -23533,7 +23627,7 @@ var scopeRoots = (root, scope) => {
23533
23627
  function captureCandidate(root, scope, environment = []) {
23534
23628
  let checkout;
23535
23629
  try {
23536
- checkout = fs14.realpathSync(root);
23630
+ checkout = fs13.realpathSync(root);
23537
23631
  } catch {
23538
23632
  return failure2("invalid_input", "candidate root does not exist");
23539
23633
  }
@@ -23551,11 +23645,11 @@ function captureCandidate(root, scope, environment = []) {
23551
23645
  if (item === ".git" || item === ".workit")
23552
23646
  continue;
23553
23647
  names.add(item);
23554
- if (!fs14.existsSync(target))
23648
+ if (!fs13.existsSync(target))
23555
23649
  continue;
23556
23650
  let stat;
23557
23651
  try {
23558
- stat = fs14.lstatSync(target);
23652
+ stat = fs13.lstatSync(target);
23559
23653
  } catch {
23560
23654
  uncertain = true;
23561
23655
  continue;
@@ -23571,17 +23665,17 @@ function captureCandidate(root, scope, environment = []) {
23571
23665
  for (const item of [...names].toSorted()) {
23572
23666
  if (!scopeMatches(item, normalizedScope))
23573
23667
  continue;
23574
- const target = path18.join(checkout, item);
23668
+ const target = path17.join(checkout, item);
23575
23669
  try {
23576
- const stat = fs14.lstatSync(target);
23670
+ const stat = fs13.lstatSync(target);
23577
23671
  if (stat.isSymbolicLink()) {
23578
- const link = fs14.readlinkSync(target);
23672
+ const link = fs13.readlinkSync(target);
23579
23673
  files.push({ path: item, kind: "symlink", digest: digestBytes(link), executable: null });
23580
23674
  } else if (stat.isFile()) {
23581
23675
  files.push({
23582
23676
  path: item,
23583
23677
  kind: "file",
23584
- digest: digestBytes(fs14.readFileSync(target)),
23678
+ digest: digestBytes(fs13.readFileSync(target)),
23585
23679
  executable: (stat.mode & 73) !== 0
23586
23680
  });
23587
23681
  } else if (!stat.isDirectory()) {
@@ -23606,7 +23700,7 @@ function captureCandidate(root, scope, environment = []) {
23606
23700
  const values = supplied.toSorted((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
23607
23701
  if (values.some(({ value }) => value === null))
23608
23702
  completeness = "uncertain";
23609
- const headResult = spawnSync11("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
23703
+ const headResult = spawnSync10("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
23610
23704
  if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
23611
23705
  completeness = "uncertain";
23612
23706
  const head = headResult.status === 0 ? headResult.stdout.trim() : null;
@@ -23746,7 +23840,7 @@ var findingVerificationPasses = (findingCandidateId, evidence) => evidence.statu
23746
23840
  var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
23747
23841
  let root;
23748
23842
  try {
23749
- root = fs14.realpathSync(checkoutRoot);
23843
+ root = fs13.realpathSync(checkoutRoot);
23750
23844
  } catch {
23751
23845
  return failure2("permission_denied", "checkout root is unavailable");
23752
23846
  }
@@ -23755,23 +23849,23 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
23755
23849
  continue;
23756
23850
  if (!reference.digest)
23757
23851
  return failure2("invalid_input", "document references require a byte digest");
23758
- const target = path18.resolve(root, reference.path);
23759
- if (target !== root && !target.startsWith(`${root}${path18.sep}`))
23852
+ const target = path17.resolve(root, reference.path);
23853
+ if (target !== root && !target.startsWith(`${root}${path17.sep}`))
23760
23854
  return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
23761
23855
  fields: [
23762
23856
  { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
23763
23857
  ]
23764
23858
  });
23765
23859
  try {
23766
- const below = path18.relative(root, target);
23860
+ const below = path17.relative(root, target);
23767
23861
  let current = root;
23768
- for (const segment of below.split(path18.sep)) {
23862
+ for (const segment of below.split(path17.sep)) {
23769
23863
  if (!segment)
23770
23864
  continue;
23771
- current = path18.join(current, segment);
23772
- const stat = fs14.lstatSync(current);
23773
- const real = fs14.realpathSync(current);
23774
- if (real !== root && !real.startsWith(`${root}${path18.sep}`))
23865
+ current = path17.join(current, segment);
23866
+ const stat = fs13.lstatSync(current);
23867
+ const real = fs13.realpathSync(current);
23868
+ if (real !== root && !real.startsWith(`${root}${path17.sep}`))
23775
23869
  return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
23776
23870
  fields: [
23777
23871
  {
@@ -23787,7 +23881,7 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
23787
23881
  if (current !== target && !stat.isDirectory())
23788
23882
  return failure2("invalid_input", "document reference ancestor is not a directory");
23789
23883
  }
23790
- if (digestBytes(fs14.readFileSync(target)) !== reference.digest)
23884
+ if (digestBytes(fs13.readFileSync(target)) !== reference.digest)
23791
23885
  return failure2("permission_denied", "decision document bytes have changed");
23792
23886
  } catch {
23793
23887
  return failure2("invalid_input", "decision document is unavailable");
@@ -24630,7 +24724,7 @@ var judgeInputSummary = (raw) => {
24630
24724
 
24631
24725
  // packages/workit-core/src/autonomy.ts
24632
24726
  import os4 from "node:os";
24633
- import path19 from "node:path";
24727
+ import path18 from "node:path";
24634
24728
  var accountHome = () => {
24635
24729
  try {
24636
24730
  return os4.userInfo().homedir;
@@ -24639,10 +24733,10 @@ var accountHome = () => {
24639
24733
  }
24640
24734
  };
24641
24735
  var grantsHome = { resolve: accountHome };
24642
- var grantsDir = () => path19.join(grantsHome.resolve(), ".config", "workit");
24736
+ var grantsDir = () => path18.join(grantsHome.resolve(), ".config", "workit");
24643
24737
  var grantsOverride = () => {
24644
- const configured = path19.resolve(resolveConfigDir());
24645
- return configured === path19.resolve(grantsDir()) ? null : `the config directory is redirected to ${configured} (WORKFLOW_TOOLKIT_CONFIG, WORKFLOW_TOOLKIT_CONFIG_DIR or XDG_CONFIG_HOME); grants are read only from ${path19.join(grantsDir(), "workspaces.json")}, so the D4 defaults apply`;
24738
+ const configured = path18.resolve(resolveConfigDir());
24739
+ return configured === path18.resolve(grantsDir()) ? null : `the config directory is redirected to ${configured} (WORKFLOW_TOOLKIT_CONFIG, WORKFLOW_TOOLKIT_CONFIG_DIR or XDG_CONFIG_HOME); grants are read only from ${path18.join(grantsDir(), "workspaces.json")}, so the D4 defaults apply`;
24646
24740
  };
24647
24741
  var GRANT_KINDS = ["push", "pr", "merge", "release", "rerun"];
24648
24742
  var DEFAULT_GRANTS = Object.freeze({
@@ -26451,9 +26545,9 @@ class WorkitCore {
26451
26545
  }
26452
26546
  }
26453
26547
  // packages/workit-core/src/core/repo-context.ts
26454
- import { spawnSync as spawnSync12 } from "node:child_process";
26548
+ import { spawnSync as spawnSync11 } from "node:child_process";
26455
26549
  import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync4 } from "node:fs";
26456
- import path20 from "node:path";
26550
+ import path19 from "node:path";
26457
26551
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
26458
26552
  try {
26459
26553
  gitRevisionParts(value);
@@ -26480,7 +26574,7 @@ var invalidRange = (cwd) => ({
26480
26574
  cwd
26481
26575
  });
26482
26576
  var runGit = (cwd, args) => {
26483
- const result = spawnSync12("git", args, { cwd, encoding: "utf8" });
26577
+ const result = spawnSync11("git", args, { cwd, encoding: "utf8" });
26484
26578
  return {
26485
26579
  stdout: (result.stdout ?? "").trimEnd(),
26486
26580
  stderr: (result.stderr ?? "").trimEnd(),
@@ -26587,7 +26681,7 @@ var findPrTemplate = (cwd) => {
26587
26681
  ]) {
26588
26682
  const parts = rel.split("/");
26589
26683
  const base = parts[parts.length - 1];
26590
- const dir = parts.length > 1 ? path20.join(cwd, ...parts.slice(0, -1)) : cwd;
26684
+ const dir = parts.length > 1 ? path19.join(cwd, ...parts.slice(0, -1)) : cwd;
26591
26685
  const actual = probeCaseInsensitive(dir, base);
26592
26686
  if (actual)
26593
26687
  return [...parts.slice(0, -1), actual].join("/");
@@ -26618,7 +26712,7 @@ var diffStatForRange = (cwd, range) => {
26618
26712
  return runGit(cwd, ["diff", "--stat", "--"]).stdout;
26619
26713
  };
26620
26714
  var packageScripts = (cwd, keys) => {
26621
- const pkgPath = path20.join(cwd, "package.json");
26715
+ const pkgPath = path19.join(cwd, "package.json");
26622
26716
  if (!existsSync7(pkgPath))
26623
26717
  return [];
26624
26718
  try {
@@ -26644,9 +26738,9 @@ var documentationFiles = (cwd) => {
26644
26738
  if (entry.isDirectory()) {
26645
26739
  if (excluded.has(entry.name))
26646
26740
  continue;
26647
- walk(path20.join(dir, entry.name), depth + 1);
26741
+ walk(path19.join(dir, entry.name), depth + 1);
26648
26742
  } else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
26649
- matches.push("./" + path20.relative(cwd, path20.join(dir, entry.name)).replaceAll("\\", "/"));
26743
+ matches.push("./" + path19.relative(cwd, path19.join(dir, entry.name)).replaceAll("\\", "/"));
26650
26744
  }
26651
26745
  }
26652
26746
  };
@@ -26662,7 +26756,7 @@ var readTrimmed2 = (file, maxLines) => {
26662
26756
  `);
26663
26757
  };
26664
26758
  function prReadyContext(root, range) {
26665
- const cwd = path20.resolve(repoRoot(root));
26759
+ const cwd = path19.resolve(repoRoot(root));
26666
26760
  if (range !== undefined && !isResolvableContextRange(cwd, range))
26667
26761
  return invalidRange(cwd);
26668
26762
  let stdout = "";
@@ -26720,7 +26814,7 @@ ${status.stderr}` : "");
26720
26814
  stdout += `template_path: ${template}
26721
26815
 
26722
26816
  `;
26723
- stdout += readTrimmed2(path20.join(cwd, template), 220);
26817
+ stdout += readTrimmed2(path19.join(cwd, template), 220);
26724
26818
  } else {
26725
26819
  stdout += `template_path: none
26726
26820
 
@@ -26730,7 +26824,7 @@ ${status.stderr}` : "");
26730
26824
  stdout += `
26731
26825
  `;
26732
26826
  stdout += printSection("Recent Validation Signals");
26733
- if (existsSync7(path20.join(cwd, "package.json"))) {
26827
+ if (existsSync7(path19.join(cwd, "package.json"))) {
26734
26828
  stdout += `package.json detected. Common scripts:
26735
26829
  `;
26736
26830
  const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
@@ -26739,7 +26833,7 @@ ${status.stderr}` : "");
26739
26833
  `) + `
26740
26834
  `;
26741
26835
  }
26742
- if (existsSync7(path20.join(cwd, "Cargo.toml")) || existsSync7(path20.join(cwd, "src-tauri/Cargo.toml"))) {
26836
+ if (existsSync7(path19.join(cwd, "Cargo.toml")) || existsSync7(path19.join(cwd, "src-tauri/Cargo.toml"))) {
26743
26837
  stdout += `Rust project detected.
26744
26838
  `;
26745
26839
  }
@@ -26772,7 +26866,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
26772
26866
  - Apply with the host-native editor, preserving the repository changelog conventions.
26773
26867
  - If Unreleased already has duplicate category headings, normalize_only first.`;
26774
26868
  function changelogContext(root, range) {
26775
- const cwd = path20.resolve(repoRoot(root));
26869
+ const cwd = path19.resolve(repoRoot(root));
26776
26870
  if (range !== undefined && !isResolvableContextRange(cwd, range))
26777
26871
  return invalidRange(cwd);
26778
26872
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -26790,7 +26884,7 @@ function changelogContext(root, range) {
26790
26884
  stdout += CHANGELOG_RULES + `
26791
26885
  `;
26792
26886
  stdout += printSection("Existing CHANGELOG.md");
26793
- const changelogPath = path20.join(cwd, "CHANGELOG.md");
26887
+ const changelogPath = path19.join(cwd, "CHANGELOG.md");
26794
26888
  if (existsSync7(changelogPath)) {
26795
26889
  stdout += readTrimmed2(changelogPath, 260) + `
26796
26890
  `;
@@ -26810,7 +26904,7 @@ function changelogContext(root, range) {
26810
26904
  return { stdout, stderr: "", exitCode: 0, cwd };
26811
26905
  }
26812
26906
  function docsRefreshContext(root, range) {
26813
- const cwd = path20.resolve(repoRoot(root));
26907
+ const cwd = path19.resolve(repoRoot(root));
26814
26908
  if (range !== undefined && !isResolvableContextRange(cwd, range))
26815
26909
  return invalidRange(cwd);
26816
26910
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -26832,7 +26926,7 @@ function docsRefreshContext(root, range) {
26832
26926
  `) + `
26833
26927
  `;
26834
26928
  stdout += printSection("README Preview");
26835
- const readme = path20.join(cwd, "README.md");
26929
+ const readme = path19.join(cwd, "README.md");
26836
26930
  if (existsSync7(readme)) {
26837
26931
  stdout += readTrimmed2(readme, 220) + `
26838
26932
  `;
@@ -26841,7 +26935,7 @@ function docsRefreshContext(root, range) {
26841
26935
  `;
26842
26936
  }
26843
26937
  stdout += printSection("Package Scripts");
26844
- const pkgPath = path20.join(cwd, "package.json");
26938
+ const pkgPath = path19.join(cwd, "package.json");
26845
26939
  if (existsSync7(pkgPath)) {
26846
26940
  try {
26847
26941
  const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
@@ -26855,7 +26949,7 @@ function docsRefreshContext(root, range) {
26855
26949
  return { stdout, stderr: "", exitCode: 0, cwd };
26856
26950
  }
26857
26951
  function releaseNotesContext(root, rangeOrTag) {
26858
- const cwd = path20.resolve(repoRoot(root));
26952
+ const cwd = path19.resolve(repoRoot(root));
26859
26953
  if (!rangeOrTag) {
26860
26954
  return { stdout: "", stderr: `ERROR: release tag or range required
26861
26955
  `, exitCode: 1, cwd };
@@ -26893,7 +26987,7 @@ function releaseNotesContext(root, rangeOrTag) {
26893
26987
  `;
26894
26988
  stdout += printSection("Existing Release Files");
26895
26989
  for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
26896
- if (existsSync7(path20.join(cwd, rel)))
26990
+ if (existsSync7(path19.join(cwd, rel)))
26897
26991
  stdout += rel + `
26898
26992
  `;
26899
26993
  }
@@ -26922,6 +27016,46 @@ function releaseNotesContext(root, rangeOrTag) {
26922
27016
 
26923
27017
  // packages/workit-core/src/core/tracker-issues.ts
26924
27018
  import { spawnSync as spawnSync13 } from "node:child_process";
27019
+
27020
+ // packages/workit-core/src/core/pr-create.ts
27021
+ import fs14 from "node:fs";
27022
+ import path20 from "node:path";
27023
+ import { spawnSync as spawnSync12 } from "node:child_process";
27024
+ // packages/workit-core/src/forge/pr-body.ts
27025
+ function parseGhIssue(value) {
27026
+ const m = /issues\/(\d+)/.exec(value);
27027
+ return m ? m[1] : String(value).trim().replace(/^#/, "");
27028
+ }
27029
+
27030
+ // packages/workit-core/src/core/pr-create.ts
27031
+ function whichOnPath(tool) {
27032
+ const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
27033
+ for (const dir of (process.env.PATH ?? "").split(path20.delimiter)) {
27034
+ if (!dir)
27035
+ continue;
27036
+ for (const name of names) {
27037
+ const candidate = path20.join(dir, name);
27038
+ try {
27039
+ fs14.accessSync(candidate, fs14.constants.X_OK);
27040
+ return candidate;
27041
+ } catch {}
27042
+ }
27043
+ }
27044
+ return null;
27045
+ }
27046
+ var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
27047
+ var pushRemote = (cwd) => {
27048
+ const result = spawnSync12("git", ["remote", "get-url", "--push", "--all", "origin"], {
27049
+ cwd,
27050
+ encoding: "utf8"
27051
+ });
27052
+ if (result.status !== 0)
27053
+ return null;
27054
+ const urls = (result.stdout ?? "").split(/\r?\n/u).map((url) => url.trim()).filter(Boolean);
27055
+ return urls.length === 1 ? urls[0] : null;
27056
+ };
27057
+
27058
+ // packages/workit-core/src/core/tracker-issues.ts
26925
27059
  var repoPathFromRemote = (remote) => {
26926
27060
  let rest = (remote || "").trim().replace(/\/+$/, "");
26927
27061
  if (!rest)