Codumentor logo Codumentor

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

  1. Plugin scaffolding registered with Codumentor's plugin system.
  2. The AgentAdapter Protocol, AgentEvent union, Credentials, AuthStatus, AgentResult types — defined exactly as specified in §4 of the index.
  3. A working claude_code tool that calls the real claude CLI via subprocess and returns a ToolResult with the standard envelope.
  4. Tier-0 sandbox: asyncio.create_subprocess_exec with scrubbed env, no containerization.
  5. Technical-user auth: API key read from codumentor.yaml and injected into the subprocess env.
  6. Backend session-id continuity via scratchpad: a second call with the same subagent_id resumes the Claude Code session via --resume.
  7. Plugin event emission for every AgentEvent kind, named plugin.external_agent.<kind>.
  8. A minimal x-external-agent-card UI custom element registered in the plugin manifest that renders the card in the transcript with backend name, status, and (when complete) token usage.
  9. /ui/plugins/external_agent/auth/probe?backend=claude-code route that returns whether the CLI is installed and whether the credentials work.
  10. Unit tests that pass under make test and do not require the real claude binary 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

  1. 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.py lines around _on_response_persist (event emission with align: "right" and event_group_id), and any plugin manifest example under ui/plugins/.
  1. Define types in adapter_base.py exactly as §4 of the index specifies. Keep this file dependency-free — no imports from other plugin modules.
  1. Implement adapters/claude_code.py: - Claude Code's CLI surface is claude --print --output-format stream-json --input-format stream-json --include-partial-messages [--resume <session-id>]. Verify the exact flag names against claude --help in your environment, and adjust if they differ. Record the exact invocation in your handoff under "Contracts established/changed." - build_argv produces the argv list. Prompt is fed on stdin, not as an argv flag. - build_env injects ANTHROPIC_API_KEY from creds.api_key; pass-through PATH, HOME, LANG. Strip everything else. - parse_stream reads stdout line-by-line; each line is a JSON object. Map Claude Code's event types to our AgentEvent kinds. At minimum: system with session_idsession_started; assistant deltas → message_delta; tool calls → tool_use / tool_result; final result → completed. Unknown event types are dropped (do not crash). - extract_session_id pulls the session id from the session_started event. - probe_auth runs claude --print "ping" with a 5s timeout and inspects exit code + stderr to distinguish "not installed," "not authenticated," "ok."
  1. runner.py: orchestrates one subprocess from start to finish. Inputs: adapter, sandbox spawner, creds, prompt, optional session id. Outputs: an async iterator of AgentEvent plus a final AgentResult. Use asyncio.subprocess with stdin/stdout pipes. The stdin write completes immediately (prompt is small); stdout is consumed by adapter.parse_stream. Capture stderr separately for the error path. Apply a configurable timeout (default 5 minutes); on timeout, terminate the subprocess and emit an error event.
  1. sandbox/tier0.py: exposes a single async def spawn(...) context manager that wraps asyncio.create_subprocess_exec. Scrubs the env to only what build_env returns. Sets cwd to the repo path resolved via config.repo_context (see doc/core-dev/repo-paths.md). No filesystem isolation in Tier 0.
  1. auth/technical_user.py: loads creds.api_key from the plugin's config dict (which is hydrated from codumentor.yaml). Resolves ${secret:…} placeholders at startup if user_secrets is configured — but do not declare a hard dependency on user_secrets in this phase.
  1. scratchpad.py: helpers get_session(ctx, subagent_id) -> str | None and set_session(ctx, subagent_id, backend, backend_session_id). Conform to the namespace in §4.5 of the index.
  1. tools/base.py: ExternalAgentTool(Tool) does: - Reads args["query"] and optional args["subagent_id"]. - If subagent_id is provided and a backend session is mapped for it, pass it to runner.spawn for resume. Otherwise generate a new subagent_id (uuid4 hex). - 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 via scratchpad.set_session, and returns a ToolResult with the envelope. - Tool argument schema is exactly {query: str, subagent_id?: str}. Do not widen.
  1. tools/claude_code.py: a thin subclass of ExternalAgentTool with backend = "claude-code", a clear description aimed at the orchestrator LLM ("delegate a focused coding task to the Claude Code CLI; use for…"), and tool name claude_code.
  1. registry.py: simple dict {name: adapter_class}. Phase 1 registers only claude-code.
  1. plugin.py: registers _on_prompt_assemble to append the configured tool instances to the tools list. Reads codumentor.yaml.external_agent.backends at init.
  1. routes.py: one FastAPI router exposing GET /ui/plugins/external_agent/auth/probe?backend=... returning {installed: bool, authenticated: bool, error?: str}.
  1. UI card (ui/plugins/external_agent/index.ts): a single web component x-external-agent-card that renders a card showing backend, status (pending/streaming/completed/error), the last message_delta snippet, and on completed the token usage. Keep it minimal — phase 3 will add more affordances. Manifest declares the eight event types from §4.2 + the custom element.
  1. Tests: - test_external_agent_adapter_claude_code.py: feed canned stream-json fixtures (capture from a real run and save under tests/fixtures/external_agent/) and assert the produced AgentEvent sequence 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 that ui_event_emitter was called with each event kind exactly once.

Acceptance criteria

Out of scope (explicitly defer)

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: