Просмотр исходного кода

refactor!: Extract fleetflow to its own repo (X:\Forge\fleetflow)

An app with a live service (fleetflow.lab dashboard), its own server stack,
and a growing multi-provider surface is a product, not a skill resource —
the iso-studio rule. Extracted with full history via git subtree split
(fleetflow-extract). /fleetflow remains invocable: ~/.claude/skills/fleetflow
is a junction to the repo, and the Process Compose service now runs from the
repo directly (immune to skill-sync operations). Skill count 103 -> 102;
parallel-ops/fleet-worker cross-references updated to point at the repo.

BREAKING CHANGE: skills/fleetflow no longer ships with the claude-mods
plugin. Mount X:\Forge\fleetflow (junction or clone) to keep /fleetflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0xDarkMatter 1 месяц назад
Родитель
Сommit
fbbd5c3077

+ 3 - 3
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)
-- **103 skills** for CLI tools, patterns, workflows, and development tasks (incl. `rembg-ops` for transparent-PNG cutouts of flat illustration/sticker/avatar art with a deterministic fallback ladder past rembg's ML failure modes; `parallel-ops` as the router for the parallel/recurring-agent-work family — fleet-ops, fleet-worker, fleetflow, loop-ops, iterate, spawn — read it first when that family is ambiguous; `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)
+- **102 skills** for CLI tools, patterns, workflows, and development tasks (incl. `rembg-ops` for transparent-PNG cutouts of flat illustration/sticker/avatar art with a deterministic fallback ladder past rembg's ML failure modes; `parallel-ops` as the router for the parallel/recurring-agent-work family — fleet-ops, fleet-worker, fleetflow (extracted to `X:\Forge\fleetflow`, mounted as a skill via junction), loop-ops, iterate, spawn — read it first when that family is ambiguous; `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`
@@ -60,8 +60,8 @@ On "INIT:" message at session start:
 | `skills/auto-skill/` | Auto-detect skill-worthy workflows; Stop hook suggests after complex sessions. `/auto-skill on/off/status` to toggle |
 | `skills/supply-chain-defense/` | Behavioural-first dependency security - Socket.dev depscore MCP, exposure-check (IOC match across npm/pnpm/yarn/bun/PyPI/Composer/Cargo/Go/RubyGems + extensions), integrity-audit (persistence), scan-extensions, install/manifest hooks. Paired with `rules/supply-chain.md` |
 | `skills/repo-doctor/` | Agentic-quality auditor - scores any repo (entry docs, comments, structure, gates, doc-pairing) with --json + --strict CI gate; monorepo-structure + comment-doctrine references. Paired with `rules/agentic-quality.md` |
-| `skills/parallel-ops/` | Router for the parallel/recurring-agent-work family (fleet-ops, fleet-worker, fleetflow, loop-ops, iterate, spawn) - read first when it's unclear which one owns a fan-out/schedule/delegation ask |
-| `skills/fleetflow/` | Heterogeneous GLM/Codex/Anthropic worker fleets from one session; escape guard + baseline-before-closeout discipline documented in-skill |
+| `skills/parallel-ops/` | Router for the parallel/recurring-agent-work family (fleet-ops, fleet-worker, fleetflow — own repo at `X:\Forge\fleetflow` — loop-ops, iterate, spawn) - read first when it's unclear which one owns a fan-out/schedule/delegation ask |
+| `X:\Forge\fleetflow` (own repo) | Heterogeneous GLM/Codex/Grok/Pi/Anthropic worker fleets from one session; extracted 2026-08-01, mounted as `/fleetflow` via junction at `~/.claude/skills/fleetflow` |
 | `tests/validate.sh` | Frontmatter + naming gate; enforces the description-budget cap (combined description+when_to_use, hard-fails over budget) |
 | `tests/doc-drift.sh` | Counts-on-disk vs docs gate; also checks section-map markers and skill-frontmatter ghost references (related-skills/depends-on naming a skill not on disk) |
 

+ 13 - 15
CHANGELOG.md

@@ -7,21 +7,19 @@ 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.
+### Removed
+- **`fleetflow` skill extracted to its own repo** (`X:\Forge\fleetflow`) with
+  full history via `git subtree split` (36+ commits) — an app with a live
+  service (the machine-wide dashboard at `https://fleetflow.lab`), its own
+  server (`ff-serve.py` + `ff-aggregate.py` + `ff-archive.sh`, recovered from
+  the authoring session's transcript after the 2026-08-01 installer incident
+  destroyed the unversioned machine-local copies), and a growing surface is a
+  product, not a skill resource (the iso-studio rule). Still invocable as
+  `/fleetflow`: `~/.claude/skills/fleetflow` is now a junction to the repo,
+  and the `fleetflow` Process Compose service runs from the repo directly.
+  Skill count 103 → 102. The never-released "Added" entry for fleetflow that
+  previously sat in this section travels with the extraction — its narrative
+  lives in the new repo's README and git history.
 - **`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,

+ 5 - 6
README.md

@@ -12,13 +12,13 @@
 
 > *A comprehensive extension toolkit that transforms Claude Code into a specialized development powerhouse.*
 
-**claude-mods** is a production-ready plugin that extends Claude Code with 103 specialized skills, 3 expert agents, 13 output styles, 13 hooks, and modern CLI tools designed for real-world development workflows. Whether you're debugging React hooks, optimizing PostgreSQL queries, or building production CLI applications, this toolkit equips Claude with the domain expertise and procedural knowledge to work at expert level across multiple technology stacks.
+**claude-mods** is a production-ready plugin that extends Claude Code with 102 specialized skills, 3 expert agents, 13 output styles, 13 hooks, and modern CLI tools designed for real-world development workflows. Whether you're debugging React hooks, optimizing PostgreSQL queries, or building production CLI applications, this toolkit equips Claude with the domain expertise and procedural knowledge to work at expert level across multiple technology stacks.
 
 Built on the [Agent Skills specification](https://agentskills.io/specification) (an open standard backed by Anthropic, Vercel, Google, Microsoft, and 40+ agent platforms), claude-mods fills critical gaps in Claude Code's capabilities: persistent session state that survives across machines, on-demand expert knowledge for specialized domains, token-efficient modern CLI tools (10-100x faster than traditional alternatives), and proven workflow patterns for TDD, code review, and feature development. The toolkit implements Anthropic's [recommended patterns for long-running agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents), ensuring your development context never vanishes when sessions end.
 
 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. 103 skills. 13 styles. 13 hooks. 14 rules. One install.**
+**3 agents. 102 skills. 13 styles. 13 hooks. 14 rules. One install.**
 
 ## Recent Updates
 
@@ -88,7 +88,7 @@ Claude Code is powerful out of the box, but it has gaps. This toolkit fills them
 
 - **Session continuity** — Tasks vanish when sessions end. We fix that with `/save` and `/sync`, implementing Anthropic's [recommended pattern](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents) for long-running agents.
 
-- **Expert-level knowledge on demand** — 103 on-demand skills covering React, TypeScript, Python, Go, Rust, PostgreSQL, and more, plus 3 specialized agents reserved for genuine context-isolation/worker roles (git operations, web scraping, project reorganization). Skills-first: knowledge loads when relevant instead of living in heavyweight agent prompts.
+- **Expert-level knowledge on demand** — 102 on-demand skills covering React, TypeScript, Python, Go, Rust, PostgreSQL, and more, plus 3 specialized agents reserved for genuine context-isolation/worker roles (git operations, web scraping, project reorganization). Skills-first: knowledge loads when relevant instead of living in heavyweight agent prompts.
 
 - **Modern CLI tools** — Stop using `grep`, `find`, and `cat`. Our rules automatically prefer `ripgrep`, `fd`, `eza`, and `bat` — 10-100x faster and token-efficient.
 
@@ -113,7 +113,7 @@ claude-mods/
 ├── .claude-plugin/     # Plugin metadata
 ├── agents/             # Expert subagents (3)
 ├── commands/           # Slash commands (3)
-├── skills/             # Custom skills (103)
+├── skills/             # Custom skills (102)
 ├── output-styles/      # Response personalities
 ├── hooks/              # Hook examples & docs
 ├── rules/              # Claude Code rules
@@ -297,10 +297,9 @@ See [skill-creator](skills/skill-creator/) for the complete guide.
 | [git-ops](skills/git-ops/) | Git orchestrator - commits, PRs, releases, changelog. Routes to background Sonnet agent. |
 | [github-ops](skills/github-ops/) | GitHub remote ops - repo create/metadata/topics, releases + README 'Recent Updates' enforcement, issue/PR management (preview-before-send), and read-only security-posture audit + scored repo-scorecard (single repo or whole `--org`) |
 | [push-gate](skills/push-gate/) | Pre-push safety gate - gitleaks + regex secret scan, forbidden-file check, no bypass |
-| [parallel-ops](skills/parallel-ops/) | Router for parallel/recurring agent work - decision table over fleet-ops, fleet-worker, fleetflow, loop-ops, iterate, spawn |
+| [parallel-ops](skills/parallel-ops/) | Router for parallel/recurring agent work - decision table over fleet-ops, fleet-worker, fleetflow (own repo), loop-ops, iterate, spawn |
 | [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 | 103 | Operational skills, CLI tools, workflows, diagnostics, security |
+| Skills | 102 | 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 |

+ 4 - 3
skills/fleet-worker/SKILL.md

@@ -23,9 +23,10 @@ mechanism is provider- and model-agnostic (any Anthropic-compatible endpoint).
 > fleet-worker brain is a `claude -p` process pointed at an **Anthropic-compatible**
 > endpoint. Grok Build CLI is its **own binary and protocol** (`grok -p`, auth via
 > `GROK_DEPLOYMENT_KEY`), not a `claude -p` wrapper — so it can't ride fleet-worker's
-> launcher. Grok is a first-class worker in **[fleetflow](../fleetflow/)** instead
-> (alongside Codex), which spawns heterogeneous OS-process workers of different
-> binaries. Use fleetflow for a Grok lane or any mixed-provider fan-out.
+> launcher. Grok is a first-class worker in **fleetflow** instead (alongside
+> Codex), which spawns heterogeneous OS-process workers of different binaries —
+> now its own repo at `X:\Forge\fleetflow`, mounted as the `/fleetflow` skill.
+> Use fleetflow for a Grok lane or any mixed-provider fan-out.
 
 **This is the spawning layer. [`fleet-ops`](../fleet-ops/) is the landing layer.**
 fleet-worker produces branches cheaply; fleet-ops lands them through a test gate

+ 1 - 1
skills/fleet-worker/references/model-routing.md

@@ -125,7 +125,7 @@ while (budget.total && budget.remaining() > 50_000) {
 
 ## See also
 
-- [`fleetflow/references/native-model-routing.md`](../../fleetflow/references/native-model-routing.md)
+- `fleetflow` `references/native-model-routing.md` (own repo — `X:\Forge\fleetflow`, mounted at `~/.claude/skills/fleetflow`)
   — the in-process half in depth: cost evidence (7-day audit), full mechanism
   (`opts.model`/`opts.effort`, `meta.phases[].model`, fork inheritance), caveats
 - [`assets/route.js`](../assets/route.js) — the paste-in helper

+ 0 - 431
skills/fleetflow/SKILL.md

@@ -1,431 +0,0 @@
----
-name: fleetflow
-description: "Heterogeneous cross-provider fleet - GLM (z.ai), Codex (OpenAI), Grok (xAI), Anthropic Sonnet/Opus/Haiku - from one session, porting the native Workflow tool's patterns (adversarial verify, judge panels, journal resume) to OS-process workers. Triggers: fleetflow, heterogeneous/mixed-model fleet, codex worker, grok worker, cross-provider fan-out, 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'."
-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.144, 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 |
-| **Grok** (xAI grok-4.5) | `grok -p` | xAI's own agentic CLI — a different provider, model, *and* toolchain (like Codex); `GROK_DEPLOYMENT_KEY` auth |
-| **Pi** (wildcard: gemini/deepseek/zai/groq/…) | `pi -p` | earendil-works Pi — one harness fronting 15+ providers; `FLEETFLOW_PI_PROVIDER`/`_MODEL` pick the brain, provider API-key env auth ([contract §4](references/worker-contracts.md)) |
-| **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/Grok columns 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, Grok | Codex/Grok = independent harnesses; good second implementations for judge panels |
-| **verify / judge** | Opus + one cross-provider dissenter (Codex, Grok, GLM, or Pi) | *never under-power a judge*; diversity beats redundancy |
-| **wildcard / third-opinion** (a provider none of the fixed brains cover) | Pi (`FLEETFLOW_PI_PROVIDER=gemini\|deepseek\|…`) | one integration, 15+ providers; no sandbox and no turn cap — worktree + stall detector are the bounds |
-| **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 same routing applies INSIDE native Workflow scripts — and it defaults
-wrong.** A 7-day session audit (2026-06-28..07-05) found 75% of 10.1M subagent
-output tokens ran on Fable/Opus, including 729 StructuredOutput extract/verdict
-calls, because `agent()` inherits the session model unless overridden and nobody
-sets overrides. Doctrine: **the stage that decides stays premium (omit the
-override — inherit); the stages that collect go cheap** —
-`{ model: 'haiku', effort: 'low' }` for extraction/classification/log scans,
-`{ model: 'sonnet' }` for finder/reviewer sweeps. When unsure, inherit: a wrong
-cheap answer that survives verification costs more than it saves. Mechanism,
-routing table, and before/after snippets:
-[references/native-model-routing.md](references/native-model-routing.md).
-
-## 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.
-
-**Inter-worker communication is hub-and-spoke, by design.** Workers never talk
-to each other — no shared memory, no message bus, no sideband files (lanes are
-isolated worktrees). The only channel is the native tool's: a worker's FINAL
-REPLY returns through `ff-collect` to the orchestrator, which embeds it in a
-later packet (the `prevResult`-into-next-prompt handoff; see
-[insights §3/§7](references/native-workflow-insights.md)). A judge packet is
-just the collected builder outputs pasted in. If a stage needs *all* sibling
-results, that is a barrier — collect everything first, then compose. (True
-peer-to-peer between long-lived workers is out of scope; that's what a message
-bus like pigeon is for.)
-
-**Clean-room / benchmark runs get their own target repo.** Lanes are worktrees
-*of some repo* — don't graft a build experiment onto an unrelated repo's object
-store. Seed a standalone repo (e.g. under `X:\Benching`), and **vendor any
-external spec INTO it** (`spec/…`) so packets reference it by *relative* path —
-the guard preamble forbids workers building absolute paths, and Codex's
-sandbox is confined to the lane, so out-of-repo specs are unreadable anyway.
-
-**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)` — and `opts` includes `--effort`, so changing only the
-effort lever is a cache miss (a different run). 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.
-
-**Manifest & resume.** Each spawn also upserts a packet into
-`.fleetflow/<run>/manifest.json` (`{run, base, created_by, phases[], packets[]}`,
-one entry per id — idempotent — carrying `{id, brain, phase, prompt_file,
-worktree, max_turns, effort, schema, key}`). It is the orchestrator-side plan,
-distinct from the per-spawn journal: it records *what was intended* so a whole
-run can be replayed. `ff-run.sh resume --run NAME` snapshots the manifest's
-packets once, then replays each through `ff-spawn` in manifest order — unchanged
-packets cache-hit (`"cached"`), changed or new ones run live; per-lane summary
-to stderr, a JSON result list on stdout, exit 0 if all ok/cached, 10 if any
-lane failed. `ff-run status --run NAME` is an alias for `ff-status`. Snapshots
-matter: ff-spawn re-orders the live manifest on each upsert (remove-then-append),
-so the replay reads from a frozen copy. When you're done,
-`ff-clean.sh --run NAME [--force]` reclaims zero-commit lanes (worktree + branch
-deleted), keeps committed ones, and removes the run's cache dirs.
-
-**Cache & tmp redirect.** Workers' `UV_CACHE_DIR`, `TMPDIR`, `TMP`, and `TEMP`
-are pointed at `${FLEETFLOW_CACHE_ROOT:-$HOME/.fleet-worker/cache}/<run>-<id>/`
-(created before launch), so pytest/uv litter and codex's AppContainer-ACL'd
-sandbox dirs land OUTSIDE the repo and lanes — never inside a worktree that
-`git worktree remove` later needs to delete. Set `FLEETFLOW_CACHE_ROOT` once for
-the whole run and pass the same value to `ff-clean` so it can find and remove
-those dirs.
-
-## 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).
-
-## Default posture: verify by default, scale to the ask
-
-The native tool's fan-outs look "automatic" because its doctrine makes them the
-default the script-author follows, not an option — and real runs routinely hit
-30–50 agents on large tasks. fleetflow adopts the same posture:
-
-- **Every run gets a verify phase unless you state why not.** Minimum: one
-  refuter per build lane (cross-provider) + a judge for anything with more
-  than one candidate. A run that skips verification is the exception and says
-  so in its summary.
-- **Scale the fan-out to the ask, not to caution.** Mechanical batch → one
-  lane per file-disjoint packet, however many that is. Discovery/audit →
-  loop-until-dry rounds, not a fixed small N. Verification typically adds
-  0.5–1.5× the build-lane count on top. 20–50 lanes on a big task is the
-  pattern working, not a smell — the native tool budgets 1000 agent calls per
-  run for exactly this reason.
-- **Throttle in waves, don't shrink the plan.** The native engine queues past
-  `min(16, cores−2)` concurrent; fleetflow's orchestrator does the same
-  manually — spawn in waves of ≤4–6 per provider (endpoint quota binds first),
-  collect as lanes finish, keep the total plan intact. **Codex on Windows is
-  the exception: ≤2–3 concurrent.** Its binding constraint isn't endpoint quota
-  but the machine-global sandbox-provisioning helper — lanes provisioning at
-  once race each other into the elevation trap (see Safety). Bound each lane
-  (`--max-turns`), never the ambition.
-- **No silent caps** (native rule, verbatim): if you sample, top-N, or skip,
-  say so in the run summary.
-
-## 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`. Grok workers run
-  `--always-approve` (autonomous tools), confined by the lane worktree — no
-  config-dir isolation needed (no Claude OAuth to collide with; auth is the
-  `GROK_DEPLOYMENT_KEY` env var, read from env, never written to disk).
-- **Codex lanes cannot `git commit` (learned 2026-07-08, codex-cli 0.142):**
-  a worktree's git metadata (`HEAD.lock`/`index.lock`) lives under the MAIN
-  repo's `.git/worktrees/`, outside the lane the sandbox confines Codex to —
-  commits die with a lock-permission error after the work itself succeeded.
-  Convention: Codex packets say "DO NOT COMMIT — leave changes in the working
-  tree", the worker reports `FILES_CHANGED`, and the **orchestrator reviews
-  the diff and commits**. (GLM/Anthropic `claude -p` workers are unaffected
-  and may self-commit.) Tightens the gate as a side effect: nothing a Codex
-  lane produces lands without an orchestrator diff review.
-- **Codex on Windows must never depend on an elevation prompt (learned
-  2026-07-27, codex-cli 0.144.1, run `bkv2p2`):** codex-cli's `elevated`
-  Windows sandbox mode provisions its AppContainer through a setup helper
-  launched with `ShellExecuteExW` — a UAC dialog. Headless, nobody can approve
-  it, so Windows cancels the launch (error `1223` = `ERROR_CANCELLED`, surfaced
-  as `orchestrator_helper_launch_canceled`) and the lane **hangs instead of
-  failing fast** — two lanes burned 2.7h. `ff-spawn` pins
-  `-c windows.sandbox="unelevated"` **per invocation** on Windows hosts
-  (`FLEETFLOW_CODEX_WINDOWS_SANDBOX` overrides it; empty passes nothing and
-  defers to the global config). Deliberately *not* an edit to the user's
-  `~/.codex/config.toml` — interactive Codex keeps whatever mode they chose.
-  Provisioning caches under `~/.codex/.sandbox`, `.sandbox-bin`,
-  `.sandbox-secrets`, which is why ten lanes can succeed and the eleventh
-  wedges: any cache invalidation — or two lanes provisioning at once —
-  re-triggers the prompt. **`ff-doctor --live` guards the guard:** `codex -c`
-  accepts unknown dotted keys silently, so if codex ever renames or drops
-  `windows.sandbox` the pin degrades to an inert no-op and lanes hang again with
-  no signal. The doctor therefore feeds it a deliberately *invalid* value and
-  requires codex to reject it — rejection proves the key is still live. It rides
-  on `codex debug prompt-input` (config load only, ~1.2s, no network, no model
-  call) rather than `codex sandbox`, because sandbox provisioning is
-  machine-global and is the very thing being guarded against; a preflight must
-  never trigger it.
-- **A stalled lane is indistinguishable from a working one — trust
-  `last_activity_s`, not `state: running`.** Through the whole 2.7h hang
-  `ff-status` reported both lanes `running` with `elapsed_s` climbing
-  normally; the only tell was that their `<run>/<id>.events.jsonl` mtimes had
-  frozen ~160s after spawn. `ff-status` now measures that silence for every
-  lane and flips it to `state: "stalled"` (plus `stalled: true`) past
-  `FLEETFLOW_STALL_SECONDS` (default 600); the monitor draws those lanes as a
-  *frozen* amber pip captioned with the silence, not the elapsed clock. Before
-  assuming a long wave is progressing:
-  `ff-status.sh --run <name> | jq '.lanes[]|select(.stalled)|{id,last_activity_s}'`.
-- **Know what the stall detector covers — `live_signal` tells you.** A stall can
-  only be proven where the brain writes *while it works*: codex's `--json` event
-  stream, or a claude/glm session transcript. Artifact and `.err` files are
-  created by the shell redirect at launch and untouched until exit, so they
-  cannot distinguish work from a wedge — an early build of this detector counted
-  them and flagged every healthy 10-minute sonnet lane as stalled. Uncovered
-  today: **grok** (`--output-format json` buffers to exit) and **claude lanes
-  spawned without `--worktree`** (their transcript dir is shared with sibling
-  lanes and the orchestrator's own session, so attributing it would be worse than
-  reporting nothing). Those report `live_signal: false`, and their `stalled:
-  false` means *cannot tell*, never *healthy* — one more reason to spawn mutating
-  workers with `--worktree`, which makes them fully covered.
-- **Killing a lane leaves orphans — reap them.** `TaskStop` (or killing the
-  background Bash task) kills the *wrapper*, not the worker's children; the
-  2026-07-27 cleanup left five live `codex.exe` / `codex-code-mode-host.exe`
-  processes. `ff-spawn` now journals a `proc` reap anchor (its own PID **and**
-  WINPID) per lane, so `ff-clean` can find that subtree afterwards — report
-  first, kill second:
-
-  ```bash
-  bash scripts/ff-clean.sh --run <name> --reap
-  ```
-
-  Add `--force` to terminate. It matches by **ancestry** (walking up to a
-  journalled anchor), never by image name, so a Codex *you* started is never
-  collateral. The blunt fallback, if the run predates the anchors:
-
-  ```powershell
-  Get-Process codex,codex-code-mode-host -ErrorAction SilentlyContinue | Stop-Process -Force
-  ```
-
-  — but that kills every Codex on the box. Either way, `taskkill /PID` on the
-  PIDs `ps -W` prints does **not** work: those are Cygwin PIDs, and Windows
-  needs the WINPID from the same table's fourth column.
-- **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).
-- **Baseline-before-closeout (learned 2026-07-08, exit 12 false positive):**
-  `--check-main-clean` compares `main` against whatever it was when the check
-  runs — if the orchestrator makes its own edits to `main` (docs, PLAN.md,
-  closeout commits) between spawning lanes and running the check, those
-  self-made edits look identical to an escaped worker's writes. Snapshot a
-  clean-`main` baseline (`git status --short` / a commit sha) **before** any
-  orchestrator-authored closeout edit, so the check compares against the
-  pre-spawn state, not a moving target the orchestrator itself just moved.
-- **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) but ≤ 2–3 for Codex on
-  Windows (the sandbox helper is machine-global, not per-lane), wall-clock
-  patience via the orchestrator's background-task notifications — never
-  poll-sleep. Patience is not indefinite: a wave that has been quiet gets a
-  stall check, not more waiting.
-- **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 (+ which `windows.sandbox` mode codex lanes will get); `--live` probes GLM endpoint, Codex auth, Grok key, Anthropic models, the `windows.sandbox` key tripwire, and 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`, Grok via `grok -p`, Pi via `pi -p` stdin + event-stream distillation, Anthropic via `claude -p`); pins `windows.sandbox=unelevated` for Codex on Windows |
-| [scripts/ff-collect.sh](scripts/ff-collect.sh) | per-brain result gate; strips ```json fences before `--schema` validation; `--repair` respawns a `<id>-repair` lane on validation failure; `--check-main-clean` escape guard |
-| [scripts/ff-status.sh](scripts/ff-status.sh) | run status as JSON (lane state `running`/`stalled`/`done`/`failed`, elapsed, `last_activity_s` + `stalled` + `live_signal` stall detector, commits, tools, tokens, activity, manifest summary); `--watch N --out status.json` feeds the live monitor; `--exit-stalled` exits 14 so a watchdog can branch without parsing |
-| [scripts/ff-run.sh](scripts/ff-run.sh) | `resume --run NAME` replays every manifest packet through ff-spawn in order (unchanged = cached, changed/new = live); `status --run NAME` aliases ff-status |
-| [scripts/ff-clean.sh](scripts/ff-clean.sh) | `--run NAME [--force]` reclaims zero-commit lanes (worktree remove + branch -D), keeps committed lanes, removes the run's cache dirs; reports locked ACL-litter dirs. `--reap [--force]` finds worker processes the wrapper left alive, matched by ancestry to the run's journalled anchors |
-| [scripts/ff-import.sh](scripts/ff-import.sh) | `--wf DIR --run NAME` imports a native Claude Code Workflow run dir (`wf_*/`) — completed agents become lanes (prompt + result envelope + journal + manifest), started-only agents are flagged incomplete; native keys are terminal, not replayable |
-| [scripts/ff-serve.py](scripts/ff-serve.py) | machine-wide dashboard server: discovers every fleetflow run (live + historical, across repos) by scanning the roots in `~/.fleetflow/roots.txt` for `.fleetflow/<run>/journal.jsonl`, serves [assets/ff-dashboard.html](assets/ff-dashboard.html) at `/` plus `/api/aggregate.json` / `/api/refresh` / `/api/health`; per-run state comes from `ff-status.sh` (never reimplemented). One process by design — request-driven, non-blocking rebuilds, no detachable watcher to die silently. Run it as a supervised service (e.g. Process Compose + a probe on `/api/health`) |
-
-**Live monitor** ([assets/ff-monitor.html](assets/ff-monitor.html)): a
-zero-dependency page reproducing the native /workflows progress surface — run
-header with square per-lane pips, a mono/technical agent grid, elapsed/tools/
-commits/tokens, and expandable per-agent detail (activity, last commit, error
-tail, artifact). The run header is **tethered** (sticky) so the summary stays
-in view while the agent grid scrolls beneath it, and it carries two segmented
-controls (persisted in localStorage): **sort** — `active` (default: running,
-then stalled, then failed, then done, so in-flight lanes float to the top),
-`name`, `tokens` — applied within each phase group while the header pip strip
-keeps arrival order; and **card size** — `S` (dense grid, stat columns hidden),
-`M` (default), `L` (one full-width card per row). Wire-up: copy it into the run dir as `index.html`, run
-`ff-status --watch 3 --out <rundir>/status.json`, serve the run dir with any
-static server, open in a browser/preview panel. It polls `status.json` every
-2.5s. Live claude-brain lanes are introspected via their session transcript (GLM:
-the isolated config dir; Anthropic brains: `~/.claude/projects/<encoded-workdir>/`,
-worktree lanes only); codex lanes via their `--json` event stream. **Grok lanes
-have no live stream**: `ff-spawn` launches grok with `--output-format json`, which
-buffers the whole turn and writes once at exit, so a grok lane shows no tools and
-no activity until it finishes. Grok *does* offer `--output-format streaming-json`
-(NDJSON `thought`/`text`/`end`), but it is one flag — adopting it for live
-progress means reconstructing the buffered envelope `ff-collect` gates on, from an
-event shape this repo has not re-verified. Until that is done and verified, grok
-is deliberately outside live introspection and stall coverage. Those streams
-double as the stall signal: a lane
-whose stream has been silent past the threshold renders as a **frozen amber pip**
-captioned with the silence (`2h39m silent`) instead of a pulsing blue one, and
-never counts toward a phase's finished fraction. Lanes with no live stream
-(`live_signal: false`) keep rendering as running — the monitor cannot show a
-verdict the data doesn't support.
-
-**Machine-wide dashboard** ([scripts/ff-serve.py](scripts/ff-serve.py) +
-[assets/ff-dashboard.html](assets/ff-dashboard.html)): where the live monitor
-watches ONE run dir, the dashboard aggregates EVERY run on the machine —
-live and archived, across repos — behind a single always-on service. Same
-layout doctrine as the monitor: pinned summary, cards beneath.
-
-**Two surfaces, like the native tool.** The served monitor is the *live*
-grid (the Background-tasks panel analogue). In-chat, the orchestrator emits a
-compact *snapshot* card at phase boundaries (spawn, phase change, all-done) —
-chat-widget sandboxes cannot poll localhost, so the inline card is a
-moment-in-time render by design, re-emitted rather than self-updating. A
-snapshot card follows the monitor's layout doctrine: the run summary (name,
-totals, per-lane strip) sits in a header pinned via `position: sticky` at the
-card's top, with agent/lane cards listed vertically beneath it ordered
-active-first — so when the card is tall enough to scroll in a side panel, the
-summary never scrolls out of view.
-
-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, `14` lane stalled), `--help` with
-EXAMPLES.
-
-## Importing a native Workflow run
-
-`ff-import --wf <wf_*/> --run <name>` reads a native Claude Code Workflow run
-directory — its `journal.jsonl` (`started`/`result` records with `v2:` hash
-keys and `agentId`) plus per-agent `agent-<id>.jsonl` transcripts — and lands
-each completed agent as a fleetflow lane: the agent's first user-role message
-(string content or content-array-with-text-blocks, both handled) becomes
-`<id>.prompt.txt`, its native `result` object becomes `<id>.result.json`
-(`{is_error:false, result:<native-result>|tojson}`), and a `native`-brain
-packet is appended to the manifest (`imported_from: <DIR>`) for provenance.
-Agents with a `started` but no `result` get a prompt file only and are
-reported `incomplete` on stdout's TSV — respawn candidates.
-
-**Caveat — imported results are terminal facts, not a replayable cache.** The
-native `v2:` keys are content hashes of the *native* `(prompt, opts)` call, not
-fleetflow's `sha256(brain+prompt+opts)`, and `native` is not a spawnable brain
-— so `ff-run resume` **skips** native packets rather than replaying them (it
-reports each `imported` and exits 0). The native script's control flow
-(pipeline/barrier/loop) is not recovered either. To continue from an imported
-result, spawn a fresh lane with a real brain and paste the imported result into
-its packet (the hub-and-spoke handoff). Use import for salvage, provenance, and
-visual continuity in the monitor — not to resume native work in place.
-
-## 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, the
-  Grok `grok -p` headless/schema/auth contract, Anthropic alias notes) and the
-  Fable/Opus orchestrator probe.
-- [references/native-model-routing.md](references/native-model-routing.md) —
-  per-stage `opts.model`/`opts.effort` routing for native Workflow scripts: the
-  cost evidence, the collect-cheap/decide-premium table, caveats (aliases, fork
-  inheritance, effort = reasoning depth).
-
-## 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.

+ 0 - 791
skills/fleetflow/assets/ff-dashboard.html

@@ -1,791 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>fleetflow</title>
-<!--
-  ff-dashboard.html - the machine-wide, multi-run, multi-repo fleetflow surface.
-
-  Sibling of ff-monitor.html, NOT a replacement: ff-monitor is the single-run page
-  a run dir serves next to its own status.json, and in-flight sessions depend on it
-  unchanged. This page polls ONE aggregate document (api/aggregate.json, built on
-  demand by ff-serve.py) covering every run the configured roots can see, plus the
-  archived history of runs whose directories have already been deleted.
-
-  CARD LANGUAGE is deliberately shared with the summon session picker
-  (~/.claude/skills/summon/assets/picker-widget.html): same header shape
-  (colour dot / name / tags / action icons), same title+summary block, same bar
-  strip, same icon-chip stat row, same mono path footer. Two surfaces that both
-  answer "what is running and what did it cost" should not look like two products.
-  Summon runs inside the chat widget host, so it can use that host's CSS variables
-  and the Tabler icon webfont; this page is served standalone under a strict
-  zero-dependency rule, so the variables are redeclared below and the icons are
-  inline SVG. Keep them visually in step - if one changes, change both.
-
-  LOAD-BEARING VISUAL RULE, inherited from ff-monitor: the `stalled` pip is amber
-  and NOT animated. A frozen pip beside pulsing ones is the entire tell that a lane
-  is wedged rather than working. Animating it would destroy the only signal that
-  distinguishes them. Same for the state dot on a card.
-
-  Zero dependencies, no build step, light+dark via prefers-color-scheme.
--->
-<style>
-  :root { color-scheme: light dark;
-    /* Names mirror the summon widget host's token set so the two surfaces can be
-       diffed by eye; values are this page's own (it has no host to inherit from). */
-    --bg:#fafaf8; --surface-1:#f2f1ed; --surface-2:#fff;
-    --border:#e4e2dd; --border-strong:#d3d1c7; --border-accent:#378add;
-    --text-primary:#1a1a17; --text-secondary:#5c5a54; --text-muted:#8a887f;
-    --text-accent:#2f6fb0;
-    --run:#378add; --ok:#1d9e75; --bad:#e24b4a; --warn:#c8871b; --idle:#d3d1c7;
-    --bg-success:#e2f4ec; --text-success:#0f6e56;
-    --bg-bad:#fbe6e5; --bg-warn:#fbf0dc;
-    --radius:8px;
-    --font-mono:ui-monospace, "Cascadia Code", Consolas, monospace; }
-  @media (prefers-color-scheme: dark) { :root {
-    --bg:#1c1c1a; --surface-1:#222220; --surface-2:#262624;
-    --border:#3a3936; --border-strong:#4a4945; --border-accent:#5aa0e6;
-    --text-primary:#ececea; --text-secondary:#b6b4ac; --text-muted:#8f8d85;
-    --text-accent:#78b4f0;
-    --warn:#e0a233; --idle:#444441;
-    --bg-success:#183a30; --text-success:#5fc9a4;
-    --bg-bad:#3d2020; --bg-warn:#3a2f1a; } }
-  * { box-sizing:border-box; margin:0; }
-  body { background:var(--bg); color:var(--text-primary);
-    font:13px/1.5 "Segoe UI", "Helvetica Neue", ui-sans-serif, sans-serif; }
-  .mono { font-family:var(--font-mono); }
-  button, input, select { font:inherit; color:inherit; }
-  svg.i { width:14px; height:14px; stroke:currentColor; fill:none; stroke-width:2;
-    stroke-linecap:round; stroke-linejoin:round; flex:none; }
-
-  /* ── top bar ─────────────────────────────────────────────────────────── */
-  .top { position:sticky; top:0; z-index:20; background:var(--surface-2);
-    border-bottom:1px solid var(--border); padding:9px 14px;
-    display:flex; align-items:center; gap:12px; flex-wrap:wrap; }
-  .brand { display:flex; align-items:center; gap:8px; flex:none; }
-  .brand svg { width:22px; height:22px; flex:none; }
-  .brand b { font-size:15px; font-weight:600; letter-spacing:-.015em; }
-  .brand .live { width:7px; height:7px; border-radius:50%; background:var(--ok);
-    box-shadow:0 0 0 3px var(--bg-success); }
-  .brand .live.off { background:var(--bad); box-shadow:0 0 0 3px var(--bg-bad); }
-  .totals { color:var(--text-secondary); font-size:12px; display:flex; gap:9px;
-    flex-wrap:wrap; }
-  .totals b { color:var(--text-primary); font-weight:600; }
-  .stamp { margin-left:auto; color:var(--text-muted); font-size:11px; }
-
-  .banner { display:none; background:var(--bad); color:#fff; padding:7px 14px; font-size:12px; }
-  .banner.on { display:block; }
-  .banner.warnstyle { background:var(--warn); color:#1a1a17; }
-
-  .wrap { display:grid; grid-template-columns:300px 1fr; align-items:start; }
-  @media (max-width:900px) { .wrap { grid-template-columns:1fr; }
-    aside { border-right:none !important; border-bottom:1px solid var(--border);
-      position:static !important; max-height:none !important; } }
-
-  /* ── sidebar ─────────────────────────────────────────────────────────── */
-  aside { border-right:1px solid var(--border); padding:10px 9px 30px;
-    max-height:calc(100vh - 46px); overflow-y:auto; position:sticky; top:46px; }
-  .ctl { display:flex; flex-direction:column; gap:7px; margin-bottom:11px; }
-  .fieldwrap { position:relative; display:flex; align-items:center; }
-  .fieldwrap svg.i { position:absolute; left:8px; color:var(--text-muted); }
-  .ctl input, .ctl select { width:100%; padding:6px 8px; border:1px solid var(--border);
-    border-radius:6px; background:var(--bg); }
-  .ctl input { padding-left:28px; }
-  .ctl-row { display:flex; gap:6px; }
-  .ctl-row select { flex:1; min-width:0; }
-  .chips { display:flex; gap:4px; flex-wrap:wrap; }
-  .fchip { font-size:11px; padding:2px 8px; border-radius:20px; cursor:pointer;
-    border:1px solid var(--border); background:transparent; color:var(--text-secondary);
-    display:inline-flex; align-items:center; gap:5px; }
-  .fchip.on { background:var(--surface-1); border-color:var(--border-strong);
-    color:var(--text-primary); font-weight:600; }
-  .fchip .sq { width:7px; height:7px; }
-  .treectl { display:flex; gap:4px; align-items:center; padding:2px 2px 6px;
-    border-bottom:1px solid var(--border); margin-bottom:7px; }
-  .tbtn { border:0; background:transparent; color:var(--text-muted); cursor:pointer;
-    font-size:11px; padding:3px 6px; border-radius:5px; display:inline-flex;
-    align-items:center; gap:4px; }
-  .tbtn:hover { background:var(--surface-1); color:var(--text-accent); }
-  .tbtn .cnt { margin-left:auto; }
-
-  .repo { margin-bottom:3px; }
-  .repo-h { width:100%; text-align:left; background:none; border:0; cursor:pointer;
-    font-size:11px; color:var(--text-secondary); padding:4px 5px; border-radius:6px;
-    display:flex; gap:6px; align-items:center; }
-  .repo-h:hover { background:var(--surface-1); }
-  .repo-h .nm { font-weight:600; overflow:hidden; text-overflow:ellipsis;
-    white-space:nowrap; letter-spacing:.01em; }
-  .repo-h .dot { width:8px; height:8px; border-radius:50%; flex:none; }
-  .repo-h .n { margin-left:auto; color:var(--text-muted); font-size:10px; flex:none; }
-  .repo-h svg.i { width:12px; height:12px; color:var(--text-muted);
-    transition:transform .12s; }
-  .repo.collapsed svg.chev { transform:rotate(-90deg); }
-  .repo.collapsed .runs { display:none; }
-  .runs { padding:1px 0 5px 5px; }
-  .runrow { width:100%; text-align:left; background:none; border:1px solid transparent;
-    border-radius:6px; padding:5px 7px; cursor:pointer; display:block; }
-  .runrow:hover { background:var(--surface-1); }
-  .runrow.on { background:var(--surface-1); border-color:var(--border-accent); }
-  .runrow .rl { display:flex; align-items:center; gap:6px; }
-  .runrow .nm { font-weight:600; font-size:12px; overflow:hidden;
-    text-overflow:ellipsis; white-space:nowrap; }
-  .runrow .rs { margin-left:auto; font-size:10px; color:var(--text-muted); flex:none; }
-  .runrow.archived .nm { color:var(--text-muted); }
-
-  /* ── pips: the shared language with ff-monitor.html ───────────────────── */
-  .sq-row { display:flex; gap:3px; flex-wrap:wrap; }
-  .sq { width:8px; height:8px; border-radius:1.5px; background:var(--idle); flex:none; }
-  .sq.running { background:var(--run); animation:pulse 1.1s ease-in-out infinite; }
-  .sq.done { background:var(--ok); } .sq.failed { background:var(--bad); }
-  .sq.stalled { background:var(--warn); }   /* amber and NOT animated - see header */
-  @keyframes pulse { 50% { opacity:.3 } }
-
-  /* ── right pane ──────────────────────────────────────────────────────── */
-  main { padding:13px 15px 46px; min-width:0; }
-  .rhead { background:var(--surface-2); border:1px solid var(--border);
-    border-radius:12px; padding:14px 16px; margin-bottom:12px; }
-  .rhead-r { display:flex; align-items:center; gap:9px; flex-wrap:wrap; }
-  .rhead h2 { font-size:19px; font-weight:600; letter-spacing:-.02em; }
-  .rhead .path { color:var(--text-muted); font-size:11px; word-break:break-all;
-    display:flex; align-items:center; gap:5px; margin-top:5px; }
-
-  /* column chart - the top-of-pane summary of where the tokens actually went */
-  .chart { margin:13px 0 3px; }
-  .chart-h { display:flex; align-items:baseline; gap:8px; font-size:10px;
-    color:var(--text-muted); text-transform:uppercase; letter-spacing:.07em;
-    margin-bottom:5px; }
-  .chart-h .pk { margin-left:auto; text-transform:none; letter-spacing:0;
-    font-family:var(--font-mono); }
-  .cols-chart { display:flex; align-items:flex-end; gap:3px; height:74px;
-    border-bottom:1px solid var(--border); padding-bottom:1px; }
-  .bar { flex:1; min-width:3px; border-radius:2px 2px 0 0; min-height:2px;
-    position:relative; cursor:default; transition:filter .1s; }
-  .bar:hover { filter:brightness(1.25); }
-  .bar.failed { outline:1.5px solid var(--bad); outline-offset:-1.5px; }
-  .bar.stalled { outline:1.5px solid var(--warn); outline-offset:-1.5px; }
-  .bar.running { animation:pulse 1.4s ease-in-out infinite; }
-  .chart-x { display:flex; gap:3px; margin-top:4px; font-size:9px;
-    color:var(--text-muted); font-family:var(--font-mono); }
-  .chart-x span { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis;
-    white-space:nowrap; text-align:center; }
-
-  .stats { display:flex; gap:18px; flex-wrap:wrap; margin-top:12px; }
-  .stat b { display:block; font-size:19px; font-weight:600; letter-spacing:-.02em; }
-  .stat span { font-size:10px; color:var(--text-muted); text-transform:uppercase;
-    letter-spacing:.06em; }
-  .stat.warn b { color:var(--warn); } .stat.bad b { color:var(--bad); }
-  .stat.ok b { color:var(--ok); }
-  .note { color:var(--text-muted); font-size:11px; margin-top:9px; line-height:1.5; }
-
-  .phase { margin-bottom:13px; }
-  .phase-h { display:flex; align-items:center; gap:8px; padding:2px 2px 7px;
-    font-size:10px; color:var(--text-muted); text-transform:uppercase; letter-spacing:.08em; }
-  .phase-h .n { margin-left:auto; font-family:var(--font-mono); }
-  .deck { display:grid; grid-template-columns:repeat(auto-fill,minmax(310px,1fr)); gap:11px; }
-
-  /* ── lane card - shape matched to the summon picker ──────────────────── */
-  .card { background:var(--surface-2); border:1px solid var(--border);
-    border-radius:12px; padding:12px 14px; display:flex; flex-direction:column; gap:8px; }
-  .card.is-stalled { border-color:var(--warn); }
-  .card.is-failed { border-color:var(--bad); }
-  .hd { display:flex; align-items:center; gap:6px; min-width:0; flex-wrap:wrap; row-gap:5px; }
-  .hd .dot { width:8px; height:8px; border-radius:50%; flex:none; }
-  .hd .dot.running { animation:pulse 1.1s ease-in-out infinite; }
-  .proj { font-size:12px; color:var(--text-secondary); font-weight:500; min-width:0;
-    overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
-  .tag { font-size:10px; border-radius:5px; padding:0 5px; border:1px solid var(--border);
-    color:var(--text-muted); flex:none; }
-  .tag.run { background:var(--bg-success); color:var(--text-success); border:0; font-weight:600; }
-  .tag.stall { background:var(--bg-warn); color:var(--warn); border:0; font-weight:600; }
-  .tag.fail { background:var(--bg-bad); color:var(--bad); border:0; font-weight:600; }
-  .ttl { font-size:15px; font-weight:500; line-height:1.3; }
-  .sum { font-size:12px; color:var(--text-secondary); line-height:1.5;
-    display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2;
-    overflow:hidden; word-break:break-word; }
-  /* token composition strip. NOT a fabricated activity timeline: this page has no
-     per-lane time series, and inventing one would look like data. It is the real
-     fresh/cached/output split, which is the number that actually explains cost. */
-  .mix { display:flex; height:5px; border-radius:3px; overflow:hidden;
-    background:var(--surface-1); }
-  .mix i { display:block; }
-  .statrow { display:flex; flex-wrap:wrap; gap:4px; }
-  .chip { font-size:11px; color:var(--text-secondary); background:var(--surface-1);
-    border-radius:6px; padding:2px 7px; display:inline-flex; align-items:center;
-    gap:4px; white-space:nowrap; }
-  .chip svg.i { color:var(--text-muted); }
-  .chip.warn { background:var(--bg-warn); color:var(--warn); }
-  .chip.warn svg.i { color:var(--warn); }
-  .dir { font-family:var(--font-mono); font-size:11px; color:var(--text-muted);
-    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:flex;
-    align-items:center; gap:5px; }
-  .errline { color:var(--bad); font-size:11px; font-family:var(--font-mono);
-    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
-
-  .empty { color:var(--text-muted); padding:32px 4px; text-align:center; line-height:1.7; }
-  table.hist { width:100%; border-collapse:collapse; font-size:12px; }
-  table.hist th { text-align:left; font-size:10px; text-transform:uppercase;
-    letter-spacing:.06em; color:var(--text-muted); font-weight:500;
-    padding:4px 8px 6px; border-bottom:1px solid var(--border); }
-  table.hist td { padding:6px 8px; border-bottom:1px solid var(--border); }
-  table.hist td.num { text-align:right; font-family:var(--font-mono); }
-</style>
-</head>
-<body>
-<div class="top">
-  <div class="brand">
-    <!-- LOGO SLOT. A conical (Erlenmeyer) flask: this dashboard is the front door
-         to the *.lab services, and the flask is that namespace's mark. Swap the
-         <path> set below to change the logo; nothing else references it. -->
-    <svg viewBox="0 0 24 24" aria-hidden="true">
-      <defs><linearGradient id="ffg" x1="0" y1="0" x2="1" y2="1">
-        <stop offset="0" stop-color="#378add"/><stop offset="1" stop-color="#1d9e75"/>
-      </linearGradient></defs>
-      <path d="M9.5 2.5v6.2L3.8 18.4A2.4 2.4 0 0 0 5.9 22h12.2a2.4 2.4 0 0 0 2.1-3.6L14.5 8.7V2.5"
-            fill="none" stroke="url(#ffg)" stroke-width="1.8"
-            stroke-linecap="round" stroke-linejoin="round"/>
-      <path d="M8.2 2.5h7.6" stroke="url(#ffg)" stroke-width="1.8" stroke-linecap="round"/>
-      <path d="M6.6 15.2h10.8l2.8 4.8A1.6 1.6 0 0 1 18.8 22H5.2a1.6 1.6 0 0 1-1.4-2z"
-            fill="url(#ffg)" stroke="none" opacity=".85"/>
-    </svg>
-    <b>fleetflow</b><span class="live" id="dot" title="aggregator status"></span>
-  </div>
-  <div class="totals" id="totals">connecting…</div>
-  <span class="stamp mono" id="stamp"></span>
-</div>
-<div class="banner" id="banner"></div>
-<div class="wrap">
-  <aside>
-    <div class="ctl">
-      <div class="fieldwrap">
-        <svg class="i" viewBox="0 0 24 24"><circle cx="10" cy="10" r="7"/><path d="M21 21l-5.2-5.2"/></svg>
-        <input id="filter" placeholder="filter run, repo, or path…" autocomplete="off">
-      </div>
-      <div class="ctl-row">
-        <select id="sort" title="sort runs">
-          <option value="state">Sort: status</option>
-          <option value="recent">Sort: most recent</option>
-          <option value="tokens">Sort: most tokens</option>
-          <option value="cost">Sort: most cost</option>
-          <option value="lanes">Sort: most lanes</option>
-          <option value="elapsed">Sort: longest</option>
-          <option value="name">Sort: name</option>
-        </select>
-      </div>
-      <div class="chips" id="statechips"></div>
-    </div>
-    <div class="treectl">
-      <button class="tbtn" id="bExpand" title="Expand every repo">
-        <svg class="i" viewBox="0 0 24 24"><path d="M7 10l5 5 5-5"/><path d="M7 4l5 5 5-5"/></svg>expand</button>
-      <button class="tbtn" id="bCollapse" title="Collapse every repo">
-        <svg class="i" viewBox="0 0 24 24"><path d="M7 14l5-5 5 5"/><path d="M7 20l5-5 5 5"/></svg>collapse</button>
-      <span class="tbtn cnt" id="navcount"></span>
-    </div>
-    <div id="nav"></div>
-  </aside>
-  <main id="main"><div class="empty">loading…</div></main>
-</div>
-
-<script>
-"use strict";
-const POLL_MS = 3000;
-
-/* ── formatting ──────────────────────────────────────────────────────────── */
-const fmt = s => s == null ? "—"
-  : s >= 86400 ? Math.floor(s/86400)+"d"+String(Math.floor(s%86400/3600)).padStart(2,"0")+"h"
-  : s >= 3600  ? Math.floor(s/3600)+"h"+String(Math.floor(s%3600/60)).padStart(2,"0")+"m"
-  : s >= 60    ? Math.floor(s/60)+"m"+String(s%60).padStart(2,"0")+"s" : s+"s";
-const tok = n => !n ? "—" : n >= 1e9 ? (n/1e9).toFixed(2)+"B" : n >= 1e6 ? (n/1e6).toFixed(1)+"M"
-  : n >= 1e3 ? (n/1e3).toFixed(1)+"k" : String(n);
-const usd = v => v == null ? "—" : v >= 100 ? "$"+v.toFixed(0) : "$"+v.toFixed(2);
-const nf  = n => (n||0).toLocaleString("en-US");
-const esc = t => (t||"").replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
-
-/* Inline SVG instead of an icon webfont - this page is served standalone under a
-   strict no-external-assets rule. Same glyph vocabulary as the summon picker's
-   Tabler set, so the two surfaces read as one family. */
-const ICO = {
-  clock:'<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
-  cpu:'<rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3"/>',
-  tool:'<path d="M7 10a4 4 0 1 1 3.5-5.9L8 6.5 9.5 9.5 12.5 11l2.4-2.5A4 4 0 0 1 10 12l-4.5 7A2 2 0 0 1 2.5 16z"/>',
-  git:'<circle cx="12" cy="12" r="3"/><path d="M3 12h6M15 12h6"/>',
-  coin:'<circle cx="12" cy="12" r="9"/><path d="M15 9.5A3 3 0 0 0 12 8h-1a2 2 0 0 0 0 4h2a2 2 0 0 1 0 4h-1a3 3 0 0 1-3-1.5M12 6v12"/>',
-  folder:'<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
-  layers:'<path d="M12 3l9 5-9 5-9-5z"/><path d="M3 14l9 5 9-5"/>',
-  activity:'<path d="M3 12h4l3 8 4-16 3 8h4"/>',
-  chev:'<path d="M6 9l6 6 6-6"/>',
-  alert:'<path d="M12 9v4M12 17h.01"/><path d="M10.3 3.9 2.4 18a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/>',
-  file:'<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/>',
-  db:'<ellipse cx="12" cy="6" rx="8" ry="3"/><path d="M4 6v12c0 1.7 3.6 3 8 3s8-1.3 8-3V6"/><path d="M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3"/>',
-};
-const ico = (n, cls) => `<svg class="i ${cls||""}" viewBox="0 0 24 24" aria-hidden="true">${ICO[n]||""}</svg>`;
-const chip = (icon, val, lbl, cls) =>
-  `<span class="chip ${cls||""}">${ico(icon)}${val}${lbl?`<span style="color:var(--text-muted)">${lbl}</span>`:""}</span>`;
-
-/* Per-brain identity (letter + colour) and per-repo accent, both matching the
-   summon picker's palette so the same project reads the same colour on both. */
-const BRAIN = { glm:["G","#7f77dd"], codex:["C","#0f6e56"], sonnet:["S","#d85a30"],
-  opus:["O","#ba7517"], haiku:["H","#d4537e"], fable:["F","#378add"],
-  grok:["X","#534ab7"], native:["N","#6a8caf"] };
-const PAL = ["#7F77DD","#1D9E75","#D85A30","#378ADD","#BA7517","#D4537E","#534AB7","#0F6E56","#993C1D"];
-const repoColor = n => { let h = 0; n = n || ""; for (let i=0;i<n.length;i++) h = (h*31 + n.charCodeAt(i))>>>0;
-  return PAL[h % PAL.length]; };
-const brainOf = b => BRAIN[b] || ["?", "#888780"];
-const STATE_COLOR = { running:"var(--run)", stalled:"var(--warn)", done:"var(--ok)",
-  failed:"var(--bad)", unknown:"var(--idle)" };
-
-/* ── state ───────────────────────────────────────────────────────────────── */
-let doc = null, misses = 0, filter = "";
-let selected = decodeURIComponent(location.hash.slice(1) || "");
-let sortMode = localStorage.getItem("ff.sort") || "state";
-let stateFilter = new Set(JSON.parse(localStorage.getItem("ff.states") || "[]"));
-let collapsed = new Set(JSON.parse(localStorage.getItem("ff.collapsed") || "[]"));
-const openCards = new Set();
-
-const runKey  = r => (r.repo || "") + "|" + (r.run || "");
-const histKey = h => "hist:" + (h.repo || "") + "|" + (h.run || "");
-const inflight = s => s === "running" || s === "stalled";
-/* A cached run's clocks must keep ticking without re-reading it: ff-aggregate
-   stamps read_at per run, so `age` is exactly how stale this card's numbers are. */
-const ageOf = r => Math.max(0, (doc?.generated_at || 0) - (r.read_at || doc?.generated_at || 0));
-const save = () => { localStorage.setItem("ff.sort", sortMode);
-  localStorage.setItem("ff.states", JSON.stringify([...stateFilter]));
-  localStorage.setItem("ff.collapsed", JSON.stringify([...collapsed])); };
-
-/* ── polling ─────────────────────────────────────────────────────────────── */
-async function tick() {
-  let d = null;
-  try {
-    const res = await fetch("api/aggregate.json?t=" + Date.now(), { cache:"no-store" });
-    if (res.ok) d = await res.json();
-  } catch (e) { d = null; }
-
-  if (!d || !Array.isArray(d.runs)) {
-    misses++;
-    // Never let a dead aggregator masquerade as a quiet fleet. The grid stays
-    // (losing it on one dropped poll would be worse) but it is explicitly
-    // labelled stale, with the age of what is on screen.
-    if (misses >= 2) {
-      document.getElementById("dot").className = "live off";
-      const age = doc ? fmt(Math.floor(Date.now()/1000) - doc.generated_at) : null;
-      banner(doc
-        ? `aggregator unreachable — everything below is a frozen snapshot from ${age} ago, not live`
-        : "aggregator unreachable — is the fleetflow service running? (process-compose: process start fleetflow)");
-    }
-    return;
-  }
-  misses = 0; doc = d;
-  document.getElementById("dot").className = "live";
-  // Three distinct unhealthy states, and conflating any two of them is how the
-  // predecessor page lied: (1) still building — normal, wait; (2) built but going
-  // stale — the rebuild is wedged even though the server answers; (3) partial —
-  // some roots unreadable. Only (1) is benign.
-  if (d.building && !d.runs.length)
-    banner("building the first aggregate — a cold scan of every root takes a few "
-      + "minutes; this fills in on its own", true);
-  else if (d.build_error)
-    banner(`aggregate rebuild failed: ${d.build_error} — showing the last good result`);
-  else if (d.age_s != null && d.age_s > Math.max(90, (d.ttl_s || 10) * 8) && !d.building)
-    banner(`aggregate is ${fmt(d.age_s)} old and no rebuild is running — the server is up `
-      + `but its build step is not completing`);
-  else if (d.error && !d.building) banner(d.error);
-  else {
-    // Only a whole ROOT failing is worth a banner — that means a drive is gone and
-    // runs are silently missing. Individual unreadable subdirectories are permanent
-    // facts of this filesystem, and a banner that is always on is one nobody reads.
-    const roots = (d.errors || []).filter(e => e.scope === "root");
-    if (roots.length) banner(`${roots.length} configured root(s) unreadable — runs there `
-      + `are missing, not absent: ` + roots.map(e => esc(e.path)).join(", "));
-    else clearBanner();
-  }
-  render();
-}
-function banner(msg, warn) {
-  const b = document.getElementById("banner");
-  b.textContent = msg; b.className = "banner on" + (warn ? " warnstyle" : "");
-}
-function clearBanner() { document.getElementById("banner").className = "banner"; }
-
-/* ── render ──────────────────────────────────────────────────────────────── */
-function render() {
-  const t = doc.totals || {};
-  document.getElementById("totals").innerHTML =
-    `<span><b>${t.live_runs || 0}</b> live</span><span><b>${t.runs || 0}</b> runs</span>` +
-    `<span><b>${t.repos || 0}</b> repos</span><span><b>${t.lanes || 0}</b> lanes</span>` +
-    `<span><b>${tok(t.tokens_total)}</b> tokens</span>` +
-    `<span><b>${t.history_runs || 0}</b> archived</span>`;
-  document.getElementById("stamp").textContent =
-    new Date((doc.generated_at || 0) * 1000).toLocaleTimeString() +
-    (doc.age_s != null ? ` · ${fmt(doc.age_s)} old` : "") +
-    (doc.building ? " · rebuilding…" : "") +
-    (doc.build_ms != null ? ` · ${doc.build_ms}ms` : "") +
-    ((doc.errors || []).length ? ` · ${doc.errors.length} dir(s) skipped` : "");
-  document.getElementById("stamp").title =
-    (doc.errors || []).map(e => `${e.path}: ${e.error}`).join("\n") || "";
-  renderChips(); renderNav(); renderDetail();
-}
-
-const STATES = [["running","live"],["stalled","stalled"],["failed","failed"],["done","done"]];
-function renderChips() {
-  const counts = {};
-  doc.runs.forEach(r => { const s = r.summary?.state || "unknown";
-    counts[s] = (counts[s] || 0) + 1; });
-  document.getElementById("statechips").innerHTML = STATES.map(([k, label]) =>
-    `<button class="fchip ${stateFilter.has(k) ? "on" : ""}" data-s="${k}"
-       title="show only ${label} runs"><span class="sq ${k}"></span>${label}
-       <span style="color:var(--text-muted)">${counts[k] || 0}</span></button>`).join("");
-  document.querySelectorAll(".fchip").forEach(b => b.onclick = () => {
-    const s = b.dataset.s;
-    stateFilter.has(s) ? stateFilter.delete(s) : stateFilter.add(s);
-    save(); render();
-  });
-}
-
-function matches(text) { return !filter || text.toLowerCase().includes(filter); }
-function passesState(r) {
-  return !stateFilter.size || stateFilter.has(r.summary?.state || "unknown");
-}
-function sortRuns(list) {
-  const rank = { stalled:0, running:1, failed:2, done:3, unknown:4 };
-  const x = list.slice();
-  const S = r => r.summary || {};
-  if (sortMode === "tokens")  x.sort((a,b) => (S(b).tokens_total||0)-(S(a).tokens_total||0));
-  else if (sortMode === "cost")    x.sort((a,b) => (S(b).cost_usd||0)-(S(a).cost_usd||0));
-  else if (sortMode === "lanes")   x.sort((a,b) => (S(b).lane_count||0)-(S(a).lane_count||0));
-  else if (sortMode === "elapsed") x.sort((a,b) => (S(b).elapsed_s||0)-(S(a).elapsed_s||0));
-  else if (sortMode === "recent")  x.sort((a,b) => (S(b).started||0)-(S(a).started||0));
-  else if (sortMode === "name")    x.sort((a,b) => (a.run||"").localeCompare(b.run||""));
-  else x.sort((a,b) => (rank[S(a).state] ?? 9) - (rank[S(b).state] ?? 9)
-                    || (S(b).started||0) - (S(a).started||0));
-  return x;
-}
-
-function renderNav() {
-  const groups = new Map();
-  let shown = 0;
-  for (const r of sortRuns(doc.runs)) {
-    if (!passesState(r)) continue;
-    if (!matches(r.run + " " + (r.repo_label || "") + " " + r.repo)) continue;
-    const g = r.repo_label || r.repo;
-    if (!groups.has(g)) groups.set(g, []);
-    groups.get(g).push(r); shown++;
-  }
-  let html = "";
-  for (const [label, runs] of groups) {
-    const live = runs.filter(r => inflight(r.summary?.state)).length;
-    const isC = collapsed.has(label);
-    html += `<div class="repo ${isC ? "collapsed" : ""}" data-g="${esc(label)}">
-      <button class="repo-h" data-toggle="${esc(label)}">
-        ${ico("chev","chev")}
-        <span class="dot" style="background:${repoColor(label)}"></span>
-        <span class="nm">${esc(label)}</span>
-        <span class="n">${live ? live + " live" : runs.length}</span>
-      </button>
-      <div class="runs">${runs.map(runRow).join("")}</div></div>`;
-  }
-  // History is a peer group in the same tree, not a separate screen: a run that was
-  // live an hour ago and cleaned since should be one click away. Runs still on disk
-  // are suppressed so nothing appears twice while it is both live and archived.
-  const onDisk = new Set(doc.runs.map(r => runKey(r).toLowerCase()));
-  const hist = (doc.history || []).filter(h =>
-    !onDisk.has((h.repo + "|" + h.run).toLowerCase()) &&
-    matches(h.run + " " + (h.repo_label || "") + " " + h.repo) &&
-    (!stateFilter.size || stateFilter.has(h.state)));
-  if (hist.length) {
-    const isC = collapsed.has("history");
-    html += `<div class="repo ${isC ? "collapsed" : ""}">
-      <button class="repo-h" data-toggle="&#0;history">${ico("chev","chev")}
-        ${ico("db")}<span class="nm">history · cleaned up</span>
-        <span class="n">${hist.length}</span></button>
-      <div class="runs">${hist.map(h => `
-        <button class="runrow archived ${selected === histKey(h) ? "on" : ""}"
-          data-k="${esc(histKey(h))}">
-          <div class="rl"><span class="sq ${h.state}"></span>
-            <span class="nm mono">${esc(h.run)}</span>
-            <span class="rs">${tok(h.tokens_total)}</span></div>
-          <div class="rl" style="margin-top:2px"><span class="rs" style="margin-left:14px">
-            ${esc(h.repo_label||"")} · ${h.lane_count} lanes · ${usd(h.cost_usd)}</span></div>
-        </button>`).join("")}</div></div>`;
-  }
-  document.getElementById("nav").innerHTML = html ||
-    `<div class="empty" style="padding:18px 4px">no runs match</div>`;
-  document.getElementById("navcount").textContent =
-    `${shown}${hist.length ? " + " + hist.length : ""} shown`;
-
-  document.querySelectorAll("[data-toggle]").forEach(b => b.onclick = () => {
-    const g = b.dataset.toggle;
-    collapsed.has(g) ? collapsed.delete(g) : collapsed.add(g);
-    save(); renderNav();
-  });
-  document.querySelectorAll(".runrow").forEach(b => b.onclick = () => {
-    selected = b.dataset.k; location.hash = encodeURIComponent(selected); render();
-  });
-}
-
-function runRow(r) {
-  const s = r.summary || {}, k = runKey(r), age = ageOf(r);
-  const pips = (r.lanes || []).slice(0, 44).map(l => `<div class="sq ${l.state}"></div>`).join("");
-  const label = r.error ? `<span style="color:var(--bad)">unreadable</span>`
-    : s.state === "stalled" ? `<span style="color:var(--warn)">${fmt((s.idle_s||0)+age)} silent</span>`
-    : s.state === "running" ? `${s.counts?.running || 0} running`
-    : tok(s.tokens_total);
-  return `<button class="runrow ${selected === k ? "on" : ""}" data-k="${esc(k)}">
-      <div class="rl"><span class="nm mono">${esc(r.run)}</span><span class="rs">${label}</span></div>
-      <div class="sq-row" style="margin-top:4px">${pips}</div></button>`;
-}
-
-/* ── column chart ────────────────────────────────────────────────────────── */
-/* One column per item, height proportional to value. This is the chart that makes
-   the cross-brain cost story visible at a glance: a codex lane at 5.4M next to a
-   GLM lane at 42.6k is a 100x column, not two numbers you have to compare by eye. */
-function columnChart(items, { label, sub, showX }) {
-  if (!items.length) return "";
-  const max = Math.max(...items.map(i => i.value), 1);
-  const peak = items.reduce((a,b) => b.value > a.value ? b : a, items[0]);
-  const bars = items.map(i => {
-    const h = i.value > 0 ? Math.max(3, Math.round(i.value / max * 72)) : 2;
-    return `<div class="bar ${i.state||""}" style="height:${h}px;background:${i.color};
-      ${i.value ? "" : "opacity:.25;"}"
-      title="${esc(i.name)} — ${tok(i.value)} tokens · ${esc(i.state||"")}"></div>`;
-  }).join("");
-  const xs = showX && items.length <= 26
-    ? `<div class="chart-x">${items.map(i =>
-        `<span title="${esc(i.name)}">${esc(i.name)}</span>`).join("")}</div>` : "";
-  return `<div class="chart">
-    <div class="chart-h"><span>${esc(label)}</span>
-      ${sub ? `<span style="text-transform:none;letter-spacing:0">${esc(sub)}</span>` : ""}
-      <span class="pk">peak ${esc(peak.name)} · ${tok(peak.value)}</span></div>
-    <div class="cols-chart">${bars}</div>${xs}</div>`;
-}
-
-/* ── detail pane ─────────────────────────────────────────────────────────── */
-function renderDetail() {
-  const m = document.getElementById("main");
-  if (!doc.runs.length && !(doc.history || []).length) {
-    m.innerHTML = doc.building
-      ? `<div class="empty">scanning for runs…<br><span class="mono">${esc((doc.roots||[]).join("  ·  "))}</span></div>`
-      : `<div class="empty">no fleetflow runs found under:<br><span class="mono">
-         ${esc((doc.roots||[]).join("  ·  ") || "(no roots)")}</span><br>
-         roots come from <span class="mono">${esc(doc.roots_source || "?")}</span></div>`;
-    return;
-  }
-  if (selected.startsWith("hist:")) {
-    const h = (doc.history || []).find(x => histKey(x) === selected);
-    if (h) { m.innerHTML = historyCard(h); return; }
-  }
-  const r = doc.runs.find(x => runKey(x) === selected);
-  m.innerHTML = r ? runDetail(r) : overview();
-}
-
-function overview() {
-  const live = doc.runs.filter(r => inflight(r.summary?.state));
-  const byTok = doc.runs.slice().sort((a,b) =>
-    (b.summary?.tokens_total||0) - (a.summary?.tokens_total||0)).slice(0, 30);
-  const cost = doc.runs.reduce((a,r) => a + (r.summary?.cost_usd || 0), 0);
-  const chart = columnChart(byTok.map(r => ({
-    name: r.run, value: r.summary?.tokens_total || 0,
-    color: repoColor(r.repo_label || r.repo), state: r.summary?.state })),
-    { label:"tokens per run · top 30", showX:false });
-  return `<div class="rhead">
-    <div class="rhead-r"><h2>${live.length ? live.length + " run" + (live.length>1?"s":"") + " in flight" : "fleet idle"}</h2>
-      <span class="tag">${doc.totals.runs} runs · ${doc.totals.repos} repos</span></div>
-    ${chart}
-    <div class="stats mono">
-      <div class="stat ${live.length?"ok":""}"><b>${live.length}</b><span>live</span></div>
-      <div class="stat"><b>${doc.totals.lanes||0}</b><span>lanes</span></div>
-      <div class="stat"><b>${tok(doc.totals.tokens_total)}</b><span>tokens</span></div>
-      <div class="stat"><b>${usd(cost)}</b><span>cost*</span></div>
-      <div class="stat"><b>${doc.totals.history_runs||0}</b><span>archived</span></div>
-    </div>
-    <div class="note">* summed from lanes that price their own turn — codex and grok
-      report none, so real spend is higher. Pick a run on the left for its lane grid.</div>
-  </div>` + (live.length ? `<div class="phase"><div class="phase-h mono"><span>in flight</span>
-      <span class="n">${live.length}</span></div>
-    <div class="deck">${live.map(runCard).join("")}</div></div>` : "");
-}
-
-/* Run-level card, same shape as a lane card so the two nest visually. */
-function runCard(r) {
-  const s = r.summary || {}, age = ageOf(r), col = repoColor(r.repo_label || r.repo);
-  return `<div class="card ${s.state==="stalled"?"is-stalled":s.state==="failed"?"is-failed":""}">
-    <div class="hd"><span class="dot ${s.state}" style="background:${STATE_COLOR[s.state]||col}"></span>
-      <span class="proj">${esc(r.repo_label || "")}</span>${stateTag(s.state, (s.idle_s||0)+age)}</div>
-    <div class="ttl mono">${esc(r.run)}</div>
-    <div class="sq-row">${(r.lanes||[]).map(l =>
-      `<div class="sq ${l.state}" title="${esc(l.id)}"></div>`).join("")}</div>
-    <div class="statrow">
-      ${chip("layers", s.lane_count||0, "lanes")}
-      ${chip("clock", fmt((s.elapsed_s||0) + (inflight(s.state)?age:0)))}
-      ${chip("activity", tok(s.tokens_total), "tok")}
-      ${chip("coin", usd(s.cost_usd))}
-    </div>
-    <div class="dir" title="${esc(r.repo)}">${ico("folder")}${esc(r.repo)}</div>
-  </div>`;
-}
-
-function stateTag(state, idle) {
-  if (state === "running") return `<span class="tag run">running</span>`;
-  if (state === "stalled") return `<span class="tag stall">stalled ${fmt(idle)}</span>`;
-  if (state === "failed")  return `<span class="tag fail">failed</span>`;
-  if (state === "done")    return `<span class="tag">done</span>`;
-  return "";
-}
-
-function runDetail(r) {
-  if (r.error) {
-    return `<div class="rhead"><div class="rhead-r"><h2>${esc(r.run)}</h2></div>
-      <div class="path mono">${ico("folder")}${esc(r.repo)}</div>
-      <div class="note" style="color:var(--bad)">${ico("alert")} could not read this run: ${esc(r.error)}</div>
-      <div class="note">It may be mid-cleanup, on an offline drive, or have a torn journal
-        write. It will reappear on its own if it comes back.</div></div>`;
-  }
-  const s = r.summary || {}, age = ageOf(r), lanes = r.lanes || [];
-  const stalledN = s.counts?.stalled || 0, failedN = s.counts?.failed || 0;
-  const runningN = s.counts?.running || 0;
-  const uncovered = lanes.filter(l => inflight(l.state) && !l.live_signal).length;
-
-  const chart = columnChart(lanes.map(l => ({
-    name: l.id, value: l.tokens_total || 0, color: brainOf(l.brain)[1], state: l.state })),
-    { label:"tokens per lane", sub:(s.brains||[]).join(" + "), showX:true });
-
-  const head = `<div class="rhead">
-    <div class="rhead-r"><h2 class="mono">${esc(r.run)}</h2>
-      ${stateTag(s.state, (s.idle_s||0)+age)}
-      ${(s.brains||[]).map(b => `<span class="tag" style="border-color:${brainOf(b)[1]};
-        color:${brainOf(b)[1]};display:inline-flex;align-items:center;gap:4px;padding:1px 6px">
-        ${brandMark(b, brainOf(b)[1])}${esc(b)}</span>`).join("")}</div>
-    <div class="path mono">${ico("folder")}${esc(r.repo)}</div>
-    <div class="sq-row" style="margin-top:9px">${lanes.map(l =>
-      `<div class="sq ${l.state}" title="${esc(l.id)} — ${esc(l.state)}"></div>`).join("")}</div>
-    ${chart}
-    <div class="stats mono">
-      <div class="stat"><b>${s.lane_count||0}</b><span>lanes</span></div>
-      <div class="stat"><b>${fmt((s.elapsed_s||0)+(inflight(s.state)?age:0))}</b><span>elapsed</span></div>
-      <div class="stat"><b>${tok(s.tokens_total)}</b><span>tokens</span></div>
-      <div class="stat"><b>${tok(s.tokens_out)}</b><span>output</span></div>
-      <div class="stat"><b>${usd(s.cost_usd)}</b><span>cost${s.cost_partial?"*":""}</span></div>
-      ${runningN ? `<div class="stat ok"><b>${runningN}</b><span>running</span></div>` : ""}
-      ${stalledN ? `<div class="stat warn"><b>${stalledN}</b><span>stalled</span></div>` : ""}
-      ${failedN  ? `<div class="stat bad"><b>${failedN}</b><span>failed</span></div>` : ""}
-    </div>
-    ${s.cost_partial ? `<div class="note">* self-reported cost covers ${s.cost_lanes}/${s.lane_count}
-      lanes — codex and grok workers do not price their own turn, so the true spend is higher.
-      Token counts cover every lane and are the comparable figure.</div>` : ""}
-    ${uncovered ? `<div class="note">${ico("alert")} ${uncovered} in-flight lane(s) emit no live
-      stream (grok, or claude lanes spawned without --worktree). For those the stall detector
-      reports “cannot tell”, never “healthy”.</div>` : ""}
-  </div>`;
-
-  const phases = [];
-  lanes.forEach(l => { const p = l.phase || "build"; if (!phases.includes(p)) phases.push(p); });
-  return head + phases.map(p => {
-    const pl = lanes.filter(l => (l.phase || "build") === p);
-    return `<div class="phase">
-      <div class="phase-h mono"><span>${esc(p)}</span>
-        <div class="sq-row">${pl.map(l => `<div class="sq ${l.state}"></div>`).join("")}</div>
-        <span class="n">${pl.filter(l => !inflight(l.state)).length}/${pl.length}</span></div>
-      <div class="deck">${pl.map(l => laneCard(l, age)).join("")}</div></div>`;
-  }).join("");
-}
-
-function laneCard(l, age) {
-  const [, col] = brainOf(l.brain);
-  const live = inflight(l.state) ? age : 0;
-  const tot = Math.max(1, l.tokens_total || 0);
-  const pct = v => ((v || 0) / tot * 100).toFixed(1) + "%";
-  return `<div class="card ${l.state==="stalled"?"is-stalled":l.state==="failed"?"is-failed":""}">
-    <div class="hd">
-      <span class="dot ${l.state}" style="background:${STATE_COLOR[l.state]||col}"></span>
-      ${brandMark(l.brain, col)}
-      <span class="proj" style="color:${col};font-weight:600">${esc(l.brain)}</span>
-      <span class="tag">${esc(l.phase || "build")}</span>
-      ${inflight(l.state) && !l.live_signal ? `<span class="tag" title="no live stream — for this
-        lane the stall detector reports &quot;cannot tell&quot;, never &quot;healthy&quot;">no stream</span>` : ""}
-      ${stateTag(l.state, l.last_activity_s + live)}
-    </div>
-    <div class="ttl mono">${esc(l.id)}</div>
-    <div class="sum">${esc(l.activity || "working")}</div>
-    <div class="mix" title="${tok(l.tokens_in)} fresh in · ${tok(l.tokens_cached)} cached · ${tok(l.tokens_out)} out">
-      <i style="width:${pct(l.tokens_in)};background:${col}"></i>
-      <i style="width:${pct(l.tokens_cached)};background:${col};opacity:.35"></i>
-      <i style="width:${pct(l.tokens_out)};background:var(--ok)"></i>
-    </div>
-    <div class="statrow">
-      ${l.stalled
-        ? chip("clock", fmt(l.last_activity_s + live), "silent", "warn")
-        : chip("clock", fmt(l.elapsed_s + live))}
-      ${chip("tool", nf(l.tools), "tools")}
-      ${chip("git", nf(l.commits), "commits")}
-      ${chip("activity", tok(l.tokens_total), "tok")}
-      ${chip("coin", usd(l.cost_usd))}
-    </div>
-    ${l.last_commit ? `<div class="dir" title="${esc(l.last_commit)}">${ico("git")}${esc(l.last_commit)}</div>` : ""}
-    ${l.err_tail ? `<div class="errline" title="${esc(l.err_tail)}">${esc(l.err_tail)}</div>` : ""}
-    <div class="dir">${ico("file")}${esc((l.artifact || "pending").split(/[\\/]/).pop())}</div>
-  </div>`;
-}
-
-function historyCard(h) {
-  const when = h.archived_at ? new Date(h.archived_at * 1000).toLocaleString() : "—";
-  // A run can be archived and STILL on disk (ff-archive called directly rather than
-  // by ff-clean). Claiming its files are gone when they are not would send someone
-  // hunting for a directory that is right there.
-  const onDisk = doc.runs.some(r => runKey(r).toLowerCase() === (h.repo + "|" + h.run).toLowerCase());
-  const fate = onDisk
-    ? `its run directory is still on disk — the live card has the full lane detail.`
-    : `this run's directory has been cleaned up. Lane prompts, diffs, and transcripts
-       are gone; this is the surviving index.`;
-  const chart = columnChart((h.lanes || []).map(l => ({
-    name: l.id, value: l.tokens_total || 0, color: brainOf(l.brain)[1], state: l.state })),
-    { label:"tokens per lane", sub:(h.brains||[]).join(" + "), showX:true });
-  return `<div class="rhead">
-    <div class="rhead-r"><h2 class="mono">${esc(h.run)}</h2>
-      <span class="tag">archived</span>${stateTag(h.state, 0)}
-      ${(h.brains||[]).map(b => `<span class="tag" style="border-color:${brainOf(b)[1]};
-        color:${brainOf(b)[1]}">${esc(b)}</span>`).join("")}</div>
-    <div class="path mono">${ico("folder")}${esc(h.repo)}</div>
-    ${chart}
-    <div class="stats mono">
-      <div class="stat"><b>${h.lane_count||0}</b><span>lanes</span></div>
-      <div class="stat"><b>${fmt(h.elapsed_s)}</b><span>elapsed</span></div>
-      <div class="stat"><b>${tok(h.tokens_total)}</b><span>tokens</span></div>
-      <div class="stat"><b>${tok(h.tokens_out)}</b><span>output</span></div>
-      <div class="stat"><b>${usd(h.cost_usd)}</b><span>cost${h.cost_partial?"*":""}</span></div>
-    </div>
-    <div class="note">archived ${esc(when)} — ${fate}
-      ${h.cost_partial ? `Cost covers ${h.cost_lanes}/${h.lane_count} lanes (codex/grok report
-      none), so the real spend is higher.` : ""}</div>
-  </div>
-  <table class="hist mono"><thead><tr><th>lane</th><th>brain</th><th>phase</th><th>state</th>
-    <th class="num">elapsed</th><th class="num">tokens</th><th class="num">out</th>
-    <th class="num">commits</th><th class="num">cost</th></tr></thead><tbody>
-    ${(h.lanes||[]).map(l => `<tr><td>${esc(l.id)}</td>
-      <td style="color:${brainOf(l.brain)[1]}">${esc(l.brain)}</td>
-      <td>${esc(l.phase||"build")}</td>
-      <td><span class="sq ${l.state}" style="display:inline-block"></span> ${esc(l.state)}</td>
-      <td class="num">${fmt(l.elapsed_s)}</td><td class="num">${tok(l.tokens_total)}</td>
-      <td class="num">${tok(l.tokens_out)}</td><td class="num">${l.commits||"—"}</td>
-      <td class="num">${usd(l.cost_usd)}</td></tr>`).join("")}
-  </tbody></table>`;
-}
-
-/* ── wiring ──────────────────────────────────────────────────────────────── */
-document.getElementById("filter").addEventListener("input", e => {
-  filter = e.target.value.trim().toLowerCase(); if (doc) render();
-});
-const sortEl = document.getElementById("sort");
-sortEl.value = sortMode;
-sortEl.addEventListener("change", e => { sortMode = e.target.value; save(); if (doc) render(); });
-document.getElementById("bExpand").onclick = () => { collapsed.clear(); save(); if (doc) renderNav(); };
-document.getElementById("bCollapse").onclick = () => {
-  doc.runs.forEach(r => collapsed.add(r.repo_label || r.repo));
-  collapsed.add("history"); save(); renderNav();
-};
-window.addEventListener("hashchange", () => {
-  selected = decodeURIComponent(location.hash.slice(1) || ""); if (doc) render();
-});
-tick();
-setInterval(tick, POLL_MS);
-</script>
-</body>
-</html>

+ 0 - 215
skills/fleetflow/assets/ff-monitor.html

@@ -1,215 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>fleetflow monitor</title>
-<style>
-  :root { color-scheme: light dark;
-    --bg:#fafaf8; --card:#fff; --border:#e4e2dd; --text:#1a1a17; --muted:#8a887f;
-    --run:#378add; --ok:#1d9e75; --bad:#e24b4a; --warn:#c8871b; --idle:#d3d1c7; }
-  @media (prefers-color-scheme: dark) { :root {
-    --bg:#1c1c1a; --card:#262624; --border:#3a3936; --text:#ececea; --muted:#8f8d85;
-    --warn:#e0a233; --idle:#444441; } }
-  * { box-sizing:border-box; margin:0; }
-  body { background:var(--bg); color:var(--text); padding:0 16px 16px;
-    font:12px/1.45 "Segoe UI", "Helvetica Neue", ui-sans-serif, sans-serif; }
-  .mono { font-family:ui-monospace, "Cascadia Code", Consolas, monospace; }
-  .shell { max-width:720px; margin:0 auto; }
-  /* tethered header: the run summary stays pinned while the agent grid scrolls
-     beneath it. The wrapper is an opaque page-bg strip (not the card itself) so
-     scrolling cards slide UNDER it rather than showing through its margins. */
-  .head-wrap { position:sticky; top:0; z-index:5; background:var(--bg);
-    padding:16px 0 10px; }
-  .head { background:var(--card); border:1px solid var(--border); border-radius:8px;
-    padding:10px 12px; }
-  .head-row { display:flex; align-items:baseline; gap:10px; }
-  .head h1 { font-size:13px; font-weight:600; letter-spacing:.01em; }
-  .meta { color:var(--muted); font-size:11px; margin-left:auto; }
-  .sub-row { display:flex; align-items:center; gap:10px; margin-top:8px; }
-  .sub-row .sq-row { margin-top:0; flex-wrap:wrap; }
-  .ctrls { display:flex; gap:8px; margin-left:auto; flex:none; }
-  .seg { display:flex; border:1px solid var(--border); border-radius:5px; overflow:hidden; }
-  .seg button { border:0; background:none; color:var(--muted); cursor:pointer;
-    padding:2px 7px; font:10px ui-monospace, "Cascadia Code", Consolas, monospace; }
-  .seg button + button { border-left:1px solid var(--border); }
-  .seg button.on { background:var(--border); color:var(--text); }
-  /* card sizes: s = dense grid, stat columns hidden; m = default; l = one full-width
-     card per row. Chosen via the size seg control, persisted in localStorage. */
-  body[data-size="s"] .grid { grid-template-columns:repeat(auto-fill,minmax(210px,1fr)); }
-  body[data-size="s"] .cols { display:none; }
-  body[data-size="l"] .grid { grid-template-columns:1fr; }
-  .sq-row { display:flex; gap:3px; margin-top:8px; }
-  .sq { width:8px; height:8px; border-radius:1.5px; background:var(--idle); }
-  .sq.running { background:var(--run); animation:pulse 1.1s ease-in-out infinite; }
-  .sq.done { background:var(--ok); } .sq.failed { background:var(--bad); }
-  /* stalled = still nominally running but its signal froze: amber and, crucially,
-     NOT animated - a frozen pip next to pulsing ones is the whole tell. */
-  .sq.stalled { background:var(--warn); }
-  @keyframes pulse { 50% { opacity:.3 } }
-  .phase { margin-bottom:10px; }
-  .phase-h { display:flex; align-items:center; gap:8px; padding:2px 2px 6px;
-    font-size:10px; color:var(--muted); text-transform:uppercase; letter-spacing:.08em; }
-  .phase-h .sq-row { margin:0; }
-  .grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:8px; }
-  .agent { background:var(--card); border:1px solid var(--border); border-radius:8px; }
-  .agent summary { list-style:none; cursor:pointer; padding:8px 10px; }
-  .agent summary::-webkit-details-marker { display:none; }
-  .row1 { display:flex; align-items:center; gap:7px; }
-  .row1 .sq { flex:none; }
-  .chip { width:15px; height:15px; border-radius:3.5px; flex:none; display:flex;
-    align-items:center; justify-content:center; color:#fff; font-size:9px;
-    font-weight:700; font-family:ui-monospace, Consolas, monospace; }
-  .id { font-weight:600; font-size:12px; }
-  .brain { font-size:10px; color:var(--muted); text-transform:uppercase;
-    letter-spacing:.06em; }
-  .row1 .sq.state { margin-left:auto; }
-  .cols { display:grid; grid-template-columns:1fr 1fr 1fr 1fr; gap:4px; margin-top:7px;
-    font-size:11px; color:var(--muted); }
-  .cols b { display:block; color:var(--text); font-weight:500; font-size:11px; }
-  .detail { border-top:1px solid var(--border); padding:7px 10px; font-size:11px;
-    color:var(--muted); display:grid; gap:4px; }
-  .detail .k { color:var(--text); }
-  .err { color:var(--bad); }
-  .warn, .cols .warn b { color:var(--warn); }
-  .foot { color:var(--muted); font-size:10px; margin-top:10px; display:flex;
-    justify-content:space-between; }
-</style>
-</head>
-<body>
-<div class="shell">
-  <div class="head-wrap">
-    <div class="head">
-      <div class="head-row"><h1 id="run">fleetflow</h1><span class="meta mono" id="meta"></span></div>
-      <div class="sub-row">
-        <div class="sq-row" id="sqs"></div>
-        <div class="ctrls mono">
-          <div class="seg" id="sortSeg" title="sort agent cards">
-            <button data-v="active">active</button><button data-v="name">name</button><button data-v="tokens">tokens</button>
-          </div>
-          <div class="seg" id="sizeSeg" title="card size">
-            <button data-v="s">S</button><button data-v="m">M</button><button data-v="l">L</button>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-  <div id="phases"></div>
-  <div class="foot mono"><span id="foot">waiting for status.json…</span><span id="ts"></span></div>
-</div>
-<script>
-const fmt = s => s >= 3600 ? Math.floor(s/3600)+"h"+String(Math.floor(s%3600/60)).padStart(2,"0")+"m"
-  : s >= 60 ? Math.floor(s/60)+"m"+String(s%60).padStart(2,"0")+"s" : s+"s";
-const tok = n => !n ? "—" : n >= 1e6 ? (n/1e6).toFixed(1)+"M" : n >= 1e3 ? (n/1e3).toFixed(1)+"k" : String(n);
-const esc = t => (t||"").replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
-const open = new Set();
-let misses = 0, last = null;
-// sort doctrine: "active" is the DEFAULT — in-flight lanes (running, then
-// stalled) float to the top so the work you're watching never scrolls away.
-const STATE_RANK = { running:0, stalled:1, failed:2, done:3 };
-const SORTS = {
-  active: (a,b) => (STATE_RANK[a.state] ?? 9) - (STATE_RANK[b.state] ?? 9)
-    || (a.started || 0) - (b.started || 0) || a.id.localeCompare(b.id),
-  name: (a,b) => a.id.localeCompare(b.id),
-  tokens: (a,b) => (b.tokens || 0) - (a.tokens || 0) || a.id.localeCompare(b.id),
-};
-let sortMode = localStorage.getItem("ff.sort") || "active";
-if (!SORTS[sortMode]) sortMode = "active";
-let cardSize = localStorage.getItem("ff.size") || "m";
-async function tick() {
-  let d = null;
-  try { d = await (await fetch("status.json?t="+Date.now())).json(); }
-  catch (e) { d = null; }
-  // torn-write guard: ff-status rewrites status.json via status.json.tmp -> mv,
-  // so a reader can catch a transient {"lanes":[]} (or an unparseable shard)
-  // mid-replace. Treat an empty/missing-lanes payload like a fetch miss - keep
-  // the previous render and count it toward the 3-miss warning.
-  if (!d || !Array.isArray(d.lanes) || d.lanes.length === 0) {
-    if (++misses >= 3) document.getElementById("foot").textContent =
-      "status.json empty or unreachable — torn write, or ff-status --watch not running?";
-    return;
-  }
-  misses = 0;
-  last = d;
-  render(d);
-}
-function render(d) {
-  const lanes = d.lanes;
-  // "stalled" is a running lane whose signal froze (ff-status, FLEETFLOW_STALL_SECONDS).
-  // It is still in flight — never count it toward the finished tally.
-  const inflight = s => s === "running" || s === "stalled";
-  const running = lanes.filter(l => l.state === "running").length;
-  const stalled = lanes.filter(l => l.state === "stalled").length;
-  document.getElementById("run").textContent = d.run;
-  const oldest = Math.min(...lanes.map(l => l.started).filter(Boolean));
-  const bits = [lanes.length + " agents"];
-  if (running) bits.push(running + " running");
-  if (stalled) bits.push(stalled + " stalled");
-  if (!running && !stalled) bits.push("finished");
-  if (oldest) bits.push(fmt(d.generated_at - oldest));
-  document.getElementById("meta").textContent = bits.join(" · ");
-  document.getElementById("sqs").innerHTML = lanes.map(l =>
-    `<div class="sq ${l.state}" title="${esc(l.id)} — ${esc(l.state)}"></div>`).join("");
-  const BRAIN = { glm:["G","#7f77dd"], codex:["C","#0f6e56"], sonnet:["S","#d85a30"],
-    opus:["O","#ba7517"], haiku:["H","#d4537e"], fable:["F","#378add"] };
-  const card = l => {
-    const [ltr, col] = BRAIN[l.brain] || ["?", "#888780"];
-    return `
-    <details class="agent" data-id="${esc(l.id)}" ${open.has(l.id) ? "open" : ""}>
-      <summary>
-        <div class="row1"><span class="chip" style="background:${col}">${ltr}</span>
-          <span class="id mono">${esc(l.id)}</span><span class="brain mono">${esc(l.brain)}</span>
-          <span class="sq state ${l.state}"></span></div>
-        <div class="cols mono">
-          ${l.stalled
-            ? `<span class="warn"><b>${fmt(l.last_activity_s)}</b>silent</span>`
-            : `<span><b>${fmt(l.elapsed_s)}</b>time</span>`}
-          <span><b>${l.tools || "—"}</b>tools</span>
-          <span><b>${l.commits || "—"}</b>commits</span>
-          <span><b>${tok(l.tokens)}</b>tokens</span>
-        </div>
-      </summary>
-      <div class="detail mono">
-        ${l.stalled ? `<div class="warn">stalled — ${fmt(l.elapsed_s)} elapsed but nothing
-          written for ${fmt(l.last_activity_s)}; treat as wedged, not working</div>` : ""}
-        <div><span class="k">act</span> ${esc(l.activity)}</div>
-        ${l.last_commit ? `<div><span class="k">git</span> ${esc(l.last_commit)}</div>` : ""}
-        ${l.err_tail ? `<div class="err">${esc(l.err_tail)}</div>` : ""}
-        <div><span class="k">out</span> ${esc((l.artifact||"pending").split(/[\\/]/).pop())}</div>
-      </div>
-    </details>`; };
-  const phases = [];
-  lanes.forEach(l => { const p = l.phase || "build"; if (!phases.includes(p)) phases.push(p); });
-  document.getElementById("phases").innerHTML = phases.map(p => {
-    // sort applies WITHIN each phase group; the header pip strip keeps arrival
-    // order so pips stay stable while cards re-rank.
-    const pl = lanes.filter(l => (l.phase || "build") === p);
-    const sorted = pl.slice().sort(SORTS[sortMode]);
-    return `<div class="phase">
-      <div class="phase-h mono"><span>${esc(p)}</span>
-        <div class="sq-row">${pl.map(l => `<div class="sq ${l.state}"></div>`).join("")}</div>
-        <span>${pl.filter(l => !inflight(l.state)).length}/${pl.length}</span></div>
-      <div class="grid">${sorted.map(card).join("")}</div>
-    </div>`; }).join("");
-  document.querySelectorAll("details.agent").forEach(el =>
-    el.addEventListener("toggle", () => el.open ? open.add(el.dataset.id) : open.delete(el.dataset.id)));
-  document.getElementById("foot").textContent = d.repo;
-  document.getElementById("ts").textContent = new Date().toLocaleTimeString();
-}
-function seg(id, value, apply) {
-  const btns = document.querySelectorAll("#" + id + " button");
-  const paint = v => btns.forEach(b => b.classList.toggle("on", b.dataset.v === v));
-  paint(value);
-  btns.forEach(b => b.addEventListener("click", () => {
-    paint(b.dataset.v); apply(b.dataset.v);
-    if (last) render(last);   // re-rank immediately, don't wait for the next poll
-  }));
-}
-document.body.dataset.size = cardSize;
-seg("sortSeg", sortMode, v => { sortMode = v; localStorage.setItem("ff.sort", v); });
-seg("sizeSeg", cardSize, v => { cardSize = v; document.body.dataset.size = v;
-  localStorage.setItem("ff.size", v); });
-tick(); setInterval(tick, 2500);
-</script>
-</body>
-</html>

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

@@ -1,6 +0,0 @@
-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.

+ 0 - 141
skills/fleetflow/references/native-model-routing.md

@@ -1,141 +0,0 @@
-# Per-Stage Model/Effort Routing in Native Workflow Scripts
-
-> Mechanism verified 2026-07-05 against the live Workflow and Agent tool schemas
-> (Claude Code v2.1.x session surface). Model strings are **aliases** the harness
-> resolves at spawn time — this doc deliberately never names a dated model ID.
->
-> Scope: the **in-process** half of routing (native `agent()` calls sharing the
-> session's provider). The cross-provider half (GLM/Codex process workers) and
-> the shared work-class taxonomy live in
-> [fleet-worker/references/model-routing.md](../../fleet-worker/references/model-routing.md).
-
-## 1. Why — the cost evidence
-
-A 7-day session-log audit (2026-06-28..07-05, all projects on this machine):
-
-- Subagent transcripts produced **10.1M output tokens**. **75% ran on the two
-  premium tiers** — 4.8M on Opus, 2.9M on Fable — vs 2.2M on Sonnet and only
-  **0.3M on Haiku**.
-- **729 StructuredOutput tool calls** in the week — overwhelmingly mechanical
-  extract/verdict stages inside Workflow runs — every one billed at the premium
-  session model.
-
-Root cause is a default, not a decision: a Workflow `agent()` call **inherits
-the main-loop model and effort unless overridden**, and nobody sets overrides.
-On a Fable/Opus session, every "grep the logs and return JSON" stage runs on the
-most expensive brain available. The fix is one line per collect-stage.
-
-## 2. The mechanism
-
-```js
-await agent(prompt, {
-  model:  'haiku',   // 'sonnet' | 'opus' | 'haiku' | 'fable' — omit to inherit session model
-  effort: 'low',     // 'low' | 'medium' | 'high' | 'xhigh' | 'max' — omit to inherit session effort
-  // ...label, phase, schema, isolation, agentType as usual
-});
-```
-
-Facts that matter (all schema-sourced):
-
-- **Omission = inheritance.** No override → the agent runs at the session's
-  resolved model *and* effort. That is the correct default for stages whose
-  failure is expensive — and the silent cost sink for stages whose failure isn't.
-- **Aliases, not IDs.** `'opus'` means "the current Opus slot", resolved by the
-  harness. Never hard-code dated model IDs in a workflow script — they rot.
-- **`meta.phases[].model` is display metadata.** It annotates the phase in the
-  /workflows progress UI so the override is visible; it does **not** route
-  anything. `opts.model` on the `agent()` call is what routes. Set both — the
-  annotation is how a reader audits the run's routing at a glance.
-- **The Agent tool takes the same `model` param** for single subagent spawns,
-  and it composes with custom `agentType`/`subagent_type` (an `Explore` scout on
-  haiku is legal). Exception: **fork-type agents always inherit** — `model` is
-  ignored for `subagent_type: 'fork'`.
-- **`effort` is a reasoning-depth knob, not just a price knob.** Low effort
-  buys shallower thinking. Cheap and shallow is exactly right for extraction;
-  it is exactly wrong for a verdict.
-- **Schema-forced output is what makes cheap models safe here.** With
-  `opts.schema`, validation runs at the tool-call layer and re-asks on mismatch
-  — a haiku extractor can't hand back malformed JSON, only wrong content, which
-  is the verifier's job to catch anyway.
-
-## 3. Routing table — stage type → tier
-
-| Stage type | Typical stages | Override |
-|---|---|---|
-| **Mechanical collect** | StructuredOutput extraction, log scans, dedup, classification, format conversion, count/enumerate | `{ model: 'haiku', effort: 'low' }` |
-| **Broad sweep** | finders, per-dimension reviewers, read-and-summarize, multi-modal search legs | `{ model: 'sonnet' }` (+ `effort: 'low'` when the sweep is wide and mechanical) |
-| **Decide** | adversarial verifiers, judges, synthesis, final report, anything expensive-if-wrong | **omit both** — inherit the session's premium model |
-
-**The rule of thumb: the stage that DECIDES stays premium; the stages that
-COLLECT go cheap.** When unsure, omit the override — a wrong cheap answer that
-survives verification costs more than the override saves.
-
-Corollaries (carried over from the shared taxonomy, they bind here too):
-
-- **Never under-power a judge.** A cheap verifier that rubber-stamps launders
-  bad findings as confirmed — worse than no verifier. Deciders inherit; don't
-  even pin them to `'opus'`, because on a Fable session that's a *downgrade*.
-- **Reach for the effort lever before the model lever.** Same model at
-  `effort: 'low'` is often the bigger saving with no quality cliff.
-- **Budget pressure degrades collectors, never deciders.**
-  [route.js](../../fleet-worker/assets/route.js) encodes this: under 15%
-  remaining budget it steps collect-stages down a tier; judge/synthesize are
-  exempt.
-
-## 4. Before / after
-
-Extraction stage (the 729-calls-a-week shape):
-
-```js
-// before — inherits Fable/Opus for a grep-and-format job
-const flaky = await agent('Scan CI logs for retry markers; return the list.', { schema: FLAKY });
-
-// after — one line
-const flaky = await agent('Scan CI logs for retry markers; return the list.',
-  { schema: FLAKY, model: 'haiku', effort: 'low' });
-```
-
-Review fan-out — sweep cheap, verify premium:
-
-```js
-const results = await pipeline(
-  DIMENSIONS,
-  d => agent(d.prompt, { label: `review:${d.key}`, phase: 'Review',
-                         schema: FINDINGS, model: 'sonnet', effort: 'low' }),
-  review => parallel(review.findings.map(f => () =>
-    agent(`Adversarially verify: ${f.title}`,
-          { label: `verify:${f.file}`, phase: 'Verify', schema: VERDICT })
-          // no model/effort — the decider inherits the premium session model
-  ))
-);
-```
-
-Single Agent-tool spawn (same param, same doctrine):
-
-```js
-Agent({ subagent_type: 'Explore', model: 'haiku',
-        prompt: 'List every file that constructs a cache key…' })
-```
-
-## 5. Caveats — what this doctrine does NOT claim
-
-- **Not a fixed price list.** Aliases float; for actual $/token use
-  loop-ops' `loop-estimate.py` + `assets/model-pricing.json`.
-- **Not applicable to forks.** Fork-type agents inherit unconditionally.
-- **Not a provider switch.** `opts.model` picks a tier on the *session's*
-  provider; pointing a stage at GLM/Codex requires an OS-process worker
-  (fleetflow/fleet-worker) — see the locus rule in
-  [fleet-worker/references/model-routing.md](../../fleet-worker/references/model-routing.md).
-- **Not free accuracy.** A haiku collector will miss things a premium collector
-  wouldn't. That's acceptable *only* when a premium-tier verify/judge stage
-  stands downstream — which is why the [default posture](../SKILL.md) pairs
-  every cheap collect fan-out with a verify phase.
-
-## See also
-
-- [native-workflow-insights.md](native-workflow-insights.md) §5 — where
-  `opts.model`/`opts.effort` sit in the progress/structure surface.
-- [fleet-worker/references/model-routing.md](../../fleet-worker/references/model-routing.md)
-  — the shared work-class taxonomy and the in-process vs provider-worker locus rule.
-- [fleet-worker/assets/route.js](../../fleet-worker/assets/route.js) — paste-in
-  helper implementing this table (`route('mechanical')` etc.).

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

@@ -1,165 +0,0 @@
-# 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").
-  When and how to actually set them — the collect-cheap/decide-premium
-  doctrine, with the cost evidence — is
-  [native-model-routing.md](native-model-routing.md).
-
-**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").

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

@@ -1,325 +0,0 @@
-# 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).
-- **Worktree-lane commit gotcha** (observed 2026-07-05): a git worktree's
-  metadata (index, refs) lives in the MAIN repo's `.git/` — outside the codex
-  sandbox's writable root — so `git commit` inside a lane fails with
-  `index.lock: Permission denied` under `--full-auto`. `ff-spawn` fixes this
-  by passing `--add-dir <absolute-git-dir>` for codex worktree lanes; if you
-  launch codex by hand, add it yourself (or use a full clone as the lane).
-- **Sandbox litter gotcha** (observed 2026-07-05): codex's sandbox creates
-  pytest temp/cache dirs (`.pytest-tmp/`, `pytest-cache-files-*/`) with
-  AppContainer ACLs that survive the run and resist unelevated deletion —
-  even `takeown` + `icacls /reset` fail. Consequences: `git worktree remove`
-  leaves a husk, and moving the repo needs a copy-around (`robocopy /XD`).
-  Mitigations: have codex packets run pytest with `-p no:cacheprovider` and a
-  tmp dir inside the lane, or plan on one elevated `Remove-Item` at cleanup.
-- **Skill-loading quirk** (observed 2026-07-05): codex reads Claude-format
-  skills from `~/.agents/skills/` at session start and rejects any whose
-  description exceeds **1024 chars** (`failed to load skill … exceeds maximum
-  length`) — non-fatal (logged to stderr, run continues), so don't gate on a
-  clean stderr; `ff-collect` correctly ignores it. Keep skill descriptions
-  ≤1024 chars if you want codex workers to load them.
-
-## 3. Grok workers (`grok -p`)
-
-xAI's **Grok Build CLI** (`grok`, alias `agent`; v0.2.93 verified) — an agentic
-coding TUI, a direct Claude Code peer, but a **non-Anthropic** worker: its own
-binary and protocol, **not** a `claude -p` wrapper. Verified against
-`grok --help` + live headless runs 2026-07-11.
-
-- **Launch** (headless, one turn, prints + exits):
-  `grok --prompt-file <f> --output-format json --always-approve --max-turns N
-  [-m MODEL] [--reasoning-effort EFFORT] [--json-schema '<SCHEMA-STRING>']`.
-  `-p/--single "<prompt>"` is the inline-prompt equivalent of `--prompt-file`.
-  ff-spawn runs it from the lane worktree (`cd`), so grok's own `-w/--worktree`
-  is deliberately unused — the cage is fleetflow's worktree, one owner per tree.
-- **Autonomous tools:** `--always-approve` auto-approves *all* tool executions —
-  the codex `--full-auto` analog and a **blast-radius flag**; safe only because
-  the lane worktree + escape guard bound it. Granular alternative:
-  `--permission-mode dontAsk` + `--allow <RULE>` (mirrors Claude Code
-  `--allowedTools`; modes: `default|acceptEdits|auto|dontAsk|bypassPermissions|plan`).
-- **Structured output:** `--json-schema '<schema>'` takes the schema as a JSON
-  **string** (not a file path), implies `--output-format json`, and surfaces an
-  **already-parsed `.structuredOutput`** field — grok validates server-side, so
-  ff-collect prefers it over re-parsing `.text`. ff-spawn passes the schema
-  out-of-band (like codex's `--output-schema`), not appended to the prompt.
-- **Envelope** (`--output-format json`): `{text, stopReason, sessionId,
-  requestId, thought, structuredOutput?}` — **no `is_error`**. A clean turn ends
-  `stopReason:"EndTurn"`.
-- **Output modes** (`--output-format`, verified live 2026-07-11): `plain` (text
-  only, the default), `json` (the single buffered envelope above), and
-  `streaming-json` — **NDJSON**, one event object per line streamed as it
-  generates: `{"type":"thought","data":"…"}` (reasoning tokens),
-  `{"type":"text","data":"…"}` (answer tokens), terminated by
-  `{"type":"end","stopReason","sessionId","requestId"}`. This is grok's analog of
-  `claude -p --output-format stream-json`. **ff-collect gates on the buffered
-  `json` envelope** (a whole-turn result is what a lane's success is judged on);
-  `streaming-json` *would be* the live-progress source — the codex `--json`
-  event-stream analog. Consumer note: it interleaves `thought` and `text`, so
-  filter `type=="text"` for answer-only, and read `structuredOutput` off the
-  terminal event (not a `thought`) on `--json-schema` lanes.
-  - **NOT WIRED UP — status as of 2026-07-27.** `ff-spawn` launches grok with
-    `--output-format json`, so **no `<id>.events.jsonl` is ever written for a
-    grok lane**. Consequences: the live monitor shows a grok lane with no tools
-    and no activity until it exits, and `ff-status` reports
-    `live_signal: false` for it, so grok lanes are outside stall detection
-    (their silence proves nothing — see ff-status's stall block). An earlier
-    version of this file, and of SKILL.md, implied the monitor already consumed
-    grok's stream; it never has.
-  - **What adopting it costs.** `--output-format` takes one value, so switching
-    to `streaming-json` means `$ART` must be *reconstructed* from the NDJSON —
-    join `type=="text"` `.data` for `text`, take `stopReason`/`sessionId`/
-    `requestId`/`structuredOutput` off the terminal `end` event — because
-    `ff-collect`'s grok gate parses the buffered envelope. That changes the gate's
-    input, so it should not be attempted on the strength of this note alone: the
-    event shape above was verified 2026-07-11 and has not been re-checked since.
-    Re-verify against a live one-turn run first, then change both sides together.
-- **Auth:** the `GROK_DEPLOYMENT_KEY` env var **only** — grok has no
-  config/auth/key subcommand to store a deployment key (`~/.grok/auth.json`
-  holds OAuth tokens, and OAuth lacked chat entitlement on the test account, so
-  the `xai-…` deployment key is the working path). ff-spawn reads it from the
-  **inherited environment** — never written to disk, args (`ps`-safe), or logs.
-  Config home is `~/.grok/`; **no per-worker config-dir isolation needed** (like
-  codex, grok has no Claude OAuth to collide with).
-- **Gate** (`ff-collect`): rc was already gated by ff-spawn (a failed grok run
-  exits nonzero + writes stderr), so the content gate is **envelope parses AND
-  non-empty `.text`** (or `.structuredOutput` for `--schema` lanes). `stopReason`
-  is informational — *not* hard-gated, because an agentic tool-turn can end on a
-  terminal reason other than `EndTurn`.
-- **Models:** `grok models` lists available IDs (test account: `grok-4.5`, the
-  default). `-m`/`FLEETFLOW_GROK_MODEL` overrides.
-- **Character:** a genuinely different **provider** (xAI), model, *and* harness —
-  its highest fleetflow value is **cross-provider dissent** (refuter/judge lanes)
-  and independent second implementations, same role codex plays.
-- **fleetflow env knobs:** `FLEETFLOW_GROK_BIN` (default `grok`; point at
-  `…/grok.exe` when it isn't on PATH), `FLEETFLOW_GROK_MODEL` (model override).
-- **Transcript archiving is not wired for grok** (its session store under
-  `~/.grok/` isn't a verified path); `ff-spawn`'s archive step skips grok with a
-  `transcript source not found … (non-fatal)` note — the lane still passes.
-- **Programmatic surface (beyond `-p`)**, for tighter SDK integration than a
-  one-shot headless call: `grok agent stdio` (drive over stdio — the SDK path),
-  `grok agent headless` (over the Grok WebSocket relay), `grok agent serve`
-  (WebSocket server, `--bind` default `127.0.0.1:2419`, `--secret`/
-  `GROK_AGENT_SECRET`), `grok agent leader` (one shared backend for multiple
-  clients; `--leader`/`--no-leader`, `--leader-socket`). fleetflow uses plain
-  headless `grok -p`; the relay/serve/leader modes are for embedding grok as a
-  long-lived backend, out of scope for hub-and-spoke lanes.
-- **MCP:** `grok mcp add|list|remove|doctor` — grok is an MCP *client* like
-  Claude Code, so a lane can be given MCP servers via its `~/.grok` config.
-- **Terms:** deployment-key usage bills to your xAI account/plan — verify its
-  terms as you would codex's ChatGPT-plan billing.
-
-## 4. Pi workers (`pi -p`)
-
-earendil-works **Pi** (`@earendil-works/pi-coding-agent`; 0.83.0 verified
-2026-08-01, local install `X:\Agents\Pi` driven via `FLEETFLOW_PI_BIN=X:/Agents/Pi/pi.cmd`).
-A minimal agent harness fronting **15+ providers / hundreds of models** — which
-makes it the fleet's **wildcard brain**: gemini, deepseek, zai, groq, mistral,
-openrouter… are `FLEETFLOW_PI_PROVIDER`/`FLEETFLOW_PI_MODEL` env changes, not
-new brain code. Also a genuinely third *harness* (its own read/bash/edit/write
-toolchain), so it doubles as cross-harness dissent alongside codex and grok.
-
-- **Launch**: `pi -p --mode json --no-extensions --no-skills
-  [--provider P] [--model M] [--thinking L] < packet > <id>.events.jsonl`
-  from the lane worktree, with `PI_CODING_AGENT_DIR` pointed at the lane's
-  isolated config dir (the GLM `CLAUDE_CONFIG_DIR` analog).
-  - Prompt goes via **stdin, never argv** — `-p` merges piped stdin into the
-    initial prompt (docs/usage.md), and the launcher is usually a `.cmd` shim,
-    so an argv prompt would die on cmd.exe's ~8K command-line cap.
-  - `--no-extensions --no-skills` because discovery loads behavior *outside*
-    the cache key's hash — same packet must mean same run. A packet that wants
-    a specific skill says so in its prompt (or the spawn adds `--skill <path>`,
-    which is additive even with `--no-skills`).
-- **Envelope**: pi's `--mode json` emits an **NDJSON event stream** (session
-  header, then `agent_start`/`turn_*`/`message_*`/`tool_execution_*`, ending in
-  `agent_end` with the full message list). **ff-spawn distills that stream into
-  a claude-style `{is_error, result, usage, num_turns, total_cost_usd,
-  modelUsage}` envelope** at `<id>.result.json`: last assistant message's text
-  → `result`; `stopReason` `error`/`aborted` (or no assistant reply) →
-  `is_error`; pi usage `{input,output,cacheRead,cacheWrite,cost.total}` summed
-  across assistant turns → claude usage names. So **ff-collect and ff-status
-  need no pi branch at all** — pi lanes ride the default claude-envelope gate
-  and the finished-lane reader unchanged. Keep it that way.
-- **Live signal**: the raw stream at `<id>.events.jsonl` is written as pi
-  works, so pi lanes are **fully covered by the stall detector** (like codex).
-  Pi's events carry no timestamps → any future density strip would be
-  `"sequence"` basis, like codex's; live token/tool introspection is not parsed
-  today (totals arrive at finish via the distilled envelope).
-- **No sandbox, no turn cap.** Pi executes tools directly with the user's
-  permissions (its security doc is explicit that it has no sandbox) — GLM-class
-  posture: the cage is the worktree lane + guard preamble + escape guard. And
-  pi has **no `--max-turns` equivalent**, so bounds are the stall detector and
-  orchestrator wall-clock patience. Pi *can* self-commit (no codex-style git
-  lock problem).
-- **No headless trust prompt** (docs/security.md): non-interactive modes never
-  ask; untrusted project resources are silently skipped under the default
-  `defaultProjectTrust: "ask"`. No UAC-style hang risk.
-- **Auth**: provider API-key env vars **only** — the isolated
-  `PI_CODING_AGENT_DIR` means the host's `~/.pi/agent/auth.json` (OAuth/login
-  store) never reaches a lane. Keys per provider: `GEMINI_API_KEY`,
-  `ZAI_API_KEY`, `DEEPSEEK_API_KEY`, `GROQ_API_KEY`, `OPENAI_API_KEY`,
-  `ANTHROPIC_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, `OPENROUTER_API_KEY`,
-  `CEREBRAS_API_KEY`, … (full list: `pi --help`). `ff-doctor --live` probes key
-  *presence* for the configured provider, not validity.
-- **Effort lever**: fleetflow `--effort low|medium|high` maps 1:1 onto pi
-  `--thinking`; `max` → `xhigh` (pi also has `off`/`minimal` below the fleet
-  scale). Provider support varies by model; unsupported levels are pi's
-  problem to clamp, not ours.
-- **Transcripts**: pi persists sessions under
-  `<PI_CODING_AGENT_DIR>/sessions/<path-slug>/<uuid>.jsonl`; the lane dir is
-  isolated, so ff-spawn's archive step takes the newest one. `pi --export
-  <session.jsonl>` renders it to HTML if a human wants to read a lane.
-- **fleetflow env knobs**: `FLEETFLOW_PI_BIN` (default `pi`; point at the local
-  install's `pi.cmd`), `FLEETFLOW_PI_PROVIDER`, `FLEETFLOW_PI_MODEL`.
-- **Terms**: pure API-key billing per provider — no subscription-automation
-  question (unlike codex/Anthropic-subscription workers). The usual per-provider
-  rate limits are the concurrency binding constraint.
-
-## 5. 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.
-
-## 6. 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).
-
-## 7. Uniform result layout
-
-```
-<repo>/.fleetflow/<run>/
-├── journal.jsonl        # started/result records, v2 content-hash keys (v: FF_VERSION)
-├── manifest.json        # orchestrator plan: {run,base,created_by,phases[],packets[]}
-├── main-baseline.txt    # main-checkout status snapshot (escape-guard baseline)
-├── <id>.prompt.txt      # guard preamble + packet (what was actually sent)
-├── <id>.result.json     # claude JSON envelope (glm/anthropic brains) OR
-│                        #   grok envelope {text,stopReason,…} (grok brain)
-├── <id>.last.txt        # codex last-message (codex brain)
-├── <id>.events.jsonl    # codex --json event stream (codex brain)
-├── <id>.transcript.jsonl# archived session transcript (claude-brain lanes; best-effort)
-├── <id>.invalid.txt     # last failed schema output (only when --repair ran)
-├── <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.
-
-## 8. Effort lever, cache redirect, transcript archiving (Wave 1)
-
-**Effort lever** (`ff-spawn --effort low|medium|high|max`). Default unset =
-inherit the brain's own. The mapping (effort IS part of the cache-key OPTS
-string, so changing only effort is a cache miss):
-
-| Brain | How effort is applied |
-|---|---|
-| glm (fleet-worker) | `FLEET_WORKER_EFFORT=<v>` in the worker env |
-| codex (`codex exec`) | `-c model_reasoning_effort="<v>"` |
-| grok (`grok -p`) | `--reasoning-effort <v>` (alias `--effort`) |
-| sonnet/opus/haiku/fable | `claude -p --settings '{"effortLevel":"<v>"}'` |
-
-Doctrine (carried from the native tool): reach for the effort lever before the
-model lever. Effort is recorded in the manifest packet, so `ff-run resume`
-replays each lane at its captured effort.
-
-**Cache & tmp redirect.** Every worker launch exports `UV_CACHE_DIR`,
-`TMPDIR`, `TMP`, `TEMP` into the worker env pointing at
-`${FLEETFLOW_CACHE_ROOT:-$HOME/.fleet-worker/cache}/<run>-<id>/` (created
-pre-launch). This keeps uv/pytest cache and codex sandbox litter OUT of the
-repo and lanes — the AppContainer-ACL'd dirs codex leaves behind resist
-unelevated deletion and would otherwise block `git worktree remove` (the
-`sandbox litter gotcha`, §2). Set `FLEETFLOW_CACHE_ROOT` once for the whole run
-and pass the same value to `ff-clean` so it can reclaim the dirs.
-
-**Transcript archiving** (best-effort, never fails the lane). After a
-non-dry-run claude-brain lane, the session transcript is copied to
-`<rundir>/<id>.transcript.jsonl`:
-
-- **glm**: newest `projects/*/*.jsonl` under the worker's isolated config dir
-  (`FLEET_WORKER_CONFIG_DIR`, default `$HOME/.fleet-worker/cfg-ff-<id>`).
-- **sonnet/opus/haiku/fable**: the result envelope's `session_id` resolves to
-  `$HOME/.claude/projects/<encoded-workdir>/<session_id>.jsonl`, where the
-  encoding is per-char `[:\\/.]` → `-` (verified empirically: `C:\Users\Mack`
-  → `C--Users-Mack`). Falls back to a `<session_id>.jsonl` search across all
-  project dirs if the encoded path misses.
-
-If the source transcript can't be found, the lane logs a `transcript source
-not found ... skipped (non-fatal)` warning and continues — the transcript is a
-convenience for inspection/audit, not a gate.

+ 0 - 20
skills/fleetflow/scripts/_env.sh

@@ -1,20 +0,0 @@
-# _env.sh - PATH self-heal sourced by every fleetflow entry script.
-#
-# WHY: GUI-launched hosts (Claude Desktop et al.) snapshot their environment
-# at app start, so User-PATH entries added later by installers (winget, npm,
-# scoop, pipx, uv) are invisible to child shells until the host restarts.
-# Sessions then fail with "claude/jq/keyring not found" despite correct
-# installs. This block re-adds the canonical per-user tool dirs when present.
-# Extend via FLEETFLOW_PATH_PREPEND (colon-separated) for exotic locations.
-for _ffd in ${FLEETFLOW_PATH_PREPEND:+$(echo "$FLEETFLOW_PATH_PREPEND" | tr ':' ' ')} \
-            "$HOME/.local/bin" \
-            "$HOME/AppData/Local/Microsoft/WinGet/Links" \
-            "$HOME/AppData/Roaming/npm" \
-            "$HOME/scoop/shims" \
-            "$HOME/AppData/Local/Programs/Python/Python313/Scripts" \
-            "$HOME/AppData/Roaming/Python/Python313/Scripts" \
-            "$HOME/.local/share/uv/tools" ; do
-  [ -d "$_ffd" ] && case ":$PATH:" in *":$_ffd:"*) ;; *) PATH="$_ffd:$PATH" ;; esac
-done
-unset _ffd
-export PATH

+ 0 - 208
skills/fleetflow/scripts/ff-clean.sh

@@ -1,208 +0,0 @@
-#!/usr/bin/env bash
-# ff-clean.sh - reclaim a fleetflow run's lanes, branches, and cache dirs.
-#
-# Per lane (worktree lane under <run>/wt-<id>):
-#   zero commits + clean      -> git worktree remove + branch -D   ("removed")
-#   zero commits + dirty      -> "kept" unless --force              (--force removes)
-#   N>0 commits               -> "kept (N commits)"                 (never auto-remove)
-#   locked ACL-litter dir     -> "locked (needs elevation)"         (continue, non-fatal)
-# Then removes each lane's cache dir under FLEETFLOW_CACHE_ROOT (default
-# $HOME/.fleet-worker/cache/<run>-<id>). Removal never aborts the run: a locked
-# lane is reported and skipped, and the script exits 0 once it has tried them all.
-# stdout: one TSV line per lane: id<TAB>status<TAB>detail. stderr: chatter.
-#
-# Exit codes: 0 done (incl. some locked lanes) | 2 usage / no such run
-set -u
-. "$(dirname "${BASH_SOURCE[0]}")/_env.sh"
-
-FF_VERSION="1.1.0"
-
-usage() {
-  cat <<'EOF'
-Usage: ff-clean.sh --run NAME [--repo PATH] [--force]
-       ff-clean.sh --run NAME --reap [--force]
-
-  --run NAME      run name under <repo>/.fleetflow/
-  --repo PATH     repo root (default: git toplevel of cwd)
-  --force         also remove DIRTY zero-commit lanes (committed lanes are
-                  ALWAYS kept - land or branch them first, then clean).
-                  With --reap, actually terminate the orphans.
-  --reap          report worker processes still alive from this run instead of
-                  touching lanes. Report-only unless --force. Windows only -
-                  identifies orphans by walking up to the reap anchors ff-spawn
-                  journalled, so a Codex you started yourself is never a match.
-
-  Lanes with commits are never removed (their work would be lost); clean only
-  after ff-collect has gated them and the orchestrator has landed/branched the
-  keepers. Locked codex sandbox-litter dirs are reported and skipped.
-
-EXAMPLES
-  ff-clean.sh --run currency
-  ff-clean.sh --run currency --force
-  ff-clean.sh --run currency --repo /path/to/repo | column -t
-  ff-clean.sh --run currency --reap             # list this run's stray workers
-  ff-clean.sh --run currency --reap --force     # and kill them
-EOF
-}
-
-err() { echo "ff-clean: $*" >&2; }
-emit() { printf '%s\t%s\t%s\n' "$1" "$2" "$3"; }   # id <TAB> status <TAB> detail
-
-RUN="" REPO="" FORCE=0 REAP=0
-while [ $# -gt 0 ]; do
-  case "$1" in
-    --run) RUN="${2:-}"; shift 2 ;;
-    --repo) REPO="${2:-}"; shift 2 ;;
-    --force) FORCE=1; shift ;;
-    --reap) REAP=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; }
-command -v git >/dev/null || { err "git required"; exit 2; }
-[ -n "$RUN" ] || { err "--run required"; usage >&2; 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; }
-
-RUNDIR="$REPO/.fleetflow/$RUN"
-MANIFEST="$RUNDIR/manifest.json"
-[ -d "$RUNDIR" ] || { err "no such run: $RUNDIR"; exit 2; }
-
-CACHE_ROOT="${FLEETFLOW_CACHE_ROOT:-$HOME/.fleet-worker/cache}"
-BASE="$(jq -r '.base // "main"' "$MANIFEST" 2>/dev/null)"; [ -n "$BASE" ] || BASE="main"
-
-# enumerate lane ids: manifest packets (authoritative) + any wt-* dirs left on
-# disk (e.g. manifest pruned but lanes remain) + journal started ids (fallback).
-list_lane_ids() {
-  local ids=""
-  [ -f "$MANIFEST" ] && ids="$(jq -r '.packets[].id' "$MANIFEST" 2>/dev/null)"
-  local d
-  for d in "$RUNDIR"/wt-*; do
-    [ -d "$d" ] || continue
-    ids="$ids
-$(basename "$d" | sed 's/^wt-//')"
-  done
-  if [ -z "$ids" ] && [ -f "$RUNDIR/journal.jsonl" ]; then
-    ids="$(jq -r 'select(.type=="started") | .id' "$RUNDIR/journal.jsonl" 2>/dev/null)"
-  fi
-  printf '%s\n' "$ids" | awk 'NF && !seen[$0]++'
-}
-
-# remove_lane <id> <force>: worktree remove (+ branch -D on success). Reports
-# "locked" if the OS refuses to delete (codex AppContainer-ACL litter).
-remove_lane() {
-  local id="$1" f="$2" wt="$RUNDIR/wt-$id" br="fleetflow/$RUN/$id" wflag=""
-  [ "$f" = 1 ] && wflag="--force"
-  if git -C "$REPO" worktree remove $wflag "$wt" 2>>"$RUNDIR/$id.clean.err"; then
-    git -C "$REPO" branch -D "$br" >/dev/null 2>>"$RUNDIR/$id.clean.err" \
-      && emit "$id" removed "worktree + branch gone" \
-      || emit "$id" removed "worktree gone (branch delete skipped)"
-  else
-    emit "$id" locked "needs elevation (see $RUNDIR/$id.clean.err)"
-  fi
-}
-
-# clean_cache <id>: best-effort removal of the lane's cache/tmp dir.
-clean_cache() {
-  local id="$1" c="$CACHE_ROOT/$RUN-$id"
-  [ -d "$c" ] || return 0
-  if rm -rf "$c" 2>>"$RUNDIR/$id.clean.err"; then err "cache removed: $c"
-  else err "cache locked (needs elevation): $c"; fi
-}
-
-# --- reap: kill worker processes the wrapper left behind -----------------------
-# TaskStop (or killing the background Bash task) kills ff-spawn, not the worker:
-# on 2026-07-27 five codex.exe / codex-code-mode-host.exe survived a lane kill.
-# Reaping by image name alone would also kill the user's own interactive Codex,
-# so we match by ANCESTRY instead: every candidate must descend from a WINPID
-# ff-spawn journalled for this run. `at` bounds PID reuse - a process that
-# started before its anchor cannot be that anchor's child.
-# Windows-only by nature (Win32_Process); elsewhere the POSIX process group dies
-# with the wrapper, so there is nothing to reap.
-reap_run() {
-  local anchors since ps1 out
-  anchors="$(jq -r 'select(.type=="proc" and .winpid != null) | .winpid' "$RUNDIR/journal.jsonl" 2>/dev/null \
-             | awk 'NF && !seen[$0]++' | paste -sd, -)"
-  since="$(jq -r 'select(.type=="proc") | .at' "$RUNDIR/journal.jsonl" 2>/dev/null | sort -n | head -1)"
-  case "$(uname -s 2>/dev/null)" in
-    MINGW*|MSYS*|CYGWIN*) ;;
-    *) err "--reap is Windows-only (elsewhere the worker dies with its process group)"; return 0 ;;
-  esac
-  if [ -z "$anchors" ]; then
-    err "no reap anchors journalled for '$RUN' (run predates ff-spawn's proc records)"
-    return 0
-  fi
-  command -v powershell.exe >/dev/null || { err "powershell.exe not found - cannot enumerate processes"; return 0; }
-  err "reaping run '$RUN': anchors=[$anchors] since=$since force=$FORCE"
-  # PowerShell does the walk: one CIM snapshot, index by ProcessId, climb each
-  # codex* process's parent chain (bounded) looking for an anchor.
-  ps1="\$ErrorActionPreference='Stop'
-\$anchors = @($anchors)
-\$since = [DateTimeOffset]::FromUnixTimeSeconds($since).UtcDateTime
-\$map = @{}
-Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | ForEach-Object { \$map[[int]\$_.ProcessId] = \$_ }
-foreach (\$p in @(\$map.Values)) {
-  if (\$p.Name -notlike 'codex*') { continue }
-  if (\$p.CreationDate -and \$p.CreationDate.ToUniversalTime() -lt \$since) { continue }
-  \$cur = \$p; \$hops = 0; \$hit = \$false
-  while (\$cur -ne \$null -and \$hops -lt 12) {
-    if (\$anchors -contains [int]\$cur.ParentProcessId) { \$hit = \$true; break }
-    \$cur = \$map[[int]\$cur.ParentProcessId]; \$hops++
-  }
-  if (-not \$hit) { continue }
-  \$status = 'alive'
-  if ('$FORCE' -eq '1') {
-    try { Stop-Process -Id \$p.ProcessId -Force -ErrorAction Stop; \$status = 'killed' }
-    catch { \$status = 'kill-failed' }
-  }
-  # [char]9, not a backtick escape: PowerShell single-quoted strings do NOT
-  # process \` sequences, so '{0}\`t{1}' emits a literal backtick-t (caught in test).
-  (\$p.ProcessId, \$p.Name, \$status) -join [char]9
-}"
-  out="$(powershell.exe -NoProfile -NonInteractive -Command "$ps1" 2>/dev/null | tr -d '\r')"
-  if [ -z "$out" ]; then
-    err "no surviving worker processes from this run"
-  else
-    printf '%s\n' "$out"
-    [ "$FORCE" = 1 ] || err "report only - re-run with --force to terminate"
-  fi
-}
-
-if [ "$REAP" = 1 ]; then
-  reap_run
-  exit 0
-fi
-
-NLANES="$(list_lane_ids | wc -l | tr -d ' ')"
-err "cleaning run '$RUN': $NLANES lane(s); FORCE=$FORCE; base=$BASE; cache=$CACHE_ROOT"
-if [ "$NLANES" = "0" ]; then
-  err "no lanes to clean"
-  exit 0
-fi
-
-# base ref must exist for rev-list counting; fall back to HEAD once for the run
-git -C "$REPO" show-ref --verify --quiet "refs/heads/$BASE" || BASE="HEAD"
-
-while read -r id; do
-  [ -n "$id" ] || continue
-  wt="$RUNDIR/wt-$id"
-  if [ -d "$wt" ]; then
-    commits="$(git -C "$wt" rev-list --count "$BASE..HEAD" 2>/dev/null || echo 0)"
-    dirt="$(git -C "$wt" status --porcelain 2>/dev/null)"
-    if [ "${commits:-0}" -eq 0 ] && [ -z "$dirt" ]; then
-      remove_lane "$id" 0
-    elif [ "${commits:-0}" -eq 0 ] && [ -n "$dirt" ]; then
-      if [ "$FORCE" = 1 ]; then remove_lane "$id" 1
-      else emit "$id" kept "dirty, zero commits (use --force)"; fi
-    else
-      emit "$id" kept "$commits commits"
-    fi
-  else
-    emit "$id" kept "no worktree lane"
-  fi
-  clean_cache "$id"
-done < <(list_lane_ids)
-
-exit 0

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

@@ -1,203 +0,0 @@
-#!/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); grok gates on a
-# parseable envelope with non-empty .text (its envelope has no is_error - a
-# failed grok run exits nonzero, which ff-spawn already caught). pi needs no
-# branch here AT ALL: ff-spawn distills its event stream into the same
-# claude-style {is_error,result,...} envelope, so pi lanes ride the default
-# gate below - keep it that way rather than adding a pi case. 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
-. "$(dirname "${BASH_SOURCE[0]}")/_env.sh"
-
-FF_VERSION="1.1.0"
-
-usage() {
-  cat <<'EOF'
-Usage: ff-collect.sh --run NAME --id ID [--repo PATH] [--schema] [--repair]
-       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 (markdown code fences are stripped first)
-  --repair             on --schema failure: save the bad output to
-                       <run>/<id>.invalid.txt and respawn a <id>-repair lane
-                       (one attempt); print the repaired text on success
-  --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 --run audit --id verdict --schema --repair
-  ff-collect.sh --check-main-clean --run audit
-EOF
-}
-
-err() { echo "ff-collect: $*" >&2; }
-
-RUN="" ID="" REPO="" SCHEMA=0 CHECK_CLEAN=0 REPAIR=0 JQERR=""
-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 ;;
-    --repair) REPAIR=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; }
-
-HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-SPAWN="$HERE/ff-spawn.sh"
-COLLECT="$HERE/ff-collect.sh"
-
-# strip markdown code fences (```json ... ```) so schema gating tolerates fenced replies
-strip_fences() { sed -E '/^[[:space:]]*```[[:alnum:]]*[[:space:]]*$/d'; }
-
-# stdin = text; returns 0 if valid JSON else 1; sets global JQERR to the parse error
-json_ok() {
-  local t; t="$(cat)"
-  JQERR="$(printf '%s' "$t" | jq empty 2>&1 1>/dev/null | head -1)"
-  printf '%s' "$t" | jq empty >/dev/null 2>&1
-}
-
-# do_repair <bad_text> <jq_error>: save bad output, respawn a <id>-repair lane
-# (same brain, --max-turns 3, no worktree), gate it, print its text on success.
-# One attempt only. Exits 0 (repaired) or 10 (repair also failed). FLEETFLOW_REPAIR_DRYRUN
-# forces the respawn to --dry-run (test/offline seam).
-do_repair() {
-  local bad="$1" jqerr="$2" rid="${ID}-repair" pf spawn_rc=0 rout rc2
-  pf="$RUNDIR/$rid.prompt-src.txt"
-  printf '%s' "$bad" > "$RUNDIR/$ID.invalid.txt"
-  err "schema validation failed (--repair): saved $RUNDIR/$ID.invalid.txt; respawning lane $rid"
-  printf 'Your previous FINAL REPLY failed JSON validation. Error: %s. Previous output: %s. Reply with ONLY the corrected JSON object, nothing else.' \
-    "$jqerr" "$bad" > "$pf"
-  if [ -n "${FLEETFLOW_REPAIR_DRYRUN:-}" ]; then
-    bash "$SPAWN" --run "$RUN" --id "$rid" --brain "$BRAIN" --max-turns 3 \
-      --repo "$REPO" --prompt-file "$pf" --dry-run >/dev/null 2>"$RUNDIR/$rid.spawn.err" || spawn_rc=$?
-  else
-    bash "$SPAWN" --run "$RUN" --id "$rid" --brain "$BRAIN" --max-turns 3 \
-      --repo "$REPO" --prompt-file "$pf" >/dev/null 2>"$RUNDIR/$rid.spawn.err" || spawn_rc=$?
-  fi
-  case "$spawn_rc" in 0|3) ;; *) err "repair spawn failed (rc=$spawn_rc; see $RUNDIR/$rid.spawn.err)"; exit 10 ;; esac
-  # gate the repair lane WITHOUT --repair (one attempt); its stdout is the repaired text
-  rout="$(bash "$COLLECT" --run "$RUN" --id "$rid" --repo "$REPO" --schema 2>"$RUNDIR/$rid.collect.err")"; rc2=$?
-  if [ "$rc2" = 0 ]; then printf '%s\n' "$rout"; exit 0; fi
-  err "repair lane also failed validation (see $RUNDIR/$rid.collect.err)"; exit 10
-}
-
-# --- 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" = "grok" ]; then
-  # grok's headless envelope is {text, stopReason, sessionId, ...} with NO
-  # is_error field. rc was already gated by ff-spawn (a failed run exits nonzero
-  # and writes stderr), so the content gate here is: envelope parses AND produced
-  # non-empty output. stopReason is informational (EndTurn = a clean turn); we do
-  # NOT hard-gate on it, because an agentic tool-turn can end on other reasons.
-  ART="$RUNDIR/$ID.result.json"
-  [ -s "$ART" ] || { err "grok result missing/empty: $ART"; exit 3; }
-  jq -e 'type=="object"' "$ART" >/dev/null 2>&1 || { err "grok envelope unparseable: $ART"; exit 10; }
-  if [ "$SCHEMA" = 1 ]; then
-    # --json-schema makes grok emit an already-parsed .structuredOutput - prefer it
-    # over re-parsing .text (grok did the validation server-side).
-    SO="$(jq -c 'if has("structuredOutput") then .structuredOutput else empty end' "$ART" 2>/dev/null)"
-    if [ -n "$SO" ]; then printf '%s\n' "$SO"; exit 0; fi
-    # fallback: no structuredOutput (schema not passed natively) - .text may hold fenced JSON
-    CLEAN="$(jq -r '.text // empty' "$ART" | strip_fences)"
-    if ! printf '%s' "$CLEAN" | json_ok; then
-      [ "$REPAIR" = 1 ] && do_repair "$CLEAN" "$JQERR"
-      err "grok final output not valid JSON (schema lane): $JQERR"; exit 10
-    fi
-    printf '%s\n' "$CLEAN"
-    exit 0
-  fi
-  TEXT="$(jq -r '.text // empty' "$ART")"
-  [ -n "$TEXT" ] || { err "grok produced empty text"; exit 10; }
-  printf '%s\n' "$TEXT"
-  exit 0
-fi
-
-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
-    CLEAN="$(strip_fences < "$ART")"
-    if ! printf '%s' "$CLEAN" | json_ok; then
-      [ "$REPAIR" = 1 ] && do_repair "$CLEAN" "$JQERR"
-      err "final message is not valid JSON (schema lane): $JQERR"; exit 10
-    fi
-    printf '%s\n' "$CLEAN"
-    exit 0
-  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
-  CLEAN="$(printf '%s' "$TEXT" | strip_fences)"
-  if ! printf '%s' "$CLEAN" | json_ok; then
-    [ "$REPAIR" = 1 ] && do_repair "$CLEAN" "$JQERR"
-    err "final text is not valid JSON (schema lane): $JQERR"; exit 10
-  fi
-  printf '%s\n' "$CLEAN"
-  exit 0
-fi
-printf '%s\n' "$TEXT"
-exit 0

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

@@ -1,226 +0,0 @@
-#!/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 - plus the codex windows.sandbox
-#   tripwire (config-only, no sandbox provisioned) - 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
-. "$(dirname "${BASH_SOURCE[0]}")/_env.sh"
-
-FF_VERSION="1.1.0"
-
-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
-GROK="${FLEETFLOW_GROK_BIN:-grok}"
-if command -v "$GROK" >/dev/null; then say "bin-grok" ok "$("$GROK" --version 2>/dev/null | head -1)"; else say "bin-grok" advisory "missing - grok brain unavailable"; fi
-PIBIN="${FLEETFLOW_PI_BIN:-pi}"
-# -f fallback matches ff-spawn: bash runs a .cmd shim but command -v rejects it
-if command -v "$PIBIN" >/dev/null || [ -f "$PIBIN" ]; then say "bin-pi" ok "pi $("$PIBIN" --version 2>/dev/null | head -1)"; else say "bin-pi" advisory "missing - pi brain unavailable (set FLEETFLOW_PI_BIN)"; 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
-
-# --- which windows.sandbox mode will codex lanes actually get? -----------------
-# Config read only (no execution) - the behavioural tripwire is in --live below.
-case "$(uname -s 2>/dev/null)" in
-  MINGW*|MSYS*|CYGWIN*)
-    WSB_EFF="${FLEETFLOW_CODEX_WINDOWS_SANDBOX-unelevated}"
-    WSB_HOST="$(sed -n '/^\[windows\]/,/^\[/p' "$HOME/.codex/config.toml" 2>/dev/null \
-                | sed -n 's/^[[:space:]]*sandbox[[:space:]]*=[[:space:]]*"\{0,1\}\([a-z]*\)"\{0,1\}.*/\1/p' | head -1)"
-    if [ -z "$WSB_EFF" ]; then
-      say "codex-winsandbox-mode" advisory "override disabled - lanes defer to config.toml (${WSB_HOST:-unset}); elevated there = headless hang"
-    elif [ "$WSB_EFF" = "elevated" ]; then
-      say "codex-winsandbox-mode" advisory "FLEETFLOW_CODEX_WINDOWS_SANDBOX=elevated re-arms the headless UAC trap"
-    else
-      say "codex-winsandbox-mode" ok "lanes pinned $WSB_EFF (host config: ${WSB_HOST:-unset})"
-    fi
-    ;;
-esac
-
-for s in ff-spawn.sh ff-collect.sh ff-status.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; }
-
-# --- install-sync: repo copy vs the installed copy at $HOME/.claude/skills ---
-# version-skew guard. Only compares when an installed copy exists AND is a
-# different directory from the one being run (running from the install itself
-# trivially matches). Drift is advisory, never a hard fail.
-INST="$HOME/.claude/skills/fleetflow/scripts"
-if [ -d "$INST" ]; then
-  INST_ABS="$(cd "$INST" 2>/dev/null && pwd)"
-  if [ -n "$INST_ABS" ] && [ "$INST_ABS" != "$HERE" ]; then
-    DIFF=0
-    for s in ff-spawn.sh ff-collect.sh ff-status.sh ff-doctor.sh ff-run.sh ff-clean.sh; do
-      [ -f "$HERE/$s" ] || continue            # not shipped here; skip
-      if [ ! -f "$INST/$s" ]; then DIFF=1; break; fi
-      h1="$(sha256sum "$HERE/$s" | cut -d' ' -f1)"
-      h2="$(sha256sum "$INST/$s" | cut -d' ' -f1)"
-      [ "$h1" = "$h2" ] || { DIFF=1; break; }
-    done
-    if [ "$DIFF" = 0 ]; then say "install-sync" ok "repo and installed copies match"
-    else say "install-sync" advisory "repo and installed copies differ - re-run install"; fi
-  else
-    say "install-sync" ok "running from the installed copy"
-  fi
-else
-  say "install-sync" advisory "no installed copy at $INST"
-fi
-
-[ "$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
-
-# --- codex windows.sandbox tripwire (guards the 2026-07-27 elevation hang) -----
-# ff-spawn pins `-c windows.sandbox=unelevated` so a headless lane never waits on
-# a UAC prompt nobody can approve. That guard fails SILENTLY if codex renames or
-# drops the key: `-c` accepts unknown dotted keys without complaint (verified -
-# `-c nosuch.key=x` exits 0), so the flag would degrade to an inert no-op and
-# lanes would hang again with no signal. We therefore probe the OPPOSITE: feed a
-# deliberately invalid value and require codex to reject it, naming the key.
-# Rejection proves the key still exists and is still typed; silent acceptance
-# means the guard has gone inert and this check is the only thing that will say so.
-#
-# `codex debug prompt-input` is the probe carrier ON PURPOSE: it only loads and
-# validates config (~1.2s, no network, no model call, no API tokens) and - unlike
-# `codex sandbox` - it does NOT provision a Windows sandbox. Sandbox provisioning
-# is machine-global (~/.codex/.sandbox*) and is the very thing that can trigger
-# the elevation helper, so a preflight check must never invoke it. Do not
-# "simplify" this to a sandbox or exec call.
-case "$(uname -s 2>/dev/null)" in
-  MINGW*|MSYS*|CYGWIN*)
-    if command -v codex >/dev/null; then
-      WSB_ERR="$(timeout 60 codex debug prompt-input -c windows.sandbox=ff-doctor-invalid 2>&1 >/dev/null)"
-      WSB_RC=$?
-      if [ "$WSB_RC" = 124 ]; then
-        say "codex-winsandbox" unreachable "config probe timed out"; UNREACH=1
-      elif [ "$WSB_RC" != 0 ] && printf '%s' "$WSB_ERR" | grep -q 'windows\.sandbox'; then
-        say "codex-winsandbox" ok "key recognised - elevation guard live"
-      elif [ "$WSB_RC" != 0 ]; then
-        say "codex-winsandbox" advisory "probe failed for another reason: $(printf '%s' "$WSB_ERR" | head -1)"
-      else
-        say "codex-winsandbox" fail "codex accepts an invalid windows.sandbox - ff-spawn's elevation guard is INERT (key renamed/removed?)"
-        FAIL=1
-      fi
-    fi
-    ;;
-esac
-
-# grok auth is the GROK_DEPLOYMENT_KEY env var (no login-status subcommand exists,
-# and OAuth on some accounts lacks chat entitlement). We probe key PRESENCE, not
-# validity - a real chat call would burn quota. Only when the binary is installed.
-if command -v "$GROK" >/dev/null; then
-  if [ -n "${GROK_DEPLOYMENT_KEY:-}" ]; then
-    say "grok-auth" ok "GROK_DEPLOYMENT_KEY set"
-  else
-    say "grok-auth" unreachable "GROK_DEPLOYMENT_KEY not set (deployment key required)"; UNREACH=1
-  fi
-fi
-
-# pi lanes run with an ISOLATED PI_CODING_AGENT_DIR (ff-spawn), so the host's
-# ~/.pi/agent/auth.json never applies - the provider's API key env var is the
-# lane's ONLY auth. Probe key PRESENCE for the configured provider, not
-# validity (a real call would burn quota, same doctrine as grok-auth).
-if command -v "$PIBIN" >/dev/null || [ -f "$PIBIN" ]; then
-  PIPROV="${FLEETFLOW_PI_PROVIDER:-}"
-  if [ -z "$PIPROV" ]; then
-    say "pi-auth" advisory "FLEETFLOW_PI_PROVIDER not set - pi lanes would use pi's default provider with no key check"
-  else
-    case "$PIPROV" in
-      anthropic)  PIKEY="ANTHROPIC_API_KEY" ;;
-      openai)     PIKEY="OPENAI_API_KEY" ;;
-      google)     PIKEY="GEMINI_API_KEY" ;;
-      zai)        PIKEY="ZAI_API_KEY" ;;
-      groq)       PIKEY="GROQ_API_KEY" ;;
-      xai)        PIKEY="XAI_API_KEY" ;;
-      deepseek)   PIKEY="DEEPSEEK_API_KEY" ;;
-      mistral)    PIKEY="MISTRAL_API_KEY" ;;
-      openrouter) PIKEY="OPENROUTER_API_KEY" ;;
-      cerebras)   PIKEY="CEREBRAS_API_KEY" ;;
-      *)          PIKEY="" ;;
-    esac
-    if [ -z "$PIKEY" ]; then
-      say "pi-auth" advisory "no env-key mapping for provider '$PIPROV' - lane auth unverified"
-    elif [ -n "$(eval "printf '%s' \"\${$PIKEY:-}\"")" ]; then
-      say "pi-auth" ok "$PIKEY set (provider $PIPROV)"
-    else
-      say "pi-auth" unreachable "$PIKEY not set (provider $PIPROV; host auth.json does not reach lanes)"; UNREACH=1
-    fi
-  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

