@plot-pm/board 0.14.4 → 0.16.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 +166 -158
- package/package.json +3 -1
- package/plot-agent-manifest.sh +137 -0
- package/plot-approve.sh +25 -1
- package/plot-budget.sh +378 -19
- package/plot-config.sh +86 -2
- package/plot-deliver.sh +26 -3
- package/plot-dispatch.sh +307 -43
- package/plot-fleet-scan.sh +438 -229
- package/plot-host.sh +322 -84
- package/plot-plan-meta.sh +68 -13
- package/plot-pr-merged.sh +29 -2
- package/plot-reap.sh +212 -23
- package/plot-resolve-artifact.sh +4 -2
- package/plot-tmp.sh +108 -0
- package/plot-transcript-quiet.sh +29 -1
- package/plot-worker-monitor.sh +66 -5
package/plot-config.sh
CHANGED
|
@@ -39,7 +39,11 @@
|
|
|
39
39
|
# Non-numeric or empty falls back to the default. It bounds
|
|
40
40
|
# a HUNG agent — one whose CLI crashed without exiting — so
|
|
41
41
|
# one dead worker cannot hold a slot for hours.
|
|
42
|
-
#
|
|
42
|
+
# Temp sweep after hours before `plot-reap.sh --sweep-temp` removes an
|
|
43
|
+
# owned `$TMPDIR/plot-*` entry or a dead pid's budget
|
|
44
|
+
# memo that a SIGKILL left behind. Default 24; a value
|
|
45
|
+
# that is not a whole number is refused by the sweep.
|
|
46
|
+
# Agent registry the directory the dispatcher writes agent manifests to,
|
|
43
47
|
# read by the board's registry. Default `.plot/agents`
|
|
44
48
|
# (repo-relative, gitignored, hence per-worktree). A board
|
|
45
49
|
# served from a worktree the dispatcher never wrote to
|
|
@@ -48,6 +52,53 @@
|
|
|
48
52
|
# the board finds the registry wherever it was started.
|
|
49
53
|
# Absent = the default, so a single-checkout project is
|
|
50
54
|
# unaffected.
|
|
55
|
+
# Board artifact the board-server.mjs this repository runs, read by
|
|
56
|
+
# plot-board-probe.sh. Declared, it is resolved FIRST and
|
|
57
|
+
# reported as `artifact_source: checkout`; absent, the
|
|
58
|
+
# order stays plugin, npm, checkout — the adopting
|
|
59
|
+
# project's case, unchanged. It exists because a
|
|
60
|
+
# repository that BUILDS the artifact must run the one it
|
|
61
|
+
# built: without it `pnpm build:board` writes a file the
|
|
62
|
+
# board never reads whenever a plugin is installed, and
|
|
63
|
+
# the symptom looks like the fix not working.
|
|
64
|
+
# A relative value resolves against the MAIN CHECKOUT (the
|
|
65
|
+
# parent of `--git-common-dir`), never `--show-toplevel`,
|
|
66
|
+
# so a dispatch desk resolves the same file rather than
|
|
67
|
+
# its own copy; an absolute one is taken as given.
|
|
68
|
+
# A declared file that is missing reports `none` and does
|
|
69
|
+
# NOT fall back to the plugin — the key names which
|
|
70
|
+
# artifact runs, so a silent substitution is the wrong
|
|
71
|
+
# answer it removes. Absent = today's order.
|
|
72
|
+
# Agent settings a JSON settings file every `claude -p` the fleet starts
|
|
73
|
+
# is given, through `--settings`. It names the plugins this
|
|
74
|
+
# project's agents start WITHOUT. Every dispatched agent
|
|
75
|
+
# inherits every `SessionStart` hook the operator's plugins
|
|
76
|
+
# declare, and the fleet starts a session on every worker
|
|
77
|
+
# start, restart, retry and hop plus every agent-runner
|
|
78
|
+
# command the board runs: measured 2026-09-30, one plugin's
|
|
79
|
+
# lockless sync ran three times at once, the 1-minute load
|
|
80
|
+
# reached 195, and the supervisor did not tick for 12
|
|
81
|
+
# minutes.
|
|
82
|
+
# Resolved by `plot-agent-settings.sh`, which prints an
|
|
83
|
+
# absolute path (exit 0), nothing for an absent or empty key
|
|
84
|
+
# (exit 0), or nothing with the reason on stderr (exit 3)
|
|
85
|
+
# for a missing, unparseable or gate-disabling file. A
|
|
86
|
+
# relative value resolves against the MAIN CHECKOUT (the
|
|
87
|
+
# parent of `--git-common-dir`), never `--show-toplevel`,
|
|
88
|
+
# for `Board artifact`'s reason: a desk must resolve the
|
|
89
|
+
# same file, and one cut from an older main may not hold it.
|
|
90
|
+
# The path travels to every consumer as
|
|
91
|
+
# `PLOT_AGENT_SETTINGS`, and each command key interpolates
|
|
92
|
+
# `${PLOT_AGENT_SETTINGS:+--settings "$PLOT_AGENT_SETTINGS"}`
|
|
93
|
+
# itself — Plot rewrites no configured command.
|
|
94
|
+
# A file setting any `plot@…` plugin false, `disableAllHooks`
|
|
95
|
+
# true, or ANY `env` key is REFUSED: those switch Plot's own
|
|
96
|
+
# four gates off, and a settings `PATH` hiding the gates'
|
|
97
|
+
# tools makes them fail open. It is a check against an
|
|
98
|
+
# accidental switch-off, not a boundary — the file is
|
|
99
|
+
# project-owned and reviewed like this one.
|
|
100
|
+
# Absent or empty = no change, so an adopting project that
|
|
101
|
+
# sets nothing behaves exactly as today.
|
|
51
102
|
# Worktree root where /plot-dispatch creates fleet worktrees. A relative
|
|
52
103
|
# value resolves against the repo root, an absolute one is
|
|
53
104
|
# taken as given. Absent = the default `repo_root/..` with
|
|
@@ -80,6 +131,13 @@
|
|
|
80
131
|
# here can invoke a skill. Absent (or `none`) = the button
|
|
81
132
|
# refuses and names this key as the fix, rather than
|
|
82
133
|
# accepting the click and doing nothing.
|
|
134
|
+
# Interrogate command how the board runs `/plot-panel <plan path>` on a Draft
|
|
135
|
+
# plan (the card's `Interrogate` button); the prompt is
|
|
136
|
+
# appended as one argument and names a FILE the board
|
|
137
|
+
# wrote. REQUIRED for the same reason as `Idea command`:
|
|
138
|
+
# a panel fans out N agents reading a plan, and no script
|
|
139
|
+
# can do that. Absent (or `none`) = the button renders
|
|
140
|
+
# disabled and names this key as the fix.
|
|
83
141
|
# Brief command how /plot-dispatch runs an agent headless to WRITE a
|
|
84
142
|
# missing hand-off brief. The prompt is appended as one
|
|
85
143
|
# argument and asks for `/plot-implement <slug>`, whose
|
|
@@ -95,6 +153,16 @@
|
|
|
95
153
|
# Hosts plans yes | no (no = refuse plan files)
|
|
96
154
|
# Tracker plot | jira | github-issues | linear (+ URL)
|
|
97
155
|
# (plot = plans in this repo ARE the tracker; absent = same)
|
|
156
|
+
# Tracker delivered status
|
|
157
|
+
# the status word a plan's issues are set to when the plan
|
|
158
|
+
# reaches Delivered, in the tracker's own vocabulary
|
|
159
|
+
# (`In Review`, `Done`). Read by plot-issue-status.sh.
|
|
160
|
+
# Absent or empty = Delivered writes nothing — never an
|
|
161
|
+
# empty status, and never the released word instead.
|
|
162
|
+
# Tracker released status
|
|
163
|
+
# the same for Released. A team whose *Done* means
|
|
164
|
+
# *shipped* sets only this one; a team watching progress
|
|
165
|
+
# sets both.
|
|
98
166
|
# Ticket prefixes the tracker project keys this repository's work lives
|
|
99
167
|
# in, comma-separated (`PROJ-A, PROJ-B`). NOT
|
|
100
168
|
# `Branch prefixes`, which sits next to it and holds
|
|
@@ -142,7 +210,23 @@ if [ "$cmd" != "get" ]; then
|
|
|
142
210
|
exit 1
|
|
143
211
|
fi
|
|
144
212
|
|
|
145
|
-
|
|
213
|
+
# THE CALLER'S ROOT IS TAKEN WHERE IT OFFERS ONE. `git rev-parse` is ~5 ms and
|
|
214
|
+
# this script runs once per config key, so a caller reading several keys pays
|
|
215
|
+
# for the same constant repeatedly. Measured on CI 2026-09-25: one board build
|
|
216
|
+
# spawned `git rev-parse --show-toplevel` 21 times out of 42 git processes
|
|
217
|
+
# total, which `plan-read-shape.test.mjs` caught as the spawn count crossing
|
|
218
|
+
# its bound. The board already exports `PLOT_REPO_ROOT` (`index.ts:65`) and
|
|
219
|
+
# `plot-deliver.sh` and `plot-issue-status.sh` already read it.
|
|
220
|
+
#
|
|
221
|
+
# IT MUST BE A DIRECTORY, and a wrong one falls back rather than failing: an
|
|
222
|
+
# exported stale path would otherwise make every config read answer from a
|
|
223
|
+
# repository that is not this one, silently. Asking git is the safe answer and
|
|
224
|
+
# stays the default for every caller that offers nothing.
|
|
225
|
+
if [ -n "${PLOT_REPO_ROOT:-}" ] && [ -d "$PLOT_REPO_ROOT" ]; then
|
|
226
|
+
root="$PLOT_REPO_ROOT"
|
|
227
|
+
else
|
|
228
|
+
root=$(git rev-parse --show-toplevel 2>/dev/null) || root="."
|
|
229
|
+
fi
|
|
146
230
|
|
|
147
231
|
# Find the first repo-root file that contains a ## Plot Config section.
|
|
148
232
|
# CLAUDE.md wins for backwards compatibility; AGENTS.md is the modern fallback.
|
package/plot-deliver.sh
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
# --who the name recorded in the `Delivered:` line (default: git user.name)
|
|
6
6
|
# <slug> the plan to deliver
|
|
7
7
|
# Output: one `step:` line per step, then a machine-countable summary:
|
|
8
|
-
# summary: phase=flipped record=written index=moved sprint=updated push=clean
|
|
8
|
+
# summary: phase=flipped record=written index=moved sprint=updated push=clean tracker=none
|
|
9
|
+
# `tracker=` is plot-issue-status.sh's outcome (none|written|no-target|
|
|
10
|
+
# unaskable|failed), or `skipped` when the push was rejected. It never
|
|
11
|
+
# changes the exit code: a failed status write is reported, not raised.
|
|
9
12
|
# Exit 0 when the plan is Delivered on the default branch (whether this
|
|
10
13
|
# run did the work or found it already done); 1 on a refusal or a
|
|
11
14
|
# failure, with the reason on stderr.
|
|
@@ -48,6 +51,7 @@
|
|
|
48
51
|
set -uo pipefail
|
|
49
52
|
|
|
50
53
|
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
54
|
+
. "$script_dir/plot-tmp.sh"
|
|
51
55
|
|
|
52
56
|
# The receipt this script leaves for plot-state-gate.sh, which refuses every
|
|
53
57
|
# other writer of a `State:` line. Sourced rather than run: the gate and the
|
|
@@ -654,6 +658,13 @@ git -C "$tmpwt" add -- "${ACTIVE_DIR#/}" >/dev/null 2>&1 || true
|
|
|
654
658
|
git -C "$tmpwt" add -- "${DELIVERED_DIR#/}" >/dev/null 2>&1 || true
|
|
655
659
|
[ "$sprint_report" = "updated" ] && git -C "$tmpwt" add -- "${SPRINT_DIR#/}" >/dev/null 2>&1
|
|
656
660
|
|
|
661
|
+
# THE BOOKED PLAN, KEPT FOR THE TRACKER. The booking worktree is removed on
|
|
662
|
+
# every exit below, and the working tree may still read `Approved`; the issue
|
|
663
|
+
# status is decided from the file that reached the default branch.
|
|
664
|
+
booked_plan=""
|
|
665
|
+
plot_tmpfile booked_plan deliver-plan
|
|
666
|
+
cp "$tmpwt/$rel" "$booked_plan" 2>/dev/null || : > "$booked_plan"
|
|
667
|
+
|
|
657
668
|
if git -C "$tmpwt" diff --cached --quiet 2>/dev/null; then
|
|
658
669
|
# THE IDEMPOTENT EXIT. Everything this run would have written was already
|
|
659
670
|
# on the default branch, so there is nothing to push and nothing wrong.
|
|
@@ -694,13 +705,25 @@ else
|
|
|
694
705
|
echo "plot-deliver: the delivery is committed on '$bookbr' but could not reach $MAIN." >&2
|
|
695
706
|
echo " Land '$bookbr' by hand, or re-run this command once the push works." >&2
|
|
696
707
|
git worktree remove --force "$tmpwt" >/dev/null 2>&1 || true
|
|
697
|
-
|
|
708
|
+
rm -f "$booked_plan"
|
|
709
|
+
# NOTHING REACHED THE DEFAULT BRANCH, so no status is owed yet.
|
|
710
|
+
echo "summary: phase=$phase_report record=$record_report index=$index_report sprint=$sprint_report push=$push_report tracker=skipped"
|
|
698
711
|
exit 1
|
|
699
712
|
fi
|
|
700
713
|
fi
|
|
701
714
|
fi
|
|
702
715
|
|
|
703
|
-
|
|
716
|
+
# THE TRACKER HEARS AFTER THE DELIVERY LANDED, and never decides it. The plan is
|
|
717
|
+
# delivered; the tracker holds a copy of one fact about it. Every outcome of
|
|
718
|
+
# plot-issue-status.sh, a failed write and an unreadable bundle included, is a
|
|
719
|
+
# report on the summary line and leaves this script's exit code alone.
|
|
720
|
+
tracker_out=$(bash "$script_dir/plot-issue-status.sh" "$booked_plan" 2>&1)
|
|
721
|
+
printf '%s\n' "$tracker_out" | grep -v '^summary: ' | sed '/^$/d; s/^/ tracker: /'
|
|
722
|
+
tracker_report=$(printf '%s' "$tracker_out" | sed -n 's/^summary: tracker=\([a-z-]*\).*/\1/p' | tail -1)
|
|
723
|
+
[ -n "$tracker_report" ] || tracker_report="failed"
|
|
724
|
+
rm -f "$booked_plan"
|
|
725
|
+
|
|
726
|
+
echo "summary: phase=$phase_report record=$record_report index=$index_report sprint=$sprint_report push=$push_report tracker=$tracker_report"
|
|
704
727
|
# THE RECEIPT IS SPENT HERE, on the action COMPLETING — never at the gate.
|
|
705
728
|
# `plot-controller-gate.sh` clears on a receipt and LEAVES it, so an
|
|
706
729
|
# interrupted run can be repeated on the same licence: this script documents
|
package/plot-dispatch.sh
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# [--max N] [--allow-local] <slug>
|
|
5
5
|
# plot-dispatch.sh --start [N] [--dry-run]
|
|
6
6
|
# plot-dispatch.sh --migrate [--yes] [--max N]
|
|
7
|
+
# plot-dispatch.sh --release <branch>
|
|
7
8
|
# --status list fleet worktrees with worker pid, liveness, and last log
|
|
8
9
|
# line; then exit. Works regardless of plan phase.
|
|
9
10
|
# --stop <br> stop the worker on <br> (branch required — never "all").
|
|
@@ -23,6 +24,18 @@
|
|
|
23
24
|
# word), on a live worker, and on a PLOT-BLOCKED marker. The
|
|
24
25
|
# worktree is inherited exactly as it stands — uncommitted work
|
|
25
26
|
# is what a stall leaves behind, and this must not destroy it.
|
|
27
|
+
# --release <br>
|
|
28
|
+
# return an ABANDONED slice to the queue: clear the `branch` of
|
|
29
|
+
# every agent manifest naming <br>, then delete the claim ref
|
|
30
|
+
# origin/<br>. --stop ends a worker and KEEPS the claim, because
|
|
31
|
+
# stopping is not abandoning; --release is the act that gives the
|
|
32
|
+
# slice up. Deleting the ref by hand is not the same act: the
|
|
33
|
+
# manifest still names the branch. Branch required. Refuses on a
|
|
34
|
+
# PR (open or merged), a host it cannot ask, a live worker (named
|
|
35
|
+
# by pid), real work (a file-changing commit on origin/<br>, or
|
|
36
|
+
# unpushed commits or uncommitted changes on the local desk), and
|
|
37
|
+
# a PLOT-BLOCKED marker. A refusal writes nothing. The desk is
|
|
38
|
+
# never touched.
|
|
26
39
|
# --migrate move legacy worktrees into the configured `Worktree root:`. An
|
|
27
40
|
# idle worktree (no live worker, no unlanded work) is moved; a
|
|
28
41
|
# busy one is skipped with the reason. Requires a `Worktree root:`
|
|
@@ -145,8 +158,10 @@
|
|
|
145
158
|
# think they won. Git is the lock only when the refs actually diverge.
|
|
146
159
|
# - Worktrees are adopted, never duplicated. A dispatcher that dies halfway
|
|
147
160
|
# through a fan-out is safe to re-run.
|
|
148
|
-
# -
|
|
149
|
-
# tell a deliberately abandoned claim from a dead worker.
|
|
161
|
+
# - A fan-out deletes nothing. Cleanup belongs to /plot-reconcile, which can
|
|
162
|
+
# tell a deliberately abandoned claim from a dead worker. The one deletion
|
|
163
|
+
# here is `--release <branch>`, which a person runs after making that call,
|
|
164
|
+
# and which refuses wherever the claim is not abandoned.
|
|
150
165
|
# - The `Started:` record is booked on the DEFAULT BRANCH, after the claims,
|
|
151
166
|
# and only for branches this run newly claimed. A re-run books nothing it
|
|
152
167
|
# merely re-adopted. If the booking cannot be pushed, the fan-out stands
|
|
@@ -163,6 +178,7 @@
|
|
|
163
178
|
set -uo pipefail
|
|
164
179
|
|
|
165
180
|
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
181
|
+
. "$script_dir/plot-tmp.sh"
|
|
166
182
|
|
|
167
183
|
# The shared worker classifier. Sourced by both this script and
|
|
168
184
|
# plot-fleet-scan.sh so a worker has ONE state, not one per reader.
|
|
@@ -191,6 +207,12 @@ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
|
191
207
|
# shellcheck source=plot-default-branch.sh
|
|
192
208
|
. "$script_dir/plot-default-branch.sh"
|
|
193
209
|
|
|
210
|
+
# The ONE writer of a manifest's empty `branch` — `clear_manifest_branch`,
|
|
211
|
+
# shared with `plot-worker-loop.sh`. `--release` clears an abandoned slice's
|
|
212
|
+
# assignment with it rather than with a second writer.
|
|
213
|
+
# shellcheck source=plot-agent-manifest.sh
|
|
214
|
+
. "$script_dir/plot-agent-manifest.sh"
|
|
215
|
+
|
|
194
216
|
# ---------------------------------------------------------------------------
|
|
195
217
|
# WHERE THE WORKTREES LIVE, and by what name
|
|
196
218
|
# ---------------------------------------------------------------------------
|
|
@@ -239,6 +261,7 @@ no_brief=0
|
|
|
239
261
|
mode=dispatch
|
|
240
262
|
stop_branch=""
|
|
241
263
|
restart_branch=""
|
|
264
|
+
release_branch=""
|
|
242
265
|
# EMPTY MEANS "THE DEFAULT", AND THE DEFAULT IS THE RULE'S. `fleetSize` owns the
|
|
243
266
|
# number and the argument for it; a literal here would be a second copy of a
|
|
244
267
|
# decision that has one home. It is filled from the rule below.
|
|
@@ -275,6 +298,9 @@ while [ $# -gt 0 ]; do
|
|
|
275
298
|
# plan for feature/x", which describes neither what was asked nor what
|
|
276
299
|
# went wrong. The branch is consumed only when it looks like one.
|
|
277
300
|
--restart) mode=restart; case "${2:-}" in */*) restart_branch="$2"; shift ;; esac ;;
|
|
301
|
+
# The same rule once more: a bare `--release <slug>` must not delete a ref
|
|
302
|
+
# named after a plan slug.
|
|
303
|
+
--release) mode=release; case "${2:-}" in */*) release_branch="$2"; shift ;; esac ;;
|
|
278
304
|
# `--start [N]` brings FREE agents into existence — registered, waiting, and
|
|
279
305
|
# holding no slice. The count is OPTIONAL and only a bare number is consumed,
|
|
280
306
|
# the same rule `--stop` and `--restart` apply to a branch: a value that does
|
|
@@ -312,10 +338,11 @@ while [ $# -gt 0 ]; do
|
|
|
312
338
|
shift ;;
|
|
313
339
|
# THE RANGE MOVED WITH THE HEADER IT PRINTS. It ended at `<slug>` and still
|
|
314
340
|
# does; adding `--agent` above pushed that line from 59 to 69, and
|
|
315
|
-
# documenting the plan-declared kind pushed it from 69 to 78
|
|
341
|
+
# documenting the plan-declared kind pushed it from 69 to 78, and
|
|
342
|
+
# `--release` pushed it from 78 to 91. Two records
|
|
316
343
|
# of one fact, and nothing compares them — a stale number here silently
|
|
317
344
|
# truncates the help rather than failing, so it is checked by a test.
|
|
318
|
-
-h|--help) sed -n '2,
|
|
345
|
+
-h|--help) sed -n '2,91p' "$0"; exit 0 ;;
|
|
319
346
|
*) slug="$1" ;;
|
|
320
347
|
esac
|
|
321
348
|
shift
|
|
@@ -366,28 +393,9 @@ fi
|
|
|
366
393
|
# until this moved. Nothing here executes at definition time; only the position
|
|
367
394
|
# changed.
|
|
368
395
|
|
|
369
|
-
#
|
|
370
|
-
#
|
|
371
|
-
#
|
|
372
|
-
# — never to `$RANDOM` or a timestamp. Two workers launched in the same second by
|
|
373
|
-
# the same fan-out would collide on either, and a collision here silently merges
|
|
374
|
-
# two agents into one manifest.
|
|
375
|
-
#
|
|
376
|
-
# Lowercased because the runtime writes its transcript filename in lowercase and
|
|
377
|
-
# the board joins on exact string equality; `uuidgen` on macOS returns uppercase.
|
|
378
|
-
plot_session_id() {
|
|
379
|
-
local id=""
|
|
380
|
-
if command -v uuidgen >/dev/null 2>&1; then
|
|
381
|
-
id=$(uuidgen 2>/dev/null | tr 'A-Z' 'a-z')
|
|
382
|
-
fi
|
|
383
|
-
if [ -z "$id" ]; then
|
|
384
|
-
# 16 random bytes rendered as a v4-shaped id. The shape matters only for
|
|
385
|
-
# recognisability; nothing parses it.
|
|
386
|
-
id=$(od -An -tx1 -N16 /dev/urandom 2>/dev/null | tr -d ' \n' \
|
|
387
|
-
| sed -E 's/(.{8})(.{4})(.{4})(.{4})(.{12})/\1-\2-\3-\4-\5/')
|
|
388
|
-
fi
|
|
389
|
-
printf '%s' "$id"
|
|
390
|
-
}
|
|
396
|
+
# `plot_session_id` — the session id a launch records — is SOURCED from
|
|
397
|
+
# `plot-agent-manifest.sh` above, because the worker loop mints one on a hop to
|
|
398
|
+
# a new branch and two generators of one id drift.
|
|
391
399
|
|
|
392
400
|
# JSON-escape one string for a manifest value.
|
|
393
401
|
#
|
|
@@ -428,6 +436,23 @@ json_escape() {
|
|
|
428
436
|
#
|
|
429
437
|
# Written to a temp file and moved into place, so a scan reading the directory
|
|
430
438
|
# never sees a half-written manifest. `mv` within one directory is atomic.
|
|
439
|
+
# The directory the `Agent registry` key names, resolved against a repo root.
|
|
440
|
+
#
|
|
441
|
+
# The case split is `resolve_wt_root`'s: absolute taken as given, relative
|
|
442
|
+
# joined onto the repo root, trailing slash trimmed as pure string work because
|
|
443
|
+
# the directory need not exist yet. One resolver, read by `start_worker` when it
|
|
444
|
+
# writes a manifest and by `--release` when it clears one, so the two cannot
|
|
445
|
+
# look in different places.
|
|
446
|
+
agent_registry_dir() { # $1=repo_root → prints the directory
|
|
447
|
+
local dir
|
|
448
|
+
dir=$("$script_dir/plot-config.sh" get "Agent registry" ".plot/agents")
|
|
449
|
+
case "$dir" in
|
|
450
|
+
/*) ;;
|
|
451
|
+
*) dir="$1/$dir" ;;
|
|
452
|
+
esac
|
|
453
|
+
printf '%s' "${dir%/}"
|
|
454
|
+
}
|
|
455
|
+
|
|
431
456
|
write_agent_manifest() { # $1=path $2=session $3=branch $4=worktree $5=command
|
|
432
457
|
local out="$1" tmp="$1.plot-tmp"
|
|
433
458
|
{
|
|
@@ -763,8 +788,20 @@ request_brief() { # $1 = branch, $2 = slug → 0 if a command was started
|
|
|
763
788
|
# this outlives the dispatch run by design, because the fan-out must not block
|
|
764
789
|
# on a `claude -p` session of unknown length. `setsid` is not used — it does
|
|
765
790
|
# not exist on macOS, where most of this fleet runs.
|
|
791
|
+
# THE SETTINGS FILE THIS PROJECT STARTS ITS AGENTS WITH. The `Brief command` is
|
|
792
|
+
# a `claude -p` session like any other agent and inherits every `SessionStart`
|
|
793
|
+
# hook the operator's plugins declare, so it carries the same variable the
|
|
794
|
+
# worker loop exports — and the configured command interpolates
|
|
795
|
+
# `${PLOT_AGENT_SETTINGS:+--settings "$PLOT_AGENT_SETTINGS"}` itself.
|
|
796
|
+
#
|
|
797
|
+
# SET INLINE rather than exported, because this spawn already names its
|
|
798
|
+
# environment on the command line. A refusal or an absent key leaves it EMPTY,
|
|
799
|
+
# which `${VAR:+…}` reads as nothing — so a broken config cannot stop a brief
|
|
800
|
+
# being written.
|
|
801
|
+
_brief_settings="$(bash "$(dirname "${BASH_SOURCE[0]}")/plot-agent-settings.sh" 2>/dev/null || echo "")"
|
|
766
802
|
( cd "$repo_root" \
|
|
767
803
|
&& PLOT_UNATTENDED=1 PLOT_PLAN_SLUG="$bslug" PLOT_BRIEF_BRANCH="$branch" \
|
|
804
|
+
PLOT_AGENT_SETTINGS="$_brief_settings" \
|
|
768
805
|
nohup sh -c "$cmd \"\$@\"" plot-brief \
|
|
769
806
|
"$(brief_prompt "$branch" "$bslug")" \
|
|
770
807
|
>"$log" 2>&1 </dev/null & ) 2>/dev/null
|
|
@@ -1119,12 +1156,7 @@ start_worker() {
|
|
|
1119
1156
|
# resolving a configured directory is a second way to be wrong.
|
|
1120
1157
|
local session manifest_dir
|
|
1121
1158
|
session=$(plot_session_id)
|
|
1122
|
-
manifest_dir=$("$
|
|
1123
|
-
case "$manifest_dir" in
|
|
1124
|
-
/*) ;;
|
|
1125
|
-
*) manifest_dir="$repo_root/$manifest_dir" ;;
|
|
1126
|
-
esac
|
|
1127
|
-
manifest_dir="${manifest_dir%/}"
|
|
1159
|
+
manifest_dir=$(agent_registry_dir "$repo_root")
|
|
1128
1160
|
mkdir -p "$manifest_dir" 2>/dev/null || true
|
|
1129
1161
|
# `printf` per field with no interpretation: a command containing quotes,
|
|
1130
1162
|
# newlines or backslashes must survive into valid JSON, and this is the one
|
|
@@ -1171,8 +1203,9 @@ start_worker() {
|
|
|
1171
1203
|
echo " refusing to start $branch — its agent manifest could not be written:"
|
|
1172
1204
|
echo " $manifest_dir/$session.json"
|
|
1173
1205
|
echo " An unregistered worker cannot be seen, stopped or reaped, and holds"
|
|
1174
|
-
echo " a claim
|
|
1175
|
-
echo " fix the path above (see the 'Agent registry'
|
|
1206
|
+
echo " a claim only 'plot-dispatch.sh --release $branch' can give up. The worktree"
|
|
1207
|
+
echo " and claim are untouched — fix the path above (see the 'Agent registry'"
|
|
1208
|
+
echo " key) and dispatch again."
|
|
1176
1209
|
return 1
|
|
1177
1210
|
fi
|
|
1178
1211
|
# TWO PIDS, TWO NAMES. `.plot-worker.pid` must name the AGENT — the process
|
|
@@ -1699,7 +1732,8 @@ if [ "$mode" = "stop" ]; then
|
|
|
1699
1732
|
exit 1; }
|
|
1700
1733
|
# The worktree and its claim are left in place: the branch is still taken,
|
|
1701
1734
|
# and deleting either would be the kind of write this design avoids.
|
|
1702
|
-
echo " worktree kept at $wt — the claim stands until you release it"
|
|
1735
|
+
echo " worktree kept at $wt — the claim stands until you release it:"
|
|
1736
|
+
echo " plot-dispatch.sh --release $stop_branch"
|
|
1703
1737
|
;;
|
|
1704
1738
|
finished*|waiting*|stalled*|failed*|ended*) echo "$stop_branch is not running ($st)" ;;
|
|
1705
1739
|
*) echo "$stop_branch has no worker" ;;
|
|
@@ -1807,6 +1841,232 @@ if [ "$mode" = "restart" ]; then
|
|
|
1807
1841
|
exit 0
|
|
1808
1842
|
fi
|
|
1809
1843
|
|
|
1844
|
+
if [ "$mode" = "release" ]; then
|
|
1845
|
+
# THE COUNTERPART TO --stop THAT NOBODY WROTE. `--stop` ends a worker and
|
|
1846
|
+
# keeps the claim, because stopping is not abandoning. This is the act that
|
|
1847
|
+
# gives an abandoned slice back to the queue.
|
|
1848
|
+
#
|
|
1849
|
+
# THE ASSIGNMENT HAS TWO RECORDS, and this clears both. The remote claim ref
|
|
1850
|
+
# is what the scan and the registry's queue read as *somebody took this*; the
|
|
1851
|
+
# agent manifest's `branch` field is what the registry wrote when it handed
|
|
1852
|
+
# the slice over. Deleting only the ref — the repair `plot-reap.sh` and
|
|
1853
|
+
# `plot-reconcile-scan.sh` leave to a person — leaves the manifest naming a
|
|
1854
|
+
# slice the queue now offers to somebody else. Measured 2026-09-26:
|
|
1855
|
+
# `feature/the-board-filters-to-my-work` was handed out twice after exactly
|
|
1856
|
+
# that hand repair, and the second agent abandoned a desk with unpushed work.
|
|
1857
|
+
#
|
|
1858
|
+
# BEFORE THE PHASE GATE, beside --stop and --restart: a claimed branch is
|
|
1859
|
+
# work in flight whatever the plan's phase now says.
|
|
1860
|
+
if [ -z "$release_branch" ]; then
|
|
1861
|
+
echo "plot-dispatch: --release needs a branch name, e.g. --release feature/x" >&2
|
|
1862
|
+
echo " A slug is not enough: which claim is abandoned is your call, and" >&2
|
|
1863
|
+
echo " a release deletes a ref that cannot be re-created." >&2
|
|
1864
|
+
exit 1
|
|
1865
|
+
fi
|
|
1866
|
+
br="$release_branch"
|
|
1867
|
+
MAIN=$(bash "$script_dir/plot-config.sh" get "Main branch")
|
|
1868
|
+
[ -n "$MAIN" ] || MAIN=$(default_branch)
|
|
1869
|
+
if [ -z "$MAIN" ]; then
|
|
1870
|
+
echo "plot-dispatch: cannot resolve the default branch — refusing to release $br." >&2
|
|
1871
|
+
echo " Nothing was written." >&2
|
|
1872
|
+
exit 1
|
|
1873
|
+
fi
|
|
1874
|
+
|
|
1875
|
+
# THE REMOTE IS READ FRESH. A stale remote-tracking ref would hide commits a
|
|
1876
|
+
# worker pushed since the last fetch, and those are the real work this must
|
|
1877
|
+
# refuse on. A fetch that fails is a remote this cannot read.
|
|
1878
|
+
if ! git fetch -q --prune origin </dev/null 2>/dev/null; then
|
|
1879
|
+
echo "plot-dispatch: cannot fetch origin — refusing to release $br." >&2
|
|
1880
|
+
echo " Without a fresh reading this cannot tell a claim from pushed work." >&2
|
|
1881
|
+
echo " Nothing was written." >&2
|
|
1882
|
+
exit 1
|
|
1883
|
+
fi
|
|
1884
|
+
ref_present=0
|
|
1885
|
+
git rev-parse -q --verify "refs/remotes/origin/$br" >/dev/null 2>&1 && ref_present=1
|
|
1886
|
+
|
|
1887
|
+
# 1. A PULL REQUEST, ASKED FIRST — `handover_refusal`'s order and for its
|
|
1888
|
+
# reason: a worker's exit state says nothing about whether its work reached
|
|
1889
|
+
# review. A merged branch's ref belongs to `plot-release-refs.sh`; an open
|
|
1890
|
+
# one is work under review.
|
|
1891
|
+
#
|
|
1892
|
+
# UNREACHABLE IS NOT "NO PR". `reached_review` reads a failed call as no PR,
|
|
1893
|
+
# which is right for a rendering and wrong here: a deleted ref cannot be
|
|
1894
|
+
# re-created, so a host that cannot be asked refuses. `--offline` promises no
|
|
1895
|
+
# host call, so it refuses too.
|
|
1896
|
+
if [ -n "$offline" ]; then
|
|
1897
|
+
echo "plot-dispatch: --release asks the host whether $br has a PR, and --offline forbids that — refusing." >&2
|
|
1898
|
+
echo " Nothing was written." >&2
|
|
1899
|
+
exit 1
|
|
1900
|
+
fi
|
|
1901
|
+
if ! pr_json=$("$script_dir/plot-host.sh" pr-state "$br" </dev/null 2>/dev/null); then
|
|
1902
|
+
echo "plot-dispatch: the host could not be asked whether $br has a PR — refusing." >&2
|
|
1903
|
+
echo " A release deletes a ref that cannot be re-created, so silence is not" >&2
|
|
1904
|
+
echo " permission. Check the host (plot-host.sh pr-state $br) and retry." >&2
|
|
1905
|
+
echo " Nothing was written." >&2
|
|
1906
|
+
exit 1
|
|
1907
|
+
fi
|
|
1908
|
+
pr_state=$(printf '%s' "$pr_json" | sed -n 's/.*"state":"\([A-Z]*\)".*/\1/p')
|
|
1909
|
+
pr_num=$(printf '%s' "$pr_json" | sed -n 's/.*"number":\([0-9]*\).*/\1/p')
|
|
1910
|
+
# ANY MERGED PR, NOT ONLY THE NEWEST — `pr-merged` reads `mergedAt` over every
|
|
1911
|
+
# PR for the branch, because a newer unmerged PR can mask a merged one.
|
|
1912
|
+
merged_answer=$("$script_dir/plot-host.sh" pr-merged "$br" </dev/null 2>/dev/null) || merged_answer=unknown
|
|
1913
|
+
case "$pr_state:$merged_answer" in
|
|
1914
|
+
OPEN:*|MERGED:*|*:merged)
|
|
1915
|
+
echo "plot-dispatch: $br has a pull request (#${pr_num:-?}, ${pr_state:-MERGED}) — refusing." >&2
|
|
1916
|
+
echo " An open PR is work under review; a merged branch's ref is released by" >&2
|
|
1917
|
+
echo " plot-release-refs.sh when its plan is delivered. Nothing was written." >&2
|
|
1918
|
+
exit 1
|
|
1919
|
+
;;
|
|
1920
|
+
*:unknown|*:)
|
|
1921
|
+
echo "plot-dispatch: the host could not say whether $br ever merged — refusing." >&2
|
|
1922
|
+
echo " Check it (plot-host.sh pr-merged $br) and retry. Nothing was written." >&2
|
|
1923
|
+
exit 1
|
|
1924
|
+
;;
|
|
1925
|
+
esac
|
|
1926
|
+
|
|
1927
|
+
# THE DESK, ASKED OF GIT — never rebuilt from the branch name, the rule every
|
|
1928
|
+
# other verb here follows. A manifest naming this branch may name a desk git
|
|
1929
|
+
# does not list (removed by hand), so its `worktree` is the fallback.
|
|
1930
|
+
release_wt=$(git worktree list --porcelain </dev/null 2>/dev/null | awk -v want="refs/heads/$br" '
|
|
1931
|
+
/^worktree / { path = substr($0, 10) }
|
|
1932
|
+
/^branch / { if (substr($0, 8) == want) { print path; exit } }')
|
|
1933
|
+
registry_dir=$(agent_registry_dir "$repo_root_early")
|
|
1934
|
+
named_manifests=()
|
|
1935
|
+
if [ -d "$registry_dir" ]; then
|
|
1936
|
+
for m in "$registry_dir"/*.json; do
|
|
1937
|
+
[ -f "$m" ] || continue
|
|
1938
|
+
m_branch=$(node -e '
|
|
1939
|
+
try {
|
|
1940
|
+
const m = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
|
|
1941
|
+
process.stdout.write(typeof m.branch === "string" ? m.branch : "");
|
|
1942
|
+
} catch { process.stdout.write(""); }
|
|
1943
|
+
' "$m" 2>/dev/null)
|
|
1944
|
+
[ "$m_branch" = "$br" ] || continue
|
|
1945
|
+
named_manifests+=("$m")
|
|
1946
|
+
if [ -z "$release_wt" ]; then
|
|
1947
|
+
m_wt=$(node -e '
|
|
1948
|
+
try {
|
|
1949
|
+
const m = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
|
|
1950
|
+
process.stdout.write(typeof m.worktree === "string" ? m.worktree : "");
|
|
1951
|
+
} catch { process.stdout.write(""); }
|
|
1952
|
+
' "$m" 2>/dev/null)
|
|
1953
|
+
[ -n "$m_wt" ] && [ -d "$m_wt" ] && release_wt="$m_wt"
|
|
1954
|
+
fi
|
|
1955
|
+
done
|
|
1956
|
+
fi
|
|
1957
|
+
[ -n "$release_wt" ] && [ -d "$release_wt" ] || release_wt=""
|
|
1958
|
+
|
|
1959
|
+
# 2. A LIVE WORKER — the measurement `--restart` makes, through the shared
|
|
1960
|
+
# classifier, never `pgrep` by name. A live pid means somebody is working,
|
|
1961
|
+
# and the one case where a claim is not abandoned.
|
|
1962
|
+
if [ -n "$release_wt" ]; then
|
|
1963
|
+
release_state=$(plot_worker_state "$release_wt" "" | cut -f1,2)
|
|
1964
|
+
case "$release_state" in
|
|
1965
|
+
running*)
|
|
1966
|
+
echo "plot-dispatch: a worker is alive on $br (pid $(printf '%s' "$release_state" | cut -f2)) — refusing." >&2
|
|
1967
|
+
echo " A live worker has not abandoned its claim. Stop it first if you mean to:" >&2
|
|
1968
|
+
echo " plot-dispatch.sh --stop $br" >&2
|
|
1969
|
+
echo " Nothing was written." >&2
|
|
1970
|
+
exit 1
|
|
1971
|
+
;;
|
|
1972
|
+
esac
|
|
1973
|
+
fi
|
|
1974
|
+
|
|
1975
|
+
# 3. REAL WORK. A claim is an EMPTY commit on the default branch's tip, so
|
|
1976
|
+
# it changes no file; `-- .` limits the count to commits that change one,
|
|
1977
|
+
# which is what separates a claim-only ref from pushed work. The local desk
|
|
1978
|
+
# is asked too, because the measured victim held its work in UNPUSHED
|
|
1979
|
+
# commits and uncommitted files that no remote reading can see. A count of
|
|
1980
|
+
# commits, not an ancestry verdict: a count above zero only ever REFUSES.
|
|
1981
|
+
if [ "$ref_present" = 1 ]; then
|
|
1982
|
+
remote_work=$(git rev-list --count "refs/remotes/origin/$MAIN..refs/remotes/origin/$br" -- . </dev/null 2>/dev/null) || remote_work=""
|
|
1983
|
+
if [ -z "$remote_work" ]; then
|
|
1984
|
+
echo "plot-dispatch: cannot count the commits on origin/$br — refusing." >&2
|
|
1985
|
+
echo " Nothing was written." >&2
|
|
1986
|
+
exit 1
|
|
1987
|
+
fi
|
|
1988
|
+
if [ "$remote_work" -gt 0 ]; then
|
|
1989
|
+
echo "plot-dispatch: origin/$br carries $remote_work commit(s) that change files — refusing." >&2
|
|
1990
|
+
echo " That is work, not an abandoned claim. Review it, or open its PR:" >&2
|
|
1991
|
+
echo " plot-open-pr.sh $br" >&2
|
|
1992
|
+
echo " Nothing was written." >&2
|
|
1993
|
+
exit 1
|
|
1994
|
+
fi
|
|
1995
|
+
fi
|
|
1996
|
+
if [ -n "$release_wt" ]; then
|
|
1997
|
+
desk_dirty=$(plot_worker_dirty "$release_wt")
|
|
1998
|
+
if [ "$ref_present" = 1 ]; then desk_base="refs/remotes/origin/$br"; else desk_base="refs/remotes/origin/$MAIN"; fi
|
|
1999
|
+
# Local commits the remote lacks exist nowhere else, so any refuses.
|
|
2000
|
+
desk_unpushed=$(git -C "$release_wt" rev-list --count "$desk_base..HEAD" -- . </dev/null 2>/dev/null) || desk_unpushed=0
|
|
2001
|
+
if [ -n "$desk_dirty" ] || [ "${desk_unpushed:-0}" -gt 0 ]; then
|
|
2002
|
+
echo "plot-dispatch: the desk at $release_wt holds work for $br — refusing." >&2
|
|
2003
|
+
[ "${desk_unpushed:-0}" -gt 0 ] && echo " $desk_unpushed unpushed commit(s) that change files" >&2
|
|
2004
|
+
[ -n "$desk_dirty" ] && echo " uncommitted changes: $(printf '%s' "$desk_dirty" | tr '\n' ' ')" >&2
|
|
2005
|
+
echo " Releasing would hand the slice to an agent that cannot see this work." >&2
|
|
2006
|
+
echo " Push it, or hand the branch to a new worker: plot-dispatch.sh --restart $br" >&2
|
|
2007
|
+
echo " Nothing was written." >&2
|
|
2008
|
+
exit 1
|
|
2009
|
+
fi
|
|
2010
|
+
|
|
2011
|
+
# 4. A PLOT-BLOCKED MARKER: the agent is waiting on a person, and a slice
|
|
2012
|
+
# waiting on an answer is not abandoned. Asked through
|
|
2013
|
+
# `plot_worker_blocked_file`, where the marker's spelling lives.
|
|
2014
|
+
if marker=$(plot_worker_blocked_file "$release_wt") && [ -n "$marker" ]; then
|
|
2015
|
+
echo "plot-dispatch: $br is blocked on a question — refusing." >&2
|
|
2016
|
+
echo " the question is in $release_wt/$marker" >&2
|
|
2017
|
+
echo " Answer it and delete the marker; the agent's slice is not abandoned." >&2
|
|
2018
|
+
echo " Nothing was written." >&2
|
|
2019
|
+
exit 1
|
|
2020
|
+
fi
|
|
2021
|
+
fi
|
|
2022
|
+
|
|
2023
|
+
if [ "$ref_present" = 0 ] && [ "${#named_manifests[@]}" -eq 0 ]; then
|
|
2024
|
+
echo "$br holds no claim: origin/$br does not exist and no manifest names it."
|
|
2025
|
+
echo " Nothing to release."
|
|
2026
|
+
exit 0
|
|
2027
|
+
fi
|
|
2028
|
+
|
|
2029
|
+
# THE MANIFESTS FIRST, THEN THE REF. If the ref deletion fails, the manifest
|
|
2030
|
+
# is already free and the ref still locks, so the scan still reads the slice
|
|
2031
|
+
# as claimed and nothing hands it out twice. The reverse order would leave the
|
|
2032
|
+
# measured failure on disk: no ref, and a manifest still naming the branch.
|
|
2033
|
+
released_manifests=0
|
|
2034
|
+
if [ "${#named_manifests[@]}" -eq 0 ]; then
|
|
2035
|
+
echo " no manifest in $registry_dir names $br"
|
|
2036
|
+
fi
|
|
2037
|
+
for m in ${named_manifests[@]+"${named_manifests[@]}"}; do
|
|
2038
|
+
if ! clear_manifest_branch "$m"; then
|
|
2039
|
+
echo "plot-dispatch: could not clear the assignment in $m — refusing to delete the ref." >&2
|
|
2040
|
+
echo " The claim on origin/$br stands; fix the file and run --release again." >&2
|
|
2041
|
+
exit 1
|
|
2042
|
+
fi
|
|
2043
|
+
released_manifests=$((released_manifests + 1))
|
|
2044
|
+
echo " cleared the assignment in $m"
|
|
2045
|
+
done
|
|
2046
|
+
|
|
2047
|
+
if [ "$ref_present" = 1 ]; then
|
|
2048
|
+
if ! git push -q origin --delete "$br" </dev/null 2>/dev/null; then
|
|
2049
|
+
# VERIFIED, NOT TRUSTED: the host has returned 503 on a push that landed.
|
|
2050
|
+
git fetch -q --prune origin </dev/null 2>/dev/null || true
|
|
2051
|
+
if git rev-parse -q --verify "refs/remotes/origin/$br" >/dev/null 2>&1; then
|
|
2052
|
+
echo "plot-dispatch: could not delete origin/$br — the claim still locks the slice." >&2
|
|
2053
|
+
echo " The manifests are already free, so nothing hands it out twice. Retry:" >&2
|
|
2054
|
+
echo " plot-dispatch.sh --release $br" >&2
|
|
2055
|
+
exit 1
|
|
2056
|
+
fi
|
|
2057
|
+
fi
|
|
2058
|
+
echo " deleted the claim ref origin/$br"
|
|
2059
|
+
else
|
|
2060
|
+
echo " origin/$br does not exist — only the assignment needed releasing"
|
|
2061
|
+
fi
|
|
2062
|
+
if [ -n "$release_wt" ]; then
|
|
2063
|
+
echo " the desk at $release_wt still holds $br and is left as it is"
|
|
2064
|
+
fi
|
|
2065
|
+
echo "released $br — the slice returns to the queue"
|
|
2066
|
+
echo "summary: released=1 manifests=$released_manifests ref=$([ "$ref_present" = 1 ] && echo deleted || echo absent)"
|
|
2067
|
+
exit 0
|
|
2068
|
+
fi
|
|
2069
|
+
|
|
1810
2070
|
if [ "$mode" = "start" ]; then
|
|
1811
2071
|
# THE LAST LINK IN THE CHAIN. `plot-dispatch.sh <slug>` queues slices and the
|
|
1812
2072
|
# registry matches them to free agents, but until this verb existed nothing
|
|
@@ -2393,18 +2653,18 @@ fi
|
|
|
2393
2653
|
# materialised into a temp file rather than parsed here — the parser stays the
|
|
2394
2654
|
# one place that knows what a plan file looks like.
|
|
2395
2655
|
#
|
|
2396
|
-
#
|
|
2397
|
-
# suffix after them, while GNU accepts it. The first version
|
|
2398
|
-
# `plot-gate-XXXXXX.md` and failed on macOS — and because the failure fell
|
|
2399
|
-
# to the working tree, the gate silently went back to reading the exact
|
|
2400
|
-
# this fix exists to stop reading. Hence also: NO working-tree fallback
|
|
2401
|
-
# If the shared blob cannot be materialised, the gate refuses.
|
|
2656
|
+
# `plot-tmp.sh` puts the template's X's last: BSD mktemp (macOS) rejects a
|
|
2657
|
+
# template with a suffix after them, while GNU accepts it. The first version
|
|
2658
|
+
# wrote `plot-gate-XXXXXX.md` and failed on macOS — and because the failure fell
|
|
2659
|
+
# back to the working tree, the gate silently went back to reading the exact
|
|
2660
|
+
# surface this fix exists to stop reading. Hence also: NO working-tree fallback
|
|
2661
|
+
# below. If the shared blob cannot be materialised, the gate refuses.
|
|
2402
2662
|
plan_file="$plan_path"
|
|
2403
2663
|
gate_blob=""
|
|
2404
2664
|
if [ -n "$gate_sha" ]; then
|
|
2405
|
-
gate_dir
|
|
2665
|
+
gate_dir=""
|
|
2666
|
+
plot_tmpdir gate_dir gate || gate_dir=""
|
|
2406
2667
|
if [ -n "$gate_dir" ]; then
|
|
2407
|
-
trap 'rm -rf "$gate_dir"' EXIT
|
|
2408
2668
|
gate_blob="$gate_dir/$(basename "$plan_path")"
|
|
2409
2669
|
git show "$gate_ref:$plan_path" >"$gate_blob" 2>/dev/null || gate_blob=""
|
|
2410
2670
|
fi
|
|
@@ -3063,6 +3323,10 @@ append_started_line() { # $1=file $2=date $3=who $4=branch
|
|
|
3063
3323
|
insert = start
|
|
3064
3324
|
for (i = start + 1; i <= n; i++) {
|
|
3065
3325
|
if (lines[i] ~ /^##[ \t]/) break
|
|
3326
|
+
# An HTML comment ends the writable region. Checked BEFORE the
|
|
3327
|
+
# placeholder arms so a commented-out `- **Started:**` template line
|
|
3328
|
+
# is never mistaken for the slot to fill.
|
|
3329
|
+
if (lines[i] ~ /<!--/) break
|
|
3066
3330
|
if (lines[i] ~ /^[ \t]*[-*][ \t]*\*\*Started:\*\*[ \t]*$/) { slot = i; break }
|
|
3067
3331
|
if (lines[i] ~ /^[ \t]*[-*][ \t]/) insert = i
|
|
3068
3332
|
}
|