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