docs: preserve reviewed NetKingdom session artifacts
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Reject generated-only commits / generated-commit-guard (push) Successful in 9s

This commit is contained in:
codex 2026-11-01 22:53:35 +01:00
parent a0d684116a
commit d3fb789505
48 changed files with 11079 additions and 0 deletions

View file

@ -0,0 +1,517 @@
.PHONY: start install install-cli dashboard-install dashboard-check db db-tools migrate seed api dashboard dashboard-local sync check-primary primary-port-clear check check-local test test-python clean register-project register-codex-project register-mcp configure-codex bootstrap-env dev-hub edge-relay mcp-profile validate-adr add-domain rename-domain add-repo list-repos register-path register-from-classification register-from-classification-all cleanup-stale tunnels-up tunnels-status tunnels-check bridges install-hooks install-hooks-all gitea-inventory token-reconcile railiance-state-hub-render railiance-state-hub-client-dry-run railiance-state-hub-server-dry-run railiance-state-hub-headroom
COMPOSE = docker compose -f infra/docker-compose.yml --env-file .env
PYTHON ?= python3
HELM ?= $(shell command -v helm 2>/dev/null || if [ -x "$$HOME/.local/bin/helm" ]; then printf "%s" "$$HOME/.local/bin/helm"; else printf "%s" "helm"; fi)
KUBECTL ?= $(shell command -v kubectl 2>/dev/null || if [ -x "$$HOME/.local/bin/kubectl" ]; then printf "%s" "$$HOME/.local/bin/kubectl"; else printf "%s" "kubectl"; fi)
RAILIANCE_STATE_HUB_RELEASE ?= state-hub
RAILIANCE_STATE_HUB_NAMESPACE ?= state-hub
RAILIANCE_STATE_HUB_CHART ?= deploy/railiance/apps/charts/state-hub
RAILIANCE_STATE_HUB_VALUES ?= deploy/railiance/apps/helm/state-hub-values.yaml
RAILIANCE_STATE_HUB_IMAGE_TAG ?= main-c103cbf@sha256:4cc4ce17bbcdb1b66132d07c5b7b741f46df83124ef1c8403ffabdafba6993d1
RAILIANCE_STATE_HUB_PLATFORM_DIR ?= deploy/railiance/platform
RAILIANCE_STATE_HUB_APP_MANIFESTS ?= deploy/railiance/apps/manifests
# Codex/WSL non-login shells may not source ~/.profile; keep uv discoverable.
UV ?= $(shell command -v uv 2>/dev/null || if [ -x "$$HOME/.local/bin/uv" ]; then printf "%s" "$$HOME/.local/bin/uv"; else printf "%s" "uv"; fi)
RMGR ?= $(shell command -v rmgr 2>/dev/null || if [ -x "$$HOME/repo-manager/.venv/bin/rmgr" ]; then printf "%s" "$$HOME/repo-manager/.venv/bin/rmgr"; else printf "%s" "rmgr"; fi)
STATE_HUB_API_BASE ?= http://127.0.0.1:8000
SYNC_PATH ?= .
SYNC_PUSH ?= 1
start:
@echo "# Normal production UI access (run in order)"
@echo "make bridges # connect local :8000 to the railiance01 primary"
@echo "make dashboard # verify the primary, then serve the UI on :3000"
@echo "make sync # push file-backed records and reconcile the exact forge commit"
@echo ""
@echo "# Optional local MCP adapter"
@echo "make mcp-http # local SSE adapter on :8001; not needed by the UI"
@echo ""
@echo "# Local fallback/development only"
@echo "make api # local Postgres + API; conflicts with the production :8000 tunnel"
@echo "make dashboard-local # dashboard against the deliberate local API"
install:
$(UV) sync
dashboard/node_modules/.bin/observable: dashboard/package.json dashboard/package-lock.json
cd dashboard && npm ci
dashboard-install: dashboard/node_modules/.bin/observable
dashboard-check: dashboard-install
cd dashboard && npm run build
## Symlink the custodian CLI into ~/.local/bin so it's on PATH system-wide
install-cli: install
mkdir -p ~/.local/bin
ln -sf "$(shell pwd)/.venv/bin/custodian" ~/.local/bin/custodian
ln -sf "$(shell pwd)/.venv/bin/statehub" ~/.local/bin/statehub
@echo "Installed: custodian → $$(readlink -f ~/.local/bin/custodian)"
@echo "Installed: statehub → $$(readlink -f ~/.local/bin/statehub)"
@echo "Make sure ~/.local/bin is on your PATH:"
@echo " echo 'export PATH=\"\$$HOME/.local/bin:\$$PATH\"' >> ~/.bashrc && source ~/.bashrc"
db:
$(COMPOSE) up -d postgres
db-tools:
$(COMPOSE) --profile tools up -d
migrate:
$(UV) run alembic upgrade head
seed:
$(UV) run python scripts/seed.py
## Start (or restart) the MCP SSE server on :8001 — primary transport for Claude Code.
## Remote clients (e.g. COULOMBCORE) connect via the ops-bridge tunnel (port 18001).
## Registration: claude mcp add-json -s user state-hub '{"type":"sse","url":"http://127.0.0.1:8001/sse"}'
mcp-http:
@fuser -k 8001/tcp 2>/dev/null && echo "Stopped running MCP server" || true
MCP_TRANSPORT=sse MCP_PORT=8001 $(UV) run python mcp_server/server.py
## Require the production identity before serving the normal dashboard. This
## prevents an accidental local `make api` from presenting an empty fallback DB
## as the live State Hub.
check-primary:
@health="$$(curl -fsS --max-time 5 http://127.0.0.1:8000/state/health 2>/dev/null)" || { \
echo "ERROR: State Hub primary is not reachable on 127.0.0.1:8000." >&2; \
echo "Run 'make bridges' first." >&2; \
exit 1; \
}; \
identity="$$(printf '%s' "$$health" | $(PYTHON) -c 'import json, sys; d=json.load(sys.stdin); print("{}/{}".format(d.get("instance_role", ""), d.get("instance_label", "")))')"; \
if [ "$$identity" != "primary/railiance01" ]; then \
echo "ERROR: 127.0.0.1:8000 is '$$identity', not the primary/railiance01 State Hub." >&2; \
echo "Stop the local API, then run 'make bridges'. Use 'make dashboard-local' only for intentional local development." >&2; \
exit 1; \
fi
## Refuse to start the production tunnel when a non-primary API already owns
## port 8000. With no listener, bridge is free to establish the tunnel.
primary-port-clear:
@if health="$$(curl -fsS --max-time 2 http://127.0.0.1:8000/state/health 2>/dev/null)"; then \
identity="$$(printf '%s' "$$health" | $(PYTHON) -c 'import json, sys; d=json.load(sys.stdin); print("{}/{}".format(d.get("instance_role", ""), d.get("instance_label", "")))')"; \
if [ "$$identity" != "primary/railiance01" ]; then \
echo "ERROR: port 8000 is occupied by '$$identity'." >&2; \
echo "Stop the local 'make api' process before starting the production tunnel." >&2; \
exit 1; \
fi; \
fi
dashboard: check-primary
@fuser -k 3000/tcp 2>/dev/null && echo "Stopped running dashboard" || true
$(MAKE) dashboard-install
cd dashboard && npm run dev
## Deliberate local-development dashboard; bypasses the production identity gate.
dashboard-local:
@echo "WARNING: serving the dashboard against the local/fallback API on :8000."
@fuser -k 3000/tcp 2>/dev/null && echo "Stopped running dashboard" || true
$(MAKE) dashboard-install
cd dashboard && npm run dev
## Fast work-record path. Repo Manager assigns only missing deterministic IDs,
## verifies the primary/railliance01 bridge, and asks central to derive the
## exact pushed Forgejo commit in one transactional request.
sync:
$(RMGR) sync --path "$(SYNC_PATH)" --api-base "$(STATE_HUB_API_BASE)" $(if $(filter 1 true yes,$(SYNC_PUSH)),--push,)
check: check-primary
@echo "State Hub primary/railiance01 is healthy."
check-local:
curl -sf http://127.0.0.1:8000/state/health | python3 -m json.tool
# CUST-WP-0067-T09. The chart ships a copy of the-custodian canon allowed-values
# because a container has no checkout to read it from. Regenerate after any
# canon vocabulary change; the -check target fails on drift so a release cannot
# validate classification against a stale vocabulary.
sync-classification-allowed:
python3 scripts/sync_classification_allowed.py
check-classification-allowed:
python3 scripts/sync_classification_allowed.py --check
# STATE-WP-0091: read-only surge-headroom check. Does not start Helm.
# Live: make railiance-state-hub-headroom
# Fixtures: make railiance-state-hub-headroom HEADROOM_FIXTURE=tests/fixtures/release_headroom/rev63-105m.json
HEADROOM_FIXTURE ?=
railiance-state-hub-headroom:
@if [ -n "$(HEADROOM_FIXTURE)" ]; then \
$(PYTHON) scripts/release_headroom_preflight.py --fixture "$(HEADROOM_FIXTURE)"; \
else \
$(PYTHON) scripts/release_headroom_preflight.py --kubectl "$(KUBECTL)"; \
fi
railiance-state-hub-render: check-classification-allowed
$(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \
--namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \
-f $(RAILIANCE_STATE_HUB_VALUES) \
--set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG)
railiance-state-hub-client-dry-run:
@set -e; \
tmpdir="$$(mktemp -d)"; \
trap 'rm -rf "$$tmpdir"' EXIT; \
$(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \
--namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \
-f $(RAILIANCE_STATE_HUB_VALUES) \
--set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG) > "$$tmpdir/state-hub.yaml"; \
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-credentials.sops.yaml.template; \
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-cluster.yaml; \
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-networkpolicies.yaml; \
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-namespace.yaml; \
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \
$(KUBECTL) apply --dry-run=client -n $(RAILIANCE_STATE_HUB_NAMESPACE) -f "$$tmpdir/state-hub.yaml"
railiance-state-hub-server-dry-run:
@set -e; \
tmpdir="$$(mktemp -d)"; \
trap 'rm -rf "$$tmpdir"' EXIT; \
$(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \
--namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \
-f $(RAILIANCE_STATE_HUB_VALUES) \
--set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG) > "$$tmpdir/state-hub.yaml"; \
$(KUBECTL) apply --server-side --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-credentials.sops.yaml.template; \
$(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-cluster.yaml; \
$(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-networkpolicies.yaml; \
$(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-namespace.yaml; \
if $(KUBECTL) get namespace $(RAILIANCE_STATE_HUB_NAMESPACE) >/dev/null 2>&1; then \
$(KUBECTL) apply --server-side --dry-run=server -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \
$(KUBECTL) apply --dry-run=server -n $(RAILIANCE_STATE_HUB_NAMESPACE) -f "$$tmpdir/state-hub.yaml"; \
else \
echo "Namespace $(RAILIANCE_STATE_HUB_NAMESPACE) does not exist; validating namespaced app manifests with client dry-run."; \
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-namespace.yaml; \
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \
$(KUBECTL) apply --dry-run=client -n $(RAILIANCE_STATE_HUB_NAMESPACE) -f "$$tmpdir/state-hub.yaml"; \
fi
test: test-python dashboard-check
test-python:
TEST_DATABASE_URL=postgresql+asyncpg://custodian:changeme@127.0.0.1:5432/custodian_test \
$(UV) run pytest -x -q
## Benchmark /state/summary revision cache (API must be running on :8000)
benchmark-summary-cache:
$(UV) run python scripts/benchmark_summary_cache.py
## ops-bridge managed tunnels
## Requires ops-bridge: bridge is at /home/worsch/.local/bin/bridge
tunnels-up: primary-port-clear
bridge up
tunnels-status:
bridge status
## End-to-end check: verifies SSH process alive + remote port listening on COULOMBCORE.
## Exits non-zero if any tunnel is not fully operational.
tunnels-check:
bridge check
## Ensure all ops-bridge tunnels are up and healthy.
## Brings up any stopped/stale tunnels, shows final status, exits non-zero if anything is still down.
bridges: primary-port-clear
@echo "==> Bringing up all tunnels..."
bridge up
@echo ""
@echo "==> Tunnel status:"
bridge status
@echo ""
@echo "==> Checking tunnel health..."
bridge check
## Start (or restart) the LOCAL FALLBACK backend — db + migrate + uvicorn.
## This replaces anything on :8000, including the production State Hub tunnel.
api: db
@echo "WARNING: starting the local fallback API; this is not the railiance01 primary."
@echo "Waiting for postgres..."; \
for i in 1 2 3 4 5 6 7 8 9 10; do \
nc -z 127.0.0.1 5432 2>/dev/null && break; \
sleep 1; \
done
$(MAKE) migrate
@fuser -k 8000/tcp 2>/dev/null && echo "Stopped running API" || true
$(UV) run uvicorn api.main:app --reload --reload-dir api --reload-dir mcp_server --reload-dir task_flow_engine --host 127.0.0.1 --port 8000
## Register a project (Claude Code): make register-project DOMAIN=railiance PROJECT_PATH=/home/worsch/railiance
register-project:
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required. Usage: make register-project DOMAIN=<domain> PROJECT_PATH=<path>"; exit 1)
@test -n "$(PROJECT_PATH)" || (echo "ERROR: PROJECT_PATH is required."; exit 1)
scripts/register_project.sh "$(DOMAIN)" "$(PROJECT_PATH)"
## Register a Codex project (AGENTS.md + HTTP API): make register-codex-project DOMAIN=capabilities PROJECT_PATH=/home/worsch/my-repo
register-codex-project:
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required. Usage: make register-codex-project DOMAIN=<domain> PROJECT_PATH=<path>"; exit 1)
@test -n "$(PROJECT_PATH)" || (echo "ERROR: PROJECT_PATH is required."; exit 1)
scripts/register_project.sh "$(DOMAIN)" "$(PROJECT_PATH)" --codex
## Register State Hub MCP for Claude Code. Optional: make register-mcp MCP_URL=http://127.0.0.1:18001/sse
register-mcp:
scripts/register-mcp.sh \
$(if $(MCP_URL),--url "$(MCP_URL)",) \
$(if $(API_BASE),--api-base "$(API_BASE)",) \
$(if $(DRY_RUN),--dry-run,)
## Configure Codex workspace-write networking for local State Hub access.
configure-codex:
scripts/configure-codex.sh \
$(if $(CODEX_HOME),--codex-home "$(CODEX_HOME)",) \
$(if $(API_BASE),--api-base "$(API_BASE)",) \
$(if $(WITH_MCP),--with-mcp,) \
$(if $(DRY_RUN),--dry-run,)
## Bootstrap a new operator/collaborator environment. Optional: make bootstrap-env ARGS="--install-missing"
bootstrap-env:
scripts/bootstrap-env.sh $(ARGS)
## Files-first local dev hub (CUST-WP-0054-T07): postgres + migrate + seed + API + repo registration.
## Optional: WITH_EDGE=1 WITH_MCP=1 make dev-hub
## Fleet tunnel when reachable: make dev-hub PROFILE=fleet
dev-hub:
bash scripts/dev_hub_up.sh $(if $(PROFILE),$(PROFILE),dev)
## Local edge relay for offline write buffering (STATE-WP-0068).
edge-relay:
@fuser -k 18080/tcp 2>/dev/null && echo "Stopped running edge relay" || true
STATEHUB_UPSTREAM_URL=$${STATEHUB_UPSTREAM_URL:-http://127.0.0.1:8000} \
$(UV) run uvicorn api.edge.relay:app --host 127.0.0.1 --port 18080
## Switch MCP/API profile: make mcp-profile PROFILE=dev|fleet|status
mcp-profile:
@test -n "$(PROFILE)" || (echo "ERROR: PROFILE is required (dev|fleet|status)."; exit 1)
bash scripts/mcp_hub_profile.sh "$(PROFILE)"
## Add a second repo to an existing domain: make add-repo DOMAIN=railiance REPO_PATH=/home/worsch/railiance-infra
add-repo:
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1)
@test -n "$(REPO_PATH)" || (echo "ERROR: REPO_PATH is required."; exit 1)
scripts/register_project.sh "$(DOMAIN)" "$(REPO_PATH)" --additional
## Create a new domain: make add-domain DOMAIN=my_domain NAME="My Domain"
add-domain:
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required (slug)."; exit 1)
@test -n "$(NAME)" || (echo "ERROR: NAME is required (display name)."; exit 1)
curl -sf -X POST http://127.0.0.1:8000/domains/ \
-H "Content-Type: application/json" \
-d "{\"slug\": \"$(DOMAIN)\", \"name\": \"$(NAME)\"}" | python3 -m json.tool
## Rename a domain: make rename-domain DOMAIN=old_slug NEW_SLUG=new_slug NEW_NAME="New Name"
rename-domain:
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN (old slug) is required."; exit 1)
@test -n "$(NEW_SLUG)" || (echo "ERROR: NEW_SLUG is required."; exit 1)
@test -n "$(NEW_NAME)" || (echo "ERROR: NEW_NAME is required."; exit 1)
curl -sf -X PATCH http://127.0.0.1:8000/domains/$(DOMAIN)/rename \
-H "Content-Type: application/json" \
-d "{\"new_slug\": \"$(NEW_SLUG)\", \"new_name\": \"$(NEW_NAME)\"}" | python3 -m json.tool
## Register this machine's local path for a repo: make register-path REPO=marki-docx PATH=/home/tegwick/marki-docx
register-path:
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make register-path REPO=<slug> PATH=<path>"; exit 1)
@test -n "$(PATH)" || (echo "ERROR: PATH is required. Usage: make register-path REPO=<slug> PATH=<path>"; exit 1)
curl -sf -X POST "http://127.0.0.1:8000/repos/$(REPO)/paths" \
-H "Content-Type: application/json" \
-d "{\"host\": \"$$(hostname)\", \"path\": \"$(PATH)\"}" | python3 -m json.tool
## List repos for a domain: make list-repos DOMAIN=railiance
list-repos:
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1)
curl -sf "http://127.0.0.1:8000/repos/?domain=$(DOMAIN)" | python3 -m json.tool
## Ingest SBOM data for a repo (all mechanisms: lockfiles + ansible + sbom-tools.yaml).
## Auto-detect all sources: make ingest-sbom REPO=the-custodian REPO_PATH=/home/worsch/the-custodian
## Single lockfile (explicit): make ingest-sbom REPO=the-custodian LOCKFILE=/path/to/uv.lock
## Dry-run (no submit): make ingest-sbom REPO=the-custodian REPO_PATH=... DRY_RUN=1
## Tip: run capture-tools first for repos with system-level tool dependencies.
ingest-sbom:
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
$(UV) run python scripts/ingest_sbom.py --repo "$(REPO)" \
$(if $(LOCKFILE),--lockfile "$(LOCKFILE)") \
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \
$(if $(DRY_RUN),--dry-run)
## Ingest capability declarations from SCOPE.md into the catalog.
## Usage: make ingest-capabilities REPO=the-custodian [REPO_PATH=/home/worsch/the-custodian]
## Or: make ingest-capabilities-all
## Add DRY_RUN=1 to preview without writing.
ingest-capabilities:
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
$(UV) run python scripts/ingest_capabilities.py --repo "$(REPO)" \
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \
$(if $(DRY_RUN),--dry-run)
ingest-capabilities-all:
$(UV) run python scripts/ingest_capabilities.py --all \
$(if $(DRY_RUN),--dry-run)
## Check Repository Definition of Integrated (DoI) criteria for a repo.
## Usage: make check-doi REPO=llm-connect
## Or: make check-doi-all
## Add JSON=1 for machine-readable output.
check-doi:
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
$(UV) run python scripts/check_doi.py --repo "$(REPO)" $(if $(JSON),--json)
check-doi-all:
$(UV) run python scripts/check_doi.py --all $(if $(JSON),--json)
## Ingest tpsc.yaml service declarations from a repo into the TPSC catalog.
## Usage: make ingest-tpsc REPO=llm-connect
## Or: make ingest-tpsc-all
## Add DRY_RUN=1 to preview without writing.
ingest-tpsc:
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
$(UV) run python scripts/ingest_tpsc.py --repo "$(REPO)" \
$(if $(DRY_RUN),--dry-run)
ingest-tpsc-all:
$(UV) run python scripts/ingest_tpsc.py --all \
$(if $(DRY_RUN),--dry-run)
## Run SBOM capture agent for a repo — generates/updates sbom-tools.yaml.
## Usage: make capture-tools REPO=railiance-infra [REPO_PATH=/home/worsch/railiance-infra]
## Add DRY_RUN=1 to preview without writing.
capture-tools:
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
$(UV) run python scripts/capture_sbom_tools.py --repo "$(REPO)" \
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \
$(if $(DRY_RUN),--dry-run)
## Check a repo for ADR-001 compliance: make validate-adr REPO=/path/to/repo [DOMAIN=custodian]
validate-adr:
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make validate-adr REPO=<path> [DOMAIN=<slug>]"; exit 1)
$(UV) run python scripts/validate_repo_adr.py "$(REPO)" $(if $(DOMAIN),--domain "$(DOMAIN)",)
## Consistency exit contract:
## - Direct scripts/consistency_check.py: 0 clean, 2 warnings-only, 1 failures.
## - Agent/operator Make wrappers below normalize warning-only 2 to shell success
## while preserving visible WARN output and keeping real failures non-zero.
## Check a single repo for ADR-001 consistency: make check-consistency REPO=the-custodian [REPO_PATH=/override]
## Exit 0 = clean or warnings-only (warnings stay visible), exit 1 = failures
check-consistency:
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make check-consistency REPO=<slug>"; exit 1)
$(UV) run python scripts/consistency_check.py --repo "$(REPO)" \
$(if $(API_BASE),--api-base "$(API_BASE)",) \
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)",); \
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
## Check and auto-fix a single repo: make fix-consistency REPO=the-custodian [REPO_PATH=/override]
## Exit 0 = clean or warnings-only (warnings stay visible), exit 1 = failures
fix-consistency:
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make fix-consistency REPO=<slug>"; exit 1)
$(UV) run python scripts/consistency_check.py --repo "$(REPO)" --fix \
$(if $(API_BASE),--api-base "$(API_BASE)",) \
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)",); \
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
## Normalize workplan frontmatter and task status literals in attached repos.
## Usage: make normalize-attached-workplans REPO=artifact-store
## make normalize-attached-workplans DIRTY=1
normalize-attached-workplans:
$(UV) run python scripts/normalize_attached_repo_workplans.py \
$(if $(REPO),--repo "$(REPO)",) \
$(if $(DIRTY),--dirty,) \
$(if $(DRY_RUN),--dry-run,)
@test -n "$(REPO)$(DIRTY)" || (echo "ERROR: set REPO=<slug> or DIRTY=1"; exit 1)
## Regenerate AGENTS.md / CLAUDE.md / .claude/rules from templates.
## Usage: make update-agent-instructions REPO=artifact-store
## make update-agent-instructions DIRTY=1
update-agent-instructions:
$(UV) run python scripts/update_agent_instruction_files.py \
$(if $(REPO),--repo "$(REPO)",) \
$(if $(DIRTY),--dirty,)
@test -n "$(REPO)$(DIRTY)" || (echo "ERROR: set REPO=<slug> or DIRTY=1"; exit 1)
## Reconcile measured token sources against State Hub.
## Usage: make token-reconcile [SINCE=2026-05-19] [APPLY=1] [ZERO_FALLBACKS=1]
token-reconcile:
$(PYTHON) scripts/token_reconcile.py \
$(if $(SINCE),--since "$(SINCE)",) \
$(if $(API_BASE),--api-base "$(API_BASE)",) \
$(if $(CODEX_HOME),--codex-home "$(CODEX_HOME)",) \
$(if $(CLAUDE_HOME),--claude-home "$(CLAUDE_HOME)",) \
$(if $(APPLY),--apply,) \
$(if $(ZERO_FALLBACKS),--zero-superseded-fallbacks,)
## Pull then fix: single repo or all repos if REPO omitted
## make fix-consistency-remote — smart pull+fix all repos that need it
## make fix-consistency-remote REPO=slug — pull+fix one repo
fix-consistency-remote:
$(UV) run python scripts/consistency_check.py \
$(if $(REPO),--repo "$(REPO)",--all) \
--remote \
$(if $(API_BASE),--api-base "$(API_BASE)",) \
$(if $(NO_WRITEBACK),--no-writeback,); \
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
## Infer repo slug from git remote URL and check: make check-consistency-here [REPO_PATH=/path/to/repo]
## Omit REPO_PATH to use the Python script's CWD (i.e. pass an empty --here flag).
check-consistency-here:
$(UV) run python scripts/consistency_check.py \
--here $(if $(REPO_PATH),"$(REPO_PATH)",) \
$(if $(API_BASE),--api-base "$(API_BASE)",); \
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
## Infer repo slug from git remote URL and fix: make fix-consistency-here [REPO_PATH=/path/to/repo]
fix-consistency-here:
$(UV) run python scripts/consistency_check.py \
--here $(if $(REPO_PATH),"$(REPO_PATH)",) \
--fix \
$(if $(API_BASE),--api-base "$(API_BASE)",); \
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
## Check all registered repos for ADR-001 consistency
check-consistency-all:
$(UV) run python scripts/consistency_check.py --all $(if $(API_BASE),--api-base "$(API_BASE)",); \
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
## Check and auto-fix all registered repos
fix-consistency-all:
$(UV) run python scripts/consistency_check.py --all --fix $(if $(API_BASE),--api-base "$(API_BASE)",); \
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
## Cancel open tasks belonging to completed/archived workstreams.
## Safe to run at any time; also suitable for a daily cron job.
## Cron example: 0 3 * * * cd ~/state-hub && make cleanup-stale
cleanup-stale:
$(UV) run python scripts/cleanup_stale_tasks.py
## Install custodian post-commit sync hook into one repo: make install-hooks REPO=marki-docx
install-hooks:
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make install-hooks REPO=<slug>"; exit 1)
bash scripts/install_hooks.sh --repo "$(REPO)"
## Install custodian post-commit sync hook into all active registered repos
install-hooks-all:
bash scripts/install_hooks.sh --all
## Remove custodian post-commit sync hook from one repo: make remove-hooks REPO=marki-docx
remove-hooks:
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make remove-hooks REPO=<slug>"; exit 1)
bash scripts/install_hooks.sh --repo "$(REPO)" --remove
## Compare Gitea coulomb org repos against state-hub registered repos
## Requires GITEA_TOKEN in env or .env: make gitea-inventory GITEA_TOKEN=<token>
gitea-inventory:
$(UV) run python scripts/gitea_inventory.py $(if $(JSON),--json)
## Register/update one repo from .repo-classification.yaml:
## make register-from-classification REPO=state-hub
## make register-from-classification PATH=/path/to/repo
## Optional: DRY_RUN=1
register-from-classification:
@test -n "$(REPO)" -o -n "$(PATH)" || (echo "ERROR: REPO or PATH is required."; exit 1)
$(UV) run python scripts/register_from_classification.py \
$(if $(PATH),--repo-path "$(PATH)",--slug "$(REPO)") \
$(if $(DRY_RUN),--dry-run,)
## Bulk register/update all active repos with accessible local paths
register-from-classification-all:
$(UV) run python scripts/register_from_classification.py --bulk \
$(if $(DRY_RUN),--dry-run,)
clean:
$(COMPOSE) down -v

View file

@ -0,0 +1,6 @@
apiVersion: v2
name: state-hub
description: State Hub API service for private Railiance operation
type: application
version: 0.1.0
appVersion: "main-c103cbf"

View file

@ -0,0 +1,69 @@
# Production values for the State Hub Railiance chart handoff.
# Non-secret values only. DATABASE_URL comes from the Secret `state-hub-env`.
namespace:
create: false
image:
repository: forgejo.coulomb.social/coulomb/state-hub
tag: "main-c103cbf@sha256:4cc4ce17bbcdb1b66132d07c5b7b741f46df83124ef1c8403ffabdafba6993d1"
ingress:
enabled: false
# MCP layer on central (CUST-WP-0067-T08). Enabled together with the image tag
# above: MCP_HOST landed in main-6c1262e, and enabling this on an older image
# would deploy a pod that binds loopback and never becomes reachable.
# ClusterIP only — no ingress, same reason the API's is disabled.
mcp:
enabled: true
resources:
requests:
# 2026-09-10: 1,078 observed 5-minute samples, p99 2.35m, peak 30.45m.
# Preserve the 500m limit for bursts; free 40m for the API rollout surge.
cpu: 10m
config:
# This deployment is the authoritative hub. Callers verify this rather than
# trusting that whatever answered on a port is central (CUST-WP-0067-T03).
instanceRole: primary
instanceLabel: railiance01
sbomNexusUrl: "http://sbom-nexus.sbom-nexus.svc.cluster.local:8010"
# Reversible T04 read cutover; set back to `legacy` to roll back.
sbomNexusReadMode: nexus
# Reversible T04 write cutover; set back to `legacy` to roll back future writes.
sbomNexusWriteMode: nexus
resources:
# Restored to the chart's 250m on 2026-09-21. The earlier 100m was a workaround
# for a node with <250m unallocated; ~830m of Knative/Kourier requests were
# released that day (the-custodian docs/kubernetes-change-gate-decision.md).
# State Hub idles near 5m but bursts past 400m under concurrent agent load.
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 1000m
memory: 2Gi
sweep:
# RMGR-WP-0005-T11: disabled while railiance01 checkouts still target the
# stale gitea-remote lineage. Re-enable only after the governed remote
# reconciliation and registrar preflight are complete.
enabled: false
# Present in the live release but previously missing here, so deploying from
# this file alone silently dropped it. Inert while enabled is false.
hostname: 239.62.205.92.host.secureserver.net
hostPath: /home/tegwick
sshHostPath: /home/tegwick/.ssh
# Forge read credential (STATE-WP-0084-T02). Coordinates only — the token lives
# in OpenBao at platform/workloads/state-hub/forge-derivation and reaches the
# pod through Kubernetes auth, never through this file or a Secret.
forgeRead:
enabled: true
# RPF-WP-0035-T04: dedicated ESO signing key, API-only explicit reference.
renamePreflight:
enabled: true
secretName: state-hub-rename-preflight

View file

@ -0,0 +1,422 @@
---
id: STATE-WP-0095
type: workplan
title: "Preserve exact closed historical source bindings in Forge receiving"
domain: infotech
repo: state-hub
status: active
owner: codex
created: "2026-10-04"
updated: "2026-10-17"
flavor: implementation
depends_on: []
state_hub_workstream_id: "79579572-8120-5d3b-aca5-dd4aff60d058"
---
## Goal
Normal Forge receiving must preserve the seven bound Activity legacy source
views and the Custodian retired ADHOC source without new identities, source
rewrites, task movement, deletion, relabeling or unretirement. This implements
receiving support before RMGR-WP-0019 registrar admission. CUST-WP-0077 owns
cross-owner coordination; deployment and replay remain explicit owner returns.
## Verify source bindings and shared views
```task
id: STATE-WP-0095-T01
status: done
priority: high
state_hub_task_id: "6a5f87bb-e63d-5e20-b9fc-c1b5cd403ff7"
```
Reviewed seven exact Activity files: four existing workplans and 64 UUID-bound
closed tasks. The archived custodian-WP-0003 parent and three split_from views
share one existing parent UUID; every repeated task binding/content agrees.
Custodian ADHOC-2026-08-25-retired retains parent
1c53d3db-6633-5e2d-987f-10706d73be9e and task
d8af3e6b-26ae-580c-bbeb-caa8ad419bec. No identity migration is authorized.
## Implement preservation receiving and global collision refusal
```task
id: STATE-WP-0095-T02
status: done
priority: high
state_hub_task_id: "ebb24194-e4cc-5130-aaa6-663f2ea2c822"
```
Prepared exact source digest manifest, existing-parent/task verification,
immutable receiving and UUID-based historical diff. Ordinary canonical sources
continue deriving their identities. Missing/changed/symlinked source, absent
bound rows or nonterminal hub histories refuse. Check task UUID holders globally,
including retired and acknowledged-stale parent workplans, before INSERT.
Offline preview tests and snapshot proof are evidence of source behavior only;
finish after applying reviewed source and its focused tests to the owning repo.
## Deliver receiving through normal deployment and verify owner return
```task
id: STATE-WP-0095-T03
status: done
priority: high
depends_on: [STATE-WP-0095-T02]
state_hub_task_id: "e8566341-cf6e-5b9c-ad80-8564d14cd52a"
decision_id: "36772026-f43d-4331-b466-5f4b1afe4eca"
```
No direct live code patch. This task is a deployment return, not policy authority
or permission to resolve identity decisions. Exact receiving fixture acceptance
and global collision rejection must hold in the delivered version.
## Verify normal historical replay preserves existing identities
```task
id: STATE-WP-0095-T04
status: done
priority: high
depends_on: [STATE-WP-0095-T03, RMGR-WP-0019-T03]
state_hub_task_id: "3bbb311f-194e-5862-8aed-358a580f22b4"
```
Verify Activity four historical parent UUIDs/64 task UUIDs and Custodian retired
parent/task unchanged; no historical row creation, movement, deletion,
relabeling or unretirement. Unrelated stale projections remain separate decisions.
2026-10-05: Source exact-set retirement support now exists. Repository projection
reconcile accepts `expected_retirement_uuids`; that set must equal the stale
projections, and any task or dependency edge on a stale row refuses the call.
Live apply still needs a reviewed rollout. On 2026-10-05 the founder accepted
review `review-1ece4f3e-d0e3-4fd6-abc8-272f1b1876f4`, which names only the three
empty projections and preserves parent `b4eb45a9-69e3-4ab0-b00c-67a53c3117c5`.
That acceptance is not a live retirement and does not resolve
`91b7c633-a66f-5d43-9f4b-6565aefb9356`.
## Offline evidence, 2026-10-04
Prepared receiver suite: Forge projection regression plus historical preservation,
missing/changed source, terminality, immutable UUID diff, and global holder query.
Real-source offline snapshot proof preserves four Activity parents/64 tasks and
one Custodian parent/task without mutations; no API writes or deployment occurred.
Exact original bindings are maintained in api/services/historical_projection.py.
2026-10-04 actual owning-source validation: 155 focused receiver/qualifier/planner
cases passed; the independent offline current-source/current-hub-snapshot proof
retained five existing parents and 65 task UUIDs, no mutations, clean historical
diff. T02 is source-complete; T03/T04 remain wait for reviewed deployment and
normal registrar/replay receiving, respectively. Evidence: docs/evidence/2026-10-04-historical-receiving-source.json.
## Verified receiving rollout and remaining replay gate, 2026-10-04
T03 is done on actual founder approval of proposal
STATE-WP-0095-T03-historical-receiving revision 1, SHA256
`d6789359b3fe7988a6201fff6f367cb46948aa197e1cc0ca66c31f45e1ea9981`,
approval receipt reference `user_reply:call_o87uEfFs3AtXfxO5YqUnSpcg:0`.
The normal owner Helm path delivered `main-5dd3df9`, commit
`5dd3df9c69eb564af10ea5ea01e349786212501a`, revision 72; API and MCP are Ready,
primary/railiance01 health and schema a0b1c2d3e4f5 verified, running source
hashes match. Rollback remains revision 71; no receipt-table downgrade is needed.
Actual historical preservation and global task-holder refusal were verified in
the delivered version. Durable receipt:
[verified historical receiving rollout](../docs/evidence/2026-10-04-historical-receiving-rollout.json).
Custodian immutable replay applied while preserving the retired historical
parent/task UUIDs. Activity immutable replay retained all four historical parent
and 64 task UUIDs and historical row contents, but refused transactionally on
three unrelated stale projections: custodian-wp-0003 (archived),
custodian-wp-0003a (done), custodian-wp-0003b (done). No retirement acknowledgement
was issued and that refusal is not represented as normal replay success.
Registrar source `c41d36c2` has 38 actual passing tests and Repo Manager's own
normal sync succeeded; registrar admission does not authorize retiring Activity
projections. T04 therefore remains wait, retaining RMGR-WP-0019-T03 and awaiting
Activity Core's precise reviewed disposition of those three stale rows. No
rename, identity migration, new paid native trial or further live action is
included in this source status update.
## Activity Core return, 2026-10-06
Activity Core returned the exact disposition (message
`cf2d5847-8b3f-4653-9d9f-d5fcfbe3f1e4`, ACTIVITY-WP-0045, activity-core commit
`941fac0`). `expected_retirement_uuids`:
`683724d9-4249-5e0e-88e0-7baff4330d58` (custodian-wp-0003),
`644017c7-c5f2-5a64-81c3-d8ee806f8251` (custodian-wp-0003a),
`a5806df9-5b9a-50da-81c1-7e606076bed9` (custodian-wp-0003b). Each has zero
tasks and zero dependency edges. None is preserved parent
`b4eb45a9-69e3-4ab0-b00c-67a53c3117c5`. Repo Manager independently verified the
set (message `916841f6`, repo-manager commit `498682c`). Nothing was retired.
T04 now waits on decision `91b7c633` and a reviewed live rollout. After that,
Repo Manager re-runs the Activity owner sync and closes RMGR-WP-0019-T03.
## Retirement rollout and T04 verification, 2026-10-06
The founder resolved decision `91b7c633` (retire exactly the three projections)
and approved the rollout. The founder ran the normal Helm upgrade to
`main-968a811` (revision 74; rollback revision 73, `main-fad9b3f`). API and MCP
are Ready, health is ok on primary/railiance01, and schema `b3c4d5e6f7a8` is
unchanged; no migrations. The activity-core reconcile at commit
`67a92d2ff8e033baf2c36998d24187fe83edec84` with `expected_retirement_uuids` set
to the three UUIDs applied: 0 created, 3 retired (tombstoned with an
`@retired-20261006` slug; UUIDs and statuses kept), 0 refused, 0 tasks
cancelled. All 7 bound Activity source files still match their digests. The four
historical parents and 64 task UUIDs are present under their bound parents with
unchanged fields. The Custodian retired parent and task are unchanged. Activity
had 54 workplans and 288 tasks before and after. Evidence:
[activity stale retirement rollout](../docs/evidence/2026-10-06-activity-stale-retirement-rollout.json).
Repo Manager's re-run of the Activity owner sync (RMGR-WP-0019-T03) and
ACTIVITY-WP-0045-T02 remain with their owners.
## Cluster exact historical receiving successor, 2026-10-17
This scoped extension retains the completed Activity/Custodian outcomes above.
Three frozen cluster archives contain three existing parent UUIDs and 15 existing
closed task UUIDs. Extend the existing exact historical receiving path before
registrar admission; never derive replacement identities or modify those
sources. Cross-owner observation attaches to existing SHR-WP-0001, with
RMGR-WP-0019 as obligor/beneficiary and NK-WP-0049-T35 benefiting indirectly.
The local derived case is waiting; no live worker injection is authorized.
## Prepare exact cluster source preservation and refusal cases
```task
id: STATE-WP-0095-T05
status: done
priority: high
state_hub_task_id: "339b9a37-181c-5599-9a89-0393e199bd4b"
```
Prepared the same full-file SHA256 and 15 task binding manifest as Repo Manager.
Candidate receiving tests pass 26: existing rows remain entirely unchanged;
missing/foreign/nonterminal rows and unknown, changed, missing or symlinked
source refuse. Read-only public metadata confirms all 18 existing rows have the
expected ownership and terminal status. These are source and metadata checks,
not delivered receiving or a committed native trial.
## Deliver reviewed cluster receiving successor before registrar admission
```task
id: STATE-WP-0095-T06
status: done
priority: high
depends_on: [STATE-WP-0095-T05]
needs_human: false
intervention_note: "Exact revision 4 approved and verified; no pending rollout intervention."
state_hub_task_id: "6af973b4-1964-5d4d-a2f8-8dbd4c01fda4"
```
Completed 2026-10-17 on the founder's approval of exact revision 4 of stable
proposal STATE-WP-0095-T06-cluster-frozen-receiving-r1 (decision
e3798419-8aa9-5425-8c08-20df6bd82a19; approved digest
sha256:63a02e8296d126c0158b7eb2c4ce79c7eeb6f8d6f61298d473c1065c7557ba32).
The ordinary owner Helm path delivered revision 76, runtime source 51bc70d and
exact image manifest sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015.
API/MCP Ready and primary/railiance01 health report expected schema
c4d5e6f7a8b9; delivered historical module sha256:e3c4bf965764b32a6df5d61c71272ee22569df9a9ddae2a3fc3df97ec554e0b2 matches.
All three parent and 15 task full DB row hashes/columns/UUIDs remained unchanged.
Five actual native probes passed: preserve, actual foreign-parent holder move,
nonterminal task, nonterminal parent and missing task. Every probe rolled back;
a fresh transaction confirmed complete row restoration. The foreign-holder
case precisely verified historical binding refusal, not the separate global
UUID-holder creation branch. No committed probe writes were made.
Evidence: [verified receiving rollout](../docs/evidence/2026-10-17-cluster-historical-receiving-rollout.json).
This actual receiving return satisfies the prerequisite of RMGR-WP-0019-T05.
Normal cluster publication and its immutable-source/row return remain T07;
source readiness and this deployment do not count that later acceptance twice.
The additive inbox migration is retained; Helm rollback does not downgrade
schema or stop an active migration Job. Original failures and exact-UID recovery
limits remain in the referenced approved packet. No TLS recovery/native private
admission or new autonomy/cost grant is inferred.
## Verify ordinary cluster receiving and immutable historical rows
```task
id: STATE-WP-0095-T07
status: wait
priority: high
depends_on: [STATE-WP-0095-T06, RMGR-WP-0019-T06, STATE-WP-0095-T10]
state_hub_task_id: "cfd1ea99-e75b-542e-a434-acefb62f3c7d"
```
Capture normal cluster owner publication/receiving receipts and compare entire
existing row snapshots and exact source bytes. No creation, movement, relabel,
unretirement or retirement is authorized for these histories. A rollback first
fences cluster sync and restores registrar refusal before removing receiving
support; it never rewrites archived IDs or UUIDs. This work does not establish
original TLS recovery or owner-native admission.
## Fix policy-compatible Secret-template server dry-run preparation
```task
id: STATE-WP-0095-T08
status: done
priority: high
state_hub_task_id: "2dd9b04f-2250-5a8a-b5bc-c8a009872858"
```
Prepared the narrow Makefile successor after the existing readiness command
refused its first Secret-template phase. Ordinary client-side apply adds the
last-applied annotation, correctly denied by the existing Platform
reject-secret-last-applied policy. Only the two Secret-template server
dry-run requests now use server-side apply with dry-run; no force-conflicts,
Secret value change, policy relaxation or actual Secret write is introduced.
Command-fixture tests verify both requests and preserve other dry-run phases.
Evidence and original refusal remain in the Custodian cluster historical
publication packet, revisions/r2 and r3-readiness. This is completed source
preparation; T06 separately records the verified actual rollout. The runtime image
continues to be main-51bc70d; Makefile/workplan changes do not create a new
application image or native acceptance.
T08 source repair refinement, 2026-10-17: the required normal Make render
correctly refused a pre-existing generated chart classification copy at v1.0
after authoritative canon reached v1.1. Regenerated only through the existing
scripts/sync_classification_allowed.py, preserving canon and strict check.
The exact source diff is version 1.0→1.1 plus publication/history/participation.
Normal Make rendering and the focused classification check passed in an
isolated owner-layout copy. The original refusal is retained in the Custodian
owner-followthrough-r1 validation receipt. This is source repair only: no live
ConfigMap apply, new runtime image, policy relaxation or T07 completion.
T07 actual ordinary-publication return, 2026-10-17: cluster owner deep consistency
and normal fast projection completed, exact archived source bytes and all 15
historical task full-row hashes remained unchanged. The later full-row check
FAILED for exactly the three parent rows. The earlier approved T06 rollout had
preserved all 18 rows at its verification stage; that success is not the later
whole-pipeline acceptance. The original snapshots retain hashes only, so exact
changed columns and original field preimages are not established. No guessed
DB repair or claim that all 18 original row hashes are restored is authorized.
T07 and RMGR-WP-0019-T06 remain waiting pending T09/T10 and a reviewed return.
## Guard immutable historical index bindings in CLI and receiving API
```task
id: STATE-WP-0095-T09
status: done
priority: high
depends_on: [STATE-WP-0095-T06]
state_hub_task_id: "8be8f13b-6352-5295-9755-de6fbfb13593"
```
Prepare and publish the narrowly scoped ordinary-binding repair for the three
existing task-bound Cluster registry entries. The CLI verifies exact frozen
source metadata/bytes and omits those bindings, including on old receivers.
The API independently validates the complete batch before any mutation, checks
actual repo ownership and the existing terminal parent/task closure, and
preserves valid frozen rows without field or timestamp assignments. Invalid
metadata, missing/foreign/nonterminal rows refuse the batch. Unknown ordinary
bindings retain existing behavior; historical IDs, UUIDs and source bytes stay
unchanged. Isolated regression tests and normal owner rendering are required.
Reviewed source preparation and canonical regression proof completed on
2026-10-17: 102 isolated tests pass and normal Make render/classification
check passes (eight objects, three unchanged delivered runtime images).
This completion record accompanies the same source publication/projection;
source preparation does not establish delivered runtime or T07 acceptance.
Central check/fix engines also refuse the complete repair batch if any fixable
context touches an exact registered parent/task UUID or frozen source path.
This runs before index bindings and before reconcile/push/individual fixes;
ordinary earlier fixes cannot partially execute before a later frozen refusal.
## Deliver and verify the ordinary-binding guard successor
```task
id: STATE-WP-0095-T10
status: done
priority: high
depends_on: [STATE-WP-0095-T09]
needs_human: false
intervention_note: "Exact revision 2 approved and verified; no pending rollout intervention."
state_hub_task_id: "88585c69-d9b9-5e1f-a256-6416ff71e824"
```
After source publication, resolve normal CI status and immutable image identity,
verify delivered guard source closure, prepare exact chart/reuse-values dry-run,
full closure/headroom/schema baseline and conditional recovery. Retain additive
migration and active-Job cancellation limitations; no Helm schema downgrade or
bounded-outage claim. Obtain scoped approval before live rollout. Then verify
new full-row baseline preservation and actual rollback-only refusal probes,
and only then return to T07 ordinary-pipeline acceptance. This prevents future
binding mutation; it does not reconstruct or restore original parent hashes.
No production changes, new autonomy or credential authority are granted here.
T09 source repair refinement, 2026-10-17: the attempted current-client regression
actually loaded an older packaged consistency checker from the owner virtualenv,
not the reviewed canonical source. That trial again failed parent-row hash
preservation; it is retained separately and does not count as R5 client proof.
Normal C-23 also legitimately normalized WP18 ready to active and created a
source commit, but the generic writeback commit included an unrelated staged
brief. A narrow successor scopes git add and git commit --only to the actual
writeback file, preserving unrelated index/worktree state. Real disposable Git
regression tests cover pre-existing staged changes, hooks, no-op and path names.
Verify the actually loaded CLI checker hash after ordinary owner package refresh
before any retry; capture legitimate source HEAD transitions rather than assume
a static revision. T07 and T10 remain wait. No original parent preimage recovery,
new runtime delivery or broader authority follows from this source repair.
T07 current-baseline client return, 2026-10-17: after the owner refreshed the
installed package and verified the actually loaded checker matched the reviewed
R6 source, ordinary Cluster deep consistency and fast projection passed.
Bindings synced 20/20, omitting the three frozen histories; all three parent
and 15 task CURRENT full-row hashes remained unchanged across
before-verified-client.json / after-verified-client.json. Unrelated staged brief
state was preserved. This proves the corrected installed client stage against
its fresh current baseline; earlier parent-hash failures remain retained
historical exceptions, not restored preimages or backfilled successes.
T10 remains wait/needs_human for exact proposal
STATE-WP-0095-T10-frozen-bindings-guard-r1, now revision 2, metadata
sha256:5d0ddebc21bcb035809cc8b57f4c2e1ba8ed09204be5b965460e178a90913f1e,
pending decision df703266-6719-4c04-ae1b-ba9afd3bca1c. The inspected candidate
image is main-c103cbf@sha256:4cc4ce17bbcdb1b66132d07c5b7b741f46df83124ef1c8403ffabdafba6993d1,
from runtime source c103cbf3c8bed5dba87a03e320595ddded9301a3. Image-only
server dry-run passed eight identities with only three image-field changes.
The exact proposal and recovery limits are retained in the Custodian dated
2026-10-17 guard-successor-review-r2 packet. No new API runtime is approved or
delivered by this source-only note; its later workplan commit does not change
the image's source pin. T09 stays done, T07/T10 stay wait pending reviewed API
delivery and explicit successor/current-baseline acceptance disposition.
T10 proposal revision history: revision 1 metadata
sha256:81d365926fc585a186955d8c98ee83501fd3cec12135b2256d71b46a75097932
and its original helper remain immutable local evidence, unapproved and
unexecuted. Revision 2 of the same stable proposal changes only the native
probe's EXPECTED hash metadata into module/sha256 records for normal publication
scanning; probe behavior, exact image, recovery and acceptance scope remain
unchanged. The same decision df703266-6719-4c04-ae1b-ba9afd3bca1c remains open
and pending. This source-only amendment grants no runtime authority or new
image/code change; T07/T10 remain wait and T09 remains done.
T10 verified receiving return, 2026-10-17: the founder approved and the owner
executed exact revision 2 of STATE-WP-0095-T10-frozen-bindings-guard-r1,
sha256:5d0ddebc21bcb035809cc8b57f4c2e1ba8ed09204be5b965460e178a90913f1e,
under decision df703266-6719-4c04-ae1b-ba9afd3bca1c. Helm revision 77 delivered
runtime source c103cbf3c8bed5dba87a03e320595ddded9301a3 and image
main-c103cbf@sha256:4cc4ce17bbcdb1b66132d07c5b7b741f46df83124ef1c8403ffabdafba6993d1.
API/MCP are Ready on expected schema c4d5e6f7a8b9. Eight actual native cases
passed with zero probe commits. All three parent and 15 task CURRENT full-row
hashes remained unchanged after delivery/probes and the actual normal Cluster
pipeline (20 ordinary bindings, three frozen bindings omitted by the verified
loaded checker). All three archived sources remained byte-identical. Exact
owned Job/pod success was observed, then their absence and removal of owned
chart staging were verified; no recovery was needed.
The verified return is the Custodian dated 2026-10-17
guard-successor-execution-r2/verified-receiving-return.json,
sha256:09958bd69367cc6903e2a21a56eb81b6a8a46c2a1907afc93f787cd63732e92a.
The original three-parent hash failures remain the founder-accepted historical
exception: unavailable original preimages were not guessed/restored, and failed
outcomes were not backfilled. The 15 original task rows and archived bytes stay
preserved. Earlier pending review notes are retained as proposal history; T10
is now done and needs_human false. T07 and this parent remain open until the
actual RMGR-WP-0019-T06 owner closure is published and returned. No native
security-target gate or broader autonomy is established by this infrastructure
return. Source appVersion and both maintained image pins now declare this same
verified image; the approved frozen rollout archive remains unchanged.

View file

@ -0,0 +1,16 @@
{
"schema": "state95.final-owner-closure.v1",
"base_revision": "bb525bc304cbd79b8a8a1540aa28bf073a4e2c32",
"files": [
{
"path": "workplans/STATE-WP-0095-historical-source-receiving.md",
"base_sha256": "bcde1a6502dfad71586dc38b2f681723dcb310c22f66f052b305ded89bf9b00d",
"candidate_sha256": "31bea1d5b54f9f6e5bc01d11d271c3a86ac76389350b467ba568b9dc93e41c61"
}
],
"patch_sha256": "sha256:cb29c018ba5ebe189e3bf40b3c4889484ed622f6f538e5671b7f80ee200c3e08",
"new_UUIDs": false,
"runtime_changed": false,
"parent": "finished",
"all10tasks": "done"
}

View file

@ -0,0 +1,224 @@
{
"ok": true,
"status": "applied",
"repo_slug": "state-hub",
"commit": "200cbd0a61a805d0fe29f9d6d1c49a6cf1f1703a",
"requests": 2,
"elapsed_ms": 15534.8,
"git": {
"upstream": "origin/main",
"behind": 0,
"ahead": 0
},
"identifiers": {
"schema": "repo-manager.identifier-assignment.v1",
"ok": true,
"executed": true,
"repo": "state-hub",
"namespace": "helixforge",
"assignments": [],
"historical_invalid_identifiers": [],
"historical_identity_collisions": [],
"files_changed": []
},
"receipt": {
"schema": "state-hub.repository-projection-reconcile.v1",
"instance_role": "primary",
"instance_label": "railiance01",
"expected_commit": "200cbd0a61a805d0fe29f9d6d1c49a6cf1f1703a",
"derived_commit": "200cbd0a61a805d0fe29f9d6d1c49a6cf1f1703a",
"outcome": {
"schema": "state-hub.projection-reset.v1",
"repo_slug": "state-hub",
"commit": "200cbd0a61a805d0fe29f9d6d1c49a6cf1f1703a",
"status": "applied",
"counts": {
"created": 0,
"updated": 70,
"retired": 0,
"refused": 0,
"released": 0,
"created_tasks": 0,
"updated_tasks": 2,
"cancelled_tasks": 0,
"created_edges": 0,
"removed_edges": 3,
"unresolved_edges": 0,
"external_edges": 6
},
"created": [],
"updated": [
"CUST-WP-0003",
"CUST-WP-0011",
"CUST-WP-0012",
"CUST-WP-0038",
"CUST-WP-0042",
"STATE-WP-0043",
"STATE-WP-0044",
"STATE-WP-0045",
"STATE-WP-0046",
"STATE-WP-0047",
"STATE-WP-0048",
"STATE-WP-0049",
"STATE-WP-0050",
"STATE-WP-0051",
"STATE-WP-0052",
"STATE-WP-0053",
"STATE-WP-0054",
"STATE-WP-0055",
"STATE-WP-0056",
"STATE-WP-0057",
"STATE-WP-0058",
"STATE-WP-0059",
"STATE-WP-0060",
"STATE-WP-0061",
"STATE-WP-0062",
"STATE-WP-0063",
"STATE-WP-0064",
"STATE-WP-0065",
"STATE-WP-0066",
"STATE-WP-0067",
"STATE-WP-0068",
"STATE-WP-0069",
"STATE-WP-0070",
"STATE-WP-0071",
"STATE-WP-0072",
"STATE-WP-0073",
"STATE-WP-0074",
"STATE-WP-0075",
"STATE-WP-0076",
"STATE-WP-0077",
"STATE-WP-0078",
"STATE-WP-0079",
"STATE-WP-0080",
"STATE-WP-0081",
"STATE-WP-0082",
"STATE-WP-0083",
"STATE-WP-0084",
"STATE-WP-0085",
"STATE-WP-0086",
"STATE-WP-0087",
"STATE-WP-0088",
"STATE-WP-0089",
"STATE-WP-0090",
"STATE-WP-0091",
"STATE-WP-0092",
"STATE-WP-0093",
"STATE-WP-0094",
"STATE-WP-0095",
"STATE-WP-0096",
"STATE-WP-0097",
"STATE-WP-0098",
"STATE-WP-ADHOC-2026-06-04",
"STATE-WP-ADHOC-2026-07-01",
"STATE-WP-ADHOC-2026-08-08",
"STATE-WP-ADHOC-2026-08-23",
"STATE-WP-ADHOC-2026-08-30",
"STATE-WP-ADHOC-2026-09-04",
"STATE-WP-ADHOC-2026-09-22",
"STATE-WP-ADHOC-2026-10-06",
"STATE-WP-ADHOC-2026-10-13"
],
"retired": [],
"released": [],
"created_tasks": [],
"updated_tasks": [
"STATE-WP-0095-T07",
"STATE-WP-0095-T10"
],
"cancelled_tasks": [],
"created_edges": [],
"removed_edges": [
{
"id": "cc2867c3-4cb1-4b63-8c0d-02529a361a15",
"from_workplan_id": "79579572-8120-5d3b-aca5-dd4aff60d058",
"from_task_id": "cfd1ea99-e75b-542e-a434-acefb62f3c7d",
"to_workplan_id": null,
"to_task_id": "4d1675b6-f49f-5b01-8d76-bbf2030d1b34",
"relationship_type": "starts_after"
},
{
"id": "e74ef85a-7884-4248-9496-7f1c3150313d",
"from_workplan_id": "79579572-8120-5d3b-aca5-dd4aff60d058",
"from_task_id": "cfd1ea99-e75b-542e-a434-acefb62f3c7d",
"to_workplan_id": null,
"to_task_id": "6af973b4-1964-5d4d-a2f8-8dbd4c01fda4",
"relationship_type": "starts_after"
},
{
"id": "713e6db5-7574-4ade-a750-84bdde02f80a",
"from_workplan_id": "79579572-8120-5d3b-aca5-dd4aff60d058",
"from_task_id": "cfd1ea99-e75b-542e-a434-acefb62f3c7d",
"to_workplan_id": null,
"to_task_id": "88585c69-d9b9-5e1f-a256-6416ff71e824",
"relationship_type": "starts_after"
}
],
"unresolved_edges": [],
"external_edges": [
{
"workplan": "STATE-WP-0098",
"from": "STATE-WP-0098-T04",
"target": "HUB-WP-0011-T01",
"relationship_type": "starts_after",
"from_workplan_id": "0c68970e-295b-57fc-ac70-e472ee3a7bc4",
"from_task_id": "077797c8-289b-55ec-9597-28f223891b02",
"to_workplan_id": null,
"to_task_id": "588e4b58-4694-513c-8e7a-1fab38fb6ce3"
},
{
"workplan": "CUST-WP-0038",
"from": "CUST-WP-0038",
"target": "CUST-WP-0011",
"relationship_type": "blocks",
"from_workplan_id": "ccb87fa4-6381-5f22-b097-6e026f56a9c1",
"from_task_id": null,
"to_workplan_id": "967baafb-d92d-405a-ba0b-0d00d37c4940",
"to_task_id": null
},
{
"workplan": "STATE-WP-0079",
"from": "STATE-WP-0079-T04",
"target": "HUB-WP-0011-T03",
"relationship_type": "starts_after",
"from_workplan_id": "ed077b62-7048-5752-bf65-f90471f45854",
"from_task_id": "3a454438-1955-547f-a491-8eab7cd25710",
"to_workplan_id": null,
"to_task_id": "61c727a5-f4b1-54c0-b634-7d71a5416496"
},
{
"workplan": "STATE-WP-0079",
"from": "STATE-WP-0079-T04",
"target": "HUB-WP-0006-T06",
"relationship_type": "starts_after",
"from_workplan_id": "ed077b62-7048-5752-bf65-f90471f45854",
"from_task_id": "3a454438-1955-547f-a491-8eab7cd25710",
"to_workplan_id": null,
"to_task_id": "7c8225f7-03dc-57b0-a668-c3d781bd828a"
},
{
"workplan": "STATE-WP-0079",
"from": "STATE-WP-0079-T06",
"target": "STATE-WP-0079-T04",
"relationship_type": "starts_after",
"from_workplan_id": "ed077b62-7048-5752-bf65-f90471f45854",
"from_task_id": "9230c99b-af58-5794-9291-452e619a30fa",
"to_workplan_id": null,
"to_task_id": "3a454438-1955-547f-a491-8eab7cd25710"
},
{
"workplan": "STATE-WP-0079",
"from": "STATE-WP-0079-T06",
"target": "STATE-WP-0079-T05",
"relationship_type": "starts_after",
"from_workplan_id": "ed077b62-7048-5752-bf65-f90471f45854",
"from_task_id": "9230c99b-af58-5794-9291-452e619a30fa",
"to_workplan_id": null,
"to_task_id": "f6cf8c01-eb59-5ca8-a541-23928d4a000c"
}
],
"refused": [],
"notes": []
}
}
}

View file

@ -0,0 +1,34 @@
{
"schema": "state95.final-closure-publication.v1",
"status": "verified",
"source_revision": "200cbd0a61a805d0fe29f9d6d1c49a6cf1f1703a",
"parent_status": "finished",
"task_count": 10,
"all_tasks_done": true,
"all_UUIDs_preserved": true,
"unrelated_dirty_and_index_preserved": true,
"native_security_target_gates_closed": 0,
"runtime_source": "c103cbf3c8bed5dba87a03e320595ddded9301a3",
"bound_receipts": [
{
"path": "docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/owner-followthrough-r9/stage2/verified-registrar-return.json",
"sha256": "sha256:6bf7fc2ee86688d6e15d584c9157bacf4fff6f87b887f28aa7ed8292df28d35e"
},
{
"path": "docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/owner-followthrough-r9/stage2/manifest.json",
"sha256": "sha256:634c816d73164a9d256b3e92205869d688dfe865edfd33fbc1be4052bb0f8795"
},
{
"path": "docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/owner-followthrough-r9/stage2/source-commit-result.json",
"sha256": "sha256:b1587c9e24befd1b4e1a4137c50a717c7c0f87d120105e8275da23d267042e8e"
},
{
"path": "docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/owner-followthrough-r9/stage2/normal-sync-result.json",
"sha256": "sha256:bd72497f8fbe6f3574b35308d788be88456f5d9905d320d14e9659a560395291"
},
{
"path": "docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/owner-followthrough-r9/stage2/source-publication-readback.json",
"sha256": "sha256:83878a62d3c6fdfc5f1568bc6cad16c7c14d2d257ecd59471473cb53da650836"
}
]
}

View file

@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Prepared exact-ten-path source publisher; requires final reviewed overlay. No deploy/sync."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
import subprocess
BASE = Path(__file__).resolve().parent
OWNER = Path('/home/worsch/state-hub')
SCANNER = ['/usr/bin/python3', '/home/worsch/repo-manager/src/repo_manager/secret_scan.py',
'--path', str(OWNER), '--binary', '/home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks']
class Refusal(RuntimeError):
pass
def sha(raw):
return hashlib.sha256(raw).hexdigest()
def git(owner, *args, check=True):
p = subprocess.run(['git', '-C', str(owner), *args], capture_output=True, timeout=150)
if check and p.returncode:
raise Refusal('git_' + args[0] + '_refused_' + str(p.returncode))
return p
def index_entries(owner):
# Stage bytes remain private to this process. Receipts expose hashes only.
return dict((entry.split(b'\t', 1)[1].decode(), entry.split(b'\t', 1)[0])
for entry in git(owner, 'ls-files', '--stage', '-z').stdout.split(b'\0') if entry)
def dirty_files(owner):
tracked = git(owner, 'diff', '--name-only', 'HEAD', '-z').stdout.split(b'\0')
untracked = git(owner, 'ls-files', '--others', '--exclude-standard', '-z').stdout.split(b'\0')
return sorted({p.decode() for p in tracked + untracked if p})
def preserved_state(owner, owned):
indices = {p: sha(v) for p, v in index_entries(owner).items() if p not in owned}
worktree = {}
for p in dirty_files(owner):
if p in owned:
continue
file = owner / p
if file.is_symlink():
raise Refusal('unrelated_symlink_requires_review')
worktree[p] = sha(file.read_bytes()) if file.exists() else None
return {'index_entries': indices, 'dirty_worktree': worktree}
def validate(owner, expected):
if git(owner, 'rev-parse', 'HEAD').stdout.decode().strip() != expected['base_revision']:
raise Refusal('source_base_drift')
files = expected['files']
names = {f['path'] for f in files}
if len(names) != len(files):
raise Refusal('duplicate_expected_path')
for f in files:
p = owner / f['path']
if p.is_symlink() or not p.is_file() or sha(p.read_bytes()) != f['candidate_sha256']:
raise Refusal('reviewed_source_drift')
# Every unrelated worktree/index entry is retained. No stash/reset/clean.
return names, preserved_state(owner, names)
def commit_source(owner, expected, scanner, execute=False, push=False):
names, before = validate(owner, expected)
result = {'schema': 'receiver-source-publication.v1', 'scope': 'source_only',
'deployed': False,
'base_revision': expected['base_revision'], 'files': sorted(names),
'preserved_before': before, 'status': 'validated_not_executed'}
if not execute:
return result
# Stage exact reviewed paths first so the normal scanner inspects them.
# A refusal retains reviewed source/staging for inspection; unrelated state stays intact.
git(owner, 'add', '--', *sorted(names))
scan = subprocess.run(scanner, cwd=owner, capture_output=True, timeout=150)
result['scanner_exit'] = scan.returncode
if scan.returncode:
raise Refusal('normal_source_scanner_refused')
# --only preserves unrelated staged content while committing exact paths.
git(owner, 'commit', '--only', '-m',
'docs(workplan): close verified historical receiving', '--', *sorted(names))
commit = git(owner, 'rev-parse', 'HEAD').stdout.decode().strip()
changed = set(git(owner, 'diff-tree', '--no-commit-id', '--name-only', '-r', commit).stdout.decode().splitlines())
if changed != names:
raise Refusal('committed_path_set_mismatch_stop')
if preserved_state(owner, names) != before:
raise Refusal('unrelated_state_changed_stop')
result.update(status='committed_not_pushed', source_revision=commit,
preserved_after=before)
if push:
# Explicit ref and bounded push. A nonzero or timeout never triggers retry.
try:
p = git(owner, 'push', 'origin', 'HEAD:refs/heads/main', check=False)
result['push_exit'] = p.returncode
except subprocess.TimeoutExpired:
result.update(status='push_timeout_remote_unverified')
return result
remote = git(owner, 'ls-remote', '--exit-code', 'origin', 'refs/heads/main', check=False)
actual = remote.stdout.split()[0].decode() if remote.returncode == 0 and remote.stdout else None
result['observed_remote_revision'] = actual
result['status'] = 'source_published_verified' if actual == commit else 'push_remote_unverified'
return result
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--execute-source-commit', action='store_true')
ap.add_argument('--push-source', action='store_true')
args = ap.parse_args()
if args.push_source:
ap.error('direct push disabled; use normal repo-local make sync after exact source commit')
publication_manifest = BASE / 'publication-manifest.json'
if not publication_manifest.is_file():
print(json.dumps({'status':'refused','reason':'reviewed_managed_UUID_T09_completion_manifest_not_ready','scope':'source_only'}))
return 1
expected = json.loads(publication_manifest.read_text())
if expected.get('publication_stage') != 'root_reviewed_UUIDs_and_T09_completion':
raise Refusal('publication_review_overlay_missing')
if len(expected['files']) != 10:
raise Refusal('expected_exact_ten_paths')
try:
result = commit_source(OWNER, expected, SCANNER, args.execute_source_commit, args.push_source)
except (Refusal, subprocess.TimeoutExpired) as exc:
result = {'schema': 'receiver-source-publication.v1', 'status': 'refused_or_unverified',
'reason': str(exc) if isinstance(exc, Refusal) else 'bounded_command_timeout',
'deployed': False}
print(json.dumps(result, indent=2))
return 0 if result['status'] in {'validated_not_executed','committed_not_pushed','source_published_verified'} else 1
if __name__ == '__main__':
raise SystemExit(main())

View file

@ -0,0 +1,16 @@
from pathlib import Path
import hashlib,json,importlib.util
p=Path(__file__).resolve().parent;owner=Path('/home/worsch/state-hub')
spec=importlib.util.spec_from_file_location('pub',p/'publish_owner_source.py');pub=importlib.util.module_from_spec(spec);spec.loader.exec_module(pub)
d=json.loads((p/'manifest.json').read_text());e=d['files'][0];assert len(d['files'])==1
assert pub.git(owner,'rev-parse','HEAD').stdout.decode().strip()==d['base_revision']
assert hashlib.sha256((p/'state-hub-workplan.patch').read_bytes()).hexdigest()=='cb29c018ba5ebe189e3bf40b3c4889484ed622f6f538e5671b7f80ee200c3e08'
f=owner/e['path'];s=p/'state-hub'/e['path'];assert not f.is_symlink() and not s.is_symlink()
assert hashlib.sha256(f.read_bytes()).hexdigest()==e['base_sha256']
assert hashlib.sha256(s.read_bytes()).hexdigest()==e['candidate_sha256']
assert not pub.git(owner,'diff','--cached','--',e['path']).stdout
before=pub.preserved_state(owner,{e['path']});f.write_bytes(s.read_bytes());assert pub.preserved_state(owner,{e['path']})==before
result=pub.commit_source(owner,d,pub.SCANNER,execute=True,push=False)
assert pub.preserved_state(owner,{e['path']})==before
(p/'source-commit-result.json').write_text(json.dumps(result,indent=2)+'\n')
print(json.dumps({k:v for k,v in result.items() if k not in {'preserved_before','preserved_after'}}))

View file

@ -0,0 +1,10 @@
from pathlib import Path
import subprocess,json
p=Path(__file__).resolve().parent
r=subprocess.run(['make','sync','SYNC_PATH=.','SYNC_PUSH=1','STATE_HUB_API_BASE=http://127.0.0.1:8000'],cwd='/home/worsch/state-hub',capture_output=True,text=True,timeout=180)
try:d=json.loads(r.stdout[r.stdout.index('{'):])
except Exception:d={'status':'unverified','reason':'normal_sync_receipt_unparseable','exit':r.returncode}
(p/'normal-sync-result.json').write_text(json.dumps(d,indent=2)+'\n')
receipt=d.get('receipt',{})
print(json.dumps({'exit':r.returncode,'status':d.get('status'),'commit':d.get('commit'),'instance_role':receipt.get('instance_role'),'instance_label':receipt.get('instance_label'),'expected_commit':receipt.get('expected_commit'),'derived_commit':receipt.get('derived_commit'),'identifier_assignments':d.get('identifiers',{}).get('assignments')}))
raise SystemExit(r.returncode)

View file

@ -0,0 +1,80 @@
{
"schema": "state95.finished-owner-readback.v1",
"status": "applied",
"source_revision": "200cbd0a61a805d0fe29f9d6d1c49a6cf1f1703a",
"remote_revision": "200cbd0a61a805d0fe29f9d6d1c49a6cf1f1703a",
"instance_role": "primary",
"instance_label": "railiance01",
"expected_commit": "200cbd0a61a805d0fe29f9d6d1c49a6cf1f1703a",
"derived_commit": "200cbd0a61a805d0fe29f9d6d1c49a6cf1f1703a",
"parent_uuid": "79579572-8120-5d3b-aca5-dd4aff60d058",
"parent_status": "finished",
"tasks": [
{
"uuid": "6a5f87bb-e63d-5e20-b9fc-c1b5cd403ff7",
"record_id": "STATE-WP-0095-T01",
"status": "done",
"needs_human": false
},
{
"uuid": "ebb24194-e4cc-5130-aaa6-663f2ea2c822",
"record_id": "STATE-WP-0095-T02",
"status": "done",
"needs_human": false
},
{
"uuid": "e8566341-cf6e-5b9c-ad80-8564d14cd52a",
"record_id": "STATE-WP-0095-T03",
"status": "done",
"needs_human": false
},
{
"uuid": "3bbb311f-194e-5862-8aed-358a580f22b4",
"record_id": "STATE-WP-0095-T04",
"status": "done",
"needs_human": false
},
{
"uuid": "339b9a37-181c-5599-9a89-0393e199bd4b",
"record_id": "STATE-WP-0095-T05",
"status": "done",
"needs_human": false
},
{
"uuid": "6af973b4-1964-5d4d-a2f8-8dbd4c01fda4",
"record_id": "STATE-WP-0095-T06",
"status": "done",
"needs_human": false
},
{
"uuid": "cfd1ea99-e75b-542e-a434-acefb62f3c7d",
"record_id": "STATE-WP-0095-T07",
"status": "done",
"needs_human": false
},
{
"uuid": "2dd9b04f-2250-5a8a-b5bc-c8a009872858",
"record_id": "STATE-WP-0095-T08",
"status": "done",
"needs_human": false
},
{
"uuid": "8be8f13b-6352-5295-9755-de6fbfb13593",
"record_id": "STATE-WP-0095-T09",
"status": "done",
"needs_human": false
},
{
"uuid": "88585c69-d9b9-5e1f-a256-6416ff71e824",
"record_id": "STATE-WP-0095-T10",
"status": "done",
"needs_human": false
}
],
"whole_unrelated_index_dirty_preserved": true,
"new_UUIDs": false,
"runtime_mutated_by_closure": false,
"declared_runtime_source": "c103cbf3c8bed5dba87a03e320595ddded9301a3",
"original_three_parent_failure": "retained_approved_historical_exception_no_guessed_restoration_no_backfill",
"residual_audit": "No own unfinished receiving/registrar-return work. Existing separate security-target/native/TLS/recovery/MFA work records remain outside this closure."
}

View file

@ -0,0 +1,47 @@
--- a/workplans/STATE-WP-0095-historical-source-receiving.md
+++ b/workplans/STATE-WP-0095-historical-source-receiving.md
@@ -4,7 +4,7 @@
title: "Preserve exact closed historical source bindings in Forge receiving"
domain: infotech
repo: state-hub
-status: active
+status: finished
owner: codex
created: "2026-10-04"
updated: "2026-10-17"
@@ -235,7 +235,7 @@
```task
id: STATE-WP-0095-T07
-status: wait
+status: done
priority: high
depends_on: [STATE-WP-0095-T06, RMGR-WP-0019-T06, STATE-WP-0095-T10]
state_hub_task_id: "cfd1ea99-e75b-542e-a434-acefb62f3c7d"
@@ -420,3 +420,26 @@
security-target gate or broader autonomy is established by this infrastructure
return. Source appVersion and both maintained image pins now declare this same
verified image; the approved frozen rollout archive remains unchanged.
+
+
+T07 final owner return and STATE95 closure, 2026-10-17: Repo Manager published
+RMGR-WP-0019 finished with T06 done at source/remote
+3bcb1e1cf3e58b263f690534f7f28024d75cdbf6. Its normal authoritative projection
+is applied on primary/railiance01 at that exact expected/derived commit; all
+six source task UUIDs/statuses and finished parent were independently read back.
+This completes the remaining registrar-return dependency after T10's verified
+revision-2 delivery, eight rollback-only native cases and actual corrected
+ordinary Cluster pipeline preserved all 18 CURRENT full-row hashes, the 15
+original task rows and three archived source byte digests.
+
+The immutable runtime return remains
+sha256:09958bd69367cc6903e2a21a56eb81b6a8a46c2a1907afc93f787cd63732e92a.
+The founder-approved original three-parent mismatch remains a retained
+historical exception; no unavailable original preimages were reconstructed,
+no failed trial was relabeled, and no original hash restoration is claimed.
+All ten STATE95 tasks are now done. The residual audit found no unfinished
+work inside this receiving/registrar-return scope; separate security-target,
+TLS/recovery/MFA and owner-native admission work remains with its existing
+live records and is not accepted by this infrastructure workplan closure.
+The runtime remains the verified c103cbf/4cc4ce17 artifact; this later source-only
+workplan commit changes neither its source pin nor the frozen rollout archive.

View file

@ -0,0 +1,445 @@
---
id: STATE-WP-0095
type: workplan
title: "Preserve exact closed historical source bindings in Forge receiving"
domain: infotech
repo: state-hub
status: finished
owner: codex
created: "2026-10-04"
updated: "2026-10-17"
flavor: implementation
depends_on: []
state_hub_workstream_id: "79579572-8120-5d3b-aca5-dd4aff60d058"
---
## Goal
Normal Forge receiving must preserve the seven bound Activity legacy source
views and the Custodian retired ADHOC source without new identities, source
rewrites, task movement, deletion, relabeling or unretirement. This implements
receiving support before RMGR-WP-0019 registrar admission. CUST-WP-0077 owns
cross-owner coordination; deployment and replay remain explicit owner returns.
## Verify source bindings and shared views
```task
id: STATE-WP-0095-T01
status: done
priority: high
state_hub_task_id: "6a5f87bb-e63d-5e20-b9fc-c1b5cd403ff7"
```
Reviewed seven exact Activity files: four existing workplans and 64 UUID-bound
closed tasks. The archived custodian-WP-0003 parent and three split_from views
share one existing parent UUID; every repeated task binding/content agrees.
Custodian ADHOC-2026-08-25-retired retains parent
1c53d3db-6633-5e2d-987f-10706d73be9e and task
d8af3e6b-26ae-580c-bbeb-caa8ad419bec. No identity migration is authorized.
## Implement preservation receiving and global collision refusal
```task
id: STATE-WP-0095-T02
status: done
priority: high
state_hub_task_id: "ebb24194-e4cc-5130-aaa6-663f2ea2c822"
```
Prepared exact source digest manifest, existing-parent/task verification,
immutable receiving and UUID-based historical diff. Ordinary canonical sources
continue deriving their identities. Missing/changed/symlinked source, absent
bound rows or nonterminal hub histories refuse. Check task UUID holders globally,
including retired and acknowledged-stale parent workplans, before INSERT.
Offline preview tests and snapshot proof are evidence of source behavior only;
finish after applying reviewed source and its focused tests to the owning repo.
## Deliver receiving through normal deployment and verify owner return
```task
id: STATE-WP-0095-T03
status: done
priority: high
depends_on: [STATE-WP-0095-T02]
state_hub_task_id: "e8566341-cf6e-5b9c-ad80-8564d14cd52a"
decision_id: "36772026-f43d-4331-b466-5f4b1afe4eca"
```
No direct live code patch. This task is a deployment return, not policy authority
or permission to resolve identity decisions. Exact receiving fixture acceptance
and global collision rejection must hold in the delivered version.
## Verify normal historical replay preserves existing identities
```task
id: STATE-WP-0095-T04
status: done
priority: high
depends_on: [STATE-WP-0095-T03, RMGR-WP-0019-T03]
state_hub_task_id: "3bbb311f-194e-5862-8aed-358a580f22b4"
```
Verify Activity four historical parent UUIDs/64 task UUIDs and Custodian retired
parent/task unchanged; no historical row creation, movement, deletion,
relabeling or unretirement. Unrelated stale projections remain separate decisions.
2026-10-05: Source exact-set retirement support now exists. Repository projection
reconcile accepts `expected_retirement_uuids`; that set must equal the stale
projections, and any task or dependency edge on a stale row refuses the call.
Live apply still needs a reviewed rollout. On 2026-10-05 the founder accepted
review `review-1ece4f3e-d0e3-4fd6-abc8-272f1b1876f4`, which names only the three
empty projections and preserves parent `b4eb45a9-69e3-4ab0-b00c-67a53c3117c5`.
That acceptance is not a live retirement and does not resolve
`91b7c633-a66f-5d43-9f4b-6565aefb9356`.
## Offline evidence, 2026-10-04
Prepared receiver suite: Forge projection regression plus historical preservation,
missing/changed source, terminality, immutable UUID diff, and global holder query.
Real-source offline snapshot proof preserves four Activity parents/64 tasks and
one Custodian parent/task without mutations; no API writes or deployment occurred.
Exact original bindings are maintained in api/services/historical_projection.py.
2026-10-04 actual owning-source validation: 155 focused receiver/qualifier/planner
cases passed; the independent offline current-source/current-hub-snapshot proof
retained five existing parents and 65 task UUIDs, no mutations, clean historical
diff. T02 is source-complete; T03/T04 remain wait for reviewed deployment and
normal registrar/replay receiving, respectively. Evidence: docs/evidence/2026-10-04-historical-receiving-source.json.
## Verified receiving rollout and remaining replay gate, 2026-10-04
T03 is done on actual founder approval of proposal
STATE-WP-0095-T03-historical-receiving revision 1, SHA256
`d6789359b3fe7988a6201fff6f367cb46948aa197e1cc0ca66c31f45e1ea9981`,
approval receipt reference `user_reply:call_o87uEfFs3AtXfxO5YqUnSpcg:0`.
The normal owner Helm path delivered `main-5dd3df9`, commit
`5dd3df9c69eb564af10ea5ea01e349786212501a`, revision 72; API and MCP are Ready,
primary/railiance01 health and schema a0b1c2d3e4f5 verified, running source
hashes match. Rollback remains revision 71; no receipt-table downgrade is needed.
Actual historical preservation and global task-holder refusal were verified in
the delivered version. Durable receipt:
[verified historical receiving rollout](../docs/evidence/2026-10-04-historical-receiving-rollout.json).
Custodian immutable replay applied while preserving the retired historical
parent/task UUIDs. Activity immutable replay retained all four historical parent
and 64 task UUIDs and historical row contents, but refused transactionally on
three unrelated stale projections: custodian-wp-0003 (archived),
custodian-wp-0003a (done), custodian-wp-0003b (done). No retirement acknowledgement
was issued and that refusal is not represented as normal replay success.
Registrar source `c41d36c2` has 38 actual passing tests and Repo Manager's own
normal sync succeeded; registrar admission does not authorize retiring Activity
projections. T04 therefore remains wait, retaining RMGR-WP-0019-T03 and awaiting
Activity Core's precise reviewed disposition of those three stale rows. No
rename, identity migration, new paid native trial or further live action is
included in this source status update.
## Activity Core return, 2026-10-06
Activity Core returned the exact disposition (message
`cf2d5847-8b3f-4653-9d9f-d5fcfbe3f1e4`, ACTIVITY-WP-0045, activity-core commit
`941fac0`). `expected_retirement_uuids`:
`683724d9-4249-5e0e-88e0-7baff4330d58` (custodian-wp-0003),
`644017c7-c5f2-5a64-81c3-d8ee806f8251` (custodian-wp-0003a),
`a5806df9-5b9a-50da-81c1-7e606076bed9` (custodian-wp-0003b). Each has zero
tasks and zero dependency edges. None is preserved parent
`b4eb45a9-69e3-4ab0-b00c-67a53c3117c5`. Repo Manager independently verified the
set (message `916841f6`, repo-manager commit `498682c`). Nothing was retired.
T04 now waits on decision `91b7c633` and a reviewed live rollout. After that,
Repo Manager re-runs the Activity owner sync and closes RMGR-WP-0019-T03.
## Retirement rollout and T04 verification, 2026-10-06
The founder resolved decision `91b7c633` (retire exactly the three projections)
and approved the rollout. The founder ran the normal Helm upgrade to
`main-968a811` (revision 74; rollback revision 73, `main-fad9b3f`). API and MCP
are Ready, health is ok on primary/railiance01, and schema `b3c4d5e6f7a8` is
unchanged; no migrations. The activity-core reconcile at commit
`67a92d2ff8e033baf2c36998d24187fe83edec84` with `expected_retirement_uuids` set
to the three UUIDs applied: 0 created, 3 retired (tombstoned with an
`@retired-20261006` slug; UUIDs and statuses kept), 0 refused, 0 tasks
cancelled. All 7 bound Activity source files still match their digests. The four
historical parents and 64 task UUIDs are present under their bound parents with
unchanged fields. The Custodian retired parent and task are unchanged. Activity
had 54 workplans and 288 tasks before and after. Evidence:
[activity stale retirement rollout](../docs/evidence/2026-10-06-activity-stale-retirement-rollout.json).
Repo Manager's re-run of the Activity owner sync (RMGR-WP-0019-T03) and
ACTIVITY-WP-0045-T02 remain with their owners.
## Cluster exact historical receiving successor, 2026-10-17
This scoped extension retains the completed Activity/Custodian outcomes above.
Three frozen cluster archives contain three existing parent UUIDs and 15 existing
closed task UUIDs. Extend the existing exact historical receiving path before
registrar admission; never derive replacement identities or modify those
sources. Cross-owner observation attaches to existing SHR-WP-0001, with
RMGR-WP-0019 as obligor/beneficiary and NK-WP-0049-T35 benefiting indirectly.
The local derived case is waiting; no live worker injection is authorized.
## Prepare exact cluster source preservation and refusal cases
```task
id: STATE-WP-0095-T05
status: done
priority: high
state_hub_task_id: "339b9a37-181c-5599-9a89-0393e199bd4b"
```
Prepared the same full-file SHA256 and 15 task binding manifest as Repo Manager.
Candidate receiving tests pass 26: existing rows remain entirely unchanged;
missing/foreign/nonterminal rows and unknown, changed, missing or symlinked
source refuse. Read-only public metadata confirms all 18 existing rows have the
expected ownership and terminal status. These are source and metadata checks,
not delivered receiving or a committed native trial.
## Deliver reviewed cluster receiving successor before registrar admission
```task
id: STATE-WP-0095-T06
status: done
priority: high
depends_on: [STATE-WP-0095-T05]
needs_human: false
intervention_note: "Exact revision 4 approved and verified; no pending rollout intervention."
state_hub_task_id: "6af973b4-1964-5d4d-a2f8-8dbd4c01fda4"
```
Completed 2026-10-17 on the founder's approval of exact revision 4 of stable
proposal STATE-WP-0095-T06-cluster-frozen-receiving-r1 (decision
e3798419-8aa9-5425-8c08-20df6bd82a19; approved digest
sha256:63a02e8296d126c0158b7eb2c4ce79c7eeb6f8d6f61298d473c1065c7557ba32).
The ordinary owner Helm path delivered revision 76, runtime source 51bc70d and
exact image manifest sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015.
API/MCP Ready and primary/railiance01 health report expected schema
c4d5e6f7a8b9; delivered historical module sha256:e3c4bf965764b32a6df5d61c71272ee22569df9a9ddae2a3fc3df97ec554e0b2 matches.
All three parent and 15 task full DB row hashes/columns/UUIDs remained unchanged.
Five actual native probes passed: preserve, actual foreign-parent holder move,
nonterminal task, nonterminal parent and missing task. Every probe rolled back;
a fresh transaction confirmed complete row restoration. The foreign-holder
case precisely verified historical binding refusal, not the separate global
UUID-holder creation branch. No committed probe writes were made.
Evidence: [verified receiving rollout](../docs/evidence/2026-10-17-cluster-historical-receiving-rollout.json).
This actual receiving return satisfies the prerequisite of RMGR-WP-0019-T05.
Normal cluster publication and its immutable-source/row return remain T07;
source readiness and this deployment do not count that later acceptance twice.
The additive inbox migration is retained; Helm rollback does not downgrade
schema or stop an active migration Job. Original failures and exact-UID recovery
limits remain in the referenced approved packet. No TLS recovery/native private
admission or new autonomy/cost grant is inferred.
## Verify ordinary cluster receiving and immutable historical rows
```task
id: STATE-WP-0095-T07
status: done
priority: high
depends_on: [STATE-WP-0095-T06, RMGR-WP-0019-T06, STATE-WP-0095-T10]
state_hub_task_id: "cfd1ea99-e75b-542e-a434-acefb62f3c7d"
```
Capture normal cluster owner publication/receiving receipts and compare entire
existing row snapshots and exact source bytes. No creation, movement, relabel,
unretirement or retirement is authorized for these histories. A rollback first
fences cluster sync and restores registrar refusal before removing receiving
support; it never rewrites archived IDs or UUIDs. This work does not establish
original TLS recovery or owner-native admission.
## Fix policy-compatible Secret-template server dry-run preparation
```task
id: STATE-WP-0095-T08
status: done
priority: high
state_hub_task_id: "2dd9b04f-2250-5a8a-b5bc-c8a009872858"
```
Prepared the narrow Makefile successor after the existing readiness command
refused its first Secret-template phase. Ordinary client-side apply adds the
last-applied annotation, correctly denied by the existing Platform
reject-secret-last-applied policy. Only the two Secret-template server
dry-run requests now use server-side apply with dry-run; no force-conflicts,
Secret value change, policy relaxation or actual Secret write is introduced.
Command-fixture tests verify both requests and preserve other dry-run phases.
Evidence and original refusal remain in the Custodian cluster historical
publication packet, revisions/r2 and r3-readiness. This is completed source
preparation; T06 separately records the verified actual rollout. The runtime image
continues to be main-51bc70d; Makefile/workplan changes do not create a new
application image or native acceptance.
T08 source repair refinement, 2026-10-17: the required normal Make render
correctly refused a pre-existing generated chart classification copy at v1.0
after authoritative canon reached v1.1. Regenerated only through the existing
scripts/sync_classification_allowed.py, preserving canon and strict check.
The exact source diff is version 1.0→1.1 plus publication/history/participation.
Normal Make rendering and the focused classification check passed in an
isolated owner-layout copy. The original refusal is retained in the Custodian
owner-followthrough-r1 validation receipt. This is source repair only: no live
ConfigMap apply, new runtime image, policy relaxation or T07 completion.
T07 actual ordinary-publication return, 2026-10-17: cluster owner deep consistency
and normal fast projection completed, exact archived source bytes and all 15
historical task full-row hashes remained unchanged. The later full-row check
FAILED for exactly the three parent rows. The earlier approved T06 rollout had
preserved all 18 rows at its verification stage; that success is not the later
whole-pipeline acceptance. The original snapshots retain hashes only, so exact
changed columns and original field preimages are not established. No guessed
DB repair or claim that all 18 original row hashes are restored is authorized.
T07 and RMGR-WP-0019-T06 remain waiting pending T09/T10 and a reviewed return.
## Guard immutable historical index bindings in CLI and receiving API
```task
id: STATE-WP-0095-T09
status: done
priority: high
depends_on: [STATE-WP-0095-T06]
state_hub_task_id: "8be8f13b-6352-5295-9755-de6fbfb13593"
```
Prepare and publish the narrowly scoped ordinary-binding repair for the three
existing task-bound Cluster registry entries. The CLI verifies exact frozen
source metadata/bytes and omits those bindings, including on old receivers.
The API independently validates the complete batch before any mutation, checks
actual repo ownership and the existing terminal parent/task closure, and
preserves valid frozen rows without field or timestamp assignments. Invalid
metadata, missing/foreign/nonterminal rows refuse the batch. Unknown ordinary
bindings retain existing behavior; historical IDs, UUIDs and source bytes stay
unchanged. Isolated regression tests and normal owner rendering are required.
Reviewed source preparation and canonical regression proof completed on
2026-10-17: 102 isolated tests pass and normal Make render/classification
check passes (eight objects, three unchanged delivered runtime images).
This completion record accompanies the same source publication/projection;
source preparation does not establish delivered runtime or T07 acceptance.
Central check/fix engines also refuse the complete repair batch if any fixable
context touches an exact registered parent/task UUID or frozen source path.
This runs before index bindings and before reconcile/push/individual fixes;
ordinary earlier fixes cannot partially execute before a later frozen refusal.
## Deliver and verify the ordinary-binding guard successor
```task
id: STATE-WP-0095-T10
status: done
priority: high
depends_on: [STATE-WP-0095-T09]
needs_human: false
intervention_note: "Exact revision 2 approved and verified; no pending rollout intervention."
state_hub_task_id: "88585c69-d9b9-5e1f-a256-6416ff71e824"
```
After source publication, resolve normal CI status and immutable image identity,
verify delivered guard source closure, prepare exact chart/reuse-values dry-run,
full closure/headroom/schema baseline and conditional recovery. Retain additive
migration and active-Job cancellation limitations; no Helm schema downgrade or
bounded-outage claim. Obtain scoped approval before live rollout. Then verify
new full-row baseline preservation and actual rollback-only refusal probes,
and only then return to T07 ordinary-pipeline acceptance. This prevents future
binding mutation; it does not reconstruct or restore original parent hashes.
No production changes, new autonomy or credential authority are granted here.
T09 source repair refinement, 2026-10-17: the attempted current-client regression
actually loaded an older packaged consistency checker from the owner virtualenv,
not the reviewed canonical source. That trial again failed parent-row hash
preservation; it is retained separately and does not count as R5 client proof.
Normal C-23 also legitimately normalized WP18 ready to active and created a
source commit, but the generic writeback commit included an unrelated staged
brief. A narrow successor scopes git add and git commit --only to the actual
writeback file, preserving unrelated index/worktree state. Real disposable Git
regression tests cover pre-existing staged changes, hooks, no-op and path names.
Verify the actually loaded CLI checker hash after ordinary owner package refresh
before any retry; capture legitimate source HEAD transitions rather than assume
a static revision. T07 and T10 remain wait. No original parent preimage recovery,
new runtime delivery or broader authority follows from this source repair.
T07 current-baseline client return, 2026-10-17: after the owner refreshed the
installed package and verified the actually loaded checker matched the reviewed
R6 source, ordinary Cluster deep consistency and fast projection passed.
Bindings synced 20/20, omitting the three frozen histories; all three parent
and 15 task CURRENT full-row hashes remained unchanged across
before-verified-client.json / after-verified-client.json. Unrelated staged brief
state was preserved. This proves the corrected installed client stage against
its fresh current baseline; earlier parent-hash failures remain retained
historical exceptions, not restored preimages or backfilled successes.
T10 remains wait/needs_human for exact proposal
STATE-WP-0095-T10-frozen-bindings-guard-r1, now revision 2, metadata
sha256:5d0ddebc21bcb035809cc8b57f4c2e1ba8ed09204be5b965460e178a90913f1e,
pending decision df703266-6719-4c04-ae1b-ba9afd3bca1c. The inspected candidate
image is main-c103cbf@sha256:4cc4ce17bbcdb1b66132d07c5b7b741f46df83124ef1c8403ffabdafba6993d1,
from runtime source c103cbf3c8bed5dba87a03e320595ddded9301a3. Image-only
server dry-run passed eight identities with only three image-field changes.
The exact proposal and recovery limits are retained in the Custodian dated
2026-10-17 guard-successor-review-r2 packet. No new API runtime is approved or
delivered by this source-only note; its later workplan commit does not change
the image's source pin. T09 stays done, T07/T10 stay wait pending reviewed API
delivery and explicit successor/current-baseline acceptance disposition.
T10 proposal revision history: revision 1 metadata
sha256:81d365926fc585a186955d8c98ee83501fd3cec12135b2256d71b46a75097932
and its original helper remain immutable local evidence, unapproved and
unexecuted. Revision 2 of the same stable proposal changes only the native
probe's EXPECTED hash metadata into module/sha256 records for normal publication
scanning; probe behavior, exact image, recovery and acceptance scope remain
unchanged. The same decision df703266-6719-4c04-ae1b-ba9afd3bca1c remains open
and pending. This source-only amendment grants no runtime authority or new
image/code change; T07/T10 remain wait and T09 remains done.
T10 verified receiving return, 2026-10-17: the founder approved and the owner
executed exact revision 2 of STATE-WP-0095-T10-frozen-bindings-guard-r1,
sha256:5d0ddebc21bcb035809cc8b57f4c2e1ba8ed09204be5b965460e178a90913f1e,
under decision df703266-6719-4c04-ae1b-ba9afd3bca1c. Helm revision 77 delivered
runtime source c103cbf3c8bed5dba87a03e320595ddded9301a3 and image
main-c103cbf@sha256:4cc4ce17bbcdb1b66132d07c5b7b741f46df83124ef1c8403ffabdafba6993d1.
API/MCP are Ready on expected schema c4d5e6f7a8b9. Eight actual native cases
passed with zero probe commits. All three parent and 15 task CURRENT full-row
hashes remained unchanged after delivery/probes and the actual normal Cluster
pipeline (20 ordinary bindings, three frozen bindings omitted by the verified
loaded checker). All three archived sources remained byte-identical. Exact
owned Job/pod success was observed, then their absence and removal of owned
chart staging were verified; no recovery was needed.
The verified return is the Custodian dated 2026-10-17
guard-successor-execution-r2/verified-receiving-return.json,
sha256:09958bd69367cc6903e2a21a56eb81b6a8a46c2a1907afc93f787cd63732e92a.
The original three-parent hash failures remain the founder-accepted historical
exception: unavailable original preimages were not guessed/restored, and failed
outcomes were not backfilled. The 15 original task rows and archived bytes stay
preserved. Earlier pending review notes are retained as proposal history; T10
is now done and needs_human false. T07 and this parent remain open until the
actual RMGR-WP-0019-T06 owner closure is published and returned. No native
security-target gate or broader autonomy is established by this infrastructure
return. Source appVersion and both maintained image pins now declare this same
verified image; the approved frozen rollout archive remains unchanged.
T07 final owner return and STATE95 closure, 2026-10-17: Repo Manager published
RMGR-WP-0019 finished with T06 done at source/remote
3bcb1e1cf3e58b263f690534f7f28024d75cdbf6. Its normal authoritative projection
is applied on primary/railiance01 at that exact expected/derived commit; all
six source task UUIDs/statuses and finished parent were independently read back.
This completes the remaining registrar-return dependency after T10's verified
revision-2 delivery, eight rollback-only native cases and actual corrected
ordinary Cluster pipeline preserved all 18 CURRENT full-row hashes, the 15
original task rows and three archived source byte digests.
The immutable runtime return remains
sha256:09958bd69367cc6903e2a21a56eb81b6a8a46c2a1907afc93f787cd63732e92a.
The founder-approved original three-parent mismatch remains a retained
historical exception; no unavailable original preimages were reconstructed,
no failed trial was relabeled, and no original hash restoration is claimed.
All ten STATE95 tasks are now done. The residual audit found no unfinished
work inside this receiving/registrar-return scope; separate security-target,
TLS/recovery/MFA and owner-native admission work remains with its existing
live records and is not accepted by this infrastructure workplan closure.
The runtime remains the verified c103cbf/4cc4ce17 artifact; this later source-only
workplan commit changes neither its source pin nor the frozen rollout archive.

View file

@ -0,0 +1,45 @@
{
"schema": "state95.registrar-closure-return.v1",
"registrar_source": "3bcb1e1cf3e58b263f690534f7f28024d75cdbf6",
"registrar_remote": "3bcb1e1cf3e58b263f690534f7f28024d75cdbf6",
"parent_status": "finished",
"tasks": [
{
"record_id": "RMGR-WP-0019-T01",
"uuid": "d78a622e-39f9-5923-b4e1-f70fdc17fb42",
"status": "done"
},
{
"record_id": "RMGR-WP-0019-T02",
"uuid": "95240c81-1805-5365-9b35-d238621a16bf",
"status": "done"
},
{
"record_id": "RMGR-WP-0019-T03",
"uuid": "682eea14-a4a8-5f33-9a9f-8f2f1ac8288d",
"status": "done"
},
{
"record_id": "RMGR-WP-0019-T04",
"uuid": "1ccb06c6-ad30-5847-8704-e5628ba81f35",
"status": "done"
},
{
"record_id": "RMGR-WP-0019-T05",
"uuid": "36f47e21-42d0-5aef-b065-4a5dd3af0726",
"status": "done"
},
{
"record_id": "RMGR-WP-0019-T06",
"uuid": "4d1675b6-f49f-5b01-8d76-bbf2030d1b34",
"status": "done"
}
],
"projection_status": "applied",
"instance_role": "primary",
"instance_label": "railiance01",
"expected_commit": "3bcb1e1cf3e58b263f690534f7f28024d75cdbf6",
"derived_commit": "3bcb1e1cf3e58b263f690534f7f28024d75cdbf6",
"verified_runtime_receipt_sha256": "sha256:09958bd69367cc6903e2a21a56eb81b6a8a46c2a1907afc93f787cd63732e92a",
"residual_audit": "STATE95 all other9tasks done; receiving/ordinarypipeline/registrar-return scope complete. Earlier3parent failure is approved retained exception, not unresolved repair. Security target/TLS/recovery/MFA/native admission remain existing separate live driver records outside STATE95; no new residual invented."
}

View file

@ -0,0 +1,972 @@
--- a/src/repo_manager/historical_sources.py
+++ b/src/repo_manager/historical_sources.py
@@ -70,6 +70,111 @@
"record_id": "CUST-WP-ADHOC-2026-08-25-RETIRED",
"uuid": "1c53d3db-6633-5e2d-987f-10706d73be9e",
"parent_record_id": "CUST-WP-ADHOC-2026-08-25-RETIRED"
+ },
+ {
+ "repo": "railiance-cluster",
+ "path": "workplans/archived/260622-RAIL-BS-WP-0002-k3s-baseline.md",
+ "sha256": "8d6f8533c119117686bc0d5bb9ed5349726aa6c3189ff98b53409c68ac685ca9",
+ "record_id": "RAIL-BS-WP-0002",
+ "uuid": "4c63dfc6-9eac-4e79-9f77-8f644ad7147d",
+ "parent_record_id": "RAIL-BS-WP-0002",
+ "tasks": [
+ {
+ "label": "T01",
+ "uuid": "3f042630-eab0-4c6a-9167-e2b28ff20e40",
+ "status": "done"
+ },
+ {
+ "label": "T02",
+ "uuid": "e8510646-46ed-4697-a345-f3d3009eea78",
+ "status": "done"
+ },
+ {
+ "label": "T03",
+ "uuid": "dab2c07f-8aa0-4635-8df6-857e87e93fc5",
+ "status": "done"
+ },
+ {
+ "label": "T04",
+ "uuid": "5c3d40e4-239b-488e-9519-6f7a38d2325f",
+ "status": "done"
+ },
+ {
+ "label": "T05",
+ "uuid": "7f9e0e58-a130-467a-a2d0-b3f2564e496f",
+ "status": "done"
+ }
+ ]
+ },
+ {
+ "repo": "railiance-cluster",
+ "path": "workplans/archived/260622-RAIL-BS-WP-0003-pgpool-ha-failover-fix.md",
+ "sha256": "4dc2f61e15adc3c5c81f9ecf8b933512275b95e8f829fac52ae7cde1385cea4a",
+ "record_id": "RAIL-BS-WP-0003",
+ "uuid": "7ee9ee22-1fae-4567-9194-8d70a9e0f45b",
+ "parent_record_id": "RAIL-BS-WP-0003",
+ "tasks": [
+ {
+ "label": "T01",
+ "uuid": "6841c93a-f146-47eb-9f7c-8fa0e02c1bbc",
+ "status": "done"
+ },
+ {
+ "label": "T02",
+ "uuid": "ab166073-30a7-4702-a037-4091e8706e20",
+ "status": "done"
+ },
+ {
+ "label": "T03",
+ "uuid": "140da396-8e30-4f4d-b88c-c42c0cd46c01",
+ "status": "done"
+ },
+ {
+ "label": "T04",
+ "uuid": "d8a3ba40-fda0-4c1f-a9f1-ffcd621a5b3d",
+ "status": "done"
+ }
+ ]
+ },
+ {
+ "repo": "railiance-cluster",
+ "path": "workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md",
+ "sha256": "04f787078232395bd2d43861de16c521848559fa286f06e1caa9cb779aa71ac3",
+ "record_id": "RAIL-BS-WP-0004",
+ "uuid": "7e8b0c20-51eb-40c9-9e3b-85dd380d7625",
+ "parent_record_id": "RAIL-BS-WP-0004",
+ "tasks": [
+ {
+ "label": "T01",
+ "uuid": "4526a842-ea31-4874-9231-92ab556cfe7b",
+ "status": "done"
+ },
+ {
+ "label": "T02",
+ "uuid": "a6313e06-1976-46a7-8e31-df4eb2eca880",
+ "status": "done"
+ },
+ {
+ "label": "T03",
+ "uuid": "05d42a55-921f-4aa7-bb76-e8af9c7e0ac3",
+ "status": "done"
+ },
+ {
+ "label": "T04",
+ "uuid": "08233868-d522-4117-bc4e-6c0f52545665",
+ "status": "done"
+ },
+ {
+ "label": "T05",
+ "uuid": "2d5acff7-4a4e-4ddd-ad06-08237ad3dac8",
+ "status": "done"
+ },
+ {
+ "label": "T06",
+ "uuid": "f8e4a094-c367-40eb-b895-da17bc144b07",
+ "status": "done"
+ }
+ ]
}
]''')
@@ -83,3 +188,24 @@
raise ValueError("Historical source binding requires owner review: " + entry["path"])
paths.add(entry["path"])
return paths
+
+
+def historical_source_occurrences(repo_root):
+ """Exact frozen closed task occurrences; never a general local-ID alias."""
+ from repo_manager.parse.workplan import parse_workplan_file
+ from uuid import UUID
+ paths=historical_source_paths(repo_root)
+ occurrences=set(paths)
+ for entry in BINDINGS:
+ if entry['repo']!=repo_root.name or 'tasks' not in entry:continue
+ parsed=parse_workplan_file(repo_root/entry['path'],repo_root=repo_root)
+ actual=[{'label':task.id,'uuid':task.state_hub_task_id,'status':task.status} for task in parsed.tasks]
+ if (parsed.id!=entry['record_id'] or parsed.state_hub_workstream_id!=entry['uuid']
+ or parsed.status not in {'finished','archived','completed'} or actual!=entry['tasks']
+ or any(task['status'] not in {'done','cancel'} for task in actual)):
+ raise ValueError('Historical task binding requires owner review: '+entry['path'])
+ UUID(entry['uuid'])
+ for number,task in enumerate(actual,1):
+ UUID(task['uuid'])
+ occurrences.add(entry['path']+'#task-block-'+str(number))
+ return occurrences
--- a/src/repo_manager/identifiers.py
+++ b/src/repo_manager/identifiers.py
@@ -854,8 +854,9 @@
# allocation candidates. Keep conformance reporting strict while allowing
# ordinary registration to preserve those bindings. Local aliases still
# require the explicit parent allowlist in the kind registry.
- from repo_manager.historical_sources import historical_source_paths
- historical_sources: set[str] = historical_source_paths(repo_root)
+ from repo_manager.historical_sources import historical_source_occurrences
+ frozen_sources = historical_source_occurrences(repo_root)
+ historical_sources: set[str] = set(frozen_sources)
for path in iter_workplan_files(repo_root):
parsed = parse_workplan_file(path, repo_root=repo_root)
if parsed.status not in {"finished", "archived", "completed"} or not parsed.id:
@@ -889,11 +890,18 @@
item for item in identity["invalid_identifiers"]
if item["source"] in historical_sources
]
- if invalid or identity["identity_collisions"]:
+ # Strict scans keep legacy nonconformance. Only collisions wholly contained
+ # in exact hash-bound closed task occurrences can be preserved by receiving;
+ # any unknown/live source or qualified/workplan collision remains fatal.
+ historical_collisions=[item for item in identity['identity_collisions']
+ if item['kind']=='task' and re.fullmatch(r'T[0-9]{2}',item['id'])
+ and all(source in frozen_sources for source in item['sources'])]
+ collisions=[item for item in identity['identity_collisions'] if item not in historical_collisions]
+ if invalid or collisions:
raise ValueError(
"work-record identities are not safe to derive: "
f"invalid={invalid!r}, "
- f"collisions={identity['identity_collisions']!r}"
+ f"collisions={collisions!r}"
)
rendered: dict[Path, str] = {}
@@ -966,6 +974,7 @@
"namespace": namespace,
"assignments": assignments,
"historical_invalid_identifiers": historical,
+ "historical_identity_collisions": historical_collisions,
"files_changed": sorted(str(path.relative_to(repo_root)) for path in rendered),
}
--- /dev/null
+++ b/tests/test_cluster_frozen_history.py
@@ -0,0 +1,52 @@
+import shutil
+from pathlib import Path
+import pytest
+from repo_manager.identifiers import ensure_missing_work_record_identifiers as ensure
+from repo_manager.record_identity import scan_record_identities
+
+FIXTURE=Path(__file__).parent/'fixtures/railiance-cluster'
+@pytest.fixture
+def repo(tmp_path):
+ root=tmp_path/'railiance-cluster';shutil.copytree(FIXTURE,root)
+ live=root/'workplans/RCLUSTER-WP-0018.md';live.write_text('---\nid: RCLUSTER-WP-0018\nstatus: ready\n---\n\n```task\nid: RCLUSTER-WP-0018-T01\nstatus: todo\n```\n')
+ return root,live
+
+def test_exact_three_histories_preserve_15_bindings_and_strict_findings(repo):
+ root,live=repo;archive={p:p.read_bytes() for p in (root/'workplans/archived').glob('*.md')};before=scan_record_identities(root)
+ assert len(before['invalid_identifiers'])==15 and len(before['identity_collisions'])==5
+ preview=ensure(root,execute=False);assert len(preview['assignments'])==2
+ assert preview['historical_invalid_identifiers']==before['invalid_identifiers']
+ assert preview['historical_identity_collisions']==before['identity_collisions']
+ result=ensure(root,execute=True);assert len(result['assignments'])==2
+ assert all(p.read_bytes()==raw for p,raw in archive.items())
+ assert not scan_record_identities(root)['ok']
+ assert ensure(root,execute=False)['assignments']==[]
+
+@pytest.mark.parametrize('change',['bytes','missing','symlink','live','nonterminal','uuid'])
+def test_unknown_history_change_refuses_before_any_allocation(repo,change):
+ root,live=repo;p=next((root/'workplans/archived').glob('*.md'));before=live.read_bytes()
+ raw=p.read_bytes()
+ if change=='missing':p.unlink()
+ elif change=='symlink':p.unlink();target=root/'saved';target.write_bytes(raw);p.symlink_to(target)
+ elif change=='live':p.write_bytes(raw.replace(b'status: finished',b'status: active',1))
+ elif change=='nonterminal':p.write_bytes(raw.replace(b'status: done',b'status: todo',1))
+ elif change=='uuid':p.write_bytes(raw.replace(b'state_hub_task_id:',b'old_task_id:',1))
+ else:p.write_bytes(raw+b'\nunreviewed change\n')
+ with pytest.raises(ValueError):ensure(root)
+ assert live.read_bytes()==before
+
+def test_any_unknown_or_live_source_in_collision_still_refuses(repo):
+ root,live=repo;before=live.read_bytes();other=root/'workplans/unknown.md'
+ other.write_text('---\nid: RCLUSTER-WP-0099\nstatus: active\n---\n\n```task\nid: T01\nstatus: todo\nstate_hub_task_id: 11111111-1111-4111-8111-111111111111\n```\n')
+ with pytest.raises(ValueError,match='identities are not safe'):ensure(root)
+ assert live.read_bytes()==before
+
+def test_other_repo_does_not_inherit_exact_frozen_exception(repo):
+ root,live=repo;other=root.with_name('other');root.rename(other)
+ with pytest.raises(ValueError,match='identities are not safe'):ensure(other)
+
+def test_qualified_collision_stays_global(repo):
+ root,live=repo;before=live.read_bytes()
+ (root/'workplans/other.md').write_text('---\nid: RCLUSTER-WP-0019\nstatus: active\n---\n\n```task\nid: RCLUSTER-WP-0018-T01\nstatus: done\nstate_hub_task_id: 11111111-1111-4111-8111-111111111111\n```\n')
+ with pytest.raises(ValueError,match='collisions='):ensure(root)
+ assert live.read_bytes()==before
--- a/workplans/RMGR-WP-0019-historical-source-admission.md
+++ b/workplans/RMGR-WP-0019-historical-source-admission.md
@@ -4,10 +4,10 @@
title: "Admit exact historical source views after receiving preservation support"
domain: infotech
repo: repo-manager
-status: finished
+status: active
owner: codex
created: "2026-10-04"
-updated: "2026-10-06"
+updated: "2026-10-17"
flavor: implementation
depends_on: []
state_hub_workstream_id: "efe0ecb2-2f3f-5f49-baee-b90ad1d353ab"
@@ -146,3 +146,62 @@
primary/railiance01, applied, 0 created / 45 updated (ACTIVITY-WP only) / 0 retired
/ 0 refused, 0 task changes, historical UUIDs preserved. Evidence:
state-hub docs/evidence/2026-10-06-activity-stale-retirement-rollout.json.
+
+
+## Cluster exact historical source successor, 2026-10-17
+
+This scoped extension preserves the completed Activity/Custodian work above.
+The cluster publisher now refuses three closed RAIL-BS source files containing
+15 existing UUID-bound bare task labels. Preserve all three source bytes, IDs,
+and UUIDs. No new alias or general syntax exemption is introduced. Evidence and
+exact receiver-first proposal: cluster-historical-publication-r1 in the Custodian
+NetKingdom change packet. The existing SHR-WP-0001 project driver owns the
+Repo Manager / State Hub cross-owner wait; NK-WP-0049-T35 benefits from the
+blocked cluster publication. No worker or human gate is woken here.
+
+## Prepare exact cluster registrar successor
+
+```task
+id: RMGR-WP-0019-T04
+status: done
+priority: high
+```
+
+Prepared full-file SHA256 bindings for three archives and 15 exact task
+occurrences; strict scans still return 15 invalid labels and five collisions.
+Unknown/live sources, changed/missing/symlinked histories, UUID drift and
+qualified/global collisions refuse before allocation. Candidate tests pass 47;
+canonical application remains a separate reviewed step. The read-only current
+cluster preview proposes seven ordinary UUID bindings in two files and changes
+no bytes. One pre-existing ADHOC field is included explicitly: task
+RCLUSTER-WP-ADHOC-2026-07-27-T01 in workplans/ADHOC-2026-07-27.md receives
+3e3f39d3-5070-5ba3-97db-19f5f361f264 through managed allocation only.
+
+## Admit cluster registrar successor only after receiving verification
+
+```task
+id: RMGR-WP-0019-T05
+status: wait
+priority: high
+depends_on: [STATE-WP-0095-T06]
+```
+
+Retain the current guard until exact reviewed receiving source has been delivered
+and verified through State Hub's normal owner deployment. Match both manifests,
+then integrate reviewed registrar source and verify ordinary allocator preview.
+No direct API patch, history rename or bypass substitutes for this gate.
+
+## Run ordinary cluster owner publication and verify preservation
+
+```task
+id: RMGR-WP-0019-T06
+status: wait
+priority: high
+depends_on: [RMGR-WP-0019-T05]
+```
+
+Only then run normal owner consistency/publication with the reviewed expected
+file set: RCLUSTER-WP-0018 and the one-field ADHOC managed backfill, plus ordinary
+generated indexes. Preserve all three archived files and all 18 existing
+parent/task rows. Return actual receiving/publication receipts to STATE-WP-0095-T07.
+Do not infer TLS recovery, credential custody or native acceptance from sync.
--- /dev/null
+++ b/tests/fixtures/railiance-cluster/workplans/archived/260622-RAIL-BS-WP-0002-k3s-baseline.md
@@ -0,0 +1,177 @@
+---
+id: RAIL-BS-WP-0002
+type: workplan
+title: "k3s and Kubernetes Platform Baseline"
+domain: financials
+repo: railiance-cluster
+status: finished
+owner: railiance
+topic_slug: railiance
+repo_goal_id: "70ab2379-fb9d-4fec-a09d-b2a717e4ace8"
+state_hub_workstream_id: "4c63dfc6-9eac-4e79-9f77-8f644ad7147d"
+created: "2026-03-09"
+updated: "2026-03-10"
+completed: "2026-03-10"
+---
+
+> **Terminology note:** Historical text in this archived workplan may use the legacy term "workstream". The fleet term is **workplan** (`canon/standards/workplan-terminology-fleet_v0.1.md`).
+
+# k3s and Kubernetes Platform Baseline
+
+## Goal
+
+Install k3s, Helm, and the baseline Kubernetes services on the converged
+HostEurope node. This workplan picks up exactly where `railiance-hosts`
+leaves off: a hardened, verified OS node that is ready for Kubernetes.
+
+## Pre-condition
+
+`railiance-infra` converge + Goss verify must pass before any task here
+is executed:
+
+```bash
+# In railiance-infra/
+make converge
+make verify # must exit 0
+```
+
+## Boundary
+
+This repo owns everything from k3s upward. It must not re-configure items
+defined in `railiance-infra/spec/server-baseline.yaml`. See ADR-003:
+`railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md`.
+
+**Out of scope here:** platform services (PostgreSQL, storage, identity)
+→ `railiance-platform`. Application deployments (Gitea, coulomb services)
+→ `railiance-apps`.
+
+---
+
+## Tasks
+
+### T01 — Ansible playbook: install k3s (server mode)
+
+```task
+id: T01
+status: done
+priority: high
+state_hub_task_id: "3f042630-eab0-4c6a-9167-e2b28ff20e40"
+completed: "2026-03-10"
+```
+
+Harden `ansible/bootstrap.yml` to a production-ready k3s install:
+
+- Use the official k3s install script pinned to a specific version
+ (`INSTALL_K3S_VERSION=v1.35.1+k3s1`)
+- `INSTALL_K3S_EXEC="server --cluster-init --write-kubeconfig-mode=644"`
+ (cluster-init enables embedded etcd for future HA expansion)
+- Wait for node `Ready` before proceeding:
+ ```bash
+ k3s kubectl wait node --all --for=condition=Ready --timeout=120s
+ ```
+- Fetch kubeconfig to the control node as `~/.kube/config-hosteurope`
+
+**Done when:** `k3s kubectl get nodes` returns `Ready` from both the server
+and the control node (via kubeconfig).
+
+---
+
+### T02 — Helm installation
+
+```task
+id: T02
+status: done
+priority: high
+state_hub_task_id: "e8510646-46ed-4697-a345-f3d3009eea78"
+completed: "2026-03-10"
+```
+
+Add a task (or a role `roles/helm/`) that:
+
+1. Downloads the Helm binary (pinned version) to `/usr/local/bin/helm`
+2. Verifies the checksum
+3. Confirms `helm version` succeeds
+
+**Done when:** `helm version` succeeds on the HostEurope node.
+
+---
+
+### T03 — Smoke test: k3s + Helm
+
+```task
+id: T03
+status: done
+priority: high
+state_hub_task_id: "dab2c07f-8aa0-4635-8df6-857e87e93fc5"
+completed: "2026-03-10"
+```
+
+Extend `tests/smoke_kube.sh` to assert:
+
+- `k3s kubectl get nodes` → node in Ready state
+- `helm version` exits 0
+- CoreDNS pod running in `kube-system`
+- Traefik ingress controller pod running (default in k3s)
+
+Run via:
+```bash
+ansible-playbook -i ansible/hosts.ini ansible/smoke.yml
+```
+or directly over SSH if the kubeconfig is available locally.
+
+**Done when:** all assertions pass and the script exits 0.
+
+---
+
+### T04 — Commit kubeconfig management notes
+
+```task
+id: T04
+status: done
+priority: medium
+state_hub_task_id: "5c3d40e4-239b-488e-9519-6f7a38d2325f"
+completed: "2026-03-10"
+```
+
+Document in `docs/kubeconfig.md`:
+
+- Where the kubeconfig is fetched to (`~/.kube/config-hosteurope`)
+- How to merge it into `~/.kube/config`
+- How to switch context: `kubectl config use-context default`
+- Security note: kubeconfig is gitignored (contains cluster CA + client cert)
+
+**Done when:** doc written and committed.
+
+---
+
+### T05 — Add `make k3s-install` and `make smoke` targets
+
+```task
+id: T05
+status: done
+priority: medium
+state_hub_task_id: "7f9e0e58-a130-467a-a2d0-b3f2564e496f"
+completed: "2026-03-10"
+```
+
+Add to Makefile (create one if none exists):
+
+```makefile
+k3s-install: ## Install k3s and Helm on all inventory hosts
+ ansible-playbook -i ansible/hosts.ini ansible/bootstrap.yml
+
+smoke: ## Run Kubernetes smoke tests
+ bash tests/smoke_kube.sh
+```
+
+**Done when:** both targets work and are listed in `make help`.
+
+---
+
+## References
+
+- Repo goal: `70ab2379-fb9d-4fec-a09d-b2a717e4ace8` (Install k3s and Kubernetes Baseline)
+- Domain goal: `6f96c712-60e6-4ea9-ab06-168878eafbce` (Three-Phoenix Secure Kubernetes Infrastructure)
+- Pre-condition: railiance-infra WP-0001 (Secure Single-Server Bootstrap) — completed 2026-03-09
+- Boundary ADR: `railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md`
+- k3s releases: https://github.com/k3s-io/k3s/releases
--- /dev/null
+++ b/tests/fixtures/railiance-cluster/workplans/archived/260622-RAIL-BS-WP-0003-pgpool-ha-failover-fix.md
@@ -0,0 +1,196 @@
+---
+id: RAIL-BS-WP-0003
+type: workplan
+title: "pgpool CrashLoopBackOff on PostgreSQL HA failover — missing secret key"
+domain: financials
+repo: railiance-cluster
+status: finished
+owner: tegwick
+created: "2026-03-10"
+updated: "2026-03-10"
+state_hub_workstream_id: "7ee9ee22-1fae-4567-9194-8d70a9e0f45b"
+---
+
+> **Terminology note:** Historical text in this archived workplan may use the legacy term "workstream". The fleet term is **workplan** (`canon/standards/workplan-terminology-fleet_v0.1.md`).
+
+# Bug Report: pgpool CrashLoopBackOff on PostgreSQL HA failover
+
+## Summary
+
+On 2026-03-10 a PostgreSQL HA failover caused all three postgresql pods to
+restart. pgpool — the connection pooler between Gitea and PostgreSQL — then
+entered CrashLoopBackOff and produced no logs. As a result Gitea's login
+and all write operations hung indefinitely. The root page was still served
+(from Valkey cache) which masked the failure.
+
+The fix was to patch a missing key in a Kubernetes secret. The root cause is
+that the `gitea-12.2.0` Helm chart (postgresql-ha subchart v16.2.2) does not
+populate the `pgpool-password` key in the `gitea-postgresql-ha-postgresql`
+secret, even though the pgpool pod requires it at startup.
+
+---
+
+## Timeline
+
+| Time (UTC) | Event |
+|---|---|
+| ~09:45 | postgresql-0, postgresql-2 pods restarted (repmgr failover) |
+| ~09:45 | pgpool pod restarted and entered CrashLoopBackOff |
+| ~11:00 | User noticed Gitea login hanging; home page still loading |
+| ~13:00 | Root cause identified: missing `pgpool-password` secret key |
+| ~13:10 | Secret patched; pgpool pod deleted and restarted cleanly |
+| ~13:15 | Gitea fully operational |
+
+---
+
+## Root Cause
+
+The Bitnami `pgpool` container startup script reads the file
+`/opt/bitnami/pgpool/secrets/pgpool-password`, which is mounted from the
+`gitea-postgresql-ha-postgresql` Kubernetes Secret via a `subPath` volume
+mount. That secret key was never created by the Helm chart, so the file did
+not exist. The container exited immediately with no logs.
+
+The pod had been running for 20 days without a restart, so this gap was
+never discovered during initial deployment.
+
+---
+
+## Evidence
+
+```bash
+# Secret was missing the pgpool-password key
+sudo k3s kubectl get secret -n default gitea-postgresql-ha-postgresql -o yaml
+# data: keys were password, postgres-password, repmgr-password only
+# pgpool-password was absent
+
+# pgpool pod describe showed 824 back-off restarts over 173 minutes
+# No logs in either current or --previous output
+sudo k3s kubectl logs -n default <pgpool-pod> --previous
+# (empty)
+
+# Gitea process had zero TCP connections to PostgreSQL port 5432
+# but many connections to Valkey port 6379
+cat /proc/<gitea-pid>/net/tcp | grep 1538 # 1538 = 5432 hex — no results
+```
+
+---
+
+## Immediate Fix Applied
+
+```bash
+# Add the missing key (value = sr-check-password = changeme4 = base64: Y2hhbmdlbWU0)
+sudo k3s kubectl patch secret -n default gitea-postgresql-ha-postgresql \
+ --type='json' \
+ -p='[{"op":"add","path":"/data/pgpool-password","value":"Y2hhbmdlbWU0"}]'
+
+# Restart pgpool
+sudo k3s kubectl delete pod -n default <pgpool-pod-name>
+```
+
+---
+
+## Risk: Fix Will Be Lost on helm upgrade
+
+The patched secret is managed by Helm (annotation:
+`meta.helm.sh/release-name: gitea`). A `helm upgrade` will regenerate the
+secret from the chart template, which does not include `pgpool-password`,
+and the bug will recur.
+
+---
+
+## Tasks
+
+### T01 — Add pgpool-password to Helm values
+
+```task
+id: T01
+status: done
+priority: high
+state_hub_task_id: "6841c93a-f146-47eb-9f7c-8fa0e02c1bbc"
+```
+
+Create or update `helm/gitea-values.yaml` (or equivalent) to permanently
+include the pgpool-password so it survives `helm upgrade`:
+
+```yaml
+postgresql-ha:
+ postgresql:
+ pgpoolPassword: <value matching sr-check-password>
+```
+
+**Done when:** `helm upgrade gitea` completes and pgpool starts cleanly
+without manual secret patching.
+
+---
+
+### T02 — Add pgpool health check to smoke test
+
+```task
+id: T02
+status: done
+priority: high
+state_hub_task_id: "ab166073-30a7-4702-a037-4091e8706e20"
+```
+
+Extend `tests/smoke_kube.sh` to assert:
+
+```bash
+# All postgresql-ha pods Running
+kubectl get pods -n default | grep gitea-postgresql-ha | grep -v Running && exit 1
+
+# pgpool specifically not in CrashLoopBackOff
+kubectl get pod -n default -l app.kubernetes.io/component=pgpool \
+ -o jsonpath='{.items[0].status.containerStatuses[0].state}' | grep -v crash
+```
+
+**Done when:** the smoke test catches a pgpool failure within 5 minutes.
+
+---
+
+### T03 — Add HA failover test
+
+```task
+id: T03
+status: done
+priority: high
+state_hub_task_id: "140da396-8e30-4f4d-b88c-c42c0cd46c01"
+```
+
+Create `tests/test_ha_failover.sh` that:
+
+1. Records Gitea login response time (baseline)
+2. Kills the primary PostgreSQL pod: `kubectl delete pod gitea-postgresql-ha-postgresql-0 -n default`
+3. Waits for repmgr to promote a replica (max 60s)
+4. Asserts Gitea login POST still succeeds within 10s
+5. Asserts pgpool pod is Running (not CrashLoopBackOff)
+6. Asserts all postgresql pods return to Running
+
+This test must pass before any PostgreSQL HA deployment is considered done.
+
+**Done when:** script exits 0 against a live cluster.
+
+---
+
+### T04 — Document the incident in docs/
+
+```task
+id: T04
+status: done
+priority: medium
+state_hub_task_id: "d8a3ba40-fda0-4c1f-a9f1-ffcd621a5b3d"
+```
+
+Add `docs/incidents/2026-03-10-pgpool-missing-secret.md` with the full
+timeline, root cause, and fix, so future operators understand what happened
+and how to recover.
+
+**Done when:** doc committed and linked from `docs/README.md`.
+
+---
+
+## References
+
+- Bitnami postgresql-ha chart v16.2.2
+- Gitea Helm chart v12.2.0
+- Related decision: D3 (HA testing policy) in `DECISIONS.md`
--- /dev/null
+++ b/tests/fixtures/railiance-cluster/workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md
@@ -0,0 +1,275 @@
+---
+id: RAIL-BS-WP-0004
+type: workplan
+title: "Integrated Backup — S2 Kubernetes Runtime Layer"
+domain: financials
+repo: railiance-cluster
+status: finished
+owner: tegwick
+topic_slug: railiance
+state_hub_workstream_id: "7e8b0c20-51eb-40c9-9e3b-85dd380d7625"
+created: "2026-02-25"
+updated: "2026-03-26"
+---
+
+> **Terminology note:** Historical text in this archived workplan may use the legacy term "workstream". The fleet term is **workplan** (`canon/standards/workplan-terminology-fleet_v0.1.md`).
+
+# Integrated Backup — S2 Kubernetes Runtime Layer
+
+## Goal
+
+Implement the Q3 (Operability & Resilience) integrated backup for
+railiance-cluster (S2). Backs up what S2 owns — the Kubernetes runtime state —
+encrypted with age, written to a local directory on the server. No external
+dependencies required.
+
+## Architecture (Decision D4)
+
+Each railiance repo implements its own backup for what it owns. No central
+backup service. See `DECISIONS.md` D4 for full rationale.
+
+**Standard interface every railiance repo must provide:**
+
+```bash
+make backup # encrypt + write to /opt/backup/railiance/<layer>/
+make restore # restore from most recent local backup
+```
+
+Encryption: age, same key pair as SOPS secrets (`.sops.yaml` public key).
+Output: `/opt/backup/railiance/cluster/` on the server.
+
+## What S2 (railiance-cluster) owns and must back up
+
+| Asset | Why it matters |
+|---|---|
+| k3s etcd snapshots | Full cluster state — all workloads, configs, secrets |
+| Helm release values | Runtime values not in git (any manually applied overrides) |
+| kubeconfig | Admin access to the cluster |
+
+**Not S2's responsibility:**
+- Custodian State Hub DB → the-custodian owns this
+- Operator workstation config (`.claude/`, `.gitconfig`) → operator's own concern
+- Application data (Gitea repos, uploads) → S5 (railiance-apps) owns this
+- PostgreSQL data volumes → S3 (railiance-platform) owns this
+
+## Encryption
+
+Reuse the age public key from `.sops.yaml`:
+
+```bash
+AGE_PUBLIC_KEY=$(grep 'age:' .sops.yaml | awk '{print $2}')
+tar -czf - <assets> | age -r "${AGE_PUBLIC_KEY}" -o backup.tar.gz.age
+```
+
+Decryption requires the private key at `~/.config/sops/age/keys.txt`
+(same key used for `sops -d`). No additional key management needed.
+
+## Extension Point EP-RAIL-005
+
+Once all five OAS layers implement this interface, the custodian can
+orchestrate a full-stack backup with:
+
+```bash
+for repo in railiance-infra railiance-cluster railiance-platform \
+ railiance-enablement railiance-apps; do
+ make -C ~/$repo backup
+done
+```
+
+No special protocol needed — just the standard interface.
+
+---
+
+## Tasks
+
+### T01 — Define backup directory and encryption wrapper
+
+```task
+id: T01
+status: done
+priority: high
+state_hub_task_id: "4526a842-ea31-4874-9231-92ab556cfe7b"
+```
+
+Create `tools/cmd/railiance-backup-s2` (replacing the old `railiance-backup`):
+
+- Backup dir: `/opt/backup/railiance/cluster/` (create with `mkdir -p`)
+- Encrypt each artifact with age using public key from `.sops.yaml`
+- Write timestamp-named files: `etcd-<ts>.snap.age`, `helm-values-<ts>.tar.gz.age`, `kubeconfig-<ts>.yaml.age`
+- Keep last 7 of each type
+- Write `.last-backup` stamp
+- Exit 0 on success, non-zero on any failure
+- No network required
+
+Also remove the old `tools/cmd/railiance-backup` (backed up Docker-based
+custodian DB — wrong scope, not applicable to this server).
+
+**Done when:** `make backup` runs on COULOMBCORE without error and files
+appear in `/opt/backup/railiance/cluster/`.
+
+---
+
+### T02 — Back up k3s state (SQLite hot backup)
+
+```task
+id: T02
+status: done
+priority: high
+state_hub_task_id: "a6313e06-1976-46a7-8e31-df4eb2eca880"
+```
+
+k3s has built-in etcd snapshot support:
+
+```bash
+sudo k3s etcd-snapshot save --name railiance-$(date -u +%Y%m%dT%H%M%SZ)
+# Default location: /var/lib/rancher/k3s/server/db/snapshots/
+```
+
+Add to the backup script: take a fresh snapshot, encrypt with age,
+copy to `/opt/backup/railiance/cluster/`.
+
+> **Note — verify etcd is in use before implementing:**
+> `k3s etcd-snapshot` only works if k3s was started with `--cluster-init`.
+> Without it, k3s uses SQLite and this command will fail.
+> Verify first: `sudo k3s etcd-snapshot ls 2>&1`
+
+> **Note — sudo required:** etcd snapshot requires root. See T06 for how
+> this is resolved (backup runs under root's crontab).
+
+**Done when:** backup includes a current etcd snapshot.
+
+---
+
+### T03 — Back up Helm release values
+
+```task
+id: T03
+status: done
+priority: medium
+state_hub_task_id: "05d42a55-921f-4aa7-bb76-e8af9c7e0ac3"
+```
+
+Capture current runtime Helm values for all releases:
+
+```bash
+KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm list -A -o json | \
+ jq -r '.[].name + " " + .namespace' | \
+ while read name ns; do
+ KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm get values "$name" -n "$ns" -o yaml
+ done
+```
+
+Tar and age-encrypt into `helm-values-<ts>.tar.gz.age`.
+
+> **Note — kubeconfig permissions:** `/etc/rancher/k3s/k3s.yaml` is root-readable
+> only by default. The backup script must either run as root (see T06) or k3s
+> must be configured with `--write-kubeconfig-mode=644`. Running as root
+> (via root crontab) is the chosen approach — no config change needed.
+
+**Done when:** backup includes a snapshot of all Helm release values.
+
+---
+
+### T04 — Back up kubeconfig
+
+```task
+id: T04
+status: done
+priority: medium
+state_hub_task_id: "08233868-d522-4117-bc4e-6c0f52545665"
+```
+
+Age-encrypt `~/.kube/config-hosteurope` (or `/etc/rancher/k3s/k3s.yaml`)
+into `kubeconfig-<ts>.yaml.age` in the backup directory.
+
+**Done when:** backup includes the encrypted kubeconfig.
+
+---
+
+### T05 — make restore target
+
+```task
+id: T05
+status: done
+priority: medium
+state_hub_task_id: "2d5acff7-4a4e-4ddd-ad06-08237ad3dac8"
+```
+
+Add `tools/cmd/railiance-restore-s2` that decrypts and lists available
+backups, with guided restore for the etcd snapshot case.
+
+Restore of etcd from snapshot:
+```bash
+sudo k3s server --cluster-reset \
+ --cluster-reset-restore-path=/var/lib/rancher/k3s/server/db/snapshots/<name>
+```
+
+**Done when:** `make restore` prints available backups and a restore guide.
+
+---
+
+### T06 — Install cron job and run restore drill
+
+```task
+id: T06
+status: done
+priority: medium
+state_hub_task_id: "f8e4a094-c367-40eb-b895-da17bc144b07"
+```
+
+#### Solving the sudo problem
+
+The backup script needs root for two reasons:
+- `k3s etcd-snapshot save` requires root
+- `/etc/rancher/k3s/k3s.yaml` (kubeconfig) is root-readable only
+
+**Solution: run the cron under root's crontab.**
+
+This is the correct pattern for system-level backup jobs. It avoids a
+proliferating sudoers whitelist (one entry per command, brittle to maintain)
+and matches how tools like `rsnapshot`, `bacula`, and `borgbackup` work in
+production. The backup writes to `/opt/backup/` which is root-owned anyway.
+
+Install the cron as root:
+
+```bash
+sudo crontab -e
+# Add:
+0 2 * * * make -C /home/tegwick/railiance-cluster backup >> /opt/backup/railiance/cluster/backup.log 2>&1
+```
+
+Note: use the absolute path to the repo — `~` does not expand reliably in
+root's crontab unless HOME is set.
+
+Verify it is installed:
+```bash
+sudo crontab -l | grep railiance
+```
+
+#### Restore drill
+
+Once T01–T04 are done, run a decrypt-and-verify drill:
+
+```bash
+# Decrypt the etcd snapshot and verify it is a valid snapshot file
+sudo age -d -i ~/.config/sops/age/keys.txt \
+ /opt/backup/railiance/cluster/etcd-$(ls /opt/backup/railiance/cluster/etcd-*.snap.age | sort -r | head -1 | xargs basename | sed 's/etcd-//;s/.snap.age//').snap.age \
+ | file -
+
+# Record the drill
+echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) restore drill OK" \
+ >> /opt/backup/railiance/cluster/restore-drill.log
+```
+
+**Done when:** cron installed under root, drill completes without error,
+log entry written.
+
+---
+
+## References
+
+- Decision D4: Integrated backup per capability (`DECISIONS.md`)
+- Decision D2: Nextcloud as optional offsite extension (still valid, not a requirement)
+- OAS Q3: Operability & Resilience
+- Extension point EP-RAIL-005: Custodian full-stack backup orchestration
+- k3s etcd snapshots: https://docs.k3s.io/datastore/backup-restore

View file

@ -0,0 +1,6 @@
schema: repo-manager.assistant-provenance.v1
cutover_commit: 329af60753b20896f30f098e8f75074652780032
known_automation:
- name: custodian-sync
email: custodian-sync@railiance.local
trailer_required: false

View file

@ -0,0 +1,10 @@
# Example dual-run pilot config (RMGR-WP-0003).
# Install: cp config/dual-run.pilot.example.yaml ~/.repo-manager/dual-run.yaml
# Env vars still override when set. Rollback: writeback/reconcile false or delete file.
writeback: true
reconcile: true
writeback_push: false # set true only if pilot checkouts can push safely
pilot_repos:
- repo-manager
meter_path: ~/.repo-manager/mutation-meter.jsonl

View file

@ -0,0 +1,12 @@
schema: repo-manager.fleet-namespace.v1
namespace: helixforge
scope: HelixForge fleet
lineage:
kind: root
parent: null
posture:
current: {N: 2, U: 1, R: 1, S: 1}
target: {N: 3, U: 2, R: 2, S: 1}
declared_by: Bernd Worsch
declared: "2026-08-21"
decision_ref: RMGR-WP-0005-T03

View file

@ -0,0 +1,71 @@
schema: repo-manager.hub-record-authority.v1
updated: "2026-08-21"
workplan_task: RMGR-WP-0005-T08
routing:
file-derived-key: "repository:<domain_slug>/<repo_slug>"
hub-native-key: "hub:<owner>"
retired-key: "archive:<record_type>"
unknown-record-policy: reject
claimed-owner-mismatch-policy: reject
classes:
file-derived:
authority: repository
offline: write-and-commit-source-file
hub-native-append:
authority: hub-core
offline: append-to-idempotent-outbox
hub-native-control:
authority: owning-central-hub
offline: no-generic-replay
derived-cache:
authority: upstream-source
offline: rebuild-or-refresh
retired:
authority: archived-provenance
offline: reject-writes
records:
agent_messages: {class: hub-native-control, owner: hub-core}
capability_catalog: {class: file-derived, owner: repo-manager, transition: retire-direct-db-writes}
capability_requests: {class: file-derived, owner: repo-manager, transition: register-entry-files}
contributions: {class: file-derived, owner: repo-manager}
decisions: {class: file-derived, owner: repo-manager, transition: retire-direct-db-writes}
doi_cache: {class: derived-cache, owner: repo-manager}
domain_goals: {class: hub-native-control, owner: hub-core}
domains: {class: hub-native-control, owner: hub-core}
extension_points: {class: file-derived, owner: repo-manager}
fabric_graph_edges: {class: derived-cache, owner: fin-hub}
fabric_graph_imports: {class: derived-cache, owner: fin-hub}
fabric_graph_nodes: {class: derived-cache, owner: fin-hub}
intake_notes: {class: file-derived, owner: repo-manager, transition: retire-direct-db-writes}
intakes: {class: file-derived, owner: repo-manager, transition: retire-direct-db-writes}
interface_changes: {class: hub-native-control, owner: hub-core}
legacy_interface_usage_buckets: {class: derived-cache, owner: hub-core}
legacy_interfaces: {class: derived-cache, owner: hub-core}
managed_repos: {class: file-derived, owner: repo-manager, note: composite-repository-projection}
progress_events: {class: hub-native-append, owner: hub-core}
repo_goals: {class: file-derived, owner: repo-manager}
sbom_entries: {class: derived-cache, owner: repo-manager}
sbom_snapshots: {class: derived-cache, owner: repo-manager}
service_catalog: {class: hub-native-control, owner: ops-hub}
service_cloud: {class: hub-native-control, owner: ops-hub}
service_first_party: {class: hub-native-control, owner: ops-hub}
service_self_hosted: {class: hub-native-control, owner: ops-hub}
service_third_party: {class: hub-native-control, owner: ops-hub}
suggestion_notes: {class: retired, owner: archive}
suggestion_relevance_bumps: {class: retired, owner: archive}
suggestions: {class: retired, owner: archive}
tasks: {class: file-derived, owner: repo-manager}
td_notes: {class: file-derived, owner: repo-manager}
technical_debt: {class: file-derived, owner: repo-manager}
token_events: {class: hub-native-append, owner: hub-core}
topics: {class: hub-native-control, owner: hub-core, transition: retire-legacy-topic-spine}
tpsc_catalog: {class: hub-native-control, owner: ops-hub}
tpsc_entries: {class: derived-cache, owner: repo-manager}
tpsc_snapshots: {class: derived-cache, owner: repo-manager}
workplan_dependencies: {class: file-derived, owner: repo-manager}
workplan_launch_requests: {class: hub-native-control, owner: activity-core}
workplans: {class: file-derived, owner: repo-manager}
write_idempotency_keys: {class: derived-cache, owner: hub-core}

View file

@ -0,0 +1,496 @@
schema: repo-manager.repository-registry.v1
registry_source: forgejo
identity_source: state-hub-primary-migration-2026-09-01
repositories:
- repository_id: 01631c7a-f612-4799-892e-a96970d06063
slug: prj-forgejo-org-refactor
lifecycle: active
forgejo_repository: coulomb/prj-forgejo-org-refactor
- repository_id: 03e6fe9f-ca84-4be5-ad5d-03c037bf6107
slug: evidence-source
lifecycle: active
forgejo_repository: coulomb/evidence-source
- repository_id: 040b00d5-9c61-4a48-b21e-59c3f6ff4f18
slug: tmux-amq
lifecycle: active
forgejo_repository: coulomb/tmux-amq
- repository_id: 077cf296-3879-4da5-ac97-46cb248c889d
slug: timeline-svg
lifecycle: active
forgejo_repository: coulomb/timeline-svg
- repository_id: 10f1c902-576d-4782-b1b2-643a6083ee36
slug: config-atlas
lifecycle: active
forgejo_repository: coulomb/config-atlas
- repository_id: 1174ca73-c53d-4bb8-bfc3-fdeda13ef706
slug: tegwick-control
lifecycle: active
forgejo_repository: coulomb/tegwick-control
- repository_id: 11c1ff2e-ad0d-4a1a-b649-7da7b98a07da
slug: sand-boxer
lifecycle: active
forgejo_repository: coulomb/sand-boxer
- repository_id: 13094d7a-b978-4aa3-8a1d-0d05b824698d
slug: llm-connect
lifecycle: active
forgejo_repository: coulomb/llm-connect
- repository_id: 14487b12-9a94-4d98-83e5-dcc0b58b650a
slug: whitehat-security
lifecycle: active
forgejo_repository: coulomb/whitehat-security
- repository_id: 146bcec9-59e8-481e-9996-f0b30ab28abb
slug: domain-tree
lifecycle: active
forgejo_repository: coulomb/domain-tree
- repository_id: 14a5657e-f875-4cd4-8801-3c7dcecb11f2
slug: vantage-point
lifecycle: active
forgejo_repository: coulomb/vantage-point
- repository_id: 155fab93-6a76-415e-a57a-7e39b4428985
slug: rapp-core-hub
lifecycle: active
forgejo_repository: coulomb/rapp-core-hub
- repository_id: 195e2991-79c3-4c61-981d-5de78f6fe5b8
slug: resource-control
lifecycle: active
forgejo_repository: coulomb/resource-control
- repository_id: 1bf99f56-6e94-4379-a9ea-295a4c181889
slug: ops-bridge
lifecycle: active
forgejo_repository: coulomb/ops-bridge
- repository_id: 1f5876f3-bc96-4f64-a9c8-f2e4578b8416
slug: reef-railiance
lifecycle: active
forgejo_repository: coulomb/reef-railiance
- repository_id: 215c052f-88e7-4b8d-af84-01008cd606d3
slug: rapp-tenant-engine
lifecycle: active
forgejo_repository: coulomb/rapp-tenant-engine
- repository_id: 217fe904-2e15-44a8-bdda-e8c6683f46c9
slug: test-driver
lifecycle: active
forgejo_repository: coulomb/test-driver
- repository_id: 29593df6-89a6-4058-9e0b-863e986b86f8
slug: policy-nexus
lifecycle: active
forgejo_repository: coulomb/policy-nexus
- repository_id: 2ad0ed58-4324-47e2-b695-d7128bc623bf
slug: binky-control
lifecycle: active
forgejo_repository: coulomb/binky-control
- repository_id: 2c0de614-e468-4eb6-8157-470649ac8c05
slug: railiance-fabric
lifecycle: active
forgejo_repository: coulomb/railiance-fabric
- repository_id: 2c72f2ad-d940-4a21-a71e-d490bd51cff2
slug: markitect-quarkdown
lifecycle: active
forgejo_repository: coulomb/markitect-quarkdown
- repository_id: 2cb4c14d-6f90-40a7-a9ee-14a663306cb5
slug: risk-nexus
lifecycle: active
forgejo_repository: coulomb/risk-nexus
- repository_id: 2db668cf-6c52-4965-951f-0d5e617de9e3
slug: railiance-hosts
lifecycle: archived
forgejo_repository: coulomb/railiance-hosts
- repository_id: 317300f6-a09b-4464-8a0e-798aad5627d0
slug: rein-openweights
lifecycle: active
forgejo_repository: coulomb/rein-openweights
- repository_id: 32549b0d-b065-4294-b577-fad643fa38de
slug: can-you-assist
lifecycle: active
forgejo_repository: coulomb/can-you-assist
- repository_id: 33b4936f-aadd-42e2-a320-33c524a07e1d
slug: ralph-workplan
lifecycle: active
forgejo_repository: coulomb/ralph-workplan
- repository_id: 3786363f-94a5-43a0-889d-8e85993b2783
slug: testdrive-jsui
lifecycle: active
forgejo_repository: coulomb/testdrive-jsui
- repository_id: 39b9170f-25d2-4728-bb68-bdd4791534ea
slug: ground-game
lifecycle: active
forgejo_repository: coulomb/ground-game
- repository_id: 3ce9ef37-9672-4096-9d28-5ef77e450c07
slug: email-connect
lifecycle: active
forgejo_repository: coulomb/email-connect
- repository_id: 3d070446-80e0-4d51-a802-d037b8ac273a
slug: rapp-openbao
lifecycle: active
forgejo_repository: coulomb/rapp-openbao
- repository_id: 423794e3-a8a4-49f3-9f07-f743e10d5f67
slug: binect-js
lifecycle: active
forgejo_repository: coulomb/binect-js
- repository_id: 4296e38c-4eb4-4710-8e35-fb37009add0b
slug: activity-core
lifecycle: active
forgejo_repository: coulomb/activity-core
- repository_id: 42f80fe5-e5bb-4765-8e4b-e369517fb36a
slug: railiance-bootstrap
lifecycle: archived
forgejo_repository: coulomb/railiance-bootstrap
- repository_id: 439a69fc-084a-4074-88fe-c99de627deeb
slug: disaster-control
lifecycle: active
forgejo_repository: coulomb/disaster-control
- repository_id: 4589488d-625b-4ffe-b8f0-48744d7b1498
slug: net-kingdom
lifecycle: active
forgejo_repository: coulomb/net-kingdom
- repository_id: 46e9c85b-057d-41e0-9382-851bebcf2cc1
slug: rapp-qonto
lifecycle: active
forgejo_repository: coulomb/rapp-qonto
- repository_id: 47f50756-aaf6-4897-9bc7-097fcd9daf46
slug: python-snake
lifecycle: active
forgejo_repository: coulomb/python-snake
- repository_id: 485187c0-4fad-42f7-984c-5e317a66c5de
slug: railiance-infra
lifecycle: active
forgejo_repository: coulomb/railiance-infra
- repository_id: 487f321e-2e53-43d4-90a6-af421e438313
slug: info-tech-canon
lifecycle: active
forgejo_repository: coulomb/info-tech-canon
- repository_id: 4a887c44-e301-4c1e-a4fe-1b4a842d8777
slug: human-resources
lifecycle: active
forgejo_repository: coulomb/human-resources
- repository_id: 4fb4a5f3-08f9-49ba-958b-414adc565168
slug: feature-control
lifecycle: active
forgejo_repository: coulomb/feature-control
- repository_id: 5115e0c5-009f-4168-b155-9943fe2ab9a7
slug: railiance-platform
lifecycle: active
forgejo_repository: coulomb/railiance-platform
- repository_id: 56ae522c-b47e-4748-8239-f61b332fe69d
slug: the-custodian
lifecycle: active
forgejo_repository: coulomb/the-custodian
- repository_id: 5bac09ed-a1c9-42fb-bd16-75c8f9e01a1a
slug: glas-harness
lifecycle: active
forgejo_repository: coulomb/glas-harness
- repository_id: 5dd1708d-96e3-4737-96be-05485548ab04
slug: prj-state-hub-retirement
lifecycle: active
forgejo_repository: coulomb/prj-state-hub-retirement
- repository_id: 60f2d8b6-f3fa-4bb0-b316-89de9b2a7d79
slug: tele-mcp
lifecycle: active
forgejo_repository: coulomb/tele-mcp
- repository_id: 69b69830-af7f-496f-9af2-cbc23a83cd42
slug: ihp-railiance-probe
lifecycle: active
forgejo_repository: coulomb/ihp-railiance-probe
- repository_id: 6b6e91cd-160f-4e62-9478-80d7588a35dd
slug: kings-guard
lifecycle: active
forgejo_repository: coulomb/kings-guard
- repository_id: 7007e599-6f2a-4fc6-b6c7-a86b6e9ccfaf
slug: reuse-surface
lifecycle: active
forgejo_repository: coulomb/reuse-surface
- repository_id: 719f49a6-63a0-47d4-9a9c-ea515be17d5a
slug: rapp-issue-core
lifecycle: active
forgejo_repository: coulomb/rapp-issue-core
- repository_id: 73be5a8e-8503-447f-87cb-de8c2c8c0b9e
slug: rein-aharness
lifecycle: active
forgejo_repository: coulomb/rein-aharness
- repository_id: 74df727e-a756-42de-b776-11928b03bd1f
slug: ops-warden
lifecycle: active
forgejo_repository: coulomb/ops-warden
- repository_id: 75d31180-acf5-4d47-aea8-2a5b1e71e6a9
slug: marki-docx
lifecycle: active
forgejo_repository: coulomb/marki-docx
- repository_id: 761e6ac3-6483-4b7f-ac7a-5e5741a01118
slug: user-engine
lifecycle: active
forgejo_repository: coulomb/user-engine
- repository_id: 76af2d82-9a4b-4797-b542-3f4f5545b6cc
slug: whynot-control
lifecycle: active
forgejo_repository: coulomb/whynot-control
- repository_id: 7945cc60-f228-4baa-879d-5aa515d004e3
slug: qonto-assistant
lifecycle: active
forgejo_repository: coulomb/qonto-assistant
- repository_id: 7b46d0dd-b261-4b8f-82c6-fa26f24e42a0
slug: sbom-nexus
lifecycle: active
forgejo_repository: coulomb/sbom-nexus
- repository_id: 80a9b0c4-f654-47fe-9721-33fc0558687e
slug: hub-core
lifecycle: active
forgejo_repository: coulomb/hub-core
- repository_id: 8142c713-7a09-4703-9771-0b9d4238fe33
slug: citation-work
lifecycle: active
forgejo_repository: coulomb/citation-work
- repository_id: 87f43c8e-01bd-465a-96ce-c0eee2ae7bcf
slug: markitect-filter
lifecycle: active
forgejo_repository: coulomb/markitect-filter
- repository_id: 8a99bb74-1ec0-4478-ac70-35a7cddb0e3c
slug: key-cape
lifecycle: active
forgejo_repository: coulomb/key-cape
- repository_id: 8c82baea-bb40-435d-ac42-ec7a7c20dbb8
slug: commerce-canon
lifecycle: active
forgejo_repository: coulomb/commerce-canon
- repository_id: 8df9b401-f2e4-4d12-b5ba-7fc191d4e606
slug: rapp-sbom-nexus
lifecycle: active
forgejo_repository: coulomb/rapp-sbom-nexus
- repository_id: 8e9ffb56-7abc-47b5-84b2-5ad392b37dcd
slug: state-hub
lifecycle: active
forgejo_repository: coulomb/state-hub
- repository_id: 90918b55-6347-4e57-8086-db7b0454f73d
slug: guide-board
lifecycle: active
forgejo_repository: coulomb/guide-board
- repository_id: 90fc8c09-30e7-439d-a873-779bd2cd5ac5
slug: coordination-engine
lifecycle: active
forgejo_repository: coulomb/coordination-engine
- repository_id: 95d05a4d-e276-4eea-a767-c78d3ac4b320
slug: rail-knative
lifecycle: active
forgejo_repository: coulomb/rail-knative
- repository_id: 9b69c35b-f3e7-490c-bb9e-d78f96992513
slug: polycode-sim
lifecycle: active
forgejo_repository: coulomb/polycode-sim
- repository_id: 9cfdd641-0c27-436f-ad3b-5d41a1353fcf
slug: rapp-user-engine
lifecycle: active
forgejo_repository: coulomb/rapp-user-engine
- repository_id: 9d7b100b-1538-483e-a873-cf70980c59e3
slug: coulomb-loop
lifecycle: active
forgejo_repository: coulomb/coulomb-loop
- repository_id: a4c297f2-b38a-42d3-b1a5-ae4b29640276
slug: markitect-main
lifecycle: active
forgejo_repository: coulomb/markitect-main
- repository_id: a4cac053-048b-453b-93aa-942260bd4e09
slug: evidence-anchor
lifecycle: active
forgejo_repository: coulomb/evidence-anchor
- repository_id: a53a75fb-e9fa-41d9-8a96-12a4b924dc63
slug: whynot-design
lifecycle: active
forgejo_repository: coulomb/whynot-design
- repository_id: a677c189-b4e2-4f2a-9e48-faa482c277e6
slug: citation-evidence
lifecycle: active
forgejo_repository: coulomb/citation-evidence
- repository_id: a7cea934-e38c-467c-b2ea-64fd3424a845
slug: kontextual-engine
lifecycle: active
forgejo_repository: coulomb/kontextual-engine
- repository_id: a9916da2-b4dd-4e12-8a30-47e47f6ed298
slug: binect-chrome
lifecycle: active
forgejo_repository: coulomb/binect-chrome
- repository_id: a9d105c6-10fa-4cf9-8bc1-e248476698d3
slug: repo-manager
lifecycle: active
forgejo_repository: coulomb/repo-manager
- repository_id: aa497d2f-d88d-4065-9ab2-e251df07ead4
slug: railiance-master
lifecycle: active
forgejo_repository: coulomb/railiance-master
- repository_id: b1321f79-ccc7-4b0a-84bf-c7b9825ca427
slug: reef-storage
lifecycle: active
forgejo_repository: coulomb/reef-storage
- repository_id: b51b2b12-dc53-4423-8d3c-78c96eee0362
slug: shard-wiki
lifecycle: active
forgejo_repository: coulomb/shard-wiki
- repository_id: b6b8dacd-4100-475a-8390-c326e1cebdec
slug: open-cmis-tck
lifecycle: active
forgejo_repository: coulomb/open-cmis-tck
- repository_id: b7add1b9-b90f-4baf-acac-2f5ce3037b5f
slug: repo-seed
lifecycle: active
forgejo_repository: coulomb/repo-seed
- repository_id: b7b3e7cc-cdde-4430-be43-e82343fdf1b0
slug: infospace-bench
lifecycle: active
forgejo_repository: coulomb/infospace-bench
- repository_id: b9066ab1-c6fe-409b-9145-64ba374654db
slug: ops-mason
lifecycle: active
forgejo_repository: coulomb/ops-mason
- repository_id: b96c952c-9993-4941-b75a-fe76eb7b2a57
slug: citation-engine
lifecycle: active
forgejo_repository: coulomb/citation-engine
- repository_id: ba599cd4-e8aa-430b-bbbe-e49f88536d70
slug: markitect-tool
lifecycle: active
forgejo_repository: coulomb/markitect-tool
- repository_id: bc978be9-ca72-42bb-a451-d4737b779c5b
slug: railiance-enablement
lifecycle: active
forgejo_repository: coulomb/railiance-enablement
- repository_id: c186e743-9ebb-425a-aecc-7e3ea7cdc2a5
slug: railiance-telemetry
lifecycle: active
forgejo_repository: coulomb/railiance-telemetry
- repository_id: c29eab12-d8b8-46b3-851b-0e23f78f91f3
slug: kaizen-agentic
lifecycle: active
forgejo_repository: coulomb/kaizen-agentic
- repository_id: c3fd0dd2-a0de-415c-8e3a-e37406f4b8f8
slug: railiance-cluster
lifecycle: active
forgejo_repository: coulomb/railiance-cluster
- repository_id: c7a3dc03-d837-4577-a107-a190d6b00705
slug: helix-forge
lifecycle: active
forgejo_repository: coulomb/helix-forge
- repository_id: c96dd8b3-3d15-4c50-8daf-0ec6a2c56ddf
slug: ops-hub
lifecycle: active
forgejo_repository: coulomb/ops-hub
- repository_id: cb06310e-4381-428b-b8f3-d3ea8ac2f70d
slug: railiance-apps
lifecycle: active
forgejo_repository: coulomb/railiance-apps
- repository_id: ce3d5e76-ee5c-40dc-acc8-c04e63a0e406
slug: secrets-engine
lifecycle: active
forgejo_repository: coulomb/secrets-engine
- repository_id: d166babe-c3f0-4e46-8091-a2c44ab557fb
slug: direkt-vermittlung-de
lifecycle: active
forgejo_repository: coulomb/direkt-vermittlung-de
- repository_id: d3581e63-4002-4c83-8c03-93547823c8c7
slug: freedom-intelligence
lifecycle: active
forgejo_repository: coulomb/freedom-intelligence
- repository_id: d416fd6f-3c9d-4ba7-a5ca-f7543940ee6a
slug: open-reuse
lifecycle: active
forgejo_repository: coulomb/open-reuse
- repository_id: d56e0504-044d-4021-ae43-e9d706dbd087
slug: phase-memory
lifecycle: active
forgejo_repository: coulomb/phase-memory
- repository_id: d5c398ad-93a6-479c-bd5e-6078e717c819
slug: rapp-policy-nexus
lifecycle: active
forgejo_repository: coulomb/rapp-policy-nexus
- repository_id: d63ab9fa-9bf2-4014-a5e8-d186c417552e
slug: target-revenue
lifecycle: active
forgejo_repository: coulomb/target-revenue
- repository_id: d70d228e-841d-42d9-a7e9-5d978b4008d8
slug: tenant-engine
lifecycle: active
forgejo_repository: coulomb/tenant-engine
- repository_id: d7c12587-e70c-4d06-8843-e582c0d683c3
slug: adaptive-pricing
lifecycle: active
forgejo_repository: coulomb/adaptive-pricing
- repository_id: da8ecb37-e1bf-4260-b853-beb40ac9235b
slug: soul-frame
lifecycle: active
forgejo_repository: coulomb/soul-frame
- repository_id: dc1866c7-08af-4117-8684-057331be20b5
slug: rail-kubernetes
lifecycle: active
forgejo_repository: coulomb/rail-kubernetes
- repository_id: dc9e7e92-2c8c-4cec-9021-9182c46ab097
slug: agentic-resources
lifecycle: active
forgejo_repository: coulomb/agentic-resources
- repository_id: de749ff1-a4a4-42f5-8ed4-6b8c05b18bd9
slug: repo-scoping
lifecycle: active
forgejo_repository: coulomb/repo-scoping
- repository_id: e1575e7c-e1b0-4b46-98be-78bd49e06318
slug: artifact-store
lifecycle: active
forgejo_repository: coulomb/artifact-store
- repository_id: e4614b15-7526-4787-bd6f-74aa3cf24c3d
slug: rapp-postgres
lifecycle: active
forgejo_repository: coulomb/rapp-postgres
- repository_id: e51fd6df-525e-457c-9ec0-924832b491bc
slug: issue-core
lifecycle: active
forgejo_repository: coulomb/issue-core
- repository_id: ea264d14-723b-48d9-b330-1e3c929934ec
slug: prj-unattended-progress-company
lifecycle: active
forgejo_repository: coulomb/prj-unattended-progress-company
- repository_id: ea797759-b691-4b5a-910e-3d77f19df0da
slug: gate-house
lifecycle: active
forgejo_repository: coulomb/gate-house
- repository_id: ec60633d-b055-43b7-a83e-7e0e9734ede1
slug: approval-engine
lifecycle: active
forgejo_repository: coulomb/approval-engine
- repository_id: ec840a06-4e33-4fdc-9762-da7a5beec254
slug: prj-canon-federation
lifecycle: active
forgejo_repository: coulomb/prj-canon-federation
- repository_id: edffc8b6-0360-471e-a6b1-df56bde1a27e
slug: railiance-forge
lifecycle: active
forgejo_repository: coulomb/railiance-forge
- repository_id: f14be058-a339-4357-a3ff-c8326f7748a7
slug: evidence-binder
lifecycle: active
forgejo_repository: coulomb/evidence-binder
- repository_id: f4038d32-948d-49e2-9b84-4fd48109100f
slug: core-hub
lifecycle: active
forgejo_repository: coulomb/core-hub
- repository_id: f5d1db1e-e6f7-4299-a496-12dbc5e73b18
slug: clay-borg
lifecycle: active
forgejo_repository: coulomb/clay-borg
- repository_id: fbf4e172-966a-4ec8-b135-2bff8951c550
slug: fin-hub
lifecycle: active
forgejo_repository: coulomb/fin-hub
- repository_id: fc3296df-e3e0-49a4-b022-4701f3c71f9b
slug: coulomb-social
lifecycle: active
forgejo_repository: coulomb/coulomb-social
- repository_id: fda8ad85-a7d7-4055-8f21-902a533e59df
slug: access-engine
lifecycle: active
forgejo_repository: coulomb/access-engine
- repository_id: ff057178-b3ca-48f4-b7b5-2cda570ed12f
slug: zone-engine
lifecycle: active
forgejo_repository: coulomb/zone-engine
- repository_id: ff7b0754-9d54-4abf-a752-d342870f5993
slug: vergabe-teilnahme
lifecycle: active
forgejo_repository: coulomb/vergabe-teilnahme
- repository_id: ffb264e4-5c5b-4c18-9c6f-df6dd5f1bb05
slug: audit-core
lifecycle: active
forgejo_repository: coulomb/audit-core

View file

@ -0,0 +1,19 @@
schema: repo-manager.state-hub-access.v1
updated: "2026-08-28"
workplan_task: CUST-WP-0067-T07
notes: >
Authoritative State Hub access map for agent-facing AGENTS.md tables.
Edit this file, then `rmgr scaffold --path <repo> --refresh-hub-access`.
Local 127.0.0.1:8000 is central via ops-bridge state-hub-primary, not a cache.
endpoints:
- context: Local workstation
url: http://127.0.0.1:8000
- context: Remote (railiance01, in-cluster)
url: http://10.43.68.154:8000
- context: Optional local edge relay
url: http://127.0.0.1:18080
retired:
- url: http://127.0.0.1:18000
reason: reverse tunnel back to the workstation; the hub is in-cluster on railiance01

View file

@ -0,0 +1,83 @@
# Implementation snapshot of the Custodian work-record kind registry.
# Canonical source:
# the-custodian/canon/standards/work-record-types.yaml (version 0.1)
version: "0.1"
status: active
kinds:
- kind: workplan
id_patterns:
- "^[A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*-WP-[0-9]{4}$"
- "^[A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*-WP-ADHOC-[0-9]{4}-[0-9]{2}-[0-9]{2}$"
legacy_patterns:
- pattern: "^CUST-WP-0000b$"
source: Custodian historical workplan suffix retained by source and hub
grandfathered: true
- pattern: "^CUST-WP-0000c$"
source: Custodian archived historical workplan suffix retained by source and hub
grandfathered: true
- pattern: "^CUST-WP-0010b$"
source: Custodian historical workplan suffix retained by source and hub
grandfathered: true
- pattern: "^CUST-WP-(0017|0053)-LEGACY$"
source: Custodian archived legacy workplan identity retained by source and hub
grandfathered: true
- pattern: "^ADHOC-[0-9]{4}-[0-9]{2}-[0-9]{2}$"
source: daily ad-hoc containers created before repository-qualified ad-hoc ids
grandfathered: true
- pattern: "^MASON-0001$"
source: ops-mason bootstrap workplan created before WP infix convention
grandfathered: true
- kind: task
id_patterns:
- "^[A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*-WP-[0-9]{4}-T[0-9]{2,3}$"
- "^[A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*-WP-ADHOC-[0-9]{4}-[0-9]{2}-[0-9]{2}-T[0-9]{2}$"
legacy_patterns:
- pattern: "^ADHOC-[0-9]{4}-[0-9]{2}-[0-9]{2}-T[0-9]{2}$"
source: daily ad-hoc tasks created before repository-qualified ad-hoc ids
grandfathered: true
- pattern: "^[A-Z]+-WP-[0-9]{4}-T[0-9]$"
source: early single-digit task suffixes (pre task-block convention)
grandfathered: true
- pattern: "^[A-Z]+-WP-[0-9]{4}-LEGACY-T[0-9]{2}$"
source: archived-workplan legacy task renumbering
grandfathered: true
- pattern: "^MASON-0001-T[0-9]{2,3}$"
source: ops-mason bootstrap tasks created before WP infix convention
grandfathered: true
- pattern: "^CUST-WP-(?:0000c-T0[1-4]|0010b-T0[1-2])$"
source: Custodian historical task identity retained by source and hub
grandfathered: true
scoped_legacy_patterns:
- pattern: "^(?:T01|T02|T03|T04|T05)$"
parent_pattern: "^CUST-WP-0006$"
source: Custodian historical task blocks with parent-qualified UUID bindings
grandfathered: true
- pattern: "^(?:T01|T02|T03|T04|T05|T06|T07|T08|T09|T10|T11|T12|T13|T14|T15|T16|T17)$"
parent_pattern: "^CUST-WP-0007$"
source: Custodian historical task blocks with parent-qualified UUID bindings
grandfathered: true
- pattern: "^(?:T01|T02|T03|T04|T05|T06|T07|T08|T09)$"
parent_pattern: "^CUST-WP-0030$"
source: Custodian historical task blocks with parent-qualified UUID bindings
grandfathered: true
- pattern: "^(?:T01|T02|T03|T04|T05|T06|T07)$"
parent_pattern: "^CUST-WP-0031$"
source: Custodian historical task blocks with parent-qualified UUID bindings
grandfathered: true
- kind: intake
id_patterns:
- "^[A-Z]+-IN-[0-9]{4}$"
legacy_patterns:
- pattern: "^AWQ-[0-9]{3}$"
source: binky-control AutopilotWorkQueue
grandfathered: true
- kind: decision
id_patterns:
- "^[A-Z]+-DEC-[0-9]{4}-[0-9]{3}$"
legacy_patterns:
- pattern: "^DEC-[0-9]{4}-[0-9]{3}$"
source: binky-control DecisionQueue
grandfathered: true
identity_reconciliation:
repeated_same_id_same_uuid: duplicate_source_occurrence
repeated_same_id_different_uuid: identity_collision

View file

@ -0,0 +1,78 @@
# RMGR-WP-0004-T08 / ADR-007 decision 1
# One prefix, one repository. Prefixes derive from repo or project identity,
# never from a flavor marker or family name. Detection only — historical
# collisions stay until ADR-007 § Migration is ruled.
standard: workplan-prefix-ownership
version: "0.1"
canon:
- the-custodian/canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md
- the-custodian/canon/standards/project-repository-flavor_v0.1.md
retired:
- prefix: NET-WP
reason: second prefix in net-kingdom; NK-WP is the repository's form (229 ids vs 49)
successors: [NK-WP]
- prefix: PRJ-WP
reason: flavor marker; never a valid prefix
- prefix: RAIL-BS-WP
reason: shared family name, not a repository
successors: [RCLUSTER-WP, RBS-WP]
- prefix: RAILIANCE-WP
reason: family name, not a repository
successors: [RPF-WP, RAPPS-WP, RFORGE-WP, RTELE-WP]
owners:
RMGR-WP: repo-manager
CUST-WP: the-custodian
STATE-WP: state-hub
CFED-WP: prj-canon-federation
SHR-WP: prj-state-hub-retirement
RCLUSTER-WP: railiance-cluster
RBS-WP: railiance-bootstrap
RPF-WP: railiance-platform
RAPPS-WP: railiance-apps
RFORGE-WP: railiance-forge
RTELE-WP: railiance-telemetry
RMASTER-WP: railiance-master
RCLK-WP: railiance-clock
RAIL-K8S-WP: rail-kubernetes
RAPP-OPENBAO-WP: rapp-openbao
RAPP-USER-ENGINE-WP: rapp-user-engine
RAPP-TENANT-ENGINE-WP: rapp-tenant-engine
RAPP-POLICY-NEXUS-WP: rapp-policy-nexus
USER-WP: user-engine
TEN-WP: tenant-engine
ACTIVITY-WP: activity-core
ADAPTIVE-WP: adaptive-pricing
AGENTIC-WP: agentic-resources
GROUND-WP: ground-game
ITC-WP: info-tech-canon
KAIZEN-WP: kaizen-agentic
LLM-WP: llm-connect
NK-WP: net-kingdom
BRIDGE-WP: ops-bridge
WARDEN-WP: ops-warden
QONTO-WP: qonto-assistant
RAIL-HO-WP: railiance-infra
SECRETS-WP: secrets-engine
TAMQ-WP: tmux-amq
RAIL-FAB-WP: railiance-fabric
WHYNOT-WP: whynot-design
OPS-WP: ops-hub
WNCTL-WP: whynot-control
CORE-WP: core-hub
DISCTL-WP: disaster-control
REPO-WP: repo-seed
historical_collisions:
- id: RAILIANCE-WP-0015
repos: [railiance-platform, railiance-apps]
- id: RAILIANCE-WP-0016
repos: [railiance-platform, railiance-apps]
- id: CUST-WP-0000
repos: [the-custodian]
- id: CUST-WP-0010
repos: [the-custodian]
- id: CUST-WP-0045
repos: [the-custodian]

View file

@ -0,0 +1,177 @@
---
id: RAIL-BS-WP-0002
type: workplan
title: "k3s and Kubernetes Platform Baseline"
domain: financials
repo: railiance-cluster
status: finished
owner: railiance
topic_slug: railiance
repo_goal_id: "70ab2379-fb9d-4fec-a09d-b2a717e4ace8"
state_hub_workstream_id: "4c63dfc6-9eac-4e79-9f77-8f644ad7147d"
created: "2026-03-09"
updated: "2026-03-10"
completed: "2026-03-10"
---
> **Terminology note:** Historical text in this archived workplan may use the legacy term "workstream". The fleet term is **workplan** (`canon/standards/workplan-terminology-fleet_v0.1.md`).
# k3s and Kubernetes Platform Baseline
## Goal
Install k3s, Helm, and the baseline Kubernetes services on the converged
HostEurope node. This workplan picks up exactly where `railiance-hosts`
leaves off: a hardened, verified OS node that is ready for Kubernetes.
## Pre-condition
`railiance-infra` converge + Goss verify must pass before any task here
is executed:
```bash
# In railiance-infra/
make converge
make verify # must exit 0
```
## Boundary
This repo owns everything from k3s upward. It must not re-configure items
defined in `railiance-infra/spec/server-baseline.yaml`. See ADR-003:
`railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md`.
**Out of scope here:** platform services (PostgreSQL, storage, identity)
→ `railiance-platform`. Application deployments (Gitea, coulomb services)
→ `railiance-apps`.
---
## Tasks
### T01 — Ansible playbook: install k3s (server mode)
```task
id: T01
status: done
priority: high
state_hub_task_id: "3f042630-eab0-4c6a-9167-e2b28ff20e40"
completed: "2026-03-10"
```
Harden `ansible/bootstrap.yml` to a production-ready k3s install:
- Use the official k3s install script pinned to a specific version
(`INSTALL_K3S_VERSION=v1.35.1+k3s1`)
- `INSTALL_K3S_EXEC="server --cluster-init --write-kubeconfig-mode=644"`
(cluster-init enables embedded etcd for future HA expansion)
- Wait for node `Ready` before proceeding:
```bash
k3s kubectl wait node --all --for=condition=Ready --timeout=120s
```
- Fetch kubeconfig to the control node as `~/.kube/config-hosteurope`
**Done when:** `k3s kubectl get nodes` returns `Ready` from both the server
and the control node (via kubeconfig).
---
### T02 — Helm installation
```task
id: T02
status: done
priority: high
state_hub_task_id: "e8510646-46ed-4697-a345-f3d3009eea78"
completed: "2026-03-10"
```
Add a task (or a role `roles/helm/`) that:
1. Downloads the Helm binary (pinned version) to `/usr/local/bin/helm`
2. Verifies the checksum
3. Confirms `helm version` succeeds
**Done when:** `helm version` succeeds on the HostEurope node.
---
### T03 — Smoke test: k3s + Helm
```task
id: T03
status: done
priority: high
state_hub_task_id: "dab2c07f-8aa0-4635-8df6-857e87e93fc5"
completed: "2026-03-10"
```
Extend `tests/smoke_kube.sh` to assert:
- `k3s kubectl get nodes` → node in Ready state
- `helm version` exits 0
- CoreDNS pod running in `kube-system`
- Traefik ingress controller pod running (default in k3s)
Run via:
```bash
ansible-playbook -i ansible/hosts.ini ansible/smoke.yml
```
or directly over SSH if the kubeconfig is available locally.
**Done when:** all assertions pass and the script exits 0.
---
### T04 — Commit kubeconfig management notes
```task
id: T04
status: done
priority: medium
state_hub_task_id: "5c3d40e4-239b-488e-9519-6f7a38d2325f"
completed: "2026-03-10"
```
Document in `docs/kubeconfig.md`:
- Where the kubeconfig is fetched to (`~/.kube/config-hosteurope`)
- How to merge it into `~/.kube/config`
- How to switch context: `kubectl config use-context default`
- Security note: kubeconfig is gitignored (contains cluster CA + client cert)
**Done when:** doc written and committed.
---
### T05 — Add `make k3s-install` and `make smoke` targets
```task
id: T05
status: done
priority: medium
state_hub_task_id: "7f9e0e58-a130-467a-a2d0-b3f2564e496f"
completed: "2026-03-10"
```
Add to Makefile (create one if none exists):
```makefile
k3s-install: ## Install k3s and Helm on all inventory hosts
ansible-playbook -i ansible/hosts.ini ansible/bootstrap.yml
smoke: ## Run Kubernetes smoke tests
bash tests/smoke_kube.sh
```
**Done when:** both targets work and are listed in `make help`.
---
## References
- Repo goal: `70ab2379-fb9d-4fec-a09d-b2a717e4ace8` (Install k3s and Kubernetes Baseline)
- Domain goal: `6f96c712-60e6-4ea9-ab06-168878eafbce` (Three-Phoenix Secure Kubernetes Infrastructure)
- Pre-condition: railiance-infra WP-0001 (Secure Single-Server Bootstrap) — completed 2026-03-09
- Boundary ADR: `railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md`
- k3s releases: https://github.com/k3s-io/k3s/releases

View file

@ -0,0 +1,196 @@
---
id: RAIL-BS-WP-0003
type: workplan
title: "pgpool CrashLoopBackOff on PostgreSQL HA failover — missing secret key"
domain: financials
repo: railiance-cluster
status: finished
owner: tegwick
created: "2026-03-10"
updated: "2026-03-10"
state_hub_workstream_id: "7ee9ee22-1fae-4567-9194-8d70a9e0f45b"
---
> **Terminology note:** Historical text in this archived workplan may use the legacy term "workstream". The fleet term is **workplan** (`canon/standards/workplan-terminology-fleet_v0.1.md`).
# Bug Report: pgpool CrashLoopBackOff on PostgreSQL HA failover
## Summary
On 2026-03-10 a PostgreSQL HA failover caused all three postgresql pods to
restart. pgpool — the connection pooler between Gitea and PostgreSQL — then
entered CrashLoopBackOff and produced no logs. As a result Gitea's login
and all write operations hung indefinitely. The root page was still served
(from Valkey cache) which masked the failure.
The fix was to patch a missing key in a Kubernetes secret. The root cause is
that the `gitea-12.2.0` Helm chart (postgresql-ha subchart v16.2.2) does not
populate the `pgpool-password` key in the `gitea-postgresql-ha-postgresql`
secret, even though the pgpool pod requires it at startup.
---
## Timeline
| Time (UTC) | Event |
|---|---|
| ~09:45 | postgresql-0, postgresql-2 pods restarted (repmgr failover) |
| ~09:45 | pgpool pod restarted and entered CrashLoopBackOff |
| ~11:00 | User noticed Gitea login hanging; home page still loading |
| ~13:00 | Root cause identified: missing `pgpool-password` secret key |
| ~13:10 | Secret patched; pgpool pod deleted and restarted cleanly |
| ~13:15 | Gitea fully operational |
---
## Root Cause
The Bitnami `pgpool` container startup script reads the file
`/opt/bitnami/pgpool/secrets/pgpool-password`, which is mounted from the
`gitea-postgresql-ha-postgresql` Kubernetes Secret via a `subPath` volume
mount. That secret key was never created by the Helm chart, so the file did
not exist. The container exited immediately with no logs.
The pod had been running for 20 days without a restart, so this gap was
never discovered during initial deployment.
---
## Evidence
```bash
# Secret was missing the pgpool-password key
sudo k3s kubectl get secret -n default gitea-postgresql-ha-postgresql -o yaml
# data: keys were password, postgres-password, repmgr-password only
# pgpool-password was absent
# pgpool pod describe showed 824 back-off restarts over 173 minutes
# No logs in either current or --previous output
sudo k3s kubectl logs -n default <pgpool-pod> --previous
# (empty)
# Gitea process had zero TCP connections to PostgreSQL port 5432
# but many connections to Valkey port 6379
cat /proc/<gitea-pid>/net/tcp | grep 1538 # 1538 = 5432 hex — no results
```
---
## Immediate Fix Applied
```bash
# Add the missing key (value = sr-check-password = changeme4 = base64: Y2hhbmdlbWU0)
sudo k3s kubectl patch secret -n default gitea-postgresql-ha-postgresql \
--type='json' \
-p='[{"op":"add","path":"/data/pgpool-password","value":"Y2hhbmdlbWU0"}]'
# Restart pgpool
sudo k3s kubectl delete pod -n default <pgpool-pod-name>
```
---
## Risk: Fix Will Be Lost on helm upgrade
The patched secret is managed by Helm (annotation:
`meta.helm.sh/release-name: gitea`). A `helm upgrade` will regenerate the
secret from the chart template, which does not include `pgpool-password`,
and the bug will recur.
---
## Tasks
### T01 — Add pgpool-password to Helm values
```task
id: T01
status: done
priority: high
state_hub_task_id: "6841c93a-f146-47eb-9f7c-8fa0e02c1bbc"
```
Create or update `helm/gitea-values.yaml` (or equivalent) to permanently
include the pgpool-password so it survives `helm upgrade`:
```yaml
postgresql-ha:
postgresql:
pgpoolPassword: <value matching sr-check-password>
```
**Done when:** `helm upgrade gitea` completes and pgpool starts cleanly
without manual secret patching.
---
### T02 — Add pgpool health check to smoke test
```task
id: T02
status: done
priority: high
state_hub_task_id: "ab166073-30a7-4702-a037-4091e8706e20"
```
Extend `tests/smoke_kube.sh` to assert:
```bash
# All postgresql-ha pods Running
kubectl get pods -n default | grep gitea-postgresql-ha | grep -v Running && exit 1
# pgpool specifically not in CrashLoopBackOff
kubectl get pod -n default -l app.kubernetes.io/component=pgpool \
-o jsonpath='{.items[0].status.containerStatuses[0].state}' | grep -v crash
```
**Done when:** the smoke test catches a pgpool failure within 5 minutes.
---
### T03 — Add HA failover test
```task
id: T03
status: done
priority: high
state_hub_task_id: "140da396-8e30-4f4d-b88c-c42c0cd46c01"
```
Create `tests/test_ha_failover.sh` that:
1. Records Gitea login response time (baseline)
2. Kills the primary PostgreSQL pod: `kubectl delete pod gitea-postgresql-ha-postgresql-0 -n default`
3. Waits for repmgr to promote a replica (max 60s)
4. Asserts Gitea login POST still succeeds within 10s
5. Asserts pgpool pod is Running (not CrashLoopBackOff)
6. Asserts all postgresql pods return to Running
This test must pass before any PostgreSQL HA deployment is considered done.
**Done when:** script exits 0 against a live cluster.
---
### T04 — Document the incident in docs/
```task
id: T04
status: done
priority: medium
state_hub_task_id: "d8a3ba40-fda0-4c1f-a9f1-ffcd621a5b3d"
```
Add `docs/incidents/2026-03-10-pgpool-missing-secret.md` with the full
timeline, root cause, and fix, so future operators understand what happened
and how to recover.
**Done when:** doc committed and linked from `docs/README.md`.
---
## References
- Bitnami postgresql-ha chart v16.2.2
- Gitea Helm chart v12.2.0
- Related decision: D3 (HA testing policy) in `DECISIONS.md`

View file

@ -0,0 +1,275 @@
---
id: RAIL-BS-WP-0004
type: workplan
title: "Integrated Backup — S2 Kubernetes Runtime Layer"
domain: financials
repo: railiance-cluster
status: finished
owner: tegwick
topic_slug: railiance
state_hub_workstream_id: "7e8b0c20-51eb-40c9-9e3b-85dd380d7625"
created: "2026-02-25"
updated: "2026-03-26"
---
> **Terminology note:** Historical text in this archived workplan may use the legacy term "workstream". The fleet term is **workplan** (`canon/standards/workplan-terminology-fleet_v0.1.md`).
# Integrated Backup — S2 Kubernetes Runtime Layer
## Goal
Implement the Q3 (Operability & Resilience) integrated backup for
railiance-cluster (S2). Backs up what S2 owns — the Kubernetes runtime state —
encrypted with age, written to a local directory on the server. No external
dependencies required.
## Architecture (Decision D4)
Each railiance repo implements its own backup for what it owns. No central
backup service. See `DECISIONS.md` D4 for full rationale.
**Standard interface every railiance repo must provide:**
```bash
make backup # encrypt + write to /opt/backup/railiance/<layer>/
make restore # restore from most recent local backup
```
Encryption: age, same key pair as SOPS secrets (`.sops.yaml` public key).
Output: `/opt/backup/railiance/cluster/` on the server.
## What S2 (railiance-cluster) owns and must back up
| Asset | Why it matters |
|---|---|
| k3s etcd snapshots | Full cluster state — all workloads, configs, secrets |
| Helm release values | Runtime values not in git (any manually applied overrides) |
| kubeconfig | Admin access to the cluster |
**Not S2's responsibility:**
- Custodian State Hub DB → the-custodian owns this
- Operator workstation config (`.claude/`, `.gitconfig`) → operator's own concern
- Application data (Gitea repos, uploads) → S5 (railiance-apps) owns this
- PostgreSQL data volumes → S3 (railiance-platform) owns this
## Encryption
Reuse the age public key from `.sops.yaml`:
```bash
AGE_PUBLIC_KEY=$(grep 'age:' .sops.yaml | awk '{print $2}')
tar -czf - <assets> | age -r "${AGE_PUBLIC_KEY}" -o backup.tar.gz.age
```
Decryption requires the private key at `~/.config/sops/age/keys.txt`
(same key used for `sops -d`). No additional key management needed.
## Extension Point EP-RAIL-005
Once all five OAS layers implement this interface, the custodian can
orchestrate a full-stack backup with:
```bash
for repo in railiance-infra railiance-cluster railiance-platform \
railiance-enablement railiance-apps; do
make -C ~/$repo backup
done
```
No special protocol needed — just the standard interface.
---
## Tasks
### T01 — Define backup directory and encryption wrapper
```task
id: T01
status: done
priority: high
state_hub_task_id: "4526a842-ea31-4874-9231-92ab556cfe7b"
```
Create `tools/cmd/railiance-backup-s2` (replacing the old `railiance-backup`):
- Backup dir: `/opt/backup/railiance/cluster/` (create with `mkdir -p`)
- Encrypt each artifact with age using public key from `.sops.yaml`
- Write timestamp-named files: `etcd-<ts>.snap.age`, `helm-values-<ts>.tar.gz.age`, `kubeconfig-<ts>.yaml.age`
- Keep last 7 of each type
- Write `.last-backup` stamp
- Exit 0 on success, non-zero on any failure
- No network required
Also remove the old `tools/cmd/railiance-backup` (backed up Docker-based
custodian DB — wrong scope, not applicable to this server).
**Done when:** `make backup` runs on COULOMBCORE without error and files
appear in `/opt/backup/railiance/cluster/`.
---
### T02 — Back up k3s state (SQLite hot backup)
```task
id: T02
status: done
priority: high
state_hub_task_id: "a6313e06-1976-46a7-8e31-df4eb2eca880"
```
k3s has built-in etcd snapshot support:
```bash
sudo k3s etcd-snapshot save --name railiance-$(date -u +%Y%m%dT%H%M%SZ)
# Default location: /var/lib/rancher/k3s/server/db/snapshots/
```
Add to the backup script: take a fresh snapshot, encrypt with age,
copy to `/opt/backup/railiance/cluster/`.
> **Note — verify etcd is in use before implementing:**
> `k3s etcd-snapshot` only works if k3s was started with `--cluster-init`.
> Without it, k3s uses SQLite and this command will fail.
> Verify first: `sudo k3s etcd-snapshot ls 2>&1`
> **Note — sudo required:** etcd snapshot requires root. See T06 for how
> this is resolved (backup runs under root's crontab).
**Done when:** backup includes a current etcd snapshot.
---
### T03 — Back up Helm release values
```task
id: T03
status: done
priority: medium
state_hub_task_id: "05d42a55-921f-4aa7-bb76-e8af9c7e0ac3"
```
Capture current runtime Helm values for all releases:
```bash
KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm list -A -o json | \
jq -r '.[].name + " " + .namespace' | \
while read name ns; do
KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm get values "$name" -n "$ns" -o yaml
done
```
Tar and age-encrypt into `helm-values-<ts>.tar.gz.age`.
> **Note — kubeconfig permissions:** `/etc/rancher/k3s/k3s.yaml` is root-readable
> only by default. The backup script must either run as root (see T06) or k3s
> must be configured with `--write-kubeconfig-mode=644`. Running as root
> (via root crontab) is the chosen approach — no config change needed.
**Done when:** backup includes a snapshot of all Helm release values.
---
### T04 — Back up kubeconfig
```task
id: T04
status: done
priority: medium
state_hub_task_id: "08233868-d522-4117-bc4e-6c0f52545665"
```
Age-encrypt `~/.kube/config-hosteurope` (or `/etc/rancher/k3s/k3s.yaml`)
into `kubeconfig-<ts>.yaml.age` in the backup directory.
**Done when:** backup includes the encrypted kubeconfig.
---
### T05 — make restore target
```task
id: T05
status: done
priority: medium
state_hub_task_id: "2d5acff7-4a4e-4ddd-ad06-08237ad3dac8"
```
Add `tools/cmd/railiance-restore-s2` that decrypts and lists available
backups, with guided restore for the etcd snapshot case.
Restore of etcd from snapshot:
```bash
sudo k3s server --cluster-reset \
--cluster-reset-restore-path=/var/lib/rancher/k3s/server/db/snapshots/<name>
```
**Done when:** `make restore` prints available backups and a restore guide.
---
### T06 — Install cron job and run restore drill
```task
id: T06
status: done
priority: medium
state_hub_task_id: "f8e4a094-c367-40eb-b895-da17bc144b07"
```
#### Solving the sudo problem
The backup script needs root for two reasons:
- `k3s etcd-snapshot save` requires root
- `/etc/rancher/k3s/k3s.yaml` (kubeconfig) is root-readable only
**Solution: run the cron under root's crontab.**
This is the correct pattern for system-level backup jobs. It avoids a
proliferating sudoers whitelist (one entry per command, brittle to maintain)
and matches how tools like `rsnapshot`, `bacula`, and `borgbackup` work in
production. The backup writes to `/opt/backup/` which is root-owned anyway.
Install the cron as root:
```bash
sudo crontab -e
# Add:
0 2 * * * make -C /home/tegwick/railiance-cluster backup >> /opt/backup/railiance/cluster/backup.log 2>&1
```
Note: use the absolute path to the repo — `~` does not expand reliably in
root's crontab unless HOME is set.
Verify it is installed:
```bash
sudo crontab -l | grep railiance
```
#### Restore drill
Once T01–T04 are done, run a decrypt-and-verify drill:
```bash
# Decrypt the etcd snapshot and verify it is a valid snapshot file
sudo age -d -i ~/.config/sops/age/keys.txt \
/opt/backup/railiance/cluster/etcd-$(ls /opt/backup/railiance/cluster/etcd-*.snap.age | sort -r | head -1 | xargs basename | sed 's/etcd-//;s/.snap.age//').snap.age \
| file -
# Record the drill
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) restore drill OK" \
>> /opt/backup/railiance/cluster/restore-drill.log
```
**Done when:** cron installed under root, drill completes without error,
log entry written.
---
## References
- Decision D4: Integrated backup per capability (`DECISIONS.md`)
- Decision D2: Nextcloud as optional offsite extension (still valid, not a requirement)
- OAS Q3: Operability & Resilience
- Extension point EP-RAIL-005: Custodian full-stack backup orchestration
- k3s etcd snapshots: https://docs.k3s.io/datastore/backup-restore

View file

@ -0,0 +1,3 @@
"""Repo Manager — HelixForge repository integration boundary."""
__version__ = "0.1.0"

View file

@ -0,0 +1,3 @@
from repo_manager.cli import main
raise SystemExit(main())

View file

@ -0,0 +1,74 @@
"""Resolve exactly one authoritative owner for a State Hub record type."""
from __future__ import annotations
from pathlib import Path
from typing import Any
import yaml
AUTHORITY_SCHEMA = "repo-manager.hub-record-authority.v1"
VALID_CLASSES = frozenset(
{"file-derived", "hub-native-append", "hub-native-control", "derived-cache", "retired"}
)
DEFAULT_CONTRACT = Path(__file__).resolve().parents[2] / "config" / "hub-record-authority.yaml"
class AuthorityError(ValueError):
"""The requested route is missing or conflicts with the authority contract."""
def load_authority_contract(path: Path = DEFAULT_CONTRACT) -> dict[str, Any]:
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict) or data.get("schema") != AUTHORITY_SCHEMA:
raise AuthorityError(f"unsupported authority contract: {path}")
records = data.get("records")
if not isinstance(records, dict) or not records:
raise AuthorityError("authority contract has no records")
for record_type, rule in records.items():
if not isinstance(rule, dict):
raise AuthorityError(f"invalid rule for {record_type}")
if rule.get("class") not in VALID_CLASSES or not rule.get("owner"):
raise AuthorityError(f"incomplete authority rule for {record_type}")
return data
def resolve_record_authority(
record_type: str,
*,
repo_slug: str | None = None,
domain_slug: str | None = None,
claimed_owner: str | None = None,
contract_path: Path = DEFAULT_CONTRACT,
) -> dict[str, str]:
"""Return the one authority route or reject an ambiguous/conflicting write."""
contract = load_authority_contract(contract_path)
rule = contract["records"].get(record_type)
if not isinstance(rule, dict):
raise AuthorityError(f"unknown record type: {record_type}")
record_class = str(rule["class"])
owner = str(rule["owner"])
if claimed_owner is not None and claimed_owner != owner:
raise AuthorityError(
f"authority mismatch for {record_type}: contract={owner}, claimed={claimed_owner}"
)
if record_class == "file-derived":
if not repo_slug or not domain_slug:
raise AuthorityError(
f"file-derived {record_type} requires repo_slug and domain_slug"
)
authority_key = f"repository:{domain_slug}/{repo_slug}"
elif record_class == "retired":
authority_key = f"archive:{record_type}"
else:
authority_key = f"hub:{owner}"
return {
"schema": AUTHORITY_SCHEMA,
"record_type": record_type,
"record_class": record_class,
"owner": owner,
"authority_key": authority_key,
}

View file

@ -0,0 +1,156 @@
"""Rebuildable local-cache status and legacy provenance export."""
from __future__ import annotations
import hashlib
import json
from datetime import datetime
from pathlib import Path
from typing import Any
from urllib.request import urlopen
from repo_manager.gitops import head_sha
from repo_manager.index_store import default_index_path, load_index
from repo_manager.parse.record import iter_record_files
from repo_manager.parse.register import iter_register_files
from repo_manager.parse.workplan import iter_workplan_files
from repo_manager.time import utc_now, utc_now_text
CACHE_STATUS_SCHEMA = "repo-manager.cache-status.v1"
CLOSED_EXPORT_SCHEMA = "repo-manager.closed-workplan-provenance.v1"
def authoritative_source_files(repo_root: Path) -> list[Path]:
repo_root = repo_root.resolve()
paths = {
*iter_workplan_files(repo_root),
*iter_record_files(repo_root),
*iter_register_files(repo_root),
}
for name in (".repo-classification.yaml", "INTENT.md", "GOAL.md"):
path = repo_root / name
if path.is_file():
paths.add(path)
return sorted(paths, key=lambda path: str(path.relative_to(repo_root)))
def source_fingerprint(repo_root: Path) -> tuple[str, list[str]]:
"""Hash the paths and bytes that feed the local repository projection."""
repo_root = repo_root.resolve()
digest = hashlib.sha256()
relative_paths: list[str] = []
for path in authoritative_source_files(repo_root):
relative = str(path.relative_to(repo_root))
relative_paths.append(relative)
digest.update(relative.encode("utf-8"))
digest.update(b"\0")
digest.update(path.read_bytes())
digest.update(b"\0")
return digest.hexdigest(), relative_paths
def _parse_timestamp(value: str) -> datetime:
parsed = datetime.fromisoformat(value)
if parsed.tzinfo is None:
raise ValueError("cache observed_at must be timezone-aware")
return parsed
def cache_status(repo_root: Path) -> dict[str, Any]:
repo_root = repo_root.resolve()
path = default_index_path(repo_root)
checked_at = utc_now()
if not path.is_file():
return {
"schema": CACHE_STATUS_SCHEMA,
"ok": True,
"advisory": True,
"cache_present": False,
"stale": True,
"stale_reasons": ["cache_missing"],
"checked_at": checked_at.isoformat().replace("+00:00", "Z"),
"repo_root": str(repo_root),
"index_path": str(path),
}
index = load_index(path)
fingerprint, source_files = source_fingerprint(repo_root)
stale_reasons: list[str] = []
if not index.source_fingerprint:
stale_reasons.append("legacy_index_missing_source_fingerprint")
elif index.source_fingerprint != fingerprint:
stale_reasons.append("authoritative_source_changed")
try:
observed_at = _parse_timestamp(index.observed_at)
age_seconds = max(0, int((checked_at - observed_at).total_seconds()))
except (TypeError, ValueError):
observed_at = None
age_seconds = None
stale_reasons.append("invalid_observed_at")
return {
"schema": CACHE_STATUS_SCHEMA,
"ok": True,
"advisory": True,
"cache_present": True,
"stale": bool(stale_reasons),
"stale_reasons": stale_reasons,
"checked_at": checked_at.isoformat().replace("+00:00", "Z"),
"observed_at": index.observed_at,
"age_seconds": age_seconds,
"repo_root": str(repo_root),
"index_path": str(path),
"indexed_head_sha": index.head_sha,
"current_head_sha": head_sha(repo_root),
"source_fingerprint": index.source_fingerprint,
"current_source_fingerprint": fingerprint,
"source_file_count": len(source_files),
"record_count": len(index.work_records),
}
def build_closed_provenance_export(
workplans: list[dict[str, Any]],
repos: list[dict[str, Any]],
*,
source: str,
) -> dict[str, Any]:
repo_slugs = {str(repo["id"]): repo.get("slug") for repo in repos if repo.get("id")}
rows = [
{
"id": row.get("id"),
"repo_id": row.get("repo_id"),
"repo_slug": repo_slugs.get(str(row.get("repo_id"))),
"slug": row.get("slug"),
"title": row.get("title"),
"status": row.get("status"),
"owner": row.get("owner"),
"created_at": row.get("created_at"),
"updated_at": row.get("updated_at"),
}
for row in workplans
if row.get("status") in {"finished", "archived"} and not row.get("backing_filename")
]
rows.sort(key=lambda row: (str(row["status"]), str(row["repo_slug"]), str(row["slug"])))
canonical_rows = json.dumps(rows, sort_keys=True, separators=(",", ":")).encode("utf-8")
return {
"schema": CLOSED_EXPORT_SCHEMA,
"ok": True,
"generated_at": utc_now_text(),
"source": source,
"filter": "backing_filename is null and status in [finished, archived]",
"count": len(rows),
"rows_sha256": hashlib.sha256(canonical_rows).hexdigest(),
"rows": rows,
}
def fetch_closed_provenance(api_base: str) -> dict[str, Any]:
base = api_base.rstrip("/")
with urlopen(f"{base}/workplans/", timeout=30) as response:
workplans = json.load(response)
with urlopen(f"{base}/repos/?limit=500", timeout=30) as response:
repos = json.load(response)
if not isinstance(workplans, list) or not isinstance(repos, list):
raise TypeError("State Hub returned a non-list workplan or repository payload")
return build_closed_provenance_export(workplans, repos, source=base)

View file

@ -0,0 +1,116 @@
"""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", "publication"}
)
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

@ -0,0 +1,3 @@
from repo_manager.commands.task_status import update_task_status
__all__ = ["update_task_status"]

View file

@ -0,0 +1,946 @@
"""Governed scaffolding for rapp-* managed workload packages."""
from __future__ import annotations
import argparse
import re
import shutil
import subprocess
from pathlib import Path
from typing import Any
PACKAGE_TYPES = (
"helm-managed-platform-service",
"manifest-managed-platform-service",
"knative-managed-service",
"grouped-composition",
)
RAILS = ("rail-kubernetes", "rail-knative")
CLASSIFICATIONS = ("public", "internal", "confidential", "restricted")
CRITICALITIES = ("low", "medium", "high", "critical")
COMPUTE_REEFS = ("reef-railiance",)
SLUG = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
REEF_SLUG = re.compile(r"^reef-[a-z0-9]+(-[a-z0-9]+)*$")
DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
EXPOSE_RE = re.compile(r"^EXPOSE\s+(\d+)", re.MULTILINE)
USER_RE = re.compile(r"^USER\s+(\d+)", re.MULTILINE)
HEALTH_PATH_RE = re.compile(r'path:\s*["\']?(/[A-Za-z0-9._/-]+)')
IMAGE_PIN_RE = re.compile(
r"(forgejo\.coulomb\.social/coulomb/[a-z0-9-]+@)sha256:[0-9a-f]{64}"
)
UNSET_DIGEST = "sha256:" + ("0" * 64)
_FAMILY_VALIDATOR = Path.home() / "railiance-master" / "tools" / "validate-family-declarations.py"
def _refuse(message: str) -> dict[str, Any]:
return {"ok": False, "error": message}
def _write(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content if content.endswith("\n") else content + "\n")
def _underscore(app: str) -> str:
return app.replace("-", "_")
def init(
path: Path,
*,
app: str,
ownership_repo: str,
rail: str = "rail-kubernetes",
classification: str = "confidential",
criticality: str = "high",
package_type: str = "manifest-managed-platform-service",
purpose: str | None = None,
force: bool = False,
) -> dict[str, Any]:
check = _check_identity(app, ownership_repo, rail, package_type, classification, criticality)
if check:
return check
rapp_id = f"rapp-{app}"
dest = path.expanduser().resolve()
dest.mkdir(parents=True, exist_ok=True)
declaration = dest / "declarations" / "rapp.yaml"
if declaration.is_file() and not force:
return _refuse(f"{declaration} already exists; refuse to overwrite a live contract")
purpose_text = purpose or f"Package and operate the {app} service on Railiance."
files = {
".repo-classification.yaml": (
"repo_classification:\n"
' standard: Repo Classification Standard\n'
' version: "1.0"\n'
" classified_by: repo-manager\n"
" category: project\n"
" domain: infotech\n"
),
"README.md": f"# {rapp_id}\n\nManaged runtime package for `{app}`.\n",
"INTENT.md": (
f"# Intent\n\nProvide the managed Railiance runtime package for `{app}`.\n"
),
"SCOPE.md": (
"# Scope\n\n## In scope\n\n- packaging, smoke, rollback, and the family declaration\n\n"
"## Out of scope\n\n- application domain ownership\n- credential values\n"
),
"AGENTS.md": (
f"# Repository agent guide\n\nWorkplans use the prefix "
f"`{rapp_id.upper()}-WP-`.\n\n"
"Cross-repo waits: attach to a `prj-` driver "
"(`coordination-engine` `spec/cross-owner-wait-mode-v0.1.md`). "
"Do not strand a `wait` or implement residual flavor.\n"
),
"CLAUDE.md": f"# {rapp_id}\n\n@SCOPE.md\n@AGENTS.md\n",
"Makefile": (
"SHELL := /bin/bash\n\n.PHONY: check\n\ncheck:\n"
"\tpython3 -m unittest discover -s tests -v\n"
),
".gitignore": ".rendered/\n__pycache__/\n",
"declarations/rapp.yaml": _declaration(
rapp_id=rapp_id,
app=app,
ownership_repo=ownership_repo,
rail=rail,
classification=classification,
criticality=criticality,
package_type=package_type,
purpose=purpose_text,
),
f"workplans/{rapp_id.upper()}-WP-0001-bootstrap.md": (
f"---\nid: {rapp_id.upper()}-WP-0001\ntype: workplan\n"
f'title: "Bootstrap {rapp_id}"\nstatus: proposed\n'
f"flavor: planning\ndepends_on: []\n---\n\n"
f"# Bootstrap {rapp_id}\n"
),
}
written = []
for rel, content in files.items():
target = dest / rel
if target.is_file() and not force and rel not in {"declarations/rapp.yaml"}:
continue
_write(target, content)
written.append(rel)
(dest / "workplans" / "archived").mkdir(exist_ok=True)
return {"ok": True, "path": str(dest), "rapp_id": rapp_id, "written": written}
def _check_identity(
app: str,
ownership_repo: str,
rail: str,
package_type: str,
classification: str,
criticality: str,
) -> dict[str, Any] | None:
if not SLUG.match(app) or app.startswith("rapp-"):
return _refuse("app must be a workload slug without the rapp- prefix")
if rail not in RAILS:
return _refuse(f"unknown rail {rail!r}; allowed: {', '.join(RAILS)}")
if package_type not in PACKAGE_TYPES:
return _refuse(f"unknown package_type {package_type!r}")
if classification not in CLASSIFICATIONS:
return _refuse(f"unknown classification {classification!r}")
if criticality not in CRITICALITIES:
return _refuse(f"unknown criticality {criticality!r}")
if not SLUG.match(ownership_repo) or ownership_repo.startswith("rapp-"):
return _refuse("ownership_repo must be an app or layer slug, not the rapp itself")
return None
def _declaration(
*,
rapp_id: str,
app: str,
ownership_repo: str,
rail: str,
classification: str,
criticality: str,
package_type: str,
purpose: str,
) -> str:
return (
"kind: managed-workload-package\n"
"repo_family: rapp\n"
f"rapp_id: {rapp_id}\n"
f"repo: {rapp_id}\n"
f"ownership_repo: {ownership_repo}\n"
"contract_version: 1.0.0\n"
"readiness_state: draft\n"
"workload_identity:\n"
f" name: {app}\n"
f" package_type: {package_type}\n"
f"data_classification: {classification}\n"
f"criticality: {criticality}\n"
f"primary_rail: {rail}\n"
"supported_rails:\n"
f" - {rail}\n"
"bound_reefs: []\n"
"runtime_dependencies:\n"
" - kubernetes-api\n"
" - openbao-database-secrets-engine\n"
"composition:\n"
f" purpose: {purpose}\n"
" member_repos:\n"
f" - repo: {rapp_id}\n"
" role: managed runtime package\n"
" deployables:\n"
f" - {app}\n"
"rollout_contract:\n"
" default_mode: kubectl-server-side-apply\n"
"smoke_contract:\n"
" required:\n"
" - healthz-ok\n"
"rollback_contract:\n"
" order:\n"
" - previous-immutable-image-digest\n"
" - apply-reviewed-git-revision\n"
)
def inspect_app(from_app: Path, app: str) -> dict[str, Any]:
root = from_app.expanduser().resolve()
container = ""
for name in ("Containerfile", "Dockerfile"):
candidate = root / name
if candidate.is_file():
container = candidate.read_text()
break
expose = EXPOSE_RE.search(container)
user = USER_RE.search(container)
port = int(expose.group(1)) if expose else 8080
uid = int(user.group(1)) if user else 10001
deploy_dir = root / "deploy"
deploy_files = sorted(deploy_dir.glob("*.yaml")) if deploy_dir.is_dir() else []
health = "/healthz"
ready = "/readyz"
blob = container
for path in deploy_files:
blob += "\n" + path.read_text()
for src in (root / "src").rglob("*.py") if (root / "src").is_dir() else []:
try:
blob += "\n" + src.read_text()
except OSError:
continue
if len(blob) > 400_000:
break
paths = HEALTH_PATH_RE.findall(blob)
if "/health" in paths and "/healthz" not in paths:
health = "/health"
ready = "/health"
if "/healthz" in paths:
health = "/healthz"
if "/readyz" in paths:
ready = "/readyz"
digest = UNSET_DIGEST
pin = IMAGE_PIN_RE.search(blob)
if pin:
digest = "sha256:" + pin.group(0).rsplit("sha256:", 1)[1]
return {
"app": app,
"root": str(root),
"port": port,
"uid": uid,
"health_path": health,
"ready_path": ready,
"image_repository": f"forgejo.coulomb.social/coulomb/{app}",
"image_digest": digest,
"deploy_files": [str(p) for p in deploy_files],
"has_chart": (root / "helm").is_dir() or (root / "charts").is_dir(),
"has_image_workflow": (root / ".forgejo" / "workflows" / "image.yaml").is_file(),
}
def skeleton(
path: Path,
*,
from_app: Path,
app: str | None = None,
package_type: str = "manifest-managed-platform-service",
force: bool = False,
dedicated_postgres: bool = False,
) -> dict[str, Any]:
dest = path.expanduser().resolve()
app_name = app or dest.name.removeprefix("rapp-")
if package_type == "helm-managed-platform-service":
return _refuse("Helm skeleton is not generated; pass an existing chart or use manifests")
if package_type not in PACKAGE_TYPES:
return _refuse(f"unknown package_type {package_type!r}")
facts = inspect_app(from_app, app_name)
written: list[str] = []
notes: list[str] = []
if facts["deploy_files"]:
manifests = dest / "manifests"
if manifests.exists() and any(manifests.glob("*.yaml")) and not force:
return _refuse(f"{manifests} already has manifests; pass --force to replace")
manifests.mkdir(parents=True, exist_ok=True)
for src in facts["deploy_files"]:
target = manifests / Path(src).name
shutil.copy2(src, target)
written.append(str(target.relative_to(dest)))
notes.append(f"absorbed {len(facts['deploy_files'])} file(s) from {from_app}/deploy")
else:
runtime = dest / "manifests" / "runtime.yaml"
if runtime.is_file() and not force:
return _refuse(f"{runtime} already exists; pass --force to replace")
_write(runtime, _runtime_manifest(app_name, facts))
written.append("manifests/runtime.yaml")
notes.append("generated Deployment/Service/ServiceAccount/NetworkPolicy")
if dedicated_postgres:
notes.append("dedicated Cluster not generated; pass reviewed CNPG YAML by hand")
makefile = dest / "Makefile"
if not makefile.is_file() or force or makefile.read_text().count("\n") < 12:
_write(makefile, _makefile(app_name, facts, dest.name))
written.append("Makefile")
render = dest / "tools" / "render.py"
if not render.is_file() or force:
_write(render, _render_py(app_name))
render.chmod(0o755)
written.append("tools/render.py")
verify = dest / "tools" / "verify_live.sh"
if not verify.is_file() or force:
_write(verify, _verify_sh(app_name, facts))
verify.chmod(0o755)
written.append("tools/verify_live.sh")
test = dest / "tests" / "test_packaging.py"
if not test.is_file() or force:
_write(test, _packaging_test(app_name))
written.append("tests/test_packaging.py")
return {"ok": True, "path": str(dest), "facts": facts, "written": written, "notes": notes}
def ensure_image_workflow(from_app: Path, app: str) -> dict[str, Any]:
root = from_app.expanduser().resolve()
if not root.is_dir():
return _refuse(f"app checkout missing: {root}")
target = root / ".forgejo" / "workflows" / "image.yaml"
if target.is_file():
return {"ok": True, "path": str(target), "written": False, "note": "already present"}
_write(target, _image_workflow(app))
return {
"ok": True,
"path": str(target),
"written": True,
"note": "first deploy waits on the CI digest; no workstation build",
}
def draft_postgres_consumer(
path: Path,
*,
app: str,
family_root: Path | None = None,
) -> dict[str, Any]:
dest = path.expanduser().resolve()
app_name = app or dest.name.removeprefix("rapp-")
slug = _underscore(app_name)
draft = (
"apiVersion: rapp-postgres.railiance.io/v1alpha1\n"
"kind: PostgresConsumer\n"
"metadata:\n"
f" name: {app_name}\n"
"spec:\n"
f" database: {slug}\n"
f" schema: {slug}\n"
f" costAttributionKey: platform:{app_name}\n"
f" clientNamespaces: [{app_name}]\n"
" roles:\n"
f" owner: {slug}_owner\n"
f" migration: {slug}_migrate\n"
f" runtime: {slug}_app\n"
" tenantKeyingRequired: true\n"
" # Draft only. Do not apply from this package. Review in rapp-postgres.\n"
)
handoff = dest / "handoffs" / "postgres-consumer.yaml"
search = (family_root or dest.parent).resolve()
live = search / "rapp-postgres" / "consumers" / f"{app_name}.yaml"
notes = []
if live.is_file():
_write(
dest / "handoffs" / "README.md",
f"# Handoffs\n\nPostgres consumer already lives at `{live}`.\n"
"This package does not apply it.\n",
)
notes.append(f"existing consumer left in place: {live}")
return {"ok": True, "path": str(live), "written": False, "notes": notes}
_write(handoff, draft)
notes.append(str(handoff))
if (search / "rapp-postgres" / "consumers").is_dir():
target = search / "rapp-postgres" / "consumers" / f"{app_name}.yaml"
if not target.is_file():
_write(target, draft)
notes.append(str(target))
return {"ok": True, "path": str(handoff), "written": True, "notes": notes}
def wrap(
path: Path,
*,
app: str,
ownership_repo: str,
from_app: Path,
rail: str = "rail-kubernetes",
classification: str = "confidential",
criticality: str = "high",
package_type: str = "manifest-managed-platform-service",
purpose: str | None = None,
force: bool = False,
dedicated_postgres: bool = False,
family_root: Path | None = None,
) -> dict[str, Any]:
dest = path.expanduser().resolve()
created = init(
dest,
app=app,
ownership_repo=ownership_repo,
rail=rail,
classification=classification,
criticality=criticality,
package_type=package_type,
purpose=purpose,
force=force,
)
if not created.get("ok"):
return created
skel = skeleton(
dest,
from_app=from_app,
app=app,
package_type=package_type,
force=force,
dedicated_postgres=dedicated_postgres,
)
if not skel.get("ok"):
return skel
image = ensure_image_workflow(from_app, app)
if not image.get("ok"):
return image
consumer = {} if dedicated_postgres else draft_postgres_consumer(
dest, app=app, family_root=family_root or dest.parent
)
if consumer and not consumer.get("ok"):
return consumer
checked = validate(dest, family_root=family_root or dest.parent)
return {
"ok": True,
"path": str(dest),
"init": created,
"skeleton": skel,
"image_workflow": image,
"postgres_consumer": consumer,
"validate": checked,
"placed": False,
"applied": False,
}
def place(path: Path, *, reef: str, family_root: Path | None = None) -> dict[str, Any]:
if not REEF_SLUG.match(reef):
return _refuse(f"unknown reef {reef!r}")
if reef == "reef-storage":
return _refuse("reef-storage hosts no rail; it is a consumed capability")
dest = path.expanduser().resolve()
declaration = dest / "declarations" / "rapp.yaml"
if not declaration.is_file():
return _refuse(f"missing {declaration}")
search = (family_root or dest.parent).resolve()
if reef not in COMPUTE_REEFS and not (search / reef).is_dir():
return _refuse(f"reef {reef!r} is not a known compute reef")
text = declaration.read_text()
if re.search(r"^bound_reefs:\n - ", text, re.MULTILINE):
text = re.sub(
r"^bound_reefs:\n(?: - .+\n)+",
f"bound_reefs:\n - {reef}\n",
text,
flags=re.MULTILINE,
)
else:
text = re.sub(
r"^bound_reefs:\s*\[\]\s*$",
f"bound_reefs:\n - {reef}",
text,
flags=re.MULTILINE,
)
if "exposure:" in text and "posture: public" in text:
return _refuse("place does not grant public exposure; edit exposure separately")
declaration.write_text(text)
return {"ok": True, "path": str(dest), "bound_reefs": [reef]}
def validate(path: Path, *, family_root: Path | None = None) -> dict[str, Any]:
dest = path.expanduser().resolve()
declaration = dest / "declarations" / "rapp.yaml"
if not declaration.is_file():
return _refuse(f"missing {declaration}")
missing = [
name
for name in (
"README.md",
"INTENT.md",
"SCOPE.md",
"AGENTS.md",
".repo-classification.yaml",
)
if not (dest / name).is_file()
]
if missing:
return _refuse(f"missing bootstrap files: {', '.join(missing)}")
if _FAMILY_VALIDATOR.is_file():
import tempfile
search_root = (family_root or dest.parent).resolve()
with tempfile.TemporaryDirectory(prefix="rmgr-rapp-") as tmp:
tmp_root = Path(tmp)
(tmp_root / dest.name).symlink_to(dest)
for sibling in ("rail-kubernetes", "rail-knative", "reef-railiance"):
candidate = search_root / sibling
if candidate.is_dir():
(tmp_root / sibling).symlink_to(candidate)
proc = subprocess.run(
["python3", str(_FAMILY_VALIDATOR), "--root", str(tmp_root)],
check=False,
capture_output=True,
text=True,
)
return {
"ok": proc.returncode == 0,
"path": str(dest),
"validator": str(_FAMILY_VALIDATOR),
"stdout": proc.stdout.strip(),
"stderr": proc.stderr.strip(),
"exit_code": proc.returncode,
}
text = declaration.read_text()
required = (
"kind: managed-workload-package",
"repo_family: rapp",
"ownership_repo:",
"primary_rail:",
"composition:",
)
absent = [item for item in required if item not in text]
if absent:
return _refuse(f"declaration missing {absent}")
return {"ok": True, "path": str(dest), "validator": None}
def pin_image(path: Path, digest: str) -> dict[str, Any]:
if not DIGEST.fullmatch(digest):
return _refuse("digest must be sha256:<64 lowercase hex>")
dest = path.expanduser().resolve()
rewritten = 0
for rel in ("manifests", "bindings", "declarations"):
root = dest / rel
if not root.exists():
continue
files = [root] if root.is_file() else list(root.rglob("*"))
for file in files:
if not file.is_file():
continue
text = file.read_text()
updated, n = IMAGE_PIN_RE.subn(rf"\g<1>{digest}", text)
if n:
file.write_text(updated)
rewritten += n
if rewritten == 0:
return _refuse("no forgejo.coulomb.social digest pin found")
makefile = dest / "Makefile"
if makefile.is_file():
makefile.write_text(
re.sub(r"sha256:[0-9a-f]{64}", digest, makefile.read_text(), count=1)
)
return {"ok": True, "path": str(dest), "digest": digest, "rewritten": rewritten}
def _runtime_manifest(app: str, facts: dict[str, Any]) -> str:
port = facts["port"]
uid = facts["uid"]
health = facts["health_path"]
ready = facts["ready_path"]
image = f"{facts['image_repository']}@{facts['image_digest']}"
return f"""apiVersion: v1
kind: Namespace
metadata:
name: {app}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {app}
namespace: {app}
labels:
app.kubernetes.io/name: {app}
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: {app}
template:
metadata:
labels:
app.kubernetes.io/name: {app}
spec:
automountServiceAccountToken: false
serviceAccountName: {app}
securityContext:
runAsNonRoot: true
runAsUser: {uid}
seccompProfile:
type: RuntimeDefault
containers:
- name: {app}
image: {image}
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: {port}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
readinessProbe:
httpGet:
path: {ready}
port: http
livenessProbe:
httpGet:
path: {health}
port: http
---
apiVersion: v1
kind: Service
metadata:
name: {app}
namespace: {app}
spec:
selector:
app.kubernetes.io/name: {app}
ports:
- name: http
port: {port}
targetPort: http
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {app}
namespace: {app}
automountServiceAccountToken: false
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {app}-default-deny
namespace: {app}
spec:
podSelector: {{}}
policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {app}-runtime
namespace: {app}
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: {app}
policyTypes: [Ingress, Egress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- {{protocol: TCP, port: {port}}}
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- {{protocol: UDP, port: 53}}
- {{protocol: TCP, port: 53}}
"""
def _makefile(app: str, facts: dict[str, Any], field_manager: str) -> str:
digest = facts["image_digest"]
repo = facts["image_repository"]
return f"""SHELL := /bin/bash
TARGET ?= railiance01
NAMESPACE := {app}
DEPLOYMENT := {app}
IMAGE_REPOSITORY := {repo}
IMAGE_DIGEST ?= {digest}
DIGEST ?=
RENDERED := .rendered
MANIFESTS := $(wildcard manifests/*.yaml)
.PHONY: check test render validate-inputs server-dry-run deploy status verify-live rollback
check: test
test:
python3 -m unittest discover -s tests -v
validate-inputs:
@echo "$(IMAGE_DIGEST)" | grep -Eq '^sha256:[0-9a-f]{{64}}$$' \\
|| (echo "IMAGE_DIGEST must be sha256:<64 hex>" >&2; exit 2)
render: validate-inputs
@rm -rf $(RENDERED)
@mkdir -p $(RENDERED)
IMAGE_REPOSITORY=$(IMAGE_REPOSITORY) IMAGE_DIGEST=$(IMAGE_DIGEST) \\
python3 tools/render.py $(MANIFESTS) --out $(RENDERED)
server-dry-run: render
{{ for f in $(RENDERED)/*.yaml; do echo '---'; cat "$$f"; done; }} \\
| ssh -o BatchMode=yes $(TARGET) \\
kubectl apply --server-side --force-conflicts --dry-run=server -f -
deploy: render
{{ for f in $(RENDERED)/*.yaml; do echo '---'; cat "$$f"; done; }} \\
| ssh -o BatchMode=yes $(TARGET) \\
kubectl apply --server-side --force-conflicts --field-manager={field_manager} -f -
ssh -o BatchMode=yes $(TARGET) \\
kubectl -n $(NAMESPACE) rollout status deployment/$(DEPLOYMENT) --timeout=180s
status:
ssh -o BatchMode=yes $(TARGET) kubectl -n $(NAMESPACE) get deploy,pods,svc
verify-live: validate-inputs
EXPECTED_IMAGE="$(IMAGE_REPOSITORY)@$(IMAGE_DIGEST)" \\
TARGET=$(TARGET) NAMESPACE=$(NAMESPACE) ./tools/verify_live.sh
rollback:
@test -n "$(DIGEST)" || (echo "DIGEST=sha256:<64 hex> is required" >&2; exit 2)
$(MAKE) deploy IMAGE_DIGEST=$(DIGEST)
"""
def _render_py(app: str) -> str:
return f'''#!/usr/bin/env python3
"""Rewrite digest pins into rendered manifests."""
from __future__ import annotations
import argparse
import os
import re
import sys
from pathlib import Path
IMAGE_LINE = re.compile(
r"(image:\\s+)(forgejo\\.coulomb\\.social/coulomb/{app})(@sha256:[0-9a-f]{{64}})?"
)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("manifests", nargs="+", type=Path)
parser.add_argument("--out", required=True, type=Path)
args = parser.parse_args()
repo = os.environ.get("IMAGE_REPOSITORY", "forgejo.coulomb.social/coulomb/{app}")
digest = os.environ.get("IMAGE_DIGEST", "")
if not re.fullmatch(r"sha256:[0-9a-f]{{64}}", digest):
print("IMAGE_DIGEST must be sha256:<64 lowercase hex>", file=sys.stderr)
return 2
replacement = rf"\\1{{repo}}@{{digest}}"
args.out.mkdir(parents=True, exist_ok=True)
for src in args.manifests:
(args.out / src.name).write_text(IMAGE_LINE.sub(replacement, src.read_text()))
return 0
if __name__ == "__main__":
raise SystemExit(main())
'''
def _verify_sh(app: str, facts: dict[str, Any]) -> str:
port = facts["port"]
health = facts["health_path"]
ready = facts["ready_path"]
return f"""#!/usr/bin/env bash
set -euo pipefail
TARGET="${{TARGET:-railiance01}}"
NAMESPACE="${{NAMESPACE:-{app}}}"
DEPLOYMENT="${{DEPLOYMENT:-{app}}}"
EXPECTED_IMAGE="${{EXPECTED_IMAGE:?EXPECTED_IMAGE is required}}"
remote() {{
ssh -o BatchMode=yes "$TARGET" "$1"
}}
live_image="$(remote "kubectl -n ${{NAMESPACE}} get deploy ${{DEPLOYMENT}} -o jsonpath='{{.spec.template.spec.containers[0].image}}'")"
if [[ "$live_image" != "$EXPECTED_IMAGE" ]]; then
echo "digest mismatch live=$live_image expected=$EXPECTED_IMAGE" >&2
exit 1
fi
remote "kubectl -n ${{NAMESPACE}} rollout status deployment/${{DEPLOYMENT}} --timeout=60s >/dev/null"
health="$(remote "kubectl -n ${{NAMESPACE}} exec deploy/${{DEPLOYMENT}} -- python3 -c \\"import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:{port}{health}').status)\\"")"
ready="$(remote "kubectl -n ${{NAMESPACE}} exec deploy/${{DEPLOYMENT}} -- python3 -c \\"import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:{port}{ready}').status)\\"")"
[[ "$health" == "200" ]]
[[ "$ready" == "200" ]]
python3 - "$live_image" "$health" "$ready" <<'PY'
import json, sys
print(json.dumps({{
"health_ok": sys.argv[2] == "200",
"ready_ok": sys.argv[3] == "200",
"live_image": sys.argv[1],
"secret_values_observed": False,
}}, sort_keys=True))
PY
"""
def _packaging_test(app: str) -> str:
return f'''import pathlib
import re
import unittest
ROOT = pathlib.Path(__file__).resolve().parents[1]
class PackagingTests(unittest.TestCase):
def test_declaration_names_the_workload(self):
decl = (ROOT / "declarations" / "rapp.yaml").read_text()
self.assertIn("ownership_repo:", decl)
self.assertIn("name: {app}", decl.split("workload_identity:", 1)[1][:200])
self.assertNotIn("posture: public", decl)
def test_package_has_no_floating_tag(self):
texts = []
for folder in ("manifests", "declarations"):
root = ROOT / folder
if not root.exists():
continue
for path in root.rglob("*"):
if path.is_file():
texts.append(path.read_text())
blob = "\\n".join(texts)
self.assertNotRegex(blob, re.compile(r"image:\\s+[^\\n]+:(latest|main)\\b"))
self.assertNotIn("sk-", blob)
'''
def _image_workflow(app: str) -> str:
return f"""name: Build and Publish Container Image
# Images are built by CI from a tarball of the pushed commit, never from
# a workstation working tree.
on:
push:
branches:
- main
paths:
- ".forgejo/workflows/image.yaml"
- "Containerfile"
- "src/**"
- "pyproject.toml"
- "README.md"
- "LICENSE"
workflow_dispatch:
env:
REGISTRY: forgejo.coulomb.social
IMAGE_NAME: coulomb/{app}
DOCKER_HOST: tcp://127.0.0.1:2375
jobs:
build-and-push:
runs-on: container-build
steps:
- name: Build and push image
env:
REGISTRY_USER: ${{{{ secrets.REGISTRY_USER }}}}
REGISTRY_TOKEN: ${{{{ secrets.REGISTRY_TOKEN }}}}
run: |
set -eu
REF="${{GITHUB_SHA:-main}}"
SHORT="${{REF:0:7}}"
mkdir -p buildctx "${{HOME}}/bin"
wget -qO /tmp/repo.tar.gz \\
"https://forgejo.coulomb.social/${{GITHUB_REPOSITORY}}/archive/${{SHORT}}.tar.gz"
tar xzf /tmp/repo.tar.gz -C buildctx --strip-components=1
wget -qO- https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \\
| tar xz --strip-components=1 -C "${{HOME}}/bin" docker/docker
export PATH="${{HOME}}/bin:${{PATH}}"
echo "${{REGISTRY_TOKEN}}" | docker login "${{REGISTRY}}" -u "${{REGISTRY_USER}}" --password-stdin
IMAGE="${{REGISTRY}}/${{IMAGE_NAME}}"
docker build -f buildctx/Containerfile -t "${{IMAGE}}:latest" -t "${{IMAGE}}:main-${{SHORT}}" buildctx
docker push "${{IMAGE}}:latest"
docker push "${{IMAGE}}:main-${{SHORT}}"
echo "pushed ${{IMAGE}}:latest and ${{IMAGE}}:main-${{SHORT}}"
- name: Report immutable digest
run: |
set -eu
export PATH="${{HOME}}/bin:${{PATH}}"
IMAGE="${{REGISTRY}}/${{IMAGE_NAME}}"
SHORT="${{GITHUB_SHA:0:7}}"
docker inspect --format='{{{{index .RepoDigests 0}}}}' "${{IMAGE}}:main-${{SHORT}}"
"""
def add_rapp_parser(sub: argparse._SubParsersAction) -> None:
rapp = sub.add_parser("rapp", help="Scaffold or validate a rapp-* package")
rapp_sub = rapp.add_subparsers(dest="rapp_command", required=True)
p_init = rapp_sub.add_parser("init", help="Create a draft rapp-* baseline")
p_init.add_argument("--path", required=True, help="Destination checkout")
p_init.add_argument("--app", required=True, help="Workload name (no rapp- prefix)")
p_init.add_argument("--ownership-repo", required=True)
p_init.add_argument("--rail", default="rail-kubernetes", choices=RAILS)
p_init.add_argument("--classification", default="confidential", choices=CLASSIFICATIONS)
p_init.add_argument("--criticality", default="high", choices=CRITICALITIES)
p_init.add_argument("--package-type", default="manifest-managed-platform-service", choices=PACKAGE_TYPES)
p_init.add_argument("--purpose", default=None)
p_init.add_argument("--force", action="store_true")
p_skel = rapp_sub.add_parser("skeleton", help="Generate or absorb runtime manifests")
p_skel.add_argument("--path", required=True)
p_skel.add_argument("--from-app", required=True)
p_skel.add_argument("--app", default=None)
p_skel.add_argument("--package-type", default="manifest-managed-platform-service", choices=PACKAGE_TYPES)
p_skel.add_argument("--force", action="store_true")
p_skel.add_argument("--dedicated-postgres", action="store_true")
p_wrap = rapp_sub.add_parser("wrap", help="init + skeleton + image + consumer + validate")
p_wrap.add_argument("--path", required=True)
p_wrap.add_argument("--app", required=True)
p_wrap.add_argument("--ownership-repo", required=True)
p_wrap.add_argument("--from-app", required=True)
p_wrap.add_argument("--rail", default="rail-kubernetes", choices=RAILS)
p_wrap.add_argument("--classification", default="confidential", choices=CLASSIFICATIONS)
p_wrap.add_argument("--criticality", default="high", choices=CRITICALITIES)
p_wrap.add_argument("--package-type", default="manifest-managed-platform-service", choices=PACKAGE_TYPES)
p_wrap.add_argument("--purpose", default=None)
p_wrap.add_argument("--family-root", default=None)
p_wrap.add_argument("--force", action="store_true")
p_wrap.add_argument("--dedicated-postgres", action="store_true")
p_place = rapp_sub.add_parser("place", help="Set bound_reefs only")
p_place.add_argument("--path", required=True)
p_place.add_argument("--reef", default="reef-railiance")
p_place.add_argument("--family-root", default=None)
p_val = rapp_sub.add_parser("validate", help="Validate a rapp-* checkout")
p_val.add_argument("--path", required=True)
p_val.add_argument("--family-root", default=None)
p_pin = rapp_sub.add_parser("pin-image", help="Rewrite digest pins")
p_pin.add_argument("--path", required=True)
p_pin.add_argument("--digest", required=True)

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 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
from repo_manager.time import utc_now_text
_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 utc_now_text()
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

@ -0,0 +1,311 @@
"""Governed mutation spine for repository-owned registers."""
from __future__ import annotations
import re
import uuid
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.register import REGISTER_SCHEMA, SUPPORTED_REGISTER_KINDS, register_path
from repo_manager.time import utc_now_text
_ENTRY_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
_PROTECTED_FIELDS = frozenset({"id", "title", "status", "notes", "created", "updated"})
_REQUIRED_DATA = {
"sbom-inventory": frozenset({"package_name", "ecosystem"}),
"repo-goals": frozenset({"description"}),
"upstream-contributions": frozenset({"type"}),
"technical-debt": frozenset(),
"extension-points": frozenset(),
"register-entries": frozenset({"register_kind"}),
}
def _now() -> str:
return utc_now_text()
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 _load(path: Path, kind: str) -> dict[str, Any]:
if not path.is_file():
return {"schema": REGISTER_SCHEMA, "kind": kind, "entries": []}
try:
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
except yaml.YAMLError as exc:
raise ValueError(f"invalid register YAML: {exc}") from exc
if not isinstance(data, dict) or not isinstance(data.get("entries", []), list):
raise TypeError("register must be a mapping with an entries list")
if data.get("kind", kind) != kind:
raise ValueError(f"register kind does not match filename {kind!r}")
data.setdefault("schema", REGISTER_SCHEMA)
data.setdefault("kind", kind)
data.setdefault("entries", [])
return data
def _save(path: Path, document: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
yaml.safe_dump(document, sort_keys=False, allow_unicode=True),
encoding="utf-8",
)
def mutate_register_entry(
repo_root: Path,
kind: str,
entry_id: str,
*,
operation: str,
title: str | None = None,
status: str | None = None,
data: dict[str, Any] | None = None,
note: str | None = None,
note_author: 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:
"""Upsert, defer, or annotate an entry in one of the shared register kinds."""
command_map = {
"upsert": "repo.register.upsert_entry",
"defer": "repo.register.defer_entry",
"note": "repo.register.add_note",
}
correlation_id = correlation_id or str(uuid.uuid4())
command = command_map.get(operation, "repo.register.upsert_entry")
repo_root = repo_root.resolve()
payload = {
"repo_root": str(repo_root),
"kind": kind,
"entry_id": entry_id,
"operation": operation,
"title": title,
"status": status,
"data": data,
"note": note,
"note_author": note_author,
"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 operation not in command_map:
return _reject(command, correlation_id, "validation_error", f"invalid operation {operation!r}")
if kind not in SUPPORTED_REGISTER_KINDS:
return _reject(command, correlation_id, "validation_error", f"unsupported register {kind!r}")
if not _ENTRY_ID_RE.fullmatch(entry_id):
return _reject(command, correlation_id, "validation_error", f"invalid entry id {entry_id!r}")
if status is not None and not status.strip():
return _reject(command, correlation_id, "validation_error", "status cannot be empty")
if data is not None and not isinstance(data, dict):
return _reject(command, correlation_id, "validation_error", "data must be an object")
protected = _PROTECTED_FIELDS.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 expected_head_sha != current_head:
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
snapshot, _index = observe_repository(repo_root, slug=repo_slug)
slug = repo_slug or snapshot.get("slug")
path = register_path(repo_root, kind)
try:
document = _load(path, kind)
except (TypeError, ValueError) as exc:
return _reject(command, correlation_id, "conflict", str(exc))
entries = document["entries"]
matches = [item for item in entries if isinstance(item, dict) and str(item.get("id")) == entry_id]
if len(matches) > 1:
return _reject(command, correlation_id, "conflict", f"duplicate entry id {entry_id!r}")
entry = matches[0] if matches else None
before = yaml.safe_dump(entry, sort_keys=True) if entry is not None else None
now = _now()
if operation == "upsert":
if entry is None:
if not title or not title.strip():
return _reject(command, correlation_id, "validation_error", "new entry requires title")
missing = sorted(key for key in _REQUIRED_DATA[kind] if key not in (data or {}))
if missing:
return _reject(
command,
correlation_id,
"validation_error",
f"{kind} entry missing data fields: {', '.join(missing)}",
)
entry = {
"id": entry_id,
"title": title.strip(),
"status": (status or "open").strip(),
**(data or {}),
"created": now,
"updated": now,
}
entries.append(entry)
else:
if title is not None:
if not title.strip():
return _reject(command, correlation_id, "validation_error", "title cannot be empty")
entry["title"] = title.strip()
if status is not None:
entry["status"] = status.strip()
entry.update(data or {})
entry["updated"] = now
elif operation == "defer":
if entry is None:
return _reject(command, correlation_id, "not_found", f"entry {entry_id!r} not found")
entry["status"] = (status or "deferred").strip()
entry["updated"] = now
else:
if entry is None:
return _reject(command, correlation_id, "not_found", f"entry {entry_id!r} not found")
if not note or not note.strip():
return _reject(command, correlation_id, "validation_error", "note requires content")
entry.setdefault("notes", []).append(
{"content": note.strip(), "author": note_author or "codex", "created": now}
)
entry["updated"] = now
after = yaml.safe_dump(entry, sort_keys=True)
if before == after:
result = CommandResult(
command=command,
status="applied",
correlation_id=correlation_id,
evidence={"status": "applied", "git_sha": current_head, "files_touched": [], "noop": True},
)
if idempotency_key:
idempotency.put(idempotency_key, digest, result.to_dict())
return result
_save(path, document)
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} {kind}/{entry_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_after, index_after = observe_repository(repo_root, slug=slug)
append_event(
index_after,
{
"type": "repo.command.applied",
"command": command,
"operation": operation,
"correlation_id": correlation_id,
"register_kind": kind,
"entry_id": entry_id,
"git_sha": git_sha,
"files_touched": [relative_path],
"source": "repo-manager",
},
)
append_event(
index_after,
{
"type": "repo.work.indexed",
"correlation_id": correlation_id,
"kind": f"register:{kind}",
"id": entry_id,
"source_path": relative_path,
},
)
save_index(index_after)
dual_run.record_mutation(
source="repo-manager",
kind=f"register_{operation}",
repo_slug=slug,
detail={
"register_kind": kind,
"entry_id": entry_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["push_ok"] = push_ok
evidence["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

@ -0,0 +1,229 @@
"""Onboard a repository into its authoritative State Hub (CUST-WP-0067-T04).
`hub-record-authority.yaml` assigns ``managed_repos`` to repo-manager as
``file-derived``: the repository's committed ``.repo-classification.yaml`` is
the source of truth and the hub holds a projection of it. Repo Manager owned
that record type without exposing any command for it, so the only working path
lived in the State Hub repo and defaulted to a local address — which is how
seven weeks of onboarding landed in a local cache instead of central.
The order here is deliberate and follows ADR-010 decision 5: make the source
file correct and *reachable* first, then project it. A hub record whose backing
file is not pushed cannot be re-derived by anyone else.
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
import httpx
import yaml
from repo_manager.classification import validate_classification
from repo_manager.gitops import head_sha, is_git_repo
CLASSIFICATION_FILENAME = ".repo-classification.yaml"
class OnboardError(RuntimeError):
"""Onboarding cannot proceed and the operator must decide what to do."""
@dataclass
class OnboardResult:
slug: str
action: str = "unknown"
steps: list[dict[str, Any]] = field(default_factory=list)
warnings: list[str] = field(default_factory=list)
def step(self, name: str, ok: bool, **detail: Any) -> None:
self.steps.append({"step": name, "ok": ok, **detail})
def to_dict(self) -> dict[str, Any]:
return {
"schema": "repo-manager.repo-onboard.v1",
"slug": self.slug,
"action": self.action,
"steps": self.steps,
"warnings": self.warnings,
}
def _git(repo: Path, *args: str) -> str:
import subprocess
proc = subprocess.run(
["git", "-C", str(repo), *args], capture_output=True, text=True, check=False
)
return proc.stdout.strip() if proc.returncode == 0 else ""
def _load_classification(repo: Path) -> dict[str, Any]:
path = repo / CLASSIFICATION_FILENAME
if not path.is_file():
raise OnboardError(
f"{CLASSIFICATION_FILENAME} is missing in {repo}. Classification is not "
"mechanical — author it with the repository owner rather than guessing "
"(see CUST-WP-0065-T01)."
)
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
data = raw.get("repo_classification", raw)
if not isinstance(data, dict):
raise OnboardError(f"{CLASSIFICATION_FILENAME} does not contain a mapping")
issues = validate_classification(data)
if issues:
detail = "; ".join(f"{i.field}: {i.message}" for i in issues)
raise OnboardError(f"{CLASSIFICATION_FILENAME} is invalid: {detail}")
return data
def _source_is_reachable(repo: Path, result: OnboardResult) -> None:
"""The backing file must be committed and pushed, or nothing can re-derive it."""
dirty = _git(repo, "status", "--porcelain", "--", CLASSIFICATION_FILENAME)
if dirty:
raise OnboardError(
f"{CLASSIFICATION_FILENAME} has uncommitted changes. Commit it first — "
"the commit is the write (ADR-010 decision 4)."
)
upstream = _git(repo, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
if not upstream:
raise OnboardError(
"no upstream branch configured; central derives from what it can fetch"
)
ahead = _git(repo, "rev-list", "--count", "@{u}..HEAD")
if ahead and ahead != "0":
raise OnboardError(
f"{ahead} commit(s) not pushed. Push before onboarding — a hub record "
"whose source is only local cannot be re-derived."
)
result.step("source_reachable", True, upstream=upstream, head=head_sha(repo))
def _hub(api_base: str, method: str, path: str, body: dict | None = None) -> tuple[int, Any]:
url = api_base.rstrip("/") + path
try:
with httpx.Client(timeout=30.0) as client:
response = client.request(method, url, json=body)
except httpx.HTTPError as exc:
raise OnboardError(f"cannot reach hub at {api_base}: {exc}") from exc
if response.status_code == 204:
return response.status_code, None
try:
return response.status_code, response.json()
except ValueError:
return response.status_code, {"_raw": response.text}
def _classification_body(data: dict[str, Any]) -> dict[str, Any]:
keys = (
"category",
"secondary_domains",
"capability_tags",
"business_stake",
"business_mechanics",
"classified_at",
"classified_by",
"standard_version",
)
return {k: data[k] for k in keys if data.get(k) is not None}
def onboard_repo(
repo_path: Path,
*,
api_base: str,
dry_run: bool = False,
slug: str | None = None,
) -> dict[str, Any]:
repo = Path(repo_path).expanduser().resolve()
if not is_git_repo(repo):
raise OnboardError(f"{repo} is not a git working copy")
slug = slug or repo.name
result = OnboardResult(slug=slug)
data = _load_classification(repo)
result.step("classification_valid", True, domain=data.get("domain"), category=data.get("category"))
_source_is_reachable(repo, result)
remote = _git(repo, "remote", "get-url", "origin") or None
code, existing = _hub(api_base, "GET", f"/repos/{slug}")
exists = code == 200
if dry_run:
result.action = "would-update" if exists else "would-create"
result.step("hub_projection", True, dry_run=True, exists=exists, api_base=api_base)
return result.to_dict()
if not exists:
code, created = _hub(
api_base,
"POST",
"/repos/",
{
"slug": slug,
"name": slug,
"domain_slug": data["domain"],
"local_path": str(repo),
"remote_url": remote,
},
)
if code not in (200, 201):
raise OnboardError(f"POST /repos/ failed ({code}): {created}")
result.action = "created"
result.step("hub_create", True, status=code)
else:
result.action = "updated"
# Correct a stale remote before anything else depends on it. The forge
# migration moved every repository from Gitea to Forgejo but never updated
# the hub, leaving 50 records pointing at a retired forge — and
# `fix-consistency` matches a checkout to its record *by remote_url*, so it
# could not find them and refused to register any of their work records.
# The working copy's origin is the derivable truth (CUST-WP-0068-T06).
if remote and existing and existing.get("remote_url") != remote:
code, _ = _hub(api_base, "PATCH", f"/repos/{slug}", {"remote_url": remote})
result.step(
"remote_url_corrected", code in (200, 201),
was=existing.get("remote_url"), now=remote, status=code,
)
code, patched = _hub(api_base, "PATCH", f"/repos/{slug}", _classification_body(data))
if code in (200, 201):
result.step("hub_classification", True, status=code)
else:
# The authoritative record now exists, which is what stops the repository
# from being recoverable only through a discardable cache. Classification
# is a projection of a file that is committed and pushed, so it can be
# re-derived at any time — do not fail the whole onboarding for it.
result.step("hub_classification", False, status=code, detail=patched)
result.warnings.append(
f"classification not projected (PATCH returned {code}); the record exists "
"and the file remains the source of truth, so re-run once the hub can "
"validate classification"
)
# Verify against the hub rather than trusting our own write.
code, verified = _hub(api_base, "GET", f"/repos/{slug}")
if code != 200:
raise OnboardError(f"verification failed: GET /repos/{slug} returned {code}")
if verified.get("category") is None and not any(
st["step"] == "hub_classification" and st["ok"] for st in result.steps
):
result.action = f"{result.action}-unclassified"
if verified.get("domain_slug") != data["domain"]:
result.warnings.append(
f"hub reports domain_slug={verified.get('domain_slug')!r}, file says {data['domain']!r}"
)
result.step("verified", True, domain_slug=verified.get("domain_slug"))
return result.to_dict()
def render(report: dict[str, Any]) -> str:
return json.dumps(report, indent=2)

View file

@ -0,0 +1,235 @@
"""Governed command: create flavor-correct repository files (RMGR-WP-0004-T03)."""
from __future__ import annotations
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from repo_manager.gitops import GitError, commit_paths, is_git_repo
from repo_manager.hub_access import apply_hub_access_block, render_hub_access_block
from repo_manager.standards import FLAVOR_MARKER_PREFIX, expected_workplan_prefix
from repo_manager.time import utc_today
DURABLE_FLAVORS = ("experimental", "research", "tooling", "product", "business")
FLAVORS = (*DURABLE_FLAVORS, "project")
@dataclass
class CommandResult:
status: str
evidence: dict[str, Any]
error: dict[str, Any] | None = None
correlation_id: str = ""
def to_dict(self) -> dict[str, Any]:
out = {
"command": "repo.scaffold",
"status": self.status,
"correlation_id": self.correlation_id,
"evidence": self.evidence,
}
if self.error:
out["error"] = self.error
return out
def _write(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if not content.endswith("\n"):
content += "\n"
path.write_text(content)
def scaffold_repository(
path: Path,
*,
flavor: str,
slug: str | None = None,
domain: str = "infotech",
wp_prefix: str | None = None,
force: bool = False,
commit: bool = True,
) -> CommandResult:
cid = str(uuid.uuid4())
dest = path.expanduser().resolve()
dest.mkdir(parents=True, exist_ok=True)
slug = slug or dest.name
flavor = flavor.strip().lower()
if flavor not in FLAVORS:
return CommandResult(
"rejected",
{"path": str(dest)},
{"message": f"unknown flavor {flavor!r}"},
cid,
)
prj = flavor == "project" or slug.startswith("prj-")
if prj:
flavor = "project"
prefix = (wp_prefix or "").strip().upper()
if prefix and not prefix.endswith("-WP"):
prefix = prefix.rstrip("-") + "-WP"
if not prefix:
derived = expected_workplan_prefix(dest, slug=slug)
return CommandResult(
"rejected",
{"path": str(dest), "suggested_prefix": derived},
{
"message": (
"project flavor requires --wp-prefix derived from the "
"project identity (never PRJ-WP-)"
)
},
cid,
)
if prefix == FLAVOR_MARKER_PREFIX or prefix.startswith("PRJ-"):
return CommandResult(
"rejected",
{"path": str(dest)},
{"message": "refusing flavor-derived prefix PRJ-WP-"},
cid,
)
else:
prefix = (wp_prefix or expected_workplan_prefix(dest, slug=slug) or "REPO-WP").upper()
if not prefix.endswith("-WP"):
prefix = prefix.rstrip("-") + "-WP"
written: list[str] = []
def put(rel: str, content: str) -> None:
target = dest / rel
if target.exists() and not force:
return
_write(target, content)
written.append(rel)
put(
".repo-classification.yaml",
"repo_classification:\n"
f" category: {'project' if prj else flavor}\n"
f" domain: {domain}\n"
" secondary_domains: []\n"
" capability_tags: []\n",
)
put("README.md", f"# {slug}\n\nSee {'GOAL.md' if prj else 'INTENT.md'} and SCOPE.md.\n")
put(
"SCOPE.md",
f"# Scope\n\n## In scope\n\n- {slug}\n\n## Out of scope\n\n- TBD\n",
)
put(
"AGENTS.md",
f"# Agent instructions — {slug}\n\n"
f"Orient: {'GOAL.md' if prj else 'INTENT.md'} → SCOPE.md → workplans/.\n"
f"Workplan prefix: `{prefix}-`.\n\n"
"## State Hub Integration\n\n"
f"{render_hub_access_block()}\n\n"
"## Cross-repo waits\n\n"
"If blocked on another repo, look for a `prj-` driver workplan and "
"follow `coordination-engine` `spec/cross-owner-wait-mode-v0.1.md`. "
"Do not mint a stranded `wait`, implement `flavor: residual`, or "
"message `ops-warden` for secrets.\n",
)
if prj:
today = utc_today().isoformat()
put(
"GOAL.md",
"---\n"
f"repo: {slug}\n"
"repo_flavor: project\n"
"project_status: draft\n"
f'started: "{today}"\n'
"---\n\n"
f"# Goal — {slug}\n\n"
"## Outcome\n\nWhat success looks like.\n\n"
"## Invariants\n\nRules that must hold during the project.\n\n"
"## Success gates\n\nCheckable conditions for goal achieved.\n\n"
"## Project retirement\n\n"
"Archive this repository when gates are met and residuals have live owners.\n",
)
if (dest / "INTENT.md").exists() and not force:
pass
else:
put(
"INTENT.md",
f"# INTENT\n\n> Why `{slug}` exists.\n\n## One-liner\n\nTBD.\n",
)
(dest / "workplans" / "archived").mkdir(parents=True, exist_ok=True)
if not prj:
bootstrap = dest / "workplans" / f"{prefix}-0001-foundation.md"
if force or not bootstrap.exists():
_write(
bootstrap,
"---\n"
f"id: {prefix}-0001\n"
"type: workplan\n"
f'title: "Foundation"\n'
f"domain: {domain}\n"
f"repo: {slug}\n"
"status: proposed\n"
"flavor: planning\n"
"depends_on: []\n"
"---\n\n"
"# Foundation\n\n"
"```task\n"
f"id: {prefix}-0001-T01\n"
"status: todo\n"
"priority: high\n"
"```\n\n"
"Establish the repository baseline.\n",
)
written.append(str(bootstrap.relative_to(dest)))
evidence: dict[str, Any] = {
"path": str(dest),
"flavor": flavor,
"slug": slug,
"wp_prefix": prefix,
"written": written,
"prj_layout": prj,
}
if commit and is_git_repo(dest) and written:
try:
sha = commit_paths(dest, written, f"chore: scaffold {flavor} repository baseline")
evidence["git_sha"] = sha
except GitError as exc:
return CommandResult("failed", evidence, {"message": str(exc)}, cid)
if not written:
evidence["noop"] = True
return CommandResult("applied", evidence, None, cid)
def refresh_hub_access(path: Path, *, commit: bool = True) -> CommandResult:
"""Replace the State Hub access table in AGENTS.md from the canonical map."""
cid = str(uuid.uuid4())
dest = path.expanduser().resolve()
agents = dest / "AGENTS.md"
evidence: dict[str, Any] = {"path": str(dest), "file": "AGENTS.md"}
if not agents.is_file():
return CommandResult(
"rejected",
evidence,
{"message": "AGENTS.md is missing; scaffold the repository first"},
cid,
)
original = agents.read_text(encoding="utf-8")
updated, action = apply_hub_access_block(original)
evidence["action"] = action
if action == "noop":
evidence["noop"] = True
return CommandResult("applied", evidence, None, cid)
agents.write_text(updated, encoding="utf-8")
evidence["written"] = ["AGENTS.md"]
if commit and is_git_repo(dest):
try:
evidence["git_sha"] = commit_paths(
dest,
["AGENTS.md"],
"docs(agents): refresh State Hub access map from repo-manager",
)
except GitError as exc:
return CommandResult("failed", evidence, {"message": str(exc)}, cid)
return CommandResult("applied", evidence, None, cid)

View file

@ -0,0 +1,424 @@
"""Governed file-backed task and daily ad-hoc mutations."""
from __future__ import annotations
import json
import re
import uuid
from datetime import date
from pathlib import Path
from typing import Any
from repo_manager import dual_run, idempotency
from repo_manager.commands.workplan import CommandResult, create_workplan
from repo_manager.gitops import GitError, commit_paths, head_sha, push_ff
from repo_manager.identifiers import derive_work_record_uuid, load_fleet_namespace
from repo_manager.index_store import append_event, default_index_path, save_index
from repo_manager.observe import observe_repository
from repo_manager.parse.workplan import _TASK_BLOCK_RE, _parse_yaml_block
from repo_manager.record_identity import classify_record_id
from repo_manager.time import utc_today
VALID_TASK_STATUSES = frozenset({"wait", "todo", "progress", "done", "cancel"})
VALID_TASK_PRIORITIES = frozenset({"low", "medium", "high", "critical"})
def _result(
command: str,
status: str,
correlation_id: str,
*,
evidence: dict[str, Any] | None = None,
code: str | None = None,
message: str | None = None,
) -> CommandResult:
error = None
if code and message:
error = {"code": code, "message": message, "correlation_id": correlation_id}
return CommandResult(
command=command,
status=status,
correlation_id=correlation_id,
evidence=evidence or {"status": status},
error=error,
)
def _is_uuid(value: str) -> bool:
try:
uuid.UUID(value)
return True
except ValueError:
return False
def _find_record(index: Any, kind: str, identifier: str) -> Any | None:
by_uuid = _is_uuid(identifier)
for record in index.work_records:
if record.kind != kind:
continue
if not by_uuid and record.id == identifier:
return record
if by_uuid and record.uuid and record.uuid.lower() == identifier.lower():
return record
return None
def _render_scalar(key: str, value: Any) -> str:
if isinstance(value, bool):
return "true" if value else "false"
if key in {"status", "priority"}:
return str(value)
return json.dumps(str(value), ensure_ascii=False)
def _patch_task_fields(
text: str,
*,
canonical_id: str,
updates: dict[str, Any],
) -> tuple[str, bool]:
matches = 0
changed = False
def replace(match: re.Match[str]) -> str:
nonlocal matches, changed
raw = match.group(1)
meta = _parse_yaml_block(raw.strip())
if str(meta.get("id") or "") != canonical_id:
return match.group(0)
matches += 1
patched = raw
for key, value in updates.items():
rendered = _render_scalar(key, value)
pattern = re.compile(rf"^(?P<prefix>{re.escape(key)}:\s*).*$", re.MULTILINE)
if pattern.search(patched):
candidate = pattern.sub(rf"\g<prefix>{rendered}", patched, count=1)
else:
candidate = f"{patched.rstrip()}\n{key}: {rendered}\n"
if candidate != patched:
changed = True
patched = candidate
return f"```task\n{patched.strip()}\n```"
result = _TASK_BLOCK_RE.sub(replace, text)
if matches != 1:
raise ValueError(f"{canonical_id}: expected exactly one task block, found {matches}")
return result, changed
def _next_task_id(parent_id: str, index: Any) -> str:
pattern = re.compile(rf"^{re.escape(parent_id)}-T(?P<number>[0-9]{{2,3}})$")
numbers = [
int(match.group("number"))
for record in index.work_records
if record.kind == "task" and record.id and (match := pattern.fullmatch(record.id))
]
number = max(numbers, default=0) + 1
width = 2 if number < 100 else 3
candidate = f"{parent_id}-T{number:0{width}d}"
if classify_record_id("task", candidate) != "canonical":
raise ValueError(f"no canonical task identifier remains under {parent_id}")
return candidate
def mutate_task(
repo_root: Path,
task_id: str | None,
*,
operation: str,
workplan_id: str | None = None,
title: str | None = None,
description: str | None = None,
status: str | None = None,
priority: str | None = None,
needs_human: bool | None = None,
intervention_note: str | None = None,
blocking_reason: 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:
command = f"repo.work.{operation}_task"
correlation_id = correlation_id or str(uuid.uuid4())
repo_root = repo_root.resolve()
payload = {
"repo_root": str(repo_root),
"task_id": task_id,
"operation": operation,
"workplan_id": workplan_id,
"title": title,
"description": description,
"status": status,
"priority": priority,
"needs_human": needs_human,
"intervention_note": intervention_note,
"blocking_reason": blocking_reason,
"expected_head_sha": expected_head_sha,
}
payload_hash = idempotency.payload_hash(payload)
if idempotency_key and (prior := idempotency.get(idempotency_key)):
if prior.get("payload_hash") != payload_hash:
return _result(
command, "rejected", correlation_id, code="conflict",
message="idempotency key reused with different payload",
)
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 operation not in {"add", "update"}:
return _result(command, "rejected", correlation_id, code="validation_error", message="invalid task operation")
if status is not None and status not in VALID_TASK_STATUSES:
return _result(command, "rejected", correlation_id, code="validation_error", message=f"invalid task status {status!r}")
if priority is not None and priority not in VALID_TASK_PRIORITIES:
return _result(command, "rejected", correlation_id, code="validation_error", message=f"invalid task priority {priority!r}")
if needs_human is True and not intervention_note:
return _result(command, "rejected", correlation_id, code="validation_error", message="intervention_note is required when needs_human is true")
current_head = head_sha(repo_root)
if expected_head_sha and current_head and expected_head_sha != current_head:
result = _result(
command, "rejected", correlation_id,
evidence={"status": "rejected", "head_sha": current_head},
code="precondition_failed",
message=f"expected_head_sha {expected_head_sha} != {current_head}",
)
if result.error:
result.error["retryable"] = True
return result
snapshot, index = observe_repository(repo_root, slug=repo_slug)
slug = repo_slug or snapshot.get("slug")
if operation == "add":
if not workplan_id:
return _result(command, "rejected", correlation_id, code="validation_error", message="add requires workplan_id")
parent = _find_record(index, "workplan", workplan_id)
if parent is None or not parent.id:
return _result(command, "rejected", correlation_id, code="not_found", message=f"workplan {workplan_id!r} not found")
if not title or not title.strip():
return _result(command, "rejected", correlation_id, code="validation_error", message="add requires a title")
canonical_id = task_id or _next_task_id(parent.id, index)
if classify_record_id("task", canonical_id) != "canonical":
return _result(command, "rejected", correlation_id, code="validation_error", message=f"invalid canonical task id {canonical_id!r}")
if _find_record(index, "task", canonical_id) is not None:
return _result(command, "rejected", correlation_id, code="conflict", message=f"task {canonical_id!r} exists")
if not canonical_id.startswith(parent.id + "-T"):
return _result(command, "rejected", correlation_id, code="validation_error", message="task id must be qualified by its workplan id")
path = repo_root / parent.source_path
task_uuid = derive_work_record_uuid(load_fleet_namespace(), canonical_id)
task_status = status or "todo"
task_priority = priority or "medium"
fields = [
f"id: {canonical_id}",
f"status: {task_status}",
f"priority: {task_priority}",
f'state_hub_task_id: "{task_uuid}"',
]
if needs_human is not None:
fields.append(f"needs_human: {'true' if needs_human else 'false'}")
if intervention_note:
fields.append(f"intervention_note: {json.dumps(intervention_note, ensure_ascii=False)}")
if blocking_reason:
fields.append(f"blocking_reason: {json.dumps(blocking_reason, ensure_ascii=False)}")
addition = (
f"\n\n## {title.strip()}\n\n```task\n"
+ "\n".join(fields)
+ "\n```\n\n"
+ ((description or "").strip() or "Task details to be completed.")
+ "\n"
)
path.write_text(path.read_text(encoding="utf-8").rstrip() + addition, encoding="utf-8")
changes = {"operation": "add", "task_id": canonical_id, "workplan_id": parent.id}
else:
if not task_id:
return _result(command, "rejected", correlation_id, code="validation_error", message="update requires task_id")
record = _find_record(index, "task", task_id)
if record is None or not record.id:
return _result(command, "rejected", correlation_id, code="not_found", message=f"task {task_id!r} not found")
updates = {
key: value
for key, value in {
"title": title.strip() if title else None,
"description": description.strip() if description else None,
"status": status,
"priority": priority,
"needs_human": needs_human,
"intervention_note": intervention_note,
"blocking_reason": blocking_reason,
}.items()
if value is not None
}
if not updates:
return _result(command, "rejected", correlation_id, code="validation_error", message="update requires at least one field")
existing_human = bool(record.extra.get("needs_human"))
resulting_human = updates.get("needs_human", existing_human)
resulting_note = updates.get("intervention_note", record.extra.get("intervention_note"))
if resulting_human and not resulting_note:
return _result(command, "rejected", correlation_id, code="validation_error", message="intervention_note is required when needs_human is true")
path = repo_root / record.source_path
patched, changed = _patch_task_fields(
path.read_text(encoding="utf-8"), canonical_id=record.id, updates=updates
)
if not changed:
result = _result(
command, "applied", correlation_id,
evidence={"status": "applied", "git_sha": current_head, "files_touched": [], "noop": True},
)
if idempotency_key:
idempotency.put(idempotency_key, payload_hash, result.to_dict())
return result
path.write_text(patched, encoding="utf-8")
canonical_id = record.id
changes = {"operation": "update", "task_id": canonical_id, "fields": sorted(updates)}
relative_path = str(path.relative_to(repo_root))
git_sha = None
push_ok = None
push_message = None
if commit:
try:
git_sha = commit_paths(
repo_root,
[relative_path],
message=(
f"{command} {canonical_id}\n\ncorrelation_id: {correlation_id}\n"
f"reason: {reason}\nsource: repo-manager\n"
),
)
except GitError as exc:
return _result(command, "failed", correlation_id, evidence={"status": "failed", "files_touched": [relative_path]}, code="internal", message=str(exc))
if push:
push_ok, push_message = push_ff(repo_root)
_snapshot_after, index_after = observe_repository(repo_root, slug=slug)
append_event(
index_after,
{
"type": "repo.command.applied",
"command": command,
"correlation_id": correlation_id,
"git_sha": git_sha,
**changes,
},
)
save_index(index_after)
dual_run.record_mutation(
source="repo-manager",
kind=f"task_{operation}",
repo_slug=slug,
detail={**changes, "git_sha": git_sha, "correlation_id": correlation_id},
)
evidence = {
"status": "applied",
"task_id": canonical_id,
"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 = _result(command, "applied", correlation_id, evidence=evidence)
if idempotency_key:
idempotency.put(idempotency_key, payload_hash, result.to_dict())
return result
def add_adhoc_task(
repo_root: Path,
title: str,
description: str,
*,
prefix: str | None = None,
on_date: date | None = None,
status: str = "todo",
priority: str = "medium",
owner: str = "codex",
correlation_id: str | None = None,
reason: str = "rmgr adhoc add",
commit: bool = True,
push: bool = False,
expected_head_sha: str | None = None,
idempotency_key: str | None = None,
repo_slug: str | None = None,
) -> CommandResult:
"""Create/reuse today's qualified ad-hoc workplan and append one task."""
correlation_id = correlation_id or str(uuid.uuid4())
repo_root = repo_root.resolve()
payload = {
"repo_root": str(repo_root), "title": title, "description": description,
"prefix": prefix, "on_date": str(on_date) if on_date else None,
"status": status, "priority": priority, "expected_head_sha": expected_head_sha,
}
payload_hash = idempotency.payload_hash(payload)
if idempotency_key and (prior := idempotency.get(idempotency_key)):
if prior.get("payload_hash") != payload_hash:
return _result("repo.work.add_adhoc_task", "rejected", correlation_id, code="conflict", message="idempotency key reused with different payload")
previous = prior.get("result") or {}
return CommandResult(
command=previous.get("command", "repo.work.add_adhoc_task"),
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"),
)
snapshot, index = observe_repository(repo_root, slug=repo_slug)
prefixes = {
record.id.split("-WP-", 1)[0]
for record in index.work_records
if record.kind == "workplan" and record.id and "-WP-" in record.id
}
selected = (prefix or "").strip().upper().removesuffix("-WP")
if not selected:
if len(prefixes) != 1:
return _result("repo.work.add_adhoc_task", "rejected", correlation_id, code="validation_error", message=f"could not infer one workplan prefix; candidates={sorted(prefixes)!r}; pass --prefix")
selected = next(iter(prefixes))
day = on_date or utc_today()
workplan_id = f"{selected}-WP-ADHOC-{day.isoformat()}"
parent = _find_record(index, "workplan", workplan_id)
if parent is None:
created = create_workplan(
repo_root,
workplan_id,
f"Ad Hoc — {day.isoformat()}",
f"Small opportunistic fixes completed on {day.isoformat()}.",
owner=owner,
status="active",
commit=False,
expected_head_sha=expected_head_sha,
repo_slug=repo_slug or snapshot.get("slug"),
)
if created.status != "applied":
return created
result = mutate_task(
repo_root,
None,
operation="add",
workplan_id=workplan_id,
title=title,
description=description,
status=status,
priority=priority,
correlation_id=correlation_id,
reason=reason,
commit=commit,
push=push,
expected_head_sha=expected_head_sha,
repo_slug=repo_slug or snapshot.get("slug"),
)
result.command = "repo.work.add_adhoc_task"
result.evidence["workplan_id"] = workplan_id
if idempotency_key and result.status == "applied":
idempotency.put(idempotency_key, payload_hash, result.to_dict())
return result

View file

@ -0,0 +1,345 @@
"""Governed command: repo.work.update_task_status (dual-run hardened)."""
from __future__ import annotations
import re
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from repo_manager import dual_run, idempotency
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.workplan import _TASK_BLOCK_RE, _parse_yaml_block
VALID_TASK_STATUSES = frozenset({"wait", "todo", "progress", "done", "cancel"})
@dataclass
class CommandResult:
status: str # applied | rejected | failed
evidence: dict[str, Any]
error: dict[str, Any] | None = None
correlation_id: str = ""
def to_dict(self) -> dict[str, Any]:
out: dict[str, Any] = {
"command": "repo.work.update_task_status",
"status": self.status,
"correlation_id": self.correlation_id,
"evidence": self.evidence,
}
if self.error:
out["error"] = self.error
return out
def _is_uuid(value: str) -> bool:
try:
uuid.UUID(value)
return True
except ValueError:
return False
def _patch_task_status_in_file(
path: Path,
*,
canonical_id: str | None,
hub_uuid: str | None,
status: str,
) -> bool:
text = path.read_text(encoding="utf-8")
def _replace(match: re.Match[str]) -> str:
block = match.group(0)
meta = _parse_yaml_block(match.group(1))
tid = str(meta["id"]) if meta.get("id") is not None else None
hub = (
str(meta["state_hub_task_id"]).strip().strip('"')
if meta.get("state_hub_task_id") is not None
else None
)
matched = False
if canonical_id and tid == canonical_id:
matched = True
if hub_uuid and hub and hub.lower() == hub_uuid.lower():
matched = True
if not matched:
return block
replaced = re.sub(
r"^(status:\s*)\S+",
rf"\g<1>{status}",
block,
count=1,
flags=re.MULTILINE,
)
if replaced != block:
return replaced
return block.replace("\n```", f"\nstatus: {status}\n```", 1)
new_text = _TASK_BLOCK_RE.sub(_replace, text)
if new_text == text:
return False
path.write_text(new_text, encoding="utf-8")
return True
def update_task_status(
repo_root: Path,
task_id: str,
status: str,
*,
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:
"""Apply task status change to the workplan file and optionally git-commit.
``task_id`` may be a canonical id (e.g. RMGR-WP-0001-T05) or a State Hub
task UUID (matched against ``state_hub_task_id`` in the file).
"""
correlation_id = correlation_id or str(uuid.uuid4())
repo_root = repo_root.resolve()
payload = {
"repo_root": str(repo_root),
"task_id": task_id,
"status": status,
"expected_head_sha": expected_head_sha,
}
ph = idempotency.payload_hash(payload)
if idempotency_key:
prior = idempotency.get(idempotency_key)
if prior:
if prior.get("payload_hash") != ph:
return CommandResult(
status="rejected",
correlation_id=correlation_id,
evidence={"status": "rejected"},
error={
"code": "conflict",
"message": "idempotency key reused with different payload",
"correlation_id": correlation_id,
},
)
# Replay prior result
prev = prior.get("result") or {}
return CommandResult(
status=prev.get("status", "applied"),
correlation_id=prev.get("correlation_id", correlation_id),
evidence=prev.get("evidence") or {"status": "applied", "replay": True},
error=prev.get("error"),
)
if status not in VALID_TASK_STATUSES:
return CommandResult(
status="rejected",
correlation_id=correlation_id,
evidence={"status": "rejected"},
error={
"code": "validation_error",
"message": f"invalid task status {status!r}",
"correlation_id": correlation_id,
},
)
current_head = head_sha(repo_root)
if expected_head_sha and current_head and expected_head_sha != current_head:
return CommandResult(
status="rejected",
correlation_id=correlation_id,
evidence={"status": "rejected", "head_sha": current_head},
error={
"code": "precondition_failed",
"message": f"expected_head_sha {expected_head_sha} != {current_head}",
"correlation_id": correlation_id,
"retryable": True,
},
)
snapshot, index = observe_repository(repo_root, slug=repo_slug)
slug = repo_slug or snapshot.get("slug")
hub_uuid = task_id if _is_uuid(task_id) else None
canonical = None if hub_uuid else task_id
match = None
for r in index.work_records:
if r.kind != "task":
continue
if canonical and r.id == canonical:
match = r
break
if hub_uuid and r.uuid and r.uuid.lower() == hub_uuid.lower():
match = r
break
if match is None:
return CommandResult(
status="rejected",
correlation_id=correlation_id,
evidence={"status": "rejected"},
error={
"code": "not_found",
"message": f"task {task_id!r} not found in workplans",
"correlation_id": correlation_id,
},
)
path = repo_root / match.source_path
if not path.is_file():
return CommandResult(
status="failed",
correlation_id=correlation_id,
evidence={"status": "failed"},
error={
"code": "not_found",
"message": f"source file missing: {match.source_path}",
"correlation_id": correlation_id,
},
)
# Already at status → treat as successful no-op applied with current head
if match.status == status:
result = CommandResult(
status="applied",
correlation_id=correlation_id,
evidence={
"status": "applied",
"git_sha": current_head,
"files_touched": [],
"noop": True,
"observed_at": index.observed_at,
},
)
if idempotency_key:
idempotency.put(idempotency_key, ph, result.to_dict())
return result
changed = _patch_task_status_in_file(
path,
canonical_id=match.id,
hub_uuid=match.uuid or hub_uuid,
status=status,
)
if not changed:
return CommandResult(
status="rejected",
correlation_id=correlation_id,
evidence={"status": "rejected", "files_touched": []},
error={
"code": "conflict",
"message": "task block not patched (id mismatch)",
"correlation_id": correlation_id,
},
)
git_sha: str | None = None
push_ok: bool | None = None
push_msg: str | None = None
if commit:
try:
git_sha = commit_paths(
repo_root,
[match.source_path],
message=(
f"repo.work.update_task_status {match.id or task_id} -> {status}\n\n"
f"correlation_id: {correlation_id}\nreason: {reason}\n"
f"source: repo-manager\n"
),
)
except GitError as exc:
return CommandResult(
status="failed",
correlation_id=correlation_id,
evidence={
"status": "failed",
"files_touched": [match.source_path],
"git_sha": None,
},
error={
"code": "internal",
"message": str(exc),
"correlation_id": correlation_id,
},
)
if push:
push_ok, push_msg = push_ff(repo_root)
_snap2, index2 = observe_repository(repo_root, slug=slug)
event = {
"type": "repo.command.applied",
"command": "repo.work.update_task_status",
"correlation_id": correlation_id,
"task_id": match.id,
"task_uuid": match.uuid,
"new_status": status,
"git_sha": git_sha,
"files_touched": [match.source_path],
"expected_head_sha_before": current_head,
"source": "repo-manager",
}
append_event(index2, event)
append_event(
index2,
{
"type": "repo.work.indexed",
"correlation_id": correlation_id,
"kind": "task",
"id": match.id,
"source_path": match.source_path,
},
)
save_index(index2)
dual_run.record_mutation(
source="repo-manager",
kind="task_status_writeback",
repo_slug=slug,
detail={
"task_id": match.id,
"status": status,
"git_sha": git_sha,
"correlation_id": correlation_id,
},
)
evidence: dict[str, Any] = {
"status": "applied",
"git_sha": git_sha,
"files_touched": [match.source_path],
"observed_at": index2.observed_at,
"index_path": str(default_index_path(repo_root)),
"source": "repo-manager",
}
if push:
evidence["push_ok"] = push_ok
evidence["push_message"] = push_msg
if commit and not git_sha:
return CommandResult(
status="failed",
correlation_id=correlation_id,
evidence=evidence,
error={
"code": "internal",
"message": "applied file mutation without git_sha",
"correlation_id": correlation_id,
},
)
result = CommandResult(
status="applied",
correlation_id=correlation_id,
evidence=evidence,
)
if idempotency_key:
idempotency.put(idempotency_key, ph, result.to_dict())
return result

View file

@ -0,0 +1,519 @@
"""Governed workplan create, update, and archive commands."""
from __future__ import annotations
import json
import re
import uuid
from dataclasses import dataclass
from datetime import date
from pathlib import Path
from typing import Any
from repo_manager import dual_run, idempotency
from repo_manager.gitops import GitError, commit_paths, head_sha, push_ff
from repo_manager.identifiers import derive_work_record_uuid, load_fleet_namespace
from repo_manager.index_store import append_event, default_index_path, save_index
from repo_manager.observe import observe_repository
from repo_manager.record_identity import classify_record_id
from repo_manager.time import utc_today
VALID_WORKPLAN_STATUSES = frozenset(
{"proposed", "ready", "active", "blocked", "backlog", "finished", "archived"}
)
_FRONTMATTER_RE = re.compile(r"\A---\r?\n(?P<meta>.*?)\r?\n---(?P<body>\r?\n.*)?\Z", re.DOTALL)
@dataclass
class CommandResult:
command: str
status: str # applied | rejected | failed
evidence: dict[str, Any]
error: dict[str, Any] | None = None
correlation_id: str = ""
def to_dict(self) -> dict[str, Any]:
out: dict[str, Any] = {
"command": self.command,
"status": self.status,
"correlation_id": self.correlation_id,
"evidence": self.evidence,
}
if self.error:
out["error"] = self.error
return out
def _result_from_prior(command: str, prior: dict[str, Any], correlation_id: str) -> CommandResult:
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"),
)
def _rejected(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 _is_uuid(value: str) -> bool:
try:
uuid.UUID(value)
return True
except ValueError:
return False
def _slugify(value: str) -> str:
return re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-") or "workplan"
def _quoted(value: str) -> str:
return json.dumps(value, ensure_ascii=False)
def _today() -> date:
return utc_today()
def _patch_frontmatter(text: str, updates: dict[str, str]) -> str | None:
match = _FRONTMATTER_RE.match(text)
if not match:
return None
meta = match.group("meta")
for key, value in updates.items():
rendered = value if key == "status" else _quoted(value)
pattern = re.compile(rf"^({re.escape(key)}:\s*).*$", re.MULTILINE)
if pattern.search(meta):
meta = pattern.sub(rf"\g<1>{rendered}", meta, count=1)
else:
meta = f"{meta.rstrip()}\n{key}: {rendered}"
return f"---\n{meta}\n---{match.group('body') or ''}"
def _find_workplan(index: Any, workplan_id: str) -> Any | None:
by_uuid = _is_uuid(workplan_id)
for record in index.work_records:
if record.kind != "workplan":
continue
if not by_uuid and record.id == workplan_id:
return record
if by_uuid and record.uuid and record.uuid.lower() == workplan_id.lower():
return record
return None
def _create_text(
*,
workplan_id: str,
title: str,
goal: str,
status: str,
domain: str,
repo_slug: str,
owner: str,
topic_slug: str,
) -> str:
today = _today().isoformat()
workplan_uuid = derive_work_record_uuid(load_fleet_namespace(), workplan_id)
return (
"---\n"
f"id: {workplan_id}\n"
"type: workplan\n"
f"title: {_quoted(title)}\n"
f"domain: {domain}\n"
f"repo: {repo_slug}\n"
f"status: {status}\n"
f"flavor: planning\n"
"depends_on: []\n"
f"owner: {owner}\n"
f"topic_slug: {topic_slug}\n"
f"created: {_quoted(today)}\n"
f"updated: {_quoted(today)}\n"
f"state_hub_workstream_id: {_quoted(str(workplan_uuid))}\n"
"---\n\n"
f"# {title}\n\n"
"## Goal\n\n"
f"{goal.strip()}\n"
)
def mutate_workplan(
repo_root: Path,
workplan_id: str,
*,
operation: str,
title: str | None = None,
goal: str | None = None,
status: str | None = None,
owner: str | None = None,
topic_slug: str | None = None,
domain: str | None = None,
filename: str | None = None,
confirm_archive: bool = False,
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:
"""Create, update, or archive a file-backed workplan.
Update/archive accepts a canonical workplan id or State Hub UUID. ``archive``
is the recoverable implementation of the legacy delete operation.
"""
commands = {
"create": "repo.work.create_workplan",
"update": "repo.work.update_workplan",
"archive": "repo.work.archive_workplan",
}
correlation_id = correlation_id or str(uuid.uuid4())
command = commands.get(operation, "repo.work.update_workplan")
repo_root = repo_root.resolve()
payload = {
"repo_root": str(repo_root),
"workplan_id": workplan_id,
"operation": operation,
"title": title,
"goal": goal,
"status": status,
"owner": owner,
"topic_slug": topic_slug,
"domain": domain,
"filename": filename,
"confirm_archive": confirm_archive,
"expected_head_sha": expected_head_sha,
}
payload_digest = idempotency.payload_hash(payload)
if idempotency_key:
prior = idempotency.get(idempotency_key)
if prior:
if prior.get("payload_hash") != payload_digest:
return _rejected(
command,
correlation_id,
"conflict",
"idempotency key reused with different payload",
)
return _result_from_prior(command, prior, correlation_id)
if operation not in commands:
return _rejected(command, correlation_id, "validation_error", f"invalid operation {operation!r}")
if status is not None and status not in VALID_WORKPLAN_STATUSES:
return _rejected(
command,
correlation_id,
"validation_error",
f"invalid workplan status {status!r}",
)
if operation == "archive" and not confirm_archive:
return _rejected(
command,
correlation_id,
"confirmation_required",
"archive requires confirm_archive=True",
)
current_head = head_sha(repo_root)
if expected_head_sha and current_head and expected_head_sha != current_head:
result = _rejected(
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
snapshot, index = observe_repository(repo_root, slug=repo_slug)
slug = repo_slug or snapshot.get("slug")
record = _find_workplan(index, workplan_id)
touched: list[str]
changes: dict[str, Any]
if operation == "create":
if classify_record_id("workplan", workplan_id) != "canonical":
return _rejected(
command,
correlation_id,
"validation_error",
f"invalid canonical workplan id {workplan_id!r}",
)
if record is not None:
return _rejected(command, correlation_id, "conflict", f"workplan {workplan_id!r} exists")
if not title or not title.strip() or not goal or not goal.strip():
return _rejected(
command,
correlation_id,
"validation_error",
"create requires non-empty title and goal",
)
create_status = status or "proposed"
if create_status not in VALID_WORKPLAN_STATUSES:
return _rejected(
command,
correlation_id,
"validation_error",
f"invalid workplan status {create_status!r}",
)
is_adhoc = "-WP-ADHOC-" in workplan_id
adhoc_date = workplan_id.rsplit("-WP-ADHOC-", 1)[-1]
name = filename or (
f"ADHOC-{adhoc_date}.md"
if is_adhoc
else f"{workplan_id}-{_slugify(title)}.md"
)
expected_adhoc_name = f"ADHOC-{adhoc_date}.md"
safe_name = (
Path(name).name == name
and name.endswith(".md")
and (
name.startswith(f"{workplan_id}-")
or (is_adhoc and name == expected_adhoc_name)
)
)
if not safe_name:
return _rejected(command, correlation_id, "validation_error", "unsafe workplan filename")
path = repo_root / "workplans" / name
if path.exists():
return _rejected(command, correlation_id, "conflict", f"source file exists: workplans/{name}")
classification = snapshot.get("classification") or {}
create_domain = domain or classification.get("domain") or "infotech"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
_create_text(
workplan_id=workplan_id,
title=title.strip(),
goal=goal,
status=create_status,
domain=create_domain,
repo_slug=slug,
owner=owner or "codex",
topic_slug=topic_slug or create_domain,
),
encoding="utf-8",
)
touched = [str(path.relative_to(repo_root))]
changes = {"title": title.strip(), "status": create_status}
else:
if record is None:
return _rejected(
command,
correlation_id,
"not_found",
f"workplan {workplan_id!r} not found in workplans",
)
path = repo_root / record.source_path
if not path.is_file():
return CommandResult(
command=command,
status="failed",
correlation_id=correlation_id,
evidence={"status": "failed"},
error={
"code": "not_found",
"message": f"source file missing: {record.source_path}",
"correlation_id": correlation_id,
},
)
if operation == "update":
updates = {
key: value.strip()
for key, value in {
"title": title,
"status": status,
"owner": owner,
"topic_slug": topic_slug,
"domain": domain,
}.items()
if value is not None and value.strip()
}
if not updates:
return _rejected(
command,
correlation_id,
"validation_error",
"update requires at least one field",
)
changes = {
key: value
for key, value in updates.items()
if key not in {"status", "title"}
or (key == "status" and value != record.status)
or (key == "title" and value != record.title)
}
if not changes:
result = CommandResult(
command=command,
status="applied",
correlation_id=correlation_id,
evidence={
"status": "applied",
"git_sha": current_head,
"files_touched": [],
"noop": True,
"observed_at": index.observed_at,
},
)
if idempotency_key:
idempotency.put(idempotency_key, payload_digest, result.to_dict())
return result
changes["updated"] = _today().isoformat()
new_text = _patch_frontmatter(path.read_text(encoding="utf-8"), changes)
if new_text is None:
return _rejected(command, correlation_id, "conflict", "workplan frontmatter is invalid")
path.write_text(new_text, encoding="utf-8")
touched = [record.source_path]
else:
if record.status == "archived" and record.source_path.startswith("workplans/archived/"):
result = CommandResult(
command=command,
status="applied",
correlation_id=correlation_id,
evidence={
"status": "applied",
"git_sha": current_head,
"files_touched": [],
"noop": True,
"observed_at": index.observed_at,
},
)
if idempotency_key:
idempotency.put(idempotency_key, payload_digest, result.to_dict())
return result
archived_dir = repo_root / "workplans" / "archived"
archived_dir.mkdir(parents=True, exist_ok=True)
archived_path = archived_dir / f"{_today():%y%m%d}-{path.name}"
if archived_path.exists():
return _rejected(
command,
correlation_id,
"conflict",
f"archive target exists: {archived_path.relative_to(repo_root)}",
)
new_text = _patch_frontmatter(
path.read_text(encoding="utf-8"),
{"status": "archived", "updated": _today().isoformat()},
)
if new_text is None:
return _rejected(command, correlation_id, "conflict", "workplan frontmatter is invalid")
archived_path.write_text(new_text, encoding="utf-8")
path.unlink()
touched = [record.source_path, str(archived_path.relative_to(repo_root))]
changes = {"status": "archived", "source_path": touched[-1]}
git_sha: str | None = None
push_ok: bool | None = None
push_message: str | None = None
if commit:
try:
git_sha = commit_paths(
repo_root,
touched,
message=(
f"{command} {workplan_id} ({operation})\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": touched, "git_sha": None},
error={"code": "internal", "message": str(exc), "correlation_id": correlation_id},
)
if push:
push_ok, push_message = push_ff(repo_root)
_snapshot_after, index_after = observe_repository(repo_root, slug=slug)
event = {
"type": "repo.command.applied",
"command": command,
"operation": operation,
"correlation_id": correlation_id,
"workplan_id": record.id if record else workplan_id,
"workplan_uuid": record.uuid if record else None,
"changes": changes,
"git_sha": git_sha,
"files_touched": touched,
"expected_head_sha_before": current_head,
"source": "repo-manager",
}
append_event(index_after, event)
append_event(
index_after,
{
"type": "repo.work.indexed",
"correlation_id": correlation_id,
"kind": "workplan",
"id": record.id if record else workplan_id,
"source_path": touched[-1],
},
)
save_index(index_after)
dual_run.record_mutation(
source="repo-manager",
kind=f"workplan_{operation}",
repo_slug=slug,
detail={
"workplan_id": record.id if record else workplan_id,
"changes": changes,
"git_sha": git_sha,
"correlation_id": correlation_id,
},
)
evidence: dict[str, Any] = {
"status": "applied",
"git_sha": git_sha,
"files_touched": touched,
"observed_at": index_after.observed_at,
"index_path": str(default_index_path(repo_root)),
"source": "repo-manager",
}
if push:
evidence["push_ok"] = push_ok
evidence["push_message"] = push_message
result = CommandResult(
command=command,
status="applied",
correlation_id=correlation_id,
evidence=evidence,
)
if idempotency_key:
idempotency.put(idempotency_key, payload_digest, result.to_dict())
return result
def create_workplan(repo_root: Path, workplan_id: str, title: str, goal: str, **kwargs: Any) -> CommandResult:
return mutate_workplan(
repo_root,
workplan_id,
operation="create",
title=title,
goal=goal,
**kwargs,
)
def update_workplan(repo_root: Path, workplan_id: str, **kwargs: Any) -> CommandResult:
return mutate_workplan(repo_root, workplan_id, operation="update", **kwargs)
def archive_workplan(repo_root: Path, workplan_id: str, **kwargs: Any) -> CommandResult:
return mutate_workplan(repo_root, workplan_id, operation="archive", **kwargs)

View file

@ -0,0 +1,209 @@
"""Dual-run flags and checkout-mutation meter (RMGR-WP-0002/0003).
Precedence (highest first):
1. Environment variables when set
2. Config file (~/.repo-manager/dual-run.yaml or RM_DUAL_RUN_CONFIG)
3. Defaults (all off)
Environment variables:
RM_WRITEBACK, RM_RECONCILE, RM_WRITEBACK_PUSH, RM_PILOT_REPOS, RM_METER_PATH,
RM_DUAL_RUN_CONFIG
Rollback: writeback/reconcile false in config and unset env → State Hub native.
"""
from __future__ import annotations
import json
import os
from functools import lru_cache
from pathlib import Path
from typing import Any, Literal
import yaml
from repo_manager.time import utc_now_text
Source = Literal["state-hub", "repo-manager"]
_TRUE = frozenset({"1", "true", "yes", "on"})
_FALSE = frozenset({"0", "false", "no", "off"})
def config_path() -> Path:
raw = os.environ.get("RM_DUAL_RUN_CONFIG", "~/.repo-manager/dual-run.yaml")
return Path(raw).expanduser()
@lru_cache(maxsize=1)
def _load_file_config() -> dict[str, Any]:
path = config_path()
if not path.is_file():
return {}
try:
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
except (OSError, yaml.YAMLError):
return {}
return data if isinstance(data, dict) else {}
def reload_config() -> None:
"""Clear cached config (tests / SIGHUP-style)."""
_load_file_config.cache_clear()
def _env_bool(name: str) -> bool | None:
raw = os.environ.get(name)
if raw is None or raw.strip() == "":
return None
v = raw.strip().lower()
if v in _TRUE:
return True
if v in _FALSE:
return False
return None
def _cfg_bool(key: str, default: bool = False) -> bool:
env_map = {
"writeback": "RM_WRITEBACK",
"reconcile": "RM_RECONCILE",
"writeback_push": "RM_WRITEBACK_PUSH",
}
env_name = env_map.get(key)
if env_name:
ev = _env_bool(env_name)
if ev is not None:
return ev
cfg = _load_file_config()
if key in cfg:
val = cfg[key]
if isinstance(val, bool):
return val
if isinstance(val, str):
return val.strip().lower() in _TRUE
return default
def writeback_enabled() -> bool:
return _cfg_bool("writeback", False)
def reconcile_enabled() -> bool:
return _cfg_bool("reconcile", False)
def writeback_push_enabled() -> bool:
return _cfg_bool("writeback_push", False)
def pilot_slugs() -> set[str] | None:
"""None = all repos; empty set = none."""
raw = os.environ.get("RM_PILOT_REPOS")
if raw is not None:
raw = raw.strip()
if not raw:
return set()
return {s.strip() for s in raw.split(",") if s.strip()}
cfg = _load_file_config()
if "pilot_repos" not in cfg:
return None
val = cfg["pilot_repos"]
if val is None:
return None
if isinstance(val, str):
if not val.strip():
return set()
return {s.strip() for s in val.split(",") if s.strip()}
if isinstance(val, list):
return {str(s).strip() for s in val if str(s).strip()}
return None
def slug_allowed(slug: str | None) -> bool:
if not slug:
return False
pilots = pilot_slugs()
if pilots is None:
return True
return slug in pilots
def writeback_for_repo(slug: str | None) -> bool:
return writeback_enabled() and slug_allowed(slug)
def reconcile_for_repo(slug: str | None) -> bool:
return reconcile_enabled() and slug_allowed(slug)
def meter_path() -> Path:
raw = os.environ.get("RM_METER_PATH")
if raw:
return Path(raw).expanduser()
cfg = _load_file_config()
if cfg.get("meter_path"):
return Path(str(cfg["meter_path"])).expanduser()
return Path("~/.repo-manager/mutation-meter.jsonl").expanduser()
def record_mutation(
*,
source: Source,
kind: str,
repo_slug: str | None,
detail: dict[str, Any] | None = None,
) -> None:
try:
path = meter_path()
path.parent.mkdir(parents=True, exist_ok=True)
row = {
"ts": utc_now_text(),
"source": source,
"kind": kind,
"repo_slug": repo_slug,
"detail": detail or {},
}
with path.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(row, default=str) + "\n")
except OSError:
pass
def meter_summary(path: Path | None = None) -> dict[str, int]:
path = path or meter_path()
counts: dict[str, int] = {"state-hub": 0, "repo-manager": 0, "total": 0}
if not path.is_file():
return counts
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
row = json.loads(line)
except json.JSONDecodeError:
continue
src = row.get("source")
if src in counts:
counts[src] += 1
counts["total"] += 1
return counts
def flags_status() -> dict[str, Any]:
return {
"config_path": str(config_path()),
"config_exists": config_path().is_file(),
"writeback": writeback_enabled(),
"reconcile": reconcile_enabled(),
"writeback_push": writeback_push_enabled(),
"pilot_repos": sorted(pilot_slugs()) if pilot_slugs() is not None else None,
"meter_path": str(meter_path()),
"meter": meter_summary(),
# legacy env-style keys for operators
"RM_WRITEBACK": writeback_enabled(),
"RM_RECONCILE": reconcile_enabled(),
"RM_WRITEBACK_PUSH": writeback_push_enabled(),
"RM_PILOT_REPOS": sorted(pilot_slugs()) if pilot_slugs() is not None else None,
"RM_METER_PATH": str(meter_path()),
}

View file

@ -0,0 +1,251 @@
"""Read-only freshness-coverage measurement (STATE-WP-0094-T05 evidence).
Gathers, from currently supported sources only:
* State Hub REST (GET only): instance identity, ``/repos/``, per-repository
``work-record-projection/snapshot`` (``backing_synced_at``).
* Forge (``git ls-remote``, read-only): per-repository source HEAD commit.
* Kubernetes (``kubectl get``, read-only): runner images and the live
Activity Core definition that would route C-rule runs to Repo Manager.
* Local State Hub edge outbox (``statehub outbox status``): queued receipts.
It never writes to the hub, the cluster, Forge or any checkout, and it does not
read workstation checkouts (ADR-002). Fields that no supported source exposes are
reported under ``gaps`` instead of being inferred.
Usage::
python -m repo_manager.freshness --out docs/evidence/<date>-freshness-coverage.json
"""
from __future__ import annotations
import argparse
import json
import re
import statistics
import subprocess
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
DEFINITIONS_CONFIGMAP = ("activity-core", "actcore-external-activity-definitions")
C_RULE_QUERY = "consistency_sweep_remote_all"
def _get(api: str, path: str, timeout: float = 30) -> Any:
with urllib.request.urlopen(f"{api}{path}", timeout=timeout) as resp:
return json.loads(resp.read())
def _run(cmd: list[str], timeout: float = 30) -> tuple[int, str]:
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=False)
except (OSError, subprocess.TimeoutExpired) as exc:
return 1, str(exc)
return proc.returncode, (proc.stdout if proc.returncode == 0 else proc.stderr).strip()
def _parse_ts(value: str | None) -> datetime | None:
if not value:
return None
return datetime.fromisoformat(value)
def forge_head(remote_url: str | None) -> dict[str, Any]:
if not remote_url:
return {"commit": None, "error": "no remote_url"}
code, out = _run(["git", "ls-remote", remote_url, "HEAD"], timeout=30)
if code != 0 or not out:
return {"commit": None, "error": (out or "empty ls-remote")[-200:]}
return {"commit": out.split()[0], "error": None}
def projection_sync(api: str, slug: str) -> dict[str, Any]:
try:
snap = _get(api, f"/repos/{slug}/work-record-projection/snapshot")
except Exception as exc: # noqa: BLE001 - evidence collector records any failure
return {"error": str(exc)[-200:]}
stamps = [w.get("backing_synced_at") for w in snap.get("workplans", [])]
stamps = [s for s in stamps if s]
return {
"error": None,
"workplans": len(snap.get("workplans", [])),
"tasks": len(snap.get("tasks", [])),
"dependencies": len(snap.get("dependencies", [])),
"last_backing_synced_at": max(stamps) if stamps else None,
}
def live_definitions() -> dict[str, Any]:
ns, name = DEFINITIONS_CONFIGMAP
code, out = _run(["kubectl", "-n", ns, "get", "configmap", name, "-o", "json"])
if code != 0:
return {"error": out[-200:], "c_rule_definitions": []}
data = json.loads(out).get("data", {})
found = []
for key, body in sorted(data.items()):
if C_RULE_QUERY not in body and "repo-manager" not in body.lower():
continue
field = {
f: (m.group(1).strip().strip('"') if m else None)
for f, m in (
("id", re.search(r"^\s*id:\s*(.+)$", body, re.MULTILINE)),
("version", re.search(r"^\s*version:\s*(.+)$", body, re.MULTILINE)),
("enabled", re.search(r"^\s*enabled:\s*(\S+)", body, re.MULTILINE)),
("status", re.search(r"^\s*status:\s*(\S+)", body, re.MULTILINE)),
("cron", re.search(r"cron_expression:\s*\"?([^\"\n]+)", body)),
)
}
found.append({"key": key, **field})
return {"error": None, "configmap": f"{ns}/{name}", "c_rule_definitions": found}
def runner_revisions(repo_root: Path) -> dict[str, Any]:
code, head = _run(["git", "-C", str(repo_root), "rev-parse", "HEAD"])
images = {}
for ns, deploy in (("state-hub", "state-hub"), ("activity-core", "actcore-worker")):
c, img = _run(
[
"kubectl",
"-n",
ns,
"get",
"deploy",
deploy,
"-o",
"jsonpath={.spec.template.spec.containers[0].image}",
]
)
images[f"{ns}/{deploy}"] = img if c == 0 else f"unavailable: {img[-120:]}"
c, rm = _run(["kubectl", "get", "deploy", "-A", "-o", "name"])
rm_deployed = [d for d in rm.splitlines() if "repo-manager" in d] if c == 0 else None
return {
"repo_manager_source_head": head if code == 0 else None,
"repo_manager_cluster_deployments": rm_deployed,
"images": images,
}
def summarize(rows: list[dict[str, Any]], now: datetime) -> dict[str, Any]:
"""Pure summary of per-repository rows (unit-tested)."""
active = [r for r in rows if r.get("status") == "active"]
ages = []
for r in active:
ts = _parse_ts(r.get("last_backing_synced_at"))
r["age_hours"] = round((now - ts).total_seconds() / 3600, 1) if ts else None
if ts:
ages.append(r["age_hours"])
ages.sort()
def within(h: float) -> int:
return sum(1 for a in ages if a <= h)
def pct(p: float) -> float | None:
if not ages:
return None
return ages[min(len(ages) - 1, round(p * (len(ages) - 1)))]
return {
"repos_total": len(rows),
"repos_active": len(active),
"active_with_projection_sync": len(ages),
"active_never_synced_or_no_workplans": len(active) - len(ages),
"active_forge_head_resolved": sum(1 for r in active if r.get("forge_commit")),
"synced_within_24h": within(24),
"synced_within_7d": within(24 * 7),
"synced_within_30d": within(24 * 30),
"age_hours": {
"min": ages[0] if ages else None,
"median": statistics.median(ages) if ages else None,
"p90": pct(0.9),
"max": ages[-1] if ages else None,
},
}
def measure(api: str, repo_root: Path, workers: int = 8) -> dict[str, Any]:
now = datetime.now(UTC)
health = _get(api, "/state/health")
repos = _get(api, "/repos/?limit=1000")
def row(repo: dict[str, Any]) -> dict[str, Any]:
forge = forge_head(repo.get("remote_url")) if repo.get("status") == "active" else {}
proj = projection_sync(api, repo["slug"])
return {
"slug": repo["slug"],
"status": repo.get("status"),
"remote_url": repo.get("remote_url"),
"forge_commit": forge.get("commit"),
"forge_error": forge.get("error"),
"hub_last_state_synced_at": repo.get("last_state_synced_at"),
"last_backing_synced_at": proj.get("last_backing_synced_at"),
"projection_error": proj.get("error"),
"workplans": proj.get("workplans"),
"tasks": proj.get("tasks"),
"dependencies": proj.get("dependencies"),
}
with ThreadPoolExecutor(max_workers=workers) as pool:
rows = sorted(pool.map(row, repos), key=lambda r: r["slug"])
code, outbox = _run(["statehub", "outbox", "status"])
try:
outbox_status = json.loads(outbox) if code == 0 else {"error": outbox[-200:]}
except json.JSONDecodeError:
outbox_status = {"error": outbox[-200:]}
try:
ops_runs = _get(api, "/ops-runs/summary")
except Exception as exc: # noqa: BLE001
ops_runs = {"error": str(exc)[-200:]}
return {
"schema": "repo-manager.freshness-coverage.v1",
"measured_at": now.isoformat().replace("+00:00", "Z"),
"hub": {k: health.get(k) for k in ("status", "instance_role", "instance_label")}
| {"api_base": api, "schema": health.get("schema")},
"runner": runner_revisions(repo_root),
"active_definition": live_definitions(),
"receipts": {
"edge_outbox": outbox_status,
"ops_runs_summary": ops_runs,
},
"summary": summarize(rows, now),
"repositories": rows,
"gaps": [
(
"State Hub does not persist per-repository projection receipts or the "
"derived/expected source commit; `rmgr sync` returns the receipt to its "
"caller only. last_backing_synced_at is the closest supported proxy for "
"'last successful receipt' and proves no commit equality with forge_commit."
),
(
"Failed receipts are not persisted anywhere queryable (mutation meter has "
"no sync/projection kinds; ops-runs summary depends on ACTIVITY_CORE_URL)."
),
(
"No supported scheduled C-rule runner exists; freshness is driven only by "
"manual/agent-initiated syncs, so the window below is observed, not an SLA."
),
],
}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--api-base", default="http://127.0.0.1:8000")
parser.add_argument("--repo-root", default=str(Path(__file__).resolve().parents[2]))
parser.add_argument("--out", default=None)
args = parser.parse_args(argv)
result = measure(args.api_base.rstrip("/"), Path(args.repo_root))
text = json.dumps(result, indent=2, sort_keys=False) + "\n"
if args.out:
Path(args.out).write_text(text)
print(json.dumps(result["summary"], indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,154 @@
"""Durable freshness receipts (RMGR-WP-0020-T01).
Contract ``repo-manager.freshness-receipt.v1``: one immutable JSON object per
repository attempt of the bounded freshness runner. It records the repository,
the Forge commit the runner read, the expected/derived/projected commits bound by
State Hub's ``state-hub.repository-projection-reconcile.v1`` receipt, the
outcome, the reason, the runner revision, the Activity Core definition and fire,
timings, and the verified primary-hub identity.
Store: an append-only JSON Lines file in the Repo Manager evidence sink, next to
the mutation meter (``~/.repo-manager/mutation-meter.jsonl``) and the
idempotency store. This is repo-manager's existing persistence pattern. The
State Hub DB is deliberately not used, because the hub is the system under
measurement and is a read model. The file needs no new service, it is readable
with ``jq``/``rmgr freshness receipts``, and appends are serialised with an
advisory lock and fsynced. A deployed runner must mount this path on a
persistent volume (T04).
Path resolution: ``RM_FRESHNESS_RECEIPTS_PATH`` env, then
``freshness_receipts_path`` in the dual-run config file, then the default.
"""
from __future__ import annotations
import fcntl
import json
import os
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any
from repo_manager.dual_run import _load_file_config
SCHEMA = "repo-manager.freshness-receipt.v1"
DEFAULT_PATH = "~/.repo-manager/freshness-receipts.jsonl"
# ``noop`` covers both "hub reported nothing to change" and "Forge HEAD already
# equals the last successful receipt" (then ``reconciled`` is False).
# ``queued`` and ``skipped`` are reserved by the contract; the bounded runner
# never queues and does not write receipts for repositories it did not attempt.
OUTCOMES = ("applied", "noop", "queued", "failed", "refused", "skipped")
SUCCESS_OUTCOMES = frozenset({"applied", "noop"})
@dataclass
class HubIdentity:
api_base: str
instance_role: str | None
instance_label: str | None
expected_instance_label: str | None
verified_primary: bool
@dataclass
class FreshnessReceipt:
receipt_id: str
repo_slug: str
outcome: str
reason: str | None
forge_commit: str | None
forge_ref: str | None
expected_commit: str | None
derived_commit: str | None
projected_commit: str | None
reconciled: bool
http_status: int | None
hub: HubIdentity
runner_revision: str
definition_id: str | None
definition_version: str | None
fire_id: str
started_at: str
finished_at: str
elapsed_ms: float
detail: dict[str, Any] = field(default_factory=dict)
schema: str = SCHEMA
def __post_init__(self) -> None:
if self.outcome not in OUTCOMES:
raise ValueError(f"invalid freshness outcome: {self.outcome!r}")
if not self.hub.verified_primary:
raise ValueError("a freshness receipt requires a verified primary hub")
@property
def succeeded(self) -> bool:
return self.outcome in SUCCESS_OUTCOMES
def to_dict(self) -> dict[str, Any]:
return asdict(self)
def receipts_path() -> Path:
raw = os.environ.get("RM_FRESHNESS_RECEIPTS_PATH")
if raw:
return Path(raw).expanduser()
cfg = _load_file_config()
if cfg.get("freshness_receipts_path"):
return Path(str(cfg["freshness_receipts_path"])).expanduser()
return Path(DEFAULT_PATH).expanduser()
def append_receipt(receipt: FreshnessReceipt, path: Path | None = None) -> Path:
"""Append one receipt durably (locked, flushed and fsynced)."""
path = path or receipts_path()
path.parent.mkdir(parents=True, exist_ok=True)
line = json.dumps(receipt.to_dict(), sort_keys=True, default=str) + "\n"
with path.open("a", encoding="utf-8") as handle:
fcntl.flock(handle, fcntl.LOCK_EX)
try:
handle.write(line)
handle.flush()
os.fsync(handle.fileno())
finally:
fcntl.flock(handle, fcntl.LOCK_UN)
return path
def load_receipts(path: Path | None = None, repo_slug: str | None = None) -> list[dict[str, Any]]:
"""Read receipts in append order; malformed or foreign lines are ignored."""
path = path or receipts_path()
if not path.is_file():
return []
rows: list[dict[str, Any]] = []
with path.open(encoding="utf-8") as handle:
for raw in handle:
try:
row = json.loads(raw)
except json.JSONDecodeError:
continue
if not isinstance(row, dict) or row.get("schema") != SCHEMA:
continue
if repo_slug and row.get("repo_slug") != repo_slug:
continue
rows.append(row)
return rows
@dataclass
class ReceiptIndex:
last: dict[str, dict[str, Any]] = field(default_factory=dict)
last_success: dict[str, dict[str, Any]] = field(default_factory=dict)
def index_receipts(rows: list[dict[str, Any]]) -> ReceiptIndex:
"""Latest receipt and latest successful receipt per repository."""
index = ReceiptIndex()
for row in rows:
slug = row.get("repo_slug")
if not slug:
continue
index.last[slug] = row
if row.get("outcome") in SUCCESS_OUTCOMES and row.get("forge_commit"):
index.last_success[slug] = row
return index

View file

@ -0,0 +1,447 @@
"""Bounded Forge-sourced freshness runner (RMGR-WP-0020-T02).
``rmgr freshness run --limit N --max-seconds S [--dry-run]``
1. Verify that the answering State Hub is the primary (``instance_role`` and
``instance_label``). Otherwise refuse the whole run and write no receipts.
2. Rank active repositories stalest first. Repositories without a receipt come
first, ordered by ``backing_synced_at`` (missing first). Then come the rest,
ordered by the time of their latest receipt of any outcome, so that a
permanently refused hole cannot starve the rotation.
3. For at most N repositories, read the Forge default-branch HEAD over the
Forgejo HTTP API. Skip with ``noop`` (no hub request) when it equals the
commit of the last successful receipt.
4. Otherwise ask State Hub to derive the projection from that immutable Forge
commit (``POST /repos/{slug}/work-record-projection/reconcile``,
STATE-WP-0086). There is no checkout, no ``--all --remote`` and no
``/consistency/sweep/remote-all``. ``acknowledge_retirements`` is always
false, so a hub refusal is recorded as ``refused`` and never forced.
5. Verify the hub receipt like ``projection_sync``: schema, primary, instance
label, and expected == derived == projected == Forge commit.
6. Persist one ``repo-manager.freshness-receipt.v1`` per attempted
repository. Stop starting work at ``max_seconds``. Each request timeout is
clipped to the remaining budget.
"""
from __future__ import annotations
import subprocess
import time
import uuid
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor, wait
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
import httpx
from repo_manager import __version__
from repo_manager.freshness_receipts import (
FreshnessReceipt,
HubIdentity,
ReceiptIndex,
append_receipt,
index_receipts,
load_receipts,
receipts_path,
)
from repo_manager.time import utc_now_text
RUN_SCHEMA = "repo-manager.freshness-run.v1"
RECONCILE_SCHEMA = "state-hub.repository-projection-reconcile.v1"
DEFAULT_FORGE_BASE = "https://forgejo.coulomb.social"
DEFAULT_FORGE_OWNER = "coulomb"
# Never start a reconcile with less budget than this; the request would only
# time out with an unknown server-side outcome.
MIN_RECONCILE_SECONDS = 5.0
# Share of max_seconds (capped at 60 s) for the pre-receipt backing_synced_at fallback.
SNAPSHOT_BUDGET_FRACTION = 0.25
class HubNotPrimary(RuntimeError):
def __init__(self, identity: dict[str, Any]):
super().__init__("answering State Hub is not the verified primary")
self.identity = identity
@dataclass
class RunConfig:
limit: int
max_seconds: float
dry_run: bool = False
api_base: str = "http://127.0.0.1:8000"
expected_instance_label: str = "railiance01"
forge_base: str = DEFAULT_FORGE_BASE
forge_owner: str = DEFAULT_FORGE_OWNER
forge_token: str | None = None
receipts_path: Path | None = None
runner_revision: str | None = None
definition_id: str | None = None
definition_version: str | None = None
fire_id: str | None = None
snapshot_workers: int = 8
@dataclass
class Candidate:
slug: str
has_receipt: bool
rank_ts: str | None
last_success_commit: str | None = None
detail: dict[str, Any] = field(default_factory=dict)
def runner_revision() -> str:
"""Source revision of this runner: git HEAD of the package checkout or version."""
root = Path(__file__).resolve().parents[2]
try:
proc = subprocess.run(
["git", "-C", str(root), "rev-parse", "HEAD"],
capture_output=True, text=True, timeout=5, check=False,
)
except (OSError, subprocess.TimeoutExpired):
proc = None
if proc is not None and proc.returncode == 0 and proc.stdout.strip():
return proc.stdout.strip()
return f"repo-manager=={__version__}"
def rank_candidates(
active_slugs: list[str],
index: ReceiptIndex,
backing_synced_at: dict[str, str | None],
) -> list[Candidate]:
"""Pure stalest-first ranking (unit-tested)."""
never: list[Candidate] = []
seen: list[Candidate] = []
for slug in active_slugs:
last = index.last.get(slug)
success = index.last_success.get(slug)
commit = success.get("forge_commit") if success else None
if last is None:
never.append(Candidate(slug, False, backing_synced_at.get(slug), commit))
else:
seen.append(Candidate(slug, True, last.get("finished_at"), commit))
# ISO-8601 UTC strings sort chronologically; None (never synced) sorts first.
never.sort(key=lambda c: (c.rank_ts is not None, c.rank_ts or "", c.slug))
seen.sort(key=lambda c: (c.rank_ts or "", c.slug))
return never + seen
class _Budget:
def __init__(self, max_seconds: float, clock: Callable[[], float]):
self.clock = clock
self.deadline = clock() + max_seconds
def remaining(self) -> float:
return self.deadline - self.clock()
def _identity(health: dict[str, Any], cfg: RunConfig) -> HubIdentity:
role = health.get("instance_role")
label = health.get("instance_label")
return HubIdentity(
api_base=cfg.api_base.rstrip("/"),
instance_role=role,
instance_label=label,
expected_instance_label=cfg.expected_instance_label,
verified_primary=role == "primary" and label == cfg.expected_instance_label,
)
def _forge_head(forge: httpx.Client, owner: str, slug: str, timeout: float) -> dict[str, Any]:
try:
repo = forge.get(f"/api/v1/repos/{owner}/{slug}", timeout=timeout)
repo.raise_for_status()
branch = repo.json().get("default_branch") or "main"
resp = forge.get(f"/api/v1/repos/{owner}/{slug}/branches/{branch}", timeout=timeout)
resp.raise_for_status()
commit = (resp.json().get("commit") or {}).get("id")
except httpx.HTTPStatusError as exc:
return {"commit": None, "ref": None,
"error": f"forge HTTP {exc.response.status_code}"}
except (httpx.HTTPError, ValueError) as exc:
return {"commit": None, "ref": None, "error": f"forge unavailable: {exc}"[:300]}
if not isinstance(commit, str) or len(commit) != 40:
return {"commit": None, "ref": branch, "error": "forge returned no full commit id"}
return {"commit": commit.lower(), "ref": branch, "error": None}
def _backing_synced(hub: httpx.Client, slugs: list[str], workers: int,
budget: _Budget) -> dict[str, str | None]:
"""Fallback ranking key, bounded to a fraction of the run budget.
Repositories whose snapshot fails or does not answer in time rank as never
synced (first), which is the conservative choice.
"""
if not slugs:
return {}
phase = max(min(budget.remaining() * SNAPSHOT_BUDGET_FRACTION, 60.0), 0.5)
request_timeout = min(20.0, phase)
def one(slug: str) -> str | None:
try:
resp = hub.get(f"/repos/{slug}/work-record-projection/snapshot",
timeout=request_timeout)
resp.raise_for_status()
stamps = [w.get("backing_synced_at") for w in resp.json().get("workplans", [])]
except (httpx.HTTPError, ValueError, AttributeError):
return None
stamps = [s for s in stamps if s]
return max(stamps) if stamps else None
pool = ThreadPoolExecutor(max_workers=max(1, workers))
futures = {pool.submit(one, slug): slug for slug in slugs}
done, _ = wait(futures, timeout=phase)
# Do not wait for stragglers; in-flight requests end within request_timeout.
pool.shutdown(wait=False, cancel_futures=True)
return {slug: (f.result() if f in done else None) for f, slug in futures.items()}
def _verify_reconcile(receipt: Any, identity: HubIdentity, slug: str,
commit: str) -> str | None:
"""Return why a hub response is not a valid exact-commit primary receipt."""
if not isinstance(receipt, dict) or receipt.get("schema") != RECONCILE_SCHEMA:
return "unexpected receipt schema"
if receipt.get("instance_role") != "primary":
return "receipt not issued by a primary hub"
if receipt.get("instance_label") != identity.instance_label:
return "receipt instance label differs from the verified primary"
outcome = receipt.get("outcome")
if not isinstance(outcome, dict):
return "receipt has no outcome"
if not (receipt.get("expected_commit") == commit
and receipt.get("derived_commit") == commit
and outcome.get("commit") == commit):
return "expected/derived/projected commit does not equal the Forge commit"
if outcome.get("repo_slug") != slug:
return "receipt binds a different repository"
if outcome.get("status") not in {"applied", "noop", "refused"}:
return f"unexpected outcome status {outcome.get('status')!r}"
return None
def _http_error_detail(exc: httpx.HTTPStatusError) -> Any:
try:
body = exc.response.json()
except ValueError:
return exc.response.text[:300]
return body.get("detail", body) if isinstance(body, dict) else body
def _reconcile(hub: httpx.Client, identity: HubIdentity, slug: str, commit: str,
timeout: float) -> dict[str, Any]:
"""One immutable-commit reconcile. Returns receipt fields (no store write)."""
path = f"/repos/{slug}/work-record-projection/reconcile"
headers = {
"Idempotency-Key": f"rmgr-freshness:v1:{slug}:{commit}",
"X-StateHub-Source-Agent": "repo-manager",
}
try:
resp = hub.post(path, json={"expected_commit": commit, "acknowledge_retirements": False},
headers=headers, timeout=timeout)
resp.raise_for_status()
body = resp.json()
except httpx.TimeoutException:
return {"outcome": "failed", "reason": "hub_timeout_outcome_unknown",
"http_status": None}
except httpx.HTTPStatusError as exc:
status = exc.response.status_code
detail = _http_error_detail(exc)
fields: dict[str, Any] = {"outcome": "failed", "http_status": status,
"detail": {"hub_error": detail}}
if status == 409 and isinstance(detail, dict) and "derived_commit" in detail:
fields["reason"] = "forge_moved_during_run"
fields["derived_commit"] = detail.get("derived_commit")
elif status == 409 and isinstance(detail, dict) and "instance_role" in detail:
fields["reason"] = "hub_not_primary"
fields["abort"] = True
fields["no_receipt"] = True
elif status == 424:
fields["reason"] = "forge_unreadable_by_hub"
elif status == 502:
fields["reason"] = "hub_derivation_failed"
else:
fields["reason"] = f"hub_http_{status}"
return fields
except (httpx.HTTPError, ValueError) as exc:
return {"outcome": "failed", "reason": "hub_unavailable", "http_status": None,
"detail": {"error": str(exc)[:300]}, "abort": True}
invalid = _verify_reconcile(body, identity, slug, commit)
if invalid:
# Kill criterion: never persist an unverified primary receipt as success.
identity_fault = "primary" in invalid or "instance label" in invalid
return {"outcome": "failed", "reason": f"invalid_receipt: {invalid}",
"http_status": resp.status_code, "abort": True,
"no_receipt": identity_fault,
"derived_commit": body.get("derived_commit") if isinstance(body, dict) else None}
outcome = body["outcome"]
status = outcome["status"]
fields = {
"outcome": status,
"reason": None,
"http_status": resp.status_code,
"expected_commit": body["expected_commit"],
"derived_commit": body["derived_commit"],
"projected_commit": outcome["commit"],
"detail": {"counts": outcome.get("counts"), "notes": outcome.get("notes")},
}
if status == "refused":
refused = outcome.get("refused") or []
fields["reason"] = "hub_refused"
fields["detail"]["refused"] = refused[:20]
return fields
def run(
cfg: RunConfig,
*,
hub_transport: httpx.BaseTransport | None = None,
forge_transport: httpx.BaseTransport | None = None,
clock: Callable[[], float] = time.monotonic,
) -> dict[str, Any]:
if cfg.limit < 1:
raise ValueError("--limit must be at least 1")
if cfg.max_seconds <= 0:
raise ValueError("--max-seconds must be positive")
budget = _Budget(cfg.max_seconds, clock)
started_at = utc_now_text()
fire_id = cfg.fire_id or str(uuid.uuid4())
revision = cfg.runner_revision or runner_revision()
store = cfg.receipts_path or receipts_path()
forge_headers = {"Authorization": f"token {cfg.forge_token}"} if cfg.forge_token else {}
summary: dict[str, Any] = {
"schema": RUN_SCHEMA,
"fire_id": fire_id,
"dry_run": cfg.dry_run,
"limit": cfg.limit,
"max_seconds": cfg.max_seconds,
"runner_revision": revision,
"definition_id": cfg.definition_id,
"definition_version": cfg.definition_version,
"receipts_path": str(store),
"started_at": started_at,
}
with httpx.Client(base_url=cfg.api_base.rstrip("/"), follow_redirects=True,
timeout=httpx.Timeout(30.0, connect=5.0),
transport=hub_transport) as hub, \
httpx.Client(base_url=cfg.forge_base.rstrip("/"), follow_redirects=True,
timeout=httpx.Timeout(15.0, connect=5.0), headers=forge_headers,
transport=forge_transport) as forge:
health = hub.get("/state/health")
health.raise_for_status()
identity = _identity(health.json(), cfg)
summary["hub"] = vars(identity)
if not identity.verified_primary:
raise HubNotPrimary(vars(identity))
repos = hub.get("/repos/", params={"limit": 1000})
repos.raise_for_status()
rows = {r["slug"]: r for r in repos.json()
if isinstance(r, dict) and r.get("status") == "active" and r.get("slug")}
active = sorted(rows)
index = index_receipts(load_receipts(store))
never = [s for s in active if s not in index.last]
synced = _backing_synced(hub, never, cfg.snapshot_workers, budget)
# A slow or failing snapshot falls back to the repository row's own
# last_state_synced_at, which costs no extra request.
fallback_used = 0
for slug in never:
if synced.get(slug) is None and rows[slug].get("last_state_synced_at"):
synced[slug] = rows[slug]["last_state_synced_at"]
fallback_used += 1
ranked = rank_candidates(active, index, synced)
selected = ranked[: cfg.limit]
summary["active_repositories"] = len(active)
summary["never_received"] = len(never)
summary["rank_fallback_last_state_synced_at"] = fallback_used
results: list[dict[str, Any]] = []
deferred: list[str] = []
aborted: str | None = None
for position, cand in enumerate(selected):
if aborted or budget.remaining() <= 0:
deferred.extend(c.slug for c in selected[position:])
break
t0 = clock()
attempt_started = utc_now_text()
head = _forge_head(forge, cfg.forge_owner, cand.slug,
timeout=min(15.0, max(budget.remaining(), 0.5)))
row: dict[str, Any] = {
"repo_slug": cand.slug,
"rank_key": {"has_receipt": cand.has_receipt, "ts": cand.rank_ts},
"forge_commit": head["commit"],
"forge_ref": head["ref"],
"last_success_commit": cand.last_success_commit,
}
fields: dict[str, Any]
if head["commit"] is None:
fields = {"outcome": "failed", "reason": "forge_head_unresolved",
"http_status": None, "detail": {"forge_error": head["error"]}}
row["action"] = "fail"
elif head["commit"] == cand.last_success_commit:
fields = {"outcome": "noop", "reason": "forge_head_matches_last_success",
"http_status": None}
row["action"] = "skip_noop"
elif cfg.dry_run:
row["action"] = "reconcile"
results.append(row)
continue
elif budget.remaining() < MIN_RECONCILE_SECONDS:
deferred.extend(c.slug for c in selected[position:])
break
else:
row["action"] = "reconcile"
fields = _reconcile(hub, identity, cand.slug, head["commit"],
timeout=min(120.0, budget.remaining()))
row.update({k: v for k, v in fields.items() if k not in {"abort", "no_receipt"}})
if fields.get("abort"):
aborted = fields["reason"]
# Never persist a receipt whose primary identity the hub did not confirm.
if fields.get("no_receipt"):
row["receipt_withheld"] = True
elif not cfg.dry_run:
receipt = FreshnessReceipt(
receipt_id=str(uuid.uuid4()),
repo_slug=cand.slug,
outcome=fields["outcome"],
reason=fields.get("reason"),
forge_commit=head["commit"],
forge_ref=head["ref"],
expected_commit=fields.get("expected_commit"),
derived_commit=fields.get("derived_commit"),
projected_commit=fields.get("projected_commit"),
reconciled=row["action"] == "reconcile",
http_status=fields.get("http_status"),
hub=identity,
runner_revision=revision,
definition_id=cfg.definition_id,
definition_version=cfg.definition_version,
fire_id=fire_id,
started_at=attempt_started,
finished_at=utc_now_text(),
elapsed_ms=round((clock() - t0) * 1000, 1),
detail=fields.get("detail") or {},
)
append_receipt(receipt, store)
row["receipt_id"] = receipt.receipt_id
results.append(row)
counts: dict[str, int] = {}
for row in results:
key = row.get("outcome") or f"plan:{row.get('action')}"
counts[key] = counts.get(key, 0) + 1
summary.update({
"selected": [c.slug for c in selected],
"results": results,
"deferred": deferred,
"aborted": aborted,
"counts": counts,
"finished_at": utc_now_text(),
"elapsed_seconds": round(cfg.max_seconds - budget.remaining(), 2),
})
return summary

View file

@ -0,0 +1,91 @@
"""Git helpers via subprocess (ADR-001)."""
from __future__ import annotations
import subprocess
from pathlib import Path
# The admitted receive scanner has a 90-second total budget. Leave bounded
# headroom for transport and Git hooks; a timeout never proves remote refusal.
PUSH_TIMEOUT_SECONDS = 120
class GitError(RuntimeError):
pass
def _run(repo: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
proc = subprocess.run(
["git", *args],
cwd=repo,
capture_output=True,
text=True,
check=False,
)
if check and proc.returncode != 0:
raise GitError(proc.stderr.strip() or proc.stdout.strip() or f"git {' '.join(args)} failed")
return proc
def head_sha(repo: Path) -> str | None:
proc = _run(repo, "rev-parse", "HEAD", check=False)
if proc.returncode != 0:
return None
return proc.stdout.strip() or None
def is_git_repo(repo: Path) -> bool:
return (repo / ".git").exists() or _run(repo, "rev-parse", "--git-dir", check=False).returncode == 0
def commit_paths(
repo: Path,
paths: list[str],
message: str,
*,
author_name: str = "repo-manager",
author_email: str = "repo-manager@local",
) -> str:
"""Stage paths and commit. Returns new HEAD sha."""
env_author = [
"-c",
f"user.name={author_name}",
"-c",
f"user.email={author_email}",
]
for path in paths:
_run(repo, "add", "--", path)
proc = subprocess.run(
["git", *env_author, "commit", "-m", message],
cwd=repo,
capture_output=True,
text=True,
check=False,
)
if proc.returncode != 0:
raise GitError(proc.stderr.strip() or proc.stdout.strip() or "git commit failed")
sha = head_sha(repo)
if not sha:
raise GitError("commit succeeded but HEAD missing")
return sha
def push_ff(repo: Path) -> tuple[bool, str]:
"""Best-effort push (push-seal compatible). Never force-pushes."""
try:
proc = subprocess.run(
["git", "push"],
cwd=repo,
capture_output=True,
text=True,
timeout=PUSH_TIMEOUT_SECONDS,
check=False,
)
if proc.returncode == 0:
return True, (proc.stdout.strip() or "pushed")
return False, (proc.stderr.strip() or proc.stdout.strip() or "push failed")
except subprocess.TimeoutExpired:
return False, "push timed out; remote outcome unverified; inspect the remote ref before retrying"
except Exception as exc: # noqa: BLE001
return False, str(exc)

View file

@ -0,0 +1,211 @@
"""Reviewed closed-source exceptions; enable only after receiving deployment.
No aliases, new IDs, binding replacement or source edits are authorized.
Strict identity scans retain nonconformance findings for these legacy labels.
"""
from hashlib import sha256
import json
BINDINGS = json.loads(r'''[
{
"repo": "activity-core",
"path": "workplans/archived/260707-custodian-WP-0003-event-bridge.md",
"sha256": "83ea8bcb151e003419b73c59dbd028f4a1ce3d1120a82e4a128a772503ff3b76",
"record_id": "custodian-WP-0003",
"uuid": "b4eb45a9-69e3-4ab0-b00c-67a53c3117c5",
"parent_record_id": "custodian-WP-0003"
},
{
"repo": "activity-core",
"path": "workplans/custodian-WP-0001-temporal-backbone.md",
"sha256": "020e2eac973f2daa41c1687f8e867da9d521f945547e6aa82c87993b3ff6fc64",
"record_id": "custodian-WP-0001",
"uuid": "e22516cc-4fdf-4dad-a928-a9e04a9fa7c7",
"parent_record_id": "custodian-WP-0001"
},
{
"repo": "activity-core",
"path": "workplans/custodian-WP-0002-triggers-ops.md",
"sha256": "d8cbc61631c01b6f11a03a63f2fb6275fc853ff7f7459ce65973ed1a233daa01",
"record_id": "custodian-WP-0002",
"uuid": "3a4f47d9-8bc1-434e-acb4-bed5d4dacda0",
"parent_record_id": "custodian-WP-0002"
},
{
"repo": "activity-core",
"path": "workplans/custodian-WP-0003a-event-bridge-model-rules-registry.md",
"sha256": "ffa6682e9ba3aaf559bd44203d561c90dea69bcf1b1e778209a7b209e6e5cdd3",
"record_id": "custodian-WP-0003a",
"uuid": "b4eb45a9-69e3-4ab0-b00c-67a53c3117c5",
"parent_record_id": "custodian-WP-0003"
},
{
"repo": "activity-core",
"path": "workplans/custodian-WP-0003b-event-bridge-parser-workflow-triggers.md",
"sha256": "4fc34e0f821e2df51e5c08c7ab34da35b65690381d01f0cfdf55d00c970b414c",
"record_id": "custodian-WP-0003b",
"uuid": "b4eb45a9-69e3-4ab0-b00c-67a53c3117c5",
"parent_record_id": "custodian-WP-0003"
},
{
"repo": "activity-core",
"path": "workplans/custodian-WP-0003c-event-bridge-context-integration.md",
"sha256": "b6cd62b26515e509670ca39736b6bcec9633beedd46c9bd8f7598c06bac7276b",
"record_id": "custodian-WP-0003c",
"uuid": "b4eb45a9-69e3-4ab0-b00c-67a53c3117c5",
"parent_record_id": "custodian-WP-0003"
},
{
"repo": "activity-core",
"path": "workplans/custodian-WP-0004-railiance-ops.md",
"sha256": "20df02eea740e109c937130e7f8801a636fc0990ed8c6d2ced55659f8957adb1",
"record_id": "custodian-WP-0004",
"uuid": "759b1255-aa78-42b5-8fab-c5fcf168f5f4",
"parent_record_id": "custodian-WP-0004"
},
{
"repo": "the-custodian",
"path": "workplans/ADHOC-2026-08-25-retired.md",
"sha256": "344a1293dbb3a67a944eb34ca4a0337487d8f047226027a2107922346fbab2dd",
"record_id": "CUST-WP-ADHOC-2026-08-25-RETIRED",
"uuid": "1c53d3db-6633-5e2d-987f-10706d73be9e",
"parent_record_id": "CUST-WP-ADHOC-2026-08-25-RETIRED"
},
{
"repo": "railiance-cluster",
"path": "workplans/archived/260622-RAIL-BS-WP-0002-k3s-baseline.md",
"sha256": "8d6f8533c119117686bc0d5bb9ed5349726aa6c3189ff98b53409c68ac685ca9",
"record_id": "RAIL-BS-WP-0002",
"uuid": "4c63dfc6-9eac-4e79-9f77-8f644ad7147d",
"parent_record_id": "RAIL-BS-WP-0002",
"tasks": [
{
"label": "T01",
"uuid": "3f042630-eab0-4c6a-9167-e2b28ff20e40",
"status": "done"
},
{
"label": "T02",
"uuid": "e8510646-46ed-4697-a345-f3d3009eea78",
"status": "done"
},
{
"label": "T03",
"uuid": "dab2c07f-8aa0-4635-8df6-857e87e93fc5",
"status": "done"
},
{
"label": "T04",
"uuid": "5c3d40e4-239b-488e-9519-6f7a38d2325f",
"status": "done"
},
{
"label": "T05",
"uuid": "7f9e0e58-a130-467a-a2d0-b3f2564e496f",
"status": "done"
}
]
},
{
"repo": "railiance-cluster",
"path": "workplans/archived/260622-RAIL-BS-WP-0003-pgpool-ha-failover-fix.md",
"sha256": "4dc2f61e15adc3c5c81f9ecf8b933512275b95e8f829fac52ae7cde1385cea4a",
"record_id": "RAIL-BS-WP-0003",
"uuid": "7ee9ee22-1fae-4567-9194-8d70a9e0f45b",
"parent_record_id": "RAIL-BS-WP-0003",
"tasks": [
{
"label": "T01",
"uuid": "6841c93a-f146-47eb-9f7c-8fa0e02c1bbc",
"status": "done"
},
{
"label": "T02",
"uuid": "ab166073-30a7-4702-a037-4091e8706e20",
"status": "done"
},
{
"label": "T03",
"uuid": "140da396-8e30-4f4d-b88c-c42c0cd46c01",
"status": "done"
},
{
"label": "T04",
"uuid": "d8a3ba40-fda0-4c1f-a9f1-ffcd621a5b3d",
"status": "done"
}
]
},
{
"repo": "railiance-cluster",
"path": "workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md",
"sha256": "04f787078232395bd2d43861de16c521848559fa286f06e1caa9cb779aa71ac3",
"record_id": "RAIL-BS-WP-0004",
"uuid": "7e8b0c20-51eb-40c9-9e3b-85dd380d7625",
"parent_record_id": "RAIL-BS-WP-0004",
"tasks": [
{
"label": "T01",
"uuid": "4526a842-ea31-4874-9231-92ab556cfe7b",
"status": "done"
},
{
"label": "T02",
"uuid": "a6313e06-1976-46a7-8e31-df4eb2eca880",
"status": "done"
},
{
"label": "T03",
"uuid": "05d42a55-921f-4aa7-bb76-e8af9c7e0ac3",
"status": "done"
},
{
"label": "T04",
"uuid": "08233868-d522-4117-bc4e-6c0f52545665",
"status": "done"
},
{
"label": "T05",
"uuid": "2d5acff7-4a4e-4ddd-ad06-08237ad3dac8",
"status": "done"
},
{
"label": "T06",
"uuid": "f8e4a094-c367-40eb-b895-da17bc144b07",
"status": "done"
}
]
}
]''')
def historical_source_paths(repo_root):
entries = [entry for entry in BINDINGS if entry["repo"] == repo_root.name]
paths = set()
for entry in entries:
path = repo_root / entry["path"]
if path.is_symlink() or not path.is_file() or sha256(path.read_bytes()).hexdigest() != entry["sha256"]:
raise ValueError("Historical source binding requires owner review: " + entry["path"])
paths.add(entry["path"])
return paths
def historical_source_occurrences(repo_root):
"""Exact frozen closed task occurrences; never a general local-ID alias."""
from repo_manager.parse.workplan import parse_workplan_file
from uuid import UUID
paths=historical_source_paths(repo_root)
occurrences=set(paths)
for entry in BINDINGS:
if entry['repo']!=repo_root.name or 'tasks' not in entry:continue
parsed=parse_workplan_file(repo_root/entry['path'],repo_root=repo_root)
actual=[{'label':task.id,'uuid':task.state_hub_task_id,'status':task.status} for task in parsed.tasks]
if (parsed.id!=entry['record_id'] or parsed.state_hub_workstream_id!=entry['uuid']
or parsed.status not in {'finished','archived','completed'} or actual!=entry['tasks']
or any(task['status'] not in {'done','cancel'} for task in actual)):
raise ValueError('Historical task binding requires owner review: '+entry['path'])
UUID(entry['uuid'])
for number,task in enumerate(actual,1):
UUID(task['uuid'])
occurrences.add(entry['path']+'#task-block-'+str(number))
return occurrences

View file

@ -0,0 +1,111 @@
"""Canonical State Hub access map for agent-facing AGENTS.md (CUST-WP-0067-T07).
The port map used to be prose copied into ~180 AGENTS.md files. Topology
changes then required a fleet-wide rewrite. This module is the single source:
edit ``config/state-hub-access.yaml`` and refresh with
``rmgr scaffold --refresh-hub-access``.
"""
from __future__ import annotations
import re
from pathlib import Path
from typing import Any
import yaml
BEGIN = "<!-- BEGIN STATE-HUB-ACCESS -->"
END = "<!-- END STATE-HUB-ACCESS -->"
DEFAULT_CONTRACT = Path(__file__).resolve().parents[2] / "config" / "state-hub-access.yaml"
_FALLBACK: dict[str, Any] = {
"endpoints": [
{"context": "Local workstation", "url": "http://127.0.0.1:8000"},
{"context": "Remote (railiance01, in-cluster)", "url": "http://10.43.68.154:8000"},
{"context": "Optional local edge relay", "url": "http://127.0.0.1:18080"},
],
"retired": [
{
"url": "http://127.0.0.1:18000",
"reason": "reverse tunnel back to the workstation; the hub is in-cluster on railiance01",
}
],
}
_TABLE_RE = re.compile(
r"\| Context \| URL \|\r?\n\|[-:| ]+\|\r?\n(?:\|[^\n]*\|\r?\n)+",
re.MULTILINE,
)
_MARKED_RE = re.compile(
re.escape(BEGIN) + r".*?" + re.escape(END),
re.DOTALL,
)
def load_access(path: Path | None = None) -> dict[str, Any]:
target = path or DEFAULT_CONTRACT
if target.is_file():
payload = yaml.safe_load(target.read_text(encoding="utf-8")) or {}
if isinstance(payload, dict) and payload.get("endpoints"):
return payload
return _FALLBACK
def render_hub_access_block(access: dict[str, Any] | None = None) -> str:
data = access or load_access()
rows = ["| Context | URL |", "|---------|-----|"]
for item in data.get("endpoints") or []:
context = str(item.get("context") or "").strip()
url = str(item.get("url") or "").strip()
rows.append(f"| {context} | `{url}` |")
retired = data.get("retired") or []
note_lines: list[str] = []
for item in retired:
url = str(item.get("url") or "").strip()
if not url:
continue
note_lines.append(
f"Do not instruct a remote agent to use `{url}`. That reverse tunnel "
"is retired; on railiance01 reach the hub at the in-cluster address above."
)
body = "\n".join(rows)
if note_lines:
body = body + "\n\n" + "\n".join(note_lines)
return f"{BEGIN}\n{body}\n{END}"
def _table_is_hub_access(table: str) -> bool:
lowered = table.lower()
return "local workstation" in lowered and (
"railiance01" in lowered or "10.43.68.154" in lowered or "127.0.0.1:18000" in lowered
)
def apply_hub_access_block(text: str, block: str | None = None) -> tuple[str, str]:
"""Insert or replace the canonical access table. Returns (new_text, action)."""
block = block or render_hub_access_block()
if block in text:
return text, "noop"
marked = _MARKED_RE.search(text)
if marked:
return text[: marked.start()] + block + text[marked.end() :], "replaced-marked"
for match in _TABLE_RE.finditer(text):
if _table_is_hub_access(match.group(0)):
return text[: match.start()] + block + "\n" + text[match.end() :], "replaced-table"
heading = re.search(r"^## State Hub Integration\s*$", text, re.MULTILINE)
if heading:
insert_at = heading.end()
return text[:insert_at] + "\n\n" + block + text[insert_at:], "inserted"
if text and not text.endswith("\n"):
text += "\n"
return text + "\n## State Hub Integration\n\n" + block + "\n", "appended"
def live_urls(access: dict[str, Any] | None = None) -> list[str]:
data = access or load_access()
return [str(item.get("url") or "").strip() for item in data.get("endpoints") or []]
def retired_urls(access: dict[str, Any] | None = None) -> list[str]:
data = access or load_access()
return [str(item.get("url") or "").strip() for item in data.get("retired") or []]