@plot-pm/board 0.10.0 → 0.12.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 +157 -151
- package/package.json +9 -1
- package/plot-agent-monitor.sh +532 -0
- package/plot-approve.sh +151 -32
- package/plot-budget.sh +439 -0
- package/plot-build-monitor.sh +435 -0
- package/plot-config.sh +17 -1
- package/plot-default-branch.sh +109 -0
- package/plot-deliver.sh +214 -119
- package/plot-dispatch.sh +1327 -188
- package/plot-fleet-scan.sh +1005 -170
- package/plot-host.sh +1438 -61
- package/plot-monitor-subject.sh +194 -0
- package/plot-plan-meta.sh +345 -47
- package/plot-pr-merged.sh +180 -0
- package/plot-reap.sh +719 -61
- package/plot-release-refs.sh +188 -46
- package/plot-resolve-artifact.sh +115 -22
- 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
|
@@ -20,9 +20,17 @@
|
|
|
20
20
|
# Two plan formats are recognized:
|
|
21
21
|
#
|
|
22
22
|
# canonical the plan template's `## Status` body section:
|
|
23
|
-
# - **
|
|
23
|
+
# - **State:** Approved
|
|
24
24
|
# - **Type:** feature
|
|
25
|
-
# (bullet, bold, and plain `
|
|
25
|
+
# (bullet, bold, and plain `State: ...` variants all accepted)
|
|
26
|
+
#
|
|
27
|
+
# `State:` is the field Plot writes. `Phase:` is the name it
|
|
28
|
+
# carried until 2026-09-07 and is read as the alternate, the
|
|
29
|
+
# same way front matter reads `status:` over `phase:` below.
|
|
30
|
+
# THE DUAL READ IS PERMANENT, NOT SCAFFOLDING: a plan file may
|
|
31
|
+
# have been written a year ago or copied from another project,
|
|
32
|
+
# and a Plot that refused to read `Phase:` would be worse at its
|
|
33
|
+
# own job than the one that confused two words.
|
|
26
34
|
#
|
|
27
35
|
# frontmatter YAML front matter at the top of the file:
|
|
28
36
|
# ---
|
|
@@ -88,7 +96,8 @@
|
|
|
88
96
|
# which is a queue. (`ready-for-review`/`in-review` DO
|
|
89
97
|
# normalize onto `approved` — those are synonyms; this is not.)
|
|
90
98
|
# phase_alt_raw secondary value when the file carries two (front matter
|
|
91
|
-
# status: AND phase
|
|
99
|
+
# status: AND phase:, or a `## Status` body carrying both
|
|
100
|
+
# State: AND Phase:), else ""
|
|
92
101
|
# phase_alt normalized phase_alt_raw (NONE when absent)
|
|
93
102
|
# type normalized plan type (feature|bug|docs|infra or "")
|
|
94
103
|
# title plan title: front matter `title:` wins, else the first H1
|
|
@@ -120,6 +129,27 @@
|
|
|
120
129
|
# `<!-- deferred -->` (bare, no colon) sets the flag with no
|
|
121
130
|
# reason; `waves[].branches[].deferred_reason` carries the
|
|
122
131
|
# sentence after the colon, "" where none was written.
|
|
132
|
+
# `<!-- waits: bug/other -->` names ONE branch this branch
|
|
133
|
+
# waits on, reported as `waves[].branches[].waits_on`. The key
|
|
134
|
+
# is ABSENT where no annotation was written — never "" — and
|
|
135
|
+
# the value is a branch name in this repo, not a plan slug and
|
|
136
|
+
# not a cross-repo reference. The parser reports what the file
|
|
137
|
+
# says: a prerequisite no plan declares still parses, and the
|
|
138
|
+
# scan is what turns that into a verdict. `waits:` and
|
|
139
|
+
# `deferred:` are independent — a branch may carry both.
|
|
140
|
+
# `<!-- builds: normalizeVersion, a shared helper -->` names
|
|
141
|
+
# what this slice BUILDS, reported as
|
|
142
|
+
# `waves[].branches[].builds`. OPTIONAL, like `Sprint:` and
|
|
143
|
+
# `Story:` — a docs plan, a rejection or a measurement builds
|
|
144
|
+
# nothing nameable, and nothing warns about its absence. The
|
|
145
|
+
# key is ABSENT where none was written, never "". The value
|
|
146
|
+
# runs to the closing marker rather than stopping at the first
|
|
147
|
+
# space the way `waits:` does: a prerequisite is a branch NAME
|
|
148
|
+
# and a deliverable is a name plus enough words to search for.
|
|
149
|
+
# An annotation rather than a `Builds:` field line precisely
|
|
150
|
+
# BECAUSE annotations already work in both slice dialects from
|
|
151
|
+
# one block of code — a field line would need two spellings,
|
|
152
|
+
# and the template writes the list dialect.
|
|
123
153
|
# prs PR numbers, sorted and unique, read from EITHER spelling:
|
|
124
154
|
# `→ #NNN` / `→ owner/repo#NNN` links in the `## Branches`
|
|
125
155
|
# section, OR `PR: #NNN` in a `## Waves` `### ` heading. The
|
|
@@ -291,6 +321,34 @@ function val_after_colon(s) {
|
|
|
291
321
|
}
|
|
292
322
|
# Template placeholders like "<!-- optional -->" mean "field absent".
|
|
293
323
|
function strip_placeholder(s) { return (s ~ /^<!--/) ? "" : s }
|
|
324
|
+
# Blank out backtick-delimited inline code, so a marker PRINTED as a literal is
|
|
325
|
+
# not read as syntax. Markdown renders `<!--` between backticks as the four
|
|
326
|
+
# characters; this parser used to read it as a comment-open, and because such a
|
|
327
|
+
# line carries no closing marker it swallowed the rest of the file — one
|
|
328
|
+
# backticked marker in a summary line cost a plan its phase, type and branches.
|
|
329
|
+
#
|
|
330
|
+
# Only the comment rules consult this. The line itself is untouched everywhere
|
|
331
|
+
# else, because branch names live in backticks too and stripping them for real
|
|
332
|
+
# would empty every `## Branches` entry.
|
|
333
|
+
#
|
|
334
|
+
# Runs of backticks are matched longest-first so a ``code`` span closes against
|
|
335
|
+
# its own delimiter. An UNPAIRED backtick leaves its tail as-is: that is prose
|
|
336
|
+
# with a stray tick, and prose is exactly where a real comment may open.
|
|
337
|
+
# NOTE: the closing-run index is named `shut` because `close` is an awk builtin
|
|
338
|
+
# and cannot be a parameter name — it fails as a syntax error on the function
|
|
339
|
+
# signature, pointing nowhere near the cause.
|
|
340
|
+
function mask_code(s, out, n, tick, shut) {
|
|
341
|
+
out = ""
|
|
342
|
+
while ((n = index(s, "`")) > 0) {
|
|
343
|
+
out = out substr(s, 1, n - 1)
|
|
344
|
+
s = substr(s, n)
|
|
345
|
+
tick = ""
|
|
346
|
+
while (substr(s, 1, 1) == "`") { tick = tick "`"; s = substr(s, 2) }
|
|
347
|
+
if ((shut = index(s, tick)) == 0) return out tick s
|
|
348
|
+
s = substr(s, shut + length(tick))
|
|
349
|
+
}
|
|
350
|
+
return out s
|
|
351
|
+
}
|
|
294
352
|
# First known phase token wins; NONE if empty; UNKNOWN otherwise.
|
|
295
353
|
function norm_phase(raw, lower, toks, n, i, t) {
|
|
296
354
|
if (raw == "") return "NONE"
|
|
@@ -346,7 +404,7 @@ function reset_state() {
|
|
|
346
404
|
fm_review = ""; fm_impl = ""; fm_approved = ""; fm_started = ""; fm_released = ""
|
|
347
405
|
fm_delivered = ""; fm_design = ""
|
|
348
406
|
fm_rounds = ""
|
|
349
|
-
canon_phase = ""; canon_type = ""
|
|
407
|
+
canon_state = ""; canon_phase = ""; canon_type = ""
|
|
350
408
|
canon_sprint = ""; canon_story = ""; canon_assignee = ""
|
|
351
409
|
canon_review = ""; canon_impl = ""; canon_approved = ""; canon_released = ""
|
|
352
410
|
canon_delivered = ""; canon_design = ""
|
|
@@ -356,7 +414,7 @@ function reset_state() {
|
|
|
356
414
|
# the field, so a consumer cannot mistake "never interrogated" for "asked
|
|
357
415
|
# nothing".
|
|
358
416
|
block_rounds = ""
|
|
359
|
-
in_fm = 0; section = ""; in_comment = 0; in_challenge = 0; in_fence = 0;
|
|
417
|
+
in_fm = 0; section = ""; in_comment = 0; in_challenge = 0; in_fence = 0; slices_seen = 0; slice_shape = ""
|
|
360
418
|
delete branches; n_branches = 0
|
|
361
419
|
delete prs; n_prs = 0
|
|
362
420
|
delete malformed_prs; n_malformed_prs = 0
|
|
@@ -364,6 +422,8 @@ function reset_state() {
|
|
|
364
422
|
delete issues; n_issues = 0
|
|
365
423
|
delete wave_names; delete wave_of; delete wave_seq; delete wave_count
|
|
366
424
|
delete deferred_of; delete deferred_why; delete claimed_of; delete ordered_b; n_waves = 0
|
|
425
|
+
delete waits_of; delete waits_set
|
|
426
|
+
delete builds_of; delete builds_set
|
|
367
427
|
delete started; n_started = 0
|
|
368
428
|
fm_changelog = ""
|
|
369
429
|
delete changelog; n_changelog = 0; changelog_seen = 0; cl_open = 0
|
|
@@ -374,8 +434,14 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
374
434
|
praw = (fm_status != "") ? fm_status : fm_phase
|
|
375
435
|
palt_raw = (fm_status != "" && fm_phase != "") ? fm_phase : ""
|
|
376
436
|
traw = fm_type
|
|
377
|
-
} else if (canon_phase != "") {
|
|
378
|
-
|
|
437
|
+
} else if (canon_state != "" || canon_phase != "") {
|
|
438
|
+
# `State:` is primary and `Phase:` the alternate, exactly as front matter
|
|
439
|
+
# reads `status:` over `phase:`. A file carrying both reports the
|
|
440
|
+
# disagreement rather than hiding it.
|
|
441
|
+
fmt = "canonical"
|
|
442
|
+
praw = (canon_state != "") ? canon_state : canon_phase
|
|
443
|
+
palt_raw = (canon_state != "" && canon_phase != "") ? canon_phase : ""
|
|
444
|
+
traw = canon_type
|
|
379
445
|
} else {
|
|
380
446
|
fmt = "none"; praw = ""; palt_raw = ""; traw = ""
|
|
381
447
|
}
|
|
@@ -508,7 +574,17 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
508
574
|
if (wave_of[i] != w) continue
|
|
509
575
|
out = out (first ? "" : ",") "{\"branch\":\"" jesc(ordered_b[i]) "\",\"deferred\":" deferred_of[i] \
|
|
510
576
|
",\"deferred_reason\":\"" jesc(deferred_why[i]) "\"" \
|
|
511
|
-
",\"claimed\":\"" jesc(claimed_of[i]) "\"
|
|
577
|
+
",\"claimed\":\"" jesc(claimed_of[i]) "\""
|
|
578
|
+
# ABSENT, NOT EMPTY, where no prerequisite was declared. The key appears
|
|
579
|
+
# only on a branch whose line carries a `waits:` annotation, so a consumer
|
|
580
|
+
# reading `waits_on` gets a branch name or nothing — never a blank string
|
|
581
|
+
# that reads as a prerequisite with no name.
|
|
582
|
+
if (waits_set[i] == 1) out = out ",\"waits_on\":\"" jesc(waits_of[i]) "\""
|
|
583
|
+
# ABSENT, NOT EMPTY, for the same reason `waits_on` is: a slice that
|
|
584
|
+
# names no deliverable emits no key, so a consumer reads a name or
|
|
585
|
+
# nothing. An empty string would read as a deliverable called "".
|
|
586
|
+
if (builds_set[i] == 1) out = out ",\"builds\":\"" jesc(builds_of[i]) "\""
|
|
587
|
+
out = out "}"
|
|
512
588
|
first = 0
|
|
513
589
|
}
|
|
514
590
|
out = out "]}"
|
|
@@ -612,6 +688,10 @@ in_fm {
|
|
|
612
688
|
else if (lower ~ /^rounds:/ && fm_rounds == "") fm_rounds = val_after_colon($0)
|
|
613
689
|
next
|
|
614
690
|
}
|
|
691
|
+
# A fence marker is a line whose first non-space run is ``` or ~~~ (an info
|
|
692
|
+
# string like ```markdown may follow). The marker line itself is never content.
|
|
693
|
+
/^[ \t]*(```|~~~)/ { in_fence = !in_fence; next }
|
|
694
|
+
in_fence { next }
|
|
615
695
|
# Interior of multi-line HTML comments is non-content (template guidance
|
|
616
696
|
# blocks); single-line "<!-- ... -->" placeholders are unaffected.
|
|
617
697
|
#
|
|
@@ -631,13 +711,25 @@ in_comment {
|
|
|
631
711
|
if ($0 ~ /-->/) { in_comment = 0; in_challenge = 0 }
|
|
632
712
|
next
|
|
633
713
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
714
|
+
# A comment opens only where the marker is SYNTAX. Inside inline code it is a
|
|
715
|
+
# literal being quoted, and the fence rules above have already consumed code
|
|
716
|
+
# blocks — so what reaches here is prose, minus its backticked spans.
|
|
717
|
+
#
|
|
718
|
+
# The cheap test guards the expensive one: /<!--/ on the raw line rejects the
|
|
719
|
+
# overwhelming majority, so mask_code runs once per line that actually carries a
|
|
720
|
+
# marker rather than twice per line of every plan. The masked verdict is decided
|
|
721
|
+
# INSIDE the rule so a line that turns out to be inline code simply falls
|
|
722
|
+
# through to the ordinary content rules below, carrying no state with it.
|
|
723
|
+
/<!--/ {
|
|
724
|
+
_masked = mask_code($0)
|
|
725
|
+
if (_masked ~ /<!--/ && _masked !~ /-->/) {
|
|
726
|
+
in_comment = 1
|
|
727
|
+
# A truncated block never closes; it simply runs to EOF as a comment, and
|
|
728
|
+
# the round stays whatever was read before the truncation — absent if the
|
|
729
|
+
# "round" line was itself lost. Nothing else in the record is affected.
|
|
730
|
+
in_challenge = ($0 ~ /CHALLENGE-THE-PLAN-METADATA/) ? 1 : 0
|
|
731
|
+
next
|
|
732
|
+
}
|
|
641
733
|
}
|
|
642
734
|
# A fenced code block is illustration, never contract — the same standing rule
|
|
643
735
|
# comment interiors and repeated headings already follow. A plan that documents
|
|
@@ -651,30 +743,34 @@ in_comment {
|
|
|
651
743
|
# example won its first-heading-wins guard and hid the real section). Toggling on
|
|
652
744
|
# a fence fence-marker line closes both.
|
|
653
745
|
#
|
|
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
746
|
# First H1 is the title fallback (front matter title: still wins in emit).
|
|
659
747
|
/^#[ \t]/ && h1_title == "" { h1_title = trim(substr($0, 2)) }
|
|
660
748
|
/^## / {
|
|
661
749
|
if ($0 ~ /^## Status/) section = "status"
|
|
662
|
-
#
|
|
750
|
+
# THE HEADING WORD NO LONGER PICKS THE LAYOUT. `## Branches`, `## Waves` and
|
|
751
|
+
# `## Slices` open ONE section, and which shape it holds is decided by the
|
|
752
|
+
# first `### ` heading in it — see `slice_shape` below.
|
|
753
|
+
#
|
|
754
|
+
# THEY USED TO SELECT IT, AND THAT MADE THE RENAME UNSAFE. The two layouts are
|
|
755
|
+
# genuinely different: the old one carries the branch on a LIST ITEM
|
|
756
|
+
# (`- ` + backtick + `feature/x` + backtick + ` -> #72`), the new one carries it in the
|
|
757
|
+
# heading (`### Name (Branch: feature/x, PR: #577)`). With the word choosing
|
|
758
|
+
# the consumer, renaming a heading silently changed which grammar was applied.
|
|
759
|
+
# Measured 2026-09-04 on `2026-08-14-parallel-agent-fleet.md`: renaming its
|
|
760
|
+
# `## Branches` to `## Slices` took it from **6 branches to 0**, with no error
|
|
761
|
+
# anywhere — the file still parsed, and simply held nothing.
|
|
762
|
+
#
|
|
763
|
+
# Deciding by shape makes the heading a NAME again. All three spellings read
|
|
764
|
+
# either layout, so a plan can be renamed without being rewritten, and the 48
|
|
765
|
+
# files still saying `## Branches` are safe to migrate a word at a time.
|
|
766
|
+
#
|
|
767
|
+
# First one wins, as before: a plan documenting the plan format quotes the
|
|
663
768
|
# 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 }
|
|
769
|
+
else if ($0 ~ /^## Branches/ || $0 ~ /^## Waves/ || $0 ~ /^## Slices/) {
|
|
770
|
+
section = slices_seen ? "" : "slices"
|
|
771
|
+
slices_seen = 1
|
|
772
|
+
slice_shape = ""
|
|
773
|
+
}
|
|
678
774
|
else if ($0 ~ /^## Approval/) section = "approval"
|
|
679
775
|
# First `## Changelog` wins, for the same reason `## Branches` does: a plan
|
|
680
776
|
# about the plan format quotes the section in prose, and the later heading is
|
|
@@ -685,18 +781,43 @@ in_fence { next }
|
|
|
685
781
|
}
|
|
686
782
|
section == "status" {
|
|
687
783
|
lower = tolower($0)
|
|
688
|
-
if (lower ~ /^[ \t]*[-*]?[ \t]*\**
|
|
784
|
+
if (lower ~ /^[ \t]*[-*]?[ \t]*\**state[:*]/ && canon_state == "") canon_state = val_after_colon($0)
|
|
785
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**phase[:*]/ && canon_phase == "") canon_phase = val_after_colon($0)
|
|
689
786
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**type[:*]/ && canon_type == "") canon_type = val_after_colon($0)
|
|
690
787
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**sprint[:*]/ && canon_sprint == "") canon_sprint = val_after_colon($0)
|
|
691
788
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**story[:*]/ && canon_story == "") canon_story = val_after_colon($0)
|
|
692
789
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**issue[:*]/ && canon_issue == "") canon_issue = val_after_colon($0)
|
|
693
790
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**review[:*]/ && canon_review == "") canon_review = val_after_colon($0)
|
|
694
791
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**impl[:*]/ && canon_impl == "") canon_impl = val_after_colon($0)
|
|
695
|
-
|
|
792
|
+
# EACH TAKES THE FIRST LINE THAT CARRIES A VALUE, NOT THE FIRST LINE.
|
|
793
|
+
#
|
|
794
|
+
# A plan may hold both a record and an unfilled placeholder for the same
|
|
795
|
+
# field, on either side of each other. Seven plans here write the placeholder
|
|
796
|
+
# as a per-line comment —
|
|
797
|
+
#
|
|
798
|
+
# - **Delivered:** <!-- YYYY-MM-DD -->
|
|
799
|
+
#
|
|
800
|
+
# — beside a real `- **Delivered:** 2026-09-01`. `strip_placeholder` used to
|
|
801
|
+
# run in `emit_record`, AFTER first-wins had already chosen: the placeholder
|
|
802
|
+
# claimed the slot, was emptied a moment later, and the record it beat was
|
|
803
|
+
# gone. Whether a plan parsed correctly then depended on which line came
|
|
804
|
+
# first, and `append_delivered_line` decides that by where a `<!--` happens
|
|
805
|
+
# to stop its scan.
|
|
806
|
+
#
|
|
807
|
+
# A DELIVERED PLAN THAT READS `delivered_raw: ""` IS INVISIBLE TO THE SCAN,
|
|
808
|
+
# which reads its rolling window from that field — the same failure the
|
|
809
|
+
# comment-block fix addressed, reached by a different road. Measured
|
|
810
|
+
# 2026-09-01 on `a-machine-is-an-instance`: parsed correctly, but only
|
|
811
|
+
# because its record sat two lines above its placeholder.
|
|
812
|
+
#
|
|
813
|
+
# `started` never had this bug because it is a list and had to filter at
|
|
814
|
+
# capture to avoid storing empties. These four are scalars, and now do the
|
|
815
|
+
# same: a placeholder no longer counts as a value, so ORDER STOPS MATTERING.
|
|
816
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**design[:*]/ && canon_design == "") canon_design = strip_placeholder(val_after_colon($0))
|
|
696
817
|
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)
|
|
818
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**approved[:*]/ && canon_approved == "") canon_approved = strip_placeholder(val_after_colon($0))
|
|
819
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**released[:*]/ && canon_released == "") canon_released = strip_placeholder(val_after_colon($0))
|
|
820
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**delivered[:*]/ && canon_delivered == "") canon_delivered = strip_placeholder(val_after_colon($0))
|
|
700
821
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**started[:*]/) {
|
|
701
822
|
_s = strip_placeholder(val_after_colon($0))
|
|
702
823
|
if (_s != "") started[++n_started] = _s
|
|
@@ -749,7 +870,23 @@ section == "changelog" {
|
|
|
749
870
|
cl_open = 0
|
|
750
871
|
next
|
|
751
872
|
}
|
|
752
|
-
|
|
873
|
+
# WHICH SHAPE THIS SECTION HOLDS, decided once from its first `### ` heading and
|
|
874
|
+
# then fixed for the rest of the section.
|
|
875
|
+
#
|
|
876
|
+
# `(Branch:` IS THE MARKER, and it is the only reliable one. A heading carrying
|
|
877
|
+
# it is the new shape by construction — that parenthetical is where the new
|
|
878
|
+
# layout puts the branch. A heading without it is the old shape, whose headings
|
|
879
|
+
# are bare names (`### Tracer`) and whose branches ride list items below.
|
|
880
|
+
#
|
|
881
|
+
# A SECTION WITH NO `### ` AT ALL is the old shape, and must be: a plan written
|
|
882
|
+
# before subheadings existed is one unnamed wave of list items, which is exactly
|
|
883
|
+
# what the old consumer produces. `slice_shape` therefore stays `""` until a
|
|
884
|
+
# heading is seen, and `""` routes to the old consumer.
|
|
885
|
+
section == "slices" && $0 ~ /^###[ \t]/ && slice_shape == "" {
|
|
886
|
+
slice_shape = (index($0, "(Branch:") > 0) ? "heading" : "list"
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
section == "slices" && slice_shape != "heading" {
|
|
753
890
|
# `### <name>` opens a wave. Branches before any subheading belong to an
|
|
754
891
|
# unnamed wave, so a pre-wave plan parses as exactly one wave.
|
|
755
892
|
if ($0 ~ /^###[ \t]/) {
|
|
@@ -784,13 +921,97 @@ section == "branches" {
|
|
|
784
921
|
# documented contract is that an annotation binds to the line carrying the
|
|
785
922
|
# branch name. A deferral whose text is wrapped onto a continuation line is
|
|
786
923
|
# not seen, exactly as `deferred` itself was not.
|
|
924
|
+
# `moved:` IS THE SAME ANSWER AS `deferred:`, and CLAUDE.md has said so since
|
|
925
|
+
# the reconcile scan was written: *"`deferred:`/`moved:` in the plan means
|
|
926
|
+
# reapable"*. `plot-reconcile-scan.sh:504` matches both in one arm; this
|
|
927
|
+
# parser matched only the first, so a slice given up with the other word read
|
|
928
|
+
# as still outstanding — to the board, the delivery gate and the fleet alike.
|
|
929
|
+
# Measured 2026-09-04 on `feature/the-reaper-sweeps-every-kind`: annotated
|
|
930
|
+
# `moved:` into the slice that superseded it, still `deferred:false` here.
|
|
931
|
+
#
|
|
932
|
+
# The two words differ in what they tell a READER — given up, versus taken
|
|
933
|
+
# somewhere else — and the reason is kept verbatim, so the distinction is not
|
|
934
|
+
# lost by being read alike.
|
|
787
935
|
defer_note = ""
|
|
788
|
-
if (index($0, "deferred:") > 0) {
|
|
936
|
+
if (index($0, "deferred:") > 0 || index($0, "moved:") > 0) {
|
|
789
937
|
_d = $0
|
|
790
|
-
sub(/^.*<!--[ \t]*deferred:[ \t]*/, "", _d)
|
|
938
|
+
sub(/^.*<!--[ \t]*(deferred|moved):[ \t]*/, "", _d)
|
|
791
939
|
sub(/[ \t]*-->.*$/, "", _d)
|
|
792
940
|
defer_note = trim(_d)
|
|
793
941
|
}
|
|
942
|
+
# THE PREREQUISITE THIS BRANCH NAMES: `<!-- waits: bug/other-branch -->`.
|
|
943
|
+
#
|
|
944
|
+
# ONE branch, never a list. A slice needing two prerequisites has not been cut
|
|
945
|
+
# finely enough, and a list invites a dependency graph nobody wants to debug.
|
|
946
|
+
# The greedy `.*` takes the LAST annotation when a line carries two, which is
|
|
947
|
+
# the same rule `deferred:` and `claimed:` already follow.
|
|
948
|
+
#
|
|
949
|
+
# The value is a BRANCH NAME, so it stops at the first whitespace rather than
|
|
950
|
+
# running to the closing marker the way a deferral reason does: a reason is a
|
|
951
|
+
# sentence, a branch name is a token, and trailing prose inside the comment
|
|
952
|
+
# would silently become part of a name that then matches nothing.
|
|
953
|
+
#
|
|
954
|
+
# `deferred:` is a judgement and `waits:` is a checkable fact, so the two are
|
|
955
|
+
# separate annotations and both may sit on one line. Read here, beside the
|
|
956
|
+
# other two, for the same reason: `match()` in the branch loop below clobbers
|
|
957
|
+
# RSTART/RLENGTH, so anything read from the whole line must be read first.
|
|
958
|
+
#
|
|
959
|
+
# `has_waits` carries presence separately from the value, because ABSENT and
|
|
960
|
+
# EMPTY are different answers — a branch declaring no prerequisite emits no
|
|
961
|
+
# `waits_on` key at all.
|
|
962
|
+
# WHAT THIS SLICE BUILDS: `<!-- builds: normalizeVersion, a shared helper -->`.
|
|
963
|
+
#
|
|
964
|
+
# AN ANNOTATION, NOT A FIELD LINE, and that is what makes it work in BOTH
|
|
965
|
+
# slice dialects with no dialect-specific code. Annotations bind to the line
|
|
966
|
+
# carrying the branch name — the list item in one spelling, the `### `
|
|
967
|
+
# heading in the other — and both dialects already read `deferred:`,
|
|
968
|
+
# `claimed:` and `waits:` with the identical block. A `Builds:` field line
|
|
969
|
+
# would have had two spellings to parse, and the template writes the LIST
|
|
970
|
+
# dialect, so the heading-only version would be absent from every plan
|
|
971
|
+
# created from it.
|
|
972
|
+
#
|
|
973
|
+
# IT BELONGS TO THE SLICE, NEVER TO THE PLAN. A plan builds several things
|
|
974
|
+
# and each slice builds one; a plan-level list is searched as a whole and
|
|
975
|
+
# reported against the wrong slice.
|
|
976
|
+
#
|
|
977
|
+
# The value runs to the closing marker, the way `deferred:` does and unlike
|
|
978
|
+
# `waits:`. A prerequisite is a branch NAME and stops at whitespace; a
|
|
979
|
+
# deliverable is a name plus enough words to search for — `normalizeVersion,
|
|
980
|
+
# a shared helper` — and cutting it at the first space would leave the half
|
|
981
|
+
# that identifies it.
|
|
982
|
+
#
|
|
983
|
+
# `has_builds` carries presence separately from the value, exactly as
|
|
984
|
+
# `waits:` does: a slice declaring nothing emits no key, so a consumer reads
|
|
985
|
+
# a deliverable or nothing and never a blank string that looks like one.
|
|
986
|
+
builds_note = ""
|
|
987
|
+
has_builds = 0
|
|
988
|
+
if ($0 ~ /<!--[ \t]*builds:[ \t]*/) {
|
|
989
|
+
_bl = $0
|
|
990
|
+
sub(/^.*<!--[ \t]*builds:[ \t]*/, "", _bl)
|
|
991
|
+
sub(/[ \t]*-->.*$/, "", _bl)
|
|
992
|
+
builds_note = trim(_bl)
|
|
993
|
+
if (builds_note != "") has_builds = 1
|
|
994
|
+
}
|
|
995
|
+
waits_note = ""
|
|
996
|
+
has_waits = 0
|
|
997
|
+
if ($0 ~ /<!--[ \t]*waits:[ \t]*/) {
|
|
998
|
+
_w = $0
|
|
999
|
+
sub(/^.*<!--[ \t]*waits:[ \t]*/, "", _w)
|
|
1000
|
+
sub(/[ \t].*$/, "", _w)
|
|
1001
|
+
sub(/-->.*$/, "", _w)
|
|
1002
|
+
waits_note = trim(_w)
|
|
1003
|
+
# THE VALUE MUST LOOK LIKE A BRANCH, and that check is what keeps a
|
|
1004
|
+
# SYNTAX EXAMPLE from becoming a declaration. A plan that documents the
|
|
1005
|
+
# annotation writes the literal marker in prose, and no comment-aware
|
|
1006
|
+
# reading can tell that apart from the real thing on the same line — the
|
|
1007
|
+
# branch prefixes can. `<branch>` is not a branch name; `bug/x` is.
|
|
1008
|
+
#
|
|
1009
|
+
# Reusing the branch prefixes rather than a new pattern: the prerequisite
|
|
1010
|
+
# IS a branch in this repo, so the two must never disagree about what a
|
|
1011
|
+
# branch name looks like.
|
|
1012
|
+
if (waits_note ~ "^(" PREFIXES ")/[^ \t]+$") has_waits = 1
|
|
1013
|
+
else waits_note = ""
|
|
1014
|
+
}
|
|
794
1015
|
# ONE LIST ITEM, AT MOST ONE CLAIM — an `if`, not the `while` this was.
|
|
795
1016
|
#
|
|
796
1017
|
# The old loop walked the line taking every backticked name on it, which is
|
|
@@ -815,7 +1036,7 @@ section == "branches" {
|
|
|
815
1036
|
# plan can make about a branch, dropped for want of a colon. A reader
|
|
816
1037
|
# writing the shorter form has said the branch will not be built; the
|
|
817
1038
|
# parser now hears it.
|
|
818
|
-
deferred_of[n_branches] = ($0 ~ /<!--[ \t]*deferred[ \t]*(:|-->)/) ? "true" : "false"
|
|
1039
|
+
deferred_of[n_branches] = ($0 ~ /<!--[ \t]*(deferred|moved)[ \t]*(:|-->)/) ? "true" : "false"
|
|
819
1040
|
# The reason travels with the flag. Empty on every non-deferred branch, and
|
|
820
1041
|
# empty is also the honest answer for the bare form: the branch IS deferred
|
|
821
1042
|
# and no reason was recorded, which is a different statement from a reason
|
|
@@ -824,6 +1045,12 @@ section == "branches" {
|
|
|
824
1045
|
# Claim reflection, written by the worker after its ref push succeeds. This
|
|
825
1046
|
# is a reflection, not the claim: git refs remain authoritative.
|
|
826
1047
|
claimed_of[n_branches] = claim_note
|
|
1048
|
+
# The prerequisite travels with the branch. Presence is tracked separately
|
|
1049
|
+
# so a branch that declares none emits no key.
|
|
1050
|
+
waits_of[n_branches] = waits_note
|
|
1051
|
+
waits_set[n_branches] = has_waits
|
|
1052
|
+
builds_of[n_branches] = builds_note
|
|
1053
|
+
builds_set[n_branches] = has_builds
|
|
827
1054
|
ordered_b[n_branches] = b
|
|
828
1055
|
}
|
|
829
1056
|
line = $0
|
|
@@ -858,7 +1085,7 @@ section == "branches" {
|
|
|
858
1085
|
# The emitted arrays (branches, prs, waves) must be byte-identical to what the
|
|
859
1086
|
# old shape produces for the same plan. So this shares every accumulation
|
|
860
1087
|
# variable with the branches handler above; only the EXTRACTION differs.
|
|
861
|
-
section == "
|
|
1088
|
+
section == "slices" && slice_shape == "heading" {
|
|
862
1089
|
# Only the `### ` heading carries meta. Body lines are prose — never scanned
|
|
863
1090
|
# for a branch or a PR, which is the whole point of the new shape.
|
|
864
1091
|
if ($0 !~ /^###[ \t]/) next
|
|
@@ -887,13 +1114,80 @@ section == "waves" {
|
|
|
887
1114
|
sub(/[ \t]*-->.*$/, "", _c)
|
|
888
1115
|
claim_note = trim(_c)
|
|
889
1116
|
}
|
|
1117
|
+
# `moved:` IS THE SAME ANSWER AS `deferred:`, and CLAUDE.md has said so since
|
|
1118
|
+
# the reconcile scan was written: *"`deferred:`/`moved:` in the plan means
|
|
1119
|
+
# reapable"*. `plot-reconcile-scan.sh:504` matches both in one arm; this
|
|
1120
|
+
# parser matched only the first, so a slice given up with the other word read
|
|
1121
|
+
# as still outstanding — to the board, the delivery gate and the fleet alike.
|
|
1122
|
+
# Measured 2026-09-04 on `feature/the-reaper-sweeps-every-kind`: annotated
|
|
1123
|
+
# `moved:` into the slice that superseded it, still `deferred:false` here.
|
|
1124
|
+
#
|
|
1125
|
+
# The two words differ in what they tell a READER — given up, versus taken
|
|
1126
|
+
# somewhere else — and the reason is kept verbatim, so the distinction is not
|
|
1127
|
+
# lost by being read alike.
|
|
890
1128
|
defer_note = ""
|
|
891
|
-
if (index($0, "deferred:") > 0) {
|
|
1129
|
+
if (index($0, "deferred:") > 0 || index($0, "moved:") > 0) {
|
|
892
1130
|
_d = $0
|
|
893
|
-
sub(/^.*<!--[ \t]*deferred:[ \t]*/, "", _d)
|
|
1131
|
+
sub(/^.*<!--[ \t]*(deferred|moved):[ \t]*/, "", _d)
|
|
894
1132
|
sub(/[ \t]*-->.*$/, "", _d)
|
|
895
1133
|
defer_note = trim(_d)
|
|
896
1134
|
}
|
|
1135
|
+
# The prerequisite, read exactly as the list-item spelling reads it. Both
|
|
1136
|
+
# dialects emit the same waves[], so a field added to one only would break
|
|
1137
|
+
# that contract the first time a plan migrated.
|
|
1138
|
+
# WHAT THIS SLICE BUILDS: `<!-- builds: normalizeVersion, a shared helper -->`.
|
|
1139
|
+
#
|
|
1140
|
+
# AN ANNOTATION, NOT A FIELD LINE, and that is what makes it work in BOTH
|
|
1141
|
+
# slice dialects with no dialect-specific code. Annotations bind to the line
|
|
1142
|
+
# carrying the branch name — the list item in one spelling, the `### `
|
|
1143
|
+
# heading in the other — and both dialects already read `deferred:`,
|
|
1144
|
+
# `claimed:` and `waits:` with the identical block. A `Builds:` field line
|
|
1145
|
+
# would have had two spellings to parse, and the template writes the LIST
|
|
1146
|
+
# dialect, so the heading-only version would be absent from every plan
|
|
1147
|
+
# created from it.
|
|
1148
|
+
#
|
|
1149
|
+
# IT BELONGS TO THE SLICE, NEVER TO THE PLAN. A plan builds several things
|
|
1150
|
+
# and each slice builds one; a plan-level list is searched as a whole and
|
|
1151
|
+
# reported against the wrong slice.
|
|
1152
|
+
#
|
|
1153
|
+
# The value runs to the closing marker, the way `deferred:` does and unlike
|
|
1154
|
+
# `waits:`. A prerequisite is a branch NAME and stops at whitespace; a
|
|
1155
|
+
# deliverable is a name plus enough words to search for — `normalizeVersion,
|
|
1156
|
+
# a shared helper` — and cutting it at the first space would leave the half
|
|
1157
|
+
# that identifies it.
|
|
1158
|
+
#
|
|
1159
|
+
# `has_builds` carries presence separately from the value, exactly as
|
|
1160
|
+
# `waits:` does: a slice declaring nothing emits no key, so a consumer reads
|
|
1161
|
+
# a deliverable or nothing and never a blank string that looks like one.
|
|
1162
|
+
builds_note = ""
|
|
1163
|
+
has_builds = 0
|
|
1164
|
+
if ($0 ~ /<!--[ \t]*builds:[ \t]*/) {
|
|
1165
|
+
_bl = $0
|
|
1166
|
+
sub(/^.*<!--[ \t]*builds:[ \t]*/, "", _bl)
|
|
1167
|
+
sub(/[ \t]*-->.*$/, "", _bl)
|
|
1168
|
+
builds_note = trim(_bl)
|
|
1169
|
+
if (builds_note != "") has_builds = 1
|
|
1170
|
+
}
|
|
1171
|
+
waits_note = ""
|
|
1172
|
+
has_waits = 0
|
|
1173
|
+
if ($0 ~ /<!--[ \t]*waits:[ \t]*/) {
|
|
1174
|
+
_w = $0
|
|
1175
|
+
sub(/^.*<!--[ \t]*waits:[ \t]*/, "", _w)
|
|
1176
|
+
sub(/[ \t].*$/, "", _w)
|
|
1177
|
+
sub(/-->.*$/, "", _w)
|
|
1178
|
+
waits_note = trim(_w)
|
|
1179
|
+
# THE VALUE MUST LOOK LIKE A BRANCH, and that check is what keeps a
|
|
1180
|
+
# SYNTAX EXAMPLE from becoming a declaration. A plan that documents the
|
|
1181
|
+
# annotation writes the literal marker in prose, and no comment-aware
|
|
1182
|
+
# reading can tell that apart from the real thing on the same line — the
|
|
1183
|
+
# branch prefixes can. `<branch>` is not a branch name; `bug/x` is.
|
|
1184
|
+
#
|
|
1185
|
+
# Reusing the branch prefixes rather than a new pattern: the prerequisite
|
|
1186
|
+
# IS a branch in this repo, so the two must never disagree about what a
|
|
1187
|
+
# branch name looks like.
|
|
1188
|
+
if (waits_note ~ "^(" PREFIXES ")/[^ \t]+$") has_waits = 1
|
|
1189
|
+
else waits_note = ""
|
|
1190
|
+
}
|
|
897
1191
|
|
|
898
1192
|
# The branch is the `Branch:` value, matched against the known prefixes exactly
|
|
899
1193
|
# as the old shape matched the backticked name. Written unquoted in the heading
|
|
@@ -910,9 +1204,13 @@ section == "waves" {
|
|
|
910
1204
|
wave_seq[n_branches] = ++wave_count[n_waves]
|
|
911
1205
|
# Bare `<!-- deferred -->` sets the flag with no reason, same as the old
|
|
912
1206
|
# shape; `deferred:` carries the reason.
|
|
913
|
-
deferred_of[n_branches] = ($0 ~ /<!--[ \t]*deferred[ \t]*(:|-->)/) ? "true" : "false"
|
|
1207
|
+
deferred_of[n_branches] = ($0 ~ /<!--[ \t]*(deferred|moved)[ \t]*(:|-->)/) ? "true" : "false"
|
|
914
1208
|
deferred_why[n_branches] = defer_note
|
|
915
1209
|
claimed_of[n_branches] = claim_note
|
|
1210
|
+
waits_of[n_branches] = waits_note
|
|
1211
|
+
waits_set[n_branches] = has_waits
|
|
1212
|
+
builds_of[n_branches] = builds_note
|
|
1213
|
+
builds_set[n_branches] = has_builds
|
|
916
1214
|
ordered_b[n_branches] = b
|
|
917
1215
|
}
|
|
918
1216
|
|