@brainervirus/workit-pi 8.13.1 → 8.14.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
@@ -7546,6 +7546,8 @@ var text = string2().check((ctx) => {
7546
7546
  ctx.issues.push({ code: "custom", input: ctx.value, message: "invalid Unicode", path: [] });
7547
7547
  });
7548
7548
  var id = text.regex(uuidPattern, "expected lowercase UUID");
7549
+ var idRefPattern = /^[0-9a-f][0-9a-f-]{7,35}$/;
7550
+ var idRef = text.regex(idRefPattern, "expected a UUID or an id prefix of 8+ characters");
7549
7551
  var digest = text.regex(digestPattern, "expected lowercase SHA-256 digest");
7550
7552
  var revision = text.regex(uuidPattern, "expected UUID revision");
7551
7553
  var validUtc = (value) => {
@@ -7825,7 +7827,7 @@ var workerReportSchema = object({
7825
7827
  outcome: _enum(["completed", "failed", "cancelled"]),
7826
7828
  summary: text,
7827
7829
  evidenceIds: array(id),
7828
- findingIds: array(id)
7830
+ findingIds: array(idRef)
7829
7831
  }).strict();
7830
7832
  var workerSchema = object({
7831
7833
  assignment: assignmentSchema,
@@ -8018,7 +8020,7 @@ var findingOperations = {
8018
8020
  action: literal("resolve"),
8019
8021
  ...taskId,
8020
8022
  expectedRevision: revision.optional(),
8021
- findingId: id,
8023
+ findingId: idRef,
8022
8024
  disposition: _enum(["open", "fixed", "dismissed", "deferred"]),
8023
8025
  reason: text,
8024
8026
  evidenceIds: array(id),
@@ -8059,7 +8061,7 @@ var workerOperations = {
8059
8061
  ...taskId,
8060
8062
  expectedRevision: revision.optional(),
8061
8063
  expectedWorkspaceRevision: revision.optional(),
8062
- workerId: id,
8064
+ workerId: idRef,
8063
8065
  report: workerReportSchema
8064
8066
  }),
8065
8067
  cancel: operation({
@@ -8067,7 +8069,7 @@ var workerOperations = {
8067
8069
  ...taskId,
8068
8070
  ...revisions,
8069
8071
  expectedWorkspaceRevision: revision.optional(),
8070
- workerId: id,
8072
+ workerId: idRef,
8071
8073
  reason: text
8072
8074
  })
8073
8075
  };
@@ -8537,6 +8539,8 @@ var workspaceConfigSchema = object({
8537
8539
  defaultEndpoint: unknown().optional(),
8538
8540
  verification: _enum(["self", "independent"]).optional(),
8539
8541
  trailers: boolean2().optional(),
8542
+ liveSessionGuard: boolean2().optional(),
8543
+ fanout: object({ tierModels: unknown().optional() }).passthrough().optional(),
8540
8544
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
8541
8545
  kind,
8542
8546
  union([boolean2(), literal("verified")]).optional()
@@ -8581,6 +8585,17 @@ var mainCheckouts = new Map;
8581
8585
  var runtimeWorkspaceIndexSchema = object({
8582
8586
  workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
8583
8587
  }).passthrough();
8588
+ var TIER_KEYS = new Set(["mundane", "standard", "hard"]);
8589
+ var TIER_HOSTS = new Set([
8590
+ "claude_code",
8591
+ "opencode",
8592
+ "codex",
8593
+ "codex_cli",
8594
+ "codex_desktop",
8595
+ "cursor",
8596
+ "pi",
8597
+ "default"
8598
+ ]);
8584
8599
 
8585
8600
  // packages/workit-core/src/core/release-tracks.ts
8586
8601
  var KNOWN_FIELDS = new Set([
package/dist/workit.js CHANGED
@@ -7771,6 +7771,8 @@ var text = string2().check((ctx) => {
7771
7771
  ctx.issues.push({ code: "custom", input: ctx.value, message: "invalid Unicode", path: [] });
7772
7772
  });
7773
7773
  var id = text.regex(uuidPattern, "expected lowercase UUID");
7774
+ var idRefPattern = /^[0-9a-f][0-9a-f-]{7,35}$/;
7775
+ var idRef = text.regex(idRefPattern, "expected a UUID or an id prefix of 8+ characters");
7774
7776
  var digest = text.regex(digestPattern, "expected lowercase SHA-256 digest");
7775
7777
  var revision = text.regex(uuidPattern, "expected UUID revision");
7776
7778
  var validUtc = (value) => {
@@ -8148,7 +8150,7 @@ var workerReportSchema = object({
8148
8150
  outcome: _enum(["completed", "failed", "cancelled"]),
8149
8151
  summary: text,
8150
8152
  evidenceIds: array(id),
8151
- findingIds: array(id)
8153
+ findingIds: array(idRef)
8152
8154
  }).strict();
8153
8155
  var workerSchema = object({
8154
8156
  assignment: assignmentSchema,
@@ -8394,7 +8396,7 @@ var findingOperations = {
8394
8396
  action: literal("resolve"),
8395
8397
  ...taskId,
8396
8398
  expectedRevision: revision.optional(),
8397
- findingId: id,
8399
+ findingId: idRef,
8398
8400
  disposition: _enum(["open", "fixed", "dismissed", "deferred"]),
8399
8401
  reason: text,
8400
8402
  evidenceIds: array(id),
@@ -8435,7 +8437,7 @@ var workerOperations = {
8435
8437
  ...taskId,
8436
8438
  expectedRevision: revision.optional(),
8437
8439
  expectedWorkspaceRevision: revision.optional(),
8438
- workerId: id,
8440
+ workerId: idRef,
8439
8441
  report: workerReportSchema
8440
8442
  }),
8441
8443
  cancel: operation({
@@ -8443,7 +8445,7 @@ var workerOperations = {
8443
8445
  ...taskId,
8444
8446
  ...revisions,
8445
8447
  expectedWorkspaceRevision: revision.optional(),
8446
- workerId: id,
8448
+ workerId: idRef,
8447
8449
  reason: text
8448
8450
  })
8449
8451
  };
@@ -8493,6 +8495,16 @@ var success = (revisionValue, workspaceRevisionValue, data) => ({
8493
8495
  data
8494
8496
  });
8495
8497
  var failure2 = (code, error, details = {}) => ({ ok: false, schemaVersion: 1, code, error, details });
8498
+ function resolveIdRef(kind, ref, ids) {
8499
+ if (ids.includes(ref))
8500
+ return success(null, null, ref);
8501
+ const matches = ids.filter((candidate) => candidate.startsWith(ref));
8502
+ if (matches.length === 1)
8503
+ return success(null, null, matches[0]);
8504
+ if (matches.length === 0)
8505
+ return failure2("not_found", `${kind} not found`);
8506
+ return failure2("invalid_input", `${kind} prefix ${ref} is ambiguous; candidates: ${matches.join(", ")}`);
8507
+ }
8496
8508
  function parseOperation(family, input) {
8497
8509
  if (typeof input === "object" && input !== null && "schemaVersion" in input && input.schemaVersion !== SCHEMA_VERSION)
8498
8510
  return failure2("unsupported_version", "unsupported schema version", { operation: family });
@@ -11036,20 +11048,26 @@ class TaskStore {
11036
11048
  return success(null, null, { key: key.data, bound: bound.data });
11037
11049
  }
11038
11050
  taskForWorker(workerId) {
11039
- if (typeof workerId !== "string" || !validId(workerId))
11051
+ if (typeof workerId !== "string" || !idRefPattern.test(workerId))
11040
11052
  return failure2("invalid_input", "needs a worker id: `workit worker cancel|report <workerId>`");
11041
11053
  const listed = this.listTaskIndex();
11042
11054
  if (!listed.ok)
11043
11055
  return listed;
11056
+ const owners = new Map;
11044
11057
  for (const entry of listed.data) {
11045
11058
  if (entry.status === "closed")
11046
11059
  continue;
11047
11060
  const loaded = this.load(entry.id);
11048
11061
  if (!loaded.ok)
11049
11062
  return loaded;
11050
- if (loaded.data?.record.workers.some((worker) => worker.id === workerId))
11051
- return success(null, null, entry.id);
11063
+ for (const worker of loaded.data?.record.workers ?? [])
11064
+ owners.set(worker.id, entry.id);
11052
11065
  }
11066
+ const resolved = resolveIdRef("worker", workerId, [...owners.keys()]);
11067
+ if (resolved.ok)
11068
+ return success(null, null, owners.get(resolved.data) ?? null);
11069
+ if (resolved.code !== "not_found")
11070
+ return resolved;
11053
11071
  return success(null, null, null);
11054
11072
  }
11055
11073
  originTaskEntry() {
@@ -13167,6 +13185,8 @@ var workspaceConfigSchema = object({
13167
13185
  defaultEndpoint: unknown().optional(),
13168
13186
  verification: _enum(["self", "independent"]).optional(),
13169
13187
  trailers: boolean2().optional(),
13188
+ liveSessionGuard: boolean2().optional(),
13189
+ fanout: object({ tierModels: unknown().optional() }).passthrough().optional(),
13170
13190
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
13171
13191
  kind,
13172
13192
  union([boolean2(), literal("verified")]).optional()
@@ -13484,24 +13504,39 @@ var resolveRuntimeReleaseTracks = (cwd) => {
13484
13504
  const matched = matchRuntimeWorkspace(cwd);
13485
13505
  return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
13486
13506
  };
13487
- var workspaceTrailers = (cwd, dir) => {
13507
+ var inheritedSetting = (cwd, dir, has) => {
13488
13508
  let matched;
13489
13509
  try {
13490
13510
  matched = matchRuntimeWorkspace(cwd, dir);
13491
13511
  } catch {
13492
- return { enabled: true, workspace: null, from: null };
13512
+ return null;
13493
13513
  }
13494
13514
  if (!matched)
13495
- return { enabled: true, workspace: null, from: null };
13515
+ return null;
13496
13516
  const { selected, matches } = matched;
13497
13517
  const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
13498
- const source = [selected, ...enclosing].find((entry) => typeof entry.trailers === "boolean");
13518
+ return { workspace: selected.name, source: [selected, ...enclosing].find(has) ?? null };
13519
+ };
13520
+ var workspaceTrailers = (cwd, dir) => {
13521
+ const found = inheritedSetting(cwd, dir, (entry) => typeof entry.trailers === "boolean");
13499
13522
  return {
13500
- enabled: source?.trailers !== false,
13501
- workspace: selected.name,
13502
- from: source?.name ?? null
13523
+ enabled: found?.source?.trailers !== false,
13524
+ workspace: found?.workspace ?? null,
13525
+ from: found?.source?.name ?? null
13503
13526
  };
13504
13527
  };
13528
+ var workspaceLiveSessionGuard = (cwd, dir) => inheritedSetting(cwd, dir, (entry) => typeof entry.liveSessionGuard === "boolean")?.source?.liveSessionGuard !== false;
13529
+ var TIER_KEYS = new Set(["mundane", "standard", "hard"]);
13530
+ var TIER_HOSTS = new Set([
13531
+ "claude_code",
13532
+ "opencode",
13533
+ "codex",
13534
+ "codex_cli",
13535
+ "codex_desktop",
13536
+ "cursor",
13537
+ "pi",
13538
+ "default"
13539
+ ]);
13505
13540
  var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
13506
13541
  const matched = matchRuntimeWorkspace(cwd);
13507
13542
  if (!matched)
@@ -18981,6 +19016,8 @@ var TASK_SCOPED_ACTIONS = new Set([
18981
19016
  "worker.cancel",
18982
19017
  "state.export"
18983
19018
  ]);
19019
+ var ONLY_OWN_REPORT = "workers can submit only their own report";
19020
+ var NO_HELPER_CONTROL = "helpers cannot cancel or control workers";
18984
19021
  var WORKER_ADDRESSED_ACTIONS = new Set(["worker.cancel", "worker.report"]);
18985
19022
  var workerCommand = (taskId, request) => {
18986
19023
  const { action, workerId, reason, report } = request;
@@ -19878,6 +19915,10 @@ class WorkitCore {
19878
19915
  }
19879
19916
  if (helper?.ok)
19880
19917
  return failure2("permission_denied", "helpers cannot resolve findings");
19918
+ const findingRef = resolveIdRef("finding", input.findingId, task.data.findings.map((entry) => entry.id));
19919
+ if (!findingRef.ok)
19920
+ return findingRef;
19921
+ input.findingId = findingRef.data;
19881
19922
  const finding = task.data.findings.find((entry) => entry.id === input.findingId);
19882
19923
  if (!finding)
19883
19924
  return failure2("not_found", "finding not found");
@@ -20029,15 +20070,31 @@ class WorkitCore {
20029
20070
  return changed;
20030
20071
  return success(changed.data.task.revision, changed.data.workspace.revision, changed.data.task.workers.at(-1));
20031
20072
  }
20073
+ const workerRef = resolveIdRef("worker", input.workerId, helperId === null ? task.data.workers.map((candidate) => candidate.id) : [helperId]);
20074
+ if (!workerRef.ok)
20075
+ return helperId === null ? workerRef : failure2("permission_denied", input.action === "report" ? ONLY_OWN_REPORT : NO_HELPER_CONTROL);
20076
+ input.workerId = workerRef.data;
20032
20077
  const entry = task.data.workers.find((candidate) => candidate.id === input.workerId);
20033
20078
  if (!entry)
20034
20079
  return failure2("not_found", "worker not found");
20035
20080
  if (input.action === "report") {
20036
20081
  if (helperId === null || helperId !== input.workerId)
20037
- return failure2("permission_denied", "workers can submit only their own report");
20082
+ return failure2("permission_denied", ONLY_OWN_REPORT);
20038
20083
  const helper = this.helperEntry(task.data, true, true);
20039
20084
  if (!helper.ok)
20040
20085
  return helper;
20086
+ const ownFindings = task.data.findings.filter((candidate) => candidate.provenance.workerId === helperId).map((candidate) => candidate.id);
20087
+ const reported = [];
20088
+ for (const findingRef of input.report.findingIds) {
20089
+ const found = resolveIdRef("finding", findingRef, ownFindings);
20090
+ if (found.ok)
20091
+ reported.push(found.data);
20092
+ else if (found.code === "not_found")
20093
+ reported.push(findingRef);
20094
+ else
20095
+ return found;
20096
+ }
20097
+ input.report.findingIds = reported;
20041
20098
  const evidence = task.data.evidence.filter((candidate) => input.report.evidenceIds.includes(candidate.id));
20042
20099
  const findings = task.data.findings.filter((candidate) => input.report.findingIds.includes(candidate.id));
20043
20100
  if (evidence.length !== input.report.evidenceIds.length || findings.length !== input.report.findingIds.length)
@@ -20065,7 +20122,7 @@ class WorkitCore {
20065
20122
  return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during report");
20066
20123
  }
20067
20124
  if (helperId !== null)
20068
- return failure2("permission_denied", "helpers cannot cancel or control workers");
20125
+ return failure2("permission_denied", NO_HELPER_CONTROL);
20069
20126
  if (task.data.status === "closed")
20070
20127
  return failure2("invalid_transition", "closed task cannot cancel workers");
20071
20128
  if (entry.data.state === "stopped") {
@@ -21803,27 +21860,231 @@ var shellPolicy = (cwd, command, dialect = "posix") => {
21803
21860
  };
21804
21861
  // packages/workit-core/src/hooks/write-gate.ts
21805
21862
  import { realpathSync as realpathSync6 } from "node:fs";
21863
+ import os7 from "node:os";
21864
+ import path27 from "node:path";
21865
+
21866
+ // packages/workit-core/src/core/live-session.ts
21867
+ import { spawnSync as spawnSync17 } from "node:child_process";
21868
+ import { createHash as createHash15 } from "node:crypto";
21869
+ import fs19 from "node:fs";
21870
+ import os6 from "node:os";
21806
21871
  import path26 from "node:path";
21872
+ var LIVE_SESSION_TTL_MS = 15 * 60000;
21873
+ var HEARTBEAT_EVERY_MS = 30000;
21874
+ var liveProbe = {
21875
+ platform: process.platform,
21876
+ run: (args) => spawnSync17("ps", args, { encoding: "utf8", timeout: 2000 }).stdout ?? "",
21877
+ start: (pid) => processStartOf(pid)
21878
+ };
21879
+ var leadOf = (session) => session.split(":")[0].replace(/-w-.*$/su, "") || session;
21880
+ var familyOf = (host) => host.replace(/_(?:cli|desktop)$/u, "");
21881
+ var processAlive = (pid) => {
21882
+ try {
21883
+ process.kill(pid, 0);
21884
+ return true;
21885
+ } catch (error) {
21886
+ return error.code === "EPERM";
21887
+ }
21888
+ };
21889
+ var processInfo = (pid) => {
21890
+ try {
21891
+ if (liveProbe.platform === "linux") {
21892
+ const stat = fs19.readFileSync(`/proc/${pid}/stat`, "utf8");
21893
+ const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
21894
+ const command = fs19.readFileSync(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ");
21895
+ return Number.isInteger(ppid) ? { ppid, command: command.trim() } : null;
21896
+ }
21897
+ if (liveProbe.platform === "win32")
21898
+ return null;
21899
+ const match = /^\s*(\d+)\s+(.*)$/su.exec(liveProbe.run(["-o", "ppid=,command=", "-p", String(pid)]));
21900
+ return match ? { ppid: Number(match[1]), command: match[2].trim() } : null;
21901
+ } catch {
21902
+ return null;
21903
+ }
21904
+ };
21905
+ 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;
21906
+ var PIDS = new Map;
21907
+ var hostPid = (transport) => {
21908
+ if (transport !== "hook-process")
21909
+ return process.pid;
21910
+ const key = `${liveProbe.platform}\x00${process.ppid}`;
21911
+ if (PIDS.has(key))
21912
+ return PIDS.get(key) ?? null;
21913
+ if (liveProbe.platform === "win32") {
21914
+ PIDS.set(key, null);
21915
+ return null;
21916
+ }
21917
+ let pid = process.ppid;
21918
+ for (let hop = 0;hop < 4; hop++) {
21919
+ const info = processInfo(pid);
21920
+ if (!info || !WRAPPER.test(info.command) || info.ppid <= 1)
21921
+ break;
21922
+ pid = info.ppid;
21923
+ }
21924
+ PIDS.set(key, pid);
21925
+ return pid;
21926
+ };
21927
+ var sessionSafe = (value) => value.replace(/[^A-Za-z0-9_.:@/+-]/gu, "-");
21928
+ var liveOwner = (input, transport, env = process.env) => {
21929
+ const session = input.session.id?.trim();
21930
+ if (!session)
21931
+ return null;
21932
+ const leads = new Set([leadOf(sessionSafe(session))]);
21933
+ if (input.session.parentId)
21934
+ leads.add(leadOf(sessionSafe(input.session.parentId)));
21935
+ if (env.WORKIT_PI_WORKER_SESSION && env.WORKIT_SESSION_ID)
21936
+ leads.add(leadOf(sessionSafe(env.WORKIT_SESSION_ID)));
21937
+ return {
21938
+ host: input.host,
21939
+ session: sessionSafe(session),
21940
+ leads: [...leads],
21941
+ pid: () => hostPid(transport)
21942
+ };
21943
+ };
21944
+ var checkoutOf = (target) => {
21945
+ let dir = path26.resolve(target);
21946
+ while (!fs19.existsSync(dir) && path26.dirname(dir) !== dir)
21947
+ dir = path26.dirname(dir);
21948
+ if (fs19.existsSync(dir) && !fs19.statSync(dir).isDirectory())
21949
+ dir = path26.dirname(dir);
21950
+ const parts = dir.split(path26.sep);
21951
+ const dotGit = parts.lastIndexOf(".git");
21952
+ if (dotGit > 0)
21953
+ dir = parts.slice(0, dotGit).join(path26.sep) || path26.sep;
21954
+ const location = resolveStore(dir);
21955
+ if (location instanceof Error || !location.shared || !location.top)
21956
+ return null;
21957
+ let top = location.top;
21958
+ try {
21959
+ top = fs19.realpathSync.native(top);
21960
+ } catch {}
21961
+ return top;
21962
+ };
21963
+ var markerFile2 = (top) => path26.join(resolveStateDir(), "live-sessions", `${createHash15("sha256").update(checkoutKey(top)).digest("hex").slice(0, 32)}.json`);
21964
+ var checkoutKey = (top, platform = process.platform) => {
21965
+ let key = path26.resolve(top);
21966
+ try {
21967
+ key = fs19.realpathSync.native(key);
21968
+ } catch {}
21969
+ return platform === "win32" ? key.replaceAll("/", "\\").toLowerCase() : key;
21970
+ };
21971
+ var readMarker = (file) => {
21972
+ try {
21973
+ const value = JSON.parse(fs19.readFileSync(file, "utf8"));
21974
+ if (value?.version !== 1 || typeof value.session !== "string" || typeof value.host !== "string" || typeof value.heartbeatAt !== "string" || Number.isNaN(Date.parse(value.heartbeatAt)))
21975
+ return null;
21976
+ return {
21977
+ version: 1,
21978
+ session: value.session,
21979
+ leads: Array.isArray(value.leads) ? value.leads.filter((item) => typeof item === "string") : [],
21980
+ host: value.host,
21981
+ pid: typeof value.pid === "number" ? value.pid : null,
21982
+ pidStart: typeof value.pidStart === "string" ? value.pidStart : null,
21983
+ machine: typeof value.machine === "string" ? value.machine : "",
21984
+ checkout: typeof value.checkout === "string" ? value.checkout : "",
21985
+ heartbeatAt: value.heartbeatAt
21986
+ };
21987
+ } catch {
21988
+ return null;
21989
+ }
21990
+ };
21991
+ var ageOf2 = (marker, now) => now.getTime() - Date.parse(marker.heartbeatAt);
21992
+ var isLive = (marker, deps) => {
21993
+ const age = ageOf2(marker, deps.now ?? new Date);
21994
+ if (age > (deps.ttlMs ?? LIVE_SESSION_TTL_MS))
21995
+ return false;
21996
+ if (marker.machine !== (deps.machine ?? os6.hostname()) || marker.pid === null)
21997
+ return true;
21998
+ if (!(deps.alive ?? processAlive)(marker.pid))
21999
+ return false;
22000
+ if (marker.pidStart === null)
22001
+ return true;
22002
+ const start = liveProbe.start(marker.pid);
22003
+ return start === null || start === marker.pidStart;
22004
+ };
22005
+ var sameLead = (marker, owner) => marker.session === owner.session || owner.leads.some((lead) => lead === marker.session || marker.leads.includes(lead));
22006
+ var sameOwner = (marker, owner, machine) => {
22007
+ if (sameLead(marker, owner))
22008
+ return true;
22009
+ if (marker.machine !== machine)
22010
+ return false;
22011
+ const pid = owner.pid();
22012
+ if (pid === null || marker.pid === null || marker.pidStart === null)
22013
+ return familyOf(marker.host) === familyOf(owner.host);
22014
+ return marker.pid === pid;
22015
+ };
22016
+ var liveConflict = (target, owner, deps = {}) => {
22017
+ try {
22018
+ const checkout = checkoutOf(target);
22019
+ if (!checkout)
22020
+ return null;
22021
+ const file = markerFile2(checkout);
22022
+ const marker = readMarker(file);
22023
+ if (!marker || sameLead(marker, owner) || !isLive(marker, deps))
22024
+ return null;
22025
+ if (sameOwner(marker, owner, deps.machine ?? os6.hostname()))
22026
+ return null;
22027
+ return { marker, file, ageMs: ageOf2(marker, deps.now ?? new Date), checkout };
22028
+ } catch {
22029
+ return null;
22030
+ }
22031
+ };
22032
+ var claimLiveSession = (target, owner, deps = {}) => {
22033
+ try {
22034
+ const checkout = checkoutOf(target);
22035
+ if (!checkout)
22036
+ return;
22037
+ const file = markerFile2(checkout);
22038
+ const now = deps.now ?? new Date;
22039
+ const machine = deps.machine ?? os6.hostname();
22040
+ const current = readMarker(file);
22041
+ if (current) {
22042
+ const age = ageOf2(current, now);
22043
+ if (sameLead(current, owner) && age >= 0 && age < HEARTBEAT_EVERY_MS)
22044
+ return;
22045
+ if (!sameLead(current, owner) && isLive(current, deps) && !sameOwner(current, owner, machine))
22046
+ return;
22047
+ }
22048
+ const pid = owner.pid();
22049
+ const marker = {
22050
+ version: 1,
22051
+ session: owner.session,
22052
+ leads: owner.leads,
22053
+ host: owner.host,
22054
+ pid,
22055
+ pidStart: pid === null ? null : liveProbe.start(pid),
22056
+ machine,
22057
+ checkout,
22058
+ heartbeatAt: now.toISOString()
22059
+ };
22060
+ fs19.mkdirSync(path26.dirname(file), { recursive: true });
22061
+ const temp = `${file}.${process.pid}.tmp`;
22062
+ fs19.writeFileSync(temp, JSON.stringify(marker));
22063
+ fs19.renameSync(temp, file);
22064
+ } catch {}
22065
+ };
22066
+
22067
+ // packages/workit-core/src/hooks/write-gate.ts
21807
22068
  var NONE3 = { kind: "none" };
21808
22069
  var CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
21809
22070
  var canonical2 = (file) => {
21810
22071
  let rest = "";
21811
- for (let dir = path26.resolve(file);; dir = path26.dirname(dir)) {
22072
+ for (let dir = path27.resolve(file);; dir = path27.dirname(dir)) {
21812
22073
  try {
21813
- return path26.join(realpathSync6(dir), rest);
22074
+ return path27.join(realpathSync6(dir), rest);
21814
22075
  } catch {
21815
- if (path26.dirname(dir) === dir)
21816
- return path26.resolve(file);
21817
- rest = rest ? path26.join(path26.basename(dir), rest) : path26.basename(dir);
22076
+ if (path27.dirname(dir) === dir)
22077
+ return path27.resolve(file);
22078
+ rest = rest ? path27.join(path27.basename(dir), rest) : path27.basename(dir);
21818
22079
  }
21819
22080
  }
21820
22081
  };
21821
22082
  var exempt = (cwd, root, file, plans) => {
21822
22083
  const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
21823
- const relative = path26.relative(fold(canonical2(root)), fold(canonical2(path26.resolve(cwd, file))));
21824
- if (relative.startsWith("..") || path26.isAbsolute(relative))
22084
+ const relative = path27.relative(fold(canonical2(root)), fold(canonical2(path27.resolve(cwd, file))));
22085
+ if (relative.startsWith("..") || path27.isAbsolute(relative))
21825
22086
  return true;
21826
- const posix = relative.split(path26.sep).join("/");
22087
+ const posix = relative.split(path27.sep).join("/");
21827
22088
  const segments = posix.split("/");
21828
22089
  return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
21829
22090
  };
@@ -21921,7 +22182,7 @@ var shellWrites = (command) => {
21921
22182
  return { writes: true, targets: unknown ? null : targets };
21922
22183
  };
21923
22184
  var cache2 = new Map;
21924
- var planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path26.resolve(root, file)) ?? "-").join("\x00");
22185
+ var planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path27.resolve(root, file)) ?? "-").join("\x00");
21925
22186
  var gateFor = (cwd) => {
21926
22187
  const store = new TaskStore(cwd);
21927
22188
  const listed = store.listTaskIndex();
@@ -21959,7 +22220,218 @@ var gateFor = (cwd) => {
21959
22220
  cache2.delete(cache2.keys().next().value);
21960
22221
  return gate;
21961
22222
  };
21962
- function writeGate(cwd, paths) {
22223
+ var MUTATING_GIT = new Set([
22224
+ "add",
22225
+ "am",
22226
+ "apply",
22227
+ "checkout",
22228
+ "cherry-pick",
22229
+ "clean",
22230
+ "commit",
22231
+ "merge",
22232
+ "mv",
22233
+ "pull",
22234
+ "rebase",
22235
+ "reset",
22236
+ "restore",
22237
+ "revert",
22238
+ "rm",
22239
+ "stash",
22240
+ "switch"
22241
+ ]);
22242
+ var DRY_N = new Set(["add", "clean", "rm", "mv"]);
22243
+ var MUTATING_WORKIT_GIT = new Set(["commit", "branch"]);
22244
+ var DIR_TARGET = ".workit-live-target";
22245
+ var OVERRIDE = "WORKIT_LIVE_SESSION=override";
22246
+ 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("--"));
22247
+ var gitMutationDirs = (cwd, command, dialect) => {
22248
+ let invocations;
22249
+ try {
22250
+ invocations = rawInvocations(command, dialect);
22251
+ } catch {
22252
+ return [];
22253
+ }
22254
+ return invocations.flatMap((invocation) => {
22255
+ if (invocation.tool !== "git" || invocation.dir === undefined)
22256
+ return [];
22257
+ const at = invocation.args.findIndex((arg) => !arg.startsWith("-"));
22258
+ const sub = invocation.args[at] ?? "";
22259
+ const rest = invocation.args.slice(at + 1);
22260
+ if (!MUTATING_GIT.has(sub) || inert(rest, sub))
22261
+ return [];
22262
+ if (sub === "stash" && ["list", "show"].includes(rest[0] ?? ""))
22263
+ return [];
22264
+ return [path27.resolve(cwd, invocation.dir ?? ".")];
22265
+ });
22266
+ };
22267
+ var CD2 = new Set(["cd", "chdir", "set-location", "sl"]);
22268
+ var PUSHD2 = new Set(["pushd", "push-location"]);
22269
+ var POPD2 = new Set(["popd", "pop-location"]);
22270
+ var cdInto = (dir, words) => {
22271
+ const target = words.slice(1).find((word) => !word.startsWith("-") || word === "-");
22272
+ if (dir === undefined || target === "-" || target && /^~|[$`*?]/.test(target))
22273
+ return;
22274
+ return target === undefined ? os7.homedir() : path27.resolve(dir, target);
22275
+ };
22276
+ var quoteWord = (word) => /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
22277
+ var segmentTargets = (cwd, command, dialect) => {
22278
+ const targets = [];
22279
+ let dir = cwd;
22280
+ let stack = [];
22281
+ const groups = [];
22282
+ const at = (file) => path27.isAbsolute(file) ? file : dir === undefined ? null : path27.resolve(dir, file);
22283
+ for (const segment of segmentsOf(command, dialect)) {
22284
+ if (segment.group === "open") {
22285
+ groups.push({ dir, stack: [...stack] });
22286
+ continue;
22287
+ }
22288
+ if (segment.group === "close") {
22289
+ const saved = groups.pop();
22290
+ if (saved)
22291
+ ({ dir, stack } = saved);
22292
+ continue;
22293
+ }
22294
+ const lead = segment.words.findIndex((word) => !/^\w+=/.test(word));
22295
+ const words = lead < 0 ? [] : segment.words.slice(lead);
22296
+ const name = (words[0]?.split(/[\\/]/).at(-1) ?? "").toLowerCase();
22297
+ if (CD2.has(name)) {
22298
+ dir = cdInto(dir, words);
22299
+ continue;
22300
+ }
22301
+ if (PUSHD2.has(name)) {
22302
+ stack.push(dir);
22303
+ dir = cdInto(dir, words);
22304
+ continue;
22305
+ }
22306
+ if (POPD2.has(name)) {
22307
+ dir = stack.length ? stack.pop() : undefined;
22308
+ continue;
22309
+ }
22310
+ for (const file of segment.redirects)
22311
+ if (!/^\/dev\/(?:null|stdout|stderr|tty)$/.test(file)) {
22312
+ const resolved = at(file);
22313
+ if (resolved)
22314
+ targets.push(resolved);
22315
+ }
22316
+ const edits = editsOf(segment.words);
22317
+ if (edits === null) {
22318
+ if (dir !== undefined)
22319
+ targets.push(path27.join(dir, DIR_TARGET));
22320
+ } else
22321
+ for (const file of edits) {
22322
+ const resolved = at(file);
22323
+ if (resolved)
22324
+ targets.push(resolved);
22325
+ }
22326
+ 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), "")))
22327
+ targets.push(path27.join(dir, DIR_TARGET));
22328
+ }
22329
+ return targets;
22330
+ };
22331
+ var overridden = (command, dialect) => {
22332
+ const segments = segmentsOf(command, dialect);
22333
+ if (segments.length !== 1 || segments[0].group || segments[0].subs?.length)
22334
+ return false;
22335
+ const words = segments[0].words;
22336
+ const lead = words.findIndex((word) => !/^\w+=/.test(word));
22337
+ return lead > 0 && words.slice(0, lead).includes(OVERRIDE);
22338
+ };
22339
+ var HOST_LABELS = {
22340
+ claude_code: "Claude Code",
22341
+ opencode: "OpenCode",
22342
+ codex_cli: "Codex CLI",
22343
+ codex_desktop: "Codex Desktop",
22344
+ cursor: "Cursor",
22345
+ pi: "Pi"
22346
+ };
22347
+ var ageText = (ms) => {
22348
+ const seconds = Math.max(0, Math.round(ms / 1000));
22349
+ return seconds < 120 ? `${seconds}s` : `${Math.round(seconds / 60)} min`;
22350
+ };
22351
+ var liveDeny = ({ marker, checkout, ageMs, file }) => {
22352
+ 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}.`;
22353
+ return {
22354
+ kind: "deny",
22355
+ 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}`,
22356
+ unblock
22357
+ };
22358
+ };
22359
+ var guarded = (targets) => {
22360
+ const seen = new Set;
22361
+ return targets.filter((target) => {
22362
+ const dir = path27.dirname(target);
22363
+ if (seen.has(dir))
22364
+ return false;
22365
+ seen.add(dir);
22366
+ return true;
22367
+ });
22368
+ };
22369
+ var liveGate = (owner, targets, deps) => {
22370
+ try {
22371
+ for (const target of guarded(targets)) {
22372
+ const conflict = liveConflict(target, owner, deps);
22373
+ if (conflict && workspaceLiveSessionGuard(conflict.checkout))
22374
+ return liveDeny(conflict);
22375
+ }
22376
+ } catch {}
22377
+ return NONE3;
22378
+ };
22379
+ var claimAll = (owner, targets, deps) => {
22380
+ for (const target of guarded(targets))
22381
+ claimLiveSession(target, owner, deps);
22382
+ };
22383
+ var liveTargets = (cwd, paths) => paths?.length ? paths.map((file) => path27.resolve(cwd, file)) : [path27.join(cwd, DIR_TARGET)];
22384
+ function writeGate(cwd, paths, owner, deps) {
22385
+ if (!owner)
22386
+ return beforeWriteGate(cwd, paths);
22387
+ const targets = liveTargets(cwd, paths);
22388
+ const live = liveGate(owner, targets, deps);
22389
+ if (live.kind !== "none")
22390
+ return live;
22391
+ const decision = beforeWriteGate(cwd, paths);
22392
+ if (decision.kind === "none")
22393
+ claimAll(owner, targets, deps);
22394
+ return decision;
22395
+ }
22396
+ function shellGate(cwd, command, owner, dialect = "posix", deps) {
22397
+ const writes = shellWrites(command);
22398
+ let targets = [];
22399
+ if (owner) {
22400
+ try {
22401
+ targets = [
22402
+ ...segmentTargets(cwd, command, dialect),
22403
+ ...gitMutationDirs(cwd, command, dialect).map((dir) => path27.join(dir, DIR_TARGET))
22404
+ ];
22405
+ } catch {
22406
+ targets = [];
22407
+ }
22408
+ }
22409
+ if (owner && targets.length) {
22410
+ const live = liveGate(owner, targets, deps);
22411
+ if (live.kind !== "none") {
22412
+ if (!overridden(command, dialect))
22413
+ return live;
22414
+ noteOverride(cwd, owner, command);
22415
+ }
22416
+ }
22417
+ const decision = writes.writes ? beforeWriteGate(cwd, writes.targets) : NONE3;
22418
+ if (owner && targets.length && decision.kind === "none")
22419
+ claimAll(owner, targets, deps);
22420
+ return decision;
22421
+ }
22422
+ var noteOverride = (cwd, owner, command) => {
22423
+ try {
22424
+ if (!inWorkitWorkspace(cwd))
22425
+ return;
22426
+ appendHookObserved(cwd, {
22427
+ type: "live_session.override",
22428
+ actor: { host: owner.host, session: owner.session, agentId: null },
22429
+ session: owner.session,
22430
+ command: command.slice(0, 200)
22431
+ });
22432
+ } catch {}
22433
+ };
22434
+ function beforeWriteGate(cwd, paths) {
21963
22435
  try {
21964
22436
  const gate = gateFor(cwd);
21965
22437
  if (!gate || gate.blockers.length === 0)
@@ -21977,7 +22449,7 @@ function writeGate(cwd, paths) {
21977
22449
  }
21978
22450
  }
21979
22451
  // packages/workit-core/src/hooks/stop.ts
21980
- import { spawnSync as spawnSync17 } from "node:child_process";
22452
+ import { spawnSync as spawnSync18 } from "node:child_process";
21981
22453
  var NONE4 = { kind: "none" };
21982
22454
  var QUESTION = /[??؟](?=[\s*_`"')\]>]*(?:\s|$))|¿/;
21983
22455
  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;
@@ -21998,7 +22470,7 @@ var asksUser = (message) => {
21998
22470
  };
21999
22471
  var short = (sha) => (sha ?? "?").slice(0, 12);
22000
22472
  var gitList = (cwd, args) => {
22001
- const run = spawnSync17("git", args, {
22473
+ const run = spawnSync18("git", args, {
22002
22474
  cwd,
22003
22475
  encoding: "utf8",
22004
22476
  timeout: 3000,
@@ -22475,8 +22947,7 @@ function handleHook(input, deps) {
22475
22947
  const raw = canDeny ? rawGitPre(input, event.command, { nudge: false }) : NONE5;
22476
22948
  if (raw.kind === "deny")
22477
22949
  return raw;
22478
- const writes = usable2(descriptor.events["write.pre"].support) ? shellWrites(event.command) : { writes: false, targets: null };
22479
- const gate = writes.writes ? writeGate(input.cwd, writes.targets) : NONE5;
22950
+ const gate = usable2(descriptor.events["write.pre"].support) ? shellGate(input.cwd, event.command, liveOwner(input, descriptor.transport), event.dialect) : NONE5;
22480
22951
  if (gate.kind === "deny")
22481
22952
  return gate;
22482
22953
  noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
@@ -22489,7 +22960,7 @@ function handleHook(input, deps) {
22489
22960
  rawGitPost(input, event.command, callKey(event.toolUseId, event.command));
22490
22961
  return NONE5;
22491
22962
  case "write.pre":
22492
- return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths) : NONE5;
22963
+ return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths, liveOwner(input, descriptor.transport)) : NONE5;
22493
22964
  case "subagent.start":
22494
22965
  return {
22495
22966
  kind: "context",
@@ -22793,13 +23264,13 @@ var skillPromptNudge = (prompt, ctx) => process.env.WORKIT_PI_WORKER_SESSION ? n
22793
23264
 
22794
23265
  // packages/workit-pi/src/launches.ts
22795
23266
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
22796
- import os6 from "node:os";
22797
- import path27 from "node:path";
23267
+ import os8 from "node:os";
23268
+ import path28 from "node:path";
22798
23269
  var launchDir = (root) => {
22799
23270
  const location = resolveStore(root);
22800
- return location instanceof Error ? null : path27.join(location.dir, "pi-launches");
23271
+ return location instanceof Error ? null : path28.join(location.dir, "pi-launches");
22801
23272
  };
22802
- var fileFor = (dir, workerId) => path27.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
23273
+ var fileFor = (dir, workerId) => path28.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
22803
23274
  var recordLaunch = (root, attempt) => {
22804
23275
  const dir = launchDir(root);
22805
23276
  if (!dir)
@@ -22809,7 +23280,7 @@ var recordLaunch = (root, attempt) => {
22809
23280
  writeFileSync3(fileFor(dir, attempt.workerId), JSON.stringify({
22810
23281
  ...attempt,
22811
23282
  pid: process.pid,
22812
- host: os6.hostname(),
23283
+ host: os8.hostname(),
22813
23284
  at: new Date().toISOString()
22814
23285
  }), { flag: "wx" });
22815
23286
  return true;
@@ -22823,7 +23294,7 @@ var clearLaunch = (root, workerId) => {
22823
23294
  rmSync2(fileFor(dir, workerId), { force: true });
22824
23295
  };
22825
23296
  var alive = (attempt) => {
22826
- if (attempt.host !== os6.hostname())
23297
+ if (attempt.host !== os8.hostname())
22827
23298
  return true;
22828
23299
  try {
22829
23300
  process.kill(attempt.pid, 0);
@@ -22845,7 +23316,7 @@ var abandonedLaunches = (root) => {
22845
23316
  const found = [];
22846
23317
  for (const name of names) {
22847
23318
  try {
22848
- const attempt = JSON.parse(readFileSync6(path27.join(dir, name), "utf8"));
23319
+ const attempt = JSON.parse(readFileSync6(path28.join(dir, name), "utf8"));
22849
23320
  if (typeof attempt.workerId === "string" && typeof attempt.session === "string") {
22850
23321
  if (!alive(attempt))
22851
23322
  found.push(attempt);
@@ -23348,7 +23819,10 @@ var readAssignment = (ctx, request) => {
23348
23819
  return failure2("permission_denied", "worker assignment is not bound to this workspace");
23349
23820
  if (task.data.status !== "active")
23350
23821
  return failure2("invalid_transition", "paused or closed tasks cannot supervise workers");
23351
- const worker = task.data.workers.find((candidate) => candidate.id === request.workerId);
23822
+ const workerId = resolveIdRef("worker", request.workerId, task.data.workers.map((candidate) => candidate.id));
23823
+ if (!workerId.ok)
23824
+ return workerId;
23825
+ const worker = task.data.workers.find((candidate) => candidate.id === workerId.data);
23352
23826
  if (!worker)
23353
23827
  return failure2("not_found", "worker assignment not found");
23354
23828
  return success(null, workspace.data.revision, {
@@ -23495,7 +23969,7 @@ function extension(pi) {
23495
23969
  if (!validated.ok)
23496
23970
  return validated;
23497
23971
  const { store, task, workspace, worker } = validated.data;
23498
- const current = [...workers.values()].find((handle) => handle.workerId === request.workerId);
23972
+ const current = [...workers.values()].find((handle) => handle.workerId === worker.id);
23499
23973
  if (request.action === "cancel") {
23500
23974
  if (!current)
23501
23975
  return failure2("recovery_required", "worker process is not live in this session");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-pi",
3
- "version": "8.13.1",
3
+ "version": "8.14.1",
4
4
  "description": "Workit native extension for Pi — tools, method skills, and session continuity",
5
5
  "keywords": [
6
6
  "agentic-coding",
@@ -25,9 +25,9 @@ ceremony; do it yourself. Workit never spawns agents; you do.
25
25
  it; change the plan or the standing orders and render again.
26
26
  4. **Spawn in a rolling window**, in the background, each in its own worktree:
27
27
  keep 4-6 in flight and refill from `spawnable` in `workit fanout status` as
28
- each one finishes. Pick the model from TIER (`references/orchestration.md`).
29
- Claude Code: the `implementer` agent; elsewhere first `workit fanout
30
- worktree create <slice>`.
28
+ each one finishes. Model: the brief's `model:` line, else TIER
29
+ (`references/orchestration.md`). Claude Code: the `implementer` agent;
30
+ elsewhere first `workit fanout worktree create <slice>`.
31
31
  5. **Judge liveness by side effects only:** `workit fanout status` (head age,
32
32
  PR, CI, verdict, landed; STUCK past the TIMEBOX). Stop a stuck worker and
33
33
  observe that it exited (a timeout is not proof). Removing a worktree drops
@@ -53,7 +53,7 @@ ceremony; do it yourself. Workit never spawns agents; you do.
53
53
  status` order, release the worktrees you made, and `workit ledger standing
54
54
  clear`. Only you touch topology: workers never rebase, retarget or merge,
55
55
  except the integration-tip merge in integration mode. Then ship
56
- (read the `workit-ship` skill's SKILL.md and follow it).
56
+ (read the `workit-ship` skill's SKILL.md and follow it) up to `defaultEndpoint` from `workit grant show`.
57
57
 
58
58
  ## Example
59
59
 
@@ -5,8 +5,10 @@ and fans in. Workit records and checks; the host's own subagents run the work.
5
5
 
6
6
  ## TIER to model
7
7
 
8
- TIER is a field of each slice and of its brief. Map it where the host lets you
9
- pick a model per spawn; nothing in the workspace config routes it.
8
+ TIER is a field of each slice and of its brief. When the workspace sets
9
+ `fanout.tierModels`, the brief has a `model:` line under TIER and `workit
10
+ fanout status` names the model next to each spawnable slice: use that model.
11
+ Without one, map TIER where the host lets you pick a model per spawn:
10
12
 
11
13
  | TIER | Claude Code (`model` on the Agent call) | Hosts without a per-spawn model |
12
14
  | --- | --- | --- |