@brainervirus/workit-cli 0.8.7 → 0.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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,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 to `<SDD_DIR>/advisories.md`.
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 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
@@ -43490,7 +43490,11 @@ var checkLauncher = (res) => {
43490
43490
  detail: "no dev checkout found (WORKFLOW_TOOLKIT_DEV) — skipping non-Cursor launcher checks"
43491
43491
  };
43492
43492
  }
43493
- return { id: "launcher", status: "pass", detail: `${res.host} launcher/hook entries present` };
43493
+ return {
43494
+ id: "launcher",
43495
+ status: "pass",
43496
+ detail: `${res.host} launcher/hook entries present`
43497
+ };
43494
43498
  };
43495
43499
  var checkUtility = (res) => {
43496
43500
  const git = commandOnPath("git", res.env);
@@ -43510,7 +43514,11 @@ var checkUtility = (res) => {
43510
43514
  fix: "Install util-linux (flock)"
43511
43515
  };
43512
43516
  }
43513
- return { id: "utility", status: "pass", detail: "git (and flock where required) on PATH" };
43517
+ return {
43518
+ id: "utility",
43519
+ status: "pass",
43520
+ detail: "git (and flock where required) on PATH"
43521
+ };
43514
43522
  };
43515
43523
  var staleEntry = (entry) => {
43516
43524
  if (!entry)
@@ -43532,7 +43540,11 @@ var checkStalePin = (res) => {
43532
43540
  };
43533
43541
  }
43534
43542
  if (!existsSync7(res.opencodeConfig)) {
43535
- return { id: "stale_pin", status: "pass", detail: "no opencode config — not registered" };
43543
+ return {
43544
+ id: "stale_pin",
43545
+ status: "pass",
43546
+ detail: "no opencode config — not registered"
43547
+ };
43536
43548
  }
43537
43549
  const cfg = readJson(res.opencodeConfig);
43538
43550
  const entries = pluginEntries(cfg);
@@ -43563,6 +43575,118 @@ var checkStalePin = (res) => {
43563
43575
  }
43564
43576
  return { id: "stale_pin", status: "pass", detail: "opencode pin resolves" };
43565
43577
  };
