Loop Agent Finalize Levels Design¶
Status: Approved (design session 2026-08-13)
Date: 2026-08-13
Related: Loop Write Target and Delivery, Loop Caller Workflows, Loop Caller Reusable
Problem¶
ci-loop-agent treated running the last platform job and opening a PR / git landing as the same flag (finalize_enabled). Consequences:
- L1 (
agent-l1) never entered finalize; a side jobrecord-l1appended the run-log. - L2/L3 with
delivery: nonealso skipped finalize, so successful Agent runs left no run-log. - Callers looked like they "turned finalize off" for L1, which contradicted the four-plane rule: delivery is a finalize concern, and run-log is platform persistence, not an Agent skill.
record-l1 is the wrong name and the wrong place in the graph. The last hop after every Agent execute is finalize.
Goals¶
- Every non-cancelled Agent execute is followed by a finalize-* job (L1 →
finalize-l1, L2/L3 →finalize-l2). - Run-log append always happens in that finalize hop (when
loop_nameis set). - Git landing / PR (
loop-finalize) is an inner step offinalize-l2, gated byfinalize_enabled(callers continue to set this fromdelivery == open_pr). - No L1-only job on
ci-loop-caller*.yaml. Detect-time skips stay on callerrecord-skip. - This repository's callers dogfood
./.github/workflows/ci-loop-agent.yamlso graph changes take effect without a tag pin.
Non-Goals¶
finalize-l3as a third job (L3 isagent-l2+auto_mergeonfinalize-l2).- Implementing L1
delivery: issue | notionconnectors in this change (the job exists so those steps attach tofinalize-l1later). - Capturing L1 token
usage_json(loop-agent-oncestill does not export it). - Moving
record-skipintoci-loop-agent(no Agent job ran).
Job graph¶
ci-loop-caller*.yaml
detect
├─ should_run=false + (budget|circuit_breaker) → record-skip → loop-run-log
└─ should_run=true → execute (matrix) → ci-loop-agent.yaml
level L1: agent-l1 → finalize-l1
level L2|L3: agent-l2 → finalize-l2
| Job | Runs when | Always does | Conditionally does |
|---|---|---|---|
finalize-l1 |
level==L1, agent-l1 not skipped/cancelled |
checkout, token, loop-run-log |
future: L1 delivery (issue / log) |
finalize-l2 |
level==L2\|L3, agent-l2 not skipped/cancelled |
checkout, token, loop-run-log (and notify when a PR number exists) |
loop-finalize iff finalize_enabled |
Cancelled Agent jobs skip the matching finalize hop (no partial log required). Failed Agent jobs do finalize so the error is recorded.
finalize_enabled contract¶
| Layer | Meaning |
|---|---|
Caller with.finalize_enabled |
Still ${{ delivery == 'open_pr' }}. Unchanged at caller YAML. |
ci-loop-agent job if |
Must not require finalize_enabled. Job runs for run-log. |
loop-finalize step if |
inputs.finalize_enabled — git landing / state / PR create only. |
When the loop-finalize step is skipped (delivery not open_pr):
- Do not treat the skipped step as a finalize failure in run-log metadata.
- Run-log
outcome: agent failure →error;verdict==REJECT→rejected; otherwiseno-changes(no platform PR). Ifloop-finalizeran, prefer itsoutputs.outcome(pr-created, …).
Naming¶
- Forbidden:
record-l1as a sibling ofagent-l1. - Required:
finalize-l1/finalize-l2mirroringagent-l1/agent-l2. - Token-resolution comments list
finalize-l1/finalize-l2, not a genericfinalize.
Caller dogfood¶
Both ci-loop-caller.yaml and ci-loop-caller-entity.yaml execute.uses this repo relative path:
./.github/workflows/ci-loop-agent.yaml
External repositories that pin y-miyazaki/config/.../ci-loop-agent.yaml@<sha> pick up the graph on the next pin bump. This repository must not wait on a tag for the invariant to hold.
delivery: issue (not implied by entity L1)¶
Entity loops and delivery: issue are independent. An entity caller (ci-loop-caller-entity) binding a GitHub Issue event to agent-l1 does not implement delivery: issue. Jobs must not be specified as "the Agent already commented, so skip the adapter."
| Pattern | delivery |
Who talks to GitHub Issues |
|---|---|---|
| issue-triage today | none |
Implementer Agent during the session (labels/comments on the triggering Issue) |
delivery: issue adapter |
issue |
finalize-l1 / finalize-l2 only — skill does not call Issue APIs |
When delivery is issue, finalize resolves the destination as:
- Specified Issue — if
target_json(or detect result) carries an Issue number (entity.number,issue_number, or equivalent), append a comment (and optional labels) to that Issue. - Create Issue — if no number is present, create a new Issue from the Agent report (title/body). Repository default is the current repo unless
target_jsonnames another.
none means finalize does not post. In-session skill mutations on the triggering entity remain a separate, explicit skill contract — they are not a substitute for the adapter and must not be retrofitted as "delivery: issue is already done."
The adapter itself is not implemented in this change. finalize-l1 is the attach point.
Tests¶
test/bats/github-actions/loop-caller-parity.bats asserts:
- both callers share
auto_merge,may_editmapping,pr_draft,record-skipskip reasons - both
uses: ./.github/workflows/ci-loop-agent.yaml ci-loop-agenthasfinalize-l1andfinalize-l2, notrecord-l1finalize-l2jobifdoes not gate onfinalize_enabledloop-finalizestep is gated onfinalize_enabled
entity_target.bats keeps target_json.finalize derived from delivery.