Browse Source

feat(skills): Add fleetflow — heterogeneous GLM/Codex/Anthropic fleet orchestration

Ports the native Workflow tool's internals to OS-process workers so brains
can differ per work class: GLM-5.2 (z.ai) for mechanical, Codex for dissent
and second implementations, Sonnet/Opus for build/judge, orchestrated by a
Fable (preferred) or Opus session.

Extraction deliverable (references/native-workflow-insights.md) documents
the wf_*/journal.jsonl replay format (v2:sha256 content-hash keys, inline
structured results, the determinism ban's real purpose), pipeline-vs-barrier
doctrine, concurrency caps + budget spine, and the quality-pattern catalog —
evidence-linked to on-disk runs inspected 2026-07-05.

Scripts: ff-spawn (worktree lane + guard preamble + journal cache with
exit-3 hits), ff-collect (per-brain gates; --check-main-clean escape guard
from the 2026-07-05 worker-escape incident), ff-doctor (--offline/--live;
live run verified all providers green with orchestrator=fable).
41-assertion offline suite; caught a jq 'false // empty' footgun in review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0xDarkMatter 5 days ago
parent
commit
623d4da8bd

+ 1 - 1
AGENTS.md

@@ -5,7 +5,7 @@
 This is **claude-mods** - a collection of custom extensions for Claude Code:
 - **3 expert agents** for pure context-isolation/worker roles (git-agent, firecrawl-expert, project-organizer) - every domain-knowledge agent became an `-ops` skill (v3.0, skills-first)
 - **3 commands** for session management and git orchestration (/sync, /save, /git-ops)
-- **100 skills** for CLI tools, patterns, workflows, and development tasks (incl. `repo-doctor` for agentic-quality repo audits, `svg-brand-tint-ops` for zero-dep in-browser SVG brand-recolour + Potrace-stage raster vectorising, `r-ops` for tidyverse-first modern R / data analysis, `loop-ops` for outer-loop design discipline, `ffmpeg-ops` for probe-first media processing and EDL-driven editing, `supply-chain-defense` for behavioural-first dependency security, `prompt-injection-defense` for instruction-integrity scanning, `pypi-ops` for OIDC Trusted Publishing to PyPI, `net-ops` for network troubleshooting, `windows-ops` / `mac-ops` for workstation diagnostics, `fleet-worker` for cheap parallel worker delegation)
+- **101 skills** for CLI tools, patterns, workflows, and development tasks (incl. `fleetflow` for heterogeneous GLM/Codex/Anthropic worker fleets, `repo-doctor` for agentic-quality repo audits, `svg-brand-tint-ops` for zero-dep in-browser SVG brand-recolour + Potrace-stage raster vectorising, `r-ops` for tidyverse-first modern R / data analysis, `loop-ops` for outer-loop design discipline, `ffmpeg-ops` for probe-first media processing and EDL-driven editing, `supply-chain-defense` for behavioural-first dependency security, `prompt-injection-defense` for instruction-integrity scanning, `pypi-ops` for OIDC Trusted Publishing to PyPI, `net-ops` for network troubleshooting, `windows-ops` / `mac-ops` for workstation diagnostics, `fleet-worker` for cheap parallel worker delegation)
 - **13 output styles** for response personality (Vesper, Spartan, Mentor, Executive, Pair, Atlas, Coach, Harbour, Meridian, Noir, Roast, Sage, Scout)
 - **13 hooks** for pre-commit linting, post-edit formatting, dangerous command warnings, uv enforcement, dependency-install + manifest-edit supply-chain advisories, hidden-Unicode scanning (session-start + pre-commit), live config-change + worktree guards, mid-session peer-writer guard + touched-files ledger, and pmail notifications - security set auto-wired via plugin hooks.json
 - **Pigeon** inter-session messaging (`pigeon send/read/reply`) - SQLite-backed pmail at `~/.claude/pmail.db`

+ 14 - 0
CHANGELOG.md

@@ -8,6 +8,20 @@ feature releases live in the README "Recent Updates" section.
 ## [Unreleased]
 
 ### Added
+- **`fleetflow` skill** - heterogeneous worker-fleet orchestration: GLM (z.ai),
+  Codex (OpenAI `codex exec`), and Anthropic Sonnet/Opus/Haiku workers as
+  isolated OS processes under a Fable-or-Opus orchestrator session. Ports the
+  native Workflow tool's internals — extracted from its tool spec and on-disk
+  run dirs (`wf_*/journal.jsonl`) into `references/native-workflow-insights.md`:
+  hash-keyed (`v2:sha256`) journal replay for resume, pipeline-vs-barrier
+  doctrine, adversarial verify / judge panel / loop-until-dry patterns, the
+  workers-return-data prompting contract, and the no-silent-caps rule. Ships
+  `ff-spawn` (uniform spawner: worktree lane + guard preamble + journal +
+  per-brain launch), `ff-collect` (per-brain gate + `--check-main-clean`
+  escape guard, born from a real worker-escape incident), `ff-doctor`
+  (`--offline`/`--live` provider probes incl. the Fable/Opus orchestrator
+  detection), a per-brain contracts reference (full `codex exec` flag map,
+  GLM auth-isolation, Anthropic alias notes), and a 41-assertion offline suite.
 - **`agentic-quality` rule** - cross-repo doctrine for code, comments, docs, and
   structure that survive the session: the cold-agent test, comment doctrine
   (contract blocks, WHY-only inline, guard comments, section markers,

+ 2 - 1
README.md

@@ -18,7 +18,7 @@ Built on the [Agent Skills specification](https://agentskills.io/specification)
 
 From Python async patterns to Rust ownership models, from AWS Fargate deployments to Craft CMS development - claude-mods provides the specialized knowledge and tools that transform Claude from a general-purpose assistant into a domain expert who understands your stack, remembers your workflow, and ships production code.
 
-**3 agents. 100 skills. 13 styles. 13 hooks. 14 rules. One install.**
+**3 agents. 101 skills. 13 styles. 13 hooks. 14 rules. One install.**
 
 ## Recent Updates
 
@@ -298,6 +298,7 @@ See [skill-creator](skills/skill-creator/) for the complete guide.
 | [push-gate](skills/push-gate/) | Pre-push safety gate - gitleaks + regex secret scan, forbidden-file check, no bypass |
 | [fleet-ops](skills/fleet-ops/) | Manage a fleet of concurrent Claude sessions - landing queue with test gate, pre-land scrub (experimental) |
 | [fleet-worker](skills/fleet-worker/) | Delegate tasks to cheap headless GLM (or any Anthropic-compatible) workers - per-task git worktree + isolated config, result gating, fan-out that hands winning branches to fleet-ops landing |
+| [fleetflow](skills/fleetflow/) | Orchestrate a heterogeneous worker fleet - GLM (z.ai), Codex (OpenAI), and Anthropic Sonnet/Opus/Haiku process workers under a Fable/Opus orchestrator, porting the native Workflow tool's patterns (hash-keyed journal resume, pipeline-vs-barrier, adversarial verify, judge panels) with an escape guard baked in; ff-spawn / ff-collect / ff-doctor scripts |
 | [summon](skills/summon/) | Claude Desktop session toolbox - cross-account transfer, recovery picker, cwd rebind, store doctor |
 | [doc-scanner](skills/doc-scanner/) | Scan and synthesize project documentation |
 | [repo-doctor](skills/repo-doctor/) | Audit any repo against the agentic-quality doctrine - entry docs, comment contracts, structure, enforcement gates, doc-pairing; scorer with --json + CI --strict, plus comment-doctrine / entry-docs / monorepo-structure references |

+ 1 - 1
docs/PLAN.md

@@ -16,7 +16,7 @@
 | Component | Count | Notes |
 |-----------|-------|-------|
 | Agents | 3 | Pure context-isolation/worker roles only: git-agent (background commits/PRs), firecrawl-expert (noisy scrapes), project-organizer (bulk restructure) |
-| Skills | 100 | Operational skills, CLI tools, workflows, diagnostics, security |
+| Skills | 101 | Operational skills, CLI tools, workflows, diagnostics, security |
 | Commands | 3 | Session management + git orchestration (sync, save, git-ops) |
 | Rules | 14 | agentic-quality, cli-tools, commit-style, dev-servers, loop-engineering, modern-tools, naming-conventions, prompt-injection, public-posts, release-review, shell-preference, skill-agent-updates, supply-chain, worktree-boundaries |
 | Output Styles | 13 | Vesper, Spartan, Mentor, Executive, Pair, Atlas, Coach, Harbour, Meridian, Noir, Roast, Sage, Scout |

+ 176 - 0
skills/fleetflow/SKILL.md

@@ -0,0 +1,176 @@
+---
+name: fleetflow
+description: "Orchestrate a heterogeneous worker fleet — GLM (z.ai), Codex (OpenAI), and Anthropic Sonnet/Opus/Haiku — from one Claude session, porting the native Workflow tool's proven patterns (phases, pipeline-vs-barrier, adversarial verify, judge panels, hash-keyed journal resume) to OS-process workers that can run DIFFERENT provider brains. Triggers on: fleetflow, heterogeneous fleet, mixed-model fleet, codex worker, orchestrate glm and codex, cross-provider fan-out, multi-provider workers, workflow across providers, codex second opinion, fable orchestrator, mixed fleet, cross-model verify."
+when_to_use: "Use when a fan-out wants DIFFERENT brains per work class — e.g. 'fan this backlog out to GLM and Codex workers', 'mixed fleet with cross-model adversarial verify', 'Codex second opinion on each lane'. Same-provider in-process fan-out → native Workflow tool; one cheap worker → fleet-worker; landing branches → fleet-ops."
+license: MIT
+allowed-tools: "Read Write Edit Bash Glob Grep Task"
+metadata:
+  author: claude-mods
+  depends-on: "fleet-worker, fleet-ops"
+  related-skills: "loop-ops, iterate, claude-code-ops"
+---
+
+# fleetflow
+
+> Facts verified as of 2026-07 (Claude Code Workflow tool, codex-cli 0.125, fleet-worker GLM-5.2/z.ai).
+
+Claude Code's native **Workflow tool** is a superb orchestration harness with one
+structural limit: every agent it spawns runs **in-process**, so they all share one
+provider (`ANTHROPIC_BASE_URL` is process-global — only the model *alias slot*
+varies per agent). fleetflow ports the Workflow tool's patterns to **OS-process
+workers**, where each worker gets its own env block — and therefore its own brain:
+
+| Worker brain | Process | Harness |
+|---|---|---|
+| **GLM-5.2 / GLM-4.5-Air** | `claude -p` → z.ai endpoint | Claude Code tools, cheap brain (via `fleet-worker`) |
+| **Codex** (GPT-class) | `codex exec` | OpenAI's own agent harness — a genuinely different toolchain *and* model |
+| **Sonnet / Haiku** | `claude -p --model sonnet\|haiku` | Claude Code tools, host auth |
+| **Opus** | `claude -p --model opus` | reserve for verify/judge lanes |
+
+The **orchestrator is this session** — run it on **Fable if the account has it,
+Opus otherwise** (`ff-doctor --live` probes and reports which). Judgment stays in
+the orchestrator; the scripts own the deterministic mechanics (spawn, journal,
+gate). That split *is* the Workflow tool's design, relocated: its JS script is
+deterministic control flow around model judgment — here the orchestrator session
+plays the script's role and the journal keeps it resumable.
+
+## Decision gate — is fleetflow the right tool?
+
+| Situation | Use |
+|---|---|
+| Fan-out where all agents can share the session's provider | **native Workflow tool** (cheaper, integrated progress UI, schema-forced outputs) |
+| One-off cheap delegation, single worker | **fleet-worker** directly |
+| Worker brains should differ by work class, or you want cross-provider dissent in verification | **fleetflow** |
+| Landing the resulting branches | **fleet-ops** (always) |
+
+Rule of thumb from fleet-worker's locus rule, extended: shell out to process
+workers only for a **large, independent, file-mutating, cost-dominant** fan-out
+you can gate before landing — *or* when the point is **model diversity** (a
+Codex refuter catches what three same-model skeptics miss).
+
+## Model routing (work class × brain)
+
+Extends [fleet-worker's routing convention](../fleet-worker/references/model-routing.md)
+with the Codex column and the orchestrator rule:
+
+| Work class | Brain | Why |
+|---|---|---|
+| **mechanical** (batch edits, verifier clones, backfills) | GLM-5.2, Haiku | proven cheap; gate catches misses |
+| **scout** (survey, inventory, locate) | Sonnet, GLM-5.2 | breadth over depth |
+| **build** (scoped features, refactors) | Sonnet, Codex | Codex = independent harness; good second implementation for judge panels |
+| **verify / judge** | Opus + one cross-provider dissenter (Codex or GLM) | *never under-power a judge*; diversity beats redundancy |
+| **synthesize / land decisions** | orchestrator (Fable > Opus) | needs the conversation's context |
+
+Two guardrails carried over verbatim from the native tool's doctrine: reach for
+the **effort lever before the model lever**, and a cheap rubber-stamp verifier is
+worse than none.
+
+## The run lifecycle
+
+```
+plan packets → ff-doctor → ff-spawn (×N, background) → ff-collect (gate) → fleet-ops land → clean up
+```
+
+1. **Plan packets that are file-disjoint.** No two lanes may touch the same file
+   — this is what makes landing conflict-free and is the #1 planning duty.
+   Pipeline-by-default thinking applies: add a barrier (wait for all lanes)
+   only when a later stage genuinely needs *all* prior results
+   (dedup, early-exit, cross-lane comparison). See
+   [references/native-workflow-insights.md](references/native-workflow-insights.md) §3.
+2. **Preflight:** `scripts/ff-doctor.sh --live` — probes every provider (GLM
+   endpoint, `codex login status`, Anthropic model availability incl. Fable) and
+   reports the orchestrator tier. Don't spawn a fleet a doctor won't bless.
+3. **Spawn:** `scripts/ff-spawn.sh --run <name> --id <id> --brain <brain>
+   --prompt-file <f> --worktree` from the orchestrator's Bash tool with
+   `run_in_background: true`, one call per lane. ff-spawn creates the worktree
+   lane (`fleetflow/<run>/<id>` at `.fleetflow/<run>/wt-<id>`, repo top — never
+   under `.claude/`), injects the guard preamble
+   ([assets/guard-preamble.txt](assets/guard-preamble.txt)), journals a
+   `started` record, runs the worker to completion, journals the `result`.
+4. **Collect + gate:** `scripts/ff-collect.sh <run> <id>` — per-brain success
+   semantics (Claude JSON `is_error`; Codex exit + last-message), then the
+   orchestrator reviews the three-dot diff (`git diff main...fleetflow/<run>/<id>`)
+   and runs the lane's tests. **Always finish with
+   `ff-collect.sh --check-main-clean`** — the escape guard (see Safety).
+5. **Land** through fleet-ops (sequential, test-gated). Delete lanes and
+   `.fleetflow/<run>/` after landing.
+
+**Resume.** The journal (`.fleetflow/<run>/journal.jsonl`) uses the native
+tool's mechanism: each spawn is keyed by a content hash of
+`(brain, prompt, opts)`. Re-running `ff-spawn` with an unchanged packet returns
+the cached result instantly (exit 3 + path); change the prompt and only that
+lane re-runs. Corollary (same reason the native tool bans `Date.now()`): keep
+timestamps and random values OUT of packet prompts, or the key changes and the
+cache never hits.
+
+## Patterns ported from the native Workflow tool
+
+Full extraction with evidence in
+[references/native-workflow-insights.md](references/native-workflow-insights.md).
+The ones to actually use:
+
+- **Adversarial verify:** for each finding/lane output, spawn 2–3 refuters
+  prompted to *refute*, majority kills. Make one refuter a different provider.
+- **Judge panel:** N independent build attempts (e.g. Sonnet vs Codex), judged
+  by Opus lanes, synthesize from the winner.
+- **Loop-until-dry:** for unknown-size discovery, keep spawning finder lanes
+  until 2 consecutive rounds add nothing new. Dedup against everything *seen*,
+  not everything *confirmed*.
+- **Completeness critic:** one final lane asking "what's missing?" — its answer
+  is the next round's packet list.
+- **No silent caps:** if you bound coverage (top-N, sampling), say so in the
+  run summary. Silent truncation reads as "covered everything".
+- **Workers return data, not prose:** every packet ends with "FINAL REPLY:
+  <exact shape>". For machine-parseable results use `--schema` (Codex
+  `--output-schema` is native; Claude workers get the schema embedded in the
+  prompt and validated at collect time).
+
+## Safety — the cage, not the model
+
+- **Isolation:** every mutating worker gets its own worktree lane *and* (GLM)
+  its own `CLAUDE_CONFIG_DIR`. Codex workers run `--full-auto` (sandboxed,
+  workspace-write) confined to their lane via `-C`.
+- **Escape guard (learned 2026-07-05, incident):** a worker CAN escape its
+  worktree by writing absolute paths — a GLM worker once wrote its output into
+  the main checkout while its own lane stayed clean. Two mechanical defenses,
+  both defaults: the guard preamble's *relative-paths-only* clause, and
+  `ff-collect.sh --check-main-clean` after every run (exit 12 = escape
+  detected; stop, `git stash push -u` to salvage, investigate).
+- **Permission posture:** workers run non-interactive
+  (`bypassPermissions` default; `FLEETFLOW_PERMISSION_MODE=dontAsk` + allowlist
+  when the orchestrator session is in auto mode — a `bypassPermissions` child
+  is hard-denied there as *Create Unsafe Agents*). Same doctrine as
+  fleet-worker; see its Permission posture section.
+- **Bounds:** `--max-turns` per worker (default 100), concurrency ≤ 4–6 per
+  provider (endpoint quota is the binding constraint), wall-clock patience via
+  the orchestrator's background-task notifications — never poll-sleep.
+- **Terms:** a subscription-authed orchestrator must stay interactive;
+  API-key-authed sessions may be automated. Codex usage bills to the ChatGPT
+  plan. Verify your own plans' terms (fleet-worker "Know your terms" applies).
+
+## Scripts
+
+| Script | Purpose |
+|---|---|
+| [scripts/ff-doctor.sh](scripts/ff-doctor.sh) | `--offline` structural preflight; `--live` probes GLM endpoint, Codex auth, Anthropic models, reports orchestrator tier (fable/opus) |
+| [scripts/ff-spawn.sh](scripts/ff-spawn.sh) | uniform spawner: worktree lane + guard preamble + journal + per-brain launch (GLM via fleet-worker, Codex via `codex exec`, Anthropic via `claude -p`) |
+| [scripts/ff-collect.sh](scripts/ff-collect.sh) | per-brain result gate; `--check-main-clean` escape guard |
+
+All follow the Skill Resource Protocol: stdout is data, chatter on stderr,
+semantic exit codes (`0` ok, `2` usage, `3` cached/missing, `7` unreachable,
+`10` worker failed, `12` escape detected), `--help` with EXAMPLES.
+
+## References
+
+- [references/native-workflow-insights.md](references/native-workflow-insights.md)
+  — the extraction: journal format on disk, resume semantics, control-flow
+  doctrine, quality patterns, caps and budget spine, with evidence.
+- [references/worker-contracts.md](references/worker-contracts.md) — per-brain
+  launch/collect/auth contracts (GLM env knobs, full `codex exec` flag map,
+  Anthropic alias notes) and the Fable/Opus orchestrator probe.
+
+## See Also
+
+- [fleet-worker](../fleet-worker/) — the single-worker spawn layer fleetflow builds on (GLM auth isolation, model routing, terms).
+- [fleet-ops](../fleet-ops/) — the landing layer; every fleetflow run ends there.
+- [loop-ops](../loop-ops/) — schedule a recurring fleetflow run as an L1/L2 loop.

+ 6 - 0
skills/fleetflow/assets/guard-preamble.txt

@@ -0,0 +1,6 @@
+HARD CONSTRAINTS (fleetflow worker):
+- You are a headless worker in a dedicated working directory (your cwd). ALL file paths you construct MUST be relative to cwd. NEVER build absolute paths into the repository or write anywhere outside your cwd — doing so escapes your isolation lane.
+- Read any file BEFORE editing it (the harness enforces this).
+- Touch ONLY the files your task lists. Do not modify CI config, shared docs, or other components unless the task explicitly says so.
+- Commit your work with conventional commits. Do NOT push.
+- Your final reply is DATA consumed by an orchestrator, not prose for a human. End with the exact FINAL REPLY shape your task specifies.

+ 162 - 0
skills/fleetflow/references/native-workflow-insights.md

@@ -0,0 +1,162 @@
+# Native Workflow Tool — Inner Workings & Extracted Patterns
+
+> Evidence base, compiled 2026-07-05: (a) the Workflow tool's published tool-spec
+> (Claude Code v2.1.x session surface), (b) on-disk inspection of real completed
+> runs under `~/.claude/projects/<encoded-cwd>/<session>/subagents/workflows/wf_*/`
+> (GlyphWeb + Simulacra sessions, July 2026). Where a claim comes only from disk
+> inspection it is marked **[disk]**; spec-sourced claims are **[spec]**.
+> Internal formats may change between Claude Code releases — re-verify on major
+> version bumps.
+
+## 1. Run anatomy on disk [disk]
+
+A workflow run is a directory `wf_<8hex>-<3hex>/` under the owning session's
+transcript dir, containing:
+
+```
+wf_21db8d15-782/
+├── journal.jsonl                    # the replay journal (see §2)
+├── agent-a01cb5f01fadf5610.jsonl    # full transcript per agent (one file each)
+├── agent-a01cb5f01fadf5610.meta.json# {"agentType":"workflow-subagent","spawnDepth":1}
+└── ...                              # (script text persists separately in the session dir)
+```
+
+- Agent IDs are 17-hex tokens prefixed `a`.
+- `meta.json` is minimal: agent type + spawn depth (nesting is capped at one
+  level [spec] — `workflow()` inside a child throws).
+- The workflow *script* is persisted under the session directory (the tool
+  result returns its path) so it can be edited and re-invoked via `scriptPath`.
+
+## 2. The journal: hash-keyed replay [disk+spec]
+
+`journal.jsonl` holds exactly two record shapes:
+
+```json
+{"type":"started","key":"v2:<sha256>","agentId":"a02ba44d2c2c1ed9c"}
+{"type":"result","key":"v2:<sha256>","agentId":"...","result":{ ...structured object... }}
+```
+
+Key facts:
+
+- **The key is a content hash of the agent call** — `(prompt, opts)` under a
+  `v2:` version prefix. Resume works by replaying the script and answering each
+  `agent()` call from the journal when its key matches: "the longest unchanged
+  prefix of agent() calls returns cached results instantly" [spec]. Same script
+  + same args → 100% cache hit.
+- **Results are stored as structured objects inline** — when the agent was
+  called with a `schema`, the validated JSON object itself is journaled (a real
+  example held `{"green":true,"failures":[],"notes":"...","ideas":[...]}`).
+  Validation happens at the tool-call layer so the model retries on mismatch
+  [spec]. The worker never "formats a report"; it emits data.
+- **This is why determinism is enforced:** `Date.now()`, `Math.random()`, and
+  argless `new Date()` *throw* inside workflow scripts [spec] — any
+  nondeterminism in prompt construction would change the hash and break replay.
+  Timestamps go in via `args`; randomness via index-varied prompts.
+
+**fleetflow port:** `ff-spawn` journals the same two record shapes into
+`.fleetflow/<run>/journal.jsonl`, keyed `v2:sha256(brain + "\n" + prompt + "\n" + opts)`.
+Same corollary: keep timestamps/randomness out of packet prompts.
+
+## 3. Control-flow doctrine [spec]
+
+The tool's most opinionated — and most portable — design:
+
+- **`pipeline()` is the default.** Items flow through stages independently with
+  *no barrier*: item A can be in stage 3 while B is in stage 1. Wall-clock =
+  slowest single-item chain, not sum of slowest-per-stage.
+- **`parallel()` is a barrier** and must be justified. Legitimate only when
+  stage N needs cross-item context from *all* of stage N−1: dedup/merge across
+  the full set, early-exit on zero total, "compare with the other findings"
+  prompts.
+- **The barrier smell test:** `await parallel(...)` → pure transform
+  (flatten/map/filter) → `await parallel(...)` means the middle transform never
+  needed the barrier — fold it into a pipeline stage.
+- Failure isolation: a stage that throws drops *that item* to `null` (skip its
+  remaining stages); `parallel()` never rejects wholesale — thunk errors become
+  `null` slots, so `.filter(Boolean)` before use.
+
+**fleetflow port:** the orchestrator plans lanes the same way — don't hold a
+whole round hostage to a barrier unless a later packet genuinely consumes all
+prior results. Failure isolation comes free (one lane failing gates out at
+collect; the others land).
+
+## 4. Concurrency, caps, budget [spec]
+
+- Concurrent agents: `min(16, cpu cores − 2)` per workflow; excess queues.
+- Lifetime cap 1000 agents per run (runaway-loop backstop), 4096 items per
+  pipeline/parallel call — an explicit error, never silent truncation.
+- **Budget spine:** `budget.total / spent() / remaining()` — a *shared* output
+  token pool across the main loop and all workflows; a hard ceiling (agent()
+  throws past it), enabling `while (budget.remaining() > 50k)` loops and
+  fleet-size-by-budget (`Math.floor(budget.total / 100k)`).
+- **No silent caps doctrine:** any bounded coverage (top-N, no-retry, sampling)
+  must be `log()`ged — "silent truncation reads as covered-everything".
+
+**fleetflow port:** concurrency ≤ 4–6 *per provider* (endpoint quota binds
+before CPU); bound each worker with `--max-turns`; declare any coverage bound
+in the run summary. Token budgeting is per-provider-plan rather than a shared
+pool — account by the provider's own usage metering, not Claude Code's
+notional `total_cost_usd`.
+
+## 5. Progress & structure surface [spec]
+
+- `meta` block (pure literal: name, description, phases) drives the /workflows
+  progress UI; `phase()` groups agents; `log()` emits narrator lines.
+- `opts.label` names an agent in the tree; `opts.phase` pins an agent to a
+  group from inside concurrent stages (avoids racing the global phase state).
+- Agents get model (`opts.model`) and effort (`opts.effort`) overrides
+  per-call; guidance: omit model (inherit) unless confident, use `effort` as
+  the finer lever ("reach for the effort lever before the model lever").
+
+**fleetflow port:** run/lane naming (`--run`/`--id`) is the phase/label
+analogue; the orchestrator's status tables to the user play the /workflows UI
+role.
+
+## 6. Quality patterns [spec]
+
+Verbatim-portable; the native spec's catalog:
+
+| Pattern | Mechanics | fleetflow note |
+|---|---|---|
+| **Adversarial verify** | N independent skeptics per finding, each prompted to REFUTE, default-refute on uncertainty, majority kills | make ≥1 skeptic a different provider — model diversity catches failure modes redundancy can't |
+| **Perspective-diverse verify** | distinct lenses (correctness/security/perf/repro) instead of N identical refuters | lens × brain grid is even stronger |
+| **Judge panel** | N independent attempts from different angles, parallel judges score, synthesize from winner + graft runners-up | natural fit: Sonnet attempt vs Codex attempt, Opus judges |
+| **Loop-until-dry** | keep spawning finders until K consecutive rounds return nothing new; dedup vs *seen*, not vs *confirmed* (else judge-rejected findings reappear forever) | identical |
+| **Multi-modal sweep** | parallel agents each searching a *different way*; each blind to the others | identical |
+| **Completeness critic** | final agent asks "what's missing?" — its findings are the next round | identical |
+
+## 7. Prompting contract [spec]
+
+Workflow subagents are told their **final text IS the return value** — not a
+human-facing message — so they return raw data. With a `schema`, the subagent
+is forced through a StructuredOutput tool call and the validated object comes
+back.
+
+**fleetflow port:** every packet ends with an explicit `FINAL REPLY:` shape.
+Codex workers can enforce it natively (`codex exec --output-schema <file>` +
+`-o last-message.txt`); Claude workers get the shape in the prompt and
+`ff-collect` + the orchestrator validate on the way in.
+
+## 8. Resume ergonomics [spec]
+
+`Workflow({scriptPath, resumeFromRunId})` after a pause/kill/edit: unchanged
+prefix replays from journal; first edited call and everything after runs live.
+Fallback when no journal: read the `agent-<id>.jsonl` transcripts and
+hand-author a continuation.
+
+**fleetflow port:** `ff-spawn` consults the run journal before spawning (cache
+hit → exit 3 + cached result path). The same fallback applies: worker stdout
+JSON files survive in `.fleetflow/<run>/` for hand-salvage.
+
+## 9. What does NOT port
+
+- **In-process schema enforcement with retry** — the tool-call layer re-asks
+  the model on validation failure; fleetflow's Claude workers get one shot +
+  collect-time validation (Codex `--output-schema` restores most of it).
+- **The shared token budget object** — no cross-provider equivalent; use
+  per-provider plan metering.
+- **The /workflows live UI** — orchestrator status tables replace it.
+- **MCP tool reach-through** (workflow agents can ToolSearch session MCP
+  servers) — process workers see only what their own config provisions. For
+  GLM workers, provision skills into the isolated config dir (fleet-worker
+  "Giving a worker skills").

+ 104 - 0
skills/fleetflow/references/worker-contracts.md

@@ -0,0 +1,104 @@
+# Worker Contracts — per-brain launch, gate, and auth
+
+> Verified against installed binaries 2026-07-05: `codex-cli 0.125.0`,
+> Claude Code v2.1.x, fleet-worker (GLM-5.2 via z.ai). Re-verify flag maps on
+> major version bumps (`ff-doctor --offline` checks the load-bearing ones).
+
+## 1. GLM workers (via fleet-worker)
+
+The [fleet-worker](../../fleet-worker/SKILL.md) launcher is the contract —
+fleetflow does not duplicate it. Load-bearing facts:
+
+- **Auth isolation is mandatory**: on a machine with a Claude subscription,
+  each worker needs its own empty `CLAUDE_CONFIG_DIR` or the host OAuth token
+  is sent to z.ai and rejected (`401`). One config dir **per parallel worker**:
+  `FLEET_WORKER_CONFIG_DIR=$HOME/.fleet-worker/cfg-ff-<id>`.
+- Key resolution: `ANTHROPIC_AUTH_TOKEN` → keyring
+  (`FLEET_WORKER_KEYRING_SERVICE`/`KEY`) → `ZHIPU_API_KEY`/`GLM_API_KEY`.
+- Launch: `fleet-worker --output-format json --max-turns N "PROMPT" > result.json`
+- **Gate**: `is_error` in the result JSON is the real success signal
+  (`subtype` lies); `fleet-collect.sh` implements it and `ff-collect` defers
+  to the same check.
+- Models: `FLEET_WORKER_MODEL` (default GLM-5.2), `FLEET_WORKER_SMALL_MODEL`
+  (GLM-4.5-Air). The isolated config starts with **no skills** — provision any
+  the packet needs into `<config>/skills/` or the lane's `.claude/skills/`.
+
+## 2. Codex workers (`codex exec`)
+
+OpenAI's agent harness, non-interactive. Flag map (codex-cli 0.125.0):
+
+| Flag | Use |
+|---|---|
+| `[PROMPT]` or stdin | prompt as arg, or piped (`-` explicit); piped stdin appends as a `<stdin>` block |
+| `-m, --model <MODEL>` | model override; omit to use the user's `config.toml` default |
+| `--full-auto` | sandboxed automatic execution (workspace-write) — the fleetflow default |
+| `-s, --sandbox <read-only\|workspace-write\|danger-full-access>` | explicit sandbox policy; prefer over `--dangerously-bypass-approvals-and-sandbox` (only for externally-sandboxed environments) |
+| `-C, --cd <DIR>` | working root — **point at the lane worktree**; this is the confinement |
+| `--add-dir <DIR>` | extra writable dirs (rarely needed; widens the cage) |
+| `-o, --output-last-message <FILE>` | final message → file; **the primary result artifact** |
+| `--output-schema <FILE>` | JSON Schema for the final response — native structured output |
+| `--json` | JSONL event stream on stdout (the transcript analogue; capture to `<id>.events.jsonl`) |
+| `--ephemeral` | don't persist session files — good fleet hygiene |
+| `--skip-git-repo-check` | only if the workdir isn't a git repo (lanes are, so unneeded) |
+| `codex exec resume <id> \| --last` | continue a previous run — the resume analogue |
+| `codex exec review` | built-in repo code-review mode — useful as a judge lane |
+
+- **Auth**: ChatGPT-plan login (`codex login status` → "Logged in using
+  ChatGPT"). Usage bills to that plan. No per-worker config isolation needed —
+  Codex has no Claude OAuth to collide with; `--ephemeral` keeps runs clean.
+- **Gate**: exit code 0 **and** non-empty last-message file. With
+  `--output-schema`, additionally `jq empty < last-message` (valid JSON) and a
+  spot-check of required keys at collect time.
+- **Character**: a genuinely different model *and* toolchain — its highest
+  value in fleetflow is dissent (refuter/judge lanes) and independent second
+  implementations, not bulk mechanical work (GLM is cheaper there).
+
+## 3. Anthropic workers (`claude -p`)
+
+- Launch: `claude -p --model <sonnet|haiku|opus> --output-format json
+  --max-turns N --permission-mode <mode> "PROMPT" > result.json` from the lane
+  worktree. Aliases resolve to current models; pin a full ID only when
+  reproducibility across weeks matters.
+- **Uses the host config/auth deliberately** (unlike GLM workers): host skills
+  and MCP are available to the worker. If you want a clean-room Anthropic
+  worker, set an isolated `CLAUDE_CONFIG_DIR` + `ANTHROPIC_API_KEY` — then
+  provision skills like a GLM worker.
+- **Gate**: same `is_error` JSON semantics as GLM (both are `claude -p`).
+- **Terms note** (from fleet-worker): automating a *subscription* is
+  restricted by Anthropic's Consumer Terms except via API key. A
+  subscription-authed orchestrator stays interactive; if a scheduler drives
+  fleetflow runs, put the workers (or the whole run) on an API key.
+
+## 4. Orchestrator: Fable if available, Opus if not
+
+The orchestrator is the session invoking this skill — its model is chosen when
+the session starts (`/model`, or the `model` field in settings), not by a
+per-call override. Doctrine:
+
+1. **Fable** (`claude-fable-5`) when the account has it — strongest judgment
+   for packet planning, diff review, and synthesis.
+2. **Opus** otherwise.
+
+`ff-doctor --live` probes availability with a 1-turn `claude -p "reply: ok"
+--model claude-fable-5 --max-turns 1` call and prints
+`orchestrator	fable|opus` so a wrapper (or the user) can set the session model
+accordingly. Never route the orchestrator to GLM/Codex — synthesis and judging
+stay on the strongest available brain (the native tool's "never under-power a
+judge", applied to yourself).
+
+## 5. Uniform result layout
+
+```
+<repo>/.fleetflow/<run>/
+├── journal.jsonl        # started/result records, v2 content-hash keys
+├── <id>.prompt.txt      # guard preamble + packet (what was actually sent)
+├── <id>.result.json     # claude JSON envelope (glm/anthropic brains)
+├── <id>.last.txt        # codex last-message (codex brain)
+├── <id>.events.jsonl    # codex --json event stream (codex brain)
+├── <id>.err             # stderr
+└── wt-<id>/             # the lane worktree (branch fleetflow/<run>/<id>)
+```
+
+`.fleetflow/` sits at the repo top (never under `.claude/` — Claude Code's
+sensitive-file guard fires there before `bypassPermissions`) and must be
+gitignored; `ff-spawn` appends it to `.git/info/exclude` if absent.

+ 110 - 0
skills/fleetflow/scripts/ff-collect.sh

@@ -0,0 +1,110 @@
+#!/usr/bin/env bash
+# ff-collect.sh - gate one fleetflow lane's result, or run the escape guard.
+#
+# Per-brain success semantics: Claude-harness brains (glm/sonnet/opus/haiku/
+# fable) gate on the JSON envelope's is_error; codex gates on a non-empty
+# last-message (plus JSON validity when a schema was used). The escape guard
+# compares the main checkout's status against the baseline snapshotted at
+# first spawn — new entries mean a worker wrote outside its lane.
+# stdout: the worker's final text (data). stderr: chatter.
+#
+# Exit codes: 0 pass | 2 usage | 3 artifact missing | 10 gate failed
+#             12 escape detected
+set -u
+
+usage() {
+  cat <<'EOF'
+Usage: ff-collect.sh --run NAME --id ID [--repo PATH] [--schema]
+       ff-collect.sh --check-main-clean [--repo PATH] [--run NAME]
+
+  --run NAME           run name
+  --id ID              lane id to gate
+  --repo PATH          repo root (default: git toplevel of cwd)
+  --schema             the lane used a JSON schema: require the final text to
+                       parse as JSON
+  --check-main-clean   escape guard - compare the MAIN checkout's git status
+                       against the run's baseline; exit 12 on new entries
+
+EXAMPLES
+  ff-collect.sh --run audit --id ts-refresh
+  ff-collect.sh --run audit --id dissent-1 --schema
+  ff-collect.sh --check-main-clean --run audit
+EOF
+}
+
+err() { echo "ff-collect: $*" >&2; }
+
+RUN="" ID="" REPO="" SCHEMA=0 CHECK_CLEAN=0
+while [ $# -gt 0 ]; do
+  case "$1" in
+    --run) RUN="${2:-}"; shift 2 ;;
+    --id) ID="${2:-}"; shift 2 ;;
+    --repo) REPO="${2:-}"; shift 2 ;;
+    --schema) SCHEMA=1; shift ;;
+    --check-main-clean) CHECK_CLEAN=1; shift ;;
+    -h|--help) usage; exit 0 ;;
+    *) err "unknown argument: $1"; usage >&2; exit 2 ;;
+  esac
+done
+
+command -v jq >/dev/null || { err "jq required"; exit 2; }
+[ -n "$REPO" ] || REPO="$(git rev-parse --show-toplevel 2>/dev/null)" || true
+[ -n "$REPO" ] && [ -d "$REPO" ] || { err "not in a git repo (or --repo invalid)"; exit 2; }
+
+# --- escape guard -------------------------------------------------------------
+if [ "$CHECK_CLEAN" = 1 ]; then
+  BASELINE=""
+  if [ -n "$RUN" ] && [ -f "$REPO/.fleetflow/$RUN/main-baseline.txt" ]; then
+    BASELINE="$REPO/.fleetflow/$RUN/main-baseline.txt"
+  else
+    # fall back to the most recent run's baseline
+    BASELINE="$(ls -t "$REPO/.fleetflow"/*/main-baseline.txt 2>/dev/null | head -1)"
+  fi
+  [ -n "$BASELINE" ] && [ -f "$BASELINE" ] || { err "no baseline found - run ff-spawn first"; exit 2; }
+  NOW="$(git -C "$REPO" status --porcelain 2>/dev/null)"
+  NEW="$(comm -13 <(sort "$BASELINE") <(printf '%s\n' "$NOW" | sort) | grep -v '^\s*$' || true)"
+  if [ -n "$NEW" ]; then
+    err "ESCAPE DETECTED - main checkout changed since baseline:"
+    printf '%s\n' "$NEW"
+    err "salvage: git -C \"$REPO\" stash push -u -- <path>; then apply in the lane"
+    exit 12
+  fi
+  echo "main-clean"
+  exit 0
+fi
+
+# --- lane gate ------------------------------------------------------------
+[ -n "$RUN" ] && [ -n "$ID" ] || { err "--run and --id required"; usage >&2; exit 2; }
+RUNDIR="$REPO/.fleetflow/$RUN"
+JOURNAL="$RUNDIR/journal.jsonl"
+
+BRAIN=""
+[ -f "$JOURNAL" ] && \
+  BRAIN="$(jq -r --arg id "$ID" 'select(.type=="result" and .id==$id) | .brain' "$JOURNAL" 2>/dev/null | tail -1)"
+
+if [ "$BRAIN" = "codex" ] || { [ -z "$BRAIN" ] && [ -f "$RUNDIR/$ID.last.txt" ]; }; then
+  ART="$RUNDIR/$ID.last.txt"
+  [ -s "$ART" ] || { err "codex last-message missing/empty: $ART"; exit 3; }
+  if [ "$SCHEMA" = 1 ]; then
+    jq empty < "$ART" 2>/dev/null || { err "final message is not valid JSON (schema lane)"; exit 10; }
+  fi
+  cat "$ART"
+  exit 0
+fi
+
+ART="$RUNDIR/$ID.result.json"
+[ -s "$ART" ] || { err "result missing/empty: $ART"; exit 3; }
+# NB: jq's // treats boolean false as absent, so probe with has()
+IS_ERR="$(jq -r 'if has("is_error") then (.is_error|tostring) else "missing" end' "$ART" 2>/dev/null)"
+[ -n "$IS_ERR" ] && [ "$IS_ERR" != "missing" ] || { err "unparseable result envelope: $ART"; exit 10; }
+if [ "$IS_ERR" != "false" ]; then
+  err "worker reported is_error=$IS_ERR"
+  jq -r '.result // empty' "$ART" | head -5 >&2
+  exit 10
+fi
+TEXT="$(jq -r '.result // empty' "$ART")"
+if [ "$SCHEMA" = 1 ]; then
+  printf '%s' "$TEXT" | jq empty 2>/dev/null || { err "final text is not valid JSON (schema lane)"; exit 10; }
+fi
+printf '%s\n' "$TEXT"
+exit 0

