@brainervirus/workit-opencode 0.8.8 → 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
@@ -3868,6 +3873,7 @@ import {
3868
3873
  fsyncSync,
3869
3874
  mkdirSync as mkdirSync9,
3870
3875
  openSync,
3876
+ readdirSync as readdirSync6,
3871
3877
  readFileSync as readFileSync16,
3872
3878
  renameSync as renameSync2,
3873
3879
  rmSync as rmSync2,
@@ -4582,6 +4588,58 @@ function sddAppendProgress({
4582
4588
  const rel = posix2(path19.relative(contained.base, path_));
4583
4589
  return { ok: true, line: trimmed, progress_path: rel };
4584
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
+ }
4585
4643
 
4586
4644
  // packages/workit-core/src/core/verify-project.ts
4587
4645
  import { spawnSync as spawnSync5 } from "node:child_process";
@@ -5193,7 +5251,7 @@ root: ${cwd}
5193
5251
  }
5194
5252
 
5195
5253
  // packages/workit-core/src/core/flow-state.ts
5196
- 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.";
5197
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`.";
5198
5256
  var MENU_CHOICES = [
5199
5257
  "subagent-driven",
@@ -5253,7 +5311,8 @@ var normalizeState = (parsed, slug) => {
5253
5311
  execution: {
5254
5312
  status: execution.status ?? "pending",
5255
5313
  mode: execution.mode ?? null,
5256
- evidence: execution.evidence ?? null
5314
+ evidence: execution.evidence ?? null,
5315
+ coordinator_session_id: execution.coordinator_session_id ?? null
5257
5316
  },
5258
5317
  handoff_destination: p.handoff_destination ?? false,
5259
5318
  updated_at: p.updated_at ?? Date.now()
@@ -5265,7 +5324,7 @@ var emptyState = (slug) => ({
5265
5324
  spec: { path: "", status: "draft", evidence: null, approved_digest: null },
5266
5325
  plan: { path: "", status: "draft", evidence: null, approved_digest: null },
5267
5326
  menu: { presented: false, chosen: "", evidence: null },
5268
- execution: { status: "pending", mode: null, evidence: null },
5327
+ execution: { status: "pending", mode: null, evidence: null, coordinator_session_id: null },
5269
5328
  handoff_destination: false,
5270
5329
  updated_at: Date.now()
5271
5330
  });
@@ -5381,6 +5440,12 @@ var validateState = (parsed, slug) => {
5381
5440
  if (execRaw?.evidence !== undefined && !validateEvidenceValue(execRaw.evidence, true)) {
5382
5441
  return { ok: false, error: "flow state execution.evidence has an unsupported shape" };
5383
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
+ }
5384
5449
  return {
5385
5450
  ok: true,
5386
5451
  state: {
@@ -5396,7 +5461,8 @@ var validateState = (parsed, slug) => {
5396
5461
  execution: {
5397
5462
  status: execRaw?.status ?? "pending",
5398
5463
  mode: execRaw?.mode ?? null,
5399
- evidence: execRaw?.evidence ?? null
5464
+ evidence: execRaw?.evidence ?? null,
5465
+ coordinator_session_id: execRaw?.coordinator_session_id ?? null
5400
5466
  },
5401
5467
  handoff_destination: parsed.handoff_destination ?? false,
5402
5468
  updated_at: parsed.updated_at ?? Date.now()
@@ -5533,7 +5599,7 @@ var resetForSpecDrift = (state) => ({
5533
5599
  spec: { ...state.spec, status: "draft", evidence: null, approved_digest: null },
5534
5600
  plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
5535
5601
  menu: { presented: false, chosen: "", evidence: null },
5536
- execution: { status: "pending", mode: null, evidence: null },
5602
+ execution: { status: "pending", mode: null, evidence: null, coordinator_session_id: null },
5537
5603
  handoff_destination: false,
5538
5604
  updated_at: Date.now()
5539
5605
  });
@@ -5576,9 +5642,14 @@ var reconcileState = (root2, slug, state) => {
5576
5642
  var deriveLegacyExecution = (root2, slug, state) => {
5577
5643
  const ledger = ledgerCompletion(root2, slug);
5578
5644
  if (state.plan.status === "approved" && state.menu.chosen === "subagent-driven" && ledger.started && !ledger.complete) {
5579
- 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
+ };
5580
5651
  }
5581
- return { status: "pending", mode: null, evidence: null };
5652
+ return { status: "pending", mode: null, evidence: null, coordinator_session_id: null };
5582
5653
  };
5583
5654
  var normalizeCompatibility = (root2, slug, parsed, state) => {
5584
5655
  if (!isRecord(parsed) || !("execution" in parsed)) {
@@ -5587,6 +5658,11 @@ var normalizeCompatibility = (root2, slug, parsed, state) => {
5587
5658
  if (derived.status !== current.status || derived.mode !== current.mode) {
5588
5659
  return { state: { ...state, execution: derived, updated_at: Date.now() }, changed: true };
5589
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 };
5590
5666
  }
5591
5667
  return { state, changed: false };
5592
5668
  };
@@ -5721,15 +5797,6 @@ var assertMutationWorkspace = (root2, ctx) => {
5721
5797
  }
5722
5798
  return { ok: true };
5723
5799
  };
5724
- var assertCoordinatorBoundary = (ctx, state) => {
5725
- if (ctx?.role === "coordinator" && state.execution.status === "active" && state.execution.mode === "subagent-driven") {
5726
- return err2("coordinator_blocked", COORDINATOR_RECOVERY_TEXT);
5727
- }
5728
- if (ctx?.role === "delegated" && !ctx.taskIdentity) {
5729
- return err2("delegated_unauthenticated", "delegated mutations require an authenticated task identity (taskIdentity) — re-run inside the delegated worker session");
5730
- }
5731
- return { ok: true };
5732
- };
5733
5800
  var MAX_CLOCK_SKEW_MS = 60000;
5734
5801
  var MAX_RECEIPTS_PER_SESSION = 10;
5735
5802
  var RECEIPT_FRESHNESS_MS = 10 * 60 * 1000;
@@ -5758,19 +5825,49 @@ var isNegativeLabel = (label) => {
5758
5825
  return firstWord.replace(/[^a-z]/g, "") === entry;
5759
5826
  });
5760
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
+ };
5761
5854
 
5762
5855
  class HostReceiptStore {
5763
5856
  #bySession = new Map;
5764
- record(sessionId, callID, selectedLabel, recordedAt = Date.now(), question) {
5765
- const label = selectedLabel.trim();
5766
- if (!label)
5857
+ record(sessionId, callID, selectedLabel, recordedAt = Date.now(), question = "", purpose) {
5858
+ const trimmed = selectedLabel.trim();
5859
+ if (!trimmed)
5767
5860
  return;
5768
5861
  if (recordedAt > Date.now() + MAX_CLOCK_SKEW_MS)
5769
5862
  return;
5863
+ const derived = purpose ?? receiptPurposeForLabel(selectedLabel);
5864
+ if (derived === undefined)
5865
+ return;
5866
+ const q = question ?? "";
5770
5867
  const queue = this.#bySession.get(sessionId) ?? [];
5771
5868
  if (queue.length >= MAX_RECEIPTS_PER_SESSION)
5772
5869
  queue.shift();
5773
- queue.push({ sessionId, callID, selectedLabel: label, recordedAt, question });
5870
+ queue.push({ sessionId, callID, selectedLabel, recordedAt, question: q, purpose: derived });
5774
5871
  this.#bySession.set(sessionId, queue);
5775
5872
  }
5776
5873
  count(sessionId) {
@@ -5787,10 +5884,40 @@ class HostReceiptStore {
5787
5884
  if (!queue || queue.length === 0) {
5788
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");
5789
5886
  }
5790
- 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
+ }
5791
5910
  const receipt = queue[index];
5792
5911
  if (isNegativeLabel(receipt.selectedLabel)) {
5793
- 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
+ }
5794
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");
5795
5922
  }
5796
5923
  if (opts.label !== undefined && !sameChoiceLabel(receipt.selectedLabel, opts.label)) {
@@ -5924,7 +6051,7 @@ var prepareFlowState = (root2, slug, opts = {}, ctx) => {
5924
6051
  spec: { path: specPath, status: "draft", evidence: null, approved_digest: null },
5925
6052
  plan: { path: planPath, status: "draft", evidence: null, approved_digest: null },
5926
6053
  menu: { presented: false, chosen: "", evidence: null },
5927
- execution: { status: "pending", mode: null, evidence: null },
6054
+ execution: { status: "pending", mode: null, evidence: null, coordinator_session_id: null },
5928
6055
  handoff_destination: false,
5929
6056
  updated_at: Date.now()
5930
6057
  });
@@ -6070,13 +6197,24 @@ var recordMenuChoice = (root2, slug, planPath, choice, evidence, ctx) => {
6070
6197
  return err2("recursive_handoff", "this flow is already a handoff destination — a second handoff is rejected");
6071
6198
  }
6072
6199
  const executing = choice === "subagent-driven" || choice === "inline";
6200
+ const coordinatorSessionId = choice === "subagent-driven" && recorded.evidence.host === "opencode" ? ctx?.sessionId ?? null : null;
6073
6201
  return {
6074
6202
  ok: true,
6075
6203
  next: {
6076
6204
  ...state,
6077
6205
  plan: { ...state.plan, path: state.plan.path || `docs/${slug}/plan.md` },
6078
6206
  menu: { presented: true, chosen: choice, evidence: recorded.evidence },
6079
- 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
+ },
6080
6218
  updated_at: Date.now()
6081
6219
  }
6082
6220
  };
@@ -6170,7 +6308,7 @@ var completeExecution = (root2, slug, deps) => {
6170
6308
  }
6171
6309
  const next = {
6172
6310
  ...reconciled.state,
6173
- execution: { ...exec, status: "completed" },
6311
+ execution: { ...exec, status: "completed", coordinator_session_id: null },
6174
6312
  handoff_destination: false,
6175
6313
  updated_at: Date.now()
6176
6314
  };
@@ -6234,7 +6372,7 @@ var slugFromSddPath = (p) => {
6234
6372
  const match = p.split(path22.sep).join("/").match(/^docs\/([^/]+)\/sdd(\/|$|['"])/);
6235
6373
  return match?.[1] ?? "";
6236
6374
  };
6237
- var assertFlowGates = (root2, planPath, opts = {}) => {
6375
+ var assertHandoffReady = (root2, planPath) => {
6238
6376
  const doc = resolveDoc(root2, "", planPath, "plan");
6239
6377
  if (!doc.ok)
6240
6378
  return err2("path_invalid", doc.error);
@@ -6249,12 +6387,15 @@ var assertFlowGates = (root2, planPath, opts = {}) => {
6249
6387
  if (state.plan.status !== "approved") {
6250
6388
  return err2("plan_not_approved", `plan not approved (status: ${state.plan.status}). Run workflow_plan_approve after the user's approval.`);
6251
6389
  }
