Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,43 @@ fi
if __dk_gate_selected review; then
# devkit:guard-review
echo "🔍 Reviewer gate (headless domain judges)..."
# Ship path only (sc-1442 message file present): start the completeness judge NOW, in parallel
# with the reviewer fleet, instead of serially at commit-msg. Its confident PASS lands in the
# shared verdict store, so the commit-msg gate re-judges it as a cache hit — the serial ~4min of
# opus overlaps the fleet instead of following it. Interactive commits (no message yet) are
# unchanged. Lifetime is scoped to this hook: the judge is either wait'ed on or killed AND reaped
# below — nothing outlives the hook to hold git's output pipe open. Review mode is excluded — it
# exports the SAME env as its reviewer intent file (review-target.sh), but completeness is a
# commit gate, not part of a range review.
#
# DK_NO_GIT_ENV_INLINE, not the __dk_no_git_env function: backgrounding a function forks a
# subshell, which would make $! a wrapper whose death leaves the judge orphaned and running. See
# review-fragments.mts.
comp_pid=""
if [ "${DEVKIT_RUN_MODE:-}" != "review" ] && [ -n "${DEVKIT_COMMIT_MSG_FILE:-}" ] && [ -f "${DEVKIT_COMMIT_MSG_FILE:-}" ]; then
echo "🧩 Completeness judge started in parallel (ship message known)..."
env -u GIT_ALTERNATE_OBJECT_DIRECTORIES -u GIT_CONFIG -u GIT_CONFIG_PARAMETERS -u GIT_CONFIG_COUNT -u GIT_OBJECT_DIRECTORY -u GIT_DIR -u GIT_WORK_TREE -u GIT_IMPLICIT_WORK_TREE -u GIT_GRAFT_FILE -u GIT_INDEX_FILE -u GIT_NO_REPLACE_OBJECTS -u GIT_REPLACE_REF_BASE -u GIT_PREFIX -u GIT_SHALLOW_FILE -u GIT_COMMON_DIR -u GIT_GLOB_PATHSPECS -u GIT_NOGLOB_PATHSPECS -u GIT_LITERAL_PATHSPECS -u GIT_ICASE_PATHSPECS node gate-engine/review/cli.mts completeness --gate "$DEVKIT_COMMIT_MSG_FILE" & comp_pid=$!
fi
rrc=0
__dk_no_git_env node gate-engine/review/cli.mts --gate || rrc=$?
crc=0
if [ -n "$comp_pid" ]; then
if [ "$rrc" -eq 0 ] || [ "$rrc" -eq 2 ]; then
wait "$comp_pid" || crc=$?
else
# The fleet already blocked this commit — stop paying for a judgement of a diff that is
# about to change. (The verdict would be keyed to THIS diff; the fix invalidates it.)
# SIGNAL THEN REAP, never signal alone: the judge inherited git's stdout/stderr, so the
# ship capture pipeline only unblocks once every copy of that write-end is closed. A hook
# that returns while a signalled child is still winding down leaves the reader waiting on
# a pipe nobody will write to again — the exact hang commit-with-gate-capture.sh's R3
# supervisor exists to bound. Both lines are status-tested (|| true) because a reaped
# job's status IS the signal (143), and under sh -e an untested non-zero would abort the
# hook here, before it reports its own verdict below.
kill "$comp_pid" 2>/dev/null || true
wait "$comp_pid" 2>/dev/null || true
fi
fi
if [ "$rrc" -eq 1 ]; then
echo " A reviewer FAILED (opus-confirmed). Fix the findings above, then re-run."
exit 1
Expand All @@ -157,7 +192,22 @@ elif [ "$rrc" -ne 0 ] && [ "$rrc" -ne 2 ]; then
echo " guard-review: unexpected exit $rrc — blocking the commit."
exit 1
fi
# Same exit contract as the commit-msg fragment (commit-msg-block.mts) — a completeness verdict
# means the same thing regardless of WHERE it was judged, just earlier here.
if [ "$crc" -eq 1 ]; then
echo " Confirmed completeness gap (hard-by-default; findings above)."
echo " Fix the gap, or — with the user's explicit OK — GUARD_NO_COMPLETENESS=1 git commit ..."
exit 1
elif [ "$crc" -eq 4 ]; then
echo " NOT a gate rejection — no defect was named; the staged content itself is unreadable."
exit 1
elif [ "$crc" -eq 3 ]; then
echo " guard-review completeness: judge unavailable — strict ship mode failed closed."
echo " Check \`claude\` CLI auth/quota, then re-run devkit ship (cleared judgements are cached)."
exit 1
fi
# rrc 0 = pass/cached/nothing-to-do, rrc 2 = inconclusive (non-strict fail-open) → continue.
# crc 0 = pass (now cached for the commit-msg gate) / skipped, crc 2 = fail-open → continue.
# /devkit:guard-review
fi

