@tekyzinc/gsd-t 2.31.18 → 2.33.12
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 +626 -581
- package/README.md +15 -3
- package/bin/gsd-t.js +1 -1
- package/commands/gsd-t-brainstorm.md +31 -15
- package/commands/gsd-t-complete-milestone.md +23 -1
- package/commands/gsd-t-debug.md +107 -2
- package/commands/gsd-t-execute.md +15 -1
- package/commands/gsd-t-help.md +16 -0
- package/commands/gsd-t-init.md +3 -0
- package/commands/gsd-t-reflect.md +134 -0
- package/commands/gsd-t-visualize.md +104 -0
- package/commands/gsd-t-wave.md +21 -1
- package/docs/GSD-T-README.md +2 -0
- package/docs/architecture.md +16 -3
- package/docs/requirements.md +106 -84
- package/package.json +40 -40
- package/scripts/gsd-t-dashboard-mockup.html +1143 -0
- package/scripts/gsd-t-dashboard-server.js +140 -0
- package/scripts/gsd-t-dashboard.html +199 -0
- package/scripts/gsd-t-event-writer.js +124 -0
- package/scripts/gsd-t-heartbeat.js +54 -1
- package/templates/CLAUDE-global.md +2 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GSD-T Dashboard Server — Zero-dep SSE server for .gsd-t/events/*.jsonl
|
|
4
|
+
* Serves gsd-t-dashboard.html and streams events to browser clients.
|
|
5
|
+
*/
|
|
6
|
+
const http = require("http");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { spawn } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const DEFAULT_PORT = 7433;
|
|
12
|
+
const MAX_EVENTS = 500;
|
|
13
|
+
const KEEPALIVE_MS = 15000;
|
|
14
|
+
const SSE_HEADERS = { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "Access-Control-Allow-Origin": "*" };
|
|
15
|
+
|
|
16
|
+
function parseEventLine(line) {
|
|
17
|
+
if (!line || !line.trim()) return null;
|
|
18
|
+
try { return JSON.parse(line.trim()); } catch { return null; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function findEventsDir(projectDir) {
|
|
22
|
+
return path.join(projectDir || process.cwd(), ".gsd-t", "events");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function safeReadJsonl(filePath) {
|
|
26
|
+
try { if (fs.lstatSync(filePath).isSymbolicLink()) return []; } catch { /* safe */ }
|
|
27
|
+
try { return fs.readFileSync(filePath, "utf8").split("\n").map(parseEventLine).filter(Boolean); } catch { return []; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readExistingEvents(eventsDir, maxEvents) {
|
|
31
|
+
const limit = maxEvents || MAX_EVENTS;
|
|
32
|
+
if (!eventsDir) return [];
|
|
33
|
+
try { fs.accessSync(eventsDir); } catch { return []; }
|
|
34
|
+
let files;
|
|
35
|
+
try { files = fs.readdirSync(eventsDir).filter((f) => f.endsWith(".jsonl")).sort().reverse(); } catch { return []; }
|
|
36
|
+
const results = [];
|
|
37
|
+
for (const f of files) {
|
|
38
|
+
if (results.length >= limit) break;
|
|
39
|
+
safeReadJsonl(path.join(eventsDir, f)).forEach((e) => { if (results.length < limit) results.push(e); });
|
|
40
|
+
}
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function tailEventsFile(filePath, callback) {
|
|
45
|
+
let offset = 0;
|
|
46
|
+
try { offset = fs.statSync(filePath).size; } catch { /* new file */ }
|
|
47
|
+
function processNewData() {
|
|
48
|
+
try { if (fs.lstatSync(filePath).isSymbolicLink()) return; } catch { return; }
|
|
49
|
+
let stat;
|
|
50
|
+
try { stat = fs.statSync(filePath); } catch { return; }
|
|
51
|
+
if (stat.size <= offset) return;
|
|
52
|
+
const fd = fs.openSync(filePath, "r");
|
|
53
|
+
let chunk;
|
|
54
|
+
try {
|
|
55
|
+
const buf = Buffer.alloc(stat.size - offset);
|
|
56
|
+
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
57
|
+
chunk = buf.toString("utf8");
|
|
58
|
+
offset = stat.size;
|
|
59
|
+
} finally { fs.closeSync(fd); }
|
|
60
|
+
chunk.split("\n").forEach((line) => { const obj = parseEventLine(line); if (obj) callback(obj); });
|
|
61
|
+
}
|
|
62
|
+
fs.watchFile(filePath, { interval: 500, persistent: true }, processNewData);
|
|
63
|
+
return () => fs.unwatchFile(filePath, processNewData);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handleRoot(req, res, htmlPath) {
|
|
67
|
+
fs.readFile(htmlPath, (err, data) => {
|
|
68
|
+
if (err) { res.writeHead(404); res.end("Not found"); return; }
|
|
69
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
70
|
+
res.end(data);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handlePing(req, res, port) {
|
|
75
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
76
|
+
res.end(JSON.stringify({ status: "ok", port }));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function handleStop(req, res, server) {
|
|
80
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
81
|
+
res.end(JSON.stringify({ status: "stopping" }));
|
|
82
|
+
setImmediate(() => server.close());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getNewestJsonl(eventsDir) {
|
|
86
|
+
try { const files = fs.readdirSync(eventsDir).filter((f) => f.endsWith(".jsonl")).sort(); return files.length ? path.join(eventsDir, files[files.length - 1]) : null; } catch { return null; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function handleEvents(req, res, eventsDir) {
|
|
90
|
+
res.writeHead(200, SSE_HEADERS);
|
|
91
|
+
readExistingEvents(eventsDir, MAX_EVENTS).forEach((e) => { try { res.write("data: " + JSON.stringify(e) + "\n\n"); } catch { /* gone */ } });
|
|
92
|
+
const watched = getNewestJsonl(eventsDir);
|
|
93
|
+
const unwatch = watched ? tailEventsFile(watched, (obj) => { try { res.write("data: " + JSON.stringify(obj) + "\n\n"); } catch { /* gone */ } }) : null;
|
|
94
|
+
const timer = setInterval(() => { try { res.write(": keepalive\n\n"); } catch { clearInterval(timer); } }, KEEPALIVE_MS);
|
|
95
|
+
req.on("close", () => { clearInterval(timer); if (unwatch) unwatch(); });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function startServer(port, eventsDir, htmlPath) {
|
|
99
|
+
const server = http.createServer((req, res) => {
|
|
100
|
+
const url = req.url.split("?")[0];
|
|
101
|
+
if (url === "/" || url === "") return handleRoot(req, res, htmlPath);
|
|
102
|
+
if (url === "/events") return handleEvents(req, res, eventsDir);
|
|
103
|
+
if (url === "/ping") return handlePing(req, res, port);
|
|
104
|
+
if (url === "/stop") return handleStop(req, res, server);
|
|
105
|
+
res.writeHead(404); res.end("Not found");
|
|
106
|
+
});
|
|
107
|
+
server.listen(port);
|
|
108
|
+
return { server, url: `http://localhost:${port}` };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { startServer, tailEventsFile, readExistingEvents, parseEventLine, findEventsDir };
|
|
112
|
+
|
|
113
|
+
if (require.main === module) {
|
|
114
|
+
const argv = process.argv.slice(2);
|
|
115
|
+
const getArg = (flag) => { const i = argv.indexOf(flag); return i >= 0 ? argv[i + 1] : null; };
|
|
116
|
+
const hasFlag = (f) => argv.includes(f);
|
|
117
|
+
const port = parseInt(getArg("--port") || DEFAULT_PORT, 10);
|
|
118
|
+
const projectDir = process.env.GSD_T_PROJECT_DIR || process.cwd();
|
|
119
|
+
const eventsDir = getArg("--events") || findEventsDir(projectDir);
|
|
120
|
+
const pidFile = path.join(projectDir, ".gsd-t", "dashboard.pid");
|
|
121
|
+
const htmlPath = path.join(__dirname, "gsd-t-dashboard.html");
|
|
122
|
+
|
|
123
|
+
if (hasFlag("--stop")) {
|
|
124
|
+
try { const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10); process.kill(pid); fs.unlinkSync(pidFile); }
|
|
125
|
+
catch (e) { process.stderr.write("No running server: " + e.message + "\n"); }
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
if (hasFlag("--detach")) {
|
|
129
|
+
const child = spawn(process.execPath, [__filename, ...argv.filter((a) => a !== "--detach")], { detached: true, stdio: "ignore" });
|
|
130
|
+
child.unref();
|
|
131
|
+
try { fs.mkdirSync(path.dirname(pidFile), { recursive: true }); } catch { /* exists */ }
|
|
132
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
const { server, url } = startServer(port, eventsDir, htmlPath);
|
|
136
|
+
process.stdout.write("GSD-T Dashboard: " + url + "\n");
|
|
137
|
+
function cleanup() { try { fs.unlinkSync(pidFile); } catch { /* ok */ } server.close(() => process.exit(0)); }
|
|
138
|
+
process.on("SIGTERM", cleanup);
|
|
139
|
+
process.on("SIGINT", cleanup);
|
|
140
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>GSD-T Agent Dashboard</title>
|
|
6
|
+
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
|
|
7
|
+
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
|
|
8
|
+
<script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
|
|
9
|
+
<script src="https://unpkg.com/reactflow@11.11.4/dist/umd/index.js"></script>
|
|
10
|
+
<link rel="stylesheet" href="https://unpkg.com/reactflow@11.11.4/dist/style.css">
|
|
11
|
+
<style>
|
|
12
|
+
:root{--bg:#0d1117;--surface:#161b22;--border:#30363d;--text:#e6edf3;--muted:#7d8590;
|
|
13
|
+
--green:#3fb950;--green-bg:#1a3a1e;--red:#f85149;--red-bg:#3a1a1a;
|
|
14
|
+
--yellow:#d29922;--yellow-bg:#3a2c10;--blue:#388bfd;--blue-bg:#1f3a5f;
|
|
15
|
+
--font:'SF Mono','Fira Code',monospace;}
|
|
16
|
+
*{box-sizing:border-box;margin:0;padding:0;}
|
|
17
|
+
body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:12px;
|
|
18
|
+
height:100vh;display:flex;flex-direction:column;overflow:hidden;}
|
|
19
|
+
.hdr{display:flex;align-items:center;gap:12px;padding:0 16px;background:var(--surface);
|
|
20
|
+
border-bottom:1px solid var(--border);height:40px;flex-shrink:0;}
|
|
21
|
+
.logo{color:var(--blue);font-weight:bold;font-size:13px;}
|
|
22
|
+
.status{display:flex;align-items:center;gap:5px;padding:2px 8px;border-radius:10px;
|
|
23
|
+
font-size:10px;border:1px solid;}
|
|
24
|
+
.status.live{background:var(--green-bg);border-color:var(--green);color:var(--green);}
|
|
25
|
+
.status.wait{background:var(--yellow-bg);border-color:var(--yellow);color:var(--yellow);}
|
|
26
|
+
.dot{width:6px;height:6px;border-radius:50%;background:currentColor;
|
|
27
|
+
animation:pdot 1.5s ease-in-out infinite;}
|
|
28
|
+
@keyframes pdot{0%,100%{opacity:1}50%{opacity:.3}}
|
|
29
|
+
.hright{margin-left:auto;color:var(--muted);font-size:11px;}
|
|
30
|
+
.main{display:flex;flex:1;overflow:hidden;}
|
|
31
|
+
.garea{flex:1;position:relative;background:var(--bg);}
|
|
32
|
+
.rfwrap{width:100%;height:100%;}
|
|
33
|
+
.react-flow__background{background:var(--bg);}
|
|
34
|
+
.react-flow__node{font-family:var(--font);font-size:11px;}
|
|
35
|
+
.agent-node{background:var(--surface);border:1px solid var(--border);border-radius:6px;
|
|
36
|
+
padding:8px 12px;min-width:120px;text-align:center;}
|
|
37
|
+
.agent-node.success{border-color:var(--green);background:var(--green-bg);}
|
|
38
|
+
.agent-node.failure{border-color:var(--red);background:var(--red-bg);}
|
|
39
|
+
.agent-node.learning,.agent-node.deferred{border-color:var(--yellow);background:var(--yellow-bg);}
|
|
40
|
+
.agent-node.session{border-color:var(--blue);background:var(--blue-bg);}
|
|
41
|
+
.node-id{color:var(--text);font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:140px;}
|
|
42
|
+
.node-cnt{color:var(--muted);font-size:9px;margin-top:2px;}
|
|
43
|
+
.sidebar{width:300px;background:var(--surface);border-left:1px solid var(--border);
|
|
44
|
+
display:flex;flex-direction:column;overflow:hidden;}
|
|
45
|
+
.sb-hdr{padding:10px 12px;border-bottom:1px solid var(--border);color:var(--muted);
|
|
46
|
+
font-size:10px;text-transform:uppercase;letter-spacing:.8px;flex-shrink:0;}
|
|
47
|
+
.feed{flex:1;overflow-y:auto;padding:8px;}
|
|
48
|
+
.feed::-webkit-scrollbar{width:4px;}
|
|
49
|
+
.feed::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px;}
|
|
50
|
+
.ev{border-radius:4px;padding:6px 8px;margin-bottom:5px;border-left:3px solid var(--border);
|
|
51
|
+
background:#1c2128;}
|
|
52
|
+
.ev.success{border-left-color:var(--green);}
|
|
53
|
+
.ev.failure{border-left-color:var(--red);}
|
|
54
|
+
.ev.learning,.ev.deferred{border-left-color:var(--yellow);}
|
|
55
|
+
.ev-type{font-weight:500;color:var(--text);font-size:11px;}
|
|
56
|
+
.ev-cmd{color:var(--blue);font-size:10px;margin-top:1px;}
|
|
57
|
+
.ev-ts{color:var(--muted);font-size:9px;margin-top:2px;}
|
|
58
|
+
.badge{display:inline-block;font-size:9px;padding:1px 5px;border-radius:3px;font-weight:bold;margin-left:4px;}
|
|
59
|
+
.badge.success{background:var(--green-bg);color:var(--green);}
|
|
60
|
+
.badge.failure{background:var(--red-bg);color:var(--red);}
|
|
61
|
+
.badge.learning,.badge.deferred{background:var(--yellow-bg);color:var(--yellow);}
|
|
62
|
+
.noevents{color:var(--muted);font-size:11px;padding:12px;text-align:center;}
|
|
63
|
+
</style>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<div class="hdr">
|
|
67
|
+
<span class="logo">GSD-T Agent Dashboard</span>
|
|
68
|
+
<div id="status" class="status wait"><span class="dot"></span><span id="status-txt">Connecting...</span></div>
|
|
69
|
+
<div class="hright"><span id="ev-count">0 events</span></div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="main">
|
|
72
|
+
<div class="garea"><div id="rf-root" class="rfwrap"></div></div>
|
|
73
|
+
<div class="sidebar">
|
|
74
|
+
<div class="sb-hdr">Live Event Feed</div>
|
|
75
|
+
<div id="feed" class="feed"><div class="noevents">Waiting for events...</div></div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<script>
|
|
79
|
+
const {useState,useEffect,useCallback,useRef}=React;
|
|
80
|
+
const {ReactFlow,useNodesState,useEdgesState,Background,Controls,MarkerType}=window.ReactFlow||{};
|
|
81
|
+
const dagre=window.dagre;
|
|
82
|
+
const params=new URLSearchParams(location.search);
|
|
83
|
+
const PORT=params.get('port')||'7433';
|
|
84
|
+
const MAX_EVENTS=200;
|
|
85
|
+
const GRAPH_W=160,GRAPH_H=60;
|
|
86
|
+
|
|
87
|
+
function outcomeClass(o){return o==='success'?'success':o==='failure'?'failure':o==='learning'||o==='deferred'?o:'';}
|
|
88
|
+
|
|
89
|
+
function layoutGraph(nodes,edges){
|
|
90
|
+
const g=new dagre.graphlib.Graph();
|
|
91
|
+
g.setGraph({rankdir:'TB',ranksep:60,nodesep:40});
|
|
92
|
+
g.setDefaultEdgeLabel(()=>({}));
|
|
93
|
+
nodes.forEach(n=>g.setNode(n.id,{width:GRAPH_W,height:GRAPH_H}));
|
|
94
|
+
edges.forEach(e=>g.setEdge(e.source,e.target));
|
|
95
|
+
dagre.layout(g);
|
|
96
|
+
return nodes.map(n=>{const p=g.node(n.id);return{...n,position:{x:p.x-GRAPH_W/2,y:p.y-GRAPH_H/2}};});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function AgentNode({data}){
|
|
100
|
+
return React.createElement('div',{className:`agent-node ${data.outcome} ${data.nodeType||''}`},
|
|
101
|
+
React.createElement('div',{className:'node-id',title:data.label},data.label),
|
|
102
|
+
React.createElement('div',{className:'node-cnt'},`${data.count} event${data.count!==1?'s':''}`));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const nodeTypes={agentNode:AgentNode};
|
|
106
|
+
|
|
107
|
+
function Dashboard(){
|
|
108
|
+
const [nodes,setNodes,onNodesChange]=useNodesState([]);
|
|
109
|
+
const [edges,setEdges,onEdgesChange]=useEdgesState([]);
|
|
110
|
+
const [events,setEvents]=useState([]);
|
|
111
|
+
const agentMap=useRef({});
|
|
112
|
+
|
|
113
|
+
const processEvent=useCallback((ev)=>{
|
|
114
|
+
setEvents(prev=>{
|
|
115
|
+
const next=[ev,...prev];
|
|
116
|
+
return next.length>MAX_EVENTS?next.slice(0,MAX_EVENTS):next;
|
|
117
|
+
});
|
|
118
|
+
if(!ev.agent_id)return;
|
|
119
|
+
const id=ev.agent_id;
|
|
120
|
+
const prev=agentMap.current[id]||{count:0,outcome:null,parent:null,nodeType:null,firstTs:null};
|
|
121
|
+
const nodeType=prev.nodeType||(ev.event_type==='session_start'?'session':ev.event_type==='subagent_spawn'?(ev.reasoning||'task'):null);
|
|
122
|
+
agentMap.current[id]={
|
|
123
|
+
count:prev.count+1,
|
|
124
|
+
outcome:ev.outcome||prev.outcome,
|
|
125
|
+
parent:ev.parent_agent_id||prev.parent||null,
|
|
126
|
+
nodeType,
|
|
127
|
+
firstTs:prev.firstTs||ev.ts||null
|
|
128
|
+
};
|
|
129
|
+
const newNodes=Object.entries(agentMap.current).map(([aid,d])=>{
|
|
130
|
+
const isSession=d.nodeType==='session';
|
|
131
|
+
const dateStr=d.firstTs?new Date(d.firstTs).toLocaleDateString('en',{month:'short',day:'numeric'}):'';
|
|
132
|
+
const lbl=isSession?`Session · ${dateStr?dateStr+' ':''}${aid.slice(0,7)}`:(d.nodeType||aid.slice(0,8)+'..'+aid.slice(-4));
|
|
133
|
+
return{id:aid,type:'agentNode',data:{label:lbl,count:d.count,outcome:d.outcome||'',nodeType:d.nodeType||''},position:{x:0,y:0}};
|
|
134
|
+
});
|
|
135
|
+
const newEdges=Object.entries(agentMap.current)
|
|
136
|
+
.filter(([,d])=>d.parent&&d.parent!=='')
|
|
137
|
+
.map(([aid,d])=>({id:`${d.parent}-${aid}`,source:d.parent,target:aid,
|
|
138
|
+
markerEnd:{type:MarkerType.ArrowClosed},style:{stroke:'#30363d'}}));
|
|
139
|
+
const laid=layoutGraph(newNodes,newEdges);
|
|
140
|
+
setNodes(laid);
|
|
141
|
+
setEdges(newEdges);
|
|
142
|
+
},[]);
|
|
143
|
+
|
|
144
|
+
useEffect(()=>{
|
|
145
|
+
let es;
|
|
146
|
+
function connect(){
|
|
147
|
+
es=new EventSource(`http://localhost:${PORT}/events`);
|
|
148
|
+
es.onopen=()=>{
|
|
149
|
+
document.getElementById('status').className='status live';
|
|
150
|
+
document.getElementById('status-txt').textContent='Live';
|
|
151
|
+
};
|
|
152
|
+
es.onmessage=(e)=>{
|
|
153
|
+
if(e.data&&e.data.trim()&&!e.data.startsWith(':')){
|
|
154
|
+
try{processEvent(JSON.parse(e.data));}catch(err){}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
es.onerror=()=>{
|
|
158
|
+
document.getElementById('status').className='status wait';
|
|
159
|
+
document.getElementById('status-txt').textContent='Reconnecting...';
|
|
160
|
+
es.close();
|
|
161
|
+
setTimeout(connect,3000);
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
connect();
|
|
165
|
+
return()=>{if(es)es.close();};
|
|
166
|
+
},[processEvent]);
|
|
167
|
+
|
|
168
|
+
useEffect(()=>{
|
|
169
|
+
document.getElementById('ev-count').textContent=`${events.length} event${events.length!==1?'s':''}`;
|
|
170
|
+
const feed=document.getElementById('feed');
|
|
171
|
+
if(!events.length){feed.innerHTML='<div class="noevents">Waiting for events...</div>';return;}
|
|
172
|
+
feed.innerHTML=events.slice(0,50).map(ev=>{
|
|
173
|
+
const oc=outcomeClass(ev.outcome);
|
|
174
|
+
const ts=ev.ts?new Date(ev.ts).toLocaleTimeString():'';
|
|
175
|
+
const cmd=ev.command||ev.phase||'';
|
|
176
|
+
return`<div class="ev${oc?' '+oc:''}">
|
|
177
|
+
<div class="ev-type">${ev.event_type||'event'}${oc?`<span class="badge ${oc}">${ev.outcome}</span>`:''}</div>
|
|
178
|
+
${cmd?`<div class="ev-cmd">${cmd}</div>`:''}
|
|
179
|
+
${ev.reasoning?`<div class="ev-ts" style="color:var(--muted)">${ev.reasoning.slice(0,60)}</div>`:''}
|
|
180
|
+
<div class="ev-ts">${ts}</div>
|
|
181
|
+
</div>`;
|
|
182
|
+
}).join('');
|
|
183
|
+
},[events]);
|
|
184
|
+
|
|
185
|
+
if(!ReactFlow)return React.createElement('div',{style:{color:'#f85149',padding:'20px'}},'ReactFlow failed to load. Check CDN.');
|
|
186
|
+
return React.createElement(ReactFlow,{
|
|
187
|
+
nodes,edges,onNodesChange,onEdgesChange,nodeTypes,fitView:true,
|
|
188
|
+
style:{background:'var(--bg)'},
|
|
189
|
+
defaultEdgeOptions:{type:'smoothstep',animated:false}
|
|
190
|
+
},
|
|
191
|
+
React.createElement(Background,{color:'#30363d',gap:24,size:1}),
|
|
192
|
+
React.createElement(Controls,{style:{background:'var(--surface)',border:'1px solid var(--border)'}})
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
ReactDOM.render(React.createElement(Dashboard),document.getElementById('rf-root'));
|
|
197
|
+
</script>
|
|
198
|
+
</body>
|
|
199
|
+
</html>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GSD-T Event Writer — Structured JSONL Event Appender
|
|
5
|
+
*
|
|
6
|
+
* Writes structured events to .gsd-t/events/YYYY-MM-DD.jsonl (UTC date rotation).
|
|
7
|
+
* CLI for use from hooks and command files.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node gsd-t-event-writer.js --type phase_transition --command gsd-t-wave \
|
|
11
|
+
* --phase execute --reasoning "Execute complete" --outcome success \
|
|
12
|
+
* --agent-id "$SESSION" --parent-id null --trace-id "$TRACE"
|
|
13
|
+
*
|
|
14
|
+
* Exit codes: 0 = success, 1 = validation error, 2 = filesystem error
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const path = require("path");
|
|
19
|
+
|
|
20
|
+
// ─── Schema ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const VALID_EVENT_TYPES = new Set([
|
|
23
|
+
"command_invoked",
|
|
24
|
+
"phase_transition",
|
|
25
|
+
"subagent_spawn",
|
|
26
|
+
"subagent_complete",
|
|
27
|
+
"tool_call",
|
|
28
|
+
"experience_retrieval",
|
|
29
|
+
"outcome_tagged",
|
|
30
|
+
"distillation",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const VALID_OUTCOMES = new Set(["success", "failure", "learning", "deferred", null]);
|
|
34
|
+
|
|
35
|
+
// ─── Exports (for testing) ────────────────────────────────────────────────────
|
|
36
|
+
module.exports = { validateEvent, resolveEventsFile, appendEvent };
|
|
37
|
+
|
|
38
|
+
// ─── CLI Entry ───────────────────────────────────────────────────────────────
|
|
39
|
+
if (require.main === module) {
|
|
40
|
+
const args = parseArgs(process.argv.slice(2));
|
|
41
|
+
const event = buildEvent(args);
|
|
42
|
+
const validationError = validateEvent(event);
|
|
43
|
+
if (validationError) {
|
|
44
|
+
process.stderr.write(validationError + "\n");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const projectDir = process.env.GSD_T_PROJECT_DIR || process.cwd();
|
|
48
|
+
const eventsFile = resolveEventsFile(projectDir);
|
|
49
|
+
const exitCode = appendEvent(eventsFile, event);
|
|
50
|
+
process.exit(exitCode);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── Arg Parsing ─────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
const map = {};
|
|
57
|
+
for (let i = 0; i < argv.length - 1; i++) {
|
|
58
|
+
if (argv[i].startsWith("--")) {
|
|
59
|
+
map[argv[i].slice(2)] = argv[i + 1];
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return map;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function nullify(val) {
|
|
67
|
+
return val === undefined || val === "null" ? null : val;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildEvent(args) {
|
|
71
|
+
return {
|
|
72
|
+
ts: new Date().toISOString(),
|
|
73
|
+
event_type: nullify(args["type"]),
|
|
74
|
+
command: nullify(args["command"]),
|
|
75
|
+
phase: nullify(args["phase"]),
|
|
76
|
+
agent_id: nullify(args["agent-id"]),
|
|
77
|
+
parent_agent_id: nullify(args["parent-id"]),
|
|
78
|
+
trace_id: nullify(args["trace-id"]),
|
|
79
|
+
reasoning: nullify(args["reasoning"]),
|
|
80
|
+
outcome: nullify(args["outcome"]),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Validation ──────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function validateEvent(event) {
|
|
87
|
+
if (!event || typeof event !== "object") return "Event must be an object";
|
|
88
|
+
if (!event.event_type) return "Missing required field: --type";
|
|
89
|
+
if (!VALID_EVENT_TYPES.has(event.event_type)) {
|
|
90
|
+
return `Invalid event_type: "${event.event_type}". Must be one of: ${[...VALID_EVENT_TYPES].join(", ")}`;
|
|
91
|
+
}
|
|
92
|
+
if (!VALID_OUTCOMES.has(event.outcome)) {
|
|
93
|
+
return `Invalid outcome: "${event.outcome}". Must be one of: success, failure, learning, deferred, null`;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── File Resolution ──────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
function resolveEventsFile(projectDir) {
|
|
101
|
+
const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD UTC
|
|
102
|
+
return path.join(projectDir, ".gsd-t", "events", `${date}.jsonl`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── Append ───────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
function appendEvent(filePath, event) {
|
|
108
|
+
try {
|
|
109
|
+
const eventsDir = path.dirname(filePath);
|
|
110
|
+
if (!fs.existsSync(eventsDir)) {
|
|
111
|
+
fs.mkdirSync(eventsDir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
// Symlink check — prevent writing to redirected files
|
|
114
|
+
try {
|
|
115
|
+
if (fs.lstatSync(filePath).isSymbolicLink()) return 2;
|
|
116
|
+
} catch {
|
|
117
|
+
// File doesn't exist yet — safe to create
|
|
118
|
+
}
|
|
119
|
+
fs.appendFileSync(filePath, JSON.stringify(event) + "\n");
|
|
120
|
+
return 0;
|
|
121
|
+
} catch {
|
|
122
|
+
return 2;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -19,7 +19,7 @@ const SAFE_SID = /^[a-zA-Z0-9_-]+$/; // Allowlist for session_id — blocks path
|
|
|
19
19
|
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days — auto-cleanup threshold
|
|
20
20
|
|
|
21
21
|
// ─── Exports (for testing) ───────────────────────────────────────────────────
|
|
22
|
-
module.exports = { scrubSecrets, scrubUrl, buildEvent, summarize, shortPath };
|
|
22
|
+
module.exports = { scrubSecrets, scrubUrl, buildEvent, summarize, shortPath, buildEventStreamEntry, appendToEventsFile };
|
|
23
23
|
|
|
24
24
|
// ─── Main (stdin processing) ─────────────────────────────────────────────────
|
|
25
25
|
if (require.main === module) {
|
|
@@ -65,6 +65,10 @@ process.stdin.on("end", () => {
|
|
|
65
65
|
try { if (fs.lstatSync(file).isSymbolicLink()) return; } catch { /* file doesn't exist yet — safe */ }
|
|
66
66
|
fs.appendFileSync(file, JSON.stringify(event) + "\n");
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
// Event stream enrichment — additive only, does not affect heartbeat writes above
|
|
70
|
+
const streamEntry = buildEventStreamEntry(hook);
|
|
71
|
+
if (streamEntry) appendToEventsFile(gsdtDir, streamEntry);
|
|
68
72
|
} catch (e) {
|
|
69
73
|
// Silent failure — never interfere with Claude Code
|
|
70
74
|
}
|
|
@@ -178,3 +182,52 @@ function shortPath(p) {
|
|
|
178
182
|
}
|
|
179
183
|
return p.replace(/\\/g, "/");
|
|
180
184
|
}
|
|
185
|
+
|
|
186
|
+
// ─── Event Stream Enrichment ──────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
function buildEventStreamEntry(hook) {
|
|
189
|
+
const ts = new Date().toISOString();
|
|
190
|
+
const base = { ts, command: null, phase: null, trace_id: null };
|
|
191
|
+
const n = hook.hook_event_name;
|
|
192
|
+
if (n === "SessionStart") {
|
|
193
|
+
return { ...base, event_type: "session_start",
|
|
194
|
+
agent_id: hook.session_id || null, parent_agent_id: null,
|
|
195
|
+
reasoning: hook.model || null, outcome: null };
|
|
196
|
+
}
|
|
197
|
+
if (n === "SessionEnd") {
|
|
198
|
+
return { ...base, event_type: "session_end",
|
|
199
|
+
agent_id: hook.session_id || null, parent_agent_id: null,
|
|
200
|
+
reasoning: hook.reason || null, outcome: null };
|
|
201
|
+
}
|
|
202
|
+
if (n === "SubagentStart") {
|
|
203
|
+
return { ...base, event_type: "subagent_spawn",
|
|
204
|
+
agent_id: hook.agent_id || null,
|
|
205
|
+
parent_agent_id: hook.parent_agent_id || hook.session_id || null,
|
|
206
|
+
reasoning: hook.agent_type || null, outcome: null };
|
|
207
|
+
}
|
|
208
|
+
if (n === "SubagentStop") {
|
|
209
|
+
return { ...base, event_type: "subagent_complete",
|
|
210
|
+
agent_id: hook.agent_id || null,
|
|
211
|
+
parent_agent_id: hook.parent_agent_id || hook.session_id || null,
|
|
212
|
+
reasoning: null, outcome: null };
|
|
213
|
+
}
|
|
214
|
+
if (n === "PostToolUse") {
|
|
215
|
+
return { ...base, event_type: "tool_call",
|
|
216
|
+
agent_id: hook.agent_id || hook.session_id || null, parent_agent_id: null,
|
|
217
|
+
reasoning: hook.tool_name || null, outcome: null };
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function appendToEventsFile(gsdtDir, entry) {
|
|
223
|
+
try {
|
|
224
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
225
|
+
const eventsDir = path.join(gsdtDir, "events");
|
|
226
|
+
if (!fs.existsSync(eventsDir)) fs.mkdirSync(eventsDir, { recursive: true });
|
|
227
|
+
const filePath = path.join(eventsDir, `${date}.jsonl`);
|
|
228
|
+
try { if (fs.lstatSync(filePath).isSymbolicLink()) return; } catch { /* safe */ }
|
|
229
|
+
fs.appendFileSync(filePath, JSON.stringify(entry) + "\n");
|
|
230
|
+
} catch {
|
|
231
|
+
// Silent failure — never interfere with Claude Code
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -56,6 +56,8 @@ PROJECT or FEATURE or SCAN
|
|
|
56
56
|
| `/user:gsd-t-status` | Cross-domain progress view |
|
|
57
57
|
| `/user:gsd-t-debug` | Systematic debugging |
|
|
58
58
|
| `/user:gsd-t-quick` | Fast task, respects contracts |
|
|
59
|
+
| `/user:gsd-t-reflect` | Generate retrospective from event stream, propose memory updates |
|
|
60
|
+
| `/user:gsd-t-visualize` | Launch browser dashboard |
|
|
59
61
|
| `/user:gsd-t-health` | Validate .gsd-t/ structure, optionally repair |
|
|
60
62
|
| `/user:gsd-t-pause` | Save exact position for reliable resume |
|
|
61
63
|
| `/user:gsd-t-populate` | Auto-populate docs from existing codebase |
|