scripts.project_status
Reference › scripts › project_status
scripts/project_status.py produces the project status report from artifacts rather than from hand-maintained state (workflow-improvements-plan Phase 4). It is run by a human and by the session-prime step, which embeds the brief form; tests/scripts/test_project_status.py and tests/scripts/test_rtm_evidence_paths.py cover it. It has no internal imports.
build_report gathers one fact per source: get_latest_tag and get_session_number, get_test_verdict (the newest run_tests-shaped JSON report under reports/), get_rtm_summary (grade counts from reports/rtm-evidence.json, written by generate_rtm_evidence), get_coverage_floor, get_infra_status (runs start_test_services with --status --json), get_stale_tasks (not-done deferred work open longer than a number of days; read-only) and _read_context_json. Options: --root (default the repository), --brief (condensed one-fact-per-line output from render_brief) and --json (the full report as JSON). By default it prints the brief followed by the JSON. It writes nothing and exits 0.
At a glance
| Source | scripts/project_status.py |
| Lines of code | 338 |
| Classes | 0 |
| Functions | 11 |
| Imports (internal) | none |
| Imported by | none |
| Tests that import it | tests/scripts/test_project_status.py, tests/scripts/test_rtm_evidence_paths.py |
Class diagram
No classes; see Functions.
Dependency diagram
No internal imports and no importers.
API
Functions
| Function | Signature | Summary |
|---|---|---|
_read_context_json |
_read_context_json(root: Path) -> dict | None |
Read context.json READ-ONLY — never through the self-healing writer. |
get_latest_tag |
get_latest_tag(root: Path) -> str | None |
Most recently created SESSION tag, or None if none exist / git fails. |
get_session_number |
get_session_number(root: Path) -> int | None |
Current active session number from context.json (read-only). |
get_test_verdict |
get_test_verdict(root: Path) -> dict |
Newest run_tests-shaped JSON report under reports/, if any exist. |
get_rtm_summary |
get_rtm_summary(root: Path) -> dict |
Grade counts from reports/rtm-evidence.json. |
get_coverage_floor |
get_coverage_floor(root: Path) -> float | None |
[tool.coverage.report] fail_under from pyproject.toml, if set. |
get_infra_status |
get_infra_status(root: Path, timeout: float = _INFRA_TIMEOUT_S) -> dict |
One-liner infra readiness via start_test_services.py --status --json. |
get_stale_tasks |
get_stale_tasks(root: Path, days: int = _STALE_TASK_DAYS) -> list[dict] |
Not-done deferred_work items open for more than days. |
build_report |
build_report(root: Path) -> dict |
Assemble the full artifact-sourced status report. |
render_brief |
render_brief(report: dict) -> str |
Condensed one-fact-per-line rendering, suitable for embedding (session prime). |
main |
main(argv: list[str] | None = None) -> int |
Source
#!/usr/bin/env python3
"""Artifact-sourced project status report (workflow-improvements-plan.md Phase 4).
Generates ONE status report purely from artifacts already on disk / already
computable — never from hand-maintained prose in plan files. Session-CLI
convention (see ``AGENTS.md`` "Plan files vs. state" — Phase 4.2): plan files
and ``session task`` hold INTENT (what's next); this script is STATE (what's
done/verified).
Sections:
1. Version — latest git tag + current session number (context.json,
READ ONLY: parsed with a bare ``json.loads()``, never
through ``ContextFileManager``'s self-healing writer
path, so this script can never mutate context.json).
2. Test verdict — newest run_tests-shaped JSON report under ``reports/``,
if one exists. None is currently produced by any gate
in this repo — reported honestly as "not found" rather
than fabricated.
3. RTM summary — grade counts from ``reports/rtm-evidence.json`` (a
multi-MB artifact; parsed once in-process here, never
surfaced whole to a caller).
4. Coverage floor — ``[tool.coverage.report] fail_under`` from
``pyproject.toml``, if set.
5. Infra — one-liner via ``scripts/start_test_services.py
--status --json``.
6. Drift warnings — ``session task`` items (``deferred_work``, not
``done``) open for more than 30 days. Plan-checkbox
drift (an item marked done in a ``plans/*.md`` file
whose linked verification command actually fails) has
no machine-readable checkbox->command registry yet in
this repo — reported explicitly as "not yet
mappable", not silently skipped.
CLI: ``python scripts/project_status.py [--root PATH] [--brief] [--json]``.
Exits 0 always (this is a report, not a gate).
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
import tomllib
from datetime import datetime, timedelta
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
PLAN_DRIFT_NOTE = (
"plan-drift check: not yet mappable (no checkbox->verification registry)"
)
_INFRA_TIMEOUT_S = 15.0
_GIT_TIMEOUT_S = 5.0
_STALE_TASK_DAYS = 30
def _read_context_json(root: Path) -> dict | None:
"""Read ``context.json`` READ-ONLY — never through the self-healing writer."""
path = root / "context.json"
if not path.is_file():
return None
try:
data = json.loads(path.read_text(encoding="utf-8"))
except OSError, json.JSONDecodeError:
return None
return data if isinstance(data, dict) else None
def get_latest_tag(root: Path) -> str | None:
"""Most recently created SESSION tag, or ``None`` if none exist / git fails.
Scoped to ``session-*``: the status brief keys session identity off the
session-tag family, and other tag families (plugin-*, adr-*) interleave
with it by creation date (measured 2026-10-18: the newest tag was
``plugin-94a23ae16dad``, which the brief must not report as a session).
"""
try:
proc = subprocess.run(
[
"git",
"for-each-ref",
"--sort=-creatordate",
"--format=%(refname:short)",
"refs/tags/session-*",
],
cwd=root,
capture_output=True,
text=True,
timeout=_GIT_TIMEOUT_S,
)
except subprocess.TimeoutExpired, OSError:
return None
if proc.returncode != 0:
return None
lines = proc.stdout.strip().splitlines()
return lines[0] if lines else None
def get_session_number(root: Path) -> int | None:
"""Current active session number from ``context.json`` (read-only)."""
data = _read_context_json(root)
if data is None:
return None
state = data.get("state")
if not isinstance(state, dict):
return None
session_num = state.get("active_session")
return session_num if isinstance(session_num, int) else None
def get_test_verdict(root: Path) -> dict:
"""Newest run_tests-shaped JSON report under ``reports/``, if any exist."""
reports_dir = root / "reports"
if not reports_dir.is_dir():
return {"found": False, "note": "no reports/ directory"}
patterns = (
"run-tests*.json",
"run_tests*.json",
"test-report*.json",
"pytest-report*.json",
)
candidates = [
p for pattern in patterns for p in reports_dir.glob(pattern) if p.is_file()
]
if not candidates:
return {
"found": False,
"note": (
"no run_tests JSON report found in reports/ (produce one with "
"`python scripts/run_tests.py --json > reports/run-tests-latest.json`)"
),
}
newest = max(candidates, key=lambda p: p.stat().st_mtime)
try:
payload = json.loads(newest.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as e:
return {"found": True, "path": str(newest.relative_to(root)), "error": str(e)}
return {"found": True, "path": str(newest.relative_to(root)), "report": payload}
def get_rtm_summary(root: Path) -> dict:
"""Grade counts from ``reports/rtm-evidence.json``."""
path = root / "reports" / "rtm-evidence.json"
if not path.is_file():
return {"found": False}
try:
with path.open(encoding="utf-8") as fh:
data = json.load(fh)
except (OSError, json.JSONDecodeError) as e:
return {"found": True, "error": str(e)}
rows = data.get("rows", []) if isinstance(data, dict) else []
grades: dict[str, int] = {}
for row in rows:
grade = row.get("grade", "UNKNOWN") if isinstance(row, dict) else "UNKNOWN"
grades[grade] = grades.get(grade, 0) + 1
return {
"found": True,
"verified": data.get("verified") if isinstance(data, dict) else None,
"requirements": data.get("requirements") if isinstance(data, dict) else None,
"rows": len(rows),
"grades": grades,
}
def get_coverage_floor(root: Path) -> float | None:
"""``[tool.coverage.report] fail_under`` from pyproject.toml, if set."""
pyproject = root / "pyproject.toml"
if not pyproject.is_file():
return None
try:
with pyproject.open("rb") as fh:
data = tomllib.load(fh)
except OSError, tomllib.TOMLDecodeError:
return None
report_cfg = data.get("tool", {}).get("coverage", {}).get("report", {})
floor = report_cfg.get("fail_under")
return floor if isinstance(floor, (int, float)) else None
def get_infra_status(root: Path, timeout: float = _INFRA_TIMEOUT_S) -> dict:
"""One-liner infra readiness via ``start_test_services.py --status --json``."""
script = root / "scripts" / "start_test_services.py"
if not script.exists():
return {"error": "scripts/start_test_services.py not found"}
try:
proc = subprocess.run(
[sys.executable, str(script), "--status", "--json"],
cwd=root,
capture_output=True,
text=True,
timeout=timeout,
)
except subprocess.TimeoutExpired:
return {"error": f"timed out after {timeout:.0f}s"}
except OSError as e:
return {"error": str(e)}
try:
report = json.loads(proc.stdout)
except json.JSONDecodeError, ValueError:
return {"error": f"could not parse infra report (rc={proc.returncode})"}
return report if isinstance(report, dict) else {"error": "unexpected infra shape"}
def get_stale_tasks(root: Path, days: int = _STALE_TASK_DAYS) -> list[dict]:
"""Not-done ``deferred_work`` items open for more than ``days``. READ-ONLY."""
data = _read_context_json(root)
if data is None:
return []
cutoff = datetime.now() - timedelta(days=days)
stale: list[dict] = []
for item in data.get("deferred_work", []):
if not isinstance(item, dict) or item.get("state") == "done":
continue
ts = item.get("timestamp")
if not ts:
continue
try:
when = datetime.fromisoformat(ts).replace(tzinfo=None)
except ValueError:
continue
if when < cutoff:
stale.append(
{
"text": item.get("text", ""),
"state": item.get("state"),
"age_days": (datetime.now() - when).days,
}
)
stale.sort(key=lambda s: s["age_days"], reverse=True)
return stale
def build_report(root: Path) -> dict:
"""Assemble the full artifact-sourced status report."""
return {
"root": str(root),
"latest_tag": get_latest_tag(root),
"session_number": get_session_number(root),
"test_verdict": get_test_verdict(root),
"rtm_summary": get_rtm_summary(root),
"coverage_floor": get_coverage_floor(root),
"infra": get_infra_status(root),
"stale_tasks": get_stale_tasks(root),
"plan_drift_note": PLAN_DRIFT_NOTE,
}
def render_brief(report: dict) -> str:
"""Condensed one-fact-per-line rendering, suitable for embedding (session prime)."""
lines: list[str] = []
session_num = report["session_number"]
lines.append(
f"tag: {report['latest_tag'] or 'none'} | "
f"session: {session_num if session_num is not None else 'unknown'}"
)
tv = report["test_verdict"]
if tv.get("found"):
r = tv.get("report", {})
if isinstance(r, dict) and ("passed" in r or "failed" in r):
lines.append(
f"tests: {tv['path']} — passed={r.get('passed', '?')} "
f"failed={r.get('failed', '?')}"
)
else:
lines.append(f"tests: {tv['path']} (unrecognized report schema)")
else:
lines.append(f"tests: {tv.get('note', 'unavailable')}")
rtm = report["rtm_summary"]
if rtm.get("found") and "error" not in rtm:
grades = rtm.get("grades", {})
grade_str = ", ".join(f"{k}={v}" for k, v in sorted(grades.items()))
lines.append(f"rtm: {rtm.get('rows', 0)} rows ({grade_str})")
elif rtm.get("found"):
lines.append(
f"rtm: error reading reports/rtm-evidence.json: {rtm.get('error')}"
)
else:
lines.append("rtm: reports/rtm-evidence.json not found")
floor = report["coverage_floor"]
lines.append(f"coverage floor: {floor if floor is not None else 'not configured'}")
infra = report["infra"]
if "error" in infra:
lines.append(f"infra: error: {infra['error']}")
else:
lines.append(
f"infra: {'all ready' if infra.get('all_required_ok') else 'NOT all ready'}"
)
stale = report["stale_tasks"]
lines.append(f"stale tasks (>{_STALE_TASK_DAYS}d open): {len(stale)}")
lines.extend(f" - [{s['age_days']}d] {s['text']}" for s in stale[:5])
if len(stale) > 5:
lines.append(f" ... and {len(stale) - 5} more")
lines.append(report["plan_drift_note"])
return "\n".join(lines)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Artifact-sourced project status report (never hand-maintained state)"
)
parser.add_argument(
"--root",
type=Path,
default=REPO_ROOT,
help="Project root (default: this script's repo).",
)
parser.add_argument(
"--brief", action="store_true", help="Condensed one-fact-per-line output."
)
parser.add_argument(
"--json", action="store_true", help="Full report as JSON (implies not --brief)."
)
args = parser.parse_args(argv)
report = build_report(args.root)
if args.json:
print(json.dumps(report, indent=2, default=str))
else:
print(render_brief(report))
if not args.brief:
print()
print(json.dumps(report, indent=2, default=str))
return 0
if __name__ == "__main__":
sys.exit(main())