@davidbalzan/groundwork-console 0.1.1 → 0.1.4

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.
Files changed (102) hide show
  1. package/README.md +5 -1
  2. package/bin/groundwork-console.mjs +12 -3
  3. package/dist/answered.js +35 -2
  4. package/dist/groundwork.js +8 -3
  5. package/dist/kitresolve.js +17 -0
  6. package/dist/mcp.js +12 -1
  7. package/dist/provision.js +23 -2
  8. package/dist/seam.js +4 -0
  9. package/dist/server.js +69 -9
  10. package/dist/skills.js +13 -5
  11. package/package.json +1 -1
  12. package/public/assets/{abnfDiagram-N423BO3Z-DPhyKd1N.js → abnfDiagram-N423BO3Z-DjcI5EtN.js} +1 -1
  13. package/public/assets/{arc-DmkLmHHi.js → arc-CLreMRTi.js} +1 -1
  14. package/public/assets/architecture-TIHT7OUA-BxHBbCM_.js +1 -0
  15. package/public/assets/{architectureDiagram-T3A2C74G-BXFkjpze.js → architectureDiagram-T3A2C74G-GVsKASaE.js} +1 -1
  16. package/public/assets/{blockDiagram-VBNYF7ZC-Ddhca5b4.js → blockDiagram-VBNYF7ZC-kUIhKmE4.js} +1 -1
  17. package/public/assets/{c4Diagram-5PPSVZJV-D4M2Oz2j.js → c4Diagram-5PPSVZJV-BQFI1po0.js} +1 -1
  18. package/public/assets/channel-DiEdto8Z.js +1 -0
  19. package/public/assets/{chunk-2GRJ4B5K-B2cLnEJA.js → chunk-2GRJ4B5K-CAC9E6Lc.js} +1 -1
  20. package/public/assets/{chunk-4I5QYGJK-CVuqmZe1.js → chunk-4I5QYGJK-Do801ucH.js} +1 -1
  21. package/public/assets/{chunk-5RXB4S5H-CAj33Qry.js → chunk-5RXB4S5H-DgGb8Ei0.js} +1 -1
  22. package/public/assets/{chunk-6Q2QTUOP-B6kTvnZS.js → chunk-6Q2QTUOP-N3v574-9.js} +1 -1
  23. package/public/assets/{chunk-7Z6QIM7H-BCYMaGkK.js → chunk-7Z6QIM7H-BGVpdvGU.js} +1 -1
  24. package/public/assets/{chunk-GF5L2VYU-eAfQwP-A.js → chunk-GF5L2VYU-CyQ84gnX.js} +1 -1
  25. package/public/assets/{chunk-I66GZJ75-DyWmv43M.js → chunk-I66GZJ75-Bazokq-r.js} +1 -1
  26. package/public/assets/{chunk-JQJVKLGR-xpwVLHiM.js → chunk-JQJVKLGR-BXG1sCkI.js} +1 -1
  27. package/public/assets/{chunk-KBJHAD2P-BsIw_3BM.js → chunk-KBJHAD2P-Bi0agcVN.js} +1 -1
  28. package/public/assets/{chunk-NSK5VX7P-Dy4p7ORp.js → chunk-NSK5VX7P-CRNBZms6.js} +1 -1
  29. package/public/assets/{chunk-QR6OTTB3-D8k2NFkq.js → chunk-QR6OTTB3-BVNnO0nC.js} +1 -1
  30. package/public/assets/{chunk-RYQCIY6F-TbGMiTbW.js → chunk-RYQCIY6F-BDA4j6A-.js} +1 -1
  31. package/public/assets/{chunk-UBXNYLIW-By3VqDlw.js → chunk-UBXNYLIW-DGeBgwe-.js} +1 -1
  32. package/public/assets/{chunk-W5SLKNZC-B6TSWP8U.js → chunk-W5SLKNZC-xcy3l2eH.js} +1 -1
  33. package/public/assets/{chunk-WRU74C26-CqCOF3fo.js → chunk-WRU74C26-CIO3U59-.js} +1 -1
  34. package/public/assets/{chunk-XXDRQBXY-B6BeKGFV.js → chunk-XXDRQBXY-lsNR2odJ.js} +1 -1
  35. package/public/assets/classDiagram-JCYQIIEL-DQfOF2lx.js +1 -0
  36. package/public/assets/classDiagram-v2-OCEON4UE-DQfOF2lx.js +1 -0
  37. package/public/assets/{cose-bilkent-JH36ORCC-2ieLCSa1.js → cose-bilkent-JH36ORCC-Dgq33rVP.js} +1 -1
  38. package/public/assets/{cynefin-VYW2F7L2-BfajhBFb.js → cynefin-VYW2F7L2-BHP3DZqn.js} +1 -1
  39. package/public/assets/{cynefinDiagram-MW4NZA55-DZFY7WjS.js → cynefinDiagram-MW4NZA55-BmxpLFSH.js} +1 -1
  40. package/public/assets/{dagre-VZM6K2ZE-DECeuEL2.js → dagre-VZM6K2ZE-3sD0lAzD.js} +1 -1
  41. package/public/assets/{diagram-7IWD3JNH-DA6fCDep.js → diagram-7IWD3JNH-CyaD0_jh.js} +1 -1
  42. package/public/assets/{diagram-B4RE2ZJO-Dj8QoIQR.js → diagram-B4RE2ZJO-td2N4qE1.js} +1 -1
  43. package/public/assets/{diagram-LBJQPF4R-9JYAugOO.js → diagram-LBJQPF4R-D_wWDJ4F.js} +1 -1
  44. package/public/assets/{diagram-Q27KOJAE-ecTfssa6.js → diagram-Q27KOJAE-DKPs8Pt1.js} +1 -1
  45. package/public/assets/{diagram-UB23O5K3-DOGgwSDm.js → diagram-UB23O5K3-DMH09z4H.js} +1 -1
  46. package/public/assets/{dist-DTWyzWtG.js → dist-BFMCu1a5.js} +1 -1
  47. package/public/assets/{ebnfDiagram-BXEA7PRR-BGmWK80p.js → ebnfDiagram-BXEA7PRR-Cl2AEyY9.js} +1 -1
  48. package/public/assets/{erDiagram-JOGREHBK-CNl1QvhN.js → erDiagram-JOGREHBK-CyAJ5sjB.js} +1 -1
  49. package/public/assets/eventmodeling-45OFAUF4-CA3qsYNk.js +1 -0
  50. package/public/assets/flowDiagram-UKHOOZJN-BmCE-89P.js +1 -0
  51. package/public/assets/{ganttDiagram-PKOTCBZU-C01y-MjT.js → ganttDiagram-PKOTCBZU-YM9FA8H8.js} +1 -1
  52. package/public/assets/{gitGraph-TEB2WS4Q-BW4zQmf5.js → gitGraph-TEB2WS4Q-ws9TESN2.js} +1 -1
  53. package/public/assets/{gitGraphDiagram-DS77QQ5N-CnlU6XMJ.js → gitGraphDiagram-DS77QQ5N-CZu8bXTF.js} +1 -1
  54. package/public/assets/{index-DrqxoBmk.js → index-C84drWhX.js} +12 -12
  55. package/public/assets/{info-DKCQHKI2--A5cJC9V.js → info-DKCQHKI2-RSQw6txJ.js} +1 -1
  56. package/public/assets/{infoDiagram-6WML65LV-Cz2UOAMW.js → infoDiagram-6WML65LV-Bxmxzous.js} +1 -1
  57. package/public/assets/{ishikawaDiagram-WSZJBQD7-B4Iqm47_.js → ishikawaDiagram-WSZJBQD7-C80fxFdg.js} +1 -1
  58. package/public/assets/{journeyDiagram-NVQOT4AX-BU0js6EE.js → journeyDiagram-NVQOT4AX-DQTG0OdM.js} +1 -1
  59. package/public/assets/{kanban-definition-27J2QSJJ-DmeEPzz0.js → kanban-definition-27J2QSJJ-B5WxFLvF.js} +1 -1
  60. package/public/assets/{line-Dxi6NubD.js → line-D-jeoEtL.js} +1 -1
  61. package/public/assets/{linear-tama2Fqh.js → linear-7oKWtFDc.js} +1 -1
  62. package/public/assets/{mermaid-parser.core-bGhrBY9T.js → mermaid-parser.core-DG-hDTtF.js} +3 -3
  63. package/public/assets/{mermaid.core-_OoTZaAK.js → mermaid.core-CxtgV-Z8.js} +4 -4
  64. package/public/assets/{mindmap-definition-FAOFIHXS-BRJt4F9_.js → mindmap-definition-FAOFIHXS-DyT9O-XZ.js} +1 -1
  65. package/public/assets/{packet-7NZHBO7P-DAZsca60.js → packet-7NZHBO7P-fYfZRQ_r.js} +1 -1
  66. package/public/assets/{pegDiagram-VL7TDLO6-2k0rcz9N.js → pegDiagram-VL7TDLO6-BIKwmoZk.js} +1 -1
  67. package/public/assets/{pie-RZYD4A2V-ojW1YX_G.js → pie-RZYD4A2V-hHye9M3B.js} +1 -1
  68. package/public/assets/{pieDiagram-7S7Q4E2Y-CrrciDrS.js → pieDiagram-7S7Q4E2Y-B9IF9fZk.js} +1 -1
  69. package/public/assets/{quadrantDiagram-CIZ2JOQS-Dj5lMd95.js → quadrantDiagram-CIZ2JOQS-CriT2z0D.js} +1 -1
  70. package/public/assets/{radar-I7S5WNFK-flsKqrK_.js → radar-I7S5WNFK-UHbLAuY0.js} +1 -1
  71. package/public/assets/{railroad-3IZDKUUU-BX9H_OcC.js → railroad-3IZDKUUU-3enNyn3l.js} +1 -1
  72. package/public/assets/railroad-abnf-AHOZXSZD-CxQOPSx1.js +1 -0
  73. package/public/assets/railroad-ebnf-EBAXGLYW-CQ4P6ont.js +1 -0
  74. package/public/assets/railroad-peg-LSFZ7HO6-sSkmKuAv.js +1 -0
  75. package/public/assets/{railroadDiagram-AXF67PYL-BRNOv6Sh.js → railroadDiagram-AXF67PYL-Bkr9TUm0.js} +1 -1
  76. package/public/assets/{requirementDiagram-LRYGKXZP-BqJbwH3b.js → requirementDiagram-LRYGKXZP-CEp-ppYZ.js} +1 -1
  77. package/public/assets/{sankeyDiagram-W5VNT64P-BE7fRCAN.js → sankeyDiagram-W5VNT64P-CU-4IBMj.js} +1 -1
  78. package/public/assets/{sequenceDiagram-SI44F4Z6-Cwt-L1ko.js → sequenceDiagram-SI44F4Z6-Asi631mZ.js} +1 -1
  79. package/public/assets/{src-Bc2f8joo.js → src-CJc45UuW.js} +1 -1
  80. package/public/assets/{stateDiagram-OKZ733FA-etLHG0qi.js → stateDiagram-OKZ733FA-DkDphzLX.js} +1 -1
  81. package/public/assets/stateDiagram-v2-UEYNNEHI-DUHdaHkv.js +1 -0
  82. package/public/assets/{swimlanes-SLNWSIFB-B06NxeYI.js → swimlanes-SLNWSIFB-BVnRBmZ_.js} +1 -1
  83. package/public/assets/swimlanesDiagram-ULZ7WXOC-RkUs-XHd.js +8 -0
  84. package/public/assets/{timeline-definition-Z64GVDOM-BihNR1uq.js → timeline-definition-Z64GVDOM-B3jYbkKD.js} +1 -1
  85. package/public/assets/{treeView-QDETBFTQ-CImTfOYf.js → treeView-QDETBFTQ-FZndVHM1.js} +1 -1
  86. package/public/assets/{treemap-6X3UGDF4-DBRjbghn.js → treemap-6X3UGDF4-CKoOe0fa.js} +1 -1
  87. package/public/assets/{vennDiagram-T6HMQDX7-HcbmXJRd.js → vennDiagram-T6HMQDX7-B7qlSH-y.js} +1 -1
  88. package/public/assets/{wardley-OPB4EBWU-xY5d-6YU.js → wardley-OPB4EBWU-DlWXVdBE.js} +1 -1
  89. package/public/assets/{wardleyDiagram-T6FBY63Y-B6KN3TYi.js → wardleyDiagram-T6FBY63Y-Dk0uBYWd.js} +1 -1
  90. package/public/assets/{xychartDiagram-ELKLHX3M-AAW8Pwrl.js → xychartDiagram-ELKLHX3M-BDrlWQ3J.js} +1 -1
  91. package/public/index.html +1 -1
  92. package/public/assets/architecture-TIHT7OUA-MgcjoiPc.js +0 -1
  93. package/public/assets/channel-BAs2g7Wn.js +0 -1
  94. package/public/assets/classDiagram-JCYQIIEL-D8vKmZUF.js +0 -1
  95. package/public/assets/classDiagram-v2-OCEON4UE-D8vKmZUF.js +0 -1
  96. package/public/assets/eventmodeling-45OFAUF4-GcvHanur.js +0 -1
  97. package/public/assets/flowDiagram-UKHOOZJN-DdgmWP6g.js +0 -1
  98. package/public/assets/railroad-abnf-AHOZXSZD-DD8mz__c.js +0 -1
  99. package/public/assets/railroad-ebnf-EBAXGLYW-CWer5P7V.js +0 -1
  100. package/public/assets/railroad-peg-LSFZ7HO6-DDrRJYYw.js +0 -1
  101. package/public/assets/stateDiagram-v2-UEYNNEHI-Crgtg4pC.js +0 -1
  102. package/public/assets/swimlanesDiagram-ULZ7WXOC-C8S-XAyy.js +0 -8
