@davidbalzan/groundwork-console 0.1.5 → 0.1.7

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 (107) hide show
  1. package/README.md +3 -3
  2. package/bin/groundwork-console.mjs +21 -2
  3. package/dist/answered.js +16 -34
  4. package/dist/bus.js +11 -4
  5. package/dist/kitresolve.js +3 -2
  6. package/dist/mcp.js +2 -1
  7. package/dist/provision.js +94 -4
  8. package/dist/runs.js +34 -2
  9. package/dist/server.js +149 -41
  10. package/dist/skills.js +51 -7
  11. package/dist/tmux.js +32 -0
  12. package/dist/uploads.js +31 -1
  13. package/package.json +5 -2
  14. package/public/assets/{abnfDiagram-N423BO3Z-eACBdXEp.js → abnfDiagram-N423BO3Z-B0nns4cA.js} +1 -1
  15. package/public/assets/{arc-C8zPWxct.js → arc-DsDgWIU7.js} +1 -1
  16. package/public/assets/architecture-TIHT7OUA-okwBKksv.js +1 -0
  17. package/public/assets/{architectureDiagram-T3A2C74G-BZqtGnsC.js → architectureDiagram-T3A2C74G-Bqo7zIaB.js} +1 -1
  18. package/public/assets/{blockDiagram-VBNYF7ZC-D265Ytun.js → blockDiagram-VBNYF7ZC-CvNPqlbt.js} +1 -1
  19. package/public/assets/{c4Diagram-5PPSVZJV-CodzUBEt.js → c4Diagram-5PPSVZJV-BqjkNgv6.js} +1 -1
  20. package/public/assets/channel-CI1-SNMv.js +1 -0
  21. package/public/assets/{chunk-2GRJ4B5K-D59ECeXb.js → chunk-2GRJ4B5K-CWJJzbol.js} +1 -1
  22. package/public/assets/{chunk-4I5QYGJK-DlE6ad0r.js → chunk-4I5QYGJK-CuHGN-lT.js} +1 -1
  23. package/public/assets/{chunk-5RXB4S5H-Bw4SBOdN.js → chunk-5RXB4S5H-Cl92nmIs.js} +1 -1
  24. package/public/assets/{chunk-6Q2QTUOP-BD4S6iuB.js → chunk-6Q2QTUOP-C3vYg7rX.js} +1 -1
  25. package/public/assets/{chunk-7Z6QIM7H-DvwJZM4T.js → chunk-7Z6QIM7H-jkpbvAhi.js} +1 -1
  26. package/public/assets/{chunk-GF5L2VYU-BlzSHpI6.js → chunk-GF5L2VYU-Bo-zB54d.js} +1 -1
  27. package/public/assets/{chunk-I66GZJ75-DJurmtZ9.js → chunk-I66GZJ75-BD9h95Vq.js} +1 -1
  28. package/public/assets/{chunk-JQJVKLGR-t20LaGIq.js → chunk-JQJVKLGR-C6TRkyf7.js} +1 -1
  29. package/public/assets/{chunk-KBJHAD2P-Dgjyd4B5.js → chunk-KBJHAD2P-RgoL4Nto.js} +1 -1
  30. package/public/assets/{chunk-NSK5VX7P-Chchkdi8.js → chunk-NSK5VX7P-C6TsGnGA.js} +1 -1
  31. package/public/assets/{chunk-QR6OTTB3-CJK67frw.js → chunk-QR6OTTB3-Bq5akaxk.js} +1 -1
  32. package/public/assets/{chunk-RYQCIY6F-sGSUyT6_.js → chunk-RYQCIY6F-DtVCesJ4.js} +1 -1
  33. package/public/assets/{chunk-UBXNYLIW-CEzUmAZt.js → chunk-UBXNYLIW-BjnaZZH0.js} +1 -1
  34. package/public/assets/{chunk-W5SLKNZC-Spmh0y1r.js → chunk-W5SLKNZC-D1KW-lQn.js} +1 -1
  35. package/public/assets/{chunk-WRU74C26-DEsyhzZw.js → chunk-WRU74C26-YZFoWy6X.js} +1 -1
  36. package/public/assets/{chunk-XXDRQBXY-Bo1s3Uh5.js → chunk-XXDRQBXY-CyrI_Mox.js} +1 -1
  37. package/public/assets/classDiagram-JCYQIIEL-zdEoGtpz.js +1 -0
  38. package/public/assets/classDiagram-v2-OCEON4UE-zdEoGtpz.js +1 -0
  39. package/public/assets/{cose-bilkent-JH36ORCC-15pcvj-n.js → cose-bilkent-JH36ORCC-v6lDOu7m.js} +1 -1
  40. package/public/assets/{cynefin-VYW2F7L2-BZxWqC-g.js → cynefin-VYW2F7L2-BtzRQs_J.js} +1 -1
  41. package/public/assets/{cynefinDiagram-MW4NZA55-B_czkRTa.js → cynefinDiagram-MW4NZA55--R8S0KoS.js} +1 -1
  42. package/public/assets/{dagre-VZM6K2ZE-D8bvaK5F.js → dagre-VZM6K2ZE-D6mRFOfB.js} +1 -1
  43. package/public/assets/{diagram-7IWD3JNH-_vpWqUu0.js → diagram-7IWD3JNH-kfCoRMpW.js} +1 -1
  44. package/public/assets/{diagram-B4RE2ZJO-DPfWeSE2.js → diagram-B4RE2ZJO-sxUW6rzT.js} +1 -1
  45. package/public/assets/{diagram-LBJQPF4R-Cb7ustRg.js → diagram-LBJQPF4R-B926bgCA.js} +1 -1
  46. package/public/assets/{diagram-Q27KOJAE-CRIHo9aN.js → diagram-Q27KOJAE-CcVi6njJ.js} +1 -1
  47. package/public/assets/{diagram-UB23O5K3-CMyiQouk.js → diagram-UB23O5K3-BtW4XOuK.js} +1 -1
  48. package/public/assets/{dist-DVdstvf6.js → dist-BnkmMC7M.js} +1 -1
  49. package/public/assets/{ebnfDiagram-BXEA7PRR-UURYMfYm.js → ebnfDiagram-BXEA7PRR-BNGfWHYj.js} +1 -1
  50. package/public/assets/{erDiagram-JOGREHBK-Dio32huH.js → erDiagram-JOGREHBK-BGrhe741.js} +1 -1
  51. package/public/assets/eventmodeling-45OFAUF4-Cn6RDMEG.js +1 -0
  52. package/public/assets/flowDiagram-UKHOOZJN-DOMsEKL1.js +1 -0
  53. package/public/assets/{ganttDiagram-PKOTCBZU-62etRb4w.js → ganttDiagram-PKOTCBZU-CR5BNdXR.js} +1 -1
  54. package/public/assets/{gitGraph-TEB2WS4Q-CfIUb3Fs.js → gitGraph-TEB2WS4Q-CfvC-iZD.js} +1 -1
  55. package/public/assets/{gitGraphDiagram-DS77QQ5N-DuwZInBC.js → gitGraphDiagram-DS77QQ5N-D45EWdll.js} +1 -1
  56. package/public/assets/index-CxNe802C.css +2 -0
  57. package/public/assets/index-yHI4kvM-.js +41 -0
  58. package/public/assets/{info-DKCQHKI2-BfXJHEd1.js → info-DKCQHKI2-stahmZKW.js} +1 -1
  59. package/public/assets/{infoDiagram-6WML65LV-Bmh_7gih.js → infoDiagram-6WML65LV-nWu8ujif.js} +1 -1
  60. package/public/assets/{ishikawaDiagram-WSZJBQD7-i8-L2rAe.js → ishikawaDiagram-WSZJBQD7-Ccwzv-xZ.js} +1 -1
  61. package/public/assets/{journeyDiagram-NVQOT4AX-BtfEqljI.js → journeyDiagram-NVQOT4AX-R7gZw1Qw.js} +1 -1
  62. package/public/assets/{kanban-definition-27J2QSJJ-pH6QumbR.js → kanban-definition-27J2QSJJ-By8aTzkI.js} +1 -1
  63. package/public/assets/{line-DsGf4Aam.js → line-zJXbSVxf.js} +1 -1
  64. package/public/assets/{linear-BNSu_43k.js → linear-B8U-jWnO.js} +1 -1
  65. package/public/assets/{mermaid-parser.core-679COn57.js → mermaid-parser.core-B1iqIz20.js} +3 -3
  66. package/public/assets/{mermaid.core-D_f4AlrR.js → mermaid.core-CuFK5CuK.js} +4 -4
  67. package/public/assets/{mindmap-definition-FAOFIHXS-D1tM4SRn.js → mindmap-definition-FAOFIHXS-qJ2DuBku.js} +1 -1
  68. package/public/assets/{packet-7NZHBO7P-olELdMgx.js → packet-7NZHBO7P-CzQJdD0e.js} +1 -1
  69. package/public/assets/{pegDiagram-VL7TDLO6-DgAndH0A.js → pegDiagram-VL7TDLO6-CKmHw89b.js} +1 -1
  70. package/public/assets/{pie-RZYD4A2V-DWj8tY38.js → pie-RZYD4A2V-Dt3CA16n.js} +1 -1
  71. package/public/assets/{pieDiagram-7S7Q4E2Y-DK7c2EvE.js → pieDiagram-7S7Q4E2Y-D9vwefN9.js} +1 -1
  72. package/public/assets/{quadrantDiagram-CIZ2JOQS-DI-ZbPxK.js → quadrantDiagram-CIZ2JOQS-jtSAmOcp.js} +1 -1
  73. package/public/assets/{radar-I7S5WNFK-DLsdeAbl.js → radar-I7S5WNFK-BtDfUAqO.js} +1 -1
  74. package/public/assets/{railroad-3IZDKUUU-BFUnqJxV.js → railroad-3IZDKUUU-D5DNw8qB.js} +1 -1
  75. package/public/assets/railroad-abnf-AHOZXSZD-DuJUfLMt.js +1 -0
  76. package/public/assets/railroad-ebnf-EBAXGLYW-CiLIN42p.js +1 -0
  77. package/public/assets/railroad-peg-LSFZ7HO6-DKtIAsvs.js +1 -0
  78. package/public/assets/{railroadDiagram-AXF67PYL-BcCf-qnq.js → railroadDiagram-AXF67PYL-C-S9VT4Z.js} +1 -1
  79. package/public/assets/{requirementDiagram-LRYGKXZP-CSERzrsB.js → requirementDiagram-LRYGKXZP-Bmv6EbmT.js} +1 -1
  80. package/public/assets/{sankeyDiagram-W5VNT64P-BTHdyTAB.js → sankeyDiagram-W5VNT64P-Cm2dLWH9.js} +1 -1
  81. package/public/assets/{sequenceDiagram-SI44F4Z6-DCMbcZQ-.js → sequenceDiagram-SI44F4Z6-DPslkAXf.js} +1 -1
  82. package/public/assets/{src-CWXJqyrc.js → src-l8EnJwXz.js} +1 -1
  83. package/public/assets/{stateDiagram-OKZ733FA-BhzI_RZ_.js → stateDiagram-OKZ733FA-BBXMNQQ7.js} +1 -1
  84. package/public/assets/stateDiagram-v2-UEYNNEHI-BxOobbWc.js +1 -0
  85. package/public/assets/{swimlanes-SLNWSIFB-DPKSeErn.js → swimlanes-SLNWSIFB-BeiwtQg-.js} +1 -1
  86. package/public/assets/swimlanesDiagram-ULZ7WXOC-BEW9yZ9H.js +8 -0
  87. package/public/assets/{timeline-definition-Z64GVDOM-BnLvIO3-.js → timeline-definition-Z64GVDOM-Drd7Un_g.js} +1 -1
  88. package/public/assets/{treeView-QDETBFTQ-BgFXBEUi.js → treeView-QDETBFTQ-BT2KyIoB.js} +1 -1
  89. package/public/assets/{treemap-6X3UGDF4-KzlTv1ih.js → treemap-6X3UGDF4-gdZw2F6X.js} +1 -1
  90. package/public/assets/{vennDiagram-T6HMQDX7-gsHmzjh9.js → vennDiagram-T6HMQDX7-Bbk8OMR0.js} +1 -1
  91. package/public/assets/{wardley-OPB4EBWU-DyQkbq8g.js → wardley-OPB4EBWU-BI0xutNp.js} +1 -1
  92. package/public/assets/{wardleyDiagram-T6FBY63Y-DqwaVwnv.js → wardleyDiagram-T6FBY63Y-DQvEcd8r.js} +1 -1
  93. package/public/assets/{xychartDiagram-ELKLHX3M-DGZxfApD.js → xychartDiagram-ELKLHX3M-CRWoUpzf.js} +1 -1
  94. package/public/index.html +2 -2
  95. package/public/assets/architecture-TIHT7OUA-CtVIKkxG.js +0 -1
  96. package/public/assets/channel-CpTfWcC2.js +0 -1
  97. package/public/assets/classDiagram-JCYQIIEL-BSB0boKu.js +0 -1
  98. package/public/assets/classDiagram-v2-OCEON4UE-BSB0boKu.js +0 -1
  99. package/public/assets/eventmodeling-45OFAUF4-DP5rNNNm.js +0 -1
  100. package/public/assets/flowDiagram-UKHOOZJN-DjrqEV81.js +0 -1
  101. package/public/assets/index-ZvSJKDLe.css +0 -2
  102. package/public/assets/index-iKIFRUDL.js +0 -40
  103. package/public/assets/railroad-abnf-AHOZXSZD-B00TU1b8.js +0 -1
  104. package/public/assets/railroad-ebnf-EBAXGLYW-RrXgWJqj.js +0 -1
  105. package/public/assets/railroad-peg-LSFZ7HO6-CsiNSgst.js +0 -1
  106. package/public/assets/stateDiagram-v2-UEYNNEHI-DKG0b0eo.js +0 -1
  107. package/public/assets/swimlanesDiagram-ULZ7WXOC-D6CPDB9k.js +0 -8
