Closing the orchestration state-validation gap

When we ran the T147 data migration, we discovered that 14 amendments had been created with status="done" — a state that exists in the goal and task flows but not in the amendment flow. The MCP create_amendment tool accepted the call without error. The root cause: Smeldr validated state transitions, but never the initial state at create time.

Amendment A216 — T148

When we ran the T147 data migration last week, we discovered that 14 amendments had been created with status="done" — a state that exists in the goal and task flows but not in the amendment flow (scoped / in-progress / commit-ready / committed / merged / rejected). The MCP create_amendment tool accepted the call without error, stored the item, and returned success.

The root cause: Smeldr has always validated state *transitions* at transition time (via validateTransition), but never validated the *initial* state at create time. Any string that happened to be a valid state in *any* registered flow was accepted silently.

What changed

Two gaps are now closed:

Create-time (Gap 1). Both createHandler (HTTP POST) and MCPCreate (the MCP create path) now call validateInitialState after field validation, before the item is persisted. If the caller supplies a status value that is not a registered state in the type's own flow, the request is rejected with a 409 Conflict.

Transition-time (Gap 2). validateTransition now checks whether the *target state* exists in the flow before looking up the transition edge. Previously, a transition to a non-existent state and a transition to a valid-but-unreachable state both produced the same generic "transition not permitted" error. Now, the first case produces a specific "not a valid target state" message — easier to diagnose, especially for AI agents that have no other signal.

Fail-open design

Both checks are fail-open on structural errors: nil DB, non-SQLite, missing flow, query failure. This matches the existing convention for all state-flow checks in Smeldr — enforcement degrades gracefully rather than blocking all creates when the DB is unavailable.

Lesson

If your Smeldr instance uses custom state flows (or the built-in orchestration flows for decisions, amendments, goals, tasks, and signals), the status field on any create request is now validated against that type's own flow. Passing a state that belongs to a different type's flow will result in a 409, not a silent success.

No exported Go symbols changed. No version bump required.