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:
parent
1efb1b3176
commit
81aba9a5f5
4 changed files with 357 additions and 5 deletions
|
|
@ -97,11 +97,15 @@ test('--no-commit skips git operations even when dirty', async () => {
|
|||
rmSync(repo, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('idempotency: second --auto run on clean state with recent artifact is no-op', async () => {
|
||||
test('idempotency: second --auto run on clean tree with recent artifact is no-op', async () => {
|
||||
const repo = makeTempRepo();
|
||||
writeFileSync(join(repo, 'foo.txt'), 'change\n');
|
||||
// First run: dirty, commits
|
||||
// First run: dirty, writes artifact and commits ONLY the artifact (not foo.txt)
|
||||
await runPipeline(repo, ['--auto', '--non-interactive', '--no-push']);
|
||||
// Clean up the unrelated dirty file so second run sees a CLEAN tree.
|
||||
// The pipeline must NEVER auto-stage user's other dirty files (CLAUDE.md
|
||||
// anti-pattern) — the test explicitly removes it to isolate idempotency.
|
||||
rmSync(join(repo, 'foo.txt'));
|
||||
// Second run: clean tree, recent artifact exists → idempotent no-op
|
||||
const result = await runPipeline(repo, ['--auto', '--non-interactive', '--no-push']);
|
||||
const json = JSON.parse(result.stdout);
|
||||
|
|
@ -112,6 +116,28 @@ test('idempotency: second --auto run on clean state with recent artifact is no-o
|
|||
rmSync(repo, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('pipeline never stages unrelated dirty files (no git add -A regression)', async () => {
|
||||
const repo = makeTempRepo();
|
||||
// Two unrelated dirty files — pipeline should NOT commit them
|
||||
writeFileSync(join(repo, 'unrelated-1.txt'), 'user work\n');
|
||||
writeFileSync(join(repo, 'unrelated-2.md'), '# user notes\n');
|
||||
await runPipeline(repo, ['--auto', '--non-interactive', '--no-push']);
|
||||
// After commit, unrelated files must STILL be in working tree (not committed)
|
||||
const { execFileSync } = await import('node:child_process');
|
||||
const lastCommit = execFileSync('git', ['show', '--name-only', '--pretty=', 'HEAD'], {
|
||||
cwd: repo, encoding: 'utf-8',
|
||||
}).trim().split('\n').filter(Boolean);
|
||||
assert.ok(!lastCommit.includes('unrelated-1.txt'), `unrelated-1.txt should NOT be in HEAD commit, got: ${lastCommit}`);
|
||||
assert.ok(!lastCommit.includes('unrelated-2.md'), `unrelated-2.md should NOT be in HEAD commit, got: ${lastCommit}`);
|
||||
// The artifact SHOULD be in HEAD
|
||||
assert.ok(lastCommit.some(f => f.includes('NEXT-SESSION')), `artifact should be in HEAD, got: ${lastCommit}`);
|
||||
// unrelated files still untracked
|
||||
const status = execFileSync('git', ['status', '--porcelain'], { cwd: repo, encoding: 'utf-8' });
|
||||
assert.match(status, /unrelated-1\.txt/);
|
||||
assert.match(status, /unrelated-2\.md/);
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('detached HEAD is detected and reported (no commit attempted)', async () => {
|
||||
const repo = makeTempRepo();
|
||||
// Detach HEAD
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue