Skip to content
Open
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
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "hermes-helmet",
"displayName": "Hermes Helmet",
"version": "1.0.1",
"version": "1.1.0",
"description": "First-officer skills for Hermes Helmet: setup, single-issue delivery, dependent-issue coordination, and multi-part writing (Helmet Write), plus the Captain's Bridge mod, which opens a pane bound to the current conversation with /captains-bridge and prepares a source-bound explanation in the background.",
"author": {
"name": "Machine Wisdom",
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hermes-helmet",
"version": "1.0.1",
"version": "1.1.0",
"description": "First-officer skills for Hermes Helmet: setup, single-issue delivery, dependent-issue coordination, and multi-part writing.",
"author": {
"name": "Machine Wisdom",
Expand Down
3 changes: 2 additions & 1 deletion docs/helmet-epic.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ yourself.
`PREFLIGHT → load/validate graph → classify children → dispatch ready frontier → WAITING/DONE`

It invokes `helmet-issue` for each ready child with bounded parallelism
(`max_epic_parallelism`, default 2), continues independent branches when another
(`max_epic_parallelism`, default 2; an explicit `--max-parallelism` persists
for the epic until a later explicit value replaces it), continues independent branches when another
child blocks, and never dispatch-labels the epic root.

## Graph model
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "hermes-helmet"
version = "1.0.1"
version = "1.1.0"
description = "Open-source software factory for coding agents: delegation, review, and repair"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
68 changes: 58 additions & 10 deletions skills/helmet-epic/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,16 @@ Do not use for:

## Accepted delivery outcome

Start from the accepted outcome and current evidence. Use actual Captain/worker
capacity and owner availability; a default parallelism limit does not imply two
available humans. Identify release-critical children, optional work, and final
Start from the accepted outcome and current evidence. Plan capacity with
[Capacity and model plan](#capacity-and-model-plan); a default parallelism limit
does not imply two available humans. Identify release-critical children, optional work, and final
owner-only actions. Do not add acceptance gates because a reviewer suggested
polish, a higher score, or stronger proof. Adopt explicit user scope changes
with their effect on timing visible.

Probe risky access, identity, topology, and API assumptions through the smallest
real authorized path before dispatching dependent machinery. A synthetic
validator is not a substitute for executing the system. Keep a short current
frontier: outcome, active child/PR, next event, wait handle/cursor, and blocker.
validator is not a substitute for executing the system.
Optional work does not become a release prerequisite; required acceptance
remains required. Any proposed graph change still follows graph acceptance.

Expand Down Expand Up @@ -200,9 +199,57 @@ helmet install-skills --target hermes
announcement while waiting. When a pass reaches pending work, register
continuation using the shared procedure below and announce its confirmed
mechanism and effective cadence once.
4. Verify observed GitHub login is Captain and differs from worker.
5. Load epic root: must be open; must **not** carry `dispatch_label`.
6. Stop on authority mismatch, closed root, missing label config, or bad budgets.
4. Apply the [capacity and model plan](#capacity-and-model-plan).
5. Verify observed GitHub login is Captain and differs from worker.
6. Load epic root: must be open; must **not** carry `dispatch_label`.
7. Stop on authority mismatch, closed root, missing label config, or bad budgets.

### Capacity and model plan

The orchestrator schedules, reconciles state, and keeps evidence. It delegates
implementation and independent review rather than repeating them.

1. **Models.** Once at startup, tell the user: for a long epic, select a
low-cost, tool-capable model for this orchestration chat. You cannot change
it yourself; continue either way. Select every delegated model explicitly by
role: the configured builder for implementation, a capable review model at
medium effort for standards and security/spec review, and a bounded
escalation for one hard question. Run deterministic tools before model
judgment. Helmet model lanes (`docs/model-lanes.md`) configure Hermes and its
services, not this chat.
2. **Two pools.** Helmet worker slots are `max_parallelism`. Host collaboration
slots are the subagents this host allows besides the parent. Neither limit
sets the other.
3. **Fill slots.** On startup and on each completion, failure, repair, or
released slot, reconcile the graph, adopt active work, then fill free slots
with eligible, non-duplicate work. `--max-parallelism` persists for the epic
until a later explicit value replaces it (`details.parallelism_source`).
Record each pass compactly:

```text
Worker pool: authorized=10 active=8 eligible-ready=2 -> dispatch 2
Review pool: usable=3 active=2 eligible-ready=1 -> dispatch 1
Idle slots: none
```

An idle slot with eligible work needs a reason: dependency, ownership, or a
security gate. Duplicate work does not count as utilization.
4. **Review assignments.** State the repository, exact head and base, review
axis and standard/spec, model and effort, changed scope, evidence that may be
reused, and the required verdict and evidence artifact. Give self-contained
context, not the full chat. Key reviews by repository + PR + head + axis and
adopt an active review before spawning another. After a repair or base
update, reuse earlier evidence only for proven-unchanged blobs; exact-head
approval stays required.
5. **Audits.** Split large audits into disjoint packets that fit one worker
context, with exact files, exclusions, expected output, and a coverage
contract. The parent reconciles coverage and findings; it does not re-read
every packet.
6. **Continuation record.** Each wake carries only: accepted authority, graph
fingerprint, deadline, the capacity record, active handles, changed heads,
new results, open gates, the next action, and evidence pointers. Replace the
previous record; do not append history. Keep receipts in artifacts and read
them only when a pending decision needs them.

### 2. LOAD + VALIDATE GRAPH

Expand Down Expand Up @@ -231,7 +278,8 @@ Buckets (always re-read GitHub + child checkpoints before acting):

### 4. DISPATCH FRONTIER

1. Ready frontier = ready children, capped by `max_parallelism - active`.
1. Ready frontier = ready children, capped by `max_parallelism - active`
(persisted override or policy default).
2. Default: continue independent branches when another fails/blocks.
3. For each frontier URL invoke `helmet-issue` helpers (`helmet issue` /
`run_preflight_and_adopt`) with epic body for merge inheritance.
Expand Down Expand Up @@ -283,7 +331,7 @@ dependents stay blocked on that closure, not on the merged code. See
- [ ] Root never receives dispatch_label
- [ ] Body fallback Parent / Blocked-by links validate; cycles rejected
- [ ] Linear graphs invoke helmet-issue in dependency order
- [ ] Parallel graphs run at most `max_epic_parallelism` (default 2)
- [ ] Parallel graphs run at most the persisted override or `max_epic_parallelism` (default 2)
- [ ] Blocked branch does not stop unrelated ready children (default)
- [ ] Graph change → GRAPH_CHANGED until `--accept-graph`
- [ ] Reinvocation resumes without duplicate roots
Expand Down
2 changes: 1 addition & 1 deletion src/hermes_helmet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from hermes_helmet.authority import Policy, load_authority, load_policy, render_crew_contract

__version__ = "1.0.1"
__version__ = "1.1.0"

__all__ = [
"Policy",
Expand Down
5 changes: 4 additions & 1 deletion src/hermes_helmet/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1591,7 +1591,10 @@ def build_parser() -> argparse.ArgumentParser:
"--max-parallelism",
type=int,
default=None,
help="Override policy max_epic_parallelism for this pass",
help=(
"Override policy max_epic_parallelism; persisted for this epic "
"until a later explicit value replaces it"
),
)
epic_p.add_argument(
"--child",
Expand Down
22 changes: 17 additions & 5 deletions src/hermes_helmet/helmet_epic.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ class EpicCheckpoint:
graph_fingerprint: str | None = None
accepted_fingerprint: str | None = None
max_parallelism: int = 2
parallelism_source: str = "policy"
continue_independent_branches: bool = True
active_children: list[str] = field(default_factory=list)
completed_children: list[str] = field(default_factory=list)
Expand Down Expand Up @@ -395,6 +396,11 @@ def _str_list(key: str) -> list[str]:
else str(raw.get("accepted_fingerprint"))
),
max_parallelism=max(1, int(raw.get("max_parallelism") or 2)),
parallelism_source=(
"override"
if raw.get("parallelism_source") == "override"
else "policy"
),
continue_independent_branches=bool(
raw.get("continue_independent_branches", True)
),
Expand Down Expand Up @@ -1865,11 +1871,16 @@ def run_epic_pass(
checkpoint.host_continuation = host_continuation
checkpoint.one_pass_only = one_pass_only
checkpoint.continue_independent_branches = continue_independent_branches
parallelism = (
max_parallelism
if max_parallelism is not None
else max(1, int(policy.max_epic_parallelism))
)
# An explicit override persists for this epic until a later explicit
# value replaces it, so continuations that omit the flag keep capacity.
if max_parallelism is not None:
parallelism = max(1, int(max_parallelism))
checkpoint.parallelism_source = "override"
elif checkpoint.parallelism_source == "override":
parallelism = max(1, int(checkpoint.max_parallelism))
else:
parallelism = max(1, int(policy.max_epic_parallelism))
checkpoint.parallelism_source = "policy"
checkpoint.max_parallelism = parallelism
if not checkpoint.started_at:
checkpoint.started_at = _utc_now()
Expand Down Expand Up @@ -2431,6 +2442,7 @@ def build_epic_status(
blocker=blocker,
terminal=state in EPIC_TERMINAL,
details={
"parallelism_source": checkpoint.parallelism_source,
"graph_source": graph.source,
"child_count": len(graph.children),
"epic_merge_mode": checkpoint.epic_merge_mode,
Expand Down
77 changes: 77 additions & 0 deletions tests/test_helmet_epic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,83 @@ def test_ready_frontier_caps_at_two_and_skips_blocked(self) -> None:
[],
)

def test_active_children_consume_override_slots(self) -> None:
ready = [f"https://github.com/x/y/issues/{n}" for n in range(1, 6)]
buckets = {
"active": [f"https://github.com/x/y/issues/{n}" for n in range(10, 18)],
"ready": ready,
"failed": [],
}
self.assertEqual(
he.compute_ready_frontier(buckets, max_parallelism=10), ready[:2]
)

def _no_dispatch_pass(self, ck: Path, policy=None, **kwargs):
checkpoint, report, _ = he.run_epic_pass(
policy or self.policy,
EPIC_URL,
checkpoint_dir=ck,
runner=self.runner,
extra_child_urls=[CHILD_A, CHILD_B, CHILD_C, CHILD_H],
apply_dispatch=False,
accept_graph=True,
**kwargs,
)
return checkpoint, report

def test_parallelism_override_persists_across_passes(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
ck = Path(tmp)
first, _ = self._no_dispatch_pass(ck, max_parallelism=10)
self.assertEqual(
(first.max_parallelism, first.parallelism_source), (10, "override")
)
second, report = self._no_dispatch_pass(ck)
self.assertEqual(
(second.max_parallelism, second.parallelism_source),
(10, "override"),
)
self.assertEqual(report.max_parallelism, 10)
self.assertEqual(report.details.get("parallelism_source"), "override")
# A policy change does not silently undo the accepted override.
third, _ = self._no_dispatch_pass(
ck, policy=_policy(max_epic_parallelism=3)
)
self.assertEqual(third.max_parallelism, 10)
# A later explicit value replaces it.
fourth, _ = self._no_dispatch_pass(ck, max_parallelism=4)
fifth, _ = self._no_dispatch_pass(ck)
self.assertEqual(
(fourth.max_parallelism, fifth.max_parallelism), (4, 4)
)

def test_policy_parallelism_follows_policy_without_override(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
ck = Path(tmp)
first, _ = self._no_dispatch_pass(ck)
self.assertEqual(
(first.max_parallelism, first.parallelism_source), (2, "policy")
)
second, _ = self._no_dispatch_pass(
ck, policy=_policy(max_epic_parallelism=5)
)
self.assertEqual(
(second.max_parallelism, second.parallelism_source), (5, "policy")
)

def test_legacy_checkpoint_parallelism_loads_as_policy(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
ck = Path(tmp)
self._no_dispatch_pass(ck, max_parallelism=7)
path = he.epic_checkpoint_path(ck, EPIC_URL)
raw = json.loads(path.read_text())
raw.pop("parallelism_source")
path.write_text(json.dumps(raw))
loaded = he.load_epic_checkpoint(path, expected_epic_url=EPIC_URL)
self.assertEqual(loaded.parallelism_source, "policy")
again, _ = self._no_dispatch_pass(ck)
self.assertEqual(again.max_parallelism, 2)

def test_linear_then_parallel_invocation_order(self) -> None:
invoked: list[str] = []

Expand Down
Loading