Engineering standard
The engineering standard this project is built under: the per-item cycle, the machine gates, the autonomy boundary and the security controls.
markdownreferencev1FGHG-Z5CZgovernancestandardgatesautonomysecurity
CUP-DEV-STD-001 — cuppy development governance
Status: PROPOSED for owner lock. Drafted and adversarially vetted 2026-10-11. Agents do not ratify standards. Binding as working practice from drafting; the lock is the owner's act and nothing below claims it has happened. Version 1.0.
Scope. Every change to the cuppy platform repository (.) and the
production tree it deploys (.-app-factory/src): code, configuration, schema,
documentation, tracking records, and the public hosts cuppy.io and semarchy.cuppy.io. Out of scope: the
content of the redesign specification in tracking/roadmap/ (owner-reviewed research; §5 holds the
research-only gate), the other projects on this host, and the vhosts and DNS of the sibling sites.
Normative keywords are RFC 2119.
Inheritance — canonical by reference, never copied. One authority per concern:
| Instrument | Authority of record | Relationship |
|---|---|---|
| Development standard | commander/commander-dev/docs/governance/CMD-DEV-STD-001.md v1.1 | adopted; this is cuppy's local binding |
| Release standard | .../CMD-REL-STD-001.md | adopted in part; §8.5 states what is blocked |
| Single-operator posture | auth/docs/governance/SINGLE-OPERATOR-GOVERNANCE.md | adopted in its compensating-controls form (§5) |
| Invariants and acceptance criteria | docs/governance/INVARIANTS.md | cuppy's own |
| Executable invariant status | apps/api/src/gates/gateCells.ts | cuppy's own; the only place a status may change |
| Incident ledger | docs/governance/LESSONS-LEARNED.md | cuppy's own |
| Identifier registry | docs/governance/ID-REGISTRY.md | cuppy's own |
Inherited text is not reproduced. A second copy of a rule is a second source of truth, and cuppy has paid
for that class already: mcp/handler.ts:199 hard-codes a version that drifted from package.json, and
.github/workflows/ci.yml restates the checks instead of calling the script that owns them.
Precedence. Where this document and an inherited instrument disagree for cuppy work, this document wins, and the disagreement MUST appear in §9. An unrecorded divergence is a defect.
Why this exists, measured
In the seven days to 2026-10-11 this project shipped, to a live public site: an unauthenticated path traversal
that streamed the production database to anonymous callers through four res.download routes; a production
process running with NODE_ENV=development, so x-semarchy-role: owner granted owner to any caller; the
owner's email returned to anonymous callers on reader routes; a first remediation round an independent review
found incomplete in four places; and a test suite that wrote 47 commits into this history because both file
stores accepted an ancestor git work tree as their own.
Every one was already forbidden by prose. Rules 5, 6, 7 and 9 of the previous CLAUDE.md named the exact
classes that shipped anyway. cuppy's problem was never the absence of rules. It was the absence of an
enforcement column. Each incident, its cause with a file reference, and the rule it produced is in
LESSONS-LEARNED.md. A rule here without a scar there, or in a sibling's ledger, SHOULD be deleted at the next
vet. There is no separate provenance section: the reason is attached to each rule, where a reader deciding
whether to keep it will look.
The enforceability census
Seventeen rules in this document carry the word gate. Every other rule — the large majority — is
judgment. The seventeen: C1, C2, C6, C7, C9 (§0); R1 (§2); §7.1; §7.2; rows 1, 3 and 5 of §7.3; the
confinement half of §7.4; all four gated rows of §7.5; and rule 6 of §5A. Every rule in §7.8 is explicitly
not among them.
Their entire mechanical surface is eight steps in one script, plus the security-class test files the suite
runs. Of the seventeen, sixteen fire only when a human or an agent chooses to run one of two scripts —
gate.sh, or restart.sh for C2 — and CI has never run, because this repository has no remote. Exactly
one control here fires without being invoked: the PreToolUse hook in §5A.
That ratio is the honest state of the project, printed up front so no later session reads this document as a description of a controlled system. The highest-value change available to cuppy is not another rule in this file — it is a git remote, which turns the invoked checks into automatic ones. Configuring it is operator-hand.
Would this standard have caught the week's defects?
work/vet/SET-002-phase0-hardening.md already answers this for the hardening set — "two of four" — including
the most important sentence in cuppy's governance to date:
"no control in this project yet looks for a class of defect nobody has written a test for; the closest thing is the adversarial vet, which is a person-or-agent process, not a gate."
This standard adds one distinction that record does not draw, and it moves the answer down:
| Defect | As a regression in a known route | As a new unguarded route |
|---|---|---|
| L1 traversal | Caught — path-confinement.test.ts, a gate | Not caught. The controls that would (FEAT-019's route-enumerating drill, FEAT-017's auth floor) are both red |
| L3 email leak | Caught — redact.test.ts, a gate | Not caught. Nothing asserts every reader route passes through the masker; §6's parity sweep is prose |
| L2 dev-mode production | Caught by a live fatal boot assertion — the one unambiguous win | the header trust path is still representable and inert only because NODE_ENV is correct; inv:role-never-from-a-request-header is red |
| L4 incomplete fix round | Not caught by any gate. R3 is the control and its enforcement is a warning | — |
| L5 47 fixture commits | Caught — gate.sh step 4, a gate | — |
Two classes caught as regressions, one caught by a boot assertion with a latent path behind it, and the class that produced the incomplete-fix round — the one that needed a second reviewer — is governed by prose and a warning. That is this standard's real measure, stated before any rule rather than after.
§1. Enforcement vocabulary
| Word | Meaning |
|---|---|
| gate | A check that fails a run: inside scripts/gate.sh, inside the suite it calls, or a hook that exits non-zero. |
| written | The mechanism exists but fails nothing today — CI with no remote, a script no path calls. Named with what makes it enforcing. |
| planned | No mechanism exists. The work item that creates one is named. |
| prose | Judgment, recorded in an artifact. Accepted risk, stated as such. |
Nothing here may be described as enforced unless its word is gate. A rule whose word is prose is the
rule that will fail next.
§0. The engineering bar, with its enforcement column
The bar is the ten absolutes in CLAUDE.md, which is the wording of record. They appear here only as
identifiers mapped to mechanisms.
| # | Rule | Mechanism | Word |
|---|---|---|---|
| C1 | Two trees, one change — until cutover | gate.sh step 6 diffs the declared source roots; inv:trees-identical-until-cutover | gate |
| C2 | Verify through the public hosts | restart.sh curls both hosts and refuses on a non-200/non-JSON | gate (conditional on C3) |
| C3 | Restart only by the script | — nothing prevents a bare pm2 restart | prose |
| C4 | Git discipline — path-scoped staging, no unasked commits | — | prose |
| C5 | Roles from tokens and the session cookie only | inv:role-never-from-a-request-header — RED | planned (FEAT-018) |
| C6 | Caller paths through confinedPath/confinedFile | path-confinement.test.ts; inv:storage-path-confined, inv:identifier-is-never-a-path | gate |
| C7 | No addresses or secrets in output | redact.test.ts; inv:redaction-fails-closed, inv:no-address-below-admin-on-any-channel; gate.sh step 5 | gate |
| C8 | Research-only gate on the Phase 1+ redesign | — | prose |
| C9 | The gate is green before any restart | gate.sh, called by restart.sh step 1 | gate (conditional on C3) |
| C10 | Closure evidence, with outputs | §2 R5 | prose |
Four observations this table exists to force:
- C3 is prose, and C2 and C9 are conditional on it. A script is not a gate if nothing requires its use. An
agent can type pm2 restart semarchy and bypass the gate, the public-host verification and the restart log
in one command. What would close it is a PreToolUse hook on Bash matching pm2 *(restart|stop|delete|reload)
outside the script — the shape of §5A's hook. Planned, not written: a Bash-matching hook needs a
false-positive pass first. Until then the three most operationally important rules here rest on compliance.
- C5 is red, and it is the rule that already failed. No test can assert that a request header confers
nothing, because the suite's own helper (apps/api/test/helpers.ts:9) authenticates by setting
x-semarchy-role under NODE_ENV=test. The suite depends on the trust path whose production absence it
would need to prove. FEAT-018 is a negative-principal test constructing the app in production mode.
- C4, C8 and C10 are prose and will decay. Accepted: C4 and C8 constrain judgment rather than program
behaviour, and C10's evidence is only meaningful to a reader.
- A rule with no row here does not exist.
§2. The per-work-item cycle
R1 — Impact record, scoped to what it can pay for
A work item whose diff touches apps/, scripts/, config/, a schema or a dependency manifest MUST commit a
JSON impact record at work/impact/<ITEM-ID>.json. Schema of record: work/impact/TEMPLATE.json. Three layers:
- L1, decision safety — twelve lenses, only the binding ones surfaced, plus
adversarialNotesand a
proposed verdict: path-confinement · authn-bypass · hub-isolation · information-disclosure ·
fail-open-defaults · env-mode-drift · injection · rate-and-abuse · secrets · idempotency · observability ·
testability.
- L2, blast radius — eleven axes, each
Touched: <what>orNone: `api · mcp_tools · hub_content ·
storage_schema · auth_session · fs_paths · redaction · tests_and_gates · public_hosts · skew · docs`.
- L3, operational — only where it binds:
perf · cost · backup · runbook · secrets · supply-chain.
Plus the keys the existing FEAT-001 record invented, which two trees make load-bearing: reload_required,
operator_hand_remaining, rollback, alongside standards, config_chain, tests, gate_cells, verdict.
A documentation-only item gets no record file — one docs: line in the session note instead. This diverges
from the inherited R1's "no exceptions, including docs-only items" and is recorded as §9 C8. Reason: cuppy is
a documentation platform whose docs commits will outnumber code commits several to one. A rule demanding a JSON
file for a typo fix is the rule the project silently stops obeying in week three, and a ledger containing a
selectively-obeyed rule is worse than one with a narrower rule that holds.
Reason for the record at all. Each L1 lens is a class this project has shipped or is one change away from
shipping. The L2 axes make "no sprawl" a measurement: a change touching more axes than its intent requires is
leaking scope by definition, and skew is an axis because cuppy's callers are coding agents holding cached
tool catalogs. JSON, because a record a script can read is a record a gate can check.
Enforcement: gate — gate.sh step 7 fails when a tracking record changed in the working tree or HEAD
has no impact record and the same range touched code.
Binding: forward from adoption. FEAT-002–FEAT-005 are grandfathered (residual R4). Retroactive red
cells make the gate red on day one with nothing to do about it, which teaches bypass.
R2 — Verdict gating, and who owns the verdict
No work item MAY start while a predecessor on its dependency track carries a verdict of amend, reject or
pending. proceed-with-mitigation unlocks the successor only after the mitigation commit lands.
Parallel-track items MAY batch into one session note per dependency layer, each keeping its own record.
**The verdict in an impact record is the implementing agent's proposal. The binding verdict is the vet
record's (R3).** An agent writing proceed in its own record and then proceeding is self-approval with an extra
file in the way. The inherited standard does not draw this distinction because it assumed an advisor at the
other end of the delivery note; cuppy has no advisor, so the distinction must be in the text.
Reason. The absence of a verdict reads as permission unless the standard says otherwise, and that is how one
bad seam becomes five dependent bad seams. FEAT-007 is already correctly blocked on FEAT-006.
Enforcement: prose. It becomes a gate when the pointer's in-flight block is machine-readable.
R3 — Post-set re-vet by an independent clean-context agent
After each set of related items, a re-vet pass MUST run without an operator prompt and MUST:
- read the set's actual diff, hunk by hunk — never the implementing agent's report;
- walk at least the twelve L1 lenses, and the L2 axes against the whole live system;
- look back over the previous three sets in
work/vet/for interaction regressions, invariants weakened,
assumptions invalidated, and unswept siblings of earlier findings;
- verify closures behaviourally: re-run each original reproducer against the public host, after first
establishing which tree and which build is serving;
- disposition every finding as fixed / scheduled-with-owner / recorded — never silently dropped;
- commit the verdict to
work/vet/<SET-ID>.md.
A set without its vet record is red and the next set does not unlock.
The re-vet agent MUST be a fresh subagent in a clean context, dispatched read-only, with no access to the implementing session's claims, and MUST NOT be the implementing agent reflecting on its own work.
Its prompt MUST be work/vet/PROMPT.md, used as committed. An implementing session that writes its own
reviewer's brief has reproduced the second-identity problem one layer up: it chooses what the reviewer looks at.
The dispatching session may append the set id and the diff range, nothing else. *This is the correction that
makes R3 an honest substitute for a human reviewer rather than a longer way to agree with oneself.*
Reason. L4 bought this rule: three fixes were item-level correct and system-level incomplete, and what found
that was an independent review reading the modules. Cuppy's one operator does not read every diff, so the
independent pass is the review. The three-set look-back exists because per-item review is blind to the
interaction between adjacent sets, and a seven-phase plan is nothing but adjacent sets. Step 4 is not optional
while two trees exist: a reproducer run against the repo tree while pm2 serves the other one is right-looking
evidence about the wrong run.
Enforcement: written, and the limit is the point. gate.sh step 8 warns when the newest vet record is
older than the newest commit touching apps/ or scripts/. It cannot fail, because the re-vet follows
deployment by design — both existing vet records were written after their sets were live — so a hard fail would
block the restart that precedes the vet. The forcing function is R3 itself, administered by the next session
reading the warning. No script can judge a vet's quality and this one does not pretend to.
R4 — The delivery note is the session note
Each set ships tracking/updates/YYYY-MM-DD-<slug>.md carrying exactly: item ids with commit SHAs; the L2 axes
touched; every deviation from the plan, however small, called out explicitly; verification evidence as
output, not claim; any docs-only docs: lines; open questions and assumptions; the one-line verdict.
Reason. The "deviation however small" clause catches the incomplete-fix class — the four round-one gaps were
all small deviations nobody wrote down. Bound to tracking/updates/, which this project already writes, rather
than the inherited docs/comms/ path nobody will create: a named authority with no artifact behind it is worse
than no authority. Enforcement: prose.
R5 — Closure evidence, three parts and an identity check
A fix is closed only when its record carries (a) the deployed build identity read from the public host,
(b) the schema/migration state at verification time, (c) the reproducer's own output, or the exact
replayable request an outside party can issue. And the evidence MUST be verified to be about the thing it
claims — check the output's own host, build and ids against the claim before publishing. "Verified" as free
text is banned.
Reason. An artifact carrying the right version string and the wrong run is worse than none, because it
survives scrutiny while being false — and with two trees and one public host, cuppy generates that artifact by
accident every time an agent curls loopback.
Enforcement: prose, reinforced by restart.sh, which prints the output an agent must paste.
§3. Machine gates: what "green" means today
One command. bash scripts/gate.sh, the only authoritative list of checks. .github/workflows/ci.yml MUST
call it rather than restate its steps — it currently restates them, the same two-lists-of-one-fact class that
produced the version drift, and its "no new commit" half is a shell no-op that can never fail.
Green means all eight steps pass in the repository tree:
| # | Check | Why it exists |
|---|---|---|
| 1 | npm run typecheck, all three workspaces | the root script called workspace scripts that did not exist and silently failed; fixed during source-tree reconciliation (FEAT-001) |
| 2 | npm run build | the repository could not build at all until 2026-10-11 |
| 3 | npm test, which is also where the gate matrix runs | the matrix refuses a coverage claim whose backing test was renamed, moved or detached |
| 4 | the suite left no tracked change and no new commit | the suite wrote 47 commits into this history (L5) |
| 5 | no shipped default token outside the config chain; no secret-shaped file tracked | change-me-admin-token is public knowledge |
| 6 | the two trees identical across the declared source roots | a one-tree fix is a system regression by construction |
| 7 | every changed code work item has an impact record (R1) | the artifact existed without the gate |
| 8 | warns when the newest vet record is older than the newest code commit (R3) | a skipped vet is how the incomplete-fix class survives |
Step 6 is an allowlist, and that is a correction rather than a detail. It enumerates the source roots the
running service is built from — apps/api/src, apps/web/src, apps/web/public, packages/shared/src, plus
the workspace manifests — and compares only those. It does not subtract a growing list of exceptions from
"everything".
Two reasons, the first being this standard's own §7.4: *an allowlist is the only form that stays correct when
something new appears.* An exclusion list silently begins comparing every directory a later phase adds. This
was measured, not predicted. Run verbatim against the two trees on 2026-10-11, the first draft's exclusion-list
comparison returned four differences — CLAUDE.md, CURRENT_WORK.md, QUICKSTART.md and a diverged
README.md — none of which is compiled into anything the service serves. So step 6 was red before a single
new governance file landed; restart.sh refuses on a red gate (C9); and the net effect of adopting the standard
would have been that cuppy could not restart at all, including to deploy the next security fix. The
allowlist comparison, run against the same two trees at the same moment, returns identical.
A gate that blocks the fix for the incident it was written for is not respected; it is bypassed, and then the policy is gone. The allowlist makes governance artifacts, scripts, docs and tracking repo-only by declaration, which is the truth, and the gate green on landing.
The comparison lives in one file, scripts/lib/tree-diff.sh, sourced by gate.sh, restart.sh and
preflight.sh. Three copies of one exclusion list was a §6 one-authority-per-concern violation inside the
governance tooling itself. That file also records, with reasons, what is deliberately not compared:
config/ and storage/ are runtime-mutable — the application writes them, so production legitimately diverges
from the repo the moment anyone saves a connection — and dist/ is build output, whose real property ("prod's
build is not stale against prod's source") restart.sh checks directly instead.
Step 7's coverage limit, disclosed rather than widened. It fires only when a tracking record and code change in the same range, so the second and later commits of a feature — code with no tracking-record edit — pass it silently. Widening it would false-red every ordinary follow-up commit, which teaches bypass. The honest position: step 7 catches the opening of an undocumented work item, not every commit inside one, and R1 covers the rest as prose.
What green does NOT mean:
- CI is written, not enforcing. It has never run; there is no remote. It becomes gate when the owner
configures one — operator-hand, and the highest-value unblock in this document.
- There is one test tier. Everything in
apps/api/test/is in-process, isolated storage, no network, no
database service. No contract tier boots the real server against a real database. Planned, Phase 1.
- A skipped or crashed run is not a pass. Exit 2 means the gate could not run: never a pass, never a fail.
- A rotten or false-positive finding is surfaced, never worked around — not with
--no-verify, not by
re-running until green. If a check is wrong, fix the check.
- The gate takes minutes, running typecheck, a full build and the whole suite serially. During a live
incident that is a hazard, not a safeguard, which is why §5 carries a break-glass path rather than leaving an agent to invent one under pressure.
§4. The artifact ledger
| Artifact | Path | Written by | Binds from | |
|---|---|---|---|---|
| Pointer of record | docs/sprints/next-task.txt | every session | now | |
| Dynamic state | CURRENT_WORK.md | every session | now | |
| Impact record | work/impact/<ITEM-ID>.json | implementing agent | adoption, forward, code items only | |
| Vet prompt | work/vet/PROMPT.md | committed template | adoption | |
| Vet record | work/vet/<SET-ID>.md | independent clean-context agent | adoption, forward | |
| Session / delivery note | tracking/updates/YYYY-MM-DD-<slug>.md | implementing agent | now | |
| Feature / bug record | `tracking/features\ | bugs/<ID>.md` | implementing agent | now |
| Invariants | docs/governance/INVARIANTS.md | any agent; rules only, never status | adoption | |
| Gate cells (status) | apps/api/src/gates/gateCells.ts | by commit only | adoption | |
| Incident ledger | docs/governance/LESSONS-LEARNED.md | the session that closed the incident | adoption | |
| ID registry | docs/governance/ID-REGISTRY.md | the commit that mints the ID | adoption | |
| Runbook | docs/runbooks/RUNBOOK-<subject>.md | the session that learned the failure mode | adoption | |
| Release note | tracking/releases/CHANGELOG.md, .../vX.Y.Z.md | scripts/release.sh only | §8 | |
| Restart log | tracking/restart-log.txt | scripts/restart.sh only | adoption |
Cost ceiling, as a rule. A code work item costs three files: its tracking record, its impact record, a share of the set's session note. A set costs one more: its vet record. A docs-only item costs one line. Anything that would raise those numbers must first retire something. Reason: a ledger demanding five files per item is abandoned by week three, and an abandoned ledger is indistinguishable from no governance while still costing the tokens to read.
Pointer hygiene. The current state block plus at most the two previous; older blocks roll into
tracking/updates/. Every session is told to read it, so an unbounded ledger is a per-session token tax.
prose.
Stale documents are retired loudly, never silently and never by deletion — a dated STALE — RETIRED banner
at line 1 naming the replacement. An agent that reads a confident, wrong document acts on it, and this
repository's docs/PASS*.md files describe a surface that has moved, several still showing x-semarchy-role as
a live auth path. prose, with the sweep as residual R7.
Counts are derived, never quoted. Test, tool, route and version numbers are read from the system (npm
test, /api/health, grep -c over the catalog) and MUST NOT be restated in a governance document as fact.
Where a document needs a number, it names the command.
Reason, not hypothetical: the first draft of this standard quoted "223 MCP tools", "260 routes" and "24
PASS* files". Measured 2026-10-11: the catalog held 217 tool definitions, api.ts registered 261
routes, and there were 23 PASS* files. Three numbers, three wrong, in the document that forbids quoting
them — one lifted from its own list of examples of stale numbers. The rule survives its author. prose, and
the only exception is a dated incident narrative, where "136 MB on 2026-10-10" is history rather than a
claim about the present.
§5. Autonomy, and what is operator-hand
An agent proceeds without an operator turn only when all three hold:
- Reversible.
git revertundoes it; no destructive migration, external publication or production
mutation; and it is inside the scoped work item or its written spec.
- The gate is green, honestly. Exit 0, with skips and rotten findings surfaced rather than counted. Exit 2
is not green.
- No open state-source disagreement on the touched seam.
Criterion 3 is load-bearing and is its own rule:
**A disagreement between the repository, the production tree, the pointer, CURRENT_WORK.md, the running pm2
process and the database is itself the defect. PAUSE and surface it. Do not pick the source that lets the
work continue.**
Reason. Two of three defects were state-source disagreements, not coding errors: the file said production and the process said development, because dotenv never overwrites an injected variable (L2). "Two trees, patch both" is a licensed, permanent disagreement of the same kind, which is why the cutover is Phase 0's first task. Three live instances are named in residual R10. gate for the tree half (step 6), prose for the rest.
Standing agent authority (no operator turn needed)
Commit each finished slice to the working branch; run the gate; build; write and update tracking, impact, vet and session records; author and republish documentation artifacts; run read-only probes against the public hosts; dispatch read-only audit subagents; mint and delete its own short-lived test fixtures.
An irreversible act is never committed and deployed in the same session. The commit lands; the deploy waits for a later turn. Reason: it is the one half of the inherited cooling-off control that costs nothing. The other half — a mandatory timed delay — is rejected (§9 C3): the sibling that built it opened a work item to walk it back after four ceremonies cost an hour of serialized waiting.
Operator-hand — landed as prepared and pending, with a runbook
An agent principal is never an approver. Where a control expects a second reviewer who does not exist, the compensating path is honest attestation in the record + the act surfaced to the operator + the act left pending or reversible — never a fabricated second identity, and never "reviewed and approved" where one agent reviewed itself. The wording:
review: "independent clean-context agent pass (work/vet/PROMPT.md), read-only, run <n>; no human reviewer —single-operator compensating controls, SoD infeasible (org size 1). NIST SP 800-53r5 AC-5; ISO/IEC 27001A.5.3; SOC 2 CC3–CC7."
Reason for the citation and not just the candour. Fabricating a second approver identity is a control deficiency and a misleading one to an auditor; the same standards that require separation of duties also provide for its infeasibility in a one-person organization, through documented compensating controls. Cuppy is about to publish audit artifacts and a benchmark whitepaper describing real defects on a live public service, so the attestation must carry the clause that makes it defensible rather than merely honest.
The following are the operator's hand regardless of what the permission system allows: pm2 restart or stop
outside restart.sh (customer-visible on a live public site); the pm2 cutover to the checkout (runbook
prepared, not executed); DNS, vhosts and certificates (shared host, three other sites front the same proxy);
.env changes including COMING_SOON* and the preview token (a product decision and a credential); secrets
creation or rotation; any write to storage/app.db outside the application; destructive migrations and any
non-additive-first schema change (§7.8 F1); writing another project's production database; configuring a git
remote, pushing, tagging, publishing (publication is the operator's, local commits are the agent's); any change
to the authorization seam; hub publication or unpublication once hubs exist (irreversible from a consumer's
view).
Break-glass: the incident path
A live security incident MAY need a restart sooner than the gate can finish. Inventing a path under pressure is how a script gets bypassed permanently, so the path is written in advance:
bash scripts/restart.sh --break-glass --reason "<incident, one line>"
Its scope is the gate's latency and nothing else. It skips only step 1. Every other refusal still
applies, and each costs seconds rather than minutes: the two-tree source parity check, the build-staleness
check, the production-mode assertion read from the process, the public-host verification, the L1 traversal
re-probe, the log append. The line is marked BREAK-GLASS. gate.sh MUST be run once the incident closes, and
the next vet record MUST disposition every break-glass line since the previous one — an undispositioned
break-glass is a finding in its own right.
Two-tree parity is never skipped, even here, and the distinction is the point: if the trees differ because
an agent has patched only one, the remedy is to patch the other — seconds of cp — not to deploy half a fix.
Shipping a one-tree fix during an incident is exactly how a fix reads as complete while the hole stays open,
which is L4's class. Break-glass buys time against a slow check; it never buys permission to skip a control.
Reason. C9 plus a minutes-long gate is a deadlock whose only exit is an agent typing a bare pm2 command,
losing the public-host verification and the reproducer re-probe at exactly the moment they matter most.
Break-glass drops the expensive check, keeps every cheap one, and leaves a reviewed record. prose for the
decision to use it; gate for everything it does not skip.
The authorization seam is frozen
lib/auth.ts, lib/access.ts, lib/comingSoon.ts, the cuppy_session cookie path, lib/bootAssertions.ts
and the NODE_ENV resolution are operator-hand; agents treat the seam as read-only outside an
owner-released work item.
Never code around a refusal. A 401, 403, 428 or a boot assertion that blocks an agent is the control working. A mitigation that weakens the guard is by definition operator-hand.
Reason. An agent that can edit the seam can unblock itself through the security control, and this is the exact
seam whose misconfiguration granted owner to anonymous callers. prose today; planned to add a gate.sh
step failing when a diff touches these files without an owner-released item id in the commit trailer.
No history rewriting, with its one recorded exception
Corrections land as forward commits referencing what they fix. Exception, recorded: on 2026-10-11 the branch
was rebased onto 9bf78d3 to drop 47 test-authored commits, after taking the safety branch
backup-before-history-cleanup. That is the sanctioned shape — safety branch first, reason in LESSONS-LEARNED
L5 — and the only one. prose.
§5A. Model-tier governance
- Orchestration — triage, adversarial vetting, synthesis, release decisions — stays in the main loop.
- Every subagent dispatch MUST set
modelexplicitly:opusfor analysis, code and vetting;sonnetfor
mass or mechanical work; haiku for the cheapest fan-out. At most four concurrent.
- Subagents never inherit the model. Inheritance binds at agent creation and persists for that agent's
life, including across every later resume.
- A subagent MUST NOT be resumed across a governance boundary (a vet round, a deploy, an acceptance).
Re-dispatch fresh with a self-contained prompt.
- A subagent that dies mid-event on a model, credit or limit error is a PAUSE, not a retry-in-place: verify
what actually landed (git log, git status, the built artifacts, the running build) before relaunching.
- Never run subagent work on the orchestration tier. Degrade opus → sonnet → haiku → the main loop; never
up-tier.
Reason. Rules 3 and 4 cost a sibling a release event: a runner agent created early in a session silently held
the then-current main-loop model and died on its credit limit during release preflight. For cuppy the same
failure mid-deploy leaves a public site half-patched across two trees.
Enforcement: gate — .claude/hooks/block-orchestration-tier-subagent.sh, a PreToolUse hook on
Agent|Task that exits 2 on an orchestration-tier model. The only control here that fires without being
invoked. Its coverage limit is in the script: it sees an explicit model only, never an inherited one —
which is how the incident happened. Rules 1, 3, 4, 5, 6 are prose and are repeated in CLAUDE.md for that
reason.
§5B. Token-budget governance
- State the estimate before the spend. Before any fan-out of three or more subagents, or any single
long-running agent: worker count, each worker's tier, a rough token estimate, in the thread, so the operator can veto before it runs rather than after.
- Always leave it recoverable. Every subagent's output MUST be committable on its own, and the orchestrator
commits in slices as results land rather than batching to the end. A limit can terminate any agent at any instant; the test of the design is whether that costs a re-run or costs the work.
- Cheapest instrument that answers the question. A targeted
grepover re-reading a file; one test file
while iterating and the full gate once at the end; the main loop over a subagent for a single lookup. Re-running a green gate to feel sure is waste, not rigour.
- Observable proxies, not an unreadable meter, and check the scope is not already covered before a
fan-out. The plan meter is not readable from inside this host, so the agent gates on what it can see: workers already spawned, whether a 429 has occurred, elapsed wall-clock, and whether a committed record already answers the question. After any 429: finish and commit what is open, no new agents, checkpoint the pointer, say plainly what remains.
Reason. Rule 2 is the only one that actually saved the work when a 429 terminated a sibling's worker mid-task. Rule 4 replaces that project's "ask the operator for the meter", whose stated fallback — assume the last quarter — would forbid most fan-outs every time and therefore be ignored: a rule practice must violate to function is a rule that decays. A duplicate fan-out is the most expensive single mistake available and the check against it costs a thousand tokens. Enforcement: prose, by construction; no hook can read a budget. The control is that the estimate is stated in the thread before the spend.
§6. Sweep and parity discipline
Capability parity is a mandatory sweep lens. Pass 1 of any sweep inventories each capability across all five
surfaces: routes/api.ts (REST) ↔ mcp/tools.ts + mcp/handler.ts (MCP tools) ↔ tier gating in
lib/access.ts ↔ lib/redact.ts (response masking) ↔ confinedPath/confinedFile in lib/fsdb.ts
(filesystem reach).
Backend-reachable with no gate is a dark-capability finding. Advertised by a description or a tier table but unenforced in the handler is a parity finding, and it is the security case:
The request handler is the only enforcement point. Tool descriptions, tier tables, MCP annotations and the web UI are advertisements. Identity comes from the authenticated boundary — a bearer token or thecuppy_sessioncookie — never from the request body or a header, and never defaults open when resolution fails.if (ctx?.something) scope(...)is a forbidden shape: absent context means an explicit refusal or an explicit public-safe redaction, chosen in the code and written down.
Reason. All three of this week's defects are findings of this one lens. The traversal was a handler with no gate behind a surface that assumed one. The owner-via-header bug was a client-supplied claim treated as enforcement. The email leak was a redaction-parity gap in a codebase that already had the masker.
Skew is a standing L2 axis. Every contract change states its stale-client and stale-server behaviour.
Reason: cuppy's clients are coding agents holding cached catalogs of a hand-rolled JSON-RPC surface pinned to
protocol 2024-11-05; a renamed tool or a tightened schema is a silent break for every agent that already
listed tools. The live drift between package.json and mcp/handler.ts:199 means a client cannot even tell
which surface it has.
Findings define classes, not instances. A finding MUST be swept across every sibling site of its class —
grep the pattern across the whole surface — before it may close. Fixing only the cited instance is an incomplete
fix that reads as complete. Reason: measured, twice. Path confinement had been reimplemented several times
with inconsistent rigour; round one hardened three of four call sites and left projectFileBytes on a
startsWith check with no separator. Enforcement: prose, carried as a class swept: line in the commit
body and in the vet record.
One authority per concern. Duplicated logic across the REST surface, the MCP handler and the web is a defect
even when it works. One confinement helper, one git-root helper, one archive guard, one masker, one identity
resolver, one authorization seam — and, after this vet, one two-tree comparison (scripts/lib/tree-diff.sh),
because the governance tooling had written the same exclusion list three times. prose;
inv:one-version-of-record is the first mechanised instance and is red.
Opportunistic extraction. A sprint that materially modifies a region of an oversized module extracts that
region as part of the change. Pure line-count refactors with no functional driver are rejected as churn.
Reason: routes/api.ts is over a thousand lines of route registrations; both ossifying and churning it are
real risks with agents building seven phases. prose.
§7. Security controls, expressed as testable controls
The rules live in INVARIANTS.md in a fixed five-field shape — Immutable because (the attack, with the
incident), AC+ (the control works), AC− (the bypass is denied — the load-bearing criterion), Verify
(the method), Std (a formal anchor). The status of each lives only in gateCells.ts and changes only by
commit. A specification that carries status becomes a ledger and rots.
7.1 The AC− rule. Every security invariant MUST carry a negative acceptance criterion. A positive test proves a feature exists; only the negative test proves the bypass is closed. An invariant with only an AC+ is incomplete. Reason: the round-one path-confinement test never varied the root, which is precisely why round one shipped a hole that a sibling-prefix and a symlink both walked through. gate (the matrix refuses a green cell with no backing test that references it).
7.2 Red is a schedule, not an omission. A control cuppy cannot yet test is written down anyway, as a
cell with status: 'red' and a named blocker, surfaced as it.todo in every run. An invariant false as built
is marked FALSE-as-built with its file and line, and is the defect-to-fix rather than an omitted statement.
Reason: every one of this week's defects was nominally forbidden by prose. A predicate in prose is a wish; a
red cell with a named blocker is a schedule. gate.
7.3 Floors and non-representability
Security-critical values are not configuration. A floor a config could relax is not a floor. Where a value should not exist at all, it is removed from the type rather than defaulted off.
| Value | Posture |
|---|---|
NODE_ENV on a host declaring public hosts | not configurable — boot assertion exits non-zero. Fatal, because the state is exploitable by someone who knows no secret |
role from x-semarchy-role / x-forwarded-host / the first x-forwarded-for hop | must be non-representable in production: the header path deleted from the production code path, not disabled by a flag. Currently flag-conditional — RED (FEAT-018) |
ADMIN_TOKEN, DEV_TOKEN equal to their shipped defaults | not configurable on a public deployment — fatal boot assertion plus gate.sh step 5 |
| redaction floor (owner email, key-shaped tokens) below admin | not disableable below platform scope |
| rate-limit identity | not configurable — the verified peer only; trust proxy MUST stay off, because this proxy appends the real peer last and Express reads from the right |
| rate-limit windows, body caps, HSTS age, CSP string, retention window, zip caps | soft config with a code ceiling, named default at the head of the chain |
COMING_SOON* and the preview token | soft config, operator-hand |
Reason. A config overlay that can relax a control is a hole, and the production-in-development-mode defect is that hole realised: a literal in the wrong file defeated the whole config chain and armed three trust paths. gate for rows 1, 3, 5; planned (FEAT-018) for row 2.
7.4 The named conflict cuppy must resolve in writing
Hub content MUST be publicly readable and the production database MUST NOT be, and one process serves both from one filesystem. Not a theoretical tension — it is how a traversal streamed the database to anonymous callers. The resolution, as a design constraint rather than a judgment call:
Every servable root is an allowlist entry, never an exclusion list, and every path derived from caller input
is resolved through confinedFile against a declared root with real-path equality. The database and any key
material live outside every servable root.
Verify: a drill that walks every file-serving route with both traversal shapes and asserts a 404 and that
the database file is never opened. gate for inv:storage-path-confined; planned (FEAT-019) for the
drill that enumerates routes rather than naming the four that were found. *This is the control whose absence
means a new unguarded route is still invisible.*
7.5 Error surface and redaction — what binds now
| Rule | Reason | Word |
|---|---|---|
| Internal errors reach callers as an id only, and the sanitiser is a deny-list of unrecognised internal errors, never an allow-list of known-bad ones | an allow-list leaks every error class it has not met yet; a sibling leaked SQL text, absolute paths and a raw validator dump in sequence for exactly this reason | gate for the id (middleware landed in FEAT-004); planned for the deny-list shape |
An error path MUST NOT change a redaction decision; a catch that yields the unmasked body is forbidden | the round-one masker failed open on any exception; it now fails closed with a correlation id | gate (redact.test.ts) |
| Secrets never appear in chat, git, a document, an error, a log, or a dev-only response field | this project leaked an email with output hygiene as its only control | gate (step 5; redact.test.ts) |
.gitignore and the image exclude *.pem *.key *.crt *.p12 *.pfx .env secrets/, asserted by the gate | an untracked-by-luck secret is not a control | gate (step 5) |
| A credential an agent generates or resets is stored in the same step in a named store outside git at mode 0600, with its existence and location in the session note | one that exists only in a transcript is lost at session end and leaked for as long as the transcript lives. Creating the store is operator-hand | prose |
7.6 Tests may never touch the production store. A run MUST assert its resolved storage path is inside the
throwaway test directory and is not the production storage/app.db, failing the whole run otherwise.
Anything that writes a real store needs a second explicit opt-in flag beyond the ordinary test environment.
Reason. apps/api/vitest.config.ts sets STORAGE_DIR: './.test-storage-mcp' — a relative path resolved
against the process working directory, with nothing asserting where it lands. A suite run from the wrong
directory writes to a real store, on a project whose production database has already been streamed to the
internet once and whose stores are git-backed, which is how 47 commits reached this history.
planned — FEAT-020, an assertion in test/global-setup.ts;
inv:tests-never-touch-the-production-store is RED with that blocker.
7.7 Concurrency — the part that is portable today. Every concurrency control is row- or key-scoped: no table-level lock, no global mutex around a hub write, nothing that blocks unrelated rows, and every new shared-table writer cites its pattern in a comment. And any read-then-insert of a monotonic key is backstopped by a UNIQUE index at the key's grain, with the racing loser's constraint error surfacing as a typed, retryable refusal that names the retry — never a generic failure, never the raw driver code. Both hold on SQLite, so both bind now. prose.
7.8 Rules that bind at Phase 1, written now because they are free now
Phase 1 moves cuppy from SQLite to PostgreSQL and Phase 3 stands up the first hub. These are the rules that are one paragraph before that work and a migration afterwards. None is enforced; none may be cited as a control. They are here so the first implementer inherits a decision instead of making one under deadline.
| # | Rule | Why it is cheap now | Lands | |
|---|---|---|---|---|
| F1 | Every schema change is a numbered, additive-first migration; a destructive default is operator-hand | nothing is applied by hand and every change is reversible | Phase 1 | |
| F2 | Bootstrapping and verification code runs AFTER the migration deploys, never before | a sibling paid for this ordering three times | Phase 1 | |
| F3 | A failed migration exits non-zero rather than serving a half-applied schema; the boot assertion "a migration is pending" refuses the start | a server serving half a schema is the worse outcome, and a restart applies no migrations by itself | Phase 1 | |
| F4 | Objects a migration tool cannot see (functions, triggers, policies) are registered in a required-objects list a test asserts, surfaced at boot as `ok \ | degraded` | an object invisible to the tool is silently dropped by a later generate | Phase 1 |
| F5 | One secret provider, fail-closed: every secret resolves through one accessor that throws rather than defaulting | the live counter-example is lib/config.ts:67 (residual R5) | Phase 1 | |
| F6 | A documented maximum rotation age with a startup warning; defer a KMS, keep the provider seam | a key nothing tracks the age of can never be rotated | Phase 1 | |
| F7 | Every input constraint is declared in the tool schema, so rejection happens at the transport before the handler runs | a server-side bound leaks the validator's internals; a declared one returns a clean protocol error | Phase 1 | |
| F8 | A response distinguishes "nothing needed redaction" from "redaction was off" | an agent consuming a hub cannot otherwise tell an honest answer from a degraded one | Phase 1 | |
| F9 | Queue claiming, lease tables and transaction-scoped advisory locks, as a closed toolkit with a required citation | these are PostgreSQL primitives that do not exist in SQLite; naming them now beats case-by-case judgment later | Phase 1 | |
| F10 | Free text is constrained for rendering safety, not only length: bidi overrides and control characters rejected | hub content is third-party text rendered to a reader who then makes a trust decision; a bidi override makes two distinct records render identically | Phase 3 | |
| F11 | A published-claims tracker: one row per claim, with its evidence and gate cell. The status column changes only on landed evidence, never on intent — and a claim may not rest on a single supporting mechanism | a sibling had to write down explicitly that deny-by-default is a mechanism, not the architecture, because one landed control was being read as satisfying a whole standard (NIST SP 800-207). A benchmark over five to ten hubs generates exactly that temptation: one green cell quoted as "isolated" | Phase 3, before the whitepaper publishes a number |
F11's gate-cell half is gate today; its tracker is not built. A benchmark whose numbers move on intent is the reputational form of a failed certification, which is why it is the one row here with a named deadline.
7.9 Communication register for security work. Work here is authorised hardening of our own code. Write
summaries, comments, records and commit messages in a defensive-remediation register: describe what is gated,
audited or closed — never an abuse recipe. Prefer "authorization gap" over "privilege escalation", "reachable by
a low-privilege caller" over "exploitable", "grant elevated access without authorization" over the escalation
verb, "adversarial review" over "red team". Keep standard NIST/ISO vocabulary: least privilege, deny-by-default,
break-glass, separation of duties. Reason. Cuppy will publish audit artifacts and a whitepaper describing real
defects on a live public service, written by agents. Hardening described in attack language gets refused,
flagged or mis-escalated — including by the agents doing the work. prose, placed in CLAUDE.md where it
loads automatically.
§8. Release process
Cuppy has no release process. This section creates the smallest honest one and states what is blocked.
8.1 One version of record. The version lives in one file: package.json → version. SemVer 2.0 core
only; pre-releases are branches and commits, never versions. No one edits it by hand — not the operator, not
an agent. scripts/release.sh is the only writer. Reason. The failure mode is live: package.json and
/api/health agree on one number while mcp/handler.ts:199 hard-codes another. inv:one-version-of-record is
RED (FEAT-016). gate once the cell is green.
8.2 Two numbers, always. /api/health MUST report both the version of the build on disk and the version the
running process loaded, plus which tree the process was launched from. When they differ the state is
restart pending. Reason. With two trees and one public host, "which build is serving?" is cuppy's most
important operational question and the first one every re-vet must answer; one endpoint change turns the
project's most dangerous standing condition into a number a session reads at preflight instead of a premise it
assumes. planned, FEAT-016.
8.3 Commit grammar. type(scope): <ID> — summary, Conventional Commits 1.0, with two load-bearing
additions: harden(...) for adversarial-pass remediation and security(...) for security-control changes. !
or a BREAKING CHANGE: footer marks a break. The scope list is itself a blast-radius declaration. Every subject
carries a registered ID. The body carries the decision or finding that prompted it, the change bullets, the
class swept: line, and the evidence lines tests:, gate cells:, verified: <public-host output>.
Reason. This repository's git log is its only durable record: no CI has ever run and there is no remote. A
week of security remediation is currently invisible in the log because it was filed as fix. One declared type
list, in this file, is the list release.sh reads. prose for authoring; planned for the registered-ID
check.
8.4 The cut. scripts/release.sh is the only path. It does not exist yet — it is written together with the
cutover, because a cut that cannot deploy from a tag has nothing to cut. Its contract, so it is not improvised:
- refuse a dirty tree; 2.
gate.shto exit 0; 3. apply pending migrations before anything restarts,
because new schema in front of old code is inert while new code in front of an old schema is the failure
direction; 4. write package.json, CHANGELOG.md and vx.y.z.md; 5. one commit
release: vx.y.z — <title> containing only those files, and an annotated tag; 6. restart through restart.sh;
- verify through the public hosts; 8. write Verify-after-deploy and Rollback last, from real output.
A red gate means no release commit. A failure inside the cut restores the files and halts with nothing advanced. Every entry carries runnable Verify-after-deploy commands against the public host and a Rollback section. Release artifacts are write-once per version. Version level: a breaking marker forces major regardless of volume; a shipped schema migration forces at least minor; volume alone never forces a major. Reason: cuppy's consumers are other people's agents with pinned tool names, so a rename is a major event even when the diff is small.
8.5 What is blocked, and on what. The inherited release standard requires every deployment stage to be a
git worktree of the same repository. Cuppy's production tree is not a checkout at all. The release process
is therefore BLOCKED on docs/sprints/RUNBOOK-cutover-to-checkout.md, prepared, verified and operator-hand. A
blocker, not an adaptation; §9 C2 records it. Adoptable and required now: 8.1 and 8.3, plus creating
tracking/releases/CHANGELOG.md with an ## Unreleased heading so a cut has somewhere to land.
8.6 Separate numbering, fenced in advance. The build version, the MCP protocol revision (2024-11-05), the
tool-catalog version and per-hub content snapshot versions are four different numbers and are never required
to agree. Only the build version is the version of record. Reason: fencing this now costs one paragraph;
discovering it across ten hubs costs a synchronisation chore that never ends.
§9. Conflict ledger
An unrecorded divergence is a defect.
C1 — Gate definition. The inherited §3 requires the suite green *with the live contract tier running and
skipped: 0 confirmed. Cuppy has one test tier and no contract tier. Cuppy's side:* green is §3's eight steps
in the repository tree, with the single-tier limitation stated rather than papered over. The tier arrives in
Phase 1; the skipped: 0 half transfers today.
C2 — Release and deployment. The inherited standard requires worktree stages and a promote ladder. Cuppy has neither, and production is not a checkout. Cuppy's side: §8 adopts one-version-of-record, the commit grammar and the changelog now; the cut and the two-number health report are blocked on the cutover and are named residuals. Cuppy does not claim the release standard.
C3 — The second-approver gap, and the cooling-off half. Several inherited controls expect a reviewer who does not exist. Cuppy's side: the sibling position — honest attestation with its standards citation, the act surfaced, the act left pending or reversible; never a fabricated second identity. The substitution for the missing human is mechanical: a fresh clean-context read-only agent running a committed prompt that probes the original reproducer through the public host before reading the implementer's disposition (R3). Self-attestation by the implementing agent is explicitly rejected, which is where cuppy diverges from the inherited R3 as written. The timed cooling-off delay is also rejected — the project that built it opened a work item to walk it back after it cost an hour of serialized waiting in one wave; cuppy keeps only the free half, "never commit and deploy an irreversible act in the same session".
C4 — Lens count. The inherited framework walks 56 lenses across three layers. Cuppy's side: twelve L1 lenses weighted to cuppy's threat model, eleven L2 axes, six L3. Scale the walk, never waive the record — the inherited framework itself says the full walk is overkill for small changes.
C5 — ID registry. One sibling rejects a separate registry as a second source of truth; another adopts one.
Cuppy's side: adopt it, because cuppy is about to mint identifiers across seven phases, five to ten vendor
hubs and a published benchmark. One line per ID, never a backlog: the content lives in the artifact the row
points at. ISSUE-NNN is dropped from the identifier set — tracking/issues/ does not exist and an id
class with no directory invites inventing one.
C6 — Boot document. An inherited quickstart is a large file carrying ports, URLs and a hand-typed preflight.
Cuppy's side: the boot document carries no instance data; hosts, ports, process names, versions and
project ids come from scripts/preflight.sh and the Commander project row, and the facts table names the source
instead of the value. A boot document that restates state goes stale within a release, and this project's two
quickstarts already disagreed with each other and with the console's resolver (residual R6).
C7 — Enrollment over big-bang. A MUST cuppy cannot satisfy everywhere on day one binds an enrolled set, declared in the gate and additive-only; a surface not yet enrolled is owed the sweep, not exempted, and a surface created under the rule enrolls at creation. Reason: a platform-wide MUST the repository violates everywhere is ignored within a week. This is how FEAT-017's route-auth floor lands.
C8 — Docs-only items get no impact record. The inherited R1 says "no exceptions, including docs-only items".
Cuppy's side: a docs: line in the session note (R1). The inherited rule was written for a project whose docs
commits are a minority of its history; cuppy is a documentation platform whose agents write docs commits
continuously, and the first rule to be quietly dropped is the one demanding a JSON file for a typo. A narrower
rule that holds beats a broader one that is abandoned.
§10. Adoption vet of this document
The findings this vet changed are listed first, because a vet that reports only comfort is not a vet.
- A deploy deadlock, measured. Step 6 compared the trees by subtracting exceptions from everything. Run
verbatim on 2026-10-11 it returns four differences — CLAUDE.md, CURRENT_WORK.md, QUICKSTART.md, a
diverged README.md — so it was already red before any new governance file landed, and restart.sh
refuses on a red gate. Adopting the standard as drafted would have made the project undeployable, including
for the next security fix. Step 6 is now an allowlist of declared source roots living in one shared
helper (§3) — which is what §7.4 already required of every other allowlist here — and it returns identical
against the same two trees. §5 gains a break-glass path for the incident case that remains.
- Three quoted counts, all wrong, in the document that forbids quoting counts. Removed; the rule now
carries its own author as the example (§4).
- An invented phase. Five rules were scheduled into a "Phase 0b" that appears nowhere in the specification,
which has Phase 0 through Phase 6. A standard that schedules work into a phase the build plan does not have is a standard the build plan ignores. All five now name their work item or say "Phase 1".
- Self-approval one layer up. R3 required an independent agent but let the implementing session write that
agent's prompt. The prompt is now a committed artifact (work/vet/PROMPT.md), and R2 distinguishes the
implementer's proposed verdict from the vet's binding one.
- A cost ceiling. The ledger required an impact record for every item including docs-only ones — five
artifacts per item on a documentation platform. Scoped to code items with a one-line alternative for docs, and the ceiling is now a rule (§4) so the next addition must retire something.
- C3 downgraded to prose. "Restart only by the script" was presented as a gate. Nothing stops a bare
pm2
command, so C2 and C9 are conditional on compliance too. Named, with the hook that would fix it.
- The security answer was too generous. L1 and L3 are caught only as regressions in known routes; the
controls for a new unguarded route are red. Stated in the measurement section, before any rule.
- An over-narrow allowlist, caught on the rebound. Fixing step 6 created the mirror risk — a false green.
apps/web/public is copied into the served bundle and llms.txt had already drifted there once, so it is
now a declared root. config/ is deliberately excluded, with the reason recorded in the helper: the
application writes it at runtime, so comparing it would manufacture a red gate out of normal operation.
restart.shlost a real property when its wrong check was removed. Byte-equaldistacross two
independent builds was never a sound assertion, but "prod's build is not stale against prod's source" is — and without it, source parity can be green while pm2 serves pre-patch code. Added as its own step.
- Cut for being copied rather than reasoned: the append-then-supersede concurrency mandate (no incident,
no referent, and cuppy's document history already works another way); a provenance section restating reasons already attached to each rule; a twelve-row self-assessment table this list replaces; and a 57-entry adoption inventory that functioned as a trophy case.
Standing assessments, briefly, so the remaining angles are on record rather than restated at length.
Adversarial: a docs-only change grants no new write authority, and the §5A hook only ever refuses.
Least privilege: the operator-hand list is broader than previous practice, standing authority narrower and
explicit. Backward compatibility: no retroactive red cells — R1 and R3 bind forward, and existing conventions
are kept rather than migrated. Configuration purity: no runtime value is introduced. Failure modes: red
cells fail closed (a missing record blocks the next item) while emitting as it.todo, so a pre-staged gate
cannot hold the build hostage; the trade is stated and residual R3 names the forcing function it lacks.
Observability: every act leaves a committed artifact and the trail is diffable git history; the two-number
health report is the gap. Portability: bash, npm, TypeScript, Vitest, twenty lines of shell — no vendor
dependency. Performance: the only runtime cost is the §5A hook, one node parse per subagent dispatch; the
gate's own minutes-long runtime is addressed by break-glass rather than ignored.
Testability: §3 defines green mechanically as one command, runnable by an agent that did not write it.
Maintenance: every rule carries a reason and an enforcement word, so a later session can delete a rule whose
scar is gone rather than keeping it out of caution. Sprawl: four prose instruments plus an index, five
executable artifacts and one hook, with a hard ceiling of five prose instruments — the sixth replaces one.
Standards: RFC 2119 here; per-invariant anchors in INVARIANTS.md (RFC 9110 §11, RFC 6797, CSP L3,
sysexits), and NIST SP 800-53r5 AC-5 / ISO/IEC 27001 A.5.3 / SOC 2 CC3–CC7 for §5's attestation.
Token economics, with one disclosure. CLAUDE.md is held under 8 KB, this document is read on demand,
and the boot document carries no state so it never needs editing when the project moves. The disclosure: that
8 KB ceiling was raised from the drafted 5 KB by this vet, after the file already exceeded it — which is
the R12 move, made in the open rather than quietly. It is argued rather than fitted: the file auto-loads into
every session, so a kilobyte here is a kilobyte on every piece of work forever, and the ten absolutes plus the
self-approval rule do not compress much below this without dropping a scar. The test of any ceiling is that the
next addition must retire something.
And this document remains long — roughly 67 KB — which is a standing finding, not a resolved one. §7.8 consolidated everything that does not yet bind, and the vet twice attempted a general compression and twice converged without real savings, because the length is reasons rather than padding. The next vet's job is deletion: find the rules whose reason restates another's and cut them, rather than adding a twelfth section.
Named residuals — recorded, not absorbed
- R1. CI exists and has never run: no remote. Every gate in §3 is an invoked command. **Highest-value single
unblock in this document**; configuring it is operator-hand.
- R2. One test tier. No contract tier boots the real server against a real database; Phase 1.
- R3. A red gate cell has no deadline. An item can be declared delivered while its own acceptance cell is
red and nothing objects. The forcing function — delivered items must have green cells — is not built.
- R4.
FEAT-002–FEAT-005have no impact record (grandfathered). - R5.
SKILL_URL_SIGNING_SECRETis unset in production and warns on every boot. The fallback chain at
lib/config.ts:67 terminates at a shipped public literal when ADMIN_TOKEN is also unset, so this is a
fail-open default and not merely a coupling (§7.8 F5). Setting it is secrets rotation: operator-hand.
- R6. Two boot documents disagreed with each other and with the Commander console's resolver. Verified
2026-10-11: commander-dev's quickstart_paths default is ['.claude/QUICKSTART.md', 'QUICKSTART.md'],
first match wins (lib/tunables.js:130), so the console renders .claude/QUICKSTART.md — while both
files claimed the root one was rendered. Closed by this set: the substance moves to .claude/QUICKSTART.md
and the root file becomes a pointer. A second cause is outside this repository and is not closed by this
set: commander-dev's boot-document resolver landed in commit feat(FEAT-081) at 2026-10-11 02:36 UTC,
while the running commander-dev process has been up since 2026-10-09 06:25 UTC — so the route the console
panel calls does not exist in the build that is serving, and a 404 renders as "not found" over a file that is
present. Restarting that project is operator-hand there, not here. A separate correction is owed:
RUNBOOK-register-in-commander.md says no project row exists, which is true of the other Commander on this
host (which reads the root file, server.js:7930) and false of commander-dev, where the row has existed
since 2026-07-20 with git_repo_path set to this repository. Scope the runbook to the Commander it means.
- R7. The
docs/PASS*.mdfiles describe a surface that has moved and several showx-semarchy-roleas a
live auth path. They need the §4 retirement banner. Count them; do not quote the count.
- R8. The rate limiter is in-memory and per process, so pm2 cluster mode stays off until a shared store
exists.
- R9. CSP carries
'unsafe-inline'onscript-srcbecause the coming-soon and activation pages render
inline scripts. Per-response nonces are real work and are not done.
- R10. Three open state-source disagreements, all §5 criterion-3 defects, all the owner's to resolve.
(a) CURRENT_WORK.md says development is not released while FEAT-002–FEAT-005 are built and deployed.
(b) It says the hotfixes "remain uncommitted in this repo" while git history shows them committed.
(c) It reports the specification at revision 18 while the newest roadmap commit says revision 19. An agent
that reads any of these and acts on it is acting on a false premise. This standard's rule is to pause; these
three are the live test of whether that rule is obeyed.
- R11. The architecture fork this standard assumes closed is open. The pointer records an unresolved owner
decision on where ACTION lives — pure documentation; executable documentation where cuppy emits a ready-to-run request the calling agent executes with its own credential; or the status quo where cuppy holds credentials and runs the call. This document assumes the status quo, and that assumption is load-bearing for §7.4's resolution, §6's five-surface parity lens and the operator-hand list. If the owner picks the emit-a-request option the threat model changes shape — a new invariant is needed (an emitted request MUST never carry a cuppy credential) and parts of §6 narrow. No rule here may be read as pre-deciding that fork, and the first vet after it closes re-opens §6, §7.4 and §5's operator-hand list.
- R12. Step 5 has one true positive to clear in the adopting commit. Run against the repository on
2026-10-11, the corrected secret check finds scripts/smoke.ts:2 —
const devToken = process.env.DEV_TOKEN || 'change-me-dev-token' — a second copy of a shipped default
outside the config chain, which is precisely the class the rule forbids. It is a dev smoke script and not in
the served path, so the exposure is low; the finding is nonetheless real and the fix is one line (resolve
through lib/config.ts, or fail when the variable is unset). It is named here rather than added to the
check's exclusion list, because excluding a true positive to make a gate green on landing is the same
mistake as step 6's exclusion list, one rule further down. The adopting commit fixes the script; the gate
stays strict.
*Every rule above traces to an entry in LESSONS-LEARNED.md or to a sibling's scar recorded there. A rule that
cannot name one SHOULD be deleted at the next vet.*
Change log. 2026-10-11: the five planned controls this document numbered FEAT-009 to FEAT-013 are renumbered FEAT-016 to FEAT-020, because the sprint builders had already allocated FEAT-009 (developer Postgres and S1 migrations), FEAT-010 (the gate scripts) and FEAT-011 (content tables) before the collision was noticed, and those records sit on branches under vet. The allocation registry is tracking/features/INDEX.md; an id is taken by adding it there in the same commit as the record.