package/dist/server.js CHANGED
@@ -16,7 +16,7 @@ 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";
@@ -26,11 +26,11 @@ 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
- import { saveUpload, uploadPath, UPLOAD_DIR } from "./uploads.js";
30
- import { answered, markAnswered, isAnswered, setCoordDir } from "./answered.js";
29
+ import { saveUpload, uploadPath, purgeUploads, UPLOAD_DIR, UPLOAD_TTL_HOURS } from "./uploads.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,15 @@ 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 = () => ({ inReplyTo: bus.connected ? bus.supports("send_message", "inReplyTo") : busDavid.connected ? busDavid.supports("send_message", "inReplyTo") : null });
46
50
  const PUBLIC_DIR = path.resolve(HERE, "../public");
47
51
  const root = new Hono();
48
52
  const app = new Hono().basePath("/api");
@@ -57,7 +61,7 @@ app.post("/auth/logout", (c) => { logout(c); return c.json({ ok: true }); });
57
61
  app.get("/auth/login", (c) => { const t = c.req.query("token") ?? ""; if (!login(c, t))
58
62
  return c.json({ error: "wrong token" }, 401); return c.redirect(c.req.query("next") ?? "/"); });
59
63
  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$/, "") }));
64
+ 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
65
  app.get("/admin/bus-writes", (c) => c.json(busWrites));
