Milestone 440 "Lessons point at rules — a rule is found through the situations that keep proving it"
#4630 "The link" (41e4fba, c839397). Migration 0111 adds lesson_rule_links, with three states: suggested, confirmed, rejected.
create_lesson and update_lesson take rule_ids.
New tool judge_lesson_link, and PUT /api/lessons/<id>/rules/<rule_id>.
get_lesson shows the lesson's rules, and get_rule shows the rule's lessons.
Backup v18.
#4631 "Judged when written" (f7d8dc2, 95be251). Migration 0112 adds lesson_no_rule. "No rule fits" is now a recorded answer, given with its reason as no_rule="why".
An unanswered create returns rule_candidates.
Every lesson payload carries rule_judgment.
list_lessons(unjudged=true) and GET /api/lessons?unjudged=1 list the open ones.
Backup v19.
Guidance is in the using-scribe skill and the create_lesson and create_rule docstrings. Plugin 2026.10.01.1724.
#3735 "Step 8 — acceptance: a lesson is recalled on a project it was not written on" (2e4c2d9)
usage_for_notes adds surfaced_away_count and pulled_away_count.
The usage chip shows them.
Verification
CI on dev passed every job on the head commit 95be251 (run 708). That covers the real-Postgres integration tests for links, no-rule answers, the unjudged list, cascades and ACL.
Deploy notes
Two migrations run on startup: 0111, then 0112. Both are additive, with no backfill.
Running sessions need /plugin and /reload-plugins to pick up plugin 2026.10.01.1724.
## What this ships
**Milestone 440 "Lessons point at rules — a rule is found through the situations that keep proving it"**
- **#4630 "The link"** (41e4fba, c839397). Migration 0111 adds `lesson_rule_links`, with three states: suggested, confirmed, rejected.
- `create_lesson` and `update_lesson` take `rule_ids`.
- New tool `judge_lesson_link`, and `PUT /api/lessons/<id>/rules/<rule_id>`.
- `get_lesson` shows the lesson's `rules`, and `get_rule` shows the rule's `lessons`.
- Backup v18.
- **#4631 "Judged when written"** (f7d8dc2, 95be251). Migration 0112 adds `lesson_no_rule`. "No rule fits" is now a recorded answer, given with its reason as `no_rule="why"`.
- An unanswered create returns `rule_candidates`.
- Every lesson payload carries `rule_judgment`.
- `list_lessons(unjudged=true)` and `GET /api/lessons?unjudged=1` list the open ones.
- Backup v19.
- Guidance is in the using-scribe skill and the `create_lesson` and `create_rule` docstrings. Plugin 2026.10.01.1724.
**#3735 "Step 8 — acceptance: a lesson is recalled on a project it was not written on"** (2e4c2d9)
- `usage_for_notes` adds `surfaced_away_count` and `pulled_away_count`.
- The usage chip shows them.
## Verification
CI on dev passed every job on the head commit 95be251 (run 708). That covers the real-Postgres integration tests for links, no-rule answers, the unjudged list, cascades and ACL.
## Deploy notes
- Two migrations run on startup: 0111, then 0112. Both are additive, with no backfill.
- Running sessions need `/plugin` and `/reload-plugins` to pick up plugin 2026.10.01.1724.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Milestone 385 step 8 (#3735 "a lesson is recalled on a project it was not
written on") is defined by opened-on-another-project. The reader's project has
been recorded on every usage event since 0c8e109, but nothing compared it with
the record's own, so the criterion was still unreadable.
- note_usage.usage_for_notes: a second aggregate in the same session joins
notes and counts surfaced_away_count / pulled_away_count. Counted only where
both projects are known and differ; ranked surfacings only (#2477). The
first aggregate is untouched, so events on deleted notes still count.
- empty_usage carries both keys zero-filled; every door that attaches usage
(list_lessons, get_lesson, snippets, knowledge) gets them through
attach_usage.
- UsageBadge tooltip says "On other projects: surfaced N×, opened M×" when it
happened, and nothing when it did not.
- Tests: the mocked split test feeds both aggregates; a unit test for the
away counters; a real-Postgres test that home, unreported and ambient
events are all left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The link between a lesson (one concrete situation) and the rule that governs
it, with the operator's soft-then-hard design built into its state:
suggested while evidence accumulates, confirmed or rejected once judged. Only
confirmed will carry a rule in retrieval (#4633); rejected is kept so the pair
is never proposed again.
- models/lesson_rule_link.py + migration 0111: one row per (lesson, rule),
CASCADE on both ends, indexed both ways, CHECK on state (rule 36), evidence
JSONB and judged_at.
- services/lesson_rules.py: require_rules (validated before any write, so
a bad id leaves nothing half-linked), set_lesson_rules (set-semantics;
a dropped rule becomes rejected, not forgotten), judge_link, and the two
reads. ACL: write on the lesson (share-aware), ownership of the rule; a
reader sees only rules they own. Decorations are fail-open (#4286).
- MCP: create_lesson / update_lesson take rule_ids; get/create/update return
`rules`; new judge_lesson_link tool. REST: the same on /api/lessons plus
PUT /api/lessons/<id>/rules/<rule_id>. Rules: rule_detail carries `lessons`.
- Backup v18: export (full and user-scoped, both ends in scope), builder,
importer; both column guards register the table.
- Tests: integration (states, set-semantics, judge, ACL all-or-nothing,
cascade both ways, CHECK, one row per pair); unit (door wiring, judge
registered, migration/model state agreement, backup skip and unjudged
stays unjudged). conftest stubs the decorations for unit tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tool-classification guard (test_mcp_auth) failed CI run 705: the new
judge_lesson_link tool sat in no set, so a read key would have been silently
denied it. It changes a link's state, so it belongs in _WRITE_TOOLS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Which rule is this lesson an instance of?" now has three recorded answers:
a rule named (a confirmed link, #4630), no rule fits (new), or unjudged.
- Model + migration 0112: lesson_no_rule (lesson_id PK, CASCADE from the
note; why; judged_at). A table rather than a key in notes.data, because
that mirror is re-composed from the body on every edit and would erase it.
- Service (lesson_rules): set_no_rule rejects any confirmed link with the
reason; a confirmation (set_lesson_rules or judge_link) deletes the answer;
require_one_answer refuses both answers in one call before any write;
judgments_for_lessons + attach_lesson_rules add rule_judgment (and no_rule)
to every lesson payload; list_unjudged lists the open ones; rule_candidates
searches rules with the lesson's claim + trigger at the explicit-search bar,
None when the search could not run.
- MCP: create_lesson/update_lesson take no_rule; an unanswered create returns
rule_candidates, rule_judgment and a rule_hint; list_lessons(unjudged=true).
- REST: the same on POST/PATCH /api/lessons and GET ?unjudged=1; create
returns rule_candidates.
- Backup v19: a lesson_no_rule section, export (full and per-user) and import.
- Guidance: create_lesson docstring, writing-records.md in using-scribe (owner,
pinned in test_guidance_ownership), create_rule docstring on linking the
lessons a new rule governs. Plugin version minted.
- Tests: door units, integration for the three states, the rejection reason,
scoping, cascade; backup registries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI run 707 failed test_the_trigger_separator_is_spelled_in_exactly_one_place:
rule_candidates joined the lesson's claim and trigger with an inline " — ",
a second spelling of embeddings.TRIGGER_SEP (#3207). It now calls
trigger_title — the same join a rule's own document title is built with,
which was the point of the query shape in the first place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What this ships
Milestone 440 "Lessons point at rules — a rule is found through the situations that keep proving it"
41e4fba,c839397). Migration 0111 addslesson_rule_links, with three states: suggested, confirmed, rejected.create_lessonandupdate_lessontakerule_ids.judge_lesson_link, andPUT /api/lessons/<id>/rules/<rule_id>.get_lessonshows the lesson'srules, andget_ruleshows the rule'slessons.f7d8dc2,95be251). Migration 0112 addslesson_no_rule. "No rule fits" is now a recorded answer, given with its reason asno_rule="why".rule_candidates.rule_judgment.list_lessons(unjudged=true)andGET /api/lessons?unjudged=1list the open ones.create_lessonandcreate_ruledocstrings. Plugin 2026.10.01.1724.#3735 "Step 8 — acceptance: a lesson is recalled on a project it was not written on" (
2e4c2d9)usage_for_notesaddssurfaced_away_countandpulled_away_count.Verification
CI on dev passed every job on the head commit
95be251(run 708). That covers the real-Postgres integration tests for links, no-rule answers, the unjudged list, cascades and ACL.Deploy notes
/pluginand/reload-pluginsto pick up plugin 2026.10.01.1724.🤖 Generated with Claude Code