@brainervirus/workit-pi 8.13.0 → 8.14.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/worker.js CHANGED
@@ -8537,6 +8537,8 @@ var workspaceConfigSchema = object({
8537
8537
  defaultEndpoint: unknown().optional(),
8538
8538
  verification: _enum(["self", "independent"]).optional(),
8539
8539
  trailers: boolean2().optional(),
8540
+ liveSessionGuard: boolean2().optional(),
8541
+ fanout: object({ tierModels: unknown().optional() }).passthrough().optional(),
8540
8542
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
8541
8543
  kind,
8542
8544
  union([boolean2(), literal("verified")]).optional()
@@ -8581,6 +8583,17 @@ var mainCheckouts = new Map;
8581
8583
  var runtimeWorkspaceIndexSchema = object({
8582
8584
  workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
8583
8585
  }).passthrough();
8586
+ var TIER_KEYS = new Set(["mundane", "standard", "hard"]);
8587
+ var TIER_HOSTS = new Set([
8588
+ "claude_code",
8589
+ "opencode",
8590
+ "codex",
8591
+ "codex_cli",
8592
+ "codex_desktop",
8593
+ "cursor",
8594
+ "pi",
8595
+ "default"
8596
+ ]);
8584
8597
 
8585
8598
  // packages/workit-core/src/core/release-tracks.ts
8586
8599
  var KNOWN_FIELDS = new Set([
@@ -8616,6 +8629,23 @@ var NETWORK_FS_TYPES = new Set([
8616
8629
  198183888
8617
8630
  ]);
8618
8631
  var TAIL_BYTES = 64 * 1024;
8632
+ var TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
8633
+ var tamperCache = new Map;
8634
+ var TRUST_REASONS = new Set([
8635
+ "unsigned",
8636
+ "hash_mismatch",
8637
+ "broken_link",
8638
+ "duplicate_id",
8639
+ "unissued_session",
8640
+ "unbound_session"
8641
+ ]);
8642
+ var SHELL_VOUCHED_HOSTS = new Set([
8643
+ "cli",
8644
+ "opencode",
8645
+ "pi",
8646
+ "codex_cli",
8647
+ "codex_desktop"
8648
+ ]);
8619
8649
  var OBSERVED_ONLY = new Set([
8620
8650
  "check",
8621
8651
  "merge.unverified",
package/dist/workit.js CHANGED
@@ -9597,7 +9597,7 @@ function acquireFileLockSync(targetPath, options) {
9597
9597
  }
9598
9598
  }
9599
9599
  // packages/workit-core/src/host-session.ts
9600
- import { createHash as createHash3 } from "node:crypto";
9600
+ import { createHash as createHash3, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
9601
9601
  import fs5 from "node:fs";
9602
9602
  import path5 from "node:path";
9603
9603
 
@@ -9969,9 +9969,10 @@ function hostSessionFromEnv(env) {
9969
9969
  return inner ? { host: inner.host, session: inner.session } : { host: declared, session: null };
9970
9970
  }
9971
9971
  var ISSUED_PER_LEAD = 64;
9972
- var ISSUED_SESSIONS_MARKER = "workit-issued-sessions/2";
9972
+ var ISSUED_SESSIONS_MARKER = "workit-issued-sessions/3";
9973
9973
  var hash = (value) => createHash3("sha256").update(value).digest("hex").slice(0, 32);
9974
9974
  var userIssuedRoot = () => path5.join(resolveStateDir(), "issued");
9975
+ var userTokenRoot = () => path5.join(resolveStateDir(), "issued-tokens");
9975
9976
  function migrateRepoIssued(cwd) {
9976
9977
  if (cwd === undefined)
9977
9978
  return;
@@ -9994,7 +9995,12 @@ function migrateRepoIssued(cwd) {
9994
9995
  const to = path5.join(userIssuedRoot(), lead);
9995
9996
  fs5.mkdirSync(to, { recursive: true, mode: 448 });
9996
9997
  for (const id of fs5.readdirSync(from)) {
9997
- fs5.copyFileSync(path5.join(from, id), path5.join(to, id));
9998
+ try {
9999
+ fs5.copyFileSync(path5.join(from, id), path5.join(to, id), fs5.constants.COPYFILE_EXCL);
10000
+ } catch (error) {
10001
+ if (error.code !== "EEXIST")
10002
+ throw error;
10003
+ }
9998
10004
  fs5.rmSync(path5.join(from, id), { force: true });
9999
10005
  }
10000
10006
  removeIfEmpty(from);
@@ -10014,21 +10020,71 @@ var removeIfEmpty = (dir) => {
10014
10020
  fs5.rmdirSync(dir);
10015
10021
  } catch {}
10016
10022
  };
10023
+ var tokenHash = (token) => createHash3("sha256").update(token).digest("hex");
10017
10024
  function recordIssuedSession(cwd, lead, issued) {
10025
+ const token = randomBytes2(18).toString("base64url");
10018
10026
  try {
10019
10027
  migrateRepoIssued(cwd);
10028
+ const own = hash(issued);
10029
+ const tokens = path5.join(userTokenRoot(), hash(lead));
10030
+ fs5.mkdirSync(tokens, { recursive: true, mode: 448 });
10031
+ fs5.writeFileSync(path5.join(tokens, own), `${tokenHash(token)}
10032
+ `, { mode: 384, flag: "wx" });
10020
10033
  const dir = path5.join(userIssuedRoot(), hash(lead));
10021
10034
  fs5.mkdirSync(dir, { recursive: true, mode: 448 });
10022
- const own = hash(issued);
10023
- fs5.writeFileSync(path5.join(dir, own), ISSUED_SESSIONS_MARKER);
10035
+ fs5.writeFileSync(path5.join(dir, own), `${ISSUED_SESSIONS_MARKER}
10036
+ `, { mode: 384 });
10037
+ pruneIssued(dir, own, tokens);
10038
+ return token;
10039
+ } catch {
10040
+ return null;
10041
+ }
10042
+ }
10043
+ function pruneIssued(dir, own, tokens) {
10044
+ try {
10024
10045
  const entries = fs5.readdirSync(dir);
10025
10046
  if (entries.length <= ISSUED_PER_LEAD)
10026
10047
  return;
10027
10048
  const byAge = entries.filter((name) => name !== own).map((name) => ({ name, at: fs5.statSync(path5.join(dir, name)).mtimeMs })).toSorted((a, b) => a.at - b.at);
10028
- for (const { name } of byAge.slice(0, entries.length - ISSUED_PER_LEAD))
10049
+ for (const { name } of byAge.slice(0, entries.length - ISSUED_PER_LEAD)) {
10029
10050
  fs5.rmSync(path5.join(dir, name), { force: true });
10051
+ fs5.rmSync(path5.join(tokens, name), { force: true });
10052
+ }
10030
10053
  } catch {}
10031
10054
  }
10055
+ var issuedRecord = (cwd, claimed) => {
10056
+ const cut = claimed.lastIndexOf(":");
10057
+ if (cut <= 0)
10058
+ return null;
10059
+ migrateRepoIssued(cwd);
10060
+ try {
10061
+ return fs5.readFileSync(path5.join(userIssuedRoot(), hash(claimed.slice(0, cut)), hash(claimed)), "utf8");
10062
+ } catch {
10063
+ return null;
10064
+ }
10065
+ };
10066
+ var TOKEN_HASH = /^([0-9a-f]{64})$/mu;
10067
+ var issuedToken = (claimed) => {
10068
+ const cut = claimed.lastIndexOf(":");
10069
+ if (cut <= 0)
10070
+ return { stored: null, leadBound: false };
10071
+ const dir = path5.join(userTokenRoot(), hash(claimed.slice(0, cut)));
10072
+ try {
10073
+ const text = fs5.readFileSync(path5.join(dir, hash(claimed)), "utf8");
10074
+ return { stored: TOKEN_HASH.exec(text)?.[1] ?? null, leadBound: true };
10075
+ } catch {
10076
+ return { stored: null, leadBound: safeList(dir).length > 0 };
10077
+ }
10078
+ };
10079
+ function issuedBinding(cwd, claimed) {
10080
+ const record = issuedRecord(cwd, claimed);
10081
+ const { stored, leadBound } = issuedToken(claimed);
10082
+ if (stored)
10083
+ return "token";
10084
+ if (record === null)
10085
+ return null;
10086
+ return leadBound ? "token" : "legacy";
10087
+ }
10032
10088
  var OWN_CHILD_SHELL = new Set(["opencode", "pi"]);
10033
10089
  var SESSION_UNSAFE = /[^A-Za-z0-9_.:@/+-]/g;
10034
10090
  function childSessionId(lead, child) {
@@ -10070,8 +10126,10 @@ var metadataLockSchema = object({
10070
10126
  var FOREIGN_LOCK_TTL_MS = 10 * 60000;
10071
10127
  var UNREADABLE_LOCK_TTL_MS = 30000;
10072
10128
  var RECLAIM_GUARD_TTL_MS = 30000;
10073
- var defaultLockTimeoutMs = 250;
10129
+ var IN_PROCESS_LOCK_TIMEOUT_MS = 250;
10130
+ var defaultLockTimeoutMs = IN_PROCESS_LOCK_TIMEOUT_MS;
10074
10131
  var defaultLockTimeout = () => defaultLockTimeoutMs;
10132
+ var hostAwareLockTimeout = (cliMs) => defaultLockTimeoutMs > IN_PROCESS_LOCK_TIMEOUT_MS ? cliMs : IN_PROCESS_LOCK_TIMEOUT_MS;
10075
10133
  var parseMetadataLock = (raw) => {
10076
10134
  let value;
10077
10135
  try {
@@ -12740,7 +12798,7 @@ import path15 from "node:path";
12740
12798
 
12741
12799
  // packages/workit-core/src/ledger.ts
12742
12800
  import { spawnSync as spawnSync9 } from "node:child_process";
12743
- import { createHash as createHash9, randomBytes as randomBytes2 } from "node:crypto";
12801
+ import { createHash as createHash9, randomBytes as randomBytes3 } from "node:crypto";
12744
12802
  import fs12 from "node:fs";
12745
12803
  import path14 from "node:path";
12746
12804
 
@@ -13109,6 +13167,8 @@ var workspaceConfigSchema = object({
13109
13167
  defaultEndpoint: unknown().optional(),
13110
13168
  verification: _enum(["self", "independent"]).optional(),
13111
13169
  trailers: boolean2().optional(),
13170
+ liveSessionGuard: boolean2().optional(),
13171
+ fanout: object({ tierModels: unknown().optional() }).passthrough().optional(),
13112
13172
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
13113
13173
  kind,
13114
13174
  union([boolean2(), literal("verified")]).optional()
@@ -13426,24 +13486,39 @@ var resolveRuntimeReleaseTracks = (cwd) => {
13426
13486
  const matched = matchRuntimeWorkspace(cwd);
13427
13487
  return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
13428
13488
  };
13429
- var workspaceTrailers = (cwd, dir) => {
13489
+ var inheritedSetting = (cwd, dir, has) => {
13430
13490
  let matched;
13431
13491
  try {
13432
13492
  matched = matchRuntimeWorkspace(cwd, dir);
13433
13493
  } catch {
13434
- return { enabled: true, workspace: null, from: null };
13494
+ return null;
13435
13495
  }
13436
13496
  if (!matched)
13437
- return { enabled: true, workspace: null, from: null };
13497
+ return null;
13438
13498
  const { selected, matches } = matched;
13439
13499
  const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
13440
- const source = [selected, ...enclosing].find((entry) => typeof entry.trailers === "boolean");
13500
+ return { workspace: selected.name, source: [selected, ...enclosing].find(has) ?? null };
13501
+ };
13502
+ var workspaceTrailers = (cwd, dir) => {
13503
+ const found = inheritedSetting(cwd, dir, (entry) => typeof entry.trailers === "boolean");
13441
13504
  return {
13442
- enabled: source?.trailers !== false,
13443
- workspace: selected.name,
13444
- from: source?.name ?? null
13505
+ enabled: found?.source?.trailers !== false,
13506
+ workspace: found?.workspace ?? null,
13507
+ from: found?.source?.name ?? null
13445
13508
  };
13446
13509
  };
13510
+ var workspaceLiveSessionGuard = (cwd, dir) => inheritedSetting(cwd, dir, (entry) => typeof entry.liveSessionGuard === "boolean")?.source?.liveSessionGuard !== false;
13511
+ var TIER_KEYS = new Set(["mundane", "standard", "hard"]);
13512
+ var TIER_HOSTS = new Set([
13513
+ "claude_code",
13514
+ "opencode",
13515
+ "codex",
13516
+ "codex_cli",
13517
+ "codex_desktop",
13518
+ "cursor",
13519
+ "pi",
13520
+ "default"
13521
+ ]);
13447
13522
  var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
13448
13523
  const matched = matchRuntimeWorkspace(cwd);
13449
13524
  if (!matched)
@@ -14289,7 +14364,7 @@ function ledgerPath(cwd) {
14289
14364
  const root = storeRoot(cwd);
14290
14365
  return root.ok ? { ok: true, value: path14.join(root.value.root, "ledger", "ledger.jsonl") } : root;
14291
14366
  }
14292
- var newId2 = () => `${Date.now().toString(36)}-${randomBytes2(6).toString("hex")}`;
14367
+ var newId2 = () => `${Date.now().toString(36)}-${randomBytes3(6).toString("hex")}`;
14293
14368
  var NETWORK_FS_TYPES = new Set([
14294
14369
  26985,
14295
14370
  20859,
@@ -14339,14 +14414,14 @@ var ledgerLock = {
14339
14414
  return null;
14340
14415
  throw error;
14341
14416
  }
14342
- const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
14417
+ const token = `${process.pid}-${randomBytes3(8).toString("hex")}`;
14343
14418
  fs12.writeFileSync(path14.join(lock, LOCK_OWNER), token);
14344
14419
  return token;
14345
14420
  },
14346
14421
  release(lock, token) {
14347
14422
  if (lockToken(lock) !== token)
14348
14423
  return false;
14349
- const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
14424
+ const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
14350
14425
  try {
14351
14426
  fs12.renameSync(lock, tomb);
14352
14427
  } catch {
@@ -14363,7 +14438,7 @@ var ledgerLock = {
14363
14438
  },
14364
14439
  token: lockToken,
14365
14440
  reap(lock, observed) {
14366
- const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
14441
+ const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
14367
14442
  try {
14368
14443
  fs12.renameSync(lock, tomb);
14369
14444
  } catch {
@@ -14381,7 +14456,7 @@ var ledgerLock = {
14381
14456
  return false;
14382
14457
  }
14383
14458
  };
14384
- function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
14459
+ function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
14385
14460
  const lock = `${file}.lock`;
14386
14461
  const deadline = Date.now() + waitMs;
14387
14462
  let token = null;
@@ -14439,10 +14514,18 @@ var lastLine = (fd, size) => {
14439
14514
  }
14440
14515
  return null;
14441
14516
  };
14517
+ var tighten = (fd) => {
14518
+ try {
14519
+ if ((fs12.fstatSync(fd).mode & 63) !== 0)
14520
+ fs12.fchmodSync(fd, 384);
14521
+ } catch {}
14522
+ };
14442
14523
  function appendLine(file, build, options) {
14443
14524
  const write = () => {
14444
- const fd = fs12.openSync(file, "a+");
14525
+ const fd = fs12.openSync(file, "a+", 384);
14445
14526
  try {
14527
+ if (process.platform !== "win32")
14528
+ tighten(fd);
14446
14529
  const size = fs12.fstatSync(fd).size;
14447
14530
  let fence = "";
14448
14531
  if (size > 0) {
@@ -14537,7 +14620,9 @@ function readLedger(cwd, options = {}) {
14537
14620
  if (!file.ok)
14538
14621
  return file;
14539
14622
  let text = "";
14623
+ let stat = null;
14540
14624
  try {
14625
+ stat = fs12.statSync(file.value);
14541
14626
  text = fs12.readFileSync(file.value, "utf8");
14542
14627
  } catch (error) {
14543
14628
  if (error.code === "ENOENT")
@@ -14557,6 +14642,7 @@ function readLedger(cwd, options = {}) {
14557
14642
  const integrity = { chained: 0, legacy: 0, unverified: [] };
14558
14643
  const earlier = new Set([null]);
14559
14644
  let chainStarted = false;
14645
+ const ids = new Set;
14560
14646
  for (const raw of text.split(`
14561
14647
  `)) {
14562
14648
  const trimmed = raw.trim();
@@ -14590,6 +14676,10 @@ function readLedger(cwd, options = {}) {
14590
14676
  reason = "unsigned";
14591
14677
  else
14592
14678
  rowIntegrity = "legacy";
14679
+ if (options.integrity && !reason && ids.has(parsed.id))
14680
+ reason = "duplicate_id";
14681
+ if (options.integrity)
14682
+ ids.add(parsed.id);
14593
14683
  if (reason)
14594
14684
  rowIntegrity = "unverified";
14595
14685
  const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
@@ -14613,7 +14703,8 @@ function readLedger(cwd, options = {}) {
14613
14703
  seq: rows.length + 1,
14614
14704
  superseded: false,
14615
14705
  supersedeIgnored: false,
14616
- ...rowIntegrity ? { integrity: rowIntegrity } : {}
14706
+ ...rowIntegrity ? { integrity: rowIntegrity } : {},
14707
+ ...reason ? { integrityReason: reason } : {}
14617
14708
  };
14618
14709
  rows.push(row);
14619
14710
  if (!options.integrity)
@@ -14641,18 +14732,48 @@ function readLedger(cwd, options = {}) {
14641
14732
  const byId = new Map;
14642
14733
  for (const row of rows) {
14643
14734
  const target = row.supersedes ? byId.get(row.supersedes) : undefined;
14644
- if (target && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
14735
+ if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
14645
14736
  target.superseded = true;
14646
14737
  else if (row.supersedes)
14647
14738
  row.supersedeIgnored = true;
14648
14739
  if (!byId.has(row.id))
14649
14740
  byId.set(row.id, row);
14650
14741
  }
14742
+ if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
14743
+ tamperCache.set(file.value, {
14744
+ size: stat.size,
14745
+ stamp: fileStamp(stat),
14746
+ breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
14747
+ });
14651
14748
  return {
14652
14749
  ok: true,
14653
14750
  value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
14654
14751
  };
14655
14752
  }
14753
+ var TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
14754
+ var tamperCache = new Map;
14755
+ var fileStamp = (stat) => `${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`;
14756
+ function ledgerTampering(cwd) {
14757
+ const file = ledgerPath(cwd);
14758
+ if (!file.ok)
14759
+ return [];
14760
+ try {
14761
+ const stat = fs12.statSync(file.value);
14762
+ const cached = tamperCache.get(file.value);
14763
+ if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
14764
+ return cached.breaks;
14765
+ } catch {
14766
+ return [];
14767
+ }
14768
+ const read = readLedger(cwd, { integrity: true });
14769
+ return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
14770
+ }
14771
+ function tamperedUnblock(cwd) {
14772
+ const file = ledgerPath(cwd);
14773
+ const where = file.ok ? file.value : "the ledger file";
14774
+ 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`;
14775
+ }
14776
+ 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)}`;
14656
14777
  function verdictBasis(verdict, current) {
14657
14778
  if (verdict.head && verdict.head === current.head)
14658
14779
  return "fresh";
@@ -14660,7 +14781,7 @@ function verdictBasis(verdict, current) {
14660
14781
  return "carried";
14661
14782
  return "stale";
14662
14783
  }
14663
- var isLiveVerdict = (row) => row.type === "verdict" && typeof row.result === "string" && !row.superseded;
14784
+ var isLiveVerdict = (row, retired) => row.type === "verdict" && typeof row.result === "string" && !retired.has(row.id);
14664
14785
  function trailerSessions(cwd, base, head) {
14665
14786
  if (!base || !head || !safeRef(base) || !safeRef(head))
14666
14787
  return [];
@@ -14741,6 +14862,56 @@ var authoredBy = (authors, session) => {
14741
14862
  return true;
14742
14863
  return false;
14743
14864
  };
14865
+ var TRUST_REASONS = new Set([
14866
+ "unsigned",
14867
+ "hash_mismatch",
14868
+ "broken_link",
14869
+ "duplicate_id",
14870
+ "unissued_session",
14871
+ "unbound_session"
14872
+ ]);
14873
+ var SHELL_VOUCHED_HOSTS = new Set([
14874
+ "cli",
14875
+ "opencode",
14876
+ "pi",
14877
+ "codex_cli",
14878
+ "codex_desktop"
14879
+ ]);
14880
+ function sessionTrust(cwd, row) {
14881
+ const session = row.actor.session;
14882
+ if (!session || row.self === true)
14883
+ return null;
14884
+ const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
14885
+ const host = row.actor.host;
14886
+ const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
14887
+ if (!issuedOnly)
14888
+ return null;
14889
+ const binding = issuedBinding(cwd, session);
14890
+ if (binding === null)
14891
+ return "unissued_session";
14892
+ return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
14893
+ }
14894
+ function trustReasons(cwd, row) {
14895
+ if (row.integrity === "unverified")
14896
+ return [row.integrityReason ?? "unsigned"];
14897
+ if (row.integrity !== "verified")
14898
+ return ["unsigned"];
14899
+ const session = sessionTrust(cwd, row);
14900
+ return session ? [session] : [];
14901
+ }
14902
+ function withIntegrity(cwd, rows) {
14903
+ if (rows.every((row) => row.integrity !== undefined))
14904
+ return rows;
14905
+ const read = readLedger(cwd, { integrity: true });
14906
+ const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
14907
+ return rows.map((row) => {
14908
+ if (row.integrity !== undefined)
14909
+ return row;
14910
+ const fresh = bySeq.get(row.seq);
14911
+ return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
14912
+ });
14913
+ }
14914
+ var untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason));
14744
14915
  var independenceReasons = (row, authors) => {
14745
14916
  const reasons = [];
14746
14917
  if (!row.actor.session)
@@ -14751,15 +14922,17 @@ var independenceReasons = (row, authors) => {
14751
14922
  reasons.push("self");
14752
14923
  return reasons;
14753
14924
  };
14754
- function effectiveOf(rows, authors, sameCode, isCurrent) {
14925
+ function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
14755
14926
  let effective = null;
14756
14927
  for (const row of rows) {
14757
14928
  if (!effective) {
14758
14929
  effective = row;
14759
14930
  continue;
14760
14931
  }
14761
- const rowIndependent = independenceReasons(row, authors).length === 0;
14762
- const effIndependent = independenceReasons(effective, authors).length === 0;
14932
+ if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
14933
+ continue;
14934
+ const rowIndependent = reasonsOf(row).length === 0;
14935
+ const effIndependent = reasonsOf(effective).length === 0;
14763
14936
  if (effIndependent && !rowIndependent && isCurrent(effective))
14764
14937
  continue;
14765
14938
  if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
@@ -14773,7 +14946,7 @@ function restackCarries(rows, branch, from, to) {
14773
14946
  return false;
14774
14947
  const edges = new Map;
14775
14948
  for (const row of rows)
14776
- if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
14949
+ if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.integrity === "verified" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
14777
14950
  edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
14778
14951
  const seen = new Set([from]);
14779
14952
  const queue = [from];
@@ -14789,7 +14962,8 @@ function restackCarries(rows, branch, from, to) {
14789
14962
  }
14790
14963
  return false;
14791
14964
  }
14792
- function checkVerdicts(cwd, branch, rows) {
14965
+ function checkVerdicts(cwd, branch, ledgerRows) {
14966
+ const rows = withIntegrity(cwd, ledgerRows);
14793
14967
  const head = branchHead(cwd, branch);
14794
14968
  const fallbackBase = defaultBase(cwd);
14795
14969
  const scope = branchScope(cwd, branch);
@@ -14819,21 +14993,47 @@ function checkVerdicts(cwd, branch, rows) {
14819
14993
  return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(rows, branch, failed.head, row.head);
14820
14994
  };
14821
14995
  const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
14996
+ const failureAuthorCache = new Map;
14997
+ const failureAuthors = (row) => {
14998
+ let set = failureAuthorCache.get(row.seq);
14999
+ if (!set) {
15000
+ set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
15001
+ failureAuthorCache.set(row.seq, set);
15002
+ }
15003
+ return set;
15004
+ };
15005
+ const reasonCache = new Map;
15006
+ const reasonsOf = (row) => {
15007
+ let reasons = reasonCache.get(row);
15008
+ if (!reasons) {
15009
+ const failing = FAILING.has(String(row.result));
15010
+ reasons = [
15011
+ ...trustReasons(cwd, row),
15012
+ ...independenceReasons(row, failing ? failureAuthors(row) : authors)
15013
+ ];
15014
+ reasonCache.set(row, reasons);
15015
+ }
15016
+ return reasons;
15017
+ };
15018
+ const retired = new Set;
15019
+ for (const row of rows)
15020
+ if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
15021
+ retired.add(row.supersedes);
14822
15022
  const byKind = new Map;
14823
15023
  for (const row of rows)
14824
- if (isLiveVerdict(row) && scope(row)) {
15024
+ if (isLiveVerdict(row, retired) && scope(row)) {
14825
15025
  const kind = typeof row.kind === "string" ? row.kind : "review";
14826
15026
  byKind.set(kind, [...byKind.get(kind) ?? [], row]);
14827
15027
  }
14828
15028
  const verdicts = [];
14829
15029
  const warnings = [];
14830
15030
  for (const [kind, list] of byKind) {
14831
- const row = effectiveOf(list, authors, sameCode, (candidate) => basisOf(candidate) !== "stale");
15031
+ const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
14832
15032
  if (!row)
14833
15033
  continue;
14834
15034
  const basis = basisOf(row);
14835
15035
  const current = basis !== "stale";
14836
- const independence = independenceReasons(row, authors);
15036
+ const independence = reasonsOf(row);
14837
15037
  const reasons = [
14838
15038
  ...current ? [] : ["stale"],
14839
15039
  ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
@@ -14850,23 +15050,31 @@ function checkVerdicts(cwd, branch, rows) {
14850
15050
  });
14851
15051
  if (reasons.length === 0) {
14852
15052
  for (const later of list)
14853
- if (later.seq > row.seq && independenceReasons(later, authors).length > 0 && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
15053
+ if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
14854
15054
  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`);
14855
15055
  }
14856
15056
  }
14857
15057
  for (const row of rows)
14858
15058
  if (row.integrity === "unverified" && scope(row))
14859
15059
  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)`);
15060
+ const tampered = ledgerTampering(cwd);
15061
+ if (tampered.length) {
15062
+ warnings.push(tamperedWarning(cwd, tampered));
15063
+ for (const entry of verdicts) {
15064
+ entry.accepted = false;
15065
+ entry.reasons = [...entry.reasons, "ledger_tampered"];
15066
+ }
15067
+ }
14860
15068
  verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
14861
15069
  const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
14862
15070
  const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
14863
15071
  const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
14864
- const accepted = newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
15072
+ const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
14865
15073
  accepted: false,
14866
15074
  verdict: failing?.verdict ?? null,
14867
15075
  reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
14868
15076
  };
14869
- const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && STRONG_RESULTS.has(String(entry.verdict.result)));
15077
+ const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && !untrusted(entry.reasons) && STRONG_RESULTS.has(String(entry.verdict.result)));
14870
15078
  return {
14871
15079
  branch,
14872
15080
  head,
@@ -21612,27 +21820,231 @@ var shellPolicy = (cwd, command, dialect = "posix") => {
21612
21820
  };
21613
21821
  // packages/workit-core/src/hooks/write-gate.ts
21614
21822
  import { realpathSync as realpathSync6 } from "node:fs";
21823
+ import os7 from "node:os";
21824
+ import path27 from "node:path";
21825
+
21826
+ // packages/workit-core/src/core/live-session.ts
21827
+ import { spawnSync as spawnSync17 } from "node:child_process";
21828
+ import { createHash as createHash15 } from "node:crypto";
21829
+ import fs19 from "node:fs";
21830
+ import os6 from "node:os";
21615
21831
  import path26 from "node:path";
21832
+ var LIVE_SESSION_TTL_MS = 15 * 60000;
21833
+ var HEARTBEAT_EVERY_MS = 30000;
21834
+ var liveProbe = {
21835
+ platform: process.platform,
21836
+ run: (args) => spawnSync17("ps", args, { encoding: "utf8", timeout: 2000 }).stdout ?? "",
21837
+ start: (pid) => processStartOf(pid)
21838
+ };
21839
+ var leadOf = (session) => session.split(":")[0].replace(/-w-.*$/su, "") || session;
21840
+ var familyOf = (host) => host.replace(/_(?:cli|desktop)$/u, "");
21841
+ var processAlive = (pid) => {
21842
+ try {
21843
+ process.kill(pid, 0);
21844
+ return true;
21845
+ } catch (error) {
21846
+ return error.code === "EPERM";
21847
+ }
21848
+ };
21849
+ var processInfo = (pid) => {
21850
+ try {
21851
+ if (liveProbe.platform === "linux") {
21852
+ const stat = fs19.readFileSync(`/proc/${pid}/stat`, "utf8");
21853
+ const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
21854
+ const command = fs19.readFileSync(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ");
21855
+ return Number.isInteger(ppid) ? { ppid, command: command.trim() } : null;
21856
+ }
21857
+ if (liveProbe.platform === "win32")
21858
+ return null;
21859
+ const match = /^\s*(\d+)\s+(.*)$/su.exec(liveProbe.run(["-o", "ppid=,command=", "-p", String(pid)]));
21860
+ return match ? { ppid: Number(match[1]), command: match[2].trim() } : null;
21861
+ } catch {
21862
+ return null;
21863
+ }
21864
+ };
21865
+ var WRAPPER = /^(?:\S*\/)?(?:sh|bash|dash|zsh|ash|ksh|fish|env|timeout|npx|npm|bunx|cmd\.exe|pwsh|powershell)(?:\s|$)|launch\.mjs|npx-cli\.js/u;
21866
+ var PIDS = new Map;
21867
+ var hostPid = (transport) => {
21868
+ if (transport !== "hook-process")
21869
+ return process.pid;
21870
+ const key = `${liveProbe.platform}\x00${process.ppid}`;
21871
+ if (PIDS.has(key))
21872
+ return PIDS.get(key) ?? null;
21873
+ if (liveProbe.platform === "win32") {
21874
+ PIDS.set(key, null);
21875
+ return null;
21876
+ }
21877
+ let pid = process.ppid;
21878
+ for (let hop = 0;hop < 4; hop++) {
21879
+ const info = processInfo(pid);
21880
+ if (!info || !WRAPPER.test(info.command) || info.ppid <= 1)
21881
+ break;
21882
+ pid = info.ppid;
21883
+ }
21884
+ PIDS.set(key, pid);
21885
+ return pid;
21886
+ };
21887
+ var sessionSafe = (value) => value.replace(/[^A-Za-z0-9_.:@/+-]/gu, "-");
21888
+ var liveOwner = (input, transport, env = process.env) => {
21889
+ const session = input.session.id?.trim();
21890
+ if (!session)
21891
+ return null;
21892
+ const leads = new Set([leadOf(sessionSafe(session))]);
21893
+ if (input.session.parentId)
21894
+ leads.add(leadOf(sessionSafe(input.session.parentId)));
21895
+ if (env.WORKIT_PI_WORKER_SESSION && env.WORKIT_SESSION_ID)
21896
+ leads.add(leadOf(sessionSafe(env.WORKIT_SESSION_ID)));
21897
+ return {
21898
+ host: input.host,
21899
+ session: sessionSafe(session),
21900
+ leads: [...leads],
21901
+ pid: () => hostPid(transport)
21902
+ };
21903
+ };
21904
+ var checkoutOf = (target) => {
21905
+ let dir = path26.resolve(target);
21906
+ while (!fs19.existsSync(dir) && path26.dirname(dir) !== dir)
21907
+ dir = path26.dirname(dir);
21908
+ if (fs19.existsSync(dir) && !fs19.statSync(dir).isDirectory())
21909
+ dir = path26.dirname(dir);
21910
+ const parts = dir.split(path26.sep);
21911
+ const dotGit = parts.lastIndexOf(".git");
21912
+ if (dotGit > 0)
21913
+ dir = parts.slice(0, dotGit).join(path26.sep) || path26.sep;
21914
+ const location = resolveStore(dir);
21915
+ if (location instanceof Error || !location.shared || !location.top)
21916
+ return null;
21917
+ let top = location.top;
21918
+ try {
21919
+ top = fs19.realpathSync.native(top);
21920
+ } catch {}
21921
+ return top;
21922
+ };
21923
+ var markerFile2 = (top) => path26.join(resolveStateDir(), "live-sessions", `${createHash15("sha256").update(checkoutKey(top)).digest("hex").slice(0, 32)}.json`);
21924
+ var checkoutKey = (top, platform = process.platform) => {
21925
+ let key = path26.resolve(top);
21926
+ try {
21927
+ key = fs19.realpathSync.native(key);
21928
+ } catch {}
21929
+ return platform === "win32" ? key.replaceAll("/", "\\").toLowerCase() : key;
21930
+ };
21931
+ var readMarker = (file) => {
21932
+ try {
21933
+ const value = JSON.parse(fs19.readFileSync(file, "utf8"));
21934
+ if (value?.version !== 1 || typeof value.session !== "string" || typeof value.host !== "string" || typeof value.heartbeatAt !== "string" || Number.isNaN(Date.parse(value.heartbeatAt)))
21935
+ return null;
21936
+ return {
21937
+ version: 1,
21938
+ session: value.session,
21939
+ leads: Array.isArray(value.leads) ? value.leads.filter((item) => typeof item === "string") : [],
21940
+ host: value.host,
21941
+ pid: typeof value.pid === "number" ? value.pid : null,
21942
+ pidStart: typeof value.pidStart === "string" ? value.pidStart : null,
21943
+ machine: typeof value.machine === "string" ? value.machine : "",
21944
+ checkout: typeof value.checkout === "string" ? value.checkout : "",
21945
+ heartbeatAt: value.heartbeatAt
21946
+ };
21947
+ } catch {
21948
+ return null;
21949
+ }
21950
+ };
21951
+ var ageOf2 = (marker, now) => now.getTime() - Date.parse(marker.heartbeatAt);
21952
+ var isLive = (marker, deps) => {
21953
+ const age = ageOf2(marker, deps.now ?? new Date);
21954
+ if (age > (deps.ttlMs ?? LIVE_SESSION_TTL_MS))
21955
+ return false;
21956
+ if (marker.machine !== (deps.machine ?? os6.hostname()) || marker.pid === null)
21957
+ return true;
21958
+ if (!(deps.alive ?? processAlive)(marker.pid))
21959
+ return false;
21960
+ if (marker.pidStart === null)
21961
+ return true;
21962
+ const start = liveProbe.start(marker.pid);
21963
+ return start === null || start === marker.pidStart;
21964
+ };
21965
+ var sameLead = (marker, owner) => marker.session === owner.session || owner.leads.some((lead) => lead === marker.session || marker.leads.includes(lead));
21966
+ var sameOwner = (marker, owner, machine) => {
21967
+ if (sameLead(marker, owner))
21968
+ return true;
21969
+ if (marker.machine !== machine)
21970
+ return false;
21971
+ const pid = owner.pid();
21972
+ if (pid === null || marker.pid === null || marker.pidStart === null)
21973
+ return familyOf(marker.host) === familyOf(owner.host);
21974
+ return marker.pid === pid;
21975
+ };
21976
+ var liveConflict = (target, owner, deps = {}) => {
21977
+ try {
21978
+ const checkout = checkoutOf(target);
21979
+ if (!checkout)
21980
+ return null;
21981
+ const file = markerFile2(checkout);
21982
+ const marker = readMarker(file);
21983
+ if (!marker || sameLead(marker, owner) || !isLive(marker, deps))
21984
+ return null;
21985
+ if (sameOwner(marker, owner, deps.machine ?? os6.hostname()))
21986
+ return null;
21987
+ return { marker, file, ageMs: ageOf2(marker, deps.now ?? new Date), checkout };
21988
+ } catch {
21989
+ return null;
21990
+ }
21991
+ };
21992
+ var claimLiveSession = (target, owner, deps = {}) => {
21993
+ try {
21994
+ const checkout = checkoutOf(target);
21995
+ if (!checkout)
21996
+ return;
21997
+ const file = markerFile2(checkout);
21998
+ const now = deps.now ?? new Date;
21999
+ const machine = deps.machine ?? os6.hostname();
22000
+ const current = readMarker(file);
22001
+ if (current) {
22002
+ const age = ageOf2(current, now);
22003
+ if (sameLead(current, owner) && age >= 0 && age < HEARTBEAT_EVERY_MS)
22004
+ return;
22005
+ if (!sameLead(current, owner) && isLive(current, deps) && !sameOwner(current, owner, machine))
22006
+ return;
22007
+ }
22008
+ const pid = owner.pid();
22009
+ const marker = {
22010
+ version: 1,
22011
+ session: owner.session,
22012
+ leads: owner.leads,
22013
+ host: owner.host,
22014
+ pid,
22015
+ pidStart: pid === null ? null : liveProbe.start(pid),
22016
+ machine,
22017
+ checkout,
22018
+ heartbeatAt: now.toISOString()
22019
+ };
22020
+ fs19.mkdirSync(path26.dirname(file), { recursive: true });
22021
+ const temp = `${file}.${process.pid}.tmp`;
22022
+ fs19.writeFileSync(temp, JSON.stringify(marker));
22023
+ fs19.renameSync(temp, file);
22024
+ } catch {}
22025
+ };
22026
+
22027
+ // packages/workit-core/src/hooks/write-gate.ts
21616
22028
  var NONE3 = { kind: "none" };
21617
22029
  var CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
21618
22030
  var canonical2 = (file) => {
21619
22031
  let rest = "";
21620
- for (let dir = path26.resolve(file);; dir = path26.dirname(dir)) {
22032
+ for (let dir = path27.resolve(file);; dir = path27.dirname(dir)) {
21621
22033
  try {
21622
- return path26.join(realpathSync6(dir), rest);
22034
+ return path27.join(realpathSync6(dir), rest);
21623
22035
  } catch {
21624
- if (path26.dirname(dir) === dir)
21625
- return path26.resolve(file);
21626
- rest = rest ? path26.join(path26.basename(dir), rest) : path26.basename(dir);
22036
+ if (path27.dirname(dir) === dir)
22037
+ return path27.resolve(file);
22038
+ rest = rest ? path27.join(path27.basename(dir), rest) : path27.basename(dir);
21627
22039
  }
21628
22040
  }
21629
22041
  };
21630
22042
  var exempt = (cwd, root, file, plans) => {
21631
22043
  const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
21632
- const relative = path26.relative(fold(canonical2(root)), fold(canonical2(path26.resolve(cwd, file))));
21633
- if (relative.startsWith("..") || path26.isAbsolute(relative))
22044
+ const relative = path27.relative(fold(canonical2(root)), fold(canonical2(path27.resolve(cwd, file))));
22045
+ if (relative.startsWith("..") || path27.isAbsolute(relative))
21634
22046
  return true;
21635
- const posix = relative.split(path26.sep).join("/");
22047
+ const posix = relative.split(path27.sep).join("/");
21636
22048
  const segments = posix.split("/");
21637
22049
  return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
21638
22050
  };
@@ -21730,7 +22142,7 @@ var shellWrites = (command) => {
21730
22142
  return { writes: true, targets: unknown ? null : targets };
21731
22143
  };
21732
22144
  var cache2 = new Map;
21733
- var planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path26.resolve(root, file)) ?? "-").join("\x00");
22145
+ var planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path27.resolve(root, file)) ?? "-").join("\x00");
21734
22146
  var gateFor = (cwd) => {
21735
22147
  const store = new TaskStore(cwd);
21736
22148
  const listed = store.listTaskIndex();
@@ -21768,7 +22180,218 @@ var gateFor = (cwd) => {
21768
22180
  cache2.delete(cache2.keys().next().value);
21769
22181
  return gate;
21770
22182
  };
21771
- function writeGate(cwd, paths) {
22183
+ var MUTATING_GIT = new Set([
22184
+ "add",
22185
+ "am",
22186
+ "apply",
22187
+ "checkout",
22188
+ "cherry-pick",
22189
+ "clean",
22190
+ "commit",
22191
+ "merge",
22192
+ "mv",
22193
+ "pull",
22194
+ "rebase",
22195
+ "reset",
22196
+ "restore",
22197
+ "revert",
22198
+ "rm",
22199
+ "stash",
22200
+ "switch"
22201
+ ]);
22202
+ var DRY_N = new Set(["add", "clean", "rm", "mv"]);
22203
+ var MUTATING_WORKIT_GIT = new Set(["commit", "branch"]);
22204
+ var DIR_TARGET = ".workit-live-target";
22205
+ var OVERRIDE = "WORKIT_LIVE_SESSION=override";
22206
+ var inert = (args, sub) => args.some((arg) => arg === "--help" || arg === "-h" || arg === "--dry-run" || DRY_N.has(sub) && /^-[a-zA-Z]*n[a-zA-Z]*$/.test(arg) && !arg.startsWith("--"));
22207
+ var gitMutationDirs = (cwd, command, dialect) => {
22208
+ let invocations;
22209
+ try {
22210
+ invocations = rawInvocations(command, dialect);
22211
+ } catch {
22212
+ return [];
22213
+ }
22214
+ return invocations.flatMap((invocation) => {
22215
+ if (invocation.tool !== "git" || invocation.dir === undefined)
22216
+ return [];
22217
+ const at = invocation.args.findIndex((arg) => !arg.startsWith("-"));
22218
+ const sub = invocation.args[at] ?? "";
22219
+ const rest = invocation.args.slice(at + 1);
22220
+ if (!MUTATING_GIT.has(sub) || inert(rest, sub))
22221
+ return [];
22222
+ if (sub === "stash" && ["list", "show"].includes(rest[0] ?? ""))
22223
+ return [];
22224
+ return [path27.resolve(cwd, invocation.dir ?? ".")];
22225
+ });
22226
+ };
22227
+ var CD2 = new Set(["cd", "chdir", "set-location", "sl"]);
22228
+ var PUSHD2 = new Set(["pushd", "push-location"]);
22229
+ var POPD2 = new Set(["popd", "pop-location"]);
22230
+ var cdInto = (dir, words) => {
22231
+ const target = words.slice(1).find((word) => !word.startsWith("-") || word === "-");
22232
+ if (dir === undefined || target === "-" || target && /^~|[$`*?]/.test(target))
22233
+ return;
22234
+ return target === undefined ? os7.homedir() : path27.resolve(dir, target);
22235
+ };
22236
+ var quoteWord = (word) => /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
22237
+ var segmentTargets = (cwd, command, dialect) => {
22238
+ const targets = [];
22239
+ let dir = cwd;
22240
+ let stack = [];
22241
+ const groups = [];
22242
+ const at = (file) => path27.isAbsolute(file) ? file : dir === undefined ? null : path27.resolve(dir, file);
22243
+ for (const segment of segmentsOf(command, dialect)) {
22244
+ if (segment.group === "open") {
22245
+ groups.push({ dir, stack: [...stack] });
22246
+ continue;
22247
+ }
22248
+ if (segment.group === "close") {
22249
+ const saved = groups.pop();
22250
+ if (saved)
22251
+ ({ dir, stack } = saved);
22252
+ continue;
22253
+ }
22254
+ const lead = segment.words.findIndex((word) => !/^\w+=/.test(word));
22255
+ const words = lead < 0 ? [] : segment.words.slice(lead);
22256
+ const name = (words[0]?.split(/[\\/]/).at(-1) ?? "").toLowerCase();
22257
+ if (CD2.has(name)) {
22258
+ dir = cdInto(dir, words);
22259
+ continue;
22260
+ }
22261
+ if (PUSHD2.has(name)) {
22262
+ stack.push(dir);
22263
+ dir = cdInto(dir, words);
22264
+ continue;
22265
+ }
22266
+ if (POPD2.has(name)) {
22267
+ dir = stack.length ? stack.pop() : undefined;
22268
+ continue;
22269
+ }
22270
+ for (const file of segment.redirects)
22271
+ if (!/^\/dev\/(?:null|stdout|stderr|tty)$/.test(file)) {
22272
+ const resolved = at(file);
22273
+ if (resolved)
22274
+ targets.push(resolved);
22275
+ }
22276
+ const edits = editsOf(segment.words);
22277
+ if (edits === null) {
22278
+ if (dir !== undefined)
22279
+ targets.push(path27.join(dir, DIR_TARGET));
22280
+ } else
22281
+ for (const file of edits) {
22282
+ const resolved = at(file);
22283
+ if (resolved)
22284
+ targets.push(resolved);
22285
+ }
22286
+ if (dir !== undefined && workitInvocations(segment.words.map(quoteWord).join(" "), "posix").some((args) => args[0] === "git" && MUTATING_WORKIT_GIT.has(args[1] ?? "") && !inert(args.slice(2), "")))
22287
+ targets.push(path27.join(dir, DIR_TARGET));
22288
+ }
22289
+ return targets;
22290
+ };
22291
+ var overridden = (command, dialect) => {
22292
+ const segments = segmentsOf(command, dialect);
22293
+ if (segments.length !== 1 || segments[0].group || segments[0].subs?.length)
22294
+ return false;
22295
+ const words = segments[0].words;
22296
+ const lead = words.findIndex((word) => !/^\w+=/.test(word));
22297
+ return lead > 0 && words.slice(0, lead).includes(OVERRIDE);
22298
+ };
22299
+ var HOST_LABELS = {
22300
+ claude_code: "Claude Code",
22301
+ opencode: "OpenCode",
22302
+ codex_cli: "Codex CLI",
22303
+ codex_desktop: "Codex Desktop",
22304
+ cursor: "Cursor",
22305
+ pi: "Pi"
22306
+ };
22307
+ var ageText = (ms) => {
22308
+ const seconds = Math.max(0, Math.round(ms / 1000));
22309
+ return seconds < 120 ? `${seconds}s` : `${Math.round(seconds / 60)} min`;
22310
+ };
22311
+ var liveDeny = ({ marker, checkout, ageMs, file }) => {
22312
+ const unblock = `Work in your own worktree (\`git worktree add <path> -b <branch>\`, or \`workit fanout worktree create <slice>\`). Only if the user confirms that session is finished: release it with \`rm '${file}'\`, or prefix one command with ${OVERRIDE}.`;
22313
+ return {
22314
+ kind: "deny",
22315
+ reason: `workit live-session guard: ${checkout} belongs to another live agent session, ${HOST_LABELS[marker.host] ?? marker.host} session ${marker.session}${marker.pid === null ? "" : ` (pid ${marker.pid})`}, last active ${ageText(ageMs)} ago. Do not edit a checkout another session is working in; read-only commands stay allowed. ${unblock}`,
22316
+ unblock
22317
+ };
22318
+ };
22319
+ var guarded = (targets) => {
22320
+ const seen = new Set;
22321
+ return targets.filter((target) => {
22322
+ const dir = path27.dirname(target);
22323
+ if (seen.has(dir))
22324
+ return false;
22325
+ seen.add(dir);
22326
+ return true;
22327
+ });
22328
+ };
22329
+ var liveGate = (owner, targets, deps) => {
22330
+ try {
22331
+ for (const target of guarded(targets)) {
22332
+ const conflict = liveConflict(target, owner, deps);
22333
+ if (conflict && workspaceLiveSessionGuard(conflict.checkout))
22334
+ return liveDeny(conflict);
22335
+ }
22336
+ } catch {}
22337
+ return NONE3;
22338
+ };
22339
+ var claimAll = (owner, targets, deps) => {
22340
+ for (const target of guarded(targets))
22341
+ claimLiveSession(target, owner, deps);
22342
+ };
22343
+ var liveTargets = (cwd, paths) => paths?.length ? paths.map((file) => path27.resolve(cwd, file)) : [path27.join(cwd, DIR_TARGET)];
22344
+ function writeGate(cwd, paths, owner, deps) {
22345
+ if (!owner)
22346
+ return beforeWriteGate(cwd, paths);
22347
+ const targets = liveTargets(cwd, paths);
22348
+ const live = liveGate(owner, targets, deps);
22349
+ if (live.kind !== "none")
22350
+ return live;
22351
+ const decision = beforeWriteGate(cwd, paths);
22352
+ if (decision.kind === "none")
22353
+ claimAll(owner, targets, deps);
22354
+ return decision;
22355
+ }
22356
+ function shellGate(cwd, command, owner, dialect = "posix", deps) {
22357
+ const writes = shellWrites(command);
22358
+ let targets = [];
22359
+ if (owner) {
22360
+ try {
22361
+ targets = [
22362
+ ...segmentTargets(cwd, command, dialect),
22363
+ ...gitMutationDirs(cwd, command, dialect).map((dir) => path27.join(dir, DIR_TARGET))
22364
+ ];
22365
+ } catch {
22366
+ targets = [];
22367
+ }
22368
+ }
22369
+ if (owner && targets.length) {
22370
+ const live = liveGate(owner, targets, deps);
22371
+ if (live.kind !== "none") {
22372
+ if (!overridden(command, dialect))
22373
+ return live;
22374
+ noteOverride(cwd, owner, command);
22375
+ }
22376
+ }
22377
+ const decision = writes.writes ? beforeWriteGate(cwd, writes.targets) : NONE3;
22378
+ if (owner && targets.length && decision.kind === "none")
22379
+ claimAll(owner, targets, deps);
22380
+ return decision;
22381
+ }
22382
+ var noteOverride = (cwd, owner, command) => {
22383
+ try {
22384
+ if (!inWorkitWorkspace(cwd))
22385
+ return;
22386
+ appendHookObserved(cwd, {
22387
+ type: "live_session.override",
22388
+ actor: { host: owner.host, session: owner.session, agentId: null },
22389
+ session: owner.session,
22390
+ command: command.slice(0, 200)
22391
+ });
22392
+ } catch {}
22393
+ };
22394
+ function beforeWriteGate(cwd, paths) {
21772
22395
  try {
21773
22396
  const gate = gateFor(cwd);
21774
22397
  if (!gate || gate.blockers.length === 0)
@@ -21786,7 +22409,7 @@ function writeGate(cwd, paths) {
21786
22409
  }
21787
22410
  }
21788
22411
  // packages/workit-core/src/hooks/stop.ts
21789
- import { spawnSync as spawnSync17 } from "node:child_process";
22412
+ import { spawnSync as spawnSync18 } from "node:child_process";
21790
22413
  var NONE4 = { kind: "none" };
21791
22414
  var QUESTION = /[??؟](?=[\s*_`"')\]>]*(?:\s|$))|¿/;
21792
22415
  var OFFER = /\b(?:let me know|want me to|should I|shall I|would you like|do you want|if you(?:'d| would) like|may I|can I go ahead)\b/i;
@@ -21807,7 +22430,7 @@ var asksUser = (message) => {
21807
22430
  };
21808
22431
  var short = (sha) => (sha ?? "?").slice(0, 12);
21809
22432
  var gitList = (cwd, args) => {
21810
- const run = spawnSync17("git", args, {
22433
+ const run = spawnSync18("git", args, {
21811
22434
  cwd,
21812
22435
  encoding: "utf8",
21813
22436
  timeout: 3000,
@@ -21898,10 +22521,10 @@ function stopObligation(input, descriptor) {
21898
22521
  const branch = currentBranch(input.cwd);
21899
22522
  if (!branch || guardedBranch(input.cwd, branch))
21900
22523
  return null;
21901
- const ledger = readLedger(input.cwd);
22524
+ const ledger = readLedger(input.cwd, { integrity: true });
21902
22525
  if (!ledger.ok)
21903
22526
  return null;
21904
- const all = ledger.value.rows;
22527
+ const all = ledger.value.rows.filter((row) => row.integrity !== "unverified");
21905
22528
  const rows = all.filter((row) => row.branch === branch);
21906
22529
  const mine = rows.filter((row) => sessionOf(row) === session);
21907
22530
  const commits = new Set(mine.filter((row) => row.type === "commit.recorded").map((row) => typeof row.sha === "string" ? row.sha : row.head).filter((sha) => typeof sha === "string"));
@@ -22240,9 +22863,9 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
22240
22863
  if (role !== "judge" || !event.agentId)
22241
22864
  return readOnly;
22242
22865
  const own = subagentSession(session, event.agentId);
22243
- if (session)
22244
- recordIssuedSession(input.cwd || process.cwd(), session, own);
22245
- return `${readOnly} Its own Workit session is ${own}: record verdicts with \`workit ledger verdict <result> --session ${own} ...\` so the ledger tells it apart from the author.`;
22866
+ const token = session ? recordIssuedSession(input.cwd || process.cwd(), session, own) : null;
22867
+ const flags = token ? `--session ${own} --token ${token}` : `--session ${own}`;
22868
+ return `${readOnly} Its own Workit session is ${own}: record verdicts with \`workit ledger verdict <result> ${flags} ...\` so the ledger tells it apart from the author.${token ? " The token is this subagent's alone: never write it into a report, a file or a commit. --token needs a workit CLI at least as new as this plugin; if the CLI answers that --token is an unknown option, update the workit CLI (workit upgrade --cli) and record again." : ""}`;
22246
22869
  };
22247
22870
  function handleHook(input, deps) {
22248
22871
  const { descriptor } = deps;
@@ -22284,8 +22907,7 @@ function handleHook(input, deps) {
22284
22907
  const raw = canDeny ? rawGitPre(input, event.command, { nudge: false }) : NONE5;
22285
22908
  if (raw.kind === "deny")
22286
22909
  return raw;
22287
- const writes = usable2(descriptor.events["write.pre"].support) ? shellWrites(event.command) : { writes: false, targets: null };
22288
- const gate = writes.writes ? writeGate(input.cwd, writes.targets) : NONE5;
22910
+ const gate = usable2(descriptor.events["write.pre"].support) ? shellGate(input.cwd, event.command, liveOwner(input, descriptor.transport), event.dialect) : NONE5;
22289
22911
  if (gate.kind === "deny")
22290
22912
  return gate;
22291
22913
  noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
@@ -22298,7 +22920,7 @@ function handleHook(input, deps) {
22298
22920
  rawGitPost(input, event.command, callKey(event.toolUseId, event.command));
22299
22921
  return NONE5;
22300
22922
  case "write.pre":
22301
- return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths) : NONE5;
22923
+ return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths, liveOwner(input, descriptor.transport)) : NONE5;
22302
22924
  case "subagent.start":
22303
22925
  return {
22304
22926
  kind: "context",
@@ -22602,13 +23224,13 @@ var skillPromptNudge = (prompt, ctx) => process.env.WORKIT_PI_WORKER_SESSION ? n
22602
23224
 
22603
23225
  // packages/workit-pi/src/launches.ts
22604
23226
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
22605
- import os6 from "node:os";
22606
- import path27 from "node:path";
23227
+ import os8 from "node:os";
23228
+ import path28 from "node:path";
22607
23229
  var launchDir = (root) => {
22608
23230
  const location = resolveStore(root);
22609
- return location instanceof Error ? null : path27.join(location.dir, "pi-launches");
23231
+ return location instanceof Error ? null : path28.join(location.dir, "pi-launches");
22610
23232
  };
22611
- var fileFor = (dir, workerId) => path27.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
23233
+ var fileFor = (dir, workerId) => path28.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
22612
23234
  var recordLaunch = (root, attempt) => {
22613
23235
  const dir = launchDir(root);
22614
23236
  if (!dir)
@@ -22618,7 +23240,7 @@ var recordLaunch = (root, attempt) => {
22618
23240
  writeFileSync3(fileFor(dir, attempt.workerId), JSON.stringify({
22619
23241
  ...attempt,
22620
23242
  pid: process.pid,
22621
- host: os6.hostname(),
23243
+ host: os8.hostname(),
22622
23244
  at: new Date().toISOString()
22623
23245
  }), { flag: "wx" });
22624
23246
  return true;
@@ -22632,7 +23254,7 @@ var clearLaunch = (root, workerId) => {
22632
23254
  rmSync2(fileFor(dir, workerId), { force: true });
22633
23255
  };
22634
23256
  var alive = (attempt) => {
22635
- if (attempt.host !== os6.hostname())
23257
+ if (attempt.host !== os8.hostname())
22636
23258
  return true;
22637
23259
  try {
22638
23260
  process.kill(attempt.pid, 0);
@@ -22654,7 +23276,7 @@ var abandonedLaunches = (root) => {
22654
23276
  const found = [];
22655
23277
  for (const name of names) {
22656
23278
  try {
22657
- const attempt = JSON.parse(readFileSync6(path27.join(dir, name), "utf8"));
23279
+ const attempt = JSON.parse(readFileSync6(path28.join(dir, name), "utf8"));
22658
23280
  if (typeof attempt.workerId === "string" && typeof attempt.session === "string") {
22659
23281
  if (!alive(attempt))
22660
23282
  found.push(attempt);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-pi",
3
- "version": "8.13.0",
3
+ "version": "8.14.0",
4
4
  "description": "Workit native extension for Pi — tools, method skills, and session continuity",
5
5
  "keywords": [
6
6
  "agentic-coding",
@@ -25,9 +25,9 @@ ceremony; do it yourself. Workit never spawns agents; you do.
25
25
  it; change the plan or the standing orders and render again.
26
26
  4. **Spawn in a rolling window**, in the background, each in its own worktree:
27
27
  keep 4-6 in flight and refill from `spawnable` in `workit fanout status` as
28
- each one finishes. Pick the model from TIER (`references/orchestration.md`).
29
- Claude Code: the `implementer` agent; elsewhere first `workit fanout
30
- worktree create <slice>`.
28
+ each one finishes. Model: the brief's `model:` line, else TIER
29
+ (`references/orchestration.md`). Claude Code: the `implementer` agent;
30
+ elsewhere first `workit fanout worktree create <slice>`.
31
31
  5. **Judge liveness by side effects only:** `workit fanout status` (head age,
32
32
  PR, CI, verdict, landed; STUCK past the TIMEBOX). Stop a stuck worker and
33
33
  observe that it exited (a timeout is not proof). Removing a worktree drops
@@ -53,7 +53,7 @@ ceremony; do it yourself. Workit never spawns agents; you do.
53
53
  status` order, release the worktrees you made, and `workit ledger standing
54
54
  clear`. Only you touch topology: workers never rebase, retarget or merge,
55
55
  except the integration-tip merge in integration mode. Then ship
56
- (read the `workit-ship` skill's SKILL.md and follow it).
56
+ (read the `workit-ship` skill's SKILL.md and follow it) up to `defaultEndpoint` from `workit grant show`.
57
57
 
58
58
  ## Example
59
59
 
@@ -5,8 +5,10 @@ and fans in. Workit records and checks; the host's own subagents run the work.
5
5
 
6
6
  ## TIER to model
7
7
 
8
- TIER is a field of each slice and of its brief. Map it where the host lets you
9
- pick a model per spawn; nothing in the workspace config routes it.
8
+ TIER is a field of each slice and of its brief. When the workspace sets
9
+ `fanout.tierModels`, the brief has a `model:` line under TIER and `workit
10
+ fanout status` names the model next to each spawnable slice: use that model.
11
+ Without one, map TIER where the host lets you pick a model per spawn:
10
12
 
11
13
  | TIER | Claude Code (`model` on the Agent call) | Hosts without a per-spawn model |
12
14
  | --- | --- | --- |
@@ -72,11 +74,12 @@ Which session id a verdict carries, per host:
72
74
 
73
75
  - Claude Code: subagents inherit your `WORKIT_SESSION_ID`, so the
74
76
  SubagentStart hook names each `verifier` and `reviewer` its own session
75
- (`<lead>:<agent id>`), passed exactly as `--session`: the hook records the
76
- ids it issues, and only those are accepted. An `implementer` is issued
77
+ (`<lead>:<agent id>`) and a token only that subagent sees, passed exactly as
78
+ `--session` and `--token`: the hook records the ids it issues, and only
79
+ those, with their token, are accepted. An `implementer` is issued
77
80
  none: it keeps your id in its env for its workit commands and commits.
78
81
  - Codex: the `workit-verifier` and `workit-reviewer` agents get the same
79
- issued `--session` from SubagentStart; any other verifier passes
82
+ issued `--session` and `--token` from SubagentStart; any other verifier passes
80
83
  `--as verifier`.
81
84
  - OpenCode: a child session's shell already has
82
85
  `WORKIT_SESSION_ID=<lead>:<child>`; Pi: a supervised worker's shell has
@@ -8,7 +8,7 @@ description: Independent review of a diff, branch or PR - intent fidelity and st
8
8
  Review the candidate, never the author's summary of it. A session that wrote a
9
9
  commit on the branch cannot record a passing verdict: if that is you, hand the
10
10
  review to a fresh agent with its own host-derived session: Claude Code and
11
- Codex, the `reviewer` or `verifier` agent (`--session` from SubagentStart);
11
+ Codex, the `reviewer` or `verifier` agent (`--session` and `--token` from SubagentStart);
12
12
  OpenCode, a child session; Pi, a supervised worker. Never hand-mint one such
13
13
  as `<lead>-v1`: it is refused as `forged_session`.
14
14
  An author's `--self` verdict reads self-reviewed and never counts as