Expose strict brief rendering without implicit Git operations

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a15374-f59f-7a31-86d2-d23b5d2ed181
This commit is contained in:
tegwick 2026-10-19 13:30:59 +02:00
parent 200cbd0a61
commit 09803dcc31
9 changed files with 199 additions and 26 deletions

View file

@ -2,18 +2,19 @@
# Custodian Brief — state-hub
**Domain:** infotech
**Last synced:** 2026-10-13 21:26 UTC
**Last synced:** 2026-10-14 00:49 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### Supply State Hub inputs that hub-core receipt tasks wait on
Progress: 2/5 done | workplan_id: `0c68970e-295b-57fc-ac70-e472ee3a7bc4`
Progress: 3/5 done | workplan_id: `0c68970e-295b-57fc-ac70-e472ee3a7bc4`
**Open tasks:**
- ► A4 work/repo UI caller inventory and owner asks `e6ef3381`
- · Named single reader and parity window `077797c8`
- · Implement the inbox change feed `14c66646`
- ! A4 work/repo UI caller inventory and owner asks `e6ef3381`
*(wait: Owner map delivered and switch-plan requests sent 2026-10-13 to hub-core (5e2f749b), activity-core (13cff5ff), repo-manager (0c49c989), sbom-nexus (63ce4ead) and policy-nexus (d5dda84d); waiting for their read smokes and reversible switch plans.)*
- ! Named single reader and parity window `077797c8`
*(wait: The first reader is chosen once hub-core implements the accepted feed contract (HUB-WP-0011-T01) and the feed is deployed. Per D3, choose a reader that does not rely on broadcast delivery stamps.)*
### Route human gates and expose actionable owner returns
Progress: 3/5 done | workplan_id: `2c4e96a3-ca0d-5003-b759-880976600ecc`
@ -22,7 +23,7 @@ Progress: 3/5 done | workplan_id: `2c4e96a3-ca0d-5003-b759-880976600ecc`
- ! Integrate deduplicated owner wakeups with the supported runner `cd73e265`
*(wait: Memo 2 plan-only and submit trials wait for a live owner return; C-25 found zero findings across 13 public repos on 2026-10-04)*
- ! Prepare approved receiving promotion and assess graph freshness (P6) `ec0acd06`
*(wait: Memo 3 apply trials need a read-only Forgejo token for the Activity Core worker (draft CCR-2026-0034 in railiance-platform); then memo 4 schedule and 48 h freshness evidence)*
*(wait: Memo 3 apply trials need a read-only Forgejo token for the Activity Core worker; then memo 4 schedule and 48 h freshness evidence. 2026-10-14: CCR-2026-0034 in railiance-platform is the keycape operator reader (applied), not this token; no Forgejo-read CCR for the Activity Core worker found.)*
### State Hub retirement strangler and disposition execution
Progress: 6/9 done | workplan_id: `ed077b62-7048-5752-bf65-f90471f45854`

View file

@ -515,3 +515,9 @@ register-from-classification-all:
clean:
$(COMPOSE) down -v
# Explicit read-model brief rendering; never stages or commits files.
.PHONY: brief-generate brief-check
brief-generate brief-check:
@test -n "$(BRIEF_REPO_PATH)" -a -n "$(BRIEF_REPO_SLUG)" || { echo 'Set BRIEF_REPO_PATH and BRIEF_REPO_SLUG'; exit 1; }
$(PYTHON) scripts/generate_custodian_brief.py --repo-path "$(BRIEF_REPO_PATH)" --repo-slug "$(BRIEF_REPO_SLUG)" --api-base "$(STATE_HUB_API_BASE)" $(if $(filter brief-check,$@),--check,)

View file

@ -14,6 +14,9 @@ repository files remain authoritative for repository-owned work records.
- FastMCP server and tool reference
- Observable dashboard
- repo registration (classification-driven) and consistency synchronization
- read-model orientation brief rendering without Git operations (`make brief-generate` /
`brief-check`, explicit repo path and slug); rejects unavailable or malformed reads
and preserves existing briefs on failure. See `docs/brief-generation.md`.
- repo classification spine (14 market domains, `.repo-classification.yaml`)
- task-flow engine and flow definitions
- SBOM, contribution, capability, demand-weighted suggestion backlog, TPSC, DoI,

