fix: scope caveman mode to the session
Mode was applied per session but stored in one file per machine ($CLAUDE_CONFIG_DIR/.caveman-active). Four bugs followed from that one fact: parallel windows shared a mode; deactivation was spelled "no flag file", so SessionStart re-derived the configured default and undid an explicit "stop caveman" after every auto-compaction; the statusline badge rendered whichever window wrote last; and "off" could not survive a SessionStart at all. payload's `source`, but it read the legacy flag, where off is absence — a deactivated session found nothing stored and fell back to getDefaultMode() regardless. The other three are consequences of the storage shape, not of when the hook re-derives. State now lives in .caveman-sessions/<session_id>.mode, keyed by the session_id Claude Code puts in every hook payload and in the statusline's stdin JSON. The legacy flag stays as a last-write-wins compat mirror and never receives the literal 'off': an older hook reading that would inject "CAVEMAN MODE ACTIVE (off)" and an older statusline would render [CAVEMAN:OFF]. Both spellings of off are read everywhere — a missing file (old) and a literal 'off' (new, durable). caveman-config.js gains the primitives: validateSessionId (a whitelist, because a session id becomes part of a path), resolveActiveMode, readSessionModeRaw, writeSessionMode, per-session prev for one-shot skills (#599), gcSessionStore, and a session_id-tagged recordModeChange (#601). Every one takes a sessionId that may be null or malformed and degrades to the previous machine-wide behavior — the old code path IS the fallback branch, which is why the existing suites, nonid, pass unchanged. SessionStart re-derives the default only on `startup` and `clear`; compact, resume, fork, an unrecognized source and the n` read the stored value. Grouping `clear` with the resets deliberately differs from #691's comment: per-session storage makes the distinction cheap, and nothing else in the conversation survives /clear either. The watchdog must not reset, or a slow payload on a compaction re-arms a session the user turned off. The three entrypoints resolve the new helpers individually instead of demanding them in their requireSibling shape checks (#848): a caveman-config.js from before this change satisfies those checks, so hard-failing would trade "machine-wide mode, no state at all" on exactly the plugin-cache drift that guard exists for. Both statusline ports take session_id from the stdin JSON with bounded, TTY-guarded reads (integer timeout — bash 3.2 rejects fractional read -t) and render nothing for a deactivated session. verify_repo.py greps them against SESSIONS_DIRNAME and SESSION_ID_RE, tws badge. caveman-stats filters the transition l window's switches no longer skew another's es (bin/install.js, uninstall.sh, uninstall.ps1) removes the session store; .caveman-history.jsonl is kept on purpose. opide behaviour — it writes its own flag in its own config dir and never sees a Claude Code session_id. Tests: 25 unit cases in tests/hooks/caveman-config.test.mjs, SessionStartSourceTests, SessionScopedModeTests, 5 statusline cases, and tests/manual/session-mode-smoke.sh (22 checks, end-to-end against a throwaway config dir). docs/testing-session-modes.md carries the full plan including the live-Claude-Code checks. Hook ced. npm test 197/197 · tests/*.js clean · unittesss · smoke 22/22 Signed-off-by: Pavel Filippenko <filippenko.ps@phystech.edu>
P
Pavel Filippenko committed
f436843949dcfe937543a12ea87f26d4a315c10c
Parent: 715a7a0