@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.
- package/loops/actions/decide-agent-retry/action.yml +10 -2
- package/loops/actions/merge-train/action.yml +5 -0
- package/loops/actions/merge-train/merge-train.sh +16 -3
- package/loops/actions/validate-merge-gate-output/action.yml +5 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +83 -0
- package/loops/docs/diagrams.md +5 -4
- package/loops/scripts/compile-agent-workflows.mjs +13 -0
- package/loops/templates/opencode/opencode.ci.json +1 -1
- package/loops/workflows/agent-apply-review.md +7 -4
- package/loops/workflows/agent-audit.md +5 -2
- package/loops/workflows/agent-implement.md +17 -4
- package/loops/workflows/agent-merge-gate.md +29 -6
- package/loops/workflows/agent-refine.md +7 -4
- package/loops/workflows/agent-release.md +5 -2
- package/loops/workflows/agent-triage.md +7 -4
- package/loops/workflows/shared/opencode-ci.md +39 -0
- package/loops/workflows/shared/platform-defaults.md +30 -17
- package/loops/workflows/work-router.yml +6 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
package/loops/docs/diagrams.md
CHANGED
|
@@ -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
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
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":{}}\'')
|
|
@@ -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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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/
|
|
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-
|
|
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-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
##
|
|
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.
|
|
@@ -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
|
|
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
|