chore(consistency): sync task status from DB [auto]

Updated by fix-consistency on 2026-09-04:
  - update .custodian-brief.md for rein-aharness

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a06ba0-10aa-7ea0-b20a-4f3fac39efe9
This commit is contained in:
custodian-sync 2026-09-04 11:00:20 +02:00
parent 7641fcde40
commit f01b765668
20 changed files with 1027 additions and 165 deletions

View file

@ -2,21 +2,20 @@
# Custodian Brief — rein-aharness
**Domain:** agents
**Last synced:** 2026-08-25 20:42 UTC
**Last synced:** 2026-09-04 09:00 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### Governed runtime integrity and intent convergence
Progress: 0/6 done | workplan_id: `eba2eff1-10a7-50a3-a70b-14e7d398f27f`
Progress: 1/6 done | workplan_id: `eba2eff1-10a7-50a3-a70b-14e7d398f27f`
**Open tasks:**
- ! Verify accepted commits and reconcile metrics/reporting `6297dee4`
- ! Remove tenant logic from the shared runtime `f6c7f80c`
- ! Align deployment, recovery, and conformance with the supported runtime `c4a3f08f`
- ! Re-prove one governed profiled run and close residuals `68a13c4f`
- ► Establish the governance and enforcement baseline `596fc138`
- ► Make each repository run a lease-bound transaction `31da4226`
- · Remove tenant logic from the shared runtime `f6c7f80c`
- · Align deployment, recovery, and conformance with the supported runtime `c4a3f08f`
---
## MCP Orientation (when available)

View file

@ -46,10 +46,15 @@ place.**
verifies the local commit, reports to the State Hub and to the
instance's kaizen metrics.
The harness is the **only credential holder and the only policy
enforcement point** for agent sessions. Instances declare policy; the
harness enforces it. Instances pin a harness major version; upgrades
happen centrally.
Among tenant repositories, blueprints, and scheduling records, only the
selected rein may receive session credential material, and it holds that
material only for the bounded session (ADR-002). The harness is the
**repository-transaction and rein-local tool-policy enforcement point**
inside a chained grant — not the only credential holder or the only
policy enforcement point in the estate. Queue, profile, sandbox,
credential custody/authorization, and provider boundaries enforce their
own grants; this runtime must not weaken them. Instances declare policy
and pin a harness major version; upgrades happen centrally.
## Strategic role

View file

@ -85,8 +85,10 @@ retains a legacy set of local task executors for unattended repository work.
executable bwrap reachability, mounted rein runtime, and model egress remain
with upstream residual `GLAS-IN-0002`.
- Direct legacy agent sessions rely on Claude Code's own tool mediation and
the host checkout. They are not an OS-level sandbox, do not lock the repo,
and do not prove that no push/network activity occurred after the run.
the host checkout. They are not an OS-level sandbox and do not prove that
no push/network activity occurred after the run. Unattended mutation now
takes a process-safe repository lock (ADR-002 / T02); commit acceptance
is still the `HEAD`-changed check until T03.
- Commit acceptance currently means only that `HEAD` changed. It does not yet
validate the changed-file set, branch/parent shape, clean working tree,
remote state, or provenance of the new commit. Metrics are written after

View file

@ -35,8 +35,11 @@ control's operating rhythm depended on a workstation cron bridge
credentials, no tool enumerations.
3. **Blueprints stay in kaizen-agentic; scheduling stays in
activity-core; provider abstraction stays in llm-connect.** The
harness consumes all three and is the only credential holder and
policy enforcement point for agent sessions.
harness consumes all three. Per ADR-002 it is the
repository-transaction and rein-local tool-policy enforcement point
within a chained grant, not the only credential holder or the only
policy enforcement point in the estate. Tenant repos, blueprints, and
schedules still must not hold session credentials or tool wiring.
4. The 2026-07-17 executor-worker prototype is adopted as the harness
seed (`agent_harness/` package). binky-control is tenant #1.
@ -55,3 +58,6 @@ Details: [docs/architecture.md](../architecture.md).
- Repo-boundary discipline continues: any pressure to add scheduling,
blueprint authoring, or tenant-specific logic here is a signal to
extend the neighbor repo or the instance model instead.
- ADR-002 (accepted 2026-08-29) amends the credential-holder and
policy-enforcement slogans in decision 3. The instance/runtime split
and the prohibition on tenant-held credentials are unchanged.

View file

@ -1,19 +1,28 @@
---
id: ADR-002
title: Governed execution is a responsibility chain, not a single enforcement point
status: proposed
status: accepted
date: "2026-08-23"
workplan_task: HARNESS-WP-0003-T01
accepted: "2026-08-29"
workplan_task: REINAH-WP-0003-T01
---
# ADR-002 — Governed execution responsibility chain
## Status
Proposed. Acceptance requires acknowledgement from the owners of Activity Core,
Glas, sand-boxer, and llm-connect. Until then this document records the
implementation target for `HARNESS-WP-0003` but does not amend `INTENT.md` or
supersede accepted ADR-001.
Accepted 2026-08-29 by operator Bernd Worsch. This is the implementation
contract for `REINAH-WP-0003`. It amends the slogan wording in `INTENT.md` and
ADR-001 on the same date; it does not replace ADR-001's instance/runtime split
or the prohibition on tenant-held credentials and tenant-specific runtime
wiring.
The proposed text required sibling-owner acknowledgement before acceptance.
Glas had already approved the boundary; Activity Core had deployed the queue
and lease behavior it asked to see in writing. The operator accepted the
substance without waiting for remaining exact-revision acknowledgements from
Activity Core, sand-boxer, and llm-connect. Those recordings remain welcome;
they are no longer a gate. Sibling objection still changes the matrix.
## Context
@ -211,4 +220,8 @@ Reviews requiring follow-up:
live spoof-refusal evidence, and no manufactured late-close run. Those facts
are incorporated in this revision for final Activity Core acknowledgement.
Pending: revised Activity Core acknowledgement, sand-boxer, and llm-connect.
Pending as recording only, not a gate: revised Activity Core
acknowledgement of this accepted text, sand-boxer, and llm-connect.
Operator acceptance: Bernd Worsch, 2026-08-29, session decision to accept
ADR-002 as written and proceed with T02 wiring.

View file

