@plainconceptsplatform/workflows 0.19.2 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/catalog-installation.js +12 -1
  2. package/dist/index.js +0 -0
  3. package/dist/stack-defaults.js +16 -16
  4. package/dist/worker-env.js +14 -0
  5. package/loops/actions/add-issue-labels/action.yml +50 -50
  6. package/loops/actions/agent-output.cjs +17 -17
  7. package/loops/actions/apply-agent-bundle/action.yml +24 -24
  8. package/loops/actions/apply-agent-comments/action.yml +42 -42
  9. package/loops/actions/apply-agent-labels/action.yml +55 -55
  10. package/loops/actions/apply-agent-output/action.yml +108 -108
  11. package/loops/actions/assess-blast-radius/action.yml +148 -0
  12. package/loops/actions/assess-blast-radius/assess-blast-radius.sh +138 -0
  13. package/loops/actions/classify-route/action.yml +100 -100
  14. package/loops/actions/cleanup-artifacts/action.yml +91 -91
  15. package/loops/actions/close-agent-issues/action.yml +43 -43
  16. package/loops/actions/create-agent-issues/action.yml +52 -52
  17. package/loops/actions/create-issue-comment/action.yml +29 -29
  18. package/loops/actions/download-agent-output/action.yml +53 -53
  19. package/loops/actions/housekeeping/action.yml +55 -2
  20. package/loops/actions/link-pr-to-issue/action.yml +40 -40
  21. package/loops/actions/list-open-issues/action.yml +33 -33
  22. package/loops/actions/load-issue-context/action.yml +45 -45
  23. package/loops/actions/merge-agent-pr/action.yml +49 -49
  24. package/loops/actions/push-agent-branch/action.yml +45 -45
  25. package/loops/actions/remove-issue-labels/action.yml +37 -37
  26. package/loops/actions/update-agent-issues/action.yml +58 -58
  27. package/loops/actions/validate-merge-gate-output/action.yml +62 -40
  28. package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +147 -31
  29. package/loops/actions/validate-refine-output/action.yml +48 -44
  30. package/loops/actions/validate-refine-output/validate-refine-output.sh +15 -4
  31. package/loops/actions/validate-review-output/action.yml +35 -35
  32. package/loops/actions/validate-triage-output/action.yml +36 -36
  33. package/loops/actions/verify-composite-actions/action.yml +9 -9
  34. package/loops/actions/verify-refine-output/action.yml +9 -9
  35. package/loops/actions/verify-refine-output/verify-refine-output.sh +6 -1
  36. package/loops/actions/verify-route-matrix/action.yml +9 -9
  37. package/loops/actions/verify-route-matrix/verify-gate-metrics.mjs +51 -0
  38. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +329 -29
  39. package/loops/scripts/compile-agent-workflows.mjs +331 -331
  40. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  41. package/loops/templates/ci/app-ci-dotnet-next.yml +330 -330
  42. package/loops/templates/ci/app-ci-node-monorepo.yml +260 -260
  43. package/loops/templates/issues/bug_report.yml +109 -109
  44. package/loops/templates/issues/feature_request.yml +75 -75
  45. package/loops/templates/opencode/opencode.ci.json +55 -49
  46. package/loops/templates/opencode/opencode.ci.json.md +59 -49
  47. package/loops/templates/release/github-release.yml +30 -30
  48. package/loops/workflows/agent-merge-gate.md +367 -148
  49. package/loops/workflows/agent-refine.md +60 -16
  50. package/loops/workflows/authorize-bot-work.yml +105 -105
  51. package/loops/workflows/shared/opencode-ci.md +206 -206
  52. package/loops/workflows/shared/platform-defaults.md +19 -19
  53. package/package.json +12 -11
@@ -2,11 +2,14 @@
2
2
  # Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-merge-gate.md. Update with `workflows update --force`; consumer edits may be overwritten.
3
3
  env:
4
4
  VERIFY_COMMANDS: ""
5
- REPO_RULES: "Make a risk-based merge decision for the selected bot pull request. Merge only when CI is green and no risk indicator is present. Do not merge protected file changes."
6
- # The list that decides whether a machine merges without a human looking. It used to be a
7
- # sentence inside REPO_RULES telling the agent to consult "the repository's guardrails or
8
- # project documentation", which named no list at all and left the most consequential check in
9
- # the pipeline resolving against nothing. Name the areas this repository will not auto-merge.
5
+ REPO_RULES: "Review the selected bot pull request for defects and report what you verified. Do not decide the outcome: the workflow computes it from your report and from facts it measured before you ran."
6
+ # Where to look first, not what to escalate on. This list used to be the check that decided
7
+ # whether a machine merged without a human, and it decided by category: the prompt told the
8
+ # agent that a match "is not a defect, it is a reason this pull request needs a person". In a
9
+ # layered application every feature PR touches an entity or a contract, so the gate escalated
10
+ # almost everything and, measured on a consumer, auto-merged 31% of terminal verdicts while
11
+ # finding zero defects. The areas are still worth naming; they are now the review pass's
12
+ # attention list. What decides is evidence, in the decision table the validator owns.
10
13
  # One line: gh-aw joins a multi-line env value onto a single line when it compiles the lock.
11
14
  RISK_INDICATORS: "Any diff touching authentication, authorization or session handling. Any change to a calculation or pricing engine, or to code handling money. Any database migration, or a change to an entity or schema. Any change to an audit or event log, or anything that could break its continuity. Any change to a public API contract or a shared library other repositories consume."
12
15
  # Paths a bot may change but never merge on its own: an extended regular expression matched
@@ -16,9 +19,42 @@ env:
16
19
  # list protects nothing and reports nothing. A match holds the merge for a human; it does not
17
20
  # stop the agent repairing failed CI on the same files.
18
21
  PROTECTED_PATHS: '^(\.|AGENTS\.md$|ARCHITECTURE\.md$|opencode\.jsonc$|package\.json$|pnpm-lock\.yaml$|Directory\.Packages\.props$|global\.json$)'
22
+ # Paths whose change needs the person who owns them. A match forces OWNER REVIEW REQUIRED and
23
+ # sets blast radius high on its own, whatever the diff's size. CODEOWNERS names who is asked
24
+ # when the repository has that file; it is never a prerequisite, because a repository without
25
+ # one must still be able to protect its auth and its infrastructure.
26
+ #
27
+ # Matches a path segment or a file stem, in both spellings, because the same default has to
28
+ # work for `src/auth/`, `src/Api/Identity/` and `AuthEndpoints.cs`. The lowercase-only,
29
+ # directory-only version this replaced matched nothing at all in a .NET consumer: replayed
30
+ # against that repository's last eighteen gated pull requests it caught none of them, while
31
+ # this one catches exactly three and they are the three that deserved an owner (a database
32
+ # migration, a change to the platform role definitions, and a downstream token service).
33
+ OWNER_PATHS: '(^|/)([Aa]uth|[Aa]uthn|[Aa]uthz|[Aa]uthentication|[Aa]uthorization|[Ii]dentity|[Ss]ecurity|[Ss]ecrets?|[Mm]igrations|[Ii]nfra|terraform|helm|k8s|deploy)(/|[A-Z][A-Za-z]*\.[a-z]+$)'
34
+ # Paths worth a second look that do not, alone, need a person. A match raises the floor to
35
+ # medium, and medium with acceptable recoverability still auto-merges. This is the line that
36
+ # separates "look here" from "stop here", which the old RISK_INDICATORS list could not.
37
+ SENSITIVE_PATHS: '(^|/)([Dd]omain|entities|[Cc]ontracts)/'
38
+ # Diff shape. Size and spread are the honest deterministic signal for a change that touches no
39
+ # path a regex would name: the one pull request in the measured sample that genuinely wanted an
40
+ # owner matched no sensitive path and was identified by 29 files and ~1600 lines across five
41
+ # architectural layers.
42
+ BLAST_HIGH_FILES: "20"
43
+ BLAST_HIGH_LINES: "800"
44
+ BLAST_MEDIUM_FILES: "5"
45
+ BLAST_MEDIUM_LINES: "200"
46
+ # Agent confidence below which the pull request goes to a human. The agent reports the number;
47
+ # this decides what it means.
48
+ CONFIDENCE_THRESHOLD: "0.8"
19
49
  WORKING_LABEL: bot-working
20
50
  IMPLEMENT_LABEL: implement
21
51
  REVIEW_LABEL: review
52
+ # Sits alongside `review`, never instead of it, so every board query that already asks for
53
+ # `review` keeps working. What it adds is the distinction the single label could not carry:
54
+ # `owner-review` says a named area changed, `blocked` says the machine could not proceed rather
55
+ # than chose not to. The belt does not retry a blocked pull request.
56
+ OWNER_REVIEW_LABEL: owner-review
57
+ BLOCKED_LABEL: blocked
22
58
  # Marks a park the machine caused — a crash, a timeout, an empty output — as opposed to one it
23
59
  # decided on. The janitor retries these after a while and never touches a decision park, because
24
60
  # re-running a decision produces the same decision. Created idempotently where it is applied.
@@ -28,6 +64,13 @@ env:
28
64
  ATTEMPT_MARKER: "<!-- agent-merge-gate-attempt -->"
29
65
  MAX_ATTEMPTS: "6"
30
66
  PARK_AT_ATTEMPT: "5"
67
+ # A smaller budget for the one failure that repeating does not fix. A crashed or timed-out run
68
+ # is a machine failure and worth repeating; a run that finished and handed back a report the
69
+ # validator could not read is a formatting problem, and the third attempt looks like the first.
70
+ # The cost is not hypothetical: every retry is a fresh agent run with this worker timeout, and
71
+ # `call-merge-gate` holds the repo-wide `merge-belt` slot while it runs, so five attempts on
72
+ # one unusable report can keep every other bot pull request in the repository waiting.
73
+ PARK_AT_UNUSABLE_OUTPUT: "2"
31
74
  ISSUE_CONTEXT_PATH: /tmp/gh-aw/agent/issue-context.json
32
75
  GH_AW_ALLOWED_BOTS: "platform-devbox[bot],github-actions[bot]"
33
76
  GIT_AUTHOR_NAME: "github-actions[bot]"
@@ -137,45 +180,53 @@ jobs:
137
180
  done
138
181
  echo "review_blocked=$([ "$decision" = 'CHANGES_REQUESTED' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
139
182
 
183
+ # Rung 4b. Everything the merge decision needs that a shell can establish, measured once,
184
+ # before any model reads the diff. The job kept its name and its `requires_review` and
185
+ # `holds_review` outputs because eight guards across this file and the route matrix read them;
186
+ # what it gained is the blast radius those guards never had.
140
187
  protected_changes:
141
188
  needs: subject
142
189
  if: needs.subject.outputs.found == 'true'
143
190
  runs-on: agents-arc
144
191
  permissions:
192
+ contents: read
145
193
  pull-requests: read
146
194
  outputs:
147
- requires_review: ${{ steps.files.outputs.requires_review }}
148
- files: ${{ steps.files.outputs.files }}
195
+ requires_review: ${{ steps.blast.outputs.requires_review }}
196
+ files: ${{ steps.blast.outputs.files }}
197
+ level: ${{ steps.blast.outputs.level }}
198
+ signals: ${{ steps.blast.outputs.signals }}
199
+ owner_hits: ${{ steps.blast.outputs.owner_hits }}
200
+ sensitive_hits: ${{ steps.blast.outputs.sensitive_hits }}
201
+ required_owners: ${{ steps.blast.outputs.required_owners }}
202
+ files_changed: ${{ steps.blast.outputs.files_changed }}
203
+ lines_changed: ${{ steps.blast.outputs.lines_changed }}
204
+ owner_hit: ${{ steps.blast.outputs.owner_hit }}
205
+ sensitive_hit: ${{ steps.blast.outputs.sensitive_hit }}
149
206
  # The decision, computed once. A protected path holds the merge for a human, but it must
150
207
  # not stop the agent repairing failed CI on those same files: blocking there strands the
151
208
  # pull request with nobody able to fix it. That pair of conditions used to be restated at
152
209
  # eight call sites, five of them steps of one job, and the trap table documents it because
153
210
  # it has already been got wrong. `holds_review` is the only place it is decided now.
154
- holds_review: ${{ steps.files.outputs.requires_review == 'true' && needs.subject.outputs.conclusion != 'failure' }}
211
+ holds_review: ${{ steps.blast.outputs.requires_review == 'true' && needs.subject.outputs.conclusion != 'failure' }}
155
212
  steps:
