@plainconceptsplatform/workflows 1.6.23 → 1.6.41
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/index.js +12 -1
- package/loops/actions/decide-agent-retry/action.yml +10 -2
- package/loops/actions/housekeeping/action.yml +11 -1
- package/loops/actions/merge-train/action.yml +5 -0
- package/loops/actions/merge-train/merge-train.sh +16 -3
- package/loops/actions/rebase-stale-patch/action.yml +41 -0
- package/loops/actions/rebase-stale-patch/rebase-stale-patch.sh +94 -0
- package/loops/actions/validate-merge-gate-output/action.yml +5 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +121 -2
- package/loops/docs/diagrams.md +5 -4
- package/loops/scripts/compile-agent-workflows.mjs +13 -0
- package/loops/workflows/agent-apply-review.md +9 -6
- package/loops/workflows/agent-audit.md +7 -4
- package/loops/workflows/agent-implement.md +54 -23
- package/loops/workflows/agent-merge-gate.md +31 -8
- package/loops/workflows/agent-refine.md +10 -7
- package/loops/workflows/agent-release.md +7 -4
- package/loops/workflows/agent-triage.md +9 -6
- package/loops/workflows/shared/opencode-ci.md +63 -8
- package/loops/workflows/shared/platform-defaults.md +30 -17
- package/loops/workflows/work-router.yml +14 -2
- package/package.json +1 -1
|
@@ -6,9 +6,12 @@ env:
|
|
|
6
6
|
# so it names its model here. `model:` below only satisfies the compiler. A repository may
|
|
7
7
|
# change it to any model its opencode.ci.json declares.
|
|
8
8
|
AGENT_LEAD_MODEL: "forge/glm-5-2"
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# Every harness tier (plan, build, fast) on one model the CI's Forge account is granted.
|
|
10
|
+
# harness.json names quasar-438b and carina-60b, which that account is not granted: Pliny-Bot
|
|
11
|
+
# #823's build tasks failed on quasar. Empty runs the tiers harness.json names.
|
|
11
12
|
AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
|
|
13
|
+
# The fast tier alone, while the CI's Forge account is not granted carina-60b.
|
|
14
|
+
AGENT_FAST_MODEL: "plainconcepts/glm-5-2"
|
|
12
15
|
VERIFY_COMMANDS: ""
|
|
13
16
|
REPO_RULES: "Implement only the selected issue. Follow repository documentation and existing conventions. Do not weaken tests, lower coverage thresholds, or bypass checks."
|
|
14
17
|
# Split out of REPO_RULES because one field asked to carry architecture, testing, coverage and
|
|
@@ -243,7 +246,7 @@ jobs:
|
|
|
243
246
|
${{ env.REVIEW_LABEL }}
|
|
244
247
|
${{ env.STALLED_LABEL }}
|
|
245
248
|
conclude:
|
|
246
|
-
needs: [agent, safe_outputs]
|
|
249
|
+
needs: [activation, agent, safe_outputs]
|
|
247
250
|
if: >
|
|
248
251
|
always() &&
|
|
249
252
|
needs.agent.result == 'success' &&
|
|
@@ -276,13 +279,27 @@ jobs:
|
|
|
276
279
|
token: ${{ steps.app-token.outputs.token }}
|
|
277
280
|
issue-number: ${{ inputs.issue-number }}
|
|
278
281
|
labels: ${{ env.WORKING_LABEL }}
|
|
282
|
+
# gh-aw pushes onto the base as it is when the run ends; a base that moved under a long run
|
|
283
|
+
# makes the commits not apply and the work is filed as an issue. A plain rebase applies most
|
|
284
|
+
# of them (Pliny-Bot #787 and #794 were rebased by hand in minutes), so it is tried here, and
|
|
285
|
+
# every step below treats the rescued pull request as the one the agent opened.
|
|
286
|
+
- name: Rebase a patch the base moved under
|
|
287
|
+
id: rescue
|
|
288
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && contains(needs.agent.outputs.output_types, 'create_pull_request')
|
|
289
|
+
uses: ./.github/actions/rebase-stale-patch
|
|
290
|
+
with:
|
|
291
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
292
|
+
run-id: ${{ github.run_id }}
|
|
293
|
+
artifact: ${{ needs.activation.outputs.artifact_prefix }}agent
|
|
294
|
+
issue-number: ${{ inputs.issue-number }}
|
|
295
|
+
default-branch: ${{ github.event.repository.default_branch }}
|
|
279
296
|
- name: Verify PR closes the source issue
|
|
280
|
-
if: needs.safe_outputs.outputs.created_pr_number != ''
|
|
297
|
+
if: needs.safe_outputs.outputs.created_pr_number != '' || steps.rescue.outputs.rescued == 'true'
|
|
281
298
|
continue-on-error: true
|
|
282
299
|
uses: ./.github/actions/link-pr-to-issue
|
|
283
300
|
with:
|
|
284
301
|
token: ${{ steps.app-token.outputs.token }}
|
|
285
|
-
pr-number: ${{ needs.safe_outputs.outputs.created_pr_number }}
|
|
302
|
+
pr-number: ${{ needs.safe_outputs.outputs.created_pr_number || steps.rescue.outputs.pr-number }}
|
|
286
303
|
issue-number: ${{ inputs.issue-number }}
|
|
287
304
|
# GitHub only stores the PR-to-issue direction (Closes #N); the reverse lookup is a
|
|
288
305
|
# body-text search. Stamping hidden markers on the issue makes issue-to-branch exact:
|
|
@@ -290,13 +307,13 @@ jobs:
|
|
|
290
307
|
# the branch without guessing. Old markers are replaced, so a re-implement after a
|
|
291
308
|
# closed pull request re-stamps cleanly.
|
|
292
309
|
- name: Record the pull request and branch on the issue
|
|
293
|
-
if: needs.safe_outputs.outputs.created_pr_number != ''
|
|
310
|
+
if: needs.safe_outputs.outputs.created_pr_number != '' || steps.rescue.outputs.rescued == 'true'
|
|
294
311
|
continue-on-error: true
|
|
295
312
|
env:
|
|
296
313
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
297
314
|
REPO: ${{ github.repository }}
|
|
298
315
|
ISSUE: ${{ inputs.issue-number }}
|
|
299
|
-
PR_NUMBER: ${{ needs.safe_outputs.outputs.created_pr_number }}
|
|
316
|
+
PR_NUMBER: ${{ needs.safe_outputs.outputs.created_pr_number || steps.rescue.outputs.pr-number }}
|
|
300
317
|
run: |
|
|
301
318
|
set -euo pipefail
|
|
302
319
|
branch=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefName --jq '.headRefName')
|
|
@@ -306,14 +323,14 @@ jobs:
|
|
|
306
323
|
gh issue edit "$ISSUE" --repo "$REPO" --body-file /tmp/issue-body.md
|
|
307
324
|
echo "Stamped PR #$PR_NUMBER and branch $branch on issue #$ISSUE"
|
|
308
325
|
- name: Mark issue as having a pull request pending
|
|
309
|
-
if: needs.safe_outputs.outputs.created_pr_number != ''
|
|
326
|
+
if: needs.safe_outputs.outputs.created_pr_number != '' || steps.rescue.outputs.rescued == 'true'
|
|
310
327
|
uses: ./.github/actions/add-issue-labels
|
|
311
328
|
with:
|
|
312
329
|
token: ${{ steps.app-token.outputs.token }}
|
|
313
330
|
issue-number: ${{ inputs.issue-number }}
|
|
314
331
|
labels: ${{ env.PR_PENDING_LABEL }}
|
|
315
332
|
- name: Reconcile the new bot pull request
|
|
316
|
-
if: needs.safe_outputs.outputs.created_pr_number != ''
|
|
333
|
+
if: needs.safe_outputs.outputs.created_pr_number != '' || steps.rescue.outputs.rescued == 'true'
|
|
317
334
|
env:
|
|
318
335
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
319
336
|
REPO: ${{ github.repository }}
|
|
@@ -354,7 +371,7 @@ jobs:
|
|
|
354
371
|
# comment exists to prevent (Odyssey #226). Distinguish them the same way the steps below
|
|
355
372
|
# do: gate on whether the agent actually asked for a pull request.
|
|
356
373
|
- name: Flag a patch that could not be pushed
|
|
357
|
-
if: needs.safe_outputs.outputs.created_pr_number == '' && needs.safe_outputs.outputs.process_safe_outputs_items_succeeded != '0' && needs.safe_outputs.outputs.process_safe_outputs_items_succeeded != '' && contains(needs.agent.outputs.output_types, 'create_pull_request')
|
|
374
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && needs.safe_outputs.outputs.process_safe_outputs_items_succeeded != '0' && needs.safe_outputs.outputs.process_safe_outputs_items_succeeded != '' && contains(needs.agent.outputs.output_types, 'create_pull_request') && steps.rescue.outputs.rescued != 'true'
|
|
358
375
|
uses: ./.github/actions/add-issue-labels
|
|
359
376
|
with:
|
|
360
377
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -363,7 +380,7 @@ jobs:
|
|
|
363
380
|
${{ env.REVIEW_LABEL }}
|
|
364
381
|
${{ env.STALLED_LABEL }}
|
|
365
382
|
- name: Say where the patch went
|
|
366
|
-
if: needs.safe_outputs.outputs.created_pr_number == '' && needs.safe_outputs.outputs.process_safe_outputs_items_succeeded != '0' && needs.safe_outputs.outputs.process_safe_outputs_items_succeeded != '' && contains(needs.agent.outputs.output_types, 'create_pull_request')
|
|
383
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && needs.safe_outputs.outputs.process_safe_outputs_items_succeeded != '0' && needs.safe_outputs.outputs.process_safe_outputs_items_succeeded != '' && contains(needs.agent.outputs.output_types, 'create_pull_request') && steps.rescue.outputs.rescued != 'true'
|
|
367
384
|
uses: ./.github/actions/create-issue-comment
|
|
368
385
|
with:
|
|
369
386
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -385,7 +402,7 @@ jobs:
|
|
|
385
402
|
# said "nothing landed" and neither could be acted on, while #175's run had produced more
|
|
386
403
|
# output than the run that succeeded in the same hour.
|
|
387
404
|
- name: Flag a request that was not applied
|
|
388
|
-
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '') && contains(needs.agent.outputs.output_types, 'create_pull_request')
|
|
405
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '') && contains(needs.agent.outputs.output_types, 'create_pull_request') && steps.rescue.outputs.rescued != 'true'
|
|
389
406
|
uses: ./.github/actions/add-issue-labels
|
|
390
407
|
with:
|
|
391
408
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -394,7 +411,7 @@ jobs:
|
|
|
394
411
|
${{ env.REVIEW_LABEL }}
|
|
395
412
|
${{ env.STALLED_LABEL }}
|
|
396
413
|
- name: Say the request was not applied
|
|
397
|
-
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '') && contains(needs.agent.outputs.output_types, 'create_pull_request')
|
|
414
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '') && contains(needs.agent.outputs.output_types, 'create_pull_request') && steps.rescue.outputs.rescued != 'true'
|
|
398
415
|
uses: ./.github/actions/create-issue-comment
|
|
399
416
|
with:
|
|
400
417
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -508,9 +525,13 @@ jobs:
|
|
|
508
525
|
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
509
526
|
incomplete:
|
|
510
527
|
needs: [agent, safe_outputs, eligibility]
|
|
528
|
+
# A published pull request is an outcome whatever the agent job's own result: Pliny-Bot #765
|
|
529
|
+
# opened #844 after 64 minutes, the agent job read as failed, and this job released the issue
|
|
530
|
+
# and flagged it stalled next to its open pull request.
|
|
511
531
|
if: >
|
|
512
532
|
always() &&
|
|
513
533
|
needs.eligibility.outputs.eligible == 'true' &&
|
|
534
|
+
needs.safe_outputs.outputs.created_pr_number == '' &&
|
|
514
535
|
(
|
|
515
536
|
needs.agent.result != 'success' ||
|
|
516
537
|
needs.safe_outputs.result != 'success'
|
|
@@ -593,7 +614,7 @@ jobs:
|
|
|
593
614
|
${{ env.WORKING_LABEL }}
|
|
594
615
|
implement
|
|
595
616
|
- name: Flag for human review
|
|
596
|
-
if: steps.decide.outputs.retry != 'true'
|
|
617
|
+
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
597
618
|
uses: ./.github/actions/add-issue-labels
|
|
598
619
|
with:
|
|
599
620
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -602,7 +623,7 @@ jobs:
|
|
|
602
623
|
${{ env.REVIEW_LABEL }}
|
|
603
624
|
${{ env.STALLED_LABEL }}
|
|
604
625
|
- name: Report missing implementation outcome
|
|
605
|
-
if: steps.decide.outputs.retry != 'true'
|
|
626
|
+
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
606
627
|
uses: ./.github/actions/create-issue-comment
|
|
607
628
|
with:
|
|
608
629
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -692,8 +713,8 @@ safe-outputs:
|
|
|
692
713
|
# 240 went on to every worker at once when the provider was slow, which fixed the deaths and
|
|
693
714
|
# made every worker equally expensive to hang. These numbers are per worker: enough headroom
|
|
694
715
|
# for a slow gateway on the work it actually does, and not four hours for a run that reads one
|
|
695
|
-
# issue. Turns remain the guard against a confused agent looping;
|
|
696
|
-
#
|
|
716
|
+
# issue. Turns remain the guard against a confused agent looping; the credit ceiling meters the
|
|
717
|
+
# Forge models at Multiverse's list prices, set in shared/opencode-ci.md.
|
|
697
718
|
#
|
|
698
719
|
# Writes code, builds, runs a test suite and pushes a branch: the longest real work in the fleet.
|
|
699
720
|
timeout-minutes: 180
|
|
@@ -763,12 +784,12 @@ timeout-minutes: 180
|
|
|
763
784
|
`proposal.md` or `tasks.md` yourself.** A hand-written `tasks.md` carries no annotations,
|
|
764
785
|
and without them `pc-plan-apply` has no workers to spawn, so the whole change is done by
|
|
765
786
|
you alone, in sequence, on one model. That is the failure this step exists to prevent.
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
in
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
787
|
+
Let it plan the way it plans on a developer's machine: tasks that do not depend on each
|
|
788
|
+
other run in the same wave, in parallel, each on its tier's model. Keep the tests for code
|
|
789
|
+
in the same task as the code, and no task whose only job is to repair what an earlier task
|
|
790
|
+
broke (#696 ended with a twelve-minute task that fixed the test doubles the earlier ones
|
|
791
|
+
broke). A chain of tasks that each wait for the last is the slow shape: Pliny-Bot #794 ran
|
|
792
|
+
four workers in three waves, one at a time, for twenty-six of its fifty-nine minutes.
|
|
772
793
|
3. `pc-plan-apply`. The tasks are implemented by the subagents it spawns, in waves, each on
|
|
773
794
|
the model tier its annotation names. **In this phase you do not edit source files
|
|
774
795
|
yourself.** If it stops because a worker cannot be resolved, report that with
|
|
@@ -844,6 +865,16 @@ timeout-minutes: 180
|
|
|
844
865
|
so every part finishes inside one call. The only valid way to end the run is the safe-output
|
|
845
866
|
call in step 6.
|
|
846
867
|
|
|
868
|
+
**If the bash tool answers "moved to the background", the run is not over and you are not
|
|
869
|
+
waiting: keep going with tool calls.** It does that to any command that passes its timeout, even
|
|
870
|
+
when you passed none, and Pliny-Bot #774 ended that way on `pnpm verify --quick` after
|
|
871
|
+
sixty-seven minutes with no pull request. Poll it: call bash with `sleep 40; tail -n 15` on the
|
|
872
|
+
command's output file, again and again, until the output shows its exit status, and only then
|
|
873
|
+
go on. Each of those calls is a tool call, so the turn never ends. To avoid it from the start,
|
|
874
|
+
run a command that may pass two minutes as
|
|
875
|
+
`(<command>) > /tmp/gh-aw/agent/step.log 2>&1; echo "exit status $?" >> /tmp/gh-aw/agent/step.log` with the bash tool's
|
|
876
|
+
`timeout` at 600000, and read `/tmp/gh-aw/agent/step.log` afterwards.
|
|
877
|
+
|
|
847
878
|
**Two things the unit tests do not catch, and CI will.** When you add a dependency, take the
|
|
848
879
|
latest released version and run the repository's audit for it (`pnpm audit --audit-level high`
|
|
849
880
|
for the web, `dotnet list package --vulnerable` for the backend): Pliny-Bot #752 added a PDF
|
|
@@ -6,9 +6,12 @@ env:
|
|
|
6
6
|
# so it names its model here. `model:` below only satisfies the compiler. A repository may
|
|
7
7
|
# change it to any model its opencode.ci.json declares.
|
|
8
8
|
AGENT_LEAD_MODEL: "forge/glm-5-2"
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# Every harness tier (plan, build, fast) on one model the CI's Forge account is granted.
|
|
10
|
+
# harness.json names quasar-438b and carina-60b, which that account is not granted: Pliny-Bot
|
|
11
|
+
# #823's build tasks failed on quasar. Empty runs the tiers harness.json names.
|
|
11
12
|
AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
|
|
13
|
+
# The fast tier alone, while the CI's Forge account is not granted carina-60b.
|
|
14
|
+
AGENT_FAST_MODEL: "plainconcepts/glm-5-2"
|
|
12
15
|
VERIFY_COMMANDS: ""
|
|
13
16
|
REPO_RULES: "Review the selected pull request for defects and report what you verified. Do not decide the outcome: the workflow computes it from your report and from facts it measured before you ran."
|
|
14
17
|
# Where to look first, not what to escalate on. This list used to be the check that decided
|
|
@@ -49,6 +52,9 @@ env:
|
|
|
49
52
|
# translations and generated code excluded) is too big to merge unseen. It was a thousand, which
|
|
50
53
|
# is a normal feature once the tests are left out. A file count alone never decides, so the high
|
|
51
54
|
# file threshold is set out of reach; spread still raises medium.
|
|
55
|
+
# "true" lets a stack root merge once every child has landed, through the same blast radius and
|
|
56
|
+
# findings as any pull request. "false" holds every root for a person.
|
|
57
|
+
STACK_ROOT_AUTOMERGE: "true"
|
|
52
58
|
BLAST_HIGH_FILES: "100000"
|
|
53
59
|
BLAST_HIGH_LINES: "10000"
|
|
54
60
|
BLAST_MEDIUM_FILES: "5"
|
|
@@ -177,6 +183,7 @@ jobs:
|
|
|
177
183
|
review_blocked: ${{ steps.review.outputs.review_blocked }}
|
|
178
184
|
pr_base: ${{ steps.stack.outputs.pr_base }}
|
|
179
185
|
stack_root: ${{ steps.stack.outputs.stack_root }}
|
|
186
|
+
root_complete: ${{ steps.stack.outputs.root_complete }}
|
|
180
187
|
steps:
|
|
181
188
|
- name: Checkout workflow actions
|
|
182
189
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
@@ -230,18 +237,28 @@ jobs:
|
|
|
230
237
|
REPO: ${{ github.repository }}
|
|
231
238
|
PR: ${{ steps.subject.outputs.pr }}
|
|
232
239
|
ISSUE: ${{ steps.subject.outputs.issue }}
|
|
240
|
+
STACK_ROOT_AUTOMERGE: ${{ env.STACK_ROOT_AUTOMERGE }}
|
|
233
241
|
run: |
|
|
234
242
|
set -euo pipefail
|
|
235
243
|
pr_base=$(gh pr view "$PR" --repo "$REPO" --json baseRefName --jq '.baseRefName // ""')
|
|
236
244
|
stack_root=false
|
|
245
|
+
root_complete=false
|
|
237
246
|
if [ -n "$ISSUE" ]; then
|
|
238
247
|
body=$(gh issue view "$ISSUE" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null || echo "")
|
|
239
248
|
if echo "$body" | grep -q "<!-- stack-root-pr: $PR -->"; then
|
|
240
249
|
stack_root=true
|
|
250
|
+
# The router announces a stack complete on the root once every child has landed and
|
|
251
|
+
# CI is green. A root without that comment is still collecting children.
|
|
252
|
+
if [ "$STACK_ROOT_AUTOMERGE" = true ] \
|
|
253
|
+
&& gh api --paginate "repos/$REPO/issues/$PR/comments?per_page=100" \
|
|
254
|
+
--jq '.[] | select(.body | contains("<!-- stack-complete -->")) | .id' 2>/dev/null | grep -q .; then
|
|
255
|
+
root_complete=true
|
|
256
|
+
fi
|
|
241
257
|
fi
|
|
242
258
|
fi
|
|
243
259
|
echo "pr_base=$pr_base" >> "$GITHUB_OUTPUT"
|
|
244
260
|
echo "stack_root=$stack_root" >> "$GITHUB_OUTPUT"
|
|
261
|
+
echo "root_complete=$root_complete" >> "$GITHUB_OUTPUT"
|
|
245
262
|
|
|
246
263
|
# Rung 4b. Everything the merge decision needs that a shell can establish, measured once,
|
|
247
264
|
# before any model reads the diff. The job kept its name and its `requires_review` and
|
|
@@ -479,6 +496,7 @@ jobs:
|
|
|
479
496
|
valid: ${{ steps.validate.outputs.valid }}
|
|
480
497
|
outcome: ${{ steps.validate.outputs.outcome }}
|
|
481
498
|
pushed: ${{ steps.validate.outputs.pushed }}
|
|
499
|
+
empty: ${{ steps.validate.outputs.empty }}
|
|
482
500
|
steps:
|
|
483
501
|
- name: Checkout workflow actions
|
|
484
502
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
@@ -589,7 +607,7 @@ jobs:
|
|
|
589
607
|
${{ needs.subject.outputs.issue != '' && format('Findings and verification on the linked issue: #{0}.', needs.subject.outputs.issue) || 'Findings and verification are in the review comment above.' }} [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
590
608
|
${{ env.EVIDENCE_BEFORE_REVIEW == 'true' && contains(fromJson('["human-review","owner-review"]'), needs.validate_output.outputs.outcome) && 'Screenshots of this head follow in their own comment if it carries a verification plan. Approving the pull request merges it.' || '' }}
|
|
591
609
|
- name: Merge approved pull request
|
|
592
|
-
if: needs.validate_output.outputs.outcome == 'auto-merge' && needs.subject.outputs.stack_root != 'true'
|
|
610
|
+
if: needs.validate_output.outputs.outcome == 'auto-merge' && (needs.subject.outputs.stack_root != 'true' || needs.subject.outputs.root_complete == 'true')
|
|
593
611
|
env:
|
|
594
612
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
595
613
|
REPO: ${{ github.repository }}
|
|
@@ -603,7 +621,7 @@ jobs:
|
|
|
603
621
|
# what a reader needs: the gate held the root, children merge into the feature branch,
|
|
604
622
|
# and a human merges the root when the stack completes.
|
|
605
623
|
- name: Hold a stack root for a human merge
|
|
606
|
-
if: needs.validate_output.outputs.outcome == 'auto-merge' && needs.subject.outputs.stack_root == 'true'
|
|
624
|
+
if: needs.validate_output.outputs.outcome == 'auto-merge' && needs.subject.outputs.stack_root == 'true' && needs.subject.outputs.root_complete != 'true'
|
|
607
625
|
uses: ./.github/actions/create-issue-comment
|
|
608
626
|
with:
|
|
609
627
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -755,6 +773,7 @@ jobs:
|
|
|
755
773
|
AGENT_RESULT: ${{ needs.agent.result }}
|
|
756
774
|
SAFE_RESULT: ${{ needs.safe_outputs.result }}
|
|
757
775
|
OUTPUT_VALID: ${{ needs.validate_output.outputs.valid }}
|
|
776
|
+
OUTPUT_EMPTY: ${{ needs.validate_output.outputs.empty }}
|
|
758
777
|
PARK_AT_ATTEMPT: ${{ env.PARK_AT_ATTEMPT }}
|
|
759
778
|
PARK_AT_UNUSABLE_OUTPUT: ${{ env.PARK_AT_UNUSABLE_OUTPUT }}
|
|
760
779
|
run: |
|
|
@@ -762,8 +781,12 @@ jobs:
|
|
|
762
781
|
attempts=${ATTEMPTS:-0}
|
|
763
782
|
# The agent ran, published, and produced something the validator refused. Repeating
|
|
764
783
|
# that reproduces it; a person reading the comment costs less than three more runs
|
|
765
|
-
# holding the merge belt.
|
|
766
|
-
|
|
784
|
+
# holding the merge belt. An agent that produced nothing at all is not that: it quit,
|
|
785
|
+
# as the Pliny-Bot gate did on #801 after one line of text and twenty-three seconds,
|
|
786
|
+
# twice, and went to a person with no finding. That is a machine failure and gets the
|
|
787
|
+
# machine budget.
|
|
788
|
+
if [ "$AGENT_RESULT" = success ] && [ "$SAFE_RESULT" = success ] && [ "$OUTPUT_VALID" != true ] \
|
|
789
|
+
&& [ "${OUTPUT_EMPTY:-false}" != true ]; then
|
|
767
790
|
threshold="$PARK_AT_UNUSABLE_OUTPUT"
|
|
768
791
|
kind=unusable
|
|
769
792
|
else
|
|
@@ -982,8 +1005,8 @@ safe-outputs:
|
|
|
982
1005
|
# 240 went on to every worker at once when the provider was slow, which fixed the deaths and
|
|
983
1006
|
# made every worker equally expensive to hang. These numbers are per worker: enough headroom
|
|
984
1007
|
# for a slow gateway on the work it actually does, and not four hours for a run that reads one
|
|
985
|
-
# issue. Turns remain the guard against a confused agent looping;
|
|
986
|
-
#
|
|
1008
|
+
# issue. Turns remain the guard against a confused agent looping; the credit ceiling meters the
|
|
1009
|
+
# Forge models at Multiverse's list prices, set in shared/opencode-ci.md.
|
|
987
1010
|
#
|
|
988
1011
|
# Reads CI failure evidence and may fix, verify and re-push, so it can do implement's work on a smaller diff.
|
|
989
1012
|
timeout-minutes: 120
|
|
@@ -5,10 +5,13 @@ env:
|
|
|
5
5
|
# every agent without its own model falls back to; the lead is where a wrong call costs a run,
|
|
6
6
|
# so it names its model here. `model:` below only satisfies the compiler. A repository may
|
|
7
7
|
# change it to any model its opencode.ci.json declares.
|
|
8
|
-
AGENT_LEAD_MODEL: "forge/glm-5-
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
AGENT_LEAD_MODEL: "forge/glm-5-3"
|
|
9
|
+
# Every harness tier (plan, build, fast) on one model the CI's Forge account is granted.
|
|
10
|
+
# harness.json names quasar-438b and carina-60b, which that account is not granted: Pliny-Bot
|
|
11
|
+
# #823's build tasks failed on quasar. Empty runs the tiers harness.json names.
|
|
11
12
|
AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
|
|
13
|
+
# The fast tier alone, while the CI's Forge account is not granted carina-60b.
|
|
14
|
+
AGENT_FAST_MODEL: "plainconcepts/glm-5-2"
|
|
12
15
|
REPO_RULES: "Refine only the selected issue into a grounded, implementation-ready user story. Read repository documentation for domain context. Write acceptance criteria that match existing patterns. Where a story or its open questions decide how business data (named people, roles, addresses) is stored, resolve it toward the repository's convention for that data -- a database entity or a settings entry -- and record that choice in the acceptance criteria; do not carry a person's name or email address into the story as something the code reads from a constant. Do not implement code. Do not use web search or web fetch tools. The issue body already contains the research that motivated it; derive your domain context from the repository's own code and documentation only."
|
|
13
16
|
# The estimate decides whether a story gets split, and the prompt tells the agent these bands
|
|
14
17
|
# come from this repository's own merged pull requests. They have to actually come from it, or
|
|
@@ -773,7 +776,7 @@ jobs:
|
|
|
773
776
|
issue-number: ${{ inputs.issue-number }}
|
|
774
777
|
labels: ${{ env.WORKING_LABEL }}
|
|
775
778
|
- name: Flag for human review
|
|
776
|
-
if: steps.decide.outputs.retry != 'true'
|
|
779
|
+
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
777
780
|
uses: ./.github/actions/add-issue-labels
|
|
778
781
|
with:
|
|
779
782
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -782,7 +785,7 @@ jobs:
|
|
|
782
785
|
${{ env.REVIEW_LABEL }}
|
|
783
786
|
${{ env.STALLED_LABEL }}
|
|
784
787
|
- name: Report missing refinement outcome
|
|
785
|
-
if: steps.decide.outputs.retry != 'true'
|
|
788
|
+
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
786
789
|
uses: ./.github/actions/create-issue-comment
|
|
787
790
|
with:
|
|
788
791
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -882,8 +885,8 @@ safe-outputs:
|
|
|
882
885
|
# 240 went on to every worker at once when the provider was slow, which fixed the deaths and
|
|
883
886
|
# made every worker equally expensive to hang. These numbers are per worker: enough headroom
|
|
884
887
|
# for a slow gateway on the work it actually does, and not four hours for a run that reads one
|
|
885
|
-
# issue. Turns remain the guard against a confused agent looping;
|
|
886
|
-
#
|
|
888
|
+
# issue. Turns remain the guard against a confused agent looping; the credit ceiling meters the
|
|
889
|
+
# Forge models at Multiverse's list prices, set in shared/opencode-ci.md.
|
|
887
890
|
#
|
|
888
891
|
# Explores the repository per work unit, then rewrites one issue body. Measured: a run needing 36 turns died at 40 minutes with the story finished.
|
|
889
892
|
timeout-minutes: 90
|
|
@@ -6,9 +6,12 @@ env:
|
|
|
6
6
|
# so it names its model here. `model:` below only satisfies the compiler. A repository may
|
|
7
7
|
# change it to any model its opencode.ci.json declares.
|
|
8
8
|
AGENT_LEAD_MODEL: "forge/glm-5-2"
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# Every harness tier (plan, build, fast) on one model the CI's Forge account is granted.
|
|
10
|
+
# harness.json names quasar-438b and carina-60b, which that account is not granted: Pliny-Bot
|
|
11
|
+
# #823's build tasks failed on quasar. Empty runs the tiers harness.json names.
|
|
11
12
|
AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
|
|
13
|
+
# The fast tier alone, while the CI's Forge account is not granted carina-60b.
|
|
14
|
+
AGENT_FAST_MODEL: "plainconcepts/glm-5-2"
|
|
12
15
|
GIT_AUTHOR_NAME: "github-actions[bot]"
|
|
13
16
|
GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
|
|
14
17
|
GIT_COMMITTER_NAME: "github-actions[bot]"
|
|
@@ -94,8 +97,8 @@ safe-outputs:
|
|
|
94
97
|
# 240 went on to every worker at once when the provider was slow, which fixed the deaths and
|
|
95
98
|
# made every worker equally expensive to hang. These numbers are per worker: enough headroom
|
|
96
99
|
# for a slow gateway on the work it actually does, and not four hours for a run that reads one
|
|
97
|
-
# issue. Turns remain the guard against a confused agent looping;
|
|
98
|
-
#
|
|
100
|
+
# issue. Turns remain the guard against a confused agent looping; the credit ceiling meters the
|
|
101
|
+
# Forge models at Multiverse's list prices, set in shared/opencode-ci.md.
|
|
99
102
|
#
|
|
100
103
|
# Reads a commit log already on disk and writes release notes. Nothing else.
|
|
101
104
|
timeout-minutes: 30
|
|
@@ -6,9 +6,12 @@ env:
|
|
|
6
6
|
# so it names its model here. `model:` below only satisfies the compiler. A repository may
|
|
7
7
|
# change it to any model its opencode.ci.json declares.
|
|
8
8
|
AGENT_LEAD_MODEL: "forge/glm-5-2"
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# Every harness tier (plan, build, fast) on one model the CI's Forge account is granted.
|
|
10
|
+
# harness.json names quasar-438b and carina-60b, which that account is not granted: Pliny-Bot
|
|
11
|
+
# #823's build tasks failed on quasar. Empty runs the tiers harness.json names.
|
|
11
12
|
AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
|
|
13
|
+
# The fast tier alone, while the CI's Forge account is not granted carina-60b.
|
|
14
|
+
AGENT_FAST_MODEL: "plainconcepts/glm-5-2"
|
|
12
15
|
REPO_RULES: "Triage issues opened by outside collaborators. Assess template completeness, security risk, change size, danger level, duplicates, clarity, reproducibility, acceptance criteria, and cross-cutting impact. When the request names specific people, email addresses, or business entities as the thing the code acts on, judge where that data is meant to live: business data that belongs in the database or a settings page is a design decision, not a wording detail, and it must not be left for the implementer to decide by hardcoding it. Do not implement code. Do not modify the issue body."
|
|
13
16
|
# What a product owner may ask for, and what has to become a maintainer-owned technical
|
|
14
17
|
# proposal instead. This is business policy, so it belongs to the repository rather than to
|
|
@@ -387,7 +390,7 @@ jobs:
|
|
|
387
390
|
issue-number: ${{ inputs.issue-number }}
|
|
388
391
|
labels: ${{ env.WORKING_LABEL }}
|
|
389
392
|
- name: Flag for human review
|
|
390
|
-
if: steps.decide.outputs.retry != 'true'
|
|
393
|
+
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
391
394
|
uses: ./.github/actions/add-issue-labels
|
|
392
395
|
with:
|
|
393
396
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -396,7 +399,7 @@ jobs:
|
|
|
396
399
|
${{ env.REVIEW_LABEL }}
|
|
397
400
|
${{ env.STALLED_LABEL }}
|
|
398
401
|
- name: Report missing triage outcome
|
|
399
|
-
if: steps.decide.outputs.retry != 'true'
|
|
402
|
+
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
400
403
|
uses: ./.github/actions/create-issue-comment
|
|
401
404
|
with:
|
|
402
405
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -454,8 +457,8 @@ safe-outputs:
|
|
|
454
457
|
# 240 went on to every worker at once when the provider was slow, which fixed the deaths and
|
|
455
458
|
# made every worker equally expensive to hang. These numbers are per worker: enough headroom
|
|
456
459
|
# for a slow gateway on the work it actually does, and not four hours for a run that reads one
|
|
457
|
-
# issue. Turns remain the guard against a confused agent looping;
|
|
458
|
-
#
|
|
460
|
+
# issue. Turns remain the guard against a confused agent looping; the credit ceiling meters the
|
|
461
|
+
# Forge models at Multiverse's list prices, set in shared/opencode-ci.md.
|
|
459
462
|
#
|
|
460
463
|
# Reads one issue and the open-issue list, runs ten checks, writes one comment. No repository exploration, no build.
|
|
461
464
|
timeout-minutes: 45
|
|
@@ -13,6 +13,33 @@ description: |
|
|
|
13
13
|
shared baseline in the consumer copy. The merge step below is package-owned and required
|
|
14
14
|
for the agent to resolve its provider and its models.
|
|
15
15
|
|
|
16
|
+
# AI credits prices for the Forge models, so the AWF proxy can meter them against max-ai-credits
|
|
17
|
+
# instead of refusing them ("Model ... has no AI credits pricing", Atlas #2016). gh-aw maps this to
|
|
18
|
+
# apiProxy.providers in the AWF config, the operator overlay the proxy checks before its curated
|
|
19
|
+
# table and its models.dev snapshot. Without it glm-5-2 was metered at models.dev's glm-5 rate by
|
|
20
|
+
# prefix match, and carina-60b, quasar-438b and qwen-3-8-27b had no price at all.
|
|
21
|
+
#
|
|
22
|
+
# Rates are Multiverse's public list prices for the CompactifAI API, the provider Forge serves these
|
|
23
|
+
# from: https://docs.compactif.ai/pricing/ (also on https://multiversecomputing.com/compactifai/api),
|
|
24
|
+
# read 2026-10-18. Values are USD per token (the page's $/1M divided by 1e6); the proxy bills one AI
|
|
25
|
+
# credit per $0.01. Where the page shows no cached rate, prompt caching is off for that model and
|
|
26
|
+
# every input token is billed at the input rate, so cache_read equals input instead of the proxy's
|
|
27
|
+
# 10%-of-input default. The key is `openai` because Forge is the proxy's openai target.
|
|
28
|
+
models:
|
|
29
|
+
providers:
|
|
30
|
+
openai:
|
|
31
|
+
models:
|
|
32
|
+
carina-60b:
|
|
33
|
+
cost: { input: "0.00000005", cache_read: "0.00000005", output: "0.0000002" }
|
|
34
|
+
qwen-3-8-27b:
|
|
35
|
+
cost: { input: "0.0000004", cache_read: "0.0000004", output: "0.000002" }
|
|
36
|
+
glm-5-2:
|
|
37
|
+
cost: { input: "0.0000011", cache_read: "0.00000022", output: "0.0000035" }
|
|
38
|
+
glm-5-3:
|
|
39
|
+
cost: { input: "0.0000011", cache_read: "0.00000022", output: "0.0000035" }
|
|
40
|
+
quasar-438b:
|
|
41
|
+
cost: { input: "0.0000006", cache_read: "0.0000006", output: "0.0000018" }
|
|
42
|
+
|
|
16
43
|
# Consumer repositories should add stack-specific steps (NuGet cache, dotnet restore,
|
|
17
44
|
# OpenSpec, Playwright, etc.) after the shared baseline. The merge step at the end is
|
|
18
45
|
# package-owned and required for the agent to resolve its provider and models. Do not
|
|
@@ -182,9 +209,10 @@ pre-agent-steps:
|
|
|
182
209
|
# worker's prompt does not load, and has never run in CI; it stays off until it does.
|
|
183
210
|
#
|
|
184
211
|
# The tier models come from AGENT_TIER_MODEL when a worker sets it. The AWF proxy refuses
|
|
185
|
-
# a model it has no AI credits price for
|
|
186
|
-
#
|
|
187
|
-
# model through the gitignored per-user override the
|
|
212
|
+
# a model it has no AI credits price for; harness.json names carina-60b and quasar-438b,
|
|
213
|
+
# which had none until the `models.providers` prices at the top of this file (Atlas #2016).
|
|
214
|
+
# CI still pins every tier to one model, through the gitignored per-user override the
|
|
215
|
+
# plugin reads first, until a run on the newly priced models has passed.
|
|
188
216
|
run: |
|
|
189
217
|
set -euo pipefail
|
|
190
218
|
if [ ! -f .opencode/package.json ]; then
|
|
@@ -193,24 +221,51 @@ pre-agent-steps:
|
|
|
193
221
|
if [ ! -d .opencode/node_modules/@opencode/plugin ]; then
|
|
194
222
|
(cd .opencode && npm install --no-audit --no-fund --no-package-lock --ignore-scripts --loglevel=error)
|
|
195
223
|
fi
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
224
|
+
# pc-subagent-monitor frees a slot only on a child's idle or completed event, which a CI
|
|
225
|
+
# run never emits: after agents.maxConcurrent tasks it denied every spawn and Pliny-Bot
|
|
226
|
+
# #823 stopped half done. Off in CI until the plugin sees a child finish.
|
|
227
|
+
for plugin in pc-system-reminders pc-subagent-monitor; do
|
|
228
|
+
if [ -f ".opencode/plugins/${plugin}.js" ]; then
|
|
229
|
+
rm ".opencode/plugins/${plugin}.js"
|
|
230
|
+
# Tracked, so hide the deletion: an agent that sees it in `git status` restores it.
|
|
231
|
+
git update-index --skip-worktree ".opencode/plugins/${plugin}.js" 2>/dev/null || true
|
|
232
|
+
fi
|
|
233
|
+
done
|
|
234
|
+
# The runners are reused VMs and keep the workspace's ignored files between jobs, so an
|
|
235
|
+
# override an earlier run wrote is still there; an empty AGENT_TIER_MODEL has to remove it
|
|
236
|
+
# or the tiers stay pinned (Pliny-Bot #774 ran on glm-5-2 after the pin was dropped).
|
|
201
237
|
if [ -n "${AGENT_TIER_MODEL:-}" ]; then
|
|
202
238
|
jq -n --arg m "$AGENT_TIER_MODEL" '{models: {plan: $m, build: $m, fast: $m}}' > .opencode/harness.user.json
|
|
239
|
+
elif [ -n "${AGENT_FAST_MODEL:-}" ]; then
|
|
240
|
+
# One tier only: the CI's Forge account is not granted carina-60b, which harness.json
|
|
241
|
+
# names for fast work, so that tier alone moves to a model the account has.
|
|
242
|
+
jq -n --arg m "$AGENT_FAST_MODEL" '{models: {fast: $m}}' > .opencode/harness.user.json
|
|
243
|
+
else
|
|
244
|
+
rm -f .opencode/harness.user.json
|
|
203
245
|
fi
|
|
204
246
|
echo "Harness plugins: $(ls .opencode/plugins | tr '\n' ' ')"
|
|
205
247
|
echo "Tier models: $(jq -c '.models' .opencode/harness.user.json 2>/dev/null || jq -c '.models' .opencode/harness.json)"
|
|
206
248
|
|
|
207
249
|
- name: Merge the CI-only OpenCode provider into opencode.jsonc
|
|
250
|
+
env:
|
|
251
|
+
GH_TOKEN: ${{ github.token }}
|
|
252
|
+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }}
|
|
208
253
|
run: |
|
|
209
254
|
set -euo pipefail
|
|
210
255
|
|
|
211
256
|
CONFIG=opencode.jsonc
|
|
212
257
|
FRAGMENT=opencode.ci.json
|
|
213
258
|
|
|
259
|
+
# The default branch's copy wins over the checkout's. A pull request branch cut before a
|
|
260
|
+
# model change keeps the old default: Pliny-Bot's gate on #836 ran forge/carina-60b,
|
|
261
|
+
# which the CI account is not granted, hours after main had moved off it.
|
|
262
|
+
if curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github.raw" \
|
|
263
|
+
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/contents/${FRAGMENT}?ref=${DEFAULT_BRANCH}" \
|
|
264
|
+
-o "${RUNNER_TEMP}/opencode.ci.json" && jq -e . "${RUNNER_TEMP}/opencode.ci.json" > /dev/null; then
|
|
265
|
+
FRAGMENT="${RUNNER_TEMP}/opencode.ci.json"
|
|
266
|
+
echo "Using $DEFAULT_BRANCH's opencode.ci.json"
|
|
267
|
+
fi
|
|
268
|
+
|
|
214
269
|
[ -f "$FRAGMENT" ] || { echo "::error::$FRAGMENT is missing from the checkout"; exit 1; }
|
|
215
270
|
|
|
216
271
|
# Pure JSON on purpose, not JSONC: jq cannot parse `//` comments, and a naive
|
|
@@ -21,20 +21,33 @@ network:
|
|
|
21
21
|
safe-outputs:
|
|
22
22
|
threat-detection: false
|
|
23
23
|
---
|
|
24
|
-
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
24
|
+
|
|
25
|
+
## These runs are not interactive
|
|
26
|
+
|
|
27
|
+
- Nothing resumes you. A turn that ends with text and no tool call ends the whole run, and the work
|
|
28
|
+
is discarded, whatever the text promised. "I'll read the context files first." as a last message
|
|
29
|
+
is a finished run: the Pliny-Bot merge gate did exactly that on #801, ended in twenty-three
|
|
30
|
+
seconds with exit code 0 and no outcome, and was retried twice before the pull request was handed
|
|
31
|
+
to a person for nothing.
|
|
32
|
+
- So every message you write carries a tool call until the last one, which is the safe-output call
|
|
33
|
+
the task names. Say what you are about to do and do it in the same message. Never announce a step
|
|
34
|
+
and stop, never wait for a command, a reviewer or a "next turn".
|
|
35
|
+
- A command the bash tool reports as moved to the background is still running: poll its output file
|
|
36
|
+
with tool calls (`sleep 40; tail -n 15 <file>`) until it shows an exit status, then go on.
|
|
37
|
+
|
|
38
|
+
## Verification, for every worker that builds or pushes
|
|
39
|
+
|
|
40
|
+
- Scope every check to the files you changed. This is the runner, not a preference: a
|
|
41
|
+
whole-repository lint, build or test run exhausts its memory and gets killed mid-run, which
|
|
42
|
+
fails the job with no useful output. Pass the changed paths to the linter (`pnpm exec biome
|
|
43
|
+
check <files>`), build only the projects containing them, and run only the test project that
|
|
44
|
+
covers them. Escalate to the full suite only after the scoped run passes and only when the
|
|
45
|
+
change crosses a project boundary.
|
|
46
|
+
- Run nothing at all for a change that touches only documentation. A cold Release build on a
|
|
47
|
+
shared runner is minutes the run does not get back.
|
|
48
|
+
- Never make a check pass by weakening it: not a deleted test, not a lowered threshold, not a
|
|
49
|
+
skipped step. Fix the cause and run it again.
|
|
50
|
+
- Once the checks pass, run the project's lint fix command over the files you changed
|
|
51
|
+
(`pnpm lint:fix`, `pnpm exec biome check --write <changed-files>`, or its equivalent), or
|
|
52
|
+
correct what lint reports where no fix command exists. A branch that arrives with lint errors
|
|
53
|
+
gets sent back for them.
|