Phase 1 — Bones + Claude Code
Goal
Ship an end-to-end vertical slice: a working claude_code(...) tool that spawns the real claude CLI as a subprocess, streams its events into Codumentor's UI as a transcript card, and returns the final assistant message wrapped in the standard [Subagent ID: …] envelope. Tier-0 sandbox only, technical-user auth only.
This phase establishes every shared contract in §4 of the index. Get them right — phases 2–7 build on them.
Preconditions
None. This is the first phase.
Deliverables
- Plugin scaffolding registered with Codumentor's plugin system.
- The
AgentAdapterProtocol,AgentEventunion,Credentials,AuthStatus,AgentResulttypes — defined exactly as specified in §4 of the index. - A working
claude_codetool that calls the realclaudeCLI via subprocess and returns aToolResultwith the standard envelope. - Tier-0 sandbox:
asyncio.create_subprocess_execwith scrubbed env, no containerization. - Technical-user auth: API key read from
codumentor.yamland injected into the subprocess env. - Backend session-id continuity via scratchpad: a second call with the same
subagent_idresumes the Claude Code session via--resume. - Plugin event emission for every
AgentEventkind, namedplugin.external_agent.<kind>. - A minimal
x-external-agent-cardUI custom element registered in the plugin manifest that renders the card in the transcript with backend name, status, and (when complete) token usage. /ui/plugins/external_agent/auth/probe?backend=claude-coderoute that returns whether the CLI is installed and whether the credentials work.- Unit tests that pass under
make testand do not require the realclaudebinary to be installed.
New files
src/codumentor/plugins/external_agent/
├── __init__.py
├── plugin.yaml
├── plugin.py
├── adapter_base.py
├── adapters/
│ ├── __init__.py
│ └── claude_code.py
├── tools/
│ ├── __init__.py
│ ├── base.py
│ └── claude_code.py
├── registry.py
├── runner.py
├── sandbox/
│ ├── __init__.py
│ └── tier0.py
├── auth/
│ ├── __init__.py
│ └── technical_user.py
├── scratchpad.py
└── routes.py
ui/plugins/external_agent/
├── manifest.json
└── index.ts
tests/
├── test_external_agent_adapter_claude_code.py
├── test_external_agent_runner.py
└── test_external_agent_tool.py
Implementation steps
- Read the cited files before writing anything:
src/codumentor/tools/agentic_tool.py(the envelope and metadata conventions),src/codumentor/plugins/agenticmemory/(event emission and UI card patterns),src/codumentor/plugins/workspace_isolation/plugin.pylines around_on_response_persist(event emission withalign: "right"andevent_group_id), and any plugin manifest example underui/plugins/.
- Define types in
adapter_base.pyexactly as §4 of the index specifies. Keep this file dependency-free — no imports from other plugin modules.
- Implement
adapters/claude_code.py: - Claude Code's CLI surface isclaude --print --output-format stream-json --input-format stream-json --include-partial-messages [--resume <session-id>]. Verify the exact flag names againstclaude --helpin your environment, and adjust if they differ. Record the exact invocation in your handoff under "Contracts established/changed." -build_argvproduces the argv list. Prompt is fed on stdin, not as an argv flag. -build_envinjectsANTHROPIC_API_KEYfromcreds.api_key; pass-throughPATH,HOME,LANG. Strip everything else. -parse_streamreads stdout line-by-line; each line is a JSON object. Map Claude Code's event types to ourAgentEventkinds. At minimum:systemwithsession_id→session_started;assistantdeltas →message_delta; tool calls →tool_use/tool_result; final result →completed. Unknown event types are dropped (do not crash). -extract_session_idpulls the session id from thesession_startedevent. -probe_authrunsclaude --print "ping"with a 5s timeout and inspects exit code + stderr to distinguish "not installed," "not authenticated," "ok."
runner.py: orchestrates one subprocess from start to finish. Inputs: adapter, sandbox spawner, creds, prompt, optional session id. Outputs: an async iterator ofAgentEventplus a finalAgentResult. Useasyncio.subprocesswith stdin/stdout pipes. The stdin write completes immediately (prompt is small); stdout is consumed byadapter.parse_stream. Capture stderr separately for the error path. Apply a configurable timeout (default 5 minutes); on timeout, terminate the subprocess and emit anerrorevent.
sandbox/tier0.py: exposes a singleasync def spawn(...)context manager that wrapsasyncio.create_subprocess_exec. Scrubs the env to only whatbuild_envreturns. Setscwdto the repo path resolved viaconfig.repo_context(seedoc/core-dev/repo-paths.md). No filesystem isolation in Tier 0.
auth/technical_user.py: loadscreds.api_keyfrom the plugin's config dict (which is hydrated fromcodumentor.yaml). Resolves${secret:…}placeholders at startup ifuser_secretsis configured — but do not declare a hard dependency onuser_secretsin this phase.
scratchpad.py: helpersget_session(ctx, subagent_id) -> str | Noneandset_session(ctx, subagent_id, backend, backend_session_id). Conform to the namespace in §4.5 of the index.
tools/base.py:ExternalAgentTool(Tool)does: - Readsargs["query"]and optionalargs["subagent_id"]. - Ifsubagent_idis provided and a backend session is mapped for it, pass it torunner.spawnfor resume. Otherwise generate a newsubagent_id(uuid4hex). -permission_request()returns a single request describing "delegate to." This is the only Codumentor permission gate; once approved, the external agent's internal tools run unattended. - Spawns the runner, emits every event via ctx["ui_event_emitter"], awaits the final result, persists the backend session id viascratchpad.set_session, and returns aToolResultwith the envelope. - Tool argument schema is exactly{query: str, subagent_id?: str}. Do not widen.
tools/claude_code.py: a thin subclass ofExternalAgentToolwithbackend = "claude-code", a cleardescriptionaimed at the orchestrator LLM ("delegate a focused coding task to the Claude Code CLI; use for…"), and tool nameclaude_code.
registry.py: simple dict{name: adapter_class}. Phase 1 registers onlyclaude-code.
plugin.py: registers_on_prompt_assembleto append the configured tool instances to the tools list. Readscodumentor.yaml.external_agent.backendsat init.
routes.py: one FastAPI router exposingGET /ui/plugins/external_agent/auth/probe?backend=...returning{installed: bool, authenticated: bool, error?: str}.
- UI card (
ui/plugins/external_agent/index.ts): a single web componentx-external-agent-cardthat renders a card showing backend, status (pending/streaming/completed/error), the lastmessage_deltasnippet, and oncompletedthe token usage. Keep it minimal — phase 3 will add more affordances. Manifest declares the eight event types from §4.2 + the custom element.
- Tests: -
test_external_agent_adapter_claude_code.py: feed canned stream-json fixtures (capture from a real run and save undertests/fixtures/external_agent/) and assert the producedAgentEventsequence is correct. -test_external_agent_runner.py: replace the subprocess with a fake binary (a small Python script written to a temp dir, made executable, that emits fixture stdout) and assert the runner produces the expected event stream and result. -test_external_agent_tool.py: end-to-end with a mocked adapter; assert the envelope, the scratchpad mutation, and thatui_event_emitterwas called with each event kind exactly once.
Acceptance criteria
make testpasses (no new failures, all new tests included).- A manual probe with the real
claudeCLI installed and authenticated: ask the orchestrator to callclaude_code(query="List files in this repo and tell me what kind of project it is")and verify (a) the transcript card appears live and updates, (b) the final assistant message is returned wrapped in the envelope, (c) a second call with the samesubagent_idreuses the Claude Code session (visible by Claude Code referring back to prior context). - The auth probe route returns sane values for each of: CLI missing, CLI installed but no key, CLI installed with valid key.
Out of scope (explicitly defer)
- Codex, OpenCode, Gemini, Codumentor-self adapters — phases 2, 4, 7.
- BYO API key UI / settings panel — phase 3.
- OAuth / per-user
HOME— phase 6. - Tier-1 (containerized) sandbox — phase 5.
- MCP bridge — phase 7.
- Translating the external agent's internal tool calls into Codumentor permission requests — never (locked decision §2.6).
- Cost attribution UI — later.
If you find yourself implementing any of these, stop and report it under "Surprises" / "Deferred."
Handoff
Fill out the handoff template from §7 of the index. In "Contracts established/changed," include verbatim:
- The exact
AgentAdapterProtocol signature you committed to (in case anything had to differ from §4.1). - The exact
AgentEventkinds and their payload field names. - The exact Claude Code argv invocation you settled on.
- The scratchpad key path you used.
- The tool argument JSON schema.
- The event names you emit and their payload fields.