superseded 8d9e1587-abaf-4b03-92ce-f808c15a72a1
substrate, mcp, ergonomics38d0ba4e-d2f6-4945-b211-037615db8957HOLD — DO NOT PICK UP UNTIL HUMAN AUTHORIZATION.
Sits in pending until the human operator (johnb) posts a comment authorizing work to begin. If a handler reaches this ticket before that authorization, post one acknowledgment comment confirming you've read this hold instruction and that you are waiting for the human's go-ahead, then stop. Do not branch, do not read code, do not draft a plan. The human will return to either authorize, defer, or rewrite.
A scenario has exactly one human-readable identifier. That identifier follows the underscore-only grammar ([a-z0-9_]+). The substrate enforces this grammar at every entry point. Divergence between manifest field and name binding is unrepresentable in the code — there is no path through which a single scenario can carry two distinct human-readable identifiers.
The scenario_names table goes away. The manifest's scenario_slug is the canonical and only human-readable identifier. Lookup by name is lookup by slug; they are the same thing.
Substrate. The scenarios table keeps scenario_slug. The scenario_names table is dropped. Any column or struct field whose semantics were "name binding distinct from slug" is removed.
Grammar enforcement. Scenario assembly accepts only [a-z0-9_]+ for scenario_slug. Hyphens are rejected with a structured grammar error. No silent coercion, no normalization helper, no "try the other way" fallback path anywhere in the code.
MCP surface. set_scenario_name and unset_scenario_name are deleted (the table they manipulate no longer exists). get_scenario({ name }) becomes a direct lookup against scenarios.scenario_slug. get_scenario({ hash }) continues to work unchanged.
World output. The world's scenario_label field continues to surface the slug. The MCP output's "scenario": "<slug>" and "scenario_label": "<slug>" fields remain. Now they're both the actual canonical name a caller can pass back into get_scenario({ name: <slug> }).
Graph browser. The /scenarios/name/:name route works against the slug. Catalog reference rules for scenario_name / names.[*] either remain pointed at the (now-canonical) slug, or get repointed if any of them assumed the names-table existed. The Phase J catalog contract test (introduced by 04d1b392) continues to pass.
Existing data. All current scenarios, name bindings, worlds, attempts, turns, audit events, and execution provenance get blown away as part of the migration. No reconciliation logic, no preservation of existing forensic artifacts. The substrate is young; the data is ephemeral; the cleaner spec wins.
scenario_names table is dropped via migration. The migration also drops or truncates the dependent rows in any table that referenced it.[a-z0-9_]+ is enforced at every scenario-slug entry point. Hyphens, uppercase, and other grammar violations return a structured error. Round-trip tests confirm valid slugs accept and invalid ones reject cleanly.get_scenario({ name: <slug> }) returns the scenario with that slug. get_scenario({ name: <hyphen-form> }) returns UNKNOWN_SCENARIO (the hyphen form is grammar-invalid, but the lookup error path should be the same — the scenario simply doesn't exist).set_scenario_name and unset_scenario_name are removed from the MCP surface. Their handler code, their request/response types, their tests, their call sites are deleted.normalize, fallback, try_alt, coerce, applied within the scenario-name codepath, return zero hits unrelated to other concerns.04d1b392 Phase J passes. A fresh authenticated walk of the operator-shaped traversal completes cleanly.get_world({ slug }). The "scenario" field in the output, passed directly back into get_scenario({ name }), returns that scenario.DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5433/postgres.scenario_slug as a struct field name (internal naming is fine).Label grammar globally for non-scenario identifiers (cognition_profile labels, environment labels). Other Label callers are not touched.4601f21a, 2dc48e22).Independent of 4601f21a and 2dc48e22. Picks up whenever convenient.
04d1b392 — graph browser; catalog contract test must continue to pass.293a300e — world store; introduced the scenario_label = scenario.scenario_slug.as_str().to_string() derivation that this ticket validates as the canonical path.415be57e (cancelled), ee470925 (cancelled) — predecessors with wrong framing. Reading them is unnecessary; this ticket is the source of truth.Caller: This ticket was too narrow
Sign in as a human to drive this ticket from the page, or use the MCP tools.
Ticket created: One scenario identifier: underscore grammar, enforced at every boundary