@brainervirus/workit-opencode 0.8.7 → 0.8.9

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
@@ -33,7 +33,7 @@ Requirements: OpenCode ≥ 1.15.0, Node ≥ 22 (the published plugin is a self-c
33
33
 
34
34
  - **Approvals** — flow gates (`workflow_spec_approve` / `workflow_plan_approve` / `workflow_plan_menu`) record native `question` receipts; the self-review validation runs automatically during the transition. Approvals bind to the document's exact SHA-256 digest: editing an approved spec/plan invalidates the approval and forces a fresh reapproval.
35
35
  - **Lifecycle** — `workflow_plan_pause` / `workflow_plan_resume` / `workflow_plan_complete` move a plan through `pending`/`active`/`paused`/`completed`, each gated by a one-use native-question receipt; completion requires a complete SDD ledger and passing repository verification.
36
- - **Delegation** — approved plans execute through subagent-driven task delegation (native `task`); while a subagent-driven plan is active, coordinator product edits are intercepted.
36
+ - **Delegation** — approved plans execute through subagent-driven task delegation (native `task`); while a subagent-driven plan is active, coordinator product edits are intercepted. Receipts are purpose-bound: each gate consumes the newest fresh receipt for exactly its purpose, so unrelated questions never authorize or mask a gate. Delegated authority is direct-child-only — authorized workers receive compact worker-only context, while mismatched session lineage is denied (`delegation_lineage_denied`).
37
37
  - **Commit** — `wk-commit` previews a Conventional Commit and confirms through a native `question`.
38
38
  - **Handoff** — `wk-handoff` seeds and spawns a native OpenCode continuation session; a destination session presents a four-choice menu (never the originating Handoff option) and carries the handoff-destination marker.
39
39
  - **Diagnostics** — durable JSONL journal plus native `client.app.log()`; nothing is mirrored to `process.stderr` or the agent conversation.
@@ -15,4 +15,6 @@ disable-model-invocation: true
15
15
  7. After any `workflow_handoff_session` result—success, partial, or failure—end the originating turn immediately after one status message. Never create todos, execute the plan inline, modify files, retry handoff, or call another tool.
16
16
  8. A destination run that executes the plan must still end with `workflow_plan_complete` (or the CLI `workit flow complete`) once the SDD ledger is complete and repository verification passes, and never finish the run while the plan is still `active`.
17
17
 
18
+ Handoff titles continuation sessions `Workit: <slug>` (never `Continue <slug>`). OpenCode's native `Continue opencode -s <session-id>` epilogue is the valid manual recovery command when host selection is unavailable (`stage: "select"`): `selected: false` with a `sessionID` is a partial success — use `opencode -s <session-id>` to resume, not a Workit bug.
19
+
18
20
  Never emit a continuation prompt, use the clipboard, or ask the user to copy text. If selected, report the session ID only if the current session remains visible. If staying, report the seeded session ID. On failure, report `stage` and `error`; preserve any returned session for the session picker and never recreate it automatically. `todowrite` and `task` are unnecessary here.
@@ -31,13 +31,15 @@ For every plan task whose ID is absent from `completed_task_ids`:
31
31
  3. Use `task` with the built-in `explore` agent for read-only discovery when needed, then a fresh built-in `general` agent to implement from the brief. The parent remains coordinator-only.
32
32
  4. Require product changes to follow TDD: failing check first, minimal implementation, passing focused check.
33
33
  5. Create the review package with `workflow_sdd_review_package` using `confirmed: true`.
34
- 6. Dispatch separate `general` agents for spec-compliance review and code-quality review. **Blocking findings** (Critical, Important, or spec-compliance) may trigger at most **two** fix+re-review rounds per task. **Advisory** findings (Minor, style, YAGNI, taste) never pause the loop — append them to `<SDD_DIR>/advisories.md` with the task id.
34
+ 6. Dispatch separate `general` agents for spec-compliance review and code-quality review. **Blocking findings** (Critical, Important, or spec-compliance) may trigger at most **two** fix+re-review rounds per task. **Advisory** findings (Minor, style, YAGNI, taste) never pause the loop — append them with `workflow_sdd_append_advisory` (`--task <id> --text <text>`) using `confirmed: true`.
35
35
  7. Append the validated ledger line with `workflow_sdd_append_progress` using `confirmed: true`, then mark the task completed with `todowrite`.
36
36
 
37
37
  Each task lands exactly one contiguous non-empty commit range (`base..head`): fix rounds append commits to that range and never rewrite/amend an active review range; each progress line records the task's real base..head shas.
38
38
 
39
39
  Never redispatch completed task IDs. Pass task briefs and review diffs to agents; do not make them reparse the plan. Keep commits on the in-place feature/bugfix branch.
40
40
 
41
+ Delegated authority is direct-child-only: a worker is the session whose host `parentID` exactly equals the activating coordinator's recorded `coordinator_session_id`; missing, mismatched, or multi-owner lineage fails closed with `delegation_lineage_denied`, and nested `opencode` launches are denied during active delegated work. An authorized child receives only the compact worker contract — execute the supplied brief, follow TDD, land one contiguous non-empty commit range, report results — never coordinator guidance, `wk-implement`, or ledger management. Coordinator bookkeeping (briefs, review packages, progress, advisories via `workflow_sdd_*`) stays with the coordinator session.
42
+
41
43
  ## Final gate
42
44
 
43
45
  After all remaining tasks, dispatch a final full-branch code review, run `workflow_verify`, and report exact per-check results. Present the full `<SDD_DIR>/advisories.md` roll-up once, then use native `question` so the user can choose which advisory items to fix, discuss, or discard. Only then may advisory fixes run. Use `workflow_git_context` for the final commit preview and the `wk-commit` skill for any approved commit. If a tracked stash reference exists, preview reapplication with `question`, then call `workflow_branch_setup` with `confirmed: true` only after approval.
@@ -7,12 +7,13 @@ Load `using-superpowers`, `subagent-driven-development`, `test-driven-developmen
7
7
 
8
8
  ## Handoff destination
9
9
 
10
- This session is a handoff destination for a continued plan. The originating session already recorded the post-plan menu choice; present exactly these four choices and never re-offer the originating handoff option:
10
+ This session is a handoff destination for a continued plan. The originating session already recorded the post-plan menu choice; present exactly these four choices plus model deferral and never re-offer the originating handoff option:
11
11
 
12
12
  - Subagent-driven
13
13
  - Inline
14
14
  - Review spec first
15
15
  - Review plan first
16
+ - Change model first
16
17
 
17
18
  <workflow-handoff-destination>true</workflow-handoff-destination>
18
19
 
@@ -24,6 +25,7 @@ This session is a handoff destination for a continued plan. The originating sess
24
25
  - Use native `todowrite` for visible task state as well as the gitignored ledger.
25
26
  - Use native `question` for branch/stash choices and guarded external mutations; call mutation tools only after approval with `confirmed: true` (grounded in the recorded NativeChoiceEvidence).
26
27
  - Flow-tool confirmations are never agent-typed booleans and never caller-supplied evidence objects: on OpenCode the plugin records the user's native-`question` answer as a host-observed one-use receipt (`attested: true`, `callID`, `selectedLabel`, `recordedAt`) consumed by `workflow_spec_approve` / `workflow_plan_approve` / `workflow_plan_menu` — no evidence argument exists, and delegated worker status comes from host session parentage (`parentID`), never a caller `role` field. On Cursor, confirmations are policy-only (`attested: false`) and subagent-driven execution is rejected as unsupported.
28
+ - Delegated authority is direct-child-only: a worker is the session whose host `parentID` exactly equals the activating coordinator's recorded `coordinator_session_id`; missing, mismatched, or multi-owner lineage fails closed with `delegation_lineage_denied`, and nested `opencode` launches are denied during active delegated work. An authorized child receives only the compact worker contract (execute the supplied brief, follow TDD, land one contiguous non-empty commit range, report results) — never coordinator guidance, `wk-implement`, or ledger management; coordinator bookkeeping via `workflow_sdd_*` stays with the coordinator session.
27
29
  - On Cursor, for every repository-scoped `workflow_*` call, pass the active Cursor workspace as `workspace_root`; never rely on the MCP process default.
28
30
  - Use native `task` with only the built-in `explore` and `general` agents.
29
31
 
@@ -52,7 +54,7 @@ For each top-level task absent from `completed_task_ids`:
52
54
  3. Delegate read-only discovery, when needed, to an `explore` agent. Delegate implementation to a fresh `general` agent. Product changes follow TDD.
53
55
  4. Create a working-state diff with `workflow_sdd_review_package` and `confirmed: true`.
54
56
  5. Delegate spec-compliance review and code-quality review to separate `general` agents.
55
- 6. **Blocking** findings (Critical, Important, or spec-compliance) may trigger at most **two** fix+re-review rounds per task. **Advisory** findings (Minor, style, YAGNI, taste) never pause the loop — append them to `<SDD_DIR>/advisories.md`.
57
+ 6. **Blocking** findings (Critical, Important, or spec-compliance) may trigger at most **two** fix+re-review rounds per task. **Advisory** findings (Minor, style, YAGNI, taste) never pause the loop — append them with `workflow_sdd_append_advisory` (`--task <id> --text <text>`, `confirmed: true`) instead of an unrestricted file edit.
56
58
  7. Append the validated ledger entry with `workflow_sdd_append_progress` and `confirmed: true`; mark the todo completed.
57
59
 
58
60
  ## Final gate
@@ -60,10 +60,13 @@ On success, use native `question` / Cursor `AskQuestion` with exactly these opti
60
60
  3. Handoff → load `wk-handoff` (new session only)
61
61
  4. Review spec first
62
62
  5. Review plan first
63
+ 6. Change model first
64
+
65
+ `Change model first` is display-only deferral: it ends the turn without calling `workflow_plan_menu` and re-presents the menu on the next turn. Every other choice must call `workflow_plan_menu` immediately after the answer and before any skill, branch question, mutation, or handoff.
63
66
 
64
67
  Never emit Superpowers text beginning “Two execution options”.
65
68
 
66
- A handoff destination session (the seeded contract carries `<workflow-handoff-destination>true</workflow-handoff-destination>`) presents exactly four choices — Subagent-driven, Inline, Review spec first, Review plan first — and never re-offers the originating handoff option.
69
+ A handoff destination session (the seeded contract carries `<workflow-handoff-destination>true</workflow-handoff-destination>`) presents exactly five choices — Subagent-driven, Inline, Review spec first, Review plan first, Change model first — and never re-offers the originating handoff option.
67
70
 
68
71
  - Specs/plans must follow `templates/spec-template.md` / `templates/plan-template.md` (mandated diagrams, tables, CA-XX).
69
72
 
package/dist/plugin.js CHANGED
@@ -14,7 +14,7 @@ var __export = (target, all) => {
14
14
  };
15
15
 
16
16
  // packages/workit-opencode/src/plugin.ts
17
- import { existsSync as existsSync21, readdirSync as readdirSync11 } from "node:fs";
17
+ import { existsSync as existsSync21, readdirSync as readdirSync12 } from "node:fs";
18
18
  import path39 from "node:path";
19
19
  import { fileURLToPath as fileURLToPath3 } from "node:url";
20
20
 
@@ -357,7 +357,7 @@ ${contract}
357
357
 
358
358
  After saving a plan, call \`workflow_docs_validate\` on the spec/plan pair. On failure, stop — do not offer execution.
359
359
 
360
- On success, use native \`question\` with exactly: Subagent-driven, Inline, Handoff (new session only), Review spec first, Review plan first. Never emit Superpowers “Two execution options” prose. No \`--stay\` in this menu.
360
+ On success, use native \`question\` with exactly: Subagent-driven, Inline, Handoff (new session only), Review spec first, Review plan first, Change model first. Change model first is display-only: ends turn without workflow_plan_menu, re-presents menu next turn; real choices call workflow_plan_menu immediately before any skill/branch/mutation/handoff. Never emit Superpowers “Two execution options” prose. No \`--stay\` in this menu.
361
361
 
362
362
  ## Library documentation
363
363
 
@@ -374,13 +374,15 @@ var SOURCE_MENU_LABELS = [
374
374
  "Inline",
375
375
  "Handoff",
376
376
  "Review spec first",
377
- "Review plan first"
377
+ "Review plan first",
378
+ "Change model first"
378
379
  ];
379
380
  var DESTINATION_MENU_LABELS = [
380
381
  "Subagent-driven",
381
382
  "Inline",
382
383
  "Review spec first",
383
- "Review plan first"
384
+ "Review plan first",
385
+ "Change model first"
384
386
  ];
385
387
  var HANDOFF_DESTINATION_MARKER = "<workflow-handoff-destination>true</workflow-handoff-destination>";
386
388
 
@@ -412,6 +414,10 @@ You referenced a doc with a backtick-only path. Deliver docs with a clickable ma
412
414
  var SDD_REMINDER_TEXT = `<workflow-sdd-reminder>
413
415
  An approved plan is subagent-driven — execute it via \`wk-implement\` / \`task\` delegation. Never implement the approved plan inline in the main session.
414
416
  </workflow-sdd-reminder>`;
417
+ var SDD_WORKER_REMINDER_TEXT = `<workflow-sdd-worker>
418
+ You are an authorized delegated worker for an active subagent-driven plan.
419
+ Execute only the supplied task brief: follow TDD (failing test first), land exactly one contiguous non-empty commit range for your task, then report status, commits, and test results to the coordinator. Do not manage coordinator bookkeeping or launch another agent harness.
420
+ </workflow-sdd-worker>`;
415
421
  var DOC_RENDER_TEXT = `<workflow-doc-render>
416
422
  When delivering a spec or plan, by default render the full markdown content of the doc in chat (headings, tables, mermaid fences preserved) — NOT a backtick-wrapped raw block.
417
423
  If the doc exceeds the render threshold (more than 150 lines, over 8KB, or more than 3 mermaid diagrams), deliver only the clickable link \`[spec.md](docs/<slug>/spec.md)\` + a 3-5 bullet summary.
@@ -445,12 +451,11 @@ var shouldInjectBrainstorm = (currentText) => !currentText.includes(BRAINSTORM_T
445
451
  var shouldInjectDebug = (currentText) => !currentText.includes(DEBUG_TEXT);
446
452
  var shouldInjectReviewReception = (currentText) => !currentText.includes(REVIEW_RECEPTION_TEXT);
447
453
  var shouldInjectDocRender = (currentText) => !currentText.includes(DOC_RENDER_TEXT);
448
- var shouldInjectSddReminder = (currentText) => !currentText.includes(SDD_REMINDER_TEXT);
449
454
  var shouldInjectConfigGuard = (currentText) => !currentText.includes(CONFIG_GUARD_TEXT);
450
455
  var shouldInjectIssueRail = (currentText) => !currentText.includes(ISSUE_RAIL_TEXT);
451
456
 
452
457
  // packages/workit-core/src/core/detector.ts
453
- import { existsSync as existsSync16, readdirSync as readdirSync6, readFileSync as readFileSync17 } from "node:fs";
458
+ import { existsSync as existsSync16, readdirSync as readdirSync7, readFileSync as readFileSync17 } from "node:fs";
454
459
  import path23 from "node:path";
455
460
 
456
461
  // packages/workit-core/src/core/init.ts
@@ -1596,7 +1601,11 @@ var checkLauncher = (res) => {
1596
1601
  detail: "no dev checkout found (WORKFLOW_TOOLKIT_DEV) — skipping non-Cursor launcher checks"
1597
1602
  };
1598
1603
  }
1599
- return { id: "launcher", status: "pass", detail: `${res.host} launcher/hook entries present` };
1604
+ return {
1605
+ id: "launcher",
1606
+ status: "pass",
1607
+ detail: `${res.host} launcher/hook entries present`
1608
+ };
1600
1609
  };
1601
1610
  var checkUtility = (res) => {
1602
1611
  const git = commandOnPath("git", res.env);
@@ -1616,7 +1625,11 @@ var checkUtility = (res) => {
1616
1625
  fix: "Install util-linux (flock)"
1617
1626
  };
1618
1627
  }
1619
- return { id: "utility", status: "pass", detail: "git (and flock where required) on PATH" };
1628
+ return {
1629
+ id: "utility",
1630
+ status: "pass",
1631
+ detail: "git (and flock where required) on PATH"
1632
+ };
1620
1633
  };
1621
1634
  var staleEntry = (entry) => {
1622
1635
  if (!entry)
@@ -1638,7 +1651,11 @@ var checkStalePin = (res) => {
1638
1651
  };
1639
1652
  }
1640
1653
  if (!existsSync7(res.opencodeConfig)) {
1641
- return { id: "stale_pin", status: "pass", detail: "no opencode config — not registered" };
1654
+ return {
1655
+ id: "stale_pin",
1656
+ status: "pass",
1657
+ detail: "no opencode config — not registered"
1658
+ };
1642
1659
  }
1643
1660
  const cfg = readJson(res.opencodeConfig);
1644
1661
  const entries = pluginEntries(cfg);
@@ -1669,6 +1686,118 @@ var checkStalePin = (res) => {
1669
1686
  }
1670
1687
  return { id: "stale_pin", status: "pass", detail: "opencode pin resolves" };
1671
1688
  };
1689
+ var pluginMcpSelectors = (res) => {
1690
+ const p = path10.join(res.cursorPluginDir, "mcp.json");
1691
+ if (!existsSync7(p))
1692
+ return null;
1693
+ const cfg = readJson(p);
1694
+ const server = cfg?.mcpServers?.workit;
1695
+ if (!server || typeof server !== "object" || Array.isArray(server))
1696
+ return null;
1697
+ const args = server.args;
1698
+ if (!Array.isArray(args))
1699
+ return null;
1700
+ return args.map(String).filter((a) => a.startsWith("--package="));
1701
+ };
1702
+ var registryLatestVersion = (res) => {
1703
+ const seam = res.env.WORKIT_DOCTOR_STALE_REGISTRY_VERSION;
1704
+ if (typeof seam === "string" && seam) {
1705
+ return /^\d+(?:\.\d+){1,2}$/.test(seam) ? seam : null;
1706
+ }
1707
+ const cmd = res.env.WORKIT_DOCTOR_STALE_REGISTRY_CMD ?? (commandOnPath("npm", res.env) ? "npm" : null);
1708
+ if (!cmd)
1709
+ return null;
1710
+ try {
1711
+ const r = spawnSync2(cmd, ["view", CURSOR_RUNTIME_PACKAGE, "version"], {
1712
+ encoding: "utf8",
1713
+ timeout: 20000,
1714
+ env: res.env
1715
+ });
1716
+ const v = (r.stdout ?? "").trim().split(/\s+/)[0];
1717
+ return r.status === 0 && /^\d+(?:\.\d+){1,2}$/.test(v) ? v : null;
1718
+ } catch {
1719
+ return null;
1720
+ }
1721
+ };
1722
+ var installedPluginVersion = (res) => {
1723
+ const pkg = readJson(path10.join(res.cursorPluginDir, "package.json"));
1724
+ return typeof pkg?.version === "string" && pkg.version ? pkg.version : null;
1725
+ };
1726
+ var checkStaleInstall = (res) => {
1727
+ if (res.host !== "cursor" && res.host !== "cli") {
1728
+ return {
1729
+ id: "stale_install",
1730
+ status: "pass",
1731
+ detail: "cursor plugin not inspected on the opencode host"
1732
+ };
1733
+ }
1734
+ const canonicalMcp = cursorMcpServerEntry("").args.find((a) => a.startsWith("--package="));
1735
+ const mcpSelectors = pluginMcpSelectors(res);
1736
+ const legacyPin = (mcpSelectors ?? []).find((s) => s !== canonicalMcp && s.includes("@brainervirus/workit-cursor@"));
1737
+ if (legacyPin) {
1738
+ return {
1739
+ id: "stale_install",
1740
+ status: "fail",
1741
+ detail: `stale_install: plugin mcp.json pins a legacy selector ${legacyPin} (canonical: ${canonicalMcp})`,
1742
+ fix: "Re-run install-cursor-plugin.sh — it rewrites the workit MCP entry to the canonical @latest selector"
1743
+ };
1744
+ }
1745
+ const hooksFile = path10.join(res.cursorPluginDir, "hooks", "hooks-cursor.json");
1746
+ const hookCmd = readJson(hooksFile)?.hooks?.sessionStart?.[0]?.command ?? null;
1747
+ const canonicalHook = canonicalCursorHook;
1748
+ const staleHook = hookCmd !== null && hookCmd !== canonicalHook && !hookCmd.startsWith("node ") && hookCmd.includes("@brainervirus/workit-cursor@");
1749
+ if (staleHook) {
1750
+ return {
1751
+ id: "stale_install",
1752
+ status: "fail",
1753
+ detail: `stale_install: sessionStart hook runs a legacy selector (canonical: ${canonicalHook})`,
1754
+ fix: "Re-run install-cursor-plugin.sh — it rewrites the sessionStart hook to the canonical @latest selector"
1755
+ };
1756
+ }
1757
+ const installed = installedPluginVersion(res);
1758
+ const source = res.dev ? readJson(path10.join(res.dev, "packages/workit-cursor/package.json"))?.version ?? null : readJson(path10.join(packageRoot(), "package.json"))?.version ?? null;
1759
+ const localDist = mcpSelectors === null && hookCmd !== null && hookCmd.startsWith("node ");
1760
+ if (localDist && installed !== null && source !== null && !semverAtLeast(installed, source)) {
1761
+ return {
1762
+ id: "stale_install",
1763
+ status: "fail",
1764
+ detail: `stale_install: installed workit-cursor ${installed} is behind the current runtime ${source}`,
1765
+ fix: "Re-run install-cursor-plugin.sh — it refreshes the plugin directory and rewrites the workit MCP/hook entries"
1766
+ };
1767
+ }
1768
+ if (installed !== null && localDist) {
1769
+ const expected = registryLatestVersion(res);
1770
+ if (expected === null) {
1771
+ return {
1772
+ id: "registry_unreachable",
1773
+ status: "warn",
1774
+ registryProbed: true,
1775
+ detail: "registry_unreachable: cannot compare installed workit-cursor against the published runtime",
1776
+ fix: "Retry when the npm registry is reachable, or re-run install-cursor-plugin.sh to refresh the install"
1777
+ };
1778
+ }
1779
+ if (!semverAtLeast(installed, expected)) {
1780
+ return {
1781
+ id: "stale_install",
1782
+ status: "fail",
1783
+ registryProbed: true,
1784
+ detail: `stale_install: local-dist workit-cursor ${installed} is behind the published runtime ${expected}`,
1785
+ fix: "Re-run install-cursor-plugin.sh — it refreshes the plugin directory with the current build"
1786
+ };
1787
+ }
1788
+ return {
1789
+ id: "stale_install",
1790
+ status: "pass",
1791
+ registryProbed: true,
1792
+ detail: `installed local-dist workit-cursor ${installed} matches the published runtime ${expected}`
1793
+ };
1794
+ }
1795
+ return {
1796
+ id: "stale_install",
1797
+ status: "pass",
1798
+ detail: installed === null ? "installed workit-cursor selectors are canonical" : `installed workit-cursor ${installed} is metadata on the canonical @latest install (fresh at launch)`
1799
+ };
1800
+ };
1672
1801
  var checkDuplicateRegistration = (res) => {
1673
1802
  const problems = [];
1674
1803
  const opencodeHost = res.host !== "cursor";
@@ -1704,7 +1833,11 @@ var checkDuplicateRegistration = (res) => {
1704
1833
  }
1705
1834
  }
1706
1835
  if (problems.length === 0) {
1707
- return { id: "duplicate_registration", status: "pass", detail: "no duplicate registrations" };
1836
+ return {
1837
+ id: "duplicate_registration",
1838
+ status: "pass",
1839
+ detail: "no duplicate registrations"
1840
+ };
1708
1841
  }
1709
1842
  return {
1710
1843
  id: "duplicate_registration",
@@ -1730,7 +1863,11 @@ var checkMalformedConfig = (res) => {
1730
1863
  files.push(res.cursorMcp);
1731
1864
  const bad = files.filter((p) => !parsesAsConfigObject(p));
1732
1865
  if (bad.length === 0)
1733
- return { id: "malformed_config", status: "pass", detail: "config files parse" };
1866
+ return {
1867
+ id: "malformed_config",
1868
+ status: "pass",
1869
+ detail: "config files parse"
1870
+ };
1734
1871
  return {
1735
1872
  id: "malformed_config",
1736
1873
  status: "fail",
@@ -1741,10 +1878,18 @@ var checkMalformedConfig = (res) => {
1741
1878
  var checkWorkspaceMismatch = (res) => {
1742
1879
  const file = path10.join(res.configDir, "workspaces.json");
1743
1880
  if (!existsSync7(file))
1744
- return { id: "workspace_mismatch", status: "pass", detail: "no workspaces configured" };
1881
+ return {
1882
+ id: "workspace_mismatch",
1883
+ status: "pass",
1884
+ detail: "no workspaces configured"
1885
+ };
1745
1886
  const ws = readJson(file);
1746
1887
  if (!Array.isArray(ws?.workspaces)) {
1747
- return { id: "workspace_mismatch", status: "pass", detail: "no workspaces configured" };
1888
+ return {
1889
+ id: "workspace_mismatch",
1890
+ status: "pass",
1891
+ detail: "no workspaces configured"
1892
+ };
1748
1893
  }
1749
1894
  const match = resolveWorkspaceFrom(res.cwd, res.configDir);
1750
1895
  if (match)
@@ -1786,7 +1931,11 @@ var checkCredentialMetadata = (res) => {
1786
1931
  }
1787
1932
  }
1788
1933
  if (tokenPaths.length === 0) {
1789
- return { id: "credential_metadata", status: "pass", detail: "no credentials configured" };
1934
+ return {
1935
+ id: "credential_metadata",
1936
+ status: "pass",
1937
+ detail: "no credentials configured"
1938
+ };
1790
1939
  }
1791
1940
  const problems = [];
1792
1941
  for (const raw of tokenPaths) {
@@ -1824,7 +1973,11 @@ var checkLogWritable = (res) => {
1824
1973
  mkdirSync3(logsDir, { recursive: true, mode: 448 });
1825
1974
  writeFileSync6(probe, `{"probe":true}
1826
1975
  `, { mode: 384 });
1827
- return { id: "log_writable", status: "pass", detail: "log directory writable" };
1976
+ return {
1977
+ id: "log_writable",
1978
+ status: "pass",
1979
+ detail: "log directory writable"
1980
+ };
1828
1981
  } catch (err) {
1829
1982
  return {
1830
1983
  id: "log_writable",
@@ -1845,6 +1998,7 @@ var RUN_CHECKS = [
1845
1998
  checkLauncher,
1846
1999
  checkUtility,
1847
2000
  checkStalePin,
2001
+ checkStaleInstall,
1848
2002
  checkDuplicateRegistration,
1849
2003
  checkMalformedConfig,
1850
2004
  checkWorkspaceMismatch,
@@ -1857,21 +2011,27 @@ var INSTALLER_REQUIRED = new Set([
1857
2011
  "launcher",
1858
2012
  "utility",
1859
2013
  "stale_pin",
2014
+ "stale_install",
1860
2015
  "duplicate_registration",
1861
2016
  "malformed_config"
1862
2017
  ]);
1863
2018
  var runDoctor = (options = {}) => {
1864
2019
  const res = resolve(options);
1865
2020
  const raw = RUN_CHECKS.map((fn) => fn(res));
1866
- const checks = res.installer ? raw.map((c) => c.status === "fail" && !INSTALLER_REQUIRED.has(c.id) ? { ...c, status: "warn", detail: `${c.detail} (not enforced by installer)` } : c) : raw;
2021
+ const checks = res.installer ? raw.map((c) => c.status === "fail" && !INSTALLER_REQUIRED.has(c.id) ? {
2022
+ ...c,
2023
+ status: "warn",
2024
+ detail: `${c.detail} (not enforced by installer)`
2025
+ } : c) : raw;
1867
2026
  const failed = checks.filter((c) => c.status === "fail").length;
1868
2027
  const warned = checks.filter((c) => c.status === "warn").length;
1869
2028
  const passed = checks.filter((c) => c.status === "pass").length;
1870
2029
  const exitCode = failed > 0 ? 1 : 0;
2030
+ const offline = !raw.some((c) => ("registryProbed" in c) && c.registryProbed);
1871
2031
  const report = {
1872
2032
  ok: failed === 0,
1873
2033
  exitCode,
1874
- offline: true,
2034
+ offline,
1875
2035
  host: res.host,
1876
2036
  checked_at: new Date().toISOString(),
1877
2037
  summary: { passed, warned, failed, total: checks.length },
@@ -1881,7 +2041,7 @@ var runDoctor = (options = {}) => {
1881
2041
  getDiagnosticLogger()?.info(EVENT.doctor, {
1882
2042
  host: res.host,
1883
2043
  exit_code: exitCode,
1884
- offline: true,
2044
+ offline,
1885
2045
  failed: checks.filter((c) => c.status === "fail").map((c) => c.id),
1886
2046
  total: checks.length
1887
2047
  });
@@ -3713,6 +3873,7 @@ import {
3713
3873
  fsyncSync,
3714
3874
  mkdirSync as mkdirSync9,
3715
3875
  openSync,
3876
+ readdirSync as readdirSync6,
3716
3877
  readFileSync as readFileSync16,
3717
3878
  renameSync as renameSync2,
3718
3879
  rmSync as rmSync2,
@@ -4427,6 +4588,58 @@ function sddAppendProgress({
4427
4588
  const rel = posix2(path19.relative(contained.base, path_));
4428
4589
  return { ok: true, line: trimmed, progress_path: rel };
4429
4590
  }
4591
+ function sddAppendAdvisory({
4592
+ advisories_path,
4593
+ task_id,
4594
+ text,
4595
+ workspace_root
4596
+ }) {
4597
+ if (typeof task_id !== "number" || !Number.isSafeInteger(task_id) || task_id <= 0) {
4598
+ return { error: "task_id must be a positive safe integer", code: "advisory_task_invalid" };
4599
+ }
4600
+ if (typeof text !== "string") {
4601
+ return {
4602
+ error: "advisory text must be a string of 1-1000 characters after normalization",
4603
+ code: "advisory_text_invalid"
4604
+ };
4605
+ }
4606
+ if (text.includes("\r") || text.includes(`
4607
+ `)) {
4608
+ return {
4609
+ error: "advisory text must be a single line (no CR/LF)",
4610
+ code: "advisory_text_invalid"
4611
+ };
4612
+ }
4613
+ const collapsed = text.trim().replace(/[ \t]+/g, " ");
4614
+ if (collapsed.length === 0 || collapsed.length > 1000) {
4615
+ return {
4616
+ error: "advisory text must be 1-1000 characters after trim and horizontal-space collapse",
4617
+ code: "advisory_text_invalid"
4618
+ };
4619
+ }
4620
+ if (!/^docs\/[^/]+\/sdd\/advisories\.md$/.test(advisories_path)) {
4621
+ return {
4622
+ error: `advisories_path must be docs/<slug>/sdd/advisories.md: ${advisories_path}`,
4623
+ code: "advisory_path_invalid"
4624
+ };
4625
+ }
4626
+ const contained = resolveDocsPath({ workspace_root, path: advisories_path });
4627
+ if (!contained.ok)
4628
+ return { error: contained.error, code: "advisory_path_invalid" };
4629
+ const abs = contained.path;
4630
+ if (existsSync12(abs) && statSync5(abs).isDirectory()) {
4631
+ return {
4632
+ error: `advisory target is a directory: ${advisories_path}`,
4633
+ code: "advisory_target_invalid"
4634
+ };
4635
+ }
4636
+ mkdirSync8(path19.dirname(abs), { recursive: true });
4637
+ const line = `- Task ${task_id}: ${collapsed}
4638
+ `;
4639
+ appendFileSync(abs, line, "utf8");
4640
+ const rel = posix2(path19.relative(contained.base, abs));
4641
+ return { ok: true, advisory: collapsed, advisories_path: rel };
4642
+ }
4430
4643
 
4431
4644
  // packages/workit-core/src/core/verify-project.ts
4432
4645
  import { spawnSync as spawnSync5 } from "node:child_process";
@@ -5038,7 +5251,7 @@ root: ${cwd}
5038
5251
  }
5039
5252
 
5040
5253
  // packages/workit-core/src/core/flow-state.ts
5041
- var COORDINATOR_RECOVERY_TEXT = "A subagent-driven plan is active: coordinator product edits are blocked. " + "Delegate product mutations (task briefs, progress, review packages) to an " + "authenticated delegated worker via `task` / `wk-implement` instead of " + "editing in the coordinator session.";
5254
+ var COORDINATOR_RECOVERY_TEXT = "A subagent-driven plan is active: coordinator product edits are blocked. " + "Delegate product mutations to an authenticated delegated worker via `task` / `wk-implement` instead of " + "editing in the coordinator session.";
5042
5255
  var CURSOR_SUBAGENT_UNSUPPORTED_TEXT = "Cursor cannot execute subagent-driven plans: the MCP has no child-session " + "support. Choose Inline, Handoff, or a review option in this session, or " + "run the plan in OpenCode with `wk-implement`.";
5043
5256
  var MENU_CHOICES = [
5044
5257
  "subagent-driven",
@@ -5098,7 +5311,8 @@ var normalizeState = (parsed, slug) => {
5098
5311
  execution: {
5099
5312
  status: execution.status ?? "pending",
5100
5313
  mode: execution.mode ?? null,
5101
- evidence: execution.evidence ?? null
5314
+ evidence: execution.evidence ?? null,
5315
+ coordinator_session_id: execution.coordinator_session_id ?? null
5102
5316
  },
5103
5317
  handoff_destination: p.handoff_destination ?? false,
5104
5318
  updated_at: p.updated_at ?? Date.now()
@@ -5110,7 +5324,7 @@ var emptyState = (slug) => ({
5110
5324
  spec: { path: "", status: "draft", evidence: null, approved_digest: null },
5111
5325
  plan: { path: "", status: "draft", evidence: null, approved_digest: null },
5112
5326
  menu: { presented: false, chosen: "", evidence: null },
5113
- execution: { status: "pending", mode: null, evidence: null },
5327
+ execution: { status: "pending", mode: null, evidence: null, coordinator_session_id: null },
5114
5328
  handoff_destination: false,
5115
5329
  updated_at: Date.now()
5116
5330
  });
@@ -5226,6 +5440,12 @@ var validateState = (parsed, slug) => {
5226
5440
  if (execRaw?.evidence !== undefined && !validateEvidenceValue(execRaw.evidence, true)) {
5227
5441
  return { ok: false, error: "flow state execution.evidence has an unsupported shape" };
5228
5442
  }
5443
+ if (execRaw?.coordinator_session_id !== undefined && execRaw.coordinator_session_id !== null && typeof execRaw.coordinator_session_id !== "string") {
5444
+ return {
5445
+ ok: false,
5446
+ error: "flow state execution.coordinator_session_id must be a string or null"
5447
+ };
5448
+ }
5229
5449
  return {
5230
5450
  ok: true,
5231
5451
  state: {
@@ -5241,7 +5461,8 @@ var validateState = (parsed, slug) => {
5241
5461
  execution: {
5242
5462
  status: execRaw?.status ?? "pending",
5243
5463
  mode: execRaw?.mode ?? null,
5244
- evidence: execRaw?.evidence ?? null
5464
+ evidence: execRaw?.evidence ?? null,
5465
+ coordinator_session_id: execRaw?.coordinator_session_id ?? null
5245
5466
  },
5246
5467
  handoff_destination: parsed.handoff_destination ?? false,
5247
5468
  updated_at: parsed.updated_at ?? Date.now()
@@ -5378,7 +5599,7 @@ var resetForSpecDrift = (state) => ({
5378
5599
  spec: { ...state.spec, status: "draft", evidence: null, approved_digest: null },
5379
5600
  plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
5380
5601
  menu: { presented: false, chosen: "", evidence: null },
5381
- execution: { status: "pending", mode: null, evidence: null },
5602
+ execution: { status: "pending", mode: null, evidence: null, coordinator_session_id: null },
5382
5603
  handoff_destination: false,
5383
5604
  updated_at: Date.now()
5384
5605
  });
@@ -5421,9 +5642,14 @@ var reconcileState = (root2, slug, state) => {
5421
5642
  var deriveLegacyExecution = (root2, slug, state) => {
5422
5643
  const ledger = ledgerCompletion(root2, slug);
5423
5644
  if (state.plan.status === "approved" && state.menu.chosen === "subagent-driven" && ledger.started && !ledger.complete) {
5424
- return { status: "active", mode: "subagent-driven", evidence: null };
5645
+ return {
5646
+ status: "active",
5647
+ mode: "subagent-driven",
5648
+ evidence: null,
5649
+ coordinator_session_id: null
5650
+ };
5425
5651
  }
5426
- return { status: "pending", mode: null, evidence: null };
5652
+ return { status: "pending", mode: null, evidence: null, coordinator_session_id: null };
5427
5653
  };
5428
5654
  var normalizeCompatibility = (root2, slug, parsed, state) => {
5429
5655
  if (!isRecord(parsed) || !("execution" in parsed)) {
@@ -5432,6 +5658,11 @@ var normalizeCompatibility = (root2, slug, parsed, state) => {
5432
5658
  if (derived.status !== current.status || derived.mode !== current.mode) {
5433
5659
  return { state: { ...state, execution: derived, updated_at: Date.now() }, changed: true };
5434
5660
  }
5661
+ return { state, changed: false };
5662
+ }
5663
+ const execRaw = isRecord(parsed.execution) ? parsed.execution : undefined;
5664
+ if (execRaw && !("coordinator_session_id" in execRaw)) {
5665
+ return { state: { ...state, updated_at: Date.now() }, changed: true };
5435
5666
  }
5436
5667
  return { state, changed: false };
5437
5668
  };
@@ -5566,15 +5797,6 @@ var assertMutationWorkspace = (root2, ctx) => {
5566
5797
  }
5567
5798
  return { ok: true };
5568
5799
  };
5569
- var assertCoordinatorBoundary = (ctx, state) => {
5570
- if (ctx?.role === "coordinator" && state.execution.status === "active" && state.execution.mode === "subagent-driven") {
5571
- return err2("coordinator_blocked", COORDINATOR_RECOVERY_TEXT);
5572
- }
5573
- if (ctx?.role === "delegated" && !ctx.taskIdentity) {
5574
- return err2("delegated_unauthenticated", "delegated mutations require an authenticated task identity (taskIdentity) — re-run inside the delegated worker session");
5575
- }
5576
- return { ok: true };
5577
- };
5578
5800
  var MAX_CLOCK_SKEW_MS = 60000;
5579
5801
  var MAX_RECEIPTS_PER_SESSION = 10;
5580
5802
  var RECEIPT_FRESHNESS_MS = 10 * 60 * 1000;
@@ -5603,19 +5825,49 @@ var isNegativeLabel = (label) => {
5603
5825
  return firstWord.replace(/[^a-z]/g, "") === entry;
5604
5826
  });
5605
5827
  };
5828
+ var receiptPurposeForLabel = (label) => {
5829
+ const n = normalizeLabel(label);
5830
+ if (n === "approve spec" || n === "approve spec recommended")
5831
+ return "spec-approval";
5832
+ if (n === "approve plan" || n === "approve plan recommended")
5833
+ return "plan-approval";
5834
+ if (n === "approve")
5835
+ return;
5836
+ if (n === "pause plan")
5837
+ return "plan-pause";
5838
+ if (n === "resume plan")
5839
+ return "plan-resume";
5840
+ if (n === "complete plan")
5841
+ return "plan-complete";
5842
+ const exec = new Set([
5843
+ "subagent driven",
5844
+ "inline",
5845
+ "handoff",
5846
+ "review spec",
5847
+ "review plan",
5848
+ "change model"
5849
+ ]);
5850
+ if (exec.has(n))
5851
+ return "execution-menu";
5852
+ return;
5853
+ };
5606
5854
 
5607
5855
  class HostReceiptStore {
5608
5856
  #bySession = new Map;
5609
- record(sessionId, callID, selectedLabel, recordedAt = Date.now(), question) {
5610
- const label = selectedLabel.trim();
5611
- if (!label)
5857
+ record(sessionId, callID, selectedLabel, recordedAt = Date.now(), question = "", purpose) {
5858
+ const trimmed = selectedLabel.trim();
5859
+ if (!trimmed)
5612
5860
  return;
5613
5861
  if (recordedAt > Date.now() + MAX_CLOCK_SKEW_MS)
5614
5862
  return;
5863
+ const derived = purpose ?? receiptPurposeForLabel(selectedLabel);
5864
+ if (derived === undefined)
5865
+ return;
5866
+ const q = question ?? "";
5615
5867
  const queue = this.#bySession.get(sessionId) ?? [];
5616
5868
  if (queue.length >= MAX_RECEIPTS_PER_SESSION)
5617
5869
  queue.shift();
5618
- queue.push({ sessionId, callID, selectedLabel: label, recordedAt, question });
5870
+ queue.push({ sessionId, callID, selectedLabel, recordedAt, question: q, purpose: derived });
5619
5871
  this.#bySession.set(sessionId, queue);
5620
5872
  }
5621
5873
  count(sessionId) {
@@ -5632,10 +5884,40 @@ class HostReceiptStore {
5632
5884
  if (!queue || queue.length === 0) {
5633
5885
  return err2("receipt_missing", "no host-observed native-question receipt for this session — ask the native " + "`question` tool and have the user answer before calling this tool");
5634
5886
  }
5635
- const index = queue.length - 1;
5887
+ let index = -1;
5888
+ if (opts.purpose !== undefined) {
5889
+ const top = queue[queue.length - 1];
5890
+ if (top && top.purpose === opts.purpose && isNegativeLabel(top.selectedLabel)) {
5891
+ const filtered = queue.filter((r) => r.purpose !== opts.purpose);
5892
+ if (filtered.length === 0)
5893
+ this.#bySession.delete(sessionId);
5894
+ else
5895
+ this.#bySession.set(sessionId, filtered);
5896
+ return err2("receipt_rejected", `the user's most recent answer (${JSON.stringify(top.selectedLabel)}) is a ` + "negative answer — it cannot authorize an approval; ask the native question again");
5897
+ }
5898
+ for (let i = queue.length - 1;i >= 0; i--) {
5899
+ if (queue[i].purpose === opts.purpose) {
5900
+ index = i;
5901
+ break;
5902
+ }
5903
+ }
5904
+ if (index === -1) {
5905
+ return err2("receipt_missing", `no host-observed receipt for purpose ${JSON.stringify(opts.purpose)} — ask the native question for that purpose`);
5906
+ }
5907
+ } else {
5908
+ index = queue.length - 1;
5909
+ }
5636
5910
  const receipt = queue[index];
