@brainervirus/workit-pi 8.13.0 → 8.13.1

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
@@ -8616,6 +8616,23 @@ var NETWORK_FS_TYPES = new Set([
8616
8616
  198183888
8617
8617
  ]);
8618
8618
  var TAIL_BYTES = 64 * 1024;
8619
+ var TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
8620
+ var tamperCache = new Map;
8621
+ var TRUST_REASONS = new Set([
8622
+ "unsigned",
8623
+ "hash_mismatch",
8624
+ "broken_link",
8625
+ "duplicate_id",
8626
+ "unissued_session",
8627
+ "unbound_session"
8628
+ ]);
8629
+ var SHELL_VOUCHED_HOSTS = new Set([
8630
+ "cli",
8631
+ "opencode",
8632
+ "pi",
8633
+ "codex_cli",
8634
+ "codex_desktop"
8635
+ ]);
8619
8636
  var OBSERVED_ONLY = new Set([
8620
8637
  "check",
8621
8638
  "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
 
@@ -14289,7 +14347,7 @@ function ledgerPath(cwd) {
14289
14347
  const root = storeRoot(cwd);
14290
14348
  return root.ok ? { ok: true, value: path14.join(root.value.root, "ledger", "ledger.jsonl") } : root;
14291
14349
  }
14292
- var newId2 = () => `${Date.now().toString(36)}-${randomBytes2(6).toString("hex")}`;
14350
+ var newId2 = () => `${Date.now().toString(36)}-${randomBytes3(6).toString("hex")}`;
14293
14351
  var NETWORK_FS_TYPES = new Set([
14294
14352
  26985,
14295
14353
  20859,
@@ -14339,14 +14397,14 @@ var ledgerLock = {
14339
14397
  return null;
14340
14398
  throw error;
14341
14399
  }
14342
- const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
14400
+ const token = `${process.pid}-${randomBytes3(8).toString("hex")}`;
14343
14401
  fs12.writeFileSync(path14.join(lock, LOCK_OWNER), token);
14344
14402
  return token;
14345
14403
  },
14346
14404
  release(lock, token) {
14347
14405
  if (lockToken(lock) !== token)
14348
14406
  return false;
14349
- const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
14407
+ const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
14350
14408
  try {
14351
14409
  fs12.renameSync(lock, tomb);
14352
14410
  } catch {
@@ -14363,7 +14421,7 @@ var ledgerLock = {
14363
14421
  },
14364
14422
  token: lockToken,
14365
14423
  reap(lock, observed) {
14366
- const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
14424
+ const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
14367
14425
  try {
14368
14426
  fs12.renameSync(lock, tomb);
14369
14427
  } catch {
@@ -14381,7 +14439,7 @@ var ledgerLock = {
14381
14439
  return false;
14382
14440
  }
14383
14441
  };
14384
- function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
14442
+ function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
14385
14443
  const lock = `${file}.lock`;
14386
14444
  const deadline = Date.now() + waitMs;
14387
14445
  let token = null;
@@ -14439,10 +14497,18 @@ var lastLine = (fd, size) => {
14439
14497
  }
14440
14498
  return null;
14441
14499
  };
14500
+ var tighten = (fd) => {
14501
+ try {
14502
+ if ((fs12.fstatSync(fd).mode & 63) !== 0)
14503
+ fs12.fchmodSync(fd, 384);
14504
+ } catch {}
14505
+ };
14442
14506
  function appendLine(file, build, options) {
14443
14507
  const write = () => {
14444
- const fd = fs12.openSync(file, "a+");
14508
+ const fd = fs12.openSync(file, "a+", 384);
14445
14509
  try {
14510
+ if (process.platform !== "win32")
14511
+ tighten(fd);
14446
14512
  const size = fs12.fstatSync(fd).size;
14447
14513
  let fence = "";
14448
14514
  if (size > 0) {
@@ -14537,7 +14603,9 @@ function readLedger(cwd, options = {}) {
14537
14603
  if (!file.ok)
14538
14604
  return file;
14539
14605
  let text = "";
14606
+ let stat = null;
14540
14607
  try {
14608
+ stat = fs12.statSync(file.value);
14541
14609
  text = fs12.readFileSync(file.value, "utf8");
14542
14610
  } catch (error) {
14543
14611
  if (error.code === "ENOENT")
@@ -14557,6 +14625,7 @@ function readLedger(cwd, options = {}) {
14557
14625
  const integrity = { chained: 0, legacy: 0, unverified: [] };
14558
14626
  const earlier = new Set([null]);
14559
14627
  let chainStarted = false;
14628
+ const ids = new Set;
14560
14629
  for (const raw of text.split(`
14561
14630
  `)) {
14562
14631
  const trimmed = raw.trim();
@@ -14590,6 +14659,10 @@ function readLedger(cwd, options = {}) {
14590
14659
  reason = "unsigned";
14591
14660
  else
14592
14661
  rowIntegrity = "legacy";
14662
+ if (options.integrity && !reason && ids.has(parsed.id))
14663
+ reason = "duplicate_id";
14664
+ if (options.integrity)
14665
+ ids.add(parsed.id);
14593
14666
  if (reason)
14594
14667
  rowIntegrity = "unverified";
14595
14668
  const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
@@ -14613,7 +14686,8 @@ function readLedger(cwd, options = {}) {
14613
14686
  seq: rows.length + 1,
14614
14687
  superseded: false,
14615
14688
  supersedeIgnored: false,
14616
- ...rowIntegrity ? { integrity: rowIntegrity } : {}
14689
+ ...rowIntegrity ? { integrity: rowIntegrity } : {},
14690
+ ...reason ? { integrityReason: reason } : {}
14617
14691
  };
14618
14692
  rows.push(row);
14619
14693
  if (!options.integrity)
@@ -14641,18 +14715,48 @@ function readLedger(cwd, options = {}) {
14641
14715
  const byId = new Map;
14642
14716
  for (const row of rows) {
14643
14717
  const target = row.supersedes ? byId.get(row.supersedes) : undefined;
14644
- if (target && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
14718
+ if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
14645
14719
  target.superseded = true;
14646
14720
  else if (row.supersedes)
14647
14721
  row.supersedeIgnored = true;
14648
14722
  if (!byId.has(row.id))
14649
14723
  byId.set(row.id, row);
14650
14724
  }
14725
+ if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
14726
+ tamperCache.set(file.value, {
14727
+ size: stat.size,
14728
+ stamp: fileStamp(stat),
14729
+ breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
14730
+ });
14651
14731
  return {
14652
14732
  ok: true,
14653
14733
  value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
14654
14734
  };
14655
14735
  }
14736
+ var TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
14737
+ var tamperCache = new Map;
14738
+ var fileStamp = (stat) => `${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`;
14739
+ function ledgerTampering(cwd) {
14740
+ const file = ledgerPath(cwd);
14741
+ if (!file.ok)
14742
+ return [];
14743
+ try {
14744
+ const stat = fs12.statSync(file.value);
14745
+ const cached = tamperCache.get(file.value);
14746
+ if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
14747
+ return cached.breaks;
14748
+ } catch {
14749
+ return [];
14750
+ }
14751
+ const read = readLedger(cwd, { integrity: true });
14752
+ return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
14753
+ }
14754
+ function tamperedUnblock(cwd) {
14755
+ const file = ledgerPath(cwd);
14756
+ const where = file.ok ? file.value : "the ledger file";
14757
+ 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`;
14758
+ }
14759
+ 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
14760
  function verdictBasis(verdict, current) {
14657
14761
  if (verdict.head && verdict.head === current.head)
14658
14762
  return "fresh";
@@ -14660,7 +14764,7 @@ function verdictBasis(verdict, current) {
14660
14764
  return "carried";
14661
14765
  return "stale";
14662
14766
  }
14663
- var isLiveVerdict = (row) => row.type === "verdict" && typeof row.result === "string" && !row.superseded;
14767
+ var isLiveVerdict = (row, retired) => row.type === "verdict" && typeof row.result === "string" && !retired.has(row.id);
14664
14768
  function trailerSessions(cwd, base, head) {
14665
14769
  if (!base || !head || !safeRef(base) || !safeRef(head))
14666
14770
  return [];
@@ -14741,6 +14845,56 @@ var authoredBy = (authors, session) => {
14741
14845
  return true;
14742
14846
  return false;
14743
14847
  };
14848
+ var TRUST_REASONS = new Set([
14849
+ "unsigned",
14850
+ "hash_mismatch",
14851
+ "broken_link",
14852
+ "duplicate_id",
14853
+ "unissued_session",
14854
+ "unbound_session"
14855
+ ]);
14856
+ var SHELL_VOUCHED_HOSTS = new Set([
14857
+ "cli",
14858
+ "opencode",
14859
+ "pi",
14860
+ "codex_cli",
14861
+ "codex_desktop"
14862
+ ]);
14863
+ function sessionTrust(cwd, row) {
14864
+ const session = row.actor.session;
14865
+ if (!session || row.self === true)
14866
+ return null;
14867
+ const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
14868
+ const host = row.actor.host;
14869
+ const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
14870
+ if (!issuedOnly)
14871
+ return null;
14872
+ const binding = issuedBinding(cwd, session);
14873
+ if (binding === null)
14874
+ return "unissued_session";
14875
+ return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
14876
+ }
14877
+ function trustReasons(cwd, row) {
14878
+ if (row.integrity === "unverified")
14879
+ return [row.integrityReason ?? "unsigned"];
14880
+ if (row.integrity !== "verified")
14881
+ return ["unsigned"];
14882
+ const session = sessionTrust(cwd, row);
14883
+ return session ? [session] : [];
14884
+ }
14885
+ function withIntegrity(cwd, rows) {
14886
+ if (rows.every((row) => row.integrity !== undefined))
14887
+ return rows;
14888
+ const read = readLedger(cwd, { integrity: true });
14889
+ const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
14890
+ return rows.map((row) => {
14891
+ if (row.integrity !== undefined)
14892
+ return row;
14893
+ const fresh = bySeq.get(row.seq);
14894
+ return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
14895
+ });
14896
+ }
14897
+ var untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason));
14744
14898
  var independenceReasons = (row, authors) => {
14745
14899
  const reasons = [];
14746
14900
  if (!row.actor.session)
@@ -14751,15 +14905,17 @@ var independenceReasons = (row, authors) => {
14751
14905
  reasons.push("self");
14752
14906
  return reasons;
14753
14907
  };
14754
- function effectiveOf(rows, authors, sameCode, isCurrent) {
14908
+ function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
14755
14909
  let effective = null;
14756
14910
  for (const row of rows) {
14757
14911
  if (!effective) {
14758
14912
  effective = row;
14759
14913
  continue;
14760
14914
  }
14761
- const rowIndependent = independenceReasons(row, authors).length === 0;
14762
- const effIndependent = independenceReasons(effective, authors).length === 0;
14915
+ if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
14916
+ continue;
14917
+ const rowIndependent = reasonsOf(row).length === 0;
14918
+ const effIndependent = reasonsOf(effective).length === 0;
14763
14919
  if (effIndependent && !rowIndependent && isCurrent(effective))
14764
14920
  continue;
14765
14921
  if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
@@ -14773,7 +14929,7 @@ function restackCarries(rows, branch, from, to) {
14773
14929
  return false;
14774
14930
  const edges = new Map;
14775
14931
  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)
14932
+ 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
14933
  edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
14778
14934
  const seen = new Set([from]);
14779
14935
  const queue = [from];
@@ -14789,7 +14945,8 @@ function restackCarries(rows, branch, from, to) {
14789
14945
  }
14790
14946
  return false;
14791
14947
  }
14792
- function checkVerdicts(cwd, branch, rows) {
14948
+ function checkVerdicts(cwd, branch, ledgerRows) {
14949
+ const rows = withIntegrity(cwd, ledgerRows);
14793
14950
  const head = branchHead(cwd, branch);
14794
14951
  const fallbackBase = defaultBase(cwd);
14795
14952
  const scope = branchScope(cwd, branch);
@@ -14819,21 +14976,47 @@ function checkVerdicts(cwd, branch, rows) {
14819
14976
  return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(rows, branch, failed.head, row.head);
14820
14977
  };
14821
14978
  const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
14979
+ const failureAuthorCache = new Map;
14980
+ const failureAuthors = (row) => {
14981
+ let set = failureAuthorCache.get(row.seq);
14982
+ if (!set) {
14983
+ set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
14984
+ failureAuthorCache.set(row.seq, set);
14985
+ }
14986
+ return set;
14987
+ };
14988
+ const reasonCache = new Map;
14989
+ const reasonsOf = (row) => {
14990
+ let reasons = reasonCache.get(row);
14991
+ if (!reasons) {
14992
+ const failing = FAILING.has(String(row.result));
14993
+ reasons = [
14994
+ ...trustReasons(cwd, row),
14995
+ ...independenceReasons(row, failing ? failureAuthors(row) : authors)
14996
+ ];
14997
+ reasonCache.set(row, reasons);
14998
+ }
14999
+ return reasons;
15000
+ };
15001
+ const retired = new Set;
15002
+ for (const row of rows)
15003
+ if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
15004
+ retired.add(row.supersedes);
14822
15005
  const byKind = new Map;
14823
15006
  for (const row of rows)
14824
- if (isLiveVerdict(row) && scope(row)) {
15007
+ if (isLiveVerdict(row, retired) && scope(row)) {
14825
15008
  const kind = typeof row.kind === "string" ? row.kind : "review";
14826
15009
  byKind.set(kind, [...byKind.get(kind) ?? [], row]);
14827
15010
  }
14828
15011
  const verdicts = [];
14829
15012
  const warnings = [];
14830
15013
  for (const [kind, list] of byKind) {
14831
- const row = effectiveOf(list, authors, sameCode, (candidate) => basisOf(candidate) !== "stale");
15014
+ const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
14832
15015
  if (!row)
14833
15016
  continue;
14834
15017
  const basis = basisOf(row);
14835
15018
  const current = basis !== "stale";
14836
- const independence = independenceReasons(row, authors);
15019
+ const independence = reasonsOf(row);
14837
15020
  const reasons = [
14838
15021
  ...current ? [] : ["stale"],
14839
15022
  ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
@@ -14850,23 +15033,31 @@ function checkVerdicts(cwd, branch, rows) {
14850
15033
  });
14851
15034
  if (reasons.length === 0) {
14852
15035
  for (const later of list)
14853
- if (later.seq > row.seq && independenceReasons(later, authors).length > 0 && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
15036
+ if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
14854
15037
  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
15038
  }
14856
15039
  }
14857
15040
  for (const row of rows)
14858
15041
  if (row.integrity === "unverified" && scope(row))
14859
15042
  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)`);
15043
+ const tampered = ledgerTampering(cwd);
15044
+ if (tampered.length) {
15045
+ warnings.push(tamperedWarning(cwd, tampered));
15046
+ for (const entry of verdicts) {
15047
+ entry.accepted = false;
15048
+ entry.reasons = [...entry.reasons, "ledger_tampered"];
15049
+ }
15050
+ }
14860
15051
  verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
14861
15052
  const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
14862
15053
  const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
14863
15054
  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: [] } : {
15055
+ const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
14865
15056
  accepted: false,
14866
15057
  verdict: failing?.verdict ?? null,
14867
15058
  reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
14868
15059
  };
14869
- const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && STRONG_RESULTS.has(String(entry.verdict.result)));
15060
+ const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && !untrusted(entry.reasons) && STRONG_RESULTS.has(String(entry.verdict.result)));
14870
15061
  return {
14871
15062
  branch,
14872
15063
  head,
@@ -21898,10 +22089,10 @@ function stopObligation(input, descriptor) {
21898
22089
  const branch = currentBranch(input.cwd);
21899
22090
  if (!branch || guardedBranch(input.cwd, branch))
21900
22091
  return null;
21901
- const ledger = readLedger(input.cwd);
22092
+ const ledger = readLedger(input.cwd, { integrity: true });
21902
22093
  if (!ledger.ok)
21903
22094
  return null;
21904
- const all = ledger.value.rows;
22095
+ const all = ledger.value.rows.filter((row) => row.integrity !== "unverified");
21905
22096
  const rows = all.filter((row) => row.branch === branch);
21906
22097
  const mine = rows.filter((row) => sessionOf(row) === session);
21907
22098
  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 +22431,9 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
22240
22431
  if (role !== "judge" || !event.agentId)
22241
22432
  return readOnly;
22242
22433
  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.`;
22434
+ const token = session ? recordIssuedSession(input.cwd || process.cwd(), session, own) : null;
22435
+ const flags = token ? `--session ${own} --token ${token}` : `--session ${own}`;
22436
+ 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
22437
  };
22247
22438
  function handleHook(input, deps) {
22248
22439
  const { descriptor } = deps;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-pi",
3
- "version": "8.13.0",
3
+ "version": "8.13.1",
4
4
  "description": "Workit native extension for Pi — tools, method skills, and session continuity",
5
5
  "keywords": [
6
6
  "agentic-coding",
@@ -72,11 +72,12 @@ Which session id a verdict carries, per host:
72
72
 
73
73
  - Claude Code: subagents inherit your `WORKIT_SESSION_ID`, so the
74
74
  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
75
+ (`<lead>:<agent id>`) and a token only that subagent sees, passed exactly as
76
+ `--session` and `--token`: the hook records the ids it issues, and only
77
+ those, with their token, are accepted. An `implementer` is issued
77
78
  none: it keeps your id in its env for its workit commands and commits.
78
79
  - Codex: the `workit-verifier` and `workit-reviewer` agents get the same
79
- issued `--session` from SubagentStart; any other verifier passes
80
+ issued `--session` and `--token` from SubagentStart; any other verifier passes
80
81
  `--as verifier`.
81
82
  - OpenCode: a child session's shell already has
82
83
  `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