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