138 lines
5.7 KiB
Bash
Executable File
138 lines
5.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Gate the CLAIM of being finished, not the artifact. A Claude Code
|
|
# `TaskCompleted` hook.
|
|
#
|
|
# ## The hole this fills
|
|
#
|
|
# Every other guard in this template fires on an artifact: `githooks/pre-commit`
|
|
# on a commit, `audit-gate.mjs` and `preflight.sh` on a release, `verify.sh`
|
|
# when somebody runs it. `GUARDS.md` §6 is the rule they follow -- guards belong
|
|
# before the artifact exists.
|
|
#
|
|
# An agent that says "done" and does not commit produces no artifact, so it
|
|
# trips none of them. That is not a hypothetical: `WORK_CYCLE.md` opens by
|
|
# saying "Done" is not a close, the tracker convention refuses a `close` under
|
|
# fifteen characters of evidence, and both exist because the claim arrives
|
|
# without the work behind it often enough to need a rule. A rule is a thing a
|
|
# reader can skip. This is the same rule with an exit code.
|
|
#
|
|
# So: one step earlier than §6. Before the artifact exists, and before the
|
|
# sentence claiming it does.
|
|
#
|
|
# ## Why it calls verify.sh and not the test command
|
|
#
|
|
# `npm test` in a repo with no tests exits 0. A gate wired to it passes
|
|
# vacuously and reports green on a repository that verified nothing, which is
|
|
# `GUARDS.md` §4 and §8's entire subject -- the guard that cannot fail is worse
|
|
# than the guard nobody wrote, because it is believed.
|
|
#
|
|
# `verify.sh` already answers the right question. It runs every check the
|
|
# project actually has, in one command, and its exit codes are built for this:
|
|
#
|
|
# 0 everything that ran passed
|
|
# 1 something failed, or the run could not start
|
|
# 2 NOTHING WAS VERIFIED -- no checks detected, or every step skipped
|
|
#
|
|
# Two is not a pass and this script does not treat it as one. A repository that
|
|
# has not got as far as having checks fails this gate, loudly, with the reason.
|
|
# That is the intended answer: wire the gate up when there is something for it
|
|
# to run, and until then know that there is not.
|
|
#
|
|
# ## Install
|
|
#
|
|
# cp docs/architecture/scripts/verify-before-done.sh scripts/
|
|
# chmod +x scripts/verify-before-done.sh
|
|
#
|
|
# and in `.claude/settings.json` at the project root:
|
|
#
|
|
# {
|
|
# "hooks": {
|
|
# "TaskCompleted": [
|
|
# { "hooks": [ { "type": "command",
|
|
# "command": "${CLAUDE_PROJECT_DIR}/scripts/verify-before-done.sh",
|
|
# "timeout": 900 } ] }
|
|
# ]
|
|
# }
|
|
# }
|
|
#
|
|
# **Set `timeout` from this repository's own suite time, not from a number
|
|
# copied out of a blog post.** Time `bash scripts/verify.sh` once and give it
|
|
# headroom. A hook killed by its own timeout reports as a failure, so a budget
|
|
# that is too small turns a passing suite into a blocked task and teaches
|
|
# everybody to remove the hook -- `GUARDS.md` §5, a guard that is often wrong is
|
|
# worse than none.
|
|
#
|
|
# ## What it costs, said plainly
|
|
#
|
|
# It runs the suite every time a task completes, not once at the end of the
|
|
# session, so a long session runs it many times. On a repository whose suite
|
|
# takes minutes that is real time and real usage. `VERIFY_BEFORE_DONE_ARGS`
|
|
# exists for that: set it to `--quick` and the gate runs the checks that are
|
|
# cheap and names the test step as skipped rather than pretending it ran.
|
|
#
|
|
# A skipped step is visible in verify.sh's table. A gate that quietly stopped
|
|
# running is not. That is the whole difference.
|
|
#
|
|
# ## THE LIMITATION, AND IT IS NOT SMALL
|
|
#
|
|
# This is a Claude Code harness feature. It gates Claude Code and nothing else.
|
|
# A Codex session, a human, or any other agent working in the same checkout
|
|
# writes past it without noticing it exists -- and at least one repository here
|
|
# shares its checkout with Codex.
|
|
#
|
|
# So this is a second layer and never the layer. The gate that catches every
|
|
# writer is `.githooks/pre-commit`, because git runs it whoever is driving. If
|
|
# this repository's real suite is not wired into that hook, wiring it there is
|
|
# worth more than installing this, and neither one replaces the other.
|
|
#
|
|
# ## Exit codes -- the hook contract, which is not the usual one
|
|
#
|
|
# 0 allow the task to complete
|
|
# 2 BLOCK it, and feed stderr back to the model as the reason
|
|
#
|
|
# Two, specifically. A hook exiting 1 is a hook that broke, and Claude Code
|
|
# treats it as non-blocking -- so a gate that returns 1 on failure fails OPEN
|
|
# and lets exactly the thing it was installed to catch straight through. It
|
|
# looks identical from the outside. Prove this one blocks before trusting it:
|
|
# break something the suite covers and watch a task refuse to finish.
|
|
set -uo pipefail
|
|
|
|
cd "${CLAUDE_PROJECT_DIR:-.}" || {
|
|
echo "verify-before-done: cannot enter ${CLAUDE_PROJECT_DIR:-.}" >&2
|
|
exit 2
|
|
}
|
|
|
|
if [ ! -f scripts/verify.sh ]; then
|
|
# Fail closed. "There is no verify.sh" is a fact about the repository worth
|
|
# blocking on -- the alternative is a gate that silently allows everything on
|
|
# exactly the projects with no other checks.
|
|
echo "verify-before-done: scripts/verify.sh not found in $PWD." >&2
|
|
echo "Install it from the template, or remove this hook from .claude/settings.json." >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Word-split on purpose: this is a small set of flags, not a path.
|
|
# shellcheck disable=SC2086
|
|
out="$(bash scripts/verify.sh ${VERIFY_BEFORE_DONE_ARGS:-} 2>&1)"
|
|
code=$?
|
|
|
|
if [ "$code" -eq 0 ]; then
|
|
exit 0
|
|
fi
|
|
|
|
# The tail, because the model reads this and the table verify.sh prints last is
|
|
# the part that says which step failed. A full log of a large suite buries it.
|
|
{
|
|
if [ "$code" -eq 2 ]; then
|
|
echo "verify.sh verified NOTHING (exit 2): no checks were detected, or every"
|
|
echo "step skipped. That is not a pass. Do not report this task as finished."
|
|
else
|
|
echo "verify.sh FAILED (exit $code). Do not report this task as finished."
|
|
fi
|
|
echo "Fix what it names, or say plainly that it is failing and why."
|
|
echo
|
|
printf '%s\n' "$out" | tail -n 200
|
|
} >&2
|
|
exit 2
|