Commit Graph
1 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 5 a33dbdcdc3 feat(missions): W1/#13 — let a model author the mission's phases
The last unstarted item from the missions-as-workflows plan, and the other half
of Slice 5: that one lets a model size the TEAM, this lets it decide what the
work IS.

Every mission's phases come from one of five hand-written recipes in
`templates/workflows/*.toml`, chosen by `template_kind` before anyone saw the
mission. That is the "do it this way: 1, 2, 3" over-specification that makes a
capable model follow a worse plan than it would have chosen. The recipes stay —
they are still the default for a mission nobody proposes a plan for, and the
fallback when a proposal is refused.

Same three verbs and the same review gate as the roster, deliberately: propose
and decide are separate because only the second changes a mission, and a second
shape would be a second thing to get right. Approving REPLACES the phases (a
plan is an answer to "what is this mission", not an addition to one), draft-only.

GROUNDED IN WHAT THE PLATFORM ACTUALLY READS, which is the part that makes this
more than a copy. `phase_config::KNOWN_KEYS` already names every phase-config key
and the code that reads it — the registry built after `task` sat unread through
every mission. A plan is validated against it, so a model cannot propose a phase
whose settings nothing will act on: the failure that registry exists to EXPOSE is
one this path cannot create. Phase kinds are checked the same way, because an
unknown kind does not error — it falls through to the catch-all purpose and runs
as a generic phase that looks like it worked.

TWO THINGS THE WORK ITSELF FOUND, both the same shape:

  - `done_when_check` — the stop-gate key added earlier today — was never
    registered in `phase_config`, so every mission that set it has been logging
    it as an unknown key. Found by a test written for a different purpose, which
    is the registry doing exactly its job. Now registered with its reader.
  - `done_when` and `max_iterations` are COLUMNS promoted out of config by
    `missions::create`; the evaluator sweep filters on the column in SQL every
    tick. My first insert wrote the config blob alone, which would have stored a
    plan's completion condition where nothing judges it. NEGATIVE CONTROL run:
    binding NULL instead of the promoted value fails
    `an_approved_plan_replaces_the_missions_phases`.

`order_idx` comes from the array's own order rather than a field the model sets:
two sources for one fact is how a plan ends up with two phase 0s, and order_idx
is what `start_pending_phases` sequences on.

MAX_PHASES is 4 and the prompt argues for one. Each phase is a full agent run in
sequence, and splitting one change into plan → implement → test is the documented
anti-pattern — a single agent doing all three keeps the context that makes the
later steps good.

543 tests pass, clippy clean. Migration 0072.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-06 20:05:28 -07:00