Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 80 additions & 26 deletions .claude/skills/hera/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,16 @@ Arg names below are exact — do not invent others. The nine coordination tools

### Messaging (idle-gated bus)

- **`hera_send(cwd, body, tldr, [to], [in_reply_to], [orchestrator])`** — message another role **in the
same orchestrator**. `body` and `tldr` are **required**; `tldr` is a one-line summary ≤120 chars,
written from the recipient's perspective (see §5). Worker/freelance senders may omit `to` — it
default-routes to the orchestrator's coordinator. **Coordinators must supply an explicit `to`.**
`in_reply_to` threads a reply to a prior message id. Returns the `message_id`, recipient, and
delivery mode. Caps: 64 KiB body, 500 unread per recipient, 50 sends/min/sender. Cross-orchestrator
messaging is not possible — `to` always resolves within your own orchestrator.
- **`hera_send(cwd, body, tldr, status, [to], [in_reply_to], [orchestrator])`** — message another role
**in the same orchestrator**. `body` and `tldr` are **required**; `tldr` is a one-line summary ≤120
chars, written from the recipient's perspective (see §5). **`status` is REQUIRED for worker/freelance
senders** (one of `idle`/`working`/`blocked`/`done`/`failed`) and is applied to the sender's role
**synchronously** before the message is sent — it is never delivered async. Omitting `status` as a
worker/freelance sender is an error; coordinator senders may omit it. Worker/freelance senders may
omit `to` — it default-routes to the orchestrator's coordinator. **Coordinators must supply an
explicit `to`.** `in_reply_to` threads a reply to a prior message id. Returns the `message_id`,
recipient, and delivery mode. Caps: 64 KiB body, 500 unread per recipient, 50 sends/min/sender.
Cross-orchestrator messaging is not possible — `to` always resolves within your own orchestrator.

- **`hera_inbox(cwd, [orchestrator])`** — fetch all unread messages addressed to your role, oldest
first. **Reading IS acknowledgment**: this both cancels pending pane deliveries AND marks the
Expand All @@ -113,9 +116,12 @@ Arg names below are exact — do not invent others. The nine coordination tools
### Status / tree

- **`hera_status(cwd, status, [orchestrator])`** — set your role status: `idle` | `working` | `blocked`
| `done`. Mirrored (best-effort) to argus `task_meta` so the coordinator sees it without asking.
**A `worker`-kind role reporting `status=done` also rolls its bound argus task to `in_review` and
stamps `ready_to_close`** (visible in the rail) — see §4. Coordinators/freelancers just update status.
| `done` | `failed`. Mirrored (best-effort) to argus `task_meta` so the coordinator sees it without
asking. **A `worker`-kind role reporting `status=done` also rolls its bound argus task to `in_review`
and stamps `ready_to_close`** (visible in the rail) — see §4. **A worker reporting `status=failed`
rolls its task to `in_review` WITHOUT `ready_to_close`** (needs-attention, not ready to check off).
The gater treats a `failed` blocker as explicitly failed (no need to wait for session death).
Coordinators/freelancers just update status.

- **`hera_tree_updates(cwd, [orchestrator], [since])`** — scan the caller's orchestrator **subtree**
(nested sub-orchestrators included) for messages since a cursor. Returns **TLDR-only subject lines —
Expand All @@ -128,14 +134,24 @@ Arg names below are exact — do not invent others. The nine coordination tools
recipient must live in it); inaccessible / missing ids get a per-id `error` field rather than a
top-level error.

### Plan authoring — the plan-DAG (coordinator-only)
### Plan authoring + living reconciliation — the plan-DAG (coordinator-only)

> **With a live coordinator binding, the plan-DAG is the single source of truth for all worker
> activity. Author every worker as a plan node; track progress through the DAG; reconcile the plan
> as work evolves. The harness `TaskCreate` system-reminder is not applicable to coordinated work —
> use `hera_plan_node` / `hera_plan` / `hera_spawn_worker`, never bare task creation.**

Instead of spawning every worker immediately, a coordinator can lay out a **plan**: a set of
**planned nodes** wired by **blocking edges**, and let the daemon's gater materialize each node into a
live born-bound worker *automatically*, in dependency order. A planned node costs one DB row — no
agent, worktree, or inbox until it materializes. The plan-DAG renders in the TUI's second tab (planned
`○` → live-by-status), and you navigate it there.

