@plainconceptsplatform/workflows 1.6.0 → 1.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +0 -0
- package/loops/actions/decide-agent-retry/action.yml +31 -3
- package/loops/actions/decide-agent-retry/classify-failure.sh +50 -0
- package/loops/actions/housekeeping/action.yml +20 -0
- package/loops/actions/identify-gate-subject/action.yml +36 -19
- package/loops/actions/merge-train/action.yml +5 -0
- package/loops/actions/merge-train/merge-train.sh +130 -33
- package/loops/actions/push-agent-branch/action.yml +14 -1
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +177 -5
- package/loops/docs/diagrams.md +2 -2
- package/loops/scripts/compile-agent-workflows.mjs +20 -3
- package/loops/workflows/agent-implement.md +78 -32
- package/loops/workflows/agent-merge-gate.md +19 -9
- package/loops/workflows/agent-refine.md +9 -0
- package/loops/workflows/work-router.yml +54 -7
- package/package.json +9 -8
|
@@ -493,6 +493,14 @@ if [ "$BELT_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
|
493
493
|
# trailing space, so a number at the end of a body is still followed by a non-digit, and the
|
|
494
494
|
# pattern matches `[^0-9]`. Four quoting layers cannot eat a character class.
|
|
495
495
|
SWEEP_OK=1
|
|
496
|
+
# The live runs come from status-filtered queries. Reading the repository's latest hundred runs of
|
|
497
|
+
# any kind let a long-queued implement fall out of the window, and the sweep cleared bot-working
|
|
498
|
+
# from issues whose runs were alive (Pliny-Bot #693 and #707).
|
|
499
|
+
if grep -q 'gh api "repos/\$REPO/actions/runs?per_page=100"' "$ROUTER_YML" ||
|
|
500
|
+
! grep -qF 'actions/runs?status=$status&per_page=100' "$ROUTER_YML"; then
|
|
501
|
+
SWEEP_OK=0
|
|
502
|
+
echo "FAIL: the stale-reservation sweep must read live runs by status, not the latest hundred runs" >&2
|
|
503
|
+
fi
|
|
496
504
|
if ! grep -qF 'test(\"#${issue}[^0-9]\")' "$ROUTER_YML"; then
|
|
497
505
|
SWEEP_OK=0
|
|
498
506
|
echo "FAIL: the stale-reservation sweep does not match a run title with a character class; a word boundary there is eaten by bash and jq" >&2
|
|
@@ -688,12 +696,15 @@ if worker_installed merge-gate; then
|
|
|
688
696
|
# The merge belt is serial for the whole repository: several overnight pull requests
|
|
689
697
|
# mean every merge moves the default branch under the rest, and gates running at once
|
|
690
698
|
# rebase onto bases other gates are about to invalidate. A per-issue group here would
|
|
691
|
-
# reintroduce that race, so assert the repo-wide lock is the one in use.
|
|
692
|
-
|
|
699
|
+
# reintroduce that race, so assert the repo-wide lock is the one in use. The exception is a gate
|
|
700
|
+
# handed red CI: it repairs and pushes and can never merge, so it queues per pull request.
|
|
701
|
+
gate_group="$(sed -n '/^ call-merge-gate:/,/^ permissions:/p' "$ROUTER_YML" | grep 'group:')"
|
|
702
|
+
if grep -q "'merge-belt'" <<<"$gate_group" &&
|
|
703
|
+
grep -q "ci-conclusion == 'failure' && format('merge-repair-{0}', needs.classify.outputs.pr-number)" <<<"$gate_group"; then
|
|
693
704
|
PASS=$((PASS + 1))
|
|
694
705
|
else
|
|
695
706
|
FAIL=$((FAIL + 1))
|
|
696
|
-
echo "FAIL: call-merge-gate must
|
|
707
|
+
echo "FAIL: call-merge-gate must queue on merge-belt, and only a gate handed red CI may queue per pull request instead" >&2
|
|
697
708
|
fi
|
|
698
709
|
fi
|
|
699
710
|
|
|
@@ -719,7 +730,9 @@ if worker_installed merge-gate; then
|
|
|
719
730
|
# One gate at a time, found by the title run-name gives it. The belt looked for a title that
|
|
720
731
|
# run-name had stopped producing, saw no live gate ever, and dispatched #680 five times in
|
|
721
732
|
# an hour onto a belt that runs one gate at a time.
|
|
722
|
-
gate_title="$(sed -n "s/.*format('\(Agent: Merge Gate - PR #\){0}'.*/\1/p" "$ROUTER_YML" | head -1)"
|
|
733
|
+
gate_title="$(sed -n "s/.*format('\(Agent: Merge Gate - PR #\){0}{1}'.*/\1/p" "$ROUTER_YML" | head -1)"
|
|
734
|
+
grep -q "' (repair)'" "$ROUTER_YML" ||
|
|
735
|
+
{ BELT_OK=0; echo "FAIL: run-name must mark a gate handed red CI as a (repair), the train counts them by it" >&2; }
|
|
723
736
|
if [ -z "$gate_title" ] || ! grep -qF "GATE_TITLE_PREFIX=\"${gate_title}\"" "$TRAIN_SH"; then
|
|
724
737
|
BELT_OK=0
|
|
725
738
|
echo "FAIL: the train looks for live gates titled differently from what run-name names them ('${gate_title:-unread}')" >&2
|
|
@@ -1771,6 +1784,151 @@ if [ -f "$GATE_VALIDATOR" ] && worker_installed merge-gate; then
|
|
|
1771
1784
|
if [ "$VALIDATOR_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
1772
1785
|
fi
|
|
1773
1786
|
|
|
1787
|
+
# The gate takes any pull request the train hands it. A subject check that refuses one without
|
|
1788
|
+
# leaving a verdict or an attempt is a loop: the train cannot tell a gate that declined from a gate
|
|
1789
|
+
# that has not run. Refusals are for what the train never hands over (a fork, a stranger, a pull
|
|
1790
|
+
# request that is not open), and the train filters those itself.
|
|
1791
|
+
if worker_installed merge-gate; then
|
|
1792
|
+
# check-diff exiting 1 is the `fail` the code_rules job reports. Under GitHub's `bash -e` a bare
|
|
1793
|
+
# assignment from it ends the step, so the report is never recorded and the gate burns its
|
|
1794
|
+
# budget on a failure it could have repaired.
|
|
1795
|
+
if grep -qF 'check-diff.mjs --base "$BASE" 2>&1) || code=$?' "$MERGE_GATE_WORKER_MD"; then
|
|
1796
|
+
PASS=$((PASS + 1))
|
|
1797
|
+
else
|
|
1798
|
+
FAIL=$((FAIL + 1))
|
|
1799
|
+
echo "FAIL: code_rules must capture check-diff's exit code with || code=\$?; a bare assignment dies under bash -e" >&2
|
|
1800
|
+
fi
|
|
1801
|
+
# A run killed by its infrastructure is repeated whatever its duration; one the agent got wrong is
|
|
1802
|
+
# not. The two signatures are read from places a tool's output cannot write: GitHub's annotations
|
|
1803
|
+
# for the runner shutdown, and the line above the sandbox teardown for an engine cut off one turn
|
|
1804
|
+
# after it asked the model something (Pliny-Bot #699 and #693's first attempt).
|
|
1805
|
+
CLASSIFY="${HERE}/../decide-agent-retry/classify-failure.sh"
|
|
1806
|
+
if [ -f "$CLASSIFY" ]; then
|
|
1807
|
+
ctmp="$(mktemp -d)"
|
|
1808
|
+
printf '%s
|
|
1809
|
+
' '2026-10-16T22:13:03Z > ci-workflow-agent · glm-5-2' '2026-10-16T22:13:05Z the grep printed a line' '2026-10-16T22:13:36Z > ci-workflow-agent · glm-5-2' '2026-10-16T22:13:37Z [INFO] Stopping containers...' >"$ctmp/cutoff.log"
|
|
1810
|
+
printf '%s
|
|
1811
|
+
' '2026-10-16T22:13:03Z > ci-workflow-agent · glm-5-2' '2026-10-16T22:13:36Z Done. The pull request is ready.' '2026-10-16T22:13:37Z [INFO] Stopping containers...' >"$ctmp/done.log"
|
|
1812
|
+
printf '%s
|
|
1813
|
+
' '2026-10-16T22:13:03Z the log quotes: Process completed with exit code 143 and The runner has received a shutdown signal' '2026-10-16T22:13:04Z Done.' '2026-10-16T22:13:37Z [INFO] Stopping containers...' >"$ctmp/quoted.log"
|
|
1814
|
+
printf '%s
|
|
1815
|
+
' 'Process completed with exit code 143.' 'Property x does not exist' >"$ctmp/sigterm.ann"
|
|
1816
|
+
printf '%s
|
|
1817
|
+
' 'Process completed with exit code 137.' >"$ctmp/sigkill.ann"
|
|
1818
|
+
printf '%s
|
|
1819
|
+
' 'Process completed with exit code 1.' >"$ctmp/plain.ann"
|
|
1820
|
+
assert "an engine stopped one turn after its request is a cut-off" cut-off "$(bash "$CLASSIFY" "$ctmp/cutoff.log" "")"
|
|
1821
|
+
assert "a run that said its piece is not a cut-off" none "$(bash "$CLASSIFY" "$ctmp/done.log" "")"
|
|
1822
|
+
assert "a tool quoting the shutdown text does not count" none "$(bash "$CLASSIFY" "$ctmp/quoted.log" "")"
|
|
1823
|
+
assert "exit 143 in the annotations is a runner shutdown" runner-shutdown "$(bash "$CLASSIFY" "$ctmp/done.log" "$ctmp/sigterm.ann")"
|
|
1824
|
+
assert "exit 137 (an out-of-memory kill) is repeated by nobody" none "$(bash "$CLASSIFY" "$ctmp/done.log" "$ctmp/sigkill.ann")"
|
|
1825
|
+
assert "an ordinary failure is not infrastructure" none "$(bash "$CLASSIFY" "$ctmp/done.log" "$ctmp/plain.ann")"
|
|
1826
|
+
assert "nothing to read classifies as nothing" none "$(bash "$CLASSIFY" "" "")"
|
|
1827
|
+
rm -rf "$ctmp"
|
|
1828
|
+
fi
|
|
1829
|
+
# The implement worker carried its own copy of the retry rule while four other workers used the
|
|
1830
|
+
# shared action, so a rule fixed in one place stayed broken in the other.
|
|
1831
|
+
IMPLEMENT_MD="${WORKFLOWS_DIR}/agent-implement.md"
|
|
1832
|
+
if [ -f "$IMPLEMENT_MD" ]; then
|
|
1833
|
+
uses=$(count -c "uses: ./.github/actions/decide-agent-retry" "$IMPLEMENT_MD")
|
|
1834
|
+
if [ "$uses" -ge 2 ] && ! grep -q 'minutes=$(( ( $(date -u -d "$finished"' "$IMPLEMENT_MD"; then
|
|
1835
|
+
PASS=$((PASS + 1))
|
|
1836
|
+
else
|
|
1837
|
+
FAIL=$((FAIL + 1))
|
|
1838
|
+
echo "FAIL: agent-implement.md must decide a retry through the shared decide-agent-retry action, in both incomplete and conclude" >&2
|
|
1839
|
+
fi
|
|
1840
|
+
fi
|
|
1841
|
+
# Implement runs spread over IMPLEMENT_PARALLELISM queues, but the children of one refine share
|
|
1842
|
+
# a queue keyed on their split parent: they build on each other's code. The queue step's
|
|
1843
|
+
# arithmetic is exercised with the same expression the router runs.
|
|
1844
|
+
if grep -q 'group: implement-queue-${{ needs.check-stack-branch.outputs.implement-queue' "$ROUTER_YML" &&
|
|
1845
|
+
sed -n '/- name: Pick the implement queue/,/- name: Resolve the feature branch/p' "$ROUTER_YML" | grep -q 'key="${parent:-$ISSUE_NUMBER}"'; then
|
|
1846
|
+
PASS=$((PASS + 1))
|
|
1847
|
+
else
|
|
1848
|
+
FAIL=$((FAIL + 1))
|
|
1849
|
+
echo "FAIL: call-implement must queue on check-stack-branch's implement-queue, keyed on the split parent" >&2
|
|
1850
|
+
fi
|
|
1851
|
+
queue_for() { local parent="$1" issue="$2" queues="$3"; case "$queues" in ''|*[!0-9]*|0) queues=1 ;; esac; local key="${parent:-$issue}"; echo $(( key % queues )); }
|
|
1852
|
+
assert "siblings of one refine share a queue" "$(queue_for 690 693 3)" "$(queue_for 690 694 3)"
|
|
1853
|
+
assert "parallelism 1 is one queue for everything" 0 "$(queue_for "" 722 1)"
|
|
1854
|
+
assert "an unset or garbled parallelism is one queue" 0 "$(queue_for "" 725 abc)"
|
|
1855
|
+
assert "independent issues spread across the queues" 1 "$(queue_for "" 724 3)"
|
|
1856
|
+
# The router queues implement runs FIFO, so a run waiting an hour behind the one in progress is
|
|
1857
|
+
# healthy. The janitor cancelling every pending run over an hour old took every issue queued
|
|
1858
|
+
# behind the first out of the queue and left it holding bot-working (Pliny-Bot #706, #644).
|
|
1859
|
+
if [ -f "${HERE}/../housekeeping/action.yml" ] && ! grep -q "liveKinds" "${HERE}/../housekeeping/action.yml"; then
|
|
1860
|
+
FAIL=$((FAIL + 1))
|
|
1861
|
+
echo "FAIL: the janitor cancels pending runs without checking for a live run of their own kind ahead of them" >&2
|
|
1862
|
+
else
|
|
1863
|
+
PASS=$((PASS + 1))
|
|
1864
|
+
fi
|
|
1865
|
+
# The implement worker names every phase skill it must load. Left as "load each phase skill",
|
|
1866
|
+
# glm-5-2 loaded pc-plan-goal, wrote proposal.md and tasks.md itself and edited the code alone:
|
|
1867
|
+
# a hand-written tasks.md has no <!-- agent --> annotations, so pc-plan-apply had no subagents
|
|
1868
|
+
# to spawn and the whole change ran on one model, in sequence (Pliny-Bot #693, #699).
|
|
1869
|
+
IMPL_MD="${WORKFLOWS_DIR}/agent-implement.md"
|
|
1870
|
+
if [ -f "$IMPL_MD" ]; then
|
|
1871
|
+
if grep -q 'You never write' "$IMPL_MD" && grep -q '`pc-plan-propose`, even when the issue is refined' "$IMPL_MD" &&
|
|
1872
|
+
grep -q 'In this phase you do not edit source files' "$IMPL_MD" && grep -q '`pc-repo-verify`, then `pc-plan-archive`' "$IMPL_MD"; then
|
|
1873
|
+
PASS=$((PASS + 1))
|
|
1874
|
+
else
|
|
1875
|
+
FAIL=$((FAIL + 1))
|
|
1876
|
+
echo "FAIL: agent-implement.md must name pc-plan-propose, pc-plan-apply, pc-repo-verify and pc-plan-archive and forbid writing tasks.md by hand" >&2
|
|
1877
|
+
fi
|
|
1878
|
+
fi
|
|
1879
|
+
# Refine writes in English and may retitle an issue written in another language; without
|
|
1880
|
+
# allow_title on update-issue the title in the call is dropped and the issue keeps its language.
|
|
1881
|
+
REFINE_MD="${WORKFLOWS_DIR}/agent-refine.md"
|
|
1882
|
+
if [ -f "$REFINE_MD" ]; then
|
|
1883
|
+
if sed -n '/^ update-issue:/,/^ add-comment:/p' "$REFINE_MD" | grep -q '^ title:' &&
|
|
1884
|
+
grep -q 'Everything you write is in English' "$REFINE_MD"; then
|
|
1885
|
+
PASS=$((PASS + 1))
|
|
1886
|
+
else
|
|
1887
|
+
FAIL=$((FAIL + 1))
|
|
1888
|
+
echo "FAIL: agent-refine.md must write in English and be allowed to retitle an issue (update-issue title:)" >&2
|
|
1889
|
+
fi
|
|
1890
|
+
fi
|
|
1891
|
+
# A token written to the runner's global git config outlives the job on a runner that is reused:
|
|
1892
|
+
# rules for revoked tokens pile up for the same prefix and git picks one at random, so the same
|
|
1893
|
+
# gate pushed for one pull request and was refused for the next (Pliny-Bot #716 and #717).
|
|
1894
|
+
PUSH_ACTION="${HERE}/../push-agent-branch/action.yml"
|
|
1895
|
+
if [ -f "$PUSH_ACTION" ] && grep -qE 'git config( --global)? url\.' "$PUSH_ACTION" && ! grep -qF 'git config --local url.' "$PUSH_ACTION"; then
|
|
1896
|
+
FAIL=$((FAIL + 1))
|
|
1897
|
+
echo "FAIL: push-agent-branch writes its token to the global git config, where stale rules from earlier jobs accumulate" >&2
|
|
1898
|
+
elif [ -f "$PUSH_ACTION" ] && grep -qE 'git config --global url\.' "$PUSH_ACTION"; then
|
|
1899
|
+
FAIL=$((FAIL + 1))
|
|
1900
|
+
echo "FAIL: push-agent-branch adds to the global git config; set the rule with --local" >&2
|
|
1901
|
+
else
|
|
1902
|
+
PASS=$((PASS + 1))
|
|
1903
|
+
fi
|
|
1904
|
+
# A conclude that dies leaves no verdict and no attempt comment unless `incomplete` records it,
|
|
1905
|
+
# and the train then reads the run as a gate that did nothing and never offers the pull request
|
|
1906
|
+
# again.
|
|
1907
|
+
if sed -n '/^ incomplete:/,/^ runs-on:/p' "$MERGE_GATE_WORKER_MD" | grep -q "needs.conclude.result == 'failure'"; then
|
|
1908
|
+
PASS=$((PASS + 1))
|
|
1909
|
+
else
|
|
1910
|
+
FAIL=$((FAIL + 1))
|
|
1911
|
+
echo "FAIL: the gate's incomplete job must record a failed conclude as an attempt, or the train strands the pull request" >&2
|
|
1912
|
+
fi
|
|
1913
|
+
# A stack child is dispatched on its feature branch, and a dispatched run reads repository secrets
|
|
1914
|
+
# by their own names, not through the router's FORGE_API_KEY mapping. Without the fallback every
|
|
1915
|
+
# stack child died at activation on "Validate CODEX_API_KEY or OPENAI_API_KEY secret".
|
|
1916
|
+
COMPILE_SCRIPT="${HERE}/../../scripts/compile-agent-workflows.mjs"
|
|
1917
|
+
if [ -f "$COMPILE_SCRIPT" ] && ! grep -qF "secrets.OPENAI_API_KEY || secrets.FORGE_API_KEY" "$COMPILE_SCRIPT"; then
|
|
1918
|
+
FAIL=$((FAIL + 1))
|
|
1919
|
+
echo "FAIL: the compile script no longer gives the engine key a FORGE_API_KEY fallback; dispatched stack children cannot start" >&2
|
|
1920
|
+
else
|
|
1921
|
+
PASS=$((PASS + 1))
|
|
1922
|
+
fi
|
|
1923
|
+
SUBJECT_YML="${HERE}/../identify-gate-subject/action.yml"
|
|
1924
|
+
if grep -qE 'does not close a numbered issue|does not carry' "$SUBJECT_YML" || grep -q 'require-label' "$SUBJECT_YML" "$MERGE_GATE_WORKER_MD"; then
|
|
1925
|
+
FAIL=$((FAIL + 1))
|
|
1926
|
+
echo "FAIL: the gate's subject job refuses a pull request for lacking an issue or a label; the train hands it back forever" >&2
|
|
1927
|
+
else
|
|
1928
|
+
PASS=$((PASS + 1))
|
|
1929
|
+
fi
|
|
1930
|
+
fi
|
|
1931
|
+
|
|
1774
1932
|
echo "── Merge gate park ───────────────────────────────────────────────────────"
|
|
1775
1933
|
|
|
1776
1934
|
# A gate verdict parks the code it was given on. The belt used to compare the standing verdict
|
|
@@ -1813,10 +1971,19 @@ if worker_installed merge-gate; then
|
|
|
1813
1971
|
assert "requested changes keep it off the train" skip "$(move REVIEW_DECISION=CHANGES_REQUESTED CI_STATUS=completed CI_CONCLUSION=success)"
|
|
1814
1972
|
assert "a parked pull request is passed by, not waited on" skip "$(move PARK=held CI_STATUS=completed CI_CONCLUSION=success)"
|
|
1815
1973
|
assert "a spent attempt budget is passed by" skip "$(move ATTEMPTS=6 MAX_ATTEMPTS=6 CI_STATUS=completed CI_CONCLUSION=failure)"
|
|
1974
|
+
# The loop that ran Pliny-Bot's gate twenty times on one pull request: the gate declined it
|
|
1975
|
+
# (its issue was closed), left no verdict and no attempt, and every tick handed it back.
|
|
1976
|
+
assert "a gate that ran and left no trace is not asked again" skip "$(move SILENT_GATE=true MERGEABLE=CONFLICTING CI_STATUS=none)"
|
|
1977
|
+
assert "a gate that ran and left no trace is not asked again, green or red" skip "$(move SILENT_GATE=true MERGEABLE=MERGEABLE CI_STATUS=completed CI_CONCLUSION=failure)"
|
|
1816
1978
|
assert "a conflict goes to the gate even with no CI" gate "$(move MERGEABLE=CONFLICTING CI_STATUS=none)"
|
|
1817
1979
|
assert "running CI is waited on" wait "$(move MERGEABLE=MERGEABLE CI_STATUS=in_progress)"
|
|
1818
1980
|
assert "CI stuck past the limit is passed by" skip "$(move MERGEABLE=MERGEABLE CI_STATUS=queued CI_STUCK=true)"
|
|
1819
1981
|
assert "red CI goes to the gate" gate "$(move MERGEABLE=MERGEABLE CI_STATUS=completed CI_CONCLUSION=failure)"
|
|
1982
|
+
# A red base branch cost every open Pliny-Bot pull request its repairs for seven hours: the
|
|
1983
|
+
# failures were main's, and the gate spent attempts on code that was not at fault.
|
|
1984
|
+
assert "a failure that is main's too is waited out, not repaired" wait "$(move MERGEABLE=MERGEABLE CI_STATUS=completed CI_CONCLUSION=failure SAME_AS_MAIN=true)"
|
|
1985
|
+
assert "a failure that is not main's still goes to the gate" gate "$(move MERGEABLE=MERGEABLE CI_STATUS=completed CI_CONCLUSION=failure SAME_AS_MAIN=false)"
|
|
1986
|
+
assert "a conflict is still the gate's even when main is red" gate "$(move MERGEABLE=CONFLICTING CI_STATUS=completed CI_CONCLUSION=failure SAME_AS_MAIN=true)"
|
|
1820
1987
|
assert "green but behind is brought up to date first" update "$(move MERGEABLE=MERGEABLE CI_STATUS=completed CI_CONCLUSION=success BEHIND_BY=2)"
|
|
1821
1988
|
assert "green and current goes to the gate" gate "$(move MERGEABLE=MERGEABLE CI_STATUS=completed CI_CONCLUSION=success BEHIND_BY=0)"
|
|
1822
1989
|
assert "no CI run is passed by" skip "$(move MERGEABLE=MERGEABLE CI_STATUS=none)"
|
|
@@ -2568,7 +2735,7 @@ if [ "$INPUT_WIRING_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)
|
|
|
2568
2735
|
BELT_QUEUE_OK=1
|
|
2569
2736
|
belt_caller=$(awk '/^ call-merge-gate:/ { found = 1; print; next } found && /^ [a-z]/ { exit } found { print }' "$ROUTER_YML")
|
|
2570
2737
|
printf '%s
|
|
2571
|
-
' "$belt_caller" | grep -qE
|
|
2738
|
+
' "$belt_caller" | grep -qE "^ *group: .*'merge-belt'" ||
|
|
2572
2739
|
{ BELT_QUEUE_OK=0; echo "FAIL: the merge gate is no longer serialised on the merge-belt concurrency group" >&2; }
|
|
2573
2740
|
printf '%s
|
|
2574
2741
|
' "$belt_caller" | grep -qE '^ *queue: max *$' ||
|
|
@@ -2596,6 +2763,11 @@ if worker_installed merge-gate; then
|
|
|
2596
2763
|
GATE_RUN_ID_OK=0
|
|
2597
2764
|
echo "FAIL: identify-gate-subject resolves the CI run only when the conclusion is unknown; a router that passes both gets an empty run ID" >&2
|
|
2598
2765
|
fi
|
|
2766
|
+
# A verdict from a run that judged an older head is not a verdict on this one (Pliny-Bot #729).
|
|
2767
|
+
grep -Fq '"$run_sha" != "$current_sha"' "$GATE_SUBJECT_ACTION" ||
|
|
2768
|
+
{ GATE_RUN_ID_OK=0; echo "FAIL: identify-gate-subject trusts the carried CI run even when it judged an older head" >&2; }
|
|
2769
|
+
grep -Fq 'select(.status == "completed")' "$GATE_SUBJECT_ACTION" ||
|
|
2770
|
+
{ GATE_RUN_ID_OK=0; echo "FAIL: identify-gate-subject re-reads a stale verdict without requiring a finished run on the head" >&2; }
|
|
2599
2771
|
fi
|
|
2600
2772
|
# Repeating a report the validator refused reproduces it, and every repeat is a fresh agent run
|
|
2601
2773
|
# holding the repo-wide merge-belt slot. The worker gives that failure a smaller budget than a
|
package/loops/docs/diagrams.md
CHANGED
|
@@ -94,7 +94,7 @@ flowchart LR
|
|
|
94
94
|
gate -->|merge| merged(("Merged"))
|
|
95
95
|
merged -->|"next PR"| train
|
|
96
96
|
gate -.->|"CI failed or conflict"| fix("Fix, push, CI again") --> ci
|
|
97
|
-
gate -.->|".github, secrets<br/>or
|
|
97
|
+
gate -.->|".github, secrets<br/>or 10000+ lines"| human(("Human review"))
|
|
98
98
|
review("Someone reviews the PR") --> applyReview("apply-review") --> ci
|
|
99
99
|
audit("audit, weekly") -->|"files one issue"| refine
|
|
100
100
|
|
|
@@ -381,7 +381,7 @@ flowchart TD
|
|
|
381
381
|
gateDisposition -->|"no verified blocker,<br/>radius low or recoverable medium"| gateRootCheck
|
|
382
382
|
gateRootCheck{"PR base is a<br/>stack feature branch<br/>and this PR is the root?"}
|
|
383
383
|
gateRootCheck -->|"child PR: no"| gateMerge
|
|
384
|
-
gateDisposition -.->|"
|
|
384
|
+
gateDisposition -.->|"10000+ lines, .github/<br/>or a secret"| gateOwner
|
|
385
385
|
gateDisposition -.->|"fragile, unmet criteria<br/>or low confidence"| gateHuman
|
|
386
386
|
gateDisposition -.->|"CI red or a verified<br/>high finding"| gateBlocked
|
|
387
387
|
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)"))
|
|
@@ -331,7 +331,19 @@ for (const file of readdirSync(workflowDirectory)) {
|
|
|
331
331
|
// runner rather than the run so a runner keeps its server warm between its own jobs.
|
|
332
332
|
// No double quotes here: the enclosing run: is a double-quoted YAML scalar, so a quote
|
|
333
333
|
// would have to be escaped. The default has no spaces, so bare is safe shell.
|
|
334
|
-
.replace(/\/tmp\/opencode-data(?!-\$\{\{)/g, () => '/tmp/opencode-data-${{ runner.name }}')
|
|
334
|
+
.replace(/\/tmp\/opencode-data(?!-\$\{\{)/g, () => '/tmp/opencode-data-${{ runner.name }}')
|
|
335
|
+
// The engine key falls back to FORGE_API_KEY, the one secret every repository here has. The
|
|
336
|
+
// router maps it to OPENAI_API_KEY when it CALLS a worker, but a stack child is DISPATCHED on
|
|
337
|
+
// its feature branch, and a dispatched run reads repository secrets by their own names: with
|
|
338
|
+
// no OPENAI_API_KEY or CODEX_API_KEY set, activation died on "Validate CODEX_API_KEY or
|
|
339
|
+
// OPENAI_API_KEY secret" and every stack child in Pliny-Bot failed that way for hours. gh-aw
|
|
340
|
+
// reads the key in four places, the validation step, the engine env and the redaction list
|
|
341
|
+
// included, and all of them must see the fallback or the key would run unredacted. The
|
|
342
|
+
// longest form goes first so the shorter ones do not match inside it.
|
|
343
|
+
.replaceAll('${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }}',
|
|
344
|
+
'${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY || secrets.FORGE_API_KEY }}')
|
|
345
|
+
.replaceAll('${{ secrets.CODEX_API_KEY }}', '${{ secrets.CODEX_API_KEY || secrets.FORGE_API_KEY }}')
|
|
346
|
+
.replaceAll('${{ secrets.OPENAI_API_KEY }}', '${{ secrets.OPENAI_API_KEY || secrets.FORGE_API_KEY }}');
|
|
335
347
|
|
|
336
348
|
let rewritten = patched;
|
|
337
349
|
// A queued implement executes hours after its run was created, but actions/checkout
|
|
@@ -340,8 +352,8 @@ for (const file of readdirSync(workflowDirectory)) {
|
|
|
340
352
|
// every later push became a rebase onto a moved parent, which gh-aw's signed-commit
|
|
341
353
|
// push cannot do (it dies with "cannot rebase: You have unstaged changes" and files the
|
|
342
354
|
// pull request as an issue instead of a pull request). Checking out the branch tip at
|
|
343
|
-
// execution time makes the commit parent current main; the implement
|
|
344
|
-
//
|
|
355
|
+
// execution time makes the commit parent current main; the implement queue, shared by the
|
|
356
|
+
// children of one refine, keeps sibling merges out of the window while the agent runs.
|
|
345
357
|
//
|
|
346
358
|
// A stack child dispatches this workflow directly (`gh workflow run
|
|
347
359
|
// agent-implement.lock.yml --ref feat/N`), and there the pin must select the feature
|
|
@@ -415,6 +427,11 @@ for (const file of readdirSync(workflowDirectory)) {
|
|
|
415
427
|
for (const check of [
|
|
416
428
|
{ name: "bundle upload glob", ok: /\n( +)- name: Upload agent artifacts\n\1 if: always\(\)\n\1 continue-on-error: true\n\1 uses: actions\/upload-artifact@[^\n]*\n\1 with:\n\1 name: \$\{\{ needs\.activation\.outputs\.artifact_prefix \}\}agent\n\1 path: \|\n(?:\1 [^\n]*\n)*\1 \/tmp\/gh-aw\/aw-\*\.bundle\n/ },
|
|
417
429
|
{ name: "placeholder step intact", ok: /\n( +)- name: Write agent output placeholder if missing\n\1 if: always\(\)\n\1 run: \|\n\1 if \[ ! -f \/tmp\/gh-aw\/agent_output\.json \]; then\n\1 echo '\{"items":\[\]\}' > \/tmp\/gh-aw\/agent_output\.json\n\1 fi\n/ },
|
|
430
|
+
// Every read of the engine key carries the FORGE_API_KEY fallback; see the rewrite above.
|
|
431
|
+
{
|
|
432
|
+
name: "engine key falls back to FORGE_API_KEY",
|
|
433
|
+
ok: lock => !lock.includes("${{ secrets.OPENAI_API_KEY }}") && !lock.includes("${{ secrets.CODEX_API_KEY }}"),
|
|
434
|
+
},
|
|
418
435
|
{ name: "failed-jobs reporter disabled", ok: /- name: Report failed jobs\n[ \t]*id: report_failed_jobs\n[ \t]*if: false[^\n]*\n/ },
|
|
419
436
|
// Only agent-refine declares update_issue; the other workers never carry the tool.
|
|
420
437
|
{
|
|
@@ -185,7 +185,7 @@ jobs:
|
|
|
185
185
|
fi
|
|
186
186
|
|
|
187
187
|
# A queued implement run can start long after it was dispatched. If triage concluded
|
|
188
|
-
# needs-maintainer while this run waited in
|
|
188
|
+
# needs-maintainer while this run waited in its implement queue, the issue now
|
|
189
189
|
# carries review and the verdict must stand: the reserve step clears review blindly,
|
|
190
190
|
# so eligibility has to catch it first (Pliny-Bot #372).
|
|
191
191
|
if jq -e 'index("review")' >/dev/null <<<"$labels"; then
|
|
@@ -245,6 +245,8 @@ jobs:
|
|
|
245
245
|
contents: read
|
|
246
246
|
issues: write
|
|
247
247
|
pull-requests: write
|
|
248
|
+
# reads the agent job's log and annotations, and re-enters through the router on a retry
|
|
249
|
+
actions: write
|
|
248
250
|
steps:
|
|
249
251
|
- name: Checkout workflow actions
|
|
250
252
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
@@ -433,8 +435,48 @@ jobs:
|
|
|
433
435
|
${{ env.IMPLEMENT_MARKER }}
|
|
434
436
|
${{ env.PATCH_WITHOUT_REQUEST_COMMENT }}
|
|
435
437
|
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
436
|
-
|
|
438
|
+
# A run that ends with nothing is not always the agent's verdict. The AI gateway can answer
|
|
439
|
+
# nothing and end the engine one turn after it began, and the runner's VM can be taken away
|
|
440
|
+
# mid-job; neither says the work was wrong, and parking them for the janitor cost up to six
|
|
441
|
+
# hours each (Pliny-Bot #693's first attempt). The shared rule repeats those, and a run that
|
|
442
|
+
# died young, within the attempt budget.
|
|
443
|
+
- name: Decide whether an empty run is worth repeating
|
|
444
|
+
id: empty_retry
|
|
437
445
|
if: needs.safe_outputs.outputs.created_pr_number == '' && !contains(needs.agent.outputs.output_types, 'create_pull_request') && !contains(needs.agent.outputs.output_types, 'noop') && needs.agent.outputs.has_patch != 'true'
|
|
446
|
+
uses: ./.github/actions/decide-agent-retry
|
|
447
|
+
with:
|
|
448
|
+
token: ${{ github.token }}
|
|
449
|
+
attempts-so-far: ${{ inputs.attempts_so_far || '0' }}
|
|
450
|
+
park-at: ${{ env.PARK_AT_ATTEMPT }}
|
|
451
|
+
under-minutes: ${{ env.RETRY_UNDER_MINUTES }}
|
|
452
|
+
- name: Report the retried empty run
|
|
453
|
+
if: steps.empty_retry.outputs.retry == 'true'
|
|
454
|
+
uses: ./.github/actions/create-issue-comment
|
|
455
|
+
with:
|
|
456
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
457
|
+
issue-number: ${{ inputs.issue-number }}
|
|
458
|
+
body: |
|
|
459
|
+
${{ env.ATTEMPT_MARKER }}
|
|
460
|
+
Attempt ${{ steps.empty_retry.outputs.next }} of ${{ env.MAX_ATTEMPTS }} ended with nothing after ${{ steps.empty_retry.outputs.minutes }} minutes (${{ steps.empty_retry.outputs.cause }}). That is what a provider or runner outage looks like, not a verdict on the work, so this is being retried.
|
|
461
|
+
The issue keeps `implement`.
|
|
462
|
+
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
463
|
+
- name: Send the issue back through the router
|
|
464
|
+
if: steps.empty_retry.outputs.retry == 'true'
|
|
465
|
+
env:
|
|
466
|
+
GH_TOKEN: ${{ github.token }}
|
|
467
|
+
REPO: ${{ github.repository }}
|
|
468
|
+
REF: ${{ github.event.repository.default_branch }}
|
|
469
|
+
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
470
|
+
NEXT: ${{ steps.empty_retry.outputs.next }}
|
|
471
|
+
run: |
|
|
472
|
+
set -euo pipefail
|
|
473
|
+
# The provider recovers in seconds, so pause before re-entering rather than dispatching
|
|
474
|
+
# back into the same outage.
|
|
475
|
+
sleep 30
|
|
476
|
+
gh workflow run work-router.yml --repo "$REPO" --ref "$REF" -f operation=implement -f issue-number="$ISSUE_NUMBER" -f attempts_so_far="$NEXT"
|
|
477
|
+
echo "Re-dispatched implement for #$ISSUE_NUMBER as attempt $NEXT."
|
|
478
|
+
- name: Flag a run that produced nothing
|
|
479
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && !contains(needs.agent.outputs.output_types, 'create_pull_request') && !contains(needs.agent.outputs.output_types, 'noop') && needs.agent.outputs.has_patch != 'true' && steps.empty_retry.outputs.retry != 'true'
|
|
438
480
|
uses: ./.github/actions/add-issue-labels
|
|
439
481
|
with:
|
|
440
482
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -443,7 +485,7 @@ jobs:
|
|
|
443
485
|
${{ env.REVIEW_LABEL }}
|
|
444
486
|
${{ env.STALLED_LABEL }}
|
|
445
487
|
- name: Say the run produced nothing
|
|
446
|
-
if: needs.safe_outputs.outputs.created_pr_number == '' && !contains(needs.agent.outputs.output_types, 'create_pull_request') && !contains(needs.agent.outputs.output_types, 'noop') && needs.agent.outputs.has_patch != 'true'
|
|
488
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && !contains(needs.agent.outputs.output_types, 'create_pull_request') && !contains(needs.agent.outputs.output_types, 'noop') && needs.agent.outputs.has_patch != 'true' && steps.empty_retry.outputs.retry != 'true'
|
|
447
489
|
uses: ./.github/actions/create-issue-comment
|
|
448
490
|
with:
|
|
449
491
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -478,36 +520,17 @@ jobs:
|
|
|
478
520
|
with:
|
|
479
521
|
client-id: ${{ secrets.BOT_APP_ID }}
|
|
480
522
|
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
523
|
+
# The shared rule, which this worker used to carry its own copy of. It repeats a run that died
|
|
524
|
+
# young, and now also one that was killed after a long time: the runner's VM taken away
|
|
525
|
+
# mid-job, or the AI gateway answering nothing and ending the engine one turn after it began.
|
|
481
526
|
- name: Decide whether this failure is worth repeating
|
|
482
527
|
id: decide
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
UNDER_MINUTES: ${{ env.RETRY_UNDER_MINUTES }}
|
|
490
|
-
run: |
|
|
491
|
-
set -euo pipefail
|
|
492
|
-
# The agent job belongs to this same run: a called workflow shares the caller's run id.
|
|
493
|
-
read -r started finished <<<"$(gh api "repos/$REPO/actions/runs/$RUN_ID/jobs?per_page=100" \
|
|
494
|
-
--jq '[.jobs[] | select(.name | endswith("agent"))] | last // empty
|
|
495
|
-
| "\(.started_at // "") \(.completed_at // "")"')"
|
|
496
|
-
minutes=-1
|
|
497
|
-
if [ -n "${started:-}" ] && [ -n "${finished:-}" ]; then
|
|
498
|
-
minutes=$(( ( $(date -u -d "$finished" +%s) - $(date -u -d "$started" +%s) ) / 60 ))
|
|
499
|
-
fi
|
|
500
|
-
retry=false
|
|
501
|
-
# An unknown duration is treated as a long run: never retry on a guess.
|
|
502
|
-
if [ "$minutes" -ge 0 ] && [ "$minutes" -lt "$UNDER_MINUTES" ] && [ "$ATTEMPTS" -lt "$PARK_AT" ]; then
|
|
503
|
-
retry=true
|
|
504
|
-
fi
|
|
505
|
-
{
|
|
506
|
-
echo "retry=$retry"
|
|
507
|
-
echo "next=$((ATTEMPTS + 1))"
|
|
508
|
-
echo "minutes=$minutes"
|
|
509
|
-
} >> "$GITHUB_OUTPUT"
|
|
510
|
-
echo "agent job ran for ${minutes}m; attempts so far ${ATTEMPTS}; retry=${retry}"
|
|
528
|
+
uses: ./.github/actions/decide-agent-retry
|
|
529
|
+
with:
|
|
530
|
+
token: ${{ github.token }}
|
|
531
|
+
attempts-so-far: ${{ inputs.attempts_so_far || '0' }}
|
|
532
|
+
park-at: ${{ env.PARK_AT_ATTEMPT }}
|
|
533
|
+
under-minutes: ${{ env.RETRY_UNDER_MINUTES }}
|
|
511
534
|
# The attempt is recorded before any label moves, so a failure in the steps below leaves a
|
|
512
535
|
# run that can be counted rather than an issue released with nothing to show for it.
|
|
513
536
|
- name: Report the failed attempt
|
|
@@ -518,7 +541,7 @@ jobs:
|
|
|
518
541
|
issue-number: ${{ inputs.issue-number }}
|
|
519
542
|
body: |
|
|
520
543
|
${{ env.ATTEMPT_MARKER }}
|
|
521
|
-
Attempt ${{ steps.decide.outputs.next }} of ${{ env.MAX_ATTEMPTS }} ended after ${{ steps.decide.outputs.minutes }} minutes
|
|
544
|
+
Attempt ${{ steps.decide.outputs.next }} of ${{ env.MAX_ATTEMPTS }} ended after ${{ steps.decide.outputs.minutes }} minutes without an answer (${{ steps.decide.outputs.cause }}). That is what a provider or runner outage looks like, not a verdict on the work, so this is being retried.
|
|
522
545
|
The issue keeps `implement`.
|
|
523
546
|
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
524
547
|
- name: Release the reservation for the retry
|
|
@@ -571,6 +594,7 @@ jobs:
|
|
|
571
594
|
body: |
|
|
572
595
|
${{ env.IMPLEMENT_MARKER }}
|
|
573
596
|
${{ env.INCOMPLETE_COMMENT }}
|
|
597
|
+
What ended: the agent job was **${{ needs.agent.result }}** after ${{ steps.decide.outputs.minutes }} minutes (-1 when that could not be read), and publishing its output was **${{ needs.safe_outputs.result }}**.
|
|
574
598
|
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
575
599
|
agent:
|
|
576
600
|
needs: [eligibility]
|
|
@@ -703,6 +727,28 @@ timeout-minutes: 180
|
|
|
703
727
|
phase skill it names, in order, and do not explore the code yourself where it says a phase
|
|
704
728
|
is skipped.
|
|
705
729
|
|
|
730
|
+
**The pipeline is these skill loads, in this order, each a call to the Skill tool.** A run
|
|
731
|
+
that edits source files without having made them has skipped the pipeline, whatever it
|
|
732
|
+
wrote instead:
|
|
733
|
+
|
|
734
|
+
1. `pc-plan-goal`, then Phase 0 as it describes.
|
|
735
|
+
2. `pc-plan-propose`, even when the issue is refined. It is what turns the issue into
|
|
736
|
+
`openspec/changes/<id>/tasks.md` with every task annotated
|
|
737
|
+
`<!-- agent: <engineer>.<tier>, depends_on: [...], touches: [...] -->`. **You never write
|
|
738
|
+
`proposal.md` or `tasks.md` yourself.** A hand-written `tasks.md` carries no annotations,
|
|
739
|
+
and without them `pc-plan-apply` has no workers to spawn, so the whole change is done by
|
|
740
|
+
you alone, in sequence, on one model. That is the failure this step exists to prevent.
|
|
741
|
+
3. `pc-plan-apply`. The tasks are implemented by the subagents it spawns, in waves, each on
|
|
742
|
+
the model tier its annotation names. **In this phase you do not edit source files
|
|
743
|
+
yourself.** If it stops because a worker cannot be resolved, report that with
|
|
744
|
+
`report_incomplete`, naming the task and the missing worker; do not do the task yourself
|
|
745
|
+
to get past it.
|
|
746
|
+
4. `pc-repo-verify`, then `pc-plan-archive`, as `pc-plan-goal` directs.
|
|
747
|
+
|
|
748
|
+
Before the first edit to a source file, check that
|
|
749
|
+
`openspec/changes/<id>/tasks.md` exists and that its tasks carry the annotation above. If it
|
|
750
|
+
does not, you have not run `pc-plan-propose`: load it now.
|
|
751
|
+
|
|
706
752
|
a. `branch` is the output mode this sandbox needs: the branch is kept, nothing is merged and
|
|
707
753
|
nothing is pushed. Without it the skill merges into the local default branch and deletes
|
|
708
754
|
the feature branch, and step 6 below then opens a pull request from a branch that is
|
|
@@ -37,11 +37,12 @@ env:
|
|
|
37
37
|
# infrastructure used to stop a merge from here; they are the review pass's to look at now, and
|
|
38
38
|
# a migration that rewrites data still parks through a `low` recoverability the agent evidences.
|
|
39
39
|
SENSITIVE_PATHS: '(^|/)([Dd]omain|entities|[Cc]ontracts)/|(^|/)([Aa]uth|[Aa]uthn|[Aa]uthz|[Aa]uthentication|[Aa]uthorization|[Ii]dentity|[Ss]ecurity|[Mm]igrations|[Ii]nfra|terraform|helm|k8s|deploy)(/|[A-Z][A-Za-z]*\.[a-z]+$)'
|
|
40
|
-
# Diff shape. Lines decide high: a change over
|
|
41
|
-
# translations and generated code excluded) is too big to merge unseen.
|
|
42
|
-
#
|
|
40
|
+
# Diff shape. Lines decide high: a change over ten thousand counted lines (tests, docs,
|
|
41
|
+
# translations and generated code excluded) is too big to merge unseen. It was a thousand, which
|
|
42
|
+
# is a normal feature once the tests are left out. A file count alone never decides, so the high
|
|
43
|
+
# file threshold is set out of reach; spread still raises medium.
|
|
43
44
|
BLAST_HIGH_FILES: "100000"
|
|
44
|
-
BLAST_HIGH_LINES: "
|
|
45
|
+
BLAST_HIGH_LINES: "10000"
|
|
45
46
|
BLAST_MEDIUM_FILES: "5"
|
|
46
47
|
BLAST_MEDIUM_LINES: "200"
|
|
47
48
|
# Paths that change no runtime behaviour: tests, docs, specs, translations and generated
|
|
@@ -182,7 +183,6 @@ jobs:
|
|
|
182
183
|
ci-conclusion: ${{ inputs.ci-conclusion }}
|
|
183
184
|
ci-run-id: ${{ inputs.ci-run-id }}
|
|
184
185
|
linked-issue: ${{ inputs.linked-issue }}
|
|
185
|
-
require-label: ${{ env.IMPLEMENT_LABEL }}
|
|
186
186
|
trusted-bots: ${{ env.TRUSTED_BOTS }}
|
|
187
187
|
- name: Block a pull request with requested changes
|
|
188
188
|
id: review
|
|
@@ -320,8 +320,13 @@ jobs:
|
|
|
320
320
|
echo "check-diff is not installed (agent-harness 3.2.0 or later ships it), so comments and tests were not checked." >> "$GITHUB_STEP_SUMMARY"
|
|
321
321
|
exit 0
|
|
322
322
|
fi
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
# `|| code=$?`, not a bare `code=$?` on the next line. GitHub runs this under `bash -e`,
|
|
324
|
+
# and `set -uo pipefail` above does not undo it, so a check-diff that exited 1 (the
|
|
325
|
+
# `fail` this step exists to report) ended the step there: the job failed, the agent
|
|
326
|
+
# was skipped, and the pull request burned its whole attempt budget on a verdict that
|
|
327
|
+
# was never recorded (Pliny-Bot #686, six attempts in twenty minutes).
|
|
328
|
+
code=0
|
|
329
|
+
report=$(node .opencode/scripts/check-diff.mjs --base "$BASE" 2>&1) || code=$?
|
|
325
330
|
case "$code" in 0) result=pass ;; 1) result=fail ;; *) result=error ;; esac
|
|
326
331
|
delimiter="EOF_$(openssl rand -hex 8)"
|
|
327
332
|
{
|
|
@@ -691,8 +696,12 @@ jobs:
|
|
|
691
696
|
${{ env.OWNER_REVIEW_LABEL }}
|
|
692
697
|
${{ env.BLOCKED_LABEL }}
|
|
693
698
|
${{ env.PR_PENDING_LABEL }}
|
|
699
|
+
# A conclude that dies (a push refused, a token rejected) is a failed attempt too. Without it
|
|
700
|
+
# here the run left no verdict and no attempt comment, which the merge train reads as a gate that
|
|
701
|
+
# did nothing and never offers the pull request again: Pliny-Bot #716 sat stranded that way after
|
|
702
|
+
# its repair was computed and then lost to a rejected git fetch in conclude.
|
|
694
703
|
incomplete:
|
|
695
|
-
needs: [subject, protected_changes, agent, safe_outputs, validate_output]
|
|
704
|
+
needs: [subject, protected_changes, agent, safe_outputs, validate_output, conclude]
|
|
696
705
|
if: >
|
|
697
706
|
always() &&
|
|
698
707
|
needs.subject.outputs.found == 'true' &&
|
|
@@ -700,7 +709,8 @@ jobs:
|
|
|
700
709
|
(
|
|
701
710
|
needs.agent.result != 'success' ||
|
|
702
711
|
needs.safe_outputs.result != 'success' ||
|
|
703
|
-
needs.validate_output.outputs.valid != 'true'
|
|
712
|
+
needs.validate_output.outputs.valid != 'true' ||
|
|
713
|
+
needs.conclude.result == 'failure'
|
|
704
714
|
)
|
|
705
715
|
runs-on: agents-arc
|
|
706
716
|
permissions:
|
|
@@ -837,9 +837,11 @@ safe-outputs:
|
|
|
837
837
|
# was sent with an incorrect parameter shape (20 bytes, missing body) and was accepted
|
|
838
838
|
# with success by the bridge, consuming the 1-per-run quota". The prompt still says to
|
|
839
839
|
# send the body once.
|
|
840
|
+
# `title:` lets refine set an English title when the issue was written in another language.
|
|
840
841
|
update-issue:
|
|
841
842
|
target: "*"
|
|
842
843
|
max: 3
|
|
844
|
+
title:
|
|
843
845
|
add-comment:
|
|
844
846
|
# Split children. An oversized story becomes several implementable ones rather than
|
|
845
847
|
# one issue nobody can land; the cap stops a runaway decomposition.
|
|
@@ -976,6 +978,13 @@ timeout-minutes: 90
|
|
|
976
978
|
The visible line is for people and the marker is read by the workflow, which turns it into the
|
|
977
979
|
`sp-N` label. A body without the marker gets no estimate label at all.
|
|
978
980
|
|
|
981
|
+
**Everything you write is in English, whatever language the issue was written in.** Bodies,
|
|
982
|
+
child issues, comments and questions are English. When the title of the issue you are refining
|
|
983
|
+
is not English, put an English title in the same `update_issue` call as the body, as
|
|
984
|
+
`{"issue_number":N,"title":"...","body":"..."}`: it carries the same meaning, short, with no
|
|
985
|
+
language tag. People write issues in their own language, and everything the workflow produces
|
|
986
|
+
from them (pull requests, commits, the run history) is read by everyone else in English.
|
|
987
|
+
|
|
979
988
|
7. **One safe-output call per turn.** Never batch multiple safe-output calls into a single
|
|
980
989
|
message: `update_issue`, `create_issue` and `add_comment` each go in their own turn, with
|
|
981
990
|
nothing else in the message. The provider caps one response at a fixed size, and a batch of
|