@plot-pm/board 0.15.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/plot-config.sh CHANGED
@@ -39,7 +39,11 @@
39
39
  # Non-numeric or empty falls back to the default. It bounds
40
40
  # a HUNG agent — one whose CLI crashed without exiting — so
41
41
  # one dead worker cannot hold a slot for hours.
42
- # Agent registry the directory the dispatcher writes agent manifests to,
42
+ # Temp sweep after hours before `plot-reap.sh --sweep-temp` removes an
43
+ # owned `$TMPDIR/plot-*` entry or a dead pid's budget
44
+ # memo that a SIGKILL left behind. Default 24; a value
45
+ # that is not a whole number is refused by the sweep.
46
+ # Agent registry the directory the dispatcher writes agent manifests to,
43
47
  # read by the board's registry. Default `.plot/agents`
44
48
  # (repo-relative, gitignored, hence per-worktree). A board
45
49
  # served from a worktree the dispatcher never wrote to
@@ -48,6 +52,53 @@
48
52
  # the board finds the registry wherever it was started.
49
53
  # Absent = the default, so a single-checkout project is
50
54
  # unaffected.
55
+ # Board artifact the board-server.mjs this repository runs, read by
56
+ # plot-board-probe.sh. Declared, it is resolved FIRST and
57
+ # reported as `artifact_source: checkout`; absent, the
58
+ # order stays plugin, npm, checkout — the adopting
59
+ # project's case, unchanged. It exists because a
60
+ # repository that BUILDS the artifact must run the one it
61
+ # built: without it `pnpm build:board` writes a file the
62
+ # board never reads whenever a plugin is installed, and
63
+ # the symptom looks like the fix not working.
64
+ # A relative value resolves against the MAIN CHECKOUT (the
65
+ # parent of `--git-common-dir`), never `--show-toplevel`,
66
+ # so a dispatch desk resolves the same file rather than
67
+ # its own copy; an absolute one is taken as given.
68
+ # A declared file that is missing reports `none` and does
69
+ # NOT fall back to the plugin — the key names which
70
+ # artifact runs, so a silent substitution is the wrong
71
+ # answer it removes. Absent = today's order.
72
+ # Agent settings a JSON settings file every `claude -p` the fleet starts
73
+ # is given, through `--settings`. It names the plugins this
74
+ # project's agents start WITHOUT. Every dispatched agent
75
+ # inherits every `SessionStart` hook the operator's plugins
76
+ # declare, and the fleet starts a session on every worker
77
+ # start, restart, retry and hop plus every agent-runner
78
+ # command the board runs: measured 2026-09-30, one plugin's
79
+ # lockless sync ran three times at once, the 1-minute load
80
+ # reached 195, and the supervisor did not tick for 12
81
+ # minutes.
82
+ # Resolved by `plot-agent-settings.sh`, which prints an
83
+ # absolute path (exit 0), nothing for an absent or empty key
84
+ # (exit 0), or nothing with the reason on stderr (exit 3)
85
+ # for a missing, unparseable or gate-disabling file. A
86
+ # relative value resolves against the MAIN CHECKOUT (the
87
+ # parent of `--git-common-dir`), never `--show-toplevel`,
88
+ # for `Board artifact`'s reason: a desk must resolve the
89
+ # same file, and one cut from an older main may not hold it.
90
+ # The path travels to every consumer as
91
+ # `PLOT_AGENT_SETTINGS`, and each command key interpolates
92
+ # `${PLOT_AGENT_SETTINGS:+--settings "$PLOT_AGENT_SETTINGS"}`
93
+ # itself — Plot rewrites no configured command.
94
+ # A file setting any `plot@…` plugin false, `disableAllHooks`
95
+ # true, or ANY `env` key is REFUSED: those switch Plot's own
96
+ # four gates off, and a settings `PATH` hiding the gates'
97
+ # tools makes them fail open. It is a check against an
98
+ # accidental switch-off, not a boundary — the file is
99
+ # project-owned and reviewed like this one.
100
+ # Absent or empty = no change, so an adopting project that
101
+ # sets nothing behaves exactly as today.
51
102
  # Worktree root where /plot-dispatch creates fleet worktrees. A relative
52
103
  # value resolves against the repo root, an absolute one is
53
104
  # taken as given. Absent = the default `repo_root/..` with
