---
name: ainb-fleet:fleet-needs
description: |
  Workflow-backed Jarvis control panel. Runs the deterministic `hangar`
  workflow with verb=needs (discover → enrich → prioritize), renders the
  Jarvis HUD from its render-ready cards, fires AskUserQuestion per blocked
  session, and routes each answer back via tmux send-keys (broker fallback
  only). Requires the workflow gate (CLAUDE_CODE_WORKFLOWS=1). If the gate is
  off, fall back to the prompt-driven `/ainb-fleet:needs` skill.
version: "0.1.0"
user-invocable: true
triggers:
  - ainb-fleet:fleet-needs
  - fleet needs workflow
  - jarvis panel
  - control panel workflow
allowed-tools:
  - Bash
  - Workflow
  - AskUserQuestion
---

# ainb fleet:fleet-needs — workflow-backed cockpit

The session "face" of the sensor-fusion hybrid. The deterministic brain is the
`hangar` workflow (verb=needs); this skill renders its output and handles the
irreducibly-interactive last mile (HUD + AskUserQuestion + routing).

## `fleet-needs` vs `needs` — which skill?

> **This skill (`fleet-needs`) is the workflow-backed "Jarvis" cockpit.** It
> runs the deterministic `hangar` workflow (verb=needs) for the batched
> discover→enrich→prioritize chain, and adds **key-route ASK answering**
> (presses the target's picker keys via tmux for an already-open
> AskUserQuestion). It **requires the workflow gate `CLAUDE_CODE_WORKFLOWS=1`**.
>
> **[`/ainb-fleet:needs`](../needs/SKILL.md) is the plain control panel** —
> prompt-driven, no gate, works everywhere. It is the canonical fallback: if the
> gate is off, this skill **stops and hands off to `needs`** (see Step 0).
>
> Both read the same underlying `ainb fleet needs` data — same panel content,
> same materialized `current_state` source (below). The difference is purely the
> machinery: workflow-batched cockpit (here) vs plain prompt panel (`needs`).

```
SESSION (this skill) ──Workflow({name:'ainb-fleet:jarvis', args:{verb:'needs'}})──▶ brain
   render HUD ◀──{banner,cards,asks}──────────────────────────────────────────┘
   AskUserQuestion per ask  ──answer──▶  tmux send-keys (write leg)
```

## Read/write split — architectural principle

| Direction | Channel | Why |
|-----------|---------|-----|
| **Reads** | materialized `current_state` (hooks → `events.jsonl` → notifyd → SQLite) FIRST; live tmux pane + JSONL transcript scan as the **fallback** for non-Claude agents (Codex/Gemini) and un-materialized sessions | event-sourced latest stage; replayable; ground truth |
| **Writes** | **tmux send-keys** — direct keystrokes to the target pane (verify with capture-pane) | deterministic, no broker latency or delivery gap |
| Fallback writes | peers/broker via `ainb fleet broadcast` | only when no tmux_session known; broker has a known delivery gap |

Reads are **hooks-primary**: a Claude session's ASK/ERR/WAIT/IDLE is learned
from the event-sourced `current_state` table (materialized per session, keyed by
cwd), not by scraping panes. The pane/transcript scan is the fallback for
sessions the hooks don't cover — chiefly non-Claude agents (Codex/Gemini fire no
Claude hooks) and any session notifyd hasn't materialized (daemon down / hooks
not installed). Each card carries a `source` (`"hook"` | `"tmux"`) telling you
which path produced it.

> **This skill does NOT install hooks.** It consumes the materialized state. The
> global Claude Code hooks are installed by
> [`ainb fleet atc setup`](../atc/SKILL.md) (into `~/.claude/settings.json`).

All write routing below uses tmux first. Broker is a last resort, not the default.
This matches the binary's default `AINB_FLEET_TRANSPORT=tmux-first`; set
`tmux-only` to disable the broker fallback entirely, or `peers` to restore the
legacy broker-first order.

## Step 0 — gate check (fallback if workflows off)

```bash
[ -n "$CLAUDE_CODE_WORKFLOWS" ] && echo "gate on" || echo "gate off"
```

If gate off → **stop and invoke `/ainb-fleet:needs`** (the prompt-skill).
The workflow path only works when `CLAUDE_CODE_WORKFLOWS=1` is set.

## Step 0.5 — choose the enrich locus (hybrid, token-efficient)

The Rust read already does the expensive part for free: each card carries
`enrich_key`, an `enriched` string when a **fresh cached** suggestion exists,
and `need_enrich: true` only when it has no cache entry. Decide how to draft the
*missing* ones by COUNT — never one subagent per session.

```bash
out=$(ainb --format json fleet needs)            # 0 tokens — cards pre-attached
stale=$(echo "$out" | jq '[.[] | select(.need_enrich==true)] | length')
```

```
stale == 0  ─▶ render straight from $out          (0 tokens — all cached/--no-enrich)
stale ≤ 6   ─▶ draft inline in THIS session        (0 subagents)
stale  > 6  ─▶ run the jarvis workflow (Step 1)     (exactly 1 batched agent)
```

Cutoff is `AINB_FLEET_ENRICH_INLINE_MAX` (default 6).

**Inline draft (≤6):** for each `need_enrich` card, you draft the `suggestion`
yourself (ASK → best option label; ERR → retry|skip|investigate; IDLE →
resume|close; else empty) and persist it so the next read is free:

```bash
ainb fleet enrich-cache put --key "<card.enrich_key>" --suggestion "<text>"
```

Then render from `$out`, substituting your drafted suggestions. No workflow, no
subagent. For `stale == 0`, skip drafting entirely.

**0-token mode:** `ainb fleet needs --no-enrich` (or `AINB_FLEET_ENRICH=0`)
returns cards with no `need_enrich` flags — render cached suggestions only.

