@plainconceptsplatform/workflows 0.17.0 → 0.20.0

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 (60) hide show
  1. package/dist/catalog-installation.js +25 -13
  2. package/dist/stack-defaults.js +16 -16
  3. package/dist/worker-env.js +24 -21
  4. package/loops/actions/add-issue-labels/action.yml +50 -50
  5. package/loops/actions/agent-output.cjs +17 -17
  6. package/loops/actions/apply-agent-bundle/action.yml +24 -24
  7. package/loops/actions/apply-agent-comments/action.yml +42 -42
  8. package/loops/actions/apply-agent-labels/action.yml +55 -55
  9. package/loops/actions/apply-agent-output/action.yml +108 -108
  10. package/loops/actions/assess-blast-radius/action.yml +148 -0
  11. package/loops/actions/assess-blast-radius/assess-blast-radius.sh +138 -0
  12. package/loops/actions/classify-route/action.yml +100 -100
  13. package/loops/actions/cleanup-artifacts/action.yml +91 -91
  14. package/loops/actions/close-agent-issues/action.yml +43 -43
  15. package/loops/actions/create-agent-issues/action.yml +52 -52
  16. package/loops/actions/create-issue-comment/action.yml +29 -29
  17. package/loops/actions/download-agent-output/action.yml +53 -53
  18. package/loops/actions/housekeeping/action.yml +55 -2
  19. package/loops/actions/identify-gate-subject/action.yml +134 -122
  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/require-open-issue/action.yml +59 -0
  27. package/loops/actions/update-agent-issues/action.yml +58 -58
  28. package/loops/actions/validate-merge-gate-output/action.yml +62 -40
  29. package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +147 -31
  30. package/loops/actions/validate-refine-output/action.yml +48 -44
  31. package/loops/actions/validate-refine-output/validate-refine-output.sh +15 -4
  32. package/loops/actions/validate-review-output/action.yml +35 -35
  33. package/loops/actions/validate-triage-output/action.yml +36 -36
  34. package/loops/actions/verify-composite-actions/action.yml +9 -9
  35. package/loops/actions/verify-refine-output/action.yml +9 -9
  36. package/loops/actions/verify-refine-output/verify-refine-output.sh +6 -1
  37. package/loops/actions/verify-route-matrix/action.yml +9 -9
  38. package/loops/actions/verify-route-matrix/verify-gate-metrics.mjs +51 -0
  39. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +630 -8
  40. package/loops/scripts/compile-agent-workflows.mjs +331 -331
  41. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  42. package/loops/templates/ci/app-ci-dotnet-next.yml +330 -330
  43. package/loops/templates/ci/app-ci-node-monorepo.yml +260 -260
  44. package/loops/templates/issues/bug_report.yml +109 -109
  45. package/loops/templates/issues/feature_request.yml +75 -75
  46. package/loops/templates/opencode/opencode.ci.json +55 -49
  47. package/loops/templates/opencode/opencode.ci.json.md +59 -49
  48. package/loops/templates/release/github-release.yml +30 -30
  49. package/loops/workflows/agent-apply-review.md +33 -3
  50. package/loops/workflows/agent-audit.md +36 -0
  51. package/loops/workflows/agent-implement.md +77 -5
  52. package/loops/workflows/agent-merge-gate.md +368 -148
  53. package/loops/workflows/agent-refine.md +93 -17
  54. package/loops/workflows/agent-release.md +4 -4
  55. package/loops/workflows/agent-triage.md +33 -1
  56. package/loops/workflows/authorize-bot-work.yml +105 -105
  57. package/loops/workflows/shared/opencode-ci.md +206 -206
  58. package/loops/workflows/shared/platform-defaults.md +19 -19
  59. package/loops/workflows/work-router.yml +25 -11
  60. package/package.json +2 -2
@@ -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]"
@@ -114,6 +157,7 @@ jobs:
114
157
  token: ${{ github.token }}
