Replace the manual copy-paste relay between Claude Desktop (Chat) and Claude Code (VS Code extension/CLI) with an automated loop.
Replace the manual copy-paste relay between Claude Desktop (Chat) and Claude Code (VS Code extension/CLI) with an automated loop:
- The user only talks to Claude Desktop: defines/refines scope, approves plans, answers questions when genuinely needed.
- Claude Desktop ("the Architect") and Claude Code ("the Builder") exchange work automatically.
- Claude Desktop drives toward completion of the agreed scope on its own, surfacing to the user only when a real decision is needed.
- The two remain independent thinkers — Claude Code doesn't blindly execute, Claude Desktop doesn't blindly accept output back. Shared memory/context is not a goal; each handoff carries whatever context is needed, the same way manual copy-paste does today.
Current scope: Salesforce dev project. Claude Code works only on local files in the VS Code project — no deploy, no environment promotion (sandbox/production) is handled by this loop yet. That's deliberately deferred.
A file-based job queue sits between the two:
.ai/
queue/ pending tasks, e.g. 0001.json, 0002.json
completed/ results, matched by task id
logs/ full raw output per run
- Architect (Claude Desktop, via an MCP server) writes a task into
queue/when the user approves a plan. - Watcher (a script running in/around VS Code) picks up new tasks, invokes Claude Code headlessly, captures the result, writes it to
completed/, logs full output tologs/. - Architect polls for the result (mechanism in §5) and reports back to the user, or requeues automatically if it can resolve the outcome itself.
This queue-directory approach was chosen over a single growing markdown log file: moving a task file from queue/ to completed/ is an atomic claim, avoids partial-read/race conditions, and the JSON structure gives extensible fields (priority, type, target) without inventing ad hoc delimiter syntax.
Task (written by Architect to queue/000N.json):
{
"id": "42",
"author": "architect",
"priority": "high",
"type": "implementation",
"target": "force-app/main/default/...",
"instructions": [
"Add a Profile Completion section",
"Use existing flow X",
"Do not modify Y"
],
"created_at": "2026-07-23T10:00:00Z"
}Result (written by watcher to completed/000N.json):
{
"task_id": "42",
"status": "success | failure | needs_input",
"summary": "...",
"remarks": "present only when status = needs_input; Claude Code's question/options as text",
"commit_hash": "...",
"log_path": ".ai/logs/000N.log",
"completed_at": "2026-07-23T10:04:00Z"
}Three result states, not two:
success— task done, nothing pending.failure— Claude Code could not complete it (error, exhausted turns, denied action it needed).needs_input— Claude Code paused to ask a question, present options, or flag something worth a decision (e.g. a design choice, or remarks after an otherwise clean run).
Claude Code has an interactive picker tool (AskUserQuestion) for presenting multiple-choice options, but that's a TTY-only UI element. In headless mode (no TTY) it cannot render, and its undocumented fallback (auto-resolving with an empty answer) is not something to depend on. So:
- The Builder invocation explicitly disables it:
--disallowedTools AskUserQuestion. This forces Claude Code to fall back to writing any such choice out as plain text instead of relying on flaky auto-resolution. - Plain clarifying questions and remarks (e.g. "LSP is clean, but note X") already just land in the final response text — headless mode is one-shot (one prompt in, one result out, session ends), so a "question" simply becomes part of that text rather than blocking.
- Claude Code's own JSON output (
--output-format json) doesn't distinguish "finished cleanly" from "paused to ask something" — both come back assubtype: success,is_error: false. So CLAUDE.md instructs Claude Code to prefix any such moment with a fixed marker, e.g.NEEDS_CLARIFICATION: .... The watcher script greps for this marker to setstatus: needs_inputand populateremarks, rather than the Architect having to infer it from unstructured prose every time.
Constraint: an MCP server can't push a message into Claude Desktop on its own — Claude Desktop only runs tool calls while it's mid-turn, in response to something the user sent. So the loop is autonomous within a turn, not truly server-initiated.
Design: the check_builder_status MCP tool long-polls instead of returning instantly — it checks .ai/completed/ internally every ~2s for up to ~25s, returning "still building" if nothing has landed yet. The Architect's Project Instructions tell it to keep calling the tool again on "still building" rather than ending its turn.
Result: the user sends one message ("build X"), and Claude Desktop stays in that same turn — write task → poll → poll → poll → result arrives → act on it — with no further input required unless it hits something it can't resolve. This uses only standard MCP tool-calling behavior (no OS-level automation or notification hacks). Known limit to watch in practice: a long-running build means many sequential tool calls within one turn; if typical task durations turn out to exceed comfortable turn length, fall back to a simpler notification-based nudge instead.
Consistent for both failure and needs_input results:
- Architect reads the result and remarks.
- If it can resolve the issue itself with reasonable confidence (clear bug fix, an objectively better/conventional choice between options, a low-stakes/reversible decision) — it revises and requeues a new task automatically, without involving the user.
- If the issue genuinely requires the user's judgment (ambiguous product/business tradeoff, something irreversible, a permission denial that reflects a real safety boundary rather than a missing allowlist entry) — it stops and asks the user directly in the chat.
This mirrors "don't ask unless truly needed" — checkpoints are the exception, not the default.
Builder invocation:
claude -p "<task>" \
--permission-mode dontAsk \
--disallowedTools AskUserQuestion \
--max-turns 20 \
--max-budget-usd 2.00 \
--output-format json
--permission-mode dontAsk: anything not explicitly allowed is auto-denied and reported in the output — confirmed this does not hang waiting for input that will never come (headless sessions abort cleanly on repeated denials even without this flag;dontAskavoids hitting that path at all).--max-turns/--max-budget-usd: hard stops against runaway loops or cost.
.claude/settings.json, scoped to the project directory:
{
"permissions": {
"defaultMode": "dontAsk",
"allow": [
"Edit(<project-path>/**)",
"Write(<project-path>/**)",
"Bash(git add *)",
"Bash(git commit *)",
"Bash(git status)",
"Bash(git log -n *)",
"Read(/)"
],
"deny": [
"Bash(git push *)",
"Bash(curl *)",
"Bash(wget *)",
"Bash(sudo *)",
"Bash(rm -rf *)",
"Edit(/)",
"Edit(~/**)"
]
}
}Build/test commands (e.g. Bash(npm test), Salesforce CLI commands) get added to allow as they're identified — start narrow, extend deliberately rather than defaulting broad.
No built-in auto-commit in Claude Code — implemented via a Stop hook (fires whenever a headless session ends, regardless of outcome — success, failure, or paused for input):
# .claude/hooks/auto-commit.sh
if git status -s | grep -q .; then
git add -A
git commit -m "Claude Code checkpoint: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
fiBecause the hook fires unconditionally on session end and only acts if there are actual uncommitted changes, every run gets checkpointed the same way — including a run that paused to ask a needs_input question after making partial progress. No special-casing required. CLAUDE.md additionally instructs Claude Code to write a meaningful commit message describing what it did, as a first line of defense; the hook is the guaranteed fallback if that's skipped.
- Architect (Claude Desktop Project): plans features, maintains scope, writes tasks to
queue/, long-polls for results, resolves or escalates outcomes, is the only side the user talks to. - Builder (Claude Code, headless CLI): executes one task per invocation, self-corrects within its own turn budget, commits its own work, reports back via the result file — has no memory of past runs beyond what's in the task and the project's current file/git state.
- Deployment and environment promotion (sandbox vs. production). Claude Code only touches local project files; git history is the only "restore" mechanism needed at this stage.
- Any non-Salesforce project (e.g. the website hosting server connection) — the loop is being designed generically so it should carry over, but is being proven out on the Salesforce project first.
Per-task files (queue/, completed/) and per-run raw transcripts (logs/000N.log) persist, so the full history is technically reconstructable — but only by piecing together many files. Add one append-only log capturing the whole timeline in order, written to by whichever side takes the action:
.ai/logs/pipeline.jsonl — one JSON object per line:
{"ts": "2026-07-23T10:00:00Z", "actor": "architect", "event": "task_queued", "task_id": "42", "instructions": ["..."]}
{"ts": "2026-07-23T10:00:03Z", "actor": "builder", "event": "run_started", "task_id": "42"}
{"ts": "2026-07-23T10:04:12Z", "actor": "builder", "event": "run_completed", "task_id": "42", "status": "success", "commit_hash": "..."}Events to log at minimum: task_queued (Architect), run_started / run_completed (watcher, on behalf of Builder), and escalated_to_user (Architect, when it decides a failure/needs_input requires the user rather than resolving it itself). Greppable/tailable in real time, and gives a single place to answer "what has this pipeline actually done" without reconstructing it from scattered files.
Claude Code should keep README.md current as it makes changes. Relying on a CLAUDE.md instruction alone isn't enough to guarantee it happens — a general standing instruction is easy for the model to deprioritize deep in a task focused on the coding instructions themselves. Two things address this:
A narrow, append-friendly template, so edits are mechanical rather than open-ended rewrites (safer over many autonomous runs, less risk of the model clobbering existing content or drifting in tone):
# Project Name
## Overview
(stable — human-maintained; Claude Code should not rewrite this section)
## Architecture / Components
(stable-ish — update only when a task explicitly changes structure)
## Setup
(stable)
## Recent Changes
- 2026-07-23: Users can now complete their profile directly from the main screen, with progress shown as they fill it in.
- ...CLAUDE.md instructs Claude Code to append a dated entry to ## Recent Changes written in plain, user-facing language describing the capability or enhancement itself — what it does and why it matters — not the task's technical instructions or prompt text (those already live in queue//pipeline.jsonl, verbatim, for anyone who needs the exact wording). The README entry should read like a changelog a non-technical stakeholder could follow, not an implementation summary. Claude Code leaves the other sections alone unless a task explicitly asks it to update setup/architecture docs.
A Stop hook as the actual guarantee, not just the instruction. Claude Code's Stop hook can block the session from ending — returning {"decision": "block", "reason": "..."} feeds that reason back to Claude as its next instruction and forces it to keep working (still bounded by --max-turns, and Claude Code itself caps consecutive blocks per turn as a safety net against a broken hook looping forever). Concretely:
#!/bin/bash
input=$(cat)
[ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ] && exit 0
if git diff --name-only | grep -qv '^README\.md$' && ! git diff --name-only | grep -q '^README\.md$'; then
echo '{"decision":"block","reason":"You changed source files but README.md Recent Changes was not updated. Add a dated entry before finishing."}'
exit 0
fi
exit 0This runs before the auto-commit hook from §8, so the sequence on session end is: check README was touched (block and force a fix if not) → once satisfied, auto-commit runs and captures everything, README included, in the same restore-point commit.
There is no session/conversation ID that ties a Claude Desktop chat to a specific VS Code project, and Claude Desktop has no per-Project MCP scoping — claude_desktop_config.json registers servers globally; every conversation in every Desktop Project can see every configured server (this differs from Claude Code CLI, which does have project/user/local MCP scope, but that's a separate product).
The binding is therefore done by naming, not by platform isolation:
- Each project gets its own uniquely-named MCP server entry in
claude_desktop_config.json, withargspointing at that project's absolute path (so its.ai/queue,completed/,logs/are physically separate too):
{
"mcpServers": {
"architect_watcher_sf_ralph": {
"command": "node",
"args": ["/absolute/path/to/ralph-project/architect_watcher.js"]
}
}
}(A second project, e.g. the website, would later be added as architect_watcher_website pointing at its own path — same pattern, no changes needed to this design.)
- The tool this exposes is namespaced as
architect_watcher_sf_ralph__check_builder_status(etc.) when Claude calls it — this is the closest thing to an "API name" identifying which loop is which. - Each Claude Desktop Project's Project Instructions explicitly name the correct server for that project ("use
architect_watcher_sf_ralph__*for this loop; do not use any otherarchitect_watcher_*server"). This is enforced by instruction text, not a platform guarantee — worth being aware the isolation is a convention, not a hard boundary. In practice the blast radius of a mistake is small: at worst, a call lands in the wrong project's queue folder, since each server can only ever reach the one path it was configured with.
architect_watcher.js— MCP server exposingcheck_builder_status(long-poll) and the write-to-queue mechanism, read by Claude Desktop.- Watcher script (VS Code side) — detects new files in
queue/, invokes Claude Code headlessly per §7, writes results per §3–4, appends events topipeline.jsonlper §11, triggers the hooks. .claude/settings.json— permission allow/deny list per §7..claude/hooks/check-readme.sh(§12) +.claude/hooks/auto-commit.sh(§8), registered in that order.README.mdstarter template per §12.CLAUDE.md— Builder role instructions, including theNEEDS_CLARIFICATION:marker convention, the Recent Changes convention, and commit message expectations.- Claude Desktop Project Instructions — Architect role, queue-write behavior, long-poll loop instruction, escalation policy per §6.