| name | fleet-orchestrator |
| description | Fan out a single high-level directive (audit, deprecation cleanup, fix, migration, version bump) across an arbitrary list of repositories as parallel Agent workers, with a supervisor that polls a shared manifest, renders a live status table, and aggregates final results. Use when the user says 'apply X across repos', 'audit N repositories', 'sweep every repo', 'run the same fix in all projects', 'parallel multi-repo', 'fleet-wide change', or provides a list of repositories with a single directive. Preferred over running issue-work in each repo sequentially. |
| argument-hint | <repos-spec> <directive-spec> [--max-parallel N] [--max-retries N] [--poll-interval SEC] [--reanchor-interval N] [--top-k N] [--agents-dir PATH] [--resume <fleet-id>] |
| user-invocable | true |
| disable-model-invocation | false |
| allowed-tools | Bash(gh *), Bash(flock *), Bash(jq *) |
| max_iterations | 10 |
| halt_conditions | [{"type":"success","expr":"All repos reach a terminal worker status (done, failed-after-retries, skipped)"},{"type":"limit","expr":"--max-parallel worker pool drains with no pending items"}] |
| on_halt | Render final fleet-status table with per-repo outcome and exit |
| loop_safe | false |
| tiers | {"light":{"ref_docs":[],"deep_checks":false},"standard":{"ref_docs":["core"],"deep_checks":false},"deep":{"ref_docs":["core"],"deep_checks":true}} |
| default_tier | standard |
| iso_class | none |
Fleet Orchestrator -- Parallel Multi-Repo Directive Executor
A supervisor skill that applies one directive across many repositories concurrently.
Each repository is handled by an independent worker Agent; progress and outcomes
flow through a single JSON manifest (fleet-status.json) updated with flock for
atomic, race-free writes.
When to Use This Skill
- Applying the same change (audit, cleanup, migration, dependency bump, policy update) to N repos.
- The user wants wall-clock compression — eight repos serialized take hours, fanned out take the length of the slowest one.
- Failure isolation matters — one flaky repo must not block the other seven.
- The directive is uniform enough that per-repo customization is minor (each worker still adapts to its repo's code style and issue landscape).
Do NOT use this skill when:
- There is only one repo — use
/issue-work directly.
- The directive requires tight cross-repo coordination (e.g., a schema change that must merge in strict order) — sequence it manually.
- Repos have incompatible toolchains and the supervisor cannot meaningfully aggregate their outcomes.
Execution Mode: Sub-agent (Parallel) with Supervisor
Agent teams are capped at one active team per session. Fleet work needs N independent
workers running concurrently with run-to-completion semantics, so we use the sub-agent
mode (Agent(..., run_in_background=true)) plus a file-based manifest for coordination.
Data passing strategy:
| Layer | Mechanism | Why |
|---|
| Shared state | _workspace/fleet/fleet-status.json | One JSON blob, race-free via flock, readable by every worker and the supervisor |
| Per-worker artifacts | _workspace/fleet/{repo-slug}/ | Per-worker scratch, logs, diff snapshots — preserved for audit |
| Supervisor → worker | Agent prompt (one-shot) | Each worker gets the spec and its repo slot at launch; no runtime re-parameterization |
| Worker → supervisor | Manifest writes | Supervisor polls; workers never call back |
Agent Configuration
| Role | subagent_type | model | run_in_background | Purpose |
|---|
| supervisor | (main session) | n/a | n/a | Input validation, worker fan-out, manifest polling, aggregation |
| worker (one per repo) | general-purpose | opus | true | Executes the directive on its assigned repo, updates manifest |
All workers use the same worker prompt template (reference/worker-template.md).
The supervisor never edits code itself — it only dispatches, monitors, and reports.
Architecture
[Supervisor (main session)]
| dispatch ^ poll every POLL_INTERVAL seconds
v |
+-------------+---------------+-------------+
| | | |
[Worker 1] [Worker 2] [Worker 3] [Worker N]
repo A repo B repo C repo N
\ \ / /
\ flock-guarded / /
\ writes to / /
\------> fleet-status.json <----------/
Workflow
Phase 1: Input Resolution and Validation
The skill accepts inputs in three forms; pick the first that matches:
| Input form | Example | How to parse |
|---|
| Inline list | repo-a repo-b repo-c "fix all TODOs" | Positional args: repos until a non-repo-like token, then the directive |
| File list | @repos.txt "fix all TODOs" | Read repos.txt, one owner/repo per line |
| Org-wide | --org kcenon "fix all TODOs" | gh repo list <org> --json nameWithOwner,isArchived --jq '[.[] | select(.isArchived == false)] | .[].nameWithOwner' |
Validation rules:
- Every repo must resolve to
owner/repo form. Reject entries without a /.
- Deduplicate repos.
- Verify each repo exists and the current user has write access:
gh api "repos/{owner}/{repo}" --jq .permissions.push.
- Repos failing step 3 are moved to a
pre-excluded list in the manifest with a reason, not dispatched.
- The directive must be non-empty. If empty, prompt the user for it (one-shot).
- If
N > --max-parallel, workers are launched in waves of --max-parallel. Default: 8 (conservative; most Macs handle this comfortably).
Phase 2: Manifest Initialization
Create _workspace/fleet/ and write the initial manifest:
FLEET_ID="fleet-$(date '+%Y%m%d-%H%M%S')"
mkdir -p "_workspace/fleet"
MANIFEST="_workspace/fleet/fleet-status.json"
jq -n \
--arg fleet_id "$FLEET_ID" \
--arg started_at "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
--arg directive "$DIRECTIVE" \
--argjson repos "$REPOS_JSON" \
'{
schema_version: "1.0.0",
fleet_id: $fleet_id,
started_at: $started_at,
directive: $directive,
max_parallel: '$MAX_PARALLEL',
max_retries: '$MAX_RETRIES',
workers: ($repos | map({
repo: .,
status: "queued",
started_at: null,
updated_at: null,
pr_url: null,
ci_conclusion: null,
merge_status: null,
retry_count: 0,
error: null,
phase: "queued"
})),
summary: null
}' > "$MANIFEST"
Full schema: reference/manifest-schema.json (JSON Schema Draft 2020-12).
Changes to the manifest shape must bump schema_version in both the seed
above and the schema file.
Phase 2.5: Top-K Agent Routing (per worker)
Motivation: the agents/ directory contains 8 specialized agents. Spawning every
applicable agent per work item costs roughly 64 K tokens of parallel context and
frequently invokes agents that are irrelevant to the change (for example
test-strategist on a documentation-only PR). This phase selects only the K
highest-scoring agents per work item, inspired by Top-K expert routing in
Mixture-of-Experts transformers.
Controlled by the --top-k N flag (default 2, 0 disables routing and
preserves the prior "all-applicable" behavior).
Agent Metadata Contract
Each agent definition under the repository's agents directory carries two
frontmatter fields used for scoring:
| Field | Type | Purpose |
|---|
applies_to | list of glob patterns | File patterns this agent is relevant for (for example **/*.ts, **/test_*.py, **/*.md) |
keywords | list of strings | Task-type triggers (for example security, performance, refactor) |
Canonical source of agent definitions: plugin/agents/*.md (the distributable
copy) with a mirror at project/.claude/agents/*.md. The orchestrator reads
from --agents-dir (default plugin/agents). Both directories are kept in
sync by the agent-sync policy; the scorer only needs one of them.
Scoring Function
For each work item (issue + PR draft for a repo) and each agent:
score(agent, work_item) =
2 * count(glob in agent.applies_to where any changed_file matches glob)
+ 1 * count(keyword in agent.keywords where keyword appears in work_item.title or work_item.body)
Rules:
- Glob matches use case-sensitive gitignore-style matching (the same semantics
git ls-files uses). **/*.ts matches src/a.ts and a.ts.
- Keyword matches are case-insensitive substring matches against the
concatenation of the issue title and body. Word-boundary matching is NOT
required —
security matches security-audit intentionally.
changed_file is sourced from git diff --name-only <base>..<head> when a
draft PR exists, otherwise from the issue body's "Where" section file list.
- Ties are broken by alphabetical agent name (stable ordering).
- Agents scoring
0 are never selected regardless of K.
Selection
positive = [a for a in agents if score(a) > 0]
selected = sort_by_score_desc(positive).take(K)
if len(selected) == 0:
# Nothing scored > 0; fall back to a single documentation-writer or the
# caller-specified default agent. Record "no-match" in the worker log.
selected = [DEFAULT_FALLBACK_AGENT]
Zero-score agents are never selected, even when K >= len(agents). To
restore the pre-Top-K "all-applicable" behavior, set --top-k 0, which
disables routing entirely.
The worker records its scoring decision to {{LOG_DIR}}/agent-routing.json:
{
"work_item": {"repo": "owner/repo", "issue": 123, "pr": null},
"k": 2,
"all_scores": [
{"agent": "code-reviewer", "score": 5, "matched_globs": ["**/*.ts"], "matched_keywords": ["security"]},
{"agent": "documentation-writer", "score": 0, "matched_globs": [], "matched_keywords": []}
],
"selected": ["code-reviewer", "qa-reviewer"]
}
This per-worker artifact is the telemetry surface required by the issue's
acceptance criteria.
Unit Test Points
The scoring function is pure (no I/O beyond reading agent frontmatter and a
file list), so it is unit-tested in isolation. Required cases:
- Docs-only PR (only
*.md changed, no keyword overlap) — selects
documentation-writer over test-strategist at K=2.
- Mixed TypeScript + security keyword —
code-reviewer and
dependency-auditor outrank structure-explorer.
- Empty
applies_to + empty keywords — agent scores 0 and is excluded.
K >= agent_count — all agents selected, order preserved by score.
- Tie on score — stable alphabetical ordering.
Tests live under tests/fleet_orchestrator/test_topk_scoring.py and
tests/fleet_orchestrator/test_docs_only_routing.py. As of #622 they run
on every PR via .github/workflows/validate-skills.yml
(python -m pytest tests/fleet_orchestrator/ -q); silent breakage in the
scorer would now fail CI rather than ship to consumers of this skill.
Fallback and Compatibility
--top-k 0 disables routing entirely; the worker spawns whatever it would
have spawned before this phase existed.
- If no agent directory is found at
--agents-dir, the worker logs a
routing: disabled notice and proceeds without Top-K selection.
- Missing
applies_to or keywords frontmatter is treated as an empty list
(score contribution 0); it does not fail the worker.
Phase 3: Fan-out Worker Dispatch
Launch workers up to --max-parallel at a time. The Agent tool with
run_in_background=true returns immediately, so all N calls go out in a
single message:
For each repo in the first wave (size min(N, MAX_PARALLEL)):
Agent(
subagent_type: "general-purpose",
model: "opus",
run_in_background: true,
description: "Fleet worker: <repo>",
prompt: <render reference/worker-template.md with substitutions>
)
Template substitutions for each worker (load reference/worker-template.md and
replace these tokens before passing as prompt):
| Token | Value |
|---|
{{REPO}} | owner/repo |
{{DIRECTIVE}} | The full directive text |
{{MANIFEST_PATH}} | Absolute path to fleet-status.json |
{{MAX_RETRIES}} | Retry cap for transient CI failures |
{{FLEET_ID}} | For log correlation |
{{TOP_K}} | --top-k value for per-worker agent routing |
{{AGENTS_DIR}} | --agents-dir value for agent frontmatter lookup |
Record each dispatched worker's task ID in a local map so the supervisor can
call TaskOutput(block=false) on it when polling.
When a worker finishes and workers[i].status becomes completed or failed,
the supervisor picks the next queued repo and dispatches a new worker (maintains
--max-parallel throughput).
Phase 4: Supervisor Polling Loop
Every --poll-interval seconds (default 120):
- Read the manifest atomically:
flock -s "$MANIFEST.lock" -c "cat $MANIFEST" | jq .
- Render a status table to the user (see the template in Status Table Rendering below).
- For each worker task ID, call
TaskOutput(block=false, timeout=2000). If the
task has exited, reconcile its status in the manifest (belt-and-suspenders:
the worker should have already written its terminal status).
- Dispatch a new worker if there is a queued repo and capacity.
- Terminate the loop when every worker is in
completed or failed.
Do NOT use blocking TaskOutput(block=true) — it would freeze the supervisor
and prevent monitoring peer workers.
Stuck-worker detection: if updated_at for any running worker is older than
10 × poll_interval, flag it as stuck in the rendered table but do not kill
it — the worker may be legitimately running a long build. Escalate to the user
only after 20 × poll_interval of silence.
Phase 5: Aggregation and Reporting
Once all workers have terminated:
- Read the final manifest.
- Compute the
summary block:
{
"total": N,
"merged": <count status=completed AND merge_status=merged>,
"draft": <count merge_status=draft>,
"failed": <count status=failed>,
"skipped": <count status=skipped>,
"duration_seconds": <ended_at - started_at>
}
- Write
summary back to the manifest (flock-guarded).
- Render the final aggregated report (see Final Report below).
- Do NOT delete
_workspace/fleet/ — keep it for the audit trail.
Phase 6: Cleanup
- Cancel any still-running background tasks (should be none, but defensive).
- Print the absolute path to
_workspace/fleet/fleet-status.json so the user
can inspect it later.
- If any worker ended in
failed with error.class == "code-review-needed",
list those repos explicitly for user attention.
Status Table Rendering
Each poll cycle renders this table (omit workers still in queued if the
fleet is large, or cap at 20 rows):
## Fleet Status — fleet-20260420-071500
Elapsed: 00:12:34 | Active: 5 / 8 | Queued: 2 | Completed: 6 | Failed: 0
| # | Repo | Phase | PR | CI | Merge | Retries | Last update |
|---|------|-------|-----|-----|-------|---------|-------------|
| 1 | kcenon/repo-a | merged | #89 | success | merged | 0 | 00:11:20 ago |
| 2 | kcenon/repo-b | ci-monitoring | #90 | in_progress | — | 1 | 00:00:42 ago |
| 3 | kcenon/repo-c | implementing | — | — | — | 0 | 00:00:18 ago |
| 4 | kcenon/repo-d | failed | #91 (draft) | failure | — | 3 | 00:03:55 ago |
| 5 | kcenon/repo-e | queued | — | — | — | 0 | — |
Phase values mirror the worker's state machine (see worker template).
Final Report
## Fleet Execution Report — fleet-20260420-071500
Directive: <short form of the directive>
| Metric | Value |
|--------|-------|
| Repos dispatched | 8 |
| Merged | 6 |
| Draft (awaiting manual review) | 1 |
| Failed | 1 |
| Duration | 00:27:14 |
### Per-Repo Outcomes
| Repo | Result | PR | CI | Notes |
|------|--------|----|-----|-------|
| kcenon/repo-a | merged | https://github.com/kcenon/repo-a/pull/89 | success | — |
| kcenon/repo-b | merged | https://github.com/kcenon/repo-b/pull/90 | success | Retried once (flaky lint) |
| kcenon/repo-c | merged | https://github.com/kcenon/repo-c/pull/41 | success | — |
| kcenon/repo-d | draft | https://github.com/kcenon/repo-d/pull/91 | failure | 3 retries exhausted; user review needed |
| kcenon/repo-e | failed | — | — | Pre-excluded: no push permission |
| ... | ... | ... | ... | ... |
### Action Required
- `kcenon/repo-d` — CI failure after 3 retries. Log: `_workspace/fleet/repo-d/retry-3.log`.
Manifest: `_workspace/fleet/fleet-status.json`
Error Handling
| Situation | Strategy |
|---|
| One worker crashes without writing terminal status | Supervisor detects exit via TaskOutput, writes {status:"failed", error:{class:"worker-crash"}} on its behalf |
| Manifest write races | flock around every write; readers use -s (shared) lock for consistency |
| Transient CI failure (test flake, runner timeout) | Worker retries up to --max-retries with exponential backoff (30s, 120s, 300s) |
| Genuine code failure | Worker converts PR to draft, writes error.class="code-review-needed", exits cleanly; supervisor flags in the final report |
| Manifest file corrupted | Fatal error — supervisor halts, prints the last good backup (worker should snapshot to fleet-status.json.N after every N writes) |
| Repo pre-flight fails (no push, archived) | Added to pre-excluded list in the manifest, never dispatched |
| Supervisor run interrupted mid-fleet | Resume is manual: re-invoke the skill with --resume <fleet-id>; it re-reads the manifest and re-launches workers in non-terminal states |
Test Scenarios
Normal Flow
- User invokes with 8 repos and a directive.
- Phase 1 validates all 8; none are pre-excluded.
- Phase 2 writes an 8-entry manifest, all queued.
- Phase 3 dispatches 8 workers in parallel.
- Phase 4 polls every 120s; workers transition queued → running → completed.
- One worker retries a flaky CI run, succeeds on retry.
- Phase 5 aggregates: 8 merged, 0 failed.
- Final report printed;
_workspace/fleet/ retained.
Expected outcome: manifest shows all 8 with status="completed" and merge_status="merged".
Failure Isolation Flow
- User invokes with 8 repos.
- Worker 3 hits a genuine compile error on retry 3.
- Worker 3 converts its PR to draft, writes
error.class="code-review-needed", exits with status failed.
- Workers 1, 2, 4-8 continue unaffected.
- Phase 5 reports 7 merged, 1 draft-awaiting-review.
Expected outcome: worker 3's failure does not stop peer workers; final report explicitly flags worker 3 for user action.
Pre-flight Exclusion Flow
- User invokes with 10 repos; 2 are archived.
- Phase 1 excludes the 2 archived repos, lists them in the manifest with
pre-excluded: "archived".
- Phase 2-5 operate on the 8 live repos.
Expected outcome: final report lists the 2 pre-excluded repos separately from the 8 dispatched.
Dependencies and Related Skills
hooks/post-task-checkpoint.sh (issue #360 — closed): ensures worker-side
checkpoints don't overwrite each other. The fleet orchestrator leans on this
for cross-worker file safety inside _workspace/.
- Atomic Multi-Phase Execution rule (issue #361 — closed, in
_policy.md):
keeps workers from stopping mid-PR for confirmation.
harness: the meta-skill that produced this pattern. Fleet orchestrator
is an instance of Fan-out/Fan-in + Supervisor from harness/reference/agent-design-patterns.md.
issue-work: the per-repo workflow each worker executes. The worker template
is a thin wrapper that adds manifest updates around issue-work's Solo mode.
pr-work: the retry path when a worker's initial PR fails CI.
References
- Worker prompt template:
reference/worker-template.md — what each per-repo worker does step-by-step.
- Manifest schema:
reference/manifest-schema.json — the authoritative JSON Schema for fleet-status.json.
- Documentation:
docs/fleet-orchestrator.md — user-facing overview, examples, and the "On the Horizon" context that motivated this skill.
Output Checklist
After a fleet run, confirm:
Reanchoring Loop Invariants
--reanchor-interval N (default 5, 0 disables) controls how often the Core invariants block from global/skills/_internal/_shared/invariants.md is emitted during the supervisor's status-polling loop.
Loop bind point: every N manifest-poll cycles. A fleet run covering 20+ repos produces enough accumulated worker outputs that the supervisor's attention drifts from the canonical rules; re-anchoring keeps the English-only, squash-merge, and CI-gate invariants in the recent context when aggregating results and deciding per-repo terminal state.
Side Effects and Loop-Safety
This skill is loop_safe: false. It spawns a fleet of worker agents that create branches, PRs, and commits across many repositories. Re-running would duplicate fleet work and re-open already-handled units. Resume an interrupted fleet via --resume <fleet-id>, never by re-invoking from the start. (The supervisor's internal status-polling loop above is bounded by --max-parallel / --reanchor-interval; that is distinct from the non-idempotent external side effects described here.)