Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .claude/hooks/rules-improver-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# .claude/hooks/rules-improver-check.sh
#
# Nudges toward `.claude/skills/improve-rules/SKILL.md` once enough evidence has
# accumulated to be worth a pass.
#
# Event-driven, not time-driven, on purpose. A weekly cron runs whether or not
# anything happened, and a pass with nothing to read either returns "no change"
# (wasted) or invents one (worse). A fix commit is the signal that the rules met
# reality and something gave, so counting those fires the improver exactly when
# there is something to learn from.
#
# NEVER blocks. This is a prompt for a human decision, not a gate — the whole
# point of the improver is that it runs with distance, so forcing it mid-session
# would defeat it.

set -euo pipefail
cd "$(dirname "$0")/../.."

THRESHOLD="${RULES_IMPROVER_THRESHOLD:-8}"
STAMP=".claude/.rules-last-run"
SINCE=$(cat "$STAMP" 2>/dev/null || echo "")

if [ -n "$SINCE" ]; then
RANGE=(--since="$SINCE")
else
# No stamp yet: look back a sensible window rather than all of history.
RANGE=(--since="6 weeks ago")
fi

FIXES=$(git log "${RANGE[@]}" --format='%s' 2>/dev/null \
| grep -icE '^(fix|revert)' || true)
FIXES=${FIXES:-0}

LEDGER=0
if [ -f docs/RULE-FEEDBACK.md ]; then
if [ -n "$SINCE" ]; then
# Ledger entries are "### YYYY-MM-DD — ..."; count those dated after the stamp.
LEDGER=$(grep -oE '^### [0-9]{4}-[0-9]{2}-[0-9]{2}' docs/RULE-FEEDBACK.md \
| awk -v s="$SINCE" '{ if (substr($2,1,10) > s) n++ } END { print n+0 }')
else
LEDGER=$(grep -cE '^### [0-9]{4}-[0-9]{2}-[0-9]{2}' docs/RULE-FEEDBACK.md || true)
fi
fi
LEDGER=${LEDGER:-0}

SIGNAL=$(( FIXES + LEDGER ))

if [ "$SIGNAL" -ge "$THRESHOLD" ]; then
cat <<MSG

─────────────────────────────────────────────────────────────────
Rules improver: $SIGNAL signals since ${SINCE:-6 weeks ago}
$FIXES fix/revert commits + $LEDGER ledger entries (threshold $THRESHOLD)

Worth an observer pass: /improve-rules
It reads the accumulated evidence and proposes ONE focused edit to
CLAUDE.md as a PR. "No change warranted" is a successful outcome.
─────────────────────────────────────────────────────────────────

MSG
fi

exit 0
6 changes: 6 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
"command": "./.claude/hooks/session-start-check.sh",
"timeout": 30,
"statusMessage": "Session orient..."
},
{
"type": "command",
"command": "./.claude/hooks/rules-improver-check.sh",
"timeout": 15,
"statusMessage": "Rules improver check..."
}
]
}
Expand Down
13 changes: 10 additions & 3 deletions .claude/skills/end-of-session/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,16 @@ session.

## Checklist

1. **CLAUDE.md** — only if a genuinely cross-cutting rule was learned (an
incident, not a preference). Don't append domain gotchas here — they go
in the matching runbook (rule 19/36).
1. **`docs/RULE-FEEDBACK.md`** — append an entry if anything this session
argued with the rules: a fix that had to clean up after an earlier change,
a correction from the user, a guard that fired, a near miss. Write it even
when unsure it matters; it costs one line and commits to nothing.

**Do NOT edit CLAUDE.md here.** Judging your own bruise worth a
constitutional rule, in the moment, is how that file reached 65 numbers with
gaps. The ledger accumulates; `.claude/skills/improve-rules/SKILL.md`
decides later, with distance, and proposes it as a PR. Domain gotchas still
go in the matching runbook (rule 19/36).
2. **ROADMAP.md** — mark completed features Done, update priorities.
3. **Domain runbooks** (`docs/domains/*.md`) — update any runbook touched
during this session; bump its "Last verified" date if you re-checked it
Expand Down
132 changes: 132 additions & 0 deletions .claude/skills/improve-rules/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
name: improve-rules
description: Observer pass over accumulated evidence that proposes one focused edit to CLAUDE.md as a PR. Run when the fix-commit counter trips, or on demand when the rules feel stale. Never edits CLAUDE.md directly on main.
---

# Improve the rules

This is the **outer** skill. The inner skill is `CLAUDE.md` itself — the
constitution every session reads. This one observes how that constitution
performed and proposes a change to it.

Two properties make it work, and dropping either turns it into rule bloat:

**It runs with distance.** Not mid-task, not while the incident is warm. The
agent that just got burned is the worst judge of whether its bruise deserves a
constitutional rule — that instinct is exactly how this file reached 65 numbers
with gaps and five conflicting citations. Evidence accumulates in
`docs/RULE-FEEDBACK.md`; judgment happens here, later, over the pile.

**It proposes, it does not decide.** Output is a pull request against `main`,
one focused edit, for a human to accept or reject. Nothing here writes to
`main`.

