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