@plot-pm/board 0.11.0 → 0.13.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 +101 -101
- package/package.json +5 -1
- package/plot-agent-monitor.sh +37 -13
- package/plot-approve.sh +65 -30
- package/plot-build-monitor.sh +435 -0
- package/plot-config.sh +8 -1
- package/plot-default-branch.sh +109 -0
- package/plot-deliver.sh +47 -27
- package/plot-dispatch.sh +546 -10
- package/plot-fleet-scan.sh +498 -175
- package/plot-host.sh +255 -26
- package/plot-monitor-subject.sh +194 -0
- package/plot-plan-meta.sh +111 -7
- package/plot-pr-merged.sh +180 -0
- package/plot-reap.sh +45 -5
- package/plot-release-refs.sh +188 -46
- package/plot-resolve-artifact.sh +115 -22
- package/plot-worker-state.sh +94 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plot-pm/board",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -23,14 +23,18 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"dist/board-server.mjs",
|
|
25
25
|
"plot-agent-monitor.sh",
|
|
26
|
+
"plot-build-monitor.sh",
|
|
27
|
+
"plot-monitor-subject.sh",
|
|
26
28
|
"plot-approve.sh",
|
|
27
29
|
"plot-budget.sh",
|
|
28
30
|
"plot-config.sh",
|
|
31
|
+
"plot-default-branch.sh",
|
|
29
32
|
"plot-deliver.sh",
|
|
30
33
|
"plot-dispatch.sh",
|
|
31
34
|
"plot-fleet-scan.sh",
|
|
32
35
|
"plot-host.sh",
|
|
33
36
|
"plot-plan-meta.sh",
|
|
37
|
+
"plot-pr-merged.sh",
|
|
34
38
|
"plot-reap.sh",
|
|
35
39
|
"plot-release-refs.sh",
|
|
36
40
|
"plot-resolve-artifact.sh",
|
package/plot-agent-monitor.sh
CHANGED
|
@@ -227,21 +227,45 @@ publish() { # $1=finding $2=evidence $3=since
|
|
|
227
227
|
# helpers throw away: `gh` absent, unauthed, or failing is `unaskable`, and an
|
|
228
228
|
# unaskable host produces NO finding rather than a wrong one. A failure to
|
|
229
229
|
# observe is not evidence of something to see.
|
|
230
|
+
#
|
|
231
|
+
# THE HOST IS ASKED THROUGH `plot-host.sh`, never `gh` directly. This port
|
|
232
|
+
# called `gh pr list` until 2026-09-05, so the monitor could see a PR on GitHub
|
|
233
|
+
# and nowhere else: on Bitbucket `command -v gh` failed and every branch on the
|
|
234
|
+
# estate read `unaskable` — the refusal was right about the wrong thing, since
|
|
235
|
+
# the host was reachable and simply not GitHub. `pr-state` answers on both.
|
|
236
|
+
#
|
|
237
|
+
# THE ADAPTER'S TWO OUTCOMES ARE ALREADY THIS PORT'S TWO. `pr-state` exits 0
|
|
238
|
+
# with `state:"NONE"` when the host answered and there is no PR, and non-zero
|
|
239
|
+
# when the call itself failed — which is exactly the line between `no PR` and
|
|
240
|
+
# `unaskable` that the `gh` version drew with `|| return 2`. So the split is
|
|
241
|
+
# read from the exit status and the payload together, and neither answer moved.
|
|
230
242
|
monitor_pr_state() { # → 0 has a PR | 1 no PR | 2 unaskable
|
|
231
243
|
[ -n "$branch" ] || return 2
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
#
|
|
236
|
-
#
|
|
237
|
-
|
|
238
|
-
#
|
|
239
|
-
|
|
240
|
-
#
|
|
241
|
-
#
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
244
|
+
local host_script out state
|
|
245
|
+
host_script="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/plot-host.sh"
|
|
246
|
+
[ -r "$host_script" ] || return 2
|
|
247
|
+
# `</dev/null` so a host CLI that decides to prompt cannot hang a monitor
|
|
248
|
+
# whose whole contract is one bounded reading per pass.
|
|
249
|
+
out=$(bash "$host_script" pr-state "$branch" </dev/null 2>/dev/null) || return 2
|
|
250
|
+
# THE EXIT STATUS DECIDES WHETHER THE HOST WAS ASKED; the payload only says
|
|
251
|
+
# what it answered. `pr-state` exits non-zero when the call failed and 0 when
|
|
252
|
+
# it did not, so anything reaching this line is an answer.
|
|
253
|
+
#
|
|
254
|
+
# A MISS IS `NONE`, AND SO IS A NULL. `state:"NONE"` is the adapter's word for
|
|
255
|
+
# "no PR found", but the same miss reaches here as `state:null` when the host
|
|
256
|
+
# CLI answers with a payload carrying no PR — measured 2026-09-05 against
|
|
257
|
+
# `test/e2e/agent-monitor-reads.test.mjs`, where `gh pr view` on a branch with
|
|
258
|
+
# no PR produced `{"number":null,"state":null,…}` and exit 0.
|
|
259
|
+
#
|
|
260
|
+
# Reading that null as `unaskable` is what the first draft of this port did,
|
|
261
|
+
# and it is wrong in the direction that hides the finding: an agent that
|
|
262
|
+
# committed and opened nothing produced no `owes a review` at all, because the
|
|
263
|
+
# monitor believed it had never measured. A failure to observe is not evidence
|
|
264
|
+
# of something to see — but neither is an observation evidence of a failure.
|
|
265
|
+
state=$(printf '%s' "$out" | jq -r '.state // "NONE"' 2>/dev/null) || return 2
|
|
266
|
+
case "$state" in
|
|
267
|
+
NONE|null|"") return 1 ;;
|
|
268
|
+
*) return 0 ;;
|
|
245
269
|
esac
|
|
246
270
|
}
|
|
247
271
|
|
package/plot-approve.sh
CHANGED
|
@@ -116,7 +116,7 @@ done
|
|
|
116
116
|
die() { echo "plot-approve: $*" >&2; exit 1; }
|
|
117
117
|
|
|
118
118
|
[ -n "$slug" ] || die "need a plan slug (usage: plot-approve.sh [--dry-run] <slug>)"
|
|
119
|
-
git rev-parse --git-dir >/dev/null 2>&1 || die "not a git repository"
|
|
119
|
+
git rev-parse --git-dir >/dev/null 2>&1 || die "not a git repository — run this from inside the checkout, or 'git init' one here"
|
|
120
120
|
|
|
121
121
|
cfg() { bash "$script_dir/plot-config.sh" get "$1" "$2"; }
|
|
122
122
|
|
|
@@ -135,10 +135,14 @@ plan_file=""
|
|
|
135
135
|
for cand in "$ACTIVE_DIR$slug.md" "$PLAN_DIR"*"$slug".md; do
|
|
136
136
|
[ -e "$cand" ] && { plan_file="$cand"; break; }
|
|
137
137
|
done
|
|
138
|
-
[ -n "$plan_file" ] || die "no plan found for '$slug' — looked in $ACTIVE_DIR and $PLAN_DIR
|
|
138
|
+
[ -n "$plan_file" ] || die "no plan found for '$slug' — looked in $ACTIVE_DIR and $PLAN_DIR.
|
|
139
|
+
Check the slug: ls $PLAN_DIR | grep -i '$slug'
|
|
140
|
+
Or create the plan first: /plot-idea"
|
|
139
141
|
|
|
140
142
|
meta=$(bash "$script_dir/plot-plan-meta.sh" "$plan_file" 2>/dev/null) || meta=""
|
|
141
|
-
[ -n "$meta" ] || die "cannot parse '$plan_file' — refusing rather than guessing
|
|
143
|
+
[ -n "$meta" ] || die "cannot parse '$plan_file' — refusing rather than guessing.
|
|
144
|
+
See what the parser reads: $script_dir/plot-plan-meta.sh $plan_file
|
|
145
|
+
A plan needs a '## Status' section with a 'State:' field."
|
|
142
146
|
|
|
143
147
|
jfield() { printf '%s' "$meta" | jq -r "$1" 2>/dev/null; }
|
|
144
148
|
|
|
@@ -163,11 +167,14 @@ plan_branches=$(jfield '.branches[]?')
|
|
|
163
167
|
case "$phase" in
|
|
164
168
|
draft|design|approved) ;;
|
|
165
169
|
delivered|released)
|
|
166
|
-
die "plan '$slug' is already $phase — nothing to approve.
|
|
170
|
+
die "plan '$slug' is already $phase — nothing to approve.
|
|
171
|
+
Nothing to do here. To take the work further: /plot-release" ;;
|
|
167
172
|
NONE|"")
|
|
168
|
-
die "cannot read the phase of '$slug' ($plan_file) — refusing rather than guessing.
|
|
173
|
+
die "cannot read the phase of '$slug' ($plan_file) — refusing rather than guessing.
|
|
174
|
+
Its '## Status' section needs a line reading '- **State:** Draft'." ;;
|
|
169
175
|
*)
|
|
170
|
-
die "plan '$slug' is in phase '$phase' — only a Draft or Design plan can be approved.
|
|
176
|
+
die "plan '$slug' is in phase '$phase' — only a Draft or Design plan can be approved.
|
|
177
|
+
If that phase is wrong, correct the 'State:' line in $plan_file and push it." ;;
|
|
171
178
|
esac
|
|
172
179
|
|
|
173
180
|
# --- refusal 2: the review channel ------------------------------------------
|
|
@@ -225,9 +232,12 @@ case "$pr_state" in
|
|
|
225
232
|
MERGED) ;;
|
|
226
233
|
OPEN) ;;
|
|
227
234
|
CLOSED)
|
|
228
|
-
die "the plan PR for '$slug' (#$pr_number) is closed.
|
|
235
|
+
die "the plan PR for '$slug' (#$pr_number) is closed.
|
|
236
|
+
Reopen it on the host, or push '$pr_branch' again and open a new one." ;;
|
|
229
237
|
NONE|*)
|
|
230
|
-
die "no PR found for branch '$pr_branch'.
|
|
238
|
+
die "no PR found for branch '$pr_branch'.
|
|
239
|
+
Push the branch: git push -u origin $pr_branch
|
|
240
|
+
Then open its PR — or run /plot-idea, which does both." ;;
|
|
231
241
|
esac
|
|
232
242
|
|
|
233
243
|
echo "step: plan $plan_file — phase=$phase review=${review} impl=${impl} pr=#$pr_number($pr_state)"
|
|
@@ -336,10 +346,14 @@ real_plan_path() { # $1 = plan file as found
|
|
|
336
346
|
}
|
|
337
347
|
|
|
338
348
|
rel=$(cd "$repo_root" && real_plan_path "$plan_file") || rel=""
|
|
339
|
-
[ -n "$rel" ] || die "$plan_file is outside the repository root
|
|
349
|
+
[ -n "$rel" ] || die "$plan_file is outside the repository root ($repo_root).
|
|
350
|
+
Move the plan under $PLAN_DIR inside this checkout and re-run."
|
|
340
351
|
|
|
341
|
-
# Flip `**
|
|
342
|
-
# section only. Both are the pre-Approved
|
|
352
|
+
# Flip `**State:** Draft` OR `**State:** Design` → `Approved` in the `## Status`
|
|
353
|
+
# section only. Both are the pre-Approved states this script advances from.
|
|
354
|
+
#
|
|
355
|
+
# Reads `State:` and `Phase:` alike: it changes the VALUE on whichever line
|
|
356
|
+
# carries it, so a plan written before the 2026-09-07 rename still approves.
|
|
343
357
|
#
|
|
344
358
|
# Scoped to that section because a plan that QUOTES a status block in its prose
|
|
345
359
|
# (this repo has several, documenting the format) would otherwise have its
|
|
@@ -353,7 +367,7 @@ flip_phase() { # $1=in $2=out → 0 if it changed the file, 1 if there was noth
|
|
|
353
367
|
awk '
|
|
354
368
|
BEGIN { section = ""; done = 0 }
|
|
355
369
|
/^## / { section = ($0 ~ /^## Status/) ? "status" : ""; print; next }
|
|
356
|
-
section == "status" && !done && tolower($0) ~ /^[ \t]*[-*]?[ \t]*\**phase[:*]/ {
|
|
370
|
+
section == "status" && !done && tolower($0) ~ /^[ \t]*[-*]?[ \t]*\**(state|phase)[:*]/ {
|
|
357
371
|
if (tolower($0) ~ /draft/) {
|
|
358
372
|
sub(/[Dd]raft/, "Approved")
|
|
359
373
|
done = 1
|
|
@@ -555,13 +569,20 @@ clear_holds() { # $1=worktree root; reads $plan_branches
|
|
|
555
569
|
}
|
|
556
570
|
|
|
557
571
|
# Update the sprint item annotation this plan appears in:
|
|
558
|
-
# - [ ] [slug] description <!-- pr: #N,
|
|
559
|
-
# /plot-sprint READS these (`pr`, `
|
|
560
|
-
#
|
|
561
|
-
#
|
|
572
|
+
# - [ ] [slug] description <!-- pr: #N, branch: feature/slug -->
|
|
573
|
+
# /plot-sprint READS these (`pr`, `branch`) and /plot-approve writes them, so an
|
|
574
|
+
# approval that skips this makes `/plot-sprint status` wrong rather than merely
|
|
575
|
+
# incomplete.
|
|
576
|
+
#
|
|
577
|
+
# `status:` IS GONE, since 2026-09-08. `a-withdrawn-item-is-not-open` measured
|
|
578
|
+
# it dead in both directions — 67 lines carried one, no reader acted on the
|
|
579
|
+
# value, and `plot-sprint-release.sh` read the field nowhere. The plan file
|
|
580
|
+
# carries `State:` and a dated `Approved:` record; a cache nobody refreshes and
|
|
581
|
+
# nobody reads is a second answer waiting to contradict the first. `pr` and
|
|
582
|
+
# `branch` stay: they name things no plan field holds.
|
|
562
583
|
#
|
|
563
584
|
# A plan in NO sprint is a no-op, never an error — that is the common case.
|
|
564
|
-
# Already-done test: the annotation already carries
|
|
585
|
+
# Already-done test: the annotation already carries this PR and branch.
|
|
565
586
|
update_sprint_annotation() { # $1=worktree root → prints none|updated|already|missing
|
|
566
587
|
local root="$1" f found=""
|
|
567
588
|
[ -n "$sprint" ] || { printf 'none'; return 0; }
|
|
@@ -586,18 +607,27 @@ update_sprint_annotation() { # $1=worktree root → prints none|updated|already|
|
|
|
586
607
|
{
|
|
587
608
|
line = $0
|
|
588
609
|
if (index(line, "<!--") == 0) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
610
|
+
print line " <!-- pr: #" pr (br != "" ? ", branch: " br : "") " -->"
|
|
611
|
+
next
|
|
612
|
+
}
|
|
613
|
+
# THE CLOSING MARKER IS SPLIT OFF BEFORE ANY FIELD IS TOUCHED.
|
|
614
|
+
# A value pattern that has to avoid `-->` cannot be written safely:
|
|
615
|
+
# `[^,>]*[^,> \t]` excluded the `>` and still swallowed the `--`, turning
|
|
616
|
+
# ` -->` into a bare `>` and corrupting the comment — measured 2026-09-08,
|
|
617
|
+
# `approve: updates the sprint annotation the sprint view reads` failed on
|
|
618
|
+
# exactly that. Excluding `-` as well is worse: it rewrites `bug/a-b` as
|
|
619
|
+
# `feature/new-b`, and hyphens are in most branch names here.
|
|
620
|
+
# With the marker held aside, every field ends at a comma or at
|
|
621
|
+
# end-of-string, and none can reach it.
|
|
622
|
+
tail = ""
|
|
623
|
+
if (match(line, /[ \t]*-->[ \t]*$/)) { tail = substr(line, RSTART); line = substr(line, 1, RSTART - 1) }
|
|
624
|
+
if (line ~ /pr:[ \t]*#?[0-9a-z]+/) sub(/pr:[ \t]*#?[0-9a-z]+/, "pr: #" pr, line)
|
|
625
|
+
else sub(/<!--/, "<!-- pr: #" pr ",", line)
|
|
626
|
+
if (br != "") {
|
|
627
|
+
if (line ~ /branch:[ \t]*[^,]/) sub(/branch:[ \t]*[^,]*/, "branch: " br, line)
|
|
628
|
+
else line = line ", branch: " br
|
|
599
629
|
}
|
|
600
|
-
print line
|
|
630
|
+
print line (tail != "" ? tail : " -->")
|
|
601
631
|
}
|
|
602
632
|
' "$found")
|
|
603
633
|
if [ "$before" = "$after" ]; then printf 'already'; return 0; fi
|
|
@@ -656,7 +686,9 @@ if [ "$same_branch" = 1 ]; then
|
|
|
656
686
|
push_report="nothing-to-commit"
|
|
657
687
|
echo "step: nothing to commit — the approval was already recorded"
|
|
658
688
|
else
|
|
659
|
-
git -C "$repo_root" commit -q -m "plot: approve $slug" || die "could not commit the approval
|
|
689
|
+
git -C "$repo_root" commit -q -m "plot: approve $slug" || die "could not commit the approval.
|
|
690
|
+
The PR is already merged; the local record is what is missing. See what git
|
|
691
|
+
refused: git -C $repo_root status. Then re-run this — it is idempotent."
|
|
660
692
|
push_report="local"
|
|
661
693
|
echo "step: recorded on $(git -C "$repo_root" branch --show-current) — push it with the implementation"
|
|
662
694
|
fi
|
|
@@ -672,7 +704,10 @@ else
|
|
|
672
704
|
# -B: a leftover branch from an earlier failed run must not block this one.
|
|
673
705
|
# It is disposable by construction — created here, pushed, deleted.
|
|
674
706
|
git worktree add -q -B "$bookbr" "$tmpwt" "origin/$MAIN" 2>/dev/null \
|
|
675
|
-
|| die "could not prepare a booking worktree at $tmpwt
|
|
707
|
+
|| die "could not prepare a booking worktree at $tmpwt.
|
|
708
|
+
Most often origin/$MAIN is not fetched, or '$bookbr' is checked out in
|
|
709
|
+
another worktree. Check both: git fetch origin $MAIN && git worktree list
|
|
710
|
+
Nothing has been written locally; the plan is untouched."
|
|
676
711
|
|
|
677
712
|
cleanup() {
|
|
678
713
|
git worktree remove --force "$tmpwt" >/dev/null 2>&1 || true
|