[codex] repo_state global refresh + mandatory header gate #10

Merged
electron-rare merged 1 commits from codex/repo-state-global-index into main 2026-02-21 02:24:29 +00:00
8 changed files with 646 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
name: Repo State
on:
push:
pull_request:
workflow_dispatch:
jobs:
repo-state:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Generate repo state
run: |
python tools/repo_state/collect.py --repo-name Kill_LIFE
- name: Upload repo-state artifact
uses: actions/upload-artifact@v4
with:
name: repo-state
path: |
docs/REPO_STATE.md
docs/repo_state.json
@@ -0,0 +1,41 @@
name: Repo State Header Gate
on:
push:
pull_request:
workflow_dispatch:
jobs:
header-gate:
runs-on: ubuntu-latest
steps:
- name: Checkout Kill_LIFE
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Clone sibling repos
run: |
cd ..
git clone --depth=1 https://github.com/electron-rare/RTC_BL_PHONE.git RTC_BL_PHONE
git clone --depth=1 https://github.com/electron-rare/le-mystere-professeur-zacus.git le-mystere-professeur-zacus
- name: Build global header
run: |
tools/repo_state/repo_refresh.sh --header-only
- name: Lint header contract
run: |
python tools/repo_state/lint_header_contract.py
- name: Upload global repo-state artifacts
uses: actions/upload-artifact@v4
with:
name: global-repo-state
path: |
artifacts/repo_state/global_summary.md
artifacts/repo_state/global_index.json
artifacts/repo_state/header.latest.md
+11
View File
@@ -0,0 +1,11 @@
<!-- REPO_STATE:v1 -->
Repo: Kill_LIFE
Branch: codex/repo-state-global-index
HEAD: 432c76a57e5cd9c1603d68f56cc6d012168be1b3
HeadDate: 2026-02-21T03:03:06+01:00
HeadSubject: Merge pull request #9 from electron-rare/codex/autonomy-plan-todo-hw-local
RepoURL: https://github.com/electron-rare/Kill_LIFE.git
ProjectKind: agentic_orchestrator
PivotChanges: [{"path": "(none)", "tags": ["general_change"]}]
ImpactGates: general_change
GeneratedAtUTC: 2026-02-21T02:22:47Z
+64
View File
@@ -0,0 +1,64 @@
# REPO_STATE Header Contract
Version: `v1`
## Source of truth
The global response header MUST be derived only from each repository file:
- `docs/REPO_STATE.md`
No free-form manual edits are allowed in generated outputs.
## Required local files (Kill_LIFE)
- `tools/repo_state/collect.py`
- `tools/repo_state/repo_refresh.sh`
- `tools/repo_state/lint_header_contract.py`
- `docs/REPO_STATE.md`
- `docs/repo_state.json`
## Header format (mandatory)
```md
[REPO-STATE UTC: <timestamp>]
Kill_LIFE | HEAD <sha> | pivots: <...> | gates: <...>
RTC_BL_PHONE | HEAD <sha> | pivots: <...> | gates: <...>
le-mystere-professeur-zacus | HEAD <sha> | pivots: <...> | gates: <...>
[/REPO-STATE]
```
## Contract checks
`repo_state_header_gate` MUST fail if any of the following is true:
- missing `<!-- REPO_STATE:v1 -->` marker in `docs/REPO_STATE.md`
- missing required keys in `docs/REPO_STATE.md` or `docs/repo_state.json`
- malformed header markers in `artifacts/repo_state/header.latest.md`
- missing one of the 3 repository lines in the generated header
## Commands
Generate local state:
```bash
python tools/repo_state/collect.py --repo-name Kill_LIFE
```
Generate global artifacts from sibling repos:
```bash
tools/repo_state/repo_refresh.sh
```
Header only:
```bash
tools/repo_state/repo_refresh.sh --header-only
```
JSON only:
```bash
tools/repo_state/repo_refresh.sh --json-only
```
+22
View File
@@ -0,0 +1,22 @@
{
"schema_version": "repo_state.v1",
"generated_at_utc": "2026-02-21T02:22:47Z",
"repo": "Kill_LIFE",
"repo_url": "https://github.com/electron-rare/Kill_LIFE.git",
"branch": "codex/repo-state-global-index",
"head": "432c76a57e5cd9c1603d68f56cc6d012168be1b3",
"head_date": "2026-02-21T03:03:06+01:00",
"head_subject": "Merge pull request #9 from electron-rare/codex/autonomy-plan-todo-hw-local",
"project_kind": "agentic_orchestrator",
"pivot_changes": [
{
"path": "(none)",
"tags": [
"general_change"
]
}
],
"impact_gates": [
"general_change"
]
}
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""Generate repo state files (Markdown + JSON) for one repository."""
from __future__ import annotations
import argparse
import json
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Set
REQUIRED_SCHEMA_VERSION = "repo_state.v1"
def run_git(repo_root: Path, *args: str) -> str:
result = subprocess.run(
["git", *args],
cwd=repo_root,
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()
def detect_project_kind(repo_root: Path) -> str:
if (repo_root / "tools" / "ai").exists() and (repo_root / "zeroclaw").exists():
return "agentic_orchestrator"
if (repo_root / "platformio.ini").exists():
return "firmware_embedded"
if (repo_root / "hardware" / "firmware" / "platformio.ini").exists():
return "hardware_firmware_hybrid"
if (repo_root / "kit-maitre-du-jeu").exists():
return "narrative_hardware_hybrid"
return "general"
def gates_for_path(path: str) -> Set[str]:
p = path.strip().lstrip("./")
gates: Set[str] = set()
if p.startswith("tools/ai/") or p.startswith("tools/repo_state/") or p.startswith("zeroclaw/"):
gates.add("agentic_orchestration")
if p.startswith(".github/workflows/"):
gates.add("ci_integrity")
if (
p == "platformio.ini"
or p.startswith("firmware/")
or p.startswith("src/")
or p.startswith("hardware/firmware/")
):
gates.add("firmware_build_test")
if p.startswith("hardware/"):
gates.add("hardware_validation")
if p == "README.md" or p.startswith("docs/") or p.startswith("specs/"):
gates.add("docs_specs_sync")
if p.startswith("compliance/") or p.startswith("security/") or p.startswith("standards/"):
gates.add("compliance_security")
if not gates:
gates.add("general_change")
return gates
def get_changed_paths(repo_root: Path, limit: int = 5) -> List[str]:
raw = run_git(repo_root, "show", "--name-only", "--pretty=format:", "HEAD")
paths = [line.strip() for line in raw.splitlines() if line.strip()]
return paths[:limit]
def build_state(repo_root: Path, repo_name_override: str | None) -> Dict[str, object]:
repo_name = repo_name_override or repo_root.name
head = run_git(repo_root, "rev-parse", "HEAD")
branch = run_git(repo_root, "rev-parse", "--abbrev-ref", "HEAD")
head_date = run_git(repo_root, "show", "-s", "--format=%cI", "HEAD")
head_subject = run_git(repo_root, "show", "-s", "--format=%s", "HEAD")
try:
repo_url = run_git(repo_root, "config", "--get", "remote.origin.url")
except subprocess.CalledProcessError:
repo_url = ""
changed_paths = get_changed_paths(repo_root)
pivot_changes: List[Dict[str, object]] = []
all_gates: Set[str] = set()
for path in changed_paths:
gates = sorted(gates_for_path(path))
pivot_changes.append({"path": path, "tags": gates})
all_gates.update(gates)
if not pivot_changes:
pivot_changes = [{"path": "(none)", "tags": ["general_change"]}]
all_gates.add("general_change")
generated_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
return {
"schema_version": REQUIRED_SCHEMA_VERSION,
"generated_at_utc": generated_at,
"repo": repo_name,
"repo_url": repo_url,
"branch": branch,
"head": head,
"head_date": head_date,
"head_subject": head_subject,
"project_kind": detect_project_kind(repo_root),
"pivot_changes": pivot_changes,
"impact_gates": sorted(all_gates),
}
def write_outputs(state: Dict[str, object], out_md: Path, out_json: Path) -> None:
out_md.parent.mkdir(parents=True, exist_ok=True)
out_json.parent.mkdir(parents=True, exist_ok=True)
pivot_changes_inline = json.dumps(state["pivot_changes"], ensure_ascii=False)
impact_gates_inline = ", ".join(state["impact_gates"])
md_content = "\n".join(
[
"<!-- REPO_STATE:v1 -->",
f"Repo: {state['repo']}",
f"Branch: {state['branch']}",
f"HEAD: {state['head']}",
f"HeadDate: {state['head_date']}",
f"HeadSubject: {state['head_subject']}",
f"RepoURL: {state['repo_url']}",
f"ProjectKind: {state['project_kind']}",
f"PivotChanges: {pivot_changes_inline}",
f"ImpactGates: {impact_gates_inline}",
f"GeneratedAtUTC: {state['generated_at_utc']}",
"",
]
)
out_md.write_text(md_content, encoding="utf-8")
out_json.write_text(json.dumps(state, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser(description="Generate docs/REPO_STATE.md and docs/repo_state.json")
parser.add_argument("--repo-name", default=None, help="Override repo name")
parser.add_argument("--repo-root", default=".", help="Path to the git repository root (default: .)")
parser.add_argument("--out-md", default="docs/REPO_STATE.md", help="Output markdown path")
parser.add_argument("--out-json", default="docs/repo_state.json", help="Output json path")
args = parser.parse_args()
repo_root = Path(args.repo_root).resolve()
if not (repo_root / ".git").exists():
raise SystemExit(f"[fail] not a git repository: {repo_root}")
state = build_state(repo_root, args.repo_name)
out_md = (repo_root / args.out_md).resolve()
out_json = (repo_root / args.out_json).resolve()
write_outputs(state, out_md, out_json)
print(f"[ok] generated {out_md}")
print(f"[ok] generated {out_json}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Lint repo-state contract for local/global header generation."""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
REQUIRED_MD_KEYS = [
"Repo",
"Branch",
"HEAD",
"HeadDate",
"ProjectKind",
"PivotChanges",
"ImpactGates",
]
REQUIRED_JSON_KEYS = [
"schema_version",
"generated_at_utc",
"repo",
"repo_url",
"branch",
"head",
"head_date",
"head_subject",
"project_kind",
"pivot_changes",
"impact_gates",
]
REQUIRED_REPOS = ["Kill_LIFE", "RTC_BL_PHONE", "le-mystere-professeur-zacus"]
def fail(msg: str) -> None:
print(f"[fail] {msg}", file=sys.stderr)
raise SystemExit(1)
def parse_kv_lines(path: Path) -> dict:
lines = path.read_text(encoding="utf-8").splitlines()
if not lines or lines[0].strip() != "<!-- REPO_STATE:v1 -->":
fail(f"missing REPO_STATE marker in {path}")
data: dict[str, str] = {}
for line in lines:
if ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--repo-root", default=".")
parser.add_argument("--header-file", default="artifacts/repo_state/header.latest.md")
args = parser.parse_args()
repo_root = Path(args.repo_root).resolve()
md_file = repo_root / "docs/REPO_STATE.md"
json_file = repo_root / "docs/repo_state.json"
refresh_script = repo_root / "tools/repo_state/repo_refresh.sh"
collect_script = repo_root / "tools/repo_state/collect.py"
header_file = (repo_root / args.header_file).resolve()
for path in [md_file, json_file, refresh_script, collect_script, header_file]:
if not path.exists():
fail(f"missing required file: {path}")
kv = parse_kv_lines(md_file)
missing_md = [k for k in REQUIRED_MD_KEYS if k not in kv]
if missing_md:
fail(f"missing keys in {md_file}: {', '.join(missing_md)}")
try:
json.loads(kv["PivotChanges"])
except json.JSONDecodeError as exc:
fail(f"invalid PivotChanges JSON in {md_file}: {exc}")
state = json.loads(json_file.read_text(encoding="utf-8"))
missing_json = [k for k in REQUIRED_JSON_KEYS if k not in state]
if missing_json:
fail(f"missing keys in {json_file}: {', '.join(missing_json)}")
header_text = header_file.read_text(encoding="utf-8")
if "[REPO-STATE UTC:" not in header_text or "[/REPO-STATE]" not in header_text:
fail(f"invalid header markers in {header_file}")
for repo in REQUIRED_REPOS:
pattern = re.compile(rf"^{re.escape(repo)}\s+\| HEAD [0-9a-f]{{7,40}} \| pivots: .+ \| gates: .+$", re.MULTILINE)
if not pattern.search(header_text):
fail(f"missing repo line in header for {repo}")
print("[ok] repo-state header contract is valid")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+211
View File
@@ -0,0 +1,211 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<USAGE
Usage: $(basename "$0") [--header-only] [--json-only] [--no-refresh]
Options:
--header-only print only artifacts/repo_state/header.latest.md
--json-only print only artifacts/repo_state/global_index.json
--no-refresh skip running collect.py in the 3 repos
USAGE
}
HEADER_ONLY=0
JSON_ONLY=0
NO_REFRESH=0
while [[ $# -gt 0 ]]; do
case "$1" in
--header-only)
HEADER_ONLY=1
shift
;;
--json-only)
JSON_ONLY=1
shift
;;
--no-refresh)
NO_REFRESH=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "[fail] unknown option: $1" >&2
usage
exit 1
;;
esac
done
if [[ "$HEADER_ONLY" == "1" && "$JSON_ONLY" == "1" ]]; then
echo "[fail] --header-only and --json-only are mutually exclusive" >&2
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
KILL_LIFE_REPO="$(cd "$SCRIPT_DIR/../.." && pwd)"
PARENT_DIR="$(cd "$KILL_LIFE_REPO/.." && pwd)"
REPO_ROOT_KILL_LIFE="${REPO_ROOT_KILL_LIFE:-$PARENT_DIR/Kill_LIFE}"
REPO_ROOT_RTC_BL_PHONE="${REPO_ROOT_RTC_BL_PHONE:-$PARENT_DIR/RTC_BL_PHONE}"
REPO_ROOT_ZACUS="${REPO_ROOT_ZACUS:-$PARENT_DIR/le-mystere-professeur-zacus}"
ART_DIR="${REPO_STATE_ART_DIR:-$KILL_LIFE_REPO/artifacts/repo_state}"
SUMMARY_FILE="$ART_DIR/global_summary.md"
INDEX_JSON_FILE="$ART_DIR/global_index.json"
HEADER_FILE="$ART_DIR/header.latest.md"
mkdir -p "$ART_DIR"
refresh_one_repo() {
local repo_name="$1"
local repo_root="$2"
if [[ ! -d "$repo_root/.git" ]]; then
echo "[fail] missing repo for $repo_name at $repo_root" >&2
return 1
fi
local collector="$repo_root/tools/repo_state/collect.py"
if [[ -f "$collector" ]]; then
python3 "$collector" --repo-name "$repo_name" --repo-root "$repo_root"
else
python3 "$KILL_LIFE_REPO/tools/repo_state/collect.py" --repo-root "$repo_root" --repo-name "$repo_name"
fi
}
if [[ "$NO_REFRESH" != "1" ]]; then
refresh_one_repo "Kill_LIFE" "$REPO_ROOT_KILL_LIFE"
refresh_one_repo "RTC_BL_PHONE" "$REPO_ROOT_RTC_BL_PHONE"
refresh_one_repo "le-mystere-professeur-zacus" "$REPO_ROOT_ZACUS"
fi
KILL_MD="$REPO_ROOT_KILL_LIFE/docs/REPO_STATE.md"
RTC_MD="$REPO_ROOT_RTC_BL_PHONE/docs/REPO_STATE.md"
ZACUS_MD="$REPO_ROOT_ZACUS/docs/REPO_STATE.md"
python3 - "$SUMMARY_FILE" "$INDEX_JSON_FILE" "$HEADER_FILE" "$KILL_MD" "$RTC_MD" "$ZACUS_MD" <<'PY'
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
summary_file = Path(sys.argv[1])
index_json_file = Path(sys.argv[2])
header_file = Path(sys.argv[3])
md_files = [Path(p) for p in sys.argv[4:]]
required_keys = [
"Repo",
"Branch",
"HEAD",
"HeadDate",
"ProjectKind",
"PivotChanges",
"ImpactGates",
]
def parse_md(path: Path) -> dict:
if not path.exists():
raise SystemExit(f"[fail] missing file: {path}")
lines = path.read_text(encoding="utf-8").splitlines()
if not lines or lines[0].strip() != "<!-- REPO_STATE:v1 -->":
raise SystemExit(f"[fail] invalid header marker in {path}")
data = {}
for line in lines:
if ":" not in line:
continue
k, v = line.split(":", 1)
data[k.strip()] = v.strip()
missing = [k for k in required_keys if k not in data]
if missing:
raise SystemExit(f"[fail] missing keys in {path}: {', '.join(missing)}")
try:
pivots = json.loads(data["PivotChanges"])
except json.JSONDecodeError as exc:
raise SystemExit(f"[fail] invalid PivotChanges JSON in {path}: {exc}")
gates = [g.strip() for g in data["ImpactGates"].split(",") if g.strip()]
return {
"repo": data["Repo"],
"branch": data["Branch"],
"head": data["HEAD"],
"head_date": data["HeadDate"],
"project_kind": data["ProjectKind"],
"pivot_changes": pivots,
"impact_gates": gates,
"source": str(path),
}
entries = [parse_md(p) for p in md_files]
now_utc = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def pivot_text(entry: dict) -> str:
pivots = entry["pivot_changes"]
if not pivots:
return "(none)"
paths = []
for item in pivots[:2]:
p = str(item.get("path", "")).strip() or "(none)"
paths.append(p)
if len(pivots) > 2:
paths.append("...")
return "; ".join(paths)
header_lines = [f"[REPO-STATE UTC: {now_utc}]"]
for entry in entries:
head_short = entry["head"][:8]
pivots = pivot_text(entry)
gates = ", ".join(entry["impact_gates"]) if entry["impact_gates"] else "general_change"
header_lines.append(f"{entry['repo']:<28} | HEAD {head_short} | pivots: {pivots} | gates: {gates}")
header_lines.append("[/REPO-STATE]")
header_text = "\n".join(header_lines) + "\n"
header_file.write_text(header_text, encoding="utf-8")
summary_lines = ["# Global Repo State", "", f"GeneratedAtUTC: `{now_utc}`", ""]
for entry in entries:
summary_lines.append(f"## {entry['repo']}")
summary_lines.append(f"- Branch: `{entry['branch']}`")
summary_lines.append(f"- HEAD: `{entry['head']}`")
summary_lines.append(f"- HeadDate: `{entry['head_date']}`")
summary_lines.append(f"- ProjectKind: `{entry['project_kind']}`")
summary_lines.append(f"- ImpactGates: `{', '.join(entry['impact_gates'])}`")
summary_lines.append(f"- Pivots: `{pivot_text(entry)}`")
summary_lines.append("")
summary_file.write_text("\n".join(summary_lines), encoding="utf-8")
index_data = {
"schema_version": "global_repo_index.v1",
"generated_at_utc": now_utc,
"repos": entries,
}
index_json_file.write_text(json.dumps(index_data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
PY
if [[ "$HEADER_ONLY" == "1" ]]; then
cat "$HEADER_FILE"
exit 0
fi
if [[ "$JSON_ONLY" == "1" ]]; then
cat "$INDEX_JSON_FILE"
exit 0
fi
echo "[ok] wrote $SUMMARY_FILE"
echo "[ok] wrote $INDEX_JSON_FILE"
echo "[ok] wrote $HEADER_FILE"
cat "$HEADER_FILE"