| ... | ... | @@ -1,549 +0,0 @@ |
| 1 | | # Plan: Claude Code Integration (Orchestration Architecture) |
| 2 | | |
| 3 | | ## Why this plan was rewritten |
| 4 | | |
| 5 | | The previous plan targeted a pre-orchestration architecture (`ProviderManager`, provider-native WS event methods, and direct provider UI wiring). The current app now routes everything through: |
| 6 | | |
| 7 | | 1. `orchestration.dispatchCommand` (client intent) |
| 8 | | 2. `OrchestrationEngine` (decide + persist + publish domain events) |
| 9 | | 3. `ProviderCommandReactor` (domain intent -> `ProviderService`) |
| 10 | | 4. `ProviderService` (adapter routing + canonical runtime stream) |
| 11 | | 5. `ProviderRuntimeIngestion` (provider runtime -> internal orchestration commands) |
| 12 | | 6. `orchestration.domainEvent` (single push channel consumed by web) |
| 13 | | |
| 14 | | Claude integration must plug into this path instead of reintroducing legacy provider-specific flows. |
| 15 | | |
| 16 | | --- |
| 17 | | |
| 18 | | ## Current constraints to design around (post-Stage 1) |
| 19 | | |
| 20 | | 1. Provider runtime ingestion expects canonical `ProviderRuntimeEvent` shapes, not provider-native payloads. |
| 21 | | 2. Start input now uses typed `providerOptions` and generic `resumeCursor`; top-level provider-specific fields were removed. |
| 22 | | 3. `resumeCursor` is intentionally opaque outside adapters and must never be synthesized from `providerThreadId`. |
| 23 | | 4. `ProviderService` still requires adapter `startSession()` to return a `ProviderSession` with `threadId`. |
| 24 | | 5. Checkpoint revert currently calls `providerService.rollbackConversation()`, so Claude adapter needs a rollback strategy compatible with current reactor behavior. |
| 25 | | 6. Web currently marks Claude as unavailable (`"Claude Code (soon)"`) and model picker is Codex-only. |
| 26 | | |
| 27 | | --- |
| 28 | | |
| 29 | | ## Architecture target |
| 30 | | |
| 31 | | Add Claude as a first-class provider adapter that emits canonical runtime events and works with existing orchestration reactors without adding new WS channels or bypass paths. |
| 32 | | |
| 33 | | Key decisions: |
| 34 | | |
| 35 | | 1. Keep orchestration provider-agnostic; adapt Claude inside adapter/layer boundaries. |
| 36 | | 2. Use the existing canonical runtime stream (`ProviderRuntimeEvent`) as the only ingestion contract. |
| 37 | | 3. Keep provider session routing in `ProviderService` and `ProviderSessionDirectory`. |
| 38 | | 4. Add explicit provider selection to turn-start intent so first turn can start Claude session intentionally. |
| 39 | | |
| 40 | | --- |
| 41 | | |
| 42 | | ## Phase 1: Contracts and command shape updates |
| 43 | | |
| 44 | | ### 1.1 Provider-aware model contract |
| 45 | | |
| 46 | | Update `packages/contracts/src/model.ts` so model resolution can be provider-aware instead of Codex-only. |
| 47 | | |
| 48 | | Expected outcomes: |
| 49 | | |
| 50 | | 1. Introduce provider-scoped model lists (Codex + Claude). |
| 51 | | 2. Add helpers that resolve model by provider. |
| 52 | | 3. Preserve backwards compatibility for existing Codex defaults. |
| 53 | | |
| 54 | | ### 1.2 Turn-start provider intent |
| 55 | | |
| 56 | | Update `packages/contracts/src/orchestration.ts`: |
| 57 | | |
| 58 | | 1. Add optional `provider: ProviderKind` to `ThreadTurnStartCommand`. |
| 59 | | 2. Carry provider through `ThreadTurnStartRequestedPayload`. |
| 60 | | 3. Keep existing command valid when provider is omitted. |
| 61 | | |
| 62 | | This removes the implicit “Codex unless session already exists” behavior as the only path. |
| 63 | | |
| 64 | | ### 1.3 Provider session start input for Claude runtime knobs (completed) |
| 65 | | |
| 66 | | Update `packages/contracts/src/provider.ts`: |
| 67 | | |
| 68 | | 1. Move provider-specific start fields into typed `providerOptions`: |
| 69 | | - `providerOptions.codex` |
| 70 | | - `providerOptions.claudeCode` |
| 71 | | 2. Keep `resumeCursor` as the single cross-provider resume input in `ProviderSessionStartInput`. |
| 72 | | 3. Deprecate/remove `resumeThreadId` from the generic start contract. |
| 73 | | 4. Treat `resumeCursor` as adapter-owned opaque state. |
| 74 | | |
| 75 | | ### 1.4 Contract tests (completed) |
| 76 | | |
| 77 | | Update/add tests in `packages/contracts/src/*.test.ts` for: |
| 78 | | |
| 79 | | 1. New command payload shape. |
| 80 | | 2. Provider-aware model resolution behavior. |
| 81 | | 3. Breaking-change expectations for removed top-level provider fields. |
| 82 | | |
| 83 | | --- |
| 84 | | |
| 85 | | ## Phase 2: Claude adapter implementation |
| 86 | | |
| 87 | | ### 2.1 Add adapter service + layer |
| 88 | | |
| 89 | | Create: |
| 90 | | |
| 91 | | 1. `apps/server/src/provider/Services/ClaudeCodeAdapter.ts` |
| 92 | | 2. `apps/server/src/provider/Layers/ClaudeCodeAdapter.ts` |
| 93 | | |
| 94 | | Adapter must implement `ProviderAdapterShape<ProviderAdapterError>`. |
| 95 | | |
| 96 | | ### 2.1.a SDK dependency and baseline config |
| 97 | | |
| 98 | | Add server dependency: |
| 99 | | |
| 100 | | 1. `@anthropic-ai/claude-agent-sdk` |
| 101 | | |
| 102 | | Baseline adapter options to support from day one: |
| 103 | | |
| 104 | | 1. `cwd` |
| 105 | | 2. `model` |
| 106 | | 3. `pathToClaudeCodeExecutable` (from `providerOptions.claudeCode.binaryPath`) |
| 107 | | 4. `permissionMode` (from `providerOptions.claudeCode.permissionMode`) |
| 108 | | 5. `maxThinkingTokens` (from `providerOptions.claudeCode.maxThinkingTokens`) |
| 109 | | 6. `resume` |
| 110 | | 7. `resumeSessionAt` |
| 111 | | 8. `includePartialMessages` |
| 112 | | 9. `canUseTool` |
| 113 | | 10. `hooks` |
| 114 | | 11. `env` and `additionalDirectories` (if needed for sandbox/workspace parity) |
| 115 | | |
| 116 | | ### 2.2 Claude runtime bridge |
| 117 | | |
| 118 | | Implement a Claude runtime bridge (either directly in adapter layer or via dedicated manager file) that wraps Agent SDK query lifecycle. |
| 119 | | |
| 120 | | Required capabilities: |
| 121 | | |
| 122 | | 1. Long-lived session context per adapter session. |
| 123 | | 2. Multi-turn input queue. |
| 124 | | 3. Interrupt support. |
| 125 | | 4. Approval request/response bridge. |
| 126 | | 5. Resume support via opaque `resumeCursor` (parsed inside Claude adapter only). |
| 127 | | |
| 128 | | #### 2.2.a Agent SDK details to preserve |
| 129 | | |
| 130 | | The adapter should explicitly rely on these SDK capabilities: |
| 131 | | |
| 132 | | 1. `query()` returns an async iterable message stream and control methods (`interrupt`, `setModel`, `setPermissionMode`, `setMaxThinkingTokens`, account/status helpers). |
| 133 | | 2. Multi-turn input is supported via async-iterable prompt input. |
| 134 | | 3. Tool approval decisions are provided via `canUseTool`. |
| 135 | | 4. Resume support uses `resume` and optional `resumeSessionAt`, both derived by parsing adapter-owned `resumeCursor`. |
| 136 | | 5. Hooks can be used for lifecycle signals (`Stop`, `PostToolUse`, etc.) when we need adapter-originated checkpoint/runtime events. |
| 137 | | |
| 138 | | #### 2.2.b Effect-native session lifecycle skeleton |
| 139 | | |
| 140 | | ```ts |
| 141 | | import { query } from "@anthropic-ai/claude-agent-sdk"; |
| 142 | | import { Effect } from "effect"; |
| 143 | | |
| 144 | | const acquireSession = (input: ProviderSessionStartInput) => |
| 145 | | Effect.acquireRelease( |
| 146 | | Effect.tryPromise({ |
| 147 | | try: async () => { |
| 148 | | const claudeOptions = input.providerOptions?.claudeCode; |
| 149 | | const resumeState = readClaudeResumeState(input.resumeCursor); |
| 150 | | const abortController = new AbortController(); |
| 151 | | const result = query({ |
| 152 | | prompt: makePromptAsyncIterable(), |
| 153 | | options: { |
| 154 | | cwd: input.cwd, |
| 155 | | model: input.model, |
| 156 | | permissionMode: claudeOptions?.permissionMode, |
| 157 | | maxThinkingTokens: claudeOptions?.maxThinkingTokens, |
| 158 | | pathToClaudeCodeExecutable: claudeOptions?.binaryPath, |
| 159 | | resume: resumeState?.threadId, |
| 160 | | resumeSessionAt: resumeState?.sessionAt, |
| 161 | | signal: abortController.signal, |
| 162 | | includePartialMessages: true, |
| 163 | | canUseTool: makeCanUseTool(), |
| 164 | | hooks: makeClaudeHooks(), |
| 165 | | }, |
| 166 | | }); |
| 167 | | return { abortController, result }; |
| 168 | | }, |
| 169 | | catch: (cause) => |
| 170 | | new ProviderAdapterProcessError({ |
| 171 | | provider: "claudeCode", |
| 172 | | sessionId: "pending", |
| 173 | | detail: "Failed to start Claude runtime session.", |
| 174 | | cause, |
| 175 | | }), |
| 176 | | }), |
| 177 | | ({ abortController }) => Effect.sync(() => abortController.abort()), |
| 178 | | ); |
| 179 | | ``` |
| 180 | | |
| 181 | | #### 2.2.c AsyncIterable -> Effect Stream integration |
| 182 | | |
| 183 | | Preferred when available in the pinned Effect version: |
| 184 | | |
| 185 | | ```ts |
| 186 | | const sdkMessageStream = Stream.fromAsyncIterable( |
| 187 | | session.result, |
| 188 | | (cause) => |
| 189 | | new ProviderAdapterProcessError({ |
| 190 | | provider: "claudeCode", |
| 191 | | sessionId, |
| 192 | | detail: "Claude runtime stream failed.", |
| 193 | | cause, |
| 194 | | }), |
| 195 | | ); |
| 196 | | ``` |
| 197 | | |
| 198 | | Portable fallback (already aligned with current server patterns): |
| 199 | | |
| 200 | | ```ts |
| 201 | | const queue = yield* Queue.unbounded<ClaudeSdkMessage>(); |
| 202 | | yield* Effect.forkScoped( |
| 203 | | Effect.tryPromise({ |
| 204 | | try: async () => { |
| 205 | | for await (const message of session.result) { |
| 206 | | Queue.offerAllUnsafe(queue, [message]); |
| 207 | | } |
| 208 | | }, |
| 209 | | catch: (cause) => |
| 210 | | new ProviderAdapterProcessError({ |
| 211 | | provider: "claudeCode", |
| 212 | | sessionId, |
| 213 | | detail: "Claude runtime stream pump failed.", |
| 214 | | cause, |
| 215 | | }), |
| 216 | | }), |
| 217 | | ); |
| 218 | | const sdkMessageStream = Stream.fromQueue(queue); |
| 219 | | ``` |
| 220 | | |
| 221 | | #### 2.2.d Multi-turn prompt queue pattern (Effect) |
| 222 | | |
| 223 | | Use an Effect queue as the single input boundary: |
| 224 | | |
| 225 | | ```ts |
| 226 | | const promptQueue = yield* Queue.unbounded<ClaudePromptEnvelope>(); |
| 227 | | |
| 228 | | const prompt: AsyncIterable<ClaudePromptEnvelope> = { |
| 229 | | [Symbol.asyncIterator]() { |
| 230 | | return { |
| 231 | | next: async () => { |
| 232 | | const item = await Effect.runPromise(Queue.take(promptQueue)); |
| 233 | | if (item.type === "terminate") { |
| 234 | | return { done: true, value: undefined }; |
| 235 | | } |
| 236 | | return { done: false, value: item }; |
| 237 | | }, |
| 238 | | }; |
| 239 | | }, |
| 240 | | }; |
| 241 | | ``` |
| 242 | | |
| 243 | | `sendTurn()` enqueues a user envelope, while `stopSession()` enqueues terminate and aborts. |
| 244 | | |
| 245 | | #### 2.2.e Approval bridge with `canUseTool` |
| 246 | | |
| 247 | | Map SDK approval checks to existing orchestration approval flows: |
| 248 | | |
| 249 | | ```ts |
| 250 | | const canUseTool = async (toolName: string, toolInput: Record<string, unknown>) => { |
| 251 | | if (approvalModeIsFullAccess(sessionId)) { |
| 252 | | return { behavior: "allow", updatedInput: toolInput }; |
| 253 | | } |
| 254 | | |
| 255 | | const requestId = ApprovalRequestId.makeUnsafe(crypto.randomUUID()); |
| 256 | | emitRuntimeEvent({ |
| 257 | | type: "approval.requested", |
| 258 | | provider: "claudeCode", |
| 259 | | sessionId, |
| 260 | | requestId, |
| 261 | | requestKind: classifyTool(toolName), |
| 262 | | detail: summarizeToolRequest(toolName, toolInput), |
| 263 | | // ... eventId/createdAt |
| 264 | | }); |
| 265 | | |
| 266 | | const decision = await waitForApprovalDecision(requestId); |
| 267 | | emitRuntimeEvent({ |
| 268 | | type: "approval.resolved", |
| 269 | | provider: "claudeCode", |
| 270 | | sessionId, |
| 271 | | requestId, |
| 272 | | decision, |
| 273 | | // ... eventId/createdAt |
| 274 | | }); |
| 275 | | |
| 276 | | return decision === "accept" || decision === "acceptForSession" |
| 277 | | ? { behavior: "allow", updatedInput: toolInput } |
| 278 | | : { behavior: "deny", message: "User declined tool execution." }; |
| 279 | | }; |
| 280 | | ``` |
| 281 | | |
| 282 | | #### 2.2.f Hooks and checkpoint signals |
| 283 | | |
| 284 | | If Claude hooks provide cleaner turn boundaries, convert them into canonical runtime events (`turn.completed` and optionally `checkpoint.captured`) so `CheckpointReactor` remains unchanged: |
| 285 | | |
| 286 | | ```ts |
| 287 | | hooks: { |
| 288 | | Stop: [ |
| 289 | | { |
| 290 | | matcher: {}, |
| 291 | | hooks: [ |
| 292 | | async () => { |
| 293 | | emitRuntimeEvent({ |
| 294 | | type: "checkpoint.captured", |
| 295 | | provider: "claudeCode", |
| 296 | | sessionId, |
| 297 | | threadId, |
| 298 | | turnId: activeTurnId, |
| 299 | | turnCount: nextTurnCount(), |
| 300 | | // ... eventId/createdAt |
| 301 | | }); |
| 302 | | }, |
| 303 | | ], |
| 304 | | }, |
| 305 | | ], |
| 306 | | } |
| 307 | | ``` |
| 308 | | |
| 309 | | #### 2.2.g Runtime control method wiring |
| 310 | | |
| 311 | | Adapter should expose SDK controls through existing service methods and runtime payload updates: |
| 312 | | |
| 313 | | 1. `interruptTurn()` -> `result.interrupt()` |
| 314 | | 2. Model override on send turn -> `result.setModel(model)` before enqueuing prompt |
| 315 | | 3. Runtime mode changes -> `result.setPermissionMode(mode)` if we support live mode switching |
| 316 | | 4. Thinking budget updates -> `result.setMaxThinkingTokens(tokens)` when configured |
| 317 | | |
| 318 | | ### 2.3 Canonical event mapping |
| 319 | | |
| 320 | | Map Claude SDK events to `ProviderRuntimeEvent`: |
| 321 | | |
| 322 | | 1. `session.started` / `session.exited` |
| 323 | | 2. `turn.started` / `turn.completed` |
| 324 | | 3. `message.delta` / `message.completed` |
| 325 | | 4. `tool.started` / `tool.completed` |
| 326 | | 5. `approval.requested` / `approval.resolved` |
| 327 | | 6. `runtime.error` |
| 328 | | |
| 329 | | No provider-native event methods should leak beyond the adapter boundary. |
| 330 | | |
| 331 | | Reference mapping table: |
| 332 | | |
| 333 | | | Claude SDK message/callback | Canonical runtime event(s) | Notes | |
| 334 | | |---|---|---| |
| 335 | | | assistant partial text | `message.delta` | Emit deltas only (do not resend full content each chunk). | |
| 336 | | | assistant final message | `message.completed` | Must include stable `itemId` for dedupe/finalize behavior. | |
| 337 | | | result (success) | `turn.completed` (`status: "completed"`) | This is the main turn boundary for checkpointing. | |
| 338 | | | result (interrupted/cancelled) | `turn.completed` (`status: "interrupted"`/`"cancelled"`) | Preserve state semantics used by ingestion + UI. | |
| 339 | | | result (error) | `runtime.error` and `turn.completed` (`status: "failed"`) | Include concise error message. | |
| 340 | | | tool start | `tool.started` | Set `toolKind` using shared classifier (`command`, `file-change`, `other`). | |
| 341 | | | tool result | `tool.completed` | Include detail summary used by activity feed. | |
| 342 | | | canUseTool prompt | `approval.requested` | Source of request id shown in UI. | |
| 343 | | | approval decision returned | `approval.resolved` | Must correlate using same request id. | |
| 344 | | | session initialization | `session.started`, optional `thread.started` | Ensure `threadId` is emitted early and consistently. | |
| 345 | | | process exit/close | `session.exited` | Required for session status projection cleanup. | |
| 346 | | |
| 347 | | ### 2.4 Session/thread/resume identifiers |
| 348 | | |
| 349 | | Define explicit adapter semantics: |
| 350 | | |
| 351 | | 1. `sessionId`: adapter-owned stable session id. |
| 352 | | 2. `threadId`: Claude conversation/session identifier returned as `ProviderThreadId`. |
| 353 | | 3. `resumeCursor`: provider-specific cursor (for example thread id + message cursor) needed for precise recovery/rollback. |
| 354 | | 4. Orchestration/shared services persist and forward `resumeCursor` unchanged without provider-specific parsing. |
| 355 | | |
| 356 | | ### 2.5 Rollback/read strategy |
| 357 | | |
| 358 | | Implement `readThread()` and `rollbackThread()` in a way compatible with `CheckpointReactor`. |
| 359 | | |
| 360 | | Preferred: |
| 361 | | |
| 362 | | 1. Track per-turn resume cursors and reconstruct session state on rollback. |
| 363 | | 2. Keep adapter `sessionId` stable across internal rehydration/restart. |
| 364 | | |
| 365 | | Fallback (explicitly documented if chosen): |
| 366 | | |
| 367 | | 1. Filesystem revert succeeds even when conversation rewind is partial. |
| 368 | | 2. Reactor behavior is updated to surface a clear warning activity instead of silent drift. |
| 369 | | |
| 370 | | ### 2.6 Adapter unit tests |
| 371 | | |
| 372 | | Add tests mirroring Codex adapter coverage: |
| 373 | | |
| 374 | | 1. Provider validation (`provider === "claudeCode"`). |
| 375 | | 2. Event mapping to canonical runtime events. |
| 376 | | 3. Approval request lifecycle. |
| 377 | | 4. Resume/recovery behavior. |
| 378 | | 5. Rollback behavior. |
| 379 | | |
| 380 | | --- |
| 381 | | |
| 382 | | ## Phase 3: Register adapter in runtime composition |
| 383 | | |
| 384 | | ### 3.1 Adapter registry wiring |
| 385 | | |
| 386 | | Update: |
| 387 | | |
| 388 | | 1. `apps/server/src/provider/Layers/ProviderAdapterRegistry.ts` |
| 389 | | |
| 390 | | Register both Codex and Claude adapters. |
| 391 | | |
| 392 | | ### 3.2 Server layer composition |
| 393 | | |
| 394 | | Update: |
| 395 | | |
| 396 | | 1. `apps/server/src/serverLayers.ts` |
| 397 | | |
| 398 | | Provide `ClaudeCodeAdapterLive` alongside Codex in `makeServerProviderLayer()`. |
| 399 | | |
| 400 | | ### 3.3 Composition tests |
| 401 | | |
| 402 | | Update integration tests to ensure: |
| 403 | | |
| 404 | | 1. Registry exposes Claude provider. |
| 405 | | 2. Provider service can route Claude sessions. |
| 406 | | |
| 407 | | --- |
| 408 | | |
| 409 | | ## Phase 4: Orchestration command/reactor updates |
| 410 | | |
| 411 | | ### 4.1 Decider propagation (completed) |
| 412 | | |
| 413 | | Update `apps/server/src/orchestration/decider.ts`: |
| 414 | | |
| 415 | | 1. Carry optional `provider` from `thread.turn.start` command into `thread.turn-start-requested` event payload. |
| 416 | | 2. Keep this behavior provider-agnostic (no provider-specific runtime fields in the event payload). |
| 417 | | |
| 418 | | ### 4.2 ProviderCommandReactor provider selection |
| 419 | | |
| 420 | | Update `apps/server/src/orchestration/Layers/ProviderCommandReactor.ts`: |
| 421 | | |
| 422 | | 1. Prefer provider from turn-start event payload when starting a new session. |
| 423 | | 2. Fallback to existing thread session provider when payload omitted. |
| 424 | | 3. Fallback to default provider only when neither is present. |
| 425 | | 4. On restart/rebind, forward the runtime session's persisted `resumeCursor` as-is (no reconstruction from `providerThreadId`). |
| 426 | | |
| 427 | | Switch behavior policy (explicit in implementation): |
| 428 | | |
| 429 | | 1. If active session provider differs from requested provider, stop and recreate session before sending turn. |
| 430 | | 2. Keep current provider when request omits provider. |
| 431 | | |
| 432 | | ### 4.3 Reactor/invariant tests |
| 433 | | |
| 434 | | Update/add tests in: |
| 435 | | |
| 436 | | 1. `apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts` |
| 437 | | 2. `apps/server/src/orchestration/*.test.ts` as needed |
| 438 | | |
| 439 | | Validate first-turn provider choice, provider switching semantics, and backward compatibility when provider is omitted. |
| 440 | | |
| 441 | | --- |
| 442 | | |
| 443 | | ## Phase 5: Web provider UX enablement |
| 444 | | |
| 445 | | ### 5.1 Enable Claude option |
| 446 | | |
| 447 | | Update `apps/web/src/session-logic.ts`: |
| 448 | | |
| 449 | | 1. Mark Claude provider option as available. |
| 450 | | |
| 451 | | ### 5.2 Provider selection in composer |
| 452 | | |
| 453 | | Update `apps/web/src/components/ChatView.tsx`: |
| 454 | | |
| 455 | | 1. Add provider picker near model/runtime controls. |
| 456 | | 2. Include selected provider in `thread.turn.start` command payload. |
| 457 | | 3. Keep selected provider sticky per thread in client state (or derive from active session provider when present). |
| 458 | | |
| 459 | | ### 5.3 Provider-aware model picker |
| 460 | | |
| 461 | | Update model picker usage so model options reflect selected provider. |
| 462 | | |
| 463 | | ### 5.4 Web tests |
| 464 | | |
| 465 | | Update/add tests for: |
| 466 | | |
| 467 | | 1. Provider picker interactions. |
| 468 | | 2. Turn-start command includes provider. |
| 469 | | 3. Model list switches by provider. |
| 470 | | |
| 471 | | --- |
| 472 | | |
| 473 | | ## Phase 6: Checkpoint and revert compatibility |
| 474 | | |
| 475 | | ### 6.1 Validate checkpoint reactor expectations |
| 476 | | |
| 477 | | Exercise `apps/server/src/orchestration/Layers/CheckpointReactor.ts` flows with Claude: |
| 478 | | |
| 479 | | 1. Turn baseline capture. |
| 480 | | 2. Turn completion checkpoint capture. |
| 481 | | 3. Revert + provider rollback coordination. |
| 482 | | |
| 483 | | ### 6.2 Policy for partial rollback support |
| 484 | | |
| 485 | | If Claude cannot support full conversation rewind at parity with Codex: |
| 486 | | |
| 487 | | 1. Document behavior clearly. |
| 488 | | 2. Emit explicit thread activity for degraded rollback behavior. |
| 489 | | 3. Ensure filesystem state remains correct and predictable. |
| 490 | | |
| 491 | | ### 6.3 Checkpoint tests |
| 492 | | |
| 493 | | Add/update tests in checkpoint + provider service integration suites for Claude revert behavior. |
| 494 | | |
| 495 | | --- |
| 496 | | |
| 497 | | ## Phase 7: End-to-end hardening and observability |
| 498 | | |
| 499 | | ### 7.1 Integration tests |
| 500 | | |
| 501 | | Add integration coverage for: |
| 502 | | |
| 503 | | 1. `thread.turn.start` with `provider: "claudeCode"` from fresh thread. |
| 504 | | 2. Session recovery after restart (provider session alias/recovery path). |
| 505 | | 3. Approval request/respond flow. |
| 506 | | 4. Interrupt behavior. |
| 507 | | 5. Revert flow with rollback. |
| 508 | | |
| 509 | | ### 7.2 WS behavior regression check |
| 510 | | |
| 511 | | Ensure no contract regressions: |
| 512 | | |
| 513 | | 1. Client still consumes only `orchestration.domainEvent`. |
| 514 | | 2. No new provider-specific WS channels introduced. |
| 515 | | |
| 516 | | ### 7.3 Logging |
| 517 | | |
| 518 | | Confirm both native and canonical provider logs remain useful with multi-adapter setup: |
| 519 | | |
| 520 | | 1. `provider-native.ndjson` |
| 521 | | 2. `provider-canonical.ndjson` |
| 522 | | |
| 523 | | --- |
| 524 | | |
| 525 | | ## File checklist |
| 526 | | |
| 527 | | Likely remaining files to touch: |
| 528 | | |
| 529 | | 1. `apps/server/src/provider/Services/ClaudeCodeAdapter.ts` (new) |
| 530 | | 2. `apps/server/src/provider/Layers/ClaudeCodeAdapter.ts` (new) |
| 531 | | 3. `apps/server/src/provider/Layers/ProviderAdapterRegistry.ts` |
| 532 | | 4. `apps/server/src/serverLayers.ts` |
| 533 | | 5. `apps/server/src/orchestration/Layers/ProviderCommandReactor.ts` |
| 534 | | 6. `apps/web/src/session-logic.ts` |
| 535 | | 7. `apps/web/src/components/ChatView.tsx` |
| 536 | | 8. Related tests under `apps/server/src/provider/Layers`, `apps/server/src/orchestration/Layers`, `apps/server/integration`, and `apps/web/src`. |
| 537 | | |
| 538 | | --- |
| 539 | | |
| 540 | | ## Delivery order |
| 541 | | |
| 542 | | 1. Claude adapter + unit tests on top of the new `providerOptions`/opaque-cursor contracts. |
| 543 | | 2. Registry/layer wiring. |
| 544 | | 3. Remaining reactor updates for provider-aware session selection/switching invariants. |
| 545 | | 4. Web provider picker + provider-aware models. |
| 546 | | 5. Checkpoint/revert compatibility. |
| 547 | | 6. End-to-end integration tests and stabilization. |
| 548 | | |
| 549 | | This order keeps risk isolated and maintains a working orchestrated path at each stage. |