@brainervirus/workit-codex 8.24.0 → 8.26.0

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