@plot-pm/board 0.16.0 → 0.16.1
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 +84 -84
- package/package.json +1 -1
- package/plot-config.sh +9 -8
- package/plot-dispatch.sh +125 -16
- package/plot-fleet-scan.sh +13 -3
package/package.json
CHANGED
package/plot-config.sh
CHANGED
|
@@ -109,14 +109,15 @@
|
|
|
109
109
|
# branch" read asks `git worktree list` instead.
|
|
110
110
|
# Agent-runner keys (optional; Plot hardcodes no agent tooling, Principle 5):
|
|
111
111
|
# Worker command how /plot-dispatch runs an agent headless on a worktree.
|
|
112
|
-
# `
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
# the
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
112
|
+
# /plot-init writes `PLOT_UNATTENDED=1 plot-worker-loop.sh`;
|
|
113
|
+
# the bare name resolves to the loop beside
|
|
114
|
+
# plot-dispatch.sh, which puts its own directory first on
|
|
115
|
+
# PATH. A free agent (`--start`) refuses a command that
|
|
116
|
+
# does not run the loop, because only the loop waits for a
|
|
117
|
+
# slice. Absent = not set up: nothing starts, and
|
|
118
|
+
# /plot-dispatch offers to write the loop. `none` = asked,
|
|
119
|
+
# and this repo starts workers by hand — a DELIBERATE
|
|
120
|
+
# absence, never run as a command.
|
|
120
121
|
# Approve command how the board runs `/plot-approve <slug>`; the prompt is
|
|
121
122
|
# appended as one argument. Absent = the board's Approve
|
|
122
123
|
# button renders disabled, naming this key as the fix.
|
package/plot-dispatch.sh
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
# hand each a queued slice with nobody touching a desk. The count
|
|
15
15
|
# is a REQUEST: a machine at its bound answers with fewer and says
|
|
16
16
|
# so, and the shortfall is reported rather than remembered.
|
|
17
|
+
# A `Worker command` that does not run plot-worker-loop.sh
|
|
18
|
+
# refuses (exit 1, `worker=no-loop`): only the loop waits.
|
|
17
19
|
# --restart <br>
|
|
18
20
|
# start a worker on <br>, which already holds a claim — the
|
|
19
21
|
# counterpart to --stop, and the only way to hand a stopped
|
|
@@ -342,7 +344,7 @@ while [ $# -gt 0 ]; do
|
|
|
342
344
|
# `--release` pushed it from 78 to 91. Two records
|
|
343
345
|
# of one fact, and nothing compares them — a stale number here silently
|
|
344
346
|
# truncates the help rather than failing, so it is checked by a test.
|
|
345
|
-
-h|--help) sed -n '2,
|
|
347
|
+
-h|--help) sed -n '2,93p' "$0"; exit 0 ;;
|
|
346
348
|
*) slug="$1" ;;
|
|
347
349
|
esac
|
|
348
350
|
shift
|
|
@@ -1022,6 +1024,46 @@ handover_refusal() { # $1=branch $2=worktree $3=state → 0 may hand over, 1 ref
|
|
|
1022
1024
|
return 0
|
|
1023
1025
|
}
|
|
1024
1026
|
|
|
1027
|
+
# WHICH COMMAND STARTS THIS AGENT is the domain's answer: `startCommand`, asked
|
|
1028
|
+
# through `board/plot-start-command.mjs`. An absent `Worker command` is not set
|
|
1029
|
+
# up and starts nothing, `none` is declined, and a free agent refuses a command
|
|
1030
|
+
# that does not run the loop (#1124). The loop's name and the value that runs it
|
|
1031
|
+
# are this script's readings; the rule names no script.
|
|
1032
|
+
#
|
|
1033
|
+
# THE VALUE IS THE BARE NAME, `PLOT_UNATTENDED=1 plot-worker-loop.sh`, the one
|
|
1034
|
+
# `/plot-init` writes: a plugin path carries a version and an absolute path
|
|
1035
|
+
# carries a machine. The launch puts this script's directory first on PATH, so
|
|
1036
|
+
# the bare name resolves to the loop shipped beside it.
|
|
1037
|
+
#
|
|
1038
|
+
# It sets `start_cmd_verb` (`run`, `unconfigured`, `declined`, `refused` or
|
|
1039
|
+
# `unaskable`), `start_cmd_run`, and `start_cmd_why` / `start_cmd_repair` where
|
|
1040
|
+
# they apply. `unaskable` is a bundle that could not answer, which starts
|
|
1041
|
+
# nothing.
|
|
1042
|
+
ask_start_command() { # $1 = free | assigned
|
|
1043
|
+
local answer rc
|
|
1044
|
+
start_cmd_verb="" start_cmd_run="" start_cmd_why="" start_cmd_repair=""
|
|
1045
|
+
start_cmd_configured=$("$script_dir/plot-config.sh" get "Worker command" "")
|
|
1046
|
+
start_cmd_bundle="$script_dir/board/plot-start-command.mjs"
|
|
1047
|
+
answer=$(printf '%s' "$start_cmd_configured" | node "$start_cmd_bundle" "$1" \
|
|
1048
|
+
plot-worker-loop.sh "PLOT_UNATTENDED=1 plot-worker-loop.sh" 2>/dev/null)
|
|
1049
|
+
rc=$?
|
|
1050
|
+
case "$rc:$answer" in
|
|
1051
|
+
0:run$'\t'*) start_cmd_verb=run; start_cmd_run=${answer#run$'\t'} ;;
|
|
1052
|
+
0:unconfigured$'\t'*)
|
|
1053
|
+
start_cmd_verb=unconfigured
|
|
1054
|
+
start_cmd_why="no 'Worker command' is configured"
|
|
1055
|
+
start_cmd_repair=${answer#unconfigured$'\t'}
|
|
1056
|
+
;;
|
|
1057
|
+
0:declined) start_cmd_verb=declined ;;
|
|
1058
|
+
3:*)
|
|
1059
|
+
start_cmd_verb=refused
|
|
1060
|
+
start_cmd_why=${answer%%$'\n'*}
|
|
1061
|
+
start_cmd_repair=${answer#*$'\n'}
|
|
1062
|
+
;;
|
|
1063
|
+
*) start_cmd_verb=unaskable; start_cmd_why="the rule's bundle $start_cmd_bundle answered exit $rc" ;;
|
|
1064
|
+
esac
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1025
1067
|
start_worker() {
|
|
1026
1068
|
local branch="$1" wt="$2"
|
|
1027
1069
|
local cmd
|
|
@@ -1084,11 +1126,26 @@ start_worker() {
|
|
|
1084
1126
|
return 1
|
|
1085
1127
|
fi
|
|
1086
1128
|
|
|
1087
|
-
cmd=$("$script_dir/plot-config.sh" get "Worker command" "")
|
|
1088
1129
|
# `none` means "asked, and this repo starts them by hand". Running it would
|
|
1089
1130
|
# spawn a worker per branch that fails with `none: command not found` — a
|
|
1090
|
-
# deliberate answer turned into N crashed workers.
|
|
1091
|
-
|
|
1131
|
+
# deliberate answer turned into N crashed workers. An absent key starts
|
|
1132
|
+
# nothing either, and its line names the value to set.
|
|
1133
|
+
ask_start_command "$([ -n "$branch" ] && echo assigned || echo free)"
|
|
1134
|
+
case "$start_cmd_verb" in
|
|
1135
|
+
run) cmd="$start_cmd_run" ;;
|
|
1136
|
+
declined) cmd="" ;;
|
|
1137
|
+
unconfigured)
|
|
1138
|
+
echo " worktree ready — ${start_cmd_why}, so nothing started:"
|
|
1139
|
+
echo " $start_cmd_repair."
|
|
1140
|
+
echo " cd $wt # ${branch:+branch $branch is claimed and waiting}"
|
|
1141
|
+
return 1
|
|
1142
|
+
;;
|
|
1143
|
+
*)
|
|
1144
|
+
echo " refusing to start ${branch:-a free agent} — ${start_cmd_why}" >&2
|
|
1145
|
+
[ -n "$start_cmd_repair" ] && echo " Repair: $start_cmd_repair." >&2
|
|
1146
|
+
return 1
|
|
1147
|
+
;;
|
|
1148
|
+
esac
|
|
1092
1149
|
if [ -z "$cmd" ]; then
|
|
1093
1150
|
# Not an error: Plot deliberately hardcodes no agent tooling (Principle 5).
|
|
1094
1151
|
# Word it as the next step rather than a failure, or a first run reads as
|
|
@@ -1099,11 +1156,7 @@ start_worker() {
|
|
|
1099
1156
|
# in the summary, because per-branch output is exactly where it was missed
|
|
1100
1157
|
# five times on 2026-08-17. Saying it in both places would train the reader
|
|
1101
1158
|
# to skip both.
|
|
1102
|
-
|
|
1103
|
-
echo " worktree ready — start it yourself:"
|
|
1104
|
-
else
|
|
1105
|
-
echo " worktree ready — no 'Worker command' configured, so start it yourself:"
|
|
1106
|
-
fi
|
|
1159
|
+
echo " worktree ready — start it yourself:"
|
|
1107
1160
|
echo " cd $wt # branch $branch is claimed and waiting"
|
|
1108
1161
|
return 1
|
|
1109
1162
|
fi
|
|
@@ -1448,7 +1501,8 @@ start_worker() {
|
|
|
1448
1501
|
PLOT_BUILD_MONITOR="$build_monitor" \
|
|
1449
1502
|
PLOT_EXIT_FILE="$wt/.plot-worker.exit" PLOT_PID_FILE="$wt/.plot-worker.pid" \
|
|
1450
1503
|
PLOT_WRAPPER_PID_FILE="$wt/.plot-worker.wrapper.pid" \
|
|
1451
|
-
|
|
1504
|
+
PLOT_SCRIPT_DIR="$script_dir" \
|
|
1505
|
+
nohup sh -c 'printf "%s" "$$" > "$PLOT_WRAPPER_PID_FILE"; wmon=""; amon=""; bmon=""; if [ -n "$PLOT_WORKER_MONITOR" ]; then "$PLOT_WORKER_MONITOR" & wmon=$!; fi; if [ -n "$PLOT_AGENT_MONITOR" ]; then "$PLOT_AGENT_MONITOR" & amon=$!; fi; if [ -n "$PLOT_BUILD_MONITOR" ]; then "$PLOT_BUILD_MONITOR" & bmon=$!; fi; PATH="$PLOT_SCRIPT_DIR:$PATH"; export PATH; ( '"$cmd"' ) & agent=$!; 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" -v bmon="$bmon" '"'"'
|
|
1452
1506
|
BEGIN { relaunch = 0; count = 1; stamped = 0 }
|
|
1453
1507
|
FNR == NR {
|
|
1454
1508
|
if ($0 ~ /^ "pid": "[^"]*",$/) {
|
|
@@ -1725,11 +1779,30 @@ if [ "$mode" = "stop" ]; then
|
|
|
1725
1779
|
case "$st" in
|
|
1726
1780
|
running*)
|
|
1727
1781
|
pid=${st#running }
|
|
1728
|
-
|
|
1782
|
+
# THE WHOLE PROCESS GROUP, NOT ONE PID. Measured 2026-09-30 (#1084):
|
|
1783
|
+
# `--stop` signalled the wrapper, the group leader, and the loop, the
|
|
1784
|
+
# prompt shell, `claude` and its children survived reparented to pid 1;
|
|
1785
|
+
# `kill -TERM -<pgid>` ended all of them. The pid is the fallback when the
|
|
1786
|
+
# group cannot be read, or is this script's own group: a worker started
|
|
1787
|
+
# without job control shares its starter's group, and signalling that
|
|
1788
|
+
# group would stop the caller with it.
|
|
1789
|
+
stop_pgid=$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ')
|
|
1790
|
+
stop_own_pgid=$(ps -o pgid= -p "$$" 2>/dev/null | tr -d ' ')
|
|
1791
|
+
stop_target="$pid"
|
|
1792
|
+
case "$stop_pgid" in
|
|
1793
|
+
''|*[!0-9]*|0|1) ;;
|
|
1794
|
+
*) [ "$stop_pgid" != "$stop_own_pgid" ] && stop_target="-$stop_pgid" ;;
|
|
1795
|
+
esac
|
|
1796
|
+
kill -TERM -- "$stop_target" 2>/dev/null && echo "stopped $stop_branch (pid $pid)" \
|
|
1729
1797
|
|| { echo "plot-dispatch: could not stop pid $pid — it may have exited between the read and the signal, or belong to another user." >&2
|
|
1730
1798
|
echo " Check it: ps -p $pid -o pid=,stat=,command=" >&2
|
|
1731
1799
|
echo " Nothing else was written; the worktree and the claim stand." >&2
|
|
1732
1800
|
exit 1; }
|
|
1801
|
+
if [ "$stop_target" = "$pid" ]; then
|
|
1802
|
+
echo " signalled pid $pid alone — its process group is unreadable or is this run's own"
|
|
1803
|
+
else
|
|
1804
|
+
echo " signalled its whole process group $stop_pgid"
|
|
1805
|
+
fi
|
|
1733
1806
|
# The worktree and its claim are left in place: the branch is still taken,
|
|
1734
1807
|
# and deleting either would be the kind of write this design avoids.
|
|
1735
1808
|
echo " worktree kept at $wt — the claim stands until you release it:"
|
|
@@ -2233,6 +2306,38 @@ EOF
|
|
|
2233
2306
|
start_headroom=${start_rest%%$'\t'*}
|
|
2234
2307
|
start_why=${start_rest#*$'\t'}
|
|
2235
2308
|
|
|
2309
|
+
# A FREE AGENT NEEDS THE LOOP, so a configured command that does not run it
|
|
2310
|
+
# refuses here, before any desk is cut. A free agent starts with an empty
|
|
2311
|
+
# `PLOT_BRANCH`, and only `plot-worker-loop.sh` waits for the registry to hand
|
|
2312
|
+
# it a slice; any other command runs its prompt at once and exits. Measured
|
|
2313
|
+
# 2026-10-01 (#1124): a plain `claude -p "… $PLOT_BRANCH …"` received
|
|
2314
|
+
# "Implementiere den Branch in nach dem Plan", exited within 30 s, and
|
|
2315
|
+
# `--start` reported `agents=1`. A branch dispatch is not affected: a worker
|
|
2316
|
+
# given a branch works on any command.
|
|
2317
|
+
#
|
|
2318
|
+
# THE DECISION IS THE DOMAIN'S: `ask_start_command` asks `startCommand`
|
|
2319
|
+
# through its bundle. A refusal names the defect and the repair; a bundle that
|
|
2320
|
+
# cannot answer starts nothing, as the fleet-size ask above does.
|
|
2321
|
+
# `worker=no-loop` is the footer word the performer reads, the way it reads
|
|
2322
|
+
# `unconfigured` and `declined`.
|
|
2323
|
+
ask_start_command free
|
|
2324
|
+
case "$start_cmd_verb" in
|
|
2325
|
+
run|declined|unconfigured) ;;
|
|
2326
|
+
refused)
|
|
2327
|
+
echo "plot-dispatch: --start refuses — ${start_cmd_why}." >&2
|
|
2328
|
+
echo " configured: $start_cmd_configured" >&2
|
|
2329
|
+
echo " Repair: ${start_cmd_repair}." >&2
|
|
2330
|
+
echo " A branch dispatch (--restart <branch>) starts a worker on any command." >&2
|
|
2331
|
+
echo "summary: agents=0 requested=${start_count:-default} running=$start_running headroom=$start_headroom worker=no-loop"
|
|
2332
|
+
exit 1
|
|
2333
|
+
;;
|
|
2334
|
+
*)
|
|
2335
|
+
echo "plot-dispatch: --start could not ask which command starts an agent — starting none." >&2
|
|
2336
|
+
echo " ${start_cmd_why}." >&2
|
|
2337
|
+
exit 1
|
|
2338
|
+
;;
|
|
2339
|
+
esac
|
|
2340
|
+
|
|
2236
2341
|
echo "starting $start_n agent(s) — machine $start_headroom, $start_running already running"
|
|
2237
2342
|
|
|
2238
2343
|
# THE WORKER COMMAND IS ASKED ONCE, BEFORE THE LOOP. `start_worker` prints its
|
|
@@ -2246,14 +2351,18 @@ EOF
|
|
|
2246
2351
|
# summary — which is now a performer as well as a person — must be able to
|
|
2247
2352
|
# tell *the machine bounded it* from *nobody has configured how to start one*.
|
|
2248
2353
|
worker_cmd_declined=0
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2354
|
+
case "$start_cmd_verb" in
|
|
2355
|
+
declined) worker_cmd_declined=1; start_worker_state=declined ;;
|
|
2356
|
+
unconfigured) start_worker_state=unconfigured ;;
|
|
2357
|
+
*) start_worker_state=configured ;;
|
|
2253
2358
|
esac
|
|
2254
2359
|
if [ "$start_worker_state" != configured ]; then
|
|
2255
2360
|
echo " no worker will start — 'Worker command' is $start_worker_state in this repo's Plot Config."
|
|
2256
|
-
|
|
2361
|
+
if [ "$start_worker_state" = unconfigured ]; then
|
|
2362
|
+
echo " The desks below are cut and registered. Repair: $start_cmd_repair."
|
|
2363
|
+
else
|
|
2364
|
+
echo " The desks below are cut and registered; start them by hand, or set the key."
|
|
2365
|
+
fi
|
|
2257
2366
|
fi
|
|
2258
2367
|
|
|
2259
2368
|
# `slug` STAYS EMPTY, and the loop reads it. A free agent belongs to no plan
|
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)
|