Roles, capability profiles and help on the new loop (M2.6)
CI / checks (push) Skipped
CI / checks (pull_request) Failing after 5m23s
CI / e2e (pull_request) Canceled after 21m42s

- roles.driver (unset: the planner's model) and roles.classifier (unset:
  the expert's); the mapping is pinned when an item opens, and the driver
  and the go-ahead reader run on the item's own (L9).
- profiles: in configuration (tools, strengths, weaknesses), used in
  briefings: the driver's instructions say what its helper and expert are
  good at, the helper's brief who reads its handback; never a model name.
- delegate(task) and ask_expert(question) for the driver. A helper works
  on a branch off the milestone's; on a handback its work comes back as
  uncommitted changes, otherwise it is discarded, so harness branches
  never hold commits only the clone has. ask_expert keeps its budget.
- Triggers: failure_limit failed commands in a row (Driver's new "stuck"
  outcome) or the turn cap, mid-milestone, call a helper in once per
  milestone; if it fails too, the item waits on the operator. A
  delegation cut off by a crash is discarded at boot.
- Evaluation capture: delegations and ask_expert as before, and the
  reader's verdicts as classifier pairs.

Tests: help tests (merge-back, discard, needs a milestone, the stuck
trigger and its once-per-milestone limit, the expert and its budget,
pinning, briefings by profile, boot discarding a cut-off delegation),
config tests for roles and profiles, and the walk delegating with a
scripted helper that sometimes fails, with a delegations_settle property.
The walk found helper commits stranded on a merged PR's branch, fixed by
returning a helper's work uncommitted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-07 16:33:26 +00:00
1 parent 04c352b30c
commit 59fd00ce8c
19 files changed
+890 -38

No files matched your search

+27 -8
View File
@@ -12,7 +12,7 @@ config change cannot split a task across two planners.
import json
import time
from dataclasses import asdict, dataclass
from dataclasses import dataclass
from typing import Any, ClassVar, Protocol
import httpx
@@ -107,21 +107,40 @@ class LLMClient(Protocol):
@dataclass(frozen=True)
class RoleMap:
executor: str
planner: str
helper: str
expert: str
executor: str # the old loop's (control.py), until M2.8
planner: str # the old loop's, until M2.8
helper: str # delegations (L9)
expert: str # ask_expert (L9)
driver: str = "" # drives the new loop; unset, the planner's model
classifier: str = "" # reads go-aheads now, actions later (L4); unset, expert's
ROLES: ClassVar[tuple[str, ...]] = ("executor", "planner", "helper", "expert")
ROLES: ClassVar[tuple[str, ...]] = (
"executor",
"planner",
"helper",
"expert",
"driver",
"classifier",
)
_FALLBACK: ClassVar[dict[str, str]] = {"driver": "planner", "classifier": "expert"}
def resolve(self, role: str) -> str:
if role not in self.ROLES:
raise LLMError(f"unknown role: {role!r}")
return str(getattr(self, role))
model = str(getattr(self, role))
if not model and role in self._FALLBACK:
return self.resolve(self._FALLBACK[role])
return model
def pinned(self) -> dict[str, str]:
"""The full resolved mapping, to record at task start (L9)."""
return asdict(self)
return {role: self.resolve(role) for role in self.ROLES}
@classmethod
def from_pinned(cls, pinned: dict[str, str], default: "RoleMap") -> "RoleMap":
"""A work item's pinned mapping; a role it predates comes from
``default``."""
return cls(**{r: pinned.get(r) or default.resolve(r) for r in cls.ROLES})
class LiteLLMClient: