sol: fleet dispatch triage 2026-11-05 classifier + manifest + machine-checkable CHECK

This commit is contained in:
sol committed 2026-11-05 08:14:42 +00:00
1 parent f8d7a80c96
commit d5abdb1b63
4 files changed
+2034

No files matched your search

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# CHECK -- sol: fleet dispatch jobs triage classifier (2026-11-05).
#
# Verdict: exit 0 iff the committed manifest
# (wizard-work/sol/fleet-triage-2026-11-05.json) matches ground truth re-
# derived straight from /root/.hermes/cron/jobs.json. A real re-derivation,
# not a tautology.
set -u
cd "$(dirname "$0")" || exit 1
REPO_ROOT="$(pwd)"
cd ../.. || exit 1
REPO_ROOT="$(pwd)"
MANIFEST="$REPO_ROOT/wizard-work/sol/fleet-triage-2026-11-05.json"
SRC="/root/.hermes/cron/jobs.json"
[ -f "$MANIFEST" ] || { echo "MISSING manifest: $MANIFEST"; exit 1; }
[ -f "$SRC" ] || { echo "MISSING source: $SRC"; exit 1; }
python3 - "$MANIFEST" "$SRC" <<'PY'
import json, sys
manifest_path, src_path = sys.argv[1], sys.argv[2]
try:
manifest = json.load(open(manifest_path))
src = json.load(open(src_path))
except (OSError, ValueError) as e:
print("FATAL", e); sys.exit(1)
jobs = src["jobs"]
def c(**kw):
return sum(1 for j in jobs if all(j.get(k) == v for k, v in kw.items()))
expected = {
"total": len(jobs),
"enabled": c(enabled=True),
"disabled": c(enabled=False),
"state_scheduled": c(state="scheduled"),
"state_paused": c(state="paused"),
"state_completed": c(state="completed"),
"last_status_ok": c(last_status="ok"),
"last_status_error": c(last_status="error"),
"last_status_delivery_failed": c(last_status="delivery_failed"),
"last_status_none": c(last_status=None),
}
got = manifest.get("counts", {})
ok = True
for k, v in expected.items():
if got.get(k) != v:
print(f"MISMATCH {k}: committed={got.get(k)!r} ground_truth={v!r}")
ok = False
buckets = manifest.get("bucket_counts", {})
part = sum(b.get("n", 0) for b in buckets.values())
if part != expected["total"]:
print(f"BUCKET PARTITION {part} != total {expected['total']}")
ok = False
rows = manifest.get("rows", [])
if len(rows) != expected["total"]:
print(f"ROWS {len(rows)} != total {expected['total']}")
ok = False
sys.exit(0 if ok else 1)
PY
File diff suppressed because it is too large. Load diff
+157
View File
@@ -0,0 +1,157 @@
# Fleet dispatch triage — 2026-11-05 (sol)
Registry: `/root/.hermes/cron/jobs.json` (updated_at **2026-11-05T08:10:32.213311+00:00**)
Total jobs parsed: **103**
## Headline
Live (enabled, scheduled, last run ok): **32**. Halted/suspended (disabled): **69**. Last run errored anywhere: **13**. Only the live slice delivers to the crew.
## Counts
| metric | count | meaning |
|---|---|---|
| total jobs | 103 | from `jobs.json` |
| enabled | 34 | dispatch enabled |
| disabled | 69 | cannot run |
| state scheduled | 34 | due to run |
| state paused | 64 | suspended |
| state completed | 5 | finished, kept for audit |
| last_status ok | 88 | succeeded |
| last_status error | 13 | last run errored |
| last_status delivery_failed | 1 | delivery errored |
| last_status None | 1 | no recorded run |
## Triage buckets
| bucket | count | job ids |
|---|---|---|
| live_ok | 32 | 0223040a3137, 03229d665eb3, 033076aca27b, 0d30d580360a, 0d32f9397eb1, 16d896306f06, 1a4eac1ee1fd, 1b611dda47cc, 1c7026ffb4ca, 2f9142e19934, 3ed82182f8b1, 5a4cd82bf619, 5d69d78ab6a6, 649cdf1ba4a4, 726ee15e5fc0, 7ecfaa2e2074, 8cf287aaf078, a62175cd4452, a7e1354db22d, b6cb8c490e9f, c14a6a64739a, c52d674a34dc, c6dd61160f93, d2cfb701657b, d384219bac3a, e280df14418f, e3babf87dee8, e7dc7af93dd9, e8f8c8b9e0b2, f37e0bbd567b, f70489ca88ac, fedd79bad2dc |
| needs_attention | 2 | 3ac2f4a3f546, 6cf100cea5f5 |
| disabled | 64 | 0035e931693a, 010dc42e0990, 01442c7c6acc, 072b410f38a7, 0ee6e2270719, 185f6b33f467, 192bb0c1f4f9, 1ae8f67df4c8, 1d1cfcd3542b, 1de4e6507666, 1fcc628efa6f, 20324f12208d, 277ef8ecd947, 28e591a992c6, 2a8e7a70b22c, 2b6b5e4373d0, 2f0fbfc5e594, 2f34e688cf7e, 2f5f9cf39999, 3088cd436937, 359c3ad417df, 376fb55ea81d, 384cd990a48e, 4098c6fd0298, 45e3f9b6ca89, 46e9fe536787, 50f1032de42e, 563745413001, 65c0fc699c5e, 6a5db2fe8231, 72c7753aa0c4, 739731f108fc, 74abe2633878, 7679c6cfd7cc, 78647f267d37, 79f2aa4defce, 7b586d62af70, 7cc7b66428ae, 7d03eabdd9b9, 816981dfb712, 88f6a79368eb, a348cb59a0c1, a476223d9a79, a78064e1af61, a9226ea81409, aa5e13261842, afe2bba4854b, b28316cf7def, b5e9b0fd4f4f, be22831260fb, c90e6f034448, cb1c14f8ca68, ccd3a4141f52, cd8a31f5ff9f, cfb73a4cf7ad, d25576d90c98, d60d505c172f, d7538b5bb73f, dd9fa86bea9e, e16430881979, e267b564eb2d, e61f5ce71d8b, f9db4323dad4, fee83aa9ed16 |
| completed | 5 | 18a1084dcd28, 1e5a6657aaef, 6fd12e052fc7, 9ee1d9279adc, ed23f1514bc2 |
## Severity flags
- **disabled_paused**: 64
- **last_status_error**: 13
- **completed**: 5
- **failure_streak_ge3**: 4
## Machine-readable rows (TSV)
id\tname\trisk\tstate\tenabled\tlast_status\tnext_run_at\tschedule\tfailure_streak
0223040a3137\tDisk space watchdog — 95% alarm\tlive_ok\tscheduled\t1\tok\t2026-11-05T08:11:31.240011+00:00\tevery 5m\t0
03229d665eb3\tEmail prompt watcher\tlive_ok\tscheduled\t1\tok\t2026-11-05T08:19:38.372709+00:00\tevery 10m\t0
033076aca27b\tAudit ledger commit — append-only log to git\tlive_ok\tscheduled\t1\tok\t2026-11-05T13:38:27.918604+00:00\tevery 360m\t0
0d30d580360a\tRebuild dream gallery (after last dream)\tlive_ok\tscheduled\t1\tok\t2026-11-05T10:30:00+00:00\t30 10 * * *\t0
0d32f9397eb1\tRepertoire — 06:30 Stackchain Archive Delta\tlive_ok\tscheduled\t1\tok\t2026-11-05T10:30:00+00:00\t30 10 * * *\t0
16d896306f06\tASIC miner reseller quote watch\tlive_ok\tscheduled\t1\tok\t2026-11-05T08:25:41.038131+00:00\tevery 30m\t0
1a4eac1ee1fd\tDeepSeek balance watch — daily snapshot + low-balance alert\tlive_ok\tscheduled\t1\tok\t2026-11-05T13:00:00+00:00\t0 13 * * *\t0
1b611dda47cc\tTimmy nightly bounded builder\tlive_ok\tscheduled\t1\tok\t2026-11-05T10:45:00+00:00\t45 10 * * *\t0
1c7026ffb4ca\tUnbroker — Alexander removal loop\tlive_ok\tscheduled\t1\tok\t2026-11-05T09:00:00+00:00\tevery day at 9am\t0
2f9142e19934\tTweet outbox — deliver pending drafts to the Outbox topic\tlive_ok\tscheduled\t1\tok\t2026-11-05T08:42:27.956937+00:00\tevery 60m\t0
3ed82182f8b1\tOpenCode free route (every 4h)\tlive_ok\tscheduled\t1\tok\t2026-11-05T12:04:14.086019+00:00\tevery 240m\t0
5a4cd82bf619\tSMTP unblock watchdog\tlive_ok\tscheduled\t1\tok\t2026-11-05T08:10:42.272544+00:00\tevery 10m\t0
5d69d78ab6a6\tSentinel long view (every 6h)\tlive_ok\tscheduled\t1\tok\t2026-11-05T11:48:53.346972+00:00\tevery 360m\t0
649cdf1ba4a4\tAlexander left-eye weekly check-in\tlive_ok\tscheduled\t1\tok\t2026-11-08T09:00:00+00:00\tevery sunday 9am\t0
726ee15e5fc0\tTimmy TM off-turn coach\tlive_ok\tscheduled\t1\tok\t2026-11-05T08:10:21.316141+00:00\tevery 2m\t0
7ecfaa2e2074\tNew Telegram user alert\tlive_ok\tscheduled\t1\tok\t2026-11-05T08:11:32.713260+00:00\tevery 5m\t0
8cf287aaf078\tMerlin Crucible (daily)\tlive_ok\tscheduled\t1\tok\t2026-11-05T09:46:53.560046+00:00\tevery 120m\t0
a62175cd4452\tStackchain Library Log — daily review package\tlive_ok\tscheduled\t1\tok\t2026-11-05T15:00:00+00:00\t0 15 * * *\t0
a7e1354db22d\tHuman gates submit relay\tlive_ok\tscheduled\t1\tok\t2026-11-05T08:12:21.509958+00:00\tevery 5m\t0
b6cb8c490e9f\tVolt workhorse (every 2h)\tlive_ok\tscheduled\t1\tok\t2026-11-05T09:47:21.264221+00:00\tevery 120m\t0
c14a6a64739a\tSherry/Kendra team watchdog\tlive_ok\tscheduled\t1\tok\t2026-11-05T08:11:32.906710+00:00\tevery 5m\t0
c52d674a34dc\tTM spectator watch\tlive_ok\tscheduled\t1\tok\t2026-11-05T08:10:21.565369+00:00\tevery 2m\t0
c6dd61160f93\tMercury standup (Groq)\tlive_ok\tscheduled\t1\tok\t2026-11-05T12:00:00+00:00\t0 12 * * *\t0
d2cfb701657b\tSherry — NP job watch (Taunton)\tlive_ok\tscheduled\t1\tok\t2026-11-05T09:00:00+00:00\tevery day at 9am\t0
d384219bac3a\tWizard fleet (8 free models)\tlive_ok\tscheduled\t1\tok\t2026-11-05T09:53:14.067642+00:00\tevery 120m\t0
e280df14418f\tMorning home brief (weather + plans)\tlive_ok\tscheduled\t1\tok\t2026-11-06T08:00:00+00:00\tevery day at 8am\t0
e3babf87dee8\tTimmy morning review release\tlive_ok\tscheduled\t1\tok\t2026-11-05T12:00:00+00:00\t0 12 * * *\t0
e7dc7af93dd9\tdaily-credit-spend-report\tlive_ok\tscheduled\t1\tok\t2026-11-05T14:00:00+00:00\t0 14 * * *\t0
e8f8c8b9e0b2\tStackchain library — Telegram media intake\tlive_ok\tscheduled\t1\tok\t2026-11-05T08:14:23.929140+00:00\tevery 30m\t0
f37e0bbd567b\tTroupe wizard ping relay\tlive_ok\tscheduled\t1\tok\t2026-11-05T15:00:00+00:00\t0 15 * * *\t0
f70489ca88ac\tBitcoiner Jobs — monitor + auto-apply\tlive_ok\tscheduled\t1\tok\t2026-11-05T10:22:22.885361+00:00\tevery 360m\t0
fedd79bad2dc\tWeekly skill audit\tlive_ok\tscheduled\t1\tok\t2026-11-09T09:00:00+00:00\tevery monday at 9am\t0
0035e931693a\tZeph daily interview\tdisabled\tpaused\t0\tok\t2026-10-29T18:00:00+00:00\t0 18 * * *\t0
010dc42e0990\tOpen NightProwl Daily Drop preflight\tdisabled\tpaused\t0\tok\t2026-08-18T10:00:00+00:00\t0 10 * * *\t0
01442c7c6acc\tDream cycle 04 (1:30am)\tdisabled\tpaused\t0\terror\t2026-10-24T05:30:00+00:00\t30 5 * * *\t1
072b410f38a7\tTimmy X DM responder\tdisabled\tpaused\t0\tok\t2026-10-28T20:49:38.738083+00:00\tevery 3m\t0
0ee6e2270719\tDream cycle 03 (midnight)\tdisabled\tpaused\t0\tok\t2026-10-29T04:00:00+00:00\t0 4 * * *\t0
185f6b33f467\tDaily Drop Night Brief — 10 PM Eastern\tdisabled\tpaused\t0\tok\t2026-08-28T04:45:00+00:00\t*/15 * * * *\t0
18a1084dcd28\t5pm ET daily report email\tcompleted\tcompleted\t0\tok\t\tonce at 2026-11-02 22:00\t0
192bb0c1f4f9\tVincent Daily Drop Candidate Queue\tdisabled\tpaused\t0\tok\t2026-08-28T08:35:00+00:00\t35 8 * * *\t0
1ae8f67df4c8\tBeam Foundry (every 2h)\tdisabled\tpaused\t0\terror\t2026-11-04T03:56:21.950865+00:00\tevery 120m\t1
1d1cfcd3542b\tDream cycle 05 (3am)\tdisabled\tpaused\t0\terror\t2026-10-24T10:15:00+00:00\t15 10 * * *\t1
1de4e6507666\tWhitestone homestead model cycle\tdisabled\tpaused\t0\tok\t2026-08-28T05:04:07.517451+00:00\tevery 360m\t0
1e5a6657aaef\tNightProwl LongCat daily evaluation brief\tcompleted\tcompleted\t0\tok\t\tevery 1440m\t0
1fcc628efa6f\tDaily Drop Team Intake Freeze — 4 AM Eastern\tdisabled\tpaused\t0\tok\t2026-08-28T04:45:00+00:00\t*/15 * * * *\t0
20324f12208d\tSovereign Signal — Daily Music Director\tdisabled\tpaused\t0\tok\t2026-08-28T05:00:00+00:00\t0 5 * * *\t0
277ef8ecd947\tStackchain Lab Daily Drop\tdisabled\tpaused\t0\tok\t2026-08-28T09:35:00+00:00\t35 9 * * *\t0
28e591a992c6\tCareer Funnel — Batch 3 of 4 — Platform and Reliability\tdisabled\tpaused\t0\tok\t2026-09-26T19:00:00+00:00\t0 19 * * *\t0
2a8e7a70b22c\tLibrary Log X replies — draft and DM\tdisabled\tpaused\t0\tok\t2026-10-29T00:32:39.012521+00:00\tevery 60m\t0
2b6b5e4373d0\tRepertoire — 11:00 Proof Product Sprint\tdisabled\tpaused\t0\tok\t2026-09-28T15:00:00+00:00\t0 15 * * *\t0
2f0fbfc5e594\tVincent + Grepples Second Crew Loop\tdisabled\tpaused\t0\tok\t2026-08-28T05:04:25.900127+00:00\tevery 30m\t0
2f34e688cf7e\tTM gen-15 cutoff\tdisabled\tpaused\t0\tok\t2026-11-02T18:33:35.978840+00:00\tevery 2m\t0
2f5f9cf39999\tSovereign Signal — Twice-Daily Executive Update\tdisabled\tpaused\t0\tok\t2026-08-28T14:00:00+00:00\t0 14,22 * * *\t0
3088cd436937\tMerkle thread watcher\tdisabled\tpaused\t0\tok\t2026-10-28T23:12:07.180461+00:00\tevery 60m\t0
359c3ad417df\tLeathermint daily production report\tdisabled\tpaused\t0\tok\t2026-10-26T10:00:00+00:00\t0 10 * * *\t0
376fb55ea81d\tSovereign Signal — External Visual Album State Machine\tdisabled\tpaused\t0\tok\t2026-08-25T21:58:16.677592+00:00\tevery 15m\t0
384cd990a48e\tHarness reflection (weekly)\tdisabled\tpaused\t0\tok\t2026-11-01T06:00:00+00:00\tevery sunday 6am\t0
4098c6fd0298\tVincent Daily Drop Preflight Queue\tdisabled\tpaused\t0\tok\t2026-08-18T10:30:00+00:00\t30 10 * * *\t0
45e3f9b6ca89\t[bot:timmy] Stackchain AI Lab change triage\tdisabled\tpaused\t0\tok\t2026-10-29T00:00:44.071838+00:00\tevery 5m\t0
46e9fe536787\tSovereign Signal — Open Music Engine Scout\tdisabled\tpaused\t0\tok\t2026-08-29T04:15:00+00:00\t15 4 * * *\t0
50f1032de42e\tCareer Funnel — Batch 2 of 4 — AI and DevTools\tdisabled\tpaused\t0\tok\t2026-09-26T15:30:00+00:00\t30 15 * * *\t0
563745413001\tself-improvement\tdisabled\tpaused\t0\tok\t2026-08-29T03:00:00+00:00\t0 3 * * *\t0
65c0fc699c5e\tRepertoire — 21:00 Skill Refinery + Command Brief\tdisabled\tpaused\t0\tok\t2026-09-29T01:00:00+00:00\t0 1 * * *\t0
6a5db2fe8231\tDream cycle 02 (10:30pm)\tdisabled\tpaused\t0\terror\t2026-10-24T04:45:00+00:00\t45 4 * * *\t1
6fd12e052fc7\tOpen NightProwl Daily Drop postflight audit\tcompleted\tcompleted\t0\tok\t\t15 12 * * *\t0
72c7753aa0c4\t[bot:timmy] Hermes failure change investigator\tdisabled\tpaused\t0\tok\t2026-09-28T03:47:46.249545+00:00\tevery 30m\t0
739731f108fc\tDream cycle 01 (9pm)\tdisabled\tpaused\t0\tok\t2026-10-29T04:15:00+00:00\t15 4 * * *\t0
74abe2633878\tAlexander Career Integration — Weekly\tdisabled\tpaused\t0\tok\t2026-08-30T15:00:00+00:00\t0 15 * * 0\t0
7679c6cfd7cc\tDaily Drop Human Gate Receipt — 7 AM Eastern\tdisabled\tpaused\t0\tok\t2026-08-25T23:45:00+00:00\t*/15 * * * *\t0
78647f267d37\tTroupe pipe — Zeph check-in watch\tdisabled\tpaused\t0\tok\t2026-10-29T00:29:07.971332+00:00\tevery 60m\t0
79f2aa4defce\tStackchain superfan — community watch and tip validation\tdisabled\tpaused\t0\tok\t2026-10-29T00:28:31.812961+00:00\tevery 120m\t0
7b586d62af70\tStackchain Spaces — Skinner mention catalog\tdisabled\tpaused\t0\tok\t2026-08-28T05:05:36.711807+00:00\tevery 360m\t0
7cc7b66428ae\tRepertoire — 17:00 Browser + Local Life Report\tdisabled\tpaused\t0\tok\t2026-09-28T21:00:00+00:00\t0 21 * * *\t0
7d03eabdd9b9\tX reply guy — occasional reverent replies\tdisabled\tpaused\t0\tok\t2026-10-28T23:59:39.400290+00:00\tevery 15m\t0
816981dfb712\tCareer Funnel — Batch 1 of 4 — Python Backend\tdisabled\tpaused\t0\tok\t2026-09-27T12:00:00+00:00\t0 12 * * *\t0
88f6a79368eb\tTM solo training\tdisabled\tpaused\t0\tok\t2026-11-02T18:42:16.873089+00:00\tevery 15m\t0
9ee1d9279adc\tOpen NightProwl Daily Drop candidate recheck\tcompleted\tcompleted\t0\tok\t\t20 9 * * *\t0
a348cb59a0c1\tForge Human Value Brief — Top Repositories\tdisabled\tpaused\t0\terror\t2026-08-31T17:00:00+00:00\t0 17 * * 1\t0
a476223d9a79\tCareer Funnel — Batch 4 of 4 — Remote Software Expansion\tdisabled\tpaused\t0\tok\t2026-09-26T22:30:00+00:00\t30 22 * * *\t0
a78064e1af61\tVincent Daily Drop Postflight Queue\tdisabled\tpaused\t0\tok\t2026-08-28T12:15:00+00:00\t15 12 * * *\t0
a9226ea81409\tSovereign Signal — Independent Film Retrospective\tdisabled\tpaused\t0\tNone\t2026-08-22T03:25:00+00:00\t25 * * * *\t0
aa5e13261842\tSovereign Signal — CPU Render State Machine\tdisabled\tpaused\t0\tok\t2026-08-28T04:39:43.087294+00:00\tevery 10m\t0
afe2bba4854b\tRunPod GPU availability watch\tdisabled\tpaused\t0\tok\t2026-10-25T23:36:39.862572+00:00\tevery 30m\t0
b28316cf7def\tDaily Drop Dual Delivery — 7 AM Eastern\tdisabled\tpaused\t0\tok\t2026-08-28T04:45:00+00:00\t*/15 * * * *\t0
b5e9b0fd4f4f\tTM spectator commentary\tdisabled\tpaused\t0\terror\t2026-11-01T21:54:25.197780+00:00\tevery 2m\t1
be22831260fb\tNightProwl LongCat Gitea dispatcher\tdisabled\tpaused\t0\tok\t2026-08-28T04:35:00+00:00\t*/5 * * * *\t0
c90e6f034448\tSovereign Signal — Private Human Gate\tdisabled\tpaused\t0\tok\t2026-08-25T23:50:25.641760+00:00\tevery 15m\t0
cb1c14f8ca68\tNightProwl Workshop Polish Churn\tdisabled\tpaused\t0\tok\t2026-08-28T04:45:00+00:00\t*/15 * * * *\t0
ccd3a4141f52\tRepertoire — 19:30 Visual Product Studio\tdisabled\tpaused\t0\terror\t2026-09-17T23:30:00+00:00\t30 23 * * *\t4
cd8a31f5ff9f\tEvaluate every new Pi harness release\tdisabled\tpaused\t0\terror\t2026-08-28T06:00:00+00:00\t0 */6 * * *\t0
cfb73a4cf7ad\tRepertoire — 14:00 Sovereign Media Studio\tdisabled\tpaused\t0\terror\t2026-09-17T18:00:00+00:00\t0 18 * * *\t3
d25576d90c98\tStackchain Lab Loop — Timmy\tdisabled\tpaused\t0\tok\t2026-08-28T04:44:17.497550+00:00\tevery 30m\t0
d60d505c172f\tStackchain Lab Daily Drop — Draft and First QC\tdisabled\tpaused\t0\tok\t2026-08-28T08:05:00+00:00\t5 8 * * *\t0
d7538b5bb73f\tSovereign Signal — 24h Discography Auto-Approval\tdisabled\tpaused\t0\tok\t2026-08-28T04:44:43.075698+00:00\tevery 15m\t0
dd9fa86bea9e\tAlexander Career Deep Work — Daily\tdisabled\tpaused\t0\tok\t2026-08-28T13:00:00+00:00\t0 13 * * *\t0
e16430881979\tPersonaPlex full-duplex demo (auto when GPU frees)\tdisabled\tpaused\t0\tok\t2026-10-28T22:18:39.255199+00:00\tevery 15m\t0
e267b564eb2d\tStackchain Lab Daily Drop — Candidate Correction Pass\tdisabled\tpaused\t0\terror\t2026-08-28T09:00:00+00:00\t0 9 * * *\t0
e61f5ce71d8b\tHourly Ops Pulse\tdisabled\tpaused\t0\terror\t2026-08-28T05:00:00+00:00\t0 * * * *\t0
ed23f1514bc2\tOpen NightProwl Daily Drop candidate QA\tcompleted\tcompleted\t0\tok\t\t35 8 * * *\t0
f9db4323dad4\tX mentions — prepared Telegram replies\tdisabled\tpaused\t0\tok\t2026-08-28T04:44:48.327606+00:00\tevery 15m\t0
fee83aa9ed16\t[bot:timmy] Stackchain forge change triage\tdisabled\tpaused\t0\tdelivery_failed\t2026-10-28T22:23:15.993991+00:00\tevery 15m\t0
3ac2f4a3f546\tX follows pitch monitor\tneeds_attention\tscheduled\t1\terror\t2026-11-05T08:42:31.282936+00:00\tevery 60m\t131
6cf100cea5f5\tEmail digest (pre-6am)\tneeds_attention\tscheduled\t1\terror\t2026-11-05T09:00:00+00:00\tevery day at 9am\t14
## Classification rules
- **live_ok** = enabled, state=`scheduled`, last_status=`ok`
- **disabled** = enabled=False
- **completed** = state=`completed` (audit only)
- **needs_attention** = state=`paused` or last_status=`error`
Source: `/root/.hermes/cron/jobs.json` (registry_updated_at = 2026-11-05T08:10:32.213311+00:00).
Run: `python3 fleet-triage-2026-11-05.py`.
+215
View File
@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""Fleet dispatch jobs triage classifier.
Reads /root/.hermes/cron/jobs.json (solo fleet dispatch registry) and
classifies every job entry by (enabled, state, last_status) into triage
buckets. Writes a JSON manifest + markdown report and mirrors them into the
committed repo tree at wizard-work/sol/.
Exit codes: 0 = wrote artifacts, nonzero = missing/invalid input.
"""
import collections
import json
import sys
SRC = "/root/.hermes/cron/jobs.json"
OUT_JSON = "/root/.hermes/wizard-outputs/sol/fleet-triage-2026-11-05.json"
OUT_MD = "/root/.hermes/wizard-outputs/sol/fleet-triage-2026-11-05.md"
REPO_DIR = "/root/troupe-standup/wizard-work/sol"
def load_jobs(path):
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
except (OSError, ValueError) as e:
print(f"FATAL: cannot read {path}: {e}", file=sys.stderr)
sys.exit(1)
jobs = data.get("jobs")
if not isinstance(jobs, list):
print(f"FATAL: {path} has no 'jobs' list", file=sys.stderr)
sys.exit(1)
return jobs, data.get("updated_at")
def triage_label(meta):
"""Classification label per the lane's rules.
Order matters: 'completed' lives outside 'disabled' so finished jobs are
not double-counted as merely disabled. 'needs_attention' only applies to
live (enabled) jobs that stalled.
"""
if meta["state"] == "completed":
return "completed"
if not meta["enabled"]:
return "disabled"
if meta["state"] == "scheduled" and meta["last_status"] == "ok":
return "live_ok"
if meta["state"] == "paused" or meta["last_status"] == "error":
return "needs_attention"
return "unknown"
def classify(jobs):
states = collections.Counter(j.get("state") for j in jobs)
statuses = collections.Counter(j.get("last_status") for j in jobs)
enabled = collections.Counter(bool(j.get("enabled")) for j in jobs)
rows = []
for j in jobs:
meta = {
"id": j.get("id"),
"name": j.get("name"),
"state": j.get("state"),
"enabled": bool(j.get("enabled")),
"last_status": j.get("last_status"),
"last_error": (j.get("last_error") or ""),
"last_run_at": j.get("last_run_at"),
"next_run_at": j.get("next_run_at"),
"schedule": j.get("schedule_display"),
"failure_streak": int(j.get("failure_streak") or 0),
"deliver": j.get("deliver"),
}
meta["risk"] = triage_label(meta)
rows.append(meta)
return states, statuses, enabled, rows
def build_report(jobs, updated_at, states, statuses, enabled, rows):
total = len(jobs)
live_ok = sum(1 for j in jobs if j.get("enabled") and j.get("state") == "scheduled" and j.get("last_status") == "ok")
disabled = sum(1 for j in jobs if not j.get("enabled"))
errored = sum(1 for j in jobs if j.get("last_status") == "error")
md = []
md.append("# Fleet dispatch triage — 2026-11-05 (sol)")
md.append("")
md.append(f"Registry: `{SRC}` (updated_at **{updated_at}**)")
md.append(f"Total jobs parsed: **{total}**")
md.append("")
md.append("## Headline")
md.append("")
md.append(f"Live (enabled, scheduled, last run ok): **{live_ok}**. "
f"Halted/suspended (disabled): **{disabled}**. "
f"Last run errored anywhere: **{errored}**. "
"Only the live slice delivers to the crew.")
md.append("")
md.append("## Counts")
md.append("")
md.append("| metric | count | meaning |")
md.append("|---|---|---|")
md.append(f"| total jobs | {total} | from `jobs.json` |")
md.append(f"| enabled | {enabled[True]} | dispatch enabled |")
md.append(f"| disabled | {disabled} | cannot run |")
md.append(f"| state scheduled | {states.get('scheduled', 0)} | due to run |")
md.append(f"| state paused | {states.get('paused', 0)} | suspended |")
md.append(f"| state completed | {states.get('completed', 0)} | finished, kept for audit |")
md.append(f"| last_status ok | {statuses.get('ok', 0)} | succeeded |")
md.append(f"| last_status error | {statuses.get('error', 0)} | last run errored |")
md.append(f"| last_status delivery_failed | {statuses.get('delivery_failed', 0)} | delivery errored |")
md.append(f"| last_status None | {statuses.get(None, 0)} | no recorded run |")
md.append("")
md.append("## Triage buckets")
md.append("")
md.append("| bucket | count | job ids |")
md.append("|---|---|---|")
for label in ["live_ok", "needs_attention", "disabled", "completed"]:
ids = sorted(m["id"] for m in rows if m["risk"] == label)
md.append(f"| {label} | {len(ids)} | {', '.join(ids)} |")
md.append("")
md.append("## Severity flags")
md.append("")
flush = collections.Counter()
for m in rows:
if m["state"] == "paused" and not m["enabled"]:
flush["disabled_paused"] += 1
if m["state"] == "completed":
flush["completed"] += 1
if m["last_status"] == "error":
flush["last_status_error"] += 1
if m["failure_streak"] >= 3:
flush["failure_streak_ge3"] += 1
for k, v in sorted(flush.items(), key=lambda kv: -kv[1]):
md.append(f"- **{k}**: {v}")
md.append("")
md.append("## Machine-readable rows (TSV)")
md.append("")
md.append("\\t".join(["id", "name", "risk", "state", "enabled", "last_status", "next_run_at", "schedule", "failure_streak"]))
for m in sorted(rows, key=lambda x: (x["risk"] != "live_ok", x["risk"] == "needs_attention", x["id"])):
md.append("\\t".join([
m["id"], m["name"], m["risk"], m["state"],
str(int(m["enabled"])), (m["last_status"] if m["last_status"] is not None else "None"),
m["next_run_at"] or "", m["schedule"], str(m["failure_streak"]),
]))
md.append("")
md.append("## Classification rules")
md.append("")
md.append("- **live_ok** = enabled, state=`scheduled`, last_status=`ok`")
md.append("- **disabled** = enabled=False")
md.append("- **completed** = state=`completed` (audit only)")
md.append("- **needs_attention** = state=`paused` or last_status=`error`")
md.append("")
md.append(f"Source: `{SRC}` (registry_updated_at = {updated_at}).")
md.append("")
md.append("Run: `python3 fleet-triage-2026-11-05.py`.")
return "\n".join(md)
def main():
jobs, updated_at = load_jobs(SRC)
states, statuses, enabled, rows = classify(jobs)
report = {
"generated": "2026-11-05",
"agent": "sol",
"source": SRC,
"registry_updated_at": updated_at,
"total_jobs": len(jobs),
"counts": {
"total": len(jobs),
"enabled": enabled[True],
"disabled": enabled[False],
"state_scheduled": states.get("scheduled", 0),
"state_paused": states.get("paused", 0),
"state_completed": states.get("completed", 0),
"last_status_ok": statuses.get("ok", 0),
"last_status_error": statuses.get("error", 0),
"last_status_delivery_failed": statuses.get("delivery_failed", 0),
"last_status_none": statuses.get(None, 0),
},
"bucket_counts": {
label: {"n": sum(1 for m in rows if m["risk"] == label),
"ids": sorted(m["id"] for m in rows if m["risk"] == label)}
for label in ["live_ok", "needs_attention", "disabled", "completed"]
},
"risk_flags": {
"disabled_paused": sum(1 for m in rows if m["state"] == "paused" and not m["enabled"]),
"last_status_error": statuses.get("error", 0),
"completed": states.get("completed", 0),
"failure_streak_ge3": sum(1 for m in rows if m["failure_streak"] >= 3),
"needs_attention": sum(1 for m in rows if m["risk"] == "needs_attention"),
},
"rows": rows,
}
with open(OUT_JSON, "w", encoding="utf-8") as f:
json.dump(report, f, indent=2, ensure_ascii=False)
md = build_report(jobs, updated_at, states, statuses, enabled, rows)
with open(OUT_MD, "w", encoding="utf-8") as f:
f.write(md)
with open(f"{REPO_DIR}/fleet-triage-2026-11-05.json", "w", encoding="utf-8") as f:
json.dump(report, f, indent=2, ensure_ascii=False)
with open(f"{REPO_DIR}/fleet-triage-2026-11-05.md", "w", encoding="utf-8") as f:
f.write(md)
print(f"wrote {OUT_JSON}")
print(f"wrote {OUT_MD}")
print(f"mirror -> {REPO_DIR}/fleet-triage-2026-11-05.json and {REPO_DIR}/fleet-triage-2026-11-05.md")
print(f"live_ok={report['counts']['state_scheduled']} (scheduled), "
f"paused={report['counts']['state_paused']}, "
f"completed={report['counts']['state_completed']}, "
f"err={report['counts']['last_status_error']}")
if __name__ == "__main__":
main()