@ -33,6 +33,11 @@ from llm_connect.claude_code import ClaudeCodeAdapter
from llm_connect.exceptions import LLMSubprocessError, LLMTimeoutError
from llm_connect.models import LLMResponse, RunConfig
from rein_aharness.execution_cancel import (
ExecutionCancel,
ExecutionCancelled,
resolve_cancel,
)
from rein_aharness.profiles import ToolProfile, get_profile
# Backward-compatible alias for the seed profile allow-list string.
@ -41,6 +46,29 @@ ALLOWED_TOOLS = get_profile("green-commit-only").allowed_tools
ToolEventCallback = Callable[[dict[str, Any]], None]
def _kill_process(proc: subprocess.Popen[str] | Any) -> None:
poll = getattr(proc, "poll", None)
if callable(poll):
try:
status = poll()
except Exception:
status = None
if isinstance(status, int):
return
kill = getattr(proc, "kill", None)
if callable(kill):
try:
kill()
except Exception:
return
wait = getattr(proc, "wait", None)
if callable(wait):
try:
wait(timeout=2)
except Exception:
return
def _is_tool_event(event: dict[str, Any]) -> bool:
"""True for tool_use/tool_result content blocks and hook lifecycle events.
@ -64,11 +92,13 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
*,
tool_profile: str | ToolProfile = "green-commit-only",
on_tool_event: ToolEventCallback | None = None,
cancel: ExecutionCancel | None = None,
**kwargs,
):
super().__init__(**kwargs)
self._workdir = workdir
self._on_tool_event = on_tool_event
self._cancel = cancel
if isinstance(tool_profile, ToolProfile):
self._profile = tool_profile
else:
@ -105,27 +135,23 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
return response
def _execute_blocking(self, cmd: list[str], prompt: str, timeout: int) -> LLMResponse:
try:
result = subprocess.run(
cmd,
input=prompt,
capture_output=True,
text=True,
timeout=timeout,
cwd=self._workdir,
)
except subprocess.TimeoutExpired as exc:
raise LLMTimeoutError(
f"claude CLI timed out after {timeout}s", cause=exc
) from exc
if result.returncode != 0:
proc = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
cwd=self._workdir,
)
stdout, stderr = self._wait_for_process(proc, prompt, timeout)
if proc.returncode != 0:
raise LLMSubprocessError(
f"claude CLI exited with code {result.returncode}",
return_code=result.returncode,
stderr=result.stderr,
f"claude CLI exited with code {proc.returncode}",
return_code=proc.returncode,
stderr=stderr,
)
return LLMResponse(
content=result.stdout,
content=stdout,
model=self._model or "claude-code-cli",
usage={},
finish_reason="stop",
@ -170,12 +196,7 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
proc.stdin.write(prompt)
proc.stdin.close()
reader_thread.start()
try:
returncode = proc.wait(timeout=timeout)
except subprocess.TimeoutExpired as exc:
proc.kill()
proc.wait()
raise LLMTimeoutError(f"claude CLI timed out after {timeout}s", cause=exc) from exc
returncode = self._wait_for_process(proc, None, timeout, communicate=False)
reader_thread.join(timeout=5)
stderr = proc.stderr.read() if proc.stderr else ""
@ -200,6 +221,38 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
},
)
def _wait_for_process(
self,
proc: subprocess.Popen[str],
prompt: str | None,
timeout: int,
*,
communicate: bool = True,
) -> Any:
cancel = resolve_cancel(self._cancel)
if cancel is not None:
cancel.check()
cancel.register_process(proc)
try:
if communicate:
result: Any = proc.communicate(input=prompt, timeout=timeout)
else:
result = proc.wait(timeout=timeout)
except subprocess.TimeoutExpired as exc:
_kill_process(proc)
raise LLMTimeoutError(
f"claude CLI timed out after {timeout}s", cause=exc
) from exc
except ExecutionCancelled:
raise
except Exception:
if cancel is not None and cancel.cancelled:
raise ExecutionCancelled(cancel.reason or "cancelled") from None
raise
if cancel is not None:
cancel.check()
return result
@staticmethod
def _handle_stream_event(event: dict[str, Any], text_parts: list[str]) -> None:
if event.get("type") != "assistant":

View file

@ -17,6 +17,13 @@ from pathlib import Path
from typing import Any
from rein_aharness.ops_run_client import OpsRun, OpsRunConfig, resolve_ops_target
from rein_aharness.repository_transaction import (
DirtyRepositoryError,
GitRepositoryError,
RepositoryBusyError,
RepositoryTransaction,
RepositoryTransactionError,
)
from rein_aharness.taskspec import TaskSpecError
# Approach command names (stable; used in metrics + ops_run.result)
@ -188,7 +195,7 @@ def execute_approach(
reopen=False,
)
try:
def _dispatch(tx: RepositoryTransaction | None) -> ApproachResult:
if name == APPROACH_FI_RESEARCH_BRIEF:
return _run_fi(target, report_to_hub=report_to_hub, commit=commit)
if name == APPROACH_BRIEF_DAILY:
@ -204,7 +211,53 @@ def execute_approach(
target, report_to_hub=report_to_hub, commit=commit
)
if name == APPROACH_AGENT_SESSION:
return _run_agent_session(run, target, report_to_hub=report_to_hub)
return _run_agent_session(
run, target, report_to_hub=report_to_hub, transaction=tx
)
return ApproachResult(
ok=False,
approach=name,
reason=f"approach not implemented: {name}",
reopen=False,
)
if name == APPROACH_AGENT_SESSION:
try:
return _dispatch(None)
except Exception as exc: # noqa: BLE001 — surface as fail ops_run
return ApproachResult(
ok=False,
approach=name,
reason=f"{type(exc).__name__}: {exc}",
reopen=True,
)
try:
with RepositoryTransaction(target, correlation_id=run.id) as tx:
result = _dispatch(tx)
result.result["repository_transaction"] = tx.evidence()
return result
except DirtyRepositoryError as exc:
return ApproachResult(
ok=False,
approach=name,
reason=f"refused: {exc}",
reopen=False,
)
except RepositoryBusyError as exc:
return ApproachResult(
ok=False,
approach=name,
reason=f"refused: {exc}",
reopen=True,
)
except (GitRepositoryError, RepositoryTransactionError) as exc:
return ApproachResult(
ok=False,
approach=name,
reason=f"refused: {exc}",
reopen=False,
)
except Exception as exc: # noqa: BLE001 — surface as fail ops_run
return ApproachResult(
ok=False,
@ -213,14 +266,6 @@ def execute_approach(
reopen=True,
)
return ApproachResult(
ok=False,
approach=name,
reason=f"approach not implemented: {name}",
reopen=False,
)
def _run_fi(target: Path, *, report_to_hub: bool, commit: bool) -> ApproachResult:
from rein_aharness.fi_research_brief import run_fi_research_brief
@ -360,7 +405,11 @@ def _run_mail_pipeline(
def _run_agent_session(
run: OpsRun, target: Path, *, report_to_hub: bool
run: OpsRun,
target: Path,
*,
report_to_hub: bool,
transaction: RepositoryTransaction | None = None,
) -> ApproachResult:
from rein_aharness.ops_run_client import ops_run_to_taskspec
from rein_aharness.runner import run_task
@ -384,16 +433,19 @@ def _run_agent_session(
)
# target already resolved into TaskSpec
assert spec.target_repo == target or True
r = run_task(spec, report_to_hub=report_to_hub)
r = run_task(spec, report_to_hub=report_to_hub, transaction=transaction)
result = {
"committed": r.committed,
"head_after": r.head_after,
"tool_profile": r.tool_profile,
"tokens_spent": r.tokens_spent,
}
if r.transaction:
result["repository_transaction"] = r.transaction
return ApproachResult(
ok=r.ok,
approach=APPROACH_AGENT_SESSION,
result={
"committed": r.committed,
"head_after": r.head_after,
"tool_profile": r.tool_profile,
"tokens_spent": r.tokens_spent,
},
result=result,
reason=r.reason,
reopen=not r.ok,
)

View file

@ -23,16 +23,30 @@ from rein_aharness.approaches import (
execute_approach,
select_approach,
)
from rein_aharness.execution_cancel import (
ExecutionCancel,
ExecutionCancelled,
using_cancel,
)
from rein_aharness.glas_execution import (
GLAS_APPROACH,
GlasExecutionError,
execute_profiled_run,
)
from rein_aharness.lease_monitor import LeaseMonitor
from rein_aharness.lease_monitor import LeaseLoss, LeaseMonitor
from rein_aharness.repository_transaction import (
DirtyRepositoryError,
GitRepositoryError,
RepositoryBusyError,
RepositoryTransaction,
RepositoryTransactionError,
)
from rein_aharness.taskspec import TaskSpecError
from rein_aharness.ops_run_client import (
ActivityCoreOpsClient,
OpsRun,
OpsRunConfig,
resolve_ops_target,
OpsRunError,
)
@ -57,19 +71,41 @@ def _heartbeat_interval(lease_seconds: int) -> float:
return max(30.0, min(300.0, lease_seconds / 3.0))
_active_run_cancel: ExecutionCancel | None = None
_active_run_cancel_lock = threading.Lock()
def _set_active_run_cancel(cancel: ExecutionCancel | None) -> None:
global _active_run_cancel
with _active_run_cancel_lock:
_active_run_cancel = cancel
def _cancel_active_run(reason: str) -> None:
with _active_run_cancel_lock:
cancel = _active_run_cancel
if cancel is not None:
cancel.cancel(reason)
class _Heartbeat:
def __init__(
self,
client: ActivityCoreOpsClient,
run_id: str,
lease_seconds: int,
cancel: ExecutionCancel | None = None,
):
self._client = client
self._run_id = run_id
self._lease = lease_seconds
self._stop = threading.Event()
self._thread: threading.Thread | None = None
self.monitor = LeaseMonitor()
self.cancel = cancel if cancel is not None else ExecutionCancel()
self.monitor = LeaseMonitor(self._on_lost)
def _on_lost(self, _loss: LeaseLoss) -> None:
self.cancel.cancel("lease-loss")
def start(self) -> None:
interval = _heartbeat_interval(self._lease)
@ -109,6 +145,41 @@ class _Heartbeat:
self.monitor.stop()
def _cancelled_result(
run_id: str,
approach: str | None,
heartbeat: _Heartbeat,
) -> ProcessResult | None:
"""Skip Activity Core close after lease loss or execution cancellation."""
loss = heartbeat.monitor.evidence
if heartbeat.monitor.lost:
assert loss is not None
return ProcessResult(
claimed=True,
run_id=run_id,
approach=approach,
ok=False,
reason=f"lease lost ({loss.error_type})",
detail={
"lease_loss": {
"error_type": loss.error_type,
"observed_at": loss.observed_at,
}
},
)
if heartbeat.cancel.cancelled:
reason = heartbeat.cancel.reason or "cancelled"
return ProcessResult(
claimed=True,
run_id=run_id,
approach=approach,
ok=False,
reason=f"execution cancelled ({reason})",
detail={"cancellation": heartbeat.cancel.evidence()},
)
return None
def process_one(
client: ActivityCoreOpsClient | None = None,
*,
@ -175,39 +246,30 @@ def process_one(
report_to_hub=report_to_hub,
)
hb = _Heartbeat(client, run.id, cfg.lease_seconds)
cancel = ExecutionCancel()
hb = _Heartbeat(client, run.id, cfg.lease_seconds, cancel=cancel)
hb.start()
execution_error: str | None = None
_set_active_run_cancel(cancel)
try:
try:
ar: ApproachResult = execute_approach(
run,
approach=approach,
config=cfg,
report_to_hub=report_to_hub,
commit=commit,
)
with using_cancel(cancel):
ar: ApproachResult = execute_approach(
run,
approach=approach,
config=cfg,
report_to_hub=report_to_hub,
commit=commit,
)
except Exception as exc:
execution_error = type(exc).__name__
finally:
hb.stop()
_set_active_run_cancel(None)
if hb.monitor.lost:
loss = hb.monitor.evidence
assert loss is not None
return ProcessResult(
claimed=True,
run_id=run.id,
approach=approach,
ok=False,
reason=f"lease lost ({loss.error_type})",
detail={
"lease_loss": {
"error_type": loss.error_type,
"observed_at": loss.observed_at,
}
},
)
refused = _cancelled_result(run.id, approach, hb)
if refused is not None:
return refused
if execution_error is not None:
return ProcessResult(
@ -280,40 +342,50 @@ def _process_profiled_run(
report_to_hub: bool,
) -> ProcessResult:
"""Execute a profiled row without consulting or falling back to legacy routing."""
hb = _Heartbeat(client, run.id, client.config.lease_seconds)
cancel = ExecutionCancel()
hb = _Heartbeat(client, run.id, client.config.lease_seconds, cancel=cancel)
hb.start()
execution_error: str | None = None
execution_reason = ""
execution_reopen = False
gateway_result: dict[str, Any] | None = None
tx_evidence: dict[str, Any] | None = None
_set_active_run_cancel(cancel)
try:
try:
gateway_result = execute_profiled_run(
run,
client.config,
report_to_hub=report_to_hub,
)
target = resolve_ops_target(run, client.config)
with using_cancel(cancel):
with RepositoryTransaction(target, correlation_id=run.id) as tx:
gateway_result = execute_profiled_run(
run,
client.config,
report_to_hub=report_to_hub,
cancel=cancel,
)
tx_evidence = tx.evidence()
except ExecutionCancelled as exc:
execution_error = type(exc).__name__
execution_reason = str(exc)
except DirtyRepositoryError as exc:
execution_error = type(exc).__name__
execution_reason = f"refused: {exc}"
except RepositoryBusyError as exc:
execution_error = type(exc).__name__
execution_reason = f"refused: {exc}"
execution_reopen = True
except (GitRepositoryError, RepositoryTransactionError, TaskSpecError) as exc:
execution_error = type(exc).__name__
execution_reason = f"refused: {exc}"
except GlasExecutionError as exc:
execution_error = type(exc).__name__
execution_reason = str(exc)
finally:
hb.stop()
_set_active_run_cancel(None)
if hb.monitor.lost:
loss = hb.monitor.evidence
assert loss is not None
return ProcessResult(
claimed=True,
run_id=run.id,
approach=GLAS_APPROACH,
ok=False,
reason=f"lease lost ({loss.error_type})",
detail={
"lease_loss": {
"error_type": loss.error_type,
"observed_at": loss.observed_at,
}
},
)
refused = _cancelled_result(run.id, GLAS_APPROACH, hb)
if refused is not None:
return refused
if execution_error is not None:
reason = execution_reason or f"profiled execution failed ({execution_error})"
@ -321,7 +393,7 @@ def _process_profiled_run(
out = client.fail(
run.id,
error=reason,
reopen=False,
reopen=execution_reopen,
result={"ok": False, "approach": GLAS_APPROACH, "reason": reason},
)
except OpsRunError as close_exc:
@ -345,6 +417,10 @@ def _process_profiled_run(
evidence = gateway_result["evidence"]
ok = gateway_result["ok"]
reason = str(evidence.get("error") or evidence.get("outcome") or "Glas execution failed")
detail = {"execution_evidence": evidence}
if tx_evidence is not None:
detail["repository_transaction"] = tx_evidence
gateway_result = {**gateway_result, "repository_transaction": tx_evidence}
try:
if ok:
out = client.complete(run.id, result=gateway_result)
@ -362,7 +438,7 @@ def _process_profiled_run(
approach=GLAS_APPROACH,
ok=False,
reason=f"close ops_run failed: {exc}; gateway_ok={ok} {reason}",
detail={"execution_evidence": evidence},
detail=detail,
)
return ProcessResult(
@ -372,7 +448,7 @@ def _process_profiled_run(
ok=ok,
reason="" if ok else reason,
ops_state=out.state,
detail={"execution_evidence": evidence},
detail=detail,
)
@ -423,6 +499,7 @@ def run_claim_loop(
def _handle_sig(*_args: Any) -> None:
logger.info("shutdown signal received")
stop.set()
_cancel_active_run("signal")
signal.signal(signal.SIGINT, _handle_sig)
signal.signal(signal.SIGTERM, _handle_sig)

View file

@ -0,0 +1,141 @@
"""One-shot cancellation for an in-flight adapter call.
Lease loss, run timeout, and process signals all need the same adapter-side
effect: stop the current subprocess or HTTP client without retaining provider
output. The claim loop owns the reason; adapters only observe and stop.
"""
from __future__ import annotations
import contextvars
import threading
from collections.abc import Callable, Iterator
from contextlib import contextmanager
from typing import Any
_ALLOWED_REASONS = frozenset({"lease-loss", "timeout", "signal"})
_REASON_LIMIT = 32
_active: contextvars.ContextVar["ExecutionCancel | None"] = contextvars.ContextVar(
"rein_aharness_execution_cancel",
default=None,
)
class ExecutionCancelled(RuntimeError):
"""Bounded proof that an adapter stopped because the run was cancelled."""
def __init__(self, reason: str) -> None:
self.reason = _normalize_reason(reason)
super().__init__(f"execution cancelled ({self.reason})")
class ExecutionCancel:
"""Thread-safe one-shot cancel with optional process/HTTP stop callbacks."""
def __init__(self) -> None:
self._lock = threading.Lock()
self._event = threading.Event()
self._reason: str | None = None
self._stops: list[Callable[[], None]] = []
@property
def cancelled(self) -> bool:
return self._event.is_set()
@property
def reason(self) -> str | None:
with self._lock:
return self._reason
def wait(self, timeout: float | None = None) -> bool:
return self._event.wait(timeout)
def check(self) -> None:
if self.cancelled:
raise ExecutionCancelled(self.reason or "cancelled")
def cancel(self, reason: str) -> str:
"""Record the first reason and invoke registered stop callbacks once."""
callbacks: list[Callable[[], None]] = []
with self._lock:
if self._reason is None:
self._reason = _normalize_reason(reason)
self._event.set()
callbacks = list(self._stops)
recorded = self._reason
for callback in callbacks:
_invoke_stop(callback)
return recorded
def register_stop(self, stop: Callable[[], None]) -> None:
"""Register a best-effort stopper; invoke immediately if already cancelled."""
invoke_now = False
with self._lock:
self._stops.append(stop)
invoke_now = self._reason is not None
if invoke_now:
_invoke_stop(stop)
def register_process(self, proc: Any) -> None:
"""Kill an in-flight subprocess. Safe against mock and exited processes."""
self.register_stop(lambda: _kill_process(proc))
def evidence(self) -> dict[str, str]:
if not self.cancelled:
return {}
return {"cancelled": "true", "reason": self.reason or "cancelled"}
def active_cancel() -> ExecutionCancel | None:
return _active.get()
def resolve_cancel(explicit: ExecutionCancel | None = None) -> ExecutionCancel | None:
return explicit if explicit is not None else active_cancel()
@contextmanager
def using_cancel(cancel: ExecutionCancel) -> Iterator[ExecutionCancel]:
token = _active.set(cancel)
try:
yield cancel
finally:
_active.reset(token)
def _normalize_reason(reason: str) -> str:
text = (reason or "").strip()[:_REASON_LIMIT]
if text in _ALLOWED_REASONS:
return text
return "cancelled"
def _invoke_stop(stop: Callable[[], None]) -> None:
try:
stop()
except Exception:
return
def _kill_process(proc: Any) -> None:
poll = getattr(proc, "poll", None)
if callable(poll):
try:
status = poll()
except Exception:
status = None
if isinstance(status, int):
return
kill = getattr(proc, "kill", None)
if callable(kill):
try:
kill()
except Exception:
return
wait = getattr(proc, "wait", None)
if callable(wait):
try:
wait(timeout=2)
except Exception:
return

View file

@ -11,6 +11,7 @@ from __future__ import annotations
from collections.abc import Callable
from typing import Any
from rein_aharness.execution_cancel import ExecutionCancel, ExecutionCancelled, resolve_cancel
from rein_aharness.ops_run_client import OpsRun, OpsRunConfig, resolve_ops_target
GLAS_APPROACH = "glas-profile"
@ -65,8 +66,12 @@ def execute_profiled_run(
report_to_hub: bool = True,
request_factory: Callable[..., Any] | None = None,
gateway: Callable[[Any], Any] | None = None,
cancel: ExecutionCancel | None = None,
) -> dict[str, Any]:
"""Invoke Glas and return its complete JSON-compatible GatewayResult."""
guard = resolve_cancel(cancel)
if guard is not None:
guard.check()
if request_factory is None or gateway is None:
try:
from glas_harness.contract import ExecutionRequest
@ -83,10 +88,16 @@ def execute_profiled_run(
request = request_factory(**_request_kwargs(run, config, report_to_hub))
result = gateway(request)
raw = result.model_dump(mode="json") if hasattr(result, "model_dump") else result
except ExecutionCancelled:
raise
except GlasExecutionError:
raise
except Exception as exc:
if guard is not None and guard.cancelled:
raise ExecutionCancelled(guard.reason or "cancelled") from None
raise GlasExecutionError(f"Glas gateway invocation failed: {exc}") from exc
if guard is not None:
guard.check()
if not isinstance(raw, dict) or not isinstance(raw.get("ok"), bool):
raise GlasExecutionError("Glas gateway returned an invalid GatewayResult")

View file

@ -11,6 +11,12 @@ from typing import Any
import httpx
from rein_aharness.execution_cancel import (
ExecutionCancel,
ExecutionCancelled,
resolve_cancel,
)
_SAFE_RESPONSE_METADATA_KEYS = frozenset(
{
"finish_reason",
@ -50,20 +56,27 @@ class LLMConnectClient:
*,
model: str = "",
config: dict[str, Any] | None = None,
cancel: ExecutionCancel | None = None,
) -> str:
run_config = dict(config or {})
if model and "model_name" not in run_config:
run_config["model_name"] = model
run_config.setdefault("timeout_seconds", int(self.timeout_seconds))
payload: dict[str, Any] = {"prompt": prompt, "config": run_config}
url = f"{self.base_url}/execute"
guard = resolve_cancel(cancel)
if guard is not None:
guard.check()
try:
resp = httpx.post(
f"{self.base_url}/execute",
json=payload,
timeout=self.timeout_seconds,
)
resp = self._post(url, payload, guard)
except ExecutionCancelled:
raise
except httpx.HTTPError as exc:
if guard is not None and guard.cancelled:
raise ExecutionCancelled(guard.reason or "cancelled") from None
raise LLMConnectError(f"llm-connect request failed: {exc}") from exc
if guard is not None:
guard.check()
if resp.status_code >= 400:
raise LLMConnectError(_llm_connect_error_text(resp))
try:
@ -76,6 +89,18 @@ class LLMConnectClient:
raise LLMConnectError("llm-connect response missing string content")
return content
def _post(
self,
url: str,
payload: dict[str, Any],
guard: ExecutionCancel | None,
) -> httpx.Response:
if guard is None:
return httpx.post(url, json=payload, timeout=self.timeout_seconds)
with httpx.Client(timeout=self.timeout_seconds) as client:
guard.register_stop(client.close)
return client.post(url, json=payload)
def _llm_connect_error_text(resp: httpx.Response) -> str:
"""Keep llm-connect's safe cause without copying an upstream response blob."""

