66/76 done · 6 phases · built + verified; yours to test · 2026-09-01

Press Raven ↔ Code Raven: Two-Way Corpus Integration

Press Raven resolves every guide to a folder in ~/Developer; Code Raven has never read that link backwards. This builds the reverse index, stops generated files being edited into oblivion, opens a return channel for content ideas, and lets a Chronicle entry belong to two projects.

66Done
76Tasks
6Phases
60Implementation
10Yours to test
19Guides indexed
The gist

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:

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

Judgment call

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.

Judgment call

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.

Judgment call

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.

Judgment call

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 UserDefaults suite com.lochlustra.Press-Raven, key corpusRootPath, and confirm the same fallback chain the Python tools use (PRESSRAVEN_CORPUS env → that key → ~/Developer/PressRaven-Corpus → ~/PressRaven)
  • Read Services/ImportService.swift and docs/sample-session-return.md and record exactly what a minimal pressraven_return batch containing one .new item at status: seed must look like
  • Determine how a pressraven_return payload can reach ImportService without 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 .new item, including its slug de-duplication
  • Record which repo-relative output files each of the four machine_target guides owns, by reading outputs[].file rather 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), and CorpusGuideKind (catalog/project/guide), all defensively decoded per CLAUDE.md
  • Add Services/CorpusIndexService.swift — a @MainActor ObservableObject with static let shared that scans <corpusRoot>/guides/*/guide.json plus <corpusRoot>/categories/*/category.json and publishes linksByProjectPath: [String: [CorpusLink]] and linksByGuideId: [String: CorpusLink]
  • Resolve each guide's project path through the same three-tier ladder Press Raven uses — project_path → machine_target.app → the category's default_project_path — and record which tier answered in CorpusLink.pathSource, so a mismatch with Press Raven is debuggable rather than mysterious
  • Read kind from each category's own category.json rather than keying off categoryID == "game", which is the coupling Press Raven already had to fix when a second project category appeared (framework.json carries 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 and Project.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.didBecomeActiveNotification alongside the existing bridge scan
  • Add corpusRootPath to 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.corpusLinks as a computed lookup through CorpusIndexService, never a stored field, so the corpus stays the single source of truth for the mapping
  • Register ~/Developer/PressRaven-Corpus as a Code Raven project and pre-create its .claude/{entries,checkins,completions,summaries,bridge,plans} directories via ensureBridgePermissions() — the one-click affordance is built into Settings → Press Raven Corpus; the click itself is yours because a running app owns projects.json and 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 by machine_target.outputs[].file, and a matches(path:) test that matches by prefix as well as equality, since PrimeSuspect/Resources/cases is 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.impactedFiles or 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.swift implementing two-sided drift detection — run export-machine.py <guide> --verify, and when it reports drift, compare each drifted output file's mtime against the newest mtime under guides/<guide>/entries/ to classify the direction as corpusAhead, appAhead, or both
  • Give appAhead and both a remedy distinct from corpusAhead — never offer --write as 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 BackgroundActivityPill rather than NotificationService.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 the pressraven_return contract emitting .new items, and deliberately omit status, since ImportService.apply overrides it to seed or drafted regardless of what the return claims
  • Populate each item's required schema fields from the CorpusLink — title always, plus family (the mini-game id) and class from machine_target.outputs[] for game and app guides — 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 class by asking rather than guessing, since stellar roster/spots/cycle and peregrine undercover leave it unset
  • Press Raven: add ProvenanceRecord.codeRaven = "code-raven" beside the three existing sources, and thread a source: parameter through ImportService.apply in place of its hardcoded sessionImport
  • 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 as unknownKeys noise 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.handleExternalChanges an explicit branch for it, so a send does not trigger a full reloadAll() across 19 guides and roughly 2,000 entries
  • Add a "Send to Corpus…" action on ScratchNote and QuickTask context menus (NotesWidget, ScratchpadView, TasksWidget), enabled only when the item's project has at least one CorpusLink, 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 dedicated SendToCorpusSheet was 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. MoveToProjectSheet picks one thing from a list
  • Write provenance back onto the source item — a sentToCorpusAt timestamp 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.md and 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 a pressraven_return batch; modeled on the proven /cr-spark routing pattern
  • Register cr-corpus in CommandInstaller.commands so it installs and version-stamps with the rest
  • Press Raven: give ImportService a fourth trigger that picks up a waiting return batch by calling the existing AppMainView.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 .new path unchanged — it already sets envelope defaults, normalizes list, strips the human-only pick/lock flags, de-duplicates the slug, and appends provenance, none of which CorpusStore.createSeed does
  • Press Raven: on decline, persist the decision so the same item is not re-proposed — **not built, and deliberately**: CorpusSendLog on 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-Corpus registered in Phase 1, corpus work stops being misfiled as Press Raven app work with no further mechanism
  • Teach /cr-entry to classify by git path rather than by working directory alone: it already runs git diff --name-only, so files under guides/<g>/entries/ route to the corpus, files under Press Raven/ route to the app, and files under a linked project route to that project
  • Have /cr-entry state 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 second projectId; the primary stays the single owner and the SSOT is unchanged
  • Add Services/ChronicleCrossFileIndex.swift — a persisted sidecar mapping secondaryProjectId → [(recordId, primaryProjectId)], so a secondary lookup is a dictionary hit plus a targeted ensureLoaded(primary) rather than a scan across every loaded project
  • Extend ChronicleManager.recordsForProject(_:includeArchived:) with includeCrossFiled: 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 totalRecordCount or generateRollup
  • Render a cross-filed record with a provenance chip naming its primary project, so it reads as borrowed context rather than native history
  • Add alsoProjectIds to EntryBridge, CheckinBridge, and CompletionBridge with defensive decoding, and populate the cross-file index from EntryBridgeImporter
  • Teach /cr-entry to propose a cross-file when the session touched both a catalog guide and a project that instantiates it — for example a guides/game-mechanics/entries/*.json edit together with a game's guide.json or its repo
  • Have /cr-entry name MiniGameConfig.divergence as 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 ChronicleRecordEditSheet for retroactive cross-filing, reusing the MoveToProjectSheet pattern
  • Add a ChronicleMigration version 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 CorpusLink exists
  • 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[].mechanic and .theme against game-mechanics and game-themes entry ids, and report any that name a missing entry
  • Add an instance back-reference lint that compares each catalog entry's hand-maintained instances array 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.md and chronicle.md for the new bridge, the cross-file index, and the alsoProjectIds field
  • Add a corpus-integration.md blueprint 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 CorpusIndexService resolves all 19 guides through the three-tier ladder with tilde and symlink normalization applied, and that pathSource matches what SessionLog.projectPath(for:) would answer for each
  • Confirm an ImplementationRecord written before this plan still decodes with alsoProjectIds absent

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 --write as 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: seed with a de-duplicated slug yours
  • Decline a different item and confirm it is not re-proposed on the next run yours
  • Run /cr-entry from ~/Developer/PressRaven-Corpus and confirm the record lands in the corpus chronicle rather than the Press Raven app chronicle yours
  • Run /cr-entry after 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-mark as missing Stellar's the-whiteboard yours
  • Confirm git status in the corpus shows no file modified by Code Raven other than newly created staged intake yours

Risks & edge cases

Unknown

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.

Edge case

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.

Edge case

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.

Thin

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.

Coupling

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.

Scope

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

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.

78/100

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.