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:
parent
26a2bb5e56
commit
734b6af828
|
|
@ -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/**)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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/
|
||||
|
|
@ -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>
|
||||
|
|
@ -53,6 +53,11 @@ Your plan must name, separately:
|
|||
- anything you will DELETE, and a migrated backlog above all
|
||||
- which scripts this project will adopt, and which it will not
|
||||
- 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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
- anything you will delete
|
||||
- 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
|
||||
branding issue, or half the status headers, looks identical to a complete one
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ disagree, and nothing will say which one is right.
|
|||
| 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 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 watches this in production, and what do I do when it breaks? | `docs/OPERATIONS.md` |
|
||||
| What happened, and when? | `docs/history/DEVELOPMENT_LOG.md` |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Owner: <who maintains this>
|
||||
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
|
||||
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,
|
||||
|
|
@ -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
|
||||
> 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
|
||||
|
||||
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 tracker is the Forgejo instance at
|
||||
**[dream.scheller.ltd](https://dream.scheller.ltd)**, and steps 1 to 3 happen
|
||||
there. Its credentials, the Cloudflare trap and the reason it has to be that
|
||||
instance rather than any tracker are in the project README's *Where the tracker
|
||||
is* — one copy, named once:
|
||||
Steps 1 to 3 happen in the tracker. Where it is, how to authenticate to it, the
|
||||
Cloudflare trap and the reason it has to be Forgejo rather than any tracker are
|
||||
in the project README's *Where the tracker is* — one copy, named once:
|
||||
|
||||
```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 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
|
||||
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`.
|
||||
- **Not the QA procedure.** That is `docs/qa/ClaudeQAPlan.md`, which ends in its
|
||||
own version of step 7.
|
||||
|
|
|
|||
|
|
@ -140,3 +140,167 @@ the operation claims**:
|
|||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -128,6 +128,23 @@ DOCS=(
|
|||
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.
|
||||
DIRS=(docs/data/img)
|
||||
|
||||
|
|
@ -373,7 +390,7 @@ for rel in "${DIRS[@]}"; do
|
|||
fi
|
||||
done
|
||||
|
||||
for rel in "${DOCS[@]}"; do
|
||||
for rel in "${DOCS[@]}" "${ROOT[@]}"; do
|
||||
src="$TEMPLATE_ABS/$rel"
|
||||
dst="$TARGET_ABS/$rel"
|
||||
|
||||
|
|
@ -398,7 +415,11 @@ for rel in "${DOCS[@]}"; do
|
|||
continue
|
||||
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
|
||||
# the disk, including on top of the file it points at. Never written through,
|
||||
|
|
|
|||
Loading…
Reference in New Issue