fix: hook version tracking, stale hook detection, stdin timeout, and session-report command (#1153, #1157, #1161, #1162) (#1163)

* fix: hook version tracking, stale hook detection, and stdin timeout increase

- Add gsd-hook-version header to all hook files for version tracking (#1153)
- Install.js now stamps current version into hooks during installation
- gsd-check-update.js detects stale hooks by comparing version headers
- gsd-statusline.js shows warning when stale hooks are detected
- Increase context monitor stdin timeout from 3s to 10s (#1162)
- Set +x permission on hook files during installation (#1162)

Fixes #1153, #1162, #1161

* feat: add /gsd:session-report command for post-session summary generation

Adds a new command that generates SESSION_REPORT.md with:
- Work performed summary (phases touched, commits, files changed)
- Key outcomes and decisions made
- Active blockers and open items
- Estimated resource usage metrics

Reports are written to .planning/reports/ with date-stamped filenames.

Closes #1157

* test: update expected skill count from 39 to 40 for new session-report command
This commit is contained in:
Tom Boucher
2026-03-18 11:57:20 -04:00
committed by GitHub
parent 14c1dd845b
commit e7198f419f
6 changed files with 212 additions and 5 deletions

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Check for GSD updates in background, write result to cache
// Called by SessionStart hook - runs once per session
@@ -43,6 +44,7 @@ if (!fs.existsSync(cacheDir)) {
// Run check in background (spawn background process, windowsHide prevents console flash)
const child = spawn(process.execPath, ['-e', `
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const cacheFile = ${JSON.stringify(cacheFile)};
@@ -51,14 +53,43 @@ const child = spawn(process.execPath, ['-e', `
// Check project directory first (local install), then global
let installed = '0.0.0';
let configDir = '';
try {
if (fs.existsSync(projectVersionFile)) {
installed = fs.readFileSync(projectVersionFile, 'utf8').trim();
configDir = path.dirname(path.dirname(projectVersionFile));
} else if (fs.existsSync(globalVersionFile)) {
installed = fs.readFileSync(globalVersionFile, 'utf8').trim();
configDir = path.dirname(path.dirname(globalVersionFile));
}
} catch (e) {}
// Check for stale hooks — compare hook version headers against installed VERSION
let staleHooks = [];
if (configDir) {
const hooksDir = path.join(configDir, 'hooks');
try {
if (fs.existsSync(hooksDir)) {
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.js'));
for (const hookFile of hookFiles) {
try {
const content = fs.readFileSync(path.join(hooksDir, hookFile), 'utf8');
const versionMatch = content.match(/\\/\\/ gsd-hook-version:\\s*(.+)/);
if (versionMatch) {
const hookVersion = versionMatch[1].trim();
if (hookVersion !== installed && !hookVersion.includes('{{')) {
staleHooks.push({ file: hookFile, hookVersion, installedVersion: installed });
}
} else {
// No version header at all — definitely stale (pre-version-tracking)
staleHooks.push({ file: hookFile, hookVersion: 'unknown', installedVersion: installed });
}
} catch (e) {}
}
}
} catch (e) {}
}
let latest = null;
try {
latest = execSync('npm view get-shit-done-cc version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();
@@ -68,7 +99,8 @@ const child = spawn(process.execPath, ['-e', `
update_available: latest && installed !== latest,
installed,
latest: latest || 'unknown',
checked: Math.floor(Date.now() / 1000)
checked: Math.floor(Date.now() / 1000),
stale_hooks: staleHooks.length > 0 ? staleHooks : undefined
};
fs.writeFileSync(cacheFile, JSON.stringify(result));

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Context Monitor - PostToolUse/AfterTool hook (Gemini uses AfterTool)
// Reads context metrics from the statusline bridge file and injects
// warnings when context usage is high. This makes the AGENT aware of
@@ -27,10 +28,11 @@ const STALE_SECONDS = 60; // ignore metrics older than 60s
const DEBOUNCE_CALLS = 5; // min tool uses between warnings
let input = '';
// Timeout guard: if stdin doesn't close within 3s (e.g. pipe issues on
// Windows/Git Bash), exit silently instead of hanging until Claude Code
// kills the process and reports "hook error". See #775.
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
// Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
// Windows/Git Bash, or slow Claude Code piping during large outputs),
// exit silently instead of hanging until Claude Code kills the process
// and reports "hook error". See #775, #1162.
const stdinTimeout = setTimeout(() => process.exit(0), 10000);
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Claude Code Statusline - GSD Edition
// Shows: model | current task | directory | context usage
@@ -99,6 +100,9 @@ process.stdin.on('end', () => {
if (cache.update_available) {
gsdUpdate = '\x1b[33m⬆ /gsd:update\x1b[0m │ ';
}
if (cache.stale_hooks && cache.stale_hooks.length > 0) {
gsdUpdate += '\x1b[31m⚠ stale hooks — run /gsd:update\x1b[0m │ ';
}
} catch (e) {}
}