Swarm: hunt a tfcompat divergence in parallel
Dispatch N agents that each independently hunt a different OpenTofu-vs-pulumi-hcl
runtime divergence and prove it with a single failing tfcompat test. The goal of a
swarm run is to surface one genuine failure and report it back to the user — not to
fix anything, not to open PRs. You (the lead) own de-confliction (every agent works a
disjoint area so no two chase the same divergence) and adjudication (deciding which
returned candidate is a real, in-direction failure worth reporting).
Each agent runs the find-tfcompat-bug skill — which is
itself entirely find-and-prove and stops at a failing test. Agents must not continue
into the fix-tfcompat-bug skill (fix, sweep, changelog,
branch, PR). Read find-tfcompat-bug first for the find-and-prove technique; this one adds
the swarm orchestration and de-confliction around it.
The deliverable
A swarm run is done when one agent has produced a genuine, in-direction failing tfcompat test and you have confirmed it. Report back to the user:
- The divergence — one line: what OpenTofu does vs. what pulumi-hcl does.
- The failing test — the
tests/tfcompat/<l1|l2>_<name>_test.go+testdata/cases/<name>/files the agent created, left in the working tree (uncommitted). - The failure output — the actual diff/error from running the test on
master, proving it fails for the right reason (real OpenTofu-vs-pulumi difference, not a harness error).
Do not fix the divergence, write a changelog, branch, or open a PR. Stop at a proven, reported failure. The user drives what happens next.
The lead's job
Partition the search space into disjoint domains — one per agent. This is what prevents collisions. Never spawn N agents with the same instructions. Good seams to split along (mix and match to reach N):
- L1 function families: collection funcs; string funcs; encoding funcs; crypto/hash
funcs; cidr/IP funcs; date/time + numeric funcs; the
to*type converters +can/try/type; the sensitivity-marks seam (sensitive/nonsensitive/issensitive); file-content/file-hash/path.*resolution. - L2 seams: resource/module/provider plumbing;
count/for_each/dynamicmeta-arguments; lifecycle (prevent_destroy/ignore_changes/replace_triggered_by/create_before_destroy); pre/postcondition + variablevalidation; preview/destroy lifecycle; sensitivity propagation through state. - The HCL expression-eval seam: for-expressions, splat, conditionals, string templates/
heredocs/trim markers, index/attr access,
==/!=coercion. (Note: this seam delegates to upstreamhclsyntax/ctyand has historically been clean — assign it only when you have spare agents.)
- L1 function families: collection funcs; string funcs; encoding funcs; crypto/hash
funcs; cidr/IP funcs; date/time + numeric funcs; the
Build one shared exclusion list and put it in every agent's prompt. It must name (a) every bug already fixed/shipped, and (b) seams already proven clean. Without this, agents re-discover already-fixed bugs and report a "failure" that is actually already handled. Re-derive it each run from
git log, the open PRs (gh pr list), and the existingtests/tfcompat/testdata/cases/directory.Tell each agent the other agents' domains ("stay out of these") as a second guard against overlap.
Spawn all N in a single message, each with
subagent_type: general-purposeandrun_in_background: true(so they run truly concurrently and notify you on completion). Give each a uniquename(e.g.hunter-<domain>). Worktree isolation is not needed — agents only add new test files undertests/tfcompat/and don't edit shared source, so they can share the working tree. (If you do hand out worktrees, remember the winning test files live in that worktree, not the main tree — collect them.)Collect candidates as they land and adjudicate. For each returned candidate, confirm it is a real, in-direction failure before declaring victory (see "Vetting" below). The first candidate that passes vetting is the deliverable — report it and wind the swarm down.
A negative is a success, not a failure. An agent that returns an honest "no in-direction divergence in my domain" has done its job — do not pressure it to invent one. A fabricated or out-of-direction "failure" is worse than a clean negative.
Keep hunting only until you have ONE confirmed failure. This is a single-failure hunt, not a steady-state swarm. You do not need to maintain headcount at N, spawn replacements indefinitely, or monitor any PRs. Once one candidate is vetted and reported, stop spawning and let remaining agents finish or wind them down. If all agents return negatives, you may spawn a fresh wave over still-unclaimed domains (with an updated exclusion list) — but if the search space is genuinely picked over, say so rather than spawning into exhausted ground.
What every agent prompt must contain
- "Read and follow
.claude/skills/find-tfcompat-bug/SKILL.md— find a candidate divergence and prove it with a failing test. Stop at the proven failure. Do NOT continue intofix-tfcompat-bug: no fix, no changelog, no branch, no PR." Plus the globalCLAUDE.mdrules (research don't guess; faithfully verify against OpenTofu source/docs/probe; no hacks). - Its assigned domain (hunt only here) and the other agents' domains (stay out).
- The shared exclusion list.
- The direction rule, restated: a bug is OpenTofu-succeeds / pulumi-hcl-errors-or- differs. pulumi-being-more-lenient is NOT the bug — discard it and keep hunting.
- Build + run:
make build, then run the candidate test with the freshly built binaries on PATH and-count=1:PATH="$PWD/bin:$PATH" go test ./tests/tfcompat/ -run 'Test(L1|L2)<Name>' -count=1 -v. The test must FAIL on master, and the failure must show the genuine OpenTofu-vs-pulumi difference (a real output diff or matchingExpectErr), not a harness/setup error. - The deliverable: exactly ONE proven failing tfcompat case — the new
_test.go+testdata/cases/<name>/files left uncommitted in the working tree, plus the captured failure output. Honest negative if the domain is genuinely clean. - The required final message: (1) one-line divergence (OpenTofu vs pulumi-hcl), (2) the test name + paths of the files it created, (3) the captured failing-test output proving it fails for the right reason — or an honest negative naming what it probed.
Vetting a returned candidate (the lead's adjudication)
Before reporting a candidate as the failure, confirm:
- It reproduces. Re-run the agent's test yourself on
masterwith freshly built binaries and-count=1. It must fail. - It fails for the right reason. The failure is a genuine OpenTofu-vs-pulumi
output diff or a matched
ExpectErr— not a missing provider, a fixture typo, a panic in the harness, or an unrelated build error. - The direction is right. OpenTofu succeeds (or produces value X); pulumi-hcl errors or produces a different value. pulumi-hcl being more lenient than OpenTofu is not the bug — reject it and keep looking.
- It isn't on the exclusion list. Not an already-shipped fix or a known-clean seam re-surfaced.
A candidate that fails any check is sent back (or the agent stands down); keep adjudicating incoming candidates until one passes all four.
De-confliction checklist (the one thing that makes a swarm work)
- N agents → N disjoint domains, no two overlapping.
- Shared exclusion list names every shipped/claimed/clean area.
- Each agent told the others' domains.
- All spawned in one message:
general-purpose, background, unique names. - Each agent stops at a proven failing test — no fix, no changelog, no PR.
- Lead vets each candidate (reproduces / right reason / right direction / not excluded) before reporting it as the single deliverable.