Skip to content

About

Replace the manual copy-paste relay between Claude Desktop (Chat) and Claude Code (VS Code extension/CLI) with an automated loop.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

claude-automation-loop

Replace the manual copy-paste relay between Claude Desktop (Chat) and Claude Code (VS Code extension/CLI) with an automated loop.

Autonomous Claude Chat ↔ Claude Code Loop — Design Document

1. Goal

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.

2. High-level architecture

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 to logs/.
  • 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.

3. Task and result schema

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).

4. Why needs_input is reliable even though Claude Code doesn't expose it natively

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 as subtype: 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 set status: needs_input and populate remarks, rather than the Architect having to infer it from unstructured prose every time.

5. Wake / autonomy mechanism

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.

6. Escalation policy (failure and needs_input)

Consistent for both failure and needs_input results:

  1. Architect reads the result and remarks.
  2. 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.
  3. 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.

7. Permission model (headless Claude Code)

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; dontAsk avoids 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.

8. Restore points

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)"
fi

Because 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.

9. Roles

  • 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.

10. Explicitly out of scope (for now)

  • 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.

11. Unified event log

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.

12. README maintenance

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 0

This 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.

13. Binding a Claude Desktop Project to a VS Code project

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, with args pointing 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 other architect_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.

14. Components to be built next

  1. architect_watcher.js — MCP server exposing check_builder_status (long-poll) and the write-to-queue mechanism, read by Claude Desktop.
  2. Watcher script (VS Code side) — detects new files in queue/, invokes Claude Code headlessly per §7, writes results per §3–4, appends events to pipeline.jsonl per §11, triggers the hooks.
  3. .claude/settings.json — permission allow/deny list per §7.
  4. .claude/hooks/check-readme.sh (§12) + .claude/hooks/auto-commit.sh (§8), registered in that order.
  5. README.md starter template per §12.
  6. CLAUDE.md — Builder role instructions, including the NEEDS_CLARIFICATION: marker convention, the Recent Changes convention, and commit message expectations.
  7. Claude Desktop Project Instructions — Architect role, queue-write behavior, long-poll loop instruction, escalation policy per §6.

About

Replace the manual copy-paste relay between Claude Desktop (Chat) and Claude Code (VS Code extension/CLI) with an automated loop.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages