@davidbalzan/groundwork-console 0.1.6 → 0.1.8

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 (104) hide show
  1. package/README.md +1 -1
  2. package/bin/groundwork-console.mjs +2 -0
  3. package/dist/answered.js +16 -34
  4. package/dist/gitinfo.js +114 -0
  5. package/dist/kitresolve.js +12 -9
  6. package/dist/mcp.js +41 -2
  7. package/dist/provision.js +94 -4
  8. package/dist/server.js +218 -41
  9. package/dist/skills.js +51 -7
  10. package/package.json +5 -2
  11. package/public/assets/{abnfDiagram-N423BO3Z-CdHjY-Rl.js → abnfDiagram-N423BO3Z-oco1yJr8.js} +1 -1
  12. package/public/assets/{arc-BrWBiZCE.js → arc-D-FYRBZ0.js} +1 -1
  13. package/public/assets/architecture-TIHT7OUA-bBJswTmy.js +1 -0
  14. package/public/assets/{architectureDiagram-T3A2C74G-BlL-ALnB.js → architectureDiagram-T3A2C74G-CIG6iOMV.js} +1 -1
  15. package/public/assets/{blockDiagram-VBNYF7ZC-DZyulKP3.js → blockDiagram-VBNYF7ZC-B6dDKm7d.js} +1 -1
  16. package/public/assets/{c4Diagram-5PPSVZJV-BMMLwghr.js → c4Diagram-5PPSVZJV-Y3UQgn94.js} +1 -1
  17. package/public/assets/channel-BeJg4QSq.js +1 -0
  18. package/public/assets/{chunk-2GRJ4B5K-BzJtMGzP.js → chunk-2GRJ4B5K-BoWCLegZ.js} +1 -1
  19. package/public/assets/{chunk-4I5QYGJK-DwFXVxXp.js → chunk-4I5QYGJK-DZTRMmZG.js} +1 -1
  20. package/public/assets/{chunk-5RXB4S5H-BohnhuTy.js → chunk-5RXB4S5H-Dw1IdYaS.js} +1 -1
  21. package/public/assets/{chunk-6Q2QTUOP-BXxDc17T.js → chunk-6Q2QTUOP-nWUr6zuz.js} +1 -1
  22. package/public/assets/{chunk-7Z6QIM7H-k5qXgkB4.js → chunk-7Z6QIM7H-DxXQZe2t.js} +1 -1
  23. package/public/assets/{chunk-GF5L2VYU-Cl8z_Pcd.js → chunk-GF5L2VYU-DbHTRiGa.js} +1 -1
  24. package/public/assets/{chunk-I66GZJ75-CCxcXl9W.js → chunk-I66GZJ75-C97VIOWu.js} +1 -1
  25. package/public/assets/{chunk-JQJVKLGR-Dah9RJ-p.js → chunk-JQJVKLGR-CeOq7ul_.js} +1 -1
  26. package/public/assets/{chunk-KBJHAD2P-B1kM7HCm.js → chunk-KBJHAD2P-D_xgFhfD.js} +1 -1
  27. package/public/assets/{chunk-NSK5VX7P-Ddw2Pneq.js → chunk-NSK5VX7P-DjNVQuCo.js} +1 -1
  28. package/public/assets/{chunk-QR6OTTB3-SR1VT_gf.js → chunk-QR6OTTB3-CGPHl8Ev.js} +1 -1
  29. package/public/assets/{chunk-RYQCIY6F-CsMzInBn.js → chunk-RYQCIY6F-Ca_NSSUq.js} +1 -1
  30. package/public/assets/{chunk-UBXNYLIW-CysKciL_.js → chunk-UBXNYLIW-BH4KEg7B.js} +1 -1
  31. package/public/assets/{chunk-W5SLKNZC-CueNCRji.js → chunk-W5SLKNZC-CzSVKrQX.js} +1 -1
  32. package/public/assets/{chunk-WRU74C26-CVxduXc3.js → chunk-WRU74C26--qTXNfid.js} +1 -1
  33. package/public/assets/{chunk-XXDRQBXY-DOdFQa-t.js → chunk-XXDRQBXY-0XJhmQM-.js} +1 -1
  34. package/public/assets/classDiagram-JCYQIIEL-B6RcDz49.js +1 -0
  35. package/public/assets/classDiagram-v2-OCEON4UE-B6RcDz49.js +1 -0
  36. package/public/assets/{cose-bilkent-JH36ORCC-B2Z9nj2r.js → cose-bilkent-JH36ORCC-CeMP2vk-.js} +1 -1
  37. package/public/assets/{cynefin-VYW2F7L2-CmV_z2bb.js → cynefin-VYW2F7L2-NtSbHkFO.js} +1 -1
  38. package/public/assets/{cynefinDiagram-MW4NZA55-zeWgiEbQ.js → cynefinDiagram-MW4NZA55-B4uWxM0-.js} +1 -1
  39. package/public/assets/{dagre-VZM6K2ZE-CaMVheN4.js → dagre-VZM6K2ZE-C8wHGLeh.js} +1 -1
  40. package/public/assets/{diagram-7IWD3JNH-BTJeDC0S.js → diagram-7IWD3JNH-DKe15FkE.js} +1 -1
  41. package/public/assets/{diagram-B4RE2ZJO-BUtRYMUD.js → diagram-B4RE2ZJO-B-pgDq0Z.js} +1 -1
  42. package/public/assets/{diagram-LBJQPF4R-DM--EdGk.js → diagram-LBJQPF4R-16bhvJKB.js} +1 -1
  43. package/public/assets/{diagram-Q27KOJAE-Dcp9D1k4.js → diagram-Q27KOJAE-BuzXd81m.js} +1 -1
  44. package/public/assets/{diagram-UB23O5K3-DzLpzKu2.js → diagram-UB23O5K3-CZzMkkP7.js} +1 -1
  45. package/public/assets/{dist-DFkPtM9v.js → dist-DoiwlY9_.js} +1 -1
  46. package/public/assets/{ebnfDiagram-BXEA7PRR-C2lFoa24.js → ebnfDiagram-BXEA7PRR-BGUXNic-.js} +1 -1
  47. package/public/assets/{erDiagram-JOGREHBK-GvedjkFY.js → erDiagram-JOGREHBK--mo3pLi7.js} +1 -1
  48. package/public/assets/eventmodeling-45OFAUF4-Bv0pQfuo.js +1 -0
  49. package/public/assets/flowDiagram-UKHOOZJN-cF6VTIV-.js +1 -0
  50. package/public/assets/{ganttDiagram-PKOTCBZU-NzyabXr_.js → ganttDiagram-PKOTCBZU-DbHv-Q1F.js} +1 -1
  51. package/public/assets/{gitGraph-TEB2WS4Q-CAnBW3hc.js → gitGraph-TEB2WS4Q-CU8msIuE.js} +1 -1
  52. package/public/assets/{gitGraphDiagram-DS77QQ5N-ChFOjBWW.js → gitGraphDiagram-DS77QQ5N-CmVui6qG.js} +1 -1
  53. package/public/assets/index-BZBf_g4s.css +2 -0
  54. package/public/assets/index-CJ73bGme.js +41 -0
  55. package/public/assets/{info-DKCQHKI2-9HGQNL8q.js → info-DKCQHKI2-CcA5kzgP.js} +1 -1
  56. package/public/assets/{infoDiagram-6WML65LV-CSZIpCN6.js → infoDiagram-6WML65LV-Drd9lSSV.js} +1 -1
  57. package/public/assets/{ishikawaDiagram-WSZJBQD7-m5LI_l0g.js → ishikawaDiagram-WSZJBQD7-CcnlbfwE.js} +1 -1
  58. package/public/assets/{journeyDiagram-NVQOT4AX-DlhpOOGP.js → journeyDiagram-NVQOT4AX-CYcWG8V9.js} +1 -1
  59. package/public/assets/{kanban-definition-27J2QSJJ-CpS1zcAJ.js → kanban-definition-27J2QSJJ-Ca5F0ySI.js} +1 -1
  60. package/public/assets/{line-DqdLvBGQ.js → line-BFnVTMoX.js} +1 -1
  61. package/public/assets/{linear-ZFSYfg_z.js → linear-BVUPf0HH.js} +1 -1
  62. package/public/assets/{mermaid-parser.core-B4KSdud0.js → mermaid-parser.core-BXY3JF8d.js} +3 -3
  63. package/public/assets/{mermaid.core-CSBAT3ig.js → mermaid.core-DqjaFy6t.js} +4 -4
  64. package/public/assets/{mindmap-definition-FAOFIHXS-DM39eR7l.js → mindmap-definition-FAOFIHXS-m3-vScki.js} +1 -1
  65. package/public/assets/{packet-7NZHBO7P-CaKShi1f.js → packet-7NZHBO7P-BTm7636C.js} +1 -1
  66. package/public/assets/{pegDiagram-VL7TDLO6-CeupMOTZ.js → pegDiagram-VL7TDLO6-C3kirtKa.js} +1 -1
  67. package/public/assets/{pie-RZYD4A2V-CdyK9ryQ.js → pie-RZYD4A2V-BsF__qFU.js} +1 -1
  68. package/public/assets/{pieDiagram-7S7Q4E2Y-BTsBpWka.js → pieDiagram-7S7Q4E2Y-YEbSx31D.js} +1 -1
  69. package/public/assets/{quadrantDiagram-CIZ2JOQS-UamjAqXX.js → quadrantDiagram-CIZ2JOQS-qY1c3Wr-.js} +1 -1
  70. package/public/assets/{radar-I7S5WNFK-BHPrA655.js → radar-I7S5WNFK-BaQRhfKS.js} +1 -1
  71. package/public/assets/{railroad-3IZDKUUU-BmnKim6H.js → railroad-3IZDKUUU-C4riB9xs.js} +1 -1
  72. package/public/assets/railroad-abnf-AHOZXSZD-CBcG7kXE.js +1 -0
  73. package/public/assets/railroad-ebnf-EBAXGLYW-EqgxHSrX.js +1 -0
  74. package/public/assets/railroad-peg-LSFZ7HO6-C5-qxTK4.js +1 -0
  75. package/public/assets/{railroadDiagram-AXF67PYL-bbNt4a4d.js → railroadDiagram-AXF67PYL-BDXHJJin.js} +1 -1
  76. package/public/assets/{requirementDiagram-LRYGKXZP-BrwBiGll.js → requirementDiagram-LRYGKXZP-5N_4nQH3.js} +1 -1
  77. package/public/assets/{sankeyDiagram-W5VNT64P-BeKu1QRQ.js → sankeyDiagram-W5VNT64P-C0zqsu_g.js} +1 -1
  78. package/public/assets/{sequenceDiagram-SI44F4Z6-DiBZBmdo.js → sequenceDiagram-SI44F4Z6-DonYJ8_h.js} +1 -1
  79. package/public/assets/{src-B2eptEoc.js → src-D5IewQLB.js} +1 -1
  80. package/public/assets/{stateDiagram-OKZ733FA-Tjie08JN.js → stateDiagram-OKZ733FA-CEY8i14f.js} +1 -1
  81. package/public/assets/stateDiagram-v2-UEYNNEHI-C13FXtls.js +1 -0
  82. package/public/assets/{swimlanes-SLNWSIFB-B_W9IMQM.js → swimlanes-SLNWSIFB-BBKzhZ8g.js} +1 -1
  83. package/public/assets/swimlanesDiagram-ULZ7WXOC-D0VKK_TP.js +8 -0
  84. package/public/assets/{timeline-definition-Z64GVDOM-B3CqBSPD.js → timeline-definition-Z64GVDOM-RI7P7P5y.js} +1 -1
  85. package/public/assets/{treeView-QDETBFTQ-BUEh9zyF.js → treeView-QDETBFTQ-Bpw_kGf5.js} +1 -1
  86. package/public/assets/{treemap-6X3UGDF4-D_t1bJ-9.js → treemap-6X3UGDF4-Deq82US6.js} +1 -1
  87. package/public/assets/{vennDiagram-T6HMQDX7-B1_XMyC0.js → vennDiagram-T6HMQDX7-DetRcU6h.js} +1 -1
  88. package/public/assets/{wardley-OPB4EBWU-BlIYjwGG.js → wardley-OPB4EBWU-Iwt8nlFq.js} +1 -1
  89. package/public/assets/{wardleyDiagram-T6FBY63Y-BUJJz-1q.js → wardleyDiagram-T6FBY63Y-iVT5tLSy.js} +1 -1
  90. package/public/assets/{xychartDiagram-ELKLHX3M-CgWS9hey.js → xychartDiagram-ELKLHX3M-B7_cZ8Wg.js} +1 -1
  91. package/public/index.html +2 -2
  92. package/public/assets/architecture-TIHT7OUA-nRAC6cKL.js +0 -1
  93. package/public/assets/channel-CmRhJjbj.js +0 -1
  94. package/public/assets/classDiagram-JCYQIIEL-BhHtVWEo.js +0 -1
  95. package/public/assets/classDiagram-v2-OCEON4UE-BhHtVWEo.js +0 -1
  96. package/public/assets/eventmodeling-45OFAUF4-CVrSQ2LC.js +0 -1
  97. package/public/assets/flowDiagram-UKHOOZJN-BWxvJEZS.js +0 -1
  98. package/public/assets/index-CsJm47wv.css +0 -2
  99. package/public/assets/index-DPLoCbew.js +0 -40
  100. package/public/assets/railroad-abnf-AHOZXSZD-n6VUqVzW.js +0 -1
  101. package/public/assets/railroad-ebnf-EBAXGLYW-Buhu71hT.js +0 -1
  102. package/public/assets/railroad-peg-LSFZ7HO6-DxxRF3eB.js +0 -1
  103. package/public/assets/stateDiagram-v2-UEYNNEHI-Bt5as9x1.js +0 -1
  104. package/public/assets/swimlanesDiagram-ULZ7WXOC-B1GEgwSB.js +0 -8