115
158
  pr-number: ${{ inputs.pr-number }}
116
159
  ci-conclusion: ${{ inputs.ci-conclusion }}
160
+ ci-run-id: ${{ inputs.ci-run-id }}
117
161
  linked-issue: ${{ inputs.linked-issue }}
118
162
  require-label: ${{ env.IMPLEMENT_LABEL }}
119
163
  - name: Block a pull request with requested changes
@@ -136,45 +180,53 @@ jobs:
136
180
  done
137
181
  echo "review_blocked=$([ "$decision" = 'CHANGES_REQUESTED' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
138
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.
139
187
  protected_changes:
140
188
  needs: subject
141
189
  if: needs.subject.outputs.found == 'true'
142
190
  runs-on: agents-arc
143
191
  permissions:
192
+ contents: read
144
193
  pull-requests: read
145
194
  outputs:
146
- requires_review: ${{ steps.files.outputs.requires_review }}
147
- 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 }}
148
206
  # The decision, computed once. A protected path holds the merge for a human, but it must
149
207
  # not stop the agent repairing failed CI on those same files: blocking there strands the
150
208
  # pull request with nobody able to fix it. That pair of conditions used to be restated at
151
209
  # eight call sites, five of them steps of one job, and the trap table documents it because
152
210
  # it has already been got wrong. `holds_review` is the only place it is decided now.
153
- 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' }}
154
212
  steps:
155
- - name: Require review for protected pull request files
156
- id: files
157
- env:
158
- GH_TOKEN: ${{ github.token }}
159
- REPO: ${{ github.repository }}
160
- PR: ${{ needs.subject.outputs.pr }}
161
- PROTECTED_PATHS: ${{ env.PROTECTED_PATHS }}
162
- run: |
163
- set -euo pipefail
164
- files=$(gh api --paginate "repos/$REPO/pulls/$PR/files?per_page=100" --jq '.[].filename')
165
- protected=$(printf '%s\n' "$files" | grep -E "$PROTECTED_PATHS" || true)
166
-
167
- if [ -n "$protected" ]; then
168
- echo "requires_review=true" >> "$GITHUB_OUTPUT"
169
- {
170
- echo 'files<<EOF'
171
- printf '%s\n' "$protected"
172
- echo EOF
173
- } >> "$GITHUB_OUTPUT"
174
- else
175
- echo "requires_review=false" >> "$GITHUB_OUTPUT"
176
- echo "files=" >> "$GITHUB_OUTPUT"
177
- 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 }}
178
230
 
179
231
  review_required:
180
232
  needs: [subject, protected_changes]
@@ -209,13 +261,15 @@ jobs:
209
261
  token: ${{ steps.app-token.outputs.token }}
210
262
  issue-number: ${{ needs.subject.outputs.issue }}
211
263
  labels: ${{ env.WORKING_LABEL }}
212
- - name: Flag human review
264
+ - name: Flag owner review
213
265
  if: needs.protected_changes.outputs.holds_review == 'true'
214
266
  uses: ./.github/actions/add-issue-labels
215
267
  with:
216
268
  token: ${{ steps.app-token.outputs.token }}
217
269
  issue-number: ${{ needs.subject.outputs.issue }}
218
- labels: ${{ env.REVIEW_LABEL }}
270
+ labels: |-
271
+ ${{ env.REVIEW_LABEL }}
272
+ ${{ env.OWNER_REVIEW_LABEL }}
219
273
  - name: Explain the merge hold
220
274
  if: needs.protected_changes.outputs.holds_review == 'true'
221
275
  uses: ./.github/actions/create-issue-comment
@@ -225,12 +279,15 @@ jobs:
225
279
  body: |
226
280
  ${{ env.GATE_MARKER }}
227
281
  PR #${{ needs.subject.outputs.pr }} changes protected files and cannot be auto-merged.
228
- 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.
229
284
 
