refactor(retrieval): the three rule arms run through one pipeline (milestone 456 step 2, #4904)
CI & Build / Plugin hooks (push) Successful in 19s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / Python tests (push) Failing after 1m28s
CI & Build / Build & push image (push) Skipped
CI & Build / integration (push) Successful in 1m15s

prompt_rule, pre_tool_rule and the rule half of the write path each wrote
the same steps out by hand: search, band, split fresh from repeats, log the
call before any early return, reserve a slot, render, record surfacings.
The copies drifted, and #3497, #3750 and #3752 were fixed one copy at a time.

- New services/retrieval_pipeline.py. run_rule_arm runs the stages in one
  order. RuleArm is the spec (source, band, compact_tail, checkpoint,
  preference_slot). RuleMoment is the query and the session ledger.
- The I/O (ranker and recorders) is passed in as RuleIO. plugin_context
  resolves it from its own names at call time, so existing patch points
  still apply.
- _rule_band, _rule_hint_line, checkpoint_for, checkpoint_reason, the band
  constant and the preference slot moved into the pipeline unchanged.
  plugin_context re-exports them.
- A recorder that raises now costs only its row, never a rendered line.
- The flags reproduce today exactly. Whether prompt_rule should band, and
  whether the act arms should reserve a preference, are step 7.
- Registry: the pipeline call sites are declared in FAN_OUT_SITES, with
  values read from the specs.
- The #3497 structural guard now checks the one implementation, and that
  plugin_context writes no rule-source row of its own.

plugin_context.py: 3,558 -> 2,980 lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 08:01:23 -04:00
co-authored by Claude Opus 5.5
parent be62abf142
commit f6b824b214
4 changed files with 783 additions and 716 deletions
+12
View File
@@ -42,6 +42,8 @@ from __future__ import annotations
from dataclasses import dataclass
from scribe.services.retrieval_pipeline import RULE_ARMS, RULE_SOURCES
# ── How a point is reached ────────────────────────────────────────────────
#
# UNBIDDEN: fires on its own, against a query the agent did not write — a
@@ -258,6 +260,16 @@ FAN_OUT_SITES: dict[str, tuple[str, ...]] = {
"enter_project", "start_planning", "get_task", "get_project",
"get_milestone",
),
# The one retrieval pipeline (milestone 456): every rule arm's call row and
# surfacing rows are written by the same two sites, so `source` arrives as
# a value. The values are the pipeline's own specs, read rather than
# restated, so an arm added there is declared here by construction.
"scribe/services/retrieval_pipeline.py::record_retrieval(source=source)": (
RULE_SOURCES
),
"scribe/services/retrieval_pipeline.py::record_rule_surfaced(source=source)": (
tuple(arm.source for arm in RULE_ARMS)
),
}