feat(subagent): default subagent dispatch to swarm in code, not just wrapper

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) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-17 00:12:52 +02:00
parent 9a84d82cdb
commit bde55dfc87
2 changed files with 15 additions and 8 deletions

View file

@ -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,

View file

@ -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",
);
});