5637
5911
  if (isNegativeLabel(receipt.selectedLabel)) {
5638
- this.#bySession.delete(sessionId);
5912
+ if (opts.purpose !== undefined) {
5913
+ const filtered = queue.filter((r) => r.purpose !== opts.purpose);
5914
+ if (filtered.length === 0)
5915
+ this.#bySession.delete(sessionId);
5916
+ else
5917
+ this.#bySession.set(sessionId, filtered);
5918
+ } else {
5919
+ this.#bySession.delete(sessionId);
5920
+ }
5639
5921
  return err2("receipt_rejected", `the user's most recent answer (${JSON.stringify(receipt.selectedLabel)}) is a ` + "negative answer — it cannot authorize an approval; ask the native question again");
5640
5922
  }
5641
5923
  if (opts.label !== undefined && !sameChoiceLabel(receipt.selectedLabel, opts.label)) {
@@ -5769,7 +6051,7 @@ var prepareFlowState = (root2, slug, opts = {}, ctx) => {
5769
6051
  spec: { path: specPath, status: "draft", evidence: null, approved_digest: null },
5770
6052
  plan: { path: planPath, status: "draft", evidence: null, approved_digest: null },
5771
6053
  menu: { presented: false, chosen: "", evidence: null },
5772
- execution: { status: "pending", mode: null, evidence: null },
6054
+ execution: { status: "pending", mode: null, evidence: null, coordinator_session_id: null },
5773
6055
  handoff_destination: false,
5774
6056
  updated_at: Date.now()
5775
6057
  });
