foundry-cc-bridge — Design Doc (Draft)
Status: Draft v0 — scaffold for collaborative refinement Owner: Dan (Human), Cowork session (AI Lead) Related: CSDLC Thesis, Process
TL;DR — A small MCP server that hands refined stories from the Cowork AI Lead to Claude Code sub-agents, runs them in isolated git worktrees, and reports back via Foundry annotations. Closes the execution loop in CSDLC without mid-flight human gates.
Overview
What Is This?
foundry-cc-bridge is a Model Context Protocol server that sits between the Cowork AI Lead and Claude Code sub-agents. The AI Lead, after refining a story to "ready for execution" in Foundry, calls a single tool (execute_story) on the bridge. The bridge creates an isolated git worktree, assembles a prompt from the story doc plus repo conventions, spawns Claude Code with that prompt, captures the result (tests, diff, PR link), and reports back via a Foundry annotation write-back and/or Slack notification.
It exists because today the AI Lead → sub-agent handoff is entirely manual: copy the refined story into a terminal, paste context, invoke claude, watch, come back. The CSDLC thesis treats that as the critical path friction — refinement quality is wasted if the execution step still requires a human babysitter.
Who Is It For?
Exactly one user archetype in v1: Dan, running the Claymore methodology across his own projects. No multi-tenant, no team features, no hosted option. If the pattern proves out, v2+ can generalize.
Design Principles
- The bridge is dumb. All intelligence lives in the AI Lead's prompt-crafting and the story doc itself. The bridge only executes.
- One tool call, one story. v1 does not batch, does not orchestrate multi-story epics. That's the AI Lead's job.
- Worktrees are cheap; sessions are expensive. Every run gets a fresh worktree. Claude Code sessions are spawned per run, not pooled.
- Foundry is the source of truth. Story doc, completion status, review annotations — all live in Foundry. The bridge is stateless-ish (just a runs table).
- Security by locality. v1 runs on Dan's Mac, inherits Dan's git/GitHub credentials. No remote hosting until the threat model is re-examined.
Tech Stack
| Layer | Technology | Rationale |
|---|---|---|
| Language | Python 3.12 | Dan's primary language; rich subprocess/asyncio; matches Anvil's stack |
| MCP SDK | mcp (official Python SDK) | First-party, actively maintained |
| Transport | stdio or localhost Streamable HTTP | Local-only. stdio preferred if Cowork supports it; otherwise 127.0.0.1 HTTP + bearer token |
| Process control | asyncio.create_subprocess_exec driving claude CLI | claude -p headless with --output-format stream-json |
| Git operations | git CLI via subprocess | Worktree support is cleanest via CLI |
| Run store | SQLite via aiosqlite | Single-file, zero-ops, survives bridge restarts |
| Foundry integration | HTTPS calls to Foundry API (reuse FOUNDRY_WRITE_TOKEN) | Bridge posts completion annotations back to the story doc |
| Notifications (v2) | Slack Web API via slack_sdk | First-party connector in Cowork; phone push works out of the box |
| Packaging | uv + pyproject.toml | Fast installs, lockfile, Dan's standard |
Key Libraries
mcp, aiosqlite, httpx, pydantic v2, structlog. Nothing surprising.
System Architecture
Data Flow
Cowork session (AI Lead)
│
│ 1. refines story to ready-for-execution in Foundry
│ 2. calls execute_story(story_path, repo, ...) via MCP
▼
┌─────────────────────────┐
│ foundry-cc-bridge │
│ (local daemon) │
│ │
│ ┌──────────────────┐ │
│ │ MCP server │◄──┼─── stdio / localhost HTTP
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Run orchestr. │ │
│ │ (asyncio) │ │
│ └──────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ SQLite Git Claude │
│ (runs) CLI Code │
│ CLI │
└─────────────────────────┘
│ │
│ ▼
│ Worktree @ ../worktrees/<story-slug>
│ ▼
│ `claude -p` headless run
│ ▼
│ git push + gh pr create
│
▼
Foundry API ←── annotation write-back
│
▼
Slack DM ←── phone notification (v2)
Layer Descriptions
MCP Server Layer. Pure transport + schema. Receives tool calls, validates args against pydantic models, hands work to the orchestrator, streams progress back as MCP progress notifications. Knows nothing about git or Claude Code.
Run Orchestrator. The brain of the bridge. State machine: pending → preparing_worktree → running → capturing → reporting → complete | failed | cancelled. Persists every transition to SQLite so the bridge can survive a restart mid-run.
Git Adapter. Thin wrapper around git worktree add/remove, git status, git diff. All git ops happen in the worktree directory; the main repo is read-only from the bridge's perspective.
Claude Code Adapter. Spawns claude -p <prompt_file> --output-format stream-json --permission-mode acceptEdits in the worktree, streams stdout/stderr to a run log, waits for exit. Parses the stream-json to extract final diff summary, test results, and any PR link the agent created.
Foundry Reporter. HTTPS client that posts an annotation to the original story doc via create_annotation. Uses FOUNDRY_WRITE_TOKEN. Best-effort — the run is considered complete even if reporting fails.
Data Model
Two stores: SQLite at ~/.foundry-cc-bridge/bridge.db for the persistent runs table, and filesystem under ~/.foundry-cc-bridge/runs/<run_id>/ for per-run artifacts (prompt, stdout.log, stderr.log, diff.patch, result.json).
class RunStatus(str, Enum):
PENDING = "pending"
PREPARING = "preparing"
RUNNING = "running"
CAPTURING = "capturing"
REPORTING = "reporting"
COMPLETE = "complete"
FAILED = "failed"
CANCELLED = "cancelled"
class Run(BaseModel):
run_id: str # ulid
story_path: str # foundry doc path
story_heading: str | None # optional section anchor
story_title: str # denormalized for display
repo_path: Path # absolute path to main repo
base_branch: str # e.g. "main"
worktree_path: Path # absolute path to worktree
feature_branch: str # "story/<slug>-<short-ulid>"
status: RunStatus
pid: int | None
started_at: datetime
ended_at: datetime | None
exit_code: int | None
pr_url: str | None # populated if CC opened a PR
test_summary: dict | None # parsed from stream-json
foundry_annotation_id: str | None
error: str | None
One Run per execute_story call. Runs are immutable after reaching a terminal state. No foreign keys — runs reference Foundry docs by path string (Foundry is the system of record).
pending ──► preparing ──► running ──► capturing ──► reporting ──► complete
│ │ │ │
└────────────┴────────────┴─────────────┴──► failed
│
└──► cancelled (via cancel_run tool)
Deployment & Infrastructure
Dependencies
- Claude Code CLI installed and authenticated on the Mac (
claudeon PATH) - git ≥ 2.20 (for worktree support)
- gh CLI authenticated (for PR creation — invoked by Claude Code, not the bridge directly)
- Foundry API reachable (currently
https://foundry-claymore.fly.dev) - Slack MCP connected in Cowork (v2, for push notifications)
If Foundry is down, runs still execute and complete — they just don't get an annotation write-back. The bridge logs the failure and moves on.
Installation
uv tool install foundry-cc-bridge (or uv tool install -e . from a local checkout during development). No separate release pipeline. No staging environment — local IS production in v1.
Security Model
Known Attack Surfaces
- Prompt injection via Foundry story doc. The AI Lead crafts the prompt for Claude Code, but the story content comes from Foundry, which has been observed to accept unauthenticated writes on
/mcp/*routes. A malicious story doc could contain instructions that alter CC's behavior. Mitigation v1: the AI Lead is the only reader of story docs before handoff, and Dan trusts his own Foundry instance. Mitigation v2: Foundry auth hardening (already on the Foundry bug list); the bridge could also diff-check the story doc against a signed version. - Arbitrary code execution via Claude Code. CC runs with
acceptEditspermission mode in the worktree. It can modify files, run tests, install dependencies, and push branches. This is by design — that's the whole point — but it means a compromised prompt is effectively RCE on Dan's machine, scoped to the worktree. Mitigation: worktree isolation limits blast radius; CC does NOT get--allow-all-toolsby default; sensitive operations are governed by CC's own permission system. - Token leakage.
FOUNDRY_WRITE_TOKENlives in the bridge's env. GitHub PAT lives inghCLI's auth store. Bridge logs redact tokens.
Auth Summary
- Bridge ← Cowork: stdio or localhost HTTP with bearer token. No exposed network surface.
- Bridge → Foundry:
FOUNDRY_WRITE_TOKENenv var. - Bridge → GitHub: inherited from
ghCLI auth. - Bridge → Claude Code: inherited from
claudeCLI auth.
Tool Surface (v1)
| Tool | Purpose | Sync / Async |
|---|---|---|
execute_story | Kick off a CC run for a refined story | Async — returns run_id immediately |
get_run_status | Poll a run's status, partial logs, final result | Sync |
list_runs | List active + recent runs (last 50) | Sync |
cancel_run | Kill a running CC subprocess, clean worktree | Sync |
get_run_logs | Fetch stdout/stderr tail for a run | Sync |
execute_story signature
class ExecuteStoryArgs(BaseModel):
story_path: str # Foundry doc path
story_heading: str | None # Optional heading anchor within the doc
repo_path: str # Absolute path to the main repo on local disk
base_branch: str = "main"
prompt_preamble: str | None # AI Lead injects additional context
allowed_tools: list[str] | None # Default: edit+bash+test
max_turns: int = 50 # CC turn budget
The AI Lead is expected to have already read the story doc via Foundry MCP, decided it's ready for execution, and now hands the path off. The bridge re-reads the story doc at execution time to ensure freshness.
Prompt Assembly
The bridge builds the CC prompt from:
- A fixed preamble (role, output format, test/PR conventions)
- The story doc content (fetched fresh from Foundry)
- Repo-specific
CLAUDE.mdif present inrepo_path - Any
prompt_preamblepassed by the AI Lead (for last-mile context)
Written to PROMPT.md inside the worktree before claude -p is invoked. Preserved on disk for audit.
Completion Signaling
Two channels, in order of importance:
- Foundry annotation write-back (primary, always-on). On run complete/failed, the bridge posts an annotation to the original story doc:
"Run {run_id} {status}. Branch: {feature_branch}. PR: {pr_url}. Tests: {test_summary}."The AI Lead sees this on the nextget_pageorlist_annotationscall — which is the natural next thing to do when Dan says "any updates on that story?" - Slack DM (v2, optional). Bridge posts the same message to a configured Slack DM channel. Phone push notifies Dan even if Cowork isn't open. Dan triages from his phone and opens iPad remote desktop if he wants to dig in.
Why This Matters
The bridge is the piece that lets the AI Lead actually close the loop in CSDLC. Without it, "ready for execution" is an aspirational label — something a human still has to act on. With it, the AI Lead can say "I've refined story 3, I'm kicking off execution now" and mean it. That's the CSDLC thesis made operational.
Phase Plan
Each phase is independently shippable. We don't start a phase until the previous one is running in production on Dan's CSDLC work.
v0 — Hello World (1-2 evenings). Python package skeleton, uv setup, MCP SDK wired up. Single tool: execute_story(repo_path, prompt_text) — no Foundry integration, takes a raw prompt. Runs claude -p synchronously, waits for completion, returns stdout. No worktree, no SQLite, no state. Success metric: AI Lead can call execute_story from Cowork and see CC's output.
v1 — Real Execution Loop (1 week). Fetch story doc from Foundry by path. Git worktree isolation, auto-generated branch names. SQLite runs table, async orchestrator, status polling via get_run_status. Prompt assembly from story + repo CLAUDE.md + preamble. Success metric: AI Lead refines a story in Foundry, calls execute_story, polls until complete, reviews the PR.
v2 — Async + Write-back (3-5 days). Run returns immediately with run_id; execution happens in background asyncio task. Foundry annotation write-back on completion. cancel_run, list_runs, get_run_logs tools. Success metric: AI Lead kicks off execution, moves on to refining the next story, sees the annotation appear when the first one finishes.
v3 — Phone Notifications (2-3 days). Slack DM channel configured via env var. Bridge posts run-complete message to Slack on terminal state transitions. Same content as Foundry annotation. Success metric: Dan gets a push notification on his phone when a run completes while he's away from the Mac.
v4 — Concurrency + Polish (deferred). Remove the single-run mutex; support N concurrent runs. Worktree cleanup policy (auto-delete on PR merge via webhook). Run timeout + force-kill. Structured run reports.
Risks & Constraints
Known Limitations
- Single-user, single-host. Bridge runs on Dan's Mac. Won't work from a different machine unless we add HTTP + real auth.
- No concurrent runs in v1. The orchestrator is designed for it, but v1 ships with a mutex to keep the first version debuggable.
- Worktrees accumulate. Cleanup policy is "delete on successful PR merge" — manual check required for failed runs.
- CC turn budget caps complexity. Stories that need >50 turns will fail. AI Lead should refine stories small enough to fit.
Technical Risks
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
claude -p stream-json format changes | Medium | Bridge can't parse results | Pin CC version; treat parse failure as "unknown result" not crash |
| Foundry goes down mid-run | Medium | No write-back annotation | Retry with exponential backoff, then log and continue |
| Prompt injection from malicious story doc | Low (v1) | RCE scoped to worktree | Worktree isolation; Foundry auth hardening on roadmap |
| Worktree conflicts with main repo state | Low | Failed git worktree add | Unique branch names per run (ulid suffix) |
| CC hangs indefinitely | Medium | Orphaned process, blocked mutex | Per-run timeout (default 30min) with SIGTERM then SIGKILL |
Decisions Log
| Date | Decision | Rationale | Alternatives |
|---|---|---|---|
| 2026-04-08 | Standalone project, not embedded in Foundry | Clear separation: Foundry owns docs, bridge owns execution | Embed in Foundry API; CC subagent in Cowork itself |
| 2026-04-08 | Python over TypeScript | Dan's primary language; cleaner async subprocess story | TypeScript (matches Foundry stack); Go (single binary) |
| 2026-04-08 | Local-only, not hosted | Inherits gh/claude/git auth for free; eliminates remote-auth threat model | Host on fly.io; Dan's homelab |
| 2026-04-08 | Primary completion signal is Foundry annotation write-back | Natural fit for "Dan asks for updates" workflow | Cowork notification (doesn't exist); webhook to session (can't inject) |
| 2026-04-08 | iPad remote desktop is the inbound mobile UX, NOT a custom messaging MCP | Preserves session continuity; no injection surface; ~0 setup cost | Custom Telegram MCP; Dispatch-only |
| 2026-04-08 | Git worktrees, one per run | Isolation; parallelism-ready; easy cleanup | Shared repo w/ branch switching; Docker |
| 2026-04-08 | Single mutex in v1 | Debuggability > throughput | Full concurrent orchestrator (deferred to v4) |
Open Questions
These are things to push on during refinement. Each is a real decision that could swing the architecture.
1. Transport: stdio or localhost HTTP? Cowork's current docs say it doesn't load stdio MCPs from claude_desktop_config.json. But the connector UI accepts remote HTTPS URLs — does it accept http://127.0.0.1:8765? Action: try adding it as a connector during v0 and see what happens.
2. Foundry as shared async queue / chat substrate. Separate from the bridge itself, should we stand up a Foundry page template for "session chat" — where Dan can post messages from his phone during the day and the parent Cowork session batch-polls them on demand? This doesn't solve session continuity (the session still only advances on Cowork turns), but it gives us a durable async queue and a shared memory surface between the parent session and any Dispatch-spawned ephemeral sessions. Leaning: yes, but as its own mini-project.
3. Dispatch integration. Any programmatic way to start a Dispatch session from an MCP tool? Registry says no, and the security argument says there never will be. Answer: no. Dispatch stays a manual fallback. Don't design for it.
4. Prompt template source of truth. The fixed preamble — lives in the bridge source? In Foundry as a doc the bridge fetches? In the repo's CLAUDE.md? Foundry is tempting for refinability, but creates a circular dependency (bridge needs Foundry to start a run). Leaning: embed a minimal default in bridge source, allow override via a Foundry path passed in tool args.
5. Auth model if we ever go remote. If v2+ moves the bridge off Dan's Mac, how does it authenticate to GitHub? Bot PAT? GitHub App? Status: flag and defer.
6. Multi-repo support. execute_story takes repo_path per call, so technically already multi-repo. But assumes claude and gh are authenticated once and work everywhere. If Dan has private repos across multiple GitHub accounts, this breaks. Leaning: punt until it's actually a problem.
7. Worktree location. ../worktrees/<slug> (sibling of main repo) or ~/.foundry-cc-bridge/worktrees/<slug> (centralized)? Sibling is nicer for IDE auto-discovery; centralized is nicer for cleanup. Leaning: sibling for v1.
8. What does "refined enough to execute" mean, formally? The AI Lead decides this via vibes today. Should the bridge enforce any contract (e.g. story doc must have an ## Acceptance Criteria section)? Leaning: AI Lead's job. The bridge stays dumb.
This draft lives at projects/foundry-cc-bridge/design-draft. Once refined, it will replace the template at projects/foundry-cc-bridge/design (which is currently in a broken state due to tool quirks during drafting).