Skip to content

Bugfix warmup — world skill inbox, load-context session claim, index regen triggers#40

Closed
willsupernormal wants to merge 3 commits into
mainfrom
will/bugfix-warmup-pr
Closed

Bugfix warmup — world skill inbox, load-context session claim, index regen triggers#40
willsupernormal wants to merge 3 commits into
mainfrom
will/bugfix-warmup-pr

Conversation

@willsupernormal

Copy link
Copy Markdown
Contributor

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.

Note: supersedes #39 (closed). That PR was opened from a fork under a misconfigured identity. Same 3 commits, same diff, same verification — re-opened from a same-repo branch under the right account.


t001 — /alive:world skill 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 counterpart alive-inbox-check.sh works correctly because it uses find_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 by alive-common.sh's find_world):

WR=$(cat ~/.config/alive/world-root 2>/dev/null | tr -d '[:space:]'); ls "$WR/03_Inbox/" 2>/dev/null | grep -v '^\.' | grep -v '^Icon'

Note on $WORLD_ROOT shadow bug. The existing $WORLD_ROOT reference elsewhere in the same skill (line 232, the generate-index.py invocation) is a pre-existing latent bug — alive-session-new.sh writes ALIVE_WORLD_ROOT (different name) to CLAUDE_ENV_FILE, not WORLD_ROOT. The skill's $WORLD_ROOT is empty in the Bash tool's shell. Out of scope for this PR but worth flagging.

Verification (before/after from /tmp):

=== BEFORE (old relative path from /tmp) ===
(empty output = bug)

=== AFTER (new absolute path from /tmp) ===
ThoughtFox Dev Onboarding Guide Apr 14 2026.txt
Will x Ben Standup Supernormal Alive Os.md

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-context doesn'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.sh writes the squirrel YAML at session start with walnut: null. Nothing updates that field until save time. load-context/SKILL.md had zero instructions to claim the walnut by writing it into the session's squirrel YAML. So between session start and first save, the YAML's walnut: 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}.yaml and replace walnut: null with walnut: {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's old_string: "walnut: null" won't match and the agent surfaces the conflict instead of silently overwriting.

End-to-end verification:

  1. Set a real session's squirrel YAML to walnut: alive-os (simulating what the new instruction does)
  2. Backdated /tmp/alive-lastcheck-{session-id} to 1 hour before alive-os/_kernel/now.json's mtime (simulating "another session saved between our last prompt and now")
  3. Pre-set the CTX_PCT thresholds markers so the re-injection block doesn't preempt
  4. Ran the unmodified alive-context-watch.sh against simulated stdin

Hook 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: null correctly bails out without injecting.


t003 — index regeneration triggers (hooks/scripts/alive-post-write.sh, hooks/scripts/alive-session-new.sh)

Diagnosis. 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.

Fix — two narrow additions:

  1. alive-post-write.sh: added */03_Inbox/* to the existing case statement that triggers generate-index.py. 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 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):

  • post-write inbox trigger: fed a fake Write to 03_Inbox/dummy-test.txt → index mtime jumped from backdated 13:53 → 15:53, debounce marker created.
  • Negative test: post-write to a non-inbox/non-now.json file (berties/random-file.txt) → no regen (proves the new glob doesn't accidentally match unrelated paths).
  • Regression test: post-write to now.json still triggers regen (existing behavior unaffected).
  • session-start sync regen: backdated _index.yaml to -3h, ran alive-session-new.sh → index mtime updated to NOW, output contains <WORLD_INDEX> block.
  • _index.yaml integrity 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 live ls for 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.

  • t001: relative path that depends on cwd
  • t002: missing skill instruction → null field → consumers no-op
  • t003: narrow trigger conditions that don't cover the full state-change surface area

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 early exit 0 skips line 201's date +%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 both additionalContext blocks 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 archive behavior. capture-context/SKILL.md doesn't reliably move source files out of 03_Inbox/ after routing them into a bundle's raw/. 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.
  • Walnut-claim coverage. create-walnut and bundle skills 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_ROOT shadow bug in world/SKILL.md line 232 — the python3 .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-root resolve pattern from t001. Tiny fix, didn't want to expand scope.
  • CTX_PCT preemption — see above.

Test plan

  • t001 verified before/after with /tmp cwd reproduction
  • t001 5 dummy test cases (real, empty, mixed-with-dotfiles, unrelated cwd, regression)
  • t002 verified end-to-end with real session YAML, real walnut, simulated parallel-session save
  • t002 negative test (walnut: null still bails — proves load fix is necessary)
  • t003 inbox trigger verified with fake post-write hook input
  • t003 negative test (non-inbox file doesn't trigger)
  • t003 regression test (now.json trigger still works)
  • t003 session-start regen verified with backdated index + fake SessionStart input
  • Windows dep audit clean (no fixes required — stat, sed -i, md5sum/shasum all have proper BSD fallbacks; no jq/yq/gdate/etc; no bash 4+ features; no hardcoded paths in hook code)
  • Cache-vs-clone parity verified (diff -r clean)
  • Live sanity check by maintainer: open a fresh Claude Code session in a real world, run /alive:world, verify inbox count is correct; run /alive:load-context {walnut}, verify the session's squirrel YAML is updated to walnut: {name}. (This is the only thing I can't run from inside an existing session.)

🤖 Generated with Claude Code

willsupernormal and others added 3 commits April 15, 2026 11:32
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 patrickSupernormal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/main returns one real code site (plugins/alive/hooks/scripts/alive-common.sh:135-139) plus two docstrings in generate-index.py / generate-graph.py.
  • git log --all -S ".config/alive/world-root" and git 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:

  1. python3 "$GENERATOR" ... > /dev/null 2>&1 || true swallows 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.
  2. 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.

@patrickSupernormal

Copy link
Copy Markdown
Collaborator

Superseded by #49

Hey 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:

This PR In #49 as Same mechanism?
t001 world inbox absolute path 04daf8c fix(world skill): use absolute path for inbox detection (t001) Yes — same ~/.config/alive/world-root approach
t002 load-context session claim aa235a2 fix(load-context): claim session for walnut so cross-session hooks fire (t002) Yes
t003 index regen triggers f6a3589 fix(index): broaden regen triggers to catch non-save state changes (t003) Yes — plus the synchronous session-start regen on top

Safe to close. The diagnostic writeups here were great — the commit bodies in #49 inherit the same clarity.

@patrickSupernormal

Copy link
Copy Markdown
Collaborator

Closing this on our side since all three commits shipped in #49 (merged as 3.1.0 on Apr 20). No response needed from you — reopen if you disagree that it's fully absorbed.

Thanks for the clean diagnostics, the writeups from here carried over into the #49 commit bodies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants