@plainconceptsplatform/workflows 0.4.33 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +88 -88
  2. package/loops/actions/agent-output.cjs +17 -17
  3. package/loops/actions/audit-close/action.yml +24 -0
  4. package/loops/actions/classify-route/action.yml +4 -3
  5. package/loops/actions/classify-route/classify-route.sh +69 -29
  6. package/loops/actions/load-issue-context/action.yml +2 -0
  7. package/loops/actions/update-changelog/action.yml +113 -0
  8. package/loops/actions/validate-merge-gate-output/action.yml +40 -0
  9. package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +34 -0
  10. package/loops/actions/validate-refine-output/validate-refine-output.sh +16 -3
  11. package/loops/actions/validate-review-output/action.yml +35 -0
  12. package/loops/actions/validate-review-output/validate-review-output.sh +30 -0
  13. package/loops/actions/validate-triage-output/action.yml +36 -0
  14. package/loops/actions/validate-triage-output/validate-triage-output.sh +36 -0
  15. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +121 -36
  16. package/loops/scripts/compile-agent-workflows.mjs +195 -6
  17. package/loops/templates/agentics/agentics-checks.yml +86 -86
  18. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  19. package/loops/templates/ci/app-ci-dotnet-next.yml +268 -171
  20. package/loops/templates/ci/app-ci-node-monorepo.yml +208 -178
  21. package/loops/templates/issues/bug_report.yml +109 -109
  22. package/loops/templates/issues/feature_request.yml +75 -75
  23. package/loops/templates/opencode/opencode.ci.json +49 -47
  24. package/loops/templates/opencode/opencode.ci.json.md +46 -41
  25. package/loops/templates/release/github-release.yml +30 -30
  26. package/loops/workflows/agent-apply-review.md +465 -373
  27. package/loops/workflows/agent-audit.md +213 -197
  28. package/loops/workflows/agent-implement.md +538 -374
  29. package/loops/workflows/agent-merge-gate.md +730 -485
  30. package/loops/workflows/agent-refine.md +609 -379
  31. package/loops/workflows/agent-release.md +258 -0
  32. package/loops/workflows/agent-triage.md +447 -0
  33. package/loops/workflows/authorize-bot-work.yml +85 -81
  34. package/loops/workflows/shared/opencode-ci.md +206 -197
  35. package/loops/workflows/shared/platform-defaults.md +19 -16
  36. package/loops/workflows/work-router.yml +862 -551
  37. package/package.json +42 -42
  38. package/dist/catalog-installation.d.ts +0 -30
  39. package/dist/catalog-installation.js +0 -352
  40. package/dist/catalog-installation.test.d.ts +0 -1
  41. package/dist/catalog-installation.test.js +0 -448
  42. package/dist/catalog-listing.d.ts +0 -13
  43. package/dist/catalog-listing.js +0 -70
  44. package/dist/catalog-listing.test.d.ts +0 -1
  45. package/dist/catalog-listing.test.js +0 -150
  46. package/dist/index.d.ts +0 -2
  47. package/dist/index.js +0 -218
  48. package/dist/index.test.d.ts +0 -1
  49. package/dist/index.test.js +0 -273
  50. package/dist/repository-inspection.d.ts +0 -23
  51. package/dist/repository-inspection.js +0 -113
  52. package/dist/repository-inspection.test.d.ts +0 -1
  53. package/dist/repository-inspection.test.js +0 -77
  54. package/dist/repository-state.d.ts +0 -18
  55. package/dist/repository-state.js +0 -77
  56. package/dist/repository-state.test.d.ts +0 -1
  57. package/dist/repository-state.test.js +0 -96
  58. package/dist/route-processing.d.ts +0 -6
  59. package/dist/route-processing.js +0 -150
  60. package/dist/route-processing.test.d.ts +0 -1
  61. package/dist/route-processing.test.js +0 -342
  62. package/dist/stack-defaults.d.ts +0 -11
  63. package/dist/stack-defaults.js +0 -104
  64. package/dist/stack-defaults.test.d.ts +0 -1
  65. package/dist/stack-defaults.test.js +0 -266
  66. package/dist/tui.d.ts +0 -25
  67. package/dist/tui.js +0 -287
  68. package/dist/tui.test.d.ts +0 -1
  69. package/dist/tui.test.js +0 -246
  70. package/dist/workflow-catalog.d.ts +0 -25
  71. package/dist/workflow-catalog.js +0 -57
  72. package/dist/workflow-catalog.test.d.ts +0 -1
  73. package/dist/workflow-catalog.test.js +0 -29
  74. package/loops/workflows/agent-direct.md +0 -363
  75. package/loops/workflows/agent-propose.md +0 -342
