get_task("T203") now works: teaching MCP tools a second identifier

get_task("T203") now works. Teaching MCP tools to resolve a human-facing ID, not just a slug, and the two bugs that surfaced on the way.

Every orchestration record in Smeldr, Task, Goal, Decision, Amendment, carries a human-facing identifier: TaskID, GoalID, DecisionNumber, AmendmentNumber. It's the name every conversation about the work actually uses ("check on T203," not "check on the item whose slug is t203-taskid-has-no-lookup-path"). Until now, every MCP tool that resolves one of these items, get_task, update_task, transition_item, publish/archive/ delete_task, only ever matched against the slug. Handed "T203," a caller had exactly one path forward: list everything and scan for a match by hand.

The shape of the fix

Every Repository[T] in Smeldr already exposes FindByID and FindBySlug. Adding a third, FindByColumn, sounds like it should just be a new required method, except Repository[T] is exported, and any custom repo implementation out there would break the moment we add a method it doesn't have.

The codebase already had an answer to this, just not one applied here yet: SeqRepository[T], an *optional* extension of Repository[T], type-asserted by callers rather than required by the interface. New ColumnLookupRepository[T] follows the same shape. SQLRepo[T] implements it with one parameterized query. MemoryRepo[T] implements it with reflection, and here's the part worth naming: it doesn't need a hand-maintained table mapping "task_id" to the TaskID field. SQLRepo already has a columnForField helper that walks a type's own db struct tags to answer "what column does this Go field map to?" MemoryRepo's own FindByColumn just asks that question in reverse, given a column name, which field does it come from, reusing the exact same tag-driven cache. No new per-type table, no field-name guessing.

A second, deliberately small map (humanIDColumns, four entries) answers a different question entirely: *which* types even have a human-facing identifier worth falling back to. That one stays explicit, not derived. A future field that happens to be named ...Number should never silently become a lookup key nobody intended it to be.

Two paths, not one

Resolving a Task by slug and resolving a Task by TaskID turned out to touch two genuinely separate code paths in this codebase. Module[T]'s six MCP methods (MCPGet, MCPUpdate, MCPPublish, MCPSchedule, MCPArchive, MCPDelete) all go through the module's own repository. transition_item doesn't: App.TransitionItem resolves compiled types with a raw SQL query against whichever table resolveItemTable finds, entirely outside Module[T]. Both got the identical fallback logic, expressed twice, once per shape. Four columns didn't justify inventing a shared abstraction to unify two paths that were already independent before this fix.

What almost shipped broken

Writing the fallback surfaced two bugs that had nothing to do with the original ask. MCPUpdate restores an item's identity fields after applying an update, including writing the caller's own slug argument back onto the item, to stop a caller overwriting it by accident. That code assumed the argument it received *was* the slug. Once resolution can also succeed via TaskID, that assumption breaks: resolving "T203" and then writing "T203" into the Slug field would have silently corrupted the real slug on the very first update. MCPPublish's slug-collision check had the same shape, checking the caller's raw identifier instead of the item's own real one.

Both were caught before shipping by asking the same question of every call site touched: does anything downstream assume this parameter *is* the slug, now that it might not be? The fix in both cases was the same one line: read the resolved item's own slug back out, never trust the argument that found it.

What this doesn't change

Every non-orchestration content type resolves exactly as it always has: no humanIDColumns entry means the fallback never triggers, and a slug miss stays a slug miss. HTTP routes stay slug-only too, deliberately: a URL path segment is a slug by REST convention, and a caller who already has a URL already has the slug they need. This is purely an MCP-side convenience, for the one surface where a caller is far more likely to be holding a human-typed identifier than a generated one.