Skip to content

docs: move inline narrative out of the AGENTS.md rules that already have a decision record #928

Closed as not planned

Description

@mforce

Problem

AGENTS.md loads into every agent session, so its length is a cost paid on every
run. #921 (PR #925) took it from 9,677 to 8,603 words by rewriting prose, and
that lever is now spent — 50 of 96 rules were rewritten and further wordsmithing
would be scraping.

What remains is structural:

words share
rule paragraphs 7,488 87%
— of which, rules that already carry a → decision record 4,689 54%
— rules with no record 2,799 32%
headings, code blocks, layout, prose 1,115 12%

Over half the file is narrative that already has a home. The file's own
preamble says a → link means "the narrative is behind that link in
docs/decisions/", yet those 48 rules average ~98 words each and carry the
reasoning inline as well as linking to it. The records are substantial, not
stubs: 530-multi-farm-tenancy is 4,079 words, 782-ci-job-gating 2,012,
271-single-serving-instance 1,479, 846-adapter-reach-ratchet 1,473.

This is not a repeat of #413, which moved rationale into the records. This
removes what stayed behind afterwards.

Scope

For each of the 48 rules carrying a → link, answer one question and edit to
match the answer:

What is the minimum an agent must know to avoid breaking this rule, without
opening the record?

Keep that inline. Move the rest into the record the rule already links to.

What must stay inline — this is the entire risk

An agent who never opens the record must still be unable to break the rule.
These never move, however long they leave the paragraph:

  • the imperative — what to do and what never to do, including the negative form
  • every identifier: command, path, file, env var, config key, class, test name
  • every exception and opt-out (Database:AllowInsecureConnection=true,
    RateLimiting:AllowNoTrustedProxies=true, the IdentityProvider exemption, …)
  • every accepted risk and statement of what is not guaranteed —
    "Nothing enforces this.", "only review catches them", "neither survives a merge
    to main", "never fail-open". A rule that loses its limit reads as a
    stronger guarantee than the repo actually offers, which is worse than a
    long rule.
  • any counter-example or wrong-fix warning that heads off a plausible mistake

What moves is incident narrative: which review round found it, what the earlier
broken version did, the measurement behind the decision.

Target

~6,300 words (−27%) if linked rules land near 50 words each. This is a
prediction, not a quota.
A rule needing 150 words to stay safe keeps them, and
the PR names which and why. Going materially below ~6,300 means deleting rules,
which is a separate decision.

Acceptance criteria

  • All 48 linked rules reviewed; the PR reports how many were shortened and
    how many deliberately left, with reasons for the latter
  • Zero loss of decision links, issue references, runbook links, backticked
    identifiers, exceptions, accepted risks or negative prohibitions —
    verified by count before and after, not by reading
  • Text removed from a rule lands in that rule's record; nothing is dropped
    outright without saying so in the PR
  • Each edited record still reads coherently after the insert
  • Every rule remains one paragraph
  • PR records before/after word counts for AGENTS.md and for every record
    that received text
  • No rule changes meaning, and docs: /deslop AGENTS.md without weakening its rules #921's "do not track issue state here" rule
    is respected
  • PR body stays short: this repo squashes with the PR title and a blank
    body, and long accounting belongs in a PR comment

Non-goals

  • Deleting rules or relaxing any policy
  • Touching the 49 unlinked rules — with no record to move text into, shortening
    those is deletion, and they carry the least written history
  • Creating new decision records
  • Hitting a word target at the cost of an accepted risk

Known follow-ups already identified

Three code-versus-doc mismatches were found during #921 and deliberately left,
because each needs a precision edit rather than a deletion. Fix them here or
split them out:

Worklist

All 48 linked rules, heaviest inline first.