Expand Down
111 changes: 108 additions & 3 deletions cli/__tests__/husky-block-exec.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,17 @@ const hasDash = existsSync('/bin/dash');
function runHook(
env = {},
selection = { biome: false, guards: ALL_GUARDS },
{ shell = 'sh', dirPrefix = 'dk-hook-exec-' } = {},
{ shell = 'sh', dirPrefix = 'dk-hook-exec-', shipMsg = false } = {},
) {
const home = mkdtempSync(join(tmpdir(), dirPrefix));
homes.push(home);
if (shipMsg) {
// The sc-1442 composed-message temp file a ship exports — its presence arms the parallel
// completeness prewarm in the review fragment.
const msgf = join(home, 'ship-msg.txt');
writeFileSync(msgf, 'feat: thing\n\nbody\n');
env = { DEVKIT_COMMIT_MSG_FILE: msgf, ...env };
}
const bin = join(home, '.bun', 'bin');
mkdirSync(bin, { recursive: true });
writeFileSync(
Expand All @@ -49,7 +56,25 @@ echo "$tool $*" >> "$HOME/calls.log"
case "$tool" in
guard-deterministic) exit \${DET_RC:-0};;
guard-decisions) exit \${DEC_RC:-0};;
guard-review) exit \${REVIEW_RC:-0};;
guard-review)
case "$1" in
completeness)
# COMP_SLOW_TERM: a judge that does not die the instant it is signalled. It releases the
# inherited stdout/stderr FIRST (\`exec >/dev/null\`) so this harness measures the HOOK's
# own return, not the pipe drain — otherwise spawnSync would block on the pipe regardless
# and a hook that never reaps would still look correct. The trap then delays before
# recording that it finished winding down, so "hook returned" and "child was reaped" are
# separable events.
if [ -n "\${COMP_SLOW_TERM:-}" ]; then
exec >/dev/null 2>&1
trap 'sleep 1; echo reaped > "$HOME/comp-reaped"; exit 143' TERM
echo running > "$HOME/comp-running"
sleep 30 &
wait $!
fi
exit \${COMP_RC:-0};;
*) exit \${REVIEW_RC:-0};;
esac;;
*) exit 0;;
esac
`,
Expand All @@ -75,7 +100,8 @@ esac
} catch {
// hook never reached the stub
}
return { status, stdout, calls };
// `home` rides along so a test can assert on markers the stubs dropped there (the reap probe).
return { status, stdout, calls, home };
}

describe('assembled hook execution (stubbed bunx, sh -e)', () => {
Expand Down Expand Up @@ -147,6 +173,85 @@ describe('assembled hook execution (stubbed bunx, sh -e)', () => {
});
});

describe('parallel completeness prewarm (ship message file present)', () => {
it('no DEVKIT_COMMIT_MSG_FILE → completeness never launched (interactive commits unchanged)', () => {
const r = runHook();
expect(r.status).toBe(0);
expect(r.calls).toContain('guard-review --gate');
expect(r.calls).not.toContain('guard-review completeness');
});

it('with the ship message file, completeness runs alongside the fleet and a clean pair passes', () => {
const r = runHook({}, undefined, { shipMsg: true });
expect(r.status).toBe(0);
expect(r.calls).toContain('guard-review completeness --gate');
expect(r.calls).toContain('guard-review --gate');
});

it('a confident completeness FAIL (exit 1) blocks the commit at pre-commit', () => {
const r = runHook({ COMP_RC: '1' }, undefined, { shipMsg: true });
expect(r.status).toBe(1);
expect(r.stdout).toContain('Confirmed completeness gap');
});

it('completeness exit 3 (strict outage) fails closed with the remedy banner', () => {
const r = runHook({ COMP_RC: '3' }, undefined, { shipMsg: true });
expect(r.status).toBe(1);
expect(r.stdout).toContain('strict ship mode failed closed');
});

it('completeness exit 4 (unreadable staged content) blocks and names the cause', () => {
const r = runHook({ COMP_RC: '4' }, undefined, { shipMsg: true });
expect(r.status).toBe(1);
expect(r.stdout).toContain('NOT a gate rejection');
});

it('completeness exit 2 fails open', () => {
expect(runHook({ COMP_RC: '2' }, undefined, { shipMsg: true }).status).toBe(0);
});

it('a fleet FAIL blocks as the fleet, never as the parallel completeness verdict', () => {
const r = runHook({ REVIEW_RC: '1', COMP_RC: '1' }, undefined, { shipMsg: true });
expect(r.status).toBe(1);
expect(r.stdout).toContain('opus-confirmed');
expect(r.stdout).not.toContain('Confirmed completeness gap');
});

it('review mode does NOT prewarm — it exports the same env for its reviewer intent file', () => {
const r = runHook({ DEVKIT_RUN_MODE: 'review', DEVKIT_REVIEW_GUARDS: 'review' }, undefined, {
shipMsg: true,
});
expect(r.status).toBe(0);
expect(r.calls).toContain('guard-review --gate');
expect(r.calls).not.toContain('guard-review completeness');
});

it('a message-file path that does not exist arms nothing (the -f guard, not just -n)', () => {
const r = runHook({ DEVKIT_COMMIT_MSG_FILE: '/nonexistent/dk-msg.txt' });
expect(r.status).toBe(0);
expect(r.calls).not.toContain('guard-review completeness');
});

// The reap contract: the judge inherits git's stdout/stderr, so a hook that returns while a
// signalled child is still winding down leaves the ship's capture reader on a pipe nobody will
// close — commit-with-gate-capture.sh's R3 hang. Signalling alone is not enough; the harness
// stub releases the pipe first so this asserts the HOOK waited, not that the pipe drained.
it('a killed completeness judge is REAPED before the hook returns, not merely signalled', () => {
const r = runHook({ REVIEW_RC: '1', COMP_SLOW_TERM: '1' }, undefined, { shipMsg: true });
expect(r.status).toBe(1); // still the fleet's verdict
expect(existsSync(join(r.home, 'comp-running'))).toBe(true); // the judge really did start
// Written only by the TERM handler, after a delay: present iff the hook waited for it.
expect(existsSync(join(r.home, 'comp-reaped'))).toBe(true);
});

it('the fleet failing CLOSED (exit 3) also kills and reaps — every block path, not just exit 1', () => {
const r = runHook({ REVIEW_RC: '3', COMP_SLOW_TERM: '1' }, undefined, { shipMsg: true });
expect(r.status).toBe(1);
expect(r.stdout).toContain('strict ship mode failed closed');
expect(existsSync(join(r.home, 'comp-reaped'))).toBe(true);
});
});

describe('biome-format re-stage step (real git)', () => {
// The re-stage step runs `git add` on files it just re-read from `git diff --cached` — for a
// release commit that force-added a gitignored `dist/` (`git add -f dist`), a plain `git add`
Expand Down
51 changes: 51 additions & 0 deletions cli/lib/husky/husky-block.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { buildCommitTerminalFragment } from './commit-terminal.mts';
import { markEnd, markStart } from './husky.mts';
import {
DK_HOOK_HELPERS,
DK_NO_GIT_ENV_INLINE,
DK_REVIEW_BASELINE_HELPER,
selectedFragment,
} from './review-fragments.mts';
Expand Down Expand Up @@ -73,8 +74,43 @@ fi
# /devkit:guard-decisions`,
review: `# devkit:guard-review
echo "🔍 Reviewer gate (headless domain judges)..."
# Ship path only (sc-1442 message file present): start the completeness judge NOW, in parallel
# with the reviewer fleet, instead of serially at commit-msg. Its confident PASS lands in the
# shared verdict store, so the commit-msg gate re-judges it as a cache hit — the serial ~4min of
# opus overlaps the fleet instead of following it. Interactive commits (no message yet) are
# unchanged. Lifetime is scoped to this hook: the judge is either wait'ed on or killed AND reaped
# below — nothing outlives the hook to hold git's output pipe open. Review mode is excluded — it
# exports the SAME env as its reviewer intent file (review-target.sh), but completeness is a
# commit gate, not part of a range review.
#
# DK_NO_GIT_ENV_INLINE, not the __dk_no_git_env function: backgrounding a function forks a
# subshell, which would make $! a wrapper whose death leaves the judge orphaned and running. See
# review-fragments.mts.
comp_pid=""
if [ "\${DEVKIT_RUN_MODE:-}" != "review" ] && [ -n "\${DEVKIT_COMMIT_MSG_FILE:-}" ] && [ -f "\${DEVKIT_COMMIT_MSG_FILE:-}" ]; then
echo "🧩 Completeness judge started in parallel (ship message known)..."
${DK_NO_GIT_ENV_INLINE} bunx guard-review completeness --gate "$DEVKIT_COMMIT_MSG_FILE" & comp_pid=$!
fi
rrc=0
__dk_no_git_env bunx guard-review --gate || rrc=$?
crc=0
if [ -n "$comp_pid" ]; then
if [ "$rrc" -eq 0 ] || [ "$rrc" -eq 2 ]; then
wait "$comp_pid" || crc=$?
else
# The fleet already blocked this commit — stop paying for a judgement of a diff that is
# about to change. (The verdict would be keyed to THIS diff; the fix invalidates it.)
# SIGNAL THEN REAP, never signal alone: the judge inherited git's stdout/stderr, so the
# ship capture pipeline only unblocks once every copy of that write-end is closed. A hook
# that returns while a signalled child is still winding down leaves the reader waiting on
# a pipe nobody will write to again — the exact hang commit-with-gate-capture.sh's R3
# supervisor exists to bound. Both lines are status-tested (|| true) because a reaped
# job's status IS the signal (143), and under sh -e an untested non-zero would abort the
# hook here, before it reports its own verdict below.
kill "$comp_pid" 2>/dev/null || true
wait "$comp_pid" 2>/dev/null || true
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fi
if [ "$rrc" -eq 1 ]; then
echo " A reviewer FAILED (opus-confirmed). Fix the findings above, then re-run."
exit 1
Expand All @@ -86,7 +122,22 @@ elif [ "$rrc" -ne 0 ] && [ "$rrc" -ne 2 ]; then
echo " guard-review: unexpected exit $rrc — blocking the commit."
exit 1
fi
# Same exit contract as the commit-msg fragment (commit-msg-block.mts) — a completeness verdict
# means the same thing regardless of WHERE it was judged, just earlier here.
if [ "$crc" -eq 1 ]; then
echo " Confirmed completeness gap (hard-by-default; findings above)."
echo " Fix the gap, or — with the user's explicit OK — GUARD_NO_COMPLETENESS=1 git commit ..."
exit 1
elif [ "$crc" -eq 4 ]; then
echo " NOT a gate rejection — no defect was named; the staged content itself is unreadable."
exit 1
elif [ "$crc" -eq 3 ]; then
echo " guard-review completeness: judge unavailable — strict ship mode failed closed."
echo " Check \\\`claude\\\` CLI auth/quota, then re-run devkit ship (cleared judgements are cached)."
exit 1
fi
# rrc 0 = pass/cached/nothing-to-do, rrc 2 = inconclusive (non-strict fail-open) → continue.
# crc 0 = pass (now cached for the commit-msg gate) / skipped, crc 2 = fail-open → continue.
# /devkit:guard-review`,
};

