@brainervirus/workit-opencode 8.9.1 → 8.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,7 +25,7 @@ REPORT: branch, head SHA, files changed, each VERIFY command with its exit code,
25
25
  each ACCEPTANCE line met / not met, rulings you made (`workit ledger ruling`),
26
26
  anything out of scope as a follow-up, not a diff.
27
27
  STANDING: <the standing orders, verbatim: `workit ledger standing add` records each one>
28
- export WORKIT_SESSION_ID=<lead>-w<n> (set by the lead; a verifier brief gets <lead>-v<n>)
28
+ export WORKIT_SESSION_ID=<lead>-w<n> (set by the lead for a worker's commits; a verifier brief sets none: the host derives it)
29
29
  ```
30
30
 
31
31
  ## Worked example
@@ -72,12 +72,22 @@ Which session id a verdict carries, per host:
72
72
 
73
73
  - Claude Code: subagents inherit your `WORKIT_SESSION_ID`, so the
74
74
  SubagentStart hook names each `verifier` and `reviewer` its own session
75
- (`<lead>:<agent id>`), passed as `--session`; that one wins over any
76
- `<lead>-v<n>` in a brief. An `implementer` uses the session its brief
77
- exports for its workit commands.
78
- - Other hosts: the session the brief sets wins: `export
79
- WORKIT_SESSION_ID=<lead>-w-<slice>` from `workit fanout brief` for a
80
- worker, `<lead>-v<n>` (or `--as verifier`) for a verifier you start.
75
+ (`<lead>:<agent id>`), passed exactly as `--session`: the hook records the
76
+ ids it issues, and only those are accepted. An `implementer` uses the
77
+ session its brief exports for its workit commands.
78
+ - Codex: the `workit-verifier` and `workit-reviewer` agents get the same
79
+ issued `--session` from SubagentStart; any other verifier passes
80
+ `--as verifier`.
81
+ - OpenCode: a child session's shell already has
82
+ `WORKIT_SESSION_ID=<lead>:<child>`; Pi: a supervised worker's shell has
83
+ `<lead>:<worker session>`. Set nothing for a verifier there.
84
+ - Cursor (no session id in the shell): the brief sets the verifier's
85
+ `WORKIT_SESSION_ID=<lead>:verifier-<random hex>`, a fresh suffix per
86
+ verifier. Cursor cannot check it: it is declared, not derived.
87
+ - A worker's commits use `export WORKIT_SESSION_ID=<lead>-w-<slice>` from
88
+ `workit fanout brief`. A verdict under an id you made up (`<lead>-v1`,
89
+ `<lead>:v1`) is refused as `forged_session` on Claude Code, Codex,
90
+ OpenCode and Pi.
81
91
 
82
92
  A batch verifier takes several slices on one surface in one session: for
83
93
  each branch it checks out the head, runs that slice's VERIFY, and records its
@@ -33,8 +33,9 @@ description: Build a requested change in small verified steps - follow local pat
33
33
  `workit ledger verdict tests-verified --self --how "<what you ran>"`; it
34
34
  reads self-reviewed, never verified. High risk, a workspace with
35
35
  `verification: independent` (`workit grant show`) or a verified merge needs
36
- a fresh verifier with its own session (`WORKIT_SESSION_ID=<yours>-v1`;
37
- Claude Code: the `verifier` agent). Before saying done, reconcile
36
+ a fresh verifier with its own host-derived session (Claude Code and Codex:
37
+ the `verifier` agent; OpenCode: a child session; Pi: a supervised worker),
38
+ never a hand-minted id. Before saying done, reconcile
38
39
  every named deliverable against the target checkout and observe it (for a push:
39
40
  `workit verify-delivery push`).
40
41
 
@@ -7,8 +7,10 @@ description: Independent review of a diff, branch or PR - intent fidelity and st
7
7
 
8
8
  Review the candidate, never the author's summary of it. A session that wrote a
9
9
  commit on the branch cannot record a passing verdict: if that is you, hand the
10
- review to a fresh agent with its own session (`WORKIT_SESSION_ID=<lead>-v<n>`;
11
- Claude Code: the `reviewer` or `verifier` agent).
10
+ review to a fresh agent with its own host-derived session: Claude Code and
11
+ Codex, the `reviewer` or `verifier` agent (`--session` from SubagentStart);
12
+ OpenCode, a child session; Pi, a supervised worker. Never hand-mint one such
13
+ as `<lead>-v1`: it is refused as `forged_session`.
12
14
  An author's `--self` verdict reads self-reviewed and never counts as
13
15
  independent; `type-check-only` never proves a behavior change.
14
16
 
package/dist/plugin.js CHANGED
@@ -9597,64 +9597,11 @@ function acquireFileLockSync(targetPath, options) {
9597
9597
  }
9598
9598
  }
9599
9599
  }
9600
- // packages/workit-core/src/host-session.ts
9601
- var HOST_SESSION_VARS = [
9602
- ["OPENCODE_SESSION_ID", () => "opencode"],
9603
- ["PI_SESSION_ID", () => "pi"],
9604
- [
9605
- "CODEX_THREAD_ID",
9606
- (env) => env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop" || env.CODEX_ELECTRON_RESOURCES_PATH ? "codex_desktop" : "codex_cli"
9607
- ]
9608
- ];
9609
- var family = (host) => host?.replace(/_(?:cli|desktop)$/, "") ?? null;
9610
- function hostSessionFromEnv(env) {
9611
- const value = (key) => env[key]?.trim() || null;
9612
- let inner = null;
9613
- for (const [key, host] of HOST_SESSION_VARS) {
9614
- const session = value(key);
9615
- if (session) {
9616
- inner = { host: host(env), session };
9617
- break;
9618
- }
9619
- }
9620
- const declared = value("WORKIT_HOST");
9621
- if (env.WORKIT_SESSION_ID !== undefined) {
9622
- const inherited = inner !== null && declared !== null && family(declared) !== family(inner.host);
9623
- if (!inherited)
9624
- return { host: declared, session: value("WORKIT_SESSION_ID") };
9625
- }
9626
- return inner ? { host: inner.host, session: inner.session } : { host: declared, session: null };
9627
- }
9628
-
9629
- // packages/workit-core/src/core/package-root.ts
9630
- import { existsSync } from "node:fs";
9631
- import path3 from "node:path";
9632
- import { fileURLToPath } from "node:url";
9633
- var packageRoot = () => {
9634
- let dir = path3.dirname(fileURLToPath(import.meta.url));
9635
- while (true) {
9636
- const parent = path3.dirname(dir);
9637
- if (existsSync(path3.join(dir, "package.json")) || parent === dir)
9638
- return dir;
9639
- dir = parent;
9640
- }
9641
- };
9642
- var assetRoot = () => {
9643
- const root = packageRoot();
9644
- const assets = path3.join(root, "assets");
9645
- return existsSync(assets) ? assets : root;
9646
- };
9647
-
9648
- // packages/workit-core/src/core/store-lock.ts
9649
- import { spawnSync as spawnSync2 } from "node:child_process";
9650
- import fs5 from "node:fs";
9651
- import { hostname } from "node:os";
9652
-
9653
9600
  // packages/workit-core/src/store/paths.ts
9654
9601
  import { spawnSync } from "node:child_process";
9655
9602
  import { createHash as createHash2 } from "node:crypto";
9656
9603
  import fs4 from "node:fs";
9657
- import path4 from "node:path";
9604
+ import path3 from "node:path";
9658
9605
  var NOT_A_REPO = /not a git repository \(or any (of the parent directories|parent up to)/u;
9659
9606
  var gitRun = (cwd, args) => {
9660
9607
  const run = spawnSync("git", args, {
@@ -9696,14 +9643,14 @@ var DISCOVERY_ENV = [
9696
9643
  ];
9697
9644
  var isGitDir = (dir) => {
9698
9645
  try {
9699
- return fs4.statSync(path4.join(dir, "HEAD")).isFile() && fs4.statSync(path4.join(dir, "objects")).isDirectory() && fs4.statSync(path4.join(dir, "refs")).isDirectory();
9646
+ return fs4.statSync(path3.join(dir, "HEAD")).isFile() && fs4.statSync(path3.join(dir, "objects")).isDirectory() && fs4.statSync(path3.join(dir, "refs")).isDirectory();
9700
9647
  } catch {
9701
9648
  return false;
9702
9649
  }
9703
9650
  };
9704
9651
  var reftableRefs = (common) => {
9705
9652
  try {
9706
- return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs4.readFileSync(path4.join(common, "config"), "utf8"));
9653
+ return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs4.readFileSync(path3.join(common, "config"), "utf8"));
9707
9654
  } catch {
9708
9655
  return false;
9709
9656
  }
@@ -9723,7 +9670,7 @@ function fastGit(root) {
9723
9670
  if (device !== null && stat.dev !== device)
9724
9671
  return { kind: "none" };
9725
9672
  device = stat.dev;
9726
- const dotGit = path4.join(dir, ".git");
9673
+ const dotGit = path3.join(dir, ".git");
9727
9674
  let entry = null;
9728
9675
  try {
9729
9676
  entry = fs4.statSync(dotGit);
@@ -9744,19 +9691,19 @@ function fastGit(root) {
9744
9691
  const match = /^gitdir: (.+)$/mu.exec(text);
9745
9692
  if (!match)
9746
9693
  return null;
9747
- gitDir = path4.resolve(dir, match[1].trim());
9694
+ gitDir = path3.resolve(dir, match[1].trim());
9748
9695
  } else
9749
9696
  return null;
9750
- if (!fs4.existsSync(path4.join(gitDir, "HEAD")))
9697
+ if (!fs4.existsSync(path3.join(gitDir, "HEAD")))
9751
9698
  return null;
9752
9699
  } else if (isGitDir(dir))
9753
9700
  gitDir = dir;
9754
9701
  if (gitDir) {
9755
9702
  let common = gitDir;
9756
9703
  try {
9757
- common = path4.resolve(gitDir, fs4.readFileSync(path4.join(gitDir, "commondir"), "utf8").trim());
9704
+ common = path3.resolve(gitDir, fs4.readFileSync(path3.join(gitDir, "commondir"), "utf8").trim());
9758
9705
  } catch {}
9759
- if (!fs4.existsSync(path4.join(common, "objects")))
9706
+ if (!fs4.existsSync(path3.join(common, "objects")))
9760
9707
  return null;
9761
9708
  return {
9762
9709
  kind: "repo",
@@ -9766,26 +9713,26 @@ function fastGit(root) {
9766
9713
  reftable: reftableRefs(common)
9767
9714
  };
9768
9715
  }
9769
- const parent = path4.dirname(dir);
9716
+ const parent = path3.dirname(dir);
9770
9717
  if (parent === dir)
9771
9718
  return { kind: "none" };
9772
9719
  dir = parent;
9773
9720
  }
9774
9721
  }
9775
9722
  function resolveStore(root) {
9776
- root = path4.resolve(root);
9723
+ root = path3.resolve(root);
9777
9724
  let directory = false;
9778
9725
  try {
9779
9726
  directory = fs4.statSync(root).isDirectory();
9780
9727
  } catch {}
9781
9728
  if (!directory)
9782
- return { dir: path4.join(root, ".workit"), shared: false, top: null };
9729
+ return { dir: path3.join(root, ".workit"), shared: false, top: null };
9783
9730
  const fast = fastGit(root);
9784
9731
  if (fast?.kind === "none")
9785
- return { dir: path4.join(root, ".workit"), shared: false, top: null };
9732
+ return { dir: path3.join(root, ".workit"), shared: false, top: null };
9786
9733
  if (fast?.kind === "repo")
9787
9734
  return {
9788
- dir: path4.join(fast.common, "workit"),
9735
+ dir: path3.join(fast.common, "workit"),
9789
9736
  shared: true,
9790
9737
  top: fast.top,
9791
9738
  git: { gitDir: fast.gitDir, top: fast.top, reftable: fast.reftable }
@@ -9795,20 +9742,20 @@ function resolveStore(root) {
9795
9742
  if (run.ok && common) {
9796
9743
  const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
9797
9744
  return {
9798
- dir: path4.join(real(path4.resolve(root, common)), "workit"),
9745
+ dir: path3.join(real(path3.resolve(root, common)), "workit"),
9799
9746
  shared: true,
9800
9747
  top: top.ok && top.stdout.trim() ? real(top.stdout.trim()) : null
9801
9748
  };
9802
9749
  }
9803
9750
  if (run.missing || !run.ok && NOT_A_REPO.test(run.stderr))
9804
- return { dir: path4.join(root, ".workit"), shared: false, top: null };
9751
+ return { dir: path3.join(root, ".workit"), shared: false, top: null };
9805
9752
  return new Error(`cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
9806
9753
  `)[0] || "no output"}); fix the repository (git status should work here)`);
