Skip to content

Phases A + B + C: agent abstraction + OpenAI Codex MVP#197

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

Phases A + B + C: agent abstraction + OpenAI Codex MVP#197
dhilgaertner wants to merge 3 commits intomainfrom
feature/crow-166-agent-abstraction-phase-a

Conversation

@dhilgaertner
Copy link
Copy Markdown
Contributor

@dhilgaertner dhilgaertner commented Apr 22, 2026

Closes #166, #167, and #168. Parent spec: #150. Lands the full agent-abstraction stack — protocols, per-session selection, and a working second agent (OpenAI Codex).

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

  • New protocols in CrowCore: CodingAgent, AgentKind, AgentRegistry, StateSignalSource, AgentStateTransition, HookConfigWriter, plus an AgentHookEvent payload type so CrowCore stays free of JSONValue.
  • ClaudeCodeAgent, ClaudeHookConfigWriter (moved from Sources/Crow/App/HookConfigGenerator.swift), and ClaudeHookSignalSource in Packages/CrowClaude/. Registered at app launch.
  • AppDelegate.hook-event 110-line state-machine switch becomes ~20 lines of apply-transition code. ClaudeStateAgentActivityState, TerminalReadiness.claudeLaunched.agentLaunched, onLaunchClaudeonLaunchAgent, SessionHookState.claudeStateactivityState.

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

  • Session.agentKind: AgentKind (persisted, backward-compat decoder).
  • AppConfig.defaultAgentKind: AgentKind (top-level, backward-compat decoder, mirrored to AppState).
  • CodingAgent gains displayName and iconSystemName so UI enumerates registered agents without baking names into CrowCore.
  • Picker in CreateSessionView and Settings → Defaults; disabled when only one agent registers; auto-enables when a second registers.
  • Sidebar leading agent icon, read-only "Agent: …" row in detail header (hidden for Manager).
  • crow new-session grows --agent <kind>; RPC accepts agent_kind.
  • Manager session hardcoded to .claudeCode.

Phase C — OpenAI Codex MVP — closes #168

Empirically verified against codex 0.123.0 (brew install --cask codex). Codex uses Claude's hook engine internally — payload schemas are byte-compatible.

Protocol extensions:

  • CodingAgent gains supportsRemoteControl, launchCommandToken, findBinary(), autoLaunchCommand(...). ClaudeCodeAgent absorbs the OTEL prefix / --continue / review-prompt / --rc logic that previously lived inline in SessionService.launchClaude.

