feat(graceful-handoff): 2.0 — Stop hook auto-execute + pipeline staging fix [skip-docs]

Step 5 of v2.0 plan + critical pipeline fix.

Stop hook (hooks/scripts/stop-context-monitor.mjs):
- Estimates context usage from transcript size (chars/3.5 / window_size)
- At ≥70%, spawns handoff-pipeline.mjs --auto --no-push synchronously
- Reads context_window_size from payload (supports 1M windows)
- Lock file at <transcript_dir>/.handoff-lock-<session_id>
- Gracefully handles missing CLAUDE_PLUGIN_ROOT, missing transcript

Pipeline fix (scripts/handoff-pipeline.mjs):
- REMOVED `git add -A` (CLAUDE.md anti-pattern: scoops up unrelated WIP)
- Now stages ONLY artifact + REMEMBER.md/TODO.md if present
- New regression test 'pipeline never stages unrelated dirty files'

Tests: 7 stop-hook tests use stub pipeline (no real git operations);
11 pipeline tests including new regression for explicit staging.
This commit is contained in:
Kjell Tore Guttormsen 2026-05-01 05:57:41 +02:00
commit 81aba9a5f5
4 changed files with 357 additions and 5 deletions

View file

@ -296,9 +296,17 @@ async function main() {
}
if (proceed) {
try {
// Stage all and run git commit (pre-commit hooks respected; never --no-verify).
execSync('git add -A', { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
execFileSync('git', ['commit', '-m', commitMessage], { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
// CRITICAL: never `git add -A` — that scoops up unrelated work-in-progress.
// Stage ONLY the handoff artifact + optional REMEMBER.md/TODO.md if present.
// Other dirty files stay in working tree for the user.
const stageList = [artifactPath];
for (const candidate of ['REMEMBER.md', 'TODO.md']) {
const p = join(classification.writeDir, candidate);
if (existsSync(p)) stageList.push(p);
}
execFileSync('git', ['add', '--', ...stageList], { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
// git commit with -- pathspec limits commit to those paths from index.
execFileSync('git', ['commit', '-m', commitMessage, '--', ...stageList], { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
actionsTaken.push('committed');
} catch (e) {
errors.push(`commit failed: ${(e.stderr || e.message || '').toString().slice(0, 200)}`);