@plainconceptsplatform/workflows 0.4.34 → 0.6.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 (61) hide show
  1. package/README.md +88 -88
  2. package/dist/action-validation.test.js +5 -2
  3. package/dist/catalog-installation.js +50 -7
  4. package/dist/catalog-installation.test.js +59 -22
  5. package/dist/catalog-listing.test.js +4 -4
  6. package/dist/index.js +48 -48
  7. package/dist/index.test.js +9 -9
  8. package/dist/repository-inspection.test.js +2 -2
  9. package/dist/route-processing.js +24 -25
  10. package/dist/route-processing.test.js +166 -233
  11. package/dist/stack-defaults.js +14 -14
  12. package/dist/stack-defaults.test.js +79 -79
  13. package/dist/tui.test.js +10 -7
  14. package/dist/workflow-catalog.d.ts +1 -1
  15. package/dist/workflow-catalog.js +1 -5
  16. package/loops/actions/add-issue-labels/action.yml +2 -2
  17. package/loops/actions/agent-output.cjs +17 -17
  18. package/loops/actions/apply-agent-bundle/apply-bundle.sh +14 -1
  19. package/loops/actions/audit-close/action.yml +24 -0
  20. package/loops/actions/classify-route/action.yml +8 -3
  21. package/loops/actions/classify-route/classify-route.sh +58 -38
  22. package/loops/actions/cleanup-artifacts/action.yml +38 -12
  23. package/loops/actions/identify-gate-subject/action.yml +3 -1
  24. package/loops/actions/remove-issue-labels/action.yml +4 -2
  25. package/loops/actions/update-changelog/action.yml +113 -0
  26. package/loops/actions/validate-merge-gate-output/action.yml +40 -40
  27. package/loops/actions/validate-refine-output/validate-refine-output.sh +34 -5
  28. package/loops/actions/validate-review-output/action.yml +35 -35
  29. package/loops/actions/validate-triage-output/action.yml +36 -36
  30. package/loops/actions/verify-refine-output/verify-refine-output.sh +20 -0
  31. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +375 -36
  32. package/loops/scripts/compile-agent-workflows.mjs +290 -6
  33. package/loops/scripts/merge-changelog.mjs +76 -0
  34. package/loops/templates/agentics/actionlint.yaml +13 -0
  35. package/loops/templates/agentics/agentics-checks.yml +125 -8
  36. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  37. package/loops/templates/ci/app-ci-dotnet-next.yml +330 -171
  38. package/loops/templates/ci/app-ci-node-monorepo.yml +260 -178
  39. package/loops/templates/issues/bug_report.yml +109 -109
  40. package/loops/templates/issues/feature_request.yml +75 -75
  41. package/loops/templates/opencode/opencode.ci.json +49 -47
  42. package/loops/templates/opencode/opencode.ci.json.md +49 -41
  43. package/loops/templates/release/github-release.yml +30 -30
  44. package/loops/workflows/agent-apply-review.md +469 -443
  45. package/loops/workflows/agent-audit.md +213 -197
  46. package/loops/workflows/agent-implement.md +640 -414
  47. package/loops/workflows/agent-merge-gate.md +844 -584
  48. package/loops/workflows/agent-refine.md +633 -418
  49. package/loops/workflows/agent-release.md +258 -0
  50. package/loops/workflows/agent-triage.md +447 -439
  51. package/loops/workflows/authorize-bot-work.yml +85 -82
  52. package/loops/workflows/shared/opencode-ci.md +206 -197
  53. package/loops/workflows/shared/platform-defaults.md +19 -16
  54. package/loops/workflows/work-router.yml +1038 -632
  55. package/package.json +7 -8
  56. package/dist/repository-state.d.ts +0 -18
  57. package/dist/repository-state.js +0 -77
  58. package/dist/repository-state.test.d.ts +0 -1
  59. package/dist/repository-state.test.js +0 -96
  60. package/loops/workflows/agent-direct.md +0 -375
  61. package/loops/workflows/agent-propose.md +0 -342
