@plot-pm/board 0.16.1 → 0.16.3
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 +167 -139
- package/package.json +2 -3
- package/plot-agent-manifest.sh +111 -15
- package/plot-agent-monitor.sh +43 -5
- package/plot-approve.sh +234 -74
- package/plot-build-monitor.sh +49 -4
- package/plot-config.sh +10 -14
- package/plot-deliver.sh +10 -732
- package/plot-desk-root.sh +127 -0
- package/plot-dispatch.sh +313 -135
- package/plot-fleet-scan.sh +746 -62
- package/plot-host.sh +356 -43
- package/plot-monitor-subject.sh +55 -3
- package/plot-plan-meta.sh +155 -139
- package/plot-pr-merged.sh +16 -0
- package/plot-reap.sh +82 -93
- package/plot-state-receipt.sh +12 -2
- package/plot-tmp.sh +90 -14
- package/plot-transcript-quiet.sh +3 -1
- package/plot-worker-state.sh +633 -24
- package/plot-resolve-artifact.sh +0 -429
- package/plot-worker-monitor.sh +0 -705
package/plot-host.sh
CHANGED
|
@@ -71,7 +71,54 @@
|
|
|
71
71
|
# pr-merge <number> [--squash] [--delete-branch]
|
|
72
72
|
# pr-ready <number> take a PR out of draft
|
|
73
73
|
# merge the PR
|
|
74
|
-
# pr-list [--state open|merged|closed|all] [--limit N] [--rich]
|
|
74
|
+
# pr-list [--state open|merged|closed|all] [--limit N] [--rich|--rich-open]
|
|
75
|
+
# --rich-open asks the rich fields of the OPEN
|
|
76
|
+
# pull requests only, and emits every row in the
|
|
77
|
+
# --rich shape. A MERGED or CLOSED row carries
|
|
78
|
+
# checks:"unknown", mergeable:"unknown",
|
|
79
|
+
# review:"" and failing_checks:[] — the ABSENT
|
|
80
|
+
# values the Bitbucket arm already emits, which
|
|
81
|
+
# mean NOT ASKED and never "no checks".
|
|
82
|
+
# WHY: the four rich fields are verdicts about a
|
|
83
|
+
# PR's HEAD, and a terminal head is finished —
|
|
84
|
+
# no check will run against it and no review will
|
|
85
|
+
# change. Measured 2026-10-01 on this
|
|
86
|
+
# repository, `--rich --state all --limit 1000`
|
|
87
|
+
# took 43.0 s against 7.3 s without the rich
|
|
88
|
+
# fields, and all 1000 rows were terminal, so
|
|
89
|
+
# 36 s bought answers nobody can use.
|
|
90
|
+
# Re-measured 2026-10-02 with 3 PRs open:
|
|
91
|
+
# `--rich --state open` 2.6-3.3 s, `--state all`
|
|
92
|
+
# 8.6-10.5 s, the split read 12.2-13.5 s.
|
|
93
|
+
# ON GITHUB IT IS TWO CALLS and the terminal
|
|
94
|
+
# rows come from the plain one; a PR open in both
|
|
95
|
+
# payloads is emitted once, from the rich call.
|
|
96
|
+
# A FAILURE IN EITHER CALL PRINTS NO ROWS AND
|
|
97
|
+
# EXITS NON-ZERO — a half answer reported as
|
|
98
|
+
# whole is what would let a caller fold a store
|
|
99
|
+
# that lost every open PR.
|
|
100
|
+
# ON BITBUCKET IT ANSWERS EXACTLY AS --rich,
|
|
101
|
+
# because that arm asks the host no verdict: its
|
|
102
|
+
# rows are already `unknown` whatever the state,
|
|
103
|
+
# so there is no second call to save.
|
|
104
|
+
# THE CALLER STILL ASKS ONCE. One `pr-list`
|
|
105
|
+
# invocation is one host question however many
|
|
106
|
+
# calls this makes, so a caller's per-refresh
|
|
107
|
+
# arithmetic is unchanged.
|
|
108
|
+
# `--limit` BOUNDS EACH CALL, NOT THE UNION, so
|
|
109
|
+
# the row count can exceed it by up to the number
|
|
110
|
+
# of open pull requests. Measured 2026-10-02 with
|
|
111
|
+
# 5 open: `--limit 5` answered 6 rows, where
|
|
112
|
+
# `--rich` answered 5 — the open call returned an
|
|
113
|
+
# open PR that fell outside the plain call's
|
|
114
|
+
# newest-5 page. It is NOT bounded per call and
|
|
115
|
+
# then trimmed, because trimming would drop
|
|
116
|
+
# either a verdict the rich call just bought or a
|
|
117
|
+
# terminal row the store needs, and the caller
|
|
118
|
+
# cannot say which. Harmless where `--limit`
|
|
119
|
+
# exceeds the open count, which is every caller
|
|
120
|
+
# today: the board asks 1000 against 5 open, and
|
|
121
|
+
# no other caller passes this flag.
|
|
75
122
|
# [--since <iso>] narrows the listing to pull
|
|
76
123
|
# requests the host has seen change since that
|
|
77
124
|
# stamp. Measured 2026-09-21 on this repository:
|
|
@@ -86,16 +133,19 @@
|
|
|
86
133
|
# two seconds fast excludes the PRs updated in
|
|
87
134
|
# that gap from every later window — forever,
|
|
88
135
|
# because the window never reopens.
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
136
|
+
# BOTH HOSTS NARROW, BY DIFFERENT ROUTES.
|
|
137
|
+
# `gh pr list` takes `--search`. `bb pr list`
|
|
138
|
+
# has no query flag (verified against bb 1.9.0:
|
|
139
|
+
# `unknown flag: --query`), so a windowed
|
|
140
|
+
# Bitbucket listing goes through
|
|
141
|
+
# `bb_window_listing`, which asks REST with
|
|
142
|
+
# `q=state="..." AND updated_on>="..."` and
|
|
143
|
+
# walks every page. It refuses a window whose
|
|
144
|
+
# rows fall short of the server's `size` rather
|
|
145
|
+
# than printing it as whole: a full answer
|
|
146
|
+
# reported as a delta is what would let a caller
|
|
147
|
+
# advance a watermark over a window it never
|
|
148
|
+
# applied.
|
|
99
149
|
# [--repo <owner/repo>] pins the list to ONE
|
|
100
150
|
# repository, exactly as pr-state and pr-merged
|
|
101
151
|
# do. A checkout with remotes on two hosts lets
|
|
@@ -247,7 +297,9 @@
|
|
|
247
297
|
# list` (no --json), pinned to bb 0.6.0. EXIT 4
|
|
248
298
|
# narrows rather than disappears: it is the
|
|
249
299
|
# tracker-DISABLED case (bb answers 404/410),
|
|
250
|
-
#
|
|
300
|
+
# and a bb whose `--help` lists no `issue`
|
|
301
|
+
# command (Quatico bb), both of
|
|
302
|
+
# which stay *this host cannot answer* where an
|
|
251
303
|
# empty list would say *there are none*. A call
|
|
252
304
|
# that failed on an enabled tracker, or any error
|
|
253
305
|
# wording this adapter does not recognise, exits
|
|
@@ -281,7 +333,8 @@
|
|
|
281
333
|
# Same three outcomes as issue-list, same codes:
|
|
282
334
|
# BITBUCKET NOW ANSWERS via `bb issue view`
|
|
283
335
|
# (pinned to 0.6.0); `url` comes from the view's
|
|
284
|
-
# footer. EXIT 4 is the tracker-DISABLED case
|
|
336
|
+
# footer. EXIT 4 is the tracker-DISABLED case
|
|
337
|
+
# or a bb with no `issue` command,
|
|
285
338
|
# EXIT 3 a lookup that failed or an unrecognised
|
|
286
339
|
# error. An issue that does not exist is a
|
|
287
340
|
# FAILURE here, not an empty body: the caller
|
|
@@ -468,11 +521,22 @@ die6() { echo "plot-host: $*" >&2; exit 6; }
|
|
|
468
521
|
# secondary message contains the phrase *"rate limit"* too — *"You have
|
|
469
522
|
# exceeded a secondary rate limit"* — so a quota test applied first claims
|
|
470
523
|
# every secondary refusal and the distinction is lost at the point it is made.
|
|
471
|
-
|
|
524
|
+
#
|
|
525
|
+
# FOUR ANSWERS, NOT THREE — #1087: a GraphQL 504 matched neither limit pattern,
|
|
526
|
+
# so it came back `failed`, and `failed` is the one kind a command fixes —
|
|
527
|
+
# `gh auth login`, wrong advice for a server that took too long to answer.
|
|
528
|
+
# `timeout` is anchored on the HTTP status word (`HTTP 50[234]`), never a bare
|
|
529
|
+
# `\b504\b`, which would match a run id or a line count just as well. It runs
|
|
530
|
+
# AFTER both limit arms: a rate-limit message that also carries a 5xx status
|
|
531
|
+
# must stay `throttled`, because patience until the reset is the right advice
|
|
532
|
+
# for it, and `timeout` would counsel exactly the wrong patience — none.
|
|
533
|
+
host_failure_kind() { # $1=stderr text → secondary|throttled|timeout|failed
|
|
472
534
|
if LC_ALL=C grep -qiE 'secondary rate|exceeded a secondary|abuse detection|abuse-detection|too many requests|\b429\b' <<<"$1"; then
|
|
473
535
|
echo secondary
|
|
474
536
|
elif LC_ALL=C grep -qiE 'rate limit|ratelimit' <<<"$1"; then
|
|
475
537
|
echo throttled
|
|
538
|
+
elif LC_ALL=C grep -qiE "HTTP 50[234]|couldn.t respond to your request in time" <<<"$1"; then
|
|
539
|
+
echo timeout
|
|
476
540
|
else
|
|
477
541
|
echo failed
|
|
478
542
|
fi
|
|
@@ -552,6 +616,16 @@ pr_list_failed() { # $1=stderr text
|
|
|
552
616
|
echo " or run against an account with quota left. No login will help." >&2
|
|
553
617
|
exit 5
|
|
554
618
|
;;
|
|
619
|
+
timeout)
|
|
620
|
+
# NO REPAIR NAMED, FOR THE SAME REASON SECONDARY NAMES NONE — #1087. A
|
|
621
|
+
# 504 means the server took too long, not that the credential is bad, so
|
|
622
|
+
# `host_repair`'s `gh auth login` would send a reader to fix a login
|
|
623
|
+
# that was never broken. The decision is to ask again, not to log in.
|
|
624
|
+
echo "plot-host: pr-list: host timed out — ${err:-the host took too long and said nothing}" >&2
|
|
625
|
+
echo " Nothing is wrong with the login: the server took too long to answer." >&2
|
|
626
|
+
echo " The next refresh asks again." >&2
|
|
627
|
+
exit 3
|
|
628
|
+
;;
|
|
555
629
|
esac
|
|
556
630
|
# THE ONE KIND A COMMAND FIXES. An auth gap and a DNS blip both land here, and
|
|
557
631
|
# the repair for the first is a login this connector can name — see
|
|
@@ -1016,7 +1090,7 @@ bb_state_listing() { # global bb args… --state <s> --json → one JSON array
|
|
|
1016
1090
|
esac
|
|
1017
1091
|
done
|
|
1018
1092
|
[ -n "$_st" ] || die "bb_state_listing: no --state"
|
|
1019
|
-
_out="$(bb ${_args[@]+"${_args[@]}"} api "/repositories/{ws}/{repo}/pullrequests?state=$(bb_query_state "$_st")&pagelen
|
|
1093
|
+
_out="$(bb ${_args[@]+"${_args[@]}"} api "/repositories/{ws}/{repo}/pullrequests?state=$(bb_query_state "$_st")&pagelen=$BB_LIST_PAGELEN")" || return $?
|
|
1020
1094
|
printf '%s' "$_out" | jq -c '.values // []'
|
|
1021
1095
|
}
|
|
1022
1096
|
|
|
@@ -2033,17 +2107,41 @@ bb_issue_exit_code() {
|
|
|
2033
2107
|
# fails LOUDLY rather than silently mis-reading a column that may have moved.
|
|
2034
2108
|
# `PLOT_BB_SKIP_VERSION_CHECK` exists for the test harness, whose stub bb has no
|
|
2035
2109
|
# meaningful version — the parse is exercised against captured fixture text.
|
|
2110
|
+
#
|
|
2111
|
+
# A VERSION MISMATCH IS NOT ALWAYS A MOVED FORMAT. Two products share the name
|
|
2112
|
+
# `bb` (see the capability check below), and Quatico `bb` — 1.9.0 measured
|
|
2113
|
+
# 2026-10-01 — has no `issue` command at all: `bb --help` lists `pr …`,
|
|
2114
|
+
# `source …` and `api`. Telling it to update would suggest a fix that does not
|
|
2115
|
+
# exist, so a mismatch first asks `bb --help` what the CLI offers. A help text
|
|
2116
|
+
# that lists a `pr` command and no `issue` command is a bb that cannot be asked
|
|
2117
|
+
# about issues, which is exit 4 — the same answer as a disabled tracker. Any
|
|
2118
|
+
# other help text keeps the refusal below, exit 3. Exits 4 or 3; 0 passes.
|
|
2036
2119
|
bb_assert_issue_version() {
|
|
2037
2120
|
[ -n "${PLOT_BB_SKIP_VERSION_CHECK:-}" ] && return 0
|
|
2038
2121
|
local v
|
|
2039
2122
|
v="$(bb --version 2>/dev/null | bb_strip_ansi | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
|
|
2040
2123
|
if [ "$v" != "$BB_ISSUE_VERSION" ]; then
|
|
2124
|
+
if bb_lacks_issue_command; then
|
|
2125
|
+
echo "plot-host: bb ${v:-unknown} lists no issue command — Quatico bb has none, unlike craftamap/bb — so this repository's issues cannot be read through it; declare the issue tracker with the \`Tracker\` config key" >&2
|
|
2126
|
+
return 4
|
|
2127
|
+
fi
|
|
2041
2128
|
echo "plot-host: bb issue parse targets $BB_ISSUE_VERSION but found '${v:-unknown}' — refusing to mis-read a format that may have moved" >&2
|
|
2042
2129
|
return 3
|
|
2043
2130
|
fi
|
|
2044
2131
|
return 0
|
|
2045
2132
|
}
|
|
2046
2133
|
|
|
2134
|
+
# Whether `bb --help` lists commands and `issue` is not one of them. A help
|
|
2135
|
+
# that lists no `pr` command either is not a command listing this can read, so
|
|
2136
|
+
# it answers *not proven* (1) and the caller keeps its exit-3 refusal: guessing
|
|
2137
|
+
# 4 from unreadable help would turn a broken bb into *no issue tracker*.
|
|
2138
|
+
bb_lacks_issue_command() {
|
|
2139
|
+
local help
|
|
2140
|
+
help="$(bb --help 2>&1 | bb_strip_ansi)" || return 1
|
|
2141
|
+
grep -qE '^[[:space:]]+pr([[:space:]]|$)' <<<"$help" || return 1
|
|
2142
|
+
! grep -qE '^[[:space:]]+issues?([[:space:]]|$)' <<<"$help"
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2047
2145
|
# --- bb capability check (--json support) ------------------------------------
|
|
2048
2146
|
#
|
|
2049
2147
|
# TWO TOOLS SHARE THE NAME `bb`. craftamap/bb is a Go binary that does NOT
|
|
@@ -2254,6 +2352,47 @@ tracker_scheme() {
|
|
|
2254
2352
|
tracker_raw | awk '{print tolower($1)}'
|
|
2255
2353
|
}
|
|
2256
2354
|
|
|
2355
|
+
# WHO ANSWERS THE OPEN-ISSUE LIST — the domain's answer, not this script's.
|
|
2356
|
+
#
|
|
2357
|
+
# `issue-list` and `issue-view` tested `tracker_scheme = jira` and sent every
|
|
2358
|
+
# other scheme to the git host. That test was a DRIFTED SECOND COPY of the rule:
|
|
2359
|
+
# measured 2026-10-01, `PLOT_HOST=github PLOT_TRACKER=linear issue-list` exited 0
|
|
2360
|
+
# having called `gh issue list`, so a repository tracking in Linear was shown
|
|
2361
|
+
# GitHub's issues under Linear's name — a list not wrong about any single row
|
|
2362
|
+
# and wrong about all of them.
|
|
2363
|
+
#
|
|
2364
|
+
# So the rule is ASKED rather than re-implemented. `issueSource` has eight unit
|
|
2365
|
+
# tests and the board already asks it (`fleet.ts:2369-2380`); a lister list in
|
|
2366
|
+
# shell would be the same copy one directory over, free to drift again the first
|
|
2367
|
+
# time a connector is added.
|
|
2368
|
+
#
|
|
2369
|
+
# THE ONE NODE HOP THIS SCRIPT MAKES, and the cost rule permits it: `plot-host.sh`
|
|
2370
|
+
# runs once per operator command or once per board PR refresh, never once per
|
|
2371
|
+
# agent per pass, and a shipped bundle answers in 39 ms
|
|
2372
|
+
# (`docs/shell-and-domain.md`).
|
|
2373
|
+
#
|
|
2374
|
+
# Prints the entry's line verbatim — `tracker\t<scheme>`, `git-host`, or
|
|
2375
|
+
# `nobody\t<reason>` — and exits 0. Exits 1, naming the entry, when the rule
|
|
2376
|
+
# CANNOT BE ASKED: no node, no bundle, a non-zero exit, or a word outside the
|
|
2377
|
+
# three. THAT CASE NEVER FALLS THROUGH TO THE GIT HOST — the fall-through is
|
|
2378
|
+
# the defect, and `an-outage-is-not-an-answer` is what a list saying *none*
|
|
2379
|
+
# because it could not ask reproduces.
|
|
2380
|
+
issue_source() {
|
|
2381
|
+
local out rc
|
|
2382
|
+
out="$(tracker_raw | node "$here/board/plot-issue-source.mjs" "$be" 2>/dev/null)"; rc=$?
|
|
2383
|
+
if [ "$rc" -ne 0 ]; then
|
|
2384
|
+
echo "plot-host: cannot ask board/plot-issue-source.mjs who lists this repository's issues (exit $rc); the question failed and the git host is not asked in its place" >&2
|
|
2385
|
+
return 1
|
|
2386
|
+
fi
|
|
2387
|
+
case "${out%%$'\t'*}" in
|
|
2388
|
+
tracker|git-host|nobody) printf '%s\n' "$out" ;;
|
|
2389
|
+
*)
|
|
2390
|
+
echo "plot-host: board/plot-issue-source.mjs answered '${out}', which is not one of tracker/git-host/nobody; the question failed and the git host is not asked in its place" >&2
|
|
2391
|
+
return 1
|
|
2392
|
+
;;
|
|
2393
|
+
esac
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2257
2396
|
tracker_base_url() {
|
|
2258
2397
|
# The base URL is the SECOND token; a bare `jira` with no URL yields "".
|
|
2259
2398
|
# PLOT_JIRA_BASE_URL overrides, for a caller that has the URL separately.
|
|
@@ -2516,27 +2655,36 @@ jira_check() {
|
|
|
2516
2655
|
# `bb` returned 50 merged PRs (ids 836→787) against a repo numbering to 836, so
|
|
2517
2656
|
# ~780 older merged PRs were invisible to the join.
|
|
2518
2657
|
#
|
|
2519
|
-
# THE
|
|
2520
|
-
#
|
|
2521
|
-
#
|
|
2658
|
+
# THE RULE IS `rules/listing-page.ts`'s `pagePossiblyTruncated`, and this is
|
|
2659
|
+
# its shell copy. `pr-list` runs on every board refresh, so the shell keeps the
|
|
2660
|
+
# rule rather than asking a bundle (docs/shell-and-domain.md), and
|
|
2661
|
+
# `packages/domain/corpus/listing-page.corpus.test.ts` holds the pair:
|
|
2522
2662
|
#
|
|
2523
2663
|
# github (HONOURS --limit) : a state is possibly truncated when it returned
|
|
2524
|
-
# AT LEAST the requested limit
|
|
2525
|
-
#
|
|
2526
|
-
#
|
|
2527
|
-
#
|
|
2528
|
-
#
|
|
2529
|
-
#
|
|
2530
|
-
#
|
|
2531
|
-
#
|
|
2532
|
-
#
|
|
2533
|
-
#
|
|
2534
|
-
#
|
|
2535
|
-
#
|
|
2536
|
-
#
|
|
2537
|
-
#
|
|
2538
|
-
#
|
|
2539
|
-
#
|
|
2664
|
+
# AT LEAST the requested limit. Fewer rows than
|
|
2665
|
+
# the limit PROVES completeness.
|
|
2666
|
+
# bitbucket (ONE FIXED PAGE): the listing ignores --limit and returns one page
|
|
2667
|
+
# per state. A page SHORTER than the page length
|
|
2668
|
+
# is the last page, so it is complete. A page AT
|
|
2669
|
+
# the length stays possibly truncated.
|
|
2670
|
+
#
|
|
2671
|
+
# THE PAGE LENGTH IS A READING, NEVER A CONSTANT THE RULE NAMES. The listing
|
|
2672
|
+
# asks `pagelen=$BB_LIST_PAGELEN` through `bb api`, and the length holds only
|
|
2673
|
+
# where that `bb` passes the query through unchanged, so it is read per version
|
|
2674
|
+
# from `BB_LIST_PAGE_VERSIONS` — pinned like `BB_ISSUE_VERSION`. A version
|
|
2675
|
+
# nobody measured has no length, and every non-empty page under it stays
|
|
2676
|
+
# possibly truncated: the rule never calls a page complete it cannot prove. A
|
|
2677
|
+
# future length of 100 is a new table entry, never a page of 50 read as short.
|
|
2678
|
+
#
|
|
2679
|
+
# Measured on `quatico/quaweb-website`: bb 1.9.0 returned 50 merged rows in one
|
|
2680
|
+
# request on 2026-09-30, and on 2026-10-01 answered `pagelen: 50` with 50 rows,
|
|
2681
|
+
# a `next` and `size: 895`. Before
|
|
2682
|
+
# 2026-10-01 every non-empty Bitbucket page was reported, so 20 open PRs read as
|
|
2683
|
+
# truncated on every refresh (#1137).
|
|
2684
|
+
#
|
|
2685
|
+
# THE PER-BRANCH SWEEP AND THE WINDOWED LISTING MAKE NO PAGE CLAIM. The sweep
|
|
2686
|
+
# proves completeness per branch and the window refuses a short read, so this
|
|
2687
|
+
# detector is not asked about either (`pr_list_states`).
|
|
2540
2688
|
#
|
|
2541
2689
|
# No --limit was requested → the caller accepted the host's default page and is
|
|
2542
2690
|
# owed no report, so no existing no-limit caller's behaviour changes.
|
|
@@ -2555,21 +2703,82 @@ jira_check() {
|
|
|
2555
2703
|
# future diff that teaches the scan to fall back, without moving the failure
|
|
2556
2704
|
# into a minutes-long pulse. See the plan's Done-when item 3.
|
|
2557
2705
|
#
|
|
2706
|
+
# The page length Bitbucket's plain listing asks for. `bb_state_listing` sends
|
|
2707
|
+
# it, and `pr_list_report_truncation` reads it as the page length only for a
|
|
2708
|
+
# `bb` in BB_LIST_PAGE_VERSIONS.
|
|
2709
|
+
BB_LIST_PAGELEN=50
|
|
2710
|
+
|
|
2711
|
+
# The `bb` versions measured to pass `pagelen` through `bb api` unchanged,
|
|
2712
|
+
# space-separated. `adapters/host/listing-paging.ts` holds the same table.
|
|
2713
|
+
BB_LIST_PAGE_VERSIONS="1.9.0"
|
|
2714
|
+
|
|
2715
|
+
# The page length the answering `bb` lists by, or nothing where its version is
|
|
2716
|
+
# not in BB_LIST_PAGE_VERSIONS. Reads the version `bb_require_json` recorded,
|
|
2717
|
+
# so it costs no `bb` call; a skipped capability check records no version.
|
|
2718
|
+
bb_list_page_length() {
|
|
2719
|
+
local v="${BB_CAP_IDENTITY#*/}"
|
|
2720
|
+
case " $BB_LIST_PAGE_VERSIONS " in
|
|
2721
|
+
*" $v "*) printf '%s\n' "$BB_LIST_PAGELEN" ;;
|
|
2722
|
+
esac
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
# A PAGE THAT IS PROVABLY WHOLE SAYS SO, and that sentence is the licence a
|
|
2726
|
+
# joining caller needs. This reported only truncation until 2026-10-02: a
|
|
2727
|
+
# complete page was SILENT, so a caller could not tell "this page holds every
|
|
2728
|
+
# PR" from "this adapter has no opinion", and the only reading left to it was
|
|
2729
|
+
# the page's own row count against its own limit. That is a coincidence rather
|
|
2730
|
+
# than evidence — measured on this repository, `--state all --limit 1000`
|
|
2731
|
+
# returned exactly 1000 rows of 1064 PRs, so the count-based test failed, the
|
|
2732
|
+
# scan's `.list-complete` was withheld, and every branch the join did not name
|
|
2733
|
+
# fell through to one `pr-state` call each: 26 calls at 3.8 s, 54-61% of the
|
|
2734
|
+
# scan's wall time in slice 1's five runs (#1017).
|
|
2735
|
+
#
|
|
2736
|
+
# THE CLAIM IS THE ADAPTER'S BECAUSE THE PAGING SEMANTICS ARE. Whether a short
|
|
2737
|
+
# page proves anything depends on how the host pages, which is exactly what
|
|
2738
|
+
# this script knows and a caller does not. `pr_sweep_report` already states its
|
|
2739
|
+
# own stronger claim in this shape and `plot-fleet-scan.sh` already reads that
|
|
2740
|
+
# sentence; this is the same contract for the second path, so a listing is no
|
|
2741
|
+
# longer the one answer a caller has to guess at.
|
|
2742
|
+
#
|
|
2743
|
+
# BOTH SENTENCES OR NEITHER. The complete and truncated reports are the two
|
|
2744
|
+
# outcomes of one decision, so they are emitted from one place: a reader can
|
|
2745
|
+
# never see both for one state, and silence now means only that no claim was
|
|
2746
|
+
# owed — no `--limit`, or an empty page.
|
|
2747
|
+
#
|
|
2558
2748
|
# $1 backend $2 requested limit (may be "") $3 state word $4 row count
|
|
2559
2749
|
pr_list_report_truncation() {
|
|
2560
|
-
local be="$1" limit="$2" state="$3" count="$4"
|
|
2750
|
+
local be="$1" limit="$2" state="$3" count="$4" len=""
|
|
2561
2751
|
[ -n "$limit" ] || return 0 # no --limit → no completeness claim owed
|
|
2562
2752
|
[ "$count" -gt 0 ] 2>/dev/null || return 0 # an empty page had nothing to hide
|
|
2563
2753
|
if [ "$be" = "github" ]; then
|
|
2564
2754
|
# github honours the limit: complete unless the page came back AT the limit.
|
|
2565
|
-
|
|
2755
|
+
# Measured 2026-10-02 on this repository (1064 PRs): `--limit 1100`
|
|
2756
|
+
# answered 1065 rows and `--limit 2000` the same 1065, so a page short of
|
|
2757
|
+
# its limit is the whole list and asking generously costs the rows that
|
|
2758
|
+
# exist rather than the rows requested.
|
|
2759
|
+
if [ "$count" -lt "$limit" ] 2>/dev/null; then pr_list_report_complete "$be" "$limit" "$state" "$count"; return 0; fi
|
|
2760
|
+
else
|
|
2761
|
+
# A fixed page: complete below a measured page length, unprovable otherwise.
|
|
2762
|
+
[ "$be" = "bitbucket" ] && len="$(bb_list_page_length)"
|
|
2763
|
+
if [ -n "$len" ] && [ "$count" -lt "$len" ] 2>/dev/null; then pr_list_report_complete "$be" "$limit" "$state" "$count"; return 0; fi
|
|
2566
2764
|
fi
|
|
2567
|
-
#
|
|
2568
|
-
#
|
|
2569
|
-
# the states that were capped, not a whole-call flag that over-reports.
|
|
2765
|
+
# Named per state so a caller can resolve exactly the states that were
|
|
2766
|
+
# capped, not a whole-call flag that over-reports.
|
|
2570
2767
|
echo "plot-host: $be pr-list state=$state possibly truncated ($count rows, requested limit $limit unprovable) — a join against this page may read older branches as 'no PR' (#333)" >&2
|
|
2571
2768
|
}
|
|
2572
2769
|
|
|
2770
|
+
# THE WORDING IS A CONTRACT between this script and `plot-fleet-scan.sh`, pinned
|
|
2771
|
+
# on both sides exactly as `pr-list sweep complete` is. A page that cannot prove
|
|
2772
|
+
# itself whole never prints it, so a match is licence and a miss is silence —
|
|
2773
|
+
# never a guess.
|
|
2774
|
+
#
|
|
2775
|
+
# PER STATE, like the truncation report beside it, because the scan asks for two
|
|
2776
|
+
# states and joins both: a caller needs to know which pages were whole, not that
|
|
2777
|
+
# some were.
|
|
2778
|
+
pr_list_report_complete() { # $1 backend $2 limit $3 state word $4 row count
|
|
2779
|
+
echo "plot-host: $1 pr-list state=$3 page complete ($4 rows below requested limit $2) — this page holds every pull request the host has in this state" >&2
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2573
2782
|
# The backends this script has an arm for, which is what "drivable" means here.
|
|
2574
2783
|
#
|
|
2575
2784
|
# THE LIST LIVES IN THE SCRIPT BECAUSE THE SCRIPT IS WHAT WOULD CHANGE. Adding a
|
|
@@ -3599,6 +3808,10 @@ case "$op" in
|
|
|
3599
3808
|
pr-list)
|
|
3600
3809
|
state="open"
|
|
3601
3810
|
rich=0
|
|
3811
|
+
# Whether the rich fields are asked of the OPEN pull requests only. Set by
|
|
3812
|
+
# `--rich-open`, which also sets `rich`: the flag narrows what is ASKED and
|
|
3813
|
+
# never what is emitted.
|
|
3814
|
+
rich_open=0
|
|
3602
3815
|
# PIN THE LIST TO ONE REPOSITORY, the same `--repo` `pr-state` and
|
|
3603
3816
|
# `pr-merged` already take. A checkout may carry several remotes on several
|
|
3604
3817
|
# hosts, and an unpinned `gh pr list` resolves whichever of them it prefers
|
|
@@ -3623,6 +3836,22 @@ case "$op" in
|
|
|
3623
3836
|
--state) state="${2:?}"; shift 2 ;;
|
|
3624
3837
|
--limit) limit="${2:?}"; shift 2 ;;
|
|
3625
3838
|
--rich) rich=1; shift ;;
|
|
3839
|
+
# THE RICH FIELDS OF THE OPEN PRs AND THE PLAIN FIELDS OF THE REST.
|
|
3840
|
+
# Measured 2026-10-01 on this repository: `--rich --state all --limit
|
|
3841
|
+
# 1000` took 43.0 s and the same call without the rich fields took 7.3 s,
|
|
3842
|
+
# while all 1000 rows were terminal — so 36 s bought four verdicts about
|
|
3843
|
+
# heads nobody can act on. Re-measured 2026-10-02 with 3 PRs open:
|
|
3844
|
+
# `--rich --state open` is 2.6-3.3 s and `--state all` is 8.6-10.5 s.
|
|
3845
|
+
#
|
|
3846
|
+
# IT IMPLIES `--rich`, so every row carries every rich field and no
|
|
3847
|
+
# consumer has to ask which call produced it. A terminal row's verdicts
|
|
3848
|
+
# arrive at the ABSENT values the Bitbucket arm already emits, which the
|
|
3849
|
+
# store's schema accepts — so no `PR_INDEX_VERSION` bump follows.
|
|
3850
|
+
#
|
|
3851
|
+
# THE SPLIT IS ONLY WORTH MAKING OVER A SET THAT HOLDS TERMINAL ROWS.
|
|
3852
|
+
# With `--state open` asked, both calls would list the same PRs and the
|
|
3853
|
+
# second would be waste, so this collapses to plain `--rich` there.
|
|
3854
|
+
--rich-open) rich=1; rich_open=1; shift ;;
|
|
3626
3855
|
--repo) repo_args=(-R "${2:?}"); shift 2 ;;
|
|
3627
3856
|
# `${2:?}` REFUSES AN EMPTY VALUE, and that is the point rather than
|
|
3628
3857
|
# boilerplate. `--since ""` would reach GitHub as `--search "updated:>"`,
|
|
@@ -3701,6 +3930,57 @@ case "$op" in
|
|
|
3701
3930
|
fi
|
|
3702
3931
|
|
|
3703
3932
|
if [ "$be" = "github" ]; then
|
|
3933
|
+
# THE SPLIT READ, AND IT IS TWO CALLS BECAUSE THE COST IS PER ROW.
|
|
3934
|
+
# `gh pr list` pages internally and takes no page-size flag, so 1000 rich
|
|
3935
|
+
# rows cost 1000 rows' worth of `statusCheckRollup` whatever the page.
|
|
3936
|
+
# Narrowing the SET is the only lever: the rich fields are asked of the
|
|
3937
|
+
# open pull requests, and everything else comes back plain.
|
|
3938
|
+
#
|
|
3939
|
+
# ORDER MATTERS — THE RICH ROWS GO FIRST. A PR that is open appears in
|
|
3940
|
+
# both payloads, and a consumer keyed by number takes the last line it
|
|
3941
|
+
# read; the second call is filtered to the terminal states instead, so no
|
|
3942
|
+
# number is emitted twice and no rich row is overwritten by a plain one.
|
|
3943
|
+
# Filtered rather than ordered, because a caller that sorts or de-dupes
|
|
3944
|
+
# differently must not be able to reach a different answer.
|
|
3945
|
+
#
|
|
3946
|
+
# ASKED ONLY WHERE THE SET CAN HOLD A TERMINAL ROW. With `--state open`
|
|
3947
|
+
# the two calls would list the same PRs, so this collapses to `--rich`.
|
|
3948
|
+
if [ "$rich_open" = 1 ] && [ "$state" != "open" ]; then
|
|
3949
|
+
# THE OPEN HALF, RICH. Asked with the caller's own `--limit` and
|
|
3950
|
+
# `--since`: a window the caller set applies to both halves, or the two
|
|
3951
|
+
# would answer about different populations.
|
|
3952
|
+
#
|
|
3953
|
+
# `|| exit $?` ON BOTH, and the rule `pr_list_call`'s header states is
|
|
3954
|
+
# what makes it load-bearing here too: this runs in a command
|
|
3955
|
+
# substitution, so a `die` inside leaves only that subshell and the
|
|
3956
|
+
# outer script would carry on with an empty payload. A failing open call
|
|
3957
|
+
# and a failing all call must each exit non-zero and print no rows.
|
|
3958
|
+
_rich_open_out="$("$0" pr-list --rich --state open \
|
|
3959
|
+
${limit:+--limit "$limit"} ${since:+--since "$since"} \
|
|
3960
|
+
${repo_args[1]+--repo "${repo_args[1]}"} \
|
|
3961
|
+
$(for _b in $branches; do printf -- '--branch %s ' "$_b"; done))" || exit $?
|
|
3962
|
+
# THE REST, PLAIN, AND THE TERMINAL ROWS ARE THE ONES KEPT. `--state
|
|
3963
|
+
# all` is asked rather than merged+closed separately: GitHub answers it
|
|
3964
|
+
# in one call, and two calls would double a cost this exists to halve.
|
|
3965
|
+
_plain_all_out="$("$0" pr-list --state "$state" \
|
|
3966
|
+
${limit:+--limit "$limit"} ${since:+--since "$since"} \
|
|
3967
|
+
${repo_args[1]+--repo "${repo_args[1]}"} \
|
|
3968
|
+
$(for _b in $branches; do printf -- '--branch %s ' "$_b"; done))" || exit $?
|
|
3969
|
+
[ -n "$_rich_open_out" ] && printf '%s\n' "$_rich_open_out"
|
|
3970
|
+
# THE ABSENT VALUES ARE THE BITBUCKET ARM'S, not invented here: a
|
|
3971
|
+
# terminal row carries every field a rich row carries, so one shape
|
|
3972
|
+
# reaches every consumer. `checks:"unknown"` means NOT ASKED and never
|
|
3973
|
+
# *no checks* — the board's fold takes the verdict it already holds for
|
|
3974
|
+
# the same number, and a row with none reads as unavailable.
|
|
3975
|
+
#
|
|
3976
|
+
# `draft`, `url` and `updatedAt` are REAL here — the plain GitHub arm
|
|
3977
|
+
# asks for them — so only the four verdicts are absent.
|
|
3978
|
+
[ -n "$_plain_all_out" ] && printf '%s\n' "$_plain_all_out" \
|
|
3979
|
+
| jq -c 'select(.state=="MERGED" or .state=="CLOSED")
|
|
3980
|
+
| . + {checks:"unknown", mergeable:"unknown", review:"",
|
|
3981
|
+
failing_checks:[]}'
|
|
3982
|
+
exit 0
|
|
3983
|
+
fi
|
|
3704
3984
|
if [ "$rich" = 1 ]; then
|
|
3705
3985
|
# `checks` has FOUR states, and two of them mean "a person is the
|
|
3706
3986
|
# blocker" rather than "a machine is busy":
|
|
@@ -3840,12 +4120,25 @@ case "$op" in
|
|
|
3840
4120
|
}'
|
|
3841
4121
|
fi
|
|
3842
4122
|
else
|
|
4123
|
+
# `isDraft`, `url` AND `updatedAt` TRAVEL ON THE PLAIN ARM TOO, and they
|
|
4124
|
+
# are cheap: measured 2026-09-21, `number,updatedAt` over 937 PRs is
|
|
4125
|
+
# 4715 ms against 5417 ms for the base fields, where
|
|
4126
|
+
# `statusCheckRollup` alone is 18 842 ms. They are scalar columns on the
|
|
4127
|
+
# PR node; the rollup is a per-row resolution, and that is the whole
|
|
4128
|
+
# 36 s `--rich-open` removes.
|
|
4129
|
+
#
|
|
4130
|
+
# ASKED HERE BECAUSE `--rich-open`'S TERMINAL ROWS COME THROUGH THIS
|
|
4131
|
+
# ARM, and a terminal row must carry every field a rich row carries —
|
|
4132
|
+
# `updatedAt` most of all, since it is the field a durable store
|
|
4133
|
+
# advances its watermark by. Without it a split full read would leave
|
|
4134
|
+
# the store unable to narrow and the daily full read would never fall
|
|
4135
|
+
# due against anything but a cold window.
|
|
3843
4136
|
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} ${search_args[@]+"${search_args[@]}"} \
|
|
3844
|
-
--json number,title,state,headRefName,author)" || exit $?
|
|
4137
|
+
--json number,title,state,headRefName,isDraft,url,updatedAt,author)" || exit $?
|
|
3845
4138
|
pr_list_report_truncation github "$limit" "$state" \
|
|
3846
4139
|
"$(jq 'length' <<<"$_gh_raw" 2>/dev/null || echo 0)"
|
|
3847
4140
|
printf '%s' "$_gh_raw" \
|
|
3848
|
-
| jq -c '.[] | {number:.number,title:.title,state:.state,head:.headRefName,author:(.author.login // "")}'
|
|
4141
|
+
| jq -c '.[] | {number:.number,title:.title,state:.state,head:.headRefName,draft:.isDraft,url:.url,updatedAt:(.updatedAt // ""),author:(.author.login // "")}'
|
|
3849
4142
|
fi
|
|
3850
4143
|
else
|
|
3851
4144
|
# `author` IS THE `nickname`, the one handle field a Bitbucket user
|
|
@@ -4266,7 +4559,19 @@ case "$op" in
|
|
|
4266
4559
|
done
|
|
4267
4560
|
limit_args=()
|
|
4268
4561
|
[ -n "$limit" ] && limit_args=(--limit "$limit")
|
|
4269
|
-
|
|
4562
|
+
# WHO LISTS THESE ISSUES IS ASKED ONCE, HERE, BEFORE THE ARMS — after the
|
|
4563
|
+
# argument parsing, so a caller's typo is still reported as a usage error
|
|
4564
|
+
# rather than as a tracker refusal. The arms below then dispatch on the
|
|
4565
|
+
# ANSWER, never on a second reading of the config.
|
|
4566
|
+
source_said="$(issue_source)" || exit 1
|
|
4567
|
+
if [ "${source_said%%$'\t'*}" = "nobody" ]; then
|
|
4568
|
+
# Exit 4, the code this op already documents as *this host cannot be
|
|
4569
|
+
# asked at all* — a configuration a person fixes, not a call to retry.
|
|
4570
|
+
# NOTHING HAS BEEN SPENT: no `gh`, no `bb`, no curl has run yet.
|
|
4571
|
+
echo "plot-host: ${source_said#*$'\t'}" >&2
|
|
4572
|
+
exit 4
|
|
4573
|
+
fi
|
|
4574
|
+
if [ "$source_said" = $'tracker\tjira' ]; then
|
|
4270
4575
|
# Jira, resolved through the REST API — DISPATCHED ON `Tracker`, never on
|
|
4271
4576
|
# `backend()`: a Bitbucket repo tracking in Jira is the normal enterprise
|
|
4272
4577
|
# case, so the git host is irrelevant here (see the Jira helpers up top).
|
|
@@ -4444,7 +4749,15 @@ case "$op" in
|
|
|
4444
4749
|
# 4 to `unsupported` and anything else to `failed` must not need a second
|
|
4445
4750
|
# table to read this op.
|
|
4446
4751
|
num="${1:?issue-view needs an issue number}"; shift
|
|
4447
|
-
|
|
4752
|
+
# Asked once, before the arms, exactly as issue-list does — the same
|
|
4753
|
+
# question with the same three answers and the same exit codes. A consumer
|
|
4754
|
+
# that maps 4 to `unsupported` needs no second table to read this op.
|
|
4755
|
+
source_said="$(issue_source)" || exit 1
|
|
4756
|
+
if [ "${source_said%%$'\t'*}" = "nobody" ]; then
|
|
4757
|
+
echo "plot-host: ${source_said#*$'\t'}" >&2
|
|
4758
|
+
exit 4
|
|
4759
|
+
fi
|
|
4760
|
+
if [ "$source_said" = $'tracker\tjira' ]; then
|
|
4448
4761
|
# Jira, dispatched on `Tracker` not `backend()` — the same rule issue-list
|
|
4449
4762
|
# follows. `num` is a Jira KEY (PROJ-123), read off issue-list moments ago.
|
|
4450
4763
|
#
|
package/plot-monitor-subject.sh
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Plot helper: the ONE answer to "is this monitor's subject still there?"
|
|
3
3
|
#
|
|
4
|
-
# SOURCED, NOT RUN, by `plot-
|
|
4
|
+
# SOURCED, NOT RUN, by `plot-agent-monitor.sh` and `plot-build-monitor.sh`.
|
|
5
5
|
# Both need the same computation and neither renders it the same way, which is
|
|
6
6
|
# the same shape as `plot-worker-state.sh` and `plot-pr-merged.sh` — and the
|
|
7
7
|
# same reason. `plot-worker-state.sh` carried five of its six states in
|
|
@@ -132,9 +132,19 @@ plot_monitor_subject() {
|
|
|
132
132
|
pid=$(cat "$pid_file" 2>/dev/null | tr -d ' \n')
|
|
133
133
|
|
|
134
134
|
# A file that exists but holds no digits is a half-written pid, which is the
|
|
135
|
-
# startup window caught mid-`printf`. Not gone
|
|
135
|
+
# startup window caught mid-`printf`. Not gone — UNLESS the wrapper has
|
|
136
|
+
# already recorded an exit beside it. `start_worker` removes that record
|
|
137
|
+
# before every start, so in the startup window it is absent. A worker loop
|
|
138
|
+
# that moves to a new desk empties the pid file it leaves
|
|
139
|
+
# (`move_worker_record`), and the wrapper still writes the exit into this
|
|
140
|
+
# desk when the loop ends; without this arm a monitor watching that desk
|
|
141
|
+
# would read `starting` forever.
|
|
136
142
|
case "$pid" in
|
|
137
|
-
'' | *[!0-9]*)
|
|
143
|
+
'' | *[!0-9]*)
|
|
144
|
+
if [ -f "$(dirname "$pid_file")/.plot-worker.exit" ]; then
|
|
145
|
+
printf 'gone'; return 0
|
|
146
|
+
fi
|
|
147
|
+
printf 'starting'; return 0 ;;
|
|
138
148
|
esac
|
|
139
149
|
|
|
140
150
|
if kill -0 "$pid" 2>/dev/null; then
|
|
@@ -192,3 +202,45 @@ plot_monitor_wait() { # $1 = seconds to wait, $2 = pid file
|
|
|
192
202
|
done
|
|
193
203
|
return 0
|
|
194
204
|
}
|
|
205
|
+
|
|
206
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
207
|
+
# WHICH DESK A MONITOR WATCHES THIS PASS, AFTER A HOP MAY HAVE MOVED ITS AGENT
|
|
208
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
209
|
+
#
|
|
210
|
+
# `PLOT_WORKTREE` is fixed at launch, but `update_manifest_on_hop` rewrites the
|
|
211
|
+
# manifest's `worktree` field when the loop cuts a new desk — so a monitor that
|
|
212
|
+
# never re-reads it watches the launch desk forever. This is the shell twin of
|
|
213
|
+
# `watchedDesk` in `packages/domain/src/rules/desk-manifest.ts`, duplicated
|
|
214
|
+
# rather than called for the reason `docs/shell-and-domain.md` gives: both
|
|
215
|
+
# monitors re-read this every pass (30 s and 300 s), and a 39 ms bundle hop paid
|
|
216
|
+
# on every pass forever is the cost the shell side exists to avoid.
|
|
217
|
+
# `desk-manifest.corpus.test.ts` holds the pair; a disagreement stops the
|
|
218
|
+
# branch rather than being adjusted away.
|
|
219
|
+
#
|
|
220
|
+
# `$1` = the manifest file this monitor was handed (`PLOT_MANIFEST_FILE`), may
|
|
221
|
+
# be empty or absent.
|
|
222
|
+
# `$2` = the desk this monitor was launched on (`PLOT_WORKTREE`).
|
|
223
|
+
# Prints the desk to watch THIS pass.
|
|
224
|
+
plot_watched_desk() { # $1 = manifest file, $2 = launch desk → prints the watched desk
|
|
225
|
+
local manifest_file="${1:-}" launched="${2:-}" field
|
|
226
|
+
# ABSENT IS NOT FALSE. No manifest file named, or one that is gone, both mean
|
|
227
|
+
# "watch what you were launched on" — never a crash and never "watch
|
|
228
|
+
# nothing". A hand-started monitor with no `PLOT_MANIFEST_FILE` is a supported
|
|
229
|
+
# shape, not an error.
|
|
230
|
+
if [ -n "$manifest_file" ] && [ -f "$manifest_file" ]; then
|
|
231
|
+
# Same grep-and-sed idiom `plot_manifest_for_worktree` uses: the manifest is
|
|
232
|
+
# pretty-printed one field per line, so this avoids parsing JSON in bash.
|
|
233
|
+
field=$(grep -m1 '"worktree":' "$manifest_file" 2>/dev/null | sed 's/.*"worktree": *"\([^"]*\)".*/\1/')
|
|
234
|
+
else
|
|
235
|
+
field=''
|
|
236
|
+
fi
|
|
237
|
+
# Whitespace-only reads as absent too, matching the rule: trim leading and
|
|
238
|
+
# trailing space before testing for emptiness.
|
|
239
|
+
field="${field#"${field%%[![:space:]]*}"}"
|
|
240
|
+
field="${field%"${field##*[![:space:]]}"}"
|
|
241
|
+
if [ -n "$field" ]; then
|
|
242
|
+
printf '%s' "$field"
|
|
243
|
+
else
|
|
244
|
+
printf '%s' "$launched"
|
|
245
|
+
fi
|
|
246
|
+
}
|