@plot-pm/board 0.9.1 → 0.11.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.
@@ -276,6 +276,32 @@ plot_worker_log() { # $1=worktree → path to the worker log, or "" (non-zero)
276
276
  [ -e "$wt/.plot-worker.log" ] || return 1
277
277
  printf '%s' "$wt/.plot-worker.log"
278
278
  }
279
+ # Why did the worker in this worktree end?
280
+ #
281
+ # A SEPARATE READING FROM `plot_worker_state`, and deliberately so. That function
282
+ # answers what the PROCESS did and what the desk holds; this answers why the
283
+ # worker stopped, which is a fact only the worker itself could record. The two
284
+ # come apart in the case that matters: a worker ended by the floor and one ended
285
+ # by a monitor finding both leave `.plot-worker.exit` holding `124`, so the exit
286
+ # code cannot tell them apart and the state word does not try to.
287
+ #
288
+ # IT IS NOT FOLDED INTO THE STATE ROW. `plot_worker_state` prints three
289
+ # tab-separated fields that three callers and one domain port parse positionally;
290
+ # a fourth field would change a contract this reading does not need to change.
291
+ # `plot_worker_log` is the precedent — a standalone reader beside the state
292
+ # function, asked by whoever wants it.
293
+ #
294
+ # ABSENT IS ABSENT, and it is load-bearing. No worktree, or no ending file in
295
+ # one, prints nothing and returns non-zero — which is what a SIGKILLed worker
296
+ # leaves behind, and what every worker that ran before this record existed
297
+ # leaves too. The domain's `readEnding` keeps that apart from a file that exists
298
+ # and does not parse; this prints the bytes and decides neither.
299
+ plot_worker_ending() { # $1=worktree → the ending record's JSON, or "" (non-zero)
300
+ local wt="$1"
301
+ [ -n "$wt" ] || return 1
302
+ [ -f "$wt/.plot-worker.ending.json" ] || return 1
303
+ cat "$wt/.plot-worker.ending.json" 2>/dev/null
304
+ }
279
305
 
280
306
  # Is a person being waited on inside this worktree?
281
307
  #