@@ -5915,13 +6197,24 @@ var recordMenuChoice = (root2, slug, planPath, choice, evidence, ctx) => {
5915
6197
  return err2("recursive_handoff", "this flow is already a handoff destination — a second handoff is rejected");
5916
6198
  }
5917
6199
  const executing = choice === "subagent-driven" || choice === "inline";
6200
+ const coordinatorSessionId = choice === "subagent-driven" && recorded.evidence.host === "opencode" ? ctx?.sessionId ?? null : null;
5918
6201
  return {
5919
6202
  ok: true,
5920
6203
  next: {
5921
6204
  ...state,
5922
6205
  plan: { ...state.plan, path: state.plan.path || `docs/${slug}/plan.md` },
5923
6206
  menu: { presented: true, chosen: choice, evidence: recorded.evidence },
5924
- execution: executing ? { status: "active", mode: choice, evidence: recorded.evidence } : { status: "pending", mode: null, evidence: recorded.evidence },
6207
+ execution: executing ? {
6208
+ status: "active",
6209
+ mode: choice,
6210
+ evidence: recorded.evidence,
6211
+ coordinator_session_id: coordinatorSessionId
6212
+ } : {
6213
+ status: "pending",
6214
+ mode: null,
6215
+ evidence: recorded.evidence,
6216
+ coordinator_session_id: null
6217
+ },
5925
6218
  updated_at: Date.now()
5926
6219
  }
5927
6220
  };
