feat(runtime): enforce governed mutation boundaries

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a06ba0-10aa-7ea0-b20a-4f3fac39efe9
This commit is contained in:
tegwick 2026-09-04 11:25:07 +02:00
parent e3c6124e22
commit 20e6f381f6
28 changed files with 1068 additions and 154 deletions

View file

@ -51,6 +51,7 @@ rein-aharness run --task-file examples/task-hello-sandbox.json \
# Primary production intake: activity-core ops_run claim (ACT-ADR-005 / REIN-A-0002) # Primary production intake: activity-core ops_run claim (ACT-ADR-005 / REIN-A-0002)
export ACTIVITY_CORE_URL=http://127.0.0.1:8010 export ACTIVITY_CORE_URL=http://127.0.0.1:8010
export ACTIVITY_CORE_WORKER_TOKEN=… export ACTIVITY_CORE_WORKER_TOKEN=…
export AGENT_HARNESS_LEGACY_APPROACHES_UNTIL=2026-12-31 # temporary profile-absent routes
export AGENT_HARNESS_REPO_MAP='{"freedom-intelligence":"~/freedom-intelligence","binky-control":"~/binky-control"}' export AGENT_HARNESS_REPO_MAP='{"freedom-intelligence":"~/freedom-intelligence","binky-control":"~/binky-control"}'
rein-aharness poll --source=ops-run --no-claim rein-aharness poll --source=ops-run --no-claim
rein-aharness run --from-ops-run rein-aharness run --from-ops-run
@ -88,8 +89,11 @@ Railiance deploy: [deploy/README.md](deploy/README.md).
Each `run` resolves the agent's tool profile + budget from the target Each `run` resolves the agent's tool profile + budget from the target
repo's instance manifest (default `green-commit-only`), loads a persona repo's instance manifest (default `green-commit-only`), loads a persona
bundle (`kaizen-agentic schedule prepare`), runs a bounded agentic bundle (`kaizen-agentic schedule prepare`), runs a bounded agentic
session (cwd-pinned, hard allow-list, never pushes), verifies a local session (cwd-pinned, hard allow-list, never pushes), verifies a local commit,
commit, writes `.kaizen/metrics`, and posts a State Hub progress event. records metrics, and posts a State Hub progress event. Local task files with a
v1 `repository_grant` receive full repository acceptance and mandatory durable
external metrics; grant-absent compatibility runs retain repository-local
`.kaizen/metrics` during migration.
When invoked through `glas-harness`, the explicit versioned Glas profile may When invoked through `glas-harness`, the explicit versioned Glas profile may
override model, tool profile, and budget for that bounded run. Workforce and override model, tool profile, and budget for that bounded run. Workforce and
activity consumers should reference the Glas profile rather than encode these activity consumers should reference the Glas profile rather than encode these
@ -98,6 +102,7 @@ rein CLI details in an assignment.
Instance manifest contract: [docs/instance-manifest.md](docs/instance-manifest.md). Instance manifest contract: [docs/instance-manifest.md](docs/instance-manifest.md).
Example: [examples/schedule.harness.yml](examples/schedule.harness.yml). Example: [examples/schedule.harness.yml](examples/schedule.harness.yml).
Repository mutation authority: [docs/repository-grant.md](docs/repository-grant.md). Repository mutation authority: [docs/repository-grant.md](docs/repository-grant.md).
Granted-run metrics: [docs/external-metrics.md](docs/external-metrics.md).
Durable result-close design: [docs/close-evidence-outbox.md](docs/close-evidence-outbox.md). Durable result-close design: [docs/close-evidence-outbox.md](docs/close-evidence-outbox.md).
Tests: `PYTHONPATH=".:$HOME/llm-connect" python3 -m pytest tests/ -q` Tests: `PYTHONPATH=".:$HOME/llm-connect" python3 -m pytest tests/ -q`

View file

@ -1,6 +1,6 @@
# SCOPE # SCOPE
> Capability snapshot: 2026-08-23. `INTENT.md` states the destination and > Capability snapshot: 2026-09-04. `INTENT.md` states the destination and
> boundaries; this file records what the repository currently implements and > boundaries; this file records what the repository currently implements and
> has evidence for. > has evidence for.
@ -22,8 +22,10 @@ retains a legacy set of local task executors for unattended repository work.
actor `agt`, returns the complete `GatewayResult`, and never falls back to a actor `agt`, returns the complete `GatewayResult`, and never falls back to a
legacy approach after profile refusal or failure. legacy approach after profile refusal or failure.
- Profile-absent rows use the coexistence registry in `approaches.py`. - Profile-absent rows use the coexistence registry in `approaches.py`.
Unmatched rows fail visibly. JSON task files remain available for local Dispatch requires an explicit, non-expired
development, and issue-core polling remains as a legacy compatibility path. `AGENT_HARNESS_LEGACY_APPROACHES_UNTIL` date; unmatched or disabled routes
fail visibly. JSON task files remain available for local development, and
issue-core polling remains as a legacy compatibility path.
- The production claim loop is a single-concurrency user systemd service on - The production claim loop is a single-concurrency user systemd service on
railiance01. Claim failures use the configured poll interval and active runs railiance01. Claim failures use the configured poll interval and active runs
receive lease heartbeats. receive lease heartbeats.
@ -34,6 +36,9 @@ retains a legacy set of local task executors for unattended repository work.
kaizen-agentic persona bundle, invoke Claude Code in the target checkout, kaizen-agentic persona bundle, invoke Claude Code in the target checkout,
impose a wall-clock timeout and named Claude tool allow-list, and require a impose a wall-clock timeout and named Claude tool allow-list, and require a
new Git `HEAD` before reporting success. new Git `HEAD` before reporting success.
- Local task files may carry an explicit v1 `repository_grant`. These runs
validate commit ancestry/count, changed paths, clean post-state, protected Git
metadata, and remote refs under the repository transaction before success.
- Optional Claude stream JSON is reduced to tool/hook audit events and can be - Optional Claude stream JSON is reduced to tool/hook audit events and can be
reported to State Hub. reported to State Hub.
- Versioned profiled rows delegate profile, rein, model, tool, sandbox, and - Versioned profiled rows delegate profile, rein, model, tool, sandbox, and
@ -58,9 +63,10 @@ retains a legacy set of local task executors for unattended repository work.
session to repository read/edit operations and selected local Git commands; session to repository read/edit operations and selected local Git commands;
lane itself is metadata and consistency validation, not an OS security lane itself is metadata and consistency validation, not an OS security
boundary. boundary.
- Runs can append `.kaizen/metrics/<agent>/executions.jsonl`, regenerate the - Grant-absent compatibility runs can append repository-local kaizen metrics.
summary, emit State Hub progress/tool/token events, and close an associated Granted runs instead require private, durable external metrics with a
Hub task. These reporting operations are best-effort. projection descriptor so accepted checkouts remain clean. State Hub
progress/tool/token events and task closing remain best-effort.
- llm-connect HTTP errors retain only bounded, allowlisted provider diagnosis. - llm-connect HTTP errors retain only bounded, allowlisted provider diagnosis.
### Packaging and deployment ### Packaging and deployment
@ -87,12 +93,11 @@ retains a legacy set of local task executors for unattended repository work.
- Direct legacy agent sessions rely on Claude Code's own tool mediation and - Direct legacy agent sessions rely on Claude Code's own tool mediation and
the host checkout. They are not an OS-level sandbox and do not prove that 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 no push/network activity occurred after the run. Unattended mutation now
takes a process-safe repository lock (ADR-002 / T02); commit acceptance takes a process-safe repository lock (ADR-002 / T02).
is still the `HEAD`-changed check until T03. - Explicitly granted local task files have full repository acceptance and
- Commit acceptance currently means only that `HEAD` changed. It does not yet clean external metrics. Grant-absent compatibility and profiled queue runs
validate the changed-file set, branch/parent shape, clean working tree, still use their prior result contracts because Activity Core carries no
remote state, or provenance of the new commit. Metrics are written after authoritative repository grant. Durable queue-close replay is also not live.
this check and can leave target-repo changes for a later commit.
- The worker is sequential and repository mapping is host configuration. There - The worker is sequential and repository mapping is host configuration. There
is no multi-worker repository lease, per-tenant process isolation, generic is no multi-worker repository lease, per-tenant process isolation, generic
credential broker, or tenant onboarding API. credential broker, or tenant onboarding API.
@ -109,8 +114,9 @@ retains a legacy set of local task executors for unattended repository work.
- Profile routing and rein selection belong to Glas. - Profile routing and rein selection belong to Glas.
- Sandbox lifecycle and isolation belong to sand-boxer. - Sandbox lifecycle and isolation belong to sand-boxer.
- Provider abstraction belongs to llm-connect. - Provider abstraction belongs to llm-connect.
- Durable work history and decisions belong to State Hub; agent memory and - Durable work history and decisions belong to State Hub; agent memory remains
metrics remain in the consuming repository. in the consuming repository. Granted-run metrics remain external until a
separately granted kaizen projection commits them to the instance.
## Orientation ## Orientation

View file

@ -3,6 +3,21 @@
Single shared harness instance on **railiance01**. Secrets stay on the host Single shared harness instance on **railiance01**. Secrets stay on the host
(Lanes 2–3); the container image is the portable runtime package. (Lanes 2–3); the container image is the portable runtime package.
## Supported runtime topology
The authoritative production runtime is the railiance01 user service
`rein-aharness-claim-loop.service`. It owns the configured repository
checkouts, private runtime state, worker credential, lease heartbeats, and
signal-driven shutdown behavior.
The Kubernetes Deployment is explicitly labeled `packaging-smoke` and runs
`sleep infinity`. It proves that the image can be scheduled with its hardened
container settings; it is not ready to claim work and is not a failover worker.
Do not give it an Activity Core worker credential or scale it as execution
capacity. Promoting Kubernetes requires a separate reviewed cutover with real
workspace, credential, repository-lock, Glas/sandbox, shutdown, and recovery
semantics.
> **Renamed from agent-harness (HARNESS-WP-0002-T02) — cutover done > **Renamed from agent-harness (HARNESS-WP-0002-T02) — cutover done
> 2026-07-26.** Railiance now runs `rein-aharness` end to end: image tag, > 2026-07-26.** Railiance now runs `rein-aharness` end to end: image tag,
> k8s namespace, CLI command, Python package, host secrets dir, and > k8s namespace, CLI command, Python package, host secrets dir, and
@ -48,6 +63,9 @@ Single shared harness instance on **railiance01**. Secrets stay on the host
- Lane 3 AppRole under `~/.local/rein-aharness/approle-binky-mail` - Lane 3 AppRole under `~/.local/rein-aharness/approle-binky-mail`
- `source ~/.local/rein-aharness/env` - `source ~/.local/rein-aharness/env`
- Hub: `http://127.0.0.1:18000` (ops-bridge) or in-cluster `state-hub.state-hub.svc` - Hub: `http://127.0.0.1:18000` (ops-bridge) or in-cluster `state-hub.state-hub.svc`
- While profile-absent tenant definitions remain, set a reviewed ISO expiry in
`AGENT_HARNESS_LEGACY_APPROACHES_UNTIL`. The checked-in example expires
2026-12-31; missing or expired values refuse compatibility dispatch.
## Build & load image (workstation → railiance01) ## Build & load image (workstation → railiance01)

View file

@ -1,5 +1,5 @@
# Long-lived instance placeholder until T03 task intake polls issue-core. # Packaging-smoke placeholder only. The supported production runtime is the
# Keeps one ready replica with harness CLI + git tools; no LLM session here. # railiance01 user service; this Deployment never claims or executes work.
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@ -8,6 +8,8 @@ metadata:
labels: labels:
app.kubernetes.io/name: rein-aharness app.kubernetes.io/name: rein-aharness
app.kubernetes.io/part-of: rein-aharness app.kubernetes.io/part-of: rein-aharness
app.kubernetes.io/component: packaging-smoke
rein-aharness.railiance.io/runtime-role: packaging-smoke
spec: spec:
replicas: 1 replicas: 1
selector: selector:
@ -18,6 +20,8 @@ spec:
labels: labels:
app.kubernetes.io/name: rein-aharness app.kubernetes.io/name: rein-aharness
app.kubernetes.io/part-of: rein-aharness app.kubernetes.io/part-of: rein-aharness
app.kubernetes.io/component: packaging-smoke
rein-aharness.railiance.io/runtime-role: packaging-smoke
spec: spec:
securityContext: securityContext:
fsGroup: 10001 fsGroup: 10001

View file

@ -9,6 +9,7 @@ AGENT_HARNESS_OPS_LABELS=automated
AGENT_HARNESS_OPS_LABELS_MODE=any AGENT_HARNESS_OPS_LABELS_MODE=any
AGENT_HARNESS_OPS_LEASE_SECONDS=900 AGENT_HARNESS_OPS_LEASE_SECONDS=900
AGENT_HARNESS_CLAIM_INTERVAL=30 AGENT_HARNESS_CLAIM_INTERVAL=30
AGENT_HARNESS_LEGACY_APPROACHES_UNTIL=2026-12-31
AGENT_HARNESS_REPO_MAP={"freedom-intelligence":"/home/tegwick/freedom-intelligence","binky-control":"/home/tegwick/binky-control"} AGENT_HARNESS_REPO_MAP={"freedom-intelligence":"/home/tegwick/freedom-intelligence","binky-control":"/home/tegwick/binky-control"}
AGENT_HARNESS_REPO_ROOTS=/home/tegwick:/home/tegwick/work AGENT_HARNESS_REPO_ROOTS=/home/tegwick:/home/tegwick/work
LLM_CONNECT_URL=k8s://activity-core/llm-connect:8080 LLM_CONNECT_URL=k8s://activity-core/llm-connect:8080

View file

@ -9,6 +9,8 @@ Type=simple
WorkingDirectory=%h/rein-aharness WorkingDirectory=%h/rein-aharness
Environment=PYTHONUNBUFFERED=1 Environment=PYTHONUNBUFFERED=1
Environment=KUBECONFIG=/etc/rancher/k3s/k3s.yaml Environment=KUBECONFIG=/etc/rancher/k3s/k3s.yaml
# Temporary profile-absent compatibility; remove after the four tenant migrations.
Environment=AGENT_HARNESS_LEGACY_APPROACHES_UNTIL=2026-12-31
# Ensure kubectl is available for k8s:// URL resolution in rein-aharness-claim # Ensure kubectl is available for k8s:// URL resolution in rein-aharness-claim
Environment=PATH=/usr/local/bin:/usr/bin:/bin:%h/bin Environment=PATH=/usr/local/bin:/usr/bin:/bin:%h/bin
# Wrapper loads claim-loop.env (JSON-safe) and resolves k8s:// ClusterIPs # Wrapper loads claim-loop.env (JSON-safe) and resolves k8s:// ClusterIPs

View file

@ -37,8 +37,8 @@
| Task intake | `intake.py` + `taskspec.py` | issue-core GET/PATCH poll+claim; JSON task-file for local dev | NATS when activity-core migrates | | Task intake | `intake.py` + `taskspec.py` | issue-core GET/PATCH poll+claim; JSON task-file for local dev | NATS when activity-core migrates |
| Persona | `persona.py` | `kaizen-agentic schedule prepare` (ADR-005) | unchanged, plus phase-memory profile hook | | Persona | `persona.py` | `kaizen-agentic schedule prepare` (ADR-005) | unchanged, plus phase-memory profile hook |
| Session | `adapter.py` | `AgenticClaudeCodeAdapter` (cwd-pinned, profile allow-list) | + hosted adapters | | Session | `adapter.py` | `AgenticClaudeCodeAdapter` (cwd-pinned, profile allow-list) | + hosted adapters |
| Orchestration | `runner.py` | profile/budget → session → commit → metrics → hub | activity-core intake | | Orchestration | `runner.py` | profile/budget → transaction → session → grant acceptance → metrics → hub | activity-core intake |
| Metrics | `metrics.py` | ADR-004 `.kaizen/metrics` write per run | correlate with Helix fleet metrics | | Metrics | `metrics.py` | compatibility `.kaizen/metrics`; durable external ledger for granted runs | kaizen-owned projection |
| Mail lane | `mailscan.py` | deterministic credentialed pre-step outside the session | pattern generalizes to other credentialed pre-steps | | Mail lane | `mailscan.py` | deterministic credentialed pre-step outside the session | pattern generalizes to other credentialed pre-steps |
| Hub reporting | `hub.py` | REST progress event + task close + token event | unchanged | | Hub reporting | `hub.py` | REST progress event + task close + token event | unchanged |
@ -71,7 +71,7 @@ non-interactive AppRole. See binky-control
- **Regulation:** manifest declares, harness enforces. flex-auth gates - **Regulation:** manifest declares, harness enforces. flex-auth gates
apply at credential acquisition; ops-warden catalogs every lane; each apply at credential acquisition; ops-warden catalogs every lane; each
instance runs under a named hub identity (`agt-…`). instance runs under a named hub identity (`agt-…`).
- **Evolution:** the harness emits `.kaizen/metrics` per run so the - **Evolution:** the harness emits kaizen-compatible metrics per run so the
kaizen optimization loop covers both blueprints and the harness itself. kaizen optimization loop covers both blueprints and the harness itself.
Harness releases are versioned; instances pin majors; blueprint Harness releases are versioned; instances pin majors; blueprint
conformance tests run before rollout. conformance tests run before rollout.

36
docs/external-metrics.md Normal file
View file

@ -0,0 +1,36 @@
# Durable external metrics
Repository-granted runs must not dirty a checkout after acceptance. Their
kaizen-compatible execution record and summary are therefore written beneath:
```text
$REIN_AHARNESS_STATE_DIR/execution-metrics/<repository-id>/<agent-id>/
```
When the explicit state variable is absent, the runtime uses
`$XDG_STATE_HOME/rein-aharness` or `~/.local/state/rein-aharness`. Repository
and agent directory names are SHA-256-derived identifiers rather than caller
text. Directories are mode `0700`; records and lock files are mode `0600`.
Each directory contains:
- `executions.jsonl`: append-only kaizen `ExecutionRecord` values;
- `summary.json`: regenerated aggregate metrics; and
- `projection.json`: repository id/name, agent name, and intended consuming
path `.kaizen/metrics/<agent>`.
Writes serialize on a process-safe lock. The execution record is flushed and
`fsync`ed before the summary and projection descriptors are atomically replaced
and directory metadata is `fsync`ed. Replaying the same transaction identity
regenerates the derived files without adding a duplicate ledger record. Failure
to persist this evidence makes a granted run unsuccessful; granted execution
cannot opt out with `--no-metrics`.
Projection is a separate repository mutation. This harness does not copy these
files back automatically, because doing so would invalidate the accepted clean
post-state. A future kaizen-owned projection lane may consume
`projection.json`, copy `executions.jsonl` and `summary.json` to the declared
target, and commit them under its own explicit repository grant. Until that
lane exists, the external files are the durable source of truth for granted
runs. Grant-absent compatibility runs continue to write repository-local
metrics directly.

View file