156
- - name: Require review for protected pull request files
157
- id: files
158
- env:
159
- GH_TOKEN: ${{ github.token }}
160
- REPO: ${{ github.repository }}
161
- PR: ${{ needs.subject.outputs.pr }}
162
- PROTECTED_PATHS: ${{ env.PROTECTED_PATHS }}
163
- run: |
164
- set -euo pipefail
165
- files=$(gh api --paginate "repos/$REPO/pulls/$PR/files?per_page=100" --jq '.[].filename')
166
- protected=$(printf '%s\n' "$files" | grep -E "$PROTECTED_PATHS" || true)
167
-
168
- if [ -n "$protected" ]; then
169
- echo "requires_review=true" >> "$GITHUB_OUTPUT"
170
- {
171
- echo 'files<<EOF'
172
- printf '%s\n' "$protected"
173
- echo EOF
174
- } >> "$GITHUB_OUTPUT"
175
- else
176
- echo "requires_review=false" >> "$GITHUB_OUTPUT"
177
- echo "files=" >> "$GITHUB_OUTPUT"
178
- fi
213
+ - name: Checkout workflow actions
214
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
215
+ with:
216
+ persist-credentials: false
217
+ - name: Assess blast radius
218
+ id: blast
219
+ uses: ./.github/actions/assess-blast-radius
220
+ with:
221
+ token: ${{ github.token }}
222
+ pr-number: ${{ needs.subject.outputs.pr }}
223
+ protected-paths: ${{ env.PROTECTED_PATHS }}
224
+ owner-paths: ${{ env.OWNER_PATHS }}
225
+ sensitive-paths: ${{ env.SENSITIVE_PATHS }}
226
+ high-files: ${{ env.BLAST_HIGH_FILES }}
227
+ high-lines: ${{ env.BLAST_HIGH_LINES }}
228
+ medium-files: ${{ env.BLAST_MEDIUM_FILES }}
229
+ medium-lines: ${{ env.BLAST_MEDIUM_LINES }}
179
230
 
180
231
  review_required:
181
232
  needs: [subject, protected_changes]
@@ -210,13 +261,15 @@ jobs:
210
261
  token: ${{ steps.app-token.outputs.token }}
211
262
  issue-number: ${{ needs.subject.outputs.issue }}
212
263
  labels: ${{ env.WORKING_LABEL }}
213
- - name: Flag human review
264
+ - name: Flag owner review
214
265
  if: needs.protected_changes.outputs.holds_review == 'true'
215
266
  uses: ./.github/actions/add-issue-labels
216
267
  with:
217
268
  token: ${{ steps.app-token.outputs.token }}
218
269
  issue-number: ${{ needs.subject.outputs.issue }}
219
- labels: ${{ env.REVIEW_LABEL }}
270
+ labels: |-
271
+ ${{ env.REVIEW_LABEL }}
272
+ ${{ env.OWNER_REVIEW_LABEL }}
220
273
  - name: Explain the merge hold
221
274
  if: needs.protected_changes.outputs.holds_review == 'true'
222
275
  uses: ./.github/actions/create-issue-comment
@@ -226,12 +279,15 @@ jobs:
226
279
  body: |
227
280
  ${{ env.GATE_MARKER }}
228
281
  PR #${{ needs.subject.outputs.pr }} changes protected files and cannot be auto-merged.
229
- The `review` label is set: a human must merge this PR manually.
282
+ The `review` and `owner-review` labels are set: the person who owns these files
283
+ decides, and merges.
230
284
 
231
285
  Protected files:
232
286
  ${{ needs.protected_changes.outputs.files }}
233
287
 
234
- **Verdict:** review
288
+ Required owners: ${{ needs.protected_changes.outputs.required_owners || 'none configured' }}
289
+
290
+ **Verdict:** owner-review
235
291
 
236
292
  reserve:
237
293
  needs: subject
@@ -280,7 +336,7 @@ jobs:
280
336
  Problems found in PR #${{ needs.subject.outputs.pr }}. ${{ steps.conflicts.outputs.has_conflicts == 'true' && 'Merge conflicts detected.' || 'CI failed.' }}
281
337
  Bot is working on fixing it.
282
338
  validate_output:
283
- needs: [activation, subject, agent, safe_outputs]
339
+ needs: [activation, subject, protected_changes, agent, safe_outputs]
284
340
  if: >
285
341
  always() &&
286
342
  needs.agent.result == 'success' &&
@@ -301,20 +357,32 @@ jobs:
301
357
  uses: ./.github/actions/download-agent-output
302
358
  with:
303
359
  artifact-name: ${{ needs.activation.outputs.artifact_prefix }}agent
304
- - name: Validate merge-gate outcome
360
+ # Where the decision is made. The agent contributed evidence; these inputs are the facts
361
+ # protected_changes measured before it ran. Neither half can produce a disposition alone.
362
+ - name: Compute the merge-gate disposition
305
363
  id: validate
306
364
  uses: ./.github/actions/validate-merge-gate-output
307
365
  with:
308
366
  output-file: ${{ steps.output.outputs.output-file }}
309
367
  issue-number: ${{ needs.subject.outputs.issue }}
310
368
  ci-conclusion: ${{ needs.subject.outputs.conclusion }}
369
+ blast-level: ${{ needs.protected_changes.outputs.level }}
370
+ protected-hit: ${{ needs.protected_changes.outputs.requires_review }}
371
+ owner-hit: ${{ needs.protected_changes.outputs.owner_hit }}
372
+ confidence-threshold: ${{ env.CONFIDENCE_THRESHOLD }}
311
373
  conclude:
312
374
  needs: [activation, subject, protected_changes, agent, safe_outputs, validate_output]
375
+ # `protected_changes.result == 'success'` is stated rather than relied on. GitHub skips a job
376
+ # whose needs failed, so this condition was never reached on that path, but every clause in
377
+ # it read as safe on a job that never ran: `requires_review` is '' when protected_changes
378
+ # fails, and '' != 'true'. A guard whose safety comes from somewhere else is a guard that
379
+ # stops working the moment someone adds always() to this job.
313
380
  if: >
314
381
  needs.agent.result == 'success' &&
315
382
  needs.safe_outputs.result == 'success' &&
383
+ needs.protected_changes.result == 'success' &&
316
384
  needs.validate_output.outputs.valid == 'true' &&
