@brainervirus/workit-opencode 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,7 +43,7 @@ plugin is a self-contained Node bundle; its default export is a dual entry
43
43
 
44
44
  - **Receipts** — native `question` answers are purpose-bound, session-bound, fresh, and one-use; unrelated questions fail closed.
45
45
  - **Delegation** — native `task` workers are direct-child-only; nested or uncertain lineage is denied (`delegation_lineage_denied`).
46
- - **Continuity** — compact task context is injected once on session start and once after compaction; unobservable shell surfaces are labeled `agent_guided`.
46
+ - **Continuity** — compact task context carries the newest decisions and bounded redacted choice summaries, injected once on session start and once after compaction; unobservable shell surfaces are labeled `agent_guided`.
47
47
 
48
48
  Workit does not register `workit_external_action` on either OpenCode version.
49
49
  Use native tools for mutations under the host permissions and target conventions;
@@ -63,9 +63,11 @@ before retrying them.
63
63
  The build bundles the `@opencode-ai/plugin` (V1) and `@opencode/plugin` (V2) SDK surfaces used by the adapters into `dist/plugin.js`, so the published plugin has **no** runtime dependency on either SDK (both stay development/build-only pins). The plugin loads through its real package entry `dist/plugin.js`; only the fourteen method skills ship under `assets/`.
64
64
 
65
65
  The V2 entry registers the same ten tools with `codemode: false`, the fourteen
66
- skills and `wk-*` commands (user collisions preserved), question receipts for
67
- `decision.record`, direct-child subagent lineage with durable dispatch claims,
68
- and the bootstrap/task/worker context plus compaction injection. Its shell
66
+ skills and up to fourteen `wk-*` commands. Existing user skills and commands
67
+ are preserved; an alias is added only when its Workit skill is registered. It
68
+ also provides question receipts for `decision.record`, direct-child subagent
69
+ lineage with durable dispatch claims, and bootstrap/task/worker context plus
70
+ compaction injection. Its shell
69
71
  permission hook adds branch-name policy denials for direct, unquoted literal
70
72
  branch-creation forms only. It leaves compliant branches, PR and worktree
71
73
  commands, and unsupported shell syntax to OpenCode's native permission rules.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: workit-implement
3
- description: Use when implementation benefits from bounded delegation or shared checkout coordination
3
+ description: Use when implementing requested code changes in a repository. Follow its rules and host permissions; use Workit tracking and delegation only when continuity or coordination helps.
4
4
  ---
5
5
 
6
6
  # Implement within authority
@@ -22,6 +22,14 @@ request, and a timeout is not proof that a worker stopped.
22
22
  until process exit or explicit recovery.
23
23
  4. Reconcile helper reports and run the checks appropriate to the requested
24
24
  outcome. If delegation is unavailable, continue inline when useful.
25
+ 5. Before a cross-repo mutation, resolve the actual checkout, branch and remote
26
+ from the request and current context. Ask only if competing plausible targets
27
+ remain unresolved. Branch, commit, direct push, PR-ready, merge and release
28
+ are distinct endpoints; perform only the authorized one under target rules.
29
+ Before saying done, reconcile every named deliverable against that checkout
30
+ and verify the requested result. For a push, observe the destination remote
31
+ ref and confirm it contains the delivered commit; report drift or missing
32
+ items as blockers instead of treating local success as remote delivery.
25
33
 
26
34
  Do not edit Workit metadata directly, create nested helper trees, widen paths, or
27
35
  create a second lifecycle. Read-only investigation and bounded reports do not
@@ -18,7 +18,11 @@ request. Preserve continuity when it helps; do not manufacture task mutations.
18
18
  3. For a separate request, do not silently resume an old objective. Park a
19
19
  concise checkpoint only when substantial work needs to continue later. Start
20
20
  a distinct tracked record only if the new work benefits from continuity,
21
- dependencies, coordination, or durable decisions.
21
+ dependencies, coordination, or durable decisions. For work spanning repos,
22
+ checkpoint each unfinished item with its checkout, branch, requested
23
+ deliverables, and delivery endpoint. Keep explicitly held items parked with
24
+ their resume condition until the user resumes them. A conversational
25
+ checkpoint is sufficient when no task record is needed.
22
26
  4. Before resuming a named tracked task, reconcile its checkout, branch, dirty
23
27
  state, current policy, stale evidence, uncertain effects, and ownership. Do
