@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.
@@ -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
- # The model every harness tier (plan, build, fast) runs on in CI, planner included. Unset,
10
- # the tiers take harness.json, whose models the AWF proxy may not price.
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; for a custom model the credit
696
- # ceiling is models.dev fallback pricing and guards nothing.
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
- Ask it for few, coarse tasks: one task per area of files, the tests for that code in the
767
- same task, and no task whose only job is to repair what an earlier task broke. Five tasks
768
- in three waves cover most issues. Every task is a fresh worker that rereads the code it
769
- needs before it edits anything, one to three minutes each: Pliny-Bot #751 split a small
770
- web feature into twelve tasks and spent fifteen of its twenty-two minutes in them, and
771
- #696 ended with a twelve-minute task that fixed the test doubles the earlier tasks broke.
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
- # The model every harness tier (plan, build, fast) runs on in CI, planner included. Unset,
10
- # the tiers take harness.json, whose models the AWF proxy may not price.
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
- if [ "$AGENT_RESULT" = success ] && [ "$SAFE_RESULT" = success ] && [ "$OUTPUT_VALID" != true ]; then
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; for a custom model the credit
986
- # ceiling is models.dev fallback pricing and guards nothing.
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-2"
9
- # The model every harness tier (plan, build, fast) runs on in CI, planner included. Unset,
10
- # the tiers take harness.json, whose models the AWF proxy may not price.
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; for a custom model the credit
886
- # ceiling is models.dev fallback pricing and guards nothing.
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
- # The model every harness tier (plan, build, fast) runs on in CI, planner included. Unset,
10
- # the tiers take harness.json, whose models the AWF proxy may not price.
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; for a custom model the credit
98
- # ceiling is models.dev fallback pricing and guards nothing.
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
- # The model every harness tier (plan, build, fast) runs on in CI, planner included. Unset,
10
- # the tiers take harness.json, whose models the AWF proxy may not price.
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; for a custom model the credit
458
- # ceiling is models.dev fallback pricing and guards nothing.
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, and harness.json names carina-60b and
186
- # quasar-438b, which it has none for (Atlas #2016), so CI pins every tier to one priced
187
- # model through the gitignored per-user override the plugin reads first.
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
- if [ -f .opencode/plugins/pc-system-reminders.js ]; then
197
- rm .opencode/plugins/pc-system-reminders.js
198
- # Tracked, so hide the deletion: an agent that sees it in `git status` restores it.
199
- git update-index --skip-worktree .opencode/plugins/pc-system-reminders.js 2>/dev/null || true
200
- fi
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
- ## Verification, for every worker that builds or pushes
26
-
27
- - Scope every check to the files you changed. This is the runner, not a preference: a
28
- whole-repository lint, build or test run exhausts its memory and gets killed mid-run, which
29
- fails the job with no useful output. Pass the changed paths to the linter (`pnpm exec biome
30
- check <files>`), build only the projects containing them, and run only the test project that
31
- covers them. Escalate to the full suite only after the scoped run passes and only when the
32
- change crosses a project boundary.
33
- - Run nothing at all for a change that touches only documentation. A cold Release build on a
34
- shared runner is minutes the run does not get back.
35
- - Never make a check pass by weakening it: not a deleted test, not a lowered threshold, not a
36
- skipped step. Fix the cause and run it again.
37
- - Once the checks pass, run the project's lint fix command over the files you changed
38
- (`pnpm lint:fix`, `pnpm exec biome check --write <changed-files>`, or its equivalent), or
39
- correct what lint reports where no fix command exists. A branch that arrives with lint errors
40
- gets sent back for them.
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.