@plot-pm/board 0.14.3 → 0.15.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 +152 -145
- package/package.json +2 -1
- package/plot-agent-manifest.sh +58 -0
- package/plot-approve.sh +23 -1
- package/plot-budget.sh +166 -1
- package/plot-config.sh +34 -1
- package/plot-deliver.sh +24 -3
- package/plot-dispatch.sh +283 -13
- package/plot-fleet-scan.sh +387 -79
- package/plot-host.sh +230 -16
- package/plot-plan-meta.sh +10 -3
- package/plot-pr-merged.sh +29 -2
- package/plot-reap.sh +152 -32
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
|
|
@@ -191,6 +206,12 @@ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
|
191
206
|
# shellcheck source=plot-default-branch.sh
|
|
192
207
|
. "$script_dir/plot-default-branch.sh"
|
|
193
208
|
|
|
209
|
+
# The ONE writer of a manifest's empty `branch` — `clear_manifest_branch`,
|
|
210
|
+
# shared with `plot-worker-loop.sh`. `--release` clears an abandoned slice's
|
|
211
|
+
# assignment with it rather than with a second writer.
|
|
212
|
+
# shellcheck source=plot-agent-manifest.sh
|
|
213
|
+
. "$script_dir/plot-agent-manifest.sh"
|
|
214
|
+
|
|
194
215
|
# ---------------------------------------------------------------------------
|
|
195
216
|
# WHERE THE WORKTREES LIVE, and by what name
|
|
196
217
|
# ---------------------------------------------------------------------------
|
|
@@ -239,6 +260,7 @@ no_brief=0
|
|
|
239
260
|
mode=dispatch
|
|
240
261
|
stop_branch=""
|
|
241
262
|
restart_branch=""
|
|
263
|
+
release_branch=""
|
|
242
264
|
# EMPTY MEANS "THE DEFAULT", AND THE DEFAULT IS THE RULE'S. `fleetSize` owns the
|
|
243
265
|
# number and the argument for it; a literal here would be a second copy of a
|
|
244
266
|
# decision that has one home. It is filled from the rule below.
|
|
@@ -275,6 +297,9 @@ while [ $# -gt 0 ]; do
|
|
|
275
297
|
# plan for feature/x", which describes neither what was asked nor what
|
|
276
298
|
# went wrong. The branch is consumed only when it looks like one.
|
|
277
299
|
--restart) mode=restart; case "${2:-}" in */*) restart_branch="$2"; shift ;; esac ;;
|
|
300
|
+
# The same rule once more: a bare `--release <slug>` must not delete a ref
|
|
301
|
+
# named after a plan slug.
|
|
302
|
+
--release) mode=release; case "${2:-}" in */*) release_branch="$2"; shift ;; esac ;;
|
|
278
303
|
# `--start [N]` brings FREE agents into existence — registered, waiting, and
|
|
279
304
|
# holding no slice. The count is OPTIONAL and only a bare number is consumed,
|
|
280
305
|
# the same rule `--stop` and `--restart` apply to a branch: a value that does
|
|
@@ -312,10 +337,11 @@ while [ $# -gt 0 ]; do
|
|
|
312
337
|
shift ;;
|
|
313
338
|
# THE RANGE MOVED WITH THE HEADER IT PRINTS. It ended at `<slug>` and still
|
|
314
339
|
# does; adding `--agent` above pushed that line from 59 to 69, and
|
|
315
|
-
# documenting the plan-declared kind pushed it from 69 to 78
|
|
340
|
+
# documenting the plan-declared kind pushed it from 69 to 78, and
|
|
341
|
+
# `--release` pushed it from 78 to 91. Two records
|
|
316
342
|
# of one fact, and nothing compares them — a stale number here silently
|
|
317
343
|
# truncates the help rather than failing, so it is checked by a test.
|
|
318
|
-
-h|--help) sed -n '2,
|
|
344
|
+
-h|--help) sed -n '2,91p' "$0"; exit 0 ;;
|
|
319
345
|
*) slug="$1" ;;
|
|
320
346
|
esac
|
|
321
347
|
shift
|
|
@@ -428,6 +454,23 @@ json_escape() {
|
|
|
428
454
|
#
|
|
429
455
|
# Written to a temp file and moved into place, so a scan reading the directory
|
|
430
456
|
# never sees a half-written manifest. `mv` within one directory is atomic.
|
|
457
|
+
# The directory the `Agent registry` key names, resolved against a repo root.
|
|
458
|
+
#
|
|
459
|
+
# The case split is `resolve_wt_root`'s: absolute taken as given, relative
|
|
460
|
+
# joined onto the repo root, trailing slash trimmed as pure string work because
|
|
461
|
+
# the directory need not exist yet. One resolver, read by `start_worker` when it
|
|
462
|
+
# writes a manifest and by `--release` when it clears one, so the two cannot
|
|
463
|
+
# look in different places.
|
|
464
|
+
agent_registry_dir() { # $1=repo_root → prints the directory
|
|
465
|
+
local dir
|
|
466
|
+
dir=$("$script_dir/plot-config.sh" get "Agent registry" ".plot/agents")
|
|
467
|
+
case "$dir" in
|
|
468
|
+
/*) ;;
|
|
469
|
+
*) dir="$1/$dir" ;;
|
|
470
|
+
esac
|
|
471
|
+
printf '%s' "${dir%/}"
|
|
472
|
+
}
|
|
473
|
+
|
|
431
474
|
write_agent_manifest() { # $1=path $2=session $3=branch $4=worktree $5=command
|
|
432
475
|
local out="$1" tmp="$1.plot-tmp"
|
|
433
476
|
{
|
|
@@ -1119,12 +1162,7 @@ start_worker() {
|
|
|
1119
1162
|
# resolving a configured directory is a second way to be wrong.
|
|
1120
1163
|
local session manifest_dir
|
|
1121
1164
|
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%/}"
|
|
1165
|
+
manifest_dir=$(agent_registry_dir "$repo_root")
|
|
1128
1166
|
mkdir -p "$manifest_dir" 2>/dev/null || true
|
|
1129
1167
|
# `printf` per field with no interpretation: a command containing quotes,
|
|
1130
1168
|
# newlines or backslashes must survive into valid JSON, and this is the one
|
|
@@ -1171,8 +1209,9 @@ start_worker() {
|
|
|
1171
1209
|
echo " refusing to start $branch — its agent manifest could not be written:"
|
|
1172
1210
|
echo " $manifest_dir/$session.json"
|
|
1173
1211
|
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'
|
|
1212
|
+
echo " a claim only 'plot-dispatch.sh --release $branch' can give up. The worktree"
|
|
1213
|
+
echo " and claim are untouched — fix the path above (see the 'Agent registry'"
|
|
1214
|
+
echo " key) and dispatch again."
|
|
1176
1215
|
return 1
|
|
1177
1216
|
fi
|
|
1178
1217
|
# TWO PIDS, TWO NAMES. `.plot-worker.pid` must name the AGENT — the process
|
|
@@ -1699,7 +1738,8 @@ if [ "$mode" = "stop" ]; then
|
|
|
1699
1738
|
exit 1; }
|
|
1700
1739
|
# The worktree and its claim are left in place: the branch is still taken,
|
|
1701
1740
|
# 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"
|
|
1741
|
+
echo " worktree kept at $wt — the claim stands until you release it:"
|
|
1742
|
+
echo " plot-dispatch.sh --release $stop_branch"
|
|
1703
1743
|
;;
|
|
1704
1744
|
finished*|waiting*|stalled*|failed*|ended*) echo "$stop_branch is not running ($st)" ;;
|
|
1705
1745
|
*) echo "$stop_branch has no worker" ;;
|
|
@@ -1807,6 +1847,232 @@ if [ "$mode" = "restart" ]; then
|
|
|
1807
1847
|
exit 0
|
|
1808
1848
|
fi
|
|
1809
1849
|
|
|
1850
|
+
if [ "$mode" = "release" ]; then
|
|
1851
|
+
# THE COUNTERPART TO --stop THAT NOBODY WROTE. `--stop` ends a worker and
|
|
1852
|
+
# keeps the claim, because stopping is not abandoning. This is the act that
|
|
1853
|
+
# gives an abandoned slice back to the queue.
|
|
1854
|
+
#
|
|
1855
|
+
# THE ASSIGNMENT HAS TWO RECORDS, and this clears both. The remote claim ref
|
|
1856
|
+
# is what the scan and the registry's queue read as *somebody took this*; the
|
|
1857
|
+
# agent manifest's `branch` field is what the registry wrote when it handed
|
|
1858
|
+
# the slice over. Deleting only the ref — the repair `plot-reap.sh` and
|
|
1859
|
+
# `plot-reconcile-scan.sh` leave to a person — leaves the manifest naming a
|
|
1860
|
+
# slice the queue now offers to somebody else. Measured 2026-09-26:
|
|
1861
|
+
# `feature/the-board-filters-to-my-work` was handed out twice after exactly
|
|
1862
|
+
# that hand repair, and the second agent abandoned a desk with unpushed work.
|
|
1863
|
+
#
|
|
1864
|
+
# BEFORE THE PHASE GATE, beside --stop and --restart: a claimed branch is
|
|
1865
|
+
# work in flight whatever the plan's phase now says.
|
|
1866
|
+
if [ -z "$release_branch" ]; then
|
|
1867
|
+
echo "plot-dispatch: --release needs a branch name, e.g. --release feature/x" >&2
|
|
1868
|
+
echo " A slug is not enough: which claim is abandoned is your call, and" >&2
|
|
1869
|
+
echo " a release deletes a ref that cannot be re-created." >&2
|
|
1870
|
+
exit 1
|
|
1871
|
+
fi
|
|
1872
|
+
br="$release_branch"
|
|
1873
|
+
MAIN=$(bash "$script_dir/plot-config.sh" get "Main branch")
|
|
1874
|
+
[ -n "$MAIN" ] || MAIN=$(default_branch)
|
|
1875
|
+
if [ -z "$MAIN" ]; then
|
|
1876
|
+
echo "plot-dispatch: cannot resolve the default branch — refusing to release $br." >&2
|
|
1877
|
+
echo " Nothing was written." >&2
|
|
1878
|
+
exit 1
|
|
1879
|
+
fi
|
|
1880
|
+
|
|
1881
|
+
# THE REMOTE IS READ FRESH. A stale remote-tracking ref would hide commits a
|
|
1882
|
+
# worker pushed since the last fetch, and those are the real work this must
|
|
1883
|
+
# refuse on. A fetch that fails is a remote this cannot read.
|
|
1884
|
+
if ! git fetch -q --prune origin </dev/null 2>/dev/null; then
|
|
1885
|
+
echo "plot-dispatch: cannot fetch origin — refusing to release $br." >&2
|
|
1886
|
+
echo " Without a fresh reading this cannot tell a claim from pushed work." >&2
|
|
1887
|
+
echo " Nothing was written." >&2
|
|
1888
|
+
exit 1
|
|
1889
|
+
fi
|
|
1890
|
+
ref_present=0
|
|
1891
|
+
git rev-parse -q --verify "refs/remotes/origin/$br" >/dev/null 2>&1 && ref_present=1
|
|
1892
|
+
|
|
1893
|
+
# 1. A PULL REQUEST, ASKED FIRST — `handover_refusal`'s order and for its
|
|
1894
|
+
# reason: a worker's exit state says nothing about whether its work reached
|
|
1895
|
+
# review. A merged branch's ref belongs to `plot-release-refs.sh`; an open
|
|
1896
|
+
# one is work under review.
|
|
1897
|
+
#
|
|
1898
|
+
# UNREACHABLE IS NOT "NO PR". `reached_review` reads a failed call as no PR,
|
|
1899
|
+
# which is right for a rendering and wrong here: a deleted ref cannot be
|
|
1900
|
+
# re-created, so a host that cannot be asked refuses. `--offline` promises no
|
|
1901
|
+
# host call, so it refuses too.
|
|
1902
|
+
if [ -n "$offline" ]; then
|
|
1903
|
+
echo "plot-dispatch: --release asks the host whether $br has a PR, and --offline forbids that — refusing." >&2
|
|
1904
|
+
echo " Nothing was written." >&2
|
|
1905
|
+
exit 1
|
|
1906
|
+
fi
|
|
1907
|
+
if ! pr_json=$("$script_dir/plot-host.sh" pr-state "$br" </dev/null 2>/dev/null); then
|
|
1908
|
+
echo "plot-dispatch: the host could not be asked whether $br has a PR — refusing." >&2
|
|
1909
|
+
echo " A release deletes a ref that cannot be re-created, so silence is not" >&2
|
|
1910
|
+
echo " permission. Check the host (plot-host.sh pr-state $br) and retry." >&2
|
|
1911
|
+
echo " Nothing was written." >&2
|
|
1912
|
+
exit 1
|
|
1913
|
+
fi
|
|
1914
|
+
pr_state=$(printf '%s' "$pr_json" | sed -n 's/.*"state":"\([A-Z]*\)".*/\1/p')
|
|
1915
|
+
pr_num=$(printf '%s' "$pr_json" | sed -n 's/.*"number":\([0-9]*\).*/\1/p')
|
|
1916
|
+
# ANY MERGED PR, NOT ONLY THE NEWEST — `pr-merged` reads `mergedAt` over every
|
|
1917
|
+
# PR for the branch, because a newer unmerged PR can mask a merged one.
|
|
1918
|
+
merged_answer=$("$script_dir/plot-host.sh" pr-merged "$br" </dev/null 2>/dev/null) || merged_answer=unknown
|
|
1919
|
+
case "$pr_state:$merged_answer" in
|
|
1920
|
+
OPEN:*|MERGED:*|*:merged)
|
|
1921
|
+
echo "plot-dispatch: $br has a pull request (#${pr_num:-?}, ${pr_state:-MERGED}) — refusing." >&2
|
|
1922
|
+
echo " An open PR is work under review; a merged branch's ref is released by" >&2
|
|
1923
|
+
echo " plot-release-refs.sh when its plan is delivered. Nothing was written." >&2
|
|
1924
|
+
exit 1
|
|
1925
|
+
;;
|
|
1926
|
+
*:unknown|*:)
|
|
1927
|
+
echo "plot-dispatch: the host could not say whether $br ever merged — refusing." >&2
|
|
1928
|
+
echo " Check it (plot-host.sh pr-merged $br) and retry. Nothing was written." >&2
|
|
1929
|
+
exit 1
|
|
1930
|
+
;;
|
|
1931
|
+
esac
|
|
1932
|
+
|
|
1933
|
+
# THE DESK, ASKED OF GIT — never rebuilt from the branch name, the rule every
|
|
1934
|
+
# other verb here follows. A manifest naming this branch may name a desk git
|
|
1935
|
+
# does not list (removed by hand), so its `worktree` is the fallback.
|
|
1936
|
+
release_wt=$(git worktree list --porcelain </dev/null 2>/dev/null | awk -v want="refs/heads/$br" '
|
|
1937
|
+
/^worktree / { path = substr($0, 10) }
|
|
1938
|
+
/^branch / { if (substr($0, 8) == want) { print path; exit } }')
|
|
1939
|
+
registry_dir=$(agent_registry_dir "$repo_root_early")
|
|
1940
|
+
named_manifests=()
|
|
1941
|
+
if [ -d "$registry_dir" ]; then
|
|
1942
|
+
for m in "$registry_dir"/*.json; do
|
|
1943
|
+
[ -f "$m" ] || continue
|
|
1944
|
+
m_branch=$(node -e '
|
|
1945
|
+
try {
|
|
1946
|
+
const m = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
|
|
1947
|
+
process.stdout.write(typeof m.branch === "string" ? m.branch : "");
|
|
1948
|
+
} catch { process.stdout.write(""); }
|
|
1949
|
+
' "$m" 2>/dev/null)
|
|
1950
|
+
[ "$m_branch" = "$br" ] || continue
|
|
1951
|
+
named_manifests+=("$m")
|
|
1952
|
+
if [ -z "$release_wt" ]; then
|
|
1953
|
+
m_wt=$(node -e '
|
|
1954
|
+
try {
|
|
1955
|
+
const m = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
|
|
1956
|
+
process.stdout.write(typeof m.worktree === "string" ? m.worktree : "");
|
|
1957
|
+
} catch { process.stdout.write(""); }
|
|
1958
|
+
' "$m" 2>/dev/null)
|
|
1959
|
+
[ -n "$m_wt" ] && [ -d "$m_wt" ] && release_wt="$m_wt"
|
|
1960
|
+
fi
|
|
1961
|
+
done
|
|
1962
|
+
fi
|
|
1963
|
+
[ -n "$release_wt" ] && [ -d "$release_wt" ] || release_wt=""
|
|
1964
|
+
|
|
1965
|
+
# 2. A LIVE WORKER — the measurement `--restart` makes, through the shared
|
|
1966
|
+
# classifier, never `pgrep` by name. A live pid means somebody is working,
|
|
1967
|
+
# and the one case where a claim is not abandoned.
|
|
1968
|
+
if [ -n "$release_wt" ]; then
|
|
1969
|
+
release_state=$(plot_worker_state "$release_wt" "" | cut -f1,2)
|
|
1970
|
+
case "$release_state" in
|
|
1971
|
+
running*)
|
|
1972
|
+
echo "plot-dispatch: a worker is alive on $br (pid $(printf '%s' "$release_state" | cut -f2)) — refusing." >&2
|
|
1973
|
+
echo " A live worker has not abandoned its claim. Stop it first if you mean to:" >&2
|
|
1974
|
+
echo " plot-dispatch.sh --stop $br" >&2
|
|
1975
|
+
echo " Nothing was written." >&2
|
|
1976
|
+
exit 1
|
|
1977
|
+
;;
|
|
1978
|
+
esac
|
|
1979
|
+
fi
|
|
1980
|
+
|
|
1981
|
+
# 3. REAL WORK. A claim is an EMPTY commit on the default branch's tip, so
|
|
1982
|
+
# it changes no file; `-- .` limits the count to commits that change one,
|
|
1983
|
+
# which is what separates a claim-only ref from pushed work. The local desk
|
|
1984
|
+
# is asked too, because the measured victim held its work in UNPUSHED
|
|
1985
|
+
# commits and uncommitted files that no remote reading can see. A count of
|
|
1986
|
+
# commits, not an ancestry verdict: a count above zero only ever REFUSES.
|
|
1987
|
+
if [ "$ref_present" = 1 ]; then
|
|
1988
|
+
remote_work=$(git rev-list --count "refs/remotes/origin/$MAIN..refs/remotes/origin/$br" -- . </dev/null 2>/dev/null) || remote_work=""
|
|
1989
|
+
if [ -z "$remote_work" ]; then
|
|
1990
|
+
echo "plot-dispatch: cannot count the commits on origin/$br — refusing." >&2
|
|
1991
|
+
echo " Nothing was written." >&2
|
|
1992
|
+
exit 1
|
|
1993
|
+
fi
|
|
1994
|
+
if [ "$remote_work" -gt 0 ]; then
|
|
1995
|
+
echo "plot-dispatch: origin/$br carries $remote_work commit(s) that change files — refusing." >&2
|
|
1996
|
+
echo " That is work, not an abandoned claim. Review it, or open its PR:" >&2
|
|
1997
|
+
echo " plot-open-pr.sh $br" >&2
|
|
1998
|
+
echo " Nothing was written." >&2
|
|
1999
|
+
exit 1
|
|
2000
|
+
fi
|
|
2001
|
+
fi
|
|
2002
|
+
if [ -n "$release_wt" ]; then
|
|
2003
|
+
desk_dirty=$(plot_worker_dirty "$release_wt")
|
|
2004
|
+
if [ "$ref_present" = 1 ]; then desk_base="refs/remotes/origin/$br"; else desk_base="refs/remotes/origin/$MAIN"; fi
|
|
2005
|
+
# Local commits the remote lacks exist nowhere else, so any refuses.
|
|
2006
|
+
desk_unpushed=$(git -C "$release_wt" rev-list --count "$desk_base..HEAD" -- . </dev/null 2>/dev/null) || desk_unpushed=0
|
|
2007
|
+
if [ -n "$desk_dirty" ] || [ "${desk_unpushed:-0}" -gt 0 ]; then
|
|
2008
|
+
echo "plot-dispatch: the desk at $release_wt holds work for $br — refusing." >&2
|
|
2009
|
+
[ "${desk_unpushed:-0}" -gt 0 ] && echo " $desk_unpushed unpushed commit(s) that change files" >&2
|
|
2010
|
+
[ -n "$desk_dirty" ] && echo " uncommitted changes: $(printf '%s' "$desk_dirty" | tr '\n' ' ')" >&2
|
|
2011
|
+
echo " Releasing would hand the slice to an agent that cannot see this work." >&2
|
|
2012
|
+
echo " Push it, or hand the branch to a new worker: plot-dispatch.sh --restart $br" >&2
|
|
2013
|
+
echo " Nothing was written." >&2
|
|
2014
|
+
exit 1
|
|
2015
|
+
fi
|
|
2016
|
+
|
|
2017
|
+
# 4. A PLOT-BLOCKED MARKER: the agent is waiting on a person, and a slice
|
|
2018
|
+
# waiting on an answer is not abandoned. Asked through
|
|
2019
|
+
# `plot_worker_blocked_file`, where the marker's spelling lives.
|
|
2020
|
+
if marker=$(plot_worker_blocked_file "$release_wt") && [ -n "$marker" ]; then
|
|
2021
|
+
echo "plot-dispatch: $br is blocked on a question — refusing." >&2
|
|
2022
|
+
echo " the question is in $release_wt/$marker" >&2
|
|
2023
|
+
echo " Answer it and delete the marker; the agent's slice is not abandoned." >&2
|
|
2024
|
+
echo " Nothing was written." >&2
|
|
2025
|
+
exit 1
|
|
2026
|
+
fi
|
|
2027
|
+
fi
|
|
2028
|
+
|
|
2029
|
+
if [ "$ref_present" = 0 ] && [ "${#named_manifests[@]}" -eq 0 ]; then
|
|
2030
|
+
echo "$br holds no claim: origin/$br does not exist and no manifest names it."
|
|
2031
|
+
echo " Nothing to release."
|
|
2032
|
+
exit 0
|
|
2033
|
+
fi
|
|
2034
|
+
|
|
2035
|
+
# THE MANIFESTS FIRST, THEN THE REF. If the ref deletion fails, the manifest
|
|
2036
|
+
# is already free and the ref still locks, so the scan still reads the slice
|
|
2037
|
+
# as claimed and nothing hands it out twice. The reverse order would leave the
|
|
2038
|
+
# measured failure on disk: no ref, and a manifest still naming the branch.
|
|
2039
|
+
released_manifests=0
|
|
2040
|
+
if [ "${#named_manifests[@]}" -eq 0 ]; then
|
|
2041
|
+
echo " no manifest in $registry_dir names $br"
|
|
2042
|
+
fi
|
|
2043
|
+
for m in ${named_manifests[@]+"${named_manifests[@]}"}; do
|
|
2044
|
+
if ! clear_manifest_branch "$m"; then
|
|
2045
|
+
echo "plot-dispatch: could not clear the assignment in $m — refusing to delete the ref." >&2
|
|
2046
|
+
echo " The claim on origin/$br stands; fix the file and run --release again." >&2
|
|
2047
|
+
exit 1
|
|
2048
|
+
fi
|
|
2049
|
+
released_manifests=$((released_manifests + 1))
|
|
2050
|
+
echo " cleared the assignment in $m"
|
|
2051
|
+
done
|
|
2052
|
+
|
|
2053
|
+
if [ "$ref_present" = 1 ]; then
|
|
2054
|
+
if ! git push -q origin --delete "$br" </dev/null 2>/dev/null; then
|
|
2055
|
+
# VERIFIED, NOT TRUSTED: the host has returned 503 on a push that landed.
|
|
2056
|
+
git fetch -q --prune origin </dev/null 2>/dev/null || true
|
|
2057
|
+
if git rev-parse -q --verify "refs/remotes/origin/$br" >/dev/null 2>&1; then
|
|
2058
|
+
echo "plot-dispatch: could not delete origin/$br — the claim still locks the slice." >&2
|
|
2059
|
+
echo " The manifests are already free, so nothing hands it out twice. Retry:" >&2
|
|
2060
|
+
echo " plot-dispatch.sh --release $br" >&2
|
|
2061
|
+
exit 1
|
|
2062
|
+
fi
|
|
2063
|
+
fi
|
|
2064
|
+
echo " deleted the claim ref origin/$br"
|
|
2065
|
+
else
|
|
2066
|
+
echo " origin/$br does not exist — only the assignment needed releasing"
|
|
2067
|
+
fi
|
|
2068
|
+
if [ -n "$release_wt" ]; then
|
|
2069
|
+
echo " the desk at $release_wt still holds $br and is left as it is"
|
|
2070
|
+
fi
|
|
2071
|
+
echo "released $br — the slice returns to the queue"
|
|
2072
|
+
echo "summary: released=1 manifests=$released_manifests ref=$([ "$ref_present" = 1 ] && echo deleted || echo absent)"
|
|
2073
|
+
exit 0
|
|
2074
|
+
fi
|
|
2075
|
+
|
|
1810
2076
|
if [ "$mode" = "start" ]; then
|
|
1811
2077
|
# THE LAST LINK IN THE CHAIN. `plot-dispatch.sh <slug>` queues slices and the
|
|
1812
2078
|
# registry matches them to free agents, but until this verb existed nothing
|
|
@@ -3063,6 +3329,10 @@ append_started_line() { # $1=file $2=date $3=who $4=branch
|
|
|
3063
3329
|
insert = start
|
|
3064
3330
|
for (i = start + 1; i <= n; i++) {
|
|
3065
3331
|
if (lines[i] ~ /^##[ \t]/) break
|
|
3332
|
+
# An HTML comment ends the writable region. Checked BEFORE the
|
|
3333
|
+
# placeholder arms so a commented-out `- **Started:**` template line
|
|
3334
|
+
# is never mistaken for the slot to fill.
|
|
3335
|
+
if (lines[i] ~ /<!--/) break
|
|
3066
3336
|
if (lines[i] ~ /^[ \t]*[-*][ \t]*\*\*Started:\*\*[ \t]*$/) { slot = i; break }
|
|
3067
3337
|
if (lines[i] ~ /^[ \t]*[-*][ \t]/) insert = i
|
|
3068
3338
|
}
|