@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,194 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Plot helper: the ONE answer to "is this monitor's subject still there?"
|
|
3
|
+
#
|
|
4
|
+
# SOURCED, NOT RUN, by `plot-worker-monitor.sh` and `plot-agent-monitor.sh`.
|
|
5
|
+
# Both need the same computation and neither renders it the same way, which is
|
|
6
|
+
# the same shape as `plot-worker-state.sh` and `plot-pr-merged.sh` — and the
|
|
7
|
+
# same reason. `plot-worker-state.sh` carried five of its six states in
|
|
8
|
+
# duplicate until 2026-08-18, and the copies had already drifted on the sixth.
|
|
9
|
+
# Two monitors deciding independently when to stop would drift the same way, and
|
|
10
|
+
# the failure would be silent: one monitor left running forever while its twin
|
|
11
|
+
# exits is exactly the leak this file exists to close, half-fixed.
|
|
12
|
+
#
|
|
13
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
+
# WHY A MONITOR NEEDS THIS AT ALL
|
|
15
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
#
|
|
17
|
+
# Measured 2026-08-30 and written up in
|
|
18
|
+
# `docs/research/2026-08-30-what-ends-a-monitor.md`: **nothing ended a monitor.**
|
|
19
|
+
# On an ordinary finish and on a `Worker bound` timeout alike, the wrapper's
|
|
20
|
+
# `wait "$agent"` returns, the wrapper writes `.plot-worker.exit` and exits, and
|
|
21
|
+
# both monitors are re-parented to `init` and loop forever. 34 of 40 monitors on
|
|
22
|
+
# the machine were `ppid=1` at the time of measurement, and 100 forks cost
|
|
23
|
+
# 23.3 ms against 4.8 ms on a quiet estate.
|
|
24
|
+
#
|
|
25
|
+
# The one run whose monitors WERE terminated is explained in that document and
|
|
26
|
+
# is not a mechanism: `nohup` does not `setsid`, so an orphan keeps the
|
|
27
|
+
# DISPATCHING SHELL's process group and a group kill sweeps it up collaterally.
|
|
28
|
+
# That fires when a human closes a terminal and never when a worker finishes —
|
|
29
|
+
# the opposite of a lifetime.
|
|
30
|
+
#
|
|
31
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
32
|
+
# THE SUBJECT IS THE AGENT, AND THAT IS THE WHOLE DESIGN
|
|
33
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
34
|
+
#
|
|
35
|
+
# A monitor exists to watch a dispatched agent. When that agent is gone there is
|
|
36
|
+
# nothing left to measure, so the monitor has finished its job rather than been
|
|
37
|
+
# interrupted — which is why this is a MEASUREMENT and not a timer.
|
|
38
|
+
#
|
|
39
|
+
# The plan forbids a timer explicitly, and the measurement says why: *"a monitor
|
|
40
|
+
# exiting after N seconds regardless would pass the visible assertions and
|
|
41
|
+
# destroy the property the whole plan rests on — a monitor that stops publishing
|
|
42
|
+
# means something."* A monitor that stops because its subject stopped carries
|
|
43
|
+
# information. One that stops because a clock ran out carries none, and is
|
|
44
|
+
# indistinguishable from one that crashed.
|
|
45
|
+
#
|
|
46
|
+
# THE AGENT COVERS ALL THREE ENDINGS. `--stop` kills the agent
|
|
47
|
+
# (`plot-dispatch.sh:752`); the `Worker bound` kills the agent
|
|
48
|
+
# (`plot-worker-loop.sh:172`); an ordinary finish is the agent exiting. In every
|
|
49
|
+
# case the wrapper survives just long enough to write `.plot-worker.exit` and
|
|
50
|
+
# then exits too. So watching the agent is sufficient, and watching the WRAPPER
|
|
51
|
+
# would be wrong: the wrapper outlives the agent by design, and a monitor bound
|
|
52
|
+
# to it would publish about a desk whose agent left.
|
|
53
|
+
#
|
|
54
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
55
|
+
# AN ABSENT PID FILE IS `starting`, NEVER `gone`
|
|
56
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
57
|
+
#
|
|
58
|
+
# `plot-dispatch.sh:478` records a sub-millisecond gap in which the wrapper has
|
|
59
|
+
# started and `.plot-worker.pid` is not yet written. The monitors start INSIDE
|
|
60
|
+
# that window — they are backgrounded before the agent, deliberately, so they
|
|
61
|
+
# exist before their subject does.
|
|
62
|
+
#
|
|
63
|
+
# So a monitor that read an absent pid file as `gone` would exit immediately on
|
|
64
|
+
# every single dispatch, and the leak would be replaced by a monitor that never
|
|
65
|
+
# runs. That is worse than the bug: an absent monitor is invisible, where an
|
|
66
|
+
# orphaned one at least shows up in `ps`.
|
|
67
|
+
#
|
|
68
|
+
# Three answers, not two:
|
|
69
|
+
#
|
|
70
|
+
# starting no pid file yet, or an unreadable one — the wrapper has not
|
|
71
|
+
# written it. Keep going; say nothing.
|
|
72
|
+
# alive the pid file names a process that exists. Keep going.
|
|
73
|
+
# gone the pid file names a process that does not exist. Stop.
|
|
74
|
+
#
|
|
75
|
+
# `starting` and `alive` are both "keep going", and they are kept apart anyway
|
|
76
|
+
# because the reason differs and a caller reporting them identically would lose
|
|
77
|
+
# the distinction the startup window depends on.
|
|
78
|
+
#
|
|
79
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
80
|
+
# THE LOWER BOUND IS THE CALLER'S, AND IT IS AN ORDERING
|
|
81
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
82
|
+
#
|
|
83
|
+
# The plan requires a monitor to outlive its agent long enough to record its
|
|
84
|
+
# finding — the Attaching slice's property, which this slice must not eat. This
|
|
85
|
+
# file does not enforce that, because it cannot: it answers a question and
|
|
86
|
+
# renders nothing.
|
|
87
|
+
#
|
|
88
|
+
# What enforces it is the ORDER in the callers: publish the pass, THEN ask.
|
|
89
|
+
# Every monitor therefore gets one final published pass after its agent has
|
|
90
|
+
# gone, which is the lower bound expressed as sequence rather than as a sleep.
|
|
91
|
+
# A caller that asked first and published second would satisfy the upper bound
|
|
92
|
+
# and silently lose the lower one — so the order is asserted in the tests, not
|
|
93
|
+
# left to a comment.
|
|
94
|
+
|
|
95
|
+
# `kill -0` is the liveness question, and it is the same one
|
|
96
|
+
# `plot-worker-state.sh` asks. It sends no signal; it only reports whether the
|
|
97
|
+
# pid can be signalled. A pid we do not own answers EPERM rather than ESRCH,
|
|
98
|
+
# which `kill -0` still reports as success — correct here, since a process we
|
|
99
|
+
# cannot signal is nonetheless a process that exists.
|
|
100
|
+
#
|
|
101
|
+
# `$1` = the path to the agent's pid file (`.plot-worker.pid`).
|
|
102
|
+
# Prints exactly one of: starting | alive | gone
|
|
103
|
+
plot_monitor_subject() {
|
|
104
|
+
local pid_file="${1:-}" pid
|
|
105
|
+
|
|
106
|
+
# No path at all: a hand-run monitor with no worktree, which has no subject to
|
|
107
|
+
# outlive and must not exit on its first pass. `starting` is the honest answer
|
|
108
|
+
# — there is nothing here that says the subject is gone.
|
|
109
|
+
[ -n "$pid_file" ] || { printf 'starting'; return 0; }
|
|
110
|
+
|
|
111
|
+
# NO PID FILE SPLITS TWO CASES, and reading them as one is what made monitors
|
|
112
|
+
# immortal. `starting` is right only while the desk is still there and the
|
|
113
|
+
# wrapper has not yet written the pid. If the DIRECTORY the pid file lives in
|
|
114
|
+
# is gone, the desk was removed — there is no subject to wait for and none is
|
|
115
|
+
# coming, so the honest answer is `gone`.
|
|
116
|
+
#
|
|
117
|
+
# Measured on CI 2026-08-31: 14 monitors at PPID 1, aged 11-13 minutes, each
|
|
118
|
+
# holding a `sleep 1`, after every test in the reconcile suite had PASSED.
|
|
119
|
+
# A test's fixture is removed at teardown, so its pid file vanishes BEFORE the
|
|
120
|
+
# agent does; `plot_monitor_wait` then never sees `gone` and loops forever,
|
|
121
|
+
# holding node's event loop open until the job ceiling kills it. That is the
|
|
122
|
+
# whole of the reconcile-suite hang, and it is why this is a two-case answer
|
|
123
|
+
# rather than one.
|
|
124
|
+
#
|
|
125
|
+
# PRODUCTION IS UNCHANGED: a real worktree outlives its agent, so the
|
|
126
|
+
# directory is present and this reads `starting` exactly as before.
|
|
127
|
+
if [ ! -f "$pid_file" ]; then
|
|
128
|
+
[ -d "$(dirname "$pid_file")" ] && { printf 'starting'; return 0; }
|
|
129
|
+
printf 'gone'; return 0
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
pid=$(cat "$pid_file" 2>/dev/null | tr -d ' \n')
|
|
133
|
+
|
|
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.
|
|
136
|
+
case "$pid" in
|
|
137
|
+
'' | *[!0-9]*) printf 'starting'; return 0 ;;
|
|
138
|
+
esac
|
|
139
|
+
|
|
140
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
141
|
+
printf 'alive'
|
|
142
|
+
else
|
|
143
|
+
printf 'gone'
|
|
144
|
+
fi
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Sleep up to `$1` seconds, but stop early the moment the subject at `$2` is
|
|
148
|
+
# gone. Returns 0 to publish another pass, 1 to leave.
|
|
149
|
+
#
|
|
150
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
151
|
+
# WHY THE WAIT IS SPLIT WHEN THE PUBLISHING IS NOT
|
|
152
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
153
|
+
#
|
|
154
|
+
# THE TWO CADENCES MUST STAY APART. The plan is explicit: the WorkerMonitor
|
|
155
|
+
# samples the process table every 30 s because a CPU delta is meaningless
|
|
156
|
+
# sampled further apart, and the AgentMonitor asks the host every 300 s because
|
|
157
|
+
# this repo has already measured what host questions on a fast loop cost. *"One
|
|
158
|
+
# subject wants tight sampling of a cheap fact; the other occasional sampling of
|
|
159
|
+
# an expensive one. Merging them would force one of those two to be wrong."*
|
|
160
|
+
#
|
|
161
|
+
# SO ONLY THE WAIT IS SPLIT, NEVER THE PASS. Publishing still happens on the
|
|
162
|
+
# monitor's own interval, unchanged — nothing here makes the AgentMonitor ask
|
|
163
|
+
# the host more often, and its 300 s stays 300 s. What is split is the IDLE TIME
|
|
164
|
+
# between passes, into short naps with a `kill -0` between them.
|
|
165
|
+
#
|
|
166
|
+
# WITHOUT THIS, THE UPPER BOUND IS THE INTERVAL. An AgentMonitor checking only
|
|
167
|
+
# after its full sleep would outlive an agent that finished in ten seconds by
|
|
168
|
+
# nearly five minutes. Bounded, technically — and still an orphan on every
|
|
169
|
+
# dispatch, on an estate where dispatches are frequent. The measurement that
|
|
170
|
+
# opened this slice counted 34 orphans; a five-minute window would have counted
|
|
171
|
+
# plenty too.
|
|
172
|
+
#
|
|
173
|
+
# THE PROBE IS FREE, WHICH IS WHY IT MAY BE FREQUENT. `kill -0` sends no signal
|
|
174
|
+
# and asks no host — it is a single syscall against the process table, the same
|
|
175
|
+
# question `plot-worker-state.sh` asks. The expensive half of an AgentMonitor
|
|
176
|
+
# pass is the host round trip, and that is in the PASS, not here.
|
|
177
|
+
#
|
|
178
|
+
# THE NAP IS THE GRANULARITY, and one second is chosen against the WorkerMonitor
|
|
179
|
+
# rather than in the abstract: a monitor may not outlive its agent by more than
|
|
180
|
+
# the tighter of the two cadences, or the fast monitor's exit would be slower
|
|
181
|
+
# than its own sampling. Any interval SHORTER than one nap sleeps once and is
|
|
182
|
+
# unaffected, which keeps `PLOT_MONITOR_INTERVAL=1` in a test behaving exactly
|
|
183
|
+
# as it reads.
|
|
184
|
+
plot_monitor_wait() { # $1 = seconds to wait, $2 = pid file
|
|
185
|
+
local remaining="${1:-0}" pid_file="${2:-}" nap
|
|
186
|
+
while [ "$remaining" -gt 0 ]; do
|
|
187
|
+
nap=1
|
|
188
|
+
[ "$remaining" -lt 1 ] && nap="$remaining"
|
|
189
|
+
sleep "$nap" || return 1
|
|
190
|
+
remaining=$((remaining - nap))
|
|
191
|
+
[ "$(plot_monitor_subject "$pid_file")" = gone ] && return 1
|
|
192
|
+
done
|
|
193
|
+
return 0
|
|
194
|
+
}
|