Watch
1
0
Fork
You've already forked SouveraineOS
0

project-state: weekly evidence, carried state, and an agent's door

Five weeks of forge evidence per week rather than a rolling window, so a
finding survives under one key and "closed after three weeks" is computable.
Rules run deterministically; agent.py hands the packet to whoever writes the
prose and never calls a model itself.

Model choice fails closed — no default, no substitute, and the exact named
model must be served or the pass refuses to run.
This commit is contained in:
Fimeg 2026-08-18 19:53:19 -04:00
commit a8caf27c14
26 changed files with 56303 additions and 0 deletions

View file

@ -0,0 +1,353 @@
#!/usr/bin/env python3
"""The agent's door into project state. No inference happens here, ever.
The deterministic layer establishes what is true; the agent reading this writes
the prose. Nothing in this file selects, calls, or configures a model the
agent invoking it is the model, and it already has the identity, the context and
the tools.
agent.py status what is collected, what is missing
agent.py packet --week 2026-W33 everything needed to write the draft
agent.py explain f-0069 one finding and every row it rests on
agent.py draft --week 2026-W33 --file draft.md --author Annie
agent.py verdict f-0069 promote --note "worth a task"
`packet` is the contract. It carries the week's state, what turned, what has
stood longest, and an `investigate` list naming where the deterministic layer
knows it is thin go read those repositories rather than trusting the packet.
See docs/tasks/79-project-state.md.
"""
import argparse
import datetime as dt
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import common as c
import collect
import synthesize as s
STORE = s.STORE
CONTRACT = {
"you_are": "the agent that writes Souveraine's weekly project-state draft",
"the_split": (
"Everything under `counted`, `movement`, `standing`, `carried` and `closed` was "
"computed from the forge by deterministic code and is authoritative. Your draft is "
"downstream of it and is disposable — it can be regenerated from this packet and "
"the stored state at any time."
),
"produce": (
"A markdown draft answering: what became true about Souveraine this week, what "
"stopped being true, what advanced, what regressed, what remains unresolved, what "
"decisions are waiting, and which milestones were crossed or are approaching."
),
"rules": [
"Cite finding ids (f-0069) and evidence ids (c:, ci:, d:, i:, p:, r:) for every claim.",
"Do not invent a shipment, release, decision, or outcome that is not in the evidence.",
"'Nothing moved here this week' is a legal sentence.",
"Where the packet's `investigate` list says the evidence is thin, go read the "
"repository before asserting; say so inline if you did not.",
"The report is internal. It publishes nowhere until a human promotes it.",
],
"when_the_packet_is_not_enough": (
"Read the repositories directly. `evidence.file` holds every row including full "
"commit bodies and changed paths; the working trees named in `repos` are checked "
"out locally. `agent.py explain <id>` prints any finding with its rows in full."
),
"return_with": "agent.py draft --week <week> --file <path> --author <you> [--model <id>]",
"no_inference_here": (
"This tool never calls a model. Model choice belongs to whoever invokes it and is "
"recorded, not decided, by --model."
),
}
EVIDENCE_KINDS = {
"c:": "commit — repo, sha, subject, body, changed paths, dimension and the basis that chose it",
"ci:": "Gitea Actions run — workflow, branch, head sha, conclusion; joins to a commit id",
"d:": "declared change — a task-index row or a SAF/docs file that moved",
"i:": "issue", "p:": "pull request", "r:": "release", "w:": "the week's rollup",
}
def load_state():
return s.load(os.path.join(STORE, "state.json"), {"findings": []})
def load_evidence(week):
return s.load(os.path.join(STORE, "evidence", f"{week}.json"))
def status():
state = load_state()
held = collect.held()
return {
"weeks_held": held,
"latest_synthesized": state.get("latest_week"),
"gaps": collect.gaps(),
"pending": s.pending(state),
"open_findings": sum(1 for f in state.get("findings", []) if f["status"] == "open"),
"drafts": sorted(os.listdir(os.path.join(STORE, "drafts")))
if os.path.isdir(os.path.join(STORE, "drafts")) else [],
"next": next_action(state, held),
}
def next_action(state, held):
if collect.gaps():
return "collect.py --backfill — weeks are missing evidence"
if s.pending(state):
return "synthesize.py --catch-up — weeks are collected but not passed over"
if not held:
return "collect.py --backfill --since <week> — nothing collected yet"
return f"agent.py packet --week {state.get('latest_week')} — state is current; write the draft"
def thin_spots(ev, findings):
"""Where the deterministic layer knows it did not see enough.
This is the half of the packet that asks for work: each entry says what is
weak, and what reading it would take to settle.
"""
out = []
unplaced = [r for r in ev["commits"] if r["dimension"] == c.UNASSIGNED]
if unplaced:
out.append({
"why": f"{len(unplaced)} commit(s) matched no dimension keyword and were not guessed at",
"how": "read these commits in their repositories and place them yourself",
"rows": [{"id": r["id"], "repo": r["repo"], "subject": r["subject"],
"paths": r["paths"][:6]} for r in unplaced],
})
blind = [name for name, meta in ev["collector"]["repos"].items()
if not meta["checkout"] and meta["commits"]]
if blind:
out.append({
"why": f"no local checkout for {', '.join(blind)}, so changed paths were unavailable "
f"and those commits were placed by subject or repository alone",
"how": "clone or fetch those repos if a dimension call there matters this week",
"rows": [],
})
single = [f for f in findings if len(f["evidence"]) == 1
and f["provenance"]["rule"] not in ("task-standing", "pull-aging", "issue-aging")]
if single:
out.append({
"why": f"{len(single)} finding(s) rest on exactly one evidence row",
"how": "agent.py explain <id>, then read the surrounding work before leaning on it",
"rows": [{"id": f["id"], "title": f["title"], "evidence": f["evidence"]}
for f in single[:12]],
})
declared_only = [dim for dim, r in ev["rollup"]["by_dimension"].items()
if r.get("declared") and not r.get("commits")]
if declared_only:
out.append({
"why": f"{', '.join(declared_only)} changed in the documentation with no commits behind it",
"how": "a doc can lead the code or lag it — check which, before calling it advanced",
"rows": [{"id": r["id"], "file": r.get("file"), "title": r.get("title"),
"change": r["change"]}
for r in ev["declared"] if r["dimension"] in declared_only][:12],
})
notes = ev["collector"].get("notes") or []
if notes:
out.append({"why": "the collector reported problems", "how": "treat these areas as incomplete",
"rows": [{"note": n} for n in notes]})
return out
def packet(week, brief=False):
label, monday, sunday = c.parse_week(week)
ev = load_evidence(label)
if not ev:
return None, f"no evidence for {label} — collect.py --week {label}"
state = s.load(os.path.join(STORE, "history", f"{label}.json")) or load_state()
if label not in state.get("weeks", []):
return None, f"{label} is collected but not synthesized — synthesize.py --catch-up"
findings = state["findings"]
this_week = [f for f in findings if f["last_seen"] == label and f["status"] == "open"]
quiet = [f for f in this_week if f["provenance"]["rule"] == "task-standing"
and f["movement"] != "blocked"]
moved = [f for f in this_week if f not in quiet]
carried = [f for f in findings if f["status"] == "open" and f["last_seen"] != label]
closed = [f for f in findings if f.get("resolved_in") == label]
turned = [f for f in moved if f.get("was") and f["was"]["movement"] != f["movement"]]
def slim(f):
row = {"id": f["id"], "key": f["key"], "dimension": f["dimension"],
"movement": f["movement"], "title": f["title"], "statement": f["statement"],
"evidence": f["evidence"], "rule": f["provenance"]["rule"],
"first_seen": f["first_seen"], "weeks_open": f.get("weeks_open")}
for extra in ("was", "weeks_in_section", "section_since", "task_state", "resolved_in"):
if f.get(extra):
row[extra] = f[extra]
if (f.get("human") or {}).get("verdict"):
row["human"] = f["human"]
return row
commits = [{"id": r["id"], "repo": r["repo"], "at": r["at"], "dimension": r["dimension"],
"basis": r["basis"], "subject": r["subject"],
**({} if brief else {"detail": r["detail"], "paths": r["paths"][:12]})}
for r in ev["commits"]]
# Only a finding that carries can be the oldest one; a CI week or a
# dimension's activity describes one week and lapses with it.
oldest = max((f for f in findings if f["status"] == "open"
and f["provenance"]["rule"] in s.CARRIES),
key=lambda f: (f.get("weeks_open", 1), f["id"]), default=None)
return {
"contract": CONTRACT,
"week": label,
"range": ev["range"],
"completeness": {
"weeks_held": collect.held(),
"gaps": collect.gaps(),
"pending": s.pending(load_state()),
"collected_at": ev["collected_at"],
"collector": {k: ev["collector"][k] for k in ("tool", "version", "forge", "api_calls",
"redacted")},
},
"counted": ev["rollup"],
"movement": [slim(f) for f in moved],
"standing": [slim(f) for f in sorted(quiet, key=lambda f: -f.get("weeks_in_section", 1))],
"carried": [slim(f) for f in sorted(carried, key=lambda f: -f.get("weeks_open", 1))],
"closed": [slim(f) for f in closed],
"continuity": {
"weeks_available": state.get("weeks", []),
"turned_this_week": [{"id": f["id"], "title": f["title"],
"from": f["was"]["movement"], "to": f["movement"],
"since": f["was"]["week"]} for f in turned],
"oldest_open": slim(oldest) if oldest else None,
},
"investigate": thin_spots(ev, this_week),
# --brief keeps every row the counts cannot replace and drops the rest:
# green CI runs are already a tally, and the task index is `standing`.
"evidence": {
"file": f"project-state/evidence/{label}.json",
"explain": "tools/project-state/agent.py explain <finding-id>",
"kinds": EVIDENCE_KINDS,
"commits": commits,
"declared": ev["declared"],
"ci": [r for r in ev["ci"] if r["conclusion"] != "success"] if brief else ev["ci"],
"ci_note": "green runs omitted; counts are under `counted.ci`" if brief else None,
"issues": ev["issues"],
"pulls": ev["pulls"],
"releases": ev["releases"],
**({} if brief else {"task_index": ev.get("index", {})}),
},
"repos": {name: {"commits": meta["commits"],
"checkout": os.path.join(c.PROJECTS, name) if meta["checkout"] else None}
for name, meta in ev["collector"]["repos"].items() if meta["commits"]},
}, None
def record_draft(week, path, author, model, note):
label, _, _ = c.parse_week(week)
try:
body = open(path).read()
except OSError as e:
note(f"cannot read {path}: {e}")
return 2
state = s.load(os.path.join(STORE, "history", f"{label}.json")) or load_state()
folder = os.path.join(STORE, "drafts")
os.makedirs(folder, exist_ok=True)
slug = "".join(ch if ch.isalnum() or ch in "-_" else "-" for ch in author).strip("-").lower()
stem = os.path.join(folder, f"{label}--{slug}")
open(stem + ".md", "w").write(body)
# The draft names the state it was written against, so a later reader can
# tell whether it still describes the findings that exist.
open(stem + ".json", "w").write(json.dumps({
"week": label,
"author": author,
"model": model,
"recorded_at": c.iso_z(dt.datetime.now(dt.timezone.utc)),
"state_updated": state.get("updated"),
"findings_open": sum(1 for f in state.get("findings", []) if f["status"] == "open"),
"evidence": f"project-state/evidence/{label}.json",
"published": False,
"source": os.path.abspath(path),
}, indent=2, ensure_ascii=False) + "\n")
note(f"{label}: draft by {author}"
+ (f" ({model})" if model else "")
+ f" -> project-state/drafts/{label}--{slug}.md; nothing published")
return 0
def main():
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
sub = ap.add_subparsers(dest="command", required=True)
sub.add_parser("status", help="what is collected, synthesized, and missing")
p = sub.add_parser("packet", help="everything needed to write the week's draft")
p.add_argument("--week", default="last")
p.add_argument("--brief", action="store_true", help="drop commit bodies and paths")
p.add_argument("--out", help="write here instead of stdout")
e = sub.add_parser("explain", help="one finding and every evidence row it cites")
e.add_argument("finding")
e.add_argument("--json", action="store_true")
d = sub.add_parser("draft", help="record a draft written by an agent or a person")
d.add_argument("--week", default="last")
d.add_argument("--file", required=True)
d.add_argument("--author", required=True)
d.add_argument("--model", help="the model that wrote it, recorded not chosen")
v = sub.add_parser("verdict", help="a person's word on a finding")
v.add_argument("finding")
v.add_argument("verdict", choices=s.VERDICTS)
v.add_argument("--note")
v.add_argument("--by")
args = ap.parse_args()
def note(message):
print(f"[project-state] {message}", file=sys.stderr)
if args.command == "status":
print(json.dumps(status(), indent=2, ensure_ascii=False))
return 0
if args.command == "packet":
data, problem = packet(args.week, args.brief)
if problem:
note(problem)
return 2
text = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
if args.out:
open(args.out, "w").write(text)
note(f"{data['week']}: packet -> {args.out} ({len(text):,} chars)")
else:
sys.stdout.write(text)
return 0
if args.command == "explain":
if not args.json:
return s.explain(load_state(), args.finding)
state = load_state()
f = next((x for x in state["findings"]
if x["id"] == args.finding or x["key"] == args.finding), None)
if not f:
note(f"no finding {args.finding}")
return 1
week = f["provenance"].get("evidence_week", f["last_seen"])
ev = load_evidence(week)
index = {row["id"]: row for group in ("commits", "ci", "issues", "pulls",
"releases", "declared")
for row in ev.get(group, [])}
index[f"w:{week}"] = ev.get("rollup", {})
print(json.dumps({"finding": f, "week": week,
"evidence": {eid: index.get(eid, "MISSING") for eid in f["evidence"]}},
indent=2, ensure_ascii=False))
return 0
if args.command == "draft":
return record_draft(args.week, args.file, args.author, args.model, note)
if args.command == "verdict":
return s.write_verdict(f"{args.finding}={args.verdict}", note, args.note, args.by)
return 2
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,475 @@
#!/usr/bin/env python3
"""Weekly evidence for the project-state pass. Deterministic; no model runs here.
Reads the forge for one ISO week commits, Actions runs, issues, pulls,
releases and the local SouveraineOS clone for what the task index and the SAF
declared, then writes project-state/evidence/<week>.json. Every row carries a
stable id; a finding cites those ids and nothing else.
tools/project-state/collect.py --week last
tools/project-state/collect.py --week 2026-W33 --out -
Env: GITEA_URL, GITEA_TOKEN (falls back to ~/.git-credentials).
See docs/tasks/79-project-state.md.
"""
import argparse
import datetime as dt
import json
import os
import re
import sys
import urllib.error
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import common as c
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
VERSION = 1
def commits(forge, repo, start, end, note):
"""Commits in the window, every branch, deduped by SHA.
A branch whose tip predates the window cannot hold work inside it, so it is
never walked the same prefilter the ledger's collector uses.
"""
full = f"{c.OWNER}/{repo}"
edge = c.iso_z(start)
rows, seen = [], set()
try:
branches = forge.pages(f"repos/{full}/branches")
except urllib.error.HTTPError as e:
note(f"{repo}: branches unavailable ({e.code})")
return []
live = [b["name"] for b in branches
if ((b.get("commit") or {}).get("timestamp") or "") >= edge]
for branch in live:
path = (f"repos/{full}/commits?sha={branch}&since={edge}&until={c.iso_z(end)}"
f"&stat=false&verification=false&files=false")
try:
page = forge.pages(path)
except urllib.error.HTTPError as e:
note(f"{repo}@{branch}: commits unavailable ({e.code})")
continue
for row in page:
sha = row.get("sha", "")
if sha in seen or len((row.get("parents") or [])) > 1:
continue
seen.add(sha)
commit = row.get("commit") or {}
author = commit.get("author") or {}
subject, _, body = (commit.get("message") or "").partition("\n")
paths = c.commit_paths(repo, sha)
dimension, basis = c.classify(repo, subject.strip(), paths)
rows.append({
"id": f"c:{repo}:{sha[:8]}",
"repo": repo,
"sha": sha,
"branch": branch,
"at": author.get("date", ""),
"author": author.get("email", ""),
"subject": subject.strip(),
"detail": re.split(r"\n\s*\n", body.strip(), maxsplit=1)[0].strip(),
"dimension": dimension,
"basis": basis,
"paths": paths[:40],
"url": row.get("html_url", ""),
})
return rows
def runs(forge, repo, start, end, note):
"""Actions runs started inside the window.
This endpoint ignores limit and page and hands back the whole history, so it
is fetched once and sliced here paging it would loop on the same rows.
"""
try:
payload = forge.get(f"repos/{c.OWNER}/{repo}/actions/runs")
except urllib.error.HTTPError as e:
if e.code != 404:
note(f"{repo}: actions unavailable ({e.code})")
return []
rows = []
for run in payload.get("workflow_runs", []) if isinstance(payload, dict) else payload:
started = c.parse_iso(run.get("started_at") or run.get("run_started_at"))
if not started or not (start <= started < end):
continue
rows.append({
"id": f"ci:{repo}:{run.get('id')}",
"repo": repo,
"run": run.get("id"),
"workflow": os.path.basename(run.get("path") or ""),
"branch": run.get("head_branch", ""),
"sha": run.get("head_sha", ""),
"commit": f"c:{repo}:{(run.get('head_sha') or '')[:8]}",
"conclusion": run.get("conclusion") or run.get("status") or "",
"started": run.get("started_at", ""),
"completed": run.get("completed_at", ""),
"title": run.get("display_title", ""),
})
return rows
def issues(forge, repo, start, end, note):
"""Every issue, open or closed — aging needs the ones that predate the week."""
try:
rows = forge.pages(f"repos/{c.OWNER}/{repo}/issues?state=all&type=issues")
except urllib.error.HTTPError as e:
if e.code != 404:
note(f"{repo}: issues unavailable ({e.code})")
return []
out = []
for i in rows:
created, closed = c.parse_iso(i.get("created_at")), c.parse_iso(i.get("closed_at"))
out.append({
"id": f"i:{repo}:{i.get('number')}",
"repo": repo,
"number": i.get("number"),
"title": i.get("title", ""),
"state": i.get("state", ""),
"labels": [l.get("name") for l in (i.get("labels") or [])],
"created": i.get("created_at", ""),
"closed": i.get("closed_at"),
"age_days": (end - created).days if created else None,
"opened_this_week": bool(created and start <= created < end),
"closed_this_week": bool(closed and start <= closed < end),
"url": i.get("html_url", ""),
})
return out
def pulls(forge, repo, start, end, note):
try:
rows = forge.pages(f"repos/{c.OWNER}/{repo}/pulls?state=all")
except urllib.error.HTTPError as e:
if e.code != 404:
note(f"{repo}: pulls unavailable ({e.code})")
return []
out = []
for p in rows:
created, merged = c.parse_iso(p.get("created_at")), c.parse_iso(p.get("merged_at"))
out.append({
"id": f"p:{repo}:{p.get('number')}",
"repo": repo,
"number": p.get("number"),
"title": p.get("title", ""),
"state": p.get("state", ""),
"created": p.get("created_at", ""),
"merged": p.get("merged_at"),
"age_days": (end - created).days if created else None,
"opened_this_week": bool(created and start <= created < end),
"merged_this_week": bool(merged and start <= merged < end),
"url": p.get("html_url", ""),
})
return out
def releases(forge, repo, start, end, note):
try:
rows = forge.pages(f"repos/{c.OWNER}/{repo}/releases")
except urllib.error.HTTPError as e:
if e.code != 404:
note(f"{repo}: releases unavailable ({e.code})")
return []
out = []
for r in rows:
published = c.parse_iso(r.get("published_at") or r.get("created_at"))
out.append({
"id": f"r:{repo}:{r.get('tag_name')}",
"repo": repo,
"tag": r.get("tag_name", ""),
"name": r.get("name", ""),
"published": r.get("published_at") or r.get("created_at", ""),
"assets": len(r.get("assets") or []),
"draft": bool(r.get("draft")),
"cut_this_week": bool(published and start <= published < end),
"age_days": (end - published).days if published else None,
"url": r.get("html_url", ""),
})
return out
# ---- declared state --------------------------------------------------------
TASK_ROW = re.compile(r"^\|\s*(\d+)\s*\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|\s*(.*?)\s*\|\s*$")
def parse_index(text):
"""The task index as {number: {section, title, left}} — the hand-written state."""
section, out = "", {}
for line in text.splitlines():
if line.startswith("## "):
section = line[3:].split("")[0].strip()
continue
m = TASK_ROW.match(line)
if m:
# The index writes both `| 08 |` and `| 8 |` for task 8; unnormalised,
# one task carried two keys and closed itself while still standing.
out[str(int(m.group(1)))] = {
"section": section,
"title": m.group(2).strip(),
"file": m.group(3).strip(),
"left": m.group(4).strip(),
}
return out
def blob_at(path, sha, filename):
return c.git(path, "show", f"{sha}:{filename}")
def boundary(path, when, branch="main"):
"""Newest commit at or before an instant — the index as it stood then."""
out = c.git(path, "rev-list", "-1", f"--before={c.iso_z(when)}", branch)
return out.strip()
def declared(start, end, note, snapshot):
"""What the task index and the SAF said before and after the week."""
path = c.checkout("SouveraineOS")
if not path:
note("SouveraineOS is not checked out — declared state skipped")
return []
before, after = boundary(path, start), boundary(path, end)
if not before or not after:
note("no SouveraineOS commit on one side of the week — declared state skipped")
return []
index_file = "docs/tasks/README.md"
was = parse_index(blob_at(path, before, index_file))
now = parse_index(blob_at(path, after, index_file))
snapshot.update(now)
rows = []
for number in sorted(set(was) | set(now), key=int):
old, new = was.get(number), now.get(number)
if old and new and old == new:
continue
if not old:
change, detail = "listed", new["section"]
elif not new:
change, detail = "delisted", old["section"]
elif old["section"] != new["section"]:
change, detail = "moved", f"{old['section']}{new['section']}"
else:
change, detail = "restated", "what's left rewritten"
rows.append({
"id": f"d:task-{number}",
"kind": "task-index",
"task": number,
"title": (new or old)["title"],
"file": (new or old)["file"],
"change": change,
"detail": detail,
"section": (new or old)["section"],
"was": old["left"] if old else None,
"now": new["left"] if new else None,
"dimension": c.classify_text(
f"{(new or old)['title']} {(new or old)['file']} {(new or old)['left']}")[0],
})
status = {"A": "added", "M": "revised", "D": "deleted", "R": "moved", "C": "copied"}
changed = c.git(path, "diff", "--name-status", before, after, "--", "saf/", "docs/")
for line in changed.splitlines():
fields = line.split("\t")
if len(fields) < 2:
continue
filename = fields[-1]
if filename.startswith("docs/tasks/"):
continue
log = c.git(path, "log", "--format=%h|%aI|%s", f"{before}..{after}", "--", filename)
touches = [entry.split("|", 2) for entry in log.splitlines() if entry]
rows.append({
"id": f"d:{filename}",
"kind": "document",
"file": filename,
"change": status.get(fields[0][0], fields[0]),
"detail": f"{len(touches)} commit(s)",
"commits": [t[0] for t in touches],
"subjects": [t[2] for t in touches],
"dimension": c.classify_text(f"{filename} {' '.join(t[2] for t in touches)}")[0],
})
if not rows:
note("declared state did not move this week")
return rows
# ---- rollup ----------------------------------------------------------------
def rollup(evidence, start, end):
by_dim = {}
for row in evidence["commits"]:
d = by_dim.setdefault(row["dimension"], {"commits": 0, "repos": [], "authors": []})
d["commits"] += 1
for key, value in (("repos", row["repo"]), ("authors", row["author"])):
if value and value not in d[key]:
d[key].append(value)
for row in evidence["declared"]:
d = by_dim.setdefault(row["dimension"], {"commits": 0, "repos": [], "authors": []})
d["declared"] = d.get("declared", 0) + 1
ci = {}
for row in evidence["ci"]:
r = ci.setdefault(row["repo"], {"runs": 0, "success": 0, "failure": 0, "other": 0})
r["runs"] += 1
key = row["conclusion"] if row["conclusion"] in ("success", "failure") else "other"
r[key] += 1
for repo, r in ci.items():
newest = max((row for row in evidence["ci"] if row["repo"] == repo),
key=lambda row: row["started"], default=None)
r["newest"] = {"id": newest["id"], "conclusion": newest["conclusion"]} if newest else None
open_issues = [i for i in evidence["issues"] if i["state"] == "open"]
open_pulls = [p for p in evidence["pulls"] if p["state"] == "open"]
basis = {}
for row in evidence["commits"]:
basis[row["basis"]] = basis.get(row["basis"], 0) + 1
return {
"by_dimension": by_dim,
"ci": ci,
"issues": {
"open": len(open_issues),
"opened_this_week": sum(i["opened_this_week"] for i in evidence["issues"]),
"closed_this_week": sum(i["closed_this_week"] for i in evidence["issues"]),
"ever_closed": sum(1 for i in evidence["issues"] if i["closed"]),
"oldest_open": max(open_issues, key=lambda i: i["age_days"] or 0)["id"] if open_issues else None,
"oldest_open_days": max((i["age_days"] or 0) for i in open_issues) if open_issues else 0,
},
"pulls": {
"open": len(open_pulls),
"merged_this_week": sum(p["merged_this_week"] for p in evidence["pulls"]),
"oldest_open_days": max((p["age_days"] or 0) for p in open_pulls) if open_pulls else 0,
},
"releases": {
"cut_this_week": [r["id"] for r in evidence["releases"] if r["cut_this_week"]],
"newest_age_days": min((r["age_days"] or 0) for r in evidence["releases"]) if evidence["releases"] else None,
},
"dimension_basis": basis,
}
def held():
"""Weeks that already have evidence on disk."""
path = os.path.join(ROOT, "project-state", "evidence")
return sorted(name[:-5] for name in os.listdir(path)) if os.path.isdir(path) else []
def gaps(since=None):
"""Weeks with no evidence, from the first week held (or --since) to now.
Nothing runs this on a schedule. The tool has to be able to say which weeks
are unfilled, or a missed Monday quietly becomes a hole in the record.
"""
have = held()
current, _, _ = c.parse_week("current")
first = since or (have[0] if have else current)
return [week for week in c.week_span(first, current) if week not in have]
def collect_week(week, args, forge=None):
label, monday, sunday = c.parse_week(week)
start, end = c.week_bounds_utc(monday, sunday)
repos = [r.strip() for r in args.repos.split(",")] if args.repos else c.REPOS
forge = forge or c.Forge()
notes = []
def note(message):
notes.append(message)
print(f"[project-state] {message}", file=sys.stderr)
evidence = {"commits": [], "ci": [], "issues": [], "pulls": [], "releases": []}
repo_summary = {}
before = forge.calls
for repo in repos:
rows = commits(forge, repo, start, end, note)
evidence["commits"] += rows
evidence["ci"] += runs(forge, repo, start, end, note)
evidence["issues"] += issues(forge, repo, start, end, note)
evidence["pulls"] += pulls(forge, repo, start, end, note)
evidence["releases"] += releases(forge, repo, start, end, note)
repo_summary[repo] = {"commits": len(rows), "checkout": bool(c.checkout(repo))}
index = {}
evidence["declared"] = declared(start, end, note, index)
evidence["index"] = index
for key in ("commits", "ci"):
evidence[key].sort(key=lambda row: row.get("at") or row.get("started") or "")
if args.redact:
for row in evidence["commits"]:
row["subject"] = c.redact(row["subject"])
row["detail"] = c.redact(row["detail"])
out = {
"week": label,
"range": {
"start": monday.isoformat(), "end": sunday.isoformat(), "tz": str(c.LOCAL),
"start_utc": c.iso_z(start), "end_utc": c.iso_z(end),
},
"collected_at": c.iso_z(dt.datetime.now(dt.timezone.utc)),
"collector": {
"tool": "tools/project-state/collect.py",
"version": VERSION,
"forge": f"gitea {forge.version()}",
"api_calls": forge.calls - before,
"redacted": bool(args.redact),
"repos": repo_summary,
"notes": notes,
},
**evidence,
}
out["rollup"] = rollup(out, start, end)
return out
def write(out, path):
os.makedirs(os.path.dirname(path), exist_ok=True)
open(path, "w").write(json.dumps(out, indent=2, ensure_ascii=False) + "\n")
print(f"[project-state] {out['week']}: {len(out['commits'])} commits, {len(out['ci'])} runs, "
f"{len(out['declared'])} declared changes, {out['collector']['api_calls']} api calls "
f"-> {path}", file=sys.stderr)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--week", default="last", help="2026-W33 | current | last (default)")
ap.add_argument("--repos", help="comma-separated override of the repo list")
ap.add_argument("--out", help="output path, '-' for stdout")
ap.add_argument("--redact", action="store_true",
help="mask hosts and home paths — for evidence leaving the forge")
ap.add_argument("--gaps", action="store_true", help="name the weeks holding no evidence")
ap.add_argument("--backfill", action="store_true", help="collect every week --gaps names")
ap.add_argument("--since", help="earliest week --gaps and --backfill consider")
args = ap.parse_args()
if args.gaps or args.backfill:
missing = gaps(args.since)
if not missing:
print(f"[project-state] no gaps; {len(held())} week(s) held", file=sys.stderr)
return 0
if args.gaps:
print("\n".join(missing))
return 0
forge = c.Forge()
if not forge.token:
print("project-state: no forge token (set GITEA_TOKEN)", file=sys.stderr)
return 2
for week in missing:
write(collect_week(week, args, forge),
os.path.join(ROOT, "project-state", "evidence", f"{week}.json"))
return 0
forge = c.Forge()
if not forge.token:
print("project-state: no forge token (set GITEA_TOKEN)", file=sys.stderr)
return 2
out = collect_week(args.week, args, forge)
if args.out == "-":
sys.stdout.write(json.dumps(out, indent=2, ensure_ascii=False) + "\n")
return 0
write(out, args.out or os.path.join(ROOT, "project-state", "evidence", f"{out['week']}.json"))
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,276 @@
"""Shared pieces for the project-state pass: forge client, weeks, dimensions.
See docs/tasks/79-project-state.md for what this is for and why it is shaped
this way.
"""
import datetime as dt
import json
import os
import re
import subprocess
import urllib.parse
import urllib.request
from zoneinfo import ZoneInfo
LOCAL = ZoneInfo("America/Toronto")
OWNER = "Fimeg"
PROJECTS = os.path.expanduser("~/Projects")
# Listed, not discovered. A repo joining Souveraine is a deliberate addition
# here, and one that goes quiet should be visible as a quiet row rather than
# silently stop being read. Same reasoning as tools/ci-status.sh.
REPOS = [
"souveraine", "SouveraineOS", "Pixel3Arch", "souveraine-viewtop",
"souveraine-updater", "souveraine-lens", "souveraine-speech",
"souveraine-player", "souveraine-usb", "culver", "linux-blueline",
"hexagonrpc", "smithay", "pocketboot", "libcmatrix", "upower",
]
# The SAF sections, plus the two the pipeline owns and SAF has no page for.
DIMENSIONS = [
"memory", "runtime", "reflection", "identity", "surface",
"sensorium", "release", "reliability", "federation", "adoption",
]
UNASSIGNED = "unassigned"
# Longer keywords score higher, so "device state" beats a bare "state" and a
# path full of a subsystem's name beats one incidental word in a subject.
KEYWORDS = {
"memory": ["memory", "memfs", "compaction", "archivist", "recall", "vault", "memoir"],
"runtime": ["server", "handler", "runtime", "substrate", "provider", "tool call",
"stream", "sse", "prompt", "context", "turn", "inference", "token"],
"reflection": ["reflection", "subconscious", "cadence", "n+1", "aster", "introspect"],
"identity": ["identity", "authority", "sessiond", "principal", "secrets", "machined",
"trust", "login", "credential", "permission", "polkit", "device state",
"device_state", "admission"],
"surface": ["shell", "panel", "tui", "surface", "qml", "dock", "dial", "osk",
"keyboard", "island", "overview", "compositor", "viewtop", "window",
"gesture", "lockscreen", "card", "render"],
"sensorium": ["sensor", "grip", "battery", "charge", "audio", "microphone", "camera",
"haptic", "bluetooth", "airpods", "librepods", "modem", "bearer",
"brightness", "fingerprint", "touch", "squeeze"],
"release": ["release", "package", "pkgbuild", "pacman", "repo-add", "distribution",
"artifact", "signing", "manifest"],
"reliability": ["crash", "stall", "deadlock", "leak", "regression", "revert",
"hotfix", "clippy", "flake", "panic", "timeout", "retry", "workflow"],
"federation": ["federation", "enrol", "enroll", "peer", "mirror", "distributed",
"multi-device", "household", "sync"],
"adoption": ["license", "licence", "install", "provision", "onboard",
"quickstart", "getting started", "contributor"],
}
# Two path shapes that no keyword scan should have to guess at.
PATH_RULES = [
("reliability", r"(^|/)\.gitea/"),
("release", r"(^|/)(packaging|pkgs?)/|PKGBUILD"),
]
# Only for a commit that neither its paths nor its subject place. A repo whose
# whole purpose is one dimension answers for it.
REPO_DEFAULT = {
"souveraine": "runtime",
"Pixel3Arch": "sensorium",
"linux-blueline": "sensorium",
"souveraine-viewtop": "surface",
"souveraine-lens": "adoption",
"souveraine-updater": "release",
"souveraine-player": "surface",
"souveraine-speech": "sensorium",
"souveraine-usb": "federation",
"culver": "surface",
"hexagonrpc": "sensorium",
"smithay": "surface",
"pocketboot": "release",
"libcmatrix": "surface",
"upower": "sensorium",
}
# Commit subjects are written for the man who owns the machines. Evidence stays
# raw because it never leaves the forge; this is here for whatever gets promoted
# to a public surface later. Same patterns as the ledger's collector.
REDACTIONS = [
(re.compile(r'\b(?:10|127)(?:\.\d{1,3}){2,3}(?::\d+)?\b'), "[host]"),
(re.compile(r'\b192\.168(?:\.\d{1,3}){1,2}(?::\d+)?\b'), "[host]"),
(re.compile(r'\b172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){1,2}(?::\d+)?\b'), "[host]"),
(re.compile(r'\b(?:WIUF[-\w]*|wiufph|archdev)\b', re.I), "[host]"),
(re.compile(r'/home/casey|/root\b'), "~"),
]
def redact(text):
for pattern, mask in REDACTIONS:
text = pattern.sub(mask, text)
return text
def score(text):
"""Dimension scores for any text; ties go to the earlier dimension."""
low = (text or "").lower()
hits = {d: sum(len(k) for k in words if k in low) for d, words in KEYWORDS.items()}
hits = {d: s for d, s in hits.items() if s}
if not hits:
return None, {}
best = max(hits, key=lambda d: (hits[d], -DIMENSIONS.index(d)))
return best, hits
def classify_text(text):
"""For a task row or a document: what is this about, or nothing."""
best, _ = score(text)
return (best, "text") if best else (UNASSIGNED, "none")
def classify(repo, subject, paths=None):
"""One dimension for a commit, and the basis that chose it.
Paths and subject are both read and weighted alike. Paths alone put
`compaction: target the active conversation` under the server module it
edits; the subject is the author saying which subsystem he thinks he moved.
"""
joined = " ".join(paths or [])
_, from_paths = score(joined)
_, from_subject = score(subject)
total = {d: from_paths.get(d, 0) + from_subject.get(d, 0)
for d in set(from_paths) | set(from_subject)}
if total:
best = max(total, key=lambda d: (total[d], -DIMENSIONS.index(d)))
return best, "paths" if from_paths.get(best, 0) else "subject"
for dim, pattern in PATH_RULES:
if joined and re.search(pattern, joined, re.I):
return dim, "paths"
if repo in REPO_DEFAULT:
return REPO_DEFAULT[repo], "repo"
return UNASSIGNED, "none"
# ---- weeks -----------------------------------------------------------------
def parse_week(spec, today=None):
"""'2026-W33', 'current', 'last' -> (label, monday, sunday) as local dates."""
today = today or dt.datetime.now(LOCAL).date()
if spec in ("current", "last", None):
monday = today - dt.timedelta(days=today.isoweekday() - 1)
if spec == "last":
monday -= dt.timedelta(days=7)
else:
m = re.fullmatch(r"(\d{4})-?W(\d{1,2})", spec.upper())
if not m:
raise ValueError(f"week must look like 2026-W33, got {spec!r}")
monday = dt.date.fromisocalendar(int(m.group(1)), int(m.group(2)), 1)
year, week, _ = monday.isocalendar()
return f"{year}-W{week:02d}", monday, monday + dt.timedelta(days=6)
def week_bounds_utc(monday, sunday):
"""Local week -> UTC instants, so a Sunday-evening commit stays in its week."""
start = dt.datetime.combine(monday, dt.time.min, LOCAL).astimezone(dt.timezone.utc)
end = dt.datetime.combine(sunday + dt.timedelta(days=1), dt.time.min, LOCAL).astimezone(dt.timezone.utc)
return start, end
def week_monday(label):
year, week = label.split("-W")
return dt.date.fromisocalendar(int(year), int(week), 1)
def label_for(monday):
y, w, _ = monday.isocalendar()
return f"{y}-W{w:02d}"
def previous_week(label):
return label_for(week_monday(label) - dt.timedelta(days=7))
def week_span(first, last):
"""Every ISO week label from first to last inclusive, in order."""
monday, end, out = week_monday(first), week_monday(last), []
while monday <= end:
out.append(label_for(monday))
monday += dt.timedelta(days=7)
return out
def iso_z(when):
return when.astimezone(dt.timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
def parse_iso(value):
if not value:
return None
try:
return dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return None
# ---- forge -----------------------------------------------------------------
class Forge:
# Gitea clamps a page to MAX_RESPONSE_ITEMS (50) and ignores a larger limit
# without saying so. The ledger lost 24 of 74 repos to this for months.
PAGE = 50
def __init__(self, base=None, token=None):
self.base = (base or os.environ.get("GITEA_URL") or "http://10.10.20.120:4455").rstrip("/")
self.token = token or os.environ.get("GITEA_TOKEN") or self._stored_token()
self.calls = 0
def _stored_token(self):
path = os.path.expanduser("~/.git-credentials")
host = urllib.parse.urlsplit(self.base).hostname or ""
try:
for line in open(path):
parsed = urllib.parse.urlsplit(line.strip())
if parsed.hostname and parsed.hostname.split("%")[0] == host:
return parsed.password or ""
except OSError:
pass
return ""
def get(self, path):
req = urllib.request.Request(f"{self.base}/api/v1/{path}")
if self.token:
req.add_header("Authorization", f"token {self.token}")
self.calls += 1
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)
def pages(self, path, max_pages=40):
sep = "&" if "?" in path else "?"
out = []
for page in range(1, max_pages + 1):
chunk = self.get(f"{path}{sep}limit={self.PAGE}&page={page}")
if isinstance(chunk, dict):
chunk = chunk.get("workflow_runs") or chunk.get("data") or []
if not chunk:
return out
out += chunk
if len(chunk) < self.PAGE:
return out
raise RuntimeError(f"{path}: {max_pages} pages and still going")
def version(self):
return self.get("version").get("version", "?")
# ---- local checkouts -------------------------------------------------------
def checkout(repo):
path = os.path.join(PROJECTS, repo)
return path if os.path.isdir(os.path.join(path, ".git")) else None
def git(path, *args):
proc = subprocess.run(["git", "-C", path, *args], capture_output=True, text=True)
return proc.stdout if proc.returncode == 0 else ""
def commit_paths(repo, sha):
"""Changed paths from the local clone, empty when it doesn't have the SHA."""
path = checkout(repo)
if not path:
return []
out = git(path, "show", "--name-only", "--format=", "--no-renames", sha)
return [line for line in out.splitlines() if line.strip()]

