Phases A + B + C: agent abstraction + OpenAI Codex MVP#197
Open
dhilgaertner wants to merge 3 commits intomainfrom
Open
Phases A + B + C: agent abstraction + OpenAI Codex MVP#197dhilgaertner wants to merge 3 commits intomainfrom
dhilgaertner wants to merge 3 commits intomainfrom
Conversation
dgershman
approved these changes
Apr 22, 2026
Collaborator
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
None found.
Security Review
Strengths:
AgentHookEventstrips raw JSON payloads at theCrowCoreboundary, preventing JSON-value type leakage into the protocol layer.AgentKindis aRawRepresentablestruct (not an enum), so downstream packages can register agents without modifyingCrowCore— good extensibility without security surface expansion.- Backward-compatible
Codabledecoders (decodeIfPresentwith.claudeCodefallback) 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.
AgentRegistryusesNSLockfor thread safety — appropriate for a low-contention singleton.
Concerns:
- None. The
ClaudeHookConfigWriterstill writes to.claude/settings.local.jsonwithin validated worktree paths. Hook commands are constructed from trustedcrowPath+ session UUID — no user-controlled injection vector.
Code Quality
Well done:
- The state machine extraction from AppDelegate into
ClaudeHookSignalSourceis clean. The 110-line inline switch is now a pure function returningAgentStateTransition, making it testable in isolation. 15 tests cover every branch. AgentStateTransitionuses 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
.claudeCodeat 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-eventhandler comment atAppDelegate.swift:887notes 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 usesAgentRegistry.shared.defaultAgent?.stateSignalSourcerather than looking up the session'sagentKind. 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:625andSessionService.swift:833:recoverOrphanandcreateReviewSessionboth setagentKind: 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).saveSettingsatAppDelegate.swift:434persists the new config but doesn't mirrordefaultAgentKindback toappState.defaultAgentKindthe way it does forremoteControlEnabledandhideSessionDetails. The Settings picker writes directly toconfig.defaultAgentKind, butappState.defaultAgentKindwill be stale until restart. Since theSettingsViewonChange callssave()which stores the config, the persisted value is correct — but any code readingappState.defaultAgentKind(like thenew-sessionRPC 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.
2ab0aab to
899a3ae
Compare
4cece2c to
2fa5736
Compare
2fa5736 to
6bfbb8c
Compare
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>
6bfbb8c to
15cf6bd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
CrowCore:CodingAgent,AgentKind,AgentRegistry,StateSignalSource,AgentStateTransition,HookConfigWriter, plus anAgentHookEventpayload type soCrowCorestays free ofJSONValue.ClaudeCodeAgent,ClaudeHookConfigWriter(moved fromSources/Crow/App/HookConfigGenerator.swift), andClaudeHookSignalSourceinPackages/CrowClaude/. Registered at app launch.AppDelegate.hook-event110-line state-machine switch becomes ~20 lines of apply-transition code.ClaudeState→AgentActivityState,TerminalReadiness.claudeLaunched→.agentLaunched,onLaunchClaude→onLaunchAgent,SessionHookState.claudeState→activityState.Phase B — per-session selection plumbing — closes #167
Session.agentKind: AgentKind(persisted, backward-compat decoder).AppConfig.defaultAgentKind: AgentKind(top-level, backward-compat decoder, mirrored toAppState).CodingAgentgainsdisplayNameandiconSystemNameso UI enumerates registered agents without baking names intoCrowCore.CreateSessionViewand Settings → Defaults; disabled when only one agent registers; auto-enables when a second registers.crow new-sessiongrows--agent <kind>; RPC acceptsagent_kind..claudeCode.Phase C — OpenAI Codex MVP — closes #168
Empirically verified against
codex0.123.0 (brew install --cask codex). Codex uses Claude's hook engine internally — payload schemas are byte-compatible.Protocol extensions:
CodingAgentgainssupportsRemoteControl,launchCommandToken,findBinary(),autoLaunchCommand(...).ClaudeCodeAgentabsorbs the OTEL prefix /--continue/ review-prompt /--rclogic that previously lived inline inSessionService.launchClaude.New
CrowCodexpackage (mirrorsCrowClaude/):OpenAICodexAgent—supportsRemoteControl = false, launches barecodex.CodexHookConfigWriter— installs a single global~/.codex/hooks.jsonat app launch (Codex hooks are global, not per-worktree). TheHookConfigWriterper-session methods are no-ops on the Codex side.CodexSignalSource— handles 6 events (SessionStart,PreToolUse,PostToolUse,UserPromptSubmit,Stop,PermissionRequest) withlastTopLevelStopAtbookkeeping.CodexScaffolder— writesAGENTS.mdto the dev root (Codex'sCLAUDE.mdanalog), preserving the user's## Known Issues / Correctionssection.CodexLauncher,CodexNotifyPayloadfor prompt generation and notify-payload translation.Hook routing:
AppDelegate.hook-eventresolves agent viaagent_kindparam >session.agentKind> app default. Whensession_idis absent (Codex global hooks have no per-session UUID), the server resolves the session viaAppState.sessionID(forWorktreePath:)matching the payload'scwd.SessionService.launchClaude→launchAgentdispatches viaagent.autoLaunchCommand.RemoteControlBadgerenders only when the session's agent reportssupportsRemoteControl == true.crow hook-eventgrows--agent;--sessionis now optional. Newcrow codex-notifysubcommand bridges Codex'snotifyconfig key into the hook-event pipeline.Codex registration is gated on binary presence. Users without
codexinstalled see no change from Phase B — picker stays disabled, no~/.codex/files created.Test plan
swift testat repo root passes (31 tests).swift testin 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).ClaudeHookSignalSourceTestscover every branch of the old AppDelegate switch..claudeCodebackward-compat path for bothSessionandAppConfig.CrowCodextests across 5 suites + 2AppState.sessionID(forWorktreePath:)tests; relocated 8ClaudeLaunchArgsTests.codexinstalled, picker stays disabled; withcodexinstalled, picker enables and shows both agents.~/.codex/hooks.jsonand~/.codex/config.tomlget written at first launch.codex; sidebar icon isterminal.fill; detail header shows "Agent: OpenAI Codex"; noRemoteControlBadge.crow codex-notifyfires, sidebar transitions.idle → .working → .done. Withcodex_hooksenabled,PreToolUseflips state mid-turn..claude/settings.local.jsonper-worktree,--continue, OTEL env, all unchanged.claude, hides agent label and remote-control badge appropriately.🤖 Generated with Claude Code