230
285
  Protected files:
231
286
  ${{ needs.protected_changes.outputs.files }}
232
287
 
233
- **Verdict:** review
288
+ Required owners: ${{ needs.protected_changes.outputs.required_owners || 'none configured' }}
289
+
290
+ **Verdict:** owner-review
234
291
 
235
292
  reserve:
236
293
  needs: subject
@@ -279,7 +336,7 @@ jobs:
279
336
  Problems found in PR #${{ needs.subject.outputs.pr }}. ${{ steps.conflicts.outputs.has_conflicts == 'true' && 'Merge conflicts detected.' || 'CI failed.' }}
280
337
  Bot is working on fixing it.
281
338
  validate_output:
282
- needs: [activation, subject, agent, safe_outputs]
339
+ needs: [activation, subject, protected_changes, agent, safe_outputs]
283
340
  if: >
284
341
  always() &&
285
342
  needs.agent.result == 'success' &&
@@ -300,20 +357,32 @@ jobs:
300
357
  uses: ./.github/actions/download-agent-output
301
358
  with:
302
359
  artifact-name: ${{ needs.activation.outputs.artifact_prefix }}agent
303
- - 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
304
363
  id: validate
305
364
  uses: ./.github/actions/validate-merge-gate-output
306
365
  with:
307
366
  output-file: ${{ steps.output.outputs.output-file }}
308
367
  issue-number: ${{ needs.subject.outputs.issue }}
309
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 }}
310
373
  conclude:
311
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.
312
380
  if: >
313
381
  needs.agent.result == 'success' &&
314
382
  needs.safe_outputs.result == 'success' &&
383
+ needs.protected_changes.result == 'success' &&
315
384
  needs.validate_output.outputs.valid == 'true' &&
316
- (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')
317
386
  runs-on: agents-arc
318
387
  permissions:
319
388
  contents: write
@@ -350,17 +419,36 @@ jobs:
350
419
  # lifecycle is; this is what a reviewer opening the pull request sees. Carrying the
351
420
  # marker and the Verdict line makes the router's verdict detection independent of
352
421
  # whether the model remembered the marker.
353
- - 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
354
426
  uses: ./.github/actions/create-issue-comment
355
427
  with:
356
428
  token: ${{ github.token }}
357
429
  issue-number: ${{ needs.subject.outputs.pr }}
358
430
  body: |
359
431
  ${{ env.GATE_MARKER }}
360
- **Verdict:** ${{ needs.validate_output.outputs.outcome }} (CI concluded ${{ needs.subject.outputs.conclusion }}).
361
- 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 }})
362
450
  - name: Merge approved pull request
363
- if: needs.validate_output.outputs.outcome == 'merge'
451
+ if: needs.validate_output.outputs.outcome == 'auto-merge'
364
452
  env:
365
453
  GH_TOKEN: ${{ steps.app-token.outputs.token }}
366
454
  REPO: ${{ github.repository }}
@@ -376,24 +464,50 @@ jobs:
376
464
  token: ${{ steps.app-token.outputs.token }}
377
465
  issue-number: ${{ needs.subject.outputs.issue }}
378
466
  labels: ${{ env.WORKING_LABEL }}
379
- - name: Flag review outcome
380
- 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)
381
472
  uses: ./.github/actions/add-issue-labels
382
473
  with:
383
474
  token: ${{ steps.app-token.outputs.token }}
384
475
  issue-number: ${{ needs.subject.outputs.issue }}
385
- 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
386
500
  # The reservation only. The pull request is still open and still waiting, so pr-pending
387
501
  # stays until the merge path below retires it.
388
- - name: Release review outcome
389
- 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)
390
504
  uses: ./.github/actions/remove-issue-labels
391
505
  with:
392
506
  token: ${{ steps.app-token.outputs.token }}
393
507
  issue-number: ${{ needs.subject.outputs.issue }}
