Files
msd-core/docs/adr/4139-compact-content-seam.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

43 KiB

ADR-4139: The compact-content seam — shrink the eager window, never the guarantee

Status Accepted
Date 2026-09-06
Issue #4139
Phase-0 sub-issue #4400
Implementation phases #4401 · #4402 · #4403 · #4404 · #4405 · #4406 · #4407 · #4408
Constrained by ADR-1610 Decision 4 · ADR-3646 §Context · ADR-3889 · ADR-3942
Corrects ADR-3646 §Context — its citation of #3647 as "still open" is stale; #3647 closed 2026-09-01

Evidence note. Every count in this ADR was measured against the tree at origin/next (708d9a0b82), not inferred from the issue text. Where this ADR contradicts #4139's own description of a mechanism, the contradiction is stated as such and the measurement is given. The issue is the requirement; it is not a source of truth about the tree.

Context

What #4139 asks for

A per-project boolean that makes MSD load token-minimized variants of its own shipped prompt content, across four token streams: (1) workflow instruction files, (2) agent-skill payloads, (3) subagent spawn prompts, (4) planning-artifact templates. Approved 2026-09-01 with three conditions: rename off the sports metaphor, pilot before proliferation, and an end-to-end accuracy spot-check in the pilot.

The mechanism as filed does not reach the stream it exists for

#4139 states its load mechanism as: "one shared gate reference file checks the config key and directs the orchestrator to Read the golfed variant. @-includes are untouched (they resolve statically)."

The last clause is where it fails. The top-level workflow files themselves are the @-includes. commands/msd/plan-phase.md reaches its workflow this way:

<execution_context>
@~/.claude/msd-core/workflows/plan-phase.md
@~/.claude/msd-core/references/ui-brand.md
</execution_context>

58 of 72 shipped commands/msd/*.md carry an @~/.claude/msd-core/workflows/<name>.md line in <execution_context>, and 58 of 72 skills/*/SKILL.md twins carry the same. Of the 14 that do not, exactly one — discuss-phase — reaches a workflow file another way: its <execution_context> says "Workflow files are loaded on-demand in the <process> section below — not upfront", and its <process> block picks between three workflow files by a config-get workflow.discuss_mode call. The other 13 (graphify, mempalace-capture, mempalace-recall, the six ns-* commands, plan-review-convergence, review-backlog, surface, workstreams) carry no top-level workflow file at all.

manager and pause-work are inside the 58, not outside it — both carry a plain eager @-include (commands/msd/manager.md:29, commands/msd/pause-work.md:24). Earlier research on this epic grouped them with discuss-phase as runtime-Read commands; that grouping is wrong, and it matters, because it would have made the deferred-load precedent look three times broader than it is. discuss-phase is the only precedent, and it is a single file.

@ is expanded by the host as static text substitution at load, before any project config exists in context. That is not this ADR's inference; it is stated by ADR-1610 Decision 4 ("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"), by #4139's own rejected-alternative 4, and by the triage addendum's claim-verification pass against the host's memory documentation.

So by the time a gate line inside plan-phase.md could be evaluated, all 98,290 bytes of plan-phase.md are already in context. Reading a smaller variant afterwards adds tokens. No gate reference file, however written, changes this — the constraint is upstream of every instruction the file contains.