package/dist/server.js CHANGED
@@ -16,21 +16,21 @@ import { agents, agentsAsync, rooms, roomMessages, roomMessageCount, statusStrea
16
16
  import { capturePane } from "./tmux.js";
17
17
  import { spawnAgent, suggestId, fleetStatus, FLEET_TEMPLATES, HARNESS_CMD, ROLE_SKILL } from "./provision.js";
18
18
  import { doctor, status, list, playbookList } from "./groundwork.js";
19
- import { groundworkSkills, playbookCards } from "./skills.js";
19
+ import { detectedHarnesses, groundworkSkills, playbookCards } from "./skills.js";
20
20
  import { extensionsOf } from "./extensions.js";
21
21
  import { parseWorkDoc, workstreamsExtensionsOf } from "@davidbalzan/groundwork-seam";
22
22
  import { EventEngine } from "./events.js";
23
- import { docGit, docsStatus, lastDocsCommit } from "./gitinfo.js";
23
+ import { docGit, docsStatus, lastDocsCommit, gitGraph, gitBranches, gitWorktrees } from "./gitinfo.js";
24
24
  import { Notifier } from "./notify.js";
25
25
  import { Runs } from "./runs.js";
26
26
  import { BusClient, AGENT_ID, busWrites, seamWrites, saveSwitches, SWITCHES_FILE } from "./mcp.js";
27
27
  import { addQueueItem, reverifyFact, scopeAllows } from "./seamwrite.js";
28
28
  import { fileURLToPath } from "node:url";
29
29
  import { saveUpload, uploadPath, purgeUploads, UPLOAD_DIR, UPLOAD_TTL_HOURS } from "./uploads.js";
30
- import { answered, markAnswered, isAnswered, setCoordDir } from "./answered.js";
30
+ import { isAnswered, setCoordDir, invalidateAnswered, removeLegacyAnsweredFile } from "./answered.js";
31
31
  import { requireAuth, login, logout, isAuthed, authEnabled, tokenFile } from "./auth.js";
32
32
  import { knowledgeIndex, knowledgeDoc, knowledgePath } from "./knowledge.js";
33
- import { requiresReport, groundworkCli, playbookCli, packageRoot, REQUIRES } from "./kitresolve.js";
33
+ import { requiresReport, groundworkCli, playbookCli, packageRoot, hasKitCheckout, REQUIRES } from "./kitresolve.js";
34
34
  let cfg = loadConfig();
35
35
  const port = Number(process.env.PORT ?? 8790);
36
36
  const HERE = path.dirname(fileURLToPath(import.meta.url));
@@ -38,11 +38,17 @@ const RULES = process.env.GROUNDWORK_CONSOLE_RULES ?? [path.join(process.env.HOM
38
38
  const PKG = JSON.parse(readFileSync(path.resolve(HERE, "../package.json"), "utf8"));
39
39
  const runs = new Runs();
40
40
  setCoordDir(cfg.coordDir);
41
+ if (removeLegacyAnsweredFile())
42
+ void runs.exec({ kind: "cli", label: "retired ~/.groundwork-console/answered.json (answered = bus inReplyTo now)", sideEffects: true }, async (log) => { log("removed the console-owned answered store; decision packets are answered by replies carrying inReplyTo"); return { removed: true }; });
41
43
  const bus = new BusClient(cfg.kitPath, cfg.coordDir, "groundwork-console", process.env.AGENT_COORD_MCP);
42
44
  const events = new EventEngine(cfg, RULES, runs, (args, log) => bus.call("send_message", args, log));
43
45
  const notifier = new Notifier(cfg, runs, events);
44
46
  const busDavid = new BusClient(cfg.kitPath, cfg.coordDir, "david", process.env.AGENT_COORD_MCP);
45
47
  const clientFor = (id) => (id === "david" ? busDavid : bus);
48
+ /** Capabilities of the connected MCP (null until a client has connected and listed tools). */
49
+ const mcpCaps = () => { const cl = bus.connected ? bus : busDavid.connected ? busDavid : null; return { inReplyTo: cl ? cl.supports("send_message", "inReplyTo") : null, sendCommand: cl ? cl.sendCommands : null }; };
50
+ /* Live tmux-attached agents — the only valid send_command targets (the MCP hard-gates to tmux-push/-remote). */
51
+ const liveAttached = () => agents(cfg.coordDir).filter((a) => a.liveness === "live" && (a.transport === "tmux-push" || a.transport === "tmux-push-remote")).map((a) => a.agentId);
46
52
  const PUBLIC_DIR = path.resolve(HERE, "../public");
47
53
  const root = new Hono();
48
54
  const app = new Hono().basePath("/api");
@@ -57,7 +63,7 @@ app.post("/auth/logout", (c) => { logout(c); return c.json({ ok: true }); });
57
63
  app.get("/auth/login", (c) => { const t = c.req.query("token") ?? ""; if (!login(c, t))
58
64
  return c.json({ error: "wrong token" }, 401); return c.redirect(c.req.query("next") ?? "/"); });
59
65
  const project = (name) => cfg.projects.find((p) => p.name === name);
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$/, "") }));
66
+ app.get("/health", (c) => c.json({ ok: true, readOnly: !busWrites.enabled, busWrites: busWrites.enabled, version: PKG.version, requires: requiresReport(cfg.kitPath, mcpCaps()), config: CONFIG_FILE, hostname: osHostname().replace(/\.local$/, "") }));
61
67
  app.get("/admin/bus-writes", (c) => c.json(busWrites));
