@plot-pm/board 0.16.1 → 0.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/board-server.mjs +167 -139
- package/package.json +2 -3
- package/plot-agent-manifest.sh +111 -15
- package/plot-agent-monitor.sh +43 -5
- package/plot-approve.sh +234 -74
- package/plot-build-monitor.sh +49 -4
- package/plot-config.sh +10 -14
- package/plot-deliver.sh +10 -732
- package/plot-desk-root.sh +127 -0
- package/plot-dispatch.sh +313 -135
- package/plot-fleet-scan.sh +746 -62
- package/plot-host.sh +356 -43
- package/plot-monitor-subject.sh +55 -3
- package/plot-plan-meta.sh +155 -139
- package/plot-pr-merged.sh +16 -0
- package/plot-reap.sh +82 -93
- package/plot-state-receipt.sh +12 -2
- package/plot-tmp.sh +90 -14
- package/plot-transcript-quiet.sh +3 -1
- package/plot-worker-state.sh +633 -24
- package/plot-resolve-artifact.sh +0 -429
- package/plot-worker-monitor.sh +0 -705
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plot-pm/board",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
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",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"plot-config.sh",
|
|
32
32
|
"plot-default-branch.sh",
|
|
33
33
|
"plot-deliver.sh",
|
|
34
|
+
"plot-desk-root.sh",
|
|
34
35
|
"plot-dispatch.sh",
|
|
35
36
|
"plot-fleet-scan.sh",
|
|
36
37
|
"plot-host.sh",
|
|
@@ -38,11 +39,9 @@
|
|
|
38
39
|
"plot-pr-merged.sh",
|
|
39
40
|
"plot-reap.sh",
|
|
40
41
|
"plot-release-refs.sh",
|
|
41
|
-
"plot-resolve-artifact.sh",
|
|
42
42
|
"plot-state-receipt.sh",
|
|
43
43
|
"plot-tmp.sh",
|
|
44
44
|
"plot-transcript-quiet.sh",
|
|
45
|
-
"plot-worker-monitor.sh",
|
|
46
45
|
"plot-worker-state.sh"
|
|
47
46
|
],
|
|
48
47
|
"scripts": {
|
package/plot-agent-manifest.sh
CHANGED
|
@@ -17,7 +17,66 @@
|
|
|
17
17
|
# `plot_session_id`, which `plot-dispatch.sh` calls to launch an agent and
|
|
18
18
|
# `plot-worker-loop.sh` calls when a hop moves the agent to a new branch; and
|
|
19
19
|
# `manifest_resume_id` with `session_handle`, the conversation handle that the
|
|
20
|
-
# loop passes to the prompt and
|
|
20
|
+
# loop passes to the prompt and the loop's own watcher probes for a transcript.
|
|
21
|
+
|
|
22
|
+
# ONE READER FOR EVERY STRING FIELD a manifest carries, and one for every
|
|
23
|
+
# counter. Six callers read one field each — `branch`, `session`, `worktree`,
|
|
24
|
+
# `resumeId`, `attempts`, `correctionAttempts` — and each carried its own
|
|
25
|
+
# fourteen-line copy of the same read-parse-default dance. The copies answered
|
|
26
|
+
# identically and drifted only in the field name, so the name is the argument.
|
|
27
|
+
#
|
|
28
|
+
# ABSENT, UNREADABLE AND WRONG-TYPED ARE ONE ANSWER, which is the contract
|
|
29
|
+
# every caller already documents: nothing for a string (status 1), `0` for a
|
|
30
|
+
# counter. A manifest that cannot be read is not permission to do anything, and
|
|
31
|
+
# a counter that cannot be read also cannot be raised.
|
|
32
|
+
manifest_string() { # $1=manifest $2=field → prints the value, or nothing
|
|
33
|
+
local manifest="$1" value
|
|
34
|
+
[ -n "$manifest" ] && [ -f "$manifest" ] || return 1
|
|
35
|
+
value=$(node -e '
|
|
36
|
+
const fs = require("fs");
|
|
37
|
+
try {
|
|
38
|
+
const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
39
|
+
const v = manifest[process.argv[2]];
|
|
40
|
+
process.stdout.write(typeof v === "string" ? v : "");
|
|
41
|
+
} catch { process.stdout.write(""); }
|
|
42
|
+
' "$manifest" "$2" 2>/dev/null) && [ -n "$value" ] || return 1
|
|
43
|
+
printf '%s' "$value"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# A non-negative integer field, or `0`. The node call itself only ever prints
|
|
47
|
+
# a clean digit string or "0", so a failed call is the one case needing a
|
|
48
|
+
# shell-side fallback.
|
|
49
|
+
manifest_count() { # $1=manifest $2=field → prints a count
|
|
50
|
+
local manifest="$1"
|
|
51
|
+
[ -n "$manifest" ] && [ -f "$manifest" ] || { printf '0'; return 0; }
|
|
52
|
+
node -e '
|
|
53
|
+
const fs = require("fs");
|
|
54
|
+
try {
|
|
55
|
+
const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
56
|
+
const n = manifest[process.argv[2]];
|
|
57
|
+
process.stdout.write(Number.isInteger(n) && n >= 0 ? String(n) : "0");
|
|
58
|
+
} catch { process.stdout.write("0"); }
|
|
59
|
+
' "$manifest" "$2" 2>/dev/null || printf '0'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Raise a non-negative integer field by one, leaving every other field verbatim.
|
|
63
|
+
#
|
|
64
|
+
# THROUGH A TEMP FILE AND A RENAME, the shape every writer here takes: a reader
|
|
65
|
+
# never sees a partial manifest. A write that fails leaves the file alone and
|
|
66
|
+
# reports it; an absent manifest is not a failure.
|
|
67
|
+
raise_manifest_count() { # $1=manifest $2=field
|
|
68
|
+
local manifest="$1" tmp="$1.plot-count-tmp"
|
|
69
|
+
[ -n "$manifest" ] && [ -f "$manifest" ] || return 0
|
|
70
|
+
node -e '
|
|
71
|
+
const fs = require("fs");
|
|
72
|
+
const [file, field, tmp] = process.argv.slice(1);
|
|
73
|
+
const manifest = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
74
|
+
const n = manifest[field];
|
|
75
|
+
manifest[field] = (Number.isInteger(n) && n >= 0 ? n : 0) + 1;
|
|
76
|
+
fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n");
|
|
77
|
+
' "$manifest" "$2" "$tmp" 2>/dev/null || { rm -f "$tmp"; return 1; }
|
|
78
|
+
mv -f "$tmp" "$manifest" 2>/dev/null || { rm -f "$tmp"; return 1; }
|
|
79
|
+
}
|
|
21
80
|
|
|
22
81
|
# Clear `branch` when a slice finishes, so the window before the next one is
|
|
23
82
|
# observable.
|
|
@@ -98,18 +157,7 @@ plot_session_id() {
|
|
|
98
157
|
# `assigned_branch` in `plot-worker-loop.sh` already takes: no handle. A hand-started loop has no
|
|
99
158
|
# manifest, and a manifest nobody can read is not a handle.
|
|
100
159
|
manifest_resume_id() { # $1=manifest → prints the handle, or nothing
|
|
101
|
-
|
|
102
|
-
[ -n "$manifest" ] && [ -f "$manifest" ] || return 1
|
|
103
|
-
local id
|
|
104
|
-
id=$(node -e '
|
|
105
|
-
const fs = require("fs");
|
|
106
|
-
try {
|
|
107
|
-
const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
108
|
-
process.stdout.write(typeof manifest.resumeId === "string" ? manifest.resumeId : "");
|
|
109
|
-
} catch { process.stdout.write(""); }
|
|
110
|
-
' "$manifest" 2>/dev/null) || return 1
|
|
111
|
-
[ -n "$id" ] || return 1
|
|
112
|
-
printf '%s' "$id"
|
|
160
|
+
manifest_string "$1" resumeId
|
|
113
161
|
}
|
|
114
162
|
|
|
115
163
|
# THE HANDLE THE PROMPT CARRIES — the manifest's `resumeId`, or the launch id.
|
|
@@ -119,8 +167,8 @@ manifest_resume_id() { # $1=manifest → prints the handle, or nothing
|
|
|
119
167
|
# the two answers are the same string and this reads as a no-op. It stops being
|
|
120
168
|
# one the moment the handle diverges from the join key — which is what the two
|
|
121
169
|
# fields exist to allow, and what a later `--fork-session` would do. The loop
|
|
122
|
-
#
|
|
123
|
-
#
|
|
170
|
+
# calls this for the prompt and its own watcher calls it for the idle verdict,
|
|
171
|
+
# so both ask about one conversation.
|
|
124
172
|
#
|
|
125
173
|
# `$PLOT_SESSION_ID` IS THE FALLBACK, NOT THE SOURCE. A hand-started loop has no
|
|
126
174
|
# manifest and a pre-`resumeId` manifest carries no handle; both are the launch
|
|
@@ -135,3 +183,51 @@ session_handle() { # → the handle, or nothing
|
|
|
135
183
|
[ -n "${PLOT_SESSION_ID:-}" ] || return 1
|
|
136
184
|
printf '%s' "$PLOT_SESSION_ID"
|
|
137
185
|
}
|
|
186
|
+
|
|
187
|
+
# The live agents whose manifests name a branch, for `claimAnswer`'s
|
|
188
|
+
# `holders` — one session id per line, in the directory's own order.
|
|
189
|
+
#
|
|
190
|
+
# ASKS EACH MANIFEST'S OWN DESK, never the branch's checkout: an agent just
|
|
191
|
+
# handed the branch has not checked it out, so a check against the branch's
|
|
192
|
+
# worktree would answer nothing even while that agent's own desk is alive and
|
|
193
|
+
# genuinely holds the slice. `queue-reading.ts:302` takes the same `running` or
|
|
194
|
+
# `waiting` bar; a dead agent's manifest is not a holder (#1039's negative
|
|
195
|
+
# control).
|
|
196
|
+
#
|
|
197
|
+
# `$2` AND `$3` EXCLUDE DIFFERENT THINGS, and a caller supplies at most one.
|
|
198
|
+
# `plot-dispatch.sh --release` has no asker and excludes `release_wt` instead —
|
|
199
|
+
# the desk its OWN live-worker refusal already checks, so this must not
|
|
200
|
+
# duplicate that refusal's case with a different message for the same desk.
|
|
201
|
+
# `plot-worker-loop.sh`'s rejection excludes its OWN manifest file instead —
|
|
202
|
+
# the supervisor wrote the rejected branch into it before the push, so
|
|
203
|
+
# counting it would answer `held-by-agent` on every rejection.
|
|
204
|
+
live_holders_of_branch() { # $1=registry_dir $2=branch $3=exclude_manifest $4=exclude_worktree → "session\tworktree" lines
|
|
205
|
+
local dir="$1" branch="$2" exclude_manifest="${3:-}" exclude_worktree="${4:-}" m m_wt
|
|
206
|
+
[ -d "$dir" ] || return 0
|
|
207
|
+
for m in "$dir"/*.json; do
|
|
208
|
+
[ -f "$m" ] && [ "$m" != "$exclude_manifest" ] || continue
|
|
209
|
+
[ "$(manifest_string "$m" branch)" = "$branch" ] || continue
|
|
210
|
+
m_wt=$(manifest_string "$m" worktree) || continue
|
|
211
|
+
[ -d "$m_wt" ] && [ "$m_wt" != "$exclude_worktree" ] || continue
|
|
212
|
+
case "$(plot_worker_state "$m_wt" "" | cut -f1)" in
|
|
213
|
+
running|waiting) printf '%s\t%s\n' "$(manifest_string "$m" session)" "$m_wt" ;;
|
|
214
|
+
esac
|
|
215
|
+
done
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# WHAT A CLAIM MEANS, ASKED OF THE DOMAIN. One of `claimAnswer`'s five words,
|
|
219
|
+
# or nothing when the bundle is absent — a checkout that vendored the skills
|
|
220
|
+
# without building the board answers no question rather than a wrong one.
|
|
221
|
+
#
|
|
222
|
+
# ONE ASKER FOR BOTH CALLERS. `plot-dispatch.sh --release` asks with `ref:
|
|
223
|
+
# unknown` and no log, because `held-by-agent` follows from `holders` alone;
|
|
224
|
+
# `plot-worker-loop.sh`'s rejection path supplies all three. The JSON shape is
|
|
225
|
+
# the bundle's contract, and two hand-built copies of it would drift.
|
|
226
|
+
claim_answer() { # $1=bundle $2=ref $3=log $4=holders, newline-separated → the word, or nothing
|
|
227
|
+
[ -f "$1" ] || return 1
|
|
228
|
+
printf '%s\n' "$4" | node -e '
|
|
229
|
+
const holders = require("fs").readFileSync(0, "utf8").split("\n").filter(Boolean);
|
|
230
|
+
const [ref, log] = process.argv.slice(1);
|
|
231
|
+
process.stdout.write(JSON.stringify({ ref, log: ref === "present" ? log : null, holders }));
|
|
232
|
+
' "$2" "$3" | node "$1" 2>/dev/null | cut -f1
|
|
233
|
+
}
|
package/plot-agent-monitor.sh
CHANGED
|
@@ -93,9 +93,13 @@ Started by plot-dispatch.sh inside the worker's wrapper. Reads its subject from
|
|
|
93
93
|
the environment, exactly as the wrapper's other children do:
|
|
94
94
|
|
|
95
95
|
PLOT_BRANCH the branch whose debts this monitor will report
|
|
96
|
-
PLOT_WORKTREE the desk it
|
|
96
|
+
PLOT_WORKTREE the desk it STARTS on; it follows the manifest's
|
|
97
|
+
`worktree` from then on (PLOT_MANIFEST_FILE below)
|
|
98
|
+
PLOT_MANIFEST_FILE the manifest this agent is named in, re-read each pass;
|
|
99
|
+
absent or unreadable keeps watching PLOT_WORKTREE
|
|
97
100
|
PLOT_MONITOR_FILE where findings are published (default:
|
|
98
|
-
$PLOT_WORKTREE/.plot-worker.monitor.agent.jsonl)
|
|
101
|
+
$PLOT_WORKTREE/.plot-worker.monitor.agent.jsonl). Set
|
|
102
|
+
explicitly, it WINS and does not follow a hop.
|
|
99
103
|
PLOT_MONITOR_INTERVAL seconds between passes (default 300)
|
|
100
104
|
|
|
101
105
|
--once take one sample and exit, rather than looping. A test
|
|
@@ -116,7 +120,14 @@ done
|
|
|
116
120
|
monitor='AgentMonitor'
|
|
117
121
|
|
|
118
122
|
branch="${PLOT_BRANCH:-}"
|
|
119
|
-
worktree
|
|
123
|
+
# THE DESK THIS MONITOR WAS LAUNCHED ON, FIXED FOR ITS WHOLE LIFE. `worktree`
|
|
124
|
+
# itself is no longer fixed: `monitor_pass` reassigns it every pass by asking
|
|
125
|
+
# `plot_watched_desk`, which follows a hop to the manifest's new `worktree`
|
|
126
|
+
# field. This is the launch desk `plot_watched_desk` falls back to when the
|
|
127
|
+
# manifest carries no override — never read directly after startup.
|
|
128
|
+
launched_worktree="${PLOT_WORKTREE:-}"
|
|
129
|
+
worktree="$launched_worktree"
|
|
130
|
+
manifest_file="${PLOT_MANIFEST_FILE:-}"
|
|
120
131
|
# FIVE MINUTES IS A HOST BUDGET, NOT CAUTION. Its findings need a PR lookup, and
|
|
121
132
|
# this repository has already measured what happens when host questions ride a
|
|
122
133
|
# fast loop. Against a stall that lasted 50 minutes, five makes it visible 45
|
|
@@ -126,7 +137,11 @@ worktree="${PLOT_WORKTREE:-}"
|
|
|
126
137
|
# prevent.
|
|
127
138
|
interval="${PLOT_MONITOR_INTERVAL:-300}"
|
|
128
139
|
|
|
129
|
-
|
|
140
|
+
# IF `PLOT_MONITOR_FILE` IS SET IT WINS AND DOES NOT FOLLOW THE DESK — the
|
|
141
|
+
# existing contract this usage text already states. Otherwise `findings`
|
|
142
|
+
# follows `worktree`, reassigned alongside it at the top of `monitor_pass`.
|
|
143
|
+
monitor_file_override="${PLOT_MONITOR_FILE:-}"
|
|
144
|
+
findings="${monitor_file_override:-${worktree:+$worktree/.plot-worker.monitor.agent.jsonl}}"
|
|
130
145
|
|
|
131
146
|
# THE SUBJECT, read the same way the WorkerMonitor reads it: `.plot-worker.pid`
|
|
132
147
|
# names the AGENT, and the wrapper passes its path in `PLOT_PID_FILE`.
|
|
@@ -478,7 +493,30 @@ sample_finding() { # → prints "finding\tevidence", or nothing
|
|
|
478
493
|
|
|
479
494
|
# One full pass: sample, publish only on a change.
|
|
480
495
|
monitor_pass() {
|
|
481
|
-
local row finding evidence
|
|
496
|
+
local row finding evidence new_worktree
|
|
497
|
+
# REASSIGNED ONCE, HERE, RATHER THAN THREADED THROUGH EACH READER.
|
|
498
|
+
# `worktree` feeds `plot_worker_dirty`, `plot_worker_blocked` and this
|
|
499
|
+
# monitor's transcript probe, so a hop is picked up in one place rather than
|
|
500
|
+
# three. `PLOT_MONITOR_FILE`, when set, WINS and does not follow the desk —
|
|
501
|
+
# the existing contract the usage text states.
|
|
502
|
+
new_worktree=$(plot_watched_desk "$manifest_file" "$launched_worktree")
|
|
503
|
+
|
|
504
|
+
# A DESK CHANGE RESETS `published`/`since` DELIBERATELY. The debt they
|
|
505
|
+
# remember is the OLD desk's: a finding like `holds unlanded work` is about
|
|
506
|
+
# commits and files on that specific tree, and after a hop the new desk has
|
|
507
|
+
# never been measured. Carrying the old `published` forward would compare
|
|
508
|
+
# the new desk's first reading against the old one's last and could publish
|
|
509
|
+
# `clear` for a debt that was never the new desk's to begin with — the exact
|
|
510
|
+
# trap the brief names. Resetting to blank treats the new desk as a fresh
|
|
511
|
+
# subject: a real finding there publishes as new, and a clean desk publishes
|
|
512
|
+
# nothing at all, which is silence rather than a false `clear`.
|
|
513
|
+
if [ "$new_worktree" != "$worktree" ]; then
|
|
514
|
+
published=''
|
|
515
|
+
since=''
|
|
516
|
+
fi
|
|
517
|
+
worktree="$new_worktree"
|
|
518
|
+
findings="${monitor_file_override:-${worktree:+$worktree/.plot-worker.monitor.agent.jsonl}}"
|
|
519
|
+
|
|
482
520
|
row=$(sample_finding)
|
|
483
521
|
finding="${row%% *}"
|
|
484
522
|
evidence=''
|