feat(analyze-before-plan): read-before bookend — scan memory (+code) before planning

Shared include lib/analyze-before-plan.md (two-pass on '## <PREFIX>-' headings, disposition-not-reading invariant, guarded no-op). Wired into the dev flows: ship-feature STEP 0d (analyzer code+memory, INPUT INJECTION into brainstorm/plan + STEP 3 reconciliation gate), bugfix STEP 2.5 (blockers-first), feat STEP 0.6 (decisions-first, MINI-PLAN names in-force or states none), hotfix opt-in. analyzer gains a RELATED MEMORY output section pointing at the include (DRY). init-project / onboard no-op by construction (guarded scan on absent/empty registries).

Mirror of the coupled-capitalize write-after (BDR-034): read-before / write-after bookend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho5EQCFTSvYamuRtVZpp2d
This commit is contained in:
Bastien Chanot 2026-06-26 23:46:41 +02:00
parent 07e846f0a6
commit 67c6a8165d
6 changed files with 228 additions and 2 deletions

View File

@ -64,6 +64,12 @@ RISKS:
OPEN QUESTIONS:
- <ambiguity to clarify>
RELATED MEMORY:
- IN FORCE: <id><title><how it constrains this work> [status] (detail each)
- ALREADY SEEN: <BLK-id><title> [status] (known cause/fix — don't re-derive)
- NON-BINDING: <c> surfaced, none binding — <bare id refs> (superseded / N-A — counted)
- SELECTION: scanned <N> headings — surfaced <K> = in-force <a> + seen <b> + non-binding <c>
```
Surface discovered patterns and conventions in the analysis output
@ -73,6 +79,26 @@ happens in the main thread via the gated capitalize flow.
---
## RELATED MEMORY (read-before)
When `.claude/memory/` exists and holds entries, run the memory-relevance scan per
`$HOME/.claude/lib/analyze-before-plan.md` (PASS 1: grep `## <PREFIX>-` headings →
select on titles → PASS 2: full-read only the selected bodies) and populate the
RELATED MEMORY section of OUTPUT.
The contract is DISPOSITION, not retrieval: every surfaced ID gets a verdict —
in-force / already-seen / non-binding. Detail the binding ones (IN FORCE, ALREADY SEEN —
they constrain the work); COUNT the non-binding (superseded / N-A) as one line with bare
refs — a per-entry paragraph on a non-binding match dilutes the in-force ones that bite.
A surfaced ID left undisposed is the gap this closes. You judge bearing; the main-thread
plan decides what to DO.
Read-only here too: reading registries is within Read/Grep; the "Do not modify files" rule
still forbids any write — Index backfill or new entries are never your job. Empty or absent
registries → omit the section (no-op).
---
## EDGE CASES
| Situation | Action |

View File

@ -60,6 +60,18 @@ Trace the bug from symptom to root cause:
# grep for the same pattern to assess blast radius
```
## STEP 2.5 — MEMORY READ-BEFORE (blockers-first)
Run the scan per `$HOME/.claude/lib/analyze-before-plan.md`, blockers-weighted: a resolved
BLK may already name THIS exact root cause; an in-force BDR may constrain the fix. Emit
RELATED MEMORY. Consumption is NATURAL — the agent emitting this IS the one writing STEP 3's
diagnosis (reader = planner, no external skill to inject into).
TEETH: STEP 3's DIAGNOSIS must name any binding prior (`PRIOR: BLK-xxx — known cause/fix`,
or `honors BDR-xxx`) OR the RELATED MEMORY line states none bears. Reading blockers then
diagnosing without naming a match is the read-then-ignore failure this prevents.
`.claude/memory/` absent → guarded no-op, proceed.
## STEP 3 — HYPOTHESIZE + PLAN
Present findings before fixing:

View File

@ -58,6 +58,13 @@ Follow `$HOME/.claude/lib/design-gate.md`:
tell the user to run `/profile design` before proceeding.
- If no signals → skip (zero overhead).
## STEP 0.6 — MEMORY READ-BEFORE (decisions-first)
Run the scan per `$HOME/.claude/lib/analyze-before-plan.md`, decisions-weighted: a BDR may
already constrain or forbid the approach; an LRN may name a gotcha to apply. Emit RELATED
MEMORY; feed STEP 1 MINI-PLAN. Inline consumption — reader = planner, no injection.
`.claude/memory/` absent → guarded no-op (zero overhead on a memory-less repo).
## STEP 1 — MINI-PLAN
Quick mental model, not a formal plan document:
@ -66,6 +73,9 @@ Quick mental model, not a formal plan document:
2. Describe the approach in 2-5 bullet points.
3. Note any edge cases to handle.
4. If tests exist for the area, note which tests to add/update.
5. Disposition (from STEP 0.6): name each in-force BDR/LRN this plan honors
(`honors BDR-xxx by …`), or state `no in-force decision constrains this feature`.
A plan with neither = read-then-ignore; the disposition must surface as a trace.
Print the plan as a compact checklist:
```

