voyage-lekerepo trial (2026-10-06) point 7, operator decision 2026-10-08 («grønt merke på låsen»): the stop guard blocked Stop three times while the executor waited for the controller, with the step's test already green. - test-lock.mjs green --cmd "<Verify>" runs the step's test command (same allowlist + denylist as done-line) and records green_at only on exit 0; pending clears it; a new lock starts without it. - stop-test-lock lets a green-marked lock stop; the write guard still refuses the locked test files. - trekexecute: D1 marks green after a PASS; each fix round marks pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
54 lines
2.5 KiB
JavaScript
54 lines
2.5 KiB
JavaScript
#!/usr/bin/env node
|
|
// Hook: stop-test-lock.mjs
|
|
// Event: Stop
|
|
// Purpose: P9 (3) -- "done" is refused while a step's tests are locked (K8,
|
|
// lib/util/test-lock.mjs). The executor unlocks a step only after its test
|
|
// passes, so a held lock means the step's test has not been seen passing.
|
|
//
|
|
// Scope: only the session that took the lock is held (the lock records
|
|
// CLAUDE_CODE_SESSION_ID, which the docs say matches the hook input's
|
|
// session_id: code.claude.com/docs/en/env-vars). Any other session
|
|
// in the same repository stops freely, so a lock left behind by a crash
|
|
// cannot trap unrelated work.
|
|
// Loop guard: when stop_hook_active is true, Claude Code is already continuing
|
|
// because of a stop hook; this hook then lets the stop through
|
|
// (code.claude.com/docs/en/hooks section Stop input; hooks-guide "Stop hook hits
|
|
// the block cap").
|
|
// TaskCompleted is not used: it fires on the TaskUpdate tool, which current
|
|
// models do not get by default (tools-reference section Task tool availability).
|
|
//
|
|
// Credit: refusing "done" until the check passes is Claude Code's own
|
|
// documented pattern (hooks reference section TaskCompleted / Stop decision control).
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import { readLock, lockForSession } from '../../lib/util/test-lock.mjs';
|
|
|
|
let input;
|
|
try {
|
|
input = JSON.parse(readFileSync(0, 'utf-8'));
|
|
} catch {
|
|
process.exit(0);
|
|
}
|
|
|
|
if (input?.stop_hook_active === true) process.exit(0);
|
|
|
|
// The lock in the session's current repo, else the one this session took
|
|
// elsewhere (the session may have moved to another directory since).
|
|
let lock = readLock(input?.cwd || process.cwd());
|
|
if (lock.state !== 'locked' || lock.lock.session !== input?.session_id) lock = lockForSession(input?.session_id);
|
|
if (lock.state !== 'locked') process.exit(0);
|
|
if (!lock.lock.session || lock.lock.session !== input?.session_id) process.exit(0);
|
|
// The step's test has been seen passing by a command (`test-lock.mjs green`):
|
|
// the session may stop -- e.g. to wait for its controller -- while the write
|
|
// guard keeps the test files locked (operator decision 2026-10-08).
|
|
if (lock.lock.green_at) process.exit(0);
|
|
|
|
const files = lock.lock.files.join(', ');
|
|
process.stdout.write(JSON.stringify({
|
|
decision: 'block',
|
|
reason:
|
|
`[voyage] Not done: step ${lock.lock.step} still holds its test lock (${files}). ` +
|
|
"The step is done when its test passes and the executor unlocks it. Run the step's " +
|
|
'test; if it passes, finish the step. If it cannot pass, say so plainly and stop.',
|
|
}));
|
|
process.exit(0);
|