@@ -1,485 +1,730 @@
1
- ---
2
- # Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-merge-gate.md. Update with `workflows update --force`; consumer edits may be overwritten.
3
- env:
4
- VERIFY_COMMANDS: "dotnet restore apps/api/Numa.slnx && dotnet build apps/api/Numa.slnx -c Release --no-restore && dotnet test apps/api/tests/Numa.UnitTests/Numa.UnitTests.csproj -c Release --no-build && dotnet test apps/api/tests/Numa.ParityTests/Numa.ParityTests.csproj -c Release --no-build"
5
- REPO_RULES: "Make a risk-based merge decision for the selected bot pull request. Merge only when CI is green and no risk indicators are present. Risk indicators for Numa: changes to QuoteCalculator or calculation waterfall, audit appender or hash chain, JWT issuance or permission checks, EF migrations or entity configurations, decimal precision or money handling, quote lifecycle transitions. Any of these require human review. Do not merge protected file changes."
6
- WORKING_LABEL: bot-working
7
- IMPLEMENT_LABEL: implement
8
- REVIEW_LABEL: review
9
- GATE_MARKER: "<!-- agent-merge-gate -->"
10
- INCOMPLETE_COMMENT: "Automated CI failure remediation ended without an outcome. The issue remains for a retry."
11
- ISSUE_CONTEXT_PATH: /tmp/gh-aw/agent/issue-context.json
12
- GH_AW_ALLOWED_BOTS: "platform-devbox[bot],github-actions[bot]"
13
- GIT_AUTHOR_NAME: "github-actions[bot]"
14
- GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
15
- GIT_COMMITTER_NAME: "github-actions[bot]"
16
- GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com"
17
- description: |
18
- Decides what happens to a bot-authored pull request once CI has reported: merge when the
19
- risk assessment is clean, hand to a human when it is not, fix CI when it failed. Called by
20
- the Work Router; does not trigger on public events.
21
-
22
- The router supplies the CI conclusion and run ID as facts, so there is no polling and no
23
- timeout branch. The conclusion is read from the inputs instead of filtered at the trigger.
24
-
25
- name: "Agent: Merge Gate"
26
-
27
- # Router-only worker. The Work Router owns triggers, classification, and rung 1-2 checks.
28
- # This workflow receives the classified inputs and runs rung 3+.
29
- imports:
30
- - github/gh-aw/.github/workflows/shared/opencode.md@v0.86.2
31
- - shared/platform-defaults.md
32
- - shared/opencode-ci.md
33
-
34
- on:
35
- workflow_call:
36
- inputs:
37
- pr-number:
38
- description: Pull request number to gate.
39
- required: true
40
- type: string
41
- linked-issue:
42
- description: Issue number the pull request closes. May be empty.
43
- required: false
44
- type: string
45
- ci-conclusion:
46
- description: CI conclusion (success, failure, action_required, cancelled, etc.).
47
- required: true
48
- type: string
49
- ci-run-id:
50
- description: CI workflow run ID for fetching failing logs.
51
- required: false
52
- type: string
53
-
54
- # Rung 4. Router has classified the event; identify-gate-subject validates PR ownership,
55
- # resolves the closing issue, and confirms the CI verdict.
56
- # A custom job, not `on.steps`, because the prompt and the precompute step need these
57
- # values and `on.steps` outputs do not reach the agent job.
58
- jobs:
59
- subject:
60
- runs-on: RunnerLandingZone
61
- permissions:
62
- contents: read
63
- issues: read
64
- pull-requests: read
65
- actions: read
66
- outputs:
67
- found: ${{ steps.subject.outputs.found }}
68
- pr: ${{ steps.subject.outputs.pr }}
69
- issue: ${{ steps.subject.outputs.issue }}
70
- conclusion: ${{ steps.subject.outputs.conclusion }}
71
- steps:
72
- - name: Checkout workflow actions
73
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
74
- with:
75
- persist-credentials: false
76
- - name: Identify the pull request, its issue, and the CI verdict
77
- id: subject
78
- uses: ./.github/actions/identify-gate-subject
79
- with:
80
- token: ${{ github.token }}
81
- pr-number: ${{ inputs.pr-number }}
82
- ci-conclusion: ${{ inputs.ci-conclusion }}
83
- linked-issue: ${{ inputs.linked-issue }}
84
- require-label: ${{ env.IMPLEMENT_LABEL }}
85
-
86
- protected_changes:
87
- needs: subject
88
- if: needs.subject.outputs.found == 'true'
89
- runs-on: RunnerLandingZone
90
- permissions:
91
- pull-requests: read
92
- outputs:
93
- requires_review: ${{ steps.files.outputs.requires_review }}
94
- files: ${{ steps.files.outputs.files }}
95
- steps:
96
- - name: Require review for protected pull request files
97
- id: files
98
- env:
99
- GH_TOKEN: ${{ github.token }}
100
- REPO: ${{ github.repository }}
101
- PR: ${{ needs.subject.outputs.pr }}
102
- run: |
103
- set -euo pipefail
104
- files=$(gh api --paginate "repos/$REPO/pulls/$PR/files?per_page=100" --jq '.[].filename')
105
- protected=$(printf '%s\n' "$files" | grep -E '^(\.|AGENTS\.md$|ARCHITECTURE\.md$|DESIGN\.md$|opencode\.jsonc$|package\.json$|pnpm-lock\.yaml$|Directory\.Packages\.props$|global\.json$)' || true)
106
-
107
- if [ -n "$protected" ]; then
108
- echo "requires_review=true" >> "$GITHUB_OUTPUT"
109
- {
110
- echo 'files<<EOF'
111
- printf '%s\n' "$protected"
112
- echo EOF
113
- } >> "$GITHUB_OUTPUT"
114
- else
115
- echo "requires_review=false" >> "$GITHUB_OUTPUT"
116
- echo "files=" >> "$GITHUB_OUTPUT"
117
- fi
118
-
119
- review_required:
120
- needs: [subject, protected_changes]
121
- if: needs.subject.outputs.found == 'true' && needs.protected_changes.outputs.requires_review == 'true'
122
- runs-on: RunnerLandingZone
123
- permissions:
124
- contents: read
125
- issues: write
126
- steps:
127
- - name: Checkout workflow actions
128
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
129
- with:
130
- persist-credentials: false
131
- - name: Create bot token
132
- id: app-token
133
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
134
- with:
135
- client-id: ${{ secrets.BOT_APP_ID }}
136
- private-key: ${{ secrets.BOT_PRIVATE_KEY }}
137
- - name: Release the issue
138
- uses: ./.github/actions/remove-issue-labels
139
- with:
140
- token: ${{ steps.app-token.outputs.token }}
141
- issue-number: ${{ needs.subject.outputs.issue }}
142
- labels: ${{ env.WORKING_LABEL }}
143
- - name: Flag human review
144
- uses: ./.github/actions/add-issue-labels
145
- with:
146
- token: ${{ steps.app-token.outputs.token }}
147
- issue-number: ${{ needs.subject.outputs.issue }}
148
- labels: ${{ env.REVIEW_LABEL }}
149
- - name: Explain the merge hold
150
- uses: ./.github/actions/create-issue-comment
151
- with:
152
- token: ${{ steps.app-token.outputs.token }}
153
- issue-number: ${{ needs.subject.outputs.issue }}
154
- body: |
155
- ${{ env.GATE_MARKER }}
156
- PR #${{ needs.subject.outputs.pr }} changes protected files and cannot be auto-merged.
157
- The `review` label is set: a human must merge this PR manually.
158
-
159
- Protected files:
160
- ${{ needs.protected_changes.outputs.files }}
161
-
162
- reserve:
163
- needs: subject
164
- if: needs.subject.outputs.found == 'true'
165
- runs-on: RunnerLandingZone
166
- permissions:
167
- contents: read
168
- issues: write
169
- pull-requests: read
170
- steps:
171
- - name: Checkout workflow actions
172
- if: needs.subject.outputs.conclusion == 'failure'
173
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
174
- with:
175
- persist-credentials: false
176
- - name: Create bot token
177
- if: needs.subject.outputs.conclusion == 'failure'
178
- id: app-token
179
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
180
- with:
181
- client-id: ${{ secrets.BOT_APP_ID }}
182
- private-key: ${{ secrets.BOT_PRIVATE_KEY }}
183
- - name: Check for merge conflicts
184
- if: needs.subject.outputs.conclusion == 'failure'
185
- id: conflicts
186
- env:
187
- GH_TOKEN: ${{ steps.app-token.outputs.token }}
188
- REPO: ${{ github.repository }}
189
- PR: ${{ needs.subject.outputs.pr }}
190
- run: |
191
- set -euo pipefail
192
- mergeable=$(gh pr view "$PR" --repo "$REPO" --json mergeable --jq '.mergeable')
193
- if [ "$mergeable" = "CONFLICTING" ]; then
194
- echo "has_conflicts=true" >> "$GITHUB_OUTPUT"
195
- else
196
- echo "has_conflicts=false" >> "$GITHUB_OUTPUT"
197
- fi
198
- - name: Comment on issue - problems found, solving them
199
- if: needs.subject.outputs.conclusion == 'failure'
200
- uses: ./.github/actions/create-issue-comment
201
- with:
202
- token: ${{ steps.app-token.outputs.token }}
203
- issue-number: ${{ needs.subject.outputs.issue }}
204
- body: |
205
- ${{ env.GATE_MARKER }}
206
- Problems found in PR #${{ needs.subject.outputs.pr }}. ${{ steps.conflicts.outputs.has_conflicts == 'true' && 'Merge conflicts detected.' || 'CI failed.' }}
207
- Bot is working on fixing it.
208
- conclude:
209
- needs: [activation, subject, protected_changes, agent, safe_outputs]
210
- if: >
211
- needs.agent.result == 'success' &&
212
- needs.safe_outputs.result == 'success' &&
213
- needs.protected_changes.outputs.requires_review != 'true'
214
- runs-on: RunnerLandingZone
215
- permissions:
216
- contents: write
217
- issues: write
218
- pull-requests: write
219
- steps:
220
- - name: Create bot token
221
- id: app-token
222
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
223
- with:
224
- client-id: ${{ secrets.BOT_APP_ID }}
225
- private-key: ${{ secrets.BOT_PRIVATE_KEY }}
226
- - name: Checkout repository
227
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
228
- with:
229
- token: ${{ steps.app-token.outputs.token }}
230
- fetch-depth: 0
231
- - name: Verify pull request closes the source issue
232
- continue-on-error: true
233
- uses: ./.github/actions/link-pr-to-issue
234
- with:
235
- token: ${{ steps.app-token.outputs.token }}
236
- pr-number: ${{ needs.subject.outputs.pr }}
237
- issue-number: ${{ needs.subject.outputs.issue }}
238
- - name: Apply agent output
239
- uses: ./.github/actions/apply-agent-output
240
- with:
241
- artifact-name: ${{ needs.activation.outputs.artifact_prefix }}agent
242
- token: ${{ steps.app-token.outputs.token }}
243
- merge-pull-request: 'true'
244
- push-to-branch: 'true'
245
- close-issues: 'true'
246
- incomplete:
247
- needs: [subject, protected_changes, agent, safe_outputs]
248
- if: >
249
- always() &&
250
- needs.subject.outputs.found == 'true' &&
251
- needs.protected_changes.outputs.requires_review != 'true' &&
252
- needs.agent.result != 'success'
253
- runs-on: RunnerLandingZone
254
- permissions:
255
- contents: read
256
- issues: write
257
- steps:
258
- - name: Checkout workflow actions
259
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
260
- with:
261
- persist-credentials: false
262
- - name: Create bot token
263
- id: app-token
264
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
265
- with:
266
- client-id: ${{ secrets.BOT_APP_ID }}
267
- private-key: ${{ secrets.BOT_PRIVATE_KEY }}
268
- - name: Release the issue
269
- uses: ./.github/actions/remove-issue-labels
270
- with:
271
- token: ${{ steps.app-token.outputs.token }}
272
- issue-number: ${{ needs.subject.outputs.issue }}
273
- labels: ${{ env.WORKING_LABEL }},${{ env.IMPLEMENT_LABEL }}
274
- - name: Flag for human review
275
- uses: ./.github/actions/add-issue-labels
276
- with:
277
- token: ${{ steps.app-token.outputs.token }}
278
- issue-number: ${{ needs.subject.outputs.issue }}
279
- labels: ${{ env.REVIEW_LABEL }}
280
- - name: Report missing remediation outcome
281
- uses: ./.github/actions/create-issue-comment
282
- with:
283
- token: ${{ steps.app-token.outputs.token }}
284
- issue-number: ${{ needs.subject.outputs.issue }}
285
- body: |
286
- ${{ env.GATE_MARKER }}
287
- Bot could not resolve PR #${{ needs.subject.outputs.pr }} automatically. The `review` label is set: a human must take over.
288
- ${{ env.INCOMPLETE_COMMENT }}
289
- [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
290
-
291
- agent:
292
- needs: protected_changes
293
- if: needs.protected_changes.outputs.requires_review != 'true'
294
-
295
- if: needs.subject.outputs.found == 'true' && needs.protected_changes.outputs.requires_review != 'true'
296
-
297
- runs-on: RunnerLandingZone
298
- runs-on-slim: RunnerLandingZone
299
-
300
- secrets:
301
- OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
302
-
303
- engine:
304
- id: opencode
305
- version: "1.2.14"
306
- env:
307
- OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
308
-
309
- model: openai/glm-5-2
310
-
311
- max-turns: 300
312
- max-turn-cache-misses: 3000
313
- max-ai-credits: 5000
314
-
315
- permissions: read-all
316
-
317
- # push-to-pull-request-branch with target "*" cannot reach a branch the shallow clone
318
- # does not have.
319
- checkout:
320
- fetch: ["*"]
321
- fetch-depth: 0
322
-
323
- # Rung 3. The diff is what the risk assessment reads, and the failing logs are what a fix
324
- # starts from. Both are known from the inputs, so neither costs the agent a turn.
325
- steps:
326
- - name: Load the issue context
327
- uses: ./.github/actions/load-issue-context
328
- with:
329
- token: ${{ github.token }}
330
- issue-number: ${{ needs.subject.outputs.issue }}
331
- output-path: ${{ env.ISSUE_CONTEXT_PATH }}
332
- - name: Fetch the diff and any failing job logs
333
- env:
334
- GH_TOKEN: ${{ github.token }}
335
- REPO: ${{ github.repository }}
336
- PR: ${{ needs.subject.outputs.pr }}
337
- RUN_ID: ${{ inputs.ci-run-id }}
338
- CONCLUSION: ${{ needs.subject.outputs.conclusion }}
339
- run: |
340
- set -euo pipefail
341
- mkdir -p /tmp/gh-aw/agent
342
- gh pr diff "$PR" --repo "$REPO" > /tmp/gh-aw/agent/diff.patch
343
- gh pr view "$PR" --repo "$REPO" --json title,body,files,additions,deletions \
344
- > /tmp/gh-aw/agent/pr.json
345
- if [ "$CONCLUSION" = "failure" ]; then
346
- gh run view "$RUN_ID" --repo "$REPO" --log-failed \
347
- > /tmp/gh-aw/agent/failed-logs.txt 2>/dev/null || \
348
- echo "logs unavailable" > /tmp/gh-aw/agent/failed-logs.txt
349
- gh run view "$RUN_ID" --repo "$REPO" --json jobs \
350
- --jq '[.jobs[] | select(.conclusion == "failure") | {name, conclusion}]' \
351
- > /tmp/gh-aw/agent/failed-jobs.json
352
- fi
353
-
354
- safe-outputs:
355
- staged: true
356
- threat-detection: false
357
- merge-pull-request:
358
- push-to-pull-request-branch:
359
- close-issue:
360
- add-comment:
361
- add-labels:
362
- remove-labels:
363
-
364
- timeout-minutes: 60
365
- ---
366
-
367
- 1. You are gating pull request **#${{ needs.subject.outputs.pr }}**, which closes issue
368
- **#${{ needs.subject.outputs.issue }}**. CI concluded
369
- **${{ needs.subject.outputs.conclusion }}**.
370
-
371
- It has already been confirmed that this is an open pull request we authored, that it closes
372
- an issue, and that the issue carries `implement`. Do not re-check any of that, and do not
373
- poll for checks: the conclusion above is the answer.
374
-
375
- 2. Read `${{ env.ISSUE_CONTEXT_PATH }}`. It contains the issue body and its discussion. When
376
- running `/repo-verify`, the acceptance criteria there define what the implementation must
377
- satisfy.
378
-
379
- 3. Branch on the conclusion.
380
-
381
- - **success** → step 4.
382
- - **action_required** CI did not run because the workflow needs approval.
383
- Call `remove_labels` to remove `bot-working` (item_number:
384
- ${{ needs.subject.outputs.issue }}), then `add_labels` to add `review`
385
- (item_number: ${{ needs.subject.outputs.issue }}), and `add_comment`
386
- (item_number: ${{ needs.subject.outputs.issue }}) saying CI requires
387
- workflow approval before it can run, and a maintainer must approve the
388
- pending run. Then stop , do not attempt to merge or fix.
389
- - **failure** → step 7.
390
- - **cancelled, timed_out, or anything else** → call `add_comment` with
391
- `item_number: ${{ needs.subject.outputs.issue }}` saying CI did not produce a verdict,
392
- then `add_labels` to add `review` (item_number: the issue), and stop.
393
- A cancelled or unknown run is not evidence of anything.
394
-
395
- Follow repository documentation and established conventions when assessing or remediating
396
- the pull request. Protect secrets, do not bypass checks, and keep remediation focused.
397
- Adhere to ${{ env.REPO_RULES }}.
398
-
399
- 4. Assess the risk of merging, as a reviewer would. Read `/tmp/gh-aw/agent/diff.patch` in full
400
- and `/tmp/gh-aw/agent/pr.json` for the shape of the change. Flag it as risky when any of
401
- these hold, and say which:
402
-
403
- - It touches authentication, authorization, secrets, credentials, or security boundaries.
404
- - It changes a repository-defined integration or cross-platform boundary.
405
- - It changes a public API or a published package's contract.
406
- - It deletes or weakens a test, or lowers a threshold.
407
- - It changes CI, CD or a workflow file.
408
- - It changes a protected file. This is normally handled before you run, but never merge one
409
- if it reaches this gate.
410
- - The diff is materially larger than the issue implied.
411
- - You are simply not confident. Low confidence is itself a flag.
412
-
413
- 5. **Clean assessment** → call `merge_pull_request` (pr_number:
414
- ${{ needs.subject.outputs.pr }}), then `close_issue` (item_number:
415
- ${{ needs.subject.outputs.issue }}), then `remove_labels` (item_number:
416
- ${{ needs.subject.outputs.issue }}) to remove `implement`, `bot-working`, and `review`.
417
- Call `add_comment` (item_number: ${{ needs.subject.outputs.issue }}):
418
- `Auto-merged. PR #${{ needs.subject.outputs.pr }} closed.`
419
-
420
- 6. **Flagged** → do not merge. Call `remove_labels` to remove `bot-working`
421
- (item_number: ${{ needs.subject.outputs.issue }}), then `add_labels` to add `review`
422
- (item_number: ${{ needs.subject.outputs.issue }}), and `add_comment`
423
- (item_number: ${{ needs.subject.outputs.issue }}) explaining exactly which criterion
424
- tripped and what a reviewer should look at. Then `add_comment`
425
- (item_number: ${{ needs.subject.outputs.pr }}) on the PR with the same message.
426
- Leave `implement` in place: the work is not finished until a human merges it.
427
-
428
- 7. **CI failed** → read `/tmp/gh-aw/agent/failed-jobs.json` and
429
- `/tmp/gh-aw/agent/failed-logs.txt`, which are already on disk. Load only skills required to
430
- fix the actual cause. Run these verification commands before a push. Do not weaken a test,
431
- disable a check, or push an unverified guess.
432
-
433
- ```
434
- ${{ env.VERIFY_COMMANDS }}
435
- ```
436
-
437
- Call `push_to_pull_request_branch` (pr_number: ${{ needs.subject.outputs.pr }}) to push
438
- the fix, then `remove_labels` (item_number: ${{ needs.subject.outputs.issue }}) to remove
439
- `bot-working`. CI will run again and trigger you again with the new result.
440
-
441
- If you cannot fix it, or the logs show you have already tried on this same head commit,
442
- stop looping: `remove_labels` (item_number: ${{ needs.subject.outputs.issue }}) to remove
443
- `implement` and `bot-working`, `add_labels` (item_number:
444
- ${{ needs.subject.outputs.issue }}) to add `review`, and `add_comment` (item_number:
445
- ${{ needs.subject.outputs.issue }}) with the failure and what you tried. The `implement`
446
- label is removed so retries do not create duplicate PRs. A human decides from there.
447
-
448
- 8. Never merge with administrator privileges and never bypass a required check. If the merge
449
- is refused, that refusal is the answer: `add_labels` to add `review`
450
- (item_number: ${{ needs.subject.outputs.issue }}), `add_comment`
451
- (item_number: ${{ needs.subject.outputs.issue }}) with the refusal, and leave it for a human.
452
-
453
- ## Diagram
454
-
455
- ```mermaid
456
- flowchart TD
457
- gateStart("Work Router<br/>merge-gate route<br/>(CI completed)") --> gateSubject
458
- gateSubject["Subject (rung 4)<br/>Our PR? Closes an implement issue?"] -->|✓| gateFacts
459
- gateSubject -.->|✗| gateIdle
460
- gateFacts("Facts (rung 3)<br/>Diff, PR shape, failing logs") --> gateCi
461
- gateCi["CI<br/>What did it conclude?"] -->|success| gateRisk
462
- gateCi -.->|failure| gateFix
463
- gateCi -.->|no verdict| gateHuman
464
- gateRisk["Risk<br/>Auth, schema, API, tests, CI, size, doubt"] -->|clean| gateMerge
465
- gateRisk -.->|flagged| gateHuman
466
- gateFix("Fix<br/>Read logs, fix the cause, /repo-verify") -->|pushed| gateWait
467
- gateFix -.->|cannot fix| gateHuman
468
- gateMerge(("Merged<br/>Issue closed, review+labels removed"))
469
- gateWait(("Pushed<br/>CI will re-run and re-trigger via Router"))
470
- gateHuman(("Review<br/>review label, reason explained"))
471
- gateIdle(("Idle<br/>Not our pull request"))
472
-
473
- classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
474
- classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
475
- classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
476
- classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
477
- classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
478
- classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
479
- class gateStart start
480
- class gateFacts,gateFix action
481
- class gateSubject,gateCi,gateRisk decision
482
- class gateIdle,gateWait idle
483
- class gateHuman failure
484
- class gateMerge success
485
- ```
1
+ ---
2
+ # Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-merge-gate.md. Update with `workflows update --force`; consumer edits may be overwritten.
3
+ env:
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 indicators are present. Review risk indicators defined in the repository's guardrails or project documentation. Any of these require human review. Do not merge protected file changes."
6
+ WORKING_LABEL: bot-working
7
+ IMPLEMENT_LABEL: implement
8
+ REVIEW_LABEL: review
9
+ PR_PENDING_LABEL: pr-pending
10
+ GATE_MARKER: "<!-- agent-merge-gate -->"
11
+ INCOMPLETE_COMMENT: "Automated CI failure remediation ended without an outcome. The issue remains for a retry."
12
+ ISSUE_CONTEXT_PATH: /tmp/gh-aw/agent/issue-context.json
13
+ GH_AW_ALLOWED_BOTS: "platform-devbox[bot],github-actions[bot]"
14
+ GIT_AUTHOR_NAME: "github-actions[bot]"
15
+ GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
16
+ GIT_COMMITTER_NAME: "github-actions[bot]"
17
+ GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com"
18
+ description: |
19
+ Decides what happens to a bot-authored pull request once CI has reported: merge when the
20
+ risk assessment is clean, hand to a human when it is not, fix CI when it failed. Called by
21
+ the Work Router; does not trigger on public events.
22
+
23
+ The router supplies the CI conclusion and run ID as facts, so there is no polling and no
24
+ timeout branch. The conclusion is read from the inputs instead of filtered at the trigger.
25
+
26
+ name: "Agent: Merge Gate"
27
+
28
+ # Router-only worker. The Work Router owns triggers, classification, and rung 1-2 checks.
29
+ # This workflow receives the classified inputs and runs rung 3+.
30
+ imports:
31
+ - github/gh-aw/.github/workflows/shared/opencode.md@v0.87.5
32
+ - shared/platform-defaults.md
33
+ - shared/opencode-ci.md
34
+
35
+ on:
36
+ workflow_call:
37
+ inputs:
38
+ pr-number:
39
+ description: Pull request number to gate.
40
+ required: true
41
+ type: string
42
+ linked-issue:
43
+ description: Issue number the pull request closes. May be empty.
44
+ required: false
45
+ type: string
46
+ ci-conclusion:
47
+ description: CI conclusion (success, failure, action_required, cancelled, etc.).
48
+ required: true
49
+ type: string
50
+ ci-run-id:
51
+ description: CI workflow run ID for fetching failing logs.
52
+ required: false
53
+ type: string
54
+
55
+ # Rung 4. Router has classified the event; identify-gate-subject validates PR ownership,
56
+ # resolves the closing issue, and confirms the CI verdict.
57
+ # A custom job, not `on.steps`, because the prompt and the precompute step need these
58
+ # values and `on.steps` outputs do not reach the agent job.
59
+ jobs:
60
+ subject:
61
+ runs-on: agents-arc
62
+ permissions:
63
+ contents: read
64
+ issues: read
65
+ pull-requests: read
66
+ actions: read
67
+ outputs:
68
+ found: ${{ steps.subject.outputs.found }}
69
+ pr: ${{ steps.subject.outputs.pr }}
70
+ issue: ${{ steps.subject.outputs.issue }}
71
+ conclusion: ${{ steps.subject.outputs.conclusion }}
72
+ run-id: ${{ steps.subject.outputs.run-id }}
73
+ review_blocked: ${{ steps.review.outputs.review_blocked }}
74
+ steps:
75
+ - name: Checkout workflow actions
76
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
77
+ with:
78
+ persist-credentials: false
79
+ - name: Identify the pull request, its issue, and the CI verdict
80
+ id: subject
81
+ uses: ./.github/actions/identify-gate-subject
82
+ with:
83
+ token: ${{ github.token }}
84
+ pr-number: ${{ inputs.pr-number }}
85
+ ci-conclusion: ${{ inputs.ci-conclusion }}
86
+ linked-issue: ${{ inputs.linked-issue }}
87
+ require-label: ${{ env.IMPLEMENT_LABEL }}
88
+ - name: Block a pull request with requested changes
89
+ id: review
90
+ env:
91
+ GH_TOKEN: ${{ github.token }}
92
+ REPO: ${{ github.repository }}
93
+ PR: ${{ steps.subject.outputs.pr }}
94
+ run: |
95
+ set -euo pipefail
96
+ # A transient API failure must not kill the gate (the reconcile cron can push the
97
+ # token into secondary rate limits). Three tries, then default to not blocked; the
98
+ # agent re-reads the review state itself before any merge.
99
+ decision=""
100
+ for _ in 1 2 3; do
101
+ if decision=$(gh pr view "$PR" --repo "$REPO" --json reviewDecision --jq '.reviewDecision // ""'); then
102
+ break
103
+ fi
104
+ sleep 5
105
+ done
106
+ echo "review_blocked=$([ "$decision" = 'CHANGES_REQUESTED' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
107
+
108
+ protected_changes:
109
+ needs: subject
110
+ if: needs.subject.outputs.found == 'true'
111
+ runs-on: agents-arc
112
+ permissions:
113
+ pull-requests: read
114
+ outputs:
115
+ requires_review: ${{ steps.files.outputs.requires_review }}
116
+ files: ${{ steps.files.outputs.files }}
117
+ steps:
118
+ - name: Require review for protected pull request files
119
+ id: files
120
+ env:
121
+ GH_TOKEN: ${{ github.token }}
122
+ REPO: ${{ github.repository }}
123
+ PR: ${{ needs.subject.outputs.pr }}
124
+ run: |
125
+ set -euo pipefail
126
+ files=$(gh api --paginate "repos/$REPO/pulls/$PR/files?per_page=100" --jq '.[].filename')
127
+ protected=$(printf '%s\n' "$files" | grep -E '^(\.|AGENTS\.md$|ARCHITECTURE\.md$|opencode\.jsonc$|package\.json$|pnpm-lock\.yaml$|Directory\.Packages\.props$|global\.json$)' || true)
128
+
129
+ if [ -n "$protected" ]; then
130
+ echo "requires_review=true" >> "$GITHUB_OUTPUT"
131
+ {
132
+ echo 'files<<EOF'
133
+ printf '%s\n' "$protected"
134
+ echo EOF
135
+ } >> "$GITHUB_OUTPUT"
136
+ else
137
+ echo "requires_review=false" >> "$GITHUB_OUTPUT"
138
+ echo "files=" >> "$GITHUB_OUTPUT"
139
+ fi
140
+
141
+ review_required:
142
+ needs: [subject, protected_changes]
143
+ # gh-aw makes the agent depend on custom jobs. Keep this job successful when
144
+ # there are no protected files instead of skipping it and blocking remediation.
145
+ if: always() && needs.subject.outputs.found == 'true'
146
+ runs-on: agents-arc
147
+ permissions:
148
+ contents: read
149
+ issues: write
150
+ steps:
151
+ - name: Checkout workflow actions
152
+ if: needs.protected_changes.outputs.requires_review == 'true' && needs.subject.outputs.conclusion != 'failure'
153
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
154
+ with:
155
+ persist-credentials: false
156
+ - name: Create bot token
157
+ if: needs.protected_changes.outputs.requires_review == 'true' && needs.subject.outputs.conclusion != 'failure'
158
+ id: app-token
159
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
160
+ with:
161
+ client-id: ${{ secrets.BOT_APP_ID }}
162
+ private-key: ${{ secrets.BOT_PRIVATE_KEY }}
163
+ - name: Release the issue
164
+ if: needs.protected_changes.outputs.requires_review == 'true' && needs.subject.outputs.conclusion != 'failure'
165
+ uses: ./.github/actions/remove-issue-labels
166
+ with:
167
+ token: ${{ steps.app-token.outputs.token }}
168
+ issue-number: ${{ needs.subject.outputs.issue }}
169
+ labels: ${{ env.WORKING_LABEL }},${{ env.PR_PENDING_LABEL }}
170
+ - name: Flag human review
171
+ if: needs.protected_changes.outputs.requires_review == 'true' && needs.subject.outputs.conclusion != 'failure'
172
+ uses: ./.github/actions/add-issue-labels
173
+ with:
174
+ token: ${{ steps.app-token.outputs.token }}
175
+ issue-number: ${{ needs.subject.outputs.issue }}
176
+ labels: ${{ env.REVIEW_LABEL }}
177
+ - name: Explain the merge hold
178
+ if: needs.protected_changes.outputs.requires_review == 'true' && needs.subject.outputs.conclusion != 'failure'
179
+ uses: ./.github/actions/create-issue-comment
180
+ with:
181
+ token: ${{ steps.app-token.outputs.token }}
182
+ issue-number: ${{ needs.subject.outputs.issue }}
183
+ body: |
184
+ ${{ env.GATE_MARKER }}
185
+ PR #${{ needs.subject.outputs.pr }} changes protected files and cannot be auto-merged.
186
+ The `review` label is set: a human must merge this PR manually.
187
+
188
+ Protected files:
189
+ ${{ needs.protected_changes.outputs.files }}
190
+
191
+ reserve:
192
+ needs: subject
193
+ if: needs.subject.outputs.found == 'true'
194
+ runs-on: agents-arc
195
+ permissions:
196
+ contents: read
197
+ issues: write
198
+ pull-requests: read
199
+ outputs:
200
+ has_conflicts: ${{ steps.conflicts.outputs.has_conflicts || 'false' }}
201
+ steps:
202
+ - name: Checkout workflow actions
203
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
204
+ with:
205
+ persist-credentials: false
206
+ - name: Create bot token
207
+ id: app-token
208
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
209
+ with:
210
+ client-id: ${{ secrets.BOT_APP_ID }}
211
+ private-key: ${{ secrets.BOT_PRIVATE_KEY }}
212
+ - name: Check for merge conflicts
213
+ id: conflicts
214
+ env:
215
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
216
+ REPO: ${{ github.repository }}
217
+ PR: ${{ needs.subject.outputs.pr }}
218
+ run: |
219
+ set -euo pipefail
220
+ mergeable=$(gh pr view "$PR" --repo "$REPO" --json mergeable --jq '.mergeable')
221
+ if [ "$mergeable" = "CONFLICTING" ]; then
222
+ echo "has_conflicts=true" >> "$GITHUB_OUTPUT"
223
+ else
224
+ echo "has_conflicts=false" >> "$GITHUB_OUTPUT"
225
+ fi
226
+ - name: Comment on issue - problems found, solving them
227
+ if: needs.subject.outputs.conclusion == 'failure'
228
+ uses: ./.github/actions/create-issue-comment
229
+ with:
230
+ token: ${{ steps.app-token.outputs.token }}
231
+ issue-number: ${{ needs.subject.outputs.issue }}
232
+ body: |
233
+ ${{ env.GATE_MARKER }}
234
+ Problems found in PR #${{ needs.subject.outputs.pr }}. ${{ steps.conflicts.outputs.has_conflicts == 'true' && 'Merge conflicts detected.' || 'CI failed.' }}
235
+ Bot is working on fixing it.
236
+ validate_output:
237
+ needs: [activation, subject, agent, safe_outputs]
238
+ if: >
239
+ always() &&
240
+ needs.agent.result == 'success' &&
241
+ needs.safe_outputs.result == 'success'
242
+ runs-on: agents-arc
243
+ permissions:
244
+ contents: read
245
+ outputs:
246
+ valid: ${{ steps.validate.outputs.valid }}
247
+ outcome: ${{ steps.validate.outputs.outcome }}
248
+ steps:
249
+ - name: Checkout workflow actions
250
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
251
+ with:
252
+ persist-credentials: false
253
+ - name: Download agent output
254
+ id: output
255
+ uses: ./.github/actions/download-agent-output
256
+ with:
257
+ artifact-name: ${{ needs.activation.outputs.artifact_prefix }}agent
258
+ - name: Validate merge-gate outcome
259
+ id: validate
260
+ uses: ./.github/actions/validate-merge-gate-output
261
+ with:
262
+ output-file: ${{ steps.output.outputs.output-file }}
263
+ issue-number: ${{ needs.subject.outputs.issue }}
264
+ ci-conclusion: ${{ needs.subject.outputs.conclusion }}
265
+ conclude:
266
+ needs: [activation, subject, protected_changes, agent, safe_outputs, validate_output]
267
+ if: >
268
+ needs.agent.result == 'success' &&
269
+ needs.safe_outputs.result == 'success' &&
270
+ needs.validate_output.outputs.valid == 'true' &&
271
+ (needs.protected_changes.outputs.requires_review != 'true' || needs.validate_output.outputs.outcome != 'merge')
272
+ runs-on: agents-arc
273
+ permissions:
274
+ contents: write
275
+ issues: write
276
+ pull-requests: write
277
+ steps:
278
+ - name: Create bot token
279
+ id: app-token
280
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
281
+ with:
282
+ client-id: ${{ secrets.BOT_APP_ID }}
283
+ private-key: ${{ secrets.BOT_PRIVATE_KEY }}
284
+ - name: Checkout repository
285
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
286
+ with:
287
+ token: ${{ steps.app-token.outputs.token }}
288
+ fetch-depth: 0
289
+ - name: Verify pull request closes the source issue
290
+ continue-on-error: true
291
+ uses: ./.github/actions/link-pr-to-issue
292
+ with:
293
+ token: ${{ steps.app-token.outputs.token }}
294
+ pr-number: ${{ needs.subject.outputs.pr }}
295
+ issue-number: ${{ needs.subject.outputs.issue }}
296
+ - name: Apply agent output
297
+ uses: ./.github/actions/apply-agent-output
298
+ with:
299
+ artifact-name: ${{ needs.activation.outputs.artifact_prefix }}agent
300
+ token: ${{ steps.app-token.outputs.token }}
301
+ push-to-branch: 'true'
302
+ apply-labels: 'false'
303
+ - name: Merge approved pull request
304
+ if: needs.validate_output.outputs.outcome == 'merge'
305
+ env:
306
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
307
+ REPO: ${{ github.repository }}
308
+ PR: ${{ needs.subject.outputs.pr }}
309
+ run: |
310
+ set -euo pipefail
311
+ head_sha=$(gh pr view "$PR" --repo "$REPO" --json headRefOid --jq '.headRefOid')
312
+ gh pr merge "$PR" --repo "$REPO" --squash --match-head-commit "$head_sha"
313
+ - name: Release remediated issue
314
+ if: needs.validate_output.outputs.outcome == 'remediated'
315
+ uses: ./.github/actions/remove-issue-labels
316
+ with:
317
+ token: ${{ steps.app-token.outputs.token }}
318
+ issue-number: ${{ needs.subject.outputs.issue }}
319
+ labels: ${{ env.WORKING_LABEL }}
320
+ - name: Flag review outcome
321
+ if: needs.validate_output.outputs.outcome == 'review'
322
+ uses: ./.github/actions/add-issue-labels
323
+ with:
324
+ token: ${{ steps.app-token.outputs.token }}
325
+ issue-number: ${{ needs.subject.outputs.issue }}
326
+ labels: ${{ env.REVIEW_LABEL }}
327
+ - name: Release review outcome
328
+ if: needs.validate_output.outputs.outcome == 'review'
329
+ uses: ./.github/actions/remove-issue-labels
330
+ with:
331
+ token: ${{ steps.app-token.outputs.token }}
332
+ issue-number: ${{ needs.subject.outputs.issue }}
333
+ labels: ${{ env.WORKING_LABEL }},${{ env.PR_PENDING_LABEL }}
334
+ - name: Clear merged issue labels
335
+ if: needs.validate_output.outputs.outcome == 'merge'
336
+ uses: ./.github/actions/remove-issue-labels
337
+ with:
338
+ token: ${{ steps.app-token.outputs.token }}
339
+ issue-number: ${{ needs.subject.outputs.issue }}
340
+ labels: ${{ env.IMPLEMENT_LABEL }},${{ env.WORKING_LABEL }},${{ env.REVIEW_LABEL }},${{ env.PR_PENDING_LABEL }}
341
+ incomplete:
342
+ needs: [subject, protected_changes, agent, safe_outputs, validate_output]
343
+ if: >
344
+ always() &&
345
+ needs.subject.outputs.found == 'true' &&
346
+ (needs.protected_changes.outputs.requires_review != 'true' || needs.subject.outputs.conclusion == 'failure') &&
347
+ (
348
+ needs.agent.result != 'success' ||
349
+ needs.safe_outputs.result != 'success' ||
350
+ needs.validate_output.outputs.valid != 'true'
351
+ )
352
+ runs-on: agents-arc
353
+ permissions:
354
+ contents: read
355
+ issues: write
356
+ steps:
357
+ - name: Checkout workflow actions
358
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
359
+ with:
360
+ persist-credentials: false
361
+ - name: Create bot token
362
+ id: app-token
363
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
364
+ with:
365
+ client-id: ${{ secrets.BOT_APP_ID }}
366
+ private-key: ${{ secrets.BOT_PRIVATE_KEY }}
367
+ - name: Release the issue
368
+ uses: ./.github/actions/remove-issue-labels
369
+ with:
370
+ token: ${{ steps.app-token.outputs.token }}
371
+ issue-number: ${{ needs.subject.outputs.issue }}
372
+ labels: ${{ env.WORKING_LABEL }},${{ env.IMPLEMENT_LABEL }},${{ env.PR_PENDING_LABEL }}
373
+ - name: Flag for human review
374
+ uses: ./.github/actions/add-issue-labels
375
+ with:
376
+ token: ${{ steps.app-token.outputs.token }}
377
+ issue-number: ${{ needs.subject.outputs.issue }}
378
+ labels: ${{ env.REVIEW_LABEL }}
379
+ - name: Report missing remediation outcome
380
+ uses: ./.github/actions/create-issue-comment
381
+ with:
382
+ token: ${{ steps.app-token.outputs.token }}
383
+ issue-number: ${{ needs.subject.outputs.issue }}
384
+ body: |
385
+ ${{ env.GATE_MARKER }}
386
+ Bot could not resolve PR #${{ needs.subject.outputs.pr }} automatically. The `review` label is set: a human must take over.
387
+ ${{ env.INCOMPLETE_COMMENT }}
388
+ [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
389
+
390
+ agent:
391
+ # The top-level guard reads both outputs. GitHub Actions does not make a
392
+ # dependency's dependencies available through `needs` transitively.
393
+ needs: [subject, protected_changes]
394
+ if: always() && (needs.protected_changes.outputs.requires_review != 'true' || needs.subject.outputs.conclusion == 'failure') && needs.subject.outputs.review_blocked != 'true'
395
+
396
+ if: always() && needs.subject.outputs.found == 'true' && (needs.protected_changes.outputs.requires_review != 'true' || needs.subject.outputs.conclusion == 'failure') && needs.subject.outputs.review_blocked != 'true'
397
+
398
+ runs-on: agents-arc
399
+ runs-on-slim: agents-arc
400
+
401
+ secrets:
402
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
403
+
404
+ engine:
405
+ id: opencode
406
+ version: "1.2.14"
407
+ env:
408
+ OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
409
+
410
+ model: openai/glm-5-3
411
+
412
+ max-turns: 300
413
+ max-turn-cache-misses: 3000
414
+ max-ai-credits: 5000
415
+
416
+ permissions: read-all
417
+
418
+ # push-to-pull-request-branch with target "*" cannot reach a branch the shallow clone
419
+ # does not have.
420
+ checkout:
421
+ fetch: ["*"]
422
+ fetch-depth: 0
423
+
424
+ # Rung 3. The diff is what the risk assessment reads, and the failing logs are what a fix
425
+ # starts from. Both are known from the inputs, so neither costs the agent a turn.
426
+ steps:
427
+ - name: Load the issue context
428
+ uses: ./.github/actions/load-issue-context
429
+ with:
430
+ token: ${{ github.token }}
431
+ issue-number: ${{ needs.subject.outputs.issue }}
432
+ output-path: ${{ env.ISSUE_CONTEXT_PATH }}
433
+ - name: Fetch the diff and any failing job logs
434
+ env:
435
+ GH_TOKEN: ${{ github.token }}
436
+ REPO: ${{ github.repository }}
437
+ PR: ${{ needs.subject.outputs.pr }}
438
+ RUN_ID: ${{ needs.subject.outputs.run-id }}
439
+ CONCLUSION: ${{ needs.subject.outputs.conclusion }}
440
+ run: |
441
+ set -euo pipefail
442
+ mkdir -p /tmp/gh-aw/agent
443
+ gh pr diff "$PR" --repo "$REPO" > /tmp/gh-aw/agent/diff.patch
444
+ gh pr view "$PR" --repo "$REPO" --json title,body,files,additions,deletions \
445
+ > /tmp/gh-aw/agent/pr.json
446
+ if [ "$CONCLUSION" = "failure" ] && [ -n "$RUN_ID" ]; then
447
+ gh run view "$RUN_ID" --repo "$REPO" --log-failed \
448
+ > /tmp/gh-aw/agent/failed-logs.txt 2>/dev/null || \
449
+ echo "logs unavailable" > /tmp/gh-aw/agent/failed-logs.txt
450
+ gh run view "$RUN_ID" --repo "$REPO" --json jobs \
451
+ --jq '[.jobs[] | select(.conclusion == "failure") | {name, conclusion}]' \
452
+ > /tmp/gh-aw/agent/failed-jobs.json
453
+ elif [ "$CONCLUSION" = "failure" ]; then
454
+ echo "[]" > /tmp/gh-aw/agent/failed-jobs.json
455
+ echo "CI run ID unavailable, cannot fetch logs" > /tmp/gh-aw/agent/failed-logs.txt
456
+ fi
457
+
458
+ safe-outputs:
459
+ # A failed run is already a red run. An issue per failure buries the real backlog
460
+ # under noise nobody closes.
461
+ report-failure-as-issue: false
462
+ threat-detection: false
463
+ # target "*" because these workers are dispatched, not triggered by the pull request:
464
+ # the default "triggering" target has no pull request in context and rejects the push
465
+ # with "requires pull request context", so the agent's fix is computed and discarded.
466
+ push-to-pull-request-branch:
467
+ target: "*"
468
+ required-title-prefix: "[bot] "
469
+ # A failed bot PR may already contain protected files. Permit a verified repair push,
470
+ # but protected_changes still prevents the later green-CI cycle from auto-merging it.
471
+ protected-files: allowed
472
+ add-comment:
473
+ target: "*"
474
+
475
+ timeout-minutes: 60
476
+ ---
477
+
478
+ 1. You are gating pull request **#${{ needs.subject.outputs.pr }}**, which closes issue
479
+ **#${{ needs.subject.outputs.issue }}**. CI concluded
480
+ **${{ needs.subject.outputs.conclusion }}**.
481
+
482
+ It has already been confirmed that this is an open pull request we authored, that it closes
483
+ an issue, and that the issue carries `implement`. Do not re-check any of that, and do not
484
+ poll for checks: the conclusion above is the answer.
485
+
486
+ 2. Read `${{ env.ISSUE_CONTEXT_PATH }}`. It contains the issue body and its discussion. When
487
+ running `/repo-verify`, the acceptance criteria there define what the implementation must
488
+ satisfy.
489
+
490
+ 3. Branch on the conclusion.
491
+
492
+ First check the issue context for `<!-- complexity: trivial -->`.
493
+
494
+ **If the trivial marker is present AND CI conclusion is success:**
495
+ Skip the full assessment (step 5). Emit a minimal assessment table with all checks marked
496
+ ✅ and the note "Trivial change, CI green — deep risk review skipped." Then proceed directly
497
+ to step 8 (merge verdict).
498
+
499
+ **If the trivial marker is absent OR CI is not success:**
500
+ Follow the normal branching below.
501
+
502
+ - **success** → step 4, then step 5 (full assessment).
503
+ - **action_required** → CI did not run because the workflow needs approval.
504
+ Emit the assessment table with ❌ on CI Status and note that a maintainer must approve
505
+ the pending run. Select the `review` verdict.
506
+ - **failure** → step 4, then step 6 (CI remediation).
507
+ - **cancelled, timed_out, or anything else** → Emit the assessment table with ❌ on CI
508
+ Status and select the `review` verdict. A cancelled or unknown run is not evidence of
509
+ anything.
510
+
511
+ Follow repository documentation and established conventions when assessing or remediating
512
+ the pull request. Protect secrets, do not bypass checks, and keep remediation focused.
513
+ Adhere to ${{ env.REPO_RULES }}.
514
+
515
+ 4. Read the diff and PR metadata. Read `/tmp/gh-aw/agent/diff.patch` in full and
516
+ `/tmp/gh-aw/agent/pr.json` for the shape of the change. If CI failed, also read
517
+ `/tmp/gh-aw/agent/failed-jobs.json` and `/tmp/gh-aw/agent/failed-logs.txt`.
518
+
519
+ These files are the factual basis for every check below. Do not guess — cite what you read.
520
+
521
+ 4b. **Merge conflict when CI is green.** If the conclusion is `success` and
522
+ `has_conflicts` is `true` (current value: `${{ needs.reserve.outputs.has_conflicts }}`),
523
+ resolve the conflict before assessing risk. You are already on the PR branch.
524
+ Rebase onto `origin/${{ github.event.repository.default_branch }}`
525
+ (`git rebase origin/${{ github.event.repository.default_branch }}`), resolve every
526
+ conflict deliberately, and run the verification commands below. Do not use `--ours`,
527
+ `--theirs`, or a blanket conflict-marker deletion without reviewing the intended
528
+ behavior from both sides.
529
+
530
+ Scope verification to the files the rebase touched: pass changed file paths to
531
+ lint/format tools instead of running them repository-wide (see step 6's scoped
532
+ verification guidance).
533
+
534
+ ```
535
+ ${{ env.VERIFY_COMMANDS }}
536
+ ```
537
+
538
+ Push the rebased branch using `push_to_pull_request_branch` (pr_number: ${{ needs.subject.outputs.pr }},
539
+ branch: the current PR branch), then emit the `add_comment` with
540
+ **Verdict:** remediated. CI will re-run on the updated branch and the merge gate
541
+ will be triggered again — the next cycle will see a clean, conflict-free PR and can
542
+ make a proper merge or review decision.
543
+
544
+ If the rebase fails or the conflicts are genuinely ambiguous, select the `review`
545
+ verdict instead and explain which conflicts could not be resolved safely.
546
+
547
+ If the conclusion is `success` and `has_conflicts` is `false`, skip this step and
548
+ proceed to step 5.
549
+
550
+ 5. Run each of these 10 checks. For each, determine a status and a short detail line.
551
+
552
+ **Check 1 — CI Status.** What did CI conclude? Success means all required checks passed.
553
+ Failure means at least one job failed. Action required means a workflow needs approval.
554
+ Flag any non-success conclusion.
555
+
556
+ **Check 2 — Auth & Security.** Does the diff touch authentication, authorization, secrets,
557
+ credentials, or security boundaries? Flag any change to auth middleware, permission checks,
558
+ token issuance, or security-related config.
559
+
560
+ **Check 3 — API & Contracts.** Does the diff change a public API or a published package's
561
+ contract? Flag changes to endpoint signatures, DTO shapes, exported interfaces, or
562
+ serialization formats that could break consumers.
563
+
564
+ **Check 4 — Tests.** Does the diff delete, weaken, or lower a threshold in a test? Flag
565
+ removed assertions, skipped tests, lowered coverage bars, or deleted test files.
566
+
567
+ **Check 5 — CI/CD & Workflow files.** Does the diff change CI, CD, or workflow files?
568
+ Flag changes to `.github/workflows/`, Dockerfiles, deployment scripts, or infrastructure
569
+ configuration.
570
+
571
+ **Check 6 — Protected files.** Does the diff change a protected file? This is normally
572
+ handled before you run, but never merge one if it reaches this gate. Flag any match against
573
+ the repository's protected file list.
574
+
575
+ **Check 7 — Scope.** Is the diff size consistent with what the issue implied? Compare the
576
+ number of files changed and lines added/removed against the complexity the issue described.
577
+ Flag if the diff is materially larger or smaller than expected.
578
+
579
+ **Check 8 — Repository risk indicators.** Does the diff touch any risk indicator defined in
580
+ ${{ env.REPO_RULES }}? Review the repository guardrails for domain-specific risk areas such
581
+ as calculation engines, audit chains, authentication, database migrations, or money handling.
582
+ Flag any
583
+ match and name the specific indicator.
584
+
585
+ **Check 9 — Mergeability.** Can the PR be merged cleanly? The value is
586
+ `${{ needs.reserve.outputs.has_conflicts }}`. If conflicts exist, this is ❌ but not a
587
+ blocking verdict — proceed to remediation (step 6). If no conflicts, ✅.
588
+
589
+ **Check 10 — Confidence.** Are you confident in the merge decision? Low confidence is
590
+ itself a flag. If you are unsure about the impact of the change, mark ⚠️ and explain what
591
+ is uncertain. A human should review when confidence is low.
592
+
593
+ 6. **CI failed** → read `/tmp/gh-aw/agent/failed-jobs.json` and
594
+ `/tmp/gh-aw/agent/failed-logs.txt`, which are already on disk. Load only skills required to
595
+ fix the actual cause. Run these verification commands before a push. Do not weaken a test,
596
+ disable a check, or push an unverified guess.
597
+
598
+ **Scoped verification.** The commands below are the full suite. This runner has limited
599
+ memory, and a whole-repo lint or build can be killed mid-run. Scope verification to the
600
+ files you actually changed first, and only escalate to the full suite when the scoped run
601
+ passes and you are still unsure:
602
+ - Lint/format (biome, eslint, prettier, ruff, etc.): pass the changed file paths as
603
+ arguments so the tool checks only those files (e.g. `pnpm exec biome check <files>`),
604
+ never the whole repository.
605
+ - Build: prefer building only the project(s) containing the changed files; use the full
606
+ solution build only when the change crosses project boundaries.
607
+ - Tests: run the test project covering the changed files; run the full suite only when
608
+ the change is cross-cutting.
609
+ If verification of exactly the CI-failing job is what you need, reproduce just that job's
610
+ command, not the entire pipeline.
611
+
612
+ A PR that already contains protected files still requires remediation. You may include those
613
+ files in the verified repair push, but the next green-CI cycle will require human review and
614
+ must not auto-merge the PR.
615
+
616
+ **If `has_conflicts` is `true` (current value: `${{ needs.reserve.outputs.has_conflicts }}`):** You are already on the PR branch. Resolve the conflict;
617
+ it is not a reason to hand the PR to a human. Merge `origin/${{ github.event.repository.default_branch }}`
618
+ into the current branch, resolve every conflict deliberately, stage the resolutions, and
619
+ commit the merge. Then run verification and push the resulting branch update. Do not use
620
+ `--ours`, `--theirs`, or a blanket conflict-marker deletion without reviewing the intended
621
+ behavior from both sides.
622
+
623
+ ```
624
+ ${{ env.VERIFY_COMMANDS }}
625
+ ```
626
+
627
+ Propose `push_to_pull_request_branch` (pr_number: ${{ needs.subject.outputs.pr }}, branch:
628
+ the current PR branch), then select the `remediated` verdict. CI will run again and trigger
629
+ you again with the new result.
630
+
631
+ Before pushing, run the project's lint fix command (e.g. `pnpm lint:fix` or
632
+ `pnpm exec biome check --write <changed-files>`) to auto-format. If lint:fix is not
633
+ available, fix formatting manually. Never push code with lint errors.
634
+
635
+ If you cannot fix it after a concrete repair attempt, or the logs show you have already tried on this same head commit,
636
+ stop looping: select the `review` verdict and explain the failure and what you tried. A human
637
+ decides from there.
638
+
639
+ 7. Decide the verdict based on the assessment table:
640
+
641
+ - **All checks ✅ → `merge`.** The PR is safe to merge. CI is green, no risk indicators
642
+ triggered, tests are intact, scope matches, mergeability is clean.
643
+ - **Any check ⚠️ or ❌ (except CI failure) → `review`.** Do not merge. Explain exactly which
644
+ check tripped, why, and what a reviewer should look at. Leave `implement` in place: the
645
+ work is not finished until a human merges it.
646
+ - **CI failed and you fixed it → `remediated`.** You pushed a verified fix and CI will
647
+ re-run.
648
+ - **CI failed and you cannot fix it → `review`.** Explain the failure and what you tried.
649
+
650
+ Never merge with administrator privileges and never bypass a required check. If the merge
651
+ is refused, that refusal is the answer: select `review` and leave it for a human.
652
+
653
+ 8. Emit exactly one `add_comment` targeting issue `${{ needs.subject.outputs.issue }}` with:
654
+ 1. `${{ env.GATE_MARKER }}`
655
+ 2. A heading: `## Merge gate decision for PR #${{ needs.subject.outputs.pr }}`
656
+ 3. A structured assessment table with all 10 check results
657
+ 4. A one-line detail per check (what was found and why it passed or flagged)
658
+ 5. A line `**Verdict:** merge`, `**Verdict:** review`, or `**Verdict:** remediated`
659
+
660
+ The workflow applies comments, labels, merges, and closures with the App token. Do not call
661
+ any tools except the one optional `push_to_pull_request_branch` for a verified CI repair
662
+ and this one `add_comment`.
663
+
664
+ Format the checks as a table with status indicators:
665
+
666
+ ```
667
+ ### Assessment
668
+
669
+ # Check Result
670
+ 1 CI Status ✅ Success / ❌ Failure: [job name] / ⚠️ Action required
671
+ 2 Auth & Security ✅ No changes / ⚠️ Touched: [area]
672
+ 3 API & Contracts ✅ No changes / ⚠️ Changed: [area]
673
+ 4 Tests ✅ Not weakened / ⚠️ Weakened: [file]
674
+ 5 CI/CD & Workflow ✅ No changes / ⚠️ Changed: [file]
675
+ 6 Protected files ✅ None touched / ❌ Touched: [file]
676
+ 7 Scope ✅ Appropriately scoped / ⚠️ [too large/small: reason]
677
+ 8 Risk indicators ✅ None triggered / ⚠️ Triggered: [indicator]
678
+ 9 Mergeability ✅ Clean / ⚠️ Conflicts
679
+ 10 Confidence ✅ High / ⚠️ Low: [reason]
680
+ ```
681
+
682
+ Then a line `**Verdict:** merge` / `**Verdict:** review` / `**Verdict:** remediated`
683
+
684
+ 9. Ignore the `## Diagram` section below. It is documentation for humans and contains no
685
+ instructions for you.
686
+
687
+ ## Diagram
688
+
689
+ ```mermaid
690
+ flowchart TD
691
+ gateStart("Work Router<br/>merge-gate route<br/>(CI completed)") --> gateSubject
692
+ gateSubject["Subject (rung 4)<br/>Our PR? Closes an implement issue?"] -->|✓| gateFacts
693
+ gateSubject -.->|✗| gateIdle
694
+ gateFacts("Facts (rung 3)<br/>Diff, PR shape, failing logs") --> gateCi
695
+ gateCi["CI<br/>What did it conclude?"] -->|success| gateProtected
696
+ gateProtected{"Protected files?"}
697
+ gateProtected -.->|yes| gateHuman
698
+ gateProtected -->|no| gateConflict
699
+ gateConflict{"Merge conflicts?"}
700
+ gateConflict -->|yes| gateRebase
701
+ gateConflict -->|no| gateTrivial
702
+ gateRebase("Rebase<br/>Resolve conflicts, /repo-verify") -->|pushed| gateWait
703
+ gateRebase -.->|cannot resolve| gateHuman
704
+ gateTrivial{"Trivial marker?"}
705
+ gateTrivial -->|yes| gateMerge
706
+ gateTrivial -->|no| gateAssess
707
+ gateCi -.->|failure| gateFix
708
+ gateCi -.->|no verdict| gateHuman
709
+ gateAssess["Assessment (10 checks)<br/>CI, Auth, API, Tests, CI/CD<br/>Protected, Scope, Risk, Merge, Confidence"] -->|all ✅| gateMerge
710
+ gateAssess -.->|any ⚠️/❌| gateHuman
711
+ gateFix("Fix<br/>Read logs, fix the cause, /repo-verify") -->|pushed| gateWait
712
+ gateFix -.->|cannot fix| gateHuman
713
+ gateMerge(("Merged<br/>Issue closed, review+labels removed"))
714
+ gateWait(("Pushed<br/>CI will re-run and re-trigger via Router"))
715
+ gateHuman(("Review<br/>review label, reason explained"))
716
+ gateIdle(("Idle<br/>Not our pull request"))
717
+
718
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
719
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
720
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
721
+ classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
722
+ classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a2a
723
+ classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
724
+ class gateStart start
725
+ class gateFacts,gateFix,gateRebase action
726
+ class gateSubject,gateCi,gateAssess,gateTrivial,gateConflict,gateProtected decision
727
+ class gateIdle,gateWait idle
728
+ class gateHuman failure
729
+ class gateMerge success
730
+ ```