@plainconceptsplatform/workflows 1.6.21 → 1.6.30

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.
@@ -61,9 +61,9 @@ runs:
61
61
  run: |
62
62
  set -euo pipefail
63
63
  # The agent job belongs to this same run: a called workflow shares the caller's run id.
64
- read -r job_id started finished <<<"$(gh api "repos/$REPO/actions/runs/$RUN_ID/jobs?per_page=100" \
64
+ read -r job_id started finished conclusion <<<"$(gh api "repos/$REPO/actions/runs/$RUN_ID/jobs?per_page=100" \
65
65
  --jq '[.jobs[] | select(.name | endswith("agent"))] | last // empty
66
- | "\(.id // "") \(.started_at // "") \(.completed_at // "")"')"
66
+ | "\(.id // "") \(.started_at // "") \(.completed_at // "") \(.conclusion // "")"')"
67
67
  minutes=-1
68
68
  if [ -n "${started:-}" ] && [ -n "${finished:-}" ]; then
69
69
  minutes=$(( ( $(date -u -d "$finished" +%s) - $(date -u -d "$started" +%s) ) / 60 ))
@@ -91,6 +91,14 @@ runs:
91
91
  fi
92
92
  rm -rf "$work"
93
93
  fi
94
+ # A cancelled agent job is a person or a newer run taking the work back, not an outage.
95
+ # Pliny-Bot #816: refine was cancelled by hand and its label removed, then this step read
96
+ # the eight-minute run as a provider failure, sent it back through the router and parked
97
+ # the issue as stalled.
98
+ if [ "${conclusion:-}" = cancelled ]; then
99
+ retry=false
100
+ cause=cancelled
101
+ fi
94
102
  {
95
103
  echo "retry=$retry"
96
104
  echo "next=$((attempts + 1))"
@@ -28,6 +28,10 @@ inputs:
28
28
  description: How many times a blocked verdict, or a merge that did not happen, is retried after the base branch moves.
29
29
  required: false
30
30
  default: '3'
31
+ stack-root-automerge:
32
+ description: A stack root whose children have all landed is merged by the gate like any other pull request, unless its blast radius or a finding sends it to a person. false keeps every root for a human.
33
+ required: false
34
+ default: 'true'
31
35
  max-repairs:
32
36
  description: How many pull requests may be in repair at once. Merges stay one at a time.
33
37
  required: false
@@ -69,6 +73,7 @@ runs:
69
73
  HOLD_LABEL: ${{ inputs.hold-label }}
70
74
  MAX_ATTEMPTS: ${{ inputs.max-gate-attempts }}
71
75
  MAX_RETRIES: ${{ inputs.max-retries }}
76
+ STACK_ROOT_AUTOMERGE: ${{ inputs.stack-root-automerge }}
72
77
  STUCK_HOURS: ${{ inputs.stuck-hours }}
73
78
  MAIN_RED_WAIT_HOURS: ${{ inputs.main-red-wait-hours }}
74
79
  MAX_REPAIRS: ${{ inputs.max-repairs }}
@@ -114,14 +114,19 @@ train_move() {
114
114
  # BASE_MOVED true when the base branch has a commit newer than the verdict
115
115
  # RETRIES blocked or failed-merge verdicts already given on this head
116
116
  # MAX_RETRIES how many of those the train retries before leaving it to a person
117
- # STACK_ROOT true for a stack's root pull request, which a person always merges
117
+ # STACK_ROOT true for a stack's root pull request, which a person merges unless it is ready
118
+ # STACK_ROOT_READY true for a root whose stack was announced complete after the last verdict, with
119
+ # STACK_ROOT_AUTOMERGE on: the train gives it one more gate run, which merges it
120
+ # unless the blast radius or a finding says a person must
118
121
  train_park() {
119
122
  local park=none
120
123
  if [ "${VERDICT_NEWER:-false}" = true ]; then
121
124
  case "${VERDICT:-}" in
122
125
  remediated | "") park=none ;;
123
126
  blocked | auto-merge)
124
- if [ "${STACK_ROOT:-false}" = true ]; then
127
+ if [ "${STACK_ROOT:-false}" = true ] && [ "${STACK_ROOT_READY:-false}" = true ]; then
128
+ park=none
129
+ elif [ "${STACK_ROOT:-false}" = true ]; then
125
130
  park=held
126
131
  elif [ "${BASE_MOVED:-false}" = true ] && [ "${RETRIES:-0}" -lt "${MAX_RETRIES:-3}" ]; then
127
132
  park=none
@@ -360,15 +365,23 @@ for line in "${candidates[@]}"; do
360
365
  fi
361
366
 
362
367
  stack_root=false
368
+ stack_root_ready=false
363
369
  if [ "$verdict_newer" = true ] && [ -n "$linked_issue" ]; then
364
370
  if gh issue view "$linked_issue" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null |
365
371
  grep -q "<!-- stack-root-pr: $pr -->"; then
366
372
  stack_root=true
373
+ if [ "${STACK_ROOT_AUTOMERGE:-false}" = true ]; then
374
+ announced_at=$(gh api --paginate "repos/$REPO/issues/$pr/comments?per_page=100" \
375
+ --jq '.[] | select(.body | contains("<!-- stack-complete -->")) | .created_at' 2>/dev/null | sort | tail -1)
376
+ if [ -n "$announced_at" ] && [ "$announced_at" \> "$verdict_at" ]; then
377
+ stack_root_ready=true
378
+ fi
379
+ fi
367
380
  fi
368
381
  fi
369
382
 
370
383
  park=$(VERDICT="$verdict" VERDICT_NEWER="$verdict_newer" BASE_MOVED="$base_moved" \
371
- RETRIES="$retries" MAX_RETRIES="$MAX_RETRIES" STACK_ROOT="$stack_root" train_park)
384
+ RETRIES="$retries" MAX_RETRIES="$MAX_RETRIES" STACK_ROOT="$stack_root" STACK_ROOT_READY="$stack_root_ready" train_park)
372
385
 
