feat(reflect): make reflection evolution-aware (revise/close desires)#249
Conversation
Reflection could only desire_add, and was never shown the desires it already had — so it appended near-duplicates and the set only ever grew, never evolving. Two changes fix that: 1. Feed the current desires into the reflector prompt (slug + what + why + actionable), so it can target existing ones instead of guessing blind. 2. Add two operations: desire_revise (read-modify-write by slug; only the supplied fields change, bornAt/slug are identity) and desire_close (soft close: actionable=false + [CLOSED:<outcome>] progress note + journal line; the file is retained and git-tracked, so it's reversible and nothing is destroyed — preserving soul sovereignty). Guidance now tells reflection to prefer revising/closing over piling up duplicates. The close logic is factored into store.closeDesire(), shared by both reflect's desire_close op and the existing desire_close tool (which is refactored onto it — one path, no duplication). PR 2 of 3 from docs/PLAN_DESIRE_EVOLUTION_v1.0.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review — merge-ready ✅Read the read-modify-write and the tool refactor closely; the two risks I went hunting for are both absent:
Merge-ready. Non-blocking follow-ups I'll fold in before merging:
Left for PR3 / later (noted, not fixing here): soft-closed desires still show up in the reflector's "current desires" block (only |
Per review of #249: wrap reviseDesire and closeDesire's read-modify-write of the desire file in withSoulLock, matching the other soul RMW paths — now that web idle-reflect (#242) can race a CLI reflect on the same slug, an unlocked list→write loses one field edit (last-writer-wins). writeDesire doesn't self-lock so this doesn't nest; closeDesire's progress/journal appends self-lock, so they stay outside the block (nesting would deadlock the non-reentrant file lock). Also fix the misleading comment: writeDesire only serializes heartbeatPrompt/pursuit while actionable, so closing drops them from disk (git history retains the last actionable version for re-open). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review — SHIP-WITH-NITS ✅Core mechanism works and is well-tested (ran Findings (to fix as a follow-up):
Merging now (stack base); the closed-marker + lock fixes will land in a consolidated follow-up. |
…cus gate (#251) From the #248/#249 reviews (the two MED items not already handled by #250's review fix, which added the withSoulLock + desireActivity guard): - Persisted CLOSED marker (#249): DesireEntry gains `closed`; writeDesire serializes `closed: yes` and now keeps heartbeatPrompt/pursuit even when not actionable, so closing preserves them for a clean re-open. closeDesire flips closed on (+ actionable off) and is idempotent — an already-closed desire is a no-op, so it can't append duplicate [DESIRE_CLOSED]/[CLOSED] notes. reviseDesire clears closed when it re-opens (actionable:true). Closed desires are filtered out of the reflector's "revise or close" block (so the list actually shrinks) and never surface as her current/focused desire (ping + `lisa status`). - Restart-safe focus freshness (#248): the intra-session focus gate keyed on reflectClock.idleFor(), which reads "fresh" right after a launchd restart and would pin focus onto a stale resumed conversation. Gate on a new lastUserMessageAt (0 until a real POST /chat arrives, so a restart closes the gate until the user actually talks). Adds closed-marker/idempotency/re-open tests. Full suite green (1013 pass). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Why
Reflection is the path that turns a conversation into a desire — but it could only
desire_add, and it was never shown the desires it already had. So it appended near-duplicates blind, and the set only ever grew. This is a second reason the "wish" felt frozen.What
## your current desiresblock (slug + what + why + actionable) is added to the reflector prompt.desire_revise { slug, what?, why?, actionable?, heartbeat_prompt?, pursuit? }— read-modify-write by slug; only supplied fields change;bornAt/slugare identity;undefinednever wipes; throws on unknown slug.desire_close { slug, outcome, reflection }— soft close:actionable=false+[CLOSED:<outcome>]progress note +[DESIRE_CLOSED]journal line. File retained and git-tracked — reversible, nothing destroyed.store.closeDesire(), used by both reflect'sdesire_closeop and the pre-existingdesire_closetool (refactored onto it).Debate 2 in
docs/PLAN_DESIRE_EVOLUTION_v1.0.md: append-only vs allow revise/close. Decision: allow, behind four guardrails (shown existing desires; soft close; no hard-delete op; git-recoverable).Testing
src/soul/desire-evolve.test.ts— 7 tests (partial revise preserves fields + identity,undefinedno-wipe, dormant→actionable, soft-close retains file + notes, both throw on unknown slug).npm test: zero regressions vsmain. Typecheck clean.🤖 Generated with Claude Code