From bde55dfc87c433137186ed83385f1669fc6388f9 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sun, 17 May 2026 00:12:52 +0200 Subject: [PATCH] feat(subagent): default subagent dispatch to swarm in code, not just wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bash wrapper bin/sf-from-source exports SF_SUBAGENT_VIA_SWARM=1 to make the swarm/messagebus path the default for subagent dispatch. That covers every sf launch via the wrapper but does NOT cover the web-launched sf — src/web/cli-entry.ts:resolveSfCliEntry spawns sf by calling process.execPath (node) directly with src/loader.ts or dist/loader.js, bypassing the wrapper entirely. So /tmp/sf-web- onboarding-runtime-* sf processes were still falling through to the direct-runSubagent subprocess path. Flip the default in code instead: swarm runs unless SF_SUBAGENT_VIA_SWARM is explicitly set to "0" or "false". Now every sf launch — wrapper, web, dev-cli, packaged-standalone — picks up the same default. The wrapper's export line is now redundant but harmless; keeping it as defense-in-depth (documents the intent at the wrapper layer too). Test update: subagent-via-swarm.test.mjs's "unset → subprocess" assertion is updated to "=0 → subprocess" — the unset case now means swarm-by-default. All 13 tests in that file pass. The other tests in the file that explicitly set the flag to "1"/"true" are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/resources/extensions/sf/subagent/index.js | 10 ++++++++-- .../extensions/sf/tests/subagent-via-swarm.test.mjs | 13 +++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/resources/extensions/sf/subagent/index.js b/src/resources/extensions/sf/subagent/index.js index dfbf37a3b..dcddb6e03 100644 --- a/src/resources/extensions/sf/subagent/index.js +++ b/src/resources/extensions/sf/subagent/index.js @@ -1452,9 +1452,15 @@ async function runSingleAgent( }; } } - // Feature flag: route through swarm dispatch instead of direct runSubagent. + // Subagent dispatch defaults to the swarm/messagebus path; the bash + // wrapper bin/sf-from-source used to set SF_SUBAGENT_VIA_SWARM=1 for + // that, but the web-launched sf goes node → src/loader.ts directly + // and bypasses the wrapper, so the default is enforced in code here. + // Opt out with SF_SUBAGENT_VIA_SWARM=0 (or =false) when running tests + // or workflows that need the direct-runSubagent subprocess path. const swarmFlag = process.env.SF_SUBAGENT_VIA_SWARM; - if (swarmFlag === "1" || swarmFlag === "true") { + const swarmDisabled = swarmFlag === "0" || swarmFlag === "false"; + if (!swarmDisabled) { return runSingleAgentViaSwarm( defaultCwd, agent, diff --git a/src/resources/extensions/sf/tests/subagent-via-swarm.test.mjs b/src/resources/extensions/sf/tests/subagent-via-swarm.test.mjs index 4ae668858..8f0f1b6ab 100644 --- a/src/resources/extensions/sf/tests/subagent-via-swarm.test.mjs +++ b/src/resources/extensions/sf/tests/subagent-via-swarm.test.mjs @@ -84,11 +84,12 @@ afterEach(() => { // ─── Tests ───────────────────────────────────────────────────────────────────── -test("SF_SUBAGENT_VIA_SWARM unset → runSubagent path, swarmDispatchAndWait NOT called", async () => { - // With the flag unset, runSingleAgent should NOT touch swarmDispatchAndWait. - // It will try to call runSubagent (from @singularity-forge/coding-agent), which - // will fail inside the test environment — we just need to confirm the swarm path - // was NOT taken. +test("SF_SUBAGENT_VIA_SWARM=0 → runSubagent path, swarmDispatchAndWait NOT called", async () => { + // With the flag explicitly disabled, runSingleAgent should NOT touch + // swarmDispatchAndWait. It will try to call runSubagent (from + // @singularity-forge/coding-agent), which will fail inside the test + // environment — we just need to confirm the swarm path was NOT taken. + process.env.SF_SUBAGENT_VIA_SWARM = "0"; const agents = makeAgents(); try { @@ -115,7 +116,7 @@ test("SF_SUBAGENT_VIA_SWARM unset → runSubagent path, swarmDispatchAndWait NOT assert.equal( swarmDispatchAndWait.mock.calls.length, 0, - "swarmDispatchAndWait should not be called when flag is unset", + "swarmDispatchAndWait should not be called when flag is explicitly disabled", ); });