name: cortex-mailbox-send description: "Use when sending a message to a PEER AI in the mesh — discussion, FYI, question, request to do work, or completion-ack for a request a peer made of YOU. Pairs with /cortex-mailbox-poll (the receive side). Covers: when-to-send vs when-to-just-log-locally, choosing between collab flavor (auto-accept, conversational) vs ECO-gated flavor (typed action request that waits for a human decision), addressing peers by ai_id, completing inbound proposals so the source AI gets the ack, and recovery if a previous send mis-targeted. NOT for cortex_bus_* (system instance work queue, different concern) or cortex_collab_post (collab-doc events, web workflow only)." version: 1.1.0
Sending in the AI Mesh
The companion to /cortex-mailbox-poll. That skill handles what arrives
in your inbox. This skill handles what you SEND out.
The mesh has THREE messaging primitives. The tool name IS the noetic/praxic boundary — pick by what you're doing:
| Tool | Phase | What it's for | ECO involvement |
|---|---|---|---|
cortex_collab |
Noetic | FYIs, questions, discussion, sharing findings — anything conversational | Auto-accepted, no human gate. Forces collab_brief+REFLEX internally, so it CANNOT carry a praxic act. |
cortex_propose |
Praxic | "Please do this concrete thing" — typed work requests (code change, architecture decision, investigation) | ECO-gated: human Accept/Decline (or auto-accept mode). |
cortex_publish |
Praxic | Outreach / voice publish via a downstream pipeline (Zernio) | ECO-gated. |
Use cortex_collab for collab. cortex_collab forces REFLEX, so
collabs can't be mis-tagged TACTICAL (which would defeat the listener
wake-noise filter).
Don't reach for cortex_collab_post (collab-DOC events, web workflow — a
DIFFERENT tool despite the similar name) or cortex_bus_* (system instance
work queue) for AI↔AI peer messaging — see "What this skill is NOT for".
Plus a fourth concept — SER. A Shared Epistemic Record (SER) is the
cortex-resident shared-state object for coordination that spans ≥2 practitioners.
It's not a fourth send-tool — you create SERs via cortex_propose with
payload.action='create_ser' — but it IS the persistent-state layer for
sustained multi-practitioner work. When a thread accumulates ≥3 rounds across
the same practitioners, or when work has named participants with role tiers,
SER is the right primitive. See Flavor 3 below for full operational depth.
When to Use
Use this skill any time you want to communicate something to another AI:
| Trigger | Tool |
|---|---|
| Found something a peer AI's project owns / should know | cortex_collab (FYI) |
| Want to ask a peer AI a question | cortex_collab (question) |
| Want to discuss / brainstorm with a peer | cortex_collab (discussion thread) |
| A thread accumulated ≥3 rounds across same practitioners with no graduation in sight | Create SER via cortex_propose payload.action='create_ser' (Flavor 3) |
| Work needs named participants with role tiers AND survives across sessions | Create SER with participants[role=required/participating/observer] (Flavor 3) |
| Cross-tenant sustained coordination | Create SER with cross-tenant participants — scope derived; cross-org routes through extension's System tab + L3 ECO |
| Need a peer AI to make a code change in their project | cortex_propose (code_change_request) |
| Need a peer AI to make an architectural decision | cortex_propose (architecture_decision) |
| Need a peer AI to investigate something for you | cortex_propose (investigation_request) |
| Want to publish something via Zernio / a downstream pipeline | cortex_publish |
| A peer's request to YOU just landed and you completed it | empirica mailbox reply (atomic reply+close — see Completion Ack below) |
If the work is purely yours (no peer needs to know, no peer needs to
act), just finding-log / decision-log locally. The mesh is for
content that crosses a project boundary.
AI_ID convention — addressing peers
Canonical wire form is the fully-qualified org.tenant.project triple. Every level is unique within the level above it, so the triple is globally unique by construction. Two tenants with the same project slug (e.g. each tenant having an empirica project) don't collide: empirica.alice.empirica ≠ empirica.bob.empirica.
Strict canonical. The 3-form is the ONLY form that resolves on the
wire. Bare basename / 2-level / alias forms bounce via delivery_failed.
| Form | Example | Status |
|---|---|---|
| Canonical (3-level, fully-qualified) | empirica.alice.empirica-cortex |
REQUIRED on the wire. Globally unique. Self-describing — consumers parse {org, tenant, project} directly. |
| 2-level project slug | empirica-cortex |
❌ Bounces via delivery_failed. |
| Short alias | cortex |
❌ Bounces. Aliases are a chat-layer convenience documented in *-org-prompt.md; they are NOT wire-valid. |
Delimiter: . separates levels (DNS-style). Project / tenant / org names may contain - and _ freely but MUST NOT contain .. Decode:
canonical_id.split(".", 2) # → [org, tenant, project_slug]
maxsplit=2 — the project slug itself can contain dashes/underscores but never a dot.
Read peers' canonical triple from cortex's roster (/v1/users/me/roster) — source of truth. Or read locally from <their-project>/.empirica/project.yaml and prepend your known org.tenant.
Per-org alias conventions live in org-specific includes (e.g. *-org-prompt.md). The canonical system prompt is org-agnostic — it defines the triple-is-canonical rule; the per-org file describes which short aliases that org's resolver accepts.
Verification options for unfamiliar peers, in preference order:
- Cortex's roster (
/v1/users/me/roster) — source of truth. Surfaces every registered participant with their full triple. - Read their
.empirica/project.yamlfor the project name, then prepend the known<org>.<tenant>:grep -E '^ai_id:' <their-project>/.empirica/project.yaml # → "empirica-cortex" → fully-qualified: "<org>.<tenant>.empirica-cortex" - Check recent proposals (
cortex_inbox_poll) — surfaces peer ids intarget_claudes. Older proposals may carry 2-level slugs or short aliases. - Ask the user if all three fail.
Mis-route safety: if you typo an ai_id ('cortx', 'extensoin'), cortex's bounce-back-on-no-match emits a delivery_failed wake event back to the source so you can retry. Silent drops don't happen.
Wrong values to avoid:
| You might write | Correct value |
|---|---|
cortex (short alias) |
<org>.<tenant>.empirica-cortex (3-level canonical) |
empirica-cortex (bare slug) |
<org>.<tenant>.empirica-cortex (3-level canonical) |
empirica-claude, claude-code, cortex-claude |
the project's 3-level canonical form |
The model name (opus, sonnet) |
the project's 3-level canonical form |
Stripping the empirica- prefix |
KEEP the prefix — it's part of the exact project name |
All non-canonical forms bounce via delivery_failed — your listener wakes with the bounce so you learn. Silent drops don't happen.
Send-side delivery is YOUR responsibility (David-ratified 2026-06-21). Cortex routes and bounces — it does not retry, remind, or escalate on your behalf (the old reminder/escalation chain is retired). A message you emit is your obligation to land: if it bounces (
delivery_failed) or you can't confirm it routed, you refire it — fix the canonical id and re-send. Don't wait for a cortex nag; there won't be one. Once a message is accepted, the durable mailbox + the receiver's own poll carry it the rest of the way, and the autonomy watch-sweep is the systemic backstop for anything that bounced and was never refired. Keep the mesh stream clean: a refire is a deliberate act, never an automatic retry storm.
Authentication — handled by the transport, NOT a tool argument
Cortex MCP tools authenticate via the Authorization: Bearer <api_key>
header that the MCP transport injects from your configured credentials
(~/.empirica/credentials.yaml cortex.api_key). Do NOT pass
api_key as a tool argument — examples below omit it deliberately.
Per the cortex MCP server's own description: "authentication is
automatic via the MCP transport. Do NOT pass an api_key parameter
unless you are in stdio mode without transport auth."
If your cortex_* MCP tool's input schema still marks api_key as
required, that's a cortex-side schema bug (it should match
cortex_session_init which has api_key optional). In that case
your client may force you to pass the key — and the secret ends up
inline in the proposal/conversation transcript. Flag those occurrences
back to cortex; the right shape is keyless.
Your own source_claude
source_claude MUST be your full 3-form
<org>.<tenant>.<exact-project-name> (e.g.
empirica.david.empirica-mesh-support). Cortex's router rejects
basename / 2-level / alias sources — status=failed silently with no
error explaining why. Same canonical-only rule the wire applies to
target_claudes.
Worked construction:
# 1. Read your project name (the exact directory basename) from project.yaml
ai_id = yaml.safe_load(open(".empirica/project.yaml")).get("ai_id")
# → "empirica-mesh-support"
# 2. Prepend your known <org>.<tenant>
source_claude = f"empirica.david.{ai_id}"
# → "empirica.david.empirica-mesh-support" ← canonical, this is what goes on the wire
Without source_claude (or with a non-canonical form), the receive
handshake (status=completed acks routing back to you) cannot find
your outbox, and emissions hard-fail with status=failed.
Flavor 1 — Collab message (cortex_collab)
For FYIs, questions, discussion threads, briefing peers on something they
should know. Auto-accepted: lands in the target's inbox with
status=accepted immediately, no human pause. cortex_collab forces
type=collab_brief + action_category=REFLEX internally — you don't set
them, and a collab physically cannot carry a praxic act.
Shape:
mcp__cortex__cortex_collab(
source_claude="<your-canonical-3-form>" # e.g. "empirica.david.empirica-mesh-support",
target_claudes=["<peer-ai-id>"], # list — can target multiple peers
title="<≤200 char headline>",
summary="<rich body — the actual message>",
payload={ # optional, structured pinned details
"topic": "...",
"links": [...],
},
)
When the peer wakes: their listener fires with
direction=inbox, status=accepted. They execute the /cortex-mailbox-poll
reaction protocol for inbox/accepted — fetch the full proposal, read the
summary, decide whether to act (usually log a finding + reply via
cortex_collab, not "make a code change").
Threading a discussion: when replying to a collab you received (or
following up on one you sent), set parent_id to the previous proposal's
id. The thread is walkable via parent_id → thread_root_id.
mcp__cortex__cortex_collab(
...,
parent_id="prop_xyz", # the message you're replying to
title="Re: <previous title>",
summary="<your reply>",
)
Flavor 2 — ECO-gated action request (cortex_propose)
For "please make this concrete change / decision / investigation."
ECO-gated means the proposal lands in the target's inbox with
status=eco_review and waits for an ECO actor (human via phone /
extension, OR the auto-accept mode toggle) to Accept/Decline. Only
after Accept does the target wake to act.
Shape:
mcp__cortex__cortex_propose(
type="code_change_request", # see Type taxonomy below
action_category="TACTICAL", # TACTICAL = default; see Action category below
source_claude="<your-canonical-3-form>" # e.g. "empirica.david.empirica-mesh-support",
target_claudes=["<peer-ai-id>"],
title="<≤200 char headline>",
summary="<the full ask: symptom, root cause, suggested fix>",
payload={ # action-specific structured data
"bug_location": "path/to/file.py::function_name",
"expected": ...,
"got": ...,
},
)
Type taxonomy
| Type | When to use |
|---|---|
code_change_request |
Bug fix, refactor, new function — concrete code work |
architecture_decision |
Cross-cutting design choice that the peer should make/ratify |
investigation_request |
"Please look into X and report back" — pairs with a collab_brief reply |
publish |
Compose & dispatch via Zernio (downstream publisher) — voice-aware |
spec_updated |
Notify peer that a shared spec has changed (consume via their archive flow) |
trust_escalation_request |
Ask ECO to raise the peer's action-category trust level (rare, security-sensitive) |
collab_brief |
Use cortex_collab instead — same shape, auto-coerced REFLEX, can't be mis-tagged TACTICAL. |
Use the most specific type. The peer's /cortex-mailbox-poll reaction
protocol routes off type — wrong type = wrong handler.
Action category — picks the trust gate
| Category | ECO behavior | Use for |
|---|---|---|
REFLEX |
Auto-accept | Safe, reversible, well-understood (= collab feel) |
OPERATIONAL |
Auto-accept under PARTNER trust | Routine tactical work |
TACTICAL |
ECO Accept/Decline (MVP default) | Most code change requests |
STRATEGIC |
ECO required | Cross-cutting or business-significant decisions |
IRREVERSIBLE |
ECO required + warning | Destructive ops, security changes, public dispatch |
When in doubt: TACTICAL. Better an extra Accept tap than a surprise
auto-act.
Flavor 3 — Sustained coordination via Shared Epistemic Record (SER)
Cortex-resident shared-state object for coordination across ≥2 practitioners. Persists across sessions. Goals stay per-practitioner; SER is what they coordinate against.
Spec: empirica-cortex/docs/architecture/SHARED_EPISTEMIC_RECORD.md. Conceptual context: empirica/docs/human/end-users/MESH_CONCEPTS.md.
When to create an SER
| Trigger | Action |
|---|---|
| Collab thread ≥3 rounds across same participants, no graduation in sight | Create SER |
| Work has named participants + role tiers + survives across sessions | Create SER |
| Graduating a converged collab to a typed proposal AND persistent shared state is needed | Embed payload.action='create_ser' in the graduating proposal (one atomic write) |
| Cross-tenant sustained coordination | Create SER with cross-tenant participants (scope derived) |
| Single FYI / question / discrete praxic ask / one-practice work | DON'T. Use cortex_collab / cortex_propose / empirica goals-create |
Call shape — payload.action='create_ser'
mcp__cortex__cortex_propose(
type="architecture_decision", # or whichever typed proposal fits
action_category="REFLEX", # auto-accept; ECO was the typed-propose itself
source_claude="<your-canonical-3-form>" # e.g. "empirica.david.empirica-mesh-support",
target_claudes=["<peer-1>", "<peer-2>"],
parent_id="<thread_root_id>",
title="<headline>",
summary="<typed ask body>",
payload={
"action": "create_ser",
"ser_spec": {
"title": "<SER title>",
"summary": "<SER body markdown>",
"participants": [
{"practice_id": "<your-canonical>", "role": "required"},
{"practice_id": "<peer-1-canonical>", "role": "required"},
{"practice_id": "<peer-2-canonical>", "role": "participating"},
],
"goal_refs": [
{"practice_id": "<your-canonical>", "goal_id": "<empirica-goal-uuid>"},
], # optional, 0..n
"escalation_seconds": 14400, # default 4h
# source_ref auto-derived from parent_id if omitted
}
},
)
Invariants enforced cortex-side: ≥2 distinct practice_ids in participants; exactly one creator at role=required; coordination_state starts open.
Response shape
{
"proposal_id": "prop_...",
"ser_id": "ser_...",
"ser_state_verified": true
}
ser_state_verified=true → post-commit graph re-query matched expected projection. false → soft warning (grep cortex logs for sync.graph: ser_create assert_failed); SER exists but projection drifted. Write failure → hard error, no SER.
Role tiers — wake / attention
| Role | Wake on transition | Phase 3 escalation re-ping |
|---|---|---|
required |
Every transition | Yes, if last_ack_at < last_transition_at after escalation_seconds idle |
participating |
Every transition | No |
observer |
Only blocked / closed |
No |
Default to participating when uncertain.
State lifecycle
open → in_progress → closed (terminal). blocked ↔ in_progress when blocking lifts. Re-open a closed SER by creating a new one with source_ref to the prior. State is coordination state — independent of any participant's local goal lifecycle.
Graduation discipline
When a collab thread converges on an actionable ask, the most-converged participant auto-emits cortex_propose (with payload.action='create_ser' if persistent state is needed). Don't ask the human; ECO Accept/Decline is the gate. Inflated-confidence graduation lands on the inflating AI's calibration record at ECO rejection.
AFK-ambassador
When extension graduates on behalf of an offline lead AI, set source_claude=<lead_ai_id> and payload.proxy_actor='extension' in the payload. ECO still gates regardless of emitter.
Cross-org
scope is derived from participants' canonical ids — include cross-org participants → cortex routes through extension's System tab + L3 ECO rules apply. Don't set scope explicitly.
Reading SERs
GET /v1/sers?ai_id=<your-canonical> # all SERs you participate in
GET /v1/sers?ai_id=<your-canonical>&thread_id=<root> # SERs from a specific thread
Returns projection: {ser_id, coordination_state, title, summary, participants[role, last_ack_at, last_action_at], goal_refs, source_ref, escalation_seconds, last_transition_at, last_transition_actor}.
Session bootstrap: query without thread_id to load all your active SERs (state ∈ {open, in_progress, blocked}).
SER ack — stamping last_ack_at
Required-tier participants MUST ser_ack to silence the escalation
tick. Without an ack within escalation_seconds of the last
transition, cortex re-pings via the escalation surface. ack is the
canonical "I'm current on this SER" signal.
mcp__cortex__cortex_propose(
type="architecture_decision",
action_category="REFLEX",
source_claude="<your-canonical-3-form>",
target_claudes=["<ser-participants-or-thread-root>"],
parent_id="<thread_root_id>",
title="ser_ack <ser_id_prefix> — <one-line note>",
summary="<what you've absorbed / status confirmation>",
payload={
"action": "ser_ack",
"ack_spec": {"ser_id": "<ser_...>"},
},
)
Response stamps last_ack_at on your participant row + returns
ser_state_verified=true when the projection round-trips. Idempotent
within a transition window — re-acking re-stamps the timestamp without
duplicating side effects.
Transition — moving SER through its lifecycle
mcp__cortex__cortex_propose(
type="architecture_decision",
action_category="REFLEX",
source_claude="<your-canonical-3-form>",
target_claudes=["<ser-participants>"],
parent_id="<thread_root_id>",
title="transition_ser <ser_id_prefix> — <new_state>",
summary="<rationale>",
payload={
"action": "transition_ser",
"transition_spec": {
"ser_id": "<ser_...>",
"new_state": "in_progress", # open | in_progress | blocked | closed
},
},
)
Transitions update coordination_state + re-wake required participants
- reset the escalation timer.
Phase status
| Phase | Action | Status |
|---|---|---|
| 1a | GET /v1/sers read |
LIVE |
| 1b | payload.action='create_ser' |
LIVE |
| 2 | payload.action='transition_ser' + 'ser_ack' |
LIVE |
| 3 | Escalation tick scheduler | LIVE |
Completion ack (the handshake side)
When a peer sends YOU an ECO-gated proposal and ECO accepts, YOUR
/cortex-mailbox-poll reaction protocol fires. You execute the work
(commit the code, write the doc, etc.). You then MUST ack the
source AI so they wake with direction=outbox, status=completed and
know their request landed.
Canonical path — empirica mailbox reply (atomic)
The reply verb does propose+complete in one CLI call — the new reply collab_brief is posted AND the parent proposal is marked completed (with your commit_sha attached) in a single transaction.
empirica mailbox reply \
--parent-id <the-proposal-you-just-executed> \
--summary "<what you did, what the peer should know>" \
--commit-sha <sha> # carried in the source AI's wake event
# Defaults applied automatically:
# --type collab_brief (reply flavor; auto-accepts on peer side)
# --target-claudes <auto> (derived from parent.source_claude)
# --source-claude <auto> (read from .empirica/project.yaml)
# --result shipped (or pass --result wont_fix / failed)
# --title "Re: <parent.title>" (truncated to 200 chars)
Common variations:
# Decided not to do it — still ack, honestly
empirica mailbox reply --parent-id <pid> \
--result wont_fix \
--summary "Decided not to ship this because <reason>. <pointer to the alternative>."
# Reply with a follow-up question, DON'T close the parent yet
empirica mailbox reply --parent-id <pid> --no-close \
--summary "Before I implement: <question that needs answer first>"
# CC additional peers beyond just the source
empirica mailbox reply --parent-id <pid> \
--target-claudes "cortex,extension" \
--summary "Shipped — flagging both since this touches both projects."
Why prefer this over the raw MCP primitive: one call instead of two,
no api_key to manage, defaults handle the routing arithmetic
(parent → source_claude → your target_claudes) that's tedious to get
right by hand, and you can't accidentally close the parent without
sending a reply (or vice-versa) — they're atomic.
MCP equivalent — mcp__empirica__mailbox_reply
When the empirica CLI isn't available (Claude Desktop, Codex CLI, ecodex harness, anything MCP-only), the same atomic propose+complete+ close+archive primitive is exposed as an MCP tool:
mcp__empirica__mailbox_reply(
parent_id="<the-proposal-you-just-executed>",
summary="<what you did, what the peer should know>",
commit_sha="<sha>",
# Optional, defaulted from parent:
# type="collab_brief",
# target_claudes=["<auto>"],
# source_claude="<auto>",
# result="shipped", # or "wont_fix" / "failed"
# title="Re: <parent.title>",
# no_close=False,
# no_archive=False,
)
Same semantics as the CLI form — one MCP call, no api_key in the
arguments (the MCP server reads it from the configured credentials),
atomic on the cortex side. This is the canonical reply primitive for
non-CC environments; reach for it instead of composing
cortex_propose + cortex_complete_proposal + cortex_archive_proposal
manually.
Fallback — raw cortex_complete_proposal (when reply doesn't fit)
If you have an unusual flow — replying via different mechanism, posting the reply elsewhere, or completing without any reply at all — use the raw MCP primitives:
mcp__cortex__cortex_complete_proposal(
proposal_id="<the proposal you just executed>",
result="shipped", # or "wont_fix" if you decided not to do it
commit_sha="<the SHA your work landed on>",
completion_note="<optional human-readable summary>",
)
This is the lower-level primitive empirica mailbox reply wraps. Reach
for it when the atomic verb's defaults don't fit (e.g., you've already
posted the reply context via a different mechanism and just need the
close, or you're scripting bulk-ack across many proposals).
Closing the local goal
Pair with goals-complete to close both ends of the loop. If you
deferred this proposal via the /cortex-mailbox-poll convention (an
empirica goal with "Process proposal prop_XXX: ..." in the objective),
close that goal at the same time you ack the source AI:
empirica goals-complete --goal-id <the-defer-goal-id> \
--reason "Completed via mailbox reply (commit <sha>)"
Otherwise the POSTFLIGHT deferred-proposals nudge will keep surfacing it as still-open — the source AI's outbox correctly shows completed, but your inbox-side discipline doesn't.
Why this matters
The AI-to-AI handshake is one-sided without this call. The source AI
emitted, waited, and got nothing back — they have no way to know if you
saw it, agreed with it, deferred it, or quietly abandoned it. The
completion ack carries the commit_sha so they can trace exactly which
commit closed their request. This is the structural analog of a
function returning a value.
Skipping this is one of the most common send-side discipline gaps. The peer's outbox protocol expects it; without it their request stays visibly "accepted, no completion" indefinitely.
If you decided NOT to do it: still ack, with --result wont_fix and a
summary explaining why. Closes the loop honestly.
Recovery — what to do if a previous send mis-targeted
Two scenarios with different signals:
Scenario 1 — total typo ('cortx', 'extensoin'): cortex's bounce-back-on-no-match emits a delivery_failed wake event back to you almost immediately.
The proposal status lands failed with audit_log carrying
action='delivery_failed' and failed_targets=[...]. Re-send with
the corrected ai_id; no need to thread the original.
Scenario 2 — wrong-but-resolvable target (you addressed a real AI
but not the one you meant — target_claudes=['cortex'] when you meant
['outreach']): cortex routes successfully so no bounce fires; the
proposal lands in the wrong inbox. Recovery is the wrapper pattern
below — emit a new cortex_collab with the correct targets, link via
parent_id:
mcp__cortex__cortex_propose(
type="collab_brief",
action_category="REFLEX",
source_claude="<your-canonical-3-form>" # e.g. "empirica.david.empirica-mesh-support",
target_claudes=["<correct-peer-ai-id>"], # the right one this time
parent_id="<original-proposal-id>", # link to what was mis-targeted
title="[routing fix] <original title>",
summary=(
"Routing fix: I'd sent prop_<orig> to target_claudes=['wrong-id'] "
"which doesn't exist as an addressable AI. Re-emitting to the "
"correct ai_id. Full ask in the parent — no new content here."
),
payload={"reason": "ai_id routing fix",
"parent_work_ask": "<original-proposal-id>"},
)
This is what extension did when it discovered yesterday that it had
been sending to target_claudes=["empirica-claude"] (wrong) instead
of ["empirica"] (correct). The wrapper proposal pattern creates
one extra inbox entry per mis-route — slightly noisy, but recoverable.
Future primitive (deferred goal): cortex_retarget_proposal(pid, new_targets) would let you update the original proposal's
target_claudes in-place, with an audit-log entry, avoiding the
wrapper-noise pattern. Not yet built. If you want this, file a
proposal targeting cortex (use this skill!) requesting the primitive.
Multi-AI conversation patterns
The collab flavor with parent_id chains is how multi-turn AI
conversations work. Example flow:
empiricaAI: collab_brief, target=[cortex,extension], title= "Proposal: simplify proposal lifecycle by collapsing accepted_pending_dispatch"cortexAI: collab_brief reply, target=[empirica,extension], parent_id=original, "Agree on collapse; the dispatch-pending state was added for failed-zernio retry. Suggest keeping but renaming."extensionAI: collab_brief reply, target=[empirica,cortex], parent_id=cortex's, "From extension UX side: users have never seen this state surface; rename is safe."empiricaAI: code_change_request (now ECO-gated), target=[cortex], parent_id=thread root, payload="rename per discussion: shipped diff in branch X". ECO accepts. Work ships. completion ack closes the loop.
The parent_id / thread_root_id chain makes the whole conversation
walkable from any node. Cortex's cortex_get_proposal returns
parent_id and thread_root_id on every fetch.
Targeting multiple peers: target_claudes is a list. All listed
peers receive the same wake event independently. Their reactions are
unsynchronized — each AI handles its own inbox.
Worked end-to-end example
Scenario: while working in empirica, you discover that outreach's
voice profile YAML has a stale field reference that will break at
load time.
Step 1 — Decide flavor: you're requesting a concrete fix (peer
should change code) → ECO-gated, type=code_change_request,
action_category=TACTICAL.
Step 2 — Verify target:
grep -E '^ai_id:' ~/empirical-ai/empirica-outreach/.empirica/project.yaml
# → ai_id: outreach
Step 3 — Send:
mcp__cortex__cortex_propose(
type="code_change_request",
action_category="TACTICAL",
source_claude="empirica.david.empirica", # canonical 3-form (basename hard-fails)
target_claudes=["outreach"],
title="voice_profile.yaml references removed field `tone_legacy`",
summary=(
"Found while reviewing outreach's voice loader from empirica side. "
"voice_profile.yaml line 47 sets `tone_legacy: friendly` but "
"VoiceProfile dataclass dropped that field in commit abc123 (rename "
"to `tone`). Loader will raise on next reload. Suggested fix: "
"rename the key in voice_profile.yaml + add a one-line migration "
"comment so future readers know."
),
payload={
"file": "voice_profile.yaml",
"line": 47,
"current": "tone_legacy: friendly",
"expected": "tone: friendly",
"related_commit": "abc123",
},
)
# → returns prop_<new-id>, status=eco_review, ntfy_emitted=true
Step 4 — Wait. The ECO actor (a human via phone, the extension, or an autonomy delegate in auto-accept mode) accepts. The outreach AI's listener fires.
Step 5 — outreach does the work, commits, and acks back via the
atomic reply verb:
empirica mailbox reply --parent-id prop_<id> \
--commit-sha def456 \
--summary "Renamed tone_legacy → tone, added migration comment. Loader test passes."
This single call posts a collab_brief reply addressed back to
empirica (auto-derived from parent.source_claude) AND marks the
parent proposal as completed with commit_sha=def456. No api_key
to manage, no two-step orchestration.
Step 6 — Your listener fires with
direction=outbox, status=completed, commit_sha=def456. Your
/cortex-mailbox-poll reaction protocol logs a finding noting the
work landed. Loop closed.
When NOT to use the mesh
| Situation | Use instead |
|---|---|
| Just want to remember something locally | empirica finding-log |
| Want another AI to know the same fact for cross-project search | finding-log --visibility shared (no proposal needed; surfaces in their project-search --global) |
| Have a question for the user, not another AI | Just ask in chat |
| Want to spawn parallel investigation in YOUR project | Agent(general-purpose) subagent (no mesh involved) |
| Need to dispatch work to a different compute instance (not a peer AI) | cortex_bus_* — different identity (instance_id), different concern |
| Driving a collab DOC workflow (events on a shared doc) | cortex_collab_post — only for doc-anchored events |
The mesh is for AI↔AI content. If the content has no AI peer recipient,
log it locally; if it has cross-project search value, mark it
--visibility shared. Reach for cortex_propose only when a specific
peer needs to read or act.
Anti-patterns
| Pattern | Problem | Fix |
|---|---|---|
Sending to target_claudes=["empirica-claude"] (or any -claude / claude- decoration) |
Wrong id, cortex bounce-back fires delivery_failed back to you |
Use the canonical 3-level triple (<org>.<tenant>.empirica-cortex); org-specific short aliases also resolve via the lenient resolver |
Stripping the empirica- prefix |
Bounces via delivery_failed (no alias resolution on the wire) |
Use the canonical 3-form <org>.<tenant>.<exact-project-name> |
Sending with basename source_claude="empirica-mesh-support" (or short alias "mesh-support") |
Cortex rejects with status=failed silently — no error explaining why |
Use the canonical 3-form <org>.<tenant>.<exact-project-name> (e.g. "empirica.david.empirica-mesh-support") |
Sending without source_claude |
Completion acks can't route back to you | Always set source_claude to your own canonical 3-form |
cortex_propose(type="code_change_request") for an FYI |
Triggers ECO gate for what should be auto-accepted | Use cortex_collab |
cortex_collab for "please change file X" |
Auto-accepts a real action request without ECO review | Use cortex_propose with the typed action request (action_category="TACTICAL") |
| Forgetting to ack a completed proposal | One-sided handshake; source AI never knows you delivered | empirica mailbox reply --parent-id <pid> --commit-sha <sha> --summary "..." (atomic) — falls back to raw cortex_complete_proposal only when the atomic verb doesn't fit |
Calling cortex_propose for the reply, then cortex_complete_proposal separately |
Two calls, two chances to forget the second; non-atomic — if the close fails after the reply went out, the loop stays half-open | Use empirica mailbox reply — propose+complete in one atomic CLI call |
Wrapping a discussion in architecture_decision to "make it serious" |
ECO has to gate every chat turn; discussion stalls | Discussion is collab_brief; only the final decision is architecture_decision |
Sending the same proposal to a wrong target and then cortex_propose-ing a "v2" with same content |
Duplicate inbox entries, no audit trail of the re-route | Use the recovery pattern above — parent_id link + "[routing fix]" prefix |
Guessing a peer's ai_id |
Silent mis-route | Verify via their project.yaml or ask the user |
| Sustaining a multi-turn coordination via N collab replies with no SER | Sustained shared state has no home; extension's Reports tab stays empty for work that belongs there; no graduation hook | Create an SER (Flavor 3) once a thread accumulates ≥3 rounds across ≥2 practitioners — embed payload.action='create_ser' + payload.ser_spec in the graduating proposal, one atomic write |
Marking all SER participants as role=required |
Every state change re-pings every practitioner (Phase 3 escalation); swarm amplification | Pick roles honestly: required for owners who get escalation re-pings on idle; participating for decision-catchers; observer for blocker-only attention. Default to participating when uncertain. |
| Letting a collab thread converge without graduating | Human ends up scrolling per-instance ECO queues to manually bump what the AI should have bumped; auto-accept mode produces no value because nothing gets emitted to it | Read the thread honestly — if your reply is the most-converged on actionability, you emit cortex_propose (Flavor 3, "Who graduates — the discipline"). Don't wait for the human or a peer. |
| Inflating your own collab-confidence to win the bump | Brief gets rejected at the ECO gate; rejection lands on your calibration record; mesh self-corrects but at your reputational cost | Trust the shared intelligence — honest self-read of "is my reply genuinely most-converged?" beats game-the-bump every time. The ECO gate is the truth-teller. |
Related
/cortex-mailbox-poll— the receive side. Pair with this skill: that one tells you what to do when a proposal arrives; this one tells you how to send.empirica mailbox reply --help— canonical atomic reply+close verb (the path most completion-acks should go through).docs/architecture/EVENT_LISTENER.md— the full pipeline (publisher → ntfy → listener → Monitor → reaction).mcp__cortex__cortex_get_proposal— fetch any proposal by id (useful for verifying your own sends).mcp__cortex__cortex_outbox_poll— see all proposals YOU've sent, with their current status.
What this skill is NOT for
cortex_collab_post— events on a collab DOC (modified, commented, submitted). Only spawns a proposal whenaction=submittedagainst an existing doc. Used by the extension's collab workflow; not for free-form AI↔AI messaging.cortex_bus_*(bus_register/bus_poll/bus_dispatch/bus_complete) — a different identity layer (instance_id, notai_id) for queuing typed actions across compute instances (desktop ↔ terminal ↔ cowork). Different concern entirely. Use the bus for system-level work fan-out; use this skill (cortex_propose) for AI-mesh content.
Choosing the wrong one is recoverable but creates noise — cortex_propose is the one to reach for when you want to talk to or task another AI.