6252
- if (opts.requireMenu && !state.menu.presented) {
6253
- 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})`);
6254
6395
  }
6255
6396
  return { ok: true };
6256
6397
  };
6257
- var assertProductGates = (root2, slug, opts = {}, ctx) => {
6398
+ var assertSddControlGates = (root2, slug, opts = {}, ctx) => {
6258
6399
  const bound = assertMutationWorkspace(root2, ctx);
6259
6400
  if (!bound.ok)
6260
6401
  return bound;
@@ -6280,9 +6421,12 @@ var assertProductGates = (root2, slug, opts = {}, ctx) => {
6280
6421
  if (validated.ok === false)
6281
6422
  return err2("docs_invalid", validated.error);
6282
6423
  }
6283
- 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 };
6284
6428
  };
6285
- var roleFromParentage = (parentID) => parentID ? "delegated" : "coordinator";
6429
+ var roleFromParentage = (parentID, coordinatorSessionId) => typeof parentID === "string" && parentID !== "" && parentID === coordinatorSessionId ? "delegated" : "coordinator";
6286
6430
  var COORDINATOR_WRITE_TOOLS = [
6287
6431
  "write",
6288
6432
  "edit",
@@ -6307,9 +6451,6 @@ var COORDINATOR_WRITE_TOOLS = [
6307
6451
  "workflow_docs_promote",
6308
6452
  "workflow_docs_layout",
6309
6453
  "workflow_docs_repo_link",
6310
- "workflow_sdd_task_brief",
6311
- "workflow_sdd_review_package",
6312
- "workflow_sdd_append_progress",
6313
6454
  "workflow_youtrack_post",
6314
6455
  "workflow_youtrack_log_time"
6315
6456
  ];
@@ -6614,10 +6755,34 @@ var isCoordinatorBashAllowed = (command) => {
6614
6755
  };
6615
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;
6616
6757
  var subagentDrivenInterception = (input) => {
6617
- 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)
6618
6762
  return { ok: true };
6619
- 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
+ }
6620
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
+ }
6621
6786
  if (COORDINATOR_WRITE_TOOLS.includes(input.tool)) {
6622
6787
  return err2("coordinator_write_denied", COORDINATOR_RECOVERY_TEXT);
6623
6788
  }
@@ -6628,6 +6793,24 @@ var subagentDrivenInterception = (input) => {
6628
6793
  }
6629
6794
  return { ok: true };
6630
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
+ };
6631
6814
 
6632
6815
  // packages/workit-core/src/core/detector.ts
6633
6816
  var detectConfigGapError = (text) => text.includes(CONFIG_GAP_MARKER);
@@ -6748,7 +6931,7 @@ var scanActiveSubagentDrivenPlans = (root2) => {
6748
6931
  if (exec.status === "active" && exec.mode === "subagent-driven")
6749
6932
  slugs.push(slug);
6750
6933
  };
6751
- for (const slug of readdirSync6(docsDir)) {
6934
+ for (const slug of readdirSync7(docsDir)) {
6752
6935
  const file = path23.join(docsDir, slug, "sdd", "flow.json");
6753
6936
  if (!existsSync16(file))
6754
6937
  continue;
@@ -19963,15 +20146,21 @@ var opencodeMutationContext = async (context2, client) => {
19963
20146
  const session = await client?.session.get({ path: { id: context2.sessionID } });
19964
20147
  parentID = session?.data?.parentID;
19965
20148
  } catch {}
19966
- const role = roleFromParentage(parentID);
20149
+ const role = roleFromParentage(parentID, undefined);
19967
20150
  return {
19968
20151
  hostWorkspace: context2.directory,
19969
20152
  role,
19970
20153
  sessionId: context2.sessionID,
20154
+ parentSessionId: parentID,
19971
20155
  taskIdentity: role === "delegated" ? context2.sessionID : undefined
19972
20156
  };
19973
20157
  };
19974
20158
  function createFlowTools(receipts, client) {
20159
+ const purposeForLifecycle = {
20160
+ "Pause plan": "plan-pause",
20161
+ "Resume plan": "plan-resume",
20162
+ "Complete plan": "plan-complete"
20163
+ };
19975
20164
  const lifecycleTool = (action, label, description) => tool({
19976
20165
  description,
19977
20166
  args: {
@@ -19982,7 +20171,10 @@ function createFlowTools(receipts, client) {
19982
20171
  if ("error" in slugged)
19983
20172
  return output2(fail(slugged.error));
19984
20173
  const slug = slugged.slug;
19985
- const consumed = receipts.consume(context2.sessionID, { label });
20174
+ const consumed = receipts.consume(context2.sessionID, {
20175
+ purpose: purposeForLifecycle[label],
20176
+ label
20177
+ });
19986
20178
  if (!consumed.ok)
19987
20179
  return output2(fail(consumed.error, { code: consumed.code }));
19988
20180
  const result = transitionExecution(context2.directory, slug, plan_path, action, createOpenCodeEvidence(consumed.receipt), await opencodeMutationContext(context2, client));
@@ -20052,7 +20244,7 @@ function createFlowTools(receipts, client) {
20052
20244
  if ("error" in slugged)
20053
20245
  return output2(fail(slugged.error));
20054
20246
  const slug = slugged.slug;
20055
- const consumed = receipts.consume(context2.sessionID);
20247
+ const consumed = receipts.consume(context2.sessionID, { purpose: "spec-approval" });
20056
20248
  if (!consumed.ok)
20057
20249
  return output2(fail(consumed.error, { code: consumed.code }));
20058
20250
  const result = transitionSpec(context2.directory, slug, spec_path, createOpenCodeEvidence(consumed.receipt), await opencodeMutationContext(context2, client));
@@ -20071,7 +20263,7 @@ function createFlowTools(receipts, client) {
20071
20263
  if ("error" in slugged)
20072
20264
  return output2(fail(slugged.error));
20073
20265
  const slug = slugged.slug;
20074
- const consumed = receipts.consume(context2.sessionID);
20266
+ const consumed = receipts.consume(context2.sessionID, { purpose: "plan-approval" });
20075
20267
  if (!consumed.ok)
20076
20268
  return output2(fail(consumed.error, { code: consumed.code }));
20077
20269
  const result = transitionPlan(context2.directory, slug, plan_path, createOpenCodeEvidence(consumed.receipt), await opencodeMutationContext(context2, client));
@@ -20097,7 +20289,10 @@ function createFlowTools(receipts, client) {
20097
20289
  if ("error" in slugged)
20098
20290
  return output2(fail(slugged.error));
20099
20291
  const slug = slugged.slug;
20100
- const consumed = receipts.consume(context2.sessionID, { label: choice });
20292
+ const consumed = receipts.consume(context2.sessionID, {
20293
+ purpose: "execution-menu",
20294
+ label: choice
20295
+ });
20101
20296
  if (!consumed.ok)
20102
20297
  return output2(fail(consumed.error, { code: consumed.code }));
20103
20298
  const result = recordMenuChoice(context2.directory, slug, plan_path, choice, createOpenCodeEvidence(consumed.receipt), await opencodeMutationContext(context2, client));
@@ -20116,11 +20311,11 @@ function createFlowTools(receipts, client) {
20116
20311
  // packages/workit-opencode/src/tools/sdd.ts
20117
20312
  var output3 = (value) => JSON.stringify(value, null, 2);
20118
20313
  var requireConfirmed2 = (confirmed) => confirmed === true ? null : output3(fail("confirmed: true required"));
20119
- var gateProductWrite = async (root2, sddPath, context2, client) => {
20314
+ var gateSddControl = async (root2, sddPath, context2, client) => {
20120
20315
  const slug = slugFromSddPath(sddPath);
20121
20316
  if (!slug)
20122
20317
  return output3(fail("could not derive slug — expected docs/<slug>/sdd/..."));
20123
- 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));
20124
20319
  if (!gate.ok)
20125
20320
  return output3(fail(gate.error, { code: gate.code }));
20126
20321
  return null;
@@ -20267,7 +20462,7 @@ function createSddTools(state, client) {
20267
20462
  const rejected = requireConfirmed2(confirmed);
20268
20463
  if (rejected)
20269
20464
  return rejected;
20270
- const gated = await gateProductWrite(context2.directory, sdd_dir, context2, client);
20465
+ const gated = await gateSddControl(context2.directory, sdd_dir, context2, client);
20271
20466
  if (gated)
20272
20467
  return gated;
20273
20468
  return invoke(() => {
@@ -20300,7 +20495,7 @@ function createSddTools(state, client) {
20300
20495
  } catch (error45) {
20301
20496
  return output3(fail(error45 instanceof Error ? error45.message : "workflow operation failed"));
20302
20497
  }
20303
- const gated = await gateProductWrite(context2.directory, sdd_dir, context2, client);
20498
+ const gated = await gateSddControl(context2.directory, sdd_dir, context2, client);
20304
20499
  if (gated)
20305
20500
  return gated;
20306
20501
  return invoke(() => sddReviewPackage({
@@ -20322,7 +20517,7 @@ function createSddTools(state, client) {
20322
20517
  const rejected = requireConfirmed2(confirmed);
20323
20518
  if (rejected)
20324
20519
  return rejected;
20325
- const gated = await gateProductWrite(context2.directory, progress_path, context2, client);
20520
+ const gated = await gateSddControl(context2.directory, progress_path, context2, client);
20326
20521
  if (gated)
20327
20522
  return gated;
20328
20523
  return invoke(() => {
@@ -20330,6 +20525,32 @@ function createSddTools(state, client) {
20330
20525
  return sddAppendProgress({ progress_path, line, workspace_root: context2.directory });
20331
20526
  });
20332
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
+ }
20333
20554
  })
20334
20555
  };
20335
20556
  }
@@ -20341,12 +20562,12 @@ import path32 from "node:path";
20341
20562
  import path31 from "node:path";
20342
20563
 
20343
20564
  // packages/workit-core/src/core/handoff-context.ts
20344
- 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";
20345
20566
  import path30 from "node:path";
20346
20567
  var DOC_RE = /docs\/([A-Za-z0-9][A-Za-z0-9._-]*)\/(spec|plan)\.md/g;
20347
20568
  var listMd = (dir) => {
20348
20569
  try {
20349
- return readdirSync7(dir).filter((f) => f.endsWith(".md")).sort();
20570
+ return readdirSync8(dir).filter((f) => f.endsWith(".md")).sort();
20350
20571
  } catch {
20351
20572
  return [];
20352
20573
  }
@@ -20372,7 +20593,7 @@ var resolveActivePair = (root2) => {
20372
20593
  let best = null;
20373
20594
  let entries = [];
20374
20595
  try {
20375
- entries = readdirSync7(docsDir);
20596
+ entries = readdirSync8(docsDir);
20376
20597
  } catch {
20377
20598
  return { error: "no pair" };
20378
20599
  }
@@ -20549,21 +20770,13 @@ function createHandoffTools(client, state) {
20549
20770
  const active = built;
20550
20771
  const slug = slugFromPath(active.plan);
20551
20772
  try {
20552
- const gate = assertFlowGates(context2.directory, active.plan);
20553
- if (!gate.ok)
20554
- return output4(fail(gate.error));
20555
- const preflight = readEffectiveFlowState(context2.directory, slug);
20556
- if (!preflight.ok)
20557
- return output4(fail(preflight.error, { code: preflight.code }));
20558
- if (preflight.state.handoff_destination) {
20559
- return output4(fail("this flow is already a handoff destination — a second handoff is rejected", {
20560
- code: "recursive_handoff"
20561
- }));
20562
- }
20773
+ const ready = assertHandoffReady(context2.directory, active.plan);
20774
+ if (!ready.ok)
20775
+ return output4(fail(ready.error, { code: ready.code }));
20563
20776
  state.set(context2.sessionID, { spec: active.spec, plan: active.plan, sdd: active.sdd });
20564
20777
  return output4(await handoffSession(client, {
20565
20778
  directory: context2.directory,
20566
- title: `Continue ${path32.basename(path32.dirname(active.plan))}`,
20779
+ title: `Workit: ${path32.basename(path32.dirname(active.plan))}`,
20567
20780
  prompt: active.prompt,
20568
20781
  stay: /(?:^|\s)--stay(?:\s|$)/.test(userMessage),
20569
20782
  afterSeed: () => idempotentMarkDestination(context2.directory, slug, active.plan)
@@ -21058,7 +21271,7 @@ function createPresentTools() {
21058
21271
  }
21059
21272
 
21060
21273
  // packages/workit-core/src/core/docs-repo.ts
21061
- 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";
21062
21275
  import { execFileSync as execFileSync5 } from "node:child_process";
21063
21276
  import path35 from "node:path";
21064
21277
  var configPath2 = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path35.join(configDir(), "docs-repo.json");
@@ -21104,7 +21317,7 @@ var listSpecs = (workspaceRoot) => {
21104
21317
  const specs = [];
21105
21318
  const docsDir = path35.join(workspaceRoot, "docs");
21106
21319
  if (existsSync19(docsDir)) {
21107
- for (const slug of readdirSync8(docsDir)) {
21320
+ for (const slug of readdirSync9(docsDir)) {
21108
21321
  if (slug.startsWith("."))
21109
21322
  continue;
21110
21323
  const spec = path35.posix.join("docs", slug, "spec.md");
@@ -21115,7 +21328,7 @@ var listSpecs = (workspaceRoot) => {
21115
21328
  if (repoPath) {
21116
21329
  const featuresDir = path35.join(repoPath, "features");
21117
21330
  if (existsSync19(featuresDir)) {
21118
- 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));
21119
21332
  if (match) {
21120
21333
  promoted = true;
21121
21334
  target = path35.join(repoPath, "features", match);
@@ -21207,7 +21420,7 @@ var promoteSpec = (workspaceRoot, slug, opts) => {
21207
21420
  }
21208
21421
  const prefix = monthPrefix();
21209
21422
  const featuresDir = path35.join(repoPath, "features");
21210
- 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;
21211
21424
  const targetDir = path35.join(featuresDir, existing ?? `${prefix}-${slug}`);
21212
21425
  mkdirSync10(targetDir, { recursive: true });
21213
21426
  const files = ["spec.md"];
@@ -21269,7 +21482,7 @@ import {
21269
21482
  existsSync as existsSync20,
21270
21483
  lstatSync as lstatSync2,
21271
21484
  mkdirSync as mkdirSync11,
21272
- readdirSync as readdirSync9,
21485
+ readdirSync as readdirSync10,
21273
21486
  readFileSync as readFileSync23,
21274
21487
  realpathSync as realpathSync5,
21275
21488
  renameSync as renameSync3,
@@ -21324,7 +21537,7 @@ var detectLegacyDocs = (workspace_root) => {
21324
21537
  const root2 = legacyRoot(workspace_root);
21325
21538
  const raw = [];
21326
21539
  if (existsSync20(root2)) {
21327
- for (const entry of readdirSync9(root2, { withFileTypes: true })) {
21540
+ for (const entry of readdirSync10(root2, { withFileTypes: true })) {
21328
21541
  const abs = path36.join(root2, entry.name);
21329
21542
  const rel = posix3(path36.relative(workspace_root, abs));
21330
21543
  if (entry.isDirectory()) {
@@ -21466,7 +21679,7 @@ var planTree = (workspace, legacyAbs, legacyRel, destRelRoot, destAbsRoot, legac
21466
21679
  if (visits.has(real))
21467
21680
  return { ok: true };
21468
21681
  visits.add(real);
21469
- for (const entry of readdirSync9(real, { withFileTypes: true })) {
21682
+ for (const entry of readdirSync10(real, { withFileTypes: true })) {
21470
21683
  const fromAbs = path36.join(real, entry.name);
21471
21684
  const fromRel = posix3(path36.join(legacyRel, entry.name));
21472
21685
  const toRel = posix3(path36.join(destRelRoot, entry.name));
@@ -21837,7 +22050,7 @@ SDD: ${value.sdd}` : null;
21837
22050
  }
