From 80e76cb91e645758d53755d2b7fbe94426d45a06 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 30 May 2026 12:40:02 -0500 Subject: feat(workflows): add drill-deck-review + extend drill-to-anki script I added a drill-deck-review workflow that walks an org-drill deck end-to-end: question-form audit on every heading (so the heading is the prompt, not the answer), content-accuracy audit via subagent against project source-of-truth, source rewrite preserving SRS state, regenerate to ~/sync/phone/anki/. The workflow covers three card families (acronym/concept, person, talking-point) and codifies the person-card pattern as "Who is X? Tell me about their Y." where X is a role descriptor that doesn't name the person and Y is the topical anchor from the answer body. The drill-to-anki.py script picks up a new strip_org_metadata helper that drops :PROPERTIES: drawers and SCHEDULED/DEADLINE/CLOSED planning lines from the rendered Anki back. Org-drill needs both in the source for SRS state and review scheduling; Anki cards shouldn't show them. INDEX entry under "On-demand utilities" wires the trigger phrases. --- .ai/scripts/drill-to-anki.py | 27 +++ claude-templates/.ai/scripts/drill-to-anki.py | 27 +++ claude-templates/.ai/workflows/INDEX.org | 2 + .../.ai/workflows/drill-deck-review.org | 210 +++++++++++++++++++++ 4 files changed, 266 insertions(+) create mode 100644 claude-templates/.ai/workflows/drill-deck-review.org diff --git a/.ai/scripts/drill-to-anki.py b/.ai/scripts/drill-to-anki.py index 50e1afd..543ccd8 100755 --- a/.ai/scripts/drill-to-anki.py +++ b/.ai/scripts/drill-to-anki.py @@ -100,6 +100,32 @@ def title_from_org(org_text: str) -> str | None: return None +def strip_org_metadata(body_lines: list[str]) -> list[str]: + """Drop :PROPERTIES: drawers and SCHEDULED/DEADLINE/CLOSED planning lines. + + Org-drill needs these in the source file (SRS state lives in the + PROPERTIES drawer; SCHEDULED carries the next-review date), but they + are noise on the back of an Anki card. + """ + cleaned: list[str] = [] + in_drawer = False + planning_re = re.compile(r"^\s*(SCHEDULED|DEADLINE|CLOSED):\s") + drawer_start_re = re.compile(r"^\s*:PROPERTIES:\s*$") + drawer_end_re = re.compile(r"^\s*:END:\s*$") + for line in body_lines: + if in_drawer: + if drawer_end_re.match(line): + in_drawer = False + continue + if drawer_start_re.match(line): + in_drawer = True + continue + if planning_re.match(line): + continue + cleaned.append(line) + return cleaned + + def parse(org_text: str) -> list[tuple[str, str, str]]: """Return [(front, back_html, tag), ...] for every :drill: card.""" cards: list[tuple[str, str, str]] = [] @@ -130,6 +156,7 @@ def parse(org_text: str) -> list[tuple[str, str, str]]: break body_lines.append(nxt) i += 1 + body_lines = strip_org_metadata(body_lines) while body_lines and not body_lines[0].strip(): body_lines.pop(0) while body_lines and not body_lines[-1].strip(): diff --git a/claude-templates/.ai/scripts/drill-to-anki.py b/claude-templates/.ai/scripts/drill-to-anki.py index 50e1afd..543ccd8 100755 --- a/claude-templates/.ai/scripts/drill-to-anki.py +++ b/claude-templates/.ai/scripts/drill-to-anki.py @@ -100,6 +100,32 @@ def title_from_org(org_text: str) -> str | None: return None +def strip_org_metadata(body_lines: list[str]) -> list[str]: + """Drop :PROPERTIES: drawers and SCHEDULED/DEADLINE/CLOSED planning lines. + + Org-drill needs these in the source file (SRS state lives in the + PROPERTIES drawer; SCHEDULED carries the next-review date), but they + are noise on the back of an Anki card. + """ + cleaned: list[str] = [] + in_drawer = False + planning_re = re.compile(r"^\s*(SCHEDULED|DEADLINE|CLOSED):\s") + drawer_start_re = re.compile(r"^\s*:PROPERTIES:\s*$") + drawer_end_re = re.compile(r"^\s*:END:\s*$") + for line in body_lines: + if in_drawer: + if drawer_end_re.match(line): + in_drawer = False + continue + if drawer_start_re.match(line): + in_drawer = True + continue + if planning_re.match(line): + continue + cleaned.append(line) + return cleaned + + def parse(org_text: str) -> list[tuple[str, str, str]]: """Return [(front, back_html, tag), ...] for every :drill: card.""" cards: list[tuple[str, str, str]] = [] @@ -130,6 +156,7 @@ def parse(org_text: str) -> list[tuple[str, str, str]]: break body_lines.append(nxt) i += 1 + body_lines = strip_org_metadata(body_lines) while body_lines and not body_lines[0].strip(): body_lines.pop(0) while body_lines and not body_lines[-1].strip(): diff --git a/claude-templates/.ai/workflows/INDEX.org b/claude-templates/.ai/workflows/INDEX.org index aa64e2e..9c9c32c 100644 --- a/claude-templates/.ai/workflows/INDEX.org +++ b/claude-templates/.ai/workflows/INDEX.org @@ -84,6 +84,8 @@ This index must list every =.org= file in =.ai/workflows/= except this one and e - Triggers: "page me on signal", "signal me when X is done", "send a signal note about X" - =cross-project-broadcast.org= — fan out a single message to every AI project's inbox via the discovery helper =cross-project-broadcast.py= + the existing =inbox-send.py=. Use sparingly for capability announcements and shared rule changes; not for project-specific handoffs. - Triggers: "broadcast this to every project", "notify every project about X", "fan out this announcement", "let every project know X is available" +- =drill-deck-review.org= — review an org-drill flashcard file, restructure cards to question-form headings (no answer hints), audit content accuracy against project source-of-truth via subagent, rewrite source preserving SRS state, regenerate the Anki =.apkg= to =~/sync/phone/anki/=. Person cards use "Who is X? Tell me about their Y."; talking-points cards stay as-is. Script behavior: =drill-to-anki.py= strips =:PROPERTIES:= drawers + =SCHEDULED:= / =DEADLINE:= planning lines from Anki output. + - Triggers: "review the drill deck", "update the drill deck", "refresh the Anki cards", "let's run the drill-deck-review workflow" - =page-me.org= — set a timed notification. - Triggers: anything containing the word "page" used as a verb ("page me", "page me in 10 minutes", "page me at 3pm") - =status-check.org= — proactive long-running-job updates. diff --git a/claude-templates/.ai/workflows/drill-deck-review.org b/claude-templates/.ai/workflows/drill-deck-review.org new file mode 100644 index 0000000..a891f62 --- /dev/null +++ b/claude-templates/.ai/workflows/drill-deck-review.org @@ -0,0 +1,210 @@ +#+TITLE: Drill Deck Review Workflow +#+AUTHOR: Craig Jennings & Claude +#+DATE: 2026-05-30 + +* Overview + +Take an org-drill flashcard file and bring it into the canonical shape — every card a question that doesn't give the answer away, every fact current — then regenerate the Anki =.apkg= and drop it where the phone can sync it. + +The workflow has three substantive passes (question-form audit, content-accuracy audit, source rewrite) followed by a mechanical regenerate-and-place step. Content review is dispatched to a subagent because it's bounded research across project source-of-truth files; the structural rewrite stays in the main thread because it touches the SRS state we don't want to lose. + +* When to Use This Workflow + +Trigger phrases: + +- "Review the drill deck" +- "Update the drill deck" +- "Refresh the Anki cards" +- "Let's run the drill-deck-review workflow" + +Typical timing: + +- After a wave of personnel changes (titles, roles, employment status) +- After a major milestone (a demo ships, a contract closes, a submission goes in) +- When org-drill review surfaces a card with stale or wrong content +- When the Anki deck on the phone hasn't been regenerated in weeks + +* Inputs + +- *Source file*: the org-drill file. Common locations: + - =deepsat.org= at the work project root (symlinked from =~/sync/org/drill/=) + - =health-drill.org= in the health project + - Any =:drill:= deck under =~/sync/org/drill/= +- *Source-of-truth docs for content accuracy*: project-specific. Typical set: + - Project-root =knowledge.org=, =status.org=, =notes.org= + - =todo.org= for the freshest signal on people / partnerships / projects + - =deepsat/assets/= (or equivalent) for meeting transcripts when a specific fact needs confirmation +- *Output location*: =~/sync/phone/anki/.apkg= (the phone-sync target). The script's default is =~/sync/org/drill/=; override with =--output= per this workflow. + +* Canonical Card Shape + +** Heading (the question) + +Every card heading is a question that doesn't reveal the answer. Not the topic name, not the acronym, not the person's name — a question that tests recall. + +Three card families have different question shapes: + +*** Acronym / concept cards +"What does X stand for and what is it?" or "What is X and why does it matter?" Promote the question that was already in the body up to the heading. + + Before: + : ** AFRL :drill: + : What does AFRL stand for and what is it? + : *** Answer + : Air Force Research Laboratory. ... + + After: + : ** What does AFRL stand for and what is it? :drill: + : Air Force Research Laboratory. ... + +*** Person cards +Format: "Who is X? Tell me about their Y." where X is a role descriptor that doesn't name the person, and Y is whatever the answer body covers (background, role, limitations, scope). The answer body opens by naming the person, then continues. + + Before: + : ** Vrezh Mikayelyan :drill: + : Who is Vrezh? What are his key limitations? + : *** Answer + : Developer (also called "Reg"). Armenia-based ... + + After: + : ** Who is DeepSat's Armenia-based developer? Tell me about his background and limitations. :drill: + : Vrezh Mikayelyan. Armenia-based, full-time as of April 2026. Worked with Hayk at Bazoomq on Armenia's first satellite ... + + Note: pick a role descriptor that genuinely identifies one person. If multiple people share the role description, add a single distinguishing detail (e.g., "the one who works evenings", "the Vineti alum"). Don't pile on parentheticals. + +*** Talking-points cards +Already in question form ("Introduce Yourself", "What is DeepSat?", "What do you do at DeepSat?"). Leave the heading alone. Still strip the =*** Answer= sub-header and audit the body content for staleness. + +** Body (the answer) + +- *No =*** Answer= sub-header.* The body /is/ the answer; the heading /is/ the question. The sub-header was a workaround for topic-as-heading cards. +- *Body opens by naming the topic.* "Air Force Research Laboratory. Air Force's R&D arm." or "Vrezh Mikayelyan. Armenia-based, full-time as of ..." The Anki back shows this directly under the front question; restating the topic makes the back read as a complete answer. +- *PROPERTIES drawer stays.* Org-drill needs the =:ID:=, =:DRILL_LAST_INTERVAL:=, =:DRILL_EASE:= etc. for SRS state. The Anki output strips it (see the script change). +- *=SCHEDULED:= / =DEADLINE:= planning lines stay.* Same reason. The Anki output strips them. + +* Approach: Phases + +** Phase A: Question-form audit (per card) + +Walk every =** ... :drill:= card and flag the ones that don't already match the canonical shape: + +- Heading is the topic / acronym / person's name → flag for rewrite. +- Heading is already a question but the body still has a =*** Answer= sub-header → flag for sub-header removal. +- Heading is a question /and/ the body is clean → no action. + +Output of Phase A: a list of cards needing rewrite, with the proposed new heading for each. For person cards, this means proposing the role descriptor up front so Phase C is mechanical. + +** Phase B: Content-accuracy audit (subagent) + +Dispatch a subagent with this contract (adapt the source-of-truth list per project): + +#+begin_example +Audit the answer bodies of every :drill: card in against + and surface every fact that is stale, wrong, +or out-of-date. Don't rewrite the cards; report only items that need +to change. + +Output shape per finding: + CARD: + CURRENT: + UPDATE: + CONFIDENCE: high / medium / low + +Categories to look for: +- Personnel: title changes, employment-status changes, departures, new joiners +- Partner status: contracts that closed or fell through, partnerships that advanced or stalled, named individuals changing roles +- Project facts: milestone shifts, submission states, exercise / demo dates +- External contacts: title or affiliation changes +- Company facts: head count, funding, customer status + +Skip cards where you find no staleness. Cap at 2,000 words. +#+end_example + +Include any user-supplied seed fixes in the dispatch (e.g., "Vrezh is now full-time, drop the 'Reg' diarization error"). The subagent folds them into its report so they land in the same disposition table. + +Output of Phase B: a structured per-card list of content updates with confidence levels. High-confidence findings get baked in during Phase C. Medium-confidence findings are reviewed inline before baking. Low-confidence findings are surfaced but skipped unless the user calls them in. + +** Phase C: Source rewrite + +Take Phase A's question-rewrite plan and Phase B's content-update list, apply them to the source file. Preserve every card's =:PROPERTIES:= drawer (especially =:ID:=) and =SCHEDULED:= line verbatim — those carry SRS state that must survive the rewrite. + +Rewrite shape per card: + +#+begin_example +** :drill: +[SCHEDULED line if present] +:PROPERTIES: +[ID + DRILL_* lines unchanged] +:END: + +#+end_example + +Drop the =*** Answer= sub-header entirely. The body that was under =*** Answer= becomes the body of the card. If the original body had a question above =*** Answer= (the pre-rewrite norm), drop that question — the new heading carries it. + +For the file as a whole, use a single =Write= rather than per-card =Edit= calls. One pass through the source, one write back. Per-card edits multiply tool calls by N and risk drift. + +** Phase D: Regenerate the Anki deck + +#+begin_src bash +.ai/scripts/drill-to-anki.py --output ~/sync/phone/anki/.apkg +#+end_src + +The script writes the =.apkg= with stable deck/model IDs derived from the deck name, so re-importing into Anki updates existing cards rather than duplicating them. + +** Phase E: Verify + +- Spot-check the new =.apkg= size against the prior version. Significant size changes are expected when many cards were rewritten; a wildly smaller file may mean the parser dropped cards (PROPERTIES drawer mishandling, etc.). +- Open the source in Emacs (or read with =head -100=) and confirm a few cards visually: question heading, no =*** Answer=, PROPERTIES preserved, body opens with topic name. +- If the user keeps an org-drill session open, mention they'll want to revert the buffer to pick up the rewrite. + +** Phase F: Commit + +Two clusters: + +- *Source rewrite*: the org file (e.g., =deepsat.org=). Commit subject: =chore(drill): restructure cards to question-form headings + content refresh=. Body lists the content-update categories (Vrezh full-time, DCVC passed, etc.) and notes that =*** Answer= sub-headers were dropped. +- *Workflow / script changes* (if any): if this run prompted updates to =drill-deck-review.org= or =drill-to-anki.py= in the rulesets repo, commit those separately with =chore(workflows):= or =chore(scripts):= subjects. + +Push both. The =.apkg= itself lives under =~/sync/phone/= which is outside the repo — no commit needed there; Syncthing (or whatever sync mechanism) handles propagation. + +* Anki Script Behavior + +The =drill-to-anki.py= script (under =.ai/scripts/=) has these contracts that this workflow depends on: + +1. *Strips =:PROPERTIES:= drawers* from the card body before rendering. Org-drill needs them in source; Anki cards shouldn't show them. +2. *Strips =SCHEDULED:= / =DEADLINE:= / =CLOSED:= planning lines* from the card body. Same reason. +3. *Does NOT strip =*** Answer= sub-headers.* If the source still has them, the Anki cards will show them. This workflow's Phase C removes them at the source. +4. *Front of each Anki card* = the heading text without the =:drill:= tag. +5. *Back of each Anki card* = the cleaned body (after #1 and #2), joined with =
= and HTML-escaped. +6. *Stable IDs* derived from the deck name + a salt, so re-importing the same deck name updates cards rather than duplicating. + +If you find the script doing something else (e.g., not stripping PROPERTIES), update the script before regenerating. Don't work around a script bug in the source rewrite — the next deck will hit the same problem. + +* Output Path Convention + +- Default in the script: =~/sync/org/drill/.apkg= (matches the convention where org sources live in project repos and symlink into =~/sync/org/drill/=). +- Default in this workflow: =~/sync/phone/anki/.apkg= (the phone-syncable Anki target). Override the script default with =--output= every time. + +Both paths can coexist. The =~/sync/org/drill/= dir holds Anki exports alongside the org sources (build-artifact convention); =~/sync/phone/anki/= holds the version that syncs to the phone (consumption-target convention). For most decks, only the =/sync/phone/= copy is actually consumed, so this workflow writes there directly and skips the intermediate. + +* Common Mistakes + +1. *Per-card =Edit= calls instead of one =Write=.* Multiplies tool calls and risks drift between cards. Read once, rewrite in memory, write once. +2. *Dropping the PROPERTIES drawer in source.* Org-drill stores SRS state there; losing it resets every card's review history. +3. *Rewriting person headings to include the name.* "Who is Vrezh Mikayelyan?" gives away the answer. The whole point is to test name recall from a role description. +4. *Forgetting to strip =*** Answer= sub-headers.* The Anki output will show them as visible card content. The source rewrite must drop them. +5. *Skipping the content-accuracy pass.* The structural rewrite alone leaves stale facts in place. The drill cards become a memorization tool for the wrong information. +6. *Outputting to the script's default path.* The phone won't pick up =~/sync/org/drill/.apkg=. Always pass =--output ~/sync/phone/anki/.apkg=. +7. *Treating subagent output as gospel.* Medium- and low-confidence findings need human review before baking. The subagent surfaces; the main thread decides. + +* Living Document + +Update this workflow as patterns emerge. Specifically: + +- New card family beyond acronym / person / talking-point → document the heading shape for it. +- New source-of-truth doc beyond the standard set → add to Phase B's dispatch contract. +- Script behavior changes → mirror them in the "Anki Script Behavior" section. + +** Updates and Learnings + +*** 2026-05-30: First run +Built against =deepsat.org= after Craig flagged that the existing apkg surfaced PROPERTIES drawers + =*** Answer= headers on the back of every card, and that the person-card content (Vrezh in particular) had drifted. The Phase B subagent surfaced 8 high-confidence content updates plus several medium-confidence enrichments. Validated by running the rewrite and regenerating =deepsat.apkg= to =~/sync/phone/anki/=. -- cgit v1.2.3