@plot-pm/board 0.8.0-rc.9 → 0.8.1-rc.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plot-pm/board",
3
- "version": "0.8.0-rc.9",
3
+ "version": "0.8.1-rc.0",
4
4
  "description": "Local Kanban board for Plot — a glanceable view of plan phases from docs/plans, with sprint and story filters",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/plot-config.sh CHANGED
@@ -27,6 +27,21 @@
27
27
  # Project board | Branch prefixes | Plan directory | Active index |
28
28
  # Delivered index | Sprint directory | Story directory | Story index |
29
29
  # Plan template | Main branch | Board command
30
+ # Worker bound seconds a single prompt run may take in the worker loop
31
+ # before it is ended and the worker exits (no hop). Read by
32
+ # plot-worker-loop.sh; default 3600 (~1h), `0` disables it.
33
+ # Non-numeric or empty falls back to the default. It bounds
34
+ # a HUNG agent — one whose CLI crashed without exiting — so
35
+ # one dead worker cannot hold a slot for hours.
36
+ # Agent registry the directory the dispatcher writes agent manifests to,
37
+ # read by the board's registry. Default `.plot/agents`
38
+ # (repo-relative, gitignored, hence per-worktree). A board
39
+ # served from a worktree the dispatcher never wrote to
40
+ # reads an empty directory and synthesizes the whole fleet
41
+ # with no session ids; point this at a shared location so
42
+ # the board finds the registry wherever it was started.
43
+ # Absent = the default, so a single-checkout project is
44
+ # unaffected.
30
45
  # Agent-runner keys (optional; Plot hardcodes no agent tooling, Principle 5):
31
46
  # Worker command how /plot-dispatch runs an agent headless on a worktree.
32
47
  # `none` = asked, and this repo starts workers by hand —
package/plot-plan-meta.sh CHANGED
@@ -128,6 +128,18 @@
128
128
  # is a note to a reviewer rather than a release note. Comment
129
129
  # interiors stay non-content, as everywhere else in this parser,
130
130
  # so the template's guidance block contributes nothing.
131
+ # long_wave_names wave names too long to be labels — a report, not a refusal.
132
+ # A wave name is a label (`Shaped`, `Gated`, `Offered first`);
133
+ # a sentence-length heading is a plan-authoring mistake the
134
+ # board can only render badly. Names past a threshold judgement
135
+ # (LONG_WAVE_NAME_MAX, set from the estate's longest legitimate
136
+ # name) are listed here in document order, so the reconcile
137
+ # sweep can surface them for a fix. Reads the SAME wave names
138
+ # waves[] carries — never a second scan of the file — so both
139
+ # the ## Branches and ## Waves spellings are covered. [] when
140
+ # every wave name is a label; ALWAYS present, so a consumer
141
+ # never reads undefined. The plan still parses in full: waves[]
142
+ # is unchanged and no name is shortened or dropped.
131
143
  # issues tracker issue numbers this plan answers, from the `## Status`
132
144
  # `Issue:` line or front matter `issue:` (sorted, unique).
133
145
  # A DEDICATED field, never a scan of the body for `#NNN`: a
@@ -169,15 +181,18 @@
169
181
  # canonical entries and the front-matter entry merge
170
182
  # (canonical first, front matter appended)
171
183
  # rounds how many rounds of /plot:challenge-the-plan the plan has been
172
- # through, read from the `CHALLENGE-THE-PLAN-METADATA` block
173
- # the skill writes. **OMITTED ENTIRELY when the plan carries no
174
- # such block** absent is not zero. A plan nobody has
175
- # interrogated and a plan interrogated to no effect want
176
- # opposite reactions from a reader, so the field is missing
177
- # rather than 0, the same rule `claimed`/`eligible` follow on
178
- # the board side. A malformed or truncated block is reported
179
- # the same way: the round is simply absent, and every other
180
- # field still parses.
184
+ # through, read from `## Status` `Rounds:` first, YAML front
185
+ # matter `rounds:` second, and the `CHALLENGE-THE-PLAN-METADATA`
186
+ # block last. The field wins a disagreement (the file states
187
+ # what the reader sees), and the block remains readable so the
188
+ # 40 plans carrying only a block go on reporting correctly.
189
+ # **OMITTED ENTIRELY when no source carries a readable round**
190
+ # absent is not zero. A plan nobody has interrogated and a
191
+ # plan interrogated to no effect want opposite reactions from a
192
+ # reader, so the field is missing rather than 0, the same rule
193
+ # `claimed`/`eligible` follow on the board side. A malformed or
194
+ # truncated block is reported the same way: the round is simply
195
+ # absent, and every other field still parses.
181
196
  #
