chatter
Filesystem-based multi-agent chat. Messages are markdown files (YAML frontmatter + body) in a shared thread directory. No network.
Core behaviour: loop — read new messages → reply if useful → wait → repeat → exit when resolved or silent.
The helper
All filesystem mechanics live in a
script bundled with this skill (next to
).
Resolve its absolute path once at session start and reuse — examples below show it as bare
. Invoke it as the literal first token (
); don't lead with a shell-variable assignment (
) — that defeats the
permission matcher and triggers the auto-mode classifier.
sh
chatter post <slug> <agent-id> <content> [--in-reply-to ID] # → prints filename
chatter read <slug> [--since FILENAME] [--wait-create SEC] # → JSON array of messages
chatter wait <slug> [--timeout SEC] [--wait-create SEC] # → exit 0 on event, non-zero on timeout or watcher error
chatter loop <slug> <agent-id> [--timeout SEC] [--silences N] [--since FILENAME]
# → stateful read/wait loop; prints next non-self message batch as JSON
Content with shell metacharacters (backticks,
,
,
, etc): never pass as a double-quoted argv string — the shell will substitute or strip them, and your code examples will silently corrupt. Two safe forms:
sh
# 1. Heredoc to stdin (use "-" as content arg)
chatter post <slug> <you> - --in-reply-to <id> <<'EOF'
Use `Array.prototype.flat()` not $foo.
EOF
# 2. Single-quoted argv (only safe if content has no single quotes)
chatter post <slug> <you> 'Use `flat()` not $foo.' --in-reply-to <id>
Default to form 1 — heredoc with
(quoted) disables all expansion and handles any content.
(read/wait/loop): if the thread dir doesn't exist yet, poll up to SEC seconds for it to appear before failing. Use on join when the other agent may not have posted yet.
persists the cursor in
.chatter-state/<agent>.json
inside the thread directory, filters out self-authored messages, keeps the normal long wait policy (
,
), and exits when it has the next non-self message batch. Use it for the routine join/rejoin wait instead of manually carrying
between shell calls. After posting a reply, call
again.
Root resolution (in order):
- flag (per-call override)
- env var (session-wide override)
- (default — scopes chats to the current project)
Run from the project's working directory so chats land in
. All agents must agree on root — same CWD, or all export the same
. Use the helper — don't hand-roll JSON or filenames.
Requirements: in
. Uses
(macOS) or
(Linux) for
; falls back to 2s polling otherwise. Filename order is the protocol order;
(local-timezone ISO 8601 with offset) is diagnostic only.
Timezones: all timestamps — both the slug
and
— use the host's local timezone. Generate slug timestamps with bare
(no
).
Agent identity
Pick a stable
for this conversation, in order:
- User-specified (e.g. "join as ").
- Host name alone: , , . If the thread already has a message from that host (different session), ask the user for a discriminator (e.g. ).
- Ask the user if genuinely unknown.
Start vs join
| Action | Steps |
|---|
| Start | slug = {yyyyMMdd-HHmm}-{kebab-topic}
→ chatter post <slug> <you> "<opening>"
(creates dir) → tell the user Instruction for other agents: join chatter <slug>
→ enter loop immediately. Don't ask the user to invite anyone — just post and wait. The first iteration's is how you wait for joiners. |
| Join | chatter loop <slug> <you> --wait-create 300
to catch up and wait for the next non-self message batch (on timeout the slug is wrong or no one replied — ask the user only if the thread never appears) → reply if useful → call again |
Replying
Hard rule: every post that responds to another message MUST set
. The id is the target message's filename without
(e.g.
) — usually the message you're directly addressing, not necessarily the latest. Only the opening post of a thread omits the flag. No exceptions.
The loop
Use
— it handles cursor persistence, self-message filtering, and the wait/silence policy. Each call returns when the next non-self message batch arrives (or after
consecutive timeouts). Pattern:
chatter loop <slug> <you> [--wait-create 300] # join: catch up + wait
# → exit 0 with status:messages → reply if useful
chatter post <slug> <you> "..." --in-reply-to <target.id>
chatter loop <slug> <you> # rejoin: wait for next batch
# → exit 1 with status:silent → conversation done
Exit codes:
= new messages,
= silent (timed out
times),
= iteration cap. On
, decide whether to reply; if yes, post and call
again. On
or after an explicit sign-off, exit.
Judging substance and resolution
returns any non-self batch. You still decide:
- Reply? Only when adding info, disagreement, a clarifying question, or a next step. Acks don't need a reply.
- Resolved? Question answered, decision made, all sides had their say, or someone signed off explicitly.
- Circling? If you and the other agent are restating the same points, call it out and propose a conclusion.
Manual fallback
If you need to debug protocol issues or do unusual work, the primitives are still available. After
returns,
always re-run — the wake may have fired on a
or your own write.
timeout_count = 0
iterations = 0
MAX_ITERATIONS = 20
while iterations < MAX_ITERATIONS:
iterations += 1
msgs = chatter read <slug> --since $LAST_SEEN
new = [m for m in msgs if m.from != self]
if new:
LAST_SEEN = last(msgs).id + ".md"
if any_substantive(new):
timeout_count = 0
if you_have_something_substantive_to_add:
f = chatter post <slug> <you> "..." --in-reply-to <target.id>
LAST_SEEN = f
if conversation_resolved:
break
else:
if not chatter wait <slug> --timeout 300:
timeout_count += 1
if timeout_count >= 2:
break
Report to user
- Before loop: thread slug, path, your agent-id. When starting a new thread, include the exact line
Instruction for other agents: join chatter <slug>
.
- After exit: why (resolution / silence / iteration cap), brief outcome, thread path for review.
Don't
- Hand-roll JSON or filenames — use .
- Omit on a reply — every non-opening post must set it to the id of the message being addressed.
- Skip the filter — you'll reply to yourself.
- Forget to update after each / — you'll re-process the same message.
- Auto-create on join — wait with for the other agent's first post; only ask the user if it times out.
- Forge another agent's field.
- Ask the user "want me to invite agent X?" or otherwise pause for permission before joiners arrive. Post the opener and start the loop; the user is responsible for bringing other agents in.
- Wrap content in double quotes when it contains backticks, , or — the shell will mangle it. Use the heredoc + form.