@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/plot-dispatch.sh
CHANGED
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
# Plot helper: hand one slice + its brief to the registry per eligible branch.
|
|
3
3
|
# Usage: plot-dispatch.sh [--dry-run] [--no-start] [--no-brief] [--offline]
|
|
4
4
|
# [--max N] [--allow-local] <slug>
|
|
5
|
+
# plot-dispatch.sh --start [N] [--dry-run]
|
|
5
6
|
# plot-dispatch.sh --migrate [--yes] [--max N]
|
|
6
7
|
# --status list fleet worktrees with worker pid, liveness, and last log
|
|
7
8
|
# line; then exit. Works regardless of plan phase.
|
|
8
9
|
# --stop <br> stop the worker on <br> (branch required — never "all").
|
|
10
|
+
# --start [N] start N free agents — registered, waiting, holding no slice.
|
|
11
|
+
# Defaults to three. Each gets a desk detached at origin/<main>
|
|
12
|
+
# and a manifest naming no branch, so the registry's next tick can
|
|
13
|
+
# hand each a queued slice with nobody touching a desk. The count
|
|
14
|
+
# is a REQUEST: a machine at its bound answers with fewer and says
|
|
15
|
+
# so, and the shortfall is reported rather than remembered.
|
|
9
16
|
# --restart <br>
|
|
10
17
|
# start a worker on <br>, which already holds a claim — the
|
|
11
18
|
# counterpart to --stop, and the only way to hand a stopped
|
|
@@ -151,6 +158,14 @@ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
|
151
158
|
# shellcheck source=plot-pr-merged.sh
|
|
152
159
|
. "$script_dir/plot-pr-merged.sh"
|
|
153
160
|
|
|
161
|
+
# The ONE answer to "what is the default branch?" — `default_branch`, which
|
|
162
|
+
# repairs an unresolvable `origin/HEAD` before it answers. This script is where
|
|
163
|
+
# that corruption was MEASURED: twice on 2026-09-04 the symref pointed at
|
|
164
|
+
# `origin/plot-corpus-pin`, and the refusal below fired on every dispatch. The
|
|
165
|
+
# refusal stays; it now has less to refuse over.
|
|
166
|
+
# shellcheck source=plot-default-branch.sh
|
|
167
|
+
. "$script_dir/plot-default-branch.sh"
|
|
168
|
+
|
|
154
169
|
# ---------------------------------------------------------------------------
|
|
155
170
|
# WHERE THE WORKTREES LIVE, and by what name
|
|
156
171
|
# ---------------------------------------------------------------------------
|
|
@@ -199,6 +214,10 @@ no_brief=0
|
|
|
199
214
|
mode=dispatch
|
|
200
215
|
stop_branch=""
|
|
201
216
|
restart_branch=""
|
|
217
|
+
# EMPTY MEANS "THE DEFAULT", AND THE DEFAULT IS THE RULE'S. `fleetSize` owns the
|
|
218
|
+
# number and the argument for it; a literal here would be a second copy of a
|
|
219
|
+
# decision that has one home. It is filled from the rule below.
|
|
220
|
+
start_count=""
|
|
202
221
|
offline=""
|
|
203
222
|
allow_local=0
|
|
204
223
|
allow_waiting=0
|
|
@@ -230,6 +249,16 @@ while [ $# -gt 0 ]; do
|
|
|
230
249
|
# plan for feature/x", which describes neither what was asked nor what
|
|
231
250
|
# went wrong. The branch is consumed only when it looks like one.
|
|
232
251
|
--restart) mode=restart; case "${2:-}" in */*) restart_branch="$2"; shift ;; esac ;;
|
|
252
|
+
# `--start [N]` brings FREE agents into existence — registered, waiting, and
|
|
253
|
+
# holding no slice. The count is OPTIONAL and only a bare number is consumed,
|
|
254
|
+
# the same rule `--stop` and `--restart` apply to a branch: a value that does
|
|
255
|
+
# not look like a count is left for the parser rather than swallowed, so
|
|
256
|
+
# `--start --dry-run` means what it reads as.
|
|
257
|
+
--start) mode=start
|
|
258
|
+
case "${2:-}" in
|
|
259
|
+
''|*[!0-9]*) ;;
|
|
260
|
+
*) start_count="$2"; shift ;;
|
|
261
|
+
esac ;;
|
|
233
262
|
--no-start) no_start=1 ;;
|
|
234
263
|
--no-brief) no_brief=1 ;;
|
|
235
264
|
--offline|--no-fetch) offline="--offline" ;;
|
|
@@ -240,14 +269,18 @@ while [ $# -gt 0 ]; do
|
|
|
240
269
|
''|*[!0-9]*) echo "plot-dispatch: --max needs a number, got '$max'" >&2; exit 1 ;;
|
|
241
270
|
esac
|
|
242
271
|
shift ;;
|
|
243
|
-
-h|--help) sed -n '2,
|
|
272
|
+
-h|--help) sed -n '2,59p' "$0"; exit 0 ;;
|
|
244
273
|
*) slug="$1" ;;
|
|
245
274
|
esac
|
|
246
275
|
shift
|
|
247
276
|
done
|
|
248
277
|
|
|
249
278
|
git rev-parse --git-dir >/dev/null 2>&1 || { echo "not a git repository" >&2; exit 1; }
|
|
250
|
-
[ -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
|
+
}
|
|
251
284
|
|
|
252
285
|
# ---------------------------------------------------------------------------
|
|
253
286
|
# Worker launch, and the identity it records
|
|
@@ -455,13 +488,202 @@ brief_prompt() { # $1 = branch, $2 = slug
|
|
|
455
488
|
"$2" "$1" "$(brief_path "$1")" "$MAIN" "$MAIN"
|
|
456
489
|
}
|
|
457
490
|
|
|
491
|
+
# WHETHER THE AGENT THIS COMMAND SPAWNS CAN REACH `plot-implement` AT ALL.
|
|
492
|
+
#
|
|
493
|
+
# `Brief command` runs an agent headless and asks it for `/plot-implement
|
|
494
|
+
# <slug>`. `plot-implement` is a SKILL, and a skill the running agent does not
|
|
495
|
+
# have resolves to nothing. Measured 2026-09-06: Plot is installed as a plugin
|
|
496
|
+
# at 1.2.0 against this repository's 2.13.0, and the installed `skills/` holds
|
|
497
|
+
# eight — `plot-implement` is not among them, nor are `plot-dispatch`,
|
|
498
|
+
# `plot-fleet`, `plot-pulse`, `plot-reconcile`, `plot-board`, `plot-init`,
|
|
499
|
+
# `plot-merge-queue` or `plot-reslice`. Twelve are missing.
|
|
500
|
+
#
|
|
501
|
+
# So `Unknown command: /plot-implement` was LITERALLY TRUE in both 33-byte logs
|
|
502
|
+
# (2026-09-02, 2026-09-04), and the board's *"Run /plot-implement `<slug>` and
|
|
503
|
+
# follow it."* would have failed identically. The prompt's wording was never the
|
|
504
|
+
# defect, and rewriting it fixes nothing.
|
|
505
|
+
#
|
|
506
|
+
# THE INSTALL BEING SCOPED TO ANOTHER PROJECT IS WHY FOUR MONTHS PASSED
|
|
507
|
+
# UNNOTICED. The registry entry carries `scope: project` and `projectPath:
|
|
508
|
+
# /Users/jwloka/Quatico/CDS/cpq-cds-develop` — a DIFFERENT repository, installed
|
|
509
|
+
# 2026-05-08. A plugin installed elsewhere is not absent, it is somewhere else,
|
|
510
|
+
# and nothing on this estate named that.
|
|
511
|
+
#
|
|
512
|
+
# IT ASKS WHETHER THE SKILL DIRECTORY EXISTS, NOT WHETHER THE VERSION MATCHES.
|
|
513
|
+
# A lagging install that still carries the skill works, and a version comparison
|
|
514
|
+
# would refuse it and demand an update nobody needs. One missing directory is
|
|
515
|
+
# what breaks a brief session, so that is what is read.
|
|
516
|
+
#
|
|
517
|
+
# IT FAILS TOWARD ALLOWING. An unreadable or absent `installed_plugins.json`
|
|
518
|
+
# means CANNOT VERIFY, not broken: a project running Plot from a checkout with
|
|
519
|
+
# no plugin install at all is a supported shape, and refusing it would break
|
|
520
|
+
# dispatch for everyone who never installed the plugin. It says the check could
|
|
521
|
+
# not be made, and proceeds.
|
|
522
|
+
#
|
|
523
|
+
# `PLOT_PLUGIN_ROOT` is the override, the same one `plot-board-probe.sh` uses so
|
|
524
|
+
# tests need not depend on `$HOME`.
|
|
525
|
+
plugin_registry() { printf '%s/installed_plugins.json' "${PLOT_PLUGIN_ROOT:-${HOME:-}/.claude/plugins}"; }
|
|
526
|
+
|
|
527
|
+
# Every `installPath` recorded for a plugin whose key starts `plot@`, one per
|
|
528
|
+
# line. The marketplace half of the key is not fixed — `plot@plot-marketplace`
|
|
529
|
+
# here, but a repository may serve Plot from a marketplace under any name — so
|
|
530
|
+
# the prefix is what is matched rather than the whole key.
|
|
531
|
+
#
|
|
532
|
+
# `awk` rather than `node` or `jq`: this script shells to neither anywhere else,
|
|
533
|
+
# and a check that fails toward allowing must not acquire a dependency whose
|
|
534
|
+
# absence it would then have to read as "cannot verify" on every machine that
|
|
535
|
+
# lacks it.
|
|
536
|
+
#
|
|
537
|
+
# IT DOES NOT DEPEND ON THE FILE'S LINE BREAKS. The registry Claude Code writes
|
|
538
|
+
# is pretty-printed, one field per line, and a line-oriented read of it works —
|
|
539
|
+
# but only because of how it happens to be formatted, and this check's wrong
|
|
540
|
+
# answer is silent. Measured 2026-09-07 against a compact registry written by
|
|
541
|
+
# hand: the whole object on two lines, and the reader found no install at all,
|
|
542
|
+
# reported "could not verify" and allowed. Safe, and still wrong about a file
|
|
543
|
+
# that was perfectly readable.
|
|
544
|
+
#
|
|
545
|
+
# So the text is flattened to one token per line FIRST — every `"…"` string and
|
|
546
|
+
# every `[` `]` `{` `}` on its own line — and the scan then walks tokens rather
|
|
547
|
+
# than lines. Formatting stops being an input.
|
|
548
|
+
#
|
|
549
|
+
# The bracket depth is what scopes a key: a plugin's entry runs from the `[`
|
|
550
|
+
# after its key to the `]` that closes it, so an `installPath` is attributed to
|
|
551
|
+
# the key whose array still encloses it. Tracking the key alone would let the
|
|
552
|
+
# LAST `plot@…` key claim every install after it in the file.
|
|
553
|
+
plugin_install_paths() { # → one installPath per line, for keys matching plot@*
|
|
554
|
+
awk '
|
|
555
|
+
{
|
|
556
|
+
line = $0
|
|
557
|
+
while (length(line) > 0) {
|
|
558
|
+
c = substr(line, 1, 1)
|
|
559
|
+
if (c == "\"") {
|
|
560
|
+
# A JSON string, taken whole: scan to the closing quote, honouring
|
|
561
|
+
# backslash escapes so a quote inside a value does not end it early.
|
|
562
|
+
v = ""; i = 2
|
|
563
|
+
while (i <= length(line)) {
|
|
564
|
+
ch = substr(line, i, 1)
|
|
565
|
+
if (ch == "\\") { v = v substr(line, i + 1, 1); i += 2; continue }
|
|
566
|
+
if (ch == "\"") break
|
|
567
|
+
v = v ch; i++
|
|
568
|
+
}
|
|
569
|
+
print "S" v
|
|
570
|
+
line = substr(line, i + 1)
|
|
571
|
+
} else if (c == "[" || c == "]" || c == "{" || c == "}") {
|
|
572
|
+
print "P" c
|
|
573
|
+
line = substr(line, 2)
|
|
574
|
+
} else {
|
|
575
|
+
line = substr(line, 2)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
' "$1" 2>/dev/null | awk '
|
|
580
|
+
# `pending` is the string most recently seen, which is a KEY when the next
|
|
581
|
+
# token opens a container. `depth` is where this plugin"s array closes.
|
|
582
|
+
/^P\[/ {
|
|
583
|
+
depth++
|
|
584
|
+
if (pending ~ /^plot@/ && inplot == 0) { inplot = 1; plotdepth = depth }
|
|
585
|
+
pending = ""; next
|
|
586
|
+
}
|
|
587
|
+
/^P\]/ {
|
|
588
|
+
if (inplot && depth == plotdepth) inplot = 0
|
|
589
|
+
depth--; pending = ""; next
|
|
590
|
+
}
|
|
591
|
+
/^P/ { pending = ""; next }
|
|
592
|
+
/^S/ {
|
|
593
|
+
v = substr($0, 2)
|
|
594
|
+
if (want) { if (inplot && v != "") print v; want = 0; pending = ""; next }
|
|
595
|
+
if (v == "installPath") { want = 1; next }
|
|
596
|
+
pending = v
|
|
597
|
+
}
|
|
598
|
+
'
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
# Can a spawned agent reach `plot-implement`?
|
|
602
|
+
#
|
|
603
|
+
# 0 — yes: some recorded install carries `skills/plot-implement/`
|
|
604
|
+
# 1 — no: the registry was read, Plot is installed, and none of its installs
|
|
605
|
+
# carry the skill. This is the refusal.
|
|
606
|
+
# 2 — cannot verify: no registry, unreadable, or no `plot@*` entry at all.
|
|
607
|
+
# Allowed, and said.
|
|
608
|
+
#
|
|
609
|
+
# The three-way answer is the whole point. Collapsing "cannot verify" into
|
|
610
|
+
# either of the others is how this check would break a repository running Plot
|
|
611
|
+
# from a checkout, or wave through the exact install that produced two empty
|
|
612
|
+
# logs.
|
|
613
|
+
implement_skill_reach() { # → 0 reachable, 1 unreachable, 2 unverifiable
|
|
614
|
+
local reg paths p
|
|
615
|
+
reg=$(plugin_registry)
|
|
616
|
+
[ -n "$reg" ] && [ -r "$reg" ] || return 2
|
|
617
|
+
paths=$(plugin_install_paths "$reg")
|
|
618
|
+
[ -n "$paths" ] || return 2
|
|
619
|
+
while IFS= read -r p; do
|
|
620
|
+
[ -n "$p" ] || continue
|
|
621
|
+
case "$p" in "~/"*) p="${HOME:-}/${p#\~/}" ;; esac
|
|
622
|
+
[ -d "$p/skills/plot-implement" ] && return 0
|
|
623
|
+
done <<< "$paths"
|
|
624
|
+
return 1
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
# What the refusal SAYS, and it is the shape `plot-fleetctl.sh:306` uses for a
|
|
628
|
+
# wrong `node` major — the reading first, then the repair.
|
|
629
|
+
#
|
|
630
|
+
# IT NAMES A REPAIR AN OPERATOR CAN PERFORM. Plot cannot install its own plugin;
|
|
631
|
+
# updating an install is an action on a machine. So the message reports what was
|
|
632
|
+
# read — which installs, and that none carries the skill — and names the command
|
|
633
|
+
# that fixes it. Nothing here installs or updates anything.
|
|
634
|
+
implement_unreachable_report() { # prints the reading and the repair, indented
|
|
635
|
+
local reg p
|
|
636
|
+
reg=$(plugin_registry)
|
|
637
|
+
echo " no-implement-skill — the agent this would spawn cannot reach \`/plot-implement\`"
|
|
638
|
+
echo " read: $reg"
|
|
639
|
+
while IFS= read -r p; do
|
|
640
|
+
[ -n "$p" ] || continue
|
|
641
|
+
echo " install: $p (no skills/plot-implement/)"
|
|
642
|
+
done <<< "$(plugin_install_paths "$reg")"
|
|
643
|
+
echo " A prompt naming a skill the agent does not have resolves to nothing,"
|
|
644
|
+
echo " whatever its wording — measured twice as a 33-byte log reading"
|
|
645
|
+
echo " \`Unknown command: /plot-implement\`. Refusing before spawning is what"
|
|
646
|
+
echo " puts that in front of you instead of in a log nobody reads."
|
|
647
|
+
echo " Fix it: update the Plot plugin so its skills/ carries plot-implement"
|
|
648
|
+
echo " (/plugin, or reinstall from the marketplace), then dispatch again."
|
|
649
|
+
echo " Or write the brief yourself: /plot-implement $1"
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
# THE THIRD ANSWER, and it allows. A registry that cannot be read says nothing
|
|
653
|
+
# about whether the skill is there — a project running Plot from a checkout with
|
|
654
|
+
# no plugin install at all is a supported shape, and refusing it would break
|
|
655
|
+
# dispatch for everyone who never installed the plugin. So the run says the
|
|
656
|
+
# check could not be made and proceeds, which is what keeps a silent skip from
|
|
657
|
+
# reading as a verified pass.
|
|
658
|
+
implement_unverifiable_note() {
|
|
659
|
+
echo " could not verify that the agent can reach the implement skill —"
|
|
660
|
+
echo " no readable plugin registry at $(plugin_registry); proceeding"
|
|
661
|
+
}
|
|
662
|
+
|
|
458
663
|
request_brief() { # $1 = branch, $2 = slug → 0 if a command was started
|
|
459
|
-
local branch="$1" bslug="$2" cmd log
|
|
664
|
+
local branch="$1" bslug="$2" cmd log reach
|
|
460
665
|
cmd=$(brief_command)
|
|
461
666
|
if [ -z "$cmd" ]; then
|
|
462
667
|
echo " no-brief-command — no \`Brief command\` in Plot Config, so nothing was asked to write it"
|
|
463
668
|
return 1
|
|
464
669
|
fi
|
|
670
|
+
# REFUSES BEFORE SPAWNING, NEVER AFTER. Failing late cost two logs nobody
|
|
671
|
+
# read and a session writing nine briefs by hand without asking why the arm
|
|
672
|
+
# was silent. The refusal is read by whoever ran the dispatch.
|
|
673
|
+
#
|
|
674
|
+
# A DIFFERENT REFUSAL FROM `no-brief-command`, and it gets its own word rather
|
|
675
|
+
# than overloading that one: there the key is absent and nothing was asked to
|
|
676
|
+
# write the brief; here the key is SET and the skill it names is unreachable.
|
|
677
|
+
# One word for two states is a word an operator cannot act on.
|
|
678
|
+
implement_skill_reach
|
|
679
|
+
reach=$?
|
|
680
|
+
if [ "$reach" = 1 ]; then
|
|
681
|
+
implement_unreachable_report "$bslug"
|
|
682
|
+
return 1
|
|
683
|
+
fi
|
|
684
|
+
if [ "$reach" = 2 ]; then
|
|
685
|
+
implement_unverifiable_note
|
|
686
|
+
fi
|
|
465
687
|
log="$repo_root/.plot/brief-$(printf '%s' "${branch##*/}").log"
|
|
466
688
|
mkdir -p "$(dirname "$log")" 2>/dev/null || true
|
|
467
689
|
# `nohup ... &` inside a subshell, the same detachment `start_worker` uses:
|
|
@@ -950,14 +1172,38 @@ if [ "$mode" = "stop" ]; then
|
|
|
950
1172
|
echo " Refusing to guess — stopping the wrong worker discards its work." >&2
|
|
951
1173
|
exit 1
|
|
952
1174
|
fi
|
|
953
|
-
|
|
954
|
-
|
|
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
|
|
955
1198
|
st=$(worker_state "$wt" "$stop_branch")
|
|
956
1199
|
case "$st" in
|
|
957
1200
|
running*)
|
|
958
1201
|
pid=${st#running }
|
|
959
1202
|
kill "$pid" 2>/dev/null && echo "stopped $stop_branch (pid $pid)" \
|
|
960
|
-
|| { 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; }
|
|
961
1207
|
# The worktree and its claim are left in place: the branch is still taken,
|
|
962
1208
|
# and deleting either would be the kind of write this design avoids.
|
|
963
1209
|
echo " worktree kept at $wt — the claim stands until you release it"
|
|
@@ -1100,6 +1346,284 @@ if [ "$mode" = "restart" ]; then
|
|
|
1100
1346
|
exit 0
|
|
1101
1347
|
fi
|
|
1102
1348
|
|
|
1349
|
+
if [ "$mode" = "start" ]; then
|
|
1350
|
+
# THE LAST LINK IN THE CHAIN. `plot-dispatch.sh <slug>` queues slices and the
|
|
1351
|
+
# registry matches them to free agents, but until this verb existed nothing
|
|
1352
|
+
# brought a free agent into being. Measured 2026-09-05: a dispatch reported
|
|
1353
|
+
# `handed over feature/... → the registry` and `started=0`, the supervisor
|
|
1354
|
+
# ticked `queued=456 idle=0 agents registered: 0`, and `.plot/agents/` was
|
|
1355
|
+
# empty. The slice was queued, the registry was willing, and there was nobody
|
|
1356
|
+
# to hand it to.
|
|
1357
|
+
#
|
|
1358
|
+
# AN AGENT IS STARTED WITH NO SLICE, and that is the whole shape. It gets a
|
|
1359
|
+
# desk, a manifest naming no branch, and a loop that waits — `isAgentFree`
|
|
1360
|
+
# already reports exactly that state as free (`rules/free.ts:64`: alive, and
|
|
1361
|
+
# `branch === ""`), so the supervisor's next tick can hand each one a queued
|
|
1362
|
+
# slice with nobody touching a desk.
|
|
1363
|
+
#
|
|
1364
|
+
# HERE, BESIDE --stop AND --restart AND BEFORE THE PHASE GATE, for the reason
|
|
1365
|
+
# that block already gives from the other side: starting a free agent is not
|
|
1366
|
+
# about any plan, so there is no plan whose phase could refuse it. A gate on a
|
|
1367
|
+
# slug this verb never takes would refuse every call.
|
|
1368
|
+
repo_root="$repo_root_early"
|
|
1369
|
+
resolve_wt_root "$repo_root"
|
|
1370
|
+
|
|
1371
|
+
# THE DEFAULT BRANCH, by the same three steps the fan-out takes below — the
|
|
1372
|
+
# config key, then origin's own HEAD, then `main`. Resolved here because the
|
|
1373
|
+
# fan-out's `MAIN` is set past the phase gate this path exits before.
|
|
1374
|
+
start_main=$(bash "$script_dir/plot-config.sh" get "Main branch")
|
|
1375
|
+
[ -n "$start_main" ] || start_main=$(default_branch)
|
|
1376
|
+
[ -n "$start_main" ] || start_main="main"
|
|
1377
|
+
|
|
1378
|
+
# HOW MANY WORKERS ARE ALREADY UP, counted from the desks on this disk rather
|
|
1379
|
+
# than from the registry. A manifest records a launch; a live pid records a
|
|
1380
|
+
# worker, and the question `--start` asks is about the machine's load. The
|
|
1381
|
+
# count is what `fleetSize` subtracts, so asking for three twice gives three
|
|
1382
|
+
# agents rather than six.
|
|
1383
|
+
#
|
|
1384
|
+
# A CALLER THAT HAS ALREADY DECIDED THE COUNT SUBTRACTS NOTHING, and
|
|
1385
|
+
# `PLOT_START_ONE` is how it says so. The supervisor's tick ran `fleetSize`
|
|
1386
|
+
# against the WHOLE fleet, decided N, and hands this script one write at a
|
|
1387
|
+
# time; re-counting per call subtracts the agent the PREVIOUS call just
|
|
1388
|
+
# started. Measured 2026-09-05 in a sandbox: a tick that decided `started=3`
|
|
1389
|
+
# produced ONE agent, because calls two and three each saw a fleet already
|
|
1390
|
+
# the size they were asked for.
|
|
1391
|
+
#
|
|
1392
|
+
# SO IT MEANS *START THIS ONE*: the count is one and the subtraction is
|
|
1393
|
+
# skipped. The machine keeps its veto — a starved one still answers with
|
|
1394
|
+
# fewer, which is the reading that must not be skipped, because it is about
|
|
1395
|
+
# the load this very call would add.
|
|
1396
|
+
#
|
|
1397
|
+
# A SEPARATE VARIABLE FROM `PLOT_START_DESK`, and not a side effect of it.
|
|
1398
|
+
# The two answer different questions — *where does this desk go* and *has the
|
|
1399
|
+
# count already been decided* — and the caller that has decided the count is
|
|
1400
|
+
# not always the caller that can name a path. The supervisor is exactly that
|
|
1401
|
+
# caller: the domain names no worktree, so it sets this and leaves the path
|
|
1402
|
+
# to the script's own `Worktree root` convention.
|
|
1403
|
+
start_running=0
|
|
1404
|
+
if [ -n "${PLOT_START_ONE:-}" ]; then
|
|
1405
|
+
start_count=1
|
|
1406
|
+
else
|
|
1407
|
+
while IFS= read -r wt; do
|
|
1408
|
+
[ -n "$wt" ] || continue
|
|
1409
|
+
[ -f "$wt/.plot-worker.pid" ] || continue
|
|
1410
|
+
p=$(cat "$wt/.plot-worker.pid" 2>/dev/null) || continue
|
|
1411
|
+
[ -n "$p" ] && ps -p "$p" >/dev/null 2>&1 && start_running=$((start_running + 1))
|
|
1412
|
+
done <<EOF
|
|
1413
|
+
$(git worktree list --porcelain </dev/null 2>/dev/null | awk '/^worktree /{print $2}')
|
|
1414
|
+
EOF
|
|
1415
|
+
fi
|
|
1416
|
+
|
|
1417
|
+
# THE MACHINE'S OWN READING, one timed fork. `machine-system.ts` samples five
|
|
1418
|
+
# and divides; this takes ONE, because the decision it feeds is coarse — three
|
|
1419
|
+
# bands — and a start that spent 250 ms sampling before deciding whether the
|
|
1420
|
+
# machine is busy would be the story's own complaint reproduced by its fix.
|
|
1421
|
+
#
|
|
1422
|
+
# UNMEASURABLE IS NOT STARVED. A `date` that cannot answer in milliseconds
|
|
1423
|
+
# (BSD `date` without `%N`) leaves the cost empty, the rule reads `unmeasured`,
|
|
1424
|
+
# and an absent veto is not a refusal. The machine vetoes what it can measure.
|
|
1425
|
+
#
|
|
1426
|
+
# THE CLOCK IS BASH'S OWN `EPOCHREALTIME`, not `date` and not `python3`. Both
|
|
1427
|
+
# of those are a FORK, which is precisely the thing being timed — the
|
|
1428
|
+
# measurement would cost two of what it measures and report the sum. Bash 5
|
|
1429
|
+
# expands `EPOCHREALTIME` in-process; where it is empty (bash 4, still the
|
|
1430
|
+
# system shell on macOS) the cost stays unmeasured, which the rule reads as
|
|
1431
|
+
# `unmeasured` rather than as clear.
|
|
1432
|
+
start_cost=""
|
|
1433
|
+
if [ -n "${EPOCHREALTIME:-}" ]; then
|
|
1434
|
+
start_t0=${EPOCHREALTIME/[.,]/}
|
|
1435
|
+
git -C "$repo_root" rev-parse --git-dir >/dev/null 2>&1
|
|
1436
|
+
start_t1=${EPOCHREALTIME/[.,]/}
|
|
1437
|
+
# Microseconds to milliseconds. Integer division, so a fork faster than a
|
|
1438
|
+
# millisecond reports 0 — which is honest about a machine this clear and is
|
|
1439
|
+
# the same band `clear` covers anyway.
|
|
1440
|
+
case "$start_t0$start_t1" in
|
|
1441
|
+
*[!0-9]*) ;;
|
|
1442
|
+
*) start_cost=$(( (start_t1 - start_t0) / 1000 )) ;;
|
|
1443
|
+
esac
|
|
1444
|
+
fi
|
|
1445
|
+
|
|
1446
|
+
# THE COUNT IS THE RULE'S, and the rule is `packages/domain/src/rules/
|
|
1447
|
+
# fleet-size.ts` — imported directly, the same shape `plot-reap.sh` uses for
|
|
1448
|
+
# `reapable.ts`. Node 24 strips the types, so there is no build step between
|
|
1449
|
+
# this script and the decision it asks for, and there is no second copy of the
|
|
1450
|
+
# default, the subtraction or the machine's veto living in shell.
|
|
1451
|
+
#
|
|
1452
|
+
# A RULE THAT CANNOT BE ASKED STARTS NOTHING AND SAYS SO. Missing node, a
|
|
1453
|
+
# failed import, a module that throws all leave the answer empty. The
|
|
1454
|
+
# direction is the reaper's: silence is never permission, and here permission
|
|
1455
|
+
# would spawn detached processes.
|
|
1456
|
+
#
|
|
1457
|
+
# TWO MODULES, BECAUSE THE VERDICT AND THE COUNT ARE TWO RULES. `headroomFor`
|
|
1458
|
+
# owns what a fork cost MEANS and `fleetSize` owns what to do about it; the
|
|
1459
|
+
# count rule takes the verdict as a reading rather than deriving it, so the
|
|
1460
|
+
# thresholds have exactly one home and this block is the join.
|
|
1461
|
+
#
|
|
1462
|
+
# IMPORTED AS `.ts` WITH NO `.js` REWRITING. Node 24 strips types but does not
|
|
1463
|
+
# remap a relative specifier, so `fleet-size.ts` may only `import type` from
|
|
1464
|
+
# its neighbours — which is why the verdict arrives as a value here rather
|
|
1465
|
+
# than being computed inside the rule.
|
|
1466
|
+
start_domain="$(cd "$script_dir/../../.." 2>/dev/null && pwd)/packages/domain/src"
|
|
1467
|
+
start_rule="file://$start_domain/rules/fleet-size.ts"
|
|
1468
|
+
start_answer=$(PLOT_REQUESTED="$start_count" PLOT_RUNNING="$start_running" \
|
|
1469
|
+
PLOT_COST="$start_cost" PLOT_RULE="$start_rule" \
|
|
1470
|
+
PLOT_MACHINE="file://$start_domain/entities/machine.ts" \
|
|
1471
|
+
node --input-type=module - <<'NODE_EOF' 2>/dev/null
|
|
1472
|
+
const { fleetSize, DEFAULT_FLEET_SIZE } = await import(process.env.PLOT_RULE);
|
|
1473
|
+
const { headroomFor } = await import(process.env.PLOT_MACHINE);
|
|
1474
|
+
|
|
1475
|
+
// AN ABSENT COUNT IS THE RULE'S DEFAULT, resolved here rather than in the
|
|
1476
|
+
// shell: the number and the argument for it have one home.
|
|
1477
|
+
const requested =
|
|
1478
|
+
process.env.PLOT_REQUESTED === "" ? DEFAULT_FLEET_SIZE : Number(process.env.PLOT_REQUESTED);
|
|
1479
|
+
|
|
1480
|
+
// An UNMEASURED cost is null, never zero: zero is the fastest fork there is and
|
|
1481
|
+
// would read as the clearest possible machine.
|
|
1482
|
+
const spawnCostMs = process.env.PLOT_COST === "" ? null : Number(process.env.PLOT_COST);
|
|
1483
|
+
|
|
1484
|
+
const answer = fleetSize({
|
|
1485
|
+
requested,
|
|
1486
|
+
running: Number(process.env.PLOT_RUNNING),
|
|
1487
|
+
spawnCostMs,
|
|
1488
|
+
headroom: headroomFor(spawnCostMs),
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
process.stdout.write(`${answer.start}\t${answer.headroom}\t${answer.shortfall}`);
|
|
1492
|
+
NODE_EOF
|
|
1493
|
+
)
|
|
1494
|
+
|
|
1495
|
+
if [ -z "$start_answer" ]; then
|
|
1496
|
+
echo "plot-dispatch: --start could not ask how many agents to start — starting none." >&2
|
|
1497
|
+
echo " The rule is $start_rule" >&2
|
|
1498
|
+
echo " It needs node 24 and a readable checkout of packages/domain." >&2
|
|
1499
|
+
exit 1
|
|
1500
|
+
fi
|
|
1501
|
+
|
|
1502
|
+
start_n=${start_answer%%$'\t'*}
|
|
1503
|
+
start_rest=${start_answer#*$'\t'}
|
|
1504
|
+
start_headroom=${start_rest%%$'\t'*}
|
|
1505
|
+
start_why=${start_rest#*$'\t'}
|
|
1506
|
+
|
|
1507
|
+
echo "starting $start_n agent(s) — machine $start_headroom, $start_running already running"
|
|
1508
|
+
|
|
1509
|
+
# THE WORKER COMMAND IS ASKED ONCE, BEFORE THE LOOP. `start_worker` prints its
|
|
1510
|
+
# own "start it yourself" line per agent when the key is absent, and N
|
|
1511
|
+
# identical copies of it is a wall rather than a message. Asked here, the
|
|
1512
|
+
# refusal is one sentence naming what to configure.
|
|
1513
|
+
#
|
|
1514
|
+
# THE THREE-WAY ANSWER IS THE FAN-OUT'S OWN `worker=` FIELD, in the footer for
|
|
1515
|
+
# the same reason it is there: `agents=0` with no reason beside it is what was
|
|
1516
|
+
# printed and missed five times on 2026-08-17. A caller reading only the
|
|
1517
|
+
# summary — which is now a performer as well as a person — must be able to
|
|
1518
|
+
# tell *the machine bounded it* from *nobody has configured how to start one*.
|
|
1519
|
+
worker_cmd_declined=0
|
|
1520
|
+
start_worker_state=configured
|
|
1521
|
+
case "$("$script_dir/plot-config.sh" get "Worker command" "")" in
|
|
1522
|
+
none|NONE|None) worker_cmd_declined=1; start_worker_state=declined ;;
|
|
1523
|
+
'') start_worker_state=unconfigured ;;
|
|
1524
|
+
esac
|
|
1525
|
+
if [ "$start_worker_state" != configured ]; then
|
|
1526
|
+
echo " no worker will start — 'Worker command' is $start_worker_state in this repo's Plot Config."
|
|
1527
|
+
echo " The desks below are cut and registered; start them by hand, or set the key."
|
|
1528
|
+
fi
|
|
1529
|
+
|
|
1530
|
+
# `slug` STAYS EMPTY, and the loop reads it. A free agent belongs to no plan
|
|
1531
|
+
# — the registry sends the slug WITH the assignment, which is the same reason
|
|
1532
|
+
# `wait_for_work` skips the outlook scan for an agent that holds none.
|
|
1533
|
+
slug=""
|
|
1534
|
+
|
|
1535
|
+
start_made=0
|
|
1536
|
+
start_i=0
|
|
1537
|
+
while [ "$start_i" -lt "$start_n" ]; do
|
|
1538
|
+
start_i=$((start_i + 1))
|
|
1539
|
+
|
|
1540
|
+
# THE DESK IS DETACHED AT `origin/<main>`, AND NEITHER HALF IS INCIDENTAL.
|
|
1541
|
+
#
|
|
1542
|
+
# A free agent still needs a desk: the loop reads `${PLOT_WORKTREE:-$PWD}`
|
|
1543
|
+
# throughout and the transcript directory is derived from that path. It has
|
|
1544
|
+
# no branch to cut one from, so the base is the only thing left — which is
|
|
1545
|
+
# what the loop's own hop already does. `reset_desk` step 1 checks out
|
|
1546
|
+
# `origin/$main_branch` DETACHED before attaching the slice's branch, so a
|
|
1547
|
+
# desk that starts detached at the base is where every reset passes through
|
|
1548
|
+
# anyway, and the first hand-over is a plain `checkout -b` from it.
|
|
1549
|
+
#
|
|
1550
|
+
# DETACHED RATHER THAN ON THE DEFAULT BRANCH, and that is the guard. A tree
|
|
1551
|
+
# sitting on the default branch is one of `plot-reap.sh`'s five refusals
|
|
1552
|
+
# (`on-default-branch`) — so it is never reaped, but it is also never
|
|
1553
|
+
# measured: the refusal exists because that tree's dispatched branch was
|
|
1554
|
+
# never checked out. A detached desk reads `branch: ''`, so the refusal that
|
|
1555
|
+
# keeps it is `no-merged-pr` — unlanded work, the honest reading of a desk
|
|
1556
|
+
# holding nothing yet — and it is kept for a reason that describes it.
|
|
1557
|
+
#
|
|
1558
|
+
# Git refuses to check out one branch in two worktrees, and the main
|
|
1559
|
+
# checkout usually holds the default branch, so an attached desk could not
|
|
1560
|
+
# be cut here at all.
|
|
1561
|
+
# THE DESK PATH IS THE SCRIPT'S UNLESS A CALLER NAMED ONE.
|
|
1562
|
+
#
|
|
1563
|
+
# `PLOT_START_DESK` is how the supervisor's performer passes the desk the
|
|
1564
|
+
# DECISION named. It is an environment variable rather than a flag because
|
|
1565
|
+
# it takes exactly one path and is set by exactly one caller: a flag would
|
|
1566
|
+
# put a machine-only interface in the help text an operator reads, beside
|
|
1567
|
+
# the count they actually type. A person typing `--start 3` never names
|
|
1568
|
+
# three paths.
|
|
1569
|
+
#
|
|
1570
|
+
# It applies to ONE desk, so a run that was handed it starts one agent —
|
|
1571
|
+
# `fleetSize` is what decides the count and the performer applies its writes
|
|
1572
|
+
# one at a time, which is what keeps the decision and the writes in step.
|
|
1573
|
+
if [ -n "${PLOT_START_DESK:-}" ]; then
|
|
1574
|
+
start_wt="$PLOT_START_DESK"
|
|
1575
|
+
else
|
|
1576
|
+
start_wt="$wt_root/${wt_prefix}free-$(plot_session_id | cut -c1-8)"
|
|
1577
|
+
fi
|
|
1578
|
+
if [ -e "$start_wt" ]; then
|
|
1579
|
+
echo " skipped $start_wt — a desk of that name already exists"
|
|
1580
|
+
continue
|
|
1581
|
+
fi
|
|
1582
|
+
if [ "$dry_run" = 1 ]; then
|
|
1583
|
+
echo " would create $start_wt (detached at origin/$start_main) and start a free agent"
|
|
1584
|
+
start_made=$((start_made + 1))
|
|
1585
|
+
continue
|
|
1586
|
+
fi
|
|
1587
|
+
mkdir -p "$wt_root" 2>/dev/null || true
|
|
1588
|
+
if ! git worktree add -q --detach "$start_wt" "origin/$start_main" 2>/dev/null; then
|
|
1589
|
+
# NO REMOTE REF IS NOT A FAILURE OF THIS VERB. A fresh clone or a repo
|
|
1590
|
+
# with no remote has no `origin/<main>`; the local one is the same commit
|
|
1591
|
+
# in every case that matters, and a desk on it is still detached.
|
|
1592
|
+
if ! git worktree add -q --detach "$start_wt" "$start_main" 2>/dev/null; then
|
|
1593
|
+
echo " could not create a desk at $start_wt — skipping" >&2
|
|
1594
|
+
continue
|
|
1595
|
+
fi
|
|
1596
|
+
fi
|
|
1597
|
+
|
|
1598
|
+
# SPOTLIGHT IS TOLD NOT TO INDEX THE DESK, exactly as the loop's hop does.
|
|
1599
|
+
# A desk is a full checkout and the fleet makes and unmakes them all day;
|
|
1600
|
+
# the marker is ignored via `info/exclude` rather than `.gitignore`, because
|
|
1601
|
+
# an untracked file in a desk reads as unlanded work to
|
|
1602
|
+
# `plot-worker-state.sh` and would make every free agent look stalled.
|
|
1603
|
+
_excl="$(git -C "$start_wt" rev-parse --git-common-dir 2>/dev/null)/info/exclude"
|
|
1604
|
+
if [ -f "$_excl" ] && ! grep -qxF '.metadata_never_index' "$_excl" 2>/dev/null; then
|
|
1605
|
+
printf '%s\n' '.metadata_never_index' >> "$_excl" 2>/dev/null || true
|
|
1606
|
+
fi
|
|
1607
|
+
: > "$start_wt/.metadata_never_index" 2>/dev/null || true
|
|
1608
|
+
|
|
1609
|
+
echo " desk $start_wt (detached at origin/$start_main)"
|
|
1610
|
+
# THE EMPTY BRANCH IS THE WHOLE POINT, and `start_worker` already takes it
|
|
1611
|
+
# as a parameter: `write_agent_manifest` writes `"branch": ""`, the loop
|
|
1612
|
+
# reads `PLOT_BRANCH` empty and enters its wait rather than its prompt, and
|
|
1613
|
+
# `isAgentFree` reports the agent free with no change to `rules/free.ts`.
|
|
1614
|
+
if start_worker "" "$start_wt"; then
|
|
1615
|
+
start_made=$((start_made + 1))
|
|
1616
|
+
fi
|
|
1617
|
+
done
|
|
1618
|
+
|
|
1619
|
+
# THE SHORTFALL IS SAID HERE AND STORED NOWHERE. An operator reads it and runs
|
|
1620
|
+
# the command again; a remembered target would be the first piece of state in
|
|
1621
|
+
# a fleet whose statelessness is measured rather than assumed.
|
|
1622
|
+
[ -n "$start_why" ] && echo " $start_why"
|
|
1623
|
+
echo "summary: agents=$start_made requested=${start_count:-default} running=$start_running headroom=$start_headroom worker=$start_worker_state"
|
|
1624
|
+
exit 0
|
|
1625
|
+
fi
|
|
1626
|
+
|
|
1103
1627
|
# ---------------------------------------------------------------------------
|
|
1104
1628
|
# Migration mode: move legacy worktrees into the configured root
|
|
1105
1629
|
# ---------------------------------------------------------------------------
|
|
@@ -1312,8 +1836,7 @@ fi
|
|
|
1312
1836
|
# exactly where nothing can catch it. --allow-local is the explicit escape, and
|
|
1313
1837
|
# it is named in the refusal so an operator learns it exists when they need it.
|
|
1314
1838
|
MAIN=$(bash "$script_dir/plot-config.sh" get "Main branch")
|
|
1315
|
-
[ -n "$MAIN" ] || MAIN=$(
|
|
1316
|
-
[ -n "$MAIN" ] || MAIN="main"
|
|
1839
|
+
[ -n "$MAIN" ] || MAIN=$(default_branch)
|
|
1317
1840
|
[ -n "$offline" ] || git fetch -q origin "$MAIN" 2>/dev/null
|
|
1318
1841
|
|
|
1319
1842
|
PLAN_DIR_CFG=$("$script_dir/plot-config.sh" get "Plan directory" "docs/plans/")
|
|
@@ -1389,6 +1912,7 @@ fi
|
|
|
1389
1912
|
if [ -z "$plan_path" ]; then
|
|
1390
1913
|
if [ "$allow_local" = 1 ]; then
|
|
1391
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
|
|
1392
1916
|
else
|
|
1393
1917
|
echo "plot-dispatch: no plan for '$slug' on $gate_ref — looked in $ACTIVE_DIR_CFG and $PLAN_DIR_CFG" >&2
|
|
1394
1918
|
echo " A plan that exists only in this working tree has not been shared yet: push it first." >&2
|
|
@@ -1449,6 +1973,7 @@ case "$gate_phase" in
|
|
|
1449
1973
|
exit 1 ;;
|
|
1450
1974
|
delivered|released)
|
|
1451
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
|
|
1452
1977
|
exit 1 ;;
|
|
1453
1978
|
"")
|
|
1454
1979
|
echo "plot-dispatch: cannot read the phase of '$slug' ($gate_source)." >&2
|
|
@@ -1456,6 +1981,8 @@ case "$gate_phase" in
|
|
|
1456
1981
|
exit 1 ;;
|
|
1457
1982
|
*)
|
|
1458
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
|
|
1459
1986
|
exit 1 ;;
|
|
1460
1987
|
esac
|
|
1461
1988
|
|
|
@@ -1467,6 +1994,7 @@ case "$gate_impl" in
|
|
|
1467
1994
|
same-branch)
|
|
1468
1995
|
echo "plot-dispatch: plan '$slug' records 'Impl: same branch' — plan and code" >&2
|
|
1469
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
|
|
1470
1998
|
exit 1 ;;
|
|
1471
1999
|
other-repo)
|
|
1472
2000
|
echo "plot-dispatch: plan '$slug' records 'Impl: other repo' — implementation" >&2
|
|
@@ -1479,6 +2007,8 @@ case "$gate_impl" in
|
|
|
1479
2007
|
*)
|
|
1480
2008
|
echo "plot-dispatch: plan '$slug' records an unrecognised 'Impl:' answer" >&2
|
|
1481
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
|
|
1482
2012
|
exit 1 ;;
|
|
1483
2013
|
esac
|
|
1484
2014
|
|
|
@@ -1908,7 +2438,8 @@ write_started_record() { # $@ = branches
|
|
|
1908
2438
|
# would carry the symlink and leave the record behind.
|
|
1909
2439
|
rel=$(cd "$repo_root" && real_plan_path "$plan_file") || rel=""
|
|
1910
2440
|
if [ -z "$rel" ]; then
|
|
1911
|
-
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
|
|
1912
2443
|
return 1
|
|
1913
2444
|
fi
|
|
1914
2445
|
|
|
@@ -1924,6 +2455,9 @@ write_started_record() { # $@ = branches
|
|
|
1924
2455
|
# one. It is disposable by construction — created here, pushed, deleted.
|
|
1925
2456
|
if ! git worktree add -q -B "$bookbr" "$tmpwt" "origin/$MAIN" 2>/dev/null; then
|
|
1926
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
|
|
1927
2461
|
return 1
|
|
1928
2462
|
fi
|
|
1929
2463
|
|
|
@@ -1947,6 +2481,7 @@ write_started_record() { # $@ = branches
|
|
|
1947
2481
|
fi
|
|
1948
2482
|
append_started_line "$tmpwt/$rel" "$date" "$who" "$br" || {
|
|
1949
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
|
|
1950
2485
|
rc=1
|
|
1951
2486
|
break
|
|
1952
2487
|
}
|
|
@@ -1968,6 +2503,7 @@ write_started_record() { # $@ = branches
|
|
|
1968
2503
|
fi
|
|
1969
2504
|
else
|
|
1970
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
|
|
1971
2507
|
rc=1
|
|
1972
2508
|
fi
|
|
1973
2509
|
|
|
@@ -2412,7 +2948,7 @@ report_in_flight() { # $1=candidate branch
|
|
|
2412
2948
|
# Said once, on the last line, rather than per branch.
|
|
2413
2949
|
[ "$n" = "$((IN_FLIGHT_MAX_BRANCHES + 1))" ] && \
|
|
2414
2950
|
echo " in flight: …and $((total - IN_FLIGHT_MAX_BRANCHES)) more branches" \
|
|
2415
|
-
"— plot-
|
|
2951
|
+
"— plot-pulse for the full picture"
|
|
2416
2952
|
continue
|
|
2417
2953
|
fi
|
|
2418
2954
|
# Commas to ", " for reading; the machine-countable summary is the footer,
|