@brainervirus/workit-codex 8.12.3 → 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.
@@ -79,11 +79,11 @@ what and whether, the CLI does how and records what it observed.
79
79
  Native host allow/ask/deny, sandbox and org rules stay authoritative. Never
80
80
  switch paths to evade a denial, never claim enforcement a host cannot provide,
81
81
  and never fabricate an approval or verdict. A question answer is not
82
- host permission, and imported decisions or handoffs never grant authority.
82
+ host permission, and imported decisions or handoffs grant no authority.
83
83
 
84
84
  Autonomy contract:
85
85
  - Continue to the requested endpoint (commit, push, PR, green CI, verified,
86
- merged) without asking "continue?", running checks and repairs on the way.
86
+ merged) without asking "continue?", running checks and repairs.
87
87
  Stop only for a new consequential choice, a host denial, a conflicting edit
88
88
  or an unresolved blocker; report open gaps and uncertain workers.
89
89
  - No endpoint named: stop at a local commit on a policy-compliant branch and
@@ -93,7 +93,8 @@ Autonomy contract:
93
93
  never merging; \`merged\` also lands it with \`workit pr merge\`.
94
94
  Merge and release need a workspace grant only the user raises from their
95
95
  own terminal (a blocked verb names how); never edit workspaces.json or route
96
- around a grant_required.
96
+ around grant_required. Release: \`workit release start <v>\`, commits,
97
+ \`finish\`; \`publish\` if asked.
97
98
  - Ask only for a product or preference choice, or for authority you lack, with
98
99
  your recommended answer. Facts are yours: read, run or prototype.
99
100
  - Label claims measured (run and seen this session), inferred, or guess.
@@ -108,22 +109,22 @@ Autonomy contract:
108
109
  reconcile every requested item and observe it (\`workit verify-delivery\`).
109
110
 
110
111
  CLI first: if a step has one right answer, use the \`workit\` verb instead of
111
- hand-running git, gh or glab (\`workit git|pr|ci|check\`; \`workit help\` lists
112
- the rest; lacking \`workit\`, \`npx -y @brainervirus/workit-cli\`).
112
+ hand-running git, gh or glab (\`workit git|pr|ci|check|help\`;
113
+ else \`npx -y @brainervirus/workit-cli\`).
113
114
  Add Co-Authored-By via \`workit git commit --trailer "K: v" -- <paths>\`, never
114
115
  an amend.
115
116
  The task families (task, policy, evidence, finding, decision, worker, state)
116
117
  are optional continuity for tracked work; each branch has one implicit task,
117
- and a solo edit needs no task. A decision
118
- records a user choice; it never authorizes an effect. Judge work once with
118
+ and a solo edit needs no task. A decision records a user choice; it never
119
+ authorizes an effect. Judge work once with
119
120
  \`workit policy assess --judge risk= behavior= product-choice= plan=\`.
120
- Across repositories, bind each item to its checkout, branch and endpoint,
121
- resolve competing targets before a mutation, and reconcile an uncertain
122
- external effect before retrying it.
121
+ Across repositories, bind items to checkout, branch and endpoint, resolve
122
+ competing targets before a mutation, and reconcile an uncertain
123
+ external effect before a retry.
123
124
 
124
125
  Mid-task: never silently drop or resume an objective (workit-continue).
125
126
 
126
- Skills: load the skill, never act from memory of it (slash aliases /wk-<name>).
127
+ Skills: load the skill, never act from memory (aliases /wk-<name>).
127
128
  - brainstorm, plan, spec, grill, should we: workit-shape
128
129
  - implement, build, add a feature: workit-implement
129
130
  - review, blast radius: workit-review
@@ -9788,7 +9789,7 @@ function acquireFileLockSync(targetPath, options) {
9788
9789
  }
9789
9790
  }
9790
9791
  // packages/workit-core/src/host-session.ts
9791
- import { createHash as createHash4 } from "node:crypto";
9792
+ import { createHash as createHash4, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
9792
9793
  import fs7 from "node:fs";
9793
9794
  import path7 from "node:path";
9794
9795
 
@@ -9880,9 +9881,10 @@ function hostSessionFromEnv(env) {
9880
9881
  return inner ? { host: inner.host, session: inner.session } : { host: declared, session: null };
9881
9882
  }
9882
9883
  var ISSUED_PER_LEAD = 64;
9883
- var ISSUED_SESSIONS_MARKER = "workit-issued-sessions/2";
9884
+ var ISSUED_SESSIONS_MARKER = "workit-issued-sessions/3";
9884
9885
  var hash = (value) => createHash4("sha256").update(value).digest("hex").slice(0, 32);
9885
9886
  var userIssuedRoot = () => path7.join(resolveStateDir(), "issued");
9887
+ var userTokenRoot = () => path7.join(resolveStateDir(), "issued-tokens");
9886
9888
  function migrateRepoIssued(cwd) {
9887
9889
  if (cwd === undefined)
9888
9890
  return;
@@ -9905,7 +9907,12 @@ function migrateRepoIssued(cwd) {
9905
9907
  const to = path7.join(userIssuedRoot(), lead);
9906
9908
  fs7.mkdirSync(to, { recursive: true, mode: 448 });
9907
9909
  for (const id of fs7.readdirSync(from)) {
9908
- fs7.copyFileSync(path7.join(from, id), path7.join(to, id));
9910
+ try {
9911
+ fs7.copyFileSync(path7.join(from, id), path7.join(to, id), fs7.constants.COPYFILE_EXCL);
9912
+ } catch (error) {
9913
+ if (error.code !== "EEXIST")
9914
+ throw error;
9915
+ }
9909
9916
  fs7.rmSync(path7.join(from, id), { force: true });
9910
9917
  }
9911
9918
  removeIfEmpty(from);
@@ -9925,21 +9932,71 @@ var removeIfEmpty = (dir) => {
9925
9932
  fs7.rmdirSync(dir);
9926
9933
  } catch {}
9927
9934
  };
9935
+ var tokenHash = (token) => createHash4("sha256").update(token).digest("hex");
9928
9936
  function recordIssuedSession(cwd, lead, issued) {
9937
+ const token = randomBytes2(18).toString("base64url");
9929
9938
  try {
9930
9939
  migrateRepoIssued(cwd);
9940
+ const own = hash(issued);
9941
+ const tokens = path7.join(userTokenRoot(), hash(lead));
9942
+ fs7.mkdirSync(tokens, { recursive: true, mode: 448 });
9943
+ fs7.writeFileSync(path7.join(tokens, own), `${tokenHash(token)}
9944
+ `, { mode: 384, flag: "wx" });
9931
9945
  const dir = path7.join(userIssuedRoot(), hash(lead));
9932
9946
  fs7.mkdirSync(dir, { recursive: true, mode: 448 });
9933
- const own = hash(issued);
9934
- fs7.writeFileSync(path7.join(dir, own), ISSUED_SESSIONS_MARKER);
9947
+ fs7.writeFileSync(path7.join(dir, own), `${ISSUED_SESSIONS_MARKER}
9948
+ `, { mode: 384 });
9949
+ pruneIssued(dir, own, tokens);
9950
+ return token;
9951
+ } catch {
9952
+ return null;
9953
+ }
9954
+ }
9955
+ function pruneIssued(dir, own, tokens) {
9956
+ try {
9935
9957
  const entries = fs7.readdirSync(dir);
9936
9958
  if (entries.length <= ISSUED_PER_LEAD)
9937
9959
  return;
9938
9960
  const byAge = entries.filter((name) => name !== own).map((name) => ({ name, at: fs7.statSync(path7.join(dir, name)).mtimeMs })).toSorted((a, b) => a.at - b.at);
9939
- for (const { name } of byAge.slice(0, entries.length - ISSUED_PER_LEAD))
9961
+ for (const { name } of byAge.slice(0, entries.length - ISSUED_PER_LEAD)) {
9940
9962
  fs7.rmSync(path7.join(dir, name), { force: true });
9963
+ fs7.rmSync(path7.join(tokens, name), { force: true });
9964
+ }
9941
9965
  } catch {}
9942
9966
  }
9967
+ var issuedRecord = (cwd, claimed) => {
9968
+ const cut = claimed.lastIndexOf(":");
9969
+ if (cut <= 0)
9970
+ return null;
9971
+ migrateRepoIssued(cwd);
9972
+ try {
9973
+ return fs7.readFileSync(path7.join(userIssuedRoot(), hash(claimed.slice(0, cut)), hash(claimed)), "utf8");
9974
+ } catch {
9975
+ return null;
9976
+ }
9977
+ };
9978
+ var TOKEN_HASH = /^([0-9a-f]{64})$/mu;
9979
+ var issuedToken = (claimed) => {
9980
+ const cut = claimed.lastIndexOf(":");
9981
+ if (cut <= 0)
9982
+ return { stored: null, leadBound: false };
9983
+ const dir = path7.join(userTokenRoot(), hash(claimed.slice(0, cut)));
9984
+ try {
9985
+ const text = fs7.readFileSync(path7.join(dir, hash(claimed)), "utf8");
9986
+ return { stored: TOKEN_HASH.exec(text)?.[1] ?? null, leadBound: true };
9987
+ } catch {
9988
+ return { stored: null, leadBound: safeList(dir).length > 0 };
9989
+ }
9990
+ };
9991
+ function issuedBinding(cwd, claimed) {
9992
+ const record = issuedRecord(cwd, claimed);
9993
+ const { stored, leadBound } = issuedToken(claimed);
9994
+ if (stored)
9995
+ return "token";
9996
+ if (record === null)
9997
+ return null;
9998
+ return leadBound ? "token" : "legacy";
9999
+ }
9943
10000
  var OWN_CHILD_SHELL = new Set(["opencode", "pi"]);
9944
10001
 
9945
10002
  // packages/workit-core/src/core/package-root.ts
@@ -9970,8 +10027,10 @@ var metadataLockSchema = object({
9970
10027
  var FOREIGN_LOCK_TTL_MS = 10 * 60000;
9971
10028
  var UNREADABLE_LOCK_TTL_MS = 30000;
9972
10029
  var RECLAIM_GUARD_TTL_MS = 30000;
9973
- var defaultLockTimeoutMs = 250;
10030
+ var IN_PROCESS_LOCK_TIMEOUT_MS = 250;
10031
+ var defaultLockTimeoutMs = IN_PROCESS_LOCK_TIMEOUT_MS;
9974
10032
  var defaultLockTimeout = () => defaultLockTimeoutMs;
10033
+ var hostAwareLockTimeout = (cliMs) => defaultLockTimeoutMs > IN_PROCESS_LOCK_TIMEOUT_MS ? cliMs : IN_PROCESS_LOCK_TIMEOUT_MS;
9975
10034
  var parseMetadataLock = (raw) => {
9976
10035
  let value;
9977
10036
  try {
@@ -12244,7 +12303,7 @@ import path15 from "node:path";
12244
12303
 
12245
12304
  // packages/workit-core/src/ledger.ts
12246
12305
  import { spawnSync as spawnSync9 } from "node:child_process";
12247
- import { createHash as createHash9, randomBytes as randomBytes2 } from "node:crypto";
12306
+ import { createHash as createHash9, randomBytes as randomBytes3 } from "node:crypto";
12248
12307
  import fs12 from "node:fs";
12249
12308
  import path14 from "node:path";
12250
12309
 
@@ -12524,6 +12583,15 @@ var repoBranchPreset = (workspaceRoot) => {
12524
12583
  const detected = detectBranchPolicy(workspaceRoot);
12525
12584
  return detected.developBranch || detected.allowed.length ? detected.preset : null;
12526
12585
  };
12586
+ var releaseTagPrefixOf = (workspaceRoot, policyPrefix) => {
12587
+ if (typeof policyPrefix === "string")
12588
+ return policyPrefix.trim();
12589
+ const r = spawnSync5("git", ["config", "--get", "gitflow.prefix.versiontag"], {
12590
+ cwd: workspaceRoot,
12591
+ encoding: "utf8"
12592
+ });
12593
+ return r.status === 0 ? (r.stdout ?? "").trim() : "v";
12594
+ };
12527
12595
 
12528
12596
  // packages/workit-core/src/core/workspaces.ts
12529
12597
  var nonBlank = string2().refine((value) => value.trim().length > 0, "must not be blank");
@@ -13626,7 +13694,7 @@ function ledgerPath(cwd) {
13626
13694
  const root = storeRoot(cwd);
13627
13695
  return root.ok ? { ok: true, value: path14.join(root.value.root, "ledger", "ledger.jsonl") } : root;
13628
13696
  }
13629
- var newId2 = () => `${Date.now().toString(36)}-${randomBytes2(6).toString("hex")}`;
13697
+ var newId2 = () => `${Date.now().toString(36)}-${randomBytes3(6).toString("hex")}`;
13630
13698
  var NETWORK_FS_TYPES = new Set([
13631
13699
  26985,
13632
13700
  20859,
@@ -13676,14 +13744,14 @@ var ledgerLock = {
13676
13744
  return null;
13677
13745
  throw error;
13678
13746
  }
13679
- const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
13747
+ const token = `${process.pid}-${randomBytes3(8).toString("hex")}`;
13680
13748
  fs12.writeFileSync(path14.join(lock, LOCK_OWNER), token);
13681
13749
  return token;
13682
13750
  },
13683
13751
  release(lock, token) {
13684
13752
  if (lockToken(lock) !== token)
13685
13753
  return false;
13686
- const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
13754
+ const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
13687
13755
  try {
13688
13756
  fs12.renameSync(lock, tomb);
13689
13757
  } catch {
@@ -13700,7 +13768,7 @@ var ledgerLock = {
13700
13768
  },
13701
13769
  token: lockToken,
13702
13770
  reap(lock, observed) {
13703
- const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
13771
+ const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
13704
13772
  try {
13705
13773
  fs12.renameSync(lock, tomb);
13706
13774
  } catch {
@@ -13718,7 +13786,7 @@ var ledgerLock = {
13718
13786
  return false;
13719
13787
  }
13720
13788
  };
13721
- function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
13789
+ function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
13722
13790
  const lock = `${file}.lock`;
13723
13791
  const deadline = Date.now() + waitMs;
13724
13792
  let token = null;
@@ -13776,10 +13844,18 @@ var lastLine = (fd, size) => {
13776
13844
  }
13777
13845
  return null;
13778
13846
  };
13847
+ var tighten = (fd) => {
13848
+ try {
13849
+ if ((fs12.fstatSync(fd).mode & 63) !== 0)
13850
+ fs12.fchmodSync(fd, 384);
13851
+ } catch {}
13852
+ };
13779
13853
  function appendLine(file, build, options) {
13780
13854
  const write = () => {
13781
- const fd = fs12.openSync(file, "a+");
13855
+ const fd = fs12.openSync(file, "a+", 384);
13782
13856
  try {
13857
+ if (process.platform !== "win32")
13858
+ tighten(fd);
13783
13859
  const size = fs12.fstatSync(fd).size;
13784
13860
  let fence = "";
13785
13861
  if (size > 0) {
@@ -13874,7 +13950,9 @@ function readLedger(cwd, options = {}) {
13874
13950
  if (!file.ok)
13875
13951
  return file;
13876
13952
  let text = "";
13953
+ let stat = null;
13877
13954
  try {
13955
+ stat = fs12.statSync(file.value);
13878
13956
  text = fs12.readFileSync(file.value, "utf8");
13879
13957
  } catch (error) {
13880
13958
  if (error.code === "ENOENT")
@@ -13894,6 +13972,7 @@ function readLedger(cwd, options = {}) {
13894
13972
  const integrity = { chained: 0, legacy: 0, unverified: [] };
13895
13973
  const earlier = new Set([null]);
13896
13974
  let chainStarted = false;
13975
+ const ids = new Set;
13897
13976
  for (const raw of text.split(`
