@plainconceptsplatform/workflows 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog-installation.d.ts +12 -0
- package/dist/catalog-installation.js +81 -0
- package/dist/catalog-installation.test.d.ts +1 -0
- package/dist/catalog-installation.test.js +85 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +57 -0
- package/dist/repository-inspection.d.ts +23 -0
- package/dist/repository-inspection.js +91 -0
- package/dist/repository-state.d.ts +18 -0
- package/dist/repository-state.js +77 -0
- package/dist/repository-state.test.d.ts +1 -0
- package/dist/repository-state.test.js +96 -0
- package/dist/workflow-catalog.d.ts +17 -0
- package/dist/workflow-catalog.js +39 -0
- package/dist/workflow-catalog.test.d.ts +1 -0
- package/dist/workflow-catalog.test.js +14 -0
- package/loops/actions/add-issue-labels/action.yml +29 -0
- package/loops/actions/agent-output.cjs +16 -0
- package/loops/actions/apply-agent-bundle/action.yml +23 -0
- package/loops/actions/apply-agent-bundle/apply-bundle.sh +45 -0
- package/loops/actions/apply-agent-comments/action.yml +41 -0
- package/loops/actions/apply-agent-labels/action.yml +54 -0
- package/loops/actions/apply-agent-output/action.yml +107 -0
- package/loops/actions/audit-close/action.yml +103 -0
- package/loops/actions/classify-route/action.yml +90 -0
- package/loops/actions/classify-route/classify-route.sh +172 -0
- package/loops/actions/cleanup-artifacts/action.yml +64 -0
- package/loops/actions/close-agent-issues/action.yml +42 -0
- package/loops/actions/create-agent-issues/action.yml +51 -0
- package/loops/actions/create-issue-comment/action.yml +28 -0
- package/loops/actions/download-agent-output/action.yml +52 -0
- package/loops/actions/identify-gate-subject/action.yml +96 -0
- package/loops/actions/link-pr-to-issue/action.yml +39 -0
- package/loops/actions/list-open-issues/action.yml +32 -0
- package/loops/actions/load-issue-context/action.yml +42 -0
- package/loops/actions/merge-agent-pr/action.yml +35 -0
- package/loops/actions/push-agent-branch/action.yml +44 -0
- package/loops/actions/remove-issue-labels/action.yml +34 -0
- package/loops/actions/stale-recovery/action.yml +287 -0
- package/loops/actions/update-agent-issues/action.yml +57 -0
- package/loops/actions/validate-refine-output/action.yml +43 -0
- package/loops/actions/validate-refine-output/validate-refine-output.sh +46 -0
- package/loops/actions/verify-composite-actions/action.yml +8 -0
- package/loops/actions/verify-composite-actions/verify-composite-actions.sh +50 -0
- package/loops/actions/verify-refine-output/action.yml +8 -0
- package/loops/actions/verify-refine-output/verify-refine-output.sh +65 -0
- package/loops/actions/verify-route-matrix/action.yml +8 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +212 -0
- package/loops/aw/repo-config.md +12 -0
- package/loops/scripts/compile-agent-workflows.mjs +13 -0
- package/loops/workflows/agent-apply-review.md +372 -0
- package/loops/workflows/agent-audit.md +197 -0
- package/loops/workflows/agent-direct.md +362 -0
- package/loops/workflows/agent-implement.md +329 -0
- package/loops/workflows/agent-merge-gate.md +470 -0
- package/loops/workflows/agent-propose.md +342 -0
- package/loops/workflows/agent-refine.md +338 -0
- package/loops/workflows/shared/opencode-ci.md +26 -0
- package/loops/workflows/shared/platform-defaults.md +14 -0
- package/loops/workflows/work-router.yml +391 -0
- package/package.json +28 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Exercise the router's real classifier. This sources classify-route.sh rather than
|
|
3
|
+
# restating it, so a change to the route table cannot pass here by being copied twice.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
ROUTER_YML="${HERE}/../../router/work-router.yml"
|
|
9
|
+
IMPLEMENT_WORKER_MD="${HERE}/../../agent-implement.md"
|
|
10
|
+
MERGE_GATE_WORKER_MD="${HERE}/../../agent-merge-gate.md"
|
|
11
|
+
|
|
12
|
+
# shellcheck source-path=SCRIPTDIR
|
|
13
|
+
# shellcheck source=../classify-route/classify-route.sh
|
|
14
|
+
source "${HERE}/../classify-route/classify-route.sh"
|
|
15
|
+
|
|
16
|
+
PASS=0
|
|
17
|
+
FAIL=0
|
|
18
|
+
|
|
19
|
+
# Classify one event and read a single field out of the result.
|
|
20
|
+
route_field() {
|
|
21
|
+
local field="$1"
|
|
22
|
+
shift
|
|
23
|
+
|
|
24
|
+
local key value
|
|
25
|
+
local -a assignments=("$@")
|
|
26
|
+
|
|
27
|
+
(
|
|
28
|
+
unset EVENT ACTION LABEL ISSUE_LABELS EVENT_ISSUE_NUMBER EVENT_PR_NUMBER \
|
|
29
|
+
COMMENT_ON_PR COMMENT_SENDER_TYPE RUN_PR_NUMBER RUN_CONCLUSION RUN_ID \
|
|
30
|
+
SCHEDULE OPERATION INPUT_ISSUE_NUMBER INPUT_PR_NUMBER INPUT_MODE \
|
|
31
|
+
INPUT_CI_CONCLUSION INPUT_CI_RUN_ID INPUT_TRIGGER_KIND
|
|
32
|
+
|
|
33
|
+
for assignment in "${assignments[@]}"; do
|
|
34
|
+
key="${assignment%%=*}"
|
|
35
|
+
value="${assignment#*=}"
|
|
36
|
+
export "${key}=${value}"
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
classify_route | sed -n "s/^${field}=//p"
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
assert() {
|
|
44
|
+
local label="$1" expected="$2" actual="$3"
|
|
45
|
+
|
|
46
|
+
if [ "$expected" = "$actual" ]; then
|
|
47
|
+
PASS=$((PASS + 1))
|
|
48
|
+
else
|
|
49
|
+
FAIL=$((FAIL + 1))
|
|
50
|
+
printf 'FAIL: %s\n expected: %s\n actual: %s\n' "$label" "$expected" "$actual" >&2
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
assert_route() {
|
|
55
|
+
local label="$1" expected="$2"
|
|
56
|
+
shift 2
|
|
57
|
+
assert "$label" "$expected" "$(route_field route "$@")"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
echo "── Label events ──────────────────────────────────────────────────────────"
|
|
61
|
+
assert_route "refine label routes to refine" refine \
|
|
62
|
+
EVENT=issues ACTION=labeled LABEL=refine EVENT_ISSUE_NUMBER=42
|
|
63
|
+
assert_route "implement label routes to implement" implement \
|
|
64
|
+
EVENT=issues ACTION=labeled LABEL=implement EVENT_ISSUE_NUMBER=42
|
|
65
|
+
assert_route "unrelated label routes nowhere" none \
|
|
66
|
+
EVENT=issues ACTION=labeled LABEL=documentation EVENT_ISSUE_NUMBER=42
|
|
67
|
+
assert_route "a non-label issue action routes nowhere" none \
|
|
68
|
+
EVENT=issues ACTION=opened EVENT_ISSUE_NUMBER=42
|
|
69
|
+
assert "refine label starts a first pass" first \
|
|
70
|
+
"$(route_field refine-mode EVENT=issues ACTION=labeled LABEL=refine EVENT_ISSUE_NUMBER=42)"
|
|
71
|
+
assert_route "direct label routes to direct" direct \
|
|
72
|
+
EVENT=issues ACTION=labeled LABEL=direct EVENT_ISSUE_NUMBER=42
|
|
73
|
+
assert "direct label starts a first pass" first \
|
|
74
|
+
"$(route_field direct-mode EVENT=issues ACTION=labeled LABEL=direct EVENT_ISSUE_NUMBER=42)"
|
|
75
|
+
|
|
76
|
+
echo "── Comment events ────────────────────────────────────────────────────────"
|
|
77
|
+
assert_route "a comment on a pull request routes to apply-review" apply-review \
|
|
78
|
+
EVENT=issue_comment COMMENT_ON_PR=true EVENT_ISSUE_NUMBER=7
|
|
79
|
+
assert_route "an author reply on a refine issue re-refines" refine \
|
|
80
|
+
EVENT=issue_comment COMMENT_ON_PR=false COMMENT_SENDER_TYPE=User \
|
|
81
|
+
'ISSUE_LABELS=["refine","review"]' EVENT_ISSUE_NUMBER=42
|
|
82
|
+
assert "an author reply is a rerefine pass" rerefine \
|
|
83
|
+
"$(route_field refine-mode EVENT=issue_comment COMMENT_ON_PR=false \
|
|
84
|
+
COMMENT_SENDER_TYPE=User 'ISSUE_LABELS=["refine"]' EVENT_ISSUE_NUMBER=42)"
|
|
85
|
+
assert_route "the bot's own comment never re-enters refine" none \
|
|
86
|
+
EVENT=issue_comment COMMENT_ON_PR=false COMMENT_SENDER_TYPE=Bot \
|
|
87
|
+
'ISSUE_LABELS=["refine"]' EVENT_ISSUE_NUMBER=42
|
|
88
|
+
assert_route "a comment on an issue without refine routes nowhere" none \
|
|
89
|
+
EVENT=issue_comment COMMENT_ON_PR=false COMMENT_SENDER_TYPE=User \
|
|
90
|
+
'ISSUE_LABELS=["bug"]' EVENT_ISSUE_NUMBER=42
|
|
91
|
+
assert_route "an author reply on a direct issue continues" direct \
|
|
92
|
+
EVENT=issue_comment COMMENT_ON_PR=false COMMENT_SENDER_TYPE=User \
|
|
93
|
+
'ISSUE_LABELS=["direct"]' EVENT_ISSUE_NUMBER=42
|
|
94
|
+
assert "a direct reply is a continue pass" continue \
|
|
95
|
+
"$(route_field direct-mode EVENT=issue_comment COMMENT_ON_PR=false \
|
|
96
|
+
COMMENT_SENDER_TYPE=User 'ISSUE_LABELS=["direct"]' EVENT_ISSUE_NUMBER=42)"
|
|
97
|
+
assert_route "the bot's own comment never re-enters direct" none \
|
|
98
|
+
EVENT=issue_comment COMMENT_ON_PR=false COMMENT_SENDER_TYPE=Bot \
|
|
99
|
+
'ISSUE_LABELS=["direct"]' EVENT_ISSUE_NUMBER=42
|
|
100
|
+
|
|
101
|
+
echo "── Review events ─────────────────────────────────────────────────────────"
|
|
102
|
+
assert_route "a review comment routes to apply-review" apply-review \
|
|
103
|
+
EVENT=pull_request_review_comment EVENT_PR_NUMBER=7
|
|
104
|
+
assert_route "a submitted review routes to apply-review" apply-review \
|
|
105
|
+
EVENT=pull_request_review EVENT_PR_NUMBER=7
|
|
106
|
+
assert_route "a pull_request_target routes to bot-approve" bot-approve \
|
|
107
|
+
EVENT=pull_request_target ACTION=opened
|
|
108
|
+
|
|
109
|
+
echo "── CI completion ─────────────────────────────────────────────────────────"
|
|
110
|
+
assert_route "App CI completion routes to merge-gate" merge-gate \
|
|
111
|
+
EVENT=workflow_run RUN_PR_NUMBER=7 RUN_CONCLUSION=success RUN_ID=99
|
|
112
|
+
assert "merge-gate carries the CI conclusion" failure \
|
|
113
|
+
"$(route_field ci-conclusion EVENT=workflow_run RUN_PR_NUMBER=7 \
|
|
114
|
+
RUN_CONCLUSION=failure RUN_ID=99)"
|
|
115
|
+
assert_route "a CI run with no pull request routes nowhere" none \
|
|
116
|
+
EVENT=workflow_run RUN_PR_NUMBER= RUN_CONCLUSION=success RUN_ID=99
|
|
117
|
+
|
|
118
|
+
echo "── Schedules ─────────────────────────────────────────────────────────────"
|
|
119
|
+
while read -r cron; do
|
|
120
|
+
selected="$(route_field route EVENT=schedule "SCHEDULE=${cron}")"
|
|
121
|
+
|
|
122
|
+
if [ "$selected" = "none" ]; then
|
|
123
|
+
FAIL=$((FAIL + 1))
|
|
124
|
+
echo "FAIL: cron '${cron}' in work-router.yml maps to no route" >&2
|
|
125
|
+
else
|
|
126
|
+
PASS=$((PASS + 1))
|
|
127
|
+
echo " ${cron} -> ${selected}"
|
|
128
|
+
fi
|
|
129
|
+
done < <(sed -n 's/^ *- cron: "\(.*\)"$/\1/p' "$ROUTER_YML")
|
|
130
|
+
|
|
131
|
+
assert_route "an unknown cron routes nowhere" none EVENT=schedule "SCHEDULE=0 0 30 2 *"
|
|
132
|
+
|
|
133
|
+
echo "── Manual dispatch ───────────────────────────────────────────────────────"
|
|
134
|
+
assert_route "refine dispatch needs an issue number" none \
|
|
135
|
+
EVENT=workflow_dispatch OPERATION=refine INPUT_ISSUE_NUMBER=
|
|
136
|
+
assert_route "refine dispatch rejects a non-numeric issue" none \
|
|
137
|
+
EVENT=workflow_dispatch OPERATION=refine INPUT_ISSUE_NUMBER=abc
|
|
138
|
+
assert_route "refine dispatch accepts a positive issue" refine \
|
|
139
|
+
EVENT=workflow_dispatch OPERATION=refine INPUT_ISSUE_NUMBER=42
|
|
140
|
+
assert_route "direct dispatch accepts a positive issue" direct \
|
|
141
|
+
EVENT=workflow_dispatch OPERATION=direct INPUT_ISSUE_NUMBER=42
|
|
142
|
+
assert_route "direct dispatch needs an issue number" none \
|
|
143
|
+
EVENT=workflow_dispatch OPERATION=direct INPUT_ISSUE_NUMBER=
|
|
144
|
+
assert_route "merge-gate dispatch needs a pull request number" none \
|
|
145
|
+
EVENT=workflow_dispatch OPERATION=merge-gate INPUT_PR_NUMBER=0
|
|
146
|
+
assert_route "merge-gate dispatch accepts a positive pull request" merge-gate \
|
|
147
|
+
EVENT=workflow_dispatch OPERATION=merge-gate INPUT_PR_NUMBER=7
|
|
148
|
+
assert_route "stale-recovery dispatch needs no numbers" stale-recovery \
|
|
149
|
+
EVENT=workflow_dispatch OPERATION=stale-recovery
|
|
150
|
+
assert_route "an unknown operation routes nowhere" none \
|
|
151
|
+
EVENT=workflow_dispatch OPERATION=deploy-everything
|
|
152
|
+
assert "a scheduled audit reports its trigger kind" scheduled \
|
|
153
|
+
"$(route_field trigger-kind EVENT=schedule "SCHEDULE=17 1 * * 1")"
|
|
154
|
+
assert "a dispatched audit reports its trigger kind" manual \
|
|
155
|
+
"$(route_field trigger-kind EVENT=workflow_dispatch OPERATION=audit INPUT_TRIGGER_KIND=manual)"
|
|
156
|
+
assert_route "propose dispatch needs no numbers" propose \
|
|
157
|
+
EVENT=workflow_dispatch OPERATION=propose
|
|
158
|
+
assert "a scheduled propose reports its trigger kind" scheduled \
|
|
159
|
+
"$(route_field trigger-kind EVENT=schedule "SCHEDULE=29 7 * * *")"
|
|
160
|
+
assert "a dispatched propose reports its trigger kind" manual \
|
|
161
|
+
"$(route_field trigger-kind EVENT=workflow_dispatch OPERATION=propose INPUT_TRIGGER_KIND=manual)"
|
|
162
|
+
|
|
163
|
+
echo "── Router wiring ─────────────────────────────────────────────────────────"
|
|
164
|
+
if grep -Fq 'protected-files: allowed' "$IMPLEMENT_WORKER_MD" &&
|
|
165
|
+
grep -Fq 'protected_changes:' "$MERGE_GATE_WORKER_MD"; then
|
|
166
|
+
PASS=$((PASS + 1))
|
|
167
|
+
else
|
|
168
|
+
FAIL=$((FAIL + 1))
|
|
169
|
+
echo "FAIL: protected changes are not held for Merge Gate review" >&2
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# This repository is public. Every route a human can start from a comment, a review or a
|
|
173
|
+
# label must pass the authorize gate, or anyone able to comment can start a model run that
|
|
174
|
+
# writes code. Asserted here because removing the gate would otherwise be a silent, one-line
|
|
175
|
+
# change that nothing fails on.
|
|
176
|
+
for route in refine implement direct apply-review; do
|
|
177
|
+
if grep -qE "route == '${route}'.*needs\.authorize\.outputs\.trusted == 'true'" "$ROUTER_YML"; then
|
|
178
|
+
PASS=$((PASS + 1))
|
|
179
|
+
else
|
|
180
|
+
FAIL=$((FAIL + 1))
|
|
181
|
+
echo "FAIL: route '${route}' does not require needs.authorize.outputs.trusted" >&2
|
|
182
|
+
fi
|
|
183
|
+
done
|
|
184
|
+
|
|
185
|
+
for route in refine implement direct apply-review merge-gate audit propose bot-approve \
|
|
186
|
+
audit-close cleanup-artifacts stale-recovery validate; do
|
|
187
|
+
if grep -q "route == '${route}'" "$ROUTER_YML"; then
|
|
188
|
+
PASS=$((PASS + 1))
|
|
189
|
+
else
|
|
190
|
+
FAIL=$((FAIL + 1))
|
|
191
|
+
echo "FAIL: work-router.yml has no job for route '${route}'" >&2
|
|
192
|
+
fi
|
|
193
|
+
done
|
|
194
|
+
|
|
195
|
+
while read -r operation; do
|
|
196
|
+
if grep -q "route == '${operation}'" "$ROUTER_YML"; then
|
|
197
|
+
PASS=$((PASS + 1))
|
|
198
|
+
else
|
|
199
|
+
FAIL=$((FAIL + 1))
|
|
200
|
+
echo "FAIL: dispatch operation '${operation}' has no job in work-router.yml" >&2
|
|
201
|
+
fi
|
|
202
|
+
done < <(sed -n '/^ operation:/,/^ issue-number:/p' "$ROUTER_YML" |
|
|
203
|
+
sed -n 's/^ - //p')
|
|
204
|
+
|
|
205
|
+
echo
|
|
206
|
+
if [ "$FAIL" -eq 0 ]; then
|
|
207
|
+
echo "Route matrix: ${PASS} passed"
|
|
208
|
+
else
|
|
209
|
+
echo "Route matrix: ${PASS} passed, ${FAIL} FAILED" >&2
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
exit $((FAIL > 0))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
env:
|
|
3
|
+
VERIFY_COMMANDS: |
|
|
4
|
+
Add repository-specific verification commands here.
|
|
5
|
+
REPO_RULES: |
|
|
6
|
+
Add repository-specific agent rules here.
|
|
7
|
+
description: Consumer-owned repository workflow configuration.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Repository workflow configuration
|
|
11
|
+
|
|
12
|
+
This file belongs to the consumer repository. The workflow package creates it only when absent and never overwrites it.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const compile = spawnSync("gh", ["aw", "compile", "--strict", "--dir", "loops"], {
|
|
4
|
+
stdio: "inherit",
|
|
5
|
+
shell: process.platform === "win32",
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
if (compile.error?.code === "ENOENT" || compile.status === null) {
|
|
9
|
+
process.stderr.write("Could not run `gh aw compile`. Install githubnext/gh-aw first.\n");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
process.exit(compile.status ?? 1);
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
---
|
|
2
|
+
env:
|
|
3
|
+
WORKING_LABEL: bot-working
|
|
4
|
+
REVIEW_LABEL: review
|
|
5
|
+
REVIEW_MARKER: "<!-- agent-apply-review -->"
|
|
6
|
+
INCOMPLETE_COMMENT: "Automated review feedback ended without an outcome. The issue remains for a retry."
|
|
7
|
+
ISSUE_CONTEXT_PATH: /tmp/gh-aw/agent/issue-context.json
|
|
8
|
+
GIT_AUTHOR_NAME: "github-actions[bot]"
|
|
9
|
+
GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
|
|
10
|
+
GIT_COMMITTER_NAME: "github-actions[bot]"
|
|
11
|
+
GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com"
|
|
12
|
+
description: |
|
|
13
|
+
Applies reviewer feedback to an open pull request the bot authored, then pushes the fixes to
|
|
14
|
+
the same branch. Called by the Work Router; does not trigger on public events.
|
|
15
|
+
|
|
16
|
+
`target: triggering` is enough for the push here, so unlike the merge gate this needs no
|
|
17
|
+
wildcard fetch: the event is the pull request and gh-aw has already checked out its head.
|
|
18
|
+
|
|
19
|
+
name: "Agent: Apply Review"
|
|
20
|
+
|
|
21
|
+
# Router-only worker. The Work Router owns triggers, classification, and rung 1-2 checks.
|
|
22
|
+
# This workflow receives the classified inputs and runs rung 3+.
|
|
23
|
+
imports:
|
|
24
|
+
- shared/platform-defaults.md
|
|
25
|
+
- shared/opencode-ci.md
|
|
26
|
+
- shared/repo-config.md
|
|
27
|
+
|
|
28
|
+
on:
|
|
29
|
+
workflow_call:
|
|
30
|
+
inputs:
|
|
31
|
+
pr-number:
|
|
32
|
+
description: Pull request number to apply review feedback on.
|
|
33
|
+
required: true
|
|
34
|
+
type: string
|
|
35
|
+
linked-issue:
|
|
36
|
+
description: Issue number the pull request closes. May be empty.
|
|
37
|
+
required: false
|
|
38
|
+
type: string
|
|
39
|
+
|
|
40
|
+
# Rung 4. Router has classified the event; this job validates PR ownership and checks for
|
|
41
|
+
# substantive feedback. A custom job, not `on.steps`, because the prompt needs these values.
|
|
42
|
+
jobs:
|
|
43
|
+
subject:
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
permissions:
|
|
46
|
+
contents: read
|
|
47
|
+
issues: read
|
|
48
|
+
pull-requests: read
|
|
49
|
+
outputs:
|
|
50
|
+
found: ${{ steps.subject.outputs.found }}
|
|
51
|
+
pr: ${{ steps.subject.outputs.pr }}
|
|
52
|
+
issue: ${{ steps.subject.outputs.issue }}
|
|
53
|
+
unresolved: ${{ steps.subject.outputs.unresolved }}
|
|
54
|
+
steps:
|
|
55
|
+
- name: Confirm this is our pull request and the feedback is substantive
|
|
56
|
+
id: subject
|
|
57
|
+
env:
|
|
58
|
+
GH_TOKEN: ${{ github.token }}
|
|
59
|
+
REPO: ${{ github.repository }}
|
|
60
|
+
PR: ${{ inputs.pr-number }}
|
|
61
|
+
LINKED_ISSUE: ${{ inputs.linked-issue }}
|
|
62
|
+
run: |
|
|
63
|
+
set -euo pipefail
|
|
64
|
+
|
|
65
|
+
none() {
|
|
66
|
+
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
67
|
+
echo "$1"
|
|
68
|
+
exit 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pr=$(gh pr view "$PR" --repo "$REPO" --json number,state,author,title,body)
|
|
72
|
+
[ "$(printf '%s' "$pr" | jq -r '.state')" = "OPEN" ] || none "PR #$PR is not open"
|
|
73
|
+
|
|
74
|
+
# Use GitHub's bot flag because App login formats differ across APIs.
|
|
75
|
+
author=$(printf '%s' "$pr" | jq -r '.author.login')
|
|
76
|
+
is_bot=$(printf '%s' "$pr" | jq -r '.author.is_bot')
|
|
77
|
+
[ "$is_bot" = "true" ] || \
|
|
78
|
+
none "PR #$PR was authored by $author, a human. Never push to a human's branch."
|
|
79
|
+
|
|
80
|
+
# The issue this pull request closes, preferring the router-provided linked issue.
|
|
81
|
+
if [ -n "$LINKED_ISSUE" ]; then
|
|
82
|
+
issue="$LINKED_ISSUE"
|
|
83
|
+
else
|
|
84
|
+
issue=$(printf '%s' "$pr" | jq -r '.body // ""' \
|
|
85
|
+
| grep -oiE '(close[sd]?|fixe?[sd]?|resolve[sd]?) +#[0-9]+' \
|
|
86
|
+
| grep -oE '[0-9]+' | head -n 1 || true)
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# Unresolved review threads, from the API rather than from the model's reading of it.
|
|
90
|
+
unresolved=$(gh api graphql -f query='
|
|
91
|
+
query($owner:String!, $name:String!, $number:Int!) {
|
|
92
|
+
repository(owner:$owner, name:$name) {
|
|
93
|
+
pullRequest(number:$number) {
|
|
94
|
+
reviewThreads(first:100) { nodes { isResolved isOutdated } }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}' \
|
|
98
|
+
-f owner="${REPO%/*}" -f name="${REPO#*/}" -F number="$PR" \
|
|
99
|
+
--jq '[.data.repository.pullRequest.reviewThreads.nodes[]
|
|
100
|
+
| select(.isResolved == false and .isOutdated == false)] | length')
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
echo "found=true"
|
|
104
|
+
echo "pr=$PR"
|
|
105
|
+
echo "issue=${issue:-}"
|
|
106
|
+
echo "unresolved=$unresolved"
|
|
107
|
+
} >> "$GITHUB_OUTPUT"
|
|
108
|
+
echo "PR #$PR has $unresolved unresolved thread(s)"
|
|
109
|
+
|
|
110
|
+
reserve:
|
|
111
|
+
needs: subject
|
|
112
|
+
if: needs.subject.outputs.found == 'true' && needs.subject.outputs.issue != ''
|
|
113
|
+
runs-on: ubuntu-latest
|
|
114
|
+
permissions:
|
|
115
|
+
contents: read
|
|
116
|
+
issues: write
|
|
117
|
+
steps:
|
|
118
|
+
- name: Checkout workflow actions
|
|
119
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
120
|
+
with:
|
|
121
|
+
persist-credentials: false
|
|
122
|
+
- name: Create bot token
|
|
123
|
+
id: app-token
|
|
124
|
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
125
|
+
with:
|
|
126
|
+
client-id: ${{ secrets.BOT_APP_ID }}
|
|
127
|
+
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
128
|
+
- name: Mark the linked issue as in progress
|
|
129
|
+
uses: ./.github/actions/add-issue-labels
|
|
130
|
+
with:
|
|
131
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
132
|
+
issue-number: ${{ needs.subject.outputs.issue }}
|
|
133
|
+
labels: ${{ env.WORKING_LABEL }}
|
|
134
|
+
- name: Clear the human-needed flag
|
|
135
|
+
uses: ./.github/actions/remove-issue-labels
|
|
136
|
+
with:
|
|
137
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
138
|
+
issue-number: ${{ needs.subject.outputs.issue }}
|
|
139
|
+
labels: ${{ env.REVIEW_LABEL }}
|
|
140
|
+
conclude:
|
|
141
|
+
needs: [activation, subject, agent, safe_outputs]
|
|
142
|
+
if: >
|
|
143
|
+
needs.agent.result == 'success' &&
|
|
144
|
+
needs.safe_outputs.result == 'success'
|
|
145
|
+
runs-on: ubuntu-latest
|
|
146
|
+
permissions:
|
|
147
|
+
contents: write
|
|
148
|
+
issues: write
|
|
149
|
+
pull-requests: write
|
|
150
|
+
steps:
|
|
151
|
+
- name: Create bot token
|
|
152
|
+
id: app-token
|
|
153
|
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
154
|
+
with:
|
|
155
|
+
client-id: ${{ secrets.BOT_APP_ID }}
|
|
156
|
+
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
157
|
+
- name: Checkout repository
|
|
158
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
159
|
+
with:
|
|
160
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
161
|
+
fetch-depth: 0
|
|
162
|
+
- name: Apply agent output
|
|
163
|
+
uses: ./.github/actions/apply-agent-output
|
|
164
|
+
with:
|
|
165
|
+
artifact-name: ${{ needs.activation.outputs.artifact_prefix }}agent
|
|
166
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
167
|
+
push-to-branch: 'true'
|
|
168
|
+
incomplete:
|
|
169
|
+
needs: [subject, agent, safe_outputs]
|
|
170
|
+
if: >
|
|
171
|
+
always() &&
|
|
172
|
+
needs.subject.outputs.found == 'true' &&
|
|
173
|
+
needs.agent.result != 'success'
|
|
174
|
+
runs-on: ubuntu-latest
|
|
175
|
+
permissions:
|
|
176
|
+
contents: read
|
|
177
|
+
issues: write
|
|
178
|
+
steps:
|
|
179
|
+
- name: Checkout workflow actions
|
|
180
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
181
|
+
with:
|
|
182
|
+
persist-credentials: false
|
|
183
|
+
- name: Create bot token
|
|
184
|
+
id: app-token
|
|
185
|
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
186
|
+
with:
|
|
187
|
+
client-id: ${{ secrets.BOT_APP_ID }}
|
|
188
|
+
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
189
|
+
- name: Release the issue
|
|
190
|
+
uses: ./.github/actions/remove-issue-labels
|
|
191
|
+
with:
|
|
192
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
193
|
+
issue-number: ${{ needs.subject.outputs.issue }}
|
|
194
|
+
labels: ${{ env.WORKING_LABEL }}
|
|
195
|
+
- name: Flag for human review
|
|
196
|
+
uses: ./.github/actions/add-issue-labels
|
|
197
|
+
with:
|
|
198
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
199
|
+
issue-number: ${{ needs.subject.outputs.issue }}
|
|
200
|
+
labels: ${{ env.REVIEW_LABEL }}
|
|
201
|
+
- name: Report missing review feedback outcome
|
|
202
|
+
uses: ./.github/actions/create-issue-comment
|
|
203
|
+
with:
|
|
204
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
205
|
+
issue-number: ${{ needs.subject.outputs.issue }}
|
|
206
|
+
body: |
|
|
207
|
+
${{ env.REVIEW_MARKER }}
|
|
208
|
+
${{ env.INCOMPLETE_COMMENT }}
|
|
209
|
+
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
210
|
+
|
|
211
|
+
if: needs.subject.outputs.found == 'true'
|
|
212
|
+
|
|
213
|
+
runs-on: ubuntu-latest
|
|
214
|
+
runs-on-slim: ubuntu-latest
|
|
215
|
+
|
|
216
|
+
secrets:
|
|
217
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
218
|
+
|
|
219
|
+
engine:
|
|
220
|
+
id: opencode
|
|
221
|
+
version: "1.2.14"
|
|
222
|
+
env:
|
|
223
|
+
OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
|
|
224
|
+
|
|
225
|
+
model: openai/glm-5-2
|
|
226
|
+
|
|
227
|
+
max-turns: 300
|
|
228
|
+
max-turn-cache-misses: 3000
|
|
229
|
+
max-ai-credits: 5000
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
permissions: read-all
|
|
233
|
+
|
|
234
|
+
# Rung 3. A reviewer often makes one point across several comments, so the agent needs the
|
|
235
|
+
# whole conversation, not just the one that triggered it. Fetching all of it is deterministic.
|
|
236
|
+
steps:
|
|
237
|
+
- name: Load the issue context
|
|
238
|
+
uses: ./.github/actions/load-issue-context
|
|
239
|
+
with:
|
|
240
|
+
token: ${{ github.token }}
|
|
241
|
+
issue-number: ${{ needs.subject.outputs.issue }}
|
|
242
|
+
output-path: ${{ env.ISSUE_CONTEXT_PATH }}
|
|
243
|
+
- name: Fetch the whole review conversation and the diff
|
|
244
|
+
env:
|
|
245
|
+
GH_TOKEN: ${{ github.token }}
|
|
246
|
+
REPO: ${{ github.repository }}
|
|
247
|
+
PR: ${{ inputs.pr-number }}
|
|
248
|
+
run: |
|
|
249
|
+
set -euo pipefail
|
|
250
|
+
mkdir -p /tmp/gh-aw/agent
|
|
251
|
+
gh pr diff "$PR" --repo "$REPO" > /tmp/gh-aw/agent/diff.patch
|
|
252
|
+
gh api "repos/$REPO/pulls/$PR/comments" --paginate \
|
|
253
|
+
--jq '[.[] | {id, path, line, author: .user.login, body, in_reply_to_id}]' \
|
|
254
|
+
> /tmp/gh-aw/agent/review-comments.json
|
|
255
|
+
gh api "repos/$REPO/issues/$PR/comments" --paginate \
|
|
256
|
+
--jq '[.[] | {author: .user.login, body}]' \
|
|
257
|
+
> /tmp/gh-aw/agent/conversation.json
|
|
258
|
+
gh api graphql -f query='
|
|
259
|
+
query($owner:String!, $name:String!, $number:Int!) {
|
|
260
|
+
repository(owner:$owner, name:$name) {
|
|
261
|
+
pullRequest(number:$number) {
|
|
262
|
+
reviewThreads(first:100) {
|
|
263
|
+
nodes {
|
|
264
|
+
isResolved isOutdated path
|
|
265
|
+
comments(first:50) { nodes { author { login } body } }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}' \
|
|
271
|
+
-f owner="${REPO%/*}" -f name="${REPO#*/}" -F number="$PR" \
|
|
272
|
+
--jq '.data.repository.pullRequest.reviewThreads.nodes' \
|
|
273
|
+
> /tmp/gh-aw/agent/review-threads.json
|
|
274
|
+
|
|
275
|
+
safe-outputs:
|
|
276
|
+
staged: true
|
|
277
|
+
threat-detection: false
|
|
278
|
+
push-to-pull-request-branch:
|
|
279
|
+
add-comment:
|
|
280
|
+
add-labels:
|
|
281
|
+
remove-labels:
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
timeout-minutes: 45
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
1. You are applying review feedback to pull request
|
|
288
|
+
**#${{ needs.subject.outputs.pr }}**, which has
|
|
289
|
+
**${{ needs.subject.outputs.unresolved }}** unresolved review thread(s).
|
|
290
|
+
|
|
291
|
+
It has already been confirmed that this pull request is open, that we authored it, and that
|
|
292
|
+
the reviewer has write access. Do not re-check any of that.
|
|
293
|
+
|
|
294
|
+
2. Read `${{ env.ISSUE_CONTEXT_PATH }}`. It contains the issue body and discussion this pull
|
|
295
|
+
request closes. The acceptance criteria there define what the implementation must still
|
|
296
|
+
satisfy after the feedback is applied.
|
|
297
|
+
|
|
298
|
+
3. Read the whole conversation, not just the comment that triggered you. It is already on disk:
|
|
299
|
+
|
|
300
|
+
- `/tmp/gh-aw/agent/review-threads.json` , every thread with its resolved and outdated state
|
|
301
|
+
- `/tmp/gh-aw/agent/review-comments.json` , every inline comment with its file and line
|
|
302
|
+
- `/tmp/gh-aw/agent/conversation.json` , every conversation comment
|
|
303
|
+
- `/tmp/gh-aw/agent/diff.patch` , the current diff
|
|
304
|
+
|
|
305
|
+
A reviewer often makes one point across several comments. Applying them one at a time
|
|
306
|
+
produces contradictory commits, so read everything before changing anything.
|
|
307
|
+
|
|
308
|
+
4. Work out which items are genuinely actionable and still outstanding. Skip anything already
|
|
309
|
+
addressed by a later commit, anything a bot wrote, anything from the pull request author,
|
|
310
|
+
and anything that is a question rather than a request. If nothing is actionable, call
|
|
311
|
+
`add_labels` to add `${{ env.REVIEW_LABEL }}`, then call `add_comment` saying so and stop.
|
|
312
|
+
|
|
313
|
+
5. Load only skills required by the feedback, then apply it. Make only changes the feedback
|
|
314
|
+
justifies: a review comment is not licence for unrelated refactoring. Never read outside this
|
|
315
|
+
repository root. Follow these repository-specific rules:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
$${{ env.REPO_RULES }}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
6. Run the repository verification commands below. The issue context at
|
|
322
|
+
`${{ env.ISSUE_CONTEXT_PATH }}` defines acceptance criteria the fix must satisfy. If a check
|
|
323
|
+
fails, fix what you broke and run it again. Do not push a branch that does not pass.
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
$${{ env.VERIFY_COMMANDS }}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
7. Call `push_to_pull_request_branch` to push the verified changes. Do not merge, do not
|
|
330
|
+
close anything, and do not change any label other than `bot-working`.
|
|
331
|
+
|
|
332
|
+
8. Call `add_comment` once:
|
|
333
|
+
`Feedback implemented. Please review and merge when ready.`
|
|
334
|
+
followed by a short list of what you changed and, if anything was deliberately not changed,
|
|
335
|
+
which item and why.
|
|
336
|
+
|
|
337
|
+
9. Call `remove_labels` to remove `bot-working`. If you failed, call `add_labels` to add
|
|
338
|
+
`${{ env.REVIEW_LABEL }}`, then say so in the comment rather than leaving the pull request
|
|
339
|
+
looking untouched.
|
|
340
|
+
|
|
341
|
+
## Diagram
|
|
342
|
+
|
|
343
|
+
```mermaid
|
|
344
|
+
flowchart TD
|
|
345
|
+
fbStart("Work Router<br/>apply-review route<br/>(review on bot PR)") --> fbSubject
|
|
346
|
+
fbSubject["Subject (rung 4)<br/>Our open PR? Substantive review?"] -->|✓| fbFacts
|
|
347
|
+
fbSubject -.->|✗| fbIdle
|
|
348
|
+
fbFacts("Facts (rung 3)<br/>Threads, comments, diff to disk") --> fbTriage
|
|
349
|
+
fbTriage["Triage<br/>Anything actionable and outstanding?"] -->|✓| fbReserve
|
|
350
|
+
fbTriage -.->|nothing| fbIdle
|
|
351
|
+
fbReserve("Reserve<br/>Propose bot-working on the issue") -->|✓| fbApply
|
|
352
|
+
fbApply("Apply<br/>Only what the feedback justifies") -->|✓| fbVerify
|
|
353
|
+
fbApply -.->|✗| fbFail
|
|
354
|
+
fbVerify["Verify<br/>/repo-verify passes?<br/>↻"] -->|✓| fbPush
|
|
355
|
+
fbVerify -.->|✗| fbApply
|
|
356
|
+
fbPush(("Pushed<br/>Same branch, bot-working removed"))
|
|
357
|
+
fbIdle(("Idle<br/>Not ours, or nothing to do"))
|
|
358
|
+
fbFail(("Fail<br/>review added, bot-working removed"))
|
|
359
|
+
|
|
360
|
+
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
|
|
361
|
+
classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
|
|
362
|
+
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
|
|
363
|
+
classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
|
|
364
|
+
classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
|
|
365
|
+
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
|
|
366
|
+
class fbStart start
|
|
367
|
+
class fbFacts,fbReserve,fbApply action
|
|
368
|
+
class fbSubject,fbTriage,fbVerify decision
|
|
369
|
+
class fbIdle idle
|
|
370
|
+
class fbFail failure
|
|
371
|
+
class fbPush success
|
|
372
|
+
```
|