@davidbalzan/groundwork-console 0.1.11 → 0.1.13
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/LICENSE +19 -0
- package/dist/bus.js +54 -3
- package/dist/events.js +1 -1
- package/dist/gitinfo.js +42 -2
- package/dist/panes.js +86 -0
- package/dist/provision.js +47 -7
- package/dist/seam.js +27 -3
- package/dist/seamwrite.js +5 -1
- package/dist/server.js +10 -10
- package/dist/tmux.js +1 -1
- package/package.json +11 -5
- package/public/assets/{abnfDiagram-N423BO3Z-SmC0YbkZ.js → abnfDiagram-N423BO3Z-2g9UHdfw.js} +1 -1
- package/public/assets/{arc-Bwss940w.js → arc-EEINoaIY.js} +1 -1
- package/public/assets/architecture-TIHT7OUA-CFm6TN4T.js +1 -0
- package/public/assets/{architectureDiagram-T3A2C74G-Cq88dj_x.js → architectureDiagram-T3A2C74G-D4F9rIiu.js} +1 -1
- package/public/assets/{blockDiagram-VBNYF7ZC-Cr4QI7D6.js → blockDiagram-VBNYF7ZC-CQZAhVXz.js} +1 -1
- package/public/assets/{c4Diagram-5PPSVZJV-C1C4eJIx.js → c4Diagram-5PPSVZJV-BlBzSIXs.js} +1 -1
- package/public/assets/channel-CHLtdsMA.js +1 -0
- package/public/assets/{chunk-2GRJ4B5K-Cpp7ECx6.js → chunk-2GRJ4B5K-ZVZxINuZ.js} +1 -1
- package/public/assets/{chunk-4I5QYGJK-Bps2fbU8.js → chunk-4I5QYGJK-8hqVPXHL.js} +1 -1
- package/public/assets/{chunk-5RXB4S5H-3UxSwRGQ.js → chunk-5RXB4S5H-D1s6L_89.js} +1 -1
- package/public/assets/{chunk-6Q2QTUOP-zawcmdf9.js → chunk-6Q2QTUOP-BWLeyKxS.js} +1 -1
- package/public/assets/{chunk-7Z6QIM7H-Dc0octNy.js → chunk-7Z6QIM7H-DjvOr6wh.js} +1 -1
- package/public/assets/{chunk-GF5L2VYU-RHui1NJm.js → chunk-GF5L2VYU-BCFrJAqb.js} +1 -1
- package/public/assets/{chunk-I66GZJ75-BlHb3oSv.js → chunk-I66GZJ75-XdTCBJhL.js} +1 -1
- package/public/assets/{chunk-JQJVKLGR-WIlvhJ6X.js → chunk-JQJVKLGR-BwEUHZBv.js} +1 -1
- package/public/assets/{chunk-KBJHAD2P-Dr1rLwdT.js → chunk-KBJHAD2P-HjBUqR8F.js} +1 -1
- package/public/assets/{chunk-NSK5VX7P-B07rzjwZ.js → chunk-NSK5VX7P-DkHwHrKE.js} +1 -1
- package/public/assets/{chunk-QR6OTTB3-ComyizSZ.js → chunk-QR6OTTB3-DF1_EWFH.js} +1 -1
- package/public/assets/{chunk-RYQCIY6F-k_etiI3Q.js → chunk-RYQCIY6F-BaBhRLGF.js} +1 -1
- package/public/assets/{chunk-UBXNYLIW-DldfSHAq.js → chunk-UBXNYLIW-BNV3VrZA.js} +1 -1
- package/public/assets/{chunk-W5SLKNZC-Dn5B1pXH.js → chunk-W5SLKNZC-5ymyg6B_.js} +1 -1
- package/public/assets/{chunk-WRU74C26-D34jYzyt.js → chunk-WRU74C26-oznLCI2s.js} +1 -1
- package/public/assets/{chunk-XXDRQBXY-BQXENxTm.js → chunk-XXDRQBXY-sauNIYLc.js} +1 -1
- package/public/assets/classDiagram-JCYQIIEL-Wf8lPn9O.js +1 -0
- package/public/assets/classDiagram-v2-OCEON4UE-Wf8lPn9O.js +1 -0
- package/public/assets/{cose-bilkent-JH36ORCC-CPGou1DN.js → cose-bilkent-JH36ORCC-B0A4Je-o.js} +1 -1
- package/public/assets/{cynefin-VYW2F7L2-DOI3jeNv.js → cynefin-VYW2F7L2-BtmksekA.js} +1 -1
- package/public/assets/{cynefinDiagram-MW4NZA55-DoW8nM2s.js → cynefinDiagram-MW4NZA55-DxlB0XWy.js} +1 -1
- package/public/assets/{dagre-VZM6K2ZE-Dq8ZHjT7.js → dagre-VZM6K2ZE-BwX2jQEP.js} +1 -1
- package/public/assets/{diagram-7IWD3JNH-3FSn8Agr.js → diagram-7IWD3JNH-CRw4eKmV.js} +1 -1
- package/public/assets/{diagram-B4RE2ZJO-BnXqbr3I.js → diagram-B4RE2ZJO-BhSHl_HC.js} +1 -1
- package/public/assets/{diagram-LBJQPF4R-B9WLcX1P.js → diagram-LBJQPF4R-BLNDbOa-.js} +1 -1
- package/public/assets/{diagram-Q27KOJAE-Cqc0NnFi.js → diagram-Q27KOJAE-OI-WVr2z.js} +1 -1
- package/public/assets/{diagram-UB23O5K3-D3DVcxz0.js → diagram-UB23O5K3-Dw4v-DRV.js} +1 -1
- package/public/assets/{dist-D9Nkbm8B.js → dist-CkCHUIyI.js} +1 -1
- package/public/assets/{ebnfDiagram-BXEA7PRR-FHf_rewb.js → ebnfDiagram-BXEA7PRR-_CCtytYQ.js} +1 -1
- package/public/assets/{erDiagram-JOGREHBK-CvPTNoqX.js → erDiagram-JOGREHBK-DPwktZ67.js} +1 -1
- package/public/assets/eventmodeling-45OFAUF4-COOjM2XZ.js +1 -0
- package/public/assets/flowDiagram-UKHOOZJN-ipIqOWay.js +1 -0
- package/public/assets/{ganttDiagram-PKOTCBZU-DWUBSW9B.js → ganttDiagram-PKOTCBZU-ChHOAX0R.js} +1 -1
- package/public/assets/{gitGraph-TEB2WS4Q-CjymOfrj.js → gitGraph-TEB2WS4Q-DTKjAplS.js} +1 -1
- package/public/assets/{gitGraphDiagram-DS77QQ5N-BZa0uJBE.js → gitGraphDiagram-DS77QQ5N-CetQtIP1.js} +1 -1
- package/public/assets/{index-CS6PrVvt.js → index-CMeHHiB8.js} +11 -11
- package/public/assets/{info-DKCQHKI2-DugyQgAo.js → info-DKCQHKI2-CEZZoshL.js} +1 -1
- package/public/assets/{infoDiagram-6WML65LV-DGlBHx3m.js → infoDiagram-6WML65LV-Dn0Ci1fh.js} +1 -1
- package/public/assets/{ishikawaDiagram-WSZJBQD7-BbqOG9Eg.js → ishikawaDiagram-WSZJBQD7-C0yvCWyv.js} +1 -1
- package/public/assets/{journeyDiagram-NVQOT4AX-Du3qH9mS.js → journeyDiagram-NVQOT4AX-CpTSt3oh.js} +1 -1
- package/public/assets/{kanban-definition-27J2QSJJ-CdVgOFzk.js → kanban-definition-27J2QSJJ-Co4MhtWb.js} +1 -1
- package/public/assets/{line-CvLqalqu.js → line-CKdpDvRy.js} +1 -1
- package/public/assets/{linear-ChXKEHCZ.js → linear-BAY9SBNB.js} +1 -1
- package/public/assets/{mermaid-parser.core-C7RowpcN.js → mermaid-parser.core-B-yKcreq.js} +3 -3
- package/public/assets/{mermaid.core-U0Q7d-gI.js → mermaid.core-CW_jKbCL.js} +4 -4
- package/public/assets/{mindmap-definition-FAOFIHXS-Dd8IqZN3.js → mindmap-definition-FAOFIHXS-CejNgzQn.js} +1 -1
- package/public/assets/{packet-7NZHBO7P-rnlOmuHK.js → packet-7NZHBO7P-B53cyWv4.js} +1 -1
- package/public/assets/{pegDiagram-VL7TDLO6-C61XZFPE.js → pegDiagram-VL7TDLO6-DAh-rohm.js} +1 -1
- package/public/assets/{pie-RZYD4A2V-BosRLnUR.js → pie-RZYD4A2V-Bbl6CzP-.js} +1 -1
- package/public/assets/{pieDiagram-7S7Q4E2Y-C066dHaW.js → pieDiagram-7S7Q4E2Y-BoiaUX85.js} +1 -1
- package/public/assets/{quadrantDiagram-CIZ2JOQS-BiL2JG5p.js → quadrantDiagram-CIZ2JOQS-2EgkGrDN.js} +1 -1
- package/public/assets/{radar-I7S5WNFK-VRvamsSS.js → radar-I7S5WNFK-1Piyk_Oc.js} +1 -1
- package/public/assets/{railroad-3IZDKUUU-_8QphIQF.js → railroad-3IZDKUUU-DcQG7hTA.js} +1 -1
- package/public/assets/railroad-abnf-AHOZXSZD-CaviTNom.js +1 -0
- package/public/assets/railroad-ebnf-EBAXGLYW-K7l-WN0j.js +1 -0
- package/public/assets/railroad-peg-LSFZ7HO6-Bu22X3mO.js +1 -0
- package/public/assets/{railroadDiagram-AXF67PYL-BO0K2FSG.js → railroadDiagram-AXF67PYL-kPPDLWet.js} +1 -1
- package/public/assets/{requirementDiagram-LRYGKXZP-Cs5N-_5S.js → requirementDiagram-LRYGKXZP-B0wNE6wF.js} +1 -1
- package/public/assets/{sankeyDiagram-W5VNT64P-BBadz3Xa.js → sankeyDiagram-W5VNT64P-CyKEUx9w.js} +1 -1
- package/public/assets/{sequenceDiagram-SI44F4Z6-B1wwHKdi.js → sequenceDiagram-SI44F4Z6-DT7UmiRF.js} +1 -1
- package/public/assets/{src-rNJ0obFg.js → src-EbLSa_z9.js} +1 -1
- package/public/assets/{stateDiagram-OKZ733FA-D2BPoAkk.js → stateDiagram-OKZ733FA-BNk8YOH4.js} +1 -1
- package/public/assets/stateDiagram-v2-UEYNNEHI-8Oafa2Rm.js +1 -0
- package/public/assets/{swimlanes-SLNWSIFB-CJw_VUg-.js → swimlanes-SLNWSIFB-B7eUL04h.js} +1 -1
- package/public/assets/swimlanesDiagram-ULZ7WXOC-BT5Ebr7W.js +8 -0
- package/public/assets/{timeline-definition-Z64GVDOM-8uI7Mh2Y.js → timeline-definition-Z64GVDOM-CDLh-h94.js} +1 -1
- package/public/assets/{treeView-QDETBFTQ-DX9MBPTO.js → treeView-QDETBFTQ-BAscBSG9.js} +1 -1
- package/public/assets/{treemap-6X3UGDF4-DqOdI7Cz.js → treemap-6X3UGDF4-B2ahLgfF.js} +1 -1
- package/public/assets/{vennDiagram-T6HMQDX7-Dv3hlyhI.js → vennDiagram-T6HMQDX7-62WQ_264.js} +1 -1
- package/public/assets/{wardley-OPB4EBWU-BlsdtYE0.js → wardley-OPB4EBWU-DUrXideD.js} +1 -1
- package/public/assets/{wardleyDiagram-T6FBY63Y-8UP_r-p8.js → wardleyDiagram-T6FBY63Y-Ci4DF612.js} +1 -1
- package/public/assets/{xychartDiagram-ELKLHX3M-CqKePl1c.js → xychartDiagram-ELKLHX3M-D--O5thj.js} +1 -1
- package/public/index.html +1 -1
- package/public/assets/architecture-TIHT7OUA-DxbIc2EO.js +0 -1
- package/public/assets/channel-q2MiI2jw.js +0 -1
- package/public/assets/classDiagram-JCYQIIEL-BBCC64Pk.js +0 -1
- package/public/assets/classDiagram-v2-OCEON4UE-BBCC64Pk.js +0 -1
- package/public/assets/eventmodeling-45OFAUF4-B3k39kpe.js +0 -1
- package/public/assets/flowDiagram-UKHOOZJN-J20LnFn0.js +0 -1
- package/public/assets/railroad-abnf-AHOZXSZD-ySvBeK5p.js +0 -1
- package/public/assets/railroad-ebnf-EBAXGLYW-B08LkBeL.js +0 -1
- package/public/assets/railroad-peg-LSFZ7HO6-CnEVYMxD.js +0 -1
- package/public/assets/stateDiagram-v2-UEYNNEHI-CsMDmX-c.js +0 -1
- package/public/assets/swimlanesDiagram-ULZ7WXOC-D83SR1DB.js +0 -8
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 David Balzan
|
|
2
|
+
|
|
3
|
+
All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software and its accompanying files are proprietary. No licence is
|
|
6
|
+
granted to use, copy, modify, merge, publish, distribute, sublicense, or sell
|
|
7
|
+
copies of this software, in whole or in part, except by a separate written
|
|
8
|
+
agreement signed by the copyright holder.
|
|
9
|
+
|
|
10
|
+
Possession of these files — including by download from a public package
|
|
11
|
+
registry — does not constitute such an agreement and grants no rights.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
package/dist/bus.js
CHANGED
|
@@ -19,6 +19,54 @@ const tailJsonl = (p, limit) => {
|
|
|
19
19
|
return { malformed: true, raw: l };
|
|
20
20
|
} });
|
|
21
21
|
};
|
|
22
|
+
/* Thresholds are judgement, not measurement, and are stated here rather than buried: "producing"
|
|
23
|
+
is the working cadence, and >8h is the shape of an overnight stall. The 2h boundary exists
|
|
24
|
+
because the previous model put a coffee break and a seventeen-hour stall in one bucket. */
|
|
25
|
+
const PRODUCING_MS = 2 * 3_600_000;
|
|
26
|
+
const STALLED_MS = 8 * 3_600_000;
|
|
27
|
+
const SCAN_LINES = 4000;
|
|
28
|
+
/** Newest emission per agent, from the room logs and the status stream. */
|
|
29
|
+
export function outputIndex(root, scanLines = SCAN_LINES) {
|
|
30
|
+
const out = new Map();
|
|
31
|
+
const note = (from, ts, basis) => {
|
|
32
|
+
if (typeof from !== "string" || typeof ts !== "number")
|
|
33
|
+
return;
|
|
34
|
+
const prev = out.get(from);
|
|
35
|
+
if (!prev || ts > prev.ts)
|
|
36
|
+
out.set(from, { ts, basis });
|
|
37
|
+
};
|
|
38
|
+
const roomDir = path.join(root, "rooms");
|
|
39
|
+
if (existsSync(roomDir))
|
|
40
|
+
for (const f of readdirSync(roomDir).filter((x) => x.endsWith(".jsonl"))) {
|
|
41
|
+
for (const m of tailJsonl(path.join(roomDir, f), scanLines))
|
|
42
|
+
note(m.from, m.ts, "room");
|
|
43
|
+
}
|
|
44
|
+
for (const m of tailJsonl(path.join(root, "status.jsonl"), scanLines))
|
|
45
|
+
note(m.agentId ?? m.from, m.ts, "status");
|
|
46
|
+
/* DMs count as output and are NOT in the room logs — they are rows in the RECIPIENT's inbox,
|
|
47
|
+
carrying the sender in `from`. Omitting them made an agent working entirely by DM read as
|
|
48
|
+
stalled; that was caught on the first live run, where this console's own worker showed
|
|
49
|
+
`stalled` while it had been reporting by DM all morning. Scanning inboxes attributes each
|
|
50
|
+
message to its sender, not its owner. */
|
|
51
|
+
const inboxDir = path.join(root, "inbox");
|
|
52
|
+
if (existsSync(inboxDir))
|
|
53
|
+
for (const f of readdirSync(inboxDir).filter((x) => x.endsWith(".jsonl"))) {
|
|
54
|
+
for (const m of tailJsonl(path.join(inboxDir, f), scanLines))
|
|
55
|
+
note(m.from, m.ts, "dm");
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/* `unknown` is NOT "no output" — it is "none within the scanned window", and the window is reported
|
|
60
|
+
so a caller can tell the two apart. Collapsing them would be the same defect one level down. */
|
|
61
|
+
const progressOf = (hit, now, scanLines) => {
|
|
62
|
+
if (!hit)
|
|
63
|
+
return { lastOutputAt: null, ageMs: null, state: "unknown", basis: null, scannedLines: scanLines };
|
|
64
|
+
const ageMs = now - hit.ts;
|
|
65
|
+
return { lastOutputAt: hit.ts, ageMs, state: ageMs < PRODUCING_MS ? "producing" : ageMs < STALLED_MS ? "quiet" : "stalled", basis: hit.basis, scannedLines: scanLines };
|
|
66
|
+
};
|
|
67
|
+
/* Reachability buckets. `idle` used to span 15 MINUTES to 24 HOURS, so a coffee break and an
|
|
68
|
+
overnight stall rendered identically; `dormant` splits that. */
|
|
69
|
+
export const reachOf = (age) => age < 15 * 60_000 ? "live" : age < 2 * 3_600_000 ? "idle" : age < 24 * 3_600_000 ? "dormant" : "dead";
|
|
22
70
|
export async function agentsAsync(root) {
|
|
23
71
|
const panes = await listPanes();
|
|
24
72
|
const base = agentsSync(root);
|
|
@@ -33,11 +81,14 @@ export async function agentsAsync(root) {
|
|
|
33
81
|
const paneActivity = ct === undefined || ct === null ? windowActivity : ct || null; // 0 = seeded, no change observed yet → treated as unknown, not as "now"
|
|
34
82
|
const lastSeen = Math.max(a.lastSeen ?? 0, paneActivity ?? 0);
|
|
35
83
|
const age = lastSeen ? Date.now() - lastSeen : Infinity;
|
|
36
|
-
|
|
84
|
+
/* paneActivity is another REACHABILITY input, not progress: a pane redraws when a message is pasted into
|
|
85
|
+
it. It is deliberately not folded into `progress`, which stays as agentsSync computed it. */
|
|
86
|
+
return { ...a, paneActivity, windowActivity, windowName: pane?.windowName ?? null, paneCommand: pane?.command ?? null, paneAlive: pane ? !pane.dead : null, lastSeen: lastSeen || null, heartbeatAgeMs: Number.isFinite(age) ? age : null, reachability: reachOf(age), active: paneActivity ? Date.now() - paneActivity < 60_000 : false };
|
|
37
87
|
}).sort((a, b) => (b.lastSeen ?? 0) - (a.lastSeen ?? 0));
|
|
38
88
|
}
|
|
39
89
|
export const agents = agentsSync;
|
|
40
|
-
export function agentsSync(root) {
|
|
90
|
+
export function agentsSync(root, scanLines = SCAN_LINES) {
|
|
91
|
+
const output = outputIndex(root, scanLines);
|
|
41
92
|
const reg = readJson(path.join(root, "agents.json"), {});
|
|
42
93
|
const transports = existsSync(path.join(root, "transports")) ? new Set(readdirSync(path.join(root, "transports")).map((f) => f.replace(/\.json$/, ""))) : new Set();
|
|
43
94
|
const now = Date.now();
|
|
@@ -48,7 +99,7 @@ export function agentsSync(root) {
|
|
|
48
99
|
const mtimes = [tPath, path.join(root, "inbox", `${a.agentId}.jsonl`), path.join(root, "cursors", `${a.agentId}.json`)].filter(existsSync).map((f) => statSync(f).mtimeMs);
|
|
49
100
|
const lastSeen = Math.max(a.lastHeartbeat ?? 0, ...mtimes);
|
|
50
101
|
const age = lastSeen ? now - lastSeen : Infinity;
|
|
51
|
-
return { ...a, transport: t?.transport ?? t?.kind ?? (t ? "tmux-push" : null), pane: t?.tmuxTarget ?? null,
|
|
102
|
+
return { ...a, transport: t?.transport ?? t?.kind ?? (t ? "tmux-push" : null), pane: t?.tmuxTarget ?? null, reachability: reachOf(age), heartbeatAgeMs: Number.isFinite(age) ? age : null, lastSeen: lastSeen || null, progress: progressOf(output.get(a.agentId), now, scanLines) };
|
|
52
103
|
}).sort((a, b) => (b.lastSeen ?? 0) - (a.lastSeen ?? 0));
|
|
53
104
|
}
|
|
54
105
|
export function rooms(root) {
|
package/dist/events.js
CHANGED
|
@@ -103,7 +103,7 @@ export class EventEngine {
|
|
|
103
103
|
}
|
|
104
104
|
/** The live coordinator agent for a project/room (role coordinator, project == room or name), else null. */
|
|
105
105
|
coordinatorFor(project, room) {
|
|
106
|
-
const a = agents(this.cfg.coordDir).find((x) => /coordinator/i.test(x.role ?? "") && (x.project === room || x.project === project) && x.
|
|
106
|
+
const a = agents(this.cfg.coordDir).find((x) => /coordinator/i.test(x.role ?? "") && (x.project === room || x.project === project) && x.reachability !== "dead");
|
|
107
107
|
return a?.agentId ?? null;
|
|
108
108
|
}
|
|
109
109
|
emitRaw(type, project, data) {
|
package/dist/gitinfo.js
CHANGED
|
@@ -139,6 +139,46 @@ async function ghPrs(projectPath) {
|
|
|
139
139
|
return null;
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
/* Which PR a board row is ABOUT, from its `pr` CELL — never from a `#n` appearing anywhere in the
|
|
143
|
+
row. The openPrs extension declares fields ["repo","pr","owner","status","mergeGate"], so the
|
|
144
|
+
answer has a designated column and reading the others is a guess.
|
|
145
|
+
|
|
146
|
+
The old matcher was `Object.values(r).some(v => /#n/.test(v))`, which meant a Status cell reading
|
|
147
|
+
"blocked by #39" made THAT row answer for PR 39 — and `find` returns the first hit, so a mention
|
|
148
|
+
beat the real row. Measured on a two-row fixture: qaOf(39) returned "FAIL — held" from #57's row
|
|
149
|
+
while #39's own row said PASS. A passing PR shown as failing, in the alarming direction.
|
|
150
|
+
|
|
151
|
+
Same defect the kit found in queue-done-loop the same afternoon: a ref MENTIONED in a row is a
|
|
152
|
+
different fact from the ref that IDENTIFIES it. */
|
|
153
|
+
export const prCellNumber = (cell) => {
|
|
154
|
+
const m = /#(\d+)\b/.exec(cell ?? "");
|
|
155
|
+
return m ? Number(m[1]) : null;
|
|
156
|
+
};
|
|
157
|
+
/* `owner/name` from a remote URL, ssh or https. null when there is no origin — in which case the
|
|
158
|
+
repo check below is SKIPPED rather than guessed, and matching falls back to the pr cell alone. */
|
|
159
|
+
export const slugFromRemoteUrl = (url) => {
|
|
160
|
+
const m = url ? /(?:[:/])([^/:]+\/[^/]+?)(?:\.git)?$/.exec(url.trim()) : null;
|
|
161
|
+
return m ? m[1] : null;
|
|
162
|
+
};
|
|
163
|
+
const originSlug = async (projectPath) => slugFromRemoteUrl(await git(projectPath, ["remote", "get-url", "origin"]));
|
|
164
|
+
/* A row's repo, when it states one. Boards legitimately carry several repos — the kit's own board
|
|
165
|
+
lists `kit#11` beside console PRs — so a bare number is ambiguous across them. Only rows that
|
|
166
|
+
name a DIFFERENT repo are rejected; a row with no repo cell is accepted, because absence is not
|
|
167
|
+
a claim. */
|
|
168
|
+
/* The gate a board reports for PR `n` in THIS repo, or undefined. Exported and used by
|
|
169
|
+
gitBranches directly, so the test exercises this function rather than a copy of it — a test that
|
|
170
|
+
re-implements the matcher pins the fixture, not the code. */
|
|
171
|
+
export const gateForPr = (rows, n, slug) => {
|
|
172
|
+
const row = rows?.find((r) => prCellNumber(r.pr) === n && repoMatches(r.repo, slug));
|
|
173
|
+
const qa = row?.mergeGate ?? row?.status;
|
|
174
|
+
return qa ? String(qa) : undefined;
|
|
175
|
+
};
|
|
176
|
+
export const repoMatches = (rowRepo, slug) => {
|
|
177
|
+
const cell = (rowRepo ?? "").trim();
|
|
178
|
+
if (!cell || !slug)
|
|
179
|
+
return true;
|
|
180
|
+
return cell === slug || cell.endsWith(`/${slug.split("/").pop()}`) || slug.endsWith(`/${cell}`) || cell === slug.split("/").pop();
|
|
181
|
+
};
|
|
142
182
|
export function gitBranches(projectPath, hints = {}) {
|
|
143
183
|
return cached(`branches:${projectPath}:${(hints.agentIds ?? []).join(",")}`, async () => {
|
|
144
184
|
const defaultBranch = "main";
|
|
@@ -147,8 +187,8 @@ export function gitBranches(projectPath, hints = {}) {
|
|
|
147
187
|
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
188
|
.filter(([ref]) => !/^refs\/remotes\/[^/]+\/HEAD$/.test(ref));
|
|
149
189
|
const [{ worktrees }, prs] = await Promise.all([gitWorktrees(projectPath, hints), ghPrs(projectPath)]);
|
|
150
|
-
const
|
|
151
|
-
|
|
190
|
+
const slug = await originSlug(projectPath);
|
|
191
|
+
const qaOf = (n) => gateForPr(hints.openPrs, n, slug);
|
|
152
192
|
const now = Date.now();
|
|
153
193
|
const branches = await Promise.all(rows.map(async ([ref, sha, short, subject, date]) => {
|
|
154
194
|
const remote = ref.startsWith("refs/remotes/");
|
package/dist/panes.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* Which tmux panes this console created.
|
|
2
|
+
*
|
|
3
|
+
* Window reuse used to be decided from `#{pane_title}`: every title in the window
|
|
4
|
+
* had to start with `<project>-`. The console DOES set those titles at spawn — and
|
|
5
|
+
* then the harness overwrites them with the conversation topic, so the check was
|
|
6
|
+
* correct for about a minute after a spawn and wrong forever after. Measured
|
|
7
|
+
* 2026-08-27: all three `groundwork-kit-*` windows on this machine were refused
|
|
8
|
+
* with intact, correct window names, and David hand-spawned three times.
|
|
9
|
+
*
|
|
10
|
+
* The fix is not a better title pattern. `pane_title` is a field ANOTHER PROGRAM
|
|
11
|
+
* owns; any regex over it is a guess about that program's output, and the next
|
|
12
|
+
* harness release changes the answer. This module records the pane ids the console
|
|
13
|
+
* itself created — identity it controls and the harness cannot touch.
|
|
14
|
+
*
|
|
15
|
+
* Pane ids are unique within a tmux server but RESET when the server restarts, so a
|
|
16
|
+
* stale id could collide with an unrelated new pane. The store records the server's
|
|
17
|
+
* pid and is discarded wholesale when it changes; a wrong-server record is worth
|
|
18
|
+
* less than no record.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
const FILE = path.join(homedir(), ".groundwork-console", "panes.json");
|
|
24
|
+
function load(serverPid) {
|
|
25
|
+
try {
|
|
26
|
+
if (existsSync(FILE)) {
|
|
27
|
+
const j = JSON.parse(readFileSync(FILE, "utf8"));
|
|
28
|
+
// A server restart resets pane ids; keeping the old map would let a recycled
|
|
29
|
+
// id read as ours. Drop it rather than carry a record we cannot trust.
|
|
30
|
+
if (j && j.serverPid === serverPid && j.owned)
|
|
31
|
+
return j;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* unreadable store: treat as empty — never as "everything is ours" */
|
|
36
|
+
}
|
|
37
|
+
return { serverPid, owned: {} };
|
|
38
|
+
}
|
|
39
|
+
function save(s) {
|
|
40
|
+
try {
|
|
41
|
+
mkdirSync(path.dirname(FILE), { recursive: true });
|
|
42
|
+
writeFileSync(FILE, JSON.stringify(s));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* disk unavailable: ownership degrades to "unknown", which REFUSES reuse.
|
|
46
|
+
That is the safe direction — it costs a manual spawn, not an adopted window. */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Record a pane this console just created for `project`. */
|
|
50
|
+
export function rememberPane(serverPid, project, paneId) {
|
|
51
|
+
const s = load(serverPid);
|
|
52
|
+
const list = s.owned[project] ?? [];
|
|
53
|
+
if (!list.includes(paneId))
|
|
54
|
+
list.push(paneId);
|
|
55
|
+
s.owned[project] = list;
|
|
56
|
+
save(s);
|
|
57
|
+
}
|
|
58
|
+
/** Forget panes that no longer exist, so the store does not grow without bound. */
|
|
59
|
+
export function prunePanes(serverPid, alive) {
|
|
60
|
+
const s = load(serverPid);
|
|
61
|
+
let changed = false;
|
|
62
|
+
for (const [proj, ids] of Object.entries(s.owned)) {
|
|
63
|
+
const keep = ids.filter((id) => alive.has(id));
|
|
64
|
+
if (keep.length !== ids.length) {
|
|
65
|
+
changed = true;
|
|
66
|
+
if (keep.length)
|
|
67
|
+
s.owned[proj] = keep;
|
|
68
|
+
else
|
|
69
|
+
delete s.owned[proj];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (changed)
|
|
73
|
+
save(s);
|
|
74
|
+
}
|
|
75
|
+
export function ownedPanes(serverPid, project) {
|
|
76
|
+
return new Set(load(serverPid).owned[project] ?? []);
|
|
77
|
+
}
|
|
78
|
+
/** Every pane id this console owns, across all projects — used to tell "another
|
|
79
|
+
fleet's pane" (a real collision) from "a pane nobody here created". */
|
|
80
|
+
export function allOwnedPanes(serverPid) {
|
|
81
|
+
const out = new Map();
|
|
82
|
+
for (const [proj, ids] of Object.entries(load(serverPid).owned))
|
|
83
|
+
for (const id of ids)
|
|
84
|
+
out.set(id, proj);
|
|
85
|
+
return out;
|
|
86
|
+
}
|
package/dist/provision.js
CHANGED
|
@@ -9,6 +9,7 @@ import { execFile } from "node:child_process";
|
|
|
9
9
|
import { promisify } from "node:util";
|
|
10
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
|
+
import { rememberPane, prunePanes, ownedPanes, allOwnedPanes } from "./panes.js";
|
|
12
13
|
const run = promisify(execFile);
|
|
13
14
|
const tmux = async (...args) => (await run("tmux", args, { timeout: 8_000 })).stdout.trim();
|
|
14
15
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -18,17 +19,52 @@ export const ROLE_COLOR = { coordinator: "green", aide: "pink", qa: "yellow", ci
|
|
|
18
19
|
const CLAUDE_HARNESS_ONLY = new Set(["claude"]); // /color and slash-skills are Claude Code; other harnesses get the skill text as a plain instruction
|
|
19
20
|
/** Window name for a role: workers share one window, singleton roles get their own (exact scheme `<project>-<role>` / `<project>-workers` — cross-fleet contract, no owner infix). */
|
|
20
21
|
export const roleWindowName = (project, role) => (role === "worker" ? `${project}-workers` : `${project}-${role}`);
|
|
22
|
+
/** Refusal reason for reusing a window, or null when it is ours.
|
|
23
|
+
*
|
|
24
|
+
* Pure so both directions are testable without tmux — and the second direction is the
|
|
25
|
+
* one that must not regress: a window we do NOT own stays refused. Ownership is pane
|
|
26
|
+
* ids the console created; `#{pane_title}` is deliberately not an input, because the
|
|
27
|
+
* harness rewrites it with the conversation topic and any pattern over it is a guess
|
|
28
|
+
* about another program's output. */
|
|
29
|
+
export function windowOwnership(a) {
|
|
30
|
+
const foreign = a.paneIds.filter((id) => !a.mine.has(id));
|
|
31
|
+
if (!foreign.length)
|
|
32
|
+
return null;
|
|
33
|
+
// Distinguish the two cases: the remedies differ, and the old message prescribed the
|
|
34
|
+
// wrong one — it said "rename or close it" when the window NAME was already correct,
|
|
35
|
+
// sending a reader to rename a correctly-named window.
|
|
36
|
+
const otherFleet = foreign.map((id) => a.everyone.get(id)).find((p) => p && p !== a.project);
|
|
37
|
+
if (otherFleet) {
|
|
38
|
+
return `window "${a.win}" (${a.target}) holds pane(s) this console created for project "${otherFleet}" — a real name collision. Close that window, or spawn under a different project name.`;
|
|
39
|
+
}
|
|
40
|
+
return (`window "${a.win}" (${a.target}) exists but this console did not create pane(s) ${foreign.join(", ")}, so it cannot claim them. ` +
|
|
41
|
+
`The window NAME is correct — do NOT rename it. Close the window (tmux kill-window -t ${a.target}) and respawn, or close just those panes. ` +
|
|
42
|
+
`Ownership is tracked by pane id, not by pane title: a title that reads like a conversation topic is expected and is not the problem.`);
|
|
43
|
+
}
|
|
21
44
|
async function roleWindow(project, role, startDir, log) {
|
|
22
45
|
const win = roleWindowName(project, role);
|
|
23
46
|
const wins = (await tmux("list-windows", "-a", "-F", "#{session_name}:#{window_index}\t#{window_name}")).split("\n").filter(Boolean).map((l) => l.split("\t"));
|
|
24
47
|
const hit = wins.find(([, name]) => name === win);
|
|
25
48
|
if (hit) {
|
|
26
|
-
// Ownership check before reuse
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
49
|
+
// Ownership check before reuse. The requirement is unchanged (contract amendment
|
|
50
|
+
// 11:46): a name-collision with panes we did not create is REFUSED, never adopted.
|
|
51
|
+
// What changed is the EVIDENCE. This read `#{pane_title}` and required every title
|
|
52
|
+
// to start with `<project>-`; the console does set those titles at spawn and the
|
|
53
|
+
// harness then overwrites them with the conversation topic, so the check was right
|
|
54
|
+
// for about a minute and wrong afterwards — measured 2026-08-27, all three
|
|
55
|
+
// groundwork-kit windows refused with intact window names.
|
|
56
|
+
//
|
|
57
|
+
// Titles are a field another program owns; a better regex over them is a guess
|
|
58
|
+
// about that program's output. Ownership is now the pane ids the console itself
|
|
59
|
+
// created (./panes.ts), which the harness cannot touch.
|
|
60
|
+
const serverPid = await tmux("display-message", "-p", "#{pid}");
|
|
61
|
+
const paneIds = (await tmux("list-panes", "-t", hit[0], "-F", "#{pane_id}")).split("\n").filter(Boolean);
|
|
62
|
+
prunePanes(serverPid, new Set((await tmux("list-panes", "-a", "-F", "#{pane_id}")).split("\n").filter(Boolean)));
|
|
63
|
+
const mine = ownedPanes(serverPid, project);
|
|
64
|
+
const everyone = allOwnedPanes(serverPid);
|
|
65
|
+
const verdict = windowOwnership({ win, target: hit[0], project, paneIds, mine, everyone });
|
|
66
|
+
if (verdict)
|
|
67
|
+
throw new Error(verdict);
|
|
32
68
|
log(`window ${win} exists (${hit[0]}) — ${role === "worker" ? "splitting another worker pane into it" : `reusing for a second ${role} spawn (split)`}`);
|
|
33
69
|
return { target: hit[0], freshPane: null };
|
|
34
70
|
}
|
|
@@ -74,7 +110,11 @@ export async function spawnAgent(req, isRegistered, log, isAttached = () => true
|
|
|
74
110
|
if (!freshPane)
|
|
75
111
|
await tmux("select-layout", "-t", target, "tiled");
|
|
76
112
|
await tmux("select-pane", "-t", paneId, "-T", req.agentId);
|
|
77
|
-
|
|
113
|
+
// Record the pane as ours BEFORE anything can fail below: ownership is about who
|
|
114
|
+
// created it, not whether the launch succeeded. A pane we made and then abandoned
|
|
115
|
+
// is still ours to reuse, and forgetting it is what blocks the window forever.
|
|
116
|
+
rememberPane(await tmux("display-message", "-p", "#{pid}"), req.project, paneId);
|
|
117
|
+
log(`pane ${paneId} in ${target} (cwd ${req.repoPath}), title ${req.agentId} (recorded as owned; title is cosmetic)`);
|
|
78
118
|
const cmd = HARNESS_CMD[req.harness];
|
|
79
119
|
await tmux("send-keys", "-t", paneId, "-l", cmd);
|
|
80
120
|
await tmux("send-keys", "-t", paneId, "C-m");
|
package/dist/seam.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Read-only views over a project's seam files, parsed ONLY through @davidbalzan/groundwork-seam. */
|
|
2
2
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { parseWorkDoc, queueItemsOf, doneEntriesOf, listWorkBoardOf, workDocIssues, SCHEMA } from "@davidbalzan/groundwork-seam";
|
|
4
|
+
import { parseWorkDoc, queueItemsOf, doneEntriesOf, listWorkBoardOf, workDocIssues, boardSchemaOf, SCHEMA } from "@davidbalzan/groundwork-seam";
|
|
5
5
|
import { parseFactsDoc } from "@davidbalzan/groundwork-seam";
|
|
6
6
|
const read = (p) => (existsSync(p) ? readFileSync(p, "utf8") : null);
|
|
7
7
|
const FACT_STALE_DAYS = 14; // groundwork doctor rule
|
|
@@ -13,7 +13,22 @@ export function boardOf(projectPath) {
|
|
|
13
13
|
const f = read(path.join(docs, "FACTS.md"));
|
|
14
14
|
const wsDoc = ws ? parseWorkDoc(ws) : null;
|
|
15
15
|
const board = wsDoc ? listWorkBoardOf(wsDoc) : [];
|
|
16
|
-
|
|
16
|
+
/* Schema comes from the seam's boardSchemaOf, not from a local re-derivation.
|
|
17
|
+
The console used to work this out itself, and console#35 fixed the first defect in that
|
|
18
|
+
copy: it read the schema off the first parsed ROW, so an empty-but-valid board yielded
|
|
19
|
+
null and rendered "unknown workstreams grammar" — the alarming reading of an empty one.
|
|
20
|
+
The seam now exports the answer, and its version is better than ours on two counts we
|
|
21
|
+
had wrong:
|
|
22
|
+
- it EXCLUDES extension tables by deriving their ids from WORKSTREAMS_EXT, so
|
|
23
|
+
registering a new extension cannot forget to exclude it. Ours allow-listed the two
|
|
24
|
+
board grammars we knew, which meant any FUTURE board schema read as unrecognised.
|
|
25
|
+
- it returns "unknown" only when a header was actually REFUSED, and null when there
|
|
26
|
+
is simply no board block. Ours collapsed those: `schema ?? (ws ? "unknown" : null)`
|
|
27
|
+
reported "unknown grammar" for a WORKSTREAMS.md that merely has no Active Streams
|
|
28
|
+
table — the same conflation console#35 fixed, one case over, still live until now.
|
|
29
|
+
So `null` here means "no board table", NOT "no file"; the caller must not render those
|
|
30
|
+
as the same thing. `files.workstreams` is what distinguishes them. */
|
|
31
|
+
const schema = wsDoc ? boardSchemaOf(wsDoc) : null;
|
|
17
32
|
const qDoc = q ? parseWorkDoc(q) : null;
|
|
18
33
|
const dDoc = d ? parseWorkDoc(d) : null;
|
|
19
34
|
const facts = f ? parseFactsDoc(f) : null;
|
|
@@ -24,9 +39,18 @@ export function boardOf(projectPath) {
|
|
|
24
39
|
return {
|
|
25
40
|
files: { workstreams: !!ws, queue: !!q, done: !!d, facts: !!f },
|
|
26
41
|
prd,
|
|
27
|
-
workstreams: { schema
|
|
42
|
+
workstreams: { schema, rows: board, issues: wsDoc ? workDocIssues(wsDoc) : [] },
|
|
28
43
|
queue: qDoc ? queueItemsOf(qDoc) : [],
|
|
29
44
|
done: dDoc ? doneEntriesOf(dDoc) : [],
|
|
45
|
+
/* QUEUE.md and DONE.md parse issues. Only WORKSTREAMS.md's were ever surfaced, so a malformed
|
|
46
|
+
queue line was parsed, kept, rendered — and reported NOWHERE. That gap is the console's half
|
|
47
|
+
of seam 0.1.6: the seam stopped DROPPING an out-of-range priority precisely so a consumer
|
|
48
|
+
could say something about it, and this consumer said nothing. `workDocIssues` already carries
|
|
49
|
+
the message (the seam builds it with invalidPriorityIssue), so nothing here re-derives it.
|
|
50
|
+
Siblings rather than a reshape of `queue`/`done`: those are arrays every caller maps over,
|
|
51
|
+
and the gap is a missing signal, not a wrong shape. */
|
|
52
|
+
queueIssues: qDoc ? workDocIssues(qDoc) : [],
|
|
53
|
+
doneIssues: dDoc ? workDocIssues(dDoc) : [],
|
|
30
54
|
facts: facts
|
|
31
55
|
? {
|
|
32
56
|
entries: facts.entries.map((e) => {
|
package/dist/seamwrite.js
CHANGED
|
@@ -65,7 +65,11 @@ export function addQueueItem(projectPath, item, log) {
|
|
|
65
65
|
log(`wrote docs/QUEUE.md (+1 item, ${out.length - src.length} bytes)`);
|
|
66
66
|
return { file: "docs/QUEUE.md", line: renderWorkDocLine(newItem), bytesDelta: out.length - src.length };
|
|
67
67
|
}
|
|
68
|
-
|
|
68
|
+
/* Display-only echo of the line we appended. `QueueItem.priority` is nullable since seam 0.1.6, and
|
|
69
|
+
interpolating a null renders the literal "(null)" — so fall back to the raw tag the seam kept. The
|
|
70
|
+
console never CREATES such an item (addQueueItem's input is narrowed to P1|P2|P3); this guards the
|
|
71
|
+
echo against a shape the type now permits. */
|
|
72
|
+
const renderWorkDocLine = (q) => `- [${q.done ? "x" : " "}] (${q.priority ?? q.priorityRaw ?? "?"}) ${q.text}`;
|
|
69
73
|
/** Re-verify (or add) a fact: seam upsertFact with a fresh timestamp. */
|
|
70
74
|
export function reverifyFact(projectPath, entry, log) {
|
|
71
75
|
const file = path.join(projectPath, "docs/FACTS.md");
|
package/dist/server.js
CHANGED
|
@@ -48,7 +48,7 @@ const clientFor = (id) => (id === "david" ? busDavid : bus);
|
|
|
48
48
|
/** Capabilities of the connected MCP (null until a client has connected and listed tools). */
|
|
49
49
|
const mcpCaps = () => { const cl = bus.connected ? bus : busDavid.connected ? busDavid : null; return { inReplyTo: cl ? cl.supports("send_message", "inReplyTo") : null, sendCommand: cl ? cl.sendCommands : null }; };
|
|
50
50
|
/* Live tmux-attached agents — the only valid send_command targets (the MCP hard-gates to tmux-push/-remote). */
|
|
51
|
-
const liveAttached = () => agents(cfg.coordDir).filter((a) => a.
|
|
51
|
+
const liveAttached = () => agents(cfg.coordDir).filter((a) => a.reachability === "live" && (a.transport === "tmux-push" || a.transport === "tmux-push-remote")).map((a) => a.agentId);
|
|
52
52
|
const PUBLIC_DIR = path.resolve(HERE, "../public");
|
|
53
53
|
const root = new Hono();
|
|
54
54
|
const app = new Hono().basePath("/api");
|
|
@@ -286,7 +286,7 @@ app.get("/projects", (c) => {
|
|
|
286
286
|
catch {
|
|
287
287
|
return null;
|
|
288
288
|
} })();
|
|
289
|
-
const liveAgents = agents(cfg.coordDir).filter((a) => a.
|
|
289
|
+
const liveAgents = agents(cfg.coordDir).filter((a) => a.reachability !== "dead");
|
|
290
290
|
return c.json(cfg.projects.map((p) => {
|
|
291
291
|
const b = boardOf(p.path);
|
|
292
292
|
const fleet = fleetStatus(liveAgents.filter((a) => a.project === p.room || a.project === p.name));
|
|
@@ -723,7 +723,7 @@ app.get("/projects/:name/fleet", async (c) => {
|
|
|
723
723
|
const p = project(c.req.param("name"));
|
|
724
724
|
if (!p)
|
|
725
725
|
return c.json({ error: "unknown project" }, 404);
|
|
726
|
-
const live = (await agentsAsync(cfg.coordDir)).filter((a) => a.
|
|
726
|
+
const live = (await agentsAsync(cfg.coordDir)).filter((a) => a.reachability !== "dead" && (a.project === p.room || a.project === p.name));
|
|
727
727
|
return c.json({ ...fleetStatus(live), templates: FLEET_TEMPLATES, window: `fleet-${p.name}` });
|
|
728
728
|
});
|
|
729
729
|
const roleOfAgent = (id) => agents(cfg.coordDir).find((a) => a.agentId === id)?.role ?? null;
|
|
@@ -788,7 +788,7 @@ app.post("/actions/prune", async (c) => {
|
|
|
788
788
|
});
|
|
789
789
|
app.post("/actions/unregister-dead", async (c) => {
|
|
790
790
|
const b = (await c.req.json().catch(() => ({})));
|
|
791
|
-
const dead = agents(cfg.coordDir).filter((a) => a.
|
|
791
|
+
const dead = agents(cfg.coordDir).filter((a) => a.reachability === "dead" && !["groundwork-console", "david"].includes(a.agentId)).map((a) => a.agentId);
|
|
792
792
|
const ids = (b.agentIds?.length ? b.agentIds.filter((id) => dead.includes(id)) : dead);
|
|
793
793
|
if (b.confirm !== "UNREGISTER")
|
|
794
794
|
return c.json({ error: "confirm must be UNREGISTER", wouldUnregister: ids }, 400);
|
|
@@ -810,7 +810,7 @@ app.post("/actions/unregister", async (c) => {
|
|
|
810
810
|
return c.json(r, r.status === "ok" ? 200 : 403);
|
|
811
811
|
});
|
|
812
812
|
/* ---- Roadmap actions: message the project's aide (plan) or coordinator (kick off) as david ---- */
|
|
813
|
-
const roleAgent = (projectName, room, roleRe) => agents(cfg.coordDir).find((a) => roleRe.test(a.role ?? "") && (a.project === room || a.project === projectName) && a.
|
|
813
|
+
const roleAgent = (projectName, room, roleRe) => agents(cfg.coordDir).find((a) => roleRe.test(a.role ?? "") && (a.project === room || a.project === projectName) && a.reachability !== "dead")?.agentId ?? null;
|
|
814
814
|
app.get("/projects/:name/agents", (c) => {
|
|
815
815
|
const p = project(c.req.param("name"));
|
|
816
816
|
if (!p)
|
|
@@ -855,7 +855,7 @@ app.post("/actions/task/:name/:phase/:line", async (c) => {
|
|
|
855
855
|
return c.json({ error: "no task at that line" }, 404);
|
|
856
856
|
if (task.done)
|
|
857
857
|
return c.json({ error: "already ticked" }, 409);
|
|
858
|
-
const live = agents(cfg.coordDir).filter((a) => a.
|
|
858
|
+
const live = agents(cfg.coordDir).filter((a) => a.reachability !== "dead");
|
|
859
859
|
const owner = b.owner && live.some((a) => a.agentId === b.owner) ? b.owner : roleAgent(p.name, p.room, /aide|liaison|curator/i);
|
|
860
860
|
if (!owner)
|
|
861
861
|
return c.json({ error: `no live owner or aide for ${p.name}` }, 409);
|
|
@@ -897,7 +897,7 @@ app.get("/projects/:name/doctor/fixers", async (c) => {
|
|
|
897
897
|
const p = project(c.req.param("name"));
|
|
898
898
|
if (!p)
|
|
899
899
|
return c.json({ error: "unknown project" }, 404);
|
|
900
|
-
const live = (await agentsAsync(cfg.coordDir)).filter((a) => a.
|
|
900
|
+
const live = (await agentsAsync(cfg.coordDir)).filter((a) => a.reachability !== "dead" && (a.project === p.room || a.project === p.name));
|
|
901
901
|
const out = {};
|
|
902
902
|
for (const [check, o] of Object.entries(DOCTOR_OWNER)) {
|
|
903
903
|
const a = live.find((x) => o.re.test(x.role ?? ""));
|
|
@@ -913,7 +913,7 @@ app.post("/actions/doctor-fix/:name", async (c) => {
|
|
|
913
913
|
const o = DOCTOR_OWNER[b.check];
|
|
914
914
|
if (!o)
|
|
915
915
|
return c.json({ error: "no fixer role for this check" }, 400);
|
|
916
|
-
const live = (await agentsAsync(cfg.coordDir)).filter((a) => a.
|
|
916
|
+
const live = (await agentsAsync(cfg.coordDir)).filter((a) => a.reachability !== "dead" && (a.project === p.room || a.project === p.name));
|
|
917
917
|
const target = live.find((x) => o.re.test(x.role ?? ""))?.agentId;
|
|
918
918
|
if (!target)
|
|
919
919
|
return c.json({ error: `no live ${o.role} for ${p.name}` }, 409);
|
|
@@ -1036,9 +1036,9 @@ app.get("/bus/panes", async (c) => {
|
|
|
1036
1036
|
const lines = Number(c.req.query("lines") ?? 40);
|
|
1037
1037
|
const p = proj ? project(proj) : undefined;
|
|
1038
1038
|
const all = await agentsAsync(cfg.coordDir);
|
|
1039
|
-
const scoped = all.filter((a) => a.pane && a.
|
|
1039
|
+
const scoped = all.filter((a) => a.pane && a.reachability !== "dead" && (!p || a.project === p.name || (p.room && a.project === p.room)));
|
|
1040
1040
|
const colors = c.req.query("colors") === "1";
|
|
1041
|
-
const out = await Promise.all(scoped.map(async (a) => ({ agentId: a.agentId, role: a.role ?? null, project: a.project ?? null, pane: a.pane, windowName: a.windowName ?? null, command: a.paneCommand, active: a.active, paneActivity: a.paneActivity,
|
|
1041
|
+
const out = await Promise.all(scoped.map(async (a) => ({ agentId: a.agentId, role: a.role ?? null, project: a.project ?? null, pane: a.pane, windowName: a.windowName ?? null, command: a.paneCommand, active: a.active, paneActivity: a.paneActivity, reachability: a.reachability, capture: await capturePane(a.pane, lines, colors) })));
|
|
1042
1042
|
return c.json(out);
|
|
1043
1043
|
});
|
|
1044
1044
|
/* Pane viewer: read-only capture of an agent's tmux pane (never sends keys). */
|
package/dist/tmux.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Read-only tmux views: pane activity (for
|
|
1
|
+
/* Read-only tmux views: pane activity (for reachability + activity indicators) and pane capture (for the pane viewer). Never sends keys. */
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
const run = promisify(execFile);
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davidbalzan/groundwork-console",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.13",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@davidbalzan/groundwork-seam": "
|
|
7
|
+
"@davidbalzan/groundwork-seam": "0.1.7",
|
|
8
8
|
"@hono/node-server": "^2.1.1",
|
|
9
9
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
10
10
|
"hono": "^4.13.2",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"typescript": "^7.0.2"
|
|
17
17
|
},
|
|
18
18
|
"description": "Groundwork Console — the cockpit for Groundwork Kit fleets: board, bus, monitor, docs, ops. One local server, token auth, tmux-aware.",
|
|
19
|
-
"license": "
|
|
19
|
+
"license": "UNLICENSED",
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
22
|
"url": "git+https://github.com/davidbalzan/groundwork-console.git"
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"public",
|
|
31
31
|
"bin",
|
|
32
32
|
"groundwork-console.rules.json",
|
|
33
|
-
"README.md"
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
34
35
|
],
|
|
35
36
|
"engines": {
|
|
36
37
|
"node": ">=22"
|
|
@@ -47,6 +48,11 @@
|
|
|
47
48
|
"publish-check": "node scripts/publish-check.mjs",
|
|
48
49
|
"test:inject": "tsx test/inject.test.mts",
|
|
49
50
|
"test:skills": "tsx test/skills.test.mts",
|
|
50
|
-
"test": "pnpm run test:inject && pnpm run test:skills"
|
|
51
|
+
"test": "pnpm run test:inject && pnpm run test:skills && pnpm run test:board && pnpm run test:panes && pnpm run test:seam-contract && pnpm run test:prref && pnpm run test:progress",
|
|
52
|
+
"test:board": "tsx test/board.test.mts",
|
|
53
|
+
"test:panes": "tsx test/panes.test.mts",
|
|
54
|
+
"test:seam-contract": "tsx test/seam-contract.test.mts",
|
|
55
|
+
"test:prref": "tsx test/prref.test.mts",
|
|
56
|
+
"test:progress": "tsx test/progress.test.mts"
|
|
51
57
|
}
|
|
52
58
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{m as e,t}from"./mermaid-parser.core-
|
|
1
|
+
import{m as e,t}from"./mermaid-parser.core-B-yKcreq.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-EbLSa_z9.js";import"./chunk-I66GZJ75-XdTCBJhL.js";import{n as i,r as a,t as o}from"./chunk-6Q2QTUOP-BWLeyKxS.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-CW_jKbCL.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-CkCHUIyI.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-B-yKcreq.js";export{e as createArchitectureServices};
|