@plot-pm/board 0.14.4 → 0.16.0
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/board-server.mjs +166 -158
- package/package.json +3 -1
- package/plot-agent-manifest.sh +137 -0
- package/plot-approve.sh +25 -1
- package/plot-budget.sh +378 -19
- package/plot-config.sh +86 -2
- package/plot-deliver.sh +26 -3
- package/plot-dispatch.sh +307 -43
- package/plot-fleet-scan.sh +438 -229
- package/plot-host.sh +322 -84
- package/plot-plan-meta.sh +68 -13
- package/plot-pr-merged.sh +29 -2
- package/plot-reap.sh +212 -23
- package/plot-resolve-artifact.sh +4 -2
- package/plot-tmp.sh +108 -0
- package/plot-transcript-quiet.sh +29 -1
- package/plot-worker-monitor.sh +66 -5
package/plot-fleet-scan.sh
CHANGED
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
# --list-eligible print EVERY claimable branch, one per line (exit 1 if none).
|
|
17
17
|
# For callers that need the count rather than one item — a dry
|
|
18
18
|
# run changes nothing, so its answer cannot go stale.
|
|
19
|
+
# WHEN THERE ARE NONE IT SAYS WHY, ON STDERR: whether every
|
|
20
|
+
# candidate in an eligible wave is taken, or no eligible wave held
|
|
21
|
+
# a startable branch at all. Two silences a reader cannot
|
|
22
|
+
# otherwise tell apart (#994). Stdout stays a bare branch list
|
|
23
|
+
# because `plot-dispatch.sh` pipes it through `sort -u` and
|
|
24
|
+
# dispatches every line, and the exit code stays 1 either way, so
|
|
25
|
+
# no caller's gate moves. An estate with NO plans exits earlier
|
|
26
|
+
# and says nothing — there is no candidate set to describe.
|
|
19
27
|
# --loose a prior wave counts as satisfied when its branches carry PUSHED
|
|
20
28
|
# work, not only merged work. Buys throughput, pays in rebase
|
|
21
29
|
# risk — the plan requires a stated reason for using it. Default
|
|
@@ -88,12 +96,21 @@
|
|
|
88
96
|
# evidence of a typo either.
|
|
89
97
|
# Output: per-plan wave report on stdout, terminated by a machine-countable
|
|
90
98
|
# summary line:
|
|
91
|
-
# summary: plans=1 waves=3 branches=5 claimed=1 eligible=2 blocked=1 deferred=1 waiting=1 prereq_missing=0 merge_detect=pr-merge host=ok main=main
|
|
99
|
+
# summary: plans=1 waves=3 branches=5 claimed=1 claimable=2 eligible=2 blocked=1 deferred=1 waiting=1 prereq_missing=0 merge_detect=pr-merge host=ok main=main
|
|
92
100
|
# `host` is one of ok, partial, throttled, secondary, failed, unasked —
|
|
93
101
|
# `partial` means some of the host's states answered and some did not,
|
|
94
102
|
# so the PR readings below are incomplete rather than absent.
|
|
95
103
|
# `blocked` counts WAVES an earlier wave holds; `waiting` and
|
|
96
104
|
# `prereq_missing` count BRANCHES their `waits:` annotation holds.
|
|
105
|
+
# `claimable` counts BRANCHES a worker may take now, and it is the key
|
|
106
|
+
# the offer paths agree with — `--next` names one of them and
|
|
107
|
+
# `--list-eligible` names all of them. `eligible=` carries the SAME
|
|
108
|
+
# number beside it and is kept for consumers that read it; it was the
|
|
109
|
+
# only name this count had until 2026-09-25, sitting between two wave
|
|
110
|
+
# counts while counting branches, so a wave printed `eligible` in the
|
|
111
|
+
# body while the footer read `eligible=0` and both were right about
|
|
112
|
+
# different questions (#994). A reader wanting the WAVE answer counts
|
|
113
|
+
# the body's verdicts; no footer key reports it.
|
|
97
114
|
# merge_detect names how merged-and-deleted branches were detected:
|
|
98
115
|
# pr-merge (exhaustive), truncated (capped walk), none (no conforming
|
|
99
116
|
# merge commits — a squash/rebase repo, where `open` says nothing about
|
|
@@ -116,13 +133,21 @@
|
|
|
116
133
|
# lifecycle state, verbatim from plot-plan-meta.sh, and the half of a
|
|
117
134
|
# row's phase git cannot answer. Which column a row reads is composed
|
|
118
135
|
# from it AND the branch state one layer up; this script decides nothing.
|
|
119
|
-
# The plan set also includes
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
# Plans are enumerated from
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
136
|
+
# The plan set also includes every plan at `Delivered`, until it is
|
|
137
|
+
# `Released` (see "the release scope"), so DONE names what the next
|
|
138
|
+
# release ships.
|
|
139
|
+
# Plans are enumerated from REFS (`git ls-tree`/`git show`), NOT from
|
|
140
|
+
# the working tree — so the list describes committed state and does not
|
|
141
|
+
# change while rebases and worker commits rewrite the checkout
|
|
142
|
+
# underneath a running fleet. `origin/<main>` first, then each prefixed
|
|
143
|
+
# branch's own tree for the plans the default branch does not carry:
|
|
144
|
+
# a plan created with `Impl: same branch` lives only on its work branch
|
|
145
|
+
# until that branch merges, and reading main alone reported its branch as
|
|
146
|
+
# an anonymous row with `plan: ""` while the Board tab showed the same
|
|
147
|
+
# plan as a Draft card (#972). A plan path is taken ONCE — the default
|
|
148
|
+
# branch wins, and among branches the first to carry it — because two
|
|
149
|
+
# branches cut from one point hold the same file and a row per branch
|
|
150
|
+
# would report one plan as several. A consequence worth stating: an
|
|
126
151
|
# UNCOMMITTED plan is invisible, deliberately — the fleet view shows
|
|
127
152
|
# what is shared, and a plan only this machine has cannot be claimed by
|
|
128
153
|
# any worker. --json carries `plan_source` (ref | worktree), which
|
|
@@ -228,6 +253,8 @@ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
|
228
253
|
# plot-dispatch.sh so a worker has ONE state, not one per reader.
|
|
229
254
|
# shellcheck source=plot-worker-state.sh
|
|
230
255
|
. "$script_dir/plot-worker-state.sh"
|
|
256
|
+
# Every temp path this scan creates, and its only EXIT/INT/TERM traps.
|
|
257
|
+
. "$script_dir/plot-tmp.sh"
|
|
231
258
|
cfg() { "$script_dir/plot-config.sh" get "$1" "${2:-}"; }
|
|
232
259
|
|
|
233
260
|
do_fetch=1
|
|
@@ -548,14 +575,15 @@ fi
|
|
|
548
575
|
# outlives the scan that fetched it — a stale `merged` read from a previous run
|
|
549
576
|
# is exactly the fabricated verdict the failure direction above forbids.
|
|
550
577
|
#
|
|
551
|
-
# Cleanup is
|
|
578
|
+
# Cleanup is registered rather than trailing: the script exits early in several
|
|
552
579
|
# places (--next with nothing to start, no active plans), and a temp directory
|
|
553
|
-
# left behind on those paths would accumulate one per poll.
|
|
580
|
+
# left behind on those paths would accumulate one per poll. It goes through
|
|
581
|
+
# `plot-tmp.sh`'s one registry: this cache had its own EXIT trap, and the
|
|
582
|
+
# `REF_TMP` trap below replaced it, so every scan left one `tmp.*` directory of
|
|
583
|
+
# ~955 files behind (measured 2026-09-30).
|
|
554
584
|
HOST_STATE_CACHE=""
|
|
555
585
|
if [ "$HOST_LOOKUP_OK" = 1 ]; then
|
|
556
|
-
HOST_STATE_CACHE
|
|
557
|
-
[ -n "$HOST_STATE_CACHE" ] \
|
|
558
|
-
&& trap 'rm -rf "$HOST_STATE_CACHE" 2>/dev/null || true' EXIT INT TERM
|
|
586
|
+
plot_tmpdir HOST_STATE_CACHE fleet-host-state 2>/dev/null || HOST_STATE_CACHE=""
|
|
559
587
|
fi
|
|
560
588
|
|
|
561
589
|
# The cache key. Shared by the join and by `host_pr_state`, because the two
|
|
@@ -697,49 +725,11 @@ REMOTE_REFS=$(git for-each-ref --format='%(refname:strip=3)%09%(objectname)' \
|
|
|
697
725
|
# falls back to the listing there, which is what it has always done.
|
|
698
726
|
TRACKED_BRANCHES=$(printf '%s\n' "$REMOTE_REFS" | cut -f1 | grep -v '^HEAD$' | grep -v '^$' | tr '\n' ' ')
|
|
699
727
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
# answer. A failed list leaves the cache EMPTY, so every branch falls through
|
|
706
|
-
# to the unanswerable `-` rather than to a fabricated "no PR".
|
|
707
|
-
#
|
|
708
|
-
# `--rich` adds `checks` and `draft` to the response — the fields `--loose`
|
|
709
|
-
# needs to verify that a prior wave's PR is actually green and ready.
|
|
710
|
-
# Requested unconditionally because the cost is zero on GitHub (same GraphQL
|
|
711
|
-
# call) and bounded on Bitbucket (N extra calls only when CI: jenkins is
|
|
712
|
-
# configured), and the parsing below already skips fields the response does
|
|
713
|
-
# not contain. The BEHAVIOUR change is in `pr_ready`, which now reads the
|
|
714
|
-
# check rollup from the cache rather than making a per-branch host call.
|
|
715
|
-
#
|
|
716
|
-
# THE CODE IS KEPT, not just tested. This guard was always correct — a failed
|
|
717
|
-
# list prefills nothing — and until 2026-08-30 it never fired, because
|
|
718
|
-
# `pr-list` swallowed its own failure and exited 0 with empty stdout. Now
|
|
719
|
-
# that it can fail, WHICH failure it was is a fact worth carrying: exit 5 is
|
|
720
|
-
# a rate limit and exit 3 is anything else, and the summary reports the
|
|
721
|
-
# difference rather than degrading silently.
|
|
722
|
-
# STDOUT TO A FILE so stderr can be captured separately — see the verdict
|
|
723
|
-
# below. It lives in `HOST_STATE_CACHE`, which already has an EXIT trap, so
|
|
724
|
-
# this adds no second cleanup path. When mktemp -d failed the cache is "",
|
|
725
|
-
# and /dev/null keeps the call working with the text simply unavailable.
|
|
726
|
-
host_list_out="${HOST_STATE_CACHE:+$HOST_STATE_CACHE/pr-list.json}"
|
|
727
|
-
host_list_out="${host_list_out:-/dev/null}"
|
|
728
|
-
# THE BRANCHES THIS SCAN TRACKS, HANDED TO THE HOST (#333). The adapter uses
|
|
729
|
-
# them only where it can — the Bitbucket arm sweeps its REST endpoint once per
|
|
730
|
-
# branch per state — and ignores them everywhere else, so the GitHub arm makes
|
|
731
|
-
# the single call it always made. Passing them unconditionally keeps one call
|
|
732
|
-
# shape here rather than a backend test this script has no business making.
|
|
733
|
-
#
|
|
734
|
-
# AN EMPTY SET PASSES NOTHING and the adapter lists as before. See
|
|
735
|
-
# `TRACKED_BRANCHES`: a completeness claim over an empty set would license
|
|
736
|
-
# `NONE` for branches nobody asked about.
|
|
737
|
-
_branch_args=()
|
|
738
|
-
for _tb in $TRACKED_BRANCHES; do _branch_args+=(--branch "$_tb"); done
|
|
739
|
-
host_err=$("$script_dir/plot-host.sh" pr-list --state all --limit "$PR_LIST_LIMIT" --rich \
|
|
740
|
-
${_branch_args[@]+"${_branch_args[@]}"} \
|
|
741
|
-
</dev/null 2>&1 >"$host_list_out"); rc=$?
|
|
742
|
-
js=$(cat "$host_list_out" 2>/dev/null)
|
|
728
|
+
# ONE `pr-list` CALL'S EXIT, READ AS A VERDICT WORD in `_plv` — a global
|
|
729
|
+
# rather than stdout, so the two calls in `prefill_pr_states` fork nothing to
|
|
730
|
+
# classify themselves. `$1` is the exit code, `$2` the call's stderr.
|
|
731
|
+
pr_list_verdict() {
|
|
732
|
+
local rc="$1" err="$2"
|
|
743
733
|
# A PARTIAL ANSWER TAKES THE PARSE PATH AND STILL DEGRADES THE VERDICT, which
|
|
744
734
|
# is a control-flow change rather than another `case` arm below: every other
|
|
745
735
|
# non-zero rc sets a verdict and returns BEFORE `$js` is read, because there
|
|
@@ -750,9 +740,11 @@ prefill_pr_states() {
|
|
|
750
740
|
# BOTH HALVES ARE REQUIRED. Falling through without setting the verdict would
|
|
751
741
|
# report `ok` over a page missing a whole state, which is #912; returning
|
|
752
742
|
# early would throw away rows the host did answer with.
|
|
753
|
-
if [ "$rc" -eq
|
|
754
|
-
|
|
755
|
-
elif [ "$rc" -
|
|
743
|
+
if [ "$rc" -eq 0 ]; then
|
|
744
|
+
_plv=ok
|
|
745
|
+
elif [ "$rc" -eq 7 ]; then
|
|
746
|
+
_plv=partial
|
|
747
|
+
else
|
|
756
748
|
# THREE OUTCOMES, NOT TWO. `unasked` already means "the question was
|
|
757
749
|
# never put" (see HOST_VERDICT above: *not a degradation, the scan was
|
|
758
750
|
# never asking*), and a host that cannot be ASKED AT ALL belongs there
|
|
@@ -774,10 +766,10 @@ prefill_pr_states() {
|
|
|
774
766
|
# always read 1. The branch was startable; the scan had stopped being
|
|
775
767
|
# able to say so.
|
|
776
768
|
case "$rc" in
|
|
777
|
-
5)
|
|
778
|
-
6)
|
|
779
|
-
4)
|
|
780
|
-
*) case "$
|
|
769
|
+
5) _plv=throttled ;;
|
|
770
|
+
6) _plv=secondary ;;
|
|
771
|
+
4) _plv=unasked ;;
|
|
772
|
+
*) case "$err" in
|
|
781
773
|
# THE WORDING IS MEASURED, NOT GUESSED. An earlier version of this
|
|
782
774
|
# list matched auth/login/credential and MISSED the message CI
|
|
783
775
|
# actually emits:
|
|
@@ -794,7 +786,7 @@ prefill_pr_states() {
|
|
|
794
786
|
# a real failure and stays `failed`, because widening this to a
|
|
795
787
|
# catch-all would turn every host outage into "nobody asked".
|
|
796
788
|
*TOKEN*|*token*|*auth*|*Auth*|*AUTH*|*login*|*Login*|*credential*|*Credential*|*"not logged"*)
|
|
797
|
-
|
|
789
|
+
_plv=unasked ;;
|
|
798
790
|
# NO REMOTE IS A CONFIGURATION, NOT A FAULT — the same reading as a
|
|
799
791
|
# missing token one line up, reached by the same route: `plot-host.sh`
|
|
800
792
|
# exits 3 for both, because both are "the op cannot proceed", and
|
|
@@ -813,19 +805,123 @@ prefill_pr_states() {
|
|
|
813
805
|
# claimable — the right refusal about the wrong thing. There is no
|
|
814
806
|
# merge state to withhold on where there is no remote to hold it.
|
|
815
807
|
*"no git remotes"*|*"no remote"*)
|
|
816
|
-
|
|
817
|
-
*)
|
|
808
|
+
_plv=unasked ;;
|
|
809
|
+
*) _plv=failed ;;
|
|
818
810
|
esac ;;
|
|
819
811
|
esac
|
|
820
|
-
return 0
|
|
821
812
|
fi
|
|
822
|
-
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
# WHICH OF TWO VERDICTS IS WORSE. Every word that returns before a payload is
|
|
816
|
+
# read outranks both words that parse one, so a failed call can never be
|
|
817
|
+
# reported as `ok` or `partial` beside a call that answered.
|
|
818
|
+
# The rank is set in `_plr`, for the same no-fork reason as `_plv`.
|
|
819
|
+
pr_list_verdict_rank() {
|
|
820
|
+
case "$1" in
|
|
821
|
+
ok) _plr=0 ;; partial) _plr=1 ;; unasked) _plr=2 ;;
|
|
822
|
+
secondary) _plr=3 ;; throttled) _plr=4 ;; *) _plr=5 ;;
|
|
823
|
+
esac
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
prefill_pr_states() {
|
|
827
|
+
[ "$HOST_LOOKUP_OK" = 1 ] || return 0
|
|
828
|
+
[ -n "$HOST_STATE_CACHE" ] || return 0
|
|
829
|
+
local js br st key rc
|
|
830
|
+
# Exit code first: non-zero is a transport failure and its stdout is not an
|
|
831
|
+
# answer. A failed list leaves the cache EMPTY, so every branch falls through
|
|
832
|
+
# to the unanswerable `-` rather than to a fabricated "no PR".
|
|
833
|
+
#
|
|
834
|
+
# `--rich` adds `checks` and `draft` to the response — the fields `--loose`
|
|
835
|
+
# needs to verify that a prior wave's PR is actually green and ready. The
|
|
836
|
+
# BEHAVIOUR change is in `pr_ready`, which reads the check rollup from the
|
|
837
|
+
# cache rather than making a per-branch host call.
|
|
838
|
+
#
|
|
839
|
+
# THE ROLLUP IS ASKED OF OPEN PRS ONLY. On GitHub it is `statusCheckRollup`,
|
|
840
|
+
# and its cost scales with the rows it is asked for. Measured 2026-09-25 on
|
|
841
|
+
# this repo through `plot-host.sh`: 957 PRs (920 merged, 34 closed, 3 open),
|
|
842
|
+
# and the one `--state all --rich` call took ~37 s of a ~55 s scan. A merged
|
|
843
|
+
# or closed PR's checks cannot change, and `pr_ready` reads them only for an
|
|
844
|
+
# open one. So TWO calls: `--state open --rich` for the PRs whose checks can
|
|
845
|
+
# still move, and `--state all` without `--rich` for every PR's state.
|
|
823
846
|
#
|
|
824
|
-
#
|
|
825
|
-
#
|
|
826
|
-
#
|
|
827
|
-
#
|
|
828
|
-
|
|
847
|
+
# THE CODE IS KEPT, not just tested. This guard was always correct — a failed
|
|
848
|
+
# list prefills nothing — and until 2026-08-30 it never fired, because
|
|
849
|
+
# `pr-list` swallowed its own failure and exited 0 with empty stdout. Now
|
|
850
|
+
# that it can fail, WHICH failure it was is a fact worth carrying: exit 5 is
|
|
851
|
+
# a rate limit and exit 3 is anything else, and the summary reports the
|
|
852
|
+
# difference rather than degrading silently.
|
|
853
|
+
# STDOUT TO A FILE so stderr can be captured separately — see the verdict
|
|
854
|
+
# below. It lives in `HOST_STATE_CACHE`, which already has an EXIT trap, so
|
|
855
|
+
# this adds no second cleanup path. When mktemp -d failed the cache is "",
|
|
856
|
+
# and /dev/null keeps the call working with the text simply unavailable.
|
|
857
|
+
local open_list_out host_list_out open_err rc_open
|
|
858
|
+
open_list_out="${HOST_STATE_CACHE:+$HOST_STATE_CACHE/pr-list-open.json}"
|
|
859
|
+
open_list_out="${open_list_out:-/dev/null}"
|
|
860
|
+
host_list_out="${HOST_STATE_CACHE:+$HOST_STATE_CACHE/pr-list.json}"
|
|
861
|
+
host_list_out="${host_list_out:-/dev/null}"
|
|
862
|
+
# THE BRANCHES THIS SCAN TRACKS, HANDED TO THE HOST (#333). The adapter uses
|
|
863
|
+
# them only where it can — the Bitbucket arm sweeps its REST endpoint once per
|
|
864
|
+
# branch per state — and ignores them everywhere else, so the GitHub arm makes
|
|
865
|
+
# the single call it always made. Passing them unconditionally keeps one call
|
|
866
|
+
# shape here rather than a backend test this script has no business making.
|
|
867
|
+
#
|
|
868
|
+
# AN EMPTY SET PASSES NOTHING and the adapter lists as before. See
|
|
869
|
+
# `TRACKED_BRANCHES`: a completeness claim over an empty set would license
|
|
870
|
+
# `NONE` for branches nobody asked about.
|
|
871
|
+
_branch_args=()
|
|
872
|
+
for _tb in $TRACKED_BRANCHES; do _branch_args+=(--branch "$_tb"); done
|
|
873
|
+
# BOTH CALLS TAKE `--limit` AND THE BRANCHES. Without `--limit` the host
|
|
874
|
+
# returns 30 (see `PR_LIST_LIMIT`). The branches make the Bitbucket arm sweep
|
|
875
|
+
# the open state exactly rather than list a fixed 50 of it: an open PR
|
|
876
|
+
# missing from the rich payload has NO row at all once the plain payload's
|
|
877
|
+
# OPEN rows are dropped below, and a missing row reads as "no PR".
|
|
878
|
+
#
|
|
879
|
+
# OPEN FIRST, THEN ALL. A PR that merges between the two calls then carries
|
|
880
|
+
# a rich OPEN row and a plain MERGED one, and the rank keeps OPEN — stale by
|
|
881
|
+
# seconds, and the next scan corrects it.
|
|
882
|
+
open_err=$("$script_dir/plot-host.sh" pr-list --state open --limit "$PR_LIST_LIMIT" --rich \
|
|
883
|
+
${_branch_args[@]+"${_branch_args[@]}"} \
|
|
884
|
+
</dev/null 2>&1 >"$open_list_out"); rc_open=$?
|
|
885
|
+
# THE VERDICT IS THE WORSE OF THE TWO, never the last one. A rich call
|
|
886
|
+
# throttled while the plain one answers leaves `checks` absent for every open
|
|
887
|
+
# PR, so `--loose` degrades to strict — and a footer reading `host=ok` would
|
|
888
|
+
# give no reason for it.
|
|
889
|
+
#
|
|
890
|
+
# A FAILED CALL PREFILLS NOTHING, whichever of the two failed. That is the
|
|
891
|
+
# single call's behaviour carried over: a verdict other than `ok` or
|
|
892
|
+
# `partial` sets the verdict and returns before any payload is read. A
|
|
893
|
+
# failed `open` call therefore decides the outcome alone, and the `all` call
|
|
894
|
+
# is not made: its answer could not change the verdict's direction, and on a
|
|
895
|
+
# throttled host it would spend quota on a payload nothing reads.
|
|
896
|
+
pr_list_verdict "$rc_open" "$open_err"; _v_open=$_plv
|
|
897
|
+
case "$_v_open" in ok|partial) ;; *) HOST_VERDICT=$_v_open; return 0 ;; esac
|
|
898
|
+
host_err=$("$script_dir/plot-host.sh" pr-list --state all --limit "$PR_LIST_LIMIT" \
|
|
899
|
+
${_branch_args[@]+"${_branch_args[@]}"} \
|
|
900
|
+
</dev/null 2>&1 >"$host_list_out"); rc=$?
|
|
901
|
+
pr_list_verdict "$rc" "$host_err"; _v_all=$_plv
|
|
902
|
+
pr_list_verdict_rank "$_v_open"; _r_open=$_plr
|
|
903
|
+
pr_list_verdict_rank "$_v_all"
|
|
904
|
+
if [ "$_r_open" -gt "$_plr" ]; then HOST_VERDICT=$_v_open; else HOST_VERDICT=$_v_all; fi
|
|
905
|
+
case "$HOST_VERDICT" in ok|partial) ;; *) return 0 ;; esac
|
|
906
|
+
# THE MERGE, AND THE TRAP IT AVOIDS. An open PR is in BOTH payloads — rich
|
|
907
|
+
# from the `open` call, plain from the `all` call — and both rows rank 1 in
|
|
908
|
+
# the dedup below. With the sort key exhausted, `sort` compares whole lines,
|
|
909
|
+
# and the plain row's `-` sentinel (0x2D) sorts before any letter: the plain
|
|
910
|
+
# row wins every time and the rollup is lost for 100% of open PRs.
|
|
911
|
+
#
|
|
912
|
+
# So the `all` payload's OPEN rows are dropped before the concatenation, and
|
|
913
|
+
# each branch contributes one OPEN row at most. Those rows are unused: the
|
|
914
|
+
# `open` call already answered for every open PR, with its rollup.
|
|
915
|
+
#
|
|
916
|
+
# THE COMPLETENESS COUNT IS TAKEN FROM THE `all` PAYLOAD BEFORE THE FILTER.
|
|
917
|
+
# `.list-complete` compares a row count against `PR_LIST_LIMIT`, and a count
|
|
918
|
+
# taken after the filter can fall below the limit for a list that reached it
|
|
919
|
+
# — a truncated list then reads as whole, and a cache miss derives `NONE` for
|
|
920
|
+
# a branch that has a PR. The `open` payload is counted too, since it is a
|
|
921
|
+
# page of its own.
|
|
922
|
+
_pr_rows=$(grep -c '"state":"[A-Z]*","head":"' "$host_list_out" 2>/dev/null) || _pr_rows=0
|
|
923
|
+
_pr_open_rows=$(grep -c '"state":"[A-Z]*","head":"' "$open_list_out" 2>/dev/null) || _pr_open_rows=0
|
|
924
|
+
js=$(cat "$open_list_out" 2>/dev/null; grep -v '"state":"OPEN","head":"' "$host_list_out" 2>/dev/null)
|
|
829
925
|
# `pr-list` emits one compact JSON object per line. PARSED IN ONE PASS, and
|
|
830
926
|
# that is a correctness-of-cost property rather than a style preference:
|
|
831
927
|
# measured 2026-08-18 on this repo's 221 PRs, a `sed` per field per row —
|
|
@@ -860,12 +956,8 @@ prefill_pr_states() {
|
|
|
860
956
|
# response, which is now only an error case) reads as empty and `pr_ready`
|
|
861
957
|
# treats it as `unknown`, which degrades to strict — the safer direction.
|
|
862
958
|
local last="" chk dft
|
|
863
|
-
# Rows parsed, for the completeness test below. Counted here because this is
|
|
864
|
-
# the one place every row passes through.
|
|
865
|
-
_pr_rows=0
|
|
866
959
|
while IFS=" " read -r st chk dft br; do
|
|
867
960
|
[ -n "$br" ] && [ -n "$st" ] || continue
|
|
868
|
-
_pr_rows=$((_pr_rows + 1))
|
|
869
961
|
[ "$br" = "$last" ] && continue
|
|
870
962
|
last="$br"
|
|
871
963
|
# A PLAIN row (no `--rich` fields) carries `-` in the checks/draft slots so
|
|
@@ -930,10 +1022,12 @@ EOF
|
|
|
930
1022
|
# records on Bitbucket, where `bb pr list` is silently partial past 50 PRs per
|
|
931
1023
|
# state.
|
|
932
1024
|
#
|
|
933
|
-
# Counted from the rows
|
|
934
|
-
# malformed line that the
|
|
1025
|
+
# Counted from the rows carrying the fields the `sed` anchors on, so a
|
|
1026
|
+
# malformed line that the parse skips cannot inflate the count into a false
|
|
935
1027
|
# claim of completeness. Fewer rows than the limit means the host had no more
|
|
936
|
-
# to give; equal to it means it may have.
|
|
1028
|
+
# to give; equal to it means it may have. BOTH PAGES must be short: the
|
|
1029
|
+
# `open` page is a listing of its own, and a truncated one drops open PRs
|
|
1030
|
+
# the filtered `all` page no longer carries.
|
|
937
1031
|
#
|
|
938
1032
|
# AN EMPTY LIST IS NOT A COMPLETE ONE, and the test that caught this is the
|
|
939
1033
|
# reason it is written down. A host that exits 0 while printing nothing —
|
|
@@ -971,11 +1065,21 @@ EOF
|
|
|
971
1065
|
# unjoined branch and still answers correctly — the per-branch N+1 that #216
|
|
972
1066
|
# removed. That is why withholding the marker is always the safe direction and
|
|
973
1067
|
# is what every failure path here does.
|
|
1068
|
+
#
|
|
1069
|
+
# THE `open` CALL MUST BE WHOLE FOR EITHER CLAIM. The `all` call's OPEN rows
|
|
1070
|
+
# were dropped above, so an `open` answer that is short — exit 7, or a sweep
|
|
1071
|
+
# that did not state its completeness — leaves open PRs with no row at all,
|
|
1072
|
+
# and completeness would turn those misses into `NONE`.
|
|
1073
|
+
[ "$_v_open" = ok ] || return 0
|
|
974
1074
|
case "$host_err" in
|
|
975
1075
|
*"pr-list sweep complete"*)
|
|
976
|
-
|
|
1076
|
+
case "$open_err" in
|
|
1077
|
+
*"pr-list sweep complete"*)
|
|
1078
|
+
printf '1' > "$HOST_STATE_CACHE/.list-complete" 2>/dev/null || true ;;
|
|
1079
|
+
esac ;;
|
|
977
1080
|
*)
|
|
978
|
-
if [ "$_pr_rows" -gt 0 ] && [ "$_pr_rows" -lt "$PR_LIST_LIMIT" ]
|
|
1081
|
+
if [ "$_pr_rows" -gt 0 ] && [ "$_pr_rows" -lt "$PR_LIST_LIMIT" ] \
|
|
1082
|
+
&& [ "$_pr_open_rows" -lt "$PR_LIST_LIMIT" ] 2>/dev/null; then
|
|
979
1083
|
printf '1' > "$HOST_STATE_CACHE/.list-complete" 2>/dev/null || true
|
|
980
1084
|
fi ;;
|
|
981
1085
|
esac
|
|
@@ -2144,37 +2248,15 @@ pr_ready() {
|
|
|
2144
2248
|
}
|
|
2145
2249
|
|
|
2146
2250
|
# ---------------------------------------------------------------------------
|
|
2147
|
-
#
|
|
2251
|
+
# Delivered plans: the release scope
|
|
2148
2252
|
# ---------------------------------------------------------------------------
|
|
2149
2253
|
#
|
|
2150
|
-
#
|
|
2151
|
-
#
|
|
2152
|
-
#
|
|
2153
|
-
#
|
|
2154
|
-
#
|
|
2155
|
-
#
|
|
2156
|
-
#
|
|
2157
|
-
# A ROLLING 24 HOURS, not the calendar day. Literally "delivered today" is
|
|
2158
|
-
# easier to explain and wrong at exactly the wrong moment: a plan delivered at
|
|
2159
|
-
# 23:50 vanishes ten minutes later, mid-session, while the branches it names
|
|
2160
|
-
# are still on screen. 24 is also the one freshness bound this repo already
|
|
2161
|
-
# uses (`Claim stale after`), so it is one unit to learn rather than two.
|
|
2162
|
-
#
|
|
2163
|
-
# THE WINDOW FILTERS BEFORE THE PARSE. Measured: ~57 ms per plan through
|
|
2164
|
-
# plot-plan-meta.sh against a scan that already runs 500–1050 ms, so parsing
|
|
2165
|
-
# fourteen delivered plans to discard thirteen would roughly double the pulse —
|
|
2166
|
-
# and that cost grows with the archive, which only ever gets larger, while the
|
|
2167
|
-
# answer stays the size of a day's work. So the cheap signal comes first (the
|
|
2168
|
-
# delivered symlink's own mtime) and only the candidates it admits are parsed.
|
|
2169
|
-
#
|
|
2170
|
-
# The pre-filter may OVER-ADMIT AND PAY A PARSE; it may never exclude. A
|
|
2171
|
-
# checkout can freshen an old file, so the `Delivered:` record keeps the last
|
|
2172
|
-
# word — but nothing mtime rules out could have been delivered inside the
|
|
2173
|
-
# window. On a fresh clone or a CI worktree every file shares one checkout
|
|
2174
|
-
# timestamp and ALL of them are admitted: correct, merely slower, once. Reaching
|
|
2175
|
-
# for `git log` per plan to avoid that would spend a git call to save a parse.
|
|
2176
|
-
DELIVERED_WINDOW_HOURS=$(cfg "Claim stale after" "24")
|
|
2177
|
-
case "$DELIVERED_WINDOW_HOURS" in (*[!0-9]*|'') DELIVERED_WINDOW_HOURS=24 ;; esac
|
|
2254
|
+
# A plan at `Delivered` stays in the pulse until it is `Released`, whatever its
|
|
2255
|
+
# age. DONE holds the release scope (`done-holds-what-is-still-yours`): every
|
|
2256
|
+
# plan whose work has landed and whose version has not shipped. A 24-hour window
|
|
2257
|
+
# on the `Delivered:` record bounded this until 2026-10-01, when 25 plans awaited
|
|
2258
|
+
# 2.22.0 and DONE showed 10. Every plan file is parsed in one pass anyway, so the
|
|
2259
|
+
# window bought no parse time; it only hid work the next release ships.
|
|
2178
2260
|
|
|
2179
2261
|
|
|
2180
2262
|
# ---------------------------------------------------------------------------
|
|
@@ -2340,52 +2422,7 @@ changed_ago_of() { # $1=branch → "<seconds since>\t<epoch of>" for the newest
|
|
|
2340
2422
|
if [ "$newest" -gt "$now" ]; then printf '0\t%s' "$newest"; else printf '%s\t%s' "$((now - newest))" "$newest"; fi
|
|
2341
2423
|
}
|
|
2342
2424
|
|
|
2343
|
-
# A plan whose delivered symlink was touched inside the window. `find -newermt`
|
|
2344
|
-
# is not portable to every BSD find in the wild, so the cutoff is computed and
|
|
2345
|
-
# compared with `stat` — one stat per file, no parse.
|
|
2346
|
-
delivered_candidates() {
|
|
2347
|
-
local cutoff now link mtime
|
|
2348
|
-
now=$(date +%s)
|
|
2349
|
-
cutoff=$((now - DELIVERED_WINDOW_HOURS * 3600))
|
|
2350
|
-
for link in "$DELIVERED_DIR"*.md; do
|
|
2351
|
-
[ -e "$link" ] || continue
|
|
2352
|
-
# `stat` follows the symlink, which is what we want: the TARGET is the plan,
|
|
2353
|
-
# and a plan edited after delivery must still admit. An unreadable time
|
|
2354
|
-
# ADMITS rather than excludes — the pre-filter may only over-admit, and the
|
|
2355
|
-
# `Delivered:` record has the last word either way.
|
|
2356
|
-
mtime=$(file_mtime "$link") || { printf '%s\n' "$link"; continue; }
|
|
2357
|
-
[ "$mtime" -ge "$cutoff" ] && printf '%s\n' "$link"
|
|
2358
|
-
done
|
|
2359
|
-
}
|
|
2360
2425
|
|
|
2361
|
-
# Does this plan's `Delivered:` record fall inside the window? The RECORD
|
|
2362
|
-
# decides — mtime only chose who got asked.
|
|
2363
|
-
#
|
|
2364
|
-
# "No date, no row." A delivered plan with an empty record does not appear at
|
|
2365
|
-
# all: no date means no membership in any window, the same rule the waiting age
|
|
2366
|
-
# already follows. Showing it always would create the one row that can never
|
|
2367
|
-
# age out of DONE, and the missing record is a bookkeeping fault
|
|
2368
|
-
# plot-reconcile-scan.sh exists to report — a view that quietly compensates
|
|
2369
|
-
# for it makes the fault harder to see.
|
|
2370
|
-
#
|
|
2371
|
-
# A BARE DATE IS ANCHORED AT THE END OF ITS DAY, not at midnight, and this is
|
|
2372
|
-
# the one detail that makes "rolling, not the calendar day" true rather than
|
|
2373
|
-
# merely stated. Every `Delivered:` record in this repo is a bare date, which
|
|
2374
|
-
# names no time — so anchoring at 00:00 measures from up to a day BEFORE the
|
|
2375
|
-
# delivery, and the window collapses back into exactly the calendar boundary
|
|
2376
|
-
# the rolling window exists to avoid: a plan delivered at 23:50 would be an
|
|
2377
|
-
# hour from expiry the moment it was written, and gone ten minutes later
|
|
2378
|
-
# mid-session while the branches it names are still on screen.
|
|
2379
|
-
#
|
|
2380
|
-
# Anchoring at 23:59:59 over-admits by at most the length of the delivery day.
|
|
2381
|
-
# That is the same direction the mtime pre-filter is allowed to err in, and for
|
|
2382
|
-
# the same reason: showing a finished plan slightly too long costs a row, while
|
|
2383
|
-
# dropping one mid-session costs the reader the work they were looking at. A
|
|
2384
|
-
# record that DOES carry a time is honoured exactly, so the imprecision belongs
|
|
2385
|
-
# to the record rather than to the rule.
|
|
2386
|
-
# The rule itself now runs inside the ONE estate parse below (`in_window`),
|
|
2387
|
-
# because asking it per plan meant an interpreter per plan. What it decides is
|
|
2388
|
-
# unchanged; only the number of processes that decide it is.
|
|
2389
2426
|
|
|
2390
2427
|
# ---------------------------------------------------------------------------
|
|
2391
2428
|
# Plan enumeration: from the REF, not from the tree
|
|
@@ -2476,15 +2513,15 @@ ref_ls() { # $1=dir → newline-separated paths under it in origin/$MAIN
|
|
|
2476
2513
|
# `ref_plan_file` is called as `$(ref_plan_file ...)`, which runs it in a
|
|
2477
2514
|
# SUBSHELL. A lazy `[ -z "$REF_TMP" ] && REF_TMP=$(mktemp -d)` inside it
|
|
2478
2515
|
# assigns in the child and the parent never sees it — so every call made a
|
|
2479
|
-
# fresh directory, the parent's variable stayed empty, and the
|
|
2480
|
-
#
|
|
2516
|
+
# fresh directory, the parent's variable stayed empty, and the exit cleanup
|
|
2517
|
+
# removed nothing. Measured while writing this: three plans, three temp dirs,
|
|
2481
2518
|
# none removed. The lifetime is owned out here, where the trap can see it.
|
|
2482
2519
|
REF_TMP=""
|
|
2483
2520
|
if [ "$PLAN_SOURCE" = "ref" ]; then
|
|
2484
|
-
REF_TMP=$(mktemp -d "${TMPDIR:-/tmp}/plot-fleet-ref.XXXXXX") || REF_TMP=""
|
|
2485
2521
|
# The scan is read-only and short-lived, and the board polls it every 5 s —
|
|
2486
|
-
# a directory that outlives the run would accumulate one per poll
|
|
2487
|
-
|
|
2522
|
+
# a directory that outlives the run would accumulate one per poll, so the
|
|
2523
|
+
# helper removes it at exit.
|
|
2524
|
+
plot_tmpdir REF_TMP fleet-ref || REF_TMP=""
|
|
2488
2525
|
# No temp dir means no way to hand the parser a file, so the ref path cannot
|
|
2489
2526
|
# work. Falling back to the checkout is the honest answer, and it announces
|
|
2490
2527
|
# itself through `plan_source` exactly like an unreadable ref.
|
|
@@ -2625,6 +2662,68 @@ ref_plan_file() { # $1=path in ref → temp file path, or "" when unreadable
|
|
|
2625
2662
|
printf '%s' "$out"
|
|
2626
2663
|
}
|
|
2627
2664
|
|
|
2665
|
+
# ---------------------------------------------------------------------------
|
|
2666
|
+
# A branch's own plans
|
|
2667
|
+
# ---------------------------------------------------------------------------
|
|
2668
|
+
#
|
|
2669
|
+
# A plan created with `Impl: same branch` lives ONLY on its work branch until
|
|
2670
|
+
# that branch merges. The enumeration above reads `origin/$MAIN` and finds
|
|
2671
|
+
# nothing, so the branch carrying the plan reached the pulse as an anonymous row
|
|
2672
|
+
# with `plan: ""` while `/api/board` showed the same plan as a Draft card.
|
|
2673
|
+
# Measured on Plot 2.20.0 (#972): two tabs of one board disagreeing about
|
|
2674
|
+
# whether a plan exists, which is worse than either answer alone.
|
|
2675
|
+
#
|
|
2676
|
+
# `board.ts:802-831` already solved this and the RULE is what transfers, not the
|
|
2677
|
+
# call: the scan keeps its own reading (Manifesto Principle 3) and the two stay
|
|
2678
|
+
# separate implementations. `readBranchPlans` is the reference.
|
|
2679
|
+
#
|
|
2680
|
+
# A NARROW READER RATHER THAN A REF ARGUMENT ON THE FOUR HELPERS ABOVE, and the
|
|
2681
|
+
# reason is `$REF_TMP`'s namespace. `ref_plan_file` caches by `basename "$p"` in
|
|
2682
|
+
# one flat directory, deliberately, so the filename a report prints is the
|
|
2683
|
+
# plan's own. Widening that across refs would let `origin/A:docs/plans/x.md` and
|
|
2684
|
+
# `origin/B:docs/plans/x.md` overwrite each other SILENTLY — the dedup's concern
|
|
2685
|
+
# ("one plan reported as several") inverted into two plans reported as one,
|
|
2686
|
+
# carrying whichever content was written last. A per-branch subdirectory makes
|
|
2687
|
+
# the collision unrepresentable instead of unlikely. The batch materialiser at
|
|
2688
|
+
# `:2543` is also built around one ref by construction: it keys a single
|
|
2689
|
+
# `cat-file --batch` stream by basename off one `PLAN_MODES`.
|
|
2690
|
+
#
|
|
2691
|
+
# REGULAR BLOBS ONLY, never mode 120000. A symlink blob holds its TARGET PATH as
|
|
2692
|
+
# content, so parsing one hands plot-plan-meta.sh a line of text where a plan
|
|
2693
|
+
# should be, and `$ACTIVE_DIR`'s links would double-count every indexed plan.
|
|
2694
|
+
# `$PLAN_DIR` alone is listed, with modes — which is also why the ref-space
|
|
2695
|
+
# symlink walk in `ref_plan_file` is deliberately NOT ported here: filtering the
|
|
2696
|
+
# links out first means there is no link left to follow.
|
|
2697
|
+
branch_plan_paths() { # $1=branch → regular .md blob paths under $PLAN_DIR
|
|
2698
|
+
# An UNREADABLE REF CONTRIBUTES NOTHING, silently — an empty set, exactly as
|
|
2699
|
+
# `planPathsInTree` returns. It never produces a blank or guessed plan, and
|
|
2700
|
+
# this is asked once per branch where a fetch may legitimately have missed one.
|
|
2701
|
+
git ls-tree -z "origin/$1" -- "$PLAN_DIR" </dev/null 2>/dev/null \
|
|
2702
|
+
| tr '\0' '\n' \
|
|
2703
|
+
| awk -F'\t' '$1 ~ /^100644 |^100755 / && $2 ~ /\.md$/ { print $2 }' || true
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
# One branch plan blob, materialized where plot-plan-meta.sh can parse it.
|
|
2707
|
+
#
|
|
2708
|
+
# NAMED UNDER A PER-BRANCH DIRECTORY for the collision reason above. The branch
|
|
2709
|
+
# name is sanitized because it carries `/` by convention (`bug/…`), which would
|
|
2710
|
+
# otherwise name a directory that does not exist.
|
|
2711
|
+
branch_plan_file() { # $1=branch, $2=path in that branch → temp file, or ""
|
|
2712
|
+
local branch="$1" p="$2" content dir out
|
|
2713
|
+
[ -n "$REF_TMP" ] || return 1
|
|
2714
|
+
content=$(git show "origin/$branch:$p" </dev/null 2>/dev/null) || return 1
|
|
2715
|
+
# AN EMPTY BLOB IS SKIPPED RATHER THAN PARSED (`board.ts:817-822`): a plan
|
|
2716
|
+
# file with no bytes parses to no phase, and a row with no phase belongs in no
|
|
2717
|
+
# column. The emptiness test sits beside the failure test rather than
|
|
2718
|
+
# replacing it, because the two are different faults.
|
|
2719
|
+
[ -n "$content" ] || return 1
|
|
2720
|
+
dir="$REF_TMP/branch/$(printf '%s' "$branch" | tr '/' '_')"
|
|
2721
|
+
mkdir -p "$dir" 2>/dev/null || return 1
|
|
2722
|
+
out="$dir/$(basename "$p")"
|
|
2723
|
+
printf '%s\n' "$content" > "$out" 2>/dev/null || return 1
|
|
2724
|
+
printf '%s' "$out"
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2628
2727
|
# Resolve which plans to report on.
|
|
2629
2728
|
#
|
|
2630
2729
|
# TWO PARALLEL ARRAYS, because a plan now has two paths that must not be
|
|
@@ -2727,7 +2826,7 @@ is_plan_phase() { # $1=normalized phase → 0 when this file is a plan
|
|
|
2727
2826
|
#
|
|
2728
2827
|
# `record` types, one per line, all tab-separated and all prefixed by the plan
|
|
2729
2828
|
# file they describe:
|
|
2730
|
-
# P <file> <phase>
|
|
2829
|
+
# P <file> <phase> one per parsed file
|
|
2731
2830
|
# W <file> <wave-idx> <branch> <deferred> <why> <wave-name> <claim>
|
|
2732
2831
|
#
|
|
2733
2832
|
# NO ASSOCIATIVE ARRAYS. `/bin/bash` on macOS is 3.2 and this script uses no
|
|
@@ -2745,7 +2844,6 @@ is_plan_phase() { # $1=normalized phase → 0 when this file is a plan
|
|
|
2745
2844
|
# gave when it failed.
|
|
2746
2845
|
plan_meta_files=()
|
|
2747
2846
|
plan_meta_phases=()
|
|
2748
|
-
plan_meta_inwindow=()
|
|
2749
2847
|
plan_meta_waves=()
|
|
2750
2848
|
|
|
2751
2849
|
# Parses every plan file given, filling the four arrays above. Called ONCE.
|
|
@@ -2756,33 +2854,6 @@ parse_plan_estate() { # $@=files to parse
|
|
|
2756
2854
|
| python3 -c '
|
|
2757
2855
|
import json, re, sys, time
|
|
2758
2856
|
|
|
2759
|
-
window = float(sys.argv[1]) * 3600
|
|
2760
|
-
now = time.time()
|
|
2761
|
-
|
|
2762
|
-
def in_window(raw):
|
|
2763
|
-
"""The `Delivered:` record against the rolling window — the same rule the
|
|
2764
|
-
per-plan test applied, moved into the one pass. A record whose date does
|
|
2765
|
-
not parse is dropped rather than coerced: Date-style leniency would turn a
|
|
2766
|
-
typo into a confident answer. A record with no time anchors at 23:59:59,
|
|
2767
|
-
so a plan delivered at 23:50 is not an hour from expiry the moment it is
|
|
2768
|
-
written. A FUTURE record is INSIDE (negative age), because hiding a live
|
|
2769
|
-
plan for a mistyped year costs more than showing one."""
|
|
2770
|
-
raw = (raw or "").strip()
|
|
2771
|
-
if not raw:
|
|
2772
|
-
return False
|
|
2773
|
-
m = re.match(r"(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2}))?", raw)
|
|
2774
|
-
if not m:
|
|
2775
|
-
return False
|
|
2776
|
-
y, mo, dy, hh, mi = m.groups()
|
|
2777
|
-
timed = hh is not None
|
|
2778
|
-
try:
|
|
2779
|
-
at = time.mktime((int(y), int(mo), int(dy),
|
|
2780
|
-
int(hh) if timed else 23, int(mi) if timed else 59,
|
|
2781
|
-
0 if timed else 59, 0, 0, -1))
|
|
2782
|
-
except (ValueError, OverflowError):
|
|
2783
|
-
return False
|
|
2784
|
-
return (now - at) <= window
|
|
2785
|
-
|
|
2786
2857
|
def clean(s):
|
|
2787
2858
|
return str(s).replace("\t", " ").replace("\n", " ")
|
|
2788
2859
|
|
|
@@ -2800,8 +2871,7 @@ for line in sys.stdin:
|
|
|
2800
2871
|
f = d.get("file")
|
|
2801
2872
|
if not f:
|
|
2802
2873
|
continue
|
|
2803
|
-
print("\t".join(["P", clean(f), clean(d.get("phase", ""))
|
|
2804
|
-
"1" if in_window(d.get("delivered_raw")) else "0"]))
|
|
2874
|
+
print("\t".join(["P", clean(f), clean(d.get("phase", ""))]))
|
|
2805
2875
|
for i, w in enumerate(d.get("waves", []) or []):
|
|
2806
2876
|
name = w.get("name")
|
|
2807
2877
|
for b in w.get("branches", []) or []:
|
|
@@ -2828,7 +2898,7 @@ for line in sys.stdin:
|
|
|
2828
2898
|
(b.get("deferred_reason") or "-"),
|
|
2829
2899
|
(b.get("waits_on") or "-"),
|
|
2830
2900
|
name or "-", b.get("claimed") or "-"]))
|
|
2831
|
-
'
|
|
2901
|
+
' 2>/dev/null) || records=""
|
|
2832
2902
|
|
|
2833
2903
|
local kind file rest
|
|
2834
2904
|
while IFS=$'\t' read -r kind file rest; do
|
|
@@ -2836,9 +2906,8 @@ for line in sys.stdin:
|
|
|
2836
2906
|
case "$kind" in
|
|
2837
2907
|
P)
|
|
2838
2908
|
plan_meta_files+=("$file")
|
|
2839
|
-
# `rest` is "<phase
|
|
2840
|
-
plan_meta_phases+=("$
|
|
2841
|
-
plan_meta_inwindow+=("${rest##* }")
|
|
2909
|
+
# `rest` is "<phase>", a single token with no tabs.
|
|
2910
|
+
plan_meta_phases+=("$rest")
|
|
2842
2911
|
plan_meta_waves+=("")
|
|
2843
2912
|
;;
|
|
2844
2913
|
W)
|
|
@@ -2962,8 +3031,8 @@ else
|
|
|
2962
3031
|
# was buying less than it appeared to: it keyed off the `$DELIVERED_DIR`
|
|
2963
3032
|
# symlink's mtime, and a fresh checkout stamps every symlink at once — 56 of
|
|
2964
3033
|
# 56 delivered links admitted here, so the parse it was meant to avoid was
|
|
2965
|
-
# already being paid in full.
|
|
2966
|
-
#
|
|
3034
|
+
# already being paid in full. The `Delivered:` record's window was the filter
|
|
3035
|
+
# that actually decided until 2026-10-01, when the phase alone took over, and the pre-filter's own
|
|
2967
3036
|
# contract was that it may only ever OVER-admit. Removing it takes that
|
|
2968
3037
|
# contract to its limit — strictly more correct, and on this repo not even
|
|
2969
3038
|
# more expensive.
|
|
@@ -3013,6 +3082,56 @@ else
|
|
|
3013
3082
|
cand_ids+=("$plan_path")
|
|
3014
3083
|
cand_reads+=("$plan_blob")
|
|
3015
3084
|
done <<< "$(ref_ls "$PLAN_DIR")"
|
|
3085
|
+
|
|
3086
|
+
# THEN EACH PREFIXED BRANCH'S OWN TREE, for the plans `origin/$MAIN` does
|
|
3087
|
+
# not carry. Appended to the SAME candidate arrays, before the one
|
|
3088
|
+
# `parse_plan_estate` call below: a second call would build a second
|
|
3089
|
+
# `plan_meta_files` index and the lookup at the row loop keys on the file
|
|
3090
|
+
# path, so the branch plans would parse and then be unfindable.
|
|
3091
|
+
#
|
|
3092
|
+
# From here the existing pipeline carries the plan unchanged — it names its
|
|
3093
|
+
# branch in `## Slices`, the wave walk finds it, and the branch stops
|
|
3094
|
+
# reaching the report through the plan-less loop in `fleet.ts`.
|
|
3095
|
+
#
|
|
3096
|
+
# THE DEDUP IS THE BOARD'S, COPIED RATHER THAN RE-DERIVED. `on_default` is
|
|
3097
|
+
# every plan path the default branch carries; `seen_branch_plans` is every
|
|
3098
|
+
# path already taken from an earlier branch. Two branches cut from one point
|
|
3099
|
+
# carry the SAME plan file, and without the second test one plan reports as
|
|
3100
|
+
# several — a regression the board measured and fixed, and the reason its
|
|
3101
|
+
# comment exists.
|
|
3102
|
+
#
|
|
3103
|
+
# WHICH BRANCHES: `REMOTE_REFS`, already read once above, filtered by
|
|
3104
|
+
# `PREFIX_RE` — the same population the board calls a prefixed branch. No
|
|
3105
|
+
# second `for-each-ref`. The narrowing to PR-less branches the slice line
|
|
3106
|
+
# offered was WITHDRAWN by the plan's Design section: it was a fallback
|
|
3107
|
+
# against a cost that does not exist. Measured 2026-09-24, one `ls-tree`
|
|
3108
|
+
# over the plan directory is ~0.00 s and the whole addition 0.24 s, 0.4% of
|
|
3109
|
+
# a scan whose wall time is 95% waiting.
|
|
3110
|
+
on_default=$'\n'"$(ref_ls "$PLAN_DIR")"$'\n'
|
|
3111
|
+
seen_branch_plans=$'\n'
|
|
3112
|
+
while IFS=$'\t' read -r branch_name _branch_sha; do
|
|
3113
|
+
[ -n "$branch_name" ] || continue
|
|
3114
|
+
[ "$branch_name" = "HEAD" ] && continue
|
|
3115
|
+
[ "$branch_name" = "$MAIN" ] && continue
|
|
3116
|
+
printf '%s' "$branch_name" | grep -Eq "^($PREFIX_RE)/" || continue
|
|
3117
|
+
while IFS= read -r bp; do
|
|
3118
|
+
[ -n "$bp" ] || continue
|
|
3119
|
+
case "$on_default" in *$'\n'"$bp"$'\n'*) continue ;; esac
|
|
3120
|
+
case "$seen_branch_plans" in *$'\n'"$bp"$'\n'*) continue ;; esac
|
|
3121
|
+
plan_blob=$(branch_plan_file "$branch_name" "$bp") || continue
|
|
3122
|
+
[ -n "$plan_blob" ] || continue
|
|
3123
|
+
# MARKED SEEN ONLY ONCE IT IS TAKEN, matching `board.ts:823`: a blob
|
|
3124
|
+
# that could not be read has not been reported, so a later branch
|
|
3125
|
+
# carrying a readable copy of the same path must still get its turn.
|
|
3126
|
+
seen_branch_plans="${seen_branch_plans}${bp}"$'\n'
|
|
3127
|
+
# THE IDENTITY STAYS THE RELATIVE PATH. The row loop parses
|
|
3128
|
+
# `plan_reads[i]` and never re-reads by the identity in `plans[i]`, so
|
|
3129
|
+
# the `docs/plans/…md` path is a usable id — and the dedup above is what
|
|
3130
|
+
# guarantees it cannot collide with a default-branch plan's.
|
|
3131
|
+
cand_ids+=("$bp")
|
|
3132
|
+
cand_reads+=("$plan_blob")
|
|
3133
|
+
done <<< "$(branch_plan_paths "$branch_name")"
|
|
3134
|
+
done <<< "$REMOTE_REFS"
|
|
3016
3135
|
else
|
|
3017
3136
|
for plan_path in "$PLAN_DIR"*.md; do
|
|
3018
3137
|
[ -e "$plan_path" ] || continue
|
|
@@ -3066,7 +3185,7 @@ if [ ${#plans[@]} -eq 0 ]; then
|
|
|
3066
3185
|
# the scan globbed it; pointing a reader at the index would now send them to
|
|
3067
3186
|
# look for the cause of an empty list in a directory nothing consults.
|
|
3068
3187
|
echo "No plans found in ${PLAN_DIR}."
|
|
3069
|
-
echo "summary: plans=0 waves=0 branches=0 claimed=0 eligible=0 blocked=0 deferred=0 waiting=0 prereq_missing=0 main=$MAIN"
|
|
3188
|
+
echo "summary: plans=0 waves=0 branches=0 claimed=0 claimable=0 eligible=0 blocked=0 deferred=0 waiting=0 prereq_missing=0 main=$MAIN"
|
|
3070
3189
|
exit 0
|
|
3071
3190
|
fi
|
|
3072
3191
|
fi
|
|
@@ -3161,7 +3280,7 @@ EOF
|
|
|
3161
3280
|
echo "$total $n"
|
|
3162
3281
|
}
|
|
3163
3282
|
|
|
3164
|
-
# WHAT WAS READ OF ONE BRANCH —
|
|
3283
|
+
# WHAT WAS READ OF ONE BRANCH — eleven tab-separated fields, and no decision.
|
|
3165
3284
|
#
|
|
3166
3285
|
# `branch_state()` UNTIL THIS SLICE, and every line of git archaeology below is
|
|
3167
3286
|
# its own, unchanged. What went is the `if` chain that merged these readings
|
|
@@ -3178,10 +3297,11 @@ EOF
|
|
|
3178
3297
|
# run of tabs collapses into one separator under `read`, so no field is ever
|
|
3179
3298
|
# empty. Nothing here is optional, so nothing can shift.
|
|
3180
3299
|
#
|
|
3181
|
-
# EIGHT FIELDS, NOT
|
|
3182
|
-
# what the host said about it — are appended by the caller, because reading
|
|
3183
|
-
# second costs a host round trip and the scan spends it only where it could
|
|
3300
|
+
# EIGHT FIELDS, NOT ELEVEN. The two the plan states — the prerequisite's name
|
|
3301
|
+
# and what the host said about it — are appended by the caller, because reading
|
|
3302
|
+
# the second costs a host round trip and the scan spends it only where it could
|
|
3184
3303
|
# change the answer. The rule reports which states those are; see the caller.
|
|
3304
|
+
# The caller appends the PR list's completeness last, once per run.
|
|
3185
3305
|
#
|
|
3186
3306
|
# THE DEFAULT BRANCH'S TIP IS READ ONCE PER RUN, not once per branch. It does
|
|
3187
3307
|
# not move while the scan runs — every fact below is derived from the ref batch
|
|
@@ -3494,6 +3614,12 @@ n_plans=0 n_waves=0 n_branches=0 n_claimed=0 n_eligible=0 n_blocked=0 n_deferred
|
|
|
3494
3614
|
# the footer keeps the two words apart for that reason.
|
|
3495
3615
|
n_waiting=0 n_prereq_missing=0
|
|
3496
3616
|
claimable=()
|
|
3617
|
+
# HOW MANY BRANCHES AN ELIGIBLE WAVE HOLDS THAT SOMEBODY IS ON — the fact that
|
|
3618
|
+
# separates "this estate has no work" from "every candidate here is taken", and
|
|
3619
|
+
# the only reason `--list-eligible` can say which of the two its silence means.
|
|
3620
|
+
# Counted across eligible waves only: a taken branch in a blocked wave was never
|
|
3621
|
+
# a candidate, so reporting it would name work the offer path never considered.
|
|
3622
|
+
n_taken_in_eligible=0
|
|
3497
3623
|
plan_files=()
|
|
3498
3624
|
# `--why-nothing`'s input: one `verdict<TAB>name:state|...` line per slice, in
|
|
3499
3625
|
# plan order. Accumulated in the SAME loop that renders the branches, so the
|
|
@@ -3524,26 +3650,14 @@ for plan in "${plans[@]}"; do
|
|
|
3524
3650
|
# is a judgment that belongs one layer up (Manifesto Principle 3).
|
|
3525
3651
|
plan_phase="${plan_meta_phases[$meta_i]}"
|
|
3526
3652
|
|
|
3527
|
-
#
|
|
3528
|
-
#
|
|
3529
|
-
#
|
|
3530
|
-
#
|
|
3531
|
-
#
|
|
3532
|
-
#
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
# `Phase: Delivered` was drift the window must not hide; under the phase rule
|
|
3536
|
-
# that drift cannot be constructed, because there is no second place for the
|
|
3537
|
-
# answer to live. One source, so nothing to disagree.
|
|
3538
|
-
#
|
|
3539
|
-
# Two exits, and both matter:
|
|
3540
|
-
# * the record's date has aged out of the window — ordinary expiry;
|
|
3541
|
-
# * there is NO record — "no date, no row". `reconcile-scan-accuracy.md` is
|
|
3542
|
-
# the live example; showing it would create the one row that can never
|
|
3543
|
-
# age out of DONE.
|
|
3544
|
-
# Both leave before a single git call is spent on the plan's branches.
|
|
3545
|
-
if is_terminal_phase "$plan_phase"; then
|
|
3546
|
-
[ "${plan_meta_inwindow[$meta_i]}" = "1" ] || continue
|
|
3653
|
+
# THE RELEASE SCOPE, decided by the PHASE alone. A terminal plan stays in the
|
|
3654
|
+
# pulse only while it is `delivered`: its work has landed and its version has
|
|
3655
|
+
# not shipped, which is what DONE holds. `released`, `rejected` and
|
|
3656
|
+
# `superseded` leave, whatever their age, and leave before a single git call
|
|
3657
|
+
# is spent on their branches. No date bounds it, so a plan delivered weeks
|
|
3658
|
+
# before a release still names the release's contents.
|
|
3659
|
+
if is_terminal_phase "$plan_phase" && [ "$plan_phase" != "delivered" ]; then
|
|
3660
|
+
continue
|
|
3547
3661
|
fi
|
|
3548
3662
|
|
|
3549
3663
|
n_plans=$((n_plans + 1))
|
|
@@ -3641,12 +3755,17 @@ for plan in "${plans[@]}"; do
|
|
|
3641
3755
|
# deciding it — see pass 1c.
|
|
3642
3756
|
readings=""
|
|
3643
3757
|
order=""
|
|
3758
|
+
# THE ELEVENTH FIELD: whether the PR list held every PR (`.list-complete`,
|
|
3759
|
+
# written by `prefill_pr_states`). The rule reads a `NONE` for a ref behind
|
|
3760
|
+
# main as `open` only when it is true; a capped list may omit a merged PR.
|
|
3761
|
+
list_complete=false
|
|
3762
|
+
[ -n "$HOST_STATE_CACHE" ] && [ -f "$HOST_STATE_CACHE/.list-complete" ] && list_complete=true
|
|
3644
3763
|
while IFS=$'\t' read -r idx br deferred why waits wname claim; do
|
|
3645
3764
|
[ -n "$br" ] || continue
|
|
3646
3765
|
# "-" is the absent marker the shim writes, for the tab-collapse reason
|
|
3647
3766
|
# above. Normalized here so everything downstream tests emptiness.
|
|
3648
3767
|
[ "$waits" = "-" ] && waits=""
|
|
3649
|
-
readings+="$(branch_readings "$br" "$deferred") ${waits:--} ?"$'\n'
|
|
3768
|
+
readings+="$(branch_readings "$br" "$deferred") ${waits:--} ? $list_complete"$'\n'
|
|
3650
3769
|
order+="$idx $br $deferred $why ${waits:--} $wname $claim"$'\n'
|
|
3651
3770
|
done <<< "$wave_lines"
|
|
3652
3771
|
|
|
@@ -3701,7 +3820,8 @@ for plan in "${plans[@]}"; do
|
|
|
3701
3820
|
# list may legitimately omit: its plan may be delivered and its ref gone.
|
|
3702
3821
|
# `host_pr_state`'s run cache keeps this at one call per prerequisite per
|
|
3703
3822
|
# run, never one per pass.
|
|
3704
|
-
|
|
3823
|
+
# Field 10 is replaced; field 11 is carried.
|
|
3824
|
+
refill+="$(printf '%s' "$rd_line" | cut -f1-9) $(waits_pr_state "$waits_br") $(printf '%s' "$rd_line" | cut -f11)"$'\n'
|
|
3705
3825
|
else
|
|
3706
3826
|
refill+="$rd_line"$'\n'
|
|
3707
3827
|
fi
|
|
@@ -3807,16 +3927,36 @@ for plan in "${plans[@]}"; do
|
|
|
3807
3927
|
wname=$(printf '%s' "$states" | awk -F'\t' -v w="$wid" '$1==w {print $7; exit}')
|
|
3808
3928
|
[ "$wname" = "-" ] && wname=""
|
|
3809
3929
|
|
|
3810
|
-
|
|
3930
|
+
# THE HEADER IS HELD, NOT PRINTED — it carries a suffix that only the
|
|
3931
|
+
# branch loop below can decide. `eligible` answers *are this wave's
|
|
3932
|
+
# prerequisites met?* while every offer path answers *can a branch here be
|
|
3933
|
+
# claimed now?*, and a wave whose branches are all taken satisfies the first
|
|
3934
|
+
# and not the second. Reported 2026-09-25 (#994) from a Bitbucket estate:
|
|
3935
|
+
# one wave `eligible`, eleven branches, both offer paths silent. The two
|
|
3936
|
+
# computations are correct and the word was carrying both answers.
|
|
3937
|
+
#
|
|
3938
|
+
# The line still prints BEFORE its branches — only the decision is delayed,
|
|
3939
|
+
# not the reading order — so the branch lines are buffered and flushed with
|
|
3940
|
+
# it. There is exactly one `echo` in that loop, which is why buffering it
|
|
3941
|
+
# costs less than threading the wave header past the six counters,
|
|
3942
|
+
# `json_branches` and `outlook_branches` the loop also builds.
|
|
3943
|
+
wave_header=" ${wname:-(unnamed)} — $verdict"
|
|
3811
3944
|
# A degradation that says nothing is indistinguishable from a bug.
|
|
3812
3945
|
# When --loose falls back to strict because the rollup cannot be had,
|
|
3813
3946
|
# say so — an operator who passed the flag and sees strict behaviour
|
|
3814
3947
|
# has no way to tell "the rollup said not-green" from "the rollup
|
|
3815
3948
|
# could not be had". Both are correct refusals; only one is about
|
|
3816
3949
|
# their PR.
|
|
3950
|
+
wave_body=""
|
|
3817
3951
|
if [ "$quiet" != 1 ] && [ "$loose" = 1 ] && [ -n "$_loose_degraded_branches" ]; then
|
|
3818
|
-
|
|
3952
|
+
wave_body+=" (--loose degraded to strict: checks unavailable for ${_loose_degraded_branches})"$'\n'
|
|
3819
3953
|
fi
|
|
3954
|
+
# HOW MANY BRANCHES OF THIS WAVE ARE TAKEN, and how many claimable. Counted
|
|
3955
|
+
# here rather than derived from the estate-wide `n_eligible`, which spans
|
|
3956
|
+
# plans. `unknown` is in neither: the host could not be asked, so the branch
|
|
3957
|
+
# is not taken and not free, and a wave holding one must never be reported
|
|
3958
|
+
# as somebody else's work.
|
|
3959
|
+
wave_taken=0 wave_free=0
|
|
3820
3960
|
json_branches=""
|
|
3821
3961
|
# The outlook's reading of this slice, built alongside the render. EVERY
|
|
3822
3962
|
# branch including the deferred ones, in the plan's order — the rule needs
|
|
@@ -3867,8 +4007,17 @@ for plan in "${plans[@]}"; do
|
|
|
3867
4007
|
if [ "${wave_claimable:$((branch_i - 1)):1}" = "1" ]; then
|
|
3868
4008
|
n_eligible=$((n_eligible + 1))
|
|
3869
4009
|
claimable+=("$br")
|
|
4010
|
+
wave_free=$((wave_free + 1))
|
|
3870
4011
|
fi
|
|
3871
|
-
|
|
4012
|
+
# TAKEN MEANS SOMEBODY HAS IT — claimed, or carrying pushed work. Read
|
|
4013
|
+
# from the branch's own state rather than from the claimable flag above,
|
|
4014
|
+
# because that flag is 0 for six different reasons and only these two mean
|
|
4015
|
+
# a person is on it. `unknown` is excluded by naming the two states rather
|
|
4016
|
+
# than by negating the flag.
|
|
4017
|
+
case "$st" in
|
|
4018
|
+
claimed|wip) wave_taken=$((wave_taken + 1)) ;;
|
|
4019
|
+
esac
|
|
4020
|
+
wave_body+=" $br — $note"$'\n'
|
|
3872
4021
|
if [ "$build_doc" = 1 ]; then
|
|
3873
4022
|
# The INTERNAL state ($st), never the prose label ($note): the board
|
|
3874
4023
|
# must not parse a string that exists for humans to read.
|
|
@@ -4067,6 +4216,45 @@ for plan in "${plans[@]}"; do
|
|
|
4067
4216
|
fi
|
|
4068
4217
|
done <<< "$states"
|
|
4069
4218
|
|
|
4219
|
+
# WHICH QUESTION THE WORD ANSWERED, said in the estate's existing
|
|
4220
|
+
# vocabulary. `someone-is-on-it` is `StartabilityVerdictSchema`'s word
|
|
4221
|
+
# (`packages/board/src/contract/schema.ts`) for exactly these two branch
|
|
4222
|
+
# states, so the wave line and its branch lines now agree rather than
|
|
4223
|
+
# offering a reader two answers to compare.
|
|
4224
|
+
#
|
|
4225
|
+
# A PROSE SUFFIX, NOT A SIXTH VERDICT. `$verdict` is untouched and reaches
|
|
4226
|
+
# `--json`, `--stream` and the outlook byte-identical: `FleetWaveSchema`'s
|
|
4227
|
+
# verdict is a strict enum, so a parsed pulse cannot carry a new word and
|
|
4228
|
+
# widening it is a separate change with its own consumers.
|
|
4229
|
+
#
|
|
4230
|
+
# THE RULE, STATED: a wave line carries BOTH answers — the verdict, then who
|
|
4231
|
+
# has it — and the suffix appears exactly when the wave is `eligible`, holds
|
|
4232
|
+
# no free branch, and holds at least one branch that is `claimed` or `wip`.
|
|
4233
|
+
# A wave with a free branch keeps bare `eligible`, which is what the offer
|
|
4234
|
+
# paths will confirm; a wave whose only non-free branches are `unknown`
|
|
4235
|
+
# keeps it too, because the host could not be asked and nobody has claimed
|
|
4236
|
+
# anything.
|
|
4237
|
+
#
|
|
4238
|
+
# `wip` COUNTS AS TAKEN, which the plan settles three times: *"a branch
|
|
4239
|
+
# counts as taken when it is claimed or in progress"*. A one-branch wave
|
|
4240
|
+
# whose branch is `wip` therefore reads `eligible — someone-is-on-it`, and
|
|
4241
|
+
# two tests in `fleet.test.mjs` asserted `/ — eligible$/` on exactly that
|
|
4242
|
+
# shape. Their subject is the VERDICT — *"has not settled its wave"*,
|
|
4243
|
+
# *"the wave must stay open"* — so the anchor moved to the verdict rather
|
|
4244
|
+
# than the suffix being suppressed; the `$` was free before a suffix existed
|
|
4245
|
+
# and asserted a second thing neither test meant. The other five sites
|
|
4246
|
+
# carrying that anchor hold a free branch and are untouched.
|
|
4247
|
+
if [ "$verdict" = "eligible" ] && [ "$wave_free" -eq 0 ] && [ "$wave_taken" -gt 0 ]; then
|
|
4248
|
+
wave_header+=" — someone-is-on-it"
|
|
4249
|
+
fi
|
|
4250
|
+
if [ "$verdict" = "eligible" ]; then
|
|
4251
|
+
n_taken_in_eligible=$((n_taken_in_eligible + wave_taken))
|
|
4252
|
+
fi
|
|
4253
|
+
if [ "$quiet" != 1 ]; then
|
|
4254
|
+
echo "$wave_header"
|
|
4255
|
+
[ -n "$wave_body" ] && printf '%s' "$wave_body"
|
|
4256
|
+
fi
|
|
4257
|
+
|
|
4070
4258
|
if [ "$build_doc" = 1 ]; then
|
|
4071
4259
|
json_waves+="${json_waves:+,}{\"name\":\"$(json_str "$wname")\""
|
|
4072
4260
|
json_waves+=",\"verdict\":\"$verdict\",\"branches\":[$json_branches]}"
|
|
@@ -4148,6 +4336,27 @@ fi
|
|
|
4148
4336
|
# "Nothing to start" is a normal state, not a failure — the exit code is what
|
|
4149
4337
|
# distinguishes it from a name, so callers can branch on it without parsing.
|
|
4150
4338
|
if [ "$next_only" = 1 ]; then
|
|
4339
|
+
# WHY THE LIST IS EMPTY, for `--list-eligible` only, and ON STDERR. An
|
|
4340
|
+
# operator reading the body's `eligible` and then getting silence here cannot
|
|
4341
|
+
# tell a claimed-out estate from an empty one, and #994 is that reading: one
|
|
4342
|
+
# wave eligible, eleven branches, nothing offered. The exit code still carries
|
|
4343
|
+
# the answer — 1 either way — so no caller's gate moves.
|
|
4344
|
+
#
|
|
4345
|
+
# STDERR BECAUSE STDOUT IS A TARGET LIST. `plot-dispatch.sh:3475` pipes this
|
|
4346
|
+
# stdout through `sort -u` and dispatches every line, so a sentence there
|
|
4347
|
+
# becomes a branch name it tries to claim. That caller already discards
|
|
4348
|
+
# stderr (`2>/dev/null`), so the sentence reaches a person and no machine.
|
|
4349
|
+
#
|
|
4350
|
+
# `--next` IS UNTOUCHED: its exit-1 contract is shipped, documented and
|
|
4351
|
+
# tested twice, and it names ONE branch for a worker that has nothing to read
|
|
4352
|
+
# a sentence with.
|
|
4353
|
+
if [ ${#claimable[@]} -eq 0 ] && [ "$list_all" = 1 ]; then
|
|
4354
|
+
if [ "$n_taken_in_eligible" -gt 0 ]; then
|
|
4355
|
+
echo "nothing claimable: $n_taken_in_eligible branch(es) in eligible waves are taken." >&2
|
|
4356
|
+
else
|
|
4357
|
+
echo "nothing claimable: no eligible wave holds a startable branch." >&2
|
|
4358
|
+
fi
|
|
4359
|
+
fi
|
|
4151
4360
|
[ ${#claimable[@]} -gt 0 ] || exit 1
|
|
4152
4361
|
if [ "$list_all" = 1 ]; then
|
|
4153
4362
|
printf '%s\n' "${claimable[@]}"
|
|
@@ -4455,4 +4664,4 @@ if [ "$build_doc" = 1 ] && [ "$record" = 1 ] && [ -n "$reading_doc" ]; then
|
|
|
4455
4664
|
fi
|
|
4456
4665
|
write_bridge
|
|
4457
4666
|
echo "Pulse complete. This report is derived — nothing was changed."
|
|
4458
|
-
echo "summary: plans=$n_plans waves=$n_waves branches=$n_branches claimed=$n_claimed eligible=$n_eligible blocked=$n_blocked deferred=$n_deferred waiting=$n_waiting prereq_missing=$n_prereq_missing merge_detect=$MERGE_DETECT host=$HOST_VERDICT main=$MAIN"
|
|
4667
|
+
echo "summary: plans=$n_plans waves=$n_waves branches=$n_branches claimed=$n_claimed claimable=$n_eligible eligible=$n_eligible blocked=$n_blocked deferred=$n_deferred waiting=$n_waiting prereq_missing=$n_prereq_missing merge_detect=$MERGE_DETECT host=$HOST_VERDICT main=$MAIN"
|