View file

@ -17,6 +17,7 @@
| workplan | STATE-WP-ADHOC-2026-09-22 | finished | — | workplans/ADHOC-2026-09-22.md |
| workplan | STATE-WP-ADHOC-2026-10-06 | finished | — | workplans/ADHOC-2026-10-06.md |
| workplan | STATE-WP-ADHOC-2026-10-13 | finished | — | workplans/ADHOC-2026-10-13.md |
| workplan | STATE-WP-ADHOC-2026-10-19 | finished | — | workplans/ADHOC-2026-10-19.md |
| workplan | CUST-WP-0003 | finished | — | workplans/CUST-WP-0003-whi-kpi-card.md |
| workplan | CUST-WP-0012 | finished | — | workplans/CUST-WP-0012-multi-user-onboarding.md |
| workplan | CUST-WP-0038 | archived | — | workplans/CUST-WP-0038-state-hub-threephoenix-ha.md |
@ -73,7 +74,7 @@
| workplan | STATE-WP-0095 | finished | — | workplans/STATE-WP-0095-historical-source-receiving.md |
| workplan | STATE-WP-0096 | finished | — | workplans/STATE-WP-0096-private-decision-dispatch.md |
| workplan | STATE-WP-0097 | finished | — | workplans/STATE-WP-0097-unlinked-decision-supersession.md |
| workplan | STATE-WP-0098 | active | — | workplans/STATE-WP-0098-hub-core-receipt-inputs.md |
| workplan | STATE-WP-0098 | blocked | — | workplans/STATE-WP-0098-hub-core-receipt-inputs.md |
| task | STATE-WP-ADHOC-2026-06-04-T01 | done | — | workplans/ADHOC-2026-06-04.md |
| task | STATE-WP-ADHOC-2026-07-01-T01 | done | — | workplans/ADHOC-2026-07-01.md |
| task | STATE-WP-ADHOC-2026-07-01-T02 | done | — | workplans/ADHOC-2026-07-01.md |
@ -90,6 +91,7 @@
| task | STATE-WP-ADHOC-2026-10-06-T02 | done | — | workplans/ADHOC-2026-10-06.md |
| task | STATE-WP-ADHOC-2026-10-06-T03 | done | — | workplans/ADHOC-2026-10-06.md |
| task | STATE-WP-ADHOC-2026-10-13-T01 | done | — | workplans/ADHOC-2026-10-13.md |
| task | STATE-WP-ADHOC-2026-10-19-T01 | done | — | workplans/ADHOC-2026-10-19.md |
| task | CUST-WP-0003-T01 | done | — | workplans/CUST-WP-0003-whi-kpi-card.md |
| task | CUST-WP-0003-T02 | done | — | workplans/CUST-WP-0003-whi-kpi-card.md |
| task | CUST-WP-0003-T03 | done | — | workplans/CUST-WP-0003-whi-kpi-card.md |
@ -394,13 +396,19 @@
| task | STATE-WP-0095-T02 | done | — | workplans/STATE-WP-0095-historical-source-receiving.md |
| task | STATE-WP-0095-T03 | done | — | workplans/STATE-WP-0095-historical-source-receiving.md |
| task | STATE-WP-0095-T04 | done | — | workplans/STATE-WP-0095-historical-source-receiving.md |
| task | STATE-WP-0095-T05 | done | — | workplans/STATE-WP-0095-historical-source-receiving.md |
| task | STATE-WP-0095-T06 | done | — | workplans/STATE-WP-0095-historical-source-receiving.md |
| task | STATE-WP-0095-T07 | done | — | workplans/STATE-WP-0095-historical-source-receiving.md |
| task | STATE-WP-0095-T08 | done | — | workplans/STATE-WP-0095-historical-source-receiving.md |
| task | STATE-WP-0095-T09 | done | — | workplans/STATE-WP-0095-historical-source-receiving.md |
| task | STATE-WP-0095-T10 | done | — | workplans/STATE-WP-0095-historical-source-receiving.md |
| task | STATE-WP-0096-T01 | done | — | workplans/STATE-WP-0096-private-decision-dispatch.md |
| task | STATE-WP-0096-T02 | done | — | workplans/STATE-WP-0096-private-decision-dispatch.md |
| task | STATE-WP-0096-T03 | done | — | workplans/STATE-WP-0096-private-decision-dispatch.md |
| task | STATE-WP-0097-T01 | done | — | workplans/STATE-WP-0097-unlinked-decision-supersession.md |
| task | STATE-WP-0097-T02 | done | — | workplans/STATE-WP-0097-unlinked-decision-supersession.md |
| task | STATE-WP-0098-T01 | done | — | workplans/STATE-WP-0098-hub-core-receipt-inputs.md |
| task | STATE-WP-0098-T02 | progress | — | workplans/STATE-WP-0098-hub-core-receipt-inputs.md |
| task | STATE-WP-0098-T02 | wait | — | workplans/STATE-WP-0098-hub-core-receipt-inputs.md |
| task | STATE-WP-0098-T03 | done | — | workplans/STATE-WP-0098-hub-core-receipt-inputs.md |
| task | STATE-WP-0098-T04 | todo | — | workplans/STATE-WP-0098-hub-core-receipt-inputs.md |
| task | STATE-WP-0098-T05 | todo | — | workplans/STATE-WP-0098-hub-core-receipt-inputs.md |
| task | STATE-WP-0098-T04 | wait | — | workplans/STATE-WP-0098-hub-core-receipt-inputs.md |
| task | STATE-WP-0098-T05 | done | — | workplans/STATE-WP-0098-hub-core-receipt-inputs.md |

