@brainervirus/workit-pi 8.13.1 → 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([
package/dist/workit.js CHANGED
@@ -13167,6 +13167,8 @@ var workspaceConfigSchema = object({
13167
13167
  defaultEndpoint: unknown().optional(),
13168
13168
  verification: _enum(["self", "independent"]).optional(),
13169
13169
  trailers: boolean2().optional(),
13170
+ liveSessionGuard: boolean2().optional(),
13171
+ fanout: object({ tierModels: unknown().optional() }).passthrough().optional(),
13170
13172
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
13171
13173
  kind,
13172
13174
  union([boolean2(), literal("verified")]).optional()
@@ -13484,24 +13486,39 @@ var resolveRuntimeReleaseTracks = (cwd) => {
13484
13486
  const matched = matchRuntimeWorkspace(cwd);
13485
13487
  return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
13486
13488
  };
13487
- var workspaceTrailers = (cwd, dir) => {
13489
+ var inheritedSetting = (cwd, dir, has) => {
13488
13490
  let matched;
13489
13491
  try {
13490
13492
  matched = matchRuntimeWorkspace(cwd, dir);
13491
13493
  } catch {
13492
- return { enabled: true, workspace: null, from: null };
13494
+ return null;
13493
13495
  }
13494
13496
  if (!matched)
13495
- return { enabled: true, workspace: null, from: null };
13497
+ return null;
13496
13498
  const { selected, matches } = matched;
13497
13499
  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");
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");
13499
13504
  return {
13500
- enabled: source?.trailers !== false,
13501
- workspace: selected.name,
13502
- from: source?.name ?? null
13505
+ enabled: found?.source?.trailers !== false,
13506
+ workspace: found?.workspace ?? null,
13507
+ from: found?.source?.name ?? null
13503
13508
  };
13504
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
+ ]);
13505
13522
  var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
13506
13523
  const matched = matchRuntimeWorkspace(cwd);
13507
13524
  if (!matched)
@@ -21803,27 +21820,231 @@ var shellPolicy = (cwd, command, dialect = "posix") => {
21803
21820
  };
21804
21821
  // packages/workit-core/src/hooks/write-gate.ts
21805
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";
21806
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
21807
22028
  var NONE3 = { kind: "none" };
21808
22029
  var CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
21809
22030
  var canonical2 = (file) => {
21810
22031
  let rest = "";
21811
- for (let dir = path26.resolve(file);; dir = path26.dirname(dir)) {
22032
+ for (let dir = path27.resolve(file);; dir = path27.dirname(dir)) {
21812
22033
  try {
21813
- return path26.join(realpathSync6(dir), rest);
22034
+ return path27.join(realpathSync6(dir), rest);
21814
22035
  } catch {
21815
- if (path26.dirname(dir) === dir)
21816
- return path26.resolve(file);
21817
- rest = rest ? path26.join(path26.basename(dir), rest) : path26.basename(dir);
22036
+ if (path27.dirname(dir) === dir)
22037
+ return path27.resolve(file);
22038
+ rest = rest ? path27.join(path27.basename(dir), rest) : path27.basename(dir);
21818
22039
  }
21819
22040
  }
21820
22041
  };
21821
22042
  var exempt = (cwd, root, file, plans) => {
21822
22043
  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))
22044
+ const relative = path27.relative(fold(canonical2(root)), fold(canonical2(path27.resolve(cwd, file))));
22045
+ if (relative.startsWith("..") || path27.isAbsolute(relative))
21825
22046
  return true;
21826
- const posix = relative.split(path26.sep).join("/");
22047
+ const posix = relative.split(path27.sep).join("/");
21827
22048
  const segments = posix.split("/");
21828
22049
  return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
21829
22050
  };
@@ -21921,7 +22142,7 @@ var shellWrites = (command) => {
21921
22142
  return { writes: true, targets: unknown ? null : targets };
21922
22143
  };
21923
22144
  var cache2 = new Map;
21924
- var planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path26.resolve(root, file)) ?? "-").join("\x00");
22145
+ var planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path27.resolve(root, file)) ?? "-").join("\x00");
21925
22146
  var gateFor = (cwd) => {
21926
22147
  const store = new TaskStore(cwd);
21927
22148
  const listed = store.listTaskIndex();
@@ -21959,7 +22180,218 @@ var gateFor = (cwd) => {
21959
22180
  cache2.delete(cache2.keys().next().value);
21960
22181
  return gate;
21961
22182
  };
21962
- function writeGate(cwd, paths) {
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) {
21963
22395
  try {
21964
22396
  const gate = gateFor(cwd);
21965
22397
  if (!gate || gate.blockers.length === 0)
@@ -21977,7 +22409,7 @@ function writeGate(cwd, paths) {
21977
22409
  }
21978
22410
  }
21979
22411
  // packages/workit-core/src/hooks/stop.ts
21980
- import { spawnSync as spawnSync17 } from "node:child_process";
22412
+ import { spawnSync as spawnSync18 } from "node:child_process";
21981
22413
  var NONE4 = { kind: "none" };
21982
22414
  var QUESTION = /[??؟](?=[\s*_`"')\]>]*(?:\s|$))|¿/;
21983
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;
@@ -21998,7 +22430,7 @@ var asksUser = (message) => {
21998
22430
  };
21999
22431
  var short = (sha) => (sha ?? "?").slice(0, 12);
22000
22432
  var gitList = (cwd, args) => {
22001
- const run = spawnSync17("git", args, {
22433
+ const run = spawnSync18("git", args, {
22002
22434
  cwd,
22003
22435
  encoding: "utf8",
22004
22436
  timeout: 3000,
@@ -22475,8 +22907,7 @@ function handleHook(input, deps) {
22475
22907
  const raw = canDeny ? rawGitPre(input, event.command, { nudge: false }) : NONE5;
22476
22908
  if (raw.kind === "deny")
22477
22909
  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;
22910
+ const gate = usable2(descriptor.events["write.pre"].support) ? shellGate(input.cwd, event.command, liveOwner(input, descriptor.transport), event.dialect) : NONE5;
22480
22911
  if (gate.kind === "deny")
22481
22912
  return gate;
22482
22913
  noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
@@ -22489,7 +22920,7 @@ function handleHook(input, deps) {
22489
22920
  rawGitPost(input, event.command, callKey(event.toolUseId, event.command));
22490
22921
  return NONE5;
22491
22922
  case "write.pre":
22492
- 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;
22493
22924
  case "subagent.start":
22494
22925
  return {
22495
22926
  kind: "context",
@@ -22793,13 +23224,13 @@ var skillPromptNudge = (prompt, ctx) => process.env.WORKIT_PI_WORKER_SESSION ? n
22793
23224
 
22794
23225
  // packages/workit-pi/src/launches.ts
22795
23226
  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";
23227
+ import os8 from "node:os";
23228
+ import path28 from "node:path";
22798
23229
  var launchDir = (root) => {
22799
23230
  const location = resolveStore(root);
22800
- return location instanceof Error ? null : path27.join(location.dir, "pi-launches");
23231
+ return location instanceof Error ? null : path28.join(location.dir, "pi-launches");
22801
23232
  };
22802
- var fileFor = (dir, workerId) => path27.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
23233
+ var fileFor = (dir, workerId) => path28.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
22803
23234
  var recordLaunch = (root, attempt) => {
22804
23235
  const dir = launchDir(root);
22805
23236
  if (!dir)
@@ -22809,7 +23240,7 @@ var recordLaunch = (root, attempt) => {
22809
23240
  writeFileSync3(fileFor(dir, attempt.workerId), JSON.stringify({
22810
23241
  ...attempt,
22811
23242
  pid: process.pid,
22812
- host: os6.hostname(),
23243
+ host: os8.hostname(),
22813
23244
  at: new Date().toISOString()
22814
23245
  }), { flag: "wx" });
22815
23246
  return true;
@@ -22823,7 +23254,7 @@ var clearLaunch = (root, workerId) => {
22823
23254
  rmSync2(fileFor(dir, workerId), { force: true });
22824
23255
  };
22825
23256
  var alive = (attempt) => {
22826
- if (attempt.host !== os6.hostname())
23257
+ if (attempt.host !== os8.hostname())
22827
23258
  return true;
22828
23259
  try {
22829
23260
  process.kill(attempt.pid, 0);
@@ -22845,7 +23276,7 @@ var abandonedLaunches = (root) => {
22845
23276
  const found = [];
22846
23277
  for (const name of names) {
22847
23278
  try {
22848
- const attempt = JSON.parse(readFileSync6(path27.join(dir, name), "utf8"));
23279
+ const attempt = JSON.parse(readFileSync6(path28.join(dir, name), "utf8"));
22849
23280
  if (typeof attempt.workerId === "string" && typeof attempt.session === "string") {
22850
23281
  if (!alive(attempt))
22851
23282
  found.push(attempt);
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.0",
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
  | --- | --- | --- |