@brainervirus/workit-cursor 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.
@@ -2,7 +2,7 @@
2
2
  "name": "workit",
3
3
  "displayName": "Workit",
4
4
  "description": "OpenCode-style verify, PR, changelog, commit, and session handoff for Cursor",
5
- "version": "0.8.8",
5
+ "version": "0.8.9",
6
6
  "author": {
7
7
  "name": "Cristhofer Pincetti"
8
8
  },
package/README.md CHANGED
@@ -46,7 +46,7 @@ npx @brainervirus/workit-cli init
46
46
 
47
47
  ## Host limitations
48
48
 
49
- Cursor adapts workit through policy-only confirmation: approvals and lifecycle transitions are recorded as policy decisions (`attested: false`) rather than fabricated delegated identity, and subagent-driven plan execution is not supported on this host. Approvals bind to the document's exact SHA-256 digest — editing an approved spec/plan invalidates the approval and forces a fresh reapproval. OpenCode records native `question` receipts and runs delegated tasks; see the root [README](../../README.md#host-capabilities) for the full host-capability matrix.
49
+ Cursor adapts workit through policy-only confirmation: approvals and lifecycle transitions are recorded as policy decisions (`attested: false`) rather than fabricated delegated identity, and subagent-driven plan execution is not supported on this host. Cursor consumes the same core outcomes but stays inline-only (the subagent-driven menu choice returns `unsupported_mode`); model selection remains a host-native action. Approvals bind to the document's exact SHA-256 digest — editing an approved spec/plan invalidates the approval and forces a fresh reapproval. OpenCode records native `question` receipts and runs delegated tasks; see the root [README](../../README.md#host-capabilities) for the full host-capability matrix.
50
50
 
51
51
  ## Configuration
52
52
 
@@ -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
 
@@ -285,13 +285,15 @@ var SOURCE_MENU_LABELS = [
285
285
  "Inline",
286
286
  "Handoff",
287
287
  "Review spec first",
288
- "Review plan first"
288
+ "Review plan first",
289
+ "Change model first"
289
290
  ];
290
291
  var DESTINATION_MENU_LABELS = [
291
292
  "Subagent-driven",
292
293
  "Inline",
293
294
  "Review spec first",
294
- "Review plan first"
295
+ "Review plan first",
296
+ "Change model first"
295
297
  ];
296
298
  var HANDOFF_DESTINATION_MARKER = "<workflow-handoff-destination>true</workflow-handoff-destination>";