13898
13977
  `)) {
13899
13978
  const trimmed = raw.trim();
@@ -13927,6 +14006,10 @@ function readLedger(cwd, options = {}) {
13927
14006
  reason = "unsigned";
13928
14007
  else
13929
14008
  rowIntegrity = "legacy";
14009
+ if (options.integrity && !reason && ids.has(parsed.id))
14010
+ reason = "duplicate_id";
14011
+ if (options.integrity)
14012
+ ids.add(parsed.id);
13930
14013
  if (reason)
13931
14014
  rowIntegrity = "unverified";
13932
14015
  const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
@@ -13950,7 +14033,8 @@ function readLedger(cwd, options = {}) {
13950
14033
  seq: rows.length + 1,
13951
14034
  superseded: false,
13952
14035
  supersedeIgnored: false,
13953
- ...rowIntegrity ? { integrity: rowIntegrity } : {}
14036
+ ...rowIntegrity ? { integrity: rowIntegrity } : {},
14037
+ ...reason ? { integrityReason: reason } : {}
13954
14038
  };
13955
14039
  rows.push(row);
13956
14040
  if (!options.integrity)
@@ -13978,18 +14062,48 @@ function readLedger(cwd, options = {}) {
13978
14062
  const byId = new Map;
13979
14063
  for (const row of rows) {
13980
14064
  const target = row.supersedes ? byId.get(row.supersedes) : undefined;
13981
- if (target && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
14065
+ if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
13982
14066
  target.superseded = true;
13983
14067
  else if (row.supersedes)
13984
14068
  row.supersedeIgnored = true;
13985
14069
  if (!byId.has(row.id))
13986
14070
  byId.set(row.id, row);
13987
14071
  }
14072
+ if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
14073
+ tamperCache.set(file.value, {
14074
+ size: stat.size,
14075
+ stamp: fileStamp(stat),
14076
+ breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
14077
+ });
13988
14078
  return {
13989
14079
  ok: true,
13990
14080
  value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
13991
14081
  };
13992
14082
  }
14083
+ var TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
14084
+ var tamperCache = new Map;
14085
+ var fileStamp = (stat) => `${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`;
14086
+ function ledgerTampering(cwd) {
14087
+ const file = ledgerPath(cwd);
14088
+ if (!file.ok)
14089
+ return [];
14090
+ try {
14091
+ const stat = fs12.statSync(file.value);
14092
+ const cached = tamperCache.get(file.value);
14093
+ if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
14094
+ return cached.breaks;
14095
+ } catch {
14096
+ return [];
14097
+ }
14098
+ const read = readLedger(cwd, { integrity: true });
14099
+ return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
14100
+ }
14101
+ function tamperedUnblock(cwd) {
14102
+ const file = ledgerPath(cwd);
14103
+ const where = file.ok ? file.value : "the ledger file";
14104
+ 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`;
14105
+ }
14106
+ 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)}`;
13993
14107
  function verdictBasis(verdict, current) {
13994
14108
  if (verdict.head && verdict.head === current.head)
13995
14109
  return "fresh";
@@ -13997,7 +14111,7 @@ function verdictBasis(verdict, current) {
13997
14111
  return "carried";
13998
14112
  return "stale";
13999
14113
  }
14000
- var isLiveVerdict = (row) => row.type === "verdict" && typeof row.result === "string" && !row.superseded;
14114
+ var isLiveVerdict = (row, retired) => row.type === "verdict" && typeof row.result === "string" && !retired.has(row.id);
14001
14115
  function trailerSessions(cwd, base, head) {
14002
14116
  if (!base || !head || !safeRef(base) || !safeRef(head))
14003
14117
  return [];
@@ -14078,6 +14192,56 @@ var authoredBy = (authors, session) => {
14078
14192
  return true;
14079
14193
  return false;
14080
14194
  };
14195
+ var TRUST_REASONS = new Set([
14196
+ "unsigned",
14197
+ "hash_mismatch",
14198
+ "broken_link",
14199
+ "duplicate_id",
14200
+ "unissued_session",
14201
+ "unbound_session"
14202
+ ]);
14203
+ var SHELL_VOUCHED_HOSTS = new Set([
14204
+ "cli",
14205
+ "opencode",
14206
+ "pi",
14207
+ "codex_cli",
14208
+ "codex_desktop"
14209
+ ]);
14210
+ function sessionTrust(cwd, row) {
14211
+ const session = row.actor.session;
14212
+ if (!session || row.self === true)
14213
+ return null;
14214
+ const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
14215
+ const host = row.actor.host;
14216
+ const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
14217
+ if (!issuedOnly)
14218
+ return null;
14219
+ const binding = issuedBinding(cwd, session);
14220
+ if (binding === null)
14221
+ return "unissued_session";
14222
+ return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
14223
+ }
14224
+ function trustReasons(cwd, row) {
14225
+ if (row.integrity === "unverified")
14226
+ return [row.integrityReason ?? "unsigned"];
14227
+ if (row.integrity !== "verified")
14228
+ return ["unsigned"];
14229
+ const session = sessionTrust(cwd, row);
14230
+ return session ? [session] : [];
14231
+ }
14232
+ function withIntegrity(cwd, rows) {
14233
+ if (rows.every((row) => row.integrity !== undefined))
14234
+ return rows;
14235
+ const read = readLedger(cwd, { integrity: true });
14236
+ const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
14237
+ return rows.map((row) => {
14238
+ if (row.integrity !== undefined)
14239
+ return row;
14240
+ const fresh = bySeq.get(row.seq);
14241
+ return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
14242
+ });
14243
+ }
14244
+ var untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason));
14081
14245
  var independenceReasons = (row, authors) => {
14082
14246
  const reasons = [];
14083
14247
  if (!row.actor.session)
@@ -14088,15 +14252,17 @@ var independenceReasons = (row, authors) => {
14088
14252
  reasons.push("self");
14089
14253
  return reasons;
14090
14254
  };
14091
- function effectiveOf(rows, authors, sameCode, isCurrent) {
14255
+ function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
14092
14256
  let effective = null;
14093
14257
  for (const row of rows) {
14094
14258
  if (!effective) {
14095
14259
  effective = row;
14096
14260
  continue;
14097
14261
  }
14098
- const rowIndependent = independenceReasons(row, authors).length === 0;
14099
- const effIndependent = independenceReasons(effective, authors).length === 0;
14262
+ if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
14263
+ continue;
14264
+ const rowIndependent = reasonsOf(row).length === 0;
14265
+ const effIndependent = reasonsOf(effective).length === 0;
14100
14266
  if (effIndependent && !rowIndependent && isCurrent(effective))
14101
14267
  continue;
14102
14268
  if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
@@ -14110,7 +14276,7 @@ function restackCarries(rows, branch, from, to) {
14110
14276
  return false;
14111
14277
  const edges = new Map;
14112
14278
  for (const row of rows)
14113
- if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
14279
+ if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.integrity === "verified" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
14114
14280
  edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
14115
14281
  const seen = new Set([from]);
14116
14282
  const queue = [from];
@@ -14126,7 +14292,8 @@ function restackCarries(rows, branch, from, to) {
14126
14292
  }
14127
14293
  return false;
14128
14294
  }
14129
- function checkVerdicts(cwd, branch, rows) {
14295
+ function checkVerdicts(cwd, branch, ledgerRows) {
14296
+ const rows = withIntegrity(cwd, ledgerRows);
14130
14297
  const head = branchHead(cwd, branch);
14131
14298
  const fallbackBase = defaultBase(cwd);
14132
14299
  const scope = branchScope(cwd, branch);
@@ -14156,21 +14323,47 @@ function checkVerdicts(cwd, branch, rows) {
14156
14323
  return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(rows, branch, failed.head, row.head);
14157
14324
  };
14158
14325
  const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
14326
+ const failureAuthorCache = new Map;
14327
+ const failureAuthors = (row) => {
14328
+ let set = failureAuthorCache.get(row.seq);
14329
+ if (!set) {
14330
+ set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
14331
+ failureAuthorCache.set(row.seq, set);
14332
+ }
14333
+ return set;
14334
+ };
14335
+ const reasonCache = new Map;
14336
+ const reasonsOf = (row) => {
14337
+ let reasons = reasonCache.get(row);
14338
+ if (!reasons) {
14339
+ const failing = FAILING.has(String(row.result));
14340
+ reasons = [
14341
+ ...trustReasons(cwd, row),
14342
+ ...independenceReasons(row, failing ? failureAuthors(row) : authors)
14343
+ ];
14344
+ reasonCache.set(row, reasons);
14345
+ }
14346
+ return reasons;
14347
+ };
14348
+ const retired = new Set;
14349
+ for (const row of rows)
14350
+ if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
14351
+ retired.add(row.supersedes);
14159
14352
  const byKind = new Map;
14160
14353
  for (const row of rows)
14161
- if (isLiveVerdict(row) && scope(row)) {
14354
+ if (isLiveVerdict(row, retired) && scope(row)) {
14162
14355
  const kind = typeof row.kind === "string" ? row.kind : "review";
14163
14356
  byKind.set(kind, [...byKind.get(kind) ?? [], row]);
14164
14357
  }
14165
14358
  const verdicts = [];
14166
14359
  const warnings = [];
14167
14360
  for (const [kind, list] of byKind) {
14168
- const row = effectiveOf(list, authors, sameCode, (candidate) => basisOf(candidate) !== "stale");
14361
+ const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
14169
14362
  if (!row)
14170
14363
  continue;
14171
14364
  const basis = basisOf(row);
14172
14365
  const current = basis !== "stale";
14173
- const independence = independenceReasons(row, authors);
14366
+ const independence = reasonsOf(row);
14174
14367
  const reasons = [
14175
14368
  ...current ? [] : ["stale"],
14176
14369
  ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
@@ -14187,23 +14380,31 @@ function checkVerdicts(cwd, branch, rows) {
14187
14380
  });
14188
14381
  if (reasons.length === 0) {
14189
14382
  for (const later of list)
14190
- if (later.seq > row.seq && independenceReasons(later, authors).length > 0 && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
14383
+ if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
14191
14384
  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`);
