@tekyzinc/gsd-t 2.17.0 → 2.18.2

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,29 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [2.18.2] - 2026-02-16
6
+
7
+ ### Added
8
+ - Gap Analysis Gate in `gsd-t-complete-milestone` — mandatory requirements verification before archiving
9
+ - Self-correction loop: auto-fixes gaps, re-verifies, re-analyzes (up to 2 cycles), stops if unresolvable
10
+ - Explicit Playwright E2E test execution in milestone test verification step
11
+
12
+ ## [2.18.1] - 2026-02-16
13
+
14
+ ### Added
15
+ - Auto-Init Guard — GSD-T workflow commands automatically run `gsd-t-init` if any init files are missing, then continue with the original command
16
+ - `gsd-t-init` copies `~/.claude/settings.local` → `.claude/settings.local.json` during project initialization
17
+ - Exempt commands that skip auto-init: `gsd-t-init`, `gsd-t-init-scan-setup`, `gsd-t-help`, `gsd-t-version-update`, `gsd-t-version-update-all`, `gsd-t-prompt`, `gsd-t-brainstorm`
18
+
19
+ ## [2.18.0] - 2026-02-16
20
+
21
+ ### Added
22
+ - Heartbeat system — real-time event streaming from Claude Code sessions via async hooks
23
+ - `scripts/gsd-t-heartbeat.js` — hook handler that writes JSONL events to `.gsd-t/heartbeat-{session_id}.jsonl`
24
+ - 9 Claude Code hooks: SessionStart, PostToolUse, SubagentStart, SubagentStop, TaskCompleted, TeammateIdle, Notification, Stop, SessionEnd
25
+ - Installer auto-configures heartbeat hooks in settings.json (all async, zero performance impact)
26
+ - Event types: session lifecycle, tool calls with file/command summaries, agent spawn/stop/idle, task completions
27
+
5
28
  ## [2.17.0] - 2026-02-16
6
29
 
7
30
  ### 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/`);
@@ -17,6 +17,22 @@ If status is not VERIFIED:
17
17
 
18
18
  If `--force` flag provided, proceed with warning in archive.
19
19
 
20
+ ## Step 1.5: Gap Analysis Gate
21
+
22
+ After verification passes, run a gap analysis against `docs/requirements.md` scoped to this milestone's deliverables:
23
+
24
+ 1. Identify which requirements this milestone was supposed to satisfy (from domain scopes, tasks, and milestone definition)
25
+ 2. Run `gsd-t-gap-analysis` against those requirements, comparing spec to actual code
26
+ 3. If **all gaps resolved** (100% Implemented) → proceed to Step 2
27
+ 4. If **gaps found** (Partial, Incorrect, or Not Implemented):
28
+ a. Auto-fix: execute remediation for each gap (prioritize Critical → High → Medium)
29
+ b. Run affected tests (unit + integration + Playwright E2E if configured)
30
+ c. Re-run `gsd-t-verify` to confirm fixes don't break anything
31
+ d. Re-run gap analysis to confirm gaps are resolved
32
+ e. If gaps remain after **2 fix cycles** → STOP and report unresolved gaps to user
33
+
34
+ This is a **mandatory gate** — the milestone cannot be archived with known gaps against its requirements.
35
+
20
36
  ## Step 2: Gather Milestone Artifacts
21
37
 
22
38
  Collect all files related to this milestone:
@@ -163,10 +179,11 @@ Before creating the git tag, verify all documentation is up to date:
163
179
 
164
180
  Before creating the git tag, verify the milestone is truly complete:
165
181
 
166
- 1. **Run the full test suite**: Execute ALL tests — unit, integration, and E2E if available
167
- 2. **Verify all pass**: Every test must pass. If any fail, fix before tagging (up to 2 attempts)
168
- 3. **Compare to baseline**: If a test baseline was recorded at milestone start, verify coverage has improved or at minimum not regressed
169
- 4. **Log test results**: Include test pass/fail counts in the milestone summary (Step 4)
182
+ 1. **Run the full test suite**: Execute ALL tests — unit, integration, and E2E
183
+ 2. **Run Playwright E2E** (if configured): Detect `playwright.config.*` or Playwright in dependencies. If present, run the full Playwright suite. If specs are missing or stale, invoke `gsd-t-test-sync` first.
184
+ 3. **Verify all pass**: Every test must pass. If any fail, fix before tagging (up to 2 attempts)
185
+ 4. **Compare to baseline**: If a test baseline was recorded at milestone start, verify coverage has improved or at minimum not regressed
186
+ 5. **Log test results**: Include test pass/fail counts in the milestone summary (Step 4)
170
187
 
171
188
  ## Step 8: Create Git Tag
172
189
 
@@ -18,6 +18,14 @@ Report current state and ask if user wants to reset or continue.
18
18
  Offer to migrate: "Found legacy GSD structure. Want me to migrate to GSD-T?"
19
19
  If yes, read `.gsd/` state and create equivalent `.gsd-t/` structure.
20
20
 
21
+ ## Step 1.5: Copy Local Settings
22
+
23
+ If `~/.claude/settings.local` exists and `.claude/settings.local.json` does not exist in the project:
24
+ 1. Create the `.claude/` directory in the project root if it doesn't exist
25
+ 2. Copy `~/.claude/settings.local` → `.claude/settings.local.json`
26
+
27
+ Skip silently if the source file doesn't exist or the target already exists.
28
+
21
29
  ## Step 2: Create Directory Structure
22
30
 
23
31
  ```
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.2",
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
+ }
@@ -176,6 +176,21 @@ On session start, a version check hook outputs one of two messages. Show the res
176
176
 
177
177
  Only execute GSD-T workflow behavior when a `/gsd-t-*` command is invoked or when actively mid-phase (resumed via `/gsd-t-resume`). **Plain text messages — especially questions — should be answered conversationally.** Do not launch into workflow execution, file reading, or phase advancement from a question or comment. If the user wants work done, they will invoke a command.
178
178
 
179
+ ## Auto-Init Guard
180
+
181
+ Before executing any GSD-T workflow command, check if **any** of these files are missing in the current project:
182
+ - `.gsd-t/progress.md`, `.gsd-t/backlog.md`, `.gsd-t/backlog-settings.md`
183
+ - `.gsd-t/contracts/`, `.gsd-t/domains/`
184
+ - `.claude/settings.local.json` (if `~/.claude/settings.local` exists)
185
+ - `CLAUDE.md`, `README.md`
186
+ - `docs/requirements.md`, `docs/architecture.md`, `docs/workflows.md`, `docs/infrastructure.md`
187
+
188
+ If any are missing:
189
+ 1. Run `gsd-t-init` automatically (it skips files that already exist)
190
+ 2. Then continue with the originally requested command
191
+
192
+ **Exempt commands** (do not trigger auto-init): `gsd-t-init`, `gsd-t-init-scan-setup`, `gsd-t-help`, `gsd-t-version-update`, `gsd-t-version-update-all`, `gsd-t-prompt`, `gsd-t-brainstorm`.
193
+
179
194
  ## Prime Rule
180
195
  KEEP GOING. Only stop for:
181
196
  1. Unrecoverable errors after 2 fix attempts