@plot-pm/board 0.15.0 → 0.16.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/plot-dispatch.sh CHANGED
@@ -178,6 +178,7 @@
178
178
  set -uo pipefail
179
179
 
180
180
  script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
181
+ . "$script_dir/plot-tmp.sh"
181
182
 
182
183
  # The shared worker classifier. Sourced by both this script and
183
184
  # plot-fleet-scan.sh so a worker has ONE state, not one per reader.
@@ -392,28 +393,9 @@ fi
392
393
  # until this moved. Nothing here executes at definition time; only the position
393
394
  # changed.
394
395
 
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
- }
396
+ # `plot_session_id` — the session id a launch records — is SOURCED from
397
+ # `plot-agent-manifest.sh` above, because the worker loop mints one on a hop to
398
+ # a new branch and two generators of one id drift.
417
399
 
418
400
  # JSON-escape one string for a manifest value.
419
401
  #
@@ -806,8 +788,20 @@ request_brief() { # $1 = branch, $2 = slug → 0 if a command was started
806
788
  # this outlives the dispatch run by design, because the fan-out must not block
807
789
  # on a `claude -p` session of unknown length. `setsid` is not used — it does
808
790
  # not exist on macOS, where most of this fleet runs.
791
+ # THE SETTINGS FILE THIS PROJECT STARTS ITS AGENTS WITH. The `Brief command` is
792
+ # a `claude -p` session like any other agent and inherits every `SessionStart`
793
+ # hook the operator's plugins declare, so it carries the same variable the
794
+ # worker loop exports — and the configured command interpolates
795
+ # `${PLOT_AGENT_SETTINGS:+--settings "$PLOT_AGENT_SETTINGS"}` itself.
796
+ #
797
+ # SET INLINE rather than exported, because this spawn already names its
798
+ # environment on the command line. A refusal or an absent key leaves it EMPTY,
799
+ # which `${VAR:+…}` reads as nothing — so a broken config cannot stop a brief
800
+ # being written.
801
+ _brief_settings="$(bash "$(dirname "${BASH_SOURCE[0]}")/plot-agent-settings.sh" 2>/dev/null || echo "")"
809
802
  ( cd "$repo_root" \
810
803
  && PLOT_UNATTENDED=1 PLOT_PLAN_SLUG="$bslug" PLOT_BRIEF_BRANCH="$branch" \
804
+ PLOT_AGENT_SETTINGS="$_brief_settings" \
811
805
  nohup sh -c "$cmd \"\$@\"" plot-brief \
812
806
  "$(brief_prompt "$branch" "$bslug")" \
813
807
  >"$log" 2>&1 </dev/null & ) 2>/dev/null
@@ -2659,18 +2653,18 @@ fi
2659
2653
  # materialised into a temp file rather than parsed here — the parser stays the
2660
2654
  # one place that knows what a plan file looks like.
2661
2655
  #
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.
2656
+ # `plot-tmp.sh` puts the template's X's last: BSD mktemp (macOS) rejects a
2657
+ # template with a suffix after them, while GNU accepts it. The first version
2658
+ # wrote `plot-gate-XXXXXX.md` and failed on macOS — and because the failure fell
2659
+ # back to the working tree, the gate silently went back to reading the exact
2660
+ # surface this fix exists to stop reading. Hence also: NO working-tree fallback
2661
+ # below. If the shared blob cannot be materialised, the gate refuses.
2668
2662
  plan_file="$plan_path"
2669
2663
  gate_blob=""
2670
2664
  if [ -n "$gate_sha" ]; then
2671
- gate_dir=$(mktemp -d "${TMPDIR:-/tmp}/plot-gate-XXXXXX") || gate_dir=""
2665
+ gate_dir=""
2666
+ plot_tmpdir gate_dir gate || gate_dir=""
2672
2667
  if [ -n "$gate_dir" ]; then
2673
- trap 'rm -rf "$gate_dir"' EXIT
2674
2668
  gate_blob="$gate_dir/$(basename "$plan_path")"
2675
2669
  git show "$gate_ref:$plan_path" >"$gate_blob" 2>/dev/null || gate_blob=""
2676
2670
  fi
@@ -133,9 +133,9 @@
133
133
  # lifecycle state, verbatim from plot-plan-meta.sh, and the half of a
134
134
  # row's phase git cannot answer. Which column a row reads is composed
135
135
  # from it AND the branch state one layer up; this script decides nothing.
136
- # The plan set also includes plans delivered inside a rolling 24 h
137
- # window (see "the last day of finished work"), so work does not
138
- # disappear at the moment it becomes finished.
136
+ # The plan set also includes every plan at `Delivered`, until it is
137
+ # `Released` (see "the release scope"), so DONE names what the next
138
+ # release ships.
139
139
  # Plans are enumerated from REFS (`git ls-tree`/`git show`), NOT from
140
140
  # the working tree — so the list describes committed state and does not
141
141
  # change while rebases and worker commits rewrite the checkout
@@ -253,6 +253,8 @@ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
253
253
  # plot-dispatch.sh so a worker has ONE state, not one per reader.
254
254
  # shellcheck source=plot-worker-state.sh
255
255
  . "$script_dir/plot-worker-state.sh"
256
+ # Every temp path this scan creates, and its only EXIT/INT/TERM traps.
257
+ . "$script_dir/plot-tmp.sh"
256
258
  cfg() { "$script_dir/plot-config.sh" get "$1" "${2:-}"; }
257
259
 
258
260
  do_fetch=1
@@ -573,14 +575,15 @@ fi
573
575
  # outlives the scan that fetched it — a stale `merged` read from a previous run
574
576
  # is exactly the fabricated verdict the failure direction above forbids.
575
577
  #
576
- # Cleanup is trapped rather than trailing: the script exits early in several
578
+ # Cleanup is registered rather than trailing: the script exits early in several
577
579
  # places (--next with nothing to start, no active plans), and a temp directory
578
- # left behind on those paths would accumulate one per poll.
580
+ # left behind on those paths would accumulate one per poll. It goes through
581
+ # `plot-tmp.sh`'s one registry: this cache had its own EXIT trap, and the
582
+ # `REF_TMP` trap below replaced it, so every scan left one `tmp.*` directory of
583
+ # ~955 files behind (measured 2026-09-30).
579
584
  HOST_STATE_CACHE=""
580
585
  if [ "$HOST_LOOKUP_OK" = 1 ]; then
581
- HOST_STATE_CACHE=$(mktemp -d 2>/dev/null) || HOST_STATE_CACHE=""
582
- [ -n "$HOST_STATE_CACHE" ] \
583
- && trap 'rm -rf "$HOST_STATE_CACHE" 2>/dev/null || true' EXIT INT TERM
586
+ plot_tmpdir HOST_STATE_CACHE fleet-host-state 2>/dev/null || HOST_STATE_CACHE=""
584
587
  fi
585
588
 
586
589
  # The cache key. Shared by the join and by `host_pr_state`, because the two
@@ -2245,37 +2248,15 @@ pr_ready() {
2245
2248
  }
2246
2249
 
2247
2250
  # ---------------------------------------------------------------------------
2248
- # Recently delivered plans: the last day of finished work
2251
+ # Delivered plans: the release scope
2249
2252
  # ---------------------------------------------------------------------------
2250
2253
  #
2251
- # The pulse read `active/` only, so a plan left the view the INSTANT it was
2252
- # delivered — taking every branch with it. Measured on this repo: five plans
2253
- # delivered in one day named eight branches between them, and DONE showed one,
2254
- # because delivery and merge are minutes apart and only whichever branch
2255
- # happened to sit in the gap survived. A group that is full by accident is
2256
- # worse than one that is empty by rule.
2257
- #
2258
- # A ROLLING 24 HOURS, not the calendar day. Literally "delivered today" is
2259
- # easier to explain and wrong at exactly the wrong moment: a plan delivered at
2260
- # 23:50 vanishes ten minutes later, mid-session, while the branches it names
2261
- # are still on screen. 24 is also the one freshness bound this repo already
2262
- # uses (`Claim stale after`), so it is one unit to learn rather than two.
2263
- #
2264
- # THE WINDOW FILTERS BEFORE THE PARSE. Measured: ~57 ms per plan through
2265
- # plot-plan-meta.sh against a scan that already runs 500–1050 ms, so parsing
2266
- # fourteen delivered plans to discard thirteen would roughly double the pulse —
2267
- # and that cost grows with the archive, which only ever gets larger, while the
2268
- # answer stays the size of a day's work. So the cheap signal comes first (the
2269
- # delivered symlink's own mtime) and only the candidates it admits are parsed.
2270
- #
2271
- # The pre-filter may OVER-ADMIT AND PAY A PARSE; it may never exclude. A
2272
- # checkout can freshen an old file, so the `Delivered:` record keeps the last
2273
- # word — but nothing mtime rules out could have been delivered inside the
2274
- # window. On a fresh clone or a CI worktree every file shares one checkout
2275
- # timestamp and ALL of them are admitted: correct, merely slower, once. Reaching
2276
- # for `git log` per plan to avoid that would spend a git call to save a parse.
2277
- DELIVERED_WINDOW_HOURS=$(cfg "Claim stale after" "24")
2278
- case "$DELIVERED_WINDOW_HOURS" in (*[!0-9]*|'') DELIVERED_WINDOW_HOURS=24 ;; esac
2254
+ # A plan at `Delivered` stays in the pulse until it is `Released`, whatever its
2255
+ # age. DONE holds the release scope (`done-holds-what-is-still-yours`): every
2256
+ # plan whose work has landed and whose version has not shipped. A 24-hour window
2257
+ # on the `Delivered:` record bounded this until 2026-10-01, when 25 plans awaited
2258
+ # 2.22.0 and DONE showed 10. Every plan file is parsed in one pass anyway, so the
2259
+ # window bought no parse time; it only hid work the next release ships.
2279
2260
 
2280
2261
 
2281
2262
  # ---------------------------------------------------------------------------
@@ -2441,52 +2422,7 @@ changed_ago_of() { # $1=branch → "<seconds since>\t<epoch of>" for the newest
2441
2422
  if [ "$newest" -gt "$now" ]; then printf '0\t%s' "$newest"; else printf '%s\t%s' "$((now - newest))" "$newest"; fi
2442
2423
  }
2443
2424
 
2444
- # A plan whose delivered symlink was touched inside the window. `find -newermt`
2445
- # is not portable to every BSD find in the wild, so the cutoff is computed and
2446
- # compared with `stat` — one stat per file, no parse.
2447
- delivered_candidates() {
2448
- local cutoff now link mtime
2449
- now=$(date +%s)
2450
- cutoff=$((now - DELIVERED_WINDOW_HOURS * 3600))
2451
- for link in "$DELIVERED_DIR"*.md; do
2452
- [ -e "$link" ] || continue
2453
- # `stat` follows the symlink, which is what we want: the TARGET is the plan,
2454
- # and a plan edited after delivery must still admit. An unreadable time
2455
- # ADMITS rather than excludes — the pre-filter may only over-admit, and the
2456
- # `Delivered:` record has the last word either way.
2457
- mtime=$(file_mtime "$link") || { printf '%s\n' "$link"; continue; }
2458
- [ "$mtime" -ge "$cutoff" ] && printf '%s\n' "$link"
2459
- done
2460
- }
2461
2425
 
2462
- # Does this plan's `Delivered:` record fall inside the window? The RECORD
2463
- # decides — mtime only chose who got asked.
2464
- #
2465
- # "No date, no row." A delivered plan with an empty record does not appear at
2466
- # all: no date means no membership in any window, the same rule the waiting age
2467
- # already follows. Showing it always would create the one row that can never
2468
- # age out of DONE, and the missing record is a bookkeeping fault
2469
- # plot-reconcile-scan.sh exists to report — a view that quietly compensates
2470
- # for it makes the fault harder to see.
2471
- #
2472
- # A BARE DATE IS ANCHORED AT THE END OF ITS DAY, not at midnight, and this is
2473
- # the one detail that makes "rolling, not the calendar day" true rather than
2474
- # merely stated. Every `Delivered:` record in this repo is a bare date, which
2475
- # names no time — so anchoring at 00:00 measures from up to a day BEFORE the
2476
- # delivery, and the window collapses back into exactly the calendar boundary
2477
- # the rolling window exists to avoid: a plan delivered at 23:50 would be an
2478
- # hour from expiry the moment it was written, and gone ten minutes later
2479
- # mid-session while the branches it names are still on screen.
2480
- #
2481
- # Anchoring at 23:59:59 over-admits by at most the length of the delivery day.
2482
- # That is the same direction the mtime pre-filter is allowed to err in, and for
2483
- # the same reason: showing a finished plan slightly too long costs a row, while
2484
- # dropping one mid-session costs the reader the work they were looking at. A
2485
- # record that DOES carry a time is honoured exactly, so the imprecision belongs
2486
- # to the record rather than to the rule.
2487
- # The rule itself now runs inside the ONE estate parse below (`in_window`),
2488
- # because asking it per plan meant an interpreter per plan. What it decides is
2489
- # unchanged; only the number of processes that decide it is.
2490
2426
 
2491
2427
  # ---------------------------------------------------------------------------
2492
2428
  # Plan enumeration: from the REF, not from the tree
@@ -2577,15 +2513,15 @@ ref_ls() { # $1=dir → newline-separated paths under it in origin/$MAIN
2577
2513
  # `ref_plan_file` is called as `$(ref_plan_file ...)`, which runs it in a
2578
2514
  # SUBSHELL. A lazy `[ -z "$REF_TMP" ] && REF_TMP=$(mktemp -d)` inside it
2579
2515
  # assigns in the child and the parent never sees it — so every call made a
2580
- # fresh directory, the parent's variable stayed empty, and the EXIT trap
2581
- # cleaned nothing. Measured while writing this: three plans, three temp dirs,
2516
+ # fresh directory, the parent's variable stayed empty, and the exit cleanup
2517
+ # removed nothing. Measured while writing this: three plans, three temp dirs,
2582
2518
  # none removed. The lifetime is owned out here, where the trap can see it.
2583
2519
  REF_TMP=""
2584
2520
  if [ "$PLAN_SOURCE" = "ref" ]; then
2585
- REF_TMP=$(mktemp -d "${TMPDIR:-/tmp}/plot-fleet-ref.XXXXXX") || REF_TMP=""
2586
2521
  # The scan is read-only and short-lived, and the board polls it every 5 s —
2587
- # a directory that outlives the run would accumulate one per poll.
2588
- [ -n "$REF_TMP" ] && trap 'rm -rf "$REF_TMP"' EXIT INT TERM
2522
+ # a directory that outlives the run would accumulate one per poll, so the
2523
+ # helper removes it at exit.
2524
+ plot_tmpdir REF_TMP fleet-ref || REF_TMP=""
2589
2525
  # No temp dir means no way to hand the parser a file, so the ref path cannot
2590
2526
  # work. Falling back to the checkout is the honest answer, and it announces
2591
2527
  # itself through `plan_source` exactly like an unreadable ref.
@@ -2890,7 +2826,7 @@ is_plan_phase() { # $1=normalized phase → 0 when this file is a plan
2890
2826
  #
2891
2827
  # `record` types, one per line, all tab-separated and all prefixed by the plan
2892
2828
  # file they describe:
2893
- # P <file> <phase> <delivered_in_window> one per parsed file
2829
+ # P <file> <phase> one per parsed file
2894
2830
  # W <file> <wave-idx> <branch> <deferred> <why> <wave-name> <claim>
2895
2831
  #
2896
2832
  # NO ASSOCIATIVE ARRAYS. `/bin/bash` on macOS is 3.2 and this script uses no
@@ -2908,7 +2844,6 @@ is_plan_phase() { # $1=normalized phase → 0 when this file is a plan
2908
2844
  # gave when it failed.
2909
2845
  plan_meta_files=()
2910
2846
  plan_meta_phases=()
2911
- plan_meta_inwindow=()
2912
2847
  plan_meta_waves=()
2913
2848
 
2914
2849
  # Parses every plan file given, filling the four arrays above. Called ONCE.
@@ -2919,33 +2854,6 @@ parse_plan_estate() { # $@=files to parse
2919
2854
  | python3 -c '
2920
2855
  import json, re, sys, time
2921
2856
 
2922
- window = float(sys.argv[1]) * 3600
2923
- now = time.time()
2924
-
2925
- def in_window(raw):
2926
- """The `Delivered:` record against the rolling window — the same rule the
2927
- per-plan test applied, moved into the one pass. A record whose date does
2928
- not parse is dropped rather than coerced: Date-style leniency would turn a
2929
- typo into a confident answer. A record with no time anchors at 23:59:59,
2930
- so a plan delivered at 23:50 is not an hour from expiry the moment it is
2931
- written. A FUTURE record is INSIDE (negative age), because hiding a live
2932
- plan for a mistyped year costs more than showing one."""
2933
- raw = (raw or "").strip()
2934
- if not raw:
2935
- return False
2936
- m = re.match(r"(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2}))?", raw)
2937
- if not m:
2938
- return False
2939
- y, mo, dy, hh, mi = m.groups()
2940
- timed = hh is not None
2941
- try:
2942
- at = time.mktime((int(y), int(mo), int(dy),
2943
- int(hh) if timed else 23, int(mi) if timed else 59,
2944
- 0 if timed else 59, 0, 0, -1))
2945
- except (ValueError, OverflowError):
2946
- return False
2947
- return (now - at) <= window
2948
-
2949
2857
  def clean(s):
2950
2858
  return str(s).replace("\t", " ").replace("\n", " ")
2951
2859
 
@@ -2963,8 +2871,7 @@ for line in sys.stdin:
2963
2871
  f = d.get("file")
2964
2872
  if not f:
2965
2873
  continue
2966
- print("\t".join(["P", clean(f), clean(d.get("phase", "")),
2967
- "1" if in_window(d.get("delivered_raw")) else "0"]))
2874
+ print("\t".join(["P", clean(f), clean(d.get("phase", ""))]))
2968
2875
  for i, w in enumerate(d.get("waves", []) or []):
2969
2876
  name = w.get("name")
2970
2877
  for b in w.get("branches", []) or []:
@@ -2991,7 +2898,7 @@ for line in sys.stdin:
2991
2898
  (b.get("deferred_reason") or "-"),
2992
2899
  (b.get("waits_on") or "-"),
2993
2900
  name or "-", b.get("claimed") or "-"]))
2994
- ' "$DELIVERED_WINDOW_HOURS" 2>/dev/null) || records=""
2901
+ ' 2>/dev/null) || records=""
2995
2902
 
2996
2903
  local kind file rest
2997
2904
  while IFS=$'\t' read -r kind file rest; do
@@ -2999,9 +2906,8 @@ for line in sys.stdin:
2999
2906
  case "$kind" in
3000
2907
  P)
3001
2908
  plan_meta_files+=("$file")
3002
- # `rest` is "<phase>\t<inwindow>"; both are single tokens with no tabs.
3003
- plan_meta_phases+=("${rest%% *}")
3004
- plan_meta_inwindow+=("${rest##* }")
2909
+ # `rest` is "<phase>", a single token with no tabs.
2910
+ plan_meta_phases+=("$rest")
3005
2911
  plan_meta_waves+=("")
3006
2912
  ;;
3007
2913
  W)
