diff --git a/.claude/skills/hera-plan/SKILL.md b/.claude/skills/hera-plan/SKILL.md new file mode 100644 index 00000000..c49c6bbd --- /dev/null +++ b/.claude/skills/hera-plan/SKILL.md @@ -0,0 +1,209 @@ +--- +name: hera-plan +description: >- + The hera plan-DAG: author staged, dependency-ordered multi-worker plans inside an argus sandbox + and let the daemon gater materialize each node into a born-bound worker as its blockers finish. + Load this (in addition to the base `hera` skill) when you are a coordinator and the work + decomposes into multiple hera worker units that have DEPENDENCIES among them (one stage needs + another's output, or a required ordering) — internal dependencies are the clean signal to plan a + DAG rather than spawn workers ad hoc. Covers the authoring/mutation verbs, gating-on-`done`, + automatic branch-stacking and its fan-in footgun, short-id node naming, sub-coordinator nodes, and + the self-guard prompt patterns. NOT for non-argus sessions; NOT for in-session ephemeral work (use + Claude's native sub-agents); NOT for independent workers with no ordering (just spawn them). +--- + +# Hera plan-DAG — staged, dependency-ordered multi-agent work + +This is the **coordinator-only** plan-DAG layer of hera. It assumes you already hold a live +coordinator binding and know the base hera model (roles, bindings, messaging, `hera_spawn_worker`, +status/tree) from the `hera` skill — **load that first if you haven't.** Every tool here takes +`cwd` (pass `cwd=$PWD`) and `orchestrator` is required when your task holds 2+ live bindings. + +## When the plan-DAG is the right tool + +The base `hera` skill's decision triad gets you here: the work decomposes into units that each must +be **their own argus session** (separate worktree / own PR / long-running / own sandbox), **and** +those units have **dependencies among them**. That dependency is the clean trigger: + +- **Dependencies present** (stage B needs stage A's branch/output, or a required ordering) → **plan + a DAG.** Author planned nodes wired by blocking edges; the gater runs them in order. *Decide this + yourself when the dependencies are obvious — don't ask the human.* Only ask the human when it's + genuinely ambiguous whether the effort warrants multi-session orchestration at all. +- **Independent units, no ordering** → don't author a DAG; just `hera_spawn_worker` them in parallel. +- **Ephemeral in-session work** (research, review, fan-out reads that return to you) → not hera at + all; use Claude's native sub-agents (Agent/Task tool). + +> **With a live coordinator binding, the plan-DAG is the single source of truth for the staged +> worker activity it covers. Author the workers as plan nodes; track progress through the DAG; +> reconcile the plan as work evolves. The harness `TaskCreate` system-reminder does not apply to +> coordinated work — use the plan/spawn tools, never bare task creation.** + +## The gating contract (how nodes become live workers) + +A **planned node** is a worker role with no live agent / worktree / inbox yet — one DB row. The +daemon gater (~60s tick) materializes it into a born-bound worker (exactly what `hera_spawn_worker` +would produce) according to this contract: + +- **A node materializes ONLY when EVERY blocker reaches hera role-status `done`** — the worker's + explicit "I'm finished" (which rolls its task to `in_review`). Role-status `done`, **not** task + status, **not** idle. +- A blocker still `working` (e.g. iterating on CI) keeps the dependent **planned** — the next stage + never starts under churning work. +- A blocker whose session **ended without ever reaching `done`** (crash, or it gave up / reported + `failed`) **HOLDS** the dependent (no materialize) and pings you. No worker is ever spawned-and- + parked behind dead or unfinished work. +- A node with **no blockers** is a root and materializes on the next tick. +- A cancelled planned node is treated as satisfied (non-blocking) — its dependents proceed. + +## Authoring verbs + +- **`hera_plan_node(cwd, name, prompt, [orchestrator], [project], [kind], [goal])`** — create ONE + planned node. **Name nodes by a `` short-id — number = serial stage, letter = + parallel member (`1a`, `2a`, `2b`, `3a`)** — optionally with a *terse* suffix (`1a-seed`, + `2a-alpha`). This is **not cosmetic**: the rail/DAG renders one box per node, and long descriptive + names (`backend-api-handlers`, `frontend`) blow the boxes wide and wreck legibility once you have + more than a handful of stages, while `2a`-style ids keep the graph tight and scannable. Names are + uniquified within the orchestrator. `project` defaults to the coordinator's own. + - `kind` — `worker` (default) or `subcoord`. A **worker** node materializes into a live born-bound + worker; `prompt` is delivered to it (a check-in standing-order is prepended automatically). A + **subcoord** node materializes into a *distinct sub-coordinator agent* — see "Sub-coordinator + nodes" below. + - `goal` — **required for `kind=subcoord`** (used instead of `prompt`): the objective handed to the + sub-coordinator. You hand only the goal — not its child orchestrator name or its sub-plan. + +- **`hera_block(cwd, blocked, blocker, [orchestrator])`** — add a blocking edge: `blocked` waits on + `blocker` reaching role-status `done` before it materializes. Both roles must be in your + orchestrator. **Rejected** on a cycle, cross-orchestrator endpoints, or a **coordinator** blocker + (a coordinator never reaches `done`, so it would be permanently unsatisfiable). + +- **`hera_plan(cwd, nodes, [edges], [orchestrator])`** — submit a WHOLE graph in one + **transactional** call: `nodes` = `[{name, prompt, [project], [kind], [goal]}]`, `edges` = + `[{blocked, blocker}]` referencing nodes by name (or existing roles). **All-or-nothing** — any + cycle / cross-orchestrator / coordinator-blocker / validation error rolls back the entire graph + (no orphan nodes). The way to lay out a multi-stage plan at once. **Name every node by its + `` short-id** so the rendered DAG stays tight. + +## Mutation verbs — the DAG is living, not authoring-time + +Update the graph as reality diverges from the plan; don't abandon it. + +- **`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). Use when you discover the spec needs revision before the node spawns. +- **`hera_unblock(cwd, blocked, blocker, [orchestrator])`** — drop one blocking edge. Idempotent. To + re-point: `hera_unblock` (old blocker) then `hera_block` (new blocker). +- **`hera_plan_node_cancel(cwd, name, [orchestrator])`** — cancel a planned node: it never + materializes, its dependents proceed (no longer gated on it), it stays visible as a grey ✕. + Rejected once materialized (use the task lifecycle to stop a running worker). + +**Standing order:** after every worker interaction, check whether the plan still mirrors reality — +`hera_plan_node_update` a changed scope, `hera_unblock` an obsolete edge, `hera_plan_node_cancel` a +superseded node. A worker re-engaging on rework after `done`/`failed` reports `working` on its next +`hera_send` by requirement, so the DAG self-corrects for a simple reopen. + +## Materialization + branch-stacking (the gater drives this, not you) + +- **Non-root nodes stack automatically**: a materializing node is branched off its most-recently- + `done` blocker's branch, so a *linear* chain produces cleanly stacked PRs. +- **Fan-in stacks on ONE blocker, not a merge of all.** A node with multiple blockers bases off the + *single* most-recently-`done` blocker's branch — it does **not** merge the others in. In a diamond + (`3a` blocked by both `2a` and `2b`), `3a` starts from whichever of `2a`/`2b` materialized later + and is **missing the other's work** unless those two were themselves stacked. For true fan-in, + either keep the stages a linear chain, or have the fan-in node merge the branches itself via a + self-rebase step (see below). +- **`done` gates materialization, but `done` ≠ merged/integrated.** A worker reaching `done` rolls + its task to `in_review` (*not* merged) — so the gater materializes the dependent the instant the + blocker *reports* done, **before** you've reviewed or merged anything. The dependent stacks on the + blocker's worker branch as it stood at `done`. That's exactly right for a linear stack where that + branch *is* the integration point; but if your workflow merges upstream work into a separate + feature branch before cutting the next stage, the materialized node will be racing ahead of your + merge — make node prompts self-defending (next section). +- **Root nodes** (no blockers) resolve their base branch as: explicit orchestrator `base_branch` → + the coordinator role's bound-task branch → the project default. Root a plan on your feature branch + by passing `base_branch` to `hera_new_orchestrator`. +- **Respond to check-ins promptly.** Each node check-ins on materialization via `hera_send`; pull it + from `hera_inbox` and reply (e.g. `"go"`). A node HELD behind a genuinely failed blocker pings you + — `hera_unblock` the edge, `hera_plan_node_cancel` the held node, or `hera_spawn_worker` a + replacement. (Coordinator-as-blocker is rejected at authoring time, so the graph can't wedge on a + never-`done` coordinator.) + +## Self-defending node prompts (the standard mitigation) + +Because a node materializes the instant its blockers *report* `done` — ahead of your review/merge — +any node that depends on upstream output should carry two prompt-side guards: + +- **Self-rebase** — the node's first step is `git merge --no-edit origin/` (or + the sibling branch in a fan-in) to pull in whatever is integrated so far. +- **Self-guard** — the node greps for the API routes / files / symbols it depends on and, if absent, + `hera_send`s you to wait instead of building against a phantom contract. + +This is what makes plan-mode safe for stacked-integration and contract-discovery work, so you rarely +need to fall back to driving every stage by hand. Reserve pure incremental `hera_spawn_worker` +(spawn the next stage manually only after you've merged the prior) for when even self-guarding is too +racy — i.e. a hard human/coordinator decision gate must sit between phases. + +## Sub-coordinator nodes (`kind=subcoord`) + +Use a subcoord node when a plan stage is itself a *sub-team* — a chunk big enough to warrant its own +coordinator and its own fan-out — rather than a single unit of work. It's the **declarative** form of +worker promotion (a worker calling `hera_new_orchestrator` on itself mid-task): you author the +sub-team as a plan node up front, and the gater materializes it as a *distinct coordinator agent* +when its blockers finish. + +- It occupies the parent DAG exactly like any node (a worker role in **your** orchestrator) — blocking + edges, gating, hold/ping, and branch-stacking all treat it identically; its worker-role `done` gates + the parent's dependents. +- At materialization it becomes **one new agent** (own task + worktree) that is simultaneously a + worker in your orchestrator AND the coordinator of a freshly-created, auto-named child orchestrator + — so it nests under you in the rail/tree via the multi-binding bridge, never sharing your task. +- You hand it only the `goal`. It runs its own planning (often `/brainstorm` → its own `hera_plan`) + and spawns its own workers. Bake rich context into the goal so it needs little back-and-forth. +- Keep it an explicit choice — default to plain worker nodes; don't spin up middle-management for a + stage one worker can do. + +## Worked example — author a staged plan-DAG and let it self-materialize + +The work has a seed, a parallel fan-out, and a fan-in: + +1. Bootstrap (if you haven't): `hera_new_orchestrator(cwd=$PWD, name="", coordinator_role_name="coord")`. + To root the plan on your current feature branch, pass `base_branch="argus/"`. +2. Submit the whole graph transactionally — short-id names, full spec baked into each prompt: + ``` + hera_plan(cwd=$PWD, + nodes=[ + {name:"1a-seed", prompt:""}, + {name:"2a-alpha", prompt:""}, + {name:"2b-beta", prompt:""}, + {name:"3a-final", prompt:""}], + edges=[ + {blocked:"2a-alpha", blocker:"1a-seed"}, + {blocked:"2b-beta", blocker:"1a-seed"}, + {blocked:"3a-final", blocker:"2a-alpha"}, + {blocked:"3a-final", blocker:"2b-beta"}]) + ``` + `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. + **Fan-in caveat:** `3a-final` bases off whichever of `2a`/`2b` finished later — it does NOT + auto-merge the other half. Give `3a-final`'s prompt a self-rebase first step (`git merge --no-edit` + the sibling / integration branch) so it actually has both halves before it builds. +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(cwd=$PWD, to="", body="go", tldr="go")`. +4. If a node is HELD behind a genuinely failed blocker, the gater pings you — `hera_unblock`, + `hera_plan_node_cancel`, or `hera_spawn_worker` a replacement. +5. **Reconcile as work unfolds** — `hera_plan_node_update` a changed scope before it materializes, + `hera_unblock` an obsolete edge, `hera_plan_node_cancel` a superseded node. Keep the DAG a live + mirror of the actual plan. + +## Gotchas worth calling out + +- **The gate is role-status `done`, not idle and not merged.** Idle-without-`done` keeps a node + planned; a session that ended without `done` HOLDS its dependents and pings you. Materialization + fires the instant a blocker *reports* done, which is before your review/merge — see the + branch-stacking and self-defending-prompts sections. +- **Fan-in does not merge — it picks one branch.** A multi-blocker node bases off only the + latest-`done` blocker's branch. Self-rebase the others in, or keep the stages linear. +- **Mutation verbs only work pre-materialization.** `hera_plan_node_update` / `hera_plan_node_cancel` + are rejected once a node has a binding — at that point manage the running worker via the task + lifecycle, not the plan. +- **Short-id names are load-bearing for legibility.** Descriptive node names blow the rail/DAG boxes + wide at scale; use `` ids. diff --git a/.claude/skills/hera/SKILL.md b/.claude/skills/hera/SKILL.md index b406599a..b96b161b 100644 --- a/.claude/skills/hera/SKILL.md +++ b/.claude/skills/hera/SKILL.md @@ -2,17 +2,21 @@ name: hera description: >- Inside an argus sandbox (cwd under ~/.argus/worktrees/ or ARGUS_TASK_ID set), coordinate - multi-agent work via hera's mcp__argus__hera_* tools — bootstrap an orchestrator, claim or - attach a worker/freelance role, and message other roles over the idle-gated bus. Use when you - need to spawn and coordinate other agent sessions, run a large multi-session project, or - message another role. NOT for non-argus sessions, where these MCP tools are not registered. + multi-agent work via hera's mcp__argus__hera_* tools — bootstrap an orchestrator, claim or attach + a worker/freelance role, spawn workers, and message roles over the idle-gated bus. Load this skill + when you are (or are becoming) a coordinator, when you're spawned as a hera worker, or before + delegating work that must run as its OWN argus session (separate worktree / long-running agent / + own PR / own sandbox) — as opposed to in-process Claude sub-agents, which stay the right tool for + ephemeral in-session fan-out. For staged or dependency-ordered multi-worker plans, this skill + routes you to the companion `hera-plan` skill. NOT for non-argus sessions, where these MCP tools + are not registered. --- # Hera — native multi-agent coordination inside argus Hera is argus's **native, in-tree** layer for running a *team* of agents. It is not a separate daemon or plugin — coordination runs in-process in the argus daemon, the rail/tree render directly -in the TUI's second tab, and agents drive it entirely through the twelve `mcp__argus__hera_*` MCP +in the TUI's second tab, and agents drive it entirely through the `mcp__argus__hera_*` MCP tools. State lives in the same `~/.argus/data.sql` (the `hera_*` tables). You never touch the plumbing; you call the tools. @@ -48,11 +52,12 @@ is no CLI fallback and nothing below applies. you **must** pass `orchestrator=` to every tool so hera knows which role you are acting as; omitting it returns an ambiguity error listing your options. -## 3. The twelve tools +## 3. The coordination tools All take `cwd`. `orchestrator` is optional with exactly one live binding and **required** with 2+. -Arg names below are exact — do not invent others. The nine coordination tools come first; the three -**plan-DAG authoring** tools are grouped at the end of this section. +Arg names below are exact — do not invent others. These nine cover bootstrap, messaging, and status; +the plan-DAG authoring/mutation tools live in the companion `hera-plan` skill (pointer at the end of +this section). ### Bootstrap / join @@ -134,107 +139,25 @@ 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 + 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 = - serial stage, letter = parallel member (e.g. `1a-seed`, `2a-alpha`, `2b-beta`, `3a-final`); - uniquified within the orchestrator. `project` defaults to the coordinator's own. - - `kind` — `worker` (default) or `subcoord`. A **worker** node materializes into a live born-bound - worker; `prompt` is delivered to it (a check-in standing-order is prepended automatically, so it - pings you on start). A **subcoord** node materializes into a *distinct sub-coordinator agent* (its - own task/worktree + a fresh child orchestrator it coordinates) — see "sub-coordinator nodes" below. - - `goal` — **required for `kind=subcoord`** (and used instead of `prompt`): the objective handed to - the sub-coordinator. You hand only the goal — you do NOT name its child orchestrator or author its - sub-plan; the sub-coordinator owns its own decomposition. - -- **`hera_block(cwd, blocked, blocker, [orchestrator])`** — add a blocking edge: `blocked` waits on - `blocker` reaching role-status `done` before it materializes. Coordinator-only; both roles must be in - your orchestrator. **Rejected** if it would create a cycle, if the roles are in different - orchestrators, or if `blocker` is a **coordinator** role (a coordinator never reaches `done`, so it - would be a permanently-unsatisfiable dependency). - -- **`hera_plan(cwd, nodes, [edges], [orchestrator])`** — submit a WHOLE graph in one **transactional** - call: `nodes` = `[{name, prompt, [project], [kind], [goal]}]` (per-node `kind` = `worker` default | - `subcoord`; a `subcoord` node needs `goal`, not `prompt`), `edges` = `[{blocked, blocker}]` - referencing nodes by name (or existing roles). Nodes are created first, then edges (cycle-checked, - single-orchestrator). - **All-or-nothing** — any cycle / cross-orchestrator / coordinator-blocker / validation error rolls - 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` - blocker's branch, so a chain produces cleanly stacked PRs. -- **Root nodes** (no blockers) resolve their base branch as: explicit orchestrator `base_branch` → - the coordinator role's bound-task branch → the project default. So a plan rooted on your feature - 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 `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 -fan-out. It is the **declarative** form of "worker promotion" (§7): instead of spawning a worker that -later calls `hera_new_orchestrator` on itself, you author the sub-team as a plan node up front, and the -gater materializes it as a *distinct coordinator agent* when its blockers finish. Mechanics: -- It occupies the parent DAG exactly like any node — it is a worker role in **your** orchestrator, so - blocking edges, gating, hold/ping, and branch-stacking all treat it identically. Its worker-role - `done` gates the parent's dependents. -- At materialization it becomes **one new agent** (own task + worktree) that is simultaneously a worker - in your orchestrator AND the coordinator of a freshly-created child orchestrator (named automatically, - de-collided) — so it nests under you in the rail/tree via the multi-binding bridge, never sharing your - task. **One claude instance = one rail element** holds. -- You hand it only the `goal`. It runs its own planning (often `/brainstorm` → its own `hera_plan`) and - spawns its own workers. Bake rich context into the goal so it needs little back-and-forth; it can - still `hera_send` you for guidance. -- Keep it an explicit choice — default to plain worker nodes for ordinary stages. Don't spin up - middle-management for a stage one worker can do. +### Plan-DAG authoring (staged, dependency-ordered work) → the `hera-plan` skill + +> **With a live coordinator binding, do NOT use the harness `TaskCreate` system-reminder for +> coordinated work — coordinate via the hera tools (`hera_spawn_worker`, or the plan-DAG), never +> bare task creation.** + +For work that decomposes into multiple hera worker units **with dependencies among them** (one stage +needs another's output, or a required ordering), hera offers a **plan-DAG**: planned nodes wired by +blocking edges that the daemon gater materializes into born-bound workers in dependency order, +auto-stacking each stage's branch on the prior. It renders in the TUI's second tab and is a *living* +graph (edit / re-point / cancel as work evolves). + +**Load the companion `hera-plan` skill** before authoring or driving a plan-DAG. It carries the six +plan tools (`hera_plan`, `hera_plan_node`, `hera_block`, `hera_plan_node_update`, `hera_unblock`, +`hera_plan_node_cancel`), the gating contract (a node materializes ONLY when every blocker reaches +role-status `done`), automatic branch-stacking and its fan-in footgun, short-id node naming, +sub-coordinator nodes, and the self-guard prompt patterns — none of which the tool schemas convey. The +decision triad in §4 tells you *when* the plan-DAG is the right tool versus spawning workers directly +or using in-session sub-agents. ## 4. Decision rules @@ -248,17 +171,20 @@ gater materializes it as a *distinct coordinator agent* when its blockers finish - **`spawn_worker` vs adopt:** native hera has **no adopt step** — workers are born bound at spawn time. (The old `depends_on`-driven auto-adopt watcher was retired with the DAG.) To delegate, just `hera_spawn_worker`. -- **Spawn now vs plan a DAG:** use `hera_spawn_worker` when you want a worker running *immediately*. - 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`. **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 - worker promoting itself mid-task (§7). Default to worker nodes. +- **The coordination decision — in-session sub-agents vs hera workers vs the plan-DAG (settle this + BEFORE delegating):** + 1. **Ephemeral, in-session work** — research, review, fan-out reads, anything that returns results + to you and needs no worktree/PR of its own? → use **Claude's native sub-agents** (Agent/Task + tool). NOT hera. Hera is overkill and slower for in-session parallelism. + 2. **Work whose unit must be its OWN argus session** — separate worktree / its own PR / long-running + / its own sandbox? → **hera**. Then split by dependency: + - Units have **dependencies among them** (one needs another's output, or a required ordering)? → + **plan-DAG**: load the `hera-plan` skill, author planned nodes + blocking edges, let the gater + run them in dependency order. *Decide this yourself when the dependency is obvious — don't ask + the human.* **Internal dependencies are the clean signal to plan a DAG** rather than spawn ad hoc. + - Units are **independent** (no ordering)? → just `hera_spawn_worker` them in parallel; no DAG. + 3. **Ask the human only** when it's genuinely ambiguous whether the work warrants multi-session + orchestration at all — never for the routine dependency call above. - **This task holds 2+ bindings?** Pass `orchestrator=` on EVERY tool call. - **Got a doorbell?** Call `hera_inbox(cwd=$PWD)` immediately — the content is in the inbox, not the doorbell line. @@ -338,44 +264,18 @@ You opened in a born-bound worker terminal: — 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="", …)`. -### (c) Author a staged plan-DAG and let it self-materialize - -You are a coordinator and the work has clear stages (a seed, a parallel fan-out, a fan-in): - -1. Bootstrap (if you haven't): `hera_new_orchestrator(cwd=$PWD, name="", coordinator_role_name="coord")`. - To root the plan on your current feature branch, pass `base_branch="argus/"`. -2. Submit the whole graph transactionally — short-id names, full spec baked into each prompt: - ``` - hera_plan(cwd=$PWD, - nodes=[ - {name:"1a-seed", prompt:""}, - {name:"2a-alpha", prompt:""}, - {name:"2b-beta", prompt:""}, - {name:"3a-final", prompt:""}], - edges=[ - {blocked:"2a-alpha", blocker:"1a-seed"}, - {blocked:"2b-beta", blocker:"1a-seed"}, - {blocked:"3a-final", blocker:"2a-alpha"}, - {blocked:"3a-final", blocker:"2b-beta"}]) - ``` - `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(cwd=$PWD, to="", 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. +### (c) Author a staged plan-DAG + +When the work decomposes into multiple multi-session units with dependencies among them, **load the +`hera-plan` skill** and author a plan-DAG — it carries the tools, gating, branch-stacking, naming, and +a full worked seed→fan-out→fan-in example. The §4 decision triad tells you when that's the right tool. ### Worker promotion: becoming a sub-coordinator > If you already know up front (at plan-authoring time) that a stage is a sub-team, prefer the -> **declarative** form: a `kind=subcoord` plan node (§3). The gater then materializes the -> sub-coordinator for you when its blockers finish. The runtime promotion below is for when a worker -> discovers the need *mid-task*. +> **declarative** form: a `kind=subcoord` plan node (see the `hera-plan` skill). The gater then +> materializes the sub-coordinator for you when its blockers finish. The runtime promotion below is for +> when a worker discovers the need *mid-task*. If a worker realizes mid-task it needs its own team (cross-repo work, real parallelism, a long sub-task): diff --git a/README.md b/README.md index 917567e5..d2284c5e 100644 --- a/README.md +++ b/README.md @@ -397,7 +397,7 @@ Argus runs an MCP server on port 7742 and auto-injects it into every agent workt | `task_complete` | Mark a task as complete (sets status, stamps `EndedAt`). Pass `cwd` or `id`. Does NOT stop a running agent — call `task_stop` first if needed. | | `task_set_result` | Persist an opaque JSON result blob the orchestrator can read (PR URL, milestone, failure reason). Pass `cwd` or `id` plus `result`. Up to 64 KiB. | -The bundled skills (`.claude/skills/{archive,argus-complete,argus-schedule,hera}`, installed via `./install-claude-skills.sh` — see [Agent-facing skills](#agent-facing-skills)) let an agent finalize, schedule, and coordinate its own work via `cwd` resolution. Completing and archiving are independent axes. +The bundled skills (`.claude/skills/{archive,argus-complete,argus-schedule,hera,hera-plan}`, installed via `./install-claude-skills.sh` — see [Agent-facing skills](#agent-facing-skills)) let an agent finalize, schedule, and coordinate its own work via `cwd` resolution. Completing and archiving are independent axes. **Inter-Task Messaging** (peer-to-peer between live or paused tasks): @@ -456,7 +456,8 @@ If the recipient has a live agent session the daemon also writes a single notifi A Claude session inside an argus worktree sees the `mcp__argus__*` tool names but not when to reach for them or how they compose. Argus ships that orientation as installable Claude assets, gated at runtime so they stay inert outside argus sandboxes: -- `.claude/skills/hera/SKILL.md` — coordinate multi-agent work over hera's `mcp__argus__hera_*` tools (bootstrap an orchestrator, claim or attach a worker/freelance role, message other roles). +- `.claude/skills/hera/SKILL.md` — coordinate multi-agent work over hera's `mcp__argus__hera_*` tools (bootstrap an orchestrator, claim or attach a worker/freelance role, message other roles); carries the decision rule for in-session sub-agents vs hera workers vs the plan-DAG. +- `.claude/skills/hera-plan/SKILL.md` — the on-demand plan-DAG companion: author staged, dependency-ordered multi-worker plans (planned nodes + blocking edges) that the daemon gater materializes in order, with branch-stacking, short-id naming, and self-guard prompt patterns. - `.claude/skills/{archive,argus-complete,argus-schedule}/SKILL.md` — let an agent finalize, archive, and schedule its own argus task via `cwd` resolution. - `claude/snippets/*.md` — always-loaded orientation snippets (`hera.md`, `argus-tasks.md`) that point the agent at the skills above. diff --git a/claude/snippets/hera.md b/claude/snippets/hera.md index 9500a000..762aa79b 100644 --- a/claude/snippets/hera.md +++ b/claude/snippets/hera.md @@ -6,4 +6,10 @@ audience: [shared] If `ARGUS_TASK_ID` is unset and `$PWD` is not under `~/.argus/worktrees/`, ignore this section. -Inside an argus sandbox, multi-agent coordination — bootstrapping an orchestrator, claiming or attaching a worker/freelance role, and messaging other roles — runs through **hera**'s `mcp__argus__hera_*` tools. When you need to spawn and coordinate other agent sessions, run a large multi-session project, or message another role, consult the `hera` skill for the tool surface, decision rules, and worked workflows. Don't hand-roll coordination. +Inside an argus sandbox, coordinating a *team* of agent sessions runs through **hera**'s `mcp__argus__hera_*` tools — never hand-roll it, and don't use bare `TaskCreate` for coordinated work. + +- **If you are spawned as a hera worker, or you hold (or are creating) a coordinator binding: load the `hera` skill in full before coordinating.** The tool schemas alone don't convey the role model, messaging, or decision rules. +- **Pick the right tool for the work:** + - Ephemeral in-session fan-out (research, review, parallel reads that return to you) → Claude's native sub-agents, NOT hera. + - Work whose unit must be its own argus session (own worktree / PR / sandbox / long-running) → hera workers via `hera_spawn_worker`. + - Those units have dependencies among them (one needs another's output, or a required ordering)? → a **plan-DAG**: load the `hera-plan` skill and author planned nodes wired by blocking edges. **Internal dependencies are the clean signal to plan a DAG** — decide it yourself when the dependency is obvious; only ask the human when it's genuinely unclear the work even warrants multi-session orchestration.