Agent
Natural-language remote control for the `fno agents` worker mesh. Describe the outcome you want - spawn or hand off work, message or inspect workers, drive a session, or stop it - and this skill resolves and runs the provider- and substrate-specific CLI command. It normalizes arguments, applies confirmation policy, and reports genuine receipts. Use when: 'spawn a worker for ab-XXXX', 'seed a session with this prompt', 'hand off this doc to codex', 'show my running agents', 'drive the reviewer', 'stop the billing worker'.From its SKILL.md
npx -y skills add bllshttng/footnote --skill agentAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
48.4 KB, as published. Nobody here has run it
Agents
Describe the agent outcome you want; this skill remembers the command.
/fno:agent is the natural-language front door over the shipped fno agents
mesh. The CLI is intentionally precise, but its provider, substrate, lifecycle,
and receipt shapes are too much syntax to keep in working memory. State the
intent in ordinary language; the skill selects the real verb and arguments.
You (the agent) are the runner. Read the user's desired outcome, route it to
the right verb, normalize it, confirm only costed or destructive operations, run
the genuine fno agents command, and report the real captured receipt -
never a fabricated one.
This skill REUSES the shipped fno agents primitives. It does not reimplement
spawn, the addressed bus, or session observation. Its value is verb routing +
input normalization + confirmation policy + honest reporting. Smart-quote and
dashless parsing also keep it reliable from remote or runner-less controls.
SKILL_DIR below is skills/agent inside this plugin.
Verb router
The first whitespace token of the argument is the verb. An unrecognized
leading token means the whole argument is a spawn payload (this preserves the
bare /agents <task> ergonomics the former dispatch front door had). spawn
means start a session with what you pass, nothing more (x-cbb0): an explicit
/skill command is dispatched per harness, a resolved backlog node id builds
via /target, and any other free text is sent verbatim as the session seed -
no /target wrap, no build inference. The focused core:
Retired verbs (x-cbb0):
bareanddiscussare gone. A conversational session is justspawn "<seed>"(the default); a one-shot Q&A to a NEW worker isspawn "<question>" headless. (The--ask/--discussnormalize flags remain hard errors.)
askis back with different semantics. The OLD ask (one-shot Q&A via a throwaway worker) stays retired. Today'sask <name> "<question>"is the synchronous request-reply lane to an EXISTING worker: it runsfno agents ask, which delivers into the live session, waits for the reply (default 600s), and prints it on stdout. Async fire-and-receipt stays onsend(fno agents mail send). See theasksection for the boundary.
| Verb | Envelope | Routes to | Cost |
|---|---|---|---|
spawn (default) | normalize + honest-receipt (no confirm: free lane) | fno agents spawn - substrate axis (x-2c27): default pane (owned-PTY drivable); trailing bg -> detached claude --bg thread; trailing headless -> one-shot (claude -p / codex --exec / agy -p) | free (claude subscription) |
handoff <doc> | normalize --handoff + honest-receipt (free lane) | fno agents spawn (Claude/Codex/Gemini continuation seed, NO /target; default pane) | free (provider subscription) |
send <name> "..." | normalize recipient + addressed write | fno agents mail send (the addressed jsonl bus, sender-excluded) | free |
ask <name> "..." | parse + refuse-empty + honest reply relay | fno agents ask - sync deliver into a live worker + reply-wait poll; reply on stdout | free |
watch <name> | thin pass-through | fno agents watch | free |
list | thin pass-through | fno agents list | free |
whoami | thin pass-through (read-only) | fno agents whoami | free |
logs <name> | thin pass-through | fno agents logs | free |
stop <name> | confirm (destructive) + pass-through | fno agents stop | free |
Advanced lifecycle intents (drive/grid/attach/resume/reconcile/rm/
ack/promote) map to their corresponding raw fno agents verbs; the skill
resolves their arguments and runs them rather than reimplementing them. The
capability matrix at docs/harness-command-matrix.md remains the per-provider
truth. For the full subcommand map - which are human-facing vs
machine-internal vs exploratory channel infra - see
references/fno-agents-subcommands.md.
Route on the verb, then run the matching section below.
spawn (default verb) - launch a worker or seed a session
Start a session with what you pass (x-cbb0): a /skill passthrough, a node-id
build, or a verbatim free-text seed. Strip a leading spawn verb if present;
otherwise the whole argument is the spawn payload.
Providers (all three are first-class)
| Provider | Dispatch | Worker | Receipt |
|---|---|---|---|
claude | fno agents spawn (default pane owned-PTY; bg -> claude --bg; headless -> claude -p) | owned pane, or backgrounded /target thread (bg) | compact JSON .short_id (reply on headless) |
codex | fno agents spawn (exec) / fno agents host (-i); headless -> codex --exec | daemon-managed PTY worker | pretty JSON .short_id |
gemini | fno agents spawn (exec) / fno agents host (-i); headless -> agy -p | daemon-managed PTY worker | pretty JSON .short_id |
All three create via spawn. The substrate axis (x-2c27) selects the host:
pane (default, owned-PTY drivable), bg (claude --bg; opencode serve session
thread), headless (one-shot claude -p / codex --exec / agy -p). bg on a
non-claude provider is a hard error pointing to headless. A one-shot Q&A is the
headless substrate (x-cbb0: it subsumes the retired one-shot ask; today's ask verb is the sync lane to an existing worker). A codex/gemini
exec worker is a single autonomous pass, not the claude "refuse to stop until
shipped" loop - do not imply loop-grade completion guarantees for them.
Every spawn captures (best-effort, non-blocking) the worker's full resume UUID
into the registry, distinct from the 8-hex short-id, so a worker is a complete,
identified citizen of the mesh that can later be addressed or escalated.
Inputs (dashless grammar - phone-first)
The documented grammar carries zero dash tokens so a phone keyboard's smart
punctuation (which rewrites -- into a long dash and mangles a lone -) can
never corrupt it. The user types barewords or natural language; you map them to
the structured fields. The canonical form:
<payload> [provider] [drive] [yolo] [model <name>] [effort <value>] [as <name>] [merge]
The angle brackets are placeholder notation, not literal syntax. Infer the fields from the user's unquoted text (a free-form phrasing maps the same way - see below):
- payload (required): a backlog node id (
ab-XXXXXXXX, builds via/target), an explicit slash command (/target ...,/pr check 42, dispatched per harness), or any other free text - sent verbatim as the session seed (a live pane), no/targetwrap. (x-cbb0:spawn "fix the login bug"seeds a session; it does NOT build. To build free text, writespawn /target <text>or pass a node id.) - one-shot Q&A: append the
headlesssubstrate (x-cbb0: it replaced the old one-shot ask andbare) -spawn "<question>" headlessreturns a single reply. (Messaging an EXISTING worker for an inline reply is theaskverb, not a spawn.) - provider (optional, trailing bareword): any non-claude harness in the
supported set - today
codex,gemini,agy,opencode, with more coming.spawn ab-X codex-> providercodex;spawn ab-X opencode->opencode. Default resolves from config ->claude. "on codex" / "with gemini" map here. The set is not hardcoded here: normalize matches any bareword in itsVALID_PROVIDERS(which mirrors the RustKNOWN_PROVIDERSsource of truth), so a new harness needs no grammar edit.docs/harness-command-matrix.mdis the per-provider capability truth. Quotes protect a trailing word that is ambiguously a provider. (megawalk drivers likehermes/openclaware a different axis, notspawnproviders.) drive(aliasinteractive, optional): route codex/gemini to a drivablehostsession instead of an autonomousspawn. No-op for claude. "drive it" / "interactive" map here.yolo(aliasesauto,-Y,--yolo, optional): typingyolodrops the sandbox for this launch - codex runs--dangerously-bypass-approvals-and-sandbox, gemini runs bare--yolo(unsandboxed full-auto). You rarely need it: with NO flag, a headless codex/gemini worker is already BOUNDED - sandboxed AND never-prompt (codex--sandbox workspace-write --ask-for-approval never, gemini--approval-mode yolo --sandbox) - so it neither hangs nor roams outside the workspace. Reach foryoloonly when you genuinely want no sandbox. For claude (which has no--yoloflag) it maps to--permission-mode bypassPermissions, the equivalent full-auto/no-gates posture, so a yolo'd claude worker runs gate-free instead of stalling on a permission prompt; an explicit--permission-modeyou pass wins over this default. "full auto" / "no sandbox" / "unsandboxed" map here. (To make full yolo the standing default for a provider instead of per-launch, setconfig.agents.<provider>.headless_yolo: true.)model <name>(optional): exact model for the worker, plumbed tofno agents spawn --model(each provider's own--model). Two-word posture so a model name that is not a posture word is read as the value:spawn ab-X model opus,spawn ab-X codex model gpt-5. "on opus" / "use sonnet" map here (name the model aftermodel). Default = the provider's default. There is NO short flag:-mis--allow-merge, so a bare-m opuswould set merge, not the model - always writemodel <name>.effort <value>(optional): reasoning-effort tier, plumbed tofno agents spawn --effort. It is orthogonal tomodel: the model selects which model runs, while effort tunes how hard it reasons. The CLI validates the provider-specific vocabulary and rejects unmappable values before spawning.as <name>(optional): explicit agent name. Default is derived (<verb>-<node-id>-<slug>/<verb>-<slug>). "call it X" / "name it X" map here.merge(optional): do NOT inject the no-merge intent. Default injects it so a fire-from-phone autonomous worker lands a PR for review, not an auto-merge. (mergeonly omits the no-merge intent; true auto-merge stays a separate opt-in.) "let it merge" / "can merge" map here.--permission-mode <v>(optional): the worker's harness permission posture, passed straight tofno agents spawn --permission-mode(claudedefault|acceptEdits|plan|bypassPermissions; codex/gemini/opencode/agy mapped). Value validation is the CLI's (fail-closed), not the skill's.-r/--role <role>(optional): per-spawn model-routing role (coordinate|tidy|orient|consolidateroute to the secondary GLM; production roles stay primary). Model routing ONLY - it does not affect pane/layout.-t/--timeout <secs>(optional): per-spawn timeout, passed tofno agents spawn --timeout(CLI default 600).--here/--fresh(optional): a spawn with NO cwd source defaults to the canonical main root (x-85fe), so the same command behaves identically wherever the launcher stands.--here/--in-placeis the explicit opt-in to keep the caller's cwd (extend my WIP right here);--freshis an accepted no-op alias (the default is already canonical). The move is never silent: a redirect note names the canonical dir and the spawn receipt carries the effectivecwdwhen it differs from the caller. A code payload still auto-isolates to a fresh worktree regardless (that lands as an explicit--cwd, which wins).- Tier-3 harness passthrough (all optional, x-b6e2): forwarded straight to
fno agents spawn, opaque to the skill; the CLI maps or fails closed per provider.--add-dir <dir>grants the worker extra write access (claude/codex/ agy; additive to its own workspace).--agent <name>pins its sub-agent (claude/opencode).--tools <list>/--deny-tools <list>scope its tool set (claude--allowedTools/--disallowedTools). A no-equivalent provider cell is rejected before spawn.
normalize.sh is the deterministic backstop: it recognizes this closed posture
vocabulary as a contiguous TRAILING run (right-anchored), so a posture word that
sits mid-task ("spawn the node that will merge two branches") stays task text and
does NOT silently set the posture. A bare as with no name after it is an error.
Dash-flags still work (undocumented back-compat). A desktop scripter's
existing --yolo / -n <name> / -m / --model <name> / -i / -y
invocations are canonicalized to the same fields (-y/--yes is accepted and
ignored - the free lanes no longer confirm, see CONFIRM). --model has no short
form (-m is --allow-merge). A genuinely unknown dash-flag fails loud.
Dashless is the only form you advertise; never instruct a user to type a dash.
Flow: NORMALIZE -> RESOLVE -> VALIDATE -> CONFIRM -> SPAWN -> REPORT
1. NORMALIZE (deterministic helper)
Run the normalizer with the spawn payload text exactly as given (pass it as ONE
argument). You may pass trailing posture barewords inside --input and let the
helper extract them, or pass the fields you inferred from natural language as the
internal flags below - the helper resolves both to identical fields, so either
path is safe. ALWAYS pass --handoff for a handoff verb, so the verbatim doc
path skips posture parsing.
bash "${SKILL_DIR}/scripts/normalize.sh" --input "<raw payload [posture barewords]>" \
[--provider <p>|-P <p>] [-n <n>|--name <n>] [--model <m>] [--effort <e>] [-i] [--yolo] [--handoff] \
[-m|--allow-merge] [-y|--yes] [-C <project>|--project <project>] [-f|--force]
It strips smart quotes, parses the trailing dashless posture run, canonicalizes a
token-initial em/en-dash to --,
derives the agent name (<verb>-<node-id>-<slug> for a node, <verb>-<slug> for free text),
resolves the provider (explicit -> config.accounts via the shipped
resolve_dispatch_target -> claude), detects the node id, resolves a
-C/--project target to its work-map resolved_cwd, picks the payload
mode, and assembles the provider-aware message. Read its key=value output
line by line. Never eval it. Captured fields: node, node_query,
spawn_next, next_scope, name, provider, mode, yolo,
yes, allow_merge, project, resolved_cwd, payload_mode, message.
The model-adjacent fields are model and effort; omit either downstream when
empty so provider defaults remain unchanged.
Cross-project target (-C/--project). By default a free-text spawn launches
in the caller's cwd. -C <project> retargets it: normalize resolves the registry
name/short_name (config.work.workspaces.*.projects in config.toml) to that
project's work-map root and emits resolved_cwd, which you pass to
spawn.sh --cwd (see SPAWN). A natural-language in <project> / in the <project> repo / as <project> is YOUR job to translate: when you judge the phrasing is a
project directive (not task prose like "fix the bug in etl"), call normalize with
-C <project>; the deterministic layer only ever sees the unambiguous flag.
Unknown project, or a mapped path missing on disk, is a loud status=error (no
spawn). A backlog node carries its OWN project, so a node + -C is a refused
conflict UNLESS you also pass -f/--force (flag-win override: run the node's
work in the forced repo). When you re-run normalize after resolving a slug/next
to an ab-id (see RESOLVE), carry -C/-f through so the conflict check fires on
the real node.
-P is the provider shorthand (-P <harness>, identical to --provider):
it selects the harness in this layer - the axis --harness shares, which spawn
forwards as --harness. It is not the project flag: an earlier -P=--project
mapping silently misrouted -P zai to a cross-project hop, so project moved to
-C. Note this is the skill-layer provider/harness axis, not the separate
model-vendor --provider the fno agents spawn CLI exposes (where -H selects
the harness and -P the vendor); z.ai is a model route selected via
--model glm-5.2, so -P zai errors here because z.ai is not a harness.
- If
status=error, STOP. Report theerror=line. Do NOT spawn. - Otherwise capture every field for RESOLVE/VALIDATE/CONFIRM/SPAWN.
Payload modes (chosen deterministically by normalize; x-cbb0):
- seed (default for free text): sent VERBATIM as the session opening seed on
the default pane - no
/targetwrap, nono-merge, no build inference. This is whatspawn "fix the login bug"produces (it seeds a session; it does NOT build). To build free text, pass an explicit/target <text>(passthrough) or a node id. - build (a resolved node id): the ONE surviving implicit
/target, config- driven not shape-inferred - dispatch_verb resolution (node > config > builtin/target). Normalized per harness (x-a5e4): claude/agy get/target <id>(+no-merge); opencode/fno:target <id>; codex$fno:target <id>- all run the real pipeline on the node. gemini is deprecated (successor: agy) and refused. - handoff (
--handoff): a doc path becomes a continuation seed (read the doc, continue from where it left off, do NOT re-derive a plan) + a standing guardrail against autonomous outward/irreversible actions. Supports Claude, Codex, and Gemini; NO/target, NOno-merge. See thehandoffsection. - passthrough (leading
/): the explicit command, normalized per harness for ANY footnote verb (x-a5e4) -/verbverbatim on claude/agy,/fno:verbon opencode,$fno:verbon codex (so/agent spawn /blueprint -H codexruns the real skill). gemini is deprecated and refused (route to claude/codex/opencode/agy).
1b. RESOLVE (id-free entry modes -> a concrete node id) (ab-f82e8083)
A backlog node id is opaque and matches ^[a-z][a-z0-9]{0,7}-[0-9a-f]{4,8}$, so
a repo's configured id_prefix/id_hex_width classifies exactly like ab-.
Two id-free ways to name a NODE resolve to a concrete id here, in order. Resolve
to ONE id, then re-run normalize with --input "<that id>" (carrying the same
--provider/--name/posture flags) so message/name/node are rebuilt as a
node build. Skip this whole step when node is already a concrete id. Free text
that names no node is NOT resolved here (x-cbb0: it is a verbatim seed, never a
fuzzy node search) - when node/node_query/spawn_next are all empty, take the
seed payload straight to SPAWN.
nodenon-empty (tier 1 exact id) -> use it as-is.spawn_next=1(tier 5) ->fno backlog next $( [[ "$next_scope" == all ]] && echo --all || true )(it already emits JSON; there is no-Jflag onnext). Take.id. If it isnull/empty, report "no ready node" and launch nothing (AC3-ERR). Otherwise that id is the node.node_querynon-empty (tier 2 slug candidate) ->fno backlog get "$node_query" --field id. On exit 0 that is the node (exact slug -> id): re-normalize with it for a node build. A bare hex token arrives here too - the resolver is format-agnostic and accepts it, so normalize never guesses a prefix. On a non-zero exit the slug missed - do NOT fuzzy-search; the token stays a verbatimseed(itspayload_mode/messagefrom normalize), so proceed to SPAWN with the seed as-is.
Termination invariant: a canonical id returned by the resolver MUST classify
as tier 1 on the re-normalize pass. Re-normalize exactly ONCE per resolution; if
the second pass still yields node_query (a repo whose ids fall outside the
shape), fall through to seed rather than resolving again.
After resolving to an id, re-normalize and continue. A resolution that names
a node in another project will boot the worker in that node's _resolved_cwd
(see SPAWN); the REPORT receipt names the resolved slug (ab-id) + project + cwd
so even a delegated next/all is never a silent surprise.
2. VALIDATE (refuse bad input before any billed launch)
-
Node must resolve, and a miss is judged by
node_bare. Ifnodeis non-empty, runfno backlog get "$node". On a miss (non-zero exit or no.id):node_bare=1- the payload was nothing but the id, so the user named it deliberately. STOP and tell the user. Do NOT spawn.node_bare=0- the id was inferred from a payload carrying other text (prose whose first word merely fits the id shape -a-fare letters, sore-addedanddead-beefmatch it - or a/target <prose>line the extraction grep'd a word out of). Degrade tonode=""with a one-line note and spawn on the message as typed; only the receipt loses its node suffix.
/target <id>with nothing else reportsnode_bare=1, so an explicit command naming a node that does not exist still refuses rather than burning a worker on it.The asymmetry is about what the user typed, not which tier matched. Refusing a spawn because ordinary prose happened to look like an id would break the free-text-is-always-a-verbatim-seed rule above.
-
Collision pre-check (read-only), with a self-handoff exception. If
nodeis non-empty, runfno agents claim status "node:$node" --jsonand inspect.stateand.holder. Iflive, decide whether the holder is a FOREIGN worker or YOURSELF handing off your own node:- Self-handoff (holder == your own claim): read your own holder from the
session manifest -
sed -n 's/^target_claim_holder: *"\?\([^"]*\)"\?/\1/p' .fno/target-state.md(empty if you hold no target claim). If it equals.holder, this is YOUR node, not a foreign collision - pass--self "<holder>"tospawn.shso it emits aself-handoffreceipt instead of a confusingalready-running./agentdoes NOT reassign the node from here: a node claim can be released only by the two sanctioned sites (handoff.shorfno backlog unclaim, holder-verified - a helper subprocess release is an authority violation), and a bg spawn cannot emit thedelegatedevent a clean takeover needs. For an immediate clean handoff use/target's self-handoff (it archives state, emits the delegated event, and releases the claim atomically); or runfno backlog unclaim <node>and re-dispatch. - Foreign collision (holder is someone else, or you hold no claim): a
worker already holds this node - tell the user (point at
fno agents logs $name) and do NOT spawn a second loop.
(
spawn.shre-checks atomically too: it treats alive-claimwhose holder matches--selfas a self-handoff and refuses any other.) - Self-handoff (holder == your own claim): read your own holder from the
session manifest -
A free-text seed (node empty) skips the node checks.
3. CONFIRM (free lane: no confirm by default)
spawn is a free, reversible lane: an autonomous worker lands a PR for
REVIEW (it never auto-merges; merge only omits the no-merge intent), so there
is nothing billed or destructive to gate. It therefore does not confirm by
default. An unattended/headless caller always skips. Otherwise let the helper
compute the decision:
bash "${SKILL_DIR}/scripts/confirm-decision.sh" \
--node "$node" --provider "$provider" --mode "$mode" \
--payload-mode "$payload_mode" --yolo "$yolo" --yes "$yes" \
--permission-mode "$permission_mode" --allow-merge "$allow_merge"
Pass --permission-mode "$permission_mode" so a gate-bypass launch still surfaces
its caveat: normalize maps --yolo on claude to permission_mode=bypassPermissions
and clears yolo, so the yolo caveat alone would go silent - the helper instead
caveats on the effective bypassPermissions mode (whether it came from --yolo or
an explicit --permission-mode).
It emits confirm_required (0|1), caveat (0|1), caveat_text, a warn line,
and reason. config.agents.confirm (always|auto|never, model default auto)
is repurposed to an opt-in "confirm even the free lanes" for a cautious
operator: only always confirms; auto (default) and never skip. A
failed/invalid read degrades to the no-confirm default (the free lane has nothing
to gate) with an fno doctor update hint - do not re-derive this; it lives in the
helper. The table it implements:
| Condition | Result |
|---|---|
posture auto (default) or never | no confirm (free lane) |
posture always (cautious opt-in) | confirm even the free lane |
-y/--yes passed | skip (accepted-and-ignored: the free lane already does not confirm) |
caveat present (codex/gemini exec / yolo / merge) | NOT a confirm - surfaces as a warn |
| unattended caller | skip |
| config read fails / invalid | no confirm (auto) + one staleness warning line |
When confirm_required=1, show the EXACT command you will run - including the
chosen spawn|host verb and --yolo when set - plus caveat_text when
caveat=1, and wait for [y/N]. On anything other than yes, STOP without
spawning. When confirm_required=0, skip straight to SPAWN; if warn is
non-empty, print it. The exact command is NOT hidden - REPORT echoes it on every
skip path.
4. SPAWN (genuine execution + honest receipt)
Only after a yes, run the spawn helper with the normalized fields. It re-checks
for a live duplicate, picks the verb from provider/mode/payload_mode, runs
the real fno agents command (name POSITIONAL, --provider, NEVER -p/--bare),
and parses the receipt deterministically:
bash "${SKILL_DIR}/scripts/spawn.sh" --name "$name" --provider "$provider" \
--message "$message" --mode "$mode" --payload-mode "$payload_mode" \
[--model "$model"] [--effort "$effort"] [--substrate "$substrate"] [--yolo] [--node "$node"] [--self "$self_holder"] [--cwd "<cwd source, see below>"] \
[--permission-mode "$permission_mode"] [--role "$role"] [--timeout "$timeout"] [--fresh] [--here]
Pass --self "$self_holder" only for a confirmed self-handoff (your
target_claim_holder matched the live .holder in the collision pre-check); it
makes the atomic re-check emit a self-handoff receipt that routes you to the
sanctioned handoff, instead of a confusing foreign already-running. It neither
spawns nor releases the claim. Pass
--model "$model" only when normalize emitted a non-empty model
(spawn.sh forwards it to fno agents spawn --model; omit it for the provider
default). Pass --effort "$effort" only when normalize emitted a non-empty
effort; spawn.sh forwards it to the fail-closed CLI mapper. Pass
--permission-mode "$permission_mode" / --role "$role" / --timeout "$timeout" only when normalize emitted a non-empty value; pass --fresh only
when fresh=1 and --here only when here=1 (spawn.sh forwards each to fno agents spawn, which validates values fail-closed). Pass --yolo only when
normalize emitted yolo=1. Pass --substrate "$substrate"
only when normalize emitted a non-empty substrate (bg -> a detached claude --bg thread; headless -> a one-shot claude -p / codex --exec / agy -p);
an empty substrate is the default pane (owned-PTY) and the flag is omitted.
Pass --node whenever node is non-empty. Choose the --cwd source in this priority order, so launch cwd
follows the work-map root:
- normalize's
resolved_cwdwhen non-empty (a-C/--projecttarget, including a-f/--forceoverride of a node's own cwd) - it wins over the node's cwd; - else the node's
_resolved_cwd(fromfno backlog get "$node"); - else the caller's
cwd.
Auto-worktree (x-9c4c). When the payload writes code and the resolved
--cwd is a repo's MAIN checkout, spawn.sh deterministically creates a
worktree on a fresh feature branch and launches the worker THERE - born
isolated, location verdict ok from line one, no reliance on the worker
self-creating a worktree. It forwards the payload's harness to fno workspace worktree ensure, so the location is whatever the per-project policy resolves to (a
claude payload lands harness-native under <repo>/.claude/worktrees/); never
assume a fixed base. "Writes code" is keyed off payload_mode,
not the message text: a node build dispatch (claude /target <id>, opencode
/fno:target <id>, or codex $fno:target <id>) and an explicit claude
/target|/execute|/fix passthrough all isolate; a seed/handoff and a non-code
claude slash command (/think writes a design doc) stay in repo root. An already-isolated
worktree cwd is not re-isolated; any creation error fails safe to repo root. This
is in spawn.sh (deterministic), so you do nothing here except relay the receipt
- its
cwd="<worktree>"field on the launched line surfaces the real launch dir.
Delegation for a self-isolating payload (x-6c22). One case skips the
pre-creation above: a claude /target carrying a resolved node. That
worker isolates itself at cold-start - fno do target start <node> runs fno worktree ensure and then the harness EnterWorktree tool - so spawn.sh
launches it at the repo ROOT and lets it own the worktree. Doing both would be
worse: EnterWorktree moves the session's cwd but leaves its PROJECT at the
launch dir, and claude keys ~/.claude/projects/ off that launch dir with no
rename hook, so pre-creating binds the project to the worktree and mints a
throwaway project dir per spawn that is orphaned when the worktree is reaped.
Isolation itself is unchanged - still a worktree off origin/main, and both
paths run the same fno workspace worktree ensure, so the per-project worktree policy
is honored either way. Only the timing, the owner, and the branch name move.
Three conditions, all load-bearing. claude, because EnterWorktree is a
Claude Code harness tool and a codex/opencode /fno:target worker cannot move
its session into a worktree it creates. A node, because fno do target start
has nothing to resolve without one - a free-text /target ship it never
isolates, it hits the location-refusal backstop. A literal /target
message, because payload_mode defaults to build, so a caller passing a
prose task without --payload-mode would otherwise be assumed to run /target
when it never will. Anything short of all three keeps pre-creation.
A delegated launch prints auto-worktree: delegated to the worker cold-start (launching at <root>) on stderr and carries NO cwd= receipt field, since no
worktree was made here.
5. REPORT (echo ONLY what actually happened)
Receipt-echo invariant (every skip path). When the confirm was auto-skipped
or bypassed, the receipt MUST echo the exact fno agents command that ran. No
launch path is ever invisible. If confirm-decision.sh emitted a warn,
include it here too.
Resolved-handle echo (ab-f82e8083, Locked Decision 5: echo always). When the
launch carries a node, the receipt MUST lead with the resolved
slug (ab-id) + project + cwd - read them from the fno backlog get "$node"
JSON (.slug, .id, .project, ._resolved_cwd), falling back to (ab-id)
when the node is not yet slugged. This holds on EVERY exact-tier + next path,
including the posture=auto silent-launch lane, so even a delegated next/all
or a cross-project resolution is visible post-hoc. (describe-it already confirmed
the handle before launch.)
Cross-project echo on the free-text path. When the launch carried no node but
normalize emitted a non-empty resolved_cwd (a -C/--project target), the
receipt MUST lead with project=<project> cwd=<resolved_cwd> (both already in
hand from normalize - no extra lookup). A free-text cross-project hop is then just
as visible as a node one; no launch silently lands in a different repo.
Read spawn.sh's single outcome line and relay it faithfully:
result=launched ... mode=exec ...-> a background worker launched. Quote the realshort_idand givefno agents logs <name>+fno agents trace <name>. When the line also carriespane="<session>:<pane_id>"(a mux-pane worker), it has no log_path - relay the pane ref and givefno mux attach <session>(the line's ownhint=) instead offno agents logs, which would miss.result=launched ... mode=spawn ...-> an autonomously seeded handoff worker launched through the provider's spawn lane. The default Rust substrate is a drivable pane; this is not a one-shot reply or a refuse-to-stop loop.result=launched ... mode=seed ...-> a verbatim free-text seed is running in a provider-native pane (the opening turn was sent as-is). Givefno agents grid <name>/drive <name>for live interaction andfno agents mail sendfor asynchronous follow-up.result=launched ... mode=interactive ...-> a drivable session staged, NOT running yet. Givefno agents grid <name>(orfno agents drive <name> --mode interactive). Note: no proactive push when it waits on input.result=replied ...(an explicitheadlesssubstrate one-shot) -> the one-shot returned its answer synchronously; the reply follows the outcome line and IS the deliverable. Relay it (preview ~15 lines; full reply infno agents logs <name>).result=pending ... pane="<session>:<pane_id>" ...-> a Codex pane was created and is running, but its rollout has not appeared yet so it has no addressable thread identity. This is a normal outcome, not a failure: report the pane as launched-but-not-yet-addressable, relay the pane ref andfno mux attach <session>(the line's ownhint=), and say the identity binds on the nextfno agents reconcile. Theshort_idon this line is the requested name, not a session handle, so do NOT present it as one and do NOT offerresumeor a session-keyed peek until it is bound.result=already-running ...-> a worker already exists for this node/name; no second loop was created. Point at its logs.result=self-handoff name=<n> reason="<...>"-> you already hold the claim on that node, so/fno:agentcannot reassign it from here and nothing was launched. Relay the reason and the two real exits it names: hand off via/fno:targetself-handoff (which archives state, emits the delegated event, and releases the claim atomically), orfno backlog unclaim <node>and then re-dispatch. Do not report this as a failure and do not retry the spawn.result=failed reason="<...>"-> report FAILED with the real reason. NEVER emit a short-id, fabricate a reply, or claim a worker launched.
Receipt families (keyed by the verb spawn.sh ran)
spawn.sh branches the receipt parse on the verb/mode it ran - never on
sniffing output (Group 1 ab-8b3e4fe0 put claude creation on spawn, so claude
takes the JSON .short_id family below, not a bare 8-hex line):
spawn --once/--substrate headless: a client-side one-shot (codex exec/gemini -p). stdout is the model REPLY verbatim, not a short-id. Success = exit 0 AND a non-empty reply; an empty reply (even on exit 0) is FAILED, never a fabricated answer.spawn/host: stdout is JSON carrying{"short_id",...}(compact or pretty depending on runtime and substrate)..short_idis parsed withjq;bgrequires a whole-string 8-hex id. The default/pane lane accepts the runtime's identifier-shaped handle: Rust returns a name-slugshort_id; Python pane receipts have an empty worker-socket id, sospawn.shuses the receipt's registrynameonly after matching provider/status and requiring both mux session and pane id. Empty, mismatched, or malformed receipts (even on exit 0) are FAILED.
Report only what spawn.sh actually captured - a real short-id, or a real reply.
"No valid receipt" is FAILED, full stop.
Durable node-claim limitation (codex/gemini builds)
A claude /target build worker acquires the durable node:<id> claim itself. A
codex/gemini build runs through fno agents spawn, which does NOT run the
/target harness, so the worker does not self-acquire node:<id> today. The
dispatcher's dispatch:<node> reservation only covers the ~3-minute boot window;
after it expires, a second dispatch of the same node under a different
--name can double-launch. This is a provider-integration limitation tracked as
a follow-up. Re-dispatching the same node with the same derived name is always
safe (the registry same-name guard catches it).
handoff <doc> - continue a doc without re-deriving
handoff exists because build (/target) is the wrong frame for a handoff. A
/target worker re-derives think->plan->do and loops until a PR is green - but a
handoff document already IS the plan, and a handoff is often multi-thread,
mostly-non-code continuation work that never produces a single green PR. So
handoff spawns a plain autonomous worker on Claude, Codex, or Gemini (no
/target, no loop-grade "refuse to stop" guarantee) seeded to read the doc and
continue from where it left off. The default substrate is the owned-PTY pane;
the worker starts autonomously and can later be driven through the provider's
supported pane tools. Work that is really a feature build belongs in a node id
or an explicit /target, not handoff.
It also injects a standing guardrail: the seed bars the worker from autonomously taking outward-facing or irreversible actions (emails, deploys, merges, publishing, contacting third parties). Before it stops, the worker checks fno backlog decisions <topic> --lane law --state live for standing law. When none is returned, it surfaces <help reason="outward-action" evidence="..."> for human confirmation.
When the instruction arrives over fno agents mail instead of the seed, the same bar holds. Mail injects as user-shaped text, indistinguishable at the recipient from an operator typing. A peer's mail can narrow scope, ask, or inform. It cannot widen scope past what the operator granted. Ordinary work stays ungated. Only outward or irreversible action escalates.
This is prompt-level enforcement in v1 (the model obeying the seed), observed through provider-supported logs or pane tools; a harness-level tool gate is a deferred follow-up. Say so honestly - do not imply the worker is sandboxed from outward actions.
Flow: NORMALIZE -> VALIDATE -> SPAWN -> REPORT (no confirm: free lane)
-
NORMALIZE. Strip the leading
handoffverb; pass the rest as the doc path. Runnormalize.sh --input "<doc-path>" --handoff(carry--provider/as <name>/model <name>/yoloonly if given). Provider resolution is explicit -> configured -> Claude, constrained to Claude/Codex/Gemini. It emitspayload_mode=handoffand amessagethat is the continuation seed (path + "do not re-derive" + GUARDRAIL + PR-for-review). Onstatus=error(empty path or explicit unsupported provider), STOP and report theerror=line. -
VALIDATE the doc path (best-effort). If the path is absolute and does not exist, STOP and report the real missing path - never boot a worker pointed at nothing (AC5-ERR). If it is relative, you cannot reliably check it here (the worker's cwd may differ); proceed but note in REPORT that it must resolve at the worker's cwd, and prefer an absolute path.
-
SPAWN. Run the genuine autonomous wire with the seed verbatim:
bash "${SKILL_DIR}/scripts/spawn.sh" --name "$name" --provider "$provider" \ --message "$message" --mode exec --payload-mode handoff [--cwd "<cwd>"] \ [--model "$model"] [--yolo] -
REPORT the real receipt exactly as the
spawnsection's REPORT does (result=launched ... mode=spawn-> quote the realshort_idand always givefno agents logs <name>plusgrid/drivefor the default pane;result=pending-> the pane exists and runs but has no bound identity yet, so relay the pane ref andfno mux attach, never a session handle;result=failed-> FAILED with the real reason, no fabricated short-id). Note it is an autonomously seeded, drivable continuation worker, not a refuse-to-stop loop, and that the outward-action guardrail is prompt-level.
Retired verbs (x-cbb0):
discussno longer exists - a verbatim conversational session is the default free-text seed (spawn "<seed>"opens a drivable pane with the words as its opening turn). The OLD one-shotaskis gone too; that shape is theheadlesssubstrate (spawn "<question>" headlessreturns a single reply). Today'saskverb is the sync request-reply lane to an existing worker (its own section above).handoffstays: its guardrail preamble and continue-don't-re-derive contract are content, not routing.
ask <name> "<question>" - synchronous reply from a live worker
The request-reply lane: deliver a message into an EXISTING worker's session,
wait for its reply, and relay it. This is fno agents ask (distributed as the
ln short-command) - NOT the retired one-shot ask, which spun up a throwaway
worker (that shape is spawn "<question>" headless).
ask vs send in one line: ask blocks and returns the ANSWER; send
returns a delivery RECEIPT and the answer arrives later as its own mail. Use
ask when you need the reply inline (pipeable stdout); use send for
cross-turn coordination or when the peer may be idle (durable fallback). Full
boundary table: docs/guides/fno-agents-ask-followup.md.
-
Parse the recipient name (first token after
ask) and the question (the rest; strip smart quotes as normalize does). Refuse an empty name or body before running anything. A trailingtimeout <secs>bareword maps to--timeout. -
Run the genuine wire:
fno agents ask "<name>" "<question>" [--timeout <secs>] -
REPORT honestly. stdout IS the recipient's reply verbatim - relay it (preview ~15 lines; full text via
fno agents logs <name>). On failure, relay the real exit meaning, never a fabricated answer:- exit 16: unknown agent - "spawn it first"; do NOT guess a recipient.
- exit 13: worker not running (the reason names why: not-found /
suspended / liveness-failed); offer
claude attach <short-id>orfno agents rm. - exit 15: reply timeout - the message WAS delivered (at-least-once);
point at
fno agents logs <name>rather than blind-retrying.
ask is free and never confirms (a read-reply exchange, nothing billed or
destructive).
send - message an agent or project over the bus
Prefer the dedicated
fno agents mail. This convenience verb keeps a spawn-and-message flow inside one skill.
The accepted forms are explicit and positional:
fno agents mail send "<agent>" "<body>"
fno agents mail send "<agent>" --kind heads-up "<body>"
fno agents mail send --to-project "<project>" --kind "<kind>" "<body>"
<kind> is one of heads-up, question, or fyi.
Only an explicit --kind token selects a kind.
A final body word such as question, heads-up, or fyi remains body text.
- Parse
send project <project> --kind <kind> "<body>"as project mode and parsesend <agent> [--kind <kind>] "<body>"as agent mode. - Refuse an empty recipient, project, kind value, or body before running anything, but do not interpret any body token as grammar.
- Map project mode to
fno agents mail send --to-project "<project>" --kind "<kind>" "<body>"and agent mode tofno agents mail send "<agent>" "<body>", inserting--kind "<kind>"only where the user explicitly supplied it. - Pass every syntactically valid explicit kind to the genuine CLI, including
questionorfyiwith an agent recipient. - Let the CLI remain the single kind/addressee authority: it resolves an agent-scoped
heads-upto the recipient's canonical session handle before the durable write, accepts all three project kinds, and refuses agent-scopedquestionorfyibefore any durable write. - On a CLI refusal, relay its stderr unchanged so the user sees the live-agent and project-note alternatives instead of a skill-layer paraphrase.
- On success, relay the real message id and routing receipt exactly as printed.
delivered (hosted) confirms that the message landed in the recipient session.
queued (durable) is not delivered; it is durable fallback awaiting the recipient's next active boundary or recovery.
Never upgrade queued mail to delivered, and never invent a receipt when the CLI printed none.
An unknown agent heads-up exits 16 and writes nothing, so report that exact failure without guessing another recipient.
send is free and never confirms because it is not a billed launch.
watch / list / whoami / logs - observe (thin pass-through)
Run the raw verb and relay its output faithfully. These are reads; no confirm.
fno agents watch "<name>" # follow a worker's output (claude-only)
fno agents list # the mesh: live + registered sessions
fno agents whoami # THIS worker's own registered name (+ enrichment)
fno agents logs "<name>" # the worker's transcript
whoami answers "what is MY registered name" - the handle peers use to address
you via fno agents mail send <name>. It reads FNO_AGENT_SELF (the env the spawn path
injects), so a worker that lost track of its name after compaction has a
fno-native answer. Exit 3 (not a registered mesh agent) for a human /
top-level session. Distinct from top-level fno whoami, which reports operating
context (fleet -> walker -> session), not the mesh name.
For a stream-json (headless) thread there is no TUI - observe via watch/logs,
never a terminal. If the raw verb errors, relay the real error; do not invent
output.
stop <name> - terminate (confirm, destructive)
Stopping a worker is destructive (it kills a running session), so confirm first
unless the caller is unattended/headless or passed -y/--yes:
About to stop a running worker:
fno agents stop <name>
This terminates the session. Proceed? [y/N]
On yes, run fno agents stop "<name>" and relay the outcome. On anything else,
STOP without stopping the worker.
Hard rules (non-negotiable)
- Never fabricate a receipt. Report ONLY a short-id / message-id / reply
that the helper or
fno agentscommand actually captured. "No receipt" is FAILED. This is the cardinal guard. - claude stays on the subscription lane. claude spawns via
fno agents spawn --harness claude(-> client-sideclaude --bg --name). NEVER-p/--bare, and NEVER route claude throughhost. - The spawn confirm posture is
config.agents.confirm(defaultauto), viaconfirm-decision.sh. Auto-skip is node-id-only and caveat-free; a caveat always confirms underauto;-y/--yesand unattended callers skip; a failed read degrades toalways. Every skip path still echoes the exact command.send/reads never confirm;stopalways confirms (destructive). - Sandboxed by default. Append
--yoloonly when the user explicitly passed it; never infer it from the payload or provider. - Do not reinvent provider routing or the bus. Provider resolution lives in
normalize.sh; addressed delivery lives infno agents mail send. This skill routes verbs and reports honestly; it does not duplicate that machinery.
Multi-CLI
This skill is Claude-Code primary. It needs fno agents (the daemon for
codex/gemini spawn/host and the bus) and claude --bg (the claude
subscription-lane spawn). On a CLI without them, the helpers report the failure
and the node stays re-dispatchable - they degrade honestly, never fake a launch.
See docs/SKILL-COMPAT-MATRIX.md.
Observability boundary (knowing a worker's state)
- exec (default): codex/gemini never surface a "waiting" state in the exec
lane. When an action needs approval, codex auto-rejects it and continues;
gemini aborts the run. Watch via
fno agents list(status ->exited) andfno agents logs <name>. - interactive (
-i): the TUI genuinely waits at approval prompts. See it viafno agents grid <name>/fno agents drive <name> --mode interactive. - no proactive push fires for a codex/gemini worker today. When a claude
--bg/targetworker stalls, it does firefno inbox notify.
Known Limitations and Deferred Work
- Fleet caps can reject spawns before worker creation. See LIMITATIONS.md.