@brainervirus/workit-cursor 0.8.1 → 0.8.3
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/.cursor-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/assets/templates/execution-contract.md +11 -0
- package/assets/templates/superpowers-doc-contract.md +2 -0
- package/dist/cursor-session-start.js +121 -19
- package/dist/mcp-server.js +1219 -539
- package/package.json +2 -2
- package/skills/wk-implement/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -39,13 +39,13 @@ npx @brainervirus/workit-cli init
|
|
|
39
39
|
|
|
40
40
|
## What it provides
|
|
41
41
|
|
|
42
|
-
- MCP server exposing the `workflow_*` tools (branch setup, PR create/context, docs validate/promote, YouTrack post/log/time, templates, rules, presentation, doctor, handoff).
|
|
42
|
+
- MCP server exposing the `workflow_*` tools (branch setup, PR create/context, docs validate/promote, YouTrack post/log/time, templates, rules, presentation, doctor, handoff, and plan lifecycle `workflow_plan_pause`/`resume`/`complete`).
|
|
43
43
|
- Session-start contract hook.
|
|
44
44
|
- 4 rules and 12 `wk-*` skills (plus 14 sanitized Superpowers skills).
|
|
45
45
|
|
|
46
46
|
## Host limitations
|
|
47
47
|
|
|
48
|
-
Cursor adapts workit through policy-only confirmation: approvals and
|
|
48
|
+
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
49
|
|
|
50
50
|
## Configuration
|
|
51
51
|
|
|
@@ -5,6 +5,17 @@ Load `using-superpowers`, `subagent-driven-development`, `test-driven-developmen
|
|
|
5
5
|
**Branch:** <BRANCH>
|
|
6
6
|
**SDD:** `<SDD_DIR>`
|
|
7
7
|
|
|
8
|
+
## Handoff destination
|
|
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:
|
|
11
|
+
|
|
12
|
+
- Subagent-driven
|
|
13
|
+
- Inline
|
|
14
|
+
- Review spec first
|
|
15
|
+
- Review plan first
|
|
16
|
+
|
|
17
|
+
<workflow-handoff-destination>true</workflow-handoff-destination>
|
|
18
|
+
|
|
8
19
|
## Hard gates
|
|
9
20
|
|
|
10
21
|
- The parent is coordinator-only: it does not edit product code or perform delegated exploration.
|
|
@@ -63,6 +63,8 @@ On success, use native `question` / Cursor `AskQuestion` with exactly these opti
|
|
|
63
63
|
|
|
64
64
|
Never emit Superpowers text beginning “Two execution options”.
|
|
65
65
|
|
|
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.
|
|
67
|
+
|
|
66
68
|
- Specs/plans must follow `templates/spec-template.md` / `templates/plan-template.md` (mandated diagrams, tables, CA-XX).
|
|
67
69
|
|
|
68
70
|
## Doc delivery
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// packages/workit-cursor/hooks/session-start.ts
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import
|
|
4
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
5
|
+
import path3 from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
8
|
// packages/workit-core/src/core/logger.ts
|
|
@@ -277,35 +277,91 @@ var DEFAULTS = {
|
|
|
277
277
|
}
|
|
278
278
|
};
|
|
279
279
|
|
|
280
|
+
// packages/workit-core/src/core/menu.ts
|
|
281
|
+
import { existsSync, readFileSync, readdirSync as readdirSync2 } from "node:fs";
|
|
282
|
+
import path2 from "node:path";
|
|
283
|
+
var SOURCE_MENU_LABELS = [
|
|
284
|
+
"Subagent-driven",
|
|
285
|
+
"Inline",
|
|
286
|
+
"Handoff",
|
|
287
|
+
"Review spec first",
|
|
288
|
+
"Review plan first"
|
|
289
|
+
];
|
|
290
|
+
var DESTINATION_MENU_LABELS = [
|
|
291
|
+
"Subagent-driven",
|
|
292
|
+
"Inline",
|
|
293
|
+
"Review spec first",
|
|
294
|
+
"Review plan first"
|
|
295
|
+
];
|
|
296
|
+
var HANDOFF_DESTINATION_MARKER = "<workflow-handoff-destination>true</workflow-handoff-destination>";
|
|
297
|
+
var findMarkedDestinations = (root) => {
|
|
298
|
+
const docsDir = path2.join(root, "docs");
|
|
299
|
+
if (!existsSync(docsDir))
|
|
300
|
+
return [];
|
|
301
|
+
const slugs = [];
|
|
302
|
+
for (const slug of readdirSync2(docsDir)) {
|
|
303
|
+
const file = path2.join(docsDir, slug, "sdd", "flow.json");
|
|
304
|
+
if (!existsSync(file))
|
|
305
|
+
continue;
|
|
306
|
+
try {
|
|
307
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
308
|
+
if (parsed.handoff_destination === true)
|
|
309
|
+
slugs.push(slug);
|
|
310
|
+
} catch {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return slugs;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// packages/workit-core/src/core/reminder.ts
|
|
318
|
+
var SOURCE_MENU_LABELS_DISPLAY = SOURCE_MENU_LABELS.map((label) => label === "Handoff" ? "Handoff (new session only)" : label);
|
|
319
|
+
var REMINDER_TEXT = `<workflow-contract-reminder>
|
|
320
|
+
- Bounded user choices → call the native \`question\` tool (never A/B/C or 1/2/3 lists in prose).
|
|
321
|
+
- After a plan is approved → native \`question\` menu with exactly: ${SOURCE_MENU_LABELS_DISPLAY.join(", ")}.
|
|
322
|
+
- Tools with \`confirmed\` → call them; never fabricate their result.
|
|
323
|
+
- Before the first \`workflow_spec_approve\`/\`workflow_plan_approve\` (self-review) run the superpowers writing-plans Self-Review checklist: spec coverage (every spec requirement maps to a task), placeholder scan, type consistency; fix findings inline.
|
|
324
|
+
- Delivering docs → clickable markdown link \`[spec.md](docs/<slug>/spec.md)\` + 3-5 bullet summary.
|
|
325
|
+
</workflow-contract-reminder>`;
|
|
326
|
+
var DESTINATION_REMINDER_TEXT = `<workflow-contract-reminder>
|
|
327
|
+
- Bounded user choices → call the native \`question\` tool (never A/B/C or 1/2/3 lists in prose).
|
|
328
|
+
- This session is a handoff destination: present the post-plan menu with exactly: ${DESTINATION_MENU_LABELS.join(", ")}.
|
|
329
|
+
- Tools with \`confirmed\` → call them; never fabricate their result.
|
|
330
|
+
- Before the first \`workflow_spec_approve\`/\`workflow_plan_approve\` (self-review) run the superpowers writing-plans Self-Review checklist: spec coverage (every spec requirement maps to a task), placeholder scan, type consistency; fix findings inline.
|
|
331
|
+
- Delivering docs → clickable markdown link \`[spec.md](docs/<slug>/spec.md)\` + 3-5 bullet summary.
|
|
332
|
+
${HANDOFF_DESTINATION_MARKER}
|
|
333
|
+
</workflow-contract-reminder>`;
|
|
334
|
+
var reminderTextFor = (destination) => destination ? DESTINATION_REMINDER_TEXT : REMINDER_TEXT;
|
|
335
|
+
|
|
280
336
|
// packages/workit-cursor/hooks/session-start.ts
|
|
281
337
|
var logger = createLogger({
|
|
282
338
|
stderr: (event) => process.stderr.write(`${JSON.stringify(event)}
|
|
283
339
|
`)
|
|
284
340
|
});
|
|
285
|
-
var hookDir =
|
|
286
|
-
var pluginDir =
|
|
287
|
-
var marker =
|
|
341
|
+
var hookDir = path3.dirname(fileURLToPath(import.meta.url));
|
|
342
|
+
var pluginDir = path3.resolve(hookDir, "..");
|
|
343
|
+
var marker = path3.join(pluginDir, ".workflow-toolkit-root");
|
|
288
344
|
setDiagnosticLogger(logger);
|
|
289
345
|
logger.info(EVENT.initialization, { host: "cursor-hook", hook_dir: hookDir });
|
|
290
346
|
var resolveRepoRoot = () => {
|
|
291
|
-
if (process.env.WORKFLOW_TOOLKIT_ROOT &&
|
|
347
|
+
if (process.env.WORKFLOW_TOOLKIT_ROOT && existsSync2(path3.join(process.env.WORKFLOW_TOOLKIT_ROOT, "templates"))) {
|
|
292
348
|
return process.env.WORKFLOW_TOOLKIT_ROOT;
|
|
293
349
|
}
|
|
294
|
-
if (
|
|
295
|
-
return
|
|
350
|
+
if (existsSync2(marker)) {
|
|
351
|
+
return readFileSync2(marker, "utf8").replace(/\n+$/, "");
|
|
296
352
|
}
|
|
297
|
-
const ownAssets =
|
|
298
|
-
if (
|
|
353
|
+
const ownAssets = path3.join(pluginDir, "assets");
|
|
354
|
+
if (existsSync2(path3.join(ownAssets, "templates"))) {
|
|
299
355
|
return ownAssets;
|
|
300
356
|
}
|
|
301
|
-
return
|
|
357
|
+
return path3.resolve(hookDir, "../../workit-core");
|
|
302
358
|
};
|
|
303
359
|
var body = null;
|
|
304
360
|
var repoRoot = resolveRepoRoot();
|
|
305
361
|
try {
|
|
306
|
-
const contract =
|
|
307
|
-
if (
|
|
308
|
-
body =
|
|
362
|
+
const contract = path3.join(repoRoot, "templates", "superpowers-doc-contract.md");
|
|
363
|
+
if (existsSync2(contract))
|
|
364
|
+
body = readFileSync2(contract, "utf8");
|
|
309
365
|
} catch (err) {
|
|
310
366
|
logger.error(EVENT.hooks, { boundary: "session-start", root: repoRoot, ...errorDetail(err) });
|
|
311
367
|
body = null;
|
|
@@ -315,7 +371,45 @@ if (body === null) {
|
|
|
315
371
|
`);
|
|
316
372
|
process.exit(0);
|
|
317
373
|
}
|
|
318
|
-
var
|
|
374
|
+
var HOOK_READ_TIMEOUT_MS = Number(process.env.WORKFLOW_HOOK_READ_TIMEOUT_MS ?? "2000");
|
|
375
|
+
var readHookInput = (timeoutMs) => {
|
|
376
|
+
if (process.stdin.isTTY)
|
|
377
|
+
return Promise.resolve({});
|
|
378
|
+
return new Promise((resolve) => {
|
|
379
|
+
let buffer = "";
|
|
380
|
+
let settled = false;
|
|
381
|
+
const timer = setTimeout(() => settle({}), timeoutMs);
|
|
382
|
+
const onData = (chunk) => {
|
|
383
|
+
buffer += String(chunk);
|
|
384
|
+
const text = buffer.trim();
|
|
385
|
+
if (!text)
|
|
386
|
+
return;
|
|
387
|
+
try {
|
|
388
|
+
const parsed = JSON.parse(text);
|
|
389
|
+
settle(typeof parsed === "object" && parsed !== null ? parsed : {});
|
|
390
|
+
} catch {}
|
|
391
|
+
};
|
|
392
|
+
const onEnd = () => settle({});
|
|
393
|
+
function settle(value) {
|
|
394
|
+
if (settled)
|
|
395
|
+
return;
|
|
396
|
+
settled = true;
|
|
397
|
+
clearTimeout(timer);
|
|
398
|
+
process.stdin.off("data", onData);
|
|
399
|
+
process.stdin.off("end", onEnd);
|
|
400
|
+
resolve(value);
|
|
401
|
+
}
|
|
402
|
+
process.stdin.on("data", onData);
|
|
403
|
+
process.stdin.on("end", onEnd);
|
|
404
|
+
});
|
|
405
|
+
};
|
|
406
|
+
var isDestination = (input) => {
|
|
407
|
+
const roots = Array.isArray(input.workspace_roots) ? input.workspace_roots : [];
|
|
408
|
+
return roots.some((root) => typeof root === "string" && findMarkedDestinations(root).length > 0);
|
|
409
|
+
};
|
|
410
|
+
var main = async () => {
|
|
411
|
+
const reminder = reminderTextFor(isDestination(await readHookInput(HOOK_READ_TIMEOUT_MS)));
|
|
412
|
+
const context = `<workflow-toolkit-askquestion-hard-gate>
|
|
319
413
|
HARD-GATE: Any user choice with options → call Cursor AskQuestion directly with workflow-specific copy. NEVER A/B/C in chat. Overrides Superpowers brainstorming conversational options.
|
|
320
414
|
</workflow-toolkit-askquestion-hard-gate>
|
|
321
415
|
|
|
@@ -340,12 +434,20 @@ ${body}
|
|
|
340
434
|
</workflow-toolkit-superpowers-doc-contract>
|
|
341
435
|
|
|
342
436
|
<workflow-toolkit-reminder>
|
|
343
|
-
HARD-GATE: Bounded user choices → call Cursor AskQuestion directly (never A/B/C or 1/2/3 lists in prose).
|
|
344
|
-
|
|
437
|
+
HARD-GATE: Bounded user choices → call Cursor AskQuestion directly (never A/B/C or 1/2/3 lists in prose).
|
|
438
|
+
|
|
439
|
+
${reminder}
|
|
345
440
|
</workflow-toolkit-reminder>`;
|
|
346
|
-
process.stdout.write(JSON.stringify({ additional_context: context }, null, 2) + `
|
|
441
|
+
process.stdout.write(JSON.stringify({ additional_context: context }, null, 2) + `
|
|
347
442
|
`);
|
|
348
|
-
process.exit(0);
|
|
443
|
+
process.exit(0);
|
|
444
|
+
};
|
|
445
|
+
main().catch((err) => {
|
|
446
|
+
logger.error(EVENT.hooks, { boundary: "session-start", ...errorDetail(err) });
|
|
447
|
+
process.stdout.write(`{}
|
|
448
|
+
`);
|
|
449
|
+
process.exit(0);
|
|
450
|
+
});
|
|
349
451
|
export {
|
|
350
452
|
logger
|
|
351
453
|
};
|