+ 0 - 184
skills/fleetflow/scripts/ff-import.sh

@@ -1,184 +0,0 @@
-#!/usr/bin/env bash
-# ff-import.sh - turn a native Claude Code Workflow run into fleetflow lanes.
-#
-# A native Workflow run dir (wf_*/) holds journal.jsonl (hash-keyed started/
-# result records keyed by agentId) plus agent-<id>.jsonl transcripts. This
-# script folds its COMPLETED agents into <repo>/.fleetflow/<run>/ as fleetflow
-# lanes so ff-collect can read them and ff-status can show them, and surfaces
-# incomplete agents (started, never finished) as respawn candidates.
-#
-#   result record  -> <id>.prompt.txt + <id>.result.json envelope + journal
-#                     started/result (brain "native", phase "imported") +
-#                     manifest packet {id, brain:"native", imported_from}
-#   started-only   -> <id>.prompt.txt only; reported INCOMPLETE (respawn via
-#                     ff-spawn --run <run> --id <id> --brain <choice> --prompt-file)
-#
-# CAVEAT: native hash keys are NOT replayable here. An imported result is a
-# terminal fact (the object the agent returned); the native script's control
-# flow (pipeline/barrier/loop) is not recovered. Continuing the work = spawning
-# NEW lanes (e.g. off an incomplete agent's prompt), not resuming the native run.
-#
-# stdout: one TSV line per agent: id<TAB>imported|incomplete<TAB>prompt_chars.
-# stderr: chatter. Exit: 0 ok | 2 usage/bad dir | 3 nothing to import.
-set -u
-. "$(dirname "${BASH_SOURCE[0]}")/_env.sh"
-
-FF_VERSION="1.1.0"
-
-usage() {
-  cat <<'EOF'
-Usage: ff-import.sh --wf DIR --run NAME [--repo PATH]
-
-  --wf DIR     native Claude Code Workflow run directory (wf_*/) containing
-               journal.jsonl and agent-<id>.jsonl transcripts (+ .meta.json)
-  --run NAME   fleetflow run name to create/extend under <repo>/.fleetflow/
-  --repo PATH  repo root (default: git toplevel of cwd)
-
-  Completed native agents become fleetflow lanes (prompt + result envelope +
-  journal + manifest). Incomplete agents (started, no result) get a prompt
-  file and are flagged INCOMPLETE for respawn. Native keys are NOT replayable -
-  imported results are terminal facts; the native control flow is not recovered.
-
-EXAMPLES
-  ff-import.sh --wf ~/.claude/projects/<enc>/<sess>/subagents/workflows/wf_ab12cd34-ef \
-               --run imported-currency
-  ff-import.sh --wf ./wf_ab12cd34-ef --run imp --repo /path/to/repo \
-    | awk -F'\t' '$2=="incomplete"{print "respawn:", $1}'
-EOF
-}
-
-err() { echo "ff-import: $*" >&2; }
-tsv() { printf '%s\t%s\t%s\n' "$1" "$2" "$3"; }   # id <TAB> status <TAB> chars
-
-WF="" RUN="" REPO=""
-while [ $# -gt 0 ]; do
-  case "$1" in
-    --wf) WF="${2:-}"; shift 2 ;;
-    --run) RUN="${2:-}"; shift 2 ;;
-    --repo) REPO="${2:-}"; shift 2 ;;
-    -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; }
-command -v git >/dev/null || { err "git required"; exit 2; }
-[ -n "$WF" ]  || { err "--wf required"; usage >&2; exit 2; }
-[ -n "$RUN" ] || { err "--run required"; usage >&2; exit 2; }
-echo "$RUN" | grep -qE '^[a-z0-9-]+$' || { err "invalid --run '$RUN' ([a-z0-9-]+)"; exit 2; }
-[ -d "$WF" ] || { err "--wf not a directory: $WF"; exit 2; }
-NJ="$WF/journal.jsonl"
-[ -f "$NJ" ] || { err "no journal.jsonl in $WF (not a native Workflow run?)"; 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; }
-
-RUNDIR="$REPO/.fleetflow/$RUN"
-JOURNAL="$RUNDIR/journal.jsonl"
-MANIFEST="$RUNDIR/manifest.json"
-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"
-
-# Extract the agent's ORIGINAL prompt from its transcript: the first user-role
-# message's text. content may be a string (the prompt) or an array of blocks
-# (later tool_result turns); we want the first one carrying text. "" if absent.
-extract_prompt() {  # <transcript-path>  -> echoes prompt text
-  local t="$1"
-  [ -f "$t" ] || return 1
-  jq -rs '
-    [.[] | select(.type=="user" and .message.role=="user") | .message.content
-       | if   type=="string" then .
-         elif type=="array"  then (map(select(.type=="text")) | .[0].text // empty)
-         else empty end]
-    | map(select((. // "") != ""))
-    | .[0] // ""' "$t" 2>/dev/null | tr -d '\r'
-}
-
-prompt_abs() { ( cd "$RUNDIR" && pwd ) >/dev/null 2>&1; printf '%s/%s' "$RUNDIR" "$1"; }
-
-# is this agent already imported into the fleetflow journal? (idempotent re-run)
-already_imported() {  # <id> <key>  -> rc 0 if present, 1 if not (stdout suppressed)
-  [ -f "$JOURNAL" ] || return 1
-  jq -es --arg id "$1" --arg k "$2" \
-    '[.[] | objects | select(.type=="started" and .brain=="native" and .id==$id and .key==$k)] | length > 0' \
-    "$JOURNAL" >/dev/null 2>&1
-}
-
-# upsert one packet into the manifest (idempotent by id), creating it if absent
-upsert_packet() {  # <id> <key>
-  local id="$1" key="$2" entry
-  entry="$(jq -nc --arg id "$id" --arg pf "$(prompt_abs "$id.prompt.txt")" \
-    --arg k "$key" --arg wf "$WF" \
-    '{id:$id,brain:"native",phase:"imported",prompt_file:$pf,imported_from:$wf,key:$k}')"
-  if [ ! -s "$MANIFEST" ]; then
-    jq -nc --arg run "$RUN" --arg by "ff-import/$FF_VERSION" --argjson entry "$entry" \
-      '{run:$run,base:"main",created_by:$by,phases:["imported"],packets:[$entry]}' > "$MANIFEST"
-  else
-    jq --argjson entry "$entry" --arg id "$id" \
-      '.packets = ((.packets // []) | map(select(.id != $id))) + [$entry]
-       | .phases = (((.phases // []) + ["imported"]) | unique)' \
-      "$MANIFEST" > "$MANIFEST.tmp" && mv -f "$MANIFEST.tmp" "$MANIFEST"
-  fi
-}
-
-# --- enumerate agents from the native journal ----------------------------------
-# result agents (completed) and started-only agents (incomplete), each with key.
-# NB: this Windows jq emits CRLF, so strip \r lest it cling to mid-stream values
-# when we re-split the multi-line capture with `read` (the native file itself is LF).
-RESULT_IDS="$(jq -r 'select(.type=="result") | "\(.agentId)\t\(.key)"' "$NJ" 2>/dev/null | tr -d '\r')"
-STARTED_IDS="$(jq -r 'select(.type=="started") | "\(.agentId)\t\(.key)"' "$NJ" 2>/dev/null | tr -d '\r')"
-N_STARTED="$(printf '%s\n' "$STARTED_IDS" | grep -c . || true)"
-N_RESULT="$(printf '%s\n' "$RESULT_IDS" | grep -c . || true)"
-if [ "$N_STARTED" -eq 0 ] && [ "$N_RESULT" -eq 0 ]; then
-  err "no started or result records in $NJ - nothing to import"
-  exit 3
-fi
-
-# ids that have a result -> imported; the rest of the started set -> incomplete
-HAVE_RESULT="$(printf '%s\n' "$RESULT_IDS" | awk -F'\t' '{print $1}' | sort -u)"
-
-err "importing native run $WF -> $RUNDIR"
-
-# --- completed agents ----------------------------------------------------------
-printf '%s\n' "$RESULT_IDS" | while IFS=$'\t' read -r id key; do
-  [ -n "$id" ] || continue
-  tf="$WF/agent-$id.jsonl"
-  prompt="$(extract_prompt "$tf" || true)"
-  if [ -z "$prompt" ] && [ ! -f "$tf" ]; then
-    err "WARN: $id has a result but no transcript $tf - empty prompt"
-  fi
-  printf '%s' "$prompt" > "$RUNDIR/$id.prompt.txt"
-  # fleetflow-compatible envelope: result is the native object serialized to a
-  # JSON STRING so ff-collect prints the original JSON text
-  jq -c 'select(.type=="result" and .agentId==$id) | {is_error:false, result: ((.result // {}) | tojson)}' \
-    --arg id "$id" "$NJ" | tail -1 > "$RUNDIR/$id.result.json"
-  upsert_packet "$id" "$key"
-  if ! already_imported "$id" "$key"; then
-    jq -nc --arg k "$key" --arg id "$id" --arg v "$FF_VERSION" \
-      '{type:"started",key:$k,id:$id,brain:"native",phase:"imported",v:$v}' >> "$JOURNAL"
-    jq -nc --arg k "$key" --arg id "$id" --arg a "$(prompt_abs "$id.result.json")" \
-      '{type:"result",key:$k,id:$id,brain:"native",rc:0,artifact:$a}' >> "$JOURNAL"
-    err "imported $id (result)"
-  else
-    err "imported $id (result, already present - refreshed files)"
-  fi
-  tsv "$id" imported "$(printf '%s' "$prompt" | wc -c | tr -d ' ')"
-done
-
-# --- incomplete agents (started, no result) ------------------------------------
-printf '%s\n' "$STARTED_IDS" | while IFS=$'\t' read -r id key; do
-  [ -n "$id" ] || continue
-  printf '%s\n' "$HAVE_RESULT" | grep -qxF "$id" && continue   # has a result -> skip
-  tf="$WF/agent-$id.jsonl"
-  prompt="$(extract_prompt "$tf" || true)"
-  if [ -z "$prompt" ] && [ ! -f "$tf" ]; then
-    err "WARN: incomplete $id has no transcript $tf - empty prompt"
-  fi
-  printf '%s' "$prompt" > "$RUNDIR/$id.prompt.txt"
-  err "incomplete $id (started, no result) - respawn candidate"
-  tsv "$id" incomplete "$(printf '%s' "$prompt" | wc -c | tr -d ' ')"
-done
-
-exit 0

+ 0 - 148
skills/fleetflow/scripts/ff-run.sh

@@ -1,148 +0,0 @@
-#!/usr/bin/env bash
-# ff-run.sh - whole-run replay/status for a fleetflow run.
-#
-# resume: replay every packet in <run>/manifest.json through ff-spawn, IN
-#   MANIFEST ORDER, sequential. Unchanged packets cache-hit (ff-spawn exit 3)
-#   and are reported "cached"; changed/new packets run live. A per-lane summary
-#   goes to stderr; a JSON result array goes to stdout. Exit 0 if every lane is
-#   ok or cached, 10 if any lane failed.
-# status: convenience alias for ff-status (its JSON on stdout, identical exit).
-# stdout: the JSON result list (resume) / ff-status JSON (status). stderr: chatter.
-#
-# Exit codes: 0 all ok/cached | 2 usage | 10 a lane failed
-set -u
-. "$(dirname "${BASH_SOURCE[0]}")/_env.sh"
-
-FF_VERSION="1.1.0"
-
-usage() {
-  cat <<'EOF'
-Usage: ff-run.sh resume  --run NAME [--repo PATH]
-       ff-run.sh status  --run NAME [--repo PATH]
-
-  resume --run NAME   replay every packet in <run>/manifest.json through
-                      ff-spawn, in order. Unchanged packets cache-hit ("cached"),
-                      changed/new ones run live. JSON result list on stdout.
-  status --run NAME   alias for ff-status (run status JSON on stdout).
-  --repo PATH         repo root (default: git toplevel of cwd)
-
-EXAMPLES
-  ff-run.sh resume --run currency
-  ff-run.sh resume --run currency --repo /path/to/repo | jq '.[] | select(.status!="cached")'
-  ff-run.sh status --run currency | jq '.lanes | length'
-EOF
-}
-
-err() { echo "ff-run: $*" >&2; }
-
-# main() wrapper - parse-before-execute guard (incident 2026-08-01; see the
-# matching comment in ff-spawn.sh). bash parses script files incrementally
-# during execution, so a concurrent edit or skills sync rewriting this file
-# while a multi-minute resume loop is running kills the run with a phantom
-# syntax error. main() forces a full parse up front. Body deliberately NOT
-# re-indented. DO NOT unwrap this as a "simplification".
-main() {
-
-HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-SPAWN="$HERE/ff-spawn.sh"
-
-MODE="" RUN="" REPO=""
-[ $# -gt 0 ] || { err "a subcommand is required (resume|status)"; usage >&2; exit 2; }
-case "$1" in
-  resume) MODE="resume"; shift ;;
-  status) MODE="status"; shift ;;
-  -h|--help) usage; exit 0 ;;
-  *) err "unknown subcommand: $1"; usage >&2; exit 2 ;;
-esac
-while [ $# -gt 0 ]; do
-  case "$1" in
-    --run) RUN="${2:-}"; shift 2 ;;
-    --repo) REPO="${2:-}"; shift 2 ;;
-    -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; }
-command -v git >/dev/null || { err "git required"; exit 2; }
-[ -n "$RUN" ] || { err "--run required"; usage >&2; 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; }
-
-# status = alias for ff-status (hand off and let it own exit codes).
-# exec THROUGH bash: a direct exec needs the +x bit, which zip installs and
-# mode-dropping checkouts lose — Git Bash fakes modes, so only Linux notices.
-if [ "$MODE" = "status" ]; then
-  if [ -n "$REPO" ]; then exec bash "$HERE/ff-status.sh" --run "$RUN" --repo "$REPO"; fi
-  exec bash "$HERE/ff-status.sh" --run "$RUN"
-fi
-
-RUNDIR="$REPO/.fleetflow/$RUN"
-MANIFEST="$RUNDIR/manifest.json"
-[ -f "$MANIFEST" ] || { err "no manifest at $MANIFEST (run ff-spawn first)"; exit 2; }
-
-# Snapshot the packets ONCE, before replay. ff-spawn upserts each packet on the
-# way in (remove-then-append), which REORDERS the live manifest - so re-reading
-# .packets[$i] mid-loop would drift and revisit the same packet. The snapshot is
-# the replay contract: spawn order = the order captured here, frozen.
-PACKETS="$(jq -c '.packets' "$MANIFEST" 2>/dev/null)"
-N="$(printf '%s' "$PACKETS" | jq -r 'length' 2>/dev/null)"
-[ "${N:-0}" -gt 0 ] 2>/dev/null || { err "manifest has no packets to replay"; exit 2; }
-
-err "resume: replaying $N packet(s) from $MANIFEST (sequential)"
-err "  #   id                       brain     status"
-err "  --  -----------------------  --------  --------"
-RESULTS="[]"
-ANY_FAIL=0
-i=0
-while [ "$i" -lt "$N" ]; do
-  pid="$(printf '%s' "$PACKETS" | jq -r ".[$i].id")"
-  pbrain="$(printf '%s' "$PACKETS" | jq -r ".[$i].brain")"
-  # imported native packets are terminal facts, not replayable ("native" is not a
-  # spawnable brain - ff-spawn rejects it). ff-run resume SKIPS them; to continue
-  # from an imported result, spawn a fresh lane with a real brain. See ff-import.
-  if [ "$pbrain" = "native" ]; then
-    err "  $((i+1))   $(printf '%-23s' "$pid")  native     imported (skipped)"
-    RESULTS="$(jq -nc --argjson R "$RESULTS" --arg id "$pid" --arg s "imported" --argjson rc 0 \
-      '$R + [{id:$id,status:$s,rc:$rc}]')"
-    i=$((i+1)); continue
-  fi
-  pphase="$(printf '%s' "$PACKETS" | jq -r ".[$i].phase // \"build\"")"
-  ppf="$(printf '%s' "$PACKETS" | jq -r ".[$i].prompt_file")"
-  pwt="$(printf '%s' "$PACKETS" | jq -r ".[$i].worktree // false")"
-  pmt="$(printf '%s' "$PACKETS" | jq -r ".[$i].max_turns // 100")"
-  peff="$(printf '%s' "$PACKETS" | jq -r ".[$i].effort // \"\"")"
-  psch="$(printf '%s' "$PACKETS" | jq -r ".[$i].schema // \"\"")"
-  # worktree is a boolean string ("true"/"false"); both are non-empty, so gate on
-  # the literal value rather than ${pwt:+...} (which would always fire).
-  WT_FLAG=""; [ "$pwt" = "true" ] && WT_FLAG="1"
-
-  bash "$SPAWN" --run "$RUN" --id "$pid" --brain "$pbrain" --phase "$pphase" \
-    --prompt-file "$ppf" --max-turns "$pmt" --repo "$REPO" \
-    ${WT_FLAG:+--worktree} ${peff:+--effort "$peff"} ${psch:+--schema "$psch"} \
-    >/dev/null 2>>"$RUNDIR/$pid.resume.err"
-  rc=$?
-  case "$rc" in
-    0) status="ran" ;;
-    3) status="cached" ;;
-    *) status="failed"; ANY_FAIL=1 ;;
-  esac
-  err "  $((i+1))   $(printf '%-23s' "$pid")  $(printf '%-8s' "$pbrain")  $status${rc:+ (rc=$rc)}"
-  RESULTS="$(jq -nc --argjson R "$RESULTS" --arg id "$pid" --arg s "$status" --argjson rc "$rc" \
-    '$R + [{id:$id,status:$s,rc:$rc}]')"
-  i=$((i+1))
-done
-
-RAN="$(printf '%s' "$RESULTS" | jq -r '[.[]|select(.status=="ran")]|length')"
-CACHED="$(printf '%s' "$RESULTS" | jq -r '[.[]|select(.status=="cached")]|length')"
-FAILED="$(printf '%s' "$RESULTS" | jq -r '[.[]|select(.status=="failed")]|length')"
-err "  --"
-err "  summary: $RAN ran, $CACHED cached, $FAILED failed"
-
-printf '%s\n' "$RESULTS"
-[ "$ANY_FAIL" = 1 ] && exit 10
-exit 0
-
-}
-
-main "$@"

