feat: advance repository records and provenance

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
tegwick 2026-08-21 22:07:48 +02:00
parent 329af60753
commit 35e86d7b85
24 changed files with 1618 additions and 51 deletions

View file

@ -0,0 +1,114 @@
"""Versioned implementation of the Custodian repository-classification contract."""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Any
CONTRACT_VERSION = "1.0"
CATEGORIES = frozenset({"experimental", "research", "project", "tooling", "product", "business"})
DOMAINS = frozenset(
{
"infotech",
"financials",
"communication",
"consumer",
"health",
"industrials",
"energy",
"utilities",
"materials",
"realestate",
"crypto",
"agents",
"space",
"government",
}
)
BUSINESS_STAKE = frozenset(
{
"execution",
"intelligence",
"finance",
"legal",
"sales",
"experience",
"technology",
"operations",
"product",
"people",
"procurement",
"sustainability",
"automation",
}
)
BUSINESS_MECHANICS = frozenset({"intention", "control", "coordination", "operation", "adaptation"})
_TAG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
@dataclass(frozen=True)
class ClassificationIssue:
field: str
message: str
class ClassificationError(ValueError):
def __init__(self, issues: list[ClassificationIssue]):
self.issues = issues
super().__init__("; ".join(f"{issue.field}: {issue.message}" for issue in issues))
def _list(data: dict[str, Any], field: str, issues: list[ClassificationIssue]) -> list[str]:
value = data.get(field, [])
if value is None:
return []
if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
issues.append(ClassificationIssue(field, "must be a list of strings"))
return []
if len(value) != len(set(value)):
issues.append(ClassificationIssue(field, "must not contain duplicates"))
return value
def validate_classification(data: dict[str, Any]) -> list[ClassificationIssue]:
"""Validate required fields and controlled vocabularies from canon v1.0."""
issues: list[ClassificationIssue] = []
category = data.get("category")
domain = data.get("domain")
if category not in CATEGORIES:
issues.append(ClassificationIssue("category", f"must be one of {', '.join(sorted(CATEGORIES))}"))
if domain not in DOMAINS:
issues.append(ClassificationIssue("domain", f"must be one of {', '.join(sorted(DOMAINS))}"))
secondary = _list(data, "secondary_domains", issues)
unknown_domains = sorted(set(secondary) - DOMAINS)
if unknown_domains:
issues.append(ClassificationIssue("secondary_domains", f"unknown values: {', '.join(unknown_domains)}"))
if domain in secondary:
issues.append(ClassificationIssue("secondary_domains", "must not repeat the primary domain"))
tags = _list(data, "capability_tags", issues)
invalid_tags = sorted(tag for tag in tags if not _TAG_RE.fullmatch(tag))
if invalid_tags:
issues.append(ClassificationIssue("capability_tags", f"not lowercase kebab-case: {', '.join(invalid_tags)}"))
stake = _list(data, "business_stake", issues)
unknown_stake = sorted(set(stake) - BUSINESS_STAKE)
if unknown_stake:
issues.append(ClassificationIssue("business_stake", f"unknown values: {', '.join(unknown_stake)}"))
mechanics = _list(data, "business_mechanics", issues)
unknown_mechanics = sorted(set(mechanics) - BUSINESS_MECHANICS)
if unknown_mechanics:
issues.append(
ClassificationIssue("business_mechanics", f"unknown values: {', '.join(unknown_mechanics)}")
)
return issues
def require_valid_classification(data: dict[str, Any]) -> dict[str, Any]:
issues = validate_classification(data)
if issues:
raise ClassificationError(issues)
return data

View file

