@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plot-pm/board",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
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",
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
- # `none` = asked, and this repo starts workers by hand —
113
- # a DELIBERATE absence, distinct from a missing key so
114
- # /plot-dispatch stops asking at every fan-out. Never run
115
- # as a command. Absent = nobody has been asked yet, and
116
- # the first dispatch asks (never /plot-init: at adoption
117
- # the question meets a need the answerer does not have,
118
- # gets a shrug, and an answered-and-wrong key is harder
119
- # 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.
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,91p' "$0"; exit 0 ;;
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
- 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
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
- if [ "$worker_cmd_declined" = 1 ]; then
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
- 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" '"'"'
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
- 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)" \
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
- start_worker_state=configured
2250
- case "$("$script_dir/plot-config.sh" get "Worker command" "")" in
2251
- none|NONE|None) worker_cmd_declined=1; start_worker_state=declined ;;
2252
- '') 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 ;;
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
- 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
2257
2366
  fi
2258
2367
 
2259
2368
  # `slug` STAYS EMPTY, and the loop reads it. A free agent belongs to no plan
@@ -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>", a single token with no tabs.
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)