@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.
@@ -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
+ }