14192
14385
  }
14193
14386
  }
14194
14387
  for (const row of rows)
14195
14388
  if (row.integrity === "unverified" && scope(row))
14196
14389
  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)`);
14390
+ const tampered = ledgerTampering(cwd);
14391
+ if (tampered.length) {
14392
+ warnings.push(tamperedWarning(cwd, tampered));
14393
+ for (const entry of verdicts) {
14394
+ entry.accepted = false;
14395
+ entry.reasons = [...entry.reasons, "ledger_tampered"];
14396
+ }
14397
+ }
14197
14398
  verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
14198
14399
  const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
14199
14400
  const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
14200
14401
  const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
14201
- const accepted = newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
14402
+ const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
14202
14403
  accepted: false,
14203
14404
  verdict: failing?.verdict ?? null,
14204
14405
  reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
14205
14406
  };
14206
- const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && STRONG_RESULTS.has(String(entry.verdict.result)));
14407
+ const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && !untrusted(entry.reasons) && STRONG_RESULTS.has(String(entry.verdict.result)));
14207
14408
  return {
14208
14409
  branch,
14209
14410
  head,
@@ -19485,7 +19686,7 @@ var refusal = (action, branch, dir) => {
19485
19686
  return `workit: raw ${quote(action.via === "api" ? `${action.forge} api` : action.forge === "gh" ? "gh pr merge" : "glab mr merge")}${action.via === "api" ? " merge" : ""} bypasses the merge grant and the verdict gate. Run ${quote("workit pr merge")} instead (it checks both; ${quote("workit help pr")}).`;
19486
19687
  const plainPush = action.kind === "push" && !action.blindForce && !action.deletes;
19487
19688
  if (dir && branch && plainPush && releaseBranch(dir, branch))
19488
- return `workit: raw ${quote("git push")} onto ${branch}, a protected release branch, bypasses workit's protected-branch check. To finish a release or hotfix, push it with ${quote(`workit git push --release ${branch} [--release v<version>]`)}: it pushes ${branch} (fast-forward only) and the release tag, verifies the remote and records it, under the ${quote("release")} grant (if refused, ask the user to run ${quote("workit grant set <workspace> release=true")}). Feature work goes through ${quote("workit git push")} and ${quote("workit pr merge")}.`;
19689
+ return `workit: raw ${quote("git push")} onto ${branch}, a protected release branch, bypasses workit's protected-branch check. To finish a release or hotfix, push it with ${quote(`workit git push --release ${branch} [--release ${releaseTagPrefixOf(dir)}<version>]`)}: it pushes ${branch} (fast-forward only) and the release tag, verifies the remote and records it, under the ${quote("release")} grant (if refused, ask the user to run ${quote("workit grant set <workspace> release=true")}). Feature work goes through ${quote("workit git push")} and ${quote("workit pr merge")}.`;
19489
19690
  return `workit: raw ${quote("git push")}${action.kind === "push" && action.blindForce ? " --force" : ""} onto ${branch}, a protected branch or the default target, bypasses workit's protected-branch check. Push your feature branch with ${quote("workit git push")} (${quote("workit git push --force-with-lease")} to rewrite it) and land it with ${quote("workit pr merge")}.`;
