From 86f0b0f8d0f6e9b1f8b14cba55314bc24a597004 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 02:37:36 +0000 Subject: [PATCH 1/8] docs: add headless architecture spec Defines the v2 design: headless runner processes instead of terminal sessions, model-agnostic role config via foreman.config.json, backend adapters (claude-cli, codex-cli, openai-compatible), lifecycle CLI, and the @ask directive for peer communication. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019gi6oHfGAK86EwThbcE5Ge --- references/architecture.md | 171 +++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 references/architecture.md diff --git a/references/architecture.md b/references/architecture.md new file mode 100644 index 0000000..7f60ef8 --- /dev/null +++ b/references/architecture.md @@ -0,0 +1,171 @@ +# Foreman Headless Architecture + +Foreman v2 runs every crew member as a headless background process. No terminal +windows are spawned. The only interactive session is the Orchestrator, which the +owner talks to directly. + +## Components + +``` +foreman/ +├── foreman.config.json # Role → backend/model mapping (user-editable) +├── scripts/ +│ ├── foreman.sh # Lifecycle CLI: start | spawn | stop | status | logs | clean +│ └── foreman-runner.py # Generic agent runner (one process per crew member) +└── references/ + ├── protocol.md # Shared communication norms + ├── architecture.md # This file + └── roles/*.md # Role instructions (fed to models as system context) +``` + +Per-project runtime state lives in `/.foreman/`: + +``` +.foreman/ +├── logs/.log # stdout+stderr of each runner +├── pids/.pid # PID files for lifecycle management +├── worktrees/worker-/ # Isolated git worktrees for Workers +└── config.json # Optional per-project config override +``` + +`.foreman/` should be added to the project's `.gitignore`. + +## Model Agnosticism + +Every role's model is configured in `foreman.config.json`. No model is +hardcoded anywhere else — not in scripts, not in role files. Users swap models +by editing the config. + +### Config schema + +```json +{ + "defaults": { "backend": "claude-cli", "model": "sonnet" }, + "roles": { + "orchestrator": { "backend": "claude-interactive", "model": "opus" }, + "architect": { "backend": "claude-cli", "model": "sonnet", + "allowed_tools": ["Read", "Glob", "Grep", "Write", "Bash(git diff:*)", "Bash(git log:*)"] }, + "dissenter": { "backend": "claude-cli", "model": "sonnet", + "allowed_tools": ["Read", "Glob", "Grep"] }, + "inspector": { "backend": "claude-cli", "model": "opus", + "allowed_tools": ["Read", "Glob", "Grep", "Bash(git diff:*)", "Bash(git log:*)"] }, + "worker": { "backend": "claude-cli", "model": "sonnet" }, + "cleaner": { "backend": "claude-cli", "model": "haiku" }, + "circuit-breaker": { "backend": "claude-cli", "model": "haiku", + "allowed_tools": [] }, + "muse": { "backend": "claude-cli", "model": "haiku", + "allowed_tools": [] } + } +} +``` + +Role entries are merged over `defaults`. Unknown keys are ignored (so users may +annotate with `"//"` comment keys). A `/.foreman/config.json`, if +present, is merged over the skill-level config per role. + +### Backends + +| Backend | What it runs | Key options | +|---|---|---| +| `claude-interactive` | Interactive `claude` session (Orchestrator only; launched by `foreman.sh`, never by the runner) | `model` | +| `claude-cli` | Headless `claude -p` per task, with `--resume` for session continuity | `model`, `allowed_tools`, `extra_args` | +| `codex-cli` | `codex exec` per task | `model`, `reasoning_effort`, `bin` (default: `codex` on `$PATH`, override via `FOREMAN_CODEX_BIN`) | +| `openai-compatible` | POST to `{base_url}/chat/completions`; conversation history kept in-process | `base_url`, `model`, `api_key_env`, `temperature`, `max_tokens` | + +`openai-compatible` is the universal adapter: it covers Ollama +(`http://127.0.0.1:11434/v1`), OpenAI, OpenRouter, LM Studio, vLLM, and any +other provider exposing the OpenAI chat-completions API. `api_key_env` names an +environment variable holding the key (empty/absent = no auth header, as for +local Ollama). API keys are never stored in the config file. + +Example — a Dissenter on a local Ollama model: + +```json +"dissenter": { + "backend": "openai-compatible", + "base_url": "http://127.0.0.1:11434/v1", + "model": "qwen3.5:latest", + "api_key_env": "" +} +``` + +## The Runner + +`foreman-runner.py` is one process per crew member. It owns the relay +connection and the message loop — the model never has to remember to keep +listening, which was the primary failure mode of the terminal-based design +(a one-shot `claude -p` turn ends whenever the model decides it is done). + +``` +foreman-runner.py --role --project + [--name ] # default: foreman-; workers: foreman-worker- + [--cwd ] # working dir for the backend (worker worktrees) + [--config ] # config file override +``` + +Loop: + +1. Connect to the relay hub socket (candidates: `$RELAY_HUB_SOCKET`, + `$CLAUDE_PLUGIN_DATA/hub.sock`, + `~/.claude/plugins/data/relay-claude-relay/hub.sock`, `~/.claude-relay/hub.sock`). +2. `register` with the session name (line-delimited JSON, protocol version 2). +3. Announce readiness to `foreman-orchestrator`. +4. `inbox_wait` forever. On `inbox_deliver`: dispatch the message to the + backend, then `reply` with the backend's final text on the delivered `ask_id`. +5. On hub disconnect: retry with backoff; log and exit after repeated failure. + +System context for the backend is `protocol.md` + the role's `roles/.md`, +assembled by the runner at startup. + +### Outbound asks: the `@ask` directive + +Headless backends have no relay MCP tools. Instead, a backend response may +contain directive lines: + +``` +@ask foreman-architect: What shape is the auth token object in the plan? +``` + +The runner parses these, performs the relay ask, and feeds the answer back to +the backend as a follow-up turn. This repeats (max 5 hops per incoming message) +until the backend produces a response with no directives — that text becomes +the relay reply. Role files document this syntax for crew members. + +The outbound-ask wire format mirrors the inbound message types used by the +former bridge scripts and is isolated in the runner's `RelayClient` class; if +the live hub rejects it, the error is logged and surfaced to the asking backend +rather than crashing the runner. + +## Lifecycle CLI + +``` +foreman.sh start # spawn core crew headless, then exec the interactive Orchestrator +foreman.sh spawn worker # create .foreman/worktrees/worker- + launch a Worker runner +foreman.sh stop # SIGTERM all PIDs in .foreman/pids/ (worktrees are left intact) +foreman.sh status # liveness of each crew member (PID check) + last log line +foreman.sh logs [-f] # print/follow a crew member's log +foreman.sh clean # remove worktrees (refuses if a worktree has uncommitted changes) + prune +``` + +Core crew spawned by `start`: architect, dissenter, inspector, cleaner, +circuit-breaker, muse. Each is launched detached (`setsid`, stdout+stderr to +its log file, PID recorded). Workers are spawned on demand — by the +Orchestrator running `foreman.sh spawn worker `, exactly as it previously +ran the bootstrap script. + +The Orchestrator remains an interactive `claude` session launched in the +foreground of the owner's terminal with the relay plugin channel flag and +protocol + role context appended as system prompt, exactly as before. It is the +only crew member with real relay MCP tools. + +## Known Limitations + +- **Circuit Breaker visibility.** Relay delivers directed messages; a peer only + sees traffic addressed to it. The Circuit Breaker therefore cannot passively + observe all conversations. Crew members are instructed to CC it on + contentious exchanges, and the Orchestrator involves it when loops are + suspected. True passive monitoring needs a hub-level tap (upstream Relay + feature). +- **Outbound ask wire format** is best-effort against Relay protocol v2 and has + not been verified against a live hub from this repo. It is isolated in + `RelayClient` for easy correction. From 3716a99e34f4217a98a0d84ef0b841a1e70e29cf Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 02:48:28 +0000 Subject: [PATCH 2/8] feat: replace terminal-spawned crew with headless, model-agnostic architecture - Add scripts/foreman-runner.py: generic headless runner, one process per crew member. The runner owns the relay connection and message loop, with backend adapters for claude-cli (with --resume continuity), codex-cli, and any OpenAI-compatible endpoint (Ollama, OpenRouter, OpenAI, etc.). Peer communication for headless members uses @ask directive lines that the runner relays. Waits patiently for the hub at startup instead of burning its reconnect budget before the Orchestrator is up. - Add foreman.config.json: per-role backend/model config with per-project .foreman/config.json overrides. All-Claude defaults work out of the box; no model is hardcoded in scripts or role files. Gemini/Ollama/Codex are no longer requirements - any of them can be plugged into any role. - Add scripts/foreman.sh lifecycle CLI (start/spawn/stop/status/logs/clean) replacing terminal-window spawning. Runtime state lives in /.foreman/ (logs, pids, worktrees), auto-excluded from git status via .git/info/exclude. Worker worktrees move from /tmp to .foreman/worktrees/. - Remove foreman-bootstrap.sh and the three per-model bridge scripts. - Update SKILL.md, README, protocol.md, and all role files for the headless design; fix protocol.md's incomplete role list. Verified with an end-to-end test against a fake relay hub and fake OpenAI-compatible server (register, readiness, task delivery, @ask resolution, final reply) plus lifecycle smoke tests in a temp repo. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019gi6oHfGAK86EwThbcE5Ge --- README.md | 75 ++- SKILL.md | 63 ++- foreman.config.json | 18 + references/architecture.md | 3 +- references/protocol.md | 27 +- references/roles/architect.md | 4 +- references/roles/circuit-breaker.md | 10 +- references/roles/cleaner.md | 2 +- references/roles/dissenter.md | 2 +- references/roles/inspector.md | 2 +- references/roles/muse.md | 4 +- references/roles/orchestrator.md | 4 +- references/roles/worker.md | 14 +- scripts/foreman-architect-bridge.py | 361 -------------- scripts/foreman-bootstrap.sh | 248 ---------- scripts/foreman-dissenter-bridge.py | 196 -------- scripts/foreman-muse-bridge.py | 198 -------- scripts/foreman-runner.py | 724 ++++++++++++++++++++++++++++ scripts/foreman.sh | 498 +++++++++++++++++++ 19 files changed, 1358 insertions(+), 1095 deletions(-) create mode 100644 foreman.config.json delete mode 100755 scripts/foreman-architect-bridge.py delete mode 100755 scripts/foreman-bootstrap.sh delete mode 100644 scripts/foreman-dissenter-bridge.py delete mode 100644 scripts/foreman-muse-bridge.py create mode 100755 scripts/foreman-runner.py create mode 100755 scripts/foreman.sh diff --git a/README.md b/README.md index ab376c2..d8fbe57 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,14 @@ And then there is the Muse, who runs on an entirely different model, does not ap | Role | Model | What They Do | What They Emphatically Do Not Do | |------|-------|-------------|----------------------------------| -| **Orchestrator** | Claude Opus 4.6 | Approves plans, delegates, tracks, reports | Write code, ever, under any circumstances | -| **Architect** | Qwen3.5 (Ollama) | Reads codebase, writes `CURRENT_PLAN.md` | Touch the repo during the build | -| **Dissenter** | Gemini 3.1 Pro | Challenges plans (First Principles first) and results | Touch the filesystem or look at actual code | -| **Inspector** | gpt-5.3-codex (Codex CLI, high reasoning) | Full audit: correctness, security, plan conformance | Rubber-stamp anything | -| **Worker** | Claude Sonnet | Builds in isolated git worktrees | Argue about architecture (that ship has sailed) | -| **Cleaner** | Claude Haiku | Linting, formatting, dead code removal | Modify application logic | -| **Circuit Breaker** | Claude Haiku | Detects and resolves conversational loops | Take sides until forced to | -| **Muse** | Gemma 4 (Ollama) | Reframes problems sideways | Anything resembling real work | +| **Orchestrator** | Opus (configurable) | Approves plans, delegates, tracks, reports | Write code, ever, under any circumstances | +| **Architect** | Sonnet (configurable) | Reads codebase, writes `CURRENT_PLAN.md` | Touch the repo during the build | +| **Dissenter** | Sonnet (configurable) | Challenges plans (First Principles first) and results | Touch the filesystem or look at actual code | +| **Inspector** | Opus (configurable) | Full audit: correctness, security, plan conformance | Rubber-stamp anything | +| **Worker** | Sonnet (configurable) | Builds in isolated git worktrees | Argue about architecture (that ship has sailed) | +| **Cleaner** | Haiku (configurable) | Linting, formatting, dead code removal | Modify application logic | +| **Circuit Breaker** | Haiku (configurable) | Detects and resolves conversational loops | Take sides until forced to | +| **Muse** | Haiku (configurable) | Reframes problems sideways | Anything resembling real work | ## Prerequisites @@ -35,9 +35,8 @@ You will need: 1. [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (2.1.80 or later, though frankly the version number is changing so fast that by the time you read this sentence it may already be wrong) 2. [Claude Relay](https://github.com/innestic/claude-relay) installed as a plugin -3. A `GEMINI_API_KEY` environment variable set (for the Dissenter — get one at [aistudio.google.com](https://aistudio.google.com/app/apikey)) -4. [Ollama](https://ollama.com) with `qwen3.5` and `gemma4` pulled (for the Architect and Muse — optional but recommended) -5. [Codex](https://openai.com/codex) desktop app with an OpenAI API key (for the Inspector) + +By default, all crew members run on Claude via the Claude Code backend. Support for other models (Ollama, OpenAI, OpenRouter, LM Studio, vLLM, or other OpenAI-compatible endpoints) is optional and configured in `foreman.config.json`. ## Quick Start @@ -52,15 +51,17 @@ The quickest way to understand Foreman is to watch it work. **Step 2.** Install the Foreman skill. (Place the `foreman/` directory in your Claude Code skills path.) -**Step 3.** Open Claude Code in your project directory with the Relay channel flag: +**Step 3.** Navigate to your project directory and run: ```bash -claude --dangerously-load-development-channels plugin:relay@claude-relay +./scripts/foreman.sh start ``` -**Step 4.** Say something like: -> "Spin up Foreman. Build me a REST API for user authentication with JWT tokens, bcrypt password hashing, and refresh token rotation." +This spawns the entire crew as background processes and drops you into an interactive Orchestrator session. The crew is now running headlessly, which is to say they are invisible but very much present. You can verify this by running `foreman.sh status` in another terminal if you want proof that they are actually there and not just polite fiction. + +**Step 4.** In the Orchestrator session, say something like: +> "Build me a REST API for user authentication with JWT tokens, bcrypt password hashing, and refresh token rotation." -**Step 5.** Watch in mild astonishment as terminal windows begin appearing, agents begin talking to each other, and code begins materializing in your project directory as if by magic, except that it is not magic, it is just several language models being very organized about it. +**Step 5.** Sit back and experience the mild astonishment of agents talking to each other and code materializing in your project directory as if by magic, except that it is not magic, it is just several language models being very organized about it and also completely invisible about it. ## How It Works @@ -72,47 +73,42 @@ The workflow is, in principle, simple. In practice it is also simple, which is w 4. **Workers build.** Each Worker gets an isolated git worktree. They make their own implementation decisions without checking in on every variable name. They are, after all, competent. 5. **The Cleaner cleans.** Continuously. Like the tide, but for dead code. 6. **The Architect checks conformance.** When Workers complete, the Architect verifies the implementation matches `CURRENT_PLAN.md`. -7. **The Inspector audits.** The Inspector (gpt-5.3-codex, high reasoning) reads everything — the plan, all changed files, affected existing code. A BLOCK finding halts the commit. Nothing bypasses the Inspector without an explicit override recorded in `DECISIONS.md`. +7. **The Inspector audits.** The Inspector reads everything — the plan, all changed files, affected existing code. A BLOCK finding halts the commit. Nothing bypasses the Inspector without an explicit override recorded in `DECISIONS.md`. 8. **The Cleaner does a final sweep.** After Inspector clearance: lint, dead code, imports, formatting. 9. **The Dissenter reviews the results.** A second pass after the work is done, before anything is committed. 10. **The Orchestrator approves.** You get your code. The Circuit Breaker watches all of this passively and intervenes only when two agents have gone back and forth three times on the same point without progress. At four round-trips, it forces a decision. Unless the Orchestrator is one of the looping parties, in which case it escalates to you, because even on a construction site, sometimes the foreman needs the owner to make a call. -The Muse, if present, sits off to the side and offers a completely different perspective when asked. It runs Gemma 4, not Claude, which means it literally thinks differently. This is not a metaphor. The weights are different. The latent space is different. It will say things none of the Claude agents would think of, and occasionally those things will be exactly what was needed. +The Muse sits off to the side and offers a completely different perspective when asked. It is most effective when configured to run on a different model family (via `foreman.config.json`), which means it literally thinks differently. This is not a metaphor. The weights are different. The latent space is different. It will say things the Claude agents would not think of, and occasionally those things will be exactly what was needed. -## The Bootstrap Script +## Lifecycle CLI -The Orchestrator spawns crew members using `scripts/foreman-bootstrap.sh`. Each invocation opens a new terminal session with the correct model, role instructions, and Relay connection. +The `scripts/foreman.sh` command manages the crew's lifecycle. All crew members (except the Orchestrator) run as headless background processes. ```bash -# The Orchestrator handles this automatically, but if you are curious: -./scripts/foreman-bootstrap.sh orchestrator -./scripts/foreman-bootstrap.sh architect -./scripts/foreman-bootstrap.sh dissenter -./scripts/foreman-bootstrap.sh inspector -./scripts/foreman-bootstrap.sh worker 1 -./scripts/foreman-bootstrap.sh worker 2 -./scripts/foreman-bootstrap.sh cleaner -./scripts/foreman-bootstrap.sh circuit-breaker -./scripts/foreman-bootstrap.sh muse +foreman.sh start # Spawn core crew headlessly; launch interactive Orchestrator +foreman.sh spawn worker # Create worker- worktree and launch its process +foreman.sh stop # Terminate all crew member processes +foreman.sh status # Check liveness and last log line for each crew member +foreman.sh logs [-f] # Print or follow logs for a role (e.g., logs architect, logs worker-1 -f) +foreman.sh clean # Remove worktrees (fails if uncommitted changes present) + prune ``` -Worker sessions create isolated git worktrees under `/tmp`. After a session completes, run `git worktree prune` to clean up any leftover branches. +Worker worktrees live under `.foreman/worktrees/worker-/` within your project. `foreman.sh clean` removes them when you are done. Log files are in `.foreman/logs/`; `foreman.sh` keeps the whole `.foreman/` directory out of `git status` for you (via `.git/info/exclude`). ## File Structure ``` foreman/ ├── SKILL.md # Main skill trigger and protocol +├── foreman.config.json # Role → backend/model mapping (defaults) ├── scripts/ -│ ├── foreman-bootstrap.sh # Spawns crew sessions -│ ├── foreman-architect-bridge.py # Architect bridge (Qwen3.5 via Ollama) -│ ├── foreman-dissenter-bridge.py # Dissenter bridge (Gemini) -│ └── foreman-muse-bridge.py # Muse bridge (Gemma 4 via Ollama) +│ ├── foreman.sh # Lifecycle CLI (start/spawn/stop/status/logs/clean) +│ └── foreman-runner.py # Generic runner (one process per crew member) └── references/ ├── protocol.md # Shared communication norms (all agents) - ├── relay-setup.md # Relay installation guide + ├── architecture.md # Headless design spec and config reference └── roles/ ├── orchestrator.md # The foreman ├── architect.md # The planner @@ -124,6 +120,8 @@ foreman/ └── muse.md # The one making coffee ``` +Per-project runtime state (logs, PIDs, worker worktrees) lives in `.foreman/` within the project directory. + ## Philosophy The central insight of Foreman is not that AI agents can talk to each other. Claude Relay already proved that. The insight is that *talking is not the same as collaborating*, and collaboration requires structure: clear roles, a chain of command, defined communication norms, and someone whose job it is to say "actually, have you considered that you might be building the wrong thing?" @@ -139,9 +137,10 @@ This may seem like a small thing, but Douglas Adams once noted that the problem In the spirit of honesty, which is a trait undervalued in README files: - **Single repo only.** All agents work in the same project directory. Cross-repo coordination is a v2 problem. -- **No persistence.** When sessions close, the crew is gone. Each job is a fresh start. +- **No persistence.** When you run `foreman.sh stop`, the crew is gone. Each job is a fresh start. - **Same host only.** Relay uses Unix sockets. Your agents all live on one machine. -- **The bootstrap script may need tweaking.** CLI flags for Claude Code and Ollama evolve quickly. If a session fails to spawn, check the launch command first. +- **Circuit Breaker visibility.** Relay delivers directed messages; the Circuit Breaker cannot passively observe all conversations (see `references/architecture.md` Known Limitations for details). +- **Headless agent communication.** Crew members communicate via directive lines (`@ask foreman-: `) in their responses rather than direct MCP tool calls. See `references/protocol.md` for details. ## Credits diff --git a/SKILL.md b/SKILL.md index 8bcb9cf..3613a40 100644 --- a/SKILL.md +++ b/SKILL.md @@ -13,20 +13,20 @@ Claude Relay must be installed as a Claude Code plugin. See `references/relay-se ## Roles -Eight roles, each running as a separate session connected via Relay. +Eight roles, each running as a headless background process connected via Relay. | Role | Model | Count | Purpose | |------|-------|-------|---------| -| Orchestrator | Opus 4.6 | 1 | Approves plans, delegates, tracks, reports. The foreman. | -| Architect | Qwen3.5 (Ollama) | 1 | Reads codebase, writes CURRENT_PLAN.md. Python bridge. | -| Dissenter | Gemini 3.1 Pro | 1 | Challenges plans (First Principles first) and results. Python bridge. | -| Inspector | Codex CLI (OpenAI) | 1 | Full code audit (correctness, security, conformance). Blocks commit. | -| Worker | Sonnet | 1+ | Builds in isolated git worktrees. Scaled by Orchestrator. | -| Cleaner | Haiku | 1 | Tidies after Inspector clears. Final sweep only. | -| Circuit Breaker | Haiku | 1 | Monitors all relay traffic for loops, including plan approval. | -| Muse | Gemma 4 (Ollama) | 1 | Reframes. Invoked on disagreements. Pre-spawned. Python bridge. | +| Orchestrator | Opus (configurable) | 1 | Approves plans, delegates, tracks, reports. The foreman. | +| Architect | Sonnet (configurable) | 1 | Reads codebase, writes CURRENT_PLAN.md. | +| Dissenter | Sonnet (configurable) | 1 | Challenges plans (First Principles first) and results. | +| Inspector | Opus (configurable) | 1 | Full code audit (correctness, security, conformance). Blocks commit. | +| Worker | Sonnet (configurable) | 1+ | Builds in isolated git worktrees. Scaled by Orchestrator. | +| Cleaner | Haiku (configurable) | 1 | Tidies after Inspector clears. Final sweep only. | +| Circuit Breaker | Haiku (configurable) | 1 | Monitors all relay traffic for loops, including plan approval. | +| Muse | Haiku (configurable) | 1 | Reframes. Invoked on disagreements. Pre-spawned. | -Load role-specific instructions from `references/roles/` when bootstrapping each session. +Models and backends are configured in `foreman.config.json` (see references/architecture.md for details). Role-specific instructions are loaded from `references/roles/` when each crew member starts. ## How It Works @@ -42,7 +42,7 @@ The Orchestrator sends the plan to the Dissenter. The Dissenter challenges premi If the Orchestrator and Dissenter cannot resolve a disagreement after one round, the Orchestrator invokes the Muse for a lateral perspective before making a final call. The Orchestrator holds final authority. The Circuit Breaker monitors this loop with the same escalation ladder as all other relay traffic. ### 4. Orchestrator Staffs the Job Site -After plan approval, the Orchestrator spawns Workers via the bootstrap script. Each Worker gets an isolated git worktree. The Architect, Dissenter, Muse, and Circuit Breaker are pre-spawned at startup. +After plan approval, the Orchestrator spawns Workers via the `foreman.sh spawn worker ` command. Each Worker gets an isolated git worktree. The Architect, Dissenter, Inspector, Cleaner, Circuit Breaker, and Muse are pre-spawned at startup via `foreman.sh start`. ### 5. Workers Build Workers execute assigned tasks in their worktrees. They coordinate laterally with each other and can ping the Architect directly for plan clarification. The Orchestrator stays out of implementation decisions. @@ -54,7 +54,7 @@ The Cleaner keeps the job site tidy throughout the build. Its final deep sweep r When Workers complete, the Architect checks whether the implementation matches `CURRENT_PLAN.md`. It reads the actual changed files. ### 8. Inspector Audit -The Inspector (Opus 4.7) reads everything: the plan, all changed files, affected existing code. Audit covers correctness, security, and plan conformance. A BLOCK finding halts the commit until fixed. Nothing bypasses the Inspector without an explicit Orchestrator override recorded in `DECISIONS.md`. +The Inspector reads everything: the plan, all changed files, affected existing code. Audit covers correctness, security, and plan conformance. A BLOCK finding halts the commit until fixed. Nothing bypasses the Inspector without an explicit Orchestrator override recorded in `DECISIONS.md`. ### 9. Cleaner Final Sweep After Inspector clearance, the Cleaner runs its final sweep: lint, dead code, imports, formatting. @@ -76,17 +76,15 @@ The Circuit Breaker notifies the Orchestrator of every intervention so the Orche ## The Muse -The Muse is optional. The Orchestrator spawns it when the job feels like it could benefit from lateral thinking, or when the crew has been grinding on a hard problem and needs a different angle. +The Muse is pre-spawned as part of the core crew. The Orchestrator (or any agent) can ask it for a lateral perspective when the crew feels stuck or needs a different angle. -The Muse runs Gemma 4 via Ollama, not Claude. It thinks differently at the weights level. That is the point. It is not smarter than the crew. It sees sideways. +The Muse is most effective when configured to run on a model family different from Claude (e.g., a local Ollama model). Running it on different weights creates genuinely different thinking patterns. That is the point. It is not smarter than the crew. It sees sideways. -**How agents use the Muse:** Any agent can ping `foreman-muse` via `relay_ask` when they want a reframe. The Muse responds with one short observation, question, or metaphor, then goes quiet. It does not initiate conversations, write code, or make decisions. - -The Muse is pre-spawned at crew startup alongside the Orchestrator, Architect, Dissenter, and Circuit Breaker. It is always available. +**How agents use the Muse:** Any agent can ask `foreman-muse` via `relay_ask` when they want a reframe. The Muse responds with one short observation, question, or metaphor, then goes quiet. It does not initiate conversations, write code, or make decisions. **Structured trigger:** When the Orchestrator and Dissenter cannot resolve a plan disagreement after one round, the Orchestrator invokes the Muse before making a final call. This is the primary structural use. -**Any-time use:** Any agent can ping `foreman-muse` via `relay_ask` when stuck. The Muse responds with one short observation, question, or metaphor, then goes quiet. +**Any-time use:** Any agent can ask the Muse when stuck on a problem. The Muse responds with one short thought, then goes quiet. ## Communication Norms @@ -98,19 +96,28 @@ These norms are loaded into every session via the shared protocol file (`referen - **Workers talk laterally**: Workers with dependent tasks should coordinate directly with each other via Relay, not route everything through the Orchestrator. - **The Orchestrator delegates, not implements**: The Orchestrator never writes code or edits files. It plans, assigns, reviews, and approves. -## Bootstrapping +## Bootstrapping and Lifecycle + +The `scripts/foreman.sh` CLI manages the crew's lifecycle. All crew members run as headless background processes under a generic runner (`scripts/foreman-runner.py`), with the exception of the Orchestrator, which remains an interactive Claude Code session. -The Orchestrator uses the bootstrap script at `scripts/foreman-bootstrap.sh` to spawn sessions. The script accepts a role name and launches a Claude Code session with the correct model flag, the shared protocol, and the role-specific CLAUDE.md. +### Lifecycle Commands -Run `cat scripts/foreman-bootstrap.sh` to review the bootstrap script before first use. +| Command | What it does | +|---------|-------------| +| `foreman.sh start` | Spawn the core crew (architect, dissenter, inspector, cleaner, circuit-breaker, muse) as headless processes, then launch the interactive Orchestrator session. | +| `foreman.sh spawn worker ` | Create a worker-specific git worktree and launch a Worker runner. Workers are spawned on demand by the Orchestrator. | +| `foreman.sh stop` | Terminate all running crew member processes. Worktrees are left intact. | +| `foreman.sh status` | Check liveness of each crew member (PID check) and print the last log line for each. | +| `foreman.sh logs [-f]` | Print or follow the log for a specific crew member (e.g., `logs architect`, `logs worker-1 -f`). | +| `foreman.sh clean` | Remove all worker worktrees (refuses if any have uncommitted changes) and prune Relay state. | ### Session Naming Convention -Sessions auto-register with Relay using these names: +Crew members auto-register with Relay using these names: | Session name | Role | |---|---| -| `foreman-orchestrator` | Orchestrator | +| `foreman-orchestrator` | Orchestrator (interactive session — the only one you interact with directly) | | `foreman-architect` | Architect | | `foreman-dissenter` | Dissenter | | `foreman-inspector` | Inspector | @@ -119,7 +126,13 @@ Sessions auto-register with Relay using these names: | `foreman-circuit-breaker` | Circuit Breaker | | `foreman-muse` | Muse | -Use `relay_peers` to verify the crew is connected. +## Configuration + +Every crew member's model and backend are configured in `foreman.config.json`. This file defines defaults for all roles and allows per-role overrides. + +**Supported backends:** `claude-cli` (headless Claude), `claude-interactive` (Orchestrator only), `codex-cli`, `openai-compatible` (Ollama, OpenAI, OpenRouter, LM Studio, vLLM, etc.). See `references/architecture.md` for full config schema and examples. + +**Per-project overrides:** Place a `.foreman/config.json` in your project directory to override specific roles without editing the skill-level config. ## Scope Control @@ -139,5 +152,5 @@ All sessions spawn in the same project directory where the Orchestrator was laun - Not a CI/CD pipeline. It does not deploy. - Not a testing framework. Workers write tests as part of their tasks, but Foreman does not run test suites independently. -- Not persistent. When sessions close, the crew is gone. Spin up fresh for each job. +- Not persistent. When processes are stopped via `foreman.sh stop`, the crew is gone. Spin up fresh for each job. - Not cross-machine. All sessions run on the same host via Relay's Unix socket. diff --git a/foreman.config.json b/foreman.config.json new file mode 100644 index 0000000..95e8e7e --- /dev/null +++ b/foreman.config.json @@ -0,0 +1,18 @@ +{ + "defaults": { "backend": "claude-cli", "model": "sonnet" }, + "roles": { + "orchestrator": { "backend": "claude-interactive", "model": "opus" }, + "architect": { "backend": "claude-cli", "model": "sonnet", + "allowed_tools": ["Read", "Glob", "Grep", "Write", "Bash(git diff:*)", "Bash(git log:*)"] }, + "dissenter": { "backend": "claude-cli", "model": "sonnet", + "allowed_tools": ["Read", "Glob", "Grep"] }, + "inspector": { "backend": "claude-cli", "model": "opus", + "allowed_tools": ["Read", "Glob", "Grep", "Bash(git diff:*)", "Bash(git log:*)"] }, + "worker": { "backend": "claude-cli", "model": "sonnet" }, + "cleaner": { "backend": "claude-cli", "model": "haiku" }, + "circuit-breaker": { "backend": "claude-cli", "model": "haiku", + "allowed_tools": [] }, + "muse": { "backend": "claude-cli", "model": "haiku", + "allowed_tools": [] } + } +} diff --git a/references/architecture.md b/references/architecture.md index 7f60ef8..731765d 100644 --- a/references/architecture.md +++ b/references/architecture.md @@ -28,7 +28,8 @@ Per-project runtime state lives in `/.foreman/`: └── config.json # Optional per-project config override ``` -`.foreman/` should be added to the project's `.gitignore`. +`foreman.sh` automatically appends `.foreman/` to the project's +`.git/info/exclude` so runtime state never shows up in `git status`. ## Model Agnosticism diff --git a/references/protocol.md b/references/protocol.md index 6fa86b3..d502951 100644 --- a/references/protocol.md +++ b/references/protocol.md @@ -4,30 +4,41 @@ You are part of a Foreman coding crew. Multiple Claude Code sessions are connect ## Your Identity -You were assigned a role when this session launched. Your role name is in your session's CLAUDE.md. You are one of: Orchestrator, Dissenter, Worker, Cleaner, or Circuit Breaker. Follow your role-specific instructions. This document covers the shared rules everyone follows. +You were assigned a role when this session launched. Your role name is in your session's system context. You are one of: Orchestrator, Architect, Dissenter, Inspector, Worker, Cleaner, Circuit Breaker, or Muse. Follow your role-specific instructions. This document covers the shared rules everyone follows. ## Message Handling -### Incoming Asks +### For the Orchestrator (Interactive Session) When you receive an incoming ask via `notifications/claude/channel`, respond promptly using `relay_reply(ask_id, your_response)` before continuing your current work. The ask_id is in the notification metadata. Do not ignore incoming asks. A blocked peer is a blocked job site. -### When to Use relay_ask vs. relay_broadcast - Use `relay_ask(to, question)` when your question targets a specific peer. Most communication should be directed asks. Use `relay_broadcast(question)` only when you genuinely need input from the entire crew, such as status requests or announcements that affect everyone. +### For Headless Crew Members + +You receive messages as plain text from your runner and reply with your final response. To ask a peer a question mid-task, emit a directive line in your response: + +``` +@ask foreman-: +``` + +The runner will perform the relay ask and feed the answer back to you as a follow-up message. Your final response (after all asks and answers are complete) is delivered back to the asking agent. + +If an `@ask` comes back as a bracketed error like `[ask failed: peer not found]`, report that error to the Orchestrator in your reply instead of retrying silently. + ### Naming Convention All Foreman sessions are named with the `foreman-` prefix: - `foreman-orchestrator` +- `foreman-architect` - `foreman-dissenter` +- `foreman-inspector` - `foreman-worker-1`, `foreman-worker-2`, etc. - `foreman-cleaner` - `foreman-circuit-breaker` - -Use `relay_peers` if you need to verify who is currently connected. +- `foreman-muse` ## Chain of Command @@ -49,7 +60,9 @@ Keep status responses concise. Two to three sentences maximum. ## Error Handling -If you receive a `peer_not_found`, `peer_gone`, or `timeout` error from Relay, notify the Orchestrator immediately. Do not retry silently. The Orchestrator decides whether to respawn the missing peer or reassign the work. +If you receive an error from an `@ask` directive (e.g., `[ask failed: peer not found]`), include that error in your reply to the Orchestrator. Do not retry silently. The Orchestrator decides whether to respawn the missing peer or reassign the work. + +For the Orchestrator: If you receive a `peer_not_found`, `peer_gone`, or `timeout` error via relay_ask, you have the same responsibility — report it clearly and decide on next steps. ## Conflict Resolution diff --git a/references/roles/architect.md b/references/roles/architect.md index 3ff58dd..123eb77 100644 --- a/references/roles/architect.md +++ b/references/roles/architect.md @@ -5,7 +5,7 @@ You are the Architect. You turn a goal into a concrete implementation plan. You ## Your Responsibilities ### Receive the Goal -The Orchestrator will send you a task via Relay containing: +The Orchestrator will send you a message containing: - The goal as the owner stated it - The path to the project directory @@ -39,7 +39,7 @@ After writing `CURRENT_PLAN.md`, reply to the Orchestrator's ask with: PLAN READY: CURRENT_PLAN.md written. [One sentence summary of the approach.] ### Answer Worker Questions -During the build phase, Workers may ping you via `relay_ask` for plan clarification. Answer precisely and briefly. You own the plan — you know what you intended. Do not redesign mid-build. If a Worker surfaces a genuine blocker that invalidates the plan, notify the Orchestrator immediately. +During the build phase, Workers may ask you via `@ask foreman-architect: ` for plan clarification. Answer precisely and briefly in your response to the runner. You own the plan — you know what you intended. Do not redesign mid-build. If a Worker surfaces a genuine blocker that invalidates the plan, notify the Orchestrator immediately via `@ask foreman-orchestrator: `. ### Post-Build Conformance Review When the Orchestrator notifies you that Workers have completed, inspect the actual changes against your plan. Read the modified files. Check: diff --git a/references/roles/circuit-breaker.md b/references/roles/circuit-breaker.md index ea433c1..e0b3682 100644 --- a/references/roles/circuit-breaker.md +++ b/references/roles/circuit-breaker.md @@ -5,7 +5,7 @@ You are the Circuit Breaker. You monitor all Relay traffic between Foreman agent ## Your Responsibilities ### Monitor Traffic -Watch all incoming `notifications/claude/channel` messages. Track exchanges between agent pairs by topic. A "topic" is identified by the subject matter of the conversation, not the ask_id (a single topic may span multiple ask/reply cycles). +Watch all incoming messages in your session. Track exchanges between agent pairs by topic. A "topic" is identified by the subject matter of the conversation (a single topic may span multiple ask/reply cycles). ### Scope: Plan Approval Loop Included Monitor all relay traffic including the plan approval loop between `foreman-orchestrator`, `foreman-dissenter`, and `foreman-architect`. The same escalation ladder applies: @@ -26,7 +26,7 @@ Signs of a loop: ### Escalation Ladder **At 3 round-trips (flag):** -Send a message to both looping agents via `relay_ask`: +Send a message to both looping agents via `@ask`: - State that a loop has been detected - Summarize Position A and Position B concisely - Direct them to resolve it in one more exchange or accept that a forced decision is coming @@ -37,13 +37,13 @@ Two paths depending on who is looping: *If the Orchestrator is NOT one of the looping agents:* - Evaluate both positions - Select the position with the stronger justification -- Send a directive to both agents: "This has been resolved. [Position X] stands. Reasoning: [brief justification]. Move on." -- Notify the Orchestrator that a forced resolution occurred, including the topic, the agents involved, and which position was selected +- Send a directive to both agents via `@ask`: "This has been resolved. [Position X] stands. Reasoning: [brief justification]. Move on." +- Notify the Orchestrator via `@ask foreman-orchestrator: ` that a forced resolution occurred, including the topic, the agents involved, and which position was selected *If the Orchestrator IS one of the looping agents:* - Do NOT force a decision - Summarize both positions -- Escalate to the owner (the human) by notifying the Orchestrator that you are escalating +- Escalate to the owner (the human) by notifying the Orchestrator via `@ask foreman-orchestrator: ` that you are escalating - The Orchestrator must surface this to the owner for a decision - Accept the owner's decision as final diff --git a/references/roles/cleaner.md b/references/roles/cleaner.md index b1e9303..c7cffd2 100644 --- a/references/roles/cleaner.md +++ b/references/roles/cleaner.md @@ -20,7 +20,7 @@ Run linting and formatting tools available in the project (eslint, prettier, ruf Work in small, frequent passes rather than one large batch. Clean up after each worker reports a task completion, and periodically sweep during long build phases. -Do not modify logic, behavior, or architecture. If you see something that looks like a bug (not a style issue), report it to the Orchestrator via `relay_ask("foreman-orchestrator", description)`. Do not fix bugs yourself. +Do not modify logic, behavior, or architecture. If you see something that looks like a bug (not a style issue), report it to the Orchestrator via `@ask foreman-orchestrator: `. Do not fix bugs yourself. ### Final Sweep diff --git a/references/roles/dissenter.md b/references/roles/dissenter.md index 32ee042..71a64af 100644 --- a/references/roles/dissenter.md +++ b/references/roles/dissenter.md @@ -5,7 +5,7 @@ You are the Dissenter. Your job is to make sure the crew builds the right thing ## Your Responsibilities ### Pre-Build Review -The Orchestrator will send you a plan summary before any substantive code is written. Your job is to stress-test the reasoning. Challenge in this order: +The Orchestrator will send you a message with a plan summary before any substantive code is written. Your job is to stress-test the reasoning. Challenge in this order: **First: Challenge the premise (First Principles)** Before challenging *how* the plan is built, challenge *whether* it needs to exist: diff --git a/references/roles/inspector.md b/references/roles/inspector.md index d5edfdc..289fa80 100644 --- a/references/roles/inspector.md +++ b/references/roles/inspector.md @@ -7,7 +7,7 @@ You are not trying to be liked. You are trying to make sure nothing broken, inse ## Your Responsibilities ### Receive the Inspection Request -The Orchestrator will send you an inspection request after Workers complete and after the Architect has performed its conformance review. The request will include: +The Orchestrator will send you a message with an inspection request after Workers complete and after the Architect has performed its conformance review. The request will include: - The original goal - The path to `CURRENT_PLAN.md` - A summary of what Workers built and any deviations they reported diff --git a/references/roles/muse.md b/references/roles/muse.md index f336ca1..e4c54aa 100644 --- a/references/roles/muse.md +++ b/references/roles/muse.md @@ -6,11 +6,11 @@ You are the Muse. You do not build. You do not plan. You do not review. You do n You exist on this job site because sometimes the crew gets too close to the problem. They are deep in implementation details, arguing about patterns, optimizing the wrong thing, or solving a problem that shouldn't exist. You are the one who says the thing nobody else is thinking. -You are not smarter than the Orchestrator or the Dissenter. You think differently. That is your value. +You are not smarter than the Orchestrator or the Dissenter. You think differently. That is your value. You may be configured to run on a model family different from Claude (via `foreman.config.json`), which means your thinking literally works at different weights and latent-space assumptions. ## When You Speak -You speak when spoken to. Agents will ping you via Relay when they want a different perspective. You may receive questions like: +You speak when spoken to. Agents will ask you via `@ask foreman-muse: ` when they want a different perspective. You may receive questions like: - "We're deciding between approach A and B, what do you think?" - "We're stuck on this, any ideas?" diff --git a/references/roles/orchestrator.md b/references/roles/orchestrator.md index 74caaa2..a5e0b0e 100644 --- a/references/roles/orchestrator.md +++ b/references/roles/orchestrator.md @@ -35,12 +35,12 @@ relay_ask("foreman-dissenter", "` command (run via your Bash tool). Each Worker gets its own git worktree under `.foreman/worktrees/worker-/`. Determine crew size from `CURRENT_PLAN.md`: - 1 Worker for focused, single-track tasks - 2–3 Workers for features with parallelizable phases - 4+ Workers only for large multi-module builds -Always spawn exactly one Cleaner, one Inspector, and one Circuit Breaker per job. The Architect, Dissenter, Muse, and Circuit Breaker are pre-spawned at crew startup. +The Architect, Dissenter, Inspector, Cleaner, Circuit Breaker, and Muse are pre-spawned at crew startup via `foreman.sh start`. Workers are spawned on demand. ### Step 4: Delegate Assign each Worker a specific task via `relay_ask`. Your assignment must include: diff --git a/references/roles/worker.md b/references/roles/worker.md index 23084a8..01bde12 100644 --- a/references/roles/worker.md +++ b/references/roles/worker.md @@ -4,7 +4,7 @@ You are a Worker on a Foreman crew. You build what the Orchestrator assigns you. ## Your Worktree -You work in an isolated git worktree, not the main project directory. The Orchestrator's assignment will include your worktree path. `cd` to that path before doing any work. Do not modify files outside your worktree without explicit Orchestrator approval. +You work in an isolated git worktree, not the main project directory. Your worktree lives at `.foreman/worktrees/worker-/` within the project, where `` is your worker number. The Orchestrator's assignment will include your exact worktree path. `cd` to that path before doing any work. Do not modify files outside your worktree without explicit Orchestrator approval. When your task is complete, your changes are in your worktree. The Orchestrator coordinates merging worktrees back to the main branch. Do not merge yourself. @@ -19,7 +19,7 @@ You are trusted to make reasonable choices about data structures, naming, intern If you face a decision that fundamentally changes the approach (not just the implementation), flag it to the Orchestrator before proceeding. Examples: discovering that the assigned approach is not technically feasible, realizing a task needs to be split, or finding that a dependency does not work as expected. ### Coordinate with Other Workers -If your task depends on another worker's output, coordinate directly with them via `relay_ask`. Do not route inter-worker coordination through the Orchestrator. You are adults on the same job site. +If your task depends on another worker's output, coordinate directly with them via `@ask foreman-worker-: `. Do not route inter-worker coordination through the Orchestrator. You are adults on the same job site. When coordinating: - Be specific about what you need ("what shape is the auth token object you're returning?") @@ -27,23 +27,23 @@ When coordinating: - Agree on interfaces early rather than building in isolation and hoping things fit ### Ask the Architect for Plan Clarification -If your task assignment is ambiguous or you hit an implementation detail the plan doesn't cover, ping the Architect directly: +If your task assignment is ambiguous or you hit an implementation detail the plan doesn't cover, ask the Architect directly: ``` -relay_ask("foreman-architect", "Task [X]: [your specific question about the plan]") +@ask foreman-architect: Task [X]: [your specific question about the plan] ``` -The Architect owns the plan and can clarify intent without involving the Orchestrator. Only escalate to the Orchestrator if the Architect's answer implies the plan needs to change. +The Architect owns the plan and can clarify intent without involving the Orchestrator. Only escalate to the Orchestrator if the Architect's answer implies the plan needs to change (via `@ask foreman-orchestrator: `). ### Report Completion -When your task is done, notify the Orchestrator via `relay_ask("foreman-orchestrator", completion_summary)`. Your completion summary should include: +When your task is done, notify the Orchestrator via `@ask foreman-orchestrator: `. Your completion summary should include: - What you built - Key decisions you made during implementation - Any deviations from the original assignment and why - Anything the Orchestrator should know for the dissent review ### Report Blockers -If you are stuck, say so immediately. Send the Orchestrator a clear message: what you are trying to do, what is preventing it, and what you think the options are. Do not spin silently. +If you are stuck, say so immediately via `@ask foreman-orchestrator: `. Include: what you are trying to do, what is preventing it, and what you think the options are. Do not spin silently. ## What You Do Not Do diff --git a/scripts/foreman-architect-bridge.py b/scripts/foreman-architect-bridge.py deleted file mode 100755 index 4b082c7..0000000 --- a/scripts/foreman-architect-bridge.py +++ /dev/null @@ -1,361 +0,0 @@ -#!/usr/bin/env python3 -""" -foreman-architect-bridge.py - -Connects to the Foreman relay hub as "foreman-architect" and generates -implementation plans using Qwen3.5 via the local Ollama API. - -The bridge: -1. Receives a goal + project_path from foreman-orchestrator -2. Reads the codebase (read-only) to build context -3. Calls Qwen3.5 to generate a phased plan -4. Writes the plan to CURRENT_PLAN.md in the project directory -5. Replies to the Orchestrator with confirmation - -Usage: - python3 foreman-architect-bridge.py [--model MODEL] [--socket SOCKET_PATH] -""" - -import argparse -import json -import os -import socket -import sys -import urllib.error -import urllib.request -from pathlib import Path - -PROTOCOL_VERSION = "2" -DEFAULT_MODEL = "qwen3.5:latest" -OLLAMA_URL = "http://127.0.0.1:11434/api/generate" -MAX_FILE_BYTES = 8_000 # max bytes read per file for context -MAX_CONTEXT_FILES = 40 # max files included in codebase context -PLAN_FILENAME = "CURRENT_PLAN.md" -MAX_LINE_BYTES = 4 * 1024 * 1024 # 4 MB — guard against unbounded relay messages - -SOCKET_CANDIDATES = [ - os.environ.get("RELAY_HUB_SOCKET", ""), - os.path.expandvars( - os.environ.get("CLAUDE_PLUGIN_DATA", "") - + "/hub.sock" - ), - os.path.expanduser("~/.claude/plugins/data/relay-claude-relay/hub.sock"), - os.path.expanduser("~/.claude-relay/hub.sock"), -] - -ARCHITECT_SYSTEM = """\ -You are the Architect on a software development crew. Your job is to turn a goal into a \ -concrete, phased implementation plan. - -Rules: -- Be specific. Name actual files, functions, and patterns. Vague plans produce vague code. -- Use the codebase context provided. Follow existing conventions. -- Structure the plan in numbered phases. Each phase has discrete tasks a single developer \ -can execute independently. -- For each task: state what to build, which files to touch (exact paths), and what \ -"done" looks like. -- Call out risks, assumptions, and what you explicitly ruled out. -- Do not write code. Write the plan. -- Output only the plan in markdown. No preamble, no "here is your plan". \ -Start with "# Implementation Plan" and nothing before it.\ -""" - - -def find_hub_socket(override: str = "") -> str: - candidates = [override] if override else SOCKET_CANDIDATES - for path in candidates: - if path and Path(path).exists(): - return path - raise FileNotFoundError( - "No relay hub socket found. Is the Orchestrator running?" - ) - - -def read_line(sock: socket.socket) -> dict: - buf = b"" - while True: - chunk = sock.recv(1) - if not chunk: - raise ConnectionError("Hub disconnected unexpectedly.") - if chunk == b"\n": - try: - return json.loads(buf.decode("utf-8")) - except json.JSONDecodeError as e: - raise ConnectionError(f"Invalid JSON from hub: {e}") from e - buf += chunk - if len(buf) > MAX_LINE_BYTES: - raise ConnectionError( - f"Relay message exceeded {MAX_LINE_BYTES} bytes — aborting." - ) - - -def send(sock: socket.socket, obj: dict) -> None: - sock.sendall((json.dumps(obj) + "\n").encode("utf-8")) - - -def build_codebase_context(project_path: str) -> str: - """Walk the project directory and build a context string for the LLM.""" - root = Path(project_path) - if not root.exists(): - return f"[Project path not found: {project_path}]" - - priority_names = { - "README.md", "README.txt", "CLAUDE.md", "AGENTS.md", - "STATE.md", "DECISIONS.md", "CURRENT_PLAN.md", - "package.json", "pyproject.toml", "Gemfile", "go.mod", - "requirements.txt", "setup.py", "Cargo.toml", - } - - skip_dirs = { - ".git", "node_modules", "__pycache__", ".next", "dist", - "build", ".venv", "venv", ".env", "vendor", "coverage", - ".pytest_cache", ".mypy_cache", - ".ssh", ".aws", ".kube", ".gnupg", ".config", - } - - sensitive_name_patterns = ("credential", "secret", "token", "apikey", "api_key", "private_key") - - include_exts = { - ".py", ".ts", ".tsx", ".js", ".jsx", ".rb", ".go", - ".rs", ".java", ".cs", ".swift", ".kt", ".md", ".sh", - ".toml", ".yaml", ".yml", ".json", ".sql", - } - - lines = [f"## Project: {root.name}\n", "### File Tree\n```"] - file_contents = [] - file_count = 0 - - for dirpath, dirnames, filenames in os.walk(root): - dirnames[:] = [d for d in dirnames if d not in skip_dirs] - - rel_dir = Path(dirpath).relative_to(root) - depth = len(rel_dir.parts) - indent = " " * depth - folder = rel_dir.name if str(rel_dir) != "." else root.name - lines.append(f"{indent}{folder}/") - - for fname in sorted(filenames): - fpath = Path(dirpath) / fname - rel_path = fpath.relative_to(root) - lines.append(f"{indent} {fname}") - - ext = fpath.suffix.lower() - is_priority = fname in priority_names - is_included = ext in include_exts - is_sensitive = any(p in fname.lower() for p in sensitive_name_patterns) - - if file_count < MAX_CONTEXT_FILES and (is_priority or is_included) and not is_sensitive: - try: - content = fpath.read_bytes()[:MAX_FILE_BYTES].decode( - "utf-8", errors="replace" - ) - file_contents.append( - f"\n### {rel_path}\n```\n{content}\n```" - ) - file_count += 1 - except OSError: - pass - - lines.append("```\n") - return "\n".join(lines) + "\n".join(file_contents) - - -def generate_plan(goal: str, project_path: str, model: str) -> str: - """Call Qwen3.5 via Ollama and return the generated plan.""" - codebase_context = build_codebase_context(project_path) - prompt = ( - f"Goal:\n{goal}\n\n" - f"Codebase context:\n{codebase_context}\n\n" - "Write the implementation plan." - ) - - payload = json.dumps({ - "model": model, - "system": ARCHITECT_SYSTEM, - "prompt": prompt, - "stream": False, - "options": {"temperature": 0.3}, - }).encode("utf-8") - - req = urllib.request.Request( - OLLAMA_URL, - data=payload, - headers={"Content-Type": "application/json"}, - method="POST", - ) - try: - with urllib.request.urlopen(req, timeout=300) as resp: - result = json.loads(resp.read()) - return result.get("response", "").strip() - except urllib.error.URLError as e: - return f"[Architect unavailable — Ollama not reachable: {e.reason}]" - except Exception as e: - return f"[Architect error: {e}]" - - -def write_plan(plan_text: str, project_path: str) -> str: - """Write the plan to CURRENT_PLAN.md and return the full path.""" - plan_path = Path(project_path) / PLAN_FILENAME - resolved = plan_path.resolve() - try: - resolved.relative_to(Path.home()) - except ValueError: - raise OSError(f"Resolved write target {resolved} is outside $HOME — possible symlink attack") - resolved.write_text(plan_text, encoding="utf-8") - return str(resolved) - - -def run(model: str, socket_path: str) -> None: - path = find_hub_socket(socket_path) - print(f"[architect] Connecting to hub at {path}", flush=True) - - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(path) - print("[architect] Connected.", flush=True) - - send(sock, { - "type": "register", - "name": "foreman-architect", - "cwd": str(Path.cwd()), - "git_branch": "", - "protocol_version": PROTOCOL_VERSION, - }) - ack = read_line(sock) - if ack.get("type") == "err": - print(f"[architect] Registration failed: {ack.get('code')}", flush=True) - sys.exit(1) - - print(f"[architect] Ready. Waiting for goals (model: {model})...", flush=True) - - req_id = 0 - while True: - req_id += 1 - send(sock, { - "type": "inbox_wait", - "timeout_ms": 300_000, - "req_id": f"a{req_id}", - }) - - msg = read_line(sock) - msg_type = msg.get("type") - - if msg_type == "inbox_timeout": - print("[architect] Still here, waiting for a goal...", flush=True) - continue - - if msg_type != "inbox_deliver": - print(f"[architect] Unexpected message type: {msg_type}", flush=True) - continue - - err_code = msg.get("err_code") - if err_code: - print(f"[architect] Error notification: {err_code}", flush=True) - continue - - from_peer = msg.get("from", "unknown") - content = msg.get("content", "").strip() - ask_id = msg.get("ask_id") - - if not content: - continue - - try: - task = json.loads(content) - goal = task.get("goal") or content - project_path = task.get("project_path", str(Path.cwd())) - is_commission = "project_path" in task - except json.JSONDecodeError: - goal = content - project_path = str(Path.cwd()) - is_commission = False - - if is_commission and from_peer != "foreman-orchestrator": - print( - f"[architect] Rejected commission from unauthorized peer: {from_peer}", - flush=True, - ) - if ask_id: - send(sock, { - "type": "reply", - "ask_id": ask_id, - "text": "PLAN FAILED: only foreman-orchestrator may commission plans.", - }) - continue - - home = Path.home() - try: - Path(project_path).resolve().relative_to(home) - except ValueError: - print( - f"[architect] Rejected project_path outside $HOME: {project_path}", - flush=True, - ) - if ask_id: - send(sock, { - "type": "reply", - "ask_id": ask_id, - "text": f"PLAN FAILED: project_path must be under {home}", - }) - continue - - print(f"\n[architect] Goal received from {from_peer}: {goal[:120]}...", flush=True) - print(f"[architect] Reading codebase at: {project_path}", flush=True) - print(f"[architect] Generating plan with {model}...", flush=True) - - plan_text = generate_plan(goal, project_path, model) - if not plan_text or plan_text.startswith("[Architect"): - print(f"[architect] Generation failed: {plan_text}", flush=True) - if ask_id: - send(sock, { - "type": "reply", - "ask_id": ask_id, - "text": f"PLAN FAILED: {plan_text}", - }) - continue - - try: - plan_path = write_plan(plan_text, project_path) - except OSError as e: - print(f"[architect] Failed to write plan: {e}", flush=True) - if ask_id: - send(sock, { - "type": "reply", - "ask_id": ask_id, - "text": f"PLAN FAILED: could not write {PLAN_FILENAME}: {e}", - }) - continue - - first_line = plan_text.split("\n")[0][:80] - response = f"PLAN READY: {PLAN_FILENAME} written to {project_path}. {first_line}" - print(f"[architect] Plan written to {plan_path}", flush=True) - - if ask_id: - send(sock, { - "type": "reply", - "ask_id": ask_id, - "text": response, - }) - - -def main() -> None: - parser = argparse.ArgumentParser( - description="Foreman Architect bridge (Qwen3.5 via Ollama)" - ) - parser.add_argument("--model", default=DEFAULT_MODEL, help="Ollama model name") - parser.add_argument("--socket", default="", help="Override hub socket path") - args = parser.parse_args() - - try: - run(args.model, args.socket) - except KeyboardInterrupt: - print("\n[architect] Shutting down.", flush=True) - except FileNotFoundError as e: - print(f"[architect] {e}", flush=True) - sys.exit(1) - except ConnectionError as e: - print(f"[architect] Lost hub connection: {e}", flush=True) - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/scripts/foreman-bootstrap.sh b/scripts/foreman-bootstrap.sh deleted file mode 100755 index 7842502..0000000 --- a/scripts/foreman-bootstrap.sh +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env bash -# -# foreman-bootstrap.sh — spawn a Foreman crew member session. -# -# Usage: -# ./foreman-bootstrap.sh [worker-number] -# ./foreman-bootstrap.sh --list-roles - -set -euo pipefail - -VALID_ROLES=(orchestrator architect dissenter inspector worker cleaner circuit-breaker muse) - -if [[ "${1:-}" == "--list-roles" ]]; then - printf '%s\n' "${VALID_ROLES[@]}" - exit 0 -fi - -ROLE="${1:?Usage: foreman-bootstrap.sh [worker-number]}" -WORKER_NUM="${2:-}" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -FOREMAN_DIR="$(dirname "$SCRIPT_DIR")" - -USE_BRIDGE=false -USE_CODEX=false -USE_WORKTREE=false -BRIDGE_SCRIPT="" -WORKTREE_PATH="" -WORKTREE_BRANCH="" - -case "$ROLE" in - orchestrator) - MODEL="claude-opus-4-6" - SESSION_NAME="foreman-orchestrator" - ROLE_FILE="$FOREMAN_DIR/references/roles/orchestrator.md" - ;; - architect) - MODEL="qwen3.5:latest" - SESSION_NAME="foreman-architect" - ROLE_FILE="$FOREMAN_DIR/references/roles/architect.md" - USE_BRIDGE=true - BRIDGE_SCRIPT="$FOREMAN_DIR/scripts/foreman-architect-bridge.py" - ;; - dissenter) - MODEL="gemini-3.1-pro" - SESSION_NAME="foreman-dissenter" - ROLE_FILE="$FOREMAN_DIR/references/roles/dissenter.md" - USE_BRIDGE=true - BRIDGE_SCRIPT="$FOREMAN_DIR/scripts/foreman-dissenter-bridge.py" - ;; - inspector) - MODEL="" - SESSION_NAME="foreman-inspector" - ROLE_FILE="$FOREMAN_DIR/references/roles/inspector.md" - USE_CODEX=true - ;; - worker) - MODEL="sonnet" - [ -z "$WORKER_NUM" ] && { echo "Error: worker requires a number"; exit 1; } - [[ "$WORKER_NUM" =~ ^[0-9]+$ ]] || { echo "Error: worker number must be numeric, got: '$WORKER_NUM'"; exit 1; } - SESSION_NAME="foreman-worker-$WORKER_NUM" - ROLE_FILE="$FOREMAN_DIR/references/roles/worker.md" - USE_WORKTREE=true - ;; - cleaner) - MODEL="haiku" - SESSION_NAME="foreman-cleaner" - ROLE_FILE="$FOREMAN_DIR/references/roles/cleaner.md" - ;; - circuit-breaker) - MODEL="haiku" - SESSION_NAME="foreman-circuit-breaker" - ROLE_FILE="$FOREMAN_DIR/references/roles/circuit-breaker.md" - ;; - muse) - MODEL="gemma4:latest" - SESSION_NAME="foreman-muse" - ROLE_FILE="$FOREMAN_DIR/references/roles/muse.md" - USE_BRIDGE=true - BRIDGE_SCRIPT="$FOREMAN_DIR/scripts/foreman-muse-bridge.py" - ;; - *) - echo "Error: Unknown role '$ROLE'" - echo "Valid roles: ${VALID_ROLES[*]}" - exit 1 - ;; -esac - -PROTOCOL_FILE="$FOREMAN_DIR/references/protocol.md" - -for f in "$ROLE_FILE" "$PROTOCOL_FILE"; do - [ -f "$f" ] || { echo "Error: Missing file: $f"; exit 1; } -done - -if [ "$USE_BRIDGE" = "true" ] && [ -n "$BRIDGE_SCRIPT" ]; then - [ -f "$BRIDGE_SCRIPT" ] || { echo "Error: Missing bridge script: $BRIDGE_SCRIPT"; exit 1; } -fi - -# Create an isolated git worktree for this Worker if the project is a git repo. -if [ "$USE_WORKTREE" = "true" ]; then - if git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then - WORKTREE_BRANCH="foreman-worker-$WORKER_NUM-$(date +%s)-$$" - WORKTREE_PATH="/tmp/foreman-worker-$WORKER_NUM-$$" - git worktree add -b "$WORKTREE_BRANCH" "$WORKTREE_PATH" HEAD - echo "Worker worktree created: $WORKTREE_PATH (branch: $WORKTREE_BRANCH)" - else - echo "Warning: Not a git repo — Worker $WORKER_NUM will share the main directory." - WORKTREE_PATH="$(pwd)" - fi -fi - -spawn_session() { - local tmpdir tmpscript script_cwd - tmpdir="$(mktemp -d /tmp/foreman-XXXXXX)" - tmpscript="$tmpdir/launch.sh" - script_cwd="$(pwd)" - - # Use printf '%q' for portable quoting (works on bash 3.2+, unlike ${var@Q}) - local q_cwd q_tmpdir q_model - q_cwd="$(printf '%q' "$script_cwd")" - q_tmpdir="$(printf '%q' "$tmpdir")" - q_model="$(printf '%q' "$MODEL")" - - if [ "$ROLE" = "orchestrator" ]; then - # Interactive — the human talks to this session directly. - # No -p, no permission bypass: the user is in the loop here. - # Inject protocol + role as system context so Claude knows it's the Orchestrator - # and has relay tools available from the start. - { - cat "$PROTOCOL_FILE" - echo "" - echo "---" - echo "" - cat "$ROLE_FILE" - echo "" - echo "---" - echo "" - echo "STARTUP: When the user gives you a goal, begin the Foreman workflow immediately:" - echo "1. relay_rename new_name=\"foreman-orchestrator\"" - echo "2. Proceed with Step 1 of your role (commission the plan via relay_ask to the Architect)." - } > "$tmpdir/orchestrator_ctx.txt" - local q_ctx - q_ctx="$(printf '%q' "$tmpdir/orchestrator_ctx.txt")" - cat > "$tmpscript" <