@plot-pm/board 0.16.1 → 0.16.2
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 +101 -96
- package/package.json +2 -1
- package/plot-approve.sh +10 -1
- package/plot-config.sh +10 -14
- package/plot-deliver.sh +10 -1
- package/plot-desk-root.sh +127 -0
- package/plot-dispatch.sh +51 -45
- package/plot-host.sh +84 -26
- package/plot-reap.sh +21 -65
- package/plot-resolve-artifact.sh +13 -19
- package/plot-tmp.sh +62 -14
- package/plot-worker-state.sh +22 -0
package/plot-reap.sh
CHANGED
|
@@ -399,79 +399,35 @@ if [ -r "$CONFIG" ]; then
|
|
|
399
399
|
fi
|
|
400
400
|
case "$MANIFEST_DIR" in /*) ;; *) MANIFEST_DIR="$ROOT/$MANIFEST_DIR" ;; esac
|
|
401
401
|
|
|
402
|
-
# Where the agent logs live
|
|
403
|
-
# `
|
|
404
|
-
#
|
|
405
|
-
# directory the board never writes to
|
|
406
|
-
# there
|
|
402
|
+
# Where the DESKS and the agent logs live: the desk root, `deskRoot`'s answer
|
|
403
|
+
# asked through `plot-desk-root.sh` — the rule `plot-dispatch.sh` creates desks
|
|
404
|
+
# under and `agentLogDir` writes logs into. One rule, so a reaper cannot sweep a
|
|
405
|
+
# directory the board never writes to and report success over a file that is
|
|
406
|
+
# still there.
|
|
407
407
|
#
|
|
408
|
-
#
|
|
409
|
-
#
|
|
410
|
-
#
|
|
411
|
-
#
|
|
408
|
+
# Two questions read the one answer. `LOG_DIR` is where a finished branch's log
|
|
409
|
+
# files are swept from; `WT_ROOT` is the directory a tree must sit under to be a
|
|
410
|
+
# candidate desk at all. The sweep's filename filter (`branch_log_files`) is
|
|
411
|
+
# unchanged, so what it may delete is still only `plot-<kind>-*` names. The
|
|
412
|
+
# default root is `<repo>/.worktrees`, which Plot owns, so a sibling checkout in
|
|
413
|
+
# the parent is never under it and stays silent.
|
|
412
414
|
#
|
|
413
|
-
#
|
|
414
|
-
# joined onto the repo root, trailing slash trimmed as pure string work because
|
|
415
|
-
# the directory need not exist. A second convention for resolving a configured
|
|
416
|
-
# directory is a second way to be wrong.
|
|
417
|
-
LOG_DIR="$(cd "$ROOT/.." && pwd)"
|
|
418
|
-
if [ -r "$CONFIG" ]; then
|
|
419
|
-
d=$(bash "$CONFIG" get "Worktree root" "" 2>/dev/null) || d=""
|
|
420
|
-
if [ -n "$d" ]; then
|
|
421
|
-
case "$d" in
|
|
422
|
-
/*) LOG_DIR="$d" ;;
|
|
423
|
-
*) LOG_DIR="$ROOT/$d" ;;
|
|
424
|
-
esac
|
|
425
|
-
LOG_DIR="${LOG_DIR%/}"
|
|
426
|
-
fi
|
|
427
|
-
fi
|
|
428
|
-
|
|
429
|
-
# Where the DESKS live, which is the same key resolved for a different question.
|
|
430
|
-
#
|
|
431
|
-
# `LOG_DIR` and this answer two things: `LOG_DIR` is where a finished branch's
|
|
432
|
-
# log files are swept from, and this is the directory a tree must sit under to
|
|
433
|
-
# be a candidate desk at all. A repository with NO configured root has no
|
|
434
|
-
# `.worktrees/` and its desks are named `plot-wt-*` beside the repo — so this
|
|
435
|
-
# stays EMPTY there rather than defaulting to the parent, because the parent
|
|
436
|
-
# holds every sibling checkout a person ever made and calling those candidate
|
|
437
|
-
# desks is the over-broad reading this slice must not introduce.
|
|
438
|
-
#
|
|
439
|
-
# IT IS RESOLVED AGAINST THE MAIN CHECKOUT, NOT `$ROOT`, and that is the one
|
|
440
|
-
# place in this script where the two differ on purpose. `git rev-parse
|
|
415
|
+
# IT IS RESOLVED AGAINST THE MAIN CHECKOUT, NOT `$ROOT`. `git rev-parse
|
|
441
416
|
# --show-toplevel` answers *this* worktree, so a reaper run from inside a desk
|
|
442
|
-
#
|
|
417
|
+
# would resolve `.worktrees` to a directory beneath that desk — which does not
|
|
443
418
|
# exist, so every tree reads as unplaceable-but-elsewhere and the reading is
|
|
444
|
-
# silently empty. Measured 2026-09-10 from
|
|
419
|
+
# silently empty. Measured 2026-09-10 from inside a desk: `$ROOT/.worktrees`
|
|
445
420
|
# named `.../plot-wt-feature-a-finished-desk-is-a-finding/.worktrees` while
|
|
446
421
|
# every desk sits under `.../plot/.worktrees`. A dispatched agent runs the
|
|
447
422
|
# reaper from exactly there, so the wrong answer would be the usual one.
|
|
448
423
|
#
|
|
449
|
-
#
|
|
450
|
-
#
|
|
451
|
-
# desk
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
# blast radius rather than a reading. Named here rather than fixed silently.
|
|
457
|
-
WT_ROOT=""
|
|
458
|
-
if [ -r "$CONFIG" ]; then
|
|
459
|
-
d=$(bash "$CONFIG" get "Worktree root" "" 2>/dev/null) || d=""
|
|
460
|
-
if [ -n "$d" ]; then
|
|
461
|
-
case "$d" in
|
|
462
|
-
/*) WT_ROOT="$d" ;;
|
|
463
|
-
*)
|
|
464
|
-
# Falls back to `$ROOT` where git cannot answer, which is the same
|
|
465
|
-
# directory on a single-checkout repository and the only one available.
|
|
466
|
-
main_checkout=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)") \
|
|
467
|
-
|| main_checkout="$ROOT"
|
|
468
|
-
[ -d "$main_checkout" ] || main_checkout="$ROOT"
|
|
469
|
-
WT_ROOT="$main_checkout/$d"
|
|
470
|
-
;;
|
|
471
|
-
esac
|
|
472
|
-
WT_ROOT="${WT_ROOT%/}"
|
|
473
|
-
fi
|
|
474
|
-
fi
|
|
424
|
+
# There is NO FALLBACK: a reaper that cannot ask the rule stops rather than
|
|
425
|
+
# compute a second default and delete from it.
|
|
426
|
+
# shellcheck source=plot-desk-root.sh
|
|
427
|
+
. "$(dirname "${BASH_SOURCE[0]}")/plot-desk-root.sh"
|
|
428
|
+
WT_ROOT=$(plot_desk_root "$(plot_repo_root)") || {
|
|
429
|
+
echo "plot-reap: cannot resolve the desk root — nothing was reaped." >&2; exit 2; }
|
|
430
|
+
LOG_DIR="$WT_ROOT"
|
|
475
431
|
|
|
476
432
|
# The files ONE branch's agent run leaves beside its worktree, removed with it.
|
|
477
433
|
#
|
package/plot-resolve-artifact.sh
CHANGED
|
@@ -165,11 +165,10 @@ fi
|
|
|
165
165
|
# MEASURED, and recorded at length in plot-dispatch.sh's held_worktree: a
|
|
166
166
|
# hand-made worktree is named for the branch with its TYPE dropped, so a gate
|
|
167
167
|
# that guessed `plot-wt-<flattened>` missed a worktree with six modified files
|
|
168
|
-
# in it.
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
# CREATE-a-fresh-one fallback below composes a name (via dispatch's rule, so the
|
|
168
|
+
# in it. Desks carry two naming conventions — `plot-wt-*` beside the repo from
|
|
169
|
+
# older dispatches, and unprefixed names under the desk root — so a path guess
|
|
170
|
+
# has two ways to be wrong. So the read asks git, and only the
|
|
171
|
+
# CREATE-a-fresh-one fallback below composes a name (under the desk root, so the
|
|
173
172
|
# fresh worktree lands where dispatch would have put it).
|
|
174
173
|
#
|
|
175
174
|
# `git worktree list --porcelain` emits `worktree <path>` then `branch
|
|
@@ -182,21 +181,15 @@ wt=$(git worktree list --porcelain </dev/null 2>/dev/null | awk -v want="refs/he
|
|
|
182
181
|
/^worktree / { path = substr($0, 10) }
|
|
183
182
|
/^branch / { if (substr($0, 8) == want) { print path; exit } }')
|
|
184
183
|
|
|
185
|
-
# No existing worktree holds it — compose the path a fresh one will take,
|
|
186
|
-
#
|
|
187
|
-
#
|
|
184
|
+
# No existing worktree holds it — compose the path a fresh one will take, under
|
|
185
|
+
# the desk root `plot-dispatch.sh` also asks for, so the fresh worktree lands
|
|
186
|
+
# where dispatch would have put it. The root is the MAIN checkout's, and there
|
|
187
|
+
# is no fallback: an unaskable rule refuses the repair.
|
|
188
|
+
# shellcheck source=plot-desk-root.sh
|
|
189
|
+
. "$script_dir/plot-desk-root.sh"
|
|
188
190
|
if [ -z "$wt" ]; then
|
|
189
|
-
wt_root=$("$
|
|
190
|
-
|
|
191
|
-
wt_root=$(cd "$repo_root/.." && pwd)
|
|
192
|
-
wt="$wt_root/plot-wt-$(printf '%s' "$branch" | tr '/' '-')"
|
|
193
|
-
else
|
|
194
|
-
case "$wt_root" in
|
|
195
|
-
/*) : ;;
|
|
196
|
-
*) wt_root="$repo_root/$wt_root" ;;
|
|
197
|
-
esac
|
|
198
|
-
wt="${wt_root%/}/$(printf '%s' "$branch" | tr '/' '-')"
|
|
199
|
-
fi
|
|
191
|
+
wt_root=$(plot_desk_root "$(plot_repo_root)") || finish refused no-desk-root
|
|
192
|
+
wt="$wt_root/$(printf '%s' "$branch" | tr '/' '-')"
|
|
200
193
|
fi
|
|
201
194
|
|
|
202
195
|
if [ "$dry_run" = 1 ]; then
|
|
@@ -263,6 +256,7 @@ if [ -d "$wt" ] && git worktree list --porcelain | grep -qx "worktree $wt"; then
|
|
|
263
256
|
finish refused worktree-busy
|
|
264
257
|
fi
|
|
265
258
|
else
|
|
259
|
+
plot_exclude_desk_root "$(plot_repo_root)"
|
|
266
260
|
if ! git worktree add -q "$wt" "$branch" 2>/dev/null; then
|
|
267
261
|
if ! git worktree add -q -b "$branch" "$wt" "origin/$branch" 2>/dev/null; then
|
|
268
262
|
echo "plot-resolve-artifact: cannot create a worktree for $branch at $wt" >&2
|
package/plot-tmp.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# plot-tmp.sh — every temp path a Plot script creates, and the process's only
|
|
2
|
-
# EXIT/INT/TERM traps. SOURCED, not run.
|
|
2
|
+
# EXIT/INT/TERM/PIPE traps. SOURCED, not run.
|
|
3
3
|
#
|
|
4
4
|
# . "$SCRIPT_DIR/plot-tmp.sh"
|
|
5
5
|
# plot_tmpdir work fleet-ref # $work = $TMPDIR/plot-fleet-ref.XXXXXX (a directory)
|
|
@@ -27,8 +27,11 @@
|
|
|
27
27
|
# first call would be installed inside that call's substitution and remove the
|
|
28
28
|
# path when the substitution closed. `PLOT_TMP_LOADED` makes a second source in
|
|
29
29
|
# the same process a no-op, because a re-run setup would truncate the live
|
|
30
|
-
# registry. INT and
|
|
31
|
-
# the script stops with 130 or
|
|
30
|
+
# registry. INT, TERM and PIPE run the cleanup, clear their own trap and
|
|
31
|
+
# re-raise, so the script stops with 130, 143 or 141 rather than running on to
|
|
32
|
+
# exit 0. PIPE is among them because a board server stopped with SIGTERM leaves
|
|
33
|
+
# its running scripts writing to closed pipes, and SIGPIPE's default action
|
|
34
|
+
# skips the EXIT trap.
|
|
32
35
|
#
|
|
33
36
|
# A script that sources this must not install its own EXIT, INT or TERM trap:
|
|
34
37
|
# the last `trap` wins, and that replacement is the defect this file fixes
|
|
@@ -42,29 +45,51 @@ PLOT_TMP_LOADED=$$
|
|
|
42
45
|
# Fixed at first source: a later `TMPDIR` change in the script does not move it.
|
|
43
46
|
# A file already at this path belongs to a dead process that had the same pid,
|
|
44
47
|
# and its `c:` commands are not this process's, so it is replaced, never read.
|
|
48
|
+
# The file itself is created at the end of this file, after the traps.
|
|
45
49
|
PLOT_TMP_REGISTRY="${TMPDIR:-/tmp}/plot-reg.$$"
|
|
46
|
-
rm -f -- "$PLOT_TMP_REGISTRY" 2>/dev/null
|
|
47
|
-
: > "$PLOT_TMP_REGISTRY" 2>/dev/null || true
|
|
48
50
|
|
|
49
51
|
# One line per entry, in registration order: `p:<path>` or `c:<command>`.
|
|
50
52
|
_plot_tmp_register() {
|
|
51
53
|
printf '%s:%s\n' "$1" "$2" >> "$PLOT_TMP_REGISTRY" 2>/dev/null || true
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
# A SIGNAL INSIDE A CREATION WAITS FOR THE REGISTRATION. bash runs a pending
|
|
57
|
+
# trap as soon as `$(mktemp …)` returns, before the next line registers the
|
|
58
|
+
# path, so a cleanup run there would remove the registry and leave the new path
|
|
59
|
+
# unlisted. While `__plot_tmp_busy` is set, the signal handler records the
|
|
60
|
+
# signal in `__plot_tmp_pending` and returns; `_plot_tmp_create` handles it once
|
|
61
|
+
# the path is registered.
|
|
62
|
+
__plot_tmp_busy=''
|
|
63
|
+
__plot_tmp_pending=''
|
|
64
|
+
|
|
65
|
+
# _plot_tmp_create VAR prefix [-d] — create, register, assign to VAR.
|
|
66
|
+
_plot_tmp_create() {
|
|
67
|
+
local __plot_tmp_new __plot_tmp_rc
|
|
68
|
+
__plot_tmp_busy=1
|
|
69
|
+
if [ "${3:-}" = -d ]; then
|
|
70
|
+
__plot_tmp_new=$(mktemp -d "${TMPDIR:-/tmp}/plot-$2.XXXXXX")
|
|
71
|
+
else
|
|
72
|
+
__plot_tmp_new=$(mktemp "${TMPDIR:-/tmp}/plot-$2.XXXXXX")
|
|
73
|
+
fi
|
|
74
|
+
__plot_tmp_rc=$?
|
|
75
|
+
[ "$__plot_tmp_rc" -eq 0 ] && _plot_tmp_register p "$__plot_tmp_new"
|
|
76
|
+
__plot_tmp_busy=''
|
|
77
|
+
if [ -n "$__plot_tmp_pending" ]; then
|
|
78
|
+
# shellcheck disable=SC2086 # "<name> <number>", split on purpose
|
|
79
|
+
_plot_tmp_on_signal $__plot_tmp_pending
|
|
80
|
+
fi
|
|
81
|
+
[ "$__plot_tmp_rc" -eq 0 ] || return 1
|
|
82
|
+
printf -v "$1" '%s' "$__plot_tmp_new"
|
|
83
|
+
}
|
|
84
|
+
|
|
54
85
|
# plot_tmpdir VAR prefix — create a directory, register it, assign it to VAR.
|
|
55
86
|
plot_tmpdir() {
|
|
56
|
-
|
|
57
|
-
__plot_tmp_new=$(mktemp -d "${TMPDIR:-/tmp}/plot-$2.XXXXXX") || return 1
|
|
58
|
-
_plot_tmp_register p "$__plot_tmp_new"
|
|
59
|
-
printf -v "$1" '%s' "$__plot_tmp_new"
|
|
87
|
+
_plot_tmp_create "$1" "$2" -d
|
|
60
88
|
}
|
|
61
89
|
|
|
62
90
|
# plot_tmpfile VAR prefix — create a file, register it, assign it to VAR.
|
|
63
91
|
plot_tmpfile() {
|
|
64
|
-
|
|
65
|
-
__plot_tmp_new=$(mktemp "${TMPDIR:-/tmp}/plot-$2.XXXXXX") || return 1
|
|
66
|
-
_plot_tmp_register p "$__plot_tmp_new"
|
|
67
|
-
printf -v "$1" '%s' "$__plot_tmp_new"
|
|
92
|
+
_plot_tmp_create "$1" "$2"
|
|
68
93
|
}
|
|
69
94
|
|
|
70
95
|
# plot_on_exit command — run `command` (one line) when the process ends.
|
|
@@ -75,9 +100,13 @@ plot_on_exit() {
|
|
|
75
100
|
# Runs every registered command and removes every registered path, in
|
|
76
101
|
# registration order, then removes the registry. Runs once: the registry is
|
|
77
102
|
# gone afterwards, so a second call finds nothing.
|
|
103
|
+
#
|
|
104
|
+
# PIPE is ignored for the cleanup: the process is ending, and a registered
|
|
105
|
+
# command that writes to a closed stdout or stderr must not stop the removals.
|
|
78
106
|
_plot_tmp_cleanup() {
|
|
79
107
|
local __plot_tmp_line
|
|
80
|
-
[ -f "$PLOT_TMP_REGISTRY" ] || return 0
|
|
108
|
+
[ -n "${__plot_tmp_owned:-}" ] && [ -f "$PLOT_TMP_REGISTRY" ] || return 0
|
|
109
|
+
trap '' PIPE
|
|
81
110
|
while IFS= read -r __plot_tmp_line; do
|
|
82
111
|
case $__plot_tmp_line in
|
|
83
112
|
c:*) eval "${__plot_tmp_line#c:}" ;;
|
|
@@ -97,6 +126,10 @@ _plot_tmp_on_exit() {
|
|
|
97
126
|
# $1 is the signal name, $2 its number. `kill` re-raises it with the default
|
|
98
127
|
# disposition; the `exit` is reached only if the shell defers the delivery.
|
|
99
128
|
_plot_tmp_on_signal() {
|
|
129
|
+
if [ -n "$__plot_tmp_busy" ]; then
|
|
130
|
+
__plot_tmp_pending="$1 $2"
|
|
131
|
+
return 0
|
|
132
|
+
fi
|
|
100
133
|
trap - EXIT "$1"
|
|
101
134
|
_plot_tmp_cleanup
|
|
102
135
|
kill -"$1" "$$"
|
|
@@ -106,3 +139,18 @@ _plot_tmp_on_signal() {
|
|
|
106
139
|
trap _plot_tmp_on_exit EXIT
|
|
107
140
|
trap '_plot_tmp_on_signal INT 2' INT
|
|
108
141
|
trap '_plot_tmp_on_signal TERM 15' TERM
|
|
142
|
+
trap '_plot_tmp_on_signal PIPE 13' PIPE
|
|
143
|
+
|
|
144
|
+
# THE REGISTRY IS CREATED AFTER THE TRAPS, so no signal lands between the file
|
|
145
|
+
# and the handler that removes it. It is replaced under the creation deferral:
|
|
146
|
+
# until `__plot_tmp_owned` is set, a file at this path is a dead process's and
|
|
147
|
+
# the cleanup leaves it unread.
|
|
148
|
+
__plot_tmp_busy=1
|
|
149
|
+
rm -f -- "$PLOT_TMP_REGISTRY" 2>/dev/null
|
|
150
|
+
: > "$PLOT_TMP_REGISTRY" 2>/dev/null || true
|
|
151
|
+
__plot_tmp_owned=1
|
|
152
|
+
__plot_tmp_busy=''
|
|
153
|
+
if [ -n "$__plot_tmp_pending" ]; then
|
|
154
|
+
# shellcheck disable=SC2086 # "<name> <number>", split on purpose
|
|
155
|
+
_plot_tmp_on_signal $__plot_tmp_pending
|
|
156
|
+
fi
|
package/plot-worker-state.sh
CHANGED
|
@@ -392,6 +392,28 @@ plot_worker_dirty() { # $1=worktree → the dirty files, one per line, leftovers
|
|
|
392
392
|
plot_worker_dirty_filter "$(git -C "$wt" status --porcelain 2>/dev/null)"
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
# Keep a file the estate writes into every desk out of `git status`.
|
|
396
|
+
#
|
|
397
|
+
# THROUGH THE CLONE'S `info/exclude`, never `.gitignore`: a `.gitignore` rule
|
|
398
|
+
# lives in the branch's own content, so a desk cut from an older branch would
|
|
399
|
+
# not see it, and `info/exclude` is per-repository and shared by every worktree.
|
|
400
|
+
# The directory and the file are CREATED when absent. Measured 2026-10-01
|
|
401
|
+
# (#1130): a clone with no `.git/info/exclude` kept `?? .metadata_never_index`
|
|
402
|
+
# in every free desk, and the AgentMonitor reported each waiting agent as
|
|
403
|
+
# `holds unlanded work`, because the writer skipped a file that did not exist.
|
|
404
|
+
#
|
|
405
|
+
# Best-effort: a desk that cannot take the line still works. Always returns 0.
|
|
406
|
+
plot_desk_exclude() { # $1=worktree $2=the exact line to exclude
|
|
407
|
+
local common excl
|
|
408
|
+
common=$(git -C "$1" rev-parse --git-common-dir 2>/dev/null) || return 0
|
|
409
|
+
[ -n "$common" ] || return 0
|
|
410
|
+
case "$common" in /*) ;; *) common="$1/$common" ;; esac
|
|
411
|
+
excl="$common/info/exclude"
|
|
412
|
+
mkdir -p "$common/info" 2>/dev/null || return 0
|
|
413
|
+
grep -qxF "$2" "$excl" 2>/dev/null || printf '%s\n' "$2" >> "$excl" 2>/dev/null || true
|
|
414
|
+
return 0
|
|
415
|
+
}
|
|
416
|
+
|
|
395
417
|
# The same filter, over status output the CALLER already has.
|
|
396
418
|
#
|
|
397
419
|
# SPLIT OUT BECAUSE THE STATUS CALL IS THE EXPENSIVE HALF and one caller had
|