@plot-pm/board 0.9.1 → 0.11.0
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 +170 -164
- package/package.json +6 -1
- package/plot-agent-monitor.sh +508 -0
- package/plot-approve.sh +145 -29
- package/plot-budget.sh +439 -0
- package/plot-config.sh +9 -0
- package/plot-deliver.sh +210 -117
- package/plot-dispatch.sh +829 -185
- package/plot-fleet-scan.sh +577 -65
- package/plot-host.sh +1329 -55
- package/plot-plan-meta.sh +246 -36
- package/plot-reap.sh +675 -57
- package/plot-transcript-quiet.sh +142 -0
- package/plot-worker-monitor.sh +644 -0
- package/plot-worker-state.sh +81 -50
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# The ONE answer to "how long has this worktree's agent been quiet?" — sourced,
|
|
3
|
+
# not run, by `plot-worker-monitor.sh`.
|
|
4
|
+
#
|
|
5
|
+
# It reads the AGENT rather than the machine. A `claude -p` session appends a
|
|
6
|
+
# timestamped line to its transcript for every model turn, tool call and tool
|
|
7
|
+
# result; the seconds since the newest of those lines is how long the agent has
|
|
8
|
+
# produced nothing. A CPU sample answers *is this process on a core right now?*,
|
|
9
|
+
# which is a different question and is zero for most of a working agent's life.
|
|
10
|
+
#
|
|
11
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
12
|
+
# THREE ANSWERS, AND THE THIRD IS NOT A FAILURE
|
|
13
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
+
#
|
|
15
|
+
# <seconds> the newest transcript line is that many seconds old
|
|
16
|
+
# unavailable no transcript can be read for this worktree, INCLUDING the
|
|
17
|
+
# case where the arguments name no worktree at all
|
|
18
|
+
#
|
|
19
|
+
# `unavailable` is a first-class answer, settled by
|
|
20
|
+
# `the-registry-supervises-its-agents`: a capability the adopting project does
|
|
21
|
+
# not provide is UNAVAILABLE, never failed and never zero. A caller that read
|
|
22
|
+
# it as "quiet for 0 seconds" would report every unreadable agent healthy; one
|
|
23
|
+
# that read it as an error would refuse to run where Plot's own contract says
|
|
24
|
+
# it should degrade. So it is a word, and callers match on it.
|
|
25
|
+
#
|
|
26
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
+
# THE TRANSCRIPT IS FOUND BY PATH, NOT BY SESSION ID — AND THAT IS A CHOICE
|
|
28
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
+
#
|
|
30
|
+
# `.plot/worker-prompt.sh:29` DOES pass `--session-id` now (2026-09-04), so an
|
|
31
|
+
# exact join is available here and this deliberately does not take it. The
|
|
32
|
+
# reason is the question, not the capability: this asks *is anything happening
|
|
33
|
+
# at this desk*, and an operator's own session at the same worktree is a true
|
|
34
|
+
# answer to it. Joining on the worker's id alone would report a desk quiet
|
|
35
|
+
# while somebody is visibly working at it.
|
|
36
|
+
#
|
|
37
|
+
# THE OPPOSITE JOIN IS RIGHT FOR THE OPPOSITE QUESTION. *What has THIS agent
|
|
38
|
+
# spent* is per session and never per worktree — one project directory measured
|
|
39
|
+
# 2026-09-03 held 45 session files, 30 of them subagents, and a sum across them
|
|
40
|
+
# belongs to no one. `rules/spend.ts` states that side; the two read the same
|
|
41
|
+
# files and must not be made to share a join.
|
|
42
|
+
#
|
|
43
|
+
# So the join here is the one `plot-quiet-stretch.mjs` already made and proved
|
|
44
|
+
# on 23 real sessions: the runtime stores a session under
|
|
45
|
+
# `$HOME/.claude/projects/<slug>` where the slug is the WORKTREE PATH with `/`
|
|
46
|
+
# and `.` replaced by `-`. A dispatched worker has its own worktree, so the
|
|
47
|
+
# path identifies the DESK without any id being passed anywhere.
|
|
48
|
+
#
|
|
49
|
+
# `agent-` PREFIXED FILES ARE SKIPPED, for wave 1's reason: a subagent's
|
|
50
|
+
# transcript is a true statement about the wrong process. A worker whose
|
|
51
|
+
# subagent is chatting while the worker itself has stopped must read as quiet.
|
|
52
|
+
#
|
|
53
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
54
|
+
# THE NEWEST LINE ACROSS ALL OF A WORKTREE'S SESSIONS
|
|
55
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
56
|
+
#
|
|
57
|
+
# A worktree can hold several sessions: a worker that hopped waves, or an
|
|
58
|
+
# operator who opened a session at the same desk. Any of them producing output
|
|
59
|
+
# means SOMEBODY is working there, and the monitor's question is about the desk.
|
|
60
|
+
# Taking the maximum timestamp is the answer that never ends a live session
|
|
61
|
+
# because a stale sibling exists beside it.
|
|
62
|
+
|
|
63
|
+
# The runtime's project-slug derivation. Duplicated from `plot-quiet-stretch.mjs`
|
|
64
|
+
# — which duplicates it from the board's `projectSlug` — because a monitor on a
|
|
65
|
+
# 30s loop must not depend on a build step. One line, pinned by a test on each
|
|
66
|
+
# side.
|
|
67
|
+
plot_transcript_slug() { # $1=worktree path → slug
|
|
68
|
+
printf '%s' "$1" | tr '/.' '--'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Where the runtime keeps this worktree's transcripts, or "" if nowhere.
|
|
72
|
+
plot_transcript_dir() { # $1=worktree → directory path (may not exist)
|
|
73
|
+
local wt="$1" home="${PLOT_TRANSCRIPT_HOME:-${HOME:-}}"
|
|
74
|
+
[ -n "$wt" ] && [ -n "$home" ] || return 0
|
|
75
|
+
printf '%s/.claude/projects/%s' "$home" "$(plot_transcript_slug "$wt")"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Seconds since this worktree's agent last wrote anything.
|
|
79
|
+
#
|
|
80
|
+
# THE MTIME IS THE READING, not the file's contents. The runtime appends as it
|
|
81
|
+
# works, so the file's modification time IS the timestamp of its newest line —
|
|
82
|
+
# and reading it costs one `stat` rather than parsing a transcript that reaches
|
|
83
|
+
# tens of megabytes on a long session. Wave 1 parsed timestamps because it was
|
|
84
|
+
# measuring a DISTRIBUTION of past gaps; this needs only the newest, on a 30s
|
|
85
|
+
# loop, for as many workers as the machine holds.
|
|
86
|
+
#
|
|
87
|
+
# A CLOCK IS THE RIGHT INSTRUMENT HERE, and that is worth stating because Plot
|
|
88
|
+
# usually refuses one. `plot-estate-changed.sh` hashes content rather than
|
|
89
|
+
# reading mtime, because its question is *did this change?* and a checkout moves
|
|
90
|
+
# mtime without changing anything. This question is *how long since output?* —
|
|
91
|
+
# which is a question about elapsed time, and mtime is the measurement of it.
|
|
92
|
+
plot_transcript_quiet_seconds() { # $1=worktree → seconds | unavailable
|
|
93
|
+
local wt="$1" dir newest now mtime
|
|
94
|
+
dir=$(plot_transcript_dir "$wt")
|
|
95
|
+
[ -n "$dir" ] && [ -d "$dir" ] || { printf 'unavailable'; return 0; }
|
|
96
|
+
|
|
97
|
+
# The newest mtime across every non-`agent-` session file in the directory.
|
|
98
|
+
# `find -print0` and a while-read keep paths with spaces intact; `stat` is
|
|
99
|
+
# asked once per file, and a worktree holds one to eight.
|
|
100
|
+
newest=''
|
|
101
|
+
while IFS= read -r -d '' f; do
|
|
102
|
+
case "$(basename "$f")" in agent-*) continue ;; esac
|
|
103
|
+
mtime=$(plot_transcript_mtime "$f") || continue
|
|
104
|
+
[ -n "$mtime" ] || continue
|
|
105
|
+
if [ -z "$newest" ] || [ "$mtime" -gt "$newest" ] 2>/dev/null; then newest="$mtime"; fi
|
|
106
|
+
done < <(find "$dir" -maxdepth 1 -type f -name '*.jsonl' -print0 2>/dev/null)
|
|
107
|
+
|
|
108
|
+
# A directory that exists but holds no session file is still UNAVAILABLE. The
|
|
109
|
+
# runtime creates the directory when the project is first opened, so an empty
|
|
110
|
+
# one means no session has written here — which is exactly "no transcript can
|
|
111
|
+
# be read", not "quiet for a very long time".
|
|
112
|
+
[ -n "$newest" ] || { printf 'unavailable'; return 0; }
|
|
113
|
+
|
|
114
|
+
now=$(date +%s)
|
|
115
|
+
local quiet=$(( now - newest ))
|
|
116
|
+
# A transcript written in the future — a clock skew across a mounted volume —
|
|
117
|
+
# reads as zero rather than negative. An end condition comparing a negative
|
|
118
|
+
# against a threshold would behave correctly by accident here and not
|
|
119
|
+
# elsewhere; clamping says what is meant.
|
|
120
|
+
[ "$quiet" -lt 0 ] && quiet=0
|
|
121
|
+
printf '%s' "$quiet"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# A file's modification time as a unix epoch. BSD and GNU `stat` disagree on the
|
|
125
|
+
# flag, and a monitor that works on the author's laptop and not in CI is a
|
|
126
|
+
# monitor nobody trusts.
|
|
127
|
+
#
|
|
128
|
+
# THE `||` IS NOT ENOUGH, AND CI MEASURED WHY. On Linux `stat -f` is not an
|
|
129
|
+
# unknown flag — it means FILESYSTEM info, and it SUCCEEDS. So the BSD form
|
|
130
|
+
# never falls through: it printed `Namelen: 255 Type: ext2/ext3` and the
|
|
131
|
+
# caller subtracted that from a clock. Two of this branch's own tests failed on
|
|
132
|
+
# it, 2026-09-02, having passed on macOS.
|
|
133
|
+
#
|
|
134
|
+
# So the answer is validated rather than trusted. Each form must yield digits;
|
|
135
|
+
# anything else is treated as that form not being available here.
|
|
136
|
+
plot_transcript_mtime() { # $1=file → epoch seconds
|
|
137
|
+
local m
|
|
138
|
+
m=$(stat -c '%Y' "$1" 2>/dev/null)
|
|
139
|
+
case "$m" in ''|*[!0-9]*) m=$(stat -f '%m' "$1" 2>/dev/null) ;; esac
|
|
140
|
+
case "$m" in ''|*[!0-9]*) return 1 ;; esac
|
|
141
|
+
printf '%s' "$m"
|
|
142
|
+
}
|