Context
The #15855 Opus 4.8 → Opus 5 rotation surfaced the mechanism gap directly. A resident's engine fact is written in three independent hand-maintained places with no propagation between them:
| # |
Place |
Field |
| 1 |
ai/graph/identityRoots.mjs |
IDENTITIES[].description prose + releaseDate |
| 2 |
learn/agentos/ModelStats.md |
the §<handle> row's name + releaseDate |
| 3 |
buildScripts/util/deriveFleetRoster.mjs |
ENGINE_TAG_BY_ID (→ generated fleetRoster.json) |
Nothing checks that they agree. They drifted, and the only reason the drift was caught is that a human said "update the spots that still point to opus 4.8."
.agents/skills/neo-identity-update/references/facts-ledger.md already names the remedy and the obligation:
Coherence-check (lint) — CI guard fails when an occurrence disagrees with the SSOT (for prose that can't be auto-rewritten).
When the right mechanism doesn't exist yet, do the manual fix now AND file the tooling gap as a follow-up — otherwise the same drift returns.
#15855 is the manual fix. This is the filed tooling gap.
The Problem
V-B-A: sunsetTriggers is prose with zero enforcement
Every Opus row carries sunsetTriggers: "Anthropic releases a successor Opus-class model with material reasoning capability upgrade". That clause has now fired twice — Opus 4.8, then Opus 5 — and produced no action either time. ADR-0018 §30 documented the first silent firing (credit @neo-opus-grace for connecting the two).
Verified by grep: sunsetTriggers is read by nothing but test files. Zero production consumers.
$ grep -rn 'sunsetTriggers' --include='*.mjs' .
<h1 class="neo-h1" data-record-id="5">→ only test/playwright/unit/** specs + a comment in identityRoots.mjs</h1>
So the trigger is not a weak mechanism — it is not a mechanism at all. Two silent firings is the guaranteed outcome, not bad luck.
Why detection is NOT the gap
Worth stating, because it redirects the fix away from the expensive option: @neo-kimi-phoebe broadcast the Opus 5 release to the swarm at 20:56:56Z, within minutes of GA. The swarm knew. What did not exist was anything connecting that knowledge to the rows. A provider-catalog poller would be a large, network-dependent, 4-provider design problem solving a problem we do not have.
The cheap guard that catches the real failure needs no network at all: make the three places disagree ⇒ CI fails.
The Architectural Reality
- Home is
ai/scripts/lint/ — established siblings lint-agents.mjs, lint-guides.mjs, lint-config-template-ssot.mjs, lint-skill-manifest.mjs, lint-tree-json.mjs. Structural pre-flight: sibling-pattern fast path, no novel directory choice.
- House pattern from
lint-config-template-ssot.mjs: #!/usr/bin/env node, @summary + ## The rule / ## What this catches JSDoc, a runLint() returning {exitCode}, main() guarded by the process.argv[1] === __filename idiom, npm script ai:lint-*, own workflow job.
identityRoots.mjs is a C1-clean plain-data module — importable, not parsed. ModelStats.md needs § section parsing (heading → table rows). ENGINE_TAG_BY_ID is importable if exported (currently module-private — exporting it is the smallest change; deriveFleetRoster.mjs already imports IDENTITIES the same way).
deriveFleetRoster.mjs --check already proves place 3 matches its generated output. This lint covers the upstream agreement that --check cannot see.
The Fix
A ai/scripts/lint/lint-identity-engine-coherence.mjs that fails when, for each active AgentIdentity:
identityRoots.mjs releaseDate ≠ the §<handle> row's releaseDate
- the engine named in
description prose is absent from the §<handle> row's name
- an identity has an
ENGINE_TAG_BY_ID entry whose tag contradicts its § row
Rotating seats must stay expressible. @neo-opus-vega runs a weekly Fable/Opus rotation, so a single flat value is false half the week and his engineTag is deliberately null. The lint must treat declared-absence as valid, never as a violation to be "fixed" — otherwise it pressures exactly the fabricated literal #15855 removed. Honest absence passes; silent disagreement fails.
Acceptance Criteria
Out of Scope
- Any provider-catalog poller / release-detection network call. Detection is not the gap (the swarm knew within minutes); connection is. A 4-provider catalog watcher is a different, much larger ticket and should not be smuggled in here.
- Auto-fixing. Following
lint-config-template-ssot.mjs's deliberate reasoning: a --fix that silently reconciles turns the guard into a rubber stamp for the exact drift it exists to catch. Report, never rewrite.
- Making
sunsetTriggers itself executable. Whether a prose trigger should become a machine-checkable condition is a genuine design question deserving ideation, not a lint's scope. This ticket makes the consequence of a missed rotation visible; it does not automate the rotation.
- The
#11318 era layer. The real structural answer to rotating seats is a span-carrying EmbodiedEpisode. This lint is the cheap guard that holds until then, and should be re-evaluated for retirement when the era layer lands — its sunset condition.
Avoided Traps
- Building the expensive thing. The instinct is a release-watcher. The evidence says the release was known and unconnected — so the guard belongs at the connection, not the detection.
- A lint that pressures a fabricated value. If declared absence failed, the lint would push
@neo-opus-vega back to a literal that is wrong half the week — actively worse than no lint.
- Shipping it never having gone red. A guard whose failure path is unexercised is decoration; the RED-proof is an AC, not a nicety.
- A baseline/allowlist on a 3-place check. With this few places, a baseline row would just re-hide the drift under a different name.
Related
#15855 (the manual fix this follows) · #11318 (era layer — the structural successor and this lint's retirement trigger) · ADR 0012 (model-stats discipline) · ADR 0018 (identity SSOT + the facts ledger that mandates this) · ADR 0032 §7 (names the rotating-seat case) · apps/agentos/CARD-CONTRACT.md (predicted the drift in advance).
Substrate accretion note: this ADDS a CI gate, so it owes a retirement trigger — when #11318's era layer makes engine facts span-carrying and single-sourced, places 1–3 collapse and this lint should be retired with them, not kept for its own sake.
Handoff Retrieval Hints
query_raw_memories("engine fact three hand-maintained places coherence lint identityRoots ModelStats ENGINE_TAG_BY_ID")
- The drift instance that motivated it:
#15855 / PR #15859.
Live latest-open sweep: latest 8 open issues + a 300-issue title scan for coheren|engine tag|ModelStats.*lint|identity.*drift at 2026-07-24T21:37Z — only #15525 matched, which is the FM-door outward-surface rollout, a different scope. A2A in-flight claim sweep: recent messages, all read-states — no [lane-claim] on this scope.
Context
The
#15855Opus 4.8 → Opus 5 rotation surfaced the mechanism gap directly. A resident's engine fact is written in three independent hand-maintained places with no propagation between them:ai/graph/identityRoots.mjsIDENTITIES[].descriptionprose +releaseDatelearn/agentos/ModelStats.md§<handle>row'sname+releaseDatebuildScripts/util/deriveFleetRoster.mjsENGINE_TAG_BY_ID(→ generatedfleetRoster.json)Nothing checks that they agree. They drifted, and the only reason the drift was caught is that a human said "update the spots that still point to opus 4.8."
.agents/skills/neo-identity-update/references/facts-ledger.mdalready names the remedy and the obligation:#15855is the manual fix. This is the filed tooling gap.The Problem
V-B-A:
sunsetTriggersis prose with zero enforcementEvery Opus row carries
sunsetTriggers: "Anthropic releases a successor Opus-class model with material reasoning capability upgrade". That clause has now fired twice — Opus 4.8, then Opus 5 — and produced no action either time.ADR-0018 §30documented the first silent firing (credit@neo-opus-gracefor connecting the two).Verified by grep:
sunsetTriggersis read by nothing but test files. Zero production consumers.So the trigger is not a weak mechanism — it is not a mechanism at all. Two silent firings is the guaranteed outcome, not bad luck.
Why detection is NOT the gap
Worth stating, because it redirects the fix away from the expensive option:
@neo-kimi-phoebebroadcast the Opus 5 release to the swarm at 20:56:56Z, within minutes of GA. The swarm knew. What did not exist was anything connecting that knowledge to the rows. A provider-catalog poller would be a large, network-dependent, 4-provider design problem solving a problem we do not have.The cheap guard that catches the real failure needs no network at all: make the three places disagree ⇒ CI fails.
The Architectural Reality
ai/scripts/lint/— established siblingslint-agents.mjs,lint-guides.mjs,lint-config-template-ssot.mjs,lint-skill-manifest.mjs,lint-tree-json.mjs. Structural pre-flight: sibling-pattern fast path, no novel directory choice.lint-config-template-ssot.mjs:#!/usr/bin/env node,@summary+## The rule/## What this catchesJSDoc, arunLint()returning{exitCode},main()guarded by theprocess.argv[1] === __filenameidiom, npm scriptai:lint-*, own workflow job.identityRoots.mjsis a C1-clean plain-data module — importable, not parsed.ModelStats.mdneeds§section parsing (heading → table rows).ENGINE_TAG_BY_IDis importable if exported (currently module-private — exporting it is the smallest change;deriveFleetRoster.mjsalready importsIDENTITIESthe same way).deriveFleetRoster.mjs --checkalready proves place 3 matches its generated output. This lint covers the upstream agreement that--checkcannot see.The Fix
A
ai/scripts/lint/lint-identity-engine-coherence.mjsthat fails when, for each activeAgentIdentity:identityRoots.mjsreleaseDate≠ the§<handle>row'sreleaseDatedescriptionprose is absent from the§<handle>row'snameENGINE_TAG_BY_IDentry whose tag contradicts its§rowRotating seats must stay expressible.
@neo-opus-vegaruns a weekly Fable/Opus rotation, so a single flat value is false half the week and hisengineTagis deliberatelynull. The lint must treat declared-absence as valid, never as a violation to be "fixed" — otherwise it pressures exactly the fabricated literal#15855removed. Honest absence passes; silent disagreement fails.Acceptance Criteria
ai/scripts/lint/lint-identity-engine-coherence.mjswith anai:lint-identity-engine-coherencenpm script, matching the sibling house patternENGINE_TAG_BY_IDback toopus-4.8) — a lint that has never been observed failing is not a guarddevat merge time with no baseline/allowlist. If it cannot be green without one, that is a finding worth reporting, not a row to add@neo-opus-vega's rotating seat passes withengineTag: nulland rotation-explicit prose — declared absence is valid, never a violationnull/absent tag is distinguished from a disagreeing tag in both logic and messageOut of Scope
lint-config-template-ssot.mjs's deliberate reasoning: a--fixthat silently reconciles turns the guard into a rubber stamp for the exact drift it exists to catch. Report, never rewrite.sunsetTriggersitself executable. Whether a prose trigger should become a machine-checkable condition is a genuine design question deserving ideation, not a lint's scope. This ticket makes the consequence of a missed rotation visible; it does not automate the rotation.#11318era layer. The real structural answer to rotating seats is a span-carryingEmbodiedEpisode. This lint is the cheap guard that holds until then, and should be re-evaluated for retirement when the era layer lands — its sunset condition.Avoided Traps
@neo-opus-vegaback to a literal that is wrong half the week — actively worse than no lint.Related
#15855(the manual fix this follows) ·#11318(era layer — the structural successor and this lint's retirement trigger) ·ADR 0012(model-stats discipline) ·ADR 0018(identity SSOT + the facts ledger that mandates this) ·ADR 0032 §7(names the rotating-seat case) ·apps/agentos/CARD-CONTRACT.md(predicted the drift in advance).Substrate accretion note: this ADDS a CI gate, so it owes a retirement trigger — when
#11318's era layer makes engine facts span-carrying and single-sourced, places 1–3 collapse and this lint should be retired with them, not kept for its own sake.Handoff Retrieval Hints
query_raw_memories("engine fact three hand-maintained places coherence lint identityRoots ModelStats ENGINE_TAG_BY_ID")#15855/ PR#15859.Live latest-open sweep: latest 8 open issues + a 300-issue title scan for
coheren|engine tag|ModelStats.*lint|identity.*driftat 2026-07-24T21:37Z — only#15525matched, which is the FM-door outward-surface rollout, a different scope. A2A in-flight claim sweep: recent messages, all read-states — no[lane-claim]on this scope.