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