View File

@ -31,6 +31,14 @@ git log --oneline -3
"This looks deeper than a hotfix. Load `$HOME/.claude/agents/bugfixer.md`
and run the BUGFIXER agent on this target."
OPTIONAL — memory check (exempt by default; hotfix = obvious fix, mirror of its capitalize
skip). For a RECURRING or urgent bug only, a quick blockers-only glance may save time:
[ -d .claude/memory ] && grep -nE '^## BLK-' .claude/memory/blockers.md # "déjà vu ?"
If a prior BLK names this bug, jump to its solution. Not mandatory; no RELATED MEMORY
disposition required at hotfix weight.
## STEP 1.5 — DESIGN GATE
Follow `$HOME/.claude/lib/design-gate.md`:

123
lib/analyze-before-plan.md Normal file
View File

@ -0,0 +1,123 @@
# ANALYZE-BEFORE-PLAN — couple the memory read to the dev flow
Inline snippet. Include at the START of a dev flow, BEFORE the plan forms. This is the
HEAD of analyze; `capitalize-commit.md` is the TAIL. Read-before / write-after — the two
ends of one bookend: a flow consults the decisions / blockers / learnings it will later add to.
It does NOT decide the plan. It hands the planner a DISPOSED list of prior entries that
bear on the work, so the plan cannot form blind to a decision already in force or a
blocker already solved.
## WHEN TO RUN
- Inline flow (feat / bugfix): main thread, AFTER the related code is read, BEFORE the
mini-plan / diagnosis. PASS 2 is bounded-tiny (selection narrows it); no subagent —
preserves stay-light.
- Orchestrator with a code-analysis step (ship-feature / init-project): run it INSIDE the
analyzer subagent. Its fresh context reads code + memory with zero redundancy against
the main thread; only its compact digest returns. The analyzer's RELATED MEMORY output
section IS this snippet's OUTPUT.
- hotfix: not wired by default (mirror of its capitalize skip). Available opt-in for a
blockers-only quick check ("urgent bug déjà vu ?").
## DO
1. PRECONDITION — NO-OP unless `.claude/memory/` exists AND holds at least one registry
file. TESTED reality: a bare `grep … .claude/memory/*.md` on an ABSENT dir (or a dir
with no `.md`) does NOT no-op — the unmatched glob is passed literally and grep ERRORS
(`No such file or directory`, exit 2). So the GUARD makes "absent → no-op", never the
grep. This is init-project's STEP 2 reality: the registries are created at STEP 5, so at
analyze time they are ABSENT — the guard must fire on absent, not merely on empty.
2. PASS 1 — list every entry, drift-proof, GUARDED:
[ -d .claude/memory ] && ls .claude/memory/*.md >/dev/null 2>&1 \
&& grep -nE '^## (BDR|LRN|BLK|EVAL)-[0-9]+' .claude/memory/*.md
One `ID — title` line per entry, for 100% of entries by construction (an entry IS its
heading). A present-but-template-empty registry (header only) → grep exits 1 (clean
no-match) → no-op naturally; only the ABSENT / no-file case needed the guard. The REGEX
is the filter, not the file list: the glob also reads journal.md (date-keyed
`## YYYY-MM-DD`) and any non-entry file, which contribute zero matches — no need to
exclude them. Do NOT read the `## Index` table: it drifts (entries land in the body, the
manual Index update lapses — measured 32-40% missing on a mature repo); headings cannot.
3. SELECT — IDs whose title bears on $REQUEST. Judge on titles (one dense line each), not
on bodies. Over-inclusion is SAFE: an entry that proves non-binding costs one bare ref
in OUTPUT, not a paragraph (see DISPOSITION) — so err toward including on a dense cluster.
4. PASS 2 — full-read ONLY the selected bodies (heading to next `##`) to recover
status / why / solution / alternatives. Unconditional for the selected set — see THE
INVARIANT for why there is no "skip if already in context" branch.
5. DISPOSE — emit RELATED MEMORY (OUTPUT). Every surfaced ID gets a verdict.
## OUTPUT — RELATED MEMORY (disposition, not a dump)
RELATED MEMORY (read-before):
IN FORCE — must constrain this work (detail each — they bite):
- BDR-026 — <title><how it constrains> [accepted]
- LRN-050 — <title><how it applies>
ALREADY SEEN — known cause/fix, don't re-derive (detail each):
- BLK-009 — <title> [upstream]
NON-BINDING — superseded / N-A, COUNTED not detailed:
- <c> surfaced, none binding — BDR-013, LRN-022, … (bare refs, one line)
SELECTION: scanned <N> headings / 4 registries —
surfaced <K> = in-force <a> + seen <b> + non-binding <c>.
- Disposition rule: DETAIL what binds (IN FORCE, ALREADY SEEN); COUNT what doesn't
(NON-BINDING) as one line + bare refs. The collective verdict still disposes each
non-binding ID — its bare ref under "none binding" IS its disposition — but a per-entry
paragraph on a non-binding match dilutes the in-force ones that bite. On a dense cluster
(K up to ~14) this is what keeps the 3 that matter from drowning under 11 that don't.
- Compact even for binding ones: ID + title + one-clause bearing. Bodies were read to
JUDGE; only the disposition persists — never paste bodies into the plan.
- a + b + c = K: every surfaced ID is accounted for. An unaccounted surfaced ID is the gap
this prevents.
- Nothing bears → `RELATED MEMORY: none of <N> entries bears on this task` (still proves
PASS 1 ran — LRN-048).
## THE INVARIANT — disposition, not reading
The guarantee is NOT "the agent read the memory" (an act, unverifiable after the fact).
It is "the plan disposed of every relevant prior entry" (a list, verifiable in OUTPUT).
LRN-048 one step further: the teeth are "did it STATE a verdict on each surfaced ID?",
not "did it look?".
So there is no "skip PASS 2 if already in context" branch. "Already in context" has no
deterministic oracle: self-judgment is the rejected behavioral guard (LRN-046); a session
marker records "was read", not "still present", so it false-skips after a compaction (and
is the marker cost BDR-033 priced); the agent cannot grep its own window. PASS 2 reads the
selected set unconditionally — cheap by construction — and the invariant bites on the
disposition, which holds whether a body was freshly read or recalled.
A decision WRITTEN earlier in the same conversation (ship-feature posts BDR-035, then a
bugfix runs) MUST still be surfaced and disposed as in-force: content sitting in context
is not the current flow having TREATED it as a constraint. Re-surfacing is the feature.
## HARD RULE — read-only
Touches nothing. No write, no Index update, no memory mutation. Symmetric to
capitalize-commit's surgical scope (there: stage ONLY memory; here: stage NOTHING). Index
backfill, if ever wanted, is `/prune-memory` passe D — never this snippet.
## ORDERING (orchestrators only)
`superpowers:brainstorming` / `writing-plans` are external skills — we cannot make them
read our registries. So this runs BEFORE them, pre-loading the disposition into the plan
they form. Mirror of capitalize-commit running BEFORE finishing-a-development-branch: there
the memory commit must precede integration; here the memory read must precede planning.
## NO-OP / IDEMPOTENT
Empty or absent registries → silent no-op (greenfield init-project; onboard, which CREATES
memory and has none prior). Pure read → safe to run twice; naturally idempotent.
## WHAT THIS DOES NOT DO
- Does NOT read the related CODE — the flow does that (feat STEP 0, bugfix STEP 2). Sole
exception: ship-feature, where the analyzer subagent this runs in reads code too (Gap A).
- Does NOT decide the plan — it primes it with a disposed list of constraints.
- Does NOT write or mutate memory — read-only; capitalize (write-after) is the other bookend.
- Does NOT depend on the `## Index` table — keys off `## <PREFIX>-` headings (drift-immune).
- Does NOT skip PASS 2 on an "already in context" guess — no oracle for it; the read is cheap.

View File

@ -60,19 +60,64 @@ During implementation (STEP 4), when making decisions about fast-lib APIs:
- Read the relevant `.ctx7-cache/<lib>.md` file before writing code.
- This avoids repeated ctx7 calls and keeps docs available without context cost.
## STEP 0d — ANALYZE BEFORE PLAN (code + memory, read-before)
Dispatch the analyzer subagent (fresh context) on the request — it produces the
read-before digest the plan must not form without:
Agent(subagent_type="analyzer", description="ship-feature — read-before", prompt="""
Read-only analysis to PRIME a feature plan (NOT debug). REQUEST: <$ARGUMENTS>.
1. CODE — locate the zone the feature touches: grep/glob the request's nouns /
identifiers across the tree → candidate files (PASS 1), read them (PASS 2).
>50 candidates → scoped sweep per your EDGE CASES, list zones, don't read all.
Too vague to locate → report ambiguous zones, do NOT block.
2. MEMORY — run the scan per $HOME/.claude/lib/analyze-before-plan.md, emit
RELATED MEMORY (disposed).
Output your standard ANALYSIS + RELATED MEMORY. No solutions.""")
The returned digest (ANALYSIS + RELATED MEMORY) stays in the orchestrator's context — it
is FED to STEP 1 and STEP 2 and reconciled at STEP 3. Degradation: request too vague →
analyzer flags ambiguous zones, does not block (STEP 1 refines). `.claude/memory/` empty or
absent → analyzer omits RELATED MEMORY (no-op); the step still returns the code ANALYSIS.
Additive — distinct from STEP 5 ANALYZE (post-impl regression) and STEP 4b DEBUG.
## STEP 1 — BRAINSTORM
Invoke `superpowers:brainstorming`. Refine request into validated design via Socratic questioning. Don't proceed until design approved.
Invoke `superpowers:brainstorming` — but FEED it the STEP 0d digest as binding context,
not the raw request alone:
"Feature request: <$ARGUMENTS>.
In-force constraints (must hold): <only the IN-FORCE + ALREADY-SEEN items from 0d's
RELATED MEMORY, detailed>.
Existing code that bears: <ANALYSIS CONTEXT / KEY COMPONENTS from 0d>.
Brainstorm WITHIN these — don't re-explore a direction an in-force BDR rejects or a
BLK already closed."
Inject ONLY what constrains: the NON-BINDING count does NOT enter the brainstorm input
(the injection inherits the OUTPUT filter — detail what binds, drop what doesn't).
Consumption = INPUT INJECTION (we can't modify the external skill; we control its input).
Refine request into validated design via Socratic questioning. Don't proceed until design approved.
## STEP 2 — PLAN
Invoke `superpowers:writing-plans`. Break design into tasks (2-5 min each). Each task: exact file paths, full code, verification steps.
Invoke `superpowers:writing-plans` with the validated design AND the 0d digest: every task
must be consistent with the in-force constraints; where a task implements or affects one,
note the ID inline. Break design into tasks (2-5 min each). Each task: exact file paths, full code, verification steps.
## STEP 3 — VALIDATION GATE ★ MANDATORY STOP
```
SHIP FEATURE — VALIDATION GATE
FEATURE: <n> | TASKS: <count>
<numbered task list>
RELATED MEMORY — disposition CLAIMED by this plan (review each):
- BDR-026 [in force] — plan honors it by: <how>
- LRN-050 [in force] — plan applies it by: <how>
- BLK-009 [already seen] — <how avoided / why N-A>
Review the claims above — flag any item the plan does NOT actually honor.
Approve and execute? (yes / request changes)
```
This block EXPOSES each in-force item with the plan's CLAIMED disposition, for human
review — it does NOT auto-detect conflicts. An agent blind to a conflict won't list it;
forcing a per-item claim is what gives the reviewer the surface to catch it. A display,
never a guarantee (same discipline as the memory-commit `✅<hash>`: show what's true, never
assert a check not performed). No RELATED MEMORY from 0d → omit the block.
Changes → back to STEP 2. Approved → continue.
## STEP 4 — IMPLEMENT
@ -175,6 +220,8 @@ The pipeline must handle these without aborting silently:
|---|---|
| STEP 0b — `CLAUDE.md` missing | STOP with the printed message ("Run `/onboard` first…"). Do not proceed. |
| STEP 0c — `ctx7` not installed but fast-libs detected | Skip pre-fetch silently. During STEP 4, log `📚 ctx7 cache miss for <lib>` and continue with vanilla model knowledge. |
| STEP 0d — request too vague for analyzer to locate a code zone | analyzer reports ambiguous zones and does NOT block; STEP 1 brainstorm refines scope. The memory disposition is still produced. |
| STEP 0d — `.claude/memory/` absent (project not yet onboarded) | analyzer's guarded scan no-ops (the `[ -d ]` guard fires — a bare glob would error); proceed with the code ANALYSIS alone. STEP 7 creates registries later. |
| STEP 1 — brainstorming returns "design unclear" twice | Escalate: ask user "Switch to /init-project (greenfield-style design) or refine the feature request?" |
| STEP 3 — user replies "request changes" | Loop back to STEP 2 with user's notes. Cap at 3 iterations; on the 3rd "request changes" without approval, ask "Pause and rescope this feature?" |
| STEP 4 — subagent crashes (tool error, not test failure) | Treat as STEP 4b error path, present hypothesis-led gate. |