+ 97 - 0
skills/fleetflow/scripts/ff-doctor.sh

@@ -0,0 +1,97 @@
+#!/usr/bin/env bash
+# ff-doctor.sh - fleetflow preflight: prove the heterogeneous fleet can run.
+#
+# --offline (default): structural checks only - binaries, sibling launchers,
+#   script syntax. CI-safe, no network.
+# --live: additionally probe each provider - GLM endpoint (via fleet-doctor),
+#   Codex auth, Anthropic model availability - and report which orchestrator
+#   tier is available (fable > opus).
+# stdout: one TSV line per check: name<TAB>status<TAB>detail. stderr: chatter.
+#
+# Exit codes: 0 all required checks ok | 2 usage | 7 live probe unreachable
+#             10 structural failure
+set -u
+
+usage() {
+  cat <<'EOF'
+Usage: ff-doctor.sh [--offline | --live]
+
+  --offline   structural checks only (default; CI-safe)
+  --live      also probe GLM endpoint, Codex auth, Anthropic models;
+              reports orchestrator tier (fable|opus)
+
+EXAMPLES
+  ff-doctor.sh --offline
+  ff-doctor.sh --live
+EOF
+}
+
+MODE="offline"
+case "${1:-}" in
+  --offline|"") MODE="offline" ;;
+  --live) MODE="live" ;;
+  -h|--help) usage; exit 0 ;;
+  *) echo "ff-doctor: unknown argument: $1" >&2; usage >&2; exit 2 ;;
+esac
+
+FAIL=0; UNREACH=0
+say() { printf '%s\t%s\t%s\n' "$1" "$2" "$3"; }
+
+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+# --- structural ---------------------------------------------------------------
+for bin in git jq sha256sum; do
+  if command -v "$bin" >/dev/null; then say "bin-$bin" ok "found"; else say "bin-$bin" fail "missing"; FAIL=1; fi
+done
+if command -v claude >/dev/null; then say "bin-claude" ok "found"; else say "bin-claude" fail "missing"; FAIL=1; fi
+if command -v codex >/dev/null; then say "bin-codex" ok "$(codex --version 2>/dev/null | head -1)"; else say "bin-codex" advisory "missing - codex brain unavailable"; fi
+
+FW="${FLEETFLOW_FLEET_WORKER:-$HOME/.claude/skills/fleet-worker/scripts/fleet-worker}"
+if [ -f "$FW" ]; then say "fleet-worker" ok "$FW"; else say "fleet-worker" advisory "not installed - glm brain unavailable"; fi
+
+for s in ff-spawn.sh ff-collect.sh; do
+  if bash -n "$HERE/$s" 2>/dev/null; then say "syntax-$s" ok "parses"; else say "syntax-$s" fail "syntax error"; FAIL=1; fi
+done
+[ -f "$HERE/../assets/guard-preamble.txt" ] && say "guard-preamble" ok "present" || { say "guard-preamble" fail "missing"; FAIL=1; }
+
+[ "$MODE" = "live" ] || { [ "$FAIL" = 0 ] && exit 0 || exit 10; }
+
+# --- live probes ----------------------------------------------------------------
+FD="$(dirname "$FW")/fleet-doctor.sh"
+if [ -f "$FD" ]; then
+  if bash "$FD" --live 2>/dev/null | grep -q "live-ping	ok"; then
+    say "glm-endpoint" ok "model resolves"
+  else
+    say "glm-endpoint" unreachable "fleet-doctor --live did not confirm (key/endpoint)"; UNREACH=1
+  fi
+else
+  say "glm-endpoint" advisory "fleet-doctor not installed"
+fi
+
+if command -v codex >/dev/null; then
+  if timeout 30 codex login status 2>&1 | grep -qi "logged in"; then
+    say "codex-auth" ok "$(timeout 30 codex login status 2>&1 | head -1)"
+  else
+    say "codex-auth" unreachable "not logged in (codex login)"; UNREACH=1
+  fi
+fi
+
+ORCH="none"
+if command -v claude >/dev/null; then
+  for m in claude-fable-5 opus; do
+    R="$(timeout 120 claude -p "reply with exactly: ok" --model "$m" --max-turns 1 --output-format json 2>/dev/null)"
+    if [ -n "$R" ] && [ "$(printf '%s' "$R" | jq -r 'if has("is_error") then (.is_error|tostring) else "true" end' 2>/dev/null)" = "false" ]; then
+      case "$m" in claude-fable-5) ORCH="fable" ;; *) ORCH="opus" ;; esac
+      say "model-$m" ok "responds"
+      break
+    else
+      say "model-$m" unavailable "no successful reply"
+    fi
+  done
+fi
+say "orchestrator" "$([ "$ORCH" = none ] && echo unreachable || echo ok)" "$ORCH"
+[ "$ORCH" = "none" ] && UNREACH=1
+
+if [ "$FAIL" != 0 ]; then exit 10; fi
+[ "$UNREACH" != 0 ] && exit 7
+exit 0

