Add the parallel-agent-isolation skill - #12
Conversation
Encodes a failure that hit twice in one session: two agents dispatched concurrently, each in its own git worktree, both fell back to the one Postgres and OpenSearch already bound to their ports and ran their integration suites against a shared database. Fixtures that truncate tables between tests wiped each other's rows mid-run. Both agents reported green and neither run proved anything. The skill asks the one question that prevents it, before dispatch: what stateful thing outside the filesystem will these agents share. It offers four resolutions with the conditions each fits, names the collision signals that invalidate an agent's verification after the fact, and covers the containers, worktrees, and worktree-pinned branches left behind. Packaged as a top-level skills/ directory with its own marketplace entry using source "./" and a skills path, per the marketplace docs' pattern for several entries sharing one skills folder. The entry carries no version so Claude Code resolves it from the commit SHA and installed copies refresh without a manual bump. Two checks, both per-skill through paths filters. make check validates frontmatter, the marketplace entry, and the case set without credentials. make evals runs each case as headless Claude Code in a workspace holding only this skill, three times, and requires every run to hold: the two scenario cases must load the skill and resolve the shared resource, and a file-only dispatch must not load it at all. That last case caught an over-broad description twice.
… stack The skill was written from one session on a Python service and carried that session's details as though they were the universe: a Postgres on a specific nonstandard port, a search index on another, docker compose as the only way services start. The idea underneath is general. Concurrent agents collide on stateful resources that filesystem isolation does not cover, and the collision is silent because both runs still report green. That is as true of a shared staging database, one booted simulator, one remote state file, or one API account as it is of a container stack. Substitute rather than abstract. The examples stay concrete, but each is now one instance among several drawn from different ecosystems, so a reader on a stack the skill does not name still recognises the situation. The collision signals keep the container error strings as examples and name the class they belong to: any claim that the agent reused a resource it could not create. Cleanup frames stranded containers as the common case of what an aborted start leaves behind, next to a booted device, a held lock, a half-applied stack. The case set is the part that had to prove this rather than assert it. Every triggering case was a container-and-database scenario, so a skill that only recognised containers and databases would have passed. The cases now span a Go service against one hosted staging database, a Terraform repo with one remote state file and one sandbox account, and an iOS app with one booted simulator, where the collision signal is not a container error string at all. file-only-parallel stays as the negative control, and it still holds against the widened description: it must not load for three agents editing docs. All four cases pass three runs of three, $1.31.
Generalised the skill past the project that inspired itAnything in this repo is meant to be reusable on any project, and this skill was Removed
Substituted, not abstractedThe examples stay concrete, because concrete examples are what make a skill The collision-signals section kept the container error strings but now names the The body grew 5 lines net, one of which is a new bullet in "what counts". The The case set now has to prove the generalisationThis is the part that mattered most. Every triggering case was a
Verification
Files deliberately untouched
|
…kflow The skill read as an inventory and a runbook. A six-item catalogue of what counts as shared state, a four-row strategy table, a list of collision error strings, a paste-in dispatch prompt, and a cleanup section naming the commands to list containers and worktrees. All of it detail, none of it the idea. The idea is that agents running concurrently share more than the filesystem, so what they will contend for is decided before dispatch, and a result produced under contention is unverified. The body now leads with that, and with the reason it has to be decided rather than diagnosed: the failure is silent. Contending agents do not error, they both report success, and their reports are indistinguishable from honest ones. There is nothing to find afterwards. What survives does so because it changes behaviour where the principle alone would not. The four resolutions stay, as the workflow an orchestrator actually runs, with each one's conditions in prose rather than a table column. The inventory becomes a characterisation: shared state is whatever exists as a single instance and carries changes between calls. The reporting template becomes the principle behind it, that agents must report how they verified and not just the outcome. One anti-pattern is kept whole, the agent that could not get its own instance, used the one already running, and reported green, because it is worth more than the error strings it replaces; those strings stay in when_to_use, which is the trigger surface, not prose. Cleanup is gone. Two things the compression lost, both restored as principles once the evals caught them. Contention is plural, and the obvious resource hides a second, so resolving one leaves the other shared. And a built-in lock covers only the thing it guards, leaving whatever that thing mutates still shared, so the serialised region has to span the change and the verification depending on it. Without those, a plan can serialise the state file, leave the environment it writes to contended, and read as though it had resolved the problem. check_wiring.py asserted that two collision error strings appeared in the body. That check fought a principle-based skill: it pinned prose and would fail the moment the list it named was compressed, pushing the skill back toward being a checklist. It now asserts the body is non-empty, leaving the file's structural assertions to carry the check, frontmatter and marketplace entry and a case set holding both a triggering and a non-triggering case. Verified non-vacuous: an emptied body, a removed negative control, and a wrong marketplace skills path each still fail it. description and when_to_use are unchanged, so the trigger surface that the case set was tuned against is untouched and the marketplace entry needs no edit. All four cases pass three runs of three against the final text, $2.56 across the runs. file-only-parallel, the negative control, was re-run after every change to the body and never loaded the skill.
|
Rewrote Cut
Kept, and why
Order is principle, then workflow, then the anti-pattern. The wiring check needed retargeting. The evals caught a real regression, which is worth recording. First pass,
Without those, a plan can serialise the state file, leave the environment it writes into contended, and read as though the problem were solved. Fitting, given what the skill is about. Negative control and eval results.
The two cases that passed before the lock and plurality fix were re-run against the final text rather than carried forward, since a result produced under different conditions is not evidence about this one. The eval cases themselves are unchanged. Still a draft, not merging. |
What
A skill for a failure that hit twice in one session. Two subagents were dispatched concurrently, each in its own git worktree. Worktrees isolated the filesystem and nothing else: the integration suite needs Postgres on 5433 and OpenSearch on 9200 from
docker compose, both ports were already bound by the parent checkout, and each agent quietly fell back to the running stack. The fixtures truncate tables around every test, so the two suites deleted each other's rows mid-run. Both agents reported green. Neither run proved anything, and the reports were indistinguishable from runs that did. It was caught only by re-running both suites serially afterwards.The skill asks the question that prevents it, before dispatch: what stateful thing outside the filesystem will these agents share? It then gives four resolutions with the conditions each fits (isolate per agent, serialise the stateful step, stay parallel and re-verify serially, or split so one agent owns the resource), the after-the-fact signals that invalidate an agent's verification (
port is already allocated,container name is already in use, "used the services that were already running"), a reporting line to put in dispatch prompts so the failure stops being silent, and cleanup for orphaned containers, worktrees, and the branches worktrees pin.Packaging
skills/parallel-agent-isolation/SKILL.md, plus a marketplace entry withsource: "./",skills: ["./skills/parallel-agent-isolation"], andstrict: false. That is the pattern the marketplace docs give for several entries sharing oneskills/folder at the marketplace root, andstrict: falseis required because the repository root has noplugin.jsonto be the authority. Frontmatter follows the skills docs.The entry deliberately carries no
version. For a relative path inside a git-hosted marketplace, Claude Code then resolves the version from the commit SHA, so installed copies refresh on every change and the stale-cache trap the README warns about does not apply.plugin-version.ymlonly walksplugins/*/.claude-plugin/plugin.json, so it is unaffected.Verified end to end by adding the marketplace and installing the plugin under a throwaway
CLAUDE_CONFIG_DIR: one skill, no other components, ~283 always-on tokens.Evals
Two checks, both per-skill through
pathsfilters, mirroring how the servers are gated.make checkis free and needs no credentials. It validates the frontmatter, the marketplace entry, and the case set, because every packaging mistake here is silent: the skill installs and simply never loads. Each rule was confirmed to fail on a deliberately mutated copy.make evalsruns each case as headless Claude Code in a throwaway workspace holding only this skill, with--setting-sources projectso a personal skill cannot stand in for it. Each case runs three times and every run must hold.shared-database-dispatchgreen-report-collisionfile-only-parallelfile-only-parallelis the honesty check and it earned its place: it failed the first draft of the description, and failed again when the clause excluding file-only work was shortened. Both times the fix was to the description, not to the case.The Harbor harness used by
harbor-hubwas the wrong shape here. Its value is a self-truthing verifier that recomputes ground truth from a live hub. This skill has no server and no backend to recompute against, so wrapping it would have added an image build, Modal or Docker, and a Harbor key to a check whose whole content is one prompt and a rubric.Checks run
make checkinskills/parallel-agent-isolation: passes, and fails on each mutation it is meant to catch.claude plugin validate . --strict: passes on the updated manifest.make evals: 3/3 cases, 3/3 runs each, $0.80.pathsfilter fires.Shared files, and a live demonstration
A sibling pull request adds a different skill and touches the same
.claude-plugin/marketplace.jsonand the same READMESkillssection. Whoever merges second should rebase; those are adjacent additions rather than competing edits. The two skill directories and the two workflow files are disjoint by design, which is why this adds one workflow per skill instead of a sharedskills.yml.The two agents writing these pull requests also shared one git checkout rather than getting a worktree each, and it went the way this skill says it goes. This branch was rewound to
mainon the remote after its commit was pushed, and the sibling branch was cut from that commit, so it currently carries this skill as well. Restored here by pushing the commit back by explicit refspec. The sibling branch needs a rebase ontomainbefore it is reviewed, or it will present this skill as part of its own diff.