words decision record rule
284 530-multi-farm-tenancy Multi-tenancy: every tenant-owned entity has AccountId, enfo…
272 782-ci-job-gating A documentation-only PR skips web and image, and the gate …
219 732-farm-code-rename A farm code changes only through Account.Rename, reached by …
213 846-adapter-reach-ratchet Every adapter declares its module reach, and shrinking it is f…
209 271-single-serving-instance Do not extend that list from memory. Two previous derivations …
200 775-ci-test-matrix The four test projects are legs of one fail-fast CI matrix (#7…
188 819-business-record-chronology Business dates, row timestamps and list sequences are separate…
174 688-i18n-help-label-pairing Help prose naming a control follows that control's LABEL, per …
157 280-seed-and-simulation Seed / simulation data is never boot-seeded (#280, #284, #279)…
157 514-module-ledger Declare cross-module references in the module ledger (#514/#84…
145 843-adapter-tiers Declare a tier's otherwise-unavailable module privilege before…
143 ? Key a registry by what the code says about itself, never by wh…
128 ? AuditEvents carries a durable, GLOBAL monotonic ordering key…
117 394-write-contract-callers A write-contract change must update its non-CI callers (#394).…
108 565-aspire-local-orchestration Run the whole stack under Aspire: aspire run from the repo r…
103 845-table-owners Every mapped table has one ledger owner (#514/#845). `TableOwn…
96 776-backend-coverage Backend coverage is measured, not gated (#776). `tools/coverag…
93 277-spa-e2e SPA E2E lives in tools/simulation/ui/ (#277/#385). Playwrigh…
90 579-suspension-issuance-window Suspension is immediate for use, not for issuance (#579). …
89 264-farm-timezone Farm timezone + tzdata/ICU (#264). A farm may be given its IAN…
87 847-seam-surface-guard No persistence type crosses an Application seam (#514/#847). P…
82 565-aspire-local-orchestration A new required config key must also be taught to the AppHost (…
80 848-generated-coupling-matrix The coupling matrix is generated from the ledger and its three…
79 729-owner-only-farm-configuration Farm configuration and identity are Owner-only (#729). GET a…
67 269-transient-db-retry-boundary Transient-DB retry stops at unreplayable work (#269). Use `Ena…
67 364-credential-epoch-revocation Credential epoch revocation (#364). Every access/refresh token…
66 407-migration-freeze InitialCreate is frozen; one migration per change (#407). EF…
66 370-sim-harness-boot-guards A new Production boot guard must be taught to the sim harness …
65 260-proxy-trust Proxy-trust boot guard (#260). HSTS (#144) and the per-IP logi…
65 347-process-role Process role, not statement order (#347). Declare a boot guard…
64 261-postgres-tls-floor Production Postgres TLS floor (#261/#262). In Production the e…
61 283-migrations-base-provisioning Base reference data ships as guarded raw-SQL migrations (#283)…
60 510-jwt-key-boot-check Both JWT keys are checked at boot, and the check is serving-on…
58 500-audit-actor Nothing writes an audit event without an actor (#500). `AuditW…
57 404-production-logs Production logs: compact JSON on stdout; base layer argument-f…
53 265-break-glass-recovery Break-glass: recover-admin (#265). Same run-then-exit shape …
52 332-gss-kerberos GSS/Kerberos negotiation off by default (#332). `PostgresConne…
47 263-migrate-command Migrate command + prod migration split (#263). `dotnet Cluckwo…
47 283-first-run-admin-provisioning First-run admin: bootstrap-admin (#283). Creates an Owner wi…
45 266-container-health-probe Container health probe: the healthcheck verb (#266). Before …
44 505-audit-events-no-time-partition AuditEvents is not time-partitioned, on purpose (#505). The …
36 318-design-time-migration-connection The design-time connection is fail-closed (#318). `AppDbContex…
35 267-container-hardening Container image hardening (#267). Runtime stage runs non-root …
34 417-schema-docs Schema docs are generated, committed, and regenerated with eve…
31 ? The SPA egg loop runs end to end (#13): daily entry (by grade)…
26 ? Run full stack (prod-like): `docker compose -f deploy/docker-c…
16 ? Run frontend dev: cd web && npm run dev → http://localhost:5…
14 ? Every rule here is one paragraph. A → link points to its his…

Activity

  1. added this to the SPA revamp milestone on Sep 21, 2026
  2. mforce commented on Sep 25, 2026

    @mforce
    OwnerAuthor

    The narrative move was built, reviewed, and dropped

    PR #956 implemented this issue as written: for each of the 47 rule paragraphs
    carrying a docs/decisions/ link, keep inline the minimum an agent needs to
    avoid breaking the rule, and move the incident narrative into the record it
    already links to. It reached 8,705 -> 8,392 words (-3.6%), well short of the
    ~6,300 prediction, because the issue's own "what must stay inline" list already
    covers most of the linked-rule text.

    Two Codex review rounds found 10 rules that lost a load-bearing statement.

    Round 1, both P1:

    • #819 lost "Legacy audit backfills use only exact whitelisted row events,
      falling back to the documented unknown sentinel", an instruction for any future
      backfill.
    • #845 lost "Insights owns no tables". The table-owner scanner accepts a new
      Insights table the moment someone declares it under that owner, so this is a
      design constraint with no guard behind it.

    Round 2, all eight P1:

    • the allowance that one email address may belong to users in several farms,
      which the account-scoped unique indexes implement, and which the Phase context
      line "identity is per-account email" does not state;
    • the rejected i18n guard's wrong-fix warning that matching needs root
      extraction, not includes() or stem matching;
    • that both closed adapter-tier sets come from AdapterTier.KnownSurfaces, which
      is where privilege is validated;
    • the cross-owner foreign-key key, which is constraint name and direction;
    • the 30-table fail-closed floor in TableOwnerRealModelTests;
    • the 40-adapter floor in AdapterReachRealTreeTests;
    • the 30-interface fail-closed floor in SeamSurfaceRealAssemblyTests;
    • and, in the 843 record, that "adding either makes that description stale, not
      red" is itself false, because AdapterTierRealTreeTests.McpTierRow_IsDormantToday
      asserts the row stays dormant.

    A token-level preservation check passed all ten, because a dropped constraint
    SENTENCE carries no unique identifier. A sentence-level audit built after round 1
    caught the round 1 pair on replay, but the pattern had already cost two rounds.

    The owner decided to ship only the corrections. Losing a rule silently
    re-enables a shipped defect, and about 3% of a file is not worth that. PR #956 is
    rebuilt to carry three genuine corrections and nothing else. Its AGENTS.md diff
    touches 4 of 252 lines, and every other rule paragraph is byte-identical to
    main.

    This issue stays open for the owner to close as not planned, or to re-scope. If
    it is re-scoped, the useful shape is probably per-rule and reviewed one rule at a
    time, not a sweep: every sweep so far has been measured in rules lost rather than
    words saved.

    Update: the #843 finding is now fixed in PR #956, so it needs no separate
    issue.
    The rule said of the dormant Cluckwork.Api.Mcp tier row that "adding
    either makes that description stale, not red".
    AdapterTierRealTreeTests.McpTierRow_IsDormantToday asserts report.Dormant
    contains Cluckwork.Api.Mcp, so mapping MapMcp or adding a tool type under that
    namespace turns CI red. The rule now says so and tells whoever activates the
    tier to drop that assertion in the same commit. 843-adapter-tiers.md carried the
    same claim as "goes stale (not red)" and is corrected too. The owner approved
    adding it to #956 after this comment was first posted.

  3. mforce commented on Sep 25, 2026

    @mforce
    OwnerAuthor

    Closed as not planned (owner decision, 2026-09-26). The narrative move was tried in PR #956. Two review rounds found ten rules it dropped, against a saving of about 3%, so the owner decided to ship only the four factual corrections (merged in #956).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions