| Dimension | Score | Delta | Notes |
|---|---|---|---|
| Cohesion | 7.0 / 10 | ▲ +0.5 | Extracted modules are exemplary: idle_tracker (29L), event_reader (70L). window_tick (610L) and session.py (789L) remain broad. |
| Coupling Strength | 6.5 / 10 | ▲ +1.5 | Callback injection pattern reduces hidden couplings. session.py 32-handler fan-out and claude_task_state 4-write-path remain. |
| Layer Discipline | 7.5 / 10 | ▲ +2.5 | Major wins: run_broker_cycle → callback; status_bubble severed from polling_strategies; shell subprocess routed through tmux_manager. |
| Encapsulation | 6.5 / 10 | = 0 | WindowStateStore API enforced; view_window() established. claude_task_state write discipline soft — 4 independent write paths persist despite declared "single authority." |
| Volatility Management | 6.5 / 10 | ▲ +1.0 | Provider abstraction is excellent. Session state changes cascade through 32 handlers (most volatile coupling). |
| Testability | 5.5 / 10 | ▲ +0.5 | New small modules are highly testable. 6 global singletons remain test obstacles for handlers. |
| AI Context Efficiency | 6.5 / 10 | ▲ +1.0 | Callback injection makes hidden deps visible. Deferred imports mostly eliminated. session.py 32-handler fan-out is the largest remaining context burden. |
| Architectural Clarity | 7.5 / 10 | (new) | Excellent documentation, consistent patterns (callback injection, self-registration). All patterns named and declared. |
| Overall | 6.8 / 10 | ▲ +1.3 | Meaningful improvement from 5.5 this morning. Trajectory is positive and concrete. Remaining issues are manageable for a solo project. |
12 concrete fixes applied across two refactoring sessions:
| Fix | Effect |
|---|---|
parse_session_map duplicate removed | Single source of truth |
window_store.window_states dict access → store API | Encapsulation enforced |
_has_insert_indicator → has_insert_indicator | No private API crossing |
_get_provider() 6 duplicates → 1 extracted function | DRY |
session_monitor.py monolith → 4 sub-modules | Cohesion, testability |
claude_task_state cleanup consolidated in session_lifecycle | Single authority declared |
hook_events → periodic_tasks → _stop_callback registration | Layer discipline |
Deferred message_queue imports → module-level | Dependency graph now visible |
status_bubble → polling_strategies → register_rc_active_provider | Subsystem boundary clean |
shell_capture ↔ shell_commands → CommandApprovalCallback Protocol | Main cycle broken |
_capture_with_scrollback subprocess → tmux_manager.capture_pane_scrollback() | Single tmux I/O path |
session_manager.get_window_state() bypasses → view_window() | Access discipline |
idle_tracker.py (29L, zero project imports) ·
event_reader.py (70L, only aiofiles + HookEvent) ·
session_lifecycle.py (115L, declared single authority) ·
Callback injection used consistently ·
CommandApprovalCallback Protocol breaks shell cycle
session.py Structural Hub: 32 Handler Dependents [High]Files: session.py (789 lines, 47 methods). Verified: 32 handler modules import session_manager directly, plus bot.py and session_monitor.py.
The public API surface remains broad after extractions:
Display names: get_display_name(), set_display_name(), sync_display_names()
Session map: load_session_map(), register_hookless_session(), wait_for_session_map_entry()
Session resolve: resolve_session_for_window(), get_recent_messages(), get_session_id_for_window()
Lifecycle: prune_stale_state(), audit_state(), resolve_stale_ids()
Provider: get_window_provider(), set_window_provider()
Modes: get_approval_mode(), set_window_approval_mode()
get_notification_mode(), set_notification_mode(), cycle_notification_mode()
get_batch_mode(), set_batch_mode(), cycle_batch_mode()
State: view_window(), clear_window_session(), set_window_cwd()
| Dimension | Assessment |
|---|---|
| Strength | HIGH — 32 callers make direct functional calls to mutable state |
| Distance | LOW — solo project, same process |
| Volatility | HIGH — WindowState schema extended repeatedly; new per-window flags cascade to 32 callers |
| Balance | UNBALANCED — HIGH strength + HIGH volatility. Distance=0 softens but does not eliminate the AI context burden. |
Root cause: Two distinct responsibilities fused in one object: (1) window state store — modes, CWD, provider, display names; (2) session resolver — session history, message lookup, session map sync.
Recommendation: The extracted WindowStateStore already exists. Have handlers import it directly for mode/state operations, and a new SessionResolver for history/message operations. Each handler's actual dependency shrinks to 1–2 method groups rather than the entire 47-method surface.
claude_task_state Has 4 Independent Write Paths [High]Files: claude_task_state.py (491 lines), 4 writer modules across 2 layers.
| Caller | Operations | Layer |
|---|---|---|
session_lifecycle.py | clear_window(), clear_subagents() | Core |
transcript_reader.py | apply_entries(), rebuild_from_entries() | Core |
handlers/hook_events.py | set_wait_header(), clear_wait_header(), mark_task_completed(), add_subagent(), remove_subagent() (5 ops) | Handler |
handlers/window_tick.py | clear_wait_header(), set_last_status() | Handler |
The authority violation: session_lifecycle.py declares: "callers must NOT touch claude_task_state or subagent state directly." Yet hook_events.py writes add_subagent/remove_subagent and window_tick.py writes clear_wait_header directly.
Coordination conflict: Both hook_events.py (Stop event) and window_tick.py (idle detection) independently call clear_wait_header. The coordination is implicit via a comment in hook_events.py:217.
| Dimension | Assessment |
|---|---|
| Strength | HIGH — 4 callers write to shared module-level dict, no ownership enforcement |
| Distance | HIGH — crosses monitoring layer ↔ handler layer |
| Volatility | HIGH — Claude Code adds new task types and subagent patterns regularly |
| Balance | UNBALANCED — HIGH strength + HIGH distance + HIGH volatility. All three dimensions bad. |
Recommendation: Move add_subagent/remove_subagent from hook_events into session_lifecycle (via new handle_subagent_start/handle_subagent_stop methods). Pick a single owner for clear_wait_header lifecycle and document the decision.
hook_events.py SessionEnd Orchestrates 5 Subsystems [Medium]File: handlers/hook_events.py, _handle_session_end() lines 287–312
session_lifecycle.handle_session_end(window_id) # Core lifecycle session_manager.clear_window_session(window_id) # Core state terminal_poll_state.clear_seen_status(window_id) # Polling infra thread_router.resolve_chat_id(user_id, thread_id) # Routing update_topic_emoji(bot, ...) # UI enqueue_status_update(bot, ...) # Message delivery
| Dimension | Assessment |
|---|---|
| Strength | HIGH — direct orchestration of 5 modules |
| Distance | LOW — solo project, same process, all in handlers/ |
| Volatility | HIGH — new hook event types added as Claude Code evolves |
| Balance | BORDERLINE — LOW distance makes this tolerable, but each new per-session subsystem requires modifying this handler. |
Recommendation: Absorb session_manager.clear_window_session() into session_lifecycle.handle_session_end(). Register terminal_poll_state.clear_seen_status as a cleanup callback. This reduces _handle_session_end to: call session_lifecycle + deliver UI updates.
window_tick.py: Wide Orchestration Hub [Low–Medium]File: handlers/window_tick.py (610 lines, 19 functions, 23 import sources). Sole public entry point: tick_window() at line 576.
The 19 functions could be grouped into 3 logical units: status resolution · UI event handling · state transitions — each ~150–200 lines. This is a complexity issue, not a coupling problem.
| Dimension | Assessment |
|---|---|
| Strength | HIGH — imports from 23 sources, writes to claude_task_state, calls 8 subsystems |
| Distance | LOW — same handlers package |
| Volatility | MEDIUM — polling behavior evolves with features but core loop is stable |
| Balance | BORDERLINE — orchestrator pattern inherently wide; extracting sub-modules is an option after Issues 1–2 are resolved. |
shell_commands → shell_capture [Low]File: handlers/shell_commands.py, line 229:
from .shell_capture import mark_telegram_command mark_telegram_command(window_id, command, user_id, thread_id)
The main cycle was broken by CommandApprovalCallback. This one-direction deferred import is a historical artifact — no cycle exists anymore. Moving the import to module-level is a one-line fix.
| Dimension | Assessment |
|---|---|
| Strength | LOW — one function, one direction |
| Distance | LOW — same package |
| Volatility | LOW — stable tracking function |
| Balance | TOLERABLE — low volatility makes this acceptable. One-line fix. |
| Issue | Strength | Distance | Volatility | Priority | Status |
|---|---|---|---|---|---|
session.py hub — 32 handler dependents |
HIGH | LOW | HIGH | High | Open |
claude_task_state — 4 write paths, authority violated |
HIGH | HIGH | HIGH | High | Open (authority declared, not enforced) |
hook_events SessionEnd — 5 subsystems |
HIGH | LOW | HIGH | Medium | Open (LOW distance softens) |
window_tick.py — 610L orchestration hub |
HIGH | LOW | MED | Low–Med | Open (complexity, not coupling) |
shell_commands:229 deferred import |
LOW | LOW | LOW | Low | Open (one-line fix) |
| Fix | Verified State |
|---|---|
parse_session_map duplicate removed | ✅ Single parse in session_map.py |
window_store.window_states dict access → store API | ✅ view_window() pattern established |
_has_insert_indicator promoted to public | ✅ has_insert_indicator in tmux_manager.py |
_get_provider() 6 duplicates → 1 at window_tick.py:64 | ✅ Single definition confirmed |
session_monitor.py → 4 sub-modules | ✅ event_reader (70L), idle_tracker (29L), session_lifecycle (115L), transcript_reader (416L) |
claude_task_state cleanup → session_lifecycle | ✅ handle_session_end owns clear_window + clear_subagents |
hook_events → periodic_tasks → _stop_callback | ✅ No run_broker_cycle call in hook_events.py |
Deferred message_queue imports → module-level | ✅ enqueue_status_update imported at line 33 |
status_bubble → polling_strategies → register_rc_active_provider | ✅ Zero coupling to polling_strategies |
shell_capture ↔ shell_commands → CommandApprovalCallback Protocol | ✅ Cycle broken; _approval_callback slot at line 65 |
_capture_with_scrollback subprocess → tmux_manager | ✅ No subprocess calls in shell_capture.py |
session_manager.get_window_state() → view_window() | ✅ Access pattern established |
| Pattern | Example | Use When |
|---|---|---|
| Callback injection | register_rc_active_provider(), _stop_callback |
Subsystem A needs to call into B at runtime, but static import would create a layer violation |
| Self-registration decorator | topic_state_registry, callback_registry |
Cleanup functions or handlers that need to be discoverable without explicit startup registration |
| Protocol boundary | AgentProvider, CommandApprovalCallback, WhisperTranscriber |
Replace concrete type deps when the dependency crosses a significant boundary |
| Pure dataclass module | message_task.py (zero project imports), expandable_quote.py |
Sum types or constants that would otherwise create circular deps |
| Singleton extracted module | idle_tracker (29L), event_reader (70L) |
Stateful singleton with a narrow responsibility — extract before it grows |
claude_task_state write discipline — move add_subagent/remove_subagent from hook_events into session_lifecycle. Pick a single owner for clear_wait_header. 2–3 file changes, high leverage.session.py into two narrower interfaces — extract SessionResolver (history, message lookup, session map) from SessionManager (mode settings, window state). Handlers import only what they need.session_manager.clear_window_session() into session_lifecycle — consolidate SessionEnd cleanup. Reduces hook_events._handle_session_end to lifecycle call + UI updates.shell_commands:229 deferred import — one-line fix. Move from .shell_capture import mark_telegram_command to module top.window_tick.py into status resolution, UI event handling, and state transitions — only worthwhile after Items 1–2 reduce the import surface.