## What counts as feedback here

Warp's version of this loop reads human PR review comments. **That signal does
not exist in this repo** — 20 merged PRs carry 0 reviews and 1 comment between
them, because `docs/FRAMEWORK.md` describes a solo project with no second pair
of eyes. Do not go looking for it and conclude there is nothing to learn.

The equivalent signal here is **the commit that had to clean up after the last
one**. A `fix:` or `revert:` commit is literally "what the agent proposed versus
what reality required", with a diff attached and no friction to capture. Roughly
one commit in four is one.

Read all four sources:

1. **Fix and revert commits since the last run.**

```bash
git log --oneline --since="$(cat .claude/.rules-last-run 2>/dev/null || echo '6 weeks ago')" \
--format='%h %s' | grep -iE '^[a-f0-9]+ (fix|revert)'
```

For each, read the diff. The question is never "was this a bug" — it is
**"which principle was in force, and why didn't it hold?"**

2. **The ledger** — `docs/RULE-FEEDBACK.md`, entries since the last run. Pay
particular attention to entries answering *"only if enforced differently"*:
those are the highest-value changes, because the rule already exists and is
simply in the wrong layer.

3. **CI failures on merged PRs.** `gh run list --status failure --limit 30`.
Which gate caught what. A class that repeatedly reaches CI before anything
catches it wants an earlier gate; a gate that has never fired is a
retirement candidate.

4. **Tripwire and hook firings.** A guard that fired and caught something is
evidence its rule earns its place — worth recording, because retirement
arguments later will ask.

## The judgment

Sort every piece of evidence into exactly one of these. Most land in the middle
two, and that is the useful finding.

| Finding | What it means | Action |
|---|---|---|
| **New failure mode** | No principle covers this shape | Propose a new principle, or a new numbered rule under an existing one |
| **Known mode, wrong layer** | A rule says it; nothing enforces it where it broke | Move enforcement: skill → hook → tripwire (`docs/FRAMEWORK.md` §3). **No text change.** |
| **Known mode, weak wording** | The rule is right but permitted this reading | Sharpen the rule's own text; keep it checkable |
| **Not a rules problem** | The rules were fine; something else failed | Record it in the ledger and change nothing |

**"Not a rules problem" is a real and frequent verdict.** A pass that proposes a
change every time it runs is not learning, it is accreting. Returning "no change
warranted, here is why" is a successful run.

## The bar for any proposed edit

Every one of these, or it does not ship:

- **Cite the incident.** Commit SHA, PR number, or ledger entry. No citation, no
rule — `docs/FRAMEWORK.md`: *rules earn their place by surviving incidents.*
- **Name the enforcement home.** Skill, hook, or tripwire. A rule with nowhere to
live is a wish.
- **Prefer strengthening a principle over adding a number.** Most incidents are
new instances of a named failure mode. A new number is for a mode that is
genuinely new.
- **Preserve every number.** 203 files cite rules by number. Never renumber,
never reuse, never delete a number anything still cites — demote to the
runbook with the number intact.
- **One change per PR.** So it can be judged on its own and reverted on its own.

## Retirement

Same evidence bar, opposite direction. A rule stops earning its place when:

- its code path no longer exists; or
- its guard has never fired and the class it prevents has not recurred; or
- nothing cites it and no incident is attached to it.

Check citations before proposing removal:

```bash
git grep -c "rule ${N}\b" -- . | wc -l
```

Demote to the matching runbook with the number preserved so citations resolve.
Delete a number outright only when nothing cites it — and record it under
`## Retired` in CLAUDE.md with the evidence, so the next pass does not
re-litigate it.

## Running it

```bash
git checkout -b chore/improve-rules-$(date +%Y%m%d)
```

Make the single edit. Then:

- Re-run the check that would have caught the original incident, and confirm it
now fails without the change and passes with it. A rule proposed without that
demonstration is a guess.
- PR body: the evidence, the finding category from the table above, and what the
change would have prevented. Link the commits.
- Stamp the run so the next pass reads the right window:

```bash
date -u +%Y-%m-%d > .claude/.rules-last-run
```

Then stop. A human merges it, and the next session inherits the improvement.
9 changes: 7 additions & 2 deletions .claude/skills/post-failure/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@ Fix the actual issue. Verify with `cd app && npm run validate && npm test`.
section, or a tripwire test naming the bug class (prefer a real-DB
integration test over a source-text tripwire where the behavior is
testable — rule 57).
- **Business rule / cross-cutting invariant** → propose a CLAUDE.md rule
update with an origin pointing at this entry.
- **Business rule / cross-cutting invariant** → append an entry to
`docs/RULE-FEEDBACK.md`, not a rule to CLAUDE.md. Say which principle this
is an instance of, or `NEW` if none fits, and answer "would a rule have
caught it?" — the answers "no" and "only if enforced differently" are the
valuable ones. `.claude/skills/improve-rules/SKILL.md` reads the ledger
later and proposes the constitutional change as a PR, with the distance that
makes the judgment worth trusting.
- **Data assumption** → update the relevant `docs/domains/*.md` runbook.

## Step 5: Update the Failure Log
Expand Down
Loading
Loading