@erclx/canon 5.14.0 → 5.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.
@@ -1,181 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Reports worker and pull request transitions as they happen. Reads only.
3
- #
4
- # The poll answers "what moved since the last run" on a schedule the session
5
- # holds. This answers "what just changed" on a loop of its own, and it exists
6
- # because the two readings a dispatch needs are not one reading: a worker that
7
- # finishes goes idle and a worker that crashes vanishes, so a watch matching
8
- # only the pull request list stays silent through the second. A third case is
9
- # neither: a worker that stops on a question or a prompt neither finishes nor
10
- # crashes, and sits reading "waiting" beside the two statuses that resolve on
11
- # their own until something reports it, which is the WORKER-STOPPED line
12
- # below, or WORKER-UNMEASURABLE for a "waiting" row too old to carry a stamp
13
- # this reads.
14
- #
15
- # `set -e` is deliberately not set. This runs for hours, and one transient `gh`
16
- # failure aborting the loop is a silent stop rather than a reported one. Every
17
- # read below states its own failure instead.
18
- set -uo pipefail
19
-
20
- # Seconds between passes. A pass costs two cheap reads and the transitions it
21
- # watches take minutes, so the number trades staleness against little. Raise it
22
- # in a project whose workers run long.
23
- INTERVAL=60
24
-
25
- # Seconds a worker can sit in "waiting" before this reports it as stopped
26
- # rather than folding it into the ordinary status-change lines below. "busy"
27
- # and "idle" resolve on their own; "waiting" does not, so a dwell that keeps
28
- # growing there is a session blocked on something outside itself.
29
- #
30
- # Measured against the live session registry: 341 usable records carried a
31
- # status at all, and exactly one carried "waiting", too sparse a sample to fit
32
- # a distribution. The number is picked from the two bounds the measurement can
33
- # still name rather than from a round guess: well above INTERVAL, so a handful
34
- # of passes confirm the row before it reports rather than one slow tool call
35
- # tripping it, and well inside the ten-to-thirty-minute span a dispatched build
36
- # ordinarily runs, so a real stall is caught with most of that window still
37
- # open to act on it.
38
- #
39
- # That one record carried `waitingFor: "approve Bash"`, a permission prompt
40
- # rather than a question, and the two clear on different schedules: a prompt
41
- # resolves the moment a person approves it, where a question can sit
42
- # legitimately while a controller finishes a turn. This number is defensible
43
- # for the second and generous for the first, and the gap stays open rather
44
- # than splitting the constant on a sample of one.
45
- STALL_THRESHOLD_S=300
46
-
47
- # Resolving the main worktree root rather than this file's folder keeps a watch
48
- # started from a linked worktree reading the same repository as one started from
49
- # main. The repository field on a roster row is that root's `.git`.
50
- MAIN_ROOT="$(git worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')"
51
- if [ -z "$MAIN_ROOT" ]; then
52
- echo "watch: not a git repository, so nothing is watched" >&2
53
- exit 1
54
- fi
55
- REPOSITORY="$MAIN_ROOT/.git"
56
-
57
- # The base branch is read rather than assumed, since this ships to projects that
58
- # do not all call it `main`. It is what separates a worker from the controlling
59
- # session, which sits on the base branch in the main worktree.
60
- BASE_REF="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)"
61
- if [ -z "$BASE_REF" ]; then
62
- BASE_REF="origin/$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name 2>/dev/null || echo main)"
63
- fi
64
- BASE_BRANCH="${BASE_REF#origin/}"
65
-
66
- # Every session in this repository holding a branch other than the base one is a
67
- # worker, whoever launched it. The prototype matched the `orchestrator-` prefix
68
- # instead, which reads a dispatched worker and misses every hand-launched one,
69
- # and those are the ordinary shape whenever the operator is launching.
70
- #
71
- # The row goes out tab-separated and comes back read with IFS set to a tab. A
72
- # name this client actually writes carries spaces, `Update session markdown
73
- # and check runnable commands (3)` among them, and splitting on whitespace
74
- # took that one apart into a wrong name, branch, status, and dwell.
75
- read_workers() {
76
- canon sessions list --json 2>/dev/null | tail -1 | jq -r \
77
- --arg repository "$REPOSITORY" --arg base "$BASE_BRANCH" '
78
- .sessions[]?
79
- | select(.repository == $repository)
80
- | select(.branch != null and .branch != $base)
81
- | [.name, .branch, .status, (.statusDwellMs // -1 | tostring)]
82
- | @tsv
83
- ' 2>/dev/null | sort
84
- }
85
-
86
- read_pulls() {
87
- gh pr list --state open --json number,headRefName,title --jq \
88
- '.[] | "PR-OPEN #\(.number) \(.headRefName) \(.title)"' 2>/dev/null | sort
89
- }
90
-
91
- # A failed read is reported and the previous baseline kept. Reading an empty
92
- # result as the current state would report every worker gone and every pull
93
- # request new on the pass after, which is the one failure that makes a detection
94
- # tool worth less than no tool.
95
- #
96
- # The two sources carry a baseline flag apiece rather than sharing one. A read
97
- # that fails on the first pass leaves its baseline empty while the other source
98
- # establishes one, and a single flag would then report that source's whole list
99
- # as new on the pass after it recovers.
100
- prev_pulls=""
101
- prev_workers=""
102
- prev_names=""
103
- pulls_seen=0
104
- workers_seen=0
105
-
106
- # One row per worker name currently reported, waiting past STALL_THRESHOLD_S
107
- # or waiting with no stamp to measure, so a reader watching the log is told
108
- # once rather than on every remaining pass. The prototype for that shape is
109
- # the WORKER-GONE/WORKER pair above, tracked in the same state a status change
110
- # is: a name drops out the pass it stops reading "waiting", which lets the
111
- # same worker report a second stall later in its life rather than being
112
- # marked forever by its first one.
113
- declare -A stalled
114
-
115
- while true; do
116
- pulls="$(read_pulls)"
117
- pulls_read=$?
118
- workers="$(read_workers)"
119
- workers_read=$?
120
-
121
- if [ "$pulls_read" -ne 0 ]; then
122
- echo "watch: the open pull request list failed to load, so none is classified this pass"
123
- fi
124
- if [ "$workers_read" -ne 0 ]; then
125
- echo "watch: the session roster failed to load, so no worker is classified this pass"
126
- fi
127
-
128
- names="$(printf '%s\n' "$workers" | awk -F'\t' 'NF {print $1}' | sort -u)"
129
-
130
- if [ "$pulls_seen" -eq 1 ] && [ "$pulls_read" -eq 0 ]; then
131
- comm -13 <(printf '%s\n' "$prev_pulls") <(printf '%s\n' "$pulls") | grep . || true
132
- fi
133
-
134
- if [ "$workers_seen" -eq 1 ] && [ "$workers_read" -eq 0 ]; then
135
- comm -13 <(printf '%s\n' "$prev_workers") <(printf '%s\n' "$workers") |
136
- grep . | sed 's/^/WORKER /' || true
137
- comm -23 <(printf '%s\n' "$prev_names") <(printf '%s\n' "$names") |
138
- grep . | sed 's/^/WORKER-GONE /' || true
139
- fi
140
-
141
- if [ "$workers_read" -eq 0 ]; then
142
- while IFS=$'\t' read -r w_name w_branch w_status w_dwell_ms; do
143
- [ -z "$w_name" ] && continue
144
-
145
- if [ "$w_status" != "waiting" ]; then
146
- unset "stalled[$w_name]"
147
- continue
148
- fi
149
-
150
- if [ "$w_dwell_ms" = "-1" ]; then
151
- if [ -z "${stalled[$w_name]:-}" ]; then
152
- echo "WORKER-UNMEASURABLE $w_name $w_branch"
153
- stalled[$w_name]=1
154
- fi
155
- continue
156
- fi
157
-
158
- w_dwell_s=$((w_dwell_ms / 1000))
159
- if [ "$w_dwell_s" -ge "$STALL_THRESHOLD_S" ]; then
160
- if [ -z "${stalled[$w_name]:-}" ]; then
161
- echo "WORKER-STOPPED $w_name $w_branch ${w_dwell_s}s"
162
- stalled[$w_name]=1
163
- fi
164
- else
165
- unset "stalled[$w_name]"
166
- fi
167
- done <<<"$workers"
168
- fi
169
-
170
- if [ "$pulls_read" -eq 0 ]; then
171
- prev_pulls="$pulls"
172
- pulls_seen=1
173
- fi
174
- if [ "$workers_read" -eq 0 ]; then
175
- prev_workers="$workers"
176
- prev_names="$names"
177
- workers_seen=1
178
- fi
179
-
180
- sleep "$INTERVAL"
181
- done