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.
Installs into .claude/skills of the current project.
Are you the author of Fleet Needs?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/stevengonsalvez-fleet-needs)
---
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.