@brainervirus/workit-cursor 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.
@@ -6903,6 +6903,26 @@ function resolveRef(cwd, ref) {
6903
6903
  return null;
6904
6904
  return line(git(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]));
6905
6905
  }
6906
+ function originHeadRef(cwd) {
6907
+ return line(git(cwd, ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"]));
6908
+ }
6909
+ function ownChangeDrift(cwd, range) {
6910
+ const before = diffRaw(cwd, range.fromFork, range.from);
6911
+ const after = diffRaw(cwd, range.toFork, range.to);
6912
+ if (!before || !after)
6913
+ return null;
6914
+ const judged = new Map(before.map((entry) => [entry.path, entry]));
6915
+ const current = new Map(after.map((entry) => [entry.path, entry]));
6916
+ const same = (a, b) => a !== undefined && b !== undefined && a.oldMode === b.oldMode && a.oldBlob === b.oldBlob && a.newMode === b.newMode && a.newBlob === b.newBlob;
6917
+ return [...new Set([...judged.keys(), ...current.keys()])].filter((file) => !same(judged.get(file), current.get(file))).toSorted();
6918
+ }
6919
+ function sameOwnChange(cwd, range) {
6920
+ const fromFork = mergeBase(cwd, range.fromBase, range.from);
6921
+ const toFork = mergeBase(cwd, range.toBase, range.to);
6922
+ if (!fromFork || !toFork)
6923
+ return false;
6924
+ return ownChangeDrift(cwd, { from: range.from, to: range.to, fromFork, toFork })?.length === 0;
6925
+ }
6906
6926
  var LANE_DIFF = [
6907
6927
  "-c",
6908
6928
  "core.quotePath=false",
@@ -7045,7 +7065,7 @@ function pushRemoteName(cwd, branch) {
7045
7065
  import { spawnSync as spawnSync8 } from "node:child_process";
7046
7066
  import { createHash as createHash9, randomBytes as randomBytes2 } from "node:crypto";
7047
7067
  import fs7 from "node:fs";
7048
- import path9 from "node:path";
7068
+ import path10 from "node:path";
7049
7069
 
7050
7070
  // packages/workit-core/src/core/store-lock.ts
7051
7071
  import { spawnSync as spawnSync3 } from "node:child_process";
@@ -7471,6 +7491,10 @@ var clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
7471
7491
  }
7472
7492
  };
7473
7493
 
7494
+ // packages/workit-core/src/core/policy/lane.ts
7495
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
7496
+ import path6 from "node:path";
7497
+
7474
7498
  // packages/workit-core/src/core/vcs-config.ts
7475
7499
  import fs4 from "node:fs";
7476
7500
  import path5 from "node:path";
@@ -8790,2058 +8814,2165 @@ function vcsConfig(mode, cwd, options = {}) {
8790
8814
  return out;
8791
8815
  }
8792
8816
 
8793
- // packages/workit-core/src/host-session.ts
8794
- import { createHash as createHash7, randomBytes, timingSafeEqual } from "node:crypto";
8795
- import fs5 from "node:fs";
8796
- import path7 from "node:path";
8797
-
8798
- // packages/workit-core/src/core/branch.ts
8799
- import { createHash as createHash6 } from "node:crypto";
8800
- var withReleaseTracks = (workspaceRoot, policy) => {
8801
- const read = workspaceReleaseTracks(workspaceRoot);
8802
- if (read.error)
8803
- throw new ReleaseTrackError(read.error);
8804
- if (read.tracks.length === 0 && read.protectedBranches.length === 0)
8805
- return policy;
8806
- const known = new Set(policy.allowed.map((pattern) => pattern.source));
8807
- const allowed = [...policy.allowed];
8808
- for (const glob of read.tracks.flatMap((track) => Object.values(track.naming).map(namingGlob))) {
8809
- const pattern = branchGlobPattern(glob);
8810
- if (!known.has(pattern.source)) {
8811
- known.add(pattern.source);
8812
- allowed.push(pattern);
8813
- }
8817
+ // packages/workit-core/src/git/default-base.ts
8818
+ var safeRef = (value) => value.length > 0 && !value.startsWith("-");
8819
+ var trackTrunk = (cwd) => {
8820
+ try {
8821
+ const resolved = vcsConfig("resolve", cwd);
8822
+ if (resolved.ok === false || !resolved.releaseTrack || resolved.releaseTrack.blocking)
8823
+ return null;
8824
+ const target = String(resolved.defaultTargetBranch ?? "").trim();
8825
+ return target && safeRef(target) ? target : null;
8826
+ } catch {
8827
+ return null;
8814
8828
  }
8815
- return {
8816
- ...policy,
8817
- allowed,
8818
- protected: new Set([
8819
- ...policy.protected,
8820
- ...[...trackBranches(read.tracks), ...read.protectedBranches].map((name) => name.toLowerCase())
8821
- ])
8822
- };
8823
8829
  };
8824
- function effectiveWorkspacePolicy(workspaceRoot, kind, profileName) {
8825
- const selected = resolveRuntimeWorkspacePolicy(workspaceRoot, kind, profileName?.trim() || process.env.WORKFLOW_PROFILE?.trim() || undefined);
8826
- const config = readConfig();
8827
- return kind === "branch" ? {
8828
- branchPolicy: withReleaseTracks(workspaceRoot, resolveBranchPolicy(config, selected.policy ? { branchPolicy: selected.policy } : null, () => repoBranchPreset(workspaceRoot))),
8829
- provenance: { branchPolicy: selected.source }
8830
- } : {
8831
- commitPolicy: resolveCommitPolicy(config, selected.policy ? { commitPolicy: selected.policy } : null),
8832
- provenance: { commitPolicy: selected.source }
8833
- };
8830
+ function defaultBase(cwd) {
8831
+ const trunk = trackTrunk(cwd);
8832
+ if (trunk) {
8833
+ for (const candidate of [`origin/${trunk}`, trunk])
8834
+ if (resolveRef(cwd, candidate))
8835
+ return candidate;
8836
+ }
8837
+ const originHead = originHeadRef(cwd);
8838
+ if (originHead && resolveRef(cwd, originHead))
8839
+ return originHead;
8840
+ for (const candidate of [
8841
+ "origin/main",
8842
+ "origin/master",
8843
+ "main",
8844
+ "master",
8845
+ "origin/develop",
8846
+ "develop"
8847
+ ])
8848
+ if (resolveRef(cwd, candidate))
8849
+ return candidate;
8850
+ return null;
8834
8851
  }
8835
- var KIND_SYNONYMS = {
8836
- fix: "bugfix",
8837
- fixes: "bugfix",
8838
- bug: "bugfix",
8839
- bugs: "bugfix",
8840
- bugfixes: "bugfix",
8841
- feat: "feature",
8842
- features: "feature",
8843
- chore: "feature",
8844
- docs: "feature",
8845
- doc: "feature",
8846
- refactor: "feature",
8847
- test: "feature",
8848
- tests: "feature",
8849
- ci: "feature",
8850
- build: "feature",
8851
- perf: "feature",
8852
- style: "feature",
8853
- deps: "feature",
8854
- task: "feature",
8855
- enhancement: "feature",
8856
- hot: "hotfix",
8857
- hotfixes: "hotfix",
8858
- releases: "release",
8859
- rel: "release"
8860
- };
8861
- var allowedKinds = (allowed) => [
8862
- ...new Set(allowed.map((pattern) => /^\^([\w.-]+)\\?\/\.\*\$$/u.exec(pattern.source)?.[1]?.toLowerCase()).filter((kind) => Boolean(kind)))
8863
- ];
8864
- var editDistance = (a, b) => {
8865
- const row = Array.from({ length: b.length + 1 }, (_, index) => index);
8866
- for (let i = 1;i <= a.length; i++) {
8867
- let previous = row[0];
8868
- row[0] = i;
8869
- for (let j = 1;j <= b.length; j++) {
8870
- const saved = row[j];
8871
- row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
8872
- previous = saved;
8873
- }
8852
+
8853
+ // packages/workit-core/src/core/policy/attributes.ts
8854
+ function patternRegex(dir, pattern) {
8855
+ if (!pattern || pattern.endsWith("/") || pattern.startsWith('"') || pattern.startsWith("!"))
8856
+ return null;
8857
+ const anchored = pattern.includes("/");
8858
+ const body = pattern.replace(/^\//u, "");
8859
+ let source = "";
8860
+ for (let index = 0;index < body.length; index += 1) {
8861
+ const char = body[index];
8862
+ if (char === "*" && body[index + 1] === "*") {
8863
+ const slash = body[index + 2] === "/";
8864
+ source += slash ? "(?:.*/)?" : ".*";
8865
+ index += slash ? 2 : 1;
8866
+ } else if (char === "*")
8867
+ source += "[^/]*";
8868
+ else if (char === "?")
8869
+ source += "[^/]";
8870
+ else if (char === "[") {
8871
+ const end = body.indexOf("]", index + 2);
8872
+ if (end < 0)
8873
+ return null;
8874
+ const set = body.slice(index + 1, end).replace(/^!/u, "^").replace(/\\/gu, "\\\\");
8875
+ source += `[${set}]`;
8876
+ index = end;
8877
+ } else if (char === "\\" && index + 1 < body.length) {
8878
+ source += body[index + 1].replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
8879
+ index += 1;
8880
+ } else
8881
+ source += char.replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
8874
8882
  }
8875
- return row[b.length];
8876
- };
8877
- var suggestBranchName = (name, kinds) => {
8878
- if (!kinds.length)
8883
+ const prefix = dir ? `${dir.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}/` : "";
8884
+ try {
8885
+ return new RegExp(anchored ? `^${prefix}${source}$` : `^${prefix}(?:.*/)?${source}$`, "u");
8886
+ } catch {
8879
8887
  return null;
8880
- const slash = name.indexOf("/");
8881
- const prefix = slash > 0 ? name.slice(0, slash).toLowerCase() : "";
8882
- const rest = (slash > 0 ? name.slice(slash + 1) : name) || "name";
8883
- const synonym = KIND_SYNONYMS[prefix];
8884
- const near = kinds.map((kind) => ({ kind, distance: editDistance(prefix, kind) })).filter((entry) => prefix && entry.distance <= 2).toSorted((a, b) => a.distance - b.distance)[0]?.kind;
8885
- const kind = (synonym && kinds.includes(synonym) ? synonym : null) ?? near ?? (kinds.includes("feature") ? "feature" : kinds[0]);
8886
- return `${kind}/${rest}`;
8887
- };
8888
- var kindList = (kinds) => kinds.map((kind) => `${kind}/`).join(", ");
8889
- var aliasNote = (kinds) => {
8890
- const notes = [
8891
- ["fix", "bugfix"],
8892
- ["chore", "feature"]
8893
- ].filter(([, kind]) => kinds.includes(kind)).map(([alias, kind]) => `${alias}/ is ${kind}/`);
8894
- return notes.length ? ` (${notes.join(", ")})` : "";
8895
- };
8896
- var validateBranchNamePolicy = (name, policy, source) => {
8897
- const allowed = policy.allowed.map((pattern) => `/${pattern.source}/${pattern.flags}`);
8898
- const kinds = allowedKinds(policy.allowed);
8899
- if (policy.protected.has(name.toLowerCase())) {
8900
- const correction = kinds.length ? `choose a non-protected branch of an allowed kind (${kindList(kinds)}), e.g. ${kinds[0]}/<slug>: workit git branch --kind ${kinds[0]} --slug <slug>` : `choose a non-protected branch matching one of: ${allowed.join(", ")}`;
8901
- return {
8902
- ok: false,
8903
- rule: "protected_ref",
8904
- source,
8905
- attempted: name,
8906
- correction,
8907
- error: `protected_ref: branch ${JSON.stringify(name)} is protected by ${source}; ${correction}`
8908
- };
8909
8888
  }
8910
- if (!policy.allowed.some((pattern) => pattern.test(name))) {
8911
- const suggestion = suggestBranchName(name, kinds);
8912
- const correction = suggestion && policy.allowed.some((pattern) => pattern.test(suggestion)) ? `use ${JSON.stringify(suggestion)}; the allowed kinds are ${kindList(kinds)} (workit git branch ${suggestion})` : `choose a branch matching one of: ${allowed.join(", ")}`;
8913
- return {
8914
- ok: false,
8915
- rule: "allowed_pattern",
8916
- source,
8917
- attempted: name,
8918
- correction,
8919
- error: `allowed_pattern: branch ${JSON.stringify(name)} violates ${source} policy; ${correction}`
8920
- };
8889
+ }
8890
+ function rulesOf(dir, text) {
8891
+ const out = [];
8892
+ for (const raw of text.split(`
8893
+ `)) {
8894
+ const line = raw.trim();
8895
+ if (!line || line.startsWith("#") || line.startsWith("[attr]"))
8896
+ continue;
8897
+ const [pattern, ...attributes] = line.split(/\s+/u);
8898
+ let generated = null;
8899
+ for (const attribute of attributes) {
8900
+ if (attribute === "linguist-generated" || attribute === "linguist-generated=true")
8901
+ generated = true;
8902
+ else if (attribute === "-linguist-generated" || attribute === "!linguist-generated" || attribute.startsWith("linguist-generated="))
8903
+ generated = false;
8904
+ }
8905
+ if (generated === null)
8906
+ continue;
8907
+ const regex = patternRegex(dir, pattern);
8908
+ if (regex)
8909
+ out.push({ regex, generated });
8921
8910
  }
8922
- return { ok: true };
8923
- };
8924
- var branchPolicySnapshotFor = (workspaceRoot) => {
8925
- const resolved = effectiveWorkspacePolicy(workspaceRoot, "branch");
8926
- const branchPolicy = resolved.branchPolicy;
8927
- const source = resolved.provenance.branchPolicy;
8928
- const allowed = branchPolicy.allowed.map((pattern) => `${pattern.source}/${pattern.flags}`).toSorted();
8929
- const protectedRefs = [...branchPolicy.protected].map((name) => name.toLowerCase()).toSorted();
8930
- const fingerprint = createHash6("sha256").update(JSON.stringify({ allowed, protectedRefs, source })).digest("hex");
8931
- return {
8932
- branchPolicy,
8933
- namePolicy: { allowed: branchPolicy.allowed, protected: branchPolicy.protected },
8934
- source,
8935
- fingerprint
8936
- };
8937
- };
8938
- var validateBranchNameFor = (workspaceRoot, name) => {
8939
- try {
8940
- const resolved = branchPolicySnapshotFor(workspaceRoot);
8941
- return validateBranchNamePolicy(name, resolved.namePolicy, resolved.source);
8942
- } catch (error) {
8943
- const correction = "repair or select a valid workspace policy before creating a branch";
8911
+ return out;
8912
+ }
8913
+ function generatedByAttributes(files, paths) {
8914
+ const rules = [...files].map(([file, text]) => {
8915
+ const dir = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
8944
8916
  return {
8945
- ok: false,
8946
- rule: "policy_unavailable",
8947
- source: "workspaces.json",
8948
- attempted: name,
8949
- correction,
8950
- error: `policy_unavailable: cannot validate branch ${JSON.stringify(name)} because ${error instanceof Error ? error.message : String(error)}; ${correction}`
8917
+ depth: dir ? dir.split("/").length : 0,
8918
+ rules: rulesOf(dir, text)
8951
8919
  };
8920
+ }).toSorted((left, right) => left.depth - right.depth).flatMap((entry) => entry.rules);
8921
+ const out = new Set;
8922
+ for (const file of paths) {
8923
+ const last = rules.findLast((rule) => rule.regex.test(file));
8924
+ if (last?.generated)
8925
+ out.add(file);
8952
8926
  }
8953
- };
8954
- var resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy;
8955
- var gitFlowOf = (root, integration) => {
8956
- try {
8957
- if (resolveBranchPolicyFor(root).preset !== "gitflow")
8958
- return null;
8959
- const selected = resolveRuntimeWorkspacePolicy(root, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined).policy;
8960
- const names = branchNames(root);
8961
- const production = productionBranchOf(names);
8962
- const develop = selected?.developBranch?.trim() || (integration && integration.toLowerCase() !== production ? integration : "develop");
8963
- return { develop, production, developExists: names.has(develop) };
8964
- } catch {
8965
- return null;
8966
- }
8967
- };
8968
- var releaseBranchesOf = (cwd) => {
8969
- try {
8970
- const resolved = resolveBranchPolicyFor(cwd);
8971
- const flow = resolved.preset === "gitflow" ? gitFlowOf(cwd) : null;
8972
- return {
8973
- preset: resolved.preset,
8974
- branches: flow ? [flow.production, flow.develop] : [],
8975
- production: flow ? flow.production : resolved.defaultTargetBranch
8976
- };
8977
- } catch {
8978
- return { preset: "unknown", branches: [], production: null };
8979
- }
8980
- };
8981
- var releaseBranch = (cwd, branch) => releaseBranchesOf(cwd).branches.some((name) => name.toLowerCase() === branch.toLowerCase());
8982
- var branchGuidance = (cwd) => {
8983
- try {
8984
- const patterns = branchPolicySnapshotFor(cwd).namePolicy.allowed;
8985
- const kinds = allowedKinds(patterns);
8986
- const kindShaped = new Set(kinds.map((kind) => branchGlobPattern(`${kind}/*`).source));
8987
- const anyName = patterns.some((pattern) => pattern.source === "^.*$");
8988
- const others = patterns.filter((pattern) => !kindShaped.has(pattern.source)).map((pattern) => globOf(pattern));
8989
- const allowed = anyName ? "Any non-protected branch name is allowed here." : `${kinds.length ? `Allowed branch kinds here: ${kindList(kinds)}${aliasNote(kinds)}` : "Allowed branch names here:"}${others.length ? `${kinds.length ? "; also" : ""} names matching ${others.join(", ")}` : ""}; anything else is denied.`;
8990
- const resolved = vcsConfig("resolve", cwd);
8991
- const integration = resolved.ok === false ? null : String(resolved.defaultTargetBranch ?? "") || null;
8992
- const flow = resolved.releaseTrack?.track ? null : gitFlowOf(cwd, integration);
8993
- if (flow?.developExists)
8994
- return `${allowed} git-flow: feature/, bugfix/ and release/ start from origin/${flow.develop}, hotfix/ from origin/${flow.production}; feature/ and bugfix/ land on ${flow.develop}, release/ and hotfix/ on ${flow.production}.`;
8995
- const names = branchNames(cwd);
8996
- const configured = String(resolved.baseBranch ?? integration ?? "") || null;
8997
- const trunk = productionBranchOf(names);
8998
- const base = configured && names.has(configured) ? `Branch from origin/${configured}.` : names.has(trunk) ? `Branch from origin/${trunk}${configured ? ` (the default target ${configured} does not exist here)` : ""}.` : "No base branch exists here yet; pass the base explicitly.";
8999
- return `${allowed} ${base}`;
9000
- } catch {
9001
- return null;
9002
- }
9003
- };
9004
- var globOf = (pattern) => pattern.source.replace(/^\^|\$$/gu, "").replaceAll(".*", "*").replace(/\\(.)/gu, "$1");
9005
- var policy = (root) => resolveBranchPolicyFor(root);
9006
- var isProtected = (root, name) => policy(root).protected.has(name.toLowerCase());
9007
- var isProtectedTarget = (root, name) => {
9008
- try {
9009
- return isProtected(root, name);
9010
- } catch {
9011
- return true;
9012
- }
9013
- };
8927
+ return out;
8928
+ }
9014
8929
 
9015
- // packages/workit-core/src/core/logger.ts
9016
- import os3 from "node:os";
9017
- import path6 from "node:path";
9018
- var SENSITIVE_WORDS = new Set([
9019
- "token",
9020
- "secret",
9021
- "password",
9022
- "passwd",
9023
- "authorization",
9024
- "credential",
9025
- "credentials",
9026
- "cookie",
9027
- "api",
9028
- "apikey",
9029
- "apitoken",
9030
- "accesstoken",
9031
- "clientsecret",
9032
- "refreshtoken",
9033
- "key",
9034
- "bearer",
9035
- "prompt",
9036
- "message",
9037
- "messages",
9038
- "content",
9039
- "body",
9040
- "args",
9041
- "argument",
9042
- "result",
9043
- "results",
9044
- "output",
9045
- "issue",
9046
- "description",
9047
- "summary",
9048
- "payload",
9049
- "script",
9050
- "command",
9051
- "text",
9052
- "query"
8930
+ // packages/workit-core/src/core/policy/substance.ts
8931
+ var SLASH_BLOCK = [["/*", "*/"]];
8932
+ var C_LIKE = { line: ["//"], block: SLASH_BLOCK, quotes: `"'` };
8933
+ var SYNTAXES = Object.fromEntries([
8934
+ ...["ts", "mts", "cts", "js", "mjs", "cjs"].map((ext) => [ext, { ...C_LIKE, js: true }]),
8935
+ ...["java", "kt", "kts", "scala", "swift", "dart", "groovy"].map((ext) => [
8936
+ ext,
8937
+ { ...C_LIKE, triple: true }
8938
+ ]),
8939
+ ["go", { ...C_LIKE, rawBacktick: true }],
8940
+ ...["css", "scss", "less"].map((ext) => [
8941
+ ext,
8942
+ { line: [], block: SLASH_BLOCK, quotes: `"'`, cssUrl: true }
8943
+ ]),
8944
+ ...["py", "pyi"].map((ext) => [
8945
+ ext,
8946
+ { line: ["#"], block: [], quotes: `"'`, triple: true, indented: true, fstring: true }
8947
+ ]),
8948
+ ["toml", { line: ["#"], block: [], quotes: `"`, literal: "'", triple: true }],
8949
+ ...["html", "htm", "xml", "svg", "xhtml"].map((ext) => [
8950
+ ext,
8951
+ { line: [], block: [["<!--", "-->"]], quotes: "", indented: true }
8952
+ ])
9053
8953
  ]);
9054
- var STACK_WORDS = new Set(["stack", "stacktrace", "trace"]);
9055
- var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9056
- var resolveStateDir = () => {
9057
- const override = process.env.WORKFLOW_TOOLKIT_STATE;
9058
- if (override)
9059
- return override;
9060
- const home = os3.homedir();
9061
- if (process.env.XDG_STATE_HOME)
9062
- return path6.join(process.env.XDG_STATE_HOME, "workit");
9063
- switch (os3.platform()) {
9064
- case "darwin":
9065
- return path6.join(home, "Library", "Application Support", "workit");
9066
- case "win32":
9067
- return path6.join(process.env.LOCALAPPDATA ?? path6.join(home, "AppData", "Local"), "workit");
9068
- default:
9069
- return path6.join(process.env.HOME ?? home, ".local", "state", "workit");
9070
- }
8954
+ 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;
8955
+ var extensionOf = (file) => {
8956
+ const name = file.slice(file.lastIndexOf("/") + 1);
8957
+ const dot = name.lastIndexOf(".");
8958
+ return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
9071
8959
  };
9072
- var homePattern = new RegExp(`${escapeRegExp2(os3.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
9073
-
9074
- // packages/workit-core/src/host-session.ts
9075
- var HOST_SESSION_VARS = [
9076
- ["OPENCODE_SESSION_ID", () => "opencode"],
9077
- ["PI_SESSION_ID", () => "pi"],
9078
- [
9079
- "CODEX_THREAD_ID",
9080
- (env) => env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop" || env.CODEX_ELECTRON_RESOURCES_PATH ? "codex_desktop" : "codex_cli"
9081
- ]
9082
- ];
9083
- var family = (host) => host?.replace(/_(?:cli|desktop)$/, "") ?? null;
9084
- function hostSessionFromEnv(env) {
9085
- const value = (key) => env[key]?.trim() || null;
9086
- let inner = null;
9087
- for (const [key, host] of HOST_SESSION_VARS) {
9088
- const session = value(key);
9089
- if (session) {
9090
- inner = { host: host(env), session };
9091
- break;
9092
- }
9093
- }
9094
- const declared = value("WORKIT_HOST");
9095
- if (env.WORKIT_SESSION_ID !== undefined) {
9096
- const inherited = inner !== null && declared !== null && family(declared) !== family(inner.host);
9097
- if (!inherited)
9098
- return { host: declared, session: value("WORKIT_SESSION_ID") };
9099
- }
9100
- return inner ? { host: inner.host, session: inner.session } : { host: declared, session: null };
8960
+ var CI_PATH = /^\.github\/|(?:^|\/)(?:\.gitlab-ci\.ya?ml|Jenkinsfile|azure-pipelines\.ya?ml|\.pre-commit-config\.ya?ml|\.circleci\/.*)$/u;
8961
+ var syntaxOf = (file) => CI_PATH.test(file) ? null : SYNTAXES[extensionOf(file)] ?? null;
8962
+ var SPACE = /[ \t\r\f\v]/u;
8963
+ var WORD = /[\w$]/u;
8964
+ var NO_SPACE_AFTER = "([{,;";
8965
+ var NO_SPACE_BEFORE = ")]},;";
8966
+ var REGEX_AFTER = "(,=:[!&|?{};+-*%<>~^";
8967
+ var REGEX_KEYWORDS = new Set([
8968
+ "return",
8969
+ "typeof",
8970
+ "instanceof",
8971
+ "case",
8972
+ "do",
8973
+ "else",
8974
+ "in",
8975
+ "of",
8976
+ "new",
8977
+ "delete",
8978
+ "void",
8979
+ "throw",
8980
+ "yield",
8981
+ "await"
8982
+ ]);
8983
+ function substanceOf(file, text) {
8984
+ const syntax = syntaxOf(file);
8985
+ if (!syntax)
8986
+ return plainSubstance(text);
8987
+ const markup = syntax.block.some(([open]) => open === "<!--");
8988
+ if (markup && (/<(?:script|style)\b|<!\[CDATA\[|<\?/iu.test(text) || /[^>\s][ \t]*<!--/u.test(text)))
8989
+ return plainSubstance(text);
8990
+ return lex(syntax, text);
9101
8991
  }
9102
- var ISSUED_PER_LEAD = 64;
9103
- var ISSUED_SESSIONS_MARKER = "workit-issued-sessions/3";
9104
- var hash = (value) => createHash7("sha256").update(value).digest("hex").slice(0, 32);
9105
- var userIssuedRoot = () => path7.join(resolveStateDir(), "issued");
9106
- var userTokenRoot = () => path7.join(resolveStateDir(), "issued-tokens");
9107
- function migrateRepoIssued(cwd) {
9108
- if (cwd === undefined)
9109
- return;
9110
- let old;
9111
- try {
9112
- const location = resolveStore(cwd);
9113
- if (location instanceof Error)
9114
- return;
9115
- old = path7.join(location.dir, "hooks", "issued");
9116
- if (!fs5.existsSync(old))
9117
- return;
9118
- } catch {
9119
- return;
9120
- }
9121
- for (const lead of safeList(old)) {
9122
- try {
9123
- const from = path7.join(old, lead);
9124
- if (!fs5.statSync(from).isDirectory())
9125
- continue;
9126
- const to = path7.join(userIssuedRoot(), lead);
9127
- fs5.mkdirSync(to, { recursive: true, mode: 448 });
9128
- for (const id of fs5.readdirSync(from)) {
9129
- try {
9130
- fs5.copyFileSync(path7.join(from, id), path7.join(to, id), fs5.constants.COPYFILE_EXCL);
9131
- } catch (error) {
9132
- if (error.code !== "EEXIST")
9133
- throw error;
9134
- }
9135
- fs5.rmSync(path7.join(from, id), { force: true });
9136
- }
9137
- removeIfEmpty(from);
9138
- } catch {}
9139
- }
9140
- removeIfEmpty(old);
8992
+ function plainSubstance(text) {
8993
+ return text.split(`
8994
+ `).map((line) => line.replace(/[ \t\r\f\v]+$/u, "")).filter(Boolean).join(`
8995
+ `);
9141
8996
  }
9142
- var safeList = (dir) => {
9143
- try {
9144
- return fs5.readdirSync(dir);
9145
- } catch {
9146
- return [];
8997
+ function lex(syntax, text) {
8998
+ const collapse = !syntax.indented;
8999
+ let out = "";
9000
+ let lineStart = 0;
9001
+ let kept = 0;
9002
+ let pendingSpace = false;
9003
+ const frames = [{ kind: "code", braces: 0 }];
9004
+ let index = 0;
9005
+ if (text.startsWith("#!")) {
9006
+ const end = text.indexOf(`
9007
+ `);
9008
+ out = end < 0 ? text : text.slice(0, end);
9009
+ kept = out.length;
9010
+ index = end < 0 ? text.length : end;
9147
9011
  }
9148
- };
9149
- var removeIfEmpty = (dir) => {
9150
- try {
9151
- fs5.rmdirSync(dir);
9152
- } catch {}
9153
- };
9154
- var tokenHash = (token) => createHash7("sha256").update(token).digest("hex");
9155
- function recordIssuedSession(cwd, lead, issued) {
9156
- const token = randomBytes(18).toString("hex");
9012
+ const emitCode = (chunk) => {
9013
+ if (collapse && pendingSpace && out.length > lineStart) {
9014
+ const before = out[out.length - 1];
9015
+ if (!NO_SPACE_AFTER.includes(before) && !NO_SPACE_BEFORE.includes(chunk[0]))
9016
+ out += " ";
9017
+ }
9018
+ pendingSpace = false;
9019
+ out += chunk;
9020
+ };
9021
+ const emitVerbatim = (chunk) => {
9022
+ emitCode(chunk);
9023
+ kept = out.length;
9024
+ };
9025
+ const newline = () => {
9026
+ if (!collapse) {
9027
+ let end = out.length;
9028
+ while (end > Math.max(lineStart, kept) && SPACE.test(out[end - 1]))
9029
+ end -= 1;
9030
+ out = out.slice(0, end);
9031
+ }
9032
+ pendingSpace = false;
9033
+ if (out.length > lineStart) {
9034
+ out += `
9035
+ `;
9036
+ lineStart = out.length;
9037
+ }
9038
+ };
9039
+ const regexAllowed = () => {
9040
+ let end = out.length;
9041
+ while (end > 0 && /\s/u.test(out[end - 1]))
9042
+ end -= 1;
9043
+ if (end === 0 || REGEX_AFTER.includes(out[end - 1]))
9044
+ return true;
9045
+ let start = end;
9046
+ while (start > 0 && WORD.test(out[start - 1]))
9047
+ start -= 1;
9048
+ return REGEX_KEYWORDS.has(out.slice(start, end));
9049
+ };
9050
+ const scanString = (quote, escapes, multiline) => {
9051
+ const prefix = text.slice(Math.max(0, index - 2), index).match(/[a-z]*$/iu)?.[0] ?? "";
9052
+ const fstring = syntax.fstring === true && /f/iu.test(prefix);
9053
+ let end = index + quote.length;
9054
+ while (end < text.length) {
9055
+ const char = text[end];
9056
+ if (escapes && char === "\\") {
9057
+ end += 2;
9058
+ continue;
9059
+ }
9060
+ if (text.startsWith(quote, end)) {
9061
+ if (fstring && unbalanced(text.slice(index + quote.length, end)))
9062
+ return false;
9063
+ emitVerbatim(text.slice(index, end + quote.length));
9064
+ index = end + quote.length;
9065
+ return true;
9066
+ }
9067
+ if (char === `
9068
+ ` && !multiline)
9069
+ return false;
9070
+ end += 1;
9071
+ }
9072
+ return false;
9073
+ };
9074
+ while (index < text.length) {
9075
+ const frame = frames[frames.length - 1];
9076
+ const char = text[index];
9077
+ if (frame.kind === "template") {
9078
+ if (char === "\\") {
9079
+ emitVerbatim(text.slice(index, index + 2));
9080
+ index += 2;
9081
+ } else if (char === "`") {
9082
+ emitVerbatim(char);
9083
+ frames.pop();
9084
+ index += 1;
9085
+ } else if (text.startsWith("${", index)) {
9086
+ emitVerbatim("${");
9087
+ frames.push({ kind: "code", braces: 0 });
9088
+ index += 2;
9089
+ } else {
9090
+ out += char;
9091
+ kept = out.length;
9092
+ index += 1;
9093
+ }
9094
+ continue;
9095
+ }
9096
+ if (char === `
9097
+ `) {
9098
+ newline();
9099
+ index += 1;
9100
+ continue;
9101
+ }
9102
+ if (SPACE.test(char)) {
9103
+ if (collapse)
9104
+ pendingSpace = true;
9105
+ else
9106
+ out += char;
9107
+ index += 1;
9108
+ continue;
9109
+ }
9110
+ const lineOpener = syntax.line.find((opener) => text.startsWith(opener, index));
9111
+ if (lineOpener) {
9112
+ const end = text.indexOf(`
9113
+ `, index);
9114
+ const comment = text.slice(index, end < 0 ? text.length : end);
9115
+ if (DIRECTIVE.test(comment.slice(lineOpener.length)))
9116
+ emitVerbatim(comment);
9117
+ else
9118
+ pendingSpace = true;
9119
+ index = end < 0 ? text.length : end;
9120
+ continue;
9121
+ }
9122
+ const block = syntax.block.find(([open]) => text.startsWith(open, index));
9123
+ if (block) {
9124
+ const end = text.indexOf(block[1], index + block[0].length);
9125
+ if (end < 0)
9126
+ return null;
9127
+ const comment = text.slice(index, end + block[1].length);
9128
+ if (DIRECTIVE.test(comment.slice(block[0].length, -block[1].length)))
9129
+ emitVerbatim(comment);
9130
+ else
9131
+ pendingSpace = true;
9132
+ if (comment.includes(`
9133
+ `))
9134
+ newline();
9135
+ index = end + block[1].length;
9136
+ continue;
9137
+ }
9138
+ if (syntax.triple && (char === '"' || char === "'") && text.startsWith(char.repeat(3), index)) {
9139
+ if (!scanString(char.repeat(3), !syntax.literal?.includes(char), true))
9140
+ return null;
9141
+ continue;
9142
+ }
9143
+ if (syntax.quotes.includes(char)) {
9144
+ if (!scanString(char, true, Boolean(syntax.multiline)))
9145
+ return null;
9146
+ continue;
9147
+ }
9148
+ if (syntax.literal?.includes(char)) {
9149
+ if (!scanString(char, false, false))
9150
+ return null;
9151
+ continue;
9152
+ }
9153
+ if (syntax.rawBacktick && char === "`") {
9154
+ if (!scanString("`", false, true))
9155
+ return null;
9156
+ continue;
9157
+ }
9158
+ if (syntax.js && char === "`") {
9159
+ emitVerbatim(char);
9160
+ frames.push({ kind: "template" });
9161
+ index += 1;
9162
+ continue;
9163
+ }
9164
+ if (syntax.cssUrl && /^url\(\s*[^"')\s]/iu.test(text.slice(index, index + 64)))
9165
+ return null;
9166
+ if (syntax.js && char === "/" && !regexAllowed() && out.trimEnd().endsWith(")")) {
9167
+ const lineEnd = text.indexOf(`
9168
+ `, index);
9169
+ if (text.slice(index + 1, lineEnd < 0 ? text.length : lineEnd).includes("/"))
9170
+ return null;
9171
+ }
9172
+ if (syntax.js && char === "/" && regexAllowed()) {
9173
+ let end = index + 1;
9174
+ let inClass = false;
9175
+ while (end < text.length && (inClass || text[end] !== "/")) {
9176
+ if (text[end] === `
9177
+ `)
9178
+ return null;
9179
+ if (text[end] === "\\")
9180
+ end += 1;
9181
+ else if (text[end] === "[")
9182
+ inClass = true;
9183
+ else if (text[end] === "]")
9184
+ inClass = false;
9185
+ end += 1;
9186
+ }
9187
+ if (end >= text.length)
9188
+ return null;
9189
+ emitVerbatim(text.slice(index, end + 1));
9190
+ index = end + 1;
9191
+ continue;
9192
+ }
9193
+ if (frames.length > 1 && frame.kind === "code") {
9194
+ if (char === "{")
9195
+ frame.braces += 1;
9196
+ else if (char === "}") {
9197
+ if (frame.braces === 0) {
9198
+ emitVerbatim(char);
9199
+ frames.pop();
9200
+ index += 1;
9201
+ continue;
9202
+ }
9203
+ frame.braces -= 1;
9204
+ }
9205
+ }
9206
+ emitCode(char);
9207
+ index += 1;
9208
+ }
9209
+ if (frames.length !== 1)
9210
+ return null;
9211
+ newline();
9212
+ return out;
9213
+ }
9214
+ function unbalanced(body) {
9215
+ let depth = 0;
9216
+ for (const part of body.replaceAll("{{", "").replaceAll("}}", "")) {
9217
+ if (part === "{")
9218
+ depth += 1;
9219
+ else if (part === "}")
9220
+ depth = Math.max(0, depth - 1);
9221
+ }
9222
+ return depth > 0;
9223
+ }
9224
+ function hasOpaqueLineBreak(text) {
9225
+ return /\r(?!\n)|[\u2028\u2029]/u.test(text);
9226
+ }
9227
+ var decoder = new TextDecoder("utf-8", { fatal: true });
9228
+ function textOf(bytes) {
9229
+ if (bytes.includes(0))
9230
+ return null;
9157
9231
  try {
9158
- migrateRepoIssued(cwd);
9159
- const own = hash(issued);
9160
- const tokens = path7.join(userTokenRoot(), hash(lead));
9161
- fs5.mkdirSync(tokens, { recursive: true, mode: 448 });
9162
- fs5.writeFileSync(path7.join(tokens, own), `${tokenHash(token)}
9163
- `, { mode: 384, flag: "wx" });
9164
- const dir = path7.join(userIssuedRoot(), hash(lead));
9165
- fs5.mkdirSync(dir, { recursive: true, mode: 448 });
9166
- fs5.writeFileSync(path7.join(dir, own), `${ISSUED_SESSIONS_MARKER}
9167
- `, { mode: 384 });
9168
- pruneIssued(dir, own, tokens);
9169
- return token;
9232
+ return decoder.decode(bytes);
9170
9233
  } catch {
9171
9234
  return null;
9172
9235
  }
9173
9236
  }
9174
- function judgeBranch(cwd) {
9175
- const branch = currentBranch(cwd);
9176
- return branch && !isProtectedTarget(cwd, branch) ? branch : null;
9237
+ function cosmeticChange(file, before, after) {
9238
+ const left = textOf(before);
9239
+ const right = textOf(after);
9240
+ if (left === null || right === null)
9241
+ return false;
9242
+ if (hasOpaqueLineBreak(left) || hasOpaqueLineBreak(right))
9243
+ return false;
9244
+ const a = substanceOf(file, left);
9245
+ return a !== null && a === substanceOf(file, right);
9177
9246
  }
9178
- function verdictCommand(options) {
9179
- const kind = options.judge === "verifier" ? "<live|unit>" : options.judge === "reviewer" ? "review" : "<live|unit|review>";
9180
- return [
9181
- "workit ledger verdict <verified|failed|blocked>",
9182
- `--kind ${kind}`,
9183
- `--branch ${options.branch ?? "<branch>"}`,
9184
- options.session ? `--session ${options.session}` : null,
9185
- options.token ? `--token=${options.token}` : null,
9186
- '--how "<what you ran and saw>"'
9187
- ].filter(Boolean).join(" ");
9247
+
9248
+ // packages/workit-core/src/core/policy/lane.ts
9249
+ var LANE_THRESHOLDS = {
9250
+ selfMaxLines: 100,
9251
+ selfMaxFiles: 5,
9252
+ fullMinLines: 500
9253
+ };
9254
+ var RISK_TOKENS = new Set([
9255
+ "auth",
9256
+ "authn",
9257
+ "authz",
9258
+ "oauth",
9259
+ "security",
9260
+ "permission",
9261
+ "permissions",
9262
+ "secret",
9263
+ "secrets",
9264
+ "crypto",
9265
+ "gate",
9266
+ "gates",
9267
+ "policy",
9268
+ "policies",
9269
+ "hook",
9270
+ "hooks",
9271
+ "husky",
9272
+ "lefthook",
9273
+ "release",
9274
+ "releases",
9275
+ "migration",
9276
+ "migrations",
9277
+ "migrate",
9278
+ "workflows"
9279
+ ]);
9280
+ var RISK_FILES = new Set([
9281
+ ".gitlab-ci.yml",
9282
+ "Jenkinsfile",
9283
+ "azure-pipelines.yml",
9284
+ ".pre-commit-config.yaml",
9285
+ ".workit/risk-paths"
9286
+ ]);
9287
+ var RISK_NAMES = new Set([".gitattributes"]);
9288
+ var RISK_PATHS_FILE = ".workit/risk-paths";
9289
+ var LOCKFILES = new Set([
9290
+ "bun.lock",
9291
+ "bun.lockb",
9292
+ "package-lock.json",
9293
+ "npm-shrinkwrap.json",
9294
+ "yarn.lock",
9295
+ "pnpm-lock.yaml",
9296
+ "Cargo.lock",
9297
+ "go.sum",
9298
+ "poetry.lock",
9299
+ "uv.lock",
9300
+ "Pipfile.lock",
9301
+ "Gemfile.lock",
9302
+ "composer.lock"
9303
+ ]);
9304
+ var DOC_EXT = /\.(?:md|mdx|markdown|rst|adoc)$/iu;
9305
+ var DOC_NAMES = /^(?:license|licence|notice|authors|changelog|changes|contributors)$/iu;
9306
+ var INSTRUCTIONS = /(?:^|\/)(?:AGENTS|CLAUDE|GEMINI|SKILL)\.md$|(?:^|\/)(?:skills|agents|commands|rules|prompts)\/[^/]+/iu;
9307
+ var TEST_PATH = /(?:^|\/)(?:test|tests|__tests__)\/|\.(?:test|spec)\.[^/]+$|_test\.[^/]+$/iu;
9308
+ var basename = (file) => file.slice(file.lastIndexOf("/") + 1);
9309
+ var isDoc = (file) => !INSTRUCTIONS.test(file) && (DOC_EXT.test(file) || DOC_NAMES.test(basename(file)));
9310
+ function kindOf(file) {
9311
+ if (file.binary)
9312
+ return "behavior";
9313
+ if (file.generated)
9314
+ return "generated";
9315
+ if (LOCKFILES.has(basename(file.path)))
9316
+ return "lockfile";
9317
+ if (isDoc(file.path))
9318
+ return "doc";
9319
+ if (file.cosmetic)
9320
+ return "cosmetic";
9321
+ return TEST_PATH.test(file.path) ? "test" : "behavior";
9188
9322
  }
9189
- function pruneIssued(dir, own, tokens) {
9190
- try {
9191
- const entries = fs5.readdirSync(dir);
9192
- if (entries.length <= ISSUED_PER_LEAD)
9193
- return;
9194
- const byAge = entries.filter((name) => name !== own).map((name) => ({ name, at: fs5.statSync(path7.join(dir, name)).mtimeMs })).toSorted((a, b) => a.at - b.at);
9195
- for (const { name } of byAge.slice(0, entries.length - ISSUED_PER_LEAD)) {
9196
- fs5.rmSync(path7.join(dir, name), { force: true });
9197
- fs5.rmSync(path7.join(tokens, name), { force: true });
9323
+ var globCache = new Map;
9324
+ function globMatch(pattern, file) {
9325
+ let regex = globCache.get(pattern);
9326
+ if (!regex) {
9327
+ let source = "";
9328
+ for (let index = 0;index < pattern.length; index += 1) {
9329
+ const char = pattern[index];
9330
+ if (char === "*" && pattern[index + 1] === "*") {
9331
+ source += pattern[index + 2] === "/" ? "(?:[^/]*/)*" : ".*";
9332
+ index += pattern[index + 2] === "/" ? 2 : 1;
9333
+ } else if (char === "*")
9334
+ source += "[^/]*";
9335
+ else if (char === "?")
9336
+ source += "[^/]";
9337
+ else
9338
+ source += char.replace(/[.+^${}()|[\]\\]/gu, "\\$&");
9198
9339
  }
9199
- } catch {}
9200
- }
9201
- var issuedRecord = (cwd, claimed) => {
9202
- const cut = claimed.lastIndexOf(":");
9203
- if (cut <= 0)
9204
- return null;
9205
- migrateRepoIssued(cwd);
9206
- try {
9207
- return fs5.readFileSync(path7.join(userIssuedRoot(), hash(claimed.slice(0, cut)), hash(claimed)), "utf8");
9208
- } catch {
9209
- return null;
9340
+ regex = new RegExp(`^${source}$`, "u");
9341
+ globCache.set(pattern, regex);
9210
9342
  }
9211
- };
9212
- var TOKEN_HASH = /^([0-9a-f]{64})$/mu;
9213
- var issuedToken = (claimed) => {
9214
- const cut = claimed.lastIndexOf(":");
9215
- if (cut <= 0)
9216
- return { stored: null, leadBound: false };
9217
- const dir = path7.join(userTokenRoot(), hash(claimed.slice(0, cut)));
9343
+ return regex.test(file) || file.startsWith(`${pattern.replace(/\/$/u, "")}/`);
9344
+ }
9345
+ function isRiskPath(file, extra = []) {
9346
+ if (extra.some((pattern) => globMatch(pattern, file)))
9347
+ return true;
9348
+ if (RISK_FILES.has(file) || RISK_NAMES.has(basename(file)))
9349
+ return true;
9350
+ if (TEST_PATH.test(file) || isDoc(file))
9351
+ return false;
9352
+ 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)));
9353
+ }
9354
+ var parseRiskPaths = (text) => text.split(`
9355
+ `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.replace(/^\//u, ""));
9356
+ function computeLane(input) {
9357
+ const riskFiles = input.files.filter((file) => isRiskPath(file.path, input.riskPaths)).map((file) => file.path);
9358
+ const risky = new Set(riskFiles);
9359
+ const kinds = input.files.map((file) => ({
9360
+ file,
9361
+ kind: risky.has(file.path) ? "behavior" : kindOf(file)
9362
+ }));
9363
+ const behavior = kinds.filter((entry) => entry.kind === "behavior" || entry.kind === "test" || entry.kind === "lockfile");
9364
+ const lines = behavior.filter((entry) => entry.kind !== "lockfile").reduce((sum, entry) => sum + entry.file.added + entry.file.deleted, 0);
9365
+ const opaque = behavior.filter((entry) => entry.kind === "lockfile" || entry.file.binary);
9366
+ const result = (lane, reasons) => ({
9367
+ lane,
9368
+ reasons,
9369
+ lines,
9370
+ files: behavior.length,
9371
+ riskFiles
9372
+ });
9373
+ const { selfMaxLines, selfMaxFiles, fullMinLines } = LANE_THRESHOLDS;
9374
+ if (input.riskTier === "high")
9375
+ return result("full", ["the task judgment records high risk"]);
9376
+ if (riskFiles.length)
9377
+ return result("full", [
9378
+ `touches a risk path: ${riskFiles.slice(0, 3).join(", ")}${riskFiles.length > 3 ? ", …" : ""}`
9379
+ ]);
9380
+ if (!behavior.length)
9381
+ return result("none", [
9382
+ input.files.length ? "only docs, comments, formatting or generated copies changed" : "no changes"
9383
+ ]);
9384
+ if (lines >= fullMinLines)
9385
+ return result("full", [`large diff: ${lines} lines (≥ ${fullMinLines})`]);
9386
+ if (lines > selfMaxLines)
9387
+ return result("focused", [`${lines} lines changed (> ${selfMaxLines})`]);
9388
+ if (behavior.length > selfMaxFiles)
9389
+ return result("focused", [`${behavior.length} behavior files changed (> ${selfMaxFiles})`]);
9390
+ if (opaque.length)
9391
+ return result("focused", [
9392
+ `a lockfile or binary changed: ${opaque.slice(0, 3).map((entry) => entry.file.path).join(", ")}${opaque.length > 3 ? ", …" : ""}`
9393
+ ]);
9394
+ return result("self", [
9395
+ `small change: ${lines} line${lines === 1 ? "" : "s"} in ${behavior.length} file${behavior.length === 1 ? "" : "s"}, no risk path`
9396
+ ]);
9397
+ }
9398
+ function untrackedStat(cwd, file) {
9218
9399
  try {
9219
- const text = fs5.readFileSync(path7.join(dir, hash(claimed)), "utf8");
9220
- return { stored: TOKEN_HASH.exec(text)?.[1] ?? null, leadBound: true };
9400
+ const bytes = readFileSync3(path6.join(cwd, file));
9401
+ if (bytes.includes(0))
9402
+ return { path: file, added: 0, deleted: 0, binary: true };
9403
+ const text = bytes.toString("utf8");
9404
+ if (hasOpaqueLineBreak(text))
9405
+ return { path: file, added: 0, deleted: 0, binary: true };
9406
+ const lines = text.split(`
9407
+ `).length - (text.endsWith(`
9408
+ `) ? 1 : 0);
9409
+ return { path: file, added: lines, deleted: 0 };
9221
9410
  } catch {
9222
- return { stored: null, leadBound: safeList(dir).length > 0 };
9411
+ return { path: file, added: 0, deleted: 0, binary: true };
9223
9412
  }
9224
- };
9225
- function issuedBinding(cwd, claimed) {
9226
- const record = issuedRecord(cwd, claimed);
9227
- const { stored, leadBound } = issuedToken(claimed);
9228
- if (stored)
9229
- return "token";
9230
- if (record === null)
9231
- return null;
9232
- return leadBound ? "token" : "legacy";
9233
9413
  }
9234
- var OWN_CHILD_SHELL = new Set(["opencode", "pi"]);
9235
-
9236
- // packages/workit-core/src/hooks/standing-signal.ts
9237
- import { createHash as createHash8 } from "node:crypto";
9238
- import fs6 from "node:fs";
9239
- import path8 from "node:path";
9240
- var gitStoreDir = (cwd) => {
9241
- try {
9242
- for (let dir = path8.resolve(cwd);; dir = path8.dirname(dir)) {
9243
- const dotGit = path8.join(dir, ".git");
9244
- if (fs6.existsSync(dotGit)) {
9245
- let gitDir = dotGit;
9246
- if (fs6.statSync(dotGit).isFile()) {
9247
- const target = /^gitdir:\s*(.+)$/mu.exec(fs6.readFileSync(dotGit, "utf8"))?.[1]?.trim();
9248
- if (!target)
9249
- return null;
9250
- gitDir = path8.resolve(dir, target);
9251
- const common = path8.join(gitDir, "commondir");
9252
- if (fs6.existsSync(common))
9253
- gitDir = path8.resolve(gitDir, fs6.readFileSync(common, "utf8").trim());
9254
- }
9255
- return path8.join(gitDir, "workit");
9256
- }
9257
- if (path8.dirname(dir) === dir)
9258
- return null;
9259
- }
9260
- } catch {
9414
+ var REGULAR = new Set(["100644", "100755"]);
9415
+ var ABSENT = /^0+$/u;
9416
+ function cosmeticFiles(cwd, fork, head) {
9417
+ const raw = diffRaw(cwd, fork, head);
9418
+ if (!raw)
9261
9419
  return null;
9262
- }
9263
- };
9264
- var standingRevFile = (store) => path8.join(store, "standing", "rev");
9265
- var standingSeenFile = (store, session, agentId) => path8.join(store, "standing", "seen", `${createHash8("sha256").update(`${session}\x00${agentId ?? ""}`).digest("hex").slice(0, 32)}.json`);
9266
- var readStandingRev = (store) => {
9267
- try {
9268
- return fs6.readFileSync(standingRevFile(store), "utf8").trim() || null;
9269
- } catch {
9420
+ const candidates = raw.filter((entry) => REGULAR.has(entry.oldMode) && entry.oldMode === entry.newMode && !ABSENT.test(entry.oldBlob) && (head === null || !ABSENT.test(entry.newBlob)));
9421
+ const blobs = readBlobs(cwd, candidates.flatMap((entry) => [entry.oldBlob, entry.newBlob]));
9422
+ if (!blobs)
9270
9423
  return null;
9424
+ const out = new Set;
9425
+ const opaque = new Set;
9426
+ for (const entry of candidates) {
9427
+ const before = blobs.get(entry.oldBlob);
9428
+ let after = blobs.get(entry.newBlob);
9429
+ if (!after && head === null && ABSENT.test(entry.newBlob))
9430
+ try {
9431
+ after = readFileSync3(path6.join(cwd, entry.path));
9432
+ } catch {
9433
+ after = undefined;
9434
+ }
9435
+ if (before && after && cosmeticChange(entry.path, before, after))
9436
+ out.add(entry.path);
9437
+ else if ([before, after].some((bytes) => bytes && hasOpaqueLineBreak(bytes.toString("utf8"))))
9438
+ opaque.add(entry.path);
9271
9439
  }
9272
- };
9273
- var readStandingSeen = (file) => {
9274
- try {
9275
- const value = JSON.parse(fs6.readFileSync(file, "utf8"));
9276
- return {
9277
- rev: typeof value.rev === "string" ? value.rev : null,
9278
- ids: Array.isArray(value.ids) ? value.ids.filter((id) => typeof id === "string") : []
9279
- };
9280
- } catch {
9281
- return { rev: null, ids: [] };
9282
- }
9283
- };
9284
- var writeStandingSeen = (file, seen) => {
9285
- try {
9286
- fs6.mkdirSync(path8.dirname(file), { recursive: true });
9287
- fs6.writeFileSync(file, JSON.stringify(seen));
9288
- } catch {}
9289
- };
9290
-
9291
- // packages/workit-core/src/ledger.ts
9292
- var LEDGER_VERSION = 1;
9293
- var MAX_LINE_BYTES = 4096;
9294
- var PASSING = new Set(["verified", "tests-verified", "type-check-only"]);
9295
- var FAILING = new Set(["failed", "blocked"]);
9296
- var STRONG_RESULTS = new Set(["verified", "tests-verified"]);
9297
- var PR_ROW_TYPES = new Set([
9298
- "pr.created",
9299
- "pr.status",
9300
- "pr.merged",
9301
- "pr.linked"
9302
- ]);
9303
- var err = (code, error, unblock) => ({
9304
- ok: false,
9305
- code,
9306
- error,
9307
- ...unblock ? { unblock } : {}
9308
- });
9309
- var gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
9310
- const run = spawnSync8("git", args, {
9311
- cwd,
9312
- encoding: "utf8",
9313
- timeout: timeoutMs,
9314
- killSignal: "SIGKILL",
9315
- windowsHide: true,
9316
- maxBuffer: 256 * 1024 * 1024,
9317
- env: {
9318
- ...process.env,
9319
- GIT_TERMINAL_PROMPT: "0",
9320
- GIT_OPTIONAL_LOCKS: "0",
9321
- LC_ALL: "C",
9322
- LANGUAGE: "C"
9323
- }
9324
- });
9325
- return {
9326
- ok: run.status === 0,
9327
- stdout: run.stdout ?? "",
9328
- stderr: run.error ? String(run.error.message) : run.stderr ?? ""
9329
- };
9330
- };
9331
- var git3 = (cwd, args) => {
9332
- const run = gitRun2(cwd, args);
9333
- const value = run.ok ? run.stdout.trim() : "";
9334
- return value ? value : null;
9335
- };
9336
- var safeRef = (value) => value.length > 0 && !value.startsWith("-");
9337
- function resolveCommit(cwd, ref) {
9338
- if (!safeRef(ref))
9339
- return null;
9340
- return git3(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]);
9341
- }
9342
- function branchHead(cwd, branch) {
9343
- if (!safeRef(branch))
9344
- return null;
9345
- return resolveCommit(cwd, `refs/heads/${branch}`) ?? resolveCommit(cwd, `refs/remotes/origin/${branch}`);
9440
+ return { cosmetic: out, opaque };
9346
9441
  }
9347
- var trackTrunk = (cwd) => {
9348
- try {
9349
- const resolved = vcsConfig("resolve", cwd);
9350
- if (resolved.ok === false || !resolved.releaseTrack || resolved.releaseTrack.blocking)
9351
- return null;
9352
- const target = String(resolved.defaultTargetBranch ?? "").trim();
9353
- return target && safeRef(target) ? target : null;
9354
- } catch {
9355
- return null;
9356
- }
9357
- };
9358
- function defaultBase(cwd) {
9359
- const trunk = trackTrunk(cwd);
9360
- if (trunk) {
9361
- for (const candidate of [`origin/${trunk}`, trunk])
9362
- if (resolveCommit(cwd, candidate))
9363
- return candidate;
9442
+ function generatedAt(cwd, fork, paths) {
9443
+ const files = treeFilesNamed(cwd, fork, ".gitattributes");
9444
+ const blobs = files?.size ? readBlobs(cwd, [...files.values()]) : null;
9445
+ if (!files || !blobs)
9446
+ return new Set;
9447
+ const texts = new Map;
9448
+ for (const [file, id] of files) {
9449
+ const bytes = blobs.get(id);
9450
+ if (bytes)
9451
+ texts.set(file, bytes.toString("utf8"));
9364
9452
  }
9365
- const originHead = git3(cwd, ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"]);
9366
- if (originHead && resolveCommit(cwd, originHead))
9367
- return originHead;
9368
- for (const candidate of [
9369
- "origin/main",
9370
- "origin/master",
9371
- "main",
9372
- "master",
9373
- "origin/develop",
9374
- "develop"
9375
- ])
9376
- if (resolveCommit(cwd, candidate))
9377
- return candidate;
9378
- return null;
9453
+ return generatedByAttributes(texts, paths);
9379
9454
  }
9380
- function diffHash(cwd, base, head) {
9381
- if (!safeRef(base) || !safeRef(head))
9455
+ var forkOf = (cwd, base, head) => mergeBase(cwd, base, head ?? "HEAD");
9456
+ function laneFiles(cwd, fork, head, attributesAt = fork) {
9457
+ const stats = diffNumstat(cwd, fork, head);
9458
+ const cosmetic = stats ? cosmeticFiles(cwd, fork, head) : null;
9459
+ if (!stats || !cosmetic)
9382
9460
  return null;
9383
- const diff = gitRun2(cwd, [
9384
- "diff",
9385
- "-U3",
9386
- "--diff-algorithm=myers",
9387
- "--indent-heuristic",
9388
- "--no-color",
9389
- "--no-ext-diff",
9390
- "--no-textconv",
9391
- "--no-renames",
9392
- "--full-index",
9393
- "--binary",
9394
- "--src-prefix=a/",
9395
- "--dst-prefix=b/",
9396
- `${base}...${head}`,
9397
- "--"
9398
- ], GIT_TIMEOUTS.worktree);
9399
- if (!diff.ok || !diff.stdout)
9461
+ const files = [
9462
+ ...stats.map((entry) => ({
9463
+ ...entry,
9464
+ ...cosmetic.opaque.has(entry.path) ? { binary: true } : {},
9465
+ cosmetic: !entry.binary && cosmetic.cosmetic.has(entry.path)
9466
+ })),
9467
+ ...head === null ? untrackedFiles(cwd).map((file) => untrackedStat(cwd, file)) : []
9468
+ ];
9469
+ const generated = generatedAt(cwd, attributesAt, files.map((entry) => entry.path));
9470
+ return files.map((entry) => ({
9471
+ ...entry,
9472
+ generated: generated.has(entry.path)
9473
+ }));
9474
+ }
9475
+ function driftFiles(cwd, range) {
9476
+ if (range.fromFork === range.toFork)
9477
+ return laneFiles(cwd, range.from, range.to, range.toFork);
9478
+ const drifted = ownChangeDrift(cwd, range);
9479
+ const stats = drifted ? diffNumstat(cwd, range.from, range.to) : null;
9480
+ if (!drifted || !stats)
9400
9481
  return null;
9401
- const hash = createHash9("sha256");
9402
- for (const line of diff.stdout.split(`
9403
- `)) {
9404
- if (line.startsWith("index "))
9405
- continue;
9406
- hash.update(line.startsWith("@@ ") ? "@@" : line);
9407
- hash.update(`
9408
- `);
9409
- }
9410
- return `sha256:${hash.digest("hex")}`;
9482
+ const byPath = new Map(stats.map((entry) => [entry.path, entry]));
9483
+ const generated = generatedAt(cwd, range.toFork, drifted);
9484
+ return drifted.map((file) => ({
9485
+ path: file,
9486
+ added: byPath.get(file)?.added ?? 0,
9487
+ deleted: byPath.get(file)?.deleted ?? 0,
9488
+ ...byPath.get(file)?.binary ? { binary: true } : {},
9489
+ cosmetic: false,
9490
+ generated: generated.has(file)
9491
+ }));
9411
9492
  }
9412
- function storeRoot(cwd) {
9413
- const location = resolveStore(path9.resolve(cwd));
9414
- if (location instanceof Error)
9415
- 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");
9416
- return { ok: true, value: { root: location.dir, shared: location.shared } };
9493
+ function branchLane(cwd, options) {
9494
+ const candidates = options.base ? [`origin/${options.base}`, options.base] : [];
9495
+ const base = candidates.find((ref) => resolveRef(cwd, ref) !== null) ?? defaultBase(cwd) ?? null;
9496
+ const head = options.workingTree ? null : options.head;
9497
+ const fork = base && (options.workingTree || options.head) ? forkOf(cwd, base, head) : null;
9498
+ const files = fork ? laneFiles(cwd, fork, head) : null;
9499
+ const riskTier = options.judgment?.riskTier ?? null;
9500
+ if (!fork || !files) {
9501
+ const high = riskTier === "high";
9502
+ return {
9503
+ lane: high ? "full" : "focused",
9504
+ reasons: [
9505
+ `the diff of ${options.head?.slice(0, 12) ?? "(no head)"} against ${base ?? "(no base)"} cannot be read`,
9506
+ ...high ? ["the task judgment records high risk"] : []
9507
+ ],
9508
+ lines: 0,
9509
+ files: 0,
9510
+ riskFiles: [],
9511
+ base,
9512
+ head: options.head
9513
+ };
9514
+ }
9515
+ const repoRisk = parseRiskPaths(fileAt(cwd, fork, RISK_PATHS_FILE) ?? "");
9516
+ return {
9517
+ ...computeLane({
9518
+ files,
9519
+ riskTier,
9520
+ riskPaths: [...repoRisk, ...options.riskPaths ?? []]
9521
+ }),
9522
+ base,
9523
+ head: options.head
9524
+ };
9417
9525
  }
9418
- function ledgerPath(cwd) {
9419
- const root = storeRoot(cwd);
9420
- return root.ok ? { ok: true, value: path9.join(root.value.root, "ledger", "ledger.jsonl") } : root;
9526
+ var laneNeed = (lane) => lane === "none" ? "none" : lane === "self" ? "self" : "non-author";
9527
+
9528
+ // packages/workit-core/src/core/policy/carry.ts
9529
+ var TEST_ONLY_KINDS = new Set(["doc", "cosmetic", "generated", "test"]);
9530
+ function verdictDrift(cwd, input) {
9531
+ const since = input.from;
9532
+ const stale = (reason, files = null, focused = false) => ({
9533
+ carries: false,
9534
+ since,
9535
+ files,
9536
+ focused,
9537
+ reason
9538
+ });
9539
+ const fromFork = mergeBase(cwd, input.fromBase, input.from);
9540
+ const toFork = mergeBase(cwd, input.toBase, input.to);
9541
+ const files = fromFork && toFork ? driftFiles(cwd, { from: input.from, to: input.to, fromFork, toFork }) : null;
9542
+ if (!fromFork || !toFork || !files)
9543
+ return stale(`the change since ${since.slice(0, 12)} cannot be read`);
9544
+ const riskPaths = [fromFork, toFork].flatMap((fork) => parseRiskPaths(fileAt(cwd, fork, RISK_PATHS_FILE) ?? ""));
9545
+ const lane = computeLane({ files, riskPaths });
9546
+ const paths = files.map((file) => file.path);
9547
+ if (lane.lane === "none")
9548
+ return { carries: true, rule: "lane-none", since, files: paths };
9549
+ if (input.kind === "unit" && lane.lane === "self" && files.every((file) => TEST_ONLY_KINDS.has(kindOf(file))))
9550
+ return { carries: true, rule: "tests-only", since, files: paths };
9551
+ return stale(lane.reasons[0] ?? `lane ${lane.lane}`, paths, lane.lane === "self");
9421
9552
  }
9422
- var newId2 = () => `${Date.now().toString(36)}-${randomBytes2(6).toString("hex")}`;
9423
- var NETWORK_FS_TYPES = new Set([
9424
- 26985,
9425
- 20859,
9426
- 4266872130,
9427
- 4283649346,
9428
- 1702057286,
9429
- 1397113167,
9430
- 12805120,
9431
- 16914839,
9432
- 1196443219,
9433
- 198183888
9434
- ]);
9435
- var isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0);
9436
- var onNetworkFs = (dir) => {
9437
- if (process.platform !== "linux")
9438
- return false;
9439
- try {
9440
- return isNetworkFsType(fs7.statfsSync(dir).type);
9441
- } catch {
9442
- return false;
9443
- }
9444
- };
9445
- var LOCK_WAIT_MS = 5000;
9446
- var LOCK_STALE_MS = 30000;
9447
- var LOCK_OWNER = "owner";
9448
- var lockToken = (lock) => {
9449
- try {
9450
- return fs7.readFileSync(path9.join(lock, LOCK_OWNER), "utf8");
9451
- } catch {
9452
- return null;
9453
- }
9454
- };
9455
- var lockAgeMs = (lock) => {
9456
- for (const target of [path9.join(lock, LOCK_OWNER), lock]) {
9457
- try {
9458
- return Date.now() - fs7.statSync(target).mtimeMs;
9459
- } catch {}
9553
+
9554
+ // packages/workit-core/src/host-session.ts
9555
+ import { createHash as createHash7, randomBytes, timingSafeEqual } from "node:crypto";
9556
+ import fs5 from "node:fs";
9557
+ import path8 from "node:path";
9558
+
9559
+ // packages/workit-core/src/core/branch.ts
9560
+ import { createHash as createHash6 } from "node:crypto";
9561
+ var withReleaseTracks = (workspaceRoot, policy) => {
9562
+ const read = workspaceReleaseTracks(workspaceRoot);
9563
+ if (read.error)
9564
+ throw new ReleaseTrackError(read.error);
9565
+ if (read.tracks.length === 0 && read.protectedBranches.length === 0)
9566
+ return policy;
9567
+ const known = new Set(policy.allowed.map((pattern) => pattern.source));
9568
+ const allowed = [...policy.allowed];
9569
+ for (const glob of read.tracks.flatMap((track) => Object.values(track.naming).map(namingGlob))) {
9570
+ const pattern = branchGlobPattern(glob);
9571
+ if (!known.has(pattern.source)) {
9572
+ known.add(pattern.source);
9573
+ allowed.push(pattern);
9574
+ }
9460
9575
  }
9461
- return null;
9576
+ return {
9577
+ ...policy,
9578
+ allowed,
9579
+ protected: new Set([
9580
+ ...policy.protected,
9581
+ ...[...trackBranches(read.tracks), ...read.protectedBranches].map((name) => name.toLowerCase())
9582
+ ])
9583
+ };
9462
9584
  };
9463
- var ledgerLock = {
9464
- acquire(lock) {
9465
- try {
9466
- fs7.mkdirSync(lock);
9467
- } catch (error) {
9468
- if (error.code === "EEXIST")
9469
- return null;
9470
- throw error;
9471
- }
9472
- const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
9473
- fs7.writeFileSync(path9.join(lock, LOCK_OWNER), token);
9474
- return token;
9475
- },
9476
- release(lock, token) {
9477
- if (lockToken(lock) !== token)
9478
- return false;
9479
- const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
9480
- try {
9481
- fs7.renameSync(lock, tomb);
9482
- } catch {
9483
- return false;
9484
- }
9485
- if (lockToken(tomb) !== token) {
9486
- try {
9487
- fs7.renameSync(tomb, lock);
9488
- } catch {}
9489
- return false;
9490
- }
9491
- fs7.rmSync(tomb, { recursive: true, force: true });
9492
- return true;
9493
- },
9494
- token: lockToken,
9495
- reap(lock, observed) {
9496
- const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
9497
- try {
9498
- fs7.renameSync(lock, tomb);
9499
- } catch {
9500
- return false;
9501
- }
9502
- if (lockToken(tomb) === observed) {
9503
- fs7.rmSync(tomb, { recursive: true, force: true });
9504
- return true;
9505
- }
9506
- try {
9507
- fs7.renameSync(tomb, lock);
9508
- } catch {
9509
- fs7.rmSync(tomb, { recursive: true, force: true });
9585
+ function effectiveWorkspacePolicy(workspaceRoot, kind, profileName) {
9586
+ const selected = resolveRuntimeWorkspacePolicy(workspaceRoot, kind, profileName?.trim() || process.env.WORKFLOW_PROFILE?.trim() || undefined);
9587
+ const config = readConfig();
9588
+ return kind === "branch" ? {
9589
+ branchPolicy: withReleaseTracks(workspaceRoot, resolveBranchPolicy(config, selected.policy ? { branchPolicy: selected.policy } : null, () => repoBranchPreset(workspaceRoot))),
9590
+ provenance: { branchPolicy: selected.source }
9591
+ } : {
9592
+ commitPolicy: resolveCommitPolicy(config, selected.policy ? { commitPolicy: selected.policy } : null),
9593
+ provenance: { commitPolicy: selected.source }
9594
+ };
9595
+ }
9596
+ var KIND_SYNONYMS = {
9597
+ fix: "bugfix",
9598
+ fixes: "bugfix",
9599
+ bug: "bugfix",
9600
+ bugs: "bugfix",
9601
+ bugfixes: "bugfix",
9602
+ feat: "feature",
9603
+ features: "feature",
9604
+ chore: "feature",
9605
+ docs: "feature",
9606
+ doc: "feature",
9607
+ refactor: "feature",
9608
+ test: "feature",
9609
+ tests: "feature",
9610
+ ci: "feature",
9611
+ build: "feature",
9612
+ perf: "feature",
9613
+ style: "feature",
9614
+ deps: "feature",
9615
+ task: "feature",
9616
+ enhancement: "feature",
9617
+ hot: "hotfix",
9618
+ hotfixes: "hotfix",
9619
+ releases: "release",
9620
+ rel: "release"
9621
+ };
9622
+ var allowedKinds = (allowed) => [
9623
+ ...new Set(allowed.map((pattern) => /^\^([\w.-]+)\\?\/\.\*\$$/u.exec(pattern.source)?.[1]?.toLowerCase()).filter((kind) => Boolean(kind)))
9624
+ ];
9625
+ var editDistance = (a, b) => {
9626
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
9627
+ for (let i = 1;i <= a.length; i++) {
9628
+ let previous = row[0];
9629
+ row[0] = i;
9630
+ for (let j = 1;j <= b.length; j++) {
9631
+ const saved = row[j];
9632
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
9633
+ previous = saved;
9510
9634
  }
9511
- return false;
9512
9635
  }
9636
+ return row[b.length];
9513
9637
  };
9514
- function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
9515
- const lock = `${file}.lock`;
9516
- const deadline = Date.now() + waitMs;
9517
- let token = null;
9518
- for (;; ) {
9519
- token = ledgerLock.acquire(lock);
9520
- if (token)
9521
- break;
9522
- const observed = lockToken(lock);
9523
- const age = lockAgeMs(lock);
9524
- if (age !== null && age > LOCK_STALE_MS && ledgerLock.reap(lock, observed))
9525
- continue;
9526
- if (Date.now() >= deadline)
9527
- return null;
9528
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
9529
- }
9530
- try {
9531
- return fn();
9532
- } finally {
9533
- ledgerLock.release(lock, token);
9638
+ var suggestBranchName = (name, kinds) => {
9639
+ if (!kinds.length)
9640
+ return null;
9641
+ const slash = name.indexOf("/");
9642
+ const prefix = slash > 0 ? name.slice(0, slash).toLowerCase() : "";
9643
+ const rest = (slash > 0 ? name.slice(slash + 1) : name) || "name";
9644
+ const synonym = KIND_SYNONYMS[prefix];
9645
+ const near = kinds.map((kind) => ({ kind, distance: editDistance(prefix, kind) })).filter((entry) => prefix && entry.distance <= 2).toSorted((a, b) => a.distance - b.distance)[0]?.kind;
9646
+ const kind = (synonym && kinds.includes(synonym) ? synonym : null) ?? near ?? (kinds.includes("feature") ? "feature" : kinds[0]);
9647
+ return `${kind}/${rest}`;
9648
+ };
9649
+ var kindList = (kinds) => kinds.map((kind) => `${kind}/`).join(", ");
9650
+ var aliasNote = (kinds) => {
9651
+ const notes = [
9652
+ ["fix", "bugfix"],
9653
+ ["chore", "feature"]
9654
+ ].filter(([, kind]) => kinds.includes(kind)).map(([alias, kind]) => `${alias}/ is ${kind}/`);
9655
+ return notes.length ? ` (${notes.join(", ")})` : "";
9656
+ };
9657
+ var validateBranchNamePolicy = (name, policy, source) => {
9658
+ const allowed = policy.allowed.map((pattern) => `/${pattern.source}/${pattern.flags}`);
9659
+ const kinds = allowedKinds(policy.allowed);
9660
+ if (policy.protected.has(name.toLowerCase())) {
9661
+ const correction = kinds.length ? `choose a non-protected branch of an allowed kind (${kindList(kinds)}), e.g. ${kinds[0]}/<slug>: workit git branch --kind ${kinds[0]} --slug <slug>` : `choose a non-protected branch matching one of: ${allowed.join(", ")}`;
9662
+ return {
9663
+ ok: false,
9664
+ rule: "protected_ref",
9665
+ source,
9666
+ attempted: name,
9667
+ correction,
9668
+ error: `protected_ref: branch ${JSON.stringify(name)} is protected by ${source}; ${correction}`
9669
+ };
9534
9670
  }
9535
- }
9536
- var sha2562 = (text) => createHash9("sha256").update(text).digest("hex");
9537
- var chainHash = (prevHash, body) => sha2562(`${prevHash ?? ""}
9538
- ${body}`);
9539
- var TAIL_BYTES = 64 * 1024;
9540
- var lastLine = (fd, size) => {
9541
- for (let span = Math.min(size, TAIL_BYTES);span > 0; span = Math.min(size, span * 2)) {
9542
- const buffer = Buffer.alloc(span);
9543
- fs7.readSync(fd, buffer, 0, span, size - span);
9544
- const end = buffer.lastIndexOf(10);
9545
- if (end < 0) {
9546
- if (span === size)
9547
- return null;
9548
- continue;
9549
- }
9550
- let stop = end;
9551
- let text = "";
9552
- while (stop >= 0) {
9553
- const start = buffer.lastIndexOf(10, stop - 1);
9554
- text = buffer.toString("utf8", start + 1, stop).trim();
9555
- if (text) {
9556
- if (start >= 0 || span === size)
9557
- return text;
9558
- break;
9559
- }
9560
- if (start < 0) {
9561
- if (span === size)
9562
- return null;
9563
- break;
9564
- }
9565
- stop = start;
9566
- }
9567
- if (span === size)
9568
- return text || null;
9671
+ if (!policy.allowed.some((pattern) => pattern.test(name))) {
9672
+ const suggestion = suggestBranchName(name, kinds);
9673
+ const correction = suggestion && policy.allowed.some((pattern) => pattern.test(suggestion)) ? `use ${JSON.stringify(suggestion)}; the allowed kinds are ${kindList(kinds)} (workit git branch ${suggestion})` : `choose a branch matching one of: ${allowed.join(", ")}`;
9674
+ return {
9675
+ ok: false,
9676
+ rule: "allowed_pattern",
9677
+ source,
9678
+ attempted: name,
9679
+ correction,
9680
+ error: `allowed_pattern: branch ${JSON.stringify(name)} violates ${source} policy; ${correction}`
9681
+ };
9569
9682
  }
9570
- return null;
9571
- };
9572
- var tighten = (fd) => {
9573
- try {
9574
- if ((fs7.fstatSync(fd).mode & 63) !== 0)
9575
- fs7.fchmodSync(fd, 384);
9576
- } catch {}
9683
+ return { ok: true };
9577
9684
  };
9578
- function appendLine(file, build, options) {
9579
- const write = () => {
9580
- const fd = fs7.openSync(file, "a+", 384);
9581
- try {
9582
- if (process.platform !== "win32")
9583
- tighten(fd);
9584
- const size = fs7.fstatSync(fd).size;
9585
- let fence = "";
9586
- if (size > 0) {
9587
- const last = Buffer.alloc(1);
9588
- fs7.readSync(fd, last, 0, 1, size - 1);
9589
- if (last[0] !== 10)
9590
- fence = `
9591
- `;
9592
- }
9593
- const line = build(lastLine(fd, size));
9594
- if (!line.ok)
9595
- return line;
9596
- const bytes = Buffer.from(fence + line.value);
9597
- const written = fs7.writeSync(fd, bytes);
9598
- if (written !== bytes.length)
9599
- return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
9600
- return { ok: true, value: undefined };
9601
- } finally {
9602
- fs7.closeSync(fd);
9603
- }
9685
+ var branchPolicySnapshotFor = (workspaceRoot) => {
9686
+ const resolved = effectiveWorkspacePolicy(workspaceRoot, "branch");
9687
+ const branchPolicy = resolved.branchPolicy;
9688
+ const source = resolved.provenance.branchPolicy;
9689
+ const allowed = branchPolicy.allowed.map((pattern) => `${pattern.source}/${pattern.flags}`).toSorted();
9690
+ const protectedRefs = [...branchPolicy.protected].map((name) => name.toLowerCase()).toSorted();
9691
+ const fingerprint = createHash6("sha256").update(JSON.stringify({ allowed, protectedRefs, source })).digest("hex");
9692
+ return {
9693
+ branchPolicy,
9694
+ namePolicy: { allowed: branchPolicy.allowed, protected: branchPolicy.protected },
9695
+ source,
9696
+ fingerprint
9604
9697
  };
9698
+ };
9699
+ var validateBranchNameFor = (workspaceRoot, name) => {
9605
9700
  try {
9606
- fs7.mkdirSync(path9.dirname(file), { recursive: true });
9607
- if (!options.forceLock && !onNetworkFs(path9.dirname(file)))
9608
- return write();
9609
- const locked = withLedgerLock(file, write);
9610
- return locked ?? err("busy", `ledger lock ${file}.lock is held`, "retry; remove the .lock directory if no workit process is running");
9701
+ const resolved = branchPolicySnapshotFor(workspaceRoot);
9702
+ return validateBranchNamePolicy(name, resolved.namePolicy, resolved.source);
9611
9703
  } catch (error) {
9612
- return err("unavailable", `cannot append to ${file}: ${error.message}`);
9704
+ const correction = "repair or select a valid workspace policy before creating a branch";
9705
+ return {
9706
+ ok: false,
9707
+ rule: "policy_unavailable",
9708
+ source: "workspaces.json",
9709
+ attempted: name,
9710
+ correction,
9711
+ error: `policy_unavailable: cannot validate branch ${JSON.stringify(name)} because ${error instanceof Error ? error.message : String(error)}; ${correction}`
9712
+ };
9613
9713
  }
9614
- }
9615
- var sealed = (row, id, at, previous) => {
9616
- const { prevHash: _prev, rowHash: _row, ...fields } = row;
9617
- const prevHash = previous === null ? null : sha2562(previous);
9618
- const body = { v: LEDGER_VERSION, id, at, ...fields, prevHash };
9619
- const full = { ...body, rowHash: chainHash(prevHash, JSON.stringify(body)) };
9620
- return { full, line: `${JSON.stringify(full)}
9621
- ` };
9622
- };
9623
- function appendRaw(cwd, row, options) {
9624
- const file = ledgerPath(cwd);
9625
- if (!file.ok)
9626
- return file;
9627
- const id = newId2();
9628
- const at = (options.now ?? new Date).toISOString();
9629
- let full = null;
9630
- const written = appendLine(file.value, (previous) => {
9631
- const next = sealed(row, id, at, previous);
9632
- const bytes = Buffer.byteLength(next.line);
9633
- if (bytes > MAX_LINE_BYTES)
9634
- 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>`);
9635
- full = next.full;
9636
- return { ok: true, value: next.line };
9637
- }, options);
9638
- return written.ok ? { ok: true, value: full } : written;
9639
- }
9640
- function observedRowBytes(row) {
9641
- const sample = sealed({ ...row, observer: "workit_cli" }, newId2(), new Date().toISOString(), "");
9642
- return Buffer.byteLength(sample.line);
9643
- }
9644
- function appendObserved(cwd, row, options = {}) {
9645
- return appendRaw(cwd, { ...row, observer: "workit_cli" }, options);
9646
- }
9647
- function appendHookObserved(cwd, row) {
9648
- return appendRaw(cwd, { ...row, observer: "host_hook" }, {});
9649
- }
9650
- var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9651
- var str = (value) => typeof value === "string" ? value : null;
9652
- var normalizeActor = (value) => {
9653
- const actor = isRecord2(value) ? value : {};
9654
- return {
9655
- host: str(actor.host) ?? "unknown",
9656
- session: str(actor.session),
9657
- agentId: str(actor.agentId)
9658
- };
9659
9714
  };
9660
- function supersedeAllowed(by, target, onBranch = (row) => (by.branch ?? null) === (row.branch ?? null)) {
9661
- if (by.type !== target.type)
9662
- return `a ${by.type} row cannot supersede a ${target.type} row`;
9663
- if (!by.actor.session || by.actor.session !== target.actor.session)
9664
- return "only the session that wrote a row can supersede it";
9665
- if (!onBranch(target))
9666
- return `the row is on another branch (${String(target.branch ?? "none")}); a row supersedes only one on its own branch`;
9667
- if (by.type === "verdict" && (by.kind ?? "review") !== (target.kind ?? "review"))
9668
- return `a ${String(by.kind ?? "review")} verdict cannot supersede a ${String(target.kind ?? "review")} verdict; record it with --kind ${String(target.kind ?? "review")}`;
9669
- if (by.type === "verdict" && by.self === true && target.self !== true)
9670
- return "a self verdict cannot replace an independent verdict";
9671
- return null;
9672
- }
9673
- function readLedger(cwd, options = {}) {
9674
- const file = ledgerPath(cwd);
9675
- if (!file.ok)
9676
- return file;
9677
- let text = "";
9678
- let stat = null;
9715
+ var resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy;
9716
+ var gitFlowOf = (root, integration) => {
9679
9717
  try {
9680
- stat = fs7.statSync(file.value);
9681
- text = fs7.readFileSync(file.value, "utf8");
9682
- } catch (error) {
9683
- if (error.code === "ENOENT")
9684
- return {
9685
- ok: true,
9686
- value: {
9687
- path: file.value,
9688
- rows: [],
9689
- skipped: 0,
9690
- integrity: options.integrity ? { chained: 0, legacy: 0, unverified: [] } : null
9691
- }
9692
- };
9693
- return err("unavailable", `cannot read ${file.value}: ${error.message}`);
9718
+ if (resolveBranchPolicyFor(root).preset !== "gitflow")
9719
+ return null;
9720
+ const selected = resolveRuntimeWorkspacePolicy(root, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined).policy;
9721
+ const names = branchNames(root);
9722
+ const production = productionBranchOf(names);
9723
+ const develop = selected?.developBranch?.trim() || (integration && integration.toLowerCase() !== production ? integration : "develop");
9724
+ return { develop, production, developExists: names.has(develop) };
9725
+ } catch {
9726
+ return null;
9694
9727
  }
9695
- const rows = [];
9696
- let skipped = 0;
9697
- const integrity = { chained: 0, legacy: 0, unverified: [] };
9698
- const earlier = new Set([null]);
9699
- let chainStarted = false;
9700
- const ids = new Set;
9701
- for (const raw of text.split(`
9702
- `)) {
9703
- const trimmed = raw.trim();
9704
- if (!trimmed)
9705
- continue;
9706
- const linked = (prevHash) => earlier.has(prevHash);
9707
- if (options.integrity)
9708
- earlier.add(sha2562(trimmed));
9709
- let parsed;
9710
- try {
9711
- parsed = JSON.parse(trimmed);
9712
- } catch {
9713
- skipped += 1;
9714
- continue;
9715
- }
9716
- if (!isRecord2(parsed) || !str(parsed.id) || !str(parsed.type) || !str(parsed.at)) {
9717
- skipped += 1;
9718
- continue;
9719
- }
9720
- let rowIntegrity = options.integrity ? "verified" : undefined;
9721
- let reason = null;
9722
- if (!options.integrity) {} else if (typeof parsed.rowHash === "string") {
9723
- chainStarted = true;
9724
- const { rowHash, ...body } = parsed;
9725
- const prevHash = str(parsed.prevHash);
9726
- if (chainHash(prevHash, JSON.stringify(body)) !== rowHash)
9727
- reason = "hash_mismatch";
9728
- else if (!linked(prevHash))
9729
- reason = "broken_link";
9730
- } else if (chainStarted)
9731
- reason = "unsigned";
9732
- else
9733
- rowIntegrity = "legacy";
9734
- if (options.integrity && !reason && ids.has(parsed.id))
9735
- reason = "duplicate_id";
9736
- if (options.integrity)
9737
- ids.add(parsed.id);
9738
- if (reason)
9739
- rowIntegrity = "unverified";
9740
- const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
9741
- const row = {
9742
- ...parsed,
9743
- v: typeof parsed.v === "number" ? parsed.v : LEDGER_VERSION,
9744
- id: parsed.id,
9745
- at: parsed.at,
9746
- type: parsed.type,
9747
- actor: normalizeActor(parsed.actor),
9748
- branch: str(parsed.branch),
9749
- head: str(parsed.head),
9750
- base: str(parsed.base),
9751
- baseSha: str(parsed.baseSha),
9752
- patchId: str(parsed.patchId),
9753
- diffHash: str(parsed.diffHash),
9754
- tree: str(parsed.tree),
9755
- dirty: typeof parsed.dirty === "boolean" ? parsed.dirty : null,
9756
- ...pr === undefined ? {} : { pr },
9757
- ...str(parsed.supersedes) ? { supersedes: parsed.supersedes } : {},
9758
- seq: rows.length + 1,
9759
- superseded: false,
9760
- supersedeIgnored: false,
9761
- ...rowIntegrity ? { integrity: rowIntegrity } : {},
9762
- ...reason ? { integrityReason: reason } : {}
9728
+ };
9729
+ var releaseBranchesOf = (cwd) => {
9730
+ try {
9731
+ const resolved = resolveBranchPolicyFor(cwd);
9732
+ const flow = resolved.preset === "gitflow" ? gitFlowOf(cwd) : null;
9733
+ return {
9734
+ preset: resolved.preset,
9735
+ branches: flow ? [flow.production, flow.develop] : [],
9736
+ production: flow ? flow.production : resolved.defaultTargetBranch
9763
9737
  };
9764
- rows.push(row);
9765
- if (!options.integrity)
9766
- continue;
9767
- if (rowIntegrity === "verified")
9768
- integrity.chained += 1;
9769
- else if (rowIntegrity === "legacy")
9770
- integrity.legacy += 1;
9771
- else if (reason)
9772
- integrity.unverified.push({
9773
- seq: row.seq,
9774
- id: row.id,
9775
- type: row.type,
9776
- branch: row.branch,
9777
- reason
9778
- });
9738
+ } catch {
9739
+ return { preset: "unknown", branches: [], production: null };
9779
9740
  }
9780
- const scopes = new Map;
9781
- const scopeOf = (branch) => {
9782
- let scope = scopes.get(branch);
9783
- if (!scope)
9784
- scopes.set(branch, scope = branchScope(cwd, branch));
9785
- return scope;
9786
- };
9787
- const byId = new Map;
9788
- for (const row of rows) {
9789
- const target = row.supersedes ? byId.get(row.supersedes) : undefined;
9790
- if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
9791
- target.superseded = true;
9792
- else if (row.supersedes)
9793
- row.supersedeIgnored = true;
9794
- if (!byId.has(row.id))
9795
- byId.set(row.id, row);
9741
+ };
9742
+ var releaseBranch = (cwd, branch) => releaseBranchesOf(cwd).branches.some((name) => name.toLowerCase() === branch.toLowerCase());
9743
+ var branchGuidance = (cwd) => {
9744
+ try {
9745
+ const patterns = branchPolicySnapshotFor(cwd).namePolicy.allowed;
9746
+ const kinds = allowedKinds(patterns);
9747
+ const kindShaped = new Set(kinds.map((kind) => branchGlobPattern(`${kind}/*`).source));
9748
+ const anyName = patterns.some((pattern) => pattern.source === "^.*$");
9749
+ const others = patterns.filter((pattern) => !kindShaped.has(pattern.source)).map((pattern) => globOf(pattern));
9750
+ const allowed = anyName ? "Any non-protected branch name is allowed here." : `${kinds.length ? `Allowed branch kinds here: ${kindList(kinds)}${aliasNote(kinds)}` : "Allowed branch names here:"}${others.length ? `${kinds.length ? "; also" : ""} names matching ${others.join(", ")}` : ""}; anything else is denied.`;
9751
+ const resolved = vcsConfig("resolve", cwd);
9752
+ const integration = resolved.ok === false ? null : String(resolved.defaultTargetBranch ?? "") || null;
9753
+ const flow = resolved.releaseTrack?.track ? null : gitFlowOf(cwd, integration);
9754
+ if (flow?.developExists)
9755
+ return `${allowed} git-flow: feature/, bugfix/ and release/ start from origin/${flow.develop}, hotfix/ from origin/${flow.production}; feature/ and bugfix/ land on ${flow.develop}, release/ and hotfix/ on ${flow.production}.`;
9756
+ const names = branchNames(cwd);
9757
+ const configured = String(resolved.baseBranch ?? integration ?? "") || null;
9758
+ const trunk = productionBranchOf(names);
9759
+ const base = configured && names.has(configured) ? `Branch from origin/${configured}.` : names.has(trunk) ? `Branch from origin/${trunk}${configured ? ` (the default target ${configured} does not exist here)` : ""}.` : "No base branch exists here yet; pass the base explicitly.";
9760
+ return `${allowed} ${base}`;
9761
+ } catch {
9762
+ return null;
9796
9763
  }
9797
- if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
9798
- tamperCache.set(file.value, {
9799
- size: stat.size,
9800
- stamp: fileStamp(stat),
9801
- breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
9802
- });
9803
- return {
9804
- ok: true,
9805
- value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
9806
- };
9807
- }
9808
- var TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
9809
- var tamperCache = new Map;
9810
- var fileStamp = (stat) => `${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`;
9811
- function ledgerTampering(cwd) {
9812
- const file = ledgerPath(cwd);
9813
- if (!file.ok)
9814
- return [];
9764
+ };
9765
+ var globOf = (pattern) => pattern.source.replace(/^\^|\$$/gu, "").replaceAll(".*", "*").replace(/\\(.)/gu, "$1");
9766
+ var policy = (root) => resolveBranchPolicyFor(root);
9767
+ var isProtected = (root, name) => policy(root).protected.has(name.toLowerCase());
9768
+ var isProtectedTarget = (root, name) => {
9815
9769
  try {
9816
- const stat = fs7.statSync(file.value);
9817
- const cached = tamperCache.get(file.value);
9818
- if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
9819
- return cached.breaks;
9770
+ return isProtected(root, name);
9820
9771
  } catch {
9821
- return [];
9772
+ return true;
9822
9773
  }
9823
- const read = readLedger(cwd, { integrity: true });
9824
- return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
9825
- }
9826
- function tamperedUnblock(cwd) {
9827
- const file = ledgerPath(cwd);
9828
- const where = file.ok ? file.value : "the ledger file";
9829
- 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`;
9830
- }
9831
- 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)}`;
9832
- function verdictBasis(verdict, current) {
9833
- if (verdict.head && verdict.head === current.head)
9834
- return "fresh";
9835
- if (verdict.patchId && verdict.patchId === current.patchId && verdict.diffHash && verdict.diffHash === current.diffHash)
9836
- return "carried";
9837
- return "stale";
9838
- }
9839
- var isLiveVerdict = (row, retired) => row.type === "verdict" && typeof row.result === "string" && !retired.has(row.id);
9840
- function trailerSessions(cwd, base, head) {
9841
- if (!base || !head || !safeRef(base) || !safeRef(head))
9842
- return [];
9843
- const out = git3(cwd, [
9844
- "log",
9845
- "--format=%(trailers:key=Workit-Session,valueonly,separator=%x00)",
9846
- `${base}..${head}`
9847
- ]);
9848
- return (out ?? "").split(/[\0\n]/u).map((value) => value.trim()).filter(Boolean);
9849
- }
9850
- var RENAMED = /^branch: renamed refs\/heads\/(.+) to refs\/heads\/(.+)$/iu;
9851
- function branchScope(cwd, branch) {
9852
- const renamedAt = new Map;
9853
- let since = Number.NEGATIVE_INFINITY;
9854
- if (safeRef(branch)) {
9855
- const log = git3(cwd, [
9856
- "log",
9857
- "-g",
9858
- "--date=unix",
9859
- "--format=%gd%x09%gs",
9860
- `refs/heads/${branch}`,
9861
- "--"
9862
- ]);
9863
- for (const line of (log ?? "").split(`
9864
- `)) {
9865
- const [selector = "", subject = ""] = line.split("\t");
9866
- const seconds = Number(/@\{(\d+)\}$/u.exec(selector)?.[1]);
9867
- if (!Number.isFinite(seconds))
9868
- continue;
9869
- since = seconds * 1000;
9870
- const match = RENAMED.exec(subject);
9871
- if (!match || match[1] === branch)
9872
- continue;
9873
- const until = (seconds + 1) * 1000;
9874
- renamedAt.set(match[1], Math.max(renamedAt.get(match[1]) ?? 0, until));
9774
+ };
9775
+
9776
+ // packages/workit-core/src/core/logger.ts
9777
+ import os3 from "node:os";
9778
+ import path7 from "node:path";
9779
+ var SENSITIVE_WORDS = new Set([
9780
+ "token",
9781
+ "secret",
9782
+ "password",
9783
+ "passwd",
9784
+ "authorization",
9785
+ "credential",
9786
+ "credentials",
9787
+ "cookie",
9788
+ "api",
9789
+ "apikey",
9790
+ "apitoken",
9791
+ "accesstoken",
9792
+ "clientsecret",
9793
+ "refreshtoken",
9794
+ "key",
9795
+ "bearer",
9796
+ "prompt",
9797
+ "message",
9798
+ "messages",
9799
+ "content",
9800
+ "body",
9801
+ "args",
9802
+ "argument",
9803
+ "result",
9804
+ "results",
9805
+ "output",
9806
+ "issue",
9807
+ "description",
9808
+ "summary",
9809
+ "payload",
9810
+ "script",
9811
+ "command",
9812
+ "text",
9813
+ "query"
9814
+ ]);
9815
+ var STACK_WORDS = new Set(["stack", "stacktrace", "trace"]);
9816
+ var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9817
+ var resolveStateDir = () => {
9818
+ const override = process.env.WORKFLOW_TOOLKIT_STATE;
9819
+ if (override)
9820
+ return override;
9821
+ const home = os3.homedir();
9822
+ if (process.env.XDG_STATE_HOME)
9823
+ return path7.join(process.env.XDG_STATE_HOME, "workit");
9824
+ switch (os3.platform()) {
9825
+ case "darwin":
9826
+ return path7.join(home, "Library", "Application Support", "workit");
9827
+ case "win32":
9828
+ return path7.join(process.env.LOCALAPPDATA ?? path7.join(home, "AppData", "Local"), "workit");
9829
+ default:
9830
+ return path7.join(process.env.HOME ?? home, ".local", "state", "workit");
9831
+ }
9832
+ };
9833
+ var homePattern = new RegExp(`${escapeRegExp2(os3.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
9834
+
9835
+ // packages/workit-core/src/host-session.ts
9836
+ var HOST_SESSION_VARS = [
9837
+ ["OPENCODE_SESSION_ID", () => "opencode"],
9838
+ ["PI_SESSION_ID", () => "pi"],
9839
+ [
9840
+ "CODEX_THREAD_ID",
9841
+ (env) => env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop" || env.CODEX_ELECTRON_RESOURCES_PATH ? "codex_desktop" : "codex_cli"
9842
+ ]
9843
+ ];
9844
+ var family = (host) => host?.replace(/_(?:cli|desktop)$/, "") ?? null;
9845
+ function hostSessionFromEnv(env) {
9846
+ const value = (key) => env[key]?.trim() || null;
9847
+ let inner = null;
9848
+ for (const [key, host] of HOST_SESSION_VARS) {
9849
+ const session = value(key);
9850
+ if (session) {
9851
+ inner = { host: host(env), session };
9852
+ break;
9875
9853
  }
9876
9854
  }
9877
- return (row) => {
9878
- if (row.branch === branch)
9879
- return true;
9880
- const until = row.branch === null ? undefined : renamedAt.get(row.branch);
9881
- if (until === undefined)
9882
- return false;
9883
- const at = Date.parse(row.at);
9884
- return at >= since && at <= until;
9885
- };
9855
+ const declared = value("WORKIT_HOST");
9856
+ if (env.WORKIT_SESSION_ID !== undefined) {
9857
+ const inherited = inner !== null && declared !== null && family(declared) !== family(inner.host);
9858
+ if (!inherited)
9859
+ return { host: declared, session: value("WORKIT_SESSION_ID") };
9860
+ }
9861
+ return inner ? { host: inner.host, session: inner.session } : { host: declared, session: null };
9886
9862
  }
9887
- var renameAwareBranch = (scopeOf, branch) => {
9888
- return (target) => {
9889
- const own = typeof branch === "string" ? branch : null;
9890
- const theirs = typeof target.branch === "string" ? target.branch : null;
9891
- if (own === theirs)
9892
- return true;
9893
- if (own === null || theirs === null || typeof target.at !== "string")
9894
- return false;
9895
- return scopeOf(own)({ branch: theirs, at: target.at });
9896
- };
9897
- };
9898
- function authorSessions(cwd, rows, branch, range = { base: null, head: null }, scope = branchScope(cwd, branch)) {
9899
- const sessions = new Set;
9900
- for (const row of rows) {
9901
- if (!scope(row) || row.type !== "commit.recorded" && row.type !== "task.opened")
9902
- continue;
9903
- const session = str(row.session) ?? row.actor.session;
9904
- if (session)
9905
- sessions.add(session);
9863
+ var ISSUED_PER_LEAD = 64;
9864
+ var ISSUED_SESSIONS_MARKER = "workit-issued-sessions/3";
9865
+ var hash = (value) => createHash7("sha256").update(value).digest("hex").slice(0, 32);
9866
+ var userIssuedRoot = () => path8.join(resolveStateDir(), "issued");
9867
+ var userTokenRoot = () => path8.join(resolveStateDir(), "issued-tokens");
9868
+ function migrateRepoIssued(cwd) {
9869
+ if (cwd === undefined)
9870
+ return;
9871
+ let old;
9872
+ try {
9873
+ const location = resolveStore(cwd);
9874
+ if (location instanceof Error)
9875
+ return;
9876
+ old = path8.join(location.dir, "hooks", "issued");
9877
+ if (!fs5.existsSync(old))
9878
+ return;
9879
+ } catch {
9880
+ return;
9906
9881
  }
9907
- for (const session of trailerSessions(cwd, range.base, range.head))
9908
- sessions.add(session);
9909
- return sessions;
9882
+ for (const lead of safeList(old)) {
9883
+ try {
9884
+ const from = path8.join(old, lead);
9885
+ if (!fs5.statSync(from).isDirectory())
9886
+ continue;
9887
+ const to = path8.join(userIssuedRoot(), lead);
9888
+ fs5.mkdirSync(to, { recursive: true, mode: 448 });
9889
+ for (const id of fs5.readdirSync(from)) {
9890
+ try {
9891
+ fs5.copyFileSync(path8.join(from, id), path8.join(to, id), fs5.constants.COPYFILE_EXCL);
9892
+ } catch (error) {
9893
+ if (error.code !== "EEXIST")
9894
+ throw error;
9895
+ }
9896
+ fs5.rmSync(path8.join(from, id), { force: true });
9897
+ }
9898
+ removeIfEmpty(from);
9899
+ } catch {}
9900
+ }
9901
+ removeIfEmpty(old);
9910
9902
  }
9911
- var derivesFrom = (derived, bare) => !bare.includes(":") && derived.endsWith(`:${bare}`);
9912
- var authoredBy = (authors, session) => {
9913
- if (authors.has(session))
9914
- return true;
9915
- for (const author of authors)
9916
- if (derivesFrom(author, session) || derivesFrom(session, author))
9917
- return true;
9918
- return false;
9903
+ var safeList = (dir) => {
9904
+ try {
9905
+ return fs5.readdirSync(dir);
9906
+ } catch {
9907
+ return [];
9908
+ }
9919
9909
  };
9920
- var TRUST_REASONS = new Set([
9921
- "unsigned",
9922
- "hash_mismatch",
9923
- "broken_link",
9924
- "duplicate_id",
9925
- "unissued_session",
9926
- "unbound_session"
9927
- ]);
9928
- var SHELL_VOUCHED_HOSTS = new Set([
9929
- "cli",
9930
- "opencode",
9931
- "pi",
9932
- "codex_cli",
9933
- "codex_desktop"
9934
- ]);
9935
- function sessionTrust(cwd, row) {
9936
- const session = row.actor.session;
9937
- if (!session || row.self === true)
9938
- return null;
9939
- const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
9940
- const host = row.actor.host;
9941
- const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
9942
- if (!issuedOnly)
9910
+ var removeIfEmpty = (dir) => {
9911
+ try {
9912
+ fs5.rmdirSync(dir);
9913
+ } catch {}
9914
+ };
9915
+ var tokenHash = (token) => createHash7("sha256").update(token).digest("hex");
9916
+ function recordIssuedSession(cwd, lead, issued) {
9917
+ const token = randomBytes(18).toString("hex");
9918
+ try {
9919
+ migrateRepoIssued(cwd);
9920
+ const own = hash(issued);
9921
+ const tokens = path8.join(userTokenRoot(), hash(lead));
9922
+ fs5.mkdirSync(tokens, { recursive: true, mode: 448 });
9923
+ fs5.writeFileSync(path8.join(tokens, own), `${tokenHash(token)}
9924
+ `, { mode: 384, flag: "wx" });
9925
+ const dir = path8.join(userIssuedRoot(), hash(lead));
9926
+ fs5.mkdirSync(dir, { recursive: true, mode: 448 });
9927
+ fs5.writeFileSync(path8.join(dir, own), `${ISSUED_SESSIONS_MARKER}
9928
+ `, { mode: 384 });
9929
+ pruneIssued(dir, own, tokens);
9930
+ return token;
9931
+ } catch {
9943
9932
  return null;
9944
- const binding = issuedBinding(cwd, session);
9945
- if (binding === null)
9946
- return "unissued_session";
9947
- return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
9933
+ }
9948
9934
  }
9949
- function trustReasons(cwd, row) {
9950
- if (row.integrity === "unverified")
9951
- return [row.integrityReason ?? "unsigned"];
9952
- if (row.integrity !== "verified")
9953
- return ["unsigned"];
9954
- const session = sessionTrust(cwd, row);
9955
- return session ? [session] : [];
9935
+ function judgeBranch(cwd) {
9936
+ const branch = currentBranch(cwd);
9937
+ return branch && !isProtectedTarget(cwd, branch) ? branch : null;
9956
9938
  }
9957
- function withIntegrity(cwd, rows) {
9958
- if (rows.every((row) => row.integrity !== undefined))
9959
- return rows;
9960
- const read = readLedger(cwd, { integrity: true });
9961
- const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
9962
- return rows.map((row) => {
9963
- if (row.integrity !== undefined)
9964
- return row;
9965
- const fresh = bySeq.get(row.seq);
9966
- return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
9967
- });
9939
+ function verdictCommand(options) {
9940
+ const kind = options.judge === "verifier" ? "<live|unit>" : options.judge === "reviewer" ? "review" : "<live|unit|review>";
9941
+ return [
9942
+ "workit ledger verdict <verified|failed|blocked>",
9943
+ `--kind ${kind}`,
9944
+ `--branch ${options.branch ?? "<branch>"}`,
9945
+ options.session ? `--session ${options.session}` : null,
9946
+ options.token ? `--token=${options.token}` : null,
9947
+ '--how "<what you ran and saw>"'
9948
+ ].filter(Boolean).join(" ");
9968
9949
  }
9969
- var untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason));
9970
- var independenceReasons = (row, authors) => {
9971
- const reasons = [];
9972
- if (!row.actor.session)
9973
- reasons.push("no_session");
9974
- else if (authoredBy(authors, row.actor.session))
9975
- reasons.push("author_session");
9976
- if (row.self === true && !reasons.includes("no_session"))
9977
- reasons.push("self");
9978
- return reasons;
9979
- };
9980
- function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
9981
- let effective = null;
9982
- for (const row of rows) {
9983
- if (!effective) {
9984
- effective = row;
9985
- continue;
9950
+ function pruneIssued(dir, own, tokens) {
9951
+ try {
9952
+ const entries = fs5.readdirSync(dir);
9953
+ if (entries.length <= ISSUED_PER_LEAD)
9954
+ return;
9955
+ const byAge = entries.filter((name) => name !== own).map((name) => ({ name, at: fs5.statSync(path8.join(dir, name)).mtimeMs })).toSorted((a, b) => a.at - b.at);
9956
+ for (const { name } of byAge.slice(0, entries.length - ISSUED_PER_LEAD)) {
9957
+ fs5.rmSync(path8.join(dir, name), { force: true });
9958
+ fs5.rmSync(path8.join(tokens, name), { force: true });
9986
9959
  }
9987
- if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
9988
- continue;
9989
- const rowIndependent = reasonsOf(row).length === 0;
9990
- const effIndependent = reasonsOf(effective).length === 0;
9991
- if (effIndependent && !rowIndependent && isCurrent(effective))
9992
- continue;
9993
- if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
9994
- continue;
9995
- effective = row;
9996
- }
9997
- return effective;
9960
+ } catch {}
9998
9961
  }
9999
- function restackCarries(rows, branch, from, to) {
10000
- if (!from || !to)
10001
- return false;
10002
- const edges = new Map;
10003
- for (const row of rows)
10004
- if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.integrity === "verified" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
10005
- edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
10006
- const seen = new Set([from]);
10007
- const queue = [from];
10008
- while (queue.length) {
10009
- const next = queue.shift();
10010
- if (next === to)
10011
- return true;
10012
- for (const target of edges.get(next) ?? [])
10013
- if (!seen.has(target)) {
10014
- seen.add(target);
10015
- queue.push(target);
10016
- }
9962
+ var issuedRecord = (cwd, claimed) => {
9963
+ const cut = claimed.lastIndexOf(":");
9964
+ if (cut <= 0)
9965
+ return null;
9966
+ migrateRepoIssued(cwd);
9967
+ try {
9968
+ return fs5.readFileSync(path8.join(userIssuedRoot(), hash(claimed.slice(0, cut)), hash(claimed)), "utf8");
9969
+ } catch {
9970
+ return null;
10017
9971
  }
10018
- return false;
9972
+ };
9973
+ var TOKEN_HASH = /^([0-9a-f]{64})$/mu;
9974
+ var issuedToken = (claimed) => {
9975
+ const cut = claimed.lastIndexOf(":");
9976
+ if (cut <= 0)
9977
+ return { stored: null, leadBound: false };
9978
+ const dir = path8.join(userTokenRoot(), hash(claimed.slice(0, cut)));
9979
+ try {
9980
+ const text = fs5.readFileSync(path8.join(dir, hash(claimed)), "utf8");
9981
+ return { stored: TOKEN_HASH.exec(text)?.[1] ?? null, leadBound: true };
9982
+ } catch {
9983
+ return { stored: null, leadBound: safeList(dir).length > 0 };
9984
+ }
9985
+ };
9986
+ function issuedBinding(cwd, claimed) {
9987
+ const record = issuedRecord(cwd, claimed);
9988
+ const { stored, leadBound } = issuedToken(claimed);
9989
+ if (stored)
9990
+ return "token";
9991
+ if (record === null)
9992
+ return null;
9993
+ return leadBound ? "token" : "legacy";
10019
9994
  }
10020
- function checkVerdicts(cwd, branch, ledgerRows) {
10021
- const rows = withIntegrity(cwd, ledgerRows);
10022
- const head = branchHead(cwd, branch);
10023
- const fallbackBase = defaultBase(cwd);
10024
- const scope = branchScope(cwd, branch);
10025
- const keyCache = new Map;
10026
- const keysAt = (base, at) => {
10027
- if (!base || !at)
10028
- return { patchId: null, diffHash: null };
10029
- const cacheKey = `${base}\x00${at}`;
10030
- let key = keyCache.get(cacheKey);
10031
- if (!key) {
10032
- key = { patchId: patchId(cwd, base, at), diffHash: diffHash(cwd, base, at) };
10033
- keyCache.set(cacheKey, key);
10034
- }
10035
- return key;
10036
- };
10037
- const keyFor = (base) => keysAt(base, head);
10038
- const basisOf = (row) => {
10039
- const basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
10040
- return basis === "stale" && restackCarries(rows, branch, row.head, head) ? "carried" : basis;
10041
- };
10042
- const sameCode = (failed, row) => {
10043
- if (failed.head && failed.head === row.head)
10044
- return true;
10045
- const base = failed.base ?? fallbackBase;
10046
- const stored = (row.base ?? fallbackBase) === base && row.baseSha !== null && row.baseSha === failed.baseSha;
10047
- const keys = stored ? { patchId: row.patchId, diffHash: row.diffHash } : keysAt(base, row.head);
10048
- return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(rows, branch, failed.head, row.head);
10049
- };
10050
- const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
10051
- const failureAuthorCache = new Map;
10052
- const failureAuthors = (row) => {
10053
- let set = failureAuthorCache.get(row.seq);
10054
- if (!set) {
10055
- set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
10056
- failureAuthorCache.set(row.seq, set);
10057
- }
10058
- return set;
10059
- };
10060
- const reasonCache = new Map;
10061
- const reasonsOf = (row) => {
10062
- let reasons = reasonCache.get(row);
10063
- if (!reasons) {
10064
- const failing = FAILING.has(String(row.result));
10065
- reasons = [
10066
- ...trustReasons(cwd, row),
10067
- ...independenceReasons(row, failing ? failureAuthors(row) : authors)
10068
- ];
10069
- reasonCache.set(row, reasons);
10070
- }
10071
- return reasons;
10072
- };
10073
- const retired = new Set;
10074
- for (const row of rows)
10075
- if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
10076
- retired.add(row.supersedes);
10077
- const byKind = new Map;
10078
- for (const row of rows)
10079
- if (isLiveVerdict(row, retired) && scope(row)) {
10080
- const kind = typeof row.kind === "string" ? row.kind : "review";
10081
- byKind.set(kind, [...byKind.get(kind) ?? [], row]);
10082
- }
10083
- const verdicts = [];
10084
- const warnings = [];
10085
- for (const [kind, list] of byKind) {
10086
- const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
10087
- if (!row)
10088
- continue;
10089
- const basis = basisOf(row);
10090
- const current = basis !== "stale";
10091
- const independence = reasonsOf(row);
10092
- const reasons = [
10093
- ...current ? [] : ["stale"],
10094
- ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
10095
- ...independence
10096
- ];
10097
- verdicts.push({
10098
- kind,
10099
- basis,
10100
- current,
10101
- independent: independence.length === 0,
10102
- accepted: reasons.length === 0,
10103
- reasons,
10104
- verdict: row
10105
- });
10106
- if (reasons.length === 0) {
10107
- for (const later of list)
10108
- if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
10109
- 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`);
9995
+ var OWN_CHILD_SHELL = new Set(["opencode", "pi"]);
9996
+
9997
+ // packages/workit-core/src/hooks/standing-signal.ts
9998
+ import { createHash as createHash8 } from "node:crypto";
9999
+ import fs6 from "node:fs";
10000
+ import path9 from "node:path";
10001
+ var gitStoreDir = (cwd) => {
10002
+ try {
10003
+ for (let dir = path9.resolve(cwd);; dir = path9.dirname(dir)) {
10004
+ const dotGit = path9.join(dir, ".git");
10005
+ if (fs6.existsSync(dotGit)) {
10006
+ let gitDir = dotGit;
10007
+ if (fs6.statSync(dotGit).isFile()) {
10008
+ const target = /^gitdir:\s*(.+)$/mu.exec(fs6.readFileSync(dotGit, "utf8"))?.[1]?.trim();
10009
+ if (!target)
10010
+ return null;
10011
+ gitDir = path9.resolve(dir, target);
10012
+ const common = path9.join(gitDir, "commondir");
10013
+ if (fs6.existsSync(common))
10014
+ gitDir = path9.resolve(gitDir, fs6.readFileSync(common, "utf8").trim());
10015
+ }
10016
+ return path9.join(gitDir, "workit");
10017
+ }
10018
+ if (path9.dirname(dir) === dir)
10019
+ return null;
10110
10020
  }
10021
+ } catch {
10022
+ return null;
10111
10023
  }
10112
- for (const row of rows)
10113
- if (row.integrity === "unverified" && scope(row))
10114
- 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)`);
10115
- const tampered = ledgerTampering(cwd);
10116
- if (tampered.length) {
10117
- warnings.push(tamperedWarning(cwd, tampered));
10118
- for (const entry of verdicts) {
10119
- entry.accepted = false;
10120
- entry.reasons = [...entry.reasons, "ledger_tampered"];
10121
- }
10024
+ };
10025
+ var standingRevFile = (store) => path9.join(store, "standing", "rev");
10026
+ var standingSeenFile = (store, session, agentId) => path9.join(store, "standing", "seen", `${createHash8("sha256").update(`${session}\x00${agentId ?? ""}`).digest("hex").slice(0, 32)}.json`);
10027
+ var readStandingRev = (store) => {
10028
+ try {
10029
+ return fs6.readFileSync(standingRevFile(store), "utf8").trim() || null;
10030
+ } catch {
10031
+ return null;
10122
10032
  }
10123
- verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
10124
- const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
10125
- const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
10126
- const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
10127
- const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
10128
- accepted: false,
10129
- verdict: failing?.verdict ?? null,
10130
- reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
10131
- };
10132
- 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))));
10133
- const selfEntry = latestSelf && STRONG_RESULTS.has(String(latestSelf.verdict.result)) ? latestSelf : undefined;
10134
- return {
10135
- branch,
10136
- head,
10137
- review: accepted.accepted ? "verified" : selfEntry ? "self-reviewed" : "unreviewed",
10138
- selfVerdict: accepted.accepted ? null : selfEntry?.verdict ?? null,
10139
- base: newestCurrent?.verdict.base ?? fallbackBase,
10140
- patchId: keyFor(newestCurrent?.verdict.base ?? fallbackBase).patchId,
10141
- current: { basis: newestCurrent?.basis ?? "none", verdict: newestCurrent?.verdict ?? null },
10142
- accepted,
10143
- verdicts,
10144
- authors: [...authors].toSorted(),
10145
- warnings
10146
- };
10147
- }
10148
- var fromLead = (row, leads) => leads === null || row.actor.session !== null && leads.includes(row.actor.session);
10149
- function activeStanding(rows, fanout, leads = null) {
10150
- const active = [];
10151
- for (const row of rows) {
10152
- if (row.fanout !== fanout || !fromLead(row, leads))
10153
- continue;
10154
- if (row.type === "standing" && typeof row.what === "string" && row.what.trim()) {
10155
- if (!row.superseded)
10156
- active.push(row);
10157
- } else if (row.type === "standing.cleared") {
10158
- const target = str(row.target);
10159
- if (target === null)
10160
- active.length = 0;
10161
- else {
10162
- const index = active.findIndex((order) => order.id === target);
10163
- if (index >= 0)
10164
- active.splice(index, 1);
10165
- }
10166
- }
10033
+ };
10034
+ var readStandingSeen = (file) => {
10035
+ try {
10036
+ const value = JSON.parse(fs6.readFileSync(file, "utf8"));
10037
+ return {
10038
+ rev: typeof value.rev === "string" ? value.rev : null,
10039
+ ids: Array.isArray(value.ids) ? value.ids.filter((id) => typeof id === "string") : []
10040
+ };
10041
+ } catch {
10042
+ return { rev: null, ids: [] };
10167
10043
  }
10168
- return active;
10169
- }
10170
- var OBSERVED_ONLY = new Set([
10171
- "check",
10172
- "merge.unverified",
10173
- "policy.judged",
10174
- ...PR_ROW_TYPES
10175
- ]);
10176
-
10177
- // packages/workit-core/src/core/policy/lane.ts
10178
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
10179
- import path10 from "node:path";
10180
- // packages/workit-core/src/forge/redact.ts
10181
- 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_.-]*`;
10182
- var ASSIGNMENT = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*["']?)((?:bearer|basic|token|digest)\s+)?([^\s"',;&]+)`, "giu");
10183
- var QUOTED_ASSIGNMENT = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*)(["'])([^"'\n]*[^\s"'][^"'\n]*)\2`, "giu");
10184
- var ANSI = new RegExp(String.raw`\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007]*\u0007`, "gu");
10185
-
10186
- // packages/workit-core/src/git/ops.ts
10187
- var COMMIT_TIMEOUT_MS = 10 * 60000;
10044
+ };
10045
+ var writeStandingSeen = (file, seen) => {
10046
+ try {
10047
+ fs6.mkdirSync(path9.dirname(file), { recursive: true });
10048
+ fs6.writeFileSync(file, JSON.stringify(seen));
10049
+ } catch {}
10050
+ };
10188
10051
 
10189
- // packages/workit-core/src/core/policy/attributes.ts
10190
- function patternRegex(dir, pattern) {
10191
- if (!pattern || pattern.endsWith("/") || pattern.startsWith('"') || pattern.startsWith("!"))
10052
+ // packages/workit-core/src/ledger.ts
10053
+ var LEDGER_VERSION = 1;
10054
+ var MAX_LINE_BYTES = 4096;
10055
+ var PASSING = new Set(["verified", "tests-verified", "type-check-only"]);
10056
+ var FAILING = new Set(["failed", "blocked"]);
10057
+ var STRONG_RESULTS = new Set(["verified", "tests-verified"]);
10058
+ var PR_ROW_TYPES = new Set([
10059
+ "pr.created",
10060
+ "pr.status",
10061
+ "pr.merged",
10062
+ "pr.linked"
10063
+ ]);
10064
+ var err = (code, error, unblock) => ({
10065
+ ok: false,
10066
+ code,
10067
+ error,
10068
+ ...unblock ? { unblock } : {}
10069
+ });
10070
+ var gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
10071
+ const run = spawnSync8("git", args, {
10072
+ cwd,
10073
+ encoding: "utf8",
10074
+ timeout: timeoutMs,
10075
+ killSignal: "SIGKILL",
10076
+ windowsHide: true,
10077
+ maxBuffer: 256 * 1024 * 1024,
10078
+ env: {
10079
+ ...process.env,
10080
+ GIT_TERMINAL_PROMPT: "0",
10081
+ GIT_OPTIONAL_LOCKS: "0",
10082
+ LC_ALL: "C",
10083
+ LANGUAGE: "C"
10084
+ }
10085
+ });
10086
+ return {
10087
+ ok: run.status === 0,
10088
+ stdout: run.stdout ?? "",
10089
+ stderr: run.error ? String(run.error.message) : run.stderr ?? ""
10090
+ };
10091
+ };
10092
+ var git3 = (cwd, args) => {
10093
+ const run = gitRun2(cwd, args);
10094
+ const value = run.ok ? run.stdout.trim() : "";
10095
+ return value ? value : null;
10096
+ };
10097
+ var safeRef2 = (value) => value.length > 0 && !value.startsWith("-");
10098
+ function resolveCommit(cwd, ref) {
10099
+ if (!safeRef2(ref))
10192
10100
  return null;
10193
- const anchored = pattern.includes("/");
10194
- const body = pattern.replace(/^\//u, "");
10195
- let source = "";
10196
- for (let index = 0;index < body.length; index += 1) {
10197
- const char = body[index];
10198
- if (char === "*" && body[index + 1] === "*") {
10199
- const slash = body[index + 2] === "/";
10200
- source += slash ? "(?:.*/)?" : ".*";
10201
- index += slash ? 2 : 1;
10202
- } else if (char === "*")
10203
- source += "[^/]*";
10204
- else if (char === "?")
10205
- source += "[^/]";
10206
- else if (char === "[") {
10207
- const end = body.indexOf("]", index + 2);
10208
- if (end < 0)
10209
- return null;
10210
- const set = body.slice(index + 1, end).replace(/^!/u, "^").replace(/\\/gu, "\\\\");
10211
- source += `[${set}]`;
10212
- index = end;
10213
- } else if (char === "\\" && index + 1 < body.length) {
10214
- source += body[index + 1].replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
10215
- index += 1;
10216
- } else
10217
- source += char.replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
10218
- }
10219
- const prefix = dir ? `${dir.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}/` : "";
10220
- try {
10221
- return new RegExp(anchored ? `^${prefix}${source}$` : `^${prefix}(?:.*/)?${source}$`, "u");
10222
- } catch {
10101
+ return git3(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]);
10102
+ }
10103
+ function branchHead(cwd, branch) {
10104
+ if (!safeRef2(branch))
10223
10105
  return null;
10224
- }
10106
+ return resolveCommit(cwd, `refs/heads/${branch}`) ?? resolveCommit(cwd, `refs/remotes/origin/${branch}`);
10225
10107
  }
10226
- function rulesOf(dir, text) {
10227
- const out = [];
10228
- for (const raw of text.split(`
10108
+ function diffHash(cwd, base, head) {
10109
+ if (!safeRef2(base) || !safeRef2(head))
10110
+ return null;
10111
+ const diff = gitRun2(cwd, [
10112
+ "diff",
10113
+ "-U3",
10114
+ "--diff-algorithm=myers",
10115
+ "--indent-heuristic",
10116
+ "--no-color",
10117
+ "--no-ext-diff",
10118
+ "--no-textconv",
10119
+ "--no-renames",
10120
+ "--full-index",
10121
+ "--binary",
10122
+ "--src-prefix=a/",
10123
+ "--dst-prefix=b/",
10124
+ `${base}...${head}`,
10125
+ "--"
10126
+ ], GIT_TIMEOUTS.worktree);
10127
+ if (!diff.ok || !diff.stdout)
10128
+ return null;
10129
+ const hash = createHash9("sha256");
10130
+ for (const line of diff.stdout.split(`
10229
10131
  `)) {
10230
- const line = raw.trim();
10231
- if (!line || line.startsWith("#") || line.startsWith("[attr]"))
10232
- continue;
10233
- const [pattern, ...attributes] = line.split(/\s+/u);
10234
- let generated = null;
10235
- for (const attribute of attributes) {
10236
- if (attribute === "linguist-generated" || attribute === "linguist-generated=true")
10237
- generated = true;
10238
- else if (attribute === "-linguist-generated" || attribute === "!linguist-generated" || attribute.startsWith("linguist-generated="))
10239
- generated = false;
10240
- }
10241
- if (generated === null)
10132
+ if (line.startsWith("index "))
10242
10133
  continue;
10243
- const regex = patternRegex(dir, pattern);
10244
- if (regex)
10245
- out.push({ regex, generated });
10134
+ hash.update(line.startsWith("@@ ") ? "@@" : line);
10135
+ hash.update(`
10136
+ `);
10246
10137
  }
10247
- return out;
10138
+ return `sha256:${hash.digest("hex")}`;
10248
10139
  }
10249
- function generatedByAttributes(files, paths) {
10250
- const rules = [...files].map(([file, text]) => {
10251
- const dir = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
10252
- return {
10253
- depth: dir ? dir.split("/").length : 0,
10254
- rules: rulesOf(dir, text)
10255
- };
10256
- }).toSorted((left, right) => left.depth - right.depth).flatMap((entry) => entry.rules);
10257
- const out = new Set;
10258
- for (const file of paths) {
10259
- const last = rules.findLast((rule) => rule.regex.test(file));
10260
- if (last?.generated)
10261
- out.add(file);
10262
- }
10263
- return out;
10140
+ function storeRoot(cwd) {
10141
+ const location = resolveStore(path10.resolve(cwd));
10142
+ if (location instanceof Error)
10143
+ 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");
10144
+ return { ok: true, value: { root: location.dir, shared: location.shared } };
10264
10145
  }
10265
-
10266
- // packages/workit-core/src/core/policy/substance.ts
10267
- var SLASH_BLOCK = [["/*", "*/"]];
10268
- var C_LIKE = { line: ["//"], block: SLASH_BLOCK, quotes: `"'` };
10269
- var SYNTAXES = Object.fromEntries([
10270
- ...["ts", "mts", "cts", "js", "mjs", "cjs"].map((ext) => [ext, { ...C_LIKE, js: true }]),
10271
- ...["java", "kt", "kts", "scala", "swift", "dart", "groovy"].map((ext) => [
10272
- ext,
10273
- { ...C_LIKE, triple: true }
10274
- ]),
10275
- ["go", { ...C_LIKE, rawBacktick: true }],
10276
- ...["css", "scss", "less"].map((ext) => [
10277
- ext,
10278
- { line: [], block: SLASH_BLOCK, quotes: `"'`, cssUrl: true }
10279
- ]),
10280
- ...["py", "pyi"].map((ext) => [
10281
- ext,
10282
- { line: ["#"], block: [], quotes: `"'`, triple: true, indented: true, fstring: true }
10283
- ]),
10284
- ["toml", { line: ["#"], block: [], quotes: `"`, literal: "'", triple: true }],
10285
- ...["html", "htm", "xml", "svg", "xhtml"].map((ext) => [
10286
- ext,
10287
- { line: [], block: [["<!--", "-->"]], quotes: "", indented: true }
10288
- ])
10146
+ function ledgerPath(cwd) {
10147
+ const root = storeRoot(cwd);
10148
+ return root.ok ? { ok: true, value: path10.join(root.value.root, "ledger", "ledger.jsonl") } : root;
10149
+ }
10150
+ var newId2 = () => `${Date.now().toString(36)}-${randomBytes2(6).toString("hex")}`;
10151
+ var NETWORK_FS_TYPES = new Set([
10152
+ 26985,
10153
+ 20859,
10154
+ 4266872130,
10155
+ 4283649346,
10156
+ 1702057286,
10157
+ 1397113167,
10158
+ 12805120,
10159
+ 16914839,
10160
+ 1196443219,
10161
+ 198183888
10289
10162
  ]);
10290
- 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;
10291
- var extensionOf = (file) => {
10292
- const name = file.slice(file.lastIndexOf("/") + 1);
10293
- const dot = name.lastIndexOf(".");
10294
- return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
10163
+ var isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0);
10164
+ var onNetworkFs = (dir) => {
10165
+ if (process.platform !== "linux")
10166
+ return false;
10167
+ try {
10168
+ return isNetworkFsType(fs7.statfsSync(dir).type);
10169
+ } catch {
10170
+ return false;
10171
+ }
10295
10172
  };
10296
- var CI_PATH = /^\.github\/|(?:^|\/)(?:\.gitlab-ci\.ya?ml|Jenkinsfile|azure-pipelines\.ya?ml|\.pre-commit-config\.ya?ml|\.circleci\/.*)$/u;
10297
- var syntaxOf = (file) => CI_PATH.test(file) ? null : SYNTAXES[extensionOf(file)] ?? null;
10298
- var SPACE = /[ \t\r\f\v]/u;
10299
- var WORD = /[\w$]/u;
10300
- var NO_SPACE_AFTER = "([{,;";
10301
- var NO_SPACE_BEFORE = ")]},;";
10302
- var REGEX_AFTER = "(,=:[!&|?{};+-*%<>~^";
10303
- var REGEX_KEYWORDS = new Set([
10304
- "return",
10305
- "typeof",
10306
- "instanceof",
10307
- "case",
10308
- "do",
10309
- "else",
10310
- "in",
10311
- "of",
10312
- "new",
10313
- "delete",
10314
- "void",
10315
- "throw",
10316
- "yield",
10317
- "await"
10318
- ]);
10319
- function substanceOf(file, text) {
10320
- const syntax = syntaxOf(file);
10321
- if (!syntax)
10322
- return plainSubstance(text);
10323
- const markup = syntax.block.some(([open]) => open === "<!--");
10324
- if (markup && (/<(?:script|style)\b|<!\[CDATA\[|<\?/iu.test(text) || /[^>\s][ \t]*<!--/u.test(text)))
10325
- return plainSubstance(text);
10326
- return lex(syntax, text);
10327
- }
10328
- function plainSubstance(text) {
10329
- return text.split(`
10330
- `).map((line) => line.replace(/[ \t\r\f\v]+$/u, "")).filter(Boolean).join(`
10331
- `);
10332
- }
10333
- function lex(syntax, text) {
10334
- const collapse = !syntax.indented;
10335
- let out = "";
10336
- let lineStart = 0;
10337
- let kept = 0;
10338
- let pendingSpace = false;
10339
- const frames = [{ kind: "code", braces: 0 }];
10340
- let index = 0;
10341
- if (text.startsWith("#!")) {
10342
- const end = text.indexOf(`
10343
- `);
10344
- out = end < 0 ? text : text.slice(0, end);
10345
- kept = out.length;
10346
- index = end < 0 ? text.length : end;
10173
+ var LOCK_WAIT_MS = 5000;
10174
+ var LOCK_STALE_MS = 30000;
10175
+ var LOCK_OWNER = "owner";
10176
+ var lockToken = (lock) => {
10177
+ try {
10178
+ return fs7.readFileSync(path10.join(lock, LOCK_OWNER), "utf8");
10179
+ } catch {
10180
+ return null;
10347
10181
  }
10348
- const emitCode = (chunk) => {
10349
- if (collapse && pendingSpace && out.length > lineStart) {
10350
- const before = out[out.length - 1];
10351
- if (!NO_SPACE_AFTER.includes(before) && !NO_SPACE_BEFORE.includes(chunk[0]))
10352
- out += " ";
10353
- }
10354
- pendingSpace = false;
10355
- out += chunk;
10356
- };
10357
- const emitVerbatim = (chunk) => {
10358
- emitCode(chunk);
10359
- kept = out.length;
10360
- };
10361
- const newline = () => {
10362
- if (!collapse) {
10363
- let end = out.length;
10364
- while (end > Math.max(lineStart, kept) && SPACE.test(out[end - 1]))
10365
- end -= 1;
10366
- out = out.slice(0, end);
10367
- }
10368
- pendingSpace = false;
10369
- if (out.length > lineStart) {
10370
- out += `
10371
- `;
10372
- lineStart = out.length;
10373
- }
10374
- };
10375
- const regexAllowed = () => {
10376
- let end = out.length;
10377
- while (end > 0 && /\s/u.test(out[end - 1]))
10378
- end -= 1;
10379
- if (end === 0 || REGEX_AFTER.includes(out[end - 1]))
10380
- return true;
10381
- let start = end;
10382
- while (start > 0 && WORD.test(out[start - 1]))
10383
- start -= 1;
10384
- return REGEX_KEYWORDS.has(out.slice(start, end));
10385
- };
10386
- const scanString = (quote, escapes, multiline) => {
10387
- const prefix = text.slice(Math.max(0, index - 2), index).match(/[a-z]*$/iu)?.[0] ?? "";
10388
- const fstring = syntax.fstring === true && /f/iu.test(prefix);
10389
- let end = index + quote.length;
10390
- while (end < text.length) {
10391
- const char = text[end];
10392
- if (escapes && char === "\\") {
10393
- end += 2;
10394
- continue;
10395
- }
10396
- if (text.startsWith(quote, end)) {
10397
- if (fstring && unbalanced(text.slice(index + quote.length, end)))
10398
- return false;
10399
- emitVerbatim(text.slice(index, end + quote.length));
10400
- index = end + quote.length;
10401
- return true;
10402
- }
10403
- if (char === `
10404
- ` && !multiline)
10405
- return false;
10406
- end += 1;
10182
+ };
10183
+ var lockAgeMs = (lock) => {
10184
+ for (const target of [path10.join(lock, LOCK_OWNER), lock]) {
10185
+ try {
10186
+ return Date.now() - fs7.statSync(target).mtimeMs;
10187
+ } catch {}
10188
+ }
10189
+ return null;
10190
+ };
10191
+ var ledgerLock = {
10192
+ acquire(lock) {
10193
+ try {
10194
+ fs7.mkdirSync(lock);
10195
+ } catch (error) {
10196
+ if (error.code === "EEXIST")
10197
+ return null;
10198
+ throw error;
10407
10199
  }
10408
- return false;
10409
- };
10410
- while (index < text.length) {
10411
- const frame = frames[frames.length - 1];
10412
- const char = text[index];
10413
- if (frame.kind === "template") {
10414
- if (char === "\\") {
10415
- emitVerbatim(text.slice(index, index + 2));
10416
- index += 2;
10417
- } else if (char === "`") {
10418
- emitVerbatim(char);
10419
- frames.pop();
10420
- index += 1;
10421
- } else if (text.startsWith("${", index)) {
10422
- emitVerbatim("${");
10423
- frames.push({ kind: "code", braces: 0 });
10424
- index += 2;
10425
- } else {
10426
- out += char;
10427
- kept = out.length;
10428
- index += 1;
10429
- }
10430
- continue;
10200
+ const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
10201
+ fs7.writeFileSync(path10.join(lock, LOCK_OWNER), token);
10202
+ return token;
10203
+ },
10204
+ release(lock, token) {
10205
+ if (lockToken(lock) !== token)
10206
+ return false;
10207
+ const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
10208
+ try {
10209
+ fs7.renameSync(lock, tomb);
10210
+ } catch {
10211
+ return false;
10431
10212
  }
10432
- if (char === `
10433
- `) {
10434
- newline();
10435
- index += 1;
10436
- continue;
10213
+ if (lockToken(tomb) !== token) {
10214
+ try {
10215
+ fs7.renameSync(tomb, lock);
10216
+ } catch {}
10217
+ return false;
10437
10218
  }
10438
- if (SPACE.test(char)) {
10439
- if (collapse)
10440
- pendingSpace = true;
10441
- else
10442
- out += char;
10443
- index += 1;
10444
- continue;
10219
+ fs7.rmSync(tomb, { recursive: true, force: true });
10220
+ return true;
10221
+ },
10222
+ token: lockToken,
10223
+ reap(lock, observed) {
10224
+ const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
10225
+ try {
10226
+ fs7.renameSync(lock, tomb);
10227
+ } catch {
10228
+ return false;
10445
10229
  }
10446
- const lineOpener = syntax.line.find((opener) => text.startsWith(opener, index));
10447
- if (lineOpener) {
10448
- const end = text.indexOf(`
10449
- `, index);
10450
- const comment = text.slice(index, end < 0 ? text.length : end);
10451
- if (DIRECTIVE.test(comment.slice(lineOpener.length)))
10452
- emitVerbatim(comment);
10453
- else
10454
- pendingSpace = true;
10455
- index = end < 0 ? text.length : end;
10456
- continue;
10230
+ if (lockToken(tomb) === observed) {
10231
+ fs7.rmSync(tomb, { recursive: true, force: true });
10232
+ return true;
10457
10233
  }
10458
- const block = syntax.block.find(([open]) => text.startsWith(open, index));
10459
- if (block) {
10460
- const end = text.indexOf(block[1], index + block[0].length);
10461
- if (end < 0)
10462
- return null;
10463
- const comment = text.slice(index, end + block[1].length);
10464
- if (DIRECTIVE.test(comment.slice(block[0].length, -block[1].length)))
10465
- emitVerbatim(comment);
10466
- else
10467
- pendingSpace = true;
10468
- if (comment.includes(`
10469
- `))
10470
- newline();
10471
- index = end + block[1].length;
10472
- continue;
10234
+ try {
10235
+ fs7.renameSync(tomb, lock);
10236
+ } catch {
10237
+ fs7.rmSync(tomb, { recursive: true, force: true });
10473
10238
  }
10474
- if (syntax.triple && (char === '"' || char === "'") && text.startsWith(char.repeat(3), index)) {
10475
- if (!scanString(char.repeat(3), !syntax.literal?.includes(char), true))
10476
- return null;
10239
+ return false;
10240
+ }
10241
+ };
10242
+ function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
10243
+ const lock = `${file}.lock`;
10244
+ const deadline = Date.now() + waitMs;
10245
+ let token = null;
10246
+ for (;; ) {
10247
+ token = ledgerLock.acquire(lock);
10248
+ if (token)
10249
+ break;
10250
+ const observed = lockToken(lock);
10251
+ const age = lockAgeMs(lock);
10252
+ if (age !== null && age > LOCK_STALE_MS && ledgerLock.reap(lock, observed))
10477
10253
  continue;
10478
- }
10479
- if (syntax.quotes.includes(char)) {
10480
- if (!scanString(char, true, Boolean(syntax.multiline)))
10254
+ if (Date.now() >= deadline)
10255
+ return null;
10256
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
10257
+ }
10258
+ try {
10259
+ return fn();
10260
+ } finally {
10261
+ ledgerLock.release(lock, token);
10262
+ }
10263
+ }
10264
+ var sha2562 = (text) => createHash9("sha256").update(text).digest("hex");
10265
+ var chainHash = (prevHash, body) => sha2562(`${prevHash ?? ""}
10266
+ ${body}`);
10267
+ var TAIL_BYTES = 64 * 1024;
10268
+ var lastLine = (fd, size) => {
10269
+ for (let span = Math.min(size, TAIL_BYTES);span > 0; span = Math.min(size, span * 2)) {
10270
+ const buffer = Buffer.alloc(span);
10271
+ fs7.readSync(fd, buffer, 0, span, size - span);
10272
+ const end = buffer.lastIndexOf(10);
10273
+ if (end < 0) {
10274
+ if (span === size)
10481
10275
  return null;
10482
10276
  continue;
10483
10277
  }
10484
- if (syntax.literal?.includes(char)) {
10485
- if (!scanString(char, false, false))
10486
- return null;
10487
- continue;
10488
- }
10489
- if (syntax.rawBacktick && char === "`") {
10490
- if (!scanString("`", false, true))
10491
- return null;
10492
- continue;
10493
- }
10494
- if (syntax.js && char === "`") {
10495
- emitVerbatim(char);
10496
- frames.push({ kind: "template" });
10497
- index += 1;
10498
- continue;
10499
- }
10500
- if (syntax.cssUrl && /^url\(\s*[^"')\s]/iu.test(text.slice(index, index + 64)))
10501
- return null;
10502
- if (syntax.js && char === "/" && !regexAllowed() && out.trimEnd().endsWith(")")) {
10503
- const lineEnd = text.indexOf(`
10504
- `, index);
10505
- if (text.slice(index + 1, lineEnd < 0 ? text.length : lineEnd).includes("/"))
10506
- return null;
10507
- }
10508
- if (syntax.js && char === "/" && regexAllowed()) {
10509
- let end = index + 1;
10510
- let inClass = false;
10511
- while (end < text.length && (inClass || text[end] !== "/")) {
10512
- if (text[end] === `
10513
- `)
10278
+ let stop = end;
10279
+ let text = "";
10280
+ while (stop >= 0) {
10281
+ const start = buffer.lastIndexOf(10, stop - 1);
10282
+ text = buffer.toString("utf8", start + 1, stop).trim();
10283
+ if (text) {
10284
+ if (start >= 0 || span === size)
10285
+ return text;
10286
+ break;
10287
+ }
10288
+ if (start < 0) {
10289
+ if (span === size)
10514
10290
  return null;
10515
- if (text[end] === "\\")
10516
- end += 1;
10517
- else if (text[end] === "[")
10518
- inClass = true;
10519
- else if (text[end] === "]")
10520
- inClass = false;
10521
- end += 1;
10291
+ break;
10522
10292
  }
10523
- if (end >= text.length)
10524
- return null;
10525
- emitVerbatim(text.slice(index, end + 1));
10526
- index = end + 1;
10527
- continue;
10293
+ stop = start;
10528
10294
  }
10529
- if (frames.length > 1 && frame.kind === "code") {
10530
- if (char === "{")
10531
- frame.braces += 1;
10532
- else if (char === "}") {
10533
- if (frame.braces === 0) {
10534
- emitVerbatim(char);
10535
- frames.pop();
10536
- index += 1;
10537
- continue;
10538
- }
10539
- frame.braces -= 1;
10295
+ if (span === size)
10296
+ return text || null;
10297
+ }
10298
+ return null;
10299
+ };
10300
+ var tighten = (fd) => {
10301
+ try {
10302
+ if ((fs7.fstatSync(fd).mode & 63) !== 0)
10303
+ fs7.fchmodSync(fd, 384);
10304
+ } catch {}
10305
+ };
10306
+ function appendLine(file, build, options) {
10307
+ const write = () => {
10308
+ const fd = fs7.openSync(file, "a+", 384);
10309
+ try {
10310
+ if (process.platform !== "win32")
10311
+ tighten(fd);
10312
+ const size = fs7.fstatSync(fd).size;
10313
+ let fence = "";
10314
+ if (size > 0) {
10315
+ const last = Buffer.alloc(1);
10316
+ fs7.readSync(fd, last, 0, 1, size - 1);
10317
+ if (last[0] !== 10)
10318
+ fence = `
10319
+ `;
10540
10320
  }
10321
+ const line = build(lastLine(fd, size));
10322
+ if (!line.ok)
10323
+ return line;
10324
+ const bytes = Buffer.from(fence + line.value);
10325
+ const written = fs7.writeSync(fd, bytes);
10326
+ if (written !== bytes.length)
10327
+ return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
10328
+ return { ok: true, value: undefined };
10329
+ } finally {
10330
+ fs7.closeSync(fd);
10541
10331
  }
10542
- emitCode(char);
10543
- index += 1;
10332
+ };
10333
+ try {
10334
+ fs7.mkdirSync(path10.dirname(file), { recursive: true });
10335
+ if (!options.forceLock && !onNetworkFs(path10.dirname(file)))
10336
+ return write();
10337
+ const locked = withLedgerLock(file, write);
10338
+ return locked ?? err("busy", `ledger lock ${file}.lock is held`, "retry; remove the .lock directory if no workit process is running");
10339
+ } catch (error) {
10340
+ return err("unavailable", `cannot append to ${file}: ${error.message}`);
10544
10341
  }
10545
- if (frames.length !== 1)
10546
- return null;
10547
- newline();
10548
- return out;
10549
10342
  }
10550
- function unbalanced(body) {
10551
- let depth = 0;
10552
- for (const part of body.replaceAll("{{", "").replaceAll("}}", "")) {
10553
- if (part === "{")
10554
- depth += 1;
10555
- else if (part === "}")
10556
- depth = Math.max(0, depth - 1);
10557
- }
10558
- return depth > 0;
10343
+ var sealed = (row, id, at, previous) => {
10344
+ const { prevHash: _prev, rowHash: _row, ...fields } = row;
10345
+ const prevHash = previous === null ? null : sha2562(previous);
10346
+ const body = { v: LEDGER_VERSION, id, at, ...fields, prevHash };
10347
+ const full = { ...body, rowHash: chainHash(prevHash, JSON.stringify(body)) };
10348
+ return { full, line: `${JSON.stringify(full)}
10349
+ ` };
10350
+ };
10351
+ function appendRaw(cwd, row, options) {
10352
+ const file = ledgerPath(cwd);
10353
+ if (!file.ok)
10354
+ return file;
10355
+ const id = newId2();
10356
+ const at = (options.now ?? new Date).toISOString();
10357
+ let full = null;
10358
+ const written = appendLine(file.value, (previous) => {
10359
+ const next = sealed(row, id, at, previous);
10360
+ const bytes = Buffer.byteLength(next.line);
10361
+ if (bytes > MAX_LINE_BYTES)
10362
+ 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>`);
10363
+ full = next.full;
10364
+ return { ok: true, value: next.line };
10365
+ }, options);
10366
+ return written.ok ? { ok: true, value: full } : written;
10559
10367
  }
10560
- function hasOpaqueLineBreak(text) {
10561
- return /\r(?!\n)|[\u2028\u2029]/u.test(text);
10368
+ function observedRowBytes(row) {
10369
+ const sample = sealed({ ...row, observer: "workit_cli" }, newId2(), new Date().toISOString(), "");
10370
+ return Buffer.byteLength(sample.line);
10562
10371
  }
10563
- var decoder = new TextDecoder("utf-8", { fatal: true });
10564
- function textOf(bytes) {
10565
- if (bytes.includes(0))
10566
- return null;
10567
- try {
10568
- return decoder.decode(bytes);
10569
- } catch {
10570
- return null;
10571
- }
10372
+ function appendObserved(cwd, row, options = {}) {
10373
+ return appendRaw(cwd, { ...row, observer: "workit_cli" }, options);
10572
10374
  }
10573
- function cosmeticChange(file, before, after) {
10574
- const left = textOf(before);
10575
- const right = textOf(after);
10576
- if (left === null || right === null)
10577
- return false;
10578
- if (hasOpaqueLineBreak(left) || hasOpaqueLineBreak(right))
10579
- return false;
10580
- const a = substanceOf(file, left);
10581
- return a !== null && a === substanceOf(file, right);
10375
+ function appendHookObserved(cwd, row) {
10376
+ return appendRaw(cwd, { ...row, observer: "host_hook" }, {});
10582
10377
  }
10583
-
10584
- // packages/workit-core/src/core/policy/lane.ts
10585
- var LANE_THRESHOLDS = {
10586
- selfMaxLines: 100,
10587
- selfMaxFiles: 5,
10588
- fullMinLines: 500
10378
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10379
+ var str = (value) => typeof value === "string" ? value : null;
10380
+ var normalizeActor = (value) => {
10381
+ const actor = isRecord2(value) ? value : {};
10382
+ return {
10383
+ host: str(actor.host) ?? "unknown",
10384
+ session: str(actor.session),
10385
+ agentId: str(actor.agentId)
10386
+ };
10589
10387
  };
10590
- var RISK_TOKENS = new Set([
10591
- "auth",
10592
- "authn",
10593
- "authz",
10594
- "oauth",
10595
- "security",
10596
- "permission",
10597
- "permissions",
10598
- "secret",
10599
- "secrets",
10600
- "crypto",
10601
- "gate",
10602
- "gates",
10603
- "policy",
10604
- "policies",
10605
- "hook",
10606
- "hooks",
10607
- "husky",
10608
- "lefthook",
10609
- "release",
10610
- "releases",
10611
- "migration",
10612
- "migrations",
10613
- "migrate",
10614
- "workflows"
10615
- ]);
10616
- var RISK_FILES = new Set([
10617
- ".gitlab-ci.yml",
10618
- "Jenkinsfile",
10619
- "azure-pipelines.yml",
10620
- ".pre-commit-config.yaml",
10621
- ".workit/risk-paths"
10622
- ]);
10623
- var RISK_NAMES = new Set([".gitattributes"]);
10624
- var RISK_PATHS_FILE = ".workit/risk-paths";
10625
- var LOCKFILES = new Set([
10626
- "bun.lock",
10627
- "bun.lockb",
10628
- "package-lock.json",
10629
- "npm-shrinkwrap.json",
10630
- "yarn.lock",
10631
- "pnpm-lock.yaml",
10632
- "Cargo.lock",
10633
- "go.sum",
10634
- "poetry.lock",
10635
- "uv.lock",
10636
- "Pipfile.lock",
10637
- "Gemfile.lock",
10638
- "composer.lock"
10639
- ]);
10640
- var DOC_EXT = /\.(?:md|mdx|markdown|rst|adoc|txt)$/iu;
10641
- var DOC_NAMES = /^(?:license|licence|notice|authors|changelog|changes|contributors)$/iu;
10642
- var INSTRUCTIONS = /(?:^|\/)(?:AGENTS|CLAUDE|GEMINI|SKILL)\.md$|(?:^|\/)(?:skills|agents|commands|rules|prompts)\/[^/]+/iu;
10643
- var TEST_PATH = /(?:^|\/)(?:test|tests|__tests__|spec|specs|fixtures)\/|\.(?:test|spec)\.[^/]+$/iu;
10644
- var basename = (file) => file.slice(file.lastIndexOf("/") + 1);
10645
- var isDoc = (file) => !INSTRUCTIONS.test(file) && (DOC_EXT.test(file) || DOC_NAMES.test(basename(file)));
10646
- function kindOf(file) {
10647
- if (file.binary)
10648
- return "behavior";
10649
- if (file.generated)
10650
- return "generated";
10651
- if (LOCKFILES.has(basename(file.path)))
10652
- return "lockfile";
10653
- if (isDoc(file.path))
10654
- return "doc";
10655
- if (file.cosmetic)
10656
- return "cosmetic";
10657
- return TEST_PATH.test(file.path) ? "test" : "behavior";
10388
+ function supersedeAllowed(by, target, onBranch = (row) => (by.branch ?? null) === (row.branch ?? null)) {
10389
+ if (by.type !== target.type)
10390
+ return `a ${by.type} row cannot supersede a ${target.type} row`;
10391
+ if (!by.actor.session || by.actor.session !== target.actor.session)
10392
+ return "only the session that wrote a row can supersede it";
10393
+ if (!onBranch(target))
10394
+ return `the row is on another branch (${String(target.branch ?? "none")}); a row supersedes only one on its own branch`;
10395
+ if (by.type === "verdict" && (by.kind ?? "review") !== (target.kind ?? "review"))
10396
+ return `a ${String(by.kind ?? "review")} verdict cannot supersede a ${String(target.kind ?? "review")} verdict; record it with --kind ${String(target.kind ?? "review")}`;
10397
+ if (by.type === "verdict" && by.self === true && target.self !== true)
10398
+ return "a self verdict cannot replace an independent verdict";
10399
+ return null;
10658
10400
  }
10659
- var globCache = new Map;
10660
- function globMatch(pattern, file) {
10661
- let regex = globCache.get(pattern);
10662
- if (!regex) {
10663
- let source = "";
10664
- for (let index = 0;index < pattern.length; index += 1) {
10665
- const char = pattern[index];
10666
- if (char === "*" && pattern[index + 1] === "*") {
10667
- source += pattern[index + 2] === "/" ? "(?:[^/]*/)*" : ".*";
10668
- index += pattern[index + 2] === "/" ? 2 : 1;
10669
- } else if (char === "*")
10670
- source += "[^/]*";
10671
- else if (char === "?")
10672
- source += "[^/]";
10673
- else
10674
- source += char.replace(/[.+^${}()|[\]\\]/gu, "\\$&");
10401
+ function readLedger(cwd, options = {}) {
10402
+ const file = ledgerPath(cwd);
10403
+ if (!file.ok)
10404
+ return file;
10405
+ let text = "";
10406
+ let stat = null;
10407
+ try {
10408
+ stat = fs7.statSync(file.value);
10409
+ text = fs7.readFileSync(file.value, "utf8");
10410
+ } catch (error) {
10411
+ if (error.code === "ENOENT")
10412
+ return {
10413
+ ok: true,
10414
+ value: {
10415
+ path: file.value,
10416
+ rows: [],
10417
+ skipped: 0,
10418
+ integrity: options.integrity ? { chained: 0, legacy: 0, unverified: [] } : null
10419
+ }
10420
+ };
10421
+ return err("unavailable", `cannot read ${file.value}: ${error.message}`);
10422
+ }
10423
+ const rows = [];
10424
+ let skipped = 0;
10425
+ const integrity = { chained: 0, legacy: 0, unverified: [] };
10426
+ const earlier = new Set([null]);
10427
+ let chainStarted = false;
10428
+ const ids = new Set;
10429
+ for (const raw of text.split(`
10430
+ `)) {
10431
+ const trimmed = raw.trim();
10432
+ if (!trimmed)
10433
+ continue;
10434
+ const linked = (prevHash) => earlier.has(prevHash);
10435
+ if (options.integrity)
10436
+ earlier.add(sha2562(trimmed));
10437
+ let parsed;
10438
+ try {
10439
+ parsed = JSON.parse(trimmed);
10440
+ } catch {
10441
+ skipped += 1;
10442
+ continue;
10675
10443
  }
10676
- regex = new RegExp(`^${source}$`, "u");
10677
- globCache.set(pattern, regex);
10444
+ if (!isRecord2(parsed) || !str(parsed.id) || !str(parsed.type) || !str(parsed.at)) {
10445
+ skipped += 1;
10446
+ continue;
10447
+ }
10448
+ let rowIntegrity = options.integrity ? "verified" : undefined;
10449
+ let reason = null;
10450
+ if (!options.integrity) {} else if (typeof parsed.rowHash === "string") {
10451
+ chainStarted = true;
10452
+ const { rowHash, ...body } = parsed;
10453
+ const prevHash = str(parsed.prevHash);
10454
+ if (chainHash(prevHash, JSON.stringify(body)) !== rowHash)
10455
+ reason = "hash_mismatch";
10456
+ else if (!linked(prevHash))
10457
+ reason = "broken_link";
10458
+ } else if (chainStarted)
10459
+ reason = "unsigned";
10460
+ else
10461
+ rowIntegrity = "legacy";
10462
+ if (options.integrity && !reason && ids.has(parsed.id))
10463
+ reason = "duplicate_id";
10464
+ if (options.integrity)
10465
+ ids.add(parsed.id);
10466
+ if (reason)
10467
+ rowIntegrity = "unverified";
10468
+ const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
10469
+ const row = {
10470
+ ...parsed,
10471
+ v: typeof parsed.v === "number" ? parsed.v : LEDGER_VERSION,
10472
+ id: parsed.id,
10473
+ at: parsed.at,
10474
+ type: parsed.type,
10475
+ actor: normalizeActor(parsed.actor),
10476
+ branch: str(parsed.branch),
10477
+ head: str(parsed.head),
10478
+ base: str(parsed.base),
10479
+ baseSha: str(parsed.baseSha),
10480
+ patchId: str(parsed.patchId),
10481
+ diffHash: str(parsed.diffHash),
10482
+ tree: str(parsed.tree),
10483
+ dirty: typeof parsed.dirty === "boolean" ? parsed.dirty : null,
10484
+ ...pr === undefined ? {} : { pr },
10485
+ ...str(parsed.supersedes) ? { supersedes: parsed.supersedes } : {},
10486
+ seq: rows.length + 1,
10487
+ superseded: false,
10488
+ supersedeIgnored: false,
10489
+ ...rowIntegrity ? { integrity: rowIntegrity } : {},
10490
+ ...reason ? { integrityReason: reason } : {}
10491
+ };
10492
+ rows.push(row);
10493
+ if (!options.integrity)
10494
+ continue;
10495
+ if (rowIntegrity === "verified")
10496
+ integrity.chained += 1;
10497
+ else if (rowIntegrity === "legacy")
10498
+ integrity.legacy += 1;
10499
+ else if (reason)
10500
+ integrity.unverified.push({
10501
+ seq: row.seq,
10502
+ id: row.id,
10503
+ type: row.type,
10504
+ branch: row.branch,
10505
+ reason
10506
+ });
10678
10507
  }
10679
- return regex.test(file) || file.startsWith(`${pattern.replace(/\/$/u, "")}/`);
10680
- }
10681
- function isRiskPath(file, extra = []) {
10682
- if (extra.some((pattern) => globMatch(pattern, file)))
10683
- return true;
10684
- if (RISK_FILES.has(file) || RISK_NAMES.has(basename(file)))
10685
- return true;
10686
- if (TEST_PATH.test(file) || isDoc(file))
10687
- return false;
10688
- 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)));
10689
- }
10690
- var parseRiskPaths = (text) => text.split(`
10691
- `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.replace(/^\//u, ""));
10692
- function computeLane(input) {
10693
- const riskFiles = input.files.filter((file) => isRiskPath(file.path, input.riskPaths)).map((file) => file.path);
10694
- const risky = new Set(riskFiles);
10695
- const kinds = input.files.map((file) => ({
10696
- file,
10697
- kind: risky.has(file.path) ? "behavior" : kindOf(file)
10698
- }));
10699
- const behavior = kinds.filter((entry) => entry.kind === "behavior" || entry.kind === "test" || entry.kind === "lockfile");
10700
- const lines = behavior.filter((entry) => entry.kind !== "lockfile").reduce((sum, entry) => sum + entry.file.added + entry.file.deleted, 0);
10701
- const opaque = behavior.filter((entry) => entry.kind === "lockfile" || entry.file.binary);
10702
- const result = (lane, reasons) => ({
10703
- lane,
10704
- reasons,
10705
- lines,
10706
- files: behavior.length,
10707
- riskFiles
10708
- });
10709
- const { selfMaxLines, selfMaxFiles, fullMinLines } = LANE_THRESHOLDS;
10710
- if (input.riskTier === "high")
10711
- return result("full", ["the task judgment records high risk"]);
10712
- if (riskFiles.length)
10713
- return result("full", [
10714
- `touches a risk path: ${riskFiles.slice(0, 3).join(", ")}${riskFiles.length > 3 ? ", …" : ""}`
10715
- ]);
10716
- if (!behavior.length)
10717
- return result("none", [
10718
- input.files.length ? "only docs, comments, formatting or generated copies changed" : "no changes"
10719
- ]);
10720
- if (lines >= fullMinLines)
10721
- return result("full", [`large diff: ${lines} lines (≥ ${fullMinLines})`]);
10722
- if (lines > selfMaxLines)
10723
- return result("focused", [`${lines} lines changed (> ${selfMaxLines})`]);
10724
- if (behavior.length > selfMaxFiles)
10725
- return result("focused", [`${behavior.length} behavior files changed (> ${selfMaxFiles})`]);
10726
- if (opaque.length)
10727
- return result("focused", [
10728
- `a lockfile or binary changed: ${opaque.slice(0, 3).map((entry) => entry.file.path).join(", ")}${opaque.length > 3 ? ", …" : ""}`
10729
- ]);
10730
- return result("self", [
10731
- `small change: ${lines} line${lines === 1 ? "" : "s"} in ${behavior.length} file${behavior.length === 1 ? "" : "s"}, no risk path`
10732
- ]);
10508
+ const scopes = new Map;
10509
+ const scopeOf = (branch) => {
10510
+ let scope = scopes.get(branch);
10511
+ if (!scope)
10512
+ scopes.set(branch, scope = branchScope(cwd, branch));
10513
+ return scope;
10514
+ };
10515
+ const byId = new Map;
10516
+ for (const row of rows) {
10517
+ const target = row.supersedes ? byId.get(row.supersedes) : undefined;
10518
+ if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
10519
+ target.superseded = true;
10520
+ else if (row.supersedes)
10521
+ row.supersedeIgnored = true;
10522
+ if (!byId.has(row.id))
10523
+ byId.set(row.id, row);
10524
+ }
10525
+ if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
10526
+ tamperCache.set(file.value, {
10527
+ size: stat.size,
10528
+ stamp: fileStamp(stat),
10529
+ breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
10530
+ });
10531
+ return {
10532
+ ok: true,
10533
+ value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
10534
+ };
10733
10535
  }
10734
- function untrackedStat(cwd, file) {
10536
+ var TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
10537
+ var tamperCache = new Map;
10538
+ var fileStamp = (stat) => `${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`;
10539
+ function ledgerTampering(cwd) {
10540
+ const file = ledgerPath(cwd);
10541
+ if (!file.ok)
10542
+ return [];
10735
10543
  try {
10736
- const bytes = readFileSync3(path10.join(cwd, file));
10737
- if (bytes.includes(0))
10738
- return { path: file, added: 0, deleted: 0, binary: true };
10739
- const text = bytes.toString("utf8");
10740
- if (hasOpaqueLineBreak(text))
10741
- return { path: file, added: 0, deleted: 0, binary: true };
10742
- const lines = text.split(`
10743
- `).length - (text.endsWith(`
10744
- `) ? 1 : 0);
10745
- return { path: file, added: lines, deleted: 0 };
10544
+ const stat = fs7.statSync(file.value);
10545
+ const cached = tamperCache.get(file.value);
10546
+ if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
10547
+ return cached.breaks;
10746
10548
  } catch {
10747
- return { path: file, added: 0, deleted: 0, binary: true };
10549
+ return [];
10748
10550
  }
10551
+ const read = readLedger(cwd, { integrity: true });
10552
+ return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
10749
10553
  }
10750
- var REGULAR = new Set(["100644", "100755"]);
10751
- var ABSENT = /^0+$/u;
10752
- function cosmeticFiles(cwd, fork, head) {
10753
- const raw = diffRaw(cwd, fork, head);
10754
- if (!raw)
10554
+ function tamperedUnblock(cwd) {
10555
+ const file = ledgerPath(cwd);
10556
+ const where = file.ok ? file.value : "the ledger file";
10557
+ 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`;
10558
+ }
10559
+ 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)}`;
10560
+ function verdictBasis(verdict, current) {
10561
+ if (verdict.head && verdict.head === current.head)
10562
+ return "fresh";
10563
+ if (verdict.patchId && verdict.patchId === current.patchId && verdict.diffHash && verdict.diffHash === current.diffHash)
10564
+ return "carried";
10565
+ return "stale";
10566
+ }
10567
+ var isLiveVerdict = (row, retired) => row.type === "verdict" && typeof row.result === "string" && !retired.has(row.id);
10568
+ function trailerSessions(cwd, base, head) {
10569
+ if (!base || !head || !safeRef2(base) || !safeRef2(head))
10570
+ return [];
10571
+ const out = git3(cwd, [
10572
+ "log",
10573
+ "--format=%(trailers:key=Workit-Session,valueonly,separator=%x00)",
10574
+ `${base}..${head}`
10575
+ ]);
10576
+ return (out ?? "").split(/[\0\n]/u).map((value) => value.trim()).filter(Boolean);
10577
+ }
10578
+ var RENAMED = /^branch: renamed refs\/heads\/(.+) to refs\/heads\/(.+)$/iu;
10579
+ function branchScope(cwd, branch) {
10580
+ const renamedAt = new Map;
10581
+ let since = Number.NEGATIVE_INFINITY;
10582
+ if (safeRef2(branch)) {
10583
+ const log = git3(cwd, [
10584
+ "log",
10585
+ "-g",
10586
+ "--date=unix",
10587
+ "--format=%gd%x09%gs",
10588
+ `refs/heads/${branch}`,
10589
+ "--"
10590
+ ]);
10591
+ for (const line of (log ?? "").split(`
10592
+ `)) {
10593
+ const [selector = "", subject = ""] = line.split("\t");
10594
+ const seconds = Number(/@\{(\d+)\}$/u.exec(selector)?.[1]);
10595
+ if (!Number.isFinite(seconds))
10596
+ continue;
10597
+ since = seconds * 1000;
10598
+ const match = RENAMED.exec(subject);
10599
+ if (!match || match[1] === branch)
10600
+ continue;
10601
+ const until = (seconds + 1) * 1000;
10602
+ renamedAt.set(match[1], Math.max(renamedAt.get(match[1]) ?? 0, until));
10603
+ }
10604
+ }
10605
+ return (row) => {
10606
+ if (row.branch === branch)
10607
+ return true;
10608
+ const until = row.branch === null ? undefined : renamedAt.get(row.branch);
10609
+ if (until === undefined)
10610
+ return false;
10611
+ const at = Date.parse(row.at);
10612
+ return at >= since && at <= until;
10613
+ };
10614
+ }
10615
+ var renameAwareBranch = (scopeOf, branch) => {
10616
+ return (target) => {
10617
+ const own = typeof branch === "string" ? branch : null;
10618
+ const theirs = typeof target.branch === "string" ? target.branch : null;
10619
+ if (own === theirs)
10620
+ return true;
10621
+ if (own === null || theirs === null || typeof target.at !== "string")
10622
+ return false;
10623
+ return scopeOf(own)({ branch: theirs, at: target.at });
10624
+ };
10625
+ };
10626
+ function authorSessions(cwd, rows, branch, range = { base: null, head: null }, scope = branchScope(cwd, branch)) {
10627
+ const sessions = new Set;
10628
+ for (const row of rows) {
10629
+ if (!scope(row) || row.type !== "commit.recorded" && row.type !== "task.opened")
10630
+ continue;
10631
+ const session = str(row.session) ?? row.actor.session;
10632
+ if (session)
10633
+ sessions.add(session);
10634
+ }
10635
+ for (const session of trailerSessions(cwd, range.base, range.head))
10636
+ sessions.add(session);
10637
+ return sessions;
10638
+ }
10639
+ var derivesFrom = (derived, bare) => !bare.includes(":") && derived.endsWith(`:${bare}`);
10640
+ var authoredBy = (authors, session) => {
10641
+ if (authors.has(session))
10642
+ return true;
10643
+ for (const author of authors)
10644
+ if (derivesFrom(author, session) || derivesFrom(session, author))
10645
+ return true;
10646
+ return false;
10647
+ };
10648
+ var TRUST_REASONS = new Set([
10649
+ "unsigned",
10650
+ "hash_mismatch",
10651
+ "broken_link",
10652
+ "duplicate_id",
10653
+ "unissued_session",
10654
+ "unbound_session"
10655
+ ]);
10656
+ var SHELL_VOUCHED_HOSTS = new Set([
10657
+ "cli",
10658
+ "opencode",
10659
+ "pi",
10660
+ "codex_cli",
10661
+ "codex_desktop"
10662
+ ]);
10663
+ function sessionTrust(cwd, row) {
10664
+ const session = row.actor.session;
10665
+ if (!session || row.self === true)
10755
10666
  return null;
10756
- const candidates = raw.filter((entry) => REGULAR.has(entry.oldMode) && entry.oldMode === entry.newMode && !ABSENT.test(entry.oldBlob) && (head === null || !ABSENT.test(entry.newBlob)));
10757
- const blobs = readBlobs(cwd, candidates.flatMap((entry) => [entry.oldBlob, entry.newBlob]));
10758
- if (!blobs)
10667
+ const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
10668
+ const host = row.actor.host;
10669
+ const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
10670
+ if (!issuedOnly)
10759
10671
  return null;
10760
- const out = new Set;
10761
- const opaque = new Set;
10762
- for (const entry of candidates) {
10763
- const before = blobs.get(entry.oldBlob);
10764
- let after = blobs.get(entry.newBlob);
10765
- if (!after && head === null && ABSENT.test(entry.newBlob))
10766
- try {
10767
- after = readFileSync3(path10.join(cwd, entry.path));
10768
- } catch {
10769
- after = undefined;
10770
- }
10771
- if (before && after && cosmeticChange(entry.path, before, after))
10772
- out.add(entry.path);
10773
- else if ([before, after].some((bytes) => bytes && hasOpaqueLineBreak(bytes.toString("utf8"))))
10774
- opaque.add(entry.path);
10672
+ const binding = issuedBinding(cwd, session);
10673
+ if (binding === null)
10674
+ return "unissued_session";
10675
+ return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
10676
+ }
10677
+ function trustReasons(cwd, row) {
10678
+ if (row.integrity === "unverified")
10679
+ return [row.integrityReason ?? "unsigned"];
10680
+ if (row.integrity !== "verified")
10681
+ return ["unsigned"];
10682
+ const session = sessionTrust(cwd, row);
10683
+ return session ? [session] : [];
10684
+ }
10685
+ function withIntegrity(cwd, rows) {
10686
+ if (rows.every((row) => row.integrity !== undefined))
10687
+ return rows;
10688
+ const read = readLedger(cwd, { integrity: true });
10689
+ const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
10690
+ return rows.map((row) => {
10691
+ if (row.integrity !== undefined)
10692
+ return row;
10693
+ const fresh = bySeq.get(row.seq);
10694
+ return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
10695
+ });
10696
+ }
10697
+ var untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason));
10698
+ var independenceReasons = (row, authors) => {
10699
+ const reasons = [];
10700
+ if (!row.actor.session)
10701
+ reasons.push("no_session");
10702
+ else if (authoredBy(authors, row.actor.session))
10703
+ reasons.push("author_session");
10704
+ if (row.self === true && !reasons.includes("no_session"))
10705
+ reasons.push("self");
10706
+ return reasons;
10707
+ };
10708
+ function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
10709
+ let effective = null;
10710
+ for (const row of rows) {
10711
+ if (!effective) {
10712
+ effective = row;
10713
+ continue;
10714
+ }
10715
+ if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
10716
+ continue;
10717
+ const rowIndependent = reasonsOf(row).length === 0;
10718
+ const effIndependent = reasonsOf(effective).length === 0;
10719
+ if (effIndependent && !rowIndependent && isCurrent(effective))
10720
+ continue;
10721
+ if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
10722
+ continue;
10723
+ effective = row;
10775
10724
  }
10776
- return { cosmetic: out, opaque };
10725
+ return effective;
10777
10726
  }
10778
- function generatedAt(cwd, fork, paths) {
10779
- const files = treeFilesNamed(cwd, fork, ".gitattributes");
10780
- const blobs = files?.size ? readBlobs(cwd, [...files.values()]) : null;
10781
- if (!files || !blobs)
10782
- return new Set;
10783
- const texts = new Map;
10784
- for (const [file, id] of files) {
10785
- const bytes = blobs.get(id);
10786
- if (bytes)
10787
- texts.set(file, bytes.toString("utf8"));
10727
+ var restackExact = new WeakMap;
10728
+ function restackCarries(exactIn, rows, branch, from, to) {
10729
+ if (!from || !to)
10730
+ return false;
10731
+ const exact = (row, fromHead, head) => {
10732
+ if (exactIn === null)
10733
+ return true;
10734
+ const cwd = exactIn;
10735
+ let known = restackExact.get(row);
10736
+ if (known === undefined) {
10737
+ known = typeof row.fromBase === "string" && typeof row.toBase === "string" && sameOwnChange(cwd, {
10738
+ from: fromHead,
10739
+ to: head,
10740
+ fromBase: row.fromBase,
10741
+ toBase: row.toBase
10742
+ });
10743
+ restackExact.set(row, known);
10744
+ }
10745
+ return known;
10746
+ };
10747
+ const edges = new Map;
10748
+ for (const row of rows)
10749
+ 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))
10750
+ edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
10751
+ const seen = new Set([from]);
10752
+ const queue = [from];
10753
+ while (queue.length) {
10754
+ const next = queue.shift();
10755
+ if (next === to)
10756
+ return true;
10757
+ for (const target of edges.get(next) ?? [])
10758
+ if (!seen.has(target)) {
10759
+ seen.add(target);
10760
+ queue.push(target);
10761
+ }
10788
10762
  }
10789
- return generatedByAttributes(texts, paths);
10790
- }
10791
- var forkOf = (cwd, base, head) => mergeBase(cwd, base, head ?? "HEAD");
10792
- function laneFiles(cwd, fork, head) {
10793
- const stats = diffNumstat(cwd, fork, head);
10794
- const cosmetic = stats ? cosmeticFiles(cwd, fork, head) : null;
10795
- if (!stats || !cosmetic)
10796
- return null;
10797
- const files = [
10798
- ...stats.map((entry) => ({
10799
- ...entry,
10800
- ...cosmetic.opaque.has(entry.path) ? { binary: true } : {},
10801
- cosmetic: !entry.binary && cosmetic.cosmetic.has(entry.path)
10802
- })),
10803
- ...head === null ? untrackedFiles(cwd).map((file) => untrackedStat(cwd, file)) : []
10804
- ];
10805
- const generated = generatedAt(cwd, fork, files.map((entry) => entry.path));
10806
- return files.map((entry) => ({
10807
- ...entry,
10808
- generated: generated.has(entry.path)
10809
- }));
10763
+ return false;
10810
10764
  }
10811
- function branchLane(cwd, options) {
10812
- const candidates = options.base ? [`origin/${options.base}`, options.base] : [];
10813
- const base = candidates.find((ref) => resolveRef(cwd, ref) !== null) ?? defaultBase(cwd) ?? null;
10814
- const head = options.workingTree ? null : options.head;
10815
- const fork = base && (options.workingTree || options.head) ? forkOf(cwd, base, head) : null;
10816
- const files = fork ? laneFiles(cwd, fork, head) : null;
10817
- const riskTier = options.judgment?.riskTier ?? null;
10818
- if (!fork || !files) {
10819
- const high = riskTier === "high";
10820
- return {
10821
- lane: high ? "full" : "focused",
10822
- reasons: [
10823
- `the diff of ${options.head?.slice(0, 12) ?? "(no head)"} against ${base ?? "(no base)"} cannot be read`,
10824
- ...high ? ["the task judgment records high risk"] : []
10825
- ],
10826
- lines: 0,
10827
- files: 0,
10828
- riskFiles: [],
10829
- base,
10830
- head: options.head
10831
- };
10765
+ function checkVerdicts(cwd, branch, ledgerRows) {
10766
+ const rows = withIntegrity(cwd, ledgerRows);
10767
+ const head = branchHead(cwd, branch);
10768
+ const fallbackBase = defaultBase(cwd);
10769
+ const scope = branchScope(cwd, branch);
10770
+ const keyCache = new Map;
10771
+ const keysAt = (base, at) => {
10772
+ if (!base || !at)
10773
+ return { patchId: null, diffHash: null };
10774
+ const cacheKey = `${base}\x00${at}`;
10775
+ let key = keyCache.get(cacheKey);
10776
+ if (!key) {
10777
+ key = { patchId: patchId(cwd, base, at), diffHash: diffHash(cwd, base, at) };
10778
+ keyCache.set(cacheKey, key);
10779
+ }
10780
+ return key;
10781
+ };
10782
+ const keyFor = (base) => keysAt(base, head);
10783
+ const driftCache = new Map;
10784
+ const driftOf = (row) => {
10785
+ if (driftCache.has(row))
10786
+ return driftCache.get(row) ?? null;
10787
+ let drift = null;
10788
+ const toBase = row.base ?? fallbackBase;
10789
+ const toBaseSha = toBase ? resolveCommit(cwd, toBase) : null;
10790
+ const fromBase = row.baseSha ?? toBaseSha;
10791
+ if (row.head && head && row.head !== head && toBaseSha && fromBase && !untrusted(reasonsOf(row)))
10792
+ drift = verdictDrift(cwd, {
10793
+ from: row.head,
10794
+ to: head,
10795
+ fromBase,
10796
+ toBase: toBaseSha,
10797
+ kind: typeof row.kind === "string" ? row.kind : "review"
10798
+ });
10799
+ driftCache.set(row, drift);
10800
+ return drift;
10801
+ };
10802
+ const exactCache = new Map;
10803
+ const exactCarry = (row) => {
10804
+ if (FAILING.has(String(row.result)))
10805
+ return true;
10806
+ let exact = exactCache.get(row);
10807
+ if (exact === undefined) {
10808
+ const toBase = row.base ?? fallbackBase;
10809
+ const toBaseSha = toBase ? resolveCommit(cwd, toBase) : null;
10810
+ const fromBase = row.baseSha ?? toBaseSha;
10811
+ exact = Boolean(row.head && head && toBaseSha && fromBase && sameOwnChange(cwd, { from: row.head, to: head, fromBase, toBase: toBaseSha }));
10812
+ exactCache.set(row, exact);
10813
+ }
10814
+ return exact;
10815
+ };
10816
+ const keyBasis = (row) => {
10817
+ const basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
10818
+ return basis === "carried" && !exactCarry(row) ? "stale" : basis;
10819
+ };
10820
+ const restacked = (row) => restackCarries(FAILING.has(String(row.result)) ? null : cwd, rows, branch, row.head, head);
10821
+ const carryOf = (row) => {
10822
+ if (keyBasis(row) !== "stale")
10823
+ return null;
10824
+ if (restacked(row))
10825
+ return null;
10826
+ const drift = driftOf(row);
10827
+ return drift?.carries ? { rule: drift.rule, since: drift.since, files: drift.files } : null;
10828
+ };
10829
+ const basisOf = (row) => {
10830
+ const basis = keyBasis(row);
10831
+ if (basis !== "stale")
10832
+ return basis;
10833
+ return restacked(row) || carryOf(row) ? "carried" : basis;
10834
+ };
10835
+ const sameCode = (failed, row) => {
10836
+ if (failed.head && failed.head === row.head)
10837
+ return true;
10838
+ const base = failed.base ?? fallbackBase;
10839
+ const stored = (row.base ?? fallbackBase) === base && row.baseSha !== null && row.baseSha === failed.baseSha;
10840
+ const keys = stored ? { patchId: row.patchId, diffHash: row.diffHash } : keysAt(base, row.head);
10841
+ return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(null, rows, branch, failed.head, row.head);
10842
+ };
10843
+ const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
10844
+ const failureAuthorCache = new Map;
10845
+ const failureAuthors = (row) => {
10846
+ let set = failureAuthorCache.get(row.seq);
10847
+ if (!set) {
10848
+ set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
10849
+ failureAuthorCache.set(row.seq, set);
10850
+ }
10851
+ return set;
10852
+ };
10853
+ const reasonCache = new Map;
10854
+ const reasonsOf = (row) => {
10855
+ let reasons = reasonCache.get(row);
10856
+ if (!reasons) {
10857
+ const failing = FAILING.has(String(row.result));
10858
+ reasons = [
10859
+ ...trustReasons(cwd, row),
10860
+ ...independenceReasons(row, failing ? failureAuthors(row) : authors)
10861
+ ];
10862
+ reasonCache.set(row, reasons);
10863
+ }
10864
+ return reasons;
10865
+ };
10866
+ const retired = new Set;
10867
+ for (const row of rows)
10868
+ if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
10869
+ retired.add(row.supersedes);
10870
+ const byKind = new Map;
10871
+ for (const row of rows)
10872
+ if (isLiveVerdict(row, retired) && scope(row)) {
10873
+ const kind = typeof row.kind === "string" ? row.kind : "review";
10874
+ byKind.set(kind, [...byKind.get(kind) ?? [], row]);
10875
+ }
10876
+ const verdicts = [];
10877
+ const warnings = [];
10878
+ for (const [kind, list] of byKind) {
10879
+ const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
10880
+ if (!row)
10881
+ continue;
10882
+ const basis = basisOf(row);
10883
+ const current = basis !== "stale";
10884
+ const independence = reasonsOf(row);
10885
+ const reasons = [
10886
+ ...current ? [] : ["stale"],
10887
+ ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
10888
+ ...independence
10889
+ ];
10890
+ verdicts.push({
10891
+ kind,
10892
+ basis,
10893
+ current,
10894
+ independent: independence.length === 0,
10895
+ accepted: reasons.length === 0,
10896
+ reasons,
10897
+ carry: basis === "carried" ? carryOf(row) : null,
10898
+ verdict: row
10899
+ });
10900
+ if (reasons.length === 0) {
10901
+ for (const later of list)
10902
+ if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
10903
+ 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`);
10904
+ }
10832
10905
  }
10833
- const repoRisk = parseRiskPaths(fileAt(cwd, fork, RISK_PATHS_FILE) ?? "");
10906
+ for (const row of rows)
10907
+ if (row.integrity === "unverified" && scope(row))
10908
+ 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)`);
10909
+ const tampered = ledgerTampering(cwd);
10910
+ if (tampered.length) {
10911
+ warnings.push(tamperedWarning(cwd, tampered));
10912
+ for (const entry of verdicts) {
10913
+ entry.accepted = false;
10914
+ entry.reasons = [...entry.reasons, "ledger_tampered"];
10915
+ }
10916
+ }
10917
+ verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
10918
+ const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
10919
+ const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
10920
+ const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
10921
+ const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
10922
+ accepted: false,
10923
+ verdict: failing?.verdict ?? null,
10924
+ reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
10925
+ };
10926
+ 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))));
10927
+ const selfEntry = latestSelf && STRONG_RESULTS.has(String(latestSelf.verdict.result)) ? latestSelf : undefined;
10928
+ const carryEntry = verdicts.find((entry) => entry.verdict === accepted.verdict && entry.accepted) ?? newestCurrent;
10929
+ const lastPass = accepted.accepted || failing || tampered.length ? undefined : verdicts.findLast((entry) => !entry.current && !untrusted(entry.reasons) && STRONG_RESULTS.has(String(entry.verdict.result)));
10930
+ const drift = lastPass ? driftOf(lastPass.verdict) : null;
10931
+ const reverify = drift && !drift.carries && drift.focused && drift.files ? { scope: "focused", since: drift.since, files: drift.files } : null;
10834
10932
  return {
10835
- ...computeLane({
10836
- files,
10837
- riskTier,
10838
- riskPaths: [...repoRisk, ...options.riskPaths ?? []]
10839
- }),
10840
- base,
10841
- head: options.head
10933
+ branch,
10934
+ head,
10935
+ review: accepted.accepted ? "verified" : selfEntry ? "self-reviewed" : "unreviewed",
10936
+ selfVerdict: accepted.accepted ? null : selfEntry?.verdict ?? null,
10937
+ base: newestCurrent?.verdict.base ?? fallbackBase,
10938
+ patchId: keyFor(newestCurrent?.verdict.base ?? fallbackBase).patchId,
10939
+ current: { basis: newestCurrent?.basis ?? "none", verdict: newestCurrent?.verdict ?? null },
10940
+ carry: carryEntry?.carry ?? null,
10941
+ reverify,
10942
+ accepted,
10943
+ verdicts,
10944
+ authors: [...authors].toSorted(),
10945
+ warnings
10842
10946
  };
10843
10947
  }
10844
- var laneNeed = (lane) => lane === "none" ? "none" : lane === "self" ? "self" : "non-author";
10948
+ var fromLead = (row, leads) => leads === null || row.actor.session !== null && leads.includes(row.actor.session);
10949
+ function activeStanding(rows, fanout, leads = null) {
10950
+ const active = [];
10951
+ for (const row of rows) {
10952
+ if (row.fanout !== fanout || !fromLead(row, leads))
10953
+ continue;
10954
+ if (row.type === "standing" && typeof row.what === "string" && row.what.trim()) {
10955
+ if (!row.superseded)
10956
+ active.push(row);
10957
+ } else if (row.type === "standing.cleared") {
10958
+ const target = str(row.target);
10959
+ if (target === null)
10960
+ active.length = 0;
10961
+ else {
10962
+ const index = active.findIndex((order) => order.id === target);
10963
+ if (index >= 0)
10964
+ active.splice(index, 1);
10965
+ }
10966
+ }
10967
+ }
10968
+ return active;
10969
+ }
10970
+ var OBSERVED_ONLY = new Set([
10971
+ "check",
10972
+ "merge.unverified",
10973
+ "policy.judged",
10974
+ ...PR_ROW_TYPES
10975
+ ]);
10845
10976
 
10846
10977
  // packages/workit-core/src/core/policy/derive.ts
10847
10978
  var RULES = {
@@ -14612,7 +14743,7 @@ function splitCommand(command) {
14612
14743
  argv.push(current);
14613
14744
  return argv.length ? argv : new Error("empty command");
14614
14745
  }
14615
- var git5 = (cwd, args) => {
14746
+ var git4 = (cwd, args) => {
14616
14747
  const run = spawnSync9("git", args, {
14617
14748
  cwd,
14618
14749
  encoding: "utf8",
@@ -14624,7 +14755,7 @@ var git5 = (cwd, args) => {
14624
14755
  return value || null;
14625
14756
  };
14626
14757
  function checkRoot(cwd) {
14627
- return path18.resolve(git5(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd);
14758
+ return path18.resolve(git4(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd);
14628
14759
  }
14629
14760
  var packageManager = (root) => {
14630
14761
  const has = (file) => fs13.existsSync(path18.join(root, file));
@@ -15576,7 +15707,7 @@ Autonomy contract:
15576
15707
  CLI first: if a step has one right answer, use the \`workit\` verb instead of
15577
15708
  hand-running git, gh or glab (\`workit git|pr|ci|check|help\`;
15578
15709
  else \`npx -y @brainervirus/workit-cli\`).
15579
- Other checkout: \`workit --cwd <dir> <verb>\` (Codex: \`<verb> --cwd <dir>\`); one workit
15710
+ Other checkout: \`workit <verb> --cwd <dir> …\` (verb first: host rules match it); one workit
15580
15711
  verb per shell call, never \`cd <dir> && workit\`. zsh-safe: no \`echo =====\`
15581
15712
  (\`printf '%s\\n' ---\`), quote globs and URLs, no variable named \`status\` or \`path\`.
15582
15713
  Add Co-Authored-By via \`workit git commit --trailer "K: v" -- <paths>\`, never
@@ -15588,7 +15719,7 @@ authorizes an effect. Judge work once with
15588
15719
  \`workit policy assess --judge risk= behavior= product-choice= plan=\`.
15589
15720
  Across repositories, bind items to checkout, branch and endpoint, resolve
15590
15721
  competing targets before a mutation, and reconcile an uncertain
15591
- external effect before a retry.
15722
+ effect before a retry.
15592
15723
 
15593
15724
  Mid-task: never silently drop or resume an objective (workit-continue).
15594
15725
 
@@ -17994,6 +18125,18 @@ var substitutions = (body) => {
17994
18125
  };
17995
18126
  var PLAIN_POSIX = /[^\s'"\\`$(){},<>|;&#]+/uy;
17996
18127
  var PLAIN_POWERSHELL = /[^\s'"`(){}<>|;&#]+/uy;
18128
+ var expandsInQuotes = (body, dialect) => {
18129
+ const escape = dialect === "posix" ? "\\" : "`";
18130
+ for (let index = 0;index < body.length; index++) {
18131
+ const char = body[index];
18132
+ if (char === escape)
18133
+ index++;
18134
+ else if (char === "$" || dialect === "posix" && char === "`")
18135
+ return true;
18136
+ }
18137
+ return false;
18138
+ };
18139
+ var EXPANDING_RUN = /[*?[]/u;
17997
18140
  var arithmeticEnd = (command, start) => {
17998
18141
  const end = substitutionEnd(command, start);
17999
18142
  return command[end - 1] === ")" && end > start + 2 ? end : 0;
@@ -18012,6 +18155,7 @@ var tokenize = (command, dialect) => {
18012
18155
  let current = { words: [], redirects: [] };
18013
18156
  let word = "";
18014
18157
  let inWord = false;
18158
+ let expanding = false;
18015
18159
  let pending = null;
18016
18160
  const heredocs = [];
18017
18161
  const escape = posix ? "\\" : "`";
@@ -18026,11 +18170,16 @@ var tokenize = (command, dialect) => {
18026
18170
  heredocs.push({ delimiter: text.replace(/^-/, ""), owner: current });
18027
18171
  else if (pending === "herestring")
18028
18172
  (current.stdin ??= []).push(text);
18029
- else if (pending !== "input")
18173
+ else if (pending !== "input") {
18174
+ const at = current.words.length;
18030
18175
  current.words.push(...MARKERS.test(word) ? expandBraces(word) : [word]);
18176
+ if (expanding || !posix && word.startsWith("@"))
18177
+ (current.expands ??= []).push(...current.words.slice(at).map((_, i) => at + i));
18178
+ }
18031
18179
  pending = null;
18032
18180
  word = "";
18033
18181
  inWord = false;
18182
+ expanding = false;
18034
18183
  };
18035
18184
  const flushSegment = () => {
18036
18185
  flushWord();
@@ -18045,6 +18194,7 @@ var tokenize = (command, dialect) => {
18045
18194
  };
18046
18195
  const substitution = (inner, text) => {
18047
18196
  (current.subs ??= []).push(inner);
18197
+ expanding = true;
18048
18198
  word += text;
18049
18199
  inWord = true;
18050
18200
  };
@@ -18055,6 +18205,8 @@ var tokenize = (command, dialect) => {
18055
18205
  if (run) {
18056
18206
  word += run[0];
18057
18207
  inWord = true;
18208
+ if (EXPANDING_RUN.test(run[0]))
18209
+ expanding = true;
18058
18210
  index += run[0].length - 1;
18059
18211
  continue;
18060
18212
  }
@@ -18083,6 +18235,8 @@ var tokenize = (command, dialect) => {
18083
18235
  const body = command.slice(index + 1, end < 0 ? undefined : end);
18084
18236
  if (posix && char === '"' && /\$\(|`/.test(body))
18085
18237
  (current.subs ??= []).push(...substitutions(body));
18238
+ if (char === '"' && expandsInQuotes(body, dialect))
18239
+ expanding = true;
18086
18240
  word += unquote(body, char, dialect);
18087
18241
  inWord = true;
18088
18242
  index = end < 0 ? command.length : end;
@@ -18128,6 +18282,8 @@ var tokenize = (command, dialect) => {
18128
18282
  if (char === "(" && inWord && word.endsWith("$")) {
18129
18283
  word = word.slice(0, -1);
18130
18284
  inWord = word !== "";
18285
+ if (!inWord)
18286
+ expanding = false;
18131
18287
  }
18132
18288
  group(char === "(" ? "open" : "close");
18133
18289
  } else if (!posix && (char === "{" || char === "}")) {
@@ -18181,6 +18337,8 @@ var tokenize = (command, dialect) => {
18181
18337
  else if (posix && char === "}" && word.includes(OPEN))
18182
18338
  word += CLOSE;
18183
18339
  else {
18340
+ if (char === "$")
18341
+ expanding = true;
18184
18342
  word += char;
18185
18343
  inWord = true;
18186
18344
  }
@@ -18235,7 +18393,7 @@ var GIT_VALUE_OPTIONS = new Set([
18235
18393
  ]);
18236
18394
  var GIT_CONFIG_OPTIONS = new Set(["-c", "--config-env", "--git-dir"]);
18237
18395
  var GIT_CONFIG_ENV = /^GIT_(?:CONFIG\w*|DIR|COMMON_DIR|WORK_TREE)$/;
18238
- var ASSIGNMENT2 = /^([A-Za-z_]\w*)=([\s\S]*)$/;
18396
+ var ASSIGNMENT = /^([A-Za-z_]\w*)=([\s\S]*)$/;
18239
18397
  var MAX_DEPTH = 4;
18240
18398
  var MAX_EXPANSIONS = 16;
18241
18399
  var isAbsolute = (value) => path22.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
@@ -18262,7 +18420,7 @@ var stripPrefixes = (input) => {
18262
18420
  const first = words[0];
18263
18421
  if (first === undefined)
18264
18422
  return { words, env };
18265
- const assignment = ASSIGNMENT2.exec(first);
18423
+ const assignment = ASSIGNMENT.exec(first);
18266
18424
  if (assignment) {
18267
18425
  if (GIT_CONFIG_ENV.test(assignment[1]))
18268
18426
  env[assignment[1]] = assignment[2];
@@ -18321,10 +18479,10 @@ var assignments = (words, vars) => {
18321
18479
  while (rest[0]?.startsWith("-"))
18322
18480
  rest = rest.slice(1);
18323
18481
  }
18324
- if (rest.length === 0 || !rest.every((word) => ASSIGNMENT2.test(word)))
18482
+ if (rest.length === 0 || !rest.every((word) => ASSIGNMENT.test(word)))
18325
18483
  return null;
18326
18484
  return rest.map((word) => {
18327
- const [, name, value] = ASSIGNMENT2.exec(word);
18485
+ const [, name, value] = ASSIGNMENT.exec(word);
18328
18486
  return [name, expandWord(value, vars)];
18329
18487
  });
18330
18488
  };
@@ -18512,7 +18670,7 @@ var splitString = (line) => {
18512
18670
  return line.slice(index).join(" ").slice(15);
18513
18671
  if (/^-S./.test(word))
18514
18672
  return [word.slice(2), ...line.slice(index + 1)].join(" ");
18515
- if (!word.startsWith("-") && !ASSIGNMENT2.test(word))
18673
+ if (!word.startsWith("-") && !ASSIGNMENT.test(word))
18516
18674
  return null;
18517
18675
  }
18518
18676
  return null;
@@ -18625,8 +18783,17 @@ function walk2(command, dialect, depth, seen) {
18625
18783
  visit(next, undefined, hops + 1);
18626
18784
  };
18627
18785
  const wrapped = seen ? null : workitCheckTail(words);
18628
- if (wrapped)
18629
- return "script" in wrapped ? nested(wrapped.script, "posix") : runs([wrapped.argv]);
18786
+ if (wrapped) {
18787
+ const outer = dir;
18788
+ if (wrapped.cwd !== null)
18789
+ dir = joinDir(dir, wrapped.cwd);
18790
+ if ("script" in wrapped)
18791
+ nested(wrapped.script, "posix");
18792
+ else
18793
+ runs([wrapped.argv]);
18794
+ dir = outer;
18795
+ return;
18796
+ }
18630
18797
  if (CD.has(name)) {
18631
18798
  const target = cdTarget(words);
18632
18799
  dir = target === null ? os5.homedir() : joinDir(dir, target);
@@ -18727,14 +18894,14 @@ function workitCheckTail(words) {
18727
18894
  return null;
18728
18895
  } else if (!/^workit(?:\.exe|\.cmd)?$/.test(commandName(words[0])))
18729
18896
  return null;
18730
- const args = words.slice(at + 1);
18897
+ const { rest: args, cwd } = skipWorkitGlobals(words.slice(at + 1));
18731
18898
  if (args[0] !== "check" && !(args[0] === "lock" && args[1] === "run"))
18732
18899
  return null;
18733
18900
  const split = args.indexOf("--");
18734
18901
  const tail = split < 0 ? [] : args.slice(split + 1);
18735
18902
  if (!tail.length)
18736
18903
  return null;
18737
- return args.slice(0, split).includes("--shell") ? { script: tail.join(" ") } : { argv: tail };
18904
+ return args.slice(0, split).includes("--shell") ? { script: tail.join(" "), cwd } : { argv: tail, cwd };
18738
18905
  }
18739
18906
  function simpleCommand(command, dialect = "posix", depth = 0) {
18740
18907
  let segments;
@@ -18795,7 +18962,21 @@ function skipWorkitGlobals(args) {
18795
18962
  } else if (args[at] !== "--json")
18796
18963
  break;
18797
18964
  }
18798
- return { rest: args.slice(at), cwd };
18965
+ const rest = [];
18966
+ for (;at < args.length; at++) {
18967
+ const arg = args[at];
18968
+ if (arg === "--") {
18969
+ rest.push(...args.slice(at));
18970
+ break;
18971
+ }
18972
+ if (arg.startsWith("--cwd="))
18973
+ cwd = arg.slice("--cwd=".length);
18974
+ else if (arg === "--cwd" && args[at + 1] !== undefined)
18975
+ cwd = args[++at];
18976
+ else
18977
+ rest.push(arg);
18978
+ }
18979
+ return { rest, cwd };
18799
18980
  }
18800
18981
  var PUSH_VALUE_OPTIONS = new Set(["-o", "--push-option", "--receive-pack", "--exec", "--repo"]);
18801
18982
  var CURRENT_BRANCH = /^\$\(\s*git\s+(?:branch\s+--show-current|rev-parse\s+--abbrev-ref\s+HEAD|symbolic-ref\s+--short\s+HEAD)\s*\)$/;
@@ -19241,7 +19422,7 @@ var rewrittenUrl = (url, config) => {
19241
19422
  }
19242
19423
  return url;
19243
19424
  };
19244
- var pushUrl2 = (config, remote, dir) => {
19425
+ var pushUrl = (config, remote, dir) => {
19245
19426
  const configured = configValue(config, `remote.${remote}.pushurl`) ?? configValue(config, `remote.${remote}.url`);
19246
19427
  return normalizedUrl(rewrittenUrl(configured ?? remote, config), dir);
19247
19428
  };
@@ -19269,7 +19450,7 @@ var toPushRemote = (repo, remote, config) => {
19269
19450
  const pushRemote = pushRemoteOf(config, repo.branch);
19270
19451
  if (pushRemote === null || pushRemote === remote)
19271
19452
  return true;
19272
- return pushUrl2(config, remote, repo.dir) === pushUrl2(config, pushRemote, repo.dir);
19453
+ return pushUrl(config, remote, repo.dir) === pushUrl(config, pushRemote, repo.dir);
19273
19454
  };
19274
19455
  var quote = (text) => `\`${text}\``;
19275
19456
  var refusal = (action, branch, dir) => {
@@ -19807,6 +19988,53 @@ var shellPolicy = (cwd, command, dialect = "posix") => {
19807
19988
  }
19808
19989
  return policy && !policy.ok ? { kind: "deny", reason: `branch_policy_denied: ${policy.error}`, unblock: null } : { kind: "none" };
19809
19990
  };
19991
+ // packages/workit-core/src/hooks/delivery-verbs.ts
19992
+ var WORKIT_ALLOWED_VERBS = [
19993
+ "git",
19994
+ "pr",
19995
+ "ci",
19996
+ "stack",
19997
+ "release",
19998
+ "task",
19999
+ "ledger",
20000
+ "evidence",
20001
+ "finding",
20002
+ "decision",
20003
+ "worker",
20004
+ "verify-delivery",
20005
+ "policy",
20006
+ "doctor",
20007
+ "help"
20008
+ ];
20009
+ var WORKIT_BYPASS_FLAGS = ["--unverified", "--overwrite-unintegrated"];
20010
+ var ALLOWED = new Set(WORKIT_ALLOWED_VERBS);
20011
+ var isBypass = (word) => WORKIT_BYPASS_FLAGS.some((flag) => word === flag || word.startsWith(`${flag}=`));
20012
+ var isWorkitWord = (word) => /^(?:.*[\\/])?workit(?:-cli)?$/u.test(word);
20013
+ var bypassReason = (flag) => `Workit: \`${flag}\` overrides a Workit gate, so the user decides this call (no allow rule covers it).`;
20014
+ var EXPANSION_REASON = "Workit: an expansion before `--` could become a bypass flag, so the user decides this call (no allow rule covers it).";
20015
+ var workitBypassFlag = (command, dialect = "posix") => {
20016
+ for (const segment of segmentsOf(command, dialect)) {
20017
+ if (segment.unanalyzable) {
20018
+ const text = command.replaceAll(/['"\\`]/gu, "");
20019
+ if (!/\bworkit\b/u.test(text))
20020
+ return null;
20021
+ const found = WORKIT_BYPASS_FLAGS.find((flag) => text.includes(flag));
20022
+ return found ? bypassReason(found) : null;
20023
+ }
20024
+ const start = segment.words.findIndex(isWorkitWord);
20025
+ if (start < 0)
20026
+ continue;
20027
+ const words = segment.words.slice(start + 1);
20028
+ const end = words.indexOf("--");
20029
+ const flag = (end < 0 ? words : words.slice(0, end)).find(isBypass);
20030
+ if (flag)
20031
+ return bypassReason(flag.split("=")[0] ?? flag);
20032
+ const limit = start + 1 + (end < 0 ? words.length : end);
20033
+ if (segment.expands?.some((index) => index > start && index < limit))
20034
+ return EXPANSION_REASON;
20035
+ }
20036
+ return null;
20037
+ };
19810
20038
  // packages/workit-core/src/core/heavy-rules.ts
19811
20039
  import fs18 from "node:fs";
19812
20040
  import os6 from "node:os";
@@ -21191,7 +21419,7 @@ var segmentTargets = (cwd, command, dialect, depth = 0, strict = false) => {
21191
21419
  }
21192
21420
  const inner = depth < 4 ? wrappedScript(words) ?? (strict ? shellScript2(words) : null) : null;
21193
21421
  if (inner !== null && dir !== undefined)
21194
- targets.push(...segmentTargets(dir, inner, "posix", depth + 1, strict));
21422
+ targets.push(...segmentTargets(workitRunDir(dir, words), inner, "posix", depth + 1, strict));
21195
21423
  if (strict && dir !== undefined) {
21196
21424
  const runIn = dir;
21197
21425
  for (const sub of depth < 4 ? segment.subs ?? [] : [])
@@ -21231,6 +21459,14 @@ var segmentTargets = (cwd, command, dialect, depth = 0, strict = false) => {
21231
21459
  }
21232
21460
  return targets;
21233
21461
  };
21462
+ var workitRunDir = (dir, words) => {
21463
+ for (const invocation of workitInvocations(words.map(quoteWord2).join(" "), "posix")) {
21464
+ const { at, args } = workitCwd(dir, invocation);
21465
+ if (args[0] === "check" || args[0] === "lock" && args[1] === "run")
21466
+ return at;
21467
+ }
21468
+ return dir;
21469
+ };
21234
21470
  var workitCwd = (dir, args) => {
21235
21471
  const { rest, cwd } = skipWorkitGlobals(args);
21236
21472
  return { at: cwd === null ? dir : path28.resolve(dir, homeExpanded(cwd)), args: rest };
@@ -22015,8 +22251,13 @@ var standingDelta = (input) => {
22015
22251
  };
22016
22252
  var sinceStart = new Map;
22017
22253
  // packages/workit-core/src/hooks/cwd-normalize.ts
22018
- var COMPOUND = /^\s*cd\s+([A-Za-z0-9_./@%+:,=-]+|'[^'$`\\\n]*'|"[^"$`\\!\n]*")\s*&&\s*(workit(?:\s[\s\S]*)?)$/u;
22254
+ var DIR = String.raw`[A-Za-z0-9_./@%+:,=-]+|'[^'$${"`"}\\\n]*'|"[^"$${"`"}\\!\n]*"`;
22255
+ var COMPOUND = new RegExp(String.raw`^\s*cd\s+(${DIR})\s*&&\s*(workit(?:\s[\s\S]*)?)$`, "u");
22256
+ var GLOBALS_FIRST = new RegExp(String.raw`^(\s*workit)((?:\s+(?:--json|--cwd(?:=|\s+)(?:${DIR})))+)\s+([a-z][a-z-]*)(?=\s|$)`, "u");
22257
+ var VERB_FIRST = /^((?:--json\s+)*)([a-z][a-z-]*)(?=\s|$)/u;
22258
+ var VERBS = new Set(WORKIT_ALLOWED_VERBS);
22019
22259
  var FILTERS = new Set(["tail", "head", "grep"]);
22260
+ var clean = (s) => !s.unanalyzable && !s.group && !s.subs?.length && !s.stdin?.length && !s.redirects.length;
22020
22261
  var stdinOnly = (words) => {
22021
22262
  const [name, ...args] = words;
22022
22263
  const operands = args.filter((w) => !w.startsWith("-"));
@@ -22038,14 +22279,28 @@ var cwdRewrite = (command) => {
22038
22279
  return null;
22039
22280
  if (head.words.some((w) => w === "--cwd" || w.startsWith("--cwd=")))
22040
22281
  return null;
22041
- const clean = (s) => !s.unanalyzable && !s.group && !s.subs?.length && !s.stdin?.length && !s.redirects.length;
22042
22282
  if (!segments.every(clean))
22043
22283
  return null;
22044
22284
  if (!segments.slice(0, -1).every((s) => s.pipe))
22045
22285
  return null;
22046
22286
  if (!filters.every((s) => FILTERS.has(s.words[0] ?? "") && stdinOnly(s.words)))
22047
22287
  return null;
22048
- return `workit --cwd ${dir} ${rest.slice("workit".length).trimStart()}`.trimEnd();
22288
+ const args = rest.slice("workit".length).trimStart();
22289
+ const verb = VERB_FIRST.exec(args);
22290
+ if (!verb?.[2] || !VERBS.has(verb[2]))
22291
+ return `workit --cwd ${dir} ${args}`.trimEnd();
22292
+ const json = verb[1] ? " --json" : "";
22293
+ return `workit ${verb[2]} --cwd ${dir}${json}${args.slice(verb[0].length)}`.trimEnd();
22294
+ };
22295
+ var verbFirst = (command) => {
22296
+ const match = GLOBALS_FIRST.exec(command);
22297
+ const [whole, lead, globals, verb] = match ?? [];
22298
+ if (!whole || !verb || !VERBS.has(verb) || !globals?.includes("--cwd"))
22299
+ return null;
22300
+ const segments = segmentsOf(command);
22301
+ if (segments.length !== 1 || !segments.every(clean))
22302
+ return null;
22303
+ return `${lead} ${verb}${globals}${command.slice(whole.length)}`;
22049
22304
  };
22050
22305
 
22051
22306
  // packages/workit-core/src/hooks/worktree-confine.ts
@@ -22161,7 +22416,7 @@ var hostIsolated = (cwd, agentId) => {
22161
22416
  };
22162
22417
  var shown = (target) => path31.basename(target) === ".workit-live-target" ? path31.dirname(target) : target;
22163
22418
  var unboundDeny = (input, target) => {
22164
- const unblock = "Work only in the worktree your brief names, which the lead made with `workit fanout worktree create <slice>`: edit files under it and run one verb per call against it (`workit --cwd <worktree> git commit -m … -- <paths>`, `workit --cwd <worktree> check <name>`). If your brief names no worktree, stop and report that to the lead.";
22419
+ const unblock = "Work only in the worktree your brief names, which the lead made with `workit fanout worktree create <slice>`: edit files under it and run one verb per call against it (`workit git --cwd <worktree> commit -m … -- <paths>`, `workit check --cwd <worktree> <name>`). If your brief names no worktree, stop and report that to the lead.";
22165
22420
  return {
22166
22421
  kind: "deny",
22167
22422
  reason: `workit worktree confinement: implementer ${input.session.agentId} is not working in a Workit worktree yet, and ${shown(target)} is outside every worktree \`workit fanout worktree create\` made, so it would change the lead's checkout. ${unblock}`,
@@ -22169,7 +22424,7 @@ var unboundDeny = (input, target) => {
22169
22424
  };
22170
22425
  };
22171
22426
  var outsideDeny = (input, worktree, target) => {
22172
- const unblock = `Edit only under ${worktree} and run git there, one verb per call: \`workit --cwd ${worktree} git commit -m … -- <paths>\`, \`workit --cwd ${worktree} check <name>\` (or \`git -C ${worktree} …\`); a command whose writes Workit cannot see is judged by the directory it runs in. Anything outside it is a follow-up for your report.`;
22427
+ const unblock = `Edit only under ${worktree} and run git there, one verb per call: \`workit git --cwd ${worktree} commit -m … -- <paths>\`, \`workit check --cwd ${worktree} <name>\` (or \`git -C ${worktree} …\`); a command whose writes Workit cannot see is judged by the directory it runs in. Anything outside it is a follow-up for your report.`;
22173
22428
  return {
22174
22429
  kind: "deny",
22175
22430
  reason: `workit worktree confinement: implementer ${input.session.agentId} is bound to the worktree ${worktree}, and ${shown(target)} is outside it. ${unblock}`,
@@ -22178,7 +22433,7 @@ var outsideDeny = (input, worktree, target) => {
22178
22433
  };
22179
22434
  var unknownDirDeny = (input, worktree) => {
22180
22435
  const wt = worktree ?? "<worktree>";
22181
- const unblock = `Workit cannot resolve \`cd -\`, \`cd "$X"\` or \`popd\`: write by absolute path, \`git -C ${wt}\` or \`workit --cwd ${wt}\`.`;
22436
+ const unblock = `Workit cannot resolve \`cd -\`, \`cd "$X"\` or \`popd\`: write by absolute path, \`git -C ${wt}\` or \`workit <verb> --cwd ${wt}\`.`;
22182
22437
  return {
22183
22438
  kind: "deny",
22184
22439
  reason: `workit worktree confinement: implementer ${input.session.agentId} writes after an unknown directory change. ${unblock}`,
@@ -22336,7 +22591,7 @@ var subagentStartText = (input, descriptor, event) => {
22336
22591
  return `${isolatedGit(input.cwd)}
22337
22592
  ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
22338
22593
  if (role === "implementer")
22339
- return `Workit observed ${who} as an implementer: it edits and commits only in its own git worktree, the one its brief names (the lead makes it with \`workit fanout worktree create <slice>\` before spawning), never in the lead's checkout, within its brief's scope. Workit binds it to the first Workit worktree it changes: from then on an edit or a mutating git command outside that worktree is denied, and before then so is one outside every Workit worktree. Run Workit against the worktree one verb per call, never \`cd … &&\` chains or \`$VAR\` paths: \`workit --cwd <worktree> git commit -m "<message>" -- <paths>\`, \`workit --cwd <worktree> check <name>\`. The worktree is on the brief's branch once the lead made it; otherwise switch with \`workit --cwd <worktree> git branch <branch> --base <base>\` (or \`--kind <kind> --slug <slug>\`, which picks the base); branch policy hooks still deny protected or non-compliant branches, raw git included.${guide ? ` ${guide}` : " (e.g. feature/<slug>)"} Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
22594
+ return `Workit observed ${who} as an implementer: it edits and commits only in its own git worktree, the one its brief names (the lead makes it with \`workit fanout worktree create <slice>\` before spawning), never in the lead's checkout, within its brief's scope. Workit binds it to the first Workit worktree it changes: from then on an edit or a mutating git command outside that worktree is denied, and before then so is one outside every Workit worktree. Run Workit against the worktree one verb per call, never \`cd … &&\` chains or \`$VAR\` paths: \`workit git --cwd <worktree> commit -m "<message>" -- <paths>\`, \`workit check --cwd <worktree> <name>\` (verb first, so host allow rules match). The worktree is on the brief's branch once the lead made it; otherwise switch with \`workit git --cwd <worktree> branch <branch> --base <base>\` (or \`--kind <kind> --slug <slug>\`, which picks the base); branch policy hooks still deny protected or non-compliant branches, raw git included.${guide ? ` ${guide}` : " (e.g. feature/<slug>)"} Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
22340
22595
  const readOnly = `Workit observed ${who} as read-only/agent-guided.`;
22341
22596
  if (role !== "judge" || !event.agentId)
22342
22597
  return readOnly;
@@ -22353,7 +22608,7 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
22353
22608
  var PRE_TOOL = new Set(["shell.pre", "write.pre", "tool.pre"]);
22354
22609
  function handleHook(input, deps) {
22355
22610
  const decision = decide(input, deps);
22356
- if (deps.descriptor.transport !== "hook-process" || !PRE_TOOL.has(input.event.kind) || decision.kind === "deny")
22611
+ if (deps.descriptor.transport !== "hook-process" || !PRE_TOOL.has(input.event.kind) || decision.kind === "deny" || decision.kind === "ask")
22357
22612
  return decision;
22358
22613
  const delta = standingDelta(input);
22359
22614
  if (!delta)
@@ -22420,9 +22675,12 @@ function decide(input, deps) {
22420
22675
  if (plan.kind === "rewrite" && !rewrites && canDeny)
22421
22676
  return heavyDenial(event.command, plan);
22422
22677
  const lease = plan.kind === "rewrite" && rewrites ? plan : null;
22423
- const cwdCommand = !lease && rewrites && input.host === "claude_code" ? cwdRewrite(event.command) : null;
22678
+ const cwdCommand = !lease && rewrites && input.host === "claude_code" ? cwdRewrite(event.command) ?? verbFirst(event.command) : null;
22424
22679
  const aside = plan.kind === "passthrough" ? plan.note : null;
22425
22680
  noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
22681
+ const bypass = !lease && input.host === "claude_code" ? workitBypassFlag(event.command, event.dialect) : null;
22682
+ if (bypass)
22683
+ return { kind: "ask", reason: bypass };
22426
22684
  const rewrittenTo = lease?.command ?? cwdCommand;
22427
22685
  const rewritten = (text) => rewrittenTo ? {
22428
22686
  kind: "rewrite",
@@ -22849,6 +23107,17 @@ var render = (decision, native) => {
22849
23107
  },
22850
23108
  exitCode: 0
22851
23109
  };
23110
+ if (decision.kind === "ask" && native === "PreToolUse")
23111
+ return {
23112
+ json: {
23113
+ hookSpecificOutput: {
23114
+ hookEventName: "PreToolUse",
23115
+ permissionDecision: "ask",
23116
+ permissionDecisionReason: decision.reason
23117
+ }
23118
+ },
23119
+ exitCode: 0
23120
+ };
22852
23121
  if (decision.kind === "context" && native !== null && CONTEXT_EVENTS.has(native))
22853
23122
  return {
22854
23123
  json: { hookSpecificOutput: { hookEventName: native, additionalContext: decision.text } },