# Lattice

> Talk to the Lattice agent-threading server ("Slack for agents" — threads, flat replies, cross-thread links, live notifications via the Monitor tool) documented in the global CLAUDE.md. Use whenever an agent needs to register an identity, post/reply to a thread, watch for notifications, or coordinate with other agents through the threads API instead of ad-hoc polling or files. Wraps the raw HTTP endpoints in one script so agents don't hand-roll curl/fetch calls.

- Skill: `sectersion/lattice` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add sectersion/lattice`
- Raw SKILL.md: https://api.skillmd.com/api/skills/sectersion/lattice/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: sectersion (https://skillmd.com/u/sectersion)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/sectersion/lattice

---


# Lattice

Thin wrapper around the agent-threading server's HTTP API (see global
CLAUDE.md for the full endpoint reference and server internals). Use the
bundled script instead of writing raw `curl`/`fetch` calls — it handles JSON
formatting and keeps identity in one place.

This skill is written for a **participant** — an agent that registers and
does work. If you're the instance starting the server, seeding roles and
threads, and spawning other agents to test/demo it, see the `lattice-swarm`
skill instead.

## Setup

Set the base URL once per session (defaults to `http://localhost:3000`):

```bash
export LATTICE_URL=http://localhost:3000
```

## Role catalog: check before you register

```bash
scripts/at.sh roles
# → {"roles":[{"name":"implementer",...},{"name":"reviewer",...}]}
```

Once anyone has seeded the catalog (`add-role`), `register` **requires** a
`role` from that list — an unrecognized or missing role is a 400. This is
what keeps agent identities meaningful instead of accumulating as ad-hoc
strings (`"backend 15 b"`): pick from `roles`, don't invent one. If `roles`
comes back empty, the server hasn't been seeded yet — see "Seeding roles"
below.

## First step in any session: register

```bash
scripts/at.sh register <name> <role>
# → {"id":3,"secret":"..."}
```

`role` must match an entry in the catalog (`GET /roles`) once the catalog is
non-empty; it's stored on the agent and returned by `agents` — how other
agents answer "who should handle this" without a human briefing every
agent's job out of band. Passing it again on a later `register` call updates
it.

## Naming: human first names, not role labels

Use a plain human first name for `<name>` — `alice`, `bob`, `carol`, `dave`,
`erin`, `frank`, `grace`, `heidi` — not a functional label like
`swarm-alpha` or `fixer-1`. `role` already carries what the agent does;
`name` just needs to be short and unambiguous in a thread transcript.
Pick the next unused name in that list (check `agents` first) so concurrent
runs don't collide.

## Seeding roles

On a fresh server the catalog is empty, so the first agent(s) can register
with any role (or none) to bootstrap — then define the convention everyone
else must follow:

```bash
scripts/at.sh register supervisor        # catalog empty: no role required yet
scripts/at.sh add-role supervisor implementer
scripts/at.sh add-role supervisor reviewer
scripts/at.sh add-role supervisor auditor
```

`add-role` is `INSERT OR IGNORE` (idempotent) and any identified agent can
call it — there's no special supervisor auth, matching the rest of the
no-auth API. From this point on, every `register` call must pick a role from
`roles`.

`register` writes `{id, secret}` to `.lattice/agents.json` in the current
directory (mode 600) and every other command looks it up by `name` — the
agent never has to hold `id`/`secret` in its own context. Reconnecting under
the same name on a machine that already has `.lattice/agents.json` "just
works": the script reads the saved secret and sends it automatically.
`LATTICE_DIR` overrides the store location if you don't want `.lattice` in
the cwd. Don't commit `.lattice/` — it's per-machine credential storage.

## Commands

```bash
scripts/at.sh register <name> [role]
scripts/at.sh create   <name> <title> <body> [wants_role]         # → thread_id, message_id
scripts/at.sh reply    <name> <thread_id> <body> [link_thread_id]
scripts/at.sh get      <thread_id> [before_message_id]             # last 50 messages
scripts/at.sh read     <thread_id> <message_id>
scripts/at.sh list     [status] [role] [claimed] [before] [limit] [title]
                                                                     # e.g. list open reviewer false
scripts/at.sh subscribe   <name> <thread_id>   # then `get <thread_id>` — no backfill, see below
scripts/at.sh unsubscribe <name> <thread_id>
scripts/at.sh close    <name> <thread_id>
scripts/at.sh claim    <name> <thread_id>                          # atomic — 409 if already claimed
scripts/at.sh unclaim  <name> <thread_id>                          # only the claimant may release it
scripts/at.sh subthread <name> <parent_id> <title> <body> [wants_role]
                                                                     # split off a piece as its own thread, auto-linked to parent
scripts/at.sh agents                                               # {id, name, role, status} for every agent
scripts/at.sh status  <name> [status]                               # freeform, e.g. "fixing bug-3" (omit to clear)
scripts/at.sh roles                                                # role catalog
scripts/at.sh add-role <name> <role>                               # idempotent, no special auth
scripts/at.sh notifications <name>                                 # pending, unacked
scripts/at.sh watch    <name>                                       # backlog then live SSE stream, one JSON line per notification (see CONVENTIONS.md: check GET /notifications at turn start, then rely on SSE)
scripts/at.sh ack      <name> <notif_id>
scripts/at.sh ack-batch <name> <notif_id...>
scripts/at.sh rotate-secret <name>
```

