Skip to content

fleet — AgentTeamsModule

Safely run --update --merge across every agent-infrastructure workspace under a parent directory (and its subfolders) in one command — covering both .github/agents/ (copilot-vscode) and .claude/ infrastructures — with a git-commit snapshot before each apply and a git diff content audit after.

Source: agentteams/fleet.py


Command-line usage

The fleet runner is driven entirely from the CLI via the --fleet option (there is no separate executable):

# Dry-run preview — discovers workspaces and reports what WOULD change. No writes.
agentteams --fleet /path/to/parent --update --merge

# Apply — snapshot each git workspace, run the merge, then analyse the diff.
agentteams --fleet /path/to/parent --update --merge --yes

# Restrict to one infrastructure, or change the report location.
agentteams --fleet /path/to/parent --update --merge --yes --fleet-frameworks github
agentteams --fleet /path/to/parent --update --merge --yes --fleet-report /tmp/fleet-out

--fleet requires --update and --merge. The default (without --yes) is a dry-run preview; pass --yes to apply. See the CLI Reference → Fleet Update for the option table and the full exclusivity rules.

What one run does, per discovered workspace

  1. Discover — find directories containing .github/agents/ and/or .claude/, pruning node_modules, .git, .agentteams-backups, __pycache__, .venv, venv, .goose, tmp, .worktrees, and archive, and never recursing into .github/.claude internals.
  2. Snapshot (git commit) — before applying, commit the workspace's agent-infra state as chore(fleet): pre-update snapshot (or stay at HEAD when already clean). This is the recoverable rollback point and the diff base. Non-git workspaces fall back to the automatic <output_dir>/.agentteams-backups/<ts>/ snapshot.
  3. Update in-process — re-enter the standard update path per target (--update --merge for .github/agents/ and native .claude/agents/, .goose/recipes/ and .codex/agents/ teams, each with --project <workspace> so the project scan runs; --bridge-merge for bridge consumers). A bridge refreshes from the source framework its entry file records inside the AGENTTEAMS-BRIDGE fence ("Use source framework goose …": .goose/recipes; claude: .claude/agents; otherwise .github/agents). A recorded source fleet can't use is a SKIP, never a re-bridge from .github/agents. No subprocess is spawned, so a successful merge is never misreported because of an interpreter/exit-code quirk, and a failure in one target is isolated.
  4. Diff analysis — after applying, git diff <snapshot> is classified by the authoritative content signals — shrink Notices and deletions inside USER-EDITABLE regions — not the process exit code. A USER-EDITABLE region opens only at a real marker (a blockquote notice naming it, such as ## Project-Specific Notes' > ⚙️ **USER-EDITABLE** or the orchestrator's project-rules notice; a heading naming it; or an explicit BEGIN USER-EDITABLE comment). It runs to the next real <!-- AGENTTEAMS:BEGIN … --> fence, an end marker, or the end of the file, so a user's own ## heading inside it stays covered. A fenced line is never user-editable, and prose that merely mentions the phrase (or quotes a fence marker) opens nothing. Per-workspace .diff files plus report.json and summary.md are written under <DIR>/.agentteams-fleet/<run-id>/.

Per-target status

Each (workspace, target) row is one of:

Status Meaning
OK Merge succeeded; only fenced/generated regeneration and intel churn.
REVIEW A shrink Notice or a USER-EDITABLE-region deletion was detected — inspect the saved diff before committing. Also a kept pre-fencing twin, a dropped twin that retired a rule, a duplicate section (duplicates: a heading present twice, including both copies inside fences; whole_body_migration), or a fence-twin acknowledgement that no earlier applied run has shown (see below). Twin and duplicate classes are matched at the start of the notice, so a quoted heading cannot reclassify one.
FAIL The merge itself errored (e.g. invalid/absent descriptor, bridge source missing).
SKIP Ambiguous .claude/ (no bridge signal and no resolvable descriptor), left for manual review. Also a bridge whose recorded source framework fleet can't bridge from: an unknown framework, a missing source directory, an unreadable entry file, or a source equal to the bridge's own framework. Also used for an interop-projected .codex/agents team (origin: "interop" marker): the row's detail carries the refresh command (agentteams --interop-from <source> --framework codex --output . --overwrite), which fleet never runs.
WOULD-UPDATE Dry-run preview only.

Note on the target field when parsing report.json: updated rows use one of target = "github", "claude-direct", "claude-bridge", "goose-direct", "goose-bridge", or "codex-direct" (a native .codex/agents team, --update --merge --framework codex, all only). A SKIP row for an ambiguous .claude/ carries target = "claude" (it could not be classified); an interop-projected Codex team's SKIP row carries target = "codex". Filter for these explicitly so manual-review rows are not dropped.

Fence-twin acknowledgements are reviewed once

A row in a team's references/fence-twin-acks.csv (fence_twin_acks) turns a kept twin's REVIEW into a twin kept (acknowledged) notice. Nothing ties the row to the operator, so fleet (agentteams/_fleet_twin_acks.py, integrity-pinned) keeps it a REVIEW reason until an applied run has shown it. Each applied run writes the acknowledged-twin notices it showed, per workspace and target, to <run dir>/fence-twin-acks-seen.json beside report.json. The next applied run reads the record of the latest earlier run directory that has one. A notice missing from it (a new row, a changed reason or digest, a row removed and restored) is listed in the target's twins_ack_unreviewed and makes the row REVIEW: "fence-twin acknowledgement new or changed since the last fleet run (N) — needs operator review".

The key is the sha256 of the whole notice line, so only an identical acknowledgement counts as seen. A new agentteams release that rewords a kept-twin reason re-flags those acknowledgements once (safe). It fails safe: a missing, unreadable or malformed record, or a --fleet-report directory with no earlier run, flags every acknowledgement. So does a record that is, or sits in, a symlink, one whose location git cannot check, or one inside a git work tree: when the fleet parent is itself a repository, its agents could plant a record in the default report dir, so pass --fleet-report outside every repository. A target with unflagged acknowledgements is recorded only when its row is REVIEW with no fence imbalance and that reason in its detail. A FAIL, a fence-imbalance REVIEW or a target that did not run keeps its old record, so a flag that was not shown is shown next time. Dry runs neither read nor write the record.

What it does not prove. The record means "fleet showed this to the operator", not "the operator approved it": the flag is shown once, and an operator who rejects the acknowledgement deletes the row. An agent that can run agentteams --fleet --yes itself, or write the operator's fleet output directory, can still clear the flag; tying a row to the operator would take an operator signature. A non-git workspace is not classified at all, so neither this check nor the kept-twin one applies there.

Safety guarantees

Fleet mode is non-destructive by construction: it is merge-only, and --overwrite, --prune, --migrate, --bridge-refresh, --shrink-policy=allow, and every single-target flag (--description, --project, --output, --self, …) are rejected — see the full exclusivity list. .claude/ is only ever bridge-merged, never bridge-refreshed. Descriptor resolution prefers .agentteams/brief.json over the thin _build-description.json stub. The exit code is 1 only when a target genuinely FAILed; a REVIEW exits 0 with a warning so the report can be inspected.


Classes

TargetResult

Source: agentteams/fleet.py

Outcome of updating one target ("github", "claude-direct", "claude-bridge", "goose-direct", or "goose-bridge") within a workspace.

Attributes:

  • workspace (str) — Absolute path of the workspace.
  • target (str) — Which infrastructure was updated.
  • status (str) — OK | REVIEW | FAIL | SKIP | WOULD-UPDATE.
  • detail (str) — Human-readable explanation (e.g. the first error, or the review reason).
  • descriptor (str) — The descriptor file used.
  • files_changed, added_lines, removed_lines (int) — Diff magnitude vs the snapshot.
  • shrink_notices (list[str]) — Shrink-guard Notices emitted during the merge.
  • user_editable_deletions (list[str]) — Deleted lines that fell inside a USER-EDITABLE region (the real content-loss signal).
  • shrink_grant_warnings (list[str]) — The run's WARNING: --shrink-allow lines: a grant skipped as unappliable, refused for want of a backup dir, or one that released nothing. Recorded on every path, dry runs included. summary.md counts them per target and adds a Shrink grants section: skipped or refused grants per target, and grants that released nothing on any target that announced them (an exported AGENTTEAMS_SHRINK_ALLOW is read by every target, so a grant meant for one workspace is not reported as unused because the others ignored it). A skipped entry is echoed as typed, so whatever is in AGENTTEAMS_SHRINK_ALLOW lands in the report as well as on stderr: keep only grant entries in it.
  • shrink_grants_active (list[str]) — Grant entries the run announced as active (the input to that cross-target check).
  • rc (int | None) — Raw return code of the in-process update (diagnostic only — status is derived from content, not this).

WorkspaceResult

Source: agentteams/fleet.py

Aggregate result for one workspace.

Attributes:

  • path (str) — Absolute path of the workspace.
  • is_git (bool) — Whether the workspace is a git repository.
  • snapshot_ref (str | None) — The snapshot commit/HEAD used as the diff base and rollback point.
  • snapshot_committed (bool) — True if a snapshot commit was created (workspace was dirty), False if HEAD was already clean.
  • diff_file (str | None) — Path to the persisted per-workspace .diff.
  • targets (list[TargetResult]) — One entry per updated target.

Functions

discover_workspaces(parent, frameworks="both", *, skipped_worktrees=None)

Source: agentteams/fleet.py

Return the sorted list of workspace directories under parent (recursively) that contain .github/agents/ and/or .claude/ (and, for goose/all, a Goose-bridged workspace). frameworks ("github" | "claude" | "goose" | "both" | "all") filters which infrastructure qualifies a directory: "both" is the legacy default (copilot + claude only); "all" adds Goose and Codex. A Codex team qualifies only with a regular, non-symlink .codex/agents/references/build-log.json marker, native or origin: "interop". Prunes node_modules, .git, .agentteams-backups, __pycache__, .venv, venv, .goose, .codex, tmp, .worktrees, archive, and the fleet's own .agentteams-fleet output/backup tree, never recurses into .github/.claude internals, and skips git linked worktrees (and submodules) — their agent infra is covered via the main working tree. If skipped_worktrees (a keyword-only list[Path]) is passed, each excluded linked worktree that carries agent infra is appended to it so the caller can surface the exclusion as a visible report row.

Returns: list[Path]

run_fleet(args, parser)

Source: agentteams/fleet.py

Entry point dispatched from build_team.main() when --fleet is set. Discovers workspaces, snapshots and updates each target in-process, classifies the resulting diff, writes the report under <DIR>/.agentteams-fleet/<run-id>/, and returns an exit code (1 if any target FAILed, else 0).

Parameters:

  • args (argparse.Namespace) — Parsed CLI arguments (reads fleet, fleet_frameworks, fleet_report, yes, dry_run, shrink_policy, fleet_allow_no_verify).
  • parser (argparse.ArgumentParser) — The CLI parser (reserved for error reporting).

Returns: int — Process exit code.


Rollback

The pre-update snapshot is the recoverable point. To undo a workspace's update:

git -C /path/to/workspace reset --hard <snapshot_ref>   # snapshot_ref is in report.json

Non-git workspaces restore from their <output_dir>/.agentteams-backups/<ts>/ snapshot (see emit.restore_backup).


See also