View file

@ -1,8 +1,9 @@
"""Process-safe transaction boundary for a local Git checkout.
This module is intentionally not wired into the claim loop yet. It provides
the repository-local half of HARNESS-WP-0003-T02 while the Activity Core lease
boundary is still under owner review.
Wired into `run_task`, profile-absent `execute_approach` mutators, and the
profiled claim path under REINAH-WP-0003-T02 / ADR-002. Repository
acceptance (T03) remains a separate read-only validator and is not applied
to live results yet.
"""
from __future__ import annotations

View file

@ -17,9 +17,17 @@ from pathlib import Path
from typing import Any, Callable
from rein_aharness import hub, metrics
from rein_aharness.execution_cancel import ExecutionCancelled
from rein_aharness.manifest import resolve_run_policy
from rein_aharness.persona import load_persona_bundle
from rein_aharness.profiles import UnknownToolProfileError, get_profile
from rein_aharness.repository_transaction import (
DirtyRepositoryError,
GitRepositoryError,
RepositoryBusyError,
RepositoryTransaction,
RepositoryTransactionError,
)
from rein_aharness.taskspec import TaskSpec
PROMPT_TEMPLATE = """\
@ -59,6 +67,7 @@ class RunResult:
# called with emit_tool_events=True. See adapter.py's module docstring
# for why this is observation, not external tool dispatch.
tool_events: list[dict[str, Any]] = field(default_factory=list)
transaction: dict[str, Any] | None = None
def _git(repo: Path, *args: str) -> str:
@ -73,6 +82,28 @@ def _git(repo: Path, *args: str) -> str:
return result.stdout.strip()
def _refused_run(
*,
reason: str,
model: str | None = None,
head_before: str = "",
transaction: dict[str, Any] | None = None,
) -> RunResult:
return RunResult(
ok=False,
committed=False,
head_before=head_before,
head_after=head_before,
persona_source="none",
session_output="",
reason=reason,
tool_profile="",
budget_tokens=None,
model=model,
transaction=transaction,
)
def run_task(
spec: TaskSpec,
adapter=None,
@ -83,21 +114,14 @@ def run_task(
model: str | None = None,
tool_profile_override: str | None = None,
budget_tokens_override: int | None = None,
transaction: RepositoryTransaction | None = None,
) -> RunResult:
if spec.repository_grant is not None:
return RunResult(
ok=False,
committed=False,
head_before="",
head_after="",
persona_source="none",
session_output="",
return _refused_run(
reason=(
"refused: repository_grant enforcement is not enabled; "
"no adapter was dispatched"
),
tool_profile="",
budget_tokens=None,
model=model,
)
try:
@ -110,31 +134,9 @@ def run_task(
budget_tokens = budget_tokens_override
profile = get_profile(profile_name)
except UnknownToolProfileError as exc:
return RunResult(
ok=False,
committed=False,
head_before="",
head_after="",
persona_source="none",
session_output="",
reason=f"refused: {exc}",
tool_profile="",
budget_tokens=None,
model=model,
)
return _refused_run(reason=f"refused: {exc}", model=model)
except Exception as exc:
return RunResult(
ok=False,
committed=False,
head_before="",
head_after="",
persona_source="none",
session_output="",
reason=f"manifest resolution failed: {exc}",
tool_profile="",
budget_tokens=None,
model=model,
)
return _refused_run(reason=f"manifest resolution failed: {exc}", model=model)
collected_events: list[dict[str, Any]] = []
@ -162,7 +164,58 @@ def run_task(
adapter_kwargs["model"] = model
adapter = AgenticClaudeCodeAdapter(**adapter_kwargs)
head_before = _git(spec.target_repo, "rev-parse", "HEAD")
def _run_locked(tx: RepositoryTransaction) -> RunResult:
return _execute_locked_task(
spec,
tx,
adapter=adapter,
profile=profile,
blueprint=blueprint,
lane=lane,
budget_tokens=budget_tokens,
collected_events=collected_events,
report_to_hub=report_to_hub,
write_metrics=write_metrics,
model=model,
)
if transaction is not None:
return _run_locked(transaction)
try:
with RepositoryTransaction(
spec.target_repo,
correlation_id=spec.hub_task_id or spec.title,
) as tx:
return _run_locked(tx)
except DirtyRepositoryError as exc:
return _refused_run(
reason=f"refused: {exc}",
model=model,
head_before=exc.baseline.head,
transaction={"baseline": exc.baseline.evidence()},
)
except RepositoryBusyError as exc:
return _refused_run(reason=f"refused: {exc}", model=model)
except (GitRepositoryError, RepositoryTransactionError) as exc:
return _refused_run(reason=f"refused: {exc}", model=model)
def _execute_locked_task(
spec: TaskSpec,
tx: RepositoryTransaction,
*,
adapter: Any,
profile: Any,
blueprint: str,
lane: str | None,
budget_tokens: int | None,
collected_events: list[dict[str, Any]],
report_to_hub: bool,
write_metrics: bool,
model: str | None,
) -> RunResult:
assert tx.baseline is not None
head_before = tx.baseline.head
persona, persona_source = load_persona_bundle(blueprint, spec.target_repo)
prompt = PROMPT_TEMPLATE.format(
persona=persona or "(no persona bundle available for this run)",
@ -188,6 +241,11 @@ def run_task(
resolved_model = response.model
session_ok = True
reason = ""
except ExecutionCancelled as exc:
session_output = ""
session_ok = False
reason = f"execution cancelled ({exc.reason})"
resolved_model = model
except Exception as exc: # adapter / budget failures must still be reported
session_output = ""
session_ok = False
@ -217,6 +275,7 @@ def run_task(
tokens_spent=tokens_spent,
execution_time_s=execution_time_s,
tool_events=collected_events,
transaction=tx.evidence(),
)
if write_metrics:

View file

@ -111,11 +111,58 @@ def test_execute_streaming_raises_timeout(tmp_path) -> None:
def test_execute_blocking_path_unchanged_without_callback(tmp_path) -> None:
adapter = AgenticClaudeCodeAdapter(workdir=tmp_path)
config = RunConfig(timeout_seconds=30)
fake_result = MagicMock(returncode=0, stdout="plain output", stderr="")
proc = MagicMock()
proc.communicate.return_value = ("plain output", "")
proc.returncode = 0
proc.poll.return_value = 0
with patch("rein_aharness.adapter.subprocess.run", return_value=fake_result) as run:
with patch("rein_aharness.adapter.subprocess.Popen", return_value=proc) as popen:
response = adapter.execute_prompt("do it", config)
assert response.content == "plain output"
argv = run.call_args.args[0]
argv = popen.call_args.args[0]
assert "--output-format" not in argv
proc.communicate.assert_called_once()
def test_blocking_cancel_kills_process_and_raises(tmp_path) -> None:
from rein_aharness.execution_cancel import ExecutionCancel, ExecutionCancelled
cancel = ExecutionCancel()
adapter = AgenticClaudeCodeAdapter(workdir=tmp_path, cancel=cancel)
config = RunConfig(timeout_seconds=30)
proc = MagicMock()
proc.poll.return_value = None
def _communicate(input=None, timeout=None): # noqa: A002
cancel.cancel("lease-loss")
raise RuntimeError("subprocess interrupted")
proc.communicate.side_effect = _communicate
with patch("rein_aharness.adapter.subprocess.Popen", return_value=proc):
with pytest.raises(ExecutionCancelled, match="lease-loss"):
adapter.execute_prompt("do it", config)
proc.kill.assert_called_once()
def test_streaming_cancel_refuses_nonzero_exit(tmp_path) -> None:
from rein_aharness.execution_cancel import ExecutionCancel, ExecutionCancelled
cancel = ExecutionCancel()
adapter = AgenticClaudeCodeAdapter(
workdir=tmp_path, on_tool_event=lambda e: None, cancel=cancel
)
config = RunConfig(timeout_seconds=30)
proc = _fake_proc([])
proc.poll.return_value = None
def _wait(timeout=None):
cancel.cancel("signal")
return 1
proc.wait.side_effect = _wait
with patch("rein_aharness.adapter.subprocess.Popen", return_value=proc):
with pytest.raises(ExecutionCancelled, match="signal"):
adapter.execute_prompt("do it", config)

View file

@ -6,7 +6,13 @@ from unittest.mock import MagicMock, patch
import time
from rein_aharness.approaches import ApproachResult, APPROACH_FI_RESEARCH_BRIEF
from rein_aharness.claim_loop import process_one, poll_peek
from rein_aharness.claim_loop import (
_cancel_active_run,
_set_active_run_cancel,
process_one,
poll_peek,
)
from rein_aharness.execution_cancel import ExecutionCancel, active_cancel
from rein_aharness.glas_execution import GLAS_APPROACH, GlasExecutionError
from rein_aharness.ops_run_client import (
ActivityCoreOpsClient,
@ -135,6 +141,78 @@ def test_process_one_refuses_close_after_lease_loss() -> None:
client.fail.assert_not_called()
def test_process_one_lease_loss_cancels_registered_adapter_process() -> None:
client = MagicMock(spec=ActivityCoreOpsClient)
client.config = OpsRunConfig(worker_id="w", lease_seconds=90)
client.claim.return_value = [_claimed_run()]
client.heartbeat.side_effect = OpsRunError("lease rejected", status_code=409)
proc = MagicMock()
proc.poll.return_value = None
def slow_execute(*_args, **_kwargs):
cancel = active_cancel()
assert cancel is not None
cancel.register_process(proc)
deadline = time.monotonic() + 1.0
while not cancel.cancelled and time.monotonic() < deadline:
time.sleep(0.005)
cancel.check()
with (
patch("rein_aharness.claim_loop._heartbeat_interval", return_value=0.01),
patch("rein_aharness.claim_loop.execute_approach", side_effect=slow_execute),
):
result = process_one(client)
assert result.ok is False
assert result.reason.startswith("lease lost")
proc.kill.assert_called()
client.complete.assert_not_called()
client.fail.assert_not_called()
def test_process_one_skips_close_after_signal_cancel() -> None:
client = MagicMock(spec=ActivityCoreOpsClient)
client.config = OpsRunConfig(worker_id="w", lease_seconds=90)
client.claim.return_value = [_claimed_run()]
ar = ApproachResult(
ok=True,
approach=APPROACH_FI_RESEARCH_BRIEF,
result={"path": "briefs/x.md"},
reason="ok",
)
def execute_then_signal(*_args, **_kwargs):
cancel = active_cancel()
assert cancel is not None
cancel.cancel("signal")
return ar
with patch("rein_aharness.claim_loop.execute_approach", side_effect=execute_then_signal):
result = process_one(client)
assert result.ok is False
assert result.reason == "execution cancelled (signal)"
assert result.detail == {"cancellation": {"cancelled": "true", "reason": "signal"}}
client.complete.assert_not_called()
client.fail.assert_not_called()
def test_shutdown_signal_cancels_active_run() -> None:
cancel = ExecutionCancel()
proc = MagicMock()
proc.poll.return_value = None
cancel.register_process(proc)
_set_active_run_cancel(cancel)
try:
_cancel_active_run("signal")
finally:
_set_active_run_cancel(None)
assert cancel.reason == "signal"
proc.kill.assert_called_once()
def test_process_one_records_adapter_exception_without_unbound_result() -> None:
client = MagicMock(spec=ActivityCoreOpsClient)
client.config = OpsRunConfig(worker_id="w", lease_seconds=90)
@ -220,8 +298,16 @@ def test_profiled_run_uses_glas_and_completes_with_full_result() -> None:
assert result.ok is True
assert result.approach == GLAS_APPROACH
assert result.detail == {"execution_evidence": gateway_result["evidence"]}
client.complete.assert_called_once_with(run.id, result=gateway_result)
assert result.detail["execution_evidence"] == gateway_result["evidence"]
transaction = result.detail["repository_transaction"]
assert transaction["correlation_id"] == run.id
assert transaction["baseline"]["clean"] is True
client.complete.assert_called_once()
complete_run_id = client.complete.call_args.args[0]
complete_result = client.complete.call_args.kwargs["result"]
assert complete_run_id == run.id
assert complete_result["evidence"] == gateway_result["evidence"]
assert complete_result["repository_transaction"] == transaction
client.fail.assert_not_called()
select.assert_not_called()
execute.assert_not_called()

View file

@ -0,0 +1,113 @@
from __future__ import annotations
import threading
import time
from unittest.mock import MagicMock
import pytest
from rein_aharness.execution_cancel import (
ExecutionCancel,
ExecutionCancelled,
active_cancel,
using_cancel,
)
def test_cancel_is_one_shot_and_bounded() -> None:
cancel = ExecutionCancel()
first = cancel.cancel("lease-loss")
second = cancel.cancel("timeout")
assert first == second == "lease-loss"
assert cancel.cancelled is True
assert cancel.reason == "lease-loss"
assert cancel.evidence() == {"cancelled": "true", "reason": "lease-loss"}
def test_unknown_reason_is_normalized() -> None:
cancel = ExecutionCancel()
cancel.cancel("provider exploded with secrets")
assert cancel.reason == "cancelled"
def test_check_raises_only_after_cancel() -> None:
cancel = ExecutionCancel()
cancel.check()
cancel.cancel("timeout")
with pytest.raises(ExecutionCancelled, match="timeout") as excinfo:
cancel.check()
assert excinfo.value.reason == "timeout"
def test_register_process_is_killed_on_cancel() -> None:
proc = MagicMock()
proc.poll.return_value = None
cancel = ExecutionCancel()
cancel.register_process(proc)
cancel.cancel("lease-loss")
proc.kill.assert_called_once()
def test_already_cancelled_register_kills_immediately() -> None:
proc = MagicMock()
proc.poll.return_value = None
cancel = ExecutionCancel()
cancel.cancel("signal")
cancel.register_process(proc)
proc.kill.assert_called_once()
def test_exited_process_is_not_killed() -> None:
proc = MagicMock()
proc.poll.return_value = 0
cancel = ExecutionCancel()
cancel.register_process(proc)
cancel.cancel("timeout")
proc.kill.assert_not_called()
def test_stop_callback_failure_does_not_block_cancel() -> None:
cancel = ExecutionCancel()
cancel.register_stop(lambda: (_ for _ in ()).throw(RuntimeError("stop failed")))
assert cancel.cancel("signal") == "signal"
def test_concurrent_cancel_invokes_stop_once() -> None:
seen = []
cancel = ExecutionCancel()
cancel.register_stop(lambda: seen.append("stop"))
threads = [
threading.Thread(target=cancel.cancel, args=("lease-loss",)) for _ in range(8)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
assert seen == ["stop"]
def test_using_cancel_exposes_active_cancel() -> None:
cancel = ExecutionCancel()
assert active_cancel() is None
with using_cancel(cancel):
assert active_cancel() is cancel
cancel.cancel("timeout")
assert active_cancel() is cancel
assert active_cancel() is None
def test_wait_returns_after_cancel() -> None:
cancel = ExecutionCancel()
started = threading.Event()
def _cancel_soon() -> None:
started.wait(timeout=1)
time.sleep(0.01)
cancel.cancel("signal")
thread = threading.Thread(target=_cancel_soon)
thread.start()
started.set()
assert cancel.wait(timeout=1) is True
thread.join()

View file

@ -127,3 +127,48 @@ def test_profiled_actor_validates_against_real_glas_and_sandboxer(tmp_path: Path
assert captured["request"].actor == "agt"
assert captured["sandbox_request"].consumer.actor == sandbox_models.ActorType.AGT
assert captured["sandbox_request"].consumer.run_id == "run-1"
def test_profiled_run_does_not_invoke_gateway_when_already_cancelled(
tmp_path: Path,
) -> None:
from rein_aharness.execution_cancel import ExecutionCancel, ExecutionCancelled
repo = _repo(tmp_path)
cancel = ExecutionCancel()
cancel.cancel("lease-loss")
called = []
with pytest.raises(ExecutionCancelled, match="lease-loss"):
execute_profiled_run(
_run(repo),
OpsRunConfig(repo_roots=(str(tmp_path),)),
request_factory=lambda **kwargs: kwargs,
gateway=lambda request: called.append(request) or {
"ok": True,
"evidence": {"outcome": "succeeded"},
},
cancel=cancel,
)
assert called == []
def test_profiled_run_refuses_success_if_cancelled_during_gateway(
tmp_path: Path,
) -> None:
from rein_aharness.execution_cancel import ExecutionCancel, ExecutionCancelled
repo = _repo(tmp_path)
cancel = ExecutionCancel()
def gateway(request):
cancel.cancel("timeout")
return {"ok": True, "evidence": {"outcome": "succeeded"}}
with pytest.raises(ExecutionCancelled, match="timeout"):
execute_profiled_run(
_run(repo),
OpsRunConfig(repo_roots=(str(tmp_path),)),
request_factory=lambda **kwargs: kwargs,
gateway=gateway,
cancel=cancel,
)

View file

@ -257,3 +257,31 @@ def test_get_client_requires_env(monkeypatch) -> None:
from rein_aharness.llm_connect_client import get_llm_connect_client
get_llm_connect_client()
def test_llm_connect_cancel_closes_client_and_raises(monkeypatch) -> None:
import rein_aharness.llm_connect_client as mod
from rein_aharness.execution_cancel import ExecutionCancel, ExecutionCancelled
closed = []
cancel = ExecutionCancel()
class FakeClient:
def __enter__(self):
return self
def __exit__(self, *args):
return False
def close(self):
closed.append("closed")
def post(self, url, json=None): # noqa: A002
cancel.cancel("timeout")
raise mod.httpx.ConnectError("connection reset")
monkeypatch.setattr(mod.httpx, "Client", lambda timeout=None: FakeClient())
client = LLMConnectClient("http://llm.test", timeout_seconds=5)
with pytest.raises(ExecutionCancelled, match="timeout"):
client.complete("hi", cancel=cancel)
assert closed == ["closed"]

View file

@ -126,6 +126,28 @@ def test_run_task_fails_without_commit(tmp_path) -> None:
assert result.reason == "session completed without committing"
def test_run_task_records_bounded_cancellation_without_session_output(tmp_path) -> None:
from rein_aharness.execution_cancel import ExecutionCancelled
repo = _make_repo(tmp_path)
class CancellingAdapter:
def execute_prompt(self, prompt, config):
raise ExecutionCancelled("lease-loss")
result = run_task(
_spec(repo),
adapter=CancellingAdapter(),
report_to_hub=False,
write_metrics=False,
)
assert result.ok is False
assert result.committed is False
assert result.session_output == ""
assert result.reason == "execution cancelled (lease-loss)"
def test_run_task_refuses_unknown_tool_profile(tmp_path) -> None:
repo = _make_repo(tmp_path)
_write_manifest(
@ -160,6 +182,21 @@ def test_run_task_resolves_manifest_profile_and_budget(tmp_path) -> None:
}
},
)
subprocess.run(["git", "add", ".kaizen"], cwd=repo, check=True)
subprocess.run(
[
"git",
"-c",
"user.email=t@t",
"-c",
"user.name=t",
"commit",
"-qm",
"configure rein",
],
cwd=repo,
check=True,
)
adapter = CommittingAdapter(repo)
result = run_task(
@ -186,6 +223,21 @@ def test_run_task_budget_exhaustion_fails(tmp_path) -> None:
}
},
)
subprocess.run(["git", "add", ".kaizen"], cwd=repo, check=True)
subprocess.run(
[
"git",
"-c",
"user.email=t@t",
"-c",
"user.name=t",
"commit",
"-qm",
"configure rein",
],
cwd=repo,
check=True,
)
result = run_task(
_spec(repo),

View file

@ -9,7 +9,7 @@ owner: codex
topic_slug: rein-aharness
priority: high
created: "2026-08-23"
updated: "2026-08-23"
updated: "2026-09-04"
related:
- REIN-A-0004
- GLAS-IN-0002
@ -65,7 +65,7 @@ waiting on the sandbox owner.
```task
id: REINAH-WP-0003-T01
status: progress
status: done
priority: high
state_hub_task_id: "596fc138-1ee3-5602-95c7-0bd728ec70ab"
```
@ -140,6 +140,19 @@ and T02T06 can cite stable decisions rather than infer ownership from code.
sand-boxer (`1bf5768c-f77a-4f08-ae02-25caff9f0166`), llm-connect
(`4bd35ce5-9c84-4695-83d5-c9bd16fa1b77`), and Glas
(`52567711-1100-43c0-9f8e-40650738eb04`).
- 2026-08-29: this State Hub instance no longer serves those request
message ids (GET 404). Glas approval of the earlier revision still
stands in the ADR text; Activity Core, sand-boxer, and llm-connect
exact-revision acknowledgements of `36e1096` /
`84b47d2e…bb206f` remain the T01 gate. Reminder messages resent from
`rein-aharness`: Activity Core `92b80a06-dc0e-46bd-a5c7-b43d1babe9ba`,
Glas `afc2af7b-8246-482b-a6f5-7acfc2aa32b1`, sand-boxer
`b220425d-5295-4bf3-af4c-34ec838f1175`, llm-connect
`4cc4a2cc-584b-423d-8f34-a0bcd981f1c7`.
- 2026-08-29: operator Bernd Worsch accepted ADR-002 as written.
`INTENT.md` and ADR-001 slogan wording were amended the same day.
Sibling exact-revision acknowledgements remain welcome as recording,
not a gate. T01 is `done`; T04 and T05 are unblocked.
## Make each repository run a lease-bound transaction
@ -211,6 +224,40 @@ class marker rather than leaving an unbound result or retaining provider text.
explicit ownership-rejection statuses as lease loss while allowing transport
errors to retry without immediately abandoning a run.
### Adapter cancellation slice — 2026-08-29
Connected lease-loss, timeout, and signal cancellation at the adapter
boundaries and enabled the repository transaction after ADR-002 acceptance.
- `rein_aharness/execution_cancel.py` is a one-shot, adapter-neutral cancel
token. It records a bounded reason (`lease-loss` / `timeout` / `signal`),
invokes registered process/HTTP stoppers once, and is published to adapters
through an explicit argument or `using_cancel()` context.
- The claim-loop heartbeat now cancels that token when Activity Core rejects
ownership. SIGINT/SIGTERM also cancel the active run. In both cases the loop
still skips Activity Core complete/fail and returns bounded cancellation or
lease-loss evidence.
- `AgenticClaudeCodeAdapter` waits on `Popen` for both blocking and streaming
sessions so an in-flight Claude process can be killed. `LLMConnectClient`
closes its HTTP client. Glas checks the token before and after
`run_execution` and refuses a late success; it still cannot abort an
in-flight sandbox (sand-boxer residual).
- `run_task` records `execution cancelled (<reason>)` without session output.
Tenant brief executors pick the token up through the active-cancel context
when they call llm-connect.
The repository lock is now wired into `runner.py`, legacy mutating approaches,
and the profiled claim path following operator acceptance of ADR-002. Remaining
T02 work is to prove lock release on timeout, signal, and result-close failure
and close the task with the resulting bounded evidence.
### Review checkpoint — 2026-09-04
Reviewed the cancellation and transaction wiring together and corrected the
affected profiled-result and clean-baseline test setup. Full verification passes:
`209 passed, 1 skipped`. T02 remains `progress` until the named timeout, signal,
and result-close lock-release cases are covered directly.
## Verify accepted commits and reconcile metrics/reporting
```task
@ -328,7 +375,7 @@ was requested.
```task
id: REINAH-WP-0003-T04
status: wait
status: todo
priority: high
state_hub_task_id: "f6c7f80c-3807-54ac-b44a-ef582433b927"
```
@ -364,7 +411,7 @@ This is read-only preparation; T04 remains `wait` until T01 accepts ADR-002.
```task
id: REINAH-WP-0003-T05
status: wait
status: todo
priority: high
state_hub_task_id: "c4a3f08f-2874-5672-9f29-aecddd697d90"
```