373
386
  attempts=0
374
387
  silent_gate=false
@@ -41,6 +41,9 @@ outputs:
41
41
  pushed:
42
42
  description: Whether the agent proposed a push to the pull request branch, applied or not.
43
43
  value: ${{ steps.validate.outputs.pushed }}
44
+ empty:
45
+ description: Whether the agent produced no safe output at all, which is a run that quit, not a report that was refused.
46
+ value: ${{ steps.validate.outputs.empty }}
44
47
  runs:
45
48
  using: composite
46
49
  steps:
@@ -66,6 +69,8 @@ runs:
66
69
  echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
67
70
  pushed=$(jq -r '[.items[]? | select(.type == "push_to_pull_request_branch")] | length > 0' "$OUTPUT_FILE" 2>/dev/null || echo false)
68
71
  echo "pushed=$pushed" >> "$GITHUB_OUTPUT"
72
+ items=$(jq -r '[.items[]?] | length' "$OUTPUT_FILE" 2>/dev/null || echo 0)
73
+ echo "empty=$([ "${items:-0}" = 0 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
69
74
 
70
75
  if [ "$outcome" = 'invalid' ]; then
71
76
  echo "::warning::Agent output has no usable merge-gate report. It will not be applied."
@@ -1828,6 +1828,23 @@ if worker_installed implement && ! grep -qF 'subagent_type: planner' "$IMPLEMENT
1828
1828
  LEAD_OK=0; echo "FAIL: the implement prompt must run propose and archive in the planner agent" >&2
1829
1829
  fi