+ 0 - 314
skills/fleetflow/scripts/ff-serve.py

@@ -1,314 +0,0 @@
-#!/usr/bin/env python3
-"""ff-serve.py - serve the machine-wide fleetflow dashboard.
-
-One process does both jobs: it serves assets/ff-dashboard.html and it builds the
-aggregate. There is deliberately no second moving part - the predecessor of this
-service was a static file server plus a detached `ff-status --watch` writing
-status.json beside it, and its dominant failure was the watcher dying while the
-server stayed up, so the page rendered a perfectly healthy-looking grid of
-frozen numbers. Nothing on screen said anything was wrong.
-
-Rebuilds are REQUEST-DRIVEN and NON-BLOCKING:
-  * a request is answered from the last good aggregate, immediately, always;
-  * if that aggregate is older than --ttl, the request also kicks off a
-    background rebuild - so nothing runs when nobody is looking, and a rebuild
-    that dies is simply restarted by the next request (a standing watcher, by
-    contrast, stays dead until a human notices);
-  * one rebuild at a time, with a watchdog so a wedged build cannot block
-    refreshes forever.
-A cold build over 50+ runs takes minutes; every response carries `age_s`, and
-the page shows its own staleness rather than implying freshness it does not have.
-
-Exit codes: 0 clean shutdown | 2 usage | 3 no roots resolved
-"""
-from __future__ import annotations
-
-import argparse
-import importlib.util
-import json
-import os
-import sys
-import threading
-import time
-from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
-from pathlib import Path
-
-SCRIPT_DIR = Path(__file__).resolve().parent
-ASSETS = SCRIPT_DIR.parent / "assets"
-
-
-def _load_aggregator():
-    """Import ff-aggregate.py by path.
-
-    The skill's scripts are named `ff-<verb>` for consistency with its shell
-    tools, and a hyphen is not a legal Python identifier - so the aggregator
-    cannot be a plain `import`. Loading it by path keeps ONE implementation of
-    discovery and roll-up shared between the CLI and this server; duplicating it
-    here is exactly how the two would drift apart.
-    """
-    path = SCRIPT_DIR / "ff-aggregate.py"
-    spec = importlib.util.spec_from_file_location("ff_aggregate", path)
-    if spec is None or spec.loader is None:
-        raise ImportError(f"cannot load {path}")
-    mod = importlib.util.module_from_spec(spec)
-    sys.modules["ff_aggregate"] = mod
-    spec.loader.exec_module(mod)
-    return mod
-
-
-agg = _load_aggregator()
-FF_VERSION = "1.2.0"
-
-
-def err(msg: str) -> None:
-    print(f"ff-serve: {msg}", file=sys.stderr, flush=True)
-
-
-class State:
-    """Everything the request thread and the builder thread share."""
-
-    def __init__(self, roots, source, opts):
-        self.roots = roots
-        self.source = source
-        self.opts = opts
-        self.lock = threading.Lock()
-        self.doc: dict | None = None
-        self.built_at = 0.0
-        self.building_since = 0.0
-        self.builds = 0
-        self.last_error: str | None = None
-        self.cache: dict = {}
-        self._load_cache()
-
-    # The on-disk cache is what makes a service restart cheap: a finished run
-    # already read once is never read again, so only genuinely live runs cost
-    # anything after the first ever build on this machine.
-    def _cache_path(self) -> Path:
-        return agg.FF_HOME / "cache" / "aggregate-cache.json"
-
-    def _load_cache(self) -> None:
-        p = self._cache_path()
-        if p.is_file():
-            try:
-                self.cache = json.loads(p.read_text(encoding="utf-8"))
-                err(f"loaded run cache ({len(self.cache)} entries) from {p}")
-            except (OSError, json.JSONDecodeError):
-                self.cache = {}
-
-    def _save_cache(self) -> None:
-        try:
-            p = self._cache_path()
-            p.parent.mkdir(parents=True, exist_ok=True)
-            tmp = p.with_suffix(".tmp")
-            tmp.write_text(json.dumps(self.cache), encoding="utf-8")
-            os.replace(tmp, p)
-        except OSError as e:
-            err(f"cache not persisted: {e}")
-
-    def maybe_build(self) -> None:
-        """Start a rebuild if one is due and none is running. Never blocks."""
-        with self.lock:
-            now = time.time()
-            running = self.building_since > 0
-            if running and now - self.building_since < self.opts.build_timeout:
-                return
-            if running:
-                # A build that has outlived the watchdog is presumed wedged (a
-                # hung git call in some repo, a drive that went away mid-scan).
-                # Let a fresh one start rather than freezing the dashboard on
-                # whatever the last good document happened to be.
-                err(f"build watchdog: previous build exceeded "
-                    f"{self.opts.build_timeout}s - starting another")
-            if not running and self.doc and now - self.built_at < self.opts.ttl:
-                return
-            self.building_since = now
-        threading.Thread(target=self._build, daemon=True).start()
-
-    def _build(self) -> None:
-        t0 = time.time()
-        try:
-            doc, cache = agg.aggregate(
-                self.roots, self.source,
-                max_depth=self.opts.max_depth, cache=self.cache,
-                live_ttl=self.opts.live_ttl, discover_ttl=self.opts.discover_ttl,
-                timeout=self.opts.timeout, workers=self.opts.workers)
-            with self.lock:
-                self.doc = doc
-                self.cache = cache
-                self.built_at = time.time()
-                self.builds += 1
-                self.last_error = None
-            self._save_cache()
-            t = doc.get("totals", {})
-            err(f"build #{self.builds}: {t.get('runs')} runs / {t.get('live_runs')} live "
-                f"/ {doc.get('runs_refreshed')} refreshed in {int((time.time()-t0)*1000)}ms")
-        except Exception as e:  # a builder crash must never take the server down
-            with self.lock:
-                self.last_error = f"{type(e).__name__}: {e}"
-            err(f"build FAILED: {self.last_error}")
-        finally:
-            with self.lock:
-                self.building_since = 0.0
-
-    def snapshot(self) -> tuple[dict | None, dict]:
-        with self.lock:
-            meta = {
-                "served_at": int(time.time()),
-                "age_s": int(time.time() - self.built_at) if self.built_at else None,
-                "building": self.building_since > 0,
-                "builds": self.builds,
-                "ttl_s": self.opts.ttl,
-                "server_version": FF_VERSION,
-                "build_error": self.last_error,
-            }
-            return self.doc, meta
-
-
-class Handler(BaseHTTPRequestHandler):
-    state: State = None  # type: ignore[assignment]
-    protocol_version = "HTTP/1.1"
-
-    def log_message(self, format, *args):  # noqa: A002 - signature is the base class's
-        pass  # quiet by design: the page polls every 3s and would flood the log
-
-    def handle(self) -> None:
-        """Swallow client-side disconnects for the WHOLE exchange, not just the
-        write. The process manager's readiness probe opens a keep-alive socket
-        and resets it once it has what it wants, which raises inside the base
-        class's own `readline` - outside any handler code. Left alone it prints
-        a full traceback per probe: this service's log was pure connection-reset
-        noise every 10 seconds, which is how a real error goes unnoticed."""
-        try:
-            super().handle()
-        except OSError:
-            pass
-
-    def _send(self, code: int, body: bytes, ctype: str) -> None:
-        self.send_response(code)
-        self.send_header("Content-Type", ctype)
-        self.send_header("Content-Length", str(len(body)))
-        self.send_header("Cache-Control", "no-store")
-        self.end_headers()
-        try:
-            self.wfile.write(body)
-        except OSError:
-            # Every way a client can leave mid-write lands here, and none of them
-            # are this server's problem: a closed tab (BrokenPipe/ConnectionReset)
-            # or the process manager's readiness probe hanging up the moment it
-            # has its status line (ConnectionAborted / WinError 10053, which is
-            # what Windows raises and what filled this service's log on day one).
-            # Catching the specific subclasses missed that third case.
-            pass
-
-    def _json(self, code: int, obj: dict) -> None:
-        self._send(code, json.dumps(obj).encode("utf-8"), "application/json; charset=utf-8")
-
-    def do_GET(self) -> None:
-        path = self.path.split("?", 1)[0].rstrip("/") or "/"
-
-        if path in ("/", "/index.html"):
-            f = ASSETS / "ff-dashboard.html"
-            try:
-                self._send(200, f.read_bytes(), "text/html; charset=utf-8")
-            except OSError as e:
-                self._send(500, f"cannot read {f}: {e}".encode(), "text/plain; charset=utf-8")
-            return
-
-        if path == "/api/aggregate.json":
-            self.state.maybe_build()
-            doc, meta = self.state.snapshot()
-            if doc is None:
-                # 200, not an error status: "building" is a normal state the page
-                # renders as a message. A 5xx here would be indistinguishable to
-                # the page from the server being gone, which is the one thing it
-                # must be able to tell apart.
-                self._json(200, {"schema": "fleetflow/aggregate/1", "runs": [], "history": [],
-                                 "errors": [], "totals": {}, "roots": [str(r) for r in self.state.roots],
-                                 "generated_at": int(time.time()),
-                                 "error": "building the first aggregate — a cold scan of every "
-                                          "root can take a few minutes; this page will fill in",
-                                 **meta})
-                return
-            self._json(200, {**doc, **meta})
-            return
-
-        if path == "/api/refresh":
-            with self.state.lock:
-                self.state.built_at = 0.0  # force the next maybe_build
-            self.state.maybe_build()
-            self._json(200, {"ok": True, "forced": True})
-            return
-
-        if path in ("/api/health", "/health"):
-            doc, meta = self.state.snapshot()
-            self._json(200, {"ok": True, "has_aggregate": doc is not None,
-                             "runs": (doc or {}).get("totals", {}).get("runs"), **meta})
-            return
-
-        self._send(404, b"not found", "text/plain; charset=utf-8")
-
-
-def main(argv: list[str]) -> int:
-    ap = argparse.ArgumentParser(
-        prog="ff-serve.py", add_help=False,
-        description="Serve the machine-wide fleetflow dashboard and build its aggregate.",
-        epilog="""ROUTES
-  /                     the dashboard (assets/ff-dashboard.html)
-  /api/aggregate.json   last good aggregate + freshness meta; triggers a
-                        background rebuild when older than --ttl
-  /api/refresh          force a rebuild on the next poll
-  /api/health           liveness + whether an aggregate exists yet
-
-EXAMPLES
-  ff-serve.py --port 8161
-  ff-serve.py --port 8161 --root X:/Forma --root X:/Roam
-  curl -s localhost:8161/api/health | jq .
-""",
-        formatter_class=argparse.RawDescriptionHelpFormatter)
-    ap.add_argument("--port", type=int, default=8161)
-    ap.add_argument("--bind", default="127.0.0.1")
-    ap.add_argument("--root", action="append", default=[], metavar="PATH")
-    ap.add_argument("--ttl", type=int, default=10, metavar="SECONDS",
-                    help="max aggregate age before a request triggers a rebuild")
-    ap.add_argument("--live-ttl", type=int, default=15, metavar="SECONDS")
-    ap.add_argument("--discover-ttl", type=int, default=120, metavar="SECONDS")
-    ap.add_argument("--max-depth", type=int, default=6)
-    ap.add_argument("--timeout", type=int, default=60, metavar="SECONDS")
-    ap.add_argument("--build-timeout", type=int, default=600, metavar="SECONDS")
-    ap.add_argument("--workers", type=int, default=12)
-    ap.add_argument("-h", "--help", action="help")
-    try:
-        a = ap.parse_args(argv)
-    except SystemExit as e:
-        return 2 if e.code not in (0, None) else 0
-
-    roots, source = agg.resolve_roots(a.root)
-    if not roots:
-        err("no roots resolved - pass --root, set $FLEETFLOW_ROOTS, "
-            "or run: ff-aggregate.py --init-roots <PATH>...")
-        return 3
-
-    state = State(roots, source, a)
-    Handler.state = state
-    err(f"fleetflow {FF_VERSION} - roots from {source}: "
-        f"{', '.join(str(r) for r in roots)}")
-
-    # Warm up straight away so the first visitor is not the one paying for a
-    # cold scan. Backgrounded: the socket must be listening immediately or the
-    # process manager's readiness probe fails while we scan the disk.
-    state.maybe_build()
-
-    srv = ThreadingHTTPServer((a.bind, a.port), Handler)
-    srv.daemon_threads = True
-    err(f"listening on http://{a.bind}:{a.port}/")
-    try:
-        srv.serve_forever()
-    except KeyboardInterrupt:
-        err("shutting down")
-    finally:
-        srv.server_close()
-    return 0
-
-
-if __name__ == "__main__":
-    sys.exit(main(sys.argv[1:]))

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

