@plot-pm/board 0.16.0 → 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 +19 -22
- package/plot-deliver.sh +10 -1
- package/plot-desk-root.sh +127 -0
- package/plot-dispatch.sh +175 -60
- package/plot-fleet-scan.sh +13 -3
- 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-fleet-scan.sh
CHANGED
|
@@ -2844,6 +2844,7 @@ is_plan_phase() { # $1=normalized phase → 0 when this file is a plan
|
|
|
2844
2844
|
# gave when it failed.
|
|
2845
2845
|
plan_meta_files=()
|
|
2846
2846
|
plan_meta_phases=()
|
|
2847
|
+
plan_meta_types=()
|
|
2847
2848
|
plan_meta_waves=()
|
|
2848
2849
|
|
|
2849
2850
|
# Parses every plan file given, filling the four arrays above. Called ONCE.
|
|
@@ -2871,7 +2872,7 @@ for line in sys.stdin:
|
|
|
2871
2872
|
f = d.get("file")
|
|
2872
2873
|
if not f:
|
|
2873
2874
|
continue
|
|
2874
|
-
print("\t".join(["P", clean(f), clean(d.get("phase", ""))]))
|
|
2875
|
+
print("\t".join(["P", clean(f), clean(d.get("phase", "")), clean(d.get("type", ""))]))
|
|
2875
2876
|
for i, w in enumerate(d.get("waves", []) or []):
|
|
2876
2877
|
name = w.get("name")
|
|
2877
2878
|
for b in w.get("branches", []) or []:
|
|
@@ -2906,8 +2907,12 @@ for line in sys.stdin:
|
|
|
2906
2907
|
case "$kind" in
|
|
2907
2908
|
P)
|
|
2908
2909
|
plan_meta_files+=("$file")
|
|
2909
|
-
# `rest` is "<phase>",
|
|
2910
|
-
plan_meta_phases+=("$rest")
|
|
2910
|
+
# `rest` is "<phase>\t<type>", two tokens with no tabs inside either.
|
|
2911
|
+
plan_meta_phases+=("${rest%%$'\t'*}")
|
|
2912
|
+
case "$rest" in
|
|
2913
|
+
*$'\t'*) plan_meta_types+=("${rest#*$'\t'}") ;;
|
|
2914
|
+
*) plan_meta_types+=("") ;;
|
|
2915
|
+
esac
|
|
2911
2916
|
plan_meta_waves+=("")
|
|
2912
2917
|
;;
|
|
2913
2918
|
W)
|
|
@@ -3659,6 +3664,11 @@ for plan in "${plans[@]}"; do
|
|
|
3659
3664
|
if is_terminal_phase "$plan_phase" && [ "$plan_phase" != "delivered" ]; then
|
|
3660
3665
|
continue
|
|
3661
3666
|
fi
|
|
3667
|
+
# A docs or infra plan is live when it merges: /plot-release never records
|
|
3668
|
+
# `Released` for one, so `delivered` is its last phase and it leaves here.
|
|
3669
|
+
if [ "$plan_phase" = "delivered" ]; then
|
|
3670
|
+
case "${plan_meta_types[$meta_i]}" in docs|infra) continue ;; esac
|
|
3671
|
+
fi
|
|
3662
3672
|
|
|
3663
3673
|
n_plans=$((n_plans + 1))
|
|
3664
3674
|
plan_target=$(readlink "$plan" 2>/dev/null && echo "" || true)
|
package/plot-host.sh
CHANGED
|
@@ -247,7 +247,9 @@
|
|
|
247
247
|
# list` (no --json), pinned to bb 0.6.0. EXIT 4
|
|
248
248
|
# narrows rather than disappears: it is the
|
|
249
249
|
# tracker-DISABLED case (bb answers 404/410),
|
|
250
|
-
#
|
|
250
|
+
# and a bb whose `--help` lists no `issue`
|
|
251
|
+
# command (Quatico bb), both of
|
|
252
|
+
# which stay *this host cannot answer* where an
|
|
251
253
|
# empty list would say *there are none*. A call
|
|
252
254
|
# that failed on an enabled tracker, or any error
|
|
253
255
|
# wording this adapter does not recognise, exits
|
|
@@ -281,7 +283,8 @@
|
|
|
281
283
|
# Same three outcomes as issue-list, same codes:
|
|
282
284
|
# BITBUCKET NOW ANSWERS via `bb issue view`
|
|
283
285
|
# (pinned to 0.6.0); `url` comes from the view's
|
|
284
|
-
# footer. EXIT 4 is the tracker-DISABLED case
|
|
286
|
+
# footer. EXIT 4 is the tracker-DISABLED case
|
|
287
|
+
# or a bb with no `issue` command,
|
|
285
288
|
# EXIT 3 a lookup that failed or an unrecognised
|
|
286
289
|
# error. An issue that does not exist is a
|
|
287
290
|
# FAILURE here, not an empty body: the caller
|
|
@@ -1016,7 +1019,7 @@ bb_state_listing() { # global bb args… --state <s> --json → one JSON array
|
|
|
1016
1019
|
esac
|
|
1017
1020
|
done
|
|
1018
1021
|
[ -n "$_st" ] || die "bb_state_listing: no --state"
|
|
1019
|
-
_out="$(bb ${_args[@]+"${_args[@]}"} api "/repositories/{ws}/{repo}/pullrequests?state=$(bb_query_state "$_st")&pagelen
|
|
1022
|
+
_out="$(bb ${_args[@]+"${_args[@]}"} api "/repositories/{ws}/{repo}/pullrequests?state=$(bb_query_state "$_st")&pagelen=$BB_LIST_PAGELEN")" || return $?
|
|
1020
1023
|
printf '%s' "$_out" | jq -c '.values // []'
|
|
1021
1024
|
}
|
|
1022
1025
|
|
|
@@ -2033,17 +2036,41 @@ bb_issue_exit_code() {
|
|
|
2033
2036
|
# fails LOUDLY rather than silently mis-reading a column that may have moved.
|
|
2034
2037
|
# `PLOT_BB_SKIP_VERSION_CHECK` exists for the test harness, whose stub bb has no
|
|
2035
2038
|
# meaningful version — the parse is exercised against captured fixture text.
|
|
2039
|
+
#
|
|
2040
|
+
# A VERSION MISMATCH IS NOT ALWAYS A MOVED FORMAT. Two products share the name
|
|
2041
|
+
# `bb` (see the capability check below), and Quatico `bb` — 1.9.0 measured
|
|
2042
|
+
# 2026-10-01 — has no `issue` command at all: `bb --help` lists `pr …`,
|
|
2043
|
+
# `source …` and `api`. Telling it to update would suggest a fix that does not
|
|
2044
|
+
# exist, so a mismatch first asks `bb --help` what the CLI offers. A help text
|
|
2045
|
+
# that lists a `pr` command and no `issue` command is a bb that cannot be asked
|
|
2046
|
+
# about issues, which is exit 4 — the same answer as a disabled tracker. Any
|
|
2047
|
+
# other help text keeps the refusal below, exit 3. Exits 4 or 3; 0 passes.
|
|
2036
2048
|
bb_assert_issue_version() {
|
|
2037
2049
|
[ -n "${PLOT_BB_SKIP_VERSION_CHECK:-}" ] && return 0
|
|
2038
2050
|
local v
|
|
2039
2051
|
v="$(bb --version 2>/dev/null | bb_strip_ansi | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
|
|
2040
2052
|
if [ "$v" != "$BB_ISSUE_VERSION" ]; then
|
|
2053
|
+
if bb_lacks_issue_command; then
|
|
2054
|
+
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
|
|
2055
|
+
return 4
|
|
2056
|
+
fi
|
|
2041
2057
|
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
2058
|
return 3
|
|
2043
2059
|
fi
|
|
2044
2060
|
return 0
|
|
2045
2061
|
}
|
|
2046
2062
|
|
|
2063
|
+
# Whether `bb --help` lists commands and `issue` is not one of them. A help
|
|
2064
|
+
# that lists no `pr` command either is not a command listing this can read, so
|
|
2065
|
+
# it answers *not proven* (1) and the caller keeps its exit-3 refusal: guessing
|
|
2066
|
+
# 4 from unreadable help would turn a broken bb into *no issue tracker*.
|
|
2067
|
+
bb_lacks_issue_command() {
|
|
2068
|
+
local help
|
|
2069
|
+
help="$(bb --help 2>&1 | bb_strip_ansi)" || return 1
|
|
2070
|
+
grep -qE '^[[:space:]]+pr([[:space:]]|$)' <<<"$help" || return 1
|
|
2071
|
+
! grep -qE '^[[:space:]]+issues?([[:space:]]|$)' <<<"$help"
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2047
2074
|
# --- bb capability check (--json support) ------------------------------------
|
|
2048
2075
|
#
|
|
2049
2076
|
# TWO TOOLS SHARE THE NAME `bb`. craftamap/bb is a Go binary that does NOT
|
|
@@ -2516,27 +2543,36 @@ jira_check() {
|
|
|
2516
2543
|
# `bb` returned 50 merged PRs (ids 836→787) against a repo numbering to 836, so
|
|
2517
2544
|
# ~780 older merged PRs were invisible to the join.
|
|
2518
2545
|
#
|
|
2519
|
-
# THE
|
|
2520
|
-
#
|
|
2521
|
-
#
|
|
2546
|
+
# THE RULE IS `rules/listing-page.ts`'s `pagePossiblyTruncated`, and this is
|
|
2547
|
+
# its shell copy. `pr-list` runs on every board refresh, so the shell keeps the
|
|
2548
|
+
# rule rather than asking a bundle (docs/shell-and-domain.md), and
|
|
2549
|
+
# `packages/domain/corpus/listing-page.corpus.test.ts` holds the pair:
|
|
2522
2550
|
#
|
|
2523
2551
|
# 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
|
-
#
|
|
2552
|
+
# AT LEAST the requested limit. Fewer rows than
|
|
2553
|
+
# the limit PROVES completeness.
|
|
2554
|
+
# bitbucket (ONE FIXED PAGE): the listing ignores --limit and returns one page
|
|
2555
|
+
# per state. A page SHORTER than the page length
|
|
2556
|
+
# is the last page, so it is complete. A page AT
|
|
2557
|
+
# the length stays possibly truncated.
|
|
2558
|
+
#
|
|
2559
|
+
# THE PAGE LENGTH IS A READING, NEVER A CONSTANT THE RULE NAMES. The listing
|
|
2560
|
+
# asks `pagelen=$BB_LIST_PAGELEN` through `bb api`, and the length holds only
|
|
2561
|
+
# where that `bb` passes the query through unchanged, so it is read per version
|
|
2562
|
+
# from `BB_LIST_PAGE_VERSIONS` — pinned like `BB_ISSUE_VERSION`. A version
|
|
2563
|
+
# nobody measured has no length, and every non-empty page under it stays
|
|
2564
|
+
# possibly truncated: the rule never calls a page complete it cannot prove. A
|
|
2565
|
+
# future length of 100 is a new table entry, never a page of 50 read as short.
|
|
2566
|
+
#
|
|
2567
|
+
# Measured on `quatico/quaweb-website`: bb 1.9.0 returned 50 merged rows in one
|
|
2568
|
+
# request on 2026-09-30, and on 2026-10-01 answered `pagelen: 50` with 50 rows,
|
|
2569
|
+
# a `next` and `size: 895`. Before
|
|
2570
|
+
# 2026-10-01 every non-empty Bitbucket page was reported, so 20 open PRs read as
|
|
2571
|
+
# truncated on every refresh (#1137).
|
|
2572
|
+
#
|
|
2573
|
+
# THE PER-BRANCH SWEEP AND THE WINDOWED LISTING MAKE NO PAGE CLAIM. The sweep
|
|
2574
|
+
# proves completeness per branch and the window refuses a short read, so this
|
|
2575
|
+
# detector is not asked about either (`pr_list_states`).
|
|
2540
2576
|
#
|
|
2541
2577
|
# No --limit was requested → the caller accepted the host's default page and is
|
|
2542
2578
|
# owed no report, so no existing no-limit caller's behaviour changes.
|
|
@@ -2555,18 +2591,40 @@ jira_check() {
|
|
|
2555
2591
|
# future diff that teaches the scan to fall back, without moving the failure
|
|
2556
2592
|
# into a minutes-long pulse. See the plan's Done-when item 3.
|
|
2557
2593
|
#
|
|
2594
|
+
# The page length Bitbucket's plain listing asks for. `bb_state_listing` sends
|
|
2595
|
+
# it, and `pr_list_report_truncation` reads it as the page length only for a
|
|
2596
|
+
# `bb` in BB_LIST_PAGE_VERSIONS.
|
|
2597
|
+
BB_LIST_PAGELEN=50
|
|
2598
|
+
|
|
2599
|
+
# The `bb` versions measured to pass `pagelen` through `bb api` unchanged,
|
|
2600
|
+
# space-separated. `adapters/host/listing-paging.ts` holds the same table.
|
|
2601
|
+
BB_LIST_PAGE_VERSIONS="1.9.0"
|
|
2602
|
+
|
|
2603
|
+
# The page length the answering `bb` lists by, or nothing where its version is
|
|
2604
|
+
# not in BB_LIST_PAGE_VERSIONS. Reads the version `bb_require_json` recorded,
|
|
2605
|
+
# so it costs no `bb` call; a skipped capability check records no version.
|
|
2606
|
+
bb_list_page_length() {
|
|
2607
|
+
local v="${BB_CAP_IDENTITY#*/}"
|
|
2608
|
+
case " $BB_LIST_PAGE_VERSIONS " in
|
|
2609
|
+
*" $v "*) printf '%s\n' "$BB_LIST_PAGELEN" ;;
|
|
2610
|
+
esac
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2558
2613
|
# $1 backend $2 requested limit (may be "") $3 state word $4 row count
|
|
2559
2614
|
pr_list_report_truncation() {
|
|
2560
|
-
local be="$1" limit="$2" state="$3" count="$4"
|
|
2615
|
+
local be="$1" limit="$2" state="$3" count="$4" len=""
|
|
2561
2616
|
[ -n "$limit" ] || return 0 # no --limit → no completeness claim owed
|
|
2562
2617
|
[ "$count" -gt 0 ] 2>/dev/null || return 0 # an empty page had nothing to hide
|
|
2563
2618
|
if [ "$be" = "github" ]; then
|
|
2564
2619
|
# github honours the limit: complete unless the page came back AT the limit.
|
|
2565
2620
|
[ "$count" -ge "$limit" ] 2>/dev/null || return 0
|
|
2621
|
+
else
|
|
2622
|
+
# A fixed page: complete below a measured page length, unprovable otherwise.
|
|
2623
|
+
[ "$be" = "bitbucket" ] && len="$(bb_list_page_length)"
|
|
2624
|
+
if [ -n "$len" ] && [ "$count" -lt "$len" ] 2>/dev/null; then return 0; fi
|
|
2566
2625
|
fi
|
|
2567
|
-
#
|
|
2568
|
-
#
|
|
2569
|
-
# the states that were capped, not a whole-call flag that over-reports.
|
|
2626
|
+
# Named per state so a caller can resolve exactly the states that were
|
|
2627
|
+
# capped, not a whole-call flag that over-reports.
|
|
2570
2628
|
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
2629
|
}
|
|
2572
2630
|
|
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
|