Content Bytes Reachable by an in-content gate?
The 75 distinct top-level msd-core/workflows/*.md files named by an eager @ 1,517,683 no
workflows/<name>/{modes,steps,templates}/*.md (runtime Read) 358,482 yes
msd-core/templates/** (referenced by path) 273,589 yes
agents/*.md via the msd_run query agent-skills CLI seam 704,846 yes (code seam)

Measured at origin/next (708d9a0b82). The 58 command files name 75 distinct workflow files between them, because several @-include more than one.

The unreachable slice is the largest one — larger than the other three combined — and it is the headline of the feature.

The obvious fix, and why it is not the one taken

The obvious response is to convert those 58 <execution_context> blocks from an eager @-include to a config-gated runtime Read. That delivers the coverage. It also inverts the fail-safe direction: today a miss is impossible because the host substitutes the text; afterwards a Read that does not fire leaves the orchestrator holding a command name, an objective paragraph, and no procedure.

That shape has already been examined in this repo and rejected. ADR-3646 §Context rejected a prose-dispatched content-resolution step precisely because a missed dispatch and a legitimate fall-back are indistinguishable at the point of failure, so the executor proceeds on the wrong content while believing it authoritative. Its Decision 2 chose a real subprocess with a real exit code instead — "a real process exit code the calling loop cannot fail to observe the way it can fail to execute a prose instruction."

The state of ADR-3646's cited evidence has moved, and this ADR records the correction. ADR-3646 §Context cites #3647 as "filed the same day as #3646, still open" — accurate on 2026-08-27, stale now. #3647 was closed 2026-09-01 as a duplicate of #3606, on the strength of PR #3687, which fixed both call sites the report named: execute:wave:pre now does generic contribution dispatch, and execute:wave:post now dispatches every kind == "step" hook generically instead of filtering to kind == "gate". ADR-3646's own reasoning anticipated this and does not depend on it — it rejected sequencing behind #3647 explicitly, "because sequencing behind an open reliability issue with no committed fix date blocks #3646 indefinitely on someone else's timeline for no architectural gain" — so its Decision stands unchanged. Only its Context needs the footnote.

But #3647's closure does not retire the concern that matters here, and reading the closure as a clean bill of health would be a mistake. The triage diagnosis on that issue attributes the observed 1-of-4 rate to two co-present mechanisms, and only one of them was fixed:

"contribution-kind hooks … are folded as natural-language text into the executor agent's own prompt at spawn time, never iterated by the wave:post loop at all, and whether an LLM executor acts on an embedded prose instruction is exactly the kind of variance that produces a 1-of-3 hit rate. Both the deterministic code-level exclusion (matches #3606) and prose-instruction variance (a separate, architectural property) can be present in the same evidence at once."

PR #3687 removed the deterministic filter. The prose-instruction variance was named as a separate, architectural property and nothing has been shipped against it. It was never independently measured either — it is an explanation offered for the residual, not a rate. So the honest position is: prose dispatch is not guaranteed, the magnitude is unknown, and no evidence exists that would let this ADR treat it as negligible.

The premise that does not survive checking

The natural argument for accepting that risk is that the feature is opt-in: a user who never sets the key keeps today's static, host-guaranteed behavior, so the exposure is confined to people who chose the trade. That argument is false, and it is worth stating why, because it is the argument this ADR was expected to make.

A global install shares one ~/.claude/msd-core/ tree and one set of skill files across every project on the machine — #4139's own rejected-alternative 3 says so, and its acceptance criteria forbid install-time file selection outright. The shipped SKILL.md is therefore a single artifact serving all projects, and an @-include in it is expanded unconditionally. There is no way to write one file that eagerly includes the workflow for opted-out projects and does not for opted-in ones. Removing the @-include removes the guarantee for everyone, opted in or not; keeping it means opted-in projects save nothing on the stream this feature exists for.

That is a hard constraint, not a design preference. Any design that saves tokens on stream 1 must ensure no user receives the canonical body eagerly.

Decision

1. The name

workflow.compact_content, surfaced as compact content mode. Satisfies approval condition 1.

"Content" is this repo's own word for this corpus — CONTRIBUTING.md §"Editing shipped content", and the guard family is named the shipped-content guards. "Prompts" would be too narrow for a set that includes planning-artifact templates. Chosen over workflow.terse_content (describes the prose style rather than the mechanism) and workflow.lean_prompts (same narrowness problem, plus "lean" already carries an unrelated meaning in this repo's vocabulary).

2. What this feature is actually for

#4139 is pitched on per-invocation token cost. ADR-1610 has already discounted that argument in this repo's own record: "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."

This ADR adopts ADR-1610's load-bearing argument instead. The justification for compact content is finite attention, not price. A 98 KB instruction file occupying the context window before the first step runs is 98 KB of attention not spent on the developer's code, and that cost is paid whether or not the tokens were cheap to transport. Cost reduction is a real secondary effect and the benchmark in Phase 4 (#4404) will report it, labeled as what it is.

This matters beyond framing: it decides what a good split looks like. Optimizing for price rewards deleting words anywhere. Optimizing for attention rewards moving words out of the always-loaded window — which is what this ADR's mechanism does, and what ADR-1610 and the discuss-phase progressive-disclosure split (#717) already established as the sanctioned direction. The workflow-size budget test says so in its own comments: the correct response to a file at its cap is "lazy extraction, never a raise."

3. The load mechanism — the eager @-include stays exactly where it is

Decision: do not convert the 58 <execution_context> blocks. Change what sits behind them.

For every covered top-level workflow:

  • msd-core/workflows/<name>.md becomes the spine. Same path, same eager @-include, same host guarantee. It is complete enough to run the workflow correctly on its own.
  • The elaborations move to msd-core/workflows/<name>/detail/*.md — one or more parts, read at runtime. Decision 6 explains why "one or more" rather than one.
  • msd-core/references/compact-content-gate.md is the single shared gate. It states the config check and the resolution rule once; workflows reference it and never restate it.

Behavior:

workflow.compact_content Eagerly loaded Then reads Instruction set held
false (default) spine its detail/ parts complete — same content as today
true spine — the spine

No command file changes. No skill file changes. No @-include is removed, converted, or made conditional. #4139's claim that "@-includes are untouched" — untrue of the design it proposed — becomes literally true of this one.

The savings are real because the eagerly-included file got smaller. For plan-phase.md, today's 98,290 bytes become a spine plus detail parts whose sum is the same; an opted-in project loads only the spine. The reduction available here is larger than #4139's projected aggregate, because it comes from removing content from the eager window rather than from rewording it.

4. The fail-safe — the degradation direction is the whole point

A missed Read under this design leaves the orchestrator running on the spine: correct, terser, and identical to the state an opted-in project runs in deliberately. It never leaves it running on nothing.

This is the property that decides the design, and it is worth being precise about why it holds rather than asserting it:

  • The baseline instruction set arrives by host substitution, which cannot be missed. That is unchanged from today, for every user, opted in or not.
  • The runtime Read is only ever additive — it supplies elaboration on top of a complete baseline. It is never the delivery mechanism for the baseline itself.
  • Therefore the worst outcome of prose-dispatch variance is the opt-in behavior, arriving for a user who did not opt in. That is a quality regression bounded by a state the project already considers acceptable — the same state Phase 2's accuracy spot-check (#4402) exists to validate before any of this proliferates.

This is why this ADR does not need to resolve the open question about prose-dispatch reliability that ADR-3646 §Context raised and #3647's closure left standing. It routes around it: the unreliable mechanism is never load-bearing. Where a stream can be served by a code seam with a real exit code instead — token stream 2, the buildAgentSkillsBlock path (#4407) — it is, for exactly ADR-3646 Decision 2's reason, and this ADR adopts that precedent rather than restating it.

Two consequences to name honestly rather than bury:

(a) Opted-out projects do acquire one new failure mode. Their elaborations now arrive by a Read that could be missed, where today they arrive by substitution. #4139's user story 3 asks that opting out "cost me nothing", and this is a deviation from that. It is the minimum possible deviation: no design that delivers stream-1 savings can leave the canonical body eagerly loaded (see §Context), so the achievable maximum is exactly this — degradation bounded to the compact baseline. The alternative shape (convert the @-include) has the same new failure mode with an unbounded consequence. Recorded as a reconciled criterion in Decision 7 rather than silently accepted.

(b) The spine's correctness is now load-bearing for everyone, not just opt-ins. A compact variant is no longer "a cheaper alternative a few users choose" — it is the floor every user lands on if a Read is missed. This raises the authoring bar, and Decision 5's protected-content list and boundary-move declaration are what enforce it.

(c) "The spine still runs the workflow" is not a machine-decidable property, and this ADR does not pretend otherwise. Decision 5's checks verify completeness once at split time, then disjointness, registration and protected-content presence forever. None of them re-derives sufficiency. A later PR could move genuinely load-bearing procedural text — text that carries no protected-content sentinel because it is a step, not a guardrail — out of a spine and into a part, and every mechanical check would stay green while the floor quietly dropped. That is the honest residual, and it is the same class of problem the Feature Review priced in, relocated from "stale duplicate text" to "boundary placement" rather than eliminated.

What Decision 5 does about it is make the move declared instead of silent: a PR in which a spine loses non-trivial lines that reappear in its detail parts fails the guard unless it carries a boundary-move declaration naming the spine — the same enforcement philosophy ADR-3942 applies to emitted-drift, where the mechanism cannot judge intent so it demands the intent be stated. A declaration is not proof of sufficiency; it is the point at which a reviewer is guaranteed to be looking. Combined with the end-to-end spot-check obligation that rides on any spine-shrinking PR (#4402 establishes it, #4405 and #4406 inherit it), that is the strongest available answer, and it is authoring discipline with a forced checkpoint rather than a structural invariant. Claiming otherwise would be the more comfortable sentence and the false one.

Alternatives considered for the fail-safe, and why they lost:

  • A preflight that fails loud when the content was not loaded. Needs a signal the workflow's own dispatch can check, and the only honest one is a load receipt the orchestrator supplies — which the orchestrator can only supply if it ran the instruction whose omission is being detected. It detects some misses, not the ones that matter, and it adds machinery to every workflow. Rejected: strictly weaker than making the miss harmless.
  • Content delivered by a real subprocess (ADR-3646 Decision 2's shape), with a content digest verified on the next msd_run call. Genuinely converts a silent miss into a hard halt, and is the right answer for stream 2 where the payload is already served by a CLI seam. Rejected for stream 1 on two grounds: a 60 KB instruction body through a subprocess stdout is subject to harness output truncation in a way a Read is not, and the design still leaves a window in which the orchestrator holds no instructions. Halting loudly is better than proceeding wrongly; not needing to halt is better than both.
  • Keeping the @-include and adding a separate additive compact overlay (the text_mode overlay shape — the <progressive_disclosure> dispatch table at msd-core/workflows/discuss-phase.md:20-40, whose workflow.text_mode row at :30 is the config-gated case). Preserves every guarantee. Rejected: it saves nothing on stream 1 — it can only add to an eager window that is already fully paid — so it is Option A wearing Option B's clothes.
  • Converting the 58 <execution_context> blocks (the shape the epic brief anticipated). Rejected on the reasoning above: it is the only option whose failure mode is unbounded, and it is not needed to reach the coverage it was proposed to buy.

On the scope decision this replaces. The maintainer was asked to choose between reducing scope to the reachable streams and expanding it to convert the @-includes, and chose to expand. That decision is honored: stream 1 is covered in full, which is what expanding scope was chosen to buy. It is covered by a route that does not require the conversion, and therefore does not require accepting the risk the conversion carries. This ADR is not re-litigating the coverage question; it is delivering the chosen coverage at the safer option's risk level.

5. Partition, not duplication

A split moves text. It does not restate it. The spine and its detail parts are pieces of one document, not two documents.

This is the decision that answers the Feature Review's actual disqualifier. That review returned No-go as filed, and the reason was not the mechanism — "the gating mechanism is sound; the disqualifier is scope" — it was "permanent dual-maintenance burden (290+ files)" where "a missed re-golf silently serves stale instructions." #4139's approval overrode that verdict but did not dissolve the cost; it accepted it, and proposed a drift-parity CI check to contain it.

A partition dissolves it instead. There is exactly one copy of each sentence, so there is no stale twin that can exist. A canonical edit lands in whichever half owns that text, and no paired edit is owed. The forever-cost the review priced in — every future content PR carrying a paired re-compaction — does not accrue.

What replaces the drift-parity check (Phase 3, #4403):

  • Completeness, once, at split time — the union of the two halves, whitespace-normalized, contains every non-trivial line the canonical file carried at the parent commit. This is what makes a split reviewable; it runs on the PR that performs the split and never again.
  • Disjointness, ongoing — no non-trivial line appears in both halves. This is the invariant that keeps duplication from creeping back in later.
  • Registration, ongoing — a detail part with no spine, or a spine referencing a detail part that does not exist, fails and names the pair.
  • Protected content, ongoing — a spine that has shed a protected-content marker fails and names the marker.
  • Boundary moves are declared, ongoing — a PR in which a spine loses non-trivial lines that reappear in its detail parts fails unless it carries a boundary-move declaration naming the spine. This is the check that answers Decision 4(c): sufficiency cannot be computed, so the guard makes the moment it could be lost impossible to pass through unnoticed.

The protected-content list is #4139's own denylist, promoted from "content the compact variant must not weaken" to "content that may not leave the spine": negative instructions and guardrails, output-format contracts, few-shot examples the workflow's own steps depend on, security and prompt-injection language, and machine-parsed structural headings.

The marker is a literal sentinel, not a category judgment. A guard cannot decide whether a sentence is "security language" — prose category membership is exactly the kind of judgment that degrades silently under time pressure, and a check that depends on it is a check that does not exist. So protection is declared at authoring time by a greppable HTML comment in the repo's existing msd: comment namespace (the same namespace as the <!-- msd:loop-host --> header plan-phase.md already carries):

<!-- msd:protected -->
… one protected block …

<!-- msd:protected:start -->
… a protected region spanning several blocks …
<!-- msd:protected:end -->

The guard's rule is mechanical and has no discretion in it: a sentinel present in the canonical file at the parent commit must be present in the spine afterwards, and every line it covers must be in the spine. A split that moves a protected block into a detail part fails and names the sentinel and the line. The categories above are authoring guidance for where to place sentinels; they are never what the guard evaluates.

Marking is a one-time cost paid during each split, on the file being split. Phase 3 (#4403) owns the sentinel syntax, the guard, and the failing-first fixture that proves the guard can actually fail — per this repo's rule that a guard nobody has seen go red is not yet a guard. This is the security review's finding on this ADR, resolved here rather than carried: the original text specified a "protected-content marker" without saying what a marker was, which left the strongest check in the set resting on reviewer judgment.

Rewriting for terseness is permitted within a half and is never a way to move a sentence into both. Where a split cannot be made by moving text alone without breaking the spine's ability to run the workflow, the correct answer is a different split point, not a duplicated paragraph.

6. Where the content lives, and what it collides with

msd-core/workflows/<name>/detail/*.md — a sibling of the existing modes/, steps/ and templates/ subdirectories, under the workflow it belongs to.

Chosen over a parallel msd-core/workflows/compact/** tree because a detail part has no meaning apart from its spine, and a parallel tree is the shape that invites the duplication Decision 5 exists to prevent. Chosen over a home outside msd-core/workflows/ because the guards that sweep that directory should see this content.

<name> is never user- or project-supplied. It is the workflow stem the spine already occupies, which comes from the static set of shipped command files — the same set listWorkflowStems walks — and each spine names its own detail parts literally rather than composing a path from an argument. Nothing in $ARGUMENTS, .planning/config.json, or any tracker payload reaches this path. The config key selects whether the read happens; it never selects what is read. A project-local file does not shadow a shipped detail part either: the read resolves against the installed ~/.claude/msd-core/ tree the same way the spine's own @-include does, so an untrusted repo cannot substitute instruction content by planting a path. This is stated rather than left to inference because the seam is new and a future phase reaching for a computed path would be a traversal vector where today there is none.

Mapped against the guards that scan this tree, resolved in the phase that first creates a file there (#4403):

Guard Scope Effect
scripts/workflow-size.cjs listWorkflowStems (:53) top-level .md, non-recursive Detail files are outside it. Spines stay inside and get smaller — the direction the budget wants.
tests/workflow-size-budget.test.cjs listWorkflowFilesRecursive (:617) recursive (#3324 sub-guard) Detail files are scanned for bare @-include-in-prompt patterns. An @ line inside a runtime-read file is inert text, so it must not appear there — the guard already enforces this and is correct to.
tests/helpers/planning-add-guard.cjs SCAN_ROOTS fully recursive over msd-core/workflows Detail files are swept by commit-docs-bypass. Expected; no exemption sought.
tests/emitted-attribution.test.cjs diffs installer output across 19 real installer spawns New installable content enters scope automatically. Byte deltas are deliberate and carry ADR-3942 acknowledgement trailers.
tests/commit-files-pathspec.test.cjs every .md under msd-core/workflows/ (and five other roots), for commit-seam invocations without a --files scope (CONTRIBUTING.md:1164-1170) Detail parts are in scope. A commit invocation that moves out of a spine into a part must keep its --files scope. Earlier research on this epic recorded this guard as "not a content-tree scanner; irrelevant here" — that is wrong, and an unscoped commit reaching the runtime is #2269, a CRITICAL-blast-radius defect.
scripts/lint-response-language-coverage.cjs top-level, with <workflow>/<modes|steps|templates>/<name>.md inheriting parent coverage detail/ is a fourth subdirectory kind the recognizer does not know. #4403 extends the recognizer; it does not carve an exemption.
NEW_FILE_CAP (tests/helpers/emitted-diff.cjs:96, checked at :403) 32,768 B, applied to every file absent from the baseline and present now Hard. Not ack-able, and not exemptible by XL/LARGE tiering. Detail content is therefore split into parts, each under the cap.
tier hard caps (tests/workflow-size-budget.test.cjs:102-104) XL_CAP 98,304 · LARGE_CAP 61,440 · DEFAULT_CAP 40,960 Apply to spines, which are existing files keeping their tier. Spines only get smaller.

That NEW_FILE_CAP row forces a layout decision, and the correct reading of it is not the obvious one. Prior research on this epic recorded the cap as living in workflow-size-budget.test.cjs and as waivable by "explicit tiering in the same PR". Both are stale: they describe the pre-#2724 test- file version. #2724 (ADR-2719 Phase 4) deleted the committed per-file baseline that version keyed off, and the cap was revived in tests/helpers/emitted-diff.cjs, where its own doc comment states the narrowing plainly — it is "a HARD cap, not ack-able … Not exempted by explicit XL/LARGE tiering the way the original test-file version was — this module is intentionally pure and has no access to that classification … so a legitimately large NEW file must be split via the same lazy-extraction pattern the tier caps already require."

So a single detail.md holding the ~63 KB that comes out of plan-phase.md is not merely friction — it is blocked outright, with no exemption path. The layout is therefore:

msd-core/workflows/<name>.md            ← spine, existing path, existing tier, eagerly @-included
msd-core/workflows/<name>/detail/*.md   ← one or more parts, each < 32,768 B, read at runtime

The spine names the parts it defers to, in the same dispatch-table shape discuss-phase.md's <progressive_disclosure> block already uses for its mode overlays. This is a better outcome than one large detail file, not a workaround for the cap: parts are individually skippable, so a workflow can defer only the sections a given invocation will not reach, and the cap is doing exactly the job ADR-1610 designed it to do.

The @-include inertness in row 2 has a design consequence worth stating plainly: a canonical workflow's own <required_reading> @ lines (e.g. plan-phase.md's five reference imports) are expanded today because the file is @-included. They stay in the spine and keep working. Moving one into a detail part would silently turn a working import into dead text, which is exactly the class of failure the #3324 sub-guard catches.

7. Acceptance criteria — the guard ledger

CI.GATE.acceptance-criteria-required treats an unmet must-have as a failed deployment, so the criteria are reconciled here explicitly rather than reinterpreted quietly at ship time. This section was written as a forward-looking reconciliation when only Phase 0 existed; #4408 (the closing phase) converts it into a ledger against #4139's own 12 acceptance-criteria checkboxes, quoted verbatim, each with the evidence that closed it. "Satisfied" means literally true; "Reconciled" means the criterion's purpose was met by a structurally different mechanism than the one the criterion's wording assumed, argued below.

# Criterion (verbatim from #4139) Verdict Evidence
1 workflow.prompt_golf is registered in the config schema; config-set/config-get persist and read it, invalid value rejected Satisfied — key renamed to workflow.compact_content (Decision 1) #4401 → PR #4441
2 /msd-new-project asks a Prompt Golf question and writes the answer Satisfied #4408 → this PR, msd-core/workflows/new-project.md Round 2
3 /msd-settings and /msd-config can toggle the key on an already-initialized project Satisfied #4408 → this PR, msd-core/workflows/settings.md (/msd-config routes here with no flag)
4 With golf on, workflows load golfed variants via a single shared gate reference file; gate logic exists in exactly one file Satisfied #4402 → PR #4471, msd-core/references/compact-content-gate.md
5 Every shipped workflow instruction file and planning-artifact template has a golfed variant registered with the parity check — no unpaired canonical file remains Reconciled (#1 below) #4402/#4403/#4405/#4406
6 msd_run query agent-skills <agent> returns the golfed payload for every agent when on, canonical when off Reconciled (#2 below) #4407 → PR #4553
7 Subagent prompts emitted by orchestrators follow golfed spawn patterns when golf is on Satisfied — carried inside the workflow spine/detail splits, no separate mechanism needed #4405 → PR #4536
8 Behavior identical under global and local installs — per-project config decides, never install-time file selection Satisfied — structural: the config key is read at runtime from .planning/config.json; no phase added install-time file selection Design invariant, unchanged across all 8 phases
9 A committed compression-rules document defines a protected-content denylist; no golfed variant weakens protected content Satisfied #4403 → PR #4497, Decision 5 above
10 Editing a canonical file without updating its golfed variant fails the drift-parity check, naming the stale pair Reconciled (#3 below) #4403 → PR #4497
11 The benchmark runs fully offline, reports per-file/aggregate reduction as a labeled proxy-tokenizer delta, reproduces its committed baseline deterministically, is reporting-only Satisfied #4404 → PR #4502
12 The full existing test suite passes with golf enabled AND with golf disabled Satisfied Every phase's msd-test run covers the key off (default, unset); tests/agent-skills.test.cjs's stream-2 fixtures (#4407) and Phase 2's live end-to-end dogfooding (#4402 — a real phase planned twice against this repo, once with the key false, once true) cover it on
13 All golfed and gate-bearing shipped content passes the existing shipped-content guards: size budgets, emitted-attribution, commit-files-pathspec, commit-docs-bypass Satisfied — ongoing, every phase Recorded per-phase in each PR's own review artifact; no phase shipped with a red shipped-content guard

Reconciled, with the reasoning (numbered to match the ledger's "Reconciled (#N below)" cells):

  1. (criterion 5) "No unpaired canonical file remains" reads as "every file has a golfed twin." What shipped instead, deliberately: streams 1/1b/4 use a partition (spine + detail, or a variant swap), not a duplicate-and-maintain pair, and the eagerly-@-included corpus was individually reviewed — some files split (#4402/#4405/#4406), others were recorded as not worth splitting (docs/PARTITION-RULES.md) because their size is safety-critical orchestration logic rather than deferrable narrative, or because a split's fixed overhead would exceed the savings. "No unpaired canonical file" is true of every file this epic covers; it was never true, nor intended to be, of files the epic explicitly declined to cover with a stated reason — which is the criterion's own spirit (a reviewed decision, not a silent gap).
  2. (criterion 6) "For every agent" reads as literally all 35. #4407 shipped 29 — six (msd-debugger, msd-executor, msd-phase-researcher, msd-plan-checker, msd-planner, msd-verifier) exceed the hard, non-ackable NEW_FILE_CAP even after aggressive compaction, a structural constraint the seam's single-file read has no mechanism to route around without building a second spine/detail-style partition mechanism scoped for agents — out of proportion to #4407's own "the code delta is small" framing. The criterion's purpose — never fail or serve nothing — holds for all 35: the #2454 fallback discloses the substitution inside the payload itself for the six uncovered agents, exactly the behavior criterion 6 and #4139's Alternative-5 rejection ("golfing in place forfeits the side-by-side comparison") both protect against losing.
  3. (criterion 10) Under Decision 5's partition shape there is no variant to go stale — a canonical edit and its elaboration are pieces of ONE document, not two. The criterion's purpose (a canonical edit cannot silently leave a paired file behind) is met structurally rather than by a drift check catching it after the fact. What #4403 built instead — disjointness (no line duplicated) plus registration (no orphaned or dangling part) — is a stronger invariant than "the parity check fails," and #4403's own failing-first fixtures prove each check can actually fail before either was trusted.

One item this ledger overturns rather than reconciles: user story 3 ("with golf off, MSD behaves exactly as it does today") is not fully achievable by any design that delivers stream-1 savings — Decision 4(a) states the residual plainly: an opted-out project's elaborations arrive by a runtime Read that could be missed, and a miss yields the compact behavior rather than a crash. This was accepted knowingly at Phase 0 and validated in #4402's live dogfooding rather than fixed, because the alternative (converting the @-includes) has an unbounded failure mode instead of a bounded one (see §Context). It is recorded here, not buried, as the one criterion whose letter was traded for its spirit.

Consequences

  • Every covered workflow becomes two files. The corpus grows in file count while shrinking in eagerly-loaded bytes, and docs/INVENTORY.md plus the manifest regenerate on every split phase.
  • The maintenance economics the Feature Review priced as disqualifying do not materialize in the form the review priced them: a partition has no twin, so no future content PR owes a paired edit. What replaces that cost is smaller but real, and Decision 4(c) names it — the split point itself can drift, and only a declaration plus a reviewer stands between a spine and a slow erosion of what it can run on its own. This ADR should be re-read if a future phase finds itself duplicating rather than moving text, or routinely waving through boundary-move declarations; either is the signal that the review's cost model has come back in a new shape.
  • Spines get smaller, which moves several files down a size tier. Tier membership in tests/workflow-size-budget.test.cjs is adjusted downward as splits land, never held at the old tier for headroom.
  • ADR-3646's Context acquires a stale citation. It is corrected here rather than by editing that ADR: #3647 is closed, its Decision is unaffected, and its reasoning explicitly disclaimed any dependence on #3647's state.
  • The residual prose-dispatch reliability question raised by #3647's closure thread stays open in this repo. This ADR does not close it and does not need it closed. Any future design that makes a runtime Read load-bearing for a baseline instruction set will need it answered; this one does not, and that is the reason it was chosen.
  • gpt-tokenizer enters devDependencies at 27.2 MB unpacked. Nothing ships to users; every npm ci, CI included, pays the install. It is a single-maintainer package, so #4404 pins an exact version rather than a range and relies on the lockfile's integrity hash — a benchmark is not worth a floating dependency, and a reporting-only script has no upgrade urgency that would justify one.

Rejected alternatives

  • Convert the 58 <execution_context> @-includes to config-gated runtime Reads. Delivers the same coverage this ADR delivers. Rejected: it removes the host guarantee for every user including those who never opt in (§Context), and its failure mode is "runs with no instructions" where this ADR's is "runs with fewer". ADR-3646 §Context rejected the same shape for a structurally identical reason.
  • Reduce scope to the lazily-reachable streams. Buildable immediately, no new risk. Rejected: it drops the majority of the value and contradicts an approved acceptance criterion, and it is unnecessary — the coverage is reachable without the risk.
  • Install-time selection of compact files. Rejected by #4139 (alternative 3) and by its acceptance criteria: a global install shares one tree across every project on the machine.
  • Runtime LLM-based compression, and character-count optimization. Rejected by #4139 (alternatives 1 and 2); nothing found here changes either rejection.
  • Compacting canonical content in place for everyone. Rejected by #4139 (alternative 5). Worth distinguishing from this ADR's decision, because they can look similar: a split preserves every word for opted-out projects and changes only when it loads. In-place compaction deletes words for everyone and forfeits the side-by-side comparison that makes the trade evaluable.

Phase plan

Phase Issue Delivers Depends on PR
0 #4400 this ADR — —
1 #4401 workflow.compact_content end to end 0 #4441
2 #4402 shared gate + pilot split + accuracy spot-check 1 #4471
3 #4403 partition rules + the five checks 2 #4497
4 #4404 offline benchmark + committed baseline 3 #4502
5 #4405 stream 1 corpus coverage (carries stream 3) 3, 4 #4536
6 #4406 stream 1b subdirectories + stream 4 templates 5 #4540
7 #4407 stream 2 agent-skill payloads via the CLI seam 6 #4553
8 #4408 user surfaces, docs, guard ledger — closes #4139 7 this PR

Guards land before content proliferates (Phases 3 and 4 precede Phase 5), satisfying approval condition 2. The pilot's end-to-end accuracy spot-check is Phase 2's, satisfying condition 3.

Open questions for the implementation phases — resolved at close

  • Whether discuss-phase — the one command that already reaches its workflow by runtime Read — should be brought onto the spine shape too. It is the sole existing instance of the substitutive load this ADR declines to generalize, which means it already carries the failure mode this ADR avoids, mitigated only by prose: commands/msd/discuss-phase.md:64 reads "MANDATORY: Read the appropriate workflow file BEFORE taking any action … Do not improvise from the summary." Giving it a spine would remove that residual entirely, and it is the one place in the tree where this ADR's mechanism would be a strict safety improvement rather than a token trade. Scoped to Phase 5 (#4405) to decide with the rest of stream 1 in view. Resolved: not converted. msd-core/workflows/discuss-phase/ carries no detail/ subdirectory as of #4408; the prose-only mitigation quoted above is unchanged. This was not revisited with an explicit for/against argument in #4405 — recorded here as a genuine gap in the epic's own closure, not a reasoned decline. A future phase wanting the safety improvement this question identifies starts from a clean slate, not from an argued rejection.
  • Whether the disjointness check should compare normalized sentences rather than normalized lines. Lines are cheaper and catch copy-paste; sentences catch reflowing. Decided in #4403 against real splits rather than in the abstract here. Resolved: lines. tests/helpers/compact-content-split.cjs's normalizeNonTrivialLines is the shipped comparison unit — confirmed against the real implementation, not asserted from the plan.