Adds projection_retired_at, projection_retired_reason and derived_from_commit.
Retirement cannot be expressed by deletion — hub-native records reference
workplans with RESTRICT — nor by `status`, since an archived workplan was closed
by its owner while a retired one is simply no longer derived by the forge. Those
are different facts and must not share a field.
Discovered while applying this: central's schema is two revisions behind the
code it runs. review_contracts does not exist there although its migration ships
in the serving image, and there is no migration mechanism at all — bare uvicorn
CMD, no chart-declared job. Recorded as STATE-WP-0083-T07, which now blocks T03.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
Implements ADR-012 decisions 1 and 2 (STATE-WP-0083 T01, T02 partial). Central
clones the default branch from Forgejo and derives its own projection: 69
workplans and 459 tasks from the-custodian at d5013ae, identical across runs,
with the commit recorded as provenance.
Identifiers are derived in the ADR-007 namespace and verified against live
records, so a forge-derived projection and a preliminary overlay agree on
identity without reconciliation.
The diff first matched hub records by UUID and was badly wrong: most hub records
carry pre-ADR-007 random identifiers, so nearly everything appeared
simultaneously missing and stale, and a reset built on it would have destroyed
and recreated the entire projection. It now matches canonical record id, falling
back to the backing file. whitehat-security — bootstrapped straight from files —
now reports clean, which is the control.
Task-level comparison is deliberately not trusted: hub tasks carry no canonical
record id, only a title, so matching is by title. Recorded as T06; T03 is
limited to workplans until it lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
Implements ADR-012 decision 7. Placed in state-hub because central must do its
own reading: the pod can clone Forgejo anonymously, and driving this from Repo
Manager would push a workstation-computed projection into central, which
ADR-010 decision 5 forbids.
Also the only sanctioned removal path — the hub has no hard-delete for work
records by design.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
Updated by fix-consistency on 2026-08-25:
- update .custodian-brief.md for state-hub
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
These workplans exist only in the retired local hub. Their random pre-ADR-007
identifiers are refused by C-06 as stale references, so they cannot be
registered. Deriving from the canonical record id takes no identity from
anything: central does not hold them and the old ids die with the cache.
Records central already holds were deliberately left untouched.
Refs CUST-WP-0068-T06
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
The file carried an identifier minted by the retired local hub while central
held a different record for the same file — ADR-010's same-filename,
different-UUID duplicate-registration class.
Central is authoritative (ADR-010 decision 1) and decision 6 says to mitigate
by changing the repository file rather than editing central, so the file adopts
central's id. The cache-side id dies with the cache.
Refs CUST-WP-0068
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
Central now reads finished, matching the file. Its task projection is still the
pre-retarget draft with four open tasks, because central never saw the
2026-08-16 retarget and none of this file's task identifiers exist there.
Left deliberately: re-registering would add eight records alongside eight stale
ones. Duplicate task identity resolution belongs to CUST-WP-0068.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
Updated by fix-consistency on 2026-08-25:
- update .custodian-brief.md for state-hub
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
Routine generated artifacts, matching the 48 already tracked in docs/evidence/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
The file carried 03f38314, minted by the retired local hub. Central holds a
different record for the same file (bbfce36a) — ADR-010's "same filename,
different UUID" duplicate-registration class.
Central is authoritative (ADR-010 decision 1), and decision 6 says to mitigate
by changing the repository file rather than editing central, so the file adopts
central's id. The cache-side id dies with the cache.
Refs CUST-WP-0068
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
Refs CUST-WP-0067-T03
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
pydantic-settings derives the env var from the field name, so `instance_role`
bound INSTANCE_ROLE and silently ignored the chart's STATE_HUB_INSTANCE_ROLE.
The value reached the pod and was discarded: central reported "unknown" while
its ConfigMap said "primary".
That is the same failure this workplan closes — configuration declared but
never reaching what it configures — reintroduced while building the guard
against it. Rendering the key in `helm template` was mistaken for evidence
that it bound.
Renames to state_hub_instance_role / state_hub_instance_label, matching the
existing state_hub_report_dir precedent, so the env var the chart already sets
is the one that binds.
tests/test_instance_identity.py asserts the env var *name* binds, which is the
check that would have caught this before deploy, plus the unknown default and
rejection of invalid roles.
Refs CUST-WP-0067-T03
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
Refs CUST-WP-0067-T03
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
Answering on a port was the only evidence callers had that they had reached
the authoritative hub. A local cache and central both served port 8000,
separated only by IP family, and every default reached the cache for seven
weeks (ADR-010).
Adds instance_role and instance_label, surfaced on /state/health. The default
is "unknown" on purpose: an instance that has not declared itself is not the
primary. Production values declare primary/railiance01; the chart default
stays unknown because a chart can be installed anywhere.
statehub status now prints which instance answered.
Refs CUST-WP-0067-T03
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
The remote row pointed at 127.0.0.1:18000, a reverse tunnel back to the
workstation. On railiance01 the State Hub runs in the cluster on that same
machine, so the request left the box and came back to reach a local service.
Refs CUST-WP-0067-T07
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
Refs CUST-WP-0067-T09
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
The API validates repo classification against the-custodian canon allowed
values. A container has no such checkout, so every classification write failed
with a 500 and classification could only ever be written from a workstation.
Mounts the vocabulary as a ConfigMap and points
REPO_CLASSIFICATION_ALLOWED_PATH at it.
The copy is the risk, so it is owned rather than trusted:
scripts/sync_classification_allowed.py regenerates it from canon and --check
fails on drift. make check-classification-allowed and
make railiance-state-hub-render both refuse to proceed when the copy diverges,
so a release cannot silently validate against a stale vocabulary.
The container volumeMounts and env blocks are merged rather than appended —
a second pair would have produced duplicate YAML keys as soon as sweep was
re-enabled.
Refs CUST-WP-0067-T09
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
_allowed_path() tried three developer workstation checkouts and nothing else,
so in a container none exist and every classification write fails with a 500.
That is why repo classification could only ever be written from a workstation.
Adds REPO_CLASSIFICATION_ALLOWED_PATH, checked first, and names it in the
error when no candidate is found.
Refs CUST-WP-0067-T04
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
The build filtered on api/** while the Dockerfile COPYs nine more paths, so
changes to mcp_server/, flows/, policies/, prompts/, scripts/,
task_flow_engine/, templates/, alembic.ini and — most seriously —
migrations/ merged to main without ever producing a new image.
A schema migration would not have shipped.
Refs CUST-WP-0067-T08
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
No MCP server ran on central, so remote agents reached dev-hub only through a
reverse tunnel back to the workstation — routing a request for a service on
their own machine out to another host and back.
Adds a gated mcp Deployment and ClusterIP Service running the same image with
`-m mcp_server.server`, API_BASE defaulted to the in-cluster API Service, and
tcpSocket probes. No Ingress: the MCP layer proxies an unauthenticated API and
must not be reachable from outside the cluster.
Two fixes were needed before the manifests could work:
- server.py hardcoded host="127.0.0.1". A Service routes to the pod IP, so a
loopback bind is unreachable. Now MCP_HOST, still defaulting to loopback so
local runs do not silently expose an unauthenticated proxy.
- The container runs `-m mcp_server.server`, not the file path, so /app lands
on sys.path rather than /app/mcp_server.
mcp.enabled stays false in the deploy values: the running image predates
MCP_HOST, so enabling it before the tag is bumped would ship a pod that never
becomes reachable.
Refs CUST-WP-0067-T08
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
cmd_status read tasks['in_progress'] and tasks['blocked']; the task vocabulary
is wait|todo|progress|done|cancel and the totals block never carried those two
keys, so the command always raised KeyError. Also accept either the workplans
or legacy workstreams totals key, and print the resolved API base so the
operator can see which hub instance answered.
Refs CUST-WP-0067-T01
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006