@plot-pm/board 0.16.3 → 0.18.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 +252 -153
- package/package.json +3 -4
- package/plot-agent-manifest.sh +9 -44
- package/plot-config.sh +16 -14
- package/plot-default-branch.sh +15 -19
- package/plot-dispatch.sh +29 -26
- package/plot-fleet-scan.sh +154 -141
- package/plot-host.sh +51 -59
- package/plot-monitor-subject.sh +7 -8
- package/plot-worker-state.sh +10 -163
- package/plot-build-monitor.sh +0 -480
- package/plot-transcript-quiet.sh +0 -172
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plot-pm/board",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Local Kanban board for Plot — a glanceable view of plan phases from docs/plans, with sprint and story filters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
"dist/board-server.mjs",
|
|
25
25
|
"plot-agent-monitor.sh",
|
|
26
26
|
"plot-agent-manifest.sh",
|
|
27
|
-
"plot-build-monitor.sh",
|
|
28
27
|
"plot-monitor-subject.sh",
|
|
29
28
|
"plot-approve.sh",
|
|
30
29
|
"plot-budget.sh",
|
|
@@ -41,7 +40,6 @@
|
|
|
41
40
|
"plot-release-refs.sh",
|
|
42
41
|
"plot-state-receipt.sh",
|
|
43
42
|
"plot-tmp.sh",
|
|
44
|
-
"plot-transcript-quiet.sh",
|
|
45
43
|
"plot-worker-state.sh"
|
|
46
44
|
],
|
|
47
45
|
"scripts": {
|
|
@@ -63,6 +61,7 @@
|
|
|
63
61
|
"@types/react": "^19.2.0",
|
|
64
62
|
"@types/react-dom": "^19.2.0",
|
|
65
63
|
"@vitejs/plugin-react": "^6.0.0",
|
|
64
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
66
65
|
"class-variance-authority": "^0.7.1",
|
|
67
66
|
"clsx": "^2.1.1",
|
|
68
67
|
"esbuild": "^0.28.0",
|
|
@@ -75,7 +74,7 @@
|
|
|
75
74
|
"typescript": "^5.9.0",
|
|
76
75
|
"vite": "^8.1.0",
|
|
77
76
|
"vite-plugin-singlefile": "^2.3.0",
|
|
78
|
-
"vitest": "
|
|
77
|
+
"vitest": "4.1.11",
|
|
79
78
|
"zod": "^4.4.0"
|
|
80
79
|
},
|
|
81
80
|
"engines": {
|
package/plot-agent-manifest.sh
CHANGED
|
@@ -43,47 +43,12 @@ manifest_string() { # $1=manifest $2=field → prints the value, or nothing
|
|
|
43
43
|
printf '%s' "$value"
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
# A non-negative integer field, or `0`. The node call itself only ever prints
|
|
47
|
-
# a clean digit string or "0", so a failed call is the one case needing a
|
|
48
|
-
# shell-side fallback.
|
|
49
|
-
manifest_count() { # $1=manifest $2=field → prints a count
|
|
50
|
-
local manifest="$1"
|
|
51
|
-
[ -n "$manifest" ] && [ -f "$manifest" ] || { printf '0'; return 0; }
|
|
52
|
-
node -e '
|
|
53
|
-
const fs = require("fs");
|
|
54
|
-
try {
|
|
55
|
-
const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
56
|
-
const n = manifest[process.argv[2]];
|
|
57
|
-
process.stdout.write(Number.isInteger(n) && n >= 0 ? String(n) : "0");
|
|
58
|
-
} catch { process.stdout.write("0"); }
|
|
59
|
-
' "$manifest" "$2" 2>/dev/null || printf '0'
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
# Raise a non-negative integer field by one, leaving every other field verbatim.
|
|
63
|
-
#
|
|
64
|
-
# THROUGH A TEMP FILE AND A RENAME, the shape every writer here takes: a reader
|
|
65
|
-
# never sees a partial manifest. A write that fails leaves the file alone and
|
|
66
|
-
# reports it; an absent manifest is not a failure.
|
|
67
|
-
raise_manifest_count() { # $1=manifest $2=field
|
|
68
|
-
local manifest="$1" tmp="$1.plot-count-tmp"
|
|
69
|
-
[ -n "$manifest" ] && [ -f "$manifest" ] || return 0
|
|
70
|
-
node -e '
|
|
71
|
-
const fs = require("fs");
|
|
72
|
-
const [file, field, tmp] = process.argv.slice(1);
|
|
73
|
-
const manifest = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
74
|
-
const n = manifest[field];
|
|
75
|
-
manifest[field] = (Number.isInteger(n) && n >= 0 ? n : 0) + 1;
|
|
76
|
-
fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n");
|
|
77
|
-
' "$manifest" "$2" "$tmp" 2>/dev/null || { rm -f "$tmp"; return 1; }
|
|
78
|
-
mv -f "$tmp" "$manifest" 2>/dev/null || { rm -f "$tmp"; return 1; }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
46
|
# Clear `branch` when a slice finishes, so the window before the next one is
|
|
82
47
|
# observable.
|
|
83
48
|
#
|
|
84
49
|
# WHY THIS EXISTS. `free = process alive AND manifest names no branch`, and the
|
|
85
50
|
# second half was unreachable. `seal_declaration` runs the moment a branch is
|
|
86
|
-
# done;
|
|
51
|
+
# done; the hop that follows runs after `--next` answers and a worktree is
|
|
87
52
|
# built. Between those two points the agent genuinely holds no slice and the
|
|
88
53
|
# manifest still named the last one, so `isFree`'s empty-branch arm — written,
|
|
89
54
|
# exported and unit-tested since `a-dispatch-asks-for-a-free-agent` — had no
|
|
@@ -95,7 +60,7 @@ raise_manifest_count() { # $1=manifest $2=field
|
|
|
95
60
|
# and the liveness check with it, since both are keyed on the worktree path.
|
|
96
61
|
# `wavesCount` counts hops and no hop has happened yet. The node one-liner
|
|
97
62
|
# round-trips the whole object, so every other field survives verbatim, the same
|
|
98
|
-
# property
|
|
63
|
+
# property the hop's own write records.
|
|
99
64
|
#
|
|
100
65
|
# ADDED, NOT SUBSTITUTED. The hop still rewrites `branch` and `worktree`
|
|
101
66
|
# together; this writes the empty value that sits between two slices. A worker
|
|
@@ -104,7 +69,7 @@ raise_manifest_count() { # $1=manifest $2=field
|
|
|
104
69
|
#
|
|
105
70
|
# ABSENT IS NOT A FAILURE. No manifest — a hand-started loop, an older
|
|
106
71
|
# dispatcher — means there is nothing to clear and nothing to report, so this
|
|
107
|
-
# returns 0 like
|
|
72
|
+
# returns 0 like the hop's own write does.
|
|
108
73
|
clear_manifest_branch() { # $1=manifest
|
|
109
74
|
local manifest="$1"
|
|
110
75
|
[ -f "$manifest" ] || return 0
|
|
@@ -149,13 +114,13 @@ plot_session_id() {
|
|
|
149
114
|
# into both, and they part at the first hop to a new branch: `session` names the
|
|
150
115
|
# agent and its manifest file for the agent's whole life, while `resumeId` names
|
|
151
116
|
# the current slice's conversation and is what the board joins the transcript
|
|
152
|
-
# on. Reading this field rather than `$PLOT_SESSION_ID` is what makes the
|
|
153
|
-
#
|
|
154
|
-
#
|
|
117
|
+
# on. Reading this field rather than `$PLOT_SESSION_ID` is what makes the hop's
|
|
118
|
+
# write (`writeResumeId` in `manifest-stamp.ts`) mean anything: a reader asks
|
|
119
|
+
# for the handle, and gets the one the hop last wrote.
|
|
155
120
|
#
|
|
156
|
-
# A PARSE FAILURE AND AN ABSENT MANIFEST ARE ONE ANSWER
|
|
157
|
-
#
|
|
158
|
-
#
|
|
121
|
+
# A PARSE FAILURE AND AN ABSENT MANIFEST ARE ONE ANSWER: no handle. A
|
|
122
|
+
# hand-started loop has no manifest, and a manifest nobody can read is not a
|
|
123
|
+
# handle.
|
|
159
124
|
manifest_resume_id() { # $1=manifest → prints the handle, or nothing
|
|
160
125
|
manifest_string "$1" resumeId
|
|
161
126
|
}
|
package/plot-config.sh
CHANGED
|
@@ -144,6 +144,18 @@
|
|
|
144
144
|
# it does today and names `no-brief-command` as the
|
|
145
145
|
# reason, so a project that has never set the key behaves
|
|
146
146
|
# exactly as before.
|
|
147
|
+
# Agent runner command | sdk: how the JS loop runs a worker's prompt.
|
|
148
|
+
# Absent = command.
|
|
149
|
+
# Agent models `role = model` list (`worker = sonnet`); a charter wins.
|
|
150
|
+
# Agent max turns turns one SDK run may take. Absent = 150.
|
|
151
|
+
# Slice max runs SDK runs one slice may start. Absent = 12.
|
|
152
|
+
# Agent context window an SDK run's `autoCompactWindow`. Absent = 200000.
|
|
153
|
+
# Agent max spend dollars one SDK run may spend (`maxBudgetUsd`); the run
|
|
154
|
+
# then ends `spend-limit`. `$` and `USD` read, e.g. `$20`.
|
|
155
|
+
# Slice max spend dollars one slice's runs may total, read from the slice-
|
|
156
|
+
# spend record before each run. Both: absent/blank = no
|
|
157
|
+
# limit; present and unreadable (0, negative, text) refuses
|
|
158
|
+
# the worker's start.
|
|
147
159
|
# Plot 2 posture keys (repo-declared ceremony bounds; all optional):
|
|
148
160
|
# Plan PRs required | never | optional (never = hard gate)
|
|
149
161
|
# Implementation home this repo | <repo/path list> | none
|
|
@@ -202,10 +214,7 @@ cmd="${1:?Usage: plot-config.sh get <key> [default]}"
|
|
|
202
214
|
key="${2:?Usage: plot-config.sh get <key> [default]}"
|
|
203
215
|
default="${3:-}"
|
|
204
216
|
|
|
205
|
-
|
|
206
|
-
echo "plot-config: unknown subcommand '$cmd' (only 'get' is supported)" >&2
|
|
207
|
-
exit 1
|
|
208
|
-
fi
|
|
217
|
+
[ "$cmd" = "get" ] || { echo "plot-config: unknown subcommand '$cmd' (only 'get' is supported)" >&2; exit 1; }
|
|
209
218
|
|
|
210
219
|
# THE CALLER'S ROOT IS TAKEN WHERE IT OFFERS ONE. `git rev-parse` is ~5 ms and
|
|
211
220
|
# this script runs once per config key, so a caller reading several keys pays
|
|
@@ -219,11 +228,8 @@ fi
|
|
|
219
228
|
# exported stale path would otherwise make every config read answer from a
|
|
220
229
|
# repository that is not this one, silently. Asking git is the safe answer and
|
|
221
230
|
# stays the default for every caller that offers nothing.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
else
|
|
225
|
-
root=$(git rev-parse --show-toplevel 2>/dev/null) || root="."
|
|
226
|
-
fi
|
|
231
|
+
root="${PLOT_REPO_ROOT:-}"
|
|
232
|
+
[ -n "$root" ] && [ -d "$root" ] || root=$(git rev-parse --show-toplevel 2>/dev/null) || root="."
|
|
227
233
|
|
|
228
234
|
# Find the first repo-root file that contains a ## Plot Config section.
|
|
229
235
|
# CLAUDE.md wins for backwards compatibility; AGENTS.md is the modern fallback.
|
|
@@ -261,8 +267,4 @@ if [ -n "$config_file" ]; then
|
|
|
261
267
|
s/^[[:space:]]+//; s/[[:space:]]+$//') # trim ends
|
|
262
268
|
fi
|
|
263
269
|
|
|
264
|
-
|
|
265
|
-
printf '%s\n' "$value"
|
|
266
|
-
else
|
|
267
|
-
printf '%s\n' "$default"
|
|
268
|
-
fi
|
|
270
|
+
printf '%s\n' "${value:-$default}"
|
package/plot-default-branch.sh
CHANGED
|
@@ -29,19 +29,12 @@
|
|
|
29
29
|
# now points at, so a second occurrence is visible in a log rather than
|
|
30
30
|
# invisible in a working system.
|
|
31
31
|
#
|
|
32
|
-
# IT
|
|
33
|
-
#
|
|
34
|
-
# somebody's choice, and `--auto` would silently overrule it.
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
# repoints `origin/HEAD` at a `plot-corpus-pin` ref it creates in `beforeAll`
|
|
39
|
-
# and restores in `afterAll` — deliberately, so two readings of the estate see
|
|
40
|
-
# one world. While that suite runs the pin RESOLVES, so this repairs nothing and
|
|
41
|
-
# the suite is unaffected. What was measured on 2026-09-04 is the state after a
|
|
42
|
-
# run that never reached its `afterAll`: the symref left behind, the ref it
|
|
43
|
-
# names gone. Fixing that belongs to the suite; this repairs the symptom and
|
|
44
|
-
# reports it loudly enough that the cause stays findable.
|
|
32
|
+
# IT REPAIRS A SYMREF THAT RESOLVES ONLY WHEN IT NAMES `plot-corpus-pin`. A
|
|
33
|
+
# clone whose `origin/HEAD` deliberately names another non-default branch is
|
|
34
|
+
# somebody's choice, and `--auto` would silently overrule it. No operator
|
|
35
|
+
# chooses the pin: `packages/domain/corpus/refs.corpus.test.ts` pins its own
|
|
36
|
+
# disposable clone, so a symref naming it in any other repository is the
|
|
37
|
+
# leftover of an interrupted run.
|
|
45
38
|
|
|
46
39
|
# Whether `refs/remotes/origin/HEAD` names a ref that exists.
|
|
47
40
|
#
|
|
@@ -51,14 +44,17 @@
|
|
|
51
44
|
# Usage: origin_head_resolves [<repo-dir>]
|
|
52
45
|
# Returns: 0 when it resolves — including when there is no symref at all, which
|
|
53
46
|
# is a FRESH CLONE rather than a corruption and has nothing to repair.
|
|
47
|
+
# A symref naming `plot-corpus-pin` is corrupt even when it resolves:
|
|
48
|
+
# that name is left behind only by an interrupted corpus run, never by
|
|
49
|
+
# a live one, since the corpus pins its own clone.
|
|
54
50
|
origin_head_resolves() {
|
|
55
51
|
local dir="${1:-.}" target
|
|
56
52
|
target=$(git -C "$dir" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null) || return 0
|
|
57
|
-
|
|
53
|
+
case "$target" in '') return 0 ;; refs/remotes/origin/plot-corpus-pin) return 1 ;; esac
|
|
58
54
|
git -C "$dir" rev-parse --verify --quiet "${target}^{commit}" >/dev/null 2>&1
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
# Repairs
|
|
57
|
+
# Repairs a `refs/remotes/origin/HEAD` that `origin_head_resolves` refuses, and says what it did.
|
|
62
58
|
#
|
|
63
59
|
# The repair is `git remote set-head origin --auto`, which asks the remote. It
|
|
64
60
|
# is cheap — measured under a second — and it is the same command that fixed
|
|
@@ -70,16 +66,16 @@ origin_head_resolves() {
|
|
|
70
66
|
# this is a self-heal on a path that has its own refusal downstream,
|
|
71
67
|
# so it must never become a second way to stop.
|
|
72
68
|
repair_origin_head() {
|
|
73
|
-
local dir="${1:-.}" was
|
|
69
|
+
local dir="${1:-.}" was why
|
|
74
70
|
origin_head_resolves "$dir" && return 0
|
|
75
71
|
|
|
76
72
|
was=$(git -C "$dir" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)
|
|
73
|
+
why="does not resolve"; [ "$was" != origin/plot-corpus-pin ] || why="is the corpus pin, left by an interrupted corpus run"
|
|
77
74
|
if ! git -C "$dir" remote set-head origin --auto >/dev/null 2>&1; then
|
|
78
|
-
echo "plot: origin/HEAD points at '$was', which
|
|
75
|
+
echo "plot: origin/HEAD points at '$was', which $why, and 'git remote set-head origin --auto' failed — the remote could not be asked." >&2
|
|
79
76
|
return 0
|
|
80
77
|
fi
|
|
81
|
-
now
|
|
82
|
-
echo "plot: repaired origin/HEAD — it pointed at '$was', which does not exist on the remote; it now points at '${now:-<unset>}'." >&2
|
|
78
|
+
echo "plot: repaired origin/HEAD — it pointed at '$was', which $why; it now points at '$(git -C "$dir" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || echo '<unset>')'." >&2
|
|
83
79
|
return 0
|
|
84
80
|
}
|
|
85
81
|
|
package/plot-dispatch.sh
CHANGED
|
@@ -1374,14 +1374,12 @@ start_worker() {
|
|
|
1374
1374
|
#
|
|
1375
1375
|
# EVERY WORKER IS BORN MONITORED, AND THAT IS ENFORCED HERE OR NOWHERE.
|
|
1376
1376
|
#
|
|
1377
|
-
#
|
|
1378
|
-
# the agent
|
|
1379
|
-
#
|
|
1380
|
-
#
|
|
1381
|
-
#
|
|
1382
|
-
#
|
|
1383
|
-
# (`plot-worker-state.sh`'s `plot_worker_idle_watch_pass`), and the wrapper
|
|
1384
|
-
# itself reports `gone`/`clear` after `wait "$agent"` returns, below.
|
|
1377
|
+
# The AgentMonitor starts INSIDE the wrapper, as its child, immediately
|
|
1378
|
+
# before the agent, watching the desk. The PROCESS is no longer a monitor's
|
|
1379
|
+
# subject: since `bug/the-loop-reports-idle` the loop's own watcher judges `idle`
|
|
1380
|
+
# (`board/plot-worker-loop.mjs`'s `idleVerdict`, built from the domain's
|
|
1381
|
+
# `idleNow`), and the wrapper itself reports `gone`/`clear` after
|
|
1382
|
+
# `wait "$agent"` returns, below.
|
|
1385
1383
|
#
|
|
1386
1384
|
# WHY INSIDE THE WRAPPER RATHER THAN BESIDE IT. The wrapper already outlives
|
|
1387
1385
|
# its agent by construction — it must, or there would be no exit code to
|
|
@@ -1481,14 +1479,8 @@ start_worker() {
|
|
|
1481
1479
|
fi
|
|
1482
1480
|
fi
|
|
1483
1481
|
|
|
1484
|
-
local agent_monitor=''
|
|
1482
|
+
local agent_monitor=''
|
|
1485
1483
|
[ -x "$script_dir/plot-agent-monitor.sh" ] && agent_monitor="$script_dir/plot-agent-monitor.sh"
|
|
1486
|
-
# THE THIRD MONITOR, born the same way and for the same reason. It watches the
|
|
1487
|
-
# RUN — a Build is its own entity in the spec, so a monitor per entity is the
|
|
1488
|
-
# pattern rather than an exception to it. Its cadence is the WorkerMonitor's
|
|
1489
|
-
# 30 s rather than the AgentMonitor's 300 s, and it can afford that against a
|
|
1490
|
-
# HOST because it asks nothing while no run is live.
|
|
1491
|
-
[ -x "$script_dir/plot-build-monitor.sh" ] && build_monitor="$script_dir/plot-build-monitor.sh"
|
|
1492
1484
|
local stamp_now
|
|
1493
1485
|
stamp_now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
1494
1486
|
# THE THREE NAMES THE CHARTER DECLARED, and they travel as env vars for the
|
|
@@ -1563,11 +1555,10 @@ start_worker() {
|
|
|
1563
1555
|
PLOT_MANIFEST_FILE="$manifest_dir/$session.json" \
|
|
1564
1556
|
PLOT_STAMP_STARTED="$stamp_now" \
|
|
1565
1557
|
PLOT_AGENT_MONITOR="$agent_monitor" \
|
|
1566
|
-
PLOT_BUILD_MONITOR="$build_monitor" \
|
|
1567
1558
|
PLOT_EXIT_FILE="$wt/.plot-worker.exit" PLOT_PID_FILE="$wt/.plot-worker.pid" \
|
|
1568
1559
|
PLOT_WRAPPER_PID_FILE="$wt/.plot-worker.wrapper.pid" \
|
|
1569
1560
|
PLOT_SCRIPT_DIR="$script_dir" \
|
|
1570
|
-
exec nohup sh -c 'printf "%s" "$$" > "$PLOT_WRAPPER_PID_FILE"; wmon=""; amon="";
|
|
1561
|
+
exec nohup sh -c 'printf "%s" "$$" > "$PLOT_WRAPPER_PID_FILE"; wmon=""; amon=""; if [ -n "$PLOT_AGENT_MONITOR" ]; then "$PLOT_AGENT_MONITOR" & amon=$!; fi; PATH="$PLOT_SCRIPT_DIR:$PATH"; export PATH; ( '"$cmd"' ) & agent=$!; trap "" TERM; printf "%s" "$agent" > "$PLOT_PID_FILE"; if [ -f "$PLOT_MANIFEST_FILE" ]; then awk -v pid="$agent" -v started="$PLOT_STAMP_STARTED" -v wrapper="$$" -v wmon="$wmon" -v amon="$amon" '"'"'
|
|
1571
1562
|
BEGIN { relaunch = 0; count = 1; stamped = 0 }
|
|
1572
1563
|
FNR == NR {
|
|
1573
1564
|
if ($0 ~ /^ "pid": "[^"]*",$/) {
|
|
@@ -1585,7 +1576,6 @@ start_worker() {
|
|
|
1585
1576
|
print " \"wrapperPid\": \"" wrapper "\","
|
|
1586
1577
|
print " \"workerMonitorPid\": \"" wmon "\","
|
|
1587
1578
|
print " \"agentMonitorPid\": \"" amon "\","
|
|
1588
|
-
print " \"buildMonitorPid\": \"" bmon "\","
|
|
1589
1579
|
if (relaunch) {
|
|
1590
1580
|
print " \"previousPid\": \"" displaced "\","
|
|
1591
1581
|
print " \"relaunches\": " count ","
|
|
@@ -1595,7 +1585,6 @@ start_worker() {
|
|
|
1595
1585
|
$0 ~ /^ "wrapperPid": "[^"]*",$/ { next }
|
|
1596
1586
|
$0 ~ /^ "workerMonitorPid": "[^"]*",$/ { next }
|
|
1597
1587
|
$0 ~ /^ "agentMonitorPid": "[^"]*",$/ { next }
|
|
1598
|
-
$0 ~ /^ "buildMonitorPid": "[^"]*",$/ { next }
|
|
1599
1588
|
relaunch && $0 ~ /^ "previousPid": "[^"]*",$/ { next }
|
|
1600
1589
|
relaunch && $0 ~ /^ "relaunches": [0-9]+,$/ { next }
|
|
1601
1590
|
relaunch && $0 ~ /^ "startedAt": "[^"]*"$/ { print " \"startedAt\": \"" started "\""; next }
|
|
@@ -3043,7 +3032,9 @@ esac
|
|
|
3043
3032
|
# merged PR outlives the branch it was cut from.
|
|
3044
3033
|
#
|
|
3045
3034
|
# `NONE` AND SILENCE ARE DIFFERENT ANSWERS. `NONE` means the host was asked and
|
|
3046
|
-
# has never seen a PR for that branch
|
|
3035
|
+
# has never seen a PR for that branch. For a name in `plot-fleet-scan.sh
|
|
3036
|
+
# --slice-names` — a slice of any live plan, not deferred — that is a slice
|
|
3037
|
+
# nobody started, which is `waiting`; for any other name it is `blocked`. A host that
|
|
3047
3038
|
# could not be asked is neither permission nor proof of a typo, so it HOLDS the
|
|
3048
3039
|
# branch at `waiting`. Both refuse; only one tells the operator to fix the plan.
|
|
3049
3040
|
|
|
@@ -3172,13 +3163,27 @@ declare -a waits_freed=()
|
|
|
3172
3163
|
# already sets: a dry run that offers what a real run would refuse is worse than
|
|
3173
3164
|
# no dry run — it is the same wrong answer with a reassurance attached.
|
|
3174
3165
|
run_waits_preflight() { # → prints refusals; fills waits_held, adds to n_skipped
|
|
3175
|
-
local br prereq answer held
|
|
3166
|
+
local br prereq answer held slice_names=""
|
|
3176
3167
|
while IFS=$'\t' read -r br prereq; do
|
|
3177
3168
|
[ -n "$br" ] || continue
|
|
3178
3169
|
answer=$(prereq_answer "$prereq")
|
|
3179
3170
|
case "$answer" in
|
|
3180
3171
|
merged) continue ;;
|
|
3181
|
-
|
|
3172
|
+
# A slice somebody may still start has no PR until its work starts: a
|
|
3173
|
+
# wait, not a typo (#1305). The set is the scan's, of the whole estate,
|
|
3174
|
+
# so a slice of ANOTHER plan reads as it does on the board. Asked once,
|
|
3175
|
+
# and only when the host answered `none`.
|
|
3176
|
+
# ABSENT IS NOT FALSE. A scan that failed or printed nothing leaves the
|
|
3177
|
+
# set unread, and an unread set holds the branch at `waiting` exactly as
|
|
3178
|
+
# an unaskable host does: telling an operator to fix a correct plan is its
|
|
3179
|
+
# own defect. stderr stays visible, and the refusal names the failure.
|
|
3180
|
+
none) [ -n "$slice_names" ] || slice_names=$'\n'"$("$script_dir/plot-fleet-scan.sh" --slice-names)"$'\n' || slice_names=$'\n\n'
|
|
3181
|
+
case "$slice_names" in
|
|
3182
|
+
$'\n\n') held=waiting
|
|
3183
|
+
echo "plot-fleet-scan.sh --slice-names gave no answer: $prereq has no PR, held as waiting rather than called a typo" ;;
|
|
3184
|
+
*$'\n'"$prereq"$'\n'*) held=waiting ;;
|
|
3185
|
+
*) held=blocked ;;
|
|
3186
|
+
esac ;;
|
|
3182
3187
|
*) held=waiting ;;
|
|
3183
3188
|
esac
|
|
3184
3189
|
# `--allow-waiting` SAYS SO ON THE LINE IT OVERRIDES, ONCE PER PREREQUISITE
|
|
@@ -3267,9 +3272,7 @@ worker_state_field() {
|
|
|
3267
3272
|
#
|
|
3268
3273
|
# `worker=` still travels in the footer. It says how this repo is configured,
|
|
3269
3274
|
# which remains a fact about the repo even where it no longer explains a count.
|
|
3270
|
-
|
|
3271
|
-
echo "summary: dispatched=$1 reused=$2 skipped=$3 started=$4 brief=missing worker=$(worker_state_field) brief_asked=${n_brief_asked:-0}"
|
|
3272
|
-
}
|
|
3275
|
+
# The line is printed once, at the end of the run.
|
|
3273
3276
|
|
|
3274
3277
|
# ---------------------------------------------------------------------------
|
|
3275
3278
|
# Parallel-agents cap: warn and raise when exceeded
|
|
@@ -4225,7 +4228,7 @@ book_started ${claimed_now[@]+"${claimed_now[@]}"} || true
|
|
|
4225
4228
|
# appears before the footer.
|
|
4226
4229
|
check_and_update_cap "$n_started"
|
|
4227
4230
|
|
|
4228
|
-
|
|
4231
|
+
echo "summary: dispatched=$n_dispatched reused=$n_reused skipped=$n_skipped started=$n_started brief=missing worker=$(worker_state_field) brief_asked=${n_brief_asked:-0}"
|
|
4229
4232
|
|
|
4230
4233
|
# THE RECEIPT IS SPENT HERE, on the fan-out COMPLETING — never at the gate.
|
|
4231
4234
|
# `plot-controller-gate.sh` clears on a receipt and LEAVES it, so a run that
|