Compare commits

..

No commits in common. "26a2bb5e56a04f80d7130abc152e5cc26ad0d309" and "c839f1eb7263671f1e5303f280e9b081e8eb9e8f" have entirely different histories.

6 changed files with 22 additions and 223 deletions

View File

@ -1,17 +1,14 @@
# Project Template # Project Template
A working documentation skeleton, the tracker convention that goes with it, and A working documentation skeleton, the tracker convention that goes with it, and
the scripts that enforce both. Adopt it with the scripts that enforce both. Copy `docs/` into a new project, replace the
`docs/architecture/scripts/scaffold.sh`, which never overwrites a file that is angle-bracket placeholders, delete the paragraphs that do not apply — then
already there — the two *Start Here* documents below say which path to take. create the labels and the first milestone in the repository's issue tracker.
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 Status: Current
Owner: _null Owner: _null
Last reviewed: 2026-08-22 Last reviewed: 2026-08-12
Governs: README.md, START-HERE-New-Project.md, START-HERE-Existing-Project.md 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 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 file list, the tracker conventions or the label names, or what
@ -154,30 +151,26 @@ and a row on the site only when a human puts it there.
### Where the tracker is ### Where the tracker is
**A Forgejo instance**, and that it is Forgejo is not a detail. The convention **[dream.scheller.ltd](https://dream.scheller.ltd)** — a Forgejo instance, and
below is portable: milestones, issues and labels exist on GitHub, GitLab and naming it is not a detail. The convention below is portable: milestones, issues
Jira, and every rule in it would work there unchanged. **The reporting is not and labels exist on GitHub, GitLab and Jira, and every rule in it would work
portable at all.** Every figure the Command Center shows — the headline there unchanged. **The reporting is not portable at all.** Every figure the
percentage, the milestone count, milestone coverage, the alert an opened `P0` Command Center shows — the headline percentage, the milestone count, milestone
raises — is read from a Forgejo repository mapped to the project. A project that coverage, the alert an opened `P0` raises — is read from a Forgejo repository
follows every rule here in a different tracker satisfies the convention mapped to the project. A project that follows every rule here in a different
completely and appears on privacyllc.dev as having no tracker at all. tracker satisfies the convention completely and appears on privacyllc.dev as
having no tracker at all.
The instance and its credentials are operator configuration, not repository Credentials are in **`~/.openclaw/docker-registry.env`**, which carries
content: they live in this machine's credential store and are loaded into the `FORGEJO_REGISTRY`, `FORGEJO_REGISTRY_USER` and `FORGEJO_REGISTRY_TOKEN`:
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 ```bash
# Load the tracker credentials from wherever this machine keeps them, then: set -a; . ~/.openclaw/docker-registry.env; set +a
python3 docs/architecture/scripts/forgejo-issue.py list python3 docs/architecture/scripts/forgejo-issue.py list
``` ```
Whatever that token turns out to reach, treat it as reaching everything until Despite its name that token is not registry-scoped — it works across the whole
somebody has checked: an API token named for one job is routinely scoped for API, with push and pull. Never print it into a log or a chat.
none. Never print it into a log or a chat.
Two things that cost an hour each otherwise. **Cloudflare fronts the instance 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 and 1010-blocks clients that do not look like a browser or curl** — Python's
@ -193,6 +186,10 @@ cannot file into another project's tracker by accident. `--repo owner/name`
overrides that deliberately, and `--dry-run` prints the payloads and changes overrides that deliberately, and `--dry-run` prints the payloads and changes
nothing. 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 ### The rules
- **Milestone = batch.** Name it `Batch 02 — Payments`, and **start it with a - **Milestone = batch.** Name it `Batch 02 — Payments`, and **start it with a
@ -205,14 +202,6 @@ nothing.
inventing one. inventing one.
- **Issue = deliverable.** One per item, in its milestone, ending with a - **Issue = deliverable.** One per item, in its milestone, ending with a
`Verify:` line stating the acceptance check. `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 - **Labels:** `P0` ships broken or loses data · `P1` materially wrong but
shippable · `P2` cosmetic or low impact · `release-blocker` means a release shippable · `P2` cosmetic or low impact · `release-blocker` means a release
built today would be wrong rather than merely incomplete. Exactly these built today would be wrong rather than merely incomplete. Exactly these

View File

@ -42,7 +42,6 @@ disagree, and nothing will say which one is right.
| How do I release, back up, or check this? | `scripts/` | | 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 | | 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 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` | | 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 **Next action and blockers are recorded at the end of every piece of work, not

View File

@ -51,26 +51,6 @@ The hooks are the other place work gets stopped:
[`architecture/githooks/`](architecture/githooks/README.md) has the one install [`architecture/githooks/`](architecture/githooks/README.md) has the one install
command and the table of what each hook runs. 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 ## Where to start in a fresh clone
1. `bash docs/architecture/scripts/check-env.sh` — what is configured and what 1. `bash docs/architecture/scripts/check-env.sh` — what is configured and what

View File

@ -54,7 +54,6 @@ can stop you, and where to start in a fresh clone.
| `scripts/scaffold.sh` | lay out a new project in this shape | | `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/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/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/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/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 | | `scripts/duplication.py` | code that exists twice, tuned so what it reports is worth reading |

View File

@ -214,36 +214,6 @@ def check_verify_line(body: str, title: str) -> str:
return body 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]: def resolve_labels(names, available: dict[str, int]) -> list[int]:
"""Names → ids, failing loudly. A typo'd severity label is reported by the """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 Command Center as *not adopted*, not as zero defects silently dropping it
@ -420,7 +390,6 @@ def create_one(api: Api, spec: dict, labels_map, ms_map, existing,
allow_dup=False) -> dict | None: allow_dup=False) -> dict | None:
title = spec["title"].strip() title = spec["title"].strip()
body = check_verify_line(spec.get("body", ""), title) body = check_verify_line(spec.get("body", ""), title)
body = check_surface_named(body, title)
dup = find_duplicate(title, existing) dup = find_duplicate(title, existing)
if dup and not allow_dup: if dup and not allow_dup:

View File

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