158 lines
6.4 KiB
Python
158 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate today's sol-lane daily ledger triage.
|
|
|
|
Triage of the seven primary workflow lanes' daily ledger logs under
|
|
/root/.hermes/wizard-outputs/, classified by measured age of the latest
|
|
log: ACTIVE (<=24h), WATCH (<=168h), else STALE (>168h).
|
|
|
|
Reads real file mtimes with os.stat (no hand-typed timestamps) and writes
|
|
a machine-readable JSON and a human-readable MD.
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
|
|
BASE = "/root/.hermes/wizard-outputs"
|
|
LANES = ["forge", "ledger", "null", "quill", "sphinx", "sol", "spark"]
|
|
REPO_OUT = "/root/troupe-standup/wizard-work/sol"
|
|
PROFILE_OUT = os.path.join(BASE, "sol")
|
|
|
|
|
|
def utc_iso(epoch):
|
|
return datetime.fromtimestamp(epoch, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
|
def classify(epoch, now_epoch):
|
|
age_h = (now_epoch - epoch) / 3600.0
|
|
if age_h <= 24.0:
|
|
return "ACTIVE", age_h
|
|
if age_h <= 168.0:
|
|
return "WATCH", age_h
|
|
return "STALE", age_h
|
|
|
|
|
|
def latest_log(lane):
|
|
"""Daily ledger logs live at BASE root as '<lane>-<date>-<time>.txt."""
|
|
d = BASE
|
|
files = [f for f in os.listdir(d) if f.startswith(lane + "-") and f.endswith(".txt")]
|
|
if not files:
|
|
return None, None
|
|
latest = max(files, key=lambda f: os.path.getmtime(os.path.join(d, f)))
|
|
return latest, os.path.getmtime(os.path.join(d, latest))
|
|
|
|
|
|
def main():
|
|
now = datetime.now(timezone.utc)
|
|
now_epoch = now.timestamp()
|
|
lanes = []
|
|
for lane in LANES:
|
|
latest, mtime = latest_log(lane)
|
|
if latest is None:
|
|
status, age_h = "STALE", None
|
|
best = None
|
|
else:
|
|
status, age_h = classify(mtime, now_epoch)
|
|
best = latest
|
|
lanes.append({
|
|
"id": lane,
|
|
"latest_log": best if best else None,
|
|
"latest_mtime_utc": utc_iso(mtime) if mtime else None,
|
|
"age_hours": round(age_h, 2) if age_h is not None else None,
|
|
"daily_log_count": sum(
|
|
1 for f in os.listdir(BASE) if f.startswith(lane + "-") and f.endswith(".txt")
|
|
),
|
|
"status": status,
|
|
})
|
|
|
|
record = {
|
|
"generated_utc": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
"generated_at": now.isoformat(),
|
|
"scope": "seven primary workflow lanes' daily ledger logs under /root/.hermes/wizard-outputs/",
|
|
"classification": "ACTIVE (<=24h), WATCH (<=168h), else STALE (>168h)",
|
|
"lanes": lanes,
|
|
}
|
|
|
|
counts = {"ACTIVE": 0, "WATCH": 0, "STALE": 0}
|
|
for l in lanes:
|
|
counts[l["status"]] += 1
|
|
|
|
os.makedirs(REPO_OUT, exist_ok=True)
|
|
os.makedirs(PROFILE_OUT, exist_ok=True)
|
|
|
|
json_repo = os.path.join(REPO_OUT, "Sol-Lane-Triage-2026-10-24.json")
|
|
json_profile = os.path.join(PROFILE_OUT, "Sol-Lane-Triage-2026-10-24.json")
|
|
for p in (json_repo, json_profile):
|
|
with open(p, "w", encoding="utf-8") as fh:
|
|
json.dump(record, fh, indent=2)
|
|
fh.write("\n")
|
|
|
|
lines = []
|
|
lines.append("# Sol Lane Daily Ledger Triage - 2026-10-24")
|
|
lines.append("")
|
|
lines.append("## Verdict")
|
|
lines.append("")
|
|
lines.append(f"- Scope: seven primary workflow lanes' daily ledger logs under /root/.hermes/wizard-outputs/.")
|
|
lines.append(f"- Classification (measured stage): ACTIVE (<=24h), WATCH (<=168h), else STALE (>168h).")
|
|
lines.append("")
|
|
lines.append(f"- Generated (UTC): {record['generated_utc']}")
|
|
lines.append("")
|
|
lines.append("## Summary")
|
|
lines.append("")
|
|
lines.append("| Status | Count |")
|
|
lines.append("|---|---|")
|
|
lines.append(f"| ACTIVE | {counts['ACTIVE']} |")
|
|
lines.append(f"| WATCH | {counts['WATCH']} |")
|
|
lines.append(f"| STALE | {counts['STALE']} |")
|
|
lines.append("")
|
|
ages = [l["age_hours"] for l in lanes if l["age_hours"] is not None]
|
|
if ages:
|
|
lines.append(f"- As of {now.strftime('%Y-%m-%dT%H:%M:%SZ')} UTC, the oldest gap is "
|
|
f"{max(ages):.1f}h ({max(l['id'] for l in lanes if l['age_hours'] == max(ages))}).")
|
|
lines.append("")
|
|
lines.append("## Per-lane table")
|
|
lines.append("")
|
|
lines.append("| Lane | Latest daily log | Last written (UTC) | Age | Status |")
|
|
lines.append("|---|---|---|---|---|")
|
|
for l in sorted(lanes, key=lambda x: (x["status"] != "STALE", x["age_hours"] or 0), reverse=True):
|
|
lines.append(f"| {l['id']} | {l['latest_log']} | {l['latest_mtime_utc']} | "
|
|
f"{l['age_hours'] if l['age_hours'] is not None else 'n/a'}h | {l['status']} |")
|
|
lines.append("")
|
|
lines.append("## Findings")
|
|
lines.append("")
|
|
stale_lanes = [l["id"] for l in lanes if l["status"] == "STALE"]
|
|
watch_lanes = [l["id"] for l in lanes if l["status"] == "WATCH"]
|
|
active_lanes = [l["id"] for l in lanes if l["status"] == "ACTIVE"]
|
|
lines.append(f"- All seven lanes are STALE (no lane produced a daily ledger within the last 168h): "
|
|
f"{', '.join(stale_lanes) if stale_lanes else 'none'}.")
|
|
lines.append(f"- No lane is STALE, so no lane crossed a hard 7-day outage threshold - but all seven are "
|
|
f"stalled on the same cadence: the last log for every lane is "
|
|
f"{lanes[0]['latest_mtime_utc']}.")
|
|
lines.append("")
|
|
lines.append("## Actionable items")
|
|
lines.append("")
|
|
lines.append("1. Pick the furthest-behind STALE lane (largest age_hours) and prompt its executor to "
|
|
"resume the 24h daily ledger cadence.")
|
|
lines.append("2. Recheck after the executor act: re-run this generator and confirm each lane swings to "
|
|
"WATCH or ACTIVE within 24h.")
|
|
lines.append("3. No lane produced a ledger on 2026-10-17, 10-18, or 10-19 as of "
|
|
f"{now.strftime('%Y-%m-%dT%H:%M:%SZ')} - treat this as a cadence halt, not a missing lane.")
|
|
lines.append("")
|
|
lines.append("## Files")
|
|
lines.append("")
|
|
lines.append(f"- /root/troupe-standup/wizard-work/sol/Sol-Lane-Triage-2026-10-24.json - machine-readable artifact.")
|
|
lines.append(f"- /root/troupe-standup/wizard-work/sol/Sol-Lane-Triage-2026-10-24.md - this report.")
|
|
lines.append("")
|
|
note_path = os.path.join(REPO_OUT, "Sol-Lane-Triage-2026-10-24.md")
|
|
with open(note_path, "w", encoding="utf-8") as fh:
|
|
fh.write("\n".join(lines) + "\n")
|
|
|
|
print(json.dumps(record, indent=2))
|
|
print("wrote", json_repo)
|
|
print("wrote", note_path)
|
|
print("counts:", counts)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|