@ -1,15 +1,16 @@
# Legacy profile-absent runtime inventory # Legacy profile-absent runtime inventory
Status: preparatory inventory for `HARNESS-WP-0003-T04` Status: active migration inventory for `REINAH-WP-0003-T04`
Captured: 2026-08-23 Captured: 2026-08-23; compatibility guard added 2026-09-04
Runtime behavior source: rein-aharness `6e0e23c` Initial inventory source: rein-aharness `6e0e23c`; guard behavior updated in
the implementation recorded by this workplan.
This inventory records the tenant-specific execution paths selected when an This inventory records the tenant-specific execution paths selected when an
Activity Core `ops_run` has no `harness_profile_ref`. It is deliberately Activity Core `ops_run` has no `harness_profile_ref`. It began as read-only
read-only preparation: T04 remains `wait` until ADR-002 is accepted, and this preparation; after ADR-002 acceptance it also records the dated compatibility
document neither enables a definition nor changes a production route. guard. This document does not itself enable a definition.
The definition state below is the committed file state in Activity Core, The definition state below is the committed file state in Activity Core,
freedom-intelligence, and binky-control. Live schedule projection, timer state, freedom-intelligence, and binky-control. Live schedule projection, timer state,
@ -24,12 +25,15 @@ id, title, hint, labels, source id, and target repo.
None of the four committed definitions below declares `harness_profile_ref` or None of the four committed definitions below declares `harness_profile_ref` or
`approach_hint`; their current route therefore depends on labels and substring `approach_hint`; their current route therefore depends on labels and substring
matching. A present profile ref bypasses this table and must never fall back to matching. Dispatch now additionally requires an explicit, non-expired ISO date
it. in `AGENT_HARNESS_LEGACY_APPROACHES_UNTIL`. Missing, malformed, or expired
values fail terminally before target resolution or executor invocation. A
present profile ref bypasses both this table and the compatibility flag and
must never fall back to either.
| Selector | Tenant coupling | Direct executor | | Selector | Tenant coupling | Direct executor |
|---|---|---| |---|---|---|
| `fi-research-brief` | FI labels, names, output schema, completion event, literal result repo, default push | `fi_research_brief.run_fi_research_brief` | | `fi-research-brief` | FI labels, names, output schema, completion event, literal result repo | `fi_research_brief.run_fi_research_brief` |
| `brief-daily` | Binky labels, files, prompt/output schema, completion event, literal result repo | `brief_daily.run_brief_daily` | | `brief-daily` | Binky labels, files, prompt/output schema, completion event, literal result repo | `brief_daily.run_brief_daily` |
| `brief-weekly` | Binky labels, milestone/RISK-005 semantics, completion event, literal result repo | `brief_weekly.run_brief_weekly` | | `brief-weekly` | Binky labels, milestone/RISK-005 semantics, completion event, literal result repo | `brief_weekly.run_brief_weekly` |
| `mail-scan+triage` | Binky labels, mailbox config/path, OpenBao path, report/log schema and events | `mailscan.run_mail_scan` then `mail_triage.run_mail_triage` | | `mail-scan+triage` | Binky labels, mailbox config/path, OpenBao path, report/log schema and events | `mailscan.run_mail_scan` then `mail_triage.run_mail_triage` |
@ -38,9 +42,10 @@ it.
An unmatched route fails terminally. Most matched executor failures set An unmatched route fails terminally. Most matched executor failures set
`reopen=true`; an unexpected exception also reopens. Activity Core applies its `reopen=true`; an unexpected exception also reopens. Activity Core applies its
attempt limit, but the harness currently has no shared repository transaction attempt limit. The harness now records a repository transaction identity, but
identity with which to distinguish a safe retry from a repeat after partial Activity Core does not yet carry an accepted repository grant or reconcile a
mutation. response-lost terminal close, so a retry after ambiguous completion remains a
T03 gate.
## Scheduled definitions ## Scheduled definitions
@ -55,13 +60,13 @@ mutation.
| Model/credential lane | HTTP `LLM_CONNECT_URL`; model from `FI_RESEARCH_BRIEF_MODEL`, then Binky-named `BRIEF_DAILY_MODEL` / `MAIL_TRIAGE_MODEL`; provider credential remains behind llm-connect | | Model/credential lane | HTTP `LLM_CONNECT_URL`; model from `FI_RESEARCH_BRIEF_MODEL`, then Binky-named `BRIEF_DAILY_MODEL` / `MAIL_TRIAGE_MODEL`; provider credential remains behind llm-connect |
| Output | `briefs/YYYY/MM/YYYY-MM-DD.md`; model JSON is rendered deterministically | | Output | `briefs/YYYY/MM/YYYY-MM-DD.md`; model JSON is rendered deterministically |
| Repository mutation | Stages only the brief path and creates one local commit | | Repository mutation | Stages only the brief path and creates one local commit |
| Publication | **Pushes `origin HEAD` by default** unless `FI_RESEARCH_BRIEF_PUSH` is false; push failure is swallowed and the run remains successful | | Publication | Local commit only. The former `FI_RESEARCH_BRIEF_PUSH` path was removed 2026-09-04; publication requires a separate owner/grant. |
| Completion | Best-effort State Hub `fi_daily_brief` with path/date/candidate count; `fi_brief_status` uses it to clear due state | | Completion | Best-effort State Hub `fi_daily_brief` with path/date/candidate count; `fi_brief_status` uses it to clear due state |
| Failure/retry | Normal generation/commit failure reopens; an existing daily path succeeds idempotently | | Failure/retry | Normal generation/commit failure reopens; an existing daily path succeeds idempotently |
| Current rollback material | Disable/pause the Activity Core definition. FI documents a disabled 07:35 host timer as break-glass, but its installer still enables that timer and must not be invoked casually | | Current rollback material | Disable/pause the Activity Core definition. FI documents a disabled 07:35 host timer as break-glass, but its installer still enables that timer and must not be invoked casually |
Migration must remove the implicit push. Publication, if still needed, becomes Publication, if still needed, must become a separately named and granted
a separately named and granted capability with remote-ref and result evidence. capability with remote-ref and result evidence.
The Binky-named fallback model environment variables must not survive in an FI The Binky-named fallback model environment variables must not survive in an FI
owned declaration. owned declaration.
@ -148,15 +153,17 @@ a repository transaction or remote-ref check.
2. **Tenant code in the shared runtime:** prompts, output schemas, file paths, 2. **Tenant code in the shared runtime:** prompts, output schemas, file paths,
event types, FI publication, Binky risk logic, mailbox paths, and event types, FI publication, Binky risk logic, mailbox paths, and
credential lanes require edits to rein-aharness. credential lanes require edits to rein-aharness.
3. **No shared repository transaction:** dirty baselines, partial writes, 3. **Queue acceptance is incomplete:** T02 now bounds dirty baselines,
unexpected commits/paths, lease loss, and retries after mutation are not transaction identity, and lease-loss cancellation, and local granted tasks
bounded. T02/T03 remain prerequisites for any retained direct executor. have T03 acceptance. Activity Core still lacks authoritative grant carriage
and terminal-close reconciliation for queued execution.
4. **Required evidence is best-effort:** Hub delivery failure does not fail or 4. **Required evidence is best-effort:** Hub delivery failure does not fail or
reconcile a run even though the due resolvers depend on those events. reconcile a run even though the due resolvers depend on those events.
5. **Completion meanings diverge:** mailbox scan and triage have different 5. **Completion meanings diverge:** mailbox scan and triage have different
events, while the Activity definition describes one intake task. events, while the Activity definition describes one intake task.
6. **Publication is implicit in FI:** default push plus swallowed failure cannot 6. **FI publication is now separated:** the compatibility executor is
meet an explicit grant/evidence contract. local-commit-only; any future publish lane still needs an explicit owner,
grant, and evidence contract.
7. **Mutable sibling/runtime dependencies:** mail scan defaults to a sibling 7. **Mutable sibling/runtime dependencies:** mail scan defaults to a sibling
email-connect checkout, and model selection depends on process environment email-connect checkout, and model selection depends on process environment
rather than a versioned execution declaration. rather than a versioned execution declaration.
@ -169,8 +176,9 @@ a repository transaction or remote-ref check.
This sequence is preparatory and does not choose a profile or capability owner This sequence is preparatory and does not choose a profile or capability owner
before ADR-002 acknowledgement. before ADR-002 acknowledgement.
1. Land T02/T03 repository transaction, lease-loss, acceptance, and evidence 1. Carry T02's transaction/lease evidence and T03's repository grant through
reconciliation for any compatibility executor that remains callable. Activity Core, then activate reconciled close evidence for any compatibility
executor that remains callable.
2. For each scheduled definition, obtain an owner-approved declaration of the 2. For each scheduled definition, obtain an owner-approved declaration of the
exact input schema, allowed paths, model route, credential route, completion exact input schema, allowed paths, model route, credential route, completion
event, commit policy, publication policy, and rollback. event, commit policy, publication policy, and rollback.
@ -184,6 +192,6 @@ before ADR-002 acknowledgement.
5. Enable the replacement, verify the due resolver and durable evidence, then 5. Enable the replacement, verify the due resolver and durable evidence, then
remove that definition from substring routing. remove that definition from substring routing.
6. After all four definitions migrate, enable 6. After all four definitions migrate, enable
`ACTIVITY_CORE_REQUIRE_HARNESS_PROFILE=true` for governed execution, delete `ACTIVITY_CORE_REQUIRE_HARNESS_PROFILE=true` for governed execution, remove
`_DEFINITION_HINTS` and tenant approach rules, and reconcile or retire the `AGENT_HARNESS_LEGACY_APPROACHES_UNTIL`, delete `_DEFINITION_HINTS` and
tenant host timers. tenant approach rules, and reconcile or retire the tenant host timers.

View file

@ -38,6 +38,7 @@ the legacy coexistence path until the migration flag is enabled upstream.
| `AGENT_HARNESS_OPS_LABELS_MODE` | `any` | `any` or `all` | | `AGENT_HARNESS_OPS_LABELS_MODE` | `any` | `any` or `all` |
| `AGENT_HARNESS_OPS_LEASE_SECONDS` | `900` | claim lease | | `AGENT_HARNESS_OPS_LEASE_SECONDS` | `900` | claim lease |
| `AGENT_HARNESS_CLAIM_INTERVAL` | `30` | empty-queue poll interval | | `AGENT_HARNESS_CLAIM_INTERVAL` | `30` | empty-queue poll interval |
| `AGENT_HARNESS_LEGACY_APPROACHES_UNTIL` | unset | ISO expiry date required for profile-absent compatibility routing |
| `AGENT_HARNESS_REPO_MAP` | `{}` | JSON slug→checkout path | | `AGENT_HARNESS_REPO_MAP` | `{}` | JSON slug→checkout path |
| `AGENT_HARNESS_REPO_ROOTS` | `~:~/work` | slug search roots | | `AGENT_HARNESS_REPO_ROOTS` | `~:~/work` | slug search roots |
@ -48,6 +49,7 @@ export AGENT_HARNESS_REPO_MAP='{
"freedom-intelligence":"/home/tegwick/freedom-intelligence", "freedom-intelligence":"/home/tegwick/freedom-intelligence",
"binky-control":"/home/tegwick/binky-control" "binky-control":"/home/tegwick/binky-control"
}' }'
export AGENT_HARNESS_LEGACY_APPROACHES_UNTIL=2026-12-31
export ACTIVITY_CORE_URL=http://127.0.0.1:8010 # or ClusterIP via tunnel export ACTIVITY_CORE_URL=http://127.0.0.1:8010 # or ClusterIP via tunnel
export ACTIVITY_CORE_WORKER_TOKEN=… # from actcore-runtime-secret export ACTIVITY_CORE_WORKER_TOKEN=… # from actcore-runtime-secret
``` ```

View file

