The Chief Ingest Protocol

August 7, 2026 · View on GitHub

How to connect anything — an agent, a cron job, a watcher, a webhook — to Chief. This document is self-contained: you can build a working source from it without reading Chief's code.

Chief does not build pipes. It defines this protocol; your source connects itself.

The contract in one sentence

You POST a candidate event; Chief answers with a Decision; you obey it (usually by doing nothing — that's the point).

1. HTTP webhook

POST http://<chief-host>:8787/v1/events
Authorization: Bearer <token from ~/.chief/config.toml [ingest].webhook_token>
Content-Type: application/json

Request body (candidate event)

fieldtyperequiredmeaning
sourcestringwho you are, e.g. "flight-watcher"
summarystring ≤200 charsone line a human could act on
topicstringrecommendedhierarchical, e.g. "travel.flight_change"; the unit of learning. Omit and Chief infers one
detailstringlonger context
suggested_actionstringwhat the user could do right now (drives actionability)
evidencestring[]URLs or local paths backing the claim (drives confidence)
claimed_urgency"low" | "medium" | "high"advisory only; Chief never trusts it blindly
expires_atISO datetimewhen this stops being worth delivering
dedup_keystringstable key for repeat sends; defaults to a hash of summary

Minimal working example:

export CHIEF_TOKEN="$(chief token)"
curl -X POST http://localhost:8787/v1/events \
  -H "Authorization: Bearer $CHIEF_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "my-agent",
    "topic": "dev.ci",
    "summary": "CI failed on main: test_auth_flow broken by PR #482",
    "suggested_action": "revert #482 or fix the fixture",
    "evidence": ["https://github.com/acme/repo/actions/runs/9"],
    "claimed_urgency": "high"
  }'

Response (Decision, HTTP 200)

{
  "event_id": "evt_20260706_1040_ab12",
  "route": "dispatch",
  "score": 0.87,
  "components": {"urgency": 0.9, "relevance": 0.9, "actionability": 0.85,
                  "novelty": 0.8, "confidence": 0.9},
  "scene": "deep_work",
  "scene_confidence": 0.75,
  "cost": 0.0,
  "matched_rules": [],
  "reason": "score 0.87 ≥ deep_work threshold 0.85; dispatchable prep work available",
  "stage": 3,
  "dispatch_task_id": "task_evt_20260706_1040_ab12"
}

route is final:

routewhat Chief doeswhat YOU do
interruptdelivers to the user, scene-cappednothing
digestbatches into the next digestnothing
dispatchruns prep work, verifies, then delivers with a plannothing
curatestores a memory for future associationnothing
dropnothing — it was noisenothing. Do not retry louder.

Errors: 401 bad token · 422 malformed event (fix your payload).

1b. chief push — the webhook as a one-liner

You don't have to hand-roll the HTTP call. Any local script, cron job, or skill can push attention in one line:

chief push "CI failed on main" --topic dev.ci --urgency high
echo '{"source":"deployer","summary":"prod deploy finished"}' | chief push

chief push reaches the running daemon's POST /v1/events with the token from your config (so chief run must be up) and prints Chief's one-line verdict — interrupt · deep_work · score 4.2 — production incident. The minimal contract is just summary; --source, --topic, --urgency, --detail, --action are optional, and --json prints the full Decision. It is the inbound pipe: you push, Chief decides, you obey (usually by doing nothing). For a zero-daemon judgment with no persistence or delivery, use chief lite instead.

1c. Telegram — push from your phone

