@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plot-pm/board",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Local Kanban board for Plot — a glanceable view of plan phases from docs/plans, with sprint and story filters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"dist/board-server.mjs",
|
|
25
25
|
"plot-agent-monitor.sh",
|
|
26
|
+
"plot-agent-manifest.sh",
|
|
26
27
|
"plot-build-monitor.sh",
|
|
27
28
|
"plot-monitor-subject.sh",
|
|
28
29
|
"plot-approve.sh",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Plot helper: the ONE writer that clears an agent manifest's `branch` field.
|
|
3
|
+
#
|
|
4
|
+
# SOURCED, NOT RUN — the shape `plot-worker-state.sh` and `plot-pr-merged.sh`
|
|
5
|
+
# already take. Two scripts clear an assignment and must clear it one way:
|
|
6
|
+
#
|
|
7
|
+
# - `plot-worker-loop.sh` clears its own manifest when a slice finishes, so
|
|
8
|
+
# the window before the next hand-over is observable as `branch: ""`;
|
|
9
|
+
# - `plot-dispatch.sh --release <branch>` clears every manifest naming an
|
|
10
|
+
# abandoned slice, beside deleting the claim ref.
|
|
11
|
+
#
|
|
12
|
+
# The function lived inside `plot-worker-loop.sh` until the second caller
|
|
13
|
+
# arrived. The loop is a script, not a library, so sourcing it would run it; the
|
|
14
|
+
# body moved here unchanged and the loop sources this file instead.
|
|
15
|
+
#
|
|
16
|
+
# Defines one function and does nothing else on load.
|
|
17
|
+
|
|
18
|
+
# Clear `branch` when a slice finishes, so the window before the next one is
|
|
19
|
+
# observable.
|
|
20
|
+
#
|
|
21
|
+
# WHY THIS EXISTS. `free = process alive AND manifest names no branch`, and the
|
|
22
|
+
# second half was unreachable. `seal_declaration` runs the moment a branch is
|
|
23
|
+
# done; `update_manifest_on_hop` runs after `--next` answers and a worktree is
|
|
24
|
+
# built. Between those two points the agent genuinely holds no slice and the
|
|
25
|
+
# manifest still named the last one, so `isFree`'s empty-branch arm — written,
|
|
26
|
+
# exported and unit-tested since `a-dispatch-asks-for-a-free-agent` — had no
|
|
27
|
+
# production caller that could ever satisfy it. Measured 2026-09-02: 2
|
|
28
|
+
# manifests on this estate, neither ever carrying `branch: ""`.
|
|
29
|
+
#
|
|
30
|
+
# `branch` AND ONLY `branch`. `worktree` still names the desk the agent is
|
|
31
|
+
# sitting at — it has not moved, and clearing it would take the transcript join
|
|
32
|
+
# and the liveness check with it, since both are keyed on the worktree path.
|
|
33
|
+
# `wavesCount` counts hops and no hop has happened yet. The node one-liner
|
|
34
|
+
# round-trips the whole object, so every other field survives verbatim, the same
|
|
35
|
+
# property `update_manifest_on_hop` records.
|
|
36
|
+
#
|
|
37
|
+
# ADDED, NOT SUBSTITUTED. The hop still rewrites `branch` and `worktree`
|
|
38
|
+
# together; this writes the empty value that sits between two slices. A worker
|
|
39
|
+
# that finishes its last branch exits with the manifest cleared and the exit
|
|
40
|
+
# trap removes the file, so the empty value is never a leftover.
|
|
41
|
+
#
|
|
42
|
+
# ABSENT IS NOT A FAILURE. No manifest — a hand-started loop, an older
|
|
43
|
+
# dispatcher — means there is nothing to clear and nothing to report, so this
|
|
44
|
+
# returns 0 like `update_manifest_on_hop` does.
|
|
45
|
+
clear_manifest_branch() { # $1=manifest
|
|
46
|
+
local manifest="$1"
|
|
47
|
+
[ -f "$manifest" ] || return 0
|
|
48
|
+
|
|
49
|
+
local tmp="$manifest.plot-free-tmp"
|
|
50
|
+
node -e '
|
|
51
|
+
const fs = require("fs");
|
|
52
|
+
const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
53
|
+
manifest.branch = "";
|
|
54
|
+
fs.writeFileSync(process.argv[2], JSON.stringify(manifest, null, 2) + "\n");
|
|
55
|
+
' "$manifest" "$tmp" 2>/dev/null || { rm -f "$tmp"; return 1; }
|
|
56
|
+
|
|
57
|
+
mv -f "$tmp" "$manifest" 2>/dev/null || { rm -f "$tmp"; return 1; }
|
|
58
|
+
}
|
package/plot-approve.sh
CHANGED
|
@@ -227,7 +227,25 @@ else
|
|
|
227
227
|
pr_branch="idea/$slug"
|
|
228
228
|
fi
|
|
229
229
|
|
|
230
|
-
|
|
230
|
+
# THE EXIT CODE IS THE READING, not the emptiness of stdout. `pr-state` exits 0
|
|
231
|
+
# with `state: NONE` when the host answered that the branch has no PR, and
|
|
232
|
+
# non-zero when the host could not be asked: 3 for a refused or failed call
|
|
233
|
+
# (a rate limit included), 4 for a backend with no answer at all. Only the
|
|
234
|
+
# first is an absence. The other two stop here with the host's own words and
|
|
235
|
+
# name no repair to the branch, because nothing about the branch was read.
|
|
236
|
+
pr_err_file=$(mktemp "${TMPDIR:-/tmp}/plot-approve-pr.XXXXXX")
|
|
237
|
+
pr_rc=0
|
|
238
|
+
pr_json=$(bash "$script_dir/plot-host.sh" pr-state "$pr_branch" 2>"$pr_err_file") || pr_rc=$?
|
|
239
|
+
pr_err=$(cat "$pr_err_file" 2>/dev/null); rm -f "$pr_err_file"
|
|
240
|
+
if [ "$pr_rc" = 4 ]; then
|
|
241
|
+
die "the host backend has no answer for the PR state of '$pr_branch' (plot-host.sh pr-state exited 4).
|
|
242
|
+
${pr_err:-The host adapter gave no reason.}
|
|
243
|
+
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."
|
|
244
|
+
elif [ "$pr_rc" != 0 ]; then
|
|
245
|
+
die "the host could not be asked for the PR of '$pr_branch' (plot-host.sh pr-state exited $pr_rc).
|
|
246
|
+
${pr_err:-The host adapter gave no reason.}
|
|
247
|
+
The plan was not approved and its phase is unchanged. Wait for the host to answer again, then re-run the approval."
|
|
248
|
+
fi
|
|
231
249
|
[ -n "$pr_json" ] || pr_json='{"number":0,"state":"NONE","draft":false,"url":""}'
|
|
232
250
|
pr_number=$(printf '%s' "$pr_json" | jq -r '.number // 0' 2>/dev/null)
|
|
233
251
|
pr_state=$(printf '%s' "$pr_json" | jq -r '.state // "NONE"' 2>/dev/null)
|
|
@@ -420,6 +438,10 @@ append_approved_line() { # $1=in $2=out $3=record
|
|
|
420
438
|
insert = start
|
|
421
439
|
for (i = start + 1; i <= n; i++) {
|
|
422
440
|
if (lines[i] ~ /^##[ \t]/) break
|
|
441
|
+
# An HTML comment ends the writable region. Checked BEFORE the
|
|
442
|
+
# placeholder arms so a commented-out `- **Approved:**` template line
|
|
443
|
+
# is never mistaken for the slot to fill.
|
|
444
|
+
if (lines[i] ~ /<!--/) break
|
|
423
445
|
if (lines[i] ~ /^[ \t]*[-*][ \t]*\*\*Approved:\*\*[ \t]*$/) { slot = i; break }
|
|
424
446
|
if (lines[i] ~ /^[ \t]*[-*][ \t]/) insert = i
|
|
425
447
|
}
|
package/plot-budget.sh
CHANGED
|
@@ -221,7 +221,7 @@ BUDGET_FALLBACK_WINDOW_MS=3600000
|
|
|
221
221
|
# which is a reading about whichever pool was spent last — so a caller deciding
|
|
222
222
|
# whether a bucket is spent must name that bucket. `graphql_budget_spent` does,
|
|
223
223
|
# and this is why.
|
|
224
|
-
|
|
224
|
+
budget_rate_read() {
|
|
225
225
|
local connector="${1:-}" account="${2:-}" bucket="${3:-}" now="${4:-}"
|
|
226
226
|
local path
|
|
227
227
|
[ -n "$now" ] || now="$(budget_now_ms)"
|
|
@@ -321,6 +321,171 @@ budget_rate() {
|
|
|
321
321
|
' "$path" 2>/dev/null || echo '{"spent":0,"spanMs":0,"perHour":null,"lines":0,"unreadable":0,"limit":null,"remaining":null,"resetAt":null,"basis":"unknown"}'
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
# THE SAME ANSWER IS SCANNED FOR ONCE PER PROCESS, and that is the whole of this
|
|
325
|
+
# memo. Measured on `quatico/quaweb-website` at Plot 2.19.0: `budget.tsv` holds
|
|
326
|
+
# 312589 lines across 17.6 MB, one read of it takes **516 ms** against 5 ms over
|
|
327
|
+
# fifty lines, and a single `pr-list` calls this three times for the same answer.
|
|
328
|
+
# The file is append-only and a process that asks twice is asking about the same
|
|
329
|
+
# window, so the second scan buys nothing a caller can observe.
|
|
330
|
+
#
|
|
331
|
+
# THE MEMO IS A FILE, AND A VARIABLE ALONE COULD NOT HAVE WORKED. Every caller
|
|
332
|
+
# in `plot-host.sh` writes `rate="$(budget_rate ...)"`, and a command
|
|
333
|
+
# substitution is a SUBSHELL: a variable the function sets inside one dies when
|
|
334
|
+
# the substitution closes. Measured on this branch with a counter — three
|
|
335
|
+
# substituted calls on one key scanned the ledger 3 times with a variable memo
|
|
336
|
+
# in place, against 1 for three direct calls. A variable memo is not slow at the
|
|
337
|
+
# call sites that exist, it is ABSENT from them, and every behavioural test
|
|
338
|
+
# still passes because the three answers are identical.
|
|
339
|
+
#
|
|
340
|
+
# `$$`, NOT `BASHPID`, IS THE SCOPE. `$$` is the invoking shell's pid and is
|
|
341
|
+
# deliberately NOT updated inside a subshell, so it names the process TREE —
|
|
342
|
+
# which is exactly "the life of the process" this memo is bounded by. `BASHPID`
|
|
343
|
+
# tracks the fork and would give every substitution its own empty cache, which
|
|
344
|
+
# is the variable memo's defect with an extra file write.
|
|
345
|
+
#
|
|
346
|
+
# THE COST IS PAID BACK ABOUT TWO THOUSAND TIMES. Measured here: a builtin
|
|
347
|
+
# `read` of one memo line is 82 us and a write 153 us, against 188 ms for one
|
|
348
|
+
# scan of a 40000-line ledger and the 516 ms reported above. The read is a
|
|
349
|
+
# builtin with no fork, which is what keeps it in that range.
|
|
350
|
+
#
|
|
351
|
+
# PER PROCESS IS THE WHOLE BOUND. There is no TTL, no `stat` of the record and
|
|
352
|
+
# no invalidation hook, because a `plot-host.sh` invocation is short-lived and a
|
|
353
|
+
# memo that tried to notice the file moving would re-`stat` on every call — the
|
|
354
|
+
# cost this exists to remove, re-introduced in a smaller form. `budget_append`
|
|
355
|
+
# writes between two reads and the second read is deliberately served the first
|
|
356
|
+
# one's answer.
|
|
357
|
+
#
|
|
358
|
+
# THE KEY IS THE TRIPLE, NEVER ONE FIELD OF IT. An EMPTY bucket means *every
|
|
359
|
+
# bucket* and is a different question from any named one — `budget_rate_read`
|
|
360
|
+
# says so in its own words above, and the two live in one process: measured with
|
|
361
|
+
# a probe, one `pr-list` asks `(github,jwloka,'')` for the concurrency bound and
|
|
362
|
+
# `(github,jwloka,graphql)` for the transport choice. A memo keyed on less than
|
|
363
|
+
# the triple answers the first with the second's reading.
|
|
364
|
+
#
|
|
365
|
+
# AN EXPLICIT `now` BYPASSES THE MEMO ENTIRELY, and a reader will ask why. It is
|
|
366
|
+
# a FOURTH question rather than a fourth key: a caller naming a moment is asking
|
|
367
|
+
# what the record looked like THEN, and every window boundary above is computed
|
|
368
|
+
# from it. Keying on it would make every lookup a miss, because the callers that
|
|
369
|
+
# omit it get `budget_now_ms()` and differ by milliseconds — a memo that is dead
|
|
370
|
+
# code and still passes every behavioural test. Serving a memo across different
|
|
371
|
+
# `now` values would answer a named moment with another one's window. Only the
|
|
372
|
+
# callers that omit it — every one in `plot-host.sh` — are memoised.
|
|
373
|
+
#
|
|
374
|
+
# NOT-YET-COMPUTED AND COMPUTED-TO-EMPTY ARE TWO STATES, and the FILE'S
|
|
375
|
+
# EXISTENCE is what separates them. The zero object is a well-formed answer for
|
|
376
|
+
# a missing record and for a `budget_path` that failed, so it is CACHED like any
|
|
377
|
+
# other — a memo that treated it as no-answer-worth-keeping would restore the
|
|
378
|
+
# scan on exactly the machines with nothing to scan. The entry is never empty:
|
|
379
|
+
# `budget_rate_read` prints a JSON object on every path, so a zero-byte entry
|
|
380
|
+
# means a torn write and is re-read rather than served.
|
|
381
|
+
#
|
|
382
|
+
# PUBLISHED BY `mv`, NEVER BY `>`, for `budget_slot_acquire`'s reason one
|
|
383
|
+
# paragraph down: a redirect creates the NAME before the CONTENT, so a sibling
|
|
384
|
+
# subshell can open the file and read half an answer. The rename is atomic
|
|
385
|
+
# within a directory, so the name and the answer arrive together.
|
|
386
|
+
budget_rate() {
|
|
387
|
+
local connector="${1:-}" account="${2:-}" bucket="${3:-}" now="${4:-}"
|
|
388
|
+
|
|
389
|
+
# A caller that named a moment is asking a different question. Straight
|
|
390
|
+
# through, neither read nor written.
|
|
391
|
+
if [ -n "$now" ]; then
|
|
392
|
+
budget_rate_read "$connector" "$account" "$bucket" "$now"
|
|
393
|
+
return $?
|
|
394
|
+
fi
|
|
395
|
+
|
|
396
|
+
local key slot
|
|
397
|
+
key="${connector}|${account}|${bucket}"
|
|
398
|
+
# Every character a variable name may not hold becomes `_`. Two distinct
|
|
399
|
+
# triples could collide only by differing in punctuation alone, which no
|
|
400
|
+
# connector, account or bucket name does.
|
|
401
|
+
slot="_budget_rate_memo_$(printf '%s' "$key" | LC_ALL=C tr -c '[:alnum:]_' '_')"
|
|
402
|
+
|
|
403
|
+
# TIER ONE, FREE: the same shell asking twice. SET, NOT NON-EMPTY — the zero
|
|
404
|
+
# object is a real answer and an empty one is a state this memo never stores.
|
|
405
|
+
if eval "[ -n \"\${${slot}+set}\" ]"; then
|
|
406
|
+
eval "printf '%s\\n' \"\${${slot}}\""
|
|
407
|
+
return 0
|
|
408
|
+
fi
|
|
409
|
+
|
|
410
|
+
# TIER TWO, 82 us: a subshell asking what its parent already asked. This is
|
|
411
|
+
# the tier that fires at every call site `plot-host.sh` actually has.
|
|
412
|
+
local dir file answer
|
|
413
|
+
dir="$(budget_memo_dir)" || dir=''
|
|
414
|
+
if [ -n "$dir" ]; then
|
|
415
|
+
file="$dir/$slot"
|
|
416
|
+
# `-s`, NOT `-f`. A zero-byte entry is a torn write, never an answer.
|
|
417
|
+
if [ -s "$file" ]; then
|
|
418
|
+
IFS= read -r answer < "$file" 2>/dev/null || answer=''
|
|
419
|
+
if [ -n "$answer" ]; then
|
|
420
|
+
eval "${slot}=\$answer"
|
|
421
|
+
printf '%s\n' "$answer"
|
|
422
|
+
return 0
|
|
423
|
+
fi
|
|
424
|
+
fi
|
|
425
|
+
fi
|
|
426
|
+
|
|
427
|
+
local rc
|
|
428
|
+
# THE EXIT CODE IS THE READ'S, never this wrapper's. `budget_rate_read`
|
|
429
|
+
# returns 0 on every path today and its callers test the CONTENT, so a memo
|
|
430
|
+
# that invented an exit status would be the one place the two disagree.
|
|
431
|
+
answer="$(budget_rate_read "$connector" "$account" "$bucket")"; rc=$?
|
|
432
|
+
if [ "$rc" -ne 0 ]; then
|
|
433
|
+
# A read that failed is not an answer, so nothing is remembered: the next
|
|
434
|
+
# caller asks again rather than inheriting a failure for the process life.
|
|
435
|
+
printf '%s\n' "$answer"
|
|
436
|
+
return "$rc"
|
|
437
|
+
fi
|
|
438
|
+
eval "${slot}=\$answer"
|
|
439
|
+
# NEVER FAILS ITS CALLER, `budget_append`'s rule for the same reason: the memo
|
|
440
|
+
# is an optimisation beside an answer that is already correct, so a cache that
|
|
441
|
+
# cannot be written must not turn a good reading into a failed one.
|
|
442
|
+
if [ -n "$dir" ] && mkdir -p "$dir" 2>/dev/null; then
|
|
443
|
+
if printf '%s\n' "$answer" >"$file.$BASHPID.tmp" 2>/dev/null; then
|
|
444
|
+
mv -f "$file.$BASHPID.tmp" "$file" 2>/dev/null || rm -f "$file.$BASHPID.tmp" 2>/dev/null || true
|
|
445
|
+
fi
|
|
446
|
+
fi
|
|
447
|
+
printf '%s\n' "$answer"
|
|
448
|
+
return 0
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
# Where THIS PROCESS's memoised rates live, and it is deliberately not beside
|
|
452
|
+
# the record. `$PLOT_BUDGET_HOME/memo/<pid>` — same override as the record and
|
|
453
|
+
# the slots, so a test pointing `PLOT_BUDGET_HOME` at a sandbox gets a sandboxed
|
|
454
|
+
# cache too, and a `$PLOT_BUDGET_HOME` that cannot be resolved means no cache
|
|
455
|
+
# rather than a cache in the wrong place.
|
|
456
|
+
#
|
|
457
|
+
# `$$` IS THE DIRECTORY NAME because it is the one identifier that is stable
|
|
458
|
+
# across a command substitution — see `budget_rate` above, where that property
|
|
459
|
+
# is the whole reason this file exists. A pid is reused by the kernel after the
|
|
460
|
+
# process ends, so an entry could in principle be inherited by a later,
|
|
461
|
+
# unrelated process holding the same pid. That is bounded by `budget_memo_clear`
|
|
462
|
+
# below, which the adapter calls on exit, and it is why the cache holds a
|
|
463
|
+
# DERIVED reading rather than anything a caller could act on irreversibly: the
|
|
464
|
+
# worst case is one stale rate, which is the same staleness the memo grants
|
|
465
|
+
# within a process by design.
|
|
466
|
+
budget_memo_dir() {
|
|
467
|
+
local home="${PLOT_BUDGET_HOME:-}"
|
|
468
|
+
if [ -z "$home" ]; then
|
|
469
|
+
[ -n "${HOME:-}" ] || return 1
|
|
470
|
+
home="$HOME/.plot/state"
|
|
471
|
+
fi
|
|
472
|
+
printf '%s\n' "$home/memo/$$"
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
# Removes this process's memo directory. Called on exit by the adapter, so a
|
|
476
|
+
# long-lived machine does not accumulate one directory per `plot-host.sh` call.
|
|
477
|
+
#
|
|
478
|
+
# NEVER FAILS ITS CALLER. It runs in a trap beside work that has already
|
|
479
|
+
# happened, and a cache that cannot be cleared must not change an exit status.
|
|
480
|
+
budget_memo_clear() {
|
|
481
|
+
local dir
|
|
482
|
+
dir="$(budget_memo_dir)" || return 0
|
|
483
|
+
case "$dir" in
|
|
484
|
+
*/memo/[0-9]*) rm -rf "$dir" 2>/dev/null || true ;;
|
|
485
|
+
esac
|
|
486
|
+
return 0
|
|
487
|
+
}
|
|
488
|
+
|
|
324
489
|
# ── The concurrency bound ────────────────────────────────────────────────────
|
|
325
490
|
#
|
|
326
491
|
# HOW MANY CALLS THIS ACCOUNT HAS OPEN AT ONCE, bounded across PROCESSES. The
|
package/plot-config.sh
CHANGED
|
@@ -80,6 +80,13 @@
|
|
|
80
80
|
# here can invoke a skill. Absent (or `none`) = the button
|
|
81
81
|
# refuses and names this key as the fix, rather than
|
|
82
82
|
# accepting the click and doing nothing.
|
|
83
|
+
# Interrogate command how the board runs `/plot-panel <plan path>` on a Draft
|
|
84
|
+
# plan (the card's `Interrogate` button); the prompt is
|
|
85
|
+
# appended as one argument and names a FILE the board
|
|
86
|
+
# wrote. REQUIRED for the same reason as `Idea command`:
|
|
87
|
+
# a panel fans out N agents reading a plan, and no script
|
|
88
|
+
# can do that. Absent (or `none`) = the button renders
|
|
89
|
+
# disabled and names this key as the fix.
|
|
83
90
|
# Brief command how /plot-dispatch runs an agent headless to WRITE a
|
|
84
91
|
# missing hand-off brief. The prompt is appended as one
|
|
85
92
|
# argument and asks for `/plot-implement <slug>`, whose
|
|
@@ -95,6 +102,16 @@
|
|
|
95
102
|
# Hosts plans yes | no (no = refuse plan files)
|
|
96
103
|
# Tracker plot | jira | github-issues | linear (+ URL)
|
|
97
104
|
# (plot = plans in this repo ARE the tracker; absent = same)
|
|
105
|
+
# Tracker delivered status
|
|
106
|
+
# the status word a plan's issues are set to when the plan
|
|
107
|
+
# reaches Delivered, in the tracker's own vocabulary
|
|
108
|
+
# (`In Review`, `Done`). Read by plot-issue-status.sh.
|
|
109
|
+
# Absent or empty = Delivered writes nothing — never an
|
|
110
|
+
# empty status, and never the released word instead.
|
|
111
|
+
# Tracker released status
|
|
112
|
+
# the same for Released. A team whose *Done* means
|
|
113
|
+
# *shipped* sets only this one; a team watching progress
|
|
114
|
+
# sets both.
|
|
98
115
|
# Ticket prefixes the tracker project keys this repository's work lives
|
|
99
116
|
# in, comma-separated (`PROJ-A, PROJ-B`). NOT
|
|
100
117
|
# `Branch prefixes`, which sits next to it and holds
|
|
@@ -142,7 +159,23 @@ if [ "$cmd" != "get" ]; then
|
|
|
142
159
|
exit 1
|
|
143
160
|
fi
|
|
144
161
|
|
|
145
|
-
|
|
162
|
+
# THE CALLER'S ROOT IS TAKEN WHERE IT OFFERS ONE. `git rev-parse` is ~5 ms and
|
|
163
|
+
# this script runs once per config key, so a caller reading several keys pays
|
|
164
|
+
# for the same constant repeatedly. Measured on CI 2026-09-25: one board build
|
|
165
|
+
# spawned `git rev-parse --show-toplevel` 21 times out of 42 git processes
|
|
166
|
+
# total, which `plan-read-shape.test.mjs` caught as the spawn count crossing
|
|
167
|
+
# its bound. The board already exports `PLOT_REPO_ROOT` (`index.ts:65`) and
|
|
168
|
+
# `plot-deliver.sh` and `plot-issue-status.sh` already read it.
|
|
169
|
+
#
|
|
170
|
+
# IT MUST BE A DIRECTORY, and a wrong one falls back rather than failing: an
|
|
171
|
+
# exported stale path would otherwise make every config read answer from a
|
|
172
|
+
# repository that is not this one, silently. Asking git is the safe answer and
|
|
173
|
+
# stays the default for every caller that offers nothing.
|
|
174
|
+
if [ -n "${PLOT_REPO_ROOT:-}" ] && [ -d "$PLOT_REPO_ROOT" ]; then
|
|
175
|
+
root="$PLOT_REPO_ROOT"
|
|
176
|
+
else
|
|
177
|
+
root=$(git rev-parse --show-toplevel 2>/dev/null) || root="."
|
|
178
|
+
fi
|
|
146
179
|
|
|
147
180
|
# Find the first repo-root file that contains a ## Plot Config section.
|
|
148
181
|
# 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.
|
|
@@ -654,6 +657,12 @@ git -C "$tmpwt" add -- "${ACTIVE_DIR#/}" >/dev/null 2>&1 || true
|
|
|
654
657
|
git -C "$tmpwt" add -- "${DELIVERED_DIR#/}" >/dev/null 2>&1 || true
|
|
655
658
|
[ "$sprint_report" = "updated" ] && git -C "$tmpwt" add -- "${SPRINT_DIR#/}" >/dev/null 2>&1
|
|
656
659
|
|
|
660
|
+
# THE BOOKED PLAN, KEPT FOR THE TRACKER. The booking worktree is removed on
|
|
661
|
+
# every exit below, and the working tree may still read `Approved`; the issue
|
|
662
|
+
# status is decided from the file that reached the default branch.
|
|
663
|
+
booked_plan=$(mktemp "${TMPDIR:-/tmp}/plot-deliver-plan.XXXXXX")
|
|
664
|
+
cp "$tmpwt/$rel" "$booked_plan" 2>/dev/null || : > "$booked_plan"
|
|
665
|
+
|
|
657
666
|
if git -C "$tmpwt" diff --cached --quiet 2>/dev/null; then
|
|
658
667
|
# THE IDEMPOTENT EXIT. Everything this run would have written was already
|
|
659
668
|
# on the default branch, so there is nothing to push and nothing wrong.
|
|
@@ -694,13 +703,25 @@ else
|
|
|
694
703
|
echo "plot-deliver: the delivery is committed on '$bookbr' but could not reach $MAIN." >&2
|
|
695
704
|
echo " Land '$bookbr' by hand, or re-run this command once the push works." >&2
|
|
696
705
|
git worktree remove --force "$tmpwt" >/dev/null 2>&1 || true
|
|
697
|
-
|
|
706
|
+
rm -f "$booked_plan"
|
|
707
|
+
# NOTHING REACHED THE DEFAULT BRANCH, so no status is owed yet.
|
|
708
|
+
echo "summary: phase=$phase_report record=$record_report index=$index_report sprint=$sprint_report push=$push_report tracker=skipped"
|
|
698
709
|
exit 1
|
|
699
710
|
fi
|
|
700
711
|
fi
|
|
701
712
|
fi
|
|
702
713
|
|
|
703
|
-
|
|
714
|
+
# THE TRACKER HEARS AFTER THE DELIVERY LANDED, and never decides it. The plan is
|
|
715
|
+
# delivered; the tracker holds a copy of one fact about it. Every outcome of
|
|
716
|
+
# plot-issue-status.sh, a failed write and an unreadable bundle included, is a
|
|
717
|
+
# report on the summary line and leaves this script's exit code alone.
|
|
718
|
+
tracker_out=$(bash "$script_dir/plot-issue-status.sh" "$booked_plan" 2>&1)
|
|
719
|
+
printf '%s\n' "$tracker_out" | grep -v '^summary: ' | sed '/^$/d; s/^/ tracker: /'
|
|
720
|
+
tracker_report=$(printf '%s' "$tracker_out" | sed -n 's/^summary: tracker=\([a-z-]*\).*/\1/p' | tail -1)
|
|
721
|
+
[ -n "$tracker_report" ] || tracker_report="failed"
|
|
722
|
+
rm -f "$booked_plan"
|
|
723
|
+
|
|
724
|
+
echo "summary: phase=$phase_report record=$record_report index=$index_report sprint=$sprint_report push=$push_report tracker=$tracker_report"
|
|
704
725
|
# THE RECEIPT IS SPENT HERE, on the action COMPLETING — never at the gate.
|
|
705
726
|
# `plot-controller-gate.sh` clears on a receipt and LEAVES it, so an
|
|
706
727
|
# interrupted run can be repeated on the same licence: this script documents
|