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:
parent
e3c6124e22
commit
20e6f381f6
28 changed files with 1068 additions and 154 deletions
|
|
@ -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`
|
||||||
|
|
|
||||||
34
SCOPE.md
34
SCOPE.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
36
docs/external-metrics.md
Normal 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.
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
12
tests/conftest.py
Normal 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")
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
67
tests/test_fi_research_brief.py
Normal file
67
tests/test_fi_research_brief.py
Normal 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)
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue