Retiring llm-connect's five legacy rows failed on `duplicate key value violates
unique constraint "tasks_pkey"`, and the cause is that a bare `T01` is not an
identifier: it is unique within its workplan, not in the fleet. Unqualified,
uuid5("T01") is the same UUID for every workplan in the fleet that has one —
llm-connect's 91 task blocks derive 49 distinct UUIDs, so creating its
workplans inserts the same task primary key repeatedly in one flush.
Tasks are now qualified with their owning workplan before derivation, which is
the rule `task_record_id_backfill.qualify_task_id` already applies to stored
ids; the two must agree or the backfill and the projection disagree about what
a task is called. Already-qualified ids are untouched.
The create path's comment claimed it was "safe only because nothing exists to
mis-match against: this workplan is new to the hub". That was true of other
workplans and false within one: the collision was among the tasks it was
inserting itself.
The failed pass rolled back cleanly — llm-connect's five legacy rows are still
live and progress events are intact.
742 pass.
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
Two rows were being proposed for retirement that were correct all along.
`testdrive-jsui-publication` and `three-phoenix-ha-cluster` carry legacy title
slugs, but their UUIDs are exactly uuid5 of MARKITECT-WP-0002 and
RCLUSTER-WP-0007 — they are those records. `_identity_is_derived` short-circuits
on the UUID *version* before the slug rule ever runs, so both read as re-keys.
Acknowledging retirements for either repository would have retired a live
record on the strength of its slug.
The derived UUID is the strongest key available and was not used in matching at
all. It is now checked first: if a row's id equals uuid5 of a wanted
identifier, that row is that record, whatever its slug says. The slug and path
heuristics now only decide rows the UUID cannot identify.
This also corrects a claim I made from the retirement list alone: those files
derive normally. Nothing was wrong with them.
738 pass.
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 previous commit made retirement stamp the slug, but `stale` excludes rows
already retired — so nothing revisits them and the stamp never reaches the 32
rows retired earlier today: core-hub's two REPO-WP rows and the 30 ad-hoc
records. Each still holds an identifier no file derives, blocking whichever
repository legitimately owns it.
The reset now stamps any retired row whose identifier is still unreleased,
using that row's own retirement timestamp rather than today's, so the record
keeps saying when it was retired. Completing a decision already taken is not a
new retirement, so this runs without acknowledgement and is reported as
`released`, separately from `retired`.
Repairing it in the reset rather than by hand keeps the hub a read model and
makes the fix reproducible for any row that reaches this state again.
735 pass.
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
`slug` is unique across the whole table, so retiring a row by timestamp alone
leaves its identifier locked to a record nothing derives any more — and the
repository that legitimately owns it can never claim it. core-hub's inherited
REPO-WP-0001/0002 rows retired cleanly and repo-seed stayed refused with
"slug already belongs to another repository", pointing at two retired rows.
Retirement now stamps the slug `<slug>@retired-YYYYMMDD`. The row, its UUID and
its progress events are untouched, so history stays attached to the record it
happened under; only the human-facing identifier is freed. Re-retiring an
already-stamped row replaces the mark rather than stacking it, or the 100-char
column overflows after a few passes.
The UUIDs never collided here: core-hub's rows predate derived identity and
carry v4 UUIDs, while repo-seed derives v5 ones. The identifier was the only
thing in the way.
733 pass.
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 previous commit tested UUID version as a proxy for "identity is derived".
It covers 19 of the 44 ad-hoc rows; the other 25 are legacy v4, so they kept
path matching and would have been updated in place — slug still
`adhoc-2026-07-02` while the file says `ADAPTIVE-WP-ADHOC-2026-07-02`. The
divergence would have persisted, and silently, for most of the records this
was meant to fix.
The distinction that actually matters is whether the row's slug is a
work-record identifier. A row whose slug is an identifier claims to be that
record, so a different derived identifier for the same file means two records.
A row whose slug is a title — `three-phoenix-ha-cluster` for RCLUSTER-WP-0007 —
never claimed one: those are hub-first rows from before ADR-001 where the
backing path is the only link there has ever been, and path matching is all
that holds them together.
730 pass.
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 ad-hoc requalification changed 30 identifiers while deliberately keeping
the filenames, per canon. The matcher's path fallback therefore matched every
old row to its new record and updated it in place — so the files said
CUST-WP-ADHOC-2026-08-25 while the hub still said adhoc-2026-08-25, with the
row's UUID still encoding the old identifier. Exactly the file/hub identity
divergence ADR-007 exists to prevent.
Path matching cannot distinguish a re-key from a rename when the path does not
change. Identity can: a derived row is UUIDv5 over the record id, so a changed
id is a different record — the old row retires and the new one is created.
Rows predating derived identity carry v4 UUIDs, where the identifier is a label
rather than the identity; those keep path matching, so rename detection still
works where it is the right answer.
The unmatched old row keeps its own slug as key, so it lands in `stale` and
becomes a retirement candidate rather than disappearing from the outcome.
720 pass.
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
Nine repositories are invisible to derivation because central may not read
them. This adds the consuming half of the credential lane MASON-WP-0003 built.
The cluster has no agent injector and no secrets-store CSI driver, so the pod
authenticates to OpenBao with a projected ServiceAccount token (audience
`openbao`, not the API server) and reads the KV path itself. `forgeRead.*`
carries coordinates only; no credential is a chart value, an image layer, or a
Kubernetes Secret.
The credential reaches git through GIT_CONFIG_* setting http.extraHeader, not
through `-c` and not through userinfo in the clone URL — both of those put the
token in the process listing. It is redacted from ForgeDeriveError, which is
logged, stored in reset outcomes, and returned over the API.
Absent stays a supported state: with no credential, or with OpenBao
unreachable, resolution returns None and public derivation runs unchanged.
Raising would turn "nine repositories are unreadable" into "the pass failed",
which is what T01 exists to prevent.
Chart default is disabled. 717 pass.
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
A private repository failed derivation the same way a broken one did, so
"cannot read" and "does not exist" were indistinguishable from outside.
They authorise opposite things: only the second can justify retiring a
record.
- ForgeUnreadableError (a ForgeDeriveError, so old callers still catch it)
for permission-shaped clone failures, including Forgejo's 404 for an
unauthenticated private repo — indistinguishable here, and the safe
reading of an ambiguous answer cannot destroy a record.
- GIT_TERMINAL_PROMPT=0: an unattended pass must fail, not block on a
username prompt. Failing is what makes the case observable.
- DerivedProjection.retirement_eligible separates "no records found" from
"no records exist". A checkout with no workplans/ directory cannot
evidence an absence — the empty-clone path that would have proposed
every record in a repository for retirement.
- Retirement from an ineligible source is refused even when acknowledged.
- Fleet keeps unreadable out of the error bucket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 3377672@bnt-lap001
Assistant-Session: 15463ccf-238f-4e13-b163-93aa25c6d166
The identifier refusal checked id only. slug carries its own unique constraint
across the whole table, so two repositories can derive different identifiers
whose slugs still collide — which left disaster-control raising IntegrityError.
First fleet-wide pass over 121 repositories: 91 applied (737 updated, 8
created), 16 refused covering 64 records, 12 errored. 745 workplans now carry
the commit they derived from, satisfying ADR-012 decision 2 for the first time.
64 is the measured size of the stale-row problem CUST-WP-0068-T09 has waited on.
Eleven of the twelve errors are private repositories the pod cannot clone
anonymously — a real limit on "the forge is the projection source", since their
absence currently looks like an error rather than a policy.
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
ADR-012 decision 7 requires the fleet form to share the per-repository
implementation: the rarely-run wide operation must be the frequently-run narrow
one, or the wide one is trusted on the strength of never having been exercised.
Failure behaviour is the substance. A refusal does not stop the pass — aborting
on the first refusal means one unresolved repository blocks reconstruction
everywhere, which in practice means permanently. An error does not stop it
either. Each repository gets its own session so one failure cannot roll back
another's work, and only repositories that applied are committed.
Refs STATE-WP-0083-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 reset raised IntegrityError on net-kingdom: its ADHOC-2026-08-23 derives to
an identifier another repository already holds — the collision CUST-WP-0066
documents, where two repositories created the same daily identifier on the same
day. Derivation is deterministic, so the clash is real rather than incidental.
It now checks, before creating anything, whether a derived identifier belongs to
another repository, and refuses naming both the record and the holder. A refusal
is something the caller can rule on; a constraint violation is a stack trace.
Acknowledging retirements deliberately does not authorise a collision. Those are
different decisions — one says the work is gone, the other says take an
identifier another repository owns — and conflating them would let a routine
acknowledgement smuggle an identity change through.
Refs STATE-WP-0083-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
Every work-record type carried a stable identifier except tasks, whose rows held
only id, workplan_id, title, status and priority — nothing connecting a row to
CUST-WP-0067-T01 in the file it came from. Matching was therefore by title, so a
renamed heading looked like one task vanishing and another appearing, and the
forge-derived reset had to refuse to touch tasks at all.
Adds tasks.record_id (nullable: no migration can invent an identity for an
existing row) and a backfill that reads the pairing from the repository files,
where a task declares both its canonical id and its projection UUID. 5516 pairs
across 121 repositories with zero conflicts; 4456 of 6073 cache task rows
identified.
Diff and reset now key on record_id where present, falling back to a
title-prefixed key so an unidentified row stays visibly unidentified.
Unknown stays unknown: a row the files do not claim keeps no identity and the
reset keeps refusing to act on it, and an existing identity is never
overwritten — a mismatch is recorded as a conflict rather than resolved.
Refs STATE-WP-0083-T06
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 as amended (STATE-WP-0083-T03). Creates what the
forge has and the hub lacks, updates what differs, retires what no longer
derives. It never deletes: hub-native records reference workplans with ON DELETE
RESTRICT, and destroying a progress event to tidy a derived projection would
lose hub-native truth to fix a derived-state problem.
Retirement is refused by default. A record that stops deriving may mean a
deliberately deleted file or a caller pointed at the wrong branch; only the
caller can say which.
Verified against live data and rolled back: whitehat-security applied 5 updates
with no retirements; the-custodian refused, naming the four hub-first records
confirmed by hand to have no backing file.
Tasks of existing workplans are deliberately untouched — hub tasks carry no
canonical identifier, so matching is by title and a renamed heading would
destroy and recreate a record. Tasks are created only alongside a new workplan,
where nothing exists to mis-match. Tracked as T06.
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