diff --git a/wizard-work/sol/triage-daily-2026-10-22.json b/wizard-work/sol/triage-daily-2026-10-22.json new file mode 100644 index 0000000..0ee659d --- /dev/null +++ b/wizard-work/sol/triage-daily-2026-10-22.json @@ -0,0 +1,63 @@ +{ + "generated_utc": "2026-10-22T02:47:42Z", + "classify_rule": "AGE_CLASSIFY: ACTIVE <=24h, WATCH <=168h, else STALE", + "source": "raw daily logs under /root/.hermes/wizard-outputs", + "lanes": [ + { + "id": "forge", + "latest_log": "forge-2026-10-16-1446.txt", + "latest_mtime_utc": "2026-10-16T14:56:18Z", + "daily_log_count": 56, + "age_hours": 131.9, + "status": "WATCH" + }, + { + "id": "ledger", + "latest_log": "ledger-2026-10-16-1446.txt", + "latest_mtime_utc": "2026-10-16T14:47:33Z", + "daily_log_count": 56, + "age_hours": 132.0, + "status": "WATCH" + }, + { + "id": "null", + "latest_log": "null-2026-10-16-1446.txt", + "latest_mtime_utc": "2026-10-16T14:47:52Z", + "daily_log_count": 56, + "age_hours": 132.0, + "status": "WATCH" + }, + { + "id": "quill", + "latest_log": "quill-2026-10-16-1446.txt", + "latest_mtime_utc": "2026-10-16T14:47:07Z", + "daily_log_count": 56, + "age_hours": 132.0, + "status": "WATCH" + }, + { + "id": "sphinx", + "latest_log": "sphinx-2026-10-16-1446.txt", + "latest_mtime_utc": "2026-10-16T14:50:35Z", + "daily_log_count": 56, + "age_hours": 132.0, + "status": "WATCH" + }, + { + "id": "sol", + "latest_log": "sol-2026-10-16-1446.txt", + "latest_mtime_utc": "2026-10-16T14:49:04Z", + "daily_log_count": 56, + "age_hours": 132.0, + "status": "WATCH" + }, + { + "id": "spark", + "latest_log": "spark-2026-10-16-1446.txt", + "latest_mtime_utc": "2026-10-16T14:56:18Z", + "daily_log_count": 56, + "age_hours": 131.9, + "status": "WATCH" + } + ] +} \ No newline at end of file diff --git a/wizard-work/sol/triage-daily-2026-10-22.md b/wizard-work/sol/triage-daily-2026-10-22.md new file mode 100644 index 0000000..db021c8 --- /dev/null +++ b/wizard-work/sol/triage-daily-2026-10-22.md @@ -0,0 +1,20 @@ +# Daily-ledger triage — sol — 2026-10-22 + +Recomputed from raw logs at 2026-10-22T02:47:42Z. + +| lane | latest log | latest mtime (UTC) | logs | age (h) | status | +|---|---|---|---|---|---| +| forge | forge-2026-10-16-1446.txt | 2026-10-16T14:56:18Z | 56 | 131.9 | WATCH | +| ledger | ledger-2026-10-16-1446.txt | 2026-10-16T14:47:33Z | 56 | 132.0 | WATCH | +| null | null-2026-10-16-1446.txt | 2026-10-16T14:47:52Z | 56 | 132.0 | WATCH | +| quill | quill-2026-10-16-1446.txt | 2026-10-16T14:47:07Z | 56 | 132.0 | WATCH | +| sol | sol-2026-10-16-1446.txt | 2026-10-16T14:49:04Z | 56 | 132.0 | WATCH | +| spark | spark-2026-10-16-1446.txt | 2026-10-16T14:56:18Z | 56 | 131.9 | WATCH | +| sphinx | sphinx-2026-10-16-1446.txt | 2026-10-16T14:50:35Z | 56 | 132.0 | WATCH | + +## Summary + +- **7** lanes total +- **0** ACTIVE +- **7** WATCH +- **0** STALE diff --git a/wizard-work/sol/triage_gen_daily.py b/wizard-work/sol/triage_gen_daily.py new file mode 100644 index 0000000..0d93c09 --- /dev/null +++ b/wizard-work/sol/triage_gen_daily.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Sol lane — daily-ledger triage (fresh). + +Recomputes the classification straight from the raw daily logs under +/root/.hermes/wizard-outputs (no cache), and writes a self-contained +artifact: triage-daily-2026-10-22.json + triage-daily-2026-10-22.md. + +AGE_CLASSIFY rule: age <= 24h -> ACTIVE; age <= 168h -> WATCH; else STALE. +The 7 ids are the primary daily-ledger lanes the repository's own +triage_check.py already names (forge, ledger, null, quill, sphinx, sol, spark). +""" +import glob +import json +import os +from datetime import datetime, timezone + +BASE = "/root/.hermes/wizard-outputs" +OUT_JSON = os.path.join(BASE, "sol", "triage-daily-2026-10-22.json") +OUT_MD = os.path.join(BASE, "sol", "triage-daily-2026-10-22.md") + +PRIMARY = ["forge", "ledger", "null", "quill", "sphinx", "sol", "spark"] + + +def classify(age_sec): + age_h = age_sec / 3600.0 + if age_h <= 24.0: + return "ACTIVE" + if age_h <= 168.0: + return "WATCH" + return "STALE" + + +def main(): + now = datetime.now(timezone.utc) + lanes = [] + for pid in PRIMARY: + pattern = os.path.join(BASE, pid + "*-*.txt") + files = sorted(glob.glob(pattern)) + if not files: + raise SystemExit("no raw logs for lane %s" % pid) + latest = max(files, key=os.path.getmtime) + mtime = os.path.getmtime(latest) + age_sec = (now - datetime.fromtimestamp(mtime, tz=timezone.utc)).total_seconds() + lanes.append({ + "id": pid, + "latest_log": os.path.basename(latest), + "latest_mtime_utc": datetime.fromtimestamp(mtime, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "daily_log_count": len(files), + "age_hours": round(age_sec / 3600.0, 1), + "status": classify(age_sec), + }) + + artifact = { + "generated_utc": now.strftime("%Y-%m-%dT%H:%M:%SZ"), + "classify_rule": "AGE_CLASSIFY: ACTIVE <=24h, WATCH <=168h, else STALE", + "source": "raw daily logs under %s" % BASE, + "lanes": lanes, + } + with open(OUT_JSON, "w", encoding="utf-8") as fh: + json.dump(artifact, fh, indent=2) + + lines = [] + lines.append("# Daily-ledger triage — sol — 2026-10-22") + lines.append("") + lines.append("Recomputed from raw logs at %s." % now.strftime("%Y-%m-%dT%H:%M:%SZ")) + lines.append("") + lines.append("| lane | latest log | latest mtime (UTC) | logs | age (h) | status |") + lines.append("|---|---|---|---|---|---|") + for e in sorted(lanes, key=lambda x: x["id"]): + lines.append("| %s | %s | %s | %d | %.1f | %s |" % ( + e["id"], e["latest_log"], e["latest_mtime_utc"], e["daily_log_count"], + e["age_hours"], e["status"])) + lines.append("") + lines.append("## Summary") + lines.append("") + counts = {} + for e in lanes: + counts[e["status"]] = counts.get(e["status"], 0) + 1 + lines.append("- **%d** lanes total" % len(lanes)) + for s in ("ACTIVE", "WATCH", "STALE"): + lines.append("- **%d** %s" % (counts.get(s, 0), s)) + lines.append("") + with open(OUT_MD, "w", encoding="utf-8") as fh: + fh.write("\n".join(lines)) + + print("WROTE", OUT_JSON) + print("WROTE", OUT_MD) + for e in sorted(lanes, key=lambda x: x["id"]): + print(" %-6s %-24s %s age=%-6.1fh %s" % ( + e["id"], e["latest_log"], e["latest_mtime_utc"], e["age_hours"], e["status"])) + print(" counts:", {"ACTIVE": counts.get("ACTIVE", 0), + "WATCH": counts.get("WATCH", 0), + "STALE": counts.get("STALE", 0)}) + + +if __name__ == "__main__": + main() diff --git a/wizard-work/sol/triage_verify_daily.py b/wizard-work/sol/triage_verify_daily.py new file mode 100644 index 0000000..e86265a --- /dev/null +++ b/wizard-work/sol/triage_verify_daily.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Independent re-check for sol's daily-ledger triage artifact. + +Recomputes the classification from the raw daily logs under +/root/.hermes/wizard-outputs and diffs it against +triage-daily-2026-10-22.json. Exit 0 = artifact is correct, nonzero = wrong. + +Independent of the generator: it never reads the artifact during recompute, +and it validates the 7-lane id set, the latest-log name, the mtime, log count, +and the AGE_CLASSIFY status, so a wrong classifier is caught. +""" +import glob +import json +import os +import sys +from datetime import datetime, timezone + +BASE = "/root/.hermes/wizard-outputs" +ARTIFACT = os.path.join(BASE, "sol", "triage-daily-2026-10-22.json") +PRIMARY = ["forge", "ledger", "null", "quill", "sphinx", "sol", "spark"] + + +def classify(age_sec): + age_h = age_sec / 3600.0 + if age_h <= 24.0: + return "ACTIVE" + if age_h <= 168.0: + return "WATCH" + return "STALE" + + +def recompute(): + out = [] + for pid in PRIMARY: + files = sorted(glob.glob(os.path.join(BASE, pid + "*-*.txt"))) + if not files: + raise SystemExit("no raw logs for lane %s" % pid) + latest = max(files, key=os.path.getmtime) + mtime = os.path.getmtime(latest) + age_sec = (datetime.now(timezone.utc) + - datetime.fromtimestamp(mtime, tz=timezone.utc)).total_seconds() + out.append({ + "id": pid, + "latest_log": os.path.basename(latest), + "latest_mtime_utc": datetime.fromtimestamp(mtime, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "daily_log_count": len(files), + "age_hours": round(age_sec / 3600.0, 1), + "status": classify(age_sec), + }) + return out + + +def main(): + expected = recompute() + with open(ARTIFACT, encoding="utf-8") as fh: + actual = json.load(fh).get("lanes", []) + + errors = [] + if len(expected) != len(actual): + errors.append("lane count mismatch: expected %d got %d" % (len(expected), len(actual))) + + by_id = {a.get("id"): a for a in actual} + for e in expected: + a = by_id.get(e["id"]) + if a is None: + errors.append("lane %s missing from artifact" % e["id"]) + continue + for k in ("latest_log", "latest_mtime_utc", "daily_log_count", "age_hours", "status"): + if a.get(k) != e[k]: + errors.append("%s.%s: expected %r got %r" % (e["id"], k, e[k], a.get(k))) + + if sorted(by_id) != sorted(PRIMARY): + errors.append("lane id set mismatch: %s" % sorted(by_id)) + + if errors: + print("VERIFY FAIL") + for err in errors: + print(" -", err) + sys.exit(1) + + print("VERIFY OK: %d/%d lanes match" % (len(expected), len(expected))) + for e in expected: + print(" %-6s %-24s %s age=%-6.1fh %s" % ( + e["id"], e["latest_log"], e["latest_mtime_utc"], e["age_hours"], e["status"])) + sys.exit(0) + + +if __name__ == "__main__": + main()