Wire delegation to A12's trigger, and implement A3's resume rule
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m8s

Two gaps found by auditing spec against code, the second created by
7b landing the harness without its boot-time half.

**The harness was not connected to anything.** Only a live test
constructed a Delegation; A12's failure-count trigger still gated to
the human, which A13 describes as the fallback rather than the path.
Consecutive command failures now hand the sub-task to the delegate,
with the failures in the brief so it need not rediscover what the
local model learned the hard way. A usable handback lets the local
loop carry on; anything else escalates, because a cap breach leaves a
half-edited tree.

**A3 step 5 was unimplemented.** A crash mid-delegation failed safe —
boot gated on "restarted mid-EXECUTING" — but always gated, even on
the clean cut the spec says should resume, and left the branch orphaned
because _abandon only runs while the process lives.

- A delegation now records an intent before the branch exists, and
  settles it whichever way the delegation ends; an aborted delegation
  is a finished one, and leaving it pending would make the next boot
  believe work was still in flight
- delegation_verdict implements the clean-cut test as an observable
  check, never model self-assessment: settled if a completion follows
  the start, absent if the branch was never created, gate on a
  tool_call with no recorded result, resume otherwise
- recover() sweeps at boot, announcing a resume with a notice rather
  than a question, since a clean cut needs no decision from the
  operator, and leaving an unclean one pending so the generic sweep
  gates on it
- resume() continues the same delegation rather than starting a
  second: no new branch, no second delegation_started, because A10
  defines a session as the delegation, not the process lifetime

Honest limit, stated in the code: "the branch sits at the expected
commit" is approximated by the branch existing and carrying the work,
since per-turn shas are not recorded in the log, so a branch rewritten
underneath us would go undetected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dongarandClaude Opus 5 committed 2026-09-08 20:03:46 +00:00
1 parent 608a3a5bfb
commit ce436262f4
5 files changed
+414 -5

No files matched your search

+7
View File
@@ -163,6 +163,7 @@ class Orchestrator:
publisher: "MilestonePublisher | None" = None,
gitea_operators: Iterable[str] = (),
board: "PlanBoard | None" = None,
delegate: Any = None,
) -> None:
self._store = store
self._transport = transport
@@ -176,6 +177,7 @@ class Orchestrator:
# the other.
self._gitea_operators = set(gitea_operators)
self._board = board
self._delegate = delegate
# Transport and the comment poller deliver on separate threads;
# state transitions must not interleave.
self._lock = threading.RLock()
@@ -184,6 +186,11 @@ class Orchestrator:
transport.on_message(self._handle)
def start(self, reconcilers: Mapping[str, Reconciler] | None = None) -> None:
# A3 step 5 runs before the generic sweep: an interrupted delegation is
# resumed or discarded by inspecting the branch and the log, and only a
# genuinely unclean cut is left for the sweep below to gate on.
if self._delegate is not None:
self._delegate.recover(self._transport.send_message)
# The publisher owns the external-truth query for its own intents, so
# a configured publisher always contributes its reconciler (A3).
registered: dict[str, Reconciler] = dict(reconcilers or {})