From 654764dff1c42528df40114785fc71f416b6a28c Mon Sep 17 00:00:00 2001 From: simbo1905 Date: Thu, 30 Jul 2026 23:53:07 +0100 Subject: [PATCH 1/5] Ignore local projects and staging artifacts --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 02be7d65..b4477032 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ .claude/ .swarmforge/ .worktrees/ +.tmp/ +projects/ From a1e21f6138bae91ea51f3790269baaea496fa82c Mon Sep 17 00:00:00 2001 From: Simon Massey <322608+simbo1905@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:33:29 +0100 Subject: [PATCH 2/5] Add OpenCode agent backend --- README.md | 4 ++-- swarmforge/scripts/swarmforge.bb | 5 +++-- test/swarmforge/script_test.clj | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 98115028..d8caffc4 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ SwarmForge is a lightweight, tmux-based orchestration layer that: - Launches a **config-driven swarm** from a project-local `swarmforge/swarmforge.conf` - Creates one tmux session per configured role and opens a terminal surface for each role when the selected backend supports it - Reads behavior from project-local `swarmforge/roles/.prompt` files plus a layered `swarmforge/constitution.prompt` -- Supports per-role backends such as `claude`, `codex`, `copilot`, or `grok` +- Supports per-role backends such as `claude`, `codex`, `copilot`, `grok`, or `opencode` - Puts the shared `swarmforge/scripts/` directory on each agent's `PATH`, including handoff helpers for active swarm communication - Creates git worktrees under `.worktrees/` for roles assigned to dedicated worktree names - Initializes a git repository in a new working directory when needed @@ -104,7 +104,7 @@ SwarmForge is a lightweight, tmux-based orchestration layer that: - **Config-Driven Topology** — The swarm shape comes from `swarmforge/swarmforge.conf`, not hardcoded shell variables. - **Project-Local Roles** — Each role is defined by `swarmforge/roles/.prompt` in the working tree being orchestrated. - **Layered Constitution** — `swarmforge/constitution.prompt` directs agents to read article files under `swarmforge/constitution/articles/`. -- **Backend Selection Per Role** — A role can launch `claude`, `codex`, `copilot`, or `grok`. +- **Backend Selection Per Role** — A role can launch `claude`, `codex`, `copilot`, `grok`, or `opencode`. For OpenCode, set the provider and model as role arguments, for example `--model opencode-go/qwen3.7-plus` or `--model opencode/deepseek-v4-pro`. - **Observable Swarm** — Open one Terminal window per role and watch the sessions in real time. - **Self-Hosted & Lightweight** — Runs locally in tmux and Terminal with minimal machinery. diff --git a/swarmforge/scripts/swarmforge.bb b/swarmforge/scripts/swarmforge.bb index ff4e9d3d..f9818bf1 100755 --- a/swarmforge/scripts/swarmforge.bb +++ b/swarmforge/scripts/swarmforge.bb @@ -166,7 +166,7 @@ (fail! (str red "Error:" reset " Duplicate worktree '" worktree "' in " (:config-file ctx)))) (when (or (str/includes? worktree "/") (#{"." ".."} worktree)) (fail! (str red "Error:" reset " Invalid worktree '" worktree "' for role '" role "'"))) - (when-not (#{"claude" "codex" "copilot" "grok"} agent) + (when-not (#{"claude" "codex" "copilot" "grok" "opencode"} agent) (fail! (str red "Error:" reset " Unsupported agent '" agent "' for role '" role "'"))) (when-not (#{"task" "batch"} receive-mode) (fail! (str red "Error:" reset " Invalid receive mode '" receive-mode "' for role '" role "' on line " line-no ": expected task or batch"))) @@ -339,7 +339,8 @@ "claude" (str "claude --append-system-prompt-file " (sq (str prompt-file)) " --permission-mode acceptEdits -n " (sq (str "SwarmForge " display)) " " (extra-args-prefix row) "\"$(cat " (sq (str prompt-file)) ")\"") "codex" (str "codex -C " (sq (str role-worktree)) " " (extra-args-prefix row) "\"$(cat " (sq (str prompt-file)) ")\"") "copilot" (str "copilot -C " (sq (str role-worktree)) " --name " (sq (str "SwarmForge " display)) " " (extra-args-prefix row) "-i \"$(cat " (sq (str prompt-file)) ")\"") - "grok" (str "grok --cwd " (sq (str role-worktree)) " " (grok-permission-prefix row) (extra-args-prefix row) "--rules \"$(cat " (sq (str prompt-file)) ")\" --verbatim \"$(cat " (sq (str prompt-file)) ")\""))) + "grok" (str "grok --cwd " (sq (str role-worktree)) " " (grok-permission-prefix row) (extra-args-prefix row) "--rules \"$(cat " (sq (str prompt-file)) ")\" --verbatim \"$(cat " (sq (str prompt-file)) ")\"") + "opencode" (str "opencode " (sq (str role-worktree)) " " (extra-args-prefix row) "--prompt \"$(cat " (sq (str prompt-file)) ")\""))) (= index 0) (str "; exit_code=$?; SWARMFORGE_TERMINAL_BACKEND=" (sq (:terminal-backend ctx)) " nohup " (sq (str (fs/path (:script-dir ctx) "swarm-cleanup.sh"))) diff --git a/test/swarmforge/script_test.clj b/test/swarmforge/script_test.clj index c7ed1e34..b40ded7d 100644 --- a/test/swarmforge/script_test.clj +++ b/test/swarmforge/script_test.clj @@ -237,6 +237,22 @@ (finally (fs/delete-tree root))))) +(deftest opencode-launch-command-passes-model-and-initial-prompt + (let [root (tmp-dir)] + (try + (let [result (run {:dir root} + (script "swarmforge.bb") + "--test-launch-command" + (str root) + "opencode" + "--model opencode-go/qwen3.7-plus --auto") + command (:out result)] + (is (str/includes? command "opencode '")) + (is (str/includes? command "--model opencode-go/qwen3.7-plus --auto --prompt")) + (is (str/includes? command ".swarmforge/prompts/coder.md"))) + (finally + (fs/delete-tree root))))) + (deftest grok-launch-command-uses-bypass-permissions-with-always-approve (let [root (tmp-dir)] (try From 7723561023dbba89e5b63fb6ea93a778dc10a27c Mon Sep 17 00:00:00 2001 From: Simon Massey <322608+simbo1905@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:35:58 +0100 Subject: [PATCH 3/5] Allow bounded alternate swarm configurations --- README.md | 1 + swarmforge/scripts/swarmforge.bb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d8caffc4..2345c29e 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ SwarmForge is a lightweight, tmux-based orchestration layer that: ## Core Features - **Config-Driven Topology** — The swarm shape comes from `swarmforge/swarmforge.conf`, not hardcoded shell variables. +- **Alternate Configurations** — Set `SWARMFORGE_CONFIG` to a project-local alternate configuration filename for a bounded smoke workflow. - **Project-Local Roles** — Each role is defined by `swarmforge/roles/.prompt` in the working tree being orchestrated. - **Layered Constitution** — `swarmforge/constitution.prompt` directs agents to read article files under `swarmforge/constitution/articles/`. - **Backend Selection Per Role** — A role can launch `claude`, `codex`, `copilot`, `grok`, or `opencode`. For OpenCode, set the provider and model as role arguments, for example `--model opencode-go/qwen3.7-plus` or `--model opencode/deepseek-v4-pro`. diff --git a/swarmforge/scripts/swarmforge.bb b/swarmforge/scripts/swarmforge.bb index f9818bf1..8a968e49 100755 --- a/swarmforge/scripts/swarmforge.bb +++ b/swarmforge/scripts/swarmforge.bb @@ -468,7 +468,7 @@ :script-dir script-dir :swarm-forge-dir swarm-forge-dir :worktrees-dir (fs/path working-dir ".worktrees") - :config-file (fs/path swarm-forge-dir "swarmforge.conf") + :config-file (fs/path swarm-forge-dir (or (System/getenv "SWARMFORGE_CONFIG") "swarmforge.conf")) :roles-dir (fs/path swarm-forge-dir "roles") :constitution-file (fs/path swarm-forge-dir "constitution.prompt") :state-dir state-dir From a33c915bf9d090dce6fc2474edb8fd057fa17728 Mon Sep 17 00:00:00 2001 From: Simon Massey <322608+simbo1905@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:40:26 +0100 Subject: [PATCH 4/5] Keep handoff daemon alive after bridge detach --- swarmforge/scripts/swarmforge.bb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/swarmforge/scripts/swarmforge.bb b/swarmforge/scripts/swarmforge.bb index 8a968e49..cae6809b 100755 --- a/swarmforge/scripts/swarmforge.bb +++ b/swarmforge/scripts/swarmforge.bb @@ -390,10 +390,16 @@ (fs/delete-if-exists (fs/path (:daemon-dir ctx) "stop")) (let [command (into (vec (sleep-inhibitor-prefix)) [(str (fs/path (:script-dir ctx) "handoffd.bb")) - (str (:working-dir ctx))])] - (process/process command - {:out (str (:handoff-daemon-log ctx)) - :err :out}) + (str (:working-dir ctx))]) + detached-command (str "nohup " + (str/join " " (map sq command)) + " >" + (sq (str (:handoff-daemon-log ctx))) + " 2>&1 &")] + ;; A babashka-managed child can be terminated with its launcher when the + ;; terminal bridge detaches. The handoff daemon must instead outlive that + ;; bridge and follow its explicit stop-file protocol. + (sh "zsh" "-c" detached-command) (println (str green "Started handoff daemon" (when (> (count command) 2) " with OS sleep prevention") "." From f25d9f2d97f238955496fab731d2dcedfeac78f3 Mon Sep 17 00:00:00 2001 From: Simon Massey <322608+simbo1905@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:54:52 +0100 Subject: [PATCH 5/5] Add smoke-only instruction profile --- README.md | 2 +- swarmforge/scripts/swarmforge.bb | 15 +++++++++++++-- test/swarmforge/script_test.clj | 17 +++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2345c29e..ddfe8d95 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ SwarmForge is a lightweight, tmux-based orchestration layer that: ## Core Features - **Config-Driven Topology** — The swarm shape comes from `swarmforge/swarmforge.conf`, not hardcoded shell variables. -- **Alternate Configurations** — Set `SWARMFORGE_CONFIG` to a project-local alternate configuration filename for a bounded smoke workflow. +- **Alternate Configurations** — Set `SWARMFORGE_CONFIG` to a project-local alternate configuration filename for a bounded smoke workflow. A smoke-only project may additionally set `SWARMFORGE_INSTRUCTION_PROFILE=role-only` to replace the normal recursive constitution bootstrap with the explicitly scoped role prompt; this is not appropriate for product work. - **Project-Local Roles** — Each role is defined by `swarmforge/roles/.prompt` in the working tree being orchestrated. - **Layered Constitution** — `swarmforge/constitution.prompt` directs agents to read article files under `swarmforge/constitution/articles/`. - **Backend Selection Per Role** — A role can launch `claude`, `codex`, `copilot`, `grok`, or `opencode`. For OpenCode, set the provider and model as role arguments, for example `--model opencode-go/qwen3.7-plus` or `--model opencode/deepseek-v4-pro`. diff --git a/swarmforge/scripts/swarmforge.bb b/swarmforge/scripts/swarmforge.bb index cae6809b..90327b49 100755 --- a/swarmforge/scripts/swarmforge.bb +++ b/swarmforge/scripts/swarmforge.bb @@ -298,10 +298,21 @@ (sh "tmux" "-S" (:tmux-socket ctx) "rename-window" "-t" (str session ":" agent-window) title) (sh "tmux" "-S" (:tmux-socket ctx) "set-window-option" "-t" (str session ":" title) "allow-rename" "off")) +(defn instruction-profile [] + ;; A project may opt into this only for a bounded, non-product smoke run. + ;; Normal swarms retain the constitution-first recursive instruction. + (case (System/getenv "SWARMFORGE_INSTRUCTION_PROFILE") + "role-only" :role-only + :standard)) + (defn write-agent-instruction-file! [role prompt-file] (spit (str prompt-file) - (str "Read swarmforge/constitution.prompt, then read every file it refers to recursively, and obey all of those instructions.\n" - "Read swarmforge/roles/" role ".prompt, then read every file it refers to recursively, and follow all of those instructions.\n"))) + (case (instruction-profile) + :role-only + (str "This is a bounded smoke workflow. Read swarmforge/roles/" role + ".prompt and follow it exactly. Do not read project constitution files unless that role prompt explicitly requires them.\n") + (str "Read swarmforge/constitution.prompt, then read every file it refers to recursively, and obey all of those instructions.\n" + "Read swarmforge/roles/" role ".prompt, then read every file it refers to recursively, and follow all of those instructions.\n")))) (defn extra-args-prefix [row] (let [args (:extra-args row)] diff --git a/test/swarmforge/script_test.clj b/test/swarmforge/script_test.clj index b40ded7d..db2ad77b 100644 --- a/test/swarmforge/script_test.clj +++ b/test/swarmforge/script_test.clj @@ -253,6 +253,23 @@ (finally (fs/delete-tree root))))) +(deftest swarmforge-role-only-instruction-profile-is-explicit-and-bounded + (let [root (tmp-dir)] + (try + (let [result (run {:dir root + :env {"SWARMFORGE_INSTRUCTION_PROFILE" "role-only"}} + (script "swarmforge.bb") + "--test-launch-command" + (str root) + "opencode") + prompt (slurp (str (fs/path root ".swarmforge/prompts/coder.md")))] + (is (str/includes? (:out result) "opencode '")) + (is (str/includes? prompt "bounded smoke workflow")) + (is (str/includes? prompt "Read swarmforge/roles/coder.prompt")) + (is (not (str/includes? prompt "Read swarmforge/constitution.prompt")))) + (finally + (fs/delete-tree root))))) + (deftest grok-launch-command-uses-bypass-permissions-with-always-approve (let [root (tmp-dir)] (try