Skip to content

Commit 6fb82c8

Browse files
nickrociclaude
andcommitted
feat(curator): generalise integrity-repair escalation to all invariant kinds
Today only broken wikilinks escalate into the Librarian->Scholar pipeline. Extend the SAME mechanism (enqueue -> Librarian researches+proposes -> Scholar executes) and the SAME in-flight concurrency guard to the other two check_invariants kinds, kind-dispatched rather than copy-pasted: - overcap_dir: a flat dir over MAX_FLAT_DIR_ENTRIES enqueues a repair task; the Librarian proposes a split_folder / move_entry rebalance. - bad_frontmatter: a missing/unparseable/incomplete frontmatter block enqueues a task; the Librarian proposes an update_entry that re-serialises valid frontmatter. check_invariants now projects over a new structured check_invariants_detailed that carries the (kind, file, target, context) needed to build a RepairTask. The Scholar's post-write check escalates each over-cap / bad-frontmatter violation through the existing repair_queue + in-flight guard (_escalate_invariant_violation), leaving the violation on disk so it re-detects and re-escalates until fixed (no max-attempts, no give-up). The Librarian prompt's INTEGRITY-REPAIR section now dispatches on `kind`. Also audit the Scholar prompt: the APPROVE-OR-VETO + salience-novelty framing could wrongly veto a repair proposal as "not novel" / "duplicate". Add an INTEGRITY-REPAIR PROPOSALS section that marks repair-originated proposals (packets carrying repair_fingerprints) as verify-and-execute, not novelty-judged, and have the salience filter skip them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 29a8039 commit 6fb82c8

7 files changed

Lines changed: 662 additions & 68 deletions

File tree

daemon/agent_mem_daemon/librarian_prompt.py

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -835,9 +835,18 @@ def _slugify(s: str) -> str:
835835
them to you. These are NOT discretionary — propose an action to repair \
836836
each one. They take priority over salience-driven proposals.
837837
838-
Each task names a ``file`` (relative to ``knowledge/``), a broken ``target`` \
839-
(the wikilink that does not resolve), and a ``context`` snippet showing \
840-
where it appears. For EACH broken-wikilink task, do this:
838+
Each task names a ``kind`` (the invariant type), a ``file`` (relative to \
839+
``knowledge/`` — for an over-cap dir this is the directory), a ``target`` \
840+
(the offending value, interpreted per kind), and a ``context`` snippet. \
841+
ALWAYS set ``salience_signal: null`` on a repair proposal — these are \
842+
integrity fixes, not salience judgments, and the Scholar will \
843+
verify-and-execute them rather than judge them for novelty. In \
844+
``reasoning``, quote the task's ``file`` and ``target`` and state which fix \
845+
you chose and why. Dispatch on ``kind``:
846+
847+
──────────────────────────────────────────────────────────────────
848+
kind: broken_wikilink (``target`` = the wikilink that does not resolve)
849+
──────────────────────────────────────────────────────────────────
841850
842851
1. **Research the intended target.** The broken target usually got the \
843852
PATH wrong, not the concept. Run ``mcp__agent_mem_library__bm25_search`` \
@@ -863,15 +872,61 @@ def _slugify(s: str) -> str:
863872
the link or replace it with plain descriptive text), preserving the rest \
864873
of the prose. Explain in ``reasoning`` why no target should exist.
865874
866-
3. In ``reasoning``, quote the task's ``file`` and ``target`` and state \
867-
which of the three fixes you chose and why (cite the entry you found, or \
868-
state that no entry exists). Set ``salience_signal: null`` for repair \
869-
proposals — they are integrity fixes, not salience judgments.
870-
871875
Do the link research with the SAME parallel-search discipline as for \
872876
dedup. A repair proposal that guesses the path without searching will \
873877
likely be vetoed.
874878
879+
──────────────────────────────────────────────────────────────────
880+
kind: overcap_dir (``file``/``target`` = the over-capacity directory)
881+
──────────────────────────────────────────────────────────────────
882+
883+
The directory holds more than 5 entry .md files and must be rebalanced. \
884+
The ``context`` lists every entry currently in it.
885+
886+
1. **Read the entries** (or their frontmatter ``title``/``keywords`` from \
887+
the snapshot) to find the natural thematic groupings. Use the existing \
888+
sub-structure of sibling folders as a guide for sensible subfolder names.
889+
890+
2. **Propose exactly ONE rebalancing action:**
891+
- **The entries split into 2+ coherent themes** → ``split_folder`` on \
892+
``folder_path`` = the over-cap dir, with ``into`` mapping each new \
893+
subfolder NAME → the list of entry paths (relative to ``knowledge/``) that \
894+
move there. Leave no destination subfolder over 5; entries you don't list \
895+
stay put (so the remainder must also be ≤5). The Scholar's \
896+
``move_entries`` call rewrites all inbound wikilinks atomically.
897+
- **A few entries clearly belong in an EXISTING sibling folder** → one \
898+
``move_entry`` per such entry (``from_path`` → ``to_path``) until the dir \
899+
is back at or below 5. Prefer this when only one or two entries are \
900+
outliers and the rest are cohesive.
901+
902+
3. Choose subfolder names that read well as a path \
903+
(``global/python/testing/`` not ``global/python/misc-2/``). Do NOT propose \
904+
a split that just shards the dir into ``part-1``/``part-2`` — the grouping \
905+
must be meaningful.
906+
907+
──────────────────────────────────────────────────────────────────
908+
kind: bad_frontmatter (``file``/``target`` = the entry with bad frontmatter)
909+
──────────────────────────────────────────────────────────────────
910+
911+
The entry's YAML frontmatter is missing, unparseable, or short the \
912+
required fields (``context`` names the exact defect). The body content is \
913+
fine — only the frontmatter block needs repair.
914+
915+
1. **Read ``file``** to recover its current frontmatter (whatever is \
916+
salvageable) and its full body.
917+
918+
2. **Propose an ``update_entry``** on ``file`` whose ``new_body`` is the \
919+
entry's UNCHANGED body preceded by a corrected frontmatter block that \
920+
re-serialises valid YAML with EVERY required field present: ``id`` \
921+
(kebab-case matching the filename), ``type``, ``scope`` (consistent with \
922+
the path — ``global`` under ``global/``, ``project:<slug>`` under \
923+
``projects/<slug>/``), ``status``, ``confidence``, ``applies-when``, \
924+
``keywords``, ``title``, ``created``, ``updated``, ``fired``, \
925+
``fired-helpful``, ``sources``. Preserve any existing valid values; fill \
926+
missing fields from the body's content and sensible defaults (e.g. \
927+
``status: provisional``, ``confidence: 0.6``, ``fired: 0``, \
928+
``fired-helpful: 0``). Do NOT rewrite the body prose.
929+
875930
═══════════════════════════════════════════════════════════════════
876931
HIERARCHY INVARIANTS (the Scholar will veto violations)
877932
═══════════════════════════════════════════════════════════════════