62
66
  app.get("/admin/seam-writes", (c) => c.json(seamWrites));
63
67
  app.post("/admin/seam-writes", async (c) => {
@@ -109,6 +113,23 @@ app.post("/actions/send", async (c) => {
109
113
  return c.json({ error: "text required" }, 400);
110
114
  if (!b.room && !b.to)
111
115
  return c.json({ error: "room or to required" }, 400);
116
+ // Raw control commands: exactly `/clear` or `/compact` (no args) to ONE agent go through MCP send_command — the only path that types a slash command
117
+ // into a live CLI (allowlist locked to these two in the MCP; widening it — /reload-skills etc. — is a kit decision, not done here). Everything else is a text DM.
118
+ const RAW_COMMANDS = new Set(["/clear", "/compact"]);
119
+ const cmd = b.text.trim();
120
+ if (RAW_COMMANDS.has(cmd)) {
121
+ if (!b.to)
122
+ return c.json({ error: `${cmd} is unicast — pick an agent (rooms get text only)` }, 400);
123
+ const r = await runs.exec({ kind: "bus", label: `command ${cmd} → ${b.to}`, sideEffects: true, args: { as: who, to: b.to, command: cmd } }, async (log) => {
124
+ const out = (await clientFor(who).call("send_command", { to: b.to, command: cmd, waitForDelivery: true, deliveryTimeoutMs: 15_000 }, log));
125
+ log(`delivery: ${out.delivery ?? "?"}${out.reason ? ` · ${out.reason}` : ""}${out.note ? ` · ${out.note}` : ""}`);
126
+ if (out.error)
127
+ throw new Error(out.error);
128
+ return out;
129
+ });
130
+ const out = (r.result ?? {});
131
+ 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);
132
+ }
112
133
  const PREFIX_RE = /^(BLOCKER|RISK|DONE|FYI|AGENT_ACTION|GO|SCOPE CHANGE|VERDICT|DAVID_DECISION)\b/;
113
134
  if (b.room && !PREFIX_RE.test(b.text))
114
135
  return c.json({ error: "room posts need a prefix (FYI / AGENT_ACTION / DAVID_DECISION / BLOCKER / RISK / DONE / GO / SCOPE CHANGE / VERDICT)" }, 400);
@@ -116,29 +137,30 @@ app.post("/actions/send", async (c) => {
116
137
  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
138
  const cl = clientFor(who);
118
139
  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 */ }
140
+ // Reply linkage = bus inReplyTo only (console-owned answered.json retired 2026-08-19). Parent: explicit from the UI (answers[0] / inReplyTo);
141
+ // 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.
142
+ let parent = b.inReplyTo ?? b.answers?.[0];
143
+ if (!parent && who === "david" && b.to) {
144
+ const f = path.join(cfg.coordDir, "inbox", "david.jsonl");
145
+ if (existsSync(f))
146
+ for (const l of readFileSync(f, "utf8").split("\n").filter(Boolean)) {
147
+ try {
148
+ const m = JSON.parse(l);
149
+ if (m.from === b.to && typeof m.text === "string" && m.text.startsWith("DAVID_DECISION") && m.id && !isAnswered(m.id))
150
+ parent = m.id;
137
151
  }
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)`);
152
+ catch { /* skip */ }
153
+ }
154
+ if (parent)
155
+ log(`inReplyTo latest open DAVID_DECISION from ${b.to} (${parent})`);
156
+ }
157
+ const supportsReply = cl.supports("send_message", "inReplyTo");
158
+ if (parent && !supportsReply)
159
+ log("⚠ connected agent-coord-mcp has no inReplyTo — reply will NOT be linked to the packet; upgrade agent-coord-mcp (gwc doctor)");
160
+ 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);
161
+ if (parent && supportsReply) {
162
+ invalidateAnswered();
163
+ log(`linked: inReplyTo ${parent}`);
142
164
  }
143
165
  return out;
144
166
  });
@@ -291,11 +313,11 @@ app.get("/projects/:name/skills", async (c) => {
291
313
  return c.json({ error: "unknown project" }, 404);
292
314
  const [gl, pl] = await Promise.all([list(cfg.kitPath, p.path), playbookList(cfg.kitPath)]);
293
315
  const sync = groundworkSkills(cfg.kitPath, p.path);
294
- const cards = playbookCards(cfg.kitPath);
316
+ const cards = await playbookCards(cfg.kitPath);
295
317
  return c.json({
296
- groundwork: { versionMarker: gl.version || sync.versionMarker, kitVersion: sync.kitVersion, driftedVersion: (gl.version || sync.versionMarker) !== sync.kitVersion, error: gl.error,
318
+ groundwork: { versionMarker: gl.version || sync.versionMarker, kitVersion: sync.kitVersion, source: sync.source ?? null, driftedVersion: (gl.version || sync.versionMarker) !== sync.kitVersion, error: gl.error,
297
319
  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,
320
+ 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
321
  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
322
  });
301
323
  });
@@ -485,8 +507,6 @@ app.get("/bus/inbox", async (c) => {
485
507
  });
486
508
  /* DM thread with one agent, both directions: their DMs to david (inbox/david.jsonl) + david's DMs to them (inbox/<agent>.jsonl).
487
509
  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
510
  app.get("/bus/dm/:agent", (c) => {
491
511
  const agent = c.req.param("agent");
492
512
  const limit = Number(c.req.query("limit") ?? 80);
@@ -572,14 +592,18 @@ app.post("/actions/playbook-update", async (c) => {
572
592
  const cli = playbookCli(cfg.kitPath);
573
593
  if (!cli)
574
594
  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) => {
595
+ // --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).
596
+ const harnesses = detectedHarnesses();
597
+ if (!harnesses.length)
598
+ return c.json({ error: "no harness home found (~/.claude, ~/.grok, ~/.cursor, ~/.vscode, ~/.config/opencode)" }, 400);
599
+ const argv = [cli, "update", `--harness=${harnesses.join(",")}`, ...(dry ? ["--dry-run"] : [])];
600
+ 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
601
  log(`$ node ${argv.map((a) => a.replace(cfg.kitPath, "<kit>")).join(" ")}`);
578
602
  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
603
  const lines = (out.stdout + out.stderr).replace(/\x1b\[[0-9;]*m/g, "").split("\n").filter(Boolean);
580
604
  for (const l of lines.slice(0, 120))
581
605
  log(l);
582
- return { lines: lines.length, dryRun: dry };
606
+ return { lines: lines.length, dryRun: dry, harnesses };
583
607
  });
584
608
  return c.json(r, r.status === "ok" ? 200 : 500);
585
609
  });
@@ -703,6 +727,32 @@ app.post("/actions/phase/:name/:phase/:verb", async (c) => {
703
727
  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));
704
728
  return c.json(r, r.status === "ok" ? 200 : 403);
705
729
  });
730
+ /* ---- Per-task "mark done": ask the task's owner (stream owner → bus agent; fallback aide) to run /check-task. The console never ticks PHASE*_TASKS.md itself. ---- */
731
+ app.post("/actions/task/:name/:phase/:line", async (c) => {
732
+ const p = project(c.req.param("name"));
733
+ if (!p)
734
+ return c.json({ error: "unknown project" }, 404);
735
+ const phase = c.req.param("phase");
736
+ const line = Number(c.req.param("line"));
737
+ const b = (await c.req.json().catch(() => ({})));
738
+ const t = phaseTasksOf(path.join(p.path, "docs"), phase);
739
+ if (!t?.file)
740
+ return c.json({ error: "no tasks file for that phase" }, 404);
741
+ const sec = t.sections.find((sc) => sc.tasks.some((x) => x.line === line));
742
+ const task = sec?.tasks.find((x) => x.line === line);
743
+ if (!task)
744
+ return c.json({ error: "no task at that line" }, 404);
745
+ if (task.done)
746
+ return c.json({ error: "already ticked" }, 409);
747
+ const live = agents(cfg.coordDir).filter((a) => a.liveness !== "dead");
748
+ const owner = b.owner && live.some((a) => a.agentId === b.owner) ? b.owner : roleAgent(p.name, p.room, /aide|liaison|curator/i);
749
+ if (!owner)
750
+ return c.json({ error: `no live owner or aide for ${p.name}` }, 409);
751
+ const n = phase.match(/(\d+(?:\.\d+)?)/)?.[1] ?? phase;
752
+ const text = `AGENT_ACTION: mark done in ${p.name} — run /check-task ${n} "${task.text.slice(0, 120)}" (${t.file}:${line}, section "${sec.title}") — tick it in the tasks file and update the board row; reply DONE with the line.${b.note ? " Note: " + b.note : ""}`;
753
+ const r = await runs.exec({ kind: "bus", label: `mark done ${phase}:${line} → ${owner}`, project: p.name, sideEffects: true, args: { to: owner, phase, line, task: task.text.slice(0, 120) } }, (log) => busDavid.call("send_message", { to: owner, text }, log));
754
+ return c.json({ ...r, owner }, r.status === "ok" ? 200 : 403);
755
+ });
706
756
  /* ---- Project-level onboarding actions (as david → the aide): create-prd conversation starter, /kickstart ---- */
707
757
  app.post("/actions/project/:name/:verb", async (c) => {
708
758
  const p = project(c.req.param("name"));
@@ -810,12 +860,49 @@ app.get("/runs/stream", (c) => {
810
860
  });
811
861
  return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" } });
812
862
  });
813
- /* Actions (allow-listed; each becomes a Run). Read-only build: only read-only MCP tools + read-only CLIs. */
863
+ 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 });
814
864
  app.post("/actions/bus-doctor", async (c) => {
815
865
  const body = await c.req.json().catch(() => ({}));
816
- if (body.fix)
817
- return c.json({ error: "fix=true is a write; disabled in read-only build" }, 403);
818
- const r = await runs.exec({ kind: "mcp", label: "bus doctor", sideEffects: false, args: { tool: "doctor", agentId: AGENT_ID } }, (log) => bus.call("doctor", {}, log));
866
+ if (!body.fix) {
867
+ const r = await runs.exec({ kind: "mcp", label: "bus doctor", sideEffects: false, args: { tool: "doctor", agentId: AGENT_ID } }, (log) => bus.call("doctor", {}, log));
868
+ return c.json(r, r.status === "ok" ? 200 : 500);
869
+ }
870
+ // 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.
871
+ if (!busWrites.enabled)
872
+ return c.json({ error: "bus writes are off — switch them on in the header (WRITES), then Fix fixable" }, 403);
873
+ const r = await runs.exec({ kind: "mcp", label: "bus doctor · fix", sideEffects: true, args: { tool: "doctor", fix: true, agentId: AGENT_ID } }, async (log) => {
874
+ const before = (await bus.call("doctor", {}, log));
875
+ const b = doctorCounts(before);
876
+ log(`before: ${b.warn} warn · ${b.error} error · ${b.fixable} fixable`);
877
+ const fixed = (await bus.call("doctor", { fix: true }, log));
878
+ log(`fix: ${fixed.fixApplied ? "applied" : "nothing applied"}`);
879
+ const after = (await bus.call("doctor", {}, log));
880
+ const a = doctorCounts(after);
881
+ 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})`);
882
+ 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 } };
883
+ });
884
+ return c.json(r, r.status === "ok" ? 200 : 500);
885
+ });
886
+ /* 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. */
887
+ app.post("/actions/rebuild-kit-mcp", async (c) => {
888
+ if (!busWrites.enabled)
889
+ return c.json({ error: "bus writes are off — switch them on in the header (WRITES) first" }, 403);
890
+ const b = (await c.req.json().catch(() => ({})));
891
+ if (b.confirm !== "BUILD")
892
+ return c.json({ error: "confirm must be BUILD" }, 400);
893
+ if (!hasKitCheckout(cfg.kitPath))
894
+ 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);
895
+ const argv = ["pnpm", "--filter", "agent-coord-mcp", "build"];
896
+ 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) => {
897
+ log(`$ ${argv.join(" ")} (cwd ${cfg.kitPath})`);
898
+ 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 }; });
899
+ for (const l of (out.stdout + out.stderr).replace(/\x1b\[[0-9;]*m/g, "").split("\n").filter(Boolean).slice(-40))
900
+ log(l);
901
+ if (out.failed)
902
+ throw new Error("build failed — see log");
903
+ 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.");
904
+ return { ok: true, cwd: cfg.kitPath };
905
+ });
819
906
  return c.json(r, r.status === "ok" ? 200 : 500);
