@plot-pm/board 0.8.1 → 0.9.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 +120 -111
- package/package.json +1 -1
- package/plot-config.sh +14 -0
- package/plot-plan-meta.sh +133 -16
package/package.json
CHANGED
package/plot-config.sh
CHANGED
|
@@ -27,6 +27,12 @@
|
|
|
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
|
+
# Worktree root where /plot-dispatch puts its worktrees. Read by
|
|
31
|
+
# plot-dispatch.sh; default is the repo's PARENT, which
|
|
32
|
+
# scatters `plot-wt-*` beside the checkout. An absolute
|
|
33
|
+
# path is taken as given; a relative one resolves against
|
|
34
|
+
# the repo root, so `.worktrees` gathers them inside it.
|
|
35
|
+
# The default is kept for repos that never set it.
|
|
30
36
|
# Worker bound seconds a single prompt run may take in the worker loop
|
|
31
37
|
# before it is ended and the worker exits (no hop). Read by
|
|
32
38
|
# plot-worker-loop.sh; default 3600 (~1h), `0` disables it.
|
|
@@ -42,6 +48,14 @@
|
|
|
42
48
|
# the board finds the registry wherever it was started.
|
|
43
49
|
# Absent = the default, so a single-checkout project is
|
|
44
50
|
# unaffected.
|
|
51
|
+
# Worktree root where /plot-dispatch creates fleet worktrees. A relative
|
|
52
|
+
# value resolves against the repo root, an absolute one is
|
|
53
|
+
# taken as given. Absent = the default `repo_root/..` with
|
|
54
|
+
# the `plot-wt-` prefix — today's behaviour, so no existing
|
|
55
|
+
# checkout moves. Under a dedicated root the prefix is
|
|
56
|
+
# dropped: the directory already says these are Plot's. Read
|
|
57
|
+
# only by the CREATION path; every "which worktree holds this
|
|
58
|
+
# branch" read asks `git worktree list` instead.
|
|
45
59
|
# Agent-runner keys (optional; Plot hardcodes no agent tooling, Principle 5):
|
|
46
60
|
# Worker command how /plot-dispatch runs an agent headless on a worktree.
|
|
47
61
|
# `none` = asked, and this repo starts workers by hand —
|
package/plot-plan-meta.sh
CHANGED
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
# the old shape invited (a second path-shaped token on a line read as a phantom
|
|
58
58
|
# branch) is structurally impossible.
|
|
59
59
|
#
|
|
60
|
+
# The old shape is now closed against the same defect from the other side: a
|
|
61
|
+
# claim must be a LIST ITEM that starts with the backticked name, so a branch
|
|
62
|
+
# cited in prose, a blockquote or an HTML comment under `## Branches` is read as
|
|
63
|
+
# the citation it is. See the anchor at `branch_claim_re` below.
|
|
64
|
+
#
|
|
60
65
|
# Phase values are normalized by scanning whitespace-separated tokens for the
|
|
61
66
|
# first known phase word — so decorated real-world values like
|
|
62
67
|
# "Delivered (2026-06-29) — split done" normalize to "delivered". A non-empty
|
|
@@ -88,9 +93,14 @@
|
|
|
88
93
|
# assignee github handle from the `## Approval` `Assignee:` line or
|
|
89
94
|
# front matter `assignee:`; "" if absent
|
|
90
95
|
# branches branch names, sorted and unique, read from EITHER spelling:
|
|
91
|
-
# the old `## Branches` section (
|
|
92
|
-
#
|
|
93
|
-
# section (`Branch:` in a `### ` heading —
|
|
96
|
+
# the old `## Branches` section (a LIST ITEM whose first token
|
|
97
|
+
# is the backtick-quoted name, matching the known prefixes) OR
|
|
98
|
+
# the new `## Waves` section (`Branch:` in a `### ` heading —
|
|
99
|
+
# see below). A backticked branch name anywhere else under
|
|
100
|
+
# `## Branches` — mid-sentence, in a blockquote, in a comment,
|
|
101
|
+
# on a wrapped continuation line — is a CITATION and claims
|
|
102
|
+
# nothing: plans name each other branches to declare
|
|
103
|
+
# dependencies, and doing so must not claim them. Both
|
|
94
104
|
# spellings emit the same array; a plan carries one or the
|
|
95
105
|
# other, and the parser reads both so a migration that moves
|
|
96
106
|
# files one at a time never makes a plan silently empty.
|
|
@@ -201,11 +211,18 @@
|
|
|
201
211
|
set -uo pipefail
|
|
202
212
|
|
|
203
213
|
prefixes='idea|feature|bug|docs|infra'
|
|
214
|
+
tracker_override='' # set by --tracker; overrides the config read below
|
|
215
|
+
tracker_override_set=0
|
|
204
216
|
files=()
|
|
205
217
|
missing=()
|
|
206
218
|
while [ $# -gt 0 ]; do
|
|
207
219
|
case "$1" in
|
|
208
220
|
--prefixes) prefixes="${2:?--prefixes needs a value}"; shift 2 ;;
|
|
221
|
+
# --tracker names the tracker directly, bypassing plot-config.sh. It exists
|
|
222
|
+
# for the contract tests (which parse fixtures outside any repo whose Plot
|
|
223
|
+
# Config could name a tracker) and for a caller that has already resolved
|
|
224
|
+
# the value once. An empty value means "GitHub", the same as no config.
|
|
225
|
+
--tracker) tracker_override="${2-}"; tracker_override_set=1; shift 2 ;;
|
|
209
226
|
-*) echo "plot-plan-meta: unknown flag: $1" >&2; shift ;;
|
|
210
227
|
*)
|
|
211
228
|
if [ -f "$1" ]; then files+=("$1"); else missing+=("$1"); fi
|
|
@@ -225,7 +242,33 @@ done
|
|
|
225
242
|
|
|
226
243
|
[ ${#files[@]} -gt 0 ] || exit 0
|
|
227
244
|
|
|
228
|
-
|
|
245
|
+
# Read the tracker config to determine whether tracker-key issue references
|
|
246
|
+
# (`PROJ-123`) are parsed in addition to GitHub's `#N`.
|
|
247
|
+
#
|
|
248
|
+
# THIS IS THE FIRST CONFIGURATION DEPENDENCY THIS SCRIPT HAS — keep it narrow:
|
|
249
|
+
# read ONE key, an unreadable or missing config means GitHub (today's
|
|
250
|
+
# behaviour), and NEVER fail a parse for want of configuration. plot-config.sh
|
|
251
|
+
# exits 0 for all cases and prints an empty string when the key is absent, so a
|
|
252
|
+
# repo with no `## Plot Config` at all still parses exactly as it does today.
|
|
253
|
+
if [ "$tracker_override_set" -eq 1 ]; then
|
|
254
|
+
tracker="$tracker_override"
|
|
255
|
+
else
|
|
256
|
+
script_dir="$(dirname "${BASH_SOURCE[0]}")"
|
|
257
|
+
tracker=$("$script_dir/plot-config.sh" get Tracker 2>/dev/null || true)
|
|
258
|
+
fi
|
|
259
|
+
# The value may carry a URL after the scheme (`jira https://…`), so match the
|
|
260
|
+
# FIRST token, lowercased. Only a tracker whose keys are `LETTERS-digits` —
|
|
261
|
+
# jira, linear — enables the key form; github, github-issues, plot, and absent
|
|
262
|
+
# all keep `#N`-only, unchanged. An unrecognized token stays GitHub too: a
|
|
263
|
+
# guess here would let `WONT-FIX` masquerade as an issue reference and hide a
|
|
264
|
+
# real ticket, which the plan interrogation explicitly rejected.
|
|
265
|
+
tracker_scheme=$(printf '%s' "$tracker" | tr '[:upper:]' '[:lower:]' | awk '{print $1}')
|
|
266
|
+
case "$tracker_scheme" in
|
|
267
|
+
jira|linear) parse_key_issues=1 ;;
|
|
268
|
+
*) parse_key_issues=0 ;;
|
|
269
|
+
esac
|
|
270
|
+
|
|
271
|
+
awk -v PREFIXES="$prefixes" -v PARSE_KEY_ISSUES="$parse_key_issues" '
|
|
229
272
|
function jesc(s) {
|
|
230
273
|
gsub(/\\/, "\\\\", s); gsub(/"/, "\\\"", s); gsub(/\t/, "\\t", s)
|
|
231
274
|
return s
|
|
@@ -316,7 +359,7 @@ function reset_state() {
|
|
|
316
359
|
fm_changelog = ""
|
|
317
360
|
delete changelog; n_changelog = 0; changelog_seen = 0; cl_open = 0
|
|
318
361
|
}
|
|
319
|
-
function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assignee, review, impl, design, approved, delivered, issue, i, j, out, sorted_b, sorted_p, sorted_i, nb, np, ni) {
|
|
362
|
+
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) {
|
|
320
363
|
if (fm_status != "" || fm_phase != "") {
|
|
321
364
|
fmt = "frontmatter"
|
|
322
365
|
praw = (fm_status != "") ? fm_status : fm_phase
|
|
@@ -350,10 +393,27 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
350
393
|
# A LIST, because one plan can answer several signals; the plan that
|
|
351
394
|
# introduced this field subsumes three.
|
|
352
395
|
issue = strip_placeholder((fm_issue != "") ? fm_issue : canon_issue)
|
|
396
|
+
# GitHub-style `#N` is parsed everywhere, regardless of tracker.
|
|
353
397
|
while (match(issue, /#[0-9]+/)) {
|
|
354
398
|
issues[++n_issues] = substr(issue, RSTART + 1, RLENGTH - 1)
|
|
355
399
|
issue = substr(issue, RSTART + RLENGTH)
|
|
356
400
|
}
|
|
401
|
+
# Jira-style `PROJ-123` is parsed ONLY when Tracker: names a non-GitHub
|
|
402
|
+
# tracker (jira, linear). PARSE_KEY_ISSUES is set by the shell before the awk
|
|
403
|
+
# invocation, based on plot-config.sh reading the Tracker key. Missing config
|
|
404
|
+
# defaults to 0 (GitHub behaviour), so this never fires in a repo with no
|
|
405
|
+
# config — the test in this branch proves that (Done-when item 6).
|
|
406
|
+
#
|
|
407
|
+
# THE PATTERN is `[A-Z]+-[0-9]+` anchored to word boundaries by iterating
|
|
408
|
+
# through the string. A greedy match of the whole field would capture only
|
|
409
|
+
# one; the loop mirrors the `#N` extraction above.
|
|
410
|
+
if (PARSE_KEY_ISSUES == 1) {
|
|
411
|
+
issue2 = strip_placeholder((fm_issue != "") ? fm_issue : canon_issue)
|
|
412
|
+
while (match(issue2, /[A-Z][A-Z0-9]*-[0-9]+/)) {
|
|
413
|
+
issues[++n_issues] = substr(issue2, RSTART, RLENGTH)
|
|
414
|
+
issue2 = substr(issue2, RSTART + RLENGTH)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
357
417
|
# Insertion sort + dedupe (portable: no gawk asort).
|
|
358
418
|
nb = 0
|
|
359
419
|
for (i = 1; i <= n_branches; i++) {
|
|
@@ -369,13 +429,36 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
369
429
|
for (j = np; j >= 1 && sorted_p[j] > prs[i]+0; j--) sorted_p[j+1] = sorted_p[j]
|
|
370
430
|
sorted_p[j+1] = prs[i]+0; np++
|
|
371
431
|
}
|
|
372
|
-
|
|
432
|
+
# Issues: sort numeric (GitHub) issues first, then string (Jira) keys.
|
|
433
|
+
# Separate into two arrays, sort each, then concatenate.
|
|
434
|
+
n_num_i = 0; n_str_i = 0
|
|
373
435
|
for (i = 1; i <= n_issues; i++) {
|
|
374
|
-
|
|
375
|
-
if (
|
|
376
|
-
|
|
377
|
-
|
|
436
|
+
v = issues[i]
|
|
437
|
+
if (v ~ /^[0-9]+$/) {
|
|
438
|
+
# Numeric: check for duplicate, then insert sorted.
|
|
439
|
+
is_dup = 0
|
|
440
|
+
for (j = 1; j <= n_num_i; j++) if (num_issues[j] == v + 0) { is_dup = 1; break }
|
|
441
|
+
if (!is_dup) {
|
|
442
|
+
for (j = n_num_i; j >= 1 && num_issues[j] > v + 0; j--) num_issues[j+1] = num_issues[j]
|
|
443
|
+
num_issues[j+1] = v + 0; n_num_i++
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
# String (Jira key): check for duplicate, then insert sorted.
|
|
447
|
+
is_dup = 0
|
|
448
|
+
for (j = 1; j <= n_str_i; j++) if (str_issues[j] == v) { is_dup = 1; break }
|
|
449
|
+
if (!is_dup) {
|
|
450
|
+
for (j = n_str_i; j >= 1 && str_issues[j] > v; j--) str_issues[j+1] = str_issues[j]
|
|
451
|
+
str_issues[j+1] = v; n_str_i++
|
|
452
|
+
}
|
|
453
|
+
}
|
|
378
454
|
}
|
|
455
|
+
# Concatenate: numeric first, then string (matches the field ordering the
|
|
456
|
+
# board expects — GitHub issues before Jira keys when both are present).
|
|
457
|
+
ni = 0
|
|
458
|
+
for (i = 1; i <= n_num_i; i++) sorted_i[++ni] = num_issues[i]
|
|
459
|
+
for (i = 1; i <= n_str_i; i++) sorted_i[++ni] = str_issues[i]
|
|
460
|
+
delete issue_is_str
|
|
461
|
+
for (i = 1; i <= n_str_i; i++) issue_is_str[str_issues[i]] = 1
|
|
379
462
|
out = "{\"file\":\"" jesc(cur_file) "\",\"format\":\"" fmt "\""
|
|
380
463
|
out = out ",\"phase_raw\":\"" jesc(praw) "\",\"phase\":\"" norm_phase(praw) "\""
|
|
381
464
|
out = out ",\"phase_alt_raw\":\"" jesc(palt_raw) "\",\"phase_alt\":\"" norm_phase(palt_raw) "\""
|
|
@@ -387,7 +470,13 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
387
470
|
out = out "],\"prs\":["
|
|
388
471
|
for (i = 1; i <= np; i++) out = out (i > 1 ? "," : "") sorted_p[i]
|
|
389
472
|
out = out "],\"issues\":["
|
|
390
|
-
for (i = 1; i <= ni; i++)
|
|
473
|
+
for (i = 1; i <= ni; i++) {
|
|
474
|
+
# Numeric issues output as JSON numbers; string issues (Jira keys) as quoted.
|
|
475
|
+
if (sorted_i[i] in issue_is_str)
|
|
476
|
+
out = out (i > 1 ? "," : "") "\"" jesc(sorted_i[i]) "\""
|
|
477
|
+
else
|
|
478
|
+
out = out (i > 1 ? "," : "") sorted_i[i]
|
|
479
|
+
}
|
|
391
480
|
out = out "],\"malformed_prs\":["
|
|
392
481
|
for (i = 1; i <= n_malformed_prs; i++) out = out (i > 1 ? "," : "") "\"" jesc(malformed_prs[i]) "\""
|
|
393
482
|
out = out "]"
|
|
@@ -463,7 +552,26 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
463
552
|
# (13), and the offender this exists to catch is a 53-character sentence, so the
|
|
464
553
|
# line sits well clear of both. Reported, never enforced — a name past it makes
|
|
465
554
|
# `long_wave_names`, and nothing refuses the plan.
|
|
466
|
-
|
|
555
|
+
# A CLAIM IS A LIST ITEM, and the anchor is what says so.
|
|
556
|
+
#
|
|
557
|
+
# This matched a backticked branch name ANYWHERE on a line, so a plan that
|
|
558
|
+
# merely CITED another plan branch under `## Branches` claimed it. Measured on
|
|
559
|
+
# the board 2026-08-23: two branches rendered twice, in two sections, wearing
|
|
560
|
+
# `claimed twice` — and /plot-dispatch would have fanned out a branch the plan
|
|
561
|
+
# does not own. Both second claims were dependency citations, written exactly as
|
|
562
|
+
# a `## Branches` section should write them.
|
|
563
|
+
#
|
|
564
|
+
# Rewording the citations was the old repair. That is a rule an author must
|
|
565
|
+
# remember in the one section where writing branch names is the entire point,
|
|
566
|
+
# and it had already been forgotten twice. Gates over rules: anchoring makes the
|
|
567
|
+
# parser UNABLE to read a citation as a claim.
|
|
568
|
+
#
|
|
569
|
+
# Licensed by a MEASUREMENT, not a preference. Swept across docs/plans/ on
|
|
570
|
+
# 2026-08-27: 259 lines under `## Branches` carry a backticked branch name and
|
|
571
|
+
# all 259 are anchored list items, so the stricter rule drops no real claim. The
|
|
572
|
+
# contract test re-runs that sweep differentially rather than pinning a total —
|
|
573
|
+
# the estate moves weekly, and an absolute number would fail a correct parser.
|
|
574
|
+
BEGIN { branch_claim_re = "^[ \t]*-[ \t]+`(" PREFIXES ")/[^`]+`"; LONG_WAVE_NAME_MAX = 40 }
|
|
467
575
|
FNR == 1 {
|
|
468
576
|
if (NR > 1) emit_record()
|
|
469
577
|
reset_state()
|
|
@@ -667,9 +775,19 @@ section == "branches" {
|
|
|
667
775
|
sub(/[ \t]*-->.*$/, "", _d)
|
|
668
776
|
defer_note = trim(_d)
|
|
669
777
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
778
|
+
# ONE LIST ITEM, AT MOST ONE CLAIM — an `if`, not the `while` this was.
|
|
779
|
+
#
|
|
780
|
+
# The old loop walked the line taking every backticked name on it, which is
|
|
781
|
+
# exactly how a citation became a second claim. Anchoring makes a second match
|
|
782
|
+
# impossible by construction, so the loop is gone rather than left as dead
|
|
783
|
+
# scaffolding that reads like several claims per item are still expected.
|
|
784
|
+
#
|
|
785
|
+
# The match now spans `- ` and the backticks, so the name is cut from the
|
|
786
|
+
# first backtick rather than from RSTART: RSTART lands on the indent.
|
|
787
|
+
if (match($0, branch_claim_re)) {
|
|
788
|
+
claim = substr($0, RSTART, RLENGTH)
|
|
789
|
+
b = substr(claim, index(claim, "`") + 1)
|
|
790
|
+
sub(/`$/, "", b)
|
|
673
791
|
branches[++n_branches] = b
|
|
674
792
|
if (n_waves == 0) { wave_names[++n_waves] = "" }
|
|
675
793
|
wave_of[n_branches] = n_waves
|
|
@@ -691,7 +809,6 @@ section == "branches" {
|
|
|
691
809
|
# is a reflection, not the claim: git refs remain authoritative.
|
|
692
810
|
claimed_of[n_branches] = claim_note
|
|
693
811
|
ordered_b[n_branches] = b
|
|
694
|
-
line = substr(line, RSTART + RLENGTH)
|
|
695
812
|
}
|
|
696
813
|
line = $0
|
|
697
814
|
# `→ #N` and `→ owner/repo#N` are both annotations: /plot-deliver instructs
|