@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/
|
|
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
|
-
|
|
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
|
-
|
|
10023
|
-
|
|
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
|
|
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
|
|
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)}-${
|
|
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}-${
|
|
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-${
|
|
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-${
|
|
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.
|
|
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,
|
|
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
|
-
|
|
14762
|
-
|
|
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,
|
|
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,
|
|
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 =
|
|
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 &&
|
|
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
|
-
|
|
22244
|
-
|
|
22245
|
-
return `${readOnly} Its own Workit session is ${own}: record verdicts with \`workit ledger verdict <result>
|
|
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
|
@@ -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>`)
|
|
76
|
-
|
|
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
|