@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plot-pm/board",
3
- "version": "0.12.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",
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. Reopen it or create a new one." ;;
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'. Run /plot-idea first, or push the 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, status: draft, branch: feature/slug -->
562
- # /plot-sprint READS these (`pr`, `status`, `branch`) and /plot-approve writes
563
- # them, so an approval that skips this makes `/plot-sprint status` wrong rather
564
- # than merely incomplete.
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 `status: approved`.
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
- line = line " <!-- pr: #" pr ", status: approved" (br != "" ? ", branch: " br : "") " -->"
593
- } else {
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
- print line
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 annotation. Never a progress file of its own.
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 update the sprint annotation${sprint:+ (sprint: $sprint)}"
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
- # Update the sprint item annotation for this plan.
416
- update_sprint_annotation() { # $1=worktree root → prints none|updated|already|missing
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 — update the sprint annotation.
506
- sprint_report=$(update_sprint_annotation "$root")
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 ] || { echo "plot-dispatch: need a plan slug" >&2; exit 1; }
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
- wt="$wt_root_early/$wt_prefix_early$(printf '%s' "$stop_branch" | tr '/' '-')"
1172
- [ -d "$wt" ] || { echo "plot-dispatch: no worktree for '$stop_branch' at $wt" >&2; exit 1; }
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; exit 1; }
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