feat(plan): six sections every plan must name, and the files an agent reads first

WORK_CYCLE.md covered only the end of the cycle. A cycle has two ends, and
the same six questions kept having to be asked out loud on every piece of
work. They are now mandatory in every plan, each justified by something this
household has actually paid for:

  unified code      eight copies of secrets.sh once existed here and five of
                    seven could not detect the most common secret shape --
                    INCLUDING THE TEMPLATE, so every project scaffolded from
                    it inherited a blind scanner
  error handling    the recurring fault is the silent pass, not the crash
  logging           an append-only log is unbounded by construction
  blind spots       named ones get fixed
  landmine fixes    the trap found while passing is cheapest to fix while
                    passing
  hardcode little   derive it, or justify the constant

GUARDS.md was five sections behind the project that has been learning; 11,
12 and 13 are backported, genericised to match the template's style. 13 is
the narrow form of the sixth rule, and WORK_CYCLE now cites it -- so
backporting it is what makes that citation true rather than a broken
reference.

Also adds the three files an agent reads BEFORE it reads docs/:

  CLAUDE.md             short, and it POINTS at DOC_TRUST_MAP rather than
                        repeating it -- a second copy of the map is the
                        failure that map exists to prevent. Carries the
                        exit-code table, the commit gates, and two standing
                        instructions: flag what looks wrong even when it is
                        not what you were asked about, and never print a
                        credential -- not from a file, not from a command's
                        output, not from a config subtree "with the secrets
                        filtered out", because that filter has failed before
                        by matching key NAMES while the secret sat inside an
                        object whose name was innocent
  .claudeignore         excludes artifacts and NEVER docs/. The Command
                        Center reads this repository's documents at a commit;
                        a generic ignore file that sweeps "documentation" or
                        "data" starves both the agent and the reconcile, and
                        everything still runs, just blind
  .claude/settings.json deny rules in the double-slash absolute form. A
                        tilde-style rule looks right in review and silently
                        matches nothing. It closes the Read TOOL only -- a
                        shell reads a file a hundred ways -- so it catches
                        the accidental read, not the determined one

scaffold.sh gains a ROOT array for the three, kept apart from DOCS so the
H1-plus-status-block check stays meaningful rather than being loosened into
a warning that is always wrong (GUARDS.md 5).

Verified: scaffold --dry-run into a scratch repo creates 22 files including
all three, with no HEADERLESS warning; doc-claims passes with 124 claimed
paths, all present.

Does not touch docs/architecture/scripts/secrets.sh, which carries someone
else's uncommitted improvement.
This commit is contained in:
null 2026-09-01 21:44:00 -05:00
parent 26a2bb5e56
commit 734b6af828
9 changed files with 386 additions and 12 deletions

24
.claude/settings.json Normal file
View File

@ -0,0 +1,24 @@
{
"//": "Project-scoped Claude Code settings. See CLAUDE.md and docs/WORK_CYCLE.md.",
"permissions": {
"//deny": [
"Deny wins over allow. Paths use the DOUBLE-SLASH absolute form -- a",
"tilde-style rule looks right in review and silently matches nothing,",
"which is indistinguishable from a rule that works until it matters.",
"",
"This closes the Read TOOL only. A shell can read a file a hundred ways,",
"so this catches the accidental read, not the determined one. The real",
"protection is having a sanctioned reader that answers the question",
"without returning the value -- see CLAUDE.md."
],
"deny": [
"Read(//**/.env)",
"Read(//**/.env.*)",
"Read(//**/*.token)",
"Read(//**/*.pem)",
"Read(//**/id_rsa*)",
"Read(//**/credentials/**)",
"Read(//**/secrets/**)"
]
}
}

45
.claudeignore Normal file
View File

@ -0,0 +1,45 @@
# What Claude should not spend context on.
#
# EXCLUDE ARTIFACTS, NEVER THE RECORD. `docs/` is load-bearing: the Command
# Center reads this repository's documents at a commit, `DOC_TRUST_MAP.md` says
# which document owns which answer, and `docs/qa/ClaudeReport.md` is parsed for
# the sentence shown on the project screen. A generic ignore file that sweeps
# "documentation" or "data" starves both the agent and the reconcile, and the
# failure is silent -- everything still runs, it is just working blind.
#
# Same for the tracker: nothing here is a substitute for it.
node_modules/
.venv/
__pycache__/
*.pyc
# Build output
dist/
build/
.next/
*.tsbuildinfo
# Databases and captures — large, binary, and derived
var/*.db
var/*.db-wal
var/*.db-shm
var/archive/
*.sqlite
*.sqlite3
# Logs and rotations
*.log
*.log.*
# Media that costs context and answers nothing
*.png
*.jpg
*.jpeg
*.webp
*.mp4
*.pdf
# Local editor state
.idea/
.vscode/

70
CLAUDE.md Normal file
View File

@ -0,0 +1,70 @@
# <PROJECT NAME>
<One or two sentences: what this is and who it is for. Not a feature list.>
**Read `docs/DOC_TRUST_MAP.md` before trusting anything else in `docs/`.** It
says which document owns which answer. This file is deliberately short and
points at it rather than repeating it — a second copy of the map is the failure
that map exists to prevent.
## What this file is for
Only what an agent needs **before** it can read anything else, and that is not
already owned by a document. If you are about to add current state, a task list,
or a summary of `docs/`, it goes in the tracker or in `docs/` respectively.
## Exit codes are an API
<Delete if this project has no scheduled or scripted work.>
| Code | Means |
| --- | --- |
| `0` | did its work |
| `1` | refused for a normal, designed reason |
| `2` | **did not run** — a precondition failed. Never a pass |
| `3` | ran and the work failed |
| `99` | a lock was held; another copy is running |
Three states, never two: `ok` / `skipped` / `failed`. An exit code alone cannot
tell "nothing to do" from "I did nothing".
## Before you finish
`docs/WORK_CYCLE.md` owns this and outranks anything here. In short:
- Run this project's own guards, not just its tests: <name them, e.g.
`python3 tests/run_all.py`, `bash scripts/doc-claims.sh`>
- **Never `git add -A`.** Stage by explicit path — <`bash scripts/commit-mine.sh`
if adopted>. A shared checkout means someone else's work is one careless
`add` away from your commit.
- Update the documents this change triggered, **in the same commit as the
code**. Check each document's `Review trigger:` line.
- Close issues with the evidence that proves them: a path, a symbol, a test
name, or the command that shows it. "Done" is not a close.
## Before you plan
`docs/WORK_CYCLE.md` lists six sections **every plan must name**: unified code,
error handling, logging, blind spots, rolled-in landmine fixes, and
hardcode-as-little-as-possible. Read them there.
## Two standing instructions
**Flag anything that looks wrong, even if it is not what you were asked about.**
Most real defects are found while looking at something else. Say so, then fix it
or ask — a known bug that ships is a decision, and it is never yours to make
silently.
**Never print a credential.** Not from a file, not from a command's output, not
from a config subtree "with the secrets filtered out" — that filter has failed
before, because it matched key *names* and the secret sat inside an object whose
name was innocent. Name the variable or the path; never the value. To compare
two secrets, compare hashes.
## Conventions
<Delete what does not apply.>
- <Language/runtime and version>
- <Naming and layout rules an agent would otherwise guess at>
- <Anything that has bitten someone twice>

View File

@ -53,6 +53,11 @@ Your plan must name, separately:
- anything you will DELETE, and a migrated backlog above all - anything you will DELETE, and a migrated backlog above all
- which scripts this project will adopt, and which it will not - which scripts this project will adopt, and which it will not
- what you will deliberately leave undone, by name, and why - what you will deliberately leave undone, by name, and why
- the six mandatory sections from <TEMPLATE>/docs/WORK_CYCLE.md: unified code,
error handling, logging, blind spots, rolled-in landmine fixes, and
hardcode-as-little-as-possible. Read them there rather than from this line —
a second copy of a list about not keeping second copies would be its own
punchline
Once the plan is approved: Once the plan is approved:

View File

@ -30,6 +30,11 @@ Your plan must name, specifically:
- the first milestone's name, and every issue you will file, each with its Verify: line - the first milestone's name, and every issue you will file, each with its Verify: line
- anything you will delete - anything you will delete
- what you will deliberately leave undone, by name, and why - what you will deliberately leave undone, by name, and why
- the six mandatory sections from <TEMPLATE>/docs/WORK_CYCLE.md: unified code,
error handling, logging, blind spots, rolled-in landmine fixes, and
hardcode-as-little-as-possible. Read them there rather than from this line —
a second copy of a list about not keeping second copies would be its own
punchline
The last one is the point. An adoption that quietly skips the webhook, or the The last one is the point. An adoption that quietly skips the webhook, or the
branding issue, or half the status headers, looks identical to a complete one branding issue, or half the status headers, looks identical to a complete one

View File

@ -33,6 +33,7 @@ disagree, and nothing will say which one is right.
| What makes a release wrong? | issues labelled `release-blocker` | | What makes a release wrong? | issues labelled `release-blocker` |
| What is the next action? | the `nextAction` field on the project at privacyllc.dev — the newest entry in `docs/history/DEVELOPMENT_LOG.md` says what it was *then* | | What is the next action? | the `nextAction` field on the project at privacyllc.dev — the newest entry in `docs/history/DEVELOPMENT_LOG.md` says what it was *then* |
| What is blocking us? | the tracker, for the work; the blockers table at privacyllc.dev, for the stakeholder-facing version | | What is blocking us? | the tracker, for the work; the blockers table at privacyllc.dev, for the stakeholder-facing version |
| What must every plan name before work starts? | `docs/WORK_CYCLE.md` — the six mandatory sections |
| What do I do when a piece of work is finished? | `docs/WORK_CYCLE.md` | | What do I do when a piece of work is finished? | `docs/WORK_CYCLE.md` |
| What watches this in production, and what do I do when it breaks? | `docs/OPERATIONS.md` | | What watches this in production, and what do I do when it breaks? | `docs/OPERATIONS.md` |
| What happened, and when? | `docs/history/DEVELOPMENT_LOG.md` | | What happened, and when? | `docs/history/DEVELOPMENT_LOG.md` |

View File

@ -1,12 +1,14 @@
# The work cycle — what happens at the end of a piece of work # The work cycle — what a plan must name, and what must be true at the end
``` ```
Status: Current Status: Current
Owner: <who maintains this> Owner: <who maintains this>
Last reviewed: <YYYY-MM-DD> Last reviewed: <YYYY-MM-DD>
Governs: what must be true before a piece of work counts as finished Governs: what every plan must name before work starts, and what must be true
before that work counts as finished
Review trigger: Any change to what the Command Center reads, or to which of Review trigger: Any change to what the Command Center reads, or to which of
those channels a person rather than an agent has to write those channels a person rather than an agent has to write; any
change to what a plan is required to contain
``` ```
> **This is a procedure, not a status board.** Nothing here records what is open, > **This is a procedure, not a status board.** Nothing here records what is open,
@ -14,6 +16,44 @@ Review trigger: Any change to what the Command Center reads, or to which of
> privacyllc.dev, and a copy of them in this file would be the second disagreeing > privacyllc.dev, and a copy of them in this file would be the second disagreeing
> record that `DOC_TRUST_MAP.md` exists to prevent. > record that `DOC_TRUST_MAP.md` exists to prevent.
## Before you start — what every plan must name
A cycle has two ends. These six are **mandatory in every plan**, and each one is
here because its absence has cost this household something real.
| Section | The question it answers | Why it is mandatory |
| --- | --- | --- |
| **Unified code** | What already exists that this should use instead of a new copy? | Eight copies of `secrets.sh` once existed here and five of seven could not detect the most common secret shape — *including the template*, so every project scaffolded from it inherited a blind scanner |
| **Error handling** | What happens when each step fails, and can the failure be told from success? | The recurring fault is not the crash but the **silent pass**: a wrapper that swallowed every job's output, a check that could not run reading as clean, an alert path that posted successfully to nobody |
| **Logging** | What is written, who reads it, and what bounds it? | An append-only log is unbounded by construction. The moment it matters is the moment it is already a problem |
| **Blind spots** | What would this change make invisible? | Named ones get fixed. Every serious defect found here recently was found by asking what would *not* be visible, not by looking where the work was |
| **Roll in landmine fixes** | What did you trip over on the way, and is it fixed or explicitly deferred? | The trap found while passing is cheapest to fix while passing. A known bug that ships is a decision, and it is never yours to make silently |
| **Hardcode as little as possible** | Where is the authority for each value, and why is this a copy of it? | See below |
None of these is a heading to fill in. A plan that says "Error handling: yes" has
answered nothing; the section exists to make you name the failure and say what
happens when it arrives.
### Hardcode as little as possible
**Derive it, or justify the constant.** A value copied from somewhere else will
drift from the thing it copied. If a list, a path, a schedule or a threshold
already exists anywhere in the system, read it from there. Ask the daemon, parse
the config, walk the crontab — do not restate it. And where a constant genuinely
must be written down, say in a comment **where the authority is** and what would
make it wrong.
`GUARDS.md` states the narrow form — *a guard that restates the list is a copy of
the list* — and it is true of everything else too, not only guards.
Two limits, so it does not become a slogan:
- **Something has to be the root.** A ledger path, a channel id, a threshold
taken from a real incident: these *are* the authority, not copies of one. The
rule is against the **second** copy.
- **Derive the input, never the assertion.** A probe that derives its own
expectation from the thing it is testing proves nothing at all.
## Why this file exists ## Why this file exists
A piece of work ends in more than one place. The code is committed; the issue A piece of work ends in more than one place. The code is committed; the issue
@ -200,14 +240,13 @@ looks like nothing at all.
## The commands ## The commands
The tracker is the Forgejo instance at Steps 1 to 3 happen in the tracker. Where it is, how to authenticate to it, the
**[dream.scheller.ltd](https://dream.scheller.ltd)**, and steps 1 to 3 happen Cloudflare trap and the reason it has to be Forgejo rather than any tracker are
there. Its credentials, the Cloudflare trap and the reason it has to be that in the project README's *Where the tracker is* — one copy, named once:
instance rather than any tracker are in the project README's *Where the tracker
is* — one copy, named once:
```bash ```bash
set -a; . ~/.openclaw/docker-registry.env; set +a # The tracker credentials live in this machine's credential store, never in the
# repository. Load them the way that README describes, then:
python3 docs/architecture/scripts/forgejo-issue.py list python3 docs/architecture/scripts/forgejo-issue.py list
python3 docs/architecture/scripts/forgejo-issue.py close 42 "Fixed in a1b2c3d; tests/foo.test.js covers it." python3 docs/architecture/scripts/forgejo-issue.py close 42 "Fixed in a1b2c3d; tests/foo.test.js covers it."
``` ```
@ -262,7 +301,7 @@ one, and never report progress the API did not return.
- **Not the work list.** That is the tracker: milestones are batches, issues are - **Not the work list.** That is the tracker: milestones are batches, issues are
deliverables. deliverables.
- **Not the release procedure.** That is `scripts/release.sh` - **Not the release procedure.** That is `docs/architecture/scripts/release.sh`
and `docs/security/SECURITY_CHECKLIST.md`. and `docs/security/SECURITY_CHECKLIST.md`.
- **Not the QA procedure.** That is `docs/qa/ClaudeQAPlan.md`, which ends in its - **Not the QA procedure.** That is `docs/qa/ClaudeQAPlan.md`, which ends in its
own version of step 7. own version of step 7.

View File

@ -140,3 +140,167 @@ the operation claims**:
Escape hatches are fine, and they have to be asked for by name, never be the Escape hatches are fine, and they have to be asked for by name, never be the
default, and say plainly what is being given up. default, and say plainly what is being given up.
## 8. Find the vacuous ones in one run: comment the whole tree out
§1 proves one guard at a time, which is the right thing to do while writing one
and far too slow for a suite that already exists. The audit that produced these
rules found six vacuous guards by reading; the sweep below found forty in ninety
seconds, because it asks every guard the same question at once.
The question is the one the failure mode is named after. **What survives when
the code is moved into a comment?** Every byte a source-grep can see is still
there; nothing runs.
```bash
# In a throwaway copy — never the working tree.
cp -a repo /tmp/mutant && cd /tmp/mutant
# Prefix every line of every source file with the language's comment marker.
# Shell keeps line 1, so the shebang survives.
npx vitest run --reporter=json --outputFile=/tmp/survivors.json
```
Then read out the tests that **passed**. Three kinds of answer come back:
- **A test that claims to check behaviour.** "Refuses anywhere that is not
Discord", "checks the guard before doing anything else", "never deletes what
production is running". These are the findings. Every one is a `toContain`
against source text whose docblock argues the same rule in the same words.
- **A negative assertion**`not.toContain`, or an empty-list expectation over
a walk. These pass on an empty tree by construction and are not findings on
their own. They need §4 applied instead: something in the file must prove the
walk found anything and the pattern still matches.
- **A structural check with nothing to do with source text** — file sizes,
counts, a fixture asserting things about itself. Not findings.
### The sweep needs §4 applied to itself, and the first one here did not
**Run it with the test database UNSET.** This is the trap that matters most,
because it fails silently and in the flattering direction. With
`TEST_DATABASE_URL` set, every database-backed file's `beforeAll` throws against
the commented-out migration runner — and vitest marks every test in a file whose
`beforeAll` threw as **skipped**, not failed. A skipped test is not a passing
test, so it never appears in the survivor list. The first sweep run here was done
that way: 42 files exploded at setup, their pure source-grep describes were never
asked the question at all, and the answer came back **208 survivors** when the
real figure was **502**. It undercounted by about 345 — and it undercounted by
counting "could not check" as "nothing found", which is the exact confusion §4
exists to forbid.
Unset the variable and those files' `beforeAll` returns early instead, so their
non-database describes still run and still answer.
**Prove the instrument before trusting the number.** A sweep that mutates nothing
reports every test as a survivor; a sweep that deletes everything reports none.
Assert that code survived the mutation *and* that the mutation happened — one
guard here measured only how much its stripper removed, so 100% removal read as a
healthy instrument.
**Mutate every file a guard can read**, not just the obvious source tree. A first
pass left `notices/`, `reporter/` and `radar/` intact and reported forty-four
working tests as suspects. A later one missed `.forgejo/workflows/*.yml`,
`src/db/migrations/*.sql`, `*.css` and `.npmrc` — five guards are only
demonstrably red once those are included.
**Write the mutated file from a variable you read first.**
`open(p, "w").write(f(open(p).read()))` truncates before it reads, so it deletes
the file rather than commenting it out, and a deletion proves something weaker.
The recursion is the lesson worth keeping: the instrument built to find guards
that cannot fail was itself a guard that could not fail, and it stayed wrong
until somebody asked it the question it asks everything else.
## 9. `toThrow()` cannot tell "refused" from "broken"
`expect(() => parse(url)).toThrow()` with no matcher is satisfied by any throw
at all — including `TypeError: parse is not a function`, which is what a deleted
export produces. An SSRF boundary asserted this way passes when the validator
has been removed entirely.
```ts
// Vacuous: green when parseWebhookUrl no longer exists.
expect(() => parseWebhookUrl("https://evil.example/api/webhooks/1/t")).toThrow();
// Names the refusal, so a broken import is a failure rather than a pass.
expect(() => parseWebhookUrl("https://evil.example/api/webhooks/1/t"))
.toThrow(InvalidInputError);
```
The same applies to `rejects.toThrow()` on a database probe: name the
constraint, or a typo in the table name passes as the constraint firing.
## 10. A floor over a walk is not a check on the walk
`expect(routes.length).toBeGreaterThan(40)` is satisfied by 41, 57 and 500
equally, and `expect(actionFiles.length).toBeGreaterThanOrEqual(1)` was
satisfied by a walk that found one of twenty-two files — while every other
assertion in that file was an `it.each` over the same walk, so the run went
green having checked almost nothing.
When the real set is discoverable, derive it twice and compare:
```ts
const independently = readdirSync(ADMIN_DIR, { recursive: true })
.map(String)
.filter((entry) => entry.endsWith("page.tsx"));
expect(pages).toHaveLength(independently.length);
```
Neither number is maintained by hand, adding a page fails nothing, and a walk
that stopped entering `[slug]` directories is a failure rather than a smaller
green run.
## 11. A guard armed by a field is only as armed as the sloppiest writer
A guard that reads a field to decide whether to fire is armed only where every
writer sets that field. The two writers you fixtured set it. The shared helper
they both call defaults it to empty, and every other writer — present and future
— writes that default straight past the guard. Nothing fails. Nothing logs. The
fixtures stay green, because they exercise the writers that get it right.
**The test to add is not another one about the guard. It is one about the
default.** Either the default carries the safe value, or the field is not
optional. A guard whose arming depends on every caller remembering an argument
is not armed; it is armed *so far*.
## 12. A guard in one caller is a guard over that caller
Putting the check inside the function that happens to be in front of you protects
that path and no other. The next caller reaches the same resource by a different
route and meets nothing.
Count the ways in before deciding where the guard goes. If there is more than
one, the guard belongs at the resource — or the resource belongs behind one door.
## 13. A guard that restates the list is a copy of the list
A guard written to stop two lists drifting apart, which contains its own copy of
one of them, is a third thing to keep in step. It passes by agreeing with itself.
```python
# Wrong: the literal is a second copy, and adding a surface updates neither.
check("every SURFACE is one the gate can see",
set(SURFACES) == {"a", "b", "c", "d"})
# Right: derive both sides from the thing that decides.
check("every SURFACE is one the gate can see",
set(SURFACES) <= set(gate_reads()))
```
**Derive it, or justify the constant.** Ask the daemon, read the config, walk the
schedule — do not restate it. Where a constant genuinely must be written down,
say in a comment *where the authority is* and what would make it wrong.
This is the narrow form of a rule `docs/WORK_CYCLE.md` requires every plan to
answer: **hardcode as little as possible.** It is true of guards, and of
everything else.
Two limits, so it does not become a slogan:
- **Something has to be the root.** A path, a channel id, a threshold taken from
a real incident: those *are* the authority, not copies of one. The rule is
against the **second** copy.
- **Derive the input, never the assertion.** A probe that derives its own
expectation from the thing it is testing proves nothing at all.

View File

@ -128,6 +128,23 @@ DOCS=(
docs/data/img/README.md docs/data/img/README.md
) )
# Root-level files an agent reads BEFORE it reads anything in docs/.
#
# Kept apart from DOCS for one reason: the header check below wants an H1
# followed by a fenced status block, which is right for a document and wrong for
# a JSON settings file and an ignore list. Listing them here rather than
# loosening that check keeps the check meaningful -- a guard that is often wrong
# is worse than none (`GUARDS.md` §5).
#
# They carry angle-bracket placeholders like every other template document, for
# the reason stated at the top of this file: an unfilled header keeps asking,
# and an invented one stops.
ROOT=(
CLAUDE.md
.claudeignore
.claude/settings.json
)
# Directories made empty, holding nothing this script is willing to invent. # Directories made empty, holding nothing this script is willing to invent.
DIRS=(docs/data/img) DIRS=(docs/data/img)
@ -373,7 +390,7 @@ for rel in "${DIRS[@]}"; do
fi fi
done done
for rel in "${DOCS[@]}"; do for rel in "${DOCS[@]}" "${ROOT[@]}"; do
src="$TEMPLATE_ABS/$rel" src="$TEMPLATE_ABS/$rel"
dst="$TARGET_ABS/$rel" dst="$TARGET_ABS/$rel"
@ -398,7 +415,11 @@ for rel in "${DOCS[@]}"; do
continue continue
fi fi
has_status_header "$src" || HEADERLESS=$((HEADERLESS + 1)) # Only documents are held to the header convention; the ROOT files above are
# deliberately not documents.
case "$rel" in
docs/*) has_status_header "$src" || HEADERLESS=$((HEADERLESS + 1)) ;;
esac
# A symlink is followed by cp, so "writing into docs/" could land anywhere on # A symlink is followed by cp, so "writing into docs/" could land anywhere on
# the disk, including on top of the file it points at. Never written through, # the disk, including on top of the file it points at. Never written through,