+ 170 - 0
skills/fleetflow/scripts/ff-spawn.sh

@@ -0,0 +1,170 @@
+#!/usr/bin/env bash
+# ff-spawn.sh - spawn one fleetflow worker lane (GLM / Codex / Anthropic brain).
+#
+# Creates the run dir + optional worktree lane, injects the guard preamble,
+# journals a hash-keyed started/result pair (native-Workflow-style replay
+# cache), launches the brain-appropriate process, and writes its artifacts.
+# stdout: the artifact path (data). stderr: progress chatter.
+#
+# Exit codes: 0 ok | 2 usage | 3 cache hit (cached artifact path on stdout)
+#             5 missing dependency | 10 worker failed
+set -u
+
+usage() {
+  cat <<'EOF'
+Usage: ff-spawn.sh --run NAME --id ID --brain BRAIN --prompt-file FILE
+                   [--worktree] [--base BRANCH] [--repo PATH] [--max-turns N]
+                   [--schema FILE] [--no-guard] [--force] [--dry-run]
+
+  --run NAME       run name (groups lanes; [a-z0-9-]+)
+  --id ID          lane id within the run ([a-z0-9-]+)
+  --brain BRAIN    glm | codex | sonnet | opus | haiku | fable
+  --prompt-file F  packet file (guard preamble is prepended unless --no-guard)
+  --worktree       give the worker its own worktree lane (branch fleetflow/RUN/ID)
+  --base BRANCH    worktree base (default: main, falls back to HEAD)
+  --repo PATH      repo root (default: git toplevel of cwd)
+  --max-turns N    worker turn cap (default: 100)
+  --schema FILE    JSON Schema for the final answer (codex: native
+                   --output-schema; other brains: appended to the prompt)
+  --no-guard       skip the guard preamble injection
+  --force          ignore a journal cache hit and re-run
+  --dry-run        do not launch a worker; write a stub result (for tests/planning)
+
+EXAMPLES
+  ff-spawn.sh --run audit --id ts-refresh --brain glm --worktree \
+              --prompt-file packets/ts.txt
+  ff-spawn.sh --run audit --id dissent-1 --brain codex \
+              --prompt-file packets/refute.txt --schema verdict.schema.json
+  ff-spawn.sh --run audit --id judge --brain opus --prompt-file packets/judge.txt
+EOF
+}
+
+err() { echo "ff-spawn: $*" >&2; }
+
+RUN="" ID="" BRAIN="" PROMPT_FILE="" WORKTREE=0 BASE="main" REPO=""
+MAX_TURNS=100 SCHEMA="" GUARD=1 FORCE=0 DRYRUN=0
+while [ $# -gt 0 ]; do
+  case "$1" in
+    --run) RUN="${2:-}"; shift 2 ;;
+    --id) ID="${2:-}"; shift 2 ;;
+    --brain) BRAIN="${2:-}"; shift 2 ;;
+    --prompt-file) PROMPT_FILE="${2:-}"; shift 2 ;;
+    --worktree) WORKTREE=1; shift ;;
+    --base) BASE="${2:-}"; shift 2 ;;
+    --repo) REPO="${2:-}"; shift 2 ;;
+    --max-turns) MAX_TURNS="${2:-}"; shift 2 ;;
+    --schema) SCHEMA="${2:-}"; shift 2 ;;
+    --no-guard) GUARD=0; shift ;;
+    --force) FORCE=1; shift ;;
+    --dry-run) DRYRUN=1; shift ;;
+    -h|--help) usage; exit 0 ;;
+    *) err "unknown argument: $1"; usage >&2; exit 2 ;;
+  esac
+done
+
+case "$BRAIN" in glm|codex|sonnet|opus|haiku|fable) ;; *) err "invalid --brain '$BRAIN'"; exit 2 ;; esac
+echo "$RUN" | grep -qE '^[a-z0-9-]+$' || { err "invalid --run"; exit 2; }
+echo "$ID"  | grep -qE '^[a-z0-9-]+$' || { err "invalid --id"; exit 2; }
+[ -f "$PROMPT_FILE" ] || { err "prompt file not found: $PROMPT_FILE"; exit 2; }
+[ -z "$SCHEMA" ] || [ -f "$SCHEMA" ] || { err "schema file not found: $SCHEMA"; exit 2; }
+command -v jq >/dev/null || { err "jq required"; exit 5; }
+command -v git >/dev/null || { err "git required"; exit 5; }
+
+[ -n "$REPO" ] || REPO="$(git rev-parse --show-toplevel 2>/dev/null)" || true
+[ -n "$REPO" ] && [ -d "$REPO" ] || { err "not in a git repo (or --repo invalid)"; exit 2; }
+
+RUNDIR="$REPO/.fleetflow/$RUN"
+mkdir -p "$RUNDIR"
+# keep the scratch tree out of git without touching the repo's .gitignore
+EXCL="$(git -C "$REPO" rev-parse --absolute-git-dir)/info/exclude"
+mkdir -p "$(dirname "$EXCL")"
+grep -qs '^\.fleetflow/$' "$EXCL" 2>/dev/null || echo ".fleetflow/" >> "$EXCL"
+
+# escape-guard baseline: snapshot the main checkout's status once per run
+BASELINE="$RUNDIR/main-baseline.txt"
+[ -f "$BASELINE" ] || git -C "$REPO" status --porcelain > "$BASELINE" 2>/dev/null
+
+# --- build the effective prompt ---------------------------------------------
+SENT="$RUNDIR/$ID.prompt.txt"
+: > "$SENT"
+if [ "$GUARD" = 1 ]; then
+  PRE="$(dirname "${BASH_SOURCE[0]}")/../assets/guard-preamble.txt"
+  [ -f "$PRE" ] && { cat "$PRE" >> "$SENT"; echo >> "$SENT"; }
+fi
+cat "$PROMPT_FILE" >> "$SENT"
+if [ -n "$SCHEMA" ] && [ "$BRAIN" != "codex" ]; then
+  { echo; echo "FINAL REPLY MUST be a single JSON object valid against this schema:";
+    cat "$SCHEMA"; } >> "$SENT"
+fi
+
+# --- journal: hash-keyed replay cache (native Workflow pattern) --------------
+OPTS="turns=$MAX_TURNS|wt=$WORKTREE|schema=$( [ -n "$SCHEMA" ] && basename "$SCHEMA" )"
+KEY="v2:$( { printf '%s\n' "$BRAIN"; cat "$SENT"; printf '%s' "$OPTS"; } | sha256sum | cut -d' ' -f1)"
+JOURNAL="$RUNDIR/journal.jsonl"
+if [ "$FORCE" = 0 ] && [ -f "$JOURNAL" ]; then
+  CACHED="$(jq -r --arg k "$KEY" 'select(.type=="result" and .key==$k and .rc==0) | .artifact' "$JOURNAL" 2>/dev/null | tail -1)"
+  if [ -n "$CACHED" ] && [ -f "$CACHED" ]; then
+    err "cache hit for $ID (unchanged packet) - use --force to re-run"
+    echo "$CACHED"
+    exit 3
+  fi
+fi
+
+# --- worktree lane ------------------------------------------------------------
+WORKDIR="$REPO"
+if [ "$WORKTREE" = 1 ]; then
+  WORKDIR="$RUNDIR/wt-$ID"
+  if [ ! -d "$WORKDIR" ]; then
+    git -C "$REPO" show-ref --verify --quiet "refs/heads/$BASE" || BASE="HEAD"
+    git -C "$REPO" worktree add -q -b "fleetflow/$RUN/$ID" "$WORKDIR" "$BASE" \
+      || { err "worktree add failed"; exit 10; }
+  fi
+fi
+
+jq -nc --arg k "$KEY" --arg id "$ID" --arg b "$BRAIN" \
+  '{type:"started",key:$k,id:$id,brain:$b}' >> "$JOURNAL"
+
+# --- launch -------------------------------------------------------------------
+ART="$RUNDIR/$ID.result.json"
+ERRF="$RUNDIR/$ID.err"
+RC=0
+if [ "$DRYRUN" = 1 ]; then
+  jq -nc '{is_error:false,result:"DRYRUN"}' > "$ART"
+else
+  case "$BRAIN" in
+    glm)
+      FW="${FLEETFLOW_FLEET_WORKER:-$HOME/.claude/skills/fleet-worker/scripts/fleet-worker}"
+      [ -f "$FW" ] || { err "fleet-worker launcher not found ($FW)"; exit 5; }
+      ( cd "$WORKDIR" && \
+        FLEET_WORKER_CONFIG_DIR="${FLEET_WORKER_CONFIG_DIR:-$HOME/.fleet-worker/cfg-ff-$ID}" \
+        bash "$FW" --output-format json --max-turns "$MAX_TURNS" "$(cat "$SENT")" \
+      ) > "$ART" 2> "$ERRF" || RC=$?
+      ;;
+    codex)
+      command -v codex >/dev/null || { err "codex CLI not found"; exit 5; }
+      ART="$RUNDIR/$ID.last.txt"
+      ( cd "$WORKDIR" && \
+        codex exec --full-auto --ephemeral --color never --json \
+          ${FLEETFLOW_CODEX_MODEL:+-m "$FLEETFLOW_CODEX_MODEL"} \
+          ${SCHEMA:+--output-schema "$SCHEMA"} \
+          -o "$ART" - < "$SENT" \
+      ) > "$RUNDIR/$ID.events.jsonl" 2> "$ERRF" || RC=$?
+      ;;
+    sonnet|opus|haiku|fable)
+      command -v claude >/dev/null || { err "claude CLI not found"; exit 5; }
+      MODEL="$BRAIN"; [ "$BRAIN" = "fable" ] && MODEL="claude-fable-5"
+      ( cd "$WORKDIR" && \
+        claude -p --model "$MODEL" --output-format json --max-turns "$MAX_TURNS" \
+          --permission-mode "${FLEETFLOW_PERMISSION_MODE:-bypassPermissions}" \
+        < "$SENT" \
+      ) > "$ART" 2> "$ERRF" || RC=$?
+      ;;
+  esac
+fi
+
+jq -nc --arg k "$KEY" --arg id "$ID" --arg b "$BRAIN" --arg a "$ART" --argjson rc "$RC" \
+  '{type:"result",key:$k,id:$id,brain:$b,rc:$rc,artifact:$a}' >> "$JOURNAL"
+
+echo "$ART"
+if [ "$RC" -ne 0 ]; then err "worker exited rc=$RC (see $ERRF)"; exit 10; fi
+exit 0

