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

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"