24
docs/brief-generation.md Normal file
View file

@ -0,0 +1,24 @@
# Render orientation briefs without Git operations
Use the existing State Hub renderer independently of consistency writeback:
```sh
make brief-generate BRIEF_REPO_PATH=/path/to/repo BRIEF_REPO_SLUG=canonical-slug
make brief-check BRIEF_REPO_PATH=/path/to/repo BRIEF_REPO_SLUG=canonical-slug
```
The Python entry point is `scripts/generate_custodian_brief.py`; pass `--api-base`
or set Make's `STATE_HUB_API_BASE` for another endpoint. It performs only API reads
and writes `.custodian-brief.md` in generation mode. It never stages, commits,
pushes, or writes to the hub. Check mode exits nonzero on drift without writing.
Timestamp-only changes are ignored.
The brief describes the current hub read model, not the Git index. Sync authored
work records before expecting their changes in the brief. API requests are not
an atomic snapshot; inspect output if concurrent work is changing the hub.
Missing or malformed endpoint responses stop generation before writing. Symlink
targets are refused. Preserve any existing local changes before refreshing.
Review and stage the brief alongside the substantive source change that needs it.
The generated-only commit guard remains in force. This command does not install
an automatic brief hook or change existing consistency writeback defaults.

View file

@ -2847,8 +2847,9 @@ def _summarise_inbox_message(msg: dict[str, Any], *, now: datetime) -> dict[str,
}
def collect_inbox_hygiene(api_base: str, repo_slug: str) -> dict[str, Any]:
messages = _api_get(
def collect_inbox_hygiene(api_base: str, repo_slug: str, *, reader=None) -> dict[str, Any]:
reader = reader or _api_get
messages = reader(
api_base,
"/messages",
{"to_agent": repo_slug, "unread_only": True, "limit": 100},
@ -3392,7 +3393,19 @@ def _commit_working_memory_notes(repo_slug: str, repo_path: str) -> bool:
return False
def _write_custodian_brief(api_base: str, repo_slug: str, repo_path: str) -> bool:
def _strict_brief_get(api_base: str, path: str, params=None):
"""Never turn an incomplete read-model snapshot into an empty brief."""
value = _api_get(api_base, path, params, return_error=True)
singular = path.startswith(("/repos/", "/topics/"))
valid = (isinstance(value, dict) and bool(value) and "_error" not in value
if singular else isinstance(value, list) and all(isinstance(row, dict) for row in value))
if not valid:
raise ValueError("brief read model unavailable or malformed at " + path)
return value
def _write_custodian_brief(api_base: str, repo_slug: str, repo_path: str, *,
commit: bool = True, strict: bool = False, check: bool = False) -> bool:
"""Generate .custodian-brief.md at the repo root and git-commit if changed.
The brief gives any agent — including subagents without MCP access and
@ -3402,7 +3415,8 @@ def _write_custodian_brief(api_base: str, repo_slug: str, repo_path: str) -> boo
import datetime as _dt
from datetime import timezone as _tz
repo = _api_get(api_base, f"/repos/{repo_slug}")
reader = _strict_brief_get if strict else _api_get
repo = reader(api_base, f"/repos/{repo_slug}")
if not repo:
return False
@ -3413,21 +3427,21 @@ def _write_custodian_brief(api_base: str, repo_slug: str, repo_path: str) -> boo
# lookup only for compatibility with older API responses that omitted it.
workstreams: list[dict] = []
for status in OPEN_WORKSTREAM_STATUSES:
rows = _api_get(api_base, "/workplans", {"repo_id": repo_id, "status": status}) or []
rows = reader(api_base, "/workplans", {"repo_id": repo_id, "status": status}) or []
if isinstance(rows, list):
workstreams.extend(rows)
_ws_for_domain = workstreams if workstreams else []
if not _ws_for_domain:
all_ws = _api_get(api_base, "/workplans", {"repo_id": repo_id}) or []
all_ws = reader(api_base, "/workplans", {"repo_id": repo_id}) or []
_ws_for_domain = all_ws if isinstance(all_ws, list) else []
if not domain_slug and _ws_for_domain:
topic = _api_get(api_base, f"/topics/{_ws_for_domain[0].get('topic_id', '')}")
topic = reader(api_base, f"/topics/{_ws_for_domain[0].get('topic_id', '')}")
if topic:
domain_slug = topic.get("domain_slug", "")
# Active repo goal (first active one if multiple)
goal_text = ""
goals = _api_get(api_base, "/repo-goals", {"repo_slug": repo_slug}) or []
goals = reader(api_base, "/repo-goals", {"repo_slug": repo_slug}) or []
if isinstance(goals, list):
active_goals = [g for g in goals if g.get("status") == "active"]
if active_goals:
@ -3455,7 +3469,7 @@ def _write_custodian_brief(api_base: str, repo_slug: str, repo_path: str) -> boo
for ws in workstreams:
ws_title = ws.get("title", ws.get("slug", "?"))
ws_id = ws["id"]
tasks = _api_get(api_base, "/tasks", {"workplan_id": ws_id}) or []
tasks = reader(api_base, "/tasks", {"workplan_id": ws_id}) or []
if not isinstance(tasks, list):
tasks = []
@ -3502,7 +3516,8 @@ def _write_custodian_brief(api_base: str, repo_slug: str, repo_path: str) -> boo
else:
lines += ["## Active Workstreams", "", "*(none — repo may need first-session setup)*"]
hygiene = collect_inbox_hygiene(api_base, repo_slug)
hygiene = (collect_inbox_hygiene(api_base, repo_slug, reader=reader) if strict
else collect_inbox_hygiene(api_base, repo_slug))
if hygiene["stale_unread_count"] or hygiene["missing_thread"] or hygiene["work_requests_unpromoted"]:
lines += ["", "## Inbox Hygiene", ""]
if hygiene["stale_unread_count"]:
@ -3539,7 +3554,7 @@ def _write_custodian_brief(api_base: str, repo_slug: str, repo_path: str) -> boo
counterpart = parse_blocked_on(blocked_on)
if not counterpart:
continue
messages = _api_get(
messages = reader(
api_base,
"/messages",
{
@ -3621,14 +3636,19 @@ def _write_custodian_brief(api_base: str, repo_slug: str, repo_path: str) -> boo
if _strip_ts(content) == _strip_ts(existing):
return False # no meaningful change
if brief_path.is_symlink():
raise ValueError("refuse redirected brief")
if check:
raise ValueError("brief differs from current read model")
brief_path.write_text(content, encoding="utf-8")
# Commit the brief so remote workers can pull it
_git_commit_writeback(
repo_path,
brief_path,
[f"update .custodian-brief.md for {repo_slug}"],
)
if commit:
_git_commit_writeback(
repo_path,
brief_path,
[f"update .custodian-brief.md for {repo_slug}"],
)
return True

View file

@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Render a current State Hub orientation brief without Git operations or hub writes."""
import argparse
import json
from pathlib import Path
import re
from consistency_check import _write_custodian_brief
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--repo-path', type=Path, required=True)
parser.add_argument('--repo-slug', required=True)
parser.add_argument('--api-base', default='http://127.0.0.1:8000')
parser.add_argument('--check', action='store_true')
args = parser.parse_args()
try:
if not re.fullmatch(r'[a-z0-9]+(?:-[a-z0-9]+)*', args.repo_slug):
raise ValueError('canonical repository slug required')
if not args.repo_path.is_dir():
raise ValueError('repository path must exist')
if (args.repo_path / '.custodian-brief.md').is_symlink():
raise ValueError('refuse redirected brief')
changed = _write_custodian_brief(args.api_base, args.repo_slug, str(args.repo_path),
commit=False, strict=True, check=args.check)
except (OSError, ValueError, KeyError, TypeError) as exc:
parser.exit(1, f'Brief generation refused: {exc}\n')
print(json.dumps({'repo': args.repo_slug, 'status': 'updated' if changed else 'current',
'source': 'state-hub-read-model', 'git_operations': False}))
if __name__ == '__main__':
main()

View file

@ -0,0 +1,49 @@
"""Rendering-only briefs must preserve existing data on incomplete API snapshots."""
from unittest.mock import Mock
import pytest
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
import consistency_check as owner
def responses(base, path, params=None, **kwargs):
if path.startswith('/repos/'):
return {'id': 'repo-1', 'domain_slug': 'infotech'}
return []
def test_render_does_not_commit_and_timestamp_only_retry_is_unchanged(tmp_path, monkeypatch):
monkeypatch.setattr(owner, '_api_get', responses)
commit = Mock(side_effect=AssertionError('Git operations forbidden'))
monkeypatch.setattr(owner, '_git_commit_writeback', commit)
assert owner._write_custodian_brief('unused', 'demo', str(tmp_path), commit=False, strict=True)
before = (tmp_path / '.custodian-brief.md').read_bytes()
assert not owner._write_custodian_brief('unused', 'demo', str(tmp_path), commit=False, strict=True)
assert (tmp_path / '.custodian-brief.md').read_bytes() == before
commit.assert_not_called()
@pytest.mark.parametrize('failed_path', ['/repos/demo', '/workplans', '/repo-goals', '/messages', '/tasks'])
@pytest.mark.parametrize('failure', [None, {'_error': 'offline'}, 'malformed'])
def test_partial_snapshot_preserves_existing_brief(tmp_path, monkeypatch, failed_path, failure):
def read(base, path, params=None, **kwargs):
if path == failed_path:
return failure
if path == '/workplans':
return [{'id': 'wp-1', 'title': 'Waiting'}]
return responses(base, path, params, **kwargs)
monkeypatch.setattr(owner, '_api_get', read)
path = tmp_path / '.custodian-brief.md'; path.write_text('preserved')
with pytest.raises(ValueError, match='unavailable or malformed'):
owner._write_custodian_brief('unused', 'demo', str(tmp_path), commit=False, strict=True)
assert path.read_text() == 'preserved'
def test_check_has_no_write(tmp_path, monkeypatch):
monkeypatch.setattr(owner, '_api_get', responses)
path = tmp_path / '.custodian-brief.md'; path.write_text('preserved')
with pytest.raises(ValueError, match='differs'):
owner._write_custodian_brief('unused', 'demo', str(tmp_path), commit=False, strict=True, check=True)
assert path.read_text() == 'preserved'

View file

@ -0,0 +1,28 @@
---
id: STATE-WP-ADHOC-2026-10-19
type: workplan
title: "Small source maintenance for 2026-10-19"
domain: infotech
repo: state-hub
status: finished
owner: codex
created: "2026-10-19"
updated: "2026-10-19"
flavor: implementation
depends_on: []
---
## Render briefs without implicit Git commits
```task
id: STATE-WP-ADHOC-2026-10-19-T01
status: done
priority: medium
```
Expose the existing brief renderer through an explicit no-Git CLI and Make targets.
Keep strict API failures from overwriting existing briefs, support read-only drift
checks, document the read-model boundary in SCOPE. Supports CUST-WP-0079 provider
adoption without generated-only guard bypass. Seventeen focused tests pass and
Custodian live read-model check passes. Broader consistency validation recorded
in the session progress receipt; no runtime deployment or scheduler activation.