394
508
  labels: ${{ env.WORKING_LABEL }}
395
509
  - name: Clear merged issue labels
396
- if: needs.validate_output.outputs.outcome == 'merge'
510
+ if: needs.validate_output.outputs.outcome == 'auto-merge'
397
511
  uses: ./.github/actions/remove-issue-labels
398
512
  with:
399
513
  token: ${{ steps.app-token.outputs.token }}
@@ -402,6 +516,8 @@ jobs:
402
516
  ${{ env.IMPLEMENT_LABEL }}
403
517
  ${{ env.WORKING_LABEL }}
404
518
  ${{ env.REVIEW_LABEL }}
519
+ ${{ env.OWNER_REVIEW_LABEL }}
520
+ ${{ env.BLOCKED_LABEL }}
405
521
  ${{ env.PR_PENDING_LABEL }}
406
522
  incomplete:
407
523
  needs: [subject, protected_changes, agent, safe_outputs, validate_output]
@@ -438,8 +554,37 @@ jobs:
438
554
  # attempts_so_far is a workflow_call input and arrives as '' when the caller passes an
439
555
  # empty expression, declared default or not; fromJson('') is a hard failure, so the empty
440
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"
441
586
  - name: Report the failed attempt
442
- if: fromJson(inputs.attempts_so_far || '0') < fromJson(env.PARK_AT_ATTEMPT)
587
+ if: steps.budget.outputs.park == 'false'
443
588
  uses: ./.github/actions/create-issue-comment
444
589
  with:
445
590
  token: ${{ steps.app-token.outputs.token }}
@@ -447,28 +592,53 @@ jobs:
447
592
  body: |
448
593
  ${{ env.ATTEMPT_MARKER }}
449
594
  Attempt ${{ inputs.attempts_so_far || '0' }} of ${{ env.MAX_ATTEMPTS }} on PR #${{ needs.subject.outputs.pr }} ended without an outcome.
450
- 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.
451
596
  [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
452
597
  - name: Report the exhausted attempt budget
453
- if: fromJson(inputs.attempts_so_far || '0') >= fromJson(env.PARK_AT_ATTEMPT)
598
+ if: steps.budget.outputs.park == 'true' && steps.budget.outputs.kind == 'machine'
454
599
  uses: ./.github/actions/create-issue-comment
455
600
  with:
456
601
  token: ${{ steps.app-token.outputs.token }}
457
602
  issue-number: ${{ needs.subject.outputs.issue }}
458
603
  body: |
459
604
  ${{ env.ATTEMPT_MARKER }}
460
- 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.
461
606
  The attempt budget for this CI verdict is exhausted. The review label is set: a human must take over.
462
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.
463
633
  - name: Park the issue for a human
464
- if: fromJson(inputs.attempts_so_far || '0') >= fromJson(env.PARK_AT_ATTEMPT)
634
+ if: steps.budget.outputs.park == 'true'
465
635
  uses: ./.github/actions/add-issue-labels
466
636
  with:
467
637
  token: ${{ steps.app-token.outputs.token }}
468
638
  issue-number: ${{ needs.subject.outputs.issue }}
469
639
  labels: |-
470
640
  ${{ env.REVIEW_LABEL }}
471
- ${{ env.STALLED_LABEL }}
641
+ ${{ steps.budget.outputs.kind == 'machine' && env.STALLED_LABEL || '' }}
472
642
  # The reservation only. A failed attempt does not close the pull request, so pr-pending
473
643
  # is still true and the board should keep saying so.
474
644
  - name: Release the issue
@@ -621,30 +791,31 @@ timeout-minutes: 120
621
791
  `push_to_pull_request_branch` tool's own description recommends rebasing; in this
622
792
  repository that advice is wrong. Merge, commit, and let the workflow push.
623
793
 
624
- 2. Read `${{ env.ISSUE_CONTEXT_PATH }}`. It contains the issue body and its discussion. When
625
- running `/repo-verify`, the acceptance criteria there define what the implementation must
626
- 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.
627
797
 
628
798
  3. Branch on the conclusion.
629
799
 
630
- 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.
631
804
 
632
- **If the trivial marker is present AND CI conclusion is success:**
633
- Skip the full assessment (step 5). Emit a minimal assessment table with all checks marked
634
- ✅ and the note "Trivial change, CI green — deep risk review skipped." Then proceed directly
635
- 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)
636
808
 
