@plot-pm/board 0.16.2 → 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 +162 -139
- package/package.json +1 -3
- package/plot-agent-manifest.sh +111 -15
- package/plot-agent-monitor.sh +43 -5
- package/plot-approve.sh +224 -73
- package/plot-build-monitor.sh +49 -4
- package/plot-deliver.sh +10 -741
- package/plot-dispatch.sh +263 -91
- package/plot-fleet-scan.sh +746 -62
- package/plot-host.sh +273 -18
- 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 +61 -28
- package/plot-state-receipt.sh +12 -2
- package/plot-tmp.sh +31 -3
- package/plot-transcript-quiet.sh +3 -1
- package/plot-worker-state.sh +611 -24
- package/plot-resolve-artifact.sh +0 -423
- 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
|
|
@@ -471,11 +521,22 @@ die6() { echo "plot-host: $*" >&2; exit 6; }
|
|
|
471
521
|
# secondary message contains the phrase *"rate limit"* too — *"You have
|
|
472
522
|
# exceeded a secondary rate limit"* — so a quota test applied first claims
|
|
473
523
|
# every secondary refusal and the distinction is lost at the point it is made.
|
|
474
|
-
|
|
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
|
|
475
534
|
if LC_ALL=C grep -qiE 'secondary rate|exceeded a secondary|abuse detection|abuse-detection|too many requests|\b429\b' <<<"$1"; then
|
|
476
535
|
echo secondary
|
|
477
536
|
elif LC_ALL=C grep -qiE 'rate limit|ratelimit' <<<"$1"; then
|
|
478
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
|
|
479
540
|
else
|
|
480
541
|
echo failed
|
|
481
542
|
fi
|
|
@@ -555,6 +616,16 @@ pr_list_failed() { # $1=stderr text
|
|
|
555
616
|
echo " or run against an account with quota left. No login will help." >&2
|
|
556
617
|
exit 5
|
|
557
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
|
+
;;
|
|
558
629
|
esac
|
|
559
630
|
# THE ONE KIND A COMMAND FIXES. An auth gap and a DNS blip both land here, and
|
|
560
631
|
# the repair for the first is a login this connector can name — see
|
|
@@ -2281,6 +2352,47 @@ tracker_scheme() {
|
|
|
2281
2352
|
tracker_raw | awk '{print tolower($1)}'
|
|
2282
2353
|
}
|
|
2283
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
|
+
|
|
2284
2396
|
tracker_base_url() {
|
|
2285
2397
|
# The base URL is the SECOND token; a bare `jira` with no URL yields "".
|
|
2286
2398
|
# PLOT_JIRA_BASE_URL overrides, for a caller that has the URL separately.
|
|
@@ -2610,6 +2722,29 @@ bb_list_page_length() {
|
|
|
2610
2722
|
esac
|
|
2611
2723
|
}
|
|
2612
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
|
+
#
|
|
2613
2748
|
# $1 backend $2 requested limit (may be "") $3 state word $4 row count
|
|
2614
2749
|
pr_list_report_truncation() {
|
|
2615
2750
|
local be="$1" limit="$2" state="$3" count="$4" len=""
|
|
@@ -2617,17 +2752,33 @@ pr_list_report_truncation() {
|
|
|
2617
2752
|
[ "$count" -gt 0 ] 2>/dev/null || return 0 # an empty page had nothing to hide
|
|
2618
2753
|
if [ "$be" = "github" ]; then
|
|
2619
2754
|
# github honours the limit: complete unless the page came back AT the limit.
|
|
2620
|
-
|
|
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
|
|
2621
2760
|
else
|
|
2622
2761
|
# A fixed page: complete below a measured page length, unprovable otherwise.
|
|
2623
2762
|
[ "$be" = "bitbucket" ] && len="$(bb_list_page_length)"
|
|
2624
|
-
if [ -n "$len" ] && [ "$count" -lt "$len" ] 2>/dev/null; then return 0; fi
|
|
2763
|
+
if [ -n "$len" ] && [ "$count" -lt "$len" ] 2>/dev/null; then pr_list_report_complete "$be" "$limit" "$state" "$count"; return 0; fi
|
|
2625
2764
|
fi
|
|
2626
2765
|
# Named per state so a caller can resolve exactly the states that were
|
|
2627
2766
|
# capped, not a whole-call flag that over-reports.
|
|
2628
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
|
|
2629
2768
|
}
|
|
2630
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
|
+
|
|
2631
2782
|
# The backends this script has an arm for, which is what "drivable" means here.
|
|
2632
2783
|
#
|
|
2633
2784
|
# THE LIST LIVES IN THE SCRIPT BECAUSE THE SCRIPT IS WHAT WOULD CHANGE. Adding a
|
|
@@ -3657,6 +3808,10 @@ case "$op" in
|
|
|
3657
3808
|
pr-list)
|
|
3658
3809
|
state="open"
|
|
3659
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
|
|
3660
3815
|
# PIN THE LIST TO ONE REPOSITORY, the same `--repo` `pr-state` and
|
|
3661
3816
|
# `pr-merged` already take. A checkout may carry several remotes on several
|
|
3662
3817
|
# hosts, and an unpinned `gh pr list` resolves whichever of them it prefers
|
|
@@ -3681,6 +3836,22 @@ case "$op" in
|
|
|
3681
3836
|
--state) state="${2:?}"; shift 2 ;;
|
|
3682
3837
|
--limit) limit="${2:?}"; shift 2 ;;
|
|
3683
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 ;;
|
|
3684
3855
|
--repo) repo_args=(-R "${2:?}"); shift 2 ;;
|
|
3685
3856
|
# `${2:?}` REFUSES AN EMPTY VALUE, and that is the point rather than
|
|
3686
3857
|
# boilerplate. `--since ""` would reach GitHub as `--search "updated:>"`,
|
|
@@ -3759,6 +3930,57 @@ case "$op" in
|
|
|
3759
3930
|
fi
|
|
3760
3931
|
|
|
3761
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
|
|
3762
3984
|
if [ "$rich" = 1 ]; then
|
|
3763
3985
|
# `checks` has FOUR states, and two of them mean "a person is the
|
|
3764
3986
|
# blocker" rather than "a machine is busy":
|
|
@@ -3898,12 +4120,25 @@ case "$op" in
|
|
|
3898
4120
|
}'
|
|
3899
4121
|
fi
|
|
3900
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.
|
|
3901
4136
|
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} ${search_args[@]+"${search_args[@]}"} \
|
|
3902
|
-
--json number,title,state,headRefName,author)" || exit $?
|
|
4137
|
+
--json number,title,state,headRefName,isDraft,url,updatedAt,author)" || exit $?
|
|
3903
4138
|
pr_list_report_truncation github "$limit" "$state" \
|
|
3904
4139
|
"$(jq 'length' <<<"$_gh_raw" 2>/dev/null || echo 0)"
|
|
3905
4140
|
printf '%s' "$_gh_raw" \
|
|
3906
|
-
| 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 // "")}'
|
|
3907
4142
|
fi
|
|
3908
4143
|
else
|
|
3909
4144
|
# `author` IS THE `nickname`, the one handle field a Bitbucket user
|
|
@@ -4324,7 +4559,19 @@ case "$op" in
|
|
|
4324
4559
|
done
|
|
4325
4560
|
limit_args=()
|
|
4326
4561
|
[ -n "$limit" ] && limit_args=(--limit "$limit")
|
|
4327
|
-
|
|
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
|
|
4328
4575
|
# Jira, resolved through the REST API — DISPATCHED ON `Tracker`, never on
|
|
4329
4576
|
# `backend()`: a Bitbucket repo tracking in Jira is the normal enterprise
|
|
4330
4577
|
# case, so the git host is irrelevant here (see the Jira helpers up top).
|
|
@@ -4502,7 +4749,15 @@ case "$op" in
|
|
|
4502
4749
|
# 4 to `unsupported` and anything else to `failed` must not need a second
|
|
4503
4750
|
# table to read this op.
|
|
4504
4751
|
num="${1:?issue-view needs an issue number}"; shift
|
|
4505
|
-
|
|
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
|
|
4506
4761
|
# Jira, dispatched on `Tracker` not `backend()` — the same rule issue-list
|
|
4507
4762
|
# follows. `num` is a Jira KEY (PROJ-123), read off issue-list moments ago.
|
|
4508
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
|
+
}
|