New CrowCodex package (mirrors CrowClaude/):

  • OpenAICodexAgentsupportsRemoteControl = false, launches bare codex.
  • CodexHookConfigWriter — installs a single global ~/.codex/hooks.json at app launch (Codex hooks are global, not per-worktree). The HookConfigWriter per-session methods are no-ops on the Codex side.
  • CodexSignalSource — handles 6 events (SessionStart, PreToolUse, PostToolUse, UserPromptSubmit, Stop, PermissionRequest) with lastTopLevelStopAt bookkeeping.
  • CodexScaffolder — writes AGENTS.md to the dev root (Codex's CLAUDE.md analog), preserving the user's ## Known Issues / Corrections section.
  • CodexLauncher, CodexNotifyPayload for prompt generation and notify-payload translation.

Hook routing:

  • AppDelegate.hook-event resolves agent via agent_kind param > session.agentKind > app default. When session_id is absent (Codex global hooks have no per-session UUID), the server resolves the session via AppState.sessionID(forWorktreePath:) matching the payload's cwd.
  • SessionService.launchClaudelaunchAgent dispatches via agent.autoLaunchCommand. RemoteControlBadge renders only when the session's agent reports supportsRemoteControl == true.
  • crow hook-event grows --agent; --session is now optional. New crow codex-notify subcommand bridges Codex's notify config key into the hook-event pipeline.

Codex registration is gated on binary presence. Users without codex installed see no change from Phase B — picker stays disabled, no ~/.codex/ files created.

Test plan

  • swift test at repo root passes (31 tests).
  • swift test in every sub-package passes — 355 tests total (CrowCore 114, CrowClaude 33, CrowCodex 27, CrowCLI 34, CrowGit 13, CrowIPC 32, CrowPersistence 24, CrowProvider 25, CrowUI 22, root 31).
  • Phase A: 15 ClaudeHookSignalSourceTests cover every branch of the old AppDelegate switch.
  • Phase B: 7 Codable tests cover the legacy-JSON-decodes-to-.claudeCode backward-compat path for both Session and AppConfig.
  • Phase C: 27 CrowCodex tests across 5 suites + 2 AppState.sessionID(forWorktreePath:) tests; relocated 8 ClaudeLaunchArgsTests.
  • Manual: launch the app — without codex installed, picker stays disabled; with codex installed, picker enables and shows both agents. ~/.codex/hooks.json and ~/.codex/config.toml get written at first launch.
  • Manual: create a Codex session — terminal launches codex; sidebar icon is terminal.fill; detail header shows "Agent: OpenAI Codex"; no RemoteControlBadge.
  • Manual: submit a prompt to Codex — when it finishes a turn, crow codex-notify fires, sidebar transitions .idle → .working → .done. With codex_hooks enabled, PreToolUse flips state mid-turn.
  • Manual: parallel Claude session continues to behave identically — .claude/settings.local.json per-worktree, --continue, OTEL env, all unchanged.
  • Manual: Manager tab still launches claude, hides agent label and remote-control badge appropriately.

🤖 Generated with Claude Code

@dhilgaertner dhilgaertner requested a review from dgershman as a code owner April 22, 2026 22:07
@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.

@dhilgaertner dhilgaertner force-pushed the feature/crow-166-agent-abstraction-phase-a branch 3 times, most recently from 2ab0aab to 899a3ae Compare April 26, 2026 06:51
@dhilgaertner dhilgaertner changed the title Phases A + B: CodingAgent protocol and per-session agent selection Phases A + B + C: agent abstraction + OpenAI Codex MVP Apr 28, 2026
@dhilgaertner dhilgaertner force-pushed the feature/crow-166-agent-abstraction-phase-a branch from 4cece2c to 2fa5736 Compare April 28, 2026 20:45
@dhilgaertner dhilgaertner force-pushed the feature/crow-166-agent-abstraction-phase-a branch from 2fa5736 to 6bfbb8c Compare May 5, 2026 20:50
Dustin Hilgaertner and others added 3 commits May 5, 2026 16:07
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>
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>
Phase C of the agent-abstraction work: register OpenAICodexAgent in a new
CrowCodex package and make sessions actually run on Codex when its binary
is installed.

Key extension points:
- CodingAgent gains supportsRemoteControl, launchCommandToken,
  findBinary(), autoLaunchCommand(...). ClaudeCodeAgent absorbs the OTEL
  prefix + --continue / review-prompt / --rc logic that previously lived
  inline in SessionService.launchClaude.
- New CrowCodex package mirrors CrowClaude: OpenAICodexAgent,
  CodexLauncher, CodexHookConfigWriter, CodexSignalSource, CodexScaffolder,
  CodexNotifyPayload. CodexHookConfigWriter installs a single global
  ~/.codex/hooks.json and updates ~/.codex/config.toml at app launch
  (Codex's hooks are global, not per-worktree like Claude's). All 6 Codex
  hook events are mapped in CodexSignalSource with lastTopLevelStopAt
  bookkeeping.
- Hook-event RPC handler resolves agent via agent_kind > session.agentKind
  > default, and resolves session by payload cwd when session_id is absent
  (Codex global hooks don't carry the Crow UUID). New
  AppState.sessionID(forWorktreePath:) helper.
- SessionService.launchClaude → launchAgent dispatches via the registered
  agent's autoLaunchCommand. RemoteControlBadge renders only when the
  session's agent supportsRemoteControl. Manager stays pinned to Claude.
- CLI: hook-event grows --agent and makes --session optional. New
  codex-notify subcommand bridges Codex's notify command into the
  hook-event pipeline by translating the JSON payload via
  CodexNotifyPayload.translate.
- Schemas verified empirically against codex 0.123.0 — Codex uses Claude
  Code's hook engine internally, so payload keys are identical (session_id,
  tool_name, cwd, source, hook_event_name).

29 new tests across CrowCodex (5 suites), AppState helper, and the
relocated ClaudeLaunchArgsTests. Total: 355 tests passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dhilgaertner dhilgaertner force-pushed the feature/crow-166-agent-abstraction-phase-a branch from 6bfbb8c to 15cf6bd Compare May 5, 2026 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants