@brainervirus/workit-codex 8.24.0 → 8.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -125,7 +125,7 @@ Autonomy contract:
125
125
  CLI first: if a step has one right answer, use the \`workit\` verb instead of
126
126
  hand-running git, gh or glab (\`workit git|pr|ci|check|help\`;
127
127
  else \`npx -y @brainervirus/workit-cli\`).
128
- Other checkout: \`workit --cwd <dir> <verb>\` (Codex: \`<verb> --cwd <dir>\`); one workit
128
+ Other checkout: \`workit <verb> --cwd <dir> …\` (verb first: host rules match it); one workit
129
129
  verb per shell call, never \`cd <dir> && workit\`. zsh-safe: no \`echo =====\`
130
130
  (\`printf '%s\\n' ---\`), quote globs and URLs, no variable named \`status\` or \`path\`.
131
131
  Add Co-Authored-By via \`workit git commit --trailer "K: v" -- <paths>\`, never
@@ -137,7 +137,7 @@ authorizes an effect. Judge work once with
137
137
  \`workit policy assess --judge risk= behavior= product-choice= plan=\`.
138
138
  Across repositories, bind items to checkout, branch and endpoint, resolve
139
139
  competing targets before a mutation, and reconcile an uncertain
140
- external effect before a retry.
140
+ effect before a retry.
141
141
 
142
142
  Mid-task: never silently drop or resume an objective (workit-continue).
143
143
 
@@ -7337,6 +7337,26 @@ function resolveRef(cwd, ref) {
7337
7337
  return null;
7338
7338
  return line(git(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]));
7339
7339
  }
7340
+ function originHeadRef(cwd) {
7341
+ return line(git(cwd, ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"]));
7342
+ }
7343
+ function ownChangeDrift(cwd, range) {
7344
+ const before = diffRaw(cwd, range.fromFork, range.from);
7345
+ const after = diffRaw(cwd, range.toFork, range.to);
7346
+ if (!before || !after)
7347
+ return null;
7348
+ const judged = new Map(before.map((entry) => [entry.path, entry]));
7349
+ const current = new Map(after.map((entry) => [entry.path, entry]));
7350
+ const same = (a, b) => a !== undefined && b !== undefined && a.oldMode === b.oldMode && a.oldBlob === b.oldBlob && a.newMode === b.newMode && a.newBlob === b.newBlob;
7351
+ return [...new Set([...judged.keys(), ...current.keys()])].filter((file) => !same(judged.get(file), current.get(file))).toSorted();
7352
+ }
7353
+ function sameOwnChange(cwd, range) {
7354
+ const fromFork = mergeBase(cwd, range.fromBase, range.from);
7355
+ const toFork = mergeBase(cwd, range.toBase, range.to);
7356
+ if (!fromFork || !toFork)
7357
+ return false;
7358
+ return ownChangeDrift(cwd, { from: range.from, to: range.to, fromFork, toFork })?.length === 0;
7359
+ }
7340
7360
  var LANE_DIFF = [
7341
7361
  "-c",
7342
7362
  "core.quotePath=false",
@@ -12157,62 +12177,14 @@ import path15 from "node:path";
12157
12177
  import { spawnSync as spawnSync8 } from "node:child_process";
12158
12178
  import { createHash as createHash10, randomBytes as randomBytes3 } from "node:crypto";
12159
12179
  import fs11 from "node:fs";
12180
+ import path14 from "node:path";
12181
+
12182
+ // packages/workit-core/src/core/policy/lane.ts
12183
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
12160
12184
  import path13 from "node:path";
12161
- var LEDGER_VERSION = 1;
12162
- var MAX_LINE_BYTES = 4096;
12163
- var PASSING = new Set(["verified", "tests-verified", "type-check-only"]);
12164
- var FAILING = new Set(["failed", "blocked"]);
12165
- var STRONG_RESULTS = new Set(["verified", "tests-verified"]);
12166
- var PR_ROW_TYPES = new Set([
12167
- "pr.created",
12168
- "pr.status",
12169
- "pr.merged",
12170
- "pr.linked"
12171
- ]);
12172
- var err = (code, error, unblock) => ({
12173
- ok: false,
12174
- code,
12175
- error,
12176
- ...unblock ? { unblock } : {}
12177
- });
12178
- var gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
12179
- const run = spawnSync8("git", args, {
12180
- cwd,
12181
- encoding: "utf8",
12182
- timeout: timeoutMs,
12183
- killSignal: "SIGKILL",
12184
- windowsHide: true,
12185
- maxBuffer: 256 * 1024 * 1024,
12186
- env: {
12187
- ...process.env,
12188
- GIT_TERMINAL_PROMPT: "0",
12189
- GIT_OPTIONAL_LOCKS: "0",
12190
- LC_ALL: "C",
12191
- LANGUAGE: "C"
12192
- }
12193
- });
12194
- return {
12195
- ok: run.status === 0,
12196
- stdout: run.stdout ?? "",
12197
- stderr: run.error ? String(run.error.message) : run.stderr ?? ""
12198
- };
12199
- };
12200
- var git3 = (cwd, args) => {
12201
- const run = gitRun2(cwd, args);
12202
- const value = run.ok ? run.stdout.trim() : "";
12203
- return value ? value : null;
12204
- };
12185
+
12186
+ // packages/workit-core/src/git/default-base.ts
12205
12187
  var safeRef = (value) => value.length > 0 && !value.startsWith("-");
12206
- function resolveCommit(cwd, ref) {
12207
- if (!safeRef(ref))
12208
- return null;
12209
- return git3(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]);
12210
- }
12211
- function branchHead(cwd, branch) {
12212
- if (!safeRef(branch))
12213
- return null;
12214
- return resolveCommit(cwd, `refs/heads/${branch}`) ?? resolveCommit(cwd, `refs/remotes/origin/${branch}`);
12215
- }
12216
12188
  var trackTrunk = (cwd) => {
12217
12189
  try {
12218
12190
  const resolved = vcsConfig("resolve", cwd);
@@ -12228,11 +12200,11 @@ function defaultBase(cwd) {
12228
12200
  const trunk = trackTrunk(cwd);
12229
12201
  if (trunk) {
12230
12202
  for (const candidate of [`origin/${trunk}`, trunk])
12231
- if (resolveCommit(cwd, candidate))
12203
+ if (resolveRef(cwd, candidate))
12232
12204
  return candidate;
12233
12205
  }
12234
- const originHead = git3(cwd, ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"]);
12235
- if (originHead && resolveCommit(cwd, originHead))
12206
+ const originHead = originHeadRef(cwd);
12207
+ if (originHead && resolveRef(cwd, originHead))
12236
12208
  return originHead;
12237
12209
  for (const candidate of [
12238
12210
  "origin/main",
@@ -12242,1475 +12214,1636 @@ function defaultBase(cwd) {
12242
12214
  "origin/develop",
12243
12215
  "develop"
12244
12216
  ])
12245
- if (resolveCommit(cwd, candidate))
12217
+ if (resolveRef(cwd, candidate))
12246
12218
  return candidate;
12247
12219
  return null;
12248
12220
  }
12249
- function diffHash(cwd, base, head) {
12250
- if (!safeRef(base) || !safeRef(head))
12251
- return null;
12252
- const diff = gitRun2(cwd, [
12253
- "diff",
12254
- "-U3",
12255
- "--diff-algorithm=myers",
12256
- "--indent-heuristic",
12257
- "--no-color",
12258
- "--no-ext-diff",
12259
- "--no-textconv",
12260
- "--no-renames",
12261
- "--full-index",
12262
- "--binary",
12263
- "--src-prefix=a/",
12264
- "--dst-prefix=b/",
12265
- `${base}...${head}`,
12266
- "--"
12267
- ], GIT_TIMEOUTS.worktree);
12268
- if (!diff.ok || !diff.stdout)
12221
+
12222
+ // packages/workit-core/src/core/policy/attributes.ts
12223
+ function patternRegex(dir, pattern) {
12224
+ if (!pattern || pattern.endsWith("/") || pattern.startsWith('"') || pattern.startsWith("!"))
12269
12225
  return null;
12270
- const hash = createHash10("sha256");
12271
- for (const line of diff.stdout.split(`
12272
- `)) {
12273
- if (line.startsWith("index "))
12274
- continue;
12275
- hash.update(line.startsWith("@@ ") ? "@@" : line);
12276
- hash.update(`
12277
- `);
12278
- }
12279
- return `sha256:${hash.digest("hex")}`;
12280
- }
12281
- function storeRoot(cwd) {
12282
- const location = resolveStore(path13.resolve(cwd));
12283
- if (location instanceof Error)
12284
- 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");
12285
- return { ok: true, value: { root: location.dir, shared: location.shared } };
12286
- }
12287
- function ledgerPath(cwd) {
12288
- const root = storeRoot(cwd);
12289
- return root.ok ? { ok: true, value: path13.join(root.value.root, "ledger", "ledger.jsonl") } : root;
12290
- }
12291
- var newId2 = () => `${Date.now().toString(36)}-${randomBytes3(6).toString("hex")}`;
12292
- var NETWORK_FS_TYPES = new Set([
12293
- 26985,
12294
- 20859,
12295
- 4266872130,
12296
- 4283649346,
12297
- 1702057286,
12298
- 1397113167,
12299
- 12805120,
12300
- 16914839,
12301
- 1196443219,
12302
- 198183888
12303
- ]);
12304
- var isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0);
12305
- var onNetworkFs = (dir) => {
12306
- if (process.platform !== "linux")
12307
- return false;
12308
- try {
12309
- return isNetworkFsType(fs11.statfsSync(dir).type);
12310
- } catch {
12311
- return false;
12226
+ const anchored = pattern.includes("/");
12227
+ const body = pattern.replace(/^\//u, "");
12228
+ let source = "";
12229
+ for (let index = 0;index < body.length; index += 1) {
12230
+ const char = body[index];
12231
+ if (char === "*" && body[index + 1] === "*") {
12232
+ const slash = body[index + 2] === "/";
12233
+ source += slash ? "(?:.*/)?" : ".*";
12234
+ index += slash ? 2 : 1;
12235
+ } else if (char === "*")
12236
+ source += "[^/]*";
12237
+ else if (char === "?")
12238
+ source += "[^/]";
12239
+ else if (char === "[") {
12240
+ const end = body.indexOf("]", index + 2);
12241
+ if (end < 0)
12242
+ return null;
12243
+ const set = body.slice(index + 1, end).replace(/^!/u, "^").replace(/\\/gu, "\\\\");
12244
+ source += `[${set}]`;
12245
+ index = end;
12246
+ } else if (char === "\\" && index + 1 < body.length) {
12247
+ source += body[index + 1].replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
12248
+ index += 1;
12249
+ } else
12250
+ source += char.replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
12312
12251
  }
12313
- };
12314
- var LOCK_WAIT_MS = 5000;
12315
- var LOCK_STALE_MS = 30000;
12316
- var LOCK_OWNER = "owner";
12317
- var lockToken = (lock) => {
12252
+ const prefix = dir ? `${dir.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}/` : "";
12318
12253
  try {
12319
- return fs11.readFileSync(path13.join(lock, LOCK_OWNER), "utf8");
12254
+ return new RegExp(anchored ? `^${prefix}${source}$` : `^${prefix}(?:.*/)?${source}$`, "u");
12320
12255
  } catch {
12321
12256
  return null;
12322
12257
  }
12323
- };
12324
- var lockAgeMs = (lock) => {
12325
- for (const target of [path13.join(lock, LOCK_OWNER), lock]) {
12326
- try {
12327
- return Date.now() - fs11.statSync(target).mtimeMs;
12328
- } catch {}
12329
- }
12330
- return null;
12331
- };
12332
- var ledgerLock = {
12333
- acquire(lock) {
12334
- try {
12335
- fs11.mkdirSync(lock);
12336
- } catch (error) {
12337
- if (error.code === "EEXIST")
12338
- return null;
12339
- throw error;
12340
- }
12341
- const token = `${process.pid}-${randomBytes3(8).toString("hex")}`;
12342
- fs11.writeFileSync(path13.join(lock, LOCK_OWNER), token);
12343
- return token;
12344
- },
12345
- release(lock, token) {
12346
- if (lockToken(lock) !== token)
12347
- return false;
12348
- const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
12349
- try {
12350
- fs11.renameSync(lock, tomb);
12351
- } catch {
12352
- return false;
12353
- }
12354
- if (lockToken(tomb) !== token) {
12355
- try {
12356
- fs11.renameSync(tomb, lock);
12357
- } catch {}
12358
- return false;
12359
- }
12360
- fs11.rmSync(tomb, { recursive: true, force: true });
12361
- return true;
12362
- },
12363
- token: lockToken,
12364
- reap(lock, observed) {
12365
- const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
12366
- try {
12367
- fs11.renameSync(lock, tomb);
12368
- } catch {
12369
- return false;
12370
- }
12371
- if (lockToken(tomb) === observed) {
12372
- fs11.rmSync(tomb, { recursive: true, force: true });
12373
- return true;
12374
- }
12375
- try {
12376
- fs11.renameSync(tomb, lock);
12377
- } catch {
12378
- fs11.rmSync(tomb, { recursive: true, force: true });
12258
+ }
12259
+ function rulesOf(dir, text) {
12260
+ const out = [];
12261
+ for (const raw of text.split(`
12262
+ `)) {
12263
+ const line = raw.trim();
12264
+ if (!line || line.startsWith("#") || line.startsWith("[attr]"))
12265
+ continue;
12266
+ const [pattern, ...attributes] = line.split(/\s+/u);
12267
+ let generated = null;
12268
+ for (const attribute of attributes) {
12269
+ if (attribute === "linguist-generated" || attribute === "linguist-generated=true")
12270
+ generated = true;
12271
+ else if (attribute === "-linguist-generated" || attribute === "!linguist-generated" || attribute.startsWith("linguist-generated="))
12272
+ generated = false;
12379
12273
  }
12380
- return false;
12381
- }
12382
- };
12383
- function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
12384
- const lock = `${file}.lock`;
12385
- const deadline = Date.now() + waitMs;
12386
- let token = null;
12387
- for (;; ) {
12388
- token = ledgerLock.acquire(lock);
12389
- if (token)
12390
- break;
12391
- const observed = lockToken(lock);
12392
- const age = lockAgeMs(lock);
12393
- if (age !== null && age > LOCK_STALE_MS && ledgerLock.reap(lock, observed))
12274
+ if (generated === null)
12394
12275
  continue;
12395
- if (Date.now() >= deadline)
12396
- return null;
12397
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
12398
- }
12399
- try {
12400
- return fn();
12401
- } finally {
12402
- ledgerLock.release(lock, token);
12276
+ const regex = patternRegex(dir, pattern);
12277
+ if (regex)
12278
+ out.push({ regex, generated });
12403
12279
  }
12280
+ return out;
12404
12281
  }
12405
- var sha2562 = (text) => createHash10("sha256").update(text).digest("hex");
12406
- var chainHash = (prevHash, body) => sha2562(`${prevHash ?? ""}
12407
- ${body}`);
12408
- var TAIL_BYTES = 64 * 1024;
12409
- var lastLine = (fd, size) => {
12410
- for (let span = Math.min(size, TAIL_BYTES);span > 0; span = Math.min(size, span * 2)) {
12411
- const buffer = Buffer.alloc(span);
12412
- fs11.readSync(fd, buffer, 0, span, size - span);
12413
- const end = buffer.lastIndexOf(10);
12414
- if (end < 0) {
12415
- if (span === size)
12416
- return null;
12417
- continue;
12418
- }
12419
- let stop = end;
12420
- let text = "";
12421
- while (stop >= 0) {
12422
- const start = buffer.lastIndexOf(10, stop - 1);
12423
- text = buffer.toString("utf8", start + 1, stop).trim();
12424
- if (text) {
12425
- if (start >= 0 || span === size)
12426
- return text;
12427
- break;
12428
- }
12429
- if (start < 0) {
12430
- if (span === size)
12431
- return null;
12432
- break;
12433
- }
12434
- stop = start;
12435
- }
12436
- if (span === size)
12437
- return text || null;
12438
- }
12439
- return null;
12440
- };
12441
- var tighten = (fd) => {
12442
- try {
12443
- if ((fs11.fstatSync(fd).mode & 63) !== 0)
12444
- fs11.fchmodSync(fd, 384);
12445
- } catch {}
12446
- };
12447
- function appendLine(file, build, options) {
12448
- const write = () => {
12449
- const fd = fs11.openSync(file, "a+", 384);
12450
- try {
12451
- if (process.platform !== "win32")
12452
- tighten(fd);
12453
- const size = fs11.fstatSync(fd).size;
12454
- let fence = "";
12455
- if (size > 0) {
12456
- const last = Buffer.alloc(1);
12457
- fs11.readSync(fd, last, 0, 1, size - 1);
12458
- if (last[0] !== 10)
12459
- fence = `
12460
- `;
12461
- }
12462
- const line = build(lastLine(fd, size));
12463
- if (!line.ok)
12464
- return line;
12465
- const bytes = Buffer.from(fence + line.value);
12466
- const written = fs11.writeSync(fd, bytes);
12467
- if (written !== bytes.length)
12468
- return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
12469
- return { ok: true, value: undefined };
12470
- } finally {
12471
- fs11.closeSync(fd);
12472
- }
12473
- };
12474
- try {
12475
- fs11.mkdirSync(path13.dirname(file), { recursive: true });
12476
- if (!options.forceLock && !onNetworkFs(path13.dirname(file)))
12477
- return write();
12478
- const locked = withLedgerLock(file, write);
12479
- return locked ?? err("busy", `ledger lock ${file}.lock is held`, "retry; remove the .lock directory if no workit process is running");
12480
- } catch (error) {
12481
- return err("unavailable", `cannot append to ${file}: ${error.message}`);
12282
+ function generatedByAttributes(files, paths) {
12283
+ const rules = [...files].map(([file, text]) => {
12284
+ const dir = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
12285
+ return {
12286
+ depth: dir ? dir.split("/").length : 0,
12287
+ rules: rulesOf(dir, text)
12288
+ };
12289
+ }).toSorted((left, right) => left.depth - right.depth).flatMap((entry) => entry.rules);
12290
+ const out = new Set;
12291
+ for (const file of paths) {
12292
+ const last = rules.findLast((rule) => rule.regex.test(file));
12293
+ if (last?.generated)
12294
+ out.add(file);
12482
12295
  }
12296
+ return out;
12483
12297
  }
12484
- var sealed = (row, id, at, previous) => {
12485
- const { prevHash: _prev, rowHash: _row, ...fields } = row;
12486
- const prevHash = previous === null ? null : sha2562(previous);
12487
- const body = { v: LEDGER_VERSION, id, at, ...fields, prevHash };
12488
- const full = { ...body, rowHash: chainHash(prevHash, JSON.stringify(body)) };
12489
- return { full, line: `${JSON.stringify(full)}
12490
- ` };
12298
+
12299
+ // packages/workit-core/src/core/policy/substance.ts
12300
+ var SLASH_BLOCK = [["/*", "*/"]];
12301
+ var C_LIKE = { line: ["//"], block: SLASH_BLOCK, quotes: `"'` };
12302
+ var SYNTAXES = Object.fromEntries([
12303
+ ...["ts", "mts", "cts", "js", "mjs", "cjs"].map((ext) => [ext, { ...C_LIKE, js: true }]),
12304
+ ...["java", "kt", "kts", "scala", "swift", "dart", "groovy"].map((ext) => [
12305
+ ext,
12306
+ { ...C_LIKE, triple: true }
12307
+ ]),
12308
+ ["go", { ...C_LIKE, rawBacktick: true }],
12309
+ ...["css", "scss", "less"].map((ext) => [
12310
+ ext,
12311
+ { line: [], block: SLASH_BLOCK, quotes: `"'`, cssUrl: true }
12312
+ ]),
12313
+ ...["py", "pyi"].map((ext) => [
12314
+ ext,
12315
+ { line: ["#"], block: [], quotes: `"'`, triple: true, indented: true, fstring: true }
12316
+ ]),
12317
+ ["toml", { line: ["#"], block: [], quotes: `"`, literal: "'", triple: true }],
12318
+ ...["html", "htm", "xml", "svg", "xhtml"].map((ext) => [
12319
+ ext,
12320
+ { line: [], block: [["<!--", "-->"]], quotes: "", indented: true }
12321
+ ])
12322
+ ]);
12323
+ 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;
12324
+ var extensionOf = (file) => {
12325
+ const name = file.slice(file.lastIndexOf("/") + 1);
12326
+ const dot = name.lastIndexOf(".");
12327
+ return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
12491
12328
  };
