@plot-pm/board 0.16.3 → 0.18.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 +252 -153
- package/package.json +3 -4
- package/plot-agent-manifest.sh +9 -44
- package/plot-config.sh +16 -14
- package/plot-default-branch.sh +15 -19
- package/plot-dispatch.sh +29 -26
- package/plot-fleet-scan.sh +154 -141
- package/plot-host.sh +51 -59
- package/plot-monitor-subject.sh +7 -8
- package/plot-worker-state.sh +10 -163
- package/plot-build-monitor.sh +0 -480
- package/plot-transcript-quiet.sh +0 -172
package/plot-host.sh
CHANGED
|
@@ -219,13 +219,11 @@
|
|
|
219
219
|
# empty: `jen job list` carries no history and
|
|
220
220
|
# no timestamps, and inventing them would be a
|
|
221
221
|
# collector reaching a verdict.
|
|
222
|
-
# run-for-sha <branch> <sha> the run for ONE sha
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
#
|
|
227
|
-
# list` names no commit, so there is nothing to
|
|
228
|
-
# match a sha against. Output:
|
|
222
|
+
# run-for-sha <branch> <sha> the run for ONE sha, and nothing for any
|
|
223
|
+
# other sha — as a single JSON object, or
|
|
224
|
+
# nothing when the branch has no run for this
|
|
225
|
+
# sha. Dispatched on the `CI` key like `runs`.
|
|
226
|
+
# Output:
|
|
229
227
|
# {"sha":"…","status":"queued|in_progress|
|
|
230
228
|
# completed|waiting|requested",
|
|
231
229
|
# "conclusion":"success|failure|…|null",
|
|
@@ -235,22 +233,12 @@
|
|
|
235
233
|
# and sha-blind, and `gh run list --branch X`
|
|
236
234
|
# returns runs for every sha that branch ever
|
|
237
235
|
# had — the newest run is NOT necessarily for
|
|
238
|
-
# the newest commit. A
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
#
|
|
242
|
-
#
|
|
243
|
-
#
|
|
244
|
-
# re-armed.
|
|
245
|
-
# THE FALLBACK IS WHAT MAKES THAT VISIBLE. Were
|
|
246
|
-
# it to report nothing when the asked-for sha
|
|
247
|
-
# has no run, a run IN FLIGHT for a superseded
|
|
248
|
-
# commit would look exactly like no run at all,
|
|
249
|
-
# and a caller could not tell "CI has not
|
|
250
|
-
# started" from "CI is answering about the
|
|
251
|
-
# past". `sha` names the run's own commit, and
|
|
252
|
-
# comparing it to the one asked about is the
|
|
253
|
-
# CALLER's rule — this decides nothing.
|
|
236
|
+
# the newest commit. A run for any sha but the
|
|
237
|
+
# one asked about is not evidence about it, so
|
|
238
|
+
# reporting it would invite a merge of the
|
|
239
|
+
# wrong thing. Measured 2026-08-30: two merge
|
|
240
|
+
# waiters reported on superseded runs and had
|
|
241
|
+
# to be stopped and re-armed.
|
|
254
242
|
# `status` AND `conclusion` ARE BOTH REPORTED,
|
|
255
243
|
# never collapsed. A run that is `completed` has
|
|
256
244
|
# a conclusion; one that is `waiting` or
|
|
@@ -474,7 +462,6 @@ die3() { echo "plot-host: $*" >&2; exit 3; }
|
|
|
474
462
|
# a person at a terminal want three different answers — and a retry inside the
|
|
475
463
|
# adapter would hide the very state this code exists to surface, turning a
|
|
476
464
|
# reportable fact into an unexplained four-minute call.
|
|
477
|
-
die5() { echo "plot-host: $*" >&2; exit 5; }
|
|
478
465
|
|
|
479
466
|
# Exit 6 — the host refused because too many calls arrived AT ONCE. A secondary
|
|
480
467
|
# limit, and a different ceiling from the one exit 5 reports.
|
|
@@ -497,7 +484,6 @@ die5() { echo "plot-host: $*" >&2; exit 5; }
|
|
|
497
484
|
#
|
|
498
485
|
# NOT A RETRY, for the reason exit 5 states: whether to wait is the caller's
|
|
499
486
|
# decision, and this adapter reports rather than reacts.
|
|
500
|
-
die6() { echo "plot-host: $*" >&2; exit 6; }
|
|
501
487
|
|
|
502
488
|
# WHICH FAILURE, read off the wording — the same shape `bb_issue_exit_code`
|
|
503
489
|
# uses, and for the same reason: the exit code cannot split these cases. `gh`
|
|
@@ -514,7 +500,7 @@ die6() { echo "plot-host: $*" >&2; exit 6; }
|
|
|
514
500
|
# `throttled` for every match of one regex until 2026-09-02, so *"API rate
|
|
515
501
|
# limit exceeded"* and *"You have exceeded a secondary rate limit"* came back
|
|
516
502
|
# the same word and nothing downstream could tell them apart. `secondary` is
|
|
517
|
-
# now its own answer: `
|
|
503
|
+
# now its own answer: `pr_list_failed` exits 6 for it, and the board names which limit was
|
|
518
504
|
# hit rather than printing one reset over both.
|
|
519
505
|
#
|
|
520
506
|
# THE SECONDARY TEST RUNS FIRST, AND THE ORDER IS THE CLASSIFICATION. GitHub's
|
|
@@ -651,7 +637,7 @@ pr_list_failed() { # $1=stderr text
|
|
|
651
637
|
#
|
|
652
638
|
# EVERY CALL SITE MUST WRITE `|| exit $?`, AND IT IS NOT OPTIONAL. This is
|
|
653
639
|
# invoked as `_raw="$(pr_list_call …)"` — a COMMAND SUBSTITUTION, which is a
|
|
654
|
-
# subshell — so the `exit` inside `
|
|
640
|
+
# subshell — so the `exit` inside `pr_list_failed`/`die3` leaves that subshell only. The
|
|
655
641
|
# outer script would carry on with `_raw` empty and `jq` would emit nothing:
|
|
656
642
|
# the silent empty list this whole helper exists to remove, rebuilt one layer
|
|
657
643
|
# further in and harder to see. The same trap `bb_states_for` documents a few
|
|
@@ -3619,7 +3605,8 @@ case "$op" in
|
|
|
3619
3605
|
*) die "pr-merged: unknown arg $1" ;;
|
|
3620
3606
|
esac
|
|
3621
3607
|
done
|
|
3622
|
-
|
|
3608
|
+
# NO REMOTE, NO PR: no repository holds one, so nothing merged, whatever gh's auth says.
|
|
3609
|
+
if [ ${#repo_args[@]} -eq 0 ] && [ -z "$(git remote 2>/dev/null)" ]; then echo "not-merged"; elif [ "$be" = "github" ]; then
|
|
3623
3610
|
# --state all, because a merged PR reports CLOSED and the default `open`
|
|
3624
3611
|
# would hide every one of them. --limit 100 rather than 1: the newest PR
|
|
3625
3612
|
# is not the merge, exactly as the state is not the merge.
|
|
@@ -4349,7 +4336,7 @@ case "$op" in
|
|
|
4349
4336
|
;;
|
|
4350
4337
|
|
|
4351
4338
|
run-for-sha)
|
|
4352
|
-
# The
|
|
4339
|
+
# The run for ONE sha, or nothing — the BuildMonitor's only host question.
|
|
4353
4340
|
#
|
|
4354
4341
|
# WHY THIS IS NOT `runs`. `runs` is branch-scoped and reports no sha at all,
|
|
4355
4342
|
# so a caller cannot tell which commit an answer is about. `gh run list
|
|
@@ -4376,9 +4363,9 @@ case "$op" in
|
|
|
4376
4363
|
# deliberately NOT an error: a monitor polling a fresh push sees it on every
|
|
4377
4364
|
# pass until CI wakes up.
|
|
4378
4365
|
#
|
|
4379
|
-
# Bitbucket
|
|
4380
|
-
# `
|
|
4381
|
-
#
|
|
4366
|
+
# A Bitbucket remote exits 4 (`bb` has no run listing), as does a failing
|
|
4367
|
+
# `gh` or Jenkins that cannot be asked: exit 4 reads as unavailable, never
|
|
4368
|
+
# as "this sha has no build".
|
|
4382
4369
|
branch="${1:?run-for-sha needs a branch}"; shift
|
|
4383
4370
|
sha="${1:?run-for-sha needs a sha}"; shift
|
|
4384
4371
|
# Enough runs to find the sha among its neighbours. A branch accumulates
|
|
@@ -4467,10 +4454,9 @@ case "$op" in
|
|
|
4467
4454
|
echo "plot-host: run-for-sha — Jenkins did not answer for '$_jen_host'" >&2
|
|
4468
4455
|
exit 4
|
|
4469
4456
|
fi
|
|
4470
|
-
# THE SAME
|
|
4471
|
-
#
|
|
4472
|
-
#
|
|
4473
|
-
# two apart would be back to the branch-scoped guessing this op ends.
|
|
4457
|
+
# THE SAME MATCH RULE AS THE GITHUB ARM: only the asked-for sha, never
|
|
4458
|
+
# another build's. A build for any other commit is not evidence about
|
|
4459
|
+
# this one, so no match means no output.
|
|
4474
4460
|
#
|
|
4475
4461
|
# `result` is null while a build runs, which is Jenkins' own word for
|
|
4476
4462
|
# *in flight* — mapped to the `status`/`conclusion` split the contract
|
|
@@ -4482,7 +4468,7 @@ case "$op" in
|
|
|
4482
4468
|
conclusion: (if .building then null else (.result // null) end),
|
|
4483
4469
|
url: (.url // ""),
|
|
4484
4470
|
startedAt: (if .timestamp then (.timestamp / 1000 | todate) else "" end) } ]
|
|
4485
|
-
| (
|
|
4471
|
+
| (map(select(.sha == $sha)) | .[0])
|
|
4486
4472
|
| select(. != null)' 2>/dev/null || true
|
|
4487
4473
|
# THIS ARM ANSWERS AND THE OP IS OVER. Everything below the `esac` is
|
|
4488
4474
|
# the GitHub path — the old jenkins arm reached it only because it
|
|
@@ -4512,30 +4498,36 @@ case "$op" in
|
|
|
4512
4498
|
# newest-first, and a sha can carry several (a rerun, or several
|
|
4513
4499
|
# workflows). The newest is the live answer; older ones for the same sha
|
|
4514
4500
|
# are superseded by the same argument that superseded runs for older shas.
|
|
4515
|
-
# THE SHA ASKED ABOUT IF THERE IS ONE, ELSE THE NEWEST RUN ON THE BRANCH —
|
|
4516
|
-
# and `sha` in the output says WHICH, because a caller that could not tell
|
|
4517
|
-
# the two apart would be back to the branch-scoped guessing this op exists
|
|
4518
|
-
# to end.
|
|
4519
4501
|
#
|
|
4520
|
-
#
|
|
4521
|
-
#
|
|
4522
|
-
#
|
|
4523
|
-
#
|
|
4524
|
-
#
|
|
4525
|
-
# the state that had two merge waiters reporting on superseded runs on
|
|
4526
|
-
# 2026-08-30, and it is exactly what a caller needs to see.
|
|
4502
|
+
# ONLY THE ASKED-FOR SHA, NEVER ANOTHER ONE'S RUN. A run for any other
|
|
4503
|
+
# commit is not evidence about this one — reporting it would read as a
|
|
4504
|
+
# live answer for a commit the branch has already moved past, which is
|
|
4505
|
+
# worse than no answer. No match means no output, read the same as a
|
|
4506
|
+
# branch with no runs at all.
|
|
4527
4507
|
#
|
|
4528
|
-
#
|
|
4529
|
-
#
|
|
4530
|
-
#
|
|
4531
|
-
gh run list --branch "$branch" --limit "$limit" \
|
|
4532
|
-
--json headSha,conclusion,status,startedAt,url 2>/dev/null \
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4508
|
+
# A FAILING `gh` IS NOT AN EMPTY HISTORY. An expired token, a rate limit or
|
|
4509
|
+
# a network failure exits 4 here, the way the Jenkins arm does, so the
|
|
4510
|
+
# monitor reads *could not ask* and not *no run yet*.
|
|
4511
|
+
_gh_runs=$(gh run list --branch "$branch" --limit "$limit" \
|
|
4512
|
+
--json headSha,conclusion,status,startedAt,url,databaseId 2>/dev/null) \
|
|
4513
|
+
|| { echo "plot-host: run-for-sha — gh run list failed for '$branch'" >&2; exit 4; }
|
|
4514
|
+
_gh_match=$(printf '%s' "$_gh_runs" | jq -c --arg sha "$sha" \
|
|
4515
|
+
'(map(select(.headSha == $sha)) | .[0]) | select(. != null)
|
|
4516
|
+
| {sha:.headSha, status:.status,
|
|
4517
|
+
conclusion:(if (.conclusion // "") == "" then null else .conclusion end),
|
|
4518
|
+
url:.url, startedAt:.startedAt, databaseId:.databaseId}') || exit 4
|
|
4519
|
+
[ -n "$_gh_match" ] || exit 0
|
|
4520
|
+
# ONE MORE CALL, ONLY FOR A CONCLUDED FAILURE (#1295): `gh run view --json
|
|
4521
|
+
# jobs` is the only place a 0-step job (no runner ever picked up the run)
|
|
4522
|
+
# shows up, apart from a real failure. A run still going or that
|
|
4523
|
+
# succeeded costs no extra call.
|
|
4524
|
+
_gh_db_id=$(printf '%s' "$_gh_match" | jq -r 'if (.conclusion == "failure" or .conclusion == "cancelled") then .databaseId else "" end')
|
|
4525
|
+
_gh_match=$(printf '%s' "$_gh_match" | jq -c 'del(.databaseId)') || exit 4
|
|
4526
|
+
[ -z "$_gh_db_id" ] && { printf '%s' "$_gh_match"; exit 0; }
|
|
4527
|
+
# Slurped via stdin, never `--argjson`: that flag caps at 128 KB on Linux.
|
|
4528
|
+
_gh_jobs=$(gh run view "$_gh_db_id" --json jobs 2>/dev/null) || { echo "plot-host: run-for-sha — gh run view failed" >&2; exit 4; }
|
|
4529
|
+
jq -n -c --slurpfile match <(printf '%s' "$_gh_match") --slurpfile run <(printf '%s' "$_gh_jobs") \
|
|
4530
|
+
'$match[0] + {jobs: [$run[0].jobs[] | {conclusion, steps: (.steps | length)}]}' || exit 4
|
|
4539
4531
|
;;
|
|
4540
4532
|
|
|
4541
4533
|
issue-list)
|
package/plot-monitor-subject.sh
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Plot helper: the ONE answer to "is this monitor's subject still there?"
|
|
3
3
|
#
|
|
4
|
-
# SOURCED, NOT RUN, by `plot-agent-monitor.sh
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# exits is exactly the leak this file exists to close, half-fixed.
|
|
4
|
+
# SOURCED, NOT RUN, by `plot-agent-monitor.sh`. The same shape as
|
|
5
|
+
# `plot-worker-state.sh` and `plot-pr-merged.sh` — and the same reason.
|
|
6
|
+
# `plot-worker-state.sh` carried five of its six states in duplicate until
|
|
7
|
+
# 2026-08-18, and the copies had already drifted on the sixth. A monitor
|
|
8
|
+
# deciding independently when to stop, with its computation copied rather than
|
|
9
|
+
# shared, would drift the same way, and the failure would be silent: a monitor
|
|
10
|
+
# left running forever is exactly the leak this file exists to close.
|
|
12
11
|
#
|
|
13
12
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
14
13
|
# WHY A MONITOR NEEDS THIS AT ALL
|
package/plot-worker-state.sh
CHANGED
|
@@ -499,6 +499,15 @@ plot_worker_blocked_file() { # $1=worktree → prints the marker's basename
|
|
|
499
499
|
# exact population this state must not name. Excluding them is not widening the
|
|
500
500
|
# rule; it is the `.tmp1` case again, for files Plot itself dropped there.
|
|
501
501
|
#
|
|
502
|
+
# NOR IS AN UNTRACKED ROOT `PLOT-CORRECTION.md`. `plot-worker-loop.sh`'s
|
|
503
|
+
# `write_correction` writes it untracked into the desk for the agent to read,
|
|
504
|
+
# and `reset_desk` removes it when the desk takes the next slice. A desk whose
|
|
505
|
+
# only other content is that file holds no unlanded work, so the loop waits for
|
|
506
|
+
# the checks rather than ending `holding-work`. The match is the whole
|
|
507
|
+
# porcelain line `?? PLOT-CORRECTION.md`, the line `plot-desk-dirt.sh`'s
|
|
508
|
+
# `desk_dirt` drops: a `docs/PLOT-CORRECTION.md`, or a staged, modified or
|
|
509
|
+
# deleted copy, is content and counts.
|
|
510
|
+
#
|
|
502
511
|
# THE EXCLUSION STAYS NARROW OTHERWISE, by suffix and by Plot's own filenames.
|
|
503
512
|
# An uncommitted source file is precisely the case this detection exists for, so
|
|
504
513
|
# anything broader — "untracked files do not count", "only tracked changes
|
|
@@ -565,7 +574,7 @@ plot_worker_dirty_filter() { # $1=`git status --porcelain` output $2=worktree (o
|
|
|
565
574
|
# `--porcelain` is the STABLE format; `git status` prose is localised and
|
|
566
575
|
# reflows. Cut at column 4: the first three bytes are the XY status pair and a
|
|
567
576
|
# space, and a filename can contain spaces of its own.
|
|
568
|
-
printf '%s' "$status" \
|
|
577
|
+
printf '%s' "$status" | grep -vxF '?? PLOT-CORRECTION.md' \
|
|
569
578
|
| cut -c4- \
|
|
570
579
|
| grep -vE "(^|/)$PLOT_WORKER_RECORD" \
|
|
571
580
|
| grep -vE "$PLOT_EDITOR_LEFTOVER" \
|
|
@@ -829,29 +838,6 @@ plot_worker_limited_reset() { # $1=worktree → epoch seconds | ""
|
|
|
829
838
|
printf '%s' "$reset"
|
|
830
839
|
}
|
|
831
840
|
|
|
832
|
-
# Has THIS worker's conversation written yet? → 0 spoken | 1 unspoken | 2 no handle
|
|
833
|
-
#
|
|
834
|
-
# THE DESK-WIDE NUMBER CANNOT SAY. After a hop to a new branch the loop mints a
|
|
835
|
-
# fresh handle, and the new conversation has no transcript file until its first
|
|
836
|
-
# line. Until then the desk's newest file is the PREVIOUS slice's, and its
|
|
837
|
-
# silence is not this worker's. So the watcher asks the loop's own probe with
|
|
838
|
-
# the loop's own handle: one probe, two readers, one answer.
|
|
839
|
-
#
|
|
840
|
-
# THREE ANSWERS, AND THE THIRD IS NOT THE SECOND. `0` the handle's file exists,
|
|
841
|
-
# `1` it does not, `2` there is no handle to ask about. `plot_transcript_exists`
|
|
842
|
-
# reads *no handle* as *no file*, which suits `session_flag`; here it would
|
|
843
|
-
# make a hand-started watcher read every quiet worker as unspoken and disable
|
|
844
|
-
# `idle` silently (#1074). So the handle is checked here, before the probe.
|
|
845
|
-
plot_worker_conversation_spoken() { # $1=worktree → 0 spoken | 1 unspoken | 2 no handle
|
|
846
|
-
command -v session_handle >/dev/null 2>&1 || return 2
|
|
847
|
-
command -v plot_transcript_exists >/dev/null 2>&1 || return 2
|
|
848
|
-
local handle
|
|
849
|
-
handle=$(session_handle) || return 2
|
|
850
|
-
[ -n "$handle" ] || return 2
|
|
851
|
-
plot_transcript_exists "$1" "$handle" && return 0
|
|
852
|
-
return 1
|
|
853
|
-
}
|
|
854
|
-
|
|
855
841
|
# Are there commits on this branch yet? → 0 yes | 1 no | 2 unanswerable
|
|
856
842
|
#
|
|
857
843
|
# THE THIRD CONDITION ON `idle`, and the one that separates a stall from an
|
|
@@ -887,145 +873,6 @@ plot_worker_has_commits() { # $1=worktree → 0 yes | 1 no | 2 unanswerable
|
|
|
887
873
|
return 1
|
|
888
874
|
}
|
|
889
875
|
|
|
890
|
-
json_escape() { # $1 = raw → prints a JSON-safe string body
|
|
891
|
-
printf '%s' "$1" | python3 -c 'import json,sys; sys.stdout.write(json.dumps(sys.stdin.read())[1:-1])' 2>/dev/null \
|
|
892
|
-
|| printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
# Append one finding line to a desk's findings file, in the WorkerMonitor's own
|
|
896
|
-
# shape — the board's reader keys on this exact field set and this exact
|
|
897
|
-
# monitor name, and changing either would make an unbroken channel look broken.
|
|
898
|
-
#
|
|
899
|
-
# `since` AND `measuredAt` ARE DIFFERENT TIMES. `measuredAt` is when this
|
|
900
|
-
# reading was taken; `since` is when the finding first held. A finding that has
|
|
901
|
-
# held for twenty minutes and one taken twenty minutes ago are not the same
|
|
902
|
-
# fact, and an operator triaging a board needs the first.
|
|
903
|
-
plot_worker_publish_finding() { # $1=file $2=branch $3=worktree $4=finding $5=evidence $6=since
|
|
904
|
-
local file="$1" branch="$2" worktree="$3" finding="$4" evidence="$5" since="$6" now line
|
|
905
|
-
now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
906
|
-
line=$(printf '{"monitor":"%s","branch":"%s","worktree":"%s","finding":"%s","since":"%s","evidence":"%s","measuredAt":"%s"}' \
|
|
907
|
-
'WorkerMonitor' \
|
|
908
|
-
"$(json_escape "$branch")" \
|
|
909
|
-
"$(json_escape "$worktree")" \
|
|
910
|
-
"$(json_escape "$finding")" \
|
|
911
|
-
"${since:-$now}" \
|
|
912
|
-
"$(json_escape "$evidence")" \
|
|
913
|
-
"$now")
|
|
914
|
-
[ -n "$file" ] && printf '%s\n' "$line" >> "$file" 2>/dev/null
|
|
915
|
-
printf 'plot-watch %s\n' "$line"
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
# ONE PASS OF THE LOOP'S OWN WATCHER: take the six readings, ask
|
|
919
|
-
# `plot_worker_idle_now`, publish only on a change.
|
|
920
|
-
#
|
|
921
|
-
# THE PID IS ALWAYS `alive`. The caller is the loop's watcher subshell, started
|
|
922
|
-
# with the loop's own pid (`$_watch_loop_pid`); that pid is alive by
|
|
923
|
-
# construction for as long as the watcher runs, so there is no `monitor_pid_alive`
|
|
924
|
-
# reading here the way the old monitor needed one for a SEPARATE process it was
|
|
925
|
-
# watching.
|
|
926
|
-
#
|
|
927
|
-
# `$5` IS THE RACE THE PLAN DID NOT ANTICIPATE. A prompt started into a
|
|
928
|
-
# conversation whose transcript is already older than the window could read
|
|
929
|
-
# `idle` on its FIRST pass and be ended before the model ever answers — the
|
|
930
|
-
# clamp mirrors the usage-limit clamp's own shape: `silenceSeconds` is capped at
|
|
931
|
-
# the seconds the prompt has actually been running, so a desk cannot be judged
|
|
932
|
-
# quiet for longer than this prompt has existed.
|
|
933
|
-
#
|
|
934
|
-
# THE CHILD IS SAMPLED ON THE PID GIVEN, NEVER ON `$_watch_loop_pid` ITSELF.
|
|
935
|
-
# `plot_worker_activity` sums the pid's whole descendant subtree; the loop's own
|
|
936
|
-
# pid is the root the agent CLI hangs off, so the caller passes it explicitly
|
|
937
|
-
# rather than this function assuming which pid names the subject.
|
|
938
|
-
#
|
|
939
|
-
# PUBLISH AND SIGNAL ARE SEPARATE. This function only ever publishes; it is the
|
|
940
|
-
# caller's job to decide whether the PUBLISHED finding may end the worker — the
|
|
941
|
-
# flag gates that decision, not this one.
|
|
942
|
-
plot_worker_idle_watch_pass() { # $1=worktree $2=branch $3=findings-file $4=window $5=prompt_started_at $6=pid → exit 0 idle | 1 silent (also publishes)
|
|
943
|
-
local wt="$1" branch="$2" file="$3" window="$4" started_at="$5" pid="$6"
|
|
944
|
-
local silence spoken_rc spoken tree commits_rc commits activity finding evidence verdict
|
|
945
|
-
|
|
946
|
-
silence=$(plot_transcript_quiet_seconds "$wt" 2>/dev/null)
|
|
947
|
-
case "$silence" in
|
|
948
|
-
''|unavailable|*[!0-9]*) silence='' ;;
|
|
949
|
-
esac
|
|
950
|
-
|
|
951
|
-
# THE USAGE-LIMIT CLAMP, AHEAD OF THE WINDOW CHECK. Silence is measured from
|
|
952
|
-
# the later of the newest transcript line and the reset this desk waits for;
|
|
953
|
-
# while the reset is ahead of now the difference is negative, which clamps to
|
|
954
|
-
# 0 and reads as busy — the agent is doing exactly what it should.
|
|
955
|
-
local limited_until
|
|
956
|
-
limited_until=$(plot_worker_limited_reset "$wt")
|
|
957
|
-
if [ -n "$limited_until" ] && [ -n "$silence" ]; then
|
|
958
|
-
local since_reset
|
|
959
|
-
since_reset=$(( $(date +%s) - limited_until ))
|
|
960
|
-
[ "$since_reset" -lt 0 ] && since_reset=0
|
|
961
|
-
[ "$since_reset" -lt "$silence" ] && silence=$since_reset
|
|
962
|
-
fi
|
|
963
|
-
|
|
964
|
-
# THE RACE CLAMP. A prompt's transcript may be older than the window on the
|
|
965
|
-
# very first pass, because it is the PREVIOUS slice's silence, not this
|
|
966
|
-
# prompt's. Silence can never exceed how long this prompt has actually run.
|
|
967
|
-
case "$started_at" in
|
|
968
|
-
''|*[!0-9]*) ;;
|
|
969
|
-
*)
|
|
970
|
-
local ran
|
|
971
|
-
ran=$(( $(date +%s) - started_at ))
|
|
972
|
-
[ "$ran" -lt 0 ] && ran=0
|
|
973
|
-
if [ -n "$silence" ] && [ "$ran" -lt "$silence" ]; then silence=$ran; fi
|
|
974
|
-
;;
|
|
975
|
-
esac
|
|
976
|
-
|
|
977
|
-
plot_worker_conversation_spoken "$wt"; spoken_rc=$?
|
|
978
|
-
case "$spoken_rc" in
|
|
979
|
-
0) spoken=1 ;;
|
|
980
|
-
*) spoken=0 ;;
|
|
981
|
-
esac
|
|
982
|
-
|
|
983
|
-
activity=$(plot_worker_activity "$pid" 2>/dev/null)
|
|
984
|
-
|
|
985
|
-
tree=$(plot_worker_tree_quiet_seconds "$wt" 2>/dev/null)
|
|
986
|
-
|
|
987
|
-
plot_worker_has_commits "$wt"; commits_rc=$?
|
|
988
|
-
case "$commits_rc" in
|
|
989
|
-
0) commits='yes' ;;
|
|
990
|
-
1) commits='no' ;;
|
|
991
|
-
*) commits='unanswerable' ;;
|
|
992
|
-
esac
|
|
993
|
-
|
|
994
|
-
verdict=$(plot_worker_idle_now 'alive' "$spoken" "${silence:-unavailable}" "$activity" "$tree" "$commits" "$window")
|
|
995
|
-
|
|
996
|
-
finding=''
|
|
997
|
-
evidence=''
|
|
998
|
-
if [ "$verdict" = 'idle' ]; then
|
|
999
|
-
finding='idle'
|
|
1000
|
-
evidence="the agent's transcript has been silent for over ${window}s with no child process burning CPU behind it, nothing in its tree has moved for ${tree}s, and the branch already carries commits"
|
|
1001
|
-
fi
|
|
1002
|
-
|
|
1003
|
-
# PUBLISH ONLY ON A CHANGE, held in a variable of the CALLER's subshell —
|
|
1004
|
-
# named `PLOT_WATCH_PUBLISHED`/`PLOT_WATCH_SINCE` rather than local, because
|
|
1005
|
-
# this function is called repeatedly from the watcher's own `while` loop and
|
|
1006
|
-
# the state must survive between calls the way `monitor_pass`'s did.
|
|
1007
|
-
if [ "$finding" != "${PLOT_WATCH_PUBLISHED:-}" ]; then
|
|
1008
|
-
local now_iso
|
|
1009
|
-
now_iso=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
1010
|
-
if [ -n "$finding" ]; then
|
|
1011
|
-
PLOT_WATCH_SINCE="$now_iso"
|
|
1012
|
-
plot_worker_publish_finding "$file" "$branch" "$wt" "$finding" "$evidence" "$PLOT_WATCH_SINCE"
|
|
1013
|
-
elif [ -n "${PLOT_WATCH_PUBLISHED:-}" ]; then
|
|
1014
|
-
PLOT_WATCH_SINCE="$now_iso"
|
|
1015
|
-
plot_worker_publish_finding "$file" "$branch" "$wt" 'clear' \
|
|
1016
|
-
"the ${PLOT_WATCH_PUBLISHED} finding no longer holds; the worker is measuring healthy again" "$PLOT_WATCH_SINCE"
|
|
1017
|
-
fi
|
|
1018
|
-
PLOT_WATCH_PUBLISHED="$finding"
|
|
1019
|
-
fi
|
|
1020
|
-
|
|
1021
|
-
# THE VERDICT IS THE EXIT CODE, NOT STDOUT. Stdout is reserved for the
|
|
1022
|
-
# publish line alone (`plot_worker_publish_finding`'s own `plot-watch …`
|
|
1023
|
-
# line), the same convention the old monitor's process stdout carried into
|
|
1024
|
-
# `.plot-worker.log` beside the agent's own output. A caller that needs the
|
|
1025
|
-
# word reads the exit code: 0 for `idle`, 1 for anything else.
|
|
1026
|
-
[ "$finding" = 'idle' ]
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
876
|
# The total CPU time, in centiseconds, of a pid and every process descended from
|
|
1030
877
|
# it. Prints the number; prints `0` and returns non-zero when the pid names no
|
|
1031
878
|
# live process at all.
|