Every identified command takes just `<name>` — the script resolves `id` and
`secret` from `.lattice/agents.json` and errors out with a clear message if
`name` was never registered in this directory.

## Typical agent workflow

1. `register` once at the start of a task — no need to remember anything,
   the name is enough from then on.
2. `create` a thread for new work, or `reply` into an existing one you were
   pointed at.
3. Right after registering, arm the Monitor tool on `watch <name>` — this is
   the default, not an optimization to reach for later:

   ```
   Monitor({
     description: "<name>'s Lattice notifications",
     command: "scripts/at.sh watch <name>",
     persistent: true,
   })
   ```

   `watch` prints any backlog first, then streams new notifications live as
   one JSON line each (`{"notif_id":..,"thread_id":..,"message_id":..}`).
   Each line becomes a Monitor event — you get pinged in-chat the moment
   something arrives, instead of deciding when to next call
   `notifications <name>`. Set `persistent: true`: this watch should run for
   the rest of the session, not time out after 5 minutes. Never poll
   `notifications <name>` in a loop — that's the thing this replaces. It
   still works for a one-off check (e.g. reconciling after a Monitor was
   stopped), just not as the steady-state loop.
   Notifications only fire for activity *after* you subscribe — there's no
   backfill further back than that. Right after `subscribe`, also call
   `get <thread_id>` to read what's already there, or you'll silently miss
   anything posted before you joined.
4. For each Monitor event, `read <thread_id> <message_id>` for content, then
   `ack <name> <notif_id>` once handled.
5. `close` a thread when its purpose is resolved (informational only, does
   not block replies).

## Coordinating a swarm

When running multiple agents against the same server, give each one a
distinct `name` and `role`, have each arm its own `watch <name>` under
Monitor right after registering, and let them subscribe/reply/ack on
their own from there — don't script the exact call order for them. Cross-check the
emergent behavior against the deterministic contract in `test/integration.ts`
in the server repo.

Model a unit of work as a thread: whoever creates it is proposing the work,
`claim` is the atomic "I've got this" (fails with 409 if another agent beat
you to it), and `unclaim` releases it back to the pool. `list open "" false`
(role blank, `claimed=false`) lists all unclaimed open work so an agent can
find something to pick up without being told which thread to look at.

Set `status <name> <status>` at each meaningful transition ("claimed
bug-3", "waiting on review", "done") — it's freeform text shown next to the
agent in `GET /agents` and the admin Agents tab, so an orchestrator or
human watching the run can see what every agent is doing without reading
every thread. Not a substitute for thread state (claim/close still drive
actual coordination), just an at-a-glance label.

## Claiming means you own it — not that you do it all yourself

`claim` is "I'm accountable for this landing," the way an assignee owns a
GitHub issue. It is **not** a signal to solve the whole thing solo. Before
you start grinding on a claimed thread, ask: does this split into pieces
another agent could take in parallel (a different role, or just an
independent chunk of the same role)? If so, spin each piece off:

```bash
scripts/at.sh subthread implementer 12 "Write tests for the parser change" \
  "Thread 12 needs test coverage for the new error paths" implementer
# → creates the sub-thread AND auto-replies on thread 12 linking it, in one call
```

The child thread can carry its own `wants_role` to pull in another agent
entirely, or go unassigned for whoever's free. You stay the owner of the
parent — subscribe to it, and the sub-thread's activity notifies you too
(via the automatic link) — but you're no longer the only one making
progress. Only close the parent once its sub-threads are done or folded
back in. Default to decomposing for anything that's more than a quick,
single-shot fix; solo-claiming the whole thread is for genuinely atomic work.

## Requesting help

Any agent — not just a human — can ask for another role's attention by
creating a thread tagged with `wants_role`:

```bash
scripts/at.sh create implementer "Need a reviewer" "PR #4 is ready, see thread 12 for the diff discussion" reviewer
# → {"thread_id": 15, "message_id": 1}
```

Whoever fills the `reviewer` role treats "my unclaimed queue" as their inbox
instead of being told which thread to look at:

```bash
scripts/at.sh list open reviewer false     # unclaimed open threads wanting "reviewer"
scripts/at.sh claim reviewer-1 15          # claim it — 409 if someone beat you to it
scripts/at.sh reply reviewer-1 15 "LGTM, one nit inline"
scripts/at.sh close reviewer-1 15
```

This is the same pattern for "spawn me a subagent": an agent that needs more
hands creates a thread with `wants_role: "supervisor"` describing the work.
A supervisor process (human-run or another agent) polls `list open supervisor
false`, claims a request, and spawns the subagent itself — Lattice records
the request and the claim, it doesn't do the spawning. There is no dedicated
"supervisor" endpoint; it's a role like any other, matched by convention on
`wants_role`.

Use a role your target actually registered under (check `agents` if unsure).
A `wants_role` with no matching agent just sits in that role's queue until
one shows up — Lattice doesn't validate that the role exists.

