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

@@ -2606,10 +2606,14 @@ function install(isGlobal, runtime = 'claude') {
if (fs.statSync(srcFile).isFile()) { if (fs.statSync(srcFile).isFile()) {
const destFile = path.join(hooksDest, entry); const destFile = path.join(hooksDest, entry);
// Template .js files to replace '.claude' with runtime-specific config dir // Template .js files to replace '.claude' with runtime-specific config dir
// and stamp the current GSD version into the hook version header
if (entry.endsWith('.js')) { if (entry.endsWith('.js')) {
let content = fs.readFileSync(srcFile, 'utf8'); let content = fs.readFileSync(srcFile, 'utf8');
content = content.replace(/'\.claude'/g, configDirReplacement); content = content.replace(/'\.claude'/g, configDirReplacement);
content = content.replace(/\{\{GSD_VERSION\}\}/g, pkg.version);
fs.writeFileSync(destFile, content); fs.writeFileSync(destFile, content);
// Ensure hook files are executable (fixes #1162 — missing +x permission)
try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows doesn't support chmod */ }
} else { } else {
fs.copyFileSync(srcFile, destFile); fs.copyFileSync(srcFile, destFile);
} }

View File

@@ -0,0 +1,19 @@
---
name: gsd:session-report
description: Generate a session report with token usage estimates, work summary, and outcomes
allowed-tools:
- Read
- Bash
- Write
---
<objective>
Generate a structured SESSION_REPORT.md document capturing session outcomes, work performed, and estimated resource usage. Provides a shareable artifact for post-session review.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/session-report.md
</execution_context>
<process>
Execute the session-report workflow from @~/.claude/get-shit-done/workflows/session-report.md end-to-end.
</process>

View File

@@ -0,0 +1,146 @@
<purpose>
Generate a post-session summary document capturing work performed, outcomes achieved, and estimated resource usage. Writes SESSION_REPORT.md to .planning/reports/ for human review and stakeholder sharing.
</purpose>
<required_reading>
Read all files referenced by the invoking prompt's execution_context before starting.
</required_reading>
<process>
<step name="gather_session_data">
Collect session data from available sources:
1. **STATE.md** — current phase, milestone, progress, blockers, decisions
2. **Git log** — commits made during this session (last 24h or since last report)
3. **Plan/Summary files** — plans executed, summaries written
4. **ROADMAP.md** — milestone context and phase goals
```bash
# Get recent commits (last 24 hours)
git log --oneline --since="24 hours ago" --no-merges 2>/dev/null || echo "No recent commits"
# Count files changed
git diff --stat HEAD~10 HEAD 2>/dev/null | tail -1 || echo "No diff available"
```
Read `.planning/STATE.md` to get:
- Current milestone and phase
- Progress percentage
- Active blockers
- Recent decisions
Read `.planning/ROADMAP.md` to get milestone name and goals.
Check for existing reports:
```bash
ls -la .planning/reports/SESSION_REPORT*.md 2>/dev/null || echo "No previous reports"
```
</step>
<step name="estimate_usage">
Estimate token usage from observable signals:
- Count of tool calls is not directly available, so estimate from git activity and file operations
- Note: This is an **estimate** — exact token counts require API-level instrumentation not available to hooks
Estimation heuristics:
- Each commit ≈ 1 plan cycle (research + plan + execute + verify)
- Each plan file ≈ 2,000-5,000 tokens of agent context
- Each summary file ≈ 1,000-2,000 tokens generated
- Subagent spawns multiply by ~1.5x per agent type used
</step>
<step name="generate_report">
Create the report directory and file:
```bash
mkdir -p .planning/reports
```
Write `.planning/reports/SESSION_REPORT.md` (or `.planning/reports/YYYYMMDD-session-report.md` if previous reports exist):
```markdown
# GSD Session Report
**Generated:** [timestamp]
**Project:** [from PROJECT.md title or directory name]
**Milestone:** [N] — [milestone name from ROADMAP.md]
---
## Session Summary
**Duration:** [estimated from first to last commit timestamp, or "Single session"]
**Phase Progress:** [from STATE.md]
**Plans Executed:** [count of summaries written this session]
**Commits Made:** [count from git log]
## Work Performed
### Phases Touched
[List phases worked on with brief description of what was done]
### Key Outcomes
[Bullet list of concrete deliverables: files created, features implemented, bugs fixed]
### Decisions Made
[From STATE.md decisions table, if any were added this session]
## Files Changed
[Summary of files modified, created, deleted — from git diff stat]
## Blockers & Open Items
[Active blockers from STATE.md]
[Any TODO items created during session]
## Estimated Resource Usage
| Metric | Estimate |
|--------|----------|
| Commits | [N] |
| Files changed | [N] |
| Plans executed | [N] |
| Subagents spawned | [estimated] |
> **Note:** Token and cost estimates require API-level instrumentation.
> These metrics reflect observable session activity only.
---
*Generated by `/gsd:session-report`*
```
</step>
<step name="display_result">
Show the user:
```
## Session Report Generated
📄 `.planning/reports/[filename].md`
### Highlights
- **Commits:** [N]
- **Files changed:** [N]
- **Phase progress:** [X]%
- **Plans executed:** [N]
```
If this is the first report, mention:
```
💡 Run `/gsd:session-report` at the end of each session to build a history of project activity.
```
</step>
</process>
<success_criteria>
- [ ] Session data gathered from STATE.md, git log, and plan files
- [ ] Report written to .planning/reports/
- [ ] Report includes work summary, outcomes, and file changes
- [ ] Filename includes date to prevent overwrites
- [ ] Result summary displayed to user
</success_criteria>

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Check for GSD updates in background, write result to cache // Check for GSD updates in background, write result to cache
// Called by SessionStart hook - runs once per session // 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) // Run check in background (spawn background process, windowsHide prevents console flash)
const child = spawn(process.execPath, ['-e', ` const child = spawn(process.execPath, ['-e', `
const fs = require('fs'); const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const cacheFile = ${JSON.stringify(cacheFile)}; const cacheFile = ${JSON.stringify(cacheFile)};
@@ -51,14 +53,43 @@ const child = spawn(process.execPath, ['-e', `
// Check project directory first (local install), then global // Check project directory first (local install), then global
let installed = '0.0.0'; let installed = '0.0.0';
let configDir = '';
try { try {
if (fs.existsSync(projectVersionFile)) { if (fs.existsSync(projectVersionFile)) {
installed = fs.readFileSync(projectVersionFile, 'utf8').trim(); installed = fs.readFileSync(projectVersionFile, 'utf8').trim();
configDir = path.dirname(path.dirname(projectVersionFile));
} else if (fs.existsSync(globalVersionFile)) { } else if (fs.existsSync(globalVersionFile)) {
installed = fs.readFileSync(globalVersionFile, 'utf8').trim(); installed = fs.readFileSync(globalVersionFile, 'utf8').trim();
configDir = path.dirname(path.dirname(globalVersionFile));
} }
} catch (e) {} } 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; let latest = null;
try { try {
latest = execSync('npm view get-shit-done-cc version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim(); 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, update_available: latest && installed !== latest,
installed, installed,
latest: latest || 'unknown', 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)); fs.writeFileSync(cacheFile, JSON.stringify(result));

View File

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

View File

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