9807
9754
  }
9808
9755
  var rebasingBranch = (gitDir) => {
9809
9756
  for (const state of ["rebase-merge", "rebase-apply"]) {
9810
9757
  try {
9811
- const ref = fs4.readFileSync(path4.join(gitDir, state, "head-name"), "utf8").trim();
9758
+ const ref = fs4.readFileSync(path3.join(gitDir, state, "head-name"), "utf8").trim();
9812
9759
  const match = /^refs\/heads\/(.+)$/u.exec(ref);
9813
9760
  if (match)
9814
9761
  return match[1];
@@ -9823,7 +9770,7 @@ function resolveTaskKey(root, location) {
9823
9770
  if (location.git && !location.git.reftable) {
9824
9771
  let head = "";
9825
9772
  try {
9826
- head = fs4.readFileSync(path4.join(location.git.gitDir, "HEAD"), "utf8").trim();
9773
+ head = fs4.readFileSync(path3.join(location.git.gitDir, "HEAD"), "utf8").trim();
9827
9774
  } catch {}
9828
9775
  const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
9829
9776
  if (ref && ref[1] !== ".invalid")
@@ -9854,7 +9801,7 @@ function resolveTaskKey(root, location) {
9854
9801
  return { key: `detached-${shortHash(real(worktree))}`, kind: "detached", branch: null };
9855
9802
  }
9856
9803
  function checkoutRootOf(root, location) {
9857
- const resolved = path4.resolve(root);
9804
+ const resolved = path3.resolve(root);
9858
9805
  let canonical = resolved;
9859
9806
  try {
9860
9807
  canonical = fs4.realpathSync(resolved);
@@ -9885,13 +9832,13 @@ function branchRenames(location) {
9885
9832
  }
9886
9833
  for (const entry of entries)
9887
9834
  if (entry.isDirectory())
9888
- walk(path4.join(dir, entry.name));
9835
+ walk(path3.join(dir, entry.name));
9889
9836
  else
9890
- files.push(path4.join(dir, entry.name));
9837
+ files.push(path3.join(dir, entry.name));
9891
9838
  };
9892
- walk(path4.join(path4.dirname(location.dir), "logs", "refs", "heads"));
9839
+ walk(path3.join(path3.dirname(location.dir), "logs", "refs", "heads"));
9893
9840
  if (location.git)
9894
- files.push(path4.join(location.git.gitDir, "logs", "HEAD"));
9841
+ files.push(path3.join(location.git.gitDir, "logs", "HEAD"));
9895
9842
  const seen = new Set;
9896
9843
  const renames = [];
9897
9844
  for (const file of files) {
@@ -9926,11 +9873,69 @@ var checkoutSlug = (root) => {
9926
9873
  let canonical = real(root);
9927
9874
  if (process.platform === "win32")
9928
9875
  canonical = canonical.toLowerCase();
9929
- const base = path4.basename(canonical).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40) || "root";
9876
+ const base = path3.basename(canonical).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40) || "root";
9930
9877
  return `${base}-${shortHash(canonical)}`;
9931
9878
  };
9932
9879
 
9880
+ // packages/workit-core/src/host-session.ts
9881
+ var HOST_SESSION_VARS = [
9882
+ ["OPENCODE_SESSION_ID", () => "opencode"],
9883
+ ["PI_SESSION_ID", () => "pi"],
9884
+ [
9885
+ "CODEX_THREAD_ID",
9886
+ (env) => env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop" || env.CODEX_ELECTRON_RESOURCES_PATH ? "codex_desktop" : "codex_cli"
9887
+ ]
9888
+ ];
9889
+ var family = (host) => host?.replace(/_(?:cli|desktop)$/, "") ?? null;
9890
+ function hostSessionFromEnv(env) {
9891
+ const value = (key) => env[key]?.trim() || null;
9892
+ let inner = null;
9893
+ for (const [key, host] of HOST_SESSION_VARS) {
9894
+ const session = value(key);
9895
+ if (session) {
9896
+ inner = { host: host(env), session };
9897
+ break;
9898
+ }
9899
+ }
9900
+ const declared = value("WORKIT_HOST");
9901
+ if (env.WORKIT_SESSION_ID !== undefined) {
9902
+ const inherited = inner !== null && declared !== null && family(declared) !== family(inner.host);
9903
+ if (!inherited)
9904
+ return { host: declared, session: value("WORKIT_SESSION_ID") };
9905
+ }
9906
+ return inner ? { host: inner.host, session: inner.session } : { host: declared, session: null };
9907
+ }
9908
+ var OWN_CHILD_SHELL = new Set(["opencode", "pi"]);
9909
+ var SESSION_UNSAFE = /[^A-Za-z0-9_.:@/+-]/g;
9910
+ function childSessionId(lead, child) {
9911
+ const leaf = child.replace(SESSION_UNSAFE, "-").slice(-120);
9912
+ const prefix = lead.replace(SESSION_UNSAFE, "-").slice(0, 127 - leaf.length);
9913
+ return prefix ? `${prefix}:${leaf}` : leaf;
9914
+ }
9915
+
9916
+ // packages/workit-core/src/core/package-root.ts
9917
+ import { existsSync } from "node:fs";
9918
+ import path4 from "node:path";
9919
+ import { fileURLToPath } from "node:url";
9920
+ var packageRoot = () => {
9921
+ let dir = path4.dirname(fileURLToPath(import.meta.url));
9922
+ while (true) {
9923
+ const parent = path4.dirname(dir);
9924
+ if (existsSync(path4.join(dir, "package.json")) || parent === dir)
9925
+ return dir;
9926
+ dir = parent;
9927
+ }
9928
+ };
9929
+ var assetRoot = () => {
9930
+ const root = packageRoot();
9931
+ const assets = path4.join(root, "assets");
9932
+ return existsSync(assets) ? assets : root;
9933
+ };
9934
+
9933
9935
  // packages/workit-core/src/core/store-lock.ts
9936
+ import { spawnSync as spawnSync2 } from "node:child_process";
9937
+ import fs5 from "node:fs";
9938
+ import { hostname } from "node:os";
9934
9939
  var metadataLockSchema = object({
9935
9940
  pid: number2().int().nonnegative().safe(),
9936
9941
  processStart: string2().nullable(),
@@ -14601,11 +14606,20 @@ function authorSessions(cwd, rows, branch, range = { base: null, head: null }, s
14601
14606
  sessions.add(session);
14602
14607
  return sessions;
14603
14608
  }
14609
+ var derivesFrom = (derived, bare) => !bare.includes(":") && derived.endsWith(`:${bare}`);
14610
+ var authoredBy = (authors, session) => {
14611
+ if (authors.has(session))
14612
+ return true;
14613
+ for (const author of authors)
14614
+ if (derivesFrom(author, session) || derivesFrom(session, author))
14615
+ return true;
14616
+ return false;
14617
+ };
14604
14618
  var independenceReasons = (row, authors) => {
14605
14619
  const reasons = [];
14606
14620
  if (!row.actor.session)
14607
14621
  reasons.push("no_session");
14608
- else if (authors.has(row.actor.session))
14622
+ else if (authoredBy(authors, row.actor.session))
14609
14623
  reasons.push("author_session");
14610
14624
  if (row.self === true && !reasons.includes("no_session"))
14611
14625
  reasons.push("self");
@@ -15686,7 +15700,8 @@ Autonomy contract:
15686
15700
  satisfies a gate; an ad-hoc \`workit check -- <cmd>\` never does.
15687
15701
  - Author is not verifier: your own verdict is \`--self\`, shown as
15688
15702
  self-reviewed, never verified. High risk, \`verification: independent\` and
15689
- merge need a verifier with its own session (\`WORKIT_SESSION_ID=<lead>-v<n>\`).
15703
+ merge need a verifier with its own host-derived session (a subagent's
15704
+ \`<lead>:<child>\`, set by the host); never hand-mint one such as \`<lead>-v1\`.
15690
15705
  - A local-commit endpoint does not imply PR readiness, and a local commit alone
15691
15706
  is not evidence of a requested remote push. Before reporting delivery,
15692
15707
  reconcile every requested item and observe it (\`workit verify-delivery\`).
@@ -21309,9 +21324,12 @@ var createV2Lifecycle = (deps) => {
21309
21324
  expectedWorkspaceRevision: workspace.data.revision
21310
21325
  } : null;
21311
21326
  };
21312
- const oldestOfSingleTask = (items) => {
21313
- const tasks = new Set(items.map((item) => item.task.id));
21314
- if (tasks.size !== 1 || items.length === 0)
21327
+ const currentTaskId = (store) => {
21328
+ const report = store.keyReport();
21329
+ return report.ok ? report.data.bound[0]?.id ?? null : null;
21330
+ };
21331
+ const oldest = (items) => {
21332
+ if (items.length === 0)
21315
21333
  return null;
21316
21334
  return [...items].toSorted((a, b) => a.entry.recordedAt < b.entry.recordedAt ? -1 : a.entry.recordedAt > b.entry.recordedAt ? 1 : a.entry.id < b.entry.id ? -1 : 1)[0];
21317
21335
  };
@@ -21320,6 +21338,16 @@ var createV2Lifecycle = (deps) => {
21320
21338
  const cancel = workerId ? `workit_worker {"action":"cancel","workerId":"${workerId}","reason":"launch not observed"}` : 'workit_worker {"action":"cancel","workerId":"<id>","reason":"launch not observed"} for the stuck worker';
21321
21339
  return `recovery_required: a previous native subagent launch is unsettled; wait for it to settle, or ${cancel} before launching again`;
21322
21340
  };
21341
+ const unboundLaunch = (coordinator) => {
21342
+ const store = new TaskStore(root);
21343
+ const current = currentTaskId(store);
21344
+ const base = current === null ? 'recovery_required: this branch has no Workit task yet; start it (workit_task start, or `workit task start "<objective>"`), then assign a worker on it with workit_worker before launching a native task' : "recovery_required: no assigned Workit worker is attributable to this coordinator on the current branch's task; assign one with workit_worker before launching a native task";
21345
+ const listed = store.listTasks();
21346
+ if (!listed.ok)
21347
+ return base;
21348
+ const stale = listed.data.flatMap((task) => task.status === "active" && task.id !== current ? task.workers.filter((worker) => (worker.data.state === "assigned" || worker.data.state === "running") && worker.provenance.session?.kind === "host" && worker.provenance.session.handle === coordinator).map((worker) => `workit_worker {"action":"cancel","taskId":"${task.id}","workerId":"${worker.id}","reason":"stale worker from another branch"}`) : []);
21349
+ return stale.length > 0 ? `${base}. Workers of this coordinator are still open on other tasks; cancel any that are stale: ${stale.join("; ")}` : base;
21350
+ };
21323
21351
  const prepareDispatch = (coordinator, callID) => {
21324
21352
  if (dispatches.has(coordinator))
21325
21353
  return "unsettled";
@@ -21332,7 +21360,8 @@ var createV2Lifecycle = (deps) => {
21332
21360
  if (managed.length === 0)
21333
21361
  return "unmanaged";
21334
21362
  const eligible = managed.flatMap((task) => task.workers.filter((entry) => entry.data.state === "assigned" && entry.data.session === null && entry.provenance.session?.kind === "host" && entry.provenance.session.host === "opencode" && entry.provenance.session.handle === coordinator).map((entry) => ({ task, entry })));
21335
- const next = oldestOfSingleTask(eligible);
21363
+ const current = currentTaskId(store);
21364
+ const next = oldest(eligible.filter((item) => item.task.id === current));
21336
21365
  if (!next)
21337
21366
  return "unbound";
21338
21367
  dispatches.set(coordinator, {
@@ -21526,9 +21555,8 @@ var createV2Lifecycle = (deps) => {
21526
21555
  if (prepared === "unsettled") {
21527
21556
  throw new Error(unsettledLaunch(coordinator));
21528
21557
  }
21529
- if (prepared === "unbound") {
21530
- throw new Error("recovery_required: no assigned Workit worker is attributable to this coordinator; assign one with workit_worker before launching a native task");
21531
- }
21558
+ if (prepared === "unbound")
21559
+ throw new Error(unboundLaunch(coordinator));
21532
21560
  } catch (error) {
21533
21561
  if (inflight.get(coordinator) === event.id)
21534
21562
  inflight.delete(coordinator);
@@ -21782,6 +21810,24 @@ var evaluateShellPermission = (root, event, workdirs = new Map) => {
21782
21810
  }
21783
21811
  };
21784
21812
 
21813
+ // packages/workit-opencode/src/v2/child-identity.ts
21814
+ var CHILD_RECORDS = {
21815
+ evidence: new Set(["record"]),
21816
+ finding: new Set(["record"])
21817
+ };
21818
+ var fields = (input) => typeof input === "object" && input !== null ? input : {};
21819
+ var childMayRecord = (family, input) => {
21820
+ const action = fields(input).action;
21821
+ return family !== null && typeof action === "string" && CHILD_RECORDS[family]?.has(action) === true;
21822
+ };
21823
+ var childTaskMissing = (store, input) => {
21824
+ if (fields(input).taskId !== undefined)
21825
+ return false;
21826
+ const report = store.keyReport();
21827
+ return !report.ok || report.data.bound.length === 0;
21828
+ };
21829
+ 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`;
21830
+
21785
21831
  // packages/workit-opencode/src/v2/shell.ts
21786
21832
  import path31 from "node:path";
21787
21833
  var commandOf = (input) => {
@@ -21838,6 +21884,54 @@ ${nudge.text}` };
21838
21884
  } catch {}
21839
21885
  };
21840
21886
 
21887
+ // packages/workit-opencode/src/v2/shell-session.ts
21888
+ var PENDING_LIMIT = 256;
21889
+ var commandText2 = (input) => {
21890
+ if (input === null || typeof input !== "object")
21891
+ return null;
21892
+ const command = input.command;
21893
+ return typeof command === "string" && command.trim() ? command : null;
21894
+ };
21895
+ var noteShellCall = (pending, event) => {
21896
+ const command = commandText2(event.input);
21897
+ if (!command)
21898
+ return;
21899
+ const sessionID = String(event.sessionID);
21900
+ const queue = pending.get(command) ?? { sessions: [], mixed: false };
21901
+ if (queue.sessions.some((other) => other !== sessionID))
21902
+ queue.mixed = true;
21903
+ queue.sessions.push(sessionID);
21904
+ pending.set(command, queue);
21905
+ if (pending.size > PENDING_LIMIT)
21906
+ pending.delete(pending.keys().next().value);
21907
+ };
21908
+ var settleShellCall = (pending, event) => {
21909
+ const command = commandText2(event.input);
21910
+ const queue = command ? pending.get(command) : undefined;
21911
+ if (!command || !queue)
21912
+ return;
21913
+ const index = queue.sessions.indexOf(String(event.sessionID));
21914
+ if (index >= 0)
21915
+ queue.sessions.splice(index, 1);
21916
+ if (queue.sessions.length === 0)
21917
+ pending.delete(command);
21918
+ };
21919
+ var applyShellSession = async (pending, event, parentOf) => {
21920
+ event.env.WORKIT_HOST = "opencode";
21921
+ delete event.env.WORKIT_SESSION_ID;
21922
+ const queue = pending.get(event.command);
21923
+ if (!queue || queue.sessions.length === 0)
21924
+ return;
21925
+ const sessionID = queue.sessions.shift();
21926
+ if (queue.sessions.length === 0)
21927
+ pending.delete(event.command);
21928
+ if (queue.mixed)
21929
+ return;
21930
+ const parent = await parentOf(sessionID);
21931
+ if (parent)
21932
+ event.env.WORKIT_SESSION_ID = childSessionId(parent, sessionID);
21933
+ };
21934
+
21841
21935
  // packages/workit-opencode/src/v2/skills.ts
21842
21936
  var hookInput2 = (root, sessionID, subagent) => ({
21843
21937
  host: "opencode",
@@ -22141,9 +22235,17 @@ var setup = async (ctx) => {
22141
22235
  };
22142
22236
  const store = new TaskStore(root);
22143
22237
  const workerId = workerIdFor(store, session.id, session.parentID, lifecycle.directChildren);
22144
- if (session.parentID !== undefined && workerId === null)
22145
- return resultContent(failure2("permission_denied", "OpenCode child session has no validated Workit worker"));
22146
22238
  const family = workitFamilyOf(spec.name);
22239
+ if (session.parentID !== undefined && workerId === null) {
22240
+ const identity = childSessionId(session.parentID, session.id);
22241
+ const lead = await sessionFacts(ctx, session.parentID);
22242
+ const directChild = lead !== null && lead.parentID === undefined && sameWorkspace(root, lead.directory);
22243
+ if (!directChild || family === null || !childMayRecord(family, input))
22244
+ return resultContent(failure2("permission_denied", childDenial(identity)));
22245
+ if (childTaskMissing(store, input))
22246
+ return resultContent(failure2("not_found", "no task for this branch yet: the lead starts one (workit_task start) before a child records on it, or pass taskId"));
22247
+ return resultContent(runFamily(family, input, { ...session, id: identity }, store, null));
22248
+ }
22147
22249
  if (family !== null)
22148
22250
  return resultContent(runFamily(family, input, session, store, workerId));
22149
22251
  if (spec.name === "workit_init_apply") {
@@ -22155,9 +22257,12 @@ var setup = async (ctx) => {
22155
22257
  }
22156
22258
  });
22157
22259
  const shellWorkdirs = new Map;
22260
+ const pendingShells = new Map;
22158
22261
  await ctx.tool.hook("execute.before", async (event) => {
22159
- if (event.tool === "shell")
22262
+ if (event.tool === "shell") {
22263
+ noteShellCall(pendingShells, { sessionID: String(event.sessionID), input: event.input });
22160
22264
  return observeShellBefore(root, event, shellWorkdirs);
22265
+ }
22161
22266
  if (event.tool === "skill")
22162
22267
  return observeSkillLoad(root, event);
22163
22268
  if (event.tool !== "subagent")
@@ -22169,9 +22274,17 @@ var setup = async (ctx) => {
22169
22274
  input: event.input
22170
22275
  });
22171
22276
  });
22277
+ const shellDomain = ctx.shell;
22278
+ if (shellDomain)
22279
+ await shellDomain.hook("create.before", (event) => applyShellSession(pendingShells, event, async (sessionID) => {
22280
+ const known = lifecycle.directChildren.get(sessionID);
22281
+ return known ?? (await sessionFacts(ctx, sessionID))?.parentID;
22282
+ }));
22172
22283
  await ctx.tool.hook("execute.after", async (event) => {
22173
- if (event.tool === "shell")
22284
+ if (event.tool === "shell") {
22285
+ settleShellCall(pendingShells, { sessionID: String(event.sessionID), input: event.input });
22174
22286
  return observeShellResult(root, event);
22287
+ }
22175
22288
  if (event.tool !== "subagent")
22176
22289
  return;
22177
22290
  await lifecycle.executeAfter({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-opencode",
3
- "version": "8.9.1",
3
+ "version": "8.10.0",
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
  "dependencies": {
42
- "@brainervirus/workit-core": "^8.9.1"
42
+ "@brainervirus/workit-core": "^8.10.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@opencode/plugin": "2.0.18"