@plot-pm/board 0.16.3 → 0.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plot-pm/board",
3
- "version": "0.16.3",
3
+ "version": "0.17.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": "^4.1.10",
77
+ "vitest": "4.1.11",
79
78
  "zod": "^4.4.0"
80
79
  },
81
80
  "engines": {
@@ -43,41 +43,6 @@ 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
  #
package/plot-config.sh CHANGED
@@ -144,6 +144,16 @@
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
154
+ # run then ends `spend-limit`. Absent = no limit, never 0.
155
+ # Slice max spend dollars one slice's runs may total, read from the
156
+ # slice-spend record before each run. Absent = no limit.
147
157
  # Plot 2 posture keys (repo-declared ceremony bounds; all optional):
148
158
  # Plan PRs required | never | optional (never = hard gate)
149
159
  # Implementation home this repo | <repo/path list> | none
@@ -202,10 +212,7 @@ cmd="${1:?Usage: plot-config.sh get <key> [default]}"
202
212
  key="${2:?Usage: plot-config.sh get <key> [default]}"
203
213
  default="${3:-}"
204
214
 
205
- if [ "$cmd" != "get" ]; then
206
- echo "plot-config: unknown subcommand '$cmd' (only 'get' is supported)" >&2
207
- exit 1
208
- fi
215
+ [ "$cmd" = "get" ] || { echo "plot-config: unknown subcommand '$cmd' (only 'get' is supported)" >&2; exit 1; }
209
216
 
210
217
  # THE CALLER'S ROOT IS TAKEN WHERE IT OFFERS ONE. `git rev-parse` is ~5 ms and
211
218
  # this script runs once per config key, so a caller reading several keys pays
@@ -219,11 +226,8 @@ fi
219
226
  # exported stale path would otherwise make every config read answer from a
220
227
  # repository that is not this one, silently. Asking git is the safe answer and
221
228
  # stays the default for every caller that offers nothing.
222
- if [ -n "${PLOT_REPO_ROOT:-}" ] && [ -d "$PLOT_REPO_ROOT" ]; then
223
- root="$PLOT_REPO_ROOT"
224
- else
225
- root=$(git rev-parse --show-toplevel 2>/dev/null) || root="."
226
- fi
229
+ root="${PLOT_REPO_ROOT:-}"
230
+ [ -n "$root" ] && [ -d "$root" ] || root=$(git rev-parse --show-toplevel 2>/dev/null) || root="."
227
231
 
228
232
  # Find the first repo-root file that contains a ## Plot Config section.
229
233
  # CLAUDE.md wins for backwards compatibility; AGENTS.md is the modern fallback.
@@ -261,8 +265,4 @@ if [ -n "$config_file" ]; then
261
265
  s/^[[:space:]]+//; s/[[:space:]]+$//') # trim ends
262
266
  fi
263
267
 
264
- if [ -n "$value" ]; then
265
- printf '%s\n' "$value"
266
- else
267
- printf '%s\n' "$default"
268
- fi
268
+ printf '%s\n' "${value:-$default}"
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
- # Two monitors start INSIDE the wrapper, as its children, immediately before
1378
- # the agent: one watches the desk (`plot-agent-monitor.sh`), one watches the
1379
- # run (`plot-build-monitor.sh`). Each has a subject the other does not and a
1380
- # cadence it cannot share — minutes on the host, seconds on a run but only
1381
- # while one is live. The PROCESS is no longer a third monitor's subject:
1382
- # since `bug/the-loop-reports-idle` the loop's own watcher judges `idle`
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='' build_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=""; bmon=""; 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=$!; 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" -v bmon="$bmon" '"'"'
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 — a typo, which is `blocked`. A host that
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
- none) held=blocked ;;
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
- print_summary() { # $1=dispatched $2=reused $3=skipped $4=started
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
- print_summary "$n_dispatched" "$n_reused" "$n_skipped" "$n_started"
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