@plainconceptsplatform/workflows 1.5.0 → 1.6.10
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/route-processing.js +9 -2
- package/dist/workflow-catalog.js +1 -1
- package/loops/actions/apply-agent-output/action.yml +5 -0
- package/loops/actions/classify-route/classify-route.sh +34 -13
- 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 +52 -17
- package/loops/actions/merge-train/action.yml +71 -0
- package/loops/actions/merge-train/merge-train.sh +495 -0
- package/loops/actions/push-agent-branch/action.yml +73 -45
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +290 -84
- package/loops/docs/diagrams.md +55 -13
- package/loops/scripts/compile-agent-workflows.mjs +20 -3
- package/loops/templates/ci/app-ci-dotnet-next.yml +13 -14
- package/loops/templates/ci/app-ci-node-monorepo.yml +13 -14
- package/loops/workflows/agent-implement.md +77 -32
- package/loops/workflows/agent-merge-gate.md +90 -62
- package/loops/workflows/agent-refine.md +9 -0
- package/loops/workflows/work-router.yml +285 -471
- package/package.json +1 -1
|
@@ -203,20 +203,23 @@ assert_route "EVIDENCE_ENABLED=false turns the merge route off" none \
|
|
|
203
203
|
EVENT=pull_request_target ACTION=closed PR_MERGED=true EVENT_PR_NUMBER=7 EVIDENCE_ENABLED=false
|
|
204
204
|
|
|
205
205
|
echo "── CI completion ─────────────────────────────────────────────────────────"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
assert "
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
assert_route "a
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
assert_route "a
|
|
219
|
-
|
|
206
|
+
# A CI verdict moves the merge train, green or red; the train picks which pull request the gate
|
|
207
|
+
# takes. Dispatching the gate for the run's own pull request is how a night of them raced each
|
|
208
|
+
# other for the default branch.
|
|
209
|
+
assert_route "a failed App CI run on a pull request moves the merge train" merge-train EVENT=workflow_run RUN_PR_NUMBER=7 RUN_CONCLUSION=failure RUN_ID=99
|
|
210
|
+
assert_route "a green App CI run on a pull request moves the merge train" merge-train EVENT=workflow_run RUN_PR_NUMBER=7 RUN_CONCLUSION=success RUN_ID=99
|
|
211
|
+
assert "the train hears the CI conclusion" failure "$(route_field ci-conclusion EVENT=workflow_run RUN_PR_NUMBER=7 RUN_CONCLUSION=failure RUN_ID=99)"
|
|
212
|
+
assert "the train hears the CI run id" 99 "$(route_field ci-run-id EVENT=workflow_run RUN_PR_NUMBER=7 RUN_CONCLUSION=failure RUN_ID=99)"
|
|
213
|
+
assert_route "a cancelled App CI run moves nothing" none EVENT=workflow_run RUN_PR_NUMBER=7 RUN_CONCLUSION=cancelled RUN_ID=99
|
|
214
|
+
assert_route "a failed CI run with no pull request routes nowhere" none EVENT=workflow_run RUN_PR_NUMBER= RUN_CONCLUSION=failure RUN_ID=99
|
|
215
|
+
# A consumer CI hands its verdict over with GITHUB_TOKEN. Old CIs dispatch merge-gate; that is a
|
|
216
|
+
# CI completion, so it moves the train instead of jumping the queue. A person or the train itself
|
|
217
|
+
# dispatching merge-gate still reaches the gate.
|
|
218
|
+
assert_route "a consumer CI's merge-gate handover moves the train" merge-train EVENT=workflow_dispatch OPERATION=merge-gate INPUT_PR_NUMBER=7 ACTOR='github-actions[bot]'
|
|
219
|
+
assert "the handover carries its verdict to the train" success "$(route_field ci-conclusion EVENT=workflow_dispatch OPERATION=merge-gate INPUT_PR_NUMBER=7 ACTOR='github-actions[bot]' INPUT_CI_CONCLUSION=success INPUT_CI_RUN_ID=5)"
|
|
220
|
+
assert_route "the train's own gate dispatch reaches the gate" merge-gate EVENT=workflow_dispatch OPERATION=merge-gate INPUT_PR_NUMBER=7 ACTOR='platform-devbox[bot]'
|
|
221
|
+
assert_route "merge-train dispatch needs no pull request" merge-train EVENT=workflow_dispatch OPERATION=merge-train
|
|
222
|
+
assert "a merge-train handover carries its run id" 5 "$(route_field ci-run-id EVENT=workflow_dispatch OPERATION=merge-train INPUT_PR_NUMBER=7 INPUT_CI_CONCLUSION=failure INPUT_CI_RUN_ID=5)"
|
|
220
223
|
|
|
221
224
|
echo "── Schedules ─────────────────────────────────────────────────────────────"
|
|
222
225
|
while read -r cron; do
|
|
@@ -490,6 +493,14 @@ if [ "$BELT_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
|
490
493
|
# trailing space, so a number at the end of a body is still followed by a non-digit, and the
|
|
491
494
|
# pattern matches `[^0-9]`. Four quoting layers cannot eat a character class.
|
|
492
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
|
|
493
504
|
if ! grep -qF 'test(\"#${issue}[^0-9]\")' "$ROUTER_YML"; then
|
|
494
505
|
SWEEP_OK=0
|
|
495
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
|
|
@@ -685,69 +696,90 @@ if worker_installed merge-gate; then
|
|
|
685
696
|
# The merge belt is serial for the whole repository: several overnight pull requests
|
|
686
697
|
# mean every merge moves the default branch under the rest, and gates running at once
|
|
687
698
|
# rebase onto bases other gates are about to invalidate. A per-issue group here would
|
|
688
|
-
# reintroduce that race, so assert the repo-wide lock is the one in use.
|
|
689
|
-
|
|
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
|
|
690
704
|
PASS=$((PASS + 1))
|
|
691
705
|
else
|
|
692
706
|
FAIL=$((FAIL + 1))
|
|
693
|
-
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
|
|
694
708
|
fi
|
|
695
709
|
fi
|
|
696
710
|
|
|
697
|
-
#
|
|
698
|
-
#
|
|
699
|
-
#
|
|
700
|
-
#
|
|
701
|
-
#
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
#
|
|
718
|
-
#
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
#
|
|
727
|
-
#
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
#
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
711
|
+
# The belt is the merge train now: one gate at a time, oldest pull request first. Its I/O lives in
|
|
712
|
+
# the merge-train action and every rule it learned as the belt is asserted there.
|
|
713
|
+
#
|
|
714
|
+
# A verdict is the gate marker AND a `**Verdict:**` line together. Comments carrying the marker
|
|
715
|
+
# alone were progress notes and failed attempts, and the belt read every one of them as final: a
|
|
716
|
+
# crashed or OOM-killed gate parked its pull request for the rest of the night. Attempts are
|
|
717
|
+
# counted separately and capped.
|
|
718
|
+
TRAIN_SH="${HERE}/../merge-train/merge-train.sh"
|
|
719
|
+
if worker_installed merge-gate; then
|
|
720
|
+
BELT_OK=1
|
|
721
|
+
if [ ! -f "$TRAIN_SH" ]; then
|
|
722
|
+
BELT_OK=0; echo "FAIL: the merge gate is installed without the merge-train action" >&2
|
|
723
|
+
else
|
|
724
|
+
grep -q 'agent-merge-gate-attempt' "$TRAIN_SH" ||
|
|
725
|
+
{ BELT_OK=0; echo "FAIL: the merge train never counts gate attempts" >&2; }
|
|
726
|
+
grep -qF 'contains("<!-- agent-merge-gate -->")) and (.body | test("Verdict:' "$TRAIN_SH" ||
|
|
727
|
+
{ BELT_OK=0; echo "FAIL: the train's verdict detection must pair the gate marker with a Verdict line" >&2; }
|
|
728
|
+
grep -qF 'inputs[attempts_so_far]=' "$TRAIN_SH" ||
|
|
729
|
+
{ BELT_OK=0; echo "FAIL: the train must forward attempts_so_far to the gate" >&2; }
|
|
730
|
+
# One gate at a time, found by the title run-name gives it. The belt looked for a title that
|
|
731
|
+
# run-name had stopped producing, saw no live gate ever, and dispatched #680 five times in
|
|
732
|
+
# an hour onto a belt that runs one gate at a time.
|
|
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; }
|
|
736
|
+
if [ -z "$gate_title" ] || ! grep -qF "GATE_TITLE_PREFIX=\"${gate_title}\"" "$TRAIN_SH"; then
|
|
737
|
+
BELT_OK=0
|
|
738
|
+
echo "FAIL: the train looks for live gates titled differently from what run-name names them ('${gate_title:-unread}')" >&2
|
|
739
|
+
fi
|
|
740
|
+
# A conflicting pull request has no refs/pull/N/merge, so CI can never run on its head; the
|
|
741
|
+
# train hands it to the gate on the branch's last verdict rather than waiting forever.
|
|
742
|
+
grep -qF 'actions/runs?branch=$head_ref' "$TRAIN_SH" ||
|
|
743
|
+
{ BELT_OK=0; echo "FAIL: the train must gate a conflicting pull request on its branch's last verdict" >&2; }
|
|
744
|
+
# GitHub computes mergeability on demand and the first read answers UNKNOWN.
|
|
745
|
+
grep -q 'mergeable=$(mergeable_state' "$TRAIN_SH" ||
|
|
746
|
+
{ BELT_OK=0; echo "FAIL: the train must poll the mergeable state; a single read answers UNKNOWN" >&2; }
|
|
747
|
+
# The park is keyed to the head commit, never the CI time: a CI re-run must not reopen a park
|
|
748
|
+
# a person owns.
|
|
749
|
+
grep -qF '[ "$verdict_at" \> "$head_committed" ]' "$TRAIN_SH" ||
|
|
750
|
+
{ BELT_OK=0; echo "FAIL: the train must key a verdict's park to the head commit" >&2; }
|
|
751
|
+
fi
|
|
752
|
+
# The App token: a dispatch or a branch update made with GITHUB_TOKEN starts no workflow.
|
|
753
|
+
train_job="$(sed -n '/^ merge-train:$/,/^ [a-z-]*:$/p' "$ROUTER_YML")"
|
|
754
|
+
if ! grep -q 'token: ${{ steps.app-token.outputs.token }}' <<<"$train_job" ||
|
|
755
|
+
! grep -q 'group: merge-train' <<<"$train_job" ||
|
|
756
|
+
! grep -q 'needs: \[classify, call-merge-gate\]' <<<"$train_job"; then
|
|
757
|
+
BELT_OK=0
|
|
758
|
+
echo "FAIL: the merge-train job must use the App token, hold the merge-train lock and tick after the gate" >&2
|
|
759
|
+
fi
|
|
760
|
+
if [ "$BELT_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
737
761
|
fi
|
|
738
|
-
if [ "$BELT_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
739
762
|
|
|
740
|
-
# GitHub delivers workflow_run only for CI runs whose actor is a human, so a bot pull
|
|
741
|
-
# CI never reaches the router's CI-completion route. The package ships a dispatch
|
|
742
|
-
#
|
|
743
|
-
#
|
|
763
|
+
# GitHub delivers workflow_run only for CI runs whose actor is a human, so a bot-updated pull
|
|
764
|
+
# request's CI never reaches the router's CI-completion route. The package ships a dispatch job in
|
|
765
|
+
# templates/ci that hands the verdict over from inside CI; a consumer CI workflow, where one exists
|
|
766
|
+
# beside the router, must carry it or those pull requests wait for the hourly tick.
|
|
744
767
|
for ci in "${WORKFLOWS_DIR}/ci.yml" "${WORKFLOWS_DIR}/app-ci.yml"; do
|
|
745
768
|
[ -f "$ci" ] || continue
|
|
746
|
-
if grep -
|
|
769
|
+
if grep -qE 'operation=merge-(gate|train)' "$ci"; then
|
|
747
770
|
PASS=$((PASS + 1))
|
|
748
771
|
else
|
|
749
772
|
FAIL=$((FAIL + 1))
|
|
750
|
-
echo "FAIL: $(basename "$ci")
|
|
773
|
+
echo "FAIL: $(basename "$ci") hands no CI verdict to the merge train; pull requests would wait for the hourly tick" >&2
|
|
774
|
+
fi
|
|
775
|
+
done
|
|
776
|
+
for template in "${HERE}/../../templates/ci/"*.yml; do
|
|
777
|
+
[ -f "$template" ] || continue
|
|
778
|
+
if grep -q 'operation=merge-train' "$template" && ! grep -q "pull_request.user.type == 'Bot'" "$template"; then
|
|
779
|
+
PASS=$((PASS + 1))
|
|
780
|
+
else
|
|
781
|
+
FAIL=$((FAIL + 1))
|
|
782
|
+
echo "FAIL: $(basename "$template") must hand every same-repository pull request's verdict to the merge train" >&2
|
|
751
783
|
fi
|
|
752
784
|
done
|
|
753
785
|
|
|
@@ -1110,7 +1142,7 @@ done < <(sed -n '/^ operation:/,/^ issue-number:/p' "$ROUTER_YML" |
|
|
|
1110
1142
|
# contents and issues and failed nightly on a 403 that named the endpoint and nothing else.
|
|
1111
1143
|
# Paired job-to-scope rather than parsed out of each action: the jobs that touch pull
|
|
1112
1144
|
# requests are few and known, and naming them here is what makes the omission visible.
|
|
1113
|
-
for pr_job in audit-close reconcile-bot-pr-runs
|
|
1145
|
+
for pr_job in audit-close reconcile-bot-pr-runs merge-train land-stack-child housekeeping; do
|
|
1114
1146
|
if ! grep -q "^ ${pr_job}:$" "$ROUTER_YML"; then
|
|
1115
1147
|
continue
|
|
1116
1148
|
fi
|
|
@@ -1752,19 +1784,165 @@ if [ -f "$GATE_VALIDATOR" ] && worker_installed merge-gate; then
|
|
|
1752
1784
|
if [ "$VALIDATOR_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
1753
1785
|
fi
|
|
1754
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
|
+
|
|
1755
1932
|
echo "── Merge gate park ───────────────────────────────────────────────────────"
|
|
1756
1933
|
|
|
1757
|
-
# A gate verdict parks the code it was given on.
|
|
1758
|
-
# the
|
|
1759
|
-
#
|
|
1760
|
-
#
|
|
1761
|
-
#
|
|
1762
|
-
#
|
|
1934
|
+
# A gate verdict parks the code it was given on. The belt used to compare the standing verdict
|
|
1935
|
+
# against the CI finish time, which made the park worthless: any later run on the same commits was
|
|
1936
|
+
# newer than the verdict, so it re-dispatched a pull request a human already owned. Lyceum PR #13
|
|
1937
|
+
# sat parked for six days while that happened. The opposite failure is what parked Pliny-Bot for
|
|
1938
|
+
# whole weekends: a park that never expired, so a pull request blocked on Friday night was still
|
|
1939
|
+
# blocked on Monday though the default branch had moved under it a dozen times. The train's two
|
|
1940
|
+
# pure functions decide both, and they are exercised here rather than read.
|
|
1763
1941
|
if worker_installed merge-gate; then
|
|
1764
1942
|
GATE_OK=1
|
|
1765
1943
|
|
|
1766
|
-
#
|
|
1767
|
-
stale_horizon=$(count -cE 'verdict"
|
|
1944
|
+
# Nothing may key the park to CI timing again.
|
|
1945
|
+
stale_horizon=$(count -cE 'verdict" \> "\$ci_finished"|latest_verdict" \> "\$ci_finished"' "$ROUTER_YML")
|
|
1768
1946
|
if [ "$stale_horizon" -eq 0 ]; then
|
|
1769
1947
|
PASS=$((PASS + 1))
|
|
1770
1948
|
else
|
|
@@ -1772,13 +1950,41 @@ if worker_installed merge-gate; then
|
|
|
1772
1950
|
echo "FAIL: the merge-gate park is keyed to the CI finish time in ${stale_horizon} place(s); a CI re-run would reopen a park a person owns" >&2
|
|
1773
1951
|
fi
|
|
1774
1952
|
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1953
|
+
if [ -f "$TRAIN_SH" ]; then
|
|
1954
|
+
# Each call in its own shell: the facts are environment variables, and one case's must not
|
|
1955
|
+
# leak into the next.
|
|
1956
|
+
park() { env "$@" bash -c "source '$TRAIN_SH'; train_park"; }
|
|
1957
|
+
move() { env "$@" bash -c "source '$TRAIN_SH'; train_move" | sed -n 's/^move=//p'; }
|
|
1958
|
+
|
|
1959
|
+
assert "no verdict leaves the pull request to the train" none "$(park VERDICT= VERDICT_NEWER=false)"
|
|
1960
|
+
assert "a verdict older than the head is spent" none "$(park VERDICT=owner-review VERDICT_NEWER=false)"
|
|
1961
|
+
assert "owner-review on this head waits for its owner" held "$(park VERDICT=owner-review VERDICT_NEWER=true BASE_MOVED=true)"
|
|
1962
|
+
assert "human-review on this head waits for a person" held "$(park VERDICT=human-review VERDICT_NEWER=true BASE_MOVED=true)"
|
|
1963
|
+
assert "remediated never parks: the gate wants the next CI" none "$(park VERDICT=remediated VERDICT_NEWER=true)"
|
|
1964
|
+
assert "blocked holds while the base has not moved" held "$(park VERDICT=blocked VERDICT_NEWER=true BASE_MOVED=false RETRIES=1)"
|
|
1965
|
+
assert "blocked is retried once the base moves" none "$(park VERDICT=blocked VERDICT_NEWER=true BASE_MOVED=true RETRIES=1 MAX_RETRIES=3)"
|
|
1966
|
+
assert "blocked holds once its retries are spent" held "$(park VERDICT=blocked VERDICT_NEWER=true BASE_MOVED=true RETRIES=3 MAX_RETRIES=3)"
|
|
1967
|
+
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)"
|
|
1968
|
+
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)"
|
|
1969
|
+
|
|
1970
|
+
assert "hold keeps a pull request off the train" skip "$(move HOLD=true CI_STATUS=completed CI_CONCLUSION=success)"
|
|
1971
|
+
assert "requested changes keep it off the train" skip "$(move REVIEW_DECISION=CHANGES_REQUESTED CI_STATUS=completed CI_CONCLUSION=success)"
|
|
1972
|
+
assert "a parked pull request is passed by, not waited on" skip "$(move PARK=held CI_STATUS=completed CI_CONCLUSION=success)"
|
|
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)"
|
|
1978
|
+
assert "a conflict goes to the gate even with no CI" gate "$(move MERGEABLE=CONFLICTING CI_STATUS=none)"
|
|
1979
|
+
assert "running CI is waited on" wait "$(move MERGEABLE=MERGEABLE CI_STATUS=in_progress)"
|
|
1980
|
+
assert "CI stuck past the limit is passed by" skip "$(move MERGEABLE=MERGEABLE CI_STATUS=queued CI_STUCK=true)"
|
|
1981
|
+
assert "red CI goes to the gate" gate "$(move MERGEABLE=MERGEABLE CI_STATUS=completed CI_CONCLUSION=failure)"
|
|
1982
|
+
assert "green but behind is brought up to date first" update "$(move MERGEABLE=MERGEABLE CI_STATUS=completed CI_CONCLUSION=success BEHIND_BY=2)"
|
|
1983
|
+
assert "green and current goes to the gate" gate "$(move MERGEABLE=MERGEABLE CI_STATUS=completed CI_CONCLUSION=success BEHIND_BY=0)"
|
|
1984
|
+
assert "no CI run is passed by" skip "$(move MERGEABLE=MERGEABLE CI_STATUS=none)"
|
|
1985
|
+
assert "a trusted bot's CI awaiting approval is approved" approve "$(move CI_STATUS=completed CI_CONCLUSION=action_required TRUSTED_AUTHOR=true)"
|
|
1986
|
+
assert "a person's CI awaiting approval is not approved by the train" skip "$(move CI_STATUS=completed CI_CONCLUSION=action_required TRUSTED_AUTHOR=false)"
|
|
1987
|
+
assert "a cancelled run is no verdict" skip "$(move MERGEABLE=MERGEABLE CI_STATUS=completed CI_CONCLUSION=cancelled)"
|
|
1782
1988
|
fi
|
|
1783
1989
|
|
|
1784
1990
|
# One cap, not four literals, and it has to match what the worker tells the reader.
|
|
@@ -2518,13 +2724,13 @@ for worker_md in "${WORKFLOWS_DIR}"/agent-*.md; do
|
|
|
2518
2724
|
done
|
|
2519
2725
|
if [ "$INPUT_WIRING_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
2520
2726
|
|
|
2521
|
-
#
|
|
2522
|
-
#
|
|
2727
|
+
# The train dispatches one gate at a time, but a person, a merge-gate label and an old consumer CI
|
|
2728
|
+
# can still dispatch one beside it. Without `queue: max` GitHub keeps one pending run in the group and cancels the
|
|
2523
2729
|
# rest with no comment and no recorded attempt; every dispatch still returns 204.
|
|
2524
2730
|
BELT_QUEUE_OK=1
|
|
2525
2731
|
belt_caller=$(awk '/^ call-merge-gate:/ { found = 1; print; next } found && /^ [a-z]/ { exit } found { print }' "$ROUTER_YML")
|
|
2526
2732
|
printf '%s
|
|
2527
|
-
' "$belt_caller" | grep -qE
|
|
2733
|
+
' "$belt_caller" | grep -qE "^ *group: .*'merge-belt'" ||
|
|
2528
2734
|
{ BELT_QUEUE_OK=0; echo "FAIL: the merge gate is no longer serialised on the merge-belt concurrency group" >&2; }
|
|
2529
2735
|
printf '%s
|
|
2530
2736
|
' "$belt_caller" | grep -qE '^ *queue: max *$' ||
|
|
@@ -2573,7 +2779,7 @@ if worker_installed merge-gate; then
|
|
|
2573
2779
|
echo "FAIL: the incomplete job must read one computed budget decision, not re-derive it" >&2
|
|
2574
2780
|
fi
|
|
2575
2781
|
# The park has to be a verdict or the belt keeps dispatching: a comment carrying the gate
|
|
2576
|
-
# marker AND a Verdict line is what
|
|
2782
|
+
# marker AND a Verdict line is what the merge train parks on.
|
|
2577
2783
|
if ! grep -A 16 "Record an unusable report as a decision" "$MERGE_GATE_WORKER_MD" |
|
|
2578
2784
|
grep -q '\${{ env.GATE_MARKER }}' ||
|
|
2579
2785
|
! grep -A 16 "Record an unusable report as a decision" "$MERGE_GATE_WORKER_MD" |
|
package/loops/docs/diagrams.md
CHANGED
|
@@ -57,7 +57,7 @@ flowchart TD
|
|
|
57
57
|
wTriage("agent-triage")
|
|
58
58
|
classify -.->|"cron or dispatch"| plumbing
|
|
59
59
|
plumbing["deterministic jobs<br/>no model runs in any of these"] --> pJobs
|
|
60
|
-
pJobs("bot-approve · audit-close<br/>cleanup-artifacts · validate<br/>reconcile-bot-pr-runs ·
|
|
60
|
+
pJobs("bot-approve · audit-close<br/>cleanup-artifacts · validate<br/>reconcile-bot-pr-runs · merge-train · land-stack-child<br/>check-stack-branch · dispatch-implement")
|
|
61
61
|
idle(("Idle<br/>run ends, ~10s, every job skipped"))
|
|
62
62
|
|
|
63
63
|
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
|
|
@@ -87,10 +87,14 @@ flowchart LR
|
|
|
87
87
|
refine -->|"8 or more"| split("Split into children") --> refine
|
|
88
88
|
refine -.->|questions| author
|
|
89
89
|
implement("implement<br/>branch, PR, closes the issue") --> ci("CI")
|
|
90
|
-
|
|
90
|
+
person("A person opens a PR") --> ci
|
|
91
|
+
ci --> train{"merge train<br/>oldest PR first,<br/>one at a time"}
|
|
92
|
+
train -.->|"green, behind main"| update("Merge main in") --> ci
|
|
93
|
+
train --> gate{"merge-gate"}
|
|
91
94
|
gate -->|merge| merged(("Merged"))
|
|
92
|
-
|
|
93
|
-
gate -.->|"
|
|
95
|
+
merged -->|"next PR"| train
|
|
96
|
+
gate -.->|"CI failed or conflict"| fix("Fix, push, CI again") --> ci
|
|
97
|
+
gate -.->|".github, secrets<br/>or 10000+ lines"| human(("Human review"))
|
|
94
98
|
review("Someone reviews the PR") --> applyReview("apply-review") --> ci
|
|
95
99
|
audit("audit, weekly") -->|"files one issue"| refine
|
|
96
100
|
|
|
@@ -99,9 +103,9 @@ flowchart LR
|
|
|
99
103
|
classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
|
|
100
104
|
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
|
|
101
105
|
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
|
|
102
|
-
class open,write,review,audit start
|
|
103
|
-
class refine,split,implement,ci,fix,applyReview,author action
|
|
104
|
-
class triage,gate decision
|
|
106
|
+
class open,write,review,audit,person start
|
|
107
|
+
class refine,split,implement,ci,fix,applyReview,author,update action
|
|
108
|
+
class triage,gate,train decision
|
|
105
109
|
class human,closed failure
|
|
106
110
|
class merged success
|
|
107
111
|
```
|
|
@@ -110,6 +114,44 @@ Audit creates work rather than consuming it, and files into `refine` rather than
|
|
|
110
114
|
`implement`, so a report of several unrelated findings becomes one properly sized issue per finding
|
|
111
115
|
instead of one pull request that has to fix them all.
|
|
112
116
|
|
|
117
|
+
### The merge train
|
|
118
|
+
|
|
119
|
+
One pull request moves at a time, the lowest number first, whoever opened it. Everything else waits
|
|
120
|
+
untouched, so a merge never invalidates work in flight. A pull request a person has to decide on is
|
|
121
|
+
passed by, so it never blocks the line. The train ticks on every merge, every CI completion, after
|
|
122
|
+
every gate, and hourly as the fallback.
|
|
123
|
+
|
|
124
|
+
```mermaid
|
|
125
|
+
flowchart TD
|
|
126
|
+
trainTick("A merge, a CI verdict,<br/>a finished gate, or the hour") --> trainLive
|
|
127
|
+
trainLive{"A gate already<br/>on the belt?"}
|
|
128
|
+
trainLive -.->|yes| trainWait
|
|
129
|
+
trainLive -->|no| trainNext
|
|
130
|
+
trainNext{"Oldest open PR from this repo,<br/>bot or writer, not draft,<br/>no hold label"}
|
|
131
|
+
trainNext -.->|"none left"| trainIdle
|
|
132
|
+
trainNext --> trainPark
|
|
133
|
+
trainPark{"Parked on this head?<br/>owner or human review,<br/>blocked with main unmoved,<br/>attempts spent"}
|
|
134
|
+
trainPark -.->|"yes: pass it by"| trainNext
|
|
135
|
+
trainPark -->|no| trainCi
|
|
136
|
+
trainCi{"Its CI"}
|
|
137
|
+
trainCi -.->|running| trainWait
|
|
138
|
+
trainCi -->|"green, behind main"| trainUpdate
|
|
139
|
+
trainCi -->|"red, conflicting,<br/>or green and current"| trainGate
|
|
140
|
+
trainUpdate(("Merge main in<br/>CI reruns on what it<br/>will land on"))
|
|
141
|
+
trainGate(("Dispatch the gate<br/>repair or squash-merge"))
|
|
142
|
+
trainWait(("Wait<br/>the next tick looks again"))
|
|
143
|
+
trainIdle(("Idle<br/>the train is empty"))
|
|
144
|
+
|
|
145
|
+
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
|
|
146
|
+
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
|
|
147
|
+
classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
|
|
148
|
+
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
|
|
149
|
+
class trainTick start
|
|
150
|
+
class trainLive,trainNext,trainPark,trainCi decision
|
|
151
|
+
class trainWait,trainIdle idle
|
|
152
|
+
class trainUpdate,trainGate success
|
|
153
|
+
```
|
|
154
|
+
|
|
113
155
|
### How a feature stacks
|
|
114
156
|
|
|
115
157
|
When refine splits a story that carries real sequencing — a feature whose children build on each
|
|
@@ -319,8 +361,8 @@ overnight pull requests mean every merge moves the default branch under the rest
|
|
|
319
361
|
|
|
320
362
|
```mermaid
|
|
321
363
|
flowchart TD
|
|
322
|
-
gateStart("Work Router<br/>merge-gate route<br/>
|
|
323
|
-
gateSubject{"subject (rung 4)<br/>
|
|
364
|
+
gateStart("Work Router<br/>merge-gate route<br/>the merge train dispatched it,<br/>oldest PR first") --> gateSubject
|
|
365
|
+
gateSubject{"subject (rung 4)<br/>Open PR from this repo,<br/>by the bot or a writer?"}
|
|
324
366
|
gateSubject -.->|no| gateIdle
|
|
325
367
|
gateSubject -->|yes| gateBranch
|
|
326
368
|
gateBranch("Check out the PR branch<br/>the push is fast-forward only,<br/>so never rebase") --> gateFacts
|
|
@@ -332,14 +374,14 @@ flowchart TD
|
|
|
332
374
|
gatePush(("Pushed<br/>CI runs again, gate re-enters"))
|
|
333
375
|
gateFix -.->|"cannot fix"| gateHuman
|
|
334
376
|
gateProtected{"protected_changes (rung 4b)<br/>blast radius from paths, CODEOWNERS<br/>and the size of the code that ships"}
|
|
335
|
-
gateProtected -.->|"
|
|
377
|
+
gateProtected -.->|".github/ path"| gateOwner
|
|
336
378
|
gateProtected -->|"measured low, medium or high"| gateAssess
|
|
337
379
|
gateAssess("Agent (rung 5)<br/>find defects, verify them,<br/>rate recoverability") --> gateDisposition
|
|
338
380
|
gateDisposition{"Disposition<br/>computed in shell from<br/>the facts and the report"}
|
|
339
381
|
gateDisposition -->|"no verified blocker,<br/>radius low or recoverable medium"| gateRootCheck
|
|
340
382
|
gateRootCheck{"PR base is a<br/>stack feature branch<br/>and this PR is the root?"}
|
|
341
383
|
gateRootCheck -->|"child PR: no"| gateMerge
|
|
342
|
-
gateDisposition -.->|"
|
|
384
|
+
gateDisposition -.->|"10000+ lines, .github/<br/>or a secret"| gateOwner
|
|
343
385
|
gateDisposition -.->|"fragile, unmet criteria<br/>or low confidence"| gateHuman
|
|
344
386
|
gateDisposition -.->|"CI red or a verified<br/>high finding"| gateBlocked
|
|
345
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)"))
|
|
@@ -350,8 +392,8 @@ flowchart TD
|
|
|
350
392
|
gateOwner --> gatePreview
|
|
351
393
|
gatePreview("operation=evidence<br/>screenshots of the head<br/>before the person decides") --> gateApproved
|
|
352
394
|
gateApproved(("Approved by a writer<br/>merge-approved merges<br/>the approved commit"))
|
|
353
|
-
gateBlocked(("BLOCKED<br/>review + blocked
|
|
354
|
-
gateIdle(("Idle<br/>
|
|
395
|
+
gateBlocked(("BLOCKED<br/>review + blocked, the train<br/>retries it when main moves"))
|
|
396
|
+
gateIdle(("Idle<br/>a fork, a stranger,<br/>or not open"))
|
|
355
397
|
gateFix -.->|"attempt 6 of 6"| gateBlocked
|
|
356
398
|
|
|
357
399
|
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
|