@brainervirus/workit-cli 0.8.8 → 0.8.10

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
@@ -25,6 +25,7 @@ workit flow pause --plan docs/<slug>/plan.md [--confirm] # active -> paused
25
25
  workit flow resume --plan docs/<slug>/plan.md [--confirm] # paused -> active
26
26
  workit flow complete --plan docs/<slug>/plan.md [--confirm] # complete (ledger + verification gated)
27
27
  workit flow review-package --plan docs/<slug>/plan.md --base <sha> --head <sha> [--confirm] # write a review diff
28
+ workit flow append-advisory --plan docs/<slug>/plan.md --task <id> --text <text> [--confirm] # record an advisory review finding
28
29
  workit handoff --message "docs/<slug>/plan.md" # print the destination handoff prompt
29
30
  workit # help
30
31
  ```
@@ -7,59 +7,61 @@ 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
 
19
20
  ## Hard gates
20
21
 
21
22
  - The parent is coordinator-only: it does not edit product code or perform delegated exploration.
22
- - Never use a worktree. Branch changes are in-place through `workflow_branch_setup` on `feature/*` or `bugfix/*`; never commit on protected branches.
23
- - Working state, briefs, ledgers, and review diffs live only under gitignored `<SDD_DIR>` in `docs/<slug>/sdd/` and use `workflow_sdd_*` tools.
23
+ - Never use a worktree. Branch changes are in-place through `workit_branch_setup` on `feature/*` or `bugfix/*`; never commit on protected branches.
24
+ - Working state, briefs, ledgers, and review diffs live only under gitignored `<SDD_DIR>` in `docs/<slug>/sdd/` and use `workit_sdd_*` tools.
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
- - 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.
27
- - On Cursor, for every repository-scoped `workflow_*` call, pass the active Cursor workspace as `workspace_root`; never rely on the MCP process default.
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 `workit_spec_approve` / `workit_plan_approve` / `workit_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 `workit_sdd_*` stays with the coordinator session.
29
+ - On Cursor, for every repository-scoped `workit_*` 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
 
30
32
  ## Flow gates (HARD)
31
33
 
32
34
  - `wk-implement` refuses to run unless the plan is `approved` (flow.json) and the post-plan menu was presented.
33
35
  - `wk-handoff` refuses to run unless both spec and plan are `approved`.
34
- - Sequence is enforced by tools: `workflow_spec_approve`, `workflow_plan_approve`, `workflow_plan_menu` — never skip a step (the spec/plan self-review runs automatically inside the transition; only the final approval asks for your confirmation).
36
+ - Sequence is enforced by tools: `workit_spec_approve`, `workit_plan_approve`, `workit_plan_menu` — never skip a step (the spec/plan self-review runs automatically inside the transition; only the final approval asks for your confirmation).
35
37
 
36
38
  ## Setup
37
39
 
38
- 0. Call `workflow_docs_validate` with the linked spec/plan paths. Hard-fail on any error before todos or branch setup.
39
- 1. Call `workflow_sdd_context` with `<PLAN_PATH>` and initialize `todowrite` from returned tasks.
40
- 2. Call `workflow_plan_tasks`; cache each top-level task's `section_text`.
40
+ 0. Call `workit_docs_validate` with the linked spec/plan paths. Hard-fail on any error before todos or branch setup.
41
+ 1. Call `workit_sdd_context` with `<PLAN_PATH>` and initialize `todowrite` from returned tasks.
42
+ 2. Call `workit_plan_tasks`; cache each top-level task's `section_text`.
41
43
  3. Mark IDs in `completed_task_ids` completed and never redispatch them.
42
- 4. Call `workflow_resolve_branch`, then show the current branch, target branch, and stash behavior before any in-place checkout/setup mutation.
44
+ 4. Call `workit_resolve_branch`, then show the current branch, target branch, and stash behavior before any in-place checkout/setup mutation.
43
45
  5. Always use `question`: for a clean tree ask whether to proceed or cancel; for a dirty tree add the stash choice and describe what will be stashed.
44
- 6. Call `workflow_branch_setup` with `confirmed: true` only after approval.
46
+ 6. Call `workit_branch_setup` with `confirmed: true` only after approval.
45
47
 
46
48
  ## Remaining-task loop
47
49
 
48
50
  For each top-level task absent from `completed_task_ids`:
49
51
 
50
52
  1. Mark it `in_progress` with `todowrite`.
51
- 2. Create a working-state brief with `workflow_sdd_task_brief` and `confirmed: true`.
53
+ 2. Create a working-state brief with `workit_sdd_task_brief` and `confirmed: true`.
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
- 4. Create a working-state diff with `workflow_sdd_review_package` and `confirmed: true`.
55
+ 4. Create a working-state diff with `workit_sdd_review_package` and `confirmed: true`.
54
56
  5. Delegate spec-compliance review and code-quality review to separate `general` agents.
55
- 6. **Blocking** findings (Critical, Important, or spec-compliance) may trigger at most **two** fix+re-review rounds per task. **Advisory** findings (Minor, style, YAGNI, taste) never pause the loop — append them to `<SDD_DIR>/advisories.md`.
56
- 7. Append the validated ledger entry with `workflow_sdd_append_progress` and `confirmed: true`; mark the todo completed.
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 `workit_sdd_append_advisory` (`--task <id> --text <text>`, `confirmed: true`) instead of an unrestricted file edit.
58
+ 7. Append the validated ledger entry with `workit_sdd_append_progress` and `confirmed: true`; mark the todo completed.
57
59
 
58
60
  ## Final gate
59
61
 
60
- Run a separate full-branch code review, then `workflow_verify`. 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. Report exact check results and never infer success. Use `workflow_git_context` for a commit preview and load `wk-commit` through `skill` for an approved commit. If working state contains a stash reference, preview reapplication through `question`, then call `workflow_branch_setup` with `confirmed: true` after approval.
62
+ Run a separate full-branch code review, then `workit_verify`. 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. Report exact check results and never infer success. Use `workit_git_context` for a commit preview and load `wk-commit` through `skill` for an approved commit. If working state contains a stash reference, preview reapplication through `question`, then call `workit_branch_setup` with `confirmed: true` after approval.
61
63
 
62
- **Mandatory:** end the run by calling `workflow_plan_complete` (OpenCode/Cursor) or the CLI `workit flow complete` (CLI host) after the final task once the SDD ledger is complete (all task IDs appended) and `workflow_verify` passes — a complete ledger and green verification are the tool's gates. Never finish the run while the plan is still `active`.
64
+ **Mandatory:** end the run by calling `workit_plan_complete` (OpenCode/Cursor) or the CLI `workit flow complete` (CLI host) after the final task once the SDD ledger is complete (all task IDs appended) and `workit_verify` passes — a complete ledger and green verification are the tool's gates. Never finish the run while the plan is still `active`.
63
65
 
64
66
  ## Task order
65
67
 
@@ -68,4 +70,4 @@ Run a separate full-branch code review, then `workflow_verify`. Present the full
68
70
  ## Quality gate (HARD)
69
71
 
70
72
  - Specs/plans are written from `templates/spec-template.md` / `templates/plan-template.md`.
71
- - After `workflow_docs_validate`, surface `quality` findings (spec scan). Hard findings (missing required section, missing CA-XX) block task start unless the user explicitly waives them. Warnings are advisory.
73
+ - After `workit_docs_validate`, surface `quality` findings (spec scan). Hard findings (missing required section, missing CA-XX) block task start unless the user explicitly waives them. Warnings are advisory.
@@ -10,7 +10,7 @@
10
10
  ## Global Constraints
11
11
 
12
12
  - 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.
13
- - The final task ends execution with `workflow_plan_complete` (or the CLI `workit flow complete`) once the SDD ledger is complete and repository verification passes — a run never finishes while the plan is still `active`.
13
+ - The final task ends execution with `workit_plan_complete` (or the CLI `workit flow complete`) once the SDD ledger is complete and repository verification passes — a run never finishes while the plan is still `active`.
14
14
  - <project-wide requirements, one line each>
15
15
 
16
16
  ---
@@ -16,13 +16,13 @@
16
16
 
17
17
  ## Architecture
18
18
 
19
- <!-- REQUIRED if this spec has flows or architecture: render a mermaid diagram (workflow_present_flow). -->
19
+ <!-- REQUIRED if this spec has flows or architecture: render a mermaid diagram (workit_present_flow). -->
20
20
  ```mermaid
21
21
  flowchart TD
22
22
  A[Start] --> B[Step]
23
23
  ```
24
24
 
25
- <!-- REQUIRED if this spec touches UI: render an ASCII wireframe (workflow_present_ascii). -->
25
+ <!-- REQUIRED if this spec touches UI: render an ASCII wireframe (workit_present_ascii). -->
26
26
  ```text
27
27
  ┌──────────────┐
28
28
  │ Header │
@@ -27,31 +27,31 @@ Plans require:
27
27
 
28
28
  `bugfix/<slug>` is also valid. Never use `main`, `develop`, `master`, or `prod`. Use plain backtick paths. Top-level headings are exactly `### Task N: Title`; steps use `- [ ] **Step N:** ...`; task headings never appear inside fences.
29
29
 
30
- Before writing **Branch:** into a new spec or plan, call `workflow_docs_branch` and write the returned `branch` verbatim. When `action` is `keep`, use the current feature/bugfix branch. When `action` is `create_from_develop` or `create_from_base`, create the branch only through `workflow_branch_setup`; it uses the configured workspace/global target branch.
30
+ Before writing **Branch:** into a new spec or plan, call `workit_docs_branch` and write the returned `branch` verbatim. When `action` is `keep`, use the current feature/bugfix branch. When `action` is `create_from_develop` or `create_from_base`, create the branch only through `workit_branch_setup`; it uses the configured workspace/global target branch.
31
31
 
32
32
  ## Execution and handoff
33
33
 
34
34
  - Implementation uses `wk-implement` and subagent-driven development, with native `todowrite` and `task`.
35
35
  - Commits use `wk-commit` after its native `question` confirmation.
36
- - Continuation uses `wk-handoff`, whose `workflow_handoff_session` creates and seeds the OpenCode session automatically.
37
- - Never use worktrees. Resolve the declared branch with `workflow_resolve_branch`, preview dirty-tree stash choices with `question`, and apply an approved in-place checkout through `workflow_branch_setup` with `confirmed: true` (grounded in the recorded NativeChoiceEvidence).
38
- - 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.
39
- - Keep all SDD state under the gitignored `docs/<slug>/sdd/`; use `workflow_sdd_context` and the registered `workflow_sdd_*` tools.
40
- - After implementation, use `question` before an approved stash reapply through `workflow_branch_setup` with `confirmed: true`.
36
+ - Continuation uses `wk-handoff`, whose `workit_handoff_session` creates and seeds the OpenCode session automatically.
37
+ - Never use worktrees. Resolve the declared branch with `workit_resolve_branch`, preview dirty-tree stash choices with `question`, and apply an approved in-place checkout through `workit_branch_setup` with `confirmed: true` (grounded in the recorded NativeChoiceEvidence).
38
+ - 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 `workit_spec_approve` / `workit_plan_approve` / `workit_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.
39
+ - Keep all SDD state under the gitignored `docs/<slug>/sdd/`; use `workit_sdd_context` and the registered `workit_sdd_*` tools.
40
+ - After implementation, use `question` before an approved stash reapply through `workit_branch_setup` with `confirmed: true`.
41
41
 
42
42
  ## YouTrack content
43
43
 
44
- Chat follows the user's language. YouTrack task comments are Spanish (`es-CL`) and use `workflow_youtrack_draft` followed by reviewed `workflow_youtrack_post` with `confirmed: true`. Preserve the user's paragraph voice; do not inject commits, file paths, or robotic bullet reports.
44
+ Chat follows the user's language. YouTrack task comments are Spanish (`es-CL`) and use `workit_youtrack_draft` followed by reviewed `workit_youtrack_post` with `confirmed: true`. Preserve the user's paragraph voice; do not inject commits, file paths, or robotic bullet reports.
45
45
 
46
46
  ## Final self-check
47
47
 
48
- Before handoff, call `workflow_docs_validate` on the linked spec/plan pair. Hard-fail on any error; never offer execution when validation fails.
48
+ Before handoff, call `workit_docs_validate` on the linked spec/plan pair. Hard-fail on any error; never offer execution when validation fails.
49
49
 
50
50
  Before handoff, verify the saved spec path, plan path, declared branch, top-level task numbering, and workflow-managed SDD directory through the registered read-only workflow tools. Report structured failures; never infer success.
51
51
 
52
52
  ## Post-plan execution choice
53
53
 
54
- After saving a plan, call `workflow_docs_validate` on the spec/plan pair. On failure, stop and fix docs — do not offer execution.
54
+ After saving a plan, call `workit_docs_validate` on the spec/plan pair. On failure, stop and fix docs — do not offer execution.
55
55
 
56
56
  On success, use native `question` / Cursor `AskQuestion` with exactly these options (no stay, no A/B/C prose duplicate):
57
57
 
@@ -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 `workit_plan_menu` and re-presents the menu on the next turn. Every other choice must call `workit_plan_menu` immediately after the answer and before any skill, branch question, mutation, or handoff.
63
66
 
64
67
  Never emit Superpowers text beginning “Two execution options”.
65
68
 
66
- A handoff destination session (the seeded contract carries `<workflow-handoff-destination>true</workflow-handoff-destination>`) presents exactly four choices — Subagent-driven, Inline, Review spec first, Review plan first — and never re-offers the originating handoff option.
69
+ A handoff destination session (the seeded contract carries `<workflow-handoff-destination>true</workflow-handoff-destination>`) presents exactly five choices — Subagent-driven, Inline, Review spec first, Review plan first, Change model first — and never re-offers the originating handoff option.
67
70
 
68
71
  - Specs/plans must follow `templates/spec-template.md` / `templates/plan-template.md` (mandated diagrams, tables, CA-XX).
69
72
 
package/dist/index.js CHANGED
@@ -46436,6 +46436,7 @@ import {
46436
46436
  fsyncSync,
46437
46437
  mkdirSync as mkdirSync8,
46438
46438
  openSync,
46439
+ readdirSync as readdirSync5,
46439
46440
  readFileSync as readFileSync13,
46440
46441
  renameSync as renameSync2,
46441
46442
  rmSync as rmSync2,
@@ -46982,6 +46983,58 @@ function sddReviewPackage({
46982
46983
  return { error: error instanceof Error ? error.message : "git diff failed" };
46983
46984
  }
46984
46985
  }
46986
+ function sddAppendAdvisory({
46987
+ advisories_path,
46988
+ task_id,
46989
+ text,
46990
+ workspace_root
46991
+ }) {
46992
+ if (typeof task_id !== "number" || !Number.isSafeInteger(task_id) || task_id <= 0) {
46993
+ return { error: "task_id must be a positive safe integer", code: "advisory_task_invalid" };
46994
+ }
46995
+ if (typeof text !== "string") {
46996
+ return {
46997
+ error: "advisory text must be a string of 1-1000 characters after normalization",
46998
+ code: "advisory_text_invalid"
46999
+ };
47000
+ }
47001
+ if (text.includes("\r") || text.includes(`
47002
+ `)) {
47003
+ return {
47004
+ error: "advisory text must be a single line (no CR/LF)",
47005
+ code: "advisory_text_invalid"
47006
+ };
47007
+ }
47008
+ const collapsed = text.trim().replace(/[ \t]+/g, " ");
47009
+ if (collapsed.length === 0 || collapsed.length > 1000) {
47010
+ return {
47011
+ error: "advisory text must be 1-1000 characters after trim and horizontal-space collapse",
47012
+ code: "advisory_text_invalid"
47013
+ };
47014
+ }
47015
+ if (!/^docs\/[^/]+\/sdd\/advisories\.md$/.test(advisories_path)) {
47016
+ return {
47017
+ error: `advisories_path must be docs/<slug>/sdd/advisories.md: ${advisories_path}`,
47018
+ code: "advisory_path_invalid"
47019
+ };
47020
+ }
47021
+ const contained = resolveDocsPath({ workspace_root, path: advisories_path });
47022
+ if (!contained.ok)
47023
+ return { error: contained.error, code: "advisory_path_invalid" };
47024
+ const abs = contained.path;
47025
+ if (existsSync12(abs) && statSync5(abs).isDirectory()) {
47026
+ return {
47027
+ error: `advisory target is a directory: ${advisories_path}`,
47028
+ code: "advisory_target_invalid"
47029
+ };
47030
+ }
47031
+ mkdirSync6(path15.dirname(abs), { recursive: true });
47032
+ const line = `- Task ${task_id}: ${collapsed}
47033
+ `;
47034
+ appendFileSync2(abs, line, "utf8");
47035
+ const rel = posix2(path15.relative(contained.base, abs));
47036
+ return { ok: true, advisory: collapsed, advisories_path: rel };
47037
+ }
46985
47038
 
46986
47039
  // packages/workit-core/src/core/verify-project.ts
46987
47040
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -47370,7 +47423,7 @@ root: ${cwd2}
47370
47423
  }
47371
47424
 
47372
47425
  // packages/workit-core/src/core/flow-state.ts
47373
- var COORDINATOR_RECOVERY_TEXT = "A subagent-driven plan is active: coordinator product edits are blocked. " + "Delegate product mutations (task briefs, progress, review packages) to an " + "authenticated delegated worker via `task` / `wk-implement` instead of " + "editing in the coordinator session.";
47426
+ 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.";
47374
47427
  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`.";
47375
47428
  var MENU_CHOICES = [
47376
47429
  "subagent-driven",
@@ -47503,6 +47556,12 @@ var validateState = (parsed, slug) => {
47503
47556
  if (execRaw?.evidence !== undefined && !validateEvidenceValue(execRaw.evidence, true)) {
47504
47557
  return { ok: false, error: "flow state execution.evidence has an unsupported shape" };
47505
47558
  }
47559
+ if (execRaw?.coordinator_session_id !== undefined && execRaw.coordinator_session_id !== null && typeof execRaw.coordinator_session_id !== "string") {
47560
+ return {
47561
+ ok: false,
47562
+ error: "flow state execution.coordinator_session_id must be a string or null"
47563
+ };
47564
+ }
47506
47565
  return {
47507
47566
  ok: true,
47508
47567
  state: {
@@ -47518,7 +47577,8 @@ var validateState = (parsed, slug) => {
47518
47577
  execution: {
47519
47578
  status: execRaw?.status ?? "pending",
47520
47579
  mode: execRaw?.mode ?? null,
47521
- evidence: execRaw?.evidence ?? null
47580
+ evidence: execRaw?.evidence ?? null,
47581
+ coordinator_session_id: execRaw?.coordinator_session_id ?? null
47522
47582
  },
47523
47583
  handoff_destination: parsed.handoff_destination ?? false,
47524
47584
  updated_at: parsed.updated_at ?? Date.now()
@@ -47529,7 +47589,7 @@ var readFlowStrict = (root, slug) => {
47529
47589
  const file = flowPath(root, slug);
47530
47590
  const rel = path18.posix.join("docs", slug, "sdd", "flow.json");
47531
47591
  if (!existsSync14(file)) {
47532
- return err2("flow_not_activated", `flow not activated for ${slug} — run workflow_flow_status first`);
47592
+ return err2("flow_not_activated", `flow not activated for ${slug} — run workit_flow_status first`);
47533
47593
  }
47534
47594
  let text;
47535
47595
  try {
@@ -47655,7 +47715,7 @@ var resetForSpecDrift = (state) => ({
47655
47715
  spec: { ...state.spec, status: "draft", evidence: null, approved_digest: null },
47656
47716
  plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
47657
47717
  menu: { presented: false, chosen: "", evidence: null },
47658
- execution: { status: "pending", mode: null, evidence: null },
47718
+ execution: { status: "pending", mode: null, evidence: null, coordinator_session_id: null },
47659
47719
  handoff_destination: false,
47660
47720
  updated_at: Date.now()
47661
47721
  });
@@ -47698,9 +47758,14 @@ var reconcileState = (root, slug, state) => {
47698
47758
  var deriveLegacyExecution = (root, slug, state) => {
47699
47759
  const ledger = ledgerCompletion(root, slug);
47700
47760
  if (state.plan.status === "approved" && state.menu.chosen === "subagent-driven" && ledger.started && !ledger.complete) {
47701
- return { status: "active", mode: "subagent-driven", evidence: null };
47761
+ return {
47762
+ status: "active",
47763
+ mode: "subagent-driven",
47764
+ evidence: null,
47765
+ coordinator_session_id: null
47766
+ };
47702
47767
  }
47703
- return { status: "pending", mode: null, evidence: null };
47768
+ return { status: "pending", mode: null, evidence: null, coordinator_session_id: null };
47704
47769
  };
47705
47770
  var normalizeCompatibility = (root, slug, parsed, state) => {
47706
47771
  if (!isRecord3(parsed) || !("execution" in parsed)) {
@@ -47709,6 +47774,11 @@ var normalizeCompatibility = (root, slug, parsed, state) => {
47709
47774
  if (derived.status !== current.status || derived.mode !== current.mode) {
47710
47775
  return { state: { ...state, execution: derived, updated_at: Date.now() }, changed: true };
47711
47776
  }
47777
+ return { state, changed: false };
47778
+ }
47779
+ const execRaw = isRecord3(parsed.execution) ? parsed.execution : undefined;
47780
+ if (execRaw && !("coordinator_session_id" in execRaw)) {
47781
+ return { state: { ...state, updated_at: Date.now() }, changed: true };
47712
47782
  }
47713
47783
  return { state, changed: false };
47714
47784
  };
@@ -47871,19 +47941,49 @@ var isNegativeLabel = (label) => {
47871
47941
  return firstWord.replace(/[^a-z]/g, "") === entry;
47872
47942
  });
47873
47943
  };
47944
+ var receiptPurposeForLabel = (label) => {
47945
+ const n = normalizeLabel(label);
47946
+ if (n === "approve spec" || n === "approve spec recommended")
47947
+ return "spec-approval";
47948
+ if (n === "approve plan" || n === "approve plan recommended")
47949
+ return "plan-approval";
47950
+ if (n === "approve")
47951
+ return;
47952
+ if (n === "pause plan")
47953
+ return "plan-pause";
47954
+ if (n === "resume plan")
47955
+ return "plan-resume";
47956
+ if (n === "complete plan")
47957
+ return "plan-complete";
47958
+ const exec3 = new Set([
47959
+ "subagent driven",
47960
+ "inline",
47961
+ "handoff",
47962
+ "review spec",
47963
+ "review plan",
47964
+ "change model"
47965
+ ]);
47966
+ if (exec3.has(n))
47967
+ return "execution-menu";
47968
+ return;
47969
+ };
47874
47970
 
47875
47971
  class HostReceiptStore {
47876
47972
  #bySession = new Map;
47877
- record(sessionId, callID, selectedLabel, recordedAt = Date.now(), question) {
47878
- const label = selectedLabel.trim();
47879
- if (!label)
47973
+ record(sessionId, callID, selectedLabel, recordedAt = Date.now(), question = "", purpose) {
47974
+ const trimmed = selectedLabel.trim();
47975
+ if (!trimmed)
47880
47976
  return;
47881
47977
  if (recordedAt > Date.now() + MAX_CLOCK_SKEW_MS)
47882
47978
  return;
47979
+ const derived = purpose ?? receiptPurposeForLabel(selectedLabel);
47980
+ if (derived === undefined)
47981
+ return;
47982
+ const q = question ?? "";
47883
47983
  const queue = this.#bySession.get(sessionId) ?? [];
47884
47984
  if (queue.length >= MAX_RECEIPTS_PER_SESSION)
47885
47985
  queue.shift();
47886
- queue.push({ sessionId, callID, selectedLabel: label, recordedAt, question });
47986
+ queue.push({ sessionId, callID, selectedLabel, recordedAt, question: q, purpose: derived });
47887
47987
  this.#bySession.set(sessionId, queue);
47888
47988
  }
47889
47989
  count(sessionId) {
@@ -47900,10 +48000,40 @@ class HostReceiptStore {
47900
48000
  if (!queue || queue.length === 0) {
47901
48001
  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");
47902
48002
  }
47903
- const index = queue.length - 1;
48003
+ let index = -1;
48004
+ if (opts.purpose !== undefined) {
48005
+ const top = queue[queue.length - 1];
48006
+ if (top && top.purpose === opts.purpose && isNegativeLabel(top.selectedLabel)) {
48007
+ const filtered = queue.filter((r) => r.purpose !== opts.purpose);
48008
+ if (filtered.length === 0)
48009
+ this.#bySession.delete(sessionId);
48010
+ else
48011
+ this.#bySession.set(sessionId, filtered);
48012
+ 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");
48013
+ }
48014
+ for (let i = queue.length - 1;i >= 0; i--) {
48015
+ if (queue[i].purpose === opts.purpose) {
48016
+ index = i;
48017
+ break;
48018
+ }
48019
+ }
48020
+ if (index === -1) {
48021
+ return err2("receipt_missing", `no host-observed receipt for purpose ${JSON.stringify(opts.purpose)} — ask the native question for that purpose`);
48022
+ }
48023
+ } else {
48024
+ index = queue.length - 1;
48025
+ }
47904
48026
  const receipt = queue[index];
47905
48027
  if (isNegativeLabel(receipt.selectedLabel)) {
47906
- this.#bySession.delete(sessionId);
48028
+ if (opts.purpose !== undefined) {
48029
+ const filtered = queue.filter((r) => r.purpose !== opts.purpose);
48030
+ if (filtered.length === 0)
48031
+ this.#bySession.delete(sessionId);
48032
+ else
48033
+ this.#bySession.set(sessionId, filtered);
48034
+ } else {
48035
+ this.#bySession.delete(sessionId);
48036
+ }
47907
48037
  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");
47908
48038
  }
47909
48039
  if (opts.label !== undefined && !sameChoiceLabel(receipt.selectedLabel, opts.label)) {
@@ -48095,7 +48225,7 @@ var completeExecution = (root, slug, deps) => {
48095
48225
  }
48096
48226
  const next = {
48097
48227
  ...reconciled.state,
48098
- execution: { ...exec3, status: "completed" },
48228
+ execution: { ...exec3, status: "completed", coordinator_session_id: null },
48099
48229
  handoff_destination: false,
48100
48230
  updated_at: Date.now()
48101
48231
  };
@@ -48155,6 +48285,38 @@ var slugFromPath = (p) => {
48155
48285
  const dirName = path18.basename(path18.dirname(p));
48156
48286
  return dirName === "." || dirName === "/" || dirName === "" ? "" : dirName;
48157
48287
  };
48288
+ var assertSddControlGates = (root, slug, opts = {}, ctx) => {
48289
+ const bound = assertMutationWorkspace(root, ctx);
48290
+ if (!bound.ok)
48291
+ return bound;
48292
+ const effective = readEffectiveFlowState(root, slug);
48293
+ if (!effective.ok)
48294
+ return effective;
48295
+ const state = effective.state;
48296
+ if (state.spec.status !== "approved") {
48297
+ return err2("spec_not_approved", `spec not approved (status: ${state.spec.status}). Run workit_spec_approve after the user's approval.`);
48298
+ }
48299
+ if (state.plan.status !== "approved") {
48300
+ return err2("plan_not_approved", `plan not approved (status: ${state.plan.status}). Run workit_plan_approve after the user's approval.`);
48301
+ }
48302
+ if (opts.requireMenu && !state.menu.presented) {
48303
+ return err2("menu_not_presented", "post-plan menu not presented. Record the native question answer with workit_plan_menu.");
48304
+ }
48305
+ if (opts.requireDocs) {
48306
+ const validated = docsValidate({
48307
+ spec_path: path18.posix.join("docs", slug, "spec.md"),
48308
+ plan_path: path18.posix.join("docs", slug, "plan.md"),
48309
+ workspace_root: root
48310
+ });
48311
+ if (validated.ok === false)
48312
+ return err2("docs_invalid", validated.error);
48313
+ }
48314
+ if (state.execution.status === "active" && state.execution.mode === "subagent-driven" && roleFromParentage(ctx?.parentSessionId, state.execution.coordinator_session_id) === "delegated") {
48315
+ 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");
48316
+ }
48317
+ return { ok: true };
48318
+ };
48319
+ var roleFromParentage = (parentID, coordinatorSessionId) => typeof parentID === "string" && parentID !== "" && parentID === coordinatorSessionId ? "delegated" : "coordinator";
48158
48320
  var BASH_READ_TOKENS = new Set([
48159
48321
  "cat",
48160
48322
  "head",
@@ -48282,12 +48444,12 @@ var COORDINATOR_SHELL_DENIED_TEXT = "Coordinator shell commands are restricted w
48282
48444
  import path20 from "node:path";
48283
48445
 
48284
48446
  // packages/workit-core/src/core/handoff-context.ts
48285
- import { existsSync as existsSync15, readFileSync as readFileSync14, readdirSync as readdirSync5, statSync as statSync8 } from "node:fs";
48447
+ import { existsSync as existsSync15, readFileSync as readFileSync14, readdirSync as readdirSync6, statSync as statSync8 } from "node:fs";
48286
48448
  import path19 from "node:path";
48287
48449
  var DOC_RE = /docs\/([A-Za-z0-9][A-Za-z0-9._-]*)\/(spec|plan)\.md/g;
48288
48450
  var listMd = (dir) => {
48289
48451
  try {
48290
- return readdirSync5(dir).filter((f) => f.endsWith(".md")).sort();
48452
+ return readdirSync6(dir).filter((f) => f.endsWith(".md")).sort();
48291
48453
  } catch {
48292
48454
  return [];
48293
48455
  }
@@ -48313,7 +48475,7 @@ var resolveActivePair = (root) => {
48313
48475
  let best = null;
48314
48476
  let entries = [];
48315
48477
  try {
48316
- entries = readdirSync5(docsDir);
48478
+ entries = readdirSync6(docsDir);
48317
48479
  } catch {
48318
48480
  return { error: "no pair" };
48319
48481
  }
@@ -48405,13 +48567,21 @@ var buildHandoffPrompt = (root, message) => {
48405
48567
  };
48406
48568
 
48407
48569
  // packages/workit-cli/src/flow.ts
48408
- var FLOW_ACTIONS = ["status", "pause", "resume", "complete", "review-package"];
48570
+ var FLOW_ACTIONS = [
48571
+ "status",
48572
+ "pause",
48573
+ "resume",
48574
+ "complete",
48575
+ "review-package",
48576
+ "append-advisory"
48577
+ ];
48409
48578
  var COMMANDS = {
48410
48579
  status: "workit flow status --plan <path>",
48411
48580
  pause: "workit flow pause --plan <path> [--confirm]",
48412
48581
  resume: "workit flow resume --plan <path> [--confirm]",
48413
48582
  complete: "workit flow complete --plan <path> [--confirm]",
48414
48583
  "review-package": "workit flow review-package --plan <path> --base <sha> --head <sha> [--confirm]",
48584
+ "append-advisory": "workit flow append-advisory --plan <path> --task <id> --text <text> [--confirm]",
48415
48585
  handoff: "workit handoff --message <text>"
48416
48586
  };
48417
48587
  var FLOW_COMMANDS = {
@@ -48419,7 +48589,8 @@ var FLOW_COMMANDS = {
48419
48589
  pause: COMMANDS.pause,
48420
48590
  resume: COMMANDS.resume,
48421
48591
  complete: COMMANDS.complete,
48422
- "review-package": COMMANDS["review-package"]
48592
+ "review-package": COMMANDS["review-package"],
48593
+ "append-advisory": COMMANDS["append-advisory"]
48423
48594
  };
48424
48595
  var CLI_FLAG_EVIDENCE = { host: "cli", attested: false, confirmation: "flag" };
48425
48596
  var CLI_TTY_EVIDENCE = { host: "cli", attested: false, confirmation: "tty" };
@@ -48452,14 +48623,22 @@ var usage = (err3, text) => {
48452
48623
  write(err3, text);
48453
48624
  return 2;
48454
48625
  };
48455
- var VALUE_FLAGS = ["--plan", "--base", "--head"];
48626
+ var VALUE_FLAGS = ["--plan", "--base", "--head", "--task", "--text"];
48456
48627
  function parseFlowFlags(action, argv, err3) {
48457
48628
  const parsed = {};
48458
48629
  let confirm = false;
48459
48630
  for (let i = 0;i < argv.length; i++) {
48460
48631
  const token = argv[i];
48461
48632
  if (VALUE_FLAGS.includes(token)) {
48462
- if (token !== "--plan" && action !== "review-package") {
48633
+ if (token !== "--plan" && action !== "review-package" && action !== "append-advisory") {
48634
+ usage(err3, `usage: ${FLOW_COMMANDS[action]} — unknown flag: ${token}`);
48635
+ return { ok: false };
48636
+ }
48637
+ if ((token === "--base" || token === "--head") && action !== "review-package") {
48638
+ usage(err3, `usage: ${FLOW_COMMANDS[action]} — unknown flag: ${token}`);
48639
+ return { ok: false };
48640
+ }
48641
+ if ((token === "--task" || token === "--text") && action !== "append-advisory") {
48463
48642
  usage(err3, `usage: ${FLOW_COMMANDS[action]} — unknown flag: ${token}`);
48464
48643
  return { ok: false };
48465
48644
  }
@@ -48473,7 +48652,7 @@ function parseFlowFlags(action, argv, err3) {
48473
48652
  usage(err3, `usage: ${FLOW_COMMANDS[action]} — unknown flag: ${value}`);
48474
48653
  return { ok: false };
48475
48654
  }
48476
- const existing = token === "--plan" ? parsed.plan : token === "--base" ? parsed.base : parsed.head;
48655
+ const existing = token === "--plan" ? parsed.plan : token === "--base" ? parsed.base : token === "--head" ? parsed.head : token === "--task" ? parsed.task : parsed.text;
48477
48656
  if (existing !== undefined) {
48478
48657
  usage(err3, `usage: ${FLOW_COMMANDS[action]} — duplicate ${token} flag`);
48479
48658
  return { ok: false };
@@ -48482,8 +48661,12 @@ function parseFlowFlags(action, argv, err3) {
48482
48661
  parsed.plan = value;
48483
48662
  else if (token === "--base")
48484
48663
  parsed.base = value;
48485
- else
48664
+ else if (token === "--head")
48486
48665
  parsed.head = value;
48666
+ else if (token === "--task")
48667
+ parsed.task = value;
48668
+ else
48669
+ parsed.text = value;
48487
48670
  i += 1;
48488
48671
  } else if (token === "--confirm") {
48489
48672
  if (action === "status") {
@@ -48514,7 +48697,27 @@ function parseFlowFlags(action, argv, err3) {
48514
48697
  return { ok: false };
48515
48698
  }
48516
48699
  }
48517
- return { ok: true, parsed: { plan: parsed.plan, confirm, base: parsed.base, head: parsed.head } };
48700
+ if (action === "append-advisory") {
48701
+ if (parsed.task === undefined) {
48702
+ usage(err3, `usage: ${FLOW_COMMANDS[action]} — --task <id> required`);
48703
+ return { ok: false };
48704
+ }
48705
+ if (parsed.text === undefined) {
48706
+ usage(err3, `usage: ${FLOW_COMMANDS[action]} — --text <text> required`);
48707
+ return { ok: false };
48708
+ }
48709
+ }
48710
+ return {
48711
+ ok: true,
48712
+ parsed: {
48713
+ plan: parsed.plan,
48714
+ confirm,
48715
+ base: parsed.base,
48716
+ head: parsed.head,
48717
+ task: parsed.task,
48718
+ text: parsed.text
48719
+ }
48720
+ };
48518
48721
  }
48519
48722
  async function resolveConfirmation(deps, confirmFlag, out, err3) {
48520
48723
  if (confirmFlag)
@@ -48590,12 +48793,34 @@ function reviewPackageCommand(root, parsed, out, err3) {
48590
48793
  }
48591
48794
  return domainFail(err3, result2.code ?? "review_package_failed", result2.error);
48592
48795
  }
48796
+ function appendAdvisoryCommand(root, parsed, out, err3) {
48797
+ const resolved = resolveCanonicalLayout({ workspace_root: root, plan_path: parsed.plan });
48798
+ if (!resolved.ok)
48799
+ return domainFail(err3, "path_invalid", resolved.error);
48800
+ const slug = resolved.layout.slug;
48801
+ const controlGate = assertSddControlGates(root, slug, { requireMenu: true, requireDocs: true });
48802
+ if (!controlGate.ok)
48803
+ return domainFail(err3, controlGate.code, controlGate.error);
48804
+ const trimmed = parsed.task.trim();
48805
+ const taskId = /^-?\d+(\.\d+)?$/.test(trimmed) ? Number(trimmed) : trimmed;
48806
+ const result2 = sddAppendAdvisory({
48807
+ advisories_path: path21.posix.join("docs", slug, "sdd", "advisories.md"),
48808
+ task_id: taskId,
48809
+ text: parsed.text,
48810
+ workspace_root: root
48811
+ });
48812
+ if ("error" in result2) {
48813
+ return domainFail(err3, result2.code ?? "advisory_failed", result2.error);
48814
+ }
48815
+ writeJSON(out, { ok: true, advisories_path: result2.advisories_path, advisory: result2.advisory });
48816
+ return 0;
48817
+ }
48593
48818
  async function runFlowCommand(argv, deps = {}) {
48594
48819
  const out = outStream(deps);
48595
48820
  const err3 = errStream(deps);
48596
48821
  const [action, ...rest2] = argv;
48597
48822
  if (!action || !FLOW_ACTIONS.includes(action)) {
48598
- return usage(err3, "usage: workit flow <status|pause|resume|complete|review-package> --plan <path> [--confirm]");
48823
+ return usage(err3, "usage: workit flow <status|pause|resume|complete|review-package|append-advisory> --plan <path> [--confirm]");
48599
48824
  }
48600
48825
  const flowAction = action;
48601
48826
  const parsed = parseFlowFlags(flowAction, rest2, err3);
@@ -48611,6 +48836,9 @@ async function runFlowCommand(argv, deps = {}) {
48611
48836
  if (flowAction === "review-package") {
48612
48837
  return reviewPackageCommand(root, parsed.parsed, out, err3);
48613
48838
  }
48839
+ if (flowAction === "append-advisory") {
48840
+ return appendAdvisoryCommand(root, parsed.parsed, out, err3);
48841
+ }
48614
48842
  return mutateCommand(root, parsed.parsed.plan, flowAction, evidence, deps, out, err3);
48615
48843
  }
48616
48844
  async function runHandoffCommand(argv, deps = {}) {
@@ -48649,6 +48877,7 @@ var COMMAND_DESCRIPTIONS = [
48649
48877
  [COMMANDS.resume, "Resume a paused plan"],
48650
48878
  [COMMANDS.complete, "Complete a plan (ledger and verification gated)"],
48651
48879
  [COMMANDS["review-package"], "Write a review diff for a base..head range"],
48880
+ [COMMANDS["append-advisory"], "Append an advisory line to docs/<slug>/sdd/advisories.md"],
48652
48881
  [COMMANDS.handoff, "Emit the destination handoff prompt for a plan"]
48653
48882
  ];
48654
48883
  var helpColumn = Math.max(...COMMAND_DESCRIPTIONS.map(([cmd]) => cmd.length)) + 2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-cli",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "private": false,
5
5
  "description": "Workit interactive setup wizard (Ink TUI)",
6
6
  "keywords": [
@@ -34,9 +34,9 @@
34
34
  "typecheck": "tsc --noEmit"
35
35
  },
36
36
  "dependencies": {
37
- "@brainervirus/workit-core": "^0.8.8",
38
- "@brainervirus/workit-cursor": "^0.8.8",
39
- "@brainervirus/workit-opencode": "^0.8.8",
37
+ "@brainervirus/workit-core": "^0.8.10",
38
+ "@brainervirus/workit-cursor": "^0.8.10",
39
+ "@brainervirus/workit-opencode": "^0.8.10",
40
40
  "@inkjs/ui": "2.0.0",
41
41
  "ink": "7.1.1",
42
42
  "react": "19.2.8"