@tekyzinc/gsd-t 3.18.13 → 3.19.0
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/CHANGELOG.md +114 -0
- package/bin/gsd-t-parallel-probe.cjs +132 -0
- package/bin/gsd-t-parallel.cjs +422 -9
- package/bin/gsd-t-task-graph.cjs +80 -19
- package/bin/gsd-t-unattended.cjs +634 -229
- package/bin/gsd-t-worker-dispatch.cjs +211 -0
- package/bin/headless-auto-spawn.cjs +44 -1
- package/bin/headless-exit-codes.cjs +36 -18
- package/bin/m44-proof-measure.cjs +285 -0
- package/bin/m46-iter-proof.cjs +149 -0
- package/bin/m46-worker-proof.cjs +201 -0
- package/bin/parallelism-report.cjs +535 -0
- package/bin/spawn-plan-writer.cjs +1 -1
- package/commands/gsd-t-debug.md +10 -14
- package/commands/gsd-t-execute.md +10 -16
- package/commands/gsd-t-help.md +1 -0
- package/commands/gsd-t-integrate.md +8 -14
- package/commands/gsd-t-quick.md +10 -14
- package/commands/gsd-t-resume.md +32 -0
- package/commands/gsd-t-status.md +10 -0
- package/commands/gsd-t-unattended-watch.md +58 -1
- package/commands/gsd-t-visualize.md +15 -12
- package/commands/gsd-t-wave.md +2 -11
- package/docs/architecture.md +82 -0
- package/docs/requirements.md +20 -0
- package/package.json +1 -1
- package/scripts/gsd-t-compact-detector.js +51 -8
- package/scripts/gsd-t-dashboard-server.js +138 -85
- package/scripts/gsd-t-transcript.html +152 -1
- package/scripts/gsd-t-update-check.js +13 -4
- package/scripts/hooks/gsd-t-conversation-capture.js +258 -0
- package/templates/CLAUDE-global.md +54 -0
|
@@ -176,102 +176,37 @@ function isValidSpawnId(id) {
|
|
|
176
176
|
return typeof id === "string" && /^[a-zA-Z0-9._-]+$/.test(id) && id.length <= 200;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
function handleTranscriptsList(req, res, projectDir) {
|
|
179
|
+
function handleTranscriptsList(req, res, projectDir, transcriptHtmlPath) {
|
|
180
180
|
const idx = readTranscriptsIndex(projectDir);
|
|
181
181
|
const sorted = idx.spawns
|
|
182
182
|
.slice()
|
|
183
183
|
.sort((a, b) => (Date.parse(b.startedAt) || 0) - (Date.parse(a.startedAt) || 0));
|
|
184
184
|
|
|
185
185
|
// Content negotiation: browser navigations send Accept: text/html, fetch()
|
|
186
|
-
// defaults to */*.
|
|
187
|
-
//
|
|
186
|
+
// defaults to */*. For text/html we serve the viewer (same HTML as
|
|
187
|
+
// /transcript/:id) with an empty spawn-id placeholder — the viewer's left
|
|
188
|
+
// rail populates from /api/spawns-index and the main pane defers until the
|
|
189
|
+
// user clicks a spawn. Programmatic clients (fetch's default */* or explicit
|
|
190
|
+
// application/json) continue to get the JSON shape the dashboard JS already
|
|
191
|
+
// consumes.
|
|
188
192
|
const accept = String(req.headers["accept"] || "");
|
|
189
|
-
if (accept.includes("text/html")) {
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
if (accept.includes("text/html") && transcriptHtmlPath) {
|
|
194
|
+
fs.readFile(transcriptHtmlPath, (err, data) => {
|
|
195
|
+
if (err) { res.writeHead(404); res.end("Transcript UI not found"); return; }
|
|
196
|
+
// Substitute the __SPAWN_ID__ placeholder with an empty string; the
|
|
197
|
+
// viewer's initialId logic falls through to location.hash (also empty)
|
|
198
|
+
// and connect('') is a no-op beyond a 404 SSE attempt — harmless, since
|
|
199
|
+
// the left rail polls /api/spawns-index independently.
|
|
200
|
+
const html = data.toString("utf8").replace(/__SPAWN_ID__/g, "");
|
|
201
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
202
|
+
res.end(html);
|
|
203
|
+
});
|
|
192
204
|
return;
|
|
193
205
|
}
|
|
194
206
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
195
207
|
res.end(JSON.stringify({ spawns: sorted }));
|
|
196
208
|
}
|
|
197
209
|
|
|
198
|
-
function renderTranscriptsHtml(spawns) {
|
|
199
|
-
const escape = (s) => String(s == null ? "" : s)
|
|
200
|
-
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
201
|
-
.replace(/"/g, """);
|
|
202
|
-
const fmtDuration = (a, b) => {
|
|
203
|
-
const start = Date.parse(a); const end = Date.parse(b || "") || Date.now();
|
|
204
|
-
if (!Number.isFinite(start)) return "—";
|
|
205
|
-
const ms = Math.max(0, end - start);
|
|
206
|
-
const s = Math.floor(ms / 1000); const m = Math.floor(s / 60); const r = s % 60;
|
|
207
|
-
return m > 0 ? `${m}m ${r}s` : `${r}s`;
|
|
208
|
-
};
|
|
209
|
-
const fmtTime = (s) => { const d = new Date(s); return Number.isFinite(d.getTime()) ? d.toLocaleString() : "—"; };
|
|
210
|
-
const liveStatuses = new Set(["initializing", "running"]);
|
|
211
|
-
const isLive = (s) => liveStatuses.has(s);
|
|
212
|
-
|
|
213
|
-
const rows = spawns.map((s) => {
|
|
214
|
-
const live = isLive(s.status);
|
|
215
|
-
const statusBadge = `<span class="status status-${escape(s.status || 'unknown')}">${escape(s.status || 'unknown')}</span>`;
|
|
216
|
-
return `<tr class="${live ? 'row-live' : ''}">
|
|
217
|
-
<td><a href="/transcript/${encodeURIComponent(s.spawnId)}">${escape(s.spawnId)}</a></td>
|
|
218
|
-
<td>${escape(s.command || '—')}</td>
|
|
219
|
-
<td>${escape(s.description || '—')}</td>
|
|
220
|
-
<td>${statusBadge}</td>
|
|
221
|
-
<td>${escape(fmtTime(s.startedAt))}</td>
|
|
222
|
-
<td>${escape(fmtDuration(s.startedAt, s.endedAt))}</td>
|
|
223
|
-
</tr>`;
|
|
224
|
-
}).join("");
|
|
225
|
-
|
|
226
|
-
const empty = `<div class="empty">
|
|
227
|
-
<h2>No spawn transcripts yet</h2>
|
|
228
|
-
<p>Transcripts appear here as soon as the first agent spawns. Run any GSD-T command (for example <code>/gsd-t-quick</code>) to generate one.</p>
|
|
229
|
-
<p><a href="/">← Back to dashboard</a></p>
|
|
230
|
-
</div>`;
|
|
231
|
-
|
|
232
|
-
const table = spawns.length ? `<table>
|
|
233
|
-
<thead><tr><th>Spawn ID</th><th>Command</th><th>Description</th><th>Status</th><th>Started</th><th>Duration</th></tr></thead>
|
|
234
|
-
<tbody>${rows}</tbody>
|
|
235
|
-
</table>` : empty;
|
|
236
|
-
|
|
237
|
-
return `<!DOCTYPE html>
|
|
238
|
-
<html lang="en"><head><meta charset="UTF-8"><title>GSD-T Transcripts</title>
|
|
239
|
-
<style>
|
|
240
|
-
:root{--bg:#0d1117;--surface:#161b22;--border:#30363d;--text:#e6edf3;--muted:#7d8590;
|
|
241
|
-
--green:#3fb950;--green-bg:#1a3a1e;--blue:#388bfd;--blue-bg:#1f3a5f;--yellow:#d29922;--red:#f85149;
|
|
242
|
-
--font:'SF Mono','Fira Code',Menlo,monospace;}
|
|
243
|
-
*{box-sizing:border-box;margin:0;padding:0}
|
|
244
|
-
body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:13px;padding:20px;line-height:1.5}
|
|
245
|
-
.hdr{display:flex;align-items:center;gap:12px;margin-bottom:18px;padding-bottom:12px;border-bottom:1px solid var(--border)}
|
|
246
|
-
.logo{color:var(--blue);font-weight:bold;font-size:14px}
|
|
247
|
-
.hright{margin-left:auto;color:var(--muted);font-size:11px}
|
|
248
|
-
a{color:var(--blue);text-decoration:none}a:hover{text-decoration:underline}
|
|
249
|
-
table{width:100%;border-collapse:collapse;background:var(--surface);border:1px solid var(--border);border-radius:6px;overflow:hidden}
|
|
250
|
-
th,td{padding:8px 12px;text-align:left;border-bottom:1px solid var(--border);font-size:12px}
|
|
251
|
-
th{background:#1c2128;color:var(--muted);text-transform:uppercase;font-size:10px;letter-spacing:0.5px}
|
|
252
|
-
tbody tr:last-child td{border-bottom:none}
|
|
253
|
-
tbody tr:hover{background:#1c2128}
|
|
254
|
-
tr.row-live{background:var(--green-bg)}
|
|
255
|
-
.status{display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;border:1px solid}
|
|
256
|
-
.status-running,.status-initializing{background:var(--green-bg);color:var(--green);border-color:var(--green)}
|
|
257
|
-
.status-done{background:var(--blue-bg);color:var(--blue);border-color:var(--blue)}
|
|
258
|
-
.status-stopped{background:#2a2a2a;color:var(--muted);border-color:var(--border)}
|
|
259
|
-
.status-failed,.status-crashed{background:#3a1a1a;color:var(--red);border-color:var(--red)}
|
|
260
|
-
.status-unknown{color:var(--muted);border-color:var(--border)}
|
|
261
|
-
.empty{text-align:center;padding:60px 20px;color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:6px}
|
|
262
|
-
.empty h2{color:var(--text);margin-bottom:12px;font-size:16px}
|
|
263
|
-
.empty p{margin-bottom:8px}
|
|
264
|
-
.empty code{background:#1c2128;padding:2px 6px;border-radius:3px;color:var(--green)}
|
|
265
|
-
</style></head><body>
|
|
266
|
-
<div class="hdr">
|
|
267
|
-
<span class="logo">GSD-T Transcripts</span>
|
|
268
|
-
<a href="/" style="font-size:11px">← Dashboard</a>
|
|
269
|
-
<span class="hright">${spawns.length} spawn${spawns.length === 1 ? '' : 's'}</span>
|
|
270
|
-
</div>
|
|
271
|
-
${table}
|
|
272
|
-
</body></html>`;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
210
|
function handleTranscriptPage(req, res, spawnId, transcriptHtmlPath) {
|
|
276
211
|
if (!isValidSpawnId(spawnId)) { res.writeHead(400); res.end("Invalid spawn id"); return; }
|
|
277
212
|
fs.readFile(transcriptHtmlPath, (err, data) => {
|
|
@@ -578,6 +513,116 @@ function listActiveSpawnPlans(projectDir) {
|
|
|
578
513
|
return listAllSpawnPlans(projectDir).filter((p) => p && p.endedAt == null);
|
|
579
514
|
}
|
|
580
515
|
|
|
516
|
+
// ── M44 D9 — Parallelism observability ──────────────────────────────────────
|
|
517
|
+
// Additive endpoints powered by bin/parallelism-report.cjs (v1.0.0 contract).
|
|
518
|
+
// Pure read-only observer; never writes, never spawns. 5-second per-response
|
|
519
|
+
// cache so rapid panel polls don't hammer the filesystem.
|
|
520
|
+
|
|
521
|
+
const PARALLELISM_CACHE_MS = 5000;
|
|
522
|
+
const _parallelismCache = { metrics: { at: 0, body: null, wave: null }, report: new Map() };
|
|
523
|
+
|
|
524
|
+
function _loadParallelismReporter() {
|
|
525
|
+
try {
|
|
526
|
+
// Resolve at call-time so tests that don't install the module don't break
|
|
527
|
+
// unrelated endpoints. Require is cached by Node after first success.
|
|
528
|
+
return require(path.join(__dirname, "..", "bin", "parallelism-report.cjs"));
|
|
529
|
+
} catch (err) {
|
|
530
|
+
return { _loadError: err && err.message || String(err) };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function handleParallelism(req, res, projectDir) {
|
|
535
|
+
const urlObj = req.url ? req.url.split("?") : ["", ""];
|
|
536
|
+
const qs = urlObj[1] || "";
|
|
537
|
+
const params = new URLSearchParams(qs);
|
|
538
|
+
const wave = params.get("wave") || null;
|
|
539
|
+
const now = Date.now();
|
|
540
|
+
const cache = _parallelismCache.metrics;
|
|
541
|
+
if (cache.body && cache.wave === wave && (now - cache.at) < PARALLELISM_CACHE_MS) {
|
|
542
|
+
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", "X-Cache": "hit" });
|
|
543
|
+
res.end(cache.body);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const reporter = _loadParallelismReporter();
|
|
547
|
+
if (reporter._loadError) {
|
|
548
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
549
|
+
res.end(JSON.stringify({ error: "parallelism-report module unavailable", detail: reporter._loadError }));
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
let metrics;
|
|
553
|
+
try {
|
|
554
|
+
metrics = reporter.computeParallelismMetrics({ projectDir, wave: wave || undefined });
|
|
555
|
+
} catch (err) {
|
|
556
|
+
// Contract says silent-fail; a thrown error here means contract regression.
|
|
557
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
558
|
+
res.end(JSON.stringify({ error: "computeParallelismMetrics threw", detail: err && err.message || String(err) }));
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const body = JSON.stringify(metrics);
|
|
562
|
+
cache.at = now;
|
|
563
|
+
cache.wave = wave;
|
|
564
|
+
cache.body = body;
|
|
565
|
+
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", "X-Cache": "miss" });
|
|
566
|
+
res.end(body);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function handleParallelismReport(req, res, projectDir) {
|
|
570
|
+
const urlObj = req.url ? req.url.split("?") : ["", ""];
|
|
571
|
+
const qs = urlObj[1] || "";
|
|
572
|
+
const params = new URLSearchParams(qs);
|
|
573
|
+
const wave = params.get("wave") || null;
|
|
574
|
+
const cacheKey = wave || "__all__";
|
|
575
|
+
const now = Date.now();
|
|
576
|
+
const cached = _parallelismCache.report.get(cacheKey);
|
|
577
|
+
if (cached && (now - cached.at) < PARALLELISM_CACHE_MS) {
|
|
578
|
+
res.writeHead(200, { "Content-Type": "text/markdown; charset=utf-8", "Access-Control-Allow-Origin": "*", "X-Cache": "hit" });
|
|
579
|
+
res.end(cached.body);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const reporter = _loadParallelismReporter();
|
|
583
|
+
if (reporter._loadError) {
|
|
584
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
585
|
+
res.end("parallelism-report module unavailable: " + reporter._loadError);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
let md;
|
|
589
|
+
try {
|
|
590
|
+
md = reporter.buildFullReport({ projectDir, wave: wave || undefined });
|
|
591
|
+
} catch (err) {
|
|
592
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
593
|
+
res.end("buildFullReport threw: " + (err && err.message || String(err)));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
_parallelismCache.report.set(cacheKey, { at: now, body: md });
|
|
597
|
+
res.writeHead(200, { "Content-Type": "text/markdown; charset=utf-8", "Access-Control-Allow-Origin": "*", "X-Cache": "miss" });
|
|
598
|
+
res.end(md);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// POST /api/unattended-stop — proxies to the existing stop-sentinel flow so
|
|
602
|
+
// the transcript panel's "Stop Supervisor" button reuses the canonical
|
|
603
|
+
// kill path. Writes `.gsd-t/.unattended/stop` sentinel; supervisor polls
|
|
604
|
+
// it and self-exits. Contract reminder: D9 does NOT implement its own
|
|
605
|
+
// stop logic, does NOT PID-kill.
|
|
606
|
+
function handleUnattendedStop(req, res, projectDir) {
|
|
607
|
+
if (req.method !== "POST") {
|
|
608
|
+
res.writeHead(405, { "Content-Type": "application/json", "Allow": "POST" });
|
|
609
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const stopDir = path.join(projectDir, ".gsd-t", ".unattended");
|
|
613
|
+
const stopFile = path.join(stopDir, "stop");
|
|
614
|
+
try {
|
|
615
|
+
fs.mkdirSync(stopDir, { recursive: true });
|
|
616
|
+
fs.writeFileSync(stopFile, new Date().toISOString() + "\n");
|
|
617
|
+
} catch (err) {
|
|
618
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
619
|
+
res.end(JSON.stringify({ error: "failed to write stop sentinel", detail: err && err.message || String(err) }));
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
623
|
+
res.end(JSON.stringify({ ok: true, sentinel: stopFile }));
|
|
624
|
+
}
|
|
625
|
+
|
|
581
626
|
function handleSpawnPlans(req, res, projectDir) {
|
|
582
627
|
const plans = listActiveSpawnPlans(projectDir).sort((a, b) => {
|
|
583
628
|
const ta = Date.parse(a.startedAt) || 0;
|
|
@@ -637,10 +682,15 @@ function startServer(port, eventsDir, htmlPath, projectDir, transcriptHtmlPath)
|
|
|
637
682
|
if (url === "/metrics") return handleMetrics(req, res, projDir);
|
|
638
683
|
if (url === "/ping") return handlePing(req, res, port);
|
|
639
684
|
if (url === "/stop") return handleStop(req, res, server);
|
|
640
|
-
if (url === "/transcripts") return handleTranscriptsList(req, res, projDir);
|
|
685
|
+
if (url === "/transcripts") return handleTranscriptsList(req, res, projDir, tHtmlPath);
|
|
641
686
|
// M44 D8 — spawn plans: GET list + SSE change stream
|
|
642
687
|
if (url === "/api/spawn-plans") return handleSpawnPlans(req, res, projDir);
|
|
643
688
|
if (url === "/api/spawn-plans/stream") return handleSpawnPlanUpdates(req, res, projDir);
|
|
689
|
+
// M44 D9 — parallelism observability (additive, read-only)
|
|
690
|
+
if (url === "/api/parallelism") return handleParallelism(req, res, projDir);
|
|
691
|
+
if (url === "/api/parallelism/report") return handleParallelismReport(req, res, projDir);
|
|
692
|
+
// M44 D9 — stop-supervisor proxy (POST only; reuses existing sentinel flow)
|
|
693
|
+
if (url === "/api/unattended-stop") return handleUnattendedStop(req, res, projDir);
|
|
644
694
|
// POST /transcript/:spawnId/kill — SIGTERM the recorded workerPid
|
|
645
695
|
const killMatch = url.match(/^\/transcript\/([^/]+)\/kill$/);
|
|
646
696
|
if (killMatch && req.method === "POST") return handleTranscriptKill(req, res, decodeURIComponent(killMatch[1]), projDir);
|
|
@@ -674,7 +724,6 @@ module.exports = {
|
|
|
674
724
|
readIndexEntry,
|
|
675
725
|
isValidSpawnId,
|
|
676
726
|
handleTranscriptsList,
|
|
677
|
-
renderTranscriptsHtml,
|
|
678
727
|
handleTranscriptStream,
|
|
679
728
|
handleTranscriptPage,
|
|
680
729
|
handleTranscriptKill,
|
|
@@ -692,6 +741,10 @@ module.exports = {
|
|
|
692
741
|
handleSpawnPlanUpdates,
|
|
693
742
|
readSpawnPlanFile,
|
|
694
743
|
spawnsDir,
|
|
744
|
+
// M44 D9 — parallelism observability
|
|
745
|
+
handleParallelism,
|
|
746
|
+
handleParallelismReport,
|
|
747
|
+
handleUnattendedStop,
|
|
695
748
|
};
|
|
696
749
|
|
|
697
750
|
if (require.main === module) {
|
|
@@ -45,6 +45,27 @@
|
|
|
45
45
|
.spawn-panel .empty { color: var(--fg-xdim); font-style: italic; padding: 4px 0; font-size: 11px; }
|
|
46
46
|
.spawn-panel .wave-group { border-top: 1px dashed var(--border); padding-top: 4px; margin-top: 4px; }
|
|
47
47
|
.spawn-panel .wave-group:first-child { border-top: none; padding-top: 0; margin-top: 0; }
|
|
48
|
+
/* M44 D9 — parallelism panel (additive; scoped to its own class). */
|
|
49
|
+
.parallelism-panel { margin-top: 14px; border: 1px solid var(--border); border-radius: 4px; padding: 10px 12px; background: var(--bg); transition: border-color 200ms ease; }
|
|
50
|
+
.parallelism-panel.color-green { border-color: #10b981; }
|
|
51
|
+
.parallelism-panel.color-yellow { border-color: #f59e0b; }
|
|
52
|
+
.parallelism-panel.color-red { border-color: #ef4444; }
|
|
53
|
+
.parallelism-panel.color-dimmed { border-color: #374151; opacity: 0.5; }
|
|
54
|
+
.parallelism-panel h3 { margin: 0 0 6px 0; font-size: 11px; text-transform: uppercase; color: var(--fg-xdim); letter-spacing: 0.08em; display: flex; justify-content: space-between; align-items: baseline; }
|
|
55
|
+
.parallelism-panel h3 .pp-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: currentColor; margin-right: 6px; vertical-align: middle; }
|
|
56
|
+
.parallelism-panel .pp-dot.dot-green { color: #10b981; }
|
|
57
|
+
.parallelism-panel .pp-dot.dot-yellow { color: #f59e0b; }
|
|
58
|
+
.parallelism-panel .pp-dot.dot-red { color: #ef4444; }
|
|
59
|
+
.parallelism-panel .pp-dot.dot-dimmed { color: #374151; }
|
|
60
|
+
.parallelism-panel .pp-row { display: flex; justify-content: space-between; font-size: 11px; padding: 2px 0; font-family: var(--mono); }
|
|
61
|
+
.parallelism-panel .pp-row .pp-k { color: var(--fg-xdim); }
|
|
62
|
+
.parallelism-panel .pp-row .pp-v { color: var(--fg); }
|
|
63
|
+
.parallelism-panel .pp-gates { margin-top: 6px; padding-top: 6px; border-top: 1px dashed var(--border); font-size: 11px; font-family: var(--mono); color: var(--fg-dim); }
|
|
64
|
+
.parallelism-panel .pp-gates span { margin-right: 8px; }
|
|
65
|
+
.parallelism-panel .pp-actions { display: flex; gap: 6px; margin-top: 8px; }
|
|
66
|
+
.parallelism-panel .pp-actions button { flex: 1; padding: 4px 8px; font-size: 11px; background: var(--bg-raised); color: var(--fg); border: 1px solid var(--border); border-radius: 3px; cursor: pointer; font-family: var(--sans); }
|
|
67
|
+
.parallelism-panel .pp-actions button:hover { border-color: var(--accent); }
|
|
68
|
+
.parallelism-panel .pp-actions button.danger:hover { border-color: #ef4444; color: #ef4444; }
|
|
48
69
|
.spawn-panel .wave-label { font-size: 10px; color: var(--fg-xdim); font-family: var(--mono); text-transform: uppercase; letter-spacing: 0.05em; margin: 4px 0 2px 0; }
|
|
49
70
|
aside h3 { margin: 0 12px 8px 12px; font-size: 11px; text-transform: uppercase; color: var(--fg-xdim); letter-spacing: 0.08em; }
|
|
50
71
|
aside .tree { font-size: 13px; }
|
|
@@ -76,6 +97,10 @@
|
|
|
76
97
|
aside .tool-row .tool-tokens { color: var(--fg-dim); flex: 0 0 auto; }
|
|
77
98
|
aside .tool-empty { color: var(--fg-xdim); font-size: 11px; font-style: italic; }
|
|
78
99
|
aside .tool-error { color: var(--yellow); font-size: 11px; font-style: italic; }
|
|
100
|
+
/* M45 D2 — in-session conversation left-rail badge */
|
|
101
|
+
aside .node.in-session .name { color: var(--yellow); }
|
|
102
|
+
aside .node.in-session.active .name { color: var(--accent-warm, var(--yellow)); }
|
|
103
|
+
aside .node .label-in-session { color: var(--yellow); margin-right: 4px; }
|
|
79
104
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
80
105
|
/* 10s attention-pulse for a freshly-arrived spawn — fires once when
|
|
81
106
|
auto-follow snaps focus to a new session, then self-removes. */
|
|
@@ -170,6 +195,20 @@
|
|
|
170
195
|
<div class="sec-totals" id="active-totals">—</div>
|
|
171
196
|
<div id="active-tasks"><div class="empty">no active spawn plan</div></div>
|
|
172
197
|
</section>
|
|
198
|
+
<!-- M44 D9 — parallelism panel (additive). Reads /api/parallelism every 5s.
|
|
199
|
+
Color border reflects color_state (green/yellow/red/dimmed). -->
|
|
200
|
+
<div class="parallelism-panel color-dimmed" id="parallelism-panel">
|
|
201
|
+
<h3><span><span class="pp-dot dot-dimmed" id="pp-dot"></span>Parallelism</span><span id="pp-mode" style="font-size:10px;color:var(--fg-xdim);">idle</span></h3>
|
|
202
|
+
<div class="pp-row"><span class="pp-k">active workers</span><span class="pp-v" id="pp-active">—</span></div>
|
|
203
|
+
<div class="pp-row"><span class="pp-k">ready tasks</span><span class="pp-v" id="pp-ready">—</span></div>
|
|
204
|
+
<div class="pp-row"><span class="pp-k">parallelism factor</span><span class="pp-v" id="pp-factor">—</span></div>
|
|
205
|
+
<div class="pp-row"><span class="pp-k">oldest spawn</span><span class="pp-v" id="pp-oldest">—</span></div>
|
|
206
|
+
<div class="pp-gates" id="pp-gates"><span>dep: —</span><span>disj: —</span><span>eco: —</span></div>
|
|
207
|
+
<div class="pp-actions">
|
|
208
|
+
<button id="pp-report-btn" title="Download full parallelism post-mortem for the current wave">📄 Full Report</button>
|
|
209
|
+
<button id="pp-stop-btn" class="danger" title="Write .gsd-t/.unattended/stop sentinel to halt an active supervisor">Stop Supervisor</button>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
173
212
|
</aside>
|
|
174
213
|
<button class="jump-to-live" id="jump-btn">↓ Jump to live</button>
|
|
175
214
|
|
|
@@ -483,14 +522,33 @@
|
|
|
483
522
|
return;
|
|
484
523
|
}
|
|
485
524
|
const currentId = (location.hash || '#').slice(1) || spawnId;
|
|
525
|
+
// M45 D2: in-session conversation NDJSONs are distinguished by the
|
|
526
|
+
// `in-session-` spawn-id prefix. The viewer labels them with
|
|
527
|
+
// `💬 conversation` instead of the default `▶ spawn`. This is a
|
|
528
|
+
// front-end-only discriminator — no server-side type field is
|
|
529
|
+
// required; the filename prefix (== spawn-id) is the contract.
|
|
530
|
+
function isInSession(node) {
|
|
531
|
+
return typeof node.spawnId === 'string' && node.spawnId.indexOf('in-session-') === 0;
|
|
532
|
+
}
|
|
486
533
|
function render(node, depth) {
|
|
487
534
|
const el = document.createElement('div');
|
|
488
535
|
el.className = 'node ' + statusClass(node);
|
|
536
|
+
if (isInSession(node)) el.classList.add('in-session');
|
|
489
537
|
if (node.spawnId === currentId) el.classList.add('active');
|
|
490
538
|
el.style.paddingLeft = (12 + depth * 14) + 'px';
|
|
491
539
|
const dot = document.createElement('span'); dot.className = 'dot';
|
|
492
540
|
const name = document.createElement('span'); name.className = 'name';
|
|
493
|
-
|
|
541
|
+
if (isInSession(node)) {
|
|
542
|
+
const badge = document.createElement('span');
|
|
543
|
+
badge.className = 'label-in-session';
|
|
544
|
+
badge.textContent = '💬 conversation';
|
|
545
|
+
name.appendChild(badge);
|
|
546
|
+
const tail = document.createElement('span');
|
|
547
|
+
tail.textContent = ' · ' + node.spawnId.slice(-8);
|
|
548
|
+
name.appendChild(tail);
|
|
549
|
+
} else {
|
|
550
|
+
name.textContent = (node.command || 'spawn') + ' · ' + node.spawnId.slice(-8);
|
|
551
|
+
}
|
|
494
552
|
name.title = (node.description || node.spawnId) + '\n' + (node.startedAt || '');
|
|
495
553
|
const kill = document.createElement('button');
|
|
496
554
|
kill.className = 'kill';
|
|
@@ -919,6 +977,99 @@
|
|
|
919
977
|
subscribePlanUpdates();
|
|
920
978
|
// Light poll every 10s as a safety net if SSE is disconnected.
|
|
921
979
|
setInterval(fetchInitialPlans, 10000);
|
|
980
|
+
|
|
981
|
+
// ── M44 D9 — Parallelism panel ─────────────────────────────────────────
|
|
982
|
+
// Polls /api/parallelism every 5s. Color border reflects color_state.
|
|
983
|
+
// Full Report button fetches /api/parallelism/report and downloads the
|
|
984
|
+
// markdown. Stop Supervisor POSTs /api/unattended-stop (writes sentinel).
|
|
985
|
+
(function wireParallelismPanel() {
|
|
986
|
+
const panel = document.getElementById('parallelism-panel');
|
|
987
|
+
if (!panel) return;
|
|
988
|
+
const dot = document.getElementById('pp-dot');
|
|
989
|
+
const modeEl = document.getElementById('pp-mode');
|
|
990
|
+
const activeEl = document.getElementById('pp-active');
|
|
991
|
+
const readyEl = document.getElementById('pp-ready');
|
|
992
|
+
const factorEl = document.getElementById('pp-factor');
|
|
993
|
+
const oldestEl = document.getElementById('pp-oldest');
|
|
994
|
+
const gatesEl = document.getElementById('pp-gates');
|
|
995
|
+
const reportBtn = document.getElementById('pp-report-btn');
|
|
996
|
+
const stopBtn = document.getElementById('pp-stop-btn');
|
|
997
|
+
|
|
998
|
+
function fmtAge(s) {
|
|
999
|
+
if (s == null || !isFinite(s)) return '—';
|
|
1000
|
+
if (s < 60) return s + 's';
|
|
1001
|
+
const m = Math.floor(s / 60), rs = s % 60;
|
|
1002
|
+
if (m < 60) return m + 'm ' + rs + 's';
|
|
1003
|
+
const h = Math.floor(m / 60), rm = m % 60;
|
|
1004
|
+
return h + 'h ' + rm + 'm';
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function applyColor(state) {
|
|
1008
|
+
const colors = ['color-green', 'color-yellow', 'color-red', 'color-dimmed'];
|
|
1009
|
+
const dots = ['dot-green', 'dot-yellow', 'dot-red', 'dot-dimmed'];
|
|
1010
|
+
for (const c of colors) panel.classList.remove(c);
|
|
1011
|
+
for (const d of dots) dot.classList.remove(d);
|
|
1012
|
+
const cls = 'color-' + (state || 'dimmed');
|
|
1013
|
+
const dcls = 'dot-' + (state || 'dimmed');
|
|
1014
|
+
panel.classList.add(cls);
|
|
1015
|
+
dot.classList.add(dcls);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function renderGates(g) {
|
|
1019
|
+
if (!g) { gatesEl.textContent = 'gates: —'; return; }
|
|
1020
|
+
const dep = g.dep_gate_veto || { count: 0 };
|
|
1021
|
+
const dis = g.disjointness_fallback || { count: 0 };
|
|
1022
|
+
const eco = g.economics_decision || { count: 0 };
|
|
1023
|
+
function tally(c) { return c === 0 ? '✓ 0' : (c <= 3 ? '⚠ ' + c : '❌ ' + c); }
|
|
1024
|
+
gatesEl.innerHTML = '<span>dep: ' + tally(dep.count) + '</span>' +
|
|
1025
|
+
'<span>disj: ' + tally(dis.count) + '</span>' +
|
|
1026
|
+
'<span>eco: ' + eco.count + '</span>';
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
async function poll() {
|
|
1030
|
+
try {
|
|
1031
|
+
const r = await fetch('/api/parallelism');
|
|
1032
|
+
if (!r.ok) return;
|
|
1033
|
+
const m = await r.json();
|
|
1034
|
+
applyColor(m.color_state);
|
|
1035
|
+
modeEl.textContent = m.parallelism_factor_mode || 'idle';
|
|
1036
|
+
activeEl.textContent = String(m.activeWorkers);
|
|
1037
|
+
readyEl.textContent = String(m.readyTasks);
|
|
1038
|
+
factorEl.textContent = (typeof m.parallelism_factor === 'number' ? m.parallelism_factor.toFixed(1) : '—') + '×';
|
|
1039
|
+
const oldest = (m.activeSpawnAges_s && m.activeSpawnAges_s.length) ? m.activeSpawnAges_s[0] : null;
|
|
1040
|
+
oldestEl.textContent = fmtAge(oldest);
|
|
1041
|
+
renderGates(m.gate_decisions);
|
|
1042
|
+
} catch { /* keep last state on transient failure */ }
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
async function downloadReport() {
|
|
1046
|
+
try {
|
|
1047
|
+
const r = await fetch('/api/parallelism/report');
|
|
1048
|
+
if (!r.ok) return;
|
|
1049
|
+
const md = await r.text();
|
|
1050
|
+
const blob = new Blob([md], { type: 'text/markdown' });
|
|
1051
|
+
const url = URL.createObjectURL(blob);
|
|
1052
|
+
const a = document.createElement('a');
|
|
1053
|
+
a.href = url;
|
|
1054
|
+
a.download = 'parallelism-report-' + new Date().toISOString().slice(0, 19).replace(/:/g, '-') + '.md';
|
|
1055
|
+
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
|
1056
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
1057
|
+
} catch { /* swallow */ }
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
async function stopSupervisor() {
|
|
1061
|
+
if (!confirm('Write .gsd-t/.unattended/stop sentinel? Active supervisor will exit on next poll.')) return;
|
|
1062
|
+
try {
|
|
1063
|
+
const r = await fetch('/api/unattended-stop', { method: 'POST' });
|
|
1064
|
+
if (r.ok) { stopBtn.textContent = 'Stop sent ✓'; setTimeout(() => { stopBtn.textContent = 'Stop Supervisor'; }, 3000); }
|
|
1065
|
+
} catch { /* swallow */ }
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (reportBtn) reportBtn.addEventListener('click', downloadReport);
|
|
1069
|
+
if (stopBtn) stopBtn.addEventListener('click', stopSupervisor);
|
|
1070
|
+
poll();
|
|
1071
|
+
setInterval(poll, 5000);
|
|
1072
|
+
})();
|
|
922
1073
|
})();
|
|
923
1074
|
</script>
|
|
924
1075
|
</body>
|
|
@@ -14,6 +14,15 @@ const CACHE_FILE = path.join(CLAUDE_DIR, ".gsd-t-update-check");
|
|
|
14
14
|
const CHANGELOG = "https://github.com/Tekyz-Inc/get-stuff-done-teams/blob/main/CHANGELOG.md";
|
|
15
15
|
const SEMVER_RE = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/;
|
|
16
16
|
|
|
17
|
+
// Local-time date stamp prefix for the version banner: "Tue: Mar 26, 2026, "
|
|
18
|
+
// The trailing two spaces are intentional — separates the date from the [GSD-T*] tag.
|
|
19
|
+
function dateStamp(now = new Date()) {
|
|
20
|
+
const day = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][now.getDay()];
|
|
21
|
+
const mon = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
22
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][now.getMonth()];
|
|
23
|
+
return `${day}: ${mon} ${now.getDate()}, ${now.getFullYear()}, `;
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
function isNewer(a, b) {
|
|
18
27
|
const ap = a.split(".").map(Number);
|
|
19
28
|
const bp = b.split(".").map(Number);
|
|
@@ -47,9 +56,9 @@ function doAutoUpdate(latest, installed) {
|
|
|
47
56
|
const updated = fs.existsSync(VERSION_FILE)
|
|
48
57
|
? fs.readFileSync(VERSION_FILE, "utf8").trim()
|
|
49
58
|
: latest;
|
|
50
|
-
console.log(
|
|
59
|
+
console.log(`${dateStamp()}[GSD-T AUTO-UPDATE] v${installed} → v${updated}. Changelog: ${CHANGELOG}`);
|
|
51
60
|
} catch {
|
|
52
|
-
console.log(
|
|
61
|
+
console.log(`${dateStamp()}[GSD-T UPDATE] v${installed} — update available (v${installed} → v${latest}). Auto-update failed — run manually: /gsd-t-version-update-all. Changelog: ${CHANGELOG}`);
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
64
|
|
|
@@ -86,7 +95,7 @@ function run() {
|
|
|
86
95
|
if (cached && cached.latest && isNewer(cached.latest, installed)) {
|
|
87
96
|
doAutoUpdate(cached.latest, installed);
|
|
88
97
|
} else {
|
|
89
|
-
console.log(
|
|
98
|
+
console.log(`${dateStamp()}[GSD-T] v${installed} — CURRENT. Changelog: ${CHANGELOG}`);
|
|
90
99
|
}
|
|
91
100
|
}
|
|
92
101
|
|
|
@@ -95,4 +104,4 @@ if (require.main === module) {
|
|
|
95
104
|
try { run(); } catch { /* graceful failure — don't block session start */ }
|
|
96
105
|
}
|
|
97
106
|
|
|
98
|
-
module.exports = { isNewer, fetchLatestVersion, doAutoUpdate, run };
|
|
107
|
+
module.exports = { isNewer, fetchLatestVersion, doAutoUpdate, run, dateStamp };
|