43578
+ var pluginMcpSelectors = (res) => {
43579
+ const p = path9.join(res.cursorPluginDir, "mcp.json");
43580
+ if (!existsSync7(p))
43581
+ return null;
43582
+ const cfg = readJson(p);
43583
+ const server = cfg?.mcpServers?.workit;
43584
+ if (!server || typeof server !== "object" || Array.isArray(server))
43585
+ return null;
43586
+ const args = server.args;
43587
+ if (!Array.isArray(args))
43588
+ return null;
43589
+ return args.map(String).filter((a) => a.startsWith("--package="));
43590
+ };
43591
+ var registryLatestVersion = (res) => {
43592
+ const seam = res.env.WORKIT_DOCTOR_STALE_REGISTRY_VERSION;
43593
+ if (typeof seam === "string" && seam) {
43594
+ return /^\d+(?:\.\d+){1,2}$/.test(seam) ? seam : null;
43595
+ }
43596
+ const cmd = res.env.WORKIT_DOCTOR_STALE_REGISTRY_CMD ?? (commandOnPath("npm", res.env) ? "npm" : null);
43597
+ if (!cmd)
43598
+ return null;
43599
+ try {
43600
+ const r = spawnSync2(cmd, ["view", CURSOR_RUNTIME_PACKAGE, "version"], {
43601
+ encoding: "utf8",
43602
+ timeout: 20000,
43603
+ env: res.env
43604
+ });
43605
+ const v = (r.stdout ?? "").trim().split(/\s+/)[0];
43606
+ return r.status === 0 && /^\d+(?:\.\d+){1,2}$/.test(v) ? v : null;
43607
+ } catch {
43608
+ return null;
43609
+ }
43610
+ };
43611
+ var installedPluginVersion = (res) => {
43612
+ const pkg = readJson(path9.join(res.cursorPluginDir, "package.json"));
43613
+ return typeof pkg?.version === "string" && pkg.version ? pkg.version : null;
43614
+ };
43615
+ var checkStaleInstall = (res) => {
43616
+ if (res.host !== "cursor" && res.host !== "cli") {
43617
+ return {
43618
+ id: "stale_install",
43619
+ status: "pass",
43620
+ detail: "cursor plugin not inspected on the opencode host"
43621
+ };
43622
+ }
43623
+ const canonicalMcp = cursorMcpServerEntry("").args.find((a) => a.startsWith("--package="));
43624
+ const mcpSelectors = pluginMcpSelectors(res);
43625
+ const legacyPin = (mcpSelectors ?? []).find((s) => s !== canonicalMcp && s.includes("@brainervirus/workit-cursor@"));
43626
+ if (legacyPin) {
43627
+ return {
43628
+ id: "stale_install",
43629
+ status: "fail",
43630
+ detail: `stale_install: plugin mcp.json pins a legacy selector ${legacyPin} (canonical: ${canonicalMcp})`,
43631
+ fix: "Re-run install-cursor-plugin.sh — it rewrites the workit MCP entry to the canonical @latest selector"
43632
+ };
43633
+ }
43634
+ const hooksFile = path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json");
43635
+ const hookCmd = readJson(hooksFile)?.hooks?.sessionStart?.[0]?.command ?? null;
43636
+ const canonicalHook = canonicalCursorHook;
43637
+ const staleHook = hookCmd !== null && hookCmd !== canonicalHook && !hookCmd.startsWith("node ") && hookCmd.includes("@brainervirus/workit-cursor@");
43638
+ if (staleHook) {
43639
+ return {
43640
+ id: "stale_install",
43641
+ status: "fail",
43642
+ detail: `stale_install: sessionStart hook runs a legacy selector (canonical: ${canonicalHook})`,
43643
+ fix: "Re-run install-cursor-plugin.sh — it rewrites the sessionStart hook to the canonical @latest selector"
43644
+ };
43645
+ }
43646
+ const installed = installedPluginVersion(res);
43647
+ const source = res.dev ? readJson(path9.join(res.dev, "packages/workit-cursor/package.json"))?.version ?? null : readJson(path9.join(packageRoot(), "package.json"))?.version ?? null;
43648
+ const localDist = mcpSelectors === null && hookCmd !== null && hookCmd.startsWith("node ");
43649
+ if (localDist && installed !== null && source !== null && !semverAtLeast(installed, source)) {
43650
+ return {
43651
+ id: "stale_install",
43652
+ status: "fail",
43653
+ detail: `stale_install: installed workit-cursor ${installed} is behind the current runtime ${source}`,
43654
+ fix: "Re-run install-cursor-plugin.sh — it refreshes the plugin directory and rewrites the workit MCP/hook entries"
43655
+ };
43656
+ }
43657
+ if (installed !== null && localDist) {
43658
+ const expected = registryLatestVersion(res);
43659
+ if (expected === null) {
43660
+ return {
43661
+ id: "registry_unreachable",
43662
+ status: "warn",
43663
+ registryProbed: true,
43664
+ detail: "registry_unreachable: cannot compare installed workit-cursor against the published runtime",
43665
+ fix: "Retry when the npm registry is reachable, or re-run install-cursor-plugin.sh to refresh the install"
43666
+ };
43667
+ }
43668
+ if (!semverAtLeast(installed, expected)) {
43669
+ return {
43670
+ id: "stale_install",
43671
+ status: "fail",
43672
+ registryProbed: true,
43673
+ detail: `stale_install: local-dist workit-cursor ${installed} is behind the published runtime ${expected}`,
43674
+ fix: "Re-run install-cursor-plugin.sh — it refreshes the plugin directory with the current build"
43675
+ };
43676
+ }
43677
+ return {
43678
+ id: "stale_install",
43679
+ status: "pass",
43680
+ registryProbed: true,
43681
+ detail: `installed local-dist workit-cursor ${installed} matches the published runtime ${expected}`
43682
+ };
43683
+ }
43684
+ return {
43685
+ id: "stale_install",
43686
+ status: "pass",
43687
+ detail: installed === null ? "installed workit-cursor selectors are canonical" : `installed workit-cursor ${installed} is metadata on the canonical @latest install (fresh at launch)`
43688
+ };
43689
+ };
43566
43690
  var checkDuplicateRegistration = (res) => {
43567
43691
  const problems = [];
43568
43692
  const opencodeHost = res.host !== "cursor";
@@ -43598,7 +43722,11 @@ var checkDuplicateRegistration = (res) => {
43598
43722
  }
43599
43723
  }