12492
- function appendRaw(cwd, row, options) {
12493
- const file = ledgerPath(cwd);
12494
- if (!file.ok)
12495
- return file;
12496
- const id = newId2();
12497
- const at = (options.now ?? new Date).toISOString();
12498
- let full = null;
12499
- const written = appendLine(file.value, (previous) => {
12500
- const next = sealed(row, id, at, previous);
12501
- const bytes = Buffer.byteLength(next.line);
12502
- if (bytes > MAX_LINE_BYTES)
12503
- 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>`);
12504
- full = next.full;
12505
- return { ok: true, value: next.line };
12506
- }, options);
12507
- return written.ok ? { ok: true, value: full } : written;
12508
- }
12509
- function observedRowBytes(row) {
12510
- const sample = sealed({ ...row, observer: "workit_cli" }, newId2(), new Date().toISOString(), "");
12511
- return Buffer.byteLength(sample.line);
12512
- }
12513
- function appendObserved(cwd, row, options = {}) {
12514
- return appendRaw(cwd, { ...row, observer: "workit_cli" }, options);
12515
- }
12516
- function appendHookObserved(cwd, row) {
12517
- return appendRaw(cwd, { ...row, observer: "host_hook" }, {});
12329
+ var CI_PATH = /^\.github\/|(?:^|\/)(?:\.gitlab-ci\.ya?ml|Jenkinsfile|azure-pipelines\.ya?ml|\.pre-commit-config\.ya?ml|\.circleci\/.*)$/u;
12330
+ var syntaxOf = (file) => CI_PATH.test(file) ? null : SYNTAXES[extensionOf(file)] ?? null;
12331
+ var SPACE = /[ \t\r\f\v]/u;
12332
+ var WORD = /[\w$]/u;
12333
+ var NO_SPACE_AFTER = "([{,;";
12334
+ var NO_SPACE_BEFORE = ")]},;";
12335
+ var REGEX_AFTER = "(,=:[!&|?{};+-*%<>~^";
12336
+ var REGEX_KEYWORDS = new Set([
12337
+ "return",
12338
+ "typeof",
12339
+ "instanceof",
12340
+ "case",
12341
+ "do",
12342
+ "else",
12343
+ "in",
12344
+ "of",
12345
+ "new",
12346
+ "delete",
12347
+ "void",
12348
+ "throw",
12349
+ "yield",
12350
+ "await"
12351
+ ]);
12352
+ function substanceOf(file, text) {
12353
+ const syntax = syntaxOf(file);
12354
+ if (!syntax)
12355
+ return plainSubstance(text);
12356
+ const markup = syntax.block.some(([open]) => open === "<!--");
12357
+ if (markup && (/<(?:script|style)\b|<!\[CDATA\[|<\?/iu.test(text) || /[^>\s][ \t]*<!--/u.test(text)))
12358
+ return plainSubstance(text);
12359
+ return lex(syntax, text);
12518
12360
  }
12519
- var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12520
- var str = (value) => typeof value === "string" ? value : null;
12521
- var normalizeActor = (value) => {
12522
- const actor = isRecord2(value) ? value : {};
12523
- return {
12524
- host: str(actor.host) ?? "unknown",
12525
- session: str(actor.session),
12526
- agentId: str(actor.agentId)
12527
- };
12528
- };
12529
- function supersedeAllowed(by, target, onBranch = (row) => (by.branch ?? null) === (row.branch ?? null)) {
12530
- if (by.type !== target.type)
12531
- return `a ${by.type} row cannot supersede a ${target.type} row`;
12532
- if (!by.actor.session || by.actor.session !== target.actor.session)
12533
- return "only the session that wrote a row can supersede it";
12534
- if (!onBranch(target))
12535
- return `the row is on another branch (${String(target.branch ?? "none")}); a row supersedes only one on its own branch`;
12536
- if (by.type === "verdict" && (by.kind ?? "review") !== (target.kind ?? "review"))
12537
- return `a ${String(by.kind ?? "review")} verdict cannot supersede a ${String(target.kind ?? "review")} verdict; record it with --kind ${String(target.kind ?? "review")}`;
12538
- if (by.type === "verdict" && by.self === true && target.self !== true)
12539
- return "a self verdict cannot replace an independent verdict";
12540
- return null;
12361
+ function plainSubstance(text) {
12362
+ return text.split(`
12363
+ `).map((line) => line.replace(/[ \t\r\f\v]+$/u, "")).filter(Boolean).join(`
12364
+ `);
12541
12365
  }
12542
- function readLedger(cwd, options = {}) {
12543
- const file = ledgerPath(cwd);
12544
- if (!file.ok)
12545
- return file;
12546
- let text = "";
12547
- let stat = null;
12548
- try {
12549
- stat = fs11.statSync(file.value);
12550
- text = fs11.readFileSync(file.value, "utf8");
12551
- } catch (error) {
12552
- if (error.code === "ENOENT")
12553
- return {
12554
- ok: true,
12555
- value: {
12556
- path: file.value,
12557
- rows: [],
12558
- skipped: 0,
12559
- integrity: options.integrity ? { chained: 0, legacy: 0, unverified: [] } : null
12560
- }
12561
- };
12562
- return err("unavailable", `cannot read ${file.value}: ${error.message}`);
12366
+ function lex(syntax, text) {
12367
+ const collapse = !syntax.indented;
12368
+ let out = "";
12369
+ let lineStart = 0;
12370
+ let kept = 0;
12371
+ let pendingSpace = false;
12372
+ const frames = [{ kind: "code", braces: 0 }];
12373
+ let index = 0;
12374
+ if (text.startsWith("#!")) {
12375
+ const end = text.indexOf(`
12376
+ `);
12377
+ out = end < 0 ? text : text.slice(0, end);
12378
+ kept = out.length;
12379
+ index = end < 0 ? text.length : end;
12563
12380
  }
12564
- const rows = [];
12565
- let skipped = 0;
12566
- const integrity = { chained: 0, legacy: 0, unverified: [] };
12567
- const earlier = new Set([null]);
12568
- let chainStarted = false;
12569
- const ids = new Set;
12570
- for (const raw of text.split(`
12571
- `)) {
12572
- const trimmed = raw.trim();
12573
- if (!trimmed)
12574
- continue;
12575
- const linked = (prevHash) => earlier.has(prevHash);
12576
- if (options.integrity)
12577
- earlier.add(sha2562(trimmed));
12578
- let parsed;
12579
- try {
12580
- parsed = JSON.parse(trimmed);
12581
- } catch {
12582
- skipped += 1;
12583
- continue;
12584
- }
12585
- if (!isRecord2(parsed) || !str(parsed.id) || !str(parsed.type) || !str(parsed.at)) {
12586
- skipped += 1;
12587
- continue;
12381
+ const emitCode = (chunk) => {
12382
+ if (collapse && pendingSpace && out.length > lineStart) {
12383
+ const before = out[out.length - 1];
12384
+ if (!NO_SPACE_AFTER.includes(before) && !NO_SPACE_BEFORE.includes(chunk[0]))
12385
+ out += " ";
12588
12386
  }
12589
- let rowIntegrity = options.integrity ? "verified" : undefined;
12590
- let reason = null;
12591
- if (!options.integrity) {} else if (typeof parsed.rowHash === "string") {
12592
- chainStarted = true;
12593
- const { rowHash, ...body } = parsed;
12594
- const prevHash = str(parsed.prevHash);
12595
- if (chainHash(prevHash, JSON.stringify(body)) !== rowHash)
12596
- reason = "hash_mismatch";
12597
- else if (!linked(prevHash))
12598
- reason = "broken_link";
12599
- } else if (chainStarted)
12600
- reason = "unsigned";
12601
- else
12602
- rowIntegrity = "legacy";
12603
- if (options.integrity && !reason && ids.has(parsed.id))
12604
- reason = "duplicate_id";
12605
- if (options.integrity)
12606
- ids.add(parsed.id);
12607
- if (reason)
12608
- rowIntegrity = "unverified";
12609
- const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
12610
- const row = {
12611
- ...parsed,
12612
- v: typeof parsed.v === "number" ? parsed.v : LEDGER_VERSION,
12613
- id: parsed.id,
12614
- at: parsed.at,
12615
- type: parsed.type,
12616
- actor: normalizeActor(parsed.actor),
12617
- branch: str(parsed.branch),
12618
- head: str(parsed.head),
12619
- base: str(parsed.base),
12620
- baseSha: str(parsed.baseSha),
12621
- patchId: str(parsed.patchId),
12622
- diffHash: str(parsed.diffHash),
12623
- tree: str(parsed.tree),
12624
- dirty: typeof parsed.dirty === "boolean" ? parsed.dirty : null,
12625
- ...pr === undefined ? {} : { pr },
12626
- ...str(parsed.supersedes) ? { supersedes: parsed.supersedes } : {},
12627
- seq: rows.length + 1,
12628
- superseded: false,
12629
- supersedeIgnored: false,
12630
- ...rowIntegrity ? { integrity: rowIntegrity } : {},
12631
- ...reason ? { integrityReason: reason } : {}
12632
- };
12633
- rows.push(row);
12634
- if (!options.integrity)
12635
- continue;
12636
- if (rowIntegrity === "verified")
12637
- integrity.chained += 1;
12638
- else if (rowIntegrity === "legacy")
12639
- integrity.legacy += 1;
12640
- else if (reason)
12641
- integrity.unverified.push({
12642
- seq: row.seq,
12643
- id: row.id,
12644
- type: row.type,
12645
- branch: row.branch,
12646
- reason
12647
- });
12648
- }
12649
- const scopes = new Map;
12650
- const scopeOf = (branch) => {
12651
- let scope = scopes.get(branch);
12652
- if (!scope)
12653
- scopes.set(branch, scope = branchScope(cwd, branch));
12654
- return scope;
12387
+ pendingSpace = false;
12388
+ out += chunk;
12655
12389
  };
12656
- const byId = new Map;
12657
- for (const row of rows) {
12658
- const target = row.supersedes ? byId.get(row.supersedes) : undefined;
12659
- if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
12660
- target.superseded = true;
12661
- else if (row.supersedes)
12662
- row.supersedeIgnored = true;
12663
- if (!byId.has(row.id))
12664
- byId.set(row.id, row);
12665
- }
12666
- if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
12667
- tamperCache.set(file.value, {
12668
- size: stat.size,
12669
- stamp: fileStamp(stat),
12670
- breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
12671
- });
12672
- return {
12673
- ok: true,
12674
- value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
12390
+ const emitVerbatim = (chunk) => {
12391
+ emitCode(chunk);
12392
+ kept = out.length;
12675
12393
  };
12676
- }
12677
- var TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
12678
- var tamperCache = new Map;
12679
- var fileStamp = (stat) => `${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`;
12680
- function ledgerTampering(cwd) {
12681
- const file = ledgerPath(cwd);
12682
- if (!file.ok)
12683
- return [];
12684
- try {
12685
- const stat = fs11.statSync(file.value);
12686
- const cached = tamperCache.get(file.value);
12687
- if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
12688
- return cached.breaks;
12689
- } catch {
12690
- return [];
12691
- }
12692
- const read = readLedger(cwd, { integrity: true });
12693
- return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
12694
- }
12695
- function tamperedUnblock(cwd) {
12696
- const file = ledgerPath(cwd);
12697
- const where = file.ok ? file.value : "the ledger file";
12698
- 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`;
12699
- }
12700
- 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)}`;
12701
- function verdictBasis(verdict, current) {
12702
- if (verdict.head && verdict.head === current.head)
12703
- return "fresh";
12704
- if (verdict.patchId && verdict.patchId === current.patchId && verdict.diffHash && verdict.diffHash === current.diffHash)
12705
- return "carried";
12706
- return "stale";
12707
- }
12708
- var isLiveVerdict = (row, retired) => row.type === "verdict" && typeof row.result === "string" && !retired.has(row.id);
12709
- function trailerSessions(cwd, base, head) {
12710
- if (!base || !head || !safeRef(base) || !safeRef(head))
12711
- return [];
12712
- const out = git3(cwd, [
12713
- "log",
12714
- "--format=%(trailers:key=Workit-Session,valueonly,separator=%x00)",
12715
- `${base}..${head}`
12716
- ]);
12717
- return (out ?? "").split(/[\0\n]/u).map((value) => value.trim()).filter(Boolean);
12718
- }
12719
- var RENAMED = /^branch: renamed refs\/heads\/(.+) to refs\/heads\/(.+)$/iu;
12720
- function branchScope(cwd, branch) {
12721
- const renamedAt = new Map;
12722
- let since = Number.NEGATIVE_INFINITY;
12723
- if (safeRef(branch)) {
12724
- const log = git3(cwd, [
12725
- "log",
12726
- "-g",
12727
- "--date=unix",
12728
- "--format=%gd%x09%gs",
12729
- `refs/heads/${branch}`,
12730
- "--"
12731
- ]);
12732
- for (const line of (log ?? "").split(`
12733
- `)) {
12734
- const [selector = "", subject = ""] = line.split("\t");
12735
- const seconds = Number(/@\{(\d+)\}$/u.exec(selector)?.[1]);
12736
- if (!Number.isFinite(seconds))
12737
- continue;
12738
- since = seconds * 1000;
12739
- const match = RENAMED.exec(subject);
12740
- if (!match || match[1] === branch)
12741
- continue;
12742
- const until = (seconds + 1) * 1000;
12743
- renamedAt.set(match[1], Math.max(renamedAt.get(match[1]) ?? 0, until));
12394
+ const newline = () => {
12395
+ if (!collapse) {
12396
+ let end = out.length;
12397
+ while (end > Math.max(lineStart, kept) && SPACE.test(out[end - 1]))
12398
+ end -= 1;
12399
+ out = out.slice(0, end);
12400
+ }
12401
+ pendingSpace = false;
12402
+ if (out.length > lineStart) {
12403
+ out += `
12404
+ `;
12405
+ lineStart = out.length;
12744
12406
  }
12745
- }
12746
- return (row) => {
12747
- if (row.branch === branch)
12748
- return true;
12749
- const until = row.branch === null ? undefined : renamedAt.get(row.branch);
12750
- if (until === undefined)
12751
- return false;
12752
- const at = Date.parse(row.at);
12753
- return at >= since && at <= until;
12754
12407
  };
12755
- }
12756
- var renameAwareBranch = (scopeOf, branch) => {
12757
- return (target) => {
12758
- const own = typeof branch === "string" ? branch : null;
12759
- const theirs = typeof target.branch === "string" ? target.branch : null;
12760
- if (own === theirs)
12408
+ const regexAllowed = () => {
12409
+ let end = out.length;
12410
+ while (end > 0 && /\s/u.test(out[end - 1]))
12411
+ end -= 1;
12412
+ if (end === 0 || REGEX_AFTER.includes(out[end - 1]))
12761
12413
  return true;
12762
- if (own === null || theirs === null || typeof target.at !== "string")
12763
- return false;
12764
- return scopeOf(own)({ branch: theirs, at: target.at });
12414
+ let start = end;
12415
+ while (start > 0 && WORD.test(out[start - 1]))
12416
+ start -= 1;
12417
+ return REGEX_KEYWORDS.has(out.slice(start, end));
12765
12418
  };
12766
- };
12767
- function authorSessions(cwd, rows, branch, range = { base: null, head: null }, scope = branchScope(cwd, branch)) {
12768
- const sessions = new Set;
12769
- for (const row of rows) {
12770
- if (!scope(row) || row.type !== "commit.recorded" && row.type !== "task.opened")
12771
- continue;
12772
- const session = str(row.session) ?? row.actor.session;
12773
- if (session)
12774
- sessions.add(session);
12775
- }
12776
- for (const session of trailerSessions(cwd, range.base, range.head))
12777
- sessions.add(session);
12778
- return sessions;
12779
- }
12780
- var derivesFrom = (derived, bare) => !bare.includes(":") && derived.endsWith(`:${bare}`);
12781
- var authoredBy = (authors, session) => {
12782
- if (authors.has(session))
12783
- return true;
12784
- for (const author of authors)
12785
- if (derivesFrom(author, session) || derivesFrom(session, author))
12786
- return true;
12787
- return false;
12788
- };
12789
- var TRUST_REASONS = new Set([
12790
- "unsigned",
12791
- "hash_mismatch",
12792
- "broken_link",
12793
- "duplicate_id",
12794
- "unissued_session",
12795
- "unbound_session"
12796
- ]);
12797
- var SHELL_VOUCHED_HOSTS = new Set([
12798
- "cli",
12799
- "opencode",
12800
- "pi",
12801
- "codex_cli",
12802
- "codex_desktop"
12803
- ]);
12804
- function sessionTrust(cwd, row) {
12805
- const session = row.actor.session;
12806
- if (!session || row.self === true)
12807
- return null;
12808
- const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
12809
- const host = row.actor.host;
12810
- const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
12811
- if (!issuedOnly)
12812
- return null;
12813
- const binding = issuedBinding(cwd, session);
12814
- if (binding === null)
12815
- return "unissued_session";
12816
- return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
12817
- }
12818
- function trustReasons(cwd, row) {
12819
- if (row.integrity === "unverified")
12820
- return [row.integrityReason ?? "unsigned"];
12821
- if (row.integrity !== "verified")
12822
- return ["unsigned"];
12823
- const session = sessionTrust(cwd, row);
12824
- return session ? [session] : [];
12825
- }
12826
- function withIntegrity(cwd, rows) {
12827
- if (rows.every((row) => row.integrity !== undefined))
12828
- return rows;
12829
- const read = readLedger(cwd, { integrity: true });
12830
- const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
12831
- return rows.map((row) => {
12832
- if (row.integrity !== undefined)
12833
- return row;
12834
- const fresh = bySeq.get(row.seq);
12835
- return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
12836
- });
12837
- }
12838
- var untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason));
12839
- var independenceReasons = (row, authors) => {
12840
- const reasons = [];
12841
- if (!row.actor.session)
12842
- reasons.push("no_session");
12843
- else if (authoredBy(authors, row.actor.session))
12844
- reasons.push("author_session");
12845
- if (row.self === true && !reasons.includes("no_session"))
12846
- reasons.push("self");
12847
- return reasons;
12848
- };
12849
- function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
12850
- let effective = null;
12851
- for (const row of rows) {
12852
- if (!effective) {
12853
- effective = row;
12854
- continue;
12419
+ const scanString = (quote, escapes, multiline) => {
12420
+ const prefix = text.slice(Math.max(0, index - 2), index).match(/[a-z]*$/iu)?.[0] ?? "";
12421
+ const fstring = syntax.fstring === true && /f/iu.test(prefix);
12422
+ let end = index + quote.length;
12423
+ while (end < text.length) {
12424
+ const char = text[end];
12425
+ if (escapes && char === "\\") {
12426
+ end += 2;
12427
+ continue;
12428
+ }
12429
+ if (text.startsWith(quote, end)) {
12430
+ if (fstring && unbalanced(text.slice(index + quote.length, end)))
12431
+ return false;
12432
+ emitVerbatim(text.slice(index, end + quote.length));
12433
+ index = end + quote.length;
12434
+ return true;
12435
+ }
12436
+ if (char === `
12437
+ ` && !multiline)
12438
+ return false;
12439
+ end += 1;
12855
12440
  }
12856
- if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
12441
+ return false;
12442
+ };
12443
+ while (index < text.length) {
12444
+ const frame = frames[frames.length - 1];
12445
+ const char = text[index];
12446
+ if (frame.kind === "template") {
12447
+ if (char === "\\") {
12448
+ emitVerbatim(text.slice(index, index + 2));
12449
+ index += 2;
12450
+ } else if (char === "`") {
12451
+ emitVerbatim(char);
12452
+ frames.pop();
12453
+ index += 1;
12454
+ } else if (text.startsWith("${", index)) {
12455
+ emitVerbatim("${");
12456
+ frames.push({ kind: "code", braces: 0 });
12457
+ index += 2;
12458
+ } else {
12459
+ out += char;
12460
+ kept = out.length;
12461
+ index += 1;
12462
+ }
12857
12463
  continue;