@@ -58,14 +109,15 @@
58
109
  # branch" read asks `git worktree list` instead.
59
110
  # Agent-runner keys (optional; Plot hardcodes no agent tooling, Principle 5):
60
111
  # Worker command how /plot-dispatch runs an agent headless on a worktree.
61
- # `none` = asked, and this repo starts workers by hand —
62
- # a DELIBERATE absence, distinct from a missing key so
63
- # /plot-dispatch stops asking at every fan-out. Never run
64
- # as a command. Absent = nobody has been asked yet, and
65
- # the first dispatch asks (never /plot-init: at adoption
66
- # the question meets a need the answerer does not have,
67
- # gets a shrug, and an answered-and-wrong key is harder
68
- # to fix than a missing one).
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.
69
121
  # Approve command how the board runs `/plot-approve <slug>`; the prompt is
70
122
  # appended as one argument. Absent = the board's Approve
71
123
  # button renders disabled, naming this key as the fix.
package/plot-deliver.sh CHANGED
@@ -51,6 +51,7 @@
51
51
  set -uo pipefail
52
52
 
53
53
  script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
54
+ . "$script_dir/plot-tmp.sh"
54
55
 
55
56
  # The receipt this script leaves for plot-state-gate.sh, which refuses every
56
57
  # other writer of a `State:` line. Sourced rather than run: the gate and the
@@ -660,7 +661,8 @@ git -C "$tmpwt" add -- "${DELIVERED_DIR#/}" >/dev/null 2>&1 || true
660
661
  # THE BOOKED PLAN, KEPT FOR THE TRACKER. The booking worktree is removed on
661
662
  # every exit below, and the working tree may still read `Approved`; the issue
662
663
  # status is decided from the file that reached the default branch.
663
- booked_plan=$(mktemp "${TMPDIR:-/tmp}/plot-deliver-plan.XXXXXX")
664
+ booked_plan=""
665
+ plot_tmpfile booked_plan deliver-plan
664
666
  cp "$tmpwt/$rel" "$booked_plan" 2>/dev/null || : > "$booked_plan"
665
667
 
666
668
  if git -C "$tmpwt" diff --cached --quiet 2>/dev/null; then
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
@@ -178,6 +180,7 @@
178
180
  set -uo pipefail
179
181
 
180
182
  script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
183
+ . "$script_dir/plot-tmp.sh"
181
184
 
182
185
  # The shared worker classifier. Sourced by both this script and
183
186
  # plot-fleet-scan.sh so a worker has ONE state, not one per reader.
@@ -341,7 +344,7 @@ while [ $# -gt 0 ]; do
341
344
  # `--release` pushed it from 78 to 91. Two records
342
345
  # of one fact, and nothing compares them — a stale number here silently
343
346
  # truncates the help rather than failing, so it is checked by a test.
344
- -h|--help) sed -n '2,91p' "$0"; exit 0 ;;
347
+ -h|--help) sed -n '2,93p' "$0"; exit 0 ;;
345
348
  *) slug="$1" ;;
346
349
  esac
347
350
  shift
@@ -392,28 +395,9 @@ fi
392
395
  # until this moved. Nothing here executes at definition time; only the position
393
396
  # changed.
394
397
 
395
- # A session id, in the shape the runtime uses for its transcript filename.
396
- #
397
- # `uuidgen` where it exists (macOS and most Linux), falling back to `/dev/urandom`
398
- # — never to `$RANDOM` or a timestamp. Two workers launched in the same second by
399
- # the same fan-out would collide on either, and a collision here silently merges
400
- # two agents into one manifest.
401
- #
402
- # Lowercased because the runtime writes its transcript filename in lowercase and
403
- # the board joins on exact string equality; `uuidgen` on macOS returns uppercase.
404
- plot_session_id() {
405
- local id=""
406
- if command -v uuidgen >/dev/null 2>&1; then
407
- id=$(uuidgen 2>/dev/null | tr 'A-Z' 'a-z')
408
- fi
409
- if [ -z "$id" ]; then
410
- # 16 random bytes rendered as a v4-shaped id. The shape matters only for
411
- # recognisability; nothing parses it.
412
- id=$(od -An -tx1 -N16 /dev/urandom 2>/dev/null | tr -d ' \n' \
413
- | sed -E 's/(.{8})(.{4})(.{4})(.{4})(.{12})/\1-\2-\3-\4-\5/')
414
- fi
415
- printf '%s' "$id"
416
- }
398
+ # `plot_session_id` — the session id a launch records — is SOURCED from
399
+ # `plot-agent-manifest.sh` above, because the worker loop mints one on a hop to
400
+ # a new branch and two generators of one id drift.
417
401
 
