Files
msd-core/docs/adr/1610-workflow-agent-size-budget-ratchet.md
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

9.6 KiB

ADR 1610: workflow & agent size-budget ratchet (per-file byte baseline + tier hard caps) [Accepted]

  • Status: Accepted — ratified 2026-07-17 (originally Proposed 2026-06-22); see "Ratification" below
  • Date: 2026-06-22

Provenance. Drafted 2026-06-22 to give an already-shipped architectural governance decision its first ADR. The decision landed across three PRs — #1089 (additive per-file workflow baseline guard), #1096 (swap enforcement to baseline + loose hard caps), #1097 (agent-size baseline + line→byte rebase) — under epic #1074, building on #717 (bytes rebase) and #683 (LF-normalized byte count) and superseding the #597 tier-max ratchet. Authored by the implementer. Verified against tests/{workflow,agent}-size-budget.test.cjs, tests/{workflow,agent}-size-baseline.json, scripts/workflow-size.cjs, and scripts/lib/allowlist-ratchet.cjs on next. The rationale here is lifted from those tests' own doc comments (the decision was documented in-code but never as an ADR).

Ratification (2026-07-17): Proposed → Accepted

Ratified by explicit maintainer directive after independent re-verification of the evidence below; the ADR had sat in Proposed for 25 days after the decision it documents had already shipped.

