|
| 1 | +"""Retract an accepted lesson from semantic memory. |
| 2 | +
|
| 3 | +This is an append-only transition: a new row with the same lesson id is |
| 4 | +written to lessons.jsonl with status='retracted'. LESSONS.md is then |
| 5 | +re-rendered from the structured source of truth. |
| 6 | +""" |
| 7 | +import argparse |
| 8 | +import datetime |
| 9 | +import os |
| 10 | +import sys |
| 11 | + |
| 12 | +BASE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) |
| 13 | +sys.path.insert(0, os.path.join(BASE, "memory")) |
| 14 | + |
| 15 | +from render_lessons import append_lesson, load_lessons, render_lessons |
| 16 | + |
| 17 | +SEMANTIC = os.path.join(BASE, "memory/semantic") |
| 18 | + |
| 19 | + |
| 20 | +def _latest_by_id(lesson_id, lessons): |
| 21 | + latest = None |
| 22 | + for lesson in lessons: |
| 23 | + if lesson.get("id") == lesson_id: |
| 24 | + latest = lesson |
| 25 | + return latest |
| 26 | + |
| 27 | + |
| 28 | +def retract_lesson(lesson_id, rationale, reviewer="host-agent", semantic_dir=SEMANTIC): |
| 29 | + if not str(rationale or "").strip(): |
| 30 | + raise ValueError("retraction rationale is required") |
| 31 | + |
| 32 | + lessons = load_lessons(semantic_dir) |
| 33 | + latest = _latest_by_id(lesson_id, lessons) |
| 34 | + if latest is None: |
| 35 | + raise ValueError(f"lesson not found: {lesson_id}") |
| 36 | + |
| 37 | + status = latest.get("status") |
| 38 | + if status != "accepted": |
| 39 | + raise ValueError( |
| 40 | + f"lesson {lesson_id} is not retractable (current status: {status})" |
| 41 | + ) |
| 42 | + |
| 43 | + now = datetime.datetime.now(datetime.timezone.utc).isoformat() |
| 44 | + updated = { |
| 45 | + **latest, |
| 46 | + "status": "retracted", |
| 47 | + "retracted_at": now, |
| 48 | + "retracted_by": reviewer, |
| 49 | + "retraction_rationale": rationale, |
| 50 | + } |
| 51 | + |
| 52 | + append_lesson(updated, semantic_dir) |
| 53 | + md_path = render_lessons(semantic_dir) |
| 54 | + return updated, md_path |
| 55 | + |
| 56 | + |
| 57 | +def main(): |
| 58 | + parser = argparse.ArgumentParser( |
| 59 | + description="Retract an accepted lesson by lesson id." |
| 60 | + ) |
| 61 | + parser.add_argument("lesson_id") |
| 62 | + parser.add_argument( |
| 63 | + "--rationale", |
| 64 | + required=True, |
| 65 | + help="Why this lesson should stop guiding future decisions.", |
| 66 | + ) |
| 67 | + parser.add_argument("--reviewer", default="host-agent") |
| 68 | + args = parser.parse_args() |
| 69 | + |
| 70 | + try: |
| 71 | + lesson, md_path = retract_lesson( |
| 72 | + lesson_id=args.lesson_id, |
| 73 | + rationale=args.rationale, |
| 74 | + reviewer=args.reviewer, |
| 75 | + ) |
| 76 | + except ValueError as exc: |
| 77 | + print(f"ERROR: {exc}", file=sys.stderr) |
| 78 | + sys.exit(1) |
| 79 | + |
| 80 | + print(f"retracted {args.lesson_id}") |
| 81 | + print(f"status: {lesson.get('status')}") |
| 82 | + print(f"re-rendered: {md_path}") |
| 83 | + |
| 84 | + |
| 85 | +if __name__ == "__main__": |
| 86 | + main() |
0 commit comments