317
- (needs.protected_changes.outputs.requires_review != 'true' || needs.validate_output.outputs.outcome != 'merge')
385
+ (needs.protected_changes.outputs.requires_review != 'true' || needs.validate_output.outputs.outcome != 'auto-merge')
318
386
  runs-on: agents-arc
319
387
  permissions:
320
388
  contents: write
@@ -351,17 +419,36 @@ jobs:
351
419
  # lifecycle is; this is what a reviewer opening the pull request sees. Carrying the
352
420
  # marker and the Verdict line makes the router's verdict detection independent of
353
421
  # whether the model remembered the marker.
354
- - name: Show the verdict on the pull request
422
+ # One block so the reader sees the whole disposition at once instead of reconstructing it
423
+ # from a word. Everything on it was either measured before the agent ran or computed from
424
+ # what the agent proved; nothing here is the model's own summary of its mood.
425
+ - name: Show the disposition on the pull request
355
426
  uses: ./.github/actions/create-issue-comment
356
427
  with:
357
428
  token: ${{ github.token }}
358
429
  issue-number: ${{ needs.subject.outputs.pr }}
359
430
  body: |
360
431
  ${{ env.GATE_MARKER }}
361
- **Verdict:** ${{ needs.validate_output.outputs.outcome }} (CI concluded ${{ needs.subject.outputs.conclusion }}).
362
- Full assessment on the linked issue: #${{ needs.subject.outputs.issue }}. [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
432
+ **Verdict:** ${{ needs.validate_output.outputs.outcome }}
433
+
434
+ | | |
435
+ |---|---|
436
+ | Disposition | `${{ needs.validate_output.outputs.outcome }}` |
437
+ | CI | ${{ needs.subject.outputs.conclusion }} |
438
+ | Blast radius | ${{ needs.protected_changes.outputs.level }} |
439
+ | Files / lines | ${{ needs.protected_changes.outputs.files_changed }} / ${{ needs.protected_changes.outputs.lines_changed }} |
440
+ | Protected paths | ${{ needs.protected_changes.outputs.requires_review == 'true' && 'yes' || 'no' }} |
441
+ | Owner paths | ${{ needs.protected_changes.outputs.owner_hit == 'true' && 'yes' || 'no' }} |
442
+ | Required owners | ${{ needs.protected_changes.outputs.required_owners || 'none configured' }} |
443
+
444
+ Why this blast radius:
445
+ ```
446
+ ${{ needs.protected_changes.outputs.signals }}
447
+ ```
448
+
449
+ Findings and verification on the linked issue: #${{ needs.subject.outputs.issue }}. [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
363
450
  - name: Merge approved pull request
364
- if: needs.validate_output.outputs.outcome == 'merge'
451
+ if: needs.validate_output.outputs.outcome == 'auto-merge'
365
452
  env:
366
453
  GH_TOKEN: ${{ steps.app-token.outputs.token }}
367
454
  REPO: ${{ github.repository }}
@@ -377,24 +464,50 @@ jobs:
377
464
  token: ${{ steps.app-token.outputs.token }}
378
465
  issue-number: ${{ needs.subject.outputs.issue }}
379
466
  labels: ${{ env.WORKING_LABEL }}
380
- - name: Flag review outcome
381
- if: needs.validate_output.outputs.outcome == 'review'
467
+ # `review` goes on for all three parked dispositions, so every board query and every
468
+ # authorize-bot-work handoff that already reads it keeps working. The second label is what
469
+ # tells a person which kind of parking this is.
470
+ - name: Flag a parked outcome
471
+ if: contains(fromJson('["human-review","owner-review","blocked"]'), needs.validate_output.outputs.outcome)
382
472
  uses: ./.github/actions/add-issue-labels
383
473
  with:
384
474
  token: ${{ steps.app-token.outputs.token }}
385
475
  issue-number: ${{ needs.subject.outputs.issue }}