@ -183,6 +183,65 @@ def main(argv: list[str] | None = None) -> int:
register_parser.add_argument("--push", action="store_true")
register_parser.add_argument("--no-commit", action="store_true")
p_intake = sub.add_parser("intake", help="Repository-owned intake records")
intake_sub = p_intake.add_subparsers(dest="record_command")
p_intake_create = intake_sub.add_parser("create", help="Create an intake")
p_intake_create.add_argument("--path", default=".")
p_intake_create.add_argument("--record-id", required=True)
p_intake_create.add_argument("--title", required=True)
p_intake_create.add_argument("--status", default="open")
p_intake_create.add_argument("--data-json", type=_json_object, default=None)
p_intake_route = intake_sub.add_parser("route", help="Route an intake")
p_intake_route.add_argument("--path", default=".")
p_intake_route.add_argument("--record-id", required=True)
p_intake_route.add_argument("--route-to", required=True)
p_intake_note = intake_sub.add_parser("note", help="Append an intake note")
p_intake_note.add_argument("--path", default=".")
p_intake_note.add_argument("--record-id", required=True)
p_intake_note.add_argument("--note", required=True)
p_intake_note.add_argument("--author", default="codex")
p_intake_close = intake_sub.add_parser("close", help="Close an intake")
p_intake_close.add_argument("--path", default=".")
p_intake_close.add_argument("--record-id", required=True)
p_intake_close.add_argument("--outcome", default=None)
p_decision = sub.add_parser("decision", help="Repository-owned decision records")
decision_sub = p_decision.add_subparsers(dest="record_command")
p_decision_create = decision_sub.add_parser("create", help="Create a decision")
p_decision_create.add_argument("--path", default=".")
p_decision_create.add_argument("--record-id", required=True)
p_decision_create.add_argument("--title", required=True)
p_decision_create.add_argument("--status", default="open")
p_decision_create.add_argument("--data-json", type=_json_object, default=None)
p_decision_update = decision_sub.add_parser("update", help="Update a decision")
p_decision_update.add_argument("--path", default=".")
p_decision_update.add_argument("--record-id", required=True)
p_decision_update.add_argument("--title", default=None)
p_decision_update.add_argument("--status", default=None)
p_decision_update.add_argument("--data-json", type=_json_object, default=None)
p_decision_resolve = decision_sub.add_parser("resolve", help="Resolve a decision")
p_decision_resolve.add_argument("--path", default=".")
p_decision_resolve.add_argument("--record-id", required=True)
p_decision_resolve.add_argument("--rationale", required=True)
p_decision_resolve.add_argument("--decided-by", required=True)
for record_parser in (
p_intake_create,
p_intake_route,
p_intake_note,
p_intake_close,
p_decision_create,
p_decision_update,
p_decision_resolve,
):
record_parser.add_argument("--reason", default="rmgr CLI")
record_parser.add_argument("--correlation-id", default=None)
record_parser.add_argument("--idempotency-key", default=None)
record_parser.add_argument("--expected-head-sha", default=None)
record_parser.add_argument("--slug", default=None)
record_parser.add_argument("--push", action="store_true")
record_parser.add_argument("--no-commit", action="store_true")
add_rapp_parser(sub)
p_conf = sub.add_parser("conform", help="Check a repository against flavor standards")
@ -205,6 +264,29 @@ def main(argv: list[str] | None = None) -> int:
p_scaf.add_argument("--force", action="store_true")
p_scaf.add_argument("--no-commit", action="store_true")
p_provenance = sub.add_parser(
"assistant-provenance",
help="Install or report coding-assistant commit provenance",
)
provenance_sub = p_provenance.add_subparsers(dest="provenance_command")
p_prov_install = provenance_sub.add_parser("install", help="Set global core.hooksPath")
p_prov_install.add_argument(
"--hooks-path",
default=str(Path(__file__).resolve().parents[2] / ".githooks"),
)
p_prov_report = provenance_sub.add_parser("report", help="Report trailers from Git history")
p_prov_report.add_argument("--path", default=".")
p_prov_report.add_argument("--rev", default="HEAD")
p_prov_report.add_argument("--max-count", type=int, default=None)
p_identifier = sub.add_parser("identifier", help="Deterministic work-record identifiers")
identifier_sub = p_identifier.add_subparsers(dest="identifier_command")
p_id_derive = identifier_sub.add_parser("derive", help="Derive one UUIDv5")
p_id_derive.add_argument("--namespace", required=True)
p_id_derive.add_argument("--record-id", required=True)
p_id_preflight = identifier_sub.add_parser("preflight", help="Scan live identifier collisions")
p_id_preflight.add_argument("--root", default=".")
args = parser.parse_args(argv)
if args.version or args.command in (None, "version"):
@ -352,6 +434,38 @@ def main(argv: list[str] | None = None) -> int:
print(json.dumps(result.to_dict(), indent=2))
return 0 if result.status == "applied" else 1
if args.command in {"intake", "decision"}:
selected = p_intake if args.command == "intake" else p_decision
if not args.record_command:
selected.print_help()
return 2
from repo_manager.commands.record import mutate_record
result = mutate_record(
Path(args.path),
args.command,
args.record_id,
operation=args.record_command,
title=getattr(args, "title", None),
status=getattr(args, "status", None),
data=getattr(args, "data_json", None),
route_to=getattr(args, "route_to", None),
note=getattr(args, "note", None),
author=getattr(args, "author", None),
outcome=getattr(args, "outcome", None),
rationale=getattr(args, "rationale", None),
decided_by=getattr(args, "decided_by", None),
correlation_id=args.correlation_id,
reason=args.reason,
commit=not args.no_commit,
push=args.push,
expected_head_sha=args.expected_head_sha,
idempotency_key=args.idempotency_key,
repo_slug=args.slug,
)
print(json.dumps(result.to_dict(), indent=2))
return 0 if result.status == "applied" else 1
if args.command == "rapp":
if args.rapp_command == "init":
result = rapp_init(
@ -438,6 +552,42 @@ def main(argv: list[str] | None = None) -> int:
print(json.dumps(result.to_dict(), indent=2))
return 0 if result.status == "applied" else 1
if args.command == "assistant-provenance":
if not args.provenance_command:
p_provenance.print_help()
return 2
from repo_manager.provenance import assistant_report, install_hook
if args.provenance_command == "install":
result = install_hook(Path(args.hooks_path))
else:
result = assistant_report(Path(args.path), rev=args.rev, max_count=args.max_count)
print(json.dumps(result, indent=2))
return 0 if result.get("ok") else 1
if args.command == "identifier":
if not args.identifier_command:
p_identifier.print_help()
return 2
from repo_manager.identifiers import derive_work_record_uuid, scan_live_identifier_collisions
if args.identifier_command == "derive":
try:
derived = derive_work_record_uuid(args.namespace, args.record_id)
except ValueError as exc:
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
return 1
result = {
"ok": True,
"namespace": args.namespace,
"record_id": args.record_id,
"uuid": str(derived),
}
else:
result = scan_live_identifier_collisions(Path(args.root))
print(json.dumps(result, indent=2))
return 0 if result.get("ok") else 1
parser.print_help()
return 0

View file

@ -0,0 +1,313 @@
"""Governed mutation commands for repository-owned intake and decision records."""
from __future__ import annotations
import re
import uuid
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import yaml
from repo_manager import dual_run, idempotency
from repo_manager.commands.workplan import CommandResult
from repo_manager.gitops import GitError, commit_paths, head_sha, push_ff
from repo_manager.index_store import append_event, default_index_path, save_index
from repo_manager.observe import observe_repository
from repo_manager.parse.record import iter_record_files, parse_record_file
_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
_PROTECTED = frozenset(
{
"id",
"kind",
"title",
"status",
"notes",
"created",
"updated",
"routed_at",
"closed_at",
"decided_at",
}
)
_OPERATIONS = {
"intake": frozenset({"create", "route", "note", "close"}),
"decision": frozenset({"create", "update", "resolve"}),
}
def _now() -> str:
return datetime.now(UTC).isoformat()
def _reject(command: str, correlation_id: str, code: str, message: str, **evidence: Any) -> CommandResult:
return CommandResult(
command=command,
status="rejected",
correlation_id=correlation_id,
evidence={"status": "rejected", **evidence},
error={"code": code, "message": message, "correlation_id": correlation_id},
)
def _command(kind: str, operation: str) -> str:
verbs = {
("intake", "create"): "repo.work.create_intake",
("intake", "route"): "repo.work.route_intake",
("intake", "note"): "repo.work.add_intake_note",
("intake", "close"): "repo.work.close_intake",
("decision", "create"): "repo.work.create_decision",
("decision", "update"): "repo.work.update_decision",
("decision", "resolve"): "repo.work.resolve_decision",
}
return verbs.get((kind, operation), f"repo.work.{operation}_{kind}")
def _default_path(repo_root: Path, kind: str) -> Path:
return repo_root / f"{kind}s" / f"{kind}s.md"
def _find(repo_root: Path, kind: str, record_id: str) -> list[tuple[Path, Any]]:
found: list[tuple[Path, Any]] = []
for path in iter_record_files(repo_root):
for record in parse_record_file(path, repo_root=repo_root):
if record.kind == kind and (record.id == record_id or record.uuid == record_id):
found.append((path, record))
return found
def _dump_block(record: dict[str, Any]) -> str:
return "```yaml\n" + yaml.safe_dump(record, sort_keys=False, allow_unicode=True).rstrip() + "\n```"
def _write_new(path: Path, kind: str, record: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
previous = path.read_text(encoding="utf-8") if path.is_file() else f"# {kind.title()} records\n"
text = previous.rstrip() + f"\n\n## {record['id']}{record['title']}\n\n" + _dump_block(record) + "\n"
path.write_text(text, encoding="utf-8")
def _replace(path: Path, record: Any, data: dict[str, Any]) -> None:
text = path.read_text(encoding="utf-8")
updated = text[: record.block_start] + _dump_block(data) + text[record.block_end :]
path.write_text(updated, encoding="utf-8")
def mutate_record(
repo_root: Path,
kind: str,
record_id: str,
*,
operation: str,
title: str | None = None,
status: str | None = None,
data: dict[str, Any] | None = None,
route_to: str | None = None,
note: str | None = None,
author: str | None = None,
outcome: str | None = None,
rationale: str | None = None,
decided_by: str | None = None,
correlation_id: str | None = None,
reason: str = "rmgr command",
commit: bool = True,
push: bool = False,
expected_head_sha: str | None = None,
idempotency_key: str | None = None,
repo_slug: str | None = None,
) -> CommandResult:
"""Mutate one intake or decision YAML block in its authoritative Markdown file."""
correlation_id = correlation_id or str(uuid.uuid4())
command = _command(kind, operation)
repo_root = repo_root.resolve()
payload = {
"repo_root": str(repo_root),
"kind": kind,
"record_id": record_id,
"operation": operation,
"title": title,
"status": status,
"data": data,
"route_to": route_to,
"note": note,
"author": author,
"outcome": outcome,
"rationale": rationale,
"decided_by": decided_by,
"expected_head_sha": expected_head_sha,
}
digest = idempotency.payload_hash(payload)
if idempotency_key:
prior = idempotency.get(idempotency_key)
if prior:
if prior.get("payload_hash") != digest:
return _reject(command, correlation_id, "conflict", "idempotency key reused")
previous = prior.get("result") or {}
return CommandResult(
command=previous.get("command", command),
status=previous.get("status", "applied"),
correlation_id=previous.get("correlation_id", correlation_id),
evidence=previous.get("evidence") or {"status": "applied", "replay": True},
error=previous.get("error"),
)
if kind not in _OPERATIONS or operation not in _OPERATIONS[kind]:
return _reject(command, correlation_id, "validation_error", f"invalid {kind!r} operation {operation!r}")
if not _ID_RE.fullmatch(record_id):
return _reject(command, correlation_id, "validation_error", f"invalid record id {record_id!r}")
if data is not None and not isinstance(data, dict):
return _reject(command, correlation_id, "validation_error", "data must be an object")
protected = _PROTECTED.intersection(data or {})
if protected:
return _reject(
command,
correlation_id,
"validation_error",
f"data contains protected fields: {', '.join(sorted(protected))}",
)
current_head = head_sha(repo_root)
if expected_head_sha and current_head and current_head != expected_head_sha:
result = _reject(
command,
correlation_id,
"precondition_failed",
f"expected_head_sha {expected_head_sha} != {current_head}",
head_sha=current_head,
)
if result.error:
result.error["retryable"] = True
return result
matches = _find(repo_root, kind, record_id)
if len(matches) > 1:
return _reject(command, correlation_id, "conflict", f"record {record_id!r} is duplicated")
match = matches[0] if matches else None
now = _now()
if operation == "create":
if match:
return _reject(command, correlation_id, "conflict", f"record {record_id!r} exists")
if not title or not title.strip():
return _reject(command, correlation_id, "validation_error", "create requires title")
record_data: dict[str, Any] = {
"id": record_id,
"kind": kind,
"title": title.strip(),
"status": status or "open",
**(data or {}),
"created": now,
"updated": now,
}
path = _default_path(repo_root, kind)
_write_new(path, kind, record_data)
else:
if not match:
return _reject(command, correlation_id, "not_found", f"record {record_id!r} not found")
path, parsed = match
record_data = dict(parsed.raw)
if kind == "intake" and operation == "route":
if not route_to or not route_to.strip():
return _reject(command, correlation_id, "validation_error", "route requires route_to")
record_data.update(status="routed", routed_to=route_to.strip(), routed_at=now)
elif kind == "intake" and operation == "note":
if not note or not note.strip():
return _reject(command, correlation_id, "validation_error", "note requires content")
record_data.setdefault("notes", []).append(
{"content": note.strip(), "author": author or "codex", "created": now}
)
elif kind == "intake" and operation == "close":
record_data.update(status="closed", closed_at=now)
if outcome:
record_data["outcome"] = outcome.strip()
elif kind == "decision" and operation == "update":
if title is None and status is None and not data:
return _reject(command, correlation_id, "validation_error", "update requires a field")
if title is not None:
if not title.strip():
return _reject(command, correlation_id, "validation_error", "title cannot be empty")
record_data["title"] = title.strip()
if status is not None:
if not status.strip():
return _reject(command, correlation_id, "validation_error", "status cannot be empty")
record_data["status"] = status.strip()
record_data.update(data or {})
else:
if not rationale or not rationale.strip() or not decided_by or not decided_by.strip():
return _reject(
command,
correlation_id,
"validation_error",
"resolve requires rationale and decided_by",
)
record_data.update(
status="resolved",
rationale=rationale.strip(),
decided_by=decided_by.strip(),
decided_at=now,
)
record_data["updated"] = now
_replace(path, parsed, record_data)
relative_path = str(path.relative_to(repo_root))
git_sha: str | None = None
push_ok: bool | None = None
push_message: str | None = None
if commit:
try:
git_sha = commit_paths(
repo_root,
[relative_path],
message=(
f"{command} {record_id}\n\n"
f"correlation_id: {correlation_id}\nreason: {reason}\nsource: repo-manager\n"
),
)
except GitError as exc:
return CommandResult(
command=command,
status="failed",
correlation_id=correlation_id,
evidence={"status": "failed", "files_touched": [relative_path], "git_sha": None},
error={"code": "internal", "message": str(exc), "correlation_id": correlation_id},
)
if push:
push_ok, push_message = push_ff(repo_root)
snapshot, index = observe_repository(repo_root, slug=repo_slug)
append_event(
index,
{
"type": "repo.command.applied",
"command": command,
"operation": operation,
"correlation_id": correlation_id,
"kind": kind,
"id": record_id,
"git_sha": git_sha,
"files_touched": [relative_path],
"source": "repo-manager",
},
)
save_index(index)
dual_run.record_mutation(
source="repo-manager",
kind=f"{kind}_{operation}",
repo_slug=repo_slug or snapshot.get("slug"),
detail={"id": record_id, "git_sha": git_sha, "correlation_id": correlation_id},
)
evidence: dict[str, Any] = {
"status": "applied",
"git_sha": git_sha,
"files_touched": [relative_path],
"index_path": str(default_index_path(repo_root)),
"source": "repo-manager",
}
if push:
evidence.update(push_ok=push_ok, push_message=push_message)
result = CommandResult(command=command, status="applied", correlation_id=correlation_id, evidence=evidence)
if idempotency_key:
idempotency.put(idempotency_key, digest, result.to_dict())
return result

View file

@ -187,4 +187,6 @@ def scaffold_repository(
except GitError as exc:
return CommandResult("failed", evidence, {"message": str(exc)}, cid)
return CommandResult("applied" if written else "rejected", evidence, None, cid)
if not written:
evidence["noop"] = True
return CommandResult("applied", evidence, None, cid)

View file

@ -0,0 +1,68 @@
"""Deterministic work-record identifiers and live-collision preflight."""
from __future__ import annotations
import re
import uuid
from collections import defaultdict
from pathlib import Path
from typing import Any
from repo_manager.parse.workplan import iter_workplan_files, parse_workplan_file
from repo_manager.prefix_registry import iter_repo_roots
DERIVATION_NAMESPACE_UUID = uuid.UUID("a4058507-5c4a-5a00-ab06-fffa4fb46009")
DERIVATION_VERSION = "repo-manager.work-record-uuid.v1"
LIVE_WORKPLAN_STATUSES = frozenset({"proposed", "ready", "active", "blocked", "backlog"})
_NAMESPACE_RE = re.compile(r"^[a-z0-9][a-z0-9.-]{0,62}$")
_RECORD_ID_RE = re.compile(r"^[A-Z][A-Z0-9-]*-WP-[0-9]{4}(?:-T[0-9]{2,})?$")
def derivation_name(namespace: str, identifier: str) -> str:
"""Return the exact UTF-8 UUIDv5 name input defined by contract v1."""
namespace = namespace.strip()
identifier = identifier.strip()
if not _NAMESPACE_RE.fullmatch(namespace):
raise ValueError("namespace must be lowercase DNS-label style")
if not _RECORD_ID_RE.fullmatch(identifier):
raise ValueError("identifier must be a canonical workplan or task id")
return f"{namespace}\n{identifier}"
def derive_work_record_uuid(namespace: str, identifier: str) -> uuid.UUID:
return uuid.uuid5(DERIVATION_NAMESPACE_UUID, derivation_name(namespace, identifier))
def scan_live_identifier_collisions(root: Path) -> dict[str, Any]:
"""Report live workplan/task identifiers that cannot safely be derived."""
by_id: dict[str, list[dict[str, str]]] = defaultdict(list)
repos = iter_repo_roots(root)
for repo in repos:
for path in iter_workplan_files(repo):
parsed = parse_workplan_file(path, repo_root=repo)
if parsed.status not in LIVE_WORKPLAN_STATUSES:
continue
if parsed.id:
by_id[parsed.id].append(
{"repo": repo.name, "path": parsed.path, "kind": "workplan", "status": parsed.status}
)
for task in parsed.tasks:
if task.id and task.status not in {"done", "cancel"}:
by_id[task.id].append(
{
"repo": repo.name,
"path": parsed.path,
"kind": "task",
"status": task.status or "unknown",
}
)
collisions = {identifier: entries for identifier, entries in sorted(by_id.items()) if len(entries) > 1}
return {
"ok": not collisions,
"root": str(root.resolve()),
"repos_scanned": [repo.name for repo in repos],
"live_identifiers": len(by_id),
"collisions": collisions,
"safe_to_derive": not collisions,
}

View file

@ -7,8 +7,10 @@ from pathlib import Path
import yaml
from repo_manager.classification import require_valid_classification
from repo_manager.gitops import head_sha, is_git_repo
from repo_manager.index_store import RepoIndex, WorkRecordEntry, _now
from repo_manager.parse.record import iter_record_files, parse_record_file
from repo_manager.parse.register import iter_register_files, parse_register_file
from repo_manager.parse.workplan import iter_workplan_files, parse_workplan_file
@ -23,8 +25,12 @@ def load_classification(repo_root: Path) -> dict | None:
return None
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if isinstance(data, dict) and "repo_classification" in data:
return data["repo_classification"]
return data if isinstance(data, dict) else None
classification = data["repo_classification"]
else:
classification = data if isinstance(data, dict) else None
if not isinstance(classification, dict):
return None
return require_valid_classification(classification)
def observe_repository(repo_root: Path, *, slug: str | None = None) -> tuple[dict, RepoIndex]:
@ -58,6 +64,11 @@ def observe_repository(repo_root: Path, *, slug: str | None = None) -> tuple[dic
title=wp.title,
source_path=wp.path,
uuid=wp.state_hub_workstream_id,
extra={
key: wp.frontmatter[key]
for key in ("depends_on", "related", "needs_human", "intervention_note")
if key in wp.frontmatter
},
)
)
for task in wp.tasks:
@ -70,6 +81,25 @@ def observe_repository(repo_root: Path, *, slug: str | None = None) -> tuple[dic
source_path=wp.path,
uuid=task.state_hub_task_id,
parent_id=wp.id,
extra={
key: task.raw[key]
for key in ("depends_on", "needs_human", "intervention_note", "blocking_reason")
if key in task.raw
},
)
)
for path in iter_record_files(repo_root):
for record in parse_record_file(path, repo_root=repo_root):
records.append(
WorkRecordEntry(
kind=record.kind,
id=record.id,
status=record.status,
title=record.title,
source_path=record.source_path,
uuid=record.uuid,
extra={"record": record.raw},
)
)
@ -116,6 +146,8 @@ def observe_repository(repo_root: Path, *, slug: str | None = None) -> tuple[dic
"workplan_count": sum(1 for r in records if r.kind == "workplan"),
"task_count": sum(1 for r in records if r.kind == "task"),
"register_entry_count": sum(1 for r in records if r.kind.startswith("register:")),
"intake_count": sum(1 for r in records if r.kind == "intake"),
"decision_count": sum(1 for r in records if r.kind == "decision"),
"record_count": len(records),
},
}