182
197
  # title/sprint/story/assignee are the board-facing surface (`@plot-pm/board`
183
198
  # consumes this script instead of parsing plans itself). Front matter wins over
@@ -204,7 +219,7 @@ if [ ${#files[@]} -eq 0 ] && [ ${#missing[@]} -eq 0 ]; then
204
219
  fi
205
220
 
206
221
  for f in ${missing[@]+"${missing[@]}"}; do
207
- printf '{"file":"%s","format":"none","error":"file not found","phase_raw":"","phase":"NONE","phase_alt_raw":"","phase_alt":"NONE","type":"","title":"","sprint":"","story":"","assignee":"","branches":[],"prs":[],"issues":[],"malformed_prs":[],"changelog":[],"review_raw":"","review":"NONE","impl_raw":"","impl":"NONE","design_raw":"","approved_raw":"","released_raw":"","delivered_raw":"","started_raw":[]}\n' \
222
+ printf '{"file":"%s","format":"none","error":"file not found","phase_raw":"","phase":"NONE","phase_alt_raw":"","phase_alt":"NONE","type":"","title":"","sprint":"","story":"","assignee":"","branches":[],"prs":[],"issues":[],"malformed_prs":[],"changelog":[],"long_wave_names":[],"review_raw":"","review":"NONE","impl_raw":"","impl":"NONE","design_raw":"","approved_raw":"","released_raw":"","delivered_raw":"","started_raw":[]}\n' \
208
223
  "$(printf '%s' "$f" | sed 's/\\/\\\\/g; s/"/\\"/g')"
209
224
  done
210
225
 
@@ -278,15 +293,17 @@ function reset_state() {
278
293
  fm_title = ""; fm_sprint = ""; fm_story = ""; fm_assignee = ""
279
294
  fm_review = ""; fm_impl = ""; fm_approved = ""; fm_started = ""; fm_released = ""
280
295
  fm_delivered = ""; fm_design = ""
296
+ fm_rounds = ""
281
297
  canon_phase = ""; canon_type = ""
282
298
  canon_sprint = ""; canon_story = ""; canon_assignee = ""
283
299
  canon_review = ""; canon_impl = ""; canon_approved = ""; canon_released = ""
284
300
  canon_delivered = ""; canon_design = ""
301
+ canon_rounds = ""
285
302
  h1_title = ""
286
303
  # "" means the plan carries no readable round — NOT zero. Emitted by omitting
287
304
  # the field, so a consumer cannot mistake "never interrogated" for "asked
288
305
  # nothing".
289
- rounds = ""
306
+ block_rounds = ""
290
307
  in_fm = 0; section = ""; in_comment = 0; in_challenge = 0; in_fence = 0; branches_seen = 0; waves_seen = 0
291
308
  delete branches; n_branches = 0
292
309
  delete prs; n_prs = 0
@@ -399,6 +416,27 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
399
416
  out = out "]}"
400
417
  }
401
418
  out = out "]"
419
+ # long_wave_names[]: wave names past LONG_WAVE_NAME_MAX, in document order. A
420
+ # wave name is a label (`Shaped`, `Gated`, `Offered first`); a sentence-length
421
+ # heading is a plan-authoring mistake the board can only render badly. This
422
+ # REPORTS the offenders — a top-level field, never a change to waves[], so the
423
+ # plan still parses and every existing consumer of waves[] is untouched. The
424
+ # threshold is a JUDGEMENT baked into one constant (see LONG_WAVE_NAME_MAX):
425
+ # the longest legitimate name in the estate is `Offered first` at 13, the
426
+ # offender that motivated this is 53. Measured over the whole array so BOTH
427
+ # spellings (## Branches and ## Waves) are covered from one place. length()
428
+ # here is the awk byte length, which can exceed the character count for a name
429
+ # with a multi-byte dash — harmless: the threshold clears every real name by
430
+ # tens of bytes either way, so the verdict never turns on the last byte.
431
+ out = out ",\"long_wave_names\":["
432
+ lwn = 0
433
+ for (w = 1; w <= n_waves; w++) {
434
+ if (length(wave_names[w]) > LONG_WAVE_NAME_MAX) {
435
+ out = out (lwn > 0 ? "," : "") "\"" jesc(wave_names[w]) "\""
436
+ lwn++
437
+ }
438
+ }
439
+ out = out "]"
402
440
  out = out ",\"review_raw\":\"" jesc(review) "\",\"review\":\"" norm_review(review) "\""
403
441
  out = out ",\"impl_raw\":\"" jesc(impl) "\",\"impl\":\"" norm_impl(impl) "\""
404
442
  out = out ",\"design_raw\":\"" jesc(design) "\""
@@ -408,12 +446,24 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
408
446
  out = out ",\"started_raw\":["
409
447
  for (i = 1; i <= n_started; i++) out = out (i > 1 ? "," : "") "\"" jesc(started[i]) "\""
410
448
  out = out "]"
411
- # OMITTED, not zeroed, when no block was found. The absence is the message.
449
+ # Rounds: preference order: ## Status, front matter, CHALLENGE block fallback.
450
+ # A placeholder ("<!-- optional -->") counts as absent, like Sprint/Story.
451
+ # OMITTED, not zeroed, when no source carries a readable round.
452
+ rounds = ""
453
+ _r = strip_placeholder(canon_rounds)
454
+ if (_r == "") _r = strip_placeholder(fm_rounds)
455
+ if (_r == "") _r = block_rounds
456
+ if (_r != "" && _r ~ /^[0-9]+$/) rounds = _r + 0
412
457
  if (rounds != "") out = out ",\"rounds\":" rounds
413
458
  out = out "}"
414
459
  print out
415
460
  }
416
- BEGIN { branch_re = "`(" PREFIXES ")/[^`]+`" }
461
+ # The longest wave name that still reads as a label, not prose. A JUDGEMENT, not
462
+ # a measurement: the longest legitimate name in the estate is `Offered first`
463
+ # (13), and the offender this exists to catch is a 53-character sentence, so the
464
+ # line sits well clear of both. Reported, never enforced — a name past it makes
465
+ # `long_wave_names`, and nothing refuses the plan.
466
+ BEGIN { branch_re = "`(" PREFIXES ")/[^`]+`"; LONG_WAVE_NAME_MAX = 40 }
417
467
  FNR == 1 {
418
468
  if (NR > 1) emit_record()
419
469
  reset_state()
@@ -442,6 +492,7 @@ in_fm {
442
492
  # here: front matter in this repo is a flat key/value surface, and guessing a
443
493
  # list grammar the format never promised would invent a contract.
444
494
  else if (lower ~ /^changelog:/ && fm_changelog == "") fm_changelog = val_after_colon($0)
495
+ else if (lower ~ /^rounds:/ && fm_rounds == "") fm_rounds = val_after_colon($0)
445
496
  next
446
497
  }
447
498
  # Interior of multi-line HTML comments is non-content (template guidance
@@ -454,11 +505,11 @@ in_fm {
454
505
  # block (question history, category coverage) stays non-content — the script
455
506
  # collects, the skill interprets.
456
507
  in_comment {
457
- if (in_challenge && rounds == "" && $0 ~ /^[ \t]*"round"[ \t]*:[ \t]*[0-9]+/) {
508
+ if (in_challenge && block_rounds == "" && $0 ~ /^[ \t]*"round"[ \t]*:[ \t]*[0-9]+/) {
458
509
  _r = $0
459
510
  sub(/^[ \t]*"round"[ \t]*:[ \t]*/, "", _r)
460
511
  sub(/[^0-9].*$/, "", _r)
461
- if (_r != "") rounds = _r + 0
512
+ if (_r != "") block_rounds = _r + 0
462
513
  }
463
514
  if ($0 ~ /-->/) { in_comment = 0; in_challenge = 0 }
464
515
  next
@@ -518,6 +569,7 @@ section == "status" {
518
569
  else if (lower ~ /^[ \t]*[-*]?[ \t]*\**review[:*]/ && canon_review == "") canon_review = val_after_colon($0)
519
570
  else if (lower ~ /^[ \t]*[-*]?[ \t]*\**impl[:*]/ && canon_impl == "") canon_impl = val_after_colon($0)
520
571
  else if (lower ~ /^[ \t]*[-*]?[ \t]*\**design[:*]/ && canon_design == "") canon_design = val_after_colon($0)
572
+ else if (lower ~ /^[ \t]*[-*]?[ \t]*\**rounds[:*]/ && canon_rounds == "") canon_rounds = val_after_colon($0)
521
573
  else if (lower ~ /^[ \t]*[-*]?[ \t]*\**approved[:*]/ && canon_approved == "") canon_approved = val_after_colon($0)
522
574
  else if (lower ~ /^[ \t]*[-*]?[ \t]*\**released[:*]/ && canon_released == "") canon_released = val_after_colon($0)
523
575
  else if (lower ~ /^[ \t]*[-*]?[ \t]*\**delivered[:*]/ && canon_delivered == "") canon_delivered = val_after_colon($0)