297
299
  var findMarkedDestinations = (root) => {
@@ -24266,6 +24266,58 @@ function sddAppendProgress({
24266
24266
  const rel = posix2(path19.relative(contained.base, path_));
24267
24267
  return { ok: true, line: trimmed, progress_path: rel };
24268
24268
  }
24269
+ function sddAppendAdvisory({
24270
+ advisories_path,
24271
+ task_id,
24272
+ text,
24273
+ workspace_root
24274
+ }) {
24275
+ if (typeof task_id !== "number" || !Number.isSafeInteger(task_id) || task_id <= 0) {
24276
+ return { error: "task_id must be a positive safe integer", code: "advisory_task_invalid" };
24277
+ }
24278
+ if (typeof text !== "string") {
24279
+ return {
24280
+ error: "advisory text must be a string of 1-1000 characters after normalization",
24281
+ code: "advisory_text_invalid"
24282
+ };
24283
+ }
24284
+ if (text.includes("\r") || text.includes(`
24285
+ `)) {
24286
+ return {
24287
+ error: "advisory text must be a single line (no CR/LF)",
24288
+ code: "advisory_text_invalid"
24289
+ };
24290
+ }
24291
+ const collapsed = text.trim().replace(/[ \t]+/g, " ");
24292
+ if (collapsed.length === 0 || collapsed.length > 1000) {
24293
+ return {
24294
+ error: "advisory text must be 1-1000 characters after trim and horizontal-space collapse",
24295
+ code: "advisory_text_invalid"
24296
+ };
24297
+ }
24298
+ if (!/^docs\/[^/]+\/sdd\/advisories\.md$/.test(advisories_path)) {
24299
+ return {
24300
+ error: `advisories_path must be docs/<slug>/sdd/advisories.md: ${advisories_path}`,
24301
+ code: "advisory_path_invalid"
24302
+ };
24303
+ }
24304
+ const contained = resolveDocsPath({ workspace_root, path: advisories_path });
24305
+ if (!contained.ok)
24306
+ return { error: contained.error, code: "advisory_path_invalid" };
24307
+ const abs = contained.path;
24308
+ if (existsSync11(abs) && statSync6(abs).isDirectory()) {
24309
+ return {
24310
+ error: `advisory target is a directory: ${advisories_path}`,
24311
+ code: "advisory_target_invalid"
24312
+ };
24313
+ }
24314
+ mkdirSync6(path19.dirname(abs), { recursive: true });
24315
+ const line = `- Task ${task_id}: ${collapsed}
24316
+ `;
24317
+ appendFileSync2(abs, line, "utf8");
24318
+ const rel = posix2(path19.relative(contained.base, abs));
24319
+ return { ok: true, advisory: collapsed, advisories_path: rel };
24320
+ }
24269
24321
  var posix2 = (p) => p.split(path19.sep).join("/"), PROGRESS_RE;
24270
24322
  var init_sdd = __esm(() => {
24271
24323
  init_docs_validate();
@@ -24286,6 +24338,7 @@ import {
24286
24338
  fsyncSync,
24287
24339
  mkdirSync as mkdirSync7,
24288
24340
  openSync,
24341
+ readdirSync as readdirSync6,
24289
24342
  readFileSync as readFileSync13,
24290
24343
  renameSync,
24291
24344
  rmSync,
@@ -24298,16 +24351,20 @@ import path20 from "node:path";
24298
24351
 
24299
24352
  class HostReceiptStore {
24300
24353
  #bySession = new Map;
24301
- record(sessionId, callID, selectedLabel, recordedAt = Date.now(), question) {
24302
- const label = selectedLabel.trim();
24303
- if (!label)
24354
+ record(sessionId, callID, selectedLabel, recordedAt = Date.now(), question = "", purpose) {
24355
+ const trimmed = selectedLabel.trim();
24356
+ if (!trimmed)
24304
24357
  return;
24305
24358
  if (recordedAt > Date.now() + MAX_CLOCK_SKEW_MS)
24306
24359
  return;
24360
+ const derived = purpose ?? receiptPurposeForLabel(selectedLabel);
24361
+ if (derived === undefined)
24362
+ return;
24363
+ const q = question ?? "";
24307
24364
  const queue = this.#bySession.get(sessionId) ?? [];
24308
24365
  if (queue.length >= MAX_RECEIPTS_PER_SESSION)
24309
24366
  queue.shift();
24310
- queue.push({ sessionId, callID, selectedLabel: label, recordedAt, question });
24367
+ queue.push({ sessionId, callID, selectedLabel, recordedAt, question: q, purpose: derived });
24311
24368
  this.#bySession.set(sessionId, queue);
24312
24369
  }
24313
24370
  count(sessionId) {
@@ -24324,10 +24381,40 @@ class HostReceiptStore {
24324
24381
  if (!queue || queue.length === 0) {
24325
24382
  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");
24326
24383
  }
24327
- const index = queue.length - 1;
24384
+ let index = -1;
24385
+ if (opts.purpose !== undefined) {
24386
+ const top = queue[queue.length - 1];
24387
+ if (top && top.purpose === opts.purpose && isNegativeLabel(top.selectedLabel)) {
24388
+ const filtered = queue.filter((r) => r.purpose !== opts.purpose);
24389
+ if (filtered.length === 0)
24390
+ this.#bySession.delete(sessionId);
24391
+ else
24392
+ this.#bySession.set(sessionId, filtered);
24393
+ 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");
24394
+ }
24395
+ for (let i = queue.length - 1;i >= 0; i--) {
24396
+ if (queue[i].purpose === opts.purpose) {
24397
+ index = i;
24398
+ break;
24399
+ }
24400
+ }
24401
+ if (index === -1) {
24402
+ return err2("receipt_missing", `no host-observed receipt for purpose ${JSON.stringify(opts.purpose)} — ask the native question for that purpose`);
24403
+ }
24404
+ } else {
24405
+ index = queue.length - 1;
24406
+ }
24328
24407
  const receipt = queue[index];
24329
24408
  if (isNegativeLabel(receipt.selectedLabel)) {
24330
- this.#bySession.delete(sessionId);
24409
+ if (opts.purpose !== undefined) {
24410
+ const filtered = queue.filter((r) => r.purpose !== opts.purpose);
24411
+ if (filtered.length === 0)
24412
+ this.#bySession.delete(sessionId);
24413
+ else
24414
+ this.#bySession.set(sessionId, filtered);
24415
+ } else {
24416
+ this.#bySession.delete(sessionId);
24417
+ }
24331
24418
  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");
24332
24419
  }
24333
24420
  if (opts.label !== undefined && !sameChoiceLabel(receipt.selectedLabel, opts.label)) {
@@ -24396,7 +24483,8 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24396
24483
  execution: {
24397
24484
  status: execution.status ?? "pending",
24398
24485
  mode: execution.mode ?? null,
24399
- evidence: execution.evidence ?? null
24486
+ evidence: execution.evidence ?? null,
24487
+ coordinator_session_id: execution.coordinator_session_id ?? null
24400
24488
  },
24401
24489
  handoff_destination: p.handoff_destination ?? false,
24402
24490
  updated_at: p.updated_at ?? Date.now()
@@ -24407,7 +24495,7 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24407
24495
  spec: { path: "", status: "draft", evidence: null, approved_digest: null },
24408
24496
  plan: { path: "", status: "draft", evidence: null, approved_digest: null },
24409
24497
  menu: { presented: false, chosen: "", evidence: null },
24410
- execution: { status: "pending", mode: null, evidence: null },
24498
+ execution: { status: "pending", mode: null, evidence: null, coordinator_session_id: null },
24411
24499
  handoff_destination: false,
24412
24500
  updated_at: Date.now()
24413
24501
  }), readFlowState = (root, slug) => {
@@ -24516,6 +24604,12 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24516
24604
  if (execRaw?.evidence !== undefined && !validateEvidenceValue(execRaw.evidence, true)) {
24517
24605
  return { ok: false, error: "flow state execution.evidence has an unsupported shape" };
24518
24606
  }
24607
+ if (execRaw?.coordinator_session_id !== undefined && execRaw.coordinator_session_id !== null && typeof execRaw.coordinator_session_id !== "string") {
24608
+ return {
24609
+ ok: false,
24610
+ error: "flow state execution.coordinator_session_id must be a string or null"
24611
+ };
24612
+ }
24519
24613
  return {
24520
24614
  ok: true,
24521
24615
  state: {
@@ -24531,7 +24625,8 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24531
24625
  execution: {
24532
24626
  status: execRaw?.status ?? "pending",
24533
24627
  mode: execRaw?.mode ?? null,
24534
- evidence: execRaw?.evidence ?? null
24628
+ evidence: execRaw?.evidence ?? null,
24629
+ coordinator_session_id: execRaw?.coordinator_session_id ?? null
24535
24630
  },
24536
24631
  handoff_destination: parsed.handoff_destination ?? false,
24537
24632
  updated_at: parsed.updated_at ?? Date.now()
@@ -24658,7 +24753,7 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24658
24753
  spec: { ...state.spec, status: "draft", evidence: null, approved_digest: null },
24659
24754
  plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
24660
24755
  menu: { presented: false, chosen: "", evidence: null },
24661
- execution: { status: "pending", mode: null, evidence: null },
24756
+ execution: { status: "pending", mode: null, evidence: null, coordinator_session_id: null },
24662
24757
  handoff_destination: false,
24663
24758
  updated_at: Date.now()
24664
24759
  }), resetForPlanDrift = (state) => ({
@@ -24697,9 +24792,14 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24697
24792
  }, deriveLegacyExecution = (root, slug, state) => {
24698
24793
  const ledger = ledgerCompletion(root, slug);
24699
24794
  if (state.plan.status === "approved" && state.menu.chosen === "subagent-driven" && ledger.started && !ledger.complete) {
24700
- return { status: "active", mode: "subagent-driven", evidence: null };
24795
+ return {
24796
+ status: "active",
24797
+ mode: "subagent-driven",
24798
+ evidence: null,
24799
+ coordinator_session_id: null
24800
+ };
24701
24801
  }
24702
- return { status: "pending", mode: null, evidence: null };
24802
+ return { status: "pending", mode: null, evidence: null, coordinator_session_id: null };
24703
24803
  }, normalizeCompatibility = (root, slug, parsed, state) => {
24704
24804
  if (!isRecord(parsed) || !("execution" in parsed)) {
24705
24805
  const derived = deriveLegacyExecution(root, slug, state);
@@ -24707,6 +24807,11 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24707
24807
  if (derived.status !== current.status || derived.mode !== current.mode) {
24708
24808
  return { state: { ...state, execution: derived, updated_at: Date.now() }, changed: true };
24709
24809
  }
24810
+ return { state, changed: false };
24811
+ }
24812
+ const execRaw = isRecord(parsed.execution) ? parsed.execution : undefined;
24813
+ if (execRaw && !("coordinator_session_id" in execRaw)) {
24814
+ return { state: { ...state, updated_at: Date.now() }, changed: true };
24710
24815
  }
24711
24816
  return { state, changed: false };
24712
24817
  }, withFlowLock = (file, fn) => {
@@ -24836,14 +24941,6 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24836
24941
  return err2("workspace_mismatch", `mutation context workspace ${JSON.stringify(ctx.hostWorkspace)} does not match flow workspace ${JSON.stringify(root)}`);
24837
24942
  }
24838
24943
  return { ok: true };
24839
- }, assertCoordinatorBoundary = (ctx, state) => {
24840
- if (ctx?.role === "coordinator" && state.execution.status === "active" && state.execution.mode === "subagent-driven") {
24841
- return err2("coordinator_blocked", COORDINATOR_RECOVERY_TEXT);
24842
- }
24843
- if (ctx?.role === "delegated" && !ctx.taskIdentity) {
24844
- return err2("delegated_unauthenticated", "delegated mutations require an authenticated task identity (taskIdentity) — re-run inside the delegated worker session");
24845
- }
24846
- return { ok: true };
24847
24944
  }, MAX_CLOCK_SKEW_MS = 60000, MAX_RECEIPTS_PER_SESSION = 10, RECEIPT_FRESHNESS_MS, EVIDENCE_WINDOW_MS, NEGATIVE_ANSWER_LABELS, isNegativeLabel = (label) => {
24848
24945
  const normalized = label.trim().toLowerCase();
24849
24946
  return NEGATIVE_ANSWER_LABELS.some((entry) => {
@@ -24854,6 +24951,31 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24854
24951
  }
24855
24952
  return firstWord.replace(/[^a-z]/g, "") === entry;
24856
24953
  });
24954
+ }, receiptPurposeForLabel = (label) => {
24955
+ const n = normalizeLabel(label);
24956
+ if (n === "approve spec" || n === "approve spec recommended")
24957
+ return "spec-approval";
24958
+ if (n === "approve plan" || n === "approve plan recommended")
24959
+ return "plan-approval";
24960
+ if (n === "approve")
24961
+ return;
24962
+ if (n === "pause plan")
24963
+ return "plan-pause";
24964
+ if (n === "resume plan")
24965
+ return "plan-resume";
24966
+ if (n === "complete plan")
24967
+ return "plan-complete";
24968
+ const exec = new Set([
24969
+ "subagent driven",
24970
+ "inline",
24971
+ "handoff",
24972
+ "review spec",
24973
+ "review plan",
24974
+ "change model"
24975
+ ]);
24976
+ if (exec.has(n))
24977
+ return "execution-menu";
24978
+ return;
24857
24979
  }, sameChoiceLabel = (a, b) => normalizeLabel(a) === normalizeLabel(b), normalizeLabel = (s) => s.replace(/\s*\([^)]*\)/g, " ").replace(/\s*\bfirst\b\s*$/i, " ").replace(/[^a-z0-9]+/gi, " ").trim().toLowerCase(), createCursorConfirmation = () => ({
24858
24980
  ok: true,
24859
24981
  evidence: { host: "cursor", attested: false, confirmation: "contract" }
@@ -24958,7 +25080,7 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24958
25080
  spec: { path: specPath, status: "draft", evidence: null, approved_digest: null },
24959
25081
  plan: { path: planPath, status: "draft", evidence: null, approved_digest: null },
24960
25082
  menu: { presented: false, chosen: "", evidence: null },
24961
- execution: { status: "pending", mode: null, evidence: null },
25083
+ execution: { status: "pending", mode: null, evidence: null, coordinator_session_id: null },
24962
25084
  handoff_destination: false,
24963
25085
  updated_at: Date.now()
24964
25086
  });
@@ -25101,13 +25223,24 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
25101
25223
  return err2("recursive_handoff", "this flow is already a handoff destination — a second handoff is rejected");
25102
25224
  }
25103
25225
  const executing = choice === "subagent-driven" || choice === "inline";
25226
+ const coordinatorSessionId = choice === "subagent-driven" && recorded.evidence.host === "opencode" ? ctx?.sessionId ?? null : null;
25104
25227
  return {
25105
25228
  ok: true,
25106
25229
  next: {
25107
25230
  ...state,
25108
25231
  plan: { ...state.plan, path: state.plan.path || `docs/${slug}/plan.md` },
25109
25232
  menu: { presented: true, chosen: choice, evidence: recorded.evidence },
25110
- execution: executing ? { status: "active", mode: choice, evidence: recorded.evidence } : { status: "pending", mode: null, evidence: recorded.evidence },
25233
+ execution: executing ? {
25234
+ status: "active",
25235
+ mode: choice,
25236
+ evidence: recorded.evidence,
25237
+ coordinator_session_id: coordinatorSessionId
25238
+ } : {
25239
+ status: "pending",
25240
+ mode: null,
25241
+ evidence: recorded.evidence,
25242
+ coordinator_session_id: null
25243
+ },
25111
25244
  updated_at: Date.now()
25112
25245
  }
25113
25246
  };
@@ -25195,7 +25328,7 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
25195
25328
  }
25196
25329
  const next = {
25197
25330
  ...reconciled.state,
25198
- execution: { ...exec, status: "completed" },
25331
+ execution: { ...exec, status: "completed", coordinator_session_id: null },
25199
25332
  handoff_destination: false,
25200
25333
  updated_at: Date.now()
25201
25334
  };
@@ -25255,7 +25388,7 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
25255
25388
  }, slugFromSddPath = (p) => {
25256
25389
  const match = p.split(path20.sep).join("/").match(/^docs\/([^/]+)\/sdd(\/|$|['"])/);
25257
25390
  return match?.[1] ?? "";
25258
- }, assertProductGates = (root, slug, opts = {}, ctx) => {
25391
+ }, assertSddControlGates = (root, slug, opts = {}, ctx) => {
25259
25392
  const bound = assertMutationWorkspace(root, ctx);
25260
25393
  if (!bound.ok)
25261
25394
  return bound;
@@ -25281,15 +25414,18 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
25281
25414
  if (validated.ok === false)
25282
25415
  return err2("docs_invalid", validated.error);
25283
25416
  }
25284
- return assertCoordinatorBoundary(ctx, state);
25285
- }, BASH_READ_TOKENS, BASH_GIT_READ_SUBCOMMANDS, BASH_GIT_MUTABLE_SUBCOMMANDS, BASH_GIT_READ_FLAGS, BASH_FIND_DENIED_FLAGS, BASH_TEST_VERBS, BASH_DENIED_HEADS, BASH_OUTPUT_FLAG_VERBS, BASH_PAREN_EXEMPT_HEADS, BASH_GIT_VALUE_FLAGS, COORDINATOR_SHELL_DENIED_TEXT;
25417
+ if (state.execution.status === "active" && state.execution.mode === "subagent-driven" && roleFromParentage(ctx?.parentSessionId, state.execution.coordinator_session_id) === "delegated") {
25418
+ 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");
25419
+ }
25420
+ return { ok: true };
25421
+ }, roleFromParentage = (parentID, coordinatorSessionId) => typeof parentID === "string" && parentID !== "" && parentID === coordinatorSessionId ? "delegated" : "coordinator", BASH_READ_TOKENS, BASH_GIT_READ_SUBCOMMANDS, BASH_GIT_MUTABLE_SUBCOMMANDS, BASH_GIT_READ_FLAGS, BASH_FIND_DENIED_FLAGS, BASH_TEST_VERBS, BASH_DENIED_HEADS, BASH_OUTPUT_FLAG_VERBS, BASH_PAREN_EXEMPT_HEADS, BASH_GIT_VALUE_FLAGS, COORDINATOR_SHELL_DENIED_TEXT;
25286
25422
  var init_flow_state = __esm(() => {
25287
25423
  init_docs_validate();
25288
25424
  init_docs_layout();
25289
25425
  init_sdd();
25290
25426
  init_verify_project();
25291
25427
  init_menu();
25292
- 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.";
25428
+ 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.";
25293
25429
  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`.";
25294
25430
  MENU_CHOICES = [
25295
25431
  "subagent-driven",
@@ -25460,7 +25596,7 @@ import {
25460
25596
  existsSync as existsSync13,
25461
25597
  lstatSync as lstatSync2,
25462
25598
  mkdirSync as mkdirSync8,
25463
- readdirSync as readdirSync6,
25599
+ readdirSync as readdirSync7,
25464
25600
  readFileSync as readFileSync14,
25465
25601
  realpathSync as realpathSync3,
25466
25602
  renameSync as renameSync2,
@@ -25505,7 +25641,7 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
25505
25641
  const root = legacyRoot(workspace_root);
25506
25642
  const raw = [];
25507
25643
  if (existsSync13(root)) {
25508
- for (const entry of readdirSync6(root, { withFileTypes: true })) {
25644
+ for (const entry of readdirSync7(root, { withFileTypes: true })) {
25509
25645
  const abs = path21.join(root, entry.name);
25510
25646
  const rel = posix3(path21.relative(workspace_root, abs));
25511
25647
  if (entry.isDirectory()) {
@@ -25639,7 +25775,7 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
25639
25775
  if (visits.has(real))
25640
25776
  return { ok: true };
25641
25777
  visits.add(real);
25642
- for (const entry of readdirSync6(real, { withFileTypes: true })) {
25778
+ for (const entry of readdirSync7(real, { withFileTypes: true })) {
25643
25779
  const fromAbs = path21.join(real, entry.name);
25644
25780
  const fromRel = posix3(path21.join(legacyRel, entry.name));
25645
25781
  const toRel = posix3(path21.join(destRelRoot, entry.name));
@@ -25828,7 +25964,7 @@ var init_docs_migration = __esm(() => {
25828
25964
  });
25829
25965
 
25830
25966
  // packages/workit-core/src/core/docs-repo.ts
25831
- import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8, readdirSync as readdirSync7 } from "node:fs";
25967
+ import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8, readdirSync as readdirSync8 } from "node:fs";
25832
25968
  import { execFileSync as execFileSync6 } from "node:child_process";
25833
25969
  import path22 from "node:path";
25834
25970
  var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path22.join(configDir(), "docs-repo.json"), readDocsRepoConfig = () => {
@@ -25868,7 +26004,7 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path22.join(conf
25868
26004
  const specs = [];
25869
26005
  const docsDir = path22.join(workspaceRoot, "docs");
25870
26006
  if (existsSync14(docsDir)) {
25871
- for (const slug of readdirSync7(docsDir)) {
26007
+ for (const slug of readdirSync8(docsDir)) {
25872
26008
  if (slug.startsWith("."))
25873
26009
  continue;
25874
26010
  const spec = path22.posix.join("docs", slug, "spec.md");
@@ -25879,7 +26015,7 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path22.join(conf
25879
26015
  if (repoPath) {
25880
26016
  const featuresDir = path22.join(repoPath, "features");
25881
26017
  if (existsSync14(featuresDir)) {
25882
- const match = readdirSync7(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d));
26018
+ const match = readdirSync8(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d));
25883
26019
  if (match) {
25884
26020
  promoted = true;
25885
26021
  target = path22.join(repoPath, "features", match);
@@ -25966,7 +26102,7 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path22.join(conf
25966
26102
  }
25967
26103
  const prefix = monthPrefix();
25968
26104
  const featuresDir = path22.join(repoPath, "features");
25969
- const existing = existsSync14(featuresDir) ? readdirSync7(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d)) : undefined;
26105
+ const existing = existsSync14(featuresDir) ? readdirSync8(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d)) : undefined;
25970
26106
  const targetDir = path22.join(featuresDir, existing ?? `${prefix}-${slug}`);
25971
26107
  mkdirSync9(targetDir, { recursive: true });
25972
26108
  const files = ["spec.md"];
@@ -26111,7 +26247,7 @@ var init_templates = __esm(() => {
26111
26247
  });
26112
26248
 
26113
26249
  // packages/workit-core/src/core/rules.ts
26114
- import { existsSync as existsSync17, mkdirSync as mkdirSync11, readFileSync as readFileSync18, readdirSync as readdirSync8, writeFileSync as writeFileSync11 } from "node:fs";
26250
+ import { existsSync as existsSync17, mkdirSync as mkdirSync11, readFileSync as readFileSync18, readdirSync as readdirSync9, writeFileSync as writeFileSync11 } from "node:fs";
26115
26251
  import path25 from "node:path";
26116
26252
  var rulesDir = () => path25.join(configDir(), "rules"), parseRule = (markdown) => {
26117
26253
  const fm = markdown.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
@@ -26141,7 +26277,7 @@ var rulesDir = () => path25.join(configDir(), "rules"), parseRule = (markdown) =
26141
26277
  const result = [];
26142
26278
  const dir = rulesDir();
26143
26279
  if (existsSync17(dir)) {
26144
- for (const entry of readdirSync8(dir)) {
26280
+ for (const entry of readdirSync9(dir)) {
26145
26281
  const file = path25.join(dir, entry, "rule.md");
26146
26282
  if (!existsSync17(file))
26147
26283
  continue;
@@ -26264,7 +26400,7 @@ import {
26264
26400
  mkdirSync as mkdirSync12,
26265
26401
  mkdtempSync,
26266
26402
  readFileSync as readFileSync19,
26267
- readdirSync as readdirSync9,
26403
+ readdirSync as readdirSync10,
26268
26404
  renameSync as renameSync3,
26269
26405
  rmSync as rmSync3,
26270
26406
  statSync as statSync9,
@@ -27894,7 +28030,7 @@ var init_server3 = __esm(async () => {
27894
28030
  const slug = slugFromSddPath(sdd_dir);
27895
28031
  if (!slug)
27896
28032
  return jsonResult({ error: "could not derive slug — expected docs/<slug>/sdd/..." });
27897
- const gate = assertProductGates(workspace_root, slug, { requireMenu: true, requireDocs: true }, cursorMutationContext(workspace_root));
28033
+ const gate = assertSddControlGates(workspace_root, slug, { requireMenu: true, requireDocs: true }, cursorMutationContext(workspace_root));
27898
28034
  if (!gate.ok)
27899
28035
  return jsonResult({ error: gate.error, code: gate.code });
27900
28036
  const data = sddTaskBrief({
@@ -27917,7 +28053,7 @@ var init_server3 = __esm(async () => {
27917
28053
  const slug = slugFromSddPath(sdd_dir);
27918
28054
  if (!slug)
27919
28055
  return jsonResult({ error: "could not derive slug — expected docs/<slug>/sdd/..." });
27920
- const gate = assertProductGates(workspace_root, slug, { requireMenu: true, requireDocs: true }, cursorMutationContext(workspace_root));
28056
+ const gate = assertSddControlGates(workspace_root, slug, { requireMenu: true, requireDocs: true }, cursorMutationContext(workspace_root));
27921
28057
  if (!gate.ok)
27922
28058
  return jsonResult({ error: gate.error, code: gate.code });
27923
28059
  const data = sddReviewPackage({
@@ -27941,7 +28077,7 @@ var init_server3 = __esm(async () => {
27941
28077
  const slug = slugFromSddPath(progress_path);
27942
28078
  if (!slug)
27943
28079
  return jsonResult({ error: "could not derive slug — expected docs/<slug>/sdd/..." });
27944
- const gate = assertProductGates(workspace_root, slug, { requireMenu: true, requireDocs: true }, cursorMutationContext(workspace_root));
28080
+ const gate = assertSddControlGates(workspace_root, slug, { requireMenu: true, requireDocs: true }, cursorMutationContext(workspace_root));
27945
28081
  if (!gate.ok)
27946
28082
  return jsonResult({ error: gate.error, code: gate.code });
27947
28083
  const data = sddAppendProgress({ progress_path, line, workspace_root });
@@ -27949,6 +28085,26 @@ var init_server3 = __esm(async () => {
27949
28085
  return jsonResult({ error: data.error });
27950
28086
  return jsonResult(withWorkspace(workspace_root, data));
27951
28087
  });
28088
+ registerTool("workflow_sdd_append_advisory", {
28089
+ description: "Append a validated advisory line to docs/<slug>/sdd/advisories.md (coordinator-owned)",
28090
+ inputSchema: {
28091
+ advisories_path: exports_external.string(),
28092
+ task_id: exports_external.number(),
28093
+ text: exports_external.string(),
28094
+ workspace_root: workspaceRootSchema
28095
+ }
28096
+ }, async ({ advisories_path, task_id, text, workspace_root }) => {
28097
+ const slug = slugFromSddPath(advisories_path);
28098
+ if (!slug)
28099
+ return jsonResult({ error: "could not derive slug — expected docs/<slug>/sdd/..." });
28100
+ const gate = assertSddControlGates(workspace_root, slug, { requireMenu: true, requireDocs: true }, cursorMutationContext(workspace_root));
28101
+ if (!gate.ok)
28102
+ return jsonResult({ error: gate.error, code: gate.code });
28103
+ const data = sddAppendAdvisory({ advisories_path, task_id, text, workspace_root });
28104
+ if ("error" in data)
28105
+ return jsonResult({ error: data.error, code: data.code });
28106
+ return jsonResult(withWorkspace(workspace_root, data));
28107
+ });
27952
28108
  registerTool("workflow_docs_branch", {
27953
28109
  description: "Resolve branch for spec/plan authors: keep current feature|bugfix or create from the configured base.",
27954
28110
  inputSchema: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-cursor",
3
- "version": "0.8.8",
3
+ "version": "0.8.9",
4
4
  "private": false,
5
5
  "description": "Workit Cursor plugin: MCP server, hooks, rules, marketplace manifest (thin over @brainervirus/workit-core)",
6
6
  "keywords": [
@@ -39,7 +39,7 @@
39
39
  "build": "bun scripts/build.ts"
40
40
  },
41
41
  "dependencies": {
42
- "@brainervirus/workit-core": "^0.8.8",
42
+ "@brainervirus/workit-core": "^0.8.9",
43
43
  "@modelcontextprotocol/sdk": "^1.12.0",
44
44
  "zod": "^3.24.0"
45
45
  },