View file

@ -0,0 +1,821 @@
#!/usr/bin/env python3
"""Turn a week of evidence plus last week's state into this week's state.
Deterministic rules run first and produce every finding that can be computed.
A model, when one is configured, only reads those findings and the rollup and
adds cross-cutting observations; it is never asked to discover a fact. Model
findings that cite an evidence id the week does not contain are dropped.
tools/project-state/synthesize.py --week 2026-W33
tools/project-state/synthesize.py --week 2026-W33 --interpret
tools/project-state/synthesize.py --explain f-0007
Env: ANALYST_URL, ANALYST_KEY, ANALYST_MODEL any OpenAI-compatible endpoint.
See docs/tasks/79-project-state.md.
"""
import argparse
import datetime as dt
import ipaddress
import re
import json
import os
import sys
import urllib.parse
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import common as c
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
STORE = os.path.join(ROOT, "project-state")
VERSION = 1
STALE_DAYS = 14
SAMPLE_PER_DIMENSION = 10
MODEL_MAX = 6
MOVEMENTS = ["milestone_reached", "regressed", "blocked", "decision_required",
"advanced", "unchanged"]
# Rules whose findings survive a week that says nothing about them. The rest
# describe one week and lapse with it.
CARRIES = {"task-standing", "task-listed", "task-moved", "task-restated",
"task-delisted", "pull-aging", "issue-aging", "issue-tracker-unused"}
CHARTER = """You read one week of evidence from the Souveraine project. Deterministic code has
already counted everything countable and its findings are included so you do not repeat
them. Your job is the three things code cannot do:
1. Work in one subsystem that changes what is true in another.
2. A contradiction between what the repository declares about itself and what its
commits, CI runs or pull requests show.
3. An assumption this week invalidated.
Rules, and a finding that breaks one is discarded unread:
- Write about the project, never about the findings list. "The deterministic finding
says X" is not a finding. Never count anything.
- Cite at least two evidence ids of different kinds a commit id (c:) with a declared
change (d:), a CI run (ci:) with a pull request (p:), and so on. One kind alone is a
restatement, not a connection.
- Never say that two commits share a dimension, or that one id appears in two samples.
That is an artefact of how the evidence was sampled, not a fact about the project.
- At most six findings. Fewer is better. An empty list is a legal answer.
- One or two plain sentences. No hype, no praise, no summary of the week.
- Do not invent releases, decisions, people, or outcomes.
Answer with JSON only: {"findings": [{"dimension": ..., "movement": ..., "title": ...,
"statement": ..., "evidence": ["id", ...]}]}
Movements: milestone_reached, regressed, blocked, decision_required, advanced, unchanged."""
def load(path, default=None):
try:
return json.load(open(path))
except (FileNotFoundError, json.JSONDecodeError):
return default if default is not None else {}
def finding(key, dimension, title, movement, statement, evidence, rule):
return {
"key": key,
"dimension": dimension,
"title": title,
"movement": movement,
"statement": statement,
"evidence": evidence,
"provenance": {"by": "deterministic", "rule": rule},
}
# ---- deterministic rules ---------------------------------------------------
def ci_findings(ev, prior_ev):
out = []
for repo, stat in sorted(ev["rollup"]["ci"].items()):
runs, failed = stat["runs"], stat["failure"]
was = (prior_ev.get("rollup", {}).get("ci", {}) or {}).get(repo)
rate = failed / runs if runs else 0
was_rate = (was["failure"] / was["runs"]) if was and was["runs"] else None
newest = stat.get("newest") or {}
cite = [newest["id"]] if newest.get("id") else []
cite += [row["id"] for row in ev["ci"]
if row["repo"] == repo and row["conclusion"] == "failure"
and row["id"] not in cite][:5]
tally = f"{failed} of {runs} runs failed"
if stat["other"]:
tally += f" and {stat['other']} never concluded"
if newest.get("conclusion") == "failure":
movement, title = "regressed", f"{repo} ends the week red"
statement = f"The last run of the week on {repo} failed; {tally}."
elif was_rate is not None and rate > was_rate + 0.15:
movement, title = "regressed", f"{repo} CI failure rate rose"
statement = f"{tally} on {repo}{rate:.0%}, up from {was_rate:.0%}."
elif failed == 0 and runs:
movement, title = "advanced", f"{repo} CI clean"
statement = f"{runs} runs on {repo}, none failed."
else:
movement, title = "unchanged", f"{repo} CI mixed"
statement = f"On {repo}, {tally}."
out.append(finding(f"ci/{repo}", "reliability", title, movement,
statement, cite, "ci-week"))
return out
def dimension_findings(ev, prior_ev):
out = []
now = ev["rollup"]["by_dimension"]
was = prior_ev.get("rollup", {}).get("by_dimension", {})
for dim in c.DIMENSIONS:
here = now.get(dim, {})
commits = here.get("commits", 0)
declared = [row for row in ev["declared"] if row["dimension"] == dim]
before = was.get(dim, {}).get("commits", 0) if was else None
cite = [row["id"] for row in ev["commits"] if row["dimension"] == dim][:8]
cite += [row["id"] for row in declared][:4]
if not commits and not declared:
if before:
statement = f"No commits, after {before} the week before."
elif before == 0:
statement = "A second quiet week."
else:
statement = "No commits, and no week before this one to compare against."
out.append(finding(f"activity/{dim}", dim, f"{dim} did not move",
"unchanged", statement, cite or [f"w:{ev['week']}"],
"dimension-activity"))
continue
parts = [f"{commits} commit(s)"]
if before is not None:
parts.append(f"{before} the week before")
statement = ", ".join(parts) + "."
if declared:
named = "; ".join(f"{row['change']} {row.get('title') or row['file']}"
for row in declared[:4])
statement += f" Declared: {named}."
out.append(finding(f"activity/{dim}", dim, f"{dim} moved",
"advanced" if declared else "unchanged", statement,
cite, "dimension-activity"))
return out
BACKWARD = {"Live": 3, "In progress": 2, "Open": 1, "Blocked or gated": 0}
# Casey writes the open question into the row itself. These are his phrasings,
# read off the index on 2026-08-18 — not a general vocabulary of hesitation.
DECISION = re.compile(
r"verdict owed|decisions? (?:are|first|before)|needs a decision|open decision"
r"|unanswered|undecided|scope and deliverable|answer it before|product call"
r"|Casey'?s(?: call| own)?\b|owed\b", re.I)
def movement_for(row):
text = row.get("now") or row.get("was") or ""
if row["section"] == "Blocked or gated":
return "blocked"
if DECISION.search(text):
return "decision_required"
return "unchanged" if row["section"] == "Open" else "advanced"
def task_findings(ev):
"""One finding per task standing in the index, moved by this week's diff.
Tracking only the diff made every restatement a permanent finding and the
task itself invisible; the index is the state, the diff is the movement.
"""
changes = {row["task"]: row for row in ev["declared"] if row["kind"] == "task-index"}
out = []
for number, row in sorted(ev.get("index", {}).items(), key=lambda kv: int(kv[0])):
change = changes.get(number)
dim = c.classify_text(f"{row['title']} {row['file']} {row['left']}")[0]
key, section, left = f"task/{number}", row["section"], row["left"][:240]
cite = [change["id"]] if change else [f"d:task-{number}"]
state = {"section": section, "left": row["left"], "file": row["file"]}
if not change:
f = finding(key, dim, f"task {number}: {row['title']}",
"blocked" if section == "Blocked or gated" else "unchanged",
f"Standing under “{section}”. {left}", cite, "task-standing")
elif change["change"] == "listed":
f = finding(key, dim, f"task {number} entered the index: {row['title']}",
movement_for(change), f"Filed under “{section}”. {left}",
cite, "task-listed")
elif change["change"] == "moved":
old, new = change["detail"].split("")
back = BACKWARD.get(new, 9) < BACKWARD.get(old, 9)
f = finding(key, dim, f"task {number} moved: {row['title']}",
"regressed" if back else movement_for(change),
f"{old}{new}. {left}", cite, "task-moved")
else:
f = finding(key, dim, f"task {number} restated: {row['title']}",
movement_for(change),
f"What's left was rewritten under “{section}”. {left}",
cite, "task-restated")
f["task_state"] = state
out.append(f)
for number, change in sorted(changes.items(), key=lambda kv: int(kv[0])):
if change["change"] != "delisted":
continue
out.append(finding(f"task/{number}", change["dimension"],
f"task {number} left the index: {change['title']}",
"milestone_reached",
f"Dropped from “{change['detail']}” — closed work moves to archive/.",
[change["id"]], "task-delisted"))
return out
def aging_findings(ev):
out = []
for issue in ev["issues"]:
if issue["state"] != "open" or (issue["age_days"] or 0) < STALE_DAYS:
continue
out.append(finding(f"issue/{issue['repo']}#{issue['number']}",
c.classify_text(issue["title"])[0],
f"{issue['repo']}#{issue['number']} open {issue['age_days']}d: {issue['title'][:70]}",
"unchanged",
f"Filed {issue['created'][:10]}, still open.",
[issue["id"]], "issue-aging"))
for pull in ev["pulls"]:
if pull["state"] != "open" or (pull["age_days"] or 0) < STALE_DAYS:
continue
out.append(finding(f"pull/{pull['repo']}#{pull['number']}",
c.classify_text(pull["title"])[0],
f"{pull['repo']}#{pull['number']} unmerged {pull['age_days']}d: {pull['title'][:70]}",
"decision_required",
f"Opened {pull['created'][:10]}, never merged or closed.",
[pull["id"]], "pull-aging"))
for rel in ev["releases"]:
if rel["cut_this_week"]:
out.append(finding(f"release/{rel['repo']}", "release",
f"{rel['repo']} released {rel['tag']}", "milestone_reached",
f"{rel['assets']} assets published {rel['published'][:10]}.",
[rel["id"]], "release-cut"))
issues = ev["rollup"]["issues"]
if issues["open"] and not issues["ever_closed"] and issues["oldest_open_days"] >= STALE_DAYS:
oldest = next((i for i in ev["issues"] if i["id"] == issues["oldest_open"]), None)
out.append(finding("issues/never-closed", "adoption",
"No issue has ever been closed", "unchanged",
f"{issues['open']} open, none closed since the tracker was first used"
+ (f"; the oldest is {oldest['repo']}#{oldest['number']}, "
f"{oldest['age_days']}d." if oldest else "."),
[i["id"] for i in ev["issues"] if i["state"] == "open"][:8],
"issue-tracker-unused"))
return out
def deterministic(ev, prior_ev):
return (task_findings(ev) + ci_findings(ev, prior_ev)
+ aging_findings(ev) + dimension_findings(ev, prior_ev))
# ---- model pass ------------------------------------------------------------
def locality(url):
host = urllib.parse.urlsplit(url).hostname or ""
try:
return "local" if ipaddress.ip_address(host).is_private else "remote"
except ValueError:
return "local" if host in ("localhost", "127.0.0.1") else "remote"
def parse_findings(text):
"""The findings, and whether they were salvaged from a truncated answer.
A reasoning model's answer arrives after its thinking, so a budget that runs
out cuts the array mid-object. Every complete object before the cut is still
a whole finding, and throwing them away because the closing bracket is
missing loses real work.
"""
start, end = text.find("{"), text.rfind("}")
if start >= 0 and end > start:
try:
return json.loads(text[start:end + 1]).get("findings", []), False
except json.JSONDecodeError:
pass
out, depth, begin = [], 0, None
for i, ch in enumerate(text[text.find("[") + 1:] if "[" in text else ""):
if ch == "{":
if depth == 0:
begin = i
depth += 1
elif ch == "}" and depth:
depth -= 1
if depth == 0:
fragment = text[text.find("[") + 1:][begin:i + 1]
try:
out.append(json.loads(fragment))
except json.JSONDecodeError:
pass
return (out, True) if out else (None, False)
class AnalystUnavailable(Exception):
"""The named model cannot be run. Never a reason to run a different one."""
def require_model(url, model, note):
"""Confirm the endpoint serves the exact model named, or refuse to proceed.
There is no default model and no fallback. A convenient default made the
choice feel fungible, and on 2026-08-18 that ended in three other models
being tried after one had been named.
"""
req = urllib.request.Request(f"{url.rstrip('/')}/v1/models")
key = os.environ.get("ANALYST_KEY")
if key:
req.add_header("Authorization", f"Bearer {key}")
try:
with urllib.request.urlopen(req, timeout=30) as r:
served = [m.get("id") for m in (json.load(r).get("data") or [])]
except Exception as e:
raise AnalystUnavailable(f"{url} did not answer /v1/models: {e}")
if model not in served:
near = [m for m in served if model.split("-")[0].lower() in (m or "").lower()][:4]
raise AnalystUnavailable(
f"{url} does not serve {model!r}"
+ (f"; it does serve {near} — name one of those explicitly if you want it"
if near else f"; {len(served)} models served, none of them that one"))
note(f"analyst model {model} confirmed served by {url}")
def interpret(ev, findings, prior_state, note):
url = os.environ.get("ANALYST_URL")
model = os.environ.get("ANALYST_MODEL")
if not url or not model:
raise AnalystUnavailable(
"--interpret needs both ANALYST_URL and ANALYST_MODEL; there is no default model")
require_model(url, model, note)
ids = {row["id"] for group in ("commits", "ci", "issues", "pulls", "releases", "declared")
for row in ev[group]}
# The whole week is 39k characters of commit subjects. A local 9B spends its
# entire budget reading that and never answers (measured 2026-08-18, 323s,
# nothing returned). Sampling per dimension is what makes the pass affordable
# — and reading every subject is a job for the deterministic half anyway.
sample = {}
for row in ev["commits"]:
seen = sample.setdefault(row["dimension"], [])
if len(seen) < SAMPLE_PER_DIMENSION:
seen.append({"id": row["id"], "subject": row["subject"]})
brief = {
"week": ev["week"],
"counts": {"by_dimension": ev["rollup"]["by_dimension"], "ci": ev["rollup"]["ci"],
"issues": ev["rollup"]["issues"], "pulls": ev["rollup"]["pulls"]},
"deterministic_findings": [
{"dimension": f["dimension"], "movement": f["movement"], "title": f["title"]}
for f in findings],
"commit_sample": sample,
"declared": [{"id": row["id"], "change": row["change"], "detail": row["detail"],
"title": row.get("title") or row.get("file")} for row in ev["declared"]],
"open_from_last_week": [
{"key": f["key"], "title": f["title"], "weeks_open": f.get("weeks_open")}
for f in prior_state.get("findings", []) if f.get("status") == "open"][:30],
}
payload = {
"model": model,
"messages": [{"role": "system", "content": CHARTER},
{"role": "user", "content": json.dumps(brief, ensure_ascii=False)}],
# Fable is a reasoning model: it fills `reasoning_content` first and
# leaves `content` empty if the budget runs out before it concludes.
# Measured 2026-08-18 — 3000 tokens went entirely to reasoning and
# returned nothing, and a 250-token cap (what the ledger's narrator
# uses) cannot ever answer.
"max_tokens": int(os.environ.get("ANALYST_MAX_TOKENS", 6000)),
"temperature": 0.3,
}
headers = {"Content-Type": "application/json"}
key = os.environ.get("ANALYST_KEY")
if key:
headers["Authorization"] = f"Bearer {key}"
started = dt.datetime.now(dt.timezone.utc)
try:
req = urllib.request.Request(f"{url.rstrip('/')}/v1/chat/completions",
data=json.dumps(payload).encode(), headers=headers)
with urllib.request.urlopen(req, timeout=900) as r:
body = json.load(r)
except Exception as e:
raise AnalystUnavailable(
f"{model} failed after {(dt.datetime.now(dt.timezone.utc) - started).seconds}s: {e}")
choice = (body.get("choices") or [{}])[0]
message = choice.get("message", {})
text = (message.get("content") or "").strip()
took = (dt.datetime.now(dt.timezone.utc) - started).seconds
meta = {"model": model, "endpoint": locality(url), "ok": True, "seconds": took,
"usage": body.get("usage"), "finish": choice.get("finish_reason"),
"reasoning_chars": len(message.get("reasoning_content") or "")}
# An empty findings list is a legal answer. No answer at all is a failure of
# the model that was named, and stays that.
if not text:
raise AnalystUnavailable(
f"{model} returned no answer in {took}s — {meta['reasoning_chars']} chars of "
f"reasoning, finish={meta['finish']}; the budget went entirely to thinking")
parsed, meta["salvaged"] = parse_findings(text)
if parsed is None:
raise AnalystUnavailable(
f"{model} produced {len(text)} chars that are not findings JSON "
f"(finish={meta['finish']})")
out, dropped, seen = [], 0, set()
for i, row in enumerate(parsed):
cited = [e for e in (row.get("evidence") or []) if e in ids]
kinds = {e.split(":", 1)[0] for e in cited}
signature = frozenset(cited)
# A connection spans two kinds of evidence. One kind, or a citation set
# already used, is the model restating itself.
if (len(cited) < 2 or len(kinds) < 2 or signature in seen
or row.get("movement") not in MOVEMENTS or len(out) >= MODEL_MAX):
dropped += 1
continue
seen.add(signature)
title = str(row.get("title") or "")[:120]
out.append({
"key": f"model/{ev['week']}/{i}",
"dimension": row.get("dimension") if row.get("dimension") in c.DIMENSIONS else c.UNASSIGNED,
"title": title,
"movement": row["movement"],
"statement": str(row.get("statement") or "")[:600],
"evidence": cited,
"provenance": {"by": "model", "rule": "cross-cutting", **meta},
})
meta["accepted"], meta["dropped"] = len(out), dropped
note(f"analyst: {len(out)} findings kept, {dropped} dropped for uncited or bad movement, {took}s")
return out, meta
# ---- carry forward ---------------------------------------------------------
def carry(prior, produced, week, analyst, closes):
"""This week's findings over last week's, with the resolution rule explicit.
A finding the week says nothing about is still open. Only positive evidence
closes one a task leaving the index, a pull merging. Absence of evidence
resolved four CI findings on the first two-week run, which is the difference
between a state document and a diff.
"""
by_key = {f["key"]: f for f in prior.get("findings", [])}
held = verdicts()
seq = max((int(f["id"][2:]) for f in prior.get("findings", []) if f.get("id", "").startswith("f-")),
default=0)
stamp = c.iso_z(dt.datetime.now(dt.timezone.utc))
weeks = prior.get("weeks", [])
if week not in weeks:
weeks = sorted(weeks + [week])
out, seen = [], set()
for new in produced:
old = by_key.get(new["key"])
seen.add(new["key"])
if old:
new["id"] = old["id"]
new["first_seen"] = old.get("first_seen", week)
new["was"] = {"movement": old.get("movement"), "week": old.get("last_seen")}
else:
seq += 1
new["id"] = f"f-{seq:04d}"
new["first_seen"] = week
new["human"] = held.get(new["key"], {"verdict": None, "note": None, "edited": False})
new["last_seen"] = week
# A model reading belongs to the week that produced it and is never
# carried as project state; it has no human verdict behind it.
new["status"] = "reading" if new["provenance"]["by"] == "model" else "open"
new["weeks_open"] = sum(1 for w in weeks if w >= new["first_seen"])
if "task_state" in new:
same_section = old and (old.get("task_state") or {}).get("section") == new["task_state"]["section"]
new["section_since"] = old.get("section_since", week) if same_section else week
new["weeks_in_section"] = sum(1 for w in weeks if w >= new["section_since"])
new["provenance"].setdefault("at", stamp)
new["provenance"].setdefault("evidence_week", week)
out.append(new)
for key, old in by_key.items():
if key in seen:
continue
if old.get("status") == "open":
if key in closes:
old["status"] = "resolved"
old["resolved_in"] = week
elif old["provenance"]["rule"] not in CARRIES:
# A CI week, a dimension's activity, a release cut: each states
# something about one week and says nothing about the next.
old["status"] = "lapsed"
old["lapsed_in"] = week
else:
old["weeks_open"] = sum(1 for w in weeks if w >= old.get("first_seen", week))
old["carried"] = True
out.append(old)
out.sort(key=lambda f: (f["status"] != "open", MOVEMENTS.index(f["movement"])
if f["movement"] in MOVEMENTS else 9, f["dimension"], f["id"]))
return {
"version": VERSION,
"updated": stamp,
"latest_week": week,
"weeks": weeks,
"analysis": {"week": week, "analyst": analyst,
"deterministic_rules": sorted({f["provenance"]["rule"] for f in produced
if f["provenance"]["by"] == "deterministic"})},
"findings": out,
}
# ---- report ----------------------------------------------------------------
BADGE = {"milestone_reached": "◆ milestone", "regressed": "▼ regressed",
"blocked": "■ blocked", "decision_required": "? decision",
"advanced": "▲ advanced", "unchanged": "· unchanged"}
def report(state, ev, week):
lines = [f"# Souveraine project state — {week}",
"",
f"{ev['range']['start']} to {ev['range']['end']} ({ev['range']['tz']}). "
f"Internal. Generated from `project-state/evidence/{week}.json`; "
f"the prose is disposable, the evidence and `state.json` are not.",
""]
r = ev["rollup"]
lines += ["## The week, counted", "",
f"- {len(ev['commits'])} commits across "
f"{len({row['repo'] for row in ev['commits']})} repos, "
f"{len(ev['ci'])} CI runs, {len(ev['declared'])} declared state changes.",
f"- Issues: {r['issues']['open']} open, {r['issues']['opened_this_week']} filed, "
f"{r['issues']['closed_this_week']} closed. "
f"Pulls: {r['pulls']['open']} open, {r['pulls']['merged_this_week']} merged.",
"- Dimension basis: "
+ ", ".join(f"{n} by {b}" for b, n in sorted(r["dimension_basis"].items(),
key=lambda kv: -kv[1]))
+ " (`paths` read the diff, `subject` read the commit subject, "
"`repo` fell back to the repository, `none` placed nothing).",
""]
open_now = [f for f in state["findings"] if f["status"] == "open"]
current = [f for f in open_now if f["last_seen"] == week]
standing = [f for f in open_now if f["last_seen"] != week]
resolved = [f for f in state["findings"] if f.get("resolved_in") == week]
read = [f for f in state["findings"]
if f["status"] == "reading" and f["last_seen"] == week]
# A task that simply stands under its section is state, not news; it belongs
# in Standing, not in a movement group that would be 60 rows long.
quiet = [f for f in current if f["provenance"]["rule"] == "task-standing"]
current = [f for f in current if f["provenance"]["rule"] != "task-standing"
or f["movement"] == "blocked"]
for movement in MOVEMENTS:
group = [f for f in current if f["movement"] == movement]
if not group:
continue
lines += [f"## {BADGE[movement]}", ""]
for f in group:
age = f" · open {f['weeks_open']} weeks since {f['first_seen']}" if f["weeks_open"] > 1 else ""
was = f["was"]["movement"] if f.get("was") else None
turn = f" · was {was.replace('_', ' ')}" if was and was != movement else ""
mark = "model" if f["provenance"]["by"] == "model" else f["provenance"]["rule"]
lines += [f"**{f['id']} · {f['dimension']} · {f['title']}** ",
f"{f['statement']} ",
f"<sub>{mark}{age}{turn} · evidence: "
f"{', '.join('`' + e + '`' for e in f['evidence'][:8])}"
f"{'' if len(f['evidence']) > 8 else ''}</sub>", ""]
if read:
analyst = state["analysis"]["analyst"] or {}
lines += ["## Read by the analyst — unreviewed", "",
f"Written by {analyst.get('model', 'a model')}, not computed. Every id below "
f"exists in the week's evidence and every finding spans two kinds of it; "
f"neither check makes the reading correct. Nothing here has a human verdict.",
""]
for f in read:
lines += [f"**{f['id']} · {f['dimension']} · {BADGE[f['movement']]} · {f['title']}** ",
f"{f['statement']} ",
f"<sub>evidence: {', '.join('`' + e + '`' for e in f['evidence'])}</sub>", ""]
if resolved:
lines += ["## Closed this week", ""]
for f in resolved:
lines += [f"- **{f['id']} · {f['title']}** — held from {f['first_seen']} "
f"to {f['last_seen']}, {f['weeks_open']} week(s)."]
lines.append("")
held = sorted((f for f in quiet if f["movement"] != "blocked"),
key=lambda f: (-f.get("weeks_in_section", 1), int(f["key"].split("/")[1])))
if held:
lines += ["## Standing longest without moving", "",
f"{len(quiet)} task(s) sat in their section untouched this week. "
f"The ten that have sat longest:", ""]
for f in held[:10]:
lines.append(f"- **{f['id']} · {f['dimension']} · {f['title']}** — "
f"{f['task_state']['section']}” for "
f"{f.get('weeks_in_section', 1)} week(s), since {f.get('section_since')}.")
lines.append("")
if standing:
lines += ["## Still open, unmentioned by this week's evidence", "",
"Nothing closed these; the week simply said nothing about them.", ""]
for f in sorted(standing, key=lambda f: -f["weeks_open"]):
lines.append(f"- **{f['id']} · {f['dimension']} · {f['title']}** — "
f"last evidence {f['last_seen']}, open {f['weeks_open']} week(s) "
f"since {f['first_seen']}.")
lines.append("")
analyst = state["analysis"]["analyst"]
lines += ["## Provenance", "",
f"- Evidence: `project-state/evidence/{week}.json`, collected "
f"{ev['collected_at']} by `{ev['collector']['tool']}` v{ev['collector']['version']} "
f"against {ev['collector']['forge']} in {ev['collector']['api_calls']} API calls.",
f"- Deterministic rules: {', '.join(state['analysis']['deterministic_rules'])}.",
(f"- Analyst: {analyst['model']} ({analyst['endpoint']}), "
f"{analyst.get('accepted', 0)} findings kept, {analyst.get('dropped', 0)} dropped."
if analyst else "- Analyst: none run; every finding here is deterministic."),
f"- Trace one: `tools/project-state/synthesize.py --explain <id>`.", ""]
if ev["collector"]["notes"]:
lines += ["Collector notes: " + "; ".join(ev["collector"]["notes"]), ""]
return "\n".join(lines)
def explain(state, finding_id):
f = next((x for x in state.get("findings", []) if x["id"] == finding_id or x["key"] == finding_id), None)
if not f:
print(f"no finding {finding_id}", file=sys.stderr)
return 1
print(json.dumps({k: v for k, v in f.items() if k != "evidence"}, indent=2, ensure_ascii=False))
week = f["provenance"].get("evidence_week", f["last_seen"])
ev = load(os.path.join(STORE, "evidence", f"{week}.json"))
index = {row["id"]: (group, row)
for group in ("commits", "ci", "issues", "pulls", "releases", "declared")
for row in ev.get(group, [])}
index[f"w:{week}"] = ("rollup", ev.get("rollup", {}))
print(f"\nevidence ({len(f['evidence'])} rows from {week}):")
for eid in f["evidence"]:
group, row = index.get(eid, (None, None))
if not row:
print(f" {eid}: MISSING from {week} evidence")
continue
print(f"\n [{group}] {eid}")
print(" " + json.dumps(row, indent=2, ensure_ascii=False).replace("\n", "\n "))
return 0
VERDICTS = ("confirm", "promote", "reject", "edit")
VERDICT_FILE = "verdicts.json"
def verdicts():
"""A person's word on a finding, keyed by finding key.
Kept outside state.json because state is derived and gets regenerated a
verdict written into it vanished the next time the week was passed over.
This file is the one thing here a machine does not write.
"""
return load(os.path.join(STORE, VERDICT_FILE), {})
def write_verdict(spec, note, text=None, by=None):
finding_id, _, verdict = spec.partition("=")
if verdict not in VERDICTS:
note(f"verdict must be one of {', '.join(VERDICTS)}, not {verdict!r}")
return 2
state = load(os.path.join(STORE, "state.json"))
target = next((f for f in state.get("findings", [])
if f["id"] == finding_id or f["key"] == finding_id), None)
if not target:
note(f"no finding {finding_id}")
return 1
held = verdicts()
held[target["key"]] = {
"verdict": verdict,
"note": text,
"by": by or os.environ.get("USER") or "unknown",
"at": c.iso_z(dt.datetime.now(dt.timezone.utc)),
"edited": verdict == "edit",
"week": state.get("latest_week"),
"finding_id": target["id"],
"title": target["title"],
}
path = os.path.join(STORE, VERDICT_FILE)
open(path, "w").write(json.dumps(held, indent=2, ensure_ascii=False) + "\n")
target["human"] = held[target["key"]]
open(os.path.join(STORE, "state.json"), "w").write(
json.dumps(state, indent=2, ensure_ascii=False) + "\n")
note(f"{target['id']} {verdict} by {held[target['key']]['by']}{target['title'][:60]}")
return 0
def evidence_weeks():
path = os.path.join(STORE, "evidence")
return sorted(name[:-5] for name in os.listdir(path)) if os.path.isdir(path) else []
def pending(state):
"""Evidence weeks the state has not yet passed over, oldest first."""
done = set(state.get("weeks", []))
return [week for week in evidence_weeks() if week not in done]
def run_week(label, prior, args, note):
ev = load(os.path.join(STORE, "evidence", f"{label}.json"))
if not ev:
note(f"no evidence for {label} — run collect.py --week {label}")
return None, None
prior_ev = load(os.path.join(STORE, "evidence", f"{c.previous_week(label)}.json"))
produced = deterministic(ev, prior_ev)
model_rows, analyst = interpret(ev, produced, prior, note) if getattr(args, "interpret", False) else ([], None)
# A task closes when it is gone from the index, not when it merely goes
# unmentioned. The delisting week still shows its milestone; the week after
# is when it stops being open state.
listed = set(ev.get("index", {}))
closes = {f["key"] for f in prior.get("findings", [])
if f["key"].startswith("task/") and f["key"].split("/", 1)[1] not in listed}
return carry(prior, produced + model_rows, label, analyst, closes), ev
def persist(state, ev, label):
text = report(state, ev, label)
for folder in ("reports", "history"):
os.makedirs(os.path.join(STORE, folder), exist_ok=True)
open(os.path.join(STORE, "reports", f"{label}.md"), "w").write(text)
blob = json.dumps(state, indent=2, ensure_ascii=False) + "\n"
open(os.path.join(STORE, "history", f"{label}.json"), "w").write(blob)
open(os.path.join(STORE, "state.json"), "w").write(blob)
return text
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--week", default="last")
ap.add_argument("--catch-up", action="store_true",
help="pass over every collected week the state has not seen, in order")
ap.add_argument("--pending", action="store_true", help="name those weeks and stop")
ap.add_argument("--interpret", action="store_true",
help="run the model pass; needs ANALYST_URL and ANALYST_MODEL, "
"fails closed if that exact model is not served")
ap.add_argument("--verdict", help="a person's word on a finding, as f-0069=promote")
ap.add_argument("--note", help="text to store with --verdict")
ap.add_argument("--by", help="who is giving the verdict (default $USER)")
ap.add_argument("--explain", help="print a finding and every evidence row it cites")
ap.add_argument("--dry-run", action="store_true", help="print the report, write nothing")
ap.add_argument("--report-only", action="store_true",
help="rewrite the report from the stored state, running nothing")
args = ap.parse_args()
state_path = os.path.join(STORE, "state.json")
prior = load(state_path, {"findings": []})
if args.explain:
return explain(prior, args.explain)
notes = []
def note(message):
notes.append(message)
print(f"[project-state] {message}", file=sys.stderr)
if args.verdict:
return write_verdict(args.verdict, note, args.note, args.by)
if args.pending:
weeks = pending(prior)
print("\n".join(weeks) if weeks else "", end="\n" if weeks else "")
note(f"{len(weeks)} week(s) collected but not yet passed over" if weeks
else f"state is current through {prior.get('latest_week', 'nothing')}")
return 0
if args.catch_up:
weeks = pending(prior)
if not weeks:
note(f"nothing pending; state is current through {prior.get('latest_week')}")
return 0
for label in weeks:
state, ev = run_week(label, prior, args, note)
if not state:
return 2
persist(state, ev, label)
note(f"{label}: {sum(1 for f in state['findings'] if f['status'] == 'open')} open")
prior = state
return 0
label, _, _ = c.parse_week(args.week)
ev = load(os.path.join(STORE, "evidence", f"{label}.json"))
if not ev:
note(f"no evidence for {label} — run collect.py --week {label}")
return 2
if args.report_only:
state = load(os.path.join(STORE, "history", f"{label}.json")) or prior
text = report(state, ev, label)
if args.dry_run:
sys.stdout.write(text)
return 0
open(os.path.join(STORE, "reports", f"{label}.md"), "w").write(text)
note(f"{label}: report rewritten from stored state")
return 0
# Re-running a week already in the state starts from the week before it, so
# a second pass does not compound onto its own output.
if label in prior.get("weeks", []):
prior = load(os.path.join(STORE, "history", f"{c.previous_week(label)}.json"),
{"findings": []})
elif prior.get("latest_week") and c.previous_week(label) != prior["latest_week"]:
note(f"state stops at {prior['latest_week']} and this is {label}"
f"{len(pending(prior))} week(s) unseen; --catch-up runs them in order")
state, ev = run_week(label, prior, args, note)
if not state:
return 2
if args.dry_run:
sys.stdout.write(report(state, ev, label))
return 0
persist(state, ev, label)
produced = [f for f in state["findings"] if f["last_seen"] == label]
note(f"{label}: {len(produced)} findings this week, "
f"{sum(1 for f in state['findings'] if f['status'] == 'open')} open "
f"-> project-state/reports/{label}.md")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except AnalystUnavailable as failure:
# Nothing was written. The named model did not run, and no other one
# stands in for it.
print(f"[project-state] analyst unavailable: {failure}", file=sys.stderr)
sys.exit(3)