@brainervirus/workit-opencode 0.8.8 → 0.8.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Requirements: OpenCode ≥ 1.15.0, Node ≥ 22 (the published plugin is a self-c
|
|
|
33
33
|
|
|
34
34
|
- **Approvals** — flow gates (`workflow_spec_approve` / `workflow_plan_approve` / `workflow_plan_menu`) record native `question` receipts; the self-review validation runs automatically during the transition. Approvals bind to the document's exact SHA-256 digest: editing an approved spec/plan invalidates the approval and forces a fresh reapproval.
|
|
35
35
|
- **Lifecycle** — `workflow_plan_pause` / `workflow_plan_resume` / `workflow_plan_complete` move a plan through `pending`/`active`/`paused`/`completed`, each gated by a one-use native-question receipt; completion requires a complete SDD ledger and passing repository verification.
|
|
36
|
-
- **Delegation** — approved plans execute through subagent-driven task delegation (native `task`); while a subagent-driven plan is active, coordinator product edits are intercepted.
|
|
36
|
+
- **Delegation** — approved plans execute through subagent-driven task delegation (native `task`); while a subagent-driven plan is active, coordinator product edits are intercepted. Receipts are purpose-bound: each gate consumes the newest fresh receipt for exactly its purpose, so unrelated questions never authorize or mask a gate. Delegated authority is direct-child-only — authorized workers receive compact worker-only context, while mismatched session lineage is denied (`delegation_lineage_denied`).
|
|
37
37
|
- **Commit** — `wk-commit` previews a Conventional Commit and confirms through a native `question`.
|
|
38
38
|
- **Handoff** — `wk-handoff` seeds and spawns a native OpenCode continuation session; a destination session presents a four-choice menu (never the originating Handoff option) and carries the handoff-destination marker.
|
|
39
39
|
- **Diagnostics** — durable JSONL journal plus native `client.app.log()`; nothing is mirrored to `process.stderr` or the agent conversation.
|
|
@@ -15,4 +15,6 @@ disable-model-invocation: true
|
|
|
15
15
|
7. After any `workflow_handoff_session` result—success, partial, or failure—end the originating turn immediately after one status message. Never create todos, execute the plan inline, modify files, retry handoff, or call another tool.
|
|
16
16
|
8. A destination run that executes the plan must still end with `workflow_plan_complete` (or the CLI `workit flow complete`) once the SDD ledger is complete and repository verification passes, and never finish the run while the plan is still `active`.
|
|
17
17
|
|
|
18
|
+
Handoff titles continuation sessions `Workit: <slug>` (never `Continue <slug>`). OpenCode's native `Continue opencode -s <session-id>` epilogue is the valid manual recovery command when host selection is unavailable (`stage: "select"`): `selected: false` with a `sessionID` is a partial success — use `opencode -s <session-id>` to resume, not a Workit bug.
|
|
19
|
+
|
|
18
20
|
Never emit a continuation prompt, use the clipboard, or ask the user to copy text. If selected, report the session ID only if the current session remains visible. If staying, report the seeded session ID. On failure, report `stage` and `error`; preserve any returned session for the session picker and never recreate it automatically. `todowrite` and `task` are unnecessary here.
|
|
@@ -31,13 +31,15 @@ For every plan task whose ID is absent from `completed_task_ids`:
|
|
|
31
31
|
3. Use `task` with the built-in `explore` agent for read-only discovery when needed, then a fresh built-in `general` agent to implement from the brief. The parent remains coordinator-only.
|
|
32
32
|
4. Require product changes to follow TDD: failing check first, minimal implementation, passing focused check.
|
|
33
33
|
5. Create the review package with `workflow_sdd_review_package` using `confirmed: true`.
|
|
34
|
-
6. Dispatch separate `general` agents for spec-compliance review and code-quality review. **Blocking findings** (Critical, Important, or spec-compliance) may trigger at most **two** fix+re-review rounds per task. **Advisory** findings (Minor, style, YAGNI, taste) never pause the loop — append them
|
|
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
|
|
@@ -3868,6 +3873,7 @@ import {
|
|
|
3868
3873
|
fsyncSync,
|
|
3869
3874
|
mkdirSync as mkdirSync9,
|
|
3870
3875
|
openSync,
|
|
3876
|
+
readdirSync as readdirSync6,
|
|
3871
3877
|
readFileSync as readFileSync16,
|
|
3872
3878
|
renameSync as renameSync2,
|
|
3873
3879
|
rmSync as rmSync2,
|
|
@@ -4582,6 +4588,58 @@ function sddAppendProgress({
|
|
|
4582
4588
|
const rel = posix2(path19.relative(contained.base, path_));
|
|
4583
4589
|
return { ok: true, line: trimmed, progress_path: rel };
|
|
4584
4590
|
}
|
|
4591
|
+
function sddAppendAdvisory({
|
|
4592
|
+
advisories_path,
|
|
4593
|
+
task_id,
|
|
4594
|
+
text,
|
|
4595
|
+
workspace_root
|
|
4596
|
+
}) {
|
|
4597
|
+
if (typeof task_id !== "number" || !Number.isSafeInteger(task_id) || task_id <= 0) {
|
|
4598
|
+
return { error: "task_id must be a positive safe integer", code: "advisory_task_invalid" };
|
|
4599
|
+
}
|
|
4600
|
+
if (typeof text !== "string") {
|
|
4601
|
+
return {
|
|
4602
|
+
error: "advisory text must be a string of 1-1000 characters after normalization",
|
|
4603
|
+
code: "advisory_text_invalid"
|
|
4604
|
+
};
|
|
4605
|
+
}
|
|
4606
|
+
if (text.includes("\r") || text.includes(`
|
|
4607
|
+
`)) {
|
|
4608
|
+
return {
|
|
4609
|
+
error: "advisory text must be a single line (no CR/LF)",
|
|
4610
|
+
code: "advisory_text_invalid"
|
|
4611
|
+
};
|
|
4612
|
+
}
|
|
4613
|
+
const collapsed = text.trim().replace(/[ \t]+/g, " ");
|
|
4614
|
+
if (collapsed.length === 0 || collapsed.length > 1000) {
|
|
4615
|
+
return {
|
|
4616
|
+
error: "advisory text must be 1-1000 characters after trim and horizontal-space collapse",
|
|
4617
|
+
code: "advisory_text_invalid"
|
|
4618
|
+
};
|
|
4619
|
+
}
|
|
4620
|
+
if (!/^docs\/[^/]+\/sdd\/advisories\.md$/.test(advisories_path)) {
|
|
4621
|
+
return {
|
|
4622
|
+
error: `advisories_path must be docs/<slug>/sdd/advisories.md: ${advisories_path}`,
|
|
4623
|
+
code: "advisory_path_invalid"
|
|
4624
|
+
};
|
|
4625
|
+
}
|
|
4626
|
+
const contained = resolveDocsPath({ workspace_root, path: advisories_path });
|
|
4627
|
+
if (!contained.ok)
|
|
4628
|
+
return { error: contained.error, code: "advisory_path_invalid" };
|
|
4629
|
+
const abs = contained.path;
|
|
4630
|
+
if (existsSync12(abs) && statSync5(abs).isDirectory()) {
|
|
4631
|
+
return {
|
|
4632
|
+
error: `advisory target is a directory: ${advisories_path}`,
|
|
4633
|
+
code: "advisory_target_invalid"
|
|
4634
|
+
};
|
|
4635
|
+
}
|
|
4636
|
+
mkdirSync8(path19.dirname(abs), { recursive: true });
|
|
4637
|
+
const line = `- Task ${task_id}: ${collapsed}
|
|
4638
|
+
`;
|
|
4639
|
+
appendFileSync(abs, line, "utf8");
|
|
4640
|
+
const rel = posix2(path19.relative(contained.base, abs));
|
|
4641
|
+
return { ok: true, advisory: collapsed, advisories_path: rel };
|
|
4642
|
+
}
|
|
4585
4643
|
|
|
4586
4644
|
// packages/workit-core/src/core/verify-project.ts
|
|
4587
4645
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
@@ -5193,7 +5251,7 @@ root: ${cwd}
|
|
|
5193
5251
|
}
|
|
5194
5252
|
|
|
5195
5253
|
// packages/workit-core/src/core/flow-state.ts
|
|
5196
|
-
var COORDINATOR_RECOVERY_TEXT = "A subagent-driven plan is active: coordinator product edits are blocked. " + "Delegate product mutations
|
|
5254
|
+
var COORDINATOR_RECOVERY_TEXT = "A subagent-driven plan is active: coordinator product edits are blocked. " + "Delegate product mutations to an authenticated delegated worker via `task` / `wk-implement` instead of " + "editing in the coordinator session.";
|
|
5197
5255
|
var CURSOR_SUBAGENT_UNSUPPORTED_TEXT = "Cursor cannot execute subagent-driven plans: the MCP has no child-session " + "support. Choose Inline, Handoff, or a review option in this session, or " + "run the plan in OpenCode with `wk-implement`.";
|
|
5198
5256
|
var MENU_CHOICES = [
|
|
5199
5257
|
"subagent-driven",
|
|
@@ -5253,7 +5311,8 @@ var normalizeState = (parsed, slug) => {
|
|
|
5253
5311
|
execution: {
|
|
5254
5312
|
status: execution.status ?? "pending",
|
|
5255
5313
|
mode: execution.mode ?? null,
|
|
5256
|
-
evidence: execution.evidence ?? null
|
|
5314
|
+
evidence: execution.evidence ?? null,
|
|
5315
|
+
coordinator_session_id: execution.coordinator_session_id ?? null
|
|
5257
5316
|
},
|
|
5258
5317
|
handoff_destination: p.handoff_destination ?? false,
|
|
5259
5318
|
updated_at: p.updated_at ?? Date.now()
|
|
@@ -5265,7 +5324,7 @@ var emptyState = (slug) => ({
|
|
|
5265
5324
|
spec: { path: "", status: "draft", evidence: null, approved_digest: null },
|
|
5266
5325
|
plan: { path: "", status: "draft", evidence: null, approved_digest: null },
|
|
5267
5326
|
menu: { presented: false, chosen: "", evidence: null },
|
|
5268
|
-
execution: { status: "pending", mode: null, evidence: null },
|
|
5327
|
+
execution: { status: "pending", mode: null, evidence: null, coordinator_session_id: null },
|
|
5269
5328
|
handoff_destination: false,
|
|
5270
5329
|
updated_at: Date.now()
|
|
5271
5330
|
});
|
|
@@ -5381,6 +5440,12 @@ var validateState = (parsed, slug) => {
|
|
|
5381
5440
|
if (execRaw?.evidence !== undefined && !validateEvidenceValue(execRaw.evidence, true)) {
|
|
5382
5441
|
return { ok: false, error: "flow state execution.evidence has an unsupported shape" };
|
|
5383
5442
|
}
|
|
5443
|
+
if (execRaw?.coordinator_session_id !== undefined && execRaw.coordinator_session_id !== null && typeof execRaw.coordinator_session_id !== "string") {
|
|
5444
|
+
return {
|
|
5445
|
+
ok: false,
|
|
5446
|
+
error: "flow state execution.coordinator_session_id must be a string or null"
|
|
5447
|
+
};
|
|
5448
|
+
}
|
|
5384
5449
|
return {
|
|
5385
5450
|
ok: true,
|
|
5386
5451
|
state: {
|
|
@@ -5396,7 +5461,8 @@ var validateState = (parsed, slug) => {
|
|
|
5396
5461
|
execution: {
|
|
5397
5462
|
status: execRaw?.status ?? "pending",
|
|
5398
5463
|
mode: execRaw?.mode ?? null,
|
|
5399
|
-
evidence: execRaw?.evidence ?? null
|
|
5464
|
+
evidence: execRaw?.evidence ?? null,
|
|
5465
|
+
coordinator_session_id: execRaw?.coordinator_session_id ?? null
|
|
5400
5466
|
},
|
|
5401
5467
|
handoff_destination: parsed.handoff_destination ?? false,
|
|
5402
5468
|
updated_at: parsed.updated_at ?? Date.now()
|
|
@@ -5533,7 +5599,7 @@ var resetForSpecDrift = (state) => ({
|
|
|
5533
5599
|
spec: { ...state.spec, status: "draft", evidence: null, approved_digest: null },
|
|
5534
5600
|
plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
|
|
5535
5601
|
menu: { presented: false, chosen: "", evidence: null },
|
|
5536
|
-
execution: { status: "pending", mode: null, evidence: null },
|
|
5602
|
+
execution: { status: "pending", mode: null, evidence: null, coordinator_session_id: null },
|
|
5537
5603
|
handoff_destination: false,
|
|
5538
5604
|
updated_at: Date.now()
|
|
5539
5605
|
});
|
|
@@ -5576,9 +5642,14 @@ var reconcileState = (root2, slug, state) => {
|
|
|
5576
5642
|
var deriveLegacyExecution = (root2, slug, state) => {
|
|
5577
5643
|
const ledger = ledgerCompletion(root2, slug);
|
|
5578
5644
|
if (state.plan.status === "approved" && state.menu.chosen === "subagent-driven" && ledger.started && !ledger.complete) {
|
|
5579
|
-
return {
|
|
5645
|
+
return {
|
|
5646
|
+
status: "active",
|
|
5647
|
+
mode: "subagent-driven",
|
|
5648
|
+
evidence: null,
|
|
5649
|
+
coordinator_session_id: null
|
|
5650
|
+
};
|
|
5580
5651
|
}
|
|
5581
|
-
return { status: "pending", mode: null, evidence: null };
|
|
5652
|
+
return { status: "pending", mode: null, evidence: null, coordinator_session_id: null };
|
|
5582
5653
|
};
|
|
5583
5654
|
var normalizeCompatibility = (root2, slug, parsed, state) => {
|
|
5584
5655
|
if (!isRecord(parsed) || !("execution" in parsed)) {
|
|
@@ -5587,6 +5658,11 @@ var normalizeCompatibility = (root2, slug, parsed, state) => {
|
|
|
5587
5658
|
if (derived.status !== current.status || derived.mode !== current.mode) {
|
|
5588
5659
|
return { state: { ...state, execution: derived, updated_at: Date.now() }, changed: true };
|
|
5589
5660
|
}
|
|
5661
|
+
return { state, changed: false };
|
|
5662
|
+
}
|
|
5663
|
+
const execRaw = isRecord(parsed.execution) ? parsed.execution : undefined;
|
|
5664
|
+
if (execRaw && !("coordinator_session_id" in execRaw)) {
|
|
5665
|
+
return { state: { ...state, updated_at: Date.now() }, changed: true };
|
|
5590
5666
|
}
|
|
5591
5667
|
return { state, changed: false };
|
|
5592
5668
|
};
|
|
@@ -5721,15 +5797,6 @@ var assertMutationWorkspace = (root2, ctx) => {
|
|
|
5721
5797
|
}
|
|
5722
5798
|
return { ok: true };
|
|
5723
5799
|
};
|
|
5724
|
-
var assertCoordinatorBoundary = (ctx, state) => {
|
|
5725
|
-
if (ctx?.role === "coordinator" && state.execution.status === "active" && state.execution.mode === "subagent-driven") {
|
|
5726
|
-
return err2("coordinator_blocked", COORDINATOR_RECOVERY_TEXT);
|
|
5727
|
-
}
|
|
5728
|
-
if (ctx?.role === "delegated" && !ctx.taskIdentity) {
|
|
5729
|
-
return err2("delegated_unauthenticated", "delegated mutations require an authenticated task identity (taskIdentity) — re-run inside the delegated worker session");
|
|
5730
|
-
}
|
|
5731
|
-
return { ok: true };
|
|
5732
|
-
};
|
|
5733
5800
|
var MAX_CLOCK_SKEW_MS = 60000;
|
|
5734
5801
|
var MAX_RECEIPTS_PER_SESSION = 10;
|
|
5735
5802
|
var RECEIPT_FRESHNESS_MS = 10 * 60 * 1000;
|
|
@@ -5758,19 +5825,49 @@ var isNegativeLabel = (label) => {
|
|
|
5758
5825
|
return firstWord.replace(/[^a-z]/g, "") === entry;
|
|
5759
5826
|
});
|
|
5760
5827
|
};
|
|
5828
|
+
var receiptPurposeForLabel = (label) => {
|
|
5829
|
+
const n = normalizeLabel(label);
|
|
5830
|
+
if (n === "approve spec" || n === "approve spec recommended")
|
|
5831
|
+
return "spec-approval";
|
|
5832
|
+
if (n === "approve plan" || n === "approve plan recommended")
|
|
5833
|
+
return "plan-approval";
|
|
5834
|
+
if (n === "approve")
|
|
5835
|
+
return;
|
|
5836
|
+
if (n === "pause plan")
|
|
5837
|
+
return "plan-pause";
|
|
5838
|
+
if (n === "resume plan")
|
|
5839
|
+
return "plan-resume";
|
|
5840
|
+
if (n === "complete plan")
|
|
5841
|
+
return "plan-complete";
|
|
5842
|
+
const exec = new Set([
|
|
5843
|
+
"subagent driven",
|
|
5844
|
+
"inline",
|
|
5845
|
+
"handoff",
|
|
5846
|
+
"review spec",
|
|
5847
|
+
"review plan",
|
|
5848
|
+
"change model"
|
|
5849
|
+
]);
|
|
5850
|
+
if (exec.has(n))
|
|
5851
|
+
return "execution-menu";
|
|
5852
|
+
return;
|
|
5853
|
+
};
|
|
5761
5854
|
|
|
5762
5855
|
class HostReceiptStore {
|
|
5763
5856
|
#bySession = new Map;
|
|
5764
|
-
record(sessionId, callID, selectedLabel, recordedAt = Date.now(), question) {
|
|
5765
|
-
const
|
|
5766
|
-
if (!
|
|
5857
|
+
record(sessionId, callID, selectedLabel, recordedAt = Date.now(), question = "", purpose) {
|
|
5858
|
+
const trimmed = selectedLabel.trim();
|
|
5859
|
+
if (!trimmed)
|
|
5767
5860
|
return;
|
|
5768
5861
|
if (recordedAt > Date.now() + MAX_CLOCK_SKEW_MS)
|
|
5769
5862
|
return;
|
|
5863
|
+
const derived = purpose ?? receiptPurposeForLabel(selectedLabel);
|
|
5864
|
+
if (derived === undefined)
|
|
5865
|
+
return;
|
|
5866
|
+
const q = question ?? "";
|
|
5770
5867
|
const queue = this.#bySession.get(sessionId) ?? [];
|
|
5771
5868
|
if (queue.length >= MAX_RECEIPTS_PER_SESSION)
|
|
5772
5869
|
queue.shift();
|
|
5773
|
-
queue.push({ sessionId, callID, selectedLabel
|
|
5870
|
+
queue.push({ sessionId, callID, selectedLabel, recordedAt, question: q, purpose: derived });
|
|
5774
5871
|
this.#bySession.set(sessionId, queue);
|
|
5775
5872
|
}
|
|
5776
5873
|
count(sessionId) {
|
|
@@ -5787,10 +5884,40 @@ class HostReceiptStore {
|
|
|
5787
5884
|
if (!queue || queue.length === 0) {
|
|
5788
5885
|
return err2("receipt_missing", "no host-observed native-question receipt for this session — ask the native " + "`question` tool and have the user answer before calling this tool");
|
|
5789
5886
|
}
|
|
5790
|
-
|
|
5887
|
+
let index = -1;
|
|
5888
|
+
if (opts.purpose !== undefined) {
|
|
5889
|
+
const top = queue[queue.length - 1];
|
|
5890
|
+
if (top && top.purpose === opts.purpose && isNegativeLabel(top.selectedLabel)) {
|
|
5891
|
+
const filtered = queue.filter((r) => r.purpose !== opts.purpose);
|
|
5892
|
+
if (filtered.length === 0)
|
|
5893
|
+
this.#bySession.delete(sessionId);
|
|
5894
|
+
else
|
|
5895
|
+
this.#bySession.set(sessionId, filtered);
|
|
5896
|
+
return err2("receipt_rejected", `the user's most recent answer (${JSON.stringify(top.selectedLabel)}) is a ` + "negative answer — it cannot authorize an approval; ask the native question again");
|
|
5897
|
+
}
|
|
5898
|
+
for (let i = queue.length - 1;i >= 0; i--) {
|
|
5899
|
+
if (queue[i].purpose === opts.purpose) {
|
|
5900
|
+
index = i;
|
|
5901
|
+
break;
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5904
|
+
if (index === -1) {
|
|
5905
|
+
return err2("receipt_missing", `no host-observed receipt for purpose ${JSON.stringify(opts.purpose)} — ask the native question for that purpose`);
|
|
5906
|
+
}
|
|
5907
|
+
} else {
|
|
5908
|
+
index = queue.length - 1;
|
|
5909
|
+
}
|
|
5791
5910
|
const receipt = queue[index];
|
|
5792
5911
|
if (isNegativeLabel(receipt.selectedLabel)) {
|
|
5793
|
-
|
|
5912
|
+
if (opts.purpose !== undefined) {
|
|
5913
|
+
const filtered = queue.filter((r) => r.purpose !== opts.purpose);
|
|
5914
|
+
if (filtered.length === 0)
|
|
5915
|
+
this.#bySession.delete(sessionId);
|
|
5916
|
+
else
|
|
5917
|
+
this.#bySession.set(sessionId, filtered);
|
|
5918
|
+
} else {
|
|
5919
|
+
this.#bySession.delete(sessionId);
|
|
5920
|
+
}
|
|
5794
5921
|
return err2("receipt_rejected", `the user's most recent answer (${JSON.stringify(receipt.selectedLabel)}) is a ` + "negative answer — it cannot authorize an approval; ask the native question again");
|
|
5795
5922
|
}
|
|
5796
5923
|
if (opts.label !== undefined && !sameChoiceLabel(receipt.selectedLabel, opts.label)) {
|
|
@@ -5924,7 +6051,7 @@ var prepareFlowState = (root2, slug, opts = {}, ctx) => {
|
|
|
5924
6051
|
spec: { path: specPath, status: "draft", evidence: null, approved_digest: null },
|
|
5925
6052
|
plan: { path: planPath, status: "draft", evidence: null, approved_digest: null },
|
|
5926
6053
|
menu: { presented: false, chosen: "", evidence: null },
|
|
5927
|
-
execution: { status: "pending", mode: null, evidence: null },
|
|
6054
|
+
execution: { status: "pending", mode: null, evidence: null, coordinator_session_id: null },
|
|
5928
6055
|
handoff_destination: false,
|
|
5929
6056
|
updated_at: Date.now()
|
|
5930
6057
|
});
|
|
@@ -6070,13 +6197,24 @@ var recordMenuChoice = (root2, slug, planPath, choice, evidence, ctx) => {
|
|
|
6070
6197
|
return err2("recursive_handoff", "this flow is already a handoff destination — a second handoff is rejected");
|
|
6071
6198
|
}
|
|
6072
6199
|
const executing = choice === "subagent-driven" || choice === "inline";
|
|
6200
|
+
const coordinatorSessionId = choice === "subagent-driven" && recorded.evidence.host === "opencode" ? ctx?.sessionId ?? null : null;
|
|
6073
6201
|
return {
|
|
6074
6202
|
ok: true,
|
|
6075
6203
|
next: {
|
|
6076
6204
|
...state,
|
|
6077
6205
|
plan: { ...state.plan, path: state.plan.path || `docs/${slug}/plan.md` },
|
|
6078
6206
|
menu: { presented: true, chosen: choice, evidence: recorded.evidence },
|
|
6079
|
-
execution: executing ? {
|
|
6207
|
+
execution: executing ? {
|
|
6208
|
+
status: "active",
|
|
6209
|
+
mode: choice,
|
|
6210
|
+
evidence: recorded.evidence,
|
|
6211
|
+
coordinator_session_id: coordinatorSessionId
|
|
6212
|
+
} : {
|
|
6213
|
+
status: "pending",
|
|
6214
|
+
mode: null,
|
|
6215
|
+
evidence: recorded.evidence,
|
|
6216
|
+
coordinator_session_id: null
|
|
6217
|
+
},
|
|
6080
6218
|
updated_at: Date.now()
|
|
6081
6219
|
}
|
|
6082
6220
|
};
|
|
@@ -6170,7 +6308,7 @@ var completeExecution = (root2, slug, deps) => {
|
|
|
6170
6308
|
}
|
|
6171
6309
|
const next = {
|
|
6172
6310
|
...reconciled.state,
|
|
6173
|
-
execution: { ...exec, status: "completed" },
|
|
6311
|
+
execution: { ...exec, status: "completed", coordinator_session_id: null },
|
|
6174
6312
|
handoff_destination: false,
|
|
6175
6313
|
updated_at: Date.now()
|
|
6176
6314
|
};
|
|
@@ -6234,7 +6372,7 @@ var slugFromSddPath = (p) => {
|
|
|
6234
6372
|
const match = p.split(path22.sep).join("/").match(/^docs\/([^/]+)\/sdd(\/|$|['"])/);
|
|
6235
6373
|
return match?.[1] ?? "";
|
|
6236
6374
|
};
|
|
6237
|
-
var
|
|
6375
|
+
var assertHandoffReady = (root2, planPath) => {
|
|
6238
6376
|
const doc = resolveDoc(root2, "", planPath, "plan");
|
|
6239
6377
|
if (!doc.ok)
|
|
6240
6378
|
return err2("path_invalid", doc.error);
|
|
@@ -6249,12 +6387,15 @@ var assertFlowGates = (root2, planPath, opts = {}) => {
|
|
|
6249
6387
|
if (state.plan.status !== "approved") {
|
|
6250
6388
|
return err2("plan_not_approved", `plan not approved (status: ${state.plan.status}). Run workflow_plan_approve after the user's approval.`);
|
|
6251
6389
|
}
|
|
6252
|
-
if (
|
|
6253
|
-
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})`);
|
|
6254
6395
|
}
|
|
6255
6396
|
return { ok: true };
|
|
6256
6397
|
};
|
|
6257
|
-
var
|
|
6398
|
+
var assertSddControlGates = (root2, slug, opts = {}, ctx) => {
|
|
6258
6399
|
const bound = assertMutationWorkspace(root2, ctx);
|
|
6259
6400
|
if (!bound.ok)
|
|
6260
6401
|
return bound;
|
|
@@ -6280,9 +6421,12 @@ var assertProductGates = (root2, slug, opts = {}, ctx) => {
|
|
|
6280
6421
|
if (validated.ok === false)
|
|
6281
6422
|
return err2("docs_invalid", validated.error);
|
|
6282
6423
|
}
|
|
6283
|
-
|
|
6424
|
+
if (state.execution.status === "active" && state.execution.mode === "subagent-driven" && roleFromParentage(ctx?.parentSessionId, state.execution.coordinator_session_id) === "delegated") {
|
|
6425
|
+
return err2("sdd_control_denied", "SDD control metadata is coordinator-owned while a subagent-driven plan is active — delegated workers cannot mutate task briefs, review packages, progress, or advisories");
|
|
6426
|
+
}
|
|
6427
|
+
return { ok: true };
|
|
6284
6428
|
};
|
|
6285
|
-
var roleFromParentage = (parentID) => parentID ? "delegated" : "coordinator";
|
|
6429
|
+
var roleFromParentage = (parentID, coordinatorSessionId) => typeof parentID === "string" && parentID !== "" && parentID === coordinatorSessionId ? "delegated" : "coordinator";
|
|
6286
6430
|
var COORDINATOR_WRITE_TOOLS = [
|
|
6287
6431
|
"write",
|
|
6288
6432
|
"edit",
|
|
@@ -6307,9 +6451,6 @@ var COORDINATOR_WRITE_TOOLS = [
|
|
|
6307
6451
|
"workflow_docs_promote",
|
|
6308
6452
|
"workflow_docs_layout",
|
|
6309
6453
|
"workflow_docs_repo_link",
|
|
6310
|
-
"workflow_sdd_task_brief",
|
|
6311
|
-
"workflow_sdd_review_package",
|
|
6312
|
-
"workflow_sdd_append_progress",
|
|
6313
6454
|
"workflow_youtrack_post",
|
|
6314
6455
|
"workflow_youtrack_log_time"
|
|
6315
6456
|
];
|
|
@@ -6614,10 +6755,34 @@ var isCoordinatorBashAllowed = (command) => {
|
|
|
6614
6755
|
};
|
|
6615
6756
|
var COORDINATOR_SHELL_DENIED_TEXT = "Coordinator shell commands are restricted while a subagent-driven plan is " + "active: only bounded read/test/review commands are allowed (the exact " + "allowlist is in flow-state.ts, isCoordinatorBashAllowed). " + COORDINATOR_RECOVERY_TEXT;
|
|
6616
6757
|
var subagentDrivenInterception = (input) => {
|
|
6617
|
-
|
|
6758
|
+
const ids = Array.from(new Set((input.activeCoordinatorIds ?? []).filter((id) => typeof id === "string" && id !== "")));
|
|
6759
|
+
const parent = typeof input.parentID === "string" && input.parentID !== "" ? input.parentID : null;
|
|
6760
|
+
const legacyActive = input.active === true;
|
|
6761
|
+
if (!legacyActive && ids.length === 0)
|
|
6618
6762
|
return { ok: true };
|
|
6619
|
-
if (
|
|
6763
|
+
if (parent !== null && ids.length === 1 && ids[0] === parent) {
|
|
6764
|
+
if (input.tool === "bash") {
|
|
6765
|
+
const tokens = (input.command ?? "").split(/[\s'"]+/).filter(Boolean);
|
|
6766
|
+
const launchesOpencode = tokens.some((t) => t.split("/").pop() === "opencode");
|
|
6767
|
+
if (launchesOpencode) {
|
|
6768
|
+
return err2("delegation_lineage_denied", "nested opencode launch is denied while a subagent-driven plan is active");
|
|
6769
|
+
}
|
|
6770
|
+
}
|
|
6771
|
+
if ([
|
|
6772
|
+
"workflow_sdd_task_brief",
|
|
6773
|
+
"workflow_sdd_review_package",
|
|
6774
|
+
"workflow_sdd_append_progress",
|
|
6775
|
+
"workflow_sdd_append_advisory"
|
|
6776
|
+
].includes(input.tool)) {
|
|
6777
|
+
return err2("delegation_lineage_denied", "SDD control metadata is coordinator-owned — workers execute briefs, not bookkeeping");
|
|
6778
|
+
}
|
|
6620
6779
|
return { ok: true };
|
|
6780
|
+
}
|
|
6781
|
+
if (parent !== null) {
|
|
6782
|
+
if (!legacyActive || ids.length > 0) {
|
|
6783
|
+
return err2("delegation_lineage_denied", "delegated writes require an exact direct-parent match to the activating coordinator");
|
|
6784
|
+
}
|
|
6785
|
+
}
|
|
6621
6786
|
if (COORDINATOR_WRITE_TOOLS.includes(input.tool)) {
|
|
6622
6787
|
return err2("coordinator_write_denied", COORDINATOR_RECOVERY_TEXT);
|
|
6623
6788
|
}
|
|
@@ -6628,6 +6793,24 @@ var subagentDrivenInterception = (input) => {
|
|
|
6628
6793
|
}
|
|
6629
6794
|
return { ok: true };
|
|
6630
6795
|
};
|
|
6796
|
+
var findActiveSubagentDrivenContexts = (root2) => {
|
|
6797
|
+
let entries = [];
|
|
6798
|
+
try {
|
|
6799
|
+
entries = readdirSync6(path22.join(root2, "docs"), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
6800
|
+
} catch {
|
|
6801
|
+
return [];
|
|
6802
|
+
}
|
|
6803
|
+
const out = [];
|
|
6804
|
+
for (const slug of entries) {
|
|
6805
|
+
try {
|
|
6806
|
+
const state = readFlowState(root2, slug);
|
|
6807
|
+
if (state.execution.status === "active" && state.execution.mode === "subagent-driven") {
|
|
6808
|
+
out.push({ slug, coordinator_session_id: state.execution.coordinator_session_id });
|
|
6809
|
+
}
|
|
6810
|
+
} catch {}
|
|
6811
|
+
}
|
|
6812
|
+
return out;
|
|
6813
|
+
};
|
|
6631
6814
|
|
|
6632
6815
|
// packages/workit-core/src/core/detector.ts
|
|
6633
6816
|
var detectConfigGapError = (text) => text.includes(CONFIG_GAP_MARKER);
|
|
@@ -6748,7 +6931,7 @@ var scanActiveSubagentDrivenPlans = (root2) => {
|
|
|
6748
6931
|
if (exec.status === "active" && exec.mode === "subagent-driven")
|
|
6749
6932
|
slugs.push(slug);
|
|
6750
6933
|
};
|
|
6751
|
-
for (const slug of
|
|
6934
|
+
for (const slug of readdirSync7(docsDir)) {
|
|
6752
6935
|
const file = path23.join(docsDir, slug, "sdd", "flow.json");
|
|
6753
6936
|
if (!existsSync16(file))
|
|
6754
6937
|
continue;
|
|
@@ -19963,15 +20146,21 @@ var opencodeMutationContext = async (context2, client) => {
|
|
|
19963
20146
|
const session = await client?.session.get({ path: { id: context2.sessionID } });
|
|
19964
20147
|
parentID = session?.data?.parentID;
|
|
19965
20148
|
} catch {}
|
|
19966
|
-
const role = roleFromParentage(parentID);
|
|
20149
|
+
const role = roleFromParentage(parentID, undefined);
|
|
19967
20150
|
return {
|
|
19968
20151
|
hostWorkspace: context2.directory,
|
|
19969
20152
|
role,
|
|
19970
20153
|
sessionId: context2.sessionID,
|
|
20154
|
+
parentSessionId: parentID,
|
|
19971
20155
|
taskIdentity: role === "delegated" ? context2.sessionID : undefined
|
|
19972
20156
|
};
|
|
19973
20157
|
};
|
|
19974
20158
|
function createFlowTools(receipts, client) {
|
|
20159
|
+
const purposeForLifecycle = {
|
|
20160
|
+
"Pause plan": "plan-pause",
|
|
20161
|
+
"Resume plan": "plan-resume",
|
|
20162
|
+
"Complete plan": "plan-complete"
|
|
20163
|
+
};
|
|
19975
20164
|
const lifecycleTool = (action, label, description) => tool({
|
|
19976
20165
|
description,
|
|
19977
20166
|
args: {
|
|
@@ -19982,7 +20171,10 @@ function createFlowTools(receipts, client) {
|
|
|
19982
20171
|
if ("error" in slugged)
|
|
19983
20172
|
return output2(fail(slugged.error));
|
|
19984
20173
|
const slug = slugged.slug;
|
|
19985
|
-
const consumed = receipts.consume(context2.sessionID, {
|
|
20174
|
+
const consumed = receipts.consume(context2.sessionID, {
|
|
20175
|
+
purpose: purposeForLifecycle[label],
|
|
20176
|
+
label
|
|
20177
|
+
});
|
|
19986
20178
|
if (!consumed.ok)
|
|
19987
20179
|
return output2(fail(consumed.error, { code: consumed.code }));
|
|
19988
20180
|
const result = transitionExecution(context2.directory, slug, plan_path, action, createOpenCodeEvidence(consumed.receipt), await opencodeMutationContext(context2, client));
|
|
@@ -20052,7 +20244,7 @@ function createFlowTools(receipts, client) {
|
|
|
20052
20244
|
if ("error" in slugged)
|
|
20053
20245
|
return output2(fail(slugged.error));
|
|
20054
20246
|
const slug = slugged.slug;
|
|
20055
|
-
const consumed = receipts.consume(context2.sessionID);
|
|
20247
|
+
const consumed = receipts.consume(context2.sessionID, { purpose: "spec-approval" });
|
|
20056
20248
|
if (!consumed.ok)
|
|
20057
20249
|
return output2(fail(consumed.error, { code: consumed.code }));
|
|
20058
20250
|
const result = transitionSpec(context2.directory, slug, spec_path, createOpenCodeEvidence(consumed.receipt), await opencodeMutationContext(context2, client));
|
|
@@ -20071,7 +20263,7 @@ function createFlowTools(receipts, client) {
|
|
|
20071
20263
|
if ("error" in slugged)
|
|
20072
20264
|
return output2(fail(slugged.error));
|
|
20073
20265
|
const slug = slugged.slug;
|
|
20074
|
-
const consumed = receipts.consume(context2.sessionID);
|
|
20266
|
+
const consumed = receipts.consume(context2.sessionID, { purpose: "plan-approval" });
|
|
20075
20267
|
if (!consumed.ok)
|
|
20076
20268
|
return output2(fail(consumed.error, { code: consumed.code }));
|
|
20077
20269
|
const result = transitionPlan(context2.directory, slug, plan_path, createOpenCodeEvidence(consumed.receipt), await opencodeMutationContext(context2, client));
|
|
@@ -20097,7 +20289,10 @@ function createFlowTools(receipts, client) {
|
|
|
20097
20289
|
if ("error" in slugged)
|
|
20098
20290
|
return output2(fail(slugged.error));
|
|
20099
20291
|
const slug = slugged.slug;
|
|
20100
|
-
const consumed = receipts.consume(context2.sessionID, {
|
|
20292
|
+
const consumed = receipts.consume(context2.sessionID, {
|
|
20293
|
+
purpose: "execution-menu",
|
|
20294
|
+
label: choice
|
|
20295
|
+
});
|
|
20101
20296
|
if (!consumed.ok)
|
|
20102
20297
|
return output2(fail(consumed.error, { code: consumed.code }));
|
|
20103
20298
|
const result = recordMenuChoice(context2.directory, slug, plan_path, choice, createOpenCodeEvidence(consumed.receipt), await opencodeMutationContext(context2, client));
|
|
@@ -20116,11 +20311,11 @@ function createFlowTools(receipts, client) {
|
|
|
20116
20311
|
// packages/workit-opencode/src/tools/sdd.ts
|
|
20117
20312
|
var output3 = (value) => JSON.stringify(value, null, 2);
|
|
20118
20313
|
var requireConfirmed2 = (confirmed) => confirmed === true ? null : output3(fail("confirmed: true required"));
|
|
20119
|
-
var
|
|
20314
|
+
var gateSddControl = async (root2, sddPath, context2, client) => {
|
|
20120
20315
|
const slug = slugFromSddPath(sddPath);
|
|
20121
20316
|
if (!slug)
|
|
20122
20317
|
return output3(fail("could not derive slug — expected docs/<slug>/sdd/..."));
|
|
20123
|
-
const gate =
|
|
20318
|
+
const gate = assertSddControlGates(root2, slug, { requireMenu: true, requireDocs: true }, await opencodeMutationContext(context2, client));
|
|
20124
20319
|
if (!gate.ok)
|
|
20125
20320
|
return output3(fail(gate.error, { code: gate.code }));
|
|
20126
20321
|
return null;
|
|
@@ -20267,7 +20462,7 @@ function createSddTools(state, client) {
|
|
|
20267
20462
|
const rejected = requireConfirmed2(confirmed);
|
|
20268
20463
|
if (rejected)
|
|
20269
20464
|
return rejected;
|
|
20270
|
-
const gated = await
|
|
20465
|
+
const gated = await gateSddControl(context2.directory, sdd_dir, context2, client);
|
|
20271
20466
|
if (gated)
|
|
20272
20467
|
return gated;
|
|
20273
20468
|
return invoke(() => {
|
|
@@ -20300,7 +20495,7 @@ function createSddTools(state, client) {
|
|
|
20300
20495
|
} catch (error45) {
|
|
20301
20496
|
return output3(fail(error45 instanceof Error ? error45.message : "workflow operation failed"));
|
|
20302
20497
|
}
|
|
20303
|
-
const gated = await
|
|
20498
|
+
const gated = await gateSddControl(context2.directory, sdd_dir, context2, client);
|
|
20304
20499
|
if (gated)
|
|
20305
20500
|
return gated;
|
|
20306
20501
|
return invoke(() => sddReviewPackage({
|
|
@@ -20322,7 +20517,7 @@ function createSddTools(state, client) {
|
|
|
20322
20517
|
const rejected = requireConfirmed2(confirmed);
|
|
20323
20518
|
if (rejected)
|
|
20324
20519
|
return rejected;
|
|
20325
|
-
const gated = await
|
|
20520
|
+
const gated = await gateSddControl(context2.directory, progress_path, context2, client);
|
|
20326
20521
|
if (gated)
|
|
20327
20522
|
return gated;
|
|
20328
20523
|
return invoke(() => {
|
|
@@ -20330,6 +20525,32 @@ function createSddTools(state, client) {
|
|
|
20330
20525
|
return sddAppendProgress({ progress_path, line, workspace_root: context2.directory });
|
|
20331
20526
|
});
|
|
20332
20527
|
}
|
|
20528
|
+
}),
|
|
20529
|
+
workflow_sdd_append_advisory: tool({
|
|
20530
|
+
description: "Append a validated advisory line to docs/<slug>/sdd/advisories.md (coordinator-owned).",
|
|
20531
|
+
args: {
|
|
20532
|
+
confirmed: tool.schema.boolean(),
|
|
20533
|
+
advisories_path: tool.schema.string(),
|
|
20534
|
+
task_id: tool.schema.number(),
|
|
20535
|
+
text: tool.schema.string()
|
|
20536
|
+
},
|
|
20537
|
+
execute: async ({ confirmed, advisories_path, task_id, text }, context2) => {
|
|
20538
|
+
const rejected = requireConfirmed2(confirmed);
|
|
20539
|
+
if (rejected)
|
|
20540
|
+
return rejected;
|
|
20541
|
+
const gated = await gateSddControl(context2.directory, advisories_path, context2, client);
|
|
20542
|
+
if (gated)
|
|
20543
|
+
return gated;
|
|
20544
|
+
return invoke(() => {
|
|
20545
|
+
relativePath(context2.directory, advisories_path);
|
|
20546
|
+
return sddAppendAdvisory({
|
|
20547
|
+
advisories_path,
|
|
20548
|
+
task_id,
|
|
20549
|
+
text,
|
|
20550
|
+
workspace_root: context2.directory
|
|
20551
|
+
});
|
|
20552
|
+
});
|
|
20553
|
+
}
|
|
20333
20554
|
})
|
|
20334
20555
|
};
|
|
20335
20556
|
}
|
|
@@ -20341,12 +20562,12 @@ import path32 from "node:path";
|
|
|
20341
20562
|
import path31 from "node:path";
|
|
20342
20563
|
|
|
20343
20564
|
// packages/workit-core/src/core/handoff-context.ts
|
|
20344
|
-
import { existsSync as existsSync18, readFileSync as readFileSync20, readdirSync as
|
|
20565
|
+
import { existsSync as existsSync18, readFileSync as readFileSync20, readdirSync as readdirSync8, statSync as statSync8 } from "node:fs";
|
|
20345
20566
|
import path30 from "node:path";
|
|
20346
20567
|
var DOC_RE = /docs\/([A-Za-z0-9][A-Za-z0-9._-]*)\/(spec|plan)\.md/g;
|
|
20347
20568
|
var listMd = (dir) => {
|
|
20348
20569
|
try {
|
|
20349
|
-
return
|
|
20570
|
+
return readdirSync8(dir).filter((f) => f.endsWith(".md")).sort();
|
|
20350
20571
|
} catch {
|
|
20351
20572
|
return [];
|
|
20352
20573
|
}
|
|
@@ -20372,7 +20593,7 @@ var resolveActivePair = (root2) => {
|
|
|
20372
20593
|
let best = null;
|
|
20373
20594
|
let entries = [];
|
|
20374
20595
|
try {
|
|
20375
|
-
entries =
|
|
20596
|
+
entries = readdirSync8(docsDir);
|
|
20376
20597
|
} catch {
|
|
20377
20598
|
return { error: "no pair" };
|
|
20378
20599
|
}
|
|
@@ -20549,21 +20770,13 @@ function createHandoffTools(client, state) {
|
|
|
20549
20770
|
const active = built;
|
|
20550
20771
|
const slug = slugFromPath(active.plan);
|
|
20551
20772
|
try {
|
|
20552
|
-
const
|
|
20553
|
-
if (!
|
|
20554
|
-
return output4(fail(
|
|
20555
|
-
const preflight = readEffectiveFlowState(context2.directory, slug);
|
|
20556
|
-
if (!preflight.ok)
|
|
20557
|
-
return output4(fail(preflight.error, { code: preflight.code }));
|
|
20558
|
-
if (preflight.state.handoff_destination) {
|
|
20559
|
-
return output4(fail("this flow is already a handoff destination — a second handoff is rejected", {
|
|
20560
|
-
code: "recursive_handoff"
|
|
20561
|
-
}));
|
|
20562
|
-
}
|
|
20773
|
+
const ready = assertHandoffReady(context2.directory, active.plan);
|
|
20774
|
+
if (!ready.ok)
|
|
20775
|
+
return output4(fail(ready.error, { code: ready.code }));
|
|
20563
20776
|
state.set(context2.sessionID, { spec: active.spec, plan: active.plan, sdd: active.sdd });
|
|
20564
20777
|
return output4(await handoffSession(client, {
|
|
20565
20778
|
directory: context2.directory,
|
|
20566
|
-
title: `
|
|
20779
|
+
title: `Workit: ${path32.basename(path32.dirname(active.plan))}`,
|
|
20567
20780
|
prompt: active.prompt,
|
|
20568
20781
|
stay: /(?:^|\s)--stay(?:\s|$)/.test(userMessage),
|
|
20569
20782
|
afterSeed: () => idempotentMarkDestination(context2.directory, slug, active.plan)
|
|
@@ -21058,7 +21271,7 @@ function createPresentTools() {
|
|
|
21058
21271
|
}
|
|
21059
21272
|
|
|
21060
21273
|
// packages/workit-core/src/core/docs-repo.ts
|
|
21061
|
-
import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync22, writeFileSync as writeFileSync12, readdirSync as
|
|
21274
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync22, writeFileSync as writeFileSync12, readdirSync as readdirSync9 } from "node:fs";
|
|
21062
21275
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
21063
21276
|
import path35 from "node:path";
|
|
21064
21277
|
var configPath2 = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path35.join(configDir(), "docs-repo.json");
|
|
@@ -21104,7 +21317,7 @@ var listSpecs = (workspaceRoot) => {
|
|
|
21104
21317
|
const specs = [];
|
|
21105
21318
|
const docsDir = path35.join(workspaceRoot, "docs");
|
|
21106
21319
|
if (existsSync19(docsDir)) {
|
|
21107
|
-
for (const slug of
|
|
21320
|
+
for (const slug of readdirSync9(docsDir)) {
|
|
21108
21321
|
if (slug.startsWith("."))
|
|
21109
21322
|
continue;
|
|
21110
21323
|
const spec = path35.posix.join("docs", slug, "spec.md");
|
|
@@ -21115,7 +21328,7 @@ var listSpecs = (workspaceRoot) => {
|
|
|
21115
21328
|
if (repoPath) {
|
|
21116
21329
|
const featuresDir = path35.join(repoPath, "features");
|
|
21117
21330
|
if (existsSync19(featuresDir)) {
|
|
21118
|
-
const match =
|
|
21331
|
+
const match = readdirSync9(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d));
|
|
21119
21332
|
if (match) {
|
|
21120
21333
|
promoted = true;
|
|
21121
21334
|
target = path35.join(repoPath, "features", match);
|
|
@@ -21207,7 +21420,7 @@ var promoteSpec = (workspaceRoot, slug, opts) => {
|
|
|
21207
21420
|
}
|
|
21208
21421
|
const prefix = monthPrefix();
|
|
21209
21422
|
const featuresDir = path35.join(repoPath, "features");
|
|
21210
|
-
const existing = existsSync19(featuresDir) ?
|
|
21423
|
+
const existing = existsSync19(featuresDir) ? readdirSync9(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d)) : undefined;
|
|
21211
21424
|
const targetDir = path35.join(featuresDir, existing ?? `${prefix}-${slug}`);
|
|
21212
21425
|
mkdirSync10(targetDir, { recursive: true });
|
|
21213
21426
|
const files = ["spec.md"];
|
|
@@ -21269,7 +21482,7 @@ import {
|
|
|
21269
21482
|
existsSync as existsSync20,
|
|
21270
21483
|
lstatSync as lstatSync2,
|
|
21271
21484
|
mkdirSync as mkdirSync11,
|
|
21272
|
-
readdirSync as
|
|
21485
|
+
readdirSync as readdirSync10,
|
|
21273
21486
|
readFileSync as readFileSync23,
|
|
21274
21487
|
realpathSync as realpathSync5,
|
|
21275
21488
|
renameSync as renameSync3,
|
|
@@ -21324,7 +21537,7 @@ var detectLegacyDocs = (workspace_root) => {
|
|
|
21324
21537
|
const root2 = legacyRoot(workspace_root);
|
|
21325
21538
|
const raw = [];
|
|
21326
21539
|
if (existsSync20(root2)) {
|
|
21327
|
-
for (const entry of
|
|
21540
|
+
for (const entry of readdirSync10(root2, { withFileTypes: true })) {
|
|
21328
21541
|
const abs = path36.join(root2, entry.name);
|
|
21329
21542
|
const rel = posix3(path36.relative(workspace_root, abs));
|
|
21330
21543
|
if (entry.isDirectory()) {
|
|
@@ -21466,7 +21679,7 @@ var planTree = (workspace, legacyAbs, legacyRel, destRelRoot, destAbsRoot, legac
|
|
|
21466
21679
|
if (visits.has(real))
|
|
21467
21680
|
return { ok: true };
|
|
21468
21681
|
visits.add(real);
|
|
21469
|
-
for (const entry of
|
|
21682
|
+
for (const entry of readdirSync10(real, { withFileTypes: true })) {
|
|
21470
21683
|
const fromAbs = path36.join(real, entry.name);
|
|
21471
21684
|
const fromRel = posix3(path36.join(legacyRel, entry.name));
|
|
21472
21685
|
const toRel = posix3(path36.join(destRelRoot, entry.name));
|
|
@@ -21837,7 +22050,7 @@ SDD: ${value.sdd}` : null;
|
|
|
21837
22050
|
}
|
|
21838
22051
|
|
|
21839
22052
|
// packages/workit-core/src/core/logger.ts
|
|
21840
|
-
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync12, readdirSync as
|
|
22053
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync12, readdirSync as readdirSync11, unlinkSync as unlinkSync3 } from "node:fs";
|
|
21841
22054
|
import os4 from "node:os";
|
|
21842
22055
|
import path37 from "node:path";
|
|
21843
22056
|
var REDACTED = "[REDACTED]";
|
|
@@ -21972,7 +22185,7 @@ var dailyFileName = (date5) => {
|
|
|
21972
22185
|
};
|
|
21973
22186
|
var pruneOldFiles = (dir) => {
|
|
21974
22187
|
try {
|
|
21975
|
-
const files =
|
|
22188
|
+
const files = readdirSync11(dir).filter((name) => DAY_FILE.test(name)).sort().reverse();
|
|
21976
22189
|
for (const file2 of files.slice(RETAINED_DAYS)) {
|
|
21977
22190
|
try {
|
|
21978
22191
|
unlinkSync3(path37.join(dir, file2));
|
|
@@ -22139,7 +22352,7 @@ var hasMarkedDestination = (root3) => {
|
|
|
22139
22352
|
return false;
|
|
22140
22353
|
let slugs;
|
|
22141
22354
|
try {
|
|
22142
|
-
slugs =
|
|
22355
|
+
slugs = readdirSync12(docsDir);
|
|
22143
22356
|
} catch {
|
|
22144
22357
|
return false;
|
|
22145
22358
|
}
|
|
@@ -22171,6 +22384,32 @@ var questionAnswerLabel = (result) => {
|
|
|
22171
22384
|
const label = first[0];
|
|
22172
22385
|
return typeof label === "string" ? label : undefined;
|
|
22173
22386
|
};
|
|
22387
|
+
var classifyQuestion = (input, label) => {
|
|
22388
|
+
const args = input;
|
|
22389
|
+
const qs = args?.questions;
|
|
22390
|
+
if (Array.isArray(qs)) {
|
|
22391
|
+
if (qs.length !== 1)
|
|
22392
|
+
return null;
|
|
22393
|
+
const q = qs[0];
|
|
22394
|
+
if (!q || typeof q !== "object")
|
|
22395
|
+
return null;
|
|
22396
|
+
if (q.multiple === true)
|
|
22397
|
+
return null;
|
|
22398
|
+
const opts = q.options;
|
|
22399
|
+
if (Array.isArray(opts) && opts.length === 0 && q.custom === true)
|
|
22400
|
+
return null;
|
|
22401
|
+
if (!Array.isArray(opts) || opts.length === 0)
|
|
22402
|
+
return null;
|
|
22403
|
+
const purpose2 = receiptPurposeForLabel(label);
|
|
22404
|
+
if (purpose2 === undefined)
|
|
22405
|
+
return null;
|
|
22406
|
+
return { purpose: purpose2, questionText: typeof q.question === "string" ? q.question : undefined };
|
|
22407
|
+
}
|
|
22408
|
+
const purpose = receiptPurposeForLabel(label);
|
|
22409
|
+
if (purpose !== undefined)
|
|
22410
|
+
return { purpose, questionText: undefined };
|
|
22411
|
+
return null;
|
|
22412
|
+
};
|
|
22174
22413
|
var plugin = async ({ client, directory }) => {
|
|
22175
22414
|
openCodeClient = client;
|
|
22176
22415
|
installUncaughtHandlers();
|
|
@@ -22180,6 +22419,17 @@ var plugin = async ({ client, directory }) => {
|
|
|
22180
22419
|
setDiagnosticLogger(logger);
|
|
22181
22420
|
const state = new WorkflowStateStore;
|
|
22182
22421
|
const receipts = new HostReceiptStore;
|
|
22422
|
+
const authorizedWorkerFor = async (sessionID) => {
|
|
22423
|
+
const ids = findActiveSubagentDrivenContexts(directory).map((c) => c.coordinator_session_id).filter((id) => typeof id === "string" && id !== "");
|
|
22424
|
+
if (ids.length !== 1)
|
|
22425
|
+
return false;
|
|
22426
|
+
try {
|
|
22427
|
+
const session = await client.session.get({ path: { id: sessionID } });
|
|
22428
|
+
return session?.data?.parentID === ids[0];
|
|
22429
|
+
} catch {
|
|
22430
|
+
return false;
|
|
22431
|
+
}
|
|
22432
|
+
};
|
|
22183
22433
|
return {
|
|
22184
22434
|
tool: createTools(adaptPluginHandoffClient(client), state, client, receipts),
|
|
22185
22435
|
"tool.execute.after": async (input, output10) => {
|
|
@@ -22188,9 +22438,11 @@ var plugin = async ({ client, directory }) => {
|
|
|
22188
22438
|
const label = questionAnswerLabel(output10);
|
|
22189
22439
|
if (label === undefined)
|
|
22190
22440
|
return;
|
|
22191
|
-
const
|
|
22192
|
-
|
|
22193
|
-
|
|
22441
|
+
const classified = classifyQuestion(input.args, label);
|
|
22442
|
+
if (!classified)
|
|
22443
|
+
return;
|
|
22444
|
+
const questionText = classified.questionText ?? input.args?.question ?? input.args?.title;
|
|
22445
|
+
receipts.record(input.sessionID, input.callID, label, Date.now(), questionText, classified.purpose);
|
|
22194
22446
|
},
|
|
22195
22447
|
"tool.execute.before": async (input, output10) => {
|
|
22196
22448
|
if (input.tool !== "bash" && !COORDINATOR_WRITE_TOOLS.includes(input.tool))
|
|
@@ -22202,12 +22454,12 @@ var plugin = async ({ client, directory }) => {
|
|
|
22202
22454
|
parentID = session?.data?.parentID;
|
|
22203
22455
|
sessionDirectory = session?.data?.directory;
|
|
22204
22456
|
} catch {}
|
|
22205
|
-
const
|
|
22457
|
+
const activeCoordinatorIds = findActiveSubagentDrivenContexts(sessionDirectory ?? directory).map((c) => c.coordinator_session_id).filter((id) => typeof id === "string" && id !== "");
|
|
22206
22458
|
const decision = subagentDrivenInterception({
|
|
22207
22459
|
tool: input.tool,
|
|
22208
22460
|
command: output10.args?.command,
|
|
22209
22461
|
parentID,
|
|
22210
|
-
|
|
22462
|
+
activeCoordinatorIds
|
|
22211
22463
|
});
|
|
22212
22464
|
if (!decision.ok)
|
|
22213
22465
|
throw new Error(decision.error);
|
|
@@ -22260,7 +22512,8 @@ var plugin = async ({ client, directory }) => {
|
|
|
22260
22512
|
const firstAnchor = firstUser.parts.find((part) => part.type === "text") ?? firstUser.parts[0];
|
|
22261
22513
|
const firstText = firstUser.parts.filter((part) => part.type === "text").map((part) => part.text ?? "").join(`
|
|
22262
22514
|
`);
|
|
22263
|
-
const
|
|
22515
|
+
const workerAuthorized = await authorizedWorkerFor(firstAnchor.sessionID);
|
|
22516
|
+
const bootstrap = workerAuthorized ? null : getWorkitBootstrap();
|
|
22264
22517
|
if (bootstrap && !firstText.includes("<workit-contract>")) {
|
|
22265
22518
|
firstUser.parts.unshift({
|
|
22266
22519
|
id: firstAnchor.id,
|
|
@@ -22269,6 +22522,14 @@ var plugin = async ({ client, directory }) => {
|
|
|
22269
22522
|
type: "text",
|
|
22270
22523
|
text: bootstrap
|
|
22271
22524
|
});
|
|
22525
|
+
} else if (workerAuthorized && !firstText.includes("workflow-sdd-worker")) {
|
|
22526
|
+
firstUser.parts.unshift({
|
|
22527
|
+
id: firstAnchor.id,
|
|
22528
|
+
sessionID: firstAnchor.sessionID,
|
|
22529
|
+
messageID: firstAnchor.messageID,
|
|
22530
|
+
type: "text",
|
|
22531
|
+
text: SDD_WORKER_REMINDER_TEXT
|
|
22532
|
+
});
|
|
22272
22533
|
}
|
|
22273
22534
|
}
|
|
22274
22535
|
const anchor = currentUser.parts.find((part) => part.type === "text") ?? currentUser.parts[0];
|
|
@@ -22303,8 +22564,12 @@ var plugin = async ({ client, directory }) => {
|
|
|
22303
22564
|
}
|
|
22304
22565
|
}
|
|
22305
22566
|
const activePlans = findActiveSubagentDrivenPlans(directory);
|
|
22306
|
-
if (activePlans.length > 0
|
|
22307
|
-
|
|
22567
|
+
if (activePlans.length > 0) {
|
|
22568
|
+
const workerAuthorized = await authorizedWorkerFor(anchor.sessionID);
|
|
22569
|
+
const sddText = workerAuthorized ? SDD_WORKER_REMINDER_TEXT : SDD_REMINDER_TEXT;
|
|
22570
|
+
if (!currentText.includes(sddText)) {
|
|
22571
|
+
currentUser.parts.unshift(makePart(sddText, "sdd"));
|
|
22572
|
+
}
|
|
22308
22573
|
}
|
|
22309
22574
|
if (detectConfigGapError(assistantText) && shouldInjectConfigGuard(currentText)) {
|
|
22310
22575
|
currentUser.parts.unshift(makePart(CONFIG_GUARD_TEXT, "cg"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainervirus/workit-opencode",
|
|
3
|
-
"version": "0.8.
|
|
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"
|