@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.
- package/dist/catalog-installation.js +25 -13
- package/dist/stack-defaults.js +16 -16
- package/dist/worker-env.js +24 -21
- package/loops/actions/add-issue-labels/action.yml +50 -50
- package/loops/actions/agent-output.cjs +17 -17
- package/loops/actions/apply-agent-bundle/action.yml +24 -24
- package/loops/actions/apply-agent-comments/action.yml +42 -42
- package/loops/actions/apply-agent-labels/action.yml +55 -55
- package/loops/actions/apply-agent-output/action.yml +108 -108
- package/loops/actions/assess-blast-radius/action.yml +148 -0
- package/loops/actions/assess-blast-radius/assess-blast-radius.sh +138 -0
- package/loops/actions/classify-route/action.yml +100 -100
- package/loops/actions/cleanup-artifacts/action.yml +91 -91
- package/loops/actions/close-agent-issues/action.yml +43 -43
- package/loops/actions/create-agent-issues/action.yml +52 -52
- package/loops/actions/create-issue-comment/action.yml +29 -29
- package/loops/actions/download-agent-output/action.yml +53 -53
- package/loops/actions/housekeeping/action.yml +55 -2
- package/loops/actions/identify-gate-subject/action.yml +134 -122
- package/loops/actions/link-pr-to-issue/action.yml +40 -40
- package/loops/actions/list-open-issues/action.yml +33 -33
- package/loops/actions/load-issue-context/action.yml +45 -45
- package/loops/actions/merge-agent-pr/action.yml +49 -49
- package/loops/actions/push-agent-branch/action.yml +45 -45
- package/loops/actions/remove-issue-labels/action.yml +37 -37
- package/loops/actions/require-open-issue/action.yml +59 -0
- package/loops/actions/update-agent-issues/action.yml +58 -58
- package/loops/actions/validate-merge-gate-output/action.yml +62 -40
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +147 -31
- package/loops/actions/validate-refine-output/action.yml +48 -44
- package/loops/actions/validate-refine-output/validate-refine-output.sh +15 -4
- package/loops/actions/validate-review-output/action.yml +35 -35
- package/loops/actions/validate-triage-output/action.yml +36 -36
- package/loops/actions/verify-composite-actions/action.yml +9 -9
- package/loops/actions/verify-refine-output/action.yml +9 -9
- package/loops/actions/verify-refine-output/verify-refine-output.sh +6 -1
- package/loops/actions/verify-route-matrix/action.yml +9 -9
- package/loops/actions/verify-route-matrix/verify-gate-metrics.mjs +51 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +630 -8
- package/loops/scripts/compile-agent-workflows.mjs +331 -331
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/ci/app-ci-dotnet-next.yml +330 -330
- package/loops/templates/ci/app-ci-node-monorepo.yml +260 -260
- package/loops/templates/issues/bug_report.yml +109 -109
- package/loops/templates/issues/feature_request.yml +75 -75
- package/loops/templates/opencode/opencode.ci.json +55 -49
- package/loops/templates/opencode/opencode.ci.json.md +59 -49
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-apply-review.md +33 -3
- package/loops/workflows/agent-audit.md +36 -0
- package/loops/workflows/agent-implement.md +77 -5
- package/loops/workflows/agent-merge-gate.md +368 -148
- package/loops/workflows/agent-refine.md +93 -17
- package/loops/workflows/agent-release.md +4 -4
- package/loops/workflows/agent-triage.md +33 -1
- package/loops/workflows/authorize-bot-work.yml +105 -105
- package/loops/workflows/shared/opencode-ci.md +206 -206
- package/loops/workflows/shared/platform-defaults.md +19 -19
- package/loops/workflows/work-router.yml +25 -11
- 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: "
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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.
|
|
147
|
-
files: ${{ steps.
|
|
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.
|
|
211
|
+
holds_review: ${{ steps.blast.outputs.requires_review == 'true' && needs.subject.outputs.conclusion != 'failure' }}
|
|
154
212
|
steps:
|
|
155
|
-
- name:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
protected
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
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:
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 }}
|
|
361
|
-
|
|
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
|
-
|
|
380
|
-
|
|
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:
|
|
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
|
|
389
|
-
if: needs.validate_output.outputs.outcome
|
|
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:
|
|
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:
|
|
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 ${{
|
|
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:
|
|
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.
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
638
|
-
|
|
809
|
+
```
|
|
810
|
+
${{ needs.protected_changes.outputs.signals }}
|
|
811
|
+
```
|
|
639
812
|
|
|
640
|
-
- **success** → step 4, then step 5 (
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
|
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
|
-
|
|
851
|
+
reach a real disposition.
|
|
681
852
|
|
|
682
|
-
If the merge cannot be completed or the conflicts are genuinely ambiguous,
|
|
683
|
-
|
|
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.
|
|
860
|
+
5. Review the change. This is the part no deterministic check can do, so spend the run here.
|
|
689
861
|
|
|
690
|
-
**
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
703
|
-
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
**
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
|
|
718
|
-
|
|
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
|
-
|
|
721
|
-
|
|
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
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
**
|
|
728
|
-
|
|
729
|
-
|
|
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
|
|
779
|
-
stop looping:
|
|
780
|
-
|
|
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
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
and
|
|
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
|
-
|
|
1001
|
+
8. Emit exactly one `add_comment` targeting issue `${{ needs.subject.outputs.issue }}`,
|
|
1002
|
+
containing, in this order:
|
|
808
1003
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
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`.
|