daemon/agent_mem_daemon/repair_queue.py

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
"""Integrity-repair task queue — escalation bridge into the curator.
22
3-
When the deterministic post-write pass (``scholar_prompt.repair_broken_wikilinks``)
4-
finds an invariant violation it CANNOT fix on its own — today, a broken
5-
wikilink with no unique on-disk target — it records a :class:`RepairTask`
6-
here. The next Librarian run drains the pending tasks, renders them into
7-
its prompt, and proposes an EXISTING curator action (rewrite the link,
8-
write the missing target, or remove it with a reason). The Scholar
9-
reviews/executes that proposal as a normal proposal. No new agent, no new
10-
role — the same Librarian→Scholar pipeline, fed a different kind of input.
3+
When a post-write check finds an invariant violation that cannot be fixed
4+
deterministically, it records a :class:`RepairTask` here. The next Librarian
5+
run drains the pending tasks, renders them into its prompt, and proposes an
6+
EXISTING curator action; the Scholar reviews/executes it as a normal
7+
proposal. No new agent, no new role — the same Librarian→Scholar pipeline,
8+
fed a different kind of input. Three invariant kinds escalate through this
9+
one mechanism (see the ``KIND_*`` constants), dispatched by ``kind``:
10+
11+
- ``broken_wikilink`` — surfaced by ``scholar_prompt.repair_broken_wikilinks``
12+
when a link has no unique on-disk target → Librarian rewrites / writes
13+
the missing target / removes the link.
14+
- ``overcap_dir`` — surfaced by ``check_invariants`` when a flat dir
15+
exceeds the entry cap → Librarian proposes a ``split_folder`` (or
16+
``move_entry``) to rebalance.
17+
- ``bad_frontmatter`` — surfaced by ``check_invariants`` when an entry's
18+
frontmatter is missing/unparseable/incomplete → Librarian proposes an
19+
``update_entry`` that re-serialises valid frontmatter.
1120
1221
────────────────────────────────────────────────────────────────────
1322
THE ONLY GUARD: in-flight, keyed on the issue fingerprint
@@ -39,12 +48,14 @@
3948
re-escalates it. (For that to hold, the escalation path must NOT neutralise
4049
the link it escalates — see ``scholar_prompt._repair_body_links``: an
4150
escalated link is left broken on disk so it stays detectable until the
42-
Scholar actually fixes it.)
43-
44-
Extensibility: :class:`RepairTask` carries a ``kind`` discriminator so
45-
other invariant types (over-cap directories, malformed frontmatter) can
46-
plug into the same queue + guard later. Only ``broken_wikilink`` is wired
47-
today.
51+
Scholar actually fixes it. The over-cap and bad-frontmatter checks are
52+
already non-destructive — they only read — so re-detection is automatic.)
53+
54+
Generality: :class:`RepairTask` carries a ``kind`` discriminator and the
55+
queue treats every kind identically. Adding a fourth invariant type is a
56+
matter of (a) a new ``KIND_*`` literal, (b) a detection point that enqueues
57+
it, and (c) a prompt section telling the Librarian how to repair it — no
58+
change to the queue itself.
4859
"""
4960