@@ -3125,8 +3031,8 @@ else
3125
3031
  # was buying less than it appeared to: it keyed off the `$DELIVERED_DIR`
3126
3032
  # symlink's mtime, and a fresh checkout stamps every symlink at once — 56 of
3127
3033
  # 56 delivered links admitted here, so the parse it was meant to avoid was
3128
- # already being paid in full. `delivered_in_window` (the `Delivered:` record)
3129
- # was always the filter that actually decided, and the pre-filter's own
3034
+ # already being paid in full. The `Delivered:` record's window was the filter
3035
+ # that actually decided until 2026-10-01, when the phase alone took over, and the pre-filter's own
3130
3036
  # contract was that it may only ever OVER-admit. Removing it takes that
3131
3037
  # contract to its limit — strictly more correct, and on this repo not even
3132
3038
  # more expensive.
@@ -3374,7 +3280,7 @@ EOF
3374
3280
  echo "$total $n"
3375
3281
  }
3376
3282
 
3377
- # WHAT WAS READ OF ONE BRANCH — ten tab-separated fields, and no decision.
3283
+ # WHAT WAS READ OF ONE BRANCH — eleven tab-separated fields, and no decision.
3378
3284
  #
3379
3285
  # `branch_state()` UNTIL THIS SLICE, and every line of git archaeology below is
