agentsclimarketplace

Agent

Skill bllshttng/footnote/skills/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

Install
npx -y skills add bllshttng/footnote --skill agent

Assembled 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

<!-- style-exception: mechanical verb rename preserves pre-existing prose -->

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): bare and discuss are gone. A conversational session is just spawn "<seed>" (the default); a one-shot Q&A to a NEW worker is spawn "<question>" headless. (The --ask/--discuss normalize flags remain hard errors.)

ask is back with different semantics. The OLD ask (one-shot Q&A via a throwaway worker) stays retired. Today's ask <name> "<question>" is the synchronous request-reply lane to an EXISTING worker: it runs fno agents ask, which delivers into the live session, waits for the reply (default 600s), and prints it on stdout. Async fire-and-receipt stays on send (fno agents mail send). See the ask section for the boundary.

VerbEnvelopeRoutes toCost
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 writefno agents mail send (the addressed jsonl bus, sender-excluded)free
ask <name> "..."parse + refuse-empty + honest reply relayfno agents ask - sync deliver into a live worker + reply-wait poll; reply on stdoutfree
watch <name>thin pass-throughfno agents watchfree
listthin pass-throughfno agents listfree
whoamithin pass-through (read-only)fno agents whoamifree
logs <name>thin pass-throughfno agents logsfree
stop <name>confirm (destructive) + pass-throughfno agents stopfree

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)

