tickmarkr 2.6.5 → 2.6.6
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/codex-commit-check.d.ts +15 -4
- package/dist/adapters/codex-commit-check.js +122 -25
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +12 -6
- package/dist/adapters/types.d.ts +4 -1
- package/dist/cli/commands/doctor.js +3 -1
- package/dist/cli/commands/fleet.js +318 -78
- package/dist/cli/commands/init.js +5 -2
- package/dist/cli/commands/plan.js +12 -1
- package/dist/cli/commands/verify.js +37 -9
- 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/run-gates.d.ts +2 -1
- package/dist/gates/run-gates.js +21 -6
- package/dist/gates/test-manifest.d.ts +3 -0
- package/dist/gates/test-manifest.js +1 -1
- package/dist/gates/timeout-shaped.d.ts +3 -0
- package/dist/gates/timeout-shaped.js +10 -0
- package/dist/run/daemon.js +184 -119
- package/dist/tui/ink/fleet-app.d.ts +26 -5
- package/dist/tui/ink/fleet-app.js +50 -12
- package/package.json +1 -1
- 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
|
@@ -49,7 +49,7 @@ const REACH_CHOICES = [
|
|
|
49
49
|
{ id: "out-workers", label: "out · workers", gloss: "workers skip it; judge/review/consult unaffected" },
|
|
50
50
|
{ id: "out-all", label: "out · all seats", gloss: "no seat routes here" },
|
|
51
51
|
];
|
|
52
|
-
export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts = {}, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, reloadGuard, stagedRouting, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, frameColumns = 100, frameRows, ...initialDenyProps }) {
|
|
52
|
+
export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts = {}, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, reloadGuard, stagedRouting, holderOf, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, frameColumns = 100, frameRows, ...initialDenyProps }) {
|
|
53
53
|
const { exit } = useApp();
|
|
54
54
|
const [, setRevision] = useState(0);
|
|
55
55
|
// the props are the fleet command's contract, one per schema-enumerated scope (initialDeny…),
|
|
@@ -71,6 +71,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
71
71
|
notice: "",
|
|
72
72
|
lastEdit: null,
|
|
73
73
|
allowOut: new Set(initialAllowOut),
|
|
74
|
+
seededAllowOut: [],
|
|
74
75
|
...Object.fromEntries(DENY_SCOPES.map((scope) => [scope.key, new Set(initialDeny[scope.key])])),
|
|
75
76
|
classifications: [],
|
|
76
77
|
efforts: structuredClone(initialEfforts),
|
|
@@ -139,12 +140,13 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
139
140
|
let routingMemo = null;
|
|
140
141
|
const stagedPolicy = () => {
|
|
141
142
|
const deny = stagedDeny();
|
|
142
|
-
const
|
|
143
|
+
const stage = stagedMetadata();
|
|
144
|
+
const key = JSON.stringify([deny, stage]);
|
|
143
145
|
if (routingMemo?.key !== key) {
|
|
144
146
|
routingMemo = {
|
|
145
147
|
key,
|
|
146
148
|
policy: stagedRouting
|
|
147
|
-
? stagedRouting(deny)
|
|
149
|
+
? stagedRouting(deny, stage)
|
|
148
150
|
: { ok: true, routing: { deny: denyBlockFrom(denyLists()) } },
|
|
149
151
|
};
|
|
150
152
|
}
|
|
@@ -163,7 +165,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
163
165
|
const reachFor = (adapter, model, identity) => {
|
|
164
166
|
const policy = stagedPolicy();
|
|
165
167
|
if (!policy.ok)
|
|
166
|
-
return { reach: "unknown", reasons: [`preview unavailable (${policy.error})`], uncoveredAlias: false };
|
|
168
|
+
return { reach: "unknown", reasons: [`preview unavailable (${policy.error})`], uncoveredAlias: false, held: [] };
|
|
167
169
|
const channel = { adapter, model, ...(identity !== undefined ? { identity } : {}) };
|
|
168
170
|
const worker = exclusionsOf(channel, policy.routing, "worker");
|
|
169
171
|
const allSeats = exclusionsOf(channel, policy.routing, "judge");
|
|
@@ -175,6 +177,10 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
175
177
|
reach,
|
|
176
178
|
reasons: worker.map(exclusionReason),
|
|
177
179
|
uncoveredAlias: identity !== undefined && !worker.some((scope) => scope.by === "deny"),
|
|
180
|
+
held: [...new Set(worker.map((scope) => scope.configPath))].flatMap((dotted) => {
|
|
181
|
+
const holder = holderOf?.(dotted);
|
|
182
|
+
return holder === undefined ? [] : [`${dotted} @ ${holder}`];
|
|
183
|
+
}),
|
|
178
184
|
};
|
|
179
185
|
};
|
|
180
186
|
// OBS-1099 review: a displayed row may fold several discovered gateway ids (foldedModels);
|
|
@@ -377,12 +383,13 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
377
383
|
// ── state transitions ──────────────────────────────────────────────────────
|
|
378
384
|
// T8: the previews fold the CURRENT staged metadata — never only what the last review saw
|
|
379
385
|
const stagedMetadata = () => {
|
|
380
|
-
const { classifications, efforts } = editorState();
|
|
381
|
-
return { classifications, efforts };
|
|
386
|
+
const { classifications, efforts, seededAllowOut } = editorState();
|
|
387
|
+
return { classifications, efforts, seededAllowOut };
|
|
382
388
|
};
|
|
383
389
|
const editorState = () => ({
|
|
384
390
|
...denyLists(),
|
|
385
391
|
allowOut: [...ui.allowOut].sort(),
|
|
392
|
+
seededAllowOut: structuredClone(ui.seededAllowOut),
|
|
386
393
|
classifications: ui.classifications.map((classification) => classification.vendor && classification.channel
|
|
387
394
|
? {
|
|
388
395
|
...classification,
|
|
@@ -430,9 +437,38 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
430
437
|
finish({ kind: "no-changes" });
|
|
431
438
|
return;
|
|
432
439
|
}
|
|
440
|
+
if (nextReview.kind === "refused") {
|
|
441
|
+
ui.notice = `w refused — ${nextReview.reason}`;
|
|
442
|
+
bump();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
433
445
|
setOverlay({ kind: "review", review: nextReview, scroll: 0 });
|
|
434
446
|
};
|
|
447
|
+
// T10 (D-925/D-939): a model the current allow form leaves out gets its OWN allow entry staged beside
|
|
448
|
+
// its classification — the ownAllow spelling Space clears — so it can be admitted in this session.
|
|
449
|
+
// Re-staging a classification replaces its seed; a row the allow form admits seeds nothing. A row a
|
|
450
|
+
// flat deny ALSO excludes still seeds, so successive owning lifts (deny, then allow) admit it.
|
|
451
|
+
const seedAllow = (adapter, model, displayModel) => {
|
|
452
|
+
// ponytail: a collapsed variant row's allow entry would name no channel (its display name is not the real
|
|
453
|
+
// model), so it is not seeded for same-session admission — classify, save, then admit next session;
|
|
454
|
+
// full grouped-row membership (collapse + catalog fold) is 2.6.7 (T10 owed review F1, D-1016)
|
|
455
|
+
if (model !== displayModel)
|
|
456
|
+
return;
|
|
457
|
+
const identity = groupOf(adapter)?.rows.find((row) => row.model === displayModel)?.evidence?.identity;
|
|
458
|
+
const self = { adapter, model: displayModel, ...(identity !== undefined ? { identity } : {}) };
|
|
459
|
+
const policy = stagedPolicy();
|
|
460
|
+
if (!policy.ok || !exclusionsOf(self, policy.routing, "judge").some((scope) => scope.by === "allow"))
|
|
461
|
+
return;
|
|
462
|
+
const entry = selectOwn(ui.allowOut, self) ?? `${adapter}:${displayModel}`;
|
|
463
|
+
ui.allowOut = new Set([...ui.allowOut, entry]);
|
|
464
|
+
ui.seededAllowOut = [
|
|
465
|
+
...ui.seededAllowOut.filter((seed) => seed.adapter !== adapter || seed.model !== model),
|
|
466
|
+
{ adapter, model, displayModel, ...(identity !== undefined ? { identity } : {}), entry },
|
|
467
|
+
];
|
|
468
|
+
};
|
|
435
469
|
const stageSuggested = (adapter, rows, firstTouch) => {
|
|
470
|
+
for (const row of rows)
|
|
471
|
+
seedAllow(adapter, row.model, row.displayModel);
|
|
436
472
|
ui.classifications = [
|
|
437
473
|
...ui.classifications,
|
|
438
474
|
...rows.map((row) => ({
|
|
@@ -487,6 +523,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
487
523
|
};
|
|
488
524
|
const applyClassification = (overlay) => {
|
|
489
525
|
const answered = overlay.vendor ? ui.channelByAdapter[overlay.adapter] : undefined;
|
|
526
|
+
seedAllow(overlay.adapter, overlay.model, overlay.displayModel ?? overlay.model);
|
|
490
527
|
ui.classifications = [
|
|
491
528
|
...ui.classifications,
|
|
492
529
|
{
|
|
@@ -1551,7 +1588,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1551
1588
|
const group = modelGroups[ui.adapterAt];
|
|
1552
1589
|
const suggested = rows
|
|
1553
1590
|
.filter((candidate) => candidate.adapter === group.adapter && !candidate.tier && candidate.suggestion !== undefined)
|
|
1554
|
-
.map((candidate) => ({ model: candidate.classifyModel ?? candidate.model, suggestion: candidate.suggestion }));
|
|
1591
|
+
.map((candidate) => ({ model: candidate.classifyModel ?? candidate.model, displayModel: candidate.model, suggestion: candidate.suggestion }));
|
|
1555
1592
|
if (!suggested.length) {
|
|
1556
1593
|
ui.notice = "no catalog tier suggestions among the visible unclassified models — evidence comes from the cached catalogs (AA index / API pricing)";
|
|
1557
1594
|
bump();
|
|
@@ -1683,7 +1720,8 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1683
1720
|
const reachDetail = (row) => {
|
|
1684
1721
|
if (row.evidence?.unauthed !== undefined)
|
|
1685
1722
|
return "unauthed — re-probe with tickmarkr doctor (Space does not toggle reach)";
|
|
1686
|
-
|
|
1723
|
+
// T10: the reasons, then where each scope is held — the overlay a reviewed w writes
|
|
1724
|
+
const reasons = `${row.reasons.join("; ")}${row.held.length ? ` — held: ${row.held.join("; ")}` : ""}`;
|
|
1687
1725
|
if (row.reach === "out-all")
|
|
1688
1726
|
return `reach: out · all seats — ${reasons} — Space picks in or out · workers`;
|
|
1689
1727
|
if (row.reach === "out-allow")
|
|
@@ -1942,8 +1980,8 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1942
1980
|
lines.push(coveringDetail(row));
|
|
1943
1981
|
if (!row.tier) {
|
|
1944
1982
|
lines.push(row.suggestion
|
|
1945
|
-
? `catalog suggests ${row.suggestion.tier} — Space/Enter classifies with the note pre-typed · s stages every visible suggestion`
|
|
1946
|
-
: "unclassified — Space/Enter classifies; unclassified models are never routed");
|
|
1983
|
+
? `catalog suggests ${row.suggestion.tier} — Space/Enter classifies with the note pre-typed (t too) · s stages every visible suggestion`
|
|
1984
|
+
: "unclassified — Space/Enter classifies (t too), then Space sets its reach in this session; unclassified models are never routed");
|
|
1947
1985
|
}
|
|
1948
1986
|
}
|
|
1949
1987
|
return lines;
|
|
@@ -2060,7 +2098,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
2060
2098
|
? _jsx(Text, { color: INK.warn, children: ` ! ${clip(ui.notice, width - 4)}` })
|
|
2061
2099
|
: details.map((line) => _jsx(Text, { dimColor: true, children: ` ${clip(line, width - 2)}` }, line)), _jsx(KeyBar, { binds: keyBinds() })] }));
|
|
2062
2100
|
}
|
|
2063
|
-
export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut = [], initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, reloadGuard, stagedRouting, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, ...initialDenyProps }) {
|
|
2101
|
+
export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut = [], initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, reloadGuard, stagedRouting, holderOf, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, ...initialDenyProps }) {
|
|
2064
2102
|
const agents = adapters.flatMap((adapter) => {
|
|
2065
2103
|
const state = health[adapter.id];
|
|
2066
2104
|
if (!state?.installed)
|
|
@@ -2084,7 +2122,7 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowO
|
|
|
2084
2122
|
// rebuilt from output.columns/rows so a pane resize re-renders at the new size. Same root
|
|
2085
2123
|
// element type on every rerender ⇒ React keeps the component instance, so staged edits,
|
|
2086
2124
|
// cursor, and overlays all survive the resize.
|
|
2087
|
-
const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, ...initialDenyProps, initialAllowOut: initialAllowOut, initialEfforts: initialEfforts, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewAdvisories: reviewAdvisories, reviewOverlay: reviewOverlay, reloadGuard: reloadGuard, stagedRouting: stagedRouting, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats,
|
|
2125
|
+
const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, ...initialDenyProps, initialAllowOut: initialAllowOut, initialEfforts: initialEfforts, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewAdvisories: reviewAdvisories, reviewOverlay: reviewOverlay, reloadGuard: reloadGuard, stagedRouting: stagedRouting, holderOf: holderOf, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats,
|
|
2088
2126
|
// v1.90.9: cap every list to the terminal minus chrome so the cursor and header can
|
|
2089
2127
|
// never scroll off-screen (omp's 218-model list did exactly that pre-cap).
|
|
2090
2128
|
// OBS-523: chrome is ≤12 rows worst case (header, borders, padding, scope+search,
|
package/package.json
CHANGED
|
@@ -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"
|