@plot-pm/board 0.13.0 → 0.14.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 +120 -119
- package/package.json +2 -1
- package/plot-approve.sh +18 -0
- package/plot-config.sh +17 -0
- package/plot-deliver.sh +39 -0
- package/plot-dispatch.sh +344 -40
- package/plot-host.sh +734 -83
- package/plot-reap.sh +106 -4
- package/plot-state-receipt.sh +308 -0
- package/plot-worker-state.sh +201 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plot-pm/board",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Local Kanban board for Plot — a glanceable view of plan phases from docs/plans, with sprint and story filters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"plot-reap.sh",
|
|
39
39
|
"plot-release-refs.sh",
|
|
40
40
|
"plot-resolve-artifact.sh",
|
|
41
|
+
"plot-state-receipt.sh",
|
|
41
42
|
"plot-transcript-quiet.sh",
|
|
42
43
|
"plot-worker-monitor.sh",
|
|
43
44
|
"plot-worker-state.sh"
|
package/plot-approve.sh
CHANGED
|
@@ -99,6 +99,12 @@ set -uo pipefail
|
|
|
99
99
|
|
|
100
100
|
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
101
101
|
|
|
102
|
+
# The receipt this script leaves for plot-state-gate.sh, which refuses every
|
|
103
|
+
# other writer of a `State:` line. Sourced rather than run: the gate and the
|
|
104
|
+
# three owning scripts must agree on where a receipt lives, and one file is how.
|
|
105
|
+
# shellcheck source=plot-state-receipt.sh
|
|
106
|
+
. "$script_dir/plot-state-receipt.sh"
|
|
107
|
+
|
|
102
108
|
dry_run=0
|
|
103
109
|
who_override=""
|
|
104
110
|
slug=""
|
|
@@ -538,6 +544,10 @@ write_transition() { # $1=file $2=record $3=recorded(yes|no) → sets phase_repo
|
|
|
538
544
|
fi
|
|
539
545
|
|
|
540
546
|
phase_report=$([ "$flipped" = 1 ] && echo flipped || echo already)
|
|
547
|
+
# The receipt plot-state-gate.sh clears on. Recorded after the `mv`, so it
|
|
548
|
+
# names a value the file actually carries — a receipt written before a failed
|
|
549
|
+
# write would license a commit of the state that was refused.
|
|
550
|
+
record_state_receipt "$f" "Approved"
|
|
541
551
|
return 0
|
|
542
552
|
}
|
|
543
553
|
|
|
@@ -776,4 +786,12 @@ else
|
|
|
776
786
|
fi
|
|
777
787
|
|
|
778
788
|
echo "summary: merged=$merged_report phase=$phase_report record=$record_report holds=$holds_report sprint=$sprint_report push=$push_report"
|
|
789
|
+
# THE RECEIPT IS SPENT HERE, on the action COMPLETING — never at the gate.
|
|
790
|
+
# `plot-controller-gate.sh` clears on a receipt and LEAVES it, so an
|
|
791
|
+
# interrupted run can be repeated on the same licence: this script documents
|
|
792
|
+
# re-running as the repair for any interruption after its irreversible step,
|
|
793
|
+
# and a receipt spent at the gate would refuse that repair in the case it is
|
|
794
|
+
# most needed. One authorisation, one completed action.
|
|
795
|
+
spend_action_receipt "plot-approve.sh"
|
|
796
|
+
|
|
779
797
|
exit 0
|
package/plot-config.sh
CHANGED
|
@@ -95,6 +95,23 @@
|
|
|
95
95
|
# Hosts plans yes | no (no = refuse plan files)
|
|
96
96
|
# Tracker plot | jira | github-issues | linear (+ URL)
|
|
97
97
|
# (plot = plans in this repo ARE the tracker; absent = same)
|
|
98
|
+
# Ticket prefixes the tracker project keys this repository's work lives
|
|
99
|
+
# in, comma-separated (`PROJ-A, PROJ-B`). NOT
|
|
100
|
+
# `Branch prefixes`, which sits next to it and holds
|
|
101
|
+
# `idea/`, `feature/`, `bug/` — that key is structural and
|
|
102
|
+
# names git branches; this one names tracker projects.
|
|
103
|
+
# Read by plot-host.sh's `issue-list` to scope the inbox:
|
|
104
|
+
# the default JQL scopes by person and state, so on a
|
|
105
|
+
# shared instance it is instance-wide and returns other
|
|
106
|
+
# customers' tickets. A LIST, because a repository mapping
|
|
107
|
+
# to several projects is the normal case; adoption seeds
|
|
108
|
+
# ONE prefix and a person adds the rest. Absent = today's
|
|
109
|
+
# unscoped query, byte for byte — a default that filtered
|
|
110
|
+
# on an undeclared key would empty every existing board's
|
|
111
|
+
# inbox on upgrade. `PLOT_JIRA_JQL` overrides both.
|
|
112
|
+
# NEVER WRITTEN EMPTY: an empty list reads as *this
|
|
113
|
+
# repository has no projects* and changes nothing about
|
|
114
|
+
# the query, so adoption omits the key instead.
|
|
98
115
|
# Git host github | bitbucket (resolves gh vs bb)
|
|
99
116
|
# CI jenkins | github-actions | none — which CI system this
|
|
100
117
|
# project uses. Recorded by /plot-board-setup; not yet
|
package/plot-deliver.sh
CHANGED
|
@@ -49,6 +49,12 @@ set -uo pipefail
|
|
|
49
49
|
|
|
50
50
|
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
51
51
|
|
|
52
|
+
# The receipt this script leaves for plot-state-gate.sh, which refuses every
|
|
53
|
+
# other writer of a `State:` line. Sourced rather than run: the gate and the
|
|
54
|
+
# three owning scripts must agree on where a receipt lives, and one file is how.
|
|
55
|
+
# shellcheck source=plot-state-receipt.sh
|
|
56
|
+
. "$script_dir/plot-state-receipt.sh"
|
|
57
|
+
|
|
52
58
|
dry_run=0
|
|
53
59
|
who_override=""
|
|
54
60
|
slug=""
|
|
@@ -172,6 +178,27 @@ if [ "$(vfield '.deliverable')" != "true" ]; then
|
|
|
172
178
|
die "$(vfield '.refusal')"
|
|
173
179
|
fi
|
|
174
180
|
|
|
181
|
+
# THE FINDING — a slice whose merged PR carried no implementation.
|
|
182
|
+
#
|
|
183
|
+
# IT REPORTS AND DOES NOT REFUSE, which is the harder call and the right one.
|
|
184
|
+
# Measured over the last 60 merged PRs on 2026-09-08, seven carried no work and
|
|
185
|
+
# only TWO were this defect: three were claim PRs whose slice finished under a
|
|
186
|
+
# DIFFERENT PR, and a gate refusing on those would have blocked a delivery whose
|
|
187
|
+
# work was complete — right about the PR and wrong about the plan.
|
|
188
|
+
#
|
|
189
|
+
# So it is worded as a QUESTION and names the next move, because a reader told
|
|
190
|
+
# only "this carried nothing" still has the decision to make.
|
|
191
|
+
empty_slices=$(printf '%s' "$verdict" | jq -r '.emptySlices // [] | .[]' 2>/dev/null)
|
|
192
|
+
if [ -n "$empty_slices" ]; then
|
|
193
|
+
echo "note: this plan's merged PR carried no implementation on:"
|
|
194
|
+
while IFS= read -r b; do
|
|
195
|
+
[ -n "$b" ] || continue
|
|
196
|
+
echo " - $b"
|
|
197
|
+
done <<< "$empty_slices"
|
|
198
|
+
echo " check whether its work landed under another PR, mark it deferred"
|
|
199
|
+
echo " (<!-- deferred: <reason> -->), or re-open it. Delivery continues."
|
|
200
|
+
fi
|
|
201
|
+
|
|
175
202
|
merged_count=$(vfield '.merged')
|
|
176
203
|
deferred_count=$(vfield '.deferred')
|
|
177
204
|
# Empty rather than `0`, so the suffix below stays absent where the old block
|
|
@@ -417,6 +444,10 @@ write_transition() { # $1=file $2=record $3=recorded(yes|no) → sets phase_repo
|
|
|
417
444
|
fi
|
|
418
445
|
|
|
419
446
|
phase_report=$([ "$flipped" = 1 ] && echo flipped || echo already)
|
|
447
|
+
# The receipt plot-state-gate.sh clears on. Recorded after the `mv`, so it
|
|
448
|
+
# names a value the file actually carries — a receipt written before a failed
|
|
449
|
+
# write would license a commit of the state that was refused.
|
|
450
|
+
record_state_receipt "$f" "Delivered"
|
|
420
451
|
return 0
|
|
421
452
|
}
|
|
422
453
|
|
|
@@ -598,4 +629,12 @@ else
|
|
|
598
629
|
fi
|
|
599
630
|
|
|
600
631
|
echo "summary: phase=$phase_report record=$record_report index=$index_report sprint=$sprint_report push=$push_report"
|
|
632
|
+
# THE RECEIPT IS SPENT HERE, on the action COMPLETING — never at the gate.
|
|
633
|
+
# `plot-controller-gate.sh` clears on a receipt and LEAVES it, so an
|
|
634
|
+
# interrupted run can be repeated on the same licence: this script documents
|
|
635
|
+
# re-running as the repair for any interruption after its irreversible step,
|
|
636
|
+
# and a receipt spent at the gate would refuse that repair in the case it is
|
|
637
|
+
# most needed. One authorisation, one completed action.
|
|
638
|
+
spend_action_receipt "plot-deliver.sh"
|
|
639
|
+
|
|
601
640
|
exit 0
|
package/plot-dispatch.sh
CHANGED
|
@@ -150,6 +150,12 @@ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
|
150
150
|
# shellcheck source=plot-worker-state.sh
|
|
151
151
|
. "$script_dir/plot-worker-state.sh"
|
|
152
152
|
|
|
153
|
+
# The controller receipt, for `spend_action_receipt` below. Sourced from the
|
|
154
|
+
# ONE file that holds both receipt kinds, for the reason that file states: the
|
|
155
|
+
# gate and the owners must agree on where a receipt lives.
|
|
156
|
+
# shellcheck source=plot-state-receipt.sh
|
|
157
|
+
. "$script_dir/plot-state-receipt.sh"
|
|
158
|
+
|
|
153
159
|
# The ONE answer to "did the host merge ANY PR for this branch?" — `pr_merged`,
|
|
154
160
|
# read by `held_worktree` rather than derived from ancestry. Sourced for the
|
|
155
161
|
# same reason `plot-reap.sh` and `plot-release-refs.sh` source it: three callers
|
|
@@ -275,12 +281,20 @@ while [ $# -gt 0 ]; do
|
|
|
275
281
|
shift
|
|
276
282
|
done
|
|
277
283
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
+
# THE TWO PRECONDITIONS OF A DISPATCH, and a sourcing test has neither.
|
|
285
|
+
# `PLOT_DISPATCH_SOURCED=1` is taking the definitions below rather than running
|
|
286
|
+
# a dispatch, so it has no slug and often no git repository; the guard that
|
|
287
|
+
# stops it sits further down, after the function it exists to expose, and these
|
|
288
|
+
# two would exit before it is reached. See that guard for why it cannot simply
|
|
289
|
+
# move up here.
|
|
290
|
+
if [ -z "${PLOT_DISPATCH_SOURCED:-}" ]; then
|
|
291
|
+
git rev-parse --git-dir >/dev/null 2>&1 || { echo "not a git repository" >&2; exit 1; }
|
|
292
|
+
[ -n "$slug" ] || [ "$mode" != dispatch ] || {
|
|
293
|
+
echo "plot-dispatch: need a plan slug (usage: plot-dispatch.sh [--dry-run] <slug>)" >&2
|
|
294
|
+
echo " Which plans could be dispatched: /plot-pulse" >&2
|
|
295
|
+
exit 1
|
|
296
|
+
}
|
|
297
|
+
fi
|
|
284
298
|
|
|
285
299
|
# ---------------------------------------------------------------------------
|
|
286
300
|
# Worker launch, and the identity it records
|
|
@@ -737,9 +751,207 @@ brief_staleness_note() { # $1 = branch → prints a hint, or nothing
|
|
|
737
751
|
echo " plan commit ${pc%% *} touched $plan_path after the brief's last change; read it before trusting the brief"
|
|
738
752
|
}
|
|
739
753
|
|
|
754
|
+
# ---------------------------------------------------------------------------
|
|
755
|
+
# WHAT THIS AGENT RUNS
|
|
756
|
+
# ---------------------------------------------------------------------------
|
|
757
|
+
#
|
|
758
|
+
# THE HARNESS IS RESOLVED, NOT ASSUMED. `Worker command` is one key and every
|
|
759
|
+
# dispatched agent got the identical command line, so a fleet could not hold a
|
|
760
|
+
# reviewer on one model beside an implementer on another. A charter
|
|
761
|
+
# (`.plot/charters/<name>.json`) names its own `harness`, `model` and `effort`,
|
|
762
|
+
# and `plot-prompt.mjs --launch` decides which apply.
|
|
763
|
+
#
|
|
764
|
+
# A CHARTER NAMES A HARNESS; IT NEVER CARRIES A COMMAND LINE. Plot exports the
|
|
765
|
+
# three names and the PROMPT FILE holds the invocation — the contract
|
|
766
|
+
# `.plot/worker-prompt.sh` already states: *"Plot exports the variables and
|
|
767
|
+
# cannot write the invocation."* So this sets three variables and never builds
|
|
768
|
+
# a command out of them.
|
|
769
|
+
#
|
|
770
|
+
# NOTHING ON THE ESTATE CHANGES UNTIL A CHARTER EXISTS. `$PLOT_AGENT` unset —
|
|
771
|
+
# which is every worker today, since the estate holds zero charters — exports
|
|
772
|
+
# none of the three, and the command line is byte-identical to what it was.
|
|
773
|
+
# So does a named agent with no charter file on this clone.
|
|
774
|
+
#
|
|
775
|
+
# A REFUSAL IS NOT A FALLBACK, for `resolve_prompt_file`'s reason one field
|
|
776
|
+
# over: a charter that cannot be believed would otherwise RUN, successfully,
|
|
777
|
+
# under an invocation the operator did not ask for, and nothing in
|
|
778
|
+
# `.plot-worker.log` would say so.
|
|
779
|
+
#
|
|
780
|
+
# A HARNESS THIS MACHINE CANNOT RUN IS THE SECOND REFUSAL, and it is a MACHINE
|
|
781
|
+
# fact rather than a charter one. `plot-prompt.mjs` answers what a charter
|
|
782
|
+
# DECLARES and reaches no machine; whether the name is on PATH is the adapter's
|
|
783
|
+
# question, asked here, in the shell that is about to spawn — beside the `node`
|
|
784
|
+
# and `launchd` probes `plot-fleetctl.sh` performs. Without it a typo in a hand
|
|
785
|
+
# written field (`agnet` for `agent`) exports a name the prompt file then
|
|
786
|
+
# ignores, and the work is done by the repo default with nothing saying so.
|
|
787
|
+
#
|
|
788
|
+
# THE BUNDLE MISSING IS ALSO NOT A REFUSAL. `plot-prompt.mjs` is vendored beside
|
|
789
|
+
# this script, and a checkout without it is a Plot installation problem rather
|
|
790
|
+
# than a statement about this agent — so it falls back and SAYS it could not
|
|
791
|
+
# ask, the shape `plot-dispatch.sh:1502` already uses for an unaskable rule.
|
|
792
|
+
#
|
|
793
|
+
# It sets `launch_harness`, `launch_model`, `launch_effort` and `launch_agent`,
|
|
794
|
+
# and returns 1 on a refusal; the caller decides what a refusal costs.
|
|
795
|
+
resolve_launch() { # $1 = repo root, $2 = agent name ('' when none)
|
|
796
|
+
local root="$1" agent="$2" resolution="" status=0 verb rest why
|
|
797
|
+
launch_harness=""
|
|
798
|
+
launch_model=""
|
|
799
|
+
launch_effort=""
|
|
800
|
+
launch_agent=""
|
|
801
|
+
|
|
802
|
+
if [ -f "$script_dir/board/plot-prompt.mjs" ]; then
|
|
803
|
+
resolution=$(node "$script_dir/board/plot-prompt.mjs" --launch "$root" "$agent" 2>/dev/null)
|
|
804
|
+
status=$?
|
|
805
|
+
else
|
|
806
|
+
echo "plot-dispatch: no plot-prompt.mjs beside this script — starting ${agent:-this agent} on the repo's 'Worker command' without asking what it declared" >&2
|
|
807
|
+
return 0
|
|
808
|
+
fi
|
|
809
|
+
|
|
810
|
+
verb=${resolution%%$'\t'*}
|
|
811
|
+
rest=${resolution#*$'\t'}
|
|
812
|
+
launch_harness=${rest%%$'\t'*}
|
|
813
|
+
rest=${rest#*$'\t'}
|
|
814
|
+
launch_model=${rest%%$'\t'*}
|
|
815
|
+
rest=${rest#*$'\t'}
|
|
816
|
+
launch_effort=${rest%%$'\t'*}
|
|
817
|
+
why=${rest#*$'\t'}
|
|
818
|
+
|
|
819
|
+
if [ "$status" -eq 3 ] || [ "$verb" = "refused" ]; then
|
|
820
|
+
launch_harness=""
|
|
821
|
+
launch_model=""
|
|
822
|
+
launch_effort=""
|
|
823
|
+
launch_why="$why"
|
|
824
|
+
return 1
|
|
825
|
+
fi
|
|
826
|
+
|
|
827
|
+
case "$verb" in
|
|
828
|
+
declared)
|
|
829
|
+
launch_agent="$why"
|
|
830
|
+
# A HARNESS THIS MACHINE CANNOT RUN REFUSES. `command -v` is the reading
|
|
831
|
+
# because it is the question the prompt file asks when it interpolates the
|
|
832
|
+
# name: a check that asked anything else would refuse launches that work
|
|
833
|
+
# and pass launches that will not. It sits on this arm because only
|
|
834
|
+
# `declared` carries a harness the launch exports — the refusal above and
|
|
835
|
+
# the `*)` arm below have both already blanked the field.
|
|
836
|
+
#
|
|
837
|
+
# AN UNNAMED HARNESS IS NOT AN UNRUNNABLE ONE. A charter naming none
|
|
838
|
+
# resolves to '' and launches exactly as it does today, which is every
|
|
839
|
+
# dispatch on the estate; the guard on a non-empty name is what keeps it
|
|
840
|
+
# that way.
|
|
841
|
+
if [ -n "$launch_harness" ] && ! command -v "$launch_harness" >/dev/null 2>&1; then
|
|
842
|
+
# The name is read BEFORE the fields are blanked: the refusal's whole
|
|
843
|
+
# job is to name what it looked for, and clearing first loses it.
|
|
844
|
+
why="charter '$launch_agent' names harness '$launch_harness', which is not on PATH"
|
|
845
|
+
launch_harness=""
|
|
846
|
+
launch_model=""
|
|
847
|
+
launch_effort=""
|
|
848
|
+
launch_why="$why"
|
|
849
|
+
return 1
|
|
850
|
+
fi
|
|
851
|
+
;;
|
|
852
|
+
*)
|
|
853
|
+
# A fallback, an unrecognised verb, or an empty answer from a bundle that
|
|
854
|
+
# could not run. Three empty names, which is what every worker exports
|
|
855
|
+
# today.
|
|
856
|
+
launch_harness=""
|
|
857
|
+
launch_model=""
|
|
858
|
+
launch_effort=""
|
|
859
|
+
;;
|
|
860
|
+
esac
|
|
861
|
+
return 0
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
# WHETHER A DESK MAY BE HANDED TO A NEW WORKER — the ONE gate, two callers.
|
|
865
|
+
#
|
|
866
|
+
# EXTRACTED RATHER THAN COPIED, and that is the point. `--restart` asked these
|
|
867
|
+
# three questions inline; `--absent` asks the same three over every orphaned
|
|
868
|
+
# desk. A second, laxer set written for the sweep is the failure
|
|
869
|
+
# `plot-dispatch --stop` already argues against for stop rules, and it matters
|
|
870
|
+
# more here because the sweep runs with nobody watching.
|
|
871
|
+
#
|
|
872
|
+
# IT RETURNS RATHER THAN EXITS, which is the only change the extraction makes.
|
|
873
|
+
# A verb acting on one branch exits on a refusal; a sweep over several must
|
|
874
|
+
# refuse one desk and carry on to the next. The caller decides which.
|
|
875
|
+
#
|
|
876
|
+
# THE ORDER IS LOAD-BEARING AND IT IS THE MEASURED ONE:
|
|
877
|
+
#
|
|
878
|
+
# 1. THE PR, BEFORE THE STATE WORD. Five of five `failed` worktrees in this
|
|
879
|
+
# estate held a PR — four open, one already merged. `plot-worker-state.sh`
|
|
880
|
+
# refines `finished` by the tree but deliberately does NOT refine `failed`,
|
|
881
|
+
# `ended` or `none`, because a recorded non-zero exit is already a specific
|
|
882
|
+
# answer about the PROCESS — and silent about the WORK. A gate written on
|
|
883
|
+
# the state word alone would have restarted all five and discarded exactly
|
|
884
|
+
# what the `finished` refusal exists to protect.
|
|
885
|
+
# 2. A LIVE WORKER. There is no --force: a flag overriding this is the flag
|
|
886
|
+
# typed reflexively, and what it would override is another agent's work in
|
|
887
|
+
# progress.
|
|
888
|
+
# 3. A `PLOT-BLOCKED` MARKER. A person owes this branch an answer, and a new
|
|
889
|
+
# worker meets the same question and writes the same marker. Asked through
|
|
890
|
+
# `plot_worker_blocked_file`, never re-globbed: the marker's spelling lives
|
|
891
|
+
# with the classification in `plot-worker-state.sh` and only there.
|
|
892
|
+
#
|
|
893
|
+
# `stalled`, `failed`, `ended` and `no worker` all pass. The PR question is what
|
|
894
|
+
# makes `failed` safe to include — and including it is the point: a gate that
|
|
895
|
+
# simply refused `failed` would pass every refusal test and leave the verb
|
|
896
|
+
# unable to do the one thing it exists for.
|
|
897
|
+
handover_refusal() { # $1=branch $2=worktree $3=state → 0 may hand over, 1 refused
|
|
898
|
+
local branch="$1" wt="$2" state="$3" pr_json pr_num pr_state marker
|
|
899
|
+
|
|
900
|
+
if reached_review "$branch"; then
|
|
901
|
+
pr_json=$("$script_dir/plot-host.sh" pr-state "$branch" </dev/null 2>/dev/null || true)
|
|
902
|
+
pr_num=$(printf '%s' "$pr_json" | sed -n 's/.*"number":\([0-9]*\).*/\1/p')
|
|
903
|
+
pr_state=$(printf '%s' "$pr_json" | sed -n 's/.*"state":"\([A-Z]*\)".*/\1/p')
|
|
904
|
+
echo "plot-dispatch: $branch has a pull request (#${pr_num:-?}, ${pr_state:-OPEN}) — refusing." >&2
|
|
905
|
+
echo " The work reached review, whatever the worker's exit code says. A" >&2
|
|
906
|
+
echo " restart here redoes work someone is already looking at." >&2
|
|
907
|
+
echo " Review it, or reap the worktree once it merges." >&2
|
|
908
|
+
return 1
|
|
909
|
+
fi
|
|
910
|
+
|
|
911
|
+
case "$state" in
|
|
912
|
+
running*)
|
|
913
|
+
echo "plot-dispatch: a worker is alive on $branch (pid ${state#running }) — refusing." >&2
|
|
914
|
+
echo " Stop it first if you mean to replace it:" >&2
|
|
915
|
+
echo " plot-dispatch.sh --stop $branch" >&2
|
|
916
|
+
return 1
|
|
917
|
+
;;
|
|
918
|
+
waiting*)
|
|
919
|
+
marker=$(plot_worker_blocked_file "$wt" || true)
|
|
920
|
+
echo "plot-dispatch: $branch is blocked on a question — refusing." >&2
|
|
921
|
+
echo " the question is in $wt/${marker:-the marker file}" >&2
|
|
922
|
+
echo " Answer it and delete the marker, then restart." >&2
|
|
923
|
+
return 1
|
|
924
|
+
;;
|
|
925
|
+
esac
|
|
926
|
+
return 0
|
|
927
|
+
}
|
|
928
|
+
|
|
740
929
|
start_worker() {
|
|
741
930
|
local branch="$1" wt="$2"
|
|
742
931
|
local cmd
|
|
932
|
+
|
|
933
|
+
# RESOLVED BEFORE `Worker command`, because the charter is the more specific
|
|
934
|
+
# answer and the config key is the fallback rather than the sole source.
|
|
935
|
+
#
|
|
936
|
+
# THE REFUSAL FIRES HERE, WHICH IS BEFORE ANYTHING THIS FUNCTION WRITES. The
|
|
937
|
+
# plan asks for a refusal "before the desk is touched"; on this codebase the
|
|
938
|
+
# desk already exists by the time `start_worker` is called — both call sites
|
|
939
|
+
# (`--restart`, and `--start`'s `git worktree add`) hand it a worktree path.
|
|
940
|
+
# What this position does guarantee is that NO WORKER IS LAUNCHED, no manifest
|
|
941
|
+
# is written and no `.plot-worker.exit` is removed, and `start_worker` pushes
|
|
942
|
+
# no claim at all — so the slice stays claimable by an agent that can run it,
|
|
943
|
+
# which is the property the plan's sentence is protecting. A desk with no
|
|
944
|
+
# worker is what `--start` already creates for a free agent, and the reaper
|
|
945
|
+
# handles it. See the PR body.
|
|
946
|
+
if ! resolve_launch "$repo_root" "${PLOT_AGENT:-}"; then
|
|
947
|
+
echo " refusing to start ${branch:-a free agent} — $launch_why" >&2
|
|
948
|
+
echo " A charter that cannot be read is a person's typo, and the repo's 'Worker command'" >&2
|
|
949
|
+
echo " would run successfully under an invocation nobody asked for." >&2
|
|
950
|
+
echo " Fix $repo_root/.plot/charters/${PLOT_AGENT:-?}.json, or unset PLOT_AGENT to start it" >&2
|
|
951
|
+
echo " on the repo's configured command deliberately." >&2
|
|
952
|
+
return 1
|
|
953
|
+
fi
|
|
954
|
+
|
|
743
955
|
cmd=$("$script_dir/plot-config.sh" get "Worker command" "")
|
|
744
956
|
# `none` means "asked, and this repo starts them by hand". Running it would
|
|
745
957
|
# spawn a worker per branch that fails with `none: command not found` — a
|
|
@@ -997,6 +1209,67 @@ start_worker() {
|
|
|
997
1209
|
# A HAND-MADE WORKTREE GETS NEITHER, and that falls out rather than being
|
|
998
1210
|
# enforced: this is the only code that starts a wrapper, and a worktree with
|
|
999
1211
|
# no wrapper has nothing for a monitor to be a child of.
|
|
1212
|
+
# WHAT THIS AGENT MAY TOUCH — `PLOT_CAPABILITIES`, AND WHY IT IS RESOLVED HERE.
|
|
1213
|
+
#
|
|
1214
|
+
# A charter's `capabilities` list is the one differentiation Plot can express
|
|
1215
|
+
# that is not a rule. Everything else an agent is told is PROSE — text it reads
|
|
1216
|
+
# and can reason past — and CLAUDE.md's own test settles what that makes it:
|
|
1217
|
+
# *can you answer "did I complete this?" without doing the work?* An agent
|
|
1218
|
+
# asked in prose not to edit the code it reviews can answer yes without it
|
|
1219
|
+
# being true. A tool the harness never offers cannot be called.
|
|
1220
|
+
#
|
|
1221
|
+
# RESOLUTION, NEVER MATCHING, the rule `plot-worker-loop.sh:916` states for the
|
|
1222
|
+
# prompt. `$PLOT_AGENT` is what the operator or the registry set; nothing here
|
|
1223
|
+
# reads a plan, ranks a candidate or chooses an agent. This slice BOUNDS an
|
|
1224
|
+
# agent and does not route to one — `matchQueue` is the assignment lock and is
|
|
1225
|
+
# untouched.
|
|
1226
|
+
#
|
|
1227
|
+
# THE LIST TRAVELS, THE SPELLING DOES NOT. Plot carries capability NAMES and
|
|
1228
|
+
# has no opinion about what they mean: Principle 5, *"Plot contains zero
|
|
1229
|
+
# hardcoded project names, paths, or configuration"*. Different harnesses spell
|
|
1230
|
+
# a tool restriction differently, so the prompt file turns a name into a flag —
|
|
1231
|
+
# the same division the prompt itself draws. There is no capability enum here,
|
|
1232
|
+
# no built-in `read-only`, and no tool table in the domain.
|
|
1233
|
+
#
|
|
1234
|
+
# ASKED OF THE BUNDLE, NOT OF THE FILE. `plot-prompt.mjs` already reads this
|
|
1235
|
+
# charter through `readCharter`, which refuses an unknown key, a run fact and
|
|
1236
|
+
# bytes that are not JSON. A `grep` for the field would happily read
|
|
1237
|
+
# capabilities out of a charter the domain refuses outright.
|
|
1238
|
+
#
|
|
1239
|
+
# AN EXIT CODE, NOT AN EMPTINESS. Three readings print nothing on stdout and
|
|
1240
|
+
# only one of them is a fault: no agent named, and a named agent with no
|
|
1241
|
+
# charter on this clone, are the estate today; a charter that EXISTS and cannot
|
|
1242
|
+
# be believed is a person's typo, and exits 3. Launching unbounded on it would
|
|
1243
|
+
# run — successfully — under a scope nobody asked for.
|
|
1244
|
+
#
|
|
1245
|
+
# NOTHING IS EXPORTED WHERE NOTHING WAS DECLARED. An agent with no charter, or
|
|
1246
|
+
# one naming no capabilities, exports no `PLOT_CAPABILITIES` at all — not an
|
|
1247
|
+
# empty string. A prompt file probes `[ -n "$PLOT_CAPABILITIES" ]`, and a
|
|
1248
|
+
# variable that is always set makes that probe meaningless.
|
|
1249
|
+
#
|
|
1250
|
+
# THE BUNDLE MISSING IS NOT A REFUSAL, `resolve_prompt_file`'s fourth arm: a
|
|
1251
|
+
# checkout without it is a Plot installation problem rather than a statement
|
|
1252
|
+
# about this agent. It says it could not ask and launches unbounded, which is
|
|
1253
|
+
# what it did before this existed.
|
|
1254
|
+
local capabilities='' cap_status=0
|
|
1255
|
+
if [ -n "${PLOT_AGENT:-}" ]; then
|
|
1256
|
+
if [ -f "$script_dir/board/plot-prompt.mjs" ]; then
|
|
1257
|
+
capabilities=$(node "$script_dir/board/plot-prompt.mjs" --capabilities "$repo_root" "$PLOT_AGENT" 2>/dev/null)
|
|
1258
|
+
cap_status=$?
|
|
1259
|
+
if [ "$cap_status" -eq 3 ]; then
|
|
1260
|
+
echo " refusing to start $branch — the charter for agent '$PLOT_AGENT' cannot be read:"
|
|
1261
|
+
node "$script_dir/board/plot-prompt.mjs" --capabilities "$repo_root" "$PLOT_AGENT" 2>&1 >/dev/null \
|
|
1262
|
+
| sed 's/^/ /'
|
|
1263
|
+
echo " A charter that cannot be believed must not launch an agent, because the"
|
|
1264
|
+
echo " launch would succeed under a scope nobody asked for. Fix"
|
|
1265
|
+
echo " $repo_root/.plot/charters/$PLOT_AGENT.json or unset PLOT_AGENT."
|
|
1266
|
+
return 1
|
|
1267
|
+
fi
|
|
1268
|
+
else
|
|
1269
|
+
echo " no plot-prompt.mjs beside this script — starting $branch without asking what agent '$PLOT_AGENT' may touch"
|
|
1270
|
+
fi
|
|
1271
|
+
fi
|
|
1272
|
+
|
|
1000
1273
|
local worker_monitor='' agent_monitor='' build_monitor=''
|
|
1001
1274
|
[ -x "$script_dir/plot-worker-monitor.sh" ] && worker_monitor="$script_dir/plot-worker-monitor.sh"
|
|
1002
1275
|
[ -x "$script_dir/plot-agent-monitor.sh" ] && agent_monitor="$script_dir/plot-agent-monitor.sh"
|
|
@@ -1008,9 +1281,38 @@ start_worker() {
|
|
|
1008
1281
|
[ -x "$script_dir/plot-build-monitor.sh" ] && build_monitor="$script_dir/plot-build-monitor.sh"
|
|
1009
1282
|
local stamp_now
|
|
1010
1283
|
stamp_now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
1011
|
-
|
|
1284
|
+
# THE THREE NAMES THE CHARTER DECLARED, and they travel as env vars for the
|
|
1285
|
+
# reason every other path here does: the `sh -c` body is single-quoted, so a
|
|
1286
|
+
# value with spaces would not survive interpolation into it.
|
|
1287
|
+
#
|
|
1288
|
+
# EMPTY IS THE ESTATE TODAY and must stay indistinguishable from before. An
|
|
1289
|
+
# agent with no charter exports three empty strings, a prompt file
|
|
1290
|
+
# interpolating `${PLOT_MODEL:-}` gets nothing, and the command line is
|
|
1291
|
+
# byte-identical to what it was — which is the 100% case, since zero charters
|
|
1292
|
+
# exist. `PLOT_AGENT` is forwarded too, so the loop's own `resolve_prompt_file`
|
|
1293
|
+
# asks about the same agent this launch resolved.
|
|
1294
|
+
( cd "$wt" && \
|
|
1295
|
+
# AN `export`, NOT AN ENV PREFIX, AND THE REASON IS A MEASUREMENT. Bash
|
|
1296
|
+
# recognises an assignment prefix BEFORE it expands parameters, so a
|
|
1297
|
+
# `${caps:+PLOT_CAPABILITIES="$caps"}` in the prefix below is not an
|
|
1298
|
+
# assignment at all — it expands to a WORD, and bash then looks for a
|
|
1299
|
+
# command by that name. Measured 2026-09-12: the empty case passed and
|
|
1300
|
+
# every non-empty one failed with `PLOT_CAPABILITIES=read-only: command
|
|
1301
|
+
# not found`, which would have broken every dispatch that had a charter
|
|
1302
|
+
# while the case with no charter went on working.
|
|
1303
|
+
#
|
|
1304
|
+
# The conditional is what keeps an undeclared agent's variable UNSET
|
|
1305
|
+
# rather than empty, so a prompt file's `[ -n "$PLOT_CAPABILITIES" ]`
|
|
1306
|
+
# probe means what it says. This subshell is already the launch's own, so
|
|
1307
|
+
# the export reaches the wrapper and nothing else.
|
|
1308
|
+
{ [ -n "$capabilities" ] && export PLOT_CAPABILITIES="$capabilities"; true; } && \
|
|
1309
|
+
PLOT_BRANCH="$branch" PLOT_WORKTREE="$wt" \
|
|
1012
1310
|
PLOT_SLUG="$slug" \
|
|
1013
1311
|
PLOT_SESSION_ID="$session" \
|
|
1312
|
+
PLOT_AGENT="${PLOT_AGENT:-}" \
|
|
1313
|
+
PLOT_HARNESS="$launch_harness" \
|
|
1314
|
+
PLOT_MODEL="$launch_model" \
|
|
1315
|
+
PLOT_EFFORT="$launch_effort" \
|
|
1014
1316
|
PLOT_MANIFEST_FILE="$manifest_dir/$session.json" \
|
|
1015
1317
|
PLOT_STAMP_STARTED="$stamp_now" \
|
|
1016
1318
|
PLOT_WORKER_MONITOR="$worker_monitor" \
|
|
@@ -1057,6 +1359,30 @@ start_worker() {
|
|
|
1057
1359
|
return 0
|
|
1058
1360
|
}
|
|
1059
1361
|
|
|
1362
|
+
# `PLOT_DISPATCH_SOURCED=1` STOPS HERE, so a test can take `resolve_launch` and
|
|
1363
|
+
# `start_worker` without dispatching anything — `plot-worker-loop.sh` states
|
|
1364
|
+
# this idiom for `resolve_prompt_file`, and this is the same one applied to the
|
|
1365
|
+
# functions that answer the same question one script over.
|
|
1366
|
+
#
|
|
1367
|
+
# AFTER BOTH DEFINITIONS IT EXISTS TO EXPOSE, and that placement is the whole
|
|
1368
|
+
# subtlety. This file is not `plot-worker-loop.sh`, where every definition
|
|
1369
|
+
# precedes every executing line: here the argument parsing runs at the TOP and
|
|
1370
|
+
# the functions are defined below it. Measured while writing this: a guard at
|
|
1371
|
+
# the top of the file returned before `resolve_launch` existed
|
|
1372
|
+
# (`resolve_launch: command not found`), and a guard between the two functions
|
|
1373
|
+
# hid `start_worker` the same way — which is the one a test must call to prove
|
|
1374
|
+
# a refusal touches no desk.
|
|
1375
|
+
#
|
|
1376
|
+
# `script_dir` IS ALREADY RESOLVED at this point, which is what a sourcing test
|
|
1377
|
+
# needs and what slicing the file cannot give it: `script_dir` is derived from
|
|
1378
|
+
# `BASH_SOURCE`, so a copy written to /tmp resolves every helper to /tmp.
|
|
1379
|
+
#
|
|
1380
|
+
# THE FLAG IS OPT-IN AND NAMED FOR THIS FILE. An unset variable leaves the
|
|
1381
|
+
# script exactly as it was — no caller changes, and a dispatched worker cannot
|
|
1382
|
+
# reach this return by accident. `return` rather than `exit` because a sourced
|
|
1383
|
+
# script returns to its sourcer.
|
|
1384
|
+
[ -n "${PLOT_DISPATCH_SOURCED:-}" ] && return 0
|
|
1385
|
+
|
|
1060
1386
|
# ---------------------------------------------------------------------------
|
|
1061
1387
|
# Inspection and shutdown
|
|
1062
1388
|
# ---------------------------------------------------------------------------
|
|
@@ -1272,44 +1598,12 @@ if [ "$mode" = "restart" ]; then
|
|
|
1272
1598
|
# Same lesson plot-reap.sh learned from the other side: it reads `mergedAt`
|
|
1273
1599
|
# and never `state`, because a merged PR reports CLOSED. There the state word
|
|
1274
1600
|
# lies about merging; here the exit code lies about completion.
|
|
1275
|
-
if reached_review "$restart_branch"; then
|
|
1276
|
-
pr_json=$("$script_dir/plot-host.sh" pr-state "$restart_branch" </dev/null 2>/dev/null || true)
|
|
1277
|
-
pr_num=$(printf '%s' "$pr_json" | sed -n 's/.*"number":\([0-9]*\).*/\1/p')
|
|
1278
|
-
pr_state=$(printf '%s' "$pr_json" | sed -n 's/.*"state":"\([A-Z]*\)".*/\1/p')
|
|
1279
|
-
echo "plot-dispatch: $restart_branch has a pull request (#${pr_num:-?}, ${pr_state:-OPEN}) — refusing." >&2
|
|
1280
|
-
echo " The work reached review, whatever the worker's exit code says. A" >&2
|
|
1281
|
-
echo " restart here redoes work someone is already looking at." >&2
|
|
1282
|
-
echo " Review it, or reap the worktree once it merges." >&2
|
|
1283
|
-
exit 1
|
|
1284
|
-
fi
|
|
1285
1601
|
|
|
1286
1602
|
# Only now the process. `worker_state` is the ONE answer to "is a worker
|
|
1287
1603
|
# running here" — asked rather than re-derived, so this cannot drift from
|
|
1288
1604
|
# `--status` and the scan the way five of six states already did once.
|
|
1289
1605
|
restart_state=$(worker_state "$restart_wt" "$restart_branch")
|
|
1290
|
-
|
|
1291
|
-
running*)
|
|
1292
|
-
# THE REFUSAL THAT PREVENTS TWO WORKERS ON ONE BRANCH. There is no
|
|
1293
|
-
# --force: a flag overriding this is the flag typed reflexively, and what
|
|
1294
|
-
# it would override is another agent's work in progress.
|
|
1295
|
-
echo "plot-dispatch: a worker is alive on $restart_branch (pid ${restart_state#running }) — refusing." >&2
|
|
1296
|
-
echo " Stop it first if you mean to replace it:" >&2
|
|
1297
|
-
echo " plot-dispatch.sh --stop $restart_branch" >&2
|
|
1298
|
-
exit 1
|
|
1299
|
-
;;
|
|
1300
|
-
waiting*)
|
|
1301
|
-
# A person owes this branch an answer. A new worker meets the same
|
|
1302
|
-
# question and writes the same marker.
|
|
1303
|
-
# ASKED, NOT RE-GLOBBED. The marker's spelling lives with the
|
|
1304
|
-
# classification in plot-worker-state.sh and only there; a copy of the
|
|
1305
|
-
# glob here is the drift `workerstate.test.mjs` pins against.
|
|
1306
|
-
marker=$(plot_worker_blocked_file "$restart_wt" || true)
|
|
1307
|
-
echo "plot-dispatch: $restart_branch is blocked on a question — refusing." >&2
|
|
1308
|
-
echo " the question is in $restart_wt/${marker:-the marker file}" >&2
|
|
1309
|
-
echo " Answer it and delete the marker, then restart." >&2
|
|
1310
|
-
exit 1
|
|
1311
|
-
;;
|
|
1312
|
-
esac
|
|
1606
|
+
handover_refusal "$restart_branch" "$restart_wt" "$restart_state" || exit 1
|
|
1313
1607
|
# `stalled`, `failed`, `ended` and `no worker` all restart. The PR question
|
|
1314
1608
|
# above is what makes `failed` safe to include — and including it is the
|
|
1315
1609
|
# point: a gate that simply refused `failed` would pass every refusal test
|
|
@@ -3206,3 +3500,13 @@ book_started ${claimed_now[@]+"${claimed_now[@]}"} || true
|
|
|
3206
3500
|
check_and_update_cap "$n_started"
|
|
3207
3501
|
|
|
3208
3502
|
print_summary "$n_dispatched" "$n_reused" "$n_skipped" "$n_started"
|
|
3503
|
+
|
|
3504
|
+
# THE RECEIPT IS SPENT HERE, on the fan-out COMPLETING — never at the gate.
|
|
3505
|
+
# `plot-controller-gate.sh` clears on a receipt and LEAVES it, so a run that
|
|
3506
|
+
# died partway can be repeated on the same licence. One authorisation, one
|
|
3507
|
+
# completed action.
|
|
3508
|
+
#
|
|
3509
|
+
# ONLY THE FAN-OUT SPENDS ONE. `--status`, `--dry-run`, `--stop`, `--restart`,
|
|
3510
|
+
# `--start` and `--migrate` all return before this line, and the gate exempts
|
|
3511
|
+
# each of them: they are reads, or writes no endpoint owns.
|
|
3512
|
+
spend_action_receipt "plot-dispatch.sh"
|