637
- **If the trivial marker is absent OR CI is not success:**
638
- Follow the normal branching below.
809
+ ```
810
+ ${{ needs.protected_changes.outputs.signals }}
811
+ ```
639
812
 
640
- - **success** → step 4, then step 5 (full assessment).
641
- - **action_required** → CI did not run because the workflow needs approval.
642
- Emit the assessment table with ❌ on CI Status and note that a maintainer must approve
643
- the pending run. Select the `review` verdict.
644
- - **failure** → step 4, then step 6 (CI remediation).
645
- - **cancelled, timed_out, or anything else** → Emit the assessment table with ❌ on CI
646
- Status and select the `review` verdict. A cancelled or unknown run is not evidence of
647
- 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.
648
819
 
649
820
  Follow repository documentation and established conventions when assessing or remediating
650
821
  the pull request. Protect secrets, do not bypass checks, and keep remediation focused.
@@ -654,7 +825,7 @@ timeout-minutes: 120
654
825
  `/tmp/gh-aw/agent/pr.json` for the shape of the change. If CI failed, also read
655
826
  `/tmp/gh-aw/agent/failed-jobs.json` and `/tmp/gh-aw/agent/failed-logs.txt`.
656
827
 
657
- 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.
658
829
 
659
830
  4b. **Merge conflict when CI is green.** If the conclusion is `success` and
660
831
  `has_conflicts` is `true` (current value: `${{ needs.reserve.outputs.has_conflicts }}`),
@@ -677,56 +848,92 @@ timeout-minutes: 120
677
848
  branch: the current PR branch), then emit the `add_comment` with
678
849
  **Verdict:** remediated. CI will re-run on the updated branch and the merge gate
679
850
  will be triggered again — the next cycle will see a clean, conflict-free PR and can
680
- make a proper merge or review decision.
851
+ reach a real disposition.
681
852
 
682
- If the merge cannot be completed or the conflicts are genuinely ambiguous, select the `review`
683
- 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.
684
856
 
685
857
  If the conclusion is `success` and `has_conflicts` is `false`, skip this step and
686
858
  proceed to step 5.
687
859
 
688
- 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.
689
861
 
690
- **Check 1 — CI Status.** What did CI conclude? Success means all required checks passed.
691
- Failure means at least one job failed. Action required means a workflow needs approval.
692
- 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.
693
865
 
694
- **Check 2 — Auth & Security.** Does the diff touch authentication, authorization, secrets,
695
- credentials, or security boundaries? Flag any change to auth middleware, permission checks,
696
- token issuance, or security-related config.
866
+ Keep a candidate only if it passes all three:
697
867
 
698
- **Check 3 — API & Contracts.** Does the diff change a public API or a published package's
699
- contract? Flag changes to endpoint signatures, DTO shapes, exported interfaces, or
700
- 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.
701
871
 
702
- **Check 4 — Tests.** Does the diff delete, weaken, or lower a threshold in a test? Flag
703
- 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.
704
875
 
705
- **Check 5 — CI/CD & Workflow files.** Does the diff change CI, CD, or workflow files?
706
- Flag changes to `.github/workflows/`, Dockerfiles, deployment scripts, or infrastructure
707
- 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.
708
879
 
709
- **Check 6 — Protected files.** Does the diff change a protected file? This is normally
710
- handled before you run, but never merge one if it reaches this gate. Flag any match against
711
- 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.
712
885
 