package/README.md CHANGED
@@ -12,7 +12,7 @@ npx @davidbalzan/groundwork-console # or: npm
12
12
  First run creates `~/.groundwork-console/config.json` (add projects from Home → **Add project**) and `~/.groundwork-console/token`, prints the URL and a sign-in link, and opens the browser already signed in. Everything runs on one port (`:8790`).
13
13
 
14
14
  ```
15
- groundwork-console [start] [--port 8790] [--host 127.0.0.1] [--no-open] [--no-auth] [--config <file>]
15
+ groundwork-console [start] [--port 8790] [--host 127.0.0.1 | --tailscale] [--no-open] [--no-auth] [--config <file>]
16
16
  groundwork-console doctor # kit requires + which bins/paths were resolved
17
17
  groundwork-console token # print the token
18
18
  groundwork-console config # print the config path
@@ -20,6 +20,10 @@ groundwork-console config # print the config path
20
20
 
21
21
  Requires on the host: Node ≥ 22, tmux (pane monitor / spawn), and either the three kit bins above on PATH or a kit checkout at `kitPath` in the config. `doctor` tells you which it found. Console requires: seam `^0.1.0` · agent-coord-mcp `^0.23.0` · groundwork `^0.3.0` · agent-coordination `^0.5.0`.
22
22
 
23
+ ## From another machine
24
+
25
+ `groundwork-console --tailscale` binds this machine's Tailscale IP (found via `tailscale ip -4`) instead of loopback and prints a sign-in link you can open from any device on your tailnet; the token cookie does the rest (plain http — keep it on the tailnet, or put TLS in front). Without Tailscale: `ssh -N -L 8790:127.0.0.1:8790 <host>` and open `http://localhost:8790`.
26
+
23
27
  ## What it does
24
28
 
25
29
  - **Home** — what needs you (DAVID_DECISION packets with age), fleet presence per project, roadmap strips.
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  /* groundwork-console — one command: starts the API + serves the built UI on one port, prints the sign-in link, opens the browser.
3
- Flags: --port 8790 · --host 127.0.0.1 · --no-open · --no-auth · --config <file>
3
+ Flags: --port 8790 · --host 127.0.0.1 · --tailscale (bind the tailnet IP, print the remote sign-in link) · --no-open · --no-auth · --config <file>
4
4
  Subcommands: token (print the token) · doctor (kit requires + paths) · config (print config path) */
5
5
  import { readFileSync, existsSync } from "node:fs";
6
+ import { execFileSync } from "node:child_process";
7
+ import os from "node:os";
6
8
  import { homedir } from "node:os";
7
9
  import path from "node:path";
8
10
  import { fileURLToPath } from "node:url";
@@ -15,7 +17,14 @@ const has = (k) => args.includes(k);
15
17
  const sub = args.find((a) => !a.startsWith("--") && !["true", "false"].includes(a) && !/^\d+$/.test(a) && !args[args.indexOf(a) - 1]?.startsWith("--"));
