@brainervirus/workit-cursor 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.
@@ -7624,6 +7624,16 @@ var init_zod = __esm(() => {
7624
7624
 
7625
7625
  // packages/workit-core/src/core/task-contract.ts
7626
7626
  import { createHash, randomUUID } from "node:crypto";
7627
+ function resolveIdRef(kind, ref, ids) {
7628
+ if (ids.includes(ref))
7629
+ return success(null, null, ref);
7630
+ const matches = ids.filter((candidate) => candidate.startsWith(ref));
7631
+ if (matches.length === 1)
7632
+ return success(null, null, matches[0]);
7633
+ if (matches.length === 0)
7634
+ return failure2("not_found", `${kind} not found`);
7635
+ return failure2("invalid_input", `${kind} prefix ${ref} is ambiguous; candidates: ${matches.join(", ")}`);
7636
+ }
7627
7637
  function parseOperation(family, input) {
7628
7638
  if (typeof input === "object" && input !== null && "schemaVersion" in input && input.schemaVersion !== SCHEMA_VERSION)
7629
7639
  return failure2("unsupported_version", "unsupported schema version", { operation: family });
@@ -7726,7 +7736,7 @@ var SCHEMA_VERSION = 1, POLICY_VERSION = "1.0.0", OPERATION_FAMILIES, uuidPatter
7726
7736
  return true;
7727
7737
  }
7728
7738
  return false;
7729
- }, text, id, digest, revision, validUtc = (value) => {
7739
+ }, text, id, idRefPattern, idRef, digest, revision, validUtc = (value) => {
7730
7740
  const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?Z$/.exec(value);
7731
7741
  if (!match)
7732
7742
  return false;
@@ -7909,6 +7919,8 @@ var init_task_contract = __esm(() => {
7909
7919
  ctx.issues.push({ code: "custom", input: ctx.value, message: "invalid Unicode", path: [] });
7910
7920
  });
7911
7921
  id = text.regex(uuidPattern, "expected lowercase UUID");
7922
+ idRefPattern = /^[0-9a-f][0-9a-f-]{7,35}$/;
7923
+ idRef = text.regex(idRefPattern, "expected a UUID or an id prefix of 8+ characters");
7912
7924
  digest = text.regex(digestPattern, "expected lowercase SHA-256 digest");
7913
7925
  revision = text.regex(uuidPattern, "expected UUID revision");
7914
7926
  utcSchema = text.check((ctx) => {
@@ -8177,7 +8189,7 @@ var init_task_contract = __esm(() => {
8177
8189
  outcome: _enum(["completed", "failed", "cancelled"]),
8178
8190
  summary: text,
8179
8191
  evidenceIds: array(id),
8180
- findingIds: array(id)
8192
+ findingIds: array(idRef)
8181
8193
  }).strict();
8182
8194
  workerSchema = object({
8183
8195
  assignment: assignmentSchema,
@@ -8369,7 +8381,7 @@ var init_task_contract = __esm(() => {
8369
8381
  action: literal("resolve"),
8370
8382
  ...taskId,
8371
8383
  expectedRevision: revision.optional(),
8372
- findingId: id,
8384
+ findingId: idRef,
8373
8385
  disposition: _enum(["open", "fixed", "dismissed", "deferred"]),
8374
8386
  reason: text,
8375
8387
  evidenceIds: array(id),
@@ -8410,7 +8422,7 @@ var init_task_contract = __esm(() => {
8410
8422
  ...taskId,
8411
8423
  expectedRevision: revision.optional(),
8412
8424
  expectedWorkspaceRevision: revision.optional(),
8413
- workerId: id,
8425
+ workerId: idRef,
8414
8426
  report: workerReportSchema
8415
8427
  }),
8416
8428
  cancel: operation({
@@ -8418,7 +8430,7 @@ var init_task_contract = __esm(() => {
8418
8430
  ...taskId,
8419
8431
  ...revisions,
8420
8432
  expectedWorkspaceRevision: revision.optional(),
8421
- workerId: id,
8433
+ workerId: idRef,
8422
8434
  reason: text
8423
8435
  })
8424
8436
  };
@@ -9570,24 +9582,26 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
9570
9582
  }, resolveRuntimeReleaseTracks = (cwd) => {
9571
9583
  const matched = matchRuntimeWorkspace(cwd);
9572
9584
  return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
9573
- }, workspaceTrailers = (cwd, dir) => {
9585
+ }, inheritedSetting = (cwd, dir, has) => {
9574
9586
  let matched;
9575
9587
  try {
9576
9588
  matched = matchRuntimeWorkspace(cwd, dir);
9577
9589
  } catch {
9578
- return { enabled: true, workspace: null, from: null };
9590
+ return null;
9579
9591
  }
9580
9592
  if (!matched)
9581
- return { enabled: true, workspace: null, from: null };
9593
+ return null;
9582
9594
  const { selected, matches } = matched;
9583
9595
  const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
9584
- const source = [selected, ...enclosing].find((entry) => typeof entry.trailers === "boolean");
9596
+ return { workspace: selected.name, source: [selected, ...enclosing].find(has) ?? null };
9597
+ }, workspaceTrailers = (cwd, dir) => {
9598
+ const found = inheritedSetting(cwd, dir, (entry) => typeof entry.trailers === "boolean");
9585
9599
  return {
9586
- enabled: source?.trailers !== false,
9587
- workspace: selected.name,
9588
- from: source?.name ?? null
9600
+ enabled: found?.source?.trailers !== false,
9601
+ workspace: found?.workspace ?? null,
9602
+ from: found?.source?.name ?? null
9589
9603
  };
9590
- }, resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
9604
+ }, workspaceLiveSessionGuard = (cwd, dir) => inheritedSetting(cwd, dir, (entry) => typeof entry.liveSessionGuard === "boolean")?.source?.liveSessionGuard !== false, TIER_KEYS, TIER_HOSTS, resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
9591
9605
  const matched = matchRuntimeWorkspace(cwd);
9592
9606
  if (!matched)
9593
9607
  return null;
@@ -9698,6 +9712,8 @@ var init_workspaces = __esm(() => {
9698
9712
  defaultEndpoint: unknown().optional(),
9699
9713
  verification: _enum(["self", "independent"]).optional(),
9700
9714
  trailers: boolean2().optional(),
9715
+ liveSessionGuard: boolean2().optional(),
9716
+ fanout: object({ tierModels: unknown().optional() }).passthrough().optional(),
9701
9717
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
9702
9718
  kind,
9703
9719
  union([boolean2(), literal("verified")]).optional()
@@ -9747,6 +9763,17 @@ var init_workspaces = __esm(() => {
9747
9763
  runtimeWorkspaceIndexSchema = object({
9748
9764
  workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
9749
9765
  }).passthrough();
9766
+ TIER_KEYS = new Set(["mundane", "standard", "hard"]);
9767
+ TIER_HOSTS = new Set([
9768
+ "claude_code",
9769
+ "opencode",
9770
+ "codex",
9771
+ "codex_cli",
9772
+ "codex_desktop",
9773
+ "cursor",
9774
+ "pi",
9775
+ "default"
9776
+ ]);
9750
9777
  });
9751
9778
 
9752
9779
  // packages/workit-core/src/core/release-tracks.ts
@@ -13447,20 +13474,26 @@ class TaskStore {
13447
13474
  return success(null, null, { key: key.data, bound: bound.data });
13448
13475
  }
13449
13476
  taskForWorker(workerId) {
13450
- if (typeof workerId !== "string" || !validId(workerId))
13477
+ if (typeof workerId !== "string" || !idRefPattern.test(workerId))
13451
13478
  return failure2("invalid_input", "needs a worker id: `workit worker cancel|report <workerId>`");
13452
13479
  const listed = this.listTaskIndex();
13453
13480
  if (!listed.ok)
13454
13481
  return listed;
13482
+ const owners = new Map;
13455
13483
  for (const entry of listed.data) {
13456
13484
  if (entry.status === "closed")
13457
13485
  continue;
13458
13486
  const loaded = this.load(entry.id);
13459
13487
  if (!loaded.ok)
13460
13488
  return loaded;
13461
- if (loaded.data?.record.workers.some((worker) => worker.id === workerId))
13462
- return success(null, null, entry.id);
13489
+ for (const worker of loaded.data?.record.workers ?? [])
13490
+ owners.set(worker.id, entry.id);
13463
13491
  }
13492
+ const resolved = resolveIdRef("worker", workerId, [...owners.keys()]);
13493
+ if (resolved.ok)
13494
+ return success(null, null, owners.get(resolved.data) ?? null);
13495
+ if (resolved.code !== "not_found")
13496
+ return resolved;
13464
13497
  return success(null, null, null);
13465
13498
  }
13466
13499
  originTaskEntry() {
@@ -16719,6 +16752,10 @@ class WorkitCore {
16719
16752
  }
16720
16753
  if (helper?.ok)
16721
16754
  return failure2("permission_denied", "helpers cannot resolve findings");
16755
+ const findingRef = resolveIdRef("finding", input.findingId, task.data.findings.map((entry) => entry.id));
16756
+ if (!findingRef.ok)
16757
+ return findingRef;
16758
+ input.findingId = findingRef.data;
16722
16759
  const finding = task.data.findings.find((entry) => entry.id === input.findingId);
16723
16760
  if (!finding)
16724
16761
  return failure2("not_found", "finding not found");
@@ -16870,15 +16907,31 @@ class WorkitCore {
16870
16907
  return changed;
16871
16908
  return success(changed.data.task.revision, changed.data.workspace.revision, changed.data.task.workers.at(-1));
16872
16909
  }
16910
+ const workerRef = resolveIdRef("worker", input.workerId, helperId === null ? task.data.workers.map((candidate) => candidate.id) : [helperId]);
16911
+ if (!workerRef.ok)
16912
+ return helperId === null ? workerRef : failure2("permission_denied", input.action === "report" ? ONLY_OWN_REPORT : NO_HELPER_CONTROL);
16913
+ input.workerId = workerRef.data;
16873
16914
  const entry = task.data.workers.find((candidate) => candidate.id === input.workerId);
16874
16915
  if (!entry)
16875
16916
  return failure2("not_found", "worker not found");
16876
16917
  if (input.action === "report") {
16877
16918
  if (helperId === null || helperId !== input.workerId)
16878
- return failure2("permission_denied", "workers can submit only their own report");
16919
+ return failure2("permission_denied", ONLY_OWN_REPORT);
16879
16920
  const helper = this.helperEntry(task.data, true, true);
16880
16921
  if (!helper.ok)
16881
16922
  return helper;
16923
+ const ownFindings = task.data.findings.filter((candidate) => candidate.provenance.workerId === helperId).map((candidate) => candidate.id);
16924
+ const reported = [];
16925
+ for (const findingRef of input.report.findingIds) {
16926
+ const found = resolveIdRef("finding", findingRef, ownFindings);
16927
+ if (found.ok)
16928
+ reported.push(found.data);
16929
+ else if (found.code === "not_found")
16930
+ reported.push(findingRef);
16931
+ else
16932
+ return found;
16933
+ }
16934
+ input.report.findingIds = reported;
16882
16935
  const evidence = task.data.evidence.filter((candidate) => input.report.evidenceIds.includes(candidate.id));
16883
16936
  const findings = task.data.findings.filter((candidate) => input.report.findingIds.includes(candidate.id));
16884
16937
  if (evidence.length !== input.report.evidenceIds.length || findings.length !== input.report.findingIds.length)
@@ -16906,7 +16959,7 @@ class WorkitCore {
16906
16959
  return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during report");
16907
16960
  }
16908
16961
  if (helperId !== null)
16909
- return failure2("permission_denied", "helpers cannot cancel or control workers");
16962
+ return failure2("permission_denied", NO_HELPER_CONTROL);
16910
16963
  if (task.data.status === "closed")
16911
16964
  return failure2("invalid_transition", "closed task cannot cancel workers");
16912
16965
  if (entry.data.state === "stopped") {
@@ -17435,7 +17488,7 @@ var MAX_JUDGED_REFS = 5, workspaceVerification = (root) => {
17435
17488
  return changed;
17436
17489
  const updated = changed.data.task.workers.find((candidate) => candidate.id === input.workerId);
17437
17490
  return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during lifecycle observation");
17438
- }, refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || scopeCovers(scope, { description: "", paths: [ref.path], exclusions: [] })), TASK_SCOPED_ACTIONS, WORKER_ADDRESSED_ACTIONS, workerCommand = (taskId, request) => {
17491
+ }, refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || scopeCovers(scope, { description: "", paths: [ref.path], exclusions: [] })), TASK_SCOPED_ACTIONS, ONLY_OWN_REPORT = "workers can submit only their own report", NO_HELPER_CONTROL = "helpers cannot cancel or control workers", WORKER_ADDRESSED_ACTIONS, workerCommand = (taskId, request) => {
17439
17492
  const { action, workerId, reason, report } = request;
17440
17493
  const rest = JSON.stringify({ reason, report });
17441
17494
  const payload = rest === "{}" ? "" : ` --payload '${rest.replace(/'/gu, "'\\''")}'`;
@@ -20052,10 +20105,238 @@ var init_policy = __esm(() => {
20052
20105
  init_shell_words();
20053
20106
  });
20054
20107
 
20108
+ // packages/workit-core/src/core/live-session.ts
20109
+ import { spawnSync as spawnSync13 } from "node:child_process";
20110
+ import { createHash as createHash15 } from "node:crypto";
20111
+ import fs17 from "node:fs";
20112
+ import os6 from "node:os";
20113
+ import path22 from "node:path";
20114
+ var LIVE_SESSION_TTL_MS, HEARTBEAT_EVERY_MS = 30000, liveProbe, leadOf = (session) => session.split(":")[0].replace(/-w-.*$/su, "") || session, familyOf = (host) => host.replace(/_(?:cli|desktop)$/u, ""), processAlive = (pid) => {
20115
+ try {
20116
+ process.kill(pid, 0);
20117
+ return true;
20118
+ } catch (error) {
20119
+ return error.code === "EPERM";
20120
+ }
20121
+ }, processInfo = (pid) => {
20122
+ try {
20123
+ if (liveProbe.platform === "linux") {
20124
+ const stat = fs17.readFileSync(`/proc/${pid}/stat`, "utf8");
20125
+ const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
20126
+ const command = fs17.readFileSync(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ");
20127
+ return Number.isInteger(ppid) ? { ppid, command: command.trim() } : null;
20128
+ }
20129
+ if (liveProbe.platform === "win32")
20130
+ return null;
20131
+ const match = /^\s*(\d+)\s+(.*)$/su.exec(liveProbe.run(["-o", "ppid=,command=", "-p", String(pid)]));
20132
+ return match ? { ppid: Number(match[1]), command: match[2].trim() } : null;
20133
+ } catch {
20134
+ return null;
20135
+ }
20136
+ }, WRAPPER, PIDS, hostPid = (transport) => {
20137
+ if (transport !== "hook-process")
20138
+ return process.pid;
20139
+ const key = `${liveProbe.platform}\x00${process.ppid}`;
20140
+ if (PIDS.has(key))
20141
+ return PIDS.get(key) ?? null;
20142
+ if (liveProbe.platform === "win32") {
20143
+ PIDS.set(key, null);
20144
+ return null;
20145
+ }
20146
+ let pid = process.ppid;
20147
+ for (let hop = 0;hop < 4; hop++) {
20148
+ const info = processInfo(pid);
20149
+ if (!info || !WRAPPER.test(info.command) || info.ppid <= 1)
20150
+ break;
20151
+ pid = info.ppid;
20152
+ }
20153
+ PIDS.set(key, pid);
20154
+ return pid;
20155
+ }, sessionSafe = (value) => value.replace(/[^A-Za-z0-9_.:@/+-]/gu, "-"), liveOwner = (input, transport, env = process.env) => {
20156
+ const session = input.session.id?.trim();
20157
+ if (!session)
20158
+ return null;
20159
+ const leads = new Set([leadOf(sessionSafe(session))]);
20160
+ if (input.session.parentId)
20161
+ leads.add(leadOf(sessionSafe(input.session.parentId)));
20162
+ if (env.WORKIT_PI_WORKER_SESSION && env.WORKIT_SESSION_ID)
20163
+ leads.add(leadOf(sessionSafe(env.WORKIT_SESSION_ID)));
20164
+ return {
20165
+ host: input.host,
20166
+ session: sessionSafe(session),
20167
+ leads: [...leads],
20168
+ pid: () => hostPid(transport)
20169
+ };
20170
+ }, checkoutOf = (target) => {
20171
+ let dir = path22.resolve(target);
20172
+ while (!fs17.existsSync(dir) && path22.dirname(dir) !== dir)
20173
+ dir = path22.dirname(dir);
20174
+ if (fs17.existsSync(dir) && !fs17.statSync(dir).isDirectory())
20175
+ dir = path22.dirname(dir);
20176
+ const parts = dir.split(path22.sep);
20177
+ const dotGit = parts.lastIndexOf(".git");
20178
+ if (dotGit > 0)
20179
+ dir = parts.slice(0, dotGit).join(path22.sep) || path22.sep;
20180
+ const location = resolveStore(dir);
20181
+ if (location instanceof Error || !location.shared || !location.top)
20182
+ return null;
20183
+ let top = location.top;
20184
+ try {
20185
+ top = fs17.realpathSync.native(top);
20186
+ } catch {}
20187
+ return top;
20188
+ }, markerFile2 = (top) => path22.join(resolveStateDir(), "live-sessions", `${createHash15("sha256").update(checkoutKey(top)).digest("hex").slice(0, 32)}.json`), checkoutKey = (top, platform = process.platform) => {
20189
+ let key = path22.resolve(top);
20190
+ try {
20191
+ key = fs17.realpathSync.native(key);
20192
+ } catch {}
20193
+ return platform === "win32" ? key.replaceAll("/", "\\").toLowerCase() : key;
20194
+ }, readMarker = (file) => {
20195
+ try {
20196
+ const value = JSON.parse(fs17.readFileSync(file, "utf8"));
20197
+ if (value?.version !== 1 || typeof value.session !== "string" || typeof value.host !== "string" || typeof value.heartbeatAt !== "string" || Number.isNaN(Date.parse(value.heartbeatAt)))
20198
+ return null;
20199
+ return {
20200
+ version: 1,
20201
+ session: value.session,
20202
+ leads: Array.isArray(value.leads) ? value.leads.filter((item) => typeof item === "string") : [],
20203
+ host: value.host,
20204
+ pid: typeof value.pid === "number" ? value.pid : null,
20205
+ pidStart: typeof value.pidStart === "string" ? value.pidStart : null,
20206
+ machine: typeof value.machine === "string" ? value.machine : "",
20207
+ checkout: typeof value.checkout === "string" ? value.checkout : "",
20208
+ heartbeatAt: value.heartbeatAt
20209
+ };
20210
+ } catch {
20211
+ return null;
20212
+ }
20213
+ }, ageOf2 = (marker, now) => now.getTime() - Date.parse(marker.heartbeatAt), isLive = (marker, deps) => {
20214
+ const age = ageOf2(marker, deps.now ?? new Date);
20215
+ if (age > (deps.ttlMs ?? LIVE_SESSION_TTL_MS))
20216
+ return false;
20217
+ if (marker.machine !== (deps.machine ?? os6.hostname()) || marker.pid === null)
20218
+ return true;
20219
+ if (!(deps.alive ?? processAlive)(marker.pid))
20220
+ return false;
20221
+ if (marker.pidStart === null)
20222
+ return true;
20223
+ const start = liveProbe.start(marker.pid);
20224
+ return start === null || start === marker.pidStart;
20225
+ }, sameLead = (marker, owner) => marker.session === owner.session || owner.leads.some((lead) => lead === marker.session || marker.leads.includes(lead)), sameOwner = (marker, owner, machine) => {
20226
+ if (sameLead(marker, owner))
20227
+ return true;
20228
+ if (marker.machine !== machine)
20229
+ return false;
20230
+ const pid = owner.pid();
20231
+ if (pid === null || marker.pid === null || marker.pidStart === null)
20232
+ return familyOf(marker.host) === familyOf(owner.host);
20233
+ return marker.pid === pid;
20234
+ }, liveConflict = (target, owner, deps = {}) => {
20235
+ try {
20236
+ const checkout = checkoutOf(target);
20237
+ if (!checkout)
20238
+ return null;
20239
+ const file = markerFile2(checkout);
20240
+ const marker = readMarker(file);
20241
+ if (!marker || sameLead(marker, owner) || !isLive(marker, deps))
20242
+ return null;
20243
+ if (sameOwner(marker, owner, deps.machine ?? os6.hostname()))
20244
+ return null;
20245
+ return { marker, file, ageMs: ageOf2(marker, deps.now ?? new Date), checkout };
20246
+ } catch {
20247
+ return null;
20248
+ }
20249
+ }, claimLiveSession = (target, owner, deps = {}) => {
20250
+ try {
20251
+ const checkout = checkoutOf(target);
20252
+ if (!checkout)
20253
+ return;
20254
+ const file = markerFile2(checkout);
20255
+ const now = deps.now ?? new Date;
20256
+ const machine = deps.machine ?? os6.hostname();
20257
+ const current = readMarker(file);
20258
+ if (current) {
20259
+ const age = ageOf2(current, now);
20260
+ if (sameLead(current, owner) && age >= 0 && age < HEARTBEAT_EVERY_MS)
20261
+ return;
20262
+ if (!sameLead(current, owner) && isLive(current, deps) && !sameOwner(current, owner, machine))
20263
+ return;
20264
+ }
20265
+ const pid = owner.pid();
20266
+ const marker = {
20267
+ version: 1,
20268
+ session: owner.session,
20269
+ leads: owner.leads,
20270
+ host: owner.host,
20271
+ pid,
20272
+ pidStart: pid === null ? null : liveProbe.start(pid),
20273
+ machine,
20274
+ checkout,
20275
+ heartbeatAt: now.toISOString()
20276
+ };
20277
+ fs17.mkdirSync(path22.dirname(file), { recursive: true });
20278
+ const temp = `${file}.${process.pid}.tmp`;
20279
+ fs17.writeFileSync(temp, JSON.stringify(marker));
20280
+ fs17.renameSync(temp, file);
20281
+ } catch {}
20282
+ };
20283
+ var init_live_session = __esm(() => {
20284
+ init_paths();
20285
+ init_logger();
20286
+ init_store_lock();
20287
+ LIVE_SESSION_TTL_MS = 15 * 60000;
20288
+ liveProbe = {
20289
+ platform: process.platform,
20290
+ run: (args) => spawnSync13("ps", args, { encoding: "utf8", timeout: 2000 }).stdout ?? "",
20291
+ start: (pid) => processStartOf(pid)
20292
+ };
20293
+ 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;
20294
+ PIDS = new Map;
20295
+ });
20296
+
20055
20297
  // packages/workit-core/src/hooks/write-gate.ts
20056
20298
  import { realpathSync as realpathSync5 } from "node:fs";
20057
- import path22 from "node:path";
20058
- function writeGate(cwd, paths) {
20299
+ import os7 from "node:os";
20300
+ import path23 from "node:path";
20301
+ function writeGate(cwd, paths, owner, deps) {
20302
+ if (!owner)
20303
+ return beforeWriteGate(cwd, paths);
20304
+ const targets = liveTargets(cwd, paths);
20305
+ const live = liveGate(owner, targets, deps);
20306
+ if (live.kind !== "none")
20307
+ return live;
20308
+ const decision = beforeWriteGate(cwd, paths);
20309
+ if (decision.kind === "none")
20310
+ claimAll(owner, targets, deps);
20311
+ return decision;
20312
+ }
20313
+ function shellGate(cwd, command, owner, dialect = "posix", deps) {
20314
+ const writes = shellWrites(command);
20315
+ let targets = [];
20316
+ if (owner) {
20317
+ try {
20318
+ targets = [
20319
+ ...segmentTargets(cwd, command, dialect),
20320
+ ...gitMutationDirs(cwd, command, dialect).map((dir) => path23.join(dir, DIR_TARGET))
20321
+ ];
20322
+ } catch {
20323
+ targets = [];
20324
+ }
20325
+ }
20326
+ if (owner && targets.length) {
20327
+ const live = liveGate(owner, targets, deps);
20328
+ if (live.kind !== "none") {
20329
+ if (!overridden(command, dialect))
20330
+ return live;
20331
+ noteOverride(cwd, owner, command);
20332
+ }
20333
+ }
20334
+ const decision = writes.writes ? beforeWriteGate(cwd, writes.targets) : NONE3;
20335
+ if (owner && targets.length && decision.kind === "none")
20336
+ claimAll(owner, targets, deps);
20337
+ return decision;
20338
+ }
20339
+ function beforeWriteGate(cwd, paths) {
20059
20340
  try {
20060
20341
  const gate = gateFor(cwd);
20061
20342
  if (!gate || gate.blockers.length === 0)
@@ -20074,21 +20355,21 @@ function writeGate(cwd, paths) {
20074
20355
  }
20075
20356
  var NONE3, CASE_INSENSITIVE, canonical2 = (file) => {
20076
20357
  let rest = "";
20077
- for (let dir = path22.resolve(file);; dir = path22.dirname(dir)) {
20358
+ for (let dir = path23.resolve(file);; dir = path23.dirname(dir)) {
20078
20359
  try {
20079
- return path22.join(realpathSync5(dir), rest);
20360
+ return path23.join(realpathSync5(dir), rest);
20080
20361
  } catch {
20081
- if (path22.dirname(dir) === dir)
20082
- return path22.resolve(file);
20083
- rest = rest ? path22.join(path22.basename(dir), rest) : path22.basename(dir);
20362
+ if (path23.dirname(dir) === dir)
20363
+ return path23.resolve(file);
20364
+ rest = rest ? path23.join(path23.basename(dir), rest) : path23.basename(dir);
20084
20365
  }
20085
20366
  }
20086
20367
  }, exempt = (cwd, root, file, plans) => {
20087
20368
  const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
20088
- const relative = path22.relative(fold(canonical2(root)), fold(canonical2(path22.resolve(cwd, file))));
20089
- if (relative.startsWith("..") || path22.isAbsolute(relative))
20369
+ const relative = path23.relative(fold(canonical2(root)), fold(canonical2(path23.resolve(cwd, file))));
20370
+ if (relative.startsWith("..") || path23.isAbsolute(relative))
20090
20371
  return true;
20091
- const posix = relative.split(path22.sep).join("/");
20372
+ const posix = relative.split(path23.sep).join("/");
20092
20373
  const segments = posix.split("/");
20093
20374
  return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
20094
20375
  }, PREFIXES2, operands = (words) => words.filter((item) => !item.startsWith("-")), editsOf = (input) => {
@@ -20180,7 +20461,7 @@ var NONE3, CASE_INSENSITIVE, canonical2 = (file) => {
20180
20461
  if (!unknown && targets.length === 0)
20181
20462
  return { writes: false, targets: null };
20182
20463
  return { writes: true, targets: unknown ? null : targets };
20183
- }, cache2, planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path22.resolve(root, file)) ?? "-").join("\x00"), gateFor = (cwd) => {
20464
+ }, cache2, planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path23.resolve(root, file)) ?? "-").join("\x00"), gateFor = (cwd) => {
20184
20465
  const store = new TaskStore(cwd);
20185
20466
  const listed = store.listTaskIndex();
20186
20467
  if (!listed.ok)
@@ -20216,21 +20497,182 @@ var NONE3, CASE_INSENSITIVE, canonical2 = (file) => {
20216
20497
  if (cache2.size > 32)
20217
20498
  cache2.delete(cache2.keys().next().value);
20218
20499
  return gate;
20500
+ }, MUTATING_GIT, DRY_N, MUTATING_WORKIT_GIT, DIR_TARGET = ".workit-live-target", OVERRIDE = "WORKIT_LIVE_SESSION=override", 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("--")), gitMutationDirs = (cwd, command, dialect) => {
20501
+ let invocations;
20502
+ try {
20503
+ invocations = rawInvocations(command, dialect);
20504
+ } catch {
20505
+ return [];
20506
+ }
20507
+ return invocations.flatMap((invocation) => {
20508
+ if (invocation.tool !== "git" || invocation.dir === undefined)
20509
+ return [];
20510
+ const at = invocation.args.findIndex((arg) => !arg.startsWith("-"));
20511
+ const sub = invocation.args[at] ?? "";
20512
+ const rest = invocation.args.slice(at + 1);
20513
+ if (!MUTATING_GIT.has(sub) || inert(rest, sub))
20514
+ return [];
20515
+ if (sub === "stash" && ["list", "show"].includes(rest[0] ?? ""))
20516
+ return [];
20517
+ return [path23.resolve(cwd, invocation.dir ?? ".")];
20518
+ });
20519
+ }, CD2, PUSHD2, POPD2, cdInto = (dir, words) => {
20520
+ const target = words.slice(1).find((word) => !word.startsWith("-") || word === "-");
20521
+ if (dir === undefined || target === "-" || target && /^~|[$`*?]/.test(target))
20522
+ return;
20523
+ return target === undefined ? os7.homedir() : path23.resolve(dir, target);
20524
+ }, quoteWord = (word) => /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`, segmentTargets = (cwd, command, dialect) => {
20525
+ const targets = [];
20526
+ let dir = cwd;
20527
+ let stack = [];
20528
+ const groups = [];
20529
+ const at = (file) => path23.isAbsolute(file) ? file : dir === undefined ? null : path23.resolve(dir, file);
20530
+ for (const segment of segmentsOf(command, dialect)) {
20531
+ if (segment.group === "open") {
20532
+ groups.push({ dir, stack: [...stack] });
20533
+ continue;
20534
+ }
20535
+ if (segment.group === "close") {
20536
+ const saved = groups.pop();
20537
+ if (saved)
20538
+ ({ dir, stack } = saved);
20539
+ continue;
20540
+ }
20541
+ const lead = segment.words.findIndex((word) => !/^\w+=/.test(word));
20542
+ const words = lead < 0 ? [] : segment.words.slice(lead);
20543
+ const name = (words[0]?.split(/[\\/]/).at(-1) ?? "").toLowerCase();
20544
+ if (CD2.has(name)) {
20545
+ dir = cdInto(dir, words);
20546
+ continue;
20547
+ }
20548
+ if (PUSHD2.has(name)) {
20549
+ stack.push(dir);
20550
+ dir = cdInto(dir, words);
20551
+ continue;
20552
+ }
20553
+ if (POPD2.has(name)) {
20554
+ dir = stack.length ? stack.pop() : undefined;
20555
+ continue;
20556
+ }
20557
+ for (const file of segment.redirects)
20558
+ if (!/^\/dev\/(?:null|stdout|stderr|tty)$/.test(file)) {
20559
+ const resolved = at(file);
20560
+ if (resolved)
20561
+ targets.push(resolved);
20562
+ }
20563
+ const edits = editsOf(segment.words);
20564
+ if (edits === null) {
20565
+ if (dir !== undefined)
20566
+ targets.push(path23.join(dir, DIR_TARGET));
20567
+ } else
20568
+ for (const file of edits) {
20569
+ const resolved = at(file);
20570
+ if (resolved)
20571
+ targets.push(resolved);
20572
+ }
20573
+ 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), "")))
20574
+ targets.push(path23.join(dir, DIR_TARGET));
20575
+ }
20576
+ return targets;
20577
+ }, overridden = (command, dialect) => {
20578
+ const segments = segmentsOf(command, dialect);
20579
+ if (segments.length !== 1 || segments[0].group || segments[0].subs?.length)
20580
+ return false;
20581
+ const words = segments[0].words;
20582
+ const lead = words.findIndex((word) => !/^\w+=/.test(word));
20583
+ return lead > 0 && words.slice(0, lead).includes(OVERRIDE);
20584
+ }, HOST_LABELS, ageText = (ms) => {
20585
+ const seconds = Math.max(0, Math.round(ms / 1000));
20586
+ return seconds < 120 ? `${seconds}s` : `${Math.round(seconds / 60)} min`;
20587
+ }, liveDeny = ({ marker, checkout, ageMs, file }) => {
20588
+ 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}.`;
20589
+ return {
20590
+ kind: "deny",
20591
+ 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}`,
20592
+ unblock
20593
+ };
20594
+ }, guarded = (targets) => {
20595
+ const seen = new Set;
20596
+ return targets.filter((target) => {
20597
+ const dir = path23.dirname(target);
20598
+ if (seen.has(dir))
20599
+ return false;
20600
+ seen.add(dir);
20601
+ return true;
20602
+ });
20603
+ }, liveGate = (owner, targets, deps) => {
20604
+ try {
20605
+ for (const target of guarded(targets)) {
20606
+ const conflict = liveConflict(target, owner, deps);
20607
+ if (conflict && workspaceLiveSessionGuard(conflict.checkout))
20608
+ return liveDeny(conflict);
20609
+ }
20610
+ } catch {}
20611
+ return NONE3;
20612
+ }, claimAll = (owner, targets, deps) => {
20613
+ for (const target of guarded(targets))
20614
+ claimLiveSession(target, owner, deps);
20615
+ }, liveTargets = (cwd, paths) => paths?.length ? paths.map((file) => path23.resolve(cwd, file)) : [path23.join(cwd, DIR_TARGET)], noteOverride = (cwd, owner, command) => {
20616
+ try {
20617
+ if (!inWorkitWorkspace(cwd))
20618
+ return;
20619
+ appendHookObserved(cwd, {
20620
+ type: "live_session.override",
20621
+ actor: { host: owner.host, session: owner.session, agentId: null },
20622
+ session: owner.session,
20623
+ command: command.slice(0, 200)
20624
+ });
20625
+ } catch {}
20219
20626
  };
20220
20627
  var init_write_gate = __esm(() => {
20221
20628
  init_task_store();
20222
20629
  init_derive();
20223
20630
  init_task_evaluation();
20631
+ init_live_session();
20632
+ init_workspaces();
20224
20633
  init_ledger();
20634
+ init_raw_git();
20225
20635
  init_shell_words();
20226
20636
  NONE3 = { kind: "none" };
20227
20637
  CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
20228
20638
  PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs"]);
20229
20639
  cache2 = new Map;
20640
+ MUTATING_GIT = new Set([
20641
+ "add",
20642
+ "am",
20643
+ "apply",
20644
+ "checkout",
20645
+ "cherry-pick",
20646
+ "clean",
20647
+ "commit",
20648
+ "merge",
20649
+ "mv",
20650
+ "pull",
20651
+ "rebase",
20652
+ "reset",
20653
+ "restore",
20654
+ "revert",
20655
+ "rm",
20656
+ "stash",
20657
+ "switch"
20658
+ ]);
20659
+ DRY_N = new Set(["add", "clean", "rm", "mv"]);
20660
+ MUTATING_WORKIT_GIT = new Set(["commit", "branch"]);
20661
+ CD2 = new Set(["cd", "chdir", "set-location", "sl"]);
20662
+ PUSHD2 = new Set(["pushd", "push-location"]);
20663
+ POPD2 = new Set(["popd", "pop-location"]);
20664
+ HOST_LABELS = {
20665
+ claude_code: "Claude Code",
20666
+ opencode: "OpenCode",
20667
+ codex_cli: "Codex CLI",
20668
+ codex_desktop: "Codex Desktop",
20669
+ cursor: "Cursor",
20670
+ pi: "Pi"
20671
+ };
20230
20672
  });
20231
20673
 
20232
20674
  // packages/workit-core/src/hooks/stop.ts
20233
- import { spawnSync as spawnSync13 } from "node:child_process";
20675
+ import { spawnSync as spawnSync14 } from "node:child_process";
20234
20676
  function stopObligation(input, descriptor) {
20235
20677
  try {
20236
20678
  const session = input.session.id;
@@ -20294,7 +20736,7 @@ var NONE4, QUESTION, OFFER, YES_NO, LIST_ITEM, asksUser = (message) => {
20294
20736
  `);
20295
20737
  return QUESTION.test(tail) || OFFER.test(tail) || YES_NO.test(tail);
20296
20738
  }, short = (sha) => (sha ?? "?").slice(0, 12), gitList = (cwd, args) => {
20297
- const run = spawnSync13("git", args, {
20739
+ const run = spawnSync14("git", args, {
20298
20740
  cwd,
20299
20741
  encoding: "utf8",
20300
20742
  timeout: 3000,
@@ -20606,9 +21048,9 @@ var init_skill_nudge = __esm(() => {
20606
21048
 
20607
21049
  // packages/workit-core/src/hooks/hosts/fields.ts
20608
21050
  import { existsSync as existsSync7, realpathSync as realpathSync6, statSync as statSync2 } from "node:fs";
20609
- import path23 from "node:path";
21051
+ import path24 from "node:path";
20610
21052
  var isRecord4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value), nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "", optionalText = (value) => typeof value === "string" ? value : null, existingDirectory = (value) => {
20611
- if (!nonEmpty2(value) || !path23.isAbsolute(value) || !existsSync7(value))
21053
+ if (!nonEmpty2(value) || !path24.isAbsolute(value) || !existsSync7(value))
20612
21054
  return null;
20613
21055
  try {
20614
21056
  return statSync2(value).isDirectory() ? realpathSync6(value) : null;
@@ -20713,8 +21155,7 @@ function handleHook(input, deps) {
20713
21155
  const raw = canDeny ? rawGitPre(input, event.command, { nudge: false }) : NONE5;
20714
21156
  if (raw.kind === "deny")
20715
21157
  return raw;
20716
- const writes = usable2(descriptor.events["write.pre"].support) ? shellWrites(event.command) : { writes: false, targets: null };
20717
- const gate = writes.writes ? writeGate(input.cwd, writes.targets) : NONE5;
21158
+ const gate = usable2(descriptor.events["write.pre"].support) ? shellGate(input.cwd, event.command, liveOwner(input, descriptor.transport), event.dialect) : NONE5;
20718
21159
  if (gate.kind === "deny")
20719
21160
  return gate;
20720
21161
  noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
@@ -20727,7 +21168,7 @@ function handleHook(input, deps) {
20727
21168
  rawGitPost(input, event.command, callKey(event.toolUseId, event.command));
20728
21169
  return NONE5;
20729
21170
  case "write.pre":
20730
- return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths) : NONE5;
21171
+ return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths, liveOwner(input, descriptor.transport)) : NONE5;
20731
21172
  case "subagent.start":
20732
21173
  return {
20733
21174
  kind: "context",
@@ -20810,6 +21251,7 @@ var init_handle = __esm(() => {
20810
21251
  init_raw_git();
20811
21252
  init_skill_nudge();
20812
21253
  init_stop();
21254
+ init_live_session();
20813
21255
  init_write_gate();
20814
21256
  NONE5 = { kind: "none" };
20815
21257
  offered = new Set;
@@ -21348,8 +21790,8 @@ var init_codex = __esm(() => {
21348
21790
  });
21349
21791
 
21350
21792
  // packages/workit-core/src/hooks/hosts/cursor.ts
21351
- import fs17 from "node:fs";
21352
- import path24 from "node:path";
21793
+ import fs18 from "node:fs";
21794
+ import path25 from "node:path";
21353
21795
  var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (value) => {
21354
21796
  if (!isRecord4(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
21355
21797
  return { ok: false, error: "hook_event_name is required" };
@@ -21430,17 +21872,17 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
21430
21872
  };
21431
21873
  }
21432
21874
  }, TRANSCRIPT_TAIL_BYTES, lastAssistantText = (file) => {
21433
- if (!file || !path24.isAbsolute(file))
21875
+ if (!file || !path25.isAbsolute(file))
21434
21876
  return null;
21435
21877
  try {
21436
- const size = fs17.statSync(file).size;
21878
+ const size = fs18.statSync(file).size;
21437
21879
  const start = Math.max(0, size - TRANSCRIPT_TAIL_BYTES);
21438
21880
  const buffer = Buffer.alloc(size - start);
21439
- const fd = fs17.openSync(file, "r");
21881
+ const fd = fs18.openSync(file, "r");
21440
21882
  try {
21441
- fs17.readSync(fd, buffer, 0, buffer.length, start);
21883
+ fs18.readSync(fd, buffer, 0, buffer.length, start);
21442
21884
  } finally {
21443
- fs17.closeSync(fd);
21885
+ fs18.closeSync(fd);
21444
21886
  }
21445
21887
  const lines = buffer.toString("utf8").split(`
21446
21888
  `);
@@ -21466,7 +21908,7 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
21466
21908
  } catch {
21467
21909
  return null;
21468
21910
  }
21469
- }, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path24.sep) ? root : `${root}${path24.sep}`), hookCwd = (input) => {
21911
+ }, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path25.sep) ? root : `${root}${path25.sep}`), hookCwd = (input) => {
21470
21912
  const cwd = existingDirectory(input.cwd);
21471
21913
  if (cwd && input.hook_event_name === "beforeShellExecution")
21472
21914
  return cwd;
@@ -21692,6 +22134,7 @@ var init_hooks = __esm(async () => {
21692
22134
  init_context();
21693
22135
  init_policy();
21694
22136
  init_write_gate();
22137
+ init_live_session();
21695
22138
  init_stop();
21696
22139
  init_raw_git();
21697
22140
  init_skill_nudge();