@plot-pm/board 0.16.1 → 0.16.3
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 +167 -139
- package/package.json +2 -3
- package/plot-agent-manifest.sh +111 -15
- package/plot-agent-monitor.sh +43 -5
- package/plot-approve.sh +234 -74
- package/plot-build-monitor.sh +49 -4
- package/plot-config.sh +10 -14
- package/plot-deliver.sh +10 -732
- package/plot-desk-root.sh +127 -0
- package/plot-dispatch.sh +313 -135
- package/plot-fleet-scan.sh +746 -62
- package/plot-host.sh +356 -43
- package/plot-monitor-subject.sh +55 -3
- package/plot-plan-meta.sh +155 -139
- package/plot-pr-merged.sh +16 -0
- package/plot-reap.sh +82 -93
- package/plot-state-receipt.sh +12 -2
- package/plot-tmp.sh +90 -14
- package/plot-transcript-quiet.sh +3 -1
- package/plot-worker-state.sh +633 -24
- package/plot-resolve-artifact.sh +0 -429
- package/plot-worker-monitor.sh +0 -705
package/plot-approve.sh
CHANGED
|
@@ -107,12 +107,13 @@ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
|
107
107
|
. "$script_dir/plot-state-receipt.sh"
|
|
108
108
|
|
|
109
109
|
dry_run=0
|
|
110
|
-
|
|
110
|
+
who_flag_given=0
|
|
111
|
+
who_flag=""
|
|
111
112
|
slug=""
|
|
112
113
|
while [ $# -gt 0 ]; do
|
|
113
114
|
case "$1" in
|
|
114
115
|
--dry-run) dry_run=1 ;;
|
|
115
|
-
--who)
|
|
116
|
+
--who) who_flag_given=1; who_flag="${2:-}"; shift ;;
|
|
116
117
|
-h|--help) sed -n '2,12p' "$0"; exit 0 ;;
|
|
117
118
|
-*) echo "plot-approve: unknown flag '$1'" >&2; exit 1 ;;
|
|
118
119
|
*) slug="$1" ;;
|
|
@@ -128,7 +129,13 @@ git rev-parse --git-dir >/dev/null 2>&1 || die "not a git repository — run thi
|
|
|
128
129
|
cfg() { bash "$script_dir/plot-config.sh" get "$1" "$2"; }
|
|
129
130
|
|
|
130
131
|
repo_root=$(git rev-parse --show-toplevel)
|
|
131
|
-
|
|
132
|
+
# The booking worktree goes under the desk root of the MAIN checkout, never of
|
|
133
|
+
# this tree: inside a desk `--show-toplevel` answers the desk, and the default
|
|
134
|
+
# `.worktrees` would resolve beneath it. No fallback — `plot-desk-root.sh`.
|
|
135
|
+
# shellcheck source=plot-desk-root.sh
|
|
136
|
+
. "$script_dir/plot-desk-root.sh"
|
|
137
|
+
main_root=$(plot_repo_root)
|
|
138
|
+
wt_root=$(plot_desk_root "$main_root") || die "cannot resolve where the booking worktree goes (see above)"
|
|
132
139
|
|
|
133
140
|
PLAN_DIR=$(cfg "Plan directory" "docs/plans/")
|
|
134
141
|
ACTIVE_DIR=$(cfg "Active index" "docs/plans/active/")
|
|
@@ -189,11 +196,40 @@ esac
|
|
|
189
196
|
# NONE means a pre-Plot-2 plan on an idea branch, which the skill documents as
|
|
190
197
|
# `pr` by default. An unrecognised value is refused rather than defaulted:
|
|
191
198
|
# "carry on" is the shape of stale assumption this whole story keeps finding.
|
|
199
|
+
#
|
|
200
|
+
# `in-session` IS NO LONGER A REFUSAL HERE, which is this slice's whole change.
|
|
201
|
+
# `plot-approve.sh --who <handle> <slug>` performs the seven mechanical steps
|
|
202
|
+
# for it exactly as it does for `pr`, because #1185 made the domain able to
|
|
203
|
+
# decide the write (`approveTransition` takes `who` and `people`). What stays a
|
|
204
|
+
# refusal is the absence of a usable reviewer: no `--who` at all, an empty one,
|
|
205
|
+
# or one `People` does not declare — the domain's `review-human` and
|
|
206
|
+
# `reviewer-undeclared` gates, asked through `decide_transition` below rather
|
|
207
|
+
# than re-implemented here. There is no `--reviewer`: a round-1 panel measured
|
|
208
|
+
# that spelling exiting 2 at the controller gate, and `--who` is the one flag
|
|
209
|
+
# this script already declares.
|
|
210
|
+
#
|
|
211
|
+
# UNDER `PLOT_UNATTENDED=1` THIS STILL REFUSES, `--who` or not. The reviewer is
|
|
212
|
+
# a human in a session that, by definition, has nobody in it under an
|
|
213
|
+
# unattended run — the same argument `plot-approve/SKILL.md` already makes for
|
|
214
|
+
# the skill's own in-session walkthrough. A default here would let the machine
|
|
215
|
+
# name the reviewer, which is exactly what `--who`'s absence of a fallback (no
|
|
216
|
+
# `PLOT_APPROVE_WHO`, no `git config user.name`) refuses for the same channel.
|
|
217
|
+
in_session=0
|
|
192
218
|
case "$review" in
|
|
193
219
|
pr|NONE) ;;
|
|
194
220
|
in-session)
|
|
195
|
-
|
|
196
|
-
|
|
221
|
+
if [ "${PLOT_UNATTENDED:-}" = "1" ]; then
|
|
222
|
+
die "plan '$slug' declares 'Review: in-session' — the reviewer is a human in the room.
|
|
223
|
+
Refusing under PLOT_UNATTENDED=1: there is nobody here to name. Approve it from a session: /plot-approve $slug"
|
|
224
|
+
fi
|
|
225
|
+
# No reviewer named: refuse before any git work. The domain's `review-human`
|
|
226
|
+
# gate says the same thing later, after a booking worktree exists; asking
|
|
227
|
+
# here keeps a refusal local and cheap. `--who` has no default.
|
|
228
|
+
if [ -z "$(printf '%s' "$who_flag" | tr -d '[:space:]')" ]; then
|
|
229
|
+
die "plan '$slug' declares 'Review: in-session' — name the reviewer with --who."
|
|
230
|
+
fi
|
|
231
|
+
in_session=1
|
|
232
|
+
;;
|
|
197
233
|
ballot)
|
|
198
234
|
die "plan '$slug' declares 'Review: ballot' — the tally is the approval.
|
|
199
235
|
A script cannot read a ballot. Approve it with /plot-approve $slug." ;;
|
|
@@ -202,71 +238,134 @@ case "$review" in
|
|
|
202
238
|
Refusing rather than treating it as 'pr' — that would approve a plan nobody discussed." ;;
|
|
203
239
|
esac
|
|
204
240
|
|
|
241
|
+
# The declared handles, for the domain's `reviewer-undeclared` gate. Read
|
|
242
|
+
# unconditionally — cheap, and a `pr` or `ballot` plan never reaches the branch
|
|
243
|
+
# that uses it — from `People`'s `handle = Spelling; handle = Spelling` form:
|
|
244
|
+
# only the text before each `=` is a handle, and `People` may be absent.
|
|
245
|
+
people_raw=$(cfg "People" "")
|
|
246
|
+
people_csv=$(printf '%s' "$people_raw" | tr ';' '\n' | while IFS= read -r entry; do
|
|
247
|
+
h="${entry%%=*}"
|
|
248
|
+
h="$(printf '%s' "$h" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//' | tr '[:upper:]' '[:lower:]')"
|
|
249
|
+
[ -n "$h" ] && printf '%s\n' "$h"
|
|
250
|
+
done | paste -sd, -)
|
|
251
|
+
[ -n "$people_csv" ] || people_csv=""
|
|
252
|
+
|
|
253
|
+
# Read even for in-session: the booking worktree below (every non-same-branch
|
|
254
|
+
# flow, in-session included) fetches and books against it.
|
|
205
255
|
MAIN=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##')
|
|
206
256
|
[ -n "$MAIN" ] || MAIN=$(bash "$script_dir/plot-host.sh" default-branch 2>/dev/null) || MAIN=""
|
|
207
257
|
[ -n "$MAIN" ] || MAIN="main"
|
|
208
258
|
|
|
209
|
-
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
[ -z "$p" ] && continue
|
|
220
|
-
[ "$p" = "idea" ] && continue
|
|
221
|
-
if git show-ref --verify --quiet "refs/heads/$p/$slug" \
|
|
222
|
-
|| git show-ref --verify --quiet "refs/remotes/origin/$p/$slug"; then
|
|
223
|
-
pr_branch="$p/$slug"
|
|
224
|
-
break
|
|
225
|
-
fi
|
|
226
|
-
done
|
|
259
|
+
if [ "$in_session" = 1 ]; then
|
|
260
|
+
# No plan PR to read or merge: the approval IS the reviewer's go, not a host
|
|
261
|
+
# state. `same_branch` stays 0 — in-session records through the same booking
|
|
262
|
+
# worktree the `pr` flow uses, since an in-session plan has no work branch of
|
|
263
|
+
# its own to record on.
|
|
264
|
+
same_branch=0
|
|
265
|
+
pr_number=0
|
|
266
|
+
pr_state="NONE"
|
|
267
|
+
pr_draft="false"
|
|
268
|
+
echo "step: plan $plan_file — phase=$phase review=${review} impl=${impl} (in-session, no plan PR)"
|
|
227
269
|
else
|
|
228
|
-
|
|
229
|
-
|
|
270
|
+
# The PR that carries the plan. `Impl: same branch` puts plan and code on the
|
|
271
|
+
# work branch, so its PR is the WORK branch's — and it must not be merged here
|
|
272
|
+
# (it merges once, at the end, carrying the implementation with it).
|
|
273
|
+
same_branch=0
|
|
274
|
+
[ "$impl" = "same-branch" ] && same_branch=1
|
|
275
|
+
|
|
276
|
+
if [ "$same_branch" = 1 ]; then
|
|
277
|
+
pr_branch="$slug"
|
|
278
|
+
for p in $(cfg "Branch prefixes" "idea/, feature/, bug/, docs/, infra/" | tr ',' ' '); do
|
|
279
|
+
p="${p%/}"; p="${p# }"
|
|
280
|
+
[ -z "$p" ] && continue
|
|
281
|
+
[ "$p" = "idea" ] && continue
|
|
282
|
+
if git show-ref --verify --quiet "refs/heads/$p/$slug" \
|
|
283
|
+
|| git show-ref --verify --quiet "refs/remotes/origin/$p/$slug"; then
|
|
284
|
+
pr_branch="$p/$slug"
|
|
285
|
+
break
|
|
286
|
+
fi
|
|
287
|
+
done
|
|
288
|
+
else
|
|
289
|
+
pr_branch="idea/$slug"
|
|
290
|
+
fi
|
|
230
291
|
|
|
231
|
-
# THE EXIT CODE IS THE READING, not the emptiness of stdout. `pr-state` exits 0
|
|
232
|
-
# with `state: NONE` when the host answered that the branch has no PR, and
|
|
233
|
-
# non-zero when the host could not be asked: 3 for a refused or failed call
|
|
234
|
-
# (a rate limit included), 4 for a backend with no answer at all. Only the
|
|
235
|
-
# first is an absence. The other two stop here with the host's own words and
|
|
236
|
-
# name no repair to the branch, because nothing about the branch was read.
|
|
237
|
-
pr_err_file=""
|
|
238
|
-
plot_tmpfile pr_err_file approve-pr
|
|
239
|
-
pr_rc=0
|
|
240
|
-
pr_json=$(bash "$script_dir/plot-host.sh" pr-state "$pr_branch" 2>"$pr_err_file") || pr_rc=$?
|
|
241
|
-
pr_err=$(cat "$pr_err_file" 2>/dev/null); rm -f "$pr_err_file"
|
|
242
|
-
if [ "$pr_rc" = 4 ]; then
|
|
243
|
-
|
|
292
|
+
# THE EXIT CODE IS THE READING, not the emptiness of stdout. `pr-state` exits 0
|
|
293
|
+
# with `state: NONE` when the host answered that the branch has no PR, and
|
|
294
|
+
# non-zero when the host could not be asked: 3 for a refused or failed call
|
|
295
|
+
# (a rate limit included), 4 for a backend with no answer at all. Only the
|
|
296
|
+
# first is an absence. The other two stop here with the host's own words and
|
|
297
|
+
# name no repair to the branch, because nothing about the branch was read.
|
|
298
|
+
pr_err_file=""
|
|
299
|
+
plot_tmpfile pr_err_file approve-pr
|
|
300
|
+
pr_rc=0
|
|
301
|
+
pr_json=$(bash "$script_dir/plot-host.sh" pr-state "$pr_branch" 2>"$pr_err_file") || pr_rc=$?
|
|
302
|
+
pr_err=$(cat "$pr_err_file" 2>/dev/null); rm -f "$pr_err_file"
|
|
303
|
+
if [ "$pr_rc" = 4 ]; then
|
|
304
|
+
die "the host backend has no answer for the PR state of '$pr_branch' (plot-host.sh pr-state exited 4).
|
|
244
305
|
${pr_err:-The host adapter gave no reason.}
|
|
245
306
|
This backend cannot report a PR's state, so the approval cannot read its gate. The plan was not approved and its phase is unchanged."
|
|
246
|
-
elif [ "$pr_rc" != 0 ]; then
|
|
247
|
-
|
|
307
|
+
elif [ "$pr_rc" != 0 ]; then
|
|
308
|
+
die "the host could not be asked for the PR of '$pr_branch' (plot-host.sh pr-state exited $pr_rc).
|
|
248
309
|
${pr_err:-The host adapter gave no reason.}
|
|
249
310
|
The plan was not approved and its phase is unchanged. Wait for the host to answer again, then re-run the approval."
|
|
250
|
-
fi
|
|
251
|
-
[ -n "$pr_json" ] || pr_json='{"number":0,"state":"NONE","draft":false,"url":""}'
|
|
252
|
-
pr_number=$(printf '%s' "$pr_json" | jq -r '.number // 0' 2>/dev/null)
|
|
253
|
-
pr_state=$(printf '%s' "$pr_json" | jq -r '.state // "NONE"' 2>/dev/null)
|
|
254
|
-
pr_draft=$(printf '%s' "$pr_json" | jq -r '.draft // false' 2>/dev/null)
|
|
255
|
-
|
|
256
|
-
# --- refusal 3: the PR ------------------------------------------------------
|
|
257
|
-
case "$pr_state" in
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
311
|
+
fi
|
|
312
|
+
[ -n "$pr_json" ] || pr_json='{"number":0,"state":"NONE","draft":false,"url":""}'
|
|
313
|
+
pr_number=$(printf '%s' "$pr_json" | jq -r '.number // 0' 2>/dev/null)
|
|
314
|
+
pr_state=$(printf '%s' "$pr_json" | jq -r '.state // "NONE"' 2>/dev/null)
|
|
315
|
+
pr_draft=$(printf '%s' "$pr_json" | jq -r '.draft // false' 2>/dev/null)
|
|
316
|
+
|
|
317
|
+
# --- refusal 3: the PR ------------------------------------------------------
|
|
318
|
+
case "$pr_state" in
|
|
319
|
+
MERGED) ;;
|
|
320
|
+
OPEN) ;;
|
|
321
|
+
CLOSED)
|
|
322
|
+
die "the plan PR for '$slug' (#$pr_number) is closed.
|
|
262
323
|
Reopen it on the host, or push '$pr_branch' again and open a new one." ;;
|
|
263
|
-
|
|
264
|
-
|
|
324
|
+
NONE|*)
|
|
325
|
+
die "no PR found for branch '$pr_branch'.
|
|
265
326
|
Push the branch: git push -u origin $pr_branch
|
|
266
327
|
Then open its PR — or run /plot-idea, which does both." ;;
|
|
267
|
-
esac
|
|
328
|
+
esac
|
|
329
|
+
fi
|
|
268
330
|
|
|
269
|
-
|
|
331
|
+
# --- refusal 4: a branch under no slice heading -----------------------------
|
|
332
|
+
#
|
|
333
|
+
# BEFORE THE MERGE, which is the whole point of its position. Step 2 merges the
|
|
334
|
+
# plan PR, and a refusal fired after that leaves the PR merged, the plan still
|
|
335
|
+
# Draft, and a refusal the operator can clear only by editing a merged plan. So
|
|
336
|
+
# it sits with refusals 1 to 3, reading the same `$meta` step 1 took, and it
|
|
337
|
+
# writes nothing.
|
|
338
|
+
#
|
|
339
|
+
# The heading is the slice's name on the board and the title of its PR
|
|
340
|
+
# (`openSlicePr` refuses `slice-unnamed`), so it is owed on the default branch
|
|
341
|
+
# before any agent starts. Without this an agent meets that refusal, writes the
|
|
342
|
+
# heading on its own branch, and the board reads `(unnamed)` until the PR
|
|
343
|
+
# merges (#1057).
|
|
344
|
+
#
|
|
345
|
+
# The rule is the domain's and it is asked, never re-implemented here:
|
|
346
|
+
# `decide_transition` asks the same one through the same bundle, so the two can
|
|
347
|
+
# never disagree about one plan.
|
|
348
|
+
transition_check_mjs="$script_dir/board/plot-transition.mjs"
|
|
349
|
+
if [ -f "$transition_check_mjs" ]; then
|
|
350
|
+
slice_err_file=""
|
|
351
|
+
plot_tmpfile slice_err_file approve-slices
|
|
352
|
+
slice_rc=0
|
|
353
|
+
printf '%s' "$meta" | node "$transition_check_mjs" --check-slices "$slug" \
|
|
354
|
+
2>"$slice_err_file" >/dev/null || slice_rc=$?
|
|
355
|
+
slice_err=$(cat "$slice_err_file" 2>/dev/null); rm -f "$slice_err_file"
|
|
356
|
+
if [ "$slice_rc" = 1 ]; then
|
|
357
|
+
die "$(printf '%s' "$slice_err" | cut -f2-)
|
|
358
|
+
The plan was not approved, nothing was merged and its phase is unchanged."
|
|
359
|
+
elif [ "$slice_rc" != 0 ]; then
|
|
360
|
+
die "cannot read the slices of '$plan_file' (plot-transition.mjs --check-slices exited $slice_rc).
|
|
361
|
+
${slice_err:-The bundle gave no reason.}
|
|
362
|
+
Refusing rather than approving a plan whose slice headings were never read."
|
|
363
|
+
fi
|
|
364
|
+
else
|
|
365
|
+
echo "plot-approve: cannot find $transition_check_mjs — slice headings went unchecked. Run 'pnpm build:board'." >&2
|
|
366
|
+
fi
|
|
367
|
+
|
|
368
|
+
[ "$in_session" = 1 ] || echo "step: plan $plan_file — phase=$phase review=${review} impl=${impl} pr=#$pr_number($pr_state)"
|
|
270
369
|
|
|
271
370
|
# --- a DRAFT is taken out of draft, not refused ------------------------------
|
|
272
371
|
#
|
|
@@ -284,16 +383,35 @@ echo "step: plan $plan_file — phase=$phase review=${review} impl=${impl} pr=#$
|
|
|
284
383
|
# mergeable on either host — and stated as its own `step:` line so a caller
|
|
285
384
|
# reading the output can see which half happened if the second one fails.
|
|
286
385
|
|
|
287
|
-
|
|
386
|
+
# `in-session` TAKES NO DEFAULT. A default here would let the machine name the
|
|
387
|
+
# reviewer, which is the one thing a script must never do for a channel that
|
|
388
|
+
# exists because a human is in the room — so `--who` alone answers, and an
|
|
389
|
+
# empty or absent one reaches the domain's `review-human` refusal below rather
|
|
390
|
+
# than silently becoming `git config user.name`. `pr` and the direct/same-branch
|
|
391
|
+
# flow keep the existing chain: `PLOT_APPROVE_WHO`, then the git identity.
|
|
392
|
+
if [ "$in_session" = 1 ]; then
|
|
393
|
+
who="$who_flag"
|
|
394
|
+
channel="in-session"
|
|
395
|
+
else
|
|
396
|
+
who="${who_flag:-${PLOT_APPROVE_WHO:-$(git config user.name 2>/dev/null || echo plot)}}"
|
|
397
|
+
fi
|
|
288
398
|
today=$(date +%Y-%m-%d)
|
|
289
399
|
|
|
290
400
|
if [ "$dry_run" = 1 ]; then
|
|
291
|
-
[ "$
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
401
|
+
if [ "$in_session" = 1 ]; then
|
|
402
|
+
echo "step: would flip Phase → Approved and fill Approved: $today, $who, in-session"
|
|
403
|
+
echo "step: would clear .plot/hold entries for: $(printf '%s' "$plan_branches" | tr '\n' ' ')"
|
|
404
|
+
echo "step: would update the sprint annotation${sprint:+ in $SPRINT_DIR (sprint $sprint)}"
|
|
405
|
+
echo "step: would append a row to .plot/state/in-session-approvals.tsv"
|
|
406
|
+
echo "summary: merged=skipped-in-session phase=would record=would holds=would sprint=would push=would"
|
|
407
|
+
else
|
|
408
|
+
[ "$pr_draft" = "true" ] && echo "step: would mark PR #$pr_number ready for review"
|
|
409
|
+
echo "step: would merge PR #$pr_number"
|
|
410
|
+
echo "step: would flip Phase → Approved and fill Approved: $today, $who, plan-PR #$pr_number merged"
|
|
411
|
+
echo "step: would clear .plot/hold entries for: $(printf '%s' "$plan_branches" | tr '\n' ' ')"
|
|
412
|
+
echo "step: would update the sprint annotation${sprint:+ in $SPRINT_DIR (sprint $sprint)}"
|
|
413
|
+
echo "summary: merged=would phase=would record=would holds=would sprint=would push=would"
|
|
414
|
+
fi
|
|
297
415
|
exit 0
|
|
298
416
|
fi
|
|
299
417
|
|
|
@@ -307,7 +425,11 @@ fi
|
|
|
307
425
|
# Merge commits, not squash: plan refinement history is the context a later
|
|
308
426
|
# reader wants. `--delete-branch` retires idea/<slug>, which has no further job.
|
|
309
427
|
merged_report="already"
|
|
310
|
-
if [ "$
|
|
428
|
+
if [ "$in_session" = 1 ]; then
|
|
429
|
+
# No plan PR exists for this channel: the approval is the reviewer's go,
|
|
430
|
+
# read nowhere on a host.
|
|
431
|
+
merged_report="skipped-in-session"
|
|
432
|
+
elif [ "$same_branch" = 1 ]; then
|
|
311
433
|
# Plan and code ride one branch; the PR merges once, at the end, and merging
|
|
312
434
|
# it here would land an unfinished implementation on the default branch.
|
|
313
435
|
merged_report="skipped-same-branch"
|
|
@@ -507,16 +629,30 @@ decide_transition() { # $1=file $2=channel → prints "<Phase>\t<record>\t<writ
|
|
|
507
629
|
|| { echo "plot-approve: cannot find $transition_mjs — run 'pnpm build:board'." >&2; return 1; }
|
|
508
630
|
m=$(bash "$script_dir/plot-plan-meta.sh" "$f" 2>/dev/null) || m=""
|
|
509
631
|
[ -n "$m" ] || { echo "plot-approve: cannot parse $f — refusing rather than guessing." >&2; return 1; }
|
|
510
|
-
|
|
632
|
+
# THE SLICES TRAVEL WITH THE TRANSITION, not only with the early check above.
|
|
633
|
+
# Measured 2026-10-02 by disabling that check: this call approved an unnamed
|
|
634
|
+
# plan outright — merged, flipped and recorded — because the field line
|
|
635
|
+
# carries no slices and the domain reads an absent reading as unmeasured. So
|
|
636
|
+
# the rule is asked twice, the second time from the file this re-parses, which
|
|
637
|
+
# on the `pr` flow is the plan on the default branch.
|
|
638
|
+
#
|
|
639
|
+
# Out of band rather than a field of its own, which is why `People` widening
|
|
640
|
+
# the line to twelve did not touch this: the slices are a nested list, and
|
|
641
|
+
# `requestFrom` pads nothing.
|
|
642
|
+
local slices_file=""
|
|
643
|
+
plot_tmpfile slices_file approve-transition-slices
|
|
644
|
+
printf '%s' "$m" > "$slices_file"
|
|
645
|
+
answer=$(printf 'approve\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t\t%s\n' \
|
|
511
646
|
"$slug" \
|
|
512
647
|
"$(printf '%s' "$m" | jq -r '.phase // ""')" \
|
|
513
648
|
"$(printf '%s' "$m" | jq -r '.review // ""')" \
|
|
514
649
|
"$(printf '%s' "$m" | jq -r '.approved_raw // ""')" \
|
|
515
650
|
"$(printf '%s' "$m" | jq -r '.delivered_raw // ""')" \
|
|
516
651
|
"$(printf '%s' "$m" | jq -r '.released_raw // ""')" \
|
|
517
|
-
"$today" "$who" "$channel" \
|
|
518
|
-
| node "$transition_mjs" 2>&1)
|
|
652
|
+
"$today" "$who" "$channel" "$people_csv" \
|
|
653
|
+
| node "$transition_mjs" --slices "$slices_file" 2>&1)
|
|
519
654
|
rc=$?
|
|
655
|
+
rm -f "$slices_file"
|
|
520
656
|
# Exit 1 is the domain's refusal and its sentence, tab-separated after the
|
|
521
657
|
# rule that fired. Exit 2 is this script handing it something unreadable,
|
|
522
658
|
# which no operator can act on — so it reports as the bug it is.
|
|
@@ -684,10 +820,11 @@ apply_local_writes() { # $1=root → sets phase_report record_report holds_repo
|
|
|
684
820
|
# here rather than trusted from the caller's copy, because on the `pr` flow
|
|
685
821
|
# those are different files and the plan on the default branch is the one that
|
|
686
822
|
# counts.
|
|
687
|
-
local
|
|
688
|
-
[ "$same_branch" = 1 ] &&
|
|
823
|
+
local write_channel="plan-PR #$pr_number merged"
|
|
824
|
+
[ "$same_branch" = 1 ] && write_channel="plan-PR #$pr_number reviewed"
|
|
825
|
+
[ "$in_session" = 1 ] && write_channel="$channel"
|
|
689
826
|
local decided record action recorded
|
|
690
|
-
decided=$(decide_transition "$f" "$
|
|
827
|
+
decided=$(decide_transition "$f" "$write_channel") || return 1
|
|
691
828
|
record=$(printf '%s' "$decided" | cut -f2)
|
|
692
829
|
action=$(printf '%s' "$decided" | cut -f3)
|
|
693
830
|
recorded=$(printf '%s' "$decided" | cut -f4)
|
|
@@ -735,6 +872,9 @@ else
|
|
|
735
872
|
|
|
736
873
|
bookbr="plot/approve-$slug"
|
|
737
874
|
tmpwt="$wt_root/.plot-approve-$slug.$$"
|
|
875
|
+
# `git worktree add` creates the desk root when it is missing, so exclude it
|
|
876
|
+
# first: a booking run must not leave `.worktrees/` untracked in `git status`.
|
|
877
|
+
plot_exclude_desk_root "$main_root"
|
|
738
878
|
# -B: a leftover branch from an earlier failed run must not block this one.
|
|
739
879
|
# It is disposable by construction — created here, pushed, deleted.
|
|
740
880
|
git worktree add -q -B "$bookbr" "$tmpwt" "origin/$MAIN" 2>/dev/null \
|
|
@@ -782,13 +922,16 @@ else
|
|
|
782
922
|
cleanup
|
|
783
923
|
else
|
|
784
924
|
# BRANCH PROTECTION FALLBACK — the only path where a micro-PR is right.
|
|
785
|
-
# Never leave the
|
|
786
|
-
#
|
|
925
|
+
# Never leave the approved plan stranded at `Phase: Draft`: the approval
|
|
926
|
+
# is decided and irreversible once committed, so the recorded phase must
|
|
927
|
+
# follow it.
|
|
928
|
+
micro_body="Records the approval of \`$slug\` (plan-PR #$pr_number merged)."
|
|
929
|
+
[ "$in_session" = 1 ] && micro_body="Records the in-session approval of \`$slug\` by $who."
|
|
787
930
|
echo "step: push rejected — opening a micro-PR instead"
|
|
788
931
|
if git push -q origin "$bookbr" 2>/dev/null \
|
|
789
932
|
&& micro_url=$(bash "$script_dir/plot-host.sh" pr-create \
|
|
790
933
|
--title "plot: approve $slug" \
|
|
791
|
-
--body "
|
|
934
|
+
--body "$micro_body" \
|
|
792
935
|
--base "$MAIN" --head "$bookbr" 2>/dev/null) \
|
|
793
936
|
&& micro_num=$(printf '%s' "$micro_url" | sed 's#.*/##') \
|
|
794
937
|
&& bash "$script_dir/plot-host.sh" pr-merge "$micro_num" --delete-branch >/dev/null 2>&1
|
|
@@ -798,8 +941,10 @@ else
|
|
|
798
941
|
cleanup
|
|
799
942
|
else
|
|
800
943
|
push_report="rejected"
|
|
944
|
+
stranded_reason="PR #$pr_number IS MERGED"
|
|
945
|
+
[ "$in_session" = 1 ] && stranded_reason="the in-session approval IS DECIDED"
|
|
801
946
|
echo "plot-approve: the approval is committed on '$bookbr' but could not reach $MAIN." >&2
|
|
802
|
-
echo "
|
|
947
|
+
echo " $stranded_reason — the plan must not stay at Phase: Draft." >&2
|
|
803
948
|
echo " Land '$bookbr' by hand, or re-run this command once the push works." >&2
|
|
804
949
|
git worktree remove --force "$tmpwt" >/dev/null 2>&1 || true
|
|
805
950
|
echo "summary: merged=$merged_report phase=$phase_report record=$record_report holds=$holds_report sprint=$sprint_report push=$push_report"
|
|
@@ -809,6 +954,21 @@ else
|
|
|
809
954
|
fi
|
|
810
955
|
fi
|
|
811
956
|
|
|
957
|
+
# THE LOG REPLACES A COUNT, NEVER A RECEIPT. Appended ONLY after the push
|
|
958
|
+
# landed this run — `nothing-to-commit` means an earlier run already recorded
|
|
959
|
+
# this approval (and, with it, this row), so appending again would double a
|
|
960
|
+
# count that exists to say how many in-session approvals happened, not how
|
|
961
|
+
# many times this script ran. A refused or interrupted run reaches neither
|
|
962
|
+
# this line nor a row, which is `plot-agent-settings.sh`'s rule applied here:
|
|
963
|
+
# read the exit code, not the emptiness.
|
|
964
|
+
if [ "$in_session" = 1 ] && [ "$push_report" != "nothing-to-commit" ] && [ "$push_report" != "n/a" ]; then
|
|
965
|
+
entry="script"
|
|
966
|
+
[ "${PLOT_APPROVE_ENTRY:-}" = "board" ] && entry="board"
|
|
967
|
+
log_dir="$main_root/.plot/state"
|
|
968
|
+
mkdir -p "$log_dir" 2>/dev/null \
|
|
969
|
+
&& printf '%s\t%s\t%s\t%s\n' "$today" "$slug" "$who" "$entry" >> "$log_dir/in-session-approvals.tsv" 2>/dev/null
|
|
970
|
+
fi
|
|
971
|
+
|
|
812
972
|
echo "summary: merged=$merged_report phase=$phase_report record=$record_report holds=$holds_report sprint=$sprint_report push=$push_report"
|
|
813
973
|
# THE RECEIPT IS SPENT HERE, on the action COMPLETING — never at the gate.
|
|
814
974
|
# `plot-controller-gate.sh` clears on a receipt and LEAVES it, so an
|
package/plot-build-monitor.sh
CHANGED
|
@@ -101,9 +101,13 @@ Started by plot-dispatch.sh inside the worker's wrapper. Reads its subject from
|
|
|
101
101
|
the environment, exactly as the wrapper's other children do:
|
|
102
102
|
|
|
103
103
|
PLOT_BRANCH the branch whose builds this monitor will report
|
|
104
|
-
PLOT_WORKTREE the desk it
|
|
104
|
+
PLOT_WORKTREE the desk it STARTS on; it follows the manifest's
|
|
105
|
+
`worktree` from then on (PLOT_MANIFEST_FILE below)
|
|
106
|
+
PLOT_MANIFEST_FILE the manifest this agent is named in, re-read each pass;
|
|
107
|
+
absent or unreadable keeps watching PLOT_WORKTREE
|
|
105
108
|
PLOT_MONITOR_FILE where findings are published (default:
|
|
106
|
-
$PLOT_WORKTREE/.plot-worker.monitor.build.jsonl)
|
|
109
|
+
$PLOT_WORKTREE/.plot-worker.monitor.build.jsonl). Set
|
|
110
|
+
explicitly, it WINS and does not follow a hop.
|
|
107
111
|
PLOT_MONITOR_INTERVAL seconds between passes (default 30)
|
|
108
112
|
|
|
109
113
|
--once take one sample and exit, rather than looping. A test
|
|
@@ -124,7 +128,17 @@ done
|
|
|
124
128
|
monitor='BuildMonitor'
|
|
125
129
|
|
|
126
130
|
branch="${PLOT_BRANCH:-}"
|
|
127
|
-
|
|
131
|
+
# The branch at start, kept for a detached or unreadable desk; `monitor_branch`
|
|
132
|
+
# re-reads the desk on every pass.
|
|
133
|
+
start_branch="$branch"
|
|
134
|
+
# THE DESK THIS MONITOR WAS LAUNCHED ON, FIXED FOR ITS WHOLE LIFE. `worktree`
|
|
135
|
+
# itself is no longer fixed: `monitor_pass` reassigns it every pass by asking
|
|
136
|
+
# `plot_watched_desk`, which follows a hop to the manifest's new `worktree`
|
|
137
|
+
# field. This is the launch desk `plot_watched_desk` falls back to when the
|
|
138
|
+
# manifest carries no override — never read directly after startup.
|
|
139
|
+
launched_worktree="${PLOT_WORKTREE:-}"
|
|
140
|
+
worktree="$launched_worktree"
|
|
141
|
+
manifest_file="${PLOT_MANIFEST_FILE:-}"
|
|
128
142
|
# THIRTY SECONDS IS AFFORDABLE ONLY BECAUSE OF THE SILENCE RULE. This cadence
|
|
129
143
|
# matches the WorkerMonitor's rather than the AgentMonitor's, and it asks a HOST
|
|
130
144
|
# — which would be the rate problem the AgentMonitor's 300 s exists to avoid,
|
|
@@ -132,7 +146,11 @@ worktree="${PLOT_WORKTREE:-}"
|
|
|
132
146
|
# is bounded by how long a build takes, not by how long a worker lives.
|
|
133
147
|
interval="${PLOT_MONITOR_INTERVAL:-30}"
|
|
134
148
|
|
|
135
|
-
|
|
149
|
+
# IF `PLOT_MONITOR_FILE` IS SET IT WINS AND DOES NOT FOLLOW THE DESK — the
|
|
150
|
+
# existing contract this usage text already states. Otherwise `findings`
|
|
151
|
+
# follows `worktree`, reassigned alongside it at the top of `monitor_pass`.
|
|
152
|
+
monitor_file_override="${PLOT_MONITOR_FILE:-}"
|
|
153
|
+
findings="${monitor_file_override:-${worktree:+$worktree/.plot-worker.monitor.build.jsonl}}"
|
|
136
154
|
|
|
137
155
|
# THE SUBJECT, read the same way the other two monitors read it.
|
|
138
156
|
pid_file="${PLOT_PID_FILE:-${worktree:+$worktree/.plot-worker.pid}}"
|
|
@@ -209,6 +227,25 @@ monitor_head_sha() { # → prints a sha, or nothing
|
|
|
209
227
|
git -C "$worktree" rev-parse --verify --quiet HEAD 2>/dev/null || true
|
|
210
228
|
}
|
|
211
229
|
|
|
230
|
+
# Which branch does the desk hold, right now?
|
|
231
|
+
#
|
|
232
|
+
# → prints the branch, or the branch the monitor started with when the desk is
|
|
233
|
+
# detached or unreadable
|
|
234
|
+
#
|
|
235
|
+
# READ ON EVERY PASS, NOT ONCE. A free agent starts with an empty `PLOT_BRANCH`
|
|
236
|
+
# and is handed its slices later, in the same desk; a dispatched agent hops to
|
|
237
|
+
# its next slice in the same desk too. A branch fixed at start made this monitor
|
|
238
|
+
# unaskable for every free agent (`monitor_run_for_sha` returns 2 on an empty
|
|
239
|
+
# branch) and wrong after every hop, so the correction path in
|
|
240
|
+
# `plot-worker-loop.sh` received no finding for most slices.
|
|
241
|
+
monitor_branch() { # → prints a branch, or nothing
|
|
242
|
+
local current=''
|
|
243
|
+
if [ -n "$worktree" ] && [ -d "$worktree" ]; then
|
|
244
|
+
current=$(git -C "$worktree" branch --show-current 2>/dev/null || true)
|
|
245
|
+
fi
|
|
246
|
+
printf '%s' "${current:-$start_branch}"
|
|
247
|
+
}
|
|
248
|
+
|
|
212
249
|
# What does the host say about the run for ONE sha?
|
|
213
250
|
#
|
|
214
251
|
# → prints the run JSON, or nothing when there is no run for it
|
|
@@ -376,6 +413,14 @@ sample_finding() { # → prints "finding\tevidence", or nothing
|
|
|
376
413
|
# One full pass: sample, publish only on a change of ANSWER-ABOUT-A-COMMIT.
|
|
377
414
|
monitor_pass() {
|
|
378
415
|
local row finding evidence head
|
|
416
|
+
# REASSIGNED ONCE, HERE, RATHER THAN THREADED THROUGH EACH READER. `worktree`
|
|
417
|
+
# feeds `monitor_head_sha`, `monitor_branch` and `publish()`'s own field, so a
|
|
418
|
+
# hop is picked up in one place rather than three. `PLOT_MONITOR_FILE`, when
|
|
419
|
+
# set, WINS and does not follow the desk — the existing contract the usage
|
|
420
|
+
# text states.
|
|
421
|
+
worktree=$(plot_watched_desk "$manifest_file" "$launched_worktree")
|
|
422
|
+
findings="${monitor_file_override:-${worktree:+$worktree/.plot-worker.monitor.build.jsonl}}"
|
|
423
|
+
branch=$(monitor_branch)
|
|
379
424
|
head=$(monitor_head_sha)
|
|
380
425
|
row=$(sample_finding)
|
|
381
426
|
finding="${row%% *}"
|
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,14 +94,15 @@
|
|
|
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
108
|
# /plot-init writes `PLOT_UNATTENDED=1 plot-worker-loop.sh`;
|