3380
3286
  # its own, unchanged. What went is the `if` chain that merged these readings
@@ -3391,10 +3297,11 @@ EOF
3391
3297
  # run of tabs collapses into one separator under `read`, so no field is ever
3392
3298
  # empty. Nothing here is optional, so nothing can shift.
3393
3299
  #
3394
- # EIGHT FIELDS, NOT TEN. The two the plan states — the prerequisite's name and
3395
- # what the host said about it — are appended by the caller, because reading the
3396
- # second costs a host round trip and the scan spends it only where it could
3300
+ # EIGHT FIELDS, NOT ELEVEN. The two the plan states — the prerequisite's name
3301
+ # and what the host said about it — are appended by the caller, because reading
3302
+ # the second costs a host round trip and the scan spends it only where it could
3397
3303
  # change the answer. The rule reports which states those are; see the caller.
3304
+ # The caller appends the PR list's completeness last, once per run.
3398
3305
  #
3399
3306
  # THE DEFAULT BRANCH'S TIP IS READ ONCE PER RUN, not once per branch. It does
3400
3307
  # not move while the scan runs — every fact below is derived from the ref batch
@@ -3743,26 +3650,14 @@ for plan in "${plans[@]}"; do
3743
3650
  # is a judgment that belongs one layer up (Manifesto Principle 3).