@ -1,6 +1,7 @@
# Repository grant contract # Repository grant contract
Status: **v1 parsed, validation-ready, not execution-enabled**. Status: **v1 enabled for local `TaskSpec` files; queued/profiled carriage is
not yet available**.
`repository_grant` is the explicit authority envelope for a bounded local Git `repository_grant` is the explicit authority envelope for a bounded local Git
mutation. It is separate from task prose, labels, organizational attribution, mutation. It is separate from task prose, labels, organizational attribution,
@ -39,20 +40,28 @@ The repository acceptance validator converts the grant into the policy used to
check descendant commits, changed paths, clean post-state, protected Git check descendant commits, changed paths, clean post-state, protected Git
metadata, and local remote-tracking refs. metadata, and local remote-tracking refs.
## Current fail-closed posture ## Current execution posture
`TaskSpec.from_file` parses this contract, but `run_task` deliberately refuses `TaskSpec.from_file` parses this contract and `run_task` executes it under the
any task that supplies it before adapter dispatch. The Activity Core and canonical repository transaction. A successful adapter result is accepted only
issue-core adapters do not currently populate it. Existing grant-absent direct when the commit ancestry/count, changed paths, clean post-state, protected Git
and compatibility runs retain their prior behavior while the transaction path metadata, and remote-tracking refs satisfy the exact grant. The result carries
remains production-inert. bounded grant, baseline, policy, commit, and path evidence without raw grant
paths, prompts, or provider output.
Execution may be enabled only after: Granted runs require durable external metrics and refuse `--no-metrics`. Their
metrics are written outside the checkout only after acceptance, so a successful
run remains clean. See [external-metrics.md](external-metrics.md).
Activity Core and issue-core adapters do not populate `repository_grant`.
Queued/profiled runs therefore cannot claim repository acceptance under this
contract. Existing grant-absent direct and compatibility runs retain their
legacy `HEAD`-changed behavior during migration.
Queued/profiled execution may use this contract only after:
1. an authoritative Activity Core/profile field carries the reviewed grant; 1. an authoritative Activity Core/profile field carries the reviewed grant;
2. the lease-bound transaction wraps adapter dispatch and result close; 2. required close evidence durably records the grant, transaction, and accepted
3. the runner validates the accepted result against this exact grant; and
4. required close evidence durably records the grant, transaction, and accepted
result identities. result identities.
Task descriptions, labels, `execution_refs`, consuming-repo defaults, and Task descriptions, labels, `execution_refs`, consuming-repo defaults, and

View file

@ -3,8 +3,8 @@
Consuming repos declare instances in `.kaizen/schedule.yml`; this package Consuming repos declare instances in `.kaizen/schedule.yml`; this package
runs them: resolve tool profile + budget, load persona orientation runs them: resolve tool profile + budget, load persona orientation
(kaizen-agentic schedule prepare), run a bounded agentic session via an (kaizen-agentic schedule prepare), run a bounded agentic session via an
llm-connect adapter, verify the local commit, write `.kaizen/metrics`, llm-connect adapter, validate granted local commits, record metrics, and report
and report to the Custodian State Hub. to the Custodian State Hub.
""" """
__version__ = "0.1.0" __version__ = "0.1.0"

View file

@ -12,7 +12,9 @@ How to add a row:
from __future__ import annotations from __future__ import annotations
import os
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import date
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -35,6 +37,21 @@ APPROACH_MAIL_TRIAGE = "mail-triage"
APPROACH_MAIL_PIPELINE = "mail-scan+triage" APPROACH_MAIL_PIPELINE = "mail-scan+triage"
APPROACH_AGENT_SESSION = "agent-session" APPROACH_AGENT_SESSION = "agent-session"
APPROACH_UNMATCHED = "unmatched" APPROACH_UNMATCHED = "unmatched"
LEGACY_APPROACHES_UNTIL_ENV = "AGENT_HARNESS_LEGACY_APPROACHES_UNTIL"
def legacy_approaches_enabled(
value: str | None = None,
*,
today: date | None = None,
) -> bool:
"""Require an explicit, non-expired ISO date for profile-absent routing."""
raw = os.environ.get(LEGACY_APPROACHES_UNTIL_ENV, "") if value is None else value
try:
expires = date.fromisoformat(raw.strip())
except (AttributeError, ValueError):
return False
return (today or date.today()) <= expires
@dataclass(frozen=True) @dataclass(frozen=True)
@ -185,6 +202,19 @@ def execute_approach(
reopen=False, reopen=False,
) )
if not legacy_approaches_enabled():
configured = os.environ.get(LEGACY_APPROACHES_UNTIL_ENV, "").strip()
state = f"expired at {configured}" if configured else "not configured"
return ApproachResult(
ok=False,
approach=name,
reason=(
"refused: profile-absent compatibility routing is disabled "
f"({LEGACY_APPROACHES_UNTIL_ENV} {state})"
),
reopen=False,
)
try: try:
target = resolve_ops_target(run, cfg) target = resolve_ops_target(run, cfg)
except TaskSpecError as exc: except TaskSpecError as exc:
@ -266,6 +296,7 @@ def execute_approach(
reopen=True, reopen=True,
) )
def _run_fi(target: Path, *, report_to_hub: bool, commit: bool) -> ApproachResult: def _run_fi(target: Path, *, report_to_hub: bool, commit: bool) -> ApproachResult:
from rein_aharness.fi_research_brief import run_fi_research_brief from rein_aharness.fi_research_brief import run_fi_research_brief

View file