View file

@ -0,0 +1,78 @@
"""Parse repository-owned intake and decision records from Markdown YAML fences."""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
import yaml
_YAML_FENCE_RE = re.compile(r"```ya?ml\s*\n(.*?)\n```", re.DOTALL | re.IGNORECASE)
SUPPORTED_RECORD_KINDS = frozenset({"intake", "decision"})
@dataclass
class ParsedRecord:
kind: str
id: str
title: str | None
status: str | None
uuid: str | None
source_path: str
raw: dict[str, Any] = field(default_factory=dict)
block_start: int = 0
block_end: int = 0
def parse_record_text(text: str, *, relative_path: str) -> list[ParsedRecord]:
"""Return intake/decision records embedded in YAML fenced blocks."""
records: list[ParsedRecord] = []
for match in _YAML_FENCE_RE.finditer(text):
try:
data = yaml.safe_load(match.group(1)) or {}
except yaml.YAMLError:
continue
if not isinstance(data, dict) or data.get("kind") not in SUPPORTED_RECORD_KINDS:
continue
record_id = data.get("id")
if record_id is None:
continue
kind = str(data["kind"])
uuid_field = "state_hub_intake_id" if kind == "intake" else "state_hub_decision_id"
records.append(
ParsedRecord(
kind=kind,
id=str(record_id),
title=str(data["title"]) if data.get("title") is not None else None,
status=str(data["status"]) if data.get("status") is not None else None,
uuid=str(data[uuid_field]) if data.get(uuid_field) is not None else None,
source_path=relative_path,
raw=data,
block_start=match.start(),
block_end=match.end(),
)
)
return records
def parse_record_file(path: Path, *, repo_root: Path) -> list[ParsedRecord]:
return parse_record_text(
path.read_text(encoding="utf-8"),
relative_path=str(path.relative_to(repo_root)),
)
def iter_record_files(repo_root: Path) -> list[Path]:
"""Find only the governed record locations; do not interpret arbitrary docs."""
files: set[Path] = set()
for relative in ("intakes", "decisions", "docs/intakes", "docs/decisions", "workplans"):
root = repo_root / relative
if root.is_dir():
files.update(path for path in root.rglob("*.md") if not path.name.startswith("."))
for name in ("INTAKES.md", "DECISIONS.md"):
path = repo_root / name
if path.is_file():
files.add(path)
return sorted(files)

