@plot-pm/board 0.9.0 → 0.9.1
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 +4 -4
- package/package.json +11 -2
- package/plot-approve.sh +628 -0
- package/plot-deliver.sh +488 -0
- package/plot-dispatch.sh +2028 -0
- package/plot-fleet-scan.sh +3496 -0
- package/plot-host.sh +1686 -0
- package/plot-reap.sh +286 -0
- package/plot-release-refs.sh +234 -0
- package/plot-resolve-artifact.sh +334 -0
- package/plot-worker-state.sh +725 -0
package/plot-deliver.sh
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Plot helper: perform the MECHANICAL half of delivering a plan.
|
|
3
|
+
# Usage: plot-deliver.sh [--dry-run] [--who <name>] <slug>
|
|
4
|
+
# --dry-run say what would happen; write nothing, push nothing
|
|
5
|
+
# --who the name recorded in the `Delivered:` line (default: git user.name)
|
|
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
|
|
9
|
+
# Exit 0 when the plan is Delivered on the default branch (whether this
|
|
10
|
+
# run did the work or found it already done); 1 on a refusal or a
|
|
11
|
+
# failure, with the reason on stderr.
|
|
12
|
+
#
|
|
13
|
+
# WHY THIS EXISTS. The board computes `allWavesMerged` — exactly the condition
|
|
14
|
+
# that says a plan is ready to deliver — but the transition itself lives only in
|
|
15
|
+
# `/plot-deliver`'s prose. So `Delivered` in the board asked for a caller with
|
|
16
|
+
# nothing safe to call, and an implementer reaching that point would have
|
|
17
|
+
# rebuilt the phase flip, the `Delivered:` record and the symlink move in
|
|
18
|
+
# TypeScript. That is precisely the drift the `plot-approve.sh` split removed.
|
|
19
|
+
#
|
|
20
|
+
# This script is the `plot-approve.sh` of delivery: one implementation, two
|
|
21
|
+
# entrances. `/plot-deliver` keeps the judgement — the completeness check, the
|
|
22
|
+
# partial-deliverable question — and delegates the writes here. The board calls
|
|
23
|
+
# this script directly (or via an agent when a `Deliver command` is set).
|
|
24
|
+
#
|
|
25
|
+
# Manifesto Principle 3 draws the line: SCRIPTS COLLECT AND REPORT; SKILLS
|
|
26
|
+
# INTERPRET AND ADAPT. Flipping a phase and writing a dated record is
|
|
27
|
+
# collecting. Deciding whether PARTIAL work counts as delivered is interpreting,
|
|
28
|
+
# and stays in the skill.
|
|
29
|
+
#
|
|
30
|
+
# IT IS IDEMPOTENT, BECAUSE ONE STEP CANNOT BE UNDONE. The push is irreversible;
|
|
31
|
+
# everything before it is local. A run interrupted between flipping the phase
|
|
32
|
+
# and pushing leaves the plan at `Delivered` in a worktree but still `Approved`
|
|
33
|
+
# on main — so `plot-deliver.sh <slug>` may run any number of times, and RUN IT
|
|
34
|
+
# AGAIN is the repair for every interruption.
|
|
35
|
+
#
|
|
36
|
+
# Each step asks THE SOURCE IT WOULD HAVE WRITTEN whether it is already done:
|
|
37
|
+
# the plan file for the phase and the record, the index directories for the
|
|
38
|
+
# symlink, the sprint file for the annotation. Never a progress file of its own.
|
|
39
|
+
#
|
|
40
|
+
# WHAT IT REFUSES, and why refusing beats guessing:
|
|
41
|
+
# - phase is not `approved` — nothing to deliver. (Already-Delivered is NOT a
|
|
42
|
+
# refusal: it is the idempotent case, and the run still checks the record
|
|
43
|
+
# and index.)
|
|
44
|
+
# - any non-deferred branch is unmerged — work is not done. This is one of
|
|
45
|
+
# Plot's four phase guardrails, moved from prose into an exit code.
|
|
46
|
+
#
|
|
47
|
+
# macOS bash 3.2 throughout: no associative arrays, no bash-4 line readers.
|
|
48
|
+
set -uo pipefail
|
|
49
|
+
|
|
50
|
+
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
51
|
+
|
|
52
|
+
dry_run=0
|
|
53
|
+
who_override=""
|
|
54
|
+
slug=""
|
|
55
|
+
while [ $# -gt 0 ]; do
|
|
56
|
+
case "$1" in
|
|
57
|
+
--dry-run) dry_run=1 ;;
|
|
58
|
+
--who) who_override="${2:?--who needs a value}"; shift ;;
|
|
59
|
+
-h|--help) sed -n '2,12p' "$0"; exit 0 ;;
|
|
60
|
+
-*) echo "plot-deliver: unknown flag '$1'" >&2; exit 1 ;;
|
|
61
|
+
*) slug="$1" ;;
|
|
62
|
+
esac
|
|
63
|
+
shift
|
|
64
|
+
done
|
|
65
|
+
|
|
66
|
+
die() { echo "plot-deliver: $*" >&2; exit 1; }
|
|
67
|
+
|
|
68
|
+
[ -n "$slug" ] || die "need a plan slug (usage: plot-deliver.sh [--dry-run] <slug>)"
|
|
69
|
+
git rev-parse --git-dir >/dev/null 2>&1 || die "not a git repository"
|
|
70
|
+
|
|
71
|
+
cfg() { bash "$script_dir/plot-config.sh" get "$1" "$2"; }
|
|
72
|
+
|
|
73
|
+
repo_root=$(git rev-parse --show-toplevel)
|
|
74
|
+
wt_root=$(cd "$repo_root/.." && pwd)
|
|
75
|
+
|
|
76
|
+
PLAN_DIR=$(cfg "Plan directory" "docs/plans/")
|
|
77
|
+
ACTIVE_DIR=$(cfg "Active index" "docs/plans/active/")
|
|
78
|
+
DELIVERED_DIR=$(cfg "Delivered index" "docs/plans/delivered/")
|
|
79
|
+
SPRINT_DIR=$(cfg "Sprint directory" "docs/sprints/")
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Step 1 — find the plan and read its state
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
plan_file=""
|
|
86
|
+
for cand in "$PLAN_DIR"*"$slug".md "$ACTIVE_DIR$slug.md" "$DELIVERED_DIR$slug.md"; do
|
|
87
|
+
[ -e "$cand" ] && { plan_file="$cand"; break; }
|
|
88
|
+
done
|
|
89
|
+
[ -n "$plan_file" ] || die "no plan found for '$slug' — looked in $PLAN_DIR, $ACTIVE_DIR, $DELIVERED_DIR"
|
|
90
|
+
|
|
91
|
+
meta=$(bash "$script_dir/plot-plan-meta.sh" "$plan_file" 2>/dev/null) || meta=""
|
|
92
|
+
[ -n "$meta" ] || die "cannot parse '$plan_file' — refusing rather than guessing"
|
|
93
|
+
|
|
94
|
+
jfield() { printf '%s' "$meta" | jq -r "$1" 2>/dev/null; }
|
|
95
|
+
|
|
96
|
+
phase=$(jfield '.phase')
|
|
97
|
+
sprint=$(jfield '.sprint')
|
|
98
|
+
delivered_raw=$(jfield '.delivered_raw')
|
|
99
|
+
|
|
100
|
+
# --- refusal 1: the phase ---------------------------------------------------
|
|
101
|
+
#
|
|
102
|
+
# `approved` and `delivered` both proceed: Delivered is the idempotent case.
|
|
103
|
+
# A run that finds the phase already flipped still has a record to check, an
|
|
104
|
+
# index link to move, and an annotation to update — the very half-states this
|
|
105
|
+
# script exists to repair.
|
|
106
|
+
case "$phase" in
|
|
107
|
+
approved|delivered) ;;
|
|
108
|
+
released)
|
|
109
|
+
die "plan '$slug' is already released — nothing to deliver." ;;
|
|
110
|
+
draft|design)
|
|
111
|
+
die "plan '$slug' is still '$phase' — approve it first." ;;
|
|
112
|
+
NONE|"")
|
|
113
|
+
die "cannot read the phase of '$slug' ($plan_file) — refusing rather than guessing." ;;
|
|
114
|
+
*)
|
|
115
|
+
die "plan '$slug' is in phase '$phase' — only an Approved plan can be delivered." ;;
|
|
116
|
+
esac
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Step 2 — verify all non-deferred branches are merged
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
#
|
|
122
|
+
# This is one of Plot's four phase guardrails. We call plot-impl-status.sh to
|
|
123
|
+
# check the merge state of all branches. Any branch that is not MERGED and not
|
|
124
|
+
# deferred is a refusal.
|
|
125
|
+
|
|
126
|
+
# Parse branches from the plan file, respecting deferred annotations.
|
|
127
|
+
# Read the plan's branches section (from EITHER spelling — ## Branches or ## Waves).
|
|
128
|
+
plan_content=$(cat "$plan_file")
|
|
129
|
+
|
|
130
|
+
# Extract branch lines from ## Branches or ## Waves section
|
|
131
|
+
branches_section=$(printf '%s' "$plan_content" | sed -n '/^## *[Bb]ranches\|^## *[Ww]aves/,/^## /p')
|
|
132
|
+
|
|
133
|
+
# A BRANCH LINE CARRIES A BRANCH PREFIX, and without that test a changelog
|
|
134
|
+
# bullet is read as a branch. The section range above closes at the next `## `,
|
|
135
|
+
# but a plan whose `## Changelog` bullet mentions a backticked identifier —
|
|
136
|
+
# `impl`, `pr_ready`, `--migrate`, `/api/story` were the four measured on
|
|
137
|
+
# 2026-08-27 — hands one of those to the merge check, which then refuses
|
|
138
|
+
# delivery over a branch that does not exist and never will.
|
|
139
|
+
#
|
|
140
|
+
# Four fully-merged plans were undeliverable for this reason. The prefixes come
|
|
141
|
+
# from `Branch prefixes` rather than a hardcoded list, the same derivation
|
|
142
|
+
# `plot-fleet-scan.sh:187` uses, so a project with its own prefixes is read
|
|
143
|
+
# correctly and one with none falls back to Plot's defaults.
|
|
144
|
+
prefix_re=$(bash "$script_dir/plot-config.sh" get "Branch prefixes" "idea/, feature/, bug/, docs/, infra/" \
|
|
145
|
+
| tr -d ' ' | tr ',' '\n' | sed 's#/$##' | grep -v '^$' | paste -sd'|' - )
|
|
146
|
+
[ -n "$prefix_re" ] || prefix_re="idea|feature|bug|docs|infra"
|
|
147
|
+
|
|
148
|
+
# Parse branches from old-style ## Branches section (backtick-quoted on list lines)
|
|
149
|
+
old_style_branches=$(printf '%s' "$branches_section" \
|
|
150
|
+
| grep -oE "^- \`($prefix_re)/[A-Za-z0-9_./-]+\`" 2>/dev/null \
|
|
151
|
+
| sed 's/^- `//; s/`$//' \
|
|
152
|
+
| sort -u || true)
|
|
153
|
+
|
|
154
|
+
# Parse branches from new-style ## Waves section (Branch: in ### headings)
|
|
155
|
+
new_style_branches=$(printf '%s' "$branches_section" \
|
|
156
|
+
| grep -oE "### .*\(Branch: ($prefix_re)/[A-Za-z0-9_./-]+" 2>/dev/null \
|
|
157
|
+
| sed 's/.*Branch: //' \
|
|
158
|
+
| sort -u || true)
|
|
159
|
+
|
|
160
|
+
# Combine both styles
|
|
161
|
+
all_branches=$(printf '%s\n%s' "$old_style_branches" "$new_style_branches" | grep -v '^$' | sort -u || true)
|
|
162
|
+
|
|
163
|
+
if [ -z "$all_branches" ]; then
|
|
164
|
+
echo "step: no branches found in plan — proceeding (nothing to verify)"
|
|
165
|
+
else
|
|
166
|
+
# Check which branches are deferred. A deferred branch has `<!-- deferred:` on its line.
|
|
167
|
+
deferred_branches=""
|
|
168
|
+
non_deferred_branches=""
|
|
169
|
+
|
|
170
|
+
for br in $all_branches; do
|
|
171
|
+
# Check both spellings: old style has branch on a `- \`branch\`` line,
|
|
172
|
+
# new style has branch in a `### ... (Branch: branch ...)` heading.
|
|
173
|
+
if printf '%s' "$branches_section" | grep -F "\`$br\`" | grep -q '<!-- *deferred' 2>/dev/null; then
|
|
174
|
+
deferred_branches="${deferred_branches}${br}
|
|
175
|
+
"
|
|
176
|
+
elif printf '%s' "$branches_section" | grep "Branch: $br" | grep -q '<!-- *deferred' 2>/dev/null; then
|
|
177
|
+
deferred_branches="${deferred_branches}${br}
|
|
178
|
+
"
|
|
179
|
+
else
|
|
180
|
+
non_deferred_branches="${non_deferred_branches}${br}
|
|
181
|
+
"
|
|
182
|
+
fi
|
|
183
|
+
done
|
|
184
|
+
|
|
185
|
+
# Trim trailing newlines
|
|
186
|
+
non_deferred_branches=$(printf '%s' "$non_deferred_branches" | grep -v '^$' || true)
|
|
187
|
+
deferred_branches=$(printf '%s' "$deferred_branches" | grep -v '^$' || true)
|
|
188
|
+
|
|
189
|
+
# Get implementation status for all branches
|
|
190
|
+
impl_status=$(bash "$script_dir/plot-impl-status.sh" "$slug" 2>/dev/null) || impl_status='{"prs":[]}'
|
|
191
|
+
|
|
192
|
+
# Check each non-deferred branch is merged
|
|
193
|
+
unmerged_branches=""
|
|
194
|
+
for br in $non_deferred_branches; do
|
|
195
|
+
[ -z "$br" ] && continue
|
|
196
|
+
# Look for this branch in the impl status. A branch is merged if its PR state is MERGED.
|
|
197
|
+
pr_state=$(printf '%s' "$impl_status" | jq -r --arg br "$br" '.prs[] | select(.branch == $br) | .state' 2>/dev/null || true)
|
|
198
|
+
if [ "$pr_state" != "MERGED" ]; then
|
|
199
|
+
unmerged_branches="${unmerged_branches}${br}
|
|
200
|
+
"
|
|
201
|
+
fi
|
|
202
|
+
done
|
|
203
|
+
|
|
204
|
+
unmerged_branches=$(printf '%s' "$unmerged_branches" | grep -v '^$' || true)
|
|
205
|
+
|
|
206
|
+
if [ -n "$unmerged_branches" ]; then
|
|
207
|
+
unmerged_count=$(printf '%s\n' "$unmerged_branches" | wc -l | tr -d ' ')
|
|
208
|
+
unmerged_list=$(printf '%s' "$unmerged_branches" | tr '\n' ', ' | sed 's/, $//')
|
|
209
|
+
die "cannot deliver: $unmerged_count branch(es) not merged: $unmerged_list
|
|
210
|
+
Merge them first, or mark them deferred with \`<!-- deferred: <reason> -->\`."
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
deferred_count=0
|
|
214
|
+
[ -n "$deferred_branches" ] && deferred_count=$(printf '%s\n' "$deferred_branches" | wc -l | tr -d ' ')
|
|
215
|
+
merged_count=$(printf '%s\n' "$non_deferred_branches" | wc -l | tr -d ' ')
|
|
216
|
+
[ -z "$non_deferred_branches" ] && merged_count=0
|
|
217
|
+
|
|
218
|
+
echo "step: verified $merged_count branch(es) merged${deferred_count:+, $deferred_count deferred}"
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
MAIN=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##')
|
|
222
|
+
[ -n "$MAIN" ] || MAIN=$(bash "$script_dir/plot-host.sh" default-branch 2>/dev/null) || MAIN=""
|
|
223
|
+
[ -n "$MAIN" ] || MAIN="main"
|
|
224
|
+
|
|
225
|
+
who="${who_override:-${PLOT_DELIVER_WHO:-$(git config user.name 2>/dev/null || echo plot)}}"
|
|
226
|
+
today=$(date +%Y-%m-%d)
|
|
227
|
+
|
|
228
|
+
if [ "$dry_run" = 1 ]; then
|
|
229
|
+
echo "step: would flip Phase → Delivered and fill Delivered: $today"
|
|
230
|
+
echo "step: would move active/ → delivered/ symlink"
|
|
231
|
+
echo "step: would update the sprint annotation${sprint:+ (sprint: $sprint)}"
|
|
232
|
+
echo "summary: phase=would record=would index=would sprint=would push=would"
|
|
233
|
+
exit 0
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
# Steps 3-6 — the local writes, in a booking worktree off origin/<default>
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
#
|
|
240
|
+
# A SEPARATE WORKTREE, not a checkout here. The caller's working tree may carry
|
|
241
|
+
# uncommitted work, and switching it out from under them is exactly the write
|
|
242
|
+
# this script otherwise refuses.
|
|
243
|
+
|
|
244
|
+
# The CANONICAL plan file, not the index symlink.
|
|
245
|
+
real_plan_path() { # $1 = plan file as found
|
|
246
|
+
local p="$1" d b t
|
|
247
|
+
d=$(cd "$(dirname "$p")" 2>/dev/null && pwd) || return 1
|
|
248
|
+
b=$(basename "$p")
|
|
249
|
+
t=$(readlink "$d/$b" 2>/dev/null || true)
|
|
250
|
+
if [ -n "$t" ]; then
|
|
251
|
+
case "$t" in
|
|
252
|
+
/*) d=$(cd "$(dirname "$t")" 2>/dev/null && pwd) || return 1 ;;
|
|
253
|
+
*) d=$(cd "$d/$(dirname "$t")" 2>/dev/null && pwd) || return 1 ;;
|
|
254
|
+
esac
|
|
255
|
+
b=$(basename "$t")
|
|
256
|
+
fi
|
|
257
|
+
case "$d" in
|
|
258
|
+
"$repo_root") printf '%s' "$b" ;;
|
|
259
|
+
"$repo_root"/*) printf '%s/%s' "${d#$repo_root/}" "$b" ;;
|
|
260
|
+
*) return 1 ;;
|
|
261
|
+
esac
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
rel=$(cd "$repo_root" && real_plan_path "$plan_file") || rel=""
|
|
265
|
+
[ -n "$rel" ] || die "$plan_file is outside the repository root"
|
|
266
|
+
|
|
267
|
+
# The filename, for symlink creation.
|
|
268
|
+
plan_basename=$(basename "$rel")
|
|
269
|
+
|
|
270
|
+
# Flip `**Phase:** Approved` → `Delivered` in the `## Status` section only.
|
|
271
|
+
flip_phase() { # $1=file → 0 if it changed the file, 1 if nothing to flip
|
|
272
|
+
local f="$1"
|
|
273
|
+
awk '
|
|
274
|
+
BEGIN { section = ""; done = 0 }
|
|
275
|
+
/^## / { section = ($0 ~ /^## Status/) ? "status" : ""; print; next }
|
|
276
|
+
section == "status" && !done && tolower($0) ~ /^[ \t]*[-*]?[ \t]*\**phase[:*]/ {
|
|
277
|
+
if (tolower($0) ~ /approved/) {
|
|
278
|
+
sub(/[Aa]pproved/, "Delivered")
|
|
279
|
+
done = 1
|
|
280
|
+
changed = 1
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
{ print }
|
|
284
|
+
END { exit (changed ? 0 : 1) }
|
|
285
|
+
' "$f" > "$f.plot-tmp"
|
|
286
|
+
local rc=$?
|
|
287
|
+
if [ "$rc" = 0 ]; then mv "$f.plot-tmp" "$f"; else rm -f "$f.plot-tmp"; fi
|
|
288
|
+
return "$rc"
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# Insert one `- **Delivered:** YYYY-MM-DD` line into the plan's `## Status` section.
|
|
292
|
+
# Fills the placeholder first; falls back to appending after the last list item.
|
|
293
|
+
append_delivered_line() { # $1=file $2=date
|
|
294
|
+
local f="$1" line
|
|
295
|
+
line="- **Delivered:** $2"
|
|
296
|
+
awk -v line="$line" '
|
|
297
|
+
{ lines[++n] = $0 }
|
|
298
|
+
END {
|
|
299
|
+
for (i = 1; i <= n; i++) {
|
|
300
|
+
if (lines[i] ~ /^##[ \t]*[Ss]tatus[ \t]*$/) { start = i; break }
|
|
301
|
+
}
|
|
302
|
+
if (!start) exit 1
|
|
303
|
+
|
|
304
|
+
insert = start
|
|
305
|
+
for (i = start + 1; i <= n; i++) {
|
|
306
|
+
if (lines[i] ~ /^##[ \t]/) break
|
|
307
|
+
if (lines[i] ~ /^[ \t]*[-*][ \t]*\*\*Delivered:\*\*[ \t]*$/) { slot = i; break }
|
|
308
|
+
if (lines[i] ~ /^[ \t]*[-*][ \t]*\*\*Delivered:\*\*[ \t]*<!--/) { slot = i; break }
|
|
309
|
+
if (lines[i] ~ /^[ \t]*[-*][ \t]/) insert = i
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
for (i = 1; i <= n; i++) {
|
|
313
|
+
if (i == slot) { print line; continue } # replaces the empty placeholder
|
|
314
|
+
print lines[i]
|
|
315
|
+
if (!slot && i == insert) print line
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
' "$f" > "$f.plot-tmp" || { rm -f "$f.plot-tmp"; return 1; }
|
|
319
|
+
mv "$f.plot-tmp" "$f"
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# Update the sprint item annotation for this plan.
|
|
323
|
+
update_sprint_annotation() { # $1=worktree root → prints none|updated|already|missing
|
|
324
|
+
local root="$1" f found=""
|
|
325
|
+
[ -n "$sprint" ] || { printf 'none'; return 0; }
|
|
326
|
+
local dir="$root/${SPRINT_DIR#/}"
|
|
327
|
+
[ -d "$dir" ] || { printf 'missing'; return 0; }
|
|
328
|
+
# Find the sprint file by content (the [<slug>] reference), not by filename.
|
|
329
|
+
for f in "$dir"/*.md; do
|
|
330
|
+
[ -e "$f" ] || continue
|
|
331
|
+
grep -q "\[$slug\]" "$f" 2>/dev/null && { found="$f"; break; }
|
|
332
|
+
done
|
|
333
|
+
[ -n "$found" ] || { printf 'missing'; return 0; }
|
|
334
|
+
|
|
335
|
+
local before after
|
|
336
|
+
before=$(cat "$found")
|
|
337
|
+
after=$(awk -v slug="$slug" '
|
|
338
|
+
index($0, "[" slug "]") == 0 { print; next }
|
|
339
|
+
{
|
|
340
|
+
line = $0
|
|
341
|
+
# Check the box
|
|
342
|
+
sub(/\[ \]/, "[x]", line)
|
|
343
|
+
# Update or add status annotation
|
|
344
|
+
if (index(line, "<!--") == 0) {
|
|
345
|
+
line = line " <!-- status: delivered -->"
|
|
346
|
+
} else {
|
|
347
|
+
if (line ~ /status:[ \t]*[a-z-]+/) sub(/status:[ \t]*[a-z-]+/, "status: delivered", line)
|
|
348
|
+
else sub(/-->/, ", status: delivered -->", line)
|
|
349
|
+
}
|
|
350
|
+
print line
|
|
351
|
+
}
|
|
352
|
+
' "$found")
|
|
353
|
+
if [ "$before" = "$after" ]; then printf 'already'; return 0; fi
|
|
354
|
+
printf '%s\n' "$after" > "$found"
|
|
355
|
+
printf 'updated'
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# Move the active/ symlink to delivered/.
|
|
359
|
+
move_index_symlink() { # $1=worktree root → prints moved|created|already|skipped
|
|
360
|
+
local root="$1"
|
|
361
|
+
local active_link="$root/${ACTIVE_DIR#/}$slug.md"
|
|
362
|
+
local delivered_link="$root/${DELIVERED_DIR#/}$slug.md"
|
|
363
|
+
local delivered_dir="$root/${DELIVERED_DIR#/}"
|
|
364
|
+
|
|
365
|
+
# Ensure delivered/ directory exists (best effort)
|
|
366
|
+
mkdir -p "$delivered_dir" 2>/dev/null || true
|
|
367
|
+
|
|
368
|
+
# Check if already in delivered/
|
|
369
|
+
if [ -e "$delivered_link" ]; then
|
|
370
|
+
# Remove from active/ if still there
|
|
371
|
+
git rm -q --ignore-unmatch "$active_link" 2>/dev/null || true
|
|
372
|
+
printf 'already'
|
|
373
|
+
return 0
|
|
374
|
+
fi
|
|
375
|
+
|
|
376
|
+
# Create the delivered/ symlink (relative path to the plan)
|
|
377
|
+
ln -sfn "../$plan_basename" "$delivered_link" 2>/dev/null || { printf 'skipped'; return 0; }
|
|
378
|
+
|
|
379
|
+
# Remove from active/ (best effort, ignore if not there)
|
|
380
|
+
git rm -q --ignore-unmatch "$active_link" 2>/dev/null || true
|
|
381
|
+
|
|
382
|
+
printf 'moved'
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
# The whole local half, run inside one directory.
|
|
386
|
+
apply_local_writes() { # $1=root → sets phase_report record_report index_report sprint_report
|
|
387
|
+
local root="$1" f="$1/$rel"
|
|
388
|
+
[ -f "$f" ] || { echo "plot-deliver: $rel is not present in $root" >&2; return 1; }
|
|
389
|
+
|
|
390
|
+
# Step 3 — flip the phase. Already-done test: the file no longer says Approved.
|
|
391
|
+
if flip_phase "$f"; then phase_report="flipped"; else phase_report="already"; fi
|
|
392
|
+
|
|
393
|
+
# Step 4 — fill the Delivered: record. Already-done test: it is non-empty.
|
|
394
|
+
local rec
|
|
395
|
+
rec=$(bash "$script_dir/plot-plan-meta.sh" "$f" 2>/dev/null | jq -r '.delivered_raw // ""' 2>/dev/null)
|
|
396
|
+
if [ -n "$rec" ]; then
|
|
397
|
+
record_report="already"
|
|
398
|
+
else
|
|
399
|
+
if append_delivered_line "$f" "$today"; then
|
|
400
|
+
record_report="written"
|
|
401
|
+
else
|
|
402
|
+
echo "plot-deliver: $rel has no '## Status' section — nowhere to record the delivery" >&2
|
|
403
|
+
return 1
|
|
404
|
+
fi
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
# Step 5 — move the index symlink (best effort).
|
|
408
|
+
index_report=$(move_index_symlink "$root")
|
|
409
|
+
|
|
410
|
+
# Step 6 — update the sprint annotation.
|
|
411
|
+
sprint_report=$(update_sprint_annotation "$root")
|
|
412
|
+
return 0
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
phase_report="" record_report="" index_report="skipped" sprint_report="none"
|
|
416
|
+
push_report="n/a"
|
|
417
|
+
|
|
418
|
+
# Fetch and create a booking worktree off origin/<default>.
|
|
419
|
+
git fetch -q origin "$MAIN" 2>/dev/null
|
|
420
|
+
|
|
421
|
+
bookbr="plot/deliver-$slug"
|
|
422
|
+
tmpwt="$wt_root/.plot-deliver-$slug.$$"
|
|
423
|
+
# -B: a leftover branch from an earlier failed run must not block this one.
|
|
424
|
+
git worktree add -q -B "$bookbr" "$tmpwt" "origin/$MAIN" 2>/dev/null \
|
|
425
|
+
|| die "could not prepare a booking worktree at $tmpwt"
|
|
426
|
+
|
|
427
|
+
cleanup() {
|
|
428
|
+
git worktree remove --force "$tmpwt" >/dev/null 2>&1 || true
|
|
429
|
+
git branch -D "$bookbr" >/dev/null 2>&1 || true
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if ! apply_local_writes "$tmpwt"; then
|
|
433
|
+
cleanup
|
|
434
|
+
exit 1
|
|
435
|
+
fi
|
|
436
|
+
|
|
437
|
+
# Stage the plan file and index changes.
|
|
438
|
+
git -C "$tmpwt" add -- "$rel" >/dev/null 2>&1 || true
|
|
439
|
+
git -C "$tmpwt" add -- "${ACTIVE_DIR#/}" >/dev/null 2>&1 || true
|
|
440
|
+
git -C "$tmpwt" add -- "${DELIVERED_DIR#/}" >/dev/null 2>&1 || true
|
|
441
|
+
[ "$sprint_report" = "updated" ] && git -C "$tmpwt" add -- "${SPRINT_DIR#/}" >/dev/null 2>&1
|
|
442
|
+
|
|
443
|
+
if git -C "$tmpwt" diff --cached --quiet 2>/dev/null; then
|
|
444
|
+
# THE IDEMPOTENT EXIT. Everything this run would have written was already
|
|
445
|
+
# on the default branch, so there is nothing to push and nothing wrong.
|
|
446
|
+
push_report="nothing-to-commit"
|
|
447
|
+
echo "step: nothing to commit — the delivery is already recorded on $MAIN"
|
|
448
|
+
cleanup
|
|
449
|
+
else
|
|
450
|
+
if ! git -C "$tmpwt" -c "user.name=$who" commit -q -m "plot: deliver $slug"; then
|
|
451
|
+
cleanup
|
|
452
|
+
die "could not commit the delivery"
|
|
453
|
+
fi
|
|
454
|
+
|
|
455
|
+
push_out=$(bash "$script_dir/plot-push-main.sh" "$bookbr" "$MAIN" 2>&1)
|
|
456
|
+
push_rc=$?
|
|
457
|
+
printf '%s\n' "$push_out" | sed 's/^/ /'
|
|
458
|
+
if [ "$push_rc" = 0 ]; then
|
|
459
|
+
push_report=$(printf '%s' "$push_out" | sed -n 's/^push: \([a-z]*\).*/\1/p' | head -1)
|
|
460
|
+
[ -n "$push_report" ] || push_report="unknown"
|
|
461
|
+
cleanup
|
|
462
|
+
else
|
|
463
|
+
# BRANCH PROTECTION FALLBACK — open a micro-PR if push is rejected.
|
|
464
|
+
echo "step: push rejected — opening a micro-PR instead"
|
|
465
|
+
if git push -q origin "$bookbr" 2>/dev/null \
|
|
466
|
+
&& micro_url=$(bash "$script_dir/plot-host.sh" pr-create \
|
|
467
|
+
--title "plot: deliver $slug" \
|
|
468
|
+
--body "Records the delivery of \`$slug\`." \
|
|
469
|
+
--base "$MAIN" --head "$bookbr" 2>/dev/null) \
|
|
470
|
+
&& micro_num=$(printf '%s' "$micro_url" | sed 's#.*/##') \
|
|
471
|
+
&& bash "$script_dir/plot-host.sh" pr-merge "$micro_num" --delete-branch >/dev/null 2>&1
|
|
472
|
+
then
|
|
473
|
+
push_report="micro-pr"
|
|
474
|
+
echo "step: delivery landed via micro-PR $micro_url"
|
|
475
|
+
cleanup
|
|
476
|
+
else
|
|
477
|
+
push_report="rejected"
|
|
478
|
+
echo "plot-deliver: the delivery is committed on '$bookbr' but could not reach $MAIN." >&2
|
|
479
|
+
echo " Land '$bookbr' by hand, or re-run this command once the push works." >&2
|
|
480
|
+
git worktree remove --force "$tmpwt" >/dev/null 2>&1 || true
|
|
481
|
+
echo "summary: phase=$phase_report record=$record_report index=$index_report sprint=$sprint_report push=$push_report"
|
|
482
|
+
exit 1
|
|
483
|
+
fi
|
|
484
|
+
fi
|
|
485
|
+
fi
|
|
486
|
+
|
|
487
|
+
echo "summary: phase=$phase_report record=$record_report index=$index_report sprint=$sprint_report push=$push_report"
|
|
488
|
+
exit 0
|