21838
22051
 
21839
22052
  // packages/workit-core/src/core/logger.ts
21840
- 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";
21841
22054
  import os4 from "node:os";
21842
22055
  import path37 from "node:path";
21843
22056
  var REDACTED = "[REDACTED]";
@@ -21972,7 +22185,7 @@ var dailyFileName = (date5) => {
21972
22185
  };
21973
22186
  var pruneOldFiles = (dir) => {
21974
22187
  try {
21975
- 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();
21976
22189
  for (const file2 of files.slice(RETAINED_DAYS)) {
21977
22190
  try {
21978
22191
  unlinkSync3(path37.join(dir, file2));
@@ -22139,7 +22352,7 @@ var hasMarkedDestination = (root3) => {
22139
22352
  return false;
22140
22353
  let slugs;
22141
22354
  try {
22142
- slugs = readdirSync11(docsDir);
22355
+ slugs = readdirSync12(docsDir);
22143
22356
  } catch {
22144
22357
  return false;
22145
22358
  }
@@ -22171,6 +22384,32 @@ var questionAnswerLabel = (result) => {
22171
22384
  const label = first[0];
22172
22385
  return typeof label === "string" ? label : undefined;
22173
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
+ };
22174
22413
  var plugin = async ({ client, directory }) => {
22175
22414
  openCodeClient = client;
22176
22415
  installUncaughtHandlers();
@@ -22180,6 +22419,17 @@ var plugin = async ({ client, directory }) => {
22180
22419
  setDiagnosticLogger(logger);
22181
22420
  const state = new WorkflowStateStore;
22182
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
+ };
22183
22433
  return {
22184
22434
  tool: createTools(adaptPluginHandoffClient(client), state, client, receipts),
22185
22435
  "tool.execute.after": async (input, output10) => {
@@ -22188,9 +22438,11 @@ var plugin = async ({ client, directory }) => {
22188
22438
  const label = questionAnswerLabel(output10);
22189
22439
  if (label === undefined)
22190
22440
  return;
22191
- const args = input.args;
22192
- const questionText = args?.question ?? args?.title;
22193
- 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);
22194
22446
  },
22195
22447
  "tool.execute.before": async (input, output10) => {
22196
22448
  if (input.tool !== "bash" && !COORDINATOR_WRITE_TOOLS.includes(input.tool))
@@ -22202,12 +22454,12 @@ var plugin = async ({ client, directory }) => {
22202
22454
  parentID = session?.data?.parentID;
22203
22455
  sessionDirectory = session?.data?.directory;
22204
22456
  } catch {}
22205
- 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 !== "");
22206
22458
  const decision = subagentDrivenInterception({
22207
22459
  tool: input.tool,
22208
22460
  command: output10.args?.command,
22209
22461
  parentID,
22210
- active
22462
+ activeCoordinatorIds
22211
22463
  });
22212
22464
  if (!decision.ok)
22213
22465
  throw new Error(decision.error);
@@ -22260,7 +22512,8 @@ var plugin = async ({ client, directory }) => {
22260
22512
  const firstAnchor = firstUser.parts.find((part) => part.type === "text") ?? firstUser.parts[0];
22261
22513
  const firstText = firstUser.parts.filter((part) => part.type === "text").map((part) => part.text ?? "").join(`
22262
22514
  `);
22263
- const bootstrap = getWorkitBootstrap();
22515
+ const workerAuthorized = await authorizedWorkerFor(firstAnchor.sessionID);
22516
+ const bootstrap = workerAuthorized ? null : getWorkitBootstrap();
22264
22517
  if (bootstrap && !firstText.includes("<workit-contract>")) {
22265
22518
  firstUser.parts.unshift({
22266
22519
  id: firstAnchor.id,
@@ -22269,6 +22522,14 @@ var plugin = async ({ client, directory }) => {
22269
22522
  type: "text",
22270
22523
  text: bootstrap
22271
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
+ });
22272
22533
  }
22273
22534
  }
22274
22535
  const anchor = currentUser.parts.find((part) => part.type === "text") ?? currentUser.parts[0];
@@ -22303,8 +22564,12 @@ var plugin = async ({ client, directory }) => {
22303
22564
  }
22304
22565
  }
22305
22566
  const activePlans = findActiveSubagentDrivenPlans(directory);
22306
- if (activePlans.length > 0 && shouldInjectSddReminder(currentText)) {
22307
- 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
+ }
22308
22573
  }
22309
22574
  if (detectConfigGapError(assistantText) && shouldInjectConfigGuard(currentText)) {
22310
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.8",
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.8"
37
+ "@brainervirus/workit-core": "^0.8.9"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@opencode-ai/plugin": "1.17.7"