@plot-pm/board 0.14.1 → 0.14.2
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 +94 -91
- package/package.json +1 -1
- package/plot-deliver.sh +66 -0
- package/plot-dispatch.sh +168 -1
- package/plot-host.sh +96 -2
- package/plot-plan-meta.sh +75 -0
package/package.json
CHANGED
package/plot-deliver.sh
CHANGED
|
@@ -419,6 +419,64 @@ decide_transition() { # $1=file → prints "<Phase>\t<record>\t<write|already>"
|
|
|
419
419
|
# writes — and appending a second is how one plan came to hold two `Delivered:`
|
|
420
420
|
# lines (2026-09-01). The domain returns the written record unchanged in that
|
|
421
421
|
# case, so the phase still flips and nothing is inserted.
|
|
422
|
+
# Would the PARSER read this phase out of the file we are about to land?
|
|
423
|
+
#
|
|
424
|
+
# THE WRITE SUCCEEDING IS NOT THE OUTCOME HOLDING, and that gap is #924. A plan
|
|
425
|
+
# carrying BOTH front matter and a `## Status` block was delivered in a project
|
|
426
|
+
# repo: `flip_phase` wrote `Delivered` into the block, `mv` landed it, the
|
|
427
|
+
# summary said `phase=flipped`, and `plot-plan-meta.sh` went on answering
|
|
428
|
+
# `approved` — because it prefers front matter wherever it exists and reads the
|
|
429
|
+
# block only in the `else if` below. The write took effect on bytes nobody
|
|
430
|
+
# reads.
|
|
431
|
+
#
|
|
432
|
+
# `flip_phase`'s awk matches only inside `section == "status"`. That one guard
|
|
433
|
+
# IS the defect: on a front-matter plan it edits the block and leaves the front
|
|
434
|
+
# matter untouched, and returns 0 for having changed something.
|
|
435
|
+
#
|
|
436
|
+
# SO THE TEST IS WHAT THE PARSER ANSWERS, NEVER WHETHER AWK CHANGED A LINE.
|
|
437
|
+
# Refusing on `flipped=0` would break every re-run of a correct delivery — a
|
|
438
|
+
# plan already carrying `Delivered` flips nothing and is fine. This asks the one
|
|
439
|
+
# question that distinguishes them: read the scratch copy the way every later
|
|
440
|
+
# consumer will read the plan, and compare.
|
|
441
|
+
#
|
|
442
|
+
# IT RUNS ON THE SCRATCH COPY, BEFORE THE `mv`, and the caller passes whichever
|
|
443
|
+
# file that arm is about to land — `$a` on the `recorded=yes` arm, `$b` on the
|
|
444
|
+
# other. Parsing `$a` on the record arm would check content that never reaches
|
|
445
|
+
# the plan. After the `mv` is too late twice over: the script's own header
|
|
446
|
+
# documents exit 0 as *"the plan is Delivered on the default branch"*, so
|
|
447
|
+
# refusing there would exit 1 on a run meeting the documented success
|
|
448
|
+
# condition — and `runAutoDeliver` spawns this detached, logging a non-zero exit
|
|
449
|
+
# to nobody while the plan sits delivered on main.
|
|
450
|
+
#
|
|
451
|
+
# AN UNREADABLE SCRATCH COPY REFUSES. `decide_transition` already takes that
|
|
452
|
+
# line — *"refusing rather than guessing"* — and a file the parser cannot read
|
|
453
|
+
# is exactly the state this gate exists to keep off the plan.
|
|
454
|
+
phase_would_read() { # $1=scratch file $2=expected phase (lowercase) → 0 agrees, 1 refuses
|
|
455
|
+
local scratch="$1" want="$2" m got
|
|
456
|
+
m=$(bash "$script_dir/plot-plan-meta.sh" "$scratch" 2>/dev/null) || m=""
|
|
457
|
+
if [ -z "$m" ]; then
|
|
458
|
+
echo "plot-deliver: $rel — the written file does not parse, so the delivery was not landed." >&2
|
|
459
|
+
echo " Nothing was written. Re-run after fixing the file." >&2
|
|
460
|
+
return 1
|
|
461
|
+
fi
|
|
462
|
+
got=$(printf '%s' "$m" | jq -r '.phase // ""')
|
|
463
|
+
[ "$got" = "$want" ] && return 0
|
|
464
|
+
|
|
465
|
+
# THE REFUSAL NAMES BOTH VALUES AND THE FILE. A message saying only "delivery
|
|
466
|
+
# failed" throws away the half a person acts on: which phase was written, and
|
|
467
|
+
# which one the parser still reads. The cause is named too, because the file
|
|
468
|
+
# holding two records of one fact is the thing to fix — and which format ought
|
|
469
|
+
# to win is a decision this gate deliberately leaves to a person.
|
|
470
|
+
echo "plot-deliver: $rel — wrote phase '$want', but the parser still reads '$got'." >&2
|
|
471
|
+
echo " The plan states its phase in TWO places and they disagree: the write" >&2
|
|
472
|
+
echo " landed in the '## Status' block while front matter takes precedence," >&2
|
|
473
|
+
echo " so the delivery would have reported a success it did not achieve." >&2
|
|
474
|
+
echo " Nothing was written — the plan is unchanged. Remove one of the two" >&2
|
|
475
|
+
echo " records (front matter, or the '## Status' block) and re-run." >&2
|
|
476
|
+
echo " See what the parser reads: $script_dir/plot-plan-meta.sh $rel" >&2
|
|
477
|
+
return 1
|
|
478
|
+
}
|
|
479
|
+
|
|
422
480
|
write_transition() { # $1=file $2=record $3=recorded(yes|no) → sets phase_report record_report
|
|
423
481
|
local f="$1" record="$2" recorded="$3" a="$1.plot-phase" b="$1.plot-record" flipped=0
|
|
424
482
|
|
|
@@ -428,6 +486,11 @@ write_transition() { # $1=file $2=record $3=recorded(yes|no) → sets phase_repo
|
|
|
428
486
|
[ -s "$a" ] || { rm -f "$a"; echo "plot-deliver: could not read $rel" >&2; return 1; }
|
|
429
487
|
|
|
430
488
|
if [ "$recorded" = "yes" ]; then
|
|
489
|
+
# THE DRY RUN, on the file this arm is about to land. A refusal discards the
|
|
490
|
+
# scratch copy and leaves the plan byte-identical — and never reaches
|
|
491
|
+
# `record_state_receipt`, which would otherwise license a commit of a state
|
|
492
|
+
# that was refused.
|
|
493
|
+
phase_would_read "$a" delivered || { rm -f "$a"; return 1; }
|
|
431
494
|
mv "$a" "$f" || { rm -f "$a"; return 1; }
|
|
432
495
|
record_report="already"
|
|
433
496
|
else
|
|
@@ -438,6 +501,9 @@ write_transition() { # $1=file $2=record $3=recorded(yes|no) → sets phase_repo
|
|
|
438
501
|
echo " with no record is invisible to the scan. Fix the section and re-run." >&2
|
|
439
502
|
return 1
|
|
440
503
|
fi
|
|
504
|
+
# `$b` AND NOT `$a`: this arm lands the file carrying the record, so `$a`
|
|
505
|
+
# is content that never reaches the plan.
|
|
506
|
+
phase_would_read "$b" delivered || { rm -f "$a" "$b"; return 1; }
|
|
441
507
|
mv "$b" "$f" || { rm -f "$a" "$b"; return 1; }
|
|
442
508
|
rm -f "$a"
|
|
443
509
|
record_report="written"
|
package/plot-dispatch.sh
CHANGED
|
@@ -56,6 +56,25 @@
|
|
|
56
56
|
# tradition of --allow-local: a gate with no exit is one people
|
|
57
57
|
# route around by never annotating at all. It says so on the
|
|
58
58
|
# line it overrides, so the override is on the record.
|
|
59
|
+
# --agent <name> dispatch this run's agents under the charter
|
|
60
|
+
# `.plot/charters/<name>.json`. It SETS `PLOT_AGENT`, which was
|
|
61
|
+
# the input all along and which nothing chose: the charter
|
|
62
|
+
# mechanism shipped in v2.17.0 with 16 readers for `harness` and
|
|
63
|
+
# no selector at all, so a kind could be declared and never
|
|
64
|
+
# asked for. The choice is EXPLICIT and never inferred — a
|
|
65
|
+
# matcher reading a plan could guess a kind, and a guess that is
|
|
66
|
+
# usually right produces a fleet whose wrong answers cannot be
|
|
67
|
+
# explained. An already-exported PLOT_AGENT is overridden, since
|
|
68
|
+
# a flag on this run is the more specific answer.
|
|
69
|
+
# AMENDED 2026-09-15: a slice may now DECLARE its kind, and
|
|
70
|
+
# `start_worker` reads it where this flag left `PLOT_AGENT`
|
|
71
|
+
# unset. That is not the matcher refused above — a declaration is
|
|
72
|
+
# a field a person wrote, and nothing here ranks candidates or
|
|
73
|
+
# infers a kind from a slice's contents. The flag still wins,
|
|
74
|
+
# because a flag typed on this run is more specific than a field
|
|
75
|
+
# written when the plan was drafted. A charter this clone does
|
|
76
|
+
# not hold is REPORTED and dispatched anyway; see
|
|
77
|
+
# `plan_declared_agent`.
|
|
59
78
|
# <slug> the plan to fan out
|
|
60
79
|
# Output: one line per branch, each optionally followed by an indented
|
|
61
80
|
# `in flight:` line naming a branch that already holds files, then the
|
|
@@ -230,6 +249,7 @@ allow_waiting=0
|
|
|
230
249
|
max=0
|
|
231
250
|
slug=""
|
|
232
251
|
migrate_yes=0
|
|
252
|
+
agent=""
|
|
233
253
|
while [ $# -gt 0 ]; do
|
|
234
254
|
case "$1" in
|
|
235
255
|
--dry-run) dry_run=1 ;;
|
|
@@ -270,17 +290,56 @@ while [ $# -gt 0 ]; do
|
|
|
270
290
|
--offline|--no-fetch) offline="--offline" ;;
|
|
271
291
|
--allow-local) allow_local=1 ;;
|
|
272
292
|
--allow-waiting) allow_waiting=1 ;;
|
|
293
|
+
# THE VALUE IS REQUIRED AND ALWAYS CONSUMED, which is the opposite of the
|
|
294
|
+
# rule `--stop`, `--restart` and `--start` follow — and the difference is
|
|
295
|
+
# the value's SHAPE rather than a change of mind. Those three take a branch
|
|
296
|
+
# (`*/*`) or a count (digits), each recognisable on sight, so an absent one
|
|
297
|
+
# can be left for the parser. An agent name is a bare word and so is a plan
|
|
298
|
+
# slug: nothing tells `--agent reviewer` from `--agent` followed by the
|
|
299
|
+
# slug. Leaving it unconsumed would let the `*)` arm below silently take
|
|
300
|
+
# the agent name as the plan to dispatch, which reads as "no such plan" and
|
|
301
|
+
# names neither what was asked nor what went wrong. So a missing value
|
|
302
|
+
# REFUSES rather than guessing.
|
|
303
|
+
--agent) agent="${2:?--agent needs a charter name, e.g. --agent reviewer}"
|
|
304
|
+
case "$agent" in
|
|
305
|
+
-*) echo "plot-dispatch: --agent needs a charter name, got '$agent'" >&2; exit 1 ;;
|
|
306
|
+
esac
|
|
307
|
+
shift ;;
|
|
273
308
|
--max) max="${2:?--max needs a value}"
|
|
274
309
|
case "$max" in
|
|
275
310
|
''|*[!0-9]*) echo "plot-dispatch: --max needs a number, got '$max'" >&2; exit 1 ;;
|
|
276
311
|
esac
|
|
277
312
|
shift ;;
|
|
278
|
-
|
|
313
|
+
# THE RANGE MOVED WITH THE HEADER IT PRINTS. It ended at `<slug>` and still
|
|
314
|
+
# does; adding `--agent` above pushed that line from 59 to 69, and
|
|
315
|
+
# documenting the plan-declared kind pushed it from 69 to 78. Two records
|
|
316
|
+
# of one fact, and nothing compares them — a stale number here silently
|
|
317
|
+
# truncates the help rather than failing, so it is checked by a test.
|
|
318
|
+
-h|--help) sed -n '2,78p' "$0"; exit 0 ;;
|
|
279
319
|
*) slug="$1" ;;
|
|
280
320
|
esac
|
|
281
321
|
shift
|
|
282
322
|
done
|
|
283
323
|
|
|
324
|
+
# `--agent` SETS THE INPUT THAT ALREADY EXISTED, and it sets it in ONE place.
|
|
325
|
+
#
|
|
326
|
+
# `PLOT_AGENT` was read in three places before this flag — `resolve_launch`'s
|
|
327
|
+
# call in `start_worker`, the `--capabilities` block, and the forwarded export
|
|
328
|
+
# into the worker's environment — and assigned in none: `plot-dispatch.sh` held
|
|
329
|
+
# `PLOT_AGENT="${PLOT_AGENT:-}"`, a pass-through of whatever the operator had
|
|
330
|
+
# already exported. So the selector existed as a variable nothing chose.
|
|
331
|
+
#
|
|
332
|
+
# ONE ASSIGNMENT RATHER THAN A THREADED ARGUMENT. Every reader already asks
|
|
333
|
+
# `${PLOT_AGENT:-}`; exporting here reaches all three with no further change and
|
|
334
|
+
# leaves no call site that could be missed when a fourth reader is added. The
|
|
335
|
+
# flag's whole meaning is "set PLOT_AGENT", which is also what keeps it from
|
|
336
|
+
# becoming a second selector competing with the variable.
|
|
337
|
+
#
|
|
338
|
+
# THE FLAG WINS OVER AN INHERITED VALUE. A dispatched worker runs with its own
|
|
339
|
+
# `PLOT_AGENT` in the environment, so a run launched from inside one would
|
|
340
|
+
# otherwise inherit a kind nobody asked for on this dispatch.
|
|
341
|
+
[ -n "$agent" ] && export PLOT_AGENT="$agent"
|
|
342
|
+
|
|
284
343
|
# THE TWO PRECONDITIONS OF A DISPATCH, and a sourcing test has neither.
|
|
285
344
|
# `PLOT_DISPATCH_SOURCED=1` is taking the definitions below rather than running
|
|
286
345
|
# a dispatch, so it has no slug and often no git repository; the guard that
|
|
@@ -930,6 +989,42 @@ start_worker() {
|
|
|
930
989
|
local branch="$1" wt="$2"
|
|
931
990
|
local cmd
|
|
932
991
|
|
|
992
|
+
# THE PLAN IS THE DEFAULT AND THE FLAG IS THE OVERRIDE.
|
|
993
|
+
#
|
|
994
|
+
# `[ -z "${PLOT_AGENT:-}" ]` is the whole precedence rule: `--agent` exported
|
|
995
|
+
# the variable before this line was reached, so a flag typed on this run stops
|
|
996
|
+
# the plan being read at all. A field written when the plan was drafted is the
|
|
997
|
+
# less specific answer — the same order `--agent` already applies to an
|
|
998
|
+
# inherited `PLOT_AGENT`.
|
|
999
|
+
#
|
|
1000
|
+
# ONE ASSIGNMENT, FOR THE REASON `--agent` GIVES ONE FIELD OVER. Four readers
|
|
1001
|
+
# ask `${PLOT_AGENT:-}` — `resolve_launch` below, the capabilities block, the
|
|
1002
|
+
# manifest, and the export into the worker environment — so setting the
|
|
1003
|
+
# variable reaches all four and leaves no call site to miss when a fifth
|
|
1004
|
+
# appears.
|
|
1005
|
+
#
|
|
1006
|
+
# A FREE AGENT HAS NO BRANCH and so has no plan to read: `--start` calls this
|
|
1007
|
+
# with `branch` empty, and `plan_declared_agent` returns 1 on it immediately.
|
|
1008
|
+
if [ -z "${PLOT_AGENT:-}" ] && [ -n "$branch" ]; then
|
|
1009
|
+
local declared charter
|
|
1010
|
+
if declared=$(plan_declared_agent "$branch"); then
|
|
1011
|
+
export PLOT_AGENT="$declared"
|
|
1012
|
+
# A MISSING CHARTER IS REPORTED AND DISPATCHED ANYWAY. Said HERE rather
|
|
1013
|
+
# than only in the manifest, because this is where a person watching the
|
|
1014
|
+
# run can still act on it: the launch below falls back to the repo default
|
|
1015
|
+
# and is otherwise silent about a declaration that reached nothing.
|
|
1016
|
+
charter=$(charter_file_for "$declared")
|
|
1017
|
+
if [ -n "$charter" ] && [ ! -f "$charter" ]; then
|
|
1018
|
+
echo " $branch declares agent '$declared', and no charter answers to that name"
|
|
1019
|
+
echo " looked for $charter"
|
|
1020
|
+
echo " starting it on the repo's 'Worker command' — a plan written where that"
|
|
1021
|
+
echo " charter exists must stay dispatchable on a clone that lacks it."
|
|
1022
|
+
else
|
|
1023
|
+
echo " $branch declares agent '$declared' — its plan selected the charter"
|
|
1024
|
+
fi
|
|
1025
|
+
fi
|
|
1026
|
+
fi
|
|
1027
|
+
|
|
933
1028
|
# RESOLVED BEFORE `Worker command`, because the charter is the more specific
|
|
934
1029
|
# answer and the config key is the fallback rather than the sole source.
|
|
935
1030
|
#
|
|
@@ -1359,6 +1454,78 @@ start_worker() {
|
|
|
1359
1454
|
return 0
|
|
1360
1455
|
}
|
|
1361
1456
|
|
|
1457
|
+
# WHICH KIND OF AGENT A BRANCH'S PLAN DECLARES — the selector nobody has to type.
|
|
1458
|
+
#
|
|
1459
|
+
# `--agent` was the only one, and an operator is the only thing that can type a
|
|
1460
|
+
# flag. The registry hands a queued slice to a free agent with no `--agent`
|
|
1461
|
+
# anywhere in the path, so an unattended fleet ran every slice as the same
|
|
1462
|
+
# undifferentiated worker. A plan that names the kind is what reaches a dispatch
|
|
1463
|
+
# nobody is watching.
|
|
1464
|
+
#
|
|
1465
|
+
# IT ANSWERS AND EXPORTS NOTHING. The caller decides, because the precedence is
|
|
1466
|
+
# the caller's: `--agent` already won over an inherited `PLOT_AGENT` and must win
|
|
1467
|
+
# over this too — a flag typed on this run is more specific than a field written
|
|
1468
|
+
# when the plan was drafted.
|
|
1469
|
+
#
|
|
1470
|
+
# THE PLAN DIRECTORY IS SEARCHED, NOT THE ACTIVE INDEX, and the candidates are
|
|
1471
|
+
# grepped before any is parsed — both `plot-open-pr.sh`'s rules, measured there:
|
|
1472
|
+
# a plan governing a branch may carry no symlink, and parsing 253 plans took
|
|
1473
|
+
# 103 s against 0.6 s for one `grep -lF` over the directory. A branch name is a
|
|
1474
|
+
# literal string, so a plan that does not contain it cannot name it.
|
|
1475
|
+
#
|
|
1476
|
+
# ABSENCE IS THE COMMON ANSWER AND NEVER AN ERROR. Every plan on the estate
|
|
1477
|
+
# names no kind, a `--restart` may run on a branch no plan names at all, and the
|
|
1478
|
+
# annotation is optional by design. All three print nothing and return 1.
|
|
1479
|
+
plan_declared_agent() { # $1 = branch → prints the declared kind, or nothing
|
|
1480
|
+
local branch="$1" plan_dir root cands f found
|
|
1481
|
+
[ -n "$branch" ] || return 1
|
|
1482
|
+
root=$(git rev-parse --show-toplevel 2>/dev/null) || return 1
|
|
1483
|
+
plan_dir=$("$script_dir/plot-config.sh" get "Plan directory" "docs/plans/")
|
|
1484
|
+
case "$plan_dir" in /*) ;; *) plan_dir="$root/$plan_dir" ;; esac
|
|
1485
|
+
|
|
1486
|
+
cands=$(grep -lF "$branch" "$plan_dir"*.md 2>/dev/null || true)
|
|
1487
|
+
for f in $cands; do
|
|
1488
|
+
[ -e "$f" ] || continue
|
|
1489
|
+
# The PARSER answers, never a grep of the plan file. A `grep agent:` would
|
|
1490
|
+
# read the marker out of prose documenting it and out of a DIFFERENT branch
|
|
1491
|
+
# line in the same plan — the annotation binds to one branch, and only the
|
|
1492
|
+
# parser knows which.
|
|
1493
|
+
found=$(bash "$script_dir/plot-plan-meta.sh" "$f" 2>/dev/null \
|
|
1494
|
+
| PLOT_WANT_BRANCH="$branch" node -e '
|
|
1495
|
+
let s = "";
|
|
1496
|
+
process.stdin.on("data", (d) => (s += d)).on("end", () => {
|
|
1497
|
+
let meta;
|
|
1498
|
+
try { meta = JSON.parse(s); } catch { return; }
|
|
1499
|
+
const want = process.env.PLOT_WANT_BRANCH;
|
|
1500
|
+
for (const wave of meta.waves ?? []) {
|
|
1501
|
+
for (const b of wave.branches ?? []) {
|
|
1502
|
+
// PRESENCE, not truthiness. The parser emits no `agent` key where
|
|
1503
|
+
// the plan declares none, which is every plan on the estate.
|
|
1504
|
+
if (b.branch === want && "agent" in b) { console.log(b.agent); return; }
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
') || found=""
|
|
1509
|
+
if [ -n "$found" ]; then printf '%s\n' "$found"; return 0; fi
|
|
1510
|
+
done
|
|
1511
|
+
return 1
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
# DOES THIS CLONE HOLD THE CHARTER? Answered so a MISSING one can be REPORTED.
|
|
1515
|
+
#
|
|
1516
|
+
# A DELIBERATE ASYMMETRY, and the plan settles it: `resolve_launch` refuses a
|
|
1517
|
+
# charter it cannot BELIEVE (malformed) and a harness not on PATH, and both
|
|
1518
|
+
# stay. A charter that simply does not EXIST is the adoption case — a plan
|
|
1519
|
+
# written where `reviewer` is declared, dispatched on a clone that declares
|
|
1520
|
+
# nothing — and refusing it would make that plan undispatchable on every such
|
|
1521
|
+
# clone. So the kind still travels, the launch falls back to the repo default,
|
|
1522
|
+
# and the run SAYS which name it looked for.
|
|
1523
|
+
charter_file_for() { # $1 = agent name → prints the path it would read
|
|
1524
|
+
local name="$1" root
|
|
1525
|
+
root=$(git rev-parse --show-toplevel 2>/dev/null) || return 1
|
|
1526
|
+
printf '%s/.plot/charters/%s.json\n' "$root" "$name"
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1362
1529
|
# `PLOT_DISPATCH_SOURCED=1` STOPS HERE, so a test can take `resolve_launch` and
|
|
1363
1530
|
# `start_worker` without dispatching anything — `plot-worker-loop.sh` states
|
|
1364
1531
|
# this idiom for `resolve_prompt_file`, and this is the same one applied to the
|
package/plot-host.sh
CHANGED
|
@@ -716,8 +716,41 @@ EOF
|
|
|
716
716
|
# failed — Jenkins is unreachable (`jen auth status` says so, while EXITING
|
|
717
717
|
# 0 — Done-when 4: the wording decides, never `$?`), or the listing
|
|
718
718
|
# was empty/garbled. `map` is {}; the caller renders rows `unknown`.
|
|
719
|
-
# unknown — the auth wording was unrecognised
|
|
720
|
-
#
|
|
719
|
+
# unknown — the auth wording was unrecognised, or the configured job is a
|
|
720
|
+
# SHAPE NOBODY MEASURED; degrade to failure-shaped (cannot verify),
|
|
721
|
+
# never to ok. `map` is {}.
|
|
722
|
+
#
|
|
723
|
+
# THE JOB'S SHAPE DECIDES THE VERB, and reading the shape off the wrong object
|
|
724
|
+
# is the defect this function carried until 2026-09-15.
|
|
725
|
+
#
|
|
726
|
+
# `job list` enumerates a CONTAINER'S CHILDREN. A `WorkflowMultiBranchProject`
|
|
727
|
+
# has one child per branch, so listing it yields exactly the branch→colour map
|
|
728
|
+
# below. A plain `WorkflowJob` has no children, so the same call yields `null` —
|
|
729
|
+
# not an error, not an empty array — and the `type=="array"` guard reported
|
|
730
|
+
# `failed`, which is the word for an unreachable host. Measured live 2026-09-15
|
|
731
|
+
# on `Quatico.Webseite/quaweb-website`: `job list quaweb/continuous-deploy`
|
|
732
|
+
# answered `null` while `job view` on the same path answered `color blue`,
|
|
733
|
+
# `lastBuild #938 SUCCESS`. A healthy, signed-in, correctly declared pipeline
|
|
734
|
+
# read as *the connector cannot be asked*.
|
|
735
|
+
#
|
|
736
|
+
# THE DECIDING `_class` IS THE CONFIGURED JOB'S OWN, AND IT IS NOT IN THE
|
|
737
|
+
# LISTING THIS FUNCTION ALREADY PERFORMS. A child's `_class` describes the
|
|
738
|
+
# CHILD: measured live, every child of the multibranch `quaweb/continuous-build`
|
|
739
|
+
# carries `...job.WorkflowJob`, and this repository's own fixture agrees. So
|
|
740
|
+
# reading `.[0]._class` from the listing would read a healthy multibranch job as
|
|
741
|
+
# plain, route it to `job view`, and break the half that works today — while
|
|
742
|
+
# every gate still passed. That mistake sank an earlier draft of the plan.
|
|
743
|
+
#
|
|
744
|
+
# SO `job view` IS ASKED FIRST, and it answers BOTH questions in ONE call: the
|
|
745
|
+
# job's own `_class`, and — for a plain job — the `color` and `lastBuild` that
|
|
746
|
+
# are its state. The multibranch path then makes the single `job list` it has
|
|
747
|
+
# always made, so a multibranch refresh costs two calls rather than one per
|
|
748
|
+
# branch, and the branch→checks map it returns is byte-identical.
|
|
749
|
+
#
|
|
750
|
+
# EXACTLY TWO SHAPES ARE READ, and anything else is `unknown` rather than a
|
|
751
|
+
# guess. A `FreeStyleProject` has a `color` and would be readable; it still
|
|
752
|
+
# reports `unknown`, which is the honest word — *a shape nobody measured*. It is
|
|
753
|
+
# deliberately NOT `failed`, which claims the host did not answer.
|
|
721
754
|
jenkins_build_map() {
|
|
722
755
|
local instance="$1"
|
|
723
756
|
local slug job
|
|
@@ -751,6 +784,67 @@ jenkins_build_map() {
|
|
|
751
784
|
printf '{"status":"unknown","map":{}}\n'; return 0
|
|
752
785
|
fi
|
|
753
786
|
|
|
787
|
+
# THE SHAPE, READ FROM THE CONFIGURED JOB ITSELF. `job view` returns that
|
|
788
|
+
# job's own `_class` — never a child's — plus the `color` and `lastBuild` a
|
|
789
|
+
# plain job's state is made of. One call, two answers.
|
|
790
|
+
#
|
|
791
|
+
# A BARE-HOST INSTANCE NAMES NO JOB, so there is nothing to view: `job` is
|
|
792
|
+
# empty, the root scope has no `_class` of its own, and the multibranch path
|
|
793
|
+
# below already handles it by listing at the root. Probing with an empty path
|
|
794
|
+
# would ask about the instance rather than about a job.
|
|
795
|
+
local shape="" view_out=""
|
|
796
|
+
if [ -n "$job" ]; then
|
|
797
|
+
view_out=$(jen -I "$slug" job view "$job" --json 2>&1) || true
|
|
798
|
+
if [ -n "$view_out" ]; then
|
|
799
|
+
shape=$(printf '%s' "$view_out" | jq -r 'if type=="object" then (._class // "") else "" end' 2>/dev/null || echo "")
|
|
800
|
+
fi
|
|
801
|
+
fi
|
|
802
|
+
|
|
803
|
+
case "$shape" in
|
|
804
|
+
# A PLAIN PIPELINE — the case that reported `failed` until 2026-09-15. Its
|
|
805
|
+
# state is already in hand: `job view` answered it, and no `job list`
|
|
806
|
+
# follows, because listing a job with no children is what returned `null`.
|
|
807
|
+
#
|
|
808
|
+
# THE BRANCH KEY IS THE JOB PATH'S LAST SEGMENT. A plain job builds one
|
|
809
|
+
# thing and Jenkins names no branch for it, so there is no branch→colour
|
|
810
|
+
# map to build. Keying on the job's own name is what lets `.map[$branch]`
|
|
811
|
+
# find it — `runs` reads that key, and the op's caller asks by the name the
|
|
812
|
+
# instance declares.
|
|
813
|
+
*'.WorkflowJob')
|
|
814
|
+
printf '%s' "$view_out" | jq -c --arg job "$job" '
|
|
815
|
+
def color_to_checks:
|
|
816
|
+
if . == null or . == "" then "none"
|
|
817
|
+
elif endswith("_anime") then "pending"
|
|
818
|
+
elif . == "blue" then "green"
|
|
819
|
+
elif . == "red" or . == "yellow" then "failing"
|
|
820
|
+
else "none"
|
|
821
|
+
end;
|
|
822
|
+
($job | split("/") | last) as $name
|
|
823
|
+
| { status: "ok",
|
|
824
|
+
map: { ($name): { color: .color,
|
|
825
|
+
checks: (.color | color_to_checks),
|
|
826
|
+
job: $job } } }
|
|
827
|
+
' 2>/dev/null || printf '{"status":"failed","map":{}}\n'
|
|
828
|
+
return 0
|
|
829
|
+
;;
|
|
830
|
+
*'.WorkflowMultiBranchProject')
|
|
831
|
+
: # fall through to the listing below — the path that has always worked
|
|
832
|
+
;;
|
|
833
|
+
'')
|
|
834
|
+
# `job view` answered nothing usable. NOT a shape verdict: an instance
|
|
835
|
+
# naming no job reaches here by design, and so does a `jen` too old to
|
|
836
|
+
# know the verb. Fall through and let the listing decide, which is
|
|
837
|
+
# exactly what this function did before the probe existed.
|
|
838
|
+
:
|
|
839
|
+
;;
|
|
840
|
+
*)
|
|
841
|
+
# A SHAPE NOBODY MEASURED. `unknown` says that; `failed` would claim
|
|
842
|
+
# Jenkins did not answer, when it answered clearly and said something
|
|
843
|
+
# this reader has never been taught to read.
|
|
844
|
+
printf '{"status":"unknown","map":{}}\n'; return 0
|
|
845
|
+
;;
|
|
846
|
+
esac
|
|
847
|
+
|
|
754
848
|
# One call, every branch — the spike's whole point (Done-when 5).
|
|
755
849
|
local out=""
|
|
756
850
|
out=$(jen -I "$slug" job list ${job:+"$job"} --json 2>&1) || true
|
package/plot-plan-meta.sh
CHANGED
|
@@ -150,6 +150,16 @@
|
|
|
150
150
|
# BECAUSE annotations already work in both slice dialects from
|
|
151
151
|
# one block of code — a field line would need two spellings,
|
|
152
152
|
# and the template writes the list dialect.
|
|
153
|
+
# `<!-- agent: reviewer -->` names which KIND of agent this
|
|
154
|
+
# slice needs, reported as `waves[].branches[].agent` and read
|
|
155
|
+
# by plot-dispatch.sh where `--agent` is absent. OPTIONAL, and
|
|
156
|
+
# the key is ABSENT where none was written, never "" — dispatch
|
|
157
|
+
# reads presence, so a blank would send it looking for a charter
|
|
158
|
+
# called "". The value runs to the closing marker, as `builds:`
|
|
159
|
+
# does. It CANNOT validate its value the way `waits:` does: a
|
|
160
|
+
# charter name is a bare word with no structure to check, so a
|
|
161
|
+
# template documents the marker inside an outer comment block,
|
|
162
|
+
# where this pattern cannot see it.
|
|
153
163
|
# prs PR numbers, sorted and unique, read from EITHER spelling:
|
|
154
164
|
# `→ #NNN` / `→ owner/repo#NNN` links in the `## Branches`
|
|
155
165
|
# section, OR `PR: #NNN` in a `## Waves` `### ` heading. The
|
|
@@ -424,6 +434,7 @@ function reset_state() {
|
|
|
424
434
|
delete deferred_of; delete deferred_why; delete claimed_of; delete ordered_b; n_waves = 0
|
|
425
435
|
delete waits_of; delete waits_set
|
|
426
436
|
delete builds_of; delete builds_set
|
|
437
|
+
delete agent_of; delete agent_set
|
|
427
438
|
delete started; n_started = 0
|
|
428
439
|
fm_changelog = ""
|
|
429
440
|
delete changelog; n_changelog = 0; changelog_seen = 0; cl_open = 0
|
|
@@ -584,6 +595,13 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
584
595
|
# names no deliverable emits no key, so a consumer reads a name or
|
|
585
596
|
# nothing. An empty string would read as a deliverable called "".
|
|
586
597
|
if (builds_set[i] == 1) out = out ",\"builds\":\"" jesc(builds_of[i]) "\""
|
|
598
|
+
# ABSENT, NOT EMPTY, the rule the three annotations above already keep.
|
|
599
|
+
# Dispatch reads the PRESENCE of this key to decide whether the plan names
|
|
600
|
+
# a kind at all, so an `"agent":""` would read as a charter called "" and
|
|
601
|
+
# send it looking for `.plot/charters/.json`.
|
|
602
|
+
# NOTE: no apostrophe may appear in this awk region — the whole program is
|
|
603
|
+
# one single-quoted shell string, and one closes it mid-comment.
|
|
604
|
+
if (agent_set[i] == 1) out = out ",\"agent\":\"" jesc(agent_of[i]) "\""
|
|
587
605
|
out = out "}"
|
|
588
606
|
first = 0
|
|
589
607
|
}
|
|
@@ -1012,6 +1030,44 @@ section == "slices" && slice_shape != "heading" {
|
|
|
1012
1030
|
if (waits_note ~ "^(" PREFIXES ")/[^ \t]+$") has_waits = 1
|
|
1013
1031
|
else waits_note = ""
|
|
1014
1032
|
}
|
|
1033
|
+
# WHICH KIND OF AGENT THIS SLICE NEEDS: `<!-- agent: reviewer -->`.
|
|
1034
|
+
#
|
|
1035
|
+
# `--agent <name>` was the only selector and only an operator could type it.
|
|
1036
|
+
# `plot-registryd` hands a queued slice to a free agent with no `--agent`
|
|
1037
|
+
# anywhere in the path, so an unattended fleet ran every slice as the same
|
|
1038
|
+
# undifferentiated worker. The declaration has to live in the PLAN to reach a
|
|
1039
|
+
# dispatch nobody is watching.
|
|
1040
|
+
#
|
|
1041
|
+
# PER-BRANCH, NEVER A `## Status` FIELD. Every Status field is plan-level, so
|
|
1042
|
+
# one there could declare a single kind for a plan with several slices — and a
|
|
1043
|
+
# reviewer slice beside an implementer slice is the population this is for.
|
|
1044
|
+
#
|
|
1045
|
+
# The value runs to the closing marker, the way `builds:` does and unlike
|
|
1046
|
+
# `waits:`. A charter name is a bare word today, so both rules agree; this one
|
|
1047
|
+
# is chosen because it does not silently truncate if a name grows a space.
|
|
1048
|
+
#
|
|
1049
|
+
# IT CANNOT VALIDATE ITS VALUE, and that is the difference from `waits:` above.
|
|
1050
|
+
# A prerequisite has branch-prefix structure, which is what keeps a SYNTAX
|
|
1051
|
+
# EXAMPLE in prose from reading as a declaration; a charter name is a bare word
|
|
1052
|
+
# with no structure to check. What protects the templates instead is NESTING:
|
|
1053
|
+
# their documentation sits inside an outer `<!-- ... -->` block, so the inner
|
|
1054
|
+
# `agent:` carries no `<!--` of its own and this pattern never matches it —
|
|
1055
|
+
# the same accident of shape that already keeps `builds:` out of the parse of
|
|
1056
|
+
# a template. Documenting it as a bare `<!-- agent: reviewer -->` line would
|
|
1057
|
+
# hand a kind to every plan created from that template. A test pins it.
|
|
1058
|
+
#
|
|
1059
|
+
# `has_agent` carries presence separately from the value, as all three
|
|
1060
|
+
# annotations before it do: a slice naming no kind emits no key, so dispatch
|
|
1061
|
+
# reads a name or nothing and never a blank string that looks like one.
|
|
1062
|
+
agent_note = ""
|
|
1063
|
+
has_agent = 0
|
|
1064
|
+
if ($0 ~ /<!--[ \t]*agent:[ \t]*/) {
|
|
1065
|
+
_ag = $0
|
|
1066
|
+
sub(/^.*<!--[ \t]*agent:[ \t]*/, "", _ag)
|
|
1067
|
+
sub(/[ \t]*-->.*$/, "", _ag)
|
|
1068
|
+
agent_note = trim(_ag)
|
|
1069
|
+
if (agent_note != "") has_agent = 1
|
|
1070
|
+
}
|
|
1015
1071
|
# ONE LIST ITEM, AT MOST ONE CLAIM — an `if`, not the `while` this was.
|
|
1016
1072
|
#
|
|
1017
1073
|
# The old loop walked the line taking every backticked name on it, which is
|
|
@@ -1051,6 +1107,10 @@ section == "slices" && slice_shape != "heading" {
|
|
|
1051
1107
|
waits_set[n_branches] = has_waits
|
|
1052
1108
|
builds_of[n_branches] = builds_note
|
|
1053
1109
|
builds_set[n_branches] = has_builds
|
|
1110
|
+
# The kind travels with the branch, presence tracked separately so a slice
|
|
1111
|
+
# declaring none emits no key.
|
|
1112
|
+
agent_of[n_branches] = agent_note
|
|
1113
|
+
agent_set[n_branches] = has_agent
|
|
1054
1114
|
ordered_b[n_branches] = b
|
|
1055
1115
|
}
|
|
1056
1116
|
line = $0
|
|
@@ -1188,6 +1248,19 @@ section == "slices" && slice_shape == "heading" {
|
|
|
1188
1248
|
if (waits_note ~ "^(" PREFIXES ")/[^ \t]+$") has_waits = 1
|
|
1189
1249
|
else waits_note = ""
|
|
1190
1250
|
}
|
|
1251
|
+
# The agent kind, read exactly as the list-item spelling reads it. Both
|
|
1252
|
+
# dialects emit the same waves[], so a field added to one only would break that
|
|
1253
|
+
# contract the first time a plan migrated. See the list-item block for why the
|
|
1254
|
+
# value runs to the closing marker and why this one cannot validate itself.
|
|
1255
|
+
agent_note = ""
|
|
1256
|
+
has_agent = 0
|
|
1257
|
+
if ($0 ~ /<!--[ \t]*agent:[ \t]*/) {
|
|
1258
|
+
_ag = $0
|
|
1259
|
+
sub(/^.*<!--[ \t]*agent:[ \t]*/, "", _ag)
|
|
1260
|
+
sub(/[ \t]*-->.*$/, "", _ag)
|
|
1261
|
+
agent_note = trim(_ag)
|
|
1262
|
+
if (agent_note != "") has_agent = 1
|
|
1263
|
+
}
|
|
1191
1264
|
|
|
1192
1265
|
# The branch is the `Branch:` value, matched against the known prefixes exactly
|
|
1193
1266
|
# as the old shape matched the backticked name. Written unquoted in the heading
|
|
@@ -1211,6 +1284,8 @@ section == "slices" && slice_shape == "heading" {
|
|
|
1211
1284
|
waits_set[n_branches] = has_waits
|
|
1212
1285
|
builds_of[n_branches] = builds_note
|
|
1213
1286
|
builds_set[n_branches] = has_builds
|
|
1287
|
+
agent_of[n_branches] = agent_note
|
|
1288
|
+
agent_set[n_branches] = has_agent
|
|
1214
1289
|
ordered_b[n_branches] = b
|
|
1215
1290
|
}
|
|
1216
1291
|
|