Loop Write Target and Delivery Design¶
Status: Approved (design session 2026-07-23)
Date: 2026-07-23
Related: Loop Engineering Design, Loop PR Body Skill Contract, Loop Caller Inputs Reference, common-loop-triage-format
Problem¶
Loop automation conflates three independent concerns:
- Autonomy — human review vs auto-merge vs read-only observation (
levelL1/L2/L3). - Worktree edits — whether the agent persists changes in git (
may_edit). - Artifact kind — fix source files vs write a structured report (
write_target).
Today loop-prompt-generate maps L2/L3 → may_edit: true and injects "a report alone is not sufficient when may_edit is true". That makes every L2+ loop an implicit fix loop. Report-only loops (e.g. tech-debt) work only by narrowing allowlist to docs/report/** — a skill-specific hack.
External delivery (GitHub Issue, Notion, PR comment) is not modeled. Skills should not embed platform integrations.
Goals¶
- Separate four planes so new skills and workflows extend without redesigning agent contracts.
- Keep Agent Skills aware only of worktree edit semantics (
may_edit,write_target,report_file). - Keep LE workflow / finalize responsible for delivery outside the worktree (
open_pr,issue,notion,log,none). - Retain
levelas an autonomy preset (worktree job selection, auto-merge) — not as a proxy for fix vs report. - Generalize
report_fileacross all skills; tech-debt is the first consumer, not a special case.
Non-Goals¶
- Phase 1 implementation of Notion / Backlog connectors (design the
deliveryenum and caller contract only). - Composite delivery (
open_pr+issuein one run) — defer; use two loops or a later ADR. - Putting
deliveryor external API details into## Constraintsor skillcategory-automation-envelope.md. - Removing
levelfrom callers — it remains required for platform job routing.
Architecture: Four Planes¶
┌─────────────────────────────────────────────────────────────────┐
│ 1. Autonomy (platform) level: L1 | L2 | L3 │
│ → worktree on/off, auto-merge preset │
├─────────────────────────────────────────────────────────────────┤
│ 2. Edit gate (agent) may_edit: true | false │
│ → persist to worktree or survey-only │
├─────────────────────────────────────────────────────────────────┤
│ 3. Artifact (agent) write_target: fix | report │
│ report_file (when report) │
│ → what git tracks after the agent run │
├─────────────────────────────────────────────────────────────────┤
│ 4. Delivery (platform) delivery: open_pr | issue | notion │
│ | log | none │
│ → how approved outcomes reach humans/systems outside git │
└─────────────────────────────────────────────────────────────────┘
Separation rule:
| Plane | Question | Seen by skill? |
|---|---|---|
| Autonomy | How much human gate / automerge? | No |
| Edit gate | Touch worktree? | Yes |
| Artifact | What in git? | Yes (when may_edit: true) |
| Delivery | Where after APPROVE? | No |
One-line mnemonic: write_target = git inside; delivery = world outside.
Field Definitions¶
level (caller — Autonomy preset)¶
| Value | Platform behavior | Does not imply |
|---|---|---|
L1 |
agent-l1 job; no worktree; no finalize PR path by default |
may_edit, write_target |
L2 |
agent-l2 + finalize; human merge on bot PR |
fix vs report |
L3 |
Same as L2 + GitHub auto-merge on bot PR | fix vs report |
level must not derive may_edit or write_target after this change. Callers supply those explicitly (defaults below are documentation presets only, not code-derived).
may_edit (caller → ## Constraints — Edit gate)¶
| Value | Agent behavior |
|---|---|
false |
Survey shape (### Candidates); no worktree edits; omit ### Changes, ## Verification |
true |
Apply shape; persist within allowlist |
write_target (caller → ## Constraints — Artifact)¶
Valid only when may_edit: true. Omit or treat as ignored when may_edit: false.
| Value | Agent writes | Typical allowlist |
|---|---|---|
fix |
Source/docs/manifests to resolve findings | src/**, docs/**, CHANGELOG.md, … |
report |
Structured report at report_file |
docs/report/<domain>/** |
Optional secondary closed-set fixes (tech-debt today): caller narrows an additional fix allowlist (e.g. docs/**, package.json) without a third enum value — write_target stays report; verifier enforces diff scope.
report_file (detect → target_json and/or ## Constraints)¶
| Condition | Required? |
|---|---|
write_target: report |
Yes — detect script or caller supplies path (e.g. docs/report/tech-debt/2026-07-23.md) |
write_target: fix |
Optional — secondary artifact path if skill emits a report in addition to fixes |
All loop skills may receive report_file in detect JSON; only report-mode loops require a non-empty value at verify time.
delivery (caller only — platform)¶
Not injected into skill ## Constraints.
| Value | When used | Input to finalize |
|---|---|---|
open_pr |
L2/L3 worktree loops | git diff + agent ## Overview / ## Summary |
issue |
Finalize Issue adapter | Agent report → specified Issue or newly created |
notion |
External doc systems | Same as issue |
log |
L1 observation | Run-log / state only |
none |
Dry-run / local | No external action |
delivery: issue destination (finalize only): if target_json already names an Issue number, comment on that Issue; otherwise create a new Issue from the Agent report. The implementer Agent must not call Issue APIs for this plane.
Entity L1 skills that mutate the triggering Issue during the session (e.g. issue-triage labels/comments) use delivery: none. That is not delivery: issue and does not implement this adapter. See Loop Agent Finalize Levels.
Existing loop-notify-pr (comment on human PR) remains a platform concern triggered by pull_request mode + finalize, not a skill field.
Caller Workflow Shape¶
Alphabetical with: keys per repository convention.
Action loop (fix)¶
delivery: open_pr
level: L2
may_edit: true
write_target: fix
allowlist: src/**,tests/**
Report loop¶
delivery: open_pr
level: L2
may_edit: true
write_target: report
allowlist: docs/report/tech-debt/**/*.md
detect_domain_env_json: '{"TECH_DEBT_DIR":"docs/report/tech-debt"}'
# report_file: supplied by detect in target_json / detect result
Observation / Issue-only (finalize adapter)¶
delivery: issue
level: L1
may_edit: false
# write_target and report_file omitted
# target_json.issue_number set → comment; unset → create Issue
This is not the issue-triage dogfood shape (delivery: none + in-session Issue mutations).
## Constraints Injection (skill-visible)¶
loop-prompt-generate emits only planes 2–3:
## Constraints
may_edit: true
write_target: report
report_file: docs/report/tech-debt/2026-07-23.md
Allowed paths: docs/report/tech-debt/**/*.md.
Do NOT modify any other files.
Do not claim files were modified unless git would show real changes.
Replace today's unconditional line "a report alone is not sufficient when may_edit is true" with target-aware text:
write_target |
Persistence obligation |
|---|---|
fix |
Must persist fixes within allowlist; survey-only output is insufficient |
report |
Must persist report_file within allowlist; source fixes outside allowlist are forbidden unless caller adds secondary fix globs and verifier allows |
Valid Combinations (normative)¶
may_edit |
write_target |
delivery |
Example loop | Valid |
|---|---|---|---|---|
false |
— | log |
L1 observe | Yes |
false |
— | issue |
Issue triage | Yes |
false |
— | notion |
External docs-updater | Yes |
false |
— | open_pr |
— | No |
true |
fix |
open_pr |
ci-sweeper, refactor, docs-updater, changelog | Yes |
true |
report |
open_pr |
tech-debt | Yes |
true |
report |
issue |
Duplicate channels | No (v1) |
true |
fix |
none |
Local experiment | Yes (rare) |
loop-detect or caller validation should reject invalid rows before execute.
Skill Contract Changes¶
Every loop automation skill:
- Branch on
may_editandwrite_targetfrom## Constraints— never onlevelordelivery. - Load
category-automation-envelope.mdon automation path; documentwrite_targetandreport_filefields. - Keep unified output shapes in
common-output-format.md(Overview,Summary,Changes/Candidates,Deferred,Verification). - Persisted report body sections remain skill-specific (e.g. tech-debt Critical/High tables).
Skills must not reference GitHub Issue, Notion, Backlog APIs, or delivery.
Platform Changes (implementation phases)¶
Phase 0 — Spec and docs (this document)¶
- Adopt four-plane model in loop-engineering docs.
- Deprecate wording "L2 = file fix" in workflow design docs; use
write_target.
Phase 1 — Caller inputs + constraints¶
- Add
may_edit,write_target,deliverytoci-loop-caller.yamlinputs (explicit; stop derivingmay_editfromlevelinbuild_constraints.sh). - Pass
may_edit,write_target,report_fileintoemit_loop_constraints. - Enrich
target_jsonwithreport_filewhen detect supplies it (generalize beyond tech-debt). - Caller validation script: combination matrix above.
Phase 2 — Migrate dogfood callers¶
| Caller | may_edit |
write_target |
delivery |
|---|---|---|---|
on-loop-changelog |
true |
fix |
open_pr |
on-loop-ci-sweeper |
true |
fix |
open_pr |
on-loop-docs-updater |
true |
fix |
open_pr |
on-loop-refactor |
true |
fix |
open_pr |
on-loop-tech-debt |
true |
report |
open_pr |
Phase 3 — Skills¶
- Update
category-automation-envelope.mdfor all loop skills (changelog, ci-sweeper, docs-updater, refactor, tech-debt). - Align tech-debt workflow design doc with skill (report primary; optional closed-set doc/manifest fixes via allowlist).
- Eval tasks for
write_target: reporton non-tech-debt skills (optional, when those loops ship).
Phase 4 — Delivery adapters¶
delivery: issuefinalize adapter (consumes agent report; no skill changes).notion/backlogas additional finalize backends behind same interface.
target_json Extension¶
{
"mode": "integration",
"key": "integration:main",
"from": { "branch": "main", "ref": "abc123" },
"to": { "branch": "main" },
"finalize": "open_pr",
"report_file": "docs/report/tech-debt/2026-07-23.md"
}
delivery stays on caller / detect env — not required inside target_json unless detect needs per-target delivery overrides later. target.finalize is derived from delivery (and optional git_landing_* on loop-detect when delivery: open_pr).
Migration Notes¶
- Backward compatibility:
may_editandwrite_targetare required on callers;loop-detectfail-closes withskip_reason: config_errorwhen omitted.emit_loop_constraints_from_levelremains deprecated for prompt-generation tests only — not for detect. RejectL1+may_edit: trueat validation (read-onlyagent-l1routing). loop-report-*naming: Report loops arewrite_target: report, not a separate skill family requirement. Name prefixloop-report-<domain>remains optional documentation convention.- tech-debt closed-set fixes: Keep as allowlist + verifier rule, not
write_target: hybrid.
References¶
- Implementation plan — phased tasks (platform → callers → skills)
- Loop PR Body Skill Contract — skill owns narrative; platform owns mechanical delivery
- loop-notify-pr Specification — platform comment delivery
build_constraints.sh— constraints injection (to be updated Phase 1)