43600
43724
  if (problems.length === 0) {
43601
- return { id: "duplicate_registration", status: "pass", detail: "no duplicate registrations" };
43725
+ return {
43726
+ id: "duplicate_registration",
43727
+ status: "pass",
43728
+ detail: "no duplicate registrations"
43729
+ };
43602
43730
  }
43603
43731
  return {
43604
43732
  id: "duplicate_registration",
@@ -43624,7 +43752,11 @@ var checkMalformedConfig = (res) => {
43624
43752
  files.push(res.cursorMcp);
43625
43753
  const bad = files.filter((p) => !parsesAsConfigObject(p));
43626
43754
  if (bad.length === 0)
43627
- return { id: "malformed_config", status: "pass", detail: "config files parse" };
43755
+ return {
43756
+ id: "malformed_config",
43757
+ status: "pass",
43758
+ detail: "config files parse"
43759
+ };
43628
43760
  return {
43629
43761
  id: "malformed_config",
43630
43762
  status: "fail",
@@ -43635,10 +43767,18 @@ var checkMalformedConfig = (res) => {
43635
43767
  var checkWorkspaceMismatch = (res) => {
43636
43768
  const file = path9.join(res.configDir, "workspaces.json");
43637
43769
  if (!existsSync7(file))
43638
- return { id: "workspace_mismatch", status: "pass", detail: "no workspaces configured" };
43770
+ return {
43771
+ id: "workspace_mismatch",
43772
+ status: "pass",
43773
+ detail: "no workspaces configured"
43774
+ };
43639
43775
  const ws = readJson(file);
43640
43776
  if (!Array.isArray(ws?.workspaces)) {
43641
- return { id: "workspace_mismatch", status: "pass", detail: "no workspaces configured" };
43777
+ return {
43778
+ id: "workspace_mismatch",
43779
+ status: "pass",
43780
+ detail: "no workspaces configured"
43781
+ };
43642
43782
  }
43643
43783
  const match = resolveWorkspaceFrom(res.cwd, res.configDir);
43644
43784
  if (match)
@@ -43680,7 +43820,11 @@ var checkCredentialMetadata = (res) => {
43680
43820
  }
43681
43821
  }
43682
43822
  if (tokenPaths.length === 0) {
43683
- return { id: "credential_metadata", status: "pass", detail: "no credentials configured" };
43823
+ return {
43824
+ id: "credential_metadata",
43825
+ status: "pass",
43826
+ detail: "no credentials configured"
43827
+ };
43684
43828
  }
43685
43829
  const problems = [];
43686
43830
  for (const raw of tokenPaths) {
@@ -43718,7 +43862,11 @@ var checkLogWritable = (res) => {
43718
43862
  mkdirSync2(logsDir, { recursive: true, mode: 448 });
43719
43863
  writeFileSync4(probe, `{"probe":true}
43720
43864
  `, { mode: 384 });
43721
- return { id: "log_writable", status: "pass", detail: "log directory writable" };
43865
+ return {
43866
+ id: "log_writable",
43867
+ status: "pass",
43868
+ detail: "log directory writable"
43869
+ };
43722
43870
  } catch (err) {
43723
43871
  return {
43724
43872
  id: "log_writable",
@@ -43739,6 +43887,7 @@ var RUN_CHECKS = [
43739
43887
  checkLauncher,
43740
43888
  checkUtility,
43741
43889
  checkStalePin,
43890
+ checkStaleInstall,
43742
43891
  checkDuplicateRegistration,
43743
43892
  checkMalformedConfig,
43744
43893
  checkWorkspaceMismatch,
@@ -43751,21 +43900,27 @@ var INSTALLER_REQUIRED = new Set([
43751
43900
  "launcher",
43752
43901
  "utility",
43753
43902
  "stale_pin",
43903
+ "stale_install",
43754
43904
  "duplicate_registration",
43755
43905
  "malformed_config"
43756
43906
  ]);
43757
43907
  var runDoctor = (options = {}) => {
43758
43908
  const res = resolve(options);
43759
43909
  const raw = RUN_CHECKS.map((fn) => fn(res));
43760
- const checks = res.installer ? raw.map((c) => c.status === "fail" && !INSTALLER_REQUIRED.has(c.id) ? { ...c, status: "warn", detail: `${c.detail} (not enforced by installer)` } : c) : raw;
43910
+ const checks = res.installer ? raw.map((c) => c.status === "fail" && !INSTALLER_REQUIRED.has(c.id) ? {
43911
+ ...c,
43912
+ status: "warn",
43913
+ detail: `${c.detail} (not enforced by installer)`
43914
+ } : c) : raw;
43761
43915
  const failed = checks.filter((c) => c.status === "fail").length;
43762
43916
  const warned = checks.filter((c) => c.status === "warn").length;
43763
43917
  const passed = checks.filter((c) => c.status === "pass").length;
43764
43918
  const exitCode = failed > 0 ? 1 : 0;
43919
+ const offline = !raw.some((c) => ("registryProbed" in c) && c.registryProbed);
43765
43920
  const report = {
43766
43921
  ok: failed === 0,
43767
43922
  exitCode,
43768
- offline: true,
43923
+ offline,
43769
43924
  host: res.host,
43770
43925
  checked_at: new Date().toISOString(),
43771
43926
  summary: { passed, warned, failed, total: checks.length },
@@ -43775,7 +43930,7 @@ var runDoctor = (options = {}) => {
43775
43930
  getDiagnosticLogger()?.info(EVENT.doctor, {
43776
43931
  host: res.host,
43777
43932
  exit_code: exitCode,
43778
- offline: true,
43933
+ offline,
43779
43934
  failed: checks.filter((c) => c.status === "fail").map((c) => c.id),
43780
43935
  total: checks.length
43781
43936
  });
@@ -46281,6 +46436,7 @@ import {
46281
46436
  fsyncSync,
46282
46437
  mkdirSync as mkdirSync8,
46283
46438
  openSync,
46439
+ readdirSync as readdirSync5,
46284
46440
  readFileSync as readFileSync13,
46285
46441
  renameSync as renameSync2,
46286
46442
  rmSync as rmSync2,
@@ -46827,6 +46983,58 @@ function sddReviewPackage({
46827
46983
  return { error: error instanceof Error ? error.message : "git diff failed" };
46828
46984
  }
46829
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
+ }
46830
47038
 
46831
47039
  // packages/workit-core/src/core/verify-project.ts
46832
47040
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -47215,7 +47423,7 @@ root: ${cwd2}
47215
47423
  }
47216
47424
 
47217
47425
  // packages/workit-core/src/core/flow-state.ts
47218
- 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.";
47219
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`.";
47220
47428
  var MENU_CHOICES = [
47221
47429
  "subagent-driven",
@@ -47348,6 +47556,12 @@ var validateState = (parsed, slug) => {
47348
47556
  if (execRaw?.evidence !== undefined && !validateEvidenceValue(execRaw.evidence, true)) {
47349
47557
  return { ok: false, error: "flow state execution.evidence has an unsupported shape" };
47350
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
+ }
47351
47565
  return {
47352
47566
  ok: true,
47353
47567
  state: {
@@ -47363,7 +47577,8 @@ var validateState = (parsed, slug) => {
47363
47577
  execution: {
47364
47578
  status: execRaw?.status ?? "pending",
47365
47579
  mode: execRaw?.mode ?? null,
47366
- evidence: execRaw?.evidence ?? null
47580
+ evidence: execRaw?.evidence ?? null,
47581
+ coordinator_session_id: execRaw?.coordinator_session_id ?? null
47367
47582
  },
47368
47583
  handoff_destination: parsed.handoff_destination ?? false,
47369
47584
  updated_at: parsed.updated_at ?? Date.now()
@@ -47500,7 +47715,7 @@ var resetForSpecDrift = (state) => ({
47500
47715
  spec: { ...state.spec, status: "draft", evidence: null, approved_digest: null },
47501
47716
  plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
47502
47717
  menu: { presented: false, chosen: "", evidence: null },
47503
- execution: { status: "pending", mode: null, evidence: null },
47718
+ execution: { status: "pending", mode: null, evidence: null, coordinator_session_id: null },
47504
47719
  handoff_destination: false,
47505
47720
  updated_at: Date.now()
47506
47721
  });
@@ -47543,9 +47758,14 @@ var reconcileState = (root, slug, state) => {
47543
47758
  var deriveLegacyExecution = (root, slug, state) => {
47544
47759
  const ledger = ledgerCompletion(root, slug);
47545
47760
  if (state.plan.status === "approved" && state.menu.chosen === "subagent-driven" && ledger.started && !ledger.complete) {
47546
- 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
+ };
47547
47767
  }
47548
- return { status: "pending", mode: null, evidence: null };
47768
+ return { status: "pending", mode: null, evidence: null, coordinator_session_id: null };
47549
47769
  };
47550
47770
  var normalizeCompatibility = (root, slug, parsed, state) => {
47551
47771
  if (!isRecord3(parsed) || !("execution" in parsed)) {
@@ -47554,6 +47774,11 @@ var normalizeCompatibility = (root, slug, parsed, state) => {
47554
47774
  if (derived.status !== current.status || derived.mode !== current.mode) {
47555
47775
  return { state: { ...state, execution: derived, updated_at: Date.now() }, changed: true };
47556
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 };
47557
47782
  }
47558
47783
  return { state, changed: false };
47559
47784
  };
@@ -47716,19 +47941,49 @@ var isNegativeLabel = (label) => {
47716
47941
  return firstWord.replace(/[^a-z]/g, "") === entry;
47717
47942
  });
47718
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
+ };
47719
47970
 
47720
47971
  class HostReceiptStore {
47721
47972
  #bySession = new Map;
47722
- record(sessionId, callID, selectedLabel, recordedAt = Date.now(), question) {
47723
- const label = selectedLabel.trim();
47724
- if (!label)
47973
+ record(sessionId, callID, selectedLabel, recordedAt = Date.now(), question = "", purpose) {
47974
+ const trimmed = selectedLabel.trim();
47975
+ if (!trimmed)
47725
47976
  return;
47726
47977
  if (recordedAt > Date.now() + MAX_CLOCK_SKEW_MS)
47727
47978
  return;
47979
+ const derived = purpose ?? receiptPurposeForLabel(selectedLabel);
47980
+ if (derived === undefined)
47981
+ return;
47982
+ const q = question ?? "";
47728
47983
  const queue = this.#bySession.get(sessionId) ?? [];
47729
47984
  if (queue.length >= MAX_RECEIPTS_PER_SESSION)
47730
47985
  queue.shift();
47731
- queue.push({ sessionId, callID, selectedLabel: label, recordedAt, question });
47986
+ queue.push({ sessionId, callID, selectedLabel, recordedAt, question: q, purpose: derived });
47732
47987
  this.#bySession.set(sessionId, queue);
47733
47988
  }
47734
47989
  count(sessionId) {
@@ -47745,10 +48000,40 @@ class HostReceiptStore {
47745
48000
  if (!queue || queue.length === 0) {
47746
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");
47747
48002
  }
47748
- 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
+ }
47749
48026
  const receipt = queue[index];
47750
48027
  if (isNegativeLabel(receipt.selectedLabel)) {
47751
- 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
+ }
47752
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");
47753
48038
  }
47754
48039
  if (opts.label !== undefined && !sameChoiceLabel(receipt.selectedLabel, opts.label)) {
@@ -47940,7 +48225,7 @@ var completeExecution = (root, slug, deps) => {
47940
48225
  }
47941
48226
  const next = {
47942
48227
  ...reconciled.state,
47943
- execution: { ...exec3, status: "completed" },
48228
+ execution: { ...exec3, status: "completed", coordinator_session_id: null },
47944
48229
  handoff_destination: false,
47945
48230
  updated_at: Date.now()
47946
48231
  };
@@ -48000,6 +48285,38 @@ var slugFromPath = (p) => {
48000
48285
  const dirName = path18.basename(path18.dirname(p));
48001
48286
  return dirName === "." || dirName === "/" || dirName === "" ? "" : dirName;
48002
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 workflow_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 workflow_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 workflow_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";
48003
48320
  var BASH_READ_TOKENS = new Set([
48004
48321
  "cat",
48005
48322
  "head",
@@ -48127,12 +48444,12 @@ var COORDINATOR_SHELL_DENIED_TEXT = "Coordinator shell commands are restricted w
48127
48444
  import path20 from "node:path";
48128
48445
 
48129
48446
  // packages/workit-core/src/core/handoff-context.ts
48130
- 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";
48131
48448
  import path19 from "node:path";
48132
48449
  var DOC_RE = /docs\/([A-Za-z0-9][A-Za-z0-9._-]*)\/(spec|plan)\.md/g;
48133
48450
  var listMd = (dir) => {
48134
48451
  try {
48135
- return readdirSync5(dir).filter((f) => f.endsWith(".md")).sort();
48452
+ return readdirSync6(dir).filter((f) => f.endsWith(".md")).sort();
48136
48453
  } catch {
48137
48454
  return [];
48138
48455
  }
@@ -48158,7 +48475,7 @@ var resolveActivePair = (root) => {
48158
48475
  let best = null;
48159
48476
  let entries = [];
48160
48477
  try {
48161
- entries = readdirSync5(docsDir);
48478
+ entries = readdirSync6(docsDir);
48162
48479
  } catch {
48163
48480
  return { error: "no pair" };
48164
48481
  }
@@ -48250,13 +48567,21 @@ var buildHandoffPrompt = (root, message) => {
48250
48567
  };
48251
48568
 
48252
48569
  // packages/workit-cli/src/flow.ts
48253
- 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
+ ];
48254
48578
  var COMMANDS = {
48255
48579
  status: "workit flow status --plan <path>",
48256
48580
  pause: "workit flow pause --plan <path> [--confirm]",
48257
48581
  resume: "workit flow resume --plan <path> [--confirm]",
48258
48582
  complete: "workit flow complete --plan <path> [--confirm]",
48259
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]",
48260
48585
  handoff: "workit handoff --message <text>"
48261
48586
  };
48262
48587
  var FLOW_COMMANDS = {
@@ -48264,7 +48589,8 @@ var FLOW_COMMANDS = {
48264
48589
  pause: COMMANDS.pause,
48265
48590
  resume: COMMANDS.resume,
48266
48591
  complete: COMMANDS.complete,
48267
- "review-package": COMMANDS["review-package"]
48592
+ "review-package": COMMANDS["review-package"],
48593
+ "append-advisory": COMMANDS["append-advisory"]
48268
48594
  };
48269
48595
  var CLI_FLAG_EVIDENCE = { host: "cli", attested: false, confirmation: "flag" };
48270
48596
  var CLI_TTY_EVIDENCE = { host: "cli", attested: false, confirmation: "tty" };
@@ -48297,14 +48623,22 @@ var usage = (err3, text) => {
48297
48623
  write(err3, text);
48298
48624
  return 2;
48299
48625
  };
48300
- var VALUE_FLAGS = ["--plan", "--base", "--head"];
48626
+ var VALUE_FLAGS = ["--plan", "--base", "--head", "--task", "--text"];
48301
48627
  function parseFlowFlags(action, argv, err3) {
48302
48628
  const parsed = {};
48303
48629
  let confirm = false;
48304
48630
  for (let i = 0;i < argv.length; i++) {
48305
48631
  const token = argv[i];
48306
48632
  if (VALUE_FLAGS.includes(token)) {
48307
- 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") {
48308
48642
  usage(err3, `usage: ${FLOW_COMMANDS[action]} — unknown flag: ${token}`);
48309
48643
  return { ok: false };
48310
48644
  }
@@ -48318,7 +48652,7 @@ function parseFlowFlags(action, argv, err3) {
48318
48652
  usage(err3, `usage: ${FLOW_COMMANDS[action]} — unknown flag: ${value}`);
48319
48653
  return { ok: false };
48320
48654
  }
48321
- 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;
48322
48656
  if (existing !== undefined) {
48323
48657
  usage(err3, `usage: ${FLOW_COMMANDS[action]} — duplicate ${token} flag`);
48324
48658
  return { ok: false };
@@ -48327,8 +48661,12 @@ function parseFlowFlags(action, argv, err3) {
48327
48661
  parsed.plan = value;
48328
48662
  else if (token === "--base")
48329
48663
  parsed.base = value;
48330
- else
48664
+ else if (token === "--head")
48331
48665
  parsed.head = value;
48666
+ else if (token === "--task")
48667
+ parsed.task = value;
48668
+ else
48669
+ parsed.text = value;
48332
48670
  i += 1;
48333
48671
  } else if (token === "--confirm") {
48334
48672
  if (action === "status") {
@@ -48359,7 +48697,27 @@ function parseFlowFlags(action, argv, err3) {
48359
48697
  return { ok: false };
48360
48698
  }
48361
48699
  }
48362
- 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
+ };
48363
48721
  }
48364
48722
  async function resolveConfirmation(deps, confirmFlag, out, err3) {
48365
48723
  if (confirmFlag)
@@ -48435,12 +48793,34 @@ function reviewPackageCommand(root, parsed, out, err3) {
48435
48793
  }
48436
48794
  return domainFail(err3, result2.code ?? "review_package_failed", result2.error);
48437
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
+ }
48438
48818
  async function runFlowCommand(argv, deps = {}) {
48439
48819
  const out = outStream(deps);
48440
48820
  const err3 = errStream(deps);
48441
48821
  const [action, ...rest2] = argv;
48442
48822
  if (!action || !FLOW_ACTIONS.includes(action)) {
48443
- 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]");
48444
48824
  }
48445
48825
  const flowAction = action;
48446
48826
  const parsed = parseFlowFlags(flowAction, rest2, err3);
@@ -48456,6 +48836,9 @@ async function runFlowCommand(argv, deps = {}) {
48456
48836
  if (flowAction === "review-package") {
48457
48837
  return reviewPackageCommand(root, parsed.parsed, out, err3);
48458
48838
  }
48839
+ if (flowAction === "append-advisory") {
48840
+ return appendAdvisoryCommand(root, parsed.parsed, out, err3);
48841
+ }
48459
48842
  return mutateCommand(root, parsed.parsed.plan, flowAction, evidence, deps, out, err3);
48460
48843
  }
48461
48844
  async function runHandoffCommand(argv, deps = {}) {
@@ -48494,6 +48877,7 @@ var COMMAND_DESCRIPTIONS = [
48494
48877
  [COMMANDS.resume, "Resume a paused plan"],
48495
48878
  [COMMANDS.complete, "Complete a plan (ledger and verification gated)"],
48496
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"],
48497
48881
  [COMMANDS.handoff, "Emit the destination handoff prompt for a plan"]
48498
48882
  ];
48499
48883
  var helpColumn = Math.max(...COMMAND_DESCRIPTIONS.map(([cmd]) => cmd.length)) + 2;
@@ -48582,7 +48966,7 @@ function runDoctorCommand(args) {
48582
48966
  if (args.includes("--json")) {
48583
48967
  console.log(JSON.stringify(report, null, 2));
48584
48968
  } else {
48585
- console.log(`workit doctor — ${report.ok ? "healthy" : "problems found"} (offline)`);
48969
+ console.log(`workit doctor — ${report.ok ? "healthy" : "problems found"} (${report.offline ? "offline" : "online"})`);
48586
48970
  for (const check2 of report.checks) {
48587
48971
  const mark = check2.status === "fail" ? "FAIL" : check2.status === "warn" ? "WARN" : "ok ";
48588
48972
  console.log(`${mark} ${check2.id} — ${check2.detail}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-cli",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
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.7",
38
- "@brainervirus/workit-cursor": "^0.8.7",
39
- "@brainervirus/workit-opencode": "^0.8.7",
37
+ "@brainervirus/workit-core": "^0.8.9",
38
+ "@brainervirus/workit-cursor": "^0.8.9",
39
+ "@brainervirus/workit-opencode": "^0.8.9",
40
40
  "@inkjs/ui": "2.0.0",
41
41
  "ink": "7.1.1",
42
42
  "react": "19.2.8"