Evidence the decision shipped:

  • Owning issue #1074 ("replace tier-max workflow size-budget ratchet with a per-file baseline + loose hard caps") is CLOSED, stateReason: COMPLETED, closed 2026-06-12T14:00:56Z.
  • All three landing PRs are MERGED: #1089 (test(#1074): add additive per-file workflow size baseline guard, 2026-06-12T03:59:57Z), #1096 (test(#1074): swap workflow size enforcement to baseline + loose hard caps, 2026-06-12T13:30:44Z), #1097 (test(#1074): agent-size-budget per-file baseline + line→byte rebase, 2026-06-12T13:58:44Z).
  • scripts/workflow-size.cjs:32-35 — lfByteCount() implements the CRLF→LF-normalized byte count described in Decision point 2 (#683).
  • scripts/workflow-size.cjs:64-72,80-82 — measureMdFiles/measureWorkflows is the single shared measurement path cited in Decision point 5, re-exported for both the guard and scripts/update-size-baseline.cjs.
  • scripts/lib/allowlist-ratchet.cjs:180 exports assertFileBaseline — the per-file baseline assertion named in Decision point 3 and Cross-references.
  • tests/workflow-size-budget.test.cjs:95-97,102 defines XL_CAP = 98304 (96 KiB), LARGE_CAP = 61440 (60 KiB), DEFAULT_CAP = 40960 (40 KiB), NEW_FILE_CAP = 32768 (32 KiB) — the exact numbers quoted in Decision point 3.

Governance: owning issue #1074, stateReason: COMPLETED, closed 2026-06-12T14:00:56Z.

Context

msd-core/workflows/*.md and agents/*.md are loaded verbatim into agent context every time the corresponding command/agent runs. Unbounded growth is paid on every invocation across every session, and — more importantly — degrades quality: larger context erodes recall and reasoning ("context rot" / finite attention budget). With prompt caching the per-invocation cost premise is weak (cache reads are ~10% of input), so the caching-independent quality argument is the load-bearing one: lean, high-signal instructions produce better plans.

The prior mechanism (#597) was a tier-max tighten-only ratchet: it bound only the single largest file per tier, leaving the other ~85 files free to grow silently. That left the actual risk — broad, quiet creep across many files — unguarded.

No ADR governs how MSD bounds instruction-document size. The decision currently lives only in test doc comments, so it is invisible to anyone browsing docs/adr/ and at risk of being weakened (e.g. "just bump the baseline") without encountering its rationale.

Decision

  1. Measure in BYTES, not lines (#717). Line count is a poor proxy — markdown tables and fenced code are token-dense, so a line budget over-penalizes prose and under-catches dense additions. Bytes are cheap, deterministic, and need no tokenizer; they are also the unit vendors bound on (Codex caps instruction docs at 32,768 bytes, project_doc_max_bytes, and truncates past it). We adopt the unit, not the exact number.

  2. Count LF-normalized bytes (#683). Normalize CRLF→LF (content.replace(/\r\n/g, '\n')) before counting so a CRLF (Windows) checkout yields the same byte count as an LF checkout — a raw on-disk count would add one byte per line on Windows and make the guard platform-dependent.

  3. Two complementary guards, neither a tier-max ceiling (#1074):

    • Per-file baseline (the anti-creep). Every workflow/agent file is pinned to its exact byte size in tests/{workflow,agent}-size-baseline.json. Any growth fails with the file name and delta. A deliberate change is recorded via npm run size:baseline as a one-line reviewable diff. This is the day-to-day guard and it covers every file, not just the largest-per-tier.
    • Tier hard caps (the outer bound). XL / LARGE / DEFAULT are absolute red lines with real headroom (XL_CAP = 98304 / 96 KiB, LARGE_CAP = 61440 / 60 KiB, DEFAULT_CAP = 40960 / 40 KiB), a few KB above the largest file in each tier — the largest XL orchestrators (plan-phase.md, execute-phase.md) currently sit in the low-90s KB, a few KB under XL_CAP. (Exact per-file sizes are not duplicated here: they live in tests/workflow-size-baseline.json, the enforced source of truth, and an absolute byte count quoted in prose drifts with every edit.) Hard caps are never raised in normal work; crossing one is a signal to do lazy extraction, not a +N bump. New workflow files default to the Codex 32 KiB anchor (NEW_FILE_CAP = 32768) unless explicitly tiered in the same PR.
  4. The metric is a proxy for bounded loaded context — do not game it. The real target is total context loaded at runtime. Because @~/.claude/msd-core/references/... imports are loaded eagerly, moving prose into an eagerly @-imported reference shrinks the measured file while leaving (or growing) total loaded context — that is gaming the proxy (Goodhart's Law: the moment the byte count is the target, the cheapest way to satisfy it is to relocate bytes, not remove them). Legitimate extraction is LAZY: content Read only at the step that needs it (the workflows/discuss-phase/modes/ progressive-disclosure pattern). The per-file baseline + tier-cap pair is itself a Goodhart hedge — two guards pulling in different directions are harder to game than one.

  5. Shared measurement path. The guard and the baseline generator both measure via scripts/workflow-size.cjs (lfByteCount/measureWorkflows, re-exported to scripts/update-size-baseline.cjs and asserted via scripts/lib/allowlist-ratchet.cjs assertFileBaseline), so the recorded baseline and the enforced size can never drift apart (#1074).

Alternatives considered (rejected & deferred)

  • Tier-max tighten-only ratchet (#597) — SUPERSEDED. Bound only the largest file per tier; the other ~85 files could grow silently. Replaced by the per-file baseline, which guards every file. Re-open only if the per-file baseline proves too noisy in practice (it has not).
  • A line-count budget — REJECTED (#717). Token-dense tables/code make lines a poor proxy; bytes are the vendor-bound, tokenizer-free unit. Re-open only if a cheap token count becomes portably available and measurably better than bytes.
  • Eager @-import extraction to "fit" a file — REJECTED as proxy-gaming (#717). Shrinks the measured file without shrinking loaded context. Only lazy (Read-at-step) extraction counts. Not re-openable — it defeats the goal the metric proxies.
  • Codex 32 KiB as a hard ceiling for the grandfathered orchestrators — REJECTED. The XL/ LARGE tiers sit above 32 KiB because they are top-level orchestrators loaded by Claude, not Codex AGENTS.md docs; 32 KiB is the new-file anchor, not a universal cap.

Consequences

  • Positive: broad, quiet size creep is caught per-file across the whole surface; deliberate growth is an explicit, reviewable one-line baseline diff; the byte unit is deterministic and cross-platform (LF-normalized); the "bounded loaded context" goal and its anti-gaming rule are recorded where a contributor will meet them; the quality (context-rot) rationale is no longer caching-dependent hand-waving.
  • Costs: every legitimate edit to a workflow/agent file must regenerate the baseline (npm run size:baseline) or the guard reds — intentional friction that makes growth visible. The baseline JSON is a merge-conflict surface on concurrent edits (resolved by regenerating).
  • Boundary: this governs instruction-document size (workflow/agent .md loaded into context). It is distinct from the install-time skill-surface budget of ADR-0010/0011 (how many skills are eagerly registered) — different lever, different file, do not conflate.

Cross-references

  • Epic #1074 (per-file baseline + hard caps); #717 (bytes rebase + rationale); #683 (LF byte count); #597 (superseded tier-max ratchet); PRs #1089 / #1096 / #1097.
  • Code: scripts/workflow-size.cjs, scripts/update-size-baseline.cjs, scripts/lib/allowlist-ratchet.cjs (assertFileBaseline), tests/{workflow,agent}-size-budget.test.cjs, tests/{workflow,agent}-size-baseline.json.
  • Related but distinct: ADR-0010/0011 (skill-surface budget — install-time, not file size); ADR-456 (test-rigor architecture — the test these guards are authored under).
  • External anchors: Codex project_doc_max_bytes (32 KiB); Anthropic "effective context engineering for AI agents" (the context-rot / attention-budget argument).