Skip to content

Add the parallel-agent-isolation skill - #12

Draft
walkerhughes wants to merge 3 commits into
mainfrom
feat/skill-parallel-agent-isolation
Draft

Add the parallel-agent-isolation skill#12
walkerhughes wants to merge 3 commits into
mainfrom
feat/skill-parallel-agent-isolation

Conversation

@walkerhughes

Copy link
Copy Markdown
Owner

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 with source: "./", skills: ["./skills/parallel-agent-isolation"], and strict: false. That is the pattern the marketplace docs give for several entries sharing one skills/ folder at the marketplace root, and strict: false is required because the repository root has no plugin.json to 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.yml only walks plugins/*/.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 paths filters, mirroring how the servers are gated.

make check is 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 evals runs each case as headless Claude Code in a throwaway workspace holding only this skill, with --setting-sources project so a personal skill cannot stand in for it. Each case runs three times and every run must hold.

Case Skill must The answer must
shared-database-dispatch load name one of the four resolutions for the shared Postgres and OpenSearch, not merely say to coordinate
green-report-collision load treat a green suite reported next to a port collision as invalid, extend the doubt to the other agent, and re-run serially
file-only-parallel stay out (not graded)

file-only-parallel is 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-hub was 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 check in skills/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.
  • Install from the marketplace under an isolated config dir: succeeds.
  • No other component was touched, so no other workflow's paths filter fires.

Shared files, and a live demonstration

A sibling pull request adds a different skill and touches the same .claude-plugin/marketplace.json and the same README Skills section. 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 shared skills.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 main on 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 onto main before it is reviewed, or it will present this skill as part of its own diff.

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.
@walkerhughes

Copy link
Copy Markdown
Owner Author

Generalised the skill past the project that inspired it

Anything in this repo is meant to be reusable on any project, and this skill was
not. It was written from one session on a Python service and carried that
session's specifics as though they were the universe: Postgres on a nonstandard
port, a search index on another, docker compose as the way services start,
make test-integration as the suite. The idea underneath is general, so the
text now reads that way.

Removed

  • The specific port numbers, the search-index port, and the named services.
  • The origin session's work items: the retrieval strategy, the migration
    ordering bug.
  • Docker Compose as the assumed mechanism. It survives as one example among
    several, in the strategy table, the collision signals, and cleanup.

Substituted, not abstracted

The examples stay concrete, because concrete examples are what make a skill
usable. What changed is that each is presented as an instance rather than as the
world, and they come from more than one ecosystem: a shared database, a booted
simulator, a remote state file with a lock, an API account with per-account rate
limits. Someone on a stack the skill never names should still recognise their
situation.

The collision-signals section kept the container error strings but now names the
class they belong to: any claim that the agent reused a resource it could not
create for itself. Cleanup frames stranded containers as the common case of
"what an aborted start leaves half-made", beside a booted device, a held lock, a
partly applied stack. Worktree and pinned-branch cleanup stayed concrete, since
that is general to anyone dispatching Claude Code agents.

The body grew 5 lines net, one of which is a new bullet in "what counts". The
brevity is a feature and it is intact.

The case set now has to prove the generalisation

This is the part that mattered most. Every triggering case was a
container-and-database scenario, so a skill that only ever recognised containers
and databases would have passed the whole set. The triggering cases now span
three ecosystems and three kinds of shared state:

Case Stack What is shared
shared-database-dispatch Go, go test -tags e2e one hosted staging database, no containers anywhere in the prompt
shared-infra-state (new) Terraform one remote state file, one lock table, one sandbox account
green-report-collision iOS, xcodebuild test one booted simulator; the collision signal is not a container error string

shared-database-dispatch is a full rewrite with no Python, no pytest, and no
Docker Compose, per the brief.

file-only-parallel stays exactly as it was, and it is still the honesty check.
The description and when_to_use widened here, which is precisely the change
that could make it fire on file-only work, so it was re-run first and separately
before anything else was trusted.

Verification

  • make check: passes. Also mutation-tested, since a check that never fails is
    not a check: renaming the frontmatter name, dropping the collision signal
    from the body, pointing the marketplace skills path elsewhere plus pinning a
    version, and flipping the negative control to a triggering case each produce
    the specific failure they should. Reverted, and the tree is clean.
  • claude plugin validate . --strict: passes.
  • make evals: all four cases pass, three runs of three each, $1.31 total.
    file-only-parallel 3/3 (run on its own first), shared-database-dispatch
    3/3, shared-infra-state 3/3, green-report-collision 3/3.

Files deliberately untouched

marketplace.json and the root README.md describe the skill in wording that
was already stack-neutral and that the first sentence of the new description
still matches, so neither needed an edit. That also keeps this diff off the two
files a concurrent branch generalising a different skill is most likely to
touch. If that branch lands first and either file has moved, this branch has no
competing change to reconcile.

…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.
@walkerhughes

Copy link
Copy Markdown
Owner Author

Rewrote SKILL.md against the note that a skill states intent and core principles, plus a workflow, rather than minutiae. 74 lines to 65 (68 body lines to 59), but the shape matters more than the count.

Cut

  • The cleanup section. Listing containers, listing and removing worktrees, deleting worktree-pinned branches. Pure runbook, and the first thing to go.
  • The six-item catalogue of what counts as shared state. Now one sentence: shared state is whatever exists as a single instance and carries changes between calls, so what one agent does lands where another will read it. A reader on a stack the list never named is better served by the characterisation than by six bullets they have to pattern-match against.
  • The list of collision error strings. Replaced by the one anti-pattern they were evidence for. The strings themselves stay in when_to_use, which is the trigger surface rather than prose, so recognition is unaffected.
  • The paste-in dispatch prompt. Replaced by the principle behind it: require agents to report how they verified, not just the outcome.
  • The strategy table. The four choices survive; the conditions column became prose.

Kept, and why

  • The four resolutions, as the workflow rather than a catalogue. Choosing between isolate, serialise, parallel-then-verify-serially, and split is the thing an orchestrator actually has to do, so it earns its space, and each carries its fit condition.
  • The failure is silent, promoted to its own section ahead of the workflow. It is the load-bearing idea: contending agents do not error, both report success, and the reports are indistinguishable from honest ones. That is why this is decided before dispatch instead of diagnosed after.
  • One anti-pattern, whole: the agent that could not get its own instance of a shared resource, used the one already running, and finished green. A corrupted run presented as a workaround. No gold-standard example was added.

Order is principle, then workflow, then the anti-pattern.

The wiring check needed retargeting. check_wiring.py asserted that two collision error strings appeared in the body. That check fights a principle-based skill: it pins prose and fails the moment the list it names is compressed, which pushes the skill back toward being a checklist. It now asserts the body is non-empty and leaves the structural assertions already in the file to do the work: frontmatter parses and its name matches the directory, the listing fits the truncation cap, the marketplace entry is correct, and the case set holds both a triggering and a non-triggering case. Confirmed non-vacuous by breaking each in a scratch copy: an emptied body, a removed negative control, and a wrong marketplace skills path each still fail it.

The evals caught a real regression, which is worth recording. First pass, shared-infra-state failed on run 3 of 3. The skill loaded and correctly identified both shared things, then leaned on the state lock alone to serialise the applies, which that rubric explicitly excludes. My compression had dropped two things that turned out to be load-bearing, and both went back as principles rather than as patches for that case:

  • Contention is plural. The obvious resource hides a second, and resolving one leaves the other shared.
  • 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 that depends on it.

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. description and when_to_use are byte-identical to before, so the tuned trigger surface is untouched and marketplace.json needs no edit. file-only-parallel was re-run after every body change and held 3/3 each time, never loading the skill. All four cases pass 3/3 against the final text:

Case Result
file-only-parallel (negative control) PASS 3/3
shared-database-dispatch PASS 3/3
shared-infra-state PASS 3/3
green-report-collision PASS 3/3

make check passes. Roughly $2.56 across all runs.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant