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.
Scanned 9/3/2026
Install to Claude Code
npx -y skills add stevengonsalvez/claudecode-bootstrap --skill fleet-needs --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Fleet Needs?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/stevengonsalvez-fleet-needs)More formats (shields.io, HTML) on the badges page.
---
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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!