@@ -1,465 +0,0 @@
-#!/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
-. "$(dirname "${BASH_SOURCE[0]}")/_env.sh"
-
-FF_VERSION="1.1.0"
-
-usage() {
-  cat <<'EOF'
-Usage: ff-spawn.sh --run NAME --id ID --brain BRAIN --prompt-file FILE
-                   [--worktree] [--base BRANCH] [--repo PATH] [--max-turns N]
-                   [--effort low|medium|high|max] [--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 | grok | pi | sonnet | opus | haiku | fable
-  --prompt-file F  packet file (guard preamble is prepended unless --no-guard)
-  --phase NAME     progress-group label (default: build) - display only
-  --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)
-  --effort LEVEL   reasoning effort lever: low|medium|high|max (default: unset =
-                   inherit the brain's own default). GLM -> FLEET_WORKER_EFFORT;
-                   claude brains -> --settings effortLevel; codex -> model_reasoning_effort.
-                   Effort IS part of the cache key (different effort = different run).
-  --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)
-
-ENV (pi brain)
-  FLEETFLOW_PI_BIN                 pi launcher (default: pi on PATH; point at a
-                                   local install's pi.cmd, e.g. X:/Agents/Pi/pi.cmd)
-  FLEETFLOW_PI_PROVIDER            provider passed to `pi --provider` (pi's
-                                   wildcard slot: gemini, deepseek, zai, groq, ...)
-  FLEETFLOW_PI_MODEL               model passed to `pi --model`
-                                   NOTE: lanes get an ISOLATED PI_CODING_AGENT_DIR,
-                                   so ~/.pi/agent/auth.json does NOT apply - the
-                                   provider's API key env var is the only auth.
-
-ENV (codex brain)
-  FLEETFLOW_CODEX_MODEL            model passed to `codex exec -m`
-  FLEETFLOW_CODEX_WINDOWS_SANDBOX  windows.sandbox override, Windows hosts only
-                                   (default: unelevated - `elevated` needs a UAC
-                                   approval no headless lane can give). Set empty
-                                   to pass nothing and defer to ~/.codex/config.toml.
-
-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 --effort high \
-              --prompt-file packets/refute.txt --schema verdict.schema.json
-  ff-spawn.sh --run audit --id judge --brain opus --effort max --prompt-file packets/judge.txt
-EOF
-}
-
-err() { echo "ff-spawn: $*" >&2; }
-
-# main() wrapper - parse-before-execute guard (incident 2026-08-01, run
-# atdw-sync, lane verify-cli-2). bash parses script files INCREMENTALLY during
-# execution: when this file was rewritten mid-run (a concurrent session edit +
-# a repo->global skills sync), the resuming parser read shifted bytes and a
-# 5-minute lane died with a phantom "line 457: syntax error" AFTER its worker
-# succeeded - in a 341-line file, with `bash -n` passing. Wrapping the whole
-# procedural body in main() forces bash to parse the entire file up front, so
-# a mid-run file replacement can no longer kill a running lane.
-# Body is deliberately NOT re-indented (keeps the diff/blame readable).
-# DO NOT unwrap this as a "simplification".
-main() {
-
-RUN="" ID="" BRAIN="" PROMPT_FILE="" WORKTREE=0 BASE="main" REPO=""
-MAX_TURNS=100 SCHEMA="" GUARD=1 FORCE=0 DRYRUN=0 PHASE="build" EFFORT=""
-while [ $# -gt 0 ]; do
-  case "$1" in
-    --run) RUN="${2:-}"; shift 2 ;;
-    --phase) PHASE="${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 ;;
-    --effort) EFFORT="${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|grok|pi|sonnet|opus|haiku|fable) ;; *) err "invalid --brain '$BRAIN'"; exit 2 ;; esac
-case "$EFFORT" in ""|low|medium|high|max) ;; *) err "invalid --effort '$EFFORT' (low|medium|high|max)"; exit 2 ;; esac
-
-# --- Windows/Codex elevation trap (incident 2026-07-27, run bkv2p2) ------------
-# codex-cli's `elevated` Windows sandbox mode provisions its AppContainer through
-# a setup helper launched via ShellExecuteExW - i.e. a UAC prompt. In a HEADLESS
-# fleet there is no approver, so Windows cancels the launch (error 1223 =
-# ERROR_CANCELLED, surfaced as `orchestrator_helper_launch_canceled`) and the
-# lane HANGS instead of failing fast: two lanes sat at state=running for 2.7h
-# with dead event streams. The provisioning cache (~/.codex/.sandbox*) is why
-# earlier lanes in the same session survive - any invalidation, or a race between
-# concurrently-provisioning lanes, re-triggers the elevation request.
-# Fleet lanes therefore pin `unelevated` PER INVOCATION. Deliberately NOT a write
-# to the user's ~/.codex/config.toml: interactive Codex keeps whatever mode they
-# chose. Escape hatches: FLEETFLOW_CODEX_WINDOWS_SANDBOX=elevated to opt back in,
-# or ="" (empty) to pass nothing and defer to the global config.
-# DO NOT remove this flag as "redundant" - it is the whole fix.
-CODEX_WINSANDBOX=""
-if [ "$BRAIN" = "codex" ]; then
-  case "$(uname -s 2>/dev/null)" in
-    MINGW*|MSYS*|CYGWIN*) CODEX_WINSANDBOX="${FLEETFLOW_CODEX_WINDOWS_SANDBOX-unelevated}" ;;
-  esac
-  # the binary accepts exactly these two values for windows.sandbox
-  case "$CODEX_WINSANDBOX" in
-    ""|elevated|unelevated) ;;
-    *) err "invalid FLEETFLOW_CODEX_WINDOWS_SANDBOX '$CODEX_WINSANDBOX' (elevated|unelevated)"; exit 2 ;;
-  esac
-fi
-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"
-# codex and grok take a native structured-output flag (--output-schema /
-# --json-schema), so their schema is passed out-of-band, not appended to the
-# prompt. Every other brain gets the schema embedded and validated at collect.
-if [ -n "$SCHEMA" ] && [ "$BRAIN" != "codex" ] && [ "$BRAIN" != "grok" ]; 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) --------------
-# effort is part of the key (different effort = a different run), per Wave 1.
-# The env-selected model is part of the key too (added 2026-08-01): for codex/
-# grok/pi the model comes from FLEETFLOW_* env, not the prompt, so without it
-# two pi lanes on DIFFERENT providers running the same packet collided into one
-# cache entry (found benching pi across google/openai/zai). Anthropic brains
-# don't need it - their model IS the brain name, already hashed. NB: this
-# invalidates pre-2026-08 journal keys for codex/grok lanes (they gain the
-# "|model=" suffix) - old runs replay live once, then re-cache.
-KEY_MODEL=""
-case "$BRAIN" in
-  codex) KEY_MODEL="${FLEETFLOW_CODEX_MODEL:-}" ;;
-  grok)  KEY_MODEL="${FLEETFLOW_GROK_MODEL:-}" ;;
-  pi)    KEY_MODEL="${FLEETFLOW_PI_PROVIDER:-}/${FLEETFLOW_PI_MODEL:-}" ;;
-esac
-OPTS="turns=$MAX_TURNS|wt=$WORKTREE|schema=$( [ -n "$SCHEMA" ] && basename "$SCHEMA" )|effort=$EFFORT|model=$KEY_MODEL"
-KEY="v2:$( { printf '%s\n' "$BRAIN"; cat "$SENT"; printf '%s' "$OPTS"; } | sha256sum | cut -d' ' -f1)"
-JOURNAL="$RUNDIR/journal.jsonl"
-
-# --- run manifest (orchestrator-side packet metadata; ff-run replays it) ----
-# Created on first spawn; each spawn upserts its packet by id (idempotent).
-MANIFEST="$RUNDIR/manifest.json"
-prompt_abs() {
-  local d
-  d="$(cd "$(dirname "$PROMPT_FILE")" 2>/dev/null && pwd)" || { printf '%s' "$PROMPT_FILE"; return; }
-  printf '%s/%s' "$d" "$(basename "$PROMPT_FILE")"
-}
-WT_JSON="false"; [ "$WORKTREE" = 1 ] && WT_JSON="true"
-MENTRY="$(jq -nc --arg id "$ID" --arg b "$BRAIN" --arg p "$PHASE" --arg pf "$(prompt_abs)" \
-  --argjson wt "$WT_JSON" --argjson mt "$MAX_TURNS" --arg e "$EFFORT" --arg s "${SCHEMA:-}" --arg k "$KEY" \
-  '{id:$id,brain:$b,phase:$p,prompt_file:$pf,worktree:$wt,max_turns:$mt,effort:$e,schema:$s,key:$k}')"
-if [ ! -s "$MANIFEST" ]; then
-  jq -nc --arg run "$RUN" --arg base "$BASE" --arg by "ff-spawn/$FF_VERSION" \
-    --argjson entry "$MENTRY" --arg phase "$PHASE" \
-    '{run:$run,base:$base,created_by:$by,phases:[$phase],packets:[$entry]}' > "$MANIFEST"
-else
-  jq --argjson entry "$MENTRY" --arg id "$ID" --arg phase "$PHASE" \
-    '.packets = ((.packets // []) | map(select(.id != $id))) + [$entry]
-     | .phases = (((.phases // []) + [$phase]) | unique)' \
-    "$MANIFEST" > "$MANIFEST.tmp" && mv -f "$MANIFEST.tmp" "$MANIFEST"
-fi
-
-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
-
-# phase is display metadata only - deliberately NOT part of the cache key.
-#
-# The exact model is journalled because for codex and grok it is otherwise
-# UNRECOVERABLE after the fact: their event streams carry no model field, so the
-# id exists only in this process's environment and dies with it. Claude-brain
-# workers self-report theirs in result.json's modelUsage, but recording it here
-# too means every lane can answer "what actually ran" the same way. Also display
-# metadata, also deliberately out of the cache key.
-SPAWN_MODEL=""
-case "$BRAIN" in
-  codex) SPAWN_MODEL="${FLEETFLOW_CODEX_MODEL:-}" ;;
-  grok)  SPAWN_MODEL="${FLEETFLOW_GROK_MODEL:-}" ;;
-  pi)    SPAWN_MODEL="${FLEETFLOW_PI_PROVIDER:+$FLEETFLOW_PI_PROVIDER/}${FLEETFLOW_PI_MODEL:-}" ;;
-  fable) SPAWN_MODEL="claude-fable-5" ;;
-  *)     SPAWN_MODEL="$BRAIN" ;;
-esac
-jq -nc --arg k "$KEY" --arg id "$ID" --arg b "$BRAIN" --arg p "$PHASE" --arg v "$FF_VERSION" \
-  --arg m "$SPAWN_MODEL" \
-  '{type:"started",key:$k,id:$id,brain:$b,phase:$p,v:$v,
-    model:(if $m=="" then null else $m end)}' >> "$JOURNAL"
-
-# --- reap anchor (2026-07-27: TaskStop left 5 orphaned codex.exe alive) --------
-# Killing the wrapper does NOT kill the worker: `codex exec` spawns codex.exe and
-# codex-code-mode-host.exe as descendants, and they survive. Journal THIS
-# process's ids so ff-clean --reap can identify that subtree afterwards - $$ is
-# ff-spawn, the worker is always below it.
-#
-# Both ids are recorded because they live in different namespaces: the PID bash
-# reports is a Cygwin id, useless to taskkill/Stop-Process, which need the
-# Windows PID. `ps -W` is the only place the two appear side by side. Windows
-# does not reparent orphans, so a dead ff-spawn's WINPID still identifies its
-# subtree - which is exactly the post-kill case reaping has to handle. `at`
-# guards against PID reuse: a process older than the spawn cannot be ours.
-REAP_WINPID=""
-case "$(uname -s 2>/dev/null)" in
-  MINGW*|MSYS*|CYGWIN*) REAP_WINPID="$(ps -W 2>/dev/null | awk -v p=$$ '$1==p {print $4; exit}')" ;;
-esac
-jq -nc --arg id "$ID" --arg b "$BRAIN" --argjson pid "$$" --arg w "${REAP_WINPID:-}" \
-  --argjson at "$(date +%s)" \
-  '{type:"proc",id:$id,brain:$b,pid:$pid,
-    winpid:(if $w=="" then null else ($w|tonumber) end),at:$at}' >> "$JOURNAL"
-
-# --- launch -------------------------------------------------------------------
-ART="$RUNDIR/$ID.result.json"
-ERRF="$RUNDIR/$ID.err"
-RC=0
-
-# cache/tmp redirect: worker/pytest/uv litter lands OUTSIDE repo + lanes
-# (a codex sandbox once left AppContainer-ACL'd pytest dirs inside a lane that
-# resisted unelevated deletion and blocked a repo move).
-CACHE_DIR="${FLEETFLOW_CACHE_ROOT:-$HOME/.fleet-worker/cache}/$RUN-$ID"
-mkdir -p "$CACHE_DIR"
-CFGD="${FLEET_WORKER_CONFIG_DIR:-$HOME/.fleet-worker/cfg-ff-$ID}"
-# precompute the claude --settings effortLevel JSON so ${EFFORT:+...} stays simple
-EFF_JSON=""
-[ -n "$EFFORT" ] && EFF_JSON="$(jq -nc --arg e "$EFFORT" '{"effortLevel":$e}' 2>/dev/null)"
-
-# archive the session transcript next to the artifact (best-effort, never fatal)
-archive_transcript() {
-  local dest="$RUNDIR/$ID.transcript.jsonl" src="" sid enc
-  case "$BRAIN" in
-    glm)
-      src="$(ls -t "$CFGD"/projects/*/*.jsonl 2>/dev/null | head -1)"
-      ;;
-    pi)
-      # pi persists sessions under <PI_CODING_AGENT_DIR>/sessions/<path-slug>/<uuid>.jsonl;
-      # the lane's dir is isolated, so the newest session there is unambiguously ours
-      src="$(ls -t "$CFGD"/sessions/*/*.jsonl 2>/dev/null | head -1)"
-      ;;
-    sonnet|opus|haiku|fable)
-      sid="$(jq -r '.session_id // empty' "$ART" 2>/dev/null)"
-      if [ -n "$sid" ]; then
-        # workdir encoding: per-char [:\\/.] -> "-" (verified empirically:
-        # C:\Users\Mack -> C--Users-Mack under ~/.claude/projects)
-        enc="$(printf '%s' "$WORKDIR" | sed 's#[:\\/.]#-#g')"
-        src="$HOME/.claude/projects/$enc/$sid.jsonl"
-        [ -f "$src" ] || src="$(ls "$HOME"/.claude/projects/*/"$sid".jsonl 2>/dev/null | head -1)"
-      fi
-      ;;
-  esac
-  if [ -n "$src" ] && [ -f "$src" ]; then
-    cp -f "$src" "$dest" 2>/dev/null && err "archived transcript -> $dest" \
-      || err "transcript copy failed ($src), skipped"
-  else
-    err "transcript source not found (${src:-no session_id}), skipped (non-fatal)"
-  fi
-}
-
-if [ "$DRYRUN" = 1 ]; then
-  # the stub must match the brain's real envelope so collect can gate it:
-  # grok emits {text,stopReason,...} (no is_error); every other brain here
-  # collects via the claude-style {is_error,result} envelope.
-  case "$BRAIN" in
-    grok) jq -nc '{text:"DRYRUN",stopReason:"EndTurn"}' > "$ART" ;;
-    *)    jq -nc '{is_error:false,result:"DRYRUN"}' > "$ART" ;;
-  esac
-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; }
-      # env(1) carries the assignments: a ${VAR:+NAME=val} expansion is NOT an
-      # assignment prefix (bash parses prefixes before expansion), so without
-      # env the expanded NAME=val word execs as a command -> rc 127.
-      ( cd "$WORKDIR" && \
-        env FLEET_WORKER_CONFIG_DIR="$CFGD" \
-        UV_CACHE_DIR="$CACHE_DIR" TMPDIR="$CACHE_DIR" TMP="$CACHE_DIR" TEMP="$CACHE_DIR" \
-        ${EFFORT:+FLEET_WORKER_EFFORT="$EFFORT"} \
-        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"
-      # a worktree's git metadata lives in the MAIN repo's .git - outside the
-      # codex sandbox's writable root - so git commit fails without this carve-out
-      GITDIR=""; [ "$WORKTREE" = 1 ] && GITDIR="$(git -C "$REPO" rev-parse --absolute-git-dir)"
-      ( cd "$WORKDIR" && \
-        UV_CACHE_DIR="$CACHE_DIR" TMPDIR="$CACHE_DIR" TMP="$CACHE_DIR" TEMP="$CACHE_DIR" \
-        codex exec --full-auto --ephemeral --color never --json \
-          ${CODEX_WINSANDBOX:+-c windows.sandbox="$CODEX_WINSANDBOX"} \
-          ${GITDIR:+--add-dir "$GITDIR"} \
-          ${FLEETFLOW_CODEX_MODEL:+-m "$FLEETFLOW_CODEX_MODEL"} \
-          ${EFFORT:+-c "model_reasoning_effort=$EFFORT"} \
-          ${SCHEMA:+--output-schema "$SCHEMA"} \
-          -o "$ART" - < "$SENT" \
-      ) > "$RUNDIR/$ID.events.jsonl" 2> "$ERRF" || RC=$?
-      ;;
-    grok)
-      # xAI's Grok Build CLI - a NON-Anthropic worker (own binary + protocol,
-      # NOT a claude -p wrapper). Auth is the GROK_DEPLOYMENT_KEY env var, read
-      # from the inherited environment - never written to disk or args.
-      GROK="${FLEETFLOW_GROK_BIN:-grok}"
-      command -v "$GROK" >/dev/null || { err "grok CLI not found ($GROK)"; exit 5; }
-      # --always-approve = codex --full-auto analog: autonomous tool use, headless.
-      # --json-schema takes the schema STRING (not a path) and implies json output,
-      # surfacing an already-parsed .structuredOutput field (see ff-collect).
-      ( cd "$WORKDIR" && \
-        UV_CACHE_DIR="$CACHE_DIR" TMPDIR="$CACHE_DIR" TMP="$CACHE_DIR" TEMP="$CACHE_DIR" \
-        "$GROK" --prompt-file "$SENT" --output-format json --always-approve \
-          --max-turns "$MAX_TURNS" \
-          ${FLEETFLOW_GROK_MODEL:+-m "$FLEETFLOW_GROK_MODEL"} \
-          ${EFFORT:+--reasoning-effort "$EFFORT"} \
-          ${SCHEMA:+--json-schema "$(cat "$SCHEMA")"} \
-      ) > "$ART" 2> "$ERRF" || RC=$?
-      ;;
-    pi)
-      # earendil-works Pi (@earendil-works/pi-coding-agent) - one harness
-      # fronting 15+ providers, which makes this brain the fleet's WILDCARD
-      # slot: gemini/deepseek/zai/groq/... are env changes, not new brain code.
-      # Posture is GLM-class: NO sandbox, so the cage is the worktree lane +
-      # guard preamble; and pi has NO --max-turns equivalent, so bounds are the
-      # stall detector + orchestrator wall-clock patience. Headless pi never
-      # shows a trust prompt (docs/security.md) - no UAC-style hang risk.
-      PI="${FLEETFLOW_PI_BIN:-pi}"
-      # -f fallback: bash executes a .cmd shim fine, but its `command -v`/-x
-      # tests reject one (no exec bit on NTFS), so a path-shaped FLEETFLOW_PI_BIN
-      # is checked for existence instead.
-      command -v "$PI" >/dev/null || [ -f "$PI" ] || { err "pi CLI not found ($PI - set FLEETFLOW_PI_BIN)"; exit 5; }
-      # fleetflow effort -> pi --thinking (pi also has off/minimal below, xhigh above)
-      PI_THINK=""
-      case "$EFFORT" in low|medium|high) PI_THINK="$EFFORT" ;; max) PI_THINK="xhigh" ;; esac
-      # - prompt via STDIN, never argv: $PI is usually a .cmd shim and cmd.exe
-      #   caps the command line at ~8K chars; a guard-preamble packet exceeds it.
-      # - --no-extensions/--no-skills: discovery loads behavior OUTSIDE the
-      #   cache key's hash - same packet must mean same run.
-      # - PI_CODING_AGENT_DIR: per-lane config/session isolation (the analog of
-      #   GLM's CLAUDE_CONFIG_DIR). Consequence: ~/.pi/agent/auth.json does NOT
-      #   apply - the provider's API key env var is the lane's only auth.
-      ( cd "$WORKDIR" && \
-        env PI_CODING_AGENT_DIR="$CFGD" \
-        UV_CACHE_DIR="$CACHE_DIR" TMPDIR="$CACHE_DIR" TMP="$CACHE_DIR" TEMP="$CACHE_DIR" \
-        "$PI" -p --mode json --no-extensions --no-skills \
-          ${FLEETFLOW_PI_PROVIDER:+--provider "$FLEETFLOW_PI_PROVIDER"} \
-          ${FLEETFLOW_PI_MODEL:+--model "$FLEETFLOW_PI_MODEL"} \
-          ${PI_THINK:+--thinking "$PI_THINK"} \
-        < "$SENT" \
-      ) > "$RUNDIR/$ID.events.jsonl" 2> "$ERRF" || RC=$?
-      # Distill the event stream into a claude-style envelope so ff-collect's
-      # default gate and ff-status's finished-lane reader work UNCHANGED ("one
-      # implementation of lane state"). The stream stays on disk as the live
-      # stall signal, exactly like codex's. Field mapping (docs/session-format.md):
-      # pi usage {input,output,cacheRead,cacheWrite,cost.total} -> claude usage
-      # names; stopReason error/aborted (or no assistant reply at all) -> is_error.
-      jq -s '
-        ([.[] | select(.type=="agent_end")] | last) as $end
-        | (($end.messages // []) | map(select(.role=="assistant"))) as $as
-        | ($as | last) as $fin
-        | ($as | map(.usage // {})) as $us
-        | (($fin.content // []) | map(select(.type=="text") | .text) | join("\n")) as $text
-        | {is_error: (($fin == null) or ($fin.stopReason == "error") or ($fin.stopReason == "aborted")),
-           result: (if $fin == null then "no assistant reply (see events/.err)"
-                    else ($fin.errorMessage // $text) end),
-           usage: {input_tokens: ($us | map(.input // 0) | add // 0),
-                   output_tokens: ($us | map(.output // 0) | add // 0),
-                   cache_read_input_tokens: ($us | map(.cacheRead // 0) | add // 0),
-                   cache_creation_input_tokens: ($us | map(.cacheWrite // 0) | add // 0)},
-           num_turns: ($as | length),
-           total_cost_usd: (($us | map(.cost.total // 0) | add // 0) as $c
-                            | if $c > 0 then $c else null end),
-           modelUsage: (if ($fin.model // "") != ""
-                        then {(($fin.provider // "pi") + "/" + $fin.model):
-                              {outputTokens: ($us | map(.output // 0) | add // 0)}}
-                        else {} end)}
-      ' "$RUNDIR/$ID.events.jsonl" > "$ART" 2>> "$ERRF" || { [ "$RC" = 0 ] && RC=10; }
-      # an error-flagged envelope must fail the spawn even on a clean exit,
-      # or the journal would cache it as a replayable success
-      [ "$RC" = 0 ] && [ "$(jq -r '.is_error' "$ART" 2>/dev/null)" = "true" ] && RC=10
-      ;;
-    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" && \
-        UV_CACHE_DIR="$CACHE_DIR" TMPDIR="$CACHE_DIR" TMP="$CACHE_DIR" TEMP="$CACHE_DIR" \
-        claude -p --model "$MODEL" --output-format json --max-turns "$MAX_TURNS" \
-          --permission-mode "${FLEETFLOW_PERMISSION_MODE:-bypassPermissions}" \
-          ${EFFORT:+--settings "$EFF_JSON"} \
-        < "$SENT" \
-      ) > "$ART" 2> "$ERRF" || RC=$?
-      ;;
-  esac
-  archive_transcript
-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
-
-}
-
-main "$@"

+ 0 - 221
skills/fleetflow/scripts/ff-status.sh

@@ -1,221 +0,0 @@
-#!/usr/bin/env bash
-# ff-status.sh - emit a fleetflow run's live status as JSON (the data feed
-# behind assets/ff-monitor.html, and a machine-readable run summary on its own).
-#
-# Reads the run journal + artifacts; never modifies anything. Lane state is
-# derived from journal records (started-without-result = running), timings
-# from artifact mtimes, activity from lane commits (claude brains) or the
-# codex event stream (item.completed counts + last item). A running lane whose
-# activity signal has gone silent past FLEETFLOW_STALL_SECONDS is reported
-# `stalled` (see the stall block below for why that state has to exist).
-# stdout: the JSON document (data only). stderr: chatter.
-#
-# Exit codes: 0 ok | 2 usage | 14 stalled lane(s) (only with --exit-stalled)
-set -u
-. "$(dirname "${BASH_SOURCE[0]}")/_env.sh"
-
-FF_VERSION="1.1.0"
-
-usage() {
-  cat <<'EOF'
-Usage: ff-status.sh --run NAME [--repo PATH] [--out FILE] [--watch SECONDS]
-
-  --run NAME       run name under <repo>/.fleetflow/
-  --repo PATH      repo root (default: git toplevel of cwd)
-  --out FILE       write JSON to FILE instead of stdout
-  --watch SECONDS  loop forever, rewriting --out every SECONDS (requires --out)
-  --exit-stalled   exit 14 if any lane is stalled (still emits the JSON first).
-                   With --watch this turns the loop into a watchdog: it polls
-                   until a lane stalls, then exits 14.
-
-Lane state: running | stalled | done | failed. A lane is `stalled` once its
-LIVE stream (codex --json events, claude/glm session transcript) has been silent
-longer than FLEETFLOW_STALL_SECONDS (default 600). `last_activity_s` is reported
-for every lane; `live_signal` says whether the lane has a stream that could
-substantiate a stall at all - where it is false, `stalled` is always false and
-means "cannot tell", not "healthy".
-
-ENV
-  FLEETFLOW_STALL_SECONDS  silence before a running lane reads stalled (600)
-
-EXAMPLES
-  ff-status.sh --run currency | jq '.lanes[] | {id, state, elapsed_s}'
-  ff-status.sh --run currency | jq '.lanes[] | select(.stalled) | {id, last_activity_s}'
-  ff-status.sh --run currency --out .fleetflow/currency/status.json --watch 3
-  ff-status.sh --run currency --exit-stalled >/dev/null || echo "a lane is wedged"
-EOF
-}
-
-err() { echo "ff-status: $*" >&2; }
-
-RUN="" REPO="" OUT="" WATCH="" EXIT_STALLED=0
-while [ $# -gt 0 ]; do
-  case "$1" in
-    --run) RUN="${2:-}"; shift 2 ;;
-    --repo) REPO="${2:-}"; shift 2 ;;
-    --out) OUT="${2:-}"; shift 2 ;;
-    --watch) WATCH="${2:-}"; shift 2 ;;
-    --exit-stalled) EXIT_STALLED=1; shift ;;
-    -h|--help) usage; exit 0 ;;
-    *) err "unknown argument: $1"; usage >&2; exit 2 ;;
-  esac
-done
-[ -n "$RUN" ] || { err "--run required"; usage >&2; exit 2; }
-command -v jq >/dev/null || { err "jq required"; exit 2; }
-[ -z "$WATCH" ] || [ -n "$OUT" ] || { err "--watch requires --out"; exit 2; }
-[ -n "$REPO" ] || REPO="$(git rev-parse --show-toplevel 2>/dev/null)" || true
-RUNDIR="$REPO/.fleetflow/$RUN"
-[ -f "$RUNDIR/journal.jsonl" ] || { err "no journal at $RUNDIR"; exit 2; }
-STALL_S="${FLEETFLOW_STALL_SECONDS:-600}"
-echo "$STALL_S" | grep -qE '^[0-9]+$' || { err "FLEETFLOW_STALL_SECONDS must be an integer"; exit 2; }
-
-mtime() { stat -c %Y "$1" 2>/dev/null || echo 0; }
-
-emit() {
-  local now lanes id brain state started finished elapsed art commits last_c tools activity tokens etail rc
-  local T enc f m last_act idle stalled live
-  # NOT local: --exit-stalled reads it after emit returns, and in --watch mode it
-  # must reset every tick so a lane that resumes writing clears the verdict.
-  STALL_ANY=0
-  now=$(date +%s)
-  lanes="[]"
-  for id in $(jq -r 'select(.type=="started") | .id' "$RUNDIR/journal.jsonl" | awk '!seen[$0]++'); do
-    brain="$(jq -r --arg id "$id" 'select(.id==$id) | .brain' "$RUNDIR/journal.jsonl" | head -1)"
-    phase="$(jq -r --arg id "$id" 'select(.type=="started" and .id==$id) | .phase // "build"' "$RUNDIR/journal.jsonl" | head -1)"
-    # state derives from the LAST STATE-BEARING journal record for this id: a
-    # respawn appends a fresh "started" AFTER an old "result", which means the
-    # lane is running again. last-result-wins (tail -1 of result records) would
-    # wrongly show done/failed.
-    # The started/result filter is load-bearing: ff-spawn also appends "proc"
-    # records (reap anchors), and any future record type would otherwise become
-    # the "last" one and silently break state derivation for that lane.
-    last_type="$(jq -r --arg id "$id" 'select(.id==$id and (.type=="started" or .type=="result")) | .type' "$RUNDIR/journal.jsonl" | tail -1)"
-    rc="$(jq -r --arg id "$id" 'select(.type=="result" and .id==$id) | .rc' "$RUNDIR/journal.jsonl" | tail -1)"
-    art="$(jq -r --arg id "$id" 'select(.type=="result" and .id==$id) | .artifact' "$RUNDIR/journal.jsonl" | tail -1)"
-    if [ "$last_type" = "started" ]; then state="running"; finished=0
-    elif [ -z "$rc" ]; then state="running"; finished=0
-    elif [ "$rc" = "0" ]; then state="done"; finished=$(mtime "$art")
-    else state="failed"; finished=$(mtime "$art"); fi
-    started=$(mtime "$RUNDIR/$id.prompt.txt")
-    if [ "$finished" -gt 0 ]; then elapsed=$((finished - started)); else elapsed=$((now - started)); fi
-    [ "$elapsed" -ge 0 ] || elapsed=0
-
-    commits=0; last_c=""
-    if [ -d "$RUNDIR/wt-$id" ]; then
-      commits="$(git -C "$RUNDIR/wt-$id" rev-list --count "main..HEAD" 2>/dev/null || echo 0)"
-      last_c="$(git -C "$RUNDIR/wt-$id" log -1 --format=%s "main..HEAD" -- 2>/dev/null | head -c 90)"
-    fi
-
-    tools=0; activity=""; tokens=0; T=""
-    if [ "$brain" = "codex" ] && [ -f "$RUNDIR/$id.events.jsonl" ]; then
-      tools="$(jq -r 'select(.type=="item.completed" and .item.type=="command_execution") | 1' "$RUNDIR/$id.events.jsonl" 2>/dev/null | wc -l | tr -d ' ')"
-      activity="$(jq -r 'select(.type=="item.completed") | .item | (.type + ": " + ((.command // .text // "") | gsub("\n";" ") | .[0:70]))' "$RUNDIR/$id.events.jsonl" 2>/dev/null | tail -1)"
-      tokens="$(jq -r 'select(.usage != null) | .usage.total_tokens // (.usage.input_tokens + .usage.output_tokens) // 0' "$RUNDIR/$id.events.jsonl" 2>/dev/null | tail -1)"
-      [ -n "$tokens" ] || tokens=0
-    elif [ "$state" != "running" ] && [ -f "$RUNDIR/$id.result.json" ]; then
-      tokens="$(jq -r '.usage.output_tokens // 0' "$RUNDIR/$id.result.json" 2>/dev/null | head -1)"
-      tools="$(jq -r '.num_turns // 0' "$RUNDIR/$id.result.json" 2>/dev/null | head -1)"
-      [ -n "$tokens" ] || tokens=0; [ -n "$tools" ] || tools=0
-    elif [ "$state" = "running" ]; then
-      # claude -p persists its session transcript as it runs - the only live
-      # signal a claude-brain lane emits. GLM workers get an isolated config dir
-      # (fleet-worker), so theirs is easy to find; Anthropic-brain workers use
-      # host auth, so theirs lands under ~/.claude/projects/<encoded-workdir>/.
-      T="$(ls -t "${FLEETFLOW_CFG_BASE:-$HOME/.fleet-worker}/cfg-ff-$id/projects"/*/*.jsonl 2>/dev/null | head -1)"
-      if [ -z "$T" ] && [ -d "$RUNDIR/wt-$id" ]; then
-        # Worktree lanes ONLY. Their workdir is unique to the lane, so the newest
-        # transcript in its project dir is unambiguously this lane's. A
-        # non-worktree lane shares the repo dir with its siblings AND with the
-        # orchestrator's own session - guessing there would attribute someone
-        # else's activity to this lane, which is worse than reporting none.
-        # Encoding is ff-spawn's archive_transcript() rule: [:\/.] -> "-" per char.
-        enc="$(printf '%s' "$RUNDIR/wt-$id" | sed 's#[:\\/.]#-#g')"
-        T="$(ls -t "$HOME/.claude/projects/$enc"/*.jsonl 2>/dev/null | head -1)"
-      fi
-      if [ -n "$T" ]; then
-        tools="$(grep -c '"type":"tool_use"' "$T" 2>/dev/null | tr -d ' ')"
-        last_tool="$(grep -o '"name":"[A-Za-z_]*"' "$T" 2>/dev/null | tail -1 | cut -d'"' -f4)"
-        [ -z "$last_tool" ] || activity="live: $last_tool"
-        tokens="$(grep -o '"output_tokens":[0-9]*' "$T" 2>/dev/null | awk -F: '{s+=$2} END {print s+0}')"
-      fi
-    fi
-    [ -n "$activity" ] || activity="${last_c:-working}"
-    etail="$(grep -v '^\s*$' "$RUNDIR/$id.err" 2>/dev/null | tail -1 | head -c 160)"
-
-    # --- stall detection (incident 2026-07-27, run bkv2p2) --------------------
-    # elapsed_s cannot distinguish a working lane from a wedged one: a codex lane
-    # blocked on an un-approvable UAC prompt (see ff-spawn's windows.sandbox
-    # guard) keeps `state: running` with a climbing clock forever - two lanes hid
-    # that way for 2.7h. The ONE signal that separates them from outside the
-    # process is whether the lane is still WRITING.
-    #
-    # Two tiers, and the split is load-bearing. A LIVE STREAM is a file the brain
-    # appends to WHILE it works: codex's --json event stream, or a claude/glm
-    # session transcript. Only those can substantiate a stall. The artifact and
-    # stderr are created by the shell's redirect at LAUNCH and then sit untouched
-    # until exit, so on their own they prove nothing - counting them as evidence
-    # flagged every healthy 10-minute sonnet lane as stalled, and grok under
-    # --output-format json (buffers to exit) has no live stream at all. They still
-    # count toward last_activity_s, since a write there IS real activity, but
-    # never toward the verdict.
-    #
-    # NEVER CLAIM A STALL YOU CANNOT SUBSTANTIATE: no live stream, no verdict.
-    # `live_signal` reports which lanes the detector actually covers, so a
-    # `stalled: false` it cannot back up is never read as a clean bill of health.
-    last_act=0; live=false
-    for f in "$RUNDIR/$id.events.jsonl" "$T"; do
-      [ -n "$f" ] && [ -f "$f" ] || continue
-      live=true
-      m="$(mtime "$f")"; [ "$m" -gt "$last_act" ] && last_act="$m"
-    done
-    for f in "$RUNDIR/$id.last.txt" "$RUNDIR/$id.result.json" "$RUNDIR/$id.err"; do
-      [ -f "$f" ] || continue
-      m="$(mtime "$f")"; [ "$m" -gt "$last_act" ] && last_act="$m"
-    done
-    # nothing on disk yet (just spawned): measure silence from the lane's start
-    [ "$last_act" -gt 0 ] || last_act=$started
-    idle=$((now - last_act)); [ "$idle" -ge 0 ] || idle=0
-    stalled=false
-    if [ "$state" = "running" ] && [ "$live" = true ] && [ "$idle" -gt "$STALL_S" ]; then
-      state="stalled"; stalled=true; STALL_ANY=1
-    fi
-
-    lanes="$(jq -nc --argjson L "$lanes" \
-      --arg id "$id" --arg brain "$brain" --arg state "$state" --arg activity "$activity" \
-      --arg last_c "$last_c" --arg etail "$etail" --arg art "${art:-}" --arg phase "${phase:-build}" \
-      --argjson started "$started" --argjson elapsed "$elapsed" \
-      --argjson idle "$idle" --argjson stalled "$stalled" --argjson live "$live" \
-      --argjson commits "${commits:-0}" --argjson tools "${tools:-0}" --argjson tokens "${tokens:-0}" \
-      '$L + [{id:$id,brain:$brain,phase:$phase,state:$state,started:$started,elapsed_s:$elapsed,
-              last_activity_s:$idle,stalled:$stalled,live_signal:$live,
-              commits:$commits,tools:$tools,tokens:$tokens,activity:$activity,
-              last_commit:$last_c,artifact:$art,err_tail:$etail}]')"
-  done
-  local manifest="null"
-  if [ -f "$RUNDIR/manifest.json" ]; then
-    manifest="$(jq -c '{packet_count:(.packets|length), phases:(.phases // [])}' "$RUNDIR/manifest.json" 2>/dev/null)"
-  fi
-  jq -nc --arg run "$RUN" --arg repo "$REPO" --argjson now "$now" --argjson lanes "$lanes" \
-    --argjson manifest "${manifest:-null}" --argjson stall "$STALL_S" \
-    '{run:$run,repo:$repo,generated_at:$now,stall_seconds:$stall,lanes:$lanes,manifest:$manifest}'
-}
-
-STALL_ANY=0
-# the JSON is always emitted first - --exit-stalled changes the exit code, never
-# the data product, so a caller can pipe and branch in the same invocation.
-if [ -n "$WATCH" ]; then
-  err "watching every ${WATCH}s -> $OUT (ctrl-c to stop)"
-  while :; do
-    emit > "$OUT.tmp" && mv -f "$OUT.tmp" "$OUT"
-    if [ "$EXIT_STALLED" = 1 ] && [ "$STALL_ANY" = 1 ]; then
-      err "stalled lane detected - exiting 14"; exit 14
-    fi
-    sleep "$WATCH"
-  done
-elif [ -n "$OUT" ]; then
-  emit > "$OUT"
-else
-  emit
-fi
-[ "$EXIT_STALLED" = 1 ] && [ "$STALL_ANY" = 1 ] && { err "stalled lane(s) detected"; exit 14; }
-exit 0

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

@@ -1,496 +0,0 @@
-#!/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"
-# heal the runner's own PATH too, or the jq guard below self-skips on hosts
-# with a stale env snapshot (the exact failure _env.sh exists to fix)
-[ -f "$S/_env.sh" ] && . "$S/_env.sh"
-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 ff-status.sh ff-run.sh ff-clean.sh ff-import.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
-
-# --- codex windows.sandbox override (2026-07-27 elevation-hang guard) ------------
-# the flag itself can't be exercised offline (no codex binary), so gate the two
-# things that CAN rot: the value validation, and the wiring into `codex exec`.
-FLEETFLOW_CODEX_WINDOWS_SANDBOX=bogus \
-  check "spawn: rejects bad windows-sandbox value" 2 \
-  bash "$S/ff-spawn.sh" --run r0 --id ws --brain codex --prompt-file "$PKT" --repo "$REPO" --dry-run
-FLEETFLOW_CODEX_WINDOWS_SANDBOX=bogus \
-  check "spawn: windows-sandbox value ignored for non-codex brains" 0 \
-  bash "$S/ff-spawn.sh" --run r0 --id ws --brain sonnet --prompt-file "$PKT" --repo "$REPO" --dry-run
-grep -q 'windows\.sandbox="\$CODEX_WINSANDBOX"' "$S/ff-spawn.sh" \
-  && ok "spawn: windows.sandbox override wired into codex exec" \
-  || bad "spawn: windows.sandbox override missing from codex exec"
-grep -q 'FLEETFLOW_CODEX_WINDOWS_SANDBOX-unelevated' "$S/ff-spawn.sh" \
-  && ok "spawn: windows.sandbox defaults to unelevated" \
-  || bad "spawn: windows.sandbox default is not unelevated"
-
-# --- 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
-
-# --- grok brain (non-Anthropic worker: grok -p envelope, no is_error) ----------
-GKP="$TMP/grok-packet.txt"; echo "grok task. FINAL REPLY: one line." > "$GKP"
-check "spawn: grok brain accepted (dry-run)" 0 bash "$S/ff-spawn.sh" --run rg --id g --brain grok --prompt-file "$GKP" --repo "$REPO" --dry-run
-jq -e '.text=="DRYRUN" and .stopReason=="EndTurn" and (has("is_error")|not)' "$REPO/.fleetflow/rg/g.result.json" >/dev/null \
-  && ok "spawn: grok dry-run stub is a grok envelope (text+stopReason, no is_error)" || bad "spawn: grok stub shape wrong"
-jq -e '.packets[]|select(.id=="g")|.brain=="grok"' "$REPO/.fleetflow/rg/manifest.json" >/dev/null \
-  && ok "manifest records brain=grok" || bad "manifest grok brain wrong"
-check "collect: grok envelope passes gate" 0 bash "$S/ff-collect.sh" --run rg --id g --repo "$REPO"
-GOUT="$(bash "$S/ff-collect.sh" --run rg --id g --repo "$REPO" 2>/dev/null)"
-[ "$GOUT" = "DRYRUN" ] && ok "collect: grok prints .text" || bad "collect: grok text wrong: '$GOUT'"
-# grok --schema lane prefers the already-parsed .structuredOutput
-jq -nc '{text:"ignored prose",stopReason:"EndTurn",structuredOutput:{verdict:"ok"}}' > "$REPO/.fleetflow/rg/gs.result.json"
-jq -nc '{type:"result",key:"v2:gs",id:"gs",brain:"grok",rc:0,artifact:"x"}' >> "$REPO/.fleetflow/rg/journal.jsonl"
-GSOUT="$(bash "$S/ff-collect.sh" --run rg --id gs --repo "$REPO" --schema 2>/dev/null)"; GSRC=$?
-{ [ "$GSRC" = "0" ] && printf '%s' "$GSOUT" | jq -e '.verdict=="ok"' >/dev/null; } \
-  && ok "collect: grok --schema returns structuredOutput" || bad "collect: grok structuredOutput wrong (rc=$GSRC out=$GSOUT)"
-# grok schema fallback: no structuredOutput, .text carries fenced JSON
-jq -nc '{text:"```json\n{\"verdict\":\"fb\"}\n```",stopReason:"EndTurn"}' > "$REPO/.fleetflow/rg/gf.result.json"
-jq -nc '{type:"result",key:"v2:gf",id:"gf",brain:"grok",rc:0,artifact:"x"}' >> "$REPO/.fleetflow/rg/journal.jsonl"
-GFOUT="$(bash "$S/ff-collect.sh" --run rg --id gf --repo "$REPO" --schema 2>/dev/null)"; GFRC=$?
-{ [ "$GFRC" = "0" ] && printf '%s' "$GFOUT" | jq -e '.verdict=="fb"' >/dev/null; } \
-  && ok "collect: grok --schema falls back to fenced .text" || bad "collect: grok fallback wrong (rc=$GFRC)"
-# grok empty-text envelope fails the gate
-jq -nc '{text:"",stopReason:"EndTurn"}' > "$REPO/.fleetflow/rg/ge.result.json"
-jq -nc '{type:"result",key:"v2:ge",id:"ge",brain:"grok",rc:0,artifact:"x"}' >> "$REPO/.fleetflow/rg/journal.jsonl"
-check "collect: grok empty text fails gate" 10 bash "$S/ff-collect.sh" --run rg --id ge --repo "$REPO"
-# ff-doctor surfaces a bin-grok structural check
-bash "$S/ff-doctor.sh" --offline 2>/dev/null | grep -qE "^bin-grok	(ok|advisory)" \
-  && ok "doctor: reports bin-grok check" || bad "doctor: bin-grok check missing"
-
-# --- phases --------------------------------------------------------------------------
-check "spawn: --phase accepted" 0 bash "$S/ff-spawn.sh" --run r1 --id ver --brain opus --phase verify --prompt-file "$PKT" --repo "$REPO" --dry-run
-bash "$S/ff-status.sh" --run r1 --repo "$REPO" 2>/dev/null | jq -e '.lanes[] | select(.id=="ver") | .phase=="verify"' >/dev/null \
-  && ok "status: phase propagates" || bad "status: phase missing"
-bash "$S/ff-status.sh" --run r1 --repo "$REPO" 2>/dev/null | jq -e '.lanes[] | select(.id=="a") | .phase=="build"' >/dev/null \
-  && ok "status: default phase is build" || bad "status: default phase wrong"
-
-# --- status feed --------------------------------------------------------------------
-check "status: no args" 2 bash "$S/ff-status.sh"
-check "status: watch without out" 2 bash "$S/ff-status.sh" --run r1 --repo "$REPO" --watch 3
-bash "$S/ff-status.sh" --run r1 --repo "$REPO" 2>/dev/null | jq -e '.lanes | length >= 2' >/dev/null \
-  && ok "status: emits lanes JSON" || bad "status: JSON invalid"
-bash "$S/ff-status.sh" --run r1 --repo "$REPO" 2>/dev/null | jq -e '.lanes[] | select(.id=="a") | .state=="done"' >/dev/null \
-  && ok "status: dry-run lane state done" || bad "status: lane state wrong"
-[ -f "$HERE/../assets/ff-monitor.html" ] && grep -q "status.json" "$HERE/../assets/ff-monitor.html" \
-  && ok "monitor asset present + polls status.json" || bad "monitor asset missing"
-# torn-write guard: an empty/missing-lanes payload is treated as a fetch miss
-grep -q "torn-write guard" "$HERE/../assets/ff-monitor.html" \
-  && ok "monitor: empty-lanes torn-write guard present" || bad "monitor: torn-write guard missing"
-grep -q "d.lanes.length === 0" "$HERE/../assets/ff-monitor.html" \
-  && ok "monitor: guards on empty lanes array" || bad "monitor: empty-lanes guard logic missing"
-
-# --- 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"
-
-# --- manifest (feature 1): created on first spawn, append, idempotent -----------
-M="$REPO/.fleetflow/r1/manifest.json"
-[ -f "$M" ] && ok "manifest created on first spawn" || bad "manifest not created"
-jq -e '.run=="r1" and .base=="main" and (.created_by|startswith("ff-spawn/"))' "$M" >/dev/null \
-  && ok "manifest header fields" || bad "manifest header wrong"
-# lane 'a' was spawned several times (cache-hit, --force, changed) yet stays 1 entry
-NA="$(jq '[.packets[]|select(.id=="a")]|length' "$M")"
-[ "$NA" = "1" ] && ok "manifest packet idempotent (one entry per id)" || bad "manifest has $NA 'a' entries"
-# every packet carries the Wave-1 fields the brief requires
-jq -e '.packets[]|select(.id=="a")|has("effort") and has("key") and has("max_turns") and has("worktree")' "$M" >/dev/null \
-  && ok "manifest packet has effort+key+max_turns+worktree" || bad "manifest packet fields missing"
-# ff-status surfaces the manifest summary
-bash "$S/ff-status.sh" --run r1 --repo "$REPO" 2>/dev/null | jq -e '.manifest.packet_count >= 1' >/dev/null \
-  && ok "status: surfaces manifest.packet_count" || bad "status: manifest summary missing"
-
-# --- ff-run.sh (feature 2): whole-run resume + status alias ---------------------
-check "ff-run: no subcommand -> 2" 2 bash "$S/ff-run.sh"
-check "ff-run: bad subcommand -> 2" 2 bash "$S/ff-run.sh" frobnicate --run r1 --repo "$REPO"
-check "ff-run: resume missing run -> 2" 2 bash "$S/ff-run.sh" resume --run nope --repo "$REPO"
-check "ff-run: resume no manifest -> 2" 2 bash "$S/ff-run.sh" resume --run r1 --repo "$TMP"
-# fresh run, two DISTINCT packets so replay order is unambiguous
-PA="$TMP/r2-a.txt"; echo "do A. FINAL REPLY: a" > "$PA"
-PB="$TMP/r2-b.txt"; echo "do B. FINAL REPLY: b" > "$PB"
-bash "$S/ff-spawn.sh" --run r2 --id a --brain sonnet --prompt-file "$PA" --repo "$REPO" --dry-run >/dev/null 2>&1
-bash "$S/ff-spawn.sh" --run r2 --id b --brain sonnet --prompt-file "$PB" --repo "$REPO" --dry-run >/dev/null 2>&1
-# resume re-spawns both -> both cache-hit -> exit 0; ids return in manifest order
-RR="$(bash "$S/ff-run.sh" resume --run r2 --repo "$REPO" 2>/dev/null)"; RC=$?
-[ "$RC" = "0" ] && ok "ff-run: all-cached resume exits 0" || bad "ff-run: resume rc=$RC"
-printf '%s' "$RR" | jq -e 'length==2 and .[0].id=="a" and .[1].id=="b" and all(.[]; .status=="cached")' >/dev/null \
-  && ok "ff-run: resume preserves packet order (no reorder drift)" || bad "ff-run: resume order wrong: $RR"
-# status subcommand == ff-status (compare stable fields; generated_at differs by design)
-SA="$(bash "$S/ff-run.sh" status --run r2 --repo "$REPO" 2>/dev/null | jq -c '{run,lanes:[.lanes[].id]}')"
-SB="$(bash "$S/ff-status.sh" --run r2 --repo "$REPO" 2>/dev/null | jq -c '{run,lanes:[.lanes[].id]}')"
-[ "$SA" = "$SB" ] && ok "ff-run status aliases ff-status" || bad "ff-run status != ff-status"
-
-# --- schema fence-strip (feature 3) --------------------------------------------
-# a result whose text is fenced JSON must still validate (--schema strips fences)
-jq -nc '{is_error:false,result:"```json\n{\"verdict\":\"ok\"}\n```"}' > "$REPO/.fleetflow/r1/fence.result.json"
-jq -nc '{type:"result",key:"v2:fence",id:"fence",brain:"sonnet",rc:0,artifact:"x"}' >> "$REPO/.fleetflow/r1/journal.jsonl"
-FOUT="$(bash "$S/ff-collect.sh" --run r1 --id fence --repo "$REPO" --schema 2>/dev/null)"; FRC=$?
-[ "$FRC" = "0" ] && ok "collect: fence-strip lets fenced JSON validate" || bad "collect: fence-strip failed rc=$FRC"
-printf '%s' "$FOUT" | jq -e '.verdict=="ok"' >/dev/null && ok "collect: fence-strip returns inner JSON" || bad "collect: fence-strip output wrong"
-
-# --- schema --repair seam (feature 3) ------------------------------------------
-# bad result + FLEETFLOW_REPAIR_DRYRUN: do_repair saves <id>.invalid.txt and
-# respawns a <id>-repair lane. The dry-run lane replies "DRYRUN" (not JSON), so
-# the repair gate fails -> exit 10; we assert the SEAM fired, not a happy path.
-jq -nc '{is_error:false,result:"this is not json"}' > "$REPO/.fleetflow/r1/rp.result.json"
-jq -nc '{type:"result",key:"v2:rp",id:"rp",brain:"sonnet",rc:0,artifact:"x"}' >> "$REPO/.fleetflow/r1/journal.jsonl"
-FLEETFLOW_REPAIR_DRYRUN=1 bash "$S/ff-collect.sh" --run r1 --id rp --repo "$REPO" --schema --repair >/dev/null 2>&1; RPRC=$?
-[ "$RPRC" = "10" ] && ok "collect: --repair exits 10 when corrected output invalid" || bad "collect: --repair rc=$RPRC (want 10)"
-[ -f "$REPO/.fleetflow/r1/rp.invalid.txt" ] && ok "collect: --repair saved <id>.invalid.txt" || bad "collect: invalid.txt missing"
-NREP="$(jq -r 'select(.type=="result" and .id=="rp-repair")|.id' "$REPO/.fleetflow/r1/journal.jsonl" | wc -l | tr -d ' ')"
-[ "$NREP" -ge 1 ] && ok "collect: --repair respawned rp-repair lane" || bad "collect: no repair lane spawned"
-grep -q 'corrected JSON' "$REPO/.fleetflow/r1/rp-repair.prompt-src.txt" 2>/dev/null \
-  && ok "collect: --repair lane got the corrective prompt" || bad "collect: repair prompt missing"
-
-# --- FF_VERSION in journal (feature 4) -----------------------------------------
-grep -q '"v":"1.1.0"' "$REPO/.fleetflow/r1/journal.jsonl" && ok "journal records FF_VERSION 1.1.0" || bad "journal missing FF_VERSION"
-# every operational script pins the same version (version-skew spine)
-VS=0
-for s in ff-spawn.sh ff-collect.sh ff-status.sh ff-doctor.sh ff-run.sh ff-clean.sh ff-import.sh; do
-  grep -q '^FF_VERSION="1.1.0"$' "$S/$s" || VS=1
-done
-[ "$VS" = "0" ] && ok "all scripts pin FF_VERSION=1.1.0" || bad "version skew across scripts"
-
-# --- effort lever (feature 5): effort is part of the cache key ------------------
-EP="$TMP/effort.txt"; echo "effort test. FINAL REPLY: e" > "$EP"
-check "spawn: effort lane first run -> 0" 0 bash "$S/ff-spawn.sh" --run r3 --id e --brain sonnet --prompt-file "$EP" --repo "$REPO" --dry-run
-check "spawn: effort lane identical -> cached" 3 bash "$S/ff-spawn.sh" --run r3 --id e --brain sonnet --prompt-file "$EP" --repo "$REPO" --dry-run
-# changing ONLY the effort must bust the cache (effort is baked into the OPTS key)
-check "spawn: effort change -> cache miss" 0 bash "$S/ff-spawn.sh" --run r3 --id e --brain sonnet --prompt-file "$EP" --repo "$REPO" --dry-run --effort high
-jq -e '.packets[]|select(.id=="e")|.effort=="high"' "$REPO/.fleetflow/r3/manifest.json" >/dev/null \
-  && ok "manifest records effort=high" || bad "manifest effort field wrong"
-
-# --- cache/tmp redirect (feature 7) --------------------------------------------
-CRT="$TMP/ffcache"; CDP="$TMP/cdp.txt"; echo "cache test. FINAL REPLY: c" > "$CDP"
-FLEETFLOW_CACHE_ROOT="$CRT" bash "$S/ff-spawn.sh" --run r4 --id c --brain sonnet --prompt-file "$CDP" --repo "$REPO" --dry-run >/dev/null 2>&1
-[ -d "$CRT/r4-c" ] && ok "spawn: cache dir created under FLEETFLOW_CACHE_ROOT" || bad "spawn: cache dir not redirected to FLEETFLOW_CACHE_ROOT"
-
-# --- ff-clean.sh (feature 8): autoclean lanes + cache --------------------------
-check "ff-clean: usage -> 2" 2 bash "$S/ff-clean.sh"
-check "ff-clean: no such run -> 2" 2 bash "$S/ff-clean.sh" --run ghost --repo "$REPO"
-# fresh run, cache redirected, three DISTINCT-prompt worktree lanes (distinct so
-# they don't cache-hit and skip worktree creation)
-CLEANROOT="$TMP/cleancache"
-for lid in cleanlane keeplane dirtlane; do
-  echo "clean-$lid task. FINAL REPLY: $lid" > "$TMP/clean-$lid.txt"
-  FLEETFLOW_CACHE_ROOT="$CLEANROOT" bash "$S/ff-spawn.sh" --run rc --id "$lid" --brain sonnet \
-    --prompt-file "$TMP/clean-$lid.txt" --repo "$REPO" --dry-run --worktree >/dev/null 2>&1
-done
-# keeplane gets a real commit (must survive every clean); dirtlane gets untracked junk
-( cd "$REPO/.fleetflow/rc/wt-keeplane" && git -c user.email=t@t -c user.name=t commit -q --allow-empty -m "real work" )
-echo "junk" > "$REPO/.fleetflow/rc/wt-dirtlane/junk.txt"
-[ -d "$CLEANROOT/rc-cleanlane" ] && ok "ff-clean: setup created cache dir" || bad "ff-clean: setup cache dir missing"
-# no --force: cleanlane removed, dirtlane kept (dirty), keeplane kept (1 commit).
-# pass the SAME FLEETFLOW_CACHE_ROOT to ff-clean so it finds the redirected dirs.
-CL="$(FLEETFLOW_CACHE_ROOT="$CLEANROOT" bash "$S/ff-clean.sh" --run rc --repo "$REPO" 2>/dev/null)"
-printf '%s' "$CL" | awk -F'\t' '$1=="cleanlane"&&$2=="removed"{f=1} END{exit !f}' && ok "ff-clean: removes zero-commit clean lane" || bad "ff-clean: clean lane not removed"
-printf '%s' "$CL" | awk -F'\t' '$1=="dirtlane"&&$2=="kept"&&$3~/dirty/{f=1} END{exit !f}' && ok "ff-clean: keeps dirty zero-commit lane (no --force)" || bad "ff-clean: dirty lane mishandled"
-printf '%s' "$CL" | awk -F'\t' '$1=="keeplane"&&$2=="kept"&&$3~/1 commits/{f=1} END{exit !f}' && ok "ff-clean: keeps committed lane" || bad "ff-clean: committed lane mishandled"
-[ -d "$REPO/.fleetflow/rc/wt-cleanlane" ] && bad "ff-clean: clean worktree dir remains" || ok "ff-clean: clean worktree removed"
-git -C "$REPO" show-ref --verify --quiet refs/heads/fleetflow/rc/keeplane && ok "ff-clean: committed branch preserved" || bad "ff-clean: keeper branch deleted"
-# --force: dirtlane now removed; keeplane STILL kept (committed lanes are never force-removed)
-CL2="$(FLEETFLOW_CACHE_ROOT="$CLEANROOT" bash "$S/ff-clean.sh" --run rc --repo "$REPO" --force 2>/dev/null)"
-printf '%s' "$CL2" | awk -F'\t' '$1=="dirtlane"&&$2=="removed"{f=1} END{exit !f}' && ok "ff-clean: --force removes dirty zero-commit lane" || bad "ff-clean: --force dirty mishandled"
-printf '%s' "$CL2" | awk -F'\t' '$1=="keeplane"&&$2=="kept"{f=1} END{exit !f}' && ok "ff-clean: --force still keeps committed lane" || bad "ff-clean: --force removed committed lane!"
-[ -d "$CLEANROOT/rc-cleanlane" ] || [ -d "$CLEANROOT/rc-dirtlane" ] || [ -d "$CLEANROOT/rc-keeplane" ] \
-  && bad "ff-clean: cache dir remains" || ok "ff-clean: cache dirs removed"
-
-# --- state derivation (feature C): last journal record wins -----------------------
-# a respawn appends "started" AFTER an old "result" -> the lane is running again,
-# NOT done/failed (the last-result-wins bug this fixes).
-RD5="$REPO/.fleetflow/r5"; mkdir -p "$RD5"
-: > "$RD5/z.prompt.txt"   # mtime source for elapsed
-printf '%s\n' \
-  '{"type":"started","key":"v2:z","id":"z","brain":"sonnet","phase":"build","v":"1.1.0"}' \
-  '{"type":"result","key":"v2:z","id":"z","brain":"sonnet","rc":0,"artifact":"x"}' \
-  '{"type":"started","key":"v2:z","id":"z","brain":"sonnet","phase":"build","v":"1.1.0"}' \
-  > "$RD5/journal.jsonl"
-bash "$S/ff-status.sh" --run r5 --repo "$REPO" 2>/dev/null \
-  | jq -e '.lanes[]|select(.id=="z")|.state=="running"' >/dev/null \
-  && ok "status: respawned lane (started,result,started) is running" || bad "status: respawned lane state wrong"
-# regression guard: same lane with result-last is still done (common path unchanged)
-printf '%s\n' \
-  '{"type":"started","key":"v2:z","id":"z","brain":"sonnet","phase":"build","v":"1.1.0"}' \
-  '{"type":"result","key":"v2:z","id":"z","brain":"sonnet","rc":0,"artifact":"x"}' \
-  > "$RD5/journal.jsonl"
-bash "$S/ff-status.sh" --run r5 --repo "$REPO" 2>/dev/null \
-  | jq -e '.lanes[]|select(.id=="z")|.state=="done"' >/dev/null \
-  && ok "status: result-last lane is still done (no regression)" || bad "status: result-last state wrong"
-
-# --- stall detection (2026-07-27 incident: 2.7h hang reported as running) --------
-# Two lanes spawned at the same instant; only one is still writing. elapsed_s
-# cannot tell them apart - last_activity_s must.
-check "status: rejects non-integer FLEETFLOW_STALL_SECONDS" 2 \
-  env FLEETFLOW_STALL_SECONDS=abc bash "$S/ff-status.sh" --run r5 --repo "$REPO"
-bash "$S/ff-status.sh" --run r5 --repo "$REPO" 2>/dev/null \
-  | jq -e '.lanes[]|has("last_activity_s") and has("stalled")' >/dev/null \
-  && ok "status: every lane carries last_activity_s + stalled" || bad "status: stall fields missing"
-bash "$S/ff-status.sh" --run r5 --repo "$REPO" 2>/dev/null | jq -e '.stall_seconds==600' >/dev/null \
-  && ok "status: default stall threshold is 600s" || bad "status: stall_seconds default wrong"
-
-NOWS="$(date +%s)"
-if touch -d "@$NOWS" "$TMP/.touchprobe" 2>/dev/null; then
-  RD6="$REPO/.fleetflow/r6"; mkdir -p "$RD6"
-  printf '%s\n' \
-    '{"type":"started","key":"v2:w","id":"wedged","brain":"codex","phase":"build","v":"1.1.0"}' \
-    '{"type":"started","key":"v2:v","id":"livewire","brain":"codex","phase":"build","v":"1.1.0"}' \
-    > "$RD6/journal.jsonl"
-  : > "$RD6/wedged.prompt.txt"; : > "$RD6/livewire.prompt.txt"
-  EV='{"type":"item.completed","item":{"type":"command_execution","command":"ls"}}'
-  echo "$EV" > "$RD6/wedged.events.jsonl"; echo "$EV" > "$RD6/livewire.events.jsonl"
-  # both spawned 2.7h ago; wedged went silent 160s in, livewire is writing now
-  touch -d "@$((NOWS-9720))" "$RD6/wedged.prompt.txt" "$RD6/livewire.prompt.txt"
-  touch -d "@$((NOWS-9560))" "$RD6/wedged.events.jsonl"
-  ST6="$(bash "$S/ff-status.sh" --run r6 --repo "$REPO" 2>/dev/null)"
-  printf '%s' "$ST6" | jq -e '.lanes[]|select(.id=="wedged")|.state=="stalled" and .stalled==true' >/dev/null \
-    && ok "status: silent running lane reports stalled" || bad "status: silent lane not stalled"
-  printf '%s' "$ST6" | jq -e '.lanes[]|select(.id=="wedged")|.last_activity_s>9000 and .last_activity_s<10000' >/dev/null \
-    && ok "status: stalled lane last_activity_s tracks the silence, not elapsed" \
-    || bad "status: stalled last_activity_s wrong"
-  printf '%s' "$ST6" | jq -e '.lanes[]|select(.id=="livewire")|.state=="running" and .stalled==false and .last_activity_s<120' >/dev/null \
-    && ok "status: same-age lane still writing stays running" || bad "status: live lane misreported"
-  # threshold is honoured in both directions
-  FLEETFLOW_STALL_SECONDS=99999 bash "$S/ff-status.sh" --run r6 --repo "$REPO" 2>/dev/null \
-    | jq -e '.lanes[]|select(.id=="wedged")|.state=="running"' >/dev/null \
-    && ok "status: raised threshold un-stalls the lane" || bad "status: threshold not honoured (raise)"
-  FLEETFLOW_STALL_SECONDS=1 bash "$S/ff-status.sh" --run r6 --repo "$REPO" 2>/dev/null \
-    | jq -e '[.lanes[]|select(.stalled)]|length==2' >/dev/null \
-    && ok "status: lowered threshold stalls both lanes" || bad "status: threshold not honoured (lower)"
-  # a finished lane is never stalled, however long it has been silent
-  FLEETFLOW_STALL_SECONDS=1 bash "$S/ff-status.sh" --run r5 --repo "$REPO" 2>/dev/null \
-    | jq -e '.lanes[]|select(.id=="z")|.state=="done" and .stalled==false' >/dev/null \
-    && ok "status: done lane never stalls" || bad "status: done lane wrongly stalled"
-
-  # --- no live stream => no verdict (the false-positive guard) -------------------
-  # A brain whose artifact/stderr are created at launch and untouched until exit
-  # (grok --output-format json; any claude lane whose transcript we can't locate)
-  # looks silent from birth. Flagging that stalled would condemn every healthy
-  # long lane, so those report live_signal=false and are never stalled.
-  RD7="$REPO/.fleetflow/r7"; mkdir -p "$RD7/wt-son"
-  printf '%s\n' \
-    '{"type":"started","key":"v2:s","id":"son","brain":"sonnet","phase":"build","v":"1.1.0"}' \
-    '{"type":"started","key":"v2:g","id":"grk","brain":"grok","phase":"build","v":"1.1.0"}' \
-    > "$RD7/journal.jsonl"
-  for lid in son grk; do : > "$RD7/$lid.prompt.txt"; : > "$RD7/$lid.err"; : > "$RD7/$lid.result.json"; done
-  touch -d "@$((NOWS-1200))" "$RD7"/son.* "$RD7"/grk.*
-  # HOME is redirected so the host-transcript lookup can't reach the real one
-  ST7="$(HOME="$TMP/fakehome" bash "$S/ff-status.sh" --run r7 --repo "$REPO" 2>/dev/null)"
-  printf '%s' "$ST7" | jq -e '.lanes[]|select(.id=="grk")|.state=="running" and .stalled==false and .live_signal==false' >/dev/null \
-    && ok "status: grok lane with no live stream never stalls" || bad "status: grok lane false-stalled"
-  printf '%s' "$ST7" | jq -e '.lanes[]|select(.id=="son")|.state=="running" and .live_signal==false' >/dev/null \
-    && ok "status: unlocatable claude transcript reports live_signal=false" \
-    || bad "status: claude lane without transcript misreported"
-  printf '%s' "$ST7" | jq -e '.lanes[]|select(.id=="son")|.last_activity_s>1000' >/dev/null \
-    && ok "status: uncovered lane still reports last_activity_s" || bad "status: last_activity_s not reported"
-
-  # a worktree'd claude lane IS covered: its host transcript is unambiguous
-  ENC="$(printf '%s' "$RD7/wt-son" | sed 's#[:\\/.]#-#g')"
-  mkdir -p "$TMP/fakehome/.claude/projects/$ENC"
-  printf '{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Edit"}]}}\n' \
-    > "$TMP/fakehome/.claude/projects/$ENC/sess.jsonl"
-  HOME="$TMP/fakehome" bash "$S/ff-status.sh" --run r7 --repo "$REPO" 2>/dev/null \
-    | jq -e '.lanes[]|select(.id=="son")|.live_signal==true and .stalled==false and .activity=="live: Edit"' >/dev/null \
-    && ok "status: worktree claude lane picks up its live host transcript" \
-    || bad "status: host transcript lookup failed"
-  touch -d "@$((NOWS-1200))" "$TMP/fakehome/.claude/projects/$ENC/sess.jsonl"
-  HOME="$TMP/fakehome" bash "$S/ff-status.sh" --run r7 --repo "$REPO" 2>/dev/null \
-    | jq -e '.lanes[]|select(.id=="son")|.state=="stalled" and .stalled==true' >/dev/null \
-    && ok "status: silent claude transcript does stall" || bad "status: silent claude lane not stalled"
-else
-  echo "  SKIP  stall detection (touch -d @epoch unsupported here)"
-fi
-# --- --exit-stalled: a watchdog exit code, never a change to the data ----------
-check "status: --exit-stalled exits 0 when nothing is stalled" 0 \
-  bash "$S/ff-status.sh" --run r1 --repo "$REPO" --exit-stalled
-if touch -d "@$NOWS" "$TMP/.touchprobe" 2>/dev/null; then
-  check "status: --exit-stalled exits 14 on a stalled lane" 14 \
-    bash "$S/ff-status.sh" --run r6 --repo "$REPO" --exit-stalled
-  SO="$(bash "$S/ff-status.sh" --run r6 --repo "$REPO" --exit-stalled 2>/dev/null)"
-  printf '%s' "$SO" | jq -e '.lanes|length==2' >/dev/null \
-    && ok "status: --exit-stalled still emits the full JSON" || bad "status: --exit-stalled ate the data"
-fi
-
-# --- reap anchors (ff-spawn proc records + ff-clean --reap) --------------------
-jq -e 'select(.type=="proc" and .id=="a") | has("pid") and has("winpid") and has("at")' \
-  "$REPO/.fleetflow/r1/journal.jsonl" >/dev/null 2>&1 \
-  && ok "spawn: journals a proc reap anchor" || bad "spawn: no proc reap anchor"
-# the proc record must not hijack state derivation (it is not state-bearing)
-RD8="$REPO/.fleetflow/r8"; mkdir -p "$RD8"; : > "$RD8/z.prompt.txt"
-printf '%s\n' \
-  '{"type":"started","key":"v2:z","id":"z","brain":"sonnet","phase":"build","v":"1.1.0"}' \
-  '{"type":"proc","id":"z","brain":"sonnet","pid":1,"winpid":2,"at":1}' \
-  '{"type":"result","key":"v2:z","id":"z","brain":"sonnet","rc":0,"artifact":"x"}' \
-  '{"type":"proc","id":"z","brain":"sonnet","pid":3,"winpid":4,"at":2}' > "$RD8/journal.jsonl"
-bash "$S/ff-status.sh" --run r8 --repo "$REPO" 2>/dev/null \
-  | jq -e '.lanes[]|select(.id=="z")|.state=="done"' >/dev/null \
-  && ok "status: trailing proc record does not hijack lane state" \
-  || bad "status: proc record broke state derivation"
-check "clean: --reap on a run with no anchors is a no-op, not an error" 0 \
-  bash "$S/ff-clean.sh" --run r8 --repo "$REPO" --reap
-grep -q 'join \[char\]9' "$S/ff-clean.sh" \
-  && ok "clean: reap emits TSV via [char]9 (PS single quotes eat backtick-t)" \
-  || bad "clean: reap tab emission is escape-fragile"
-
-# --- ff-doctor windows.sandbox tripwire ---------------------------------------
-grep -q 'codex debug prompt-input' "$S/ff-doctor.sh" \
-  && ok "doctor: sandbox tripwire uses the config-only probe" \
-  || bad "doctor: tripwire probe missing"
-# code lines only - the guard comment names `codex sandbox` to explain the ban
-grep -vE '^[[:space:]]*#' "$S/ff-doctor.sh" | grep -q 'codex sandbox' \
-  && bad "doctor: must NOT invoke codex sandbox (provisioning is machine-global)" \
-  || ok "doctor: never provisions a sandbox"
-bash "$S/ff-doctor.sh" --offline 2>/dev/null | grep -qE "^codex-winsandbox-mode	(ok|advisory)" \
-  && ok "doctor: reports which windows.sandbox mode lanes will get" \
-  || echo "  SKIP  doctor: winsandbox-mode check (non-Windows host)"
-
-# monitor renders stalled distinctly from running
-grep -q '\.sq\.stalled' "$HERE/../assets/ff-monitor.html" \
-  && ok "monitor: stalled pip has its own style" || bad "monitor: stalled pip style missing"
-grep -q 'l\.stalled' "$HERE/../assets/ff-monitor.html" \
-  && ok "monitor: renders the stalled flag" || bad "monitor: stalled flag unused"
-grep -q 'inflight' "$HERE/../assets/ff-monitor.html" \
-  && ok "monitor: stalled lanes excluded from the finished tally" \
-  || bad "monitor: stalled lanes counted as finished"
-
-# monitor: tethered header + sort/size controls
-grep -q 'position:sticky' "$HERE/../assets/ff-monitor.html" \
-  && ok "monitor: run header is tethered (sticky)" || bad "monitor: sticky header missing"
-grep -q '"ff.sort"' "$HERE/../assets/ff-monitor.html" \
-  && grep -q '|| "active"' "$HERE/../assets/ff-monitor.html" \
-  && ok "monitor: sort control defaults to active-first" \
-  || bad "monitor: active-first sort default missing"
-grep -q 'STATE_RANK' "$HERE/../assets/ff-monitor.html" \
-  && ok "monitor: active sort ranks running before stalled/failed/done" \
-  || bad "monitor: state-rank sort missing"
-grep -q 'data-size="s"' "$HERE/../assets/ff-monitor.html" \
-  && grep -q '"ff.size"' "$HERE/../assets/ff-monitor.html" \
-  && ok "monitor: card-size control present + persisted" \
-  || bad "monitor: card-size control missing"
-
-# --- ff-import.sh (feature B): native Workflow run import ------------------------
-# build a synthetic native wf_ dir: journal.jsonl (started/result keyed by
-# agentId) + two agent transcripts (one string content, one content-array).
-WFD="$TMP/wf_ab12cd34-ef"; mkdir -p "$WFD"
-printf '%s\n' \
-  '{"type":"started","key":"v2:aaaa","agentId":"a01cb5f01fadf5610"}' \
-  '{"type":"result","key":"v2:aaaa","agentId":"a01cb5f01fadf5610","result":{"verdict":"ok","score":7}}' \
-  '{"type":"started","key":"v2:bbbb","agentId":"a02deadbeef00000"}' \
-  > "$WFD/journal.jsonl"
-jq -nc '{type:"user",message:{role:"user",content:"Refute the claim that X is safe."}}' \
-  > "$WFD/agent-a01cb5f01fadf5610.jsonl"
-jq -nc '{type:"user",message:{role:"user",content:[{type:"text",text:"Find any bugs in module Y."}]}}' \
-  > "$WFD/agent-a02deadbeef00000.jsonl"
-IMP="$(bash "$S/ff-import.sh" --wf "$WFD" --run imp1 --repo "$REPO" 2>/dev/null)"; IRC=$?
-[ "$IRC" = "0" ] && ok "ff-import: exits 0 on import" || bad "ff-import: rc=$IRC"
-printf '%s' "$IMP" | awk -F'\t' '$1=="a01cb5f01fadf5610"&&$2=="imported"{f=1} END{exit !f}' \
-  && ok "ff-import: completed agent reported imported" || bad "ff-import: imported TSV wrong"
-printf '%s' "$IMP" | awk -F'\t' '$1=="a02deadbeef00000"&&$2=="incomplete"{f=1} END{exit !f}' \
-  && ok "ff-import: started-only agent reported incomplete" || bad "ff-import: incomplete TSV wrong"
-PC="$(printf '%s' "$IMP" | awk -F'\t' '$1=="a01cb5f01fadf5610"{print $3}')"
-[ -n "$PC" ] && [ "$PC" -gt 0 ] 2>/dev/null && ok "ff-import: prompt_chars > 0" || bad "ff-import: prompt_chars wrong ($PC)"
-IRD="$REPO/.fleetflow/imp1"
-[ -f "$IRD/a01cb5f01fadf5610.prompt.txt" ] && ok "ff-import: wrote prompt.txt (completed)" || bad "ff-import: prompt.txt missing"
-grep -q "Refute the claim" "$IRD/a01cb5f01fadf5610.prompt.txt" && ok "ff-import: prompt extracted (string content)" || bad "ff-import: string-content prompt wrong"
-grep -q "Find any bugs" "$IRD/a02deadbeef00000.prompt.txt" && ok "ff-import: prompt extracted (content array)" || bad "ff-import: array-content prompt wrong"
-[ -f "$IRD/a01cb5f01fadf5610.result.json" ] && ok "ff-import: wrote result.json (completed)" || bad "ff-import: result.json missing"
-[ ! -f "$IRD/a02deadbeef00000.result.json" ] && ok "ff-import: incomplete agent has no result.json" || bad "ff-import: incomplete got result.json"
-jq -e '.is_error==false and (.result|fromjson|.verdict=="ok" and .score==7)' "$IRD/a01cb5f01fadf5610.result.json" >/dev/null \
-  && ok "ff-import: result.json wraps native result (tojson)" || bad "ff-import: result.json shape wrong"
-NJ="$IRD/journal.jsonl"
-[ "$(jq -r 'select(.type=="result" and .id=="a01cb5f01fadf5610")|.brain' "$NJ")" = "native" ] \
-  && ok "ff-import: journal result brain=native" || bad "ff-import: journal brain wrong"
-# phase lives on the started record (same convention as ff-spawn), not the result
-[ "$(jq -r 'select(.type=="started" and .id=="a01cb5f01fadf5610")|.phase' "$NJ")" = "imported" ] \
-  && ok "ff-import: journal phase=imported" || bad "ff-import: journal phase wrong"
-[ -z "$(jq -r 'select(.type=="result" and .id=="a02deadbeef00000")' "$NJ")" ] \
-  && ok "ff-import: incomplete agent has no result record" || bad "ff-import: incomplete got a result record"
-jq -e --arg wf "$WFD" '.packets[]|select(.id=="a01cb5f01fadf5610")|.brain=="native" and .imported_from==$wf' "$IRD/manifest.json" >/dev/null \
-  && ok "ff-import: manifest packet brain=native + imported_from" || bad "ff-import: manifest packet wrong"
-# nothing to import -> exit 3
-WFE="$TMP/wf_empty"; mkdir -p "$WFE"; : > "$WFE/journal.jsonl"
-check "ff-import: empty wf journal -> 3" 3 bash "$S/ff-import.sh" --wf "$WFE" --run imp2 --repo "$REPO"
-# ff-run resume SKIPS imported native packets (terminal, not replayable)
-RRN="$(bash "$S/ff-run.sh" resume --run imp1 --repo "$REPO" 2>/dev/null)"; RCRR=$?
-[ "$RCRR" = "0" ] && ok "ff-run: resume skips native packets (exit 0)" || bad "ff-run: resume on imported run rc=$RCRR"
-printf '%s' "$RRN" | jq -e 'any(.[]; .id=="a01cb5f01fadf5610" and .status=="imported")' >/dev/null \
-  && ok "ff-run: native packet reported imported (skipped)" || bad "ff-run: native packet not skipped"
-
-echo "=== $PASS passed, $FAILN failed ==="
-[ "$FAILN" = 0 ] || exit 1
-exit 0

+ 4 - 4
skills/parallel-ops/SKILL.md

@@ -6,7 +6,7 @@ license: MIT
 allowed-tools: "Read"
 metadata:
   author: claude-mods
-  related-skills: "fleet-ops, fleet-worker, fleetflow, loop-ops, iterate, spawn"
+  related-skills: "fleet-ops, fleet-worker, loop-ops, iterate, spawn"
 ---
 
 # Parallel Ops — router
@@ -21,8 +21,8 @@ you need, and stop reading here.
 | You want | Go to | Not this, because |
 |---|---|---|
 | Parallel subtasks run by your session's OWN provider and model tier, in-process | native Workflow tool / `Agent` subagents (`isolation: worktree`) | not a fleet skill at all — no dedicated skill needed |
-| Work done by a CHEAPER brain than your session (GLM/Haiku/Sonnet-under-Opus) — one subtask or a whole fan-out, all one brain type | [fleet-worker](../fleet-worker/) | [fleetflow](../fleetflow/) is overkill when every worker runs the same brain |
-| DIFFERENT brains per work class in one run, or cross-provider dissent in verify (e.g. Codex or Grok refutes GLM) | [fleetflow](../fleetflow/) | [fleet-worker](../fleet-worker/) runs one brain type per run (any provider, but not mixed). Also the home for a **Grok** (xAI) worker — its own binary, not a `claude -p` brain |
+| Work done by a CHEAPER brain than your session (GLM/Haiku/Sonnet-under-Opus) — one subtask or a whole fan-out, all one brain type | [fleet-worker](../fleet-worker/) | fleetflow is overkill when every worker runs the same brain |
+| DIFFERENT brains per work class in one run, or cross-provider dissent in verify (e.g. Codex or Grok refutes GLM) | fleetflow (own repo — `X:\Forge\fleetflow`, mounted at `~/.claude/skills/fleetflow`) | [fleet-worker](../fleet-worker/) runs one brain type per run (any provider, but not mixed). fleetflow is also the home for a **Grok** (xAI) worker — its own binary, not a `claude -p` brain |
 | Work that RECURS on a schedule across sessions — cron, routine, unattended ticks | [loop-ops](../loop-ops/) | [iterate](../iterate/) is one continuous session, not a schedule |
 | Drive ONE mechanical metric to a target in one continuous session (even a long overnight one) | [iterate](../iterate/) | [loop-ops](../loop-ops/) is the scheduler *around* sessions, not the session itself |
 | Land/merge branches that parallel work produced | [fleet-ops](../fleet-ops/) | the terminus for every branch-producing row above (in-process subagents and prompt authoring produce no branches) |
@@ -63,7 +63,7 @@ land or inner + outer.
 
 - [fleet-ops](../fleet-ops/) — landing discipline: test-gated queue, pre-land scrub, auto-rebase, revert
 - [fleet-worker](../fleet-worker/) — one cheap headless worker (GLM, Sonnet, Haiku) fanned out and gated
-- [fleetflow](../fleetflow/) — heterogeneous cross-provider fleet (GLM + Codex + Grok + Anthropic)
+- fleetflow — heterogeneous cross-provider fleet (GLM + Codex + Grok + Pi + Anthropic); extracted to its own repo at `X:\Forge\fleetflow`, still invocable as `/fleetflow` via the junction at `~/.claude/skills/fleetflow`
 - [loop-ops](../loop-ops/) — outer-loop design: risk tiers, kill switch, scheduling
 - [iterate](../iterate/) — autonomous single-metric improvement loop
 - [spawn](../spawn/) — generates expert-agent prompt files (authoring, not runtime)