@plot-pm/board 0.14.1 → 0.14.3

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-plan-meta.sh CHANGED
@@ -150,6 +150,16 @@
150
150
  # BECAUSE annotations already work in both slice dialects from
151
151
  # one block of code — a field line would need two spellings,
152
152
  # and the template writes the list dialect.
153
+ # `<!-- agent: reviewer -->` names which KIND of agent this
154
+ # slice needs, reported as `waves[].branches[].agent` and read
155
+ # by plot-dispatch.sh where `--agent` is absent. OPTIONAL, and
156
+ # the key is ABSENT where none was written, never "" — dispatch
157
+ # reads presence, so a blank would send it looking for a charter
158
+ # called "". The value runs to the closing marker, as `builds:`
159
+ # does. It CANNOT validate its value the way `waits:` does: a
160
+ # charter name is a bare word with no structure to check, so a
161
+ # template documents the marker inside an outer comment block,
162
+ # where this pattern cannot see it.
153
163
  # prs PR numbers, sorted and unique, read from EITHER spelling:
154
164
  # `→ #NNN` / `→ owner/repo#NNN` links in the `## Branches`
155
165
  # section, OR `PR: #NNN` in a `## Waves` `### ` heading. The
@@ -424,24 +434,41 @@ function reset_state() {
424
434
  delete deferred_of; delete deferred_why; delete claimed_of; delete ordered_b; n_waves = 0
425
435
  delete waits_of; delete waits_set
426
436
  delete builds_of; delete builds_set
437
+ delete agent_of; delete agent_set
427
438
  delete started; n_started = 0
428
439
  fm_changelog = ""
429
440
  delete changelog; n_changelog = 0; changelog_seen = 0; cl_open = 0
430
441
  }