@@ -513,58 +539,63 @@ plot_worker_activity() { # $1=pid → working | idle | "" (empty = nothing to me
513
539
 
514
540
  # Refine a clean exit into finished / waiting / stalled.
515
541
  #
516
- # THE ORDER IS LOAD-BEARING, and each step earns its place from a measured
517
- # mistake rather than from tidiness:
518
- #
519
- # AN OPEN OR MERGED PR OUTRANKS EVERYTHING BELOW IT. Work that reached review
520
- # has left the worker's hands, so leftover local edits mean nothing there a
521
- # scratch file beside a merged PR is not unfinished work.
522
- #
523
- # `waiting` OUTRANKS `stalled`, because a marker is the worker saying *your
524
- # turn*, and a worker asking a question has almost always left the work it was
525
- # doing uncommitted beside the question. Checking dirtiness first would report
526
- # every such branch `stalled`. Measured: a guard restarted one branch TWICE
527
- # while its worker waited on an answer, and the second restart re-ran work the
528
- # first had finished. That is a loop, not a rescue.
529
- #
530
- # UNCOMMITTED **OR** UNPUSHED. Committing clears dirtiness, so a worker that
531
- # tidied up and stopped before pushing would otherwise read `finished` with
532
- # nobody able to see its commits. Both are "work only this machine holds".
542
+ # THE DECISION IS NOT MADE HERE. `taskState` lives in `@plot-pm/domain` and this
543
+ # function asks it. The four readings and the order they are decided in — a PR
544
+ # outranking everything, `waiting` outranking `stalled`, an unanswerable
545
+ # `unpushed` refusing to become `stalled` are one implementation now, testable
546
+ # over all 24 combinations of the four readings instead of over the worktrees an
547
+ # estate happens to produce. That is what this script carried in duplicate until
548
+ # 2026-08-18, five of six states in two places, and the copies had drifted.
549
+ #
550
+ # WHAT STAYS HERE IS THE READING. The four world questions below are shell's,
551
+ # and each is asked exactly as it was: the marker is a FILE `plot_worker_blocked`
552
+ # globs for, dirtiness is `plot_worker_dirty`'s filtered answer, and `unpushed`
553
+ # is `@{upstream}` and nothing else.
554
+ #
555
+ # ONLY `@{upstream}` ANSWERS "PUSHED?", and when there is no upstream the
556
+ # question is UNANSWERABLE rather than answered zero or answered anything
557
+ # else. This went in the wrong direction first and was measured doing it: a
558
+ # fallback that counted against `origin/main` reported EVERY clean branch
559
+ # `stalled` in a repo with no remote, because `rev-list --count "..HEAD"` with
560
+ # an empty left side counts the whole history from the root commit. Nine commits
561
+ # of ordinary history read as nine commits of unpushed work.
562
+ #
563
+ # The fallback was also wrong where it worked. A branch legitimately ahead of
564
+ # `origin/main` is the NORMAL state of every branch under review — it is what
565
+ # having commits means — so counting against the trunk marks finished work
566
+ # `stalled` for as long as it exists. Only the branch's OWN upstream separates
567
+ # "pushed" from "not pushed"; the trunk answers a different question entirely.
568
+ #
569
+ # So an absent upstream travels to the rule as an EMPTY field, which the rule
570
+ # reads as unanswerable and does not turn into `stalled`. A failure to observe
571
+ # is not evidence of something to see — the same principle `local_ahead_of`
572
+ # states in plot-fleet-scan.sh, reached the hard way.
573
+ #
574
+ # A RULE THAT CANNOT BE ASKED REFUSES, and it says so with the rebuild in the
575
+ # message — the shape `plot-fleet-scan.sh` uses at its own bundle call. There is
576
+ # no shell fallback: a second implementation kept "just in case" is the
577
+ # duplication this move removes, and it would be the copy nobody tests. An
578
+ # EMPTY answer is checked separately from a non-zero exit, because a bundle that
579
+ # writes nothing exits 0 and `||` alone cannot see it.
533
580
  plot_worker_task_state() { # $1=worktree $2=pr-fact → finished|waiting|stalled
534
- local wt="$1" has_pr="$2"
535
- [ "$has_pr" = "pr" ] && { printf 'finished'; return; }
536
- plot_worker_blocked "$wt" && { printf 'waiting'; return; }
537
- [ -n "$(plot_worker_dirty "$wt")" ] && { printf 'stalled'; return; }
581
+ local wt="$1" has_pr="$2" here ahead has_pr_flag blocked_flag dirty_flag answer
582
+ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
583
+
584
+ [ "$has_pr" = "pr" ] && has_pr_flag=1 || has_pr_flag=0
585
+ plot_worker_blocked "$wt" && blocked_flag=1 || blocked_flag=0
586
+ [ -n "$(plot_worker_dirty "$wt")" ] && dirty_flag=1 || dirty_flag=0
587
+
538
588
  # UNPUSHED IS A REF QUESTION, asked THROUGH the worktree because that is the
539
- # checkout whose HEAD is the branch.
540
- #
541
- # ONLY `@{upstream}` ANSWERS IT, and when there is no upstream the question is
542
- # UNANSWERABLE rather than answered zero — or answered anything else. This
543
- # went in the wrong direction first and was measured doing it: a fallback that
544
- # counted against `origin/main` reported EVERY clean branch `stalled` in a
545
- # repo with no remote, because `rev-list --count "..HEAD"` with an empty left
546
- # side counts the whole history from the root commit. Nine commits of ordinary
547
- # history read as nine commits of unpushed work.
548
- #
549
- # The fallback was also wrong where it worked. A branch legitimately ahead of
550
- # `origin/main` is the NORMAL state of every branch under review — it is what
551
- # having commits means — so counting against the trunk marks finished work
552
- # `stalled` for as long as it exists. Only the branch's OWN upstream separates
553
- # "pushed" from "not pushed"; the trunk answers a different question entirely.
554
- #
555
- # So an absent upstream yields no verdict here and falls through to
556
- # `finished`, which is the answer the branch gave before this state existed.
557
- # A failure to observe is not evidence of something to see — the same
558
- # principle `local_ahead_of` states in plot-fleet-scan.sh, reached the hard
559
- # way.
560
- local ahead
561
- if ahead=$(git -C "$wt" rev-list --count '@{upstream}..HEAD' 2>/dev/null); then
562
- case "$ahead" in
563
- ''|0|*[!0-9]*) ;;
564
- *) printf 'stalled'; return ;;
565
- esac
566
- fi
567
- printf 'finished'
589
+ # checkout whose HEAD is the branch. An unreadable count stays EMPTY.
590
+ ahead=$(git -C "$wt" rev-list --count '@{upstream}..HEAD' 2>/dev/null) || ahead=""
591
+
592
+ answer=$(printf '%s\t%s\t%s\t%s' \
593
+ "$has_pr_flag" "$blocked_flag" "$dirty_flag" "$ahead" \
594
+ | node "$here/board/plot-task.mjs" 2>/dev/null) \
595
+ || { echo "error: cannot read the task state — run 'pnpm build:board'." >&2; exit 2; }
596
+ [ -n "$answer" ] \
597
+ || { echo "error: the task state rule answered nothing — run 'pnpm build:board'." >&2; exit 2; }
598
+ printf '%s' "$answer"
568
599
  }
569
600
 
570
601
  # Classify the worker in a worktree.