@ -305,7 +305,10 @@ def main(argv: list[str] | None = None) -> int:
run.add_argument( run.add_argument(
"--no-metrics", "--no-metrics",
action="store_true", action="store_true",
help="Skip writing .kaizen/metrics in the target repo", help=(
"Skip compatibility .kaizen/metrics writes "
"(not permitted with repository_grant)"
),
) )
run.add_argument( run.add_argument(
"--stream-tool-events", "--stream-tool-events",

View file

@ -420,20 +420,6 @@ def run_fi_research_brief(
) )
committed = True committed = True
head_after = _git(repo, "rev-parse", "HEAD") head_after = _git(repo, "rev-parse", "HEAD")
# Best-effort push so workstation / Forgejo see the brief.
# Diverged branches must not fail the run; log via reason only if push fails
# after a successful write.
if committed and os.environ.get("FI_RESEARCH_BRIEF_PUSH", "1").strip().lower() not in {
"0",
"false",
"no",
"off",
}:
try:
_git(repo, "push", "origin", "HEAD")
except FiResearchBriefError:
# Leave brief committed locally; operators reconcile git separately.
pass
except FiResearchBriefError as exc: except FiResearchBriefError as exc:
result = FiResearchBriefResult( result = FiResearchBriefResult(
ok=False, ok=False,

View file

@ -1,7 +1,8 @@
"""Write per-run records into the target repo's `.kaizen/metrics` tree. """Write per-run metrics to compatibility or durable external storage.
Follows kaizen-agentic ADR-004 conventions so the optimization loop can Legacy grant-absent runs retain the kaizen-agentic ADR-004 repository layout.
observe harness-run agents: Accepted grant runs use private external state and a projection descriptor so
metrics cannot dirty the validated checkout.
.kaizen/metrics/<agent>/ .kaizen/metrics/<agent>/
executions.jsonl # append-only executions.jsonl # append-only
@ -10,7 +11,11 @@ observe harness-run agents:
from __future__ import annotations from __future__ import annotations
import fcntl
import hashlib
import json import json
import os
import uuid
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
@ -46,6 +51,20 @@ def metrics_dir(project_root: Path, agent: str) -> Path:
return Path(project_root) / ".kaizen" / "metrics" / agent return Path(project_root) / ".kaizen" / "metrics" / agent
def external_metrics_dir(
project_root: Path | str,
agent: str,
*,
state_dir: Path | None = None,
) -> Path:
"""Return the private durable metrics directory for a granted run."""
root = Path(project_root).expanduser().resolve()
base = state_dir.expanduser().resolve() if state_dir else _state_dir()
repo_id = hashlib.sha256(str(root).encode("utf-8")).hexdigest()[:32]
agent_id = hashlib.sha256(str(agent).encode("utf-8")).hexdigest()[:32]
return base / "execution-metrics" / repo_id / agent_id
def _utc_now() -> str: def _utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace( return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace(
"+00:00", "Z" "+00:00", "Z"
@ -133,18 +152,17 @@ def record_execution(
directory = metrics_dir(root, agent) directory = metrics_dir(root, agent)
directory.mkdir(parents=True, exist_ok=True) directory.mkdir(parents=True, exist_ok=True)
record = ExecutionRecord( record = _execution_record(
timestamp=_utc_now(), root,
agent=agent, agent,
success=success, success=success,
execution_time_s=float(execution_time_s), execution_time_s=execution_time_s,
session_id=session_id,
metadata=metadata or {},
repo=root.name,
tokens=tokens, tokens=tokens,
committed=committed, committed=committed,
head_after=head_after, head_after=head_after,
reason=reason, reason=reason,
metadata=metadata,
session_id=session_id,
) )
executions_path = directory / "executions.jsonl" executions_path = directory / "executions.jsonl"
@ -158,3 +176,189 @@ def record_execution(
encoding="utf-8", encoding="utf-8",
) )
return executions_path return executions_path
def record_external_execution(
project_root: Path | str,
agent: str,
*,
success: bool,
execution_time_s: float = 0.0,
tokens: int | None = None,
committed: bool | None = None,
head_after: str | None = None,
reason: str | None = None,
metadata: dict[str, Any] | None = None,
session_id: str | None = None,
state_dir: Path | None = None,
) -> Path:
"""Durably record granted-run metrics without dirtying the target checkout."""
root = Path(project_root).expanduser().resolve()
projection_target = _projection_target(agent)
directory = external_metrics_dir(root, agent, state_dir=state_dir)
_ensure_private_directory(directory)
lock_path = directory / ".lock"
lock_fd = os.open(lock_path, os.O_RDWR | os.O_CREAT, 0o600)
try:
os.fchmod(lock_fd, 0o600)
fcntl.flock(lock_fd, fcntl.LOCK_EX)
record = _execution_record(
root,
agent,
success=success,
execution_time_s=execution_time_s,
tokens=tokens,
committed=committed,
head_after=head_after,
reason=reason,
metadata=metadata,
session_id=session_id,
)
executions_path = directory / "executions.jsonl"
existing = (
executions_path.read_text(encoding="utf-8")
if executions_path.exists()
else ""
)
if existing and not existing.endswith("\n"):
raise OSError("external metrics ledger has an incomplete record")
records = _load_external_executions(executions_path) if existing else []
already_recorded = session_id is not None and any(
item.get("session_id") == session_id for item in records
)
if not already_recorded:
record_line = record.to_json_line()
_atomic_write_text(executions_path, existing + record_line + "\n")
records.append(json.loads(record_line))
_atomic_write_text(
directory / "summary.json",
json.dumps(regenerate_summary(agent, records), indent=2, sort_keys=True)
+ "\n",
)
repo_id = directory.parent.name
_atomic_write_text(
directory / "projection.json",
json.dumps(
{
"agent": agent,
"repository_id": repo_id,
"repository_name": root.name,
"target_relative_directory": projection_target,
},
indent=2,
sort_keys=True,
)
+ "\n",
)
_fsync_directory(directory)
return executions_path
finally:
try:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
finally:
os.close(lock_fd)
def _projection_target(agent: str) -> str:
"""Return a checkout-relative projection target for a safe agent identity."""
if (
not agent
or agent in {".", ".."}
or "/" in agent
or "\\" in agent
or "\x00" in agent
or len(agent) > 200
):
raise OSError("agent identity cannot form a safe metrics projection")
return f".kaizen/metrics/{agent}"
def _execution_record(
root: Path,
agent: str,
*,
success: bool,
execution_time_s: float,
tokens: int | None,
committed: bool | None,
head_after: str | None,
reason: str | None,
metadata: dict[str, Any] | None,
session_id: str | None,
) -> ExecutionRecord:
return ExecutionRecord(
timestamp=_utc_now(),
agent=agent,
success=success,
execution_time_s=float(execution_time_s),
session_id=session_id,
metadata=metadata or {},
repo=root.name,
tokens=tokens,
committed=committed,
head_after=head_after,
reason=reason,
)
def _load_external_executions(path: Path) -> list[dict[str, Any]]:
records: list[dict[str, Any]] = []
for line in path.read_text(encoding="utf-8").splitlines():
try:
value = json.loads(line)
except json.JSONDecodeError as exc:
raise OSError("external metrics ledger contains invalid JSON") from exc
if not isinstance(value, dict):
raise OSError("external metrics ledger contains a non-object record")
records.append(value)
return records
def _state_dir() -> Path:
explicit = os.environ.get("REIN_AHARNESS_STATE_DIR", "").strip()
if explicit:
return Path(explicit).expanduser().resolve()
xdg = os.environ.get("XDG_STATE_HOME", "").strip()
if xdg:
return (Path(xdg).expanduser() / "rein-aharness").resolve()
return (Path.home() / ".local" / "state" / "rein-aharness").resolve()
def _ensure_private_directory(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
current = path
while current.name and current != current.parent:
os.chmod(current, 0o700)
if current.name == "execution-metrics":
break
current = current.parent
def _atomic_write_text(path: Path, value: str) -> None:
temporary = path.parent / f".{path.name}.{uuid.uuid4().hex}.tmp"
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
fd = -1
handle.write(value)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
os.chmod(path, 0o600)
_fsync_directory(path.parent)
except BaseException:
if fd >= 0:
os.close(fd)
try:
temporary.unlink()
except FileNotFoundError:
pass
raise
def _fsync_directory(path: Path) -> None:
fd = os.open(path, os.O_RDONLY)
try:
os.fsync(fd)
finally:
os.close(fd)

View file

@ -1,10 +1,4 @@
"""Versioned authority contract for repository mutation. """Versioned authority contract for accepted local repository mutation."""
The grant is parsed by TaskSpec but is not yet wired into run execution. A
supplied grant therefore causes run_task to refuse before adapter dispatch.
This keeps the contract reviewable without implying enforcement that the live
runner does not yet provide.
"""
from __future__ import annotations from __future__ import annotations

View file

@ -1,9 +1,9 @@
"""Process-safe transaction boundary for a local Git checkout. """Process-safe transaction boundary for a local Git checkout.
Wired into `run_task`, profile-absent `execute_approach` mutators, and the Wired into `run_task`, profile-absent `execute_approach` mutators, and the
profiled claim path under REINAH-WP-0003-T02 / ADR-002. Repository profiled claim path under REINAH-WP-0003-T02 / ADR-002. Explicit local
acceptance (T03) remains a separate read-only validator and is not applied TaskSpec grants activate repository acceptance; queued/profiled grant carriage
to live results yet. remains an upstream contract dependency.
""" """
from __future__ import annotations from __future__ import annotations

View file

@ -2,10 +2,10 @@
Flow: lock target repo → resolve tool profile / budget from instance Flow: lock target repo → resolve tool profile / budget from instance
manifest → snapshot HEAD → persona bundle → prompt → agentic session → manifest → snapshot HEAD → persona bundle → prompt → agentic session →
verify a new commit exists → kaizen metrics + hub progress event validate an explicit repository grant when present → metrics + hub progress
(+ task close). The run *fails* if the session pushed anywhere or left event (+ task close). Granted runs use durable external metrics so repository
the repo dirty in a way it should not — the worker never pushes; acceptance remains clean. The worker never pushes; publishing is a separate,
publishing is a separate, explicitly-granted lane. explicitly-granted lane.
""" """
from __future__ import annotations from __future__ import annotations
@ -24,6 +24,7 @@ from rein_aharness.profiles import UnknownToolProfileError, get_profile
from rein_aharness.repository_transaction import ( from rein_aharness.repository_transaction import (
DirtyRepositoryError, DirtyRepositoryError,
GitRepositoryError, GitRepositoryError,
RepositoryAcceptanceError,
RepositoryBusyError, RepositoryBusyError,
RepositoryTransaction, RepositoryTransaction,
RepositoryTransactionError, RepositoryTransactionError,
@ -116,11 +117,11 @@ def run_task(
budget_tokens_override: int | None = None, budget_tokens_override: int | None = None,
transaction: RepositoryTransaction | None = None, transaction: RepositoryTransaction | None = None,
) -> RunResult: ) -> RunResult:
if spec.repository_grant is not None: if spec.repository_grant is not None and not write_metrics:
return _refused_run( return _refused_run(
reason=( reason=(
"refused: repository_grant enforcement is not enabled; " "refused: repository_grant runs require durable external metrics; "
"no adapter was dispatched" "--no-metrics is incompatible"
), ),
model=model, model=model,
) )
@ -255,11 +256,62 @@ def _execute_locked_task(
head_after = _git(spec.target_repo, "rev-parse", "HEAD") head_after = _git(spec.target_repo, "rev-parse", "HEAD")
committed = head_after != head_before committed = head_after != head_before
if session_ok and spec.repository_grant is not None:
try:
tx.validate_acceptance(spec.repository_grant.acceptance_policy())
except RepositoryAcceptanceError as exc:
session_ok = False
reason = str(exc)
ok = session_ok and committed ok = session_ok and committed
if session_ok and not committed: if session_ok and not committed and spec.repository_grant is None:
reason = "session completed without committing" reason = "session completed without committing"
tokens_spent = budget_tracker.spent if budget_tracker is not None else None tokens_spent = budget_tracker.spent if budget_tracker is not None else None
transaction_evidence = tx.evidence()
if spec.repository_grant is not None:
transaction_evidence["repository_grant"] = spec.repository_grant.evidence()
metric_metadata = {
"task_title": spec.title,
"tool_profile": profile.name,
"labels": list(spec.labels),
"completion_event_type": spec.completion_event_type,
}
if spec.repository_grant is not None:
metric_metadata["repository_grant_id"] = spec.repository_grant.grant_id
if write_metrics:
try:
recorder = (
metrics.record_external_execution
if spec.repository_grant is not None
else metrics.record_execution
)
recorder(
spec.target_repo,
spec.agent,
success=ok,
execution_time_s=execution_time_s,
tokens=tokens_spent,
committed=committed,
head_after=head_after,
reason=reason or None,
metadata=metric_metadata,
session_id=tx.transaction_id,
)
if spec.repository_grant is not None:
transaction_evidence["metrics"] = {
"storage": "external",
"session_id": tx.transaction_id,
"projection_ready": True,
}
except OSError as exc:
if spec.repository_grant is not None:
ok = False
reason = (
"required external metrics persistence failed "
f"({type(exc).__name__})"
)
result = RunResult( result = RunResult(
ok=ok, ok=ok,
@ -275,30 +327,9 @@ def _execute_locked_task(
tokens_spent=tokens_spent, tokens_spent=tokens_spent,
execution_time_s=execution_time_s, execution_time_s=execution_time_s,
tool_events=collected_events, tool_events=collected_events,
transaction=tx.evidence(), transaction=transaction_evidence,
) )
if write_metrics:
try:
metrics.record_execution(
spec.target_repo,
spec.agent,
success=ok,
execution_time_s=execution_time_s,
tokens=tokens_spent,
committed=committed,
head_after=head_after,
reason=reason or None,
metadata={
"task_title": spec.title,
"tool_profile": profile.name,
"labels": list(spec.labels),
"completion_event_type": spec.completion_event_type,
},
)
except OSError:
pass # metrics must not block run completion reporting
if report_to_hub: if report_to_hub:
detail = { detail = {
"repo": spec.target_repo.name, "repo": spec.target_repo.name,
@ -314,6 +345,7 @@ def _execute_locked_task(
"budget_tokens": budget_tokens, "budget_tokens": budget_tokens,
"tokens_spent": tokens_spent, "tokens_spent": tokens_spent,
"execution_time_s": round(execution_time_s, 3), "execution_time_s": round(execution_time_s, 3),
"repository_transaction": transaction_evidence,
} }
hub.post_progress_event( hub.post_progress_event(
summary=f"executor run: {spec.title} ({'ok' if ok else 'failed'})", summary=f"executor run: {spec.title} ({'ok' if ok else 'failed'})",

12
tests/conftest.py Normal file
View file

@ -0,0 +1,12 @@
from __future__ import annotations
from pathlib import Path
import pytest
@pytest.fixture(autouse=True)
def isolated_runtime_state(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Keep repository locks and close evidence out of operator-owned state."""
monkeypatch.setenv("REIN_AHARNESS_STATE_DIR", str(tmp_path / "runtime-state"))
monkeypatch.setenv("AGENT_HARNESS_LEGACY_APPROACHES_UNTIL", "2099-12-31")

View file

@ -2,6 +2,8 @@
from __future__ import annotations from __future__ import annotations
from datetime import date
from rein_aharness.approaches import ( from rein_aharness.approaches import (
APPROACH_AGENT_SESSION, APPROACH_AGENT_SESSION,
APPROACH_BRIEF_DAILY, APPROACH_BRIEF_DAILY,
@ -9,6 +11,8 @@ from rein_aharness.approaches import (
APPROACH_FI_RESEARCH_BRIEF, APPROACH_FI_RESEARCH_BRIEF,
APPROACH_MAIL_PIPELINE, APPROACH_MAIL_PIPELINE,
APPROACH_UNMATCHED, APPROACH_UNMATCHED,
execute_approach,
legacy_approaches_enabled,
select_approach, select_approach,
) )
from rein_aharness.ops_run_client import OpsRun from rein_aharness.ops_run_client import OpsRun
@ -113,3 +117,26 @@ def test_fi_before_generic_automated() -> None:
assert ( assert (
select_approach(_run(labels=["research-brief"])) == APPROACH_FI_RESEARCH_BRIEF select_approach(_run(labels=["research-brief"])) == APPROACH_FI_RESEARCH_BRIEF
) )
def test_legacy_approaches_require_non_expired_explicit_date() -> None:
today = date(2026, 9, 4)
assert legacy_approaches_enabled("2026-09-04", today=today) is True
assert legacy_approaches_enabled("2026-09-03", today=today) is False
assert legacy_approaches_enabled("true", today=today) is False
assert legacy_approaches_enabled("", today=today) is False
def test_execute_approach_refuses_profile_absent_route_without_flag(
monkeypatch,
) -> None:
monkeypatch.delenv("AGENT_HARNESS_LEGACY_APPROACHES_UNTIL")
result = execute_approach(
_run(labels=["research-brief"], target_repo="not-resolved"),
report_to_hub=False,
)
assert result.ok is False
assert result.reopen is False
assert "compatibility routing is disabled" in result.reason

View file

@ -2,8 +2,10 @@
from __future__ import annotations from __future__ import annotations
from unittest.mock import MagicMock, patch import subprocess
import time import time
from pathlib import Path
from unittest.mock import MagicMock, patch
from rein_aharness.approaches import ApproachResult, APPROACH_FI_RESEARCH_BRIEF from rein_aharness.approaches import ApproachResult, APPROACH_FI_RESEARCH_BRIEF
from rein_aharness.claim_loop import ( from rein_aharness.claim_loop import (
@ -20,6 +22,7 @@ from rein_aharness.ops_run_client import (
OpsRunConfig, OpsRunConfig,
OpsRunError, OpsRunError,
) )
from rein_aharness.repository_transaction import RepositoryTransaction
def _claimed_run() -> OpsRun: def _claimed_run() -> OpsRun:
@ -37,6 +40,41 @@ def _claimed_run() -> OpsRun:
) )
def _profiled_case(
tmp_path: Path,
) -> tuple[Path, OpsRun, MagicMock]:
repo = tmp_path / "freedom-intelligence"
repo.mkdir()
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
(repo / "README.md").write_text("controlled target\n", encoding="utf-8")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
[
"git",
"-c",
"user.email=test@example.invalid",
"-c",
"user.name=test",
"commit",
"-qm",
"baseline",
],
cwd=repo,
check=True,
)
run = _claimed_run()
run.harness_profile_ref = "harness.agent-dev-local@1.0.0"
client = MagicMock(spec=ActivityCoreOpsClient)
client.config = OpsRunConfig(
worker_id="w",
lease_seconds=90,
repo_roots=(str(tmp_path),),
)
client.claim.return_value = [run]
return repo, run, client
def test_process_one_empty() -> None: def test_process_one_empty() -> None:
client = MagicMock(spec=ActivityCoreOpsClient) client = MagicMock(spec=ActivityCoreOpsClient)
client.config = OpsRunConfig(worker_id="w", lease_seconds=90) client.config = OpsRunConfig(worker_id="w", lease_seconds=90)
@ -231,12 +269,8 @@ def test_process_one_records_adapter_exception_without_unbound_result() -> None:
client.fail.assert_not_called() client.fail.assert_not_called()
def test_profiled_exception_after_lease_loss_skips_close() -> None: def test_profiled_exception_after_lease_loss_skips_close(tmp_path: Path) -> None:
client = MagicMock(spec=ActivityCoreOpsClient) repo, _run, client = _profiled_case(tmp_path)
client.config = OpsRunConfig(worker_id="w", lease_seconds=90)
run = _claimed_run()
run.harness_profile_ref = "harness.agent-dev-local@1.0.0"
client.claim.return_value = [run]
client.heartbeat.side_effect = OpsRunError("lease rejected", status_code=409) client.heartbeat.side_effect = OpsRunError("lease rejected", status_code=409)
def slow_profile(*_args, **_kwargs): def slow_profile(*_args, **_kwargs):
@ -253,6 +287,8 @@ def test_profiled_exception_after_lease_loss_skips_close() -> None:
assert result.reason.startswith("lease lost") assert result.reason.startswith("lease lost")
client.complete.assert_not_called() client.complete.assert_not_called()
client.fail.assert_not_called() client.fail.assert_not_called()
with RepositoryTransaction(repo) as retry:
assert retry.locked is True
def test_poll_peek() -> None: def test_poll_peek() -> None:
@ -263,13 +299,11 @@ def test_poll_peek() -> None:
assert rows[0]["approach"] == APPROACH_FI_RESEARCH_BRIEF assert rows[0]["approach"] == APPROACH_FI_RESEARCH_BRIEF
def test_profiled_run_uses_glas_and_completes_with_full_result() -> None: def test_profiled_run_uses_glas_and_completes_with_full_result(
client = MagicMock(spec=ActivityCoreOpsClient) tmp_path: Path,
client.config = OpsRunConfig(worker_id="w", lease_seconds=90) ) -> None:
run = _claimed_run() _repo, run, client = _profiled_case(tmp_path)
run.harness_profile_ref = "harness.agent-dev-local@1.0.0"
run.approach_hint = "fi-research-brief" run.approach_hint = "fi-research-brief"
client.claim.return_value = [run]
client.complete.return_value = OpsRun( client.complete.return_value = OpsRun(
id=run.id, id=run.id,
activity_definition_id="def", activity_definition_id="def",
@ -313,13 +347,12 @@ def test_profiled_run_uses_glas_and_completes_with_full_result() -> None:
execute.assert_not_called() execute.assert_not_called()
def test_profile_refusal_fails_terminally_without_legacy_fallback() -> None: def test_profile_refusal_fails_terminally_without_legacy_fallback(
client = MagicMock(spec=ActivityCoreOpsClient) tmp_path: Path,
client.config = OpsRunConfig(worker_id="w", lease_seconds=90) ) -> None:
run = _claimed_run() _repo, run, client = _profiled_case(tmp_path)
run.harness_profile_ref = "harness.unknown@9.9.9" run.harness_profile_ref = "harness.unknown@9.9.9"
run.approach_hint = "fi-research-brief" run.approach_hint = "fi-research-brief"
client.claim.return_value = [run]
client.fail.return_value = OpsRun( client.fail.return_value = OpsRun(
id=run.id, id=run.id,
activity_definition_id="def", activity_definition_id="def",
@ -346,6 +379,49 @@ def test_profile_refusal_fails_terminally_without_legacy_fallback() -> None:
execute.assert_not_called() execute.assert_not_called()
def test_profiled_signal_cancellation_releases_repository_lock(
tmp_path: Path,
) -> None:
repo, _run, client = _profiled_case(tmp_path)
def cancel_during_gateway(*_args, **_kwargs):
_cancel_active_run("signal")
return {"ok": True, "evidence": {"outcome": "late-success"}}
with patch(
"rein_aharness.claim_loop.execute_profiled_run",
side_effect=cancel_during_gateway,
):
result = process_one(client)
assert result.ok is False
assert result.reason == "execution cancelled (signal)"
client.complete.assert_not_called()
client.fail.assert_not_called()
with RepositoryTransaction(repo) as retry:
assert retry.locked is True
def test_profiled_close_failure_happens_after_repository_lock_release(
tmp_path: Path,
) -> None:
repo, _run, client = _profiled_case(tmp_path)
client.complete.side_effect = OpsRunError("complete transport failed")
gateway_result = {"ok": True, "evidence": {"outcome": "succeeded"}}
with patch(
"rein_aharness.claim_loop.execute_profiled_run",
return_value=gateway_result,
):
result = process_one(client)
assert result.ok is False
assert result.reason.startswith("close ops_run failed:")
assert "repository_transaction" in result.detail
with RepositoryTransaction(repo) as retry:
assert retry.locked is True
def test_poll_peek_reports_authoritative_profile_route() -> None: def test_poll_peek_reports_authoritative_profile_route() -> None:
client = MagicMock(spec=ActivityCoreOpsClient) client = MagicMock(spec=ActivityCoreOpsClient)
run = _claimed_run() run = _claimed_run()

View file

@ -0,0 +1,67 @@
from __future__ import annotations
import json
import subprocess
from datetime import date
from pathlib import Path
from rein_aharness import fi_research_brief
def _repo(tmp_path: Path) -> Path:
repo = tmp_path / "freedom-intelligence"
repo.mkdir()
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
(repo / "README.md").write_text("baseline\n", encoding="utf-8")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
[
"git",
"-c",
"user.email=test@example.invalid",
"-c",
"user.name=test",
"commit",
"-qm",
"baseline",
],
cwd=repo,
check=True,
)
return repo
def test_fi_brief_never_pushes_even_when_legacy_env_requests_it(
tmp_path: Path,
monkeypatch,
) -> None:
repo = _repo(tmp_path)
monkeypatch.setenv("FI_RESEARCH_BRIEF_PUSH", "1")
original_git = fi_research_brief._git
calls: list[tuple[str, ...]] = []
def observed_git(target: Path, *args: str) -> str:
calls.append(args)
return original_git(target, *args)
monkeypatch.setattr(fi_research_brief, "_git", observed_git)
result = fi_research_brief.run_fi_research_brief(
repo,
day=date(2026, 9, 4),
report_to_hub=False,
complete_fn=lambda _prompt: json.dumps(
{
"headline_deltas": ["No material delta after allowlist review."],
"axis_a": [],
"axis_b": [],
"axis_c": [],
"axis_d": [],
"collection_candidates": [],
"lab_implications": ["Recheck tomorrow."],
}
),
)
assert result.ok is True
assert result.committed is True
assert not any(args and args[0] == "push" for args in calls)

View file

@ -3,7 +3,14 @@ from __future__ import annotations
import json import json
from pathlib import Path from pathlib import Path
from rein_aharness.metrics import record_execution, regenerate_summary import pytest
from rein_aharness.metrics import (
external_metrics_dir,
record_execution,
record_external_execution,
regenerate_summary,
)
def test_record_execution_writes_jsonl_and_summary(tmp_path: Path) -> None: def test_record_execution_writes_jsonl_and_summary(tmp_path: Path) -> None:
@ -49,3 +56,89 @@ def test_summary_aggregates_multiple(tmp_path: Path) -> None:
def test_regenerate_summary_empty() -> None: def test_regenerate_summary_empty() -> None:
assert regenerate_summary("x", [])["execution_count"] == 0 assert regenerate_summary("x", [])["execution_count"] == 0
def test_external_metrics_are_durable_and_projection_ready(tmp_path: Path) -> None:
repo = tmp_path / "repo"
repo.mkdir()
state_dir = tmp_path / "state"
path = record_external_execution(
repo,
"coach",
success=True,
committed=True,
head_after="abc123",
metadata={"repository_grant_id": "grant-1"},
session_id="transaction-1",
state_dir=state_dir,
)
directory = external_metrics_dir(repo, "coach", state_dir=state_dir)
assert path == directory / "executions.jsonl"
assert path.stat().st_mode & 0o777 == 0o600
assert directory.stat().st_mode & 0o777 == 0o700
record = json.loads(path.read_text(encoding="utf-8").strip())
assert record["metadata"]["repository_grant_id"] == "grant-1"
assert record["session_id"] == "transaction-1"
projection = json.loads(
(directory / "projection.json").read_text(encoding="utf-8")
)
assert projection["repository_name"] == "repo"
assert projection["target_relative_directory"] == ".kaizen/metrics/coach"
assert not (repo / ".kaizen").exists()
def test_external_metrics_deduplicate_transaction_replay(tmp_path: Path) -> None:
repo = tmp_path / "repo"
repo.mkdir()
state_dir = tmp_path / "state"
for _attempt in range(2):
path = record_external_execution(
repo,
"coach",
success=True,
session_id="transaction-1",
state_dir=state_dir,
)
assert len(path.read_text(encoding="utf-8").splitlines()) == 1
summary = json.loads((path.parent / "summary.json").read_text(encoding="utf-8"))
assert summary["execution_count"] == 1
def test_external_metrics_refuse_an_incomplete_ledger(tmp_path: Path) -> None:
repo = tmp_path / "repo"
repo.mkdir()
state_dir = tmp_path / "state"
directory = external_metrics_dir(repo, "coach", state_dir=state_dir)
directory.mkdir(parents=True)
ledger = directory / "executions.jsonl"
ledger.write_text('{"incomplete":true}', encoding="utf-8")
with pytest.raises(OSError, match="incomplete record"):
record_external_execution(
repo,
"coach",
success=True,
state_dir=state_dir,
)
assert ledger.read_text(encoding="utf-8") == '{"incomplete":true}'
def test_external_metrics_refuse_unsafe_projection_agent(tmp_path: Path) -> None:
repo = tmp_path / "repo"
repo.mkdir()
state_dir = tmp_path / "state"
with pytest.raises(OSError, match="safe metrics projection"):
record_external_execution(
repo,
"../escape",
success=True,
state_dir=state_dir,
)
assert not state_dir.exists()

View file

@ -6,6 +6,7 @@ from pathlib import Path
import pytest import pytest
from rein_aharness.metrics import external_metrics_dir
from rein_aharness.repository_grant import RepositoryGrant, RepositoryGrantError from rein_aharness.repository_grant import RepositoryGrant, RepositoryGrantError
from rein_aharness.repository_transaction import RepositoryTransaction from rein_aharness.repository_transaction import RepositoryTransaction
from rein_aharness.runner import run_task from rein_aharness.runner import run_task
@ -242,18 +243,129 @@ def test_taskspec_file_wraps_invalid_repository_grant(tmp_path: Path) -> None:
TaskSpec.from_file(task_file) TaskSpec.from_file(task_file)
def test_runner_refuses_grant_before_adapter_dispatch_or_mutation(tmp_path: Path) -> None: def test_runner_accepts_granted_commit_and_keeps_checkout_clean(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
class Adapter:
def execute_prompt(self, prompt: str, config: object):
docs = repo / "docs"
docs.mkdir()
(docs / "result.md").write_text("accepted\n", encoding="utf-8")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
[
"git",
"-c",
"user.email=test@example.invalid",
"-c",
"user.name=test",
"commit",
"-qm",
"accepted result",
],
cwd=repo,
check=True,
)
from llm_connect.models import LLMResponse
return LLMResponse(
content="done",
model="fake",
usage={},
finish_reason="stop",
)
result = run_task(
TaskSpec(
title="bounded change",
description="update docs",
target_repo=repo,
repository_grant=RepositoryGrant.from_mapping(_grant()),
),
adapter=Adapter(),
report_to_hub=False,
write_metrics=True,
)
assert result.ok is True
assert result.committed is True
assert result.transaction is not None
assert result.transaction["acceptance"]["accepted"] is True
assert result.transaction["acceptance"]["changed_paths"] == ["docs/result.md"]
assert result.transaction["repository_grant"]["grant_id"]
assert result.transaction["metrics"] == {
"storage": "external",
"session_id": result.transaction["transaction_id"],
"projection_ready": True,
}
assert _git(repo, "status", "--porcelain=v2") == ""
assert not (repo / ".kaizen" / "metrics").exists()
metric_path = external_metrics_dir(repo, "coach") / "executions.jsonl"
metric_record = json.loads(metric_path.read_text(encoding="utf-8").strip())
assert metric_record["session_id"] == result.transaction["transaction_id"]
def test_runner_rejects_commit_outside_repository_grant(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
class Adapter:
def execute_prompt(self, prompt: str, config: object):
(repo / "UNRELATED.md").write_text("not granted\n", encoding="utf-8")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
[
"git",
"-c",
"user.email=test@example.invalid",
"-c",
"user.name=test",
"commit",
"-qm",
"ungranted result",
],
cwd=repo,
check=True,
)
from llm_connect.models import LLMResponse
return LLMResponse(
content="done",
model="fake",
usage={},
finish_reason="stop",
)
result = run_task(
TaskSpec(
title="bounded change",
description="update docs",
target_repo=repo,
repository_grant=RepositoryGrant.from_mapping(_grant()),
),
adapter=Adapter(),
report_to_hub=False,
write_metrics=True,
)
assert result.ok is False
assert result.committed is True
assert result.reason.startswith("repository acceptance failed: path-not-granted")
assert result.transaction is not None
assert "acceptance" not in result.transaction
def test_runner_refuses_grant_when_durable_metrics_are_disabled(
tmp_path: Path,
) -> None:
repo = _make_repo(tmp_path) repo = _make_repo(tmp_path)
called = False called = False
class Adapter: class Adapter:
def execute_prompt(self, prompt: str, config: object) -> None: def execute_prompt(self, prompt: str, config: object):
nonlocal called nonlocal called
called = True called = True
raise AssertionError("adapter must not be dispatched") raise AssertionError("adapter must not be dispatched")
head_before = _git(repo, "rev-parse", "HEAD")
status_before = _git(repo, "status", "--porcelain=v2")
result = run_task( result = run_task(
TaskSpec( TaskSpec(
title="bounded change", title="bounded change",
@ -267,11 +379,69 @@ def test_runner_refuses_grant_before_adapter_dispatch_or_mutation(tmp_path: Path
) )
assert result.ok is False assert result.ok is False
assert result.committed is False assert "require durable external metrics" in result.reason
assert result.reason.startswith("refused: repository_grant enforcement")
assert called is False assert called is False
assert _git(repo, "rev-parse", "HEAD") == head_before
assert _git(repo, "status", "--porcelain=v2") == status_before
def test_runner_fails_granted_result_when_external_metrics_cannot_persist(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
repo = _make_repo(tmp_path)
class Adapter:
def execute_prompt(self, prompt: str, config: object):
docs = repo / "docs"
docs.mkdir()
(docs / "result.md").write_text("accepted\n", encoding="utf-8")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
[
"git",
"-c",
"user.email=test@example.invalid",
"-c",
"user.name=test",
"commit",
"-qm",
"accepted result",
],
cwd=repo,
check=True,
)
from llm_connect.models import LLMResponse
return LLMResponse(
content="done",
model="fake",
usage={},
finish_reason="stop",
)
def fail_metrics(*args: object, **kwargs: object) -> None:
raise OSError("state volume unavailable")
monkeypatch.setattr(
"rein_aharness.runner.metrics.record_external_execution",
fail_metrics,
)
result = run_task(
TaskSpec(
title="bounded change",
description="update docs",
target_repo=repo,
repository_grant=RepositoryGrant.from_mapping(_grant()),
),
adapter=Adapter(),
report_to_hub=False,
write_metrics=True,
)
assert result.ok is False
assert result.reason == "required external metrics persistence failed (OSError)"
assert result.transaction is not None
assert result.transaction["acceptance"]["accepted"] is True
assert _git(repo, "status", "--porcelain=v2") == ""
def _git(repo: Path, *args: str) -> str: def _git(repo: Path, *args: str) -> str:

View file

@ -78,9 +78,15 @@ def test_transaction_captures_clean_branch_and_remote_refs(tmp_path: Path) -> No
def test_dirty_baseline_is_refused_without_changing_user_files(tmp_path: Path) -> None: def test_dirty_baseline_is_refused_without_changing_user_files(tmp_path: Path) -> None:
repo = _make_repo(tmp_path) repo = _make_repo(tmp_path)
subprocess.run(
["git", "update-ref", "refs/remotes/origin/main", "HEAD"],
cwd=repo,
check=True,
)
changed = repo / "README.md" changed = repo / "README.md"
changed.write_text("operator change\n", encoding="utf-8") changed.write_text("operator change\n", encoding="utf-8")
before = _status(repo) before = _status(repo)
remote_refs_before = _remote_refs(repo)
with pytest.raises(DirtyRepositoryError) as excinfo: with pytest.raises(DirtyRepositoryError) as excinfo:
with RepositoryTransaction(repo, state_dir=tmp_path / "state"): with RepositoryTransaction(repo, state_dir=tmp_path / "state"):
@ -90,6 +96,7 @@ def test_dirty_baseline_is_refused_without_changing_user_files(tmp_path: Path) -
assert excinfo.value.baseline.dirty_entries == 1 assert excinfo.value.baseline.dirty_entries == 1
assert changed.read_text(encoding="utf-8") == "operator change\n" assert changed.read_text(encoding="utf-8") == "operator change\n"
assert _status(repo) == before assert _status(repo) == before
assert _remote_refs(repo) == remote_refs_before
# Refusal released the lock; inspection can explicitly opt into dirty state. # Refusal released the lock; inspection can explicitly opt into dirty state.
with RepositoryTransaction( with RepositoryTransaction(
@ -469,3 +476,18 @@ def _status(repo: Path) -> str:
capture_output=True, capture_output=True,
text=True, text=True,
).stdout ).stdout
def _remote_refs(repo: Path) -> str:
return subprocess.run(
[
"git",
"for-each-ref",
"--format=%(refname) %(objectname)",
"refs/remotes",
],
cwd=repo,
check=True,
capture_output=True,
text=True,
).stdout

View file

@ -8,6 +8,7 @@ import pytest
import yaml import yaml
from rein_aharness.manifest import HARNESS_MAJOR from rein_aharness.manifest import HARNESS_MAJOR
from rein_aharness.repository_transaction import RepositoryTransaction
from rein_aharness.runner import RunResult, run_task from rein_aharness.runner import RunResult, run_task
from rein_aharness.taskspec import TaskSpec, TaskSpecError from rein_aharness.taskspec import TaskSpec, TaskSpecError
@ -148,6 +149,26 @@ def test_run_task_records_bounded_cancellation_without_session_output(tmp_path)
assert result.reason == "execution cancelled (lease-loss)" assert result.reason == "execution cancelled (lease-loss)"
def test_run_task_timeout_releases_repository_lock(tmp_path) -> None:
repo = _make_repo(tmp_path)
class TimingOutAdapter:
def execute_prompt(self, prompt, config):
raise TimeoutError("adapter deadline elapsed")
result = run_task(
_spec(repo),
adapter=TimingOutAdapter(),
report_to_hub=False,
write_metrics=False,
)
assert result.ok is False
assert result.reason == "session failed: adapter deadline elapsed"
with RepositoryTransaction(repo) as retry:
assert retry.locked is True
def test_run_task_refuses_unknown_tool_profile(tmp_path) -> None: def test_run_task_refuses_unknown_tool_profile(tmp_path) -> None:
repo = _make_repo(tmp_path) repo = _make_repo(tmp_path)
_write_manifest( _write_manifest(

View file

@ -158,7 +158,7 @@ and T02–T06 can cite stable decisions rather than infer ownership from code.
```task ```task
id: REINAH-WP-0003-T02 id: REINAH-WP-0003-T02
status: progress status: done
priority: high priority: high
state_hub_task_id: "31da4226-27c0-5e3c-baeb-de01e07cebb1" state_hub_task_id: "31da4226-27c0-5e3c-baeb-de01e07cebb1"
``` ```
@ -258,6 +258,22 @@ affected profiled-result and clean-baseline test setup. Full verification passes
`209 passed, 1 skipped`. T02 remains `progress` until the named timeout, signal, `209 passed, 1 skipped`. T02 remains `progress` until the named timeout, signal,
and result-close lock-release cases are covered directly. and result-close lock-release cases are covered directly.
### Completion — 2026-09-04
Added direct integration proof that the canonical repository lock is released
after an adapter timeout, Activity Core lease loss, process-signal cancellation,
and a terminal result-close transport failure. The close-failure test also
confirms transaction evidence survives in the bounded process result. Refusal
coverage now proves both the checkout status and local remote-tracking refs are
unchanged. Profiled tests use disposable repositories, and an autouse fixture
keeps runtime state outside operator-owned paths. The documented full-suite
command passes without an environment override: `212 passed, 1 skipped`.
Together with the existing cross-process contention, distinct-repository,
dirty/staged/untracked, detached/moved-HEAD, metadata, remote-ref, bounded
evidence, and lease-cancellation cases, all T02 exit criteria are met. T02 is
`done`; T03 is unblocked.
## Verify accepted commits and reconcile metrics/reporting ## Verify accepted commits and reconcile metrics/reporting
```task ```task
@ -371,11 +387,41 @@ terminal evidence, and expired-lease behavior in message
`1c13bfb1-9015-4b8a-8b30-ee748c1d8dc5`. No claim-loop or deployment change `1c13bfb1-9015-4b8a-8b30-ee748c1d8dc5`. No claim-loop or deployment change
was requested. was requested.
### Local grant activation and metrics atomicity — 2026-09-04
Enabled the reviewed v1 `repository_grant` for local `TaskSpec` files now that
T02 supplies the lease-bound transaction. Successful granted runs validate the
exact commit count/ancestry, changed paths, clean post-state, protected Git
metadata, and remote-tracking refs before reporting success. Their bounded
transaction evidence includes grant and acceptance identities without raw
grant paths, prompts, or provider output. Adversarial runner coverage rejects
out-of-grant commits.
Chose the durable-external metrics branch of ADR-002's T03 decision. Granted
runs cannot use `--no-metrics`; they atomically append a kaizen-compatible
record under private `REIN_AHARNESS_STATE_DIR` storage and regenerate a summary
plus projection descriptor without touching the accepted checkout. Ledger
corruption or persistence failure is terminal for the granted result. A future
kaizen-owned, separately granted projection may commit those files to the
instance; the rein does not invalidate its own accepted post-state. Legacy
grant-absent runs retain repository-local metrics during migration.
Full verification passes: `222 passed, 1 skipped`. T03 remains `wait`, not
`done`, on two verified Activity Core contract gaps at current main `b72fdb5`:
the queue schema carries no authoritative repository grant, and complete/fail
accept only an actively leased claimed row while normalized result storage
drops transaction/grant/acceptance identity. A response-lost retry therefore
returns an ambiguous 409 and cannot be safely treated as delivered. Requested
a separately typed grant field plus identity-preserving, conflict-detecting
terminal reconciliation in State Hub message
`6c4189a5-5971-4eb9-a7bd-62fe368f9a02`. Live outbox activation stays
fail-closed until that exact upstream contract is available.
## Remove tenant logic from the shared runtime ## Remove tenant logic from the shared runtime
```task ```task
id: REINAH-WP-0003-T04 id: REINAH-WP-0003-T04
status: todo status: wait
priority: high priority: high
state_hub_task_id: "f6c7f80c-3807-54ac-b44a-ef582433b927" state_hub_task_id: "f6c7f80c-3807-54ac-b44a-ef582433b927"
``` ```
@ -407,11 +453,36 @@ and owner files. It identifies inputs, outputs, credentials, completion events,
commit/push behavior, retry posture, rollback material, and migration gates. commit/push behavior, retry posture, rollback material, and migration gates.
This is read-only preparation; T04 remains `wait` until T01 accepts ADR-002. This is read-only preparation; T04 remains `wait` until T01 accepts ADR-002.
### Dated compatibility guard — 2026-09-04
Profile-absent `ops_run` dispatch now requires an explicit, non-expired ISO
date in `AGENT_HARNESS_LEGACY_APPROACHES_UNTIL`. Missing, malformed, or expired
values refuse terminally before target resolution or tenant executor dispatch.
Authoritative `harness_profile_ref` rows bypass the legacy registry and this
flag entirely. The deployment example currently declares `2026-12-31`, making
the remaining four tenant migrations visible and time-bounded rather than a
silent permanent default. Focused tests cover boundary-day, expired, malformed,
missing, and pre-dispatch refusal behavior.
Removed the FI compatibility executor's `git push origin HEAD` path entirely.
`FI_RESEARCH_BRIEF_PUSH` no longer widens behavior even when set; the executor
creates a local commit only. A regression test observes every Git invocation
and proves no push occurs. Any publication now requires a separate owner,
grant, and evidence contract.
T04 remains `wait`: the FI and Binky definition owners still need to move
their four scheduled behaviors to approved profiles/tenant-owned capabilities
before the compatibility flag and tenant modules can be removed. Requests:
Freedom Intelligence `d0b45acb-0002-405b-9620-cc44568170fb`, Binky
`c941f47b-d100-4df3-b4f4-feb1e187d2d8`, kaizen-agentic
`29a010f4-7a5c-42cd-a13e-3a13326f75ff`, and Glas
`e91f3f67-2d5d-4ad2-923c-431ebafed77e`.
## Align deployment, recovery, and conformance with the supported runtime ## Align deployment, recovery, and conformance with the supported runtime
```task ```task
id: REINAH-WP-0003-T05 id: REINAH-WP-0003-T05
status: todo status: progress
priority: high priority: high
state_hub_task_id: "c4a3f08f-2874-5672-9f29-aecddd697d90" state_hub_task_id: "c4a3f08f-2874-5672-9f29-aecddd697d90"
``` ```
@ -441,6 +512,20 @@ runtime boundary is unavailable; recovery drills leave no held repo lock,
claimed row, orphan sandbox, or lost required evidence; and CI/release gates claimed row, orphan sandbox, or lost required evidence; and CI/release gates
exercise the same contract versions as production. exercise the same contract versions as production.
### Supported topology declaration — 2026-09-04
Selected the existing railiance01 user systemd service as the authoritative
production topology. It is the only runtime that owns real checkouts, private
state, worker credentials, heartbeats, and signal shutdown. The Kubernetes
Deployment is now explicitly labeled `packaging-smoke`; its source comments and
deployment guide state that `sleep infinity` is neither ready to claim nor a
failover worker and must never receive the Activity Core worker credential.
T05 remains `progress`. Reproducible pinned sibling packages, startup readiness,
recovery/outbox controls, and non-skipped cross-package CI still need
implementation; the currently catalogued local Glas profiles are also
operationally `blocked` on `GLAS-IN-0002`.
## Re-prove one governed profiled run and close residuals ## Re-prove one governed profiled run and close residuals
```task ```task