@plot-pm/board 0.10.0 → 0.12.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 +157 -151
- package/package.json +9 -1
- package/plot-agent-monitor.sh +532 -0
- package/plot-approve.sh +151 -32
- package/plot-budget.sh +439 -0
- package/plot-build-monitor.sh +435 -0
- package/plot-config.sh +17 -1
- package/plot-default-branch.sh +109 -0
- package/plot-deliver.sh +214 -119
- package/plot-dispatch.sh +1327 -188
- package/plot-fleet-scan.sh +1005 -170
- package/plot-host.sh +1438 -61
- package/plot-monitor-subject.sh +194 -0
- package/plot-plan-meta.sh +345 -47
- package/plot-pr-merged.sh +180 -0
- package/plot-reap.sh +719 -61
- package/plot-release-refs.sh +188 -46
- package/plot-resolve-artifact.sh +115 -22
- package/plot-transcript-quiet.sh +142 -0
- package/plot-worker-monitor.sh +644 -0
- package/plot-worker-state.sh +81 -50
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Plot helper: the ONE answer to "did the host merge ANY PR for this branch?"
|
|
3
|
+
#
|
|
4
|
+
# SOURCED, NOT RUN. `. "$script_dir/plot-pr-merged.sh"` defines `pr_merged` and
|
|
5
|
+
# `pr_open`; the file does nothing else on load. That is what makes sourcing it
|
|
6
|
+
# safe, and it is the same shape — and the same reason — as
|
|
7
|
+
# `plot-worker-state.sh`: the logic could not simply stay in `plot-reap.sh`,
|
|
8
|
+
# because that file parses `$@` and `exit 2`s on an unknown argument at load
|
|
9
|
+
# time, so sourcing it would run the reaper's argument parser against its
|
|
10
|
+
# caller's arguments.
|
|
11
|
+
#
|
|
12
|
+
# FOUR CALLERS. `plot-reap.sh`, `plot-release-refs.sh`, `plot-dispatch.sh`, and
|
|
13
|
+
# `plot-quiet-stretch.sh` — the last sourcing it guarded and calling it behind
|
|
14
|
+
# `command -v pr_merged`, so it still works with this file absent.
|
|
15
|
+
#
|
|
16
|
+
# WHY IT WAS EXTRACTED. `plot-reap.sh` and `plot-release-refs.sh` gate on the
|
|
17
|
+
# SAME fact — has this branch's work landed — and they must never disagree
|
|
18
|
+
# about it. The reaper removes a checkout, which is re-creatable with
|
|
19
|
+
# `git worktree add`; ref deletion is not re-creatable at all. A second
|
|
20
|
+
# implementation that drifted toward permissive would therefore fail in the
|
|
21
|
+
# direction that cannot be undone. One function, one answer, no drift.
|
|
22
|
+
#
|
|
23
|
+
# THIS FILE IS NOW THE ADAPTER, AND THE DECISION IS THE DOMAIN'S. The two
|
|
24
|
+
# functions still ask the host, because the host is asked in shell; what they no
|
|
25
|
+
# longer do is decide. Each turns its lookup into one of three READINGS —
|
|
26
|
+
# `found`, `none`, `unaskable` — and hands the pair to
|
|
27
|
+
# `board/plot-landed.mjs`, which bundles `rules/landed.ts`. The layering runs
|
|
28
|
+
# one way: caller → this adapter → the rule.
|
|
29
|
+
#
|
|
30
|
+
# WHAT MOVED IS THE COUPLING. `pr_merged` and `pr_open` fail in the SAME
|
|
31
|
+
# direction and to OPPOSITE effect — an unreachable host makes the first refuse
|
|
32
|
+
# a removal and the second release its veto — so neither is safe alone and the
|
|
33
|
+
# pair is. That was a comment in this file and could not be checked. It is now
|
|
34
|
+
# `mayRemove` in the rule, asserted over all nine combinations of the two
|
|
35
|
+
# readings, and exactly one of them permits a removal.
|
|
36
|
+
#
|
|
37
|
+
# WHY THIS STILL ASKS `gh` AND NOT `plot-host.sh`. Measured 2026-09-06, and it
|
|
38
|
+
# is the answer to a question this file's exemption in
|
|
39
|
+
# `scripts/check-host-cli-callers.sh` was left open for.
|
|
40
|
+
#
|
|
41
|
+
# `plot-host.sh pr-merged` prints `merged`/`not-merged`/`unknown`, three words
|
|
42
|
+
# that read like the three readings below. THEY DO NOT MATCH. An ABSENT CLI
|
|
43
|
+
# arrives from the adapter as `not-merged`, where `_plot_merged_lookup` answers
|
|
44
|
+
# `unaskable`:
|
|
45
|
+
#
|
|
46
|
+
# plot-host.sh pr-merged <branch> → not-merged (exit 0)
|
|
47
|
+
# _plot_merged_lookup <branch> → unaskable
|
|
48
|
+
#
|
|
49
|
+
# The cause is the adapter's `is_lookup_miss`. A missing binary makes the shell
|
|
50
|
+
# say `bash: gh: command not found`, and that matches the same `not found` it
|
|
51
|
+
# uses to recognise a genuine "no pull requests found" — one phrase covering two
|
|
52
|
+
# conditions the rule below exists to keep apart.
|
|
53
|
+
#
|
|
54
|
+
# THE DIRECTION IS WHY IT BLOCKS RATHER THAN ANNOYS. `not-merged` reads as
|
|
55
|
+
# `none` — the host spoke and said nothing merged — so `mayRemove` may permit a
|
|
56
|
+
# removal where `unaskable` refuses. `plot-release-refs.sh` deletes remote refs
|
|
57
|
+
# on this answer and a deleted ref is not re-creatable.
|
|
58
|
+
#
|
|
59
|
+
# AND `pr_open` HAS NO OP TO CALL AT ALL. It needs found/none/unaskable about
|
|
60
|
+
# ANY open PR; `pr-state` answers about ONE — the newest — and reports a failed
|
|
61
|
+
# lookup with the same `state:"NONE"` payload as a real absence. It can express
|
|
62
|
+
# neither "any" nor "unaskable".
|
|
63
|
+
#
|
|
64
|
+
# THE COST WAS MEASURED TOO, and it is the smaller objection. Ten sequential
|
|
65
|
+
# calls on this machine: 4.75 s direct, 9.25 s through the adapter — +450 ms per
|
|
66
|
+
# branch, from the connector's slot and budget accounting rather than from the
|
|
67
|
+
# spawn. Across the 48 branches the fleet scan walks that is ~21 s. Real, but it
|
|
68
|
+
# is the correctness gap above that decides this, not the clock.
|
|
69
|
+
#
|
|
70
|
+
# Both halves are pinned in `test/reconcile/host.test.mjs`, so the exemption
|
|
71
|
+
# rests on tests that fail when it stops being true.
|
|
72
|
+
#
|
|
73
|
+
# `mergedAt` IS READ, NEVER `state`. A merged PR reports state CLOSED, and
|
|
74
|
+
# trusting `state` would refuse every squash-merged branch — which is the whole
|
|
75
|
+
# population these scripts exist for. Squash-merge rewrites the commits, so the
|
|
76
|
+
# branch stays "ahead of main" forever and ancestry alone can never clear it.
|
|
77
|
+
#
|
|
78
|
+
# AND THE QUESTION IS "ANY", NOT "THE NEWEST". This asked with `--limit 1`
|
|
79
|
+
# until 2026-08-27, which returns only the most recent PR — so a newer,
|
|
80
|
+
# unmerged PR sitting in front of the real merge reported `no merged PR` about
|
|
81
|
+
# a branch whose work was on main. Measured that day against the live host:
|
|
82
|
+
#
|
|
83
|
+
# an-unreachable-host-says-so newest #473 null → real merge #446
|
|
84
|
+
# the-scan-sees-a-stale-sprint-tally newest #464 null → real merge #463
|
|
85
|
+
# a-plan-cites-a-jira-key newest #476 null → real merge #447
|
|
86
|
+
#
|
|
87
|
+
# The masking PRs were ones the fleet opened ITSELF on already-merged waves,
|
|
88
|
+
# which closes a loop: a leftover worktree lets auto-dispatch adopt a merged
|
|
89
|
+
# branch, its worker opens a duplicate, the duplicate is newer, the reaper
|
|
90
|
+
# keeps the worktree — the input to step one. Reading only the newest PR is the
|
|
91
|
+
# SAME error as reading `state`, one level out: the newest PR is not the merge,
|
|
92
|
+
# just as the state is not the merge.
|
|
93
|
+
#
|
|
94
|
+
# 100 rather than unbounded: `gh` has no "all" sentinel, and this runs on paths
|
|
95
|
+
# where the estate may hold dozens of branches. A branch carrying more than 100
|
|
96
|
+
# PRs whose only merge is the oldest would still be missed — a far narrower
|
|
97
|
+
# window than "any duplicate at all", and it fails SAFE, toward keeping.
|
|
98
|
+
|
|
99
|
+
# Where the rule lives, resolved from THIS file rather than from the cwd.
|
|
100
|
+
#
|
|
101
|
+
# Both callers run with their cwd wherever the operator invoked them, and the
|
|
102
|
+
# reconcile suite runs them against sandbox repos in the temp directory. The
|
|
103
|
+
# artifact sits beside this script in the plot checkout and in the published
|
|
104
|
+
# npm package alike, which is why both are vendored together.
|
|
105
|
+
_plot_landed_mjs="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/board/plot-landed.mjs"
|
|
106
|
+
|
|
107
|
+
# Ask the rule about one branch's two lookups.
|
|
108
|
+
#
|
|
109
|
+
# $1=merged reading, $2=open reading, both `found`/`none`/`unaskable`.
|
|
110
|
+
# Prints the rule's two words; returns non-zero when it could not be asked at
|
|
111
|
+
# all, which every caller below reads as the refusing direction.
|
|
112
|
+
_plot_landed() {
|
|
113
|
+
[ -f "$_plot_landed_mjs" ] || return 1
|
|
114
|
+
printf '%s\t%s\n' "$1" "$2" | node "$_plot_landed_mjs" 2>/dev/null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# What the host says about ANY PR on this branch: found / none / unaskable.
|
|
118
|
+
#
|
|
119
|
+
# A MISSING CLI, AN UNAUTHED ONE AND A NETWORK FAILURE ARE ALL `unaskable`, and
|
|
120
|
+
# they are not `none`. The whole rule turns on that difference — a lookup that
|
|
121
|
+
# ran and matched nothing is the host speaking, while a lookup that did not run
|
|
122
|
+
# is silence, and silence is never permission.
|
|
123
|
+
_plot_merged_lookup() {
|
|
124
|
+
local br="$1" out
|
|
125
|
+
command -v gh >/dev/null 2>&1 || { echo unaskable; return; }
|
|
126
|
+
out=$(gh pr list --head "$br" --state all --limit 100 --json mergedAt 2>/dev/null) \
|
|
127
|
+
|| { echo unaskable; return; }
|
|
128
|
+
case "$out" in *'"mergedAt":"'*) echo found ;; *) echo none ;; esac
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# What the host says about an OPEN PR on this branch: found / none / unaskable.
|
|
132
|
+
_plot_open_lookup() {
|
|
133
|
+
local br="$1" out
|
|
134
|
+
command -v gh >/dev/null 2>&1 || { echo unaskable; return; }
|
|
135
|
+
out=$(gh pr list --head "$br" --state open --limit 1 --json number 2>/dev/null) \
|
|
136
|
+
|| { echo unaskable; return; }
|
|
137
|
+
case "$out" in *'"number"'*) echo found ;; *) echo none ;; esac
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Did the host merge ANY PR for this branch?
|
|
141
|
+
#
|
|
142
|
+
# Returns 0 (merged) / 1 (not merged, or the host cannot be asked). The failure
|
|
143
|
+
# direction is deliberate and load-bearing: an unreachable host, an unauthed
|
|
144
|
+
# `gh`, a missing CLI all answer "not merged", so every caller KEEPS what it
|
|
145
|
+
# was considering removing. Silence is never permission.
|
|
146
|
+
#
|
|
147
|
+
# The rule answers `unknown` on that silence and this function reports it as a
|
|
148
|
+
# refusal, which is the same contract the four callers were written against —
|
|
149
|
+
# `plot-reap.sh:385` says so explicitly. A rule that cannot be reached at all
|
|
150
|
+
# refuses here too, for the same reason.
|
|
151
|
+
pr_merged() {
|
|
152
|
+
local answer
|
|
153
|
+
answer=$(_plot_landed "$(_plot_merged_lookup "$1")" none) || return 1
|
|
154
|
+
case "$answer" in landed*) return 0 ;; *) return 1 ;; esac
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Does the branch have an OPEN PR right now?
|
|
158
|
+
#
|
|
159
|
+
# A DIFFERENT question from `pr_merged`, and it exists for a case measured by
|
|
160
|
+
# hand on 2026-08-28. `changeset-release/main` is merged — repeatedly — and
|
|
161
|
+
# Changesets RECREATES and reuses the very same branch for the next release.
|
|
162
|
+
# Its ref carries a live release PR while an older PR of its own has merged, so
|
|
163
|
+
# the merged gate alone says "delete" about a branch somebody is actively
|
|
164
|
+
# using.
|
|
165
|
+
#
|
|
166
|
+
# So an open PR VETOES a deletion even where an older one merged. That is
|
|
167
|
+
# strictly narrower than the merge gate rather than a second opinion on it: it
|
|
168
|
+
# can only ever keep a ref, never release one.
|
|
169
|
+
#
|
|
170
|
+
# Returns 0 when an open PR exists. An unreachable host returns 1 — and note
|
|
171
|
+
# that this is the SAME direction as `pr_merged`'s failure, but it has the
|
|
172
|
+
# OPPOSITE effect, since this answer vetoes. The safety therefore does not come
|
|
173
|
+
# from this function: it comes from `pr_merged` already having refused on the
|
|
174
|
+
# same silence, so a host that cannot be asked deletes nothing regardless.
|
|
175
|
+
# `mayRemove` is where that pair is now asserted rather than described.
|
|
176
|
+
pr_open() {
|
|
177
|
+
local answer
|
|
178
|
+
answer=$(_plot_landed none "$(_plot_open_lookup "$1")") || return 1
|
|
179
|
+
case "$answer" in *" open-pr") return 0 ;; *) return 1 ;; esac
|
|
180
|
+
}
|