feat(do): terminal-free background-agent spawn backend ($FLOW_TERM=bg)#74
Merged
Conversation
Add a first-class `--bg` spawn backend so users who live in Claude Code's Agent View can have `flow do` launch sessions as background agents instead of opening a terminal tab. The user's `claude` alias injects `--bg`, which makes claude ignore flow's pre-allocated `--session-id` and mint its own — leaking a phantom id so `flow do` re-resumes a session that never existed. This backend stops fighting the alias: flow spawns `claude --bg` deliberately (via Go exec, so the interactive alias never applies) and captures the REAL session id from the registry. - spawner.IsBackground(): $FLOW_TERM=bg selector (+ BackgroundOverride test seam). bg is its own predicate, not a Detect() backend — it bypasses SpawnTab entirely. - harness.BackgroundLauncher: optional capability (claude implements it; codex/gemini don't → clean error, no silent tab fallback). - claude bg impl: `claude --bg --name <title> <prompt>`, parse the `backgrounded · <shortId> · <name>` banner, resolve the full session id via one `claude agents --json` lookup (no polling — the banner only prints after registration). Resume by id; report already-running. - transcript: resolve bg/worktree sessions by globbing the globally-unique session id (`~/.claude/projects/*/<sid>.jsonl`) when the cwd-encoded path misses — handles the worktree-relocation gotcha. - do.go: bg branch parallel to --auto; capability gate; record the captured real session id + harness. - flow show: live `bg_status:` (status/state/pid); flow list: bg agents fold into the [live] marker. TDD throughout, behind a BGCommandRunner seam with banner/JSON captured from the live CLI; a package TestMain keeps the suite hermetic (no real claude). Validated end-to-end against the real binary. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add bg mode to the "What you get" tab bullet, a dedicated "Background agents (FLOW_TERM=bg)" subsection under how-it-works (spawn + real session-id capture, idempotent resume / already-running, no-tab attach via Agent View, live bg status in show/list, transcript glob, Claude-only capability gate), and the supported-hosts line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…idance for present sessions
`claude --bg --resume <id>` does NOT preserve the session id (verified
against the live CLI and the Agent View docs): --bg manages its own id, so
resume starts a fresh background process that inherits the prior
conversation under a NEW id. The previous code ran `--bg --resume <oldid>`
but kept recording the old id — re-introducing the exact phantom-id leak
this feature exists to fix (flow pointed at a dead session; a real orphan
agent was observed in testing).
Corrected, doc-aligned behavior:
- BackgroundAgents() now queries `claude agents --json --all`, so exited /
failed / stopped / completed sessions are visible — flow can tell
"removed" from "present but not running".
- Re-run of a task whose session is PRESENT in the registry (any state)
no longer spawns: a bg session is recovered by attaching in the Agent
View (a stopped/failed one restarts from where it left off, keeping its
id), so flow points the user there. Message distinguishes live
("Already running … attach") from exited ("in your Agent View
(stopped) — attach to resume").
- Re-run when the session is ABSENT (rm'd / untracked) forks it back via
`claude --bg --resume <oldid>`, and ResumeBackground now CAPTURES and
returns the new minted id (history inherited) so flow re-records it —
never pointing at the dead id again.
- liveSessionsForTasks counts a bg agent as [live] only when it has a
running pid (>0); exited-but-listed sessions no longer false-positive.
ResumeBackground's signature changes to return (BackgroundAgent, error).
Spawn and resume now share launchAndCapture (banner parse → registry
lookup). Validated end-to-end on the real binary across all three
branches (live / stopped-present / removed). README bg section updated to
describe the real resume semantics. make test green, go vet clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rt / else→fork+recapture
Two fixes from live testing of the bg backend:
1. work_dir bug: the bg spawn exec'd `claude --bg` without setting the
process cwd, so the agent started in whatever directory flow was invoked
from (e.g. a worktree) instead of the task's work_dir — wrong repo,
wrong CLAUDE.md, and (because bypassPermissions is accepted per-context)
it blocked on a permission prompt even with --dangerously-skip-permissions.
BGCommandRunner now takes a workDir and sets cmd.Dir; SpawnBackground /
ResumeBackground take work_dir and run there (agents --json stays
cwd-independent). The interactive and --auto paths already did this.
2. Resume model, simplified to match how the Agent View actually works
(no attach — attach is interactive, flow can't drive it headlessly):
- session still LIVE in the registry (process up, pid present) →
don't spawn/resume; just report it's open in the Agent View.
- session not running (stopped/failed/done) OR removed → fork it back
with `claude --bg --resume <oldid>` and re-record the new minted id
(history inherited). Plain `--resume` would keep the id but wouldn't
be a background agent, so it can't be used in bg mode.
Verified end to end on the real binary: agent now runs in work_dir;
--with delivers on both spawn and resume (injection marker + obeyed
instruction in the transcript); a removed session's fork-resume inherits
the prior conversation (old marker present in the new session's jsonl) and
registers in the Agent View under the re-recorded id.
Also: app TestMain forces spawner.BackgroundOverride=false so the suite is
hermetic against an ambient $FLOW_TERM=bg in the dev environment. README bg
section updated. make test green, go vet clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… the way The Install-section block recommended aliasing `claude` to `claude --bg` "so flow's sessions land in the agents view" — now obsolete and harmful: flow execs claude directly (the alias doesn't affect its bg spawns), and on the terminal path the alias re-injects --bg, making claude ignore flow's pinned --session-id (the phantom-id bug this feature fixes). The dedicated "Background agents (FLOW_TERM=bg)" section already documents the correct, flow-integrated mechanism, so the alias guidance is removed entirely. Also align the capture description to `claude agents --json --all`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… timeout; newest transcript Regressions caught by an independent review of the bg backend: - HIGH: `flow list` and `flow show` spawned `claude agents --json --all` on EVERY invocation for ALL users — the registry lookup was gated only on "harness implements BackgroundLauncher", which every claude task does. Non-bg users paid ~230ms–2s + a subprocess on the hottest command, with no timeout (a stalled daemon could hang it). Now gated on spawner.IsBackground(): only bg-mode invocations consult the registry; FLOW_TERM-unset users get the exact pre-feature behavior (no subprocess, no bg_status, no bg-sourced [live]). Added a 5s timeout on the agents query so it can never hang flow show/list. - MEDIUM: `claude agents --json --all` also lists interactive (terminal-tab) sessions; parseBackgroundAgents didn't filter, so an ordinary `flow do` tab session got a misleading `bg_status:` line in flow show. Now only `kind:"background"` entries are returned. Also stop printing `pid 0` for exited bg sessions (show formats via bgStateLabel, omitting pid when 0). - LOW: transcript glob fallback now picks the most-recently-modified match when a session id appears under two project dirs (was lexicographic), so it resolves the live copy rather than a stale one. Tests: kind-filter drop-interactive test; a regression guard asserting non-bg mode issues no `claude agents` subprocess; existing bg tests opt into bg mode via stubBGMode. make test green, go vet clean. Verified live: non-bg list/show fast with no bg_status; interactive session not mislabeled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jun 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A first-class
$FLOW_TERM=bgspawn backend so users who live in Claude Code's Agent View (claude agents) can haveflow dolaunch sessions as terminal-free background agents instead of opening a terminal tab.Why
A
claudealias that injects--bgmakesclaudeignore flow's pre-allocated--session-idand mint its own. flow records a phantom id, and the nextflow dore---resumes a session that never existed. Rather than fight the alias, flow now spawnsclaude --bgdeliberately (via Goexec, which never applies the user's interactive shell alias) and captures the real, harness-minted session id from the agent registry — restoring the DB-authoritative binding contract.How
spawner.IsBackground()—$FLOW_TERM=bgselector (+ a test override). bg is its own predicate, not aDetect()terminal backend: it bypassesSpawnTabentirely.harness.BackgroundLauncher— new optional capability interface (SpawnBackground/ResumeBackground/BackgroundAgents). Only the claude adapter implements it;$FLOW_TERM=bgagainst a codex/gemini-pinned task fails cleanly — no silent tab fallback.claude --bg --name "<project>/<task>" <prompt>, parse thebackgrounded · <shortId> · <name>banner, then resolve the full session id via oneclaude agents --json --alllookup (no polling — the banner only prints after the session is registered). The spawn runs in the task'swork_dir(cmd.Dir), so the agent starts in the right repo.do.go— bg branch parallel to--auto: capability gate; spawn/resume protocol; record the captured real session id + harness.flow show— livebg_status: <status> / <state> (pid …).flow list— bg agents with a live pid fold into the[live]marker.~/.claude/projects/*/<sid>.jsonl), so it works even when a bg session relocates into a git worktree.Spawn / re-run / resume protocol
Re-running
flow do <task>is idempotent and mirrors the Agent View:--fresh)--bgspawn; capture + record the real idclaude --bg --resume <oldid>forks a fresh process seeded from the saved transcript; capture + re-record the new id (history inherited)claude --bg --resumedoes not preserve the id (verified against the live CLI + Agent View docs):--bgmanages its own id. Plainclaude --resumekeeps the id but isn't a background agent, so it can't be used in bg mode. Hence flow re-records the new id — the task never points at a dead session.--with/--with-fileWork on bg spawn and resume — the instruction rides via
opts.Injectand is appended behind the shared[via flow do --with]marker, exactly like the interactive path.Testing
TDD throughout, behind a
BGCommandRunnerseam fed banner/JSON captured from the live CLI; a packageTestMainkeeps the suite hermetic (no real claude; andspawner.BackgroundOverride=falseso an ambient$FLOW_TERM=bgin the dev env doesn't leak into terminal-path tests).make testgreen,go vetclean.Verified end-to-end against the real binary:
bg_statuslive inflow show,[live]inflow list, agent runs inwork_dirclaude rm→ re-run resumes: forks, re-records the new id, registers in the Agent View, inherits the prior conversation (old marker present in the resumed transcript)--withdelivers on both spawn and resume (injection marker + obeyed instruction in the transcript)flow transcriptresolves the jsonl (incl. the worktree-relocation case)Out of scope (per brief)
--model/--effort/--permission-modeto bg spawn (later).🤖 Generated with Claude Code