1830
1830
  if [ "$LEAD_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
1831
+ # The harness plugins only load with .opencode/node_modules installed; without it every CI run
1832
+ # spawned untiered workers on the session's model. And the tier model a worker pins has to be one
1833
+ # opencode.ci.json declares, under the provider name harness.json uses.
1834
+ PLUGINS_OK=1
1835
+ if ! grep -qF "(cd .opencode && npm install" "${WORKFLOWS_DIR}/shared/opencode-ci.md"; then
1836
+ PLUGINS_OK=0; echo "FAIL: the CI setup must install .opencode's dependencies or the harness plugins never load" >&2
1837
+ fi
1838
+ for route in refine implement triage apply-review merge-gate audit release; do
1839
+ worker_md="${WORKFLOWS_DIR}/agent-${route}.md"
1840
+ [ -f "$worker_md" ] || continue
1841
+ tier=$(sed -n 's/^ AGENT_TIER_MODEL: "\(.*\)"$/\1/p' "$worker_md")
1842
+ [ -n "$tier" ] || continue
1843
+ if [ -f "$CI_JSON" ] && ! jq -e --arg p "${tier%%/*}" --arg m "${tier#*/}" '.providers[$p].models[$m]' "$CI_JSON" > /dev/null; then
1844
+ PLUGINS_OK=0; echo "FAIL: agent-${route} pins its tiers to ${tier}, which opencode.ci.json does not declare" >&2
1845
+ fi
1846
+ done
1847
+ if [ "$PLUGINS_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
1831
1848
  # The shape of the tests reaches the agent only through these two prompts (check-diff measures size,
1832
1849
  # never shape), so losing the sentence silently brings the pyramid back.
1833
1850
  if worker_installed implement; then
@@ -2024,6 +2041,8 @@ if worker_installed merge-gate; then
2024
2041
  assert "blocked holds once its retries are spent" held "$(park VERDICT=blocked VERDICT_NEWER=true BASE_MOVED=true RETRIES=3 MAX_RETRIES=3)"
2025
2042
  assert "a merge that did not happen is retried once the base moves" none "$(park VERDICT=auto-merge VERDICT_NEWER=true BASE_MOVED=true RETRIES=1)"
2026
2043
  assert "a stack root is never retried into a merge" held "$(park VERDICT=auto-merge VERDICT_NEWER=true BASE_MOVED=true RETRIES=0 STACK_ROOT=true)"
2044
+ assert "a root whose stack was announced complete gets one more gate run" none "$(park VERDICT=auto-merge VERDICT_NEWER=true BASE_MOVED=false RETRIES=0 STACK_ROOT=true STACK_ROOT_READY=true)"
2045
+ assert "a ready root whose merge failed is held again, not looped" held "$(park VERDICT=owner-review VERDICT_NEWER=true STACK_ROOT=true STACK_ROOT_READY=true)"
2027
2046
 
2028
2047
  assert "hold keeps a pull request off the train" skip "$(move HOLD=true CI_STATUS=completed CI_CONCLUSION=success)"
2029
2048
  assert "requested changes keep it off the train" skip "$(move REVIEW_DECISION=CHANGES_REQUESTED CI_STATUS=completed CI_CONCLUSION=success)"
@@ -2721,6 +2740,70 @@ if worker_installed implement; then
2721
2740
  echo "FAIL: the implement prompt does not forbid running commands in the background and ending the turn to wait for them" >&2
2722
2741
  fi
2723
2742
  fi
2743
+ # The bash tool moves any command past its timeout to the background and says so. Pliny-Bot #774 took
2744
+ # that for an end, wrote "I'll wait for the verify command" and stopped after sixty-seven minutes. The
2745
+ # prompt must say that the message is not the end and to poll with tool calls.
2746
+ if worker_installed implement; then
2747
+ if grep -qF 'answers "moved to the background"' "${WORKFLOWS_DIR}/agent-implement.md" && grep -qF "sleep 40; tail -n 15" "${WORKFLOWS_DIR}/agent-implement.md"; then
2748
+ PASS=$((PASS + 1))
2749
+ else
2750
+ FAIL=$((FAIL + 1))
2751
+ echo "FAIL: the implement prompt does not tell the agent to keep polling when bash moves a command to the background" >&2
2752
+ fi
2753
+ fi
2754
+ # Every worker is told that a turn ending in text ends the run. The Pliny-Bot merge gate said "I'll read
2755
+ # the required context files first." and exited cleanly after twenty-three seconds, twice on one head.
2756
+ PLATFORM_DEFAULTS_MD="${WORKFLOWS_DIR}/shared/platform-defaults.md"
2757
+ if [ -f "$PLATFORM_DEFAULTS_MD" ]; then
2758
+ if grep -qF "A turn that ends with text and no tool call ends the whole run" "$PLATFORM_DEFAULTS_MD" && grep -qF "every message you write carries a tool call" "$PLATFORM_DEFAULTS_MD"; then
2759
+ PASS=$((PASS + 1))
2760
+ else
2761
+ FAIL=$((FAIL + 1))
2762
+ echo "FAIL: the shared worker defaults do not say that a text-only turn ends the run" >&2
2763
+ fi
2764
+ fi
2765
+ # A gate agent that produced no output at all quit; it did not write a report the validator refused.
2766
+ # It gets the machine budget, not the two-strike human-review of an unusable report.
2767
+ if worker_installed merge-gate; then
2768
+ if grep -qF 'OUTPUT_EMPTY: ${{ needs.validate_output.outputs.empty }}' "${WORKFLOWS_DIR}/agent-merge-gate.md" && grep -qF '[ "${OUTPUT_EMPTY:-false}" != true ]' "${WORKFLOWS_DIR}/agent-merge-gate.md" && grep -qF 'echo "empty=' "${HERE}/../validate-merge-gate-output/action.yml"; then
2769
+ PASS=$((PASS + 1))
2770
+ else
2771
+ FAIL=$((FAIL + 1))
2772
+ echo "FAIL: a gate agent that produced nothing is sent to a person instead of being retried" >&2
2773
+ fi
2774
+ fi
2775
+ # A cancelled agent job is work taken back, not an outage: no retry, no stalled flag. Pliny-Bot #816 was
2776
+ # cancelled by hand, re-dispatched as "a provider outage" and parked as stalled.
2777
+ DECIDE_YML="${HERE}/../decide-agent-retry/action.yml"
2778
+ if [ -f "$DECIDE_YML" ] && grep -qF 'cause=cancelled' "$DECIDE_YML" && grep -qF "steps.decide.outputs.cause != 'cancelled'" "${WORKFLOWS_DIR}/agent-refine.md" && grep -qF "steps.decide.outputs.cause != 'cancelled'" "${WORKFLOWS_DIR}/agent-implement.md"; then
2779
+ PASS=$((PASS + 1))
2780
+ else
2781
+ FAIL=$((FAIL + 1))
2782
+ echo "FAIL: a cancelled agent run is retried or parked as stalled" >&2
2783
+ fi
2784
+ # No prompt rule stopped the model ending a turn in text; the compiled engine command continues the
2785
+ # session when no safe output exists. Pliny-Bot #771 passed verify and exited with nothing delivered.
2786
+ COMPILE_MJS="${HERE}/../../scripts/compile-agent-workflows.mjs"
2787
+ if [ -f "$COMPILE_MJS" ] && grep -qF 'opencode run --continue' "$COMPILE_MJS" && grep -qF 'gh-aw/safeoutputs/outputs.jsonl' "$COMPILE_MJS"; then
2788
+ PASS=$((PASS + 1))
2789
+ else
2790
+ FAIL=$((FAIL + 1))
2791
+ echo "FAIL: the compiled engine command does not continue a session that delivered nothing" >&2
2792
+ fi
2793
+ # A stack root merges itself once every child has landed, through the same gate as any pull request.
2794
+ # The people it still stops are the gate's own: a blast radius over the limit, a protected path, a
2795
+ # finding. The gate must read the router's completion announcement, and the train must give the root
2796
+ # one more run after it.
2797
+ GATE_MD="${WORKFLOWS_DIR}/agent-merge-gate.md"
2798
+ TRAIN_SH="${HERE}/../merge-train/merge-train.sh"
2799
+ if worker_installed merge-gate && [ -f "$TRAIN_SH" ]; then
2800
+ if grep -qF 'root_complete=$root_complete' "$GATE_MD" && grep -qF "needs.subject.outputs.root_complete == 'true'" "$GATE_MD" && grep -qF 'STACK_ROOT_AUTOMERGE:' "$GATE_MD" && grep -qF 'STACK_ROOT_READY' "$TRAIN_SH"; then
2801
+ PASS=$((PASS + 1))
2802
+ else
2803
+ FAIL=$((FAIL + 1))
2804
+ echo "FAIL: a completed stack root is not merged by the gate and re-run by the train" >&2
2805
+ fi
2806
+ fi
2724
2807
  # The merge train wakes on a finished CI run only. Pliny-Bot had four green pull requests wait an hour
2725
2808
  # because a repair gate dispatched for red CI kept them marked as having a gate, and nothing called the
2726
2809
  # train again once it was cancelled. The hourly sweep is now a heartbeat, and a repair gate whose CI has
@@ -184,9 +184,10 @@ flowchart TD
184
184
 
185
185
  The stack exists only in markers on the issues — `stack-root-branch` and `stack-root-pr` on the
186
186
  feature issue, `stack-root-branch` stamped on each child at hand-over — so a repository with none
187
- of them runs exactly the pipeline above and nothing else. The root is never auto-merged: a person
188
- merges it after every child has landed and the feature branch is green, which is the point where
189
- the feature's own acceptance journey, not one child's scope, is the thing being verified.
187
+ of them runs exactly the pipeline above and nothing else. The root is held while children land.
188
+ Once every child has landed and the feature branch is green, the gate decides it like any pull
189
+ request and merges it, unless the blast radius, a protected path or a finding sends it to a person
190
+ (`STACK_ROOT_AUTOMERGE: "false"` restores a human merge for every root).
190
191
 
191
192
  ---
192
193
 
@@ -385,7 +386,7 @@ flowchart TD
385
386
  gateDisposition -.->|"fragile, unmet criteria<br/>or low confidence"| gateHuman
386
387
  gateDisposition -.->|"CI red or a verified<br/>high finding"| gateBlocked
387
388
  gateMerge(("AUTO-MERGE<br/>squash into the PR's base<br/>(main, or the feature branch:<br/>then the child issue closes,<br/>pr-pending removed)"))
388
- gateRootCheck -->|"yes: the root"| gateHeld(("HELD<br/>stack root never auto-merges,<br/>a person merges it once every<br/>child has landed and it is green"))
389
+ gateRootCheck -->|"yes: the root"| gateHeld(("HELD<br/>stack root waits for its children,<br/>then merges like any pull request<br/>unless the gate sends it to a person"))
389
390
  gateHuman(("HUMAN REVIEW<br/>review label,<br/>implement label kept"))
390
391
  gateOwner(("OWNER REVIEW<br/>review + owner-review,<br/>CODEOWNERS asked when named"))
391
392
  gateHuman --> gatePreview
@@ -3,6 +3,14 @@
3
3
  const OPENCODE_VERSION = '2.0.22'
4
4
  const OPENCODE_PACKAGE = '@opencode/cli'
5
5
 
6
+ const OPENCODE_NUDGE =
7
+ '; rc=$?; for nudge in 1 2; do [ -s "${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" ] && break; ' +
8
+ 'echo "No safe output yet, continuing the session ($nudge of 2)"; ' +
9
+ 'OPENCODE_DISABLE_MODELS_FETCH=1 opencode run --continue --print-logs --log-level info ' +
10
+ '"Your last message ended without a tool call, so nothing has been delivered and this run is about to end. ' +
11
+ 'Do not explain or plan: continue the task now and finish with the safe-output call the task names."; ' +
12
+ 'rc=$?; done; exit $rc'
13
+
6
14
  // The install the compiled locks carry, shared so both rewrite sites emit exactly
7
15
  // the same command.
8
16
  //
@@ -153,6 +161,11 @@ for (const file of readdirSync(workflowDirectory)) {
153
161
  // "Invalid value for flag --log-level", aborting before the agent runs.
154
162
  // The old upper-case spelling was valid in v1 and is not in v2.
155
163
  .replace(/opencode run(?: --print-logs)? --log-level (?:DEBUG|ERROR|INFO|debug|error|info)/g, "OPENCODE_DISABLE_MODELS_FETCH=1 opencode run --print-logs --log-level info")
164
+ // A turn that ends in text ends `opencode run`, whatever the text promised, and no prompt rule
165
+ // stopped glm-5-2 doing it: Pliny-Bot #771 passed verify, wrote "let me discover the exact
166
+ // safeoutputs command" and exited with nothing delivered. When no safe output exists yet, the
167
+ // same session is continued, twice at most, and told to finish.
168
+ .replace(/("\$\(cat \/tmp\/gh-aw\/aw-prompts\/prompt\.txt\)")'/g, (_, prompt) => prompt + OPENCODE_NUDGE + "'")
156
169
  .replace(/GH_AW_INFO_MODEL: "[^"]*"/g, 'GH_AW_INFO_MODEL: "per-agent"')
157
170
  .replace(/OPENCODE_MODEL: [^\n]+/g, "OPENCODE_MODEL: ''")
158
171
  .replace(/GH_AW_INFO_MODEL_COSTS: '[^']*'/g, 'GH_AW_INFO_MODEL_COSTS: \'{"providers":{}}\'')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://opencode.ai/config.json",
3
- "model": "forge/carina-60b",
3
+ "model": "forge/glm-5-2",
4
4
  "default_agent": "ci-workflow-agent",
5
5
  "agents": {
6
6
  "ci-workflow-agent": {
@@ -5,7 +5,10 @@ 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/quasar-438b"
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.
11
+ AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
9
12
  # Printed by the prompt as the verification block. Consumers set their own commands.
10
13
  VERIFY_COMMANDS: ""
11
14
  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."
@@ -378,7 +381,7 @@ jobs:
378
381
  issue-number: ${{ needs.subject.outputs.issue }}
379
382
  labels: ${{ env.WORKING_LABEL }}
380
383
  - name: Flag for human review
381
- if: steps.decide.outputs.retry != 'true'
384
+ if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
382
385
  uses: ./.github/actions/add-issue-labels
383
386
  with:
384
387
  token: ${{ steps.app-token.outputs.token }}
@@ -387,7 +390,7 @@ jobs:
387
390
  ${{ env.REVIEW_LABEL }}
388
391
  ${{ env.STALLED_LABEL }}
389
392
  - name: Report missing review feedback outcome
390
- if: steps.decide.outputs.retry != 'true'
393
+ if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
391
394
  uses: ./.github/actions/create-issue-comment
392
395
  with:
393
396
  token: ${{ steps.app-token.outputs.token }}
@@ -411,7 +414,7 @@ engine:
411
414
  env:
412
415
  OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
413
416
 
414
- model: openai/quasar-438b
417
+ model: openai/glm-5-2
415
418
 
416
419
  max-turns: 300
417
420
  max-turn-cache-misses: 3000
@@ -5,7 +5,10 @@ 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/quasar-438b"
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.
11
+ AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
9
12
  REPO_RULES: "Read-only repository audit. Report only reproducible, actionable defects with evidence. Do not modify files, commit, push, or run write operations."
10
13
  # Split out of REPO_RULES, which carried both the read-only discipline above and the list
11
14
  # below. All four consuming repositories had customised the list and none could touch it
@@ -72,7 +75,7 @@ engine:
72
75
  env:
73
76
  OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
74
77
 
75
- model: openai/quasar-438b
78
+ model: openai/glm-5-2
76
79
 
77
80
  max-turns: 300
78
81
  max-turn-cache-misses: 3000
@@ -5,7 +5,10 @@ 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/quasar-438b"
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.
11
+ AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
9
12
  VERIFY_COMMANDS: ""
10
13
  REPO_RULES: "Implement only the selected issue. Follow repository documentation and existing conventions. Do not weaken tests, lower coverage thresholds, or bypass checks."
11
14
  # Split out of REPO_RULES because one field asked to carry architecture, testing, coverage and
@@ -590,7 +593,7 @@ jobs:
590
593
  ${{ env.WORKING_LABEL }}
591
594
  implement
592
595
  - name: Flag for human review
593
- if: steps.decide.outputs.retry != 'true'
596
+ if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
594
597
  uses: ./.github/actions/add-issue-labels
595
598
  with:
596
599
  token: ${{ steps.app-token.outputs.token }}
@@ -599,7 +602,7 @@ jobs:
599
602
  ${{ env.REVIEW_LABEL }}
600
603
  ${{ env.STALLED_LABEL }}
601
604
  - name: Report missing implementation outcome
602
- if: steps.decide.outputs.retry != 'true'
605
+ if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
603
606
  uses: ./.github/actions/create-issue-comment
604
607
  with:
605
608
  token: ${{ steps.app-token.outputs.token }}
@@ -627,7 +630,7 @@ engine:
627
630
  env:
628
631
  OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
629
632
 
630
- model: openai/quasar-438b
633
+ model: openai/glm-5-2
631
634
 
632
635
  max-turns: 300
633
636
  max-turn-cache-misses: 3000
@@ -841,6 +844,16 @@ timeout-minutes: 180
841
844
  so every part finishes inside one call. The only valid way to end the run is the safe-output
842
845
  call in step 6.
843
846
 
847
+ **If the bash tool answers "moved to the background", the run is not over and you are not
848
+ waiting: keep going with tool calls.** It does that to any command that passes its timeout, even
849
+ when you passed none, and Pliny-Bot #774 ended that way on `pnpm verify --quick` after
850
+ sixty-seven minutes with no pull request. Poll it: call bash with `sleep 40; tail -n 15` on the
851
+ command's output file, again and again, until the output shows its exit status, and only then
852
+ go on. Each of those calls is a tool call, so the turn never ends. To avoid it from the start,
853
+ run a command that may pass two minutes as
854
+ `(<command>) > /tmp/gh-aw/agent/step.log 2>&1; echo "exit status $?" >> /tmp/gh-aw/agent/step.log` with the bash tool's
855
+ `timeout` at 600000, and read `/tmp/gh-aw/agent/step.log` afterwards.
856
+
844
857
  **Two things the unit tests do not catch, and CI will.** When you add a dependency, take the
845
858
  latest released version and run the repository's audit for it (`pnpm audit --audit-level high`
846
859
  for the web, `dotnet list package --vulnerable` for the backend): Pliny-Bot #752 added a PDF
@@ -5,7 +5,10 @@ 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/quasar-438b"
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.
11
+ AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
9
12
  VERIFY_COMMANDS: ""
10
13
  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."
11
14
  # Where to look first, not what to escalate on. This list used to be the check that decided
@@ -46,6 +49,9 @@ env:
46
49
  # translations and generated code excluded) is too big to merge unseen. It was a thousand, which
47
50
  # is a normal feature once the tests are left out. A file count alone never decides, so the high
48
51
  # file threshold is set out of reach; spread still raises medium.
52
+ # "true" lets a stack root merge once every child has landed, through the same blast radius and
53
+ # findings as any pull request. "false" holds every root for a person.
54
+ STACK_ROOT_AUTOMERGE: "true"
49
55
  BLAST_HIGH_FILES: "100000"
50
56
  BLAST_HIGH_LINES: "10000"
51
57
  BLAST_MEDIUM_FILES: "5"
@@ -174,6 +180,7 @@ jobs:
174
180
  review_blocked: ${{ steps.review.outputs.review_blocked }}
175
181
  pr_base: ${{ steps.stack.outputs.pr_base }}
176
182
  stack_root: ${{ steps.stack.outputs.stack_root }}
183
+ root_complete: ${{ steps.stack.outputs.root_complete }}
177
184
  steps:
178
185
  - name: Checkout workflow actions
179
186
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -227,18 +234,28 @@ jobs:
227
234
  REPO: ${{ github.repository }}
228
235
  PR: ${{ steps.subject.outputs.pr }}
229
236
  ISSUE: ${{ steps.subject.outputs.issue }}
237
+ STACK_ROOT_AUTOMERGE: ${{ env.STACK_ROOT_AUTOMERGE }}
230
238
  run: |
231
239
  set -euo pipefail
232
240
  pr_base=$(gh pr view "$PR" --repo "$REPO" --json baseRefName --jq '.baseRefName // ""')
233
241
  stack_root=false
242
+ root_complete=false
234
243
  if [ -n "$ISSUE" ]; then
235
244
  body=$(gh issue view "$ISSUE" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null || echo "")
236
245
  if echo "$body" | grep -q "<!-- stack-root-pr: $PR -->"; then
237
246
  stack_root=true
247
+ # The router announces a stack complete on the root once every child has landed and
248
+ # CI is green. A root without that comment is still collecting children.
249
+ if [ "$STACK_ROOT_AUTOMERGE" = true ] \
250
+ && gh api --paginate "repos/$REPO/issues/$PR/comments?per_page=100" \
251
+ --jq '.[] | select(.body | contains("<!-- stack-complete -->")) | .id' 2>/dev/null | grep -q .; then
252
+ root_complete=true
253
+ fi
238
254
  fi
239
255
  fi
240
256
  echo "pr_base=$pr_base" >> "$GITHUB_OUTPUT"
241
257
  echo "stack_root=$stack_root" >> "$GITHUB_OUTPUT"
258
+ echo "root_complete=$root_complete" >> "$GITHUB_OUTPUT"
242
259
 
243
260
  # Rung 4b. Everything the merge decision needs that a shell can establish, measured once,
244
261
  # before any model reads the diff. The job kept its name and its `requires_review` and
@@ -476,6 +493,7 @@ jobs:
476
493
  valid: ${{ steps.validate.outputs.valid }}
477
494
  outcome: ${{ steps.validate.outputs.outcome }}
478
495
  pushed: ${{ steps.validate.outputs.pushed }}
496
+ empty: ${{ steps.validate.outputs.empty }}
479
497
  steps:
480
498
  - name: Checkout workflow actions
481
499
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -586,7 +604,7 @@ jobs:
586
604
  ${{ 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 }})
587
605
  ${{ 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.' || '' }}
588
606
  - name: Merge approved pull request
589
- if: needs.validate_output.outputs.outcome == 'auto-merge' && needs.subject.outputs.stack_root != 'true'
607
+ if: needs.validate_output.outputs.outcome == 'auto-merge' && (needs.subject.outputs.stack_root != 'true' || needs.subject.outputs.root_complete == 'true')
590
608
  env:
591
609
  GH_TOKEN: ${{ steps.app-token.outputs.token }}
592
610
  REPO: ${{ github.repository }}
@@ -600,7 +618,7 @@ jobs:
600
618
  # what a reader needs: the gate held the root, children merge into the feature branch,
601
619
  # and a human merges the root when the stack completes.
602
620
  - name: Hold a stack root for a human merge
603
- if: needs.validate_output.outputs.outcome == 'auto-merge' && needs.subject.outputs.stack_root == 'true'
621
+ if: needs.validate_output.outputs.outcome == 'auto-merge' && needs.subject.outputs.stack_root == 'true' && needs.subject.outputs.root_complete != 'true'
604
622
  uses: ./.github/actions/create-issue-comment
605
623
  with:
606
624
  token: ${{ steps.app-token.outputs.token }}
@@ -752,6 +770,7 @@ jobs:
752
770
  AGENT_RESULT: ${{ needs.agent.result }}
753
771
  SAFE_RESULT: ${{ needs.safe_outputs.result }}
754
772
  OUTPUT_VALID: ${{ needs.validate_output.outputs.valid }}
773
+ OUTPUT_EMPTY: ${{ needs.validate_output.outputs.empty }}
755
774
  PARK_AT_ATTEMPT: ${{ env.PARK_AT_ATTEMPT }}
756
775
  PARK_AT_UNUSABLE_OUTPUT: ${{ env.PARK_AT_UNUSABLE_OUTPUT }}
757
776
  run: |
@@ -759,8 +778,12 @@ jobs:
759
778
  attempts=${ATTEMPTS:-0}
760
779
  # The agent ran, published, and produced something the validator refused. Repeating
761
780
  # that reproduces it; a person reading the comment costs less than three more runs
762
- # holding the merge belt.
763
- if [ "$AGENT_RESULT" = success ] && [ "$SAFE_RESULT" = success ] && [ "$OUTPUT_VALID" != true ]; then
781
+ # holding the merge belt. An agent that produced nothing at all is not that: it quit,
782
+ # as the Pliny-Bot gate did on #801 after one line of text and twenty-three seconds,
783
+ # twice, and went to a person with no finding. That is a machine failure and gets the
784
+ # machine budget.
785
+ if [ "$AGENT_RESULT" = success ] && [ "$SAFE_RESULT" = success ] && [ "$OUTPUT_VALID" != true ] \
786
+ && [ "${OUTPUT_EMPTY:-false}" != true ]; then
764
787
  threshold="$PARK_AT_UNUSABLE_OUTPUT"
765
788
  kind=unusable
766
789
  else
@@ -875,7 +898,7 @@ engine:
875
898
  env:
876
899
  OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
877
900
 
878
- model: openai/quasar-438b
901
+ model: openai/glm-5-2
879
902
 
880
903
  max-turns: 300
881
904
  max-turn-cache-misses: 3000
@@ -5,7 +5,10 @@ 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-3"
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.
11
+ AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
9
12
  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."
10
13
  # The estimate decides whether a story gets split, and the prompt tells the agent these bands
11
14
  # come from this repository's own merged pull requests. They have to actually come from it, or
@@ -770,7 +773,7 @@ jobs:
770
773
  issue-number: ${{ inputs.issue-number }}
771
774
  labels: ${{ env.WORKING_LABEL }}
772
775
  - name: Flag for human review
773
- if: steps.decide.outputs.retry != 'true'
776
+ if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
774
777
  uses: ./.github/actions/add-issue-labels
775
778
  with:
776
779
  token: ${{ steps.app-token.outputs.token }}
@@ -779,7 +782,7 @@ jobs:
779
782
  ${{ env.REVIEW_LABEL }}
780
783
  ${{ env.STALLED_LABEL }}
781
784
  - name: Report missing refinement outcome
782
- if: steps.decide.outputs.retry != 'true'
785
+ if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
783
786
  uses: ./.github/actions/create-issue-comment
784
787
  with:
785
788
  token: ${{ steps.app-token.outputs.token }}
@@ -812,7 +815,7 @@ engine:
812
815
  env:
813
816
  OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
814
817
 
815
- model: openai/glm-5-3
818
+ model: openai/glm-5-2
816
819
  # 150 rather than 500. With a four-hour clock this is the loop guard, and the worst
817
820
  # observed run used 44 turns, so this leaves better than three times the worst case.
818
821
  max-turns: 150
@@ -5,7 +5,10 @@ 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/quasar-438b"
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.
11
+ AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
9
12
  GIT_AUTHOR_NAME: "github-actions[bot]"
10
13
  GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
11
14
  GIT_COMMITTER_NAME: "github-actions[bot]"
@@ -47,7 +50,7 @@ engine:
47
50
  env:
48
51
  OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
49
52
 
50
- model: openai/quasar-438b
53
+ model: openai/glm-5-2
51
54
 
52
55
  max-turns: 100
53
56
  max-turn-cache-misses: 3000
@@ -5,7 +5,10 @@ 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/quasar-438b"
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.
11
+ AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
9
12
  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."
10
13
  # What a product owner may ask for, and what has to become a maintainer-owned technical
11
14
  # proposal instead. This is business policy, so it belongs to the repository rather than to
@@ -384,7 +387,7 @@ jobs:
384
387
  issue-number: ${{ inputs.issue-number }}
385
388
  labels: ${{ env.WORKING_LABEL }}
386
389
  - name: Flag for human review
387
- if: steps.decide.outputs.retry != 'true'
390
+ if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
388
391
  uses: ./.github/actions/add-issue-labels
389
392
  with:
390
393
  token: ${{ steps.app-token.outputs.token }}
@@ -393,7 +396,7 @@ jobs:
393
396
  ${{ env.REVIEW_LABEL }}
394
397
  ${{ env.STALLED_LABEL }}
395
398
  - name: Report missing triage outcome
396
- if: steps.decide.outputs.retry != 'true'
399
+ if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
397
400
  uses: ./.github/actions/create-issue-comment
398
401
  with:
399
402
  token: ${{ steps.app-token.outputs.token }}
@@ -417,7 +420,7 @@ engine:
417
420
  env:
418
421
  OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
419
422
 
420
- model: openai/quasar-438b
423
+ model: openai/glm-5-2
421
424
  max-turns: 120
422
425
  max-turn-cache-misses: 3000
423
426
  max-ai-credits: 5000
@@ -165,6 +165,45 @@ pre-agent-steps:
165
165
  - name: Install workspace dependencies
166
166
  run: pnpm install --frozen-lockfile
167
167
 
168
+ - name: Cache the agent-harness plugins' dependencies
169
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
170
+ with:
171
+ path: .opencode/node_modules
172
+ key: opencode-plugins-${{ runner.os }}-${{ hashFiles('.opencode/package.json') }}
173
+
174
+ - name: Install the agent-harness plugins' dependencies
175
+ # The harness plugins under .opencode/plugins import @opencode/plugin, which lives in
176
+ # .opencode/node_modules, and that directory is gitignored. Without it OpenCode logs
177
+ # "failed to load plugin ... Cannot find package '@opencode/plugin'" and goes on: no tier
178
+ # workers, no planner, no concurrency cap. Every CI run until 1.6.23 ran that way, so
179
+ # pc-plan-apply spawned the untiered base engineers on the session's model.
180
+ #
181
+ # Two of the three run here. pc-system-reminders gates shell and edits on skills a CI
182
+ # worker's prompt does not load, and has never run in CI; it stays off until it does.
183
+ #
184
+ # 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.
188
+ run: |
189
+ set -euo pipefail
190
+ if [ ! -f .opencode/package.json ]; then
191
+ echo "No .opencode/package.json: agent-harness is not installed, nothing to load"; exit 0
192
+ fi
193
+ if [ ! -d .opencode/node_modules/@opencode/plugin ]; then
194
+ (cd .opencode && npm install --no-audit --no-fund --no-package-lock --ignore-scripts --loglevel=error)
195
+ 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
201
+ if [ -n "${AGENT_TIER_MODEL:-}" ]; then
202
+ jq -n --arg m "$AGENT_TIER_MODEL" '{models: {plan: $m, build: $m, fast: $m}}' > .opencode/harness.user.json
203
+ fi
204
+ echo "Harness plugins: $(ls .opencode/plugins | tr '\n' ' ')"
205
+ echo "Tier models: $(jq -c '.models' .opencode/harness.user.json 2>/dev/null || jq -c '.models' .opencode/harness.json)"
206
+
168
207
  - name: Merge the CI-only OpenCode provider into opencode.jsonc
169
208
  run: |
170
209
  set -euo pipefail
@@ -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.
@@ -157,6 +157,10 @@ env:
157
157
  # How many times a `blocked` verdict, or a merge that did not happen, is retried once the base
158
158
  # branch has moved. After that the pull request waits for a person or a new commit.
159
159
  MERGE_TRAIN_MAX_RETRIES: "3"
160
+ # "true" lets a stack root merge itself once every child has landed and CI is green, through the
161
+ # same gate as any pull request: a change over the blast radius limit, a protected path or an
162
+ # unresolved finding still goes to a person. "false" keeps every root for a human.
163
+ MERGE_TRAIN_STACK_ROOT_AUTOMERGE: "true"
160
164
  # "true" logs the move the train would make and makes none. For trying the train on a
161
165
  # repository before letting it act.
162
166
  MERGE_TRAIN_DRY_RUN: "false"
@@ -922,6 +926,7 @@ jobs:
922
926
  hold-label: ${{ env.MERGE_TRAIN_HOLD_LABEL }}
923
927
  max-gate-attempts: ${{ env.MAX_GATE_ATTEMPTS }}
924
928
  max-retries: ${{ env.MERGE_TRAIN_MAX_RETRIES }}
929
+ stack-root-automerge: ${{ env.MERGE_TRAIN_STACK_ROOT_AUTOMERGE }}
925
930
  max-repairs: ${{ env.MERGE_TRAIN_REPAIRS }}
926
931
  dry-run: ${{ env.MERGE_TRAIN_DRY_RUN }}
927
932
  ci-run-id: ${{ needs.classify.outputs.ci-run-id }}
@@ -1265,7 +1270,7 @@ jobs:
1265
1270
  if [ "${already:-0}" -gt 0 ]; then
1266
1271
  echo "Stack #$parent already has a completion announcement; staying silent."
1267
1272
  else
1268
- gh pr comment "$root_pr" --repo "$REPO" --body "Stack complete: all children have merged into the feature branch and CI is green. This root pull request is ready for a human to review and merge into $DEFAULT_BRANCH. $completion_marker"
1273
+ gh pr comment "$root_pr" --repo "$REPO" --body "Stack complete: all children have merged into the feature branch and CI is green. This root pull request is ready to merge into $DEFAULT_BRANCH: the merge gate decides it next, and a person is asked only when its blast radius or a finding says so. $completion_marker"
1269
1274
  echo "Stack #$parent complete: announced root PR #$root_pr."
1270
1275
  fi
1271
1276
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/workflows",
3
- "version": "1.6.21",
3
+ "version": "1.6.30",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",