418
402
  # JSON-escape one string for a manifest value.
419
403
  #
@@ -806,8 +790,20 @@ request_brief() { # $1 = branch, $2 = slug → 0 if a command was started
806
790
  # this outlives the dispatch run by design, because the fan-out must not block
807
791
  # on a `claude -p` session of unknown length. `setsid` is not used — it does
808
792
  # not exist on macOS, where most of this fleet runs.
793
+ # THE SETTINGS FILE THIS PROJECT STARTS ITS AGENTS WITH. The `Brief command` is
794
+ # a `claude -p` session like any other agent and inherits every `SessionStart`
795
+ # hook the operator's plugins declare, so it carries the same variable the
796
+ # worker loop exports — and the configured command interpolates
797
+ # `${PLOT_AGENT_SETTINGS:+--settings "$PLOT_AGENT_SETTINGS"}` itself.
798
+ #
799
+ # SET INLINE rather than exported, because this spawn already names its
800
+ # environment on the command line. A refusal or an absent key leaves it EMPTY,
801
+ # which `${VAR:+…}` reads as nothing — so a broken config cannot stop a brief
802
+ # being written.
803
+ _brief_settings="$(bash "$(dirname "${BASH_SOURCE[0]}")/plot-agent-settings.sh" 2>/dev/null || echo "")"
809
804
  ( cd "$repo_root" \
810
805
  && PLOT_UNATTENDED=1 PLOT_PLAN_SLUG="$bslug" PLOT_BRIEF_BRANCH="$branch" \
806
+ PLOT_AGENT_SETTINGS="$_brief_settings" \
811
807
  nohup sh -c "$cmd \"\$@\"" plot-brief \
812
808
  "$(brief_prompt "$branch" "$bslug")" \
813
809
  >"$log" 2>&1 </dev/null & ) 2>/dev/null
@@ -1028,6 +1024,46 @@ handover_refusal() { # $1=branch $2=worktree $3=state → 0 may hand over, 1 ref
1028
1024
  return 0
1029
1025
  }
1030
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
+
1031
1067
  start_worker() {
1032
1068
  local branch="$1" wt="$2"
1033
1069
  local cmd
@@ -1090,11 +1126,26 @@ start_worker() {
1090
1126
  return 1
1091
1127
  fi
1092
1128
 
1093
- cmd=$("$script_dir/plot-config.sh" get "Worker command" "")
1094
1129
  # `none` means "asked, and this repo starts them by hand". Running it would
1095
1130
  # spawn a worker per branch that fails with `none: command not found` — a
1096
- # deliberate answer turned into N crashed workers.
1097
- case "$cmd" in none|NONE|None) cmd="" ;; esac
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
1098
1149
  if [ -z "$cmd" ]; then
1099
1150
  # Not an error: Plot deliberately hardcodes no agent tooling (Principle 5).
1100
1151
  # Word it as the next step rather than a failure, or a first run reads as
@@ -1105,11 +1156,7 @@ start_worker() {
1105
1156
  # in the summary, because per-branch output is exactly where it was missed
1106
1157
  # five times on 2026-08-17. Saying it in both places would train the reader
1107
1158
  # to skip both.
1108
- if [ "$worker_cmd_declined" = 1 ]; then
1109
- echo " worktree ready — start it yourself:"
1110
- else
1111
- echo " worktree ready — no 'Worker command' configured, so start it yourself:"
1112
- fi
1159
+ echo " worktree ready — start it yourself:"
1113
1160
  echo " cd $wt # branch $branch is claimed and waiting"
1114
1161
  return 1
1115
1162
  fi
@@ -1454,7 +1501,8 @@ start_worker() {
1454
1501
  PLOT_BUILD_MONITOR="$build_monitor" \
1455
1502
  PLOT_EXIT_FILE="$wt/.plot-worker.exit" PLOT_PID_FILE="$wt/.plot-worker.pid" \
1456
1503
  PLOT_WRAPPER_PID_FILE="$wt/.plot-worker.wrapper.pid" \
1457
- 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; ( '"$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" '"'"'
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" '"'"'
1458
1506
  BEGIN { relaunch = 0; count = 1; stamped = 0 }
1459
1507
  FNR == NR {
1460
1508
  if ($0 ~ /^ "pid": "[^"]*",$/) {
@@ -1731,11 +1779,30 @@ if [ "$mode" = "stop" ]; then
1731
1779
  case "$st" in
1732
1780
  running*)
1733
1781
  pid=${st#running }
1734
- kill "$pid" 2>/dev/null && echo "stopped $stop_branch (pid $pid)" \
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)" \
1735
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
1736
1798
  echo " Check it: ps -p $pid -o pid=,stat=,command=" >&2
1737
1799
  echo " Nothing else was written; the worktree and the claim stand." >&2
1738
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
1739
1806
  # The worktree and its claim are left in place: the branch is still taken,
1740
1807
  # and deleting either would be the kind of write this design avoids.
1741
1808
  echo " worktree kept at $wt — the claim stands until you release it:"
@@ -2239,6 +2306,38 @@ EOF
2239
2306
  start_headroom=${start_rest%%$'\t'*}
2240
2307
  start_why=${start_rest#*$'\t'}
2241
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
+
2242
2341
  echo "starting $start_n agent(s) — machine $start_headroom, $start_running already running"
2243
2342
 
2244
2343
  # THE WORKER COMMAND IS ASKED ONCE, BEFORE THE LOOP. `start_worker` prints its
@@ -2252,14 +2351,18 @@ EOF
2252
2351
  # summary — which is now a performer as well as a person — must be able to
2253
2352
  # tell *the machine bounded it* from *nobody has configured how to start one*.
2254
2353
  worker_cmd_declined=0
2255
- start_worker_state=configured
2256
- case "$("$script_dir/plot-config.sh" get "Worker command" "")" in
2257
- none|NONE|None) worker_cmd_declined=1; start_worker_state=declined ;;
2258
- '') start_worker_state=unconfigured ;;
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 ;;
2259
2358
  esac
2260
2359
  if [ "$start_worker_state" != configured ]; then
2261
2360
  echo " no worker will start — 'Worker command' is $start_worker_state in this repo's Plot Config."
2262
- echo " The desks below are cut and registered; start them by hand, or set the key."
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
2263
2366
  fi
2264
2367
 
2265
2368
  # `slug` STAYS EMPTY, and the loop reads it. A free agent belongs to no plan
@@ -2659,18 +2762,18 @@ fi
2659
2762
  # materialised into a temp file rather than parsed here — the parser stays the
2660
2763
  # one place that knows what a plan file looks like.
2661
2764
  #
2662
- # The template's X's must TRAIL: BSD mktemp (macOS) rejects a template with a
2663
- # suffix after them, while GNU accepts it. The first version wrote
2664
- # `plot-gate-XXXXXX.md` and failed on macOS — and because the failure fell back
2665
- # to the working tree, the gate silently went back to reading the exact surface
2666
- # this fix exists to stop reading. Hence also: NO working-tree fallback below.
2667
- # If the shared blob cannot be materialised, the gate refuses.
2765
+ # `plot-tmp.sh` puts the template's X's last: BSD mktemp (macOS) rejects a
2766
+ # template with a suffix after them, while GNU accepts it. The first version
2767
+ # wrote `plot-gate-XXXXXX.md` and failed on macOS — and because the failure fell
2768
+ # back to the working tree, the gate silently went back to reading the exact
2769
+ # surface this fix exists to stop reading. Hence also: NO working-tree fallback
2770
+ # below. If the shared blob cannot be materialised, the gate refuses.
2668
2771
  plan_file="$plan_path"
2669
2772
  gate_blob=""
2670
2773
  if [ -n "$gate_sha" ]; then
2671
- gate_dir=$(mktemp -d "${TMPDIR:-/tmp}/plot-gate-XXXXXX") || gate_dir=""
2774
+ gate_dir=""
2775
+ plot_tmpdir gate_dir gate || gate_dir=""
2672
2776
  if [ -n "$gate_dir" ]; then
2673
- trap 'rm -rf "$gate_dir"' EXIT
2674
2777
  gate_blob="$gate_dir/$(basename "$plan_path")"
2675
2778
  git show "$gate_ref:$plan_path" >"$gate_blob" 2>/dev/null || gate_blob=""
2676
2779
  fi