@plot-pm/board 0.10.0 → 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.
- package/dist/board-server.mjs +156 -150
- package/package.json +5 -1
- package/plot-agent-monitor.sh +508 -0
- package/plot-approve.sh +145 -29
- package/plot-budget.sh +439 -0
- package/plot-config.sh +9 -0
- package/plot-deliver.sh +210 -117
- package/plot-dispatch.sh +829 -185
- package/plot-fleet-scan.sh +577 -65
- package/plot-host.sh +1329 -55
- package/plot-plan-meta.sh +234 -40
- package/plot-reap.sh +675 -57
- package/plot-transcript-quiet.sh +142 -0
- package/plot-worker-monitor.sh +644 -0
- package/plot-worker-state.sh +81 -50
package/plot-plan-meta.sh
CHANGED
|
@@ -120,6 +120,14 @@
|
|
|
120
120
|
# `<!-- deferred -->` (bare, no colon) sets the flag with no
|
|
121
121
|
# reason; `waves[].branches[].deferred_reason` carries the
|
|
122
122
|
# sentence after the colon, "" where none was written.
|
|
123
|
+
# `<!-- waits: bug/other -->` names ONE branch this branch
|
|
124
|
+
# waits on, reported as `waves[].branches[].waits_on`. The key
|
|
125
|
+
# is ABSENT where no annotation was written — never "" — and
|
|
126
|
+
# the value is a branch name in this repo, not a plan slug and
|
|
127
|
+
# not a cross-repo reference. The parser reports what the file
|
|
128
|
+
# says: a prerequisite no plan declares still parses, and the
|
|
129
|
+
# scan is what turns that into a verdict. `waits:` and
|
|
130
|
+
# `deferred:` are independent — a branch may carry both.
|
|
123
131
|
# prs PR numbers, sorted and unique, read from EITHER spelling:
|
|
124
132
|
# `→ #NNN` / `→ owner/repo#NNN` links in the `## Branches`
|
|
125
133
|
# section, OR `PR: #NNN` in a `## Waves` `### ` heading. The
|
|
@@ -291,6 +299,34 @@ function val_after_colon(s) {
|
|
|
291
299
|
}
|
|
292
300
|
# Template placeholders like "<!-- optional -->" mean "field absent".
|
|
293
301
|
function strip_placeholder(s) { return (s ~ /^<!--/) ? "" : s }
|
|
302
|
+
# Blank out backtick-delimited inline code, so a marker PRINTED as a literal is
|
|
303
|
+
# not read as syntax. Markdown renders `<!--` between backticks as the four
|
|
304
|
+
# characters; this parser used to read it as a comment-open, and because such a
|
|
305
|
+
# line carries no closing marker it swallowed the rest of the file — one
|
|
306
|
+
# backticked marker in a summary line cost a plan its phase, type and branches.
|
|
307
|
+
#
|
|
308
|
+
# Only the comment rules consult this. The line itself is untouched everywhere
|
|
309
|
+
# else, because branch names live in backticks too and stripping them for real
|
|
310
|
+
# would empty every `## Branches` entry.
|
|
311
|
+
#
|
|
312
|
+
# Runs of backticks are matched longest-first so a ``code`` span closes against
|
|
313
|
+
# its own delimiter. An UNPAIRED backtick leaves its tail as-is: that is prose
|
|
314
|
+
# with a stray tick, and prose is exactly where a real comment may open.
|
|
315
|
+
# NOTE: the closing-run index is named `shut` because `close` is an awk builtin
|
|
316
|
+
# and cannot be a parameter name — it fails as a syntax error on the function
|
|
317
|
+
# signature, pointing nowhere near the cause.
|
|
318
|
+
function mask_code(s, out, n, tick, shut) {
|
|
319
|
+
out = ""
|
|
320
|
+
while ((n = index(s, "`")) > 0) {
|
|
321
|
+
out = out substr(s, 1, n - 1)
|
|
322
|
+
s = substr(s, n)
|
|
323
|
+
tick = ""
|
|
324
|
+
while (substr(s, 1, 1) == "`") { tick = tick "`"; s = substr(s, 2) }
|
|
325
|
+
if ((shut = index(s, tick)) == 0) return out tick s
|
|
326
|
+
s = substr(s, shut + length(tick))
|
|
327
|
+
}
|
|
328
|
+
return out s
|
|
329
|
+
}
|
|
294
330
|
# First known phase token wins; NONE if empty; UNKNOWN otherwise.
|
|
295
331
|
function norm_phase(raw, lower, toks, n, i, t) {
|
|
296
332
|
if (raw == "") return "NONE"
|
|
@@ -356,7 +392,7 @@ function reset_state() {
|
|
|
356
392
|
# the field, so a consumer cannot mistake "never interrogated" for "asked
|
|
357
393
|
# nothing".
|
|
358
394
|
block_rounds = ""
|
|
359
|
-
in_fm = 0; section = ""; in_comment = 0; in_challenge = 0; in_fence = 0;
|
|
395
|
+
in_fm = 0; section = ""; in_comment = 0; in_challenge = 0; in_fence = 0; slices_seen = 0; slice_shape = ""
|
|
360
396
|
delete branches; n_branches = 0
|
|
361
397
|
delete prs; n_prs = 0
|
|
362
398
|
delete malformed_prs; n_malformed_prs = 0
|
|
@@ -364,6 +400,7 @@ function reset_state() {
|
|
|
364
400
|
delete issues; n_issues = 0
|
|
365
401
|
delete wave_names; delete wave_of; delete wave_seq; delete wave_count
|
|
366
402
|
delete deferred_of; delete deferred_why; delete claimed_of; delete ordered_b; n_waves = 0
|
|
403
|
+
delete waits_of; delete waits_set
|
|
367
404
|
delete started; n_started = 0
|
|
368
405
|
fm_changelog = ""
|
|
369
406
|
delete changelog; n_changelog = 0; changelog_seen = 0; cl_open = 0
|
|
@@ -508,7 +545,13 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
508
545
|
if (wave_of[i] != w) continue
|
|
509
546
|
out = out (first ? "" : ",") "{\"branch\":\"" jesc(ordered_b[i]) "\",\"deferred\":" deferred_of[i] \
|
|
510
547
|
",\"deferred_reason\":\"" jesc(deferred_why[i]) "\"" \
|
|
511
|
-
",\"claimed\":\"" jesc(claimed_of[i]) "\"
|
|
548
|
+
",\"claimed\":\"" jesc(claimed_of[i]) "\""
|
|
549
|
+
# ABSENT, NOT EMPTY, where no prerequisite was declared. The key appears
|
|
550
|
+
# only on a branch whose line carries a `waits:` annotation, so a consumer
|
|
551
|
+
# reading `waits_on` gets a branch name or nothing — never a blank string
|
|
552
|
+
# that reads as a prerequisite with no name.
|
|
553
|
+
if (waits_set[i] == 1) out = out ",\"waits_on\":\"" jesc(waits_of[i]) "\""
|
|
554
|
+
out = out "}"
|
|
512
555
|
first = 0
|
|
513
556
|
}
|
|
514
557
|
out = out "]}"
|
|
@@ -612,6 +655,10 @@ in_fm {
|
|
|
612
655
|
else if (lower ~ /^rounds:/ && fm_rounds == "") fm_rounds = val_after_colon($0)
|
|
613
656
|
next
|
|
614
657
|
}
|
|
658
|
+
# A fence marker is a line whose first non-space run is ``` or ~~~ (an info
|
|
659
|
+
# string like ```markdown may follow). The marker line itself is never content.
|
|
660
|
+
/^[ \t]*(```|~~~)/ { in_fence = !in_fence; next }
|
|
661
|
+
in_fence { next }
|
|
615
662
|
# Interior of multi-line HTML comments is non-content (template guidance
|
|
616
663
|
# blocks); single-line "<!-- ... -->" placeholders are unaffected.
|
|
617
664
|
#
|
|
@@ -631,13 +678,25 @@ in_comment {
|
|
|
631
678
|
if ($0 ~ /-->/) { in_comment = 0; in_challenge = 0 }
|
|
632
679
|
next
|
|
633
680
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
681
|
+
# A comment opens only where the marker is SYNTAX. Inside inline code it is a
|
|
682
|
+
# literal being quoted, and the fence rules above have already consumed code
|
|
683
|
+
# blocks — so what reaches here is prose, minus its backticked spans.
|
|
684
|
+
#
|
|
685
|
+
# The cheap test guards the expensive one: /<!--/ on the raw line rejects the
|
|
686
|
+
# overwhelming majority, so mask_code runs once per line that actually carries a
|
|
687
|
+
# marker rather than twice per line of every plan. The masked verdict is decided
|
|
688
|
+
# INSIDE the rule so a line that turns out to be inline code simply falls
|
|
689
|
+
# through to the ordinary content rules below, carrying no state with it.
|
|
690
|
+
/<!--/ {
|
|
691
|
+
_masked = mask_code($0)
|
|
692
|
+
if (_masked ~ /<!--/ && _masked !~ /-->/) {
|
|
693
|
+
in_comment = 1
|
|
694
|
+
# A truncated block never closes; it simply runs to EOF as a comment, and
|
|
695
|
+
# the round stays whatever was read before the truncation — absent if the
|
|
696
|
+
# "round" line was itself lost. Nothing else in the record is affected.
|
|
697
|
+
in_challenge = ($0 ~ /CHALLENGE-THE-PLAN-METADATA/) ? 1 : 0
|
|
698
|
+
next
|
|
699
|
+
}
|
|
641
700
|
}
|
|
642
701
|
# A fenced code block is illustration, never contract — the same standing rule
|
|
643
702
|
# comment interiors and repeated headings already follow. A plan that documents
|
|
@@ -651,30 +710,34 @@ in_comment {
|
|
|
651
710
|
# example won its first-heading-wins guard and hid the real section). Toggling on
|
|
652
711
|
# a fence fence-marker line closes both.
|
|
653
712
|
#
|
|
654
|
-
# A fence marker is a line whose first non-space run is ``` or ~~~ (an info
|
|
655
|
-
# string like ```markdown may follow). The marker line itself is never content.
|
|
656
|
-
/^[ \t]*(```|~~~)/ { in_fence = !in_fence; next }
|
|
657
|
-
in_fence { next }
|
|
658
713
|
# First H1 is the title fallback (front matter title: still wins in emit).
|
|
659
714
|
/^#[ \t]/ && h1_title == "" { h1_title = trim(substr($0, 2)) }
|
|
660
715
|
/^## / {
|
|
661
716
|
if ($0 ~ /^## Status/) section = "status"
|
|
662
|
-
#
|
|
717
|
+
# THE HEADING WORD NO LONGER PICKS THE LAYOUT. `## Branches`, `## Waves` and
|
|
718
|
+
# `## Slices` open ONE section, and which shape it holds is decided by the
|
|
719
|
+
# first `### ` heading in it — see `slice_shape` below.
|
|
720
|
+
#
|
|
721
|
+
# THEY USED TO SELECT IT, AND THAT MADE THE RENAME UNSAFE. The two layouts are
|
|
722
|
+
# genuinely different: the old one carries the branch on a LIST ITEM
|
|
723
|
+
# (`- ` + backtick + `feature/x` + backtick + ` -> #72`), the new one carries it in the
|
|
724
|
+
# heading (`### Name (Branch: feature/x, PR: #577)`). With the word choosing
|
|
725
|
+
# the consumer, renaming a heading silently changed which grammar was applied.
|
|
726
|
+
# Measured 2026-09-04 on `2026-08-14-parallel-agent-fleet.md`: renaming its
|
|
727
|
+
# `## Branches` to `## Slices` took it from **6 branches to 0**, with no error
|
|
728
|
+
# anywhere — the file still parsed, and simply held nothing.
|
|
729
|
+
#
|
|
730
|
+
# Deciding by shape makes the heading a NAME again. All three spellings read
|
|
731
|
+
# either layout, so a plan can be renamed without being rewritten, and the 48
|
|
732
|
+
# files still saying `## Branches` are safe to migrate a word at a time.
|
|
733
|
+
#
|
|
734
|
+
# First one wins, as before: a plan documenting the plan format quotes the
|
|
663
735
|
# section in prose, and those later headings are illustration, not contract.
|
|
664
|
-
else if ($0 ~ /^## Branches/
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
# as silently empty.
|
|
670
|
-
# `## Slices` is the spelling the design spec uses, and `## Waves` is what 132
|
|
671
|
-
# plans already say. They are ONE section here, sharing waves_seen, because the
|
|
672
|
-
# shape is identical: the branch and PR ride the `### ` heading either way. A
|
|
673
|
-
# third arm would be a second implementation of a re-spelling, and the two
|
|
674
|
-
# would drift. No existing plan is rewritten to say Slices — a delivered plan
|
|
675
|
-
# describes what was built in the vocabulary of its day, and churning 132
|
|
676
|
-
# files git blame for a word buys nothing. New plans may use either.
|
|
677
|
-
else if ($0 ~ /^## Waves/ || $0 ~ /^## Slices/) { section = waves_seen ? "" : "waves"; waves_seen = 1 }
|
|
736
|
+
else if ($0 ~ /^## Branches/ || $0 ~ /^## Waves/ || $0 ~ /^## Slices/) {
|
|
737
|
+
section = slices_seen ? "" : "slices"
|
|
738
|
+
slices_seen = 1
|
|
739
|
+
slice_shape = ""
|
|
740
|
+
}
|
|
678
741
|
else if ($0 ~ /^## Approval/) section = "approval"
|
|
679
742
|
# First `## Changelog` wins, for the same reason `## Branches` does: a plan
|
|
680
743
|
# about the plan format quotes the section in prose, and the later heading is
|
|
@@ -692,11 +755,35 @@ section == "status" {
|
|
|
692
755
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**issue[:*]/ && canon_issue == "") canon_issue = val_after_colon($0)
|
|
693
756
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**review[:*]/ && canon_review == "") canon_review = val_after_colon($0)
|
|
694
757
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**impl[:*]/ && canon_impl == "") canon_impl = val_after_colon($0)
|
|
695
|
-
|
|
758
|
+
# EACH TAKES THE FIRST LINE THAT CARRIES A VALUE, NOT THE FIRST LINE.
|
|
759
|
+
#
|
|
760
|
+
# A plan may hold both a record and an unfilled placeholder for the same
|
|
761
|
+
# field, on either side of each other. Seven plans here write the placeholder
|
|
762
|
+
# as a per-line comment —
|
|
763
|
+
#
|
|
764
|
+
# - **Delivered:** <!-- YYYY-MM-DD -->
|
|
765
|
+
#
|
|
766
|
+
# — beside a real `- **Delivered:** 2026-09-01`. `strip_placeholder` used to
|
|
767
|
+
# run in `emit_record`, AFTER first-wins had already chosen: the placeholder
|
|
768
|
+
# claimed the slot, was emptied a moment later, and the record it beat was
|
|
769
|
+
# gone. Whether a plan parsed correctly then depended on which line came
|
|
770
|
+
# first, and `append_delivered_line` decides that by where a `<!--` happens
|
|
771
|
+
# to stop its scan.
|
|
772
|
+
#
|
|
773
|
+
# A DELIVERED PLAN THAT READS `delivered_raw: ""` IS INVISIBLE TO THE SCAN,
|
|
774
|
+
# which reads its rolling window from that field — the same failure the
|
|
775
|
+
# comment-block fix addressed, reached by a different road. Measured
|
|
776
|
+
# 2026-09-01 on `a-machine-is-an-instance`: parsed correctly, but only
|
|
777
|
+
# because its record sat two lines above its placeholder.
|
|
778
|
+
#
|
|
779
|
+
# `started` never had this bug because it is a list and had to filter at
|
|
780
|
+
# capture to avoid storing empties. These four are scalars, and now do the
|
|
781
|
+
# same: a placeholder no longer counts as a value, so ORDER STOPS MATTERING.
|
|
782
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**design[:*]/ && canon_design == "") canon_design = strip_placeholder(val_after_colon($0))
|
|
696
783
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**rounds[:*]/ && canon_rounds == "") canon_rounds = val_after_colon($0)
|
|
697
|
-
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**approved[:*]/ && canon_approved == "") canon_approved = val_after_colon($0)
|
|
698
|
-
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**released[:*]/ && canon_released == "") canon_released = val_after_colon($0)
|
|
699
|
-
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**delivered[:*]/ && canon_delivered == "") canon_delivered = val_after_colon($0)
|
|
784
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**approved[:*]/ && canon_approved == "") canon_approved = strip_placeholder(val_after_colon($0))
|
|
785
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**released[:*]/ && canon_released == "") canon_released = strip_placeholder(val_after_colon($0))
|
|
786
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**delivered[:*]/ && canon_delivered == "") canon_delivered = strip_placeholder(val_after_colon($0))
|
|
700
787
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**started[:*]/) {
|
|
701
788
|
_s = strip_placeholder(val_after_colon($0))
|
|
702
789
|
if (_s != "") started[++n_started] = _s
|
|
@@ -749,7 +836,23 @@ section == "changelog" {
|
|
|
749
836
|
cl_open = 0
|
|
750
837
|
next
|
|
751
838
|
}
|
|
752
|
-
|
|
839
|
+
# WHICH SHAPE THIS SECTION HOLDS, decided once from its first `### ` heading and
|
|
840
|
+
# then fixed for the rest of the section.
|
|
841
|
+
#
|
|
842
|
+
# `(Branch:` IS THE MARKER, and it is the only reliable one. A heading carrying
|
|
843
|
+
# it is the new shape by construction — that parenthetical is where the new
|
|
844
|
+
# layout puts the branch. A heading without it is the old shape, whose headings
|
|
845
|
+
# are bare names (`### Tracer`) and whose branches ride list items below.
|
|
846
|
+
#
|
|
847
|
+
# A SECTION WITH NO `### ` AT ALL is the old shape, and must be: a plan written
|
|
848
|
+
# before subheadings existed is one unnamed wave of list items, which is exactly
|
|
849
|
+
# what the old consumer produces. `slice_shape` therefore stays `""` until a
|
|
850
|
+
# heading is seen, and `""` routes to the old consumer.
|
|
851
|
+
section == "slices" && $0 ~ /^###[ \t]/ && slice_shape == "" {
|
|
852
|
+
slice_shape = (index($0, "(Branch:") > 0) ? "heading" : "list"
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
section == "slices" && slice_shape != "heading" {
|
|
753
856
|
# `### <name>` opens a wave. Branches before any subheading belong to an
|
|
754
857
|
# unnamed wave, so a pre-wave plan parses as exactly one wave.
|
|
755
858
|
if ($0 ~ /^###[ \t]/) {
|
|
@@ -784,13 +887,64 @@ section == "branches" {
|
|
|
784
887
|
# documented contract is that an annotation binds to the line carrying the
|
|
785
888
|
# branch name. A deferral whose text is wrapped onto a continuation line is
|
|
786
889
|
# not seen, exactly as `deferred` itself was not.
|
|
890
|
+
# `moved:` IS THE SAME ANSWER AS `deferred:`, and CLAUDE.md has said so since
|
|
891
|
+
# the reconcile scan was written: *"`deferred:`/`moved:` in the plan means
|
|
892
|
+
# reapable"*. `plot-reconcile-scan.sh:504` matches both in one arm; this
|
|
893
|
+
# parser matched only the first, so a slice given up with the other word read
|
|
894
|
+
# as still outstanding — to the board, the delivery gate and the fleet alike.
|
|
895
|
+
# Measured 2026-09-04 on `feature/the-reaper-sweeps-every-kind`: annotated
|
|
896
|
+
# `moved:` into the slice that superseded it, still `deferred:false` here.
|
|
897
|
+
#
|
|
898
|
+
# The two words differ in what they tell a READER — given up, versus taken
|
|
899
|
+
# somewhere else — and the reason is kept verbatim, so the distinction is not
|
|
900
|
+
# lost by being read alike.
|
|
787
901
|
defer_note = ""
|
|
788
|
-
if (index($0, "deferred:") > 0) {
|
|
902
|
+
if (index($0, "deferred:") > 0 || index($0, "moved:") > 0) {
|
|
789
903
|
_d = $0
|
|
790
|
-
sub(/^.*<!--[ \t]*deferred:[ \t]*/, "", _d)
|
|
904
|
+
sub(/^.*<!--[ \t]*(deferred|moved):[ \t]*/, "", _d)
|
|
791
905
|
sub(/[ \t]*-->.*$/, "", _d)
|
|
792
906
|
defer_note = trim(_d)
|
|
793
907
|
}
|
|
908
|
+
# THE PREREQUISITE THIS BRANCH NAMES: `<!-- waits: bug/other-branch -->`.
|
|
909
|
+
#
|
|
910
|
+
# ONE branch, never a list. A slice needing two prerequisites has not been cut
|
|
911
|
+
# finely enough, and a list invites a dependency graph nobody wants to debug.
|
|
912
|
+
# The greedy `.*` takes the LAST annotation when a line carries two, which is
|
|
913
|
+
# the same rule `deferred:` and `claimed:` already follow.
|
|
914
|
+
#
|
|
915
|
+
# The value is a BRANCH NAME, so it stops at the first whitespace rather than
|
|
916
|
+
# running to the closing marker the way a deferral reason does: a reason is a
|
|
917
|
+
# sentence, a branch name is a token, and trailing prose inside the comment
|
|
918
|
+
# would silently become part of a name that then matches nothing.
|
|
919
|
+
#
|
|
920
|
+
# `deferred:` is a judgement and `waits:` is a checkable fact, so the two are
|
|
921
|
+
# separate annotations and both may sit on one line. Read here, beside the
|
|
922
|
+
# other two, for the same reason: `match()` in the branch loop below clobbers
|
|
923
|
+
# RSTART/RLENGTH, so anything read from the whole line must be read first.
|
|
924
|
+
#
|
|
925
|
+
# `has_waits` carries presence separately from the value, because ABSENT and
|
|
926
|
+
# EMPTY are different answers — a branch declaring no prerequisite emits no
|
|
927
|
+
# `waits_on` key at all.
|
|
928
|
+
waits_note = ""
|
|
929
|
+
has_waits = 0
|
|
930
|
+
if ($0 ~ /<!--[ \t]*waits:[ \t]*/) {
|
|
931
|
+
_w = $0
|
|
932
|
+
sub(/^.*<!--[ \t]*waits:[ \t]*/, "", _w)
|
|
933
|
+
sub(/[ \t].*$/, "", _w)
|
|
934
|
+
sub(/-->.*$/, "", _w)
|
|
935
|
+
waits_note = trim(_w)
|
|
936
|
+
# THE VALUE MUST LOOK LIKE A BRANCH, and that check is what keeps a
|
|
937
|
+
# SYNTAX EXAMPLE from becoming a declaration. A plan that documents the
|
|
938
|
+
# annotation writes the literal marker in prose, and no comment-aware
|
|
939
|
+
# reading can tell that apart from the real thing on the same line — the
|
|
940
|
+
# branch prefixes can. `<branch>` is not a branch name; `bug/x` is.
|
|
941
|
+
#
|
|
942
|
+
# Reusing the branch prefixes rather than a new pattern: the prerequisite
|
|
943
|
+
# IS a branch in this repo, so the two must never disagree about what a
|
|
944
|
+
# branch name looks like.
|
|
945
|
+
if (waits_note ~ "^(" PREFIXES ")/[^ \t]+$") has_waits = 1
|
|
946
|
+
else waits_note = ""
|
|
947
|
+
}
|
|
794
948
|
# ONE LIST ITEM, AT MOST ONE CLAIM — an `if`, not the `while` this was.
|
|
795
949
|
#
|
|
796
950
|
# The old loop walked the line taking every backticked name on it, which is
|
|
@@ -815,7 +969,7 @@ section == "branches" {
|
|
|
815
969
|
# plan can make about a branch, dropped for want of a colon. A reader
|
|
816
970
|
# writing the shorter form has said the branch will not be built; the
|
|
817
971
|
# parser now hears it.
|
|
818
|
-
deferred_of[n_branches] = ($0 ~ /<!--[ \t]*deferred[ \t]*(:|-->)/) ? "true" : "false"
|
|
972
|
+
deferred_of[n_branches] = ($0 ~ /<!--[ \t]*(deferred|moved)[ \t]*(:|-->)/) ? "true" : "false"
|
|
819
973
|
# The reason travels with the flag. Empty on every non-deferred branch, and
|
|
820
974
|
# empty is also the honest answer for the bare form: the branch IS deferred
|
|
821
975
|
# and no reason was recorded, which is a different statement from a reason
|
|
@@ -824,6 +978,10 @@ section == "branches" {
|
|
|
824
978
|
# Claim reflection, written by the worker after its ref push succeeds. This
|
|
825
979
|
# is a reflection, not the claim: git refs remain authoritative.
|
|
826
980
|
claimed_of[n_branches] = claim_note
|
|
981
|
+
# The prerequisite travels with the branch. Presence is tracked separately
|
|
982
|
+
# so a branch that declares none emits no key.
|
|
983
|
+
waits_of[n_branches] = waits_note
|
|
984
|
+
waits_set[n_branches] = has_waits
|
|
827
985
|
ordered_b[n_branches] = b
|
|
828
986
|
}
|
|
829
987
|
line = $0
|
|
@@ -858,7 +1016,7 @@ section == "branches" {
|
|
|
858
1016
|
# The emitted arrays (branches, prs, waves) must be byte-identical to what the
|
|
859
1017
|
# old shape produces for the same plan. So this shares every accumulation
|
|
860
1018
|
# variable with the branches handler above; only the EXTRACTION differs.
|
|
861
|
-
section == "
|
|
1019
|
+
section == "slices" && slice_shape == "heading" {
|
|
862
1020
|
# Only the `### ` heading carries meta. Body lines are prose — never scanned
|
|
863
1021
|
# for a branch or a PR, which is the whole point of the new shape.
|
|
864
1022
|
if ($0 !~ /^###[ \t]/) next
|
|
@@ -887,13 +1045,47 @@ section == "waves" {
|
|
|
887
1045
|
sub(/[ \t]*-->.*$/, "", _c)
|
|
888
1046
|
claim_note = trim(_c)
|
|
889
1047
|
}
|
|
1048
|
+
# `moved:` IS THE SAME ANSWER AS `deferred:`, and CLAUDE.md has said so since
|
|
1049
|
+
# the reconcile scan was written: *"`deferred:`/`moved:` in the plan means
|
|
1050
|
+
# reapable"*. `plot-reconcile-scan.sh:504` matches both in one arm; this
|
|
1051
|
+
# parser matched only the first, so a slice given up with the other word read
|
|
1052
|
+
# as still outstanding — to the board, the delivery gate and the fleet alike.
|
|
1053
|
+
# Measured 2026-09-04 on `feature/the-reaper-sweeps-every-kind`: annotated
|
|
1054
|
+
# `moved:` into the slice that superseded it, still `deferred:false` here.
|
|
1055
|
+
#
|
|
1056
|
+
# The two words differ in what they tell a READER — given up, versus taken
|
|
1057
|
+
# somewhere else — and the reason is kept verbatim, so the distinction is not
|
|
1058
|
+
# lost by being read alike.
|
|
890
1059
|
defer_note = ""
|
|
891
|
-
if (index($0, "deferred:") > 0) {
|
|
1060
|
+
if (index($0, "deferred:") > 0 || index($0, "moved:") > 0) {
|
|
892
1061
|
_d = $0
|
|
893
|
-
sub(/^.*<!--[ \t]*deferred:[ \t]*/, "", _d)
|
|
1062
|
+
sub(/^.*<!--[ \t]*(deferred|moved):[ \t]*/, "", _d)
|
|
894
1063
|
sub(/[ \t]*-->.*$/, "", _d)
|
|
895
1064
|
defer_note = trim(_d)
|
|
896
1065
|
}
|
|
1066
|
+
# The prerequisite, read exactly as the list-item spelling reads it. Both
|
|
1067
|
+
# dialects emit the same waves[], so a field added to one only would break
|
|
1068
|
+
# that contract the first time a plan migrated.
|
|
1069
|
+
waits_note = ""
|
|
1070
|
+
has_waits = 0
|
|
1071
|
+
if ($0 ~ /<!--[ \t]*waits:[ \t]*/) {
|
|
1072
|
+
_w = $0
|
|
1073
|
+
sub(/^.*<!--[ \t]*waits:[ \t]*/, "", _w)
|
|
1074
|
+
sub(/[ \t].*$/, "", _w)
|
|
1075
|
+
sub(/-->.*$/, "", _w)
|
|
1076
|
+
waits_note = trim(_w)
|
|
1077
|
+
# THE VALUE MUST LOOK LIKE A BRANCH, and that check is what keeps a
|
|
1078
|
+
# SYNTAX EXAMPLE from becoming a declaration. A plan that documents the
|
|
1079
|
+
# annotation writes the literal marker in prose, and no comment-aware
|
|
1080
|
+
# reading can tell that apart from the real thing on the same line — the
|
|
1081
|
+
# branch prefixes can. `<branch>` is not a branch name; `bug/x` is.
|
|
1082
|
+
#
|
|
1083
|
+
# Reusing the branch prefixes rather than a new pattern: the prerequisite
|
|
1084
|
+
# IS a branch in this repo, so the two must never disagree about what a
|
|
1085
|
+
# branch name looks like.
|
|
1086
|
+
if (waits_note ~ "^(" PREFIXES ")/[^ \t]+$") has_waits = 1
|
|
1087
|
+
else waits_note = ""
|
|
1088
|
+
}
|
|
897
1089
|
|
|
898
1090
|
# The branch is the `Branch:` value, matched against the known prefixes exactly
|
|
899
1091
|
# as the old shape matched the backticked name. Written unquoted in the heading
|
|
@@ -910,9 +1102,11 @@ section == "waves" {
|
|
|
910
1102
|
wave_seq[n_branches] = ++wave_count[n_waves]
|
|
911
1103
|
# Bare `<!-- deferred -->` sets the flag with no reason, same as the old
|
|
912
1104
|
# shape; `deferred:` carries the reason.
|
|
913
|
-
deferred_of[n_branches] = ($0 ~ /<!--[ \t]*deferred[ \t]*(:|-->)/) ? "true" : "false"
|
|
1105
|
+
deferred_of[n_branches] = ($0 ~ /<!--[ \t]*(deferred|moved)[ \t]*(:|-->)/) ? "true" : "false"
|
|
914
1106
|
deferred_why[n_branches] = defer_note
|
|
915
1107
|
claimed_of[n_branches] = claim_note
|
|
1108
|
+
waits_of[n_branches] = waits_note
|
|
1109
|
+
waits_set[n_branches] = has_waits
|
|
916
1110
|
ordered_b[n_branches] = b
|
|
917
1111
|
}
|
|
918
1112
|
|