@plainconceptsplatform/workflows 0.5.1 → 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 (58) hide show
  1. package/dist/action-validation.test.d.ts +1 -0
  2. package/dist/action-validation.test.js +87 -0
  3. package/dist/catalog-installation.d.ts +30 -0
  4. package/dist/catalog-installation.js +422 -0
  5. package/dist/catalog-installation.test.d.ts +1 -0
  6. package/dist/catalog-installation.test.js +485 -0
  7. package/dist/catalog-listing.d.ts +13 -0
  8. package/dist/catalog-listing.js +70 -0
  9. package/dist/catalog-listing.test.d.ts +1 -0
  10. package/dist/catalog-listing.test.js +150 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +218 -0
  13. package/dist/index.test.d.ts +1 -0
  14. package/dist/index.test.js +273 -0
  15. package/dist/repository-inspection.d.ts +23 -0
  16. package/dist/repository-inspection.js +113 -0
  17. package/dist/repository-inspection.test.d.ts +1 -0
  18. package/dist/repository-inspection.test.js +77 -0
  19. package/dist/route-processing.d.ts +6 -0
  20. package/dist/route-processing.js +149 -0
  21. package/dist/route-processing.test.d.ts +1 -0
  22. package/dist/route-processing.test.js +283 -0
  23. package/dist/stack-defaults.d.ts +11 -0
  24. package/dist/stack-defaults.js +104 -0
  25. package/dist/stack-defaults.test.d.ts +1 -0
  26. package/dist/stack-defaults.test.js +266 -0
  27. package/dist/tui.d.ts +25 -0
  28. package/dist/tui.js +287 -0
  29. package/dist/tui.test.d.ts +1 -0
  30. package/dist/tui.test.js +249 -0
  31. package/dist/workflow-catalog.d.ts +25 -0
  32. package/dist/workflow-catalog.js +55 -0
  33. package/dist/workflow-catalog.test.d.ts +1 -0
  34. package/dist/workflow-catalog.test.js +29 -0
  35. package/loops/actions/add-issue-labels/action.yml +2 -2
  36. package/loops/actions/apply-agent-bundle/apply-bundle.sh +14 -1
  37. package/loops/actions/classify-route/action.yml +7 -0
  38. package/loops/actions/classify-route/classify-route.sh +20 -5
  39. package/loops/actions/cleanup-artifacts/action.yml +38 -12
  40. package/loops/actions/identify-gate-subject/action.yml +3 -1
  41. package/loops/actions/remove-issue-labels/action.yml +4 -2
  42. package/loops/actions/validate-refine-output/validate-refine-output.sh +18 -2
  43. package/loops/actions/verify-refine-output/verify-refine-output.sh +20 -0
  44. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +300 -16
  45. package/loops/scripts/compile-agent-workflows.mjs +96 -1
  46. package/loops/scripts/merge-changelog.mjs +76 -0
  47. package/loops/templates/agentics/actionlint.yaml +13 -0
  48. package/loops/templates/agentics/agentics-checks.yml +203 -86
  49. package/loops/templates/ci/app-ci-dotnet-next.yml +64 -2
  50. package/loops/templates/ci/app-ci-node-monorepo.yml +52 -0
  51. package/loops/templates/opencode/opencode.ci.json +2 -2
  52. package/loops/templates/opencode/opencode.ci.json.md +4 -1
  53. package/loops/workflows/agent-apply-review.md +5 -1
  54. package/loops/workflows/agent-implement.md +105 -3
  55. package/loops/workflows/agent-merge-gate.md +139 -25
  56. package/loops/workflows/agent-refine.md +26 -2
  57. package/loops/workflows/work-router.yml +227 -51
  58. package/package.json +1 -1
