@plot-pm/board 0.16.0 → 0.16.2
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 -96
- package/package.json +2 -1
- package/plot-approve.sh +10 -1
- package/plot-config.sh +19 -22
- package/plot-deliver.sh +10 -1
- package/plot-desk-root.sh +127 -0
- package/plot-dispatch.sh +175 -60
- package/plot-fleet-scan.sh +13 -3
- package/plot-host.sh +84 -26
- package/plot-reap.sh +21 -65
- package/plot-resolve-artifact.sh +13 -19
- package/plot-tmp.sh +62 -14
- package/plot-worker-state.sh +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plot-pm/board",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
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",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"plot-config.sh",
|
|
32
32
|
"plot-default-branch.sh",
|
|
33
33
|
"plot-deliver.sh",
|
|
34
|
+
"plot-desk-root.sh",
|
|
34
35
|
"plot-dispatch.sh",
|
|
35
36
|
"plot-fleet-scan.sh",
|
|
36
37
|
"plot-host.sh",
|
package/plot-approve.sh
CHANGED
|
@@ -128,7 +128,13 @@ git rev-parse --git-dir >/dev/null 2>&1 || die "not a git repository — run thi
|
|
|
128
128
|
cfg() { bash "$script_dir/plot-config.sh" get "$1" "$2"; }
|
|
129
129
|
|
|
130
130
|
repo_root=$(git rev-parse --show-toplevel)
|
|
131
|
-
|
|
131
|
+
# The booking worktree goes under the desk root of the MAIN checkout, never of
|
|
132
|
+
# this tree: inside a desk `--show-toplevel` answers the desk, and the default
|
|
133
|
+
# `.worktrees` would resolve beneath it. No fallback — `plot-desk-root.sh`.
|
|
134
|
+
# shellcheck source=plot-desk-root.sh
|
|
135
|
+
. "$script_dir/plot-desk-root.sh"
|
|
136
|
+
main_root=$(plot_repo_root)
|
|
137
|
+
wt_root=$(plot_desk_root "$main_root") || die "cannot resolve where the booking worktree goes (see above)"
|
|
132
138
|
|
|
133
139
|
PLAN_DIR=$(cfg "Plan directory" "docs/plans/")
|
|
134
140
|
ACTIVE_DIR=$(cfg "Active index" "docs/plans/active/")
|
|
@@ -735,6 +741,9 @@ else
|
|
|
735
741
|
|
|
736
742
|
bookbr="plot/approve-$slug"
|
|
737
743
|
tmpwt="$wt_root/.plot-approve-$slug.$$"
|
|
744
|
+
# `git worktree add` creates the desk root when it is missing, so exclude it
|
|
745
|
+
# first: a booking run must not leave `.worktrees/` untracked in `git status`.
|
|
746
|
+
plot_exclude_desk_root "$main_root"
|
|
738
747
|
# -B: a leftover branch from an earlier failed run must not block this one.
|
|
739
748
|
# It is disposable by construction — created here, pushed, deleted.
|
|
740
749
|
git worktree add -q -B "$bookbr" "$tmpwt" "origin/$MAIN" 2>/dev/null \
|
package/plot-config.sh
CHANGED
|
@@ -27,12 +27,7 @@
|
|
|
27
27
|
# Project board | Branch prefixes | Plan directory | Active index |
|
|
28
28
|
# Delivered index | Sprint directory | Story directory | Story index |
|
|
29
29
|
# Plan template | Worker prompt template | Main branch | Board command
|
|
30
|
-
# Worktree root
|
|
31
|
-
# plot-dispatch.sh; default is the repo's PARENT, which
|
|
32
|
-
# scatters `plot-wt-*` beside the checkout. An absolute
|
|
33
|
-
# path is taken as given; a relative one resolves against
|
|
34
|
-
# the repo root, so `.worktrees` gathers them inside it.
|
|
35
|
-
# The default is kept for repos that never set it.
|
|
30
|
+
# Worktree root the desk root; see its entry under the agent keys below.
|
|
36
31
|
# Worker bound seconds a single prompt run may take in the worker loop
|
|
37
32
|
# before it is ended and the worker exits (no hop). Read by
|
|
38
33
|
# plot-worker-loop.sh; default 3600 (~1h), `0` disables it.
|
|
@@ -99,24 +94,26 @@
|
|
|
99
94
|
# project-owned and reviewed like this one.
|
|
100
95
|
# Absent or empty = no change, so an adopting project that
|
|
101
96
|
# sets nothing behaves exactly as today.
|
|
102
|
-
# Worktree root where /plot-dispatch creates fleet
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
97
|
+
# Worktree root the desk root: where /plot-dispatch creates fleet
|
|
98
|
+
# worktrees and the board writes its action records. A
|
|
99
|
+
# relative value resolves against the MAIN checkout, an
|
|
100
|
+
# absolute one is taken as given. Absent or empty =
|
|
101
|
+
# `<repo>/.worktrees`. The rule is `deskRoot`, asked through
|
|
102
|
+
# `plot-desk-root.sh`; no script resolves it itself. Desks
|
|
103
|
+
# carry no prefix; `plot-wt-*` desks an older dispatch made
|
|
104
|
+
# beside the repo stay there. Every "which worktree holds
|
|
105
|
+
# this branch" read asks `git worktree list` instead.
|
|
110
106
|
# Agent-runner keys (optional; Plot hardcodes no agent tooling, Principle 5):
|
|
111
107
|
# Worker command how /plot-dispatch runs an agent headless on a worktree.
|
|
112
|
-
# `
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
# the
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
108
|
+
# /plot-init writes `PLOT_UNATTENDED=1 plot-worker-loop.sh`;
|
|
109
|
+
# the bare name resolves to the loop beside
|
|
110
|
+
# plot-dispatch.sh, which puts its own directory first on
|
|
111
|
+
# PATH. A free agent (`--start`) refuses a command that
|
|
112
|
+
# does not run the loop, because only the loop waits for a
|
|
113
|
+
# slice. Absent = not set up: nothing starts, and
|
|
114
|
+
# /plot-dispatch offers to write the loop. `none` = asked,
|
|
115
|
+
# and this repo starts workers by hand — a DELIBERATE
|
|
116
|
+
# absence, never run as a command.
|
|
120
117
|
# Approve command how the board runs `/plot-approve <slug>`; the prompt is
|
|
121
118
|
# appended as one argument. Absent = the board's Approve
|
|
122
119
|
# button renders disabled, naming this key as the fix.
|
package/plot-deliver.sh
CHANGED
|
@@ -81,7 +81,13 @@ git rev-parse --git-dir >/dev/null 2>&1 || die "not a git repository — run thi
|
|
|
81
81
|
cfg() { bash "$script_dir/plot-config.sh" get "$1" "$2"; }
|
|
82
82
|
|
|
83
83
|
repo_root=$(git rev-parse --show-toplevel)
|
|
84
|
-
|
|
84
|
+
# The booking worktree goes under the desk root of the MAIN checkout, never of
|
|
85
|
+
# this tree: inside a desk `--show-toplevel` answers the desk, and the default
|
|
86
|
+
# `.worktrees` would resolve beneath it. No fallback — `plot-desk-root.sh`.
|
|
87
|
+
# shellcheck source=plot-desk-root.sh
|
|
88
|
+
. "$script_dir/plot-desk-root.sh"
|
|
89
|
+
main_root=$(plot_repo_root)
|
|
90
|
+
wt_root=$(plot_desk_root "$main_root") || die "cannot resolve where the booking worktree goes (see above)"
|
|
85
91
|
|
|
86
92
|
PLAN_DIR=$(cfg "Plan directory" "docs/plans/")
|
|
87
93
|
ACTIVE_DIR=$(cfg "Active index" "docs/plans/active/")
|
|
@@ -635,6 +641,9 @@ git fetch -q origin "$MAIN" 2>/dev/null
|
|
|
635
641
|
|
|
636
642
|
bookbr="plot/deliver-$slug"
|
|
637
643
|
tmpwt="$wt_root/.plot-deliver-$slug.$$"
|
|
644
|
+
# `git worktree add` creates the desk root when it is missing, so exclude it
|
|
645
|
+
# first: a booking run must not leave `.worktrees/` untracked in `git status`.
|
|
646
|
+
plot_exclude_desk_root "$main_root"
|
|
638
647
|
# -B: a leftover branch from an earlier failed run must not block this one.
|
|
639
648
|
git worktree add -q -B "$bookbr" "$tmpwt" "origin/$MAIN" 2>/dev/null \
|
|
640
649
|
|| die "could not prepare a booking worktree at $tmpwt.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# The ONE answer to "where do desks and action records go?" — sourced, not run.
|
|
3
|
+
#
|
|
4
|
+
# Every script that resolved this privately asks here instead. Nine sites
|
|
5
|
+
# computed the rule on 2026-10-01 and two of them already answered `.worktrees`
|
|
6
|
+
# while seven answered the checkout's parent, so one of the two populations was
|
|
7
|
+
# always writing somewhere the other did not read.
|
|
8
|
+
#
|
|
9
|
+
# The rule itself is `deskRoot` in the domain, asked through
|
|
10
|
+
# `board/plot-desk-root.mjs`, per *A Shell Script Asks The Domain*: every caller
|
|
11
|
+
# runs once per operator command, which is the cost rule's permitted case.
|
|
12
|
+
#
|
|
13
|
+
# NO CALLER KEEPS A FALLBACK DEFAULT. When the bundle cannot answer — no `node`,
|
|
14
|
+
# a missing file — the caller stops with the reason. A silent fallback is a
|
|
15
|
+
# second default, and two defaults are the defect this removes.
|
|
16
|
+
#
|
|
17
|
+
# plot_repo_root → the MAIN checkout, from anywhere
|
|
18
|
+
# plot_desk_root [repo [configured]] → the desk root; exit 3 when unaskable
|
|
19
|
+
# plot_exclude_desk_root [repo] → keep it out of `git status`; never fails
|
|
20
|
+
#
|
|
21
|
+
# shellcheck shell=bash
|
|
22
|
+
|
|
23
|
+
# The MAIN checkout, from a desk as readily as from the checkout itself.
|
|
24
|
+
#
|
|
25
|
+
# `--show-toplevel` answers the DESK inside a linked worktree, so with
|
|
26
|
+
# `.worktrees` as the default it would resolve `<desk>/.worktrees` and place
|
|
27
|
+
# every tree under a root nothing else reads. `plot-reap.sh:440-447` recorded
|
|
28
|
+
# that failure on 2026-09-10, when every tree read as unplaceable.
|
|
29
|
+
#
|
|
30
|
+
# The parent of `--git-common-dir` is the main checkout from anywhere, because
|
|
31
|
+
# every linked worktree shares that one directory. It is asked FIRST and
|
|
32
|
+
# `--show-toplevel` is the fallback, since the common dir is also correct in a
|
|
33
|
+
# non-worktree checkout: there `.git` is a real directory in the root.
|
|
34
|
+
#
|
|
35
|
+
# PHYSICAL, through `pwd -P`: `git worktree list` and `--show-toplevel` print
|
|
36
|
+
# resolved paths, and a desk root composed from a logical one (`/tmp` against
|
|
37
|
+
# `/private/tmp` on macOS) is a prefix no worktree path ever starts with.
|
|
38
|
+
plot_repo_root() {
|
|
39
|
+
local common root=''
|
|
40
|
+
common=$(git rev-parse --git-common-dir 2>/dev/null) && [ -n "$common" ] && {
|
|
41
|
+
common=$(cd -- "$common" 2>/dev/null && pwd -P) || common=''
|
|
42
|
+
[ -n "$common" ] && root=$(dirname -- "$common")
|
|
43
|
+
}
|
|
44
|
+
[ -n "$root" ] || root=$(git rev-parse --show-toplevel 2>/dev/null) || root=$(pwd)
|
|
45
|
+
printf '%s\n' "$root"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Resolved ONCE at source time, the way `plot-pr-merged.sh:105` resolves its
|
|
49
|
+
# own bundle. Computing it inside a function reads `BASH_SOURCE[0]` at call
|
|
50
|
+
# time, which is the caller's file once the function has been exported.
|
|
51
|
+
_plot_desk_root_mjs="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/board/plot-desk-root.mjs"
|
|
52
|
+
_plot_desk_root_config="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/plot-config.sh"
|
|
53
|
+
|
|
54
|
+
# The desk root for a repository, or exit 3 with the reason on stderr.
|
|
55
|
+
#
|
|
56
|
+
# A second argument is a value the caller was handed in place of the key — a
|
|
57
|
+
# `--worktrees DIR` flag — and goes through the same rule; without one the
|
|
58
|
+
# `Worktree root` key is read.
|
|
59
|
+
#
|
|
60
|
+
# READ THE EXIT CODE. A caller that treats an empty answer as a location would
|
|
61
|
+
# compose paths against the filesystem root.
|
|
62
|
+
plot_desk_root() {
|
|
63
|
+
local repo="${1:-}" bundle="$_plot_desk_root_mjs" configured answer
|
|
64
|
+
[ -n "$repo" ] || repo=$(plot_repo_root)
|
|
65
|
+
if [ ! -f "$bundle" ]; then
|
|
66
|
+
printf 'plot: cannot resolve the desk root: %s is missing\n' "$bundle" >&2
|
|
67
|
+
return 3
|
|
68
|
+
fi
|
|
69
|
+
if [ "$#" -ge 2 ]; then
|
|
70
|
+
configured="$2"
|
|
71
|
+
else
|
|
72
|
+
# Read from the TARGET repository, never the ambient one: `plot-config.sh`
|
|
73
|
+
# walks up from its cwd, so a caller asking about another checkout would
|
|
74
|
+
# otherwise get this one's key.
|
|
75
|
+
# A read that fails is a refusal, never the absent row: reading it as empty
|
|
76
|
+
# would answer the default for a repository that configured another root.
|
|
77
|
+
configured=$(cd -- "$repo" 2>/dev/null && bash "$_plot_desk_root_config" get "Worktree root" "" 2>/dev/null) || {
|
|
78
|
+
printf 'plot: cannot resolve the desk root: the Worktree root key in %s could not be read\n' "$repo" >&2
|
|
79
|
+
return 3
|
|
80
|
+
}
|
|
81
|
+
fi
|
|
82
|
+
answer=$(node "$bundle" "$repo" "$configured" 2>&1) || {
|
|
83
|
+
printf 'plot: cannot resolve the desk root: %s\n' "$answer" >&2
|
|
84
|
+
return 3
|
|
85
|
+
}
|
|
86
|
+
[ -n "$answer" ] || {
|
|
87
|
+
printf 'plot: the desk root rule answered nothing for %s\n' "$repo" >&2
|
|
88
|
+
return 3
|
|
89
|
+
}
|
|
90
|
+
printf '%s\n' "$answer"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Keep the desk root out of `git status`, when it lies inside the repository.
|
|
94
|
+
#
|
|
95
|
+
# The line goes into the COMMON git directory's `info/exclude`: git reads that
|
|
96
|
+
# file from the common directory only, so a linked worktree's private gitdir is
|
|
97
|
+
# the wrong place and a desk writing there would exclude nothing. The write is
|
|
98
|
+
# idempotent, and `info/exclude` is never committed.
|
|
99
|
+
#
|
|
100
|
+
# Called by whoever CREATES the directory. Best-effort and always exit 0: a
|
|
101
|
+
# failure leaves untracked files in a listing, which is untidy, while refusing
|
|
102
|
+
# to create a desk over it would stop the work.
|
|
103
|
+
plot_exclude_desk_root() {
|
|
104
|
+
local repo="${1:-}" bundle="$_plot_desk_root_mjs" line common exclude configured
|
|
105
|
+
[ -n "$repo" ] || repo=$(plot_repo_root)
|
|
106
|
+
[ -f "$bundle" ] || return 0
|
|
107
|
+
configured=$(cd -- "$repo" 2>/dev/null && bash "$_plot_desk_root_config" get "Worktree root" "" 2>/dev/null) || configured=''
|
|
108
|
+
line=$(node "$bundle" --exclude-line "$repo" "$configured" 2>/dev/null) || return 0
|
|
109
|
+
# Empty means the root lies outside the repository, which needs no line.
|
|
110
|
+
[ -n "$line" ] || return 0
|
|
111
|
+
# Already ignored, by a `.gitignore` line /plot-init wrote or by an earlier
|
|
112
|
+
# run. A second rule for a path already ignored is noise in a file people read.
|
|
113
|
+
( cd -- "$repo" 2>/dev/null && git check-ignore -q ".$line" ) && return 0
|
|
114
|
+
common=$(cd -- "$repo" 2>/dev/null && git rev-parse --git-common-dir 2>/dev/null) || return 0
|
|
115
|
+
[ -n "$common" ] || return 0
|
|
116
|
+
case "$common" in /*) ;; *) common="$repo/$common" ;; esac
|
|
117
|
+
exclude="$common/info/exclude"
|
|
118
|
+
if [ -f "$exclude" ] && grep -qxF "$line" "$exclude" 2>/dev/null; then return 0; fi
|
|
119
|
+
mkdir -p "$common/info" 2>/dev/null || return 0
|
|
120
|
+
# A file not ending in a newline would otherwise join this line to its last.
|
|
121
|
+
if [ -s "$exclude" ] && [ "$(tail -c 1 "$exclude" 2>/dev/null)" != "" ]; then
|
|
122
|
+
printf '\n%s\n' "$line" >> "$exclude" 2>/dev/null || true
|
|
123
|
+
else
|
|
124
|
+
printf '%s\n' "$line" >> "$exclude" 2>/dev/null || true
|
|
125
|
+
fi
|
|
126
|
+
return 0
|
|
127
|
+
}
|
package/plot-dispatch.sh
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
# hand each a queued slice with nobody touching a desk. The count
|
|
15
15
|
# is a REQUEST: a machine at its bound answers with fewer and says
|
|
16
16
|
# so, and the shortfall is reported rather than remembered.
|
|
17
|
+
# A `Worker command` that does not run plot-worker-loop.sh
|
|
18
|
+
# refuses (exit 1, `worker=no-loop`): only the loop waits.
|
|
17
19
|
# --restart <br>
|
|
18
20
|
# start a worker on <br>, which already holds a claim — the
|
|
19
21
|
# counterpart to --stop, and the only way to hand a stopped
|
|
@@ -217,40 +219,37 @@ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
|
217
219
|
# WHERE THE WORKTREES LIVE, and by what name
|
|
218
220
|
# ---------------------------------------------------------------------------
|
|
219
221
|
#
|
|
220
|
-
# Two facts
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
# directories — it is a workaround for sharing a parent with other projects.
|
|
224
|
-
# Under a dedicated `Worktree root:` the directory already says what they are,
|
|
225
|
-
# so the prefix answers a question nobody is asking and is dropped. The legacy
|
|
226
|
-
# default keeps it, where it is still doing its job. Two conventions coexist
|
|
227
|
-
# permanently, and that is the intended outcome, not a transition cost.
|
|
222
|
+
# Two facts: the root directory the worktrees sit in, and the prefix their
|
|
223
|
+
# directory names carry. The root is the desk root, `deskRoot`'s answer asked
|
|
224
|
+
# through `plot-desk-root.sh`:
|
|
228
225
|
#
|
|
229
|
-
# `Worktree root:` absent
|
|
230
|
-
# relative value
|
|
231
|
-
# absolute value
|
|
226
|
+
# `Worktree root:` absent or empty → <main checkout>/.worktrees
|
|
227
|
+
# relative value → <main checkout>/<value>
|
|
228
|
+
# absolute value → <value> as given
|
|
229
|
+
#
|
|
230
|
+
# The prefix is always empty. `plot-wt-` made Plot's worktrees identifiable
|
|
231
|
+
# among unrelated directories in the checkout's parent; the desk root is a
|
|
232
|
+
# directory Plot owns, so the directory already says what they are. Desks a
|
|
233
|
+
# dispatch created under the old default keep their `plot-wt-` names and their
|
|
234
|
+
# place: every read of "which worktree holds this branch" asks `git worktree
|
|
235
|
+
# list`, and `plot-reap.sh` still recognises the legacy path.
|
|
236
|
+
#
|
|
237
|
+
# The root is the MAIN checkout's, wherever this runs: inside a desk,
|
|
238
|
+
# `--show-toplevel` answers the desk and `.worktrees` would resolve beneath it.
|
|
239
|
+
# There is NO FALLBACK: when the rule cannot be asked, dispatch stops with the
|
|
240
|
+
# reason rather than compute a second default.
|
|
232
241
|
#
|
|
233
242
|
# THIS FUNCTION ONLY COMPOSES A ROOT AND A PREFIX. It is the CREATION side. Every
|
|
234
243
|
# read of "which worktree holds this branch" asks `git worktree list` instead —
|
|
235
244
|
# see THE HELD-BRANCH GATE. A second naming convention gives path-guessing a
|
|
236
245
|
# second way to be wrong, so path-guessing is confined to creation alone.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return
|
|
245
|
-
fi
|
|
246
|
-
case "$configured" in
|
|
247
|
-
/*) wt_root="$configured" ;; # absolute: taken as given
|
|
248
|
-
*) wt_root="$rr/$configured" ;; # relative: against the repo root
|
|
249
|
-
esac
|
|
250
|
-
# Normalise away a trailing slash so composed paths never double it. The
|
|
251
|
-
# directory may not exist yet (created on first dispatch), so this is pure
|
|
252
|
-
# string work, not a `cd`.
|
|
253
|
-
wt_root="${wt_root%/}"
|
|
246
|
+
# shellcheck source=plot-desk-root.sh
|
|
247
|
+
. "$script_dir/plot-desk-root.sh"
|
|
248
|
+
resolve_wt_root() { # sets globals wt_root, wt_prefix; exits 3 when unaskable
|
|
249
|
+
wt_root=$(plot_desk_root "$(plot_repo_root)") || {
|
|
250
|
+
echo "plot-dispatch: cannot resolve where the worktrees go — nothing was dispatched." >&2
|
|
251
|
+
exit 3
|
|
252
|
+
}
|
|
254
253
|
wt_prefix=""
|
|
255
254
|
}
|
|
256
255
|
|
|
@@ -342,7 +341,7 @@ while [ $# -gt 0 ]; do
|
|
|
342
341
|
# `--release` pushed it from 78 to 91. Two records
|
|
343
342
|
# of one fact, and nothing compares them — a stale number here silently
|
|
344
343
|
# truncates the help rather than failing, so it is checked by a test.
|
|
345
|
-
-h|--help) sed -n '2,
|
|
344
|
+
-h|--help) sed -n '2,93p' "$0"; exit 0 ;;
|
|
346
345
|
*) slug="$1" ;;
|
|
347
346
|
esac
|
|
348
347
|
shift
|
|
@@ -1022,6 +1021,46 @@ handover_refusal() { # $1=branch $2=worktree $3=state → 0 may hand over, 1 ref
|
|
|
1022
1021
|
return 0
|
|
1023
1022
|
}
|
|
1024
1023
|
|
|
1024
|
+
# WHICH COMMAND STARTS THIS AGENT is the domain's answer: `startCommand`, asked
|
|
1025
|
+
# through `board/plot-start-command.mjs`. An absent `Worker command` is not set
|
|
1026
|
+
# up and starts nothing, `none` is declined, and a free agent refuses a command
|
|
1027
|
+
# that does not run the loop (#1124). The loop's name and the value that runs it
|
|
1028
|
+
# are this script's readings; the rule names no script.
|
|
1029
|
+
#
|
|
1030
|
+
# THE VALUE IS THE BARE NAME, `PLOT_UNATTENDED=1 plot-worker-loop.sh`, the one
|
|
1031
|
+
# `/plot-init` writes: a plugin path carries a version and an absolute path
|
|
1032
|
+
# carries a machine. The launch puts this script's directory first on PATH, so
|
|
1033
|
+
# the bare name resolves to the loop shipped beside it.
|
|
1034
|
+
#
|
|
1035
|
+
# It sets `start_cmd_verb` (`run`, `unconfigured`, `declined`, `refused` or
|
|
1036
|
+
# `unaskable`), `start_cmd_run`, and `start_cmd_why` / `start_cmd_repair` where
|
|
1037
|
+
# they apply. `unaskable` is a bundle that could not answer, which starts
|
|
1038
|
+
# nothing.
|
|
1039
|
+
ask_start_command() { # $1 = free | assigned
|
|
1040
|
+
local answer rc
|
|
1041
|
+
start_cmd_verb="" start_cmd_run="" start_cmd_why="" start_cmd_repair=""
|
|
1042
|
+
start_cmd_configured=$("$script_dir/plot-config.sh" get "Worker command" "")
|
|
1043
|
+
start_cmd_bundle="$script_dir/board/plot-start-command.mjs"
|
|
1044
|
+
answer=$(printf '%s' "$start_cmd_configured" | node "$start_cmd_bundle" "$1" \
|
|
1045
|
+
plot-worker-loop.sh "PLOT_UNATTENDED=1 plot-worker-loop.sh" 2>/dev/null)
|
|
1046
|
+
rc=$?
|
|
1047
|
+
case "$rc:$answer" in
|
|
1048
|
+
0:run$'\t'*) start_cmd_verb=run; start_cmd_run=${answer#run$'\t'} ;;
|
|
1049
|
+
0:unconfigured$'\t'*)
|
|
1050
|
+
start_cmd_verb=unconfigured
|
|
1051
|
+
start_cmd_why="no 'Worker command' is configured"
|
|
1052
|
+
start_cmd_repair=${answer#unconfigured$'\t'}
|
|
1053
|
+
;;
|
|
1054
|
+
0:declined) start_cmd_verb=declined ;;
|
|
1055
|
+
3:*)
|
|
1056
|
+
start_cmd_verb=refused
|
|
1057
|
+
start_cmd_why=${answer%%$'\n'*}
|
|
1058
|
+
start_cmd_repair=${answer#*$'\n'}
|
|
1059
|
+
;;
|
|
1060
|
+
*) start_cmd_verb=unaskable; start_cmd_why="the rule's bundle $start_cmd_bundle answered exit $rc" ;;
|
|
1061
|
+
esac
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1025
1064
|
start_worker() {
|
|
1026
1065
|
local branch="$1" wt="$2"
|
|
1027
1066
|
local cmd
|
|
@@ -1084,11 +1123,26 @@ start_worker() {
|
|
|
1084
1123
|
return 1
|
|
1085
1124
|
fi
|
|
1086
1125
|
|
|
1087
|
-
cmd=$("$script_dir/plot-config.sh" get "Worker command" "")
|
|
1088
1126
|
# `none` means "asked, and this repo starts them by hand". Running it would
|
|
1089
1127
|
# spawn a worker per branch that fails with `none: command not found` — a
|
|
1090
|
-
# deliberate answer turned into N crashed workers.
|
|
1091
|
-
|
|
1128
|
+
# deliberate answer turned into N crashed workers. An absent key starts
|
|
1129
|
+
# nothing either, and its line names the value to set.
|
|
1130
|
+
ask_start_command "$([ -n "$branch" ] && echo assigned || echo free)"
|
|
1131
|
+
case "$start_cmd_verb" in
|
|
1132
|
+
run) cmd="$start_cmd_run" ;;
|
|
1133
|
+
declined) cmd="" ;;
|
|
1134
|
+
unconfigured)
|
|
1135
|
+
echo " worktree ready — ${start_cmd_why}, so nothing started:"
|
|
1136
|
+
echo " $start_cmd_repair."
|
|
1137
|
+
echo " cd $wt # ${branch:+branch $branch is claimed and waiting}"
|
|
1138
|
+
return 1
|
|
1139
|
+
;;
|
|
1140
|
+
*)
|
|
1141
|
+
echo " refusing to start ${branch:-a free agent} — ${start_cmd_why}" >&2
|
|
1142
|
+
[ -n "$start_cmd_repair" ] && echo " Repair: $start_cmd_repair." >&2
|
|
1143
|
+
return 1
|
|
1144
|
+
;;
|
|
1145
|
+
esac
|
|
1092
1146
|
if [ -z "$cmd" ]; then
|
|
1093
1147
|
# Not an error: Plot deliberately hardcodes no agent tooling (Principle 5).
|
|
1094
1148
|
# Word it as the next step rather than a failure, or a first run reads as
|
|
@@ -1099,11 +1153,7 @@ start_worker() {
|
|
|
1099
1153
|
# in the summary, because per-branch output is exactly where it was missed
|
|
1100
1154
|
# five times on 2026-08-17. Saying it in both places would train the reader
|
|
1101
1155
|
# to skip both.
|
|
1102
|
-
|
|
1103
|
-
echo " worktree ready — start it yourself:"
|
|
1104
|
-
else
|
|
1105
|
-
echo " worktree ready — no 'Worker command' configured, so start it yourself:"
|
|
1106
|
-
fi
|
|
1156
|
+
echo " worktree ready — start it yourself:"
|
|
1107
1157
|
echo " cd $wt # branch $branch is claimed and waiting"
|
|
1108
1158
|
return 1
|
|
1109
1159
|
fi
|
|
@@ -1448,7 +1498,8 @@ start_worker() {
|
|
|
1448
1498
|
PLOT_BUILD_MONITOR="$build_monitor" \
|
|
1449
1499
|
PLOT_EXIT_FILE="$wt/.plot-worker.exit" PLOT_PID_FILE="$wt/.plot-worker.pid" \
|
|
1450
1500
|
PLOT_WRAPPER_PID_FILE="$wt/.plot-worker.wrapper.pid" \
|
|
1451
|
-
|
|
1501
|
+
PLOT_SCRIPT_DIR="$script_dir" \
|
|
1502
|
+
nohup sh -c 'printf "%s" "$$" > "$PLOT_WRAPPER_PID_FILE"; wmon=""; amon=""; bmon=""; if [ -n "$PLOT_WORKER_MONITOR" ]; then "$PLOT_WORKER_MONITOR" & wmon=$!; fi; if [ -n "$PLOT_AGENT_MONITOR" ]; then "$PLOT_AGENT_MONITOR" & amon=$!; fi; if [ -n "$PLOT_BUILD_MONITOR" ]; then "$PLOT_BUILD_MONITOR" & bmon=$!; fi; PATH="$PLOT_SCRIPT_DIR:$PATH"; export PATH; ( '"$cmd"' ) & agent=$!; printf "%s" "$agent" > "$PLOT_PID_FILE"; if [ -f "$PLOT_MANIFEST_FILE" ]; then awk -v pid="$agent" -v started="$PLOT_STAMP_STARTED" -v wrapper="$$" -v wmon="$wmon" -v amon="$amon" -v bmon="$bmon" '"'"'
|
|
1452
1503
|
BEGIN { relaunch = 0; count = 1; stamped = 0 }
|
|
1453
1504
|
FNR == NR {
|
|
1454
1505
|
if ($0 ~ /^ "pid": "[^"]*",$/) {
|
|
@@ -1591,9 +1642,13 @@ charter_file_for() { # $1 = agent name → prints the path it would read
|
|
|
1591
1642
|
# inspectable and stoppable even if the plan was since delivered or rejected.
|
|
1592
1643
|
# Refusing to show a running worker because of a phase change would strand it.
|
|
1593
1644
|
repo_root_early=$(git rev-parse --show-toplevel)
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1645
|
+
# Only the two verbs that enumerate desks ask for the root here; every other
|
|
1646
|
+
# path asks where it composes one, so its own refusals answer first.
|
|
1647
|
+
if [ "$mode" = status ] || [ "$mode" = stop ]; then
|
|
1648
|
+
resolve_wt_root
|
|
1649
|
+
wt_root_early="$wt_root"
|
|
1650
|
+
wt_prefix_early="$wt_prefix"
|
|
1651
|
+
fi
|
|
1597
1652
|
|
|
1598
1653
|
# States: "running <pid>" | "finished <pid>" | "waiting <pid> (answer it)"
|
|
1599
1654
|
# | "stalled <pid> (work unfinished)" | "failed <pid> (exit N)"
|
|
@@ -1725,11 +1780,30 @@ if [ "$mode" = "stop" ]; then
|
|
|
1725
1780
|
case "$st" in
|
|
1726
1781
|
running*)
|
|
1727
1782
|
pid=${st#running }
|
|
1728
|
-
|
|
1783
|
+
# THE WHOLE PROCESS GROUP, NOT ONE PID. Measured 2026-09-30 (#1084):
|
|
1784
|
+
# `--stop` signalled the wrapper, the group leader, and the loop, the
|
|
1785
|
+
# prompt shell, `claude` and its children survived reparented to pid 1;
|
|
1786
|
+
# `kill -TERM -<pgid>` ended all of them. The pid is the fallback when the
|
|
1787
|
+
# group cannot be read, or is this script's own group: a worker started
|
|
1788
|
+
# without job control shares its starter's group, and signalling that
|
|
1789
|
+
# group would stop the caller with it.
|
|
1790
|
+
stop_pgid=$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ')
|
|
1791
|
+
stop_own_pgid=$(ps -o pgid= -p "$$" 2>/dev/null | tr -d ' ')
|
|
1792
|
+
stop_target="$pid"
|
|
1793
|
+
case "$stop_pgid" in
|
|
1794
|
+
''|*[!0-9]*|0|1) ;;
|
|
1795
|
+
*) [ "$stop_pgid" != "$stop_own_pgid" ] && stop_target="-$stop_pgid" ;;
|
|
1796
|
+
esac
|
|
1797
|
+
kill -TERM -- "$stop_target" 2>/dev/null && echo "stopped $stop_branch (pid $pid)" \
|
|
1729
1798
|
|| { echo "plot-dispatch: could not stop pid $pid — it may have exited between the read and the signal, or belong to another user." >&2
|
|
1730
1799
|
echo " Check it: ps -p $pid -o pid=,stat=,command=" >&2
|
|
1731
1800
|
echo " Nothing else was written; the worktree and the claim stand." >&2
|
|
1732
1801
|
exit 1; }
|
|
1802
|
+
if [ "$stop_target" = "$pid" ]; then
|
|
1803
|
+
echo " signalled pid $pid alone — its process group is unreadable or is this run's own"
|
|
1804
|
+
else
|
|
1805
|
+
echo " signalled its whole process group $stop_pgid"
|
|
1806
|
+
fi
|
|
1733
1807
|
# The worktree and its claim are left in place: the branch is still taken,
|
|
1734
1808
|
# and deleting either would be the kind of write this design avoids.
|
|
1735
1809
|
echo " worktree kept at $wt — the claim stands until you release it:"
|
|
@@ -2087,7 +2161,6 @@ if [ "$mode" = "start" ]; then
|
|
|
2087
2161
|
# about any plan, so there is no plan whose phase could refuse it. A gate on a
|
|
2088
2162
|
# slug this verb never takes would refuse every call.
|
|
2089
2163
|
repo_root="$repo_root_early"
|
|
2090
|
-
resolve_wt_root "$repo_root"
|
|
2091
2164
|
|
|
2092
2165
|
# THE DEFAULT BRANCH, by the same three steps the fan-out takes below — the
|
|
2093
2166
|
# config key, then origin's own HEAD, then `main`. Resolved here because the
|
|
@@ -2233,6 +2306,38 @@ EOF
|
|
|
2233
2306
|
start_headroom=${start_rest%%$'\t'*}
|
|
2234
2307
|
start_why=${start_rest#*$'\t'}
|
|
2235
2308
|
|
|
2309
|
+
# A FREE AGENT NEEDS THE LOOP, so a configured command that does not run it
|
|
2310
|
+
# refuses here, before any desk is cut. A free agent starts with an empty
|
|
2311
|
+
# `PLOT_BRANCH`, and only `plot-worker-loop.sh` waits for the registry to hand
|
|
2312
|
+
# it a slice; any other command runs its prompt at once and exits. Measured
|
|
2313
|
+
# 2026-10-01 (#1124): a plain `claude -p "… $PLOT_BRANCH …"` received
|
|
2314
|
+
# "Implementiere den Branch in nach dem Plan", exited within 30 s, and
|
|
2315
|
+
# `--start` reported `agents=1`. A branch dispatch is not affected: a worker
|
|
2316
|
+
# given a branch works on any command.
|
|
2317
|
+
#
|
|
2318
|
+
# THE DECISION IS THE DOMAIN'S: `ask_start_command` asks `startCommand`
|
|
2319
|
+
# through its bundle. A refusal names the defect and the repair; a bundle that
|
|
2320
|
+
# cannot answer starts nothing, as the fleet-size ask above does.
|
|
2321
|
+
# `worker=no-loop` is the footer word the performer reads, the way it reads
|
|
2322
|
+
# `unconfigured` and `declined`.
|
|
2323
|
+
ask_start_command free
|
|
2324
|
+
case "$start_cmd_verb" in
|
|
2325
|
+
run|declined|unconfigured) ;;
|
|
2326
|
+
refused)
|
|
2327
|
+
echo "plot-dispatch: --start refuses — ${start_cmd_why}." >&2
|
|
2328
|
+
echo " configured: $start_cmd_configured" >&2
|
|
2329
|
+
echo " Repair: ${start_cmd_repair}." >&2
|
|
2330
|
+
echo " A branch dispatch (--restart <branch>) starts a worker on any command." >&2
|
|
2331
|
+
echo "summary: agents=0 requested=${start_count:-default} running=$start_running headroom=$start_headroom worker=no-loop"
|
|
2332
|
+
exit 1
|
|
2333
|
+
;;
|
|
2334
|
+
*)
|
|
2335
|
+
echo "plot-dispatch: --start could not ask which command starts an agent — starting none." >&2
|
|
2336
|
+
echo " ${start_cmd_why}." >&2
|
|
2337
|
+
exit 1
|
|
2338
|
+
;;
|
|
2339
|
+
esac
|
|
2340
|
+
|
|
2236
2341
|
echo "starting $start_n agent(s) — machine $start_headroom, $start_running already running"
|
|
2237
2342
|
|
|
2238
2343
|
# THE WORKER COMMAND IS ASKED ONCE, BEFORE THE LOOP. `start_worker` prints its
|
|
@@ -2246,14 +2351,18 @@ EOF
|
|
|
2246
2351
|
# summary — which is now a performer as well as a person — must be able to
|
|
2247
2352
|
# tell *the machine bounded it* from *nobody has configured how to start one*.
|
|
2248
2353
|
worker_cmd_declined=0
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2354
|
+
case "$start_cmd_verb" in
|
|
2355
|
+
declined) worker_cmd_declined=1; start_worker_state=declined ;;
|
|
2356
|
+
unconfigured) start_worker_state=unconfigured ;;
|
|
2357
|
+
*) start_worker_state=configured ;;
|
|
2253
2358
|
esac
|
|
2254
2359
|
if [ "$start_worker_state" != configured ]; then
|
|
2255
2360
|
echo " no worker will start — 'Worker command' is $start_worker_state in this repo's Plot Config."
|
|
2256
|
-
|
|
2361
|
+
if [ "$start_worker_state" = unconfigured ]; then
|
|
2362
|
+
echo " The desks below are cut and registered. Repair: $start_cmd_repair."
|
|
2363
|
+
else
|
|
2364
|
+
echo " The desks below are cut and registered; start them by hand, or set the key."
|
|
2365
|
+
fi
|
|
2257
2366
|
fi
|
|
2258
2367
|
|
|
2259
2368
|
# `slug` STAYS EMPTY, and the loop reads it. A free agent belongs to no plan
|
|
@@ -2261,6 +2370,9 @@ EOF
|
|
|
2261
2370
|
# `wait_for_work` skips the outlook scan for an agent that holds none.
|
|
2262
2371
|
slug=""
|
|
2263
2372
|
|
|
2373
|
+
# Where the desks go, asked only now: every refusal above answers first, and
|
|
2374
|
+
# a broken runtime is named by the start-command check rather than here.
|
|
2375
|
+
resolve_wt_root
|
|
2264
2376
|
start_made=0
|
|
2265
2377
|
start_i=0
|
|
2266
2378
|
while [ "$start_i" -lt "$start_n" ]; do
|
|
@@ -2314,6 +2426,9 @@ EOF
|
|
|
2314
2426
|
continue
|
|
2315
2427
|
fi
|
|
2316
2428
|
mkdir -p "$wt_root" 2>/dev/null || true
|
|
2429
|
+
# The desk root lies inside the repository by default, so keep it out of
|
|
2430
|
+
# `git status` the moment it exists.
|
|
2431
|
+
plot_exclude_desk_root "$(plot_repo_root)"
|
|
2317
2432
|
if ! git worktree add -q --detach "$start_wt" "origin/$start_main" 2>/dev/null; then
|
|
2318
2433
|
# NO REMOTE REF IS NOT A FAILURE OF THIS VERB. A fresh clone or a repo
|
|
2319
2434
|
# with no remote has no `origin/<main>`; the local one is the same commit
|
|
@@ -2329,10 +2444,7 @@ EOF
|
|
|
2329
2444
|
# the marker is ignored via `info/exclude` rather than `.gitignore`, because
|
|
2330
2445
|
# an untracked file in a desk reads as unlanded work to
|
|
2331
2446
|
# `plot-worker-state.sh` and would make every free agent look stalled.
|
|
2332
|
-
|
|
2333
|
-
if [ -f "$_excl" ] && ! grep -qxF '.metadata_never_index' "$_excl" 2>/dev/null; then
|
|
2334
|
-
printf '%s\n' '.metadata_never_index' >> "$_excl" 2>/dev/null || true
|
|
2335
|
-
fi
|
|
2447
|
+
plot_desk_exclude "$start_wt" '.metadata_never_index'
|
|
2336
2448
|
: > "$start_wt/.metadata_never_index" 2>/dev/null || true
|
|
2337
2449
|
|
|
2338
2450
|
echo " desk $start_wt (detached at origin/$start_main)"
|
|
@@ -2375,8 +2487,9 @@ if [ "$mode" = "migrate" ]; then
|
|
|
2375
2487
|
configured_root=$("$script_dir/plot-config.sh" get "Worktree root" "")
|
|
2376
2488
|
if [ -z "$configured_root" ]; then
|
|
2377
2489
|
echo "plot-dispatch --migrate: no 'Worktree root:' configured — nothing to migrate."
|
|
2378
|
-
echo " When Worktree root is absent, worktrees
|
|
2379
|
-
echo "
|
|
2490
|
+
echo " When Worktree root is absent, new worktrees go to <repo>/.worktrees;"
|
|
2491
|
+
echo " desks an older dispatch made beside the repo (plot-wt-*) stay there."
|
|
2492
|
+
echo " To move those, first add a 'Worktree root:' key to ## Plot Config."
|
|
2380
2493
|
exit 0
|
|
2381
2494
|
fi
|
|
2382
2495
|
|
|
@@ -2933,13 +3046,12 @@ run_waits_preflight() { # → prints refusals; fills waits_held, adds to n_skipp
|
|
|
2933
3046
|
}
|
|
2934
3047
|
|
|
2935
3048
|
# Where the worktrees live and what their names carry — see resolve_wt_root.
|
|
2936
|
-
# The default is
|
|
2937
|
-
# key relocates them
|
|
2938
|
-
#
|
|
2939
|
-
#
|
|
2940
|
-
# outside the repo.
|
|
3049
|
+
# The default is `<main checkout>/.worktrees` with no prefix; a `Worktree root:`
|
|
3050
|
+
# key relocates them. A root inside the repository is kept out of `git status`
|
|
3051
|
+
# by the `info/exclude` line `plot_exclude_desk_root` writes when a desk is
|
|
3052
|
+
# created.
|
|
2941
3053
|
repo_root=$(git rev-parse --show-toplevel)
|
|
2942
|
-
resolve_wt_root
|
|
3054
|
+
resolve_wt_root
|
|
2943
3055
|
|
|
2944
3056
|
n_dispatched=0 n_reused=0 n_skipped=0 n_started=0
|
|
2945
3057
|
n_brief_asked=0
|
|
@@ -3180,6 +3292,9 @@ write_started_record() { # $@ = branches
|
|
|
3180
3292
|
# stale tip and the push would be a guaranteed non-fast-forward.
|
|
3181
3293
|
git fetch -q origin "$MAIN" 2>/dev/null
|
|
3182
3294
|
|
|
3295
|
+
# `git worktree add` creates the desk root when it is missing; exclude it
|
|
3296
|
+
# first so the booking leaves no untracked `.worktrees/` behind.
|
|
3297
|
+
plot_exclude_desk_root "$(plot_repo_root)"
|
|
3183
3298
|
# -B: a leftover branch from an earlier failed booking must not block this
|
|
3184
3299
|
# one. It is disposable by construction — created here, pushed, deleted.
|
|
3185
3300
|
if ! git worktree add -q -B "$bookbr" "$tmpwt" "origin/$MAIN" 2>/dev/null; then
|