431
442
  function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assignee, review, impl, design, approved, delivered, issue, issue2, i, j, v, is_dup, out, sorted_b, sorted_p, sorted_i, nb, np, ni, num_issues, str_issues, n_num_i, n_str_i, issue_is_str) {
432
- if (fm_status != "" || fm_phase != "") {
433
- fmt = "frontmatter"
434
- praw = (fm_status != "") ? fm_status : fm_phase
435
- palt_raw = (fm_status != "" && fm_phase != "") ? fm_phase : ""
436
- traw = fm_type
437
- } else if (canon_state != "" || canon_phase != "") {
443
+ # THE PHASE IS READ FROM THE FIELD PLOT WRITES. A canonical `State:`/`Phase:`
444
+ # outranks front matter, because every lifecycle script writes the canonical
445
+ # body and none writes front matter: `plot-approve.sh` holds zero front-matter
446
+ # references, and the five in `plot-deliver.sh` all REFUSE the case rather
447
+ # than write it. A plan carrying both therefore reported a value no transition
448
+ # had ever touched - measured 2026-09-17, an approval wrote `State: Approved`,
449
+ # the parser answered `draft`, and the scan dispatched nothing.
450
+ #
451
+ # `phase_alt` carries the loser, so the disagreement stays readable - that is
452
+ # the field the delivery gate in `plot-deliver.sh` reads.
453
+ #
454
+ # THE PRECEDENCE MOVES FOR THE FIELDS PLOT OWNS, NOT FOR EVERY FIELD. The
455
+ # board-facing fields below, `design_raw` among them, keep front-matter-wins:
456
+ # no lifecycle script writes any of them, so neither record is more current.
457
+ if (canon_state != "" || canon_phase != "") {
438
458
  # `State:` is primary and `Phase:` the alternate, exactly as front matter
439
459
  # reads `status:` over `phase:`. A file carrying both reports the
440
- # disagreement rather than hiding it.
460
+ # disagreement rather than hiding it, and front matter joins that alternate
461
+ # when the canonical body answered first.
441
462
  fmt = "canonical"
442
463
  praw = (canon_state != "") ? canon_state : canon_phase
443
464
  palt_raw = (canon_state != "" && canon_phase != "") ? canon_phase : ""
465
+ if (palt_raw == "") palt_raw = (fm_status != "") ? fm_status : fm_phase
444
466
  traw = canon_type
467
+ } else if (fm_status != "" || fm_phase != "") {
468
+ fmt = "frontmatter"
469
+ praw = (fm_status != "") ? fm_status : fm_phase
470
+ palt_raw = (fm_status != "" && fm_phase != "") ? fm_phase : ""
471
+ traw = fm_type
445
472
  } else {
446
473
  fmt = "none"; praw = ""; palt_raw = ""; traw = ""
447
474
  }
@@ -584,6 +611,13 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
584
611
  # names no deliverable emits no key, so a consumer reads a name or
585
612
  # nothing. An empty string would read as a deliverable called "".
586
613
  if (builds_set[i] == 1) out = out ",\"builds\":\"" jesc(builds_of[i]) "\""
614
+ # ABSENT, NOT EMPTY, the rule the three annotations above already keep.
615
+ # Dispatch reads the PRESENCE of this key to decide whether the plan names
616
+ # a kind at all, so an `"agent":""` would read as a charter called "" and
617
+ # send it looking for `.plot/charters/.json`.
618
+ # NOTE: no apostrophe may appear in this awk region — the whole program is
619
+ # one single-quoted shell string, and one closes it mid-comment.
620
+ if (agent_set[i] == 1) out = out ",\"agent\":\"" jesc(agent_of[i]) "\""
587
621
  out = out "}"
588
622
  first = 0
589
623
  }
@@ -1012,6 +1046,44 @@ section == "slices" && slice_shape != "heading" {
1012
1046
  if (waits_note ~ "^(" PREFIXES ")/[^ \t]+$") has_waits = 1
1013
1047
  else waits_note = ""
1014
1048
  }
1049
+ # WHICH KIND OF AGENT THIS SLICE NEEDS: `<!-- agent: reviewer -->`.
1050
+ #
1051
+ # `--agent <name>` was the only selector and only an operator could type it.
1052
+ # `plot-registryd` hands a queued slice to a free agent with no `--agent`
1053
+ # anywhere in the path, so an unattended fleet ran every slice as the same
1054
+ # undifferentiated worker. The declaration has to live in the PLAN to reach a
1055
+ # dispatch nobody is watching.
1056
+ #
1057
+ # PER-BRANCH, NEVER A `## Status` FIELD. Every Status field is plan-level, so
1058
+ # one there could declare a single kind for a plan with several slices — and a
1059
+ # reviewer slice beside an implementer slice is the population this is for.
1060
+ #
1061
+ # The value runs to the closing marker, the way `builds:` does and unlike
1062
+ # `waits:`. A charter name is a bare word today, so both rules agree; this one
1063
+ # is chosen because it does not silently truncate if a name grows a space.
1064
+ #
1065
+ # IT CANNOT VALIDATE ITS VALUE, and that is the difference from `waits:` above.
1066
+ # A prerequisite has branch-prefix structure, which is what keeps a SYNTAX
1067
+ # EXAMPLE in prose from reading as a declaration; a charter name is a bare word
1068
+ # with no structure to check. What protects the templates instead is NESTING:
1069
+ # their documentation sits inside an outer `<!-- ... -->` block, so the inner
1070
+ # `agent:` carries no `<!--` of its own and this pattern never matches it —
1071
+ # the same accident of shape that already keeps `builds:` out of the parse of
1072
+ # a template. Documenting it as a bare `<!-- agent: reviewer -->` line would
1073
+ # hand a kind to every plan created from that template. A test pins it.
1074
+ #
1075
+ # `has_agent` carries presence separately from the value, as all three
1076
+ # annotations before it do: a slice naming no kind emits no key, so dispatch
1077
+ # reads a name or nothing and never a blank string that looks like one.
1078
+ agent_note = ""
1079
+ has_agent = 0
1080
+ if ($0 ~ /<!--[ \t]*agent:[ \t]*/) {
1081
+ _ag = $0
1082
+ sub(/^.*<!--[ \t]*agent:[ \t]*/, "", _ag)
1083
+ sub(/[ \t]*-->.*$/, "", _ag)
1084
+ agent_note = trim(_ag)
1085
+ if (agent_note != "") has_agent = 1
1086
+ }
1015
1087
  # ONE LIST ITEM, AT MOST ONE CLAIM — an `if`, not the `while` this was.
1016
1088
  #
1017
1089
  # The old loop walked the line taking every backticked name on it, which is
@@ -1051,6 +1123,10 @@ section == "slices" && slice_shape != "heading" {
1051
1123
  waits_set[n_branches] = has_waits
1052
1124
  builds_of[n_branches] = builds_note
1053
1125
  builds_set[n_branches] = has_builds
1126
+ # The kind travels with the branch, presence tracked separately so a slice
1127
+ # declaring none emits no key.
1128
+ agent_of[n_branches] = agent_note
1129
+ agent_set[n_branches] = has_agent
1054
1130
  ordered_b[n_branches] = b
1055
1131
  }
1056
1132
  line = $0
@@ -1188,6 +1264,19 @@ section == "slices" && slice_shape == "heading" {
1188
1264
  if (waits_note ~ "^(" PREFIXES ")/[^ \t]+$") has_waits = 1
1189
1265
  else waits_note = ""
1190
1266
  }
1267
+ # The agent kind, read exactly as the list-item spelling reads it. Both
1268
+ # dialects emit the same waves[], so a field added to one only would break that
1269
+ # contract the first time a plan migrated. See the list-item block for why the
1270
+ # value runs to the closing marker and why this one cannot validate itself.
1271
+ agent_note = ""
1272
+ has_agent = 0
1273
+ if ($0 ~ /<!--[ \t]*agent:[ \t]*/) {
1274
+ _ag = $0
1275
+ sub(/^.*<!--[ \t]*agent:[ \t]*/, "", _ag)
1276
+ sub(/[ \t]*-->.*$/, "", _ag)
1277
+ agent_note = trim(_ag)
1278
+ if (agent_note != "") has_agent = 1
1279
+ }
1191
1280
 
1192
1281
  # The branch is the `Branch:` value, matched against the known prefixes exactly
1193
1282
  # as the old shape matched the backticked name. Written unquoted in the heading
@@ -1211,6 +1300,8 @@ section == "slices" && slice_shape == "heading" {
1211
1300
  waits_set[n_branches] = has_waits
1212
1301
  builds_of[n_branches] = builds_note
1213
1302
  builds_set[n_branches] = has_builds
1303
+ agent_of[n_branches] = agent_note
1304
+ agent_set[n_branches] = has_agent
1214
1305
  ordered_b[n_branches] = b
1215
1306
  }
1216
1307