@@ -1,443 +1,469 @@
1
- ---
2
- # Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-apply-review.md. Update with `workflows update --force`; consumer edits may be overwritten.
3
- env:
4
- REPO_RULES: "Apply only actionable outstanding reviewer feedback to the selected bot pull request. Make minimal changes that address each comment. Preserve architecture and do not weaken tests. Run full verification after changes."
5
- WORKING_LABEL: bot-working
6
- REVIEW_LABEL: review
7
- REVIEW_MARKER: "<!-- agent-apply-review -->"
8
- INCOMPLETE_COMMENT: "Automated review feedback ended without an outcome. The issue remains for a retry."
9
- ISSUE_CONTEXT_PATH: /tmp/gh-aw/agent/issue-context.json
10
- GH_AW_ALLOWED_BOTS: "platform-devbox[bot],github-actions[bot]"
11
- GIT_AUTHOR_NAME: "github-actions[bot]"
12
- GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
13
- GIT_COMMITTER_NAME: "github-actions[bot]"
14
- GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com"
15
- description: |
16
- Applies reviewer feedback to an open pull request the bot authored, then pushes the fixes to
17
- the same branch. Called by the Work Router; does not trigger on public events.
18
-
19
- `target: triggering` is enough for the push here, so unlike the merge gate this needs no
20
- wildcard fetch: the event is the pull request and gh-aw has already checked out its head.
21
-
22
- name: "Agent: Apply Review"
23
-
24
- # Router-only worker. The Work Router owns triggers, classification, and rung 1-2 checks.
25
- # This workflow receives the classified inputs and runs rung 3+.
26
- imports:
27
- - github/gh-aw/.github/workflows/shared/opencode.md@v0.86.2
28
- - shared/platform-defaults.md
29
- - shared/opencode-ci.md
30
-
31
- on:
32
- workflow_call:
33
- inputs:
34
- pr-number:
35
- description: Pull request number to apply review feedback on.
36
- required: true
37
- type: string
38
- linked-issue:
39
- description: Issue number the pull request closes. May be empty.
40
- required: false
41
- type: string
42
-
43
- # Rung 4. Router has classified the event; this job validates PR ownership and checks for
44
- # substantive feedback. A custom job, not `on.steps`, because the prompt needs these values.
45
- jobs:
46
- subject:
47
- runs-on: RunnerLandingZone
48
- permissions:
49
- contents: read
50
- issues: read
51
- pull-requests: read
52
- outputs:
53
- found: ${{ steps.subject.outputs.found }}
54
- pr: ${{ steps.subject.outputs.pr }}
55
- issue: ${{ steps.subject.outputs.issue }}
56
- unresolved: ${{ steps.subject.outputs.unresolved }}
57
- steps:
58
- - name: Confirm this is our pull request and the feedback is substantive
59
- id: subject
60
- env:
61
- GH_TOKEN: ${{ github.token }}
62
- REPO: ${{ github.repository }}
63
- PR: ${{ inputs.pr-number }}
64
- LINKED_ISSUE: ${{ inputs.linked-issue }}
65
- run: |
66
- set -euo pipefail
67
-
68
- none() {
69
- echo "found=false" >> "$GITHUB_OUTPUT"
70
- echo "$1"
71
- exit 0
72
- }
73
-
74
- pr=$(gh pr view "$PR" --repo "$REPO" --json number,state,author,title,body)
75
- [ "$(printf '%s' "$pr" | jq -r '.state')" = "OPEN" ] || none "PR #$PR is not open"
76
-
77
- # Use GitHub's bot flag because App login formats differ across APIs.
78
- author=$(printf '%s' "$pr" | jq -r '.author.login')
79
- is_bot=$(printf '%s' "$pr" | jq -r '.author.is_bot')
80
- [ "$is_bot" = "true" ] || \
81
- none "PR #$PR was authored by $author, a human. Never push to a human's branch."
82
-
83
- # The issue this pull request closes, preferring the router-provided linked issue.
84
- if [ -n "$LINKED_ISSUE" ]; then
85
- issue="$LINKED_ISSUE"
86
- else
87
- issue=$(printf '%s' "$pr" | jq -r '.body // ""' \
88
- | grep -oiE '(close[sd]?|fixe?[sd]?|resolve[sd]?) +#[0-9]+' \
89
- | grep -oE '[0-9]+' | head -n 1 || true)
90
- fi
91
-
92
- # Unresolved review threads, from the API rather than from the model's reading of it.
93
- unresolved=$(gh api graphql -f query='
94
- query($owner:String!, $name:String!, $number:Int!) {
95
- repository(owner:$owner, name:$name) {
96
- pullRequest(number:$number) {
97
- reviewThreads(first:100) { nodes { isResolved isOutdated } }
98
- }
99
- }
100
- }' \
101
- -f owner="${REPO%/*}" -f name="${REPO#*/}" -F number="$PR" \
102
- --jq '[.data.repository.pullRequest.reviewThreads.nodes[]
103
- | select(.isResolved == false and .isOutdated == false)] | length')
104
-
105
- {
106
- echo "found=true"
107
- echo "pr=$PR"
108
- echo "issue=${issue:-}"
109
- echo "unresolved=$unresolved"
110
- } >> "$GITHUB_OUTPUT"
111
- echo "PR #$PR has $unresolved unresolved thread(s)"
112
-
113
- reserve:
114
- needs: subject
115
- if: needs.subject.outputs.found == 'true' && needs.subject.outputs.issue != ''
116
- runs-on: RunnerLandingZone
117
- permissions:
118
- contents: read
119
- issues: write
120
- steps:
121
- - name: Checkout workflow actions
122
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
123
- with:
124
- persist-credentials: false
125
- - name: Create bot token
126
- id: app-token
127
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
128
- with:
129
- client-id: ${{ secrets.BOT_APP_ID }}
130
- private-key: ${{ secrets.BOT_PRIVATE_KEY }}
131
- - name: Mark the linked issue as in progress
132
- uses: ./.github/actions/add-issue-labels
133
- with:
134
- token: ${{ steps.app-token.outputs.token }}
135
- issue-number: ${{ needs.subject.outputs.issue }}
136
- labels: ${{ env.WORKING_LABEL }}
137
- - name: Clear the human-needed flag
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.REVIEW_LABEL }}
143
- validate_output:
144
- needs: [activation, subject, agent, safe_outputs]
145
- if: always() && needs.agent.result == 'success' && needs.safe_outputs.result == 'success'
146
- runs-on: RunnerLandingZone
147
- permissions:
148
- contents: read
149
- pull-requests: read
150
- outputs:
151
- valid: ${{ steps.validate.outputs.valid }}
152
- outcome: ${{ steps.validate.outputs.outcome }}
153
- steps:
154
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
155
- with:
156
- persist-credentials: false
157
- - id: output
158
- uses: ./.github/actions/download-agent-output
159
- with:
160
- artifact-name: ${{ needs.activation.outputs.artifact_prefix }}agent
161
- - name: Load unresolved review threads
162
- env:
163
- GH_TOKEN: ${{ github.token }}
164
- REPO: ${{ github.repository }}
165
- PR: ${{ needs.subject.outputs.pr }}
166
- run: |
167
- set -euo pipefail
168
- gh api graphql -f query='
169
- query($owner:String!, $name:String!, $number:Int!) {
170
- repository(owner:$owner, name:$name) {
171
- pullRequest(number:$number) {
172
- reviewThreads(first:100) { nodes { id isResolved isOutdated } }
173
- }
174
- }
175
- }' -f owner="${REPO%/*}" -f name="${REPO#*/}" -F number="$PR" \
176
- --jq '.data.repository.pullRequest.reviewThreads.nodes' > /tmp/review-threads.json
177
- - id: validate
178
- uses: ./.github/actions/validate-review-output
179
- with:
180
- output-file: ${{ steps.output.outputs.output-file }}
181
- pr-number: ${{ needs.subject.outputs.pr }}
182
- review-threads-file: /tmp/review-threads.json
183
- conclude:
184
- needs: [activation, subject, agent, safe_outputs, validate_output]
185
- if: >
186
- needs.agent.result == 'success' &&
187
- needs.safe_outputs.result == 'success' &&
188
- needs.validate_output.outputs.valid == 'true'
189
- runs-on: RunnerLandingZone
190
- permissions:
191
- contents: write
192
- issues: write
193
- pull-requests: write
194
- steps:
195
- - name: Create bot token
196
- id: app-token
197
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
198
- with:
199
- client-id: ${{ secrets.BOT_APP_ID }}
200
- private-key: ${{ secrets.BOT_PRIVATE_KEY }}
201
- - name: Checkout repository
202
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
203
- with:
204
- token: ${{ steps.app-token.outputs.token }}
205
- fetch-depth: 0
206
- - name: Apply agent output
207
- uses: ./.github/actions/apply-agent-output
208
- with:
209
- artifact-name: ${{ needs.activation.outputs.artifact_prefix }}agent
210
- token: ${{ steps.app-token.outputs.token }}
211
- push-to-branch: 'true'
212
- apply-labels: 'false'
213
- - name: Release implemented review
214
- if: needs.validate_output.outputs.outcome == 'implemented'
215
- uses: ./.github/actions/remove-issue-labels
216
- with:
217
- token: ${{ steps.app-token.outputs.token }}
218
- issue-number: ${{ needs.subject.outputs.issue }}
219
- labels: ${{ env.WORKING_LABEL }}
220
- - name: Flag review outcome
221
- if: needs.validate_output.outputs.outcome != 'implemented'
222
- uses: ./.github/actions/add-issue-labels
223
- with:
224
- token: ${{ steps.app-token.outputs.token }}
225
- issue-number: ${{ needs.subject.outputs.issue }}
226
- labels: ${{ env.REVIEW_LABEL }}
227
- - name: Release review outcome
228
- if: needs.validate_output.outputs.outcome != 'implemented'
229
- uses: ./.github/actions/remove-issue-labels
230
- with:
231
- token: ${{ steps.app-token.outputs.token }}
232
- issue-number: ${{ needs.subject.outputs.issue }}
233
- labels: ${{ env.WORKING_LABEL }}
234
- incomplete:
235
- needs: [subject, agent, safe_outputs, validate_output]
236
- if: >
237
- always() &&
238
- needs.subject.outputs.found == 'true' &&
239
- (needs.agent.result != 'success' || needs.safe_outputs.result != 'success' || needs.validate_output.outputs.valid != 'true')
240
- runs-on: RunnerLandingZone
241
- permissions:
242
- contents: read
243
- issues: write
244
- steps:
245
- - name: Checkout workflow actions
246
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
247
- with:
248
- persist-credentials: false
249
- - name: Create bot token
250
- id: app-token
251
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
252
- with:
253
- client-id: ${{ secrets.BOT_APP_ID }}
254
- private-key: ${{ secrets.BOT_PRIVATE_KEY }}
255
- - name: Release the issue
256
- uses: ./.github/actions/remove-issue-labels
257
- with:
258
- token: ${{ steps.app-token.outputs.token }}
259
- issue-number: ${{ needs.subject.outputs.issue }}
260
- labels: ${{ env.WORKING_LABEL }}
261
- - name: Flag for human review
262
- uses: ./.github/actions/add-issue-labels
263
- with:
264
- token: ${{ steps.app-token.outputs.token }}
265
- issue-number: ${{ needs.subject.outputs.issue }}
266
- labels: ${{ env.REVIEW_LABEL }}
267
- - name: Report missing review feedback outcome
268
- uses: ./.github/actions/create-issue-comment
269
- with:
270
- token: ${{ steps.app-token.outputs.token }}
271
- issue-number: ${{ needs.subject.outputs.issue }}
272
- body: |
273
- ${{ env.REVIEW_MARKER }}
274
- ${{ env.INCOMPLETE_COMMENT }}
275
- [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
276
-
277
- if: needs.subject.outputs.found == 'true'
278
-
279
- runs-on: RunnerLandingZone
280
- runs-on-slim: RunnerLandingZone
281
-
282
- secrets:
283
- OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
284
-
285
- engine:
286
- id: opencode
287
- version: "1.2.14"
288
- env:
289
- OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
290
-
291
- model: openai/glm-5-2
292
-
293
- max-turns: 300
294
- max-turn-cache-misses: 3000
295
- max-ai-credits: 5000
296
-
297
-
298
- permissions: read-all
299
-
300
- # Rung 3. A reviewer often makes one point across several comments, so the agent needs the
301
- # whole conversation, not just the one that triggered it. Fetching all of it is deterministic.
302
- steps:
303
- - name: Load the issue context
304
- uses: ./.github/actions/load-issue-context
305
- with:
306
- token: ${{ github.token }}
307
- issue-number: ${{ needs.subject.outputs.issue }}
308
- output-path: ${{ env.ISSUE_CONTEXT_PATH }}
309
- - name: Fetch the whole review conversation and the diff
310
- env:
311
- GH_TOKEN: ${{ github.token }}
312
- REPO: ${{ github.repository }}
313
- PR: ${{ inputs.pr-number }}
314
- run: |
315
- set -euo pipefail
316
- mkdir -p /tmp/gh-aw/agent
317
- gh pr diff "$PR" --repo "$REPO" > /tmp/gh-aw/agent/diff.patch
318
- gh api "repos/$REPO/pulls/$PR/comments" --paginate \
319
- --jq '[.[] | {id, path, line, author: .user.login, body, in_reply_to_id}]' \
320
- > /tmp/gh-aw/agent/review-comments.json
321
- gh api "repos/$REPO/issues/$PR/comments" --paginate \
322
- --jq '[.[] | {author: .user.login, body}]' \
323
- > /tmp/gh-aw/agent/conversation.json
324
- gh api graphql -f query='
325
- query($owner:String!, $name:String!, $number:Int!) {
326
- repository(owner:$owner, name:$name) {
327
- pullRequest(number:$number) {
328
- reviewThreads(first:100) {
329
- nodes { id
330
- isResolved isOutdated path
331
- comments(first:50) { nodes { author { login } body } }
332
- }
333
- }
334
- }
335
- }
336
- }' \
337
- -f owner="${REPO%/*}" -f name="${REPO#*/}" -F number="$PR" \
338
- --jq '.data.repository.pullRequest.reviewThreads.nodes' \
339
- > /tmp/gh-aw/agent/review-threads.json
340
-
341
- safe-outputs:
342
- staged: true
343
- threat-detection: false
344
- push-to-pull-request-branch:
345
- add-comment:
346
- target: "*"
347
-
348
-
349
- timeout-minutes: 45
350
- ---
351
-
352
- 1. You are applying review feedback to pull request
353
- **#${{ needs.subject.outputs.pr }}**, which has
354
- **${{ needs.subject.outputs.unresolved }}** unresolved review thread(s).
355
-
356
- It has already been confirmed that this pull request is open, that we authored it, and that
357
- the reviewer has write access. Do not re-check any of that.
358
-
359
- 2. Read `${{ env.ISSUE_CONTEXT_PATH }}`. It contains the issue body and discussion this pull
360
- request closes. The acceptance criteria there define what the implementation must still
361
- satisfy after the feedback is applied.
362
-
363
- 3. Read the whole conversation, not just the comment that triggered you. It is already on disk:
364
-
365
- - `/tmp/gh-aw/agent/review-threads.json` , every thread with its resolved and outdated state
366
- - `/tmp/gh-aw/agent/review-comments.json` , every inline comment with its file and line
367
- - `/tmp/gh-aw/agent/conversation.json` , every conversation comment
368
- - `/tmp/gh-aw/agent/diff.patch` , the current diff
369
-
370
- A reviewer often makes one point across several comments. Applying them one at a time
371
- produces contradictory commits, so read everything before changing anything.
372
-
373
- 4. Work out which items are genuinely actionable and still outstanding. Skip anything already
374
- addressed by a later commit, anything a bot wrote, anything from the pull request author,
375
- and anything that is a question rather than a request. You must account for every unresolved
376
- human review thread by its `PRRT_...` ID. Do not claim feedback was implemented when no branch
377
- change is needed: that requires reviewer confirmation.
378
-
379
- 5. Load only skills required by the feedback, then apply it. Make only changes the feedback
380
- justifies: a review comment is not licence for unrelated refactoring. Never read outside this
381
- repository root. Follow repository documentation and established conventions. Keep changes
382
- focused, protect secrets, and do not modify generated files unless the feedback requires it.
383
- Adhere to ${{ env.REPO_RULES }}.
384
-
385
- 6. Run the repository verification commands below. The issue context at
386
- `${{ env.ISSUE_CONTEXT_PATH }}` defines acceptance criteria the fix must satisfy. If a check
387
- fails, fix what you broke and run it again. Do not push a branch that does not pass.
388
-
389
- ```
390
- ${{ env.VERIFY_COMMANDS }}
391
- ```
392
-
393
- 7. Select one review outcome.
394
-
395
- - **implemented**: You made the requested change, verification passed, and you will propose
396
- exactly one `push_to_pull_request_branch`.
397
- - **already-satisfied**: The current branch already satisfies the request. Do not push; the
398
- reviewer must confirm this assessment.
399
- - **needs-human**: The feedback is ambiguous, unsafe, or cannot be applied. Do not push.
400
-
401
- 8. Emit exactly one `add_comment` on PR `${{ needs.subject.outputs.pr }}`. Include every
402
- unresolved human review thread ID and an explanation for it, then exactly one line:
403
- `**Review outcome:** implemented`, `**Review outcome:** already-satisfied`, or
404
- `**Review outcome:** needs-human`.
405
-
406
- 9. Do not merge, close, or change labels. The workflow validates your outcome and owns those
407
- state transitions.
408
-
409
- 10. Ignore the `## Diagram` section below. It is documentation for humans and contains no
410
- instructions for you.
411
-
412
- ## Diagram
413
-
414
- ```mermaid
415
- flowchart TD
416
- fbStart("Work Router<br/>apply-review route<br/>(review on bot PR)") --> fbSubject
417
- fbSubject["Subject (rung 4)<br/>Our open PR? Substantive review?"] -->|✓| fbFacts
418
- fbSubject -.->|✗| fbIdle
419
- fbFacts("Facts (rung 3)<br/>Threads, comments, diff to disk") --> fbTriage
420
- fbTriage["Triage<br/>Anything actionable and outstanding?"] -->|✓| fbReserve
421
- fbTriage -.->|nothing| fbIdle
422
- fbReserve("Reserve<br/>Propose bot-working on the issue") -->|✓| fbApply
423
- fbApply("Apply<br/>Only what the feedback justifies") -->|✓| fbVerify
424
- fbApply -.->|✗| fbFail
425
- fbVerify["Verify<br/>/repo-verify passes?<br/>↻"] -->|✓| fbPush
426
- fbVerify -.->|✗| fbApply
427
- fbPush(("Pushed<br/>Same branch, bot-working removed"))
428
- fbIdle(("Idle<br/>Not ours, or nothing to do"))
429
- fbFail(("Fail<br/>review added, bot-working removed"))
430
-
431
- classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
432
- classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
433
- classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
434
- classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
435
- classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
436
- classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
437
- class fbStart start
438
- class fbFacts,fbReserve,fbApply action
439
- class fbSubject,fbTriage,fbVerify decision
440
- class fbIdle idle
441
- class fbFail failure
442
- class fbPush success
443
- ```
1
+ ---
2
+ # Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-apply-review.md. Update with `workflows update --force`; consumer edits may be overwritten.
3
+ env:
4
+ # Printed by the prompt as the verification block. Consumers set their own commands.
5
+ VERIFY_COMMANDS: ""
6
+ REPO_RULES: "Apply only actionable outstanding reviewer feedback to the selected bot pull request. Make minimal changes that address each comment. Preserve architecture and do not weaken tests. Run full verification after changes."
7
+ WORKING_LABEL: bot-working
8
+ REVIEW_LABEL: review
9
+ PR_PENDING_LABEL: pr-pending
10
+ REVIEW_MARKER: "<!-- agent-apply-review -->"
11
+ INCOMPLETE_COMMENT: "Automated review feedback 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
+ Applies reviewer feedback to an open pull request the bot authored, then pushes the fixes to
20
+ the same branch. Called by the Work Router; does not trigger on public events.
21
+
22
+ `target: triggering` is enough for the push here, so unlike the merge gate this needs no
23
+ wildcard fetch: the event is the pull request and gh-aw has already checked out its head.
24
+
25
+ name: "Agent: Apply Review"
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.87.5
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 apply review feedback on.
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
+
46
+ # Rung 4. Router has classified the event; this job validates PR ownership and checks for
47
+ # substantive feedback. A custom job, not `on.steps`, because the prompt needs these values.
48
+ jobs:
49
+ subject:
50
+ runs-on: agents-arc
51
+ permissions:
52
+ contents: read
53
+ issues: read
54
+ pull-requests: read
55
+ outputs:
56
+ found: ${{ steps.subject.outputs.found }}
57
+ pr: ${{ steps.subject.outputs.pr }}
58
+ issue: ${{ steps.subject.outputs.issue }}
59
+ unresolved: ${{ steps.subject.outputs.unresolved }}
60
+ steps:
61
+ - name: Confirm this is our pull request and the feedback is substantive
62
+ id: subject
63
+ env:
64
+ GH_TOKEN: ${{ github.token }}
65
+ REPO: ${{ github.repository }}
66
+ PR: ${{ inputs.pr-number }}
67
+ LINKED_ISSUE: ${{ inputs.linked-issue }}
68
+ run: |
69
+ set -euo pipefail
70
+
71
+ none() {
72
+ echo "found=false" >> "$GITHUB_OUTPUT"
73
+ echo "$1"
74
+ exit 0
75
+ }
76
+
77
+ pr=$(gh pr view "$PR" --repo "$REPO" --json number,state,author,title,body)
78
+ [ "$(printf '%s' "$pr" | jq -r '.state')" = "OPEN" ] || none "PR #$PR is not open"
79
+
80
+ # Use GitHub's bot flag because App login formats differ across APIs.
81
+ author=$(printf '%s' "$pr" | jq -r '.author.login')
82
+ is_bot=$(printf '%s' "$pr" | jq -r '.author.is_bot')
83
+ [ "$is_bot" = "true" ] || \
84
+ none "PR #$PR was authored by $author, a human. Never push to a human's branch."
85
+
86
+ # The issue this pull request closes, preferring the router-provided linked issue.
87
+ if [ -n "$LINKED_ISSUE" ]; then
88
+ issue="$LINKED_ISSUE"
89
+ else
90
+ issue=$(printf '%s' "$pr" | jq -r '.body // ""' \
91
+ | grep -oiE '(close[sd]?|fixe?[sd]?|resolve[sd]?) +#[0-9]+' \
92
+ | grep -oE '[0-9]+' | head -n 1 || true)
93
+ fi
94
+
95
+ # Unresolved review threads, from the API rather than from the model's reading of it.
96
+ unresolved=$(gh api graphql -f query='
97
+ query($owner:String!, $name:String!, $number:Int!) {
98
+ repository(owner:$owner, name:$name) {
99
+ pullRequest(number:$number) {
100
+ reviewThreads(first:100) { nodes { isResolved isOutdated } }
101
+ }
102
+ }
103
+ }' \
104
+ -f owner="${REPO%/*}" -f name="${REPO#*/}" -F number="$PR" \
105
+ --jq '[.data.repository.pullRequest.reviewThreads.nodes[]
106
+ | select(.isResolved == false and .isOutdated == false)] | length')
107
+
108
+ {
109
+ echo "found=true"
110
+ echo "pr=$PR"
111
+ echo "issue=${issue:-}"
112
+ echo "unresolved=$unresolved"
113
+ } >> "$GITHUB_OUTPUT"
114
+ echo "PR #$PR has $unresolved unresolved thread(s)"
115
+
116
+ reserve:
117
+ needs: subject
118
+ if: needs.subject.outputs.found == 'true' && needs.subject.outputs.issue != ''
119
+ runs-on: agents-arc
120
+ permissions:
121
+ contents: read
122
+ issues: write
123
+ steps:
124
+ - name: Checkout workflow actions
125
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
126
+ with:
127
+ persist-credentials: false
128
+ - name: Create bot token
129
+ id: app-token
130
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
131
+ with:
132
+ client-id: ${{ secrets.BOT_APP_ID }}
133
+ private-key: ${{ secrets.BOT_PRIVATE_KEY }}
134
+ - name: Mark the linked issue as in progress
135
+ uses: ./.github/actions/add-issue-labels
136
+ with:
137
+ token: ${{ steps.app-token.outputs.token }}
138
+ issue-number: ${{ needs.subject.outputs.issue }}
139
+ labels: ${{ env.WORKING_LABEL }}
140
+ - name: Clear the human-needed flag
141
+ uses: ./.github/actions/remove-issue-labels
142
+ with:
143
+ token: ${{ steps.app-token.outputs.token }}
144
+ issue-number: ${{ needs.subject.outputs.issue }}
145
+ labels: ${{ env.REVIEW_LABEL }}
146
+ validate_output:
147
+ needs: [activation, subject, agent, safe_outputs]
148
+ if: always() && needs.agent.result == 'success' && needs.safe_outputs.result == 'success'
149
+ runs-on: agents-arc
150
+ permissions:
151
+ contents: read
152
+ pull-requests: read
153
+ outputs:
154
+ valid: ${{ steps.validate.outputs.valid }}
155
+ outcome: ${{ steps.validate.outputs.outcome }}
156
+ steps:
157
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
158
+ with:
159
+ persist-credentials: false
160
+ - id: output
161
+ uses: ./.github/actions/download-agent-output
162
+ with:
163
+ artifact-name: ${{ needs.activation.outputs.artifact_prefix }}agent
164
+ - name: Load unresolved review threads
165
+ env:
166
+ GH_TOKEN: ${{ github.token }}
167
+ REPO: ${{ github.repository }}
168
+ PR: ${{ needs.subject.outputs.pr }}
169
+ run: |
170
+ set -euo pipefail
171
+ gh api graphql -f query='
172
+ query($owner:String!, $name:String!, $number:Int!) {
173
+ repository(owner:$owner, name:$name) {
174
+ pullRequest(number:$number) {
175
+ reviewThreads(first:100) { nodes { id isResolved isOutdated } }
176
+ }
177
+ }
178
+ }' -f owner="${REPO%/*}" -f name="${REPO#*/}" -F number="$PR" \
179
+ --jq '.data.repository.pullRequest.reviewThreads.nodes' > /tmp/review-threads.json
180
+ - id: validate
181
+ uses: ./.github/actions/validate-review-output
182
+ with:
183
+ output-file: ${{ steps.output.outputs.output-file }}
184
+ pr-number: ${{ needs.subject.outputs.pr }}
185
+ review-threads-file: /tmp/review-threads.json
186
+ conclude:
187
+ needs: [activation, subject, agent, safe_outputs, validate_output]
188
+ if: >
189
+ needs.agent.result == 'success' &&
190
+ needs.safe_outputs.result == 'success' &&
191
+ needs.validate_output.outputs.valid == 'true'
192
+ runs-on: agents-arc
193
+ permissions:
194
+ contents: write
195
+ issues: write
196
+ pull-requests: write
197
+ steps:
198
+ - name: Create bot token
199
+ id: app-token
200
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
201
+ with:
202
+ client-id: ${{ secrets.BOT_APP_ID }}
203
+ private-key: ${{ secrets.BOT_PRIVATE_KEY }}
204
+ - name: Checkout repository
205
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
206
+ with:
207
+ token: ${{ steps.app-token.outputs.token }}
208
+ fetch-depth: 0
209
+ - name: Apply agent output
210
+ uses: ./.github/actions/apply-agent-output
211
+ with:
212
+ artifact-name: ${{ needs.activation.outputs.artifact_prefix }}agent
213
+ token: ${{ steps.app-token.outputs.token }}
214
+ push-to-branch: 'true'
215
+ apply-labels: 'false'
216
+ - name: Release implemented review
217
+ if: needs.validate_output.outputs.outcome == 'implemented'
218
+ uses: ./.github/actions/remove-issue-labels
219
+ with:
220
+ token: ${{ steps.app-token.outputs.token }}
221
+ issue-number: ${{ needs.subject.outputs.issue }}
222
+ labels: ${{ env.WORKING_LABEL }}
223
+ - name: Flag review outcome
224
+ if: needs.validate_output.outputs.outcome != 'implemented'
225
+ uses: ./.github/actions/add-issue-labels
226
+ with:
227
+ token: ${{ steps.app-token.outputs.token }}
228
+ issue-number: ${{ needs.subject.outputs.issue }}
229
+ labels: ${{ env.REVIEW_LABEL }}
230
+ - name: Release review outcome
231
+ if: needs.validate_output.outputs.outcome != 'implemented'
232
+ uses: ./.github/actions/remove-issue-labels
233
+ with:
234
+ token: ${{ steps.app-token.outputs.token }}
235
+ issue-number: ${{ needs.subject.outputs.issue }}
236
+ labels: |
237
+ ${{ env.WORKING_LABEL }}
238
+ ${{ env.PR_PENDING_LABEL }}
239
+ incomplete:
240
+ needs: [subject, agent, safe_outputs, validate_output]
241
+ if: >
242
+ always() &&
243
+ needs.subject.outputs.found == 'true' &&
244
+ (needs.agent.result != 'success' || needs.safe_outputs.result != 'success' || needs.validate_output.outputs.valid != 'true')
245
+ runs-on: agents-arc
246
+ permissions:
247
+ contents: read
248
+ issues: write
249
+ steps:
250
+ - name: Checkout workflow actions
251
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
252
+ with:
253
+ persist-credentials: false
254
+ - name: Create bot token
255
+ id: app-token
256
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
257
+ with:
258
+ client-id: ${{ secrets.BOT_APP_ID }}
259
+ private-key: ${{ secrets.BOT_PRIVATE_KEY }}
260
+ - name: Release the issue
261
+ uses: ./.github/actions/remove-issue-labels
262
+ with:
263
+ token: ${{ steps.app-token.outputs.token }}
264
+ issue-number: ${{ needs.subject.outputs.issue }}
265
+ labels: ${{ env.WORKING_LABEL }}
266
+ - name: Flag for human review
267
+ uses: ./.github/actions/add-issue-labels
268
+ with:
269
+ token: ${{ steps.app-token.outputs.token }}
270
+ issue-number: ${{ needs.subject.outputs.issue }}
271
+ labels: ${{ env.REVIEW_LABEL }}
272
+ - name: Report missing review feedback outcome
273
+ uses: ./.github/actions/create-issue-comment
274
+ with:
275
+ token: ${{ steps.app-token.outputs.token }}
276
+ issue-number: ${{ needs.subject.outputs.issue }}
277
+ body: |
278
+ ${{ env.REVIEW_MARKER }}
279
+ ${{ env.INCOMPLETE_COMMENT }}
280
+ [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
281
+
282
+ if: needs.subject.outputs.found == 'true'
283
+
284
+ runs-on: agents-arc
285
+ runs-on-slim: agents-arc
286
+
287
+ secrets:
288
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
289
+
290
+ engine:
291
+ id: opencode
292
+ version: "1.2.14"
293
+ env:
294
+ OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
295
+
296
+ model: openai/glm-5-3
297
+
298
+ max-turns: 300
299
+ max-turn-cache-misses: 3000
300
+ max-ai-credits: 5000
301
+
302
+
303
+ permissions: read-all
304
+
305
+ # Rung 3. A reviewer often makes one point across several comments, so the agent needs the
306
+ # whole conversation, not just the one that triggered it. Fetching all of it is deterministic.
307
+ steps:
308
+ - name: Load the issue context
309
+ uses: ./.github/actions/load-issue-context
310
+ with:
311
+ token: ${{ github.token }}
312
+ issue-number: ${{ needs.subject.outputs.issue }}
313
+ output-path: ${{ env.ISSUE_CONTEXT_PATH }}
314
+ - name: Fetch the whole review conversation and the diff
315
+ env:
316
+ GH_TOKEN: ${{ github.token }}
317
+ REPO: ${{ github.repository }}
318
+ PR: ${{ inputs.pr-number }}
319
+ run: |
320
+ set -euo pipefail
321
+ mkdir -p /tmp/gh-aw/agent
322
+ gh pr diff "$PR" --repo "$REPO" > /tmp/gh-aw/agent/diff.patch
323
+ gh api "repos/$REPO/pulls/$PR/comments" --paginate \
324
+ --jq '[.[] | {id, path, line, author: .user.login, body, in_reply_to_id}]' \
325
+ > /tmp/gh-aw/agent/review-comments.json
326
+ gh api "repos/$REPO/issues/$PR/comments" --paginate \
327
+ --jq '[.[] | {author: .user.login, body}]' \
328
+ > /tmp/gh-aw/agent/conversation.json
329
+ gh api graphql -f query='
330
+ query($owner:String!, $name:String!, $number:Int!) {
331
+ repository(owner:$owner, name:$name) {
332
+ pullRequest(number:$number) {
333
+ reviewThreads(first:100) {
334
+ nodes { id
335
+ isResolved isOutdated path
336
+ comments(first:50) { nodes { author { login } body } }
337
+ }
338
+ }
339
+ }
340
+ }
341
+ }' \
342
+ -f owner="${REPO%/*}" -f name="${REPO#*/}" -F number="$PR" \
343
+ --jq '.data.repository.pullRequest.reviewThreads.nodes' \
344
+ > /tmp/gh-aw/agent/review-threads.json
345
+
346
+ safe-outputs:
347
+ # A failed run is already a red run. An issue per failure buries the real backlog
348
+ # under noise nobody closes.
349
+ report-failure-as-issue: false
350
+ threat-detection: false
351
+ # target "*" because these workers are dispatched, not triggered by the pull request:
352
+ # the default "triggering" target has no pull request in context and rejects the push
353
+ # with "requires pull request context", so the agent's fix is computed and discarded.
354
+ push-to-pull-request-branch:
355
+ target: "*"
356
+ add-comment:
357
+ target: "*"
358
+
359
+
360
+ timeout-minutes: 45
361
+ ---
362
+
363
+ 1. You are applying review feedback to pull request
364
+ **#${{ needs.subject.outputs.pr }}**, which has
365
+ **${{ needs.subject.outputs.unresolved }}** unresolved review thread(s).
366
+
367
+ It has already been confirmed that this pull request is open, that we authored it, and that
368
+ the reviewer has write access. Do not re-check any of that.
369
+
370
+ 2. Read `${{ env.ISSUE_CONTEXT_PATH }}`. It contains the issue body and discussion this pull
371
+ request closes. The acceptance criteria there define what the implementation must still
372
+ satisfy after the feedback is applied.
373
+
374
+ 3. Read the whole conversation, not just the comment that triggered you. It is already on disk:
375
+
376
+ - `/tmp/gh-aw/agent/review-threads.json` , every thread with its resolved and outdated state
377
+ - `/tmp/gh-aw/agent/review-comments.json` , every inline comment with its file and line
378
+ - `/tmp/gh-aw/agent/conversation.json` , every conversation comment
379
+ - `/tmp/gh-aw/agent/diff.patch` , the current diff
380
+
381
+ A reviewer often makes one point across several comments. Applying them one at a time
382
+ produces contradictory commits, so read everything before changing anything.
383
+
384
+ 4. Work out which items are genuinely actionable and still outstanding. Skip anything already
385
+ addressed by a later commit, anything a bot wrote, anything from the pull request author,
386
+ and anything that is a question rather than a request. You must account for every unresolved
387
+ human review thread by its `PRRT_...` ID. Do not claim feedback was implemented when no branch
388
+ change is needed: that requires reviewer confirmation.
389
+
390
+ 5. Load only skills required by the feedback, then apply it. Make only changes the feedback
391
+ justifies: a review comment is not licence for unrelated refactoring. Never read outside this
392
+ repository root. Follow repository documentation and established conventions. Keep changes
393
+ focused, protect secrets, and do not modify generated files unless the feedback requires it.
394
+ Adhere to ${{ env.REPO_RULES }}.
395
+
396
+ 6. Run the repository verification commands below. The issue context at
397
+ `${{ env.ISSUE_CONTEXT_PATH }}` defines acceptance criteria the fix must satisfy. If a check
398
+ fails, fix what you broke and run it again. Do not push a branch that does not pass.
399
+
400
+ **Scoped verification.** This runner has limited memory, and a whole-repo lint or build
401
+ can be killed mid-run. Scope verification to the files you actually changed first, and
402
+ only escalate to the full suite when the scoped run passes and you are still unsure:
403
+ - Lint/format (biome, eslint, prettier, ruff, etc.): pass the changed file paths as
404
+ arguments so the tool checks only those files (e.g. `pnpm exec biome check <files>`),
405
+ never the whole repository.
406
+ - Build: prefer building only the project(s) containing the changed files; use the full
407
+ solution build only when the change crosses project boundaries.
408
+ - Tests: run the test project covering the changed files; run the full suite only when
409
+ the change is cross-cutting.
410
+
411
+ ```
412
+ ${{ env.VERIFY_COMMANDS }}
413
+ ```
414
+
415
+ 7. Before pushing, run the project's lint fix command (e.g. `pnpm lint:fix` or
416
+ `pnpm exec biome check --write <changed-files>`) to auto-format. If lint:fix is not
417
+ available, fix formatting manually. Never push code with lint errors.
418
+
419
+ 8. Select one review outcome.
420
+
421
+ - **implemented**: You made the requested change, verification passed, and you will propose
422
+ exactly one `push_to_pull_request_branch`.
423
+ - **already-satisfied**: The current branch already satisfies the request. Do not push; the
424
+ reviewer must confirm this assessment.
425
+ - **needs-human**: The feedback is ambiguous, unsafe, or cannot be applied. Do not push.
426
+
427
+ 9. Emit exactly one `add_comment` on PR `${{ needs.subject.outputs.pr }}`. Include every
428
+ unresolved human review thread ID and an explanation for it, then exactly one line:
429
+ `**Review outcome:** implemented`, `**Review outcome:** already-satisfied`, or
430
+ `**Review outcome:** needs-human`.
431
+
432
+ 9. Do not merge, close, or change labels. The workflow validates your outcome and owns those
433
+ state transitions.
434
+
435
+ 10. Ignore the `## Diagram` section below. It is documentation for humans and contains no
436
+ instructions for you.
437
+
438
+ ## Diagram
439
+
440
+ ```mermaid
441
+ flowchart TD
442
+ fbStart("Work Router<br/>apply-review route<br/>(review on bot PR)") --> fbSubject
443
+ fbSubject["Subject (rung 4)<br/>Our open PR? Substantive review?"] -->|✓| fbFacts
444
+ fbSubject -.->|✗| fbIdle
445
+ fbFacts("Facts (rung 3)<br/>Threads, comments, diff to disk") --> fbTriage
446
+ fbTriage["Triage<br/>Anything actionable and outstanding?"] -->|✓| fbReserve
447
+ fbTriage -.->|nothing| fbIdle
448
+ fbReserve("Reserve<br/>Propose bot-working on the issue") -->|✓| fbApply
449
+ fbApply("Apply<br/>Only what the feedback justifies") -->|✓| fbVerify
450
+ fbApply -.->|✗| fbFail
451
+ fbVerify["Verify<br/>/repo-verify passes?<br/>↻"] -->|✓| fbPush
452
+ fbVerify -.->|✗| fbApply
453
+ fbPush(("Pushed<br/>Same branch, bot-working removed"))
454
+ fbIdle(("Idle<br/>Not ours, or nothing to do"))
455
+ fbFail(("Fail<br/>review added, bot-working removed"))
456
+
457
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
458
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
459
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
460
+ classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
461
+ classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
462
+ classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
463
+ class fbStart start
464
+ class fbFacts,fbReserve,fbApply action
465
+ class fbSubject,fbTriage,fbVerify decision
466
+ class fbIdle idle
467
+ class fbFail failure
468
+ class fbPush success
469
+ ```