@plot-pm/board 0.12.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 +111 -111
- package/package.json +1 -1
- package/plot-approve.sh +59 -27
- package/plot-deliver.sh +43 -25
- package/plot-dispatch.sh +46 -5
- package/plot-host.sh +139 -13
- package/plot-worker-state.sh +94 -0
package/package.json
CHANGED
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,7 +346,8 @@ 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
352
|
# Flip `**State:** Draft` OR `**State:** Design` → `Approved` in the `## Status`
|
|
342
353
|
# section only. Both are the pre-Approved states this script advances from.
|
|
@@ -558,13 +569,20 @@ clear_holds() { # $1=worktree root; reads $plan_branches
|
|
|
558
569
|
}
|
|
559
570
|
|
|
560
571
|
# Update the sprint item annotation this plan appears in:
|
|
561
|
-
# - [ ] [slug] description <!-- pr: #N,
|
|
562
|
-
# /plot-sprint READS these (`pr`, `
|
|
563
|
-
#
|
|
564
|
-
#
|
|
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.
|
|
565
583
|
#
|
|
566
584
|
# A plan in NO sprint is a no-op, never an error — that is the common case.
|
|
567
|
-
# Already-done test: the annotation already carries
|
|
585
|
+
# Already-done test: the annotation already carries this PR and branch.
|
|
568
586
|
update_sprint_annotation() { # $1=worktree root → prints none|updated|already|missing
|
|
569
587
|
local root="$1" f found=""
|
|
570
588
|
[ -n "$sprint" ] || { printf 'none'; return 0; }
|
|
@@ -589,18 +607,27 @@ update_sprint_annotation() { # $1=worktree root → prints none|updated|already|
|
|
|
589
607
|
{
|
|
590
608
|
line = $0
|
|
591
609
|
if (index(line, "<!--") == 0) {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (line ~ /status:[ \t]*[a-z-]+/) sub(/status:[ \t]*[a-z-]+/, "status: approved", line)
|
|
595
|
-
else sub(/-->/, ", status: approved -->", line)
|
|
596
|
-
if (line ~ /pr:[ \t]*#?[0-9a-z]+/) sub(/pr:[ \t]*#?[0-9a-z]+/, "pr: #" pr, line)
|
|
597
|
-
else sub(/<!--/, "<!-- pr: #" pr ",", line)
|
|
598
|
-
if (br != "") {
|
|
599
|
-
if (line ~ /branch:[ \t]*[^,>]+/) sub(/branch:[ \t]*[^,>]*[^,> \t]/, "branch: " br, line)
|
|
600
|
-
else sub(/-->/, ", branch: " br " -->", line)
|
|
601
|
-
}
|
|
610
|
+
print line " <!-- pr: #" pr (br != "" ? ", branch: " br : "") " -->"
|
|
611
|
+
next
|
|
602
612
|
}
|
|
603
|
-
|
|
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
|
|
629
|
+
}
|
|
630
|
+
print line (tail != "" ? tail : " -->")
|
|
604
631
|
}
|
|
605
632
|
' "$found")
|
|
606
633
|
if [ "$before" = "$after" ]; then printf 'already'; return 0; fi
|
|
@@ -659,7 +686,9 @@ if [ "$same_branch" = 1 ]; then
|
|
|
659
686
|
push_report="nothing-to-commit"
|
|
660
687
|
echo "step: nothing to commit — the approval was already recorded"
|
|
661
688
|
else
|
|
662
|
-
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."
|
|
663
692
|
push_report="local"
|
|
664
693
|
echo "step: recorded on $(git -C "$repo_root" branch --show-current) — push it with the implementation"
|
|
665
694
|
fi
|
|
@@ -675,7 +704,10 @@ else
|
|
|
675
704
|
# -B: a leftover branch from an earlier failed run must not block this one.
|
|
676
705
|
# It is disposable by construction — created here, pushed, deleted.
|
|
677
706
|
git worktree add -q -B "$bookbr" "$tmpwt" "origin/$MAIN" 2>/dev/null \
|
|
678
|
-
|| 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."
|
|
679
711
|
|
|
680
712
|
cleanup() {
|
|
681
713
|
git worktree remove --force "$tmpwt" >/dev/null 2>&1 || true
|
package/plot-deliver.sh
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
#
|
|
36
36
|
# Each step asks THE SOURCE IT WOULD HAVE WRITTEN whether it is already done:
|
|
37
37
|
# the plan file for the phase and the record, the index directories for the
|
|
38
|
-
# symlink, the sprint file for the
|
|
38
|
+
# symlink, the sprint file for the tick. Never a progress file of its own.
|
|
39
39
|
#
|
|
40
40
|
# WHAT IT REFUSES, and why refusing beats guessing:
|
|
41
41
|
# - phase is not `approved` — nothing to deliver. (Already-Delivered is NOT a
|
|
@@ -66,7 +66,7 @@ done
|
|
|
66
66
|
die() { echo "plot-deliver: $*" >&2; exit 1; }
|
|
67
67
|
|
|
68
68
|
[ -n "$slug" ] || die "need a plan slug (usage: plot-deliver.sh [--dry-run] <slug>)"
|
|
69
|
-
git rev-parse --git-dir >/dev/null 2>&1 || die "not a git repository"
|
|
69
|
+
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"
|
|
70
70
|
|
|
71
71
|
cfg() { bash "$script_dir/plot-config.sh" get "$1" "$2"; }
|
|
72
72
|
|
|
@@ -86,10 +86,13 @@ plan_file=""
|
|
|
86
86
|
for cand in "$PLAN_DIR"*"$slug".md "$ACTIVE_DIR$slug.md" "$DELIVERED_DIR$slug.md"; do
|
|
87
87
|
[ -e "$cand" ] && { plan_file="$cand"; break; }
|
|
88
88
|
done
|
|
89
|
-
[ -n "$plan_file" ] || die "no plan found for '$slug' — looked in $PLAN_DIR, $ACTIVE_DIR, $DELIVERED_DIR
|
|
89
|
+
[ -n "$plan_file" ] || die "no plan found for '$slug' — looked in $PLAN_DIR, $ACTIVE_DIR, $DELIVERED_DIR.
|
|
90
|
+
Check the slug: ls $PLAN_DIR | grep -i '$slug'"
|
|
90
91
|
|
|
91
92
|
meta=$(bash "$script_dir/plot-plan-meta.sh" "$plan_file" 2>/dev/null) || meta=""
|
|
92
|
-
[ -n "$meta" ] || die "cannot parse '$plan_file' — refusing rather than guessing
|
|
93
|
+
[ -n "$meta" ] || die "cannot parse '$plan_file' — refusing rather than guessing.
|
|
94
|
+
See what the parser reads: $script_dir/plot-plan-meta.sh $plan_file
|
|
95
|
+
A plan needs a '## Status' section with a 'State:' field."
|
|
93
96
|
|
|
94
97
|
jfield() { printf '%s' "$meta" | jq -r "$1" 2>/dev/null; }
|
|
95
98
|
|
|
@@ -106,13 +109,16 @@ delivered_raw=$(jfield '.delivered_raw')
|
|
|
106
109
|
case "$phase" in
|
|
107
110
|
approved|delivered) ;;
|
|
108
111
|
released)
|
|
109
|
-
die "plan '$slug' is already released — nothing to deliver.
|
|
112
|
+
die "plan '$slug' is already released — nothing to deliver.
|
|
113
|
+
Nothing to do here; the work shipped." ;;
|
|
110
114
|
draft|design)
|
|
111
|
-
die "plan '$slug' is still '$phase' — approve it first
|
|
115
|
+
die "plan '$slug' is still '$phase' — approve it first: /plot-approve $slug" ;;
|
|
112
116
|
NONE|"")
|
|
113
|
-
die "cannot read the phase of '$slug' ($plan_file) — refusing rather than guessing.
|
|
117
|
+
die "cannot read the phase of '$slug' ($plan_file) — refusing rather than guessing.
|
|
118
|
+
Its '## Status' section needs a line reading '- **State:** Approved'." ;;
|
|
114
119
|
*)
|
|
115
|
-
die "plan '$slug' is in phase '$phase' — only an Approved plan can be delivered.
|
|
120
|
+
die "plan '$slug' is in phase '$phase' — only an Approved plan can be delivered.
|
|
121
|
+
If that phase is wrong, correct the 'State:' line in $plan_file and push it." ;;
|
|
116
122
|
esac
|
|
117
123
|
|
|
118
124
|
# ---------------------------------------------------------------------------
|
|
@@ -157,7 +163,8 @@ fi
|
|
|
157
163
|
|
|
158
164
|
verdict=$(PLOT_REPO_ROOT="$repo_root" PLOT_SCRIPTS_DIR="$script_dir" \
|
|
159
165
|
node "$ask_mjs" deliverable "$slug" "$plan_file" 2>/dev/null) || verdict=""
|
|
160
|
-
[ -n "$verdict" ] || die "cannot determine deliverability of '$slug' — refusing rather than guessing.
|
|
166
|
+
[ -n "$verdict" ] || die "cannot determine deliverability of '$slug' — refusing rather than guessing.
|
|
167
|
+
See what it said: PLOT_REPO_ROOT=$repo_root PLOT_SCRIPTS_DIR=$script_dir node $ask_mjs deliverable $slug $plan_file"
|
|
161
168
|
|
|
162
169
|
vfield() { printf '%s' "$verdict" | jq -r "$1" 2>/dev/null; }
|
|
163
170
|
|
|
@@ -187,7 +194,7 @@ today=$(date +%Y-%m-%d)
|
|
|
187
194
|
if [ "$dry_run" = 1 ]; then
|
|
188
195
|
echo "step: would flip Phase → Delivered and fill Delivered: $today"
|
|
189
196
|
echo "step: would move active/ → delivered/ symlink"
|
|
190
|
-
echo "step: would
|
|
197
|
+
echo "step: would tick the sprint item${sprint:+ (sprint: $sprint)}"
|
|
191
198
|
echo "summary: phase=would record=would index=would sprint=would push=would"
|
|
192
199
|
exit 0
|
|
193
200
|
fi
|
|
@@ -221,7 +228,8 @@ real_plan_path() { # $1 = plan file as found
|
|
|
221
228
|
}
|
|
222
229
|
|
|
223
230
|
rel=$(cd "$repo_root" && real_plan_path "$plan_file") || rel=""
|
|
224
|
-
[ -n "$rel" ] || die "$plan_file is outside the repository root
|
|
231
|
+
[ -n "$rel" ] || die "$plan_file is outside the repository root ($repo_root).
|
|
232
|
+
Move the plan under $PLAN_DIR inside this checkout and re-run."
|
|
225
233
|
|
|
226
234
|
# The filename, for symlink creation.
|
|
227
235
|
plan_basename=$(basename "$rel")
|
|
@@ -412,8 +420,20 @@ write_transition() { # $1=file $2=record $3=recorded(yes|no) → sets phase_repo
|
|
|
412
420
|
return 0
|
|
413
421
|
}
|
|
414
422
|
|
|
415
|
-
#
|
|
416
|
-
|
|
423
|
+
# Tick this plan's sprint item.
|
|
424
|
+
#
|
|
425
|
+
# THE TICK ONLY, since 2026-09-08. This also wrote a `<!-- status: delivered -->`
|
|
426
|
+
# annotation, and `a-withdrawn-item-is-not-open` measured that record dead in
|
|
427
|
+
# both directions: 67 lines carried one, none carried a value any reader acted
|
|
428
|
+
# on, and `plot-sprint-release.sh` read the field nowhere. The plan file carries
|
|
429
|
+
# `State:` and a dated `Delivered:` record, and the estate-outranks-the-checkbox
|
|
430
|
+
# rule points at those — a cache nobody refreshes and nobody reads is a second
|
|
431
|
+
# answer waiting to contradict the first.
|
|
432
|
+
#
|
|
433
|
+
# The box is NOT the same record. `/plot-sprint close` step 2a exists to tick
|
|
434
|
+
# boxes the estate says are done, and ticking here is what keeps that step's
|
|
435
|
+
# work to the items it genuinely cannot resolve.
|
|
436
|
+
update_sprint_item() { # $1=worktree root → prints none|updated|already|missing
|
|
417
437
|
local root="$1" f found=""
|
|
418
438
|
[ -n "$sprint" ] || { printf 'none'; return 0; }
|
|
419
439
|
local dir="$root/${SPRINT_DIR#/}"
|
|
@@ -431,15 +451,8 @@ update_sprint_annotation() { # $1=worktree root → prints none|updated|already|
|
|
|
431
451
|
index($0, "[" slug "]") == 0 { print; next }
|
|
432
452
|
{
|
|
433
453
|
line = $0
|
|
434
|
-
# Check the box
|
|
454
|
+
# Check the box. Nothing else on the line is touched.
|
|
435
455
|
sub(/\[ \]/, "[x]", line)
|
|
436
|
-
# Update or add status annotation
|
|
437
|
-
if (index(line, "<!--") == 0) {
|
|
438
|
-
line = line " <!-- status: delivered -->"
|
|
439
|
-
} else {
|
|
440
|
-
if (line ~ /status:[ \t]*[a-z-]+/) sub(/status:[ \t]*[a-z-]+/, "status: delivered", line)
|
|
441
|
-
else sub(/-->/, ", status: delivered -->", line)
|
|
442
|
-
}
|
|
443
456
|
print line
|
|
444
457
|
}
|
|
445
458
|
' "$found")
|
|
@@ -502,8 +515,8 @@ apply_local_writes() { # $1=root → sets phase_report record_report index_repo
|
|
|
502
515
|
# Step 5 — move the index symlink (best effort).
|
|
503
516
|
index_report=$(move_index_symlink "$root")
|
|
504
517
|
|
|
505
|
-
# Step 6 —
|
|
506
|
-
sprint_report=$(
|
|
518
|
+
# Step 6 — tick the sprint item.
|
|
519
|
+
sprint_report=$(update_sprint_item "$root")
|
|
507
520
|
return 0
|
|
508
521
|
}
|
|
509
522
|
|
|
@@ -517,7 +530,10 @@ bookbr="plot/deliver-$slug"
|
|
|
517
530
|
tmpwt="$wt_root/.plot-deliver-$slug.$$"
|
|
518
531
|
# -B: a leftover branch from an earlier failed run must not block this one.
|
|
519
532
|
git worktree add -q -B "$bookbr" "$tmpwt" "origin/$MAIN" 2>/dev/null \
|
|
520
|
-
|| die "could not prepare a booking worktree at $tmpwt
|
|
533
|
+
|| die "could not prepare a booking worktree at $tmpwt.
|
|
534
|
+
Most often origin/$MAIN is not fetched, or '$bookbr' is checked out in
|
|
535
|
+
another worktree. Check both: git fetch origin $MAIN && git worktree list
|
|
536
|
+
Nothing has been written locally; the plan is untouched."
|
|
521
537
|
|
|
522
538
|
cleanup() {
|
|
523
539
|
git worktree remove --force "$tmpwt" >/dev/null 2>&1 || true
|
|
@@ -544,7 +560,9 @@ if git -C "$tmpwt" diff --cached --quiet 2>/dev/null; then
|
|
|
544
560
|
else
|
|
545
561
|
if ! git -C "$tmpwt" -c "user.name=$who" commit -q -m "plot: deliver $slug"; then
|
|
546
562
|
cleanup
|
|
547
|
-
die "could not commit the delivery
|
|
563
|
+
die "could not commit the delivery.
|
|
564
|
+
See what git refused: git -C $tmpwt status
|
|
565
|
+
Nothing was pushed; re-run this — it is idempotent."
|
|
548
566
|
fi
|
|
549
567
|
|
|
550
568
|
push_out=$(bash "$script_dir/plot-push-main.sh" "$bookbr" "$MAIN" 2>&1)
|
package/plot-dispatch.sh
CHANGED
|
@@ -276,7 +276,11 @@ while [ $# -gt 0 ]; do
|
|
|
276
276
|
done
|
|
277
277
|
|
|
278
278
|
git rev-parse --git-dir >/dev/null 2>&1 || { echo "not a git repository" >&2; exit 1; }
|
|
279
|
-
[ -n "$slug" ] || [ "$mode" != dispatch ] || {
|
|
279
|
+
[ -n "$slug" ] || [ "$mode" != dispatch ] || {
|
|
280
|
+
echo "plot-dispatch: need a plan slug (usage: plot-dispatch.sh [--dry-run] <slug>)" >&2
|
|
281
|
+
echo " Which plans could be dispatched: /plot-pulse" >&2
|
|
282
|
+
exit 1
|
|
283
|
+
}
|
|
280
284
|
|
|
281
285
|
# ---------------------------------------------------------------------------
|
|
282
286
|
# Worker launch, and the identity it records
|
|
@@ -1168,14 +1172,38 @@ if [ "$mode" = "stop" ]; then
|
|
|
1168
1172
|
echo " Refusing to guess — stopping the wrong worker discards its work." >&2
|
|
1169
1173
|
exit 1
|
|
1170
1174
|
fi
|
|
1171
|
-
|
|
1172
|
-
|
|
1175
|
+
# ASK GIT WHICH WORKTREE HOLDS THE BRANCH, then fall back to the dispatch
|
|
1176
|
+
# path. `--restart` below already asks, and this did not: it rebuilt one path
|
|
1177
|
+
# from the branch name and reported that single path as though it were the
|
|
1178
|
+
# only place a desk could be. On 2026-09-07 the desk existed elsewhere — a
|
|
1179
|
+
# worktree made by hand, which is the population that never follows
|
|
1180
|
+
# dispatch's naming — and the refusal sent a reader to `kill`.
|
|
1181
|
+
#
|
|
1182
|
+
# A refusal that is confidently wrong is worse than one that is terse, so the
|
|
1183
|
+
# path-guess survives only as the LAST candidate and the refusal below says
|
|
1184
|
+
# which places were looked in.
|
|
1185
|
+
wt=$(git worktree list --porcelain </dev/null 2>/dev/null | awk -v want="refs/heads/$stop_branch" '
|
|
1186
|
+
/^worktree / { path = substr($0, 10) }
|
|
1187
|
+
/^branch / { if (substr($0, 8) == want) { print path; exit } }')
|
|
1188
|
+
wt_guess="$wt_root_early/$wt_prefix_early$(printf '%s' "$stop_branch" | tr '/' '-')"
|
|
1189
|
+
[ -n "$wt" ] && [ -d "$wt" ] || wt="$wt_guess"
|
|
1190
|
+
if [ ! -d "$wt" ]; then
|
|
1191
|
+
echo "plot-dispatch: no worktree holds '$stop_branch' — nothing to stop." >&2
|
|
1192
|
+
echo " Asked git for every worktree, and looked at $wt_guess." >&2
|
|
1193
|
+
echo " If a worker is running somewhere this cannot see, that machine is" >&2
|
|
1194
|
+
echo " where to stop it: /plot-dispatch --status names the desks here." >&2
|
|
1195
|
+
echo " Nothing was killed." >&2
|
|
1196
|
+
exit 1
|
|
1197
|
+
fi
|
|
1173
1198
|
st=$(worker_state "$wt" "$stop_branch")
|
|
1174
1199
|
case "$st" in
|
|
1175
1200
|
running*)
|
|
1176
1201
|
pid=${st#running }
|
|
1177
1202
|
kill "$pid" 2>/dev/null && echo "stopped $stop_branch (pid $pid)" \
|
|
1178
|
-
|| { echo "plot-dispatch: could not stop pid $pid" >&2
|
|
1203
|
+
|| { echo "plot-dispatch: could not stop pid $pid — it may have exited between the read and the signal, or belong to another user." >&2
|
|
1204
|
+
echo " Check it: ps -p $pid -o pid=,stat=,command=" >&2
|
|
1205
|
+
echo " Nothing else was written; the worktree and the claim stand." >&2
|
|
1206
|
+
exit 1; }
|
|
1179
1207
|
# The worktree and its claim are left in place: the branch is still taken,
|
|
1180
1208
|
# and deleting either would be the kind of write this design avoids.
|
|
1181
1209
|
echo " worktree kept at $wt — the claim stands until you release it"
|
|
@@ -1884,6 +1912,7 @@ fi
|
|
|
1884
1912
|
if [ -z "$plan_path" ]; then
|
|
1885
1913
|
if [ "$allow_local" = 1 ]; then
|
|
1886
1914
|
echo "plot-dispatch: no plan found for '$slug' — looked in $ACTIVE_DIR_CFG and $PLAN_DIR_CFG" >&2
|
|
1915
|
+
echo " Check the slug: ls $PLAN_DIR_CFG | grep -i '$slug'" >&2
|
|
1887
1916
|
else
|
|
1888
1917
|
echo "plot-dispatch: no plan for '$slug' on $gate_ref — looked in $ACTIVE_DIR_CFG and $PLAN_DIR_CFG" >&2
|
|
1889
1918
|
echo " A plan that exists only in this working tree has not been shared yet: push it first." >&2
|
|
@@ -1944,6 +1973,7 @@ case "$gate_phase" in
|
|
|
1944
1973
|
exit 1 ;;
|
|
1945
1974
|
delivered|released)
|
|
1946
1975
|
echo "plot-dispatch: plan '$slug' is already $gate_phase — its work is done." >&2
|
|
1976
|
+
echo " Nothing to dispatch. To start new work: /plot-idea" >&2
|
|
1947
1977
|
exit 1 ;;
|
|
1948
1978
|
"")
|
|
1949
1979
|
echo "plot-dispatch: cannot read the phase of '$slug' ($gate_source)." >&2
|
|
@@ -1951,6 +1981,8 @@ case "$gate_phase" in
|
|
|
1951
1981
|
exit 1 ;;
|
|
1952
1982
|
*)
|
|
1953
1983
|
echo "plot-dispatch: plan '$slug' is in phase '$gate_phase', not Approved." >&2
|
|
1984
|
+
echo " Correct the 'State:' line in the plan and push it, or approve it:" >&2
|
|
1985
|
+
echo " /plot-approve $slug" >&2
|
|
1954
1986
|
exit 1 ;;
|
|
1955
1987
|
esac
|
|
1956
1988
|
|
|
@@ -1962,6 +1994,7 @@ case "$gate_impl" in
|
|
|
1962
1994
|
same-branch)
|
|
1963
1995
|
echo "plot-dispatch: plan '$slug' records 'Impl: same branch' — plan and code" >&2
|
|
1964
1996
|
echo " travel on one branch, so there is nothing to fan out." >&2
|
|
1997
|
+
echo " Implement on that branch instead: /plot-implement $slug" >&2
|
|
1965
1998
|
exit 1 ;;
|
|
1966
1999
|
other-repo)
|
|
1967
2000
|
echo "plot-dispatch: plan '$slug' records 'Impl: other repo' — implementation" >&2
|
|
@@ -1974,6 +2007,8 @@ case "$gate_impl" in
|
|
|
1974
2007
|
*)
|
|
1975
2008
|
echo "plot-dispatch: plan '$slug' records an unrecognised 'Impl:' answer" >&2
|
|
1976
2009
|
echo " ('$gate_impl'). Refusing rather than guessing." >&2
|
|
2010
|
+
echo " Set the plan's 'Impl:' line to one of: own branches, same branch," >&2
|
|
2011
|
+
echo " other repo, none — then push it." >&2
|
|
1977
2012
|
exit 1 ;;
|
|
1978
2013
|
esac
|
|
1979
2014
|
|
|
@@ -2403,7 +2438,8 @@ write_started_record() { # $@ = branches
|
|
|
2403
2438
|
# would carry the symlink and leave the record behind.
|
|
2404
2439
|
rel=$(cd "$repo_root" && real_plan_path "$plan_file") || rel=""
|
|
2405
2440
|
if [ -z "$rel" ]; then
|
|
2406
|
-
echo "plot-dispatch: $plan_file is outside the repository root" >&2
|
|
2441
|
+
echo "plot-dispatch: $plan_file is outside the repository root ($repo_root)." >&2
|
|
2442
|
+
echo " Move the plan under $PLAN_DIR_CFG inside this checkout and re-run." >&2
|
|
2407
2443
|
return 1
|
|
2408
2444
|
fi
|
|
2409
2445
|
|
|
@@ -2419,6 +2455,9 @@ write_started_record() { # $@ = branches
|
|
|
2419
2455
|
# one. It is disposable by construction — created here, pushed, deleted.
|
|
2420
2456
|
if ! git worktree add -q -B "$bookbr" "$tmpwt" "origin/$MAIN" 2>/dev/null; then
|
|
2421
2457
|
echo "plot-dispatch: could not prepare a booking worktree at $tmpwt" >&2
|
|
2458
|
+
echo " Most often origin/$MAIN is not fetched, or '$bookbr' is checked out in" >&2
|
|
2459
|
+
echo " another worktree. Check both: git fetch origin $MAIN && git worktree list" >&2
|
|
2460
|
+
echo " The branches were dispatched; only the plan's Started record is missing." >&2
|
|
2422
2461
|
return 1
|
|
2423
2462
|
fi
|
|
2424
2463
|
|
|
@@ -2442,6 +2481,7 @@ write_started_record() { # $@ = branches
|
|
|
2442
2481
|
fi
|
|
2443
2482
|
append_started_line "$tmpwt/$rel" "$date" "$who" "$br" || {
|
|
2444
2483
|
echo "plot-dispatch: $rel has no '## Status' section — nowhere to record" >&2
|
|
2484
|
+
echo " Add one to the plan (see .plot/templates/plan.md) and push it." >&2
|
|
2445
2485
|
rc=1
|
|
2446
2486
|
break
|
|
2447
2487
|
}
|
|
@@ -2463,6 +2503,7 @@ write_started_record() { # $@ = branches
|
|
|
2463
2503
|
fi
|
|
2464
2504
|
else
|
|
2465
2505
|
echo "plot-dispatch: $rel is not on origin/$MAIN" >&2
|
|
2506
|
+
echo " Push the plan to $MAIN first; the fleet reads plans from the shared ref." >&2
|
|
2466
2507
|
rc=1
|
|
2467
2508
|
fi
|
|
2468
2509
|
|