3744
3651
  plan_phase="${plan_meta_phases[$meta_i]}"
3745
3652
 
3746
- # The delivered window, applied to the plans the PHASE put in the terminal
3747
- # group. Enumeration grouped them; the `Delivered:` RECORD decides which of
3748
- # them still appears.
3749
- #
3750
- # THE TEST IS THE PHASE, not the path. It read `case "$plan" in "$DELIVERED_DIR"*)`
3751
- # — the directory the link sat in — and that made "which group is this plan
3752
- # in" a fact about a symlink while "what phase is it" was a fact about the
3753
- # file. The old comment here noted that an active plan carrying
3754
- # `Phase: Delivered` was drift the window must not hide; under the phase rule
3755
- # that drift cannot be constructed, because there is no second place for the
3756
- # answer to live. One source, so nothing to disagree.
3757
- #
3758
- # Two exits, and both matter:
3759
- # * the record's date has aged out of the window — ordinary expiry;
3760
- # * there is NO record — "no date, no row". `reconcile-scan-accuracy.md` is
3761
- # the live example; showing it would create the one row that can never
3762
- # age out of DONE.
3763
- # Both leave before a single git call is spent on the plan's branches.
3764
- if is_terminal_phase "$plan_phase"; then
3765
- [ "${plan_meta_inwindow[$meta_i]}" = "1" ] || continue
3653
+ # THE RELEASE SCOPE, decided by the PHASE alone. A terminal plan stays in the
3654
+ # pulse only while it is `delivered`: its work has landed and its version has
3655
+ # not shipped, which is what DONE holds. `released`, `rejected` and
3656
+ # `superseded` leave, whatever their age, and leave before a single git call
3657
+ # is spent on their branches. No date bounds it, so a plan delivered weeks
3658
+ # before a release still names the release's contents.
3659
+ if is_terminal_phase "$plan_phase" && [ "$plan_phase" != "delivered" ]; then
3660
+ continue
3766
3661
  fi
