@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 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. Save version
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
- // 5. Summary
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.17.0",
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
+ }