Skip to content

Phases A + B: CodingAgent protocol and per-session agent selection#197

Open
dhilgaertner wants to merge 2 commits intomainfrom
feature/crow-166-agent-abstraction-phase-a
Open

Phases A + B: CodingAgent protocol and per-session agent selection#197
dhilgaertner wants to merge 2 commits intomainfrom
feature/crow-166-agent-abstraction-phase-a

Conversation

@dhilgaertner
Copy link
Copy Markdown
Contributor

@dhilgaertner dhilgaertner commented Apr 22, 2026

Closes #166 and #167. Parent spec: #150. Phase A is behavior-preserving; Phase B adds data-model/UI plumbing without exposing any new user-visible agents yet. Phase C (#168) flips a bit by registering a second agent in AgentRegistry — no new call sites needed.

Phase A — CodingAgent protocol (behavior-preserving) — closes #166

  • Adds CodingAgent, AgentKind, AgentRegistry, StateSignalSource, AgentStateTransition, HookConfigWriter protocols in CrowCore, plus an AgentHookEvent payload type so CrowCore stays free of JSONValue.
  • Implements ClaudeCodeAgent, ClaudeHookConfigWriter (moved from Sources/Crow/App/HookConfigGenerator.swift), and ClaudeHookSignalSource in Packages/CrowClaude/; registers ClaudeCodeAgent in AgentRegistry.shared at app launch.
  • Extracts the hook-event state machine out of AppDelegate.hook-event: the handler now flattens payload → AgentHookEvent, asks the agent's signal source for an AgentStateTransition, and applies it. 110-line switch becomes ~20 lines of apply-transition code.
  • Renames for agent-agnostic naming: ClaudeStateAgentActivityState, TerminalReadiness.claudeLaunched.agentLaunched, AppState.onLaunchClaudeonLaunchAgent, SessionHookState.claudeStateactivityState.
  • Zero user-visible change; no Session model change in this phase.

Phase B — per-session agent selection plumbing — closes #167

  • Session.agentKind: AgentKind (persisted, defaults to .claudeCode, backward-compatible decoder).
  • AppConfig.defaultAgentKind: AgentKind (top-level field, backward-compatible decoder, mirrored to AppState).
  • CodingAgent gains displayName and iconSystemName so UI can enumerate registered agents without baking names into CrowCore.
  • CreateSessionView: "Agent" picker seeded from AppState.defaultAgentKind, disabled when only one agent is registered.
  • SettingsView: "Default Agent" picker in the Defaults section, same single-option-disabled treatment.
  • Sidebar SessionRow: leading agent icon with tooltip.
  • SessionDetailView: read-only "Agent: <displayName>" row (hidden for Manager, which stays pinned to Claude Code).
  • crow new-session CLI grows --agent <kind>; RPC accepts agent_kind.
  • Manager session hardcodes .claudeCode at construction — never reads the config default.

Test plan

  • swift test at repo root passes (20 tests).
  • swift test in every sub-package passes — 301 tests total (CrowCore 114, CrowClaude 23, CrowCLI 34, CrowGit 13, CrowIPC 32, CrowPersistence 23, CrowProvider 25, CrowUI 17, Crow/Tests 20).
  • Phase A: 15 ClaudeHookSignalSourceTests cover every branch of the old AppDelegate switch (PreToolUse/AskUserQuestion, PreToolUse/other, PostToolUse, permission_prompt, idle_prompt, PermissionRequest-vs-question precedence, Stop, SessionStart/resume, SessionStart/fresh, SessionEnd, Task/Subagent preserving .waiting, blanket notification clear, unknown/PreCompact).
  • Phase B: 7 new Codable tests cover backward-compat (legacy JSON without agentKind / defaultAgentKind decodes to .claudeCode), round-trip of custom raw values, and on-disk shape stability.
  • Manual A: launch a session, verify sidebar dot progresses gray→yellow→blue→green and Claude badge flips Working/Question/Permission/Done at the same moments as before.
  • Manual A: verify <worktree>/.claude/settings.local.json is still written with all 17 hook entries; deleting a session removes them.
  • Manual B: existing sessions.json / config.json load cleanly — every session shows the Claude sparkles icon in sidebar, "Agent: Claude Code" label in detail header.
  • Manual B: New-session dialog shows picker (disabled), Settings shows "Default Agent" picker (disabled).
  • Manual B: crow new-session --name test --agent claude-code succeeds and the returned JSON includes agent_kind.
  • Manual B (Phase C rehearsal): temporarily register a stub agent, relaunch — both pickers become enabled, creating a session with the stub persists its kind, sidebar/header reflect the choice.

🤖 Generated with Claude Code

