claude-village - implementation progress
Last updated: 2026-04-21
Status legend
[ ]pending / available to claim[~]in progress (owner + started timestamp)[x]completed (commit sha)[!]blocked (reason)
Foundation (serial - must land in order)
| # | Task | Status | Owner | Commit / Notes |
|---|---|---|---|---|
| 1 | Repo scaffold (Electron + Vite + React + TS + Vitest + Playwright + ESLint/Prettier + CI) | [x] |
agent-scaffold | 9692804 |
| 2 | Shared types (AgentEvent, AgentState, SessionState, ZoneId, ...) |
[x] |
agent-shared-types | fe9dd4e |
Main-process parallel block (each depends only on Task 2)
| # | Task | Status | Owner | Commit / Notes |
|---|---|---|---|---|
| 3 | session-watcher.ts + unit tests (JSONL tailing, offset tracking) |
[x] |
agent-session-watcher | 91249bf |
| 4 | hook-server.ts + unit tests (HTTP + socket Claude hook listener) |
[x] |
agent-hook-server | afab039 |
| 5 | classifier.ts + unit tests (event -> zone/animation/tooltip) |
[x] |
agent-classifier-orchestrator | bccba07 |
| 6 | session-store.ts + unit tests (in-memory + JSON pinned.json snapshot) |
[x] |
agent-session-store-orchestrator | 21532a0 |
| 7 | ipc-bridge.ts (wires 3-6 to ipcMain; depends on 3-6) |
[x] |
agent-ipc-bridge-orchestrator | f0a102e |
Renderer parallel block (depends on Task 2; can mock IPC until Task 7)
| # | Task | Status | Owner | Commit / Notes |
|---|---|---|---|---|
| 8 | Tab chrome + sidebar + SessionContext | [x] |
agent-tab-chrome-orchestrator | 7b5a97a |
| 9 | VillageScene + Zone (9 zones, orbit camera, walkable grid) |
[x] |
agent-village-scene-orchestrator | a121f89 |
| 10 | pathfinding.ts + unit tests (A* on the voxel grid) |
[x] |
agent-pathfinding-orchestrator | 3d749e4 |
| 11 | Character component (depends on 9 + 10) |
[x] |
agent-11-orchestrator | 0c76cfd |
| 12 | TooltipLayer (depends on 9 + 11) |
[x] |
agent-12-orchestrator | cd59f9c |
| 13 | TimelineStrip with click-to-focus (depends on 8) |
[x] |
agent-13-orchestrator | b986313 |
| 14 | Conversation animations + bubble drawer (depends on 11 + 12) | [x] |
agent-14-orchestrator | 44717ae |
| 15 | SettingsScreen + About modal (depends on 8) |
[x] |
agent-15-orchestrator | 78bced5 |
Integration and ship
| # | Task | Status | Owner | Commit / Notes |
|---|---|---|---|---|
| 16 | End-to-end integration test (Playwright over Electron) | [x] |
agent-16-orchestrator | 41ffcd3 |
| 17 | Packaging (electron-builder, .dmg, install doc) |
[x] |
agent-17-orchestrator | c76818f |
Milestones
M1- Foundation done (1, 2 complete) - unblocks parallel work.M2- Main process wired (3-7 complete) - renderer can drop IPC mocks.M3- Renderer MVP (8, 9, 10, 11 complete) - characters render and move in the village.M4- Feature-complete (8-15 complete) - tooltips, timeline, conversations, settings.M5- Ship (16, 17 complete) - test green, DMG built.
Tech debt / follow-ups (not blocking, track here)
ESLint 9 flat-config migration - currentlyDone -ESLINT_USE_FLAT_CONFIG=falsebridges the gap. Port.eslintrc.cjstoeslint.config.jsflat config and drop the env var. Easier to do now than after Tasks 8+ add React component lint rules.eslint.config.jsnow drives lint,ESLINT_USE_FLAT_CONFIGand--extremoved from scripts. Legacy plugins (@typescript-eslint/recommended,react-hooks/recommended) bridged via@eslint/eslintrcFlatCompat;eslint-plugin-reactuses its native flat export.Remove(done in task 3; flag is fully removed from--passWithNoTestsfrom thetestscript once Task 2 or Task 3 lands real tests.package.json)Resolved by droppingbetter-sqlite3native rebuild - before Task 6 starts, add apostinstallstep andpnpm.onlyBuiltDependencies.better-sqlite3entirely. Task 6 was reworked to snapshot pinned session ids to a plain JSON file at{userData}/pinned.jsonviasrc/main/session-store.ts. No native modules, no rebuild dance, no ABI flip.
Post-v1 status
- Shipping. All 17 tasks complete, M5 reached. A signed-off-locally DMG is produced by
pnpm package. - Key simplifications since the design doc was written:
- No native modules (dropped
better-sqlite3for a JSON snapshot at{userData}/pinned.json). - No rebuild dance -
pnpm packageis justelectron-vite build && electron-builder --mac. - Preload is emitted as CommonJS (
out/preload/index.cjs) because Electron's preload sandbox rejects ESM.
- No native modules (dropped
- Logging live -
electron-logwrites to{userData}/logs/main.log(rolling 5MB x 3). INFO by default, DEBUG whenCV_DEBUG=1. - CI test reports - Vitest emits JUnit + HTML + v8 coverage under
reports/whenCI=true; Playwright emits HTML + JUnit underplaywright-report/. Both uploaded as GitHub Actions artifacts and published viadorny/test-reporter. - E2E coverage - grew from 1 to 3 specs: session-in-sidebar, active-auto-opens-tab (+ canvas renders), and Settings gear to About flow with Esc close.
- GitHub Pages -
.github/workflows/pages.ymlruns on main pushes, builds a site viascripts/build-pages.mjs(blue-themed, sidebar nav, responsive / mobile drawer), publishes docs + unit / coverage / e2e reports underhttps://haimadrian.github.io/claude-village/. - Release workflow -
.github/workflows/release.ymltriggered on tag pushv*or manual dispatch. Builds + packages + publishes the.dmgas a GitHub Release asset. - WebStorm shared launchers -
.idea/runConfigurations/has four launchers (Run app dev, Unit tests, E2E build + Playwright, Build release .dmg). Only that subdir is kept in git; rest of.idea/is ignored.
Post-v1 polish pass (not in the original plan, shipped in-session)
- Hook port is pinned -
127.0.0.1:49251always. If busy the app shows anElectron.dialog.showErrorBoxand quits with exit code 1, so the~/.claude/settings.jsonsnippet never goes stale. E2E bypasses viaCV_HOOK_PORT=0(random port). - Hook snippet correct - uses
--data-binary @-(Claude Code writes payload on stdin, not an env var) and.*regex matcher. Trailing2>/dev/null || true+--max-time 2so the hook is a silent no-op when claude-village isn't running. - UX pass - sidebar sorts by
lastActivityAt; status computed at render time (active < 60s, idle < 10m, else ended) with opacity dim; horizontal tab-nav scroll chrome hidden;Settingsbutton relocated to sidebar bottom withflexShrink: 0+ border-top separator (pinned regardless of session-list scroll); Character name 16px + speech bubble 14px; zone icons + name labels have nativetitle=tooltips; per-tab refresh button top-right of tab body callsrefreshSession(id); global<style>zeroes body/html/#root scrollbars. - Session titles -
custom-titleandsummaryJSONL events captured byevent-normalizerand stored assession.title. Shown in sidebar (truncated 28 chars + ellipsis) and tab nav (14 chars) with full title on hover viatitle=attr. Title-only events do not bumplastActivityAt. - Session age filter - configurable in Settings:
1 day / 1 week / 1 month / 3 months / 6 months / 1 year / All. Default1 month. Persisted inlocalStorageunderclaudeVillage.sessionAgeFilter. Change dispatches acv:filter-changedevent that Shell listens for. - Sidebar refresh - ↻ button next to "Sessions" heading calls
listSessions()IPC + merges, for the chokidar-missed-a-new-file case. Spins 600ms.
Accepted spec deviations from Task 1 (documented for posterity)
viteadded as a direct devDependency (needed sotsconfig.web.json's"types": ["vite/client"]resolves without relying on pnpm hoisting).@vitejs/plugin-reactpinned to^4.7.0(Vite 5 / electron-vite 2.3 compat).Superseded: flat config migration landed inESLINT_USE_FLAT_CONFIG=falseadded tolintandlint:fixscripts (see tech debt above).chore/eslint-flat-config; the env var is no longer needed.
Post-v1 upgrade path (not part of the 17-task plan)
See design doc Section 14 (Asset tiers). Shipped on Tier 1 (programmatic cubes); Tier 2 pipeline is now live.
Tier 2 asset swap - import Kenney.nl CC0 voxel packs, replace box characters and zone props with GLB models loaded viaPipeline landed inuseGLTF.feat/assets-tier2-kenney(commit3b75ce1).useGLTF+Suspense+GltfErrorBoundarywired with programmatic-cube fallback on load failure; 11 placeholder GLBs (~85 KB total) ship today viasrc/renderer/assets/models/. Real Kenney CC0 packs (Mini Characters 1.1, Castle Kit, Nature Kit, Dungeon Pack, Conquer, Platformer Kit) are a filename-matched drop-in with zero code change, tracked as follow-up "drop real Kenney GLBs into place".- Tier 3 custom assets - author bespoke props in MagicaVoxel (tavern, Nether portal, signposts).
- Tier 4 AI-generated props - use Meshy.ai / Luma / Rodin for one-off decorative items, keep prompts in sidecar files.
Post-v1 maintenance pass (2026-04-21)
Five parallel worktrees, each orchestrated by an isolated agent, then squash-merged to main in a single cleanup pass to keep the history readable.
- Agent movement bug fix (
bfad47f- wasfix/agent-movement). Characters were frozen in Tavern because<group position={...}>re-appliedcurrentWorldon every store patch, snapping them back each tool event. Moved initial-position capture into auseRefset once on mount;useFramenow owns the transform. Zone positions and the walkable grid are memoized inVillageScene. Added lightweight per-frame separation steering (src/renderer/village/separation.ts, radius 0.8, strength 3, max 2 u/s) so characters no longer pass through each other. Store now advancesagent.currentZonein lockstep withtargetZoneso zone-focus and tooltips stay coherent. Mayor fixed along the way (shared code path). - Session status "ended" while active fix (
1c68c75- wasfix/session-status-active). Two bugs: the store never flippedstatusback toactivewhen activity arrived after asession-end, and the tab body rendered raws.statusinstead of the derived live status the sidebar uses. Store now reopens on any activity event;deriveStatushoisted intosrc/renderer/sessionStatus.tsand used in both the sidebar and the tab body. - ESLint 9 flat-config migration (
d0c4fe0- waschore/eslint-flat-config)..eslintrc.cjsremoved,eslint.config.js(ESM flat) added. Legacy plugins bridged via@eslint/eslintrcFlatCompat;eslint-plugin-reactuses its native flat export.ESLINT_USE_FLAT_CONFIG=falseand--extdropped from scripts. - Install / Uninstall hook from Settings (
34a67b4- wasfeat/hook-autoinstall). Newsrc/main/hook-installer.tswith purecomputeMerged/computeRemovedhelpers + atomic temp-file-rename filesystem wrappers. Three new IPC channels (hooks:read,hooks:install,hooks:uninstall). Settings screen now shows Install / Uninstall buttons above the manual snippet, with a side-by-side before/after diff modal and a post-action banner. User entries are preserved; ours are identified by port49251. 16 new unit tests. - Tier 2 voxel assets (
3b75ce1- wasfeat/assets-tier2-kenney). See above in the upgrade-path section.
All five streams passed pnpm lint && pnpm typecheck && pnpm test && pnpm build on their branches, and again on main after each squash merge. Post-merge test count: 79 unit + 3 e2e.
Follow-up waves (same day)
Three additional parallel waves after the first batch, also merged as clean squash commits:
- Speech-bubble empty / arrow-only fix (
cbd9d75- wasfix/bubble-keep-last-action). Classifier trims excerpts and falls back toDone/User/Thinkingwhen the summary collapses to an empty string (the previous?? "Done"never fired because""is not nullish). ExportedisTrivialSummaryguardsrecentActions.pushso arrow-only or punctuation-only results (->,...) never overwrite the last meaningful bubble. Timeline still shows arrows. 4 new classifier tests + 2 new session-store tests. - Scene polish (
832199d- wasfeat/scene-polish-world). Drei<Sky>+<Cloud>replace the flat Canvas background, a 200x200 transparent-blue water plane surrounds a round 48-segment grass island. Zone ringRADIUS8 -> 13, walkable grid 32 -> 48. Floating HTML emoji replaced with primitive-geometry 3D icons per zone (books, pickaxe, pine, mug, flame, sparkle, wheat, signpost, office block) in a newZoneIcon3D.tsx. Brown signpost column replaced by an actual post + plank + drei<Text>zone-name, oriented toward the island centre. Characters target a per-(zone, agent) hashed slot outside the zone footprint via a newslots.tshelper so multiple agents at the same zone never overlap with the building. 10 new slot tests, all e2e still green. - Character face, hair, arms (
a72b001- wasfeat/character-face-hair-hands). Minecraft-style decorations rendered as a sharedCharacterDecorationsoverlay on both the Tier 1FallbackCharacterand the Tier 2 cloned GLB: two dark eye boxes and a mouth on the head front face, a hair slab plus front fringe on top (per-agent colour from a 5-entry palette hashed with djb2 in a newappearance.ts), and two skin-coloured arm boxes hanging from the torso. Ghost opacity propagates to every new material. Placeholder GLBs untouched; real Kenney packs already ship their own faces/hair/arms, so no code change needed when they drop in. 4 new appearance tests.
Final post-merge test count: 101 unit + 3 e2e. Main log is still linear: 9 squash commits + 1 docs commit since b7f9edd.
2026-04-21 wave 4: scene depth, legs, signpost, quit-on-close
Four more parallel worktrees, all merged to main as independent squash commits:
- Quit on window close (
b0077b0- wasfix/quit-on-window-close). Dropped theprocess.platform !== "darwin"guard from thewindow-all-closedhandler soapp.quit()fires everywhere. The existingbefore-quithandler tears down the SessionWatcher and HookServer so shutdown is clean. Electron's stock macOS pattern keeps the app alive after the last window closes; claude-village has no useful headless-alive behaviour so this matches the user's expectation. - Character legs (
10d42aa- wasfeat/character-legs). Shortened the Tier 1 torso from 1.6 to 0.8 tall and added two 0.25 x 1.0 x 0.3 leg boxes below so the foot bottom lands on the grass at neutral bob. Legs render on both Tier 1 FallbackCharacter and Tier 2 CharacterMesh via a shared overlay, keeping ghost-opacity plumbing identical. NewtrousersColor(id)helper (5-colour djb2-hashed palette) mirrorshairColor. - Signpost text + zone/signpost tooltips (
b58d81a- wasfix/signpost-text-and-tooltips). Plank swapped to a lighter pine tone; zone-name rendered at 0.28 fontSize in near-black with a white outline, and duplicated on the back plank face so it is readable from every camera angle. ExplicituserData.tooltipKindstamped on every signpost sub-mesh (post, plank, text) plus an invisible generous hitbox, and the cloned GLB scene is traversed so every descendant carrieszone-grounduserData. Hover now reliably resolves the zone tooltip regardless of which part of the building, signpost, or icon the raycast hits. - Scene depth: waves + minor islands + boats + free camera + seabed (
f1324c1- wasfeat/scene-depth-waves-boats). Animated PlaneGeometry water with summed sinusoids and recomputed normals; opaque deep-blue seabed plane at y=-1.6. Main island is now a 3-unit cylinder with earthy sides so it reads as a raised landmass from below. 8 deterministic minor islands (seeded mulberry32, dirt+grass cap + 1-3 cone-on-trunk trees) scattered in the annulus outside the main island. 4 boats on distinct orbits withlookAttangent orientation, bob, pitch, and roll, all driven by one shareduseFrame. OrbitControls getsscreenSpacePanning,minDistance=4,maxDistance=80,maxPolarAngle=0.55pi, plus invisible zone click pads that dispatchvillage:focus-zoneand aCameraTargetLerperthat glides the orbit target to any focused zone or agent.
Post-wave-4 totals: 118 unit + 3 e2e, lint / typecheck / build all green. Linear log, 15 squash + docs commits since b7f9edd.
2026-04-21 wave 5: subagent rendering + walk speed
- Walk speed (
37fd154). Characters used to walk at 3 u/s so a cross-ring traversal took around 8 seconds - usually longer than the gap between tool events, so the character almost never reached its destination before being redirected. BumpedSPEEDto 8 u/s inCharacter.tsx; separation max-step stays at 2 u/s so collision avoidance is still gentle. - Subagent characters render + e2e (
10cce7d). Only the Mayor was ever visible becauseevent-normalizer.tshard-codedagentId: sessionId, kind: "main"for every event, andhook-server.tsonly tagged subagents when the payload carried a non-existentagent_idfield. Fixed by detectingTask/Agenttool dispatches in both ingress paths and emitting syntheticsubagent-start/subagent-endevents withagentId = <sessionId>:<tool_use_id>.normalizeJsonlEventgrew a plural siblingnormalizeJsonlEventsreturningAgentEvent[];SessionWatcherconsumes the array API. Newtests/e2e/multi-agent.spec.tslaunches Electron withCV_HOOK_PORT=49333and an emptyCLAUDE_CONFIG_DIR, POSTs a fullSessionStart -> Read -> Task -> PostToolUse -> Stopflow into the hook server, and asserts both the mayor and the subagent render via drei<Html>label title attributes (Mayor carries the shield prefix, subagents do not). 8 new unit tests (5 normalizer, 3 hook-server) cover Task pre/post pairing, stable id linkage, fallback counter, non-Task no-synthesis, and the nested-subagent guard.
Known follow-up (not shipped in wave 5): Claude Code writes the subagent's own transcript to a SEPARATE JSONL file. The synthetic subagent character currently shows up and expires on the parent's Task pre/post pair but its recentActions stays empty. Wiring the child transcript back into the parent session needs a file correlation layer and was deliberately deferred.
Post-wave-5 totals: 126 unit + 4 e2e. Linear log, 17 squash + docs commits since b7f9edd.
2026-04-21 wave 6: keyboard camera, readable labels, e2e harness
- Keyboard arrow camera (
cdd0604). Arrow keys pan the orbit target in the xz plane relative to the camera's forward direction;+/-/ PgUp / PgDn dolly; Shift-modifier multiplies speed by 2.5. PurepanDeltaForKeys/dollyDeltaForKeyshelpers unit-tested. Handler is a no-op while an input / textarea / contenteditable is focused. Keyboard input cancels any in-flightvillage:focus-zone/village:focus-agentlerp so the user always wins. - Readable labels + white Mayor shirt + orphan-patch guard (
b18cc89). Character labels now show "Mayor" / "Agent N" (1-based, Map insertion order per session) instead of the hashed agent id;agentLabels.tsowns the ordering. Sidebar + tab chrome use Claude Code'ssession.titlefromcustom-title/summaryJSONL events and fall back to "New session" instead of a sliced UUID. Mayor body + arms are a fixed near-white#f2f2f2via a newshirtColorForhelper; subagents keep their deterministic hashed colour. SessionContext hardened: orphanagent-upsert/timeline-appendpatches lazily materialise a placeholder session so a subagent-start patch that lands before its session-upsert sibling no longer drops the character silently. - Multi-agent e2e harness fix (
61a5fb4). The hard-codedCV_HOOK_PORT=49333intests/e2e/multi-agent.spec.tscausedEADDRINUSEon re-runs while the previous Electron child still held the port;src/main/index.tsthen calledapp.exit(1)beforecreateWindow, sofirstWindow()returned a stale handle with no renderer. Fixed by allocating an ephemeral port perbeforeAllvia apickFreePort()helper. The original "0 labels" failure I chased in the main thread was actually a stale build artefact -pnpm e2edoes not build, so running it after a fresh merge compares against the prior bundle. Rebuilding before the run resolves it; usepnpm e2e:fullif in doubt.
2026-04-21 wave 7: waiting indicator, underwater world, sidebar icons
Three parallel worktrees, each merged as an independent squash commit once all three returned and main verified green end-to-end.
- 3D yellow "!" above waiting agents (
d775a0f- wasfeat/waiting-indicator). New optionalAgentState.waitingForInputflag.session-store.ts applyInnersets the flag onassistant-message(main) andsubagent-end; clears it onpre-tool-use,post-tool-use,user-message, andsubagent-start.session-enddoes NOT set it. Each transition emits anagent-upsertpatch.Character.tsxrenders a WaitingIndicator (emissive yellow bar + dot) above the label when the flag is true, with a gentle sine bob and scale pulse; ghost opacity propagates. - Island greenery + deeper ocean + seabed + fish school (
4104e8e- wasfeat/island-sea-decoration).MAIN_ISLAND_HEIGHT3 -> 14 with tapered bottom; seabed moved to y=-18 and gets its ownSeabed.tsxwith a displaced 64x64 sand plane, scattered rocks, swaying seagrass clusters, and coral / anemone tufts. Island grass cap getsIslandGreenery.tsxscattering ~90 grass tufts and ~32 flowers from a seeded mulberry32 layout, excluding zone footprints and character slots. NewFishSchool.tsxrenders 22 fish on seeded circular paths between y=-14 and y=-2; the whole subtree'sgroup.visibleis toggled each frame based oncamera.position.y < -0.2so the fish pay nothing when the camera is above water. OrbitControlsmaxPolarAngle0.55pi -> 0.7pi so the camera can dip underwater. 25 new unit tests on the pure layout + path helpers. - Sidebar Settings + Help + About icon buttons (
696dd9a- wasfeat/sidebar-help-about-icons). Replaced the single "Settings" text button at the sidebar bottom with three 36x36 icon-only buttons: Settings⚙, Help?, Aboutⓘ. NewHelpModal.tsxmirrors AboutModal's shell (Esc to close) and covers overview, camera (orbit + zoom + zone click + timeline click), mouse (tooltips), keyboard shortcuts, and a zones table iteratingsrc/shared/zones.tsso future zone additions surface automatically. Removed the About-from-Settings nested button; the macOSmenu:aboutIPC path stays wired in App.tsx.
Post-wave-7 totals: 180 unit + 4 e2e, lint / typecheck / build all green. Main log: 25 squash + docs commits since b7f9edd.
2026-04-21 wave 8: underwater atmosphere, visual polish, regression fixes
Six small commits that landed one after another as the user play-tested the scene:
- Underwater atmosphere (
ba9dbc0). NewUnderwaterAtmospherecomponent attachesTHREE.FogExp2in deep teal-blue and swapsscene.backgroundto the same colour whencamera.position.y < -0.2; above the threshold both are cleared so drei<Sky>paints as before.<Sky>and<Clouds>wrapped in a single group whose.visibleflips on the same threshold so they stop leaking through the water.WavyWateris alreadyDoubleSide, so looking up from below shows the blue underside. - Underwater fog density reduced (
629c4e3). Initial 0.06 FogExp2 density was ~90 percent opaque at ~30 units and hid the seabed. Dropped to 0.022 so the seabed, coral, and seagrass stay readable while distant minor islands past ~60 units still blur into the blue. - Arrow-key forward speed boost (
2f63d9d). Forward / back pan moves the orbit target along the camera look vector; at the same world-units per second it reads as "slow" while left / right strafing looks fine. NewFORWARD_MULTIPLIER = 2.2applied only toArrowUp/ArrowDown. Diagonal cap bumped so up+right does not race past forward-only. FivekeyboardPantests updated to expect the boosted axis. - GLTF body-tint fix (
a898eb6). All subagents were still rendering white because the GLTF export+load round-trip moves the original mesh name ("body") onto the wrappingObject3Dnode; the actualMeshchild ends up unnamed. The runtime tint checkmesh.name === "body"therefore never matched on loaded GLBs. Fixed by walking the parent chain when deciding whether to tint, so meshes whose ancestor is named "body" pick up the per-agent shirt colour. - Tooltip regression + dedicated e2e (
ac16d0c). Drei<Html>in non-transform mode leavespointer-events: autoon its wrapper div, so the character name label and speech bubble sat above the canvas DOM and swallowed browserpointermoveevents.TooltipLayer's canvas pointermove never fired, the raycaster never ran, and no tooltip appeared while a character was on screen. ForcedpointerEvents: "none"on the Html wrapper AND inner div for the label + speech bubble + tooltip panel itself (speech bubble inner keepsautofor its click handler). Newtests/e2e/tooltip.spec.tssweeps a 5x5 grid over the canvas, asserts the tooltip appears after the 200ms debounce, then clears to 0 onmouse.move(0,0). Regression cannot land silently a fourth time. - Underwater atmosphere tests shipped alongside the fog feature: 3 Vitest cases for a pure
isUnderwater(cameraY)helper.
Post-wave-8 totals: 183 unit + 5 e2e, lint / typecheck / build all green. Main log: 31 squash + docs commits since b7f9edd.
2026-04-21 wave 9: ghost retirement timer wired
- Ghost retirement timer (
feat/wire-ghost-retirement). The Settings input was local React state that nothing else read.IDLE_BEFORE_GHOST_MSis now a mutable instance field onSessionStorewithsetIdleBeforeGhostMs;expireGhostsreads the live field so changes take effect on the next 30s tick. Newsrc/main/user-settings.tspersists the user's choice to{userData}/user-settings.jsonvia the same atomic temp-file + rename pattern the hook installer uses, with pureparseUserSettings/mergeUserSettingshelpers for unit coverage. Two new IPC channels (settings:read,settings:write) validate[1, 60], callstore.setIdleBeforeGhostMs, then persist. SettingsScreen seeds the input from the read on mount, debounces writes at 400ms on change, and surfaces a "Saved" / error banner. Docs (docs/usage.md) now describe the actual single-knob shape (1h ghost TTL fixed).
How to update this file
When an agent starts a task:
| 5 | classifier.ts... | `[~]` | agent-abc (2026-04-20 14:03) | - |
When finishing:
| 5 | classifier.ts... | `[x]` | agent-abc | 1a2b3c4 |
If blocked:
| 5 | classifier.ts... | `[!]` | agent-abc | blocked: waiting on task 2 shared types |