File-queue inbox (--inbox)
Experimental. The inbox is intended for external automation — scripts, CI hooks, schedulers, multi-machine deployments. The agents-fleet coordinator (the in-session LLM) has the native
spawn_workertool and should NOT round-trip through the inbox for in-session delegation. See the architecture diagram in the README for the canonical delegation flow.
The inbox is a third inbound surface for a running agents-fleet session (alongside the Ink REPL and the optional Telegram channel). It lets external agents — another CLI session on the same machine, a shell script, an MCP host, a cron job — inject tasks by dropping JSON descriptor files into a watched directory.
The dispatched tasks flow through the same coordinator path as REPL / Telegram input, so they appear in the chip strip, are tracked by IntelDatabase, and feed the evolution pipeline.
Quick start
Opt in at startup:
agents-fleet --telegram --inboxThe session prints a one-line dim notice on startup:
[inbox] watching /home/you/.fleet/inbox (poll every 2s) — anyone with write access to this directory can inject tasks (spawn_worker refused)From any other process on the same machine, drop a descriptor:
echo '{"version":1,"kind":"prompt","prompt":"Summarise today changes"}' \
> ~/.fleet/inbox/note-$(date +%s).task.jsonWithin ~2 seconds the file is renamed to *.task.json.processed and a sibling *.task.json.result is written confirming dispatch. The prompt shows up in the transcript prefixed with [via inbox: <descriptor-name>].
Descriptor format
Only *.task.json files are picked up. Any other extension is ignored.
kind: "prompt"
{
"version": 1,
"kind": "prompt",
"prompt": "your prompt text",
"createdAt": "2026-05-30T12:34:56.000Z"
}Dispatched via coordinator.sendPrompt(text) — the same path Telegram inbound text uses.
kind: "spawn_worker"
{
"version": 1,
"kind": "spawn_worker",
"agent_type": "general-purpose",
"name": "fix-issue-xyz",
"prompt": "Fix the broken X by editing src/foo.ts",
"task_id": "T-1"
}Refused by default (see Security). When trusted, the descriptor is rendered as a synthesised natural-language prompt and dispatched to the coordinator, which decides whether to invoke its own spawn_worker tool. This keeps every spawn flowing through the coordinator's permission gating, crew resolution and intel capture — the inbox can never spawn a worker that wouldn't be valid from Telegram or the REPL.
agent_type must be one of: general-purpose | explorer | coder | reviewer | tester | custom | coordinator | researcher.
Crew targeting (v2)
When a crew is active, the coordinator's spawn_worker tool resolves a specific crew member by skill (or role) and composes the worker's system prompt from <role> + <skills[]>. Without these fields, the spawn falls back to a default skill for agent_type — bypassing the crew system and breaking per-(skill, role) telemetry attribution in IntelDatabase.
Inbox v2 (additive — same version: 1) extends the descriptor with the crew-targeting parameters the spawn_worker tool already accepts:
| Field | Type | Purpose |
|---|---|---|
skill | string | Target a specific crew member by skill name (e.g. fleet-coder). v1 alias for role. |
role | string | Explicit role name. Wins over skill when both are supplied. |
skills | string[] | Atomic skills composed with the role (e.g. ["vitest-patterns", "security-audit-checklist"]). |
model | string | Model override; defaults to the role artifact's model or the session model. |
cwd | string | Worker working-directory override (coders default to an auto-created worktree). |
All five fields are OPTIONAL. v1 descriptors (no skill/role/etc.) still validate and dispatch unchanged.
Empty strings are rejected — omit the field instead.
Writer example (crew-targeted spawn):
cat > ~/.fleet/inbox/feature-x.task.json <<'EOF'
{
"version": 1,
"kind": "spawn_worker",
"agent_type": "coder",
"skill": "fleet-coder",
"name": "feature-x-impl",
"prompt": "Implement feature X following the spec at .plans/feature-x/02-requirements.md"
}
EOFDeliberate omissions from the inbox surface:
permissionOverrides— would let any process with write access to~/.fleet/inbox/escalatecanShell/canWrite/writeScopes.--inbox-trust-allis already a coarse gate; per-spawn escalation belongs in the operator-driven path only.team— the spawn_worker tool reads the active team from the store, not from its input. To target a different team, switch the active team via a separatekind: "prompt"descriptor first.topology— set at the crew level viasave_crew; no per-spawn override exists in the tool surface.
Reading the result
The .result companion is written by the inbox dispatcher the moment the synthesised prompt is handed to the coordinator — before the coordinator's spawn_worker tool runs. That means the spawn_worker return value (agent_id, skill_applied, crew_match, crew_warning) is not propagated into the .result file in v2 (documented gap).
The dispatcher instead instructs the coordinator to echo any crew_warning in its reply, so an operator watching the transcript can see when a spawn fell back to a default skill. Future work: extend InboxResultPayload to capture the spawn_worker tool return via a coordinator-side callback. Tracking issue: see PR description.
Limits
- Descriptors larger than 64 KB are refused.
- Symlinks are refused.
- Malformed JSON / schema violations are refused.
Lifecycle on disk
Per descriptor name.task.json:
| State | File on disk |
|---|---|
| Pending | name.task.json |
| Dispatched OK | name.task.json.processed + name.task.json.result |
| Dispatch failed | name.task.json.failed + name.task.json.result |
The .result companion is JSON:
{ "ok": true, "dispatchedAt": "2026-05-30T12:34:57.000Z", "descriptor": { "kind": "prompt" } }
{ "ok": false, "error": "schema validation failed at 'prompt': required", "failedAt": "..." }Writers should poll for the .result companion to observe completion.
Poller behaviour
- Default poll interval: 2000 ms.
- Override with
AGENTS_FLEET_INBOX_POLL_MS=<ms>. - Polls only when the coordinator is NOT mid-processing — avoids stacking inputs on a busy coordinator.
- One descriptor per tick (sorted by filename).
- If the inbox directory is deleted at runtime, the watcher recreates it on the next tick and continues.
- The poll timer is
unref()ed — it never keeps the process alive on its own.
Shutdown
The watcher is stopped early in the graceful shutdown pipeline (right after the Telegram transport, before session save) so no fresh descriptors are dispatched mid-shutdown. Bounded by the same 1.5s budget as the Telegram stop.
Security
The inbox is trust-as-much-as-the-shell: anyone with write access to ~/.fleet/inbox/ can inject tasks. That is the same trust boundary as ~/.fleet/config.json and ~/.fleet/telegram.json. The directory is created with mode 0o700 and written files with mode 0o600 on POSIX systems.
spawn_worker v1 posture
kind: "spawn_worker" is refused by default. To enable it, pass --inbox-trust-all (or set AGENTS_FLEET_INBOX_TRUST_ALL=1):
agents-fleet --inbox --inbox-trust-allThe trust-all rationale: even with the flag, the spawn is dispatched to the coordinator as a natural-language prompt — the coordinator's own spawn_worker tool then performs the actual spawn with all the normal permission / skill resolution / intel hooks. The inbox just provides the prompt.
Without --inbox-trust-all, every spawn_worker descriptor lands in *.task.json.failed with a clear .result.error explaining why.
CLI compatibility
--inbox is incompatible with:
-p/--prompt(single-shot mode exits before any poll can run)--no-coordinator(no coordinator to dispatch to)
These produce a clear error at startup, mirroring --telegram.
Example: writer-side shell snippet
NAME="task-$(date +%s)"
cat > "$HOME/.fleet/inbox/${NAME}.task.json" <<'JSON'
{
"version": 1,
"kind": "prompt",
"prompt": "List the workers in the team and summarise their last activity."
}
JSON
# Wait for completion:
while [ ! -f "$HOME/.fleet/inbox/${NAME}.task.json.result" ]; do
sleep 1
done
cat "$HOME/.fleet/inbox/${NAME}.task.json.result"