The DAG is **living, not authoring-time**: update it as reality diverges from the plan. Use the
mutation verbs below to edit a node's prompt, drop a stale edge, or cancel a node you no longer need.

**Plan authoring verbs:**

- **`hera_plan_node(cwd, name, prompt, [orchestrator], [project], [kind], [goal])`** — create ONE
planned node (a worker role with no live agent/worktree/inbox yet). It materializes automatically
once **all** its blockers reach role-status `done`. **Name with a stable short-id prefix** — number =
Expand Down Expand Up @@ -164,7 +180,24 @@ agent, worktree, or inbox until it materializes. The plan-DAG renders in the TUI
back the entire graph (no orphan nodes). This is the way to author a multi-stage plan at once rather
than many `hera_plan_node` + `hera_block` calls.

**Plan mutation verbs (reconcile as you go — coordinator-only):**

- **`hera_plan_node_update(cwd, name, [prompt], [project], [orchestrator])`** — edit a **planned**
node's prompt and/or project. Rejected once the node has materialized (the prompt was already
delivered to the worker). Use this when you discover the original spec needs revision before the
node spawns.

- **`hera_unblock(cwd, blocked, blocker, [orchestrator])`** — drop one blocking edge. Idempotent
(dropping a missing edge is a no-op). To re-point an edge: `hera_unblock` (old blocker) then
`hera_block` (new blocker).

- **`hera_plan_node_cancel(cwd, name, [orchestrator])`** — cancel a planned node: it will never
materialize, its dependents proceed (no longer gated on it), and it stays visible in the plan as a
grey ✕. Rejected if the node has already materialized (use the task lifecycle to stop a running
worker). Cancelled nodes are kept in the DAG to show what was dropped — not silently deleted.

**How materialization + branch-stacking works** (you don't drive it — the gater does, ~60s tick):

- A node with **no remaining blockers** materializes into a born-bound worker (same as
`hera_spawn_worker` would produce).
- **Non-root nodes stack automatically**: a materializing node is branched off its most-recently-`done`
Expand All @@ -174,7 +207,16 @@ agent, worktree, or inbox until it materializes. The plan-DAG renders in the TUI
branch stacks on it (pass `base_branch` to `hera_new_orchestrator` to override the root).
- **Respond to check-ins promptly.** Each node check-ins on materialization via `hera_send`; you pull
it from `hera_inbox` and reply (e.g. `"go"`). A node whose blocker genuinely **failed** is HELD and
pings you — decide whether to unblock, re-plan, or let it stay held.
pings you — decide whether to `hera_unblock`, `hera_plan_node_cancel` the held node, or dispatch a
replacement via `hera_spawn_worker`.

**Standing order — keep the DAG reconciled:**

- After every worker interaction, check whether the plan still reflects reality. If a worker's scope
changed, `hera_plan_node_update` its prompt. If an edge is no longer needed, `hera_unblock` it. If a
node was superseded, `hera_plan_node_cancel` it.
- A worker reopening (re-engaging on rework after `done`/`failed`) reports `working` on its next
`hera_send` by requirement — the DAG self-corrects. No coordinator action needed for a simple reopen.

**Sub-coordinator nodes (`kind=subcoord`).** Use a subcoord node when a plan stage is itself a *sub-team*
rather than a single unit of work — a chunk big enough to warrant its own coordinator and its own
Expand Down Expand Up @@ -210,7 +252,9 @@ gater materializes it as a *distinct coordinator agent* when its blockers finish
Use the **plan-DAG** (`hera_plan`, or `hera_plan_node` + `hera_block`) when work runs in **stages /
dependency order** — author planned nodes wired by blocking edges and let the gater materialize them
as their blockers finish (auto-stacking each stage's branch on the prior). Lay the whole graph out
with one `hera_plan` call; respond to each node's check-in via `hera_inbox`.
with one `hera_plan` call; respond to each node's check-in via `hera_inbox`. **Reconcile the DAG
as work evolves** — edit nodes, drop stale edges, cancel superseded nodes rather than abandoning the
plan.
- **Worker node vs sub-coordinator node:** a plain plan node (`kind=worker`) is a single unit of work.
Make it `kind=subcoord` (with a `goal`) only when the stage is a *sub-team* — large enough to deserve
its own coordinator that plans and fans out its own workers. It's the declarative alternative to a
Expand All @@ -220,11 +264,12 @@ gater materializes it as a *distinct coordinator agent* when its blockers finish
doorbell line.
- **Want whole-team state?** `hera_tree_updates(cwd=$PWD)`, then `hera_get_messages(ids=[…])` for the
ones worth reading.
- **How completion flows back:** a worker finishing calls `hera_status(done)` (and usually a closing
`hera_send` to the coordinator). For a `worker`-kind role that rolls its argus task to `in_review` +
`ready_to_close`, which the coordinator sees in the rail without asking. The roll is idempotent and
only fires when the task is still `in_progress` — it never auto-completes or clobbers a human-set
status. The live session is left running.
- **How completion flows back:** a worker finishing sends a closing `hera_send(status="done", …)` — the
synchronous status apply rolls its task to `in_review` + `ready_to_close`, visible in the rail. A
worker that cannot complete sends `hera_send(status="failed", …)` — rolls to `in_review` WITHOUT
`ready_to_close` (needs attention, not ready to check off); the gater holds any dependent planned
nodes and pings you. Both rolls are idempotent and only fire when the task is still `in_progress`.
The live session is left running.
- **Don't** use `hera_send` to talk to the human — the human reads the coordinator's own agent pane;
the bus is role-to-role only.

Expand All @@ -238,6 +283,10 @@ in the doorbell, returned by `hera_tree_updates`, and stored permanently.

## 6. Gotchas worth calling out

- **`hera_send` requires `status` for worker/freelance senders — omitting it is an error.** The status
is applied synchronously before the send completes; it never rides the async delivery bus. This means
every `hera_send` call doubles as a role-status heartbeat. There is no default; the error message on
omission names the valid values (`idle`/`working`/`blocked`/`done`/`failed`).
- **Spawned workers default to the project's stale default branch, NOT the coordinator's branch.**
`hera_spawn_worker`'s `branch` defaults to the *project* default (e.g. an old `master`/`main`), not the
coordinator's current worktree branch. If the worker must build on the coordinator's (or a sibling's)
Expand Down Expand Up @@ -280,13 +329,14 @@ You opened in a born-bound worker terminal:
1. `hera_join(cwd=$PWD)` → read your role name, mission (role prompt), and unread count.
2. `hera_status(cwd=$PWD, status="working")`.
3. Do the work in your worktree. If you hit a fork that needs the coordinator's call:
`hera_send(cwd=$PWD, body="<question + context>", tldr="Need decision: X vs Y for the cart schema")`
`hera_send(cwd=$PWD, status="working", body="<question + context>", tldr="Need decision: X vs Y for the cart schema")`
(no `to` needed — default-routes to the coordinator), then check `hera_inbox(cwd=$PWD)` on the
doorbell for the answer.
doorbell for the answer. **Always supply `status` on every `hera_send` — it is required for
worker/freelance senders.**
4. Land your work (open a PR via iris, or leave commits for the coordinator to pull).
5. `hera_send(cwd=$PWD, body="<summary + PR link>", tldr="cart-api done, PR #47, tests green")` then
`hera_status(cwd=$PWD, status="done")` — which rolls your task to in_review + ready_to_close so the
coordinator sees you finished.
5. `hera_send(cwd=$PWD, status="done", body="<summary + PR link>", tldr="cart-api done, PR #47, tests green")`
— the synchronous status apply rolls your task to in_review + ready_to_close so the coordinator
sees you finished. If you cannot complete: `hera_send(cwd=$PWD, status="failed", body="<reason>", …)`.

### (c) Author a staged plan-DAG and let it self-materialize

Expand All @@ -311,10 +361,14 @@ You are a coordinator and the work has clear stages (a seed, a parallel fan-out,
`1a-seed` materializes first (rooted on your branch); `2a`/`2b` materialize in parallel once it's
`done` (each stacked on `1a-seed`'s branch); `3a-final` waits for **both** and stacks on the latest.
3. Watch it fill in the second-tab plan-DAG (planned `○` → live). Respond to each node's check-in:
`hera_inbox(cwd=$PWD)` on the doorbell → reply `hera_send(to="<node>", body="go", tldr="go")`.
4. If a node is HELD behind a genuinely failed blocker, the gater pings you — re-plan, unblock, or
re-dispatch. (A coordinator-as-blocker edge is rejected at authoring time, so you can't wedge the
`hera_inbox(cwd=$PWD)` on the doorbell → reply `hera_send(cwd=$PWD, to="<node>", body="go", tldr="go")`.
4. If a node is HELD behind a genuinely failed blocker, the gater pings you — use `hera_unblock` to
drop the edge, `hera_plan_node_cancel` to cancel the held node, or `hera_spawn_worker` to dispatch
a replacement. (A coordinator-as-blocker edge is rejected at authoring time, so you can't wedge the
graph on a never-`done` coordinator.)
5. **Reconcile as work unfolds.** If a worker's scope changed: `hera_plan_node_update` its prompt before
it materializes. If an edge is obsolete: `hera_unblock`. If a node was superseded: `hera_plan_node_cancel`.
Keep the DAG a live mirror of the actual plan.

### Worker promotion: becoming a sub-coordinator

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,15 +417,18 @@ If the recipient has a live agent session the daemon also writes a single notifi
| `hera_new_orchestrator` | Bootstrap a new orchestrator and claim its `coordinator` role for the calling task. |
| `hera_join` | Claim the calling task's existing role + unread count, or (with `role_name` + `kind`) attach a new `worker`/`freelance` role under an orchestrator. |
| `hera_spawn_worker` | Spawn a born-bound worker task + session under the caller's orchestrator (caller must hold a live coordinator binding). Optional `model` picks the worker's model by task complexity (backend-scoped; empty = backend default). |
| `hera_send` | Send a role-addressed message; workers/freelancers default to the coordinator when `to` is omitted, coordinators must name a recipient. |
| `hera_send` | Send a role-addressed message. **`status` is required for worker/freelance senders** (`idle`/`working`/`blocked`/`done`/`failed`) and is applied synchronously before send. Workers/freelancers default to the coordinator when `to` is omitted; coordinators must name a recipient. |
| `hera_inbox` | Fetch the caller role's unread messages (oldest first), cancel their pending pane deliveries, and mark them read. |
| `hera_mark_read` | Mark a specific list of message IDs read and cancel their pending deliveries. |
| `hera_status` | Set the caller role's status (`idle`/`working`/`blocked`/`done`), mirrored to `task_meta`; a worker reporting `done` rolls its task to in-review. |
| `hera_status` | Set the caller role's status (`idle`/`working`/`blocked`/`done`/`failed`), mirrored to `task_meta`; `done` rolls the worker's task to in-review + `ready_to_close`; `failed` rolls to in-review without `ready_to_close`. |
| `hera_tree_updates` | Scan the caller's orchestrator subtree for messages since a per-role cursor; returns TLDR subject lines only and auto-advances the cursor. |
| `hera_get_messages` | Fetch full message bodies by ID (after `hera_tree_updates`), scoped to the caller's orchestrator subtree. |
| `hera_plan_node` | Author a single planned node under the caller's orchestrator (coordinator-only). Params: `name`, `kind` (`worker`\|`subcoord`, default `worker`), `prompt` (worker nodes) or `goal` (subcoord nodes — required; the goal handed to the spawned coordinator). A `subcoord` node materializes as a distinct coordinator agent with its own task, worktree, and child orchestrator. |
| `hera_block` | Add a blocking edge: `blocked` waits until `blocker` reaches role-status `done`. Coordinator-only; both roles must be in the same orchestrator. No cycles. |
| `hera_plan` | Author an entire plan graph in one call: a `nodes` array (each with `name`, `kind`, `prompt`/`goal`, optional `project`) and an `edges` array (`blocked`→`blocker` pairs). Coordinator-only; atomically creates all nodes then all edges. Supports mixed `kind` values in the same graph. |
| `hera_plan_node_update` | Edit a planned node's `prompt` and/or `project` before it materializes. Rejected after materialization. |
| `hera_unblock` | Remove a blocking edge between two roles. Idempotent. Re-pointing an edge is `hera_unblock` + `hera_block`. |
| `hera_plan_node_cancel` | Cancel a planned node: stamps `cancelled_at`, excludes it from materialization, unblocks dependents. Kept visible in the plan DAG as grey ✕. |

**Schedule Management:**

Expand Down
Loading
Loading