24
28
  not change branches, stash, fetch large histories, or seize ownership just
package/dist/plugin.js CHANGED
@@ -12560,7 +12560,8 @@ let imported decisions grant authority. Never turn a host deny into allow or
12560
12560
  claim enforcement/evidence a host cannot provide. A precise request with settled
12561
12561
  constraints does not need an interview. Present genuine unresolved options with
12562
12562
  evidence and a recommendation, then continue toward the requested delivery
12563
- endpoint: investigation, implementation, PR/MR-ready, merge, or release. Run
12563
+ endpoint: investigation, implementation, branch/commit/push, PR/MR-ready,
12564
+ merge, or release. Run
12564
12565
  applicable checks and safe repairs without repeated continuation questions;
12565
12566
  stop for missing host authority, a new consequential choice, a conflicting edit,
12566
12567
  or an unresolved blocker. A tracked record may close when actual evidence
@@ -12572,6 +12573,12 @@ confirm the stop. Preserve unresolved requirements, gaps, and uncertain workers.
12572
12573
  When context changes, distinguish a quick question, same-task adjustment, and
12573
12574
  separate request. Answer a quick question without pausing/resuming task state;
12574
12575
  park a concise checkpoint only when substantial work needs to continue later.
12576
+ For work spanning repositories, bind each unfinished item to its actual checkout,
12577
+ branch, requested deliverables, and delivery endpoint. Preserve held items with
12578
+ an explicit resume condition. Resolve competing plausible targets before a
12579
+ mutation; ask only when available context cannot settle that consequential choice.
12580
+ Before reporting delivery, reconcile every requested item and observe the actual
12581
+ target result; a local commit alone is not evidence of a requested remote push.
12575
12582
 
12576
12583
  Skill routing: slash aliases /wk-* load on demand. Use workit-steer for a
12577
12584
  substantial interruption or change of direction, workit-deslop when relevant
@@ -12618,6 +12625,13 @@ var compactText = (value, limit) => value === null ? null : value.replace(privat
12618
12625
  return `${match.slice(0, space)} <redacted>`;
12619
12626
  }).slice(0, limit);
12620
12627
  var compactBytes = (value) => new TextEncoder().encode(value).byteLength;