5061
from __future__ import annotations
@@ -57,18 +68,31 @@
5768
# issue" so repeated detections collapse onto one in-flight attempt.
5869
Fingerprint = Tuple[str, str, str]
5970

60-
# v1 wires only this kind. New invariant types add their own literal and
61-
# reuse the same enqueue/drain/clear machinery.
71+
# Invariant-type discriminators. Every kind reuses the SAME
72+
# enqueue/drain/clear machinery + in-flight guard — the queue itself stays
73+
# kind-agnostic; only the detection point (Scholar) and the rendering point
74+
# (Librarian prompt) branch on the value.
75+
#
76+
# - ``broken_wikilink`` — a wikilink with no unique on-disk target.
77+
# - ``overcap_dir`` — a flat directory over ``MAX_FLAT_DIR_ENTRIES``.
78+
# - ``bad_frontmatter`` — an entry whose frontmatter is missing,
79+
# unparseable, or short the required fields.
6280
KIND_BROKEN_WIKILINK = "broken_wikilink"
81+
KIND_OVERCAP_DIR = "overcap_dir"
82+
KIND_BAD_FRONTMATTER = "bad_frontmatter"
6383

6484

6585
@dataclass(frozen=True)
6686
class RepairTask:
6787
"""One integrity-repair task escalated to the Librarian.
6888
69-
``kind`` discriminator for the invariant type (``broken_wikilink``).
70-
``file`` knowledge-root-relative path of the file holding the issue.
71-
``target`` the offending value — for a wikilink, its broken target.
89+
``kind`` discriminator for the invariant type (one of ``KIND_*``).
90+
``file`` knowledge-root-relative path of the file (or directory, for
91+
``overcap_dir``) holding the issue.
92+
``target`` the offending value, interpreted per ``kind``: a wikilink's
93+
broken target; the directory path for an over-cap dir; the
94+
entry path for bad frontmatter. Always non-empty so the
95+
fingerprint is a stable identity.
7296
``context`` a short human-readable snippet so the Librarian can see
7397
where/how the issue appears without re-reading the file.
7498
"""

daemon/agent_mem_daemon/scholar.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -244,16 +244,55 @@ def _repair_wikilinks_safe(record: runs.InvocationRecord) -> None:
244244
log.exception("scholar.review: wikilink repair raised")
245245

246246

247+
def _escalate_invariant_violation(violation: scholar_prompt.InvariantViolation) -> None:
248+
"""Enqueue a repair task for one escalating invariant violation.
249+
250+
The SAME mechanism as ``_escalate_unresolved_wikilink``: build the
251+
violation's :class:`repair_queue.RepairTask` and ``enqueue`` it. The
252+
in-flight guard inside the queue collapses repeated detections of the
253+
same fingerprint onto one attempt; there is no max-attempts cap, so a
254+
still-unfixed violation re-escalates on the next pass once its marker is
255+
released. Display-only violations (``repair_kind is None``) yield no
256+
task and are skipped."""
257+
task = violation.to_repair_task()
258+
if task is None:
259+
return
260+
enqueued = repair_queue.get_queue().enqueue(task)
261+
if enqueued:
262+
log.info(
263+
"scholar.review: escalated %s to Librarian (file=%s target=%s)",
264+
task.kind,
265+
task.file,
266+
task.target,
267+
)
268+
else:
269+
log.debug(
270+
"scholar.review: %s already in-flight; skipping duplicate escalation "
271+
"(file=%s target=%s)",
272+
task.kind,
273+
task.file,
274+
task.target,
275+
)
276+
277+
247278
def _check_invariants_safe(record: runs.InvocationRecord) -> None:
248-
"""Deterministic post-write invariants check. Safety net for anything
249-
that survived the Scholar's judgement, the reconciler, and the
250-
wikilink repair pass."""
279+
"""Deterministic post-write invariants check + escalation. Safety net
280+
for anything that survived the Scholar's judgement, the reconciler, and
281+
the wikilink repair pass.
282+
283+
Over-cap directories and bad/unparseable frontmatter are not
284+
deterministically fixable, so each such violation is ESCALATED into the
285+
Librarian→Scholar pipeline via the same repair queue + in-flight guard
286+
that broken wikilinks use (``_escalate_invariant_violation``). The
287+
violation is left in place on disk, so the next pass re-detects and
288+
re-escalates it until the Scholar actually fixes it."""
251289
try:
252-
violations = scholar_prompt.check_invariants(knowledge_dir())
290+
violations = scholar_prompt.check_invariants_detailed(knowledge_dir())
253291
if violations:
254292
record.decisions["invariant_violations"] = len(violations)
255293
for v in violations:
256-
log.warning("scholar.review: post-write violation: %s", v)
294+
log.warning("scholar.review: post-write violation: %s", v.message)
295+
_escalate_invariant_violation(v)
257296
else:
258297
log.debug("scholar.review: invariants clean")
259298
except Exception:

0 commit comments

Comments
 (0)