@tekyzinc/gsd-t 2.17.0 → 2.18.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 +9 -0
- package/bin/gsd-t.js +86 -2
- package/package.json +2 -1
- package/scripts/gsd-t-heartbeat.js +156 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [2.18.0] - 2026-02-16
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Heartbeat system — real-time event streaming from Claude Code sessions via async hooks
|
|
9
|
+
- `scripts/gsd-t-heartbeat.js` — hook handler that writes JSONL events to `.gsd-t/heartbeat-{session_id}.jsonl`
|
|
10
|
+
- 9 Claude Code hooks: SessionStart, PostToolUse, SubagentStart, SubagentStop, TaskCompleted, TeammateIdle, Notification, Stop, SessionEnd
|
|
11
|
+
- Installer auto-configures heartbeat hooks in settings.json (all async, zero performance impact)
|
|
12
|
+
- Event types: session lifecycle, tool calls with file/command summaries, agent spawn/stop/idle, task completions
|
|
13
|
+
|
|
5
14
|
## [2.17.0] - 2026-02-16
|
|
6
15
|
|
|
7
16
|
### Added
|
package/bin/gsd-t.js
CHANGED
|
@@ -24,6 +24,7 @@ const { execSync } = require("child_process");
|
|
|
24
24
|
|
|
25
25
|
const CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
26
26
|
const COMMANDS_DIR = path.join(CLAUDE_DIR, "commands");
|
|
27
|
+
const SCRIPTS_DIR = path.join(CLAUDE_DIR, "scripts");
|
|
27
28
|
const GLOBAL_CLAUDE_MD = path.join(CLAUDE_DIR, "CLAUDE.md");
|
|
28
29
|
const SETTINGS_JSON = path.join(CLAUDE_DIR, "settings.json");
|
|
29
30
|
const VERSION_FILE = path.join(CLAUDE_DIR, ".gsd-t-version");
|
|
@@ -33,6 +34,7 @@ const UPDATE_CHECK_FILE = path.join(CLAUDE_DIR, ".gsd-t-update-check");
|
|
|
33
34
|
// Where our package files live (relative to this script)
|
|
34
35
|
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
35
36
|
const PKG_COMMANDS = path.join(PKG_ROOT, "commands");
|
|
37
|
+
const PKG_SCRIPTS = path.join(PKG_ROOT, "scripts");
|
|
36
38
|
const PKG_TEMPLATES = path.join(PKG_ROOT, "templates");
|
|
37
39
|
const PKG_EXAMPLES = path.join(PKG_ROOT, "examples");
|
|
38
40
|
|
|
@@ -144,6 +146,84 @@ function getInstalledCommands() {
|
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
// ─── Heartbeat ──────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
const HEARTBEAT_SCRIPT = "gsd-t-heartbeat.js";
|
|
152
|
+
const HEARTBEAT_HOOKS = [
|
|
153
|
+
"SessionStart", "PostToolUse", "SubagentStart", "SubagentStop",
|
|
154
|
+
"TaskCompleted", "TeammateIdle", "Notification", "Stop", "SessionEnd"
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
function installHeartbeat() {
|
|
158
|
+
ensureDir(SCRIPTS_DIR);
|
|
159
|
+
|
|
160
|
+
// Copy heartbeat script
|
|
161
|
+
const src = path.join(PKG_SCRIPTS, HEARTBEAT_SCRIPT);
|
|
162
|
+
const dest = path.join(SCRIPTS_DIR, HEARTBEAT_SCRIPT);
|
|
163
|
+
|
|
164
|
+
if (!fs.existsSync(src)) {
|
|
165
|
+
warn("Heartbeat script not found in package — skipping");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const srcContent = fs.readFileSync(src, "utf8");
|
|
170
|
+
const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, "utf8") : "";
|
|
171
|
+
|
|
172
|
+
if (srcContent !== destContent) {
|
|
173
|
+
copyFile(src, dest, HEARTBEAT_SCRIPT);
|
|
174
|
+
} else {
|
|
175
|
+
info("Heartbeat script unchanged");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Configure hooks in settings.json
|
|
179
|
+
const hooksAdded = configureHeartbeatHooks(dest);
|
|
180
|
+
if (hooksAdded > 0) {
|
|
181
|
+
success(`${hooksAdded} heartbeat hooks configured in settings.json`);
|
|
182
|
+
} else {
|
|
183
|
+
info("Heartbeat hooks already configured");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function configureHeartbeatHooks(scriptPath) {
|
|
188
|
+
let settings = {};
|
|
189
|
+
if (fs.existsSync(SETTINGS_JSON)) {
|
|
190
|
+
try {
|
|
191
|
+
settings = JSON.parse(fs.readFileSync(SETTINGS_JSON, "utf8"));
|
|
192
|
+
} catch {
|
|
193
|
+
warn("settings.json has invalid JSON — cannot configure hooks");
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!settings.hooks) settings.hooks = {};
|
|
199
|
+
|
|
200
|
+
const cmd = `node "${scriptPath.replace(/\\/g, "\\\\")}"`;
|
|
201
|
+
let added = 0;
|
|
202
|
+
|
|
203
|
+
for (const event of HEARTBEAT_HOOKS) {
|
|
204
|
+
if (!settings.hooks[event]) settings.hooks[event] = [];
|
|
205
|
+
|
|
206
|
+
// Check if heartbeat hook already exists for this event
|
|
207
|
+
const hasHeartbeat = settings.hooks[event].some((entry) =>
|
|
208
|
+
entry.hooks && entry.hooks.some((h) => h.command && h.command.includes(HEARTBEAT_SCRIPT))
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (!hasHeartbeat) {
|
|
212
|
+
settings.hooks[event].push({
|
|
213
|
+
matcher: "",
|
|
214
|
+
hooks: [{ type: "command", command: cmd, async: true }],
|
|
215
|
+
});
|
|
216
|
+
added++;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (added > 0) {
|
|
221
|
+
fs.writeFileSync(SETTINGS_JSON, JSON.stringify(settings, null, 2));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return added;
|
|
225
|
+
}
|
|
226
|
+
|
|
147
227
|
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
148
228
|
|
|
149
229
|
function doInstall(opts = {}) {
|
|
@@ -226,10 +306,14 @@ function doInstall(opts = {}) {
|
|
|
226
306
|
copyFile(globalSrc, GLOBAL_CLAUDE_MD, "CLAUDE.md installed → ~/.claude/CLAUDE.md");
|
|
227
307
|
}
|
|
228
308
|
|
|
229
|
-
// 4.
|
|
309
|
+
// 4. Install heartbeat script + hooks
|
|
310
|
+
heading("Heartbeat (Real-time Events)");
|
|
311
|
+
installHeartbeat();
|
|
312
|
+
|
|
313
|
+
// 5. Save version
|
|
230
314
|
saveInstalledVersion();
|
|
231
315
|
|
|
232
|
-
//
|
|
316
|
+
// 6. Summary
|
|
233
317
|
heading("Installation Complete!");
|
|
234
318
|
log("");
|
|
235
319
|
log(` Commands: ${gsdtCommands.length} GSD-T + ${utilityCommands.length} utility commands in ~/.claude/commands/`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.0",
|
|
4
4
|
"description": "GSD-T: Contract-Driven Development for Claude Code — 41 slash commands with backlog management, impact analysis, test sync, and milestone archival",
|
|
5
5
|
"author": "Tekyz, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"bin/",
|
|
25
25
|
"commands/",
|
|
26
|
+
"scripts/",
|
|
26
27
|
"templates/",
|
|
27
28
|
"examples/",
|
|
28
29
|
"docs/",
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GSD-T Heartbeat — Claude Code Hook Event Writer
|
|
5
|
+
*
|
|
6
|
+
* Writes structured events to .gsd-t/heartbeat-{session_id}.jsonl
|
|
7
|
+
* Installed as an async hook for multiple Claude Code events.
|
|
8
|
+
*
|
|
9
|
+
* Events captured:
|
|
10
|
+
* SessionStart, PostToolUse, SubagentStart, SubagentStop,
|
|
11
|
+
* TaskCompleted, TeammateIdle, Notification, Stop, SessionEnd
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
|
|
17
|
+
let input = "";
|
|
18
|
+
process.stdin.setEncoding("utf8");
|
|
19
|
+
process.stdin.on("data", (d) => (input += d));
|
|
20
|
+
process.stdin.on("end", () => {
|
|
21
|
+
try {
|
|
22
|
+
const hook = JSON.parse(input);
|
|
23
|
+
const dir = hook.cwd || process.cwd();
|
|
24
|
+
const gsdtDir = path.join(dir, ".gsd-t");
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(gsdtDir)) return;
|
|
27
|
+
|
|
28
|
+
const sid = hook.session_id || "unknown";
|
|
29
|
+
const file = path.join(gsdtDir, `heartbeat-${sid}.jsonl`);
|
|
30
|
+
|
|
31
|
+
const event = buildEvent(hook);
|
|
32
|
+
if (event) {
|
|
33
|
+
fs.appendFileSync(file, JSON.stringify(event) + "\n");
|
|
34
|
+
}
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// Silent failure — never interfere with Claude Code
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function buildEvent(hook) {
|
|
41
|
+
const base = {
|
|
42
|
+
ts: new Date().toISOString(),
|
|
43
|
+
sid: hook.session_id,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
switch (hook.hook_event_name) {
|
|
47
|
+
case "SessionStart":
|
|
48
|
+
return {
|
|
49
|
+
...base,
|
|
50
|
+
evt: "session_start",
|
|
51
|
+
data: { source: hook.source, model: hook.model },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
case "PostToolUse":
|
|
55
|
+
return {
|
|
56
|
+
...base,
|
|
57
|
+
evt: "tool",
|
|
58
|
+
tool: hook.tool_name,
|
|
59
|
+
data: summarize(hook.tool_name, hook.tool_input),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
case "SubagentStart":
|
|
63
|
+
return {
|
|
64
|
+
...base,
|
|
65
|
+
evt: "agent_spawn",
|
|
66
|
+
data: { agent_id: hook.agent_id, agent_type: hook.agent_type },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
case "SubagentStop":
|
|
70
|
+
return {
|
|
71
|
+
...base,
|
|
72
|
+
evt: "agent_stop",
|
|
73
|
+
data: { agent_id: hook.agent_id, agent_type: hook.agent_type },
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
case "TaskCompleted":
|
|
77
|
+
return {
|
|
78
|
+
...base,
|
|
79
|
+
evt: "task_done",
|
|
80
|
+
data: { task: hook.task_subject, agent: hook.teammate_name },
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
case "TeammateIdle":
|
|
84
|
+
return {
|
|
85
|
+
...base,
|
|
86
|
+
evt: "agent_idle",
|
|
87
|
+
data: { agent: hook.teammate_name, team: hook.team_name },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
case "Notification":
|
|
91
|
+
return {
|
|
92
|
+
...base,
|
|
93
|
+
evt: "notification",
|
|
94
|
+
data: { message: hook.message, title: hook.title },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
case "Stop":
|
|
98
|
+
return { ...base, evt: "session_stop" };
|
|
99
|
+
|
|
100
|
+
case "SessionEnd":
|
|
101
|
+
return {
|
|
102
|
+
...base,
|
|
103
|
+
evt: "session_end",
|
|
104
|
+
data: { reason: hook.reason },
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
default:
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function summarize(tool, input) {
|
|
113
|
+
if (!tool || !input) return {};
|
|
114
|
+
switch (tool) {
|
|
115
|
+
case "Read":
|
|
116
|
+
return { file: shortPath(input.file_path) };
|
|
117
|
+
case "Edit":
|
|
118
|
+
return { file: shortPath(input.file_path) };
|
|
119
|
+
case "Write":
|
|
120
|
+
return { file: shortPath(input.file_path) };
|
|
121
|
+
case "Bash":
|
|
122
|
+
return {
|
|
123
|
+
cmd: (input.command || "").slice(0, 150),
|
|
124
|
+
desc: input.description,
|
|
125
|
+
};
|
|
126
|
+
case "Grep":
|
|
127
|
+
return { pattern: input.pattern, path: shortPath(input.path) };
|
|
128
|
+
case "Glob":
|
|
129
|
+
return { pattern: input.pattern };
|
|
130
|
+
case "Task":
|
|
131
|
+
return { desc: input.description, type: input.subagent_type };
|
|
132
|
+
case "WebSearch":
|
|
133
|
+
return { query: input.query };
|
|
134
|
+
case "WebFetch":
|
|
135
|
+
return { url: input.url };
|
|
136
|
+
case "NotebookEdit":
|
|
137
|
+
return { file: shortPath(input.notebook_path) };
|
|
138
|
+
default:
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function shortPath(p) {
|
|
144
|
+
if (!p) return null;
|
|
145
|
+
// Convert absolute paths to relative for readability
|
|
146
|
+
const cwd = process.cwd();
|
|
147
|
+
if (p.startsWith(cwd)) {
|
|
148
|
+
return p.slice(cwd.length + 1).replace(/\\/g, "/");
|
|
149
|
+
}
|
|
150
|
+
// For home-dir paths, abbreviate
|
|
151
|
+
const home = require("os").homedir();
|
|
152
|
+
if (p.startsWith(home)) {
|
|
153
|
+
return "~" + p.slice(home.length).replace(/\\/g, "/");
|
|
154
|
+
}
|
|
155
|
+
return p.replace(/\\/g, "/");
|
|
156
|
+
}
|