ADR-148: framework surface ships operator content; dev harness in project scope¶
Context¶
The projected framework surface — the roots the reconciler symlinks into every
operator's ~/.claude (skills/, agents/, commands/, hooks/ways/, ADR-142) —
is the product. Everything in it reaches every install. That surface must therefore
carry operator content only: guidance and tools useful to someone who adopted
agent-ways to steer Claude in their project.
It had leaked. Two artifacts — the project-pulse skill and the meta/project-health
way — are maintainer tools for developing agent-ways itself: track Claude Code
upstream releases against agent-ways' commits, reconcile agent-ways' own ADRs against
its shipped code. An operator has no use for either, yet both sat in projected roots
(skills/, hooks/ways/), so they symlinked into every install. The backing tool,
scripts/project-pulse, correctly lived in non-projected scripts/ — but the skill
and way that fronted it did not.
The tell was in the old gate. project-health fired on when: project: ~/.claude.
Pre-1.0 that meant "only when developing agent-ways," because ~/.claude was
the repo then. 1.0 made ~/.claude a projection (ADR-142), so the gate pointed at
the wrong place and the way went dead — but its intent was always dev-only. It was
never operator content; it was dev harness with a dev gate the projection model broke.
A second pre-1.0 fossil blocked the clean fix. The repo's .gitignore used a
deny-all-then-allowlist posture (*, then ! each tracked path) — necessary when
the repo was an in-place overlay on a live Claude Code install and had to avoid
committing the install's runtime state. That posture made the repo's own project
scope (<checkout>/.claude/) untrackable, so dev harness had nowhere tracked-and-
non-projected to live.
Decision¶
The projected framework surface ships operator content only. agent-ways' own
dev-harness artifacts live in the repo's project scope — .claude/ways/ and
.claude/skills/ — tracked and non-projected.
This is the placement test for any new skill/way/command: would an operator who adopted agent-ways use this, or is it for developing agent-ways itself? Operator content goes in the projected roots; agent-ways' own tooling goes in the repo's project scope, exactly where any other project's dev guidance would live (ADR-143's project tier). agent-ways thereby dogfoods its own three-root design — these are its first project-scoped artifacts.
Concretely:
- skills/project-pulse/ → .claude/skills/project-pulse/
- hooks/ways/meta/project-health/ → .claude/ways/meta/project-health/
- scripts/project-pulse stays put (already non-projected; unchanged).
- The project-health way drops its when: project: gate entirely — a project-
scoped way fires only in its own project by construction, so no gate (and no new
matcher condition) is needed.
Enabling change: the .gitignore converts from the pre-1.0 deny-all overlay to a
conventional "track by default, ignore build output and runtime junk" list, which is
correct now that the repo is a normal application (ADR-142) rather than an overlay on
a CC install. .claude/ runtime state stays ignored; .claude/ways/ and
.claude/skills/ are un-ignored so the dev harness is tracked and shared.
Consequences¶
Positive¶
- Operator installs stop carrying maintainer tooling they can't use — the product surface is honestly operator-only.
- agent-ways dogfoods ADR-143's project tier, which is a working proof of the three-root design (and directly relevant to the adopter story).
- A durable, one-question placement rule for contributors, so this class of leak doesn't recur.
- The broken
project: ~/.claudegate disappears rather than being re-plumbed — and with it the need for a newwhen: git_remote:matcher condition that existed only to re-gate this one way.
Negative / caveats¶
- A project-scoped semantic way (like
project-health) only fires once the project corpus includes project-local ways; in the dev checkout that may need aways corpusregen. The skill is discovered natively regardless, so the capability the dev actually reaches for (/project-pulse) works immediately. - The conventional
.gitignoreis "risky by default" (junk creeps in unless ignored) where the deny-all was "safe by default." Mitigated by an explicit ignore list for every build-output and runtime-junk category, verified against the tracked baseline.
Neutral¶
scripts/project-pulseis unchanged — the capability was never the problem, only the surface its skill/way sat on.
Alternatives Considered¶
- Delete the skill and way outright. Rejected: the capability is genuinely useful for developing agent-ways (tracking Claude Code changes, ADR reconciliation). The problem was placement, not existence — relocate, don't destroy.
- Generalize
project-pulseinto an adopter-facing tool (parameterize the upstream repo so any project can track its own upstream). Rejected as the framing here: it's a dev harness, not a half-built product feature. Generalizing it would be dressing up a maintenance tool as something operators want. If a genuine adopter-facing "project pulse" is ever wanted, that's its own decision with its own design — not a reason to keep this one in the shipped surface. - Re-gate the way with a new
when: git_remote:matcher condition. Rejected: building a matcher feature to keep one dev way firing in the projected surface is backwards. Project scope is the gate; the feature isn't needed. - Add targeted
.gitignorenegations for.claude/ways/.claude/skillsonly, keeping the deny-all. Rejected: it preserves the pre-1.0 fossil and adds more allowlist cruft. Converting to conventional fixes the root cause once.