Răsfoiți Sursa

feat(skills): Restore fleetflow pi wildcard brain lost to skills sync

Replays the 2026-08-01 X:\Agents\Pi session's edits (transcript
0b9c6cf3, 00:47-01:16Z) on top of current main - the repo->global
skills sync had reverted ff-spawn/ff-doctor/ff-collect/worker-contracts
to their Jul-27 state while SKILL.md kept documenting the brain.

- ff-spawn: pi brain via 'pi -p --mode json' (stdin prompt), per-lane
  PI_CODING_AGENT_DIR isolation, effort->--thinking mapping, NDJSON
  event-stream distillation into the claude-style envelope; also
  restores the 2026-07-31 SPAWN_MODEL journal layer (transcript
  2cec154a) the pi edits build on
- ff-doctor: bin-pi check + pi-auth env-key presence probe
- ff-collect: guard comment - pi lanes ride the default gate, no branch
- worker-contracts SS4: full pi contract
- SKILL.md: pi rows in brain + routing tables

Verified: bash -n clean, tests/run.sh 162/162 green on this base.
Originally live-verified end-to-end (gemini-2.5-flash lane) before the
sync wiped it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0xDarkMatter 1 lună în urmă
părinte
comite
1a535e9f6e

+ 4 - 2
skills/fleetflow/SKILL.md

@@ -25,6 +25,7 @@ workers**, where each worker gets its own env block — and therefore its own br
 | **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 |
 
@@ -59,7 +60,8 @@ with the Codex/Grok columns and the orchestrator rule:
 | **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, or GLM) | *never under-power a judge*; diversity beats redundancy |
+| **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
@@ -325,7 +327,7 @@ default the script-author follows, not an option — and real runs routinely hit
 | 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`, Anthropic via `claude -p`); pins `windows.sandbox=unelevated` for Codex on Windows |
+| [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 |

+ 70 - 4
skills/fleetflow/references/worker-contracts.md

@@ -161,7 +161,73 @@ binary and protocol, **not** a `claude -p` wrapper. Verified against
 - **Terms:** deployment-key usage bills to your xAI account/plan — verify its
   terms as you would codex's ChatGPT-plan billing.
 
-## 4. Anthropic workers (`claude -p`)
+## 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
@@ -177,7 +243,7 @@ binary and protocol, **not** a `claude -p` wrapper. Verified against
   subscription-authed orchestrator stays interactive; if a scheduler drives
   fleetflow runs, put the workers (or the whole run) on an API key.
 
-## 5. Orchestrator: Fable if available, Opus if not
+## 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
@@ -194,7 +260,7 @@ 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).
 
-## 6. Uniform result layout
+## 7. Uniform result layout
 
 ```
 <repo>/.fleetflow/<run>/
@@ -216,7 +282,7 @@ judge", applied to yourself).
 sensitive-file guard fires there before `bypassPermissions`) and must be
 gitignored; `ff-spawn` appends it to `.git/info/exclude` if absent.
 
-## 7. Effort lever, cache redirect, transcript archiving (Wave 1)
+## 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

+ 4 - 1
skills/fleetflow/scripts/ff-collect.sh

@@ -5,7 +5,10 @@
 # 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). The escape guard
+# 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.

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

@@ -51,6 +51,9 @@ if command -v claude >/dev/null; then say "bin-claude" ok "found"; else say "bin
 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
@@ -170,6 +173,38 @@ if command -v "$GROK" >/dev/null; then
   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

+ 113 - 5
skills/fleetflow/scripts/ff-spawn.sh

@@ -22,7 +22,7 @@ Usage: ff-spawn.sh --run NAME --id ID --brain BRAIN --prompt-file FILE
 
   --run NAME       run name (groups lanes; [a-z0-9-]+)
   --id ID          lane id within the run ([a-z0-9-]+)
-  --brain BRAIN    glm | codex | grok | sonnet | opus | haiku | fable
+  --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)
@@ -39,6 +39,16 @@ Usage: ff-spawn.sh --run NAME --id ID --brain BRAIN --prompt-file FILE
   --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
@@ -92,7 +102,7 @@ while [ $# -gt 0 ]; do
   esac
 done
 
-case "$BRAIN" in glm|codex|grok|sonnet|opus|haiku|fable) ;; *) err "invalid --brain '$BRAIN'"; exit 2 ;; esac
+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) ------------
@@ -159,7 +169,20 @@ fi
 
 # --- journal: hash-keyed replay cache (native Workflow pattern) --------------
 # effort is part of the key (different effort = a different run), per Wave 1.
-OPTS="turns=$MAX_TURNS|wt=$WORKTREE|schema=$( [ -n "$SCHEMA" ] && basename "$SCHEMA" )|effort=$EFFORT"
+# 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"
 
@@ -206,9 +229,26 @@ if [ "$WORKTREE" = 1 ]; then
   fi
 fi
 
-# phase is display metadata only - deliberately NOT part of the cache key
+# 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" \
-  '{type:"started",key:$k,id:$id,brain:$b,phase:$p,v:$v}' >> "$JOURNAL"
+  --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
@@ -253,6 +293,11 @@ archive_transcript() {
     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
@@ -330,6 +375,69 @@ else
           ${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"