Every Press Raven guide already names a working folder in ~/Developer, and the folders it names are registered Code Raven projects. Nothing reads that link in reverse, so Code Raven cannot tell that Data/rotations.json in the Stellar repo is a generated file, cannot route a content idea back to the corpus, and cannot file a Chronicle entry anywhere but the folder the terminal happened to be in.
This plan reads the existing join backwards and builds four things on it: awareness, a safety interlock, a return channel through Press Raven's existing staging door, and a Chronicle record that can belong to two projects at once.
The problem
The join exists and nobody reads it backwards. SessionLog.projectPath(for:) resolves any guide through a three-tier ladder — project_path, then machine_target.app, then the category's default_project_path. All 19 guides resolve through it. Four name a game or app repo; the remaining six categories default to ~/Developer/PressRaven-Corpus. Every one of those app repos is a registered Code Raven project, matching Project.path exactly after tilde expansion.
A content idea implemented in the project repo is scheduled for deletion. export-machine.py --verify computes drift symmetrically — on_disk(p) != data — but the only remedy it prints is --write, which overwrites, and MachineExportService.checkDrift renders the result as one amber arrow meaning "the corpus is ahead of the app." So an idea captured in Code Raven against Stellar and implemented by editing Data/rotations.json is destroyed by the next export, and the check meant to catch it misreports which side moved.
This is sharpened by SessionLaunchers.writesCorpusDirectly(_:): every guide except a game gets a contract telling the session to write corpus files directly. Game sessions open in the app repo instead — precisely the folder where a content idea gets implemented and lost.
Chronicle entries cannot say where the work belongs. ImplementationRecord.projectId is a single non-optional let, and /cr-entry derives the project from the working directory. Press Raven has no Chronicle of its own — its Swift contains zero references to .claude. Press Raven's Chronicle is Code Raven's Chronicle, scoped to the PressRaven project. Three failures follow:
~/Developer/PressRaven-Corpusis not a registered project and has no.claude/directory at all, so corpus work has nowhere to be recorded — even though six of the ten categories name it as their working folder.- Corpus work is therefore filed against the Press Raven app. "Framed Educational Games for Press Raven and Seeded a 35-Mechanic Catalog" and "Applied the First Post-Mortem to the Itinerary Framework" are both pure corpus work sitting in the app's chronicle.
- Dual-belonging work has no representation.
.claude/HANDOFF-2026-08-27.mdis a live case: four Peregrine mini-games read the player's SwiftData library via@Query, so authored corpus questions will not appear until the Peregrine app falls back to a bundled bank. Its own summary reads "THE BLOCKER IS NOT IN THIS REPO."
The drift is already measurable. MiniGameConfig.mechanic is a plain string, and the comment beside it says "Press Raven does not resolve cross-guide references, so a script validates these." No such script exists. place-the-mark.json lists instances: ["peregrine:museum-restoration (unbuilt, possibly absorbed)"] while Stellar's the-whiteboard declares mechanic: "place-the-mark" at build_status: "built". The back-reference is stale in exactly the direction this closes.
Approach
Read the existing three-tier ladder, not just machine_target
The obvious join is machine_target.app, and it covers four guides. Using it alone would leave the other fifteen — every field guide, itinerary, catalog and cheat sheet — invisible, and would silently disagree with Press Raven about where a guide's work happens. CorpusIndexService resolves through the same ladder SessionLog uses and records which tier answered in pathSource, so a disagreement is debuggable rather than mysterious.
Two-sided drift, because one-sided drift is the bug
--verify already detects that the two sides differ. What it cannot say is which one moved, and the remedy it offers assumes the corpus did. Comparing each drifted output file's mtime against the newest entry mtime under guides/<guide>/entries/ supplies the missing half, and an appAhead result gets a different remedy than corpusAhead — never a bare --write.
Enter through ImportService, not a new door
Press Raven already has one staging door for AI collaboration: the pressraven_return contract, with .new/.edit/.suggest/.note item kinds and a rule that nothing applies without explicit commit. Code Raven emits that same payload rather than a bespoke one, so every existing validation, staleness check and staging view applies unchanged. Press Raven gains one trigger, not one pipeline.
A secondary affiliation, not a second projectId
alsoProjectIds: [UUID]? is additive; the primary project stays the single owner and every existing query is untouched. A ChronicleCrossFileIndex sidecar — the same shape as the existing ChronicleArchiveMeta sidecar — makes the secondary lookup a dictionary hit rather than a scan across every loaded project.
Register the corpus as a project
Most of the Chronicle complaint dissolves here, with no new mechanism. Six of the ten categories already name ~/Developer/PressRaven-Corpus as their working folder; it simply has no .claude/ directory and no registration, so its history lands on the app.
Decisions made for you
Code Raven creates, never mutates
Code Raven may write new staged items into the corpus and nothing else — no edits to an existing entry, guide.json, or ledger file. Two writers over one file is how the Prime Directive gets quietly broken. Reversible in principle; the restriction lives in CorpusReturnWriter.
includeCrossFiled defaults to true for display, false for counting
Defaulting true is safe because the array is empty on every existing record, so nothing changes until something is cross-filed. Counting and rollups stay cross-file-blind so a record filed twice is never counted twice. Reversible by flipping one default.
The instance lint reports, it does not write
instances is derivable, and deriving it would destroy the editorial annotation it carries — "unbuilt, possibly absorbed" is a judgement no scan can reproduce. The guide's own rule is that costumes are authored there. The lint surfaces the difference and leaves the writing to a person.
Phase 0 is a phase, not an assumption
Three facts carry the plan and one of them — how a pressraven_return payload reaches ImportService without a human paste — is genuinely unknown. Writing Phase 3 before checking would mean guessing at another app's contract.
The work
Phase 0: Verify the joins before building on them
5 tasks Cheap, and the rest of the plan is wrong if any of it is.- Confirm Code Raven can read the corpus root from
UserDefaultssuitecom.lochlustra.Press-Raven, keycorpusRootPath, and confirm the same fallback chain the Python tools use (PRESSRAVEN_CORPUSenv → that key →~/Developer/PressRaven-Corpus→~/PressRaven) - Read
Services/ImportService.swiftanddocs/sample-session-return.mdand record exactly what a minimalpressraven_returnbatch containing one.newitem atstatus: seedmust look like - Determine how a
pressraven_returnpayload can reachImportServicewithout a human paste, given its three current triggers are ⇧⌘I, clipboard, and drop — and record whether a watched-folder trigger is a small addition or a structural one - Confirm
CorpusStore.createSeed(guideID:title:note:)(CorpusStore.swift:582) is reachable as the promotion path from a staged.newitem, including its slug de-duplication - Record which repo-relative output files each of the four
machine_targetguides owns, by readingoutputs[].filerather than by running the exporter
Phase 1: Code Raven learns the corpus exists
10 tasks Read-only. Lands independently and changes no existing behavior.- Add
Models/CorpusLink.swift—CorpusLink(guideId, guideTitle, categoryId, kind, corpusGuideURL, resolvedProjectPath, pathSource, machineOutputs),CorpusMachineOutput(file, miniGame, class, shape), andCorpusGuideKind(catalog/project/guide), all defensively decoded per CLAUDE.md - Add
Services/CorpusIndexService.swift— a@MainActor ObservableObjectwithstatic let sharedthat scans<corpusRoot>/guides/*/guide.jsonplus<corpusRoot>/categories/*/category.jsonand publisheslinksByProjectPath: [String: [CorpusLink]]andlinksByGuideId: [String: CorpusLink] - Resolve each guide's project path through the same three-tier ladder Press Raven uses —
project_path→machine_target.app→ the category'sdefault_project_path— and record which tier answered inCorpusLink.pathSource, so a mismatch with Press Raven is debuggable rather than mysterious - Read
kindfrom each category's owncategory.jsonrather than keying offcategoryID == "game", which is the coupling Press Raven already had to fix when a second project category appeared (framework.jsoncarries only display order and names, not the kind itself) - Normalize the join key in exactly one place — expand
~, resolve symlinks, strip trailing slashes on both the resolved corpus path andProject.path— so the match is exact rather than string-similar - Cache the index with mtime-keyed invalidation over the guides and categories directories, refreshing on
NSApplication.didBecomeActiveNotificationalongside the existing bridge scan - Add
corpusRootPathto Code Raven settings, defaulting to the Phase 0 chain, with a "corpus not found" state that disables every corpus feature rather than erroring - Add
Project.corpusLinksas a computed lookup throughCorpusIndexService, never a stored field, so the corpus stays the single source of truth for the mapping - Register
~/Developer/PressRaven-Corpusas a Code Raven project and pre-create its.claude/{entries,checkins,completions,summaries,bridge,plans}directories viaensureBridgePermissions()— the one-click affordance is built into Settings → Press Raven Corpus; the click itself is yours because a running app ownsprojects.jsonand writing it from outside would be clobbered on the app's next save - Verify the index resolves all 19 guides, that the four app-backed guides land on their registered projects, and that the 15 corpus-default guides land on the newly registered corpus project
Phase 2: The interlock — stop the silent loss before it happens
7 tasks The phase that pays for the plan.- Add
Services/CorpusOwnedPathGuard.swift— given a project, return the set of repo-relative paths owned bymachine_target.outputs[].file, and amatches(path:)test that matches by prefix as well as equality, sincePrimeSuspect/Resources/casesis a directory rather than a file - Flag machine-owned paths in the git working-tree surfaces: when a linked project shows a modified file the guard matches, surface an amber "generated from the corpus — this edit will be overwritten by the next export" marker naming the guide it comes from
- Flag machine-owned paths in plan and checkin flows: when
ImplementationRecord.impactedFilesor a checkin's file list contains a guarded path, attach the warning to the record and to the plan's workflow banner - Add
Services/CorpusDriftService.swiftimplementing two-sided drift detection — runexport-machine.py <guide> --verify, and when it reports drift, compare each drifted output file's mtime against the newest mtime underguides/<guide>/entries/to classify the direction ascorpusAhead,appAhead, orboth - Give
appAheadandbotha remedy distinct fromcorpusAhead— never offer--writeas the only action when the app side moved, and instead offer "capture this edit back into the corpus," routing into Phase 3 - Surface drift state on a linked project's dashboard as a compact strip: guide name, drift direction, machine-owned file count, last verified timestamp
- Route drift checks through the Workbench
BackgroundActivityPillrather thanNotificationService.notify, per the established convention for automatic background activity
Phase 3: The return channel — content ideas reach the corpus
18 tasks Touches both repos.- Record the Phase 0 intake-trigger decision here with its reasoning before implementing, so the choice is reviewable rather than assumed
- Add
Models/CorpusReturnBatch.swift— a Swift encoder for thepressraven_returncontract emitting.newitems, and deliberately omitstatus, sinceImportService.applyoverrides it toseedordraftedregardless of what the return claims - Populate each item's required schema fields from the
CorpusLink—titlealways, plusfamily(the mini-game id) andclassfrommachine_target.outputs[]forgameandappguides — and block the send with a clear reason rather than emitting an item that will stage as INVALID - Handle the four outputs that declare no
classby asking rather than guessing, sincestellarroster/spots/cycle andperegrineundercover leave it unset - Press Raven: add
ProvenanceRecord.codeRaven = "code-raven"beside the three existing sources, and thread asource:parameter throughImportService.applyin place of its hardcodedsessionImport - Carry the Code Raven source kind, source UUID, and originating project name in the provenance record's
note, rather than as top-level entry keys that would surface asunknownKeysnoise on every item - Add
Services/CorpusReturnWriter.swift— writes one batch file atomically into the corpus intake location, and never touches an existing corpus file - Place the intake outside the FSEvents-watched corpus root, or give
CorpusStore.handleExternalChangesan explicit branch for it, so a send does not trigger a fullreloadAll()across 19 guides and roughly 2,000 entries - Add a "Send to Corpus…" action on
ScratchNoteandQuickTaskcontext menus (NotesWidget, ScratchpadView, TasksWidget), enabled only when the item's project has at least oneCorpusLink, presenting a guide picker seeded with that project's own guide — **Spark deferred**: sparks are app-global with no project, and the Spark Inbox exists for cross-app product triage rather than corpus content, so wiring it would need a project picker for a surface whose purpose is different - Reuse
Views/Components/MoveToProjectSheet.swift's shape for the guide picker — **deviated**: a dedicatedSendToCorpusSheetwas built instead. The interaction is not identical in kind after all; the sheet has to resolve schema requirements, offer family and class values, warn about a guide that cannot accept a new entry, and report where the batch landed.MoveToProjectSheetpicks one thing from a list - Write provenance back onto the source item — a
sentToCorpusAttimestamp and the resulting batch id — so an idea cannot be silently sent twice and Code Raven can show where it went - Add
Resources/Commands/cr-corpus.mdand its global~/.claude/commands/copy — a batch classifier that reads unrouted content-shaped notes and tasks for linked projects, proposes a target guide and entry class for each, and emits apressraven_returnbatch; modeled on the proven/cr-sparkrouting pattern - Register
cr-corpusinCommandInstaller.commandsso it installs and version-stamps with the rest - Press Raven: give
ImportServicea fourth trigger that picks up a waiting return batch by calling the existingAppMainView.importText(_:), reusing its parse, validate, and stage path unchanged - Surface waiting items as a non-modal notice the author clicks rather than auto-presenting the staging sheet, since all three existing triggers are human-initiated and the sheet is modal
- Press Raven: render a Code Raven originated staged item with a visible provenance chip naming the originating project, so a seed never masquerades as authored content
- Let accept run through
ImportService.apply's existing.newpath unchanged — it already sets envelope defaults, normalizeslist, strips the human-onlypick/lockflags, de-duplicates the slug, and appends provenance, none of whichCorpusStore.createSeeddoes - Press Raven: on decline, persist the decision so the same item is not re-proposed — **not built, and deliberately**:
CorpusSendLogon the Code Raven side already prevents an item being sent twice, so a second declined-id store in Press Raven would be a duplicate source of truth for the same fact. A cancelled batch stays in the inbox rather than being retired, which is the only state that needed handling
Phase 4: Chronicle routing — entries that know where they belong
13 tasks Independent of Phases 2–3.- Give the corpus its own chronicle — with
~/Developer/PressRaven-Corpusregistered in Phase 1, corpus work stops being misfiled as Press Raven app work with no further mechanism - Teach
/cr-entryto classify by git path rather than by working directory alone: it already runsgit diff --name-only, so files underguides/<g>/entries/route to the corpus, files underPress Raven/route to the app, and files under a linked project route to that project - Have
/cr-entrystate its routing decision in one line before writing, matching how it already announces checkin-versus-entry, so a wrong route is correctable in the moment rather than discovered weeks later - Add
ImplementationRecord.alsoProjectIds: [UUID]?— a secondary affiliation, additive and defensively decoded, never a secondprojectId; the primary stays the single owner and the SSOT is unchanged - Add
Services/ChronicleCrossFileIndex.swift— a persisted sidecar mappingsecondaryProjectId → [(recordId, primaryProjectId)], so a secondary lookup is a dictionary hit plus a targetedensureLoaded(primary)rather than a scan across every loaded project - Extend
ChronicleManager.recordsForProject(_:includeArchived:)withincludeCrossFiled: Bool = true, safe to default true because the array is empty for every existing record, so nothing changes until something is cross-filed - Leave counting and rollup queries cross-file-blind, so a record filed into two projects is never counted twice in
totalRecordCountorgenerateRollup - Render a cross-filed record with a provenance chip naming its primary project, so it reads as borrowed context rather than native history
- Add
alsoProjectIdstoEntryBridge,CheckinBridge, andCompletionBridgewith defensive decoding, and populate the cross-file index fromEntryBridgeImporter - Teach
/cr-entryto propose a cross-file when the session touched both a catalog guide and a project that instantiates it — for example aguides/game-mechanics/entries/*.jsonedit together with a game'sguide.jsonor its repo - Have
/cr-entrynameMiniGameConfig.divergenceas the corpus-side counterpart when it proposes such a cross-file, since that field is already the declared channel for feeding a build's departures back up into the catalog - Add a "File also in…" action on
ChronicleRecordEditSheetfor retroactive cross-filing, reusing theMoveToProjectSheetpattern - Add a
ChronicleMigrationversion that backfills nothing and only asserts the new field decodes on existing records, so the additive change is proven safe rather than assumed
Phase 5: Surfacing and the validator Press Raven already said it needs
7 tasks Polish, plus a gap Press Raven names in a code comment.- Add a Corpus strip to a linked project's dashboard — backing guide, drift direction, machine-owned file count, pending seed count — visible only when a
CorpusLinkexists - Add the reverse view on the corpus project — which projects consume which guides, and which mini-games instantiate which catalog mechanics
- Add the cross-guide reference validator: resolve every
mini_games[].mechanicand.themeagainstgame-mechanicsandgame-themesentry ids, and report any that name a missing entry - Add an instance back-reference lint that compares each catalog entry's hand-maintained
instancesarray against the mini-games actually declaring that mechanic, and reports the difference - Report the lint's findings rather than auto-writing
instances, since that field carries editorial annotation — "unbuilt, possibly absorbed" — that a derived list would destroy, and the guide's own rule is that costumes are authored there - Update
.claude/context/blueprints/cli-bridge.mdandchronicle.mdfor the new bridge, the cross-file index, and thealsoProjectIdsfield - Add a
corpus-integration.mdblueprint and register it in.claude/context/blueprints/index.md
Verification
6 tasks Claude runs these.- Build Code Raven clean:
xcodebuild -scheme "Code Raven" -destination 'platform=macOS' build - Verify the build product rather than trusting the log:
scripts/verify-build-product.sh - Build Press Raven clean:
xcodebuild -project "Press Raven.xcodeproj" -scheme "Press Raven" build - Run the RavenKit test suite:
cd RavenKit && swift test - Confirm
CorpusIndexServiceresolves all 19 guides through the three-tier ladder with tilde and symlink normalization applied, and thatpathSourcematches whatSessionLog.projectPath(for:)would answer for each - Confirm an
ImplementationRecordwritten before this plan still decodes withalsoProjectIdsabsent
Manual Testing
10 tasks Yours — GUI verification cannot be driven from this terminal.- Verify the Corpus strip appears on Stellar, Peregrine, Hopper and Drive Corps, and does not appear on CodeRaven or VentureRaven yours
- Hand-edit a machine-owned file in the Stellar repo and confirm Code Raven classifies the drift as app-ahead and does not offer
--writeas the remedy yours - Send a Code Raven note against Stellar to the corpus, and confirm it arrives in Press Raven's import staging with a Code Raven provenance chip and nothing written to disk yet yours
- Commit that staged item and confirm it becomes a real entry at
status: seedwith a de-duplicated slug yours - Decline a different item and confirm it is not re-proposed on the next run yours
- Run
/cr-entryfrom~/Developer/PressRaven-Corpusand confirm the record lands in the corpus chronicle rather than the Press Raven app chronicle yours - Run
/cr-entryafter a session touching both a mechanic catalog entry and a game's guide, and confirm the proposed primary and secondary projects match your intent yours - Confirm a cross-filed record appears in both chronicles, carries a provenance chip in the secondary, and increments the session count in only one yours
- Review the cross-guide validator output and confirm it flags
place-the-markas missing Stellar'sthe-whiteboardyours - Confirm
git statusin the corpus shows no file modified by Code Raven other than newly created staged intake yours
Risks & edge cases
The intake trigger may not be small
ImportService's three triggers are all human-initiated: ⇧⌘I, clipboard, and drop on the document. A watched folder may be a few lines beside CorpusWatcher, or it may cut against a deliberate design that every import is a decision someone made. Phase 0 checks before Phase 3 commits, and the fallback — leaving the batch on the clipboard for a ⇧⌘I — costs one keystroke and no architecture.
mtime is a proxy, not proof, of which side moved
A git checkout, a clone, or a file copied rather than edited resets mtimes and can misclassify drift direction. The classification is a warning, not a gate, and both directions remain visible — but a confidently wrong appAhead could make someone hesitate over a legitimate export. Content hashing against the last known export would be firmer and is not in this plan.
Path normalization is the whole join
The index is one string comparison away from returning nothing. ~ expansion, symlinks, trailing slashes, and the space in ~/Developer/Drive Corps all have to be handled in one place. A silent empty index looks identical to "this project has no corpus link," which is why Phase 1 ends with an explicit verification that all 19 guides resolve.
What counts as "content-shaped" is unspecified
Phase 3 gives every note in a linked project a "Send to Corpus…" action and leaves the judgement to the user, with /cr-corpus offering a batch classification. There is no automatic detection, and there probably should not be one until the manual path has been used enough to show a pattern.
Code Raven starts depending on another app's file formats
Reading guide.json, category.json and framework.json means a Press Raven schema change can break Code Raven's index. Defensive decoding keeps it from crashing, but it can degrade to silence. The mitigating fact is that these files are the corpus's public surface — Python tools already read them the same way — and the Prime Directive commits Press Raven to keeping them plain.
Two repos, one plan
Phases 3 and 5 change Press Raven. If Press Raven work is happening in parallel, these tasks will need coordination. Phases 1, 2 and 4 are Code Raven only and carry most of the value, which is why they are ordered first.
Out of scope
- Two-way sync of corpus entries. Code Raven creates staged items and never edits existing corpus content. Making Code Raven a second writer would put the Prime Directive in contest with itself.
- Rewriting
export-machine.py's projections. The ~30shapefunctions stay app-specific and stay in Press Raven. This plan reads--verifyand interprets its result; it does not change what the exporter writes. - Extracting the corpus index into RavenKit. RavenKit's stated rule is that it holds only what two or more Raven apps demonstrably duplicate, and today it is consumed by Press Raven alone. The index is used by one side, so extraction now would be speculative.
- VentureRaven. The same asymmetry may exist there, but nothing in the reported problem involves it, and the Spark Inbox already covers CR↔VR routing.
- The Peregrine
@Queryblocker named in.claude/HANDOFF-2026-08-27.md. This plan makes that class of problem visible and cross-filable; it does not fix that specific one, which lives in the Peregrine app. - Migrating existing Press Raven entries into the corpus chronicle. They are readable where they are; retroactive re-filing is a separate judgement call, offered case by case through the Phase 4 "File also in…" action.
Final review
What it does well. It builds on facts already in the data rather than asking for new declarations. No guide gains a field, no project gains a config file, and the mapping stays owned by the corpus. Phase 1 changes nothing observable, Phase 2 delivers the safety value on its own, and Phase 4 solves most of the Chronicle complaint by registering one directory. Where Press Raven already has a vocabulary — the pressraven_return contract, MiniGameConfig.divergence, CategoryDecisions — the plan speaks it rather than adding a synonym.
Where it is weakest. Phase 3 is the least certain part and the most user-visible, and its shape depends on a Phase 0 answer nobody has yet. The two-sided drift classification rests on mtime, which is a proxy. And the plan assumes the corpus schema stays stable enough for a second reader, which is true today by policy rather than by contract.
What I would adjust if pushed. Two things. First, Phase 4's first two tasks — register the corpus, teach /cr-entry to route by git path — could ship before Phase 1 entirely and would fix the reported Chronicle pain within an hour. If the goal is relief rather than architecture, invert that order. Second, alsoProjectIds plus a sidecar index is real complexity for a case that may arise a few times a month; a simpler first cut is to let /cr-entry write two linked records with a shared continuedFromRecordId, which needs no model change at all. I did not choose that because two records drift independently and the user's example — a framework change that is also a game change — is one piece of work, not two.
Where it may be inadequate. If the real pattern is that the user wants to work in one place and have it land in both, rather than record afterwards where it landed, then this plan solves the bookkeeping and leaves the workflow untouched. Nothing here changes where a session opens or what it can reach. That is a larger question and this plan does not answer it.
High confidence in Phases 1, 2 and 4 — the join is verified against real data, the failure modes were reproduced rather than inferred, and the Chronicle change is additive. The deduction is almost entirely Phase 3: the intake trigger is an open question about another app's deliberate design, and the ordering argument in the final review (ship the corpus registration first) is strong enough that I would expect the plan to be resequenced before it is executed.