@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.
- package/dist/board-server.mjs +170 -164
- package/package.json +6 -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 +246 -36
- 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
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
# A file with neither is reported as format "none" (pre-plot / legacy plan).
|
|
38
38
|
#
|
|
39
39
|
# The IMPLEMENTATION section (which branches, in which waves, with which PRs)
|
|
40
|
-
# has
|
|
40
|
+
# has THREE spellings, and this parser reads all of them:
|
|
41
41
|
#
|
|
42
42
|
# ## Branches (old) the branch rides the list line, meta mixed with prose:
|
|
43
43
|
# ### Removed
|
|
@@ -47,6 +47,14 @@
|
|
|
47
47
|
# ### Removed (Branch: bug/foo, PR: #300)
|
|
48
48
|
# - loses its half
|
|
49
49
|
#
|
|
50
|
+
# ## Slices the design-spec word for the same shape as `## Waves`,
|
|
51
|
+
# read by the same handler. A Slice holds one branch and
|
|
52
|
+
# belongs to one plan; a Wave is the fleet cohort that
|
|
53
|
+
# spans plans. The section here was always the former, so
|
|
54
|
+
# `## Slices` is the accurate name and `## Waves` is the
|
|
55
|
+
# one 132 delivered plans already carry. No plan is
|
|
56
|
+
# rewritten: both are read, forever if need be.
|
|
57
|
+
#
|
|
50
58
|
# Both emit the SAME branches/prs/waves arrays. The new shape is the format Plot
|
|
51
59
|
# writes and documents; the old one is kept readable because a format change owes
|
|
52
60
|
# its estate a migration that moves files one at a time, and a plan moved one
|
|
@@ -95,8 +103,9 @@
|
|
|
95
103
|
# branches branch names, sorted and unique, read from EITHER spelling:
|
|
96
104
|
# the old `## Branches` section (a LIST ITEM whose first token
|
|
97
105
|
# is the backtick-quoted name, matching the known prefixes) OR
|
|
98
|
-
# the new `## Waves` section (`Branch:` in a
|
|
99
|
-
# see below). A backticked branch name
|
|
106
|
+
# the new `## Waves` / `## Slices` section (`Branch:` in a
|
|
107
|
+
# `### ` heading — see below). A backticked branch name
|
|
108
|
+
# anywhere else under
|
|
100
109
|
# `## Branches` — mid-sentence, in a blockquote, in a comment,
|
|
101
110
|
# on a wrapped continuation line — is a CITATION and claims
|
|
102
111
|
# nothing: plans name each other branches to declare
|
|
@@ -111,6 +120,14 @@
|
|
|
111
120
|
# `<!-- deferred -->` (bare, no colon) sets the flag with no
|
|
112
121
|
# reason; `waves[].branches[].deferred_reason` carries the
|
|
113
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.
|
|
114
131
|
# prs PR numbers, sorted and unique, read from EITHER spelling:
|
|
115
132
|
# `→ #NNN` / `→ owner/repo#NNN` links in the `## Branches`
|
|
116
133
|
# section, OR `PR: #NNN` in a `## Waves` `### ` heading. The
|
|
@@ -282,6 +299,34 @@ function val_after_colon(s) {
|
|
|
282
299
|
}
|
|
283
300
|
# Template placeholders like "<!-- optional -->" mean "field absent".
|
|
284
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
|
+
}
|
|
285
330
|
# First known phase token wins; NONE if empty; UNKNOWN otherwise.
|
|
286
331
|
function norm_phase(raw, lower, toks, n, i, t) {
|
|
287
332
|
if (raw == "") return "NONE"
|
|
@@ -347,7 +392,7 @@ function reset_state() {
|
|
|
347
392
|
# the field, so a consumer cannot mistake "never interrogated" for "asked
|
|
348
393
|
# nothing".
|
|
349
394
|
block_rounds = ""
|
|
350
|
-
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 = ""
|
|
351
396
|
delete branches; n_branches = 0
|
|
352
397
|
delete prs; n_prs = 0
|
|
353
398
|
delete malformed_prs; n_malformed_prs = 0
|
|
@@ -355,6 +400,7 @@ function reset_state() {
|
|
|
355
400
|
delete issues; n_issues = 0
|
|
356
401
|
delete wave_names; delete wave_of; delete wave_seq; delete wave_count
|
|
357
402
|
delete deferred_of; delete deferred_why; delete claimed_of; delete ordered_b; n_waves = 0
|
|
403
|
+
delete waits_of; delete waits_set
|
|
358
404
|
delete started; n_started = 0
|
|
359
405
|
fm_changelog = ""
|
|
360
406
|
delete changelog; n_changelog = 0; changelog_seen = 0; cl_open = 0
|
|
@@ -499,7 +545,13 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
499
545
|
if (wave_of[i] != w) continue
|
|
500
546
|
out = out (first ? "" : ",") "{\"branch\":\"" jesc(ordered_b[i]) "\",\"deferred\":" deferred_of[i] \
|
|
501
547
|
",\"deferred_reason\":\"" jesc(deferred_why[i]) "\"" \
|
|
502
|
-
",\"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 "}"
|
|
503
555
|
first = 0
|
|
504
556
|
}
|
|
505
557
|
out = out "]}"
|
|
@@ -603,6 +655,10 @@ in_fm {
|
|
|
603
655
|
else if (lower ~ /^rounds:/ && fm_rounds == "") fm_rounds = val_after_colon($0)
|
|
604
656
|
next
|
|
605
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 }
|
|
606
662
|
# Interior of multi-line HTML comments is non-content (template guidance
|
|
607
663
|
# blocks); single-line "<!-- ... -->" placeholders are unaffected.
|
|
608
664
|
#
|
|
@@ -622,13 +678,25 @@ in_comment {
|
|
|
622
678
|
if ($0 ~ /-->/) { in_comment = 0; in_challenge = 0 }
|
|
623
679
|
next
|
|
624
680
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
+
}
|
|
632
700
|
}
|
|
633
701
|
# A fenced code block is illustration, never contract — the same standing rule
|
|
634
702
|
# comment interiors and repeated headings already follow. A plan that documents
|
|
@@ -642,23 +710,34 @@ in_comment {
|
|
|
642
710
|
# example won its first-heading-wins guard and hid the real section). Toggling on
|
|
643
711
|
# a fence fence-marker line closes both.
|
|
644
712
|
#
|
|
645
|
-
# A fence marker is a line whose first non-space run is ``` or ~~~ (an info
|
|
646
|
-
# string like ```markdown may follow). The marker line itself is never content.
|
|
647
|
-
/^[ \t]*(```|~~~)/ { in_fence = !in_fence; next }
|
|
648
|
-
in_fence { next }
|
|
649
713
|
# First H1 is the title fallback (front matter title: still wins in emit).
|
|
650
714
|
/^#[ \t]/ && h1_title == "" { h1_title = trim(substr($0, 2)) }
|
|
651
715
|
/^## / {
|
|
652
716
|
if ($0 ~ /^## Status/) section = "status"
|
|
653
|
-
#
|
|
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
|
|
654
735
|
# section in prose, and those later headings are illustration, not contract.
|
|
655
|
-
else if ($0 ~ /^## Branches/
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
# as silently empty.
|
|
661
|
-
else if ($0 ~ /^## Waves/) { 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
|
+
}
|
|
662
741
|
else if ($0 ~ /^## Approval/) section = "approval"
|
|
663
742
|
# First `## Changelog` wins, for the same reason `## Branches` does: a plan
|
|
664
743
|
# about the plan format quotes the section in prose, and the later heading is
|
|
@@ -676,11 +755,35 @@ section == "status" {
|
|
|
676
755
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**issue[:*]/ && canon_issue == "") canon_issue = val_after_colon($0)
|
|
677
756
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**review[:*]/ && canon_review == "") canon_review = val_after_colon($0)
|
|
678
757
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**impl[:*]/ && canon_impl == "") canon_impl = val_after_colon($0)
|
|
679
|
-
|
|
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))
|
|
680
783
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**rounds[:*]/ && canon_rounds == "") canon_rounds = val_after_colon($0)
|
|
681
|
-
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**approved[:*]/ && canon_approved == "") canon_approved = val_after_colon($0)
|
|
682
|
-
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**released[:*]/ && canon_released == "") canon_released = val_after_colon($0)
|
|
683
|
-
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))
|
|
684
787
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**started[:*]/) {
|
|
685
788
|
_s = strip_placeholder(val_after_colon($0))
|
|
686
789
|
if (_s != "") started[++n_started] = _s
|
|
@@ -733,7 +836,23 @@ section == "changelog" {
|
|
|
733
836
|
cl_open = 0
|
|
734
837
|
next
|
|
735
838
|
}
|
|
736
|
-
|
|
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" {
|
|
737
856
|
# `### <name>` opens a wave. Branches before any subheading belong to an
|
|
738
857
|
# unnamed wave, so a pre-wave plan parses as exactly one wave.
|
|
739
858
|
if ($0 ~ /^###[ \t]/) {
|
|
@@ -768,13 +887,64 @@ section == "branches" {
|
|
|
768
887
|
# documented contract is that an annotation binds to the line carrying the
|
|
769
888
|
# branch name. A deferral whose text is wrapped onto a continuation line is
|
|
770
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.
|
|
771
901
|
defer_note = ""
|
|
772
|
-
if (index($0, "deferred:") > 0) {
|
|
902
|
+
if (index($0, "deferred:") > 0 || index($0, "moved:") > 0) {
|
|
773
903
|
_d = $0
|
|
774
|
-
sub(/^.*<!--[ \t]*deferred:[ \t]*/, "", _d)
|
|
904
|
+
sub(/^.*<!--[ \t]*(deferred|moved):[ \t]*/, "", _d)
|
|
775
905
|
sub(/[ \t]*-->.*$/, "", _d)
|
|
776
906
|
defer_note = trim(_d)
|
|
777
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
|
+
}
|
|
778
948
|
# ONE LIST ITEM, AT MOST ONE CLAIM — an `if`, not the `while` this was.
|
|
779
949
|
#
|
|
780
950
|
# The old loop walked the line taking every backticked name on it, which is
|
|
@@ -799,7 +969,7 @@ section == "branches" {
|
|
|
799
969
|
# plan can make about a branch, dropped for want of a colon. A reader
|
|
800
970
|
# writing the shorter form has said the branch will not be built; the
|
|
801
971
|
# parser now hears it.
|
|
802
|
-
deferred_of[n_branches] = ($0 ~ /<!--[ \t]*deferred[ \t]*(:|-->)/) ? "true" : "false"
|
|
972
|
+
deferred_of[n_branches] = ($0 ~ /<!--[ \t]*(deferred|moved)[ \t]*(:|-->)/) ? "true" : "false"
|
|
803
973
|
# The reason travels with the flag. Empty on every non-deferred branch, and
|
|
804
974
|
# empty is also the honest answer for the bare form: the branch IS deferred
|
|
805
975
|
# and no reason was recorded, which is a different statement from a reason
|
|
@@ -808,6 +978,10 @@ section == "branches" {
|
|
|
808
978
|
# Claim reflection, written by the worker after its ref push succeeds. This
|
|
809
979
|
# is a reflection, not the claim: git refs remain authoritative.
|
|
810
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
|
|
811
985
|
ordered_b[n_branches] = b
|
|
812
986
|
}
|
|
813
987
|
line = $0
|
|
@@ -842,7 +1016,7 @@ section == "branches" {
|
|
|
842
1016
|
# The emitted arrays (branches, prs, waves) must be byte-identical to what the
|
|
843
1017
|
# old shape produces for the same plan. So this shares every accumulation
|
|
844
1018
|
# variable with the branches handler above; only the EXTRACTION differs.
|
|
845
|
-
section == "
|
|
1019
|
+
section == "slices" && slice_shape == "heading" {
|
|
846
1020
|
# Only the `### ` heading carries meta. Body lines are prose — never scanned
|
|
847
1021
|
# for a branch or a PR, which is the whole point of the new shape.
|
|
848
1022
|
if ($0 !~ /^###[ \t]/) next
|
|
@@ -871,13 +1045,47 @@ section == "waves" {
|
|
|
871
1045
|
sub(/[ \t]*-->.*$/, "", _c)
|
|
872
1046
|
claim_note = trim(_c)
|
|
873
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.
|
|
874
1059
|
defer_note = ""
|
|
875
|
-
if (index($0, "deferred:") > 0) {
|
|
1060
|
+
if (index($0, "deferred:") > 0 || index($0, "moved:") > 0) {
|
|
876
1061
|
_d = $0
|
|
877
|
-
sub(/^.*<!--[ \t]*deferred:[ \t]*/, "", _d)
|
|
1062
|
+
sub(/^.*<!--[ \t]*(deferred|moved):[ \t]*/, "", _d)
|
|
878
1063
|
sub(/[ \t]*-->.*$/, "", _d)
|
|
879
1064
|
defer_note = trim(_d)
|
|
880
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
|
+
}
|
|
881
1089
|
|
|
882
1090
|
# The branch is the `Branch:` value, matched against the known prefixes exactly
|
|
883
1091
|
# as the old shape matched the backticked name. Written unquoted in the heading
|
|
@@ -894,9 +1102,11 @@ section == "waves" {
|
|
|
894
1102
|
wave_seq[n_branches] = ++wave_count[n_waves]
|
|
895
1103
|
# Bare `<!-- deferred -->` sets the flag with no reason, same as the old
|
|
896
1104
|
# shape; `deferred:` carries the reason.
|
|
897
|
-
deferred_of[n_branches] = ($0 ~ /<!--[ \t]*deferred[ \t]*(:|-->)/) ? "true" : "false"
|
|
1105
|
+
deferred_of[n_branches] = ($0 ~ /<!--[ \t]*(deferred|moved)[ \t]*(:|-->)/) ? "true" : "false"
|
|
898
1106
|
deferred_why[n_branches] = defer_note
|
|
899
1107
|
claimed_of[n_branches] = claim_note
|
|
1108
|
+
waits_of[n_branches] = waits_note
|
|
1109
|
+
waits_set[n_branches] = has_waits
|
|
900
1110
|
ordered_b[n_branches] = b
|
|
901
1111
|
}
|
|
902
1112
|
|