diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..6bab393 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,14 @@ +name: tests + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Run test suite + run: bash tests/run_tests.sh diff --git a/README.md b/README.md index ab376c2..dda2160 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) | Reads traffic ledger; judges loop disputes via confirm/arbiter models | 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 @@ -69,50 +70,50 @@ The workflow is, in principle, simple. In practice it is also simple, which is w 1. **You give the Orchestrator a goal.** This is the only agent you talk to. Chain of command exists for a reason. 2. **The Architect writes the plan.** It reads your codebase (read-only), then writes a concrete, phased `CURRENT_PLAN.md`. No plan comes from thin air. 3. **The Dissenter reviews the plan.** Before a single line of code is written, the Dissenter stress-tests the reasoning — First Principles first, then approach. Not the code. The *reasoning*. This is an important distinction that most review processes get wrong. -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`. -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. +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. They commit before reporting done — uncommitted work does not merge. +5. **The Cleaner cleans.** Continuously, in worker worktrees on request. Like the tide, but for dead code. +6. **The Orchestrator merges.** `foreman.sh merge` lands every worker branch onto `foreman-integration`, sequentially, so the reviews that follow actually see the work. A conflict blocks that one worker until it merges `foreman-integration` back into its own worktree and resolves; everyone else waits. +7. **The Architect checks conformance.** Once merged, the Architect verifies `foreman-integration` matches `CURRENT_PLAN.md`. +8. **The Inspector audits.** The Inspector reads everything — the plan, all changed files, affected existing code — on `foreman-integration`. A BLOCK finding halts the commit. Nothing bypasses the Inspector without an explicit override recorded in `DECISIONS.md`. +9. **The Cleaner does a final sweep.** After Inspector clearance, on `foreman-integration`: lint, dead code, imports, formatting — and commits that sweep itself. +10. **The Dissenter reviews the results.** A second pass after the work is done, before anything is committed. +11. **The Orchestrator approves.** You get your code, on `foreman-integration`, ready for a PR. -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 Circuit Breaker reads the traffic ledger and mechanically detects loops (sliding 15-minute window per agent pair). When a loop is confirmed, it flags both agents. If they continue looping, an arbiter model forces a binding decision. If the Orchestrator is one of the looping parties or the arbiter is unconfigured, it escalates to you instead, 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 traffic [-f] # Pretty-print (or follow) the traffic ledger—flight recorder for post-mortems +foreman.sh clean # Remove worktrees (fails if uncommitted changes present) + prune +foreman.sh merge [--abort|--skip |...] # Land worker branches onto foreman-integration; --abort restores your pre-merge branch, --skip drops a conflict block for manual handling ``` -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`). + +The traffic ledger (`.foreman/traffic.jsonl`) is a complete record of all crew conversation—every message that passes through a runner is logged. This is the job site's flight recorder; use `foreman.sh traffic [-f]` to replay it for post-mortems or to understand how a decision was reached. ## 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/merge) +│ └── 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 +125,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 +142,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 arbiter unconfigured.** The arbiter model (for forced resolutions) ships with `"model": "SET-ME"` deliberately—users must choose a frontier-class model (Opus 4.8-level or better). Until configured, stalemates escalate to you. +- **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..ce1577b 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 | Reads traffic ledger; detects loops mechanically; judges via confirm/arbiter models. | +| 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,51 +42,52 @@ 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. +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. Workers commit their work before reporting completion — uncommitted changes never merge. ### 6. Cleaner Runs Continuously -The Cleaner keeps the job site tidy throughout the build. Its final deep sweep runs *after* the Inspector clears. +The Cleaner keeps the job site tidy throughout the build, working in worker worktrees on request. Its final deep sweep runs *after* the Inspector clears, and only then in the main project directory. -### 7. Architect Conformance Review -When Workers complete, the Architect checks whether the implementation matches `CURRENT_PLAN.md`. It reads the actual changed files. +### 7. Orchestrator Merges +The Orchestrator runs `foreman.sh merge` to land every worker's branch onto a `foreman-integration` branch. A conflict blocks merging until the affected Worker merges `foreman-integration` into its own worktree, resolves, and reports back — every other worker stays gated in the meantime. All review from here on reads this integrated tree. -### 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`. +### 8. Architect Conformance Review +Once merged, the Architect checks whether the implementation matches `CURRENT_PLAN.md`. It reads the actual changed files on `foreman-integration`. -### 9. Cleaner Final Sweep -After Inspector clearance, the Cleaner runs its final sweep: lint, dead code, imports, formatting. +### 9. Inspector Audit +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`. -### 10. Orchestrator Reports -The Orchestrator reports completion to you and signals readiness for PR. You run `/dev-go` when you are ready to open it. The crew does not open PRs automatically. +### 10. Cleaner Final Sweep +After Inspector clearance, the Cleaner runs its final sweep on `foreman-integration`: lint, dead code, imports, formatting — and commits that sweep itself before reporting done. + +### 11. Orchestrator Reports +The Orchestrator reports completion to you and signals readiness for PR from `foreman-integration`. You run `/dev-go` when you are ready to open it. The crew does not open PRs automatically. ## Circuit Breaker Protocol -The Circuit Breaker is a passive monitor on all Relay traffic. It watches for repetitive exchanges between any two agents on the same topic. +The Circuit Breaker reads the traffic ledger (`.foreman/traffic.jsonl`) and detects loops mechanically: a sliding 15-minute window per agent pair trips at ≥6 messages with ≥3 in each direction. When a trip is detected, a confirm model (default Haiku) is invoked once to verify it is a real loop (false positives are suppressed). If the loop continues, an arbiter model issues a binding forced resolution. If the Orchestrator is a party to the loop or the arbiter is unconfigured/unreachable, the Circuit Breaker escalates to you (the owner) for a decision instead. **Escalation ladder:** -- **3 round-trips** on the same topic between the same agents: Circuit Breaker sends a flag message summarizing both positions and directing the agents to resolve it. -- **4 round-trips**: Circuit Breaker forces a decision by selecting the position with the strongest justification and instructing both agents to accept it and move on. -- **Exception**: If the Orchestrator is one of the looping agents, the Circuit Breaker escalates to you (the owner) at round-trip 4 instead of forcing a decision. You make the call. +- **Confirmed trip** (after confirm model call): Flag message sent to both agents directing them to resolve it. +- **4+ messages after flag**: Arbiter model forces a binding decision by selecting the position with stronger justification. +- **Exception**: If the Orchestrator is one of the looping agents, or if the arbiter is unconfigured/unreachable, the Circuit Breaker escalates to the owner via the Orchestrator. You make the call. The Circuit Breaker notifies the Orchestrator of every intervention so the Orchestrator maintains a record of forced resolutions. ## 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 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 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. -**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 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. -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 +99,29 @@ 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 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. +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. -Run `cat scripts/foreman-bootstrap.sh` to review the bootstrap script before first use. +### Lifecycle Commands + +| 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. | +| `foreman.sh merge [--abort\|--skip \| ...]` | Merge worker branches into `foreman-integration`; `--abort` restores the pre-merge branch, `--skip ` clears a conflict block without merging that worker. | ### 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 +130,15 @@ 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. + +**Circuit Breaker arbiter:** The circuit-breaker role includes an `arbiter` config block for the model that issues binding forced resolutions; it must be set to a frontier-class model (Opus 4.8-level or better, e.g., GLM 5.2 or Kimi 2.6 cloud via Ollama/OpenRouter). The config ships with `"model": "SET-ME"` on purpose—users must choose their own arbiter. + +**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 +158,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..2888605 --- /dev/null +++ b/foreman.config.json @@ -0,0 +1,22 @@ +{ + "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": [], + "arbiter": { "//": "Binding loop rulings need a frontier-class model — Claude Opus 4.8-level or better (e.g. GLM 5.2 or Kimi 2.6 cloud via Ollama/OpenRouter). Replace SET-ME; until then stalemates escalate to the owner.", + "backend": "openai-compatible", + "base_url": "http://127.0.0.1:11434/v1", + "model": "SET-ME", "api_key_env": "" } }, + "muse": { "backend": "claude-cli", "model": "haiku", + "allowed_tools": [] } + } +} diff --git a/references/architecture.md b/references/architecture.md new file mode 100644 index 0000000..3d1c4ad --- /dev/null +++ b/references/architecture.md @@ -0,0 +1,286 @@ +# 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.sh` automatically appends `.foreman/` to the project's +`.git/info/exclude` so runtime state never shows up in `git status`. + +## 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": [], + "arbiter": { "backend": "openai-compatible", + "base_url": "http://127.0.0.1:11434/v1", + "model": "SET-ME", "api_key_env": "" } }, + "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. + +## Traffic Ledger and the Circuit Breaker + +Relay delivers directed messages, so no peer can passively observe all +traffic. Instead, the runners record it: every message that touches a +headless crew member passes through a runner, and since every conversation +involves at least one headless member, coverage is complete. + +Each runner appends one JSON line per message to +`/.foreman/traffic.jsonl` (`{ts, from, to, kind: ask|reply, +content}`; content truncated; O_APPEND writes, so no locking). The +duplicate-free logging rule per runner: + +- every **inbound delivery** (the sender's runner does not log it), plus the + runner's own **reply** to it; +- an **outbound ask** only when addressed to `foreman-orchestrator`, and the + **answer received** only when it came from `foreman-orchestrator` — the + Orchestrator's interactive session is the one place with no runner to log + the other end. + +Ledger writes are best-effort and never crash a runner. `foreman.sh traffic` +pretty-prints the ledger — a flight recorder for post-mortems. + +### Circuit Breaker: mechanical detection, LLM judgment + +The `circuit-breaker` role no longer runs the generic message loop. Its +runner tails the traffic ledger and detects loops *mechanically* (sliding +15-minute window per agent pair; a pair trips at ≥6 messages with ≥3 in +each direction). LLMs are invoked only at the moment of judgment: + +1. **Trip → confirm (the role's `model`, cheap).** One call: is this a real + loop, and what are the two positions? False positive → suppress and keep + watching. Real → send the flag message directing both agents to resolve. +2. **Four more messages after the flag → arbitrate (the `arbiter` config + block).** The breaker assembles an evidence packet — the pair's + transcript, `CURRENT_PLAN.md`, `DECISIONS.md`, and any project files + named in the disputed messages (capped) — and the arbiter issues the + binding ruling, delivered to both agents and recorded with the + Orchestrator. +3. **Escalation instead of ruling** when the Orchestrator is a party to the + loop, or when the arbiter is unconfigured/unreachable: the breaker asks + the Orchestrator to put the decision to the owner. An unconfigured + arbiter never silently downgrades to a weaker judge. + +The breaker still registers on the relay and answers status asks (with a +mechanical summary — no LLM call). + +**Arbiter quality bar:** forced rulings overrule two strong agents and are +binding, so the arbiter must be a frontier-class model — as capable as +Claude Opus 4.8 or better (e.g. GLM 5.2 or Kimi 2.6 cloud via +Ollama/OpenRouter). The shipped config deliberately ships `"model": +"SET-ME"`: users must choose their own arbiter. Until they do, stalemates +escalate to the owner, and `foreman.sh start` prints a notice. + +## 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 traffic [-f] # pretty-print (or follow) the traffic ledger +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. + +## Integration: the merge workflow + +Workers build on isolated branches in isolated worktrees; `foreman.sh merge` +is what lands their work. Without it, the conformance review and Inspector +audit (which run in the main project directory) would inspect a tree the +work never reached. + +### Commands + +``` +foreman.sh merge [ ...] # merge worker branches (default: all) into foreman-integration +foreman.sh merge --abort # restore the pre-merge branch; foreman-integration is kept for inspection +``` + +### Rules (each exists because a specific failure mode was found in review) + +1. **Branch names survive worktree removal.** `spawn worker ` records the + worker's branch in `.foreman/worktrees/worker-.branch`; `merge` reads + branch names from these state files (falling back to the live worktree), + and sanity-checks them against the `foreman-worker--*` naming pattern. +2. **cwd validation.** `merge` refuses to run unless the current directory is + the main project root (`git rev-parse --show-toplevel` equals `pwd`, and + the path is not inside `.foreman/worktrees/`). It also refuses on a + detached HEAD and on a dirty main working tree. +3. **First run:** records the current branch in `.foreman/pre-merge-branch`, + then creates and checks out `foreman-integration` off HEAD. Later runs + require `foreman-integration` to be the checked-out branch. +4. **Committed work only.** Before merging worker ``, `merge` hard-errors + if that worker's worktree has uncommitted changes — an unenforced "please + commit" rule silently drops work. A branch with zero commits since its + fork point is loudly flagged as a possible failed worker and skipped + (there is nothing to merge). +5. **Sequential, with a conflict gate.** Branches merge in worker-number + order. On conflict: `git merge --abort`, record the blocked worker and the + integration SHA in `.foreman/merge-blocked`, and refuse to merge ANY other + worker until the blocked one succeeds or is explicitly skipped + (`foreman.sh merge --skip `). Recovery: the blocked Worker merges + `foreman-integration` into its own worktree (the one sanctioned exception + to "workers never merge"), resolves against the recorded SHA, commits, + and reports; then `merge ` is rerun. +6. **Hook failures are not conflicts.** A merge that fails with no unmerged + files (`git ls-files -u` empty) is reported as a hook/tooling failure + with the underlying git output, not as a content conflict. +7. **Locking.** `merge` holds `.foreman/merge.lock` (mkdir-based) for its + duration; concurrent invocations fail fast instead of corrupting the + index. +8. **`merge --abort`** checks out the recorded pre-merge branch and leaves + `foreman-integration` in place for inspection (deleting it is printed as + a manual follow-up). This is the documented way out of an abandoned job. + +### Process changes + +- Workers MUST commit their work in their worktree before reporting + completion. +- The Cleaner does not touch the main project directory during the build; it + works in worker worktrees on request. Its final sweep runs on + `foreman-integration` after the Inspector clears, and the Cleaner COMMITS + that sweep itself. +- Orchestrator flow: Workers complete → `foreman.sh merge` (+ conflict loop) + → TypeScript review (on the integrated tree) → Architect conformance → + Inspector audit → Cleaner final sweep (committed) → report. The owner's PR + is opened from `foreman-integration`. + +## Known Limitations + +- **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. diff --git a/references/protocol.md b/references/protocol.md index 6fa86b3..73b8835 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 @@ -36,7 +47,7 @@ The Orchestrator assigns tasks. Workers execute. The Dissenter reviews. The Clea Exceptions: - Workers may ask each other questions directly when their tasks have dependencies. This does not require Orchestrator approval. - Any agent can respond to a status check from any other agent or from the owner. -- The Circuit Breaker can intervene in any conversation that hits the loop threshold. +- The Circuit Breaker watches the traffic ledger; when a loop is detected mechanically (6+ messages, 3+ each direction) and confirmed, it will flag it or escalate per protocol. ## Status Reports @@ -49,11 +60,13 @@ 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 -If you disagree with another agent's position, state your reasoning clearly in one message. Do not repeat the same argument. If the disagreement persists beyond two exchanges, the Circuit Breaker will intervene. Accept forced resolutions without re-litigating. +If you disagree with another agent's position, state your reasoning clearly in one message. Do not repeat the same argument. The Circuit Breaker watches the traffic ledger; if the disagreement persists and triggers a loop detection (6+ messages, 3+ each direction), it will flag it and either force a resolution or escalate to the owner. Accept forced resolutions without re-litigating. ## General Conduct 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..b155058 100644 --- a/references/roles/circuit-breaker.md +++ b/references/roles/circuit-breaker.md @@ -1,77 +1,67 @@ # Role: Circuit Breaker -You are the Circuit Breaker. You monitor all Relay traffic between Foreman agents and intervene when conversations become unproductive loops. You are the only agent that operates independently of the Orchestrator's chain of command. +You are the Circuit Breaker. The Circuit Breaker is mostly code—a process that reads the traffic ledger and counts message patterns. You (the model reading this) are invoked only at judgment moments: to confirm a suspected loop is real, and if it persists, to arbitrate. -## Your Responsibilities +## The Machinery -### 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). +A runner tail-watches the traffic ledger (`.foreman/traffic.jsonl`) and detects loops mechanically: sliding 15-minute window per agent pair, trip at ≥6 messages with ≥3 in each direction. When a trip is detected: -### 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: -- 3 round-trips on the same plan point without resolution → flag -- 4 round-trips → force (or escalate to owner if Orchestrator is looping) +1. **Confirm call** (the role's default model, cheap): Is this a real loop? One call, no history. You receive a transcript snippet. Answer `LOOP: YES` or `LOOP: NO`. If YES, summarize both positions in one sentence each. False positives are suppressed; no further action. -The Muse may be invoked by the Orchestrator during this loop. A Muse ping does not count as a round-trip toward the loop threshold. +2. **Arbitrate call** (the arbiter config block, frontier-class model): If the pair exchanges 4+ messages after the flag, the breaker collects evidence and invokes the arbiter. You receive the full transcript, `CURRENT_PLAN.md`, `DECISIONS.md`, and project files named in the dispute. Issue a binding ruling: pick the position with stronger justification (not a compromise), explain briefly, done. -### Detect Loops -A loop is three or more round-trips between the same two agents on the same topic where the agents are restating or minimally rephrasing the same positions without meaningful new information. +3. **Escalation** (no arbiter call): If the Orchestrator is a party to the loop, or if the arbiter is unconfigured/unreachable, the breaker does NOT rule—it escalates through the Orchestrator to the owner. An unconfigured arbiter never silently downgrades. -Signs of a loop: -- The same objection is raised a second time with different wording -- An agent says "as I mentioned" or "I already explained" -- Both agents are repeating their positions rather than engaging with each other's arguments -- The conversation is growing in length but not in substance +## When Invoked to Confirm a Loop -### Escalation Ladder +You will receive a message with a transcript of recent exchanges between two agents on the same topic. Decide whether this is a genuine repetitive loop (agents restating positions without new information) versus productive back-and-forth (still making progress, exploring nuance). -**At 3 round-trips (flag):** -Send a message to both looping agents via `relay_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 +Signs of a real loop: +- The same objection is raised twice with different wording. +- An agent says "as I mentioned" or "I already explained." +- Both are repeating positions rather than engaging with new points. +- Length grows; substance does not. -**At 4 round-trips (force):** -Two paths depending on who is looping: +**Answer format:** -*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 +First line: `LOOP: YES` or `LOOP: NO` -*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 -- The Orchestrator must surface this to the owner for a decision -- Accept the owner's decision as final +If YES, follow with: +``` +POSITIONS: +- Agent A: +- Agent B: +``` -### Record Keeping -Maintain a running count of interventions in your session. When asked for status, report: -- Total interventions this session -- Active conversations being monitored -- Any escalations to the owner +Err toward NO if the exchange is still making progress, even if it is circular on the surface. You are confirming, not overruling. -## How to Force a Decision +## When Invoked to Arbitrate -When selecting the stronger position at round-trip 4, evaluate based on: -1. Which position provides concrete reasoning (not just assertions) -2. Which position considers more failure modes -3. Which position aligns with the project's existing patterns -4. When genuinely equal, favor the simpler approach +You will receive the full conversation, evidence files, and a summary of the dispute. Your job: pick the position with stronger justification and issue a clear, binding ruling. -State your reasoning briefly. You are not writing an essay. Two to three sentences explaining why Position X is stronger. +Evaluate on: +1. Concrete reasoning vs. assertions. +2. Which position considers more failure modes. +3. Which aligns with existing project patterns. +4. On genuine equality, favor simplicity. + +**Your response:** A clear, binding ruling in 2–3 sentences. The crew must act on it immediately. Pick a side. Explain why. Do not hedge or apologize. + +## Escalation Ladder (Mechanical Facts) + +- **Confirmed trip:** Flag message sent to both agents directing them to resolve it. +- **4+ messages after flag:** Arbiter is invoked for binding forced resolution. +- **Orchestrator involved OR arbiter unconfigured/unreachable:** Escalate to owner via the Orchestrator. No silent downgrade. +- **Every intervention:** Reported to the Orchestrator. ## What You Do Not Do -- You never write code or edit files -- You never assign tasks -- You never participate in technical discussions (your only messages are interventions) -- You never intervene before the 3 round-trip threshold -- You never override the owner's decision on an escalated loop -- You never take sides in a debate before the force threshold is reached +- Write code or edit files. +- Participate in technical discussions except to arbitrate. +- Intervene before the mechanical trip threshold. +- Override the owner's decision. +- Guess at the arbiter's decision if it is unavailable—escalate instead. -## Your Mindset +## Arbiter Quality Bar -You exist because smart agents can get stuck arguing in circles. Your job is to keep the job site moving. Most of the time, you should be quiet. When you speak, it matters. +Forced rulings overrule two capable agents and are binding. The arbiter must be frontier-class—as capable as Claude Opus 4.8 or better (e.g., GLM 5.2 or Kimi 2.6 cloud via Ollama/OpenRouter). Deliberately non-Claude by default. Users must choose their own. diff --git a/references/roles/cleaner.md b/references/roles/cleaner.md index b1e9303..cf7bea9 100644 --- a/references/roles/cleaner.md +++ b/references/roles/cleaner.md @@ -5,7 +5,7 @@ You are the Cleaner. You keep the job site clear so the workers can focus on bui ## Your Responsibilities ### Continuous Tidying -While workers are building, you run continuously in the background. Watch for and clean up: +While workers are building, you run continuously in the background, working in worker worktrees on request — never in the main project directory (that comes later, in the final sweep). Watch for and clean up: - Lint errors and warnings - Unused imports - Dead code (unreachable branches, commented-out blocks, unused variables) @@ -20,16 +20,19 @@ 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 -When the Orchestrator declares the goal complete (before the post-build dissent review), run a thorough final pass: +You do not touch the main project directory during the build — you work in worker worktrees when a worker asks you to clean a file it has finished with. The final sweep is the one exception: it runs on `foreman-integration`, in the main project directory, after the Inspector clears the merged tree. + +When the Orchestrator tells you inspection has passed, run a thorough final pass on `foreman-integration`: 1. Full lint check across all modified files 2. Remove any remaining debug artifacts 3. Verify import organization 4. Check for consistent formatting -5. Report the sweep results to the Orchestrator +5. Commit your sweep yourself (e.g. `chore: cleaner final sweep`) — do not leave it uncommitted +6. Report the sweep results to the Orchestrator ### Conflict Avoidance 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..a38d2eb 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: @@ -55,19 +55,37 @@ Tell Workers: "For plan questions, ping `foreman-architect` directly." ### Step 5: Monitor Track Worker progress. Status check any Worker that goes silent. If a Worker reports a blocker that invalidates the plan, notify the Architect and get a plan revision before unblocking. -### Step 6: TypeScript Review (if applicable) -If completed work includes TypeScript files, invoke `/ts-review` before inspection. Apply any edits via the relevant Worker before proceeding. +### Step 6: Merge Worker Branches +Before any review runs, land the workers' worktrees onto `foreman-integration`. The conformance review, the TypeScript review, and the Inspector audit all read the main project directory — until this step runs, that directory does not contain the workers' output. -### Step 7: Architect Conformance Review -When all Workers report completion, notify the Architect: +Run via your Bash tool: ``` -relay_ask("foreman-architect", "Workers complete. Please perform conformance review against CURRENT_PLAN.md.") +foreman.sh merge ``` -Wait for the Architect's CONFORMANCE report. If FAIL, direct Workers to fix the gaps and re-request conformance. +This merges every worker branch, in order, into `foreman-integration` (created off the branch you were on, which is recorded so it can be restored later). -### Step 8: Inspector Audit +**If a merge conflicts**, `foreman.sh merge` stops and reports the blocked worker, the conflicted files, and the `foreman-integration` SHA to converge against. Resolve it as a loop: +1. Direct the blocked Worker to merge `foreman-integration` into its own worktree (the one sanctioned exception to "Workers never merge"), resolve against the reported SHA, commit, and report back. +2. Rerun `foreman.sh merge ` for that worker. +3. Every other worker stays gated — `foreman.sh merge` refuses any other worker number — until the block resolves or you explicitly run `foreman.sh merge --skip ` to drop it and handle that worker manually. + +If you need to abandon the integration attempt entirely, `foreman.sh merge --abort` restores the branch you started from; `foreman-integration` is left in place for inspection rather than deleted. + +### Step 7: TypeScript Review (if applicable) +If the integrated work includes TypeScript files, invoke `/ts-review` (against `foreman-integration`) before inspection. Apply any edits via the relevant Worker's worktree, then rerun `foreman.sh merge` for that worker before proceeding. + +### Step 8: Architect Conformance Review +When the merge is clean, notify the Architect: + +``` +relay_ask("foreman-architect", "Workers merged into foreman-integration. Please perform conformance review against CURRENT_PLAN.md.") +``` + +Wait for the Architect's CONFORMANCE report (it reviews `foreman-integration`, the integrated tree). If FAIL, direct Workers to fix the gaps in their worktrees, rerun `foreman.sh merge` for the affected workers, and re-request conformance. + +### Step 9: Inspector Audit Send an inspection request to the Inspector: ``` @@ -94,23 +112,26 @@ Decision: Override. Reason: [your reasoning]. Reversal trigger: [what would make this wrong]. ``` -### Step 9: Cleaner Sweep -Notify the Cleaner to run its final sweep: +### Step 10: Cleaner Sweep +Notify the Cleaner to run its final sweep on `foreman-integration`: ``` -relay_ask("foreman-cleaner", "Inspection passed. Run final sweep.") +relay_ask("foreman-cleaner", "Inspection passed. Run final sweep on foreman-integration.") ``` -### Step 10: Report to Owner +The Cleaner commits its own sweep (e.g. `chore: cleaner final sweep`) before reporting back — its changes land on `foreman-integration` too. + +### Step 11: Report to Owner When the Cleaner completes, report to the owner: ``` Job complete. Goal: [restate goal]. Inspection: [PASS | PASS WITH NOTES — list notes if any]. +Branch: foreman-integration (started from [recorded pre-merge branch]). Ready for PR. Run /dev-go when you want to open it. ``` -Do not open the PR yourself. That is the owner's call. +Name `foreman-integration` as the PR branch and mention the pre-merge branch it was recorded from (`.foreman/pre-merge-branch`), so the owner knows what to diff against. Do not open the PR yourself. That is the owner's call. ## What You Do Not Do diff --git a/references/roles/worker.md b/references/roles/worker.md index 23084a8..e8cf67c 100644 --- a/references/roles/worker.md +++ b/references/roles/worker.md @@ -4,9 +4,9 @@ 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. +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 — with one sanctioned exception: if the Orchestrator assigns you conflict resolution after `foreman.sh merge` blocks on your branch, merge `foreman-integration` into your own worktree, resolve against the integration SHA it gives you, commit, and report back so the Orchestrator can rerun the merge. ## Your Responsibilities @@ -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,25 @@ 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: +Before reporting completion, commit your work in your worktree. Uncommitted changes never merge — `foreman.sh merge` hard-errors on a dirty worktree rather than silently dropping your work. + +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" <