MiraHandoff carries one fact about your relationship to a piece of work: sentAt. Add a disposition beside it — held, sent, done, dropped — and switch every counting site from “not sent” to “still open”. The vocabulary is lifted from TriageLifecycleState, which already solved this for findings.
The problem
MiraHandoff has sentAt: Date? and the derived
isSent, and that is the entire state machine. Five call sites read it:
handoffSubtitle (MiraLedPaneView:325), openReports (:80),
finishedReports (:86), openHandoffCount
(MiraHandoffStore:73), and handoffContextSummary (:91).
Sending is therefore the only transition available. Fourteen unsent handoffs sit across three reports; a handoff read in August and decided against is indistinguishable from one never opened. Two reports read “5 of 5 still to send” and will keep reading that indefinitely, which makes the count worth ignoring — and a count worth ignoring is worse than no count.
Approach
disposition counts, sentAt timestamps
Both fields stay. isSent keeps its current meaning
(sentAt != nil) and keeps gating the editing affordances —
checkbox toggles, the notes editor, the “Send again” label. Every site that
reads isSent to count switches to disposition;
nothing that reads it for editability changes.
The two can disagree, and that is the intent. A handoff sent in July and
abandoned in August is .dropped with a non-nil sentAt.
The timestamp is history; the disposition is status.
Four cases, one of them open
held is the default and the only state that counts.
sent, done and dropped all drain.
dropped is the overridden analogue from
TriageLifecycleState: never counted, never resurfaced, and not
listed back to Mira in her context summary.
Migration is one line in the existing decoder
MiraHandoff already hand-writes init(from:) with
decodeIfPresent on every field. The new one defaults to
sentAt != nil ? .sent : .held, so the fourteen stored handoffs land
in the right state with no migration pass and no version stamp.
Two counting sites the note did not list
openReports and finishedReports (MiraLedPaneView
lines 78–86) also partition on isSent. Left alone, a report
whose handoffs are every one of them dropped would sit in the open list forever
with the subtitle reading “nothing open”. Phase 3 changes both.
Decisions made for you
“Send again” will not un-finish a done handoff
markSent promotes to .sent only from
.held. Setting it unconditionally would mean re-opening a completed
prompt to copy it quietly reverted the state you had recorded. Reversible by
dropping the guard.
Dropping is confirmed rather than reversible
The note asks for dropped to behave like overridden:
gone, permanent. Strict permanence and a mis-click are a bad combination, so the
menu offers no revert and a confirmationDialog stands in for one.
Deleting the report remains the escape hatch.
Marking done retires findings; dropping does not
Sending already retires the source findings as .accepted. Marking
a never-sent handoff done should do the same, or work you finished keeps feeding
the triage queue. Dropping deliberately does not: coveredFindingKeys
already includes a dropped handoff’s keys, so regeneration will not re-cluster
them, and the findings stay individually visible in the raw-findings section.
Rejecting Mira’s framing of the work is not the same as silencing the
observations behind it.
Dropped handoffs are omitted from Mira’s context, not counted
Even a bare count invites her to raise what you silenced. Done gets a count line; dropped gets nothing.
The work
Phase 1 — The model
5 tasks Lands alone. Nothing reads the new field yet.- Add
MiraHandoffDispositionenum toCode Raven/Models/MiraHandoff.swiftaboveMiraHandoff, with casesheld,sent,done,dropped, conforming toString, Codable, CaseIterable - Give the enum
label,accent,systemImage,countsAsOpen(true only forheld), andisTerminal(true fordoneanddropped), matching the shape ofMiraHandoffConfidencein the same file - Add
var disposition: MiraHandoffDispositiontoMiraHandoffbesidesentAt, with= .heldin the memberwiseinitsoMiraHandoffGenerator.parseneeds no change - Add
dispositiontoMiraHandoff.CodingKeysand decode it in the hand-writteninit(from:), defaulting tosentAt != nil ? .sent : .heldso the fourteen stored handoffs load with the right state - Add
var isOpen: Bool { disposition.countsAsOpen }toMiraHandoffso no call site re-derives the rule
Phase 2 — The store
4 tasks Depends on Phase 1.- Add
setDisposition(_:handoffId:reportId:projectId:)toMiraHandoffStoreimmediately aftermarkSent, routing through the privatemutateHandoff - Have
markSentsetdisposition = .sentonly when the current disposition is.held, so “Send again” on a done or dropped handoff does not silently un-finish it - Change
openHandoffCountto.filter { $0.isOpen } - Rewrite
handoffContextSummaryto bucket by disposition: held listed by title, sent listed by title, done as a bare count line, dropped omitted entirely
Phase 3 — The counting surfaces
2 tasks This is the part that makes the pane drain.- Change
MiraLedPaneView.handoffSubtitleto count$0.isOpen, reading “N of M open” and “M handoffs · nothing open” - Change
openReportsandfinishedReportsto partition onisOpen, so a report whose handoffs are all dropped moves into the finished list
Phase 4 — Surfacing it
6 tasks Depends on Phase 3. A quiet mark, not a badge cluster.- Add a
dispositionMark(_:)view toMiraHandoffReportView— icon plus label in the disposition’s accent, no capsule — placed incardHeaderleft of the confidence badge, rendered only when the disposition is not held - Change the card’s trailing
.opacityto read from disposition: 1.0 held, 0.62 sent and done, 0.45 dropped - Add a
MenutosendRowbeside the Send button offering “Mark as done”, “Drop this”, and “Back to open”, omitting entries the current disposition makes meaningless - Gate “Drop this” behind a
confirmationDialogstating that dropping is permanent, since the state has no reverse - Call
retireSourceFindings(of:)when a held handoff is marked done directly, and skip it when the handoff was already sent - Leave source findings untouched when a handoff is dropped, with a comment recording why
Verification
3 tasks Claude’s, before handing back.- Build with
xcodebuild -scheme "Code Raven" -destination 'platform=macOS' build - Confirm the build product is fresh with
scripts/verify-build-product.sh - Grep for any remaining
!$0.isSentinCode Raven/Views/Mira/to confirm no counting site was missed
Manual testing
7 tasks Yours — the pane cannot be driven from a terminal here.- Open the Mira-led pane and confirm the three existing reports list with open counts rather than “5 of 5 still to send”you
- Mark one held handoff done and confirm the subtitle decrements immediatelyyou
- Drop one held handoff, confirm the dialog appears, and confirm the card fades and the count decrementsyou
- Quit and relaunch, then confirm both dispositions survived the round-trip to diskyou
- Drain every handoff in one report and confirm it moves from the open list into the finished listyou
- Ask Mira what you are working on and confirm she never names the dropped handoffyou
- Press “Send again” on a handoff already marked done and confirm it stays marked doneyou
Risks & edge cases
Two fields, one concept
sentAt and disposition both describe sending. The
plan assigns each a job and keeps them consistent through markSent,
but a future call site that sets sentAt directly would desynchronise
them with no compiler complaint. Collapsing sentAt into an associated
value on .sent would rule that out, at the cost of rewriting the
decoder and every isSent read.
A held pile still has no time dimension
Fourteen handoffs held today become fourteen held in November. Nothing in this version distinguishes “decided against, never marked” from “not read yet”. Fading is the answer and it is deliberately deferred; the signal to build it is the held list becoming unreadable again.
openHandoffCount has no callers
Its doc comment says it drives the pane badge. A grep across the project finds no reference. Phase 2 fixes it anyway because leaving one counting function on the old rule is how the next surface picks up the bug — worth knowing that fixing it changes nothing visible.
Runtime behaviour cannot be checked here
GUI verification is not available from the terminal in this project. Every claim in the verification phase is a compile check; the manual testing list is the only thing that confirms the pane actually drains.
Out of scope
No decay on held
Findings have fading and fadeThreshold, and a handoff
held since August is arguably the same state. Add it only if the pile stops being
readable without it.
No un-drop
dropped matches overridden. The confirmation dialog is
what protects against a mis-click; deleting the report is the only reversal.
No per-disposition filter or sort in the report body
Dropped cards stay in place, faded. The report is the record of what Mira proposed, and hiding part of it makes the record lie.
Final review
The work is small and the shape is borrowed rather than invented, which is
where most of the confidence comes from. TriageLifecycleState already
proved this vocabulary in the same subsystem, the decoder already has the
defaulting pattern the migration needs, and mutateHandoff already does
the persistence. Seventeen implementation tasks across four files, and no data
migration.
The weakest part is keeping sentAt alongside disposition.
It is the right call for this change — folding the timestamp into the enum
would touch every isSent read for no user-visible gain — but it
leaves two fields describing one concept, held in sync by convention rather than by
the type system. If a third thing ever writes sentAt, the counts drift
silently.
The adjustment worth considering is whether sent should count as
open. It currently does not, and this plan preserves that. A handoff sent to Claude
Code and never followed up on is arguably still your problem, and it would show up
in a “3 open” count if it did. Leaving it out keeps the change to the
stated scope; if the sent pile turns out to hide unfinished work, the fix is one
line in countsAsOpen.
High on the mechanism: the enum, the decoder default, and the store method are all copies of patterns already working in this subsystem, and the two extra counting sites the note missed are found and included. Held back from higher by the sentAt / disposition redundancy, which is correct for this change but leaves a consistency invariant enforced by convention, and by the UI half being unverifiable from here.