@davidbalzan/groundwork-console 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/groundwork-console.mjs +2 -0
- package/dist/answered.js +16 -34
- package/dist/gitinfo.js +114 -0
- package/dist/kitresolve.js +12 -9
- package/dist/mcp.js +41 -2
- package/dist/provision.js +94 -4
- package/dist/server.js +218 -41
- package/dist/skills.js +51 -7
- package/package.json +5 -2
- package/public/assets/{abnfDiagram-N423BO3Z-CdHjY-Rl.js → abnfDiagram-N423BO3Z-oco1yJr8.js} +1 -1
- package/public/assets/{arc-BrWBiZCE.js → arc-D-FYRBZ0.js} +1 -1
- package/public/assets/architecture-TIHT7OUA-bBJswTmy.js +1 -0
- package/public/assets/{architectureDiagram-T3A2C74G-BlL-ALnB.js → architectureDiagram-T3A2C74G-CIG6iOMV.js} +1 -1
- package/public/assets/{blockDiagram-VBNYF7ZC-DZyulKP3.js → blockDiagram-VBNYF7ZC-B6dDKm7d.js} +1 -1
- package/public/assets/{c4Diagram-5PPSVZJV-BMMLwghr.js → c4Diagram-5PPSVZJV-Y3UQgn94.js} +1 -1
- package/public/assets/channel-BeJg4QSq.js +1 -0
- package/public/assets/{chunk-2GRJ4B5K-BzJtMGzP.js → chunk-2GRJ4B5K-BoWCLegZ.js} +1 -1
- package/public/assets/{chunk-4I5QYGJK-DwFXVxXp.js → chunk-4I5QYGJK-DZTRMmZG.js} +1 -1
- package/public/assets/{chunk-5RXB4S5H-BohnhuTy.js → chunk-5RXB4S5H-Dw1IdYaS.js} +1 -1
- package/public/assets/{chunk-6Q2QTUOP-BXxDc17T.js → chunk-6Q2QTUOP-nWUr6zuz.js} +1 -1
- package/public/assets/{chunk-7Z6QIM7H-k5qXgkB4.js → chunk-7Z6QIM7H-DxXQZe2t.js} +1 -1
- package/public/assets/{chunk-GF5L2VYU-Cl8z_Pcd.js → chunk-GF5L2VYU-DbHTRiGa.js} +1 -1
- package/public/assets/{chunk-I66GZJ75-CCxcXl9W.js → chunk-I66GZJ75-C97VIOWu.js} +1 -1
- package/public/assets/{chunk-JQJVKLGR-Dah9RJ-p.js → chunk-JQJVKLGR-CeOq7ul_.js} +1 -1
- package/public/assets/{chunk-KBJHAD2P-B1kM7HCm.js → chunk-KBJHAD2P-D_xgFhfD.js} +1 -1
- package/public/assets/{chunk-NSK5VX7P-Ddw2Pneq.js → chunk-NSK5VX7P-DjNVQuCo.js} +1 -1
- package/public/assets/{chunk-QR6OTTB3-SR1VT_gf.js → chunk-QR6OTTB3-CGPHl8Ev.js} +1 -1
- package/public/assets/{chunk-RYQCIY6F-CsMzInBn.js → chunk-RYQCIY6F-Ca_NSSUq.js} +1 -1
- package/public/assets/{chunk-UBXNYLIW-CysKciL_.js → chunk-UBXNYLIW-BH4KEg7B.js} +1 -1
- package/public/assets/{chunk-W5SLKNZC-CueNCRji.js → chunk-W5SLKNZC-CzSVKrQX.js} +1 -1
- package/public/assets/{chunk-WRU74C26-CVxduXc3.js → chunk-WRU74C26--qTXNfid.js} +1 -1
- package/public/assets/{chunk-XXDRQBXY-DOdFQa-t.js → chunk-XXDRQBXY-0XJhmQM-.js} +1 -1
- package/public/assets/classDiagram-JCYQIIEL-B6RcDz49.js +1 -0
- package/public/assets/classDiagram-v2-OCEON4UE-B6RcDz49.js +1 -0
- package/public/assets/{cose-bilkent-JH36ORCC-B2Z9nj2r.js → cose-bilkent-JH36ORCC-CeMP2vk-.js} +1 -1
- package/public/assets/{cynefin-VYW2F7L2-CmV_z2bb.js → cynefin-VYW2F7L2-NtSbHkFO.js} +1 -1
- package/public/assets/{cynefinDiagram-MW4NZA55-zeWgiEbQ.js → cynefinDiagram-MW4NZA55-B4uWxM0-.js} +1 -1
- package/public/assets/{dagre-VZM6K2ZE-CaMVheN4.js → dagre-VZM6K2ZE-C8wHGLeh.js} +1 -1
- package/public/assets/{diagram-7IWD3JNH-BTJeDC0S.js → diagram-7IWD3JNH-DKe15FkE.js} +1 -1
- package/public/assets/{diagram-B4RE2ZJO-BUtRYMUD.js → diagram-B4RE2ZJO-B-pgDq0Z.js} +1 -1
- package/public/assets/{diagram-LBJQPF4R-DM--EdGk.js → diagram-LBJQPF4R-16bhvJKB.js} +1 -1
- package/public/assets/{diagram-Q27KOJAE-Dcp9D1k4.js → diagram-Q27KOJAE-BuzXd81m.js} +1 -1
- package/public/assets/{diagram-UB23O5K3-DzLpzKu2.js → diagram-UB23O5K3-CZzMkkP7.js} +1 -1
- package/public/assets/{dist-DFkPtM9v.js → dist-DoiwlY9_.js} +1 -1
- package/public/assets/{ebnfDiagram-BXEA7PRR-C2lFoa24.js → ebnfDiagram-BXEA7PRR-BGUXNic-.js} +1 -1
- package/public/assets/{erDiagram-JOGREHBK-GvedjkFY.js → erDiagram-JOGREHBK--mo3pLi7.js} +1 -1
- package/public/assets/eventmodeling-45OFAUF4-Bv0pQfuo.js +1 -0
- package/public/assets/flowDiagram-UKHOOZJN-cF6VTIV-.js +1 -0
- package/public/assets/{ganttDiagram-PKOTCBZU-NzyabXr_.js → ganttDiagram-PKOTCBZU-DbHv-Q1F.js} +1 -1
- package/public/assets/{gitGraph-TEB2WS4Q-CAnBW3hc.js → gitGraph-TEB2WS4Q-CU8msIuE.js} +1 -1
- package/public/assets/{gitGraphDiagram-DS77QQ5N-ChFOjBWW.js → gitGraphDiagram-DS77QQ5N-CmVui6qG.js} +1 -1
- package/public/assets/index-BZBf_g4s.css +2 -0
- package/public/assets/index-CJ73bGme.js +41 -0
- package/public/assets/{info-DKCQHKI2-9HGQNL8q.js → info-DKCQHKI2-CcA5kzgP.js} +1 -1
- package/public/assets/{infoDiagram-6WML65LV-CSZIpCN6.js → infoDiagram-6WML65LV-Drd9lSSV.js} +1 -1
- package/public/assets/{ishikawaDiagram-WSZJBQD7-m5LI_l0g.js → ishikawaDiagram-WSZJBQD7-CcnlbfwE.js} +1 -1
- package/public/assets/{journeyDiagram-NVQOT4AX-DlhpOOGP.js → journeyDiagram-NVQOT4AX-CYcWG8V9.js} +1 -1
- package/public/assets/{kanban-definition-27J2QSJJ-CpS1zcAJ.js → kanban-definition-27J2QSJJ-Ca5F0ySI.js} +1 -1
- package/public/assets/{line-DqdLvBGQ.js → line-BFnVTMoX.js} +1 -1
- package/public/assets/{linear-ZFSYfg_z.js → linear-BVUPf0HH.js} +1 -1
- package/public/assets/{mermaid-parser.core-B4KSdud0.js → mermaid-parser.core-BXY3JF8d.js} +3 -3
- package/public/assets/{mermaid.core-CSBAT3ig.js → mermaid.core-DqjaFy6t.js} +4 -4
- package/public/assets/{mindmap-definition-FAOFIHXS-DM39eR7l.js → mindmap-definition-FAOFIHXS-m3-vScki.js} +1 -1
- package/public/assets/{packet-7NZHBO7P-CaKShi1f.js → packet-7NZHBO7P-BTm7636C.js} +1 -1
- package/public/assets/{pegDiagram-VL7TDLO6-CeupMOTZ.js → pegDiagram-VL7TDLO6-C3kirtKa.js} +1 -1
- package/public/assets/{pie-RZYD4A2V-CdyK9ryQ.js → pie-RZYD4A2V-BsF__qFU.js} +1 -1
- package/public/assets/{pieDiagram-7S7Q4E2Y-BTsBpWka.js → pieDiagram-7S7Q4E2Y-YEbSx31D.js} +1 -1
- package/public/assets/{quadrantDiagram-CIZ2JOQS-UamjAqXX.js → quadrantDiagram-CIZ2JOQS-qY1c3Wr-.js} +1 -1
- package/public/assets/{radar-I7S5WNFK-BHPrA655.js → radar-I7S5WNFK-BaQRhfKS.js} +1 -1
- package/public/assets/{railroad-3IZDKUUU-BmnKim6H.js → railroad-3IZDKUUU-C4riB9xs.js} +1 -1
- package/public/assets/railroad-abnf-AHOZXSZD-CBcG7kXE.js +1 -0
- package/public/assets/railroad-ebnf-EBAXGLYW-EqgxHSrX.js +1 -0
- package/public/assets/railroad-peg-LSFZ7HO6-C5-qxTK4.js +1 -0
- package/public/assets/{railroadDiagram-AXF67PYL-bbNt4a4d.js → railroadDiagram-AXF67PYL-BDXHJJin.js} +1 -1
- package/public/assets/{requirementDiagram-LRYGKXZP-BrwBiGll.js → requirementDiagram-LRYGKXZP-5N_4nQH3.js} +1 -1
- package/public/assets/{sankeyDiagram-W5VNT64P-BeKu1QRQ.js → sankeyDiagram-W5VNT64P-C0zqsu_g.js} +1 -1
- package/public/assets/{sequenceDiagram-SI44F4Z6-DiBZBmdo.js → sequenceDiagram-SI44F4Z6-DonYJ8_h.js} +1 -1
- package/public/assets/{src-B2eptEoc.js → src-D5IewQLB.js} +1 -1
- package/public/assets/{stateDiagram-OKZ733FA-Tjie08JN.js → stateDiagram-OKZ733FA-CEY8i14f.js} +1 -1
- package/public/assets/stateDiagram-v2-UEYNNEHI-C13FXtls.js +1 -0
- package/public/assets/{swimlanes-SLNWSIFB-B_W9IMQM.js → swimlanes-SLNWSIFB-BBKzhZ8g.js} +1 -1
- package/public/assets/swimlanesDiagram-ULZ7WXOC-D0VKK_TP.js +8 -0
- package/public/assets/{timeline-definition-Z64GVDOM-B3CqBSPD.js → timeline-definition-Z64GVDOM-RI7P7P5y.js} +1 -1
- package/public/assets/{treeView-QDETBFTQ-BUEh9zyF.js → treeView-QDETBFTQ-Bpw_kGf5.js} +1 -1
- package/public/assets/{treemap-6X3UGDF4-D_t1bJ-9.js → treemap-6X3UGDF4-Deq82US6.js} +1 -1
- package/public/assets/{vennDiagram-T6HMQDX7-B1_XMyC0.js → vennDiagram-T6HMQDX7-DetRcU6h.js} +1 -1
- package/public/assets/{wardley-OPB4EBWU-BlIYjwGG.js → wardley-OPB4EBWU-Iwt8nlFq.js} +1 -1
- package/public/assets/{wardleyDiagram-T6FBY63Y-BUJJz-1q.js → wardleyDiagram-T6FBY63Y-iVT5tLSy.js} +1 -1
- package/public/assets/{xychartDiagram-ELKLHX3M-CgWS9hey.js → xychartDiagram-ELKLHX3M-B7_cZ8Wg.js} +1 -1
- package/public/index.html +2 -2
- package/public/assets/architecture-TIHT7OUA-nRAC6cKL.js +0 -1
- package/public/assets/channel-CmRhJjbj.js +0 -1
- package/public/assets/classDiagram-JCYQIIEL-BhHtVWEo.js +0 -1
- package/public/assets/classDiagram-v2-OCEON4UE-BhHtVWEo.js +0 -1
- package/public/assets/eventmodeling-45OFAUF4-CVrSQ2LC.js +0 -1
- package/public/assets/flowDiagram-UKHOOZJN-BWxvJEZS.js +0 -1
- package/public/assets/index-CsJm47wv.css +0 -2
- package/public/assets/index-DPLoCbew.js +0 -40
- package/public/assets/railroad-abnf-AHOZXSZD-n6VUqVzW.js +0 -1
- package/public/assets/railroad-ebnf-EBAXGLYW-Buhu71hT.js +0 -1
- package/public/assets/railroad-peg-LSFZ7HO6-DxxRF3eB.js +0 -1
- package/public/assets/stateDiagram-v2-UEYNNEHI-Bt5as9x1.js +0 -1
- package/public/assets/swimlanesDiagram-ULZ7WXOC-B1GEgwSB.js +0 -8
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ groundwork-console token # print the token
|
|
|
18
18
|
groundwork-console config # print the config path
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Requires on the host: Node ≥ 22, tmux (pane monitor / spawn), and either the three kit bins above on PATH or a kit checkout at `kitPath` in the config. `doctor` tells you which it found. Console requires: seam `^0.1.0` · agent-coord-mcp `^0.
|
|
21
|
+
Requires on the host: Node ≥ 22, tmux (pane monitor / spawn), and either the three kit bins above on PATH or a kit checkout at `kitPath` in the config. `doctor` tells you which it found. Console requires: seam `^0.1.0` · agent-coord-mcp `^0.24.0` · groundwork `^0.3.0` · agent-coordination `^0.5.0`.
|
|
22
22
|
|
|
23
23
|
## From another machine
|
|
24
24
|
|
|
@@ -58,6 +58,8 @@ if (sub === "doctor") {
|
|
|
58
58
|
console.log(`groundwork-console ${pkg.version} · config ${CONFIG_FILE} · store ${cfg.coordDir}`);
|
|
59
59
|
for (const k of ["seam", "mcp", "groundwork", "playbook"]) console.log(` ${r[k].ok === false ? "✗" : r[k].ok ? "✓" : "?"} ${k.padEnd(10)} ${String(r[k].installed ?? "missing").padEnd(8)} needs ${r[k].required}${r.paths[k] ? ` ← ${r.paths[k]}` : ""}`);
|
|
60
60
|
console.log(` kit checkout: ${r.paths.kitCheckout ?? "none (using installed packages)"}`);
|
|
61
|
+
// Capability probe: connect once to the MCP (as groundwork-console, read-only) and check send_message.inReplyTo — the bus feature the console's answered-decisions logic relies on.
|
|
62
|
+
try { const { BusClient } = await import("../dist/mcp.js"); const cl = new BusClient(cfg.kitPath, cfg.coordDir, "groundwork-console"); await cl.connect(); const ok = cl.supports("send_message", "inReplyTo"); await cl.close(); console.log(` ${ok ? "✓" : "⚠"} mcp capability inReplyTo ${ok ? "present" : "missing — decision replies will not be linked/cleared; upgrade agent-coord-mcp"}`); } catch (e) { console.log(` ? mcp capability probe failed: ${e.message}`); }
|
|
61
63
|
if (!r.ok) { console.log("\nfix: npm i -g agent-coord-mcp @davidbalzan/groundwork agent-coordination"); process.exit(1); }
|
|
62
64
|
process.exit(0);
|
|
63
65
|
}
|
package/dist/answered.js
CHANGED
|
@@ -1,40 +1,11 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
/*
|
|
5
|
-
|
|
6
|
-
Persisted in ~/.groundwork-console/answered.json — never touches ~/agent-coord. */
|
|
7
|
-
const FILE = path.join(homedir(), ".groundwork-console", "answered.json");
|
|
8
|
-
let cache = null;
|
|
9
|
-
export function answered() {
|
|
10
|
-
if (cache)
|
|
11
|
-
return cache;
|
|
12
|
-
try {
|
|
13
|
-
cache = existsSync(FILE) ? JSON.parse(readFileSync(FILE, "utf8")) : {};
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
cache = {};
|
|
17
|
-
}
|
|
18
|
-
return cache;
|
|
19
|
-
}
|
|
20
|
-
export function markAnswered(ids, meta) {
|
|
21
|
-
const a = answered();
|
|
22
|
-
let n = 0;
|
|
23
|
-
for (const id of ids)
|
|
24
|
-
if (id && !a[id]) {
|
|
25
|
-
a[id] = { ts: Date.now(), ...meta };
|
|
26
|
-
n++;
|
|
27
|
-
}
|
|
28
|
-
if (n) {
|
|
29
|
-
mkdirSync(path.dirname(FILE), { recursive: true });
|
|
30
|
-
writeFileSync(FILE, JSON.stringify(a, null, 2));
|
|
31
|
-
}
|
|
32
|
-
return n;
|
|
33
|
-
}
|
|
34
|
-
/* Bus-native answered set (kit ≥ 18ffbf4): any message FROM david carrying inReplyTo:<id> answers <id>. Scanned from ~/agent-coord inbox/*.jsonl + rooms/*.jsonl, cached 10 s. Unioned with the console-owned set above. */
|
|
4
|
+
/* Answered DAVID_DECISION packets — bus-native only (kit ≥ 18ffbf4): a message FROM david carrying inReplyTo:<id> answers <id>.
|
|
5
|
+
Derived by scanning ~/agent-coord inbox/*.jsonl + rooms/*.jsonl (cached 10 s). The console-owned answered.json store was retired 2026-08-19. */
|
|
35
6
|
let busCache = null;
|
|
36
7
|
let coordRoot = null;
|
|
37
|
-
export function setCoordDir(dir) { coordRoot = dir; }
|
|
8
|
+
export function setCoordDir(dir) { coordRoot = dir; busCache = null; }
|
|
38
9
|
function busAnswered() {
|
|
39
10
|
if (!coordRoot)
|
|
40
11
|
return new Set();
|
|
@@ -64,4 +35,15 @@ function busAnswered() {
|
|
|
64
35
|
busCache = { at: Date.now(), ids };
|
|
65
36
|
return ids;
|
|
66
37
|
}
|
|
67
|
-
export const isAnswered = (id) => typeof id === "string" &&
|
|
38
|
+
export const isAnswered = (id) => typeof id === "string" && busAnswered().has(id);
|
|
39
|
+
/** Forget the cache (after the console itself sends a reply) so the next read reflects it immediately. */
|
|
40
|
+
export const invalidateAnswered = () => { busCache = null; };
|
|
41
|
+
/** One-time cleanup of the retired console-owned store. Returns true if a file was removed. */
|
|
42
|
+
const LEGACY_FILE = path.join(homedir(), ".groundwork-console", "answered.json");
|
|
43
|
+
export function removeLegacyAnsweredFile() { try {
|
|
44
|
+
if (existsSync(LEGACY_FILE)) {
|
|
45
|
+
unlinkSync(LEGACY_FILE);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch { /* leave it */ } return false; }
|
package/dist/gitinfo.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* Read-only git views for a project's docs: uncommitted hunks (working tree vs HEAD, staged included), last commit touching the file + its hunks, and a docs/ status map. */
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
4
6
|
const run = promisify(execFile);
|
|
5
7
|
async function git(cwd, args) { try {
|
|
6
8
|
const r = await run("git", args, { cwd, maxBuffer: 8 << 20 });
|
|
@@ -67,3 +69,115 @@ export async function lastDocsCommit(projectPath) {
|
|
|
67
69
|
const files = ((await git(projectPath, ["show", "--format=", "--name-only", short, "--", "docs"])) ?? "").split("\n").filter(Boolean).map((f) => f.replace(/^docs\//, ""));
|
|
68
70
|
return { short, subject, date, files };
|
|
69
71
|
}
|
|
72
|
+
const GIT_CACHE_MS = 3_000;
|
|
73
|
+
const STALE_DAYS = 14;
|
|
74
|
+
const GRAPH_MAX = 500;
|
|
75
|
+
const cache = new Map();
|
|
76
|
+
function cached(key, fn) {
|
|
77
|
+
const hit = cache.get(key);
|
|
78
|
+
if (hit && Date.now() - hit.at < GIT_CACHE_MS)
|
|
79
|
+
return hit.value;
|
|
80
|
+
const value = fn();
|
|
81
|
+
cache.set(key, { at: Date.now(), value });
|
|
82
|
+
value.catch(() => cache.delete(key));
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
const splitRecords = (out) => (out ?? "").split("\n").filter(Boolean).map((l) => l.split("\x1f"));
|
|
86
|
+
export function gitGraph(projectPath, limit = 200) {
|
|
87
|
+
const n = Math.max(1, Math.min(GRAPH_MAX, Math.floor(limit) || 200));
|
|
88
|
+
return cached(`graph:${projectPath}:${n}`, async () => {
|
|
89
|
+
const head = (await git(projectPath, ["rev-parse", "HEAD"]))?.trim() ?? "";
|
|
90
|
+
const rows = splitRecords(await git(projectPath, ["log", "--all", "--date-order", `-n${n}`, "--format=%H%x1f%h%x1f%P%x1f%s%x1f%an%x1f%aI%x1f%D"]));
|
|
91
|
+
const commits = rows.map(([sha, short, parents, subject, author, date, refs]) => ({ sha, short, parents: (parents ?? "").split(" ").filter(Boolean), subject: subject ?? "", author: author ?? "", date: date ?? "", refs: (refs ?? "").split(",").map((r) => r.trim().replace(/^HEAD -> /, "HEAD -> ")).filter(Boolean) }));
|
|
92
|
+
return { head, commits };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** `git worktree list --porcelain` → entries; dirty = `git status --porcelain` non-empty (skipped for prunable/missing paths); agent from the fleet sibling dir name `<repo>-<agentId>[-<slice>]`, else board owner whose Branch·Worktree column names the branch/path. */
|
|
96
|
+
export function gitWorktrees(projectPath, hints = {}) {
|
|
97
|
+
return cached(`worktrees:${projectPath}:${(hints.agentIds ?? []).join(",")}`, async () => {
|
|
98
|
+
const out = (await git(projectPath, ["worktree", "list", "--porcelain"])) ?? "";
|
|
99
|
+
const blocks = out.split(/\n\s*\n/).map((b) => b.split("\n").filter(Boolean)).filter((b) => b.length);
|
|
100
|
+
const repoBase = path.basename(projectPath.replace(/\/+$/, ""));
|
|
101
|
+
const agentIds = [...(hints.agentIds ?? [])].sort((a, b) => b.length - a.length); // longest first: "<project>-worker-1" before "<project>"
|
|
102
|
+
const worktrees = await Promise.all(blocks.map(async (b, i) => {
|
|
103
|
+
const get = (k) => b.find((l) => l === k || l.startsWith(k + " "))?.slice(k.length).trim();
|
|
104
|
+
const wt = get("worktree") ?? "";
|
|
105
|
+
const sha = get("HEAD") ?? "";
|
|
106
|
+
const branchRef = get("branch");
|
|
107
|
+
const prunable = get("prunable") !== undefined;
|
|
108
|
+
const detached = b.includes("detached") || !branchRef;
|
|
109
|
+
const branch = branchRef ? branchRef.replace(/^refs\/heads\//, "") : null;
|
|
110
|
+
const dirty = !prunable && wt && existsSync(wt) ? ((await git(wt, ["status", "--porcelain"])) ?? "").trim().length > 0 : false;
|
|
111
|
+
const dir = path.basename(wt);
|
|
112
|
+
let agent;
|
|
113
|
+
if (i > 0 && dir.startsWith(repoBase + "-")) {
|
|
114
|
+
const suffix = dir.slice(repoBase.length + 1);
|
|
115
|
+
agent = agentIds.find((id) => suffix === id || suffix.startsWith(id + "-"));
|
|
116
|
+
}
|
|
117
|
+
if (!agent) {
|
|
118
|
+
const row = hints.boardRows?.find((r) => r.branchWorktree && ((branch && r.branchWorktree.includes(branch)) || r.branchWorktree.includes(dir)));
|
|
119
|
+
if (row?.owner)
|
|
120
|
+
agent = row.owner;
|
|
121
|
+
}
|
|
122
|
+
return { path: wt, branch, sha, short: sha.slice(0, 7), detached, dirty, prunable, ...(agent ? { agent } : {}), isMain: i === 0 };
|
|
123
|
+
}));
|
|
124
|
+
return { worktrees };
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** gh (optional): open/merged/closed PRs by head branch. Missing/unauthenticated gh → null (envelope `ghAvailable:false`). */
|
|
128
|
+
async function ghPrs(projectPath) {
|
|
129
|
+
try {
|
|
130
|
+
const r = await run("gh", ["pr", "list", "--state", "all", "--limit", "50", "--json", "number,url,state,title,headRefName"], { cwd: projectPath, maxBuffer: 8 << 20, timeout: 15_000 });
|
|
131
|
+
const list = JSON.parse(r.stdout);
|
|
132
|
+
const m = new Map();
|
|
133
|
+
for (const p of list)
|
|
134
|
+
if (!m.has(p.headRefName))
|
|
135
|
+
m.set(p.headRefName, { number: p.number, url: p.url, state: p.state, title: p.title });
|
|
136
|
+
return m;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export function gitBranches(projectPath, hints = {}) {
|
|
143
|
+
return cached(`branches:${projectPath}:${(hints.agentIds ?? []).join(",")}`, async () => {
|
|
144
|
+
const defaultBranch = "main";
|
|
145
|
+
const base = `origin/${defaultBranch}`;
|
|
146
|
+
const hasBase = (await git(projectPath, ["rev-parse", "--verify", "-q", `${base}^{commit}`]))?.trim() ? true : false;
|
|
147
|
+
const rows = splitRecords(await git(projectPath, ["for-each-ref", "--sort=-committerdate", "--format=%(refname)%1f%(objectname)%1f%(objectname:short)%1f%(subject)%1f%(committerdate:iso-strict)", "refs/heads", "refs/remotes"]))
|
|
148
|
+
.filter(([ref]) => !/^refs\/remotes\/[^/]+\/HEAD$/.test(ref));
|
|
149
|
+
const [{ worktrees }, prs] = await Promise.all([gitWorktrees(projectPath, hints), ghPrs(projectPath)]);
|
|
150
|
+
const qaOf = (n) => { const row = hints.openPrs?.find((r) => Object.values(r).some((v) => new RegExp(`(^|[^0-9])#${n}([^0-9]|$)`).test(v))); if (!row)
|
|
151
|
+
return undefined; const qa = row.mergeGate ?? row.status ?? row.state; return qa ? String(qa) : undefined; };
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
const branches = await Promise.all(rows.map(async ([ref, sha, short, subject, date]) => {
|
|
154
|
+
const remote = ref.startsWith("refs/remotes/");
|
|
155
|
+
const name = ref.replace(/^refs\/(heads|remotes)\//, "");
|
|
156
|
+
let ahead = null, behind = null, merged = false;
|
|
157
|
+
if (hasBase) {
|
|
158
|
+
const lr = (await git(projectPath, ["rev-list", "--left-right", "--count", `${base}...${ref}`]))?.trim().split(/\s+/);
|
|
159
|
+
if (lr && lr.length === 2) {
|
|
160
|
+
behind = +lr[0];
|
|
161
|
+
ahead = +lr[1];
|
|
162
|
+
}
|
|
163
|
+
merged = (await gitOk(projectPath, ["merge-base", "--is-ancestor", ref, base]));
|
|
164
|
+
}
|
|
165
|
+
const stale = date ? now - Date.parse(date) > STALE_DAYS * 86_400_000 : false;
|
|
166
|
+
const localName = remote ? name.replace(/^[^/]+\//, "") : name;
|
|
167
|
+
const wt = remote ? undefined : worktrees.find((w) => w.branch === name);
|
|
168
|
+
const boardRow = hints.boardRows?.find((r) => r.branchWorktree && r.branchWorktree.includes(localName));
|
|
169
|
+
const owner = boardRow?.owner || wt?.agent;
|
|
170
|
+
const pr = prs?.get(localName);
|
|
171
|
+
const prOut = pr ? { ...pr, ...(qaOf(pr.number) ? { qa: qaOf(pr.number) } : {}) } : undefined;
|
|
172
|
+
return { name, remote, sha, short, subject: subject ?? "", date: date ?? "", ahead, behind, merged, stale, ...(wt ? { worktree: wt.path } : {}), ...(owner ? { owner } : {}), ...(prOut ? { pr: prOut } : {}) };
|
|
173
|
+
}));
|
|
174
|
+
return { defaultBranch, base, baseFound: hasBase, ghAvailable: prs !== null, branches };
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async function gitOk(cwd, args) { try {
|
|
178
|
+
await run("git", args, { cwd });
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return false;
|
|
183
|
+
} }
|
package/dist/kitresolve.js
CHANGED
|
@@ -2,7 +2,9 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
5
|
+
// mcp ^0.24.0 ships send_message.inReplyTo (npm 2026-08-19) — that is the floor. send_command's allowlist (/reload-skills since 0.25.0) is NOT a floor:
|
|
6
|
+
// it is feature-detected per connection (BusClient.sendCommands → /health.requires.mcp.capabilities.sendCommand) and the console degrades gracefully without it.
|
|
7
|
+
export const REQUIRES = { seam: "^0.1.0", mcp: "^0.24.0", groundwork: "^0.3.0", playbook: "^0.5.0" };
|
|
6
8
|
const which = (bin) => { try {
|
|
7
9
|
return execFileSync("which", [bin], { encoding: "utf8" }).trim() || null;
|
|
8
10
|
}
|
|
@@ -56,25 +58,26 @@ export function installedVersions(kitPath) {
|
|
|
56
58
|
const gw = groundworkCli(kitPath), pb = playbookCli(kitPath), mcp = mcpServer(kitPath);
|
|
57
59
|
return { seam, groundwork: pkgVersionNear(gw), playbook: pkgVersionNear(pb), mcp: pkgVersionNear(mcp), paths: { groundwork: gw, playbook: pb, mcp, kitCheckout: hasKitCheckout(kitPath) ? kitPath : null } };
|
|
58
60
|
}
|
|
59
|
-
/**
|
|
61
|
+
/** Floor check: the console's requirement is a MINIMUM within the same major (kit packages bump minors often; 0.25.1 must satisfy "^0.24.0").
|
|
62
|
+
Written as ^a.b.c for familiarity, evaluated as >= a.b.c and < (a+1).0.0 — NOT strict caret semantics for 0.x. */
|
|
60
63
|
export function satisfies(v, range) {
|
|
61
64
|
if (!v)
|
|
62
65
|
return null;
|
|
63
66
|
const m = range.match(/^\^(\d+)\.(\d+)\.(\d+)$/);
|
|
64
67
|
if (!m)
|
|
65
68
|
return null;
|
|
66
|
-
const [a, b, c] = v.split(".").map(
|
|
69
|
+
const [a, b, c] = v.split(".").map((n) => parseInt(n, 10));
|
|
67
70
|
const [x, y, z] = m.slice(1).map(Number);
|
|
71
|
+
if (![a, b, c].every(Number.isFinite))
|
|
72
|
+
return null;
|
|
68
73
|
if (a !== x)
|
|
69
|
-
return false;
|
|
70
|
-
|
|
71
|
-
return false;
|
|
72
|
-
return a > x || b > y || (b === y && c >= z);
|
|
74
|
+
return false; // different major: not satisfied (breaking either way)
|
|
75
|
+
return b > y || (b === y && c >= z);
|
|
73
76
|
}
|
|
74
|
-
export function requiresReport(kitPath) {
|
|
77
|
+
export function requiresReport(kitPath, caps) {
|
|
75
78
|
const inst = installedVersions(kitPath);
|
|
76
79
|
const check = (k) => ({ required: REQUIRES[k], installed: inst[k], ok: satisfies(inst[k], REQUIRES[k]) });
|
|
77
|
-
const r = { seam: check("seam"), mcp: check("mcp"), groundwork: check("groundwork"), playbook: check("playbook") };
|
|
80
|
+
const r = { seam: check("seam"), mcp: { ...check("mcp"), capabilities: caps ?? { inReplyTo: null, sendCommand: null }, warnings: caps?.inReplyTo === false ? ["inReplyTo missing — decision replies will not be linked/cleared; upgrade agent-coord-mcp"] : [] }, groundwork: check("groundwork"), playbook: check("playbook") };
|
|
78
81
|
return { ...r, paths: inst.paths, ok: Object.values(r).every((x) => x.ok !== false) };
|
|
79
82
|
}
|
|
80
83
|
/** Package root dir for a kit package: the checkout's packages/<dir> if present, else the installed npm package (found from its bin). Null when neither exists. */
|
package/dist/mcp.js
CHANGED
|
@@ -14,7 +14,7 @@ const IDENTITIES = {
|
|
|
14
14
|
};
|
|
15
15
|
export const AGENT_ID = "groundwork-console";
|
|
16
16
|
export const READ_ONLY_TOOLS = new Set(["doctor", "list_agents", "list_rooms", "list_scopes", "status", "ping", "read_messages", "retrieve_room_history", "retrieve_message", "list_work"]);
|
|
17
|
-
export const WRITE_TOOLS_GATED = new Set(["send_message", "post_status", "prune", "force_unregister", "unregister", "join_room", "leave_room", "delete_room", "set_room_topic", "set_room_motd", "import_work", "export_work"]);
|
|
17
|
+
export const WRITE_TOOLS_GATED = new Set(["send_message", "send_command", "post_status", "prune", "force_unregister", "unregister", "join_room", "leave_room", "delete_room", "set_room_topic", "set_room_motd", "import_work", "export_work"]);
|
|
18
18
|
const SWITCH_FILE = path.join(homedir(), ".groundwork-console", "switches.json");
|
|
19
19
|
function loadSwitches() {
|
|
20
20
|
let saved = {};
|
|
@@ -74,12 +74,17 @@ export class BusClient {
|
|
|
74
74
|
const reg = await client.callTool({ name: "register", arguments: { agentId: id.agentId, project: id.project, role: id.role, force: true } });
|
|
75
75
|
log?.(`register → ${JSON.stringify(reg.structuredContent ?? textOf(reg)).slice(0, 160)}`);
|
|
76
76
|
// Feature detection: which args each tool accepts (e.g. send_message.inReplyTo landed in kit 18ffbf4; the live bin may predate it).
|
|
77
|
+
let sendCommandDesc = "";
|
|
77
78
|
try {
|
|
78
79
|
const tools = await client.listTools();
|
|
79
|
-
for (const t of tools.tools)
|
|
80
|
+
for (const t of tools.tools) {
|
|
80
81
|
this.toolArgs.set(t.name, new Set(Object.keys(t.inputSchema?.properties ?? {})));
|
|
82
|
+
if (t.name === "send_command")
|
|
83
|
+
sendCommandDesc = t.description ?? "";
|
|
84
|
+
}
|
|
81
85
|
}
|
|
82
86
|
catch { /* older server: no detection, pass nothing extra */ }
|
|
87
|
+
this.commands = await detectCommands(client, sendCommandDesc, log);
|
|
83
88
|
this.client = client;
|
|
84
89
|
return client;
|
|
85
90
|
})();
|
|
@@ -91,8 +96,14 @@ export class BusClient {
|
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
98
|
toolArgs = new Map();
|
|
99
|
+
commands = null;
|
|
100
|
+
/** send_command allowlist detected on the connected server (e.g. ["/clear","/compact"] on 0.24.0, +"/reload-skills" on 0.25.0); null = not connected / undetectable. */
|
|
101
|
+
get sendCommands() { return this.client ? this.commands : null; }
|
|
102
|
+
/** True when the connected server accepts this raw control command. */
|
|
103
|
+
acceptsCommand(cmd) { return this.sendCommands?.includes(cmd) ?? false; }
|
|
94
104
|
/** True when the connected server's tool schema lists this argument (false when unknown or not connected yet). */
|
|
95
105
|
supports(tool, arg) { return this.toolArgs.get(tool)?.has(arg) ?? false; }
|
|
106
|
+
get connected() { return !!this.client; }
|
|
96
107
|
async call(name, args, log) {
|
|
97
108
|
const isWrite = WRITE_TOOLS_GATED.has(name);
|
|
98
109
|
if (!READ_ONLY_TOOLS.has(name) && !isWrite)
|
|
@@ -108,6 +119,34 @@ export class BusClient {
|
|
|
108
119
|
}
|
|
109
120
|
async close() { await this.client?.close(); this.client = null; }
|
|
110
121
|
}
|
|
122
|
+
/* send_command allowlist detection. The MCP does not expose the allowlist in the tool schema (command is a plain string), so:
|
|
123
|
+
1. probe once per connection with a command that can never be valid — the server rejects it BEFORE looking at to/room or touching any pane
|
|
124
|
+
(transport.ts sendCommandTool: normalizeControlCommand → `unsupported command '…'. Allowed: /clear, /compact[, /reload-skills]`), so this is
|
|
125
|
+
side-effect-free and needs no bus-writes gate; 2. fall back to the tool description ("allowlist is locked to /clear, /compact and /reload-skills"). */
|
|
126
|
+
const PROBE_COMMAND = "/__groundwork-console-probe__";
|
|
127
|
+
async function detectCommands(client, description, log) {
|
|
128
|
+
const fromText = (t) => { const m = t.match(/Allowed:\s*([^\n]+)/i) ?? t.match(/locked to ([^.;\n]+)/i); return m ? [...m[1].matchAll(/\/[a-z][\w-]*/g)].map((x) => x[0]) : []; };
|
|
129
|
+
try {
|
|
130
|
+
const res = await client.callTool({ name: "send_command", arguments: { from: "groundwork-console", command: PROBE_COMMAND } });
|
|
131
|
+
const out = (res.structuredContent ?? parseText(textOf(res)));
|
|
132
|
+
const list = fromText(typeof out === "string" ? out : out?.error ?? textOf(res));
|
|
133
|
+
if (list.length) {
|
|
134
|
+
log?.(`send_command allowlist (probe): ${list.join(" ")}`);
|
|
135
|
+
return list;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
const list = fromText(e.message ?? "");
|
|
140
|
+
if (list.length)
|
|
141
|
+
return list;
|
|
142
|
+
}
|
|
143
|
+
const list = fromText(description);
|
|
144
|
+
if (list.length) {
|
|
145
|
+
log?.(`send_command allowlist (description): ${list.join(" ")}`);
|
|
146
|
+
return list;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
111
150
|
function textOf(r) { return (r.content ?? []).map((c) => c.text ?? "").join("\n"); }
|
|
112
151
|
function parseText(t) { try {
|
|
113
152
|
return JSON.parse(t);
|
package/dist/provision.js
CHANGED
|
@@ -25,6 +25,30 @@ async function fleetWindow(project, startDir, log) {
|
|
|
25
25
|
log(`created window ${win} in session ${session} (first pane ${pane} reused for the first agent — no spare shell)`);
|
|
26
26
|
return { target: `${session}:${win}`, freshPane: pane };
|
|
27
27
|
}
|
|
28
|
+
const PROMPT_GLYPH = { claude: /^\s*[❯>]\s?/, grok: /^\s*[❯>›]\s?/, codex: /^\s*[❯>›]\s?/, opencode: /^\s*[❯>›]\s?/ };
|
|
29
|
+
/** Was `line` submitted? Decided from the INPUT BOX only — the last prompt-glyph line framed by a rule above it (Claude Code always frames the box; other harnesses: within the last 4 lines) —
|
|
30
|
+
never from transcript echoes ("❯ <submitted text>" followed by output). Stuck ⇔ the input still holds the typed text, or a slash popup is open under it. Pure; unit-tested in scripts/test-inject.mjs. */
|
|
31
|
+
export function inputSubmitted(captureText, harness, line) {
|
|
32
|
+
const lines = captureText.split("\n").map((l) => l.replace(/\u00a0/g, " ").replace(/\s+$/, ""));
|
|
33
|
+
const isRule = (l) => !!l && /^\s*[─━-]{10,}\s*$/.test(l);
|
|
34
|
+
const needle = line.slice(0, Math.min(line.length, 40));
|
|
35
|
+
let idx = -1;
|
|
36
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
37
|
+
if (!PROMPT_GLYPH[harness].test(lines[i]))
|
|
38
|
+
continue;
|
|
39
|
+
if (isRule(lines[i - 1]) || (harness !== "claude" && i >= lines.length - 4)) {
|
|
40
|
+
idx = i;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
break;
|
|
45
|
+
} // Claude Code always frames its box with a rule; an unframed ❯ line is a transcript echo
|
|
46
|
+
if (idx < 0)
|
|
47
|
+
return true; // no input box visible → nothing is sitting in the input
|
|
48
|
+
const inputBlock = lines.slice(idx, Math.min(lines.length, idx + 3)).filter((l) => !isRule(l)).join(" "); // the box can wrap
|
|
49
|
+
const popup = /\/[a-z][\w-]*\s{2,}\S/.test(lines.slice(idx + 1, idx + 7).join(" ")); // slash popup rows: "/coord-worker Act as…"
|
|
50
|
+
return !inputBlock.includes(needle) && !popup;
|
|
51
|
+
}
|
|
28
52
|
export async function spawnAgent(req, isRegistered, log, isAttached = () => true, roleOf = () => null) {
|
|
29
53
|
if (!/^[a-z0-9][a-z0-9-_]*$/i.test(req.agentId))
|
|
30
54
|
throw new Error("agentId: letters, digits, - _");
|
|
@@ -44,14 +68,78 @@ export async function spawnAgent(req, isRegistered, log, isAttached = () => true
|
|
|
44
68
|
await tmux("send-keys", "-t", paneId, "C-m");
|
|
45
69
|
log(`launched: ${cmd}`);
|
|
46
70
|
const wait = Math.min(Math.max(req.waitSeconds ?? 8, 3), 60);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
71
|
+
const capture = async () => { try {
|
|
72
|
+
return await tmux("capture-pane", "-p", "-t", paneId, "-S", "-40");
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return "";
|
|
76
|
+
} };
|
|
77
|
+
const paneAlive = async () => { try {
|
|
78
|
+
const out = await tmux("list-panes", "-t", target, "-F", "#{pane_id}\t#{pane_dead}");
|
|
79
|
+
return out.split("\n").some((l) => l.startsWith(paneId + "\t0"));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
} };
|
|
84
|
+
// (1) Ready-prompt wait: poll the pane until the harness input prompt is visible (bounded by waitSeconds). Claude Code: "? for shortcuts" / the "›" input box; others: a prompt glyph on the last lines.
|
|
85
|
+
const READY = { claude: /\? for shortcuts|bypass permissions|esc to interrupt|Welcome to Claude Code|Claude Code v\d/i, grok: /grok|›/i, codex: /codex|›/i, opencode: /opencode|›/i }; // never the bare shell prompt: an early inject would type into zsh
|
|
86
|
+
const t0 = Date.now();
|
|
87
|
+
let ready = false;
|
|
88
|
+
while (Date.now() - t0 < wait * 1000) {
|
|
89
|
+
if (!(await paneAlive()))
|
|
90
|
+
break;
|
|
91
|
+
if (READY[req.harness].test(await capture())) {
|
|
92
|
+
ready = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
await sleep(700);
|
|
96
|
+
}
|
|
97
|
+
log(ready ? `harness prompt visible after ${((Date.now() - t0) / 1000).toFixed(1)}s` : `⚠ no harness prompt seen within ${wait}s — injecting anyway`);
|
|
98
|
+
if (!(await paneAlive()))
|
|
99
|
+
throw new Error(`pane ${paneId} died before the seed (harness failed to start?) — check the window ${target}`);
|
|
100
|
+
// (2) Verified injection: type the line, settle, C-m, then confirm the typed text left the input; on a stuck slash popup (Claude Code eats the first C-m while the command menu is open) → Escape + C-m, up to 3 tries.
|
|
101
|
+
// "Submitted" is decided from the INPUT LINE only (the line starting with the harness prompt glyph), never from transcript echoes: Claude Code echoes the
|
|
102
|
+
// submitted command in the transcript, and an Escape sent on a false "stuck" would interrupt the running turn (skill boot / registration).
|
|
103
|
+
const submitted = async (line) => inputSubmitted(await capture(), req.harness, line);
|
|
104
|
+
const inject = async (line) => {
|
|
105
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
106
|
+
if (!(await paneAlive()))
|
|
107
|
+
throw new Error(`✗ inject failed: pane died — "${line}"`);
|
|
108
|
+
if (attempt === 1) {
|
|
109
|
+
await tmux("send-keys", "-t", paneId, "-l", line);
|
|
110
|
+
await sleep(450);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
await tmux("send-keys", "-t", paneId, "Escape");
|
|
114
|
+
await sleep(250);
|
|
115
|
+
}
|
|
116
|
+
await tmux("send-keys", "-t", paneId, "C-m");
|
|
117
|
+
const d = Date.now() + 5_000;
|
|
118
|
+
let ok = false;
|
|
119
|
+
while (Date.now() < d) {
|
|
120
|
+
await sleep(400);
|
|
121
|
+
if (await submitted(line)) {
|
|
122
|
+
ok = true;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (ok) {
|
|
127
|
+
log(`injected${attempt > 1 ? ` (attempt ${attempt})` : ""}: ${line}`);
|
|
128
|
+
await sleep(500);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
log(`… "${line.slice(0, 50)}" still in the input after C-m (attempt ${attempt}) — ${attempt < 3 ? "Escape + retry" : "giving up"}`);
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`✗ inject failed after 3 tries: "${line}" — the harness did not accept the line; seed aborted (registration wait skipped)`);
|
|
134
|
+
};
|
|
50
135
|
if (req.model && CLAUDE_HARNESS_ONLY.has(req.harness))
|
|
51
136
|
await inject(`/model ${req.model}`);
|
|
52
137
|
if (CLAUDE_HARNESS_ONLY.has(req.harness))
|
|
53
138
|
await inject(`/color ${ROLE_COLOR[req.role]}`);
|
|
54
|
-
|
|
139
|
+
// Skill line: the common coord-* signature (kit b84e14c): --id --role --project --room [--rooms] --repo [--lane]. Cards accept it once the machine's playbook is updated from the kit; older cards ignore unknown flags.
|
|
140
|
+
const roleId = `coord-${req.role === "coordinator" ? "coordinator" : req.role}`;
|
|
141
|
+
const skill = `${ROLE_SKILL[req.role]} --id=${req.agentId} --role=${roleId} --project=${req.project} --room=${req.room ?? req.project} --repo=${req.repoPath}${req.lane ? ` --lane=${req.lane}` : ""}`;
|
|
142
|
+
log(`seed (common coord-* signature, kit b84e14c): ${skill}`);
|
|
55
143
|
await inject(CLAUDE_HARNESS_ONLY.has(req.harness) ? skill : `Act per the ${ROLE_SKILL[req.role].slice(1)} playbook card: ${skill}${req.note ? ". " + req.note : ""}`);
|
|
56
144
|
if (req.note && CLAUDE_HARNESS_ONLY.has(req.harness))
|
|
57
145
|
await inject(req.note);
|
|
@@ -63,6 +151,8 @@ export async function spawnAgent(req, isRegistered, log, isAttached = () => true
|
|
|
63
151
|
registered = true;
|
|
64
152
|
break;
|
|
65
153
|
}
|
|
154
|
+
if (!(await paneAlive()))
|
|
155
|
+
throw new Error(`✗ pane ${paneId} died while waiting for ${req.agentId} to register — seed aborted`);
|
|
66
156
|
await sleep(2_000);
|
|
67
157
|
}
|
|
68
158
|
log(registered ? `✓ ${req.agentId} registered on the bus` : `⚠ ${req.agentId} not registered after 90s — check the pane`);
|