Compare commits

...

2 Commits

Author SHA1 Message Date
null 26a2bb5e56 feat(guards): gate the claim of being finished, not only the artifact
Every 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 and trips
none of them. That gap already has two rules pointing at it -- `WORK_CYCLE.md`
opens with *"Done" is not a close*, and the tracker convention refuses a close
under fifteen characters of evidence -- and a rule is a thing a reader can skip.
This is the same rule with an exit code.

`scripts/verify-before-done.sh` is a Claude Code `TaskCompleted` hook. Verified
against Claude Code 2.1.251: the event exists and the binary carries the string
`TaskCompleted hook prevented continuation`.

WIRED TO `verify.sh`, NOT TO THE TEST COMMAND. `npm test` in a repo with no
tests exits 0, so a gate built on it reports green having verified nothing --
`GUARDS.md` §4 and §8's whole subject. `verify.sh` already distinguishes the
three answers, and this treats exit 2 (nothing was verified) as a failure with
its own message rather than a pass. A repository with no checks yet fails this
gate loudly, which is the intended answer.

IT EXITS 2 TO BLOCK, AND THAT IS NOT THE USUAL CONVENTION. Claude Code reads a
`1` from a hook as "the hook broke" and continues, so a gate written the
ordinary way fails OPEN and lets through exactly what it was installed to catch,
looking identical from the outside. `docs/TOOLS.md` now says this beside the
exit-2-is-never-a-pass rule it inverts.

THE LIMITATION IS IN THE HEADER IN CAPITALS, because it decides whether this is
worth installing: it gates Claude Code and nothing else. A Codex session or a
human in the same checkout writes past it. It is a second layer and never the
layer -- `.githooks/pre-commit` is what git runs whoever is driving, and if a
project's real suite is not wired into that hook, wiring it there is worth more
than installing this.

Proved rather than assumed, all four outcomes, per §1: no `verify.sh` -> 2 (fails
closed); pass -> 0 with empty stderr; fail -> 2 naming the code; verified-nothing
-> 2 with the distinct message.

Docs in the same commit, per the triggers `doc-triggers.py` reported:
`architecture/README.md` gains the row its own "Adding one" rule requires,
`TOOLS.md` gains the paragraph under "Which ones can stop you", and
`DOC_TRUST_MAP.md` gains the question it now answers. `doc-claims.sh` re-run:
51 claimed paths across the three, all present.

Prompted by an XDA piece on Claude Code shipping unverified work. The idea is
theirs; the exit-2 contract, the `verify.sh` wiring and the Codex caveat are
what it needed to be true here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 08:42:00 -05:00
null 694b161bb1 docs(tracker): an issue that produces a data shape names its surface
A batch of twenty-eight issues in one adopting project could close in full —
every P1 included — with nothing new visible to anybody using the product.
Cost per project, hours with coverage bounds, model attribution and an
algorithm version on every stored figure: all of it verifiable through a JSON
flag, none of it required to appear on a screen.

Two of those issues exist specifically to correct what somebody believes when
they look at the admin — wall-clock is not summed effort, and figures computed
by different algorithm versions are not comparable. Satisfied in JSON, they
correct nobody.

So the rule, and the check that carries it:

  README.md — a schema, a stored figure, a computed field or an API response
  is not a deliverable by itself. Say which screen shows it, link the issue
  that provides one, or record that it is deliberately not shown.

  forgejo-issue.py — warns when the acceptance check's subject is the data
  (retrievable, returned, reported, recorded) and nothing in it requires a
  person to see the result.

Both tests read the `Verify:` line rather than the body. A first attempt read
the whole body and fired on 44 of 73 real open issues, because bodies say
"page" and "renders" incidentally; a warning at that rate is one nobody reads,
and it would have taken the `Verify:`-not-last warning down with it. Narrowed
to the acceptance line, it fires on 8 of the same 73 — all eight in the batch
that prompted this, none anywhere else.

A warning and not a refusal. API-only is a legitimate decision; it has to be a
decision rather than an omission.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:05:11 -05:00
6 changed files with 223 additions and 22 deletions

View File

@ -1,14 +1,17 @@
# Project Template
A working documentation skeleton, the tracker convention that goes with it, and
the scripts that enforce both. Copy `docs/` into a new project, replace the
angle-bracket placeholders, delete the paragraphs that do not apply — then
create the labels and the first milestone in the repository's issue tracker.
the scripts that enforce both. Adopt it with
`docs/architecture/scripts/scaffold.sh`, which never overwrites a file that is
already there — the two *Start Here* documents below say which path to take.
Then replace the angle-bracket placeholders, delete the paragraphs that do not
apply, and create the labels and the first milestone in the repository's issue
tracker.
```
Status: Current
Owner: _null
Last reviewed: 2026-08-12
Last reviewed: 2026-08-22
Governs: README.md, START-HERE-New-Project.md, START-HERE-Existing-Project.md
Review trigger: Any change to how a project adopts this template — scaffold.sh's
file list, the tracker conventions or the label names, or what
@ -151,26 +154,30 @@ and a row on the site only when a human puts it there.
### Where the tracker is
**[dream.scheller.ltd](https://dream.scheller.ltd)** — a Forgejo instance, and
naming it is not a detail. The convention below is portable: milestones, issues
and labels exist on GitHub, GitLab and Jira, and every rule in it would work
there unchanged. **The reporting is not portable at all.** Every figure the
Command Center shows — the headline percentage, the milestone count, milestone
coverage, the alert an opened `P0` raises — is read from a Forgejo repository
mapped to the project. A project that follows every rule here in a different
tracker satisfies the convention completely and appears on privacyllc.dev as
having no tracker at all.
**A Forgejo instance**, and that it is Forgejo is not a detail. The convention
below is portable: milestones, issues and labels exist on GitHub, GitLab and
Jira, and every rule in it would work there unchanged. **The reporting is not
portable at all.** Every figure the Command Center shows — the headline
percentage, the milestone count, milestone coverage, the alert an opened `P0`
raises — is read from a Forgejo repository mapped to the project. A project that
follows every rule here in a different tracker satisfies the convention
completely and appears on privacyllc.dev as having no tracker at all.
Credentials are in **`~/.openclaw/docker-registry.env`**, which carries
`FORGEJO_REGISTRY`, `FORGEJO_REGISTRY_USER` and `FORGEJO_REGISTRY_TOKEN`:
The instance and its credentials are operator configuration, not repository
content: they live in this machine's credential store and are loaded into the
environment before the call. **Nothing here names either**, and neither should
anything you add — a template is copied into repositories that may end up
public, and a document naming a host, a credential file and what that token can
reach is a map even when it holds no secret.
```bash
set -a; . ~/.openclaw/docker-registry.env; set +a
# Load the tracker credentials from wherever this machine keeps them, then:
python3 docs/architecture/scripts/forgejo-issue.py list
```
Despite its name that token is not registry-scoped — it works across the whole
API, with push and pull. Never print it into a log or a chat.
Whatever that token turns out to reach, treat it as reaching everything until
somebody has checked: an API token named for one job is routinely scoped for
none. Never print it into a log or a chat.
Two things that cost an hour each otherwise. **Cloudflare fronts the instance
and 1010-blocks clients that do not look like a browser or curl** — Python's
@ -186,10 +193,6 @@ cannot file into another project's tracker by accident. `--repo owner/name`
overrides that deliberately, and `--dry-run` prints the payloads and changes
nothing.
> This block names one instance and one path on the machine that runs it. If
> this repository will be public, replace both with your own before the first
> push.
### The rules
- **Milestone = batch.** Name it `Batch 02 — Payments`, and **start it with a
@ -202,6 +205,14 @@ nothing.
inventing one.
- **Issue = deliverable.** One per item, in its milestone, ending with a
`Verify:` line stating the acceptance check.
- **An issue that produces a data shape names its surface.** A schema, a stored
figure, a computed field or an API response is not a deliverable by itself —
somebody has to be able to see it. Say which screen shows it, link the issue
that provides one, or record the decision that it is deliberately not shown.
Skip it and the `Verify:` line drifts toward *"a caller can retrieve it"*,
which is satisfiable with nothing visible: a batch of twenty-eight issues
closed that way leaves the product exactly as it was, and every figure it
built is reachable only by whoever knows the JSON.
- **Labels:** `P0` ships broken or loses data · `P1` materially wrong but
shippable · `P2` cosmetic or low impact · `release-blocker` means a release
built today would be wrong rather than merely incomplete. Exactly these

View File

@ -42,6 +42,7 @@ disagree, and nothing will say which one is right.
| How do I release, back up, or check this? | `scripts/` |
| Which script do I run, and can it stop me? | `docs/TOOLS.md` — the signpost; `docs/architecture/README.md` has the table |
| What runs before a commit? | `.githooks/` |
| What stops an agent reporting work it did not verify? | `docs/architecture/scripts/verify-before-done.sh`, and `docs/TOOLS.md` for why it is a second layer rather than the layer |
| How do I write a check that will actually catch something? | `docs/architecture/GUARDS.md` |
**Next action and blockers are recorded at the end of every piece of work, not

View File

@ -51,6 +51,26 @@ The hooks are the other place work gets stopped:
[`architecture/githooks/`](architecture/githooks/README.md) has the one install
command and the table of what each hook runs.
**And a third place, which stops something the other two cannot: the claim.**
`scripts/verify-before-done.sh` is a Claude Code `TaskCompleted` hook that runs
`verify.sh` and refuses to let a task be reported as finished when it fails.
Every other gate here fires on an **artifact** — a commit, a build, a tag — so
an agent that says "done" without committing trips none of them, which is the
gap `WORK_CYCLE.md` is describing when it says *"Done" is not a close*.
Two things to know before installing it, both in the script's header at length:
- **It exits `2` to block, and `1` fails open.** Claude Code reads a `1` from a
hook as "the hook broke" and continues, so a gate written the ordinary way
lets through precisely what it was installed to catch, and looks identical
doing it. This is the exit-code rule above with the numbers swapped, and it is
the one place in this template where that is true.
- **It gates Claude Code and nothing else.** A Codex session, a human, or any
other agent in the same checkout writes past it without knowing it is there.
It is a second layer; `.githooks/pre-commit` is the layer, because git runs
that whoever is driving. If this project's real suite is not wired into that
hook, wiring it there is worth more than installing this.
## Where to start in a fresh clone
1. `bash docs/architecture/scripts/check-env.sh` — what is configured and what

View File

@ -54,6 +54,7 @@ can stop you, and where to start in a fresh clone.
| `scripts/scaffold.sh` | lay out a new project in this shape |
| `scripts/doc-triggers.py` | which documents a **pending** change fires, read from the `Governs:` headers of everything under `docs/` and of the documents at the repository root, narrowed by the optional `Fires on:` — the kinds of change (added, deleted, moved, changed) a document's trigger actually names, so one governing `docs/**` for existence changes alone does not fire on every edit. The `Review trigger` on each document names the change that should send somebody back to it; this is the check that asks before the commit rather than after |
| `scripts/prove-guard.sh` | breaks the thing a guard protects, requires the guard to go red, restores the file from a trap. `GUARDS.md` §1 written out as a command, including the count — one failing test reported on six lines is not six failures |
| `scripts/verify-before-done.sh` | a Claude Code `TaskCompleted` hook: runs `verify.sh` and **blocks the claim of being finished** when it fails. One step earlier than `GUARDS.md` §6 — every other guard here fires on an artifact, and an agent that says "done" without committing produces none. Wired to `verify.sh` rather than the test command because `npm test` on a repo with no tests exits 0, and exit **2** (verified nothing) is treated as a failure, not a pass. Exits 2 to block: a hook exiting 1 fails **open**. Gates Claude Code only — a Codex session in the same checkout writes past it, so `.githooks/pre-commit` remains the gate that catches every writer |
| `scripts/commit-mine.sh` | commits only the paths you name, by pathspec, after the secret scan. For a tree something else is also writing: what anyone else has staged is reported and left exactly as it was |
| `scripts/doc-claims.sh` | every file a document names must exist, and (`--covers`) every file that exists is named — the second is the one that catches a list missing rows |
| `scripts/duplication.py` | code that exists twice, tuned so what it reports is worth reading |

View File

@ -214,6 +214,36 @@ def check_verify_line(body: str, title: str) -> str:
return body
def check_surface_named(body: str, title: str) -> str:
"""An issue that produces a data shape names its surface.
Both tests read the `Verify:` line, not the body bodies say "page" and
"renders" incidentally all the time, and the acceptance check is the thing
that decides whether the issue can close with nothing visible.
Deliberately narrow. It fires only when the acceptance check's subject is
the data itself retrievable, returned, reported, recorded and nothing in
it requires a person to see the result. A warning that fires on most issues
is one nobody reads, and it would take the `Verify:`-not-last warning down
with it.
A warning rather than a refusal: API-only is a legitimate decision, it just
has to be a decision rather than an omission."""
lines = [l for l in body.strip().splitlines() if l.strip()]
verify = next((l for l in reversed(lines) if l.strip().startswith("Verify:")), "")
data_is_the_subject = re.search(
r"(retrievable|returns the|--json\b|a caller|is stored|recorded with|"
r"\breports\b|available (?:from|via) the API)", verify, re.I)
a_person_sees_it = re.search(
r"\b(admin|screen|page|shown|shows|displayed|visible|sees|operator|"
r"renders|dialog|button|UI)\b", verify, re.I)
if data_is_the_subject and not a_person_sees_it:
print(f'warning: "{title}" accepts on the data being retrievable and never on\n'
" anyone seeing it. Name the screen, link the issue that provides\n"
" one, or state that it is deliberately API-only.",
file=sys.stderr)
return body
def resolve_labels(names, available: dict[str, int]) -> list[int]:
"""Names → ids, failing loudly. A typo'd severity label is reported by the
Command Center as *not adopted*, not as zero defects silently dropping it
@ -390,6 +420,7 @@ def create_one(api: Api, spec: dict, labels_map, ms_map, existing,
allow_dup=False) -> dict | None:
title = spec["title"].strip()
body = check_verify_line(spec.get("body", ""), title)
body = check_surface_named(body, title)
dup = find_duplicate(title, existing)
if dup and not allow_dup:

View File

@ -0,0 +1,137 @@
#!/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