tickmarkr 2.6.5 → 2.6.7
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/README.md +7 -3
- package/dist/adapters/claude-code.js +8 -2
- package/dist/adapters/codex-commit-check.d.ts +16 -4
- package/dist/adapters/codex-commit-check.js +150 -26
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +20 -8
- package/dist/adapters/types.d.ts +4 -1
- package/dist/cli/commands/beat.d.ts +6 -1
- package/dist/cli/commands/beat.js +123 -76
- package/dist/cli/commands/doctor.js +3 -1
- package/dist/cli/commands/fleet.js +368 -85
- package/dist/cli/commands/init.js +23 -9
- package/dist/cli/commands/plan.js +12 -1
- package/dist/cli/commands/verify.js +37 -9
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +10 -5
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/config/fleet-overlay.d.ts +14 -0
- package/dist/config/fleet-overlay.js +154 -25
- package/dist/drivers/orca.d.ts +17 -3
- package/dist/drivers/orca.js +64 -20
- package/dist/gates/baseline.d.ts +9 -0
- package/dist/gates/baseline.js +14 -3
- package/dist/gates/cache.js +1 -1
- package/dist/gates/review.js +15 -0
- package/dist/gates/run-gates.d.ts +20 -7
- package/dist/gates/run-gates.js +345 -210
- package/dist/gates/test-manifest.d.ts +11 -0
- package/dist/gates/test-manifest.js +151 -111
- package/dist/gates/timeout-shaped.d.ts +3 -0
- package/dist/gates/timeout-shaped.js +10 -0
- package/dist/run/beat-lifecycle.d.ts +154 -0
- package/dist/run/beat-lifecycle.js +825 -0
- package/dist/run/daemon.js +202 -126
- package/dist/run/git-trust.d.ts +27 -0
- package/dist/run/git-trust.js +178 -0
- package/dist/run/git.js +34 -14
- package/dist/run/journal.js +15 -3
- package/dist/run/lease.d.ts +23 -0
- package/dist/run/lease.js +232 -38
- package/dist/run/merge.js +2 -2
- package/dist/run/stall.js +20 -1
- package/dist/run/supervision.d.ts +8 -0
- package/dist/run/supervision.js +24 -4
- package/dist/run/verification-job.d.ts +51 -0
- package/dist/run/verification-job.js +137 -0
- package/dist/tui/ink/fleet-app.d.ts +35 -5
- package/dist/tui/ink/fleet-app.js +244 -124
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +19 -10
- package/skills/tickmarkr-loop/SKILL.md +19 -10
- package/skills/tickmarkr-overseer/SKILL.md +25 -16
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +58 -16
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +19 -6
- package/skills/tickmarkr-overseer/scripts/watch-parks.sh +121 -64
|
@@ -113,16 +113,25 @@ For `resume`, use the same host-owned split form with `tickmarkr resume <runId>`
|
|
|
113
113
|
in the process table to the host PTY; verify no agent session is an ancestor. If an agent is
|
|
114
114
|
an ancestor, stop that launch and relaunch through the host split before continuing.
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
116
|
+
Detached beats are product-owned, one per (tier, seat), from the repository root, through the
|
|
117
|
+
shipped lifecycle verbs — never a hand-rolled setsid plus nohup wrapper, pidfile or shell loop:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
cd <repo> && tickmarkr beat start <tier> --seat <seat>
|
|
121
|
+
cd <repo> && tickmarkr beat status <tier> --seat <seat>
|
|
122
|
+
cd <repo> && tickmarkr beat stop <tier> --seat <seat>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`start` launches the `--loop` child in its own session, logs to `<state-dir>/supervision/<tier>.log`,
|
|
126
|
+
records its pid, birth, argv and cwd, and exits 0 only after it reads back that exact child's own
|
|
127
|
+
advancing beat. A repeat start for the same (tier, seat) is an "already running" no-op; a stale,
|
|
128
|
+
dead, foreign, reused-pid, unreadable or busy tier is a nonzero refusal that changes nothing.
|
|
129
|
+
Verify ppid 1 is never proof on its own — an orphan of a dying session shows it too; `status`
|
|
130
|
+
is the check: it reads the recorded process identity and beat freshness, writes nothing, and exits
|
|
131
|
+
nonzero for STALE, MISMATCH or UNREADABLE. `stop` signals only that recorded pid after its
|
|
132
|
+
generation and identity still match, and reads back DISARMED. Never put beats in a harness
|
|
133
|
+
background task or a visible tab. Include a run-end `beat stop` item for every beat started, also
|
|
134
|
+
on failure, park or handoff, and require its DISARMED read-back.
|
|
126
135
|
|
|
127
136
|
Keep only watchers that must WAKE the seat in the harness. Re-arm them on each wake and at
|
|
128
137
|
their harness cap; daemon and beat lifetimes must not depend on that cap.
|
|
@@ -108,16 +108,25 @@ For `resume`, use the same host-owned split form with `tickmarkr resume <runId>`
|
|
|
108
108
|
in the process table to the host PTY; verify no agent session is an ancestor. If an agent is
|
|
109
109
|
an ancestor, stop that launch and relaunch through the host split before continuing.
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
Detached beats are product-owned, one per (tier, seat), from the repository root, through the
|
|
112
|
+
shipped lifecycle verbs — never a hand-rolled setsid plus nohup wrapper, pidfile or shell loop:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
cd <repo> && tickmarkr beat start <tier> --seat <seat>
|
|
116
|
+
cd <repo> && tickmarkr beat status <tier> --seat <seat>
|
|
117
|
+
cd <repo> && tickmarkr beat stop <tier> --seat <seat>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`start` launches the `--loop` child in its own session, logs to `<state-dir>/supervision/<tier>.log`,
|
|
121
|
+
records its pid, birth, argv and cwd, and exits 0 only after it reads back that exact child's own
|
|
122
|
+
advancing beat. A repeat start for the same (tier, seat) is an "already running" no-op; a stale,
|
|
123
|
+
dead, foreign, reused-pid, unreadable or busy tier is a nonzero refusal that changes nothing.
|
|
124
|
+
Verify ppid 1 is never proof on its own — an orphan of a dying session shows it too; `status`
|
|
125
|
+
is the check: it reads the recorded process identity and beat freshness, writes nothing, and exits
|
|
126
|
+
nonzero for STALE, MISMATCH or UNREADABLE. `stop` signals only that recorded pid after its
|
|
127
|
+
generation and identity still match, and reads back DISARMED. Never put beats in a harness
|
|
128
|
+
background task or a visible tab. Include a run-end `beat stop` item for every beat started, also
|
|
129
|
+
on failure, park or handoff, and require its DISARMED read-back.
|
|
121
130
|
|
|
122
131
|
Keep only watchers that must WAKE the seat in the harness. Re-arm them on each wake and at
|
|
123
132
|
their harness cap; daemon and beat lifetimes must not depend on that cap.
|
|
@@ -326,16 +326,25 @@ For `resume`, use the same host-owned split form with `tickmarkr resume <runId>`
|
|
|
326
326
|
in the process table to the host PTY; verify no agent session is an ancestor. If an agent is
|
|
327
327
|
an ancestor, stop that launch and relaunch through the host split before continuing.
|
|
328
328
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
329
|
+
Detached beats are product-owned, one per (tier, seat), from the repository root, through the
|
|
330
|
+
shipped lifecycle verbs — never a hand-rolled setsid plus nohup wrapper, pidfile or shell loop:
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
cd <repo> && tickmarkr beat start <tier> --seat <seat>
|
|
334
|
+
cd <repo> && tickmarkr beat status <tier> --seat <seat>
|
|
335
|
+
cd <repo> && tickmarkr beat stop <tier> --seat <seat>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
`start` launches the `--loop` child in its own session, logs to `<state-dir>/supervision/<tier>.log`,
|
|
339
|
+
records its pid, birth, argv and cwd, and exits 0 only after it reads back that exact child's own
|
|
340
|
+
advancing beat. A repeat start for the same (tier, seat) is an "already running" no-op; a stale,
|
|
341
|
+
dead, foreign, reused-pid, unreadable or busy tier is a nonzero refusal that changes nothing.
|
|
342
|
+
Verify ppid 1 is never proof on its own — an orphan of a dying session shows it too; `status`
|
|
343
|
+
is the check: it reads the recorded process identity and beat freshness, writes nothing, and exits
|
|
344
|
+
nonzero for STALE, MISMATCH or UNREADABLE. `stop` signals only that recorded pid after its
|
|
345
|
+
generation and identity still match, and reads back DISARMED. Never put beats in a harness
|
|
346
|
+
background task or a visible tab. Include a run-end `beat stop` item for every beat started, also
|
|
347
|
+
on failure, park or handoff, and require its DISARMED read-back.
|
|
339
348
|
|
|
340
349
|
Keep only watchers that must WAKE the seat in the harness. Re-arm them on each wake and at
|
|
341
350
|
their harness cap; daemon and beat lifetimes must not depend on that cap.
|
|
@@ -1166,14 +1175,14 @@ watching it. Two thirds of that line were constants, not measurements.
|
|
|
1166
1175
|
|
|
1167
1176
|
The beat is one shipped command and it arms through two explicit verbs, `--new-arm` and `--loop` —
|
|
1168
1177
|
never a bare invocation. `--loop` already implies `--new-arm`: it creates a durable arm and beats in
|
|
1169
|
-
this process every 10 seconds, exiting
|
|
1170
|
-
|
|
1171
|
-
|
|
1178
|
+
this process every 10 seconds, exiting nonzero with its reason within one interval after a stand-down.
|
|
1179
|
+
Never launch it by hand: `beat start` runs it detached and owns it, following the start/stop/status
|
|
1180
|
+
lifecycle in Host-owned daemon and detached beats; never as a harness task or visible tab:
|
|
1172
1181
|
|
|
1173
1182
|
```bash
|
|
1174
|
-
#
|
|
1175
|
-
cd <repo> && tickmarkr beat overseer --seat <overseer-agent-or-pane>
|
|
1176
|
-
cd <repo> && tickmarkr beat overseer --seat <overseer-agent-or-pane>
|
|
1183
|
+
# beat start owns the detached --loop child; do not execute the loop as a foreground or harness task.
|
|
1184
|
+
cd <repo> && tickmarkr beat start overseer --seat <overseer-agent-or-pane>
|
|
1185
|
+
cd <repo> && tickmarkr beat stop overseer --seat <overseer-agent-or-pane> # deliberately hand off; reads back DISARMED
|
|
1177
1186
|
```
|
|
1178
1187
|
|
|
1179
1188
|
**The legacy wrapper loop, `while :; do tickmarkr beat overseer --seat <pane>; sleep 10; done`, is the
|
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# classify-vitest-log.sh <log> — the ONE strict Vitest log classifier
|
|
3
|
-
# wrapper (scripts/run-ci-vitest.sh) and grade-ci.sh both call it, so the badge
|
|
4
|
-
# disagree. Reads a raw Vitest log or a gh job log (gh's "job<TAB>step<TAB>timestamp "
|
|
5
|
-
# styling — the raw ESC byte and gh's `^[` rendering of it — are stripped) and prints ONE line:
|
|
6
|
-
# VITEST_LOG verdict=<CLEAN|RPC_ONLY|RED> reason=<why> summaries=S passed=P skipped=K failed=F
|
|
7
|
-
# timedout=T unhandled=U runner_rpc_timeouts=R coverage_misses=C signals=G terminal_errors=E
|
|
8
|
-
# unhandled counts the unhandled errors that are NOT runner RPC timeouts. Exit 0 CLEAN or RPC_ONLY, 1 RED
|
|
2
|
+
# classify-vitest-log.sh <log> [expected-files] — the ONE strict Vitest log classifier
|
|
3
|
+
# (OBS-1184). The public CI wrapper (scripts/run-ci-vitest.sh) and grade-ci.sh both call it, so the badge
|
|
4
|
+
# and the grade cannot disagree. Reads a raw Vitest log or a gh job log (gh's "job<TAB>step<TAB>timestamp "
|
|
5
|
+
# prefix and ANSI styling — the raw ESC byte and gh's `^[` rendering of it — are stripped) and prints ONE line:
|
|
6
|
+
# VITEST_LOG verdict=<CLEAN|RPC_ONLY|RED|INFRA_KILLED> reason=<why> summaries=S passed=P skipped=K failed=F
|
|
7
|
+
# timedout=T unhandled=U runner_rpc_timeouts=R coverage_misses=C signals=G terminal_errors=E runner_kills=X
|
|
8
|
+
# unhandled counts the unhandled errors that are NOT runner RPC timeouts. Exit 0 CLEAN or RPC_ONLY, 1 RED,
|
|
9
|
+
# 3 INFRA_KILLED.
|
|
10
|
+
#
|
|
11
|
+
# INFRA_KILLED (D-897): the GitHub runner's own shutdown or cancellation annotation ("##[error]The runner
|
|
12
|
+
# has received a shutdown signal…" / "##[error]The operation was canceled.") with no failed or timed-out
|
|
13
|
+
# test and no coverage-threshold miss. It is never green and never passes a gate — the proof was not
|
|
14
|
+
# finished — but it is not a test red either. Those failures dominate a kill (RED); a kill excuses only
|
|
15
|
+
# the cut-off or short log it leaves behind (runner_kills counts the annotations either way). A failure
|
|
16
|
+
# the reporter already printed before the kill cut its summary off counts too: a dot-reporter "x" (its
|
|
17
|
+
# stream has no newline, so a test's "stdout | …"/"stderr | …" header may finish the same line), a
|
|
18
|
+
# default-reporter "×" or failed module record "❯ file (N tests…)" (❯ marks a failed module even with
|
|
19
|
+
# no failed test — a collect or hook error), a " FAIL " badge or the "Failed Tests/Suites N" banner (only
|
|
20
|
+
# beside a kill — without one, the complete summaries' own counts decide). Only one printed after the last
|
|
21
|
+
# complete summary counts: a complete summary's own counts settle every line before it. Test output never
|
|
22
|
+
# counts: a console block runs from its header until the reporter speaks again — a dot run, a ⎯ banner or
|
|
23
|
+
# a default-reporter module record (✓/❯/↓ file (N tests…)) right after a blank line, or the next summary —
|
|
24
|
+
# so blank lines inside a test's output do not end it.
|
|
25
|
+
# ponytail: a test that prints, after a blank line of its own, a line the reporter could have printed is
|
|
26
|
+
# indistinguishable from the reporter; only that deliberate forgery escapes the block.
|
|
27
|
+
#
|
|
28
|
+
# The declared contract (D-893), when a caller supplies one: expected-files is the number of test files
|
|
29
|
+
# the run must collect — the summaries' "(N)" totals must sum to it exactly. The caller derives it from
|
|
30
|
+
# the declared projects' own candidate-tree discovery, so a never-started declared project is a short
|
|
31
|
+
# count: project presence comes from that discovery, never from log text a test can print. A short count
|
|
32
|
+
# is RED reason=partial: a complete summary of the projects that happened to start proves nothing about
|
|
33
|
+
# the one that did not. With or without a contract, a summary whose finished files (failed + passed +
|
|
34
|
+
# skipped + todo) fall short of its own "(N)" is RED reason=partial too: Vitest's total counts the queued
|
|
35
|
+
# and running files an abort left unfinished.
|
|
9
36
|
#
|
|
10
37
|
# RED unless every Vitest summary is complete (Test Files, Tests and Duration lines, and every
|
|
11
38
|
# "Unhandled Errors" block closed by the summary that follows it — a cut-off log is RED), with no
|
|
@@ -29,21 +56,28 @@
|
|
|
29
56
|
set -u
|
|
30
57
|
|
|
31
58
|
log=${1:?log path required}
|
|
32
|
-
fields='summaries=0 passed=0 skipped=0 failed=0 timedout=0 unhandled=0 runner_rpc_timeouts=0 coverage_misses=0 signals=0 terminal_errors=0'
|
|
59
|
+
fields='summaries=0 passed=0 skipped=0 failed=0 timedout=0 unhandled=0 runner_rpc_timeouts=0 coverage_misses=0 signals=0 terminal_errors=0 runner_kills=0'
|
|
33
60
|
if [ ! -s "$log" ] || [ ! -r "$log" ]; then
|
|
34
61
|
echo "VITEST_LOG verdict=RED reason=unreadable $fields"
|
|
35
62
|
exit 1
|
|
36
63
|
fi
|
|
37
64
|
|
|
38
65
|
esc=$(printf '\033')
|
|
39
|
-
awk -v esc="$esc" '
|
|
66
|
+
awk -v esc="$esc" -v expected="${2:-}" '
|
|
40
67
|
function num(s, word) { return match(s, "[0-9]+ " word) ? substr(s, RSTART, RLENGTH) + 0 : 0 }
|
|
41
68
|
function close_block() { rpc += (found < cap ? found : cap); armed = 0; pending = 0 }
|
|
42
69
|
{
|
|
43
70
|
line = $0
|
|
44
71
|
gsub(/\^\[\[[0-9;]*m/, "", line); gsub(esc "\\[[0-9;]*m", "", line)
|
|
45
72
|
sub(/^[^\t]*\t[^\t]*\t[0-9T:.-]+Z[ ]?/, "", line)
|
|
73
|
+
if (line ~ /^[ \t]*(·|x|\*|-)*(stdout|stderr) \| /) {
|
|
74
|
+
if (line ~ /^[ \t]*(·|\*|-)*x/) reported++
|
|
75
|
+
console = 1
|
|
76
|
+
} else if (after_blank && line ~ /^[ \t]*((·|x|\*|-)*(·|x)(·|x|\*|-)*|(⎯)+( .* (⎯)+)?|(✓|❯|↓) .* [(][0-9]+ tests?( [|] [0-9]+ (failed|skipped))*[)].*)[ \t]*$/) console = 0
|
|
77
|
+
if (!console && line ~ /^[ \t]*((·|\*|-)*x(·|x|\*|-)*|× .*|❯ .* [(][0-9]+ tests?( [|] [0-9]+ (failed|skipped))*[)].*|FAIL[ \t].*|(⎯)+ Failed (Tests|Suites) [0-9]+ (⎯)+)[ \t]*$/) reported++
|
|
78
|
+
after_blank = line !~ /[^ \t]/
|
|
46
79
|
}
|
|
80
|
+
line ~ /^##\[error\](The runner has received a shutdown signal|The operation was cancell?ed)/ { kills++ }
|
|
47
81
|
# The Vitest timeout message carries its millisecond count; a test that PRINTS a fingerprint normalized
|
|
48
82
|
# to #ms (the daemon OBS-1106 notifications) is output, not a timed-out test. A real timeout also fails
|
|
49
83
|
# its test, so the summary failed count still reds it.
|
|
@@ -59,13 +93,17 @@ awk -v esc="$esc" '
|
|
|
59
93
|
}
|
|
60
94
|
line ~ /^[ \t]*Test Files[ \t]/ {
|
|
61
95
|
if (armed) close_block()
|
|
62
|
-
summaries++; in_summary = 1; saw_tests = 0
|
|
96
|
+
summaries++; in_summary = 1; saw_tests = 0; console = 0
|
|
63
97
|
failed += num(line, "failed"); passed += num(line, "passed"); skipped += num(line, "skipped")
|
|
98
|
+
finished = num(line, "failed") + num(line, "passed") + num(line, "skipped") + num(line, "todo")
|
|
99
|
+
total = match(line, /\([0-9]+\)[ \t]*$/) ? substr(line, RSTART + 1) + 0 : -1
|
|
100
|
+
if (total >= 0) collected += total
|
|
101
|
+
if (finished != total) unfinished++
|
|
64
102
|
next
|
|
65
103
|
}
|
|
66
104
|
in_summary && line ~ /^[ \t]*Tests[ \t]/ { saw_tests = 1; failed_tests += num(line, "failed"); next }
|
|
67
105
|
in_summary && line ~ /^[ \t]*Errors[ \t]+[0-9]+ error/ { summary_errors += num(line, "error"); next }
|
|
68
|
-
in_summary && line ~ /^[ \t]*Duration[ \t]/ { if (saw_tests) complete++; in_summary = 0; next }
|
|
106
|
+
in_summary && line ~ /^[ \t]*Duration[ \t]/ { if (saw_tests) { complete++; reported = 0 }; in_summary = 0; next }
|
|
69
107
|
armed && line ~ /^(⎯)+ Unhandled Error (⎯)+[ \t]*$/ { pending = 1; next }
|
|
70
108
|
line ~ /^(⎯)+ (Unhandled|Startup|Collect) Error (⎯)+[ \t]*$/ { terminal++; next }
|
|
71
109
|
pending && line ~ /[^ \t]/ { pending = 0; if (line ~ /^Error: \[vitest-worker\]: Timeout calling "[A-Za-z]+"[ \t]*$/) found++ }
|
|
@@ -73,19 +111,23 @@ awk -v esc="$esc" '
|
|
|
73
111
|
unhandled = tally + untallied
|
|
74
112
|
if (summary_errors > unhandled) unhandled = summary_errors
|
|
75
113
|
other = unhandled - rpc
|
|
76
|
-
|
|
114
|
+
partial = unfinished || (expected != "" && collected != expected + 0)
|
|
115
|
+
# a kill always names a class: the failure that dominates it, or the kill itself
|
|
116
|
+
if (kills) reason = (failed || failed_tests || reported) ? "failed" : timedout ? "timed-out" : coverage ? "coverage-threshold" : "runner-shutdown"
|
|
117
|
+
else if (summaries == 0) reason = "no-summary"
|
|
77
118
|
else if (complete != summaries || opening || armed) reason = "truncated"
|
|
78
119
|
else if (failed || failed_tests) reason = "failed"
|
|
79
120
|
else if (timedout) reason = "timed-out"
|
|
121
|
+
else if (partial) reason = "partial"
|
|
80
122
|
else if (coverage) reason = "coverage-threshold"
|
|
81
123
|
else if (signals) reason = "signal"
|
|
82
124
|
else if (terminal) reason = "terminal-error"
|
|
83
125
|
else if (other > 0) reason = "unhandled-error"
|
|
84
126
|
else if (tally + untallied != summary_errors) reason = "unhandled-mismatch"
|
|
85
|
-
verdict = reason != "" ? "RED" : (unhandled > 0 ? "RPC_ONLY" : "CLEAN")
|
|
127
|
+
verdict = reason == "runner-shutdown" ? "INFRA_KILLED" : (reason != "" ? "RED" : (unhandled > 0 ? "RPC_ONLY" : "CLEAN"))
|
|
86
128
|
if (reason == "") reason = "none"
|
|
87
|
-
printf "VITEST_LOG verdict=%s reason=%s summaries=%d passed=%d skipped=%d failed=%d timedout=%d unhandled=%d runner_rpc_timeouts=%d coverage_misses=%d signals=%d terminal_errors=%d\n", \
|
|
88
|
-
verdict, reason, summaries, passed, skipped, failed, timedout, other, rpc, coverage, signals, terminal
|
|
89
|
-
exit (verdict == "RED")
|
|
129
|
+
printf "VITEST_LOG verdict=%s reason=%s summaries=%d passed=%d skipped=%d failed=%d timedout=%d unhandled=%d runner_rpc_timeouts=%d coverage_misses=%d signals=%d terminal_errors=%d runner_kills=%d\n", \
|
|
130
|
+
verdict, reason, summaries, passed, skipped, failed, timedout, other, rpc, coverage, signals, terminal, kills
|
|
131
|
+
exit (verdict == "INFRA_KILLED" ? 3 : verdict == "RED")
|
|
90
132
|
}
|
|
91
133
|
' "$log"
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# grade-ci.sh <run-id> <expected-count> [tag] — grade both public CI jobs from their JOB LOGS.
|
|
3
3
|
# Grade only at run-end. A missing/in-progress/empty log is UNREADABLE, never evidence of green.
|
|
4
|
-
#
|
|
4
|
+
# Exit 0 GREEN, 1 RED, 2 UNREADABLE, 3 INFRA_KILLED: a job the GitHub runner shut down or cancelled with
|
|
5
|
+
# no failed or timed-out test (the classifier's class, D-897) — never green, never a pass, only unproven.
|
|
6
|
+
# UNREADABLE dominates a mixed result, then RED: a failure anywhere dominates a kill.
|
|
5
7
|
set -u
|
|
6
8
|
|
|
7
9
|
run=${1:?run id required}
|
|
@@ -14,7 +16,8 @@ mkdir -p "$out_dir" || { echo "UNREADABLE: cannot create log directory $out_dir"
|
|
|
14
16
|
|
|
15
17
|
verdict=0
|
|
16
18
|
mark_unreadable() { verdict=2; }
|
|
17
|
-
mark_red() { [ "$verdict" -eq
|
|
19
|
+
mark_red() { [ "$verdict" -eq 2 ] || verdict=1; }
|
|
20
|
+
mark_killed() { [ "$verdict" -ne 0 ] || verdict=3; }
|
|
18
21
|
field() { printf '%s\n' "$classified" | sed -n "s/^VITEST_LOG.* $1=\([^ ]*\).*/\1/p"; }
|
|
19
22
|
|
|
20
23
|
jobs=$(gh run view "$run" --repo "$repo" --json jobs \
|
|
@@ -52,10 +55,12 @@ while IFS=$'\t' read -r id name status conclusion; do
|
|
|
52
55
|
# (scripts/run-ci-vitest.sh), so the badge and this grade cannot disagree. Its rules — complete
|
|
53
56
|
# summaries, failed/timed-out tests, coverage-threshold misses, and the RPC-timeout-only exception
|
|
54
57
|
# to "any unhandled error is RED" (OBS-1058) — live in classify-vitest-log.sh, stated once there.
|
|
55
|
-
|
|
58
|
+
# The expected count is the job's declared file contract: both steps' summaries must collect it all.
|
|
59
|
+
classified=$(bash "$here/classify-vitest-log.sh" "$log" "$expected" 2>&1)
|
|
56
60
|
log_verdict=$(field verdict)
|
|
57
61
|
passed=$(field passed); skipped=$(field skipped); failed=$(field failed); timedout=$(field timedout)
|
|
58
62
|
unhandled=$(field unhandled); rpc=$(field runner_rpc_timeouts); coverage=$(field coverage_misses)
|
|
63
|
+
kills=$(field runner_kills)
|
|
59
64
|
errors=$(grep -oE '##\[error\].*' "$log" | sort | uniq -c | sed 's/^ *//' | tr '\n' ';')
|
|
60
65
|
# The commands' own outcomes stand, as in the wrapper: GREEN needs a success conclusion with no step
|
|
61
66
|
# exit annotation, or a failure explained ONLY by steps that exited exactly 1 on a log of their own
|
|
@@ -81,8 +86,16 @@ while IFS=$'\t' read -r id name status conclusion; do
|
|
|
81
86
|
done <<< "$exits"
|
|
82
87
|
fi
|
|
83
88
|
|
|
84
|
-
echo "$name: oracle=[${oracle:-MISSING}] files=[$(printf '%s' "$files" | tr '\n' '|')] passed=$passed skipped=$skipped failed=$failed timedout=$timedout unhandled=$unhandled runner_rpc_timeouts=$rpc coverage_misses=$coverage log=[$(field reason)] outcome=[$outcome] errors=[$errors]"
|
|
85
|
-
|
|
89
|
+
echo "$name: oracle=[${oracle:-MISSING}] files=[$(printf '%s' "$files" | tr '\n' '|')] passed=$passed skipped=$skipped failed=$failed timedout=$timedout unhandled=$unhandled runner_rpc_timeouts=$rpc coverage_misses=$coverage runner_kills=${kills:-?} log=[$(field reason)] outcome=[$outcome] errors=[$errors]"
|
|
90
|
+
# A killed runner skips the count step, so its missing oracle is the kill's, not an unreadable log:
|
|
91
|
+
# INFRA_KILLED unless a step's own failing exit or a failed/timed-out test dominates it (then RED).
|
|
92
|
+
if [ "$log_verdict" = INFRA_KILLED ] && [ "${outcome#exit-}" = "$outcome" ]; then
|
|
93
|
+
echo "$name: INFRA_KILLED"
|
|
94
|
+
mark_killed
|
|
95
|
+
elif [ "${kills:-0}" -gt 0 ] 2>/dev/null; then
|
|
96
|
+
echo "$name: RED"
|
|
97
|
+
mark_red
|
|
98
|
+
elif [ -z "$oracle" ] || [ -z "$files" ] || [ -z "$log_verdict" ]; then
|
|
86
99
|
echo "$name: UNREADABLE"
|
|
87
100
|
mark_unreadable
|
|
88
101
|
elif [ "$oracle" = "COUNT_ORACLE GREEN expected=$expected actual=$expected" ] \
|
|
@@ -104,5 +117,5 @@ if [ "$seen_macos" -ne 1 ]; then
|
|
|
104
117
|
mark_unreadable
|
|
105
118
|
fi
|
|
106
119
|
|
|
107
|
-
echo "VERDICT rc=$verdict (0=all GREEN 1=RED 2=UNREADABLE)"
|
|
120
|
+
echo "VERDICT rc=$verdict (0=all GREEN 1=RED 2=UNREADABLE 3=INFRA_KILLED)"
|
|
108
121
|
exit "$verdict"
|
|
@@ -18,79 +18,136 @@
|
|
|
18
18
|
# a self-criticism — the one sentence nobody audits, including its writer. The gap is real and the watcher
|
|
19
19
|
# is justified by the ABSENCE OF COVERAGE, not by that number. See OBS-435.
|
|
20
20
|
#
|
|
21
|
-
# Prints ONE wake reason and exits. Re-arm after every
|
|
21
|
+
# Prints ONE wake reason, ending with its `CURSOR <run> <line>` receipt, and exits. Re-arm after every
|
|
22
|
+
# wake WITH that receipt: it is the acknowledgment, and only the rows it covers are suppressed.
|
|
22
23
|
#
|
|
23
|
-
# usage: watch-parks.sh <runs-dir> [poll-s] [cap-s]
|
|
24
|
-
# Tracks the NEWEST run
|
|
24
|
+
# usage: watch-parks.sh <runs-dir> [poll-s] [cap-s] [--since-run <run-id> --since-line <n>]
|
|
25
|
+
# Tracks the NEWEST run (the latestRunId rule: lexical run-id order, journal present), so it follows a
|
|
26
|
+
# resume or a fresh run without being re-aimed.
|
|
27
|
+
#
|
|
28
|
+
# ── THE CURSOR (D-829) — what replaced arm-time seeding ────────────────────────────────────────────────
|
|
29
|
+
# Earlier versions seeded on the journal AS ARMED — a park count, then a line baseline. Both read "already
|
|
30
|
+
# in the file when I started" as "already ruled", and the gap between a wake and its re-arm is exactly
|
|
31
|
+
# where that is false: a park appended while the seat was busy ruling the last one was seeded as history
|
|
32
|
+
# and never reported, released or not. The seat's own acknowledgment is the only honest baseline, so the
|
|
33
|
+
# START read happens before any sleep. The closed cursor inputs:
|
|
34
|
+
# same-run acknowledged receipt run == newest run: complete rows at lines <= n are suppressed, no others
|
|
35
|
+
# gap park a task-human row after n is reported at START
|
|
36
|
+
# later-release a gap park released before the re-arm is STILL reported — this answers "which
|
|
37
|
+
# parks has the seat not seen", never "which parks are still open"
|
|
38
|
+
# new-run newest run != receipt run (or no receipt): the receipt resets to line 0
|
|
39
|
+
# empty-run no run has a journal yet: the receipt is `CURSOR none 0`, and re-arming with
|
|
40
|
+
# `--since-run none --since-line 0` is the same as no receipt, so the first run
|
|
41
|
+
# that appears is read from line 0
|
|
42
|
+
# torn tail a final line without its newline is not a row: never reported, never counted, so
|
|
43
|
+
# the cursor never advances past it and its completed form is reported later
|
|
44
|
+
# A receipt line beyond its run's complete lines is not this journal's acknowledgment: refused (exit 65),
|
|
45
|
+
# because silently suppressing rows on a mismatched receipt is the failure this file exists to prevent.
|
|
46
|
+
# A journal that cannot be read is not an empty scan: exit 66, no receipt, no WATCH_CAP_REACHED. Nor is a
|
|
47
|
+
# runs dir or run directory that cannot be listed or searched: discovery skips only ABSENCE (no runs dir
|
|
48
|
+
# yet, a run with no journal yet), because falling back to an older run hides the newest run's parks. Any
|
|
49
|
+
# other reader failure, or reader output whose last line is not a receipt, is an error too — never
|
|
50
|
+
# "nothing new".
|
|
51
|
+
#
|
|
52
|
+
# A `run-end` after the cursor wakes too (the milestone verdict is a decision), and a park wake reports a
|
|
53
|
+
# run-end in the same range, so a returned cursor never covers a row the seat was not shown.
|
|
25
54
|
|
|
26
55
|
set -u
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
56
|
+
POS=() SINCE_RUN="" SINCE=""
|
|
57
|
+
while [ $# -gt 0 ]; do
|
|
58
|
+
case "$1" in
|
|
59
|
+
--since-run) SINCE_RUN="${2-}"; shift; [ $# -gt 0 ] && shift ;;
|
|
60
|
+
--since-line) SINCE="${2-}"; shift; [ $# -gt 0 ] && shift ;;
|
|
61
|
+
*) POS+=("$1"); shift ;;
|
|
62
|
+
esac
|
|
63
|
+
done
|
|
64
|
+
RUNS="${POS[0]:?runs dir required (e.g. .tickmarkr/runs)}"
|
|
65
|
+
POLL="${POS[1]:-45}"
|
|
66
|
+
CAP="${POS[2]:-28800}"
|
|
67
|
+
# The receipt is both halves or neither: a line without its run cannot be bound to a journal. Run ids follow
|
|
68
|
+
# parseRunId (src/run/journal.ts): run- then an alphanumeric, then alphanumerics, `_` or `-`.
|
|
69
|
+
if [ -n "$SINCE_RUN$SINCE" ] && ! [[ ( "$SINCE_RUN" =~ ^run-[A-Za-z0-9][A-Za-z0-9_-]*$ && "$SINCE" =~ ^[0-9]+$ ) || ( "$SINCE_RUN" = none && "$SINCE" = 0 ) ]]; then
|
|
70
|
+
echo "watch-parks.sh: the receipt is --since-run <run-id> --since-line <n> together, got run='$SINCE_RUN' line='$SINCE'" >&2
|
|
71
|
+
exit 64
|
|
72
|
+
fi
|
|
30
73
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
74
|
+
# ONE JSON reader for every scan, START and poll alike. It applies the journal reader's rule
|
|
75
|
+
# (src/run/journal.ts parseJournalText: skip blanks, drop an unparseable line, keep each row's PHYSICAL
|
|
76
|
+
# line) to the newline-terminated lines only. argv: runs dir, receipt run, receipt line. Prints the wake, if
|
|
77
|
+
# any, then always `CURSOR <run> <last complete line>` (the receipt unchanged while no run has a journal).
|
|
78
|
+
# Exit 0 = scanned (a wake is any line before the receipt), 65 = refused receipt, 66 = failed read. It sets exitCode rather than calling exit, so a
|
|
79
|
+
# wake larger than the pipe buffer drains before node quits (a 1,500-park wake was cut off mid-list).
|
|
80
|
+
# It reports rows; it never refolds which parks are still open — that is the decision fold's job.
|
|
81
|
+
READER='
|
|
82
|
+
process.exitCode = (() => {
|
|
83
|
+
const fs = require("fs"), { join } = require("path");
|
|
84
|
+
const [runs, sinceRun, sinceLine] = process.argv.slice(1);
|
|
85
|
+
const fail = (what, e) => { console.error(`watch-parks.sh: cannot read ${what}: ${e.code ?? e.message}`); return 66; };
|
|
86
|
+
// Discovery: newest run id first, skipping only a journal that is ABSENT (ENOENT/ENOTDIR). EACCES on an
|
|
87
|
+
// unsearchable run directory, or ELOOP, is a failed read, never a fall-back to an older run.
|
|
88
|
+
let ids = [];
|
|
89
|
+
try { ids = fs.readdirSync(runs).filter((d) => d.startsWith("run-")).sort().reverse(); } catch (e) {
|
|
90
|
+
if (e.code !== "ENOENT") return fail(runs, e);
|
|
42
91
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
92
|
+
let run, file;
|
|
93
|
+
for (const id of ids) {
|
|
94
|
+
const f = join(runs, id, "journal.jsonl");
|
|
95
|
+
try { fs.statSync(f); } catch (e) {
|
|
96
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR") continue;
|
|
97
|
+
return fail(f, e);
|
|
98
|
+
}
|
|
99
|
+
[run, file] = [id, f];
|
|
100
|
+
break;
|
|
48
101
|
}
|
|
102
|
+
if (!run) { console.log(`CURSOR ${sinceRun || "none"} ${sinceLine || 0}`); return 0; }
|
|
103
|
+
const since = run === sinceRun ? Number(sinceLine) : 0;
|
|
104
|
+
let text;
|
|
105
|
+
try { text = fs.readFileSync(file, "utf8"); } catch (e) { return fail(file, e); }
|
|
106
|
+
const lines = text.split("\n");
|
|
107
|
+
const complete = lines.length - 1; // the last element is "" or a torn tail: never a complete line
|
|
108
|
+
if (since > complete) {
|
|
109
|
+
console.error(`watch-parks.sh: receipt ${run} line ${since} is beyond its ${complete} complete lines — not an acknowledgment of this journal`);
|
|
110
|
+
return 65;
|
|
111
|
+
}
|
|
112
|
+
const parks = [], ends = [];
|
|
113
|
+
for (let n = since + 1; n <= complete; n++) {
|
|
114
|
+
let row;
|
|
115
|
+
try { row = JSON.parse(lines[n - 1]); } catch { continue; }
|
|
116
|
+
if (row?.event === "task-human") parks.push([n, row]);
|
|
117
|
+
else if (row?.event === "run-end") ends.push([n, row]);
|
|
118
|
+
}
|
|
119
|
+
const out = [];
|
|
120
|
+
if (parks.length) {
|
|
121
|
+
out.push(`PARK ${parks.length} new — ${run}`);
|
|
122
|
+
for (const [n, row] of parks) out.push(` ${row.taskId}: ${String(row.data?.reason ?? "").replace(/\s+/g, " ").slice(0, 160)}`, ` park ${n}@${row.ts}`);
|
|
123
|
+
out.push(" a park waits for a RULING and nothing else releases it — read the gate evidence, then rule");
|
|
124
|
+
}
|
|
125
|
+
if (ends.length) {
|
|
126
|
+
out.push(`RUN_END ${run} tipVerify=${ends.at(-1)[1].data?.tipVerify ?? "unknown"}`);
|
|
127
|
+
out.push(" read tipVerify as a FIELD; a failing verify is a DIFFERENT event with no pass field");
|
|
128
|
+
}
|
|
129
|
+
out.push(`CURSOR ${run} ${complete}`);
|
|
130
|
+
console.log(out.join("\n"));
|
|
131
|
+
return 0;
|
|
132
|
+
})();
|
|
133
|
+
'
|
|
49
134
|
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# ...and seed a LINE POSITION, because the park counter is not enough (evidence rule 30).
|
|
57
|
-
# `run-end` is a HISTORICAL RECORD once written. A whole-file grep for it finds the PREVIOUS run-end
|
|
58
|
-
# on every resume, and on any re-arm after a run has ended — so the watcher exits in its first poll,
|
|
59
|
-
# a supervisor re-execs it into the same instant exit, and the process table shows coverage that does
|
|
60
|
-
# not exist. Measured 2026-08-07: re-arming this watcher after run 264's run-end would have done
|
|
61
|
-
# exactly that. Only lines appended AFTER arming are evidence about now.
|
|
62
|
-
base=0
|
|
63
|
-
[ -n "${J:-}" ] && base=$(wc -l < "$J" 2>/dev/null || echo 0)
|
|
64
|
-
since_arm() { tail -n +$((base + 1)) "$1" 2>/dev/null; }
|
|
135
|
+
# Sets `last` to the scan, which always ends in its receipt, and returns 0; any nonzero is a failed read.
|
|
136
|
+
CURSOR_RE='^CURSOR (run-[A-Za-z0-9][A-Za-z0-9_-]* [0-9]+|none 0)$'
|
|
137
|
+
scan() {
|
|
138
|
+
last=$(node -e "$READER" "$RUNS" "$SINCE_RUN" "$SINCE") || return
|
|
139
|
+
[[ "${last##*$'\n'}" =~ $CURSOR_RE ]] || { echo "watch-parks.sh: reader output ended without a receipt" >&2; return 70; }
|
|
140
|
+
}
|
|
65
141
|
|
|
66
|
-
elapsed=0
|
|
67
|
-
while
|
|
142
|
+
elapsed=0 last=""
|
|
143
|
+
while :; do
|
|
144
|
+
scan || { rc=$?; echo "WATCH_ERROR — journal scan failed (status $rc); no receipt, no coverage until re-armed" >&2; exit "$rc"; }
|
|
145
|
+
# Only a wake prints lines before its receipt.
|
|
146
|
+
[[ "$last" == *$'\n'* ]] && { printf '%s\n' "$last"; exit 0; }
|
|
147
|
+
[ "$elapsed" -ge "$CAP" ] && break
|
|
68
148
|
sleep "$POLL"
|
|
69
149
|
elapsed=$((elapsed + POLL))
|
|
70
|
-
|
|
71
|
-
J=$(newest_journal)
|
|
72
|
-
[ -z "${J:-}" ] && continue
|
|
73
|
-
|
|
74
|
-
# A new run resets the baseline — its parks AND its whole journal are unseen by definition.
|
|
75
|
-
if [ "$J" != "$seen_run" ]; then seen=0; seen_run="$J"; base=0; fi
|
|
76
|
-
|
|
77
|
-
now=$(count_parks "$J")
|
|
78
|
-
if [ "$now" -gt "$seen" ]; then
|
|
79
|
-
echo "PARK $((now - seen)) new — $(basename "$(dirname "$J")")"
|
|
80
|
-
grep '"event":"task-human"' "$J" 2>/dev/null | tail -n "$((now - seen))" \
|
|
81
|
-
| sed -n 's/.*"taskId":"\([^"]*\)".*"reason":"\([^"]\{0,160\}\).*/ \1: \2/p'
|
|
82
|
-
echo " a park waits for a RULING and nothing else releases it — read the gate evidence, then rule"
|
|
83
|
-
exit 0
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
# A run that ended is also this seat's business: the milestone verdict is a decision.
|
|
87
|
-
# Scoped to lines appended since arming — see the `base` comment above.
|
|
88
|
-
if since_arm "$J" | grep -q '"event":"run-end"'; then
|
|
89
|
-
tv=$(since_arm "$J" | grep '"event":"run-end"' | tail -1 | sed -n 's/.*"tipVerify":"\([a-z]*\)".*/\1/p')
|
|
90
|
-
echo "RUN_END $(basename "$(dirname "$J")") tipVerify=${tv:-unknown}"
|
|
91
|
-
echo " read tipVerify as a FIELD; a failing verify is a DIFFERENT event with no pass field"
|
|
92
|
-
exit 0
|
|
93
|
-
fi
|
|
94
150
|
done
|
|
95
151
|
|
|
96
|
-
echo "WATCH_CAP_REACHED — no new park or run-end in ${CAP}s
|
|
152
|
+
echo "WATCH_CAP_REACHED — no new park or run-end in ${CAP}s"
|
|
153
|
+
printf '%s\n' "$last"
|