Lays down the behavior-preserving agent abstraction from Phase A: new
CodingAgent/AgentKind/AgentRegistry/StateSignalSource/AgentStateTransition/
HookConfigWriter protocols in CrowCore, with ClaudeCodeAgent,
ClaudeHookConfigWriter, and ClaudeHookSignalSource implementing them in
CrowClaude. The hook-event RPC handler now applies AgentStateTransition
values from the signal source instead of inlining the state machine, and
HookConfigGenerator moved out of the main app. ClaudeState →
AgentActivityState, .claudeLaunched → .agentLaunched, onLaunchClaude →
onLaunchAgent. No user-visible change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dhilgaertner dhilgaertner requested a review from dgershman as a code owner April 22, 2026 22:07
Phase B of the agent abstraction. Session and AppConfig now carry an
AgentKind — persisted, backward-compatible with existing JSON. The new
CodingAgent.displayName / iconSystemName drive a picker in the new-session
dialog and a "Default Agent" picker in Settings; both are visible but
disabled until a second agent is registered (Phase C flips the bit by
calling AgentRegistry.shared.register). The sidebar row gets a leading
agent icon and the detail header gains a read-only "Agent: <name>" label
(hidden for Manager, which stays pinned to Claude Code). The new-session
RPC and crow CLI accept an --agent / agent_kind param.

Codable tests cover the legacy-JSON-decodes-to-.claudeCode path for both
Session and AppConfig so PRs preserving these models stay covered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dhilgaertner dhilgaertner changed the title Phase A: introduce CodingAgent protocol (behavior-preserving) Phases A + B: CodingAgent protocol and per-session agent selection Apr 22, 2026
Copy link
Copy Markdown
Collaborator

@dgershman dgershman left a comment

Choose a reason for hiding this comment

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

Code & Security Review

Critical Issues

None found.

Security Review

Strengths:

  • AgentHookEvent strips raw JSON payloads at the CrowCore boundary, preventing JSON-value type leakage into the protocol layer.
  • AgentKind is a RawRepresentable struct (not an enum), so downstream packages can register agents without modifying CrowCore — good extensibility without security surface expansion.
  • Backward-compatible Codable decoders (decodeIfPresent with .claudeCode fallback) prevent crash-on-load from legacy data files.
  • Existing input validation on session names, path-traversal guards on worktree/terminal paths, and UUID validation are all preserved through the refactor.
  • AgentRegistry uses NSLock for thread safety — appropriate for a low-contention singleton.

Concerns:

  • None. The ClaudeHookConfigWriter still writes to .claude/settings.local.json within validated worktree paths. Hook commands are constructed from trusted crowPath + session UUID — no user-controlled injection vector.

Code Quality

Well done:

  • The state machine extraction from AppDelegate into ClaudeHookSignalSource is clean. The 110-line inline switch is now a pure function returning AgentStateTransition, making it testable in isolation. 15 tests cover every branch.
  • AgentStateTransition uses a three-variant enum (leave/clear/set) for both notification and tool activity updates — expressive and prevents accidental state clobbering.
  • Manager session is correctly hardcoded to .claudeCode at construction time (SessionService.swift:248), never reading the config default.
  • UI pickers are disabled when only one agent is registered (availableAgents.count < 2), preventing dead UI elements.
  • The hook-event handler comment at AppDelegate.swift:887 notes the Phase A limitation (always routes through default agent) — good breadcrumb for Phase C.

Minor observations (non-blocking):

  • AppDelegate.swift:887: The hook-event handler always uses AgentRegistry.shared.defaultAgent?.stateSignalSource rather than looking up the session's agentKind. The comment explains this is Phase A behavior, but when Phase C lands, this line will need to resolve per-session. Consider a // TODO(phase-c) marker.
  • SessionService.swift:625 and SessionService.swift:833: recoverOrphan and createReviewSession both set agentKind: appState.defaultAgentKind. This is correct behavior today but worth noting — recovered orphans will inherit the current default, not whatever agent they were originally created with (that info is lost).
  • saveSettings at AppDelegate.swift:434 persists the new config but doesn't mirror defaultAgentKind back to appState.defaultAgentKind the way it does for remoteControlEnabled and hideSessionDetails. The Settings picker writes directly to config.defaultAgentKind, but appState.defaultAgentKind will be stale until restart. Since the SettingsView onChange calls save() which stores the config, the persisted value is correct — but any code reading appState.defaultAgentKind (like the new-session RPC handler) will see the old value until app restart.

Summary Table

Priority Issue
🟡 Yellow saveSettings doesn't sync defaultAgentKind back to appState (stale until restart)
🟢 Green Add // TODO(phase-c) to hook-event handler's default-agent lookup
🟢 Green Orphan recovery inherits current default agent, not original — acceptable but worth documenting

Recommendation: Approve. This is a well-structured, behavior-preserving refactor with thorough test coverage (15 signal-source tests + 7 Codable tests). The one yellow item (stale appState.defaultAgentKind after settings change) is low-impact since only one agent exists today, but should be fixed before Phase C when users can actually pick a different default.

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.

Agent abstraction Phase B: per-session selection plumbing Agent abstraction Phase A: introduce CodingAgent protocol (behavior-preserving)

2 participants