@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.
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
|
|
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
|
|
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) => {
|
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
|
24303
|
-
if (!
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 ? {
|
|
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
|
-
},
|
|
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
|
-
|
|
25285
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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) ?
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
42
|
+
"@brainervirus/workit-core": "^0.8.9",
|
|
43
43
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
44
44
|
"zod": "^3.24.0"
|
|
45
45
|
},
|