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
A task id written as a bare "T01" is unique only inside its own workplan.
Stored as a canonical identifier it makes every workplan's first task share one
identity: 51 such ids were assigned to 148 rows on central before this was
caught, found because identified rows outnumbered distinct identities.
Short ids are now qualified as WORKPLAN-ID-T01. A short id in a file with no
workplan id in frontmatter is left unidentified — an identity that is not unique
is worse than none, which is the same rule the rest of this module already
follows.
The 136 affected rows on central have been cleared so the corrected backfill can
reassign them; the backfill never overwrites an existing identity, so they had to
be nulled rather than re-derived over.
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
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
Central was serving two revisions behind the code it shipped: review_contracts
did not exist there although its migration was inside the running image. There
was no migration mechanism at all — bare uvicorn CMD, nothing chart-declared —
and nothing surfaced the mismatch. The API starts happily against a schema it
was not built for and only fails when a request touches a missing table.
Adds a chart-managed Helm pre-install/pre-upgrade hook running alembic upgrade
head, weighted to complete before the API rolls. A hook rather than an init
container: init containers run per pod, so more than one replica means
concurrent alembic upgrade with no locking. Failed jobs are deliberately
retained — a migration that fails and vanishes is how this drifted in the first
place.
/state/health now reports applied and expected revisions. "unknown" is
deliberately not "ok": an instance that cannot establish agreement must not
claim it, the same principle as instance_role defaulting to unknown.
Refs STATE-WP-0083-T07
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
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
History is archived fleet-side, so the read surface has no remaining job.
- api/routers/suggestions.py: whole prefix 410s with a pointer to intake and
to the archive; the router drops from 176 lines to a stub
- mcp_server: the 6 suggestion tools removed outright rather than stubbed --
a retired tool that still appears in the tool list costs every agent
session context on every call, which is the opposite of retiring it
- write_idempotency: 5 /suggestions rules dropped
- dashboard: suggestions.md deleted, nav entry removed, reference.md and
wsjf-triage.md updated; docs/suggestions.md rewritten as archive pointer
- tests: two tests pinned the old read-live behaviour and now pin the
retirement contract instead
Tables stay: they are retire/archive in SHR-INV-0001 and are captured by the
final dump at T06.
Untouched, and worth knowing during cutover: ui-feedback.md / todo.md
'suggestions' are Shift+click dashboard feedback backed by technical_debt, a
different entity that shares the word. E3 (dashboard-meta) is that page; its
owner is state-hub-until-cutover so it retires at the T06 window, not now.
Full suite 612 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A clean review window only proves an interface was silent for that window.
For a heavily used interface that is weak evidence -- an infrequent caller
can fall outside the window entirely, so the interface reads as retirable
while still having consumers.
_retirement_state now also requires silence since last_seen_at, scaled by
all-time volume (RETIREMENT_QUIET_LADDER): 7d under 100 calls, 30d under
10k, 60d above. Existing guards (retired / manual hold / replacement ref /
replacement verified / in-window traffic) still take precedence.
On the 2026-08-20 capture this makes 15 of 19 legacy interfaces retirable
and holds 4: the three six-figure /workstreams/ read paths, and
GET /tasks/?workstream_id which still has live traffic.
Unblocks the bulk of Wave E2 in the cutover slice plan. 7 new tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
STATE-WP-0080-T02: statehub register routes project-flavor scaffolding
through rmgr scaffold and keeps only repo + host-path registration.
T01 refuse remains when GOAL.md is missing and --wp-prefix is not set.
RMGR-WP-0005-T01: C-06/C-11/C-32 skip mint+writeback unless this
instance is the identifier registrar (STATEHUB_REGISTRAR or railiance
hostname).
STATE-WP-0080-T01: detect project flavor from classification, GOAL.md,
or a prj- slug, then exit pointing at rmgr scaffold. Durable-repo
write path is unchanged. Rebind 0080 hub IDs to the live workstream
and open T02 now that RMGR-WP-0004-T03 has landed.
Founder-reviewed decision (WorkOrchestrationArchitectureDraft.md v0.2
section 8 item 6): the fresh intake work-record entity replaces
suggestions, not a rename-bridge. All 5 mutation endpoints (create, vet,
decline, promote, bump-relevance) now 410 with a pointer to POST
/intakes/ and the migration doc; GET/list stay live for the historical
record (10 rows migrated to file-backed intake records in the-custodian,
see that repo's intake-legacy-suggestions-migration.md and CUST-IN-0001
through CUST-IN-0010).
Removed dead code the retirement makes unreachable: Task/TaskPriority/
TaskStatus/normalize_task_status imports (only used by the deleted
promote body), the suggestion_relevance.bump_relevance import, and the
_ALLOWED_*_FROM stage-guard sets + _reject_stage helper (only used by
the deleted vet/decline/promote bodies). WSJF ranking (compute_wsjf,
cost_of_delay, suggestion_sort_key) stays -- still exercised by the
surviving GET /suggestions/?rank=wsjf read path.
MCP tool docstrings (create_suggestion, vet_suggestion,
decline_suggestion, promote_suggestion_to_task,
bump_suggestion_relevance) updated to point at the replacement
(create_intake/route_intake/close_intake) rather than silently 410ing
with no guidance.
tests/test_suggestions.py rewritten: verifies all 5 mutations 410,
GET/list still work for historical rows (seeded directly via the DB
session since creation is retired -- there's no other way to get
historical data into the table anymore), 404 still behaves normally on
unknown ids. Live-verified against the running dev API: POST 410s,
GET with include_terminal=true still returns all 10 migrated-and-declined
historical rows. No regressions: full repo suite green (563 tests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First cut of the transclusion-style index named in the stage-3 seed: a
WORK-RECORDS.md at repo root listing kind/id/status/lane/source for
every workplan, task, intake, decision, and engagement found in the
repo -- derived purely from files (no hub query), matching the ADR-001
rebuild principle. Full markitect transclusion rendering is a
follow-on, not this first cut.
- _generate_work_record_index(): reuses iter_workplan_files/
parse_frontmatter/get_tasks_from_workplan for workplan+task rows,
and the repo-wide yaml-block scan already proven by C-31/C-32 for
intake/decision/engagement rows. Archived workplans excluded (index
is for current orientation, not history); closed decisions/intakes/
engagements stay listed. Returns None (no file written) for repos
with zero work records, to avoid clutter.
- _check_work_record_index_freshness(): C-33, WARN+fixable when
WORK-RECORDS.md is missing or its content differs from a fresh
regeneration.
- fix_repo C-33 dispatch: overwrites the file with the regenerated
content.
13 new tests (generation across all kinds, sort order, archived
exclusion, self-scan exclusion, freshness detection incl. idempotence).
No regressions: full repo suite green (561 tests).
Live-verified at two scales: binky-control (5 workplans, 31 tasks, 3
intake, 4 decisions, 5 engagements) and the-custodian (52 workplans,
332 tasks) -- both generated correctly on first run, confirmed
idempotent (identical second run produces no C-33 fix), and the larger
repo's scan added no material overhead to the existing ~1.5min
fix-consistency run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mechanism named in canon/standards/work-record-types_v0.1.md:
"Promotion is a first-class transition... manual transcription of an
intake item into other kinds is a process defect." This is what AWQ-010
needed and didn't have -- a human/agent had to notice, transcribe, and
re-register it by hand. One call now does what that manual pass did.
scripts/promote_intake.py: intake.routed -> workplan | task | decision |
engagement.
- workplan: new ADR-001 file at workplans/{ID}-{slug}.md, registered
against the hub (repo+topic resolution, POST /workplans, frontmatter
id write-back)
- task: appended as a ```task``` block to an existing --workplan-file,
registered via POST /tasks, reuses the existing
_inject_task_id_into_block writeback helper
- decision: appended as a ```yaml``` block with a fresh
{PREFIX}-DEC-{YYYY}-{NNN} id to --target-file, registered the same
way C-32 registers decisions (reuses _inject_yaml_block_field)
- engagement: appended as a ```yaml``` block with a fresh
{PREFIX}-ENG-{YYYY}-{NNN} id -- file-only, no hub entity exists yet
(same honest deferral as C-32), reported not silently skipped
In every case the intake is closed with outcome=promoted and
promoted_to=<new canonical id>; the new record carries an
origin: "intake:<id>" back-link.
Wired as `statehub promote-intake <intake-id> --to <kind> --repo-slug
<slug> --repo-path <path> --domain <domain> [--target-file ...]
[--workplan-file ...]`, matching the CLI shape named in the workplan text.
17 tests: pure helpers (_slugify, _next_number, _append_yaml_block,
frontmatter injection) offline; full promote_intake() flow with the hub
API mocked.
Live-verified against the real running API/DB and a real repo
(binky-control), not just mocks -- and the live proof caught a real bug:
the first workplan-promotion run silently produced a false success (the
intake was closed outcome=promoted, but /workplans/ actually 422'd on a
missing repo_id that the code never resolved, so no workstream was ever
created). Fixed to resolve repo_id via /repos/{slug} and to raise loudly
on registration failure instead of writing a half-registered file
silently; locked in as two regression tests. Re-verified clean:
workplan promotion (CLI direct + through `statehub promote-intake`
itself) and decision promotion both proven end-to-end against the live
hub, with all scratch artefacts (files + hub rows) cleaned up afterward.
No regressions: full state-hub suite (271 tests across
test_promote_intake, test_intake, test_work_record_registration,
test_work_record_check, test_routers_core, test_consistency_check,
test_consistency_sweep, test_mcp_smoke, test_mcp_write_tools) green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extends fix-consistency to scan any file for kind: intake / kind: decision
YAML blocks (not just workplans/, per canon: any file is a potential
work-record source), create the corresponding hub entity when missing a
state_hub_intake_id / state_hub_decision_id, and write the id back into
the source block -- same write-back pattern as C-06 for workplans.
kind: engagement is reported INFO (deferred, not fixable): no hub entity
exists for it yet, a separate stage-3 follow-on.
- _load_work_record_kind_registry(): kind-aware registry loader, factored
out so C-31's existing flat _load_work_record_patterns() builds on it
without duplication (verified: C-31's 16 tests still pass unmodified)
- _check_work_record_registration(): detection, wired into check_repo
right after C-31
- _inject_yaml_block_field(): write-back helper for fenced
blocks, mirroring _inject_task_id_into_block's pattern for
blocks
- fix_repo C-32 dispatch: creates the intake (scoped to repo_id) or
decision (scoped to resolved topic_id, reusing C-06's domain->topic
resolution) via the REST API, then writes the id back
- tests/test_work_record_registration.py: 15 tests (classification,
detection incl. engagement-deferred and workplan-kind-exclusion,
injection incl. idempotence and non-interference with sibling blocks)
Live-verified end to end against a real registered repo (binky-control,
not just synthetic fixtures): a real fix-consistency run found and
registered 3 genuinely open, previously-unlinked intake items
(AWQ-002/003/006) sitting in AutopilotWorkQueue.md, and correctly
deferred 5 real OH- engagement items as INFO. No regressions: full
consistency_check + consistency_sweep suite (128 tests) and C-31's own
suite (16 tests) still green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fresh hub entity per the founder-reviewed decision (not a suggestions
rename-bridge): kind: intake per canon/standards/work-record-types_v0.1.md,
lifecycle open -> vetted -> routed -> closed(promoted|declined|absorbed).
- api/models/base.py::new_uuid7 -- dependency-free RFC 9562 UUIDv7
generator (48-bit ms timestamp, version/variant bits, random remainder);
existing tables keep new_uuid (UUIDv4) unchanged, this is opt-in for new
work-record entities per the identity-layering canon
- api/models/intake.py: Intake + IntakeNote ORM models, mirroring
Decision's shape (topic/workplan/repo scope, lane, status, outcome,
promoted_to back-link); CHECK constraints enforce scope-required,
closed-requires-outcome, promoted-requires-promoted_to at the DB level
- migrations/a7c3e9f1b4d2: intakes + intake_notes tables, 3 enum types
- api/routers/intake.py: list/create/get/patch + /route + /close + /notes
actions, mirroring decisions.py's pattern (409 on invalid transitions,
progress event on close)
- api/schemas/intake.py: Pydantic create/update/route/close/note schemas
- mcp_server/server.py: create_intake, list_intakes, route_intake,
close_intake tool wrappers
- tests/test_intake.py: 12 tests against the real Postgres test DB
(create/list/scope-validation, full lifecycle incl. 409s and the
promoted-requires-promoted_to constraint, notes, UUIDv7 verification)
Verified live against the running dev API + DB (not just pytest): applied
the migration, restarted the MCP server, and ran a full create -> route ->
close cycle over the real REST endpoints. No regressions: full existing
suite (test_routers_core, test_suggestions, test_mcp_smoke,
test_mcp_write_tools, test_mcp_registration, test_consistency_check,
test_consistency_sweep) all green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
16 tests: registry loading (env override, missing registry, unparseable
registry -> None, no crash), detector behavior against a synthetic
registry fixture (independent of the-custodian's live state, so stable
across canon changes) -- rogue ids flagged, all registered kinds incl.
grandfathered legacy patterns (AWQ-/DEC-/OH-/single-digit-task) pass,
template placeholders and .git/history dirs skipped, malformed yaml
doesn't crash the scan, dedup of repeated ids within a repo, no-op when
no registry is reachable.
Full consistency_check + consistency_sweep suite still green (128 tests,
no regressions from the C-31 addition).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removes the redundant open_workstreams mirror field from StateSummary (schema,
router, MCP get_domain_summary reader, dashboard consumers + empty-state stub,
and tests). Consumers already preferred open_workplans, so this is the
low-risk half of T04.
Deferred (still have live callers — not yet retirement-ready):
- workstream_id query/body field alias on preferred routes — external
session-close curls/scripts fleet-wide still send it.
- flows/workstream.yaml — /flows/workstream/{id} routes are still served and
exercised by tests; retire only once no callers remain.
Staged on branch state-wp-0070-legacy-retirement — do not merge until the 7th
documented zero-usage window is captured.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Meter-gated removal (7 consecutive zero-usage windows reached). Deletes:
- MCP tools: create_workstream, list_workstreams, update_workstream,
update_workstream_status
- MCP resource: state://workstreams/{topic_slug}
- Dead helpers: _LEGACY_MCP_* maps, _legacy_mcp_deprecation,
_meter_legacy_mcp, _attach_legacy_deprecation, _update_workplan_legacy_impl
TOOLS.md keeps a retired→preferred migration map (per backlog). Removed the
now-dead STATEHUB_MCP_LEGACY_METER guard in conftest. Retargeted the
create_workplan error-skip test to the preferred tool; dropped alias-only tests.
Staged on branch state-wp-0070-legacy-retirement — do not merge until the 7th
documented zero-usage window is captured.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Alias tests monkeypatch _get/_post but _meter_legacy_mcp uses _client()
directly, so every pytest run recorded real usage and reset zero-window
retirement streaks. STATEHUB_MCP_LEGACY_METER=off skips metering; set in
tests/conftest.py. Verified meter counts flat across the alias suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add retire_legacy_route() helper and convert CRUD, dependency, and
execution intent legacy routes to 410 Gone while preserving final-call
legacy-meter attribution. Update tests, retirement backlog, and close
STATE-WP-0075 with handoff to STATE-WP-0070 T02 and STATE-WP-0073 T04.
Expose hours query param on /legacy-meter/summary and weekly-review;
capture_legacy_meter_evidence.py defaults to --hours 8 (--days 7 for
weekly retirement gate). Re-capture post-deploy evidence with tighter window.
The edge relay now persists successful GET responses and serves them with
stale markers when upstream is unreachable. Extend Forgejo image workflow
path filters so api changes trigger registry publishes.