+ 95 - 0
skills/fleetflow/tests/run.sh

@@ -0,0 +1,95 @@
+#!/usr/bin/env bash
+# Offline behavioural suite for the fleetflow skill scripts.
+# Self-contained: builds a throwaway git repo, exercises spawn/collect/doctor
+# via --dry-run (no network, no workers). Exits nonzero on any failure.
+set -u
+
+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+S="$HERE/../scripts"
+PASS=0; FAILN=0
+ok()   { PASS=$((PASS+1)); echo "  PASS  $1"; }
+bad()  { FAILN=$((FAILN+1)); echo "  FAIL  $1"; }
+check() { # desc, expected-rc, cmd...
+  local desc="$1" want="$2"; shift 2
+  "$@" >/dev/null 2>&1; local got=$?
+  [ "$got" = "$want" ] && ok "$desc (exit $want)" || bad "$desc: wanted $want got $got"
+}
+
+command -v jq >/dev/null 2>&1 || { echo "SKIP: jq unavailable on this platform"; exit 0; }
+command -v git >/dev/null 2>&1 || { echo "SKIP: git unavailable"; exit 0; }
+
+TMP="$(mktemp -d)"
+trap 'rm -rf "$TMP"' EXIT
+REPO="$TMP/repo"
+mkdir -p "$REPO" && git -C "$REPO" init -q -b main
+git -C "$REPO" -c user.email=t@t -c user.name=t commit -q --allow-empty -m init
+PKT="$TMP/packet.txt"; echo "Do the thing. FINAL REPLY: one line." > "$PKT"
+
+# --- syntax + help ------------------------------------------------------------
+for s in ff-spawn.sh ff-collect.sh ff-doctor.sh; do
+  bash -n "$S/$s" 2>/dev/null && ok "syntax $s" || bad "syntax $s"
+  bash "$S/$s" --help 2>/dev/null | grep -q "EXAMPLES" && ok "$s --help has EXAMPLES" || bad "$s --help lacks EXAMPLES"
+  check "$s --help exits 0" 0 bash "$S/$s" --help
+done
+
+# --- usage validation -----------------------------------------------------------
+check "spawn: no args" 2 bash "$S/ff-spawn.sh"
+check "spawn: bad brain" 2 bash "$S/ff-spawn.sh" --run r1 --id a --brain gpt9 --prompt-file "$PKT" --repo "$REPO"
+check "spawn: bad run name" 2 bash "$S/ff-spawn.sh" --run "R 1" --id a --brain glm --prompt-file "$PKT" --repo "$REPO"
+check "spawn: missing prompt file" 2 bash "$S/ff-spawn.sh" --run r1 --id a --brain glm --prompt-file "$TMP/nope" --repo "$REPO"
+check "collect: no args" 2 bash "$S/ff-collect.sh" --repo "$REPO"
+check "doctor: bad flag" 2 bash "$S/ff-doctor.sh" --frobnicate
+
+# --- dry-run lifecycle -----------------------------------------------------------
+check "spawn: dry-run ok" 0 bash "$S/ff-spawn.sh" --run r1 --id a --brain sonnet --prompt-file "$PKT" --repo "$REPO" --dry-run
+[ -f "$REPO/.fleetflow/r1/a.result.json" ] && ok "artifact written" || bad "artifact missing"
+[ -f "$REPO/.fleetflow/r1/journal.jsonl" ] && ok "journal exists" || bad "journal missing"
+N_STARTED="$(jq -r 'select(.type=="started")|.key' "$REPO/.fleetflow/r1/journal.jsonl" | wc -l)"
+N_RESULT="$(jq -r 'select(.type=="result")|.key' "$REPO/.fleetflow/r1/journal.jsonl" | wc -l)"
+[ "$N_STARTED" -ge 1 ] && [ "$N_RESULT" -ge 1 ] && ok "journal has started+result" || bad "journal records missing"
+grep -q '^v2:' <(jq -r '.key' "$REPO/.fleetflow/r1/journal.jsonl") && ok "keys carry v2: prefix" || bad "key prefix wrong"
+grep -qs '^\.fleetflow/$' "$REPO/.git/info/exclude" && ok ".fleetflow gitignored via info/exclude" || bad "info/exclude not updated"
+[ -f "$REPO/.fleetflow/r1/main-baseline.txt" ] && ok "escape baseline snapshotted" || bad "baseline missing"
+grep -q "relative to cwd" "$REPO/.fleetflow/r1/a.prompt.txt" && ok "guard preamble injected" || bad "guard preamble absent"
+
+# resume: identical packet -> cache hit (exit 3); --force -> re-run (exit 0)
+check "spawn: cache hit on identical packet" 3 bash "$S/ff-spawn.sh" --run r1 --id a --brain sonnet --prompt-file "$PKT" --repo "$REPO" --dry-run
+check "spawn: --force re-runs" 0 bash "$S/ff-spawn.sh" --run r1 --id a --brain sonnet --prompt-file "$PKT" --repo "$REPO" --dry-run --force
+echo "changed" >> "$PKT"
+check "spawn: changed packet re-runs" 0 bash "$S/ff-spawn.sh" --run r1 --id a --brain sonnet --prompt-file "$PKT" --repo "$REPO" --dry-run
+
+# worktree lane creation
+check "spawn: worktree lane" 0 bash "$S/ff-spawn.sh" --run r1 --id lane --brain sonnet --prompt-file "$PKT" --repo "$REPO" --dry-run --worktree
+git -C "$REPO" show-ref --verify --quiet refs/heads/fleetflow/r1/lane && ok "lane branch created" || bad "lane branch missing"
+[ -d "$REPO/.fleetflow/r1/wt-lane" ] && ok "lane worktree created" || bad "lane worktree missing"
+
+# --- collect gating ---------------------------------------------------------------
+check "collect: dry-run result passes" 0 bash "$S/ff-collect.sh" --run r1 --id a --repo "$REPO"
+OUT="$(bash "$S/ff-collect.sh" --run r1 --id a --repo "$REPO" 2>/dev/null)"
+[ "$OUT" = "DRYRUN" ] && ok "collect prints final text" || bad "collect text wrong: '$OUT'"
+jq -nc '{is_error:true,result:"boom"}' > "$REPO/.fleetflow/r1/bad.result.json"
+jq -nc '{type:"result",key:"v2:x",id:"bad",brain:"sonnet",rc:0,artifact:"x"}' >> "$REPO/.fleetflow/r1/journal.jsonl"
+check "collect: is_error=true fails gate" 10 bash "$S/ff-collect.sh" --run r1 --id bad --repo "$REPO"
+check "collect: missing artifact" 3 bash "$S/ff-collect.sh" --run r1 --id ghost --repo "$REPO"
+# codex-style artifact: last.txt path with schema validation
+printf '{"verdict":"ok"}' > "$REPO/.fleetflow/r1/cx.last.txt"
+check "collect: codex last-message passes" 0 bash "$S/ff-collect.sh" --run r1 --id cx --repo "$REPO"
+check "collect: codex schema-valid JSON" 0 bash "$S/ff-collect.sh" --run r1 --id cx --repo "$REPO" --schema
+printf 'not json' > "$REPO/.fleetflow/r1/cx.last.txt"
+check "collect: codex schema-invalid fails" 10 bash "$S/ff-collect.sh" --run r1 --id cx --repo "$REPO" --schema
+
+# --- escape guard ------------------------------------------------------------------
+check "escape guard: clean main" 0 bash "$S/ff-collect.sh" --check-main-clean --run r1 --repo "$REPO"
+echo rogue > "$REPO/rogue.txt"
+check "escape guard: detects new file" 12 bash "$S/ff-collect.sh" --check-main-clean --run r1 --repo "$REPO"
+rm "$REPO/rogue.txt"
+check "escape guard: clean again" 0 bash "$S/ff-collect.sh" --check-main-clean --run r1 --repo "$REPO"
+
+# --- doctor (offline only; never hits network) ---------------------------------------
+bash "$S/ff-doctor.sh" --offline >/dev/null 2>&1; RC=$?
+[ "$RC" = 0 ] || [ "$RC" = 10 ] && ok "doctor --offline runs (rc=$RC)" || bad "doctor --offline rc=$RC"
+bash "$S/ff-doctor.sh" --offline 2>/dev/null | grep -qE "^bin-jq	ok" && ok "doctor TSV output" || bad "doctor TSV output missing"
+
+echo "=== $PASS passed, $FAILN failed ==="
+[ "$FAILN" = 0 ] || exit 1
+exit 0