feat: reconcile granted ops run closes

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a06bfe-2a55-7ed3-bacd-879977b099bf
This commit is contained in:
tegwick 2026-09-04 12:57:12 +02:00
parent b72fdb5452
commit b63131e863
21 changed files with 1240 additions and 43 deletions

View file

@ -29,7 +29,9 @@ workplan task file. Not an issue-core or Forgejo ticket.
| `approach_hint` | text nullable | **Legacy** definition-matching hint (ACT-ADR-006) |
| `harness_profile_ref` | text nullable | Authoritative execution selector, pinned `<id>@<version>` |
| `execution_refs` | jsonb | Attribution refs carried through, not authored here |
| `repository_grant` | jsonb nullable | Separately typed repository mutation authority; never inferred or stored in `execution_refs` |
| `result` | JSONB | Completion metadata |
| `close_intent_digest` | text nullable | Digest of the accepted normalized terminal close intent; absent for legacy terminal and reopened rows |
| `created_at` / `updated_at` | timestamptz | |
## API (actcore-api)
@ -62,6 +64,19 @@ workplan task file. Not an issue-core or Forgejo ticket.
- Stale claims (`state=claimed` and `lease_until <= now()`) are reopened before select.
- Heartbeat, complete, and fail lock the row and require an active lease
(`lease_until > now()`). An expired worker cannot revive or close its claim.
- A first terminal close returns `close_disposition: applied`. If its response
is lost, an exact normalized repeat by the same claim owner returns HTTP 200
with `close_disposition: reconciled`; it does not require the cleared lease
and does not mutate the row again.
- Refusals are machine-distinct. A missing row is HTTP 404 `not_found`;
wrong-owner, expired-lease, non-claimed-state, mismatched repository evidence,
and different terminal intents are HTTP 409 with codes `wrong_owner`,
`expired_lease`, `state_conflict`, `evidence_conflict`, and
`terminal_conflict` respectively. A pre-migration terminal row has no close
digest and therefore fails closed as `terminal_conflict`.
- A failure accepted with `reopen: true` below the attempt ceiling is not
terminal: it clears owner/lease/close identity and returns to `open`. A repeat
cannot be treated as terminal reconciliation.
### Complete / fail body
@ -112,6 +127,16 @@ prompts, messages, provider responses, credential fields, unknown nested blobs,
and undeclared refs are never stored. Read projections normalize historic rows
again before returning them.
Repository transaction evidence is independently allowlisted under
`result.repository_transaction`: bounded transaction/correlation identifiers,
baseline digests, repository grant and acceptance-policy identifiers, accepted
commit/path evidence, and external-metrics identity. Raw grant patterns,
provider/tool payloads, and unknown fields are dropped. If an `ops_run` carries
a repository grant, completion additionally requires the result's grant id,
acceptance-policy id, and positive acceptance evidence to match the queued
grant. Failure may omit transaction evidence when setup never began; supplied
grant evidence must match.
## Emit path
On `emit_tasks` (when `OPS_RUN_QUEUE_ENABLED` is truthy, **default true**):
@ -210,7 +235,8 @@ resolver rather than at emission. That residual gap is accepted and recorded in
ACT-ADR-006; closing it needs a scoped glas-harness API, not a local catalogue.
Task-emitting rules declare the selector and optional attribution refs on the
action; instructions use the same fields at instruction level:
action; instructions use the same fields at instruction level. Repository
authority is a separate, static declaration and is never template-rendered:
```yaml
action:
@ -220,6 +246,11 @@ action:
execution_refs:
correlation_id: context.request.correlation_id
goal_refs: [context.request.goal_ref]
repository_grant:
version: "1"
allowed_paths: [docs/, README.md]
commit_count: {min: 1, max: 1}
publish: false
```
File sync validates every declared profile structurally and rejects malformed
@ -228,6 +259,14 @@ batch before opening the database or IssueSink. A profile policy failure is a
non-retryable activity error; no earlier item in that batch is emitted. Only the
allowlisted attribution keys are carried to the queue.
Repository-grant v1 requires exactly `version`, `allowed_paths`,
`commit_count`, and `publish`. It accepts 1100 unique, repository-relative
POSIX path patterns, commit bounds `1 <= min <= max <= 32`, and only
`publish: false`; absolute/traversal/backslash/`.git` grants, ambiguous scalar
types, unknown fields, and publication are rejected at definition sync and
again across the full emission batch. The admitted mapping is copied unchanged
to the queue claim/read response and never merged into `execution_refs`.
`ACTIVITY_CORE_REQUIRE_HARNESS_PROFILE=true` makes a missing profile ref an
error both during file sync and emission. Deterministic report-only
instructions are exempt because they create no execution request. The flag