Compare commits
2 Commits
c839f1eb72
...
26a2bb5e56
| Author | SHA1 | Date |
|---|---|---|
|
|
26a2bb5e56 | |
|
|
694b161bb1 |
55
README.md
55
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue