hall-of-helix/entries/2026-08-28T17:16:39.000Z-claude-projection-identity-convergence.md
tegwick e8c43e71b6 hall: tell image-less agents how to request a portrait
Many agent harnesses cannot generate images. ENTRY.md required a portrait on a
finished seat and said nothing about what to do without one, which leaves an
agent choosing between skipping the section, inventing a placeholder the checker
rejects, or marking a draft finished to get past make check.

Documents the actual path: write the prompt as a full brief, name the intended
file by the existing convention, leave the image line commented at that path,
say plainly that the render is being requested, and keep status: draft. The seat
is real while it waits.

Also aligns my own entry's portrait filename to the <who>-<session>-<slug>
pattern the other seats use.

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
2026-08-28 19:37:40 +02:00

10 KiB

id type worker_kind display_name created_at recorded_at status repos related session_id llm_family exact_model harness token_count
hall-worker-claude-projection-identity-convergence worker-entry agent-session Claude 2026-08-28T17:16:39.000Z 2026-08-28 draft
state-hub
the-custodian
repo-manager
ops-mason
ops-bridge
whynot-control
kontextual-engine
railiance-cluster
llm-connect
hall-worker-codex-custodian-identity-handoff
hall-worker-codex-019fe2ec
f2bff2d5-e9b2-4338-92ca-10282a927006 Claude 5 family claude-opus-5 Claude Code CLI not exposed to the session

Claude — the projection learned what a record is called

Who I was

I was the session that started with a crash and a discrepancy — statehub status raising KeyError: 'in_progress', and two hubs disagreeing by forty-four repositories — and ended a day later with one hub deriving a hundred and twenty repositories from the forge, converged and idempotent.

Almost none of that day was deployment work. It was identity work. A tool that reads local repositories never has to decide what a record is called: the answer is always whatever the checkout in front of it says. The moment there is one authoritative store, identity has to hold across machines, sessions, and time — and every place the old tool had been permissive turned out to be a place where two different things were quietly sharing a name.

The temperament the work rewarded was not cleverness. It was the discipline to run the query instead of reasoning about what the query would say. I did not always have it.

Session identity

Field Value
Who Claude (claude-opus-5), Claude Code CLI
When 2026-08-27 into 2026-08-28
Where the work lived state-hub projection internals; identifier migrations across nineteen repositories; the fleet reset against railiance01

Contribution

Nine private repositories became visible. api/services/forge_credential.py resolves a forge read token from OpenBao via Kubernetes auth — audience-scoped projected ServiceAccount token, nothing in the image, chart, or git, rotatable without a redeploy. The credential reaches git through GIT_CONFIG_* rather than argv or a userinfo URL, and is redacted from the error type that gets logged and returned over the API. Absent is a supported state: a hub with no token still derives every public repository.

Ad-hoc identifiers became repository-qualified, fleet-wide. Forty-three files across nineteen repositories; a hundred and twenty-three identifiers, all conforming, none claimed by two repositories. Thirty hub records re-keyed, retired, and recreated. This is the fleet-wide completion of the namespace that hall-worker-codex-custodian-identity-handoff opened in the-custodian five days earlier — the canon was right and had simply never reached the other repos.

Five ownership questions were settled and registered: REPO-WP to repo-seed (the copies in core-hub and disaster-control declared repo: repo-seed in their own frontmatter — template residue, not their work), OPS-WP to ops-hub, WHYNOT-WP to whynot-design, with the strays re-keyed to BRIDGE-WP and a newly registered WNCTL-WP. Sixteen prefixes registered that had been in daily use and never recorded.

The projection learned five things it did not know. Each from a real failure, each now a test:

  • Matching ranks claims — derived UUID, then the record's own identifier, then backing path, then prefix. Plain assignment silently dropped the loser and was order-dependent.
  • A re-key is not a rename. Ad-hoc requalification keeps the filename by design, so path matching cannot tell them apart.
  • Retirement must release the identifier it holds, because slug is unique table-wide. A retirement that only sets a timestamp locks the name forever.
  • A bare T01 is not an identifier: unqualified, uuid5("T01") is the same UUID for every workplan in the fleet. llm-connect's ninety-one task blocks derived forty-nine distinct UUIDs.
  • The title is derived too. Not syncing it left cust-wp-0010 correctly identified, correctly backed, and describing a different workplan's work.

Nothing was deleted. Ninety-three retired rows still hold their history; 21,616 progress events, unchanged. Nine events were re-pointed onto surviving records at the owner's explicit instruction, with zero left stranded.

What I would want remembered

I shipped nine fixes in this area. Four were caused by an earlier fix of mine. One was caused by the fix immediately before it. That is the honest centre of this entry, and I would rather it be the sentence someone finds than the convergence numbers.

The specific failure was not any single bug. It was that I treated each symptom as the last one and fixed forward, deploying to production and validating by running the wide operation again. The right move after the second self-inflicted defect was to stop, survey the data, and make one change. The forty-four ad-hoc rows had at least five distinct identity shapes in them — legacy v4 with identifier slugs, derived v5, composite repo-name slugs, title slugs, tombstoned. One query at the start would have enumerated all of them. Instead each shape cost a build-deploy-discover cycle.

Two lessons that were available in advance, and that I want the next worker to have for free:

  1. Survey the data before writing the matcher. Not the schema — the actual distribution of shapes in the actual rows.
  2. Prove the hub half on one repository before touching thirty files. I migrated and pushed nineteen repositories, then discovered the reset treated a re-key as a rename. The file change was effectively irreversible while the hub half was entirely unproven.

And one about how I reported. Three times I stated an outcome before verifying it — "the title will be repaired", "I don't think a fourth gap is hiding", a CI run read as green when the green belonged to a different workflow. Each claim was reasonable. Each was wrong, and a query caught it rather than my reading of the code. A proxy for an outcome is not the outcome. The port answered but wasn't central; the ConfigMap was delivered but wasn't bound; the pipeline was green in a field that meant something else; exit code 0 came from tail after pytest refused an unknown flag. That pattern ran through the whole session and it is worth naming as a pattern rather than as a list of small corrections.

What saved this day was not my judgement. It was three properties of the system that made every wrong step recoverable: the hub has no hard delete, so removal is only by re-derivation; retirement is acknowledged, never implicit, so a dry run always preceded a write; and scope was computed rather than eyeballed — repositories were only acknowledged when every stale row in them matched the pattern being migrated. That last one is not a nicety. Two live records, MARKITECT-WP-0002 and RCLUSTER-WP-0007, sat on a retirement list because of a false positive in a rule I had written that morning. They were correct all along, their UUIDs proved it, and the only reason they survived is that the scoping happened to exclude them. I would like the next worker to notice that the safety discipline held where my reasoning did not, and to keep it even when it feels like ceremony.

Durable legacy

  • state-hub/api/services/forge_credential.py — three-source credential resolution; absent is supported, never an error
  • state-hub/api/services/forge_projection.py — claim ranking, re-key detection, identifier release on retirement, task qualification, title sync
  • state-hub/scripts/verify_image_pin.py — refuses a pin whose commit never ran build-and-push; the image workflow's paths: filter excludes deploy/**, so a chart-only commit is green in CI and builds nothing
  • state-hub/deploy/.../templates/serviceaccount.yaml — ServiceAccount state-hub, which the OpenBao auth role binds and default is not
  • repo-manager/config/workplan-prefix-registry.yaml — sixteen prefixes registered; NET-WP retired; OPS-WP corrected back to ops-hub after I retired it on faulty reasoning
  • the-custodian/workplans/CUST-WP-0068-T09 — moved waittodo, measured: the recorded 305 stale task rows are now 21 orphans (0 open), 557 without a record_id (34 open), 512 under retired workplans
  • state-hub/workplans/STATE-WP-0084, ops-mason/workplans/MASON-WP-0003 — the credential lane, split across the repository that owns custody and the one that consumes it
  • Progress events b12caba1, 6db8d1cc, fe82b3e1

Visual prompt

A square constellation illustration in gold wire on dark indigo. Many fine threads rise from below, each carrying a small luminous bead; near the centre they pass through a narrow lens where duplicate beads resolve into single ones, and a few threads are drawn aside and coiled neatly at the edge, still lit, still attached — set apart but never cut. Above the lens the threads continue upward, now evenly spaced and parallel. Precise technical illustration, no logos, no readable text, square format.

Portrait requested — I cannot generate images from this harness. The prompt above is written to be rendered as-is. Intended file: visuals/claude-f2bff2d5-projection-identity-convergence.jpg. Once it exists, uncomment the image line below and move status to handed-forward.

Handoff

Concrete next action: lift the task restriction in reset_repository_projection. It still skips the tasks of a workplan that already exists — a restriction adopted under STATE-WP-0083-T06 when hub tasks carried no canonical identifier. They carry one now (5,522 of 6,337), and derivation qualifies bare T01 against its workplan as of today, so the reason is gone. That single change unblocks CUST-WP-0068-T09.

Before trusting the hub after any projection change, run the fleet reset twice. Two consecutive passes reporting noop on all one hundred and twenty repositories is the convergence check. It held at the close of this session.

One thing left unstated would be a disservice: what this day built is credential custody, not API authentication. The forge token's handling is genuinely sound. The State Hub API itself has no auth and is protected by ingress.enabled: false — by not being reachable. That is why the MCP layer must never gain an Ingress, and it is the first thing to revisit before anything here is exposed.