@brainervirus/workit-opencode 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.
@@ -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
- (call the skill tool with `workit-ship`).
56
+ (call the skill tool with `workit-ship`) 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
  | --- | --- | --- |
package/dist/plugin.js CHANGED
@@ -7772,6 +7772,8 @@ var text = string2().check((ctx) => {
7772
7772
  ctx.issues.push({ code: "custom", input: ctx.value, message: "invalid Unicode", path: [] });
7773
7773
  });
7774
7774
  var id = text.regex(uuidPattern, "expected lowercase UUID");
7775
+ var idRefPattern = /^[0-9a-f][0-9a-f-]{7,35}$/;
7776
+ var idRef = text.regex(idRefPattern, "expected a UUID or an id prefix of 8+ characters");
7775
7777
  var digest = text.regex(digestPattern, "expected lowercase SHA-256 digest");
7776
7778
  var revision = text.regex(uuidPattern, "expected UUID revision");
7777
7779
  var validUtc = (value) => {
@@ -8149,7 +8151,7 @@ var workerReportSchema = object({
8149
8151
  outcome: _enum(["completed", "failed", "cancelled"]),
8150
8152
  summary: text,
8151
8153
  evidenceIds: array(id),
8152
- findingIds: array(id)
8154
+ findingIds: array(idRef)
8153
8155
  }).strict();
8154
8156
  var workerSchema = object({
8155
8157
  assignment: assignmentSchema,
@@ -8395,7 +8397,7 @@ var findingOperations = {
8395
8397
  action: literal("resolve"),
8396
8398
  ...taskId,
8397
8399
  expectedRevision: revision.optional(),
8398
- findingId: id,
8400
+ findingId: idRef,
8399
8401
  disposition: _enum(["open", "fixed", "dismissed", "deferred"]),
8400
8402
  reason: text,
8401
8403
  evidenceIds: array(id),
@@ -8436,7 +8438,7 @@ var workerOperations = {
8436
8438
  ...taskId,
8437
8439
  expectedRevision: revision.optional(),
8438
8440
  expectedWorkspaceRevision: revision.optional(),
8439
- workerId: id,
8441
+ workerId: idRef,
8440
8442
  report: workerReportSchema
8441
8443
  }),
8442
8444
  cancel: operation({
@@ -8444,7 +8446,7 @@ var workerOperations = {
8444
8446
  ...taskId,
8445
8447
  ...revisions,
8446
8448
  expectedWorkspaceRevision: revision.optional(),
8447
- workerId: id,
8449
+ workerId: idRef,
8448
8450
  reason: text
8449
8451
  })
8450
8452
  };
@@ -8494,6 +8496,16 @@ var success = (revisionValue, workspaceRevisionValue, data) => ({
8494
8496
  data
8495
8497
  });
8496
8498
  var failure2 = (code, error, details = {}) => ({ ok: false, schemaVersion: 1, code, error, details });
8499
+ function resolveIdRef(kind, ref, ids) {
8500
+ if (ids.includes(ref))
8501
+ return success(null, null, ref);
8502
+ const matches = ids.filter((candidate) => candidate.startsWith(ref));
8503
+ if (matches.length === 1)
8504
+ return success(null, null, matches[0]);
8505
+ if (matches.length === 0)
8506
+ return failure2("not_found", `${kind} not found`);
8507
+ return failure2("invalid_input", `${kind} prefix ${ref} is ambiguous; candidates: ${matches.join(", ")}`);
8508
+ }
8497
8509
  function parseOperation(family, input) {
8498
8510
  if (typeof input === "object" && input !== null && "schemaVersion" in input && input.schemaVersion !== SCHEMA_VERSION)
8499
8511
  return failure2("unsupported_version", "unsupported schema version", { operation: family });
@@ -11003,20 +11015,26 @@ class TaskStore {
11003
11015
  return success(null, null, { key: key.data, bound: bound.data });
11004
11016
  }
11005
11017
  taskForWorker(workerId) {
11006
- if (typeof workerId !== "string" || !validId(workerId))
11018
+ if (typeof workerId !== "string" || !idRefPattern.test(workerId))
11007
11019
  return failure2("invalid_input", "needs a worker id: `workit worker cancel|report <workerId>`");
11008
11020
  const listed = this.listTaskIndex();
11009
11021
  if (!listed.ok)
11010
11022
  return listed;
11023
+ const owners = new Map;
11011
11024
  for (const entry of listed.data) {
11012
11025
  if (entry.status === "closed")
11013
11026
  continue;
11014
11027
  const loaded = this.load(entry.id);
11015
11028
  if (!loaded.ok)
11016
11029
  return loaded;
11017
- if (loaded.data?.record.workers.some((worker) => worker.id === workerId))
11018
- return success(null, null, entry.id);
11030
+ for (const worker of loaded.data?.record.workers ?? [])
11031
+ owners.set(worker.id, entry.id);
11019
11032
  }
11033
+ const resolved = resolveIdRef("worker", workerId, [...owners.keys()]);
11034
+ if (resolved.ok)
11035
+ return success(null, null, owners.get(resolved.data) ?? null);
11036
+ if (resolved.code !== "not_found")
11037
+ return resolved;
11020
11038
  return success(null, null, null);
11021
11039
  }
11022
11040
  originTaskEntry() {
@@ -13161,6 +13179,8 @@ var workspaceConfigSchema = object({
13161
13179
  defaultEndpoint: unknown().optional(),
13162
13180
  verification: _enum(["self", "independent"]).optional(),
13163
13181
  trailers: boolean2().optional(),
13182
+ liveSessionGuard: boolean2().optional(),
13183
+ fanout: object({ tierModels: unknown().optional() }).passthrough().optional(),
13164
13184
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
13165
13185
  kind,
13166
13186
  union([boolean2(), literal("verified")]).optional()
@@ -13478,6 +13498,31 @@ var resolveRuntimeReleaseTracks = (cwd) => {
13478
13498
  const matched = matchRuntimeWorkspace(cwd);
13479
13499
  return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
13480
13500
  };
13501
+ var inheritedSetting = (cwd, dir, has) => {
13502
+ let matched;
13503
+ try {
13504
+ matched = matchRuntimeWorkspace(cwd, dir);
13505
+ } catch {
13506
+ return null;
13507
+ }
13508
+ if (!matched)
13509
+ return null;
13510
+ const { selected, matches } = matched;
13511
+ const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
13512
+ return { workspace: selected.name, source: [selected, ...enclosing].find(has) ?? null };
13513
+ };
13514
+ var workspaceLiveSessionGuard = (cwd, dir) => inheritedSetting(cwd, dir, (entry) => typeof entry.liveSessionGuard === "boolean")?.source?.liveSessionGuard !== false;
13515
+ var TIER_KEYS = new Set(["mundane", "standard", "hard"]);
13516
+ var TIER_HOSTS = new Set([
13517
+ "claude_code",
13518
+ "opencode",
13519
+ "codex",
13520
+ "codex_cli",
13521
+ "codex_desktop",
13522
+ "cursor",
13523
+ "pi",
13524
+ "default"
13525
+ ]);
13481
13526
  var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
13482
13527
  const matched = matchRuntimeWorkspace(cwd);
13483
13528
  if (!matched)
@@ -18939,6 +18984,8 @@ var TASK_SCOPED_ACTIONS = new Set([
18939
18984
  "worker.cancel",
18940
18985
  "state.export"
18941
18986
  ]);
18987
+ var ONLY_OWN_REPORT = "workers can submit only their own report";
18988
+ var NO_HELPER_CONTROL = "helpers cannot cancel or control workers";
18942
18989
  var WORKER_ADDRESSED_ACTIONS = new Set(["worker.cancel", "worker.report"]);
18943
18990
  var workerCommand = (taskId, request) => {
18944
18991
  const { action, workerId, reason, report } = request;
@@ -19836,6 +19883,10 @@ class WorkitCore {
19836
19883
  }
19837
19884
  if (helper?.ok)
19838
19885
  return failure2("permission_denied", "helpers cannot resolve findings");
19886
+ const findingRef = resolveIdRef("finding", input.findingId, task.data.findings.map((entry) => entry.id));
19887
+ if (!findingRef.ok)
19888
+ return findingRef;
19889
+ input.findingId = findingRef.data;
19839
19890
  const finding = task.data.findings.find((entry) => entry.id === input.findingId);
19840
19891
  if (!finding)
19841
19892
  return failure2("not_found", "finding not found");
@@ -19987,15 +20038,31 @@ class WorkitCore {
19987
20038
  return changed;
19988
20039
  return success(changed.data.task.revision, changed.data.workspace.revision, changed.data.task.workers.at(-1));
19989
20040
  }
20041
+ const workerRef = resolveIdRef("worker", input.workerId, helperId === null ? task.data.workers.map((candidate) => candidate.id) : [helperId]);
20042
+ if (!workerRef.ok)
20043
+ return helperId === null ? workerRef : failure2("permission_denied", input.action === "report" ? ONLY_OWN_REPORT : NO_HELPER_CONTROL);
20044
+ input.workerId = workerRef.data;
19990
20045
  const entry = task.data.workers.find((candidate) => candidate.id === input.workerId);
19991
20046
  if (!entry)
19992
20047
  return failure2("not_found", "worker not found");
19993
20048
  if (input.action === "report") {
19994
20049
  if (helperId === null || helperId !== input.workerId)
19995
- return failure2("permission_denied", "workers can submit only their own report");
20050
+ return failure2("permission_denied", ONLY_OWN_REPORT);
19996
20051
  const helper = this.helperEntry(task.data, true, true);
19997
20052
  if (!helper.ok)
19998
20053
  return helper;
20054
+ const ownFindings = task.data.findings.filter((candidate) => candidate.provenance.workerId === helperId).map((candidate) => candidate.id);
20055
+ const reported = [];
20056
+ for (const findingRef of input.report.findingIds) {
20057
+ const found = resolveIdRef("finding", findingRef, ownFindings);
20058
+ if (found.ok)
20059
+ reported.push(found.data);
20060
+ else if (found.code === "not_found")
20061
+ reported.push(findingRef);
20062
+ else
20063
+ return found;
20064
+ }
20065
+ input.report.findingIds = reported;
19999
20066
  const evidence = task.data.evidence.filter((candidate) => input.report.evidenceIds.includes(candidate.id));
20000
20067
  const findings = task.data.findings.filter((candidate) => input.report.findingIds.includes(candidate.id));
20001
20068
  if (evidence.length !== input.report.evidenceIds.length || findings.length !== input.report.findingIds.length)
@@ -20023,7 +20090,7 @@ class WorkitCore {
20023
20090
  return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during report");
20024
20091
  }
20025
20092
  if (helperId !== null)
20026
- return failure2("permission_denied", "helpers cannot cancel or control workers");
20093
+ return failure2("permission_denied", NO_HELPER_CONTROL);
20027
20094
  if (task.data.status === "closed")
20028
20095
  return failure2("invalid_transition", "closed task cannot cancel workers");
20029
20096
  if (entry.data.state === "stopped") {
@@ -21684,27 +21751,231 @@ var shellPolicy = (cwd, command, dialect = "posix") => {
21684
21751
  };
21685
21752
  // packages/workit-core/src/hooks/write-gate.ts
21686
21753
  import { realpathSync as realpathSync6 } from "node:fs";
21754
+ import os7 from "node:os";
21755
+ import path27 from "node:path";
21756
+
21757
+ // packages/workit-core/src/core/live-session.ts
21758
+ import { spawnSync as spawnSync17 } from "node:child_process";
21759
+ import { createHash as createHash14 } from "node:crypto";
21760
+ import fs18 from "node:fs";
21761
+ import os6 from "node:os";
21687
21762
  import path26 from "node:path";
21763
+ var LIVE_SESSION_TTL_MS = 15 * 60000;
21764
+ var HEARTBEAT_EVERY_MS = 30000;
21765
+ var liveProbe = {
21766
+ platform: process.platform,
21767
+ run: (args) => spawnSync17("ps", args, { encoding: "utf8", timeout: 2000 }).stdout ?? "",
21768
+ start: (pid) => processStartOf(pid)
21769
+ };
21770
+ var leadOf = (session) => session.split(":")[0].replace(/-w-.*$/su, "") || session;
21771
+ var familyOf = (host) => host.replace(/_(?:cli|desktop)$/u, "");
21772
+ var processAlive = (pid) => {
21773
+ try {
21774
+ process.kill(pid, 0);
21775
+ return true;
21776
+ } catch (error) {
21777
+ return error.code === "EPERM";
21778
+ }
21779
+ };
21780
+ var processInfo = (pid) => {
21781
+ try {
21782
+ if (liveProbe.platform === "linux") {
21783
+ const stat = fs18.readFileSync(`/proc/${pid}/stat`, "utf8");
21784
+ const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
21785
+ const command = fs18.readFileSync(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ");
21786
+ return Number.isInteger(ppid) ? { ppid, command: command.trim() } : null;
21787
+ }
21788
+ if (liveProbe.platform === "win32")
21789
+ return null;
21790
+ const match = /^\s*(\d+)\s+(.*)$/su.exec(liveProbe.run(["-o", "ppid=,command=", "-p", String(pid)]));
21791
+ return match ? { ppid: Number(match[1]), command: match[2].trim() } : null;
21792
+ } catch {
21793
+ return null;
21794
+ }
21795
+ };
21796
+ 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;
21797
+ var PIDS = new Map;
21798
+ var hostPid = (transport) => {
21799
+ if (transport !== "hook-process")
21800
+ return process.pid;
21801
+ const key = `${liveProbe.platform}\x00${process.ppid}`;
21802
+ if (PIDS.has(key))
21803
+ return PIDS.get(key) ?? null;
21804
+ if (liveProbe.platform === "win32") {
21805
+ PIDS.set(key, null);
21806
+ return null;
21807
+ }
21808
+ let pid = process.ppid;
21809
+ for (let hop = 0;hop < 4; hop++) {
21810
+ const info = processInfo(pid);
21811
+ if (!info || !WRAPPER.test(info.command) || info.ppid <= 1)
21812
+ break;
21813
+ pid = info.ppid;
21814
+ }
21815
+ PIDS.set(key, pid);
21816
+ return pid;
21817
+ };
21818
+ var sessionSafe = (value) => value.replace(/[^A-Za-z0-9_.:@/+-]/gu, "-");
21819
+ var liveOwner = (input, transport, env = process.env) => {
21820
+ const session = input.session.id?.trim();
21821
+ if (!session)
21822
+ return null;
21823
+ const leads = new Set([leadOf(sessionSafe(session))]);
21824
+ if (input.session.parentId)
21825
+ leads.add(leadOf(sessionSafe(input.session.parentId)));
21826
+ if (env.WORKIT_PI_WORKER_SESSION && env.WORKIT_SESSION_ID)
21827
+ leads.add(leadOf(sessionSafe(env.WORKIT_SESSION_ID)));
21828
+ return {
21829
+ host: input.host,
21830
+ session: sessionSafe(session),
21831
+ leads: [...leads],
21832
+ pid: () => hostPid(transport)
21833
+ };
21834
+ };
21835
+ var checkoutOf = (target) => {
21836
+ let dir = path26.resolve(target);
21837
+ while (!fs18.existsSync(dir) && path26.dirname(dir) !== dir)
21838
+ dir = path26.dirname(dir);
21839
+ if (fs18.existsSync(dir) && !fs18.statSync(dir).isDirectory())
21840
+ dir = path26.dirname(dir);
21841
+ const parts = dir.split(path26.sep);
21842
+ const dotGit = parts.lastIndexOf(".git");
21843
+ if (dotGit > 0)
21844
+ dir = parts.slice(0, dotGit).join(path26.sep) || path26.sep;
21845
+ const location = resolveStore(dir);
21846
+ if (location instanceof Error || !location.shared || !location.top)
21847
+ return null;
21848
+ let top = location.top;
21849
+ try {
21850
+ top = fs18.realpathSync.native(top);
21851
+ } catch {}
21852
+ return top;
21853
+ };
21854
+ var markerFile2 = (top) => path26.join(resolveStateDir(), "live-sessions", `${createHash14("sha256").update(checkoutKey(top)).digest("hex").slice(0, 32)}.json`);
21855
+ var checkoutKey = (top, platform = process.platform) => {
21856
+ let key = path26.resolve(top);
21857
+ try {
21858
+ key = fs18.realpathSync.native(key);
21859
+ } catch {}
21860
+ return platform === "win32" ? key.replaceAll("/", "\\").toLowerCase() : key;
21861
+ };
21862
+ var readMarker = (file) => {
21863
+ try {
21864
+ const value = JSON.parse(fs18.readFileSync(file, "utf8"));
21865
+ if (value?.version !== 1 || typeof value.session !== "string" || typeof value.host !== "string" || typeof value.heartbeatAt !== "string" || Number.isNaN(Date.parse(value.heartbeatAt)))
21866
+ return null;
21867
+ return {
21868
+ version: 1,
21869
+ session: value.session,
21870
+ leads: Array.isArray(value.leads) ? value.leads.filter((item) => typeof item === "string") : [],
21871
+ host: value.host,
21872
+ pid: typeof value.pid === "number" ? value.pid : null,
21873
+ pidStart: typeof value.pidStart === "string" ? value.pidStart : null,
21874
+ machine: typeof value.machine === "string" ? value.machine : "",
21875
+ checkout: typeof value.checkout === "string" ? value.checkout : "",
21876
+ heartbeatAt: value.heartbeatAt
21877
+ };
21878
+ } catch {
21879
+ return null;
21880
+ }
21881
+ };
21882
+ var ageOf2 = (marker, now) => now.getTime() - Date.parse(marker.heartbeatAt);
21883
+ var isLive = (marker, deps) => {
21884
+ const age = ageOf2(marker, deps.now ?? new Date);
21885
+ if (age > (deps.ttlMs ?? LIVE_SESSION_TTL_MS))
21886
+ return false;
21887
+ if (marker.machine !== (deps.machine ?? os6.hostname()) || marker.pid === null)
21888
+ return true;
21889
+ if (!(deps.alive ?? processAlive)(marker.pid))
21890
+ return false;
21891
+ if (marker.pidStart === null)
21892
+ return true;
21893
+ const start = liveProbe.start(marker.pid);
21894
+ return start === null || start === marker.pidStart;
21895
+ };
21896
+ var sameLead = (marker, owner) => marker.session === owner.session || owner.leads.some((lead) => lead === marker.session || marker.leads.includes(lead));
21897
+ var sameOwner = (marker, owner, machine) => {
21898
+ if (sameLead(marker, owner))
21899
+ return true;
21900
+ if (marker.machine !== machine)
21901
+ return false;
21902
+ const pid = owner.pid();
21903
+ if (pid === null || marker.pid === null || marker.pidStart === null)
21904
+ return familyOf(marker.host) === familyOf(owner.host);
21905
+ return marker.pid === pid;
21906
+ };
21907
+ var liveConflict = (target, owner, deps = {}) => {
21908
+ try {
21909
+ const checkout = checkoutOf(target);
21910
+ if (!checkout)
21911
+ return null;
21912
+ const file = markerFile2(checkout);
21913
+ const marker = readMarker(file);
21914
+ if (!marker || sameLead(marker, owner) || !isLive(marker, deps))
21915
+ return null;
21916
+ if (sameOwner(marker, owner, deps.machine ?? os6.hostname()))
21917
+ return null;
21918
+ return { marker, file, ageMs: ageOf2(marker, deps.now ?? new Date), checkout };
21919
+ } catch {
21920
+ return null;
21921
+ }
21922
+ };
21923
+ var claimLiveSession = (target, owner, deps = {}) => {
21924
+ try {
21925
+ const checkout = checkoutOf(target);
21926
+ if (!checkout)
21927
+ return;
21928
+ const file = markerFile2(checkout);
21929
+ const now = deps.now ?? new Date;
21930
+ const machine = deps.machine ?? os6.hostname();
21931
+ const current = readMarker(file);
21932
+ if (current) {
21933
+ const age = ageOf2(current, now);
21934
+ if (sameLead(current, owner) && age >= 0 && age < HEARTBEAT_EVERY_MS)
21935
+ return;
21936
+ if (!sameLead(current, owner) && isLive(current, deps) && !sameOwner(current, owner, machine))
21937
+ return;
21938
+ }
21939
+ const pid = owner.pid();
21940
+ const marker = {
21941
+ version: 1,
21942
+ session: owner.session,
21943
+ leads: owner.leads,
21944
+ host: owner.host,
21945
+ pid,
21946
+ pidStart: pid === null ? null : liveProbe.start(pid),
21947
+ machine,
21948
+ checkout,
21949
+ heartbeatAt: now.toISOString()
21950
+ };
21951
+ fs18.mkdirSync(path26.dirname(file), { recursive: true });
21952
+ const temp = `${file}.${process.pid}.tmp`;
21953
+ fs18.writeFileSync(temp, JSON.stringify(marker));
21954
+ fs18.renameSync(temp, file);
21955
+ } catch {}
21956
+ };
21957
+
21958
+ // packages/workit-core/src/hooks/write-gate.ts
21688
21959
  var NONE3 = { kind: "none" };
21689
21960
  var CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
21690
21961
  var canonical2 = (file) => {
21691
21962
  let rest = "";
21692
- for (let dir = path26.resolve(file);; dir = path26.dirname(dir)) {
21963
+ for (let dir = path27.resolve(file);; dir = path27.dirname(dir)) {
21693
21964
  try {
21694
- return path26.join(realpathSync6(dir), rest);
21965
+ return path27.join(realpathSync6(dir), rest);
21695
21966
  } catch {
21696
- if (path26.dirname(dir) === dir)
21697
- return path26.resolve(file);
21698
- rest = rest ? path26.join(path26.basename(dir), rest) : path26.basename(dir);
21967
+ if (path27.dirname(dir) === dir)
21968
+ return path27.resolve(file);
21969
+ rest = rest ? path27.join(path27.basename(dir), rest) : path27.basename(dir);
21699
21970
  }
21700
21971
  }
21701
21972
  };
21702
21973
  var exempt = (cwd, root, file, plans) => {
21703
21974
  const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
21704
- const relative = path26.relative(fold(canonical2(root)), fold(canonical2(path26.resolve(cwd, file))));
21705
- if (relative.startsWith("..") || path26.isAbsolute(relative))
21975
+ const relative = path27.relative(fold(canonical2(root)), fold(canonical2(path27.resolve(cwd, file))));
21976
+ if (relative.startsWith("..") || path27.isAbsolute(relative))
21706
21977
  return true;
21707
- const posix = relative.split(path26.sep).join("/");
21978
+ const posix = relative.split(path27.sep).join("/");
21708
21979
  const segments = posix.split("/");
21709
21980
  return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
21710
21981
  };
@@ -21802,7 +22073,7 @@ var shellWrites = (command) => {
21802
22073
  return { writes: true, targets: unknown ? null : targets };
21803
22074
  };
21804
22075
  var cache2 = new Map;
21805
- var planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path26.resolve(root, file)) ?? "-").join("\x00");
22076
+ var planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path27.resolve(root, file)) ?? "-").join("\x00");
21806
22077
  var gateFor = (cwd) => {
21807
22078
  const store = new TaskStore(cwd);
21808
22079
  const listed = store.listTaskIndex();
@@ -21840,7 +22111,218 @@ var gateFor = (cwd) => {
21840
22111
  cache2.delete(cache2.keys().next().value);
21841
22112
  return gate;
21842
22113
  };
21843
- function writeGate(cwd, paths) {
22114
+ var MUTATING_GIT = new Set([
22115
+ "add",
22116
+ "am",
22117
+ "apply",
22118
+ "checkout",
22119
+ "cherry-pick",
22120
+ "clean",
22121
+ "commit",
22122
+ "merge",
22123
+ "mv",
22124
+ "pull",
22125
+ "rebase",
22126
+ "reset",
22127
+ "restore",
22128
+ "revert",
22129
+ "rm",
22130
+ "stash",
22131
+ "switch"
22132
+ ]);
22133
+ var DRY_N = new Set(["add", "clean", "rm", "mv"]);
22134
+ var MUTATING_WORKIT_GIT = new Set(["commit", "branch"]);
22135
+ var DIR_TARGET = ".workit-live-target";
22136
+ var OVERRIDE = "WORKIT_LIVE_SESSION=override";
22137
+ 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("--"));
22138
+ var gitMutationDirs = (cwd, command, dialect) => {
22139
+ let invocations;
22140
+ try {
22141
+ invocations = rawInvocations(command, dialect);
22142
+ } catch {
22143
+ return [];
22144
+ }
22145
+ return invocations.flatMap((invocation) => {
22146
+ if (invocation.tool !== "git" || invocation.dir === undefined)
22147
+ return [];
22148
+ const at = invocation.args.findIndex((arg) => !arg.startsWith("-"));
22149
+ const sub = invocation.args[at] ?? "";
22150
+ const rest = invocation.args.slice(at + 1);
22151
+ if (!MUTATING_GIT.has(sub) || inert(rest, sub))
22152
+ return [];
22153
+ if (sub === "stash" && ["list", "show"].includes(rest[0] ?? ""))
22154
+ return [];
22155
+ return [path27.resolve(cwd, invocation.dir ?? ".")];
22156
+ });
22157
+ };
22158
+ var CD2 = new Set(["cd", "chdir", "set-location", "sl"]);
22159
+ var PUSHD2 = new Set(["pushd", "push-location"]);
22160
+ var POPD2 = new Set(["popd", "pop-location"]);
22161
+ var cdInto = (dir, words) => {
22162
+ const target = words.slice(1).find((word) => !word.startsWith("-") || word === "-");
22163
+ if (dir === undefined || target === "-" || target && /^~|[$`*?]/.test(target))
22164
+ return;
22165
+ return target === undefined ? os7.homedir() : path27.resolve(dir, target);
22166
+ };
22167
+ var quoteWord = (word) => /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
22168
+ var segmentTargets = (cwd, command, dialect) => {
22169
+ const targets = [];
22170
+ let dir = cwd;
22171
+ let stack = [];
22172
+ const groups = [];
22173
+ const at = (file) => path27.isAbsolute(file) ? file : dir === undefined ? null : path27.resolve(dir, file);
22174
+ for (const segment of segmentsOf(command, dialect)) {
22175
+ if (segment.group === "open") {
22176
+ groups.push({ dir, stack: [...stack] });
22177
+ continue;
22178
+ }
22179
+ if (segment.group === "close") {
22180
+ const saved = groups.pop();
22181
+ if (saved)
22182
+ ({ dir, stack } = saved);
22183
+ continue;
22184
+ }
22185
+ const lead = segment.words.findIndex((word) => !/^\w+=/.test(word));
22186
+ const words = lead < 0 ? [] : segment.words.slice(lead);
22187
+ const name = (words[0]?.split(/[\\/]/).at(-1) ?? "").toLowerCase();
22188
+ if (CD2.has(name)) {
22189
+ dir = cdInto(dir, words);
22190
+ continue;
22191
+ }
22192
+ if (PUSHD2.has(name)) {
22193
+ stack.push(dir);
22194
+ dir = cdInto(dir, words);
22195
+ continue;
22196
+ }
22197
+ if (POPD2.has(name)) {
22198
+ dir = stack.length ? stack.pop() : undefined;
22199
+ continue;
22200
+ }
22201
+ for (const file of segment.redirects)
22202
+ if (!/^\/dev\/(?:null|stdout|stderr|tty)$/.test(file)) {
22203
+ const resolved = at(file);
22204
+ if (resolved)
22205
+ targets.push(resolved);
22206
+ }
22207
+ const edits = editsOf(segment.words);
22208
+ if (edits === null) {
22209
+ if (dir !== undefined)
22210
+ targets.push(path27.join(dir, DIR_TARGET));
22211
+ } else
22212
+ for (const file of edits) {
22213
+ const resolved = at(file);
22214
+ if (resolved)
22215
+ targets.push(resolved);
22216
+ }
22217
+ 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), "")))
22218
+ targets.push(path27.join(dir, DIR_TARGET));
22219
+ }
22220
+ return targets;
22221
+ };
22222
+ var overridden = (command, dialect) => {
22223
+ const segments = segmentsOf(command, dialect);
22224
+ if (segments.length !== 1 || segments[0].group || segments[0].subs?.length)
22225
+ return false;
22226
+ const words = segments[0].words;
22227
+ const lead = words.findIndex((word) => !/^\w+=/.test(word));
22228
+ return lead > 0 && words.slice(0, lead).includes(OVERRIDE);
22229
+ };
22230
+ var HOST_LABELS = {
22231
+ claude_code: "Claude Code",
22232
+ opencode: "OpenCode",
22233
+ codex_cli: "Codex CLI",
22234
+ codex_desktop: "Codex Desktop",
22235
+ cursor: "Cursor",
22236
+ pi: "Pi"
22237
+ };
22238
+ var ageText = (ms) => {
22239
+ const seconds = Math.max(0, Math.round(ms / 1000));
22240
+ return seconds < 120 ? `${seconds}s` : `${Math.round(seconds / 60)} min`;
22241
+ };
22242
+ var liveDeny = ({ marker, checkout, ageMs, file }) => {
22243
+ 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}.`;
22244
+ return {
22245
+ kind: "deny",
22246
+ 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}`,
22247
+ unblock
22248
+ };
22249
+ };
22250
+ var guarded = (targets) => {
22251
+ const seen = new Set;
22252
+ return targets.filter((target) => {
22253
+ const dir = path27.dirname(target);
22254
+ if (seen.has(dir))
22255
+ return false;
22256
+ seen.add(dir);
22257
+ return true;
22258
+ });
22259
+ };
22260
+ var liveGate = (owner, targets, deps) => {
22261
+ try {
22262
+ for (const target of guarded(targets)) {
22263
+ const conflict = liveConflict(target, owner, deps);
22264
+ if (conflict && workspaceLiveSessionGuard(conflict.checkout))
22265
+ return liveDeny(conflict);
22266
+ }
22267
+ } catch {}
22268
+ return NONE3;
22269
+ };
22270
+ var claimAll = (owner, targets, deps) => {
22271
+ for (const target of guarded(targets))
22272
+ claimLiveSession(target, owner, deps);
22273
+ };
22274
+ var liveTargets = (cwd, paths) => paths?.length ? paths.map((file) => path27.resolve(cwd, file)) : [path27.join(cwd, DIR_TARGET)];
22275
+ function writeGate(cwd, paths, owner, deps) {
22276
+ if (!owner)
22277
+ return beforeWriteGate(cwd, paths);
22278
+ const targets = liveTargets(cwd, paths);
22279
+ const live = liveGate(owner, targets, deps);
22280
+ if (live.kind !== "none")
22281
+ return live;
22282
+ const decision = beforeWriteGate(cwd, paths);
22283
+ if (decision.kind === "none")
22284
+ claimAll(owner, targets, deps);
22285
+ return decision;
22286
+ }
22287
+ function shellGate(cwd, command, owner, dialect = "posix", deps) {
22288
+ const writes = shellWrites(command);
22289
+ let targets = [];
22290
+ if (owner) {
22291
+ try {
22292
+ targets = [
22293
+ ...segmentTargets(cwd, command, dialect),
22294
+ ...gitMutationDirs(cwd, command, dialect).map((dir) => path27.join(dir, DIR_TARGET))
22295
+ ];
22296
+ } catch {
22297
+ targets = [];
22298
+ }
22299
+ }
22300
+ if (owner && targets.length) {
22301
+ const live = liveGate(owner, targets, deps);
22302
+ if (live.kind !== "none") {
22303
+ if (!overridden(command, dialect))
22304
+ return live;
22305
+ noteOverride(cwd, owner, command);
22306
+ }
22307
+ }
22308
+ const decision = writes.writes ? beforeWriteGate(cwd, writes.targets) : NONE3;
22309
+ if (owner && targets.length && decision.kind === "none")
22310
+ claimAll(owner, targets, deps);
22311
+ return decision;
22312
+ }
22313
+ var noteOverride = (cwd, owner, command) => {
22314
+ try {
22315
+ if (!inWorkitWorkspace(cwd))
22316
+ return;
22317
+ appendHookObserved(cwd, {
22318
+ type: "live_session.override",
22319
+ actor: { host: owner.host, session: owner.session, agentId: null },
22320
+ session: owner.session,
22321
+ command: command.slice(0, 200)
22322
+ });
22323
+ } catch {}
22324
+ };
22325
+ function beforeWriteGate(cwd, paths) {
21844
22326
  try {
21845
22327
  const gate = gateFor(cwd);
21846
22328
  if (!gate || gate.blockers.length === 0)
@@ -21858,7 +22340,7 @@ function writeGate(cwd, paths) {
21858
22340
  }
21859
22341
  }
21860
22342
  // packages/workit-core/src/hooks/stop.ts
21861
- import { spawnSync as spawnSync17 } from "node:child_process";
22343
+ import { spawnSync as spawnSync18 } from "node:child_process";
21862
22344
  var NONE4 = { kind: "none" };
21863
22345
  var QUESTION = /[??؟](?=[\s*_`"')\]>]*(?:\s|$))|¿/;
21864
22346
  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;
@@ -21879,7 +22361,7 @@ var asksUser = (message) => {
21879
22361
  };
21880
22362
  var short = (sha) => (sha ?? "?").slice(0, 12);
21881
22363
  var gitList = (cwd, args) => {
21882
- const run = spawnSync17("git", args, {
22364
+ const run = spawnSync18("git", args, {
21883
22365
  cwd,
21884
22366
  encoding: "utf8",
21885
22367
  timeout: 3000,
@@ -22341,11 +22823,11 @@ var OPENCODE_DESCRIPTOR = {
22341
22823
  ]
22342
22824
  };
22343
22825
  // packages/workit-opencode/src/shared/init-apply.ts
22344
- import path31 from "node:path";
22826
+ import path32 from "node:path";
22345
22827
 
22346
22828
  // packages/workit-core/src/core/gitignore.ts
22347
22829
  import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
22348
- import path27 from "node:path";
22830
+ import path28 from "node:path";
22349
22831
  var GITIGNORE_ENTRIES = [
22350
22832
  "# workit: SDD working state (never commit)",
22351
22833
  "docs/*/sdd/",
@@ -22365,7 +22847,7 @@ var GITIGNORE_ENTRIES = [
22365
22847
  var ensureProjectGitignore = (workspaceRoot, confirmed) => {
22366
22848
  if (!confirmed)
22367
22849
  return { ok: false, error: "confirmed: true required" };
22368
- const file = path27.join(workspaceRoot, ".gitignore");
22850
+ const file = path28.join(workspaceRoot, ".gitignore");
22369
22851
  const existing = existsSync9(file) ? readFileSync6(file, "utf8") : "";
22370
22852
  const existingLines = new Set(existing.split(`
22371
22853
  `).map((l) => l.trim()).filter(Boolean));
@@ -22393,25 +22875,25 @@ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
22393
22875
 
22394
22876
  // packages/workit-core/src/core/hygiene.ts
22395
22877
  import { existsSync as existsSync10, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
22396
- import path28 from "node:path";
22878
+ import path29 from "node:path";
22397
22879
  var repoRoot3 = assetRoot();
22398
- var templatesDir = () => path28.join(repoRoot3, "templates", "hygiene");
22880
+ var templatesDir = () => path29.join(repoRoot3, "templates", "hygiene");
22399
22881
  var packageJson = (root) => {
22400
- if (!existsSync10(path28.join(root, "package.json")))
22882
+ if (!existsSync10(path29.join(root, "package.json")))
22401
22883
  return null;
22402
22884
  try {
22403
- return JSON.parse(readFileSync7(path28.join(root, "package.json"), "utf8"));
22885
+ return JSON.parse(readFileSync7(path29.join(root, "package.json"), "utf8"));
22404
22886
  } catch {
22405
22887
  return null;
22406
22888
  }
22407
22889
  };
22408
22890
  var isOpenSource = (root) => {
22409
- if (existsSync10(path28.join(root, "LICENSE")))
22891
+ if (existsSync10(path29.join(root, "LICENSE")))
22410
22892
  return true;
22411
22893
  const pkg = packageJson(root);
22412
22894
  if (pkg && !pkg.private)
22413
22895
  return true;
22414
- return path28.basename(root) === "workflow-toolkit";
22896
+ return path29.basename(root) === "workflow-toolkit";
22415
22897
  };
22416
22898
  var licenseHolder = (root) => {
22417
22899
  const pkg = packageJson(root);
@@ -22434,13 +22916,13 @@ var planHygieneFiles = (root, opts = {}) => {
22434
22916
  files.push("LICENSE", "CONTRIBUTING.md");
22435
22917
  const planned = [];
22436
22918
  for (const file of files) {
22437
- if (existsSync10(path28.join(root, file)))
22919
+ if (existsSync10(path29.join(root, file)))
22438
22920
  continue;
22439
- const tpl = path28.join(templatesDir(), file);
22921
+ const tpl = path29.join(templatesDir(), file);
22440
22922
  if (!existsSync10(tpl))
22441
22923
  continue;
22442
- const content = readFileSync7(tpl, "utf8").replace(/<PROJECT>/g, path28.basename(root)).replace(/<YEAR>/g, String(new Date().getFullYear())).replace(/<HOLDER>\s*/g, licenseHolder(root));
22443
- planned.push({ path: path28.join(root, file), content });
22924
+ const content = readFileSync7(tpl, "utf8").replace(/<PROJECT>/g, path29.basename(root)).replace(/<YEAR>/g, String(new Date().getFullYear())).replace(/<HOLDER>\s*/g, licenseHolder(root));
22925
+ planned.push({ path: path29.join(root, file), content });
22444
22926
  }
22445
22927
  return planned;
22446
22928
  };
@@ -22450,19 +22932,19 @@ var ensureHygieneFiles = (root, opts) => {
22450
22932
  const created = [];
22451
22933
  for (const planned of planHygieneFiles(root, opts)) {
22452
22934
  writeFileSync5(planned.path, planned.content, "utf8");
22453
- created.push(path28.basename(planned.path));
22935
+ created.push(path29.basename(planned.path));
22454
22936
  }
22455
22937
  return { ok: true, created };
22456
22938
  };
22457
22939
 
22458
22940
  // packages/workit-core/src/core/init.ts
22459
- import fs18 from "node:fs";
22460
- import path30 from "node:path";
22461
- import { spawnSync as spawnSync18 } from "node:child_process";
22941
+ import fs19 from "node:fs";
22942
+ import path31 from "node:path";
22943
+ import { spawnSync as spawnSync19 } from "node:child_process";
22462
22944
 
22463
22945
  // packages/workit-core/src/core/workspace-branch-policy.ts
22464
22946
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
22465
- import path29 from "node:path";
22947
+ import path30 from "node:path";
22466
22948
  import { isDeepStrictEqual } from "node:util";
22467
22949
  var readFileSafe = (p) => {
22468
22950
  try {
@@ -22473,7 +22955,7 @@ var readFileSafe = (p) => {
22473
22955
  };
22474
22956
  function applyWorkspaceBranchPolicy(opts) {
22475
22957
  const { workspace_root, env = process.env } = opts;
22476
- const dir = path29.join(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
22958
+ const dir = path30.join(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
22477
22959
  const {
22478
22960
  status,
22479
22961
  path: wsPath,
@@ -22485,7 +22967,7 @@ function applyWorkspaceBranchPolicy(opts) {
22485
22967
  if (status === "malformed" || status === "invalid")
22486
22968
  return { ok: false, error: workspacesError ?? `invalid workspaces.json: ${wsPath}` };
22487
22969
  const detection = detectBranchPolicy(workspace_root);
22488
- const name = String(env.WORKFLOW_BP_NAME ?? path29.basename(workspace_root));
22970
+ const name = String(env.WORKFLOW_BP_NAME ?? path30.basename(workspace_root));
22489
22971
  const integration = env.WORKFLOW_BP_INTEGRATION ?? detection.integration;
22490
22972
  const policy = {
22491
22973
  preset: detection.preset,
@@ -22534,7 +23016,7 @@ function applyWorkspaceBranchPolicy(opts) {
22534
23016
  ok: false,
22535
23017
  error: `workspaces.json changed while applying the branch policy: ${wsPath}`
22536
23018
  };
22537
- mkdirSync4(path29.dirname(wsPath), { recursive: true });
23019
+ mkdirSync4(path30.dirname(wsPath), { recursive: true });
22538
23020
  const tmp = `${wsPath}.${process.pid}.tmp`;
22539
23021
  try {
22540
23022
  writeFileSync6(tmp, JSON.stringify({ ...document, workspaces: next }, null, 2) + `
@@ -22569,7 +23051,7 @@ function applyWorkspaceBranchPolicy(opts) {
22569
23051
  var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE";
22570
23052
  var readJson = (p) => {
22571
23053
  try {
22572
- const parsed = JSON.parse(fs18.readFileSync(p, "utf8"));
23054
+ const parsed = JSON.parse(fs19.readFileSync(p, "utf8"));
22573
23055
  return isConfigObject(parsed) ? parsed : null;
22574
23056
  } catch {
22575
23057
  return null;
@@ -22581,7 +23063,7 @@ var resolveInitProvider = (cwd) => {
22581
23063
  return env.toLowerCase();
22582
23064
  const root = cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
22583
23065
  try {
22584
- const out = spawnSync18("git", ["remote", "get-url", "origin"], {
23066
+ const out = spawnSync19("git", ["remote", "get-url", "origin"], {
22585
23067
  cwd: root,
22586
23068
  encoding: "utf8"
22587
23069
  });
@@ -22599,7 +23081,7 @@ var youtrackJsonContent = (dir) => {
22599
23081
  const meetingIssue = process.env.WORKFLOW_YT_MEETING_ISSUE;
22600
23082
  return {
22601
23083
  baseUrl: process.env.WORKFLOW_YT_BASE_URL ?? "https://youtrack.example.com",
22602
- tokenFile: process.env.WORKFLOW_YT_TOKEN_FILE ?? path30.join(dir, "youtrack.token"),
23084
+ tokenFile: process.env.WORKFLOW_YT_TOKEN_FILE ?? path31.join(dir, "youtrack.token"),
22603
23085
  ...meetingIssue ? {
22604
23086
  meetingIssue,
22605
23087
  meetingIssues: { general: { issue: meetingIssue, label: "General meetings" } }
@@ -22629,19 +23111,19 @@ var vcsJsonContent = () => {
22629
23111
  };
22630
23112
  function initApplyData(action, env = process.env) {
22631
23113
  const dir = String(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
22632
- fs18.mkdirSync(dir, { recursive: true });
23114
+ fs19.mkdirSync(dir, { recursive: true });
22633
23115
  switch (action) {
22634
23116
  case "youtrack_json": {
22635
- const out = path30.join(dir, "youtrack.json");
22636
- fs18.writeFileSync(out, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
23117
+ const out = path31.join(dir, "youtrack.json");
23118
+ fs19.writeFileSync(out, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
22637
23119
  `, "utf8");
22638
23120
  return { action, ok: true, path: out };
22639
23121
  }
22640
23122
  case "youtrack_token_placeholder": {
22641
- const p = path30.join(dir, "youtrack.token");
23123
+ const p = path31.join(dir, "youtrack.token");
22642
23124
  const preserved = writeFileExclusive(p, TOKEN_PLACEHOLDER2 + `
22643
23125
  `, 384) === "preserved";
22644
- const abs = path30.resolve(p);
23126
+ const abs = path31.resolve(p);
22645
23127
  return {
22646
23128
  action,
22647
23129
  ok: true,
@@ -22653,14 +23135,14 @@ function initApplyData(action, env = process.env) {
22653
23135
  };
22654
23136
  }
22655
23137
  case "youtrack_scaffold": {
22656
- const jsonOut = path30.join(dir, "youtrack.json");
22657
- const tokenOut = path30.join(dir, "youtrack.token");
22658
- fs18.writeFileSync(jsonOut, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
23138
+ const jsonOut = path31.join(dir, "youtrack.json");
23139
+ const tokenOut = path31.join(dir, "youtrack.token");
23140
+ fs19.writeFileSync(jsonOut, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
22659
23141
  `, "utf8");
22660
23142
  const preserved = writeFileExclusive(tokenOut, TOKEN_PLACEHOLDER2 + `
22661
23143
  `, 384) === "preserved";
22662
- const configPath = path30.resolve(jsonOut);
22663
- const tokenPath = path30.resolve(tokenOut);
23144
+ const configPath = path31.resolve(jsonOut);
23145
+ const tokenPath = path31.resolve(tokenOut);
22664
23146
  const prev = process.env.WORKFLOW_YOUTRACK_CONFIG;
22665
23147
  process.env.WORKFLOW_YOUTRACK_CONFIG = configPath;
22666
23148
  let tokenCreate = {};
@@ -22712,10 +23194,10 @@ function initApplyData(action, env = process.env) {
22712
23194
  }
22713
23195
  }
22714
23196
  case "vcs_scaffold": {
22715
- const jsonOut = path30.join(dir, "vcs.json");
22716
- fs18.writeFileSync(jsonOut, JSON.stringify(vcsJsonContent(), null, 2) + `
23197
+ const jsonOut = path31.join(dir, "vcs.json");
23198
+ fs19.writeFileSync(jsonOut, JSON.stringify(vcsJsonContent(), null, 2) + `
22717
23199
  `, "utf8");
22718
- const configPath = path30.resolve(jsonOut);
23200
+ const configPath = path31.resolve(jsonOut);
22719
23201
  const prev = process.env.WORKFLOW_VCS_CONFIG;
22720
23202
  process.env.WORKFLOW_VCS_CONFIG = configPath;
22721
23203
  try {
@@ -22834,7 +23316,7 @@ var executeInitApply = (args, directory, runtime) => {
22834
23316
  protectedNames: args.branch_policy_protected
22835
23317
  }, current);
22836
23318
  writeConfig(next);
22837
- return output(ok({ action: "config", path: path31.join(configDir(), "config.json"), ...next }));
23319
+ return output(ok({ action: "config", path: path32.join(configDir(), "config.json"), ...next }));
22838
23320
  }
22839
23321
  const env = Object.fromEntries(Object.entries({
22840
23322
  WORKFLOW_YT_BASE_URL: args.base_url,
@@ -22851,14 +23333,14 @@ var executeInitApply = (args, directory, runtime) => {
22851
23333
 
22852
23334
  // packages/workit-opencode/src/shared/session.ts
22853
23335
  import { realpathSync as realpathSync7 } from "node:fs";
22854
- import path32 from "node:path";
23336
+ import path33 from "node:path";
22855
23337
  var sameWorkspace = (expected, observed) => {
22856
23338
  if (typeof observed !== "string" || !observed)
22857
23339
  return false;
22858
23340
  try {
22859
23341
  return realpathSync7(expected) === realpathSync7(observed);
22860
23342
  } catch {
22861
- return path32.resolve(expected) === path32.resolve(observed);
23343
+ return path33.resolve(expected) === path33.resolve(observed);
22862
23344
  }
22863
23345
  };
22864
23346
 
@@ -22930,7 +23412,7 @@ var workitFamilyOf = (toolName) => {
22930
23412
 
22931
23413
  // packages/workit-opencode/src/tools/context.ts
22932
23414
  import { realpathSync as realpathSync8 } from "node:fs";
22933
- import path33 from "node:path";
23415
+ import path34 from "node:path";
22934
23416
  var createContextTool = () => ({
22935
23417
  description: "Read Git, pull request, YouTrack, issue, changelog, release, or affected-file context.",
22936
23418
  execute: async (args, context) => {
@@ -22940,7 +23422,7 @@ var createContextTool = () => ({
22940
23422
  let root;
22941
23423
  try {
22942
23424
  const cwd = parsed.data.cwd;
22943
- root = realpathSync8(cwd ? path33.resolve(context.directory, cwd) : context.directory);
23425
+ root = realpathSync8(cwd ? path34.resolve(context.directory, cwd) : context.directory);
22944
23426
  } catch {
22945
23427
  return JSON.stringify(failure2("invalid_input", "context target directory cannot be resolved"), null, 2);
22946
23428
  }
@@ -23484,8 +23966,9 @@ var evaluateShellPermission = (root, event, workdirs = new Map) => {
23484
23966
  event.message = reason;
23485
23967
  };
23486
23968
  const resources = event.resources.filter((resource) => typeof resource === "string");
23969
+ const owner = liveOwner({ host: "opencode", session: { id: event.sessionID ?? "", parentId: null } }, "in-process-plugin");
23487
23970
  if (WRITE_ACTIONS.has(event.action)) {
23488
- const decision = writeGate(root, resources);
23971
+ const decision = writeGate(root, resources, owner);
23489
23972
  if (decision.kind === "deny")
23490
23973
  deny(decision.reason);
23491
23974
  return;
@@ -23507,9 +23990,8 @@ var evaluateShellPermission = (root, event, workdirs = new Map) => {
23507
23990
  }, resource, { nudge: false });
23508
23991
  if (raw.kind === "deny")
23509
23992
  return deny(raw.reason);
23510
- const writes = shellWrites(resource);
23511
- const gated = writes.writes ? writeGate(root, writes.targets) : null;
23512
- if (gated?.kind === "deny")
23993
+ const gated = shellGate(cwd, resource, owner);
23994
+ if (gated.kind === "deny")
23513
23995
  return deny(gated.reason);
23514
23996
  }
23515
23997
  };
@@ -23533,7 +24015,7 @@ var childTaskMissing = (store, input) => {
23533
24015
  var childDenial = (identity) => `OpenCode child session has no validated Workit worker: as ${identity} it may only record evidence or findings (workit_evidence/workit_finding action "record") on an existing task; for anything else the lead assigns it a worker with workit_worker before launching it`;
23534
24016
 
23535
24017
  // packages/workit-opencode/src/v2/shell.ts
23536
- import path34 from "node:path";
24018
+ import path35 from "node:path";
23537
24019
  var commandOf = (input) => {
23538
24020
  if (input === null || typeof input !== "object")
23539
24021
  return null;
@@ -23542,7 +24024,7 @@ var commandOf = (input) => {
23542
24024
  };
23543
24025
  var hookInput = (root, sessionID, id, shell) => ({
23544
24026
  host: "opencode",
23545
- cwd: shell.workdir ? path34.resolve(root, shell.workdir) : root,
24027
+ cwd: shell.workdir ? path35.resolve(root, shell.workdir) : root,
23546
24028
  session: { id: sessionID, agentId: null, agentType: null, parentId: null },
23547
24029
  permissionMode: null,
23548
24030
  transcriptPath: null,
@@ -23727,37 +24209,37 @@ var createStopControl = (root, port) => {
23727
24209
 
23728
24210
  // packages/workit-opencode/src/v2/registry.ts
23729
24211
  import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
23730
- import path36 from "node:path";
24212
+ import path37 from "node:path";
23731
24213
 
23732
24214
  // packages/workit-opencode/src/shared/assets.ts
23733
24215
  import { existsSync as existsSync11, realpathSync as realpathSync9 } from "node:fs";
23734
- import path35 from "node:path";
24216
+ import path36 from "node:path";
23735
24217
  import { fileURLToPath as fileURLToPath2 } from "node:url";
23736
24218
  var moduleDir = () => {
23737
24219
  try {
23738
- return realpathSync9(path35.dirname(fileURLToPath2(import.meta.url)));
24220
+ return realpathSync9(path36.dirname(fileURLToPath2(import.meta.url)));
23739
24221
  } catch {
23740
- return path35.dirname(fileURLToPath2(import.meta.url));
24222
+ return path36.dirname(fileURLToPath2(import.meta.url));
23741
24223
  }
23742
24224
  };
23743
24225
  var packageRoot2 = () => {
23744
24226
  const dir = moduleDir();
23745
24227
  for (const candidate of [
23746
- path35.resolve(dir, ".."),
23747
- path35.resolve(dir, "..", ".."),
23748
- path35.resolve(dir, "..", "..", "..")
24228
+ path36.resolve(dir, ".."),
24229
+ path36.resolve(dir, "..", ".."),
24230
+ path36.resolve(dir, "..", "..", "..")
23749
24231
  ]) {
23750
- if (existsSync11(path35.join(candidate, "package.json")))
24232
+ if (existsSync11(path36.join(candidate, "package.json")))
23751
24233
  return candidate;
23752
24234
  }
23753
- return path35.resolve(dir, "..");
24235
+ return path36.resolve(dir, "..");
23754
24236
  };
23755
24237
  var skillsRoot = () => {
23756
24238
  const root = packageRoot2();
23757
- const canonical = path35.resolve(root, "..", "workit-core", "skills");
23758
- if (existsSync11(path35.join(root, "src")) && existsSync11(canonical))
24239
+ const canonical = path36.resolve(root, "..", "workit-core", "skills");
24240
+ if (existsSync11(path36.join(root, "src")) && existsSync11(canonical))
23759
24241
  return canonical;
23760
- return path35.join(root, "assets", "skills");
24242
+ return path36.join(root, "assets", "skills");
23761
24243
  };
23762
24244
 
23763
24245
  // packages/workit-opencode/src/v2/registry.ts
@@ -23784,11 +24266,11 @@ var registerSkills = async (ctx) => {
23784
24266
  if (!existsSync12(skillsDir))
23785
24267
  return new Set;
23786
24268
  const listed = await ctx.skill.list().catch(() => ({ data: [] }));
23787
- const own = (skill) => typeof skill.path === "string" && path36.resolve(skill.path).startsWith(skillsDir + path36.sep);
24269
+ const own = (skill) => typeof skill.path === "string" && path37.resolve(skill.path).startsWith(skillsDir + path37.sep);
23788
24270
  const userSkills = new Set((listed?.data ?? []).filter((skill) => !own(skill)).map((skill) => skill.id));
23789
24271
  const packaged = new Map;
23790
24272
  for (const id of WORKIT_METHOD_SKILLS) {
23791
- const file = path36.join(skillsDir, id, "SKILL.md");
24273
+ const file = path37.join(skillsDir, id, "SKILL.md");
23792
24274
  if (!userSkills.has(id) && existsSync12(file))
23793
24275
  packaged.set(id, file);
23794
24276
  }
@@ -23836,22 +24318,22 @@ var registerCommands = async (ctx, registeredSkills) => {
23836
24318
 
23837
24319
  // packages/workit-opencode/src/stale-sources.ts
23838
24320
  import { existsSync as existsSync13, readdirSync as readdirSync6 } from "node:fs";
23839
- import path37 from "node:path";
24321
+ import path38 from "node:path";
23840
24322
  import { fileURLToPath as fileURLToPath3 } from "node:url";
23841
24323
  var typescriptFiles = (directory) => {
23842
24324
  if (!existsSync13(directory))
23843
24325
  return [];
23844
24326
  return readdirSync6(directory, { withFileTypes: true }).flatMap((entry) => {
23845
- const file = path37.join(directory, entry.name);
24327
+ const file = path38.join(directory, entry.name);
23846
24328
  return entry.isDirectory() ? typescriptFiles(file) : entry.isFile() && file.endsWith(".ts") ? [file] : [];
23847
24329
  });
23848
24330
  };
23849
24331
  var pluginSourceFiles = [
23850
24332
  ...new Set([
23851
24333
  fileURLToPath3(import.meta.url),
23852
- ...typescriptFiles(path37.join(packageRoot2(), "src")),
23853
- path37.join(packageRoot2(), "..", "workit-core", "src", "core.ts"),
23854
- ...typescriptFiles(path37.join(packageRoot2(), "..", "workit-core", "src", "core"))
24334
+ ...typescriptFiles(path38.join(packageRoot2(), "src")),
24335
+ path38.join(packageRoot2(), "..", "workit-core", "src", "core.ts"),
24336
+ ...typescriptFiles(path38.join(packageRoot2(), "..", "workit-core", "src", "core"))
23855
24337
  ])
23856
24338
  ].filter((file) => existsSync13(file)).toSorted();
23857
24339
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-opencode",
3
- "version": "8.13.1",
3
+ "version": "8.14.1",
4
4
  "private": false,
5
5
  "description": "Workit OpenCode plugin — native tools, adaptive policy, and sixteen method skills",
6
6
  "keywords": [
@@ -39,7 +39,7 @@
39
39
  "typecheck": "tsc --noEmit"
40
40
  },
41
41
  "devDependencies": {
42
- "@brainervirus/workit-core": "^8.13.1",
42
+ "@brainervirus/workit-core": "^8.14.1",
43
43
  "@opencode/plugin": "2.0.18"
44
44
  },
45
45
  "engines": {