Bugfix warmup — world skill inbox, load-context session claim, index regen triggers#40
Bugfix warmup — world skill inbox, load-context session claim, index regen triggers#40willsupernormal wants to merge 3 commits into
Conversation
The Load Sequence ran `ls 03_Inbox/ 2>/dev/null` with a relative path. When the Bash tool's cwd isn't exactly the world root the command silently returns empty, and the skill renders "0 items (clean)" even when real files are sitting in 03_Inbox/. The hook counterpart alive-inbox-check.sh works correctly because it uses find_world + absolute path — the skill side didn't have the same discipline. Replaced with a one-liner that resolves the world root from ~/.config/alive/world-root (the install-time config file, always present on a configured install, cross-platform safe) and then lists the absolute inbox path. Works regardless of the Bash tool's cwd. Verified: old command from /tmp returns empty; new command from /tmp correctly lists both files currently in Will's 03_Inbox/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…re (t002)
The Tier 1 brief pack flow had no instruction to update the squirrel
YAML's walnut: field after loading a walnut. Squirrel YAMLs are
created by alive-session-new.sh with walnut: null and only get the
walnut name written at save time, so any pre-save session window is
invisible to consumers that read the YAML.
The visible symptom: alive-context-watch.sh exits at line 160
([ "$WALNUT" = "null" ] && exit 0) before reaching the
external-change-detection block, so when one session saves to a
walnut the parallel session never sees the "another session just
saved to X" injection on its next UserPromptSubmit. Same gap also
silently degrades the statusline and project.py's recent-sessions
aggregator, both of which read walnut: from squirrel YAMLs.
Fix is one new mandatory step at the end of Tier 1 telling load to
Edit .alive/_squirrels/{session_id}.yaml and replace
"walnut: null" with "walnut: {name}". Single Edit call, uses the
session ID injected at session start. Also updated the visual
"reads as you go" example to surface the claim.
Verified empirically end-to-end: set my own session's squirrel YAML
to walnut: alive-os, set lastcheck marker to a time before
alive-os/_kernel/now.json's mtime, ran the existing context-watch
hook unmodified — it correctly emitted "Another session just saved
to alive-os. Changed: now.json tasks.json log.md..." additionalContext.
The existing hook works fine, the only missing link was the walnut
field never getting populated.
Follow-ups (not in this PR): create-walnut and bundle skills should
also claim the session, and there's a broader question for Ben about
whether walnut-claim should be hook-enforced rather than skill-instructed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…003) generate-index.py was only triggered by the post-write hook chain log.md -> project.py -> now.json -> generate-index.py. Any state change that doesn't route through a walnut save left _index.yaml stale until the next save -- files dropped into 03_Inbox/ via Finder, parallel session writes, manual edits, agent captures. Two narrow additions to close the gap: 1. alive-post-write.sh: add */03_Inbox/* to the existing case statement that triggers generate-index.py. Net effect: when the agent uses Write or Edit on a file under any 03_Inbox/, the index regenerates (debounced via the existing /tmp/alive-index-regen marker, 5 minute window). 2. alive-session-new.sh: run generate-index.py synchronously just before reading _index.yaml for the SessionStart injection. Measured at ~110ms on a 16-walnut world, well under the 10s hook timeout. Touches the debounce marker so subsequent post-write triggers within 5 minutes skip. This catches everything that happened between sessions -- Finder drops, parallel saves, external edits -- so the injected <WORLD_INDEX> is always fresh from byte one of every session. Verified end-to-end: - post-write hook fed a fake Write to 03_Inbox/foo.txt -> index mtime jumped, debounce marker created. - session-new hook fed a backdated index (-2h) -> index mtime updated to NOW, output contained <WORLD_INDEX> block, fake session YAML cleaned up. The Finder-during-active-session window is still uncovered (no tool event fires for that path), but the world skill's t001 fix already does live ls for inbox detection so the dashboard stays accurate. The only stale consumer in that window is the injected snapshot, which is acceptable -- it gets refreshed on the next session start. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
patrickSupernormal
left a comment
There was a problem hiding this comment.
Overall
Strong diagnostics and writeup across all three bugs. Recommend merging after restructuring t001 -- the other two ship as-is with minor follow-ups.
t001 (world/SKILL.md) -- blocker
The fix reads ~/.config/alive/world-root as the sole resolution path, on the premise that the file is "always present on a configured install." Verified against origin/main: no commit in repo history has ever written that file.
git grep "world-root" origin/mainreturns one real code site (plugins/alive/hooks/scripts/alive-common.sh:135-139) plus two docstrings ingenerate-index.py/generate-graph.py.git log --all -S ".config/alive/world-root"andgit log --all -G "mkdir -p.*\.config/alive"both return empty.hermes/install.sh,plugins/alive/onboarding/world-builder.html,docs/,plugins/alive/CLAUDE.md-- zero references.
find_world() treats the file as a fallback after cwd walk-up for 01_Archive/ + 02_Life/ markers. On installs where walk-up works (the normal case), the file has never existed. Confirmed on my own install: ~/.config/alive/ doesn't exist, alive has been working fine for months via walk-up.
Failure mode matches the bug being fixed: $WR empty -> ls "/03_Inbox/" -> empty -> dashboard renders "0 items (clean)".
Suggestion: source alive-common.sh and invoke find_world to inherit the full resolution strategy. Alternative: inline walk-up-then-config-fallback in the one-liner. Either way, don't stake the fix on a file the codebase doesn't create.
Secondary: if the config file IS intended as canonical for skill-side lookups, the install flow needs to write it. The alive-common.sh:135 comment ("world-root stored at install time") describes a contract the repo doesn't honor. Probably adjacent to the walnut-claim-hook conversation already flagged in Follow-ups.
t002 (load-context/SKILL.md) -- ship it
Empirically confirmed in an unrelated session: my squirrel YAML still reads walnut: null 30+ minutes after running /alive:load-context alive, exactly as described. The patch is the missing piece.
One non-blocking nit: the step text's example walnut names (walnut: berties, walnut: alive-os) are world-specific -- would read cleaner as walnut: {name} to match the rest of the skill's voice.
Endorse the hook-enforcement follow-up. PostToolUse on Read of */key.md is probably the right seat for this -- skill instructions depend on agents reading them, hooks don't.
t003 (post-write + session-new hooks) -- ship it
Clean fixes. Two nits worth considering before or after merge:
python3 "$GENERATOR" ... > /dev/null 2>&1 || trueswallows stderr. Silent regen failures leave the session booting on a stale index with no signal. Consider redirecting stderr to a rotating log (e.g.$WORLD_ROOT/.alive/_generated/index-regen.log) and surfacing a banner in the SessionStart injection when the last regen errored.- Scaling: 110ms on 16 walnuts. Linear would put 100 walnuts around 700ms -- still within the 10s hook budget, but worth documenting expected scaling so future authors know when this becomes a concern.
Uncovered edge case (Finder drops during active session) is acknowledged and acceptable.
Pattern observation
The closing observation about trigger-graph vs pull-based freshness is the real insight in this PR. The three bugs are symptomatic of the same assumption -- projections stay fresh because saves fire often. They don't. This class keeps biting until either every state mutation fires a trigger, or consumers check freshness at read time.
Worth pulling into one design conversation alongside the capture-context archive follow-up and the walnut-claim hook-enforcement follow-up. Same pattern, three symptoms.
Superseded by #49Hey Will — this one's been absorbed into the v3 skill audit PR, which just merged as 3.1.0 (commit 292f211 on main). All three of this PR's commits are in #49:
Safe to close. The diagnostic writeups here were great — the commit bodies in #49 inherit the same clarity. |
Summary
Three bugs diagnosed during the 2026-04-14 always-alive-loop design session, all sharing a pattern: hooks and skills with the right intent but brittle implementation that silently failed. Shipping as a precursor PR so larger work (always-alive-loop, Cowork restoration) lands on clean foundations.
4 files, +31/-6, 3 commits — one per bug. All three have empirical end-to-end verification against a real ALIVE world. No new runtime dependencies.
t001 —
/alive:worldskill inbox detection (skills/world/SKILL.md)Diagnosis. The Load Sequence ran
ls 03_Inbox/ 2>/dev/null | grep -v '^\.' | grep -v '^Icon'— a relative path. When the Bash tool's cwd isn't exactly the world root the command silently returns empty and the dashboard renders "0 items (clean)" even with real files in the inbox. The hook counterpartalive-inbox-check.shworks correctly because it usesfind_world+ absolute path; the skill side didn't have the same discipline.Fix. Replaced with a one-liner that resolves the world root from
~/.config/alive/world-root(the install-time config file, cross-platform, always present on a configured install — already used byalive-common.sh'sfind_world):Note on
$WORLD_ROOTshadow bug. The existing$WORLD_ROOTreference elsewhere in the same skill (line 232, thegenerate-index.pyinvocation) is a pre-existing latent bug —alive-session-new.shwritesALIVE_WORLD_ROOT(different name) toCLAUDE_ENV_FILE, notWORLD_ROOT. The skill's$WORLD_ROOTis empty in the Bash tool's shell. Out of scope for this PR but worth flagging.Verification (before/after from
/tmp):5 dummy test cases all green: real inbox, empty inbox, mixed inbox with
.DS_Store+.archive/subdir correctly filtered, run from unrelated cwd (~/Library), regression test of the old broken command.t002 —
/alive:load-contextdoesn't claim the session for its walnut (skills/load-context/SKILL.md)Diagnosis. Originally suspected to be a bug in
alive-context-watch.sh(stale tmp markers, session ID compare, mtime logic). Tracing the hook against a real world showed the hook logic is correct — it was bailing on line 160 ([ "$WALNUT" = "null" ] && exit 0) and never reaching the external-change-detection block.Root cause.
alive-session-new.shwrites the squirrel YAML at session start withwalnut: null. Nothing updates that field until save time.load-context/SKILL.mdhad zero instructions to claim the walnut by writing it into the session's squirrel YAML. So between session start and first save, the YAML'swalnut:field is null, and any consumer that reads it (the context-watch hook, the statusline,project.py's recent-sessions aggregator) silently no-ops.Confirmed live: a real session that had been working on a walnut for 90 minutes had
walnut: null,saves: 0, and zero/tmp/alive-lastcheck-{session-id}marker file.Fix. Added a new mandatory step 4 to Tier 1 instructing load-context to Edit
.alive/_squirrels/{session_id}.yamland replacewalnut: nullwithwalnut: {name}. Single Edit call. Uses the session ID from the SessionStart injection. If the YAML already has a different walnut name (rare — cross-walnut session), the Edit'sold_string: "walnut: null"won't match and the agent surfaces the conflict instead of silently overwriting.End-to-end verification:
walnut: alive-os(simulating what the new instruction does)/tmp/alive-lastcheck-{session-id}to 1 hour beforealive-os/_kernel/now.json's mtime (simulating "another session saved between our last prompt and now")alive-context-watch.shagainst simulated stdinHook output:
{ "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "Another session just saved to alive-os. Changed: now.json tasks.json log.md. You should re-read _kernel/now.json, _kernel/tasks.json and _kernel/log.md before continuing -- your context may be stale. Ask the human if they want you to refresh." } }The injection fires correctly. The hook was never broken — the only missing link was the walnut field never being populated. Negative test also green: same setup with
walnut: nullcorrectly bails out without injecting.t003 — index regeneration triggers (
hooks/scripts/alive-post-write.sh,hooks/scripts/alive-session-new.sh)Diagnosis.
generate-index.pywas only triggered by the post-write hook chainlog.md → project.py → now.json → generate-index.py. Any state change that doesn't route through a walnut save left_index.yamlstale until the next save — files dropped into03_Inbox/via Finder, parallel session writes, manual edits, agent captures.Fix — two narrow additions:
alive-post-write.sh: added*/03_Inbox/*to the existing case statement that triggersgenerate-index.py. When the agent uses Write or Edit on a file under any03_Inbox/, the index regenerates (debounced via the existing/tmp/alive-index-regenmarker, 5-minute window).alive-session-new.sh: rungenerate-index.pysynchronously just before reading_index.yamlfor the SessionStart injection. Measured at ~110ms on a 16-walnut world, well under the 10s SessionStart hook timeout. Touches the debounce marker so subsequent post-write triggers within 5 minutes skip. This catches everything that happened between sessions — Finder drops, parallel saves, external edits — so the injected<WORLD_INDEX>is always fresh from byte one of every session.Verification (6 dummy tests, all green):
03_Inbox/dummy-test.txt→ index mtime jumped from backdated 13:53 → 15:53, debounce marker created.berties/random-file.txt) → no regen (proves the new glob doesn't accidentally match unrelated paths).now.jsonstill triggers regen (existing behavior unaffected)._index.yamlto -3h, ranalive-session-new.sh→ index mtime updated to NOW, output contains<WORLD_INDEX>block._index.yamlintegrity preserved after regen.Uncovered edge case (acceptable for v1). Files dragged into
03_Inbox/via Finder during an active session don't fire any Claude Code tool event, so neither trigger catches them. The world skill's t001 fix already does livelsfor inbox detection so the dashboard view stays accurate; the only stale consumer in that window is the injected<WORLD_INDEX>snapshot, which gets refreshed on the next session start.Pattern observation
Three of the three bugs share a pattern: hooks and skills had the right intent but brittle implementation that silently failed.
The architectural insight: ALIVE projections (
_index.yaml,now.json) assume save events fire frequently enough to keep derived state fresh. They don't. Any state change that bypasses the post-write trigger chain leaves projections stale indefinitely. This class of bug will keep biting other parts of the system until either the trigger graph widens to cover every state mutation, or projections become pull-based with freshness checks at the consumer side. This PR widens the trigger graph for the inbox case and the session-start case — worth a structural conversation about going further.Pre-existing edge case discovered (not in this PR, just flagging)
In
alive-context-watch.sh, the CTX_PCT re-injection block (lines 33-148) exits at line 147 when a threshold fires, before reaching the external-change-detection block. This means the cross-session injection is preempted on the (at most 4) prompts per session where CTX_PCT crosses a 20%/40%/60%/80% threshold. Lastcheck marker doesn't advance on threshold-firing prompts (the earlyexit 0skips line 201'sdate +%s > "$LASTCHECK"), so the very next non-firing prompt catches up correctly. Mostly benign — at worst a one-prompt delay, never a lost detection — but worth noting in case anyone trips over it later. Could be cleaned up by emitting bothadditionalContextblocks in a single hook output instead of exiting early. Out of scope for this warmup PR.Follow-ups (intentionally not in this PR)
capture-context/SKILL.mddoesn't reliably move source files out of03_Inbox/after routing them into a bundle'sraw/. Designed and tested an archive-to-03_Inbox/.archive/YYYY-MM/patch in the same session, then rolled it back at the user's request — they're still discussing whether to keep originals as raw+references or pure-archive. Will reopen as a separate PR once that decision lands.create-walnutandbundleskills should also claim the session in the squirrel YAML when they're the entry point to a walnut. Open question: should walnut-claim be hook-enforced (e.g. PostToolUse on Read of*/key.md) rather than skill-instructed? Skill instructions depend on the agent following them; hook enforcement is guaranteed.$WORLD_ROOTshadow bug inworld/SKILL.mdline 232 — thepython3 .alive/scripts/generate-index.py "$WORLD_ROOT"invocation references a variable that's never set in the Bash tool's shell. Should follow the same~/.config/alive/world-rootresolve pattern from t001. Tiny fix, didn't want to expand scope.Test plan
/tmpcwd reproductionstat,sed -i,md5sum/shasumall have proper BSD fallbacks; nojq/yq/gdate/etc; no bash 4+ features; no hardcoded paths in hook code)diff -rclean)/alive:world, verify inbox count is correct; run/alive:load-context {walnut}, verify the session's squirrel YAML is updated towalnut: {name}. (This is the only thing I can't run from inside an existing session.)🤖 Generated with Claude Code