713
- **Check 7 — Scope.** Is the diff size consistent with what the issue implied? Compare the
714
- number of files changed and lines added/removed against the complexity the issue described.
715
- 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".
716
890
 
717
- **Check 8 — Repository risk indicators.** Does the diff touch any of these?
718
- ${{ 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.
719
894
 
720
- Name the specific indicator you matched. A match is not a defect, it is a reason this
721
- 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.
722
897
 
723
- **Check 9 — Mergeability.** Can the PR be merged cleanly? The value is
724
- `${{ needs.reserve.outputs.has_conflicts }}`. If conflicts exist, this is ❌ but not a
725
- 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.
726
902
 
727
- **Check 10 — Confidence.** Are you confident in the merge decision? Low confidence is
728
- itself a flag. If you are unsure about the impact of the change, mark ⚠️ and explain what
729
- 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.
730
937
 
731
938
  6. **CI failed** → read `/tmp/gh-aw/agent/failed-jobs.json` and
732
939
  `/tmp/gh-aw/agent/failed-logs.txt`, which are already on disk. Load only skills required to
@@ -775,52 +982,65 @@ timeout-minutes: 120
775
982
  the current PR branch), then select the `remediated` verdict. CI will run again and trigger
776
983
  you again with the new result.
777
984
 
778
- If you cannot fix it after a concrete repair attempt, or the logs show you have already tried on this same head commit,
779
- stop looping: select the `review` verdict and explain the failure and what you tried. A human
780
- decides from there.
781
-
782
- 7. Decide the verdict based on the assessment table:
783
-
784
- - **All checks ✅ → `merge`.** The PR is safe to merge. CI is green, no risk indicators
785
- triggered, tests are intact, scope matches, mergeability is clean.
786
- - **Any check ⚠️ or ❌ (except CI failure) → `review`.** Do not merge. Explain exactly which
787
- check tripped, why, and what a reviewer should look at. Leave `implement` in place: the
788
- work is not finished until a human merges it.
789
- - **CI failed and you fixed it → `remediated`.** You pushed a verified fix and CI will
790
- re-run.
791
- - **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.
792
989
 
793
- Never merge with administrator privileges and never bypass a required check. If the merge
794
- 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.
795
991
 
796
- 8. Emit exactly one `add_comment` targeting issue `${{ needs.subject.outputs.issue }}` with:
797
- 1. `${{ env.GATE_MARKER }}`
798
- 2. A heading: `## Merge gate decision for PR #${{ needs.subject.outputs.pr }}`
799
- 3. A structured assessment table with all 10 check results
800
- 4. A one-line detail per check (what was found and why it passed or flagged)
801
- 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.
802
995
 
803
- The workflow applies comments, labels, merges, and closures with the App token. Do not call
804
- any tools except the one optional `push_to_pull_request_branch` for a verified CI repair
805
- 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.
806
1000
 
807
- 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:
808
1003
 
809
- ```
810
- ### Assessment
811
-
812
- # Check Result
813
- 1 CI Status ✅ Success / ❌ Failure: [job name] / ⚠️ Action required
814
- 2 Auth & Security ✅ No changes / ⚠️ Touched: [area]
815
- 3 API & Contracts ✅ No changes / ⚠️ Changed: [area]
816
- 4 Tests ✅ Not weakened / ⚠️ Weakened: [file]
817
- 5 CI/CD & Workflow ✅ No changes / ⚠️ Changed: [file]
818
- 6 Protected files ✅ None touched / ❌ Touched: [file]
819
- 7 Scope ✅ Appropriately scoped / ⚠️ [too large/small: reason]
820
- 8 Risk indicators ✅ None triggered / ⚠️ Triggered: [indicator]
821
- 9 Mergeability ✅ Clean / ⚠️ Conflicts
822
- 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
+ }
823
1038
  ```
824
1039
 
825
- 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.
826
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`.