386
- labels: ${{ env.REVIEW_LABEL }}
476
+ labels: |-
477
+ ${{ env.REVIEW_LABEL }}
478
+ ${{ needs.validate_output.outputs.outcome == 'owner-review' && env.OWNER_REVIEW_LABEL || '' }}
479
+ ${{ needs.validate_output.outputs.outcome == 'blocked' && env.BLOCKED_LABEL || '' }}
480
+ # Asking the owner is best effort on purpose. A CODEOWNERS entry can name a team this App
481
+ # cannot request, and a failed request must not strand a pull request whose label and
482
+ # comment already say who is wanted.
483
+ - name: Request the owners named by CODEOWNERS
484
+ if: needs.validate_output.outputs.outcome == 'owner-review' && needs.protected_changes.outputs.required_owners != ''
485
+ continue-on-error: true
486
+ env:
487
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
488
+ REPO: ${{ github.repository }}
489
+ PR: ${{ needs.subject.outputs.pr }}
490
+ OWNERS: ${{ needs.protected_changes.outputs.required_owners }}
491
+ run: |
492
+ set -euo pipefail
493
+ for owner in $OWNERS; do
494
+ case "$owner" in
495
+ *@*) continue ;; # an email address is not a reviewer
496
+ @*/*) gh pr edit "$PR" --repo "$REPO" --add-reviewer "${owner#@}" || true ;;
497
+ @*) gh pr edit "$PR" --repo "$REPO" --add-reviewer "${owner#@}" || true ;;
498
+ esac
499
+ done
387
500
  # The reservation only. The pull request is still open and still waiting, so pr-pending
388
501
  # stays until the merge path below retires it.
389
- - name: Release review outcome
390
- if: needs.validate_output.outputs.outcome == 'review'
502
+ - name: Release a parked outcome
503
+ if: contains(fromJson('["human-review","owner-review","blocked"]'), needs.validate_output.outputs.outcome)
391
504
  uses: ./.github/actions/remove-issue-labels
392
505
  with:
393
506
  token: ${{ steps.app-token.outputs.token }}
394
507
  issue-number: ${{ needs.subject.outputs.issue }}
395
508
  labels: ${{ env.WORKING_LABEL }}
396
509
  - name: Clear merged issue labels
397
- if: needs.validate_output.outputs.outcome == 'merge'
510
+ if: needs.validate_output.outputs.outcome == 'auto-merge'
398
511
  uses: ./.github/actions/remove-issue-labels
399
512
  with:
400
513
  token: ${{ steps.app-token.outputs.token }}
@@ -403,6 +516,8 @@ jobs:
403
516
  ${{ env.IMPLEMENT_LABEL }}
404
517
  ${{ env.WORKING_LABEL }}
405
518
  ${{ env.REVIEW_LABEL }}
519
+ ${{ env.OWNER_REVIEW_LABEL }}
520
+ ${{ env.BLOCKED_LABEL }}
406
521
  ${{ env.PR_PENDING_LABEL }}
407
522
  incomplete:
408
523
  needs: [subject, protected_changes, agent, safe_outputs, validate_output]
@@ -439,8 +554,37 @@ jobs:
439
554
  # attempts_so_far is a workflow_call input and arrives as '' when the caller passes an
440
555
  # empty expression, declared default or not; fromJson('') is a hard failure, so the empty
441
556
  # case reads as 0.
557
+ #
558
+ # Which budget applies is decided once, here, rather than restated in each step condition:
559
+ # the same pair of conditions spread across four `if:` expressions is what the trap table
560
+ # already records going wrong for the protected-files hold.
561
+ - name: Choose the budget this failure gets
562
+ id: budget
563
+ env:
564
+ ATTEMPTS: ${{ inputs.attempts_so_far || '0' }}
565
+ AGENT_RESULT: ${{ needs.agent.result }}
566
+ SAFE_RESULT: ${{ needs.safe_outputs.result }}
567
+ OUTPUT_VALID: ${{ needs.validate_output.outputs.valid }}
568
+ PARK_AT_ATTEMPT: ${{ env.PARK_AT_ATTEMPT }}
569
+ PARK_AT_UNUSABLE_OUTPUT: ${{ env.PARK_AT_UNUSABLE_OUTPUT }}
570
+ run: |
571
+ set -euo pipefail
572
+ attempts=${ATTEMPTS:-0}
573
+ # The agent ran, published, and produced something the validator refused. Repeating
574
+ # that reproduces it; a person reading the comment costs less than three more runs
575
+ # holding the merge belt.
576
+ if [ "$AGENT_RESULT" = success ] && [ "$SAFE_RESULT" = success ] && [ "$OUTPUT_VALID" != true ]; then
577
+ threshold="$PARK_AT_UNUSABLE_OUTPUT"
578
+ kind=unusable
579
+ else
580
+ threshold="$PARK_AT_ATTEMPT"
581
+ kind=machine
582
+ fi
583
+ echo "kind=$kind" >> "$GITHUB_OUTPUT"
584
+ echo "threshold=$threshold" >> "$GITHUB_OUTPUT"
585
+ echo "park=$([ "$attempts" -ge "$threshold" ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
442
586
  - name: Report the failed attempt
443
- if: fromJson(inputs.attempts_so_far || '0') < fromJson(env.PARK_AT_ATTEMPT)
587
+ if: steps.budget.outputs.park == 'false'
444
588
  uses: ./.github/actions/create-issue-comment
445
589
  with:
446
590
  token: ${{ steps.app-token.outputs.token }}
@@ -448,28 +592,53 @@ jobs:
448
592
  body: |
449
593
  ${{ env.ATTEMPT_MARKER }}
450
594
  Attempt ${{ inputs.attempts_so_far || '0' }} of ${{ env.MAX_ATTEMPTS }} on PR #${{ needs.subject.outputs.pr }} ended without an outcome.
451
- The issue keeps `implement`; the merge belt will retry.
595
+ This failure parks at ${{ steps.budget.outputs.threshold }}. The issue keeps `implement`; the merge belt will retry.
452
596
  [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
453
597
  - name: Report the exhausted attempt budget
454
- if: fromJson(inputs.attempts_so_far || '0') >= fromJson(env.PARK_AT_ATTEMPT)
598
+ if: steps.budget.outputs.park == 'true' && steps.budget.outputs.kind == 'machine'
455
599
  uses: ./.github/actions/create-issue-comment
456
600
  with:
457
601
  token: ${{ steps.app-token.outputs.token }}
458
602
  issue-number: ${{ needs.subject.outputs.issue }}
459
603
  body: |
460
604
  ${{ env.ATTEMPT_MARKER }}
461
- Attempt ${{ inputs.attempts_so_far || '0' }} of ${{ env.MAX_ATTEMPTS }} on PR #${{ needs.subject.outputs.pr }} ended without an outcome.
605
+ Attempt ${{ inputs.attempts_so_far || '0' }} of ${{ steps.budget.outputs.threshold }} on PR #${{ needs.subject.outputs.pr }} ended without an outcome.
462
606
  The attempt budget for this CI verdict is exhausted. The review label is set: a human must take over.
463
607
  [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
608
+ # A verdict, not an attempt record, and this is the point of the whole split. The belt
609
+ # bounds its own retries by counting attempt comments to MAX_GATE_ATTEMPTS, so a worker
610
+ # that merely stopped parking would still be dispatched to the cap: the budget above would
611
+ # have saved nothing. A comment carrying the gate marker and a Verdict line is the contract
612
+ # the belt already respects -- it parks the pull request until a new commit moves the head
613
+ # past it -- and an unusable report is a decision, not a failure to repeat. It carries no
614
+ # attempt marker, so it is counted once, as what it is.
615
+ - name: Record an unusable report as a decision
616
+ if: steps.budget.outputs.park == 'true' && steps.budget.outputs.kind == 'unusable'
617
+ uses: ./.github/actions/create-issue-comment
618
+ with:
619
+ token: ${{ steps.app-token.outputs.token }}
620
+ issue-number: ${{ needs.subject.outputs.issue }}
621
+ body: |
622
+ ${{ env.GATE_MARKER }}
623
+ The agent finished on PR #${{ needs.subject.outputs.pr }} but its report could not be
624
+ read, ${{ steps.budget.outputs.threshold }} times on this head. Repeating it reproduces
625
+ it, so the belt stops here rather than spending the rest of the budget holding the
626
+ merge slot. The run log holds the output the gate refused.
627
+
628
+ **Verdict:** human-review
629
+ [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
630
+ # `stalled` means a park the machine caused, and the janitor retries those. An unusable
631
+ # report is a park the machine caused and retrying reproduces it, so it gets `review`
632
+ # alone: the janitor's own rule is retry a failure, report a decision.
464
633
  - name: Park the issue for a human
465
- if: fromJson(inputs.attempts_so_far || '0') >= fromJson(env.PARK_AT_ATTEMPT)
634
+ if: steps.budget.outputs.park == 'true'
466
635
  uses: ./.github/actions/add-issue-labels
467
636
  with:
468
637
  token: ${{ steps.app-token.outputs.token }}
469
638
  issue-number: ${{ needs.subject.outputs.issue }}
470
639
  labels: |-
471
640
  ${{ env.REVIEW_LABEL }}
472
- ${{ env.STALLED_LABEL }}
641
+ ${{ steps.budget.outputs.kind == 'machine' && env.STALLED_LABEL || '' }}
473
642
  # The reservation only. A failed attempt does not close the pull request, so pr-pending
474
643
  # is still true and the board should keep saying so.
475
644
  - name: Release the issue
@@ -622,30 +791,31 @@ timeout-minutes: 120
622
791
  `push_to_pull_request_branch` tool's own description recommends rebasing; in this
623
792
  repository that advice is wrong. Merge, commit, and let the workflow push.
624
793
 
625
- 2. Read `${{ env.ISSUE_CONTEXT_PATH }}`. It contains the issue body and its discussion. When
626
- running `/repo-verify`, the acceptance criteria there define what the implementation must
627
- satisfy.
794
+ 2. Read `${{ env.ISSUE_CONTEXT_PATH }}`. It contains the issue body and its discussion. The
795
+ acceptance criteria there define what this implementation had to satisfy, and step 5c asks
796
+ you to check the diff against them.
628
797
 
629
798
  3. Branch on the conclusion.
630
799
 
631
- First check the issue context for `<!-- complexity: trivial -->`.
800
+ You do not choose the outcome. The workflow computes it from your report and from facts it
801
+ measured before you started. Two of those facts are already decided and you cannot argue with
802
+ either: CI concluded what it concluded, and the blast radius below was measured from the
803
+ changed paths and the diff shape.
632
804
 
633
- **If the trivial marker is present AND CI conclusion is success:**
634
- Skip the full assessment (step 5). Emit a minimal assessment table with all checks marked
635
- ✅ and the note "Trivial change, CI green — deep risk review skipped." Then proceed directly
636
- to step 8 (merge verdict).
805
+ **Measured blast radius: `${{ needs.protected_changes.outputs.level }}`**
806
+ (${{ needs.protected_changes.outputs.files_changed }} files,
807
+ ${{ needs.protected_changes.outputs.lines_changed }} lines changed)
637
808
 
638
- **If the trivial marker is absent OR CI is not success:**
639
- Follow the normal branching below.
809
+ ```
810
+ ${{ needs.protected_changes.outputs.signals }}
811
+ ```
640
812
 
641
- - **success** → step 4, then step 5 (full assessment).
642
- - **action_required** → CI did not run because the workflow needs approval.
643
- Emit the assessment table with ❌ on CI Status and note that a maintainer must approve
644
- the pending run. Select the `review` verdict.
645
- - **failure** → step 4, then step 6 (CI remediation).
646
- - **cancelled, timed_out, or anything else** → Emit the assessment table with ❌ on CI
647
- Status and select the `review` verdict. A cancelled or unknown run is not evidence of
648
- anything.
813
+ - **success** → step 4, then step 5 (review the change).
814
+ - **failure** → step 4, then step 6 (CI remediation).
815
+ - **action_required, cancelled, timed_out, or anything else** → CI did not produce a usable
816
+ verdict, so there is nothing to merge on. Do step 5 anyway so the report is on the record,
817
+ and say in `reason` which conclusion you saw. The workflow blocks on a non-success
818
+ conclusion without needing you to.
649
819
 
650
820
  Follow repository documentation and established conventions when assessing or remediating
651
821
  the pull request. Protect secrets, do not bypass checks, and keep remediation focused.
@@ -655,7 +825,7 @@ timeout-minutes: 120
655
825
  `/tmp/gh-aw/agent/pr.json` for the shape of the change. If CI failed, also read
656
826
  `/tmp/gh-aw/agent/failed-jobs.json` and `/tmp/gh-aw/agent/failed-logs.txt`.
657
827
 
658
- These files are the factual basis for every check below. Do not guess — cite what you read.
828
+ These files are the factual basis for everything below. Do not guess — cite what you read.
659
829
 
660
830
  4b. **Merge conflict when CI is green.** If the conclusion is `success` and
661
831
  `has_conflicts` is `true` (current value: `${{ needs.reserve.outputs.has_conflicts }}`),
@@ -678,56 +848,92 @@ timeout-minutes: 120
678
848
  branch: the current PR branch), then emit the `add_comment` with
679
849
  **Verdict:** remediated. CI will re-run on the updated branch and the merge gate
680
850
  will be triggered again — the next cycle will see a clean, conflict-free PR and can
681
- make a proper merge or review decision.
851
+ reach a real disposition.
682
852
 
683
- If the merge cannot be completed or the conflicts are genuinely ambiguous, select the `review`
684
- verdict instead and explain which conflicts could not be resolved safely.
853
+ If the merge cannot be completed or the conflicts are genuinely ambiguous, do not push.
854
+ Report `assessed`, and say in `reason` which conflicts could not be resolved safely. An
855
+ unresolved conflict is not a mergeable state, so the workflow will not merge it.
685
856
 
686
857
  If the conclusion is `success` and `has_conflicts` is `false`, skip this step and
687
858
  proceed to step 5.
688
859
 
689
- 5. Run each of these 10 checks. For each, determine a status and a short detail line.
860
+ 5. Review the change. This is the part no deterministic check can do, so spend the run here.
690
861
 
691
- **Check 1 — CI Status.** What did CI conclude? Success means all required checks passed.
692
- Failure means at least one job failed. Action required means a workflow needs approval.
693
- Flag any non-success conclusion.
862
+ **5a. Find defects.** Read the diff and the code it touches. You are looking for problems
863
+ that would matter after this merges: correctness, missing edge cases, broken contracts,
864
+ regressions, security, tests that do not actually test the behaviour they name.
694
865
 
695
- **Check 2 — Auth & Security.** Does the diff touch authentication, authorization, secrets,
696
- credentials, or security boundaries? Flag any change to auth middleware, permission checks,
697
- token issuance, or security-related config.
866
+ Keep a candidate only if it passes all three:
698
867
 
699
- **Check 3 — API & Contracts.** Does the diff change a public API or a published package's
700
- contract? Flag changes to endpoint signatures, DTO shapes, exported interfaces, or
701
- serialization formats that could break consumers.
868
+ - A specific, reproducible problem in a specific file or component.
869
+ - Real impact: security risk, data loss, crash, or broken functionality.
870
+ - Something a developer could pick up and fix without further investigation.
702
871
 
703
- **Check 4 — Tests.** Does the diff delete, weaken, or lower a threshold in a test? Flag
704
- removed assertions, skipped tests, lowered coverage bars, or deleted test files.
872
+ Discard anything vague, stylistic, theoretical, or nice-to-have. **Finding nothing is a good
873
+ result.** An empty `findings` array on a clean change is the correct output and costs you
874
+ nothing. Do not pad the list.
705
875
 
706
- **Check 5 — CI/CD & Workflow files.** Does the diff change CI, CD, or workflow files?
707
- Flag changes to `.github/workflows/`, Dockerfiles, deployment scripts, or infrastructure
708
- configuration.
876
+ Read `${{ env.RISK_INDICATORS }}` as a list of places worth looking first in this repository.
877
+ It is an attention list, not a verdict. Touching one of those areas is not a finding. A
878
+ defect you can demonstrate in one of them is.
709
879
 
710
- **Check 6 — Protected files.** Does the diff change a protected file? This is normally
711
- handled before you run, but never merge one if it reaches this gate. Flag any match against
712
- the repository's protected file list.
880
+ **5b. Have each candidate verified independently.** Do not be the one who checks your own
881
+ work. Hand each candidate off for verification as a claim on its own: the file, the line,
882
+ what you think is wrong, and what would settle it. Do not pass on the reasoning that produced
883
+ it, and do not say what you hope comes back. A verifier that has read the code fresh and
884
+ tried to disprove the claim is the check you cannot perform on yourself.
713
885
 
714
- **Check 7 — Scope.** Is the diff size consistent with what the issue implied? Compare the
715
- number of files changed and lines added/removed against the complexity the issue described.
716
- Flag if the diff is materially larger or smaller than expected.
886
+ Take the answer. Not verified means the finding is a warning at most, whatever you believed
887
+ when you wrote it. Verified means the verification string comes back with it, and that string
888
+ is the evidence the merge decision will rest on: a command with its observed output, or a
889
+ code path quoted end to end. Never "this looks wrong" or "this could fail if".
717
890
 
718
- **Check 8 — Repository risk indicators.** Does the diff touch any of these?
719
- ${{ env.RISK_INDICATORS }}
891
+ Two things make this cheap to do honestly. An unverified finding is capped at a warning by the
892
+ workflow whatever severity you claim, so overstating one gains you nothing. A verified high or
893
+ critical finding blocks the merge, so inventing one costs somebody a morning.
720
894
 
721
- Name the specific indicator you matched. A match is not a defect, it is a reason this
722
- pull request needs a person, so do not argue it away because the change looks correct.
895
+ With no candidates, verify nothing and move on. This step exists for claims, not for
896
+ reassurance about their absence.
723
897
 
724
- **Check 9 — Mergeability.** Can the PR be merged cleanly? The value is
725
- `${{ needs.reserve.outputs.has_conflicts }}`. If conflicts exist, this is ❌ but not a
726
- blocking verdict — proceed to remediation (step 6). If no conflicts, ✅.
898
+ **5c. Check the acceptance criteria.** The issue context at `${{ env.ISSUE_CONTEXT_PATH }}`
899
+ says what this change was supposed to do. Confirm the diff does it. Set
900
+ `acceptanceCriteriaMet` to false only when you can name a criterion the diff does not
901
+ satisfy.
727
902
 
728
- **Check 10 — Confidence.** Are you confident in the merge decision? Low confidence is
729
- itself a flag. If you are unsure about the impact of the change, mark ⚠️ and explain what
730
- is uncertain. A human should review when confidence is low.
903
+ **5d. Answer the recoverability checklist.** How easy would this be to undo if it were
904
+ wrong? Cite the diff for each answer, and record the ones that fired in
905
+ `recoverabilitySignals`:
906
+
907
+ - behind a feature flag
908
+ - revertible by reverting the commit, with no manual step
909
+ - no persistent data mutated
910
+ - no irreversible migration
911
+ - backward compatible with existing callers and stored data
912
+ - observable after deploy
913
+ - small affected surface
914
+
915
+ `high` when the change can be reverted cleanly and touches no persistent state. `medium` when
916
+ a revert works but something (a cache, a config, a client) needs attention. `low` when a
917
+ revert would not restore the previous behaviour: a migration that drops or rewrites data, a
918
+ contract other repositories already consume, anything that leaves state behind.
919
+
920
+ **A `low` rating must name what cannot be undone**, in `recoverabilitySignals`. `low` parks
921
+ the pull request for a person, so it is the one judgement of yours that can hold up a merge on
922
+ its own, and the same rule applies to it as to a finding: unevidenced, it does not count. A
923
+ `low` with an empty `recoverabilitySignals` is read as `medium`. This is not an invitation to
924
+ pad the list — it is the difference between "this rewrites the plan rows in place" and a
925
+ reflex.
926
+
927
+ **5e. Raise the blast radius if the paths missed something.** The measured level came from
928
+ file paths and diff shape. If the change introduces something those rules cannot see — a new
929
+ authorization decision point, a new trust boundary, a write to shared state from a path that
930
+ never wrote before — set `blastRadiusRaise` with the level and the reason. You can only raise
931
+ it. A lower value is ignored.
932
+
933
+ **5f. State your confidence.** A number between 0 and 1, for the whole assessment, not for
934
+ any one finding. Below ${{ env.CONFIDENCE_THRESHOLD }} sends the pull request to a person, so
935
+ it is the honest way to say you could not get comfortable. Use it when the change is in an
936
+ area you could not fully trace, not as a reflex.
731
937
 
732
938
  6. **CI failed** → read `/tmp/gh-aw/agent/failed-jobs.json` and
733
939
  `/tmp/gh-aw/agent/failed-logs.txt`, which are already on disk. Load only skills required to
@@ -776,52 +982,65 @@ timeout-minutes: 120
776
982
  the current PR branch), then select the `remediated` verdict. CI will run again and trigger
777
983
  you again with the new result.
778
984
 
779
- If you cannot fix it after a concrete repair attempt, or the logs show you have already tried on this same head commit,
780
- stop looping: select the `review` verdict and explain the failure and what you tried. A human
781
- decides from there.
782
-
783
- 7. Decide the verdict based on the assessment table:
784
-
785
- - **All checks ✅ → `merge`.** The PR is safe to merge. CI is green, no risk indicators
786
- triggered, tests are intact, scope matches, mergeability is clean.
787
- - **Any check ⚠️ or ❌ (except CI failure) → `review`.** Do not merge. Explain exactly which
788
- check tripped, why, and what a reviewer should look at. Leave `implement` in place: the
789
- work is not finished until a human merges it.
790
- - **CI failed and you fixed it → `remediated`.** You pushed a verified fix and CI will
791
- re-run.
792
- - **CI failed and you cannot fix it → `review`.** Explain the failure and what you tried.
985
+ If you cannot fix it after a concrete repair attempt, or the logs show you have already tried
986
+ on this same head commit, stop looping: report `assessed` with no push, and say in `reason`
987
+ what failed and what you tried. CI is not green, so the workflow blocks the pull request and
988
+ a person decides from there.
793
989
 
794
- Never merge with administrator privileges and never bypass a required check. If the merge
795
- is refused, that refusal is the answer: select `review` and leave it for a human.
990
+ 7. Say which of two things you did, and nothing more.
796
991
 
797
- 8. Emit exactly one `add_comment` targeting issue `${{ needs.subject.outputs.issue }}` with:
798
- 1. `${{ env.GATE_MARKER }}`
799
- 2. A heading: `## Merge gate decision for PR #${{ needs.subject.outputs.pr }}`
800
- 3. A structured assessment table with all 10 check results
801
- 4. A one-line detail per check (what was found and why it passed or flagged)
802
- 5. A line `**Verdict:** merge`, `**Verdict:** review`, or `**Verdict:** remediated`
992
+ - **`remediated`** — CI failed or the branch conflicted, you fixed it, you verified the fix,
993
+ and you are pushing it. Exactly one `push_to_pull_request_branch` goes with this word.
994
+ - **`assessed`** — you reviewed the change and are reporting what you found. No push.
803
995
 
804
- The workflow applies comments, labels, merges, and closures with the App token. Do not call
805
- any tools except the one optional `push_to_pull_request_branch` for a verified CI repair
806
- and this one `add_comment`.
996
+ These are the only two words the workflow accepts. You do not write `merge`, `review`,
997
+ `auto-merge`, `blocked`, or any other outcome: the workflow computes the disposition from
998
+ your report and from the facts it measured, and a word it does not recognise parks the pull
999
+ request. Never merge with administrator privileges and never bypass a required check.
807
1000
 
808
- Format the checks as a table with status indicators:
1001
+ 8. Emit exactly one `add_comment` targeting issue `${{ needs.subject.outputs.issue }}`,
1002
+ containing, in this order:
809
1003
 
810
- ```
811
- ### Assessment
812
-
813
- # Check Result
814
- 1 CI Status ✅ Success / ❌ Failure: [job name] / ⚠️ Action required
815
- 2 Auth & Security ✅ No changes / ⚠️ Touched: [area]
816
- 3 API & Contracts ✅ No changes / ⚠️ Changed: [area]
817
- 4 Tests ✅ Not weakened / ⚠️ Weakened: [file]
818
- 5 CI/CD & Workflow ✅ No changes / ⚠️ Changed: [file]
819
- 6 Protected files ✅ None touched / ❌ Touched: [file]
820
- 7 Scope ✅ Appropriately scoped / ⚠️ [too large/small: reason]
821
- 8 Risk indicators ✅ None triggered / ⚠️ Triggered: [indicator]
822
- 9 Mergeability ✅ Clean / ⚠️ Conflicts
823
- 10 Confidence ✅ High / ⚠️ Low: [reason]
1004
+ 1. `${{ env.GATE_MARKER }}`
1005
+ 2. A heading: `## Merge gate review of PR #${{ needs.subject.outputs.pr }}`
1006
+ 3. A line `**Verdict:** assessed` or `**Verdict:** remediated`
1007
+ 4. Prose a person can read: what this change does, what you looked at, what you found or did
1008
+ not find, and what you ran to check. If you remediated, say what failed and what you
1009
+ changed. Short. Nobody reads a wall.
1010
+ 5. A fenced `json` block, exactly one, as the last thing in the comment.
1011
+
1012
+ The JSON block is what the workflow reads. Every field is required except
1013
+ `blastRadiusRaise`, which is omitted when the measured level stands:
1014
+
1015
+ ```json
1016
+ {
1017
+ "findings": [
1018
+ {
1019
+ "severity": "critical|high|medium|low",
1020
+ "confidence": 0.0,
1021
+ "verified": true,
1022
+ "verification": "the command you ran and what it printed, or the code path quoted end to end",
1023
+ "category": "correctness|security|contract|tests|regression",
1024
+ "file": "src/...",
1025
+ "line": 0,
1026
+ "finding": "one sentence: what is wrong",
1027
+ "evidence": "what in the diff or the code shows it",
1028
+ "suggestedFix": "what would fix it"
1029
+ }
1030
+ ],
1031
+ "recoverability": "high|medium|low",
1032
+ "recoverabilitySignals": ["revertible with no manual step", "no persistent state written"],
1033
+ "blastRadiusRaise": { "to": "high", "reason": "adds a new authorization decision point" },
1034
+ "acceptanceCriteriaMet": true,
1035
+ "confidence": 0.0,
1036
+ "reason": "one sentence a person would accept as the summary"
1037
+ }
824
1038
  ```
825
1039
 
826
- Then a line `**Verdict:** merge` / `**Verdict:** review` / `**Verdict:** remediated`
1040
+ `"findings": []` on a clean change is the expected output, not a failure to do the job.
827
1041
 
1042
+ The workflow applies comments, labels, merges, and closures with the App token. Reading the
1043
+ repository, running verification commands and delegating a finding to be checked are all part
1044
+ of the job. What is restricted is what leaves this run: the only safe outputs you may call are
1045
+ the one optional `push_to_pull_request_branch` for a verified repair and this one
1046
+ `add_comment`.