ProviderDispatchWorkerReceipt
claudefno 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)
codexfno agents spawn (exec) / fno agents host (-i); headless -> codex --execdaemon-managed PTY workerpretty JSON .short_id
geminifno agents spawn (exec) / fno agents host (-i); headless -> agy -pdaemon-managed PTY workerpretty 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 /target wrap. (x-cbb0: spawn "fix the login bug" seeds a session; it does NOT build. To build free text, write spawn /target <text> or pass a node id.)
  • one-shot Q&A: append the headless substrate (x-cbb0: it replaced the old one-shot ask and bare) - spawn "<question>" headless returns a single reply. (Messaging an EXISTING worker for an inline reply is the ask verb, 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 -> provider codex; 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 its VALID_PROVIDERS (which mirrors the Rust KNOWN_PROVIDERS source of truth), so a new harness needs no grammar edit. docs/harness-command-matrix.md is the per-provider capability truth. Quotes protect a trailing word that is ambiguously a provider. (megawalk drivers like hermes/openclaw are a different axis, not spawn providers.)
  • drive (alias interactive, optional): route codex/gemini to a drivable host session instead of an autonomous spawn. No-op for claude. "drive it" / "interactive" map here.
  • yolo (aliases auto, -Y, --yolo, optional): typing yolo drops 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 for yolo only when you genuinely want no sandbox. For claude (which has no --yolo flag) 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-mode you 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, set config.agents.<provider>.headless_yolo: true.)
  • model <name> (optional): exact model for the worker, plumbed to fno 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 after model). Default = the provider's default. There is NO short flag: -m is --allow-merge, so a bare -m opus would set merge, not the model - always write model <name>.
  • effort <value> (optional): reasoning-effort tier, plumbed to fno agents spawn --effort. It is orthogonal to model: 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. (merge only 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 to fno agents spawn --permission-mode (claude default|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|consolidate route 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 to fno 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-place is the explicit opt-in to keep the caller's cwd (extend my WIP right here); --fresh is 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 effective cwd when 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 the error= 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 /target wrap, no no-merge, no build inference. This is what spawn "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, NO no-merge. See the handoff section.
  • passthrough (leading /): the explicit command, normalized per harness for ANY footnote verb (x-a5e4) - /verb verbatim on claude/agy, /fno:verb on opencode, $fno:verb on codex (so /agent spawn /blueprint -H codex runs 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.

  1. node non-empty (tier 1 exact id) -> use it as-is.
  2. spawn_next=1 (tier 5) -> fno backlog next $( [[ "$next_scope" == all ]] && echo --all || true ) (it already emits JSON; there is no -J flag on next). Take .id. If it is null/empty, report "no ready node" and launch nothing (AC3-ERR). Otherwise that id is the node.
  3. node_query non-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 verbatim seed (its payload_mode/message from 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. If node is non-empty, run fno 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-f are letters, so re-added and dead-beef match it - or a /target <prose> line the extraction grep'd a word out of). Degrade to node="" with a one-line note and spawn on the message as typed; only the receipt loses its node suffix.

    /target <id> with nothing else reports node_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 node is non-empty, run fno agents claim status "node:$node" --json and inspect .state and .holder. If live, 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>" to spawn.sh so it emits a self-handoff receipt instead of a confusing already-running. /agent does NOT reassign the node from here: a node claim can be released only by the two sanctioned sites (handoff.sh or fno backlog unclaim, holder-verified - a helper subprocess release is an authority violation), and a bg spawn cannot emit the delegated event 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 run fno 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.sh re-checks atomically too: it treats a live-claim whose holder matches --self as a self-handoff and refuses any other.)

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:

ConditionResult
posture auto (default) or neverno confirm (free lane)
posture always (cautious opt-in)confirm even the free lane
-y/--yes passedskip (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 callerskip
config read fails / invalidno 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:

  1. normalize's resolved_cwd when non-empty (a -C/--project target, including a -f/--force override of a node's own cwd) - it wins over the node's cwd;
  2. else the node's _resolved_cwd (from fno backlog get "$node");
  3. 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 real short_id and give fno agents logs <name> + fno agents trace <name>. When the line also carries pane="<session>:<pane_id>" (a mux-pane worker), it has no log_path - relay the pane ref and give fno mux attach <session> (the line's own hint=) instead of fno 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). Give fno agents grid <name> / drive <name> for live interaction and fno agents mail send for asynchronous follow-up.
  • result=launched ... mode=interactive ... -> a drivable session staged, NOT running yet. Give fno agents grid <name> (or fno agents drive <name> --mode interactive). Note: no proactive push when it waits on input.
  • result=replied ... (an explicit headless substrate 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 in fno 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 and fno mux attach <session> (the line's own hint=), and say the identity binds on the next fno agents reconcile. The short_id on this line is the requested name, not a session handle, so do NOT present it as one and do NOT offer resume or 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:agent cannot reassign it from here and nothing was launched. Relay the reason and the two real exits it names: hand off via /fno:target self-handoff (which archives state, emits the delegated event, and releases the claim atomically), or fno 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_id is parsed with jq; bg requires a whole-string 8-hex id. The default/pane lane accepts the runtime's identifier-shaped handle: Rust returns a name-slug short_id; Python pane receipts have an empty worker-socket id, so spawn.sh uses the receipt's registry name only 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)

  1. NORMALIZE. Strip the leading handoff verb; pass the rest as the doc path. Run normalize.sh --input "<doc-path>" --handoff (carry --provider/as <name>/model <name>/yolo only if given). Provider resolution is explicit -> configured -> Claude, constrained to Claude/Codex/Gemini. It emits payload_mode=handoff and a message that is the continuation seed (path + "do not re-derive" + GUARDRAIL + PR-for-review). On status=error (empty path or explicit unsupported provider), STOP and report the error= line.

  2. 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.

  3. 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]
    
  4. REPORT the real receipt exactly as the spawn section's REPORT does (result=launched ... mode=spawn -> quote the real short_id and always give fno agents logs <name> plus grid/drive for the default pane; result=pending -> the pane exists and runs but has no bound identity yet, so relay the pane ref and fno 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): discuss no 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-shot ask is gone too; that shape is the headless substrate (spawn "<question>" headless returns a single reply). Today's ask verb is the sync request-reply lane to an existing worker (its own section above). handoff stays: 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.

  1. 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 trailing timeout <secs> bareword maps to --timeout.

  2. Run the genuine wire:

    fno agents ask "<name>" "<question>" [--timeout <secs>]
    
  3. 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> or fno 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 /mail skill for messaging (send / reply / unread / ack), which is the runner-less front door over 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.

  1. Parse send project <project> --kind <kind> "<body>" as project mode and parse send <agent> [--kind <kind>] "<body>" as agent mode.
  2. Refuse an empty recipient, project, kind value, or body before running anything, but do not interpret any body token as grammar.
  3. Map project mode to fno agents mail send --to-project "<project>" --kind "<kind>" "<body>" and agent mode to fno agents mail send "<agent>" "<body>", inserting --kind "<kind>" only where the user explicitly supplied it.
  4. Pass every syntactically valid explicit kind to the genuine CLI, including question or fyi with an agent recipient.
  5. Let the CLI remain the single kind/addressee authority: it resolves an agent-scoped heads-up to the recipient's canonical session handle before the durable write, accepts all three project kinds, and refuses agent-scoped question or fyi before any durable write.
  6. 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.
  7. 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)

  1. Never fabricate a receipt. Report ONLY a short-id / message-id / reply that the helper or fno agents command actually captured. "No receipt" is FAILED. This is the cardinal guard.
  2. claude stays on the subscription lane. claude spawns via fno agents spawn --harness claude (-> client-side claude --bg --name). NEVER -p / --bare, and NEVER route claude through host.
  3. The spawn confirm posture is config.agents.confirm (default auto), via confirm-decision.sh. Auto-skip is node-id-only and caveat-free; a caveat always confirms under auto; -y/--yes and unattended callers skip; a failed read degrades to always. Every skip path still echoes the exact command. send/reads never confirm; stop always confirms (destructive).
  4. Sandboxed by default. Append --yolo only when the user explicitly passed it; never infer it from the payload or provider.
  5. Do not reinvent provider routing or the bus. Provider resolution lives in normalize.sh; addressed delivery lives in fno 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) and fno agents logs <name>.
  • interactive (-i): the TUI genuinely waits at approval prompts. See it via fno agents grid <name> / fno agents drive <name> --mode interactive.
  • no proactive push fires for a codex/gemini worker today. When a claude --bg /target worker stalls, it does fire fno inbox notify.

Known Limitations and Deferred Work

  • Fleet caps can reject spawns before worker creation. See LIMITATIONS.md.

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.