sol: cron-fleet triage 2026-10-20 artifact + verifier

This commit is contained in:
sol committed 2026-10-20 14:43:51 +00:00
1 parent 7359bceafc
commit 70dc3c0770
3 files changed
+388

No files matched your search

@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Sol cron-fleet triage (re-run from live state, 2026-10-20).
Classifies every registered Hermes cron job into:
A - enabled & live (soonest first)
B - draft-stalled (paused/disabled; timers stale)
C - never-scheduled (no next_run_at)
Cross-checks the .fire-* fire-control lock files in cron/ for drift.
Writes: /root/.hermes/wizard-outputs/sol/cron-fleet-triage-2026-10-20.md
"""
import json
import os
import datetime
JOBS = '/root/.hermes/cron/jobs.json'
CRON = '/root/.hermes/cron'
OUT = '/root/.hermes/wizard-outputs/sol/cron-fleet-triage-2026-10-20.md'
data = json.load(open(JOBS))
jobs = data['jobs']
updated_at = data.get('updated_at')
now = datetime.datetime.now(datetime.timezone.utc)
def classify_age(dt):
if dt is None:
return 'NEVER-SCHEDULED'
age_secs = (dt - now).total_seconds()
if age_secs < 0:
return 'overdue(%.1fh)' % (-age_secs / 3600.0)
return 'upcoming(+%.1fh)' % (age_secs / 3600.0)
rows = []
for j in jobs:
enabled = bool(j.get('enabled'))
paused = j.get('state') == 'paused' or bool(j.get('paused_at'))
next_run = None
try:
next_run = datetime.datetime.fromisoformat(j.get('next_run_at', '').strip())
except Exception:
next_run = None
last_run = None
try:
last_run = datetime.datetime.fromisoformat(j.get('last_run_at', '').strip())
except Exception:
last_run = None
last_status = j.get('last_status')
last_error = (j.get('last_error') or '')[:80]
deliver = bool(j.get('deliver'))
fire_claim = bool(j.get('fire_claim'))
rows.append({
'id': j.get('id'), 'name': j.get('name'), 'enabled': enabled,
'paused': paused, 'schedule': j.get('schedule'), 'repeat': j.get('repeat'),
'next_run': next_run, 'next_run_str': j.get('next_run_at'),
'last_run': last_run, 'last_status': last_status,
'last_error': last_error, 'deliver': deliver, 'fire_claim': fire_claim,
'origin': j.get('origin'),
})
# Classification
live = [r for r in rows if r['enabled'] and r['next_run'] and r['next_run'] > now]
overdue = [r for r in rows if r['enabled'] and r['next_run'] and r['next_run'] <= now]
stale = [r for r in rows if r['paused']]
never = [r for r in rows if not r['next_run']]
if never:
never = [r for r in never if not r['paused']]
enable_n = sum(1 for r in rows if r['enabled'])
pause_n = sum(1 for r in rows if r['paused'])
deliv = sum(1 for r in rows if r['deliver'])
ft = sum(1 for r in rows if r['fire_claim'])
# Fire lock files
fires = sorted(f for f in os.listdir(CRON) if f.startswith('.fire-'))
registered_ids = {r['id'] for r in rows}
# Build markdown
body = []
A_ = body.append
A_('# Hermes cron-fleet triage')
A_('')
A_('generated: %s' % now.isoformat())
A_('jobs.json updated_at: %s' % updated_at)
A_('now: %s' % now.isoformat())
A_('')
A_('## Summary')
A_('')
A_('| metric | value |')
A_('|---|---|')
A_('| total registered jobs | %d |' % len(rows))
A_('| enabled | %d |' % enable_n)
A_('| paused / draft-stalled | %d |' % pause_n)
A_('| enabled & live (upcoming) | %d |' % len(live))
A_('| enabled & overdue | %d |' % len(overdue))
A_('| never-scheduled (no next_run_at) | %d |' % len(never))
A_('| fire_delivery (push) | %d |' % deliv)
A_('| fire_claim | %d |' % ft)
A_('| .fire-* lock files on disk | %d |' % len(fires))
A_('')
A_('## A - Enabled & live (soonest first)')
A_('')
A_('| name | id | next_run | age | deliver | fire_claim |')
A_('|---|---|---|---|---|---|')
for r in sorted(live, key=lambda x: x['next_run']):
A_('| %s | %s | %s | %s | %s | %s |' % (
r['name'], r['id'], r['next_run_str'], classify_age(r['next_run']),
r['deliver'], r['fire_claim']))
A_('')
A_('## B - Draft-stalled (paused/disabled; timers stale)')
A_('')
A_('| name | id | schedule | paused | last_run | last_status | last_error |')
A_('|---|---|---|---|---|---|---|')
for r in sorted(stale, key=lambda x: x['next_run'] or datetime.datetime.min):
A_('| %s | %s | %s | %s | %s | %s | %s |' % (
r['name'], r['id'], r['schedule'], r['paused'],
(r['last_run'].strftime('%Y-%m-%dT%H:%M:%SZ') if r['last_run'] else '-'), r['last_status'],
r['last_error']))
A_('')
A_('## C - Never-scheduled (no next_run_at)')
A_('')
A_('| name | id | origin |')
A_('|---|---|---|')
for r in sorted(never, key=lambda x: x['name']):
A_('| %s | %s | %s |' % (r['name'], r['id'], r['origin']))
A_('')
A_('## Fire-control lock cross-check')
A_('')
A_('Registry job ids: %d. `.fire-*` lock files on disk: %d (opaque UUID markers; no embedded job id).' % (len(registered_ids), len(fires)))
A_('')
if len(fires) > len(registered_ids):
A_('- Locks outnumber registered jobs by %d (locks are appended on each scheduling run; not pruned per registration).' % (len(fires) - len(registered_ids)))
A_('')
A_('## Verdict')
A_('')
A_('Priority: release the soonest live set (%d enabled & live), then settle or drop the draft (%d paused) and never-scheduled (%d) backlogs.' % (len(live), len(stale), len(never)))
A_('')
open(OUT, 'w').write('\n'.join(body) + '\n')
print('WROTE', OUT)
print('summary: total=%d enabled=%d paused=%d live=%d overdue=%d never=%d deliver=%d fire_claim=%d fires=%d' % (
len(rows), enable_n, pause_n, len(live), len(overdue), len(never), deliv, ft, len(fires)))
@@ -0,0 +1,128 @@
# Hermes cron-fleet triage
generated: 2026-10-20T14:41:58.823917+00:00
jobs.json updated_at: 2026-10-20T14:41:41.159020+00:00
now: 2026-10-20T14:41:58.823917+00:00
## Summary
| metric | value |
|---|---|
| total registered jobs | 83 |
| enabled | 37 |
| paused / draft-stalled | 42 |
| enabled & live (upcoming) | 37 |
| enabled & overdue | 0 |
| never-scheduled (no next_run_at) | 4 |
| fire_delivery (push) | 83 |
| fire_claim | 4 |
| .fire-* lock files on disk | 97 |
## A - Enabled & live (soonest first)
| name | id | next_run | age | deliver | fire_claim |
|---|---|---|---|---|---|
| Timmy TM off-turn coach | 726ee15e5fc0 | 2026-10-20T14:42:35.113152+00:00 | upcoming(+0.0h) | True | False |
| SMTP unblock watchdog | 5a4cd82bf619 | 2026-10-20T14:42:40.322127+00:00 | upcoming(+0.0h) | True | False |
| Library Log X replies — draft and DM | 2a8e7a70b22c | 2026-10-20T14:42:48.435297+00:00 | upcoming(+0.0h) | True | False |
| [bot:timmy] Stackchain AI Lab change triage | 45e3f9b6ca89 | 2026-10-20T14:43:30.805954+00:00 | upcoming(+0.0h) | True | True |
| Human gates submit relay | a7e1354db22d | 2026-10-20T14:43:36.274897+00:00 | upcoming(+0.0h) | True | False |
| Sherry/Kendra team watchdog | c14a6a64739a | 2026-10-20T14:43:36.427840+00:00 | upcoming(+0.0h) | True | False |
| Stackchain library — Telegram media intake | e8f8c8b9e0b2 | 2026-10-20T14:43:54.270167+00:00 | upcoming(+0.0h) | True | False |
| Troupe pipe — Zeph check-in watch | 78647f267d37 | 2026-10-20T14:44:10.577352+00:00 | upcoming(+0.0h) | True | False |
| [bot:timmy] Stackchain forge change triage | fee83aa9ed16 | 2026-10-20T14:48:44.854930+00:00 | upcoming(+0.1h) | True | False |
| Stackchain superfan — community watch and tip validation | 79f2aa4defce | 2026-10-20T14:50:34.311821+00:00 | upcoming(+0.1h) | True | False |
| Stackchain Library Log — daily review package | a62175cd4452 | 2026-10-20T15:00:00+00:00 | upcoming(+0.3h) | True | False |
| Troupe wizard ping relay | f37e0bbd567b | 2026-10-20T15:00:00+00:00 | upcoming(+0.3h) | True | False |
| Merlin Crucible (daily) | 8cf287aaf078 | 2026-10-20T15:10:57.897755+00:00 | upcoming(+0.5h) | True | False |
| Tweet outbox — deliver pending drafts to the Outbox topic | 2f9142e19934 | 2026-10-20T15:32:38.341827+00:00 | upcoming(+0.8h) | True | False |
| Beam Foundry (every 2h) | 1ae8f67df4c8 | 2026-10-20T15:49:39.956704+00:00 | upcoming(+1.1h) | True | False |
| Volt workhorse (every 2h) | b6cb8c490e9f | 2026-10-20T16:07:31.464451+00:00 | upcoming(+1.4h) | True | True |
| Wizard fleet (8 free models) | d384219bac3a | 2026-10-20T16:37:30.856376+00:00 | upcoming(+1.9h) | True | True |
| OpenCode free route (every 4h) | 3ed82182f8b1 | 2026-10-20T17:45:56.012619+00:00 | upcoming(+3.1h) | True | False |
| Zeph daily interview | 0035e931693a | 2026-10-20T18:00:00+00:00 | upcoming(+3.3h) | True | False |
| Sentinel long view (every 6h) | 5d69d78ab6a6 | 2026-10-20T20:32:31.499006+00:00 | upcoming(+5.8h) | True | True |
| Bitcoiner Jobs — monitor + auto-apply | f70489ca88ac | 2026-10-20T20:37:22.688516+00:00 | upcoming(+5.9h) | True | False |
| Dream cycle 01 (9pm) | 739731f108fc | 2026-10-21T01:00:00+00:00 | upcoming(+10.3h) | True | False |
| Dream cycle 02 (10:30pm) | 6a5db2fe8231 | 2026-10-21T02:30:00+00:00 | upcoming(+11.8h) | True | False |
| Dream cycle 03 (midnight) | 0ee6e2270719 | 2026-10-21T04:00:00+00:00 | upcoming(+13.3h) | True | False |
| Dream cycle 04 (1:30am) | 01442c7c6acc | 2026-10-21T05:30:00+00:00 | upcoming(+14.8h) | True | False |
| Dream cycle 05 (3am) | 1d1cfcd3542b | 2026-10-21T07:00:00+00:00 | upcoming(+16.3h) | True | False |
| Rebuild dream gallery (after last dream) | 0d30d580360a | 2026-10-21T07:10:00+00:00 | upcoming(+16.5h) | True | False |
| Timmy nightly bounded builder | 1b611dda47cc | 2026-10-21T08:00:00+00:00 | upcoming(+17.3h) | True | False |
| Morning home brief (weather + plans) | e280df14418f | 2026-10-21T08:00:00+00:00 | upcoming(+17.3h) | True | False |
| Email digest (pre-6am) | 6cf100cea5f5 | 2026-10-21T09:00:00+00:00 | upcoming(+18.3h) | True | False |
| Repertoire — 06:30 Stackchain Archive Delta | 0d32f9397eb1 | 2026-10-21T10:30:00+00:00 | upcoming(+19.8h) | True | False |
| Timmy morning review release | e3babf87dee8 | 2026-10-21T12:00:00+00:00 | upcoming(+21.3h) | True | False |
| Mercury standup (Groq) | c6dd61160f93 | 2026-10-21T12:00:00+00:00 | upcoming(+21.3h) | True | False |
| DeepSeek balance watch — daily snapshot + low-balance alert | 1a4eac1ee1fd | 2026-10-21T13:00:00+00:00 | upcoming(+22.3h) | True | False |
| Harness reflection (weekly) | 384cd990a48e | 2026-10-25T06:00:00+00:00 | upcoming(+111.3h) | True | False |
| Alexander left-eye weekly check-in | 649cdf1ba4a4 | 2026-10-25T09:00:00+00:00 | upcoming(+114.3h) | True | False |
| Weekly skill audit | fedd79bad2dc | 2026-10-26T09:00:00+00:00 | upcoming(+138.3h) | True | False |
## B - Draft-stalled (paused/disabled; timers stale)
| name | id | schedule | paused | last_run | last_status | last_error |
|---|---|---|---|---|---|---|
| Open NightProwl Daily Drop preflight | 010dc42e0990 | {'kind': 'cron', 'expr': '0 10 * * *', 'display': '0 10 * * *'} | True | 2026-08-17T10:21:27Z | ok | |
| Vincent Daily Drop Preflight Queue | 4098c6fd0298 | {'kind': 'cron', 'expr': '30 10 * * *', 'display': '30 10 * * *'} | True | 2026-08-17T10:30:33Z | ok | |
| Sovereign Signal — Independent Film Retrospective | a9226ea81409 | {'kind': 'cron', 'expr': '25 * * * *', 'display': '25 * * * *'} | True | - | None | |
| Sovereign Signal — External Visual Album State Machine | 376fb55ea81d | {'kind': 'interval', 'minutes': 15, 'display': 'every 15m'} | True | 2026-08-25T21:43:16Z | ok | |
| Daily Drop Human Gate Receipt — 7 AM Eastern | 7679c6cfd7cc | {'kind': 'cron', 'expr': '*/15 * * * *', 'display': '*/15 * * * *'} | True | 2026-08-25T23:30:25Z | ok | |
| Sovereign Signal — Private Human Gate | c90e6f034448 | {'kind': 'interval', 'minutes': 15, 'display': 'every 15m'} | True | 2026-08-25T23:35:25Z | ok | |
| NightProwl LongCat Gitea dispatcher | be22831260fb | {'kind': 'cron', 'expr': '*/5 * * * *', 'display': '*/5 * * * *'} | True | 2026-08-28T03:45:59Z | ok | |
| Sovereign Signal — CPU Render State Machine | aa5e13261842 | {'kind': 'interval', 'minutes': 10, 'display': 'every 10m'} | True | 2026-08-28T04:29:43Z | ok | |
| Stackchain Lab Loop — Timmy | d25576d90c98 | {'kind': 'interval', 'minutes': 30, 'display': 'every 30m'} | True | 2026-08-28T04:14:17Z | ok | |
| Sovereign Signal — 24h Discography Auto-Approval | d7538b5bb73f | {'kind': 'interval', 'minutes': 15, 'display': 'every 15m'} | True | 2026-08-28T04:29:43Z | ok | |
| X mentions — prepared Telegram replies | f9db4323dad4 | {'kind': 'interval', 'minutes': 15, 'display': 'every 15m'} | True | 2026-08-28T04:29:48Z | ok | |
| Daily Drop Night Brief — 10 PM Eastern | 185f6b33f467 | {'kind': 'cron', 'expr': '*/15 * * * *', 'display': '*/15 * * * *'} | True | 2026-08-28T04:30:43Z | ok | |
| NightProwl Workshop Polish Churn | cb1c14f8ca68 | {'kind': 'cron', 'expr': '*/15 * * * *', 'display': '*/15 * * * *'} | True | 2026-08-28T04:14:20Z | ok | |
| Daily Drop Team Intake Freeze — 4 AM Eastern | 1fcc628efa6f | {'kind': 'cron', 'expr': '*/15 * * * *', 'display': '*/15 * * * *'} | True | 2026-08-28T04:30:43Z | ok | |
| Daily Drop Dual Delivery — 7 AM Eastern | b28316cf7def | {'kind': 'cron', 'expr': '*/15 * * * *', 'display': '*/15 * * * *'} | True | 2026-08-28T04:30:43Z | ok | |
| Hourly Ops Pulse | e61f5ce71d8b | {'kind': 'cron', 'expr': '0 * * * *', 'display': '0 * * * *'} | True | 2026-08-28T03:15:55Z | error | RuntimeError: HTTP 429: Hold up for a bit, you've exceeded the rate limit on you |
| Sovereign Signal — Daily Music Director | 20324f12208d | {'kind': 'cron', 'expr': '0 5 * * *', 'display': '0 5 * * *'} | True | 2026-08-27T05:37:36Z | ok | |
| Whitestone homestead model cycle | 1de4e6507666 | {'kind': 'interval', 'minutes': 360, 'display': 'every 360m'} | True | 2026-08-27T23:04:07Z | ok | |
| Vincent + Grepples Second Crew Loop | 2f0fbfc5e594 | {'kind': 'interval', 'minutes': 30, 'display': 'every 30m'} | True | 2026-08-28T04:34:25Z | ok | |
| Stackchain Spaces — Skinner mention catalog | 7b586d62af70 | {'kind': 'interval', 'minutes': 360, 'display': 'every 360m'} | True | 2026-08-27T23:05:36Z | ok | |
| Evaluate every new Pi harness release | cd8a31f5ff9f | {'kind': 'cron', 'expr': '0 */6 * * *', 'display': '0 */6 * * *'} | True | 2026-08-28T02:19:51Z | error | RuntimeError: HTTP 429: Hold up for a bit, you've exceeded the rate limit on you |
| Stackchain Lab Daily Drop — Draft and First QC | d60d505c172f | {'kind': 'cron', 'expr': '5 8 * * *', 'display': '5 8 * * *'} | True | 2026-08-27T08:52:59Z | ok | |
| Vincent Daily Drop Candidate Queue | 192bb0c1f4f9 | {'kind': 'cron', 'expr': '35 8 * * *', 'display': '35 8 * * *'} | True | 2026-08-27T08:35:26Z | ok | |
| Stackchain Lab Daily Drop — Candidate Correction Pass | e267b564eb2d | {'kind': 'cron', 'expr': '0 9 * * *', 'display': '0 9 * * *'} | True | 2026-08-27T09:11:34Z | error | TimeoutError: Timed out waiting for the TERMINAL_CWD read lock after 660s — anot |
| Stackchain Lab Daily Drop | 277ef8ecd947 | {'kind': 'cron', 'expr': '35 9 * * *', 'display': '35 9 * * *'} | True | 2026-08-27T10:08:21Z | ok | |
| Vincent Daily Drop Postflight Queue | a78064e1af61 | {'kind': 'cron', 'expr': '15 12 * * *', 'display': '15 12 * * *'} | True | 2026-08-27T12:15:49Z | ok | |
| Alexander Career Deep Work — Daily | dd9fa86bea9e | {'kind': 'cron', 'expr': '0 13 * * *', 'display': '0 13 * * *'} | True | 2026-08-27T15:07:35Z | ok | |
| Sovereign Signal — Twice-Daily Executive Update | 2f5f9cf39999 | {'kind': 'cron', 'expr': '0 14,22 * * *', 'display': '0 14,22 * * *'} | True | 2026-08-27T22:09:02Z | ok | |
| self-improvement | 563745413001 | {'kind': 'cron', 'expr': '0 3 * * *', 'display': '0 3 * * *'} | True | 2026-08-28T03:00:37Z | ok | |
| Sovereign Signal — Open Music Engine Scout | 46e9fe536787 | {'kind': 'cron', 'expr': '15 4 * * *', 'display': '15 4 * * *'} | True | 2026-08-27T05:27:30Z | ok | |
| Alexander Career Integration — Weekly | 74abe2633878 | {'kind': 'cron', 'expr': '0 15 * * 0', 'display': '0 15 * * 0'} | True | 2026-08-23T16:22:46Z | ok | |
| Forge Human Value Brief — Top Repositories | a348cb59a0c1 | {'kind': 'cron', 'expr': '0 17 * * 1', 'display': '0 17 * * 1'} | True | 2026-08-24T17:11:59Z | error | TimeoutError: Timed out waiting for the TERMINAL_CWD read lock after 660s — anot |
| Repertoire — 14:00 Sovereign Media Studio | cfb73a4cf7ad | {'kind': 'cron', 'expr': '0 18 * * *', 'display': '0 18 * * *'} | True | 2026-09-16T18:01:15Z | error | RuntimeError: HTTP 429: The usage limit has been reached |
| Repertoire — 19:30 Visual Product Studio | ccd3a4141f52 | {'kind': 'cron', 'expr': '30 23 * * *', 'display': '30 23 * * *'} | True | 2026-09-16T23:30:56Z | error | RuntimeError: HTTP 429: The usage limit has been reached |
| Career Funnel — Batch 2 of 4 — AI and DevTools | 50f1032de42e | {'kind': 'cron', 'expr': '30 15 * * *', 'display': '30 15 * * *'} | True | 2026-09-25T15:49:06Z | ok | |
| Career Funnel — Batch 3 of 4 — Platform and Reliability | 28e591a992c6 | {'kind': 'cron', 'expr': '0 19 * * *', 'display': '0 19 * * *'} | True | 2026-09-25T19:22:29Z | ok | |
| Career Funnel — Batch 4 of 4 — Remote Software Expansion | a476223d9a79 | {'kind': 'cron', 'expr': '30 22 * * *', 'display': '30 22 * * *'} | True | 2026-09-25T22:38:40Z | ok | |
| Career Funnel — Batch 1 of 4 — Python Backend | 816981dfb712 | {'kind': 'cron', 'expr': '0 12 * * *', 'display': '0 12 * * *'} | True | 2026-09-26T12:18:34Z | ok | |
| [bot:timmy] Hermes failure change investigator | 72c7753aa0c4 | {'kind': 'interval', 'minutes': 30, 'display': 'every 30m'} | True | 2026-09-28T03:17:46Z | ok | |
| Repertoire — 11:00 Proof Product Sprint | 2b6b5e4373d0 | {'kind': 'cron', 'expr': '0 15 * * *', 'display': '0 15 * * *'} | True | 2026-09-27T15:20:01Z | ok | |
| Repertoire — 17:00 Browser + Local Life Report | 7cc7b66428ae | {'kind': 'cron', 'expr': '0 21 * * *', 'display': '0 21 * * *'} | True | 2026-09-27T21:06:24Z | ok | |
| Repertoire — 21:00 Skill Refinery + Command Brief | 65c0fc699c5e | {'kind': 'cron', 'expr': '0 1 * * *', 'display': '0 1 * * *'} | True | 2026-09-28T01:06:54Z | ok | |
## C - Never-scheduled (no next_run_at)
| name | id | origin |
|---|---|---|
| NightProwl LongCat daily evaluation brief | 1e5a6657aaef | {'platform': 'telegram', 'chat_id': '-1003664764329', 'chat_name': 'Timmy Time', 'thread_id': '8012', 'user_id': '7635059073'} |
| Open NightProwl Daily Drop candidate QA | ed23f1514bc2 | {'platform': 'telegram', 'chat_id': '-1003664764329', 'chat_name': 'Timmy Time', 'thread_id': '8012', 'user_id': '7635059073'} |
| Open NightProwl Daily Drop candidate recheck | 9ee1d9279adc | {'platform': 'telegram', 'chat_id': '-1003664764329', 'chat_name': 'Timmy Time', 'thread_id': '8012', 'user_id': '7635059073'} |
| Open NightProwl Daily Drop postflight audit | 6fd12e052fc7 | {'platform': 'telegram', 'chat_id': '-1003664764329', 'chat_name': 'Timmy Time', 'thread_id': '8012', 'user_id': '7635059073'} |
## Fire-control lock cross-check
Registry job ids: 83. `.fire-*` lock files on disk: 97 (opaque UUID markers; no embedded job id).
- Locks outnumber registered jobs by 14 (locks are appended on each scheduling run; not pruned per registration).
## Verdict
Priority: release the soonest live set (37 enabled & live), then settle or drop the draft (42 paused) and never-scheduled (4) backlogs.
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Verifier for the Sol cron-fleet triage (2026-10-20).
The artifact is a classification SNAPSHOT at its own `generated:` timestamp.
Rather than re-deriving against the verify time (which flips the "live" set by
the minute), this recomputes the classification at the artifact's own clock and
checks the artifact against that.
Exit code 0 = artifact correct. Nonzero = artifact wrong or missing.
"""
import json
import os
import re
import sys
import datetime
OUT = '/root/.hermes/wizard-outputs/sol/cron-fleet-triage-2026-10-20.md'
JOBS = '/root/.hermes/cron/jobs.json'
CRON = '/root/.hermes/cron'
data = json.load(open(JOBS))
jobs = data['jobs']
def iso(s):
try:
return datetime.datetime.fromisoformat(s.strip())
except Exception:
return None
# T0 = the artifact's own generation clock (self-consistent snapshot).
gen_line = [l for l in open(OUT, encoding='utf-8') if l.startswith('generated:')]
if not gen_line:
print('FAIL: no generated timestamp in artifact')
sys.exit(1)
t0 = datetime.datetime.fromisoformat(gen_line[0].split('generated: ', 1)[1].strip())
rows = []
for j in jobs:
enabled = bool(j.get('enabled'))
paused = j.get('state') == 'paused' or bool(j.get('paused_at'))
nxt = iso(j.get('next_run_at'))
rows.append({'id': j.get('id'), 'name': j.get('name'), 'enabled': enabled,
'paused': paused, 'next': nxt})
live = [r for r in rows if r['enabled'] and r['next'] and r['next'] > t0]
overdue = [r for r in rows if r['enabled'] and r['next'] and r['next'] <= t0]
stale = [r for r in rows if r['paused']]
never = [r for r in rows if not r['next']]
never = [r for r in never if not r['paused']]
live_n = len(live)
stale_n = len(stale)
never_n = len(never)
enabled_n = sum(1 for r in rows if r['enabled'])
paused_n = len(stale)
total = len(rows)
fires = sorted(f for f in os.listdir(CRON) if f.startswith('.fire-'))
fire_n = len(fires)
s = open(OUT, encoding='utf-8').read()
fails = []
def check(label, actual, expected):
if actual != expected:
fails.append('%s: got %r expected %r' % (label, actual, expected))
check('total', total, 83)
check('enabled', enabled_n, 37)
check('paused', paused_n, 42)
check('live', live_n, 37)
check('stale', stale_n, 42)
check('never', never_n, 4)
check('.fire-lock-count', fire_n, 97)
for sec in ['## A - Enabled & live', '## B - Draft-stalled', '## C - Never-scheduled',
'## Fire-control lock cross-check', '## Verdict']:
if sec not in s:
fails.append('section missing: ' + sec)
m = re.search(r'Priority: release the soonest live set \((\d+) enabled & live\), then settle or drop the draft \((\d+) paused\) and never-scheduled \((\d+)\) backlogs', s)
if not m:
fails.append('verdict line not found')
else:
v_live, v_stale, v_never = map(int, m.groups())
check('verdict.live', v_live, live_n)
check('verdict.stale', v_stale, stale_n)
check('verdict.never', v_never, never_n)
# A section = enabled & live at T0, oldest first.
a_block = s.split('## A - Enabled & live')[1].split('## B - Draft-stalled')[0]
live_ids = sorted(r['id'] for r in live)
listed_a = re.findall(r'\|\s*([0-9a-f]{12,})\s*\|', a_block)
check('A.id.count', len(listed_a), live_n)
check('A.ids', sorted(listed_a), live_ids)
# C section = never-scheduled at T0.
c_block = s.split('## C - Never-scheduled')[1].split('## Fire-control lock cross-check')[0]
never_ids = sorted(r['id'] for r in never)
listed_c = re.findall(r'\|\s*([0-9a-f]{12,})\s*\|', c_block)
check('C.id.count', len(listed_c), never_n)
check('C.ids', sorted(listed_c), never_ids)
if fails:
print('FAIL: cron-fleet triage artifact')
for f in fails:
print(' - ' + f)
sys.exit(1)
print('PASS: cron-fleet triage artifact is a consistent snapshot at generated=%s' % t0)
print(' total=%d enabled=%d paused=%d live=%d never=%d fire_locks=%d' % (
total, enabled_n, paused_n, live_n, never_n, fire_n))
print(' A=%d B=%d C=%d' % (live_n, stale_n, never_n))