Expand Down
15 changes: 15 additions & 0 deletions cli/lib/husky/review-fragments.mts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ export const DK_NO_GIT_ENV_HELPER = `__dk_no_git_env() {
env ${GIT_ENV_VARS.map((name) => `-u ${name}`).join(' \\\n ')} "$@"
}`;

/**
* The same scrub as a PREFIX for one simple command, for the single case the function form cannot
* serve: a BACKGROUNDED gate whose pid the hook must later signal.
*
* `__dk_no_git_env cmd &` backgrounds a shell FUNCTION, which forks a subshell — so `$!` is that
* subshell and the gate itself is a grandchild. Signalling the subshell then kills a wrapper while
* the real judge runs on, orphaned, still holding the stdout/stderr it inherited from git (and
* still spending model budget on a verdict nobody will read). Backgrounding a simple command
* instead makes the shell fork-and-exec directly, so `$!` IS the gate and one kill reaches it.
*
* Single line, no continuations: it has to sit inline ahead of a command in a background job.
* Same GIT_ENV_VARS source as the function above, so the two can never scrub different sets.
*/
export const DK_NO_GIT_ENV_INLINE = `env ${GIT_ENV_VARS.map((name) => `-u ${name}`).join(' ')}`;

// Review mode has its own positive guard allowlist. Normal commit/ship runs select everything in
// the generated hook; review runs only ids named by DEVKIT_REVIEW_GUARDS.
export const DK_GATE_SELECTED_HELPER = `__dk_gate_selected() {
Expand Down
Loading
Loading