Issue: ruvnet/ruflo#2661 (P0)
Fix: ruvnet/ruflo#2662, shipped starting in @claude-flow/cli@3.27.0
Every Git worktree of a ruflo project started its own autonomous daemon, each daemon independently scheduled three Claude-powered background workers, and nothing coordinated across worktrees — so the number of autonomous Claude launches per hour scaled linearly with worktree count, with no ceiling.
None of the individual pieces were new bugs. Each had already been fixed in isolation:
- #1117 — orphaned Claude children after worker timeout
- #1914 — one workspace's daemon no longer kills another's
- #2407 — concurrent duplicate daemon starts inside one workspace
- #2484 — atomic same-workspace deduplication
Every one of those fixes was worktree-scoped — correct in isolation, but built on an identity model where the dedup key was the worktree's filesystem path, never the repository. A recent submodule split encouraged people to use Git worktrees for milestone isolation, multiplying the worktree count per project. That was the trigger, not the defect — it just exposed a lifecycle/scheduling flaw that had been latent all along.
Each daemon's default schedule:
| Worker | Interval |
|---|---|
audit |
every 10 min |
optimize |
every 15 min |
testgaps |
every 20 min |
Claude launches per hour per daemon = 60/10 + 60/15 + 60/20 = 6 + 4 + 3 = 13
Fleet launches per hour = 13 × active worktrees
| Active worktrees | Launches/hour | Launches/12h TTL |
|---|---|---|
| 1 | 13 | 156 |
| 4 | 52 | 624 |
| 8 | 104 | 1,248 |
| 12 | 156 | 1,872 |
And the trigger for all of it was just... claude being present on PATH. HeadlessWorkerExecutor auto-enabled itself on a successful claude --version probe — no separate consent gate, no budget, no cross-daemon awareness. A developer running a handful of parallel worktrees for legitimate isolation could burn through their interactive Claude quota without ever knowingly opting into background model usage.
Shipped in two phases on the same PR.
1. AI workers are opt-in now. DaemonConfig.aiWorkersEnabled defaults to false. Without explicit consent, the daemon never even probes claude --version — every worker runs its $0 local path. A default install now produces zero autonomous Claude launches, regardless of worktree count. Consent, in precedence order:
daemon start --headless # explicit flag
daemon.aiWorkers.enabled: true # project config
RUFLO_DAEMON_AI_WORKERS=1 # env var
Consent is deliberately not restored from a stale daemon-state.json, and the worker-execution path re-checks the gate independently (defense in depth, not just a startup check).
2. --headless became a real gate. It existed before this fix — it was forwarded to the forked child process and then silently ignored. Now it actually threads into DaemonConfig.aiWorkersEnabled on both the foreground and background paths.
3. A user-global launch budget, enforced before any process spawns:
const permit = await globalAiBudget.reserve({ repositoryId, worktreeRoot, workerType, model, estimatedTokens });
if (!permit.allowed) { emitSkippedReceipt(permit.reason); return; }Defaults (exactly what the issue asked for):
maxConcurrentGlobal: 1
maxLaunchesPerHour: 2
maxLaunchesPerDay: 12
pauseOnQuotaErrorMinutes: 60The ledger lives under ~/.claude-flow/, owner-only permissions, symlinks rejected, atomic reservation with stale-lock takeover. A 429 / rate-limit / usage-exhaustion failure response opens a global circuit breaker — every daemon on the machine stops launching for the cooldown window, not just the one that hit the limit. Denials return an error result immediately (local fallback) instead of queueing, so there's no retry storm hammering the budget. The whole thing fails closed: if the ledger itself errors, nothing launches.
4. ruflo daemon stop --all. Stops every ruflo daemon across every worktree on the machine — identified only by self-identifying argv patterns, so interactive Claude sessions are never candidates for termination. SIGTERM, 2-second grace period, SIGKILL fallback, POSIX process-group signaling so orphaned children actually die too.
5. Visibility. daemon start now warns when it detects a multi-daemon fleet. daemon status --all gained a budget panel (launches this hour/24h, active children, circuit-breaker state) and per-workspace launch attribution, so if the shared budget is getting consumed, you can see by which worktree.
6. Real repository identity, separated from worktree identity:
interface GitWorkspaceIdentity {
worktreeRoot: string; // git rev-parse --show-toplevel — per worktree
commonGitDir: string; // git rev-parse --git-common-dir — shared by ALL worktrees
repositoryId: string; // sha256(canonical commonGitDir) — stable repo key
head: string;
}Two worktrees of the same repository now resolve to the same repositoryId. This is the piece the original worktree-scoped fixes structurally couldn't provide, because they never distinguished "this worktree" from "this repository."
7. Cross-worktree job dedup. Before any launch:
jobKey = sha256(repositoryId, HEAD, workerType, configHash)
If the same job already succeeded within its freshness window, skip. Ten worktrees checked out at the same commit now run each analysis exactly once, not ten times. This sits underneath the budget as an optimization — the budget reservation remains the hard invariant even if two daemons race the same key and both attempt a launch.
8. Worktree-removal self-shutdown. The daemon's lifecycle monitor previously only ran when a TTL or idle timeout was configured — meaning it could be skipped entirely. It now always runs: a daemon whose workspace directory has been deleted (worktree removed) shuts itself down within one 60-second check instead of continuing to schedule jobs against a tree that no longer exists.
41 new tests across daemon-ai-workers-optin-2661, global-ai-budget, git-workspace-identity, and ai-job-dedup suites. 227 daemon/headless/services tests green. Runtime-smoked on a host with claude actually on PATH — the exact precondition that triggers the fanout in the first place.
This fixes the invariants that make the P0 a P0 — zero autonomous launches by default, a hard global concurrency/rate ceiling, cross-worktree dedup, and a quota circuit breaker that never touches interactive sessions. It does not yet implement the full architecture the issue originally sketched:
- No single elected repository-level supervisor with worktree leases (15-minute heartbeat expiry) — coordination currently happens via the shared budget ledger + job dedup, not one process owning the schedule.
- No structured per-launch token telemetry — receipts log operational metadata (timestamp, launch/denial/pause reason) but not input/output token counts, since
claude --printisn't invoked in a mode that reliably exposes usage. - No dedicated
daemon budget show/pause/resumesubcommands — budget state is visible viadaemon status --allbut not independently controllable yet. - No one-time upgrade migration warning for pre-existing multi-daemon fleets.
Full detail in the issue thread and PR #2662.
v3/@claude-flow/cli/src/services/git-workspace-identity.ts— repository identityv3/@claude-flow/cli/src/services/ai-job-dedup.ts— cross-worktree job dedupv3/@claude-flow/cli/src/services/global-ai-budget.ts— the global launch budget + circuit breakerv3/@claude-flow/cli/src/services/headless-worker-executor.ts— where the budget reservation gates the actualspawn('claude', ...)callv3/@claude-flow/cli/src/commands/daemon.ts—stop --all,status --allbudget panel,--headlessgate