16
18
 
17
19
  const port = String(flag("--port", process.env.PORT ?? "8790"));
18
- const host = String(flag("--host", process.env.GROUNDWORK_CONSOLE_HOST ?? "127.0.0.1"));
20
+ /** Tailnet IPv4 of this machine: `tailscale ip -4` (CLI or the macOS app), else the first 100.64/10 interface address. */
21
+ function tailscaleIp() {
22
+ for (const bin of ["tailscale", "/Applications/Tailscale.app/Contents/MacOS/Tailscale"]) { try { const ip = execFileSync(bin, ["ip", "-4"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim().split("\n")[0]; if (/^100\./.test(ip)) return ip; } catch { /* next */ } }
23
+ for (const addrs of Object.values(os.networkInterfaces())) for (const a of addrs ?? []) if (a.family === "IPv4" && /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(a.address)) return a.address;
24
+ return null;
25
+ }
26
+ let host = String(flag("--host", process.env.GROUNDWORK_CONSOLE_HOST ?? "127.0.0.1"));
27
+ if (has("--tailscale")) { const ip = tailscaleIp(); if (!ip) { console.error("--tailscale: no tailnet address found (is Tailscale running? try `tailscale ip -4`)"); process.exit(1); } host = ip; if (!process.env.GROUNDWORK_CONSOLE_PUBLIC_API) process.env.GROUNDWORK_CONSOLE_PUBLIC_API = `http://${ip}:${port}/api`; }
19
28
  if (flag("--config")) process.env.GROUNDWORK_CONSOLE_CONFIG = String(flag("--config"));
20
29
  process.env.PORT = port; process.env.GROUNDWORK_CONSOLE_HOST = host;
21
30
  if (has("--no-auth")) process.env.GROUNDWORK_CONSOLE_AUTH = "off";
@@ -43,5 +52,5 @@ const { CONFIG_FILE } = await import("../dist/config.js");
43
52
  const base = `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`;
44
53
  const token = authEnabled ? loadToken() : null;
45
54
  const url = token ? `${base}/api/auth/login?token=${encodeURIComponent(token)}&next=/` : `${base}/`;
46
- console.log(`\n groundwork-console ${pkg.version}\n UI ${base}/\n config ${CONFIG_FILE}\n token ${authEnabled ? tokenFile : "auth off"}\n sign in ${url}\n`);
55
+ console.log(`\n groundwork-console ${pkg.version}\n UI ${base}/\n config ${CONFIG_FILE}\n token ${authEnabled ? tokenFile : "auth off"}\n sign in ${url}${has("--tailscale") ? `\n remote open the sign-in link above from any device on your tailnet (token cookie, 30 d). Plain http — keep it on the tailnet.` : ""}\n`);
47
56
  if (!has("--no-open") && host !== "0.0.0.0") { try { const { default: open } = await import("open"); await open(url); } catch { /* headless: link is printed */ } }
package/dist/answered.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import path from "node:path";
4
4
  /* Console-owned "answered" set for DAVID_DECISION packets (room posts + DMs), keyed by message id.
@@ -31,4 +31,37 @@ export function markAnswered(ids, meta) {
31
31
  }
32
32
  return n;
33
33
  }
34
- export const isAnswered = (id) => typeof id === "string" && !!answered()[id];
34
+ /* Bus-native answered set (kit ≥ 18ffbf4): any message FROM david carrying inReplyTo:<id> answers <id>. Scanned from ~/agent-coord inbox/*.jsonl + rooms/*.jsonl, cached 10 s. Unioned with the console-owned set above. */
35
+ let busCache = null;
36
+ let coordRoot = null;
37
+ export function setCoordDir(dir) { coordRoot = dir; }
38
+ function busAnswered() {
39
+ if (!coordRoot)
40
+ return new Set();
41
+ if (busCache && Date.now() - busCache.at < 10_000)
42
+ return busCache.ids;
43
+ const ids = new Set();
44
+ for (const sub of ["inbox", "rooms"]) {
45
+ const dir = path.join(coordRoot, sub);
46
+ if (!existsSync(dir))
47
+ continue;
48
+ for (const f of readdirSync(dir).filter((n) => n.endsWith(".jsonl"))) {
49
+ try {
50
+ for (const l of readFileSync(path.join(dir, f), "utf8").split("\n")) {
51
+ if (!l.includes("inReplyTo"))
52
+ continue;
53
+ try {
54
+ const m = JSON.parse(l);
55
+ if (m.from === "david" && typeof m.inReplyTo === "string")
56
+ ids.add(m.inReplyTo);
57
+ }
58
+ catch { /* skip */ }
59
+ }
60
+ }
61
+ catch { /* skip */ }
62
+ }
63
+ }
64
+ busCache = { at: Date.now(), ids };
65
+ return ids;
66
+ }
67
+ export const isAnswered = (id) => typeof id === "string" && (!!answered()[id] || busAnswered().has(id));
@@ -1,6 +1,5 @@
1
1
  /* Groundwork + playbook CLIs, JSON mode (shipped by grok 2026-08-16). Cached briefly; both are read-only commands. */
2
2
  import { execFile } from "node:child_process";
3
- import path from "node:path";
4
3
  import { promisify } from "node:util";
5
4
  const run = promisify(execFile);
6
5
  const cache = new Map();
@@ -9,6 +8,11 @@ async function json(key, ttl, argv, fallback) {
9
8
  if (hit && Date.now() - hit.at < ttl)
10
9
  return hit.value;
11
10
  let value;
11
+ if (argv[0] === MISSING) {
12
+ value = { ...fallback, ranAt: Date.now(), error: `${key.startsWith("pb") ? "agent-coordination" : "groundwork"} CLI not found — npm i -g (or set kitPath to a kit checkout)` };
13
+ cache.set(key, { at: Date.now(), value });
14
+ return value;
15
+ }
12
16
  try {
13
17
  const r = await run("node", argv, { timeout: 20_000, maxBuffer: 4 << 20 });
14
18
  value = { ...JSON.parse(r.stdout), ranAt: Date.now() };
@@ -26,8 +30,9 @@ async function json(key, ttl, argv, fallback) {
26
30
  return value;
27
31
  }
28
32
  import { groundworkCli, playbookCli } from "./kitresolve.js";
29
- const gw = (kit) => groundworkCli(kit) ?? path.join(kit, "packages/groundwork/src/cli.mjs");
30
- const pb = (kit) => playbookCli(kit) ?? path.join(kit, "packages/coord-playbook/bin/install.mjs");
33
+ const MISSING = "/dev/null/missing-cli"; // resolver found nothing → the run fails fast with a clear message below instead of a fake kit path
34
+ const gw = (kit) => groundworkCli(kit) ?? MISSING;
35
+ const pb = (kit) => playbookCli(kit) ?? MISSING;
31
36
  export const doctor = (kit, proj) => json(`doctor:${proj}`, 30_000, [gw(kit), "doctor", proj, "--json"], { ok: false, exit: 1, checks: [], ranAt: 0 });
32
37
  export const status = (kit, proj) => json(`status:${proj}`, 15_000, [gw(kit), "status", proj, "--json"], { streams: [], next: null, queueFound: false, phases: [], ranAt: 0 });
33
38
  export const list = (kit, proj) => json(`list:${proj}`, 60_000, [gw(kit), "list", proj, "--json"], { version: "", project: proj, skills: [], ranAt: 0 });
@@ -77,3 +77,20 @@ export function requiresReport(kitPath) {
77
77
  const r = { seam: check("seam"), mcp: check("mcp"), groundwork: check("groundwork"), playbook: check("playbook") };
78
78
  return { ...r, paths: inst.paths, ok: Object.values(r).every((x) => x.ok !== false) };
79
79
  }
80
+ /** Package root dir for a kit package: the checkout's packages/<dir> if present, else the installed npm package (found from its bin). Null when neither exists. */
81
+ export function packageRoot(kitPath, which) {
82
+ const dir = { groundwork: "packages/groundwork", playbook: "packages/coord-playbook", mcp: "packages/coord-mcp" }[which];
83
+ const local = path.join(kitPath, dir);
84
+ if (existsSync(path.join(local, "package.json")))
85
+ return local;
86
+ const bin = which === "groundwork" ? groundworkCli(kitPath) : which === "playbook" ? playbookCli(kitPath) : mcpServer(kitPath);
87
+ if (!bin)
88
+ return null;
89
+ let d = path.dirname(bin);
90
+ for (let i = 0; i < 6; i++) {
91
+ if (existsSync(path.join(d, "package.json")))
92
+ return d;
93
+ d = path.dirname(d);
94
+ }
95
+ return null;
96
+ }
package/dist/mcp.js CHANGED
@@ -54,7 +54,8 @@ export class BusClient {
54
54
  this.identity = identity;
55
55
  this.mcpPath = mcpPath;
56
56
  }
57
- get serverPath() { return this.mcpPath ?? mcpServer(this.kitPath) ?? path.join(this.kitPath, "packages/coord-mcp/dist/server.js"); }
57
+ get serverPath() { const p = this.mcpPath ?? mcpServer(this.kitPath); if (!p)
58
+ throw new Error("agent-coord-mcp not found — npm i -g agent-coord-mcp (or set kitPath to a kit checkout)"); return p; }
58
59
  get agentId() { return IDENTITIES[this.identity].agentId; }
59
60
  async connect(log) {
60
61
  if (this.client)
@@ -72,6 +73,13 @@ export class BusClient {
72
73
  await client.connect(transport);
73
74
  const reg = await client.callTool({ name: "register", arguments: { agentId: id.agentId, project: id.project, role: id.role, force: true } });
74
75
  log?.(`register → ${JSON.stringify(reg.structuredContent ?? textOf(reg)).slice(0, 160)}`);
76
+ // Feature detection: which args each tool accepts (e.g. send_message.inReplyTo landed in kit 18ffbf4; the live bin may predate it).
77
+ try {
78
+ const tools = await client.listTools();
79
+ for (const t of tools.tools)
80
+ this.toolArgs.set(t.name, new Set(Object.keys(t.inputSchema?.properties ?? {})));
81
+ }
82
+ catch { /* older server: no detection, pass nothing extra */ }
75
83
  this.client = client;
76
84
  return client;
77
85
  })();
@@ -82,6 +90,9 @@ export class BusClient {
82
90
  this.connecting = null;
83
91
  }
84
92
  }
93
+ toolArgs = new Map();
94
+ /** True when the connected server's tool schema lists this argument (false when unknown or not connected yet). */
95
+ supports(tool, arg) { return this.toolArgs.get(tool)?.has(arg) ?? false; }
85
96
  async call(name, args, log) {
86
97
  const isWrite = WRITE_TOOLS_GATED.has(name);
87
98
  if (!READ_ONLY_TOOLS.has(name) && !isWrite)
package/dist/provision.js CHANGED
@@ -25,7 +25,7 @@ async function fleetWindow(project, startDir, log) {
25
25
  log(`created window ${win} in session ${session} (first pane ${pane} reused for the first agent — no spare shell)`);
26
26
  return { target: `${session}:${win}`, freshPane: pane };
27
27
  }
28
- export async function spawnAgent(req, isRegistered, log, isAttached = () => true) {
28
+ export async function spawnAgent(req, isRegistered, log, isAttached = () => true, roleOf = () => null) {
29
29
  if (!/^[a-z0-9][a-z0-9-_]*$/i.test(req.agentId))
30
30
  throw new Error("agentId: letters, digits, - _");
31
31
  if (isRegistered(req.agentId))
@@ -91,7 +91,28 @@ export async function spawnAgent(req, isRegistered, log, isAttached = () => true
91
91
  }
92
92
  log(attached ? `✓ ${req.agentId} attached (tmux-push) — DMs will land in ${paneId}` : `✗ ${req.agentId} still not attached — DMs will sit in its inbox until it attaches (Fleets shows "not attached")`);
93
93
  }
94
- return { agentId: req.agentId, pane: paneId, window: target, registered, attached };
94
+ // Role: every spawned agent must carry its role type on the bus (presence bar, fleet graph, Fix→role routing all key on it). If it registered without one, tell it — into its own pane — and re-check.
95
+ const ROLE_RE = { coordinator: /coordinator/i, worker: /worker|repo-owner|consumer-owner|reference|infra|ops|lane/i, qa: /\bqa\b|quality/i, ci: /\bci\b/i, aide: /aide|liaison|curator/i, audit: /audit/i };
96
+ let roleOk = false;
97
+ if (registered) {
98
+ roleOk = ROLE_RE[req.role].test(roleOf(req.agentId) ?? "");
99
+ if (!roleOk) {
100
+ log(`⚠ registered without a ${req.role} role (role="${roleOf(req.agentId) ?? ""}") — asking it to declare one`);
101
+ await inject(`Declare your role on the coord bus now: call register (or join) again with agentId ${req.agentId} and role: { roleId: "coord-${req.role === "coordinator" ? "coordinator" : req.role}", displayName: "${req.role}" } — the console, fleet graph and Fix→role routing rely on it.`);
102
+ const d4 = Date.now() + 45_000;
103
+ while (Date.now() < d4) {
104
+ if (ROLE_RE[req.role].test(roleOf(req.agentId) ?? "")) {
105
+ roleOk = true;
106
+ break;
107
+ }
108
+ await sleep(2_000);
109
+ }
110
+ log(roleOk ? `✓ role declared: ${roleOf(req.agentId)}` : `✗ still no ${req.role} role — Fleets will show it as "—"; DM it to fix`);
111
+ }
112
+ else
113
+ log(`✓ role: ${roleOf(req.agentId)}`);
114
+ }
115
+ return { agentId: req.agentId, pane: paneId, window: target, registered, attached, roleOk };
95
116
  }
96
117
  export function suggestId(project, role, existing) {
97
118
  const base = `${project}-${role}`;
package/dist/seam.js CHANGED
@@ -18,8 +18,12 @@ export function boardOf(projectPath) {
18
18
  const dDoc = d ? parseWorkDoc(d) : null;
19
19
  const facts = f ? parseFactsDoc(f) : null;
20
20
  const now = Date.now();
21
+ // Onboarding state for the Roadmap empty state: is there a PRD, and what does its Status line say?
22
+ const prdSrc = read(path.join(docs, "PRD.md"));
23
+ const prd = prdSrc === null ? null : { status: prdSrc.match(/^\*\*Status\*\*:\s*([^\n]+)/m)?.[1]?.trim() ?? null, roadmap: existsSync(path.join(docs, "PRODUCTION_ROADMAP.md")) };
21
24
  return {
22
25
  files: { workstreams: !!ws, queue: !!q, done: !!d, facts: !!f },
26
+ prd,
23
27
  workstreams: { schema: schema ?? (ws ? "unknown" : null), rows: board, issues: wsDoc ? workDocIssues(wsDoc) : [] },
24
28
  queue: qDoc ? queueItemsOf(qDoc) : [],
25
29
  done: dDoc ? doneEntriesOf(dDoc) : [],
package/dist/server.js CHANGED
@@ -8,6 +8,7 @@ import { serve } from "@hono/node-server";
8
8
  import { serveStatic } from "@hono/node-server/serve-static";
9
9
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
10
10
  import path from "node:path";
11
+ import { hostname as osHostname } from "node:os";
11
12
  import { loadConfig, saveProjects, validateProjectPath, CONFIG_FILE } from "./config.js";
12
13
  import { boardOf, docOf, docsIndex, phaseTasksOf } from "./seam.js";
13
14
  import { DocWriteError, sha256, writeDoc } from "./docwrite.js";
@@ -26,16 +27,17 @@ import { BusClient, AGENT_ID, busWrites, seamWrites, saveSwitches, SWITCHES_FILE
26
27
  import { addQueueItem, reverifyFact, scopeAllows } from "./seamwrite.js";
27
28
  import { fileURLToPath } from "node:url";
28
29
  import { saveUpload, uploadPath, UPLOAD_DIR } from "./uploads.js";
29
- import { answered, markAnswered, isAnswered } from "./answered.js";
30
+ import { answered, markAnswered, isAnswered, setCoordDir } from "./answered.js";
30
31
  import { requireAuth, login, logout, isAuthed, authEnabled, tokenFile } from "./auth.js";
31
32
  import { knowledgeIndex, knowledgeDoc, knowledgePath } from "./knowledge.js";
32
- import { requiresReport, groundworkCli, REQUIRES } from "./kitresolve.js";
33
+ import { requiresReport, groundworkCli, playbookCli, packageRoot, REQUIRES } from "./kitresolve.js";
33
34
  let cfg = loadConfig();
34
35
  const port = Number(process.env.PORT ?? 8790);
35
36
  const HERE = path.dirname(fileURLToPath(import.meta.url));
36
37
  const RULES = process.env.GROUNDWORK_CONSOLE_RULES ?? [path.join(process.env.HOME ?? "", ".groundwork-console", "rules.json"), path.resolve(HERE, "../groundwork-console.rules.json"), path.resolve(HERE, "../../../groundwork-console.rules.json")].find((f) => existsSync(f)) ?? path.resolve(HERE, "../../../groundwork-console.rules.json");
37
38
  const PKG = JSON.parse(readFileSync(path.resolve(HERE, "../package.json"), "utf8"));
38
39
  const runs = new Runs();
40
+ setCoordDir(cfg.coordDir);
39
41
  const bus = new BusClient(cfg.kitPath, cfg.coordDir, "groundwork-console", process.env.AGENT_COORD_MCP);
40
42
  const events = new EventEngine(cfg, RULES, runs, (args, log) => bus.call("send_message", args, log));
41
43
  const notifier = new Notifier(cfg, runs, events);
@@ -46,6 +48,7 @@ const root = new Hono();
46
48
  const app = new Hono().basePath("/api");
47
49
  app.use("*", cors({ origin: (o) => o ?? "*", credentials: true }));
48
50
  app.use("*", requireAuth());
51
+ app.onError((err, c) => { console.error(`[api] ${c.req.method} ${c.req.path}: ${err.message}`); return c.json({ error: err.message }, 500); });
49
52
  app.get("/auth/status", (c) => c.json({ enabled: authEnabled, authed: isAuthed(c) }));
50
53
  app.post("/auth/login", async (c) => { const b = (await c.req.json().catch(() => ({}))); if (!b.token || !login(c, b.token))
51
54
  return c.json({ error: "wrong token — it's in ~/.groundwork-console/token (or GROUNDWORK_CONSOLE_TOKEN)" }, 401); return c.json({ ok: true }); });
@@ -54,7 +57,7 @@ app.post("/auth/logout", (c) => { logout(c); return c.json({ ok: true }); });
54
57
  app.get("/auth/login", (c) => { const t = c.req.query("token") ?? ""; if (!login(c, t))
55
58
  return c.json({ error: "wrong token" }, 401); return c.redirect(c.req.query("next") ?? "/"); });
56
59
  const project = (name) => cfg.projects.find((p) => p.name === name);
57
- app.get("/health", (c) => c.json({ ok: true, readOnly: !busWrites.enabled, busWrites: busWrites.enabled, version: PKG.version, requires: requiresReport(cfg.kitPath), config: CONFIG_FILE }));
60
+ app.get("/health", (c) => c.json({ ok: true, readOnly: !busWrites.enabled, busWrites: busWrites.enabled, version: PKG.version, requires: requiresReport(cfg.kitPath), config: CONFIG_FILE, hostname: osHostname().replace(/\.local$/, "") }));
58
61
  app.get("/admin/bus-writes", (c) => c.json(busWrites));
59
62
  app.get("/admin/seam-writes", (c) => c.json(seamWrites));
60
63
  app.post("/admin/seam-writes", async (c) => {
@@ -111,7 +114,13 @@ app.post("/actions/send", async (c) => {
111
114
  return c.json({ error: "room posts need a prefix (FYI / AGENT_ACTION / DAVID_DECISION / BLOCKER / RISK / DONE / GO / SCOPE CHANGE / VERDICT)" }, 400);
112
115
  const target = b.to ? `→ ${b.to}` : `#${b.room.replace(/^#/, "")}`;
113
116
  const r = await runs.exec({ kind: "bus", label: `send as ${who} ${target}`, sideEffects: true, args: { as: who, room: b.room, to: b.to, kind: b.kind, text: b.text.slice(0, 200) } }, async (log) => {
114
- const out = await clientFor(who).call("send_message", { ...(b.to ? { to: b.to } : { room: b.room.replace(/^#/, "") }), text: b.text, ...(b.kind ? { kind: b.kind } : {}) }, log);
117
+ const cl = clientFor(who);
118
+ await cl.connect(log);
119
+ const parent = b.inReplyTo ?? b.answers?.[0];
120
+ const withReply = parent && cl.supports("send_message", "inReplyTo") ? { inReplyTo: parent } : {};
121
+ if (parent && !("inReplyTo" in withReply))
122
+ log("bus predates inReplyTo — using the console-side answered set only");
123
+ const out = await cl.call("send_message", { ...(b.to ? { to: b.to } : { room: b.room.replace(/^#/, "") }), text: b.text, ...(b.kind ? { kind: b.kind } : {}), ...withReply }, log);
115
124
  // Answered bookkeeping (console-owned, until the bus has inReplyTo): explicit ids from the UI, plus — for a DM as David — every open DAVID_DECISION DM from that agent.
116
125
  if (who === "david") {
117
126
  const ids = [...(b.answers ?? [])];
@@ -182,7 +191,8 @@ app.get("/kit", (c) => {
182
191
  app.get("/projects", (c) => {
183
192
  const sc = scopes(cfg.coordDir);
184
193
  const kitGw = (() => { try {
185
- return JSON.parse(readFileSync(path.join(cfg.kitPath, "packages/groundwork/package.json"), "utf8")).version;
194
+ const r = packageRoot(cfg.kitPath, "groundwork");
195
+ return r ? JSON.parse(readFileSync(path.join(r, "package.json"), "utf8")).version : null;
186
196
  }
187
197
  catch {
188
198
  return null;
@@ -505,6 +515,32 @@ app.get("/bus/unread", async (c) => {
505
515
  }
506
516
  });
507
517
  /* ---- CLI updates (repo / global writes) as shell Runs; playbook has native --dry-run; groundwork update reports git status after ---- */
518
+ /* groundwork init — only for repos that are NOT inited (no docs/.groundwork/VERSION); the console never re-inits. Shell Run, typed confirm. */
519
+ app.post("/actions/groundwork-init/:name", async (c) => {
520
+ const p = project(c.req.param("name"));
521
+ if (!p)
522
+ return c.json({ error: "unknown project" }, 404);
523
+ const b = (await c.req.json().catch(() => ({})));
524
+ if (b.confirm !== p.name)
525
+ return c.json({ error: "confirm must equal the project name" }, 400);
526
+ if (existsSync(path.join(p.path, "docs/.groundwork/VERSION")))
527
+ return c.json({ error: "already inited (docs/.groundwork/VERSION exists) — use update" }, 409);
528
+ const cli = groundworkCli(cfg.kitPath);
529
+ if (!cli)
530
+ return c.json({ error: "groundwork CLI not found — npm i -g @davidbalzan/groundwork" }, 500);
531
+ const argv = [cli, "init", p.path, ...(b.minimal ? ["--minimal"] : [])];
532
+ const r = await runs.exec({ kind: "shell", label: `groundwork init · ${p.name}${b.minimal ? " --minimal" : ""}`, project: p.name, sideEffects: true, args: { argv: argv.slice(1) } }, async (log) => {
533
+ log(`$ node ${argv.map((a) => a.replace(cfg.kitPath, "<kit>").replace(p.path, "<repo>")).join(" ")}`);
534
+ const out = await execP("node", argv, { timeout: 120_000, maxBuffer: 8 << 20 }).catch((e) => { log(`exit: ${e.message}`); return { stdout: e.stdout ?? "", stderr: e.stderr ?? "" }; });
535
+ for (const l of (out.stdout + out.stderr).replace(/\x1b\[[0-9;]*m/g, "").split("\n").filter(Boolean).slice(0, 120))
536
+ log(l);
537
+ const st = await execP("git", ["status", "--porcelain"], { cwd: p.path }).then((x) => x.stdout).catch(() => "");
538
+ const changed = st.split("\n").filter(Boolean);
539
+ log(`git status: ${changed.length} path(s) changed — review, then commit (the console does not commit)`);
540
+ return { changed, cli: "groundwork init", inited: existsSync(path.join(p.path, "docs/.groundwork/VERSION")) };
541
+ });
542
+ return c.json(r, r.status === "ok" ? 200 : 500);
543
+ });
508
544
  app.post("/actions/groundwork-update/:name", async (c) => {
509
545
  const p = project(c.req.param("name"));
510
546
  if (!p)
@@ -512,7 +548,9 @@ app.post("/actions/groundwork-update/:name", async (c) => {
512
548
  const b = (await c.req.json().catch(() => ({})));
513
549
  if (b.confirm !== p.name)
514
550
  return c.json({ error: "confirm must equal the project name" }, 400);
515
- const cli = groundworkCli(cfg.kitPath) ?? path.join(cfg.kitPath, "packages/groundwork/src/cli.mjs");
551
+ const cli = groundworkCli(cfg.kitPath);
552
+ if (!cli)
553
+ return c.json({ error: "groundwork CLI not found — npm i -g @davidbalzan/groundwork (or set kitPath to a kit checkout)" }, 500);
516
554
  const argv = [cli, "update", p.path, ...(b.all ? ["--all"] : []), ...(b.docs ? ["--docs"] : [])];
517
555
  const r = await runs.exec({ kind: "shell", label: `groundwork update · ${p.name}${b.all ? " --all" : ""}${b.docs ? " --docs" : ""}`, project: p.name, sideEffects: true, args: { argv: argv.slice(1) } }, async (log) => {
518
556
  log(`$ node ${argv.map((a) => a.replace(cfg.kitPath, "<kit>").replace(p.path, "<repo>")).join(" ")}`);
@@ -531,7 +569,9 @@ app.post("/actions/playbook-update", async (c) => {
531
569
  const dry = b.dryRun !== false;
532
570
  if (!dry && b.confirm !== "UPDATE")
533
571
  return c.json({ error: "confirm must be UPDATE to apply" }, 400);
534
- const cli = path.join(cfg.kitPath, "packages/coord-playbook/bin/install.mjs");
572
+ const cli = playbookCli(cfg.kitPath);
573
+ if (!cli)
574
+ return c.json({ error: "agent-coordination CLI not found — npm i -g agent-coordination (or set kitPath to a kit checkout)" }, 500);
535
575
  const argv = [cli, "update", ...(dry ? ["--dry-run"] : [])];
536
576
  const r = await runs.exec({ kind: "shell", label: `agent-coordination update${dry ? " (dry-run)" : ""}`, sideEffects: !dry, args: { argv: argv.slice(1) } }, async (log) => {
537
577
  log(`$ node ${argv.map((a) => a.replace(cfg.kitPath, "<kit>")).join(" ")}`);
@@ -551,6 +591,7 @@ app.get("/projects/:name/fleet", async (c) => {
551
591
  const live = (await agentsAsync(cfg.coordDir)).filter((a) => a.liveness !== "dead" && (a.project === p.room || a.project === p.name));
552
592
  return c.json({ ...fleetStatus(live), templates: FLEET_TEMPLATES, window: `fleet-${p.name}` });
553
593
  });
594
+ const roleOfAgent = (id) => agents(cfg.coordDir).find((a) => a.agentId === id)?.role ?? null;
554
595
  app.post("/actions/spawn-fleet", async (c) => {
555
596
  if (!busWrites.enabled)
556
597
  return c.json({ error: "bus writes are off (header toggle)" }, 403);
@@ -572,7 +613,7 @@ app.post("/actions/spawn-fleet", async (c) => {
572
613
  for (const step of plan) {
573
614
  const agentId = suggestId(p.name, step.role, [...ids(), ...out.map((o) => o.agentId)]);
574
615
  log(`── ${step.role} → ${agentId} (${step.harness})`);
575
- const res = await spawnAgent({ project: p.name, repoPath: p.path, room: p.room, role: step.role, harness: step.harness, agentId, lane: step.lane, waitSeconds: b.waitSeconds }, (id) => ids().includes(id), log, attached);
616
+ const res = await spawnAgent({ project: p.name, repoPath: p.path, room: p.room, role: step.role, harness: step.harness, agentId, lane: step.lane, waitSeconds: b.waitSeconds }, (id) => ids().includes(id), log, attached, roleOfAgent);
576
617
  out.push({ agentId: res.agentId, pane: res.pane, registered: res.registered, attached: res.attached });
577
618
  }
578
619
  log(`fleet ${t.id}: ${out.filter((o) => o.registered).length}/${out.length} registered · ${out.filter((o) => o.attached).length}/${out.length} attached`);
@@ -597,7 +638,7 @@ app.post("/actions/spawn", async (c) => {
597
638
  const ids = () => agents(cfg.coordDir).map((a) => a.agentId);
598
639
  const attached = (id) => existsSync(path.join(cfg.coordDir, "transports", `${id}.json`));
599
640
  const agentId = b.agentId?.trim() || suggestId(p.name, b.role, ids());
600
- const r = runs.execAsync({ kind: "shell", label: `spawn ${agentId} (${b.role} · ${b.harness}) → fleet-${p.name}`, project: p.name, sideEffects: true, args: { role: b.role, harness: b.harness, agentId, lane: b.lane, model: b.model } }, (log) => spawnAgent({ project: p.name, repoPath: p.path, room: p.room, role: b.role, harness: b.harness, agentId, lane: b.lane, model: b.model, note: b.note, waitSeconds: b.waitSeconds }, (id) => ids().includes(id), log, attached));
641
+ const r = runs.execAsync({ kind: "shell", label: `spawn ${agentId} (${b.role} · ${b.harness}) → fleet-${p.name}`, project: p.name, sideEffects: true, args: { role: b.role, harness: b.harness, agentId, lane: b.lane, model: b.model } }, (log) => spawnAgent({ project: p.name, repoPath: p.path, room: p.room, role: b.role, harness: b.harness, agentId, lane: b.lane, model: b.model, note: b.note, waitSeconds: b.waitSeconds }, (id) => ids().includes(id), log, attached, roleOfAgent));
601
642
  return c.json(r);
602
643
  });
603
644
  /* ---- Gated bus admin: prune stale, unregister one (writes; require switch + confirm) ---- */
@@ -662,6 +703,25 @@ app.post("/actions/phase/:name/:phase/:verb", async (c) => {
662
703
  const r = await runs.exec({ kind: "bus", label: `${verb} ${phase} → ${target}`, project: p.name, sideEffects: true, args: { to: target, text: text.slice(0, 200) } }, (log) => busDavid.call("send_message", { to: target, text }, log));
663
704
  return c.json(r, r.status === "ok" ? 200 : 403);
664
705
  });
706
+ /* ---- Project-level onboarding actions (as david → the aide): create-prd conversation starter, /kickstart ---- */
707
+ app.post("/actions/project/:name/:verb", async (c) => {
708
+ const p = project(c.req.param("name"));
709
+ if (!p)
710
+ return c.json({ error: "unknown project" }, 404);
711
+ const verb = c.req.param("verb");
712
+ const b = (await c.req.json().catch(() => ({})));
713
+ const target = roleAgent(p.name, p.room, /aide|liaison|curator/i);
714
+ if (!target)
715
+ return c.json({ error: `no live aide for ${p.name}` }, 409);
716
+ const text = verb === "kickstart"
717
+ ? `AGENT_ACTION: run /kickstart in ${p.name} (${p.path}) — PRD is in docs/PRD.md; produce the roadmap + docs scaffold (TECH_STACK, ARCHITECTURE_GUIDE, DECISIONS, PRODUCTION_ROADMAP, WORKSTREAMS.v1, QUEUE, DONE, FACTS) and report when the doctor is clean.${b.note ? " Note: " + b.note : ""}`
718
+ : verb === "create-prd" ? `AGENT_ACTION: let's write the PRD for ${p.name} (${p.path}) — run /create-prd with me; ask what you need to know, keep it to one pass, Status: Draft until I approve.${b.note ? " Note: " + b.note : ""}`
719
+ : null;
720
+ if (!text)
721
+ return c.json({ error: "verb must be kickstart | create-prd" }, 400);
722
+ const r = await runs.exec({ kind: "bus", label: `${verb} → ${target}`, project: p.name, sideEffects: true, args: { to: target, text: text.slice(0, 200) } }, (log) => busDavid.call("send_message", { to: target, text }, log));
723
+ return c.json(r, r.status === "ok" ? 200 : 403);
724
+ });
665
725
  /* ---- Doctor → Fix: route a doctor finding to the role that owns it (live agent required); sent as david ---- */
666
726
  const DOCTOR_OWNER = {
667
727
  "orphan-wikilinks": { role: "aide", re: /aide|liaison|curator/i, how: "fix the orphan wikilinks (create the target doc or correct the link); docs are your lane" },
package/dist/skills.js CHANGED
@@ -3,17 +3,22 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
3
  import { createHash } from "node:crypto";
4
4
  import { homedir } from "node:os";
5
5
  import path from "node:path";
6
+ import { packageRoot } from "./kitresolve.js";
6
7
  const sha = (p) => (existsSync(p) ? createHash("sha1").update(readFileSync(p)).digest("hex").slice(0, 10) : null);
7
8
  const dirs = (p) => (existsSync(p) ? readdirSync(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) : []);
8
9
  const front = (p) => { if (!existsSync(p))
9
10
  return {}; const m = readFileSync(p, "utf8").match(/^---\n([\s\S]*?)\n---/); const o = {}; m?.[1].split("\n").forEach((l) => { const i = l.indexOf(":"); if (i > 0)
10
11
  o[l.slice(0, i).trim()] = l.slice(i + 1).trim(); }); return o; };
11
12
  export function groundworkSkills(kitPath, projectPath) {
12
- const payload = path.join(kitPath, "packages/groundwork/payload/skills");
13
- const pathsMjs = path.join(kitPath, "packages/groundwork/src/lib/paths.mjs");
13
+ const root = packageRoot(kitPath, "groundwork");
14
+ const versionMarker0 = existsSync(path.join(projectPath, "docs/.groundwork/VERSION")) ? readFileSync(path.join(projectPath, "docs/.groundwork/VERSION"), "utf8").trim() : null;
15
+ if (!root)
16
+ return { versionMarker: versionMarker0, kitVersion: null, driftedVersion: false, skills: [], error: "groundwork not found — npm i -g @davidbalzan/groundwork (or set kitPath to a kit checkout)" };
17
+ const payload = path.join(root, "payload/skills");
18
+ const pathsMjs = path.join(root, "src/lib/paths.mjs");
14
19
  const minimal = existsSync(pathsMjs) ? [...(readFileSync(pathsMjs, "utf8").match(/MINIMAL_SKILLS = \[([\s\S]*?)\]/)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((m) => m[1]) : [];
15
20
  const versionMarker = existsSync(path.join(projectPath, "docs/.groundwork/VERSION")) ? readFileSync(path.join(projectPath, "docs/.groundwork/VERSION"), "utf8").trim() : null;
16
- const kitVersion = JSON.parse(readFileSync(path.join(kitPath, "packages/groundwork/package.json"), "utf8")).version;
21
+ const kitVersion = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version;
17
22
  return {
18
23
  versionMarker, kitVersion, driftedVersion: !!versionMarker && versionMarker !== kitVersion,
19
24
  skills: dirs(payload).map((name) => {
@@ -32,8 +37,11 @@ export function groundworkSkills(kitPath, projectPath) {
32
37
  /* Global playbook cards live in more than one harness home: Claude → ~/.claude/skills, Grok → ~/.grok/skills (+ ~/.grok/groundwork-playbook). Measure each. */
33
38
  const HARNESS_HOMES = [["claude", path.join(homedir(), ".claude/skills")], ["grok", path.join(homedir(), ".grok/skills")]];
34
39
  export function playbookCards(kitPath) {
35
- const srcRoot = path.join(kitPath, "packages/coord-playbook/skills");
36
- const kitVersion = JSON.parse(readFileSync(path.join(kitPath, "packages/coord-playbook/package.json"), "utf8")).version;
40
+ const root = packageRoot(kitPath, "playbook");
41
+ if (!root)
42
+ return { kitVersion: null, globalDir: HARNESS_HOMES[0][1], homes: [], cards: [], error: "agent-coordination not found — npm i -g agent-coordination (or set kitPath to a kit checkout)" };
43
+ const srcRoot = path.join(root, "skills");
44
+ const kitVersion = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version;
37
45
  const homes = HARNESS_HOMES.filter(([, d]) => existsSync(d));
38
46
  return {
39
47
  kitVersion, globalDir: HARNESS_HOMES[0][1], homes: homes.map(([h, d]) => ({ harness: h, dir: d })),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@davidbalzan/groundwork-console",
3
3
  "private": false,
4
- "version": "0.1.1",
4
+ "version": "0.1.4",
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "@davidbalzan/groundwork-seam": "^0.1.0",
@@ -1 +1 @@
1
- import{m as e,t}from"./mermaid-parser.core-bGhrBY9T.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-Bc2f8joo.js";import"./chunk-I66GZJ75-DyWmv43M.js";import{n as i,r as a,t as o}from"./chunk-6Q2QTUOP-B6kTvnZS.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-_OoTZaAK.js";var c=e().RailroadAbnf.parser.LangiumParser,l=n(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=n(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=n(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=n(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=n(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=n(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=n(e=>{s(e,o),e.title&&o.setTitle(e.title),e.rules.map(e=>o.addRule(m(e)))},`populateDb`),g={parser:{parse:n(e=>{o.clear(),r.debug(`[ABNF Parser] Starting Langium parse`);let n=c.parse(e);if(n.lexerErrors.length>0||n.parserErrors.length>0)throw new t(n);let i=n.value;r.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),r.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:o}},db:o,renderer:a,styles:i};export{g as diagram};
1
+ import{m as e,t}from"./mermaid-parser.core-DG-hDTtF.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-CJc45UuW.js";import"./chunk-I66GZJ75-Bazokq-r.js";import{n as i,r as a,t as o}from"./chunk-6Q2QTUOP-N3v574-9.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-CxtgV-Z8.js";var c=e().RailroadAbnf.parser.LangiumParser,l=n(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=n(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=n(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=n(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=n(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=n(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=n(e=>{s(e,o),e.title&&o.setTitle(e.title),e.rules.map(e=>o.addRule(m(e)))},`populateDb`),g={parser:{parse:n(e=>{o.clear(),r.debug(`[ABNF Parser] Starting Langium parse`);let n=c.parse(e);if(n.lexerErrors.length>0||n.parserErrors.length>0)throw new t(n);let i=n.value;r.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),r.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:o}},db:o,renderer:a,styles:i};export{g as diagram};
@@ -1 +1 @@
1
- import{n as e,t}from"./path-fybaL0A-.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-DTWyzWtG.js";function m(e){return e.innerRadius}function h(e){return e.outerRadius}function g(e){return e.startAngle}function _(e){return e.endAngle}function v(e){return e&&e.padAngle}function y(e,t,n,r,i,a,o,s){var c=n-e,l=r-t,u=o-i,d=s-a,f=d*c-u*l;if(!(f*f<1e-12))return f=(u*(t-a)-d*(e-i))/f,[e+f*c,t+f*l]}function b(e,t,n,r,i,a,o){var c=e-n,l=t-r,u=(o?a:-a)/d(c*c+l*l),f=u*l,p=-u*c,m=e+f,h=t+p,g=n+f,_=r+p,v=(m+g)/2,y=(h+_)/2,b=g-m,x=_-h,S=b*b+x*x,C=i-a,w=m*_-g*h,T=(x<0?-1:1)*d(s(0,C*C*S-w*w)),E=(w*x-b*T)/S,D=(-w*b-x*T)/S,O=(w*x+b*T)/S,k=(-w*b+x*T)/S,A=E-v,j=D-y,M=O-v,N=k-y;return A*A+j*j>M*M+N*N&&(E=O,D=k),{cx:E,cy:D,x01:-f,y01:-p,x11:E*(i/C-1),y11:D*(i/C-1)}}function x(){var s=m,x=h,S=e(0),C=null,w=g,T=_,E=v,D=null,O=t(k);function k(){var e,t,m=+s.apply(this,arguments),h=+x.apply(this,arguments),g=w.apply(this,arguments)-r,_=T.apply(this,arguments)-r,v=l(_-g),k=_>g;if(D||=e=O(),h<m&&(t=h,h=m,m=t),!(h>1e-12))D.moveTo(0,0);else if(v>c-1e-12)D.moveTo(h*u(g),h*a(g)),D.arc(0,0,h,g,_,!k),m>1e-12&&(D.moveTo(m*u(_),m*a(_)),D.arc(0,0,m,_,g,k));else{var A=g,j=_,M=g,N=_,P=v,F=v,I=E.apply(this,arguments)/2,L=I>1e-12&&(C?+C.apply(this,arguments):d(m*m+h*h)),R=p(l(h-m)/2,+S.apply(this,arguments)),z=R,B=R,V,H;if(L>1e-12){var U=o(L/m*a(I)),W=o(L/h*a(I));(P-=U*2)>1e-12?(U*=k?1:-1,M+=U,N-=U):(P=0,M=N=(g+_)/2),(F-=W*2)>1e-12?(W*=k?1:-1,A+=W,j-=W):(F=0,A=j=(g+_)/2)}var G=h*u(A),K=h*a(A),q=m*u(N),J=m*a(N);if(R>1e-12){var Y=h*u(j),X=h*a(j),Z=m*u(M),Q=m*a(M),$;if(v<i){if($=y(G,K,Z,Q,Y,X,q,J)){var ee=G-$[0],te=K-$[1],ne=Y-$[0],re=X-$[1],ie=1/a(f((ee*ne+te*re)/(d(ee*ee+te*te)*d(ne*ne+re*re)))/2),ae=d($[0]*$[0]+$[1]*$[1]);z=p(R,(m-ae)/(ie-1)),B=p(R,(h-ae)/(ie+1))}else z=B=0}}F>1e-12?B>1e-12?(V=b(Z,Q,G,K,h,B,k),H=b(Y,X,q,J,h,B,k),D.moveTo(V.cx+V.x01,V.cy+V.y01),B<R?D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,h,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),!k),D.arc(H.cx,H.cy,B,n(H.y11,H.x11),n(H.y01,H.x01),!k))):(D.moveTo(G,K),D.arc(0,0,h,A,j,!k)):D.moveTo(G,K),!(m>1e-12)||!(P>1e-12)?D.lineTo(q,J):z>1e-12?(V=b(q,J,Y,X,m,-z,k),H=b(G,K,Z,Q,m,-z,k),D.lineTo(V.cx+V.x01,V.cy+V.y01),z<R?D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,m,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),k),D.arc(H.cx,H.cy,z,n(H.y11,H.x11),n(H.y01,H.x01),!k))):D.arc(0,0,m,N,M,k)}if(D.closePath(),e)return D=null,e+``||null}return k.centroid=function(){var e=(+s.apply(this,arguments)+ +x.apply(this,arguments))/2,t=(+w.apply(this,arguments)+ +T.apply(this,arguments))/2-i/2;return[u(t)*e,a(t)*e]},k.innerRadius=function(t){return arguments.length?(s=typeof t==`function`?t:e(+t),k):s},k.outerRadius=function(t){return arguments.length?(x=typeof t==`function`?t:e(+t),k):x},k.cornerRadius=function(t){return arguments.length?(S=typeof t==`function`?t:e(+t),k):S},k.padRadius=function(t){return arguments.length?(C=t==null?null:typeof t==`function`?t:e(+t),k):C},k.startAngle=function(t){return arguments.length?(w=typeof t==`function`?t:e(+t),k):w},k.endAngle=function(t){return arguments.length?(T=typeof t==`function`?t:e(+t),k):T},k.padAngle=function(t){return arguments.length?(E=typeof t==`function`?t:e(+t),k):E},k.context=function(e){return arguments.length?(D=e??null,k):D},k}export{x as t};
1
+ import{n as e,t}from"./path-fybaL0A-.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-BFMCu1a5.js";function m(e){return e.innerRadius}function h(e){return e.outerRadius}function g(e){return e.startAngle}function _(e){return e.endAngle}function v(e){return e&&e.padAngle}function y(e,t,n,r,i,a,o,s){var c=n-e,l=r-t,u=o-i,d=s-a,f=d*c-u*l;if(!(f*f<1e-12))return f=(u*(t-a)-d*(e-i))/f,[e+f*c,t+f*l]}function b(e,t,n,r,i,a,o){var c=e-n,l=t-r,u=(o?a:-a)/d(c*c+l*l),f=u*l,p=-u*c,m=e+f,h=t+p,g=n+f,_=r+p,v=(m+g)/2,y=(h+_)/2,b=g-m,x=_-h,S=b*b+x*x,C=i-a,w=m*_-g*h,T=(x<0?-1:1)*d(s(0,C*C*S-w*w)),E=(w*x-b*T)/S,D=(-w*b-x*T)/S,O=(w*x+b*T)/S,k=(-w*b+x*T)/S,A=E-v,j=D-y,M=O-v,N=k-y;return A*A+j*j>M*M+N*N&&(E=O,D=k),{cx:E,cy:D,x01:-f,y01:-p,x11:E*(i/C-1),y11:D*(i/C-1)}}function x(){var s=m,x=h,S=e(0),C=null,w=g,T=_,E=v,D=null,O=t(k);function k(){var e,t,m=+s.apply(this,arguments),h=+x.apply(this,arguments),g=w.apply(this,arguments)-r,_=T.apply(this,arguments)-r,v=l(_-g),k=_>g;if(D||=e=O(),h<m&&(t=h,h=m,m=t),!(h>1e-12))D.moveTo(0,0);else if(v>c-1e-12)D.moveTo(h*u(g),h*a(g)),D.arc(0,0,h,g,_,!k),m>1e-12&&(D.moveTo(m*u(_),m*a(_)),D.arc(0,0,m,_,g,k));else{var A=g,j=_,M=g,N=_,P=v,F=v,I=E.apply(this,arguments)/2,L=I>1e-12&&(C?+C.apply(this,arguments):d(m*m+h*h)),R=p(l(h-m)/2,+S.apply(this,arguments)),z=R,B=R,V,H;if(L>1e-12){var U=o(L/m*a(I)),W=o(L/h*a(I));(P-=U*2)>1e-12?(U*=k?1:-1,M+=U,N-=U):(P=0,M=N=(g+_)/2),(F-=W*2)>1e-12?(W*=k?1:-1,A+=W,j-=W):(F=0,A=j=(g+_)/2)}var G=h*u(A),K=h*a(A),q=m*u(N),J=m*a(N);if(R>1e-12){var Y=h*u(j),X=h*a(j),Z=m*u(M),Q=m*a(M),$;if(v<i){if($=y(G,K,Z,Q,Y,X,q,J)){var ee=G-$[0],te=K-$[1],ne=Y-$[0],re=X-$[1],ie=1/a(f((ee*ne+te*re)/(d(ee*ee+te*te)*d(ne*ne+re*re)))/2),ae=d($[0]*$[0]+$[1]*$[1]);z=p(R,(m-ae)/(ie-1)),B=p(R,(h-ae)/(ie+1))}else z=B=0}}F>1e-12?B>1e-12?(V=b(Z,Q,G,K,h,B,k),H=b(Y,X,q,J,h,B,k),D.moveTo(V.cx+V.x01,V.cy+V.y01),B<R?D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,h,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),!k),D.arc(H.cx,H.cy,B,n(H.y11,H.x11),n(H.y01,H.x01),!k))):(D.moveTo(G,K),D.arc(0,0,h,A,j,!k)):D.moveTo(G,K),!(m>1e-12)||!(P>1e-12)?D.lineTo(q,J):z>1e-12?(V=b(q,J,Y,X,m,-z,k),H=b(G,K,Z,Q,m,-z,k),D.lineTo(V.cx+V.x01,V.cy+V.y01),z<R?D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,m,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),k),D.arc(H.cx,H.cy,z,n(H.y11,H.x11),n(H.y01,H.x01),!k))):D.arc(0,0,m,N,M,k)}if(D.closePath(),e)return D=null,e+``||null}return k.centroid=function(){var e=(+s.apply(this,arguments)+ +x.apply(this,arguments))/2,t=(+w.apply(this,arguments)+ +T.apply(this,arguments))/2-i/2;return[u(t)*e,a(t)*e]},k.innerRadius=function(t){return arguments.length?(s=typeof t==`function`?t:e(+t),k):s},k.outerRadius=function(t){return arguments.length?(x=typeof t==`function`?t:e(+t),k):x},k.cornerRadius=function(t){return arguments.length?(S=typeof t==`function`?t:e(+t),k):S},k.padRadius=function(t){return arguments.length?(C=t==null?null:typeof t==`function`?t:e(+t),k):C},k.startAngle=function(t){return arguments.length?(w=typeof t==`function`?t:e(+t),k):w},k.endAngle=function(t){return arguments.length?(T=typeof t==`function`?t:e(+t),k):T},k.padAngle=function(t){return arguments.length?(E=typeof t==`function`?t:e(+t),k):E},k.context=function(e){return arguments.length?(D=e??null,k):D},k}export{x as t};
@@ -0,0 +1 @@
1
+ import"./chunk-KEIR6QF5-BICK3FdT.js";import{M as e}from"./mermaid-parser.core-DG-hDTtF.js";export{e as createArchitectureServices};