@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.
- package/README.md +3 -3
- package/bin/groundwork-console.mjs +21 -2
- package/dist/answered.js +16 -34
- package/dist/bus.js +11 -4
- package/dist/kitresolve.js +3 -2
- package/dist/mcp.js +2 -1
- package/dist/provision.js +94 -4
- package/dist/runs.js +34 -2
- package/dist/server.js +149 -41
- package/dist/skills.js +51 -7
- package/dist/tmux.js +32 -0
- package/dist/uploads.js +31 -1
- package/package.json +5 -2
- package/public/assets/{abnfDiagram-N423BO3Z-eACBdXEp.js → abnfDiagram-N423BO3Z-B0nns4cA.js} +1 -1
- package/public/assets/{arc-C8zPWxct.js → arc-DsDgWIU7.js} +1 -1
- package/public/assets/architecture-TIHT7OUA-okwBKksv.js +1 -0
- package/public/assets/{architectureDiagram-T3A2C74G-BZqtGnsC.js → architectureDiagram-T3A2C74G-Bqo7zIaB.js} +1 -1
- package/public/assets/{blockDiagram-VBNYF7ZC-D265Ytun.js → blockDiagram-VBNYF7ZC-CvNPqlbt.js} +1 -1
- package/public/assets/{c4Diagram-5PPSVZJV-CodzUBEt.js → c4Diagram-5PPSVZJV-BqjkNgv6.js} +1 -1
- package/public/assets/channel-CI1-SNMv.js +1 -0
- package/public/assets/{chunk-2GRJ4B5K-D59ECeXb.js → chunk-2GRJ4B5K-CWJJzbol.js} +1 -1
- package/public/assets/{chunk-4I5QYGJK-DlE6ad0r.js → chunk-4I5QYGJK-CuHGN-lT.js} +1 -1
- package/public/assets/{chunk-5RXB4S5H-Bw4SBOdN.js → chunk-5RXB4S5H-Cl92nmIs.js} +1 -1
- package/public/assets/{chunk-6Q2QTUOP-BD4S6iuB.js → chunk-6Q2QTUOP-C3vYg7rX.js} +1 -1
- package/public/assets/{chunk-7Z6QIM7H-DvwJZM4T.js → chunk-7Z6QIM7H-jkpbvAhi.js} +1 -1
- package/public/assets/{chunk-GF5L2VYU-BlzSHpI6.js → chunk-GF5L2VYU-Bo-zB54d.js} +1 -1
- package/public/assets/{chunk-I66GZJ75-DJurmtZ9.js → chunk-I66GZJ75-BD9h95Vq.js} +1 -1
- package/public/assets/{chunk-JQJVKLGR-t20LaGIq.js → chunk-JQJVKLGR-C6TRkyf7.js} +1 -1
- package/public/assets/{chunk-KBJHAD2P-Dgjyd4B5.js → chunk-KBJHAD2P-RgoL4Nto.js} +1 -1
- package/public/assets/{chunk-NSK5VX7P-Chchkdi8.js → chunk-NSK5VX7P-C6TsGnGA.js} +1 -1
- package/public/assets/{chunk-QR6OTTB3-CJK67frw.js → chunk-QR6OTTB3-Bq5akaxk.js} +1 -1
- package/public/assets/{chunk-RYQCIY6F-sGSUyT6_.js → chunk-RYQCIY6F-DtVCesJ4.js} +1 -1
- package/public/assets/{chunk-UBXNYLIW-CEzUmAZt.js → chunk-UBXNYLIW-BjnaZZH0.js} +1 -1
- package/public/assets/{chunk-W5SLKNZC-Spmh0y1r.js → chunk-W5SLKNZC-D1KW-lQn.js} +1 -1
- package/public/assets/{chunk-WRU74C26-DEsyhzZw.js → chunk-WRU74C26-YZFoWy6X.js} +1 -1
- package/public/assets/{chunk-XXDRQBXY-Bo1s3Uh5.js → chunk-XXDRQBXY-CyrI_Mox.js} +1 -1
- package/public/assets/classDiagram-JCYQIIEL-zdEoGtpz.js +1 -0
- package/public/assets/classDiagram-v2-OCEON4UE-zdEoGtpz.js +1 -0
- package/public/assets/{cose-bilkent-JH36ORCC-15pcvj-n.js → cose-bilkent-JH36ORCC-v6lDOu7m.js} +1 -1
- package/public/assets/{cynefin-VYW2F7L2-BZxWqC-g.js → cynefin-VYW2F7L2-BtzRQs_J.js} +1 -1
- package/public/assets/{cynefinDiagram-MW4NZA55-B_czkRTa.js → cynefinDiagram-MW4NZA55--R8S0KoS.js} +1 -1
- package/public/assets/{dagre-VZM6K2ZE-D8bvaK5F.js → dagre-VZM6K2ZE-D6mRFOfB.js} +1 -1
- package/public/assets/{diagram-7IWD3JNH-_vpWqUu0.js → diagram-7IWD3JNH-kfCoRMpW.js} +1 -1
- package/public/assets/{diagram-B4RE2ZJO-DPfWeSE2.js → diagram-B4RE2ZJO-sxUW6rzT.js} +1 -1
- package/public/assets/{diagram-LBJQPF4R-Cb7ustRg.js → diagram-LBJQPF4R-B926bgCA.js} +1 -1
- package/public/assets/{diagram-Q27KOJAE-CRIHo9aN.js → diagram-Q27KOJAE-CcVi6njJ.js} +1 -1
- package/public/assets/{diagram-UB23O5K3-CMyiQouk.js → diagram-UB23O5K3-BtW4XOuK.js} +1 -1
- package/public/assets/{dist-DVdstvf6.js → dist-BnkmMC7M.js} +1 -1
- package/public/assets/{ebnfDiagram-BXEA7PRR-UURYMfYm.js → ebnfDiagram-BXEA7PRR-BNGfWHYj.js} +1 -1
- package/public/assets/{erDiagram-JOGREHBK-Dio32huH.js → erDiagram-JOGREHBK-BGrhe741.js} +1 -1
- package/public/assets/eventmodeling-45OFAUF4-Cn6RDMEG.js +1 -0
- package/public/assets/flowDiagram-UKHOOZJN-DOMsEKL1.js +1 -0
- package/public/assets/{ganttDiagram-PKOTCBZU-62etRb4w.js → ganttDiagram-PKOTCBZU-CR5BNdXR.js} +1 -1
- package/public/assets/{gitGraph-TEB2WS4Q-CfIUb3Fs.js → gitGraph-TEB2WS4Q-CfvC-iZD.js} +1 -1
- package/public/assets/{gitGraphDiagram-DS77QQ5N-DuwZInBC.js → gitGraphDiagram-DS77QQ5N-D45EWdll.js} +1 -1
- package/public/assets/index-CxNe802C.css +2 -0
- package/public/assets/index-yHI4kvM-.js +41 -0
- package/public/assets/{info-DKCQHKI2-BfXJHEd1.js → info-DKCQHKI2-stahmZKW.js} +1 -1
- package/public/assets/{infoDiagram-6WML65LV-Bmh_7gih.js → infoDiagram-6WML65LV-nWu8ujif.js} +1 -1
- package/public/assets/{ishikawaDiagram-WSZJBQD7-i8-L2rAe.js → ishikawaDiagram-WSZJBQD7-Ccwzv-xZ.js} +1 -1
- package/public/assets/{journeyDiagram-NVQOT4AX-BtfEqljI.js → journeyDiagram-NVQOT4AX-R7gZw1Qw.js} +1 -1
- package/public/assets/{kanban-definition-27J2QSJJ-pH6QumbR.js → kanban-definition-27J2QSJJ-By8aTzkI.js} +1 -1
- package/public/assets/{line-DsGf4Aam.js → line-zJXbSVxf.js} +1 -1
- package/public/assets/{linear-BNSu_43k.js → linear-B8U-jWnO.js} +1 -1
- package/public/assets/{mermaid-parser.core-679COn57.js → mermaid-parser.core-B1iqIz20.js} +3 -3
- package/public/assets/{mermaid.core-D_f4AlrR.js → mermaid.core-CuFK5CuK.js} +4 -4
- package/public/assets/{mindmap-definition-FAOFIHXS-D1tM4SRn.js → mindmap-definition-FAOFIHXS-qJ2DuBku.js} +1 -1
- package/public/assets/{packet-7NZHBO7P-olELdMgx.js → packet-7NZHBO7P-CzQJdD0e.js} +1 -1
- package/public/assets/{pegDiagram-VL7TDLO6-DgAndH0A.js → pegDiagram-VL7TDLO6-CKmHw89b.js} +1 -1
- package/public/assets/{pie-RZYD4A2V-DWj8tY38.js → pie-RZYD4A2V-Dt3CA16n.js} +1 -1
- package/public/assets/{pieDiagram-7S7Q4E2Y-DK7c2EvE.js → pieDiagram-7S7Q4E2Y-D9vwefN9.js} +1 -1
- package/public/assets/{quadrantDiagram-CIZ2JOQS-DI-ZbPxK.js → quadrantDiagram-CIZ2JOQS-jtSAmOcp.js} +1 -1
- package/public/assets/{radar-I7S5WNFK-DLsdeAbl.js → radar-I7S5WNFK-BtDfUAqO.js} +1 -1
- package/public/assets/{railroad-3IZDKUUU-BFUnqJxV.js → railroad-3IZDKUUU-D5DNw8qB.js} +1 -1
- package/public/assets/railroad-abnf-AHOZXSZD-DuJUfLMt.js +1 -0
- package/public/assets/railroad-ebnf-EBAXGLYW-CiLIN42p.js +1 -0
- package/public/assets/railroad-peg-LSFZ7HO6-DKtIAsvs.js +1 -0
- package/public/assets/{railroadDiagram-AXF67PYL-BcCf-qnq.js → railroadDiagram-AXF67PYL-C-S9VT4Z.js} +1 -1
- package/public/assets/{requirementDiagram-LRYGKXZP-CSERzrsB.js → requirementDiagram-LRYGKXZP-Bmv6EbmT.js} +1 -1
- package/public/assets/{sankeyDiagram-W5VNT64P-BTHdyTAB.js → sankeyDiagram-W5VNT64P-Cm2dLWH9.js} +1 -1
- package/public/assets/{sequenceDiagram-SI44F4Z6-DCMbcZQ-.js → sequenceDiagram-SI44F4Z6-DPslkAXf.js} +1 -1
- package/public/assets/{src-CWXJqyrc.js → src-l8EnJwXz.js} +1 -1
- package/public/assets/{stateDiagram-OKZ733FA-BhzI_RZ_.js → stateDiagram-OKZ733FA-BBXMNQQ7.js} +1 -1
- package/public/assets/stateDiagram-v2-UEYNNEHI-BxOobbWc.js +1 -0
- package/public/assets/{swimlanes-SLNWSIFB-DPKSeErn.js → swimlanes-SLNWSIFB-BeiwtQg-.js} +1 -1
- package/public/assets/swimlanesDiagram-ULZ7WXOC-BEW9yZ9H.js +8 -0
- package/public/assets/{timeline-definition-Z64GVDOM-BnLvIO3-.js → timeline-definition-Z64GVDOM-Drd7Un_g.js} +1 -1
- package/public/assets/{treeView-QDETBFTQ-BgFXBEUi.js → treeView-QDETBFTQ-BT2KyIoB.js} +1 -1
- package/public/assets/{treemap-6X3UGDF4-KzlTv1ih.js → treemap-6X3UGDF4-gdZw2F6X.js} +1 -1
- package/public/assets/{vennDiagram-T6HMQDX7-gsHmzjh9.js → vennDiagram-T6HMQDX7-Bbk8OMR0.js} +1 -1
- package/public/assets/{wardley-OPB4EBWU-DyQkbq8g.js → wardley-OPB4EBWU-BI0xutNp.js} +1 -1
- package/public/assets/{wardleyDiagram-T6FBY63Y-DqwaVwnv.js → wardleyDiagram-T6FBY63Y-DQvEcd8r.js} +1 -1
- package/public/assets/{xychartDiagram-ELKLHX3M-DGZxfApD.js → xychartDiagram-ELKLHX3M-CRWoUpzf.js} +1 -1
- package/public/index.html +2 -2
- package/public/assets/architecture-TIHT7OUA-CtVIKkxG.js +0 -1
- package/public/assets/channel-CpTfWcC2.js +0 -1
- package/public/assets/classDiagram-JCYQIIEL-BSB0boKu.js +0 -1
- package/public/assets/classDiagram-v2-OCEON4UE-BSB0boKu.js +0 -1
- package/public/assets/eventmodeling-45OFAUF4-DP5rNNNm.js +0 -1
- package/public/assets/flowDiagram-UKHOOZJN-DjrqEV81.js +0 -1
- package/public/assets/index-ZvSJKDLe.css +0 -2
- package/public/assets/index-iKIFRUDL.js +0 -40
- package/public/assets/railroad-abnf-AHOZXSZD-B00TU1b8.js +0 -1
- package/public/assets/railroad-ebnf-EBAXGLYW-RrXgWJqj.js +0 -1
- package/public/assets/railroad-peg-LSFZ7HO6-CsiNSgst.js +0 -1
- package/public/assets/stateDiagram-v2-UEYNNEHI-DKG0b0eo.js +0 -1
- 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 {
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
140
|
-
if (
|
|
141
|
-
log(`
|
|
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
|
-
|
|
576
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
818
|
-
|
|
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
|
-
|
|
874
|
-
|
|
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`:
|
|
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
|
-
|
|
39
|
-
|
|
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 ?
|
|
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.
|
|
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-
|
|
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-
|
|
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};
|