@brainervirus/workit-pi 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.
package/dist/workit.js CHANGED
@@ -8407,6 +8407,7 @@ var workspaceConfigSchema = object({
8407
8407
  trailers: boolean2().optional(),
8408
8408
  liveSessionGuard: boolean2().optional(),
8409
8409
  fanout: object({ tierModels: unknown().optional() }).passthrough().optional(),
8410
+ models: object({ roles: unknown().optional() }).passthrough().optional(),
8410
8411
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
8411
8412
  kind,
8412
8413
  union([boolean2(), literal("verified")]).optional()
@@ -8433,7 +8434,10 @@ var workspaceConfigSchema = object({
8433
8434
  });
8434
8435
  }
8435
8436
  });
8436
- var workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).optional() }).passthrough().superRefine((file, context) => {
8437
+ var workspacesFileSchema = object({
8438
+ workspaces: array(workspaceConfigSchema).optional(),
8439
+ models: object({ roles: unknown().optional() }).passthrough().optional()
8440
+ }).passthrough().superRefine((file, context) => {
8437
8441
  const names = new Set;
8438
8442
  for (const [index, workspace] of (file.workspaces ?? []).entries()) {
8439
8443
  if (names.has(workspace.name)) {
@@ -8745,6 +8749,34 @@ var workspaceTrailers = (cwd, dir) => {
8745
8749
  };
8746
8750
  };
8747
8751
  var workspaceLiveSessionGuard = (cwd, dir) => inheritedSetting(cwd, dir, (entry) => typeof entry.liveSessionGuard === "boolean")?.source?.liveSessionGuard !== false;
8752
+ var modelSources = (cwd, dir) => {
8753
+ const out = [];
8754
+ try {
8755
+ const matched = matchRuntimeWorkspace(cwd, dir);
8756
+ if (matched) {
8757
+ const enclosing = matched.matches.filter((entry) => entry !== matched.selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
8758
+ for (const entry of [matched.selected, ...enclosing])
8759
+ out.push({
8760
+ name: entry.name,
8761
+ global: false,
8762
+ models: entry.models,
8763
+ tierModels: entry.fanout?.tierModels
8764
+ });
8765
+ }
8766
+ } catch {}
8767
+ try {
8768
+ const document = JSON.parse(readFileSync2(path4.join(dir ?? configDir(), "workspaces.json"), "utf8"));
8769
+ if (document && typeof document === "object" && !Array.isArray(document))
8770
+ out.push({
8771
+ name: "global",
8772
+ global: true,
8773
+ models: document.models,
8774
+ tierModels: undefined
8775
+ });
8776
+ } catch {}
8777
+ return out;
8778
+ };
8779
+ var hostFamily = (host) => host.replace(/_(?:cli|desktop)$/u, "");
8748
8780
  var TIER_KEYS = new Set(["mundane", "standard", "hard"]);
8749
8781
  var TIER_HOSTS = new Set([
8750
8782
  "claude_code",
@@ -8965,6 +8997,26 @@ function resolveRef(cwd, ref) {
8965
8997
  return null;
8966
8998
  return line(git(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]));
8967
8999
  }
9000
+ function originHeadRef(cwd) {
9001
+ return line(git(cwd, ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"]));
9002
+ }
9003
+ function ownChangeDrift(cwd, range) {
9004
+ const before = diffRaw(cwd, range.fromFork, range.from);
9005
+ const after = diffRaw(cwd, range.toFork, range.to);
9006
+ if (!before || !after)
9007
+ return null;
9008
+ const judged = new Map(before.map((entry) => [entry.path, entry]));
9009
+ const current = new Map(after.map((entry) => [entry.path, entry]));
9010
+ const same = (a, b) => a !== undefined && b !== undefined && a.oldMode === b.oldMode && a.oldBlob === b.oldBlob && a.newMode === b.newMode && a.newBlob === b.newBlob;
9011
+ return [...new Set([...judged.keys(), ...current.keys()])].filter((file) => !same(judged.get(file), current.get(file))).toSorted();
9012
+ }
9013
+ function sameOwnChange(cwd, range) {
9014
+ const fromFork = mergeBase(cwd, range.fromBase, range.from);
9015
+ const toFork = mergeBase(cwd, range.toBase, range.to);
9016
+ if (!fromFork || !toFork)
9017
+ return false;
9018
+ return ownChangeDrift(cwd, { from: range.from, to: range.to, fromFork, toFork })?.length === 0;
9019
+ }
8968
9020
  var LANE_DIFF = [
8969
9021
  "-c",
8970
9022
  "core.quotePath=false",
@@ -12860,9 +12912,9 @@ var workerBlocksTransition = (transition, state) => transition === "pause" ? fal
12860
12912
 
12861
12913
  // packages/workit-core/src/core/task-evaluation.ts
12862
12914
  import { createHash as createHash12 } from "node:crypto";
12863
- import * as fs15 from "node:fs";
12864
- import { spawnSync as spawnSync11 } from "node:child_process";
12865
- import path19 from "node:path";
12915
+ import * as fs14 from "node:fs";
12916
+ import { spawnSync as spawnSync10 } from "node:child_process";
12917
+ import path18 from "node:path";
12866
12918
 
12867
12919
  // packages/workit-core/src/check-config.ts
12868
12920
  import { spawnSync as spawnSync8 } from "node:child_process";
@@ -13093,125 +13145,20 @@ function checkCommand(args, version, onPath = workitAvailable()) {
13093
13145
 
13094
13146
  // packages/workit-core/src/core/policy/derive.ts
13095
13147
  import { existsSync as existsSync5 } from "node:fs";
13096
- import path18 from "node:path";
13148
+ import path17 from "node:path";
13097
13149
 
13098
13150
  // packages/workit-core/src/ledger.ts
13099
13151
  import { spawnSync as spawnSync9 } from "node:child_process";
13100
13152
  import { createHash as createHash11, randomBytes as randomBytes3 } from "node:crypto";
13101
13153
  import fs13 from "node:fs";
13102
- import path15 from "node:path";
13154
+ import path16 from "node:path";
13103
13155
 
13104
- // packages/workit-core/src/hooks/standing-signal.ts
13105
- import { createHash as createHash10 } from "node:crypto";
13106
- import fs12 from "node:fs";
13156
+ // packages/workit-core/src/core/policy/lane.ts
13157
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
13107
13158
  import path14 from "node:path";
13108
- var gitStoreDir = (cwd) => {
13109
- try {
13110
- for (let dir = path14.resolve(cwd);; dir = path14.dirname(dir)) {
13111
- const dotGit = path14.join(dir, ".git");
13112
- if (fs12.existsSync(dotGit)) {
13113
- let gitDir = dotGit;
13114
- if (fs12.statSync(dotGit).isFile()) {
13115
- const target = /^gitdir:\s*(.+)$/mu.exec(fs12.readFileSync(dotGit, "utf8"))?.[1]?.trim();
13116
- if (!target)
13117
- return null;
13118
- gitDir = path14.resolve(dir, target);
13119
- const common = path14.join(gitDir, "commondir");
13120
- if (fs12.existsSync(common))
13121
- gitDir = path14.resolve(gitDir, fs12.readFileSync(common, "utf8").trim());
13122
- }
13123
- return path14.join(gitDir, "workit");
13124
- }
13125
- if (path14.dirname(dir) === dir)
13126
- return null;
13127
- }
13128
- } catch {
13129
- return null;
13130
- }
13131
- };
13132
- var standingRevFile = (store) => path14.join(store, "standing", "rev");
13133
- var standingSeenFile = (store, session, agentId) => path14.join(store, "standing", "seen", `${createHash10("sha256").update(`${session}\x00${agentId ?? ""}`).digest("hex").slice(0, 32)}.json`);
13134
- var readStandingRev = (store) => {
13135
- try {
13136
- return fs12.readFileSync(standingRevFile(store), "utf8").trim() || null;
13137
- } catch {
13138
- return null;
13139
- }
13140
- };
13141
- var readStandingSeen = (file) => {
13142
- try {
13143
- const value = JSON.parse(fs12.readFileSync(file, "utf8"));
13144
- return {
13145
- rev: typeof value.rev === "string" ? value.rev : null,
13146
- ids: Array.isArray(value.ids) ? value.ids.filter((id) => typeof id === "string") : []
13147
- };
13148
- } catch {
13149
- return { rev: null, ids: [] };
13150
- }
13151
- };
13152
- var writeStandingSeen = (file, seen) => {
13153
- try {
13154
- fs12.mkdirSync(path14.dirname(file), { recursive: true });
13155
- fs12.writeFileSync(file, JSON.stringify(seen));
13156
- } catch {}
13157
- };
13158
13159
 
13159
- // packages/workit-core/src/ledger.ts
13160
- var LEDGER_VERSION = 1;
13161
- var MAX_LINE_BYTES = 4096;
13162
- var PASSING = new Set(["verified", "tests-verified", "type-check-only"]);
13163
- var FAILING = new Set(["failed", "blocked"]);
13164
- var STRONG_RESULTS = new Set(["verified", "tests-verified"]);
13165
- var PR_ROW_TYPES = new Set([
13166
- "pr.created",
13167
- "pr.status",
13168
- "pr.merged",
13169
- "pr.linked"
13170
- ]);
13171
- var err = (code, error, unblock) => ({
13172
- ok: false,
13173
- code,
13174
- error,
13175
- ...unblock ? { unblock } : {}
13176
- });
13177
- var gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
13178
- const run = spawnSync9("git", args, {
13179
- cwd,
13180
- encoding: "utf8",
13181
- timeout: timeoutMs,
13182
- killSignal: "SIGKILL",
13183
- windowsHide: true,
13184
- maxBuffer: 256 * 1024 * 1024,
13185
- env: {
13186
- ...process.env,
13187
- GIT_TERMINAL_PROMPT: "0",
13188
- GIT_OPTIONAL_LOCKS: "0",
13189
- LC_ALL: "C",
13190
- LANGUAGE: "C"
13191
- }
13192
- });
13193
- return {
13194
- ok: run.status === 0,
13195
- stdout: run.stdout ?? "",
13196
- stderr: run.error ? String(run.error.message) : run.stderr ?? ""
13197
- };
13198
- };
13199
- var git4 = (cwd, args) => {
13200
- const run = gitRun2(cwd, args);
13201
- const value = run.ok ? run.stdout.trim() : "";
13202
- return value ? value : null;
13203
- };
13160
+ // packages/workit-core/src/git/default-base.ts
13204
13161
  var safeRef = (value) => value.length > 0 && !value.startsWith("-");
13205
- function resolveCommit(cwd, ref) {
13206
- if (!safeRef(ref))
13207
- return null;
13208
- return git4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]);
13209
- }
13210
- function branchHead(cwd, branch) {
13211
- if (!safeRef(branch))
13212
- return null;
13213
- return resolveCommit(cwd, `refs/heads/${branch}`) ?? resolveCommit(cwd, `refs/remotes/origin/${branch}`);
13214
- }
13215
13162
  var trackTrunk = (cwd) => {
13216
13163
  try {
13217
13164
  const resolved = vcsConfig("resolve", cwd);
@@ -13227,11 +13174,11 @@ function defaultBase(cwd) {
13227
13174
  const trunk = trackTrunk(cwd);
13228
13175
  if (trunk) {
13229
13176
  for (const candidate of [`origin/${trunk}`, trunk])
13230
- if (resolveCommit(cwd, candidate))
13177
+ if (resolveRef(cwd, candidate))
13231
13178
  return candidate;
13232
13179
  }
13233
- const originHead = git4(cwd, ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"]);
13234
- if (originHead && resolveCommit(cwd, originHead))
13180
+ const originHead = originHeadRef(cwd);
13181
+ if (originHead && resolveRef(cwd, originHead))
13235
13182
  return originHead;
13236
13183
  for (const candidate of [
13237
13184
  "origin/main",
@@ -13241,1514 +13188,1691 @@ function defaultBase(cwd) {
13241
13188
  "origin/develop",
13242
13189
  "develop"
13243
13190
  ])
13244
- if (resolveCommit(cwd, candidate))
13191
+ if (resolveRef(cwd, candidate))
13245
13192
  return candidate;
13246
13193
  return null;
13247
13194
  }
13248
- function diffHash(cwd, base, head) {
13249
- if (!safeRef(base) || !safeRef(head))
13250
- return null;
13251
- const diff = gitRun2(cwd, [
13252
- "diff",
13253
- "-U3",
13254
- "--diff-algorithm=myers",
13255
- "--indent-heuristic",
13256
- "--no-color",
13257
- "--no-ext-diff",
13258
- "--no-textconv",
13259
- "--no-renames",
13260
- "--full-index",
13261
- "--binary",
13262
- "--src-prefix=a/",
13263
- "--dst-prefix=b/",
13264
- `${base}...${head}`,
13265
- "--"
13266
- ], GIT_TIMEOUTS.worktree);
13267
- if (!diff.ok || !diff.stdout)
13195
+
13196
+ // packages/workit-core/src/core/policy/attributes.ts
13197
+ function patternRegex(dir, pattern) {
13198
+ if (!pattern || pattern.endsWith("/") || pattern.startsWith('"') || pattern.startsWith("!"))
13268
13199
  return null;
13269
- const hash = createHash11("sha256");
13270
- for (const line of diff.stdout.split(`
13271
- `)) {
13272
- if (line.startsWith("index "))
13273
- continue;
13274
- hash.update(line.startsWith("@@ ") ? "@@" : line);
13275
- hash.update(`
13276
- `);
13277
- }
13278
- return `sha256:${hash.digest("hex")}`;
13279
- }
13280
- function storeRoot(cwd) {
13281
- const location = resolveStore(path15.resolve(cwd));
13282
- if (location instanceof Error)
13283
- 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");
13284
- return { ok: true, value: { root: location.dir, shared: location.shared } };
13285
- }
13286
- function ledgerPath(cwd) {
13287
- const root = storeRoot(cwd);
13288
- return root.ok ? { ok: true, value: path15.join(root.value.root, "ledger", "ledger.jsonl") } : root;
13289
- }
13290
- var newId2 = () => `${Date.now().toString(36)}-${randomBytes3(6).toString("hex")}`;
13291
- var NETWORK_FS_TYPES = new Set([
13292
- 26985,
13293
- 20859,
13294
- 4266872130,
13295
- 4283649346,
13296
- 1702057286,
13297
- 1397113167,
13298
- 12805120,
13299
- 16914839,
13300
- 1196443219,
13301
- 198183888
13302
- ]);
13303
- var isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0);
13304
- var onNetworkFs = (dir) => {
13305
- if (process.platform !== "linux")
13306
- return false;
13307
- try {
13308
- return isNetworkFsType(fs13.statfsSync(dir).type);
13309
- } catch {
13310
- return false;
13200
+ const anchored = pattern.includes("/");
13201
+ const body = pattern.replace(/^\//u, "");
13202
+ let source = "";
13203
+ for (let index = 0;index < body.length; index += 1) {
13204
+ const char = body[index];
13205
+ if (char === "*" && body[index + 1] === "*") {
13206
+ const slash = body[index + 2] === "/";
13207
+ source += slash ? "(?:.*/)?" : ".*";
13208
+ index += slash ? 2 : 1;
13209
+ } else if (char === "*")
13210
+ source += "[^/]*";
13211
+ else if (char === "?")
13212
+ source += "[^/]";
13213
+ else if (char === "[") {
13214
+ const end = body.indexOf("]", index + 2);
13215
+ if (end < 0)
13216
+ return null;
13217
+ const set = body.slice(index + 1, end).replace(/^!/u, "^").replace(/\\/gu, "\\\\");
13218
+ source += `[${set}]`;
13219
+ index = end;
13220
+ } else if (char === "\\" && index + 1 < body.length) {
13221
+ source += body[index + 1].replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
13222
+ index += 1;
13223
+ } else
13224
+ source += char.replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
13311
13225
  }
13312
- };
13313
- var LOCK_WAIT_MS = 5000;
13314
- var LOCK_STALE_MS = 30000;
13315
- var LOCK_OWNER = "owner";
13316
- var lockToken = (lock) => {
13226
+ const prefix = dir ? `${dir.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}/` : "";
13317
13227
  try {
13318
- return fs13.readFileSync(path15.join(lock, LOCK_OWNER), "utf8");
13228
+ return new RegExp(anchored ? `^${prefix}${source}$` : `^${prefix}(?:.*/)?${source}$`, "u");
13319
13229
  } catch {
13320
13230
  return null;
13321
13231
  }
13322
- };
13323
- var lockAgeMs = (lock) => {
13324
- for (const target of [path15.join(lock, LOCK_OWNER), lock]) {
13325
- try {
13326
- return Date.now() - fs13.statSync(target).mtimeMs;
13327
- } catch {}
13328
- }
13329
- return null;
13330
- };
13331
- var ledgerLock = {
13332
- acquire(lock) {
13333
- try {
13334
- fs13.mkdirSync(lock);
13335
- } catch (error) {
13336
- if (error.code === "EEXIST")
13337
- return null;
13338
- throw error;
13232
+ }
13233
+ function rulesOf(dir, text) {
13234
+ const out = [];
13235
+ for (const raw of text.split(`
13236
+ `)) {
13237
+ const line = raw.trim();
13238
+ if (!line || line.startsWith("#") || line.startsWith("[attr]"))
13239
+ continue;
13240
+ const [pattern, ...attributes] = line.split(/\s+/u);
13241
+ let generated = null;
13242
+ for (const attribute of attributes) {
13243
+ if (attribute === "linguist-generated" || attribute === "linguist-generated=true")
13244
+ generated = true;
13245
+ else if (attribute === "-linguist-generated" || attribute === "!linguist-generated" || attribute.startsWith("linguist-generated="))
13246
+ generated = false;
13339
13247
  }
13340
- const token = `${process.pid}-${randomBytes3(8).toString("hex")}`;
13341
- fs13.writeFileSync(path15.join(lock, LOCK_OWNER), token);
13342
- return token;
13343
- },
13344
- release(lock, token) {
13345
- if (lockToken(lock) !== token)
13346
- return false;
13347
- const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
13348
- try {
13349
- fs13.renameSync(lock, tomb);
13350
- } catch {
13351
- return false;
13352
- }
13353
- if (lockToken(tomb) !== token) {
13354
- try {
13355
- fs13.renameSync(tomb, lock);
13356
- } catch {}
13357
- return false;
13358
- }
13359
- fs13.rmSync(tomb, { recursive: true, force: true });
13360
- return true;
13361
- },
13362
- token: lockToken,
13363
- reap(lock, observed) {
13364
- const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
13365
- try {
13366
- fs13.renameSync(lock, tomb);
13367
- } catch {
13368
- return false;
13369
- }
13370
- if (lockToken(tomb) === observed) {
13371
- fs13.rmSync(tomb, { recursive: true, force: true });
13372
- return true;
13373
- }
13374
- try {
13375
- fs13.renameSync(tomb, lock);
13376
- } catch {
13377
- fs13.rmSync(tomb, { recursive: true, force: true });
13378
- }
13379
- return false;
13380
- }
13381
- };
13382
- function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
13383
- const lock = `${file}.lock`;
13384
- const deadline = Date.now() + waitMs;
13385
- let token = null;
13386
- for (;; ) {
13387
- token = ledgerLock.acquire(lock);
13388
- if (token)
13389
- break;
13390
- const observed = lockToken(lock);
13391
- const age = lockAgeMs(lock);
13392
- if (age !== null && age > LOCK_STALE_MS && ledgerLock.reap(lock, observed))
13248
+ if (generated === null)
13393
13249
  continue;
13394
- if (Date.now() >= deadline)
13395
- return null;
13396
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
13250
+ const regex = patternRegex(dir, pattern);
13251
+ if (regex)
13252
+ out.push({ regex, generated });
13397
13253
  }
13398
- try {
13399
- return fn();
13400
- } finally {
13401
- ledgerLock.release(lock, token);
13254
+ return out;
13255
+ }
13256
+ function generatedByAttributes(files, paths) {
13257
+ const rules = [...files].map(([file, text]) => {
13258
+ const dir = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
13259
+ return {
13260
+ depth: dir ? dir.split("/").length : 0,
13261
+ rules: rulesOf(dir, text)
13262
+ };
13263
+ }).toSorted((left, right) => left.depth - right.depth).flatMap((entry) => entry.rules);
13264
+ const out = new Set;
13265
+ for (const file of paths) {
13266
+ const last = rules.findLast((rule) => rule.regex.test(file));
13267
+ if (last?.generated)
13268
+ out.add(file);
13402
13269
  }
13270
+ return out;
13403
13271
  }
13404
- var sha2562 = (text) => createHash11("sha256").update(text).digest("hex");
13405
- var chainHash = (prevHash, body) => sha2562(`${prevHash ?? ""}
13406
- ${body}`);
13407
- var TAIL_BYTES = 64 * 1024;
13408
- var lastLine = (fd, size) => {
13409
- for (let span = Math.min(size, TAIL_BYTES);span > 0; span = Math.min(size, span * 2)) {
13410
- const buffer = Buffer.alloc(span);
13411
- fs13.readSync(fd, buffer, 0, span, size - span);
13412
- const end = buffer.lastIndexOf(10);
13413
- if (end < 0) {
13414
- if (span === size)
13415
- return null;
13416
- continue;
13272
+
13273
+ // packages/workit-core/src/core/policy/substance.ts
13274
+ var SLASH_BLOCK = [["/*", "*/"]];
13275
+ var C_LIKE = { line: ["//"], block: SLASH_BLOCK, quotes: `"'` };
13276
+ var SYNTAXES = Object.fromEntries([
13277
+ ...["ts", "mts", "cts", "js", "mjs", "cjs"].map((ext) => [ext, { ...C_LIKE, js: true }]),
13278
+ ...["java", "kt", "kts", "scala", "swift", "dart", "groovy"].map((ext) => [
13279
+ ext,
13280
+ { ...C_LIKE, triple: true }
13281
+ ]),
13282
+ ["go", { ...C_LIKE, rawBacktick: true }],
13283
+ ...["css", "scss", "less"].map((ext) => [
13284
+ ext,
13285
+ { line: [], block: SLASH_BLOCK, quotes: `"'`, cssUrl: true }
13286
+ ]),
13287
+ ...["py", "pyi"].map((ext) => [
13288
+ ext,
13289
+ { line: ["#"], block: [], quotes: `"'`, triple: true, indented: true, fstring: true }
13290
+ ]),
13291
+ ["toml", { line: ["#"], block: [], quotes: `"`, literal: "'", triple: true }],
13292
+ ...["html", "htm", "xml", "svg", "xhtml"].map((ext) => [
13293
+ ext,
13294
+ { line: [], block: [["<!--", "-->"]], quotes: "", indented: true }
13295
+ ])
13296
+ ]);
13297
+ 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;
13298
+ var extensionOf = (file) => {
13299
+ const name = file.slice(file.lastIndexOf("/") + 1);
13300
+ const dot = name.lastIndexOf(".");
13301
+ return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
13302
+ };
13303
+ var CI_PATH = /^\.github\/|(?:^|\/)(?:\.gitlab-ci\.ya?ml|Jenkinsfile|azure-pipelines\.ya?ml|\.pre-commit-config\.ya?ml|\.circleci\/.*)$/u;
13304
+ var syntaxOf = (file) => CI_PATH.test(file) ? null : SYNTAXES[extensionOf(file)] ?? null;
13305
+ var SPACE = /[ \t\r\f\v]/u;
13306
+ var WORD = /[\w$]/u;
13307
+ var NO_SPACE_AFTER = "([{,;";
13308
+ var NO_SPACE_BEFORE = ")]},;";
13309
+ var REGEX_AFTER = "(,=:[!&|?{};+-*%<>~^";
13310
+ var REGEX_KEYWORDS = new Set([
13311
+ "return",
13312
+ "typeof",
13313
+ "instanceof",
13314
+ "case",
13315
+ "do",
13316
+ "else",
13317
+ "in",
13318
+ "of",
13319
+ "new",
13320
+ "delete",
13321
+ "void",
13322
+ "throw",
13323
+ "yield",
13324
+ "await"
13325
+ ]);
13326
+ function substanceOf(file, text) {
13327
+ const syntax = syntaxOf(file);
13328
+ if (!syntax)
13329
+ return plainSubstance(text);
13330
+ const markup = syntax.block.some(([open]) => open === "<!--");
13331
+ if (markup && (/<(?:script|style)\b|<!\[CDATA\[|<\?/iu.test(text) || /[^>\s][ \t]*<!--/u.test(text)))
13332
+ return plainSubstance(text);
13333
+ return lex(syntax, text);
13334
+ }
13335
+ function plainSubstance(text) {
13336
+ return text.split(`
13337
+ `).map((line) => line.replace(/[ \t\r\f\v]+$/u, "")).filter(Boolean).join(`
13338
+ `);
13339
+ }
13340
+ function lex(syntax, text) {
13341
+ const collapse = !syntax.indented;
13342
+ let out = "";
13343
+ let lineStart = 0;
13344
+ let kept = 0;
13345
+ let pendingSpace = false;
13346
+ const frames = [{ kind: "code", braces: 0 }];
13347
+ let index = 0;
13348
+ if (text.startsWith("#!")) {
13349
+ const end = text.indexOf(`
13350
+ `);
13351
+ out = end < 0 ? text : text.slice(0, end);
13352
+ kept = out.length;
13353
+ index = end < 0 ? text.length : end;
13354
+ }
13355
+ const emitCode = (chunk) => {
13356
+ if (collapse && pendingSpace && out.length > lineStart) {
13357
+ const before = out[out.length - 1];
13358
+ if (!NO_SPACE_AFTER.includes(before) && !NO_SPACE_BEFORE.includes(chunk[0]))
13359
+ out += " ";
13417
13360
  }
13418
- let stop = end;
13419
- let text = "";
13420
- while (stop >= 0) {
13421
- const start = buffer.lastIndexOf(10, stop - 1);
13422
- text = buffer.toString("utf8", start + 1, stop).trim();
13423
- if (text) {
13424
- if (start >= 0 || span === size)
13425
- return text;
13426
- break;
13427
- }
13428
- if (start < 0) {
13429
- if (span === size)
13430
- return null;
13431
- break;
13432
- }
13433
- stop = start;
13361
+ pendingSpace = false;
13362
+ out += chunk;
13363
+ };
13364
+ const emitVerbatim = (chunk) => {
13365
+ emitCode(chunk);
13366
+ kept = out.length;
13367
+ };
13368
+ const newline = () => {
13369
+ if (!collapse) {
13370
+ let end = out.length;
13371
+ while (end > Math.max(lineStart, kept) && SPACE.test(out[end - 1]))
13372
+ end -= 1;
13373
+ out = out.slice(0, end);
13434
13374
  }
13435
- if (span === size)
13436
- return text || null;
13437
- }
13438
- return null;
13439
- };
13440
- var tighten = (fd) => {
13441
- try {
13442
- if ((fs13.fstatSync(fd).mode & 63) !== 0)
13443
- fs13.fchmodSync(fd, 384);
13444
- } catch {}
13445
- };
13446
- function appendLine(file, build, options) {
13447
- const write = () => {
13448
- const fd = fs13.openSync(file, "a+", 384);
13449
- try {
13450
- if (process.platform !== "win32")
13451
- tighten(fd);
13452
- const size = fs13.fstatSync(fd).size;
13453
- let fence = "";
13454
- if (size > 0) {
13455
- const last = Buffer.alloc(1);
13456
- fs13.readSync(fd, last, 0, 1, size - 1);
13457
- if (last[0] !== 10)
13458
- fence = `
13375
+ pendingSpace = false;
13376
+ if (out.length > lineStart) {
13377
+ out += `
13459
13378
  `;
13460
- }
13461
- const line = build(lastLine(fd, size));
13462
- if (!line.ok)
13463
- return line;
13464
- const bytes = Buffer.from(fence + line.value);
13465
- const written = fs13.writeSync(fd, bytes);
13466
- if (written !== bytes.length)
13467
- return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
13468
- return { ok: true, value: undefined };
13469
- } finally {
13470
- fs13.closeSync(fd);
13379
+ lineStart = out.length;
13471
13380
  }
13472
13381
  };
13473
- try {
13474
- fs13.mkdirSync(path15.dirname(file), { recursive: true });
13475
- if (!options.forceLock && !onNetworkFs(path15.dirname(file)))
13476
- return write();
13477
- const locked = withLedgerLock(file, write);
13478
- return locked ?? err("busy", `ledger lock ${file}.lock is held`, "retry; remove the .lock directory if no workit process is running");
13479
- } catch (error) {
13480
- return err("unavailable", `cannot append to ${file}: ${error.message}`);
13481
- }
13482
- }
13483
- var sealed = (row, id, at, previous) => {
13484
- const { prevHash: _prev, rowHash: _row, ...fields } = row;
13485
- const prevHash = previous === null ? null : sha2562(previous);
13486
- const body = { v: LEDGER_VERSION, id, at, ...fields, prevHash };
13487
- const full = { ...body, rowHash: chainHash(prevHash, JSON.stringify(body)) };
13488
- return { full, line: `${JSON.stringify(full)}
13489
- ` };
13490
- };
13491
- function appendRaw(cwd, row, options) {
13492
- const file = ledgerPath(cwd);
13493
- if (!file.ok)
13494
- return file;
13495
- const id = newId2();
13496
- const at = (options.now ?? new Date).toISOString();
13497
- let full = null;
13498
- const written = appendLine(file.value, (previous) => {
13499
- const next = sealed(row, id, at, previous);
13500
- const bytes = Buffer.byteLength(next.line);
13501
- if (bytes > MAX_LINE_BYTES)
13502
- 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>`);
13503
- full = next.full;
13504
- return { ok: true, value: next.line };
13505
- }, options);
13506
- return written.ok ? { ok: true, value: full } : written;
13507
- }
13508
- function observedRowBytes(row) {
13509
- const sample = sealed({ ...row, observer: "workit_cli" }, newId2(), new Date().toISOString(), "");
13510
- return Buffer.byteLength(sample.line);
13511
- }
13512
- function appendObserved(cwd, row, options = {}) {
13513
- return appendRaw(cwd, { ...row, observer: "workit_cli" }, options);
13514
- }
13515
- function appendHookObserved(cwd, row) {
13516
- return appendRaw(cwd, { ...row, observer: "host_hook" }, {});
13517
- }
13518
- var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13519
- var str2 = (value) => typeof value === "string" ? value : null;
13520
- var normalizeActor = (value) => {
13521
- const actor = isRecord3(value) ? value : {};
13522
- return {
13523
- host: str2(actor.host) ?? "unknown",
13524
- session: str2(actor.session),
13525
- agentId: str2(actor.agentId)
13382
+ const regexAllowed = () => {
13383
+ let end = out.length;
13384
+ while (end > 0 && /\s/u.test(out[end - 1]))
13385
+ end -= 1;
13386
+ if (end === 0 || REGEX_AFTER.includes(out[end - 1]))
13387
+ return true;
13388
+ let start = end;
13389
+ while (start > 0 && WORD.test(out[start - 1]))
13390
+ start -= 1;
13391
+ return REGEX_KEYWORDS.has(out.slice(start, end));
13526
13392
  };
13527
- };
13528
- function supersedeAllowed(by, target, onBranch = (row) => (by.branch ?? null) === (row.branch ?? null)) {
13529
- if (by.type !== target.type)
13530
- return `a ${by.type} row cannot supersede a ${target.type} row`;
13531
- if (!by.actor.session || by.actor.session !== target.actor.session)
13532
- return "only the session that wrote a row can supersede it";
13533
- if (!onBranch(target))
13534
- return `the row is on another branch (${String(target.branch ?? "none")}); a row supersedes only one on its own branch`;
13535
- if (by.type === "verdict" && (by.kind ?? "review") !== (target.kind ?? "review"))
13536
- return `a ${String(by.kind ?? "review")} verdict cannot supersede a ${String(target.kind ?? "review")} verdict; record it with --kind ${String(target.kind ?? "review")}`;
13537
- if (by.type === "verdict" && by.self === true && target.self !== true)
13538
- return "a self verdict cannot replace an independent verdict";
13539
- return null;
13540
- }
13541
- function readLedger(cwd, options = {}) {
13542
- const file = ledgerPath(cwd);
13543
- if (!file.ok)
13544
- return file;
13545
- let text = "";
13546
- let stat = null;
13547
- try {
13548
- stat = fs13.statSync(file.value);
13549
- text = fs13.readFileSync(file.value, "utf8");
13550
- } catch (error) {
13551
- if (error.code === "ENOENT")
13552
- return {
13553
- ok: true,
13554
- value: {
13555
- path: file.value,
13556
- rows: [],
13557
- skipped: 0,
13558
- integrity: options.integrity ? { chained: 0, legacy: 0, unverified: [] } : null
13559
- }
13560
- };
13561
- return err("unavailable", `cannot read ${file.value}: ${error.message}`);
13562
- }
13563
- const rows = [];
13564
- let skipped = 0;
13565
- const integrity = { chained: 0, legacy: 0, unverified: [] };
13566
- const earlier = new Set([null]);
13567
- let chainStarted = false;
13568
- const ids = new Set;
13569
- for (const raw of text.split(`
13570
- `)) {
13571
- const trimmed = raw.trim();
13572
- if (!trimmed)
13573
- continue;
13574
- const linked = (prevHash) => earlier.has(prevHash);
13575
- if (options.integrity)
13576
- earlier.add(sha2562(trimmed));
13577
- let parsed;
13578
- try {
13579
- parsed = JSON.parse(trimmed);
13580
- } catch {
13581
- skipped += 1;
13582
- continue;
13393
+ const scanString = (quote, escapes, multiline) => {
13394
+ const prefix = text.slice(Math.max(0, index - 2), index).match(/[a-z]*$/iu)?.[0] ?? "";
13395
+ const fstring = syntax.fstring === true && /f/iu.test(prefix);
13396
+ let end = index + quote.length;
13397
+ while (end < text.length) {
13398
+ const char = text[end];
13399
+ if (escapes && char === "\\") {
13400
+ end += 2;
13401
+ continue;
13402
+ }
13403
+ if (text.startsWith(quote, end)) {
13404
+ if (fstring && unbalanced(text.slice(index + quote.length, end)))
13405
+ return false;
13406
+ emitVerbatim(text.slice(index, end + quote.length));
13407
+ index = end + quote.length;
13408
+ return true;
13409
+ }
13410
+ if (char === `
13411
+ ` && !multiline)
13412
+ return false;
13413
+ end += 1;
13583
13414
  }
13584
- if (!isRecord3(parsed) || !str2(parsed.id) || !str2(parsed.type) || !str2(parsed.at)) {
13585
- skipped += 1;
13415
+ return false;
13416
+ };
13417
+ while (index < text.length) {
13418
+ const frame = frames[frames.length - 1];
13419
+ const char = text[index];
13420
+ if (frame.kind === "template") {
13421
+ if (char === "\\") {
13422
+ emitVerbatim(text.slice(index, index + 2));
13423
+ index += 2;
13424
+ } else if (char === "`") {
13425
+ emitVerbatim(char);
13426
+ frames.pop();
13427
+ index += 1;
13428
+ } else if (text.startsWith("${", index)) {
13429
+ emitVerbatim("${");
13430
+ frames.push({ kind: "code", braces: 0 });
13431
+ index += 2;
13432
+ } else {
13433
+ out += char;
13434
+ kept = out.length;
13435
+ index += 1;
13436
+ }
13586
13437
  continue;
13587
13438
  }
13588
- let rowIntegrity = options.integrity ? "verified" : undefined;
13589
- let reason = null;
13590
- if (!options.integrity) {} else if (typeof parsed.rowHash === "string") {
13591
- chainStarted = true;
13592
- const { rowHash, ...body } = parsed;
13593
- const prevHash = str2(parsed.prevHash);
13594
- if (chainHash(prevHash, JSON.stringify(body)) !== rowHash)
13595
- reason = "hash_mismatch";
13596
- else if (!linked(prevHash))
13597
- reason = "broken_link";
13598
- } else if (chainStarted)
13599
- reason = "unsigned";
13600
- else
13601
- rowIntegrity = "legacy";
13602
- if (options.integrity && !reason && ids.has(parsed.id))
13603
- reason = "duplicate_id";
13604
- if (options.integrity)
13605
- ids.add(parsed.id);
13606
- if (reason)
13607
- rowIntegrity = "unverified";
13608
- const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
13609
- const row = {
13610
- ...parsed,
13611
- v: typeof parsed.v === "number" ? parsed.v : LEDGER_VERSION,
13612
- id: parsed.id,
13613
- at: parsed.at,
13614
- type: parsed.type,
13615
- actor: normalizeActor(parsed.actor),
13616
- branch: str2(parsed.branch),
13617
- head: str2(parsed.head),
13618
- base: str2(parsed.base),
13619
- baseSha: str2(parsed.baseSha),
13620
- patchId: str2(parsed.patchId),
13621
- diffHash: str2(parsed.diffHash),
13622
- tree: str2(parsed.tree),
13623
- dirty: typeof parsed.dirty === "boolean" ? parsed.dirty : null,
13624
- ...pr === undefined ? {} : { pr },
13625
- ...str2(parsed.supersedes) ? { supersedes: parsed.supersedes } : {},
13626
- seq: rows.length + 1,
13627
- superseded: false,
13628
- supersedeIgnored: false,
13629
- ...rowIntegrity ? { integrity: rowIntegrity } : {},
13630
- ...reason ? { integrityReason: reason } : {}
13631
- };
13632
- rows.push(row);
13633
- if (!options.integrity)
13439
+ if (char === `
13440
+ `) {
13441
+ newline();
13442
+ index += 1;
13634
13443
  continue;
13635
- if (rowIntegrity === "verified")
13636
- integrity.chained += 1;
13637
- else if (rowIntegrity === "legacy")
13638
- integrity.legacy += 1;
13639
- else if (reason)
13640
- integrity.unverified.push({
13641
- seq: row.seq,
13642
- id: row.id,
13643
- type: row.type,
13644
- branch: row.branch,
13645
- reason
13646
- });
13647
- }
13648
- const scopes = new Map;
13649
- const scopeOf = (branch) => {
13650
- let scope = scopes.get(branch);
13651
- if (!scope)
13652
- scopes.set(branch, scope = branchScope(cwd, branch));
13653
- return scope;
13654
- };
13655
- const byId = new Map;
13656
- for (const row of rows) {
13657
- const target = row.supersedes ? byId.get(row.supersedes) : undefined;
13658
- if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
13659
- target.superseded = true;
13660
- else if (row.supersedes)
13661
- row.supersedeIgnored = true;
13662
- if (!byId.has(row.id))
13663
- byId.set(row.id, row);
13664
- }
13665
- if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
13666
- tamperCache.set(file.value, {
13667
- size: stat.size,
13668
- stamp: fileStamp(stat),
13669
- breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
13670
- });
13671
- return {
13672
- ok: true,
13673
- value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
13674
- };
13675
- }
13676
- var TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
13677
- var tamperCache = new Map;
13678
- var fileStamp = (stat) => `${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`;
13679
- function ledgerTampering(cwd) {
13680
- const file = ledgerPath(cwd);
13681
- if (!file.ok)
13682
- return [];
13683
- try {
13684
- const stat = fs13.statSync(file.value);
13685
- const cached = tamperCache.get(file.value);
13686
- if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
13687
- return cached.breaks;
13688
- } catch {
13689
- return [];
13690
- }
13691
- const read = readLedger(cwd, { integrity: true });
13692
- return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
13693
- }
13694
- function tamperedUnblock(cwd) {
13695
- const file = ledgerPath(cwd);
13696
- const where = file.ok ? file.value : "the ledger file";
13697
- 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`;
13698
- }
13699
- 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)}`;
13700
- function verdictBasis(verdict, current) {
13701
- if (verdict.head && verdict.head === current.head)
13702
- return "fresh";
13703
- if (verdict.patchId && verdict.patchId === current.patchId && verdict.diffHash && verdict.diffHash === current.diffHash)
13704
- return "carried";
13705
- return "stale";
13706
- }
13707
- var isLiveVerdict = (row, retired) => row.type === "verdict" && typeof row.result === "string" && !retired.has(row.id);
13708
- function trailerSessions(cwd, base, head) {
13709
- if (!base || !head || !safeRef(base) || !safeRef(head))
13710
- return [];
13711
- const out = git4(cwd, [
13712
- "log",
13713
- "--format=%(trailers:key=Workit-Session,valueonly,separator=%x00)",
13714
- `${base}..${head}`
13715
- ]);
13716
- return (out ?? "").split(/[\0\n]/u).map((value) => value.trim()).filter(Boolean);
13717
- }
13718
- var RENAMED = /^branch: renamed refs\/heads\/(.+) to refs\/heads\/(.+)$/iu;
13719
- function branchScope(cwd, branch) {
13720
- const renamedAt = new Map;
13721
- let since = Number.NEGATIVE_INFINITY;
13722
- if (safeRef(branch)) {
13723
- const log = git4(cwd, [
13724
- "log",
13725
- "-g",
13726
- "--date=unix",
13727
- "--format=%gd%x09%gs",
13728
- `refs/heads/${branch}`,
13729
- "--"
13730
- ]);
13731
- for (const line of (log ?? "").split(`
13732
- `)) {
13733
- const [selector = "", subject = ""] = line.split("\t");
13734
- const seconds = Number(/@\{(\d+)\}$/u.exec(selector)?.[1]);
13735
- if (!Number.isFinite(seconds))
13736
- continue;
13737
- since = seconds * 1000;
13738
- const match = RENAMED.exec(subject);
13739
- if (!match || match[1] === branch)
13740
- continue;
13741
- const until = (seconds + 1) * 1000;
13742
- renamedAt.set(match[1], Math.max(renamedAt.get(match[1]) ?? 0, until));
13743
13444
  }
13744
- }
13745
- return (row) => {
13746
- if (row.branch === branch)
13747
- return true;
13748
- const until = row.branch === null ? undefined : renamedAt.get(row.branch);
13749
- if (until === undefined)
13750
- return false;
13751
- const at = Date.parse(row.at);
13752
- return at >= since && at <= until;
13753
- };
13754
- }
13755
- var renameAwareBranch = (scopeOf, branch) => {
13756
- return (target) => {
13757
- const own = typeof branch === "string" ? branch : null;
13758
- const theirs = typeof target.branch === "string" ? target.branch : null;
13759
- if (own === theirs)
13760
- return true;
13761
- if (own === null || theirs === null || typeof target.at !== "string")
13762
- return false;
13763
- return scopeOf(own)({ branch: theirs, at: target.at });
13764
- };
13765
- };
13766
- function authorSessions(cwd, rows, branch, range = { base: null, head: null }, scope = branchScope(cwd, branch)) {
13767
- const sessions = new Set;
13768
- for (const row of rows) {
13769
- if (!scope(row) || row.type !== "commit.recorded" && row.type !== "task.opened")
13770
- continue;
13771
- const session = str2(row.session) ?? row.actor.session;
13772
- if (session)
13773
- sessions.add(session);
13774
- }
13775
- for (const session of trailerSessions(cwd, range.base, range.head))
13776
- sessions.add(session);
13777
- return sessions;
13778
- }
13779
- var derivesFrom = (derived, bare) => !bare.includes(":") && derived.endsWith(`:${bare}`);
13780
- var authoredBy = (authors, session) => {
13781
- if (authors.has(session))
13782
- return true;
13783
- for (const author of authors)
13784
- if (derivesFrom(author, session) || derivesFrom(session, author))
13785
- return true;
13786
- return false;
13787
- };
13788
- var TRUST_REASONS = new Set([
13789
- "unsigned",
13790
- "hash_mismatch",
13791
- "broken_link",
13792
- "duplicate_id",
13793
- "unissued_session",
13794
- "unbound_session"
13795
- ]);
13796
- var SHELL_VOUCHED_HOSTS = new Set([
13797
- "cli",
13798
- "opencode",
13799
- "pi",
13800
- "codex_cli",
13801
- "codex_desktop"
13802
- ]);
13803
- function sessionTrust(cwd, row) {
13804
- const session = row.actor.session;
13805
- if (!session || row.self === true)
13806
- return null;
13807
- const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
13808
- const host = row.actor.host;
13809
- const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
13810
- if (!issuedOnly)
13811
- return null;
13812
- const binding = issuedBinding(cwd, session);
13813
- if (binding === null)
13814
- return "unissued_session";
13815
- return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
13816
- }
13817
- function trustReasons(cwd, row) {
13818
- if (row.integrity === "unverified")
13819
- return [row.integrityReason ?? "unsigned"];
13820
- if (row.integrity !== "verified")
13821
- return ["unsigned"];
13822
- const session = sessionTrust(cwd, row);
13823
- return session ? [session] : [];
13824
- }
13825
- function withIntegrity(cwd, rows) {
13826
- if (rows.every((row) => row.integrity !== undefined))
13827
- return rows;
13828
- const read = readLedger(cwd, { integrity: true });
13829
- const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
13830
- return rows.map((row) => {
13831
- if (row.integrity !== undefined)
13832
- return row;
13833
- const fresh = bySeq.get(row.seq);
13834
- return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
13835
- });
13836
- }
13837
- var untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason));
13838
- var independenceReasons = (row, authors) => {
13839
- const reasons = [];
13840
- if (!row.actor.session)
13841
- reasons.push("no_session");
13842
- else if (authoredBy(authors, row.actor.session))
13843
- reasons.push("author_session");
13844
- if (row.self === true && !reasons.includes("no_session"))
13845
- reasons.push("self");
13846
- return reasons;
13847
- };
13848
- function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
13849
- let effective = null;
13850
- for (const row of rows) {
13851
- if (!effective) {
13852
- effective = row;
13445
+ if (SPACE.test(char)) {
13446
+ if (collapse)
13447
+ pendingSpace = true;
13448
+ else
13449
+ out += char;
13450
+ index += 1;
13853
13451
  continue;
13854
13452
  }
13855
- if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
13453
+ const lineOpener = syntax.line.find((opener) => text.startsWith(opener, index));
13454
+ if (lineOpener) {
13455
+ const end = text.indexOf(`
13456
+ `, index);
13457
+ const comment = text.slice(index, end < 0 ? text.length : end);
13458
+ if (DIRECTIVE.test(comment.slice(lineOpener.length)))
13459
+ emitVerbatim(comment);
13460
+ else
13461
+ pendingSpace = true;
13462
+ index = end < 0 ? text.length : end;
13856
13463
  continue;
13857
- const rowIndependent = reasonsOf(row).length === 0;
13858
- const effIndependent = reasonsOf(effective).length === 0;
13859
- if (effIndependent && !rowIndependent && isCurrent(effective))
13464
+ }
13465
+ const block = syntax.block.find(([open]) => text.startsWith(open, index));
13466
+ if (block) {
13467
+ const end = text.indexOf(block[1], index + block[0].length);
13468
+ if (end < 0)
13469
+ return null;
13470
+ const comment = text.slice(index, end + block[1].length);
13471
+ if (DIRECTIVE.test(comment.slice(block[0].length, -block[1].length)))
13472
+ emitVerbatim(comment);
13473
+ else
13474
+ pendingSpace = true;
13475
+ if (comment.includes(`
13476
+ `))
13477
+ newline();
13478
+ index = end + block[1].length;
13860
13479
  continue;
13861
- if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
13480
+ }
13481
+ if (syntax.triple && (char === '"' || char === "'") && text.startsWith(char.repeat(3), index)) {
13482
+ if (!scanString(char.repeat(3), !syntax.literal?.includes(char), true))
13483
+ return null;
13862
13484
  continue;
13863
- effective = row;
13864
- }
13865
- return effective;
13866
- }
13867
- function restackCarries(rows, branch, from, to) {
13868
- if (!from || !to)
13869
- return false;
13870
- const edges = new Map;
13871
- for (const row of rows)
13872
- if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.integrity === "verified" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
13873
- edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
13874
- const seen = new Set([from]);
13875
- const queue = [from];
13876
- while (queue.length) {
13877
- const next = queue.shift();
13878
- if (next === to)
13879
- return true;
13880
- for (const target of edges.get(next) ?? [])
13881
- if (!seen.has(target)) {
13882
- seen.add(target);
13883
- queue.push(target);
13884
- }
13885
- }
13886
- return false;
13887
- }
13888
- function checkVerdicts(cwd, branch, ledgerRows) {
13889
- const rows = withIntegrity(cwd, ledgerRows);
13890
- const head = branchHead(cwd, branch);
13891
- const fallbackBase = defaultBase(cwd);
13892
- const scope = branchScope(cwd, branch);
13893
- const keyCache = new Map;
13894
- const keysAt = (base, at) => {
13895
- if (!base || !at)
13896
- return { patchId: null, diffHash: null };
13897
- const cacheKey = `${base}\x00${at}`;
13898
- let key = keyCache.get(cacheKey);
13899
- if (!key) {
13900
- key = { patchId: patchId(cwd, base, at), diffHash: diffHash(cwd, base, at) };
13901
- keyCache.set(cacheKey, key);
13902
13485
  }
13903
- return key;
13904
- };
13905
- const keyFor = (base) => keysAt(base, head);
13906
- const basisOf = (row) => {
13907
- const basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
13908
- return basis === "stale" && restackCarries(rows, branch, row.head, head) ? "carried" : basis;
13909
- };
13910
- const sameCode = (failed, row) => {
13911
- if (failed.head && failed.head === row.head)
13912
- return true;
13913
- const base = failed.base ?? fallbackBase;
13914
- const stored = (row.base ?? fallbackBase) === base && row.baseSha !== null && row.baseSha === failed.baseSha;
13915
- const keys = stored ? { patchId: row.patchId, diffHash: row.diffHash } : keysAt(base, row.head);
13916
- return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(rows, branch, failed.head, row.head);
13917
- };
13918
- const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
13919
- const failureAuthorCache = new Map;
13920
- const failureAuthors = (row) => {
13921
- let set = failureAuthorCache.get(row.seq);
13922
- if (!set) {
13923
- set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
13924
- failureAuthorCache.set(row.seq, set);
13486
+ if (syntax.quotes.includes(char)) {
13487
+ if (!scanString(char, true, Boolean(syntax.multiline)))
13488
+ return null;
13489
+ continue;
13925
13490
  }
13926
- return set;
13927
- };
13928
- const reasonCache = new Map;
13929
- const reasonsOf = (row) => {
13930
- let reasons = reasonCache.get(row);
13931
- if (!reasons) {
13932
- const failing = FAILING.has(String(row.result));
13933
- reasons = [
13934
- ...trustReasons(cwd, row),
13935
- ...independenceReasons(row, failing ? failureAuthors(row) : authors)
13936
- ];
13937
- reasonCache.set(row, reasons);
13491
+ if (syntax.literal?.includes(char)) {
13492
+ if (!scanString(char, false, false))
13493
+ return null;
13494
+ continue;
13938
13495
  }
13939
- return reasons;
13940
- };
13941
- const retired = new Set;
13942
- for (const row of rows)
13943
- if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
13944
- retired.add(row.supersedes);
13945
- const byKind = new Map;
13946
- for (const row of rows)
13947
- if (isLiveVerdict(row, retired) && scope(row)) {
13948
- const kind = typeof row.kind === "string" ? row.kind : "review";
13949
- byKind.set(kind, [...byKind.get(kind) ?? [], row]);
13496
+ if (syntax.rawBacktick && char === "`") {
13497
+ if (!scanString("`", false, true))
13498
+ return null;
13499
+ continue;
13950
13500
  }
13951
- const verdicts = [];
13952
- const warnings = [];
13953
- for (const [kind, list] of byKind) {
13954
- const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
13955
- if (!row)
13501
+ if (syntax.js && char === "`") {
13502
+ emitVerbatim(char);
13503
+ frames.push({ kind: "template" });
13504
+ index += 1;
13956
13505
  continue;
13957
- const basis = basisOf(row);
13958
- const current = basis !== "stale";
13959
- const independence = reasonsOf(row);
13960
- const reasons = [
13961
- ...current ? [] : ["stale"],
13962
- ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
13963
- ...independence
13964
- ];
13965
- verdicts.push({
13966
- kind,
13967
- basis,
13968
- current,
13969
- independent: independence.length === 0,
13970
- accepted: reasons.length === 0,
13971
- reasons,
13972
- verdict: row
13973
- });
13974
- if (reasons.length === 0) {
13975
- for (const later of list)
13976
- if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
13977
- 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`);
13978
13506
  }
13979
- }
13980
- for (const row of rows)
13981
- if (row.integrity === "unverified" && scope(row))
13982
- 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)`);
13983
- const tampered = ledgerTampering(cwd);
13984
- if (tampered.length) {
13985
- warnings.push(tamperedWarning(cwd, tampered));
13986
- for (const entry of verdicts) {
13987
- entry.accepted = false;
13988
- entry.reasons = [...entry.reasons, "ledger_tampered"];
13507
+ if (syntax.cssUrl && /^url\(\s*[^"')\s]/iu.test(text.slice(index, index + 64)))
13508
+ return null;
13509
+ if (syntax.js && char === "/" && !regexAllowed() && out.trimEnd().endsWith(")")) {
13510
+ const lineEnd = text.indexOf(`
13511
+ `, index);
13512
+ if (text.slice(index + 1, lineEnd < 0 ? text.length : lineEnd).includes("/"))
13513
+ return null;
13514
+ }
13515
+ if (syntax.js && char === "/" && regexAllowed()) {
13516
+ let end = index + 1;
13517
+ let inClass = false;
13518
+ while (end < text.length && (inClass || text[end] !== "/")) {
13519
+ if (text[end] === `
13520
+ `)
13521
+ return null;
13522
+ if (text[end] === "\\")
13523
+ end += 1;
13524
+ else if (text[end] === "[")
13525
+ inClass = true;
13526
+ else if (text[end] === "]")
13527
+ inClass = false;
13528
+ end += 1;
13529
+ }
13530
+ if (end >= text.length)
13531
+ return null;
13532
+ emitVerbatim(text.slice(index, end + 1));
13533
+ index = end + 1;
13534
+ continue;
13535
+ }
13536
+ if (frames.length > 1 && frame.kind === "code") {
13537
+ if (char === "{")
13538
+ frame.braces += 1;
13539
+ else if (char === "}") {
13540
+ if (frame.braces === 0) {
13541
+ emitVerbatim(char);
13542
+ frames.pop();
13543
+ index += 1;
13544
+ continue;
13545
+ }
13546
+ frame.braces -= 1;
13547
+ }
13989
13548
  }
13549
+ emitCode(char);
13550
+ index += 1;
13990
13551
  }
13991
- verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
13992
- const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
13993
- const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
13994
- const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
13995
- const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
13996
- accepted: false,
13997
- verdict: failing?.verdict ?? null,
13998
- reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
13999
- };
14000
- 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))));
14001
- const selfEntry = latestSelf && STRONG_RESULTS.has(String(latestSelf.verdict.result)) ? latestSelf : undefined;
14002
- return {
14003
- branch,
14004
- head,
14005
- review: accepted.accepted ? "verified" : selfEntry ? "self-reviewed" : "unreviewed",
14006
- selfVerdict: accepted.accepted ? null : selfEntry?.verdict ?? null,
14007
- base: newestCurrent?.verdict.base ?? fallbackBase,
14008
- patchId: keyFor(newestCurrent?.verdict.base ?? fallbackBase).patchId,
14009
- current: { basis: newestCurrent?.basis ?? "none", verdict: newestCurrent?.verdict ?? null },
14010
- accepted,
14011
- verdicts,
14012
- authors: [...authors].toSorted(),
14013
- warnings
14014
- };
13552
+ if (frames.length !== 1)
13553
+ return null;
13554
+ newline();
13555
+ return out;
13556
+ }
13557
+ function unbalanced(body) {
13558
+ let depth = 0;
13559
+ for (const part of body.replaceAll("{{", "").replaceAll("}}", "")) {
13560
+ if (part === "{")
13561
+ depth += 1;
13562
+ else if (part === "}")
13563
+ depth = Math.max(0, depth - 1);
13564
+ }
13565
+ return depth > 0;
13566
+ }
13567
+ function hasOpaqueLineBreak(text) {
13568
+ return /\r(?!\n)|[\u2028\u2029]/u.test(text);
13569
+ }
13570
+ var decoder = new TextDecoder("utf-8", { fatal: true });
13571
+ function textOf(bytes) {
13572
+ if (bytes.includes(0))
13573
+ return null;
13574
+ try {
13575
+ return decoder.decode(bytes);
13576
+ } catch {
13577
+ return null;
13578
+ }
13579
+ }
13580
+ function cosmeticChange(file, before, after) {
13581
+ const left = textOf(before);
13582
+ const right = textOf(after);
13583
+ if (left === null || right === null)
13584
+ return false;
13585
+ if (hasOpaqueLineBreak(left) || hasOpaqueLineBreak(right))
13586
+ return false;
13587
+ const a = substanceOf(file, left);
13588
+ return a !== null && a === substanceOf(file, right);
13589
+ }
13590
+
13591
+ // packages/workit-core/src/core/policy/lane.ts
13592
+ var LANE_THRESHOLDS = {
13593
+ selfMaxLines: 100,
13594
+ selfMaxFiles: 5,
13595
+ fullMinLines: 500
13596
+ };
13597
+ var RISK_TOKENS = new Set([
13598
+ "auth",
13599
+ "authn",
13600
+ "authz",
13601
+ "oauth",
13602
+ "security",
13603
+ "permission",
13604
+ "permissions",
13605
+ "secret",
13606
+ "secrets",
13607
+ "crypto",
13608
+ "gate",
13609
+ "gates",
13610
+ "policy",
13611
+ "policies",
13612
+ "hook",
13613
+ "hooks",
13614
+ "husky",
13615
+ "lefthook",
13616
+ "release",
13617
+ "releases",
13618
+ "migration",
13619
+ "migrations",
13620
+ "migrate",
13621
+ "workflows"
13622
+ ]);
13623
+ var RISK_FILES = new Set([
13624
+ ".gitlab-ci.yml",
13625
+ "Jenkinsfile",
13626
+ "azure-pipelines.yml",
13627
+ ".pre-commit-config.yaml",
13628
+ ".workit/risk-paths"
13629
+ ]);
13630
+ var RISK_NAMES = new Set([".gitattributes"]);
13631
+ var RISK_PATHS_FILE = ".workit/risk-paths";
13632
+ var LOCKFILES = new Set([
13633
+ "bun.lock",
13634
+ "bun.lockb",
13635
+ "package-lock.json",
13636
+ "npm-shrinkwrap.json",
13637
+ "yarn.lock",
13638
+ "pnpm-lock.yaml",
13639
+ "Cargo.lock",
13640
+ "go.sum",
13641
+ "poetry.lock",
13642
+ "uv.lock",
13643
+ "Pipfile.lock",
13644
+ "Gemfile.lock",
13645
+ "composer.lock"
13646
+ ]);
13647
+ var DOC_EXT = /\.(?:md|mdx|markdown|rst|adoc)$/iu;
13648
+ var DOC_NAMES = /^(?:license|licence|notice|authors|changelog|changes|contributors)$/iu;
13649
+ var INSTRUCTIONS = /(?:^|\/)(?:AGENTS|CLAUDE|GEMINI|SKILL)\.md$|(?:^|\/)(?:skills|agents|commands|rules|prompts)\/[^/]+/iu;
13650
+ var TEST_PATH = /(?:^|\/)(?:test|tests|__tests__)\/|\.(?:test|spec)\.[^/]+$|_test\.[^/]+$/iu;
13651
+ var basename = (file) => file.slice(file.lastIndexOf("/") + 1);
13652
+ var isDoc = (file) => !INSTRUCTIONS.test(file) && (DOC_EXT.test(file) || DOC_NAMES.test(basename(file)));
13653
+ function kindOf(file) {
13654
+ if (file.binary)
13655
+ return "behavior";
13656
+ if (file.generated)
13657
+ return "generated";
13658
+ if (LOCKFILES.has(basename(file.path)))
13659
+ return "lockfile";
13660
+ if (isDoc(file.path))
13661
+ return "doc";
13662
+ if (file.cosmetic)
13663
+ return "cosmetic";
13664
+ return TEST_PATH.test(file.path) ? "test" : "behavior";
14015
13665
  }
14016
- var fromLead = (row, leads) => leads === null || row.actor.session !== null && leads.includes(row.actor.session);
14017
- function activeStanding(rows, fanout, leads = null) {
14018
- const active = [];
14019
- for (const row of rows) {
14020
- if (row.fanout !== fanout || !fromLead(row, leads))
14021
- continue;
14022
- if (row.type === "standing" && typeof row.what === "string" && row.what.trim()) {
14023
- if (!row.superseded)
14024
- active.push(row);
14025
- } else if (row.type === "standing.cleared") {
14026
- const target = str2(row.target);
14027
- if (target === null)
14028
- active.length = 0;
14029
- else {
14030
- const index = active.findIndex((order) => order.id === target);
14031
- if (index >= 0)
14032
- active.splice(index, 1);
14033
- }
13666
+ var globCache = new Map;
13667
+ function globMatch(pattern, file) {
13668
+ let regex = globCache.get(pattern);
13669
+ if (!regex) {
13670
+ let source = "";
13671
+ for (let index = 0;index < pattern.length; index += 1) {
13672
+ const char = pattern[index];
13673
+ if (char === "*" && pattern[index + 1] === "*") {
13674
+ source += pattern[index + 2] === "/" ? "(?:[^/]*/)*" : ".*";
13675
+ index += pattern[index + 2] === "/" ? 2 : 1;
13676
+ } else if (char === "*")
13677
+ source += "[^/]*";
13678
+ else if (char === "?")
13679
+ source += "[^/]";
13680
+ else
13681
+ source += char.replace(/[.+^${}()|[\]\\]/gu, "\\$&");
14034
13682
  }
13683
+ regex = new RegExp(`^${source}$`, "u");
13684
+ globCache.set(pattern, regex);
14035
13685
  }
14036
- return active;
13686
+ return regex.test(file) || file.startsWith(`${pattern.replace(/\/$/u, "")}/`);
14037
13687
  }
14038
- var OBSERVED_ONLY = new Set([
14039
- "check",
14040
- "merge.unverified",
14041
- "policy.judged",
14042
- ...PR_ROW_TYPES
14043
- ]);
14044
-
14045
- // packages/workit-core/src/core/policy/lane.ts
14046
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
14047
- import path17 from "node:path";
14048
-
14049
- // packages/workit-core/src/core/pr-create.ts
14050
- import fs14 from "node:fs";
14051
- import path16 from "node:path";
14052
- import { spawnSync as spawnSync10 } from "node:child_process";
14053
- // packages/workit-core/src/forge/pr-body.ts
14054
- function parseGhIssue(value) {
14055
- const m = /issues\/(\d+)/.exec(value);
14056
- return m ? m[1] : String(value).trim().replace(/^#/, "");
13688
+ function isRiskPath(file, extra = []) {
13689
+ if (extra.some((pattern) => globMatch(pattern, file)))
13690
+ return true;
13691
+ if (RISK_FILES.has(file) || RISK_NAMES.has(basename(file)))
13692
+ return true;
13693
+ if (TEST_PATH.test(file) || isDoc(file))
13694
+ return false;
13695
+ 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)));
14057
13696
  }
14058
-
14059
- // packages/workit-core/src/core/pr-create.ts
14060
- function whichOnPath(tool) {
14061
- const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
14062
- for (const dir of (process.env.PATH ?? "").split(path16.delimiter)) {
14063
- if (!dir)
14064
- continue;
14065
- for (const name of names) {
14066
- const candidate = path16.join(dir, name);
13697
+ var parseRiskPaths = (text) => text.split(`
13698
+ `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.replace(/^\//u, ""));
13699
+ function computeLane(input) {
13700
+ const riskFiles = input.files.filter((file) => isRiskPath(file.path, input.riskPaths)).map((file) => file.path);
13701
+ const risky = new Set(riskFiles);
13702
+ const kinds = input.files.map((file) => ({
13703
+ file,
13704
+ kind: risky.has(file.path) ? "behavior" : kindOf(file)
13705
+ }));
13706
+ const behavior = kinds.filter((entry) => entry.kind === "behavior" || entry.kind === "test" || entry.kind === "lockfile");
13707
+ const lines = behavior.filter((entry) => entry.kind !== "lockfile").reduce((sum, entry) => sum + entry.file.added + entry.file.deleted, 0);
13708
+ const opaque = behavior.filter((entry) => entry.kind === "lockfile" || entry.file.binary);
13709
+ const result = (lane, reasons) => ({
13710
+ lane,
13711
+ reasons,
13712
+ lines,
13713
+ files: behavior.length,
13714
+ riskFiles
13715
+ });
13716
+ const { selfMaxLines, selfMaxFiles, fullMinLines } = LANE_THRESHOLDS;
13717
+ if (input.riskTier === "high")
13718
+ return result("full", ["the task judgment records high risk"]);
13719
+ if (riskFiles.length)
13720
+ return result("full", [
13721
+ `touches a risk path: ${riskFiles.slice(0, 3).join(", ")}${riskFiles.length > 3 ? ", …" : ""}`
13722
+ ]);
13723
+ if (!behavior.length)
13724
+ return result("none", [
13725
+ input.files.length ? "only docs, comments, formatting or generated copies changed" : "no changes"
13726
+ ]);
13727
+ if (lines >= fullMinLines)
13728
+ return result("full", [`large diff: ${lines} lines (≥ ${fullMinLines})`]);
13729
+ if (lines > selfMaxLines)
13730
+ return result("focused", [`${lines} lines changed (> ${selfMaxLines})`]);
13731
+ if (behavior.length > selfMaxFiles)
13732
+ return result("focused", [`${behavior.length} behavior files changed (> ${selfMaxFiles})`]);
13733
+ if (opaque.length)
13734
+ return result("focused", [
13735
+ `a lockfile or binary changed: ${opaque.slice(0, 3).map((entry) => entry.file.path).join(", ")}${opaque.length > 3 ? ", …" : ""}`
13736
+ ]);
13737
+ return result("self", [
13738
+ `small change: ${lines} line${lines === 1 ? "" : "s"} in ${behavior.length} file${behavior.length === 1 ? "" : "s"}, no risk path`
13739
+ ]);
13740
+ }
13741
+ function untrackedStat(cwd, file) {
13742
+ try {
13743
+ const bytes = readFileSync4(path14.join(cwd, file));
13744
+ if (bytes.includes(0))
13745
+ return { path: file, added: 0, deleted: 0, binary: true };
13746
+ const text = bytes.toString("utf8");
13747
+ if (hasOpaqueLineBreak(text))
13748
+ return { path: file, added: 0, deleted: 0, binary: true };
13749
+ const lines = text.split(`
13750
+ `).length - (text.endsWith(`
13751
+ `) ? 1 : 0);
13752
+ return { path: file, added: lines, deleted: 0 };
13753
+ } catch {
13754
+ return { path: file, added: 0, deleted: 0, binary: true };
13755
+ }
13756
+ }
13757
+ var REGULAR = new Set(["100644", "100755"]);
13758
+ var ABSENT = /^0+$/u;
13759
+ function cosmeticFiles(cwd, fork, head) {
13760
+ const raw = diffRaw(cwd, fork, head);
13761
+ if (!raw)
13762
+ return null;
13763
+ const candidates = raw.filter((entry) => REGULAR.has(entry.oldMode) && entry.oldMode === entry.newMode && !ABSENT.test(entry.oldBlob) && (head === null || !ABSENT.test(entry.newBlob)));
13764
+ const blobs = readBlobs(cwd, candidates.flatMap((entry) => [entry.oldBlob, entry.newBlob]));
13765
+ if (!blobs)
13766
+ return null;
13767
+ const out = new Set;
13768
+ const opaque = new Set;
13769
+ for (const entry of candidates) {
13770
+ const before = blobs.get(entry.oldBlob);
13771
+ let after = blobs.get(entry.newBlob);
13772
+ if (!after && head === null && ABSENT.test(entry.newBlob))
14067
13773
  try {
14068
- fs14.accessSync(candidate, fs14.constants.X_OK);
14069
- return candidate;
14070
- } catch {}
14071
- }
13774
+ after = readFileSync4(path14.join(cwd, entry.path));
13775
+ } catch {
13776
+ after = undefined;
13777
+ }
13778
+ if (before && after && cosmeticChange(entry.path, before, after))
13779
+ out.add(entry.path);
13780
+ else if ([before, after].some((bytes) => bytes && hasOpaqueLineBreak(bytes.toString("utf8"))))
13781
+ opaque.add(entry.path);
14072
13782
  }
14073
- return null;
13783
+ return { cosmetic: out, opaque };
14074
13784
  }
14075
- var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
14076
- var pushRemote = (cwd) => {
14077
- const result = spawnSync10("git", ["remote", "get-url", "--push", "--all", "origin"], {
14078
- cwd,
14079
- encoding: "utf8"
14080
- });
14081
- if (result.status !== 0)
13785
+ function generatedAt(cwd, fork, paths) {
13786
+ const files = treeFilesNamed(cwd, fork, ".gitattributes");
13787
+ const blobs = files?.size ? readBlobs(cwd, [...files.values()]) : null;
13788
+ if (!files || !blobs)
13789
+ return new Set;
13790
+ const texts = new Map;
13791
+ for (const [file, id] of files) {
13792
+ const bytes = blobs.get(id);
13793
+ if (bytes)
13794
+ texts.set(file, bytes.toString("utf8"));
13795
+ }
13796
+ return generatedByAttributes(texts, paths);
13797
+ }
13798
+ var forkOf = (cwd, base, head) => mergeBase(cwd, base, head ?? "HEAD");
13799
+ function laneFiles(cwd, fork, head, attributesAt = fork) {
13800
+ const stats = diffNumstat(cwd, fork, head);
13801
+ const cosmetic = stats ? cosmeticFiles(cwd, fork, head) : null;
13802
+ if (!stats || !cosmetic)
14082
13803
  return null;
14083
- const urls = (result.stdout ?? "").split(/\r?\n/u).map((url) => url.trim()).filter(Boolean);
14084
- return urls.length === 1 ? urls[0] : null;
14085
- };
14086
-
14087
- // packages/workit-core/src/forge/redact.ts
14088
- 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_.-]*`;
14089
- var ASSIGNMENT = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*["']?)((?:bearer|basic|token|digest)\s+)?([^\s"',;&]+)`, "giu");
14090
- var QUOTED_ASSIGNMENT = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*)(["'])([^"'\n]*[^\s"'][^"'\n]*)\2`, "giu");
14091
- var ANSI = new RegExp(String.raw`\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007]*\u0007`, "gu");
13804
+ const files = [
13805
+ ...stats.map((entry) => ({
13806
+ ...entry,
13807
+ ...cosmetic.opaque.has(entry.path) ? { binary: true } : {},
13808
+ cosmetic: !entry.binary && cosmetic.cosmetic.has(entry.path)
13809
+ })),
13810
+ ...head === null ? untrackedFiles(cwd).map((file) => untrackedStat(cwd, file)) : []
13811
+ ];
13812
+ const generated = generatedAt(cwd, attributesAt, files.map((entry) => entry.path));
13813
+ return files.map((entry) => ({
13814
+ ...entry,
13815
+ generated: generated.has(entry.path)
13816
+ }));
13817
+ }
13818
+ function driftFiles(cwd, range) {
13819
+ if (range.fromFork === range.toFork)
13820
+ return laneFiles(cwd, range.from, range.to, range.toFork);
13821
+ const drifted = ownChangeDrift(cwd, range);
13822
+ const stats = drifted ? diffNumstat(cwd, range.from, range.to) : null;
13823
+ if (!drifted || !stats)
13824
+ return null;
13825
+ const byPath = new Map(stats.map((entry) => [entry.path, entry]));
13826
+ const generated = generatedAt(cwd, range.toFork, drifted);
13827
+ return drifted.map((file) => ({
13828
+ path: file,
13829
+ added: byPath.get(file)?.added ?? 0,
13830
+ deleted: byPath.get(file)?.deleted ?? 0,
13831
+ ...byPath.get(file)?.binary ? { binary: true } : {},
13832
+ cosmetic: false,
13833
+ generated: generated.has(file)
13834
+ }));
13835
+ }
13836
+ function branchLane(cwd, options) {
13837
+ const candidates = options.base ? [`origin/${options.base}`, options.base] : [];
13838
+ const base = candidates.find((ref) => resolveRef(cwd, ref) !== null) ?? defaultBase(cwd) ?? null;
13839
+ const head = options.workingTree ? null : options.head;
13840
+ const fork = base && (options.workingTree || options.head) ? forkOf(cwd, base, head) : null;
13841
+ const files = fork ? laneFiles(cwd, fork, head) : null;
13842
+ const riskTier = options.judgment?.riskTier ?? null;
13843
+ if (!fork || !files) {
13844
+ const high = riskTier === "high";
13845
+ return {
13846
+ lane: high ? "full" : "focused",
13847
+ reasons: [
13848
+ `the diff of ${options.head?.slice(0, 12) ?? "(no head)"} against ${base ?? "(no base)"} cannot be read`,
13849
+ ...high ? ["the task judgment records high risk"] : []
13850
+ ],
13851
+ lines: 0,
13852
+ files: 0,
13853
+ riskFiles: [],
13854
+ base,
13855
+ head: options.head
13856
+ };
13857
+ }
13858
+ const repoRisk = parseRiskPaths(fileAt(cwd, fork, RISK_PATHS_FILE) ?? "");
13859
+ return {
13860
+ ...computeLane({
13861
+ files,
13862
+ riskTier,
13863
+ riskPaths: [...repoRisk, ...options.riskPaths ?? []]
13864
+ }),
13865
+ base,
13866
+ head: options.head
13867
+ };
13868
+ }
13869
+ var laneNeed = (lane) => lane === "none" ? "none" : lane === "self" ? "self" : "non-author";
14092
13870
 
14093
- // packages/workit-core/src/git/ops.ts
14094
- var COMMIT_TIMEOUT_MS = 10 * 60000;
13871
+ // packages/workit-core/src/core/policy/carry.ts
13872
+ var TEST_ONLY_KINDS = new Set(["doc", "cosmetic", "generated", "test"]);
13873
+ function verdictDrift(cwd, input) {
13874
+ const since = input.from;
13875
+ const stale = (reason, files = null, focused = false) => ({
13876
+ carries: false,
13877
+ since,
13878
+ files,
13879
+ focused,
13880
+ reason
13881
+ });
13882
+ const fromFork = mergeBase(cwd, input.fromBase, input.from);
13883
+ const toFork = mergeBase(cwd, input.toBase, input.to);
13884
+ const files = fromFork && toFork ? driftFiles(cwd, { from: input.from, to: input.to, fromFork, toFork }) : null;
13885
+ if (!fromFork || !toFork || !files)
13886
+ return stale(`the change since ${since.slice(0, 12)} cannot be read`);
13887
+ const riskPaths = [fromFork, toFork].flatMap((fork) => parseRiskPaths(fileAt(cwd, fork, RISK_PATHS_FILE) ?? ""));
13888
+ const lane = computeLane({ files, riskPaths });
13889
+ const paths = files.map((file) => file.path);
13890
+ if (lane.lane === "none")
13891
+ return { carries: true, rule: "lane-none", since, files: paths };
13892
+ if (input.kind === "unit" && lane.lane === "self" && files.every((file) => TEST_ONLY_KINDS.has(kindOf(file))))
13893
+ return { carries: true, rule: "tests-only", since, files: paths };
13894
+ return stale(lane.reasons[0] ?? `lane ${lane.lane}`, paths, lane.lane === "self");
13895
+ }
14095
13896
 
14096
- // packages/workit-core/src/core/policy/attributes.ts
14097
- function patternRegex(dir, pattern) {
14098
- if (!pattern || pattern.endsWith("/") || pattern.startsWith('"') || pattern.startsWith("!"))
14099
- return null;
14100
- const anchored = pattern.includes("/");
14101
- const body = pattern.replace(/^\//u, "");
14102
- let source = "";
14103
- for (let index = 0;index < body.length; index += 1) {
14104
- const char = body[index];
14105
- if (char === "*" && body[index + 1] === "*") {
14106
- const slash = body[index + 2] === "/";
14107
- source += slash ? "(?:.*/)?" : ".*";
14108
- index += slash ? 2 : 1;
14109
- } else if (char === "*")
14110
- source += "[^/]*";
14111
- else if (char === "?")
14112
- source += "[^/]";
14113
- else if (char === "[") {
14114
- const end = body.indexOf("]", index + 2);
14115
- if (end < 0)
13897
+ // packages/workit-core/src/hooks/standing-signal.ts
13898
+ import { createHash as createHash10 } from "node:crypto";
13899
+ import fs12 from "node:fs";
13900
+ import path15 from "node:path";
13901
+ var gitStoreDir = (cwd) => {
13902
+ try {
13903
+ for (let dir = path15.resolve(cwd);; dir = path15.dirname(dir)) {
13904
+ const dotGit = path15.join(dir, ".git");
13905
+ if (fs12.existsSync(dotGit)) {
13906
+ let gitDir = dotGit;
13907
+ if (fs12.statSync(dotGit).isFile()) {
13908
+ const target = /^gitdir:\s*(.+)$/mu.exec(fs12.readFileSync(dotGit, "utf8"))?.[1]?.trim();
13909
+ if (!target)
13910
+ return null;
13911
+ gitDir = path15.resolve(dir, target);
13912
+ const common = path15.join(gitDir, "commondir");
13913
+ if (fs12.existsSync(common))
13914
+ gitDir = path15.resolve(gitDir, fs12.readFileSync(common, "utf8").trim());
13915
+ }
13916
+ return path15.join(gitDir, "workit");
13917
+ }
13918
+ if (path15.dirname(dir) === dir)
14116
13919
  return null;
14117
- const set = body.slice(index + 1, end).replace(/^!/u, "^").replace(/\\/gu, "\\\\");
14118
- source += `[${set}]`;
14119
- index = end;
14120
- } else if (char === "\\" && index + 1 < body.length) {
14121
- source += body[index + 1].replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
14122
- index += 1;
14123
- } else
14124
- source += char.replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
13920
+ }
13921
+ } catch {
13922
+ return null;
14125
13923
  }
14126
- const prefix = dir ? `${dir.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}/` : "";
13924
+ };
13925
+ var standingRevFile = (store) => path15.join(store, "standing", "rev");
13926
+ var standingSeenFile = (store, session, agentId) => path15.join(store, "standing", "seen", `${createHash10("sha256").update(`${session}\x00${agentId ?? ""}`).digest("hex").slice(0, 32)}.json`);
13927
+ var readStandingRev = (store) => {
14127
13928
  try {
14128
- return new RegExp(anchored ? `^${prefix}${source}$` : `^${prefix}(?:.*/)?${source}$`, "u");
13929
+ return fs12.readFileSync(standingRevFile(store), "utf8").trim() || null;
14129
13930
  } catch {
14130
13931
  return null;
14131
13932
  }
14132
- }
14133
- function rulesOf(dir, text) {
14134
- const out = [];
14135
- for (const raw of text.split(`
14136
- `)) {
14137
- const line = raw.trim();
14138
- if (!line || line.startsWith("#") || line.startsWith("[attr]"))
14139
- continue;
14140
- const [pattern, ...attributes] = line.split(/\s+/u);
14141
- let generated = null;
14142
- for (const attribute of attributes) {
14143
- if (attribute === "linguist-generated" || attribute === "linguist-generated=true")
14144
- generated = true;
14145
- else if (attribute === "-linguist-generated" || attribute === "!linguist-generated" || attribute.startsWith("linguist-generated="))
14146
- generated = false;
14147
- }
14148
- if (generated === null)
14149
- continue;
14150
- const regex = patternRegex(dir, pattern);
14151
- if (regex)
14152
- out.push({ regex, generated });
14153
- }
14154
- return out;
14155
- }
14156
- function generatedByAttributes(files, paths) {
14157
- const rules = [...files].map(([file, text]) => {
14158
- const dir = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
13933
+ };
13934
+ var readStandingSeen = (file) => {
13935
+ try {
13936
+ const value = JSON.parse(fs12.readFileSync(file, "utf8"));
14159
13937
  return {
14160
- depth: dir ? dir.split("/").length : 0,
14161
- rules: rulesOf(dir, text)
13938
+ rev: typeof value.rev === "string" ? value.rev : null,
13939
+ ids: Array.isArray(value.ids) ? value.ids.filter((id) => typeof id === "string") : []
14162
13940
  };
14163
- }).toSorted((left, right) => left.depth - right.depth).flatMap((entry) => entry.rules);
14164
- const out = new Set;
14165
- for (const file of paths) {
14166
- const last = rules.findLast((rule) => rule.regex.test(file));
14167
- if (last?.generated)
14168
- out.add(file);
13941
+ } catch {
13942
+ return { rev: null, ids: [] };
14169
13943
  }
14170
- return out;
14171
- }
13944
+ };
13945
+ var writeStandingSeen = (file, seen) => {
13946
+ try {
13947
+ fs12.mkdirSync(path15.dirname(file), { recursive: true });
13948
+ fs12.writeFileSync(file, JSON.stringify(seen));
13949
+ } catch {}
13950
+ };
14172
13951
 
14173
- // packages/workit-core/src/core/policy/substance.ts
14174
- var SLASH_BLOCK = [["/*", "*/"]];
14175
- var C_LIKE = { line: ["//"], block: SLASH_BLOCK, quotes: `"'` };
14176
- var SYNTAXES = Object.fromEntries([
14177
- ...["ts", "mts", "cts", "js", "mjs", "cjs"].map((ext) => [ext, { ...C_LIKE, js: true }]),
14178
- ...["java", "kt", "kts", "scala", "swift", "dart", "groovy"].map((ext) => [
14179
- ext,
14180
- { ...C_LIKE, triple: true }
14181
- ]),
14182
- ["go", { ...C_LIKE, rawBacktick: true }],
14183
- ...["css", "scss", "less"].map((ext) => [
14184
- ext,
14185
- { line: [], block: SLASH_BLOCK, quotes: `"'`, cssUrl: true }
14186
- ]),
14187
- ...["py", "pyi"].map((ext) => [
14188
- ext,
14189
- { line: ["#"], block: [], quotes: `"'`, triple: true, indented: true, fstring: true }
14190
- ]),
14191
- ["toml", { line: ["#"], block: [], quotes: `"`, literal: "'", triple: true }],
14192
- ...["html", "htm", "xml", "svg", "xhtml"].map((ext) => [
14193
- ext,
14194
- { line: [], block: [["<!--", "-->"]], quotes: "", indented: true }
14195
- ])
13952
+ // packages/workit-core/src/ledger.ts
13953
+ var LEDGER_VERSION = 1;
13954
+ var MAX_LINE_BYTES = 4096;
13955
+ var PASSING = new Set(["verified", "tests-verified", "type-check-only"]);
13956
+ var FAILING = new Set(["failed", "blocked"]);
13957
+ var STRONG_RESULTS = new Set(["verified", "tests-verified"]);
13958
+ var PR_ROW_TYPES = new Set([
13959
+ "pr.created",
13960
+ "pr.status",
13961
+ "pr.merged",
13962
+ "pr.linked"
14196
13963
  ]);
14197
- 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;
14198
- var extensionOf = (file) => {
14199
- const name = file.slice(file.lastIndexOf("/") + 1);
14200
- const dot = name.lastIndexOf(".");
14201
- return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
13964
+ var err = (code, error, unblock) => ({
13965
+ ok: false,
13966
+ code,
13967
+ error,
13968
+ ...unblock ? { unblock } : {}
13969
+ });
13970
+ var gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
13971
+ const run = spawnSync9("git", args, {
13972
+ cwd,
13973
+ encoding: "utf8",
13974
+ timeout: timeoutMs,
13975
+ killSignal: "SIGKILL",
13976
+ windowsHide: true,
13977
+ maxBuffer: 256 * 1024 * 1024,
13978
+ env: {
13979
+ ...process.env,
13980
+ GIT_TERMINAL_PROMPT: "0",
13981
+ GIT_OPTIONAL_LOCKS: "0",
13982
+ LC_ALL: "C",
13983
+ LANGUAGE: "C"
13984
+ }
13985
+ });
13986
+ return {
13987
+ ok: run.status === 0,
13988
+ stdout: run.stdout ?? "",
13989
+ stderr: run.error ? String(run.error.message) : run.stderr ?? ""
13990
+ };
14202
13991
  };
14203
- var CI_PATH = /^\.github\/|(?:^|\/)(?:\.gitlab-ci\.ya?ml|Jenkinsfile|azure-pipelines\.ya?ml|\.pre-commit-config\.ya?ml|\.circleci\/.*)$/u;
14204
- var syntaxOf = (file) => CI_PATH.test(file) ? null : SYNTAXES[extensionOf(file)] ?? null;
14205
- var SPACE = /[ \t\r\f\v]/u;
14206
- var WORD = /[\w$]/u;
14207
- var NO_SPACE_AFTER = "([{,;";
14208
- var NO_SPACE_BEFORE = ")]},;";
14209
- var REGEX_AFTER = "(,=:[!&|?{};+-*%<>~^";
14210
- var REGEX_KEYWORDS = new Set([
14211
- "return",
14212
- "typeof",
14213
- "instanceof",
14214
- "case",
14215
- "do",
14216
- "else",
14217
- "in",
14218
- "of",
14219
- "new",
14220
- "delete",
14221
- "void",
14222
- "throw",
14223
- "yield",
14224
- "await"
14225
- ]);
14226
- function substanceOf(file, text) {
14227
- const syntax = syntaxOf(file);
14228
- if (!syntax)
14229
- return plainSubstance(text);
14230
- const markup = syntax.block.some(([open]) => open === "<!--");
14231
- if (markup && (/<(?:script|style)\b|<!\[CDATA\[|<\?/iu.test(text) || /[^>\s][ \t]*<!--/u.test(text)))
14232
- return plainSubstance(text);
14233
- return lex(syntax, text);
13992
+ var git4 = (cwd, args) => {
13993
+ const run = gitRun2(cwd, args);
13994
+ const value = run.ok ? run.stdout.trim() : "";
13995
+ return value ? value : null;
13996
+ };
13997
+ var safeRef2 = (value) => value.length > 0 && !value.startsWith("-");
13998
+ function resolveCommit(cwd, ref) {
13999
+ if (!safeRef2(ref))
14000
+ return null;
14001
+ return git4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]);
14234
14002
  }
14235
- function plainSubstance(text) {
14236
- return text.split(`
14237
- `).map((line) => line.replace(/[ \t\r\f\v]+$/u, "")).filter(Boolean).join(`
14238
- `);
14003
+ function branchHead(cwd, branch) {
14004
+ if (!safeRef2(branch))
14005
+ return null;
14006
+ return resolveCommit(cwd, `refs/heads/${branch}`) ?? resolveCommit(cwd, `refs/remotes/origin/${branch}`);
14239
14007
  }
14240
- function lex(syntax, text) {
14241
- const collapse = !syntax.indented;
14242
- let out = "";
14243
- let lineStart = 0;
14244
- let kept = 0;
14245
- let pendingSpace = false;
14246
- const frames = [{ kind: "code", braces: 0 }];
14247
- let index = 0;
14248
- if (text.startsWith("#!")) {
14249
- const end = text.indexOf(`
14008
+ function diffHash(cwd, base, head) {
14009
+ if (!safeRef2(base) || !safeRef2(head))
14010
+ return null;
14011
+ const diff = gitRun2(cwd, [
14012
+ "diff",
14013
+ "-U3",
14014
+ "--diff-algorithm=myers",
14015
+ "--indent-heuristic",
14016
+ "--no-color",
14017
+ "--no-ext-diff",
14018
+ "--no-textconv",
14019
+ "--no-renames",
14020
+ "--full-index",
14021
+ "--binary",
14022
+ "--src-prefix=a/",
14023
+ "--dst-prefix=b/",
14024
+ `${base}...${head}`,
14025
+ "--"
14026
+ ], GIT_TIMEOUTS.worktree);
14027
+ if (!diff.ok || !diff.stdout)
14028
+ return null;
14029
+ const hash = createHash11("sha256");
14030
+ for (const line of diff.stdout.split(`
14031
+ `)) {
14032
+ if (line.startsWith("index "))
14033
+ continue;
14034
+ hash.update(line.startsWith("@@ ") ? "@@" : line);
14035
+ hash.update(`
14250
14036
  `);
14251
- out = end < 0 ? text : text.slice(0, end);
14252
- kept = out.length;
14253
- index = end < 0 ? text.length : end;
14254
14037
  }
14255
- const emitCode = (chunk) => {
14256
- if (collapse && pendingSpace && out.length > lineStart) {
14257
- const before = out[out.length - 1];
14258
- if (!NO_SPACE_AFTER.includes(before) && !NO_SPACE_BEFORE.includes(chunk[0]))
14259
- out += " ";
14260
- }
14261
- pendingSpace = false;
14262
- out += chunk;
14263
- };
14264
- const emitVerbatim = (chunk) => {
14265
- emitCode(chunk);
14266
- kept = out.length;
14267
- };
14268
- const newline = () => {
14269
- if (!collapse) {
14270
- let end = out.length;
14271
- while (end > Math.max(lineStart, kept) && SPACE.test(out[end - 1]))
14272
- end -= 1;
14273
- out = out.slice(0, end);
14038
+ return `sha256:${hash.digest("hex")}`;
14039
+ }
14040
+ function storeRoot(cwd) {
14041
+ const location = resolveStore(path16.resolve(cwd));
14042
+ if (location instanceof Error)
14043
+ 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");
14044
+ return { ok: true, value: { root: location.dir, shared: location.shared } };
14045
+ }
14046
+ function ledgerPath(cwd) {
14047
+ const root = storeRoot(cwd);
14048
+ return root.ok ? { ok: true, value: path16.join(root.value.root, "ledger", "ledger.jsonl") } : root;
14049
+ }
14050
+ var newId2 = () => `${Date.now().toString(36)}-${randomBytes3(6).toString("hex")}`;
14051
+ var NETWORK_FS_TYPES = new Set([
14052
+ 26985,
14053
+ 20859,
14054
+ 4266872130,
14055
+ 4283649346,
14056
+ 1702057286,
14057
+ 1397113167,
14058
+ 12805120,
14059
+ 16914839,
14060
+ 1196443219,
14061
+ 198183888
14062
+ ]);
14063
+ var isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0);
14064
+ var onNetworkFs = (dir) => {
14065
+ if (process.platform !== "linux")
14066
+ return false;
14067
+ try {
14068
+ return isNetworkFsType(fs13.statfsSync(dir).type);
14069
+ } catch {
14070
+ return false;
14071
+ }
14072
+ };
14073
+ var LOCK_WAIT_MS = 5000;
14074
+ var LOCK_STALE_MS = 30000;
14075
+ var LOCK_OWNER = "owner";
14076
+ var lockToken = (lock) => {
14077
+ try {
14078
+ return fs13.readFileSync(path16.join(lock, LOCK_OWNER), "utf8");
14079
+ } catch {
14080
+ return null;
14081
+ }
14082
+ };
14083
+ var lockAgeMs = (lock) => {
14084
+ for (const target of [path16.join(lock, LOCK_OWNER), lock]) {
14085
+ try {
14086
+ return Date.now() - fs13.statSync(target).mtimeMs;
14087
+ } catch {}
14088
+ }
14089
+ return null;
14090
+ };
14091
+ var ledgerLock = {
14092
+ acquire(lock) {
14093
+ try {
14094
+ fs13.mkdirSync(lock);
14095
+ } catch (error) {
14096
+ if (error.code === "EEXIST")
14097
+ return null;
14098
+ throw error;
14274
14099
  }
14275
- pendingSpace = false;
14276
- if (out.length > lineStart) {
14277
- out += `
14278
- `;
14279
- lineStart = out.length;
14100
+ const token = `${process.pid}-${randomBytes3(8).toString("hex")}`;
14101
+ fs13.writeFileSync(path16.join(lock, LOCK_OWNER), token);
14102
+ return token;
14103
+ },
14104
+ release(lock, token) {
14105
+ if (lockToken(lock) !== token)
14106
+ return false;
14107
+ const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
14108
+ try {
14109
+ fs13.renameSync(lock, tomb);
14110
+ } catch {
14111
+ return false;
14280
14112
  }
14281
- };
14282
- const regexAllowed = () => {
14283
- let end = out.length;
14284
- while (end > 0 && /\s/u.test(out[end - 1]))
14285
- end -= 1;
14286
- if (end === 0 || REGEX_AFTER.includes(out[end - 1]))
14287
- return true;
14288
- let start = end;
14289
- while (start > 0 && WORD.test(out[start - 1]))
14290
- start -= 1;
14291
- return REGEX_KEYWORDS.has(out.slice(start, end));
14292
- };
14293
- const scanString = (quote, escapes, multiline) => {
14294
- const prefix = text.slice(Math.max(0, index - 2), index).match(/[a-z]*$/iu)?.[0] ?? "";
14295
- const fstring = syntax.fstring === true && /f/iu.test(prefix);
14296
- let end = index + quote.length;
14297
- while (end < text.length) {
14298
- const char = text[end];
14299
- if (escapes && char === "\\") {
14300
- end += 2;
14301
- continue;
14302
- }
14303
- if (text.startsWith(quote, end)) {
14304
- if (fstring && unbalanced(text.slice(index + quote.length, end)))
14305
- return false;
14306
- emitVerbatim(text.slice(index, end + quote.length));
14307
- index = end + quote.length;
14308
- return true;
14309
- }
14310
- if (char === `
14311
- ` && !multiline)
14312
- return false;
14313
- end += 1;
14113
+ if (lockToken(tomb) !== token) {
14114
+ try {
14115
+ fs13.renameSync(tomb, lock);
14116
+ } catch {}
14117
+ return false;
14314
14118
  }
14315
- return false;
14316
- };
14317
- while (index < text.length) {
14318
- const frame = frames[frames.length - 1];
14319
- const char = text[index];
14320
- if (frame.kind === "template") {
14321
- if (char === "\\") {
14322
- emitVerbatim(text.slice(index, index + 2));
14323
- index += 2;
14324
- } else if (char === "`") {
14325
- emitVerbatim(char);
14326
- frames.pop();
14327
- index += 1;
14328
- } else if (text.startsWith("${", index)) {
14329
- emitVerbatim("${");
14330
- frames.push({ kind: "code", braces: 0 });
14331
- index += 2;
14332
- } else {
14333
- out += char;
14334
- kept = out.length;
14335
- index += 1;
14336
- }
14337
- continue;
14119
+ fs13.rmSync(tomb, { recursive: true, force: true });
14120
+ return true;
14121
+ },
14122
+ token: lockToken,
14123
+ reap(lock, observed) {
14124
+ const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
14125
+ try {
14126
+ fs13.renameSync(lock, tomb);
14127
+ } catch {
14128
+ return false;
14338
14129
  }
14339
- if (char === `
14340
- `) {
14341
- newline();
14342
- index += 1;
14343
- continue;
14130
+ if (lockToken(tomb) === observed) {
14131
+ fs13.rmSync(tomb, { recursive: true, force: true });
14132
+ return true;
14344
14133
  }
14345
- if (SPACE.test(char)) {
14346
- if (collapse)
14347
- pendingSpace = true;
14348
- else
14349
- out += char;
14350
- index += 1;
14351
- continue;
14134
+ try {
14135
+ fs13.renameSync(tomb, lock);
14136
+ } catch {
14137
+ fs13.rmSync(tomb, { recursive: true, force: true });
14352
14138
  }
14353
- const lineOpener = syntax.line.find((opener) => text.startsWith(opener, index));
14354
- if (lineOpener) {
14355
- const end = text.indexOf(`
14356
- `, index);
14357
- const comment = text.slice(index, end < 0 ? text.length : end);
14358
- if (DIRECTIVE.test(comment.slice(lineOpener.length)))
14359
- emitVerbatim(comment);
14360
- else
14361
- pendingSpace = true;
14362
- index = end < 0 ? text.length : end;
14139
+ return false;
14140
+ }
14141
+ };
14142
+ function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
14143
+ const lock = `${file}.lock`;
14144
+ const deadline = Date.now() + waitMs;
14145
+ let token = null;
14146
+ for (;; ) {
14147
+ token = ledgerLock.acquire(lock);
14148
+ if (token)
14149
+ break;
14150
+ const observed = lockToken(lock);
14151
+ const age = lockAgeMs(lock);
14152
+ if (age !== null && age > LOCK_STALE_MS && ledgerLock.reap(lock, observed))
14363
14153
  continue;
14364
- }
14365
- const block = syntax.block.find(([open]) => text.startsWith(open, index));
14366
- if (block) {
14367
- const end = text.indexOf(block[1], index + block[0].length);
14368
- if (end < 0)
14154
+ if (Date.now() >= deadline)
14155
+ return null;
14156
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
14157
+ }
14158
+ try {
14159
+ return fn();
14160
+ } finally {
14161
+ ledgerLock.release(lock, token);
14162
+ }
14163
+ }
14164
+ var sha2562 = (text) => createHash11("sha256").update(text).digest("hex");
14165
+ var chainHash = (prevHash, body) => sha2562(`${prevHash ?? ""}
14166
+ ${body}`);
14167
+ var TAIL_BYTES = 64 * 1024;
14168
+ var lastLine = (fd, size) => {
14169
+ for (let span = Math.min(size, TAIL_BYTES);span > 0; span = Math.min(size, span * 2)) {
14170
+ const buffer = Buffer.alloc(span);
14171
+ fs13.readSync(fd, buffer, 0, span, size - span);
14172
+ const end = buffer.lastIndexOf(10);
14173
+ if (end < 0) {
14174
+ if (span === size)
14369
14175
  return null;
14370
- const comment = text.slice(index, end + block[1].length);
14371
- if (DIRECTIVE.test(comment.slice(block[0].length, -block[1].length)))
14372
- emitVerbatim(comment);
14373
- else
14374
- pendingSpace = true;
14375
- if (comment.includes(`
14376
- `))
14377
- newline();
14378
- index = end + block[1].length;
14379
14176
  continue;
14380
14177
  }
14381
- if (syntax.triple && (char === '"' || char === "'") && text.startsWith(char.repeat(3), index)) {
14382
- if (!scanString(char.repeat(3), !syntax.literal?.includes(char), true))
14383
- return null;
14384
- continue;
14178
+ let stop = end;
14179
+ let text = "";
14180
+ while (stop >= 0) {
14181
+ const start = buffer.lastIndexOf(10, stop - 1);
14182
+ text = buffer.toString("utf8", start + 1, stop).trim();
14183
+ if (text) {
14184
+ if (start >= 0 || span === size)
14185
+ return text;
14186
+ break;
14187
+ }
14188
+ if (start < 0) {
14189
+ if (span === size)
14190
+ return null;
14191
+ break;
14192
+ }
14193
+ stop = start;
14385
14194
  }
14386
- if (syntax.quotes.includes(char)) {
14387
- if (!scanString(char, true, Boolean(syntax.multiline)))
14388
- return null;
14389
- continue;
14195
+ if (span === size)
14196
+ return text || null;
14197
+ }
14198
+ return null;
14199
+ };
14200
+ var tighten = (fd) => {
14201
+ try {
14202
+ if ((fs13.fstatSync(fd).mode & 63) !== 0)
14203
+ fs13.fchmodSync(fd, 384);
14204
+ } catch {}
14205
+ };
14206
+ function appendLine(file, build, options) {
14207
+ const write = () => {
14208
+ const fd = fs13.openSync(file, "a+", 384);
14209
+ try {
14210
+ if (process.platform !== "win32")
14211
+ tighten(fd);
14212
+ const size = fs13.fstatSync(fd).size;
14213
+ let fence = "";
14214
+ if (size > 0) {
14215
+ const last = Buffer.alloc(1);
14216
+ fs13.readSync(fd, last, 0, 1, size - 1);
14217
+ if (last[0] !== 10)
14218
+ fence = `
14219
+ `;
14220
+ }
14221
+ const line = build(lastLine(fd, size));
14222
+ if (!line.ok)
14223
+ return line;
14224
+ const bytes = Buffer.from(fence + line.value);
14225
+ const written = fs13.writeSync(fd, bytes);
14226
+ if (written !== bytes.length)
14227
+ return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
14228
+ return { ok: true, value: undefined };
14229
+ } finally {
14230
+ fs13.closeSync(fd);
14390
14231
  }
14391
- if (syntax.literal?.includes(char)) {
14392
- if (!scanString(char, false, false))
14393
- return null;
14232
+ };
14233
+ try {
14234
+ fs13.mkdirSync(path16.dirname(file), { recursive: true });
14235
+ if (!options.forceLock && !onNetworkFs(path16.dirname(file)))
14236
+ return write();
14237
+ const locked = withLedgerLock(file, write);
14238
+ return locked ?? err("busy", `ledger lock ${file}.lock is held`, "retry; remove the .lock directory if no workit process is running");
14239
+ } catch (error) {
14240
+ return err("unavailable", `cannot append to ${file}: ${error.message}`);
14241
+ }
14242
+ }
14243
+ var sealed = (row, id, at, previous) => {
14244
+ const { prevHash: _prev, rowHash: _row, ...fields } = row;
14245
+ const prevHash = previous === null ? null : sha2562(previous);
14246
+ const body = { v: LEDGER_VERSION, id, at, ...fields, prevHash };
14247
+ const full = { ...body, rowHash: chainHash(prevHash, JSON.stringify(body)) };
14248
+ return { full, line: `${JSON.stringify(full)}
14249
+ ` };
14250
+ };
14251
+ function appendRaw(cwd, row, options) {
14252
+ const file = ledgerPath(cwd);
14253
+ if (!file.ok)
14254
+ return file;
14255
+ const id = newId2();
14256
+ const at = (options.now ?? new Date).toISOString();
14257
+ let full = null;
14258
+ const written = appendLine(file.value, (previous) => {
14259
+ const next = sealed(row, id, at, previous);
14260
+ const bytes = Buffer.byteLength(next.line);
14261
+ if (bytes > MAX_LINE_BYTES)
14262
+ 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>`);
14263
+ full = next.full;
14264
+ return { ok: true, value: next.line };
14265
+ }, options);
14266
+ return written.ok ? { ok: true, value: full } : written;
14267
+ }
14268
+ function observedRowBytes(row) {
14269
+ const sample = sealed({ ...row, observer: "workit_cli" }, newId2(), new Date().toISOString(), "");
14270
+ return Buffer.byteLength(sample.line);
14271
+ }
14272
+ function appendObserved(cwd, row, options = {}) {
14273
+ return appendRaw(cwd, { ...row, observer: "workit_cli" }, options);
14274
+ }
14275
+ function appendHookObserved(cwd, row) {
14276
+ return appendRaw(cwd, { ...row, observer: "host_hook" }, {});
14277
+ }
14278
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14279
+ var str2 = (value) => typeof value === "string" ? value : null;
14280
+ var normalizeActor = (value) => {
14281
+ const actor = isRecord3(value) ? value : {};
14282
+ return {
14283
+ host: str2(actor.host) ?? "unknown",
14284
+ session: str2(actor.session),
14285
+ agentId: str2(actor.agentId)
14286
+ };
14287
+ };
14288
+ function supersedeAllowed(by, target, onBranch = (row) => (by.branch ?? null) === (row.branch ?? null)) {
14289
+ if (by.type !== target.type)
14290
+ return `a ${by.type} row cannot supersede a ${target.type} row`;
14291
+ if (!by.actor.session || by.actor.session !== target.actor.session)
14292
+ return "only the session that wrote a row can supersede it";
14293
+ if (!onBranch(target))
14294
+ return `the row is on another branch (${String(target.branch ?? "none")}); a row supersedes only one on its own branch`;
14295
+ if (by.type === "verdict" && (by.kind ?? "review") !== (target.kind ?? "review"))
14296
+ return `a ${String(by.kind ?? "review")} verdict cannot supersede a ${String(target.kind ?? "review")} verdict; record it with --kind ${String(target.kind ?? "review")}`;
14297
+ if (by.type === "verdict" && by.self === true && target.self !== true)
14298
+ return "a self verdict cannot replace an independent verdict";
14299
+ return null;
14300
+ }
14301
+ function readLedger(cwd, options = {}) {
14302
+ const file = ledgerPath(cwd);
14303
+ if (!file.ok)
14304
+ return file;
14305
+ let text = "";
14306
+ let stat = null;
14307
+ try {
14308
+ stat = fs13.statSync(file.value);
14309
+ text = fs13.readFileSync(file.value, "utf8");
14310
+ } catch (error) {
14311
+ if (error.code === "ENOENT")
14312
+ return {
14313
+ ok: true,
14314
+ value: {
14315
+ path: file.value,
14316
+ rows: [],
14317
+ skipped: 0,
14318
+ integrity: options.integrity ? { chained: 0, legacy: 0, unverified: [] } : null
14319
+ }
14320
+ };
14321
+ return err("unavailable", `cannot read ${file.value}: ${error.message}`);
14322
+ }
14323
+ const rows = [];
14324
+ let skipped = 0;
14325
+ const integrity = { chained: 0, legacy: 0, unverified: [] };
14326
+ const earlier = new Set([null]);
14327
+ let chainStarted = false;
14328
+ const ids = new Set;
14329
+ for (const raw of text.split(`
14330
+ `)) {
14331
+ const trimmed = raw.trim();
14332
+ if (!trimmed)
14394
14333
  continue;
14395
- }
14396
- if (syntax.rawBacktick && char === "`") {
14397
- if (!scanString("`", false, true))
14398
- return null;
14334
+ const linked = (prevHash) => earlier.has(prevHash);
14335
+ if (options.integrity)
14336
+ earlier.add(sha2562(trimmed));
14337
+ let parsed;
14338
+ try {
14339
+ parsed = JSON.parse(trimmed);
14340
+ } catch {
14341
+ skipped += 1;
14399
14342
  continue;
14400
14343
  }
14401
- if (syntax.js && char === "`") {
14402
- emitVerbatim(char);
14403
- frames.push({ kind: "template" });
14404
- index += 1;
14344
+ if (!isRecord3(parsed) || !str2(parsed.id) || !str2(parsed.type) || !str2(parsed.at)) {
14345
+ skipped += 1;
14405
14346
  continue;
14406
14347
  }
14407
- if (syntax.cssUrl && /^url\(\s*[^"')\s]/iu.test(text.slice(index, index + 64)))
14408
- return null;
14409
- if (syntax.js && char === "/" && !regexAllowed() && out.trimEnd().endsWith(")")) {
14410
- const lineEnd = text.indexOf(`
14411
- `, index);
14412
- if (text.slice(index + 1, lineEnd < 0 ? text.length : lineEnd).includes("/"))
14413
- return null;
14414
- }
14415
- if (syntax.js && char === "/" && regexAllowed()) {
14416
- let end = index + 1;
14417
- let inClass = false;
14418
- while (end < text.length && (inClass || text[end] !== "/")) {
14419
- if (text[end] === `
14420
- `)
14421
- return null;
14422
- if (text[end] === "\\")
14423
- end += 1;
14424
- else if (text[end] === "[")
14425
- inClass = true;
14426
- else if (text[end] === "]")
14427
- inClass = false;
14428
- end += 1;
14429
- }
14430
- if (end >= text.length)
14431
- return null;
14432
- emitVerbatim(text.slice(index, end + 1));
14433
- index = end + 1;
14348
+ let rowIntegrity = options.integrity ? "verified" : undefined;
14349
+ let reason = null;
14350
+ if (!options.integrity) {} else if (typeof parsed.rowHash === "string") {
14351
+ chainStarted = true;
14352
+ const { rowHash, ...body } = parsed;
14353
+ const prevHash = str2(parsed.prevHash);
14354
+ if (chainHash(prevHash, JSON.stringify(body)) !== rowHash)
14355
+ reason = "hash_mismatch";
14356
+ else if (!linked(prevHash))
14357
+ reason = "broken_link";
14358
+ } else if (chainStarted)
14359
+ reason = "unsigned";
14360
+ else
14361
+ rowIntegrity = "legacy";
14362
+ if (options.integrity && !reason && ids.has(parsed.id))
14363
+ reason = "duplicate_id";
14364
+ if (options.integrity)
14365
+ ids.add(parsed.id);
14366
+ if (reason)
14367
+ rowIntegrity = "unverified";
14368
+ const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
14369
+ const row = {
14370
+ ...parsed,
14371
+ v: typeof parsed.v === "number" ? parsed.v : LEDGER_VERSION,
14372
+ id: parsed.id,
14373
+ at: parsed.at,
14374
+ type: parsed.type,
14375
+ actor: normalizeActor(parsed.actor),
14376
+ branch: str2(parsed.branch),
14377
+ head: str2(parsed.head),
14378
+ base: str2(parsed.base),
14379
+ baseSha: str2(parsed.baseSha),
14380
+ patchId: str2(parsed.patchId),
14381
+ diffHash: str2(parsed.diffHash),
14382
+ tree: str2(parsed.tree),
14383
+ dirty: typeof parsed.dirty === "boolean" ? parsed.dirty : null,
14384
+ ...pr === undefined ? {} : { pr },
14385
+ ...str2(parsed.supersedes) ? { supersedes: parsed.supersedes } : {},
14386
+ seq: rows.length + 1,
14387
+ superseded: false,
14388
+ supersedeIgnored: false,
14389
+ ...rowIntegrity ? { integrity: rowIntegrity } : {},
14390
+ ...reason ? { integrityReason: reason } : {}
14391
+ };
14392
+ rows.push(row);
14393
+ if (!options.integrity)
14434
14394
  continue;
14435
- }
14436
- if (frames.length > 1 && frame.kind === "code") {
14437
- if (char === "{")
14438
- frame.braces += 1;
14439
- else if (char === "}") {
14440
- if (frame.braces === 0) {
14441
- emitVerbatim(char);
14442
- frames.pop();
14443
- index += 1;
14444
- continue;
14445
- }
14446
- frame.braces -= 1;
14447
- }
14448
- }
14449
- emitCode(char);
14450
- index += 1;
14395
+ if (rowIntegrity === "verified")
14396
+ integrity.chained += 1;
14397
+ else if (rowIntegrity === "legacy")
14398
+ integrity.legacy += 1;
14399
+ else if (reason)
14400
+ integrity.unverified.push({
14401
+ seq: row.seq,
14402
+ id: row.id,
14403
+ type: row.type,
14404
+ branch: row.branch,
14405
+ reason
14406
+ });
14451
14407
  }
14452
- if (frames.length !== 1)
14453
- return null;
14454
- newline();
14455
- return out;
14456
- }
14457
- function unbalanced(body) {
14458
- let depth = 0;
14459
- for (const part of body.replaceAll("{{", "").replaceAll("}}", "")) {
14460
- if (part === "{")
14461
- depth += 1;
14462
- else if (part === "}")
14463
- depth = Math.max(0, depth - 1);
14408
+ const scopes = new Map;
14409
+ const scopeOf = (branch) => {
14410
+ let scope = scopes.get(branch);
14411
+ if (!scope)
14412
+ scopes.set(branch, scope = branchScope(cwd, branch));
14413
+ return scope;
14414
+ };
14415
+ const byId = new Map;
14416
+ for (const row of rows) {
14417
+ const target = row.supersedes ? byId.get(row.supersedes) : undefined;
14418
+ if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
14419
+ target.superseded = true;
14420
+ else if (row.supersedes)
14421
+ row.supersedeIgnored = true;
14422
+ if (!byId.has(row.id))
14423
+ byId.set(row.id, row);
14464
14424
  }
14465
- return depth > 0;
14466
- }
14467
- function hasOpaqueLineBreak(text) {
14468
- return /\r(?!\n)|[\u2028\u2029]/u.test(text);
14425
+ if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
14426
+ tamperCache.set(file.value, {
14427
+ size: stat.size,
14428
+ stamp: fileStamp(stat),
14429
+ breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
14430
+ });
14431
+ return {
14432
+ ok: true,
14433
+ value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
14434
+ };
14469
14435
  }
14470
- var decoder = new TextDecoder("utf-8", { fatal: true });
14471
- function textOf(bytes) {
14472
- if (bytes.includes(0))
14473
- return null;
14436
+ var TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
14437
+ var tamperCache = new Map;
14438
+ var fileStamp = (stat) => `${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`;
14439
+ function ledgerTampering(cwd) {
14440
+ const file = ledgerPath(cwd);
14441
+ if (!file.ok)
14442
+ return [];
14474
14443
  try {
14475
- return decoder.decode(bytes);
14444
+ const stat = fs13.statSync(file.value);
14445
+ const cached = tamperCache.get(file.value);
14446
+ if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
14447
+ return cached.breaks;
14476
14448
  } catch {
14477
- return null;
14449
+ return [];
14478
14450
  }
14451
+ const read = readLedger(cwd, { integrity: true });
14452
+ return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
14479
14453
  }
14480
- function cosmeticChange(file, before, after) {
14481
- const left = textOf(before);
14482
- const right = textOf(after);
14483
- if (left === null || right === null)
14484
- return false;
14485
- if (hasOpaqueLineBreak(left) || hasOpaqueLineBreak(right))
14486
- return false;
14487
- const a = substanceOf(file, left);
14488
- return a !== null && a === substanceOf(file, right);
14454
+ function tamperedUnblock(cwd) {
14455
+ const file = ledgerPath(cwd);
14456
+ const where = file.ok ? file.value : "the ledger file";
14457
+ 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`;
14489
14458
  }
14490
-
14491
- // packages/workit-core/src/core/policy/lane.ts
14492
- var LANE_THRESHOLDS = {
14493
- selfMaxLines: 100,
14494
- selfMaxFiles: 5,
14495
- fullMinLines: 500
14496
- };
14497
- var RISK_TOKENS = new Set([
14498
- "auth",
14499
- "authn",
14500
- "authz",
14501
- "oauth",
14502
- "security",
14503
- "permission",
14504
- "permissions",
14505
- "secret",
14506
- "secrets",
14507
- "crypto",
14508
- "gate",
14509
- "gates",
14510
- "policy",
14511
- "policies",
14512
- "hook",
14513
- "hooks",
14514
- "husky",
14515
- "lefthook",
14516
- "release",
14517
- "releases",
14518
- "migration",
14519
- "migrations",
14520
- "migrate",
14521
- "workflows"
14522
- ]);
14523
- var RISK_FILES = new Set([
14524
- ".gitlab-ci.yml",
14525
- "Jenkinsfile",
14526
- "azure-pipelines.yml",
14527
- ".pre-commit-config.yaml",
14528
- ".workit/risk-paths"
14529
- ]);
14530
- var RISK_NAMES = new Set([".gitattributes"]);
14531
- var RISK_PATHS_FILE = ".workit/risk-paths";
14532
- var LOCKFILES = new Set([
14533
- "bun.lock",
14534
- "bun.lockb",
14535
- "package-lock.json",
14536
- "npm-shrinkwrap.json",
14537
- "yarn.lock",
14538
- "pnpm-lock.yaml",
14539
- "Cargo.lock",
14540
- "go.sum",
14541
- "poetry.lock",
14542
- "uv.lock",
14543
- "Pipfile.lock",
14544
- "Gemfile.lock",
14545
- "composer.lock"
14546
- ]);
14547
- var DOC_EXT = /\.(?:md|mdx|markdown|rst|adoc|txt)$/iu;
14548
- var DOC_NAMES = /^(?:license|licence|notice|authors|changelog|changes|contributors)$/iu;
14549
- var INSTRUCTIONS = /(?:^|\/)(?:AGENTS|CLAUDE|GEMINI|SKILL)\.md$|(?:^|\/)(?:skills|agents|commands|rules|prompts)\/[^/]+/iu;
14550
- var TEST_PATH = /(?:^|\/)(?:test|tests|__tests__|spec|specs|fixtures)\/|\.(?:test|spec)\.[^/]+$/iu;
14551
- var basename = (file) => file.slice(file.lastIndexOf("/") + 1);
14552
- var isDoc = (file) => !INSTRUCTIONS.test(file) && (DOC_EXT.test(file) || DOC_NAMES.test(basename(file)));
14553
- function kindOf(file) {
14554
- if (file.binary)
14555
- return "behavior";
14556
- if (file.generated)
14557
- return "generated";
14558
- if (LOCKFILES.has(basename(file.path)))
14559
- return "lockfile";
14560
- if (isDoc(file.path))
14561
- return "doc";
14562
- if (file.cosmetic)
14563
- return "cosmetic";
14564
- return TEST_PATH.test(file.path) ? "test" : "behavior";
14459
+ 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)}`;
14460
+ function verdictBasis(verdict, current) {
14461
+ if (verdict.head && verdict.head === current.head)
14462
+ return "fresh";
14463
+ if (verdict.patchId && verdict.patchId === current.patchId && verdict.diffHash && verdict.diffHash === current.diffHash)
14464
+ return "carried";
14465
+ return "stale";
14565
14466
  }
14566
- var globCache = new Map;
14567
- function globMatch(pattern, file) {
14568
- let regex = globCache.get(pattern);
14569
- if (!regex) {
14570
- let source = "";
14571
- for (let index = 0;index < pattern.length; index += 1) {
14572
- const char = pattern[index];
14573
- if (char === "*" && pattern[index + 1] === "*") {
14574
- source += pattern[index + 2] === "/" ? "(?:[^/]*/)*" : ".*";
14575
- index += pattern[index + 2] === "/" ? 2 : 1;
14576
- } else if (char === "*")
14577
- source += "[^/]*";
14578
- else if (char === "?")
14579
- source += "[^/]";
14580
- else
14581
- source += char.replace(/[.+^${}()|[\]\\]/gu, "\\$&");
14467
+ var isLiveVerdict = (row, retired) => row.type === "verdict" && typeof row.result === "string" && !retired.has(row.id);
14468
+ function trailerSessions(cwd, base, head) {
14469
+ if (!base || !head || !safeRef2(base) || !safeRef2(head))
14470
+ return [];
14471
+ const out = git4(cwd, [
14472
+ "log",
14473
+ "--format=%(trailers:key=Workit-Session,valueonly,separator=%x00)",
14474
+ `${base}..${head}`
14475
+ ]);
14476
+ return (out ?? "").split(/[\0\n]/u).map((value) => value.trim()).filter(Boolean);
14477
+ }
14478
+ var RENAMED = /^branch: renamed refs\/heads\/(.+) to refs\/heads\/(.+)$/iu;
14479
+ function branchScope(cwd, branch) {
14480
+ const renamedAt = new Map;
14481
+ let since = Number.NEGATIVE_INFINITY;
14482
+ if (safeRef2(branch)) {
14483
+ const log = git4(cwd, [
14484
+ "log",
14485
+ "-g",
14486
+ "--date=unix",
14487
+ "--format=%gd%x09%gs",
14488
+ `refs/heads/${branch}`,
14489
+ "--"
14490
+ ]);
14491
+ for (const line of (log ?? "").split(`
14492
+ `)) {
14493
+ const [selector = "", subject = ""] = line.split("\t");
14494
+ const seconds = Number(/@\{(\d+)\}$/u.exec(selector)?.[1]);
14495
+ if (!Number.isFinite(seconds))
14496
+ continue;
14497
+ since = seconds * 1000;
14498
+ const match = RENAMED.exec(subject);
14499
+ if (!match || match[1] === branch)
14500
+ continue;
14501
+ const until = (seconds + 1) * 1000;
14502
+ renamedAt.set(match[1], Math.max(renamedAt.get(match[1]) ?? 0, until));
14582
14503
  }
14583
- regex = new RegExp(`^${source}$`, "u");
14584
- globCache.set(pattern, regex);
14585
14504
  }
14586
- return regex.test(file) || file.startsWith(`${pattern.replace(/\/$/u, "")}/`);
14505
+ return (row) => {
14506
+ if (row.branch === branch)
14507
+ return true;
14508
+ const until = row.branch === null ? undefined : renamedAt.get(row.branch);
14509
+ if (until === undefined)
14510
+ return false;
14511
+ const at = Date.parse(row.at);
14512
+ return at >= since && at <= until;
14513
+ };
14587
14514
  }
14588
- function isRiskPath(file, extra = []) {
14589
- if (extra.some((pattern) => globMatch(pattern, file)))
14590
- return true;
14591
- if (RISK_FILES.has(file) || RISK_NAMES.has(basename(file)))
14515
+ var renameAwareBranch = (scopeOf, branch) => {
14516
+ return (target) => {
14517
+ const own = typeof branch === "string" ? branch : null;
14518
+ const theirs = typeof target.branch === "string" ? target.branch : null;
14519
+ if (own === theirs)
14520
+ return true;
14521
+ if (own === null || theirs === null || typeof target.at !== "string")
14522
+ return false;
14523
+ return scopeOf(own)({ branch: theirs, at: target.at });
14524
+ };
14525
+ };
14526
+ function authorSessions(cwd, rows, branch, range = { base: null, head: null }, scope = branchScope(cwd, branch)) {
14527
+ const sessions = new Set;
14528
+ for (const row of rows) {
14529
+ if (!scope(row) || row.type !== "commit.recorded" && row.type !== "task.opened")
14530
+ continue;
14531
+ const session = str2(row.session) ?? row.actor.session;
14532
+ if (session)
14533
+ sessions.add(session);
14534
+ }
14535
+ for (const session of trailerSessions(cwd, range.base, range.head))
14536
+ sessions.add(session);
14537
+ return sessions;
14538
+ }
14539
+ var derivesFrom = (derived, bare) => !bare.includes(":") && derived.endsWith(`:${bare}`);
14540
+ var authoredBy = (authors, session) => {
14541
+ if (authors.has(session))
14592
14542
  return true;
14593
- if (TEST_PATH.test(file) || isDoc(file))
14594
- return false;
14595
- 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)));
14543
+ for (const author of authors)
14544
+ if (derivesFrom(author, session) || derivesFrom(session, author))
14545
+ return true;
14546
+ return false;
14547
+ };
14548
+ var TRUST_REASONS = new Set([
14549
+ "unsigned",
14550
+ "hash_mismatch",
14551
+ "broken_link",
14552
+ "duplicate_id",
14553
+ "unissued_session",
14554
+ "unbound_session"
14555
+ ]);
14556
+ var SHELL_VOUCHED_HOSTS = new Set([
14557
+ "cli",
14558
+ "opencode",
14559
+ "pi",
14560
+ "codex_cli",
14561
+ "codex_desktop"
14562
+ ]);
14563
+ function sessionTrust(cwd, row) {
14564
+ const session = row.actor.session;
14565
+ if (!session || row.self === true)
14566
+ return null;
14567
+ const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
14568
+ const host = row.actor.host;
14569
+ const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
14570
+ if (!issuedOnly)
14571
+ return null;
14572
+ const binding = issuedBinding(cwd, session);
14573
+ if (binding === null)
14574
+ return "unissued_session";
14575
+ return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
14596
14576
  }
14597
- var parseRiskPaths = (text) => text.split(`
14598
- `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.replace(/^\//u, ""));
14599
- function computeLane(input) {
14600
- const riskFiles = input.files.filter((file) => isRiskPath(file.path, input.riskPaths)).map((file) => file.path);
14601
- const risky = new Set(riskFiles);
14602
- const kinds = input.files.map((file) => ({
14603
- file,
14604
- kind: risky.has(file.path) ? "behavior" : kindOf(file)
14605
- }));
14606
- const behavior = kinds.filter((entry) => entry.kind === "behavior" || entry.kind === "test" || entry.kind === "lockfile");
14607
- const lines = behavior.filter((entry) => entry.kind !== "lockfile").reduce((sum, entry) => sum + entry.file.added + entry.file.deleted, 0);
14608
- const opaque = behavior.filter((entry) => entry.kind === "lockfile" || entry.file.binary);
14609
- const result = (lane, reasons) => ({
14610
- lane,
14611
- reasons,
14612
- lines,
14613
- files: behavior.length,
14614
- riskFiles
14577
+ function trustReasons(cwd, row) {
14578
+ if (row.integrity === "unverified")
14579
+ return [row.integrityReason ?? "unsigned"];
14580
+ if (row.integrity !== "verified")
14581
+ return ["unsigned"];
14582
+ const session = sessionTrust(cwd, row);
14583
+ return session ? [session] : [];
14584
+ }
14585
+ function withIntegrity(cwd, rows) {
14586
+ if (rows.every((row) => row.integrity !== undefined))
14587
+ return rows;
14588
+ const read = readLedger(cwd, { integrity: true });
14589
+ const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
14590
+ return rows.map((row) => {
14591
+ if (row.integrity !== undefined)
14592
+ return row;
14593
+ const fresh = bySeq.get(row.seq);
14594
+ return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
14615
14595
  });
14616
- const { selfMaxLines, selfMaxFiles, fullMinLines } = LANE_THRESHOLDS;
14617
- if (input.riskTier === "high")
14618
- return result("full", ["the task judgment records high risk"]);
14619
- if (riskFiles.length)
14620
- return result("full", [
14621
- `touches a risk path: ${riskFiles.slice(0, 3).join(", ")}${riskFiles.length > 3 ? ", …" : ""}`
14622
- ]);
14623
- if (!behavior.length)
14624
- return result("none", [
14625
- input.files.length ? "only docs, comments, formatting or generated copies changed" : "no changes"
14626
- ]);
14627
- if (lines >= fullMinLines)
14628
- return result("full", [`large diff: ${lines} lines (≥ ${fullMinLines})`]);
14629
- if (lines > selfMaxLines)
14630
- return result("focused", [`${lines} lines changed (> ${selfMaxLines})`]);
14631
- if (behavior.length > selfMaxFiles)
14632
- return result("focused", [`${behavior.length} behavior files changed (> ${selfMaxFiles})`]);
14633
- if (opaque.length)
14634
- return result("focused", [
14635
- `a lockfile or binary changed: ${opaque.slice(0, 3).map((entry) => entry.file.path).join(", ")}${opaque.length > 3 ? ", …" : ""}`
14636
- ]);
14637
- return result("self", [
14638
- `small change: ${lines} line${lines === 1 ? "" : "s"} in ${behavior.length} file${behavior.length === 1 ? "" : "s"}, no risk path`
14639
- ]);
14640
14596
  }
14641
- function untrackedStat(cwd, file) {
14642
- try {
14643
- const bytes = readFileSync4(path17.join(cwd, file));
14644
- if (bytes.includes(0))
14645
- return { path: file, added: 0, deleted: 0, binary: true };
14646
- const text = bytes.toString("utf8");
14647
- if (hasOpaqueLineBreak(text))
14648
- return { path: file, added: 0, deleted: 0, binary: true };
14649
- const lines = text.split(`
14650
- `).length - (text.endsWith(`
14651
- `) ? 1 : 0);
14652
- return { path: file, added: lines, deleted: 0 };
14653
- } catch {
14654
- return { path: file, added: 0, deleted: 0, binary: true };
14597
+ var untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason));
14598
+ var independenceReasons = (row, authors) => {
14599
+ const reasons = [];
14600
+ if (!row.actor.session)
14601
+ reasons.push("no_session");
14602
+ else if (authoredBy(authors, row.actor.session))
14603
+ reasons.push("author_session");
14604
+ if (row.self === true && !reasons.includes("no_session"))
14605
+ reasons.push("self");
14606
+ return reasons;
14607
+ };
14608
+ function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
14609
+ let effective = null;
14610
+ for (const row of rows) {
14611
+ if (!effective) {
14612
+ effective = row;
14613
+ continue;
14614
+ }
14615
+ if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
14616
+ continue;
14617
+ const rowIndependent = reasonsOf(row).length === 0;
14618
+ const effIndependent = reasonsOf(effective).length === 0;
14619
+ if (effIndependent && !rowIndependent && isCurrent(effective))
14620
+ continue;
14621
+ if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
14622
+ continue;
14623
+ effective = row;
14655
14624
  }
14625
+ return effective;
14656
14626
  }
14657
- var REGULAR = new Set(["100644", "100755"]);
14658
- var ABSENT = /^0+$/u;
14659
- function cosmeticFiles(cwd, fork, head) {
14660
- const raw = diffRaw(cwd, fork, head);
14661
- if (!raw)
14662
- return null;
14663
- const candidates = raw.filter((entry) => REGULAR.has(entry.oldMode) && entry.oldMode === entry.newMode && !ABSENT.test(entry.oldBlob) && (head === null || !ABSENT.test(entry.newBlob)));
14664
- const blobs = readBlobs(cwd, candidates.flatMap((entry) => [entry.oldBlob, entry.newBlob]));
14665
- if (!blobs)
14666
- return null;
14667
- const out = new Set;
14668
- const opaque = new Set;
14669
- for (const entry of candidates) {
14670
- const before = blobs.get(entry.oldBlob);
14671
- let after = blobs.get(entry.newBlob);
14672
- if (!after && head === null && ABSENT.test(entry.newBlob))
14673
- try {
14674
- after = readFileSync4(path17.join(cwd, entry.path));
14675
- } catch {
14676
- after = undefined;
14627
+ var restackExact = new WeakMap;
14628
+ function restackCarries(exactIn, rows, branch, from, to) {
14629
+ if (!from || !to)
14630
+ return false;
14631
+ const exact = (row, fromHead, head) => {
14632
+ if (exactIn === null)
14633
+ return true;
14634
+ const cwd = exactIn;
14635
+ let known = restackExact.get(row);
14636
+ if (known === undefined) {
14637
+ known = typeof row.fromBase === "string" && typeof row.toBase === "string" && sameOwnChange(cwd, {
14638
+ from: fromHead,
14639
+ to: head,
14640
+ fromBase: row.fromBase,
14641
+ toBase: row.toBase
14642
+ });
14643
+ restackExact.set(row, known);
14644
+ }
14645
+ return known;
14646
+ };
14647
+ const edges = new Map;
14648
+ for (const row of rows)
14649
+ 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))
14650
+ edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
14651
+ const seen = new Set([from]);
14652
+ const queue = [from];
14653
+ while (queue.length) {
14654
+ const next = queue.shift();
14655
+ if (next === to)
14656
+ return true;
14657
+ for (const target of edges.get(next) ?? [])
14658
+ if (!seen.has(target)) {
14659
+ seen.add(target);
14660
+ queue.push(target);
14677
14661
  }
14678
- if (before && after && cosmeticChange(entry.path, before, after))
14679
- out.add(entry.path);
14680
- else if ([before, after].some((bytes) => bytes && hasOpaqueLineBreak(bytes.toString("utf8"))))
14681
- opaque.add(entry.path);
14682
14662
  }
14683
- return { cosmetic: out, opaque };
14663
+ return false;
14684
14664
  }
14685
- function generatedAt(cwd, fork, paths) {
14686
- const files = treeFilesNamed(cwd, fork, ".gitattributes");
14687
- const blobs = files?.size ? readBlobs(cwd, [...files.values()]) : null;
14688
- if (!files || !blobs)
14689
- return new Set;
14690
- const texts = new Map;
14691
- for (const [file, id] of files) {
14692
- const bytes = blobs.get(id);
14693
- if (bytes)
14694
- texts.set(file, bytes.toString("utf8"));
14665
+ function checkVerdicts(cwd, branch, ledgerRows) {
14666
+ const rows = withIntegrity(cwd, ledgerRows);
14667
+ const head = branchHead(cwd, branch);
14668
+ const fallbackBase = defaultBase(cwd);
14669
+ const scope = branchScope(cwd, branch);
14670
+ const keyCache = new Map;
14671
+ const keysAt = (base, at) => {
14672
+ if (!base || !at)
14673
+ return { patchId: null, diffHash: null };
14674
+ const cacheKey = `${base}\x00${at}`;
14675
+ let key = keyCache.get(cacheKey);
14676
+ if (!key) {
14677
+ key = { patchId: patchId(cwd, base, at), diffHash: diffHash(cwd, base, at) };
14678
+ keyCache.set(cacheKey, key);
14679
+ }
14680
+ return key;
14681
+ };
14682
+ const keyFor = (base) => keysAt(base, head);
14683
+ const driftCache = new Map;
14684
+ const driftOf = (row) => {
14685
+ if (driftCache.has(row))
14686
+ return driftCache.get(row) ?? null;
14687
+ let drift = null;
14688
+ const toBase = row.base ?? fallbackBase;
14689
+ const toBaseSha = toBase ? resolveCommit(cwd, toBase) : null;
14690
+ const fromBase = row.baseSha ?? toBaseSha;
14691
+ if (row.head && head && row.head !== head && toBaseSha && fromBase && !untrusted(reasonsOf(row)))
14692
+ drift = verdictDrift(cwd, {
14693
+ from: row.head,
14694
+ to: head,
14695
+ fromBase,
14696
+ toBase: toBaseSha,
14697
+ kind: typeof row.kind === "string" ? row.kind : "review"
14698
+ });
14699
+ driftCache.set(row, drift);
14700
+ return drift;
14701
+ };
14702
+ const exactCache = new Map;
14703
+ const exactCarry = (row) => {
14704
+ if (FAILING.has(String(row.result)))
14705
+ return true;
14706
+ let exact = exactCache.get(row);
14707
+ if (exact === undefined) {
14708
+ const toBase = row.base ?? fallbackBase;
14709
+ const toBaseSha = toBase ? resolveCommit(cwd, toBase) : null;
14710
+ const fromBase = row.baseSha ?? toBaseSha;
14711
+ exact = Boolean(row.head && head && toBaseSha && fromBase && sameOwnChange(cwd, { from: row.head, to: head, fromBase, toBase: toBaseSha }));
14712
+ exactCache.set(row, exact);
14713
+ }
14714
+ return exact;
14715
+ };
14716
+ const keyBasis = (row) => {
14717
+ const basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
14718
+ return basis === "carried" && !exactCarry(row) ? "stale" : basis;
14719
+ };
14720
+ const restacked = (row) => restackCarries(FAILING.has(String(row.result)) ? null : cwd, rows, branch, row.head, head);
14721
+ const carryOf = (row) => {
14722
+ if (keyBasis(row) !== "stale")
14723
+ return null;
14724
+ if (restacked(row))
14725
+ return null;
14726
+ const drift = driftOf(row);
14727
+ return drift?.carries ? { rule: drift.rule, since: drift.since, files: drift.files } : null;
14728
+ };
14729
+ const basisOf = (row) => {
14730
+ const basis = keyBasis(row);
14731
+ if (basis !== "stale")
14732
+ return basis;
14733
+ return restacked(row) || carryOf(row) ? "carried" : basis;
14734
+ };
14735
+ const sameCode = (failed, row) => {
14736
+ if (failed.head && failed.head === row.head)
14737
+ return true;
14738
+ const base = failed.base ?? fallbackBase;
14739
+ const stored = (row.base ?? fallbackBase) === base && row.baseSha !== null && row.baseSha === failed.baseSha;
14740
+ const keys = stored ? { patchId: row.patchId, diffHash: row.diffHash } : keysAt(base, row.head);
14741
+ return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(null, rows, branch, failed.head, row.head);
14742
+ };
14743
+ const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
14744
+ const failureAuthorCache = new Map;
14745
+ const failureAuthors = (row) => {
14746
+ let set = failureAuthorCache.get(row.seq);
14747
+ if (!set) {
14748
+ set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
14749
+ failureAuthorCache.set(row.seq, set);
14750
+ }
14751
+ return set;
14752
+ };
14753
+ const reasonCache = new Map;
14754
+ const reasonsOf = (row) => {
14755
+ let reasons = reasonCache.get(row);
14756
+ if (!reasons) {
14757
+ const failing = FAILING.has(String(row.result));
14758
+ reasons = [
14759
+ ...trustReasons(cwd, row),
14760
+ ...independenceReasons(row, failing ? failureAuthors(row) : authors)
14761
+ ];
14762
+ reasonCache.set(row, reasons);
14763
+ }
14764
+ return reasons;
14765
+ };
14766
+ const retired = new Set;
14767
+ for (const row of rows)
14768
+ if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
14769
+ retired.add(row.supersedes);
14770
+ const byKind = new Map;
14771
+ for (const row of rows)
14772
+ if (isLiveVerdict(row, retired) && scope(row)) {
14773
+ const kind = typeof row.kind === "string" ? row.kind : "review";
14774
+ byKind.set(kind, [...byKind.get(kind) ?? [], row]);
14775
+ }
14776
+ const verdicts = [];
14777
+ const warnings = [];
14778
+ for (const [kind, list] of byKind) {
14779
+ const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
14780
+ if (!row)
14781
+ continue;
14782
+ const basis = basisOf(row);
14783
+ const current = basis !== "stale";
14784
+ const independence = reasonsOf(row);
14785
+ const reasons = [
14786
+ ...current ? [] : ["stale"],
14787
+ ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
14788
+ ...independence
14789
+ ];
14790
+ verdicts.push({
14791
+ kind,
14792
+ basis,
14793
+ current,
14794
+ independent: independence.length === 0,
14795
+ accepted: reasons.length === 0,
14796
+ reasons,
14797
+ carry: basis === "carried" ? carryOf(row) : null,
14798
+ verdict: row
14799
+ });
14800
+ if (reasons.length === 0) {
14801
+ for (const later of list)
14802
+ if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
14803
+ 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`);
14804
+ }
14695
14805
  }
14696
- return generatedByAttributes(texts, paths);
14697
- }
14698
- var forkOf = (cwd, base, head) => mergeBase(cwd, base, head ?? "HEAD");
14699
- function laneFiles(cwd, fork, head) {
14700
- const stats = diffNumstat(cwd, fork, head);
14701
- const cosmetic = stats ? cosmeticFiles(cwd, fork, head) : null;
14702
- if (!stats || !cosmetic)
14703
- return null;
14704
- const files = [
14705
- ...stats.map((entry) => ({
14706
- ...entry,
14707
- ...cosmetic.opaque.has(entry.path) ? { binary: true } : {},
14708
- cosmetic: !entry.binary && cosmetic.cosmetic.has(entry.path)
14709
- })),
14710
- ...head === null ? untrackedFiles(cwd).map((file) => untrackedStat(cwd, file)) : []
14711
- ];
14712
- const generated = generatedAt(cwd, fork, files.map((entry) => entry.path));
14713
- return files.map((entry) => ({
14714
- ...entry,
14715
- generated: generated.has(entry.path)
14716
- }));
14717
- }
14718
- function branchLane(cwd, options) {
14719
- const candidates = options.base ? [`origin/${options.base}`, options.base] : [];
14720
- const base = candidates.find((ref) => resolveRef(cwd, ref) !== null) ?? defaultBase(cwd) ?? null;
14721
- const head = options.workingTree ? null : options.head;
14722
- const fork = base && (options.workingTree || options.head) ? forkOf(cwd, base, head) : null;
14723
- const files = fork ? laneFiles(cwd, fork, head) : null;
14724
- const riskTier = options.judgment?.riskTier ?? null;
14725
- if (!fork || !files) {
14726
- const high = riskTier === "high";
14727
- return {
14728
- lane: high ? "full" : "focused",
14729
- reasons: [
14730
- `the diff of ${options.head?.slice(0, 12) ?? "(no head)"} against ${base ?? "(no base)"} cannot be read`,
14731
- ...high ? ["the task judgment records high risk"] : []
14732
- ],
14733
- lines: 0,
14734
- files: 0,
14735
- riskFiles: [],
14736
- base,
14737
- head: options.head
14738
- };
14806
+ for (const row of rows)
14807
+ if (row.integrity === "unverified" && scope(row))
14808
+ 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)`);
14809
+ const tampered = ledgerTampering(cwd);
14810
+ if (tampered.length) {
14811
+ warnings.push(tamperedWarning(cwd, tampered));
14812
+ for (const entry of verdicts) {
14813
+ entry.accepted = false;
14814
+ entry.reasons = [...entry.reasons, "ledger_tampered"];
14815
+ }
14739
14816
  }
14740
- const repoRisk = parseRiskPaths(fileAt(cwd, fork, RISK_PATHS_FILE) ?? "");
14817
+ verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
14818
+ const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
14819
+ const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
14820
+ const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
14821
+ const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
14822
+ accepted: false,
14823
+ verdict: failing?.verdict ?? null,
14824
+ reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
14825
+ };
14826
+ 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))));
14827
+ const selfEntry = latestSelf && STRONG_RESULTS.has(String(latestSelf.verdict.result)) ? latestSelf : undefined;
14828
+ const carryEntry = verdicts.find((entry) => entry.verdict === accepted.verdict && entry.accepted) ?? newestCurrent;
14829
+ const lastPass = accepted.accepted || failing || tampered.length ? undefined : verdicts.findLast((entry) => !entry.current && !untrusted(entry.reasons) && STRONG_RESULTS.has(String(entry.verdict.result)));
14830
+ const drift = lastPass ? driftOf(lastPass.verdict) : null;
14831
+ const reverify = drift && !drift.carries && drift.focused && drift.files ? { scope: "focused", since: drift.since, files: drift.files } : null;
14741
14832
  return {
14742
- ...computeLane({
14743
- files,
14744
- riskTier,
14745
- riskPaths: [...repoRisk, ...options.riskPaths ?? []]
14746
- }),
14747
- base,
14748
- head: options.head
14833
+ branch,
14834
+ head,
14835
+ review: accepted.accepted ? "verified" : selfEntry ? "self-reviewed" : "unreviewed",
14836
+ selfVerdict: accepted.accepted ? null : selfEntry?.verdict ?? null,
14837
+ base: newestCurrent?.verdict.base ?? fallbackBase,
14838
+ patchId: keyFor(newestCurrent?.verdict.base ?? fallbackBase).patchId,
14839
+ current: { basis: newestCurrent?.basis ?? "none", verdict: newestCurrent?.verdict ?? null },
14840
+ carry: carryEntry?.carry ?? null,
14841
+ reverify,
14842
+ accepted,
14843
+ verdicts,
14844
+ authors: [...authors].toSorted(),
14845
+ warnings
14749
14846
  };
14750
14847
  }
14751
- var laneNeed = (lane) => lane === "none" ? "none" : lane === "self" ? "self" : "non-author";
14848
+ var fromLead = (row, leads) => leads === null || row.actor.session !== null && leads.includes(row.actor.session);
14849
+ function activeStanding(rows, fanout, leads = null) {
14850
+ const active = [];
14851
+ for (const row of rows) {
14852
+ if (row.fanout !== fanout || !fromLead(row, leads))
14853
+ continue;
14854
+ if (row.type === "standing" && typeof row.what === "string" && row.what.trim()) {
14855
+ if (!row.superseded)
14856
+ active.push(row);
14857
+ } else if (row.type === "standing.cleared") {
14858
+ const target = str2(row.target);
14859
+ if (target === null)
14860
+ active.length = 0;
14861
+ else {
14862
+ const index = active.findIndex((order) => order.id === target);
14863
+ if (index >= 0)
14864
+ active.splice(index, 1);
14865
+ }
14866
+ }
14867
+ }
14868
+ return active;
14869
+ }
14870
+ var OBSERVED_ONLY = new Set([
14871
+ "check",
14872
+ "merge.unverified",
14873
+ "policy.judged",
14874
+ ...PR_ROW_TYPES
14875
+ ]);
14752
14876
 
14753
14877
  // packages/workit-core/src/core/policy/derive.ts
14754
14878
  var RULES = {
@@ -14903,7 +15027,7 @@ var productDecision = (task, ledger) => {
14903
15027
  };
14904
15028
  };
14905
15029
  var planRecorded = (task, root) => {
14906
- const cited = (latestJudgment(task)?.refs ?? []).find((ref) => ref.kind === "external" || ref.kind === "file" && existsSync5(path18.resolve(root, ref.path)));
15030
+ const cited = (latestJudgment(task)?.refs ?? []).find((ref) => ref.kind === "external" || ref.kind === "file" && existsSync5(path17.resolve(root, ref.path)));
14907
15031
  if (cited)
14908
15032
  return {
14909
15033
  met: true,
@@ -14981,14 +15105,14 @@ function writeBlockers(task, root) {
14981
15105
 
14982
15106
  // packages/workit-core/src/core/task-evaluation.ts
14983
15107
  var digestBytes = (value) => createHash12("sha256").update(value).digest("hex");
14984
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path19.sep}`);
14985
- var relative = (root, candidate) => path19.relative(root, candidate).split(path19.sep).join("/") || ".";
15108
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path18.sep}`);
15109
+ var relative = (root, candidate) => path18.relative(root, candidate).split(path18.sep).join("/") || ".";
14986
15110
  var canonicalPath = (value) => {
14987
15111
  if (typeof value !== "string" || !value)
14988
15112
  return null;
14989
15113
  if (value.includes("\\") || value.split("/").includes(".."))
14990
15114
  return null;
14991
- const normalized = path19.posix.normalize(value);
15115
+ const normalized = path18.posix.normalize(value);
14992
15116
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
14993
15117
  return null;
14994
15118
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -15024,7 +15148,7 @@ var compareCodeUnits2 = (left, right) => {
15024
15148
  return left.length - right.length;
15025
15149
  };
15026
15150
  var gitPaths = (root) => {
15027
- const result = spawnSync11("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
15151
+ const result = spawnSync10("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
15028
15152
  cwd: root,
15029
15153
  encoding: "buffer"
15030
15154
  });
@@ -15036,21 +15160,21 @@ var gitPaths = (root) => {
15036
15160
  git: !/not a git repository/i.test(stderr)
15037
15161
  };
15038
15162
  }
15039
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path19.sep).join("/")) : [];
15040
- const stagedDeleted = spawnSync11("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
15163
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path18.sep).join("/")) : [];
15164
+ const stagedDeleted = spawnSync10("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
15041
15165
  if (stagedDeleted.status !== 0) {
15042
15166
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
15043
15167
  if (!/not a git repository/i.test(stderr))
15044
15168
  return { paths, uncertain: true, git: true };
15045
15169
  } else if (stagedDeleted.stdout) {
15046
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path19.sep).join("/")));
15170
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path18.sep).join("/")));
15047
15171
  }
15048
15172
  return { paths: [...new Set(paths)], uncertain: false, git: true };
15049
15173
  };
15050
15174
  var walk = (root, directory, output) => {
15051
15175
  let names;
15052
15176
  try {
15053
- names = fs15.readdirSync(directory);
15177
+ names = fs14.readdirSync(directory);
15054
15178
  } catch {
15055
15179
  return true;
15056
15180
  }
@@ -15058,10 +15182,10 @@ var walk = (root, directory, output) => {
15058
15182
  for (const name of names) {
15059
15183
  if (name === ".git" || name === ".workit")
15060
15184
  continue;
15061
- const target = path19.join(directory, name);
15185
+ const target = path18.join(directory, name);
15062
15186
  let stat;
15063
15187
  try {
15064
- stat = fs15.lstatSync(target);
15188
+ stat = fs14.lstatSync(target);
15065
15189
  } catch {
15066
15190
  uncertain = true;
15067
15191
  continue;
@@ -15076,14 +15200,14 @@ var walk = (root, directory, output) => {
15076
15200
  var scopeRoots = (root, scope) => {
15077
15201
  const roots = [];
15078
15202
  for (const item of scope.paths.length ? scope.paths : ["."]) {
15079
- const target = path19.resolve(root, item);
15203
+ const target = path18.resolve(root, item);
15080
15204
  if (!inside(root, target))
15081
15205
  return failure2("invalid_input", `candidate scope escapes checkout: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} is outside the checkout` }] });
15082
15206
  let ancestor = target;
15083
- while (!fs15.existsSync(ancestor) && ancestor !== root)
15084
- ancestor = path19.dirname(ancestor);
15207
+ while (!fs14.existsSync(ancestor) && ancestor !== root)
15208
+ ancestor = path18.dirname(ancestor);
15085
15209
  try {
15086
- if (!inside(root, fs15.realpathSync(ancestor)))
15210
+ if (!inside(root, fs14.realpathSync(ancestor)))
15087
15211
  return failure2("invalid_input", `candidate scope escapes checkout through a symlink: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} resolves outside the checkout` }] });
15088
15212
  } catch {
15089
15213
  return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
@@ -15095,7 +15219,7 @@ var scopeRoots = (root, scope) => {
15095
15219
  function captureCandidate(root, scope, environment = []) {
15096
15220
  let checkout;
15097
15221
  try {
15098
- checkout = fs15.realpathSync(root);
15222
+ checkout = fs14.realpathSync(root);
15099
15223
  } catch {
15100
15224
  return failure2("invalid_input", "candidate root does not exist");
15101
15225
  }
@@ -15113,11 +15237,11 @@ function captureCandidate(root, scope, environment = []) {
15113
15237
  if (item === ".git" || item === ".workit")
15114
15238
  continue;
15115
15239
  names.add(item);
15116
- if (!fs15.existsSync(target))
15240
+ if (!fs14.existsSync(target))
15117
15241
  continue;
15118
15242
  let stat;
15119
15243
  try {
15120
- stat = fs15.lstatSync(target);
15244
+ stat = fs14.lstatSync(target);
15121
15245
  } catch {
15122
15246
  uncertain = true;
15123
15247
  continue;
@@ -15133,17 +15257,17 @@ function captureCandidate(root, scope, environment = []) {
15133
15257
  for (const item of [...names].toSorted()) {
15134
15258
  if (!scopeMatches(item, normalizedScope))
15135
15259
  continue;
15136
- const target = path19.join(checkout, item);
15260
+ const target = path18.join(checkout, item);
15137
15261
  try {
15138
- const stat = fs15.lstatSync(target);
15262
+ const stat = fs14.lstatSync(target);
15139
15263
  if (stat.isSymbolicLink()) {
15140
- const link = fs15.readlinkSync(target);
15264
+ const link = fs14.readlinkSync(target);
15141
15265
  files.push({ path: item, kind: "symlink", digest: digestBytes(link), executable: null });
15142
15266
  } else if (stat.isFile()) {
15143
15267
  files.push({
15144
15268
  path: item,
15145
15269
  kind: "file",
15146
- digest: digestBytes(fs15.readFileSync(target)),
15270
+ digest: digestBytes(fs14.readFileSync(target)),
15147
15271
  executable: (stat.mode & 73) !== 0
15148
15272
  });
15149
15273
  } else if (!stat.isDirectory()) {
@@ -15168,7 +15292,7 @@ function captureCandidate(root, scope, environment = []) {
15168
15292
  const values = supplied.toSorted((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
15169
15293
  if (values.some(({ value }) => value === null))
15170
15294
  completeness = "uncertain";
15171
- const headResult = spawnSync11("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
15295
+ const headResult = spawnSync10("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
15172
15296
  if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
15173
15297
  completeness = "uncertain";
15174
15298
  const head = headResult.status === 0 ? headResult.stdout.trim() : null;
@@ -15308,7 +15432,7 @@ var findingVerificationPasses = (findingCandidateId, evidence) => evidence.statu
15308
15432
  var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
15309
15433
  let root;
15310
15434
  try {
15311
- root = fs15.realpathSync(checkoutRoot);
15435
+ root = fs14.realpathSync(checkoutRoot);
15312
15436
  } catch {
15313
15437
  return failure2("permission_denied", "checkout root is unavailable");
15314
15438
  }
@@ -15317,23 +15441,23 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
15317
15441
  continue;
15318
15442
  if (!reference.digest)
15319
15443
  return failure2("invalid_input", "document references require a byte digest");
15320
- const target = path19.resolve(root, reference.path);
15321
- if (target !== root && !target.startsWith(`${root}${path19.sep}`))
15444
+ const target = path18.resolve(root, reference.path);
15445
+ if (target !== root && !target.startsWith(`${root}${path18.sep}`))
15322
15446
  return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
15323
15447
  fields: [
15324
15448
  { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
15325
15449
  ]
15326
15450
  });
15327
15451
  try {
15328
- const below = path19.relative(root, target);
15452
+ const below = path18.relative(root, target);
15329
15453
  let current = root;
15330
- for (const segment of below.split(path19.sep)) {
15454
+ for (const segment of below.split(path18.sep)) {
15331
15455
  if (!segment)
15332
15456
  continue;
15333
- current = path19.join(current, segment);
15334
- const stat = fs15.lstatSync(current);
15335
- const real = fs15.realpathSync(current);
15336
- if (real !== root && !real.startsWith(`${root}${path19.sep}`))
15457
+ current = path18.join(current, segment);
15458
+ const stat = fs14.lstatSync(current);
15459
+ const real = fs14.realpathSync(current);
15460
+ if (real !== root && !real.startsWith(`${root}${path18.sep}`))
15337
15461
  return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
15338
15462
  fields: [
15339
15463
  {
@@ -15349,7 +15473,7 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
15349
15473
  if (current !== target && !stat.isDirectory())
15350
15474
  return failure2("invalid_input", "document reference ancestor is not a directory");
15351
15475
  }
15352
- if (digestBytes(fs15.readFileSync(target)) !== reference.digest)
15476
+ if (digestBytes(fs14.readFileSync(target)) !== reference.digest)
15353
15477
  return failure2("permission_denied", "decision document bytes have changed");
15354
15478
  } catch {
15355
15479
  return failure2("invalid_input", "decision document is unavailable");
@@ -15780,7 +15904,7 @@ Autonomy contract:
15780
15904
  CLI first: if a step has one right answer, use the \`workit\` verb instead of
15781
15905
  hand-running git, gh or glab (\`workit git|pr|ci|check|help\`;
15782
15906
  else \`npx -y @brainervirus/workit-cli\`).
15783
- Other checkout: \`workit --cwd <dir> <verb>\` (Codex: \`<verb> --cwd <dir>\`); one workit
15907
+ Other checkout: \`workit <verb> --cwd <dir> …\` (verb first: host rules match it); one workit
15784
15908
  verb per shell call, never \`cd <dir> && workit\`. zsh-safe: no \`echo =====\`
15785
15909
  (\`printf '%s\\n' ---\`), quote globs and URLs, no variable named \`status\` or \`path\`.
15786
15910
  Add Co-Authored-By via \`workit git commit --trailer "K: v" -- <paths>\`, never
@@ -15792,7 +15916,7 @@ authorizes an effect. Judge work once with
15792
15916
  \`workit policy assess --judge risk= behavior= product-choice= plan=\`.
15793
15917
  Across repositories, bind items to checkout, branch and endpoint, resolve
15794
15918
  competing targets before a mutation, and reconcile an uncertain
15795
- external effect before a retry.
15919
+ effect before a retry.
15796
15920
 
15797
15921
  Mid-task: never silently drop or resume an objective (workit-continue).
15798
15922
 
@@ -15934,21 +16058,21 @@ var exportDigest = (bundle) => sha256({
15934
16058
  task: bundle.task
15935
16059
  });
15936
16060
  // packages/workit-core/src/core/route-intent.ts
15937
- import { spawnSync as spawnSync13 } from "node:child_process";
16061
+ import { spawnSync as spawnSync12 } from "node:child_process";
15938
16062
  import { existsSync as existsSync7 } from "node:fs";
15939
- import path22 from "node:path";
16063
+ import path21 from "node:path";
15940
16064
 
15941
16065
  // packages/workit-core/src/hooks/raw-git.ts
15942
- import { spawnSync as spawnSync12 } from "node:child_process";
16066
+ import { spawnSync as spawnSync11 } from "node:child_process";
15943
16067
  import { createHash as createHash14 } from "node:crypto";
15944
- import fs17 from "node:fs";
16068
+ import fs16 from "node:fs";
15945
16069
  import os4 from "node:os";
15946
- import path21 from "node:path";
16070
+ import path20 from "node:path";
15947
16071
 
15948
16072
  // packages/workit-core/src/hooks/session-marker.ts
15949
16073
  import { createHash as createHash13 } from "node:crypto";
15950
- import fs16 from "node:fs";
15951
- import path20 from "node:path";
16074
+ import fs15 from "node:fs";
16075
+ import path19 from "node:path";
15952
16076
  var sessionMarkerFile = (input, session, store) => {
15953
16077
  let dir = store;
15954
16078
  if (dir === undefined) {
@@ -15958,13 +16082,13 @@ var sessionMarkerFile = (input, session, store) => {
15958
16082
  dir = location.dir;
15959
16083
  }
15960
16084
  const name = createHash13("sha256").update(`${input.host}\x00${session}`).digest("hex").slice(0, 32);
15961
- return path20.join(dir, "hooks", `skills-${name}.json`);
16085
+ return path19.join(dir, "hooks", `skills-${name}.json`);
15962
16086
  };
15963
16087
  var list2 = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
15964
16088
  var EMPTY = { nudged: [], loaded: [], raw: [], stops: [], judges: [] };
15965
16089
  var readRaw = (file) => {
15966
16090
  try {
15967
- return fs16.readFileSync(file, "utf8");
16091
+ return fs15.readFileSync(file, "utf8");
15968
16092
  } catch {
15969
16093
  return "";
15970
16094
  }
@@ -15985,13 +16109,13 @@ var parseMarker = (text) => {
15985
16109
  };
15986
16110
  var readSessionMarker = (file) => parseMarker(readRaw(file));
15987
16111
  var writeAtomic = (file, marker) => {
15988
- fs16.mkdirSync(path20.dirname(file), { recursive: true });
16112
+ fs15.mkdirSync(path19.dirname(file), { recursive: true });
15989
16113
  const temp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
15990
16114
  try {
15991
- fs16.writeFileSync(temp, JSON.stringify(marker));
15992
- fs16.renameSync(temp, file);
16115
+ fs15.writeFileSync(temp, JSON.stringify(marker));
16116
+ fs15.renameSync(temp, file);
15993
16117
  } catch (error) {
15994
- fs16.rmSync(temp, { force: true });
16118
+ fs15.rmSync(temp, { force: true });
15995
16119
  throw error;
15996
16120
  }
15997
16121
  };
@@ -16272,6 +16396,18 @@ var substitutions = (body) => {
16272
16396
  };
16273
16397
  var PLAIN_POSIX = /[^\s'"\\`$(){},<>|;&#]+/uy;
16274
16398
  var PLAIN_POWERSHELL = /[^\s'"`(){}<>|;&#]+/uy;
16399
+ var expandsInQuotes = (body, dialect) => {
16400
+ const escape = dialect === "posix" ? "\\" : "`";
16401
+ for (let index = 0;index < body.length; index++) {
16402
+ const char = body[index];
16403
+ if (char === escape)
16404
+ index++;
16405
+ else if (char === "$" || dialect === "posix" && char === "`")
16406
+ return true;
16407
+ }
16408
+ return false;
16409
+ };
16410
+ var EXPANDING_RUN = /[*?[]/u;
16275
16411
  var arithmeticEnd = (command, start) => {
16276
16412
  const end = substitutionEnd(command, start);
16277
16413
  return command[end - 1] === ")" && end > start + 2 ? end : 0;
@@ -16290,6 +16426,7 @@ var tokenize = (command, dialect) => {
16290
16426
  let current = { words: [], redirects: [] };
16291
16427
  let word = "";
16292
16428
  let inWord = false;
16429
+ let expanding = false;
16293
16430
  let pending = null;
16294
16431
  const heredocs = [];
16295
16432
  const escape = posix ? "\\" : "`";
@@ -16304,11 +16441,16 @@ var tokenize = (command, dialect) => {
16304
16441
  heredocs.push({ delimiter: text.replace(/^-/, ""), owner: current });
16305
16442
  else if (pending === "herestring")
16306
16443
  (current.stdin ??= []).push(text);
16307
- else if (pending !== "input")
16444
+ else if (pending !== "input") {
16445
+ const at = current.words.length;
16308
16446
  current.words.push(...MARKERS.test(word) ? expandBraces(word) : [word]);
16447
+ if (expanding || !posix && word.startsWith("@"))
16448
+ (current.expands ??= []).push(...current.words.slice(at).map((_, i) => at + i));
16449
+ }
16309
16450
  pending = null;
16310
16451
  word = "";
16311
16452
  inWord = false;
16453
+ expanding = false;
16312
16454
  };
16313
16455
  const flushSegment = () => {
16314
16456
  flushWord();
@@ -16323,6 +16465,7 @@ var tokenize = (command, dialect) => {
16323
16465
  };
16324
16466
  const substitution = (inner, text) => {
16325
16467
  (current.subs ??= []).push(inner);
16468
+ expanding = true;
16326
16469
  word += text;
16327
16470
  inWord = true;
16328
16471
  };
@@ -16333,6 +16476,8 @@ var tokenize = (command, dialect) => {
16333
16476
  if (run) {
16334
16477
  word += run[0];
16335
16478
  inWord = true;
16479
+ if (EXPANDING_RUN.test(run[0]))
16480
+ expanding = true;
16336
16481
  index += run[0].length - 1;
16337
16482
  continue;
16338
16483
  }
@@ -16361,6 +16506,8 @@ var tokenize = (command, dialect) => {
16361
16506
  const body = command.slice(index + 1, end < 0 ? undefined : end);
16362
16507
  if (posix && char === '"' && /\$\(|`/.test(body))
16363
16508
  (current.subs ??= []).push(...substitutions(body));
16509
+ if (char === '"' && expandsInQuotes(body, dialect))
16510
+ expanding = true;
16364
16511
  word += unquote(body, char, dialect);
16365
16512
  inWord = true;
16366
16513
  index = end < 0 ? command.length : end;
@@ -16406,6 +16553,8 @@ var tokenize = (command, dialect) => {
16406
16553
  if (char === "(" && inWord && word.endsWith("$")) {
16407
16554
  word = word.slice(0, -1);
16408
16555
  inWord = word !== "";
16556
+ if (!inWord)
16557
+ expanding = false;
16409
16558
  }
16410
16559
  group(char === "(" ? "open" : "close");
16411
16560
  } else if (!posix && (char === "{" || char === "}")) {
@@ -16459,6 +16608,8 @@ var tokenize = (command, dialect) => {
16459
16608
  else if (posix && char === "}" && word.includes(OPEN))
16460
16609
  word += CLOSE;
16461
16610
  else {
16611
+ if (char === "$")
16612
+ expanding = true;
16462
16613
  word += char;
16463
16614
  inWord = true;
16464
16615
  }
@@ -16513,17 +16664,17 @@ var GIT_VALUE_OPTIONS = new Set([
16513
16664
  ]);
16514
16665
  var GIT_CONFIG_OPTIONS = new Set(["-c", "--config-env", "--git-dir"]);
16515
16666
  var GIT_CONFIG_ENV = /^GIT_(?:CONFIG\w*|DIR|COMMON_DIR|WORK_TREE)$/;
16516
- var ASSIGNMENT2 = /^([A-Za-z_]\w*)=([\s\S]*)$/;
16667
+ var ASSIGNMENT = /^([A-Za-z_]\w*)=([\s\S]*)$/;
16517
16668
  var MAX_DEPTH = 4;
16518
16669
  var MAX_EXPANSIONS = 16;
16519
- var isAbsolute = (value) => path21.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
16670
+ var isAbsolute = (value) => path20.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
16520
16671
  var HERE = /^(?:\$PWD|\$\{PWD\}|\$\(\s*pwd\s*\)|`pwd`|\$\(\s*git\s+rev-parse\s+--show-toplevel\s*\))(?=$|[\\/])/;
16521
16672
  var joinDir = (base, raw) => {
16522
16673
  const next = raw.replace(HERE, ".");
16523
16674
  if (base === undefined || /[$`*?]/.test(next) || next === "-")
16524
16675
  return;
16525
- const expanded = next === "~" || next.startsWith("~/") ? path21.join(os4.homedir(), next.slice(1)) : next;
16526
- return base === null || isAbsolute(expanded) ? expanded : path21.join(base, expanded);
16676
+ const expanded = next === "~" || next.startsWith("~/") ? path20.join(os4.homedir(), next.slice(1)) : next;
16677
+ return base === null || isAbsolute(expanded) ? expanded : path20.join(base, expanded);
16527
16678
  };
16528
16679
  var rebase = (base, inner) => inner === null ? base : inner === undefined ? undefined : joinDir(base, inner);
16529
16680
  var cdTarget = (words) => {
@@ -16540,7 +16691,7 @@ var stripPrefixes = (input) => {
16540
16691
  const first = words[0];
16541
16692
  if (first === undefined)
16542
16693
  return { words, env };
16543
- const assignment = ASSIGNMENT2.exec(first);
16694
+ const assignment = ASSIGNMENT.exec(first);
16544
16695
  if (assignment) {
16545
16696
  if (GIT_CONFIG_ENV.test(assignment[1]))
16546
16697
  env[assignment[1]] = assignment[2];
@@ -16599,10 +16750,10 @@ var assignments = (words, vars) => {
16599
16750
  while (rest[0]?.startsWith("-"))
16600
16751
  rest = rest.slice(1);
16601
16752
  }
16602
- if (rest.length === 0 || !rest.every((word) => ASSIGNMENT2.test(word)))
16753
+ if (rest.length === 0 || !rest.every((word) => ASSIGNMENT.test(word)))
16603
16754
  return null;
16604
16755
  return rest.map((word) => {
16605
- const [, name, value] = ASSIGNMENT2.exec(word);
16756
+ const [, name, value] = ASSIGNMENT.exec(word);
16606
16757
  return [name, expandWord(value, vars)];
16607
16758
  });
16608
16759
  };
@@ -16790,7 +16941,7 @@ var splitString = (line) => {
16790
16941
  return line.slice(index).join(" ").slice(15);
16791
16942
  if (/^-S./.test(word))
16792
16943
  return [word.slice(2), ...line.slice(index + 1)].join(" ");
16793
- if (!word.startsWith("-") && !ASSIGNMENT2.test(word))
16944
+ if (!word.startsWith("-") && !ASSIGNMENT.test(word))
16794
16945
  return null;
16795
16946
  }
16796
16947
  return null;
@@ -16903,8 +17054,17 @@ function walk2(command, dialect, depth, seen) {
16903
17054
  visit(next, undefined, hops + 1);
16904
17055
  };
16905
17056
  const wrapped = seen ? null : workitCheckTail(words);
16906
- if (wrapped)
16907
- return "script" in wrapped ? nested(wrapped.script, "posix") : runs([wrapped.argv]);
17057
+ if (wrapped) {
17058
+ const outer = dir;
17059
+ if (wrapped.cwd !== null)
17060
+ dir = joinDir(dir, wrapped.cwd);
17061
+ if ("script" in wrapped)
17062
+ nested(wrapped.script, "posix");
17063
+ else
17064
+ runs([wrapped.argv]);
17065
+ dir = outer;
17066
+ return;
17067
+ }
16908
17068
  if (CD.has(name)) {
16909
17069
  const target = cdTarget(words);
16910
17070
  dir = target === null ? os4.homedir() : joinDir(dir, target);
@@ -17005,14 +17165,14 @@ function workitCheckTail(words) {
17005
17165
  return null;
17006
17166
  } else if (!/^workit(?:\.exe|\.cmd)?$/.test(commandName(words[0])))
17007
17167
  return null;
17008
- const args = words.slice(at + 1);
17168
+ const { rest: args, cwd } = skipWorkitGlobals(words.slice(at + 1));
17009
17169
  if (args[0] !== "check" && !(args[0] === "lock" && args[1] === "run"))
17010
17170
  return null;
17011
17171
  const split = args.indexOf("--");
17012
17172
  const tail = split < 0 ? [] : args.slice(split + 1);
17013
17173
  if (!tail.length)
17014
17174
  return null;
17015
- return args.slice(0, split).includes("--shell") ? { script: tail.join(" ") } : { argv: tail };
17175
+ return args.slice(0, split).includes("--shell") ? { script: tail.join(" "), cwd } : { argv: tail, cwd };
17016
17176
  }
17017
17177
  function simpleCommand(command, dialect = "posix", depth = 0) {
17018
17178
  let segments;
@@ -17073,7 +17233,21 @@ function skipWorkitGlobals(args) {
17073
17233
  } else if (args[at] !== "--json")
17074
17234
  break;
17075
17235
  }
17076
- return { rest: args.slice(at), cwd };
17236
+ const rest = [];
17237
+ for (;at < args.length; at++) {
17238
+ const arg = args[at];
17239
+ if (arg === "--") {
17240
+ rest.push(...args.slice(at));
17241
+ break;
17242
+ }
17243
+ if (arg.startsWith("--cwd="))
17244
+ cwd = arg.slice("--cwd=".length);
17245
+ else if (arg === "--cwd" && args[at + 1] !== undefined)
17246
+ cwd = args[++at];
17247
+ else
17248
+ rest.push(arg);
17249
+ }
17250
+ return { rest, cwd };
17077
17251
  }
17078
17252
  var PUSH_VALUE_OPTIONS = new Set(["-o", "--push-option", "--receive-pack", "--exec", "--repo"]);
17079
17253
  var CURRENT_BRANCH = /^\$\(\s*git\s+(?:branch\s+--show-current|rev-parse\s+--abbrev-ref\s+HEAD|symbolic-ref\s+--short\s+HEAD)\s*\)$/;
@@ -17353,7 +17527,7 @@ var repoAt = (dir) => {
17353
17527
  return { location, dir, branch: resolveTaskKey(dir, location).branch };
17354
17528
  };
17355
17529
  var isWorkitWorkspace = (repo) => {
17356
- if (fs17.existsSync(repo.location.dir))
17530
+ if (fs16.existsSync(repo.location.dir))
17357
17531
  return true;
17358
17532
  try {
17359
17533
  return resolveRuntimeWorkspaceVcs(repo.location.top ?? repo.dir) !== null;
@@ -17374,17 +17548,17 @@ var headSha2 = (repo) => {
17374
17548
  if (!git || git.reftable)
17375
17549
  return null;
17376
17550
  try {
17377
- const head = fs17.readFileSync(path21.join(git.gitDir, "HEAD"), "utf8").trim();
17551
+ const head = fs16.readFileSync(path20.join(git.gitDir, "HEAD"), "utf8").trim();
17378
17552
  if (/^[0-9a-f]{40,64}$/.test(head))
17379
17553
  return head;
17380
17554
  const ref = /^ref: (refs\/.+)$/.exec(head)?.[1];
17381
17555
  if (!ref)
17382
17556
  return null;
17383
- const common = path21.dirname(repo.location.dir);
17557
+ const common = path20.dirname(repo.location.dir);
17384
17558
  try {
17385
- return fs17.readFileSync(path21.join(common, ref), "utf8").trim() || null;
17559
+ return fs16.readFileSync(path20.join(common, ref), "utf8").trim() || null;
17386
17560
  } catch {}
17387
- const packed = fs17.readFileSync(path21.join(common, "packed-refs"), "utf8");
17561
+ const packed = fs16.readFileSync(path20.join(common, "packed-refs"), "utf8");
17388
17562
  return new RegExp(`^([0-9a-f]{40,64}) ${ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m").exec(packed)?.[1] ?? null;
17389
17563
  } catch {
17390
17564
  return null;
@@ -17396,7 +17570,7 @@ var gitConfig = (dir, invocation, cache) => {
17396
17570
  if (cached)
17397
17571
  return cached;
17398
17572
  const config = new Map;
17399
- const listed = spawnSync12("git", [...invocation.config ?? [], "config", "-z", "--list"], {
17573
+ const listed = spawnSync11("git", [...invocation.config ?? [], "config", "-z", "--list"], {
17400
17574
  cwd: dir,
17401
17575
  encoding: "utf8",
17402
17576
  timeout: 3000,
@@ -17472,8 +17646,8 @@ var expandGitAliases = (root, invocation, cache = new Map, depth = 0) => {
17472
17646
  const sub = invocation.args[0];
17473
17647
  if (invocation.tool !== "git" || invocation.dir === undefined || !sub || sub.startsWith("-") || GIT_COMMANDS.has(sub) || depth >= MAX_DEPTH)
17474
17648
  return [invocation];
17475
- const dir = path21.resolve(root, invocation.dir ?? ".");
17476
- if (!fs17.existsSync(dir))
17649
+ const dir = path20.resolve(root, invocation.dir ?? ".");
17650
+ if (!fs16.existsSync(dir))
17477
17651
  return [invocation];
17478
17652
  const config = gitConfig(dir, invocation, cache);
17479
17653
  const alias = configValue(config, `alias.${sub.toLowerCase()}`);
@@ -17495,11 +17669,11 @@ var expandGitAliases = (root, invocation, cache = new Map, depth = 0) => {
17495
17669
  var normalizedUrl = (url, dir) => {
17496
17670
  const trimmed = url.trim();
17497
17671
  if (/^file:\/\//i.test(trimmed))
17498
- return path21.resolve(dir, trimmed.slice(7)).replace(/\/+$/, "").replace(/\.git$/, "");
17672
+ return path20.resolve(dir, trimmed.slice(7)).replace(/\/+$/, "").replace(/\.git$/, "");
17499
17673
  const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(trimmed);
17500
17674
  const scp = !scheme && /^(?:[^@/\\]+@)?[^/\\:]{2,}:/.test(trimmed);
17501
17675
  if (!scheme && !scp)
17502
- return path21.resolve(dir, trimmed).replace(/\/+$/, "").replace(/\.git$/, "");
17676
+ return path20.resolve(dir, trimmed).replace(/\/+$/, "").replace(/\.git$/, "");
17503
17677
  const value = scheme ? trimmed.slice(scheme[0].length).replace(/^[^@/]*@/, "").replace(/^([^/:]+):\d+(?=\/)/, "$1") : trimmed.replace(/^[^@/]*@/, "").replace(":", "/");
17504
17678
  return value.toLowerCase().replace(/\/{2,}/g, "/").replace(/^www\./, "").replace(/\/+$/, "").replace(/\.git$/, "");
17505
17679
  };
@@ -17519,7 +17693,7 @@ var rewrittenUrl = (url, config) => {
17519
17693
  }
17520
17694
  return url;
17521
17695
  };
17522
- var pushUrl2 = (config, remote, dir) => {
17696
+ var pushUrl = (config, remote, dir) => {
17523
17697
  const configured = configValue(config, `remote.${remote}.pushurl`) ?? configValue(config, `remote.${remote}.url`);
17524
17698
  return normalizedUrl(rewrittenUrl(configured ?? remote, config), dir);
17525
17699
  };
@@ -17547,7 +17721,7 @@ var toPushRemote = (repo, remote, config) => {
17547
17721
  const pushRemote = pushRemoteOf(config, repo.branch);
17548
17722
  if (pushRemote === null || pushRemote === remote)
17549
17723
  return true;
17550
- return pushUrl2(config, remote, repo.dir) === pushUrl2(config, pushRemote, repo.dir);
17724
+ return pushUrl(config, remote, repo.dir) === pushUrl(config, pushRemote, repo.dir);
17551
17725
  };
17552
17726
  var quote = (text) => `\`${text}\``;
17553
17727
  var refusal = (action, branch, dir) => {
@@ -17560,7 +17734,7 @@ var refusal = (action, branch, dir) => {
17560
17734
  };
17561
17735
  var refFacts = (repo, remote) => {
17562
17736
  const facts = { heads: new Set, tags: new Set, remoteHeads: new Set };
17563
- const listed = spawnSync12("git", ["for-each-ref", "--format=%(refname)", "refs/heads", "refs/tags", `refs/remotes/${remote}`], { cwd: repo.dir, encoding: "utf8", timeout: 5000 });
17737
+ const listed = spawnSync11("git", ["for-each-ref", "--format=%(refname)", "refs/heads", "refs/tags", `refs/remotes/${remote}`], { cwd: repo.dir, encoding: "utf8", timeout: 5000 });
17564
17738
  const remotePrefix = `refs/remotes/${remote}/`;
17565
17739
  for (const ref of (listed.stdout ?? "").split(`
17566
17740
  `)) {
@@ -17577,7 +17751,7 @@ var globBranches = (repo, glob) => {
17577
17751
  const source = glob.source.startsWith("refs/") ? glob.source : `refs/heads/${glob.source}`;
17578
17752
  const [sourcePrefix, sourceSuffix = ""] = source.split("*");
17579
17753
  const [destinationPrefix, destinationSuffix = ""] = glob.destination.split("*");
17580
- const listed = spawnSync12("git", [
17754
+ const listed = spawnSync11("git", [
17581
17755
  "for-each-ref",
17582
17756
  "--format=%(refname)",
17583
17757
  sourcePrefix.slice(0, sourcePrefix.lastIndexOf("/") + 1)
@@ -17654,7 +17828,7 @@ var tagRefusal = (action, facts, dir) => {
17654
17828
  return `workit: raw ${quote(`git push${action.allTags ? " --tags" : ""}`)} of ${tags.length ? `tag ${tags.join(", ")}` : "tags"} bypasses the release grant. Push a release tag with ${quote(`workit git push --release ${named}`)} (${shape} tags whose commit the production branch already has; a tag never moves), under the ${quote("release")} grant (if refused, ask the user to run ${quote("workit grant set <workspace> release=true")}).`;
17655
17829
  };
17656
17830
  var remoteHeads = (repo, remote) => {
17657
- const listed = spawnSync12("git", ["ls-remote", "--heads", remote], {
17831
+ const listed = spawnSync11("git", ["ls-remote", "--heads", remote], {
17658
17832
  cwd: repo.dir,
17659
17833
  encoding: "utf8",
17660
17834
  timeout: 3000,
@@ -17723,7 +17897,7 @@ var actionsIn = (cwd, command, dialect, cache = new Map) => {
17723
17897
  const out = [];
17724
17898
  const repos = new Map;
17725
17899
  const repoFor = (relative) => {
17726
- const dir = path21.resolve(cwd, relative ?? ".");
17900
+ const dir = path20.resolve(cwd, relative ?? ".");
17727
17901
  if (!repos.has(dir)) {
17728
17902
  const repo = repoAt(dir);
17729
17903
  repos.set(dir, repo && isWorkitWorkspace(repo) ? repo : null);
@@ -17809,7 +17983,7 @@ var analysisDenial = (error) => ({
17809
17983
  });
17810
17984
  var NEXT_COMMAND = "next";
17811
17985
  var MARKER_TTL_SECONDS = 3600;
17812
- var markerFile = (repo, session, key) => path21.join(repo.location.dir, "hooks", `commit-${createHash14("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`);
17986
+ var markerFile = (repo, session, key) => path20.join(repo.location.dir, "hooks", `commit-${createHash14("sha256").update(`${session}\x00${key}`).digest("hex").slice(0, 32)}.json`);
17813
17987
  var callKey = (toolUseId, command) => toolUseId ?? `cmd:${createHash14("sha256").update(command).digest("hex")}`;
17814
17988
  function noteRawCommit(input, command, key) {
17815
17989
  try {
@@ -17820,14 +17994,14 @@ function noteRawCommit(input, command, key) {
17820
17994
  if (action.kind !== "commit")
17821
17995
  continue;
17822
17996
  const file = markerFile(repo, input.session.id, key);
17823
- fs17.mkdirSync(path21.dirname(file), { recursive: true });
17824
- fs17.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
17997
+ fs16.mkdirSync(path20.dirname(file), { recursive: true });
17998
+ fs16.writeFileSync(file, `${JSON.stringify({ head: headSha2(repo), at })}
17825
17999
  `);
17826
18000
  }
17827
18001
  } catch {}
17828
18002
  }
17829
18003
  var headCommit = (dir) => {
17830
- const run = spawnSync12("git", [
18004
+ const run = spawnSync11("git", [
17831
18005
  "log",
17832
18006
  "-1",
17833
18007
  "--format=%H%x1f%ct%x1f%s%x1f%(trailers:key=Workit-Session,valueonly,separator=%x2c)"
@@ -17844,11 +18018,11 @@ var headCommit = (dir) => {
17844
18018
  var settle2 = (input, repo, file) => {
17845
18019
  let marker;
17846
18020
  try {
17847
- marker = JSON.parse(fs17.readFileSync(file, "utf8"));
18021
+ marker = JSON.parse(fs16.readFileSync(file, "utf8"));
17848
18022
  } catch {
17849
18023
  return;
17850
18024
  }
17851
- fs17.rmSync(file, { force: true });
18025
+ fs16.rmSync(file, { force: true });
17852
18026
  const session = input.session.id;
17853
18027
  const now = Date.now() / 1000;
17854
18028
  if (typeof marker.at !== "number" || now - marker.at > MARKER_TTL_SECONDS)
@@ -17897,7 +18071,7 @@ function settlePendingCommit(input) {
17897
18071
  if (!repo)
17898
18072
  return;
17899
18073
  const file = markerFile(repo, input.session.id, NEXT_COMMAND);
17900
- if (fs17.existsSync(file))
18074
+ if (fs16.existsSync(file))
17901
18075
  settle2(input, repo, file);
17902
18076
  } catch {}
17903
18077
  }
@@ -18041,7 +18215,7 @@ var targetsOf = (invocations) => invocations.flatMap((invocation) => invocation.
18041
18215
  branch,
18042
18216
  dir: invocation.dir
18043
18217
  })) : []);
18044
- var localBranchExists = (dir, branch) => spawnSync13("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
18218
+ var localBranchExists = (dir, branch) => spawnSync12("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
18045
18219
  cwd: dir,
18046
18220
  stdio: "ignore",
18047
18221
  timeout: 5000
@@ -18051,7 +18225,7 @@ var judgedDir = (root, dir) => {
18051
18225
  return null;
18052
18226
  if (dir === null)
18053
18227
  return root;
18054
- const resolved = path22.resolve(root, dir);
18228
+ const resolved = path21.resolve(root, dir);
18055
18229
  return existsSync7(resolved) && inWorkitWorkspace(resolved) ? resolved : null;
18056
18230
  };
18057
18231
  var shellBranchPolicyViolation = (root, command, dialect = "posix") => {
@@ -18287,7 +18461,7 @@ var judgeInputSummary = (raw) => {
18287
18461
 
18288
18462
  // packages/workit-core/src/autonomy.ts
18289
18463
  import os5 from "node:os";
18290
- import path23 from "node:path";
18464
+ import path22 from "node:path";
18291
18465
  var accountHome = () => {
18292
18466
  try {
18293
18467
  return os5.userInfo().homedir;
@@ -18296,10 +18470,10 @@ var accountHome = () => {
18296
18470
  }
18297
18471
  };
18298
18472
  var grantsHome = { resolve: accountHome };
18299
- var grantsDir = () => path23.join(grantsHome.resolve(), ".config", "workit");
18473
+ var grantsDir = () => path22.join(grantsHome.resolve(), ".config", "workit");
18300
18474
  var grantsOverride = () => {
18301
- const configured = path23.resolve(resolveConfigDir());
18302
- return configured === path23.resolve(grantsDir()) ? null : `the config directory is redirected to ${configured} (WORKFLOW_TOOLKIT_CONFIG, WORKFLOW_TOOLKIT_CONFIG_DIR or XDG_CONFIG_HOME); grants are read only from ${path23.join(grantsDir(), "workspaces.json")}, so the D4 defaults apply`;
18475
+ const configured = path22.resolve(resolveConfigDir());
18476
+ return configured === path22.resolve(grantsDir()) ? null : `the config directory is redirected to ${configured} (WORKFLOW_TOOLKIT_CONFIG, WORKFLOW_TOOLKIT_CONFIG_DIR or XDG_CONFIG_HOME); grants are read only from ${path22.join(grantsDir(), "workspaces.json")}, so the D4 defaults apply`;
18303
18477
  };
18304
18478
  var GRANT_KINDS = ["push", "pr", "merge", "release", "rerun"];
18305
18479
  var DEFAULT_GRANTS = Object.freeze({
@@ -20108,9 +20282,9 @@ class WorkitCore {
20108
20282
  }
20109
20283
  }
20110
20284
  // packages/workit-core/src/core/repo-context.ts
20111
- import { spawnSync as spawnSync14 } from "node:child_process";
20285
+ import { spawnSync as spawnSync13 } from "node:child_process";
20112
20286
  import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync as readdirSync4 } from "node:fs";
20113
- import path24 from "node:path";
20287
+ import path23 from "node:path";
20114
20288
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
20115
20289
  try {
20116
20290
  gitRevisionParts(value);
@@ -20137,7 +20311,7 @@ var invalidRange = (cwd) => ({
20137
20311
  cwd
20138
20312
  });
20139
20313
  var runGit = (cwd, args) => {
20140
- const result = spawnSync14("git", args, { cwd, encoding: "utf8" });
20314
+ const result = spawnSync13("git", args, { cwd, encoding: "utf8" });
20141
20315
  return {
20142
20316
  stdout: (result.stdout ?? "").trimEnd(),
20143
20317
  stderr: (result.stderr ?? "").trimEnd(),
@@ -20244,7 +20418,7 @@ var findPrTemplate = (cwd) => {
20244
20418
  ]) {
20245
20419
  const parts = rel.split("/");
20246
20420
  const base = parts[parts.length - 1];
20247
- const dir = parts.length > 1 ? path24.join(cwd, ...parts.slice(0, -1)) : cwd;
20421
+ const dir = parts.length > 1 ? path23.join(cwd, ...parts.slice(0, -1)) : cwd;
20248
20422
  const actual = probeCaseInsensitive(dir, base);
20249
20423
  if (actual)
20250
20424
  return [...parts.slice(0, -1), actual].join("/");
@@ -20275,7 +20449,7 @@ var diffStatForRange = (cwd, range) => {
20275
20449
  return runGit(cwd, ["diff", "--stat", "--"]).stdout;
20276
20450
  };
20277
20451
  var packageScripts = (cwd, keys) => {
20278
- const pkgPath = path24.join(cwd, "package.json");
20452
+ const pkgPath = path23.join(cwd, "package.json");
20279
20453
  if (!existsSync8(pkgPath))
20280
20454
  return [];
20281
20455
  try {
@@ -20301,9 +20475,9 @@ var documentationFiles = (cwd) => {
20301
20475
  if (entry.isDirectory()) {
20302
20476
  if (excluded.has(entry.name))
20303
20477
  continue;
20304
- walk(path24.join(dir, entry.name), depth + 1);
20478
+ walk(path23.join(dir, entry.name), depth + 1);
20305
20479
  } else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
20306
- matches.push("./" + path24.relative(cwd, path24.join(dir, entry.name)).replaceAll("\\", "/"));
20480
+ matches.push("./" + path23.relative(cwd, path23.join(dir, entry.name)).replaceAll("\\", "/"));
20307
20481
  }
20308
20482
  }
20309
20483
  };
@@ -20319,7 +20493,7 @@ var readTrimmed2 = (file, maxLines) => {
20319
20493
  `);
20320
20494
  };
20321
20495
  function prReadyContext(root, range) {
20322
- const cwd = path24.resolve(repoRoot(root));
20496
+ const cwd = path23.resolve(repoRoot(root));
20323
20497
  if (range !== undefined && !isResolvableContextRange(cwd, range))
20324
20498
  return invalidRange(cwd);
20325
20499
  let stdout = "";
@@ -20377,7 +20551,7 @@ ${status.stderr}` : "");
20377
20551
  stdout += `template_path: ${template}
20378
20552
 
20379
20553
  `;
20380
- stdout += readTrimmed2(path24.join(cwd, template), 220);
20554
+ stdout += readTrimmed2(path23.join(cwd, template), 220);
20381
20555
  } else {
20382
20556
  stdout += `template_path: none
20383
20557
 
@@ -20387,7 +20561,7 @@ ${status.stderr}` : "");
20387
20561
  stdout += `
20388
20562
  `;
20389
20563
  stdout += printSection("Recent Validation Signals");
20390
- if (existsSync8(path24.join(cwd, "package.json"))) {
20564
+ if (existsSync8(path23.join(cwd, "package.json"))) {
20391
20565
  stdout += `package.json detected. Common scripts:
20392
20566
  `;
20393
20567
  const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
@@ -20396,7 +20570,7 @@ ${status.stderr}` : "");
20396
20570
  `) + `
20397
20571
  `;
20398
20572
  }
20399
- if (existsSync8(path24.join(cwd, "Cargo.toml")) || existsSync8(path24.join(cwd, "src-tauri/Cargo.toml"))) {
20573
+ if (existsSync8(path23.join(cwd, "Cargo.toml")) || existsSync8(path23.join(cwd, "src-tauri/Cargo.toml"))) {
20400
20574
  stdout += `Rust project detected.
20401
20575
  `;
20402
20576
  }
@@ -20429,7 +20603,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
20429
20603
  - Apply with the host-native editor, preserving the repository changelog conventions.
20430
20604
  - If Unreleased already has duplicate category headings, normalize_only first.`;
20431
20605
  function changelogContext(root, range) {
20432
- const cwd = path24.resolve(repoRoot(root));
20606
+ const cwd = path23.resolve(repoRoot(root));
20433
20607
  if (range !== undefined && !isResolvableContextRange(cwd, range))
20434
20608
  return invalidRange(cwd);
20435
20609
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -20447,7 +20621,7 @@ function changelogContext(root, range) {
20447
20621
  stdout += CHANGELOG_RULES + `
20448
20622
  `;
20449
20623
  stdout += printSection("Existing CHANGELOG.md");
20450
- const changelogPath = path24.join(cwd, "CHANGELOG.md");
20624
+ const changelogPath = path23.join(cwd, "CHANGELOG.md");
20451
20625
  if (existsSync8(changelogPath)) {
20452
20626
  stdout += readTrimmed2(changelogPath, 260) + `
20453
20627
  `;
@@ -20467,7 +20641,7 @@ function changelogContext(root, range) {
20467
20641
  return { stdout, stderr: "", exitCode: 0, cwd };
20468
20642
  }
20469
20643
  function docsRefreshContext(root, range) {
20470
- const cwd = path24.resolve(repoRoot(root));
20644
+ const cwd = path23.resolve(repoRoot(root));
20471
20645
  if (range !== undefined && !isResolvableContextRange(cwd, range))
20472
20646
  return invalidRange(cwd);
20473
20647
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -20489,7 +20663,7 @@ function docsRefreshContext(root, range) {
20489
20663
  `) + `
20490
20664
  `;
20491
20665
  stdout += printSection("README Preview");
20492
- const readme = path24.join(cwd, "README.md");
20666
+ const readme = path23.join(cwd, "README.md");
20493
20667
  if (existsSync8(readme)) {
20494
20668
  stdout += readTrimmed2(readme, 220) + `
20495
20669
  `;
@@ -20498,7 +20672,7 @@ function docsRefreshContext(root, range) {
20498
20672
  `;
20499
20673
  }
20500
20674
  stdout += printSection("Package Scripts");
20501
- const pkgPath = path24.join(cwd, "package.json");
20675
+ const pkgPath = path23.join(cwd, "package.json");
20502
20676
  if (existsSync8(pkgPath)) {
20503
20677
  try {
20504
20678
  const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
@@ -20512,7 +20686,7 @@ function docsRefreshContext(root, range) {
20512
20686
  return { stdout, stderr: "", exitCode: 0, cwd };
20513
20687
  }
20514
20688
  function releaseNotesContext(root, rangeOrTag) {
20515
- const cwd = path24.resolve(repoRoot(root));
20689
+ const cwd = path23.resolve(repoRoot(root));
20516
20690
  if (!rangeOrTag) {
20517
20691
  return { stdout: "", stderr: `ERROR: release tag or range required
20518
20692
  `, exitCode: 1, cwd };
@@ -20550,7 +20724,7 @@ function releaseNotesContext(root, rangeOrTag) {
20550
20724
  `;
20551
20725
  stdout += printSection("Existing Release Files");
20552
20726
  for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
20553
- if (existsSync8(path24.join(cwd, rel)))
20727
+ if (existsSync8(path23.join(cwd, rel)))
20554
20728
  stdout += rel + `
20555
20729
  `;
20556
20730
  }
@@ -20579,6 +20753,46 @@ function releaseNotesContext(root, rangeOrTag) {
20579
20753
 
20580
20754
  // packages/workit-core/src/core/tracker-issues.ts
20581
20755
  import { spawnSync as spawnSync15 } from "node:child_process";
20756
+
20757
+ // packages/workit-core/src/core/pr-create.ts
20758
+ import fs17 from "node:fs";
20759
+ import path24 from "node:path";
20760
+ import { spawnSync as spawnSync14 } from "node:child_process";
20761
+ // packages/workit-core/src/forge/pr-body.ts
20762
+ function parseGhIssue(value) {
20763
+ const m = /issues\/(\d+)/.exec(value);
20764
+ return m ? m[1] : String(value).trim().replace(/^#/, "");
20765
+ }
20766
+
20767
+ // packages/workit-core/src/core/pr-create.ts
20768
+ function whichOnPath(tool) {
20769
+ const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
20770
+ for (const dir of (process.env.PATH ?? "").split(path24.delimiter)) {
20771
+ if (!dir)
20772
+ continue;
20773
+ for (const name of names) {
20774
+ const candidate = path24.join(dir, name);
20775
+ try {
20776
+ fs17.accessSync(candidate, fs17.constants.X_OK);
20777
+ return candidate;
20778
+ } catch {}
20779
+ }
20780
+ }
20781
+ return null;
20782
+ }
20783
+ var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
20784
+ var pushRemote = (cwd) => {
20785
+ const result = spawnSync14("git", ["remote", "get-url", "--push", "--all", "origin"], {
20786
+ cwd,
20787
+ encoding: "utf8"
20788
+ });
20789
+ if (result.status !== 0)
20790
+ return null;
20791
+ const urls = (result.stdout ?? "").split(/\r?\n/u).map((url) => url.trim()).filter(Boolean);
20792
+ return urls.length === 1 ? urls[0] : null;
20793
+ };
20794
+
20795
+ // packages/workit-core/src/core/tracker-issues.ts
20582
20796
  var repoPathFromRemote = (remote) => {
20583
20797
  let rest = (remote || "").trim().replace(/\/+$/, "");
20584
20798
  if (!rest)
@@ -21404,6 +21618,53 @@ var shellPolicy = (cwd, command, dialect = "posix") => {
21404
21618
  }
21405
21619
  return policy && !policy.ok ? { kind: "deny", reason: `branch_policy_denied: ${policy.error}`, unblock: null } : { kind: "none" };
21406
21620
  };
21621
+ // packages/workit-core/src/hooks/delivery-verbs.ts
21622
+ var WORKIT_ALLOWED_VERBS = [
21623
+ "git",
21624
+ "pr",
21625
+ "ci",
21626
+ "stack",
21627
+ "release",
21628
+ "task",
21629
+ "ledger",
21630
+ "evidence",
21631
+ "finding",
21632
+ "decision",
21633
+ "worker",
21634
+ "verify-delivery",
21635
+ "policy",
21636
+ "doctor",
21637
+ "help"
21638
+ ];
21639
+ var WORKIT_BYPASS_FLAGS = ["--unverified", "--overwrite-unintegrated"];
21640
+ var ALLOWED = new Set(WORKIT_ALLOWED_VERBS);
21641
+ var isBypass = (word) => WORKIT_BYPASS_FLAGS.some((flag) => word === flag || word.startsWith(`${flag}=`));
21642
+ var isWorkitWord = (word) => /^(?:.*[\\/])?workit(?:-cli)?$/u.test(word);
21643
+ var bypassReason = (flag) => `Workit: \`${flag}\` overrides a Workit gate, so the user decides this call (no allow rule covers it).`;
21644
+ var EXPANSION_REASON = "Workit: an expansion before `--` could become a bypass flag, so the user decides this call (no allow rule covers it).";
21645
+ var workitBypassFlag = (command, dialect = "posix") => {
21646
+ for (const segment of segmentsOf(command, dialect)) {
21647
+ if (segment.unanalyzable) {
21648
+ const text = command.replaceAll(/['"\\`]/gu, "");
21649
+ if (!/\bworkit\b/u.test(text))
21650
+ return null;
21651
+ const found = WORKIT_BYPASS_FLAGS.find((flag) => text.includes(flag));
21652
+ return found ? bypassReason(found) : null;
21653
+ }
21654
+ const start = segment.words.findIndex(isWorkitWord);
21655
+ if (start < 0)
21656
+ continue;
21657
+ const words = segment.words.slice(start + 1);
21658
+ const end = words.indexOf("--");
21659
+ const flag = (end < 0 ? words : words.slice(0, end)).find(isBypass);
21660
+ if (flag)
21661
+ return bypassReason(flag.split("=")[0] ?? flag);
21662
+ const limit = start + 1 + (end < 0 ? words.length : end);
21663
+ if (segment.expands?.some((index) => index > start && index < limit))
21664
+ return EXPANSION_REASON;
21665
+ }
21666
+ return null;
21667
+ };
21407
21668
  // packages/workit-core/src/core/heavy-rules.ts
21408
21669
  import fs20 from "node:fs";
21409
21670
  import os6 from "node:os";
@@ -22788,7 +23049,7 @@ var segmentTargets = (cwd, command, dialect, depth = 0, strict = false) => {
22788
23049
  }
22789
23050
  const inner = depth < 4 ? wrappedScript(words) ?? (strict ? shellScript2(words) : null) : null;
22790
23051
  if (inner !== null && dir !== undefined)
22791
- targets.push(...segmentTargets(dir, inner, "posix", depth + 1, strict));
23052
+ targets.push(...segmentTargets(workitRunDir(dir, words), inner, "posix", depth + 1, strict));
22792
23053
  if (strict && dir !== undefined) {
22793
23054
  const runIn = dir;
22794
23055
  for (const sub of depth < 4 ? segment.subs ?? [] : [])
@@ -22828,6 +23089,14 @@ var segmentTargets = (cwd, command, dialect, depth = 0, strict = false) => {
22828
23089
  }
22829
23090
  return targets;
22830
23091
  };
23092
+ var workitRunDir = (dir, words) => {
23093
+ for (const invocation of workitInvocations(words.map(quoteWord2).join(" "), "posix")) {
23094
+ const { at, args } = workitCwd(dir, invocation);
23095
+ if (args[0] === "check" || args[0] === "lock" && args[1] === "run")
23096
+ return at;
23097
+ }
23098
+ return dir;
23099
+ };
22831
23100
  var workitCwd = (dir, args) => {
22832
23101
  const { rest, cwd } = skipWorkitGlobals(args);
22833
23102
  return { at: cwd === null ? dir : path32.resolve(dir, homeExpanded(cwd)), args: rest };
@@ -23575,8 +23844,13 @@ var standingDelta = (input) => {
23575
23844
  };
23576
23845
  var sinceStart = new Map;
23577
23846
  // packages/workit-core/src/hooks/cwd-normalize.ts
23578
- var COMPOUND = /^\s*cd\s+([A-Za-z0-9_./@%+:,=-]+|'[^'$`\\\n]*'|"[^"$`\\!\n]*")\s*&&\s*(workit(?:\s[\s\S]*)?)$/u;
23847
+ var DIR = String.raw`[A-Za-z0-9_./@%+:,=-]+|'[^'$${"`"}\\\n]*'|"[^"$${"`"}\\!\n]*"`;
23848
+ var COMPOUND = new RegExp(String.raw`^\s*cd\s+(${DIR})\s*&&\s*(workit(?:\s[\s\S]*)?)$`, "u");
23849
+ var GLOBALS_FIRST = new RegExp(String.raw`^(\s*workit)((?:\s+(?:--json|--cwd(?:=|\s+)(?:${DIR})))+)\s+([a-z][a-z-]*)(?=\s|$)`, "u");
23850
+ var VERB_FIRST = /^((?:--json\s+)*)([a-z][a-z-]*)(?=\s|$)/u;
23851
+ var VERBS = new Set(WORKIT_ALLOWED_VERBS);
23579
23852
  var FILTERS = new Set(["tail", "head", "grep"]);
23853
+ var clean = (s) => !s.unanalyzable && !s.group && !s.subs?.length && !s.stdin?.length && !s.redirects.length;
23580
23854
  var stdinOnly = (words) => {
23581
23855
  const [name, ...args] = words;
23582
23856
  const operands = args.filter((w) => !w.startsWith("-"));
@@ -23598,14 +23872,28 @@ var cwdRewrite = (command) => {
23598
23872
  return null;
23599
23873
  if (head.words.some((w) => w === "--cwd" || w.startsWith("--cwd=")))
23600
23874
  return null;
23601
- const clean = (s) => !s.unanalyzable && !s.group && !s.subs?.length && !s.stdin?.length && !s.redirects.length;
23602
23875
  if (!segments.every(clean))
23603
23876
  return null;
23604
23877
  if (!segments.slice(0, -1).every((s) => s.pipe))
23605
23878
  return null;
23606
23879
  if (!filters.every((s) => FILTERS.has(s.words[0] ?? "") && stdinOnly(s.words)))
23607
23880
  return null;
23608
- return `workit --cwd ${dir} ${rest.slice("workit".length).trimStart()}`.trimEnd();
23881
+ const args = rest.slice("workit".length).trimStart();
23882
+ const verb = VERB_FIRST.exec(args);
23883
+ if (!verb?.[2] || !VERBS.has(verb[2]))
23884
+ return `workit --cwd ${dir} ${args}`.trimEnd();
23885
+ const json = verb[1] ? " --json" : "";
23886
+ return `workit ${verb[2]} --cwd ${dir}${json}${args.slice(verb[0].length)}`.trimEnd();
23887
+ };
23888
+ var verbFirst = (command) => {
23889
+ const match = GLOBALS_FIRST.exec(command);
23890
+ const [whole, lead, globals, verb] = match ?? [];
23891
+ if (!whole || !verb || !VERBS.has(verb) || !globals?.includes("--cwd"))
23892
+ return null;
23893
+ const segments = segmentsOf(command);
23894
+ if (segments.length !== 1 || !segments.every(clean))
23895
+ return null;
23896
+ return `${lead} ${verb}${globals}${command.slice(whole.length)}`;
23609
23897
  };
23610
23898
 
23611
23899
  // packages/workit-core/src/hooks/worktree-confine.ts
@@ -23721,7 +24009,7 @@ var hostIsolated = (cwd, agentId) => {
23721
24009
  };
23722
24010
  var shown = (target) => path34.basename(target) === ".workit-live-target" ? path34.dirname(target) : target;
23723
24011
  var unboundDeny = (input, target) => {
23724
- 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.";
24012
+ 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.";
23725
24013
  return {
23726
24014
  kind: "deny",
23727
24015
  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}`,
@@ -23729,7 +24017,7 @@ var unboundDeny = (input, target) => {
23729
24017
  };
23730
24018
  };
23731
24019
  var outsideDeny = (input, worktree, target) => {
23732
- 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.`;
24020
+ 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.`;
23733
24021
  return {
23734
24022
  kind: "deny",
23735
24023
  reason: `workit worktree confinement: implementer ${input.session.agentId} is bound to the worktree ${worktree}, and ${shown(target)} is outside it. ${unblock}`,
@@ -23738,7 +24026,7 @@ var outsideDeny = (input, worktree, target) => {
23738
24026
  };
23739
24027
  var unknownDirDeny = (input, worktree) => {
23740
24028
  const wt = worktree ?? "<worktree>";
23741
- const unblock = `Workit cannot resolve \`cd -\`, \`cd "$X"\` or \`popd\`: write by absolute path, \`git -C ${wt}\` or \`workit --cwd ${wt}\`.`;
24029
+ const unblock = `Workit cannot resolve \`cd -\`, \`cd "$X"\` or \`popd\`: write by absolute path, \`git -C ${wt}\` or \`workit <verb> --cwd ${wt}\`.`;
23742
24030
  return {
23743
24031
  kind: "deny",
23744
24032
  reason: `workit worktree confinement: implementer ${input.session.agentId} writes after an unknown directory change. ${unblock}`,
@@ -23896,7 +24184,7 @@ var subagentStartText = (input, descriptor, event) => {
23896
24184
  return `${isolatedGit(input.cwd)}
23897
24185
  ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
23898
24186
  if (role === "implementer")
23899
- 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.`;
24187
+ 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.`;
23900
24188
  const readOnly = `Workit observed ${who} as read-only/agent-guided.`;
23901
24189
  if (role !== "judge" || !event.agentId)
23902
24190
  return readOnly;
@@ -23913,7 +24201,7 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
23913
24201
  var PRE_TOOL = new Set(["shell.pre", "write.pre", "tool.pre"]);
23914
24202
  function handleHook(input, deps) {
23915
24203
  const decision = decide(input, deps);
23916
- if (deps.descriptor.transport !== "hook-process" || !PRE_TOOL.has(input.event.kind) || decision.kind === "deny")
24204
+ if (deps.descriptor.transport !== "hook-process" || !PRE_TOOL.has(input.event.kind) || decision.kind === "deny" || decision.kind === "ask")
23917
24205
  return decision;
23918
24206
  const delta = standingDelta(input);
23919
24207
  if (!delta)
@@ -23980,9 +24268,12 @@ function decide(input, deps) {
23980
24268
  if (plan.kind === "rewrite" && !rewrites && canDeny)
23981
24269
  return heavyDenial(event.command, plan);
23982
24270
  const lease = plan.kind === "rewrite" && rewrites ? plan : null;
23983
- const cwdCommand = !lease && rewrites && input.host === "claude_code" ? cwdRewrite(event.command) : null;
24271
+ const cwdCommand = !lease && rewrites && input.host === "claude_code" ? cwdRewrite(event.command) ?? verbFirst(event.command) : null;
23984
24272
  const aside = plan.kind === "passthrough" ? plan.note : null;
23985
24273
  noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
24274
+ const bypass = !lease && input.host === "claude_code" ? workitBypassFlag(event.command, event.dialect) : null;
24275
+ if (bypass)
24276
+ return { kind: "ask", reason: bypass };
23986
24277
  const rewrittenTo = lease?.command ?? cwdCommand;
23987
24278
  const rewritten = (text) => rewrittenTo ? {
23988
24279
  kind: "rewrite",
@@ -24410,6 +24701,92 @@ var cancelHint = (workerId) => `cancel it first: workit_worker {"action":"cancel
24410
24701
  import { spawn as nodeSpawn } from "node:child_process";
24411
24702
  import { randomUUID as randomUUID4 } from "node:crypto";
24412
24703
 
24704
+ // packages/workit-core/src/core/role-models.ts
24705
+ var MODEL_ROLES = [
24706
+ "decider",
24707
+ "implementer",
24708
+ "verifier",
24709
+ "reviewer",
24710
+ "explore",
24711
+ "babysit"
24712
+ ];
24713
+ var MODEL_TIERS = ["mundane", "standard", "hard"];
24714
+ var HOST_KEYS = new Set([
24715
+ "claude_code",
24716
+ "opencode",
24717
+ "codex",
24718
+ "codex_cli",
24719
+ "codex_desktop",
24720
+ "cursor",
24721
+ "pi",
24722
+ "default"
24723
+ ]);
24724
+ var BUILT_IN_CLAUDE_CODE = {
24725
+ decider: { model: "fable", effort: "high" },
24726
+ "implementer.hard": { model: "opus" },
24727
+ "implementer.standard": { model: "sonnet" },
24728
+ "implementer.mundane": { model: "haiku" },
24729
+ verifier: { model: "sonnet" },
24730
+ reviewer: { model: "opus" },
24731
+ explore: { model: "haiku" },
24732
+ babysit: { model: "haiku" }
24733
+ };
24734
+ var isObject7 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
24735
+ var name = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
24736
+ var leaf = (value) => {
24737
+ const text = name(value);
24738
+ if (text)
24739
+ return { model: text, effort: null };
24740
+ if (!isObject7(value))
24741
+ return null;
24742
+ const model = name(value.model);
24743
+ return model ? { model, effort: name(value.effort) } : null;
24744
+ };
24745
+ var isFlat = (value) => typeof value === "string" || isObject7(value) && Object.hasOwn(value, "model");
24746
+ var forHost = (value, host) => {
24747
+ if (isFlat(value))
24748
+ return leaf(value);
24749
+ if (!isObject7(value))
24750
+ return null;
24751
+ const family = host ? hostFamily(host) : null;
24752
+ for (const key of [family, host, "default"])
24753
+ if (key && Object.hasOwn(value, key)) {
24754
+ const found = leaf(value[key]);
24755
+ if (found)
24756
+ return found;
24757
+ }
24758
+ return null;
24759
+ };
24760
+ var rolesOf = (models) => isObject7(models) && isObject7(models.roles) ? models.roles : null;
24761
+ var resolveRoleModel = (cwd, selector, host, dir) => {
24762
+ const { role, tier } = selector;
24763
+ for (const source of modelSources(cwd, dir)) {
24764
+ const where = source.global ? "the global default" : `workspace ${source.name}`;
24765
+ const roles = rolesOf(source.models);
24766
+ const configured = roles && Object.hasOwn(roles, role) ? roles[role] : undefined;
24767
+ const fromRoles = role === "implementer" ? tier && isObject7(configured) && Object.hasOwn(configured, tier) ? forHost(configured[tier], host) : null : forHost(configured, host);
24768
+ if (fromRoles)
24769
+ return { ...fromRoles, source: `models.roles in ${where}` };
24770
+ if (role === "implementer" && tier && isObject7(source.tierModels)) {
24771
+ const fromTiers = Object.hasOwn(source.tierModels, tier) ? forHost(source.tierModels[tier], host) : null;
24772
+ if (fromTiers)
24773
+ return { model: fromTiers.model, effort: null, source: `fanout.tierModels in ${where}` };
24774
+ }
24775
+ }
24776
+ if (host && hostFamily(host) === "claude_code") {
24777
+ const builtIn = BUILT_IN_CLAUDE_CODE[tier ? `${role}.${tier}` : role];
24778
+ if (builtIn)
24779
+ return { model: builtIn.model, effort: builtIn.effort ?? null, source: "built-in" };
24780
+ }
24781
+ return null;
24782
+ };
24783
+ var uniformImplementerModel = (cwd, host, dir) => {
24784
+ const tiers = MODEL_TIERS.map((tier) => resolveRoleModel(cwd, { role: "implementer", tier }, host, dir));
24785
+ const [first] = tiers;
24786
+ return first && tiers.every((t) => t?.model === first.model && t.effort === first.effort) ? first : null;
24787
+ };
24788
+ var ROLE_SELECTORS = MODEL_ROLES.flatMap((role) => role === "implementer" ? MODEL_TIERS.map((tier) => ({ role, tier })) : [{ role }]);
24789
+
24413
24790
  // packages/workit-pi/src/worker-protocol.ts
24414
24791
  var MAX_WORKER_LINE_BYTES = 64 * 1024;
24415
24792
  var MAX_WORKER_STDERR_BYTES = 64 * 1024;
@@ -24481,7 +24858,9 @@ var roleTools = (role) => {
24481
24858
  return [...readOnly, "write", "edit", ...familyTools];
24482
24859
  return [...readOnly, ...familyTools];
24483
24860
  };
24861
+ var roleModel = (cwd, role) => role === "implementer" ? uniformImplementerModel(cwd, "pi") : resolveRoleModel(cwd, { role: role === "investigator" ? "explore" : "reviewer" }, "pi");
24484
24862
  function workerCommand2(runtime, assignment) {
24863
+ const configured = roleModel(runtime.root, assignment.role);
24485
24864
  return {
24486
24865
  command: runtime.node,
24487
24866
  args: [
@@ -24492,6 +24871,8 @@ function workerCommand2(runtime, assignment) {
24492
24871
  ...runtime.sessionId ? ["--session-id", runtime.sessionId] : [],
24493
24872
  "--approve",
24494
24873
  "--offline",
24874
+ ...configured ? ["--model", configured.model] : [],
24875
+ ...configured?.effort ? ["--thinking", configured.effort] : [],
24495
24876
  "--no-context-files",
24496
24877
  "--tools",
24497
24878
  roleTools(assignment.role).join(","),