@plot-pm/board 0.16.2 → 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 +162 -139
- package/package.json +1 -3
- package/plot-agent-manifest.sh +111 -15
- package/plot-agent-monitor.sh +43 -5
- package/plot-approve.sh +224 -73
- package/plot-build-monitor.sh +49 -4
- package/plot-deliver.sh +10 -741
- package/plot-dispatch.sh +263 -91
- package/plot-fleet-scan.sh +746 -62
- package/plot-host.sh +273 -18
- 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 +61 -28
- package/plot-state-receipt.sh +12 -2
- package/plot-tmp.sh +31 -3
- package/plot-transcript-quiet.sh +3 -1
- package/plot-worker-state.sh +611 -24
- package/plot-resolve-artifact.sh +0 -423
- package/plot-worker-monitor.sh +0 -705
package/plot-deliver.sh
CHANGED
|
@@ -1,744 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# <slug> the plan to deliver
|
|
7
|
-
# Output: one `step:` line per step, then a machine-countable summary:
|
|
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.
|
|
12
|
-
# Exit 0 when the plan is Delivered on the default branch (whether this
|
|
13
|
-
# run did the work or found it already done); 1 on a refusal or a
|
|
14
|
-
# failure, with the reason on stderr.
|
|
15
|
-
#
|
|
16
|
-
# WHY THIS EXISTS. The board computes `allSlicesMerged` — exactly the condition
|
|
17
|
-
# that says a plan is ready to deliver — but the transition itself lives only in
|
|
18
|
-
# `/plot-deliver`'s prose. So `Delivered` in the board asked for a caller with
|
|
19
|
-
# nothing safe to call, and an implementer reaching that point would have
|
|
20
|
-
# rebuilt the phase flip, the `Delivered:` record and the symlink move in
|
|
21
|
-
# TypeScript. That is precisely the drift the `plot-approve.sh` split removed.
|
|
22
|
-
#
|
|
23
|
-
# This script is the `plot-approve.sh` of delivery: one implementation, two
|
|
24
|
-
# entrances. `/plot-deliver` keeps the judgement — the completeness check, the
|
|
25
|
-
# partial-deliverable question — and delegates the writes here. The board calls
|
|
26
|
-
# this script directly (or via an agent when a `Deliver command` is set).
|
|
27
|
-
#
|
|
28
|
-
# Manifesto Principle 3 draws the line: SCRIPTS COLLECT AND REPORT; SKILLS
|
|
29
|
-
# INTERPRET AND ADAPT. Flipping a phase and writing a dated record is
|
|
30
|
-
# collecting. Deciding whether PARTIAL work counts as delivered is interpreting,
|
|
31
|
-
# and stays in the skill.
|
|
32
|
-
#
|
|
33
|
-
# IT IS IDEMPOTENT, BECAUSE ONE STEP CANNOT BE UNDONE. The push is irreversible;
|
|
34
|
-
# everything before it is local. A run interrupted between flipping the phase
|
|
35
|
-
# and pushing leaves the plan at `Delivered` in a worktree but still `Approved`
|
|
36
|
-
# on main — so `plot-deliver.sh <slug>` may run any number of times, and RUN IT
|
|
37
|
-
# AGAIN is the repair for every interruption.
|
|
38
|
-
#
|
|
39
|
-
# Each step asks THE SOURCE IT WOULD HAVE WRITTEN whether it is already done:
|
|
40
|
-
# the plan file for the phase and the record, the index directories for the
|
|
41
|
-
# symlink, the sprint file for the tick. Never a progress file of its own.
|
|
42
|
-
#
|
|
43
|
-
# WHAT IT REFUSES, and why refusing beats guessing:
|
|
44
|
-
# - phase is not `approved` — nothing to deliver. (Already-Delivered is NOT a
|
|
45
|
-
# refusal: it is the idempotent case, and the run still checks the record
|
|
46
|
-
# and index.)
|
|
47
|
-
# - any non-deferred branch is unmerged — work is not done. This is one of
|
|
48
|
-
# Plot's four phase guardrails, moved from prose into an exit code.
|
|
49
|
-
#
|
|
50
|
-
# macOS bash 3.2 throughout: no associative arrays, no bash-4 line readers.
|
|
2
|
+
# Launcher. The mechanical half of delivering (or releasing) a plan is
|
|
3
|
+
# `board/plot-deliver.mjs`, a JS entry point over the domain — see its header
|
|
4
|
+
# for what it does. This resolves it, passes arguments and the exit code
|
|
5
|
+
# through, and decides nothing.
|
|
51
6
|
set -uo pipefail
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
# other writer of a `State:` line. Sourced rather than run: the gate and the
|
|
58
|
-
# three owning scripts must agree on where a receipt lives, and one file is how.
|
|
59
|
-
# shellcheck source=plot-state-receipt.sh
|
|
60
|
-
. "$script_dir/plot-state-receipt.sh"
|
|
61
|
-
|
|
62
|
-
dry_run=0
|
|
63
|
-
who_override=""
|
|
64
|
-
slug=""
|
|
65
|
-
while [ $# -gt 0 ]; do
|
|
66
|
-
case "$1" in
|
|
67
|
-
--dry-run) dry_run=1 ;;
|
|
68
|
-
--who) who_override="${2:?--who needs a value}"; shift ;;
|
|
69
|
-
-h|--help) sed -n '2,12p' "$0"; exit 0 ;;
|
|
70
|
-
-*) echo "plot-deliver: unknown flag '$1'" >&2; exit 1 ;;
|
|
71
|
-
*) slug="$1" ;;
|
|
72
|
-
esac
|
|
73
|
-
shift
|
|
74
|
-
done
|
|
75
|
-
|
|
76
|
-
die() { echo "plot-deliver: $*" >&2; exit 1; }
|
|
77
|
-
|
|
78
|
-
[ -n "$slug" ] || die "need a plan slug (usage: plot-deliver.sh [--dry-run] <slug>)"
|
|
79
|
-
git rev-parse --git-dir >/dev/null 2>&1 || die "not a git repository — run this from inside the checkout, or 'git init' one here"
|
|
80
|
-
|
|
81
|
-
cfg() { bash "$script_dir/plot-config.sh" get "$1" "$2"; }
|
|
82
|
-
|
|
83
|
-
repo_root=$(git rev-parse --show-toplevel)
|
|
84
|
-
# The booking worktree goes under the desk root of the MAIN checkout, never of
|
|
85
|
-
# this tree: inside a desk `--show-toplevel` answers the desk, and the default
|
|
86
|
-
# `.worktrees` would resolve beneath it. No fallback — `plot-desk-root.sh`.
|
|
87
|
-
# shellcheck source=plot-desk-root.sh
|
|
88
|
-
. "$script_dir/plot-desk-root.sh"
|
|
89
|
-
main_root=$(plot_repo_root)
|
|
90
|
-
wt_root=$(plot_desk_root "$main_root") || die "cannot resolve where the booking worktree goes (see above)"
|
|
91
|
-
|
|
92
|
-
PLAN_DIR=$(cfg "Plan directory" "docs/plans/")
|
|
93
|
-
ACTIVE_DIR=$(cfg "Active index" "docs/plans/active/")
|
|
94
|
-
DELIVERED_DIR=$(cfg "Delivered index" "docs/plans/delivered/")
|
|
95
|
-
SPRINT_DIR=$(cfg "Sprint directory" "docs/sprints/")
|
|
96
|
-
|
|
97
|
-
# ---------------------------------------------------------------------------
|
|
98
|
-
# Step 1 — find the plan and read its state
|
|
99
|
-
# ---------------------------------------------------------------------------
|
|
100
|
-
|
|
101
|
-
plan_file=""
|
|
102
|
-
for cand in "$PLAN_DIR"*"$slug".md "$ACTIVE_DIR$slug.md" "$DELIVERED_DIR$slug.md"; do
|
|
103
|
-
[ -e "$cand" ] && { plan_file="$cand"; break; }
|
|
104
|
-
done
|
|
105
|
-
[ -n "$plan_file" ] || die "no plan found for '$slug' — looked in $PLAN_DIR, $ACTIVE_DIR, $DELIVERED_DIR.
|
|
106
|
-
Check the slug: ls $PLAN_DIR | grep -i '$slug'"
|
|
107
|
-
|
|
108
|
-
meta=$(bash "$script_dir/plot-plan-meta.sh" "$plan_file" 2>/dev/null) || meta=""
|
|
109
|
-
[ -n "$meta" ] || die "cannot parse '$plan_file' — refusing rather than guessing.
|
|
110
|
-
See what the parser reads: $script_dir/plot-plan-meta.sh $plan_file
|
|
111
|
-
A plan needs a '## Status' section with a 'State:' field."
|
|
112
|
-
|
|
113
|
-
jfield() { printf '%s' "$meta" | jq -r "$1" 2>/dev/null; }
|
|
114
|
-
|
|
115
|
-
phase=$(jfield '.phase')
|
|
116
|
-
sprint=$(jfield '.sprint')
|
|
117
|
-
delivered_raw=$(jfield '.delivered_raw')
|
|
118
|
-
|
|
119
|
-
# --- refusal 1: the phase ---------------------------------------------------
|
|
120
|
-
#
|
|
121
|
-
# `approved` and `delivered` both proceed: Delivered is the idempotent case.
|
|
122
|
-
# A run that finds the phase already flipped still has a record to check, an
|
|
123
|
-
# index link to move, and an annotation to update — the very half-states this
|
|
124
|
-
# script exists to repair.
|
|
125
|
-
case "$phase" in
|
|
126
|
-
approved|delivered) ;;
|
|
127
|
-
released)
|
|
128
|
-
die "plan '$slug' is already released — nothing to deliver.
|
|
129
|
-
Nothing to do here; the work shipped." ;;
|
|
130
|
-
draft|design)
|
|
131
|
-
die "plan '$slug' is still '$phase' — approve it first: /plot-approve $slug" ;;
|
|
132
|
-
NONE|"")
|
|
133
|
-
die "cannot read the phase of '$slug' ($plan_file) — refusing rather than guessing.
|
|
134
|
-
Its '## Status' section needs a line reading '- **State:** Approved'." ;;
|
|
135
|
-
*)
|
|
136
|
-
die "plan '$slug' is in phase '$phase' — only an Approved plan can be delivered.
|
|
137
|
-
If that phase is wrong, correct the 'State:' line in $plan_file and push it." ;;
|
|
138
|
-
esac
|
|
139
|
-
|
|
140
|
-
# ---------------------------------------------------------------------------
|
|
141
|
-
# Step 2 — ask whether every non-deferred branch has merged
|
|
142
|
-
# ---------------------------------------------------------------------------
|
|
143
|
-
#
|
|
144
|
-
# This is one of Plot's four phase guardrails, and it is NO LONGER DECIDED HERE.
|
|
145
|
-
#
|
|
146
|
-
# THE SCRIPT USED TO PARSE THE PLAN ITSELF. Ninety-odd lines of `sed` and `grep`
|
|
147
|
-
# re-derived the branches section, re-derived which lines named a branch, and
|
|
148
|
-
# re-derived which of those were deferred — a second implementation of a job
|
|
149
|
-
# `plot-plan-meta.sh` already owns, with its own bug history. The measured ones:
|
|
150
|
-
# a `## Changelog` bullet read as a branch (four fully-merged plans undeliverable,
|
|
151
|
-
# 2026-08-27), and a `## Slices` section that parsed to ZERO branches so the gate
|
|
152
|
-
# passed having checked nothing (two approved plans, 2026-08-30).
|
|
153
|
-
#
|
|
154
|
-
# A THIRD was measured while this block was being removed, and it is the one
|
|
155
|
-
# that argues hardest for the removal. On
|
|
156
|
-
# `docs/plans/2026-08-21-waves-name-themselves.md` the old range matched a
|
|
157
|
-
# `## Waves` heading that opens DESIGN PROSE — the plan argues for that spelling
|
|
158
|
-
# and illustrates it — and closed at the next `## `, never reaching the real
|
|
159
|
-
# `## Branches` section a hundred lines below. It read three branch names out of
|
|
160
|
-
# the illustration, one of them `bug/one` from inside a code fence, and reported
|
|
161
|
-
# none of the plan's actual work. `plot-plan-meta.sh` reads the same file
|
|
162
|
-
# correctly. That plan was delivered by hand.
|
|
163
|
-
#
|
|
164
|
-
# So the reading stays in the scripts and the DECISION moves inside:
|
|
165
|
-
# `plot-plan-meta.sh` says which branches the plan names and which it gave up,
|
|
166
|
-
# `plot-impl-status.sh` says which the host merged, and the domain's `deliver`
|
|
167
|
-
# rule says whether that makes the plan deliverable. Manifesto Principle 3 —
|
|
168
|
-
# scripts collect and report — with the line drawn where the design plan draws
|
|
169
|
-
# it: parsing a plan is adaptation, "these branches make the plan deliverable"
|
|
170
|
-
# is a decision.
|
|
171
|
-
#
|
|
172
|
-
# THE REFUSAL SENTENCE COMES BACK FROM THE DOMAIN, unchanged in wording, so the
|
|
173
|
-
# two cannot describe one rule differently.
|
|
174
|
-
|
|
175
|
-
ask_mjs="$script_dir/board/plot-ask.mjs"
|
|
176
|
-
if [ ! -f "$ask_mjs" ]; then
|
|
177
|
-
die "cannot find $ask_mjs — run 'pnpm build:board' to build it."
|
|
178
|
-
fi
|
|
179
|
-
|
|
180
|
-
verdict=$(PLOT_REPO_ROOT="$repo_root" PLOT_SCRIPTS_DIR="$script_dir" \
|
|
181
|
-
node "$ask_mjs" deliverable "$slug" "$plan_file" 2>/dev/null) || verdict=""
|
|
182
|
-
[ -n "$verdict" ] || die "cannot determine deliverability of '$slug' — refusing rather than guessing.
|
|
183
|
-
See what it said: PLOT_REPO_ROOT=$repo_root PLOT_SCRIPTS_DIR=$script_dir node $ask_mjs deliverable $slug $plan_file"
|
|
184
|
-
|
|
185
|
-
vfield() { printf '%s' "$verdict" | jq -r "$1" 2>/dev/null; }
|
|
186
|
-
|
|
187
|
-
if [ "$(vfield '.deliverable')" != "true" ]; then
|
|
188
|
-
die "$(vfield '.refusal')"
|
|
189
|
-
fi
|
|
190
|
-
|
|
191
|
-
# THE FINDING — a slice whose merged PR carried no implementation.
|
|
192
|
-
#
|
|
193
|
-
# IT REPORTS AND DOES NOT REFUSE, which is the harder call and the right one.
|
|
194
|
-
# Measured over the last 60 merged PRs on 2026-09-08, seven carried no work and
|
|
195
|
-
# only TWO were this defect: three were claim PRs whose slice finished under a
|
|
196
|
-
# DIFFERENT PR, and a gate refusing on those would have blocked a delivery whose
|
|
197
|
-
# work was complete — right about the PR and wrong about the plan.
|
|
198
|
-
#
|
|
199
|
-
# So it is worded as a QUESTION and names the next move, because a reader told
|
|
200
|
-
# only "this carried nothing" still has the decision to make.
|
|
201
|
-
empty_slices=$(printf '%s' "$verdict" | jq -r '.emptySlices // [] | .[]' 2>/dev/null)
|
|
202
|
-
if [ -n "$empty_slices" ]; then
|
|
203
|
-
echo "note: this plan's merged PR carried no implementation on:"
|
|
204
|
-
while IFS= read -r b; do
|
|
205
|
-
[ -n "$b" ] || continue
|
|
206
|
-
echo " - $b"
|
|
207
|
-
done <<< "$empty_slices"
|
|
208
|
-
echo " check whether its work landed under another PR, mark it deferred"
|
|
209
|
-
echo " (<!-- deferred: <reason> -->), or re-open it. Delivery continues."
|
|
210
|
-
fi
|
|
211
|
-
|
|
212
|
-
merged_count=$(vfield '.merged')
|
|
213
|
-
deferred_count=$(vfield '.deferred')
|
|
214
|
-
# Empty rather than `0`, so the suffix below stays absent where the old block
|
|
215
|
-
# left it absent — `${x:+...}` fires on any non-empty value, and "0 deferred"
|
|
216
|
-
# is a line no run has ever printed.
|
|
217
|
-
[ "$deferred_count" = "0" ] && deferred_count=""
|
|
218
|
-
if [ "$merged_count" = "0" ] && [ -z "$deferred_count" ]; then
|
|
219
|
-
echo "step: no branches found in plan — proceeding (nothing to verify)"
|
|
220
|
-
else
|
|
221
|
-
echo "step: verified $merged_count branch(es) merged${deferred_count:+, $deferred_count deferred}"
|
|
222
|
-
fi
|
|
223
|
-
|
|
224
|
-
MAIN=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##')
|
|
225
|
-
[ -n "$MAIN" ] || MAIN=$(bash "$script_dir/plot-host.sh" default-branch 2>/dev/null) || MAIN=""
|
|
226
|
-
[ -n "$MAIN" ] || MAIN="main"
|
|
227
|
-
|
|
228
|
-
who="${who_override:-${PLOT_DELIVER_WHO:-$(git config user.name 2>/dev/null || echo plot)}}"
|
|
229
|
-
today=$(date +%Y-%m-%d)
|
|
230
|
-
|
|
231
|
-
if [ "$dry_run" = 1 ]; then
|
|
232
|
-
echo "step: would flip Phase → Delivered and fill Delivered: $today"
|
|
233
|
-
echo "step: would move active/ → delivered/ symlink"
|
|
234
|
-
echo "step: would tick the sprint item${sprint:+ (sprint: $sprint)}"
|
|
235
|
-
echo "summary: phase=would record=would index=would sprint=would push=would"
|
|
236
|
-
exit 0
|
|
237
|
-
fi
|
|
238
|
-
|
|
239
|
-
# ---------------------------------------------------------------------------
|
|
240
|
-
# Steps 3-6 — the local writes, in a booking worktree off origin/<default>
|
|
241
|
-
# ---------------------------------------------------------------------------
|
|
242
|
-
#
|
|
243
|
-
# A SEPARATE WORKTREE, not a checkout here. The caller's working tree may carry
|
|
244
|
-
# uncommitted work, and switching it out from under them is exactly the write
|
|
245
|
-
# this script otherwise refuses.
|
|
246
|
-
|
|
247
|
-
# The CANONICAL plan file, not the index symlink.
|
|
248
|
-
real_plan_path() { # $1 = plan file as found
|
|
249
|
-
local p="$1" d b t
|
|
250
|
-
d=$(cd "$(dirname "$p")" 2>/dev/null && pwd) || return 1
|
|
251
|
-
b=$(basename "$p")
|
|
252
|
-
t=$(readlink "$d/$b" 2>/dev/null || true)
|
|
253
|
-
if [ -n "$t" ]; then
|
|
254
|
-
case "$t" in
|
|
255
|
-
/*) d=$(cd "$(dirname "$t")" 2>/dev/null && pwd) || return 1 ;;
|
|
256
|
-
*) d=$(cd "$d/$(dirname "$t")" 2>/dev/null && pwd) || return 1 ;;
|
|
257
|
-
esac
|
|
258
|
-
b=$(basename "$t")
|
|
259
|
-
fi
|
|
260
|
-
case "$d" in
|
|
261
|
-
"$repo_root") printf '%s' "$b" ;;
|
|
262
|
-
"$repo_root"/*) printf '%s/%s' "${d#$repo_root/}" "$b" ;;
|
|
263
|
-
*) return 1 ;;
|
|
264
|
-
esac
|
|
7
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
bundle="$script_dir/board/plot-deliver.mjs"
|
|
9
|
+
[ -f "$bundle" ] || {
|
|
10
|
+
echo "plot-deliver: cannot find $bundle — update the plot plugin in this repository, or — in the plot repository itself — run: pnpm build:board" >&2
|
|
11
|
+
exit 2
|
|
265
12
|
}
|
|
266
|
-
|
|
267
|
-
rel=$(cd "$repo_root" && real_plan_path "$plan_file") || rel=""
|
|
268
|
-
[ -n "$rel" ] || die "$plan_file is outside the repository root ($repo_root).
|
|
269
|
-
Move the plan under $PLAN_DIR inside this checkout and re-run."
|
|
270
|
-
|
|
271
|
-
# The filename, for symlink creation.
|
|
272
|
-
plan_basename=$(basename "$rel")
|
|
273
|
-
|
|
274
|
-
# Flip `**State:** Approved` → `Delivered` in the `## Status` section only.
|
|
275
|
-
# Reads `State:` and `Phase:` alike: it changes the VALUE on whichever line
|
|
276
|
-
# carries it, so a plan written before the 2026-09-07 rename still delivers.
|
|
277
|
-
#
|
|
278
|
-
# READS ONE FILE AND WRITES ANOTHER, rather than editing in place. It edited in
|
|
279
|
-
# place until 2026-09-02, which is what let the phase land without its record —
|
|
280
|
-
# see write_transition() below.
|
|
281
|
-
flip_phase() { # $1=in $2=out → 0 if it changed the file, 1 if nothing to flip
|
|
282
|
-
awk '
|
|
283
|
-
BEGIN { section = ""; done = 0 }
|
|
284
|
-
/^## / { section = ($0 ~ /^## Status/) ? "status" : ""; print; next }
|
|
285
|
-
section == "status" && !done && tolower($0) ~ /^[ \t]*[-*]?[ \t]*\**(state|phase)[:*]/ {
|
|
286
|
-
if (tolower($0) ~ /approved/) {
|
|
287
|
-
sub(/[Aa]pproved/, "Delivered")
|
|
288
|
-
done = 1
|
|
289
|
-
changed = 1
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
{ print }
|
|
293
|
-
END { exit (changed ? 0 : 1) }
|
|
294
|
-
' "$1" > "$2"
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
# Insert one `- **Delivered:** YYYY-MM-DD` line into the plan's `## Status` section.
|
|
298
|
-
# Fills the placeholder first; falls back to appending after the last list item.
|
|
299
|
-
#
|
|
300
|
-
# IT STOPS AT AN HTML COMMENT, and that is the whole of the fix made 2026-09-01.
|
|
301
|
-
# The plan template ends `## Status` with
|
|
302
|
-
#
|
|
303
|
-
# <!-- Transition records — written by the workflow commands, not by hand:
|
|
304
|
-
# - **Started:** <date>, <who>, <branch>
|
|
305
|
-
# -->
|
|
306
|
-
#
|
|
307
|
-
# and those `- **Started:**` lines ARE list items. The scan tracked the last one
|
|
308
|
-
# it saw, so on any plan whose `Started:` records had been filled in, the record
|
|
309
|
-
# was appended INSIDE the comment — inert, and invisible to
|
|
310
|
-
# `plot-plan-meta.sh`, which reported `delivered_raw: ""` for a plan that had
|
|
311
|
-
# just been delivered. Measured on `a-browser-test-serves-its-own-state`: phase
|
|
312
|
-
# flipped to Delivered, two `Delivered:` lines written into the comment (the
|
|
313
|
-
# script is idempotent on the PHASE, so a second run wrote a second line), and
|
|
314
|
-
# no readable record at all.
|
|
315
|
-
#
|
|
316
|
-
# A comment is where a plan keeps the shape of a record rather than a record, so
|
|
317
|
-
# the insertion point is the last list item BEFORE one — never inside.
|
|
318
|
-
#
|
|
319
|
-
# READS ONE FILE AND WRITES ANOTHER, for the reason flip_phase() gives.
|
|
320
|
-
append_delivered_line() { # $1=in $2=out $3=record
|
|
321
|
-
local line
|
|
322
|
-
line="- **Delivered:** $3"
|
|
323
|
-
awk -v line="$line" '
|
|
324
|
-
{ lines[++n] = $0 }
|
|
325
|
-
END {
|
|
326
|
-
for (i = 1; i <= n; i++) {
|
|
327
|
-
if (lines[i] ~ /^##[ \t]*[Ss]tatus[ \t]*$/) { start = i; break }
|
|
328
|
-
}
|
|
329
|
-
if (!start) exit 1
|
|
330
|
-
|
|
331
|
-
insert = start
|
|
332
|
-
for (i = start + 1; i <= n; i++) {
|
|
333
|
-
if (lines[i] ~ /^##[ \t]/) break
|
|
334
|
-
# An HTML comment ends the writable region. Checked BEFORE the
|
|
335
|
-
# placeholder arms so a commented-out `- **Delivered:**` template line
|
|
336
|
-
# is never mistaken for the slot to fill.
|
|
337
|
-
if (lines[i] ~ /<!--/) break
|
|
338
|
-
if (lines[i] ~ /^[ \t]*[-*][ \t]*\*\*Delivered:\*\*[ \t]*$/) { slot = i; break }
|
|
339
|
-
if (lines[i] ~ /^[ \t]*[-*][ \t]/) insert = i
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
for (i = 1; i <= n; i++) {
|
|
343
|
-
if (i == slot) { print line; continue } # replaces the empty placeholder
|
|
344
|
-
print lines[i]
|
|
345
|
-
if (!slot && i == insert) print line
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
' "$1" > "$2"
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
# ---------------------------------------------------------------------------
|
|
352
|
-
# THE TRANSITION — one value, decided in the domain, written whole or not at all
|
|
353
|
-
# ---------------------------------------------------------------------------
|
|
354
|
-
#
|
|
355
|
-
# WHAT THIS CLOSES. The phase and the record were two independent steps until
|
|
356
|
-
# 2026-09-02: `flip_phase` edited the file, and `append_delivered_line` then
|
|
357
|
-
# edited it again. The second could fail — a plan with no `## Status` heading is
|
|
358
|
-
# its documented refusal — and the first had already landed. That is not
|
|
359
|
-
# theoretical: measured 2026-08-20, a plan carrying `Phase: Delivered` with no
|
|
360
|
-
# `Delivered:` line was filtered out of `plot-fleet-scan.sh` ENTIRELY, which
|
|
361
|
-
# reads its delivered window from `delivered_raw` — the record itself. The scan
|
|
362
|
-
# reported zero plans for it. A phase without its record does not make a plan
|
|
363
|
-
# half-delivered; it makes it invisible.
|
|
364
|
-
#
|
|
365
|
-
# SO THE TWO ARE ONE WRITE. The domain says so in a type: `transitions/plan.ts`
|
|
366
|
-
# gives `Decision` a required `phase` AND a required `record`, so a decision
|
|
367
|
-
# missing either does not compile. `plot-transition.mjs` carries that value out
|
|
368
|
-
# to here, and this function is the half that could still have broken it —
|
|
369
|
-
# both edits run against scratch copies, and the plan file is replaced only
|
|
370
|
-
# once both have succeeded.
|
|
371
|
-
#
|
|
372
|
-
# THE DOMAIN DECIDES, THIS PERFORMS. The phase word and the record's text come
|
|
373
|
-
# back from `plot-transition.mjs`; the awk that knows where a `## Status` line
|
|
374
|
-
# lives stays here, because that is adaptation and it carries bug history worth
|
|
375
|
-
# keeping (the HTML-comment fix above, 2026-09-01).
|
|
376
|
-
#
|
|
377
|
-
# IDEMPOTENCE IS UNCHANGED, and it is still the source that answers. The domain
|
|
378
|
-
# reads the phase and the record THIS SCRIPT PARSED FROM THE FILE IT IS ABOUT TO
|
|
379
|
-
# WRITE, and answers `already` when both are present — the same question the two
|
|
380
|
-
# steps asked separately, asked once. No progress file appears here, because a
|
|
381
|
-
# progress file is exactly what would disagree with the repository when somebody
|
|
382
|
-
# intervened by hand between two runs.
|
|
383
|
-
transition_mjs="$script_dir/board/plot-transition.mjs"
|
|
384
|
-
|
|
385
|
-
# Ask the domain for the transition, and refuse in its words.
|
|
386
|
-
#
|
|
387
|
-
# Called with the file's OWN parse rather than the caller's: on the booking-
|
|
388
|
-
# worktree flow those are different files, and the plan on the default branch is
|
|
389
|
-
# the one that counts.
|
|
390
|
-
decide_transition() { # $1=file → prints "<Phase>\t<record>\t<write|already>"
|
|
391
|
-
local f="$1" m answer rc
|
|
392
|
-
[ -f "$transition_mjs" ] \
|
|
393
|
-
|| { echo "plot-deliver: cannot find $transition_mjs — run 'pnpm build:board'." >&2; return 1; }
|
|
394
|
-
m=$(bash "$script_dir/plot-plan-meta.sh" "$f" 2>/dev/null) || m=""
|
|
395
|
-
[ -n "$m" ] || { echo "plot-deliver: cannot parse $f — refusing rather than guessing." >&2; return 1; }
|
|
396
|
-
answer=$(printf 'deliver\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t\t\t\n' \
|
|
397
|
-
"$slug" \
|
|
398
|
-
"$(printf '%s' "$m" | jq -r '.phase // ""')" \
|
|
399
|
-
"$(printf '%s' "$m" | jq -r '.review // ""')" \
|
|
400
|
-
"$(printf '%s' "$m" | jq -r '.approved_raw // ""')" \
|
|
401
|
-
"$(printf '%s' "$m" | jq -r '.delivered_raw // ""')" \
|
|
402
|
-
"$(printf '%s' "$m" | jq -r '.released_raw // ""')" \
|
|
403
|
-
"$today" \
|
|
404
|
-
| node "$transition_mjs" 2>&1)
|
|
405
|
-
rc=$?
|
|
406
|
-
# Exit 1 is the domain's refusal and its sentence, tab-separated after the
|
|
407
|
-
# rule that fired. Exit 2 is this script handing it something unreadable,
|
|
408
|
-
# which no operator can act on — so it reports as the bug it is.
|
|
409
|
-
if [ "$rc" != 0 ]; then
|
|
410
|
-
if [ "$rc" = 1 ]; then
|
|
411
|
-
echo "plot-deliver: $(printf '%s' "$answer" | cut -f2-)" >&2
|
|
412
|
-
else
|
|
413
|
-
echo "plot-deliver: $answer" >&2
|
|
414
|
-
fi
|
|
415
|
-
return 1
|
|
416
|
-
fi
|
|
417
|
-
printf '%s' "$answer"
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
# Apply the decided transition: the phase and the record, or neither.
|
|
421
|
-
#
|
|
422
|
-
# BOTH EDITS RUN AGAINST SCRATCH FILES and the plan is replaced by one `mv`.
|
|
423
|
-
# An `## Status` section that cannot take the record leaves the file exactly as
|
|
424
|
-
# it was found — INCLUDING ITS PHASE — so re-running is still the repair, and
|
|
425
|
-
# the half-state that made a plan invisible cannot be reached from here.
|
|
426
|
-
#
|
|
427
|
-
# `$3` says whether the file already carries the record. A plan can carry one
|
|
428
|
-
# while its phase lags — written by hand, or an earlier run cut between the two
|
|
429
|
-
# writes — and appending a second is how one plan came to hold two `Delivered:`
|
|
430
|
-
# lines (2026-09-01). The domain returns the written record unchanged in that
|
|
431
|
-
# case, so the phase still flips and nothing is inserted.
|
|
432
|
-
# Would the PARSER read this phase out of the file we are about to land?
|
|
433
|
-
#
|
|
434
|
-
# THE WRITE SUCCEEDING IS NOT THE OUTCOME HOLDING, and that gap is #924. A plan
|
|
435
|
-
# carrying BOTH front matter and a `## Status` block was delivered in a project
|
|
436
|
-
# repo: `flip_phase` wrote `Delivered` into the block, `mv` landed it, the
|
|
437
|
-
# summary said `phase=flipped`, and `plot-plan-meta.sh` went on answering
|
|
438
|
-
# `approved` — because it preferred front matter wherever it existed. The write
|
|
439
|
-
# took effect on bytes nobody reads.
|
|
440
|
-
#
|
|
441
|
-
# THAT PRECEDENCE INVERTED IN #933, so the two-record plan now delivers rather
|
|
442
|
-
# than refusing here: the parser reads the `## Status` block, which is the field
|
|
443
|
-
# every lifecycle script writes. This gate is unchanged and is not softened —
|
|
444
|
-
# its condition simply stops holding for that shape. It still fires on a scratch
|
|
445
|
-
# copy the parser cannot read, and it still asks the parser rather than trusting
|
|
446
|
-
# that awk changed a line.
|
|
447
|
-
#
|
|
448
|
-
# `flip_phase`'s awk matches only inside `section == "status"`. That one guard
|
|
449
|
-
# IS the defect: on a front-matter plan it edits the block and leaves the front
|
|
450
|
-
# matter untouched, and returns 0 for having changed something.
|
|
451
|
-
#
|
|
452
|
-
# SO THE TEST IS WHAT THE PARSER ANSWERS, NEVER WHETHER AWK CHANGED A LINE.
|
|
453
|
-
# Refusing on `flipped=0` would break every re-run of a correct delivery — a
|
|
454
|
-
# plan already carrying `Delivered` flips nothing and is fine. This asks the one
|
|
455
|
-
# question that distinguishes them: read the scratch copy the way every later
|
|
456
|
-
# consumer will read the plan, and compare.
|
|
457
|
-
#
|
|
458
|
-
# IT RUNS ON THE SCRATCH COPY, BEFORE THE `mv`, and the caller passes whichever
|
|
459
|
-
# file that arm is about to land — `$a` on the `recorded=yes` arm, `$b` on the
|
|
460
|
-
# other. Parsing `$a` on the record arm would check content that never reaches
|
|
461
|
-
# the plan. After the `mv` is too late twice over: the script's own header
|
|
462
|
-
# documents exit 0 as *"the plan is Delivered on the default branch"*, so
|
|
463
|
-
# refusing there would exit 1 on a run meeting the documented success
|
|
464
|
-
# condition — and `runAutoDeliver` spawns this detached, logging a non-zero exit
|
|
465
|
-
# to nobody while the plan sits delivered on main.
|
|
466
|
-
#
|
|
467
|
-
# AN UNREADABLE SCRATCH COPY REFUSES. `decide_transition` already takes that
|
|
468
|
-
# line — *"refusing rather than guessing"* — and a file the parser cannot read
|
|
469
|
-
# is exactly the state this gate exists to keep off the plan.
|
|
470
|
-
phase_would_read() { # $1=scratch file $2=expected phase (lowercase) → 0 agrees, 1 refuses
|
|
471
|
-
local scratch="$1" want="$2" m got
|
|
472
|
-
m=$(bash "$script_dir/plot-plan-meta.sh" "$scratch" 2>/dev/null) || m=""
|
|
473
|
-
if [ -z "$m" ]; then
|
|
474
|
-
echo "plot-deliver: $rel — the written file does not parse, so the delivery was not landed." >&2
|
|
475
|
-
echo " Nothing was written. Re-run after fixing the file." >&2
|
|
476
|
-
return 1
|
|
477
|
-
fi
|
|
478
|
-
got=$(printf '%s' "$m" | jq -r '.phase // ""')
|
|
479
|
-
[ "$got" = "$want" ] && return 0
|
|
480
|
-
|
|
481
|
-
# THE REFUSAL NAMES BOTH VALUES AND THE FILE. A message saying only "delivery
|
|
482
|
-
# failed" throws away the half a person acts on: which phase was written, and
|
|
483
|
-
# which one the parser still reads. The cause is named too, because the file
|
|
484
|
-
# holding two records of one fact is the thing to fix — and which format ought
|
|
485
|
-
# to win is a decision this gate deliberately leaves to a person.
|
|
486
|
-
echo "plot-deliver: $rel — wrote phase '$want', but the parser still reads '$got'." >&2
|
|
487
|
-
echo " The write landed and the parser reads something else, so the delivery" >&2
|
|
488
|
-
echo " would have reported a success it did not achieve." >&2
|
|
489
|
-
echo " Nothing was written — the plan is unchanged. Check what the plan says" >&2
|
|
490
|
-
echo " its phase is, and where: a plan stating it in two places reports the" >&2
|
|
491
|
-
echo " '## Status' block, which is the field every lifecycle script writes." >&2
|
|
492
|
-
echo " See what the parser reads: $script_dir/plot-plan-meta.sh $rel" >&2
|
|
493
|
-
return 1
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
write_transition() { # $1=file $2=record $3=recorded(yes|no) → sets phase_report record_report
|
|
497
|
-
local f="$1" record="$2" recorded="$3" a="$1.plot-phase" b="$1.plot-record" flipped=0
|
|
498
|
-
|
|
499
|
-
if flip_phase "$f" "$a"; then flipped=1; else flipped=0; fi
|
|
500
|
-
# awk wrote `$a` either way; where nothing flipped it is a faithful copy, so
|
|
501
|
-
# the record still has a file to be inserted into.
|
|
502
|
-
[ -s "$a" ] || { rm -f "$a"; echo "plot-deliver: could not read $rel" >&2; return 1; }
|
|
503
|
-
|
|
504
|
-
if [ "$recorded" = "yes" ]; then
|
|
505
|
-
# THE DRY RUN, on the file this arm is about to land. A refusal discards the
|
|
506
|
-
# scratch copy and leaves the plan byte-identical — and never reaches
|
|
507
|
-
# `record_state_receipt`, which would otherwise license a commit of a state
|
|
508
|
-
# that was refused.
|
|
509
|
-
phase_would_read "$a" delivered || { rm -f "$a"; return 1; }
|
|
510
|
-
mv "$a" "$f" || { rm -f "$a"; return 1; }
|
|
511
|
-
record_report="already"
|
|
512
|
-
else
|
|
513
|
-
if ! append_delivered_line "$a" "$b" "$record"; then
|
|
514
|
-
rm -f "$a" "$b"
|
|
515
|
-
echo "plot-deliver: $rel has no '## Status' section — nowhere to record the delivery." >&2
|
|
516
|
-
echo " Nothing was written: the phase is not flipped either, because a phase" >&2
|
|
517
|
-
echo " with no record is invisible to the scan. Fix the section and re-run." >&2
|
|
518
|
-
return 1
|
|
519
|
-
fi
|
|
520
|
-
# `$b` AND NOT `$a`: this arm lands the file carrying the record, so `$a`
|
|
521
|
-
# is content that never reaches the plan.
|
|
522
|
-
phase_would_read "$b" delivered || { rm -f "$a" "$b"; return 1; }
|
|
523
|
-
mv "$b" "$f" || { rm -f "$a" "$b"; return 1; }
|
|
524
|
-
rm -f "$a"
|
|
525
|
-
record_report="written"
|
|
526
|
-
fi
|
|
527
|
-
|
|
528
|
-
phase_report=$([ "$flipped" = 1 ] && echo flipped || echo already)
|
|
529
|
-
# The receipt plot-state-gate.sh clears on. Recorded after the `mv`, so it
|
|
530
|
-
# names a value the file actually carries — a receipt written before a failed
|
|
531
|
-
# write would license a commit of the state that was refused.
|
|
532
|
-
record_state_receipt "$f" "Delivered"
|
|
533
|
-
return 0
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
# Tick this plan's sprint item.
|
|
537
|
-
#
|
|
538
|
-
# THE TICK ONLY, since 2026-09-08. This also wrote a `<!-- status: delivered -->`
|
|
539
|
-
# annotation, and `a-withdrawn-item-is-not-open` measured that record dead in
|
|
540
|
-
# both directions: 67 lines carried one, none carried a value any reader acted
|
|
541
|
-
# on, and `plot-sprint-release.sh` read the field nowhere. The plan file carries
|
|
542
|
-
# `State:` and a dated `Delivered:` record, and the estate-outranks-the-checkbox
|
|
543
|
-
# rule points at those — a cache nobody refreshes and nobody reads is a second
|
|
544
|
-
# answer waiting to contradict the first.
|
|
545
|
-
#
|
|
546
|
-
# The box is NOT the same record. `/plot-sprint close` step 2a exists to tick
|
|
547
|
-
# boxes the estate says are done, and ticking here is what keeps that step's
|
|
548
|
-
# work to the items it genuinely cannot resolve.
|
|
549
|
-
update_sprint_item() { # $1=worktree root → prints none|updated|already|missing
|
|
550
|
-
local root="$1" f found=""
|
|
551
|
-
[ -n "$sprint" ] || { printf 'none'; return 0; }
|
|
552
|
-
local dir="$root/${SPRINT_DIR#/}"
|
|
553
|
-
[ -d "$dir" ] || { printf 'missing'; return 0; }
|
|
554
|
-
# Find the sprint file by content (the [<slug>] reference), not by filename.
|
|
555
|
-
for f in "$dir"/*.md; do
|
|
556
|
-
[ -e "$f" ] || continue
|
|
557
|
-
grep -q "\[$slug\]" "$f" 2>/dev/null && { found="$f"; break; }
|
|
558
|
-
done
|
|
559
|
-
[ -n "$found" ] || { printf 'missing'; return 0; }
|
|
560
|
-
|
|
561
|
-
local before after
|
|
562
|
-
before=$(cat "$found")
|
|
563
|
-
after=$(awk -v slug="$slug" '
|
|
564
|
-
index($0, "[" slug "]") == 0 { print; next }
|
|
565
|
-
{
|
|
566
|
-
line = $0
|
|
567
|
-
# Check the box. Nothing else on the line is touched.
|
|
568
|
-
sub(/\[ \]/, "[x]", line)
|
|
569
|
-
print line
|
|
570
|
-
}
|
|
571
|
-
' "$found")
|
|
572
|
-
if [ "$before" = "$after" ]; then printf 'already'; return 0; fi
|
|
573
|
-
printf '%s\n' "$after" > "$found"
|
|
574
|
-
printf 'updated'
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
# Move the active/ symlink to delivered/.
|
|
578
|
-
move_index_symlink() { # $1=worktree root → prints moved|created|already|skipped
|
|
579
|
-
local root="$1"
|
|
580
|
-
local active_link="$root/${ACTIVE_DIR#/}$slug.md"
|
|
581
|
-
local delivered_link="$root/${DELIVERED_DIR#/}$slug.md"
|
|
582
|
-
local delivered_dir="$root/${DELIVERED_DIR#/}"
|
|
583
|
-
|
|
584
|
-
# Ensure delivered/ directory exists (best effort)
|
|
585
|
-
mkdir -p "$delivered_dir" 2>/dev/null || true
|
|
586
|
-
|
|
587
|
-
# Check if already in delivered/
|
|
588
|
-
if [ -e "$delivered_link" ]; then
|
|
589
|
-
# Remove from active/ if still there
|
|
590
|
-
git rm -q --ignore-unmatch "$active_link" 2>/dev/null || true
|
|
591
|
-
printf 'already'
|
|
592
|
-
return 0
|
|
593
|
-
fi
|
|
594
|
-
|
|
595
|
-
# Create the delivered/ symlink (relative path to the plan)
|
|
596
|
-
ln -sfn "../$plan_basename" "$delivered_link" 2>/dev/null || { printf 'skipped'; return 0; }
|
|
597
|
-
|
|
598
|
-
# Remove from active/ (best effort, ignore if not there)
|
|
599
|
-
git rm -q --ignore-unmatch "$active_link" 2>/dev/null || true
|
|
600
|
-
|
|
601
|
-
printf 'moved'
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
# The whole local half, run inside one directory.
|
|
605
|
-
apply_local_writes() { # $1=root → sets phase_report record_report index_report sprint_report
|
|
606
|
-
local root="$1" f="$1/$rel"
|
|
607
|
-
[ -f "$f" ] || { echo "plot-deliver: $rel is not present in $root" >&2; return 1; }
|
|
608
|
-
|
|
609
|
-
# Step 3 — THE TRANSITION: the phase and its record, together or not at all.
|
|
610
|
-
#
|
|
611
|
-
# ONE STEP WHERE THERE WERE TWO, which is the whole of this change. The domain
|
|
612
|
-
# decides what the two lines say and whether they are owed at all; this writes
|
|
613
|
-
# them as one replacement of the file. Already-done test: the domain answers
|
|
614
|
-
# `already` when the file it is about to write ALREADY carries both, which is
|
|
615
|
-
# the source asked directly — never a progress file.
|
|
616
|
-
local decided record action recorded
|
|
617
|
-
decided=$(decide_transition "$f") || return 1
|
|
618
|
-
record=$(printf '%s' "$decided" | cut -f2)
|
|
619
|
-
action=$(printf '%s' "$decided" | cut -f3)
|
|
620
|
-
recorded=$(printf '%s' "$decided" | cut -f4)
|
|
621
|
-
if [ "$action" = "already" ]; then
|
|
622
|
-
phase_report="already"
|
|
623
|
-
record_report="already"
|
|
624
|
-
else
|
|
625
|
-
write_transition "$f" "$record" "$recorded" || return 1
|
|
626
|
-
fi
|
|
627
|
-
|
|
628
|
-
# Step 5 — move the index symlink (best effort).
|
|
629
|
-
index_report=$(move_index_symlink "$root")
|
|
630
|
-
|
|
631
|
-
# Step 6 — tick the sprint item.
|
|
632
|
-
sprint_report=$(update_sprint_item "$root")
|
|
633
|
-
return 0
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
phase_report="" record_report="" index_report="skipped" sprint_report="none"
|
|
637
|
-
push_report="n/a"
|
|
638
|
-
|
|
639
|
-
# Fetch and create a booking worktree off origin/<default>.
|
|
640
|
-
git fetch -q origin "$MAIN" 2>/dev/null
|
|
641
|
-
|
|
642
|
-
bookbr="plot/deliver-$slug"
|
|
643
|
-
tmpwt="$wt_root/.plot-deliver-$slug.$$"
|
|
644
|
-
# `git worktree add` creates the desk root when it is missing, so exclude it
|
|
645
|
-
# first: a booking run must not leave `.worktrees/` untracked in `git status`.
|
|
646
|
-
plot_exclude_desk_root "$main_root"
|
|
647
|
-
# -B: a leftover branch from an earlier failed run must not block this one.
|
|
648
|
-
git worktree add -q -B "$bookbr" "$tmpwt" "origin/$MAIN" 2>/dev/null \
|
|
649
|
-
|| die "could not prepare a booking worktree at $tmpwt.
|
|
650
|
-
Most often origin/$MAIN is not fetched, or '$bookbr' is checked out in
|
|
651
|
-
another worktree. Check both: git fetch origin $MAIN && git worktree list
|
|
652
|
-
Nothing has been written locally; the plan is untouched."
|
|
653
|
-
|
|
654
|
-
cleanup() {
|
|
655
|
-
git worktree remove --force "$tmpwt" >/dev/null 2>&1 || true
|
|
656
|
-
git branch -D "$bookbr" >/dev/null 2>&1 || true
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
if ! apply_local_writes "$tmpwt"; then
|
|
660
|
-
cleanup
|
|
661
|
-
exit 1
|
|
662
|
-
fi
|
|
663
|
-
|
|
664
|
-
# Stage the plan file and index changes.
|
|
665
|
-
git -C "$tmpwt" add -- "$rel" >/dev/null 2>&1 || true
|
|
666
|
-
git -C "$tmpwt" add -- "${ACTIVE_DIR#/}" >/dev/null 2>&1 || true
|
|
667
|
-
git -C "$tmpwt" add -- "${DELIVERED_DIR#/}" >/dev/null 2>&1 || true
|
|
668
|
-
[ "$sprint_report" = "updated" ] && git -C "$tmpwt" add -- "${SPRINT_DIR#/}" >/dev/null 2>&1
|
|
669
|
-
|
|
670
|
-
# THE BOOKED PLAN, KEPT FOR THE TRACKER. The booking worktree is removed on
|
|
671
|
-
# every exit below, and the working tree may still read `Approved`; the issue
|
|
672
|
-
# status is decided from the file that reached the default branch.
|
|
673
|
-
booked_plan=""
|
|
674
|
-
plot_tmpfile booked_plan deliver-plan
|
|
675
|
-
cp "$tmpwt/$rel" "$booked_plan" 2>/dev/null || : > "$booked_plan"
|
|
676
|
-
|
|
677
|
-
if git -C "$tmpwt" diff --cached --quiet 2>/dev/null; then
|
|
678
|
-
# THE IDEMPOTENT EXIT. Everything this run would have written was already
|
|
679
|
-
# on the default branch, so there is nothing to push and nothing wrong.
|
|
680
|
-
push_report="nothing-to-commit"
|
|
681
|
-
echo "step: nothing to commit — the delivery is already recorded on $MAIN"
|
|
682
|
-
cleanup
|
|
683
|
-
else
|
|
684
|
-
if ! git -C "$tmpwt" -c "user.name=$who" commit -q -m "plot: deliver $slug"; then
|
|
685
|
-
cleanup
|
|
686
|
-
die "could not commit the delivery.
|
|
687
|
-
See what git refused: git -C $tmpwt status
|
|
688
|
-
Nothing was pushed; re-run this — it is idempotent."
|
|
689
|
-
fi
|
|
690
|
-
|
|
691
|
-
push_out=$(bash "$script_dir/plot-push-main.sh" "$bookbr" "$MAIN" 2>&1)
|
|
692
|
-
push_rc=$?
|
|
693
|
-
printf '%s\n' "$push_out" | sed 's/^/ /'
|
|
694
|
-
if [ "$push_rc" = 0 ]; then
|
|
695
|
-
push_report=$(printf '%s' "$push_out" | sed -n 's/^push: \([a-z]*\).*/\1/p' | head -1)
|
|
696
|
-
[ -n "$push_report" ] || push_report="unknown"
|
|
697
|
-
cleanup
|
|
698
|
-
else
|
|
699
|
-
# BRANCH PROTECTION FALLBACK — open a micro-PR if push is rejected.
|
|
700
|
-
echo "step: push rejected — opening a micro-PR instead"
|
|
701
|
-
if git push -q origin "$bookbr" 2>/dev/null \
|
|
702
|
-
&& micro_url=$(bash "$script_dir/plot-host.sh" pr-create \
|
|
703
|
-
--title "plot: deliver $slug" \
|
|
704
|
-
--body "Records the delivery of \`$slug\`." \
|
|
705
|
-
--base "$MAIN" --head "$bookbr" 2>/dev/null) \
|
|
706
|
-
&& micro_num=$(printf '%s' "$micro_url" | sed 's#.*/##') \
|
|
707
|
-
&& bash "$script_dir/plot-host.sh" pr-merge "$micro_num" --delete-branch >/dev/null 2>&1
|
|
708
|
-
then
|
|
709
|
-
push_report="micro-pr"
|
|
710
|
-
echo "step: delivery landed via micro-PR $micro_url"
|
|
711
|
-
cleanup
|
|
712
|
-
else
|
|
713
|
-
push_report="rejected"
|
|
714
|
-
echo "plot-deliver: the delivery is committed on '$bookbr' but could not reach $MAIN." >&2
|
|
715
|
-
echo " Land '$bookbr' by hand, or re-run this command once the push works." >&2
|
|
716
|
-
git worktree remove --force "$tmpwt" >/dev/null 2>&1 || true
|
|
717
|
-
rm -f "$booked_plan"
|
|
718
|
-
# NOTHING REACHED THE DEFAULT BRANCH, so no status is owed yet.
|
|
719
|
-
echo "summary: phase=$phase_report record=$record_report index=$index_report sprint=$sprint_report push=$push_report tracker=skipped"
|
|
720
|
-
exit 1
|
|
721
|
-
fi
|
|
722
|
-
fi
|
|
723
|
-
fi
|
|
724
|
-
|
|
725
|
-
# THE TRACKER HEARS AFTER THE DELIVERY LANDED, and never decides it. The plan is
|
|
726
|
-
# delivered; the tracker holds a copy of one fact about it. Every outcome of
|
|
727
|
-
# plot-issue-status.sh, a failed write and an unreadable bundle included, is a
|
|
728
|
-
# report on the summary line and leaves this script's exit code alone.
|
|
729
|
-
tracker_out=$(bash "$script_dir/plot-issue-status.sh" "$booked_plan" 2>&1)
|
|
730
|
-
printf '%s\n' "$tracker_out" | grep -v '^summary: ' | sed '/^$/d; s/^/ tracker: /'
|
|
731
|
-
tracker_report=$(printf '%s' "$tracker_out" | sed -n 's/^summary: tracker=\([a-z-]*\).*/\1/p' | tail -1)
|
|
732
|
-
[ -n "$tracker_report" ] || tracker_report="failed"
|
|
733
|
-
rm -f "$booked_plan"
|
|
734
|
-
|
|
735
|
-
echo "summary: phase=$phase_report record=$record_report index=$index_report sprint=$sprint_report push=$push_report tracker=$tracker_report"
|
|
736
|
-
# THE RECEIPT IS SPENT HERE, on the action COMPLETING — never at the gate.
|
|
737
|
-
# `plot-controller-gate.sh` clears on a receipt and LEAVES it, so an
|
|
738
|
-
# interrupted run can be repeated on the same licence: this script documents
|
|
739
|
-
# re-running as the repair for any interruption after its irreversible step,
|
|
740
|
-
# and a receipt spent at the gate would refuse that repair in the case it is
|
|
741
|
-
# most needed. One authorisation, one completed action.
|
|
742
|
-
spend_action_receipt "plot-deliver.sh"
|
|
743
|
-
|
|
744
|
-
exit 0
|
|
13
|
+
exec node "$bundle" "$@"
|