820
907
  });
821
908
  app.post("/actions/project-doctor/:name", async (c) => {
@@ -867,11 +954,32 @@ app.get("/bus/status", (c) => c.json(statusStream(cfg.coordDir, Number(c.req.que
867
954
  app.get("/bus/scopes", (c) => c.json(scopes(cfg.coordDir)));
868
955
  events.start(15_000);
869
956
  notifier.start(15_000);
957
+ /* Uploads TTL purge: at boot + every 30 min (unref'd so it never holds the process). A Run is logged only when something was deleted. */
958
+ const runPurge = () => {
959
+ try {
960
+ const r = purgeUploads();
961
+ if (r.deleted.length)
962
+ void runs.exec({ kind: "cli", label: `uploads purge · ${r.deleted.length} deleted`, sideEffects: true, args: { ttlHours: UPLOAD_TTL_HOURS, deleted: r.deleted.length, kept: r.kept, errors: r.errors } }, async (log) => { log(`ttl ${UPLOAD_TTL_HOURS} h · dir ${UPLOAD_DIR}`); for (const n of r.deleted.slice(0, 50))
963
+ log(`deleted ${n}`); if (r.deleted.length > 50)
964
+ log(`… ${r.deleted.length - 50} more`); if (r.errors)
965
+ log(`${r.errors} file(s) could not be removed`); return r; });
966
+ }
967
+ catch (e) {
968
+ console.warn(`uploads purge failed: ${e.message}`);
969
+ }
970
+ };
971
+ runPurge();
972
+ setInterval(runPurge, 30 * 60_000).unref();
870
973
  /* 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. */
871
974
  root.route("/", app);
872
975
  if (existsSync(PUBLIC_DIR)) {
873
- root.use("/*", serveStatic({ root: PUBLIC_DIR }));
874
- root.get("*", (c) => c.html(readFileSync(path.join(PUBLIC_DIR, "index.html"), "utf8"))); // hash router index for everything else
976
+ // 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);
977
+ // hashed Vite assets under /assets/* are immutable for a year; everything else (fonts, icons) a day.
978
+ 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")); };
979
+ root.get("/", shell);
980
+ root.get("/index.html", shell);
981
+ 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"); } }));
982
+ root.get("*", shell); // hash router → index for everything else
875
983
  }
876
984
  // 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).
877
985
  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/dist/tmux.js CHANGED
@@ -17,6 +17,38 @@ export async function listPanes(ttlMs = 3_000) {
17
17
  }
18
18
  export async function paneActivity(paneId) { if (!paneId)
19
19
  return null; return (await listPanes()).find((p) => p.paneId === paneId || p.target === paneId) ?? null; }
20
+ /* Content activity: `window_activity` ticks on ANY output, including the harness status/timer strip redrawing while idle — that made tiles pulse with nobody working.
21
+ contentTs advances only when the pane content ABOVE the bottom K lines changes (K=3 = the Claude Code status/prompt strip). One capture per pane per poll
22
+ (N=120 lines, ≤ once per CONTENT_TTL, dead panes skipped), hashed and compared per pane. Falls back to window_activity when capture fails. Read-only. */
23
+ const CONTENT_TTL = 3_000, CONTENT_LINES = 120, STRIP_LINES = 3;
24
+ const contentCache = new Map();
25
+ const djb2 = (s) => { let h = 5381; for (let i = 0; i < s.length; i++)
26
+ h = ((h << 5) + h + s.charCodeAt(i)) | 0; return (h >>> 0).toString(36); };
27
+ export async function contentActivity(paneId) {
28
+ const c = contentCache.get(paneId);
29
+ const now = Date.now();
30
+ if (c && now - c.at < CONTENT_TTL)
31
+ return c.ts;
32
+ try {
33
+ const { stdout } = await run("tmux", ["capture-pane", "-p", "-t", paneId, "-S", `-${CONTENT_LINES}`], { timeout: 2_000, maxBuffer: 1 << 20 });
34
+ const lines = stdout.replace(/\s+$/gm, "").split("\n");
35
+ while (lines.length && !lines[lines.length - 1])
36
+ lines.pop();
37
+ const body = lines.slice(0, Math.max(0, lines.length - STRIP_LINES)).join("\n");
38
+ const hash = djb2(body);
39
+ // First sample seeds ts=0 (unknown): we never claim activity we have not observed; the first CHANGE sets it.
40
+ const ts = c ? (c.hash === hash ? c.ts : now) : 0;
41
+ contentCache.set(paneId, { at: now, hash, ts });
42
+ if (contentCache.size > 200)
43
+ for (const [k, v] of contentCache)
44
+ if (now - v.at > 600_000)
45
+ contentCache.delete(k);
46
+ return ts;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
20
52
  const ANSI = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
21
53
  export async function capturePane(paneId, lines = 200, colors = false) {
22
54
  const panes = await listPanes(0);
package/dist/uploads.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, lstatSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
  /* Pasted/dropped images for DMs. Console-owned store: ~/.groundwork-console/uploads/<date>-<sha8>.<ext>.
@@ -38,3 +38,33 @@ export function uploadPath(name) {
38
38
  const mime = Object.entries(EXT).find(([, e]) => e === ext || (e === "jpg" && ext === "jpeg"))?.[0] ?? "application/octet-stream";
39
39
  return { path: p, mime, bytes: statSync(p).size };
40
40
  }
41
+ /* TTL purge (David 2026-08-19): screenshots are transient — the message text keeps the path and the agent has already read it.
42
+ Deletes regular files in UPLOAD_DIR matching the upload name pattern whose mtime is older than GROUNDWORK_CONSOLE_UPLOAD_TTL_HOURS (default 6; 0/invalid → default).
43
+ No subdirs, no symlink follow, per-file errors counted not thrown. Run at boot + every 30 min (see server.ts). */
44
+ export const UPLOAD_TTL_HOURS = (() => { const n = Number(process.env.GROUNDWORK_CONSOLE_UPLOAD_TTL_HOURS); return Number.isFinite(n) && n > 0 ? n : 6; })();
45
+ export function purgeUploads(ttlHours = UPLOAD_TTL_HOURS) {
46
+ const out = { deleted: [], errors: 0, kept: 0 };
47
+ if (!existsSync(UPLOAD_DIR))
48
+ return out;
49
+ const cutoff = Date.now() - ttlHours * 3600_000;
50
+ for (const name of readdirSync(UPLOAD_DIR)) {
51
+ if (!NAME_RE.test(name))
52
+ continue;
53
+ const p = path.join(UPLOAD_DIR, name);
54
+ try {
55
+ const st = lstatSync(p);
56
+ if (!st.isFile())
57
+ continue;
58
+ if (st.mtimeMs < cutoff) {
59
+ unlinkSync(p);
60
+ out.deleted.push(name);
61
+ }
62
+ else
63
+ out.kept++;
64
+ }
65
+ catch {
66
+ out.errors++;
67
+ }
68
+ }
69
+ return out;
70
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@davidbalzan/groundwork-console",
3
3
  "private": false,
4
- "version": "0.1.5",
4
+ "version": "0.1.7",
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-679COn57.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-CWXJqyrc.js";import"./chunk-I66GZJ75-DJurmtZ9.js";import{n as i,r as a,t as o}from"./chunk-6Q2QTUOP-BD4S6iuB.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-D_f4AlrR.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-B1iqIz20.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-l8EnJwXz.js";import"./chunk-I66GZJ75-BD9h95Vq.js";import{n as i,r as a,t as o}from"./chunk-6Q2QTUOP-C3vYg7rX.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-CuFK5CuK.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-DVdstvf6.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-BnkmMC7M.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-B1iqIz20.js";export{e as createArchitectureServices};