12628
+ var compactTimestamp = (value) => `${value.slice(0, 19)}.${value.slice(20, -1).padEnd(9, "0")}`;
12629
+ var compactDecisionChoice = (entry) => {
12630
+ const stated = entry.data.binding.statedChoice?.text;
12631
+ const approved = entry.data.response === "approved" ? entry.data.binding.approvedContent : "";
12632
+ const source = stated?.trim() || approved.trim() || null;
12633
+ return compactText(source, COMPACT_TEXT_BYTES);
12634
+ };
12621
12635
  var compactReference = (ref) => {
12622
12636
  if (ref.kind === "file")
12623
12637
  return {
@@ -12633,11 +12647,18 @@ var compactReference = (ref) => {
12633
12647
  return { kind: ref.kind };
12634
12648
  };
12635
12649
  function compactTaskContext(view) {
12636
- const decisions = view.task.decisions.slice().sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0).slice(0, COMPACT_MAX_ITEMS).map((entry) => ({
12650
+ const decisions = view.task.decisions.slice().sort((left, right) => {
12651
+ const leftAt = compactTimestamp(left.recordedAt);
12652
+ const rightAt = compactTimestamp(right.recordedAt);
12653
+ if (leftAt !== rightAt)
12654
+ return leftAt > rightAt ? -1 : 1;
12655
+ return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
12656
+ }).slice(0, COMPACT_MAX_ITEMS).map((entry) => ({
12637
12657
  id: entry.id,
12638
12658
  purpose: entry.data.purpose,
12639
12659
  status: entry.data.response,
12640
12660
  digest: entry.data.digest,
12661
+ choice: compactDecisionChoice(entry) || `task.inspect taskId=${view.task.id} view=full; decision=${entry.id}`,
12641
12662
  references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
12642
12663
  }));
12643
12664
  const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
@@ -31346,7 +31367,6 @@ var createV2Lifecycle = (deps) => {
31346
31367
  const dispatches = new Map;
31347
31368
  const inflight = new Map;
31348
31369
  const lifecycleBindings = new Map;
31349
- const unresolvedTaskLaunches = new Set;
31350
31370
  const now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
31351
31371
  const readSession = async (sessionID) => {
31352
31372
  try {
@@ -31443,7 +31463,7 @@ var createV2Lifecycle = (deps) => {
31443
31463
  });
31444
31464
  return "prepared";
31445
31465
  };
31446
- const commitDispatchStart = (coordinator, childID) => {
31466
+ const commitDispatchStart = (coordinator, childID, retainPending = false) => {
31447
31467
  const pending = dispatches.get(coordinator);
31448
31468
  if (!pending)
31449
31469
  return false;
@@ -31463,9 +31483,11 @@ var createV2Lifecycle = (deps) => {
31463
31483
  });
31464
31484
  if (!committed.ok)
31465
31485
  return false;
31466
- dispatches.delete(coordinator);
31467
- if (inflight.get(coordinator) === pending.callID)
31468
- inflight.delete(coordinator);
31486
+ if (!retainPending) {
31487
+ dispatches.delete(coordinator);
31488
+ if (inflight.get(coordinator) === pending.callID)
31489
+ inflight.delete(coordinator);
31490
+ }
31469
31491
  lifecycleBindings.set(childID, {
31470
31492
  parentID: coordinator,
31471
31493
  taskId: pending.taskId,
@@ -31478,32 +31500,32 @@ var createV2Lifecycle = (deps) => {
31478
31500
  const listed = store.listTasks();
31479
31501
  const workspace = store.readWorkspace();
31480
31502
  if (!listed.ok || !workspace.ok || !workspace.data)
31481
- return;
31503
+ return false;
31482
31504
  const persisted = listed.data.flatMap((task) => task.status === "active" ? task.workers.filter((entry) => entry.id === binding?.workerId || entry.data.session?.kind === "host" && entry.data.session.handle === sessionID).map((entry) => ({ task, entry })) : []);
31483
31505
  const matches = persisted.filter(({ entry }) => entry.data.session?.kind === "host");
31484
31506
  if (binding && matches.length !== 1)
31485
- return;
31507
+ return false;
31486
31508
  const selected = binding ? persisted.find(({ task, entry }) => task.id === binding.taskId && entry.id === binding.workerId) : matches.length === 1 ? matches[0] : undefined;
31487
31509
  if (!selected)
31488
- return;
31510
+ return false;
31489
31511
  if (selected.entry.data.session?.kind !== "host")
31490
- return;
31512
+ return false;
31491
31513
  if (selected.entry.provenance.session?.kind !== "host")
31492
- return;
31514
+ return false;
31493
31515
  if (!binding && deleted)
31494
- return;
31516
+ return false;
31495
31517
  if (!deleted) {
31496
31518
  const observed = await readSession(sessionID);
31497
31519
  const observedParent = parentOf(observed);
31498
31520
  if (!observed || !sameWorkspace(root, observed.directory) || !observedParent)
31499
- return;
31521
+ return false;
31500
31522
  if (!binding) {
31501
31523
  const coordinator = workerCoordinatorFor(selected.task, selected.entry);
31502
31524
  if (!coordinator || observedParent !== coordinator)
31503
- return;
31525
+ return false;
31504
31526
  parentID = coordinator;
31505
31527
  } else if (observedParent !== parentID)
31506
- return;
31528
+ return false;
31507
31529
  }
31508
31530
  directChildren.set(sessionID, parentID);
31509
31531
  const core = new WorkitCore(store, {
@@ -31525,34 +31547,51 @@ var createV2Lifecycle = (deps) => {
31525
31547
  observation: { event: state, sessionID }
31526
31548
  });
31527
31549
  if (!result.ok)
31528
- return;
31550
+ return false;
31529
31551
  lifecycleBindings.set(sessionID, {
31530
31552
  parentID,
31531
31553
  taskId: selected.task.id,
31532
31554
  workerId: selected.entry.id
31533
31555
  });
31556
+ return true;
31534
31557
  };
31535
31558
  const reconcilePending = async (coordinator) => {
31536
31559
  const pending = dispatches.get(coordinator);
31537
- if (!pending?.childID)
31560
+ if (!pending)
31561
+ return;
31562
+ const task = new TaskStore(root).readTask(pending.taskId);
31563
+ if (task.ok && task.data.workers.find((worker) => worker.id === pending.workerId)?.data.state === "stopped") {
31564
+ dispatches.delete(coordinator);
31565
+ if (inflight.get(coordinator) === pending.callID)
31566
+ inflight.delete(coordinator);
31567
+ return;
31568
+ }
31569
+ if (!pending.childID)
31538
31570
  return;
31539
31571
  const info = await readSession(pending.childID);
31540
- if (!info || info.id !== pending.childID || !info.outcome)
31572
+ if (!info || info.id !== pending.childID || parentOf(info) !== coordinator || !sameWorkspace(root, info.directory))
31541
31573
  return;
31542
- await observeLifecycle(pending.childID, coordinator, "stopped", {
31574
+ directChildren.set(info.id, coordinator);
31575
+ const alreadyStarted = lifecycleBindings.get(info.id);
31576
+ if ((!alreadyStarted || alreadyStarted.parentID !== coordinator || alreadyStarted.taskId !== pending.taskId || alreadyStarted.workerId !== pending.workerId) && !commitDispatchStart(coordinator, info.id, Boolean(info.outcome)))
31577
+ return;
31578
+ const terminal = info.outcome !== undefined || pending.completionReport !== undefined;
31579
+ if (!terminal)
31580
+ return;
31581
+ const stopped = await observeLifecycle(info.id, coordinator, "stopped", {
31543
31582
  taskId: pending.taskId,
31544
31583
  workerId: pending.workerId
31545
- });
31546
- dispatches.delete(coordinator);
31547
- if (inflight.get(coordinator) === pending.callID)
31548
- inflight.delete(coordinator);
31584
+ }, false, info.outcome === "failed" || info.outcome === "interrupted" ? undefined : pending.completionReport);
31585
+ if (stopped) {
31586
+ dispatches.delete(coordinator);
31587
+ if (inflight.get(coordinator) === pending.callID)
31588
+ inflight.delete(coordinator);
31589
+ }
31549
31590
  };
31550
31591
  const coordinatorVeto = (coordinator) => {
31551
31592
  const listed = new TaskStore(root).listTasks();
31552
31593
  if (listed.ok && listed.data.some((task) => task.workers.some((worker) => (worker.data.state === "cancelling" || worker.data.state === "unknown") && worker.provenance.session?.kind === "host" && worker.provenance.session.handle === coordinator)))
31553
31594
  return "recovery_required: a cancelled worker remains uncertain; repeat worker.cancel on the ended worker to confirm its stop, then retry";
31554
- if (unresolvedTaskLaunches.delete(coordinator))
31555
- return "recovery_required: a cancelled worker remains uncertain";
31556
31595
  return null;
31557
31596
  };
31558
31597
  const executeBefore = async (event) => {
@@ -31616,34 +31655,50 @@ var createV2Lifecycle = (deps) => {
31616
31655
  const coordinator = event.sessionID;
31617
31656
  const pending = dispatches.get(coordinator);
31618
31657
  const generation = pending?.callID === event.id ? pending : undefined;
31658
+ let started = false;
31659
+ let terminalObserved = false;
31660
+ let terminalObservationRequired = false;
31661
+ let childSession = null;
31619
31662
  if (event.status === "completed") {
31620
31663
  const metadata = record3(event.result.metadata);
31621
31664
  const child = sessionIDOf(metadata.sessionID ?? metadata.sessionId);
31665
+ const output = contentText(event.result.content);
31666
+ const completedOutput = /<subagent\b[^>]*\bstate="completed"/.test(output);
31622
31667
  if (child) {
31623
- const childSession = await readSession(child);
31624
- if (parentOf(childSession) === coordinator) {
31668
+ if (generation)
31669
+ generation.childID = child;
31670
+ childSession = await readSession(child);
31671
+ if (parentOf(childSession) === coordinator && sameWorkspace(root, childSession?.directory)) {
31625
31672
  directChildren.set(child, coordinator);
31673
+ const terminal = childSession?.outcome !== undefined || completedOutput;
31674
+ terminalObservationRequired = terminal;
31626
31675
  if (generation)
31627
- commitDispatchStart(coordinator, child);
31676
+ started = commitDispatchStart(coordinator, child, terminal);
31628
31677
  }
31629
31678
  }
31630
- const output = contentText(event.result.content);
31631
- const completedChild = child && /<subagent\b[^>]*\bstate="completed"/.test(output) && lifecycleBindings.has(child) ? child : null;
31679
+ const completedChild = child && (childSession?.outcome !== undefined || completedOutput) && lifecycleBindings.has(child) ? child : null;
31632
31680
  if (completedChild) {
31633
31681
  const binding = lifecycleBindings.get(completedChild);
31634
- if (binding)
31635
- await observeLifecycle(completedChild, coordinator, "stopped", binding, false, {
31682
+ if (binding) {
31683
+ const canReportCompletion = completedOutput && childSession?.outcome !== "failed" && childSession?.outcome !== "interrupted";
31684
+ if (canReportCompletion && generation)
31685
+ generation.completionReport = {
31686
+ outcome: "completed",
31687
+ summary: output.trim() || "Native worker completed.",
31688
+ evidenceIds: [],
31689
+ findingIds: []
31690
+ };
31691
+ terminalObserved = canReportCompletion ? await observeLifecycle(completedChild, coordinator, "stopped", binding, false, generation?.completionReport ?? {
31636
31692
  outcome: "completed",
31637
31693
  summary: output.trim() || "Native worker completed.",
31638
31694
  evidenceIds: [],
31639
31695
  findingIds: []
31640
- });
31696
+ }) : await observeLifecycle(completedChild, coordinator, "stopped", binding);
31697
+ }
31641
31698
  }
31642
- } else {
31643
- const message = String(event.error?.message ?? event.error ?? "").toLowerCase();
31644
- if (generation && /(?:cancel|interrupt|unknown|uncertain)/.test(message))
31645
- unresolvedTaskLaunches.add(coordinator);
31646
31699
  }
31700
+ if (generation && (event.status === "error" || !started || terminalObservationRequired && !terminalObserved))
31701
+ return;
31647
31702
  if (generation) {
31648
31703
  dispatches.delete(coordinator);
31649
31704
  if (inflight.get(coordinator) === event.id)
@@ -31804,7 +31859,8 @@ var parseFrontmatter = (markdown) => {
31804
31859
  var registerSkills = async (ctx) => {
31805
31860
  const skillsDir = path27.join(assetsRoot(), "skills");
31806
31861
  if (!existsSync14(skillsDir))
31807
- return;
31862
+ return new Set;
31863
+ const registered = new Set;
31808
31864
  await ctx.skill.transform((editor) => {
31809
31865
  const existing = new Set(editor.list().map((skill) => skill.id));
31810
31866
  for (const id of WORKIT_METHOD_SKILLS) {
@@ -31821,15 +31877,17 @@ var registerSkills = async (ctx) => {
31821
31877
  path: file,
31822
31878
  content: parsed.body
31823
31879
  });
31880
+ registered.add(id);
31824
31881
  }
31825
31882
  });
31883
+ return registered;
31826
31884
  };
31827
- var registerCommands = async (ctx) => {
31885
+ var registerCommands = async (ctx, registeredSkills) => {
31828
31886
  const listed = await ctx.command.list();
31829
31887
  const existing = new Set((listed?.data ?? []).map((command) => command.name));
31830
31888
  await ctx.command.transform((editor) => {
31831
31889
  for (const [alias, skill] of Object.entries(WORKIT_SKILL_ALIASES)) {
31832
- if (existing.has(alias))
31890
+ if (!registeredSkills.has(skill) || existing.has(alias))
31833
31891
  continue;
31834
31892
  editor.add({
31835
31893
  name: alias,
@@ -32061,8 +32119,8 @@ var setup = async (ctx) => {
32061
32119
  await ctx.session.hook("compaction", (event) => {
32062
32120
  injectCompactionContext(root, String(event.sessionID), event.system);
32063
32121
  });
32064
- await registerSkills(ctx);
32065
- await registerCommands(ctx);
32122
+ const registeredSkills = await registerSkills(ctx);
32123
+ await registerCommands(ctx, registeredSkills);
32066
32124
  const subscription = new AbortController;
32067
32125
  (async () => {
32068
32126
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-opencode",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "private": false,
5
5
  "description": "Workit OpenCode plugin — native tools, adaptive policy, and fourteen method skills",
6
6
  "keywords": [
@@ -39,7 +39,7 @@
39
39
  "typecheck": "tsc --noEmit"
40
40
  },
41
41
  "dependencies": {
42
- "@brainervirus/workit-core": "^2.0.0"
42
+ "@brainervirus/workit-core": "^2.0.2"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@opencode-ai/plugin": "1.18.30",