Tasks: Phase 8 — Claude Backend (Core) — Claude Agent SDK¶
Input: Design documents from /docs/product-specs/021-claude-agent-sdk/
Prerequisites: Phases 1–7 complete (base.py, validators.py, sk_backend.py, selector.py, tool_adapters.py, mcp_bridge.py, otel_bridge.py)
Tests: TDD approach — write failing tests first, then implement.
Organization: Phase 8 is the core Claude backend implementation. Tasks are organized into two workstreams: (A) ClaudeBackend + ClaudeSession core implementation, and (B) BackendSelector update. Workstream B depends on A.
Format: [ID] [P?] [Story] Description¶
- [P]: Can run in parallel (different files, no dependencies)
- [Story]: Which user story this task belongs to (e.g., US1, US2)
- Include exact file paths in descriptions
Phase 8A: ClaudeSession & ClaudeBackend — Core Implementation¶
Goal: Create claude_backend.py implementing ClaudeSession(AgentSession) and ClaudeBackend(AgentBackend). This is the core execution path for provider: anthropic agents — single-turn invocations via query(), stateful multi-turn sessions via ClaudeSDKClient, streaming support, subprocess lifecycle management, permission mode mapping, and structured output handling.
Plan Reference: plan.md lines 410–474 (Phase 8: Claude Backend Core)
Spec Reference: spec.md lines 27–41 (US1 — Run a Claude-Native Agent), lines 93–108 (US5 — Streaming Chat), lines 179–193 (US10 — Structured Output), lines 111–123 (US6 — Parallel Subagents), lines 125–141 (US7 — File System Access), lines 143–157 (US8 — Permission Governance), lines 159–177 (US9 — Flexible Auth)
Research Reference: research.md lines 30–101 (§2 — API Entry Points, §2a — ClaudeAgentOptions), lines 106–118 (§3 — Permission Mode Mapping), lines 122–137 (§4 — Authentication), lines 199–230 (§7 — Streaming Message Types), lines 234–243 (§8 — Retry Behaviour), lines 266–281 (§10 — Structured Output)
Data Model Reference: data-model.md lines 202–257 (§4 — ExecutionResult), lines 260–295 (§5 — AgentSession), lines 298–338 (§6 — AgentBackend), lines 342–402 (§7–8 — Entity Relationships, Lifecycle)
Quickstart Reference: quickstart.md lines 139–188 (§2 — Building ClaudeAgentOptions), lines 190–248 (§3 — Stateless Test Invocation), lines 250–278 (§4 — Streaming Chat Session), lines 280–324 (§5 — Authentication Env Vars)
Contract Reference: contracts/execution-result.md lines 17–72 (ExecutionResult, Error Conditions), lines 74–97 (AgentSession), lines 100–128 (AgentBackend Lifecycle)
Independent Test: Mock claude_agent_sdk.query() to return a canned AssistantMessage + ResultMessage stream. Call ClaudeBackend.invoke_once("Hello"). Assert ExecutionResult.response contains the expected text, is_error is False, and token_usage is populated.
Tests for Phase 8A (TDD — write first, verify they FAIL)¶
- T001 [P] [US1] Write unit test for
build_options()intests/unit/lib/backends/test_claude_backend.py - Ref:
plan.md:433–434("BuildClaudeAgentOptionsviabuild_options()"),quickstart.md:139–188 - Create a minimal
Agentfixture withprovider: anthropic,model.name: claude-sonnet-4-6, and optionalclaudeblock. - Assert returned
ClaudeAgentOptionshas correctmodel,system_prompt,permission_mode,max_turns,env,mcp_servers,allowed_tools. - Test with no
claudeblock → defaults:permission_mode=None,max_turns=None, no extra MCP servers. - Test with full
claudeblock → all fields mapped: working dir →cwd,extended_thinking.budget_tokens→max_thinking_tokens,web_search,bash,file_system,subagents,allowed_tools. - Test
response_formatdict →output_formattranslated correctly (NOT using SK's_wrap_response_format). -
Ref:
research.md:266–281(§10 — Structured Output) -
T002 [P] [US1] Write unit test for permission mode mapping in
tests/unit/lib/backends/test_claude_backend.py - Ref:
research.md:106–118(§3 — Permission Mode Mapping),plan.md:446 - Assert:
manual→"default",acceptEdits→"acceptEdits",acceptAll→"bypassPermissions". - Test override for test mode: when
mode="test"andpermission_modeis notmanual, override to"bypassPermissions". - Test
mode="test"withpermission_mode: manual→ keeps"default"(no override). - Test
mode="test"withbash.enabled: trueandallow_side_effects=False→ bash force-disabled. - Test
mode="test"withfile_system.write: trueandallow_side_effects=False→ file_system.write force-disabled. -
Test
mode="test"withbash.enabled: trueandallow_side_effects=True→ bash NOT force-disabled (preserved). -
T003 [P] [US1] Write unit test for
ClaudeBackend.__init__()stores config only (no I/O) intests/unit/lib/backends/test_claude_backend.py - Ref:
plan.md:424("stores config only — no I/O, no subprocess"),plan.md:295–298(lazy-init pattern) - Construct
ClaudeBackend(agent, tool_instances, mode="test")— assert no SDK imports triggered, no subprocess spawned. - Assert
._initialized is Falseafter construction. -
Assert
._options is Noneafter construction (options built lazily ininitialize()). -
T004 [P] [US1] Write unit test for
ClaudeBackend.initialize()calls validators in correct order intests/unit/lib/backends/test_claude_backend.py - Ref:
plan.md:425–434(initialize() steps 1–9), FR-029 (CLAUDE.md collision warning) - Mock all validators (
validate_nodejs,validate_credentials,validate_embedding_provider,validate_tool_filtering,validate_working_directory,validate_response_format). - Assert they are called in the documented order.
- Assert
validate_working_directory()is called withagent.claude.working_directory(covers FR-029 CLAUDE.md collision detection). - Assert
._initialized is Trueafterinitialize(). -
Assert
._optionsis aClaudeAgentOptionsinstance afterinitialize(). -
T005 [P] [US1] Write unit test for
ClaudeBackend.invoke_once()happy path intests/unit/lib/backends/test_claude_backend.py - Ref:
plan.md:435–441(invoke_once steps),quickstart.md:190–248,contracts/execution-result.md:17–32 - Mock
claude_agent_sdk.query()to yield:AssistantMessage(content=[TextBlock(text="Hello world")]), thenResultMessage(is_error=False, num_turns=1, usage={"input_tokens": 10, "output_tokens": 5}, structured_output=None, session_id="abc"). - Assert
ExecutionResult.response == "Hello world". - Assert
ExecutionResult.is_error is False. - Assert
ExecutionResult.token_usage.prompt_tokens == 10. - Assert
ExecutionResult.token_usage.completion_tokens == 5. -
Assert
ExecutionResult.num_turns == 1. -
T006 [P] [US2] Write unit test for
invoke_once()tool call extraction intests/unit/lib/backends/test_claude_backend.py - Ref:
plan.md:437("Collects AssistantMessage blocks — text, tool_use, tool_result"),research.md:229,contracts/execution-result.md:36–52 - Mock stream with
ToolUseBlock(id="toolu_01", name="kb_search", input={"query": "refund"})andToolResultBlock(tool_use_id="toolu_01", content=[TextBlock(text="30-day guarantee")], is_error=False). - Assert
ExecutionResult.tool_calls[0] == {"name": "kb_search", "arguments": {"query": "refund"}, "call_id": "toolu_01"}. -
Assert
ExecutionResult.tool_results[0] == {"call_id": "toolu_01", "result": "30-day guarantee", "is_error": False}. -
T007 [P] [US10] Write unit test for
invoke_once()structured output handling intests/unit/lib/backends/test_claude_backend.py - Ref:
plan.md:439("Validates structured output against schema"),research.md:266–281(§10),spec.md:179–193(US10) - Mock
ResultMessage.structured_output = {"name": "Widget", "price": 9.99}. - Assert
ExecutionResult.structured_output == {"name": "Widget", "price": 9.99}. - Assert
ExecutionResult.responsecontains the JSON-serialized string for NLP/G-Eval consumption. -
Test schema validation failure: structured output does not match configured schema →
ExecutionResult.is_error is True,error_reasonmentions "schema validation". -
T008 [P] [US1] Write unit test for
invoke_once()max_turns exceeded detection intests/unit/lib/backends/test_claude_backend.py - Ref:
plan.md:441("Detects max_turns exceeded via ResultMessage.num_turns >= max_turns"),contracts/execution-result.md:68(error condition) - Mock
ResultMessage.num_turns = 10withmax_turns = 10in agent config. - Assert
ExecutionResult.is_error is True. - Assert
ExecutionResult.error_reason == "max_turns limit reached". -
Assert partial response text is preserved.
-
T009 [P] [US1] Write unit test for
invoke_once()subprocess crash handling intests/unit/lib/backends/test_claude_backend.py - Ref:
plan.md:442("Session-level retry on ProcessError"),research.md:234–243(§8),contracts/execution-result.md:69,spec.md:210(edge case — subprocess terminates) - Mock
claude_agent_sdk.query()to raiseProcessErroron first call, succeed on second. - Assert retry happens (up to 3 attempts, exponential backoff).
-
Mock all 3 retries failing → assert
BackendSessionErroris raised with "subprocess terminated unexpectedly". -
T010 [P] [US9] Write unit test for auth env var handling in
tests/unit/lib/backends/test_claude_backend.py - Ref:
plan.md:434("validate_credentials → stores env dict"),research.md:122–137(§4),quickstart.md:280–324,contracts/agent-yaml-schema.md:67–84 - Design note: The SDK merges
os.environwithClaudeAgentOptions.envautomatically.validate_credentials()returns only extra env vars needed (e.g.,CLAUDE_CODE_USE_BEDROCK=1). Forapi_key/oauth_token, it validates presence and returns{}— the keys are inherited viaos.environ. - Test
auth_provider: api_key→validate_credentials()returns{}(validates ANTHROPIC_API_KEY exists, inherited by subprocess automatically). - Test
auth_provider: oauth_token→validate_credentials()returns{}(validates CLAUDE_CODE_OAUTH_TOKEN exists, inherited automatically). - Test
auth_provider: bedrock→validate_credentials()returns{"CLAUDE_CODE_USE_BEDROCK": "1"}inClaudeAgentOptions.env. - Test
auth_provider: vertex→validate_credentials()returns{"CLAUDE_CODE_USE_VERTEX": "1"}inClaudeAgentOptions.env. - Test
auth_provider: foundry→validate_credentials()returns{"CLAUDE_CODE_USE_FOUNDRY": "1"}inClaudeAgentOptions.env. -
Assert extra env vars are merged into
ClaudeAgentOptions.env. -
T011 [P] [US5] Write unit test for
ClaudeSession.send_streaming()intests/unit/lib/backends/test_claude_backend.py - Ref:
plan.md:419("yields text chunks from StreamEvent / AssistantMessage blocks"),quickstart.md:250–278,research.md:199–230(§7) - Mock
ClaudeSDKClient.query()andreceive_response()to yield multipleAssistantMessagewithTextBlockchunks. - Assert chunks arrive progressively (not all at once).
-
Assert final
ResultMessageterminates the stream. -
T012 [P] [US5] Write unit test for
ClaudeSession.send()(non-streaming) intests/unit/lib/backends/test_claude_backend.py - Ref:
plan.md:418("collects full response from receive_response() iterator"),data-model.md:275–280 - Mock
ClaudeSDKClient.query()+receive_response()yieldingAssistantMessage+ResultMessage. - Assert
ExecutionResult.responseis the concatenated text from allTextBlocks. -
Assert
token_usageis extracted fromResultMessage.usage. -
T013 [P] [US1] Write unit test for
ClaudeSession.close()callsclient.disconnect()intests/unit/lib/backends/test_claude_backend.py - Ref:
plan.md:421("close() → client.disconnect()"),data-model.md:392–396 - Mock
ClaudeSDKClient.disconnect(). -
Assert
close()callsdisconnect()exactly once. -
T014 [P] [US5] Write unit test for
ClaudeSessionmulti-turn state tracking intests/unit/lib/backends/test_claude_backend.py - Ref:
research.md:59–80(multi-turn state is OPT-IN),research.md:81("session wrapper must pass continue_conversation=True and track session_id") - Mock two successive
send()calls. - Assert first call uses the base options (no
continue_conversation, noresume). - Assert second call creates a NEW
ClaudeAgentOptionsinstance (not mutating the original) withcontinue_conversation=Trueandresume=<session_id from first ResultMessage>. -
Assert the original base options object is NOT mutated after two turns.
-
T015 [P] [US1] Write unit test for
ClaudeBackend.initialize()lazy-init guard intests/unit/lib/backends/test_claude_backend.py - Ref:
plan.md:295–298("initialize() is called lazily on the first invoke_once() or create_session()") - Call
invoke_once()without callinginitialize()first → assertinitialize()is called automatically. - Call
create_session()without callinginitialize()first → assertinitialize()is called automatically. -
Call
initialize()explicitly, theninvoke_once()→ assertinitialize()is NOT called a second time. -
T016 [P] [US1] Write unit test for
ClaudeBackend.teardown()intests/unit/lib/backends/test_claude_backend.py - Ref:
plan.md:444("teardown() → cleanup"),data-model.md:336–338 - Assert
teardown()resets_initialized = Falseand_options = None. - Assert calling
invoke_once()afterteardown()triggers re-initialization.
Implementation for Phase 8A¶
- T017 [US1] Create
src/holodeck/lib/backends/claude_backend.pywith module docstring, imports, and helper functions - Ref:
plan.md:414,quickstart.md:139–188 - Import from
claude_agent_sdk:ClaudeAgentOptions,ClaudeSDKClient,query,AssistantMessage,ResultMessage,StreamEvent,ProcessError,TextBlock,ToolUseBlock,ToolResultBlock. - Import from
holodeck:ExecutionResult,AgentSession,BackendSessionError,BackendInitError,TokenUsage,Agent. - Implement helper
_extract_result_text(content: list) -> str— extracts text from content blocks. - Implement helper
_build_permission_mode(claude_config, mode) -> str | None— maps HoloDeck permission modes to SDK literals perresearch.md:106–118. - Implement helper
_build_output_format(response_format) -> dict | None— translates to plaindict[str, Any]forClaudeAgentOptions.output_format, NOT using SK's_wrap_response_format. Note: SDK has noOutputFormatclass —output_formatisdict[str, Any] | None. -
Extract shared instruction resolver: Move
AgentFactory._load_instructions()logic fromsrc/holodeck/lib/test_runner/agent_factory.pyinto a shared utility (e.g.,src/holodeck/lib/instruction_resolver.pyorsrc/holodeck/config/loader.py). Implementresolve_instructions(instructions: Instructions, base_dir: Path | None = None) -> str. Both SK backend and Claude backend import from this shared location. UpdateAgentFactoryto use the shared function. -
T018 [US1] Implement
build_options()function insrc/holodeck/lib/backends/claude_backend.py - Ref:
plan.md:433–434,quickstart.md:139–188,research.md:83–101(§2a — ClaudeAgentOptions fields) - Accept
agent: Agent,tool_server: McpSdkServerConfig | None,mcp_configs: dict,auth_env: dict,otel_env: dict,mode: str,allow_side_effects: bool. - Build
system_promptfromagent.instructions(resolve inline or file). - Build
mcp_serversdict: mergeholodeck_toolsserver (if tool_server) + external MCP configs. - Build
envdict: mergeauth_env+otel_env. - Map
extended_thinking.budget_tokens→max_thinking_tokens(only ifextended_thinking.enabled). - Map
allowed_toolsfromagent.claude.allowed_toolsmerged with SDK tool names. - Handle test mode safety: when
mode="test", forcebash.enabled=Falseandfile_system.write=Falseunlessallow_side_effects=True. -
Return
ClaudeAgentOptions(...)with all fields. -
T019 [US1] Implement
ClaudeSessionclass insrc/holodeck/lib/backends/claude_backend.py - Ref:
plan.md:416–421,quickstart.md:250–278,research.md:59–80(multi-turn OPT-IN) __init__(self, options: ClaudeAgentOptions)— store options as_base_options(immutable reference), set_session_id = None,_client = None.async _ensure_client(self)— createClaudeSDKClientand enter async context if not already._build_turn_options(self) -> ClaudeAgentOptions— for turn 1, return a copy of_base_options. For turn 2+, create a NEWClaudeAgentOptionswithcontinue_conversation=Trueandresume=_session_id. NEVER mutate_base_options.async send(self, message: str) -> ExecutionResult— call_client.query(message), collect fromreceive_response(), extract text/tool_calls/tool_results/token_usage, update_session_idfromResultMessage, buildExecutionResult.async send_streaming(self, message: str) -> AsyncGenerator[str, None]— yieldTextBlock.textchunks progressively fromreceive_response(). Return type matchesAgentSessionProtocol (AsyncGenerator[str, None]).async close(self)— call_client.disconnect()if client exists, set_client = None.-
Handle
ProcessError→ raiseBackendSessionError("subprocess terminated unexpectedly: {detail}"). -
T020 [US1] Implement
ClaudeBackendclass insrc/holodeck/lib/backends/claude_backend.py - Ref:
plan.md:423–444,data-model.md:298–338(§6 — AgentBackend),quickstart.md:190–248 __init__(self, agent: Agent, tool_instances: dict[str, VectorStoreTool | HierarchicalDocumentTool] | None = None, mode: str = "test", allow_side_effects: bool = False)— store config only, no I/O. Set_initialized = False,_options = None.async initialize(self)— idempotent (check_initialized). Steps:validate_nodejs()auth_env = validate_credentials(agent.model)validate_embedding_provider(agent)(if vectorstore tools)validate_tool_filtering(agent)(warn + skip)- Build tool adapters from
tool_instances→create_tool_adapters()+build_holodeck_sdk_server() - Build MCP bridge configs →
build_claude_mcp_configs() - Build OTel env vars →
translate_observability()(if observability configured) - Build
ClaudeAgentOptionsviabuild_options() validate_working_directory(claude.working_directory)validate_response_format(agent.response_format)- Set
_initialized = True
async invoke_once(self, message: str, context=None) -> ExecutionResult— lazy-init guard → callinitialize()if not yet. Usequery(prompt=message, options=self._options). CollectAssistantMessageblocks (text, tool_use, tool_result) +ResultMessagefor token usage/structured output. Detectmax_turnsexceeded. Retry onProcessError(3 attempts, exponential backoff). ReturnExecutionResult.async create_session(self) -> ClaudeSession— lazy-init guard → returnClaudeSession(self._options).-
async teardown(self)— reset_initialized = False,_options = None. -
T021 [US1] Implement subprocess retry logic with exponential backoff in
src/holodeck/lib/backends/claude_backend.py - Ref:
plan.md:442("3 attempts, exponential backoff"),research.md:234–243(§8 — Retry Behaviour) - Retry only on
ProcessError(subprocess spawn/crash). NOT on API-level errors (SDK handles those internally per FR-013). - Backoff: 1s → 2s → 4s (exponential).
- After 3 failures: raise
BackendSessionError("Claude subprocess failed after 3 retries: {last_error}"). -
Log each retry attempt at WARNING level.
-
T022 [US10] Implement structured output schema validation in
src/holodeck/lib/backends/claude_backend.py - Ref:
plan.md:439("Validates structured output against schema if response_format configured"),spec.md:189–193(US10 acceptance),research.md:266–281(§10) - When
response_formatis configured andResultMessage.structured_outputis returned:- Validate against configured JSON Schema using
jsonschema.validate(). - Set
ExecutionResult.structured_outputto the validated object. - Set
ExecutionResult.responsetojson.dumps(structured_output)for NLP/G-Eval metrics.
- Validate against configured JSON Schema using
- On validation failure: set
ExecutionResult.is_error = True,error_reason = "Structured output does not match configured schema: {detail}".
Checkpoint: At this point, claude_backend.py should contain a complete ClaudeSession and ClaudeBackend with all unit tests passing against mocked SDK. Run: pytest tests/unit/lib/backends/test_claude_backend.py -n auto -v
Phase 8B: BackendSelector Update¶
Goal: Update selector.py to route provider: anthropic agents to ClaudeBackend instead of raising BackendInitError. This completes the routing so that BackendSelector.select() can produce both SK and Claude backends.
Plan Reference: plan.md lines 448–464 (Phase 8.2 — selector.py)
Spec Reference: spec.md lines 218–233 (FR-001, FR-010, FR-011)
Data Model Reference: data-model.md lines 362–373 (BackendSelector routing)
Independent Test: Call BackendSelector.select(agent) with a provider: anthropic agent. Assert a ClaudeBackend instance is returned (not BackendInitError). Call with a provider: openai agent → assert SKBackend is returned (unchanged).
Tests for Phase 8B (TDD — write first, verify they FAIL)¶
- T023 [P] [US1] Write unit test for
BackendSelectorroutinganthropictoClaudeBackendintests/unit/lib/backends/test_selector.py - Ref:
plan.md:448–464,data-model.md:362–373 - Replaces existing
test_anthropic_raises_backend_init_error()— after Phase 8B, Anthropic no longer raises an error. - Mock
ClaudeBackend.__init__andinitialize(). - Create agent with
provider: anthropic→ assertBackendSelector.select()returnsClaudeBackendinstance. -
Assert
initialize()was called on the returned backend. -
T024 [P] [US1] Write unit test for
BackendSelectorpassingtool_instancesandmodetoClaudeBackendintests/unit/lib/backends/test_selector.py - Ref:
plan.md:453–458(selector signature with tool_instances, mode) - Mock
ClaudeBackend.__init__. - Call
BackendSelector.select(agent, tool_instances={"kb": mock_tool}, mode="chat"). -
Assert
ClaudeBackendwas constructed withtool_instances={"kb": mock_tool}andmode="chat". -
T025 [P] [US1] Write unit test for unsupported provider raises
BackendInitErrorintests/unit/lib/backends/test_selector.py - Ref:
selector.py:46(existing behaviour) - Create agent with a hypothetical unsupported provider → assert
BackendInitErrorraised. - Note: Existing SK-path tests (
test_openai_returns_sk_backend,test_azure_openai_returns_sk_backend,test_ollama_returns_sk_backend) already cover FR-010 and must continue to pass after theselect()signature change. No new SK-path tests needed.
Implementation for Phase 8B¶
- T026 [US1] Update
src/holodeck/lib/backends/selector.pyto routeProviderEnum.ANTHROPICtoClaudeBackend - Ref:
plan.md:448–464 - Import
ClaudeBackendfromclaude_backend. - Update
select()signature to accept optionaltool_instances: dict[str, VectorStoreTool | HierarchicalDocumentTool] | None = None,mode: str = "test",allow_side_effects: bool = False. New params have defaults so existing callers are unaffected. - For
ProviderEnum.ANTHROPIC: instantiateClaudeBackend(agent, tool_instances, mode, allow_side_effects), callawait backend.initialize(), return. - Remove the current
BackendInitError("Anthropic provider is not yet supported...")block. - Preserve
SKBackendpath forOPENAI,AZURE_OPENAI,OLLAMA— no changes. -
Existing tests must still pass:
test_openai_returns_sk_backend,test_azure_openai_returns_sk_backend,test_ollama_returns_sk_backend,test_initialize_awaited_on_returned_backend. -
T027 [US1] Update
src/holodeck/lib/backends/__init__.pyto exportClaudeBackendandClaudeSession - Ref:
plan.md:95(claude_backend.py in project structure) - Add imports:
from holodeck.lib.backends.claude_backend import ClaudeBackend, ClaudeSession. - Add to
__all__:"ClaudeBackend","ClaudeSession".
Checkpoint: At this point, BackendSelector.select() should route Anthropic agents to ClaudeBackend. Existing SK-path tests must still pass. Run: pytest tests/unit/lib/backends/test_selector.py tests/unit/lib/backends/test_claude_backend.py -n auto -v
Phase 8C: Code Quality & Verification¶
Goal: Run all code quality checks and verify zero regressions across the full test suite.
Plan Reference: plan.md lines 551–561 (Phase 12: Code Quality — run after each phase)
- T028 Run
make formatto apply Black + Ruff formatting to all new/modified files - Ref:
plan.md:556 -
Files:
src/holodeck/lib/backends/claude_backend.py,src/holodeck/lib/backends/selector.py,src/holodeck/lib/backends/__init__.py,tests/unit/lib/backends/test_claude_backend.py,tests/unit/lib/backends/test_selector.py. -
T029 Run
make lint-fixto auto-fix linting issues in new/modified files -
Ref:
plan.md:557 -
T030 Run
make type-checkto verify MyPy strict mode passes for all new code - Ref:
plan.md:558 - All new functions must have type hints on parameters and return values.
-
Fix any MyPy errors before proceeding.
-
T031 Run
make test-unitto verify zero regressions across the full unit test suite - Ref:
plan.md:559,plan.md:299("existing unit tests MUST pass without modification") - Run:
pytest tests/unit/ -n auto -v - All existing tests from Phases 1–7 must still pass.
-
All new Phase 8 tests must pass.
-
T032 Run
make securityto verify Bandit + Safety + detect-secrets pass - Ref:
plan.md:560 - No new security warnings allowed in
claude_backend.pyorselector.py.
Checkpoint: Full CI pipeline green. Ready for Phase 9 (Test Runner Updates).
Dependencies & Execution Order¶
Phase Dependencies¶
- Phase 8A Tests (T001–T016): No dependencies on each other — all [P] parallelizable. All depend on Phases 1–7 being complete.
- Phase 8A Implementation (T017–T022): T017 must come first (module skeleton + shared instruction resolver). T018–T022 depend on T017. T019 and T020 can be partially parallelized but T020 (
ClaudeBackend) references T019 (ClaudeSession). - Phase 8B Tests (T023–T025): Depend on Phase 8A implementation being complete. All [P] parallelizable among themselves.
- Phase 8B Implementation (T026–T027): Depend on Phase 8A implementation + Phase 8B tests. T027 depends on T026.
- Phase 8C Quality (T028–T032): Depends on all Phase 8A + 8B being complete. T028–T030 are parallelizable. T031 depends on T028–T030. T032 depends on T031.
Execution Order (Critical Path)¶
T001–T016 (parallel) ─── all Phase 8A tests
│
T017 ─── module skeleton + instruction resolver extraction
│
T018, T019, T020, T021, T022 ─── Phase 8A implementation (mostly sequential)
│
T023–T025 (parallel) ─── Phase 8B tests
│
T026, T027 ─── Phase 8B implementation (sequential)
│
T028–T030 (parallel) ─── formatting, linting, type-check
│
T031 ─── full test suite
│
T032 ─── security scan
Within Phase 8A Implementation¶
- T017 (skeleton + instruction resolver) → T018 (build_options) → T019 (ClaudeSession) → T020 (ClaudeBackend) → T021 (retry logic) → T022 (structured output validation)
- T021 and T022 can be done in either order after T020.
Parallel Opportunities¶
# Launch all Phase 8A tests in parallel:
Task: T001 — build_options unit test
Task: T002 — permission mode mapping test
Task: T003 — __init__ no-I/O test
Task: T004 — initialize() validator order test
Task: T005 — invoke_once() happy path test
Task: T006 — tool call extraction test
Task: T007 — structured output test
Task: T008 — max_turns exceeded test
Task: T009 — subprocess crash retry test
Task: T010 — auth env var handling test
Task: T011 — send_streaming() test
Task: T012 — send() non-streaming test
Task: T013 — close() test
Task: T014 — multi-turn state tracking test
Task: T015 — lazy-init guard test
Task: T016 — teardown() test
# Launch all Phase 8B tests in parallel:
Task: T023 — selector routes anthropic to ClaudeBackend (replaces existing error test)
Task: T024 — selector passes tool_instances/mode
Task: T025 — unsupported provider error
# Launch quality checks in parallel:
Task: T028 — format
Task: T029 — lint-fix
Task: T030 — type-check
Implementation Strategy¶
MVP First (Phase 8A Only)¶
- Write all T001–T016 tests (TDD — verify they FAIL)
- Implement T017–T022 (claude_backend.py)
- Verify all Phase 8A tests pass
- STOP and VALIDATE:
ClaudeBackend.invoke_once()works with mocked SDK - This alone is a demonstrable milestone — the core execution engine exists
Full Phase 8¶
- Complete Phase 8A (MVP above)
- Write Phase 8B tests (T023–T025)
- Implement Phase 8B (T026–T027 — selector update + exports)
- Run quality checks (T028–T032)
- STOP and VALIDATE:
BackendSelector.select()routes to both backends
User Story Coverage¶
| User Story | Tasks | Key Deliverable |
|---|---|---|
| US1 (Run Claude-Native Agent) | T001–T005, T008–T009, T013, T015–T020, T023–T027 | Core execution path |
| US2 (Vectorstore/Doc Tools) | T006 (tool extraction) | Tool calls via adapters (Phase 5) |
| US3 (MCP Tools) | — | MCP bridge (Phase 6, complete) |
| US5 (Streaming Chat) | T011–T012, T014 | ClaudeSession.send_streaming() |
| US6 (Parallel Subagents) | — | Subagent config passed via build_options() |
| US7 (File System Access) | — | File system config passed via build_options() |
| US8 (Permission Governance) | T002 | Permission mode mapping |
| US9 (Flexible Auth) | T010 | Auth env var handling |
| US10 (Structured Output) | T007, T022 | Schema validation + output handling |
Notes¶
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability
- TDD: write tests first, verify they fail, then implement
- All
claude_agent_sdkcalls are mocked in unit tests — no API key needed - Integration tests (requiring real API keys) are deferred to Phase 11 per plan.md:541–548
- Commit after each logical group (Phase 8A tests → Phase 8A impl → Phase 8B tests → Phase 8B impl → quality)
- Stop at any checkpoint to validate independently