19490
19691
  };
19491
19692
  var refFacts = (repo, remote) => {
@@ -19572,15 +19773,16 @@ var guardedTarget = (dir, target) => {
19572
19773
  return null;
19573
19774
  };
19574
19775
  var isTag = (facts, name) => facts.tags.has(name) && !facts.heads.has(name);
19575
- var tagRefusal = (action, facts) => {
19776
+ var tagRefusal = (action, facts, dir) => {
19576
19777
  const tags = [
19577
19778
  ...action.tags ?? [],
19578
19779
  ...action.targets.filter((name) => name !== null && isTag(facts, name))
19579
19780
  ];
19580
19781
  if (!tags.length && !action.allTags)
19581
19782
  return null;
19582
- const named = tags.length ? tags.join(" --release ") : "v<version>";
19583
- 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}`)} (v<version> 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")}).`;
19783
+ const shape = `${releaseTagPrefixOf(dir)}<version>`;
19784
+ const named = tags.length ? tags.join(" --release ") : shape;
19785
+ 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")}).`;
19584
19786
  };
19585
19787
  var remoteHeads = (repo, remote) => {
19586
19788
  const listed = spawnSync11("git", ["ls-remote", "--heads", remote], {
@@ -19646,7 +19848,7 @@ var pushRefusal = (action, repo, config) => {
19646
19848
  if (guarded)
19647
19849
  return refusal(action, guarded, repo.dir);
19648
19850
  }
19649
- return tagRefusal(pushed, facts) ?? (action.deletes ? null : offPolicyPush(pushed, repo, facts, remote));
19851
+ return tagRefusal(pushed, facts, repo.dir) ?? (action.deletes ? null : offPolicyPush(pushed, repo, facts, remote));
19650
19852
  };
19651
19853
  var actionsIn = (cwd, command, dialect, cache = new Map) => {
19652
19854
  const out = [];
@@ -20302,10 +20504,10 @@ function stopObligation(input, descriptor) {
20302
20504
  const branch = currentBranch(input.cwd);
20303
20505
  if (!branch || guardedBranch(input.cwd, branch))
20304
20506
  return null;
20305
- const ledger = readLedger(input.cwd);
20507
+ const ledger = readLedger(input.cwd, { integrity: true });
20306
20508
  if (!ledger.ok)
20307
20509
  return null;
20308
- const all = ledger.value.rows;
20510
+ const all = ledger.value.rows.filter((row) => row.integrity !== "unverified");
20309
20511
  const rows = all.filter((row) => row.branch === branch);
20310
20512
  const mine = rows.filter((row) => sessionOf(row) === session);
20311
20513
  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"));
@@ -20681,9 +20883,9 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
20681
20883
  if (role !== "judge" || !event.agentId)
20682
20884
  return readOnly;
20683
20885
  const own = subagentSession(session, event.agentId);
20684
- if (session)
20685
- recordIssuedSession(input.cwd || process.cwd(), session, own);
20686
- 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.`;
20886
+ const token = session ? recordIssuedSession(input.cwd || process.cwd(), session, own) : null;
20887
+ const flags = token ? `--session ${own} --token ${token}` : `--session ${own}`;
20888
+ 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." : ""}`;
20687
20889
  };
20688
20890
  function handleHook(input, deps) {
20689
20891
  const { descriptor } = deps;
@@ -37,6 +37,6 @@ if (quiet)
37
37
  process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: quiet } })}
38
38
  `);
39
39
  else {
40
- const { runCodexHook } = await import("./workit-hook-2d3wce88.js");
40
+ const { runCodexHook } = await import("./workit-hook-j1zd509p.js");
41
41
  await runCodexHook([payload]);
42
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-codex",
3
- "version": "8.12.3",
3
+ "version": "8.13.1",
4
4
  "private": false,
5
5
  "description": "Workit Codex CLI/desktop plugin — shared MCP transport, hooks, method skills, and verifier/reviewer/implementer agents",
6
6
  "keywords": [
@@ -38,9 +38,9 @@
38
38
  "scripts": {
39
39
  "build": "bun scripts/build.ts"
40
40
  },
41
- "dependencies": {
42
- "@brainervirus/workit-core": "^8.12.3",
43
- "@brainervirus/workit-mcp": "^8.12.3"
41
+ "devDependencies": {
42
+ "@brainervirus/workit-core": "^8.13.1",
43
+ "@brainervirus/workit-mcp": "^8.13.1"
44
44
  },
45
45
  "engines": {
46
46
  "node": ">=24"
@@ -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