If you've wired a Telegram bot ([delivery] telegram_token + chat_id), it is a two-way pipe: Chief pushes worthy events to your phone, and any message you send to the bot becomes a candidate event — the off-box inbound path for sources that can't reach 127.0.0.1. Messages are accepted only from the configured chat_id (a bot is reachable by anyone who finds it; a stranger's message is dropped, never ingested), and the bot replies with the decision. Long or multiline messages are summarized to one line for scoring with the full original preserved in detail; the polling loop survives network failures with capped backoff, so the pipe recovers on its own.

2. MCP

Chief exposes an MCP server (python -m ingest.mcp_server, stdio) with tools:

  • propose(event) -> Decision — same contract as the webhook
  • feedback(event_id, signal) — report reactions/results: acted, read, dismissed_fast, muted, task_ok, task_fail
  • digest(now=False) — digest queue status
  • policy(action, text?) — read (show) or append (edit) POLICY.md
  • stats(days=7) — tact-report counters

2b. Feedback — teach Chief your preferences

POST http://<chief-host>:8787/v1/feedback
Authorization: Bearer <token>
{"event_id": "evt_...", "signal": "should_not_interrupt"}

Signals (strongest first): should_interrupt / should_not_interrupt (natural feedback — "this deserved my attention" / "this didn't"), then acted, read, dismissed_fast, muted, task_ok, task_fail. Known but unroutable events still record the signal ({"learned": false}); unknown signals get 422. MCP agents use the feedback tool; the console and Telegram expose 👍/👎 buttons that post the two natural signals.

2c. Connectors — out-of-the-box sources

Chief ingests from any Composio trigger:

POST http://<chief-host>:8787/v1/connectors/composio
webhook-id / webhook-timestamp / webhook-signature   (svix-style HMAC)

The v3 envelope ({id, metadata:{trigger_slug,...}, data, timestamp}) is HMAC-verified against [connectors.composio].webhook_secret, replay-checked (±5 min), and translated into a candidate event (trigger_slug → topic family). Wire it with chief connect composio --secret whsec_…. The connector registry documents open slots for zapier/n8n and MCP-push agents.

3. Rules of good citizenship

  1. Never send empty reports. "All clear / nothing new / check complete" gets dropped, and it trains the user to ignore your source.
  2. One event per fact. Send bursts and Chief will dedup (24h) and merge near-duplicates (10-min window) anyway.
  3. claimed_urgency is a hint, not a lever. Inflating it is the fastest way to teach Chief's learner to discount your topic.
  4. Fill suggested_action and evidence. Actionability and verifiable evidence are two of the five scoring dimensions — they are how good events win.

4. Outbound — receive deliveries anywhere

The exit is a protocol too. Configure one receiver URL and Chief POSTs every delivered event to it — anything that can accept an HTTP POST (a phone-app bridge, an ntfy relay you host, a desktop applet, a home-automation hub) becomes a delivery channel by implementing this one contract:

chief connect webhook --url https://your-receiver/hook --secret <random>

What arrives

POST <your url>
content-type: application/json
chief-event-id: evt_20260721_1512_ab3f    (stable across retries; use for dedup)
chief-timestamp: <unix seconds>            (only when a secret is set)
chief-signature: v1,<base64 hmac>          (only when a secret is set)

{"event_id": "evt_20260721_1512_ab3f",
 "topic": "dev.incident",
 "summary": "checkout 500s spiking on prod",
 "plan": "Rollback prepared: `deploy revert 4a1c`",   // null unless dispatched
 "level": "ring",
 "sent_at": 1784718720.5}

level is Chief's scene-capped noise decision (terminal < desktop < silent < vibrate < ring) — your receiver maps it to however it makes noise. --max-level caps what Chief will ask of this receiver.

Verify the signature (do this)

Same svix-style scheme as the Composio inbound connector — HMAC-SHA256 over "{event_id}.{timestamp}." + raw body, base64, v1, prefix:

import base64, hashlib, hmac

def verify(secret: str, event_id: str, timestamp: str, body: bytes, header: str) -> bool:
    mac = hmac.new(secret.encode(), f"{event_id}.{timestamp}.".encode() + body,
                   hashlib.sha256).digest()
    return hmac.compare_digest(header.removeprefix("v1,"), base64.b64encode(mac).decode())

Without a secret the POST is unsigned and your receiver can't tell Chief from anyone who found its URL — set one.

Honest semantics

  • Chief picks one channel per delivery (the weakest that can express the level); the webhook competes with terminal/desktop/Telegram by --max-level.
  • If the picked channel fails, delivery falls back down the chain (webhook down → Telegram → desktop → terminal): degraded loudness beats a silent loss. The webhook itself is retried 3× with backoff first. Only when every channel fails is the event logged and lost — there is no outbound queue, so a receiver that must not miss events should be highly available.
  • Retries keep the same chief-event-id; receivers should use it as their idempotency key so a timed-out response cannot create duplicate work.
  • Feedback flows back through the normal surface: your receiver can POST /v1/feedback with {"event_id", "signal"} (see §2b) to close the loop.