12858
- const rowIndependent = reasonsOf(row).length === 0;
12859
- const effIndependent = reasonsOf(effective).length === 0;
12860
- if (effIndependent && !rowIndependent && isCurrent(effective))
12464
+ }
12465
+ if (char === `
12466
+ `) {
12467
+ newline();
12468
+ index += 1;
12861
12469
  continue;
12862
- if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
12470
+ }
12471
+ if (SPACE.test(char)) {
12472
+ if (collapse)
12473
+ pendingSpace = true;
12474
+ else
12475
+ out += char;
12476
+ index += 1;
12863
12477
  continue;
12864
- effective = row;
12865
- }
12866
- return effective;
12867
- }
12868
- function restackCarries(rows, branch, from, to) {
12869
- if (!from || !to)
12870
- return false;
12871
- const edges = new Map;
12872
- for (const row of rows)
12873
- if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.integrity === "verified" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
12874
- edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
12875
- const seen = new Set([from]);
12876
- const queue = [from];
12877
- while (queue.length) {
12878
- const next = queue.shift();
12879
- if (next === to)
12880
- return true;
12881
- for (const target of edges.get(next) ?? [])
12882
- if (!seen.has(target)) {
12883
- seen.add(target);
12884
- queue.push(target);
12885
- }
12886
- }
12887
- return false;
12888
- }
12889
- function checkVerdicts(cwd, branch, ledgerRows) {
12890
- const rows = withIntegrity(cwd, ledgerRows);
12891
- const head = branchHead(cwd, branch);
12892
- const fallbackBase = defaultBase(cwd);
12893
- const scope = branchScope(cwd, branch);
12894
- const keyCache = new Map;
12895
- const keysAt = (base, at) => {
12896
- if (!base || !at)
12897
- return { patchId: null, diffHash: null };
12898
- const cacheKey = `${base}\x00${at}`;
12899
- let key = keyCache.get(cacheKey);
12900
- if (!key) {
12901
- key = { patchId: patchId(cwd, base, at), diffHash: diffHash(cwd, base, at) };
12902
- keyCache.set(cacheKey, key);
12903
12478
  }
12904
- return key;
12905
- };
12906
- const keyFor = (base) => keysAt(base, head);
12907
- const basisOf = (row) => {
12908
- const basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
12909
- return basis === "stale" && restackCarries(rows, branch, row.head, head) ? "carried" : basis;
12910
- };
12911
- const sameCode = (failed, row) => {
12912
- if (failed.head && failed.head === row.head)
12913
- return true;
12914
- const base = failed.base ?? fallbackBase;
12915
- const stored = (row.base ?? fallbackBase) === base && row.baseSha !== null && row.baseSha === failed.baseSha;
12916
- const keys = stored ? { patchId: row.patchId, diffHash: row.diffHash } : keysAt(base, row.head);
12917
- return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(rows, branch, failed.head, row.head);
12918
- };
12919
- const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
12920
- const failureAuthorCache = new Map;
12921
- const failureAuthors = (row) => {
12922
- let set = failureAuthorCache.get(row.seq);
12923
- if (!set) {
12924
- set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
12925
- failureAuthorCache.set(row.seq, set);
12479
+ const lineOpener = syntax.line.find((opener) => text.startsWith(opener, index));
12480
+ if (lineOpener) {
12481
+ const end = text.indexOf(`
12482
+ `, index);
12483
+ const comment = text.slice(index, end < 0 ? text.length : end);
12484
+ if (DIRECTIVE.test(comment.slice(lineOpener.length)))
12485
+ emitVerbatim(comment);
12486
+ else
12487
+ pendingSpace = true;
12488
+ index = end < 0 ? text.length : end;
12489
+ continue;
12926
12490
  }
12927
- return set;
12928
- };
12929
- const reasonCache = new Map;
12930
- const reasonsOf = (row) => {
12931
- let reasons = reasonCache.get(row);
12932
- if (!reasons) {
12933
- const failing = FAILING.has(String(row.result));
12934
- reasons = [
12935
- ...trustReasons(cwd, row),
12936
- ...independenceReasons(row, failing ? failureAuthors(row) : authors)
12937
- ];
12938
- reasonCache.set(row, reasons);
12491
+ const block = syntax.block.find(([open]) => text.startsWith(open, index));
12492
+ if (block) {
12493
+ const end = text.indexOf(block[1], index + block[0].length);
12494
+ if (end < 0)
12495
+ return null;
12496
+ const comment = text.slice(index, end + block[1].length);
12497
+ if (DIRECTIVE.test(comment.slice(block[0].length, -block[1].length)))
12498
+ emitVerbatim(comment);
12499
+ else
12500
+ pendingSpace = true;
12501
+ if (comment.includes(`
12502
+ `))
12503
+ newline();
12504
+ index = end + block[1].length;
12505
+ continue;
12939
12506
  }
12940
- return reasons;
12941
- };
12942
- const retired = new Set;
12943
- for (const row of rows)
12944
- if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
12945
- retired.add(row.supersedes);
12946
- const byKind = new Map;
12947
- for (const row of rows)
12948
- if (isLiveVerdict(row, retired) && scope(row)) {
12949
- const kind = typeof row.kind === "string" ? row.kind : "review";
12950
- byKind.set(kind, [...byKind.get(kind) ?? [], row]);
12507
+ if (syntax.triple && (char === '"' || char === "'") && text.startsWith(char.repeat(3), index)) {
12508
+ if (!scanString(char.repeat(3), !syntax.literal?.includes(char), true))
12509
+ return null;
12510
+ continue;
12951
12511
  }
12952
- const verdicts = [];
12953
- const warnings = [];
12954
- for (const [kind, list] of byKind) {
12955
- const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
12956
- if (!row)
12512
+ if (syntax.quotes.includes(char)) {
12513
+ if (!scanString(char, true, Boolean(syntax.multiline)))
12514
+ return null;
12957
12515
  continue;
12958
- const basis = basisOf(row);
12959
- const current = basis !== "stale";
12960
- const independence = reasonsOf(row);
12961
- const reasons = [
12962
- ...current ? [] : ["stale"],
12963
- ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
12964
- ...independence
12965
- ];
12966
- verdicts.push({
12967
- kind,
12968
- basis,
12969
- current,
12970
- independent: independence.length === 0,
12971
- accepted: reasons.length === 0,
12972
- reasons,
12973
- verdict: row
12974
- });
12975
- if (reasons.length === 0) {
12976
- for (const later of list)
12977
- if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
12978
- 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`);
12979
12516
  }
12980
- }
12981
- for (const row of rows)
12982
- if (row.integrity === "unverified" && scope(row))
12983
- 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)`);
12984
- const tampered = ledgerTampering(cwd);
12985
- if (tampered.length) {
12986
- warnings.push(tamperedWarning(cwd, tampered));
12987
- for (const entry of verdicts) {
12988
- entry.accepted = false;
12989
- entry.reasons = [...entry.reasons, "ledger_tampered"];
12517
+ if (syntax.literal?.includes(char)) {
12518
+ if (!scanString(char, false, false))
12519
+ return null;
12520
+ continue;
12990
12521
  }
12991
- }
12992
- verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
12993
- const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
12994
- const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
12995
- const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
12996
- const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
12997
- accepted: false,
12998
- verdict: failing?.verdict ?? null,
12999
- reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
13000
- };
13001
- 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))));
13002
- const selfEntry = latestSelf && STRONG_RESULTS.has(String(latestSelf.verdict.result)) ? latestSelf : undefined;
13003
- return {
13004
- branch,
13005
- head,
13006
- review: accepted.accepted ? "verified" : selfEntry ? "self-reviewed" : "unreviewed",
13007
- selfVerdict: accepted.accepted ? null : selfEntry?.verdict ?? null,
13008
- base: newestCurrent?.verdict.base ?? fallbackBase,
13009
- patchId: keyFor(newestCurrent?.verdict.base ?? fallbackBase).patchId,
13010
- current: { basis: newestCurrent?.basis ?? "none", verdict: newestCurrent?.verdict ?? null },
13011
- accepted,
13012
- verdicts,
13013
- authors: [...authors].toSorted(),
13014
- warnings
13015
- };
13016
- }
13017
- var fromLead = (row, leads) => leads === null || row.actor.session !== null && leads.includes(row.actor.session);
13018
- function activeStanding(rows, fanout, leads = null) {
13019
- const active = [];
13020
- for (const row of rows) {
13021
- if (row.fanout !== fanout || !fromLead(row, leads))
12522
+ if (syntax.rawBacktick && char === "`") {
12523
+ if (!scanString("`", false, true))
12524
+ return null;
13022
12525
  continue;
13023
- if (row.type === "standing" && typeof row.what === "string" && row.what.trim()) {
13024
- if (!row.superseded)
13025
- active.push(row);
13026
- } else if (row.type === "standing.cleared") {
13027
- const target = str(row.target);
13028
- if (target === null)
13029
- active.length = 0;
13030
- else {
13031
- const index = active.findIndex((order) => order.id === target);
13032
- if (index >= 0)
13033
- active.splice(index, 1);
12526
+ }
12527
+ if (syntax.js && char === "`") {
12528
+ emitVerbatim(char);
12529
+ frames.push({ kind: "template" });
12530
+ index += 1;
12531
+ continue;
12532
+ }
12533
+ if (syntax.cssUrl && /^url\(\s*[^"')\s]/iu.test(text.slice(index, index + 64)))
12534
+ return null;
12535
+ if (syntax.js && char === "/" && !regexAllowed() && out.trimEnd().endsWith(")")) {
12536
+ const lineEnd = text.indexOf(`
12537
+ `, index);
12538
+ if (text.slice(index + 1, lineEnd < 0 ? text.length : lineEnd).includes("/"))
12539
+ return null;
12540
+ }
12541
+ if (syntax.js && char === "/" && regexAllowed()) {
12542
+ let end = index + 1;
12543
+ let inClass = false;
12544
+ while (end < text.length && (inClass || text[end] !== "/")) {
12545
+ if (text[end] === `
12546
+ `)
12547
+ return null;
12548
+ if (text[end] === "\\")
12549
+ end += 1;
12550
+ else if (text[end] === "[")
12551
+ inClass = true;
12552
+ else if (text[end] === "]")
12553
+ inClass = false;
12554
+ end += 1;
12555
+ }
12556
+ if (end >= text.length)
12557
+ return null;
12558
+ emitVerbatim(text.slice(index, end + 1));
12559
+ index = end + 1;
12560
+ continue;
12561
+ }
12562
+ if (frames.length > 1 && frame.kind === "code") {
12563
+ if (char === "{")
12564
+ frame.braces += 1;
12565
+ else if (char === "}") {
12566
+ if (frame.braces === 0) {
12567
+ emitVerbatim(char);
12568
+ frames.pop();
12569
+ index += 1;
12570
+ continue;
12571
+ }
12572
+ frame.braces -= 1;
13034
12573
  }
13035
12574
  }
12575
+ emitCode(char);
12576
+ index += 1;
13036
12577
  }
13037
- return active;
13038
- }
13039
- var OBSERVED_ONLY = new Set([
13040
- "check",
13041
- "merge.unverified",
13042
- "policy.judged",
13043
- ...PR_ROW_TYPES
13044
- ]);
13045
-
13046
- // packages/workit-core/src/core/policy/lane.ts
13047
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
13048
- import path14 from "node:path";
13049
- // packages/workit-core/src/forge/redact.ts
13050
- 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_.-]*`;
13051
- var ASSIGNMENT = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*["']?)((?:bearer|basic|token|digest)\s+)?([^\s"',;&]+)`, "giu");
13052
- var QUOTED_ASSIGNMENT = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*)(["'])([^"'\n]*[^\s"'][^"'\n]*)\2`, "giu");
13053
- var ANSI = new RegExp(String.raw`\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007]*\u0007`, "gu");
13054
-
13055
- // packages/workit-core/src/git/ops.ts
13056
- var COMMIT_TIMEOUT_MS = 10 * 60000;
13057
-
13058
- // packages/workit-core/src/core/policy/attributes.ts
13059
- function patternRegex(dir, pattern) {
13060
- if (!pattern || pattern.endsWith("/") || pattern.startsWith('"') || pattern.startsWith("!"))
12578
+ if (frames.length !== 1)
13061
12579
  return null;
13062
- const anchored = pattern.includes("/");
13063
- const body = pattern.replace(/^\//u, "");
13064
- let source = "";
13065
- for (let index = 0;index < body.length; index += 1) {
13066
- const char = body[index];
13067
- if (char === "*" && body[index + 1] === "*") {
13068
- const slash = body[index + 2] === "/";
13069
- source += slash ? "(?:.*/)?" : ".*";
13070
- index += slash ? 2 : 1;
13071
- } else if (char === "*")
13072
- source += "[^/]*";
13073
- else if (char === "?")
13074
- source += "[^/]";
13075
- else if (char === "[") {
13076
- const end = body.indexOf("]", index + 2);
13077
- if (end < 0)
13078
- return null;
13079
- const set = body.slice(index + 1, end).replace(/^!/u, "^").replace(/\\/gu, "\\\\");
13080
- source += `[${set}]`;
13081
- index = end;
13082
- } else if (char === "\\" && index + 1 < body.length) {
13083
- source += body[index + 1].replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
13084
- index += 1;
13085
- } else
13086
- source += char.replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
12580
+ newline();
12581
+ return out;
12582
+ }
12583
+ function unbalanced(body) {
12584
+ let depth = 0;
12585
+ for (const part of body.replaceAll("{{", "").replaceAll("}}", "")) {
12586
+ if (part === "{")
12587
+ depth += 1;
12588
+ else if (part === "}")
12589
+ depth = Math.max(0, depth - 1);
13087
12590
  }
13088
- const prefix = dir ? `${dir.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}/` : "";
12591
+ return depth > 0;
12592
+ }
12593
+ function hasOpaqueLineBreak(text) {
12594
+ return /\r(?!\n)|[\u2028\u2029]/u.test(text);
12595
+ }
12596
+ var decoder = new TextDecoder("utf-8", { fatal: true });
12597
+ function textOf(bytes) {
12598
+ if (bytes.includes(0))
12599
+ return null;
13089
12600
  try {
13090
- return new RegExp(anchored ? `^${prefix}${source}$` : `^${prefix}(?:.*/)?${source}$`, "u");
12601
+ return decoder.decode(bytes);
13091
12602
  } catch {
13092
12603
  return null;
13093
12604
  }
13094
12605
  }
13095
- function rulesOf(dir, text) {
13096
- const out = [];
13097
- for (const raw of text.split(`
13098
- `)) {
13099
- const line = raw.trim();
13100
- if (!line || line.startsWith("#") || line.startsWith("[attr]"))
13101
- continue;
13102
- const [pattern, ...attributes] = line.split(/\s+/u);
13103
- let generated = null;
13104
- for (const attribute of attributes) {
13105
- if (attribute === "linguist-generated" || attribute === "linguist-generated=true")
13106
- generated = true;
13107
- else if (attribute === "-linguist-generated" || attribute === "!linguist-generated" || attribute.startsWith("linguist-generated="))
13108
- generated = false;
13109
- }
13110
- if (generated === null)
13111
- continue;
13112
- const regex = patternRegex(dir, pattern);
13113
- if (regex)
13114
- out.push({ regex, generated });
13115
- }
13116
- return out;
13117
- }
13118
- function generatedByAttributes(files, paths) {
13119
- const rules = [...files].map(([file, text]) => {
13120
- const dir = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
13121
- return {
13122
- depth: dir ? dir.split("/").length : 0,
13123
- rules: rulesOf(dir, text)
13124
- };
13125
- }).toSorted((left, right) => left.depth - right.depth).flatMap((entry) => entry.rules);
13126
- const out = new Set;
13127
- for (const file of paths) {
13128
- const last = rules.findLast((rule) => rule.regex.test(file));
13129
- if (last?.generated)
13130
- out.add(file);
13131
- }
13132
- return out;
12606
+ function cosmeticChange(file, before, after) {
12607
+ const left = textOf(before);
12608
+ const right = textOf(after);
12609
+ if (left === null || right === null)
12610
+ return false;
12611
+ if (hasOpaqueLineBreak(left) || hasOpaqueLineBreak(right))
12612
+ return false;
12613
+ const a = substanceOf(file, left);
12614
+ return a !== null && a === substanceOf(file, right);
13133
12615
  }
13134
12616
 
13135
- // packages/workit-core/src/core/policy/substance.ts
13136
- var SLASH_BLOCK = [["/*", "*/"]];
13137
- var C_LIKE = { line: ["//"], block: SLASH_BLOCK, quotes: `"'` };
13138
- var SYNTAXES = Object.fromEntries([
13139
- ...["ts", "mts", "cts", "js", "mjs", "cjs"].map((ext) => [ext, { ...C_LIKE, js: true }]),
13140
- ...["java", "kt", "kts", "scala", "swift", "dart", "groovy"].map((ext) => [
13141
- ext,
13142
- { ...C_LIKE, triple: true }
13143
- ]),
13144
- ["go", { ...C_LIKE, rawBacktick: true }],
13145
- ...["css", "scss", "less"].map((ext) => [
13146
- ext,
13147
- { line: [], block: SLASH_BLOCK, quotes: `"'`, cssUrl: true }
13148
- ]),
13149
- ...["py", "pyi"].map((ext) => [
13150
- ext,
13151
- { line: ["#"], block: [], quotes: `"'`, triple: true, indented: true, fstring: true }
13152
- ]),
13153
- ["toml", { line: ["#"], block: [], quotes: `"`, literal: "'", triple: true }],
13154
- ...["html", "htm", "xml", "svg", "xhtml"].map((ext) => [
13155
- ext,
13156
- { line: [], block: [["<!--", "-->"]], quotes: "", indented: true }
13157
- ])
13158
- ]);
13159
- 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;
13160
- var extensionOf = (file) => {
13161
- const name = file.slice(file.lastIndexOf("/") + 1);
13162
- const dot = name.lastIndexOf(".");
13163
- return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
12617
+ // packages/workit-core/src/core/policy/lane.ts
12618
+ var LANE_THRESHOLDS = {
12619
+ selfMaxLines: 100,
12620
+ selfMaxFiles: 5,
12621
+ fullMinLines: 500
13164
12622
  };
13165
- var CI_PATH = /^\.github\/|(?:^|\/)(?:\.gitlab-ci\.ya?ml|Jenkinsfile|azure-pipelines\.ya?ml|\.pre-commit-config\.ya?ml|\.circleci\/.*)$/u;
13166
- var syntaxOf = (file) => CI_PATH.test(file) ? null : SYNTAXES[extensionOf(file)] ?? null;
13167
- var SPACE = /[ \t\r\f\v]/u;
13168
- var WORD = /[\w$]/u;
13169
- var NO_SPACE_AFTER = "([{,;";
13170
- var NO_SPACE_BEFORE = ")]},;";
13171
- var REGEX_AFTER = "(,=:[!&|?{};+-*%<>~^";
13172
- var REGEX_KEYWORDS = new Set([
13173
- "return",
13174
- "typeof",
13175
- "instanceof",
13176
- "case",
13177
- "do",
13178
- "else",
13179
- "in",
13180
- "of",
13181
- "new",
13182
- "delete",
13183
- "void",
13184
- "throw",
13185
- "yield",
13186
- "await"
12623
+ var RISK_TOKENS = new Set([
12624
+ "auth",
12625
+ "authn",
12626
+ "authz",
12627
+ "oauth",
12628
+ "security",
12629
+ "permission",
12630
+ "permissions",
12631
+ "secret",
12632
+ "secrets",
12633
+ "crypto",
12634
+ "gate",
12635
+ "gates",
12636
+ "policy",
12637
+ "policies",
12638
+ "hook",
12639
+ "hooks",
12640
+ "husky",
12641
+ "lefthook",
12642
+ "release",
12643
+ "releases",
12644
+ "migration",
12645
+ "migrations",
12646
+ "migrate",
12647
+ "workflows"
13187
12648
  ]);
13188
- function substanceOf(file, text) {
13189
- const syntax = syntaxOf(file);
13190
- if (!syntax)
13191
- return plainSubstance(text);
13192
- const markup = syntax.block.some(([open]) => open === "<!--");
13193
- if (markup && (/<(?:script|style)\b|<!\[CDATA\[|<\?/iu.test(text) || /[^>\s][ \t]*<!--/u.test(text)))
13194
- return plainSubstance(text);
13195
- return lex(syntax, text);
12649
+ var RISK_FILES = new Set([
12650
+ ".gitlab-ci.yml",
12651
+ "Jenkinsfile",
12652
+ "azure-pipelines.yml",
12653
+ ".pre-commit-config.yaml",
12654
+ ".workit/risk-paths"
12655
+ ]);
12656
+ var RISK_NAMES = new Set([".gitattributes"]);
12657
+ var RISK_PATHS_FILE = ".workit/risk-paths";
12658
+ var LOCKFILES = new Set([
12659
+ "bun.lock",
12660
+ "bun.lockb",
12661
+ "package-lock.json",
12662
+ "npm-shrinkwrap.json",
12663
+ "yarn.lock",
12664
+ "pnpm-lock.yaml",
12665
+ "Cargo.lock",
12666
+ "go.sum",
12667
+ "poetry.lock",
12668
+ "uv.lock",
12669
+ "Pipfile.lock",
12670
+ "Gemfile.lock",
12671
+ "composer.lock"
12672
+ ]);
12673
+ var DOC_EXT = /\.(?:md|mdx|markdown|rst|adoc)$/iu;
12674
+ var DOC_NAMES = /^(?:license|licence|notice|authors|changelog|changes|contributors)$/iu;
12675
+ var INSTRUCTIONS = /(?:^|\/)(?:AGENTS|CLAUDE|GEMINI|SKILL)\.md$|(?:^|\/)(?:skills|agents|commands|rules|prompts)\/[^/]+/iu;
12676
+ var TEST_PATH = /(?:^|\/)(?:test|tests|__tests__)\/|\.(?:test|spec)\.[^/]+$|_test\.[^/]+$/iu;
12677
+ var basename = (file) => file.slice(file.lastIndexOf("/") + 1);
12678
+ var isDoc = (file) => !INSTRUCTIONS.test(file) && (DOC_EXT.test(file) || DOC_NAMES.test(basename(file)));
12679
+ function kindOf(file) {
12680
+ if (file.binary)
12681
+ return "behavior";
12682
+ if (file.generated)
12683
+ return "generated";
12684
+ if (LOCKFILES.has(basename(file.path)))
12685
+ return "lockfile";
12686
+ if (isDoc(file.path))
12687
+ return "doc";
12688
+ if (file.cosmetic)
12689
+ return "cosmetic";
12690
+ return TEST_PATH.test(file.path) ? "test" : "behavior";
13196
12691
  }
13197
- function plainSubstance(text) {
13198
- return text.split(`
13199
- `).map((line) => line.replace(/[ \t\r\f\v]+$/u, "")).filter(Boolean).join(`
13200
- `);
12692
+ var globCache = new Map;
12693
+ function globMatch(pattern, file) {
12694
+ let regex = globCache.get(pattern);
12695
+ if (!regex) {
12696
+ let source = "";
12697
+ for (let index = 0;index < pattern.length; index += 1) {
12698
+ const char = pattern[index];
12699
+ if (char === "*" && pattern[index + 1] === "*") {
12700
+ source += pattern[index + 2] === "/" ? "(?:[^/]*/)*" : ".*";
12701
+ index += pattern[index + 2] === "/" ? 2 : 1;
12702
+ } else if (char === "*")
12703
+ source += "[^/]*";
12704
+ else if (char === "?")
12705
+ source += "[^/]";
12706
+ else
12707
+ source += char.replace(/[.+^${}()|[\]\\]/gu, "\\$&");
12708
+ }
12709
+ regex = new RegExp(`^${source}$`, "u");
12710
+ globCache.set(pattern, regex);
12711
+ }
12712
+ return regex.test(file) || file.startsWith(`${pattern.replace(/\/$/u, "")}/`);
13201
12713
  }
13202
- function lex(syntax, text) {
13203
- const collapse = !syntax.indented;
13204
- let out = "";
13205
- let lineStart = 0;
13206
- let kept = 0;
13207
- let pendingSpace = false;
13208
- const frames = [{ kind: "code", braces: 0 }];
13209
- let index = 0;
13210
- if (text.startsWith("#!")) {
13211
- const end = text.indexOf(`
13212
- `);
13213
- out = end < 0 ? text : text.slice(0, end);
13214
- kept = out.length;
13215
- index = end < 0 ? text.length : end;
12714
+ function isRiskPath(file, extra = []) {
12715
+ if (extra.some((pattern) => globMatch(pattern, file)))
12716
+ return true;
12717
+ if (RISK_FILES.has(file) || RISK_NAMES.has(basename(file)))
12718
+ return true;
12719
+ if (TEST_PATH.test(file) || isDoc(file))
12720
+ return false;
12721
+ 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)));
12722
+ }
12723
+ var parseRiskPaths = (text) => text.split(`
12724
+ `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.replace(/^\//u, ""));
12725
+ function computeLane(input) {
12726
+ const riskFiles = input.files.filter((file) => isRiskPath(file.path, input.riskPaths)).map((file) => file.path);
12727
+ const risky = new Set(riskFiles);
12728
+ const kinds = input.files.map((file) => ({
12729
+ file,
12730
+ kind: risky.has(file.path) ? "behavior" : kindOf(file)
12731
+ }));
12732
+ const behavior = kinds.filter((entry) => entry.kind === "behavior" || entry.kind === "test" || entry.kind === "lockfile");
12733
+ const lines = behavior.filter((entry) => entry.kind !== "lockfile").reduce((sum, entry) => sum + entry.file.added + entry.file.deleted, 0);
12734
+ const opaque = behavior.filter((entry) => entry.kind === "lockfile" || entry.file.binary);
12735
+ const result = (lane, reasons) => ({
12736
+ lane,
12737
+ reasons,
12738
+ lines,
12739
+ files: behavior.length,
12740
+ riskFiles
12741
+ });
12742
+ const { selfMaxLines, selfMaxFiles, fullMinLines } = LANE_THRESHOLDS;
12743
+ if (input.riskTier === "high")
12744
+ return result("full", ["the task judgment records high risk"]);
12745
+ if (riskFiles.length)
12746
+ return result("full", [
12747
+ `touches a risk path: ${riskFiles.slice(0, 3).join(", ")}${riskFiles.length > 3 ? ", …" : ""}`
12748
+ ]);
12749
+ if (!behavior.length)
12750
+ return result("none", [
12751
+ input.files.length ? "only docs, comments, formatting or generated copies changed" : "no changes"
12752
+ ]);
12753
+ if (lines >= fullMinLines)
12754
+ return result("full", [`large diff: ${lines} lines (≥ ${fullMinLines})`]);
12755
+ if (lines > selfMaxLines)
12756
+ return result("focused", [`${lines} lines changed (> ${selfMaxLines})`]);
12757
+ if (behavior.length > selfMaxFiles)
12758
+ return result("focused", [`${behavior.length} behavior files changed (> ${selfMaxFiles})`]);
12759
+ if (opaque.length)
12760
+ return result("focused", [
12761
+ `a lockfile or binary changed: ${opaque.slice(0, 3).map((entry) => entry.file.path).join(", ")}${opaque.length > 3 ? ", …" : ""}`
12762
+ ]);
12763
+ return result("self", [
12764
+ `small change: ${lines} line${lines === 1 ? "" : "s"} in ${behavior.length} file${behavior.length === 1 ? "" : "s"}, no risk path`
12765
+ ]);
12766
+ }
12767
+ function untrackedStat(cwd, file) {
12768
+ try {
12769
+ const bytes = readFileSync4(path13.join(cwd, file));
12770
+ if (bytes.includes(0))
12771
+ return { path: file, added: 0, deleted: 0, binary: true };
12772
+ const text = bytes.toString("utf8");
12773
+ if (hasOpaqueLineBreak(text))
12774
+ return { path: file, added: 0, deleted: 0, binary: true };
12775
+ const lines = text.split(`
12776
+ `).length - (text.endsWith(`
12777
+ `) ? 1 : 0);
12778
+ return { path: file, added: lines, deleted: 0 };
12779
+ } catch {
12780
+ return { path: file, added: 0, deleted: 0, binary: true };
13216
12781
  }
13217
- const emitCode = (chunk) => {
13218
- if (collapse && pendingSpace && out.length > lineStart) {
13219
- const before = out[out.length - 1];
13220
- if (!NO_SPACE_AFTER.includes(before) && !NO_SPACE_BEFORE.includes(chunk[0]))
13221
- out += " ";
13222
- }
13223
- pendingSpace = false;
13224
- out += chunk;
13225
- };
13226
- const emitVerbatim = (chunk) => {
13227
- emitCode(chunk);
13228
- kept = out.length;
13229
- };
13230
- const newline = () => {
13231
- if (!collapse) {
13232
- let end = out.length;
13233
- while (end > Math.max(lineStart, kept) && SPACE.test(out[end - 1]))
13234
- end -= 1;
13235
- out = out.slice(0, end);
13236
- }
13237
- pendingSpace = false;
13238
- if (out.length > lineStart) {
13239
- out += `
13240
- `;
13241
- lineStart = out.length;
13242
- }
13243
- };
13244
- const regexAllowed = () => {
13245
- let end = out.length;
13246
- while (end > 0 && /\s/u.test(out[end - 1]))
13247
- end -= 1;
13248
- if (end === 0 || REGEX_AFTER.includes(out[end - 1]))
13249
- return true;
13250
- let start = end;
13251
- while (start > 0 && WORD.test(out[start - 1]))
13252
- start -= 1;
13253
- return REGEX_KEYWORDS.has(out.slice(start, end));
13254
- };
13255
- const scanString = (quote, escapes, multiline) => {
13256
- const prefix = text.slice(Math.max(0, index - 2), index).match(/[a-z]*$/iu)?.[0] ?? "";
13257
- const fstring = syntax.fstring === true && /f/iu.test(prefix);
13258
- let end = index + quote.length;
13259
- while (end < text.length) {
13260
- const char = text[end];
13261
- if (escapes && char === "\\") {
13262
- end += 2;
13263
- continue;
13264
- }
13265
- if (text.startsWith(quote, end)) {
13266
- if (fstring && unbalanced(text.slice(index + quote.length, end)))
13267
- return false;
13268
- emitVerbatim(text.slice(index, end + quote.length));
13269
- index = end + quote.length;
13270
- return true;
12782
+ }
12783
+ var REGULAR = new Set(["100644", "100755"]);
12784
+ var ABSENT = /^0+$/u;
12785
+ function cosmeticFiles(cwd, fork, head) {
12786
+ const raw = diffRaw(cwd, fork, head);
12787
+ if (!raw)
12788
+ return null;
12789
+ const candidates = raw.filter((entry) => REGULAR.has(entry.oldMode) && entry.oldMode === entry.newMode && !ABSENT.test(entry.oldBlob) && (head === null || !ABSENT.test(entry.newBlob)));
12790
+ const blobs = readBlobs(cwd, candidates.flatMap((entry) => [entry.oldBlob, entry.newBlob]));
12791
+ if (!blobs)
12792
+ return null;
12793
+ const out = new Set;
12794
+ const opaque = new Set;
12795
+ for (const entry of candidates) {
12796
+ const before = blobs.get(entry.oldBlob);
12797
+ let after = blobs.get(entry.newBlob);
12798
+ if (!after && head === null && ABSENT.test(entry.newBlob))
12799
+ try {
12800
+ after = readFileSync4(path13.join(cwd, entry.path));
12801
+ } catch {
12802
+ after = undefined;
13271
12803
  }
13272
- if (char === `
13273
- ` && !multiline)
13274
- return false;
13275
- end += 1;
13276
- }
13277
- return false;
13278
- };
13279
- while (index < text.length) {
13280
- const frame = frames[frames.length - 1];
13281
- const char = text[index];
13282
- if (frame.kind === "template") {
13283
- if (char === "\\") {
13284
- emitVerbatim(text.slice(index, index + 2));
13285
- index += 2;
13286
- } else if (char === "`") {
13287
- emitVerbatim(char);
13288
- frames.pop();
13289
- index += 1;
13290
- } else if (text.startsWith("${", index)) {
13291
- emitVerbatim("${");
13292
- frames.push({ kind: "code", braces: 0 });
13293
- index += 2;
13294
- } else {
13295
- out += char;
13296
- kept = out.length;
13297
- index += 1;
13298
- }
13299
- continue;
13300
- }
13301
- if (char === `
13302
- `) {
13303
- newline();
13304
- index += 1;
13305
- continue;
13306
- }
13307
- if (SPACE.test(char)) {
13308
- if (collapse)
13309
- pendingSpace = true;
13310
- else
13311
- out += char;
13312
- index += 1;
13313
- continue;
13314
- }
13315
- const lineOpener = syntax.line.find((opener) => text.startsWith(opener, index));
13316
- if (lineOpener) {
13317
- const end = text.indexOf(`
13318
- `, index);
13319
- const comment = text.slice(index, end < 0 ? text.length : end);
13320
- if (DIRECTIVE.test(comment.slice(lineOpener.length)))
13321
- emitVerbatim(comment);
13322
- else
13323
- pendingSpace = true;
13324
- index = end < 0 ? text.length : end;
13325
- continue;
13326
- }
13327
- const block = syntax.block.find(([open]) => text.startsWith(open, index));
13328
- if (block) {
13329
- const end = text.indexOf(block[1], index + block[0].length);
13330
- if (end < 0)
13331
- return null;
13332
- const comment = text.slice(index, end + block[1].length);
13333
- if (DIRECTIVE.test(comment.slice(block[0].length, -block[1].length)))
13334
- emitVerbatim(comment);
13335
- else
13336
- pendingSpace = true;
13337
- if (comment.includes(`
13338
- `))
13339
- newline();
13340
- index = end + block[1].length;
13341
- continue;
13342
- }
13343
- if (syntax.triple && (char === '"' || char === "'") && text.startsWith(char.repeat(3), index)) {
13344
- if (!scanString(char.repeat(3), !syntax.literal?.includes(char), true))
13345
- return null;
13346
- continue;
13347
- }
13348
- if (syntax.quotes.includes(char)) {
13349
- if (!scanString(char, true, Boolean(syntax.multiline)))
13350
- return null;
13351
- continue;
13352
- }
13353
- if (syntax.literal?.includes(char)) {
13354
- if (!scanString(char, false, false))
13355
- return null;
13356
- continue;
13357
- }
13358
- if (syntax.rawBacktick && char === "`") {
13359
- if (!scanString("`", false, true))
13360
- return null;
13361
- continue;
13362
- }
13363
- if (syntax.js && char === "`") {
13364
- emitVerbatim(char);
13365
- frames.push({ kind: "template" });
13366
- index += 1;
13367
- continue;
13368
- }
13369
- if (syntax.cssUrl && /^url\(\s*[^"')\s]/iu.test(text.slice(index, index + 64)))
13370
- return null;
13371
- if (syntax.js && char === "/" && !regexAllowed() && out.trimEnd().endsWith(")")) {
13372
- const lineEnd = text.indexOf(`
13373
- `, index);
13374
- if (text.slice(index + 1, lineEnd < 0 ? text.length : lineEnd).includes("/"))
13375
- return null;
13376
- }
13377
- if (syntax.js && char === "/" && regexAllowed()) {
13378
- let end = index + 1;
13379
- let inClass = false;
13380
- while (end < text.length && (inClass || text[end] !== "/")) {
13381
- if (text[end] === `
13382
- `)
13383
- return null;
13384
- if (text[end] === "\\")
13385
- end += 1;
13386
- else if (text[end] === "[")
13387
- inClass = true;
13388
- else if (text[end] === "]")
13389
- inClass = false;
13390
- end += 1;
13391
- }
13392
- if (end >= text.length)
13393
- return null;
13394
- emitVerbatim(text.slice(index, end + 1));
13395
- index = end + 1;
13396
- continue;
13397
- }
13398
- if (frames.length > 1 && frame.kind === "code") {
13399
- if (char === "{")
13400
- frame.braces += 1;
13401
- else if (char === "}") {
13402
- if (frame.braces === 0) {
13403
- emitVerbatim(char);
13404
- frames.pop();
13405
- index += 1;
13406
- continue;
13407
- }
13408
- frame.braces -= 1;
13409
- }
13410
- }
13411
- emitCode(char);
13412
- index += 1;
12804
+ if (before && after && cosmeticChange(entry.path, before, after))
12805
+ out.add(entry.path);
12806
+ else if ([before, after].some((bytes) => bytes && hasOpaqueLineBreak(bytes.toString("utf8"))))
12807
+ opaque.add(entry.path);
13413
12808
  }
13414
- if (frames.length !== 1)
13415
- return null;
13416
- newline();
13417
- return out;
12809
+ return { cosmetic: out, opaque };
13418
12810
  }
13419
- function unbalanced(body) {
13420
- let depth = 0;
13421
- for (const part of body.replaceAll("{{", "").replaceAll("}}", "")) {
13422
- if (part === "{")
13423
- depth += 1;
13424
- else if (part === "}")
13425
- depth = Math.max(0, depth - 1);
12811
+ function generatedAt(cwd, fork, paths) {
12812
+ const files = treeFilesNamed(cwd, fork, ".gitattributes");
12813
+ const blobs = files?.size ? readBlobs(cwd, [...files.values()]) : null;
12814
+ if (!files || !blobs)
12815
+ return new Set;
12816
+ const texts = new Map;
12817
+ for (const [file, id] of files) {
12818
+ const bytes = blobs.get(id);
12819
+ if (bytes)
12820
+ texts.set(file, bytes.toString("utf8"));
13426
12821
  }
13427
- return depth > 0;
13428
- }
13429
- function hasOpaqueLineBreak(text) {
13430
- return /\r(?!\n)|[\u2028\u2029]/u.test(text);
12822
+ return generatedByAttributes(texts, paths);
13431
12823
  }
13432
- var decoder = new TextDecoder("utf-8", { fatal: true });
13433
- function textOf(bytes) {
13434
- if (bytes.includes(0))
12824
+ var forkOf = (cwd, base, head) => mergeBase(cwd, base, head ?? "HEAD");
12825
+ function laneFiles(cwd, fork, head, attributesAt = fork) {
12826
+ const stats = diffNumstat(cwd, fork, head);
12827
+ const cosmetic = stats ? cosmeticFiles(cwd, fork, head) : null;
12828
+ if (!stats || !cosmetic)
13435
12829
  return null;
13436
- try {
13437
- return decoder.decode(bytes);
13438
- } catch {
12830
+ const files = [
12831
+ ...stats.map((entry) => ({
12832
+ ...entry,
12833
+ ...cosmetic.opaque.has(entry.path) ? { binary: true } : {},
12834
+ cosmetic: !entry.binary && cosmetic.cosmetic.has(entry.path)
12835
+ })),
12836
+ ...head === null ? untrackedFiles(cwd).map((file) => untrackedStat(cwd, file)) : []
12837
+ ];
12838
+ const generated = generatedAt(cwd, attributesAt, files.map((entry) => entry.path));
12839
+ return files.map((entry) => ({
12840
+ ...entry,
12841
+ generated: generated.has(entry.path)
12842
+ }));
12843
+ }
12844
+ function driftFiles(cwd, range) {
12845
+ if (range.fromFork === range.toFork)
12846
+ return laneFiles(cwd, range.from, range.to, range.toFork);
12847
+ const drifted = ownChangeDrift(cwd, range);
12848
+ const stats = drifted ? diffNumstat(cwd, range.from, range.to) : null;
12849
+ if (!drifted || !stats)
13439
12850
  return null;
12851
+ const byPath = new Map(stats.map((entry) => [entry.path, entry]));
12852
+ const generated = generatedAt(cwd, range.toFork, drifted);
12853
+ return drifted.map((file) => ({
12854
+ path: file,
12855
+ added: byPath.get(file)?.added ?? 0,
12856
+ deleted: byPath.get(file)?.deleted ?? 0,
12857
+ ...byPath.get(file)?.binary ? { binary: true } : {},
12858
+ cosmetic: false,
12859
+ generated: generated.has(file)
12860
+ }));
12861
+ }
12862
+ function branchLane(cwd, options) {
12863
+ const candidates = options.base ? [`origin/${options.base}`, options.base] : [];
12864
+ const base = candidates.find((ref) => resolveRef(cwd, ref) !== null) ?? defaultBase(cwd) ?? null;
12865
+ const head = options.workingTree ? null : options.head;
12866
+ const fork = base && (options.workingTree || options.head) ? forkOf(cwd, base, head) : null;
12867
+ const files = fork ? laneFiles(cwd, fork, head) : null;
12868
+ const riskTier = options.judgment?.riskTier ?? null;
12869
+ if (!fork || !files) {
12870
+ const high = riskTier === "high";
12871
+ return {
12872
+ lane: high ? "full" : "focused",
12873
+ reasons: [
12874
+ `the diff of ${options.head?.slice(0, 12) ?? "(no head)"} against ${base ?? "(no base)"} cannot be read`,
12875
+ ...high ? ["the task judgment records high risk"] : []
12876
+ ],
12877
+ lines: 0,
12878
+ files: 0,
12879
+ riskFiles: [],
12880
+ base,
12881
+ head: options.head
12882
+ };
13440
12883
  }
12884
+ const repoRisk = parseRiskPaths(fileAt(cwd, fork, RISK_PATHS_FILE) ?? "");
12885
+ return {
12886
+ ...computeLane({
12887
+ files,
12888
+ riskTier,
12889
+ riskPaths: [...repoRisk, ...options.riskPaths ?? []]
12890
+ }),
12891
+ base,
12892
+ head: options.head
12893
+ };
13441
12894
  }
13442
- function cosmeticChange(file, before, after) {
13443
- const left = textOf(before);
13444
- const right = textOf(after);
13445
- if (left === null || right === null)
13446
- return false;
13447
- if (hasOpaqueLineBreak(left) || hasOpaqueLineBreak(right))
13448
- return false;
13449
- const a = substanceOf(file, left);
13450
- return a !== null && a === substanceOf(file, right);
12895
+ var laneNeed = (lane) => lane === "none" ? "none" : lane === "self" ? "self" : "non-author";
12896
+
12897
+ // packages/workit-core/src/core/policy/carry.ts
12898
+ var TEST_ONLY_KINDS = new Set(["doc", "cosmetic", "generated", "test"]);
12899
+ function verdictDrift(cwd, input) {
12900
+ const since = input.from;
12901
+ const stale = (reason, files = null, focused = false) => ({
12902
+ carries: false,
12903
+ since,
12904
+ files,
12905
+ focused,
12906
+ reason
12907
+ });
12908
+ const fromFork = mergeBase(cwd, input.fromBase, input.from);
12909
+ const toFork = mergeBase(cwd, input.toBase, input.to);
12910
+ const files = fromFork && toFork ? driftFiles(cwd, { from: input.from, to: input.to, fromFork, toFork }) : null;
12911
+ if (!fromFork || !toFork || !files)
12912
+ return stale(`the change since ${since.slice(0, 12)} cannot be read`);
12913
+ const riskPaths = [fromFork, toFork].flatMap((fork) => parseRiskPaths(fileAt(cwd, fork, RISK_PATHS_FILE) ?? ""));
12914
+ const lane = computeLane({ files, riskPaths });
12915
+ const paths = files.map((file) => file.path);
12916
+ if (lane.lane === "none")
12917
+ return { carries: true, rule: "lane-none", since, files: paths };
12918
+ if (input.kind === "unit" && lane.lane === "self" && files.every((file) => TEST_ONLY_KINDS.has(kindOf(file))))
12919
+ return { carries: true, rule: "tests-only", since, files: paths };
12920
+ return stale(lane.reasons[0] ?? `lane ${lane.lane}`, paths, lane.lane === "self");
13451
12921
  }
13452
12922
 
13453
- // packages/workit-core/src/core/policy/lane.ts
13454
- var LANE_THRESHOLDS = {
13455
- selfMaxLines: 100,
13456
- selfMaxFiles: 5,
13457
- fullMinLines: 500
13458
- };
13459
- var RISK_TOKENS = new Set([
13460
- "auth",
13461
- "authn",
13462
- "authz",
13463
- "oauth",
13464
- "security",
13465
- "permission",
13466
- "permissions",
13467
- "secret",
13468
- "secrets",
13469
- "crypto",
13470
- "gate",
13471
- "gates",
13472
- "policy",
13473
- "policies",
13474
- "hook",
13475
- "hooks",
13476
- "husky",
13477
- "lefthook",
13478
- "release",
13479
- "releases",
13480
- "migration",
13481
- "migrations",
13482
- "migrate",
13483
- "workflows"
13484
- ]);
13485
- var RISK_FILES = new Set([
13486
- ".gitlab-ci.yml",
13487
- "Jenkinsfile",
13488
- "azure-pipelines.yml",
13489
- ".pre-commit-config.yaml",
13490
- ".workit/risk-paths"
13491
- ]);
13492
- var RISK_NAMES = new Set([".gitattributes"]);
13493
- var RISK_PATHS_FILE = ".workit/risk-paths";
13494
- var LOCKFILES = new Set([
13495
- "bun.lock",
13496
- "bun.lockb",
13497
- "package-lock.json",
13498
- "npm-shrinkwrap.json",
13499
- "yarn.lock",
13500
- "pnpm-lock.yaml",
13501
- "Cargo.lock",
13502
- "go.sum",
13503
- "poetry.lock",
13504
- "uv.lock",
13505
- "Pipfile.lock",
13506
- "Gemfile.lock",
13507
- "composer.lock"
12923
+ // packages/workit-core/src/ledger.ts
12924
+ var LEDGER_VERSION = 1;
12925
+ var MAX_LINE_BYTES = 4096;
12926
+ var PASSING = new Set(["verified", "tests-verified", "type-check-only"]);
12927
+ var FAILING = new Set(["failed", "blocked"]);
12928
+ var STRONG_RESULTS = new Set(["verified", "tests-verified"]);
12929
+ var PR_ROW_TYPES = new Set([
12930
+ "pr.created",
12931
+ "pr.status",
12932
+ "pr.merged",
12933
+ "pr.linked"
13508
12934
  ]);
13509
- var DOC_EXT = /\.(?:md|mdx|markdown|rst|adoc|txt)$/iu;
13510
- var DOC_NAMES = /^(?:license|licence|notice|authors|changelog|changes|contributors)$/iu;
13511
- var INSTRUCTIONS = /(?:^|\/)(?:AGENTS|CLAUDE|GEMINI|SKILL)\.md$|(?:^|\/)(?:skills|agents|commands|rules|prompts)\/[^/]+/iu;
13512
- var TEST_PATH = /(?:^|\/)(?:test|tests|__tests__|spec|specs|fixtures)\/|\.(?:test|spec)\.[^/]+$/iu;
13513
- var basename = (file) => file.slice(file.lastIndexOf("/") + 1);
13514
- var isDoc = (file) => !INSTRUCTIONS.test(file) && (DOC_EXT.test(file) || DOC_NAMES.test(basename(file)));
13515
- function kindOf(file) {
13516
- if (file.binary)
13517
- return "behavior";
13518
- if (file.generated)
13519
- return "generated";
13520
- if (LOCKFILES.has(basename(file.path)))
13521
- return "lockfile";
13522
- if (isDoc(file.path))
13523
- return "doc";
13524
- if (file.cosmetic)
13525
- return "cosmetic";
13526
- return TEST_PATH.test(file.path) ? "test" : "behavior";
13527
- }
13528
- var globCache = new Map;
13529
- function globMatch(pattern, file) {
13530
- let regex = globCache.get(pattern);
13531
- if (!regex) {
13532
- let source = "";
13533
- for (let index = 0;index < pattern.length; index += 1) {
13534
- const char = pattern[index];
13535
- if (char === "*" && pattern[index + 1] === "*") {
13536
- source += pattern[index + 2] === "/" ? "(?:[^/]*/)*" : ".*";
13537
- index += pattern[index + 2] === "/" ? 2 : 1;
13538
- } else if (char === "*")
13539
- source += "[^/]*";
13540
- else if (char === "?")
13541
- source += "[^/]";
13542
- else
13543
- source += char.replace(/[.+^${}()|[\]\\]/gu, "\\$&");
12935
+ var err = (code, error, unblock) => ({
12936
+ ok: false,
12937
+ code,
12938
+ error,
12939
+ ...unblock ? { unblock } : {}
12940
+ });
12941
+ var gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
12942
+ const run = spawnSync8("git", args, {
12943
+ cwd,
12944
+ encoding: "utf8",
12945
+ timeout: timeoutMs,
12946
+ killSignal: "SIGKILL",
12947
+ windowsHide: true,
12948
+ maxBuffer: 256 * 1024 * 1024,
12949
+ env: {
12950
+ ...process.env,
12951
+ GIT_TERMINAL_PROMPT: "0",
12952
+ GIT_OPTIONAL_LOCKS: "0",
12953
+ LC_ALL: "C",
12954
+ LANGUAGE: "C"
13544
12955
  }
13545
- regex = new RegExp(`^${source}$`, "u");
13546
- globCache.set(pattern, regex);
12956
+ });
12957
+ return {
12958
+ ok: run.status === 0,
12959
+ stdout: run.stdout ?? "",
12960
+ stderr: run.error ? String(run.error.message) : run.stderr ?? ""
12961
+ };
12962
+ };
12963
+ var git3 = (cwd, args) => {
12964
+ const run = gitRun2(cwd, args);
12965
+ const value = run.ok ? run.stdout.trim() : "";
12966
+ return value ? value : null;
12967
+ };
12968
+ var safeRef2 = (value) => value.length > 0 && !value.startsWith("-");
12969
+ function resolveCommit(cwd, ref) {
12970
+ if (!safeRef2(ref))
12971
+ return null;
12972
+ return git3(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]);
12973
+ }
12974
+ function branchHead(cwd, branch) {
12975
+ if (!safeRef2(branch))
12976
+ return null;
12977
+ return resolveCommit(cwd, `refs/heads/${branch}`) ?? resolveCommit(cwd, `refs/remotes/origin/${branch}`);
12978
+ }
12979
+ function diffHash(cwd, base, head) {
12980
+ if (!safeRef2(base) || !safeRef2(head))
12981
+ return null;
12982
+ const diff = gitRun2(cwd, [
12983
+ "diff",
12984
+ "-U3",
12985
+ "--diff-algorithm=myers",
12986
+ "--indent-heuristic",
12987
+ "--no-color",
12988
+ "--no-ext-diff",
12989
+ "--no-textconv",
12990
+ "--no-renames",
12991
+ "--full-index",
12992
+ "--binary",
12993
+ "--src-prefix=a/",
12994
+ "--dst-prefix=b/",
12995
+ `${base}...${head}`,
12996
+ "--"
12997
+ ], GIT_TIMEOUTS.worktree);
12998
+ if (!diff.ok || !diff.stdout)
12999
+ return null;
13000
+ const hash = createHash10("sha256");
13001
+ for (const line of diff.stdout.split(`
13002
+ `)) {
13003
+ if (line.startsWith("index "))
13004
+ continue;
13005
+ hash.update(line.startsWith("@@ ") ? "@@" : line);
13006
+ hash.update(`
13007
+ `);
13547
13008
  }
13548
- return regex.test(file) || file.startsWith(`${pattern.replace(/\/$/u, "")}/`);
13009
+ return `sha256:${hash.digest("hex")}`;
13549
13010
  }
13550
- function isRiskPath(file, extra = []) {
13551
- if (extra.some((pattern) => globMatch(pattern, file)))
13552
- return true;
13553
- if (RISK_FILES.has(file) || RISK_NAMES.has(basename(file)))
13011
+ function storeRoot(cwd) {
13012
+ const location = resolveStore(path14.resolve(cwd));
13013
+ if (location instanceof Error)
13014
+ 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");
13015
+ return { ok: true, value: { root: location.dir, shared: location.shared } };
13016
+ }
13017
+ function ledgerPath(cwd) {
13018
+ const root = storeRoot(cwd);
13019
+ return root.ok ? { ok: true, value: path14.join(root.value.root, "ledger", "ledger.jsonl") } : root;
13020
+ }
13021
+ var newId2 = () => `${Date.now().toString(36)}-${randomBytes3(6).toString("hex")}`;
13022
+ var NETWORK_FS_TYPES = new Set([
13023
+ 26985,
13024
+ 20859,
13025
+ 4266872130,
13026
+ 4283649346,
13027
+ 1702057286,
13028
+ 1397113167,
13029
+ 12805120,
13030
+ 16914839,
13031
+ 1196443219,
13032
+ 198183888
13033
+ ]);
13034
+ var isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0);
13035
+ var onNetworkFs = (dir) => {
13036
+ if (process.platform !== "linux")
13037
+ return false;
13038
+ try {
13039
+ return isNetworkFsType(fs11.statfsSync(dir).type);
13040
+ } catch {
13041
+ return false;
13042
+ }
13043
+ };
13044
+ var LOCK_WAIT_MS = 5000;
13045
+ var LOCK_STALE_MS = 30000;
13046
+ var LOCK_OWNER = "owner";
13047
+ var lockToken = (lock) => {
13048
+ try {
13049
+ return fs11.readFileSync(path14.join(lock, LOCK_OWNER), "utf8");
13050
+ } catch {
13051
+ return null;
13052
+ }
13053
+ };
13054
+ var lockAgeMs = (lock) => {
13055
+ for (const target of [path14.join(lock, LOCK_OWNER), lock]) {
13056
+ try {
13057
+ return Date.now() - fs11.statSync(target).mtimeMs;
13058
+ } catch {}
13059
+ }
13060
+ return null;
13061
+ };
13062
+ var ledgerLock = {
13063
+ acquire(lock) {
13064
+ try {
13065
+ fs11.mkdirSync(lock);
13066
+ } catch (error) {
13067
+ if (error.code === "EEXIST")
13068
+ return null;
13069
+ throw error;
13070
+ }
13071
+ const token = `${process.pid}-${randomBytes3(8).toString("hex")}`;
13072
+ fs11.writeFileSync(path14.join(lock, LOCK_OWNER), token);
13073
+ return token;
13074
+ },
13075
+ release(lock, token) {
13076
+ if (lockToken(lock) !== token)
13077
+ return false;
13078
+ const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
13079
+ try {
13080
+ fs11.renameSync(lock, tomb);
13081
+ } catch {
13082
+ return false;
13083
+ }
13084
+ if (lockToken(tomb) !== token) {
13085
+ try {
13086
+ fs11.renameSync(tomb, lock);
13087
+ } catch {}
13088
+ return false;
13089
+ }
13090
+ fs11.rmSync(tomb, { recursive: true, force: true });
13554
13091
  return true;
13555
- if (TEST_PATH.test(file) || isDoc(file))
13092
+ },
13093
+ token: lockToken,
13094
+ reap(lock, observed) {
13095
+ const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
13096
+ try {
13097
+ fs11.renameSync(lock, tomb);
13098
+ } catch {
13099
+ return false;
13100
+ }
13101
+ if (lockToken(tomb) === observed) {
13102
+ fs11.rmSync(tomb, { recursive: true, force: true });
13103
+ return true;
13104
+ }
13105
+ try {
13106
+ fs11.renameSync(tomb, lock);
13107
+ } catch {
13108
+ fs11.rmSync(tomb, { recursive: true, force: true });
13109
+ }
13556
13110
  return false;
13557
- 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)));
13111
+ }
13112
+ };
13113
+ function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
13114
+ const lock = `${file}.lock`;
13115
+ const deadline = Date.now() + waitMs;
13116
+ let token = null;
13117
+ for (;; ) {
13118
+ token = ledgerLock.acquire(lock);
13119
+ if (token)
13120
+ break;
13121
+ const observed = lockToken(lock);
13122
+ const age = lockAgeMs(lock);
13123
+ if (age !== null && age > LOCK_STALE_MS && ledgerLock.reap(lock, observed))
13124
+ continue;
13125
+ if (Date.now() >= deadline)
13126
+ return null;
13127
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
13128
+ }
13129
+ try {
13130
+ return fn();
13131
+ } finally {
13132
+ ledgerLock.release(lock, token);
13133
+ }
13558
13134
  }
13559
- var parseRiskPaths = (text) => text.split(`
13560
- `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.replace(/^\//u, ""));
13561
- function computeLane(input) {
13562
- const riskFiles = input.files.filter((file) => isRiskPath(file.path, input.riskPaths)).map((file) => file.path);
13563
- const risky = new Set(riskFiles);
13564
- const kinds = input.files.map((file) => ({
13565
- file,
13566
- kind: risky.has(file.path) ? "behavior" : kindOf(file)
13567
- }));
13568
- const behavior = kinds.filter((entry) => entry.kind === "behavior" || entry.kind === "test" || entry.kind === "lockfile");
13569
- const lines = behavior.filter((entry) => entry.kind !== "lockfile").reduce((sum, entry) => sum + entry.file.added + entry.file.deleted, 0);
13570
- const opaque = behavior.filter((entry) => entry.kind === "lockfile" || entry.file.binary);
13571
- const result = (lane, reasons) => ({
13572
- lane,
13573
- reasons,
13574
- lines,
13575
- files: behavior.length,
13576
- riskFiles
13577
- });
13578
- const { selfMaxLines, selfMaxFiles, fullMinLines } = LANE_THRESHOLDS;
13579
- if (input.riskTier === "high")
13580
- return result("full", ["the task judgment records high risk"]);
13581
- if (riskFiles.length)
13582
- return result("full", [
13583
- `touches a risk path: ${riskFiles.slice(0, 3).join(", ")}${riskFiles.length > 3 ? ", …" : ""}`
13584
- ]);
13585
- if (!behavior.length)
13586
- return result("none", [
13587
- input.files.length ? "only docs, comments, formatting or generated copies changed" : "no changes"
13588
- ]);
13589
- if (lines >= fullMinLines)
13590
- return result("full", [`large diff: ${lines} lines (≥ ${fullMinLines})`]);
13591
- if (lines > selfMaxLines)
13592
- return result("focused", [`${lines} lines changed (> ${selfMaxLines})`]);
13593
- if (behavior.length > selfMaxFiles)
13594
- return result("focused", [`${behavior.length} behavior files changed (> ${selfMaxFiles})`]);
13595
- if (opaque.length)
13596
- return result("focused", [
13597
- `a lockfile or binary changed: ${opaque.slice(0, 3).map((entry) => entry.file.path).join(", ")}${opaque.length > 3 ? ", …" : ""}`
13598
- ]);
13599
- return result("self", [
13600
- `small change: ${lines} line${lines === 1 ? "" : "s"} in ${behavior.length} file${behavior.length === 1 ? "" : "s"}, no risk path`
13601
- ]);
13135
+ var sha2562 = (text) => createHash10("sha256").update(text).digest("hex");
13136
+ var chainHash = (prevHash, body) => sha2562(`${prevHash ?? ""}
13137
+ ${body}`);
13138
+ var TAIL_BYTES = 64 * 1024;
13139
+ var lastLine = (fd, size) => {
13140
+ for (let span = Math.min(size, TAIL_BYTES);span > 0; span = Math.min(size, span * 2)) {
13141
+ const buffer = Buffer.alloc(span);
13142
+ fs11.readSync(fd, buffer, 0, span, size - span);
13143
+ const end = buffer.lastIndexOf(10);
13144
+ if (end < 0) {
13145
+ if (span === size)
13146
+ return null;
13147
+ continue;
13148
+ }
13149
+ let stop = end;
13150
+ let text = "";
13151
+ while (stop >= 0) {
13152
+ const start = buffer.lastIndexOf(10, stop - 1);
13153
+ text = buffer.toString("utf8", start + 1, stop).trim();
13154
+ if (text) {
13155
+ if (start >= 0 || span === size)
13156
+ return text;
13157
+ break;
13158
+ }
13159
+ if (start < 0) {
13160
+ if (span === size)
13161
+ return null;
13162
+ break;
13163
+ }
13164
+ stop = start;
13165
+ }
13166
+ if (span === size)
13167
+ return text || null;
13168
+ }
13169
+ return null;
13170
+ };
13171
+ var tighten = (fd) => {
13172
+ try {
13173
+ if ((fs11.fstatSync(fd).mode & 63) !== 0)
13174
+ fs11.fchmodSync(fd, 384);
13175
+ } catch {}
13176
+ };
13177
+ function appendLine(file, build, options) {
13178
+ const write = () => {
13179
+ const fd = fs11.openSync(file, "a+", 384);
13180
+ try {
13181
+ if (process.platform !== "win32")
13182
+ tighten(fd);
13183
+ const size = fs11.fstatSync(fd).size;
13184
+ let fence = "";
13185
+ if (size > 0) {
13186
+ const last = Buffer.alloc(1);
13187
+ fs11.readSync(fd, last, 0, 1, size - 1);
13188
+ if (last[0] !== 10)
13189
+ fence = `
13190
+ `;
13191
+ }
13192
+ const line = build(lastLine(fd, size));
13193
+ if (!line.ok)
13194
+ return line;
13195
+ const bytes = Buffer.from(fence + line.value);
13196
+ const written = fs11.writeSync(fd, bytes);
13197
+ if (written !== bytes.length)
13198
+ return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
13199
+ return { ok: true, value: undefined };
13200
+ } finally {
13201
+ fs11.closeSync(fd);
13202
+ }
13203
+ };
13204
+ try {
13205
+ fs11.mkdirSync(path14.dirname(file), { recursive: true });
13206
+ if (!options.forceLock && !onNetworkFs(path14.dirname(file)))
13207
+ return write();
13208
+ const locked = withLedgerLock(file, write);
13209
+ return locked ?? err("busy", `ledger lock ${file}.lock is held`, "retry; remove the .lock directory if no workit process is running");
13210
+ } catch (error) {
13211
+ return err("unavailable", `cannot append to ${file}: ${error.message}`);
13212
+ }
13213
+ }
13214
+ var sealed = (row, id, at, previous) => {
13215
+ const { prevHash: _prev, rowHash: _row, ...fields } = row;
13216
+ const prevHash = previous === null ? null : sha2562(previous);
13217
+ const body = { v: LEDGER_VERSION, id, at, ...fields, prevHash };
13218
+ const full = { ...body, rowHash: chainHash(prevHash, JSON.stringify(body)) };
13219
+ return { full, line: `${JSON.stringify(full)}
13220
+ ` };
13221
+ };
13222
+ function appendRaw(cwd, row, options) {
13223
+ const file = ledgerPath(cwd);
13224
+ if (!file.ok)
13225
+ return file;
13226
+ const id = newId2();
13227
+ const at = (options.now ?? new Date).toISOString();
13228
+ let full = null;
13229
+ const written = appendLine(file.value, (previous) => {
13230
+ const next = sealed(row, id, at, previous);
13231
+ const bytes = Buffer.byteLength(next.line);
13232
+ if (bytes > MAX_LINE_BYTES)
13233
+ 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>`);
13234
+ full = next.full;
13235
+ return { ok: true, value: next.line };
13236
+ }, options);
13237
+ return written.ok ? { ok: true, value: full } : written;
13238
+ }
13239
+ function observedRowBytes(row) {
13240
+ const sample = sealed({ ...row, observer: "workit_cli" }, newId2(), new Date().toISOString(), "");
13241
+ return Buffer.byteLength(sample.line);
13242
+ }
13243
+ function appendObserved(cwd, row, options = {}) {
13244
+ return appendRaw(cwd, { ...row, observer: "workit_cli" }, options);
13245
+ }
13246
+ function appendHookObserved(cwd, row) {
13247
+ return appendRaw(cwd, { ...row, observer: "host_hook" }, {});
13248
+ }
13249
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13250
+ var str = (value) => typeof value === "string" ? value : null;
13251
+ var normalizeActor = (value) => {
13252
+ const actor = isRecord2(value) ? value : {};
13253
+ return {
13254
+ host: str(actor.host) ?? "unknown",
13255
+ session: str(actor.session),
13256
+ agentId: str(actor.agentId)
13257
+ };
13258
+ };
13259
+ function supersedeAllowed(by, target, onBranch = (row) => (by.branch ?? null) === (row.branch ?? null)) {
13260
+ if (by.type !== target.type)
13261
+ return `a ${by.type} row cannot supersede a ${target.type} row`;
13262
+ if (!by.actor.session || by.actor.session !== target.actor.session)
13263
+ return "only the session that wrote a row can supersede it";
13264
+ if (!onBranch(target))
13265
+ return `the row is on another branch (${String(target.branch ?? "none")}); a row supersedes only one on its own branch`;
13266
+ if (by.type === "verdict" && (by.kind ?? "review") !== (target.kind ?? "review"))
13267
+ return `a ${String(by.kind ?? "review")} verdict cannot supersede a ${String(target.kind ?? "review")} verdict; record it with --kind ${String(target.kind ?? "review")}`;
13268
+ if (by.type === "verdict" && by.self === true && target.self !== true)
13269
+ return "a self verdict cannot replace an independent verdict";
13270
+ return null;
13271
+ }
13272
+ function readLedger(cwd, options = {}) {
13273
+ const file = ledgerPath(cwd);
13274
+ if (!file.ok)
13275
+ return file;
13276
+ let text = "";
13277
+ let stat = null;
13278
+ try {
13279
+ stat = fs11.statSync(file.value);
13280
+ text = fs11.readFileSync(file.value, "utf8");
13281
+ } catch (error) {
13282
+ if (error.code === "ENOENT")
13283
+ return {
13284
+ ok: true,
13285
+ value: {
13286
+ path: file.value,
13287
+ rows: [],
13288
+ skipped: 0,
13289
+ integrity: options.integrity ? { chained: 0, legacy: 0, unverified: [] } : null
13290
+ }
13291
+ };
13292
+ return err("unavailable", `cannot read ${file.value}: ${error.message}`);
13293
+ }
13294
+ const rows = [];
13295
+ let skipped = 0;
13296
+ const integrity = { chained: 0, legacy: 0, unverified: [] };
13297
+ const earlier = new Set([null]);
13298
+ let chainStarted = false;
13299
+ const ids = new Set;
13300
+ for (const raw of text.split(`
13301
+ `)) {
13302
+ const trimmed = raw.trim();
13303
+ if (!trimmed)
13304
+ continue;
13305
+ const linked = (prevHash) => earlier.has(prevHash);
13306
+ if (options.integrity)
13307
+ earlier.add(sha2562(trimmed));
13308
+ let parsed;
13309
+ try {
13310
+ parsed = JSON.parse(trimmed);
13311
+ } catch {
13312
+ skipped += 1;
13313
+ continue;
13314
+ }
13315
+ if (!isRecord2(parsed) || !str(parsed.id) || !str(parsed.type) || !str(parsed.at)) {
13316
+ skipped += 1;
13317
+ continue;
13318
+ }
13319
+ let rowIntegrity = options.integrity ? "verified" : undefined;
13320
+ let reason = null;
13321
+ if (!options.integrity) {} else if (typeof parsed.rowHash === "string") {
13322
+ chainStarted = true;
13323
+ const { rowHash, ...body } = parsed;
13324
+ const prevHash = str(parsed.prevHash);
13325
+ if (chainHash(prevHash, JSON.stringify(body)) !== rowHash)
13326
+ reason = "hash_mismatch";
13327
+ else if (!linked(prevHash))
13328
+ reason = "broken_link";
13329
+ } else if (chainStarted)
13330
+ reason = "unsigned";
13331
+ else
13332
+ rowIntegrity = "legacy";
13333
+ if (options.integrity && !reason && ids.has(parsed.id))
13334
+ reason = "duplicate_id";
13335
+ if (options.integrity)
13336
+ ids.add(parsed.id);
13337
+ if (reason)
13338
+ rowIntegrity = "unverified";
13339
+ const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
13340
+ const row = {
13341
+ ...parsed,
13342
+ v: typeof parsed.v === "number" ? parsed.v : LEDGER_VERSION,
13343
+ id: parsed.id,
13344
+ at: parsed.at,
13345
+ type: parsed.type,
13346
+ actor: normalizeActor(parsed.actor),
13347
+ branch: str(parsed.branch),
13348
+ head: str(parsed.head),
13349
+ base: str(parsed.base),
13350
+ baseSha: str(parsed.baseSha),
13351
+ patchId: str(parsed.patchId),
13352
+ diffHash: str(parsed.diffHash),
13353
+ tree: str(parsed.tree),
13354
+ dirty: typeof parsed.dirty === "boolean" ? parsed.dirty : null,
13355
+ ...pr === undefined ? {} : { pr },
13356
+ ...str(parsed.supersedes) ? { supersedes: parsed.supersedes } : {},
13357
+ seq: rows.length + 1,
13358
+ superseded: false,
13359
+ supersedeIgnored: false,
13360
+ ...rowIntegrity ? { integrity: rowIntegrity } : {},
13361
+ ...reason ? { integrityReason: reason } : {}
13362
+ };
13363
+ rows.push(row);
13364
+ if (!options.integrity)
13365
+ continue;
13366
+ if (rowIntegrity === "verified")
13367
+ integrity.chained += 1;
13368
+ else if (rowIntegrity === "legacy")
13369
+ integrity.legacy += 1;
13370
+ else if (reason)
13371
+ integrity.unverified.push({
13372
+ seq: row.seq,
13373
+ id: row.id,
13374
+ type: row.type,
13375
+ branch: row.branch,
13376
+ reason
13377
+ });
13378
+ }
13379
+ const scopes = new Map;
13380
+ const scopeOf = (branch) => {
13381
+ let scope = scopes.get(branch);
13382
+ if (!scope)
13383
+ scopes.set(branch, scope = branchScope(cwd, branch));
13384
+ return scope;
13385
+ };
13386
+ const byId = new Map;
13387
+ for (const row of rows) {
13388
+ const target = row.supersedes ? byId.get(row.supersedes) : undefined;
13389
+ if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
13390
+ target.superseded = true;
13391
+ else if (row.supersedes)
13392
+ row.supersedeIgnored = true;
13393
+ if (!byId.has(row.id))
13394
+ byId.set(row.id, row);
13395
+ }
13396
+ if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
13397
+ tamperCache.set(file.value, {
13398
+ size: stat.size,
13399
+ stamp: fileStamp(stat),
13400
+ breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
13401
+ });
13402
+ return {
13403
+ ok: true,
13404
+ value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
13405
+ };
13602
13406
  }
13603
- function untrackedStat(cwd, file) {
13407
+ var TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
13408
+ var tamperCache = new Map;
13409
+ var fileStamp = (stat) => `${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`;
13410
+ function ledgerTampering(cwd) {
13411
+ const file = ledgerPath(cwd);
13412
+ if (!file.ok)
13413
+ return [];
13604
13414
  try {
13605
- const bytes = readFileSync4(path14.join(cwd, file));
13606
- if (bytes.includes(0))
13607
- return { path: file, added: 0, deleted: 0, binary: true };
13608
- const text = bytes.toString("utf8");
13609
- if (hasOpaqueLineBreak(text))
13610
- return { path: file, added: 0, deleted: 0, binary: true };
13611
- const lines = text.split(`
13612
- `).length - (text.endsWith(`
13613
- `) ? 1 : 0);
13614
- return { path: file, added: lines, deleted: 0 };
13415
+ const stat = fs11.statSync(file.value);
13416
+ const cached = tamperCache.get(file.value);
13417
+ if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
13418
+ return cached.breaks;
13615
13419
  } catch {
13616
- return { path: file, added: 0, deleted: 0, binary: true };
13420
+ return [];
13617
13421
  }
13422
+ const read = readLedger(cwd, { integrity: true });
13423
+ return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
13618
13424
  }
13619
- var REGULAR = new Set(["100644", "100755"]);
13620
- var ABSENT = /^0+$/u;
13621
- function cosmeticFiles(cwd, fork, head) {
13622
- const raw = diffRaw(cwd, fork, head);
13623
- if (!raw)
13425
+ function tamperedUnblock(cwd) {
13426
+ const file = ledgerPath(cwd);
13427
+ const where = file.ok ? file.value : "the ledger file";
13428
+ 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`;
13429
+ }
13430
+ 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)}`;
13431
+ function verdictBasis(verdict, current) {
13432
+ if (verdict.head && verdict.head === current.head)
13433
+ return "fresh";
13434
+ if (verdict.patchId && verdict.patchId === current.patchId && verdict.diffHash && verdict.diffHash === current.diffHash)
13435
+ return "carried";
13436
+ return "stale";
13437
+ }
13438
+ var isLiveVerdict = (row, retired) => row.type === "verdict" && typeof row.result === "string" && !retired.has(row.id);
13439
+ function trailerSessions(cwd, base, head) {
13440
+ if (!base || !head || !safeRef2(base) || !safeRef2(head))
13441
+ return [];
13442
+ const out = git3(cwd, [
13443
+ "log",
13444
+ "--format=%(trailers:key=Workit-Session,valueonly,separator=%x00)",
13445
+ `${base}..${head}`
13446
+ ]);
13447
+ return (out ?? "").split(/[\0\n]/u).map((value) => value.trim()).filter(Boolean);
13448
+ }
13449
+ var RENAMED = /^branch: renamed refs\/heads\/(.+) to refs\/heads\/(.+)$/iu;
13450
+ function branchScope(cwd, branch) {
13451
+ const renamedAt = new Map;
13452
+ let since = Number.NEGATIVE_INFINITY;
13453
+ if (safeRef2(branch)) {
13454
+ const log = git3(cwd, [
13455
+ "log",
13456
+ "-g",
13457
+ "--date=unix",
13458
+ "--format=%gd%x09%gs",
13459
+ `refs/heads/${branch}`,
13460
+ "--"
13461
+ ]);
13462
+ for (const line of (log ?? "").split(`
13463
+ `)) {
13464
+ const [selector = "", subject = ""] = line.split("\t");
13465
+ const seconds = Number(/@\{(\d+)\}$/u.exec(selector)?.[1]);
13466
+ if (!Number.isFinite(seconds))
13467
+ continue;
13468
+ since = seconds * 1000;
13469
+ const match = RENAMED.exec(subject);
13470
+ if (!match || match[1] === branch)
13471
+ continue;
13472
+ const until = (seconds + 1) * 1000;
13473
+ renamedAt.set(match[1], Math.max(renamedAt.get(match[1]) ?? 0, until));
13474
+ }
13475
+ }
13476
+ return (row) => {
13477
+ if (row.branch === branch)
13478
+ return true;
13479
+ const until = row.branch === null ? undefined : renamedAt.get(row.branch);
13480
+ if (until === undefined)
13481
+ return false;
13482
+ const at = Date.parse(row.at);
13483
+ return at >= since && at <= until;
13484
+ };
13485
+ }
13486
+ var renameAwareBranch = (scopeOf, branch) => {
13487
+ return (target) => {
13488
+ const own = typeof branch === "string" ? branch : null;
13489
+ const theirs = typeof target.branch === "string" ? target.branch : null;
13490
+ if (own === theirs)
13491
+ return true;
13492
+ if (own === null || theirs === null || typeof target.at !== "string")
13493
+ return false;
13494
+ return scopeOf(own)({ branch: theirs, at: target.at });
13495
+ };
13496
+ };
13497
+ function authorSessions(cwd, rows, branch, range = { base: null, head: null }, scope = branchScope(cwd, branch)) {
13498
+ const sessions = new Set;
13499
+ for (const row of rows) {
13500
+ if (!scope(row) || row.type !== "commit.recorded" && row.type !== "task.opened")
13501
+ continue;
13502
+ const session = str(row.session) ?? row.actor.session;
13503
+ if (session)
13504
+ sessions.add(session);
13505
+ }
13506
+ for (const session of trailerSessions(cwd, range.base, range.head))
13507
+ sessions.add(session);
13508
+ return sessions;
13509
+ }
13510
+ var derivesFrom = (derived, bare) => !bare.includes(":") && derived.endsWith(`:${bare}`);
13511
+ var authoredBy = (authors, session) => {
13512
+ if (authors.has(session))
13513
+ return true;
13514
+ for (const author of authors)
13515
+ if (derivesFrom(author, session) || derivesFrom(session, author))
13516
+ return true;
13517
+ return false;
13518
+ };
13519
+ var TRUST_REASONS = new Set([
13520
+ "unsigned",
13521
+ "hash_mismatch",
13522
+ "broken_link",
13523
+ "duplicate_id",
13524
+ "unissued_session",
13525
+ "unbound_session"
13526
+ ]);
13527
+ var SHELL_VOUCHED_HOSTS = new Set([
13528
+ "cli",
13529
+ "opencode",
13530
+ "pi",
13531
+ "codex_cli",
13532
+ "codex_desktop"
13533
+ ]);
13534
+ function sessionTrust(cwd, row) {
13535
+ const session = row.actor.session;
13536
+ if (!session || row.self === true)
13624
13537
  return null;
13625
- const candidates = raw.filter((entry) => REGULAR.has(entry.oldMode) && entry.oldMode === entry.newMode && !ABSENT.test(entry.oldBlob) && (head === null || !ABSENT.test(entry.newBlob)));
13626
- const blobs = readBlobs(cwd, candidates.flatMap((entry) => [entry.oldBlob, entry.newBlob]));
13627
- if (!blobs)
13538
+ const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
13539
+ const host = row.actor.host;
13540
+ const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
13541
+ if (!issuedOnly)
13628
13542
  return null;
13629
- const out = new Set;
13630
- const opaque = new Set;
13631
- for (const entry of candidates) {
13632
- const before = blobs.get(entry.oldBlob);
13633
- let after = blobs.get(entry.newBlob);
13634
- if (!after && head === null && ABSENT.test(entry.newBlob))
13635
- try {
13636
- after = readFileSync4(path14.join(cwd, entry.path));
13637
- } catch {
13638
- after = undefined;
13639
- }
13640
- if (before && after && cosmeticChange(entry.path, before, after))
13641
- out.add(entry.path);
13642
- else if ([before, after].some((bytes) => bytes && hasOpaqueLineBreak(bytes.toString("utf8"))))
13643
- opaque.add(entry.path);
13543
+ const binding = issuedBinding(cwd, session);
13544
+ if (binding === null)
13545
+ return "unissued_session";
13546
+ return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
13547
+ }
13548
+ function trustReasons(cwd, row) {
13549
+ if (row.integrity === "unverified")
13550
+ return [row.integrityReason ?? "unsigned"];
13551
+ if (row.integrity !== "verified")
13552
+ return ["unsigned"];
13553
+ const session = sessionTrust(cwd, row);
13554
+ return session ? [session] : [];
13555
+ }
13556
+ function withIntegrity(cwd, rows) {
13557
+ if (rows.every((row) => row.integrity !== undefined))
13558
+ return rows;
13559
+ const read = readLedger(cwd, { integrity: true });
13560
+ const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
13561
+ return rows.map((row) => {
13562
+ if (row.integrity !== undefined)
13563
+ return row;
13564
+ const fresh = bySeq.get(row.seq);
13565
+ return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
13566
+ });
13567
+ }
13568
+ var untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason));
13569
+ var independenceReasons = (row, authors) => {
13570
+ const reasons = [];
13571
+ if (!row.actor.session)
13572
+ reasons.push("no_session");
13573
+ else if (authoredBy(authors, row.actor.session))
13574
+ reasons.push("author_session");
13575
+ if (row.self === true && !reasons.includes("no_session"))
13576
+ reasons.push("self");
13577
+ return reasons;
13578
+ };
13579
+ function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
13580
+ let effective = null;
13581
+ for (const row of rows) {
13582
+ if (!effective) {
13583
+ effective = row;
13584
+ continue;
13585
+ }
13586
+ if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
13587
+ continue;
13588
+ const rowIndependent = reasonsOf(row).length === 0;
13589
+ const effIndependent = reasonsOf(effective).length === 0;
13590
+ if (effIndependent && !rowIndependent && isCurrent(effective))
13591
+ continue;
13592
+ if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
13593
+ continue;
13594
+ effective = row;
13644
13595
  }
13645
- return { cosmetic: out, opaque };
13596
+ return effective;
13646
13597
  }
13647
- function generatedAt(cwd, fork, paths) {
13648
- const files = treeFilesNamed(cwd, fork, ".gitattributes");
13649
- const blobs = files?.size ? readBlobs(cwd, [...files.values()]) : null;
13650
- if (!files || !blobs)
13651
- return new Set;
13652
- const texts = new Map;
13653
- for (const [file, id] of files) {
13654
- const bytes = blobs.get(id);
13655
- if (bytes)
13656
- texts.set(file, bytes.toString("utf8"));
13598
+ var restackExact = new WeakMap;
13599
+ function restackCarries(exactIn, rows, branch, from, to) {
13600
+ if (!from || !to)
13601
+ return false;
13602
+ const exact = (row, fromHead, head) => {
13603
+ if (exactIn === null)
13604
+ return true;
13605
+ const cwd = exactIn;
13606
+ let known = restackExact.get(row);
13607
+ if (known === undefined) {
13608
+ known = typeof row.fromBase === "string" && typeof row.toBase === "string" && sameOwnChange(cwd, {
13609
+ from: fromHead,
13610
+ to: head,
13611
+ fromBase: row.fromBase,
13612
+ toBase: row.toBase
13613
+ });
13614
+ restackExact.set(row, known);
13615
+ }
13616
+ return known;
13617
+ };
13618
+ const edges = new Map;
13619
+ for (const row of rows)
13620
+ 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))
13621
+ edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
13622
+ const seen = new Set([from]);
13623
+ const queue = [from];
13624
+ while (queue.length) {
13625
+ const next = queue.shift();
13626
+ if (next === to)
13627
+ return true;
13628
+ for (const target of edges.get(next) ?? [])
13629
+ if (!seen.has(target)) {
13630
+ seen.add(target);
13631
+ queue.push(target);
13632
+ }
13657
13633
  }
13658
- return generatedByAttributes(texts, paths);
13659
- }
13660
- var forkOf = (cwd, base, head) => mergeBase(cwd, base, head ?? "HEAD");
13661
- function laneFiles(cwd, fork, head) {
13662
- const stats = diffNumstat(cwd, fork, head);
13663
- const cosmetic = stats ? cosmeticFiles(cwd, fork, head) : null;
13664
- if (!stats || !cosmetic)
13665
- return null;
13666
- const files = [
13667
- ...stats.map((entry) => ({
13668
- ...entry,
13669
- ...cosmetic.opaque.has(entry.path) ? { binary: true } : {},
13670
- cosmetic: !entry.binary && cosmetic.cosmetic.has(entry.path)
13671
- })),
13672
- ...head === null ? untrackedFiles(cwd).map((file) => untrackedStat(cwd, file)) : []
13673
- ];
13674
- const generated = generatedAt(cwd, fork, files.map((entry) => entry.path));
13675
- return files.map((entry) => ({
13676
- ...entry,
13677
- generated: generated.has(entry.path)
13678
- }));
13634
+ return false;
13679
13635
  }
13680
- function branchLane(cwd, options) {
13681
- const candidates = options.base ? [`origin/${options.base}`, options.base] : [];
13682
- const base = candidates.find((ref) => resolveRef(cwd, ref) !== null) ?? defaultBase(cwd) ?? null;
13683
- const head = options.workingTree ? null : options.head;
13684
- const fork = base && (options.workingTree || options.head) ? forkOf(cwd, base, head) : null;
13685
- const files = fork ? laneFiles(cwd, fork, head) : null;
13686
- const riskTier = options.judgment?.riskTier ?? null;
13687
- if (!fork || !files) {
13688
- const high = riskTier === "high";
13689
- return {
13690
- lane: high ? "full" : "focused",
13691
- reasons: [
13692
- `the diff of ${options.head?.slice(0, 12) ?? "(no head)"} against ${base ?? "(no base)"} cannot be read`,
13693
- ...high ? ["the task judgment records high risk"] : []
13694
- ],
13695
- lines: 0,
13696
- files: 0,
13697
- riskFiles: [],
13698
- base,
13699
- head: options.head
13700
- };
13636
+ function checkVerdicts(cwd, branch, ledgerRows) {
13637
+ const rows = withIntegrity(cwd, ledgerRows);
13638
+ const head = branchHead(cwd, branch);
13639
+ const fallbackBase = defaultBase(cwd);
13640
+ const scope = branchScope(cwd, branch);
13641
+ const keyCache = new Map;
13642
+ const keysAt = (base, at) => {
13643
+ if (!base || !at)
13644
+ return { patchId: null, diffHash: null };
13645
+ const cacheKey = `${base}\x00${at}`;
13646
+ let key = keyCache.get(cacheKey);
13647
+ if (!key) {
13648
+ key = { patchId: patchId(cwd, base, at), diffHash: diffHash(cwd, base, at) };
13649
+ keyCache.set(cacheKey, key);
13650
+ }
13651
+ return key;
13652
+ };
13653
+ const keyFor = (base) => keysAt(base, head);
13654
+ const driftCache = new Map;
13655
+ const driftOf = (row) => {
13656
+ if (driftCache.has(row))
13657
+ return driftCache.get(row) ?? null;
13658
+ let drift = null;
13659
+ const toBase = row.base ?? fallbackBase;
13660
+ const toBaseSha = toBase ? resolveCommit(cwd, toBase) : null;
13661
+ const fromBase = row.baseSha ?? toBaseSha;
13662
+ if (row.head && head && row.head !== head && toBaseSha && fromBase && !untrusted(reasonsOf(row)))
13663
+ drift = verdictDrift(cwd, {
13664
+ from: row.head,
13665
+ to: head,
13666
+ fromBase,
13667
+ toBase: toBaseSha,
13668
+ kind: typeof row.kind === "string" ? row.kind : "review"
13669
+ });
13670
+ driftCache.set(row, drift);
13671
+ return drift;
13672
+ };
13673
+ const exactCache = new Map;
13674
+ const exactCarry = (row) => {
13675
+ if (FAILING.has(String(row.result)))
13676
+ return true;
13677
+ let exact = exactCache.get(row);
13678
+ if (exact === undefined) {
13679
+ const toBase = row.base ?? fallbackBase;
13680
+ const toBaseSha = toBase ? resolveCommit(cwd, toBase) : null;
13681
+ const fromBase = row.baseSha ?? toBaseSha;
13682
+ exact = Boolean(row.head && head && toBaseSha && fromBase && sameOwnChange(cwd, { from: row.head, to: head, fromBase, toBase: toBaseSha }));
13683
+ exactCache.set(row, exact);
13684
+ }
13685
+ return exact;
13686
+ };
13687
+ const keyBasis = (row) => {
13688
+ const basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
13689
+ return basis === "carried" && !exactCarry(row) ? "stale" : basis;
13690
+ };
13691
+ const restacked = (row) => restackCarries(FAILING.has(String(row.result)) ? null : cwd, rows, branch, row.head, head);
13692
+ const carryOf = (row) => {
13693
+ if (keyBasis(row) !== "stale")
13694
+ return null;
13695
+ if (restacked(row))
13696
+ return null;
13697
+ const drift = driftOf(row);
13698
+ return drift?.carries ? { rule: drift.rule, since: drift.since, files: drift.files } : null;
13699
+ };
13700
+ const basisOf = (row) => {
13701
+ const basis = keyBasis(row);
13702
+ if (basis !== "stale")
13703
+ return basis;
13704
+ return restacked(row) || carryOf(row) ? "carried" : basis;
13705
+ };
13706
+ const sameCode = (failed, row) => {
13707
+ if (failed.head && failed.head === row.head)
13708
+ return true;
13709
+ const base = failed.base ?? fallbackBase;
13710
+ const stored = (row.base ?? fallbackBase) === base && row.baseSha !== null && row.baseSha === failed.baseSha;
13711
+ const keys = stored ? { patchId: row.patchId, diffHash: row.diffHash } : keysAt(base, row.head);
13712
+ return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(null, rows, branch, failed.head, row.head);
13713
+ };
13714
+ const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
13715
+ const failureAuthorCache = new Map;
13716
+ const failureAuthors = (row) => {
13717
+ let set = failureAuthorCache.get(row.seq);
13718
+ if (!set) {
13719
+ set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
13720
+ failureAuthorCache.set(row.seq, set);
13721
+ }
13722
+ return set;
13723
+ };
13724
+ const reasonCache = new Map;
13725
+ const reasonsOf = (row) => {
13726
+ let reasons = reasonCache.get(row);
13727
+ if (!reasons) {
13728
+ const failing = FAILING.has(String(row.result));
13729
+ reasons = [
13730
+ ...trustReasons(cwd, row),
13731
+ ...independenceReasons(row, failing ? failureAuthors(row) : authors)
13732
+ ];
13733
+ reasonCache.set(row, reasons);
13734
+ }
13735
+ return reasons;
13736
+ };
13737
+ const retired = new Set;
13738
+ for (const row of rows)
13739
+ if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
13740
+ retired.add(row.supersedes);
13741
+ const byKind = new Map;
13742
+ for (const row of rows)
13743
+ if (isLiveVerdict(row, retired) && scope(row)) {
13744
+ const kind = typeof row.kind === "string" ? row.kind : "review";
13745
+ byKind.set(kind, [...byKind.get(kind) ?? [], row]);
13746
+ }
13747
+ const verdicts = [];
13748
+ const warnings = [];
13749
+ for (const [kind, list] of byKind) {
13750
+ const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
13751
+ if (!row)
13752
+ continue;
13753
+ const basis = basisOf(row);
13754
+ const current = basis !== "stale";
13755
+ const independence = reasonsOf(row);
13756
+ const reasons = [
13757
+ ...current ? [] : ["stale"],
13758
+ ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
13759
+ ...independence
13760
+ ];
13761
+ verdicts.push({
13762
+ kind,
13763
+ basis,
13764
+ current,
13765
+ independent: independence.length === 0,
13766
+ accepted: reasons.length === 0,
13767
+ reasons,
13768
+ carry: basis === "carried" ? carryOf(row) : null,
13769
+ verdict: row
13770
+ });
13771
+ if (reasons.length === 0) {
13772
+ for (const later of list)
13773
+ if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
13774
+ 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`);
13775
+ }
13701
13776
  }
13702
- const repoRisk = parseRiskPaths(fileAt(cwd, fork, RISK_PATHS_FILE) ?? "");
13777
+ for (const row of rows)
13778
+ if (row.integrity === "unverified" && scope(row))
13779
+ 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)`);
13780
+ const tampered = ledgerTampering(cwd);
13781
+ if (tampered.length) {
13782
+ warnings.push(tamperedWarning(cwd, tampered));
13783
+ for (const entry of verdicts) {
13784
+ entry.accepted = false;
13785
+ entry.reasons = [...entry.reasons, "ledger_tampered"];
13786
+ }
13787
+ }
13788
+ verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
13789
+ const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
13790
+ const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
13791
+ const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
13792
+ const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
13793
+ accepted: false,
13794
+ verdict: failing?.verdict ?? null,
13795
+ reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
13796
+ };
13797
+ 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))));
13798
+ const selfEntry = latestSelf && STRONG_RESULTS.has(String(latestSelf.verdict.result)) ? latestSelf : undefined;
13799
+ const carryEntry = verdicts.find((entry) => entry.verdict === accepted.verdict && entry.accepted) ?? newestCurrent;
13800
+ const lastPass = accepted.accepted || failing || tampered.length ? undefined : verdicts.findLast((entry) => !entry.current && !untrusted(entry.reasons) && STRONG_RESULTS.has(String(entry.verdict.result)));
13801
+ const drift = lastPass ? driftOf(lastPass.verdict) : null;
13802
+ const reverify = drift && !drift.carries && drift.focused && drift.files ? { scope: "focused", since: drift.since, files: drift.files } : null;
13703
13803
  return {
13704
- ...computeLane({
13705
- files,
13706
- riskTier,
13707
- riskPaths: [...repoRisk, ...options.riskPaths ?? []]
13708
- }),
13709
- base,
13710
- head: options.head
13804
+ branch,
13805
+ head,
13806
+ review: accepted.accepted ? "verified" : selfEntry ? "self-reviewed" : "unreviewed",
13807
+ selfVerdict: accepted.accepted ? null : selfEntry?.verdict ?? null,
13808
+ base: newestCurrent?.verdict.base ?? fallbackBase,
13809
+ patchId: keyFor(newestCurrent?.verdict.base ?? fallbackBase).patchId,
13810
+ current: { basis: newestCurrent?.basis ?? "none", verdict: newestCurrent?.verdict ?? null },
13811
+ carry: carryEntry?.carry ?? null,
13812
+ reverify,
13813
+ accepted,
13814
+ verdicts,
13815
+ authors: [...authors].toSorted(),
13816
+ warnings
13711
13817
  };
13712
13818
  }
13713
- var laneNeed = (lane) => lane === "none" ? "none" : lane === "self" ? "self" : "non-author";
13819
+ var fromLead = (row, leads) => leads === null || row.actor.session !== null && leads.includes(row.actor.session);
13820
+ function activeStanding(rows, fanout, leads = null) {
13821
+ const active = [];
13822
+ for (const row of rows) {
13823
+ if (row.fanout !== fanout || !fromLead(row, leads))
13824
+ continue;
13825
+ if (row.type === "standing" && typeof row.what === "string" && row.what.trim()) {
13826
+ if (!row.superseded)
13827
+ active.push(row);
13828
+ } else if (row.type === "standing.cleared") {
13829
+ const target = str(row.target);
13830
+ if (target === null)
13831
+ active.length = 0;
13832
+ else {
13833
+ const index = active.findIndex((order) => order.id === target);
13834
+ if (index >= 0)
13835
+ active.splice(index, 1);
13836
+ }
13837
+ }
13838
+ }
13839
+ return active;
13840
+ }
13841
+ var OBSERVED_ONLY = new Set([
13842
+ "check",
13843
+ "merge.unverified",
13844
+ "policy.judged",
13845
+ ...PR_ROW_TYPES
13846
+ ]);
13714
13847
 
13715
13848
  // packages/workit-core/src/core/policy/derive.ts
13716
13849
  var RULES = {
@@ -17724,6 +17857,18 @@ var substitutions = (body) => {
17724
17857
  };
17725
17858
  var PLAIN_POSIX = /[^\s'"\\`$(){},<>|;&#]+/uy;
17726
17859
  var PLAIN_POWERSHELL = /[^\s'"`(){}<>|;&#]+/uy;
17860
+ var expandsInQuotes = (body, dialect) => {
17861
+ const escape = dialect === "posix" ? "\\" : "`";
17862
+ for (let index = 0;index < body.length; index++) {
17863
+ const char = body[index];
17864
+ if (char === escape)
17865
+ index++;
17866
+ else if (char === "$" || dialect === "posix" && char === "`")
17867
+ return true;
17868
+ }
17869
+ return false;
17870
+ };
17871
+ var EXPANDING_RUN = /[*?[]/u;
17727
17872
  var arithmeticEnd = (command, start) => {
17728
17873
  const end = substitutionEnd(command, start);
17729
17874
  return command[end - 1] === ")" && end > start + 2 ? end : 0;
@@ -17742,6 +17887,7 @@ var tokenize = (command, dialect) => {
17742
17887
  let current = { words: [], redirects: [] };
17743
17888
  let word = "";
17744
17889
  let inWord = false;
17890
+ let expanding = false;
17745
17891
  let pending = null;
17746
17892
  const heredocs = [];
17747
17893
  const escape = posix ? "\\" : "`";
@@ -17756,11 +17902,16 @@ var tokenize = (command, dialect) => {
17756
17902
  heredocs.push({ delimiter: text.replace(/^-/, ""), owner: current });
17757
17903
  else if (pending === "herestring")
17758
17904
  (current.stdin ??= []).push(text);
17759
- else if (pending !== "input")
17905
+ else if (pending !== "input") {
17906
+ const at = current.words.length;
17760
17907
  current.words.push(...MARKERS.test(word) ? expandBraces(word) : [word]);
17908
+ if (expanding || !posix && word.startsWith("@"))
17909
+ (current.expands ??= []).push(...current.words.slice(at).map((_, i) => at + i));
17910
+ }
17761
17911
  pending = null;
17762
17912
  word = "";
17763
17913
  inWord = false;
17914
+ expanding = false;
17764
17915
  };
17765
17916
  const flushSegment = () => {
17766
17917
  flushWord();
@@ -17775,6 +17926,7 @@ var tokenize = (command, dialect) => {
17775
17926
  };
17776
17927
  const substitution = (inner, text) => {
17777
17928
  (current.subs ??= []).push(inner);
17929
+ expanding = true;
17778
17930
  word += text;
17779
17931
  inWord = true;
17780
17932
  };
@@ -17785,6 +17937,8 @@ var tokenize = (command, dialect) => {
17785
17937
  if (run) {
17786
17938
  word += run[0];
17787
17939
  inWord = true;
17940
+ if (EXPANDING_RUN.test(run[0]))
17941
+ expanding = true;
17788
17942
  index += run[0].length - 1;
17789
17943
  continue;
17790
17944
  }
@@ -17813,6 +17967,8 @@ var tokenize = (command, dialect) => {
17813
17967
  const body = command.slice(index + 1, end < 0 ? undefined : end);
17814
17968
  if (posix && char === '"' && /\$\(|`/.test(body))
17815
17969
  (current.subs ??= []).push(...substitutions(body));
17970
+ if (char === '"' && expandsInQuotes(body, dialect))
17971
+ expanding = true;
17816
17972
  word += unquote(body, char, dialect);
17817
17973
  inWord = true;
17818
17974
  index = end < 0 ? command.length : end;
@@ -17858,6 +18014,8 @@ var tokenize = (command, dialect) => {
17858
18014
  if (char === "(" && inWord && word.endsWith("$")) {
17859
18015
  word = word.slice(0, -1);
17860
18016
  inWord = word !== "";
18017
+ if (!inWord)
18018
+ expanding = false;
17861
18019
  }
17862
18020
  group(char === "(" ? "open" : "close");
17863
18021
  } else if (!posix && (char === "{" || char === "}")) {
@@ -17911,6 +18069,8 @@ var tokenize = (command, dialect) => {
17911
18069
  else if (posix && char === "}" && word.includes(OPEN))
17912
18070
  word += CLOSE;
17913
18071
  else {
18072
+ if (char === "$")
18073
+ expanding = true;
17914
18074
  word += char;
17915
18075
  inWord = true;
17916
18076
  }
@@ -17965,7 +18125,7 @@ var GIT_VALUE_OPTIONS = new Set([
17965
18125
  ]);
17966
18126
  var GIT_CONFIG_OPTIONS = new Set(["-c", "--config-env", "--git-dir"]);
17967
18127
  var GIT_CONFIG_ENV = /^GIT_(?:CONFIG\w*|DIR|COMMON_DIR|WORK_TREE)$/;
17968
- var ASSIGNMENT2 = /^([A-Za-z_]\w*)=([\s\S]*)$/;
18128
+ var ASSIGNMENT = /^([A-Za-z_]\w*)=([\s\S]*)$/;
17969
18129
  var MAX_DEPTH = 4;
17970
18130
  var MAX_EXPANSIONS = 16;
17971
18131
  var isAbsolute = (value) => path20.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
@@ -17992,7 +18152,7 @@ var stripPrefixes = (input) => {
17992
18152
  const first = words[0];
17993
18153
  if (first === undefined)
17994
18154
  return { words, env };
17995
- const assignment = ASSIGNMENT2.exec(first);
18155
+ const assignment = ASSIGNMENT.exec(first);
17996
18156
  if (assignment) {
17997
18157
  if (GIT_CONFIG_ENV.test(assignment[1]))
17998
18158
  env[assignment[1]] = assignment[2];
@@ -18051,10 +18211,10 @@ var assignments = (words, vars) => {
18051
18211
  while (rest[0]?.startsWith("-"))
18052
18212
  rest = rest.slice(1);
18053
18213
  }
18054
- if (rest.length === 0 || !rest.every((word) => ASSIGNMENT2.test(word)))
18214
+ if (rest.length === 0 || !rest.every((word) => ASSIGNMENT.test(word)))
18055
18215
  return null;
18056
18216
  return rest.map((word) => {
18057
- const [, name, value] = ASSIGNMENT2.exec(word);
18217
+ const [, name, value] = ASSIGNMENT.exec(word);
18058
18218
  return [name, expandWord(value, vars)];
18059
18219
  });
18060
18220
  };
@@ -18242,7 +18402,7 @@ var splitString = (line) => {
18242
18402
  return line.slice(index).join(" ").slice(15);
18243
18403
  if (/^-S./.test(word))
18244
18404
  return [word.slice(2), ...line.slice(index + 1)].join(" ");
18245
- if (!word.startsWith("-") && !ASSIGNMENT2.test(word))
18405
+ if (!word.startsWith("-") && !ASSIGNMENT.test(word))
18246
18406
  return null;
18247
18407
  }
18248
18408
  return null;
@@ -18355,8 +18515,17 @@ function walk2(command, dialect, depth, seen) {
18355
18515
  visit(next, undefined, hops + 1);
18356
18516
  };
18357
18517
  const wrapped = seen ? null : workitCheckTail(words);
18358
- if (wrapped)
18359
- return "script" in wrapped ? nested(wrapped.script, "posix") : runs([wrapped.argv]);
18518
+ if (wrapped) {
18519
+ const outer = dir;
18520
+ if (wrapped.cwd !== null)
18521
+ dir = joinDir(dir, wrapped.cwd);
18522
+ if ("script" in wrapped)
18523
+ nested(wrapped.script, "posix");
18524
+ else
18525
+ runs([wrapped.argv]);
18526
+ dir = outer;
18527
+ return;
18528
+ }
18360
18529
  if (CD.has(name)) {
18361
18530
  const target = cdTarget(words);
18362
18531
  dir = target === null ? os5.homedir() : joinDir(dir, target);
@@ -18457,14 +18626,14 @@ function workitCheckTail(words) {
18457
18626
  return null;
18458
18627
  } else if (!/^workit(?:\.exe|\.cmd)?$/.test(commandName(words[0])))
18459
18628
  return null;
18460
- const args = words.slice(at + 1);
18629
+ const { rest: args, cwd } = skipWorkitGlobals(words.slice(at + 1));
18461
18630
  if (args[0] !== "check" && !(args[0] === "lock" && args[1] === "run"))
18462
18631
  return null;
18463
18632
  const split = args.indexOf("--");
18464
18633
  const tail = split < 0 ? [] : args.slice(split + 1);
18465
18634
  if (!tail.length)
18466
18635
  return null;
18467
- return args.slice(0, split).includes("--shell") ? { script: tail.join(" ") } : { argv: tail };
18636
+ return args.slice(0, split).includes("--shell") ? { script: tail.join(" "), cwd } : { argv: tail, cwd };
18468
18637
  }
18469
18638
  function simpleCommand(command, dialect = "posix", depth = 0) {
18470
18639
  let segments;
@@ -18525,7 +18694,21 @@ function skipWorkitGlobals(args) {
18525
18694
  } else if (args[at] !== "--json")
18526
18695
  break;
18527
18696
  }
18528
- return { rest: args.slice(at), cwd };
18697
+ const rest = [];
18698
+ for (;at < args.length; at++) {
18699
+ const arg = args[at];
18700
+ if (arg === "--") {
18701
+ rest.push(...args.slice(at));
18702
+ break;
18703
+ }
18704
+ if (arg.startsWith("--cwd="))
18705
+ cwd = arg.slice("--cwd=".length);
18706
+ else if (arg === "--cwd" && args[at + 1] !== undefined)
18707
+ cwd = args[++at];
18708
+ else
18709
+ rest.push(arg);
18710
+ }
18711
+ return { rest, cwd };
18529
18712
  }
18530
18713
  var PUSH_VALUE_OPTIONS = new Set(["-o", "--push-option", "--receive-pack", "--exec", "--repo"]);
18531
18714
  var CURRENT_BRANCH = /^\$\(\s*git\s+(?:branch\s+--show-current|rev-parse\s+--abbrev-ref\s+HEAD|symbolic-ref\s+--short\s+HEAD)\s*\)$/;
@@ -18971,7 +19154,7 @@ var rewrittenUrl = (url, config) => {
18971
19154
  }
18972
19155
  return url;
18973
19156
  };
18974
- var pushUrl2 = (config, remote, dir) => {
19157
+ var pushUrl = (config, remote, dir) => {
18975
19158
  const configured = configValue(config, `remote.${remote}.pushurl`) ?? configValue(config, `remote.${remote}.url`);
18976
19159
  return normalizedUrl(rewrittenUrl(configured ?? remote, config), dir);
18977
19160
  };
@@ -18999,7 +19182,7 @@ var toPushRemote = (repo, remote, config) => {
18999
19182
  const pushRemote = pushRemoteOf(config, repo.branch);
19000
19183
  if (pushRemote === null || pushRemote === remote)
19001
19184
  return true;
19002
- return pushUrl2(config, remote, repo.dir) === pushUrl2(config, pushRemote, repo.dir);
19185
+ return pushUrl(config, remote, repo.dir) === pushUrl(config, pushRemote, repo.dir);
19003
19186
  };
19004
19187
  var quote = (text) => `\`${text}\``;
19005
19188
  var refusal = (action, branch, dir) => {
@@ -19537,6 +19720,53 @@ var shellPolicy = (cwd, command, dialect = "posix") => {
19537
19720
  }
19538
19721
  return policy && !policy.ok ? { kind: "deny", reason: `branch_policy_denied: ${policy.error}`, unblock: null } : { kind: "none" };
19539
19722
  };
19723
+ // packages/workit-core/src/hooks/delivery-verbs.ts
19724
+ var WORKIT_ALLOWED_VERBS = [
19725
+ "git",
19726
+ "pr",
19727
+ "ci",
19728
+ "stack",
19729
+ "release",
19730
+ "task",
19731
+ "ledger",
19732
+ "evidence",
19733
+ "finding",
19734
+ "decision",
19735
+ "worker",
19736
+ "verify-delivery",
19737
+ "policy",
19738
+ "doctor",
19739
+ "help"
19740
+ ];
19741
+ var WORKIT_BYPASS_FLAGS = ["--unverified", "--overwrite-unintegrated"];
19742
+ var ALLOWED = new Set(WORKIT_ALLOWED_VERBS);
19743
+ var isBypass = (word) => WORKIT_BYPASS_FLAGS.some((flag) => word === flag || word.startsWith(`${flag}=`));
19744
+ var isWorkitWord = (word) => /^(?:.*[\\/])?workit(?:-cli)?$/u.test(word);
19745
+ var bypassReason = (flag) => `Workit: \`${flag}\` overrides a Workit gate, so the user decides this call (no allow rule covers it).`;
19746
+ var EXPANSION_REASON = "Workit: an expansion before `--` could become a bypass flag, so the user decides this call (no allow rule covers it).";
19747
+ var workitBypassFlag = (command, dialect = "posix") => {
19748
+ for (const segment of segmentsOf(command, dialect)) {
19749
+ if (segment.unanalyzable) {
19750
+ const text = command.replaceAll(/['"\\`]/gu, "");
19751
+ if (!/\bworkit\b/u.test(text))
19752
+ return null;
19753
+ const found = WORKIT_BYPASS_FLAGS.find((flag) => text.includes(flag));
19754
+ return found ? bypassReason(found) : null;
19755
+ }
19756
+ const start = segment.words.findIndex(isWorkitWord);
19757
+ if (start < 0)
19758
+ continue;
19759
+ const words = segment.words.slice(start + 1);
19760
+ const end = words.indexOf("--");
19761
+ const flag = (end < 0 ? words : words.slice(0, end)).find(isBypass);
19762
+ if (flag)
19763
+ return bypassReason(flag.split("=")[0] ?? flag);
19764
+ const limit = start + 1 + (end < 0 ? words.length : end);
19765
+ if (segment.expands?.some((index) => index > start && index < limit))
19766
+ return EXPANSION_REASON;
19767
+ }
19768
+ return null;
19769
+ };
19540
19770
  // packages/workit-core/src/core/heavy-rules.ts
19541
19771
  import fs16 from "node:fs";
19542
19772
  import os6 from "node:os";
@@ -20860,7 +21090,7 @@ var segmentTargets = (cwd, command, dialect, depth = 0, strict = false) => {
20860
21090
  }
20861
21091
  const inner = depth < 4 ? wrappedScript(words) ?? (strict ? shellScript2(words) : null) : null;
20862
21092
  if (inner !== null && dir !== undefined)
20863
- targets.push(...segmentTargets(dir, inner, "posix", depth + 1, strict));
21093
+ targets.push(...segmentTargets(workitRunDir(dir, words), inner, "posix", depth + 1, strict));
20864
21094
  if (strict && dir !== undefined) {
20865
21095
  const runIn = dir;
20866
21096
  for (const sub of depth < 4 ? segment.subs ?? [] : [])
@@ -20900,6 +21130,14 @@ var segmentTargets = (cwd, command, dialect, depth = 0, strict = false) => {
20900
21130
  }
20901
21131
  return targets;
20902
21132
  };
21133
+ var workitRunDir = (dir, words) => {
21134
+ for (const invocation of workitInvocations(words.map(quoteWord2).join(" "), "posix")) {
21135
+ const { at, args } = workitCwd(dir, invocation);
21136
+ if (args[0] === "check" || args[0] === "lock" && args[1] === "run")
21137
+ return at;
21138
+ }
21139
+ return dir;
21140
+ };
20903
21141
  var workitCwd = (dir, args) => {
20904
21142
  const { rest, cwd } = skipWorkitGlobals(args);
20905
21143
  return { at: cwd === null ? dir : path25.resolve(dir, homeExpanded(cwd)), args: rest };
@@ -21684,8 +21922,13 @@ var standingDelta = (input) => {
21684
21922
  };
21685
21923
  var sinceStart = new Map;
21686
21924
  // packages/workit-core/src/hooks/cwd-normalize.ts
21687
- var COMPOUND = /^\s*cd\s+([A-Za-z0-9_./@%+:,=-]+|'[^'$`\\\n]*'|"[^"$`\\!\n]*")\s*&&\s*(workit(?:\s[\s\S]*)?)$/u;
21925
+ var DIR = String.raw`[A-Za-z0-9_./@%+:,=-]+|'[^'$${"`"}\\\n]*'|"[^"$${"`"}\\!\n]*"`;
21926
+ var COMPOUND = new RegExp(String.raw`^\s*cd\s+(${DIR})\s*&&\s*(workit(?:\s[\s\S]*)?)$`, "u");
21927
+ var GLOBALS_FIRST = new RegExp(String.raw`^(\s*workit)((?:\s+(?:--json|--cwd(?:=|\s+)(?:${DIR})))+)\s+([a-z][a-z-]*)(?=\s|$)`, "u");
21928
+ var VERB_FIRST = /^((?:--json\s+)*)([a-z][a-z-]*)(?=\s|$)/u;
21929
+ var VERBS = new Set(WORKIT_ALLOWED_VERBS);
21688
21930
  var FILTERS = new Set(["tail", "head", "grep"]);
21931
+ var clean = (s) => !s.unanalyzable && !s.group && !s.subs?.length && !s.stdin?.length && !s.redirects.length;
21689
21932
  var stdinOnly = (words) => {
21690
21933
  const [name, ...args] = words;
21691
21934
  const operands = args.filter((w) => !w.startsWith("-"));
@@ -21707,14 +21950,28 @@ var cwdRewrite = (command) => {
21707
21950
  return null;
21708
21951
  if (head.words.some((w) => w === "--cwd" || w.startsWith("--cwd=")))
21709
21952
  return null;
21710
- const clean = (s) => !s.unanalyzable && !s.group && !s.subs?.length && !s.stdin?.length && !s.redirects.length;
21711
21953
  if (!segments.every(clean))
21712
21954
  return null;
21713
21955
  if (!segments.slice(0, -1).every((s) => s.pipe))
21714
21956
  return null;
21715
21957
  if (!filters.every((s) => FILTERS.has(s.words[0] ?? "") && stdinOnly(s.words)))
21716
21958
  return null;
21717
- return `workit --cwd ${dir} ${rest.slice("workit".length).trimStart()}`.trimEnd();
21959
+ const args = rest.slice("workit".length).trimStart();
21960
+ const verb = VERB_FIRST.exec(args);
21961
+ if (!verb?.[2] || !VERBS.has(verb[2]))
21962
+ return `workit --cwd ${dir} ${args}`.trimEnd();
21963
+ const json = verb[1] ? " --json" : "";
21964
+ return `workit ${verb[2]} --cwd ${dir}${json}${args.slice(verb[0].length)}`.trimEnd();
21965
+ };
21966
+ var verbFirst = (command) => {
21967
+ const match = GLOBALS_FIRST.exec(command);
21968
+ const [whole, lead, globals, verb] = match ?? [];
21969
+ if (!whole || !verb || !VERBS.has(verb) || !globals?.includes("--cwd"))
21970
+ return null;
21971
+ const segments = segmentsOf(command);
21972
+ if (segments.length !== 1 || !segments.every(clean))
21973
+ return null;
21974
+ return `${lead} ${verb}${globals}${command.slice(whole.length)}`;
21718
21975
  };
21719
21976
 
21720
21977
  // packages/workit-core/src/hooks/worktree-confine.ts
@@ -21830,7 +22087,7 @@ var hostIsolated = (cwd, agentId) => {
21830
22087
  };
21831
22088
  var shown = (target) => path28.basename(target) === ".workit-live-target" ? path28.dirname(target) : target;
21832
22089
  var unboundDeny = (input, target) => {
21833
- 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.";
22090
+ 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.";
21834
22091
  return {
21835
22092
  kind: "deny",
21836
22093
  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}`,
@@ -21838,7 +22095,7 @@ var unboundDeny = (input, target) => {
21838
22095
  };
21839
22096
  };
21840
22097
  var outsideDeny = (input, worktree, target) => {
21841
- 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.`;
22098
+ 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.`;
21842
22099
  return {
21843
22100
  kind: "deny",
21844
22101
  reason: `workit worktree confinement: implementer ${input.session.agentId} is bound to the worktree ${worktree}, and ${shown(target)} is outside it. ${unblock}`,
@@ -21847,7 +22104,7 @@ var outsideDeny = (input, worktree, target) => {
21847
22104
  };
21848
22105
  var unknownDirDeny = (input, worktree) => {
21849
22106
  const wt = worktree ?? "<worktree>";
21850
- const unblock = `Workit cannot resolve \`cd -\`, \`cd "$X"\` or \`popd\`: write by absolute path, \`git -C ${wt}\` or \`workit --cwd ${wt}\`.`;
22107
+ const unblock = `Workit cannot resolve \`cd -\`, \`cd "$X"\` or \`popd\`: write by absolute path, \`git -C ${wt}\` or \`workit <verb> --cwd ${wt}\`.`;
21851
22108
  return {
21852
22109
  kind: "deny",
21853
22110
  reason: `workit worktree confinement: implementer ${input.session.agentId} writes after an unknown directory change. ${unblock}`,
@@ -22005,7 +22262,7 @@ var subagentStartText = (input, descriptor, event) => {
22005
22262
  return `${isolatedGit(input.cwd)}
22006
22263
  ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
22007
22264
  if (role === "implementer")
22008
- 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.`;
22265
+ 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.`;
22009
22266
  const readOnly = `Workit observed ${who} as read-only/agent-guided.`;
22010
22267
  if (role !== "judge" || !event.agentId)
22011
22268
  return readOnly;
@@ -22022,7 +22279,7 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
22022
22279
  var PRE_TOOL = new Set(["shell.pre", "write.pre", "tool.pre"]);
22023
22280
  function handleHook(input, deps) {
22024
22281
  const decision = decide(input, deps);
22025
- if (deps.descriptor.transport !== "hook-process" || !PRE_TOOL.has(input.event.kind) || decision.kind === "deny")
22282
+ if (deps.descriptor.transport !== "hook-process" || !PRE_TOOL.has(input.event.kind) || decision.kind === "deny" || decision.kind === "ask")
22026
22283
  return decision;
22027
22284
  const delta = standingDelta(input);
22028
22285
  if (!delta)
@@ -22089,9 +22346,12 @@ function decide(input, deps) {
22089
22346
  if (plan.kind === "rewrite" && !rewrites && canDeny)
22090
22347
  return heavyDenial(event.command, plan);
22091
22348
  const lease = plan.kind === "rewrite" && rewrites ? plan : null;
22092
- const cwdCommand = !lease && rewrites && input.host === "claude_code" ? cwdRewrite(event.command) : null;
22349
+ const cwdCommand = !lease && rewrites && input.host === "claude_code" ? cwdRewrite(event.command) ?? verbFirst(event.command) : null;
22093
22350
  const aside = plan.kind === "passthrough" ? plan.note : null;
22094
22351
  noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
22352
+ const bypass = !lease && input.host === "claude_code" ? workitBypassFlag(event.command, event.dialect) : null;
22353
+ if (bypass)
22354
+ return { kind: "ask", reason: bypass };
22095
22355
  const rewrittenTo = lease?.command ?? cwdCommand;
22096
22356
  const rewritten = (text) => rewrittenTo ? {
22097
22357
  kind: "rewrite",
@@ -22518,6 +22778,17 @@ var render = (decision, native) => {
22518
22778
  },
22519
22779
  exitCode: 0
22520
22780
  };
22781
+ if (decision.kind === "ask" && native === "PreToolUse")
22782
+ return {
22783
+ json: {
22784
+ hookSpecificOutput: {
22785
+ hookEventName: "PreToolUse",
22786
+ permissionDecision: "ask",
22787
+ permissionDecisionReason: decision.reason
22788
+ }
22789
+ },
22790
+ exitCode: 0
22791
+ };
22521
22792
  if (decision.kind === "context" && native !== null && CONTEXT_EVENTS.has(native))
22522
22793
  return {
22523
22794
  json: { hookSpecificOutput: { hookEventName: native, additionalContext: decision.text } },