## Step 1 — run the jarvis workflow with verb=needs (only when stale > 6)

`hangar` is the single multi-verb workflow under `ainb-fleet`. The `needs`
verb runs the Discover ▸ Enrich ▸ Prioritize chain and returns the render-ready
panel data. Other verbs (`standup`, `sequence`) are wired into the same workflow.

```
Workflow({ name: 'ainb-fleet:jarvis', args: { verb: 'needs' } })
```

It runs in background; wait for completion. The result is render-ready:

```jsonc
{ "banner": { "need", "err", "ask", "idle", "wait", "top": {session,kind} },
  "cards":  [ { "emoji", "kind", "session", "line", "enriched", "options?" } ],
  "asks":   [ { "question", "header", "options[{label,description}]",
               "multiSelect", "suggestion", "route": {target,hint} } ] }
```

## Step 1.5 — handle a read failure

If the result has a non-empty `error` field, the fleet read genuinely failed
(e.g. an API throttle that persisted across the workflow's retries) — this is
NOT an empty fleet. Render the error, do not show "0 NEED YOU":

```
╔════════════════════════════════════╗
║  ⚠ FLEET READ FAILED               ║
║  <result.error>                     ║
║  retry in a moment                  ║
╚════════════════════════════════════╝
```

Then stop (offer to re-run). Only proceed to Step 2 when `error` is absent.

## Step 2 — render the Jarvis HUD

From `banner` + `cards`, render this exact layout in chat:

```
╔════════════════════════════════════╗
║  ⚡ FLEET STATUS · N NEED YOU ⚡    ║
║  🔴 X err  🟡 Y ask  ⚪ Z idle      ║
║  top: <banner.top.session> (<KIND>) ║
╚════════════════════════════════════╝

▸ <emoji> <session> ─ <line>
    [suggest: <enriched>]
    ① <option>  ② <option>  ③ <option>      (ASK only)
```

Rules:
- Banner always present (0 → "0 NEED YOU", skip top line).
- One `▸` card per `cards[]` entry, in order (already priority-sorted).
- `[suggest: …]` line only when `enriched` is non-null.
- ASK cards show `options` as ① ② ③ ④ ⑤.
- Cap at 10 cards; if more, render 10 then `+ N more`.

## Step 3 — fire AskUserQuestion (batched ≤ 4)

`asks[]` are AskUserQuestion-ready. **The tool caps at 4 questions per call** —
batch in groups of 4 (highest-priority first, asks[] is already sorted):

```
for batch of up to 4 asks:
  AskUserQuestion({ questions: batch.map(a => ({
    question: a.question, header: a.header,
    options: a.options, multiSelect: a.multiSelect })) })
```

When `suggestion` is set, mention it ("recommended: <suggestion>") so Stevie
can one-tap the drafted choice.

## Step 4 — route answers back

Two routes, picked by ask `kind`:

### 4a — ASK kind → **key-route** (click the target's picker)

The target session already has its own AskUserQuestion picker open and is
waiting for `Down`/`Enter`. Sending the answer as TEXT would land as a fresh
user message the target then has to re-interpret. Worse, the broker-delivery
path has a known gap. So for ASK answers, **press the picker keys directly via
tmux send-keys** — the same UI keystrokes the human would use:

```bash
# idx = 0-based index of the option Stevie picked in asks[].options
# target = the ask's route.target (the target's tmux_session)
for _ in $(seq 1 "$idx"); do tmux send-keys -t "$target" Down; done
tmux send-keys -t "$target" Enter
```

The picker explicitly advertises `Enter to select · Tab/Arrow keys to navigate`
in its footer — that's the contract.

**Caveats:**
- The picker must still be the active surface in the target pane. If the
  target moved on (Esc'd / scrolled away), arrow-Enter lands somewhere
  unintended. `route.hint == 'broker'` doesn't help — the underlying race
  is the same.
- Option ordering matters and is preserved by the workflow (the target's
  `context.options[]` is returned verbatim).

### 4b — ERR / IDLE / WAIT kinds → **text-route via tmux send-keys**

No picker on the target; the answer is a normal prompt. Per the read/write
principle, prefer tmux directly — it lands reliably and is verifiable via
capture-pane. Broker is fallback only.

```bash
# default: write via tmux, verify via capture-pane
tmux send-keys -t "$target" -l "<answer text>"
tmux send-keys -t "$target" Enter

# verify it landed in the pane (the matching read)
tmux capture-pane -t "$target" -p -S -40 | grep -F "<answer text>" && echo "✓"
```

Only when `tmux_session` is unknown (rare — bg jobs, dead tmux), fall back to:

```bash
ainb fleet broadcast "<answer>" --filter "<route.target>"   # broker — last resort
```

### Verify the write landed (capture-pane is the matching read)

```bash
# the read leg that matches the write
tmux capture-pane -t "$target" -p -S -50 | grep -F "<answer>"
# slower confirmation: target's JSONL transcript records the user message
ls -t ~/.claude/projects/<cwd-slug>/*.jsonl | head -1 | xargs grep -F "<answer>"
```

## Caveats

- **AskUserQuestion is session-only** — the workflow cannot fire it; that's why
  this skill exists. The workflow only produces the `asks[]` shape.
- **Enrich cost** — one Haiku agent per blocked session, unbounded. A 17-session
  fleet measured ~920k tokens / ~175s on a cold run; resume is ~0 tokens.
- **Race window** — a just-answered session may reappear once before its tail
  moves; always-fresh discover re-reads live each invocation.
- **`unknown` session targets** — sessions ainb can't name (bg jobs, dead tmux)
  surface with `route.target: "unknown"`; those can't auto-route — tell Stevie.