@@ -6015,7 +6308,7 @@ var completeExecution = (root2, slug, deps) => {
6015
6308
  }
6016
6309
  const next = {
6017
6310
  ...reconciled.state,
6018
- execution: { ...exec, status: "completed" },
6311
+ execution: { ...exec, status: "completed", coordinator_session_id: null },
6019
6312
  handoff_destination: false,
6020
6313
  updated_at: Date.now()
6021
6314
  };
@@ -6079,7 +6372,7 @@ var slugFromSddPath = (p) => {
6079
6372
  const match = p.split(path22.sep).join("/").match(/^docs\/([^/]+)\/sdd(\/|$|['"])/);
6080
6373
  return match?.[1] ?? "";
6081
6374
  };
6082
- var assertFlowGates = (root2, planPath, opts = {}) => {
6375
+ var assertHandoffReady = (root2, planPath) => {
6083
6376
  const doc = resolveDoc(root2, "", planPath, "plan");
6084
6377
  if (!doc.ok)
6085
6378
  return err2("path_invalid", doc.error);
@@ -6094,12 +6387,15 @@ var assertFlowGates = (root2, planPath, opts = {}) => {
6094
6387
  if (state.plan.status !== "approved") {
6095
6388
  return err2("plan_not_approved", `plan not approved (status: ${state.plan.status}). Run workflow_plan_approve after the user's approval.`);
6096
6389
  }
6097
- if (opts.requireMenu && !state.menu.presented) {
6098
- return err2("menu_not_presented", "post-plan menu not presented. Ask the native question menu (Subagent-driven/Inline/Handoff/Review spec/Review plan) and record the answer with workflow_plan_menu.");
6390
+ if (state.handoff_destination) {
6391
+ return err2("recursive_handoff", "this flow is already a handoff destination a second handoff is rejected");
6392
+ }
6393
+ if (!state.menu.presented || state.menu.chosen !== "handoff") {
6394
+ return err2("handoff_not_chosen", `handoff requires the execution menu choice "handoff" (chosen: ${JSON.stringify(state.menu.chosen)}, presented: ${state.menu.presented})`);
6099
6395
  }
6100
6396
  return { ok: true };
6101
6397
  };
6102
- var assertProductGates = (root2, slug, opts = {}, ctx) => {
6398
+ var assertSddControlGates = (root2, slug, opts = {}, ctx) => {
6103
6399
  const bound = assertMutationWorkspace(root2, ctx);
6104
6400
  if (!bound.ok)
6105
6401
  return bound;
@@ -6125,9 +6421,12 @@ var assertProductGates = (root2, slug, opts = {}, ctx) => {
6125
6421
  if (validated.ok === false)
6126
6422
  return err2("docs_invalid", validated.error);
6127
6423
  }
6128
- return assertCoordinatorBoundary(ctx, state);
6424
+ if (state.execution.status === "active" && state.execution.mode === "subagent-driven" && roleFromParentage(ctx?.parentSessionId, state.execution.coordinator_session_id) === "delegated") {
6425
+ return err2("sdd_control_denied", "SDD control metadata is coordinator-owned while a subagent-driven plan is active — delegated workers cannot mutate task briefs, review packages, progress, or advisories");
6426
+ }
6427
+ return { ok: true };
6129
6428
  };
6130
- var roleFromParentage = (parentID) => parentID ? "delegated" : "coordinator";
6429
+ var roleFromParentage = (parentID, coordinatorSessionId) => typeof parentID === "string" && parentID !== "" && parentID === coordinatorSessionId ? "delegated" : "coordinator";
6131
6430
  var COORDINATOR_WRITE_TOOLS = [
6132
6431
  "write",
6133
6432
  "edit",
@@ -6152,9 +6451,6 @@ var COORDINATOR_WRITE_TOOLS = [
6152
6451
  "workflow_docs_promote",
6153
6452
  "workflow_docs_layout",
6154
6453
  "workflow_docs_repo_link",
6155
- "workflow_sdd_task_brief",
6156
- "workflow_sdd_review_package",
6157
- "workflow_sdd_append_progress",
6158
6454
  "workflow_youtrack_post",
6159
6455
  "workflow_youtrack_log_time"
6160
6456
  ];
@@ -6459,10 +6755,34 @@ var isCoordinatorBashAllowed = (command) => {
6459
6755
  };
6460
6756
  var COORDINATOR_SHELL_DENIED_TEXT = "Coordinator shell commands are restricted while a subagent-driven plan is " + "active: only bounded read/test/review commands are allowed (the exact " + "allowlist is in flow-state.ts, isCoordinatorBashAllowed). " + COORDINATOR_RECOVERY_TEXT;
6461
6757
  var subagentDrivenInterception = (input) => {
6462
- if (input.parentID)
6758
+ const ids = Array.from(new Set((input.activeCoordinatorIds ?? []).filter((id) => typeof id === "string" && id !== "")));
6759
+ const parent = typeof input.parentID === "string" && input.parentID !== "" ? input.parentID : null;
6760
+ const legacyActive = input.active === true;
6761
+ if (!legacyActive && ids.length === 0)
6463
6762
  return { ok: true };
6464
- if (!input.active)
6763
+ if (parent !== null && ids.length === 1 && ids[0] === parent) {
6764
+ if (input.tool === "bash") {
6765
+ const tokens = (input.command ?? "").split(/[\s'"]+/).filter(Boolean);
6766
+ const launchesOpencode = tokens.some((t) => t.split("/").pop() === "opencode");
6767
+ if (launchesOpencode) {
6768
+ return err2("delegation_lineage_denied", "nested opencode launch is denied while a subagent-driven plan is active");
6769
+ }
6770
+ }
6771
+ if ([
6772
+ "workflow_sdd_task_brief",
6773
+ "workflow_sdd_review_package",
6774
+ "workflow_sdd_append_progress",
6775
+ "workflow_sdd_append_advisory"
6776
+ ].includes(input.tool)) {
6777
+ return err2("delegation_lineage_denied", "SDD control metadata is coordinator-owned — workers execute briefs, not bookkeeping");
6778
+ }
6465
6779
  return { ok: true };
6780
+ }
6781
+ if (parent !== null) {
6782
+ if (!legacyActive || ids.length > 0) {
6783
+ return err2("delegation_lineage_denied", "delegated writes require an exact direct-parent match to the activating coordinator");
6784
+ }
6785
+ }
6466
6786
  if (COORDINATOR_WRITE_TOOLS.includes(input.tool)) {
6467
6787
  return err2("coordinator_write_denied", COORDINATOR_RECOVERY_TEXT);
6468
6788
  }
@@ -6473,6 +6793,24 @@ var subagentDrivenInterception = (input) => {
6473
6793
  }
6474
6794
  return { ok: true };
6475
6795
  };
6796
+ var findActiveSubagentDrivenContexts = (root2) => {
6797
+ let entries = [];
6798
+ try {
6799
+ entries = readdirSync6(path22.join(root2, "docs"), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
6800
+ } catch {
6801
+ return [];
6802
+ }
6803
+ const out = [];
6804
+ for (const slug of entries) {
6805
+ try {
6806
+ const state = readFlowState(root2, slug);
6807
+ if (state.execution.status === "active" && state.execution.mode === "subagent-driven") {
6808
+ out.push({ slug, coordinator_session_id: state.execution.coordinator_session_id });
6809
+ }
6810
+ } catch {}
6811
+ }
6812
+ return out;
6813
+ };
6476
6814
 
6477
6815
  // packages/workit-core/src/core/detector.ts
6478
6816
  var detectConfigGapError = (text) => text.includes(CONFIG_GAP_MARKER);
@@ -6593,7 +6931,7 @@ var scanActiveSubagentDrivenPlans = (root2) => {
6593
6931
  if (exec.status === "active" && exec.mode === "subagent-driven")
6594
6932
  slugs.push(slug);
6595
6933
  };
6596
- for (const slug of readdirSync6(docsDir)) {
6934
+ for (const slug of readdirSync7(docsDir)) {
6597
6935
  const file = path23.join(docsDir, slug, "sdd", "flow.json");
6598
6936
  if (!existsSync16(file))
6599
6937
  continue;
@@ -19808,15 +20146,21 @@ var opencodeMutationContext = async (context2, client) => {
19808
20146
  const session = await client?.session.get({ path: { id: context2.sessionID } });
19809
20147
  parentID = session?.data?.parentID;
19810
20148
  } catch {}
19811
- const role = roleFromParentage(parentID);
20149
+ const role = roleFromParentage(parentID, undefined);
19812
20150
  return {
19813
20151
  hostWorkspace: context2.directory,
19814
20152
  role,
19815
20153
  sessionId: context2.sessionID,
20154
+ parentSessionId: parentID,
19816
20155
  taskIdentity: role === "delegated" ? context2.sessionID : undefined
19817
20156
  };
19818
20157
  };
19819
20158
  function createFlowTools(receipts, client) {
20159
+ const purposeForLifecycle = {
20160
+ "Pause plan": "plan-pause",
20161
+ "Resume plan": "plan-resume",
20162
+ "Complete plan": "plan-complete"
20163
+ };
19820
20164
  const lifecycleTool = (action, label, description) => tool({
19821
20165
  description,
19822
20166
  args: {
@@ -19827,7 +20171,10 @@ function createFlowTools(receipts, client) {
19827
20171
  if ("error" in slugged)
19828
20172
  return output2(fail(slugged.error));
19829
20173
  const slug = slugged.slug;
19830
- const consumed = receipts.consume(context2.sessionID, { label });
20174
+ const consumed = receipts.consume(context2.sessionID, {
20175
+ purpose: purposeForLifecycle[label],
20176
+ label
20177
+ });
19831
20178
  if (!consumed.ok)
19832
20179
  return output2(fail(consumed.error, { code: consumed.code }));
19833
20180
  const result = transitionExecution(context2.directory, slug, plan_path, action, createOpenCodeEvidence(consumed.receipt), await opencodeMutationContext(context2, client));
@@ -19897,7 +20244,7 @@ function createFlowTools(receipts, client) {
19897
20244
  if ("error" in slugged)
19898
20245
  return output2(fail(slugged.error));
19899
20246
  const slug = slugged.slug;
19900
- const consumed = receipts.consume(context2.sessionID);
20247
+ const consumed = receipts.consume(context2.sessionID, { purpose: "spec-approval" });
19901
20248
  if (!consumed.ok)
19902
20249
  return output2(fail(consumed.error, { code: consumed.code }));
19903
20250
  const result = transitionSpec(context2.directory, slug, spec_path, createOpenCodeEvidence(consumed.receipt), await opencodeMutationContext(context2, client));
@@ -19916,7 +20263,7 @@ function createFlowTools(receipts, client) {
19916
20263
  if ("error" in slugged)
19917
20264
  return output2(fail(slugged.error));
19918
20265
  const slug = slugged.slug;
19919
- const consumed = receipts.consume(context2.sessionID);
20266
+ const consumed = receipts.consume(context2.sessionID, { purpose: "plan-approval" });
19920
20267
  if (!consumed.ok)
19921
20268
  return output2(fail(consumed.error, { code: consumed.code }));
19922
20269
  const result = transitionPlan(context2.directory, slug, plan_path, createOpenCodeEvidence(consumed.receipt), await opencodeMutationContext(context2, client));
@@ -19942,7 +20289,10 @@ function createFlowTools(receipts, client) {
19942
20289
  if ("error" in slugged)
19943
20290
  return output2(fail(slugged.error));
19944
20291
  const slug = slugged.slug;
19945
- const consumed = receipts.consume(context2.sessionID, { label: choice });
20292
+ const consumed = receipts.consume(context2.sessionID, {
20293
+ purpose: "execution-menu",
20294
+ label: choice
20295
+ });
19946
20296
  if (!consumed.ok)
19947
20297
  return output2(fail(consumed.error, { code: consumed.code }));
19948
20298
  const result = recordMenuChoice(context2.directory, slug, plan_path, choice, createOpenCodeEvidence(consumed.receipt), await opencodeMutationContext(context2, client));
@@ -19961,11 +20311,11 @@ function createFlowTools(receipts, client) {
19961
20311
  // packages/workit-opencode/src/tools/sdd.ts
19962
20312
  var output3 = (value) => JSON.stringify(value, null, 2);
19963
20313
  var requireConfirmed2 = (confirmed) => confirmed === true ? null : output3(fail("confirmed: true required"));
19964
- var gateProductWrite = async (root2, sddPath, context2, client) => {
20314
+ var gateSddControl = async (root2, sddPath, context2, client) => {
19965
20315
  const slug = slugFromSddPath(sddPath);
19966
20316
  if (!slug)
19967
20317
  return output3(fail("could not derive slug — expected docs/<slug>/sdd/..."));
19968
- const gate = assertProductGates(root2, slug, { requireMenu: true, requireDocs: true }, await opencodeMutationContext(context2, client));
20318
+ const gate = assertSddControlGates(root2, slug, { requireMenu: true, requireDocs: true }, await opencodeMutationContext(context2, client));
19969
20319
  if (!gate.ok)
19970
20320
  return output3(fail(gate.error, { code: gate.code }));
19971
20321
  return null;
@@ -20112,7 +20462,7 @@ function createSddTools(state, client) {
20112
20462
  const rejected = requireConfirmed2(confirmed);
20113
20463
  if (rejected)
20114
20464
  return rejected;
20115
- const gated = await gateProductWrite(context2.directory, sdd_dir, context2, client);
20465
+ const gated = await gateSddControl(context2.directory, sdd_dir, context2, client);
20116
20466
  if (gated)
20117
20467
  return gated;
20118
20468
  return invoke(() => {
@@ -20145,7 +20495,7 @@ function createSddTools(state, client) {
20145
20495
  } catch (error45) {
20146
20496
  return output3(fail(error45 instanceof Error ? error45.message : "workflow operation failed"));
20147
20497
  }
20148
- const gated = await gateProductWrite(context2.directory, sdd_dir, context2, client);
20498
+ const gated = await gateSddControl(context2.directory, sdd_dir, context2, client);
20149
20499
  if (gated)
20150
20500
  return gated;
20151
20501
  return invoke(() => sddReviewPackage({
@@ -20167,7 +20517,7 @@ function createSddTools(state, client) {
20167
20517
  const rejected = requireConfirmed2(confirmed);
20168
20518
  if (rejected)
20169
20519
  return rejected;
20170
- const gated = await gateProductWrite(context2.directory, progress_path, context2, client);
20520
+ const gated = await gateSddControl(context2.directory, progress_path, context2, client);
20171
20521
  if (gated)
20172
20522
  return gated;
20173
20523
  return invoke(() => {
@@ -20175,6 +20525,32 @@ function createSddTools(state, client) {
20175
20525
  return sddAppendProgress({ progress_path, line, workspace_root: context2.directory });
20176
20526
  });
20177
20527
  }
20528
+ }),
20529
+ workflow_sdd_append_advisory: tool({
20530
+ description: "Append a validated advisory line to docs/<slug>/sdd/advisories.md (coordinator-owned).",
20531
+ args: {
20532
+ confirmed: tool.schema.boolean(),
20533
+ advisories_path: tool.schema.string(),
20534
+ task_id: tool.schema.number(),
20535
+ text: tool.schema.string()
20536
+ },
20537
+ execute: async ({ confirmed, advisories_path, task_id, text }, context2) => {
20538
+ const rejected = requireConfirmed2(confirmed);
20539
+ if (rejected)
20540
+ return rejected;
20541
+ const gated = await gateSddControl(context2.directory, advisories_path, context2, client);
20542
+ if (gated)
20543
+ return gated;
20544
+ return invoke(() => {
20545
+ relativePath(context2.directory, advisories_path);
20546
+ return sddAppendAdvisory({
20547
+ advisories_path,
20548
+ task_id,
20549
+ text,
20550
+ workspace_root: context2.directory
20551
+ });
20552
+ });
20553
+ }
20178
20554
  })
20179
20555
  };
20180
20556
  }
@@ -20186,12 +20562,12 @@ import path32 from "node:path";
20186
20562
  import path31 from "node:path";
20187
20563
 
20188
20564
  // packages/workit-core/src/core/handoff-context.ts
20189
- import { existsSync as existsSync18, readFileSync as readFileSync20, readdirSync as readdirSync7, statSync as statSync8 } from "node:fs";
20565
+ import { existsSync as existsSync18, readFileSync as readFileSync20, readdirSync as readdirSync8, statSync as statSync8 } from "node:fs";
20190
20566
  import path30 from "node:path";
20191
20567
  var DOC_RE = /docs\/([A-Za-z0-9][A-Za-z0-9._-]*)\/(spec|plan)\.md/g;
20192
20568
  var listMd = (dir) => {
20193
20569
  try {
20194
- return readdirSync7(dir).filter((f) => f.endsWith(".md")).sort();
20570
+ return readdirSync8(dir).filter((f) => f.endsWith(".md")).sort();
20195
20571
  } catch {
20196
20572
  return [];
20197
20573
  }
@@ -20217,7 +20593,7 @@ var resolveActivePair = (root2) => {
20217
20593
  let best = null;
20218
20594
  let entries = [];
20219
20595
  try {
20220
- entries = readdirSync7(docsDir);
20596
+ entries = readdirSync8(docsDir);
20221
20597
  } catch {
20222
20598
  return { error: "no pair" };
20223
20599
  }
@@ -20394,21 +20770,13 @@ function createHandoffTools(client, state) {
20394
20770
  const active = built;
20395
20771
  const slug = slugFromPath(active.plan);
20396
20772
  try {
20397
- const gate = assertFlowGates(context2.directory, active.plan);
20398
- if (!gate.ok)
20399
- return output4(fail(gate.error));
20400
- const preflight = readEffectiveFlowState(context2.directory, slug);
20401
- if (!preflight.ok)
20402
- return output4(fail(preflight.error, { code: preflight.code }));
20403
- if (preflight.state.handoff_destination) {
20404
- return output4(fail("this flow is already a handoff destination — a second handoff is rejected", {
20405
- code: "recursive_handoff"
20406
- }));
20407
- }
20773
+ const ready = assertHandoffReady(context2.directory, active.plan);
20774
+ if (!ready.ok)
20775
+ return output4(fail(ready.error, { code: ready.code }));
20408
20776
  state.set(context2.sessionID, { spec: active.spec, plan: active.plan, sdd: active.sdd });
20409
20777
  return output4(await handoffSession(client, {
20410
20778
  directory: context2.directory,
20411
- title: `Continue ${path32.basename(path32.dirname(active.plan))}`,
20779
+ title: `Workit: ${path32.basename(path32.dirname(active.plan))}`,
20412
20780
  prompt: active.prompt,
20413
20781
  stay: /(?:^|\s)--stay(?:\s|$)/.test(userMessage),
20414
20782
  afterSeed: () => idempotentMarkDestination(context2.directory, slug, active.plan)
@@ -20903,7 +21271,7 @@ function createPresentTools() {
20903
21271
  }
20904
21272
 
20905
21273
  // packages/workit-core/src/core/docs-repo.ts
20906
- import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync22, writeFileSync as writeFileSync12, readdirSync as readdirSync8 } from "node:fs";
21274
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync22, writeFileSync as writeFileSync12, readdirSync as readdirSync9 } from "node:fs";
20907
21275
  import { execFileSync as execFileSync5 } from "node:child_process";
20908
21276
  import path35 from "node:path";
20909
21277
  var configPath2 = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path35.join(configDir(), "docs-repo.json");
@@ -20949,7 +21317,7 @@ var listSpecs = (workspaceRoot) => {
20949
21317
  const specs = [];
20950
21318
  const docsDir = path35.join(workspaceRoot, "docs");
20951
21319
  if (existsSync19(docsDir)) {
20952
- for (const slug of readdirSync8(docsDir)) {
21320
+ for (const slug of readdirSync9(docsDir)) {
20953
21321
  if (slug.startsWith("."))
20954
21322
  continue;
20955
21323
  const spec = path35.posix.join("docs", slug, "spec.md");
@@ -20960,7 +21328,7 @@ var listSpecs = (workspaceRoot) => {
20960
21328
  if (repoPath) {
20961
21329
  const featuresDir = path35.join(repoPath, "features");
20962
21330
  if (existsSync19(featuresDir)) {
20963
- const match = readdirSync8(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d));
21331
+ const match = readdirSync9(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d));
20964
21332
  if (match) {
20965
21333
  promoted = true;
20966
21334
  target = path35.join(repoPath, "features", match);
@@ -21052,7 +21420,7 @@ var promoteSpec = (workspaceRoot, slug, opts) => {
21052
21420
  }
21053
21421
  const prefix = monthPrefix();
21054
21422
  const featuresDir = path35.join(repoPath, "features");
21055
- const existing = existsSync19(featuresDir) ? readdirSync8(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d)) : undefined;
21423
+ const existing = existsSync19(featuresDir) ? readdirSync9(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d)) : undefined;
21056
21424
  const targetDir = path35.join(featuresDir, existing ?? `${prefix}-${slug}`);
21057
21425
  mkdirSync10(targetDir, { recursive: true });
21058
21426
  const files = ["spec.md"];
@@ -21114,7 +21482,7 @@ import {
21114
21482
  existsSync as existsSync20,
21115
21483
  lstatSync as lstatSync2,
21116
21484
  mkdirSync as mkdirSync11,
21117
- readdirSync as readdirSync9,
21485
+ readdirSync as readdirSync10,
21118
21486
  readFileSync as readFileSync23,
21119
21487
  realpathSync as realpathSync5,
21120
21488
  renameSync as renameSync3,
@@ -21169,7 +21537,7 @@ var detectLegacyDocs = (workspace_root) => {
21169
21537
  const root2 = legacyRoot(workspace_root);
21170
21538
  const raw = [];
21171
21539
  if (existsSync20(root2)) {
21172
- for (const entry of readdirSync9(root2, { withFileTypes: true })) {
21540
+ for (const entry of readdirSync10(root2, { withFileTypes: true })) {
21173
21541
  const abs = path36.join(root2, entry.name);
21174
21542
  const rel = posix3(path36.relative(workspace_root, abs));
21175
21543
  if (entry.isDirectory()) {
@@ -21311,7 +21679,7 @@ var planTree = (workspace, legacyAbs, legacyRel, destRelRoot, destAbsRoot, legac
21311
21679
  if (visits.has(real))
21312
21680
  return { ok: true };
21313
21681
  visits.add(real);
21314
- for (const entry of readdirSync9(real, { withFileTypes: true })) {
21682
+ for (const entry of readdirSync10(real, { withFileTypes: true })) {
21315
21683
  const fromAbs = path36.join(real, entry.name);
21316
21684
  const fromRel = posix3(path36.join(legacyRel, entry.name));
21317
21685
  const toRel = posix3(path36.join(destRelRoot, entry.name));
@@ -21682,7 +22050,7 @@ SDD: ${value.sdd}` : null;
21682
22050
  }
21683
22051
 
21684
22052
  // packages/workit-core/src/core/logger.ts
21685
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync12, readdirSync as readdirSync10, unlinkSync as unlinkSync3 } from "node:fs";
22053
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync12, readdirSync as readdirSync11, unlinkSync as unlinkSync3 } from "node:fs";
21686
22054
  import os4 from "node:os";
21687
22055
  import path37 from "node:path";
21688
22056
  var REDACTED = "[REDACTED]";
@@ -21817,7 +22185,7 @@ var dailyFileName = (date5) => {
21817
22185
  };
21818
22186
  var pruneOldFiles = (dir) => {
21819
22187
  try {
21820
- const files = readdirSync10(dir).filter((name) => DAY_FILE.test(name)).sort().reverse();
22188
+ const files = readdirSync11(dir).filter((name) => DAY_FILE.test(name)).sort().reverse();
21821
22189
  for (const file2 of files.slice(RETAINED_DAYS)) {
21822
22190
  try {
21823
22191
  unlinkSync3(path37.join(dir, file2));
@@ -21984,7 +22352,7 @@ var hasMarkedDestination = (root3) => {
21984
22352
  return false;
21985
22353
  let slugs;
21986
22354
  try {
21987
- slugs = readdirSync11(docsDir);
22355
+ slugs = readdirSync12(docsDir);
21988
22356
  } catch {
21989
22357
  return false;
21990
22358
  }
@@ -22016,6 +22384,32 @@ var questionAnswerLabel = (result) => {
22016
22384
  const label = first[0];
22017
22385
  return typeof label === "string" ? label : undefined;
22018
22386
  };
22387
+ var classifyQuestion = (input, label) => {
22388
+ const args = input;
22389
+ const qs = args?.questions;
22390
+ if (Array.isArray(qs)) {
22391
+ if (qs.length !== 1)
22392
+ return null;
22393
+ const q = qs[0];
22394
+ if (!q || typeof q !== "object")
22395
+ return null;
22396
+ if (q.multiple === true)
22397
+ return null;
22398
+ const opts = q.options;
22399
+ if (Array.isArray(opts) && opts.length === 0 && q.custom === true)
22400
+ return null;
22401
+ if (!Array.isArray(opts) || opts.length === 0)
22402
+ return null;
22403
+ const purpose2 = receiptPurposeForLabel(label);
22404
+ if (purpose2 === undefined)
22405
+ return null;
22406
+ return { purpose: purpose2, questionText: typeof q.question === "string" ? q.question : undefined };
22407
+ }
22408
+ const purpose = receiptPurposeForLabel(label);
22409
+ if (purpose !== undefined)
22410
+ return { purpose, questionText: undefined };
22411
+ return null;
22412
+ };
22019
22413
  var plugin = async ({ client, directory }) => {
22020
22414
  openCodeClient = client;
22021
22415
  installUncaughtHandlers();
@@ -22025,6 +22419,17 @@ var plugin = async ({ client, directory }) => {
22025
22419
  setDiagnosticLogger(logger);
22026
22420
  const state = new WorkflowStateStore;
22027
22421
  const receipts = new HostReceiptStore;
22422
+ const authorizedWorkerFor = async (sessionID) => {
22423
+ const ids = findActiveSubagentDrivenContexts(directory).map((c) => c.coordinator_session_id).filter((id) => typeof id === "string" && id !== "");
22424
+ if (ids.length !== 1)
22425
+ return false;
22426
+ try {
22427
+ const session = await client.session.get({ path: { id: sessionID } });
22428
+ return session?.data?.parentID === ids[0];
22429
+ } catch {
22430
+ return false;
22431
+ }
22432
+ };
22028
22433
  return {
22029
22434
  tool: createTools(adaptPluginHandoffClient(client), state, client, receipts),
22030
22435
  "tool.execute.after": async (input, output10) => {
@@ -22033,9 +22438,11 @@ var plugin = async ({ client, directory }) => {
22033
22438
  const label = questionAnswerLabel(output10);
22034
22439
  if (label === undefined)
22035
22440
  return;
22036
- const args = input.args;
22037
- const questionText = args?.question ?? args?.title;
22038
- receipts.record(input.sessionID, input.callID, label, Date.now(), questionText);
22441
+ const classified = classifyQuestion(input.args, label);
22442
+ if (!classified)
22443
+ return;
22444
+ const questionText = classified.questionText ?? input.args?.question ?? input.args?.title;
22445
+ receipts.record(input.sessionID, input.callID, label, Date.now(), questionText, classified.purpose);
22039
22446
  },
22040
22447
  "tool.execute.before": async (input, output10) => {
22041
22448
  if (input.tool !== "bash" && !COORDINATOR_WRITE_TOOLS.includes(input.tool))
@@ -22047,12 +22454,12 @@ var plugin = async ({ client, directory }) => {
22047
22454
  parentID = session?.data?.parentID;
22048
22455
  sessionDirectory = session?.data?.directory;
22049
22456
  } catch {}
22050
- const active = findActiveSubagentDrivenPlans(sessionDirectory ?? directory).length > 0;
22457
+ const activeCoordinatorIds = findActiveSubagentDrivenContexts(sessionDirectory ?? directory).map((c) => c.coordinator_session_id).filter((id) => typeof id === "string" && id !== "");
22051
22458
  const decision = subagentDrivenInterception({
22052
22459
  tool: input.tool,
22053
22460
  command: output10.args?.command,
22054
22461
  parentID,
22055
- active
22462
+ activeCoordinatorIds
22056
22463
  });
22057
22464
  if (!decision.ok)
22058
22465
  throw new Error(decision.error);
@@ -22105,7 +22512,8 @@ var plugin = async ({ client, directory }) => {
22105
22512
  const firstAnchor = firstUser.parts.find((part) => part.type === "text") ?? firstUser.parts[0];
22106
22513
  const firstText = firstUser.parts.filter((part) => part.type === "text").map((part) => part.text ?? "").join(`
22107
22514
  `);
22108
- const bootstrap = getWorkitBootstrap();
22515
+ const workerAuthorized = await authorizedWorkerFor(firstAnchor.sessionID);
22516
+ const bootstrap = workerAuthorized ? null : getWorkitBootstrap();
22109
22517
  if (bootstrap && !firstText.includes("<workit-contract>")) {
22110
22518
  firstUser.parts.unshift({
22111
22519
  id: firstAnchor.id,
@@ -22114,6 +22522,14 @@ var plugin = async ({ client, directory }) => {
22114
22522
  type: "text",
22115
22523
  text: bootstrap
22116
22524
  });
22525
+ } else if (workerAuthorized && !firstText.includes("workflow-sdd-worker")) {
22526
+ firstUser.parts.unshift({
22527
+ id: firstAnchor.id,
22528
+ sessionID: firstAnchor.sessionID,
22529
+ messageID: firstAnchor.messageID,
22530
+ type: "text",
22531
+ text: SDD_WORKER_REMINDER_TEXT
22532
+ });
22117
22533
  }
22118
22534
  }
22119
22535
  const anchor = currentUser.parts.find((part) => part.type === "text") ?? currentUser.parts[0];
@@ -22148,8 +22564,12 @@ var plugin = async ({ client, directory }) => {
22148
22564
  }
22149
22565
  }
22150
22566
  const activePlans = findActiveSubagentDrivenPlans(directory);
22151
- if (activePlans.length > 0 && shouldInjectSddReminder(currentText)) {
22152
- currentUser.parts.unshift(makePart(SDD_REMINDER_TEXT, "sdd"));
22567
+ if (activePlans.length > 0) {
22568
+ const workerAuthorized = await authorizedWorkerFor(anchor.sessionID);
22569
+ const sddText = workerAuthorized ? SDD_WORKER_REMINDER_TEXT : SDD_REMINDER_TEXT;
22570
+ if (!currentText.includes(sddText)) {
22571
+ currentUser.parts.unshift(makePart(sddText, "sdd"));
22572
+ }
22153
22573
  }
22154
22574
  if (detectConfigGapError(assistantText) && shouldInjectConfigGuard(currentText)) {
22155
22575
  currentUser.parts.unshift(makePart(CONFIG_GUARD_TEXT, "cg"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-opencode",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "private": false,
5
5
  "description": "Workit OpenCode plugin (thin wrapper over @brainervirus/workit-core)",
6
6
  "keywords": [
@@ -34,7 +34,7 @@
34
34
  "typecheck": "tsc --noEmit"
35
35
  },
36
36
  "dependencies": {
37
- "@brainervirus/workit-core": "^0.8.7"
37
+ "@brainervirus/workit-core": "^0.8.9"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@opencode-ai/plugin": "1.17.7"