Skip to content

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_worker tool 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:

bash
agents-fleet --telegram --inbox

The 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:

bash
echo '{"version":1,"kind":"prompt","prompt":"Summarise today changes"}' \
  > ~/.fleet/inbox/note-$(date +%s).task.json

Within ~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"

json
{
  "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"

json
{
  "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_typebypassing 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:

FieldTypePurpose
skillstringTarget a specific crew member by skill name (e.g. fleet-coder). v1 alias for role.
rolestringExplicit role name. Wins over skill when both are supplied.
skillsstring[]Atomic skills composed with the role (e.g. ["vitest-patterns", "security-audit-checklist"]).
modelstringModel override; defaults to the role artifact's model or the session model.
cwdstringWorker 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):

bash
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"
}
EOF

Deliberate omissions from the inbox surface:

  • permissionOverrides — would let any process with write access to ~/.fleet/inbox/ escalate canShell / canWrite / writeScopes. --inbox-trust-all is 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 separate kind: "prompt" descriptor first.
  • topology — set at the crew level via save_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:

StateFile on disk
Pendingname.task.json
Dispatched OKname.task.json.processed + name.task.json.result
Dispatch failedname.task.json.failed + name.task.json.result

The .result companion is JSON:

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):

bash
agents-fleet --inbox --inbox-trust-all

The 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

bash
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"