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> |
||
|---|---|---|
| docs | ||
| .gitignore | ||
| README.md | ||
| START-HERE-Existing-Project.md | ||
| START-HERE-New-Project.md | ||
| Template.code-workspace | ||
| project-readme-template.md | ||
README.md
Project Template
A working documentation skeleton, the tracker convention that goes with it, and
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-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
the Command Center needs at adoption; any script added to or
removed from docs/architecture/scripts/, which the day-one table
here names; any change to the QA run-state grammar or to what
reconcile reads
Why this exists
The Command Center at privacyllc.dev reports on a repository: how much is done, what the QA verdict is, and what the next milestone entails. It can only do that for repositories that keep those things somewhere it knows to look.
Eight repositories were surveyed before this was rewritten. Exactly one kept the markdown batch ledger the previous version of this template asked for. Three had invented their own dialects; one had pushed nothing but a README. Meanwhile, with no convention at all, those same repositories had accumulated 69, 54 and 205 issues — because an issue tracker has structure built into it and a convention has only whoever remembers it.
The split
| Kind of truth | Lives in | Why |
|---|---|---|
| Narrative documents — vision, trust map, QA playbook, QA verdict, security | git, as markdown | prose with an argument, versioned with the code, readable at any sha |
| Work items — batches, tasks, defects, blockers | the issue tracker | built for concurrent uncoordinated writers, atomic state, closes #N from a commit, real timestamps |
Nothing belongs in both. A to-do list in markdown and a tracker with the same items is two records that will disagree, and neither will say which is right.
What the Command Center actually reads
| Source | What is read |
|---|---|
| The repository's issue tracker | issue counts excluding pull requests, milestones with their own counters, and issues carrying the four severity labels |
docs/qa/ClaudeReport.md |
the three run-state lines — round, build SHA, and the overall sentence, quoted verbatim |
docs/data/img/ |
icon.webp, logo.webp, banner.webp — fetched and rendered on the project page |
Everything else in this tree is here because a project needs it, not because something parses it. Rename, split or extend those freely.
A repository that has not adopted the labels is reported as not adopted rather than as zero defects, and one whose tracker is switched off is reported as switched off rather than as an empty backlog. Absence is never rendered as a measurement.
The files in that table are read at a commit, and the commit is remembered.
When the SHA they were read from falls behind the repository's newest commit,
the docs report is marked stale — which is the accurate description of a
document that described the code at a commit nobody is running any more. It is
also what an unpushed correction looks like from outside. Documentation edits
therefore ride in the same commit as the change that caused them, and reach the
site on the same push. See docs/WORK_CYCLE.md.
What updates itself, and what does not
The numbers on the project screen come out of this repository's tracker. The
percentage is defined rather than picked: closed issues over all issues, asked
with type=issues so pull requests are excluded and state=all rather than the
API's default of open. Beside it sits a second figure counted from the
repository's own milestones, closed over all — this deployment keeps
milestones in the tracker and never mirrors them into the site's table, so that
is where the milestone reading comes from.
| On the project screen | Source | How it gets there |
|---|---|---|
| Headline % — issues closed / all issues | the repository's tracker | automatic — on reconcile, and immediately on a webhook delivery |
| Second figure — milestones closed / all milestones | the repository's milestones | same read, same moment |
| Milestone coverage | how many issues sit inside a milestone at all | same read |
| QA verdict — round, build SHA, overall sentence | docs/qa/ClaudeReport.md |
push the repository, then reconcile |
| Current summary / Next action | manual fields on the project | PATCH /agent/projects/<slug> — nothing else writes them |
| The Milestones card, and a typed weighted plan | the Command Center's own milestone table | admin UI only |
| Blockers | the Command Center's own blockers table | admin UI only |
So closing an issue and closing a milestone each move a number a stakeholder can see, the same day, with no further step. That is the whole reason the convention insists on both.
The last two rows are the ones that surprise people, and they are lists rather than figures. The Milestones card reads the site's own table, which is empty on this deployment by design — a repository can carry twenty-nine milestones, have them counted in the figure above, and still show "No milestones have been added yet" on that card. It is not a sync failure and reconciling will not change it.
Blockers have a sharper version of the same split. The agent API can narrate a
blocker in a check-in's blockers field, and it has no route that creates or
resolves one in the table. A run that posts the check-in and reports "blocker
filed" has told the truth about the note and a falsehood about the record.
An issue in no milestone is counted in the headline and invisible to the
milestone figure. The site measures that gap on purpose — it is the reason
Verify: lines and milestones are per-issue requirements here rather than
housekeeping. Portfolio-wide when this was written, 272 of 650 issues belonged
to no milestone at all, including every one of the 205 in the repository that
had never adopted the convention.
The digest is the part somebody actually reads
A worker polls the site every sixty seconds and sends a project digest to
Discord. It is where this convention stops being bookkeeping: the digest's
per-project figures come from issues opened and closed and from pushes, and
its next-step line comes, by rule, from nextAction or the live milestone's
open issues — never from a guess.
Three consequences, each the direct form of a step in the cycle below:
- Work with no issue opened or closed produces a push and nothing else. The project reads as quiet on a day it was not.
- A project with no
nextActionand no open milestone gives the digest nothing to say about what happens next, and the digest will not invent one. - An opened issue labelled
P0raises an immediate alert — but only from a repository whose webhook is registered. Without one, the site sees the repository through the periodic sweep alone, and a P0 filed at nine is news at the next reconcile rather than at nine.
After each piece of work
docs/WORK_CYCLE.md is the seven-step version, with the argument for each step.
The short form:
close what you finished, with the evidence · file what you found · close the milestone if the batch landed · update the documents the change triggered, in the same commit · push · log the entry, with Next action and Blockers · then reconcile and write the summary and next action.
The last two steps are the ones that get skipped, and the only ones nobody can do for you later — by the time somebody notices the project screen is a week stale, the person who knew what came next has forgotten.
Where each of those lives is deliberate and worth reading once: the next action has a live copy (the project field) and a dated copy (the newest log entry) and they are different kinds of fact; a blocker is an issue in the tracker first, and a row on the site only when a human puts it there.
The tracker convention
Where the tracker is
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.
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.
# Load the tracker credentials from wherever this machine keeps them, then:
python3 docs/architecture/scripts/forgejo-issue.py list
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
urllib among them — so every request needs User-Agent: curl/8.5.0;
forgejo-issue.py already sends it and anything new must too, or the failure
arrives as a Cloudflare HTML page that looks nothing like a Forgejo error. And
/issues returns pull requests too unless type=issues is passed, which is
why the counts here exclude them and a hand-rolled query disagrees by exactly
the number of open PRs.
forgejo-issue.py takes the repository from this checkout's git remote, so it
cannot file into another project's tracker by accident. --repo owner/name
overrides that deliberately, and --dry-run prints the payloads and changes
nothing.
The rules
- Milestone = batch. Name it
Batch 02 — Payments, and start it with a word rather than a version number: the dashboard reads a leading version token as the whole phase and drops the rest, so0.2 Paymentsdisplays as0.2and the batch loses its name on the project card. No comma either — a comma breaks themilestones=filter and the card shows the wrong next action. The description says what the batch is for and how anybody will know it landed. A due date means a commitment — leave it empty rather than 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:
P0ships broken or loses data ·P1materially wrong but shippable ·P2cosmetic or low impact ·release-blockermeans a release built today would be wrong rather than merely incomplete. Exactly these names — they are queried by name. - Defects are issues with a severity label. The QA verdict stays in
ClaudeReport.md; the counts come from the tracker. - Close with
closes #Nin the commit that does the work, so the record comes from the thing that happened rather than from a date typed afterwards. Where no single commit finished it, close it by hand with the evidence — a path, a symbol, a test name, or the command that proves it. "Done" is not a close, because a close with nothing in it cannot be reopened with confidence. - Close the milestone when the batch lands. Closing the last issue under it does not, and a milestone that is complete but open reads as a batch still in progress. It is how a batch becomes visible as shipped rather than merely finished — and it is counted: milestones closed over milestones total is one of the two figures the project screen shows.
- Every issue belongs to a milestone. An issue filed outside one still counts against the headline percentage and is invisible to the milestone figure, so the two readings describe different amounts of work. The site measures that gap rather than ignoring it.
The rule that keeps the numbers honest
Do not pad the tracker. Every open issue is a denominator. If the real answer is "one milestone, three issues", file exactly that. Invented future work makes every percentage drawn from the tracker wrong, permanently and in the same direction.
The same arithmetic runs the other way, and this half is the one that actually happens: an issue left open after the work is done understates the project for as long as it stays open, and the understatement compounds. A fortnight of finished work with unclosed issues reads, from outside, as a fortnight of no progress at all.
What comes with it, besides the documents
docs/architecture/scripts/ holds working code, not examples. Each takes its
configuration from the environment and hard-codes nothing about any deployment,
so a script copied from here cannot quietly point at the project it came from.
docs/architecture/README.md has the full table; the ones worth knowing about
on day one:
release.sh |
version bump, guards, build, verify, push. Refuses to build on a half-run test suite or a malformed public origin — both of which had shipped in every release of the project this came from. |
verify.sh |
every check the project has, in one command, with one table saying which passed, which failed, and which did not run |
check-env.sh |
which variables are set and which are missing, before anything reads them |
secrets.sh |
credential shapes in a staged diff, using the project's own patterns where it has written them down — and --built, the compiled bundle, which is the artifact users actually receive and the one the repository scan never sees |
backup.sh restore-check.sh |
a dump verified before it is trusted, and the restore that proves it is a backup rather than a file. The second exists because the first says so: a backup nobody has restored is a guess. |
preflight.sh |
headers, TLS, and — behind --auth — login rate limiting and account enumeration, against a live URL. Refuses any host but its configured origin. |
controls.sh |
which operational controls this project has, each row saying whether that is measured, declared, not applicable, or simply unknown |
prove-guard.sh |
breaks what a guard protects and requires the guard to go red, then restores the file from a trap. GUARDS.md §1 as a command — the rule that a guard nobody has seen fail is not yet evidence. |
doc-claims.sh |
every file a document names must exist — and --covers, whether every file that exists is named. The second is the one that catches a list quietly missing rows. |
duplication.py dead-code.py |
code that exists twice; exports nothing imports and assets nothing renders |
forgejo-issue.py |
files and closes issues in the convention below, with every rule of the convention above as a check the script refuses to break |
deploy.py |
updates a running stack to a published image. Publishing and deploying are separate decisions; release.sh makes the first and this makes the second. |
release-notes.mjs |
tags the release and writes the notes: sections from the commit types the hook enforces, the image and its digest, and the batches the release finished. Set RELEASE_DEPLOY_NOTE to say how this project deploys, or empty where publishing and deploying are one act. |
docs/architecture/githooks/ holds three hooks — typecheck and tests before a
commit, a conventional type in the message, and a push afterwards so a guarded
commit does not sit unpushed. They live in the repository rather than in
.git/hooks, which is not versioned; see their README for the one command that
points git at them.
docs/architecture/GUARDS.md is worth reading before you write a check.
Every rule in it was learned from a guard that had been green for months over
something broken, and the first — prove a guard fails before believing it
passes — is the one that costs thirty seconds and catches the rest.
Adopting it
Two documents, because the two jobs are different in the only way that matters — whether anything can be destroyed on the way in. Each holds one prompt, written to be handed to an agent whole:
| Starting from | Read |
|---|---|
| an empty repository | Brand New Project — Start Here |
| a repository that already has documents, a tracker, or conventions | Existing Project — Start Here |
Both begin by putting the agent in plan mode and require it to say what it will create, what it will keep, what it will delete and what it will leave undone, before it touches anything. An adoption that quietly skips the webhook or half the status headers looks identical to a complete one from outside.
The mechanism in both is docs/architecture/scripts/scaffold.sh, and the
important thing about it is what it refuses to do: it never overwrites a file
that is already there. Do not adopt this template with cp -r — a scaffold that
clobbers a written document destroys work nothing would notice was gone, and
cp -r has no such protection.
The Command Center's Docs template screen also generates an adoption prompt, editable before you copy it. These two documents are the source. That screen is a convenience and a separate copy, so where the two disagree the files here are the ones kept in step with the scripts they invoke — and a difference between them is worth reporting rather than picking a side.