| 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 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 (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)
[ -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.
out=$(ainb --format json fleet needs)
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:
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:
{ "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: ") 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:
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.
tmux send-keys -t "$target" -l "<answer text>"
tmux send-keys -t "$target" Enter
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:
ainb fleet broadcast "<answer>" --filter "<route.target>"
Verify the write landed (capture-pane is the matching read)
tmux capture-pane -t "$target" -p -S -50 | grep -F "<answer>"
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.