62
68
  app.get("/admin/seam-writes", (c) => c.json(seamWrites));
63
69
  app.post("/admin/seam-writes", async (c) => {
@@ -109,6 +115,36 @@ app.post("/actions/send", async (c) => {
109
115
  return c.json({ error: "text required" }, 400);
110
116
  if (!b.room && !b.to)
111
117
  return c.json({ error: "room or to required" }, 400);
118
+ // Raw control commands: a bare `/<cmd>` (no args) to ONE agent goes through MCP send_command — the only path that types a slash command into a live CLI.
119
+ // The allowlist is the CONNECTED server's (feature-detected: 0.24.0 = /clear /compact; 0.25.0 adds /reload-skills — BusClient.sendCommands). A bare slash
120
+ // command the server does not accept degrades to the text wrapper `AGENT_ACTION: run /<cmd>` (never a 4xx; the agent runs it from the request, #14).
121
+ const cmd = b.text.trim();
122
+ let degraded;
123
+ if (/^\/[a-z][\w-]*$/i.test(cmd) && b.to) {
124
+ const cl = clientFor(who);
125
+ try {
126
+ await cl.connect();
127
+ }
128
+ catch { /* not connected → no raw path; fall through to the text DM */ }
129
+ if (!cl.acceptsCommand(cmd)) {
130
+ const have = cl.sendCommands;
131
+ degraded = `${cmd} is not in the connected agent-coord-mcp's send_command allowlist (${have ? have.join(" ") : "unknown"})${cmd === "/reload-skills" ? " — needs agent-coord-mcp ≥0.25.0 + MCP session restart" : ""}; sent as a text request instead`;
132
+ b.text = `AGENT_ACTION: run ${cmd}`;
133
+ }
134
+ }
135
+ if (/^\/[a-z][\w-]*$/i.test(cmd) && !degraded && (b.to ? clientFor(who).acceptsCommand(cmd) : false)) {
136
+ const r = await runs.exec({ kind: "bus", label: `command ${cmd} → ${b.to}`, sideEffects: true, args: { as: who, to: b.to, command: cmd } }, async (log) => {
137
+ const out = (await clientFor(who).call("send_command", { to: b.to, command: cmd, waitForDelivery: true, deliveryTimeoutMs: 15_000 }, log));
138
+ log(`delivery: ${out.delivery ?? "?"}${out.reason ? ` · ${out.reason}` : ""}${out.note ? ` · ${out.note}` : ""}`);
139
+ if (out.error)
140
+ throw new Error(out.error);
141
+ return out;
142
+ });
143
+ const out = (r.result ?? {});
144
+ return c.json({ ...r, raw: true, delivery: out.delivery ?? null, warning: out.delivery === "pending" ? `pusher has not confirmed typing ${cmd} yet${out.reason ? ` — ${out.reason}` : ""}` : undefined }, r.status === "ok" ? 200 : 403);
145
+ }
146
+ if (/^\/[a-z][\w-]*$/i.test(cmd) && b.room && !b.to)
147
+ return c.json({ error: `${cmd} is unicast — pick an agent (rooms get text only)` }, 400);
112
148
  const PREFIX_RE = /^(BLOCKER|RISK|DONE|FYI|AGENT_ACTION|GO|SCOPE CHANGE|VERDICT|DAVID_DECISION)\b/;
113
149
  if (b.room && !PREFIX_RE.test(b.text))
114
150
  return c.json({ error: "room posts need a prefix (FYI / AGENT_ACTION / DAVID_DECISION / BLOCKER / RISK / DONE / GO / SCOPE CHANGE / VERDICT)" }, 400);
@@ -116,32 +152,85 @@ app.post("/actions/send", async (c) => {
116
152
  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) => {
117
153
  const cl = clientFor(who);
118
154
  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);
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.
125
- if (who === "david") {
126
- const ids = [...(b.answers ?? [])];
127
- if (b.to) {
128
- const f = path.join(cfg.coordDir, "inbox", "david.jsonl");
129
- if (existsSync(f))
130
- for (const l of readFileSync(f, "utf8").split("\n").filter(Boolean)) {
131
- try {
132
- const m = JSON.parse(l);
133
- if (m.from === b.to && typeof m.text === "string" && m.text.startsWith("DAVID_DECISION") && m.id)
134
- ids.push(m.id);
135
- }
136
- catch { /* skip */ }
155
+ if (degraded)
156
+ log(`⚠ ${degraded}`);
157
+ // Reply linkage = bus inReplyTo only (console-owned answered.json retired 2026-08-19). Parent: explicit from the UI (answers[0] / inReplyTo);
158
+ // for a DM as David with no explicit parent, the latest still-open DAVID_DECISION DM from that agent — so "reply to the agent" clears its packet.
159
+ let parent = b.inReplyTo ?? b.answers?.[0];
160
+ if (!parent && who === "david" && b.to) {
161
+ const f = path.join(cfg.coordDir, "inbox", "david.jsonl");
162
+ if (existsSync(f))
163
+ for (const l of readFileSync(f, "utf8").split("\n").filter(Boolean)) {
164
+ try {
165
+ const m = JSON.parse(l);
166
+ if (m.from === b.to && typeof m.text === "string" && m.text.startsWith("DAVID_DECISION") && m.id && !isAnswered(m.id))
167
+ parent = m.id;
137
168
  }
138
- }
139
- const n = markAnswered(ids, { by: "david", to: b.to ?? b.room, note: b.text.slice(0, 80) });
140
- if (n)
141
- log(`answered: ${n} decision packet${n === 1 ? "" : "s"} marked (console-owned ~/.groundwork-console/answered.json)`);
169
+ catch { /* skip */ }
170
+ }
171
+ if (parent)
172
+ log(`inReplyTo latest open DAVID_DECISION from ${b.to} (${parent})`);
173
+ }
174
+ const supportsReply = cl.supports("send_message", "inReplyTo");
175
+ if (parent && !supportsReply)
176
+ log("⚠ connected agent-coord-mcp has no inReplyTo — reply will NOT be linked to the packet; upgrade agent-coord-mcp (gwc doctor)");
177
+ const out = await cl.call("send_message", { ...(b.to ? { to: b.to } : { room: b.room.replace(/^#/, "") }), text: b.text, ...(b.kind ? { kind: b.kind } : {}), ...(parent && supportsReply ? { inReplyTo: parent } : {}) }, log);
178
+ if (parent && supportsReply) {
179
+ invalidateAnswered();
180
+ log(`linked: inReplyTo ${parent}`);
142
181
  }
143
182
  return out;
144
183
  });
184
+ return c.json({ ...r, ...(degraded ? { raw: false, degraded: true, warning: degraded } : {}) }, r.status === "ok" ? 200 : 403);
185
+ });
186
+ /* Ops › Skills: after a playbook update, push `/reload-skills` RAW to every live tmux-attached agent (send_command, bus-writes gated, only when the connected
187
+ MCP's allowlist carries it — 0.25.0+). Per-agent delivery confirmed|pending in the Run. Targets may be narrowed with `agentIds`. */
188
+ app.get("/actions/reload-skills", async (c) => {
189
+ const cl = bus;
190
+ try {
191
+ await cl.connect();
192
+ }
193
+ catch { /* report what we know */ }
194
+ return c.json({ supported: cl.acceptsCommand("/reload-skills"), allowlist: cl.sendCommands, agents: liveAttached(), hint: cl.acceptsCommand("/reload-skills") ? null : "needs agent-coord-mcp ≥0.25.0 (send_command allowlist with /reload-skills) + a restart of the console's MCP session and the fleet's" });
195
+ });
196
+ app.post("/actions/reload-skills", async (c) => {
197
+ const b = (await c.req.json().catch(() => ({})));
198
+ if (b.confirm !== "RELOAD")
199
+ return c.json({ error: "confirm must be RELOAD" }, 400);
200
+ if (!busWrites.enabled)
201
+ return c.json({ error: "bus writes are off — switch them on in the header (WRITES) first" }, 403);
202
+ const cl = bus;
203
+ try {
204
+ await cl.connect();
205
+ }
206
+ catch (e) {
207
+ return c.json({ error: e.message }, 500);
208
+ }
209
+ if (!cl.acceptsCommand("/reload-skills"))
210
+ return c.json({ error: `connected agent-coord-mcp does not accept /reload-skills (allowlist ${cl.sendCommands?.join(" ") ?? "unknown"}) — needs ≥0.25.0 + MCP session restart` }, 409);
211
+ const live = liveAttached();
212
+ const targets = (b.agentIds?.length ? b.agentIds.filter((id) => live.includes(id)) : live);
213
+ if (!targets.length)
214
+ return c.json({ error: "no live tmux-attached agents to reload" }, 400);
215
+ const r = await runs.exec({ kind: "bus", label: `reload skills → ${targets.length} session${targets.length === 1 ? "" : "s"}`, sideEffects: true, args: { agentIds: targets } }, async (log) => {
216
+ const results = [];
217
+ for (const to of targets) {
218
+ try {
219
+ const out = (await cl.call("send_command", { to, command: "/reload-skills", waitForDelivery: true, deliveryTimeoutMs: 15_000 }, log));
220
+ const delivery = out.error ? `error: ${out.error}` : out.delivery ?? "?";
221
+ results.push({ agentId: to, delivery, ...(out.reason ? { reason: out.reason } : {}) });
222
+ log(`${to}: ${delivery}${out.reason ? ` · ${out.reason}` : ""}`);
223
+ }
224
+ catch (e) {
225
+ results.push({ agentId: to, delivery: `error: ${e.message}` });
226
+ log(`${to}: error ${e.message}`);
227
+ }
228
+ }
229
+ const confirmed = results.filter((x) => x.delivery === "confirmed").length, pending = results.filter((x) => x.delivery === "pending").length;
230
+ if (confirmed + pending === 0)
231
+ throw new Error(`no session accepted /reload-skills (${results.map((x) => `${x.agentId}: ${x.delivery}`).join("; ")})`); // an all-error push is a failed Run, never ok
232
+ return { results, confirmed, pending };
233
+ });
145
234
  return c.json(r, r.status === "ok" ? 200 : 403);
146
235
  });
147
236
  /* Uploads — pasted images for DMs. Path + served URL both go into the message text as `[image: <path> · <url>]`.
@@ -285,17 +374,61 @@ app.get("/projects/:name/status", async (c) => {
285
374
  return c.json({ error: "unknown project" }, 404);
286
375
  return c.json(await status(cfg.kitPath, p.path));
287
376
  });
377
+ /* ---- Git branches screen v1 (docs/contracts/git-screen-v1.md): read-only git/gh, 3 s cache, errors → { error } ---- */
378
+ const gitHints = (p) => {
379
+ const src = existsSync(path.join(p.path, "docs/WORKSTREAMS.md")) ? readFileSync(path.join(p.path, "docs/WORKSTREAMS.md"), "utf8") : null;
380
+ const rows = boardOf(p.path).workstreams.rows.flatMap((r) => ("branchWorktree" in r ? [{ owner: r.owner, branchWorktree: r.branchWorktree }] : []));
381
+ let openPrs = [];
382
+ try {
383
+ openPrs = src ? workstreamsExtensionsOf(parseWorkDoc(src)).openPrs : [];
384
+ }
385
+ catch { /* board unparsable → no qa column */ }
386
+ return { agentIds: agents(cfg.coordDir).map((a) => a.agentId), boardRows: rows, openPrs };
387
+ };
388
+ app.get("/projects/:name/git/graph", async (c) => {
389
+ const p = project(c.req.param("name"));
390
+ if (!p)
391
+ return c.json({ error: "unknown project" }, 404);
392
+ try {
393
+ return c.json(await gitGraph(p.path, Number(c.req.query("limit") ?? 200)));
394
+ }
395
+ catch (e) {
396
+ return c.json({ error: e.message }, 500);
397
+ }
398
+ });
399
+ app.get("/projects/:name/git/branches", async (c) => {
400
+ const p = project(c.req.param("name"));
401
+ if (!p)
402
+ return c.json({ error: "unknown project" }, 404);
403
+ try {
404
+ return c.json(await gitBranches(p.path, gitHints(p)));
405
+ }
406
+ catch (e) {
407
+ return c.json({ error: e.message }, 500);
408
+ }
409
+ });
410
+ app.get("/projects/:name/git/worktrees", async (c) => {
411
+ const p = project(c.req.param("name"));
412
+ if (!p)
413
+ return c.json({ error: "unknown project" }, 404);
414
+ try {
415
+ return c.json(await gitWorktrees(p.path, gitHints(p)));
416
+ }
417
+ catch (e) {
418
+ return c.json({ error: e.message }, 500);
419
+ }
420
+ });
288
421
  app.get("/projects/:name/skills", async (c) => {
289
422
  const p = project(c.req.param("name"));
290
423
  if (!p)
291
424
  return c.json({ error: "unknown project" }, 404);
292
425
  const [gl, pl] = await Promise.all([list(cfg.kitPath, p.path), playbookList(cfg.kitPath)]);
293
426
  const sync = groundworkSkills(cfg.kitPath, p.path);
294
- const cards = playbookCards(cfg.kitPath);
427
+ const cards = await playbookCards(cfg.kitPath);
295
428
  return c.json({
296
- groundwork: { versionMarker: gl.version || sync.versionMarker, kitVersion: sync.kitVersion, driftedVersion: (gl.version || sync.versionMarker) !== sync.kitVersion, error: gl.error,
429
+ groundwork: { versionMarker: gl.version || sync.versionMarker, kitVersion: sync.kitVersion, source: sync.source ?? null, driftedVersion: (gl.version || sync.versionMarker) !== sync.kitVersion, error: gl.error,
297
430
  skills: gl.skills.map((s) => ({ ...s, tier: s.set, inSync: sync.skills.find((x) => x.name === s.name)?.inSync ?? null })) },
298
- playbook: { kitVersion: pl.version || cards.kitVersion, globalDir: cards.globalDir, homes: cards.homes, legacyDirs: cards.legacyDirs, error: pl.error,
431
+ playbook: { kitVersion: pl.version || cards.kitVersion, source: cards.source ?? null, rewrite: cards.rewrite ?? null, globalDir: cards.globalDir, homes: cards.homes, legacyDirs: cards.legacyDirs, error: pl.error,
299
432
  cards: pl.cards.map((k) => { const x = cards.cards.find((y) => y.name === k.name); const kh = k.harness; return { ...k, inSync: x?.inSync ?? null, slices: x?.slices ?? 0, legacyEssay: x?.legacyEssay ?? false, homes: x?.homes ?? {}, mirrors: { cursor: kh.cursor, vscode: kh.vscode, opencode: kh.opencode, grok: kh.grok ?? x?.homes?.grok?.installed ?? false } }; }) },
300
433
  });
301
434
  });
@@ -485,8 +618,6 @@ app.get("/bus/inbox", async (c) => {
485
618
  });
486
619
  /* DM thread with one agent, both directions: their DMs to david (inbox/david.jsonl) + david's DMs to them (inbox/<agent>.jsonl).
487
620
  Read-only file scan (no cursor consumed) — this is a view, the Bus inbox stays the place that marks read. */
488
- app.get("/bus/answered", (c) => c.json(answered()));
489
- app.post("/bus/answered", async (c) => { const b = (await c.req.json()); const n = markAnswered(b.ids ?? [], { by: "david", note: b.note }); return c.json({ ok: true, marked: n }); });
490
621
  app.get("/bus/dm/:agent", (c) => {
491
622
  const agent = c.req.param("agent");
492
623
  const limit = Number(c.req.query("limit") ?? 80);
@@ -572,14 +703,18 @@ app.post("/actions/playbook-update", async (c) => {
572
703
  const cli = playbookCli(cfg.kitPath);
573
704
  if (!cli)
574
705
  return c.json({ error: "agent-coordination CLI not found — npm i -g agent-coordination (or set kitPath to a kit checkout)" }, 500);
575
- const argv = [cli, "update", ...(dry ? ["--dry-run"] : [])];
576
- const r = await runs.exec({ kind: "shell", label: `agent-coordination update${dry ? " (dry-run)" : ""}`, sideEffects: !dry, args: { argv: argv.slice(1) } }, async (log) => {
706
+ // --harness= every home detected on this machine (~/.claude, ~/.grok, …) so grok & co. mirrors are refreshed too — not only the Claude home (the no-flag default is all five, but be explicit in the Run).
707
+ const harnesses = detectedHarnesses();
708
+ if (!harnesses.length)
709
+ return c.json({ error: "no harness home found (~/.claude, ~/.grok, ~/.cursor, ~/.vscode, ~/.config/opencode)" }, 400);
710
+ const argv = [cli, "update", `--harness=${harnesses.join(",")}`, ...(dry ? ["--dry-run"] : [])];
711
+ const r = await runs.exec({ kind: "shell", label: `agent-coordination update${dry ? " (dry-run · preview)" : ""} · ${harnesses.join(",")}`, sideEffects: !dry, args: { argv: argv.slice(1), harnesses } }, async (log) => {
577
712
  log(`$ node ${argv.map((a) => a.replace(cfg.kitPath, "<kit>")).join(" ")}`);
578
713
  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 ?? "" }; });
579
714
  const lines = (out.stdout + out.stderr).replace(/\x1b\[[0-9;]*m/g, "").split("\n").filter(Boolean);
580
715
  for (const l of lines.slice(0, 120))
581
716
  log(l);
582
- return { lines: lines.length, dryRun: dry };
717
+ return { lines: lines.length, dryRun: dry, harnesses };
583
718
  });
584
719
  return c.json(r, r.status === "ok" ? 200 : 500);
585
720
  });
@@ -836,12 +971,49 @@ app.get("/runs/stream", (c) => {
836
971
  });
837
972
  return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" } });
838
973
  });
839
- /* Actions (allow-listed; each becomes a Run). Read-only build: only read-only MCP tools + read-only CLIs. */
974
+ const doctorCounts = (d) => ({ warn: d?.summary?.warn ?? (d?.findings ?? []).filter((f) => f.level === "warn").length, error: d?.summary?.error ?? (d?.findings ?? []).filter((f) => f.level === "error").length, fixable: (d?.findings ?? []).filter((f) => f.fixable && f.level !== "ok").length });
840
975
  app.post("/actions/bus-doctor", async (c) => {
841
976
  const body = await c.req.json().catch(() => ({}));
842
- if (body.fix)
843
- return c.json({ error: "fix=true is a write; disabled in read-only build" }, 403);
844
- const r = await runs.exec({ kind: "mcp", label: "bus doctor", sideEffects: false, args: { tool: "doctor", agentId: AGENT_ID } }, (log) => bus.call("doctor", {}, log));
977
+ if (!body.fix) {
978
+ const r = await runs.exec({ kind: "mcp", label: "bus doctor", sideEffects: false, args: { tool: "doctor", agentId: AGENT_ID } }, (log) => bus.call("doctor", {}, log));
979
+ return c.json(r, r.status === "ok" ? 200 : 500);
980
+ }
981
+ // fix=true: the MCP doctor's OWN fix path (orphan markers etc. — only the findings it tags fixable). Gated like every bus write. Before → fix → after, with the delta.
982
+ if (!busWrites.enabled)
983
+ return c.json({ error: "bus writes are off — switch them on in the header (WRITES), then Fix fixable" }, 403);
984
+ const r = await runs.exec({ kind: "mcp", label: "bus doctor · fix", sideEffects: true, args: { tool: "doctor", fix: true, agentId: AGENT_ID } }, async (log) => {
985
+ const before = (await bus.call("doctor", {}, log));
986
+ const b = doctorCounts(before);
987
+ log(`before: ${b.warn} warn · ${b.error} error · ${b.fixable} fixable`);
988
+ const fixed = (await bus.call("doctor", { fix: true }, log));
989
+ log(`fix: ${fixed.fixApplied ? "applied" : "nothing applied"}`);
990
+ const after = (await bus.call("doctor", {}, log));
991
+ const a = doctorCounts(after);
992
+ log(`after: ${a.warn} warn · ${a.error} error · ${a.fixable} fixable (Δ warn ${a.warn - b.warn}, error ${a.error - b.error}, fixable ${a.fixable - b.fixable})`);
993
+ return { ...after, fixed: { applied: !!fixed.fixApplied, fixedFindings: (before.findings ?? []).filter((f) => f.fixable && f.level !== "ok" && !(after.findings ?? []).some((g) => g.check === f.check && g.level !== "ok")).map((f) => f.check) }, before: b, after: a, delta: { warn: a.warn - b.warn, error: a.error - b.error, fixable: a.fixable - b.fixable } };
994
+ });
995
+ return c.json(r, r.status === "ok" ? 200 : 500);
996
+ });
997
+ /* dist-behind-source is not doctor-fixable: rebuild the kit's MCP (the doctor's own advice: `npm run build` in packages/coord-mcp — here via the kit workspace: `pnpm --filter agent-coord-mcp build`, cwd = kit checkout). Only when a kit checkout is configured; never touches ~/agent-coord. */
998
+ app.post("/actions/rebuild-kit-mcp", async (c) => {
999
+ if (!busWrites.enabled)
1000
+ return c.json({ error: "bus writes are off — switch them on in the header (WRITES) first" }, 403);
1001
+ const b = (await c.req.json().catch(() => ({})));
1002
+ if (b.confirm !== "BUILD")
1003
+ return c.json({ error: "confirm must be BUILD" }, 400);
1004
+ if (!hasKitCheckout(cfg.kitPath))
1005
+ return c.json({ error: `no kit checkout at kitPath (${cfg.kitPath || "unset"}) — on this machine the MCP is the installed npm package; upgrade it with npm i -g agent-coord-mcp@latest, or ask the kit coordinator to publish` }, 409);
1006
+ const argv = ["pnpm", "--filter", "agent-coord-mcp", "build"];
1007
+ const r = await runs.exec({ kind: "shell", label: "rebuild kit MCP (pnpm --filter agent-coord-mcp build)", sideEffects: true, args: { cwd: cfg.kitPath, argv } }, async (log) => {
1008
+ log(`$ ${argv.join(" ")} (cwd ${cfg.kitPath})`);
1009
+ const out = await execP(argv[0], argv.slice(1), { cwd: cfg.kitPath, timeout: 180_000, maxBuffer: 8 << 20 }).catch((e) => { log(`exit: ${e.message}`); return { stdout: e.stdout ?? "", stderr: e.stderr ?? "", failed: true }; });
1010
+ for (const l of (out.stdout + out.stderr).replace(/\x1b\[[0-9;]*m/g, "").split("\n").filter(Boolean).slice(-40))
1011
+ log(l);
1012
+ if (out.failed)
1013
+ throw new Error("build failed — see log");
1014
+ log("built. Live MCP sessions still run the old inode: restart agents (detach_agent + attach_agent) or wait for their next session; the console's own client reconnects on next use.");
1015
+ return { ok: true, cwd: cfg.kitPath };
1016
+ });
845
1017
  return c.json(r, r.status === "ok" ? 200 : 500);
846
1018
  });
847
1019
  app.post("/actions/project-doctor/:name", async (c) => {
@@ -912,8 +1084,13 @@ setInterval(runPurge, 30 * 60_000).unref();
912
1084
  /* Packaged mode: the built web UI lives in ../public (copied at prepack) and is served by the same process at / — one port, no Vite. Dev keeps using Vite on :5173 with the /api proxy. */
913
1085
  root.route("/", app);
914
1086
  if (existsSync(PUBLIC_DIR)) {
915
- root.use("/*", serveStatic({ root: PUBLIC_DIR }));
916
- root.get("*", (c) => c.html(readFileSync(path.join(PUBLIC_DIR, "index.html"), "utf8"))); // hash router index for everything else
1087
+ // Cache policy: the shell (index.html) is never cached (so an upgraded API serves the new UI on next load — David saw a 0.1.5 UI on 0.1.6 until a hard refresh);
1088
+ // hashed Vite assets under /assets/* are immutable for a year; everything else (fonts, icons) a day.
1089
+ const shell = (c) => { c.header("Cache-Control", "no-cache"); c.header("X-Console-Version", PKG.version); return c.html(readFileSync(path.join(PUBLIC_DIR, "index.html"), "utf8")); };
1090
+ root.get("/", shell);
1091
+ root.get("/index.html", shell);
1092
+ root.use("/*", serveStatic({ root: PUBLIC_DIR, onFound: (p, c) => { c.header("Cache-Control", /\/assets\/[^/]+-[\w-]{6,}\.\w+$/.test(p) ? "public, max-age=31536000, immutable" : "public, max-age=86400"); } }));
1093
+ root.get("*", shell); // hash router → index for everything else
917
1094
  }
918
1095
  // Bind loopback only: the console can write to the bus and to docs/ and has no auth yet. Set GROUNDWORK_CONSOLE_HOST=0.0.0.0 to expose (put auth in front first).
919
1096
  const hostname = process.env.GROUNDWORK_CONSOLE_HOST ?? "127.0.0.1";
package/dist/skills.js CHANGED
@@ -1,10 +1,41 @@
1
- /* Supplement to the CLIs' `list --json`: byte-compare installed SKILL.md with the kit source (in-sync signal the CLIs do not report). */
1
+ /* Supplement to the CLIs' `list --json`: compare installed SKILL.md with what the installer would write (in-sync signal the CLIs do not report).
2
+ groundwork: byte-copy → raw hash. playbook: per-harness placeholder rewrite → hash the rewritten source (see loadRewrite). */
2
3
  import { existsSync, readFileSync, readdirSync } from "node:fs";
3
4
  import { createHash } from "node:crypto";
4
5
  import { homedir } from "node:os";
5
6
  import path from "node:path";
6
7
  import { packageRoot } from "./kitresolve.js";
8
+ import { pathToFileURL } from "node:url";
9
+ const FALLBACK_REWRITE = {
10
+ claude: { tool: (n) => `mcp__agent-coord__${n}`, tools: "mcp__agent-coord__*" },
11
+ grok: { tool: (n) => `agent-coord__${n}`, tools: "agent-coord__*", playbookHome: "~/.grok/groundwork-playbook" },
12
+ cursor: { tool: (n) => n, tools: "the agent-coord MCP tools" },
13
+ vscode: { tool: (n) => `#tool:${n}`, tools: "#tool:agent-coord/*" },
14
+ opencode: { tool: (n) => `agent-coord_${n}`, tools: "agent-coord_*" },
15
+ generic: { tool: (n) => `the \`${n}\` tool on the \`agent-coord\` MCP server`, tools: "the tools on the `agent-coord` MCP server" },
16
+ };
17
+ const fallbackRewrite = (text, harness) => { const spec = FALLBACK_REWRITE[harness] ?? FALLBACK_REWRITE.generic; let out = text.replaceAll("{{tools:agent-coord}}", spec.tools).replace(/\{\{tool:([a-z0-9_]+)\}\}/g, (_m, n) => spec.tool(n)); if (spec.playbookHome)
18
+ out = out.replaceAll("~/.claude/groundwork-playbook", spec.playbookHome); return out; };
19
+ const rewriteCache = new Map();
20
+ async function loadRewrite(root) {
21
+ const hit = rewriteCache.get(root);
22
+ if (hit)
23
+ return hit;
24
+ let out = { fn: fallbackRewrite, source: "console fallback table" };
25
+ const mod = path.join(root, "lib", "packaging.mjs");
26
+ if (existsSync(mod)) {
27
+ try {
28
+ const m = (await import(pathToFileURL(mod).href));
29
+ if (typeof m.rewritePlaceholders === "function")
30
+ out = { fn: m.rewritePlaceholders, source: "kit lib/packaging.mjs" };
31
+ }
32
+ catch { /* fallback */ }
33
+ }
34
+ rewriteCache.set(root, out);
35
+ return out;
36
+ }
7
37
  const sha = (p) => (existsSync(p) ? createHash("sha1").update(readFileSync(p)).digest("hex").slice(0, 10) : null);
38
+ const shaText = (t) => createHash("sha1").update(t).digest("hex").slice(0, 10);
8
39
  const dirs = (p) => (existsSync(p) ? readdirSync(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) : []);
9
40
  const front = (p) => { if (!existsSync(p))
10
41
  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)
@@ -14,13 +45,13 @@ export function groundworkSkills(kitPath, projectPath) {
14
45
  const versionMarker0 = existsSync(path.join(projectPath, "docs/.groundwork/VERSION")) ? readFileSync(path.join(projectPath, "docs/.groundwork/VERSION"), "utf8").trim() : null;
15
46
  if (!root)
16
47
  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");
48
+ const payload = path.join(root, "payload/skills"); // groundwork init/update byte-copies SKILL.md (copyFileSafe) → raw hash comparison is correct here
18
49
  const pathsMjs = path.join(root, "src/lib/paths.mjs");
19
50
  const minimal = existsSync(pathsMjs) ? [...(readFileSync(pathsMjs, "utf8").match(/MINIMAL_SKILLS = \[([\s\S]*?)\]/)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((m) => m[1]) : [];
20
51
  const versionMarker = existsSync(path.join(projectPath, "docs/.groundwork/VERSION")) ? readFileSync(path.join(projectPath, "docs/.groundwork/VERSION"), "utf8").trim() : null;
21
52
  const kitVersion = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version;
22
53
  return {
23
- versionMarker, kitVersion, driftedVersion: !!versionMarker && versionMarker !== kitVersion,
54
+ versionMarker, kitVersion, source: root, driftedVersion: !!versionMarker && versionMarker !== kitVersion,
24
55
  skills: dirs(payload).map((name) => {
25
56
  const src = path.join(payload, name, "SKILL.md");
26
57
  const inst = path.join(projectPath, ".claude/skills", name, "SKILL.md");
@@ -35,19 +66,32 @@ export function groundworkSkills(kitPath, projectPath) {
35
66
  };
36
67
  }
37
68
  /* Global playbook cards live in more than one harness home: Claude → ~/.claude/skills, Grok → ~/.grok/skills (+ ~/.grok/groundwork-playbook). Measure each. */
38
- const HARNESS_HOMES = [["claude", path.join(homedir(), ".claude/skills")], ["grok", path.join(homedir(), ".grok/skills")]];
39
- export function playbookCards(kitPath) {
69
+ // Same home overrides the installer honours (CLAUDE_HOME / GROK_HOME), so measurement and install target the same dirs (and tests can point both at a temp home).
70
+ const HARNESS_HOMES = [["claude", path.join(process.env.CLAUDE_HOME || path.join(homedir(), ".claude"), "skills")], ["grok", path.join(process.env.GROK_HOME || path.join(homedir(), ".grok"), "skills")]];
71
+ /* Harness homes the playbook installer can target (same env overrides as install.mjs). `detectedHarnesses` = those whose home dir exists on this machine —
72
+ passed as --harness= to `update` so grok/cursor/... mirrors are refreshed only where the harness is present (the installer's no-flag default is all five). */
73
+ export const INSTALLER_HOMES = {
74
+ claude: process.env.CLAUDE_HOME || path.join(homedir(), ".claude"), grok: process.env.GROK_HOME || path.join(homedir(), ".grok"), cursor: process.env.CURSOR_HOME || path.join(homedir(), ".cursor"),
75
+ vscode: process.env.VSCODE_HOME || path.join(homedir(), ".vscode"), opencode: process.env.OPENCODE_HOME || path.join(homedir(), ".config/opencode"),
76
+ };
77
+ export const detectedHarnesses = () => Object.entries(INSTALLER_HOMES).filter(([, d]) => existsSync(d)).map(([h]) => h);
78
+ /* Exposed for the fixture test: the fallback table must produce the same bytes as the kit's rewritePlaceholders. */
79
+ export const __rewrite = { fallbackRewrite, loadRewrite };
80
+ export async function playbookCards(kitPath) {
40
81
  const root = packageRoot(kitPath, "playbook");
41
82
  if (!root)
42
83
  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
84
  const srcRoot = path.join(root, "skills");
44
85
  const kitVersion = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version;
45
86
  const homes = HARNESS_HOMES.filter(([, d]) => existsSync(d));
87
+ const rw = await loadRewrite(root);
88
+ // expected installed bytes per harness = installer rewrite of the card (same transform install.mjs applies before writing SKILL.md)
89
+ const expected = (src, harness) => shaText(rw.fn(readFileSync(src, "utf8"), harness));
46
90
  return {
47
- kitVersion, globalDir: HARNESS_HOMES[0][1], homes: homes.map(([h, d]) => ({ harness: h, dir: d })),
91
+ kitVersion, source: root, rewrite: rw.source, globalDir: HARNESS_HOMES[0][1], homes: homes.map(([h, d]) => ({ harness: h, dir: d })),
48
92
  cards: dirs(srcRoot).filter((n) => n !== "references").map((name) => {
49
93
  const src = path.join(srcRoot, name, "SKILL.md");
50
- const per = Object.fromEntries(homes.map(([h, d]) => { const inst = path.join(d, name, "SKILL.md"); const installed = existsSync(inst); const slices = existsSync(path.join(d, name, "references")) ? readdirSync(path.join(d, name, "references")).filter((f) => f.endsWith(".md")) : []; return [h, { installed, inSync: installed ? sha(src) === sha(inst) : null, slices: slices.length, legacyEssay: slices.includes("playbook.md") }]; }));
94
+ const per = Object.fromEntries(homes.map(([h, d]) => { const inst = path.join(d, name, "SKILL.md"); const installed = existsSync(inst); const slices = existsSync(path.join(d, name, "references")) ? readdirSync(path.join(d, name, "references")).filter((f) => f.endsWith(".md")) : []; return [h, { installed, inSync: installed ? expected(src, h) === sha(inst) : null, slices: slices.length, legacyEssay: slices.includes("playbook.md") }]; }));
51
95
  const claude = per.claude ?? { installed: false, inSync: null, slices: 0, legacyEssay: false };
52
96
  return { name, installed: claude.installed, inSync: claude.inSync, slices: claude.slices, legacyEssay: claude.legacyEssay, homes: per, description: front(src).description ?? "", mirrors: { opencode: existsSync(path.join(homedir(), ".config/opencode/skills", name)), cursor: existsSync(path.join(homedir(), ".cursor/commands", `${name}.md`)), vscode: existsSync(path.join(homedir(), ".vscode/prompts", `${name}.prompt.md`)) } };
53
97
  }),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@davidbalzan/groundwork-console",
3
3
  "private": false,
4
- "version": "0.1.6",
4
+ "version": "0.1.8",
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "@davidbalzan/groundwork-seam": "^0.1.0",
@@ -44,6 +44,9 @@
44
44
  "build:web": "pnpm --filter @groundwork-console/web build && rm -rf public && cp -R ../web/dist public",
45
45
  "typecheck": "tsc -p tsconfig.json --noEmit",
46
46
  "start": "node bin/groundwork-console.mjs",
47
- "publish-check": "node scripts/publish-check.mjs"
47
+ "publish-check": "node scripts/publish-check.mjs",
48
+ "test:inject": "tsx test/inject.test.mts",
49
+ "test:skills": "tsx test/skills.test.mts",
50
+ "test": "pnpm run test:inject && pnpm run test:skills"
48
51
  }
49
52
  }
@@ -1 +1 @@
1
- import{m as e,t}from"./mermaid-parser.core-B4KSdud0.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-B2eptEoc.js";import"./chunk-I66GZJ75-CCxcXl9W.js";import{n as i,r as a,t as o}from"./chunk-6Q2QTUOP-BXxDc17T.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-CSBAT3ig.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-BXY3JF8d.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-D5IewQLB.js";import"./chunk-I66GZJ75-C97VIOWu.js";import{n as i,r as a,t as o}from"./chunk-6Q2QTUOP-nWUr6zuz.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-DqjaFy6t.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-DFkPtM9v.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-DoiwlY9_.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-BXY3JF8d.js";export{e as createArchitectureServices};