Merge pull request #1266 from gsd-build/fix/stale-hook-check-path-1249
fix: stale hook detection checks wrong directory path
This commit is contained in:
@@ -409,6 +409,39 @@ git bisect bad # or good, based on testing
|
||||
|
||||
100 commits between working and broken: ~7 tests to find exact breaking commit.
|
||||
|
||||
## Follow the Indirection
|
||||
|
||||
**When:** Code constructs paths, URLs, keys, or references from variables — and the constructed value might not point where you expect.
|
||||
|
||||
**The trap:** You read code that builds a path like `path.join(configDir, 'hooks')` and assume it's correct because it looks reasonable. But you never verified that the constructed path matches where another part of the system actually writes/reads.
|
||||
|
||||
**How:**
|
||||
1. Find the code that **produces** the value (writer/installer/creator)
|
||||
2. Find the code that **consumes** the value (reader/checker/validator)
|
||||
3. Trace the actual resolved value in both — do they agree?
|
||||
4. Check every variable in the path construction — where does each come from? What's its actual value at runtime?
|
||||
|
||||
**Common indirection bugs:**
|
||||
- Path A writes to `dir/sub/hooks/` but Path B checks `dir/hooks/` (directory mismatch)
|
||||
- Config value comes from cache/template that wasn't updated
|
||||
- Variable is derived differently in two places (e.g., one adds a subdirectory, the other doesn't)
|
||||
- Template placeholder (`{{VERSION}}`) not substituted in all code paths
|
||||
|
||||
**Example:** Stale hook warning persists after update
|
||||
```
|
||||
Check code says: hooksDir = path.join(configDir, 'hooks')
|
||||
configDir = ~/.claude
|
||||
→ checks ~/.claude/hooks/
|
||||
|
||||
Installer says: hooksDest = path.join(targetDir, 'hooks')
|
||||
targetDir = ~/.claude/get-shit-done
|
||||
→ writes to ~/.claude/get-shit-done/hooks/
|
||||
|
||||
MISMATCH: Checker looks in wrong directory → hooks "not found" → reported as stale
|
||||
```
|
||||
|
||||
**The discipline:** Never assume a constructed path is correct. Resolve it to its actual value and verify the other side agrees. When two systems share a resource (file, directory, key), trace the full path in both.
|
||||
|
||||
## Technique Selection
|
||||
|
||||
| Situation | Technique |
|
||||
@@ -419,6 +452,7 @@ git bisect bad # or good, based on testing
|
||||
| Know the desired output | Working backwards |
|
||||
| Used to work, now doesn't | Differential debugging, Git bisect |
|
||||
| Many possible causes | Comment out everything, Binary search |
|
||||
| Paths, URLs, keys constructed from variables | Follow the indirection |
|
||||
| Always | Observability first (before making changes) |
|
||||
|
||||
## Combining Techniques
|
||||
|
||||
@@ -3900,6 +3900,11 @@ function install(isGlobal, runtime = 'claude') {
|
||||
}
|
||||
}
|
||||
|
||||
// Clear stale update cache so next session re-evaluates hook versions
|
||||
// targetDir is e.g. ~/.claude/get-shit-done/, parent is the config dir
|
||||
const updateCacheFile = path.join(path.dirname(targetDir), 'cache', 'gsd-update-check.json');
|
||||
try { fs.unlinkSync(updateCacheFile); } catch (e) { /* cache may not exist yet */ }
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
|
||||
process.exit(1);
|
||||
|
||||
@@ -65,9 +65,10 @@ const child = spawn(process.execPath, ['-e', `
|
||||
} catch (e) {}
|
||||
|
||||
// Check for stale hooks — compare hook version headers against installed VERSION
|
||||
// Hooks live inside get-shit-done/hooks/, not configDir/hooks/
|
||||
let staleHooks = [];
|
||||
if (configDir) {
|
||||
const hooksDir = path.join(configDir, 'hooks');
|
||||
const hooksDir = path.join(configDir, 'get-shit-done', 'hooks');
|
||||
try {
|
||||
if (fs.existsSync(hooksDir)) {
|
||||
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.startsWith('gsd-') && f.endsWith('.js'));
|
||||
|
||||
@@ -968,6 +968,26 @@ describe('stale hook filter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── stale hook path regression (#1249) ──────────────────────────────────────
|
||||
|
||||
describe('stale hook path', () => {
|
||||
test('gsd-check-update.js checks get-shit-done/hooks/ not configDir/hooks/', () => {
|
||||
const content = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'hooks', 'gsd-check-update.js'), 'utf-8'
|
||||
);
|
||||
assert.ok(
|
||||
content.includes("path.join(configDir, 'get-shit-done', 'hooks')"),
|
||||
'stale hook check must look in configDir/get-shit-done/hooks/, not configDir/hooks/'
|
||||
);
|
||||
assert.ok(
|
||||
!content.includes("path.join(configDir, 'hooks')") ||
|
||||
content.indexOf("path.join(configDir, 'get-shit-done', 'hooks')") <
|
||||
content.indexOf("path.join(configDir, 'hooks')") + 100, // allow the old pattern only if corrected version exists first
|
||||
'should not use the wrong hooks path'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── resolveWorktreeRoot ─────────────────────────────────────────────────────
|
||||
|
||||
describe('resolveWorktreeRoot', () => {
|
||||
|
||||
Reference in New Issue
Block a user