3767
3662
 
3768
3663
  n_plans=$((n_plans + 1))
@@ -3860,12 +3755,17 @@ for plan in "${plans[@]}"; do
3860
3755
  # deciding it — see pass 1c.
3861
3756
  readings=""
3862
3757
  order=""
3758
+ # THE ELEVENTH FIELD: whether the PR list held every PR (`.list-complete`,
3759
+ # written by `prefill_pr_states`). The rule reads a `NONE` for a ref behind
3760
+ # main as `open` only when it is true; a capped list may omit a merged PR.
3761
+ list_complete=false
3762
+ [ -n "$HOST_STATE_CACHE" ] && [ -f "$HOST_STATE_CACHE/.list-complete" ] && list_complete=true
3863
3763
  while IFS=$'\t' read -r idx br deferred why waits wname claim; do
3864
3764
  [ -n "$br" ] || continue
3865
3765
  # "-" is the absent marker the shim writes, for the tab-collapse reason
3866
3766
  # above. Normalized here so everything downstream tests emptiness.
3867
3767
  [ "$waits" = "-" ] && waits=""
3868
- readings+="$(branch_readings "$br" "$deferred") ${waits:--} ?"$'\n'
3768
+ readings+="$(branch_readings "$br" "$deferred") ${waits:--} ? $list_complete"$'\n'
3869
3769
  order+="$idx $br $deferred $why ${waits:--} $wname $claim"$'\n'
3870
3770
  done <<< "$wave_lines"
3871
3771
 
@@ -3920,7 +3820,8 @@ for plan in "${plans[@]}"; do
3920
3820
  # list may legitimately omit: its plan may be delivered and its ref gone.
3921
3821
  # `host_pr_state`'s run cache keeps this at one call per prerequisite per
3922
3822
  # run, never one per pass.
3923
- refill+="$(printf '%s' "$rd_line" | cut -f1-9) $(waits_pr_state "$waits_br")"$'\n'
3823
+ # Field 10 is replaced; field 11 is carried.
3824
+ refill+="$(printf '%s' "$rd_line" | cut -f1-9) $(waits_pr_state "$waits_br") $(printf '%s' "$rd_line" | cut -f11)"$'\n'
3924
3825
  else
3925
3826
  refill+="$rd_line"$'\n'
3926
3827
  fi