View file

@ -0,0 +1,135 @@
"""Install and report on coding-assistant commit provenance."""
from __future__ import annotations
import re
import subprocess
from collections import defaultdict
from pathlib import Path
from typing import Any
import yaml
_TRAILER_RE = re.compile(r"^(Assistant(?:-Model|-Process|-Session)?):\s*(.+)$", re.MULTILINE)
def install_hook(hooks_path: Path) -> dict[str, Any]:
"""Configure the user's Git installation to use the governed hook directory."""
hooks_path = hooks_path.expanduser().resolve()
hook = hooks_path / "prepare-commit-msg"
if not hook.is_file():
return {"ok": False, "error": f"missing hook: {hook}"}
if not hook.stat().st_mode & 0o111:
return {"ok": False, "error": f"hook is not executable: {hook}"}
completed = subprocess.run(
["git", "config", "--global", "core.hooksPath", str(hooks_path)],
check=False,
capture_output=True,
text=True,
)
if completed.returncode:
return {"ok": False, "error": completed.stderr.strip() or "git config failed"}
return {"ok": True, "hooks_path": str(hooks_path), "hook": str(hook)}
def _cutover(repo_root: Path) -> str | None:
path = repo_root / "config" / "assistant-provenance.yaml"
if not path.is_file():
return None
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
value = data.get("cutover_commit") if isinstance(data, dict) else None
return str(value) if value else None
def _commits(repo_root: Path, rev: str, max_count: int | None) -> list[dict[str, Any]]:
command = ["git", "log", "--reverse", "--format=%H%x1f%aI%x1f%an%x1f%ae%x1f%B%x1e"]
if max_count:
command.append(f"--max-count={max_count}")
command.append(rev)
completed = subprocess.run(command, cwd=repo_root, check=True, capture_output=True, text=True)
commits: list[dict[str, Any]] = []
for raw in completed.stdout.split("\x1e"):
raw = raw.strip("\n")
if not raw:
continue
parts = raw.split("\x1f", 4)
if len(parts) != 5:
continue
sha, authored_at, author, email, body = parts
trailers = {key: value.strip() for key, value in _TRAILER_RE.findall(body)}
commits.append(
{
"sha": sha,
"authored_at": authored_at,
"author": author,
"email": email,
"assistant": trailers.get("Assistant"),
"model": trailers.get("Assistant-Model"),
"process": trailers.get("Assistant-Process"),
"session": trailers.get("Assistant-Session"),
}
)
return commits
def assistant_report(repo_root: Path, *, rev: str = "HEAD", max_count: int | None = None) -> dict[str, Any]:
"""Derive assistant activity and interleaved-session signals from Git alone."""
repo_root = repo_root.resolve()
commits = _commits(repo_root, rev, max_count)
cutover = _cutover(repo_root)
cutover_seen = False
assistants: dict[str, dict[str, Any]] = defaultdict(
lambda: {"commit_count": 0, "models": set(), "sessions": set(), "processes": set()}
)
unattributed = {"before_cutover": 0, "after_cutover": 0, "known_automation": 0}
session_sequence: list[str] = []
for commit in commits:
if cutover and commit["sha"].startswith(cutover):
cutover_seen = True
assistant = commit["assistant"]
if assistant:
entry = assistants[assistant]
entry["commit_count"] += 1
for field, bucket in (("model", "models"), ("session", "sessions"), ("process", "processes")):
if commit[field]:
entry[bucket].add(commit[field])
if commit["session"]:
session_sequence.append(commit["session"])
elif commit["author"] == "custodian-sync" or commit["email"] == "custodian-sync@railiance.local":
unattributed["known_automation"] += 1
elif cutover_seen:
unattributed["after_cutover"] += 1
else:
unattributed["before_cutover"] += 1
interleaved: set[tuple[str, str]] = set()
positions: dict[str, list[int]] = defaultdict(list)
for position, session in enumerate(session_sequence):
positions[session].append(position)
sessions = sorted(positions)
for index, first in enumerate(sessions):
for second in sessions[index + 1 :]:
merged = [session_sequence[pos] for pos in sorted(positions[first] + positions[second])]
compressed = [value for pos, value in enumerate(merged) if pos == 0 or value != merged[pos - 1]]
if len(compressed) >= 3:
interleaved.add((first, second))
return {
"ok": True,
"repo": str(repo_root),
"revision": rev,
"cutover_commit": cutover,
"commit_count": len(commits),
"assistants": {
name: {
"commit_count": item["commit_count"],
"models": sorted(item["models"]),
"sessions": sorted(item["sessions"]),
"processes": sorted(item["processes"]),
}
for name, item in sorted(assistants.items())
},
"interleaved_sessions": [list(pair) for pair in sorted(interleaved)],
"unattributed": unattributed,
}