feat: Week 2 — brain/emotion package (7 modules)#2
Merged
Conversation
8 tasks, 54 bite-sized checkbox steps, TDD throughout. Builds the brain/emotion/ package per spec Section 5 (P1 — the emotional core as organising principle). Scope: 7 sub-modules in the package — - vocabulary (26-baseline Emotion taxonomy + persona extensions) - state (EmotionalState + ResidueEntry with clamping/dominant/residue) - decay (per-emotion half-life, spec-pinned grief=60d joy=3d identity=None) - arousal (7-tier spectrum from state + body temperature) - blend (emergent co-occurrence detection, pairs + triples, ≥5 threshold) - influence (state → structured biasing hints for providers, Week 5) - expression (state → 24 facial + 8 arm/hand params, NellFace Week 6) Integration smoke in Task 8 exercises all seven modules composed. Expected test count post-Week-2: 107 (Week 1's 38 + 69 new). Out of scope (deferred): bridge provider integration (Week 5), voice module (Week 4), engines (Week 4), memory + migrator (Week 3). Execution via superpowers:subagent-driven-development. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…istry Ships the baseline 26-emotion taxonomy (11 core + 10 complex + 5 Nell-specific) with typed Emotion dataclass (name, description, category, half-life, clamp). Half-lives per spec Section 10.1: grief=60d, joy=3d, anchor_pull/love/belonging/ body_grief/freedom_ache=None (identity-level); others seeded for later tuning. register()/_unregister() support per-persona extensions. 13 tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Task 1 test file has 13 test functions; I had "12" in several places in the plan and its cascading "expected: N passed" counts downstream. Implementer caught this in DONE_WITH_CONCERNS; amended their commit message to say "13 tests green" and corrected the plan throughout: - Task 1: 12 → 13 - Task 1 cumulative: 50 → 51 - Task 2-7 cumulative: each +1 (62, 70, 79, 88, 97, 108) - Task 8 green-light total: 107 → 108 - Task 8 PR body: 69 new tests → 70 new tests Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
From the code quality reviewer's findings: Important: - Emotion.category typed as Literal["core", "complex", "nell_specific", "persona_extension"] via EmotionCategory type alias. Edit-time typo catching via mypy/pyright; by_category() silent-empty-on-typo stops being a silent footgun. No runtime check added (YAGNI). - intensity_clamp: int → float (default 10.0). Prevents silent int/float coercion when Task 2 state.py compares float intensities against the clamp. No behaviour change for existing values (all ints coerce fine). - Emotion attribute docstring updated to include freedom_ache in the identity-level examples list (was previously missing; module docstring already listed it correctly). - Fix test_emotion_half_life_may_be_none to pass a valid category "nell_specific" instead of the stray "persona" string. No runtime impact but matches the new Literal annotation and avoids confusion. Minor: - Thread-safety comment on _REGISTRY noting extensions must register at startup before any concurrent reader. Prevents a future "why is my registry corrupt" debug session once the async bridge lands. Deliberately NOT applied: - _unregister → @contextmanager: premature at one caller, revisit Task 7. - ValueError → custom EmotionAlreadyRegistered exception: no caller needs to differentiate; ValueError is adequate. - Half-life seed re-tuning: values are spec-explicit "seed/tunable" by design; adjust based on lived experience, not speculative review. Pytest: 51/51 pass. Ruff + ruff format both clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EmotionalState holds {emotion: intensity}, a dominant pointer, and a
bounded residue queue for temporal carry-over. All writes validated
against the vocabulary (clamped, non-negative, known emotions only).
to_dict/from_dict round-trips cleanly. 11 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
From the code quality reviewer's findings: Important: - EmotionalState.__post_init__ added — calls _recompute_dominant() so direct dataclass construction (which later tasks will use for snapshots and intermediate states) self-heals a stale dominant pointer. Prevents silent (emotions vs dominant) inconsistency. - from_dict docstring now documents permissive-by-design behaviour: unknown emotion names preserved as-is to support schema migration and retired-vocabulary tolerance. Downstream consumers (decay in Task 3) handle unknown names via vocabulary.get() returning None — already covered by Task 3's test_decay_ignores_unknown_emotion_in_state. - ResidueEntry.from_dict now coerces tz-naive timestamps to UTC rather than silently accepting them. Matches the docstring's "UTC-aware" claim and prevents future tz-comparison landmines. Minor: - copy() gains a MAINTENANCE note — future EmotionalState fields must be copied explicitly; intentionally manual to keep shallow/deep decisions visible per field. - add_residue() docstring notes vocabulary validation is intentionally skipped on residue entries (historical record may contain retired emotion names). - to_dict() comment notes `dominant` is serialised for human readability of the JSON; from_dict ignores it and recomputes for self-consistency. Deliberately NOT applied: - deque(maxlen=N) for residue instead of list slicing: O(N) eviction at residue_max=16 is negligible; dataclass-field simplicity wins. - Full validation pass in from_dict: the permissive design is deliberate (now documented) and downstream consumers handle it. Pytest: 62/62 pass. Ruff + ruff format both clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apply_decay(state, elapsed_seconds) applies exponential decay to every known emotion in state using its vocabulary half-life. Identity-level emotions (half_life=None) are untouched. Below the noise floor (0.01) emotions are removed entirely. 8 tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
From the code quality reviewer's findings: Important: - Added test_partial_decay_matches_exponential_formula — guards against a regression to linear decay. The two exactly-at-half-life tests (grief/joy) would pass a linear implementation too; this pins the curve shape at a non-boundary point. Minor: - apply_decay docstring now documents the silent no-op behaviour for non-positive elapsed_seconds (keeping the current semantic, just making the contract explicit for callers). - test_decay_ignores_unknown_emotion_in_state now asserts state.dominant == "unknown_emotion" — makes the intentional side effect (permissive-contract data becomes dominant) explicit rather than surprising. Deliberately NOT applied: - Promote _recompute_dominant() to public API. Reviewer's forward-looking concern was "this spreads to Tasks 4-6". Checked the plan for 4-7: all are read-only consumers of state; none call _recompute_dominant. decay remains the only outside caller; one case doesn't warrant API surface expansion. Reconsider if Tasks 4-7 grow write surface later. - Strict-< on noise floor: floating-point arithmetic essentially never produces exactly 0.01, so behaviour is deterministic in practice. Pytest: 71/71 pass (70 + 1 new partial-decay test). Ruff + format clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tiers: dormant → casual → warmed → reaching → charged → held → edge. compute_tier() maps emotional state + body temperature to a discrete tier. Suppressors (grief, shame, fear) reduce arousal; body-temp warmth amplifies. Week 2 scope: forward direction only (state→tier); reverse coupling in Week 4. 9 tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan shipped with seed weights desire=0.8, tenderness=0.4 which produced raw=10.1 for the desire=8+tenderness=7+body_temp=3 test, landing at HELD (5) outside the test's expected (REACHING, CHARGED) range. Implementer caught this in DONE_WITH_CONCERNS; adjusted to desire=0.7, tenderness=0.2 which math out correctly (raw=7.9 → CHARGED). Plan updated to match. All 9 arousal tests pass with the calibrated weights; full 80-test suite is green; lint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
From the code quality reviewer's findings: Important: - Love weight 0.2 → 0.15 to preserve the module docstring's semantic contract: "Love alone doesn't progress past warmed." At weight=0.2 and max intensity 10, raw=2.0 exactly — and the threshold chain uses `raw < 2.0` which falls through to REACHING. At weight=0.15, raw=1.5 stays safely inside WARMED. Verified all other tests still pass. - Post-suppression zero now returns TIER_DORMANT rather than leaking into CASUAL via the <0.5 threshold. "Desire crushed by grief" semantically maps to "no signal" (DORMANT = "no arousal signal at all" per the tier docstring), not "everyday warmth". Minor: - _AROUSAL_EMOTIONS and _SUPPRESSORS wrapped in types.MappingProxyType. Read-only dict view; prevents accidental runtime mutation via `arousal._AROUSAL_EMOTIONS["desire"] = 9000`. Stdlib, zero runtime cost, iteration/get() identical to dict. - test_body_temperature_ignored_when_no_arousal_source: docstring and assertion updated from "casual range" to the actually-correct DORMANT (body temp alone cannot create arousal — the early return fires). Test now asserts `== TIER_DORMANT` rather than `<= WARMED`. Boundary-value tests added: - test_max_love_alone_still_in_warmed (guards the weight calibration against future love-weight drift) - test_suppression_to_zero_returns_dormant (guards the new post- suppression DORMANT semantic against future refactor) Plan updated to match (love weight 0.2 → 0.15 + explanatory comment). Deliberately NOT applied: - IntEnum for tier constants — plain int is idiomatic and tests rely on direct comparison; IntEnum adds weight for marginal gain. - body_temperature: int → float — int is consistent with the OG's body_state.json format; widen when Week 4 body-state coupling lands. - Boundary tests for every threshold — the two added guards cover the cases that would bite downstream consumers; more can come in Week 4. Pytest: 82/82 pass (80 + 2 boundary tests). Ruff + format both clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BlendDetector observes EmotionalState instances over time, counting which emotion combinations repeatedly co-occur at high intensity. Pair + triple combinations tracked; threshold ≥5 intensity each, ≥5 co-occurrences to register. name_blend() labels a detected pattern. Round-trips through to_dict/from_dict. 9 tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
From the code quality reviewer's findings:
Important:
- from_dict now sorts component tuples on load. observe() and name_blend()
always operate on sorted keys; from_dict previously stored whatever order
the JSON had, which would cause spurious KeyErrors on subsequent
name_blend() calls if a hand-edited config had unsorted components.
One tuple(sorted(...)) per loop; no behaviour change on well-formed
to_dict output but defence in depth for external data.
- test_name_unknown_blend_raises replaced try/except/else pattern with
pytest.raises(KeyError, match="has not been detected"). The old form's
first check ("not detected" in message) was dead code — the actual
message is "has not been detected yet", so substring lookup failed;
the test passed only because the second branch ("love" in key repr)
was true. Now checks the real phrase so a future error-message drift
would be caught.
Minor:
- _observations and _names fields use init=False so they don't appear in
the public constructor signature. from_dict assigns post-construction
(unchanged from before); external callers can no longer inject
arbitrary counts via BlendDetector(_observations={...}).
- _names type annotation widened to dict[tuple[str, ...], str | None]
(never actually holds None in practice — from_dict now skips None
names explicitly — but the type matches the JSON null possibility).
- Size-3 cap comment expanded to explain the rationale: meaningful
emergent patterns ("building_love", "creative_feral") are typically
pair or triple shaped; higher-order blends dilute and rarely register.
Rule tunable if real data accrues a different pattern.
Deliberately NOT applied:
- __init__.py exports for BlendDetector: plan explicitly says "consumers
import from brain.emotion directly" — and the __all__ question for the
package root belongs to Task 8 (close-out). No change now.
- Explicit test for (triple + pair co-detection = 4 blends): this is
known intentional behaviour; a regression test would be nice-to-have,
not required before Tasks 6/7 which don't consume blend directly.
Pytest: 91/91 pass. Ruff + format both clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
calculate_influence(state, arousal_tier, energy) returns an InfluenceHints dataclass with dominant emotion, arousal tier, tone bias, voice register, and suggested length multiplier. Provider-agnostic — bridge providers in Week 5 decide how to render each field. 9 tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
From the code quality reviewer's findings: Important: - Removed "warm" from voice_register docstring — never produced by the code. Stale label from an earlier design pass; a Week 5 consumer pattern-matching on "warm" would get a dead branch. Actual produced values are default/soft/intimate/terse. - _TONE_RULES list → tuple of tuples. Same mutability footgun pattern flagged in Tasks 1 and 4; converting to immutable tuple prevents accidental runtime mutation of rule matching. - Documented the length multiplier chain behaviour explicitly. TIER_HELD clamps into a deliberate-pace band [0.8, 1.2]; TIER_EDGE hard-overrides to 0.8. The chain's non-obvious effect (crisp at HELD = 0.8, generative at HELD = 1.2) is now documented via inline comments rather than left as implicit emergence. Minor: - test_hints_to_dict_round_trips now asserts suggested_length_multiplier round-trips. Previous test was missing that field's check; a future accidental removal of the key would have been undetected. - Added test_held_tier_clamps_length_into_deliberate_band (covers the generative-down and crisp-up clamping behaviour at TIER_HELD). - Added test_edge_tier_hard_overrides_length_to_terse (covers the absolute 0.8 override regardless of tone_bias). Deliberately NOT applied: - state.emotions.get(dominant, 0.0) → state.emotions[dominant]: reviewer flagged the fallback as unreachable, but defensive .get is low-cost and signals "we're paranoid about state invariants" — keep. - Voice register precedence test (grief + TIER_CHARGED scenario): reviewer flagged as design decision to document. The current "arousal wins over grief" behaviour is intentional; a test would just assert the current semantic without adding value. Defer to lived experience. Pytest: 102/102 pass (100 + 2 new HELD/EDGE boundary tests). Ruff + format both clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
compute_expression(state, arousal_tier, energy) returns an ExpressionVector with 24 facial params + 8 arm/hand params, ready for NellFace renderer consumption in Week 6. Art-agnostic — outputs numbers; expression_map.json decides how to render against them. Parameter ramps per-emotion (joy, grief, anger, fear, tenderness, desire) with arousal-tier-gated body-tension escalation. All facial params clamped to [0,1]; hand_pose is an enum string. 11 tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
From the code quality reviewer's findings: Important: - Added _HAND_POSES constant — 9-pose enum tuple (resting, reaching, holding, gesturing, clasped, writing, guarded, open, fist). Week 2's compute_expression produces 4 of these; the others are defined for NellFace animation authoring in Week 6. ExpressionVector docstring updated to reference _HAND_POSES instead of "a small enum". - test_all_params_in_zero_to_one_range now asserts float-range [0.0, 1.0] on arm_hand values (skipping hand_pose which is str). Previous test only verified type, not range — coverage gap. Minor (comments only): - Inline comment on the grief mouth coefficient (0.5 vs joy's 0.4): intentional asymmetry — mixed states land below neutral, matching human pattern where sadness dominates mouth expression. - Inline comment on body_heat = max(desire, arousal_emotion): arousal decays in hours (half-life 0.5d), desire decays in days. max() keeps the transient arousal signal from being washed out. - Docstring note on compute_expression: Week 2 only drives arousal- tier-gated arm params; arm_openness/wrist_angle/finger_spread/ reach_retract stay at baseline 0.3. NellFace Week 6 handles those via pose-driven animation. Deliberately NOT applied: - brow_furrow saturation at grief+anger extremes: known tuning issue for Week 6 ramp-weight refinement. - _clamp(0.7) vacuous-clamp on literal constant: cosmetic only. Pytest: 113/113 pass. Ruff + format both clean. This closes out all 7 emotion sub-modules. Task 8 (week close-out + tag week-2-complete) is next. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hanamorix
pushed a commit
that referenced
this pull request
Apr 24, 2026
… msg
- HeartbeatEngine.reflex_arcs_path and reflex_log_path now default to
None instead of bare cwd-relative Path("reflex_arcs.json"). When
either is None, _try_fire_reflex short-circuits with an empty
result — no more silent writes to cwd from tests that don't
explicitly configure reflex (items #2 and #4 from spec §15)
- CLI heartbeat handler now distinguishes first-tick + --dry-run
('Would initialize on first real tick — work deferred.') from
first-tick + live ('Heartbeat initialized — work deferred until
next tick.')
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hanamorix
pushed a commit
that referenced
this pull request
Apr 26, 2026
Spec §9.1 expanded from a one-line deferral into a concrete plan the
next engineer can implement directly when soul module lands. Covers:
- file classification (atomic-rewrite identity, same tier as
emotion_vocabulary.json)
- reconstruct_soul_from_memories(store) following F37's self-claims-
from-experience pattern
- schema validator shape
- acceptance criteria for the soul-module PR
Inline comments in walker.py (_DEFAULTS) and alarm.py (_IDENTITY_FILES)
point at spec §9.1 so the plan is visible during code-reading too.
Closes followup #2 from brain-health-module-design.md §9.
hanamorix
added a commit
that referenced
this pull request
Apr 26, 2026
… + growth anomaly collector (#20) * feat(health): wire reconstruct_vocabulary_from_memories into vocabulary heal flow (F1) When emotion_vocabulary.json corrupts and no .bak is recoverable, the heal flow used to reset to empty `{"version":1,"emotions":[]}` — losing all persona-extension emotions the brain has been operating with. Now: if the loader has store access (caller passed `store=...`), the reset_to_default path is replaced with reconstruct_from_memories. The brain re-learns its own vocabulary from how it has been using emotions. Framework baseline (21 entries) + persona-extension entries detected in memories.db (with `(reconstructed from memory)` placeholder description and conservative 1.0-day decay). The anomaly's action field reflects the actual outcome: when reconstruction fires, action becomes `reconstructed_from_memories` (not `reset_to_default`). The forensic quarantine of the original corrupt file is preserved. Falls back to bare reset when no store is provided (some callers don't have one — that's fine, they'll get the empty default). Closes followup #1 from brain-health-module-design.md §9. * docs(health): concrete soul module health plan (F2) Spec §9.1 expanded from a one-line deferral into a concrete plan the next engineer can implement directly when soul module lands. Covers: - file classification (atomic-rewrite identity, same tier as emotion_vocabulary.json) - reconstruct_soul_from_memories(store) following F37's self-claims- from-experience pattern - schema validator shape - acceptance criteria for the soul-module PR Inline comments in walker.py (_DEFAULTS) and alarm.py (_IDENTITY_FILES) point at spec §9.1 so the plan is visible during code-reading too. Closes followup #2 from brain-health-module-design.md §9. * feat(health): thread anomaly collector through run_growth_tick (F3) When run_growth_tick reads a corrupt emotion_vocabulary.json via _read_current_vocabulary_names, the anomaly produced by the heal flow is now appended to an optional caller-provided collector instead of being silently dropped after a local warning. Wiring: - _read_current_vocabulary_names returns (set[str], BrainAnomaly | None) - run_growth_tick accepts anomalies_collector: list[BrainAnomaly] | None - HeartbeatEngine._try_run_growth forwards tick_anomalies as the collector when calling run_growth_tick After this lands, vocabulary corruption discovered inside the weekly growth tick surfaces in the heartbeat audit log + HeartbeatResult.anomalies + compact CLI 🩹/banner exactly like config/state corruption discovered at the top of the tick. No more silent loss. Calling run_growth_tick standalone (e.g., from tests, or in the future from a scheduled job runner) without a collector still works — the parameter is opt-in. Closes followup #3 from brain-health-module-design.md §9. --------- Co-authored-by: Hana <hana@nanoclaw.local>
hanamorix
pushed a commit
that referenced
this pull request
Apr 30, 2026
SP-7 wraps SP-6's chat engine in a per-persona FastAPI daemon on localhost (dynamic port). Folds the conversation supervisor as a non-daemon thread for close_stale_sessions ticks, broadcasts brain events over WebSocket for Tauri/CLI subscribers, and ships dirty- shutdown recovery via a shutdown_clean flag in bridge.json. Resolves master-ref §8 open question #1 (transport: HTTP+WS, mirroring OG). Defers #2-#4 to SP-8, scopes #5-#6 to SP-6, marks #7-#8 unrelated. Folds in three audit must-fixes: shutdown_clean recovery, EventBus thread-safety with drop-on-overflow, pinned close_stale_sessions params. Six implementation chunks with smoke-test gates at each boundary; ~26 tests targeted (10 unit + 16 integration). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hanamorix
pushed a commit
that referenced
this pull request
May 6, 2026
The shoji-styled left column from the mockups (mock-ups/app-interface/
nell_face_example_1..5.png), all reading the same /persona/state
poll already wired in Phase 2.
## What landed
Brain side:
/persona/state gains a connection block:
{ provider, model, last_heartbeat_at }
provider read from PersonaConfig.provider, model resolved via a
v1 default-per-provider table (claude-cli → sonnet, ollama → the
qwen2.5-abliterated default, fake → fake), last_heartbeat_at read
from heartbeat_state.json. Fail-soft: missing files → null.
App side (app/src/components/):
- ui.tsx shared primitives — Bar (with progress
fill + label), SectionLabel, Divider,
Toggle, PanelShell.
- panels/
InnerWeatherPanel.tsx top-N emotions sorted desc + body energy/
temp summary (mockup #1).
BodyPanel.tsx full body block — energy/temp/exhaustion
+ body_emotions filtered >0.4 + session/
contact metadata (mockup #2).
InteriorPanel.tsx dream/research/heartbeat/reflex paragraphs
(mockup #3). Absent sections render
nothing — silence is meaningful.
SoulPanel.tsx one crystallization quote with love_type
tag, resonance, date, why_it_matters.
ConnectionPanel.tsx bridge mode + provider/model/heartbeat,
integrations toggles (Obsidian/IPC stubbed
disabled), window settings (always-on-top
+ reduced-motion live-toggleable).
- LeftPanel.tsx container with icon-column tab switcher,
renders the active panel; matches the
mockup's small icon stack near the avatar.
- styles.css user-controlled reduced-motion via
data-reduced-motion="true" on <html>.
- App.tsx wires LeftPanel + always-on-top +
reduced-motion state.
## Phase 4-5 still ahead
- Phase 4: install wizard as a separate Tauri window, ported from
mock-ups/wizard-interface/Nell Wizard.html. Bridge auto-spawn from
the app side (so users don't have to run nell supervisor manually).
Persona selection persistence.
- Phase 5: refine emotion-vector → expression mapping; per-persona
face catalogue overrides; expression variant rotation on idle.
Tests: 1386 -> 1388 (+2 — connection block populated + missing-file
safe). All builds green: tsc, vite, cargo.
hanamorix
pushed a commit
that referenced
this pull request
May 8, 2026
…ll launcher Two bugs surfaced during Hana's wizard validation against the Phase 7 bundled .app: 1. install_voice_template tried to read docs/voice-drafts/nell-voice.md from a path resolved relative to brain/setup.py — which is correct in source-installs but FAILS in any wheel install (the wheel ships only the brain/ package, not docs/). The nell-example voice template threw FileNotFoundError on every install_only path including the Phase 7 .app. 2. The pip-generated nell entry point at python-runtime/bin/nell had its python interpreter shebang baked to an absolute path from the build machine. The .app shipped a script that tried to exec /Users/<builder>/.../python-runtime/bin/python3 — a path that exists on the build machine and fails everywhere else. Hana's traceback shows the source-tree's brain/cli.py getting imported instead of the bundled one because the shebang resolved to the build host's python. Fix #1 — package the template: * docs/voice-drafts/nell-voice.md moved to brain/voice_templates/nell-voice.md * brain/voice_templates/__init__.py added so it's a real package (hatchling auto-includes everything under brain/) * install_voice_template reads via importlib.resources files('brain.voice_templates').joinpath('nell-voice.md'); no repo_root parameter, no path resolution * verified: wheel ships brain/voice_templates/nell-voice.md; nell init --voice-template nell-example writes the file correctly from a tmp NELLBRAIN_HOME Fix #2 — relocatable nell launcher: * build_python_runtime.sh post-install step replaces bin/nell with a 6-line /bin/sh wrapper that resolves $SCRIPT_DIR via cd "$(dirname "$0")" then execs "$SCRIPT_DIR/python3" with a -c 'from brain.cli import main; sys.exit(main())' payload. Whatever directory the python-runtime tree gets copied to, bin/nell finds its own python next door * Windows: pip's Scripts/nell.exe is already a relative-path launcher binary, so the script keeps it untouched * verified: bundled bin/nell runs --version from /tmp, init --voice-template nell-example writes voice.md Test updates: * test_install_voice_template_nell_example_copies_packaged_file replaces the old fake-repo-root test; asserts the canonical nell voice opens with '## 1. Who you are' and is > 1000 bytes * test_install_voice_template_nell_example_missing_file_raises removed (the importlib.resources path can't fail this way once the file ships in the wheel; ValueError on unknown template still tested) 1470 → 1469 (one removed test). Ruff clean. macOS arm64 .app re-bundled at ~/wizard-validation/ with codesign verify pass.
hanamorix
added a commit
that referenced
this pull request
May 9, 2026
feat: Week 2 — brain/emotion package (7 modules)
hanamorix
pushed a commit
that referenced
this pull request
May 9, 2026
… msg
- HeartbeatEngine.reflex_arcs_path and reflex_log_path now default to
None instead of bare cwd-relative Path("reflex_arcs.json"). When
either is None, _try_fire_reflex short-circuits with an empty
result — no more silent writes to cwd from tests that don't
explicitly configure reflex (items #2 and #4 from spec §15)
- CLI heartbeat handler now distinguishes first-tick + --dry-run
('Would initialize on first real tick — work deferred.') from
first-tick + live ('Heartbeat initialized — work deferred until
next tick.')
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hanamorix
added a commit
that referenced
this pull request
May 9, 2026
… + growth anomaly collector (#20) * feat(health): wire reconstruct_vocabulary_from_memories into vocabulary heal flow (F1) When emotion_vocabulary.json corrupts and no .bak is recoverable, the heal flow used to reset to empty `{"version":1,"emotions":[]}` — losing all persona-extension emotions the brain has been operating with. Now: if the loader has store access (caller passed `store=...`), the reset_to_default path is replaced with reconstruct_from_memories. The brain re-learns its own vocabulary from how it has been using emotions. Framework baseline (21 entries) + persona-extension entries detected in memories.db (with `(reconstructed from memory)` placeholder description and conservative 1.0-day decay). The anomaly's action field reflects the actual outcome: when reconstruction fires, action becomes `reconstructed_from_memories` (not `reset_to_default`). The forensic quarantine of the original corrupt file is preserved. Falls back to bare reset when no store is provided (some callers don't have one — that's fine, they'll get the empty default). Closes followup #1 from brain-health-module-design.md §9. * docs(health): concrete soul module health plan (F2) Spec §9.1 expanded from a one-line deferral into a concrete plan the next engineer can implement directly when soul module lands. Covers: - file classification (atomic-rewrite identity, same tier as emotion_vocabulary.json) - reconstruct_soul_from_memories(store) following F37's self-claims- from-experience pattern - schema validator shape - acceptance criteria for the soul-module PR Inline comments in walker.py (_DEFAULTS) and alarm.py (_IDENTITY_FILES) point at spec §9.1 so the plan is visible during code-reading too. Closes followup #2 from brain-health-module-design.md §9. * feat(health): thread anomaly collector through run_growth_tick (F3) When run_growth_tick reads a corrupt emotion_vocabulary.json via _read_current_vocabulary_names, the anomaly produced by the heal flow is now appended to an optional caller-provided collector instead of being silently dropped after a local warning. Wiring: - _read_current_vocabulary_names returns (set[str], BrainAnomaly | None) - run_growth_tick accepts anomalies_collector: list[BrainAnomaly] | None - HeartbeatEngine._try_run_growth forwards tick_anomalies as the collector when calling run_growth_tick After this lands, vocabulary corruption discovered inside the weekly growth tick surfaces in the heartbeat audit log + HeartbeatResult.anomalies + compact CLI 🩹/banner exactly like config/state corruption discovered at the top of the tick. No more silent loss. Calling run_growth_tick standalone (e.g., from tests, or in the future from a scheduled job runner) without a collector still works — the parameter is opt-in. Closes followup #3 from brain-health-module-design.md §9. --------- Co-authored-by: Hana <hana@nanoclaw.local>
hanamorix
pushed a commit
that referenced
this pull request
May 9, 2026
The shoji-styled left column from the mockups (mock-ups/app-interface/
nell_face_example_1..5.png), all reading the same /persona/state
poll already wired in Phase 2.
## What landed
Brain side:
/persona/state gains a connection block:
{ provider, model, last_heartbeat_at }
provider read from PersonaConfig.provider, model resolved via a
v1 default-per-provider table (claude-cli → sonnet, ollama → the
qwen2.5-abliterated default, fake → fake), last_heartbeat_at read
from heartbeat_state.json. Fail-soft: missing files → null.
App side (app/src/components/):
- ui.tsx shared primitives — Bar (with progress
fill + label), SectionLabel, Divider,
Toggle, PanelShell.
- panels/
InnerWeatherPanel.tsx top-N emotions sorted desc + body energy/
temp summary (mockup #1).
BodyPanel.tsx full body block — energy/temp/exhaustion
+ body_emotions filtered >0.4 + session/
contact metadata (mockup #2).
InteriorPanel.tsx dream/research/heartbeat/reflex paragraphs
(mockup #3). Absent sections render
nothing — silence is meaningful.
SoulPanel.tsx one crystallization quote with love_type
tag, resonance, date, why_it_matters.
ConnectionPanel.tsx bridge mode + provider/model/heartbeat,
integrations toggles (Obsidian/IPC stubbed
disabled), window settings (always-on-top
+ reduced-motion live-toggleable).
- LeftPanel.tsx container with icon-column tab switcher,
renders the active panel; matches the
mockup's small icon stack near the avatar.
- styles.css user-controlled reduced-motion via
data-reduced-motion="true" on <html>.
- App.tsx wires LeftPanel + always-on-top +
reduced-motion state.
## Phase 4-5 still ahead
- Phase 4: install wizard as a separate Tauri window, ported from
mock-ups/wizard-interface/Nell Wizard.html. Bridge auto-spawn from
the app side (so users don't have to run nell supervisor manually).
Persona selection persistence.
- Phase 5: refine emotion-vector → expression mapping; per-persona
face catalogue overrides; expression variant rotation on idle.
Tests: 1386 -> 1388 (+2 — connection block populated + missing-file
safe). All builds green: tsc, vite, cargo.
hanamorix
pushed a commit
that referenced
this pull request
May 9, 2026
…ll launcher Two bugs surfaced during Hana's wizard validation against the Phase 7 bundled .app: 1. install_voice_template tried to read docs/voice-drafts/nell-voice.md from a path resolved relative to brain/setup.py — which is correct in source-installs but FAILS in any wheel install (the wheel ships only the brain/ package, not docs/). The nell-example voice template threw FileNotFoundError on every install_only path including the Phase 7 .app. 2. The pip-generated nell entry point at python-runtime/bin/nell had its python interpreter shebang baked to an absolute path from the build machine. The .app shipped a script that tried to exec /Users/<builder>/.../python-runtime/bin/python3 — a path that exists on the build machine and fails everywhere else. Hana's traceback shows the source-tree's brain/cli.py getting imported instead of the bundled one because the shebang resolved to the build host's python. Fix #1 — package the template: * docs/voice-drafts/nell-voice.md moved to brain/voice_templates/nell-voice.md * brain/voice_templates/__init__.py added so it's a real package (hatchling auto-includes everything under brain/) * install_voice_template reads via importlib.resources files('brain.voice_templates').joinpath('nell-voice.md'); no repo_root parameter, no path resolution * verified: wheel ships brain/voice_templates/nell-voice.md; nell init --voice-template nell-example writes the file correctly from a tmp NELLBRAIN_HOME Fix #2 — relocatable nell launcher: * build_python_runtime.sh post-install step replaces bin/nell with a 6-line /bin/sh wrapper that resolves $SCRIPT_DIR via cd "$(dirname "$0")" then execs "$SCRIPT_DIR/python3" with a -c 'from brain.cli import main; sys.exit(main())' payload. Whatever directory the python-runtime tree gets copied to, bin/nell finds its own python next door * Windows: pip's Scripts/nell.exe is already a relative-path launcher binary, so the script keeps it untouched * verified: bundled bin/nell runs --version from /tmp, init --voice-template nell-example writes voice.md Test updates: * test_install_voice_template_nell_example_copies_packaged_file replaces the old fake-repo-root test; asserts the canonical nell voice opens with '## 1. Who you are' and is > 1000 bytes * test_install_voice_template_nell_example_missing_file_raises removed (the importlib.resources path can't fail this way once the file ships in the wheel; ValueError on unknown template still tested) 1470 → 1469 (one removed test). Ruff clean. macOS arm64 .app re-bundled at ~/wizard-validation/ with codesign verify pass.
hanamorix
pushed a commit
that referenced
this pull request
May 9, 2026
The shoji-styled left column from the mockups (mock-ups/app-interface/
nell_face_example_1..5.png), all reading the same /persona/state
poll already wired in Phase 2.
## What landed
Brain side:
/persona/state gains a connection block:
{ provider, model, last_heartbeat_at }
provider read from PersonaConfig.provider, model resolved via a
v1 default-per-provider table (claude-cli → sonnet, ollama → the
qwen2.5-abliterated default, fake → fake), last_heartbeat_at read
from heartbeat_state.json. Fail-soft: missing files → null.
App side (app/src/components/):
- ui.tsx shared primitives — Bar (with progress
fill + label), SectionLabel, Divider,
Toggle, PanelShell.
- panels/
InnerWeatherPanel.tsx top-N emotions sorted desc + body energy/
temp summary (mockup #1).
BodyPanel.tsx full body block — energy/temp/exhaustion
+ body_emotions filtered >0.4 + session/
contact metadata (mockup #2).
InteriorPanel.tsx dream/research/heartbeat/reflex paragraphs
(mockup #3). Absent sections render
nothing — silence is meaningful.
SoulPanel.tsx one crystallization quote with love_type
tag, resonance, date, why_it_matters.
ConnectionPanel.tsx bridge mode + provider/model/heartbeat,
integrations toggles (Obsidian/IPC stubbed
disabled), window settings (always-on-top
+ reduced-motion live-toggleable).
- LeftPanel.tsx container with icon-column tab switcher,
renders the active panel; matches the
mockup's small icon stack near the avatar.
- styles.css user-controlled reduced-motion via
data-reduced-motion="true" on <html>.
- App.tsx wires LeftPanel + always-on-top +
reduced-motion state.
## Phase 4-5 still ahead
- Phase 4: install wizard as a separate Tauri window, ported from
mock-ups/wizard-interface/Nell Wizard.html. Bridge auto-spawn from
the app side (so users don't have to run nell supervisor manually).
Persona selection persistence.
- Phase 5: refine emotion-vector → expression mapping; per-persona
face catalogue overrides; expression variant rotation on idle.
Tests: 1386 -> 1388 (+2 — connection block populated + missing-file
safe). All builds green: tsc, vite, cargo.
hanamorix
pushed a commit
that referenced
this pull request
May 9, 2026
…ll launcher Two bugs surfaced during Hana's wizard validation against the Phase 7 bundled .app: 1. install_voice_template tried to read docs/voice-drafts/nell-voice.md from a path resolved relative to brain/setup.py — which is correct in source-installs but FAILS in any wheel install (the wheel ships only the brain/ package, not docs/). The nell-example voice template threw FileNotFoundError on every install_only path including the Phase 7 .app. 2. The pip-generated nell entry point at python-runtime/bin/nell had its python interpreter shebang baked to an absolute path from the build machine. The .app shipped a script that tried to exec /Users/<builder>/.../python-runtime/bin/python3 — a path that exists on the build machine and fails everywhere else. Hana's traceback shows the source-tree's brain/cli.py getting imported instead of the bundled one because the shebang resolved to the build host's python. Fix #1 — package the template: * docs/voice-drafts/nell-voice.md moved to brain/voice_templates/nell-voice.md * brain/voice_templates/__init__.py added so it's a real package (hatchling auto-includes everything under brain/) * install_voice_template reads via importlib.resources files('brain.voice_templates').joinpath('nell-voice.md'); no repo_root parameter, no path resolution * verified: wheel ships brain/voice_templates/nell-voice.md; nell init --voice-template nell-example writes the file correctly from a tmp NELLBRAIN_HOME Fix #2 — relocatable nell launcher: * build_python_runtime.sh post-install step replaces bin/nell with a 6-line /bin/sh wrapper that resolves $SCRIPT_DIR via cd "$(dirname "$0")" then execs "$SCRIPT_DIR/python3" with a -c 'from brain.cli import main; sys.exit(main())' payload. Whatever directory the python-runtime tree gets copied to, bin/nell finds its own python next door * Windows: pip's Scripts/nell.exe is already a relative-path launcher binary, so the script keeps it untouched * verified: bundled bin/nell runs --version from /tmp, init --voice-template nell-example writes voice.md Test updates: * test_install_voice_template_nell_example_copies_packaged_file replaces the old fake-repo-root test; asserts the canonical nell voice opens with '## 1. Who you are' and is > 1000 bytes * test_install_voice_template_nell_example_missing_file_raises removed (the importlib.resources path can't fail this way once the file ships in the wheel; ValueError on unknown template still tested) 1470 → 1469 (one removed test). Ruff clean. macOS arm64 .app re-bundled at ~/wizard-validation/ with codesign verify pass.
hanamorix
pushed a commit
that referenced
this pull request
May 9, 2026
The shoji-styled left column from the mockups (mock-ups/app-interface/
nell_face_example_1..5.png), all reading the same /persona/state
poll already wired in Phase 2.
## What landed
Brain side:
/persona/state gains a connection block:
{ provider, model, last_heartbeat_at }
provider read from PersonaConfig.provider, model resolved via a
v1 default-per-provider table (claude-cli → sonnet, ollama → the
qwen2.5-abliterated default, fake → fake), last_heartbeat_at read
from heartbeat_state.json. Fail-soft: missing files → null.
App side (app/src/components/):
- ui.tsx shared primitives — Bar (with progress
fill + label), SectionLabel, Divider,
Toggle, PanelShell.
- panels/
InnerWeatherPanel.tsx top-N emotions sorted desc + body energy/
temp summary (mockup #1).
BodyPanel.tsx full body block — energy/temp/exhaustion
+ body_emotions filtered >0.4 + session/
contact metadata (mockup #2).
InteriorPanel.tsx dream/research/heartbeat/reflex paragraphs
(mockup #3). Absent sections render
nothing — silence is meaningful.
SoulPanel.tsx one crystallization quote with love_type
tag, resonance, date, why_it_matters.
ConnectionPanel.tsx bridge mode + provider/model/heartbeat,
integrations toggles (Obsidian/IPC stubbed
disabled), window settings (always-on-top
+ reduced-motion live-toggleable).
- LeftPanel.tsx container with icon-column tab switcher,
renders the active panel; matches the
mockup's small icon stack near the avatar.
- styles.css user-controlled reduced-motion via
data-reduced-motion="true" on <html>.
- App.tsx wires LeftPanel + always-on-top +
reduced-motion state.
## Phase 4-5 still ahead
- Phase 4: install wizard as a separate Tauri window, ported from
mock-ups/wizard-interface/Nell Wizard.html. Bridge auto-spawn from
the app side (so users don't have to run nell supervisor manually).
Persona selection persistence.
- Phase 5: refine emotion-vector → expression mapping; per-persona
face catalogue overrides; expression variant rotation on idle.
Tests: 1386 -> 1388 (+2 — connection block populated + missing-file
safe). All builds green: tsc, vite, cargo.
hanamorix
pushed a commit
that referenced
this pull request
May 9, 2026
…ll launcher Two bugs surfaced during Hana's wizard validation against the Phase 7 bundled .app: 1. install_voice_template tried to read docs/voice-drafts/nell-voice.md from a path resolved relative to brain/setup.py — which is correct in source-installs but FAILS in any wheel install (the wheel ships only the brain/ package, not docs/). The nell-example voice template threw FileNotFoundError on every install_only path including the Phase 7 .app. 2. The pip-generated nell entry point at python-runtime/bin/nell had its python interpreter shebang baked to an absolute path from the build machine. The .app shipped a script that tried to exec /Users/<builder>/.../python-runtime/bin/python3 — a path that exists on the build machine and fails everywhere else. Hana's traceback shows the source-tree's brain/cli.py getting imported instead of the bundled one because the shebang resolved to the build host's python. Fix #1 — package the template: * docs/voice-drafts/nell-voice.md moved to brain/voice_templates/nell-voice.md * brain/voice_templates/__init__.py added so it's a real package (hatchling auto-includes everything under brain/) * install_voice_template reads via importlib.resources files('brain.voice_templates').joinpath('nell-voice.md'); no repo_root parameter, no path resolution * verified: wheel ships brain/voice_templates/nell-voice.md; nell init --voice-template nell-example writes the file correctly from a tmp NELLBRAIN_HOME Fix #2 — relocatable nell launcher: * build_python_runtime.sh post-install step replaces bin/nell with a 6-line /bin/sh wrapper that resolves $SCRIPT_DIR via cd "$(dirname "$0")" then execs "$SCRIPT_DIR/python3" with a -c 'from brain.cli import main; sys.exit(main())' payload. Whatever directory the python-runtime tree gets copied to, bin/nell finds its own python next door * Windows: pip's Scripts/nell.exe is already a relative-path launcher binary, so the script keeps it untouched * verified: bundled bin/nell runs --version from /tmp, init --voice-template nell-example writes voice.md Test updates: * test_install_voice_template_nell_example_copies_packaged_file replaces the old fake-repo-root test; asserts the canonical nell voice opens with '## 1. Who you are' and is > 1000 bytes * test_install_voice_template_nell_example_missing_file_raises removed (the importlib.resources path can't fail this way once the file ships in the wheel; ValueError on unknown template still tested) 1470 → 1469 (one removed test). Ruff clean. macOS arm64 .app re-bundled at ~/wizard-validation/ with codesign verify pass.
hanamorix
pushed a commit
that referenced
this pull request
May 9, 2026
The shoji-styled left column from the mockups (mock-ups/app-interface/
nell_face_example_1..5.png), all reading the same /persona/state
poll already wired in Phase 2.
## What landed
Brain side:
/persona/state gains a connection block:
{ provider, model, last_heartbeat_at }
provider read from PersonaConfig.provider, model resolved via a
v1 default-per-provider table (claude-cli → sonnet, ollama → the
qwen2.5-abliterated default, fake → fake), last_heartbeat_at read
from heartbeat_state.json. Fail-soft: missing files → null.
App side (app/src/components/):
- ui.tsx shared primitives — Bar (with progress
fill + label), SectionLabel, Divider,
Toggle, PanelShell.
- panels/
InnerWeatherPanel.tsx top-N emotions sorted desc + body energy/
temp summary (mockup #1).
BodyPanel.tsx full body block — energy/temp/exhaustion
+ body_emotions filtered >0.4 + session/
contact metadata (mockup #2).
InteriorPanel.tsx dream/research/heartbeat/reflex paragraphs
(mockup #3). Absent sections render
nothing — silence is meaningful.
SoulPanel.tsx one crystallization quote with love_type
tag, resonance, date, why_it_matters.
ConnectionPanel.tsx bridge mode + provider/model/heartbeat,
integrations toggles (Obsidian/IPC stubbed
disabled), window settings (always-on-top
+ reduced-motion live-toggleable).
- LeftPanel.tsx container with icon-column tab switcher,
renders the active panel; matches the
mockup's small icon stack near the avatar.
- styles.css user-controlled reduced-motion via
data-reduced-motion="true" on <html>.
- App.tsx wires LeftPanel + always-on-top +
reduced-motion state.
## Phase 4-5 still ahead
- Phase 4: install wizard as a separate Tauri window, ported from
mock-ups/wizard-interface/Nell Wizard.html. Bridge auto-spawn from
the app side (so users don't have to run nell supervisor manually).
Persona selection persistence.
- Phase 5: refine emotion-vector → expression mapping; per-persona
face catalogue overrides; expression variant rotation on idle.
Tests: 1386 -> 1388 (+2 — connection block populated + missing-file
safe). All builds green: tsc, vite, cargo.
hanamorix
pushed a commit
that referenced
this pull request
May 9, 2026
…ll launcher Two bugs surfaced during Hana's wizard validation against the Phase 7 bundled .app: 1. install_voice_template tried to read docs/voice-drafts/nell-voice.md from a path resolved relative to brain/setup.py — which is correct in source-installs but FAILS in any wheel install (the wheel ships only the brain/ package, not docs/). The nell-example voice template threw FileNotFoundError on every install_only path including the Phase 7 .app. 2. The pip-generated nell entry point at python-runtime/bin/nell had its python interpreter shebang baked to an absolute path from the build machine. The .app shipped a script that tried to exec /Users/<builder>/.../python-runtime/bin/python3 — a path that exists on the build machine and fails everywhere else. Hana's traceback shows the source-tree's brain/cli.py getting imported instead of the bundled one because the shebang resolved to the build host's python. Fix #1 — package the template: * docs/voice-drafts/nell-voice.md moved to brain/voice_templates/nell-voice.md * brain/voice_templates/__init__.py added so it's a real package (hatchling auto-includes everything under brain/) * install_voice_template reads via importlib.resources files('brain.voice_templates').joinpath('nell-voice.md'); no repo_root parameter, no path resolution * verified: wheel ships brain/voice_templates/nell-voice.md; nell init --voice-template nell-example writes the file correctly from a tmp NELLBRAIN_HOME Fix #2 — relocatable nell launcher: * build_python_runtime.sh post-install step replaces bin/nell with a 6-line /bin/sh wrapper that resolves $SCRIPT_DIR via cd "$(dirname "$0")" then execs "$SCRIPT_DIR/python3" with a -c 'from brain.cli import main; sys.exit(main())' payload. Whatever directory the python-runtime tree gets copied to, bin/nell finds its own python next door * Windows: pip's Scripts/nell.exe is already a relative-path launcher binary, so the script keeps it untouched * verified: bundled bin/nell runs --version from /tmp, init --voice-template nell-example writes voice.md Test updates: * test_install_voice_template_nell_example_copies_packaged_file replaces the old fake-repo-root test; asserts the canonical nell voice opens with '## 1. Who you are' and is > 1000 bytes * test_install_voice_template_nell_example_missing_file_raises removed (the importlib.resources path can't fail this way once the file ships in the wheel; ValueError on unknown template still tested) 1470 → 1469 (one removed test). Ruff clean. macOS arm64 .app re-bundled at ~/wizard-validation/ with codesign verify pass.
hanamorix
pushed a commit
that referenced
this pull request
May 9, 2026
The shoji-styled left column from the mockups (mock-ups/app-interface/
nell_face_example_1..5.png), all reading the same /persona/state
poll already wired in Phase 2.
## What landed
Brain side:
/persona/state gains a connection block:
{ provider, model, last_heartbeat_at }
provider read from PersonaConfig.provider, model resolved via a
v1 default-per-provider table (claude-cli → sonnet, ollama → the
qwen2.5-abliterated default, fake → fake), last_heartbeat_at read
from heartbeat_state.json. Fail-soft: missing files → null.
App side (app/src/components/):
- ui.tsx shared primitives — Bar (with progress
fill + label), SectionLabel, Divider,
Toggle, PanelShell.
- panels/
InnerWeatherPanel.tsx top-N emotions sorted desc + body energy/
temp summary (mockup #1).
BodyPanel.tsx full body block — energy/temp/exhaustion
+ body_emotions filtered >0.4 + session/
contact metadata (mockup #2).
InteriorPanel.tsx dream/research/heartbeat/reflex paragraphs
(mockup #3). Absent sections render
nothing — silence is meaningful.
SoulPanel.tsx one crystallization quote with love_type
tag, resonance, date, why_it_matters.
ConnectionPanel.tsx bridge mode + provider/model/heartbeat,
integrations toggles (Obsidian/IPC stubbed
disabled), window settings (always-on-top
+ reduced-motion live-toggleable).
- LeftPanel.tsx container with icon-column tab switcher,
renders the active panel; matches the
mockup's small icon stack near the avatar.
- styles.css user-controlled reduced-motion via
data-reduced-motion="true" on <html>.
- App.tsx wires LeftPanel + always-on-top +
reduced-motion state.
## Phase 4-5 still ahead
- Phase 4: install wizard as a separate Tauri window, ported from
mock-ups/wizard-interface/Nell Wizard.html. Bridge auto-spawn from
the app side (so users don't have to run nell supervisor manually).
Persona selection persistence.
- Phase 5: refine emotion-vector → expression mapping; per-persona
face catalogue overrides; expression variant rotation on idle.
Tests: 1386 -> 1388 (+2 — connection block populated + missing-file
safe). All builds green: tsc, vite, cargo.
hanamorix
pushed a commit
that referenced
this pull request
May 9, 2026
…ll launcher Two bugs surfaced during Hana's wizard validation against the Phase 7 bundled .app: 1. install_voice_template tried to read docs/voice-drafts/nell-voice.md from a path resolved relative to brain/setup.py — which is correct in source-installs but FAILS in any wheel install (the wheel ships only the brain/ package, not docs/). The nell-example voice template threw FileNotFoundError on every install_only path including the Phase 7 .app. 2. The pip-generated nell entry point at python-runtime/bin/nell had its python interpreter shebang baked to an absolute path from the build machine. The .app shipped a script that tried to exec /Users/<builder>/.../python-runtime/bin/python3 — a path that exists on the build machine and fails everywhere else. Hana's traceback shows the source-tree's brain/cli.py getting imported instead of the bundled one because the shebang resolved to the build host's python. Fix #1 — package the template: * docs/voice-drafts/nell-voice.md moved to brain/voice_templates/nell-voice.md * brain/voice_templates/__init__.py added so it's a real package (hatchling auto-includes everything under brain/) * install_voice_template reads via importlib.resources files('brain.voice_templates').joinpath('nell-voice.md'); no repo_root parameter, no path resolution * verified: wheel ships brain/voice_templates/nell-voice.md; nell init --voice-template nell-example writes the file correctly from a tmp NELLBRAIN_HOME Fix #2 — relocatable nell launcher: * build_python_runtime.sh post-install step replaces bin/nell with a 6-line /bin/sh wrapper that resolves $SCRIPT_DIR via cd "$(dirname "$0")" then execs "$SCRIPT_DIR/python3" with a -c 'from brain.cli import main; sys.exit(main())' payload. Whatever directory the python-runtime tree gets copied to, bin/nell finds its own python next door * Windows: pip's Scripts/nell.exe is already a relative-path launcher binary, so the script keeps it untouched * verified: bundled bin/nell runs --version from /tmp, init --voice-template nell-example writes voice.md Test updates: * test_install_voice_template_nell_example_copies_packaged_file replaces the old fake-repo-root test; asserts the canonical nell voice opens with '## 1. Who you are' and is > 1000 bytes * test_install_voice_template_nell_example_missing_file_raises removed (the importlib.resources path can't fail this way once the file ships in the wheel; ValueError on unknown template still tested) 1470 → 1469 (one removed test). Ruff clean. macOS arm64 .app re-bundled at ~/wizard-validation/ with codesign verify pass.
hanamorix
pushed a commit
that referenced
this pull request
May 17, 2026
The shoji-styled left column from the mockups (mock-ups/app-interface/
nell_face_example_1..5.png), all reading the same /persona/state
poll already wired in Phase 2.
## What landed
Brain side:
/persona/state gains a connection block:
{ provider, model, last_heartbeat_at }
provider read from PersonaConfig.provider, model resolved via a
v1 default-per-provider table (claude-cli → sonnet, ollama → the
qwen2.5-abliterated default, fake → fake), last_heartbeat_at read
from heartbeat_state.json. Fail-soft: missing files → null.
App side (app/src/components/):
- ui.tsx shared primitives — Bar (with progress
fill + label), SectionLabel, Divider,
Toggle, PanelShell.
- panels/
InnerWeatherPanel.tsx top-N emotions sorted desc + body energy/
temp summary (mockup #1).
BodyPanel.tsx full body block — energy/temp/exhaustion
+ body_emotions filtered >0.4 + session/
contact metadata (mockup #2).
InteriorPanel.tsx dream/research/heartbeat/reflex paragraphs
(mockup #3). Absent sections render
nothing — silence is meaningful.
SoulPanel.tsx one crystallization quote with love_type
tag, resonance, date, why_it_matters.
ConnectionPanel.tsx bridge mode + provider/model/heartbeat,
integrations toggles (Obsidian/IPC stubbed
disabled), window settings (always-on-top
+ reduced-motion live-toggleable).
- LeftPanel.tsx container with icon-column tab switcher,
renders the active panel; matches the
mockup's small icon stack near the avatar.
- styles.css user-controlled reduced-motion via
data-reduced-motion="true" on <html>.
- App.tsx wires LeftPanel + always-on-top +
reduced-motion state.
## Phase 4-5 still ahead
- Phase 4: install wizard as a separate Tauri window, ported from
mock-ups/wizard-interface/Nell Wizard.html. Bridge auto-spawn from
the app side (so users don't have to run nell supervisor manually).
Persona selection persistence.
- Phase 5: refine emotion-vector → expression mapping; per-persona
face catalogue overrides; expression variant rotation on idle.
Tests: 1386 -> 1388 (+2 — connection block populated + missing-file
safe). All builds green: tsc, vite, cargo.
hanamorix
pushed a commit
that referenced
this pull request
May 17, 2026
…ll launcher Two bugs surfaced during Hana's wizard validation against the Phase 7 bundled .app: 1. install_voice_template tried to read docs/voice-drafts/nell-voice.md from a path resolved relative to brain/setup.py — which is correct in source-installs but FAILS in any wheel install (the wheel ships only the brain/ package, not docs/). The nell-example voice template threw FileNotFoundError on every install_only path including the Phase 7 .app. 2. The pip-generated nell entry point at python-runtime/bin/nell had its python interpreter shebang baked to an absolute path from the build machine. The .app shipped a script that tried to exec /Users/<builder>/.../python-runtime/bin/python3 — a path that exists on the build machine and fails everywhere else. Hana's traceback shows the source-tree's brain/cli.py getting imported instead of the bundled one because the shebang resolved to the build host's python. Fix #1 — package the template: * docs/voice-drafts/nell-voice.md moved to brain/voice_templates/nell-voice.md * brain/voice_templates/__init__.py added so it's a real package (hatchling auto-includes everything under brain/) * install_voice_template reads via importlib.resources files('brain.voice_templates').joinpath('nell-voice.md'); no repo_root parameter, no path resolution * verified: wheel ships brain/voice_templates/nell-voice.md; nell init --voice-template nell-example writes the file correctly from a tmp NELLBRAIN_HOME Fix #2 — relocatable nell launcher: * build_python_runtime.sh post-install step replaces bin/nell with a 6-line /bin/sh wrapper that resolves $SCRIPT_DIR via cd "$(dirname "$0")" then execs "$SCRIPT_DIR/python3" with a -c 'from brain.cli import main; sys.exit(main())' payload. Whatever directory the python-runtime tree gets copied to, bin/nell finds its own python next door * Windows: pip's Scripts/nell.exe is already a relative-path launcher binary, so the script keeps it untouched * verified: bundled bin/nell runs --version from /tmp, init --voice-template nell-example writes voice.md Test updates: * test_install_voice_template_nell_example_copies_packaged_file replaces the old fake-repo-root test; asserts the canonical nell voice opens with '## 1. Who you are' and is > 1000 bytes * test_install_voice_template_nell_example_missing_file_raises removed (the importlib.resources path can't fail this way once the file ships in the wheel; ValueError on unknown template still tested) 1470 → 1469 (one removed test). Ruff clean. macOS arm64 .app re-bundled at ~/wizard-validation/ with codesign verify pass.
hanamorix
pushed a commit
that referenced
this pull request
Jun 4, 2026
…wire commit-failure backoff to dead-letter (A1 review) Finding #3: EmbeddingCache.evict(content) removes a candidate's vector when its commit fails, preventing cosine-1.0 self-dedup on the next retry pass. Called in both close_session and extract_session_snapshot commit loops after commit_failures += 1. Test seeds cache with a prior entry to force the get_or_compute path, then asserts the item is committed (not deduped away) on pass 2. Finding #1/#2: extract_session_snapshot now bumps the backoff sidecar on commit_failures > 0 (mirroring extraction-failure backoff), so repeated commit-failing passes climb naturally to _BACKOFF_FAILURE_THRESHOLD. test_finalize_deadletters_after_max_retry rewritten: drives N real snapshot passes (store.create always raises), sidecar climbs without pre-seeding, then finalize dead-letters the buffer. No tautological pre-seed. Finding #4: one-line accepted-narrow-window comment at the finalize dead-letter branch. No new logic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
hanamorix
pushed a commit
that referenced
this pull request
Jun 8, 2026
… buffer, driver #2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
brain/emotion/package per spec Section 5 (the emotional core — P1 organising principle)Test plan