@@ -1,86 +1,203 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/templates/agentics/agentics-checks.yml. Update with `workflows update --force`; consumer edits may be overwritten.
2
- name: Agentics checks
3
-
4
- on:
5
- pull_request:
6
- paths:
7
- - .github/workflows/*.md
8
- - .github/workflows/shared/*.md
9
- - .github/workflows/*.yml
10
- - .github/workflows/aw.json
11
- - .github/actions/**
12
-
13
- env:
14
- GH_AW_VERSION: v0.83.4
15
- ACTIONLINT_VERSION: 1.7.7
16
- ACTIONLINT_SHA256: 023070a287cd8cccd71515fedc843f1985bf96c436b7effaecce67290e7e0757
17
- AGENTICS_COMPILE_COMMAND: gh aw compile --strict
18
-
19
- permissions:
20
- contents: read
21
-
22
- concurrency:
23
- group: agentics-checks-${{ github.event.pull_request.number }}
24
- cancel-in-progress: true
25
-
26
- jobs:
27
- verify-lockfiles:
28
- name: Verify generated agent lockfiles
29
- runs-on: ubuntu-latest
30
- timeout-minutes: 10
31
- steps:
32
- - name: Checkout repository
33
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
34
- with:
35
- persist-credentials: false
36
- - name: Install gh-aw
37
- uses: github/gh-aw-actions/setup@e89c65e17eb281bbd5ff2ff9e9199a03e96654c7 # v0.83.4
38
- with:
39
- version: ${{ env.GH_AW_VERSION }}
40
- - name: Compile agent workflows strictly
41
- run: ${{ env.AGENTICS_COMPILE_COMMAND }}
42
- - name: Verify generated lockfiles are current
43
- shell: bash
44
- run: |
45
- set -euo pipefail
46
- if git diff --exit-code -- .github/workflows/*.lock.yml; then
47
- exit 0
48
- fi
49
- if git diff --exit-code -I '^[[:space:]]*GH_AW_INFO_MODEL_COSTS:' -- .github/workflows/*.lock.yml; then
50
- echo "::warning::Only volatile GH_AW_INFO_MODEL_COSTS metadata changed."
51
- exit 0
52
- fi
53
- echo "::error::Generated agent lockfiles are stale. Run '${AGENTICS_COMPILE_COMMAND}' and commit the resulting .lock.yml files."
54
- git diff -- .github/workflows/*.lock.yml
55
- exit 1
56
-
57
- lint-workflows:
58
- name: Lint authored workflows and shell
59
- runs-on: ubuntu-latest
60
- timeout-minutes: 10
61
- steps:
62
- - name: Checkout repository
63
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
64
- with:
65
- persist-credentials: false
66
- - name: Install actionlint
67
- shell: bash
68
- run: |
69
- set -euo pipefail
70
- tarball="$RUNNER_TEMP/actionlint.tar.gz"
71
- curl -fsSL -o "$tarball" "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
72
- echo "${ACTIONLINT_SHA256} $tarball" | sha256sum --check --strict
73
- mkdir -p "$RUNNER_TEMP/bin"
74
- tar -xzf "$tarball" -C "$RUNNER_TEMP/bin" actionlint
75
- echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
76
- - name: Lint authored workflows
77
- shell: bash
78
- run: |
79
- set -euo pipefail
80
- mapfile -t authored < <(git ls-files '.github/workflows/*.yml' | grep -vE '\.lock\.yml$|agentics-maintenance\.yml$')
81
- if [ "${#authored[@]}" -gt 0 ]; then
82
- actionlint "${authored[@]}"
83
- fi
84
- - name: Lint composite action shell
85
- shell: bash
86
- run: find .github/actions -name '*.sh' -print0 | xargs -0 -r shellcheck -x
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/templates/agentics/agentics-checks.yml. Update with `workflows update --force`; consumer edits may be overwritten.
2
+ name: Agentics checks
3
+
4
+ on:
5
+ pull_request:
6
+ paths:
7
+ - .github/workflows/*.md
8
+ - .github/workflows/shared/*.md
9
+ - .github/workflows/*.yml
10
+ - .github/workflows/aw.json
11
+ - .github/actions/**
12
+ # A push to the default branch that bypassed review is exactly the case that needs the check:
13
+ # seven broken locks reached Pliny-Bot's main in twenty minutes with no pull request and no
14
+ # run of this workflow. A red run here is the detection.
15
+ push:
16
+ branches: [main]
17
+ paths:
18
+ - .github/workflows/*.md
19
+ - .github/workflows/shared/*.md
20
+ - .github/workflows/*.yml
21
+ - .github/workflows/aw.json
22
+ - .github/actions/**
23
+
24
+ env:
25
+ # Must match the version the locks were compiled with (first line of every .lock.yml) and the
26
+ # `github/gh-aw/...@vX` import pin in every worker. A different compiler regenerates every lock
27
+ # and the freshness diff measures the version, not the change under review.
28
+ GH_AW_VERSION: v0.87.5
29
+ ACTIONLINT_VERSION: 1.7.7
30
+ ACTIONLINT_SHA256: 023070a287cd8cccd71515fedc843f1985bf96c436b7effaecce67290e7e0757
31
+ SHELLCHECK_VERSION: 0.10.0
32
+ SHELLCHECK_SHA256: 6c881ab0698e4e6ea235245f22832860544f17ba386442fe7e9d629f8cbedf87
33
+ # The same script a developer runs. It wraps `gh aw compile --strict` and applies the
34
+ # repository's post-processing; a bare `gh aw compile` on this side would regenerate every lock
35
+ # without the transform, and the diff below could never be clean.
36
+ AGENTICS_COMPILE_COMMAND: node scripts/compile-agent-workflows.mjs
37
+
38
+ permissions:
39
+ contents: read
40
+
41
+ concurrency:
42
+ group: agentics-checks-${{ github.event.pull_request.number || github.ref }}
43
+ cancel-in-progress: true
44
+
45
+ jobs:
46
+ verify-lockfiles:
47
+ name: Verify generated agent lockfiles
48
+ runs-on: ubuntu-latest
49
+ timeout-minutes: 10
50
+ env:
51
+ # gh-aw resolves the `github/gh-aw/...` imports through the GitHub API on every compile,
52
+ # and an unauthenticated gh on a hosted runner refuses API calls.
53
+ GH_TOKEN: ${{ github.token }}
54
+ steps:
55
+ - name: Checkout repository
56
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
57
+ with:
58
+ persist-credentials: false
59
+ - name: Install gh-aw
60
+ shell: bash
61
+ env:
62
+ GH_TOKEN: ${{ github.token }}
63
+ run: |
64
+ set -euo pipefail
65
+ gh extension install github/gh-aw --pin "$GH_AW_VERSION"
66
+ gh aw --version
67
+ # The compile command is a node script. Hosted images have node; a setup step pins the
68
+ # major version so the script sees the same runtime everywhere.
69
+ - name: Set up Node
70
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
71
+ with:
72
+ node-version: 22
73
+ - name: Verify the locks and the workers agree on the gh-aw version
74
+ shell: bash
75
+ run: |
76
+ set -euo pipefail
77
+ # `gh aw --version` writes to stderr; without 2>&1 the grep saw nothing and set -e
78
+ # ended the step with no message at all.
79
+ want="$(gh aw --version 2>&1 | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)"
80
+ [ "$want" = "$GH_AW_VERSION" ] || { echo "::error::gh aw reports '${want:-nothing}', this workflow pins $GH_AW_VERSION"; exit 1; }
81
+ status=0
82
+ for lock in .github/workflows/*.lock.yml; do
83
+ have="$(sed -n '1s/.*"compiler_version":"\([^"]*\)".*/\1/p' "$lock")"
84
+ if [ "$have" != "$want" ]; then
85
+ echo "::error::$lock was compiled by ${have:-an unknown version}; this job runs $want. Recompile with the pinned version, or bump GH_AW_VERSION and the worker import pins together."
86
+ status=1
87
+ fi
88
+ done
89
+ while read -r file pin; do
90
+ if [ "$pin" != "$want" ]; then
91
+ echo "::error::$file imports gh-aw shared files at $pin; this job runs $want."
92
+ status=1
93
+ fi
94
+ done < <(grep -HoE 'github/gh-aw/[^@]*@v[0-9]+\.[0-9]+\.[0-9]+' .github/workflows/*.md | sed -E 's/^([^:]*):.*@(v[0-9.]+)$/\1 \2/' | sort -u)
95
+ exit "$status"
96
+ - name: Compile agent workflows strictly
97
+ run: ${{ env.AGENTICS_COMPILE_COMMAND }}
98
+ - name: Verify generated lockfiles are current
99
+ shell: bash
100
+ run: |
101
+ set -euo pipefail
102
+ if git diff --exit-code -- .github/workflows/*.lock.yml; then
103
+ exit 0
104
+ fi
105
+ if git diff --exit-code -I '^[[:space:]]*GH_AW_INFO_MODEL_COSTS:' -- .github/workflows/*.lock.yml; then
106
+ echo "::warning::Only volatile GH_AW_INFO_MODEL_COSTS metadata changed."
107
+ exit 0
108
+ fi
109
+ echo "::error::Generated agent lockfiles are stale. Run '${AGENTICS_COMPILE_COMMAND}' and commit the resulting .lock.yml files."
110
+ git diff -- .github/workflows/*.lock.yml
111
+ exit 1
112
+ # gh aw compile leaves a lock alone when its hashes still match the source, so the script
113
+ # must recognise a lock it already processed. If a second run changes anything, the
114
+ # post-processing is not idempotent and every consumer's freshness check is measuring the
115
+ # transform instead of the drift.
116
+ - name: Verify a second compile changes nothing
117
+ shell: bash
118
+ run: |
119
+ set -euo pipefail
120
+ cp -R .github/workflows "$RUNNER_TEMP/first-pass"
121
+ ${AGENTICS_COMPILE_COMMAND}
122
+ if ! diff -r -I '^[[:space:]]*GH_AW_INFO_MODEL_COSTS:' "$RUNNER_TEMP/first-pass" .github/workflows; then
123
+ echo "::error::Running '${AGENTICS_COMPILE_COMMAND}' twice produced different locks; the post-processing is not idempotent."
124
+ exit 1
125
+ fi
126
+
127
+ lint-workflows:
128
+ name: Lint authored workflows and shell
129
+ runs-on: ubuntu-latest
130
+ timeout-minutes: 10
131
+ steps:
132
+ - name: Checkout repository
133
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
134
+ with:
135
+ persist-credentials: false
136
+ - name: Install actionlint
137
+ shell: bash
138
+ run: |
139
+ set -euo pipefail
140
+ tarball="$RUNNER_TEMP/actionlint.tar.gz"
141
+ curl -fsSL -o "$tarball" "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
142
+ echo "${ACTIONLINT_SHA256} $tarball" | sha256sum --check --strict
143
+ mkdir -p "$RUNNER_TEMP/bin"
144
+ tar -xzf "$tarball" -C "$RUNNER_TEMP/bin" actionlint
145
+ echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
146
+ # Pinned rather than taken from the image: a copy of this job that ran on a self-hosted
147
+ # runner died with "shellcheck: No such file or directory".
148
+ - name: Install shellcheck
149
+ shell: bash
150
+ run: |
151
+ set -euo pipefail
152
+ tarball="$RUNNER_TEMP/shellcheck.tar.xz"
153
+ curl -fsSL -o "$tarball" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz"
154
+ echo "${SHELLCHECK_SHA256} $tarball" | sha256sum --check --strict
155
+ mkdir -p "$RUNNER_TEMP/bin"
156
+ tar -xJf "$tarball" -C "$RUNNER_TEMP/bin" --strip-components=1 "shellcheck-v${SHELLCHECK_VERSION}/shellcheck"
157
+ echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
158
+ # actionlint 1.7.7 predates `concurrency.queue`, which GitHub accepts and the router uses;
159
+ # that one message is a known false positive and is ignored by its exact text so anything
160
+ # else under `concurrency` still fails. shellcheck style notes (SC2129 and friends) are
161
+ # opinions about consumer scripts, not defects; warnings and errors still fail the job.
162
+ - name: Lint authored workflows
163
+ shell: bash
164
+ env:
165
+ SHELLCHECK_OPTS: -S warning
166
+ run: |
167
+ set -euo pipefail
168
+ mapfile -t authored < <(git ls-files '.github/workflows/*.yml' | grep -vE '\.lock\.yml$|agentics-maintenance\.yml$')
169
+ if [ "${#authored[@]}" -gt 0 ]; then
170
+ actionlint -ignore 'unexpected key "queue" for "concurrency" section' "${authored[@]}"
171
+ fi
172
+ - name: Lint composite action shell
173
+ shell: bash
174
+ run: find .github/actions -name '*.sh' -print0 | xargs -0 -r shellcheck -x
175
+ # A password written into a workflow is a finding whatever it protects, and one sat in
176
+ # these files for weeks: a throwaway SQL container credential, repeated inline in six
177
+ # to nine places per repository, found by a policy scan rather than by us. Anything a
178
+ # job needs at container-start time belongs in an expression or a secret, and this is
179
+ # the check that says so before a reviewer has to.
180
+ - name: No written-down passwords in workflows
181
+ shell: bash
182
+ run: |
183
+ set -euo pipefail
184
+ mapfile -t authored < <(git ls-files '.github/workflows/*.yml' '.github/actions/**' 2>/dev/null || true)
185
+ [ "${#authored[@]}" -gt 0 ] || exit 0
186
+ # Two passes, because "is this a literal" is the hard half. The first finds every
187
+ # assignment to a password-ish name whose value is quoted; the second keeps only
188
+ # the lines with no dollar sign, so a value built from an expression or read from a
189
+ # shell variable passes and a written-down word does not. A literal that happens to
190
+ # contain a dollar sign slips through; erring that way beats a check people learn to
191
+ # route around.
192
+ # Two shapes: a password-ish name assigned a quoted value, and a command-line
193
+ # flag given one. The literal these repositories carried appeared as both.
194
+ hits=$(grep -nEi \
195
+ -e "(password|passwd|pwd)[\"']?[[:space:]]*[:=][[:space:]]*[\"'][^\"']" \
196
+ -e "(^|[[:space:]])(-P|--password)[[:space:]=]*[\"'][^\"']" \
197
+ "${authored[@]}" | grep -v '[$]' || true)
198
+ if [ -n "$hits" ]; then
199
+ printf '%s\n' "$hits"
200
+ echo "::error::A password is written into a workflow. Use a secret, or derive it per run from an expression."
201
+ exit 1
202
+ fi
203
+ echo "No written-down passwords."
@@ -33,7 +33,17 @@ env:
33
33
  SEED_PROJECT: src/App.Seed
34
34
  PRIMARY_DATABASE: app
35
35
  DATA_LAYER_DATABASE: numa_dl
36
- SQL_SA_PASSWORD: "YourStrong!Passw0rd"
36
+ # Derived per run rather than written down. The container is ephemeral, bound to a random
37
+ # host port and reachable only from this job, so the value protects nothing, but a literal
38
+ # password in a config file is a policy finding wherever it sits, and this file is public.
39
+ # It has to be a workflow-level expression: service containers receive their environment
40
+ # before any step runs, so a value generated in a step would arrive too late.
41
+ #
42
+ # The same expression is repeated inside the service's `options` above rather than read
43
+ # from here, because the `env` context is not available in `jobs.<id>.services` — only
44
+ # inside that service's own `env:`. Written as ${{ env.SQL_SA_PASSWORD }} there, the health
45
+ # check probes with an empty password and the service never reports healthy.
46
+ SQL_SA_PASSWORD: "Aa1!${{ github.run_id }}"
37
47
  WEB_PACKAGE: "@app/web"
38
48
  NPM_REGISTRY_URL: "//npm.pkg.github.com/"
39
49
  TRUFFLEHOG_IMAGE: "trufflesecurity/trufflehog:latest"
@@ -66,7 +76,7 @@ jobs:
66
76
  # exit code 1". Steps read the assigned port from the job.services context.
67
77
  - 1433
68
78
  options: >-
69
- --health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${{ env.SQL_SA_PASSWORD }}' -Q 'SELECT 1' -C || exit 1"
79
+ --health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Aa1!${{ github.run_id }}' -Q 'SELECT 1' -C || exit 1"
70
80
  --health-interval 10s --health-timeout 5s --health-retries 12 --health-start-period 30s
71
81
  defaults:
72
82
  run:
@@ -266,3 +276,55 @@ jobs:
266
276
  with:
267
277
  format: cyclonedx-json
268
278
  artifact-name: ${{ env.SBOM_ARTIFACT_NAME }}
279
+
280
+ # A bot pull request is opened with GITHUB_TOKEN, and GitHub delivers no workflow_run event
281
+ # for the CI runs that follow (the run's actor is the bot). The router's CI-completion route
282
+ # never sees them, so this job hands the verdict over from inside CI. workflow_dispatch is
283
+ # one of the two events GITHUB_TOKEN may raise, so no App token is needed here, and the
284
+ # router treats a dispatch as trusted. Human pull requests are left to the workflow_run
285
+ # route, which does fire for them. The reconcile belt remains the hourly fallback.
286
+ #
287
+ # If a deploy pipeline calls this workflow with `uses:`, that caller must also grant
288
+ # `actions: write`. A called workflow may not request a scope its caller withheld, and the
289
+ # whole run fails to start with zero jobs and no annotation when it does.
290
+ dispatch-merge-gate:
291
+ name: Dispatch merge gate
292
+ needs: [api, mutation-api, web, mutation-web]
293
+ if: >
294
+ always() &&
295
+ github.event_name == 'pull_request' &&
296
+ github.event.pull_request.head.repo.full_name == github.repository &&
297
+ github.event.pull_request.draft == false &&
298
+ github.event.pull_request.user.type == 'Bot'
299
+ runs-on: agents-arc
300
+ timeout-minutes: 5
301
+ permissions:
302
+ contents: read
303
+ actions: write
304
+ steps:
305
+ - name: Dispatch the merge gate with this run's verdict
306
+ env:
307
+ GH_TOKEN: ${{ github.token }}
308
+ REPO: ${{ github.repository }}
309
+ REF: ${{ github.event.repository.default_branch }}
310
+ PR_NUMBER: ${{ github.event.pull_request.number }}
311
+ RUN_ID: ${{ github.run_id }}
312
+ RESULTS: ${{ join(needs.*.result, ' ') }}
313
+ run: |
314
+ set -euo pipefail
315
+ conclusion=success
316
+ for result in $RESULTS; do
317
+ case "$result" in
318
+ failure) conclusion=failure; break ;;
319
+ cancelled | timed_out) conclusion=cancelled ;;
320
+ esac
321
+ done
322
+ # A cancelled run was superseded by a newer push, whose own run dispatches.
323
+ if [ "$conclusion" = cancelled ]; then
324
+ echo "CI was cancelled; not dispatching the merge gate."
325
+ exit 0
326
+ fi
327
+ echo "Dispatching the merge gate for PR #$PR_NUMBER with CI $conclusion (run $RUN_ID)."
328
+ gh workflow run work-router.yml --repo "$REPO" --ref "$REF" \
329
+ -f operation=merge-gate -f pr-number="$PR_NUMBER" \
330
+ -f ci-conclusion="$conclusion" -f ci-run-id="$RUN_ID"
@@ -206,3 +206,55 @@ jobs:
206
206
  with:
207
207
  format: cyclonedx-json
208
208
  artifact-name: ${{ env.SBOM_ARTIFACT_NAME }}
209
+
210
+ # A bot pull request is opened with GITHUB_TOKEN, and GitHub delivers no workflow_run event
211
+ # for the CI runs that follow (the run's actor is the bot). The router's CI-completion route
212
+ # never sees them, so this job hands the verdict over from inside CI. workflow_dispatch is
213
+ # one of the two events GITHUB_TOKEN may raise, so no App token is needed here, and the
214
+ # router treats a dispatch as trusted. Human pull requests are left to the workflow_run
215
+ # route, which does fire for them. The reconcile belt remains the hourly fallback.
216
+ #
217
+ # If a deploy pipeline calls this workflow with `uses:`, that caller must also grant
218
+ # `actions: write`. A called workflow may not request a scope its caller withheld, and the
219
+ # whole run fails to start with zero jobs and no annotation when it does.
220
+ dispatch-merge-gate:
221
+ name: Dispatch merge gate
222
+ needs: [lint, web, mutation-web, e2e, desktop, mobile]
223
+ if: >
224
+ always() &&
225
+ github.event_name == 'pull_request' &&
226
+ github.event.pull_request.head.repo.full_name == github.repository &&
227
+ github.event.pull_request.draft == false &&
228
+ github.event.pull_request.user.type == 'Bot'
229
+ runs-on: agents-arc
230
+ timeout-minutes: 5
231
+ permissions:
232
+ contents: read
233
+ actions: write
234
+ steps:
235
+ - name: Dispatch the merge gate with this run's verdict
236
+ env:
237
+ GH_TOKEN: ${{ github.token }}
238
+ REPO: ${{ github.repository }}
239
+ REF: ${{ github.event.repository.default_branch }}
240
+ PR_NUMBER: ${{ github.event.pull_request.number }}
241
+ RUN_ID: ${{ github.run_id }}
242
+ RESULTS: ${{ join(needs.*.result, ' ') }}
243
+ run: |
244
+ set -euo pipefail
245
+ conclusion=success
246
+ for result in $RESULTS; do
247
+ case "$result" in
248
+ failure) conclusion=failure; break ;;
249
+ cancelled | timed_out) conclusion=cancelled ;;
250
+ esac
251
+ done
252
+ # A cancelled run was superseded by a newer push, whose own run dispatches.
253
+ if [ "$conclusion" = cancelled ]; then
254
+ echo "CI was cancelled; not dispatching the merge gate."
255
+ exit 0
256
+ fi
257
+ echo "Dispatching the merge gate for PR #$PR_NUMBER with CI $conclusion (run $RUN_ID)."
258
+ gh workflow run work-router.yml --repo "$REPO" --ref "$REF" \
259
+ -f operation=merge-gate -f pr-number="$PR_NUMBER" \
260
+ -f ci-conclusion="$conclusion" -f ci-run-id="$RUN_ID"
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "provider": {
32
32
  "forge": {
33
- "api": "{env:FORGE_API_URL}",
33
+ "api": "{env:OPENAI_BASE_URL}",
34
34
  "options": {
35
- "apiKey": "{env:FORGE_API_KEY}"
35
+ "apiKey": "{env:OPENAI_API_KEY}"
36
36
  },
37
37
  "models": {
38
38
  "glm-5-3": {
@@ -17,7 +17,10 @@ repository root as `opencode.ci.json`.
17
17
  ### Provider
18
18
 
19
19
  - The `forge` provider is OpenAI-compatible and reads its endpoint from
20
- `FORGE_API_URL` and its key from `FORGE_API_KEY` at runtime.
20
+ `OPENAI_BASE_URL` and its key from `OPENAI_API_KEY` at runtime. Those are the
21
+ two variables every worker's agent job sets (`engine.env` in the worker and
22
+ the `OPENAI_API_KEY` secret), so the file works with no consumer wiring. An
23
+ earlier template read `FORGE_API_URL`/`FORGE_API_KEY`, which nothing set.
21
24
  - Each consumer configures its own router endpoint, credential, model IDs, and
22
25
  capabilities. The workflow package does not assume a particular gateway
23
26
  host, authentication system, or model vendor.
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  # Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-apply-review.md. Update with `workflows update --force`; consumer edits may be overwritten.
3
3
  env:
4
+ # Printed by the prompt as the verification block. Consumers set their own commands.
5
+ VERIFY_COMMANDS: ""
4
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."
5
7
  WORKING_LABEL: bot-working
6
8
  REVIEW_LABEL: review
@@ -231,7 +233,9 @@ jobs:
231
233
  with:
232
234
  token: ${{ steps.app-token.outputs.token }}
233
235
  issue-number: ${{ needs.subject.outputs.issue }}
234
- labels: ${{ env.WORKING_LABEL }},${{ env.PR_PENDING_LABEL }}
236
+ labels: |
237
+ ${{ env.WORKING_LABEL }}
238
+ ${{ env.PR_PENDING_LABEL }}
235
239
  incomplete:
236
240
  needs: [subject, agent, safe_outputs, validate_output]
237
241
  if: >
@@ -12,7 +12,20 @@ env:
12
12
  GIT_COMMITTER_NAME: "github-actions[bot]"
13
13
  GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com"
14
14
  IMPLEMENT_MARKER: "<!-- agent-implement -->"
15
- INCOMPLETE_COMMENT: "Automated implementation ended without an outcome. The implement label remains for a retry."
15
+ ATTEMPT_MARKER: "<!-- agent-implement-attempt -->"
16
+ # The model provider fails in bursts: the same model answers "not found" or 401 for a minute
17
+ # and works again immediately after, and a run that dies that way used to burn the issue and
18
+ # hand it to a human. Retry those, and give up on the fifth, which is an outage not a blip.
19
+ MAX_ATTEMPTS: "5"
20
+ PARK_AT_ATTEMPT: "4"
21
+ # Only a run that died before it could do any work is worth repeating. A provider failure
22
+ # kills the run in a couple of minutes with no answer; a run that worked for half an hour and
23
+ # then failed produced an answer that was wrong, and repeating it costs the whole fleet the
24
+ # same half hour to be wrong again. Observed: "Model not found" died in seconds, while a run
25
+ # whose own build failed to compile had spent 182 turns, and an out-of-memory kill came after
26
+ # a full verification suite.
27
+ RETRY_UNDER_MINUTES: "6"
28
+ INCOMPLETE_COMMENT: "Automated implementation ran and ended without an outcome. The issue is released and flagged for review: a run that got this far and still failed will fail the same way again."
16
29
  ISSUE_CONTEXT_PATH: /tmp/gh-aw/agent/implementation-context.json
17
30
  GH_AW_ALLOWED_BOTS: "platform-devbox[bot],github-actions[bot]"
18
31
  description: |
@@ -42,6 +55,11 @@ on:
42
55
  description: Issue number to implement.
43
56
  required: true
44
57
  type: string
58
+ attempts_so_far:
59
+ description: Failed implement runs already made for this issue. Parked when it reaches the cap.
60
+ required: false
61
+ type: string
62
+ default: '0'
45
63
  jobs:
46
64
  eligibility:
47
65
  runs-on: agents-arc
@@ -202,6 +220,8 @@ jobs:
202
220
  permissions:
203
221
  contents: read
204
222
  issues: write
223
+ # the retry re-enters through the router, which is a workflow_dispatch
224
+ actions: write
205
225
  steps:
206
226
  - name: Checkout workflow actions
207
227
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -213,19 +233,90 @@ jobs:
213
233
  with:
214
234
  client-id: ${{ secrets.BOT_APP_ID }}
215
235
  private-key: ${{ secrets.BOT_PRIVATE_KEY }}
236
+ - name: Decide whether this failure is worth repeating
237
+ id: decide
238
+ env:
239
+ GH_TOKEN: ${{ github.token }}
240
+ REPO: ${{ github.repository }}
241
+ RUN_ID: ${{ github.run_id }}
242
+ ATTEMPTS: ${{ inputs.attempts_so_far || '0' }}
243
+ PARK_AT: ${{ env.PARK_AT_ATTEMPT }}
244
+ UNDER_MINUTES: ${{ env.RETRY_UNDER_MINUTES }}
245
+ run: |
246
+ set -euo pipefail
247
+ # The agent job belongs to this same run: a called workflow shares the caller's run id.
248
+ read -r started finished <<<"$(gh api "repos/$REPO/actions/runs/$RUN_ID/jobs?per_page=100" \
249
+ --jq '[.jobs[] | select(.name | endswith("agent"))] | last // empty
250
+ | "\(.started_at // "") \(.completed_at // "")"')"
251
+ minutes=-1
252
+ if [ -n "${started:-}" ] && [ -n "${finished:-}" ]; then
253
+ minutes=$(( ( $(date -u -d "$finished" +%s) - $(date -u -d "$started" +%s) ) / 60 ))
254
+ fi
255
+ retry=false
256
+ # An unknown duration is treated as a long run: never retry on a guess.
257
+ if [ "$minutes" -ge 0 ] && [ "$minutes" -lt "$UNDER_MINUTES" ] && [ "$ATTEMPTS" -lt "$PARK_AT" ]; then
258
+ retry=true
259
+ fi
260
+ {
261
+ echo "retry=$retry"
262
+ echo "next=$((ATTEMPTS + 1))"
263
+ echo "minutes=$minutes"
264
+ } >> "$GITHUB_OUTPUT"
265
+ echo "agent job ran for ${minutes}m; attempts so far ${ATTEMPTS}; retry=${retry}"
266
+ # The attempt is recorded before any label moves, so a failure in the steps below leaves a
267
+ # run that can be counted rather than an issue released with nothing to show for it.
268
+ - name: Report the failed attempt
269
+ if: steps.decide.outputs.retry == 'true'
270
+ uses: ./.github/actions/create-issue-comment
271
+ with:
272
+ token: ${{ steps.app-token.outputs.token }}
273
+ issue-number: ${{ inputs.issue-number }}
274
+ body: |
275
+ ${{ env.ATTEMPT_MARKER }}
276
+ Attempt ${{ steps.decide.outputs.next }} of ${{ env.MAX_ATTEMPTS }} ended after ${{ steps.decide.outputs.minutes }} minutes, before the run could produce an answer. That is what a provider outage looks like, so this is being retried.
277
+ The issue keeps `implement`.
278
+ [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
279
+ - name: Release the reservation for the retry
280
+ if: steps.decide.outputs.retry == 'true'
281
+ uses: ./.github/actions/remove-issue-labels
282
+ with:
283
+ token: ${{ steps.app-token.outputs.token }}
284
+ issue-number: ${{ inputs.issue-number }}
285
+ labels: ${{ env.WORKING_LABEL }}
286
+ - name: Send the issue back through the router
287
+ if: steps.decide.outputs.retry == 'true'
288
+ env:
289
+ GH_TOKEN: ${{ github.token }}
290
+ REPO: ${{ github.repository }}
291
+ REF: ${{ github.event.repository.default_branch }}
292
+ ISSUE_NUMBER: ${{ inputs.issue-number }}
293
+ NEXT: ${{ steps.decide.outputs.next }}
294
+ run: |
295
+ set -euo pipefail
296
+ # The provider recovers in seconds, so pause before re-entering rather than dispatching
297
+ # back into the same outage. The router's own classify and authorize jobs add more.
298
+ sleep 30
299
+ gh workflow run work-router.yml --repo "$REPO" --ref "$REF" \
300
+ -f operation=implement -f issue-number="$ISSUE_NUMBER" -f attempts_so_far="$NEXT"
301
+ echo "Re-dispatched implement for #$ISSUE_NUMBER as attempt $NEXT."
216
302
  - name: Release the selected issue
303
+ if: steps.decide.outputs.retry != 'true'
217
304
  uses: ./.github/actions/remove-issue-labels
218
305
  with:
219
306
  token: ${{ steps.app-token.outputs.token }}
220
307
  issue-number: ${{ inputs.issue-number }}
221
- labels: ${{ env.WORKING_LABEL }},implement
308
+ labels: |
309
+ ${{ env.WORKING_LABEL }}
310
+ implement
222
311
  - name: Flag for human review
312
+ if: steps.decide.outputs.retry != 'true'
223
313
  uses: ./.github/actions/add-issue-labels
224
314
  with:
225
315
  token: ${{ steps.app-token.outputs.token }}
226
316
  issue-number: ${{ inputs.issue-number }}
227
317
  labels: ${{ env.REVIEW_LABEL }}
228
318
  - name: Report missing implementation outcome
319
+ if: steps.decide.outputs.retry != 'true'
229
320
  uses: ./.github/actions/create-issue-comment
230
321
  with:
231
322
  token: ${{ steps.app-token.outputs.token }}
@@ -425,7 +516,8 @@ timeout-minutes: 90
425
516
  expects you to use good judgment. If two approaches are equally valid, pick one and
426
517
  proceed. You can always iterate based on PR feedback.
427
518
 
428
- 4. Verify before you conclude. From the repository root:
519
+ 4. Verify before you conclude, running only what your change can affect. From the
520
+ repository root:
429
521
 
430
522
  **Scoped verification.** This runner has limited memory, and a whole-repo lint or build
431
523
  can be killed mid-run. Scope verification to the files you actually changed first, and
@@ -442,6 +534,10 @@ timeout-minutes: 90
442
534
  ${{ env.VERIFY_COMMANDS }}
443
535
  ```
444
536
 
537
+ Run only the parts your change can affect, and none of them for a change that touches
538
+ only documentation. A cold Release build takes minutes on a shared runner, and running
539
+ it for a change that never left the front end is time the run does not get back.
540
+
445
541
  If a check fails, fix the cause and rerun. Do not weaken a test, lower a threshold, or skip
446
542
  a check to make it pass. After all checks pass, run the project's lint fix command (e.g.
447
543
  `pnpm lint:fix` or `pnpm exec biome check --write <changed-files>`) to auto-format the
@@ -475,6 +571,12 @@ timeout-minutes: 90
475
571
  safeoutputs/create_pull_request(title="[bot] Fix X", body="Closes #${{ inputs.issue-number }}\n\n...", branch="fix/x")
476
572
  ```
477
573
 
574
+ Send it complete, first time. Each of these has an allowance of one call per run, and a
575
+ call that fails still spends it: a short payload sent to find out what the tool accepts
576
+ can come back a success, take the allowance with it, and leave the real call refused as
577
+ over the limit. That has happened, and the run ends having done all the work and
578
+ published none of it. Do not probe, and do not send a partial payload to test the shape.
579
+
478
580
  Choose exactly one:
479
581
 
480
582
  - **`safeoutputs/create_pull_request`** , propose a pull request against `main` with