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]>
42 lines
2.0 KiB
SQL
42 lines
2.0 KiB
SQL
-- A model's proposal for a mission's PHASES, and whether a human accepted it.
|
|
--
|
|
-- The sibling of `mission_team_proposals` (0070), and deliberately the same
|
|
-- shape: propose, review, approve, apply. That one lets a model size the team;
|
|
-- this one lets a model decide what the work actually IS — the phases, their
|
|
-- order, and what each must satisfy.
|
|
--
|
|
-- What it replaces: five hand-written recipes in `templates/workflows/*.toml`,
|
|
-- one of which every mission picks wholesale. A recipe is a fixed answer to
|
|
-- "what phases does this kind of mission have", written before anyone saw the
|
|
-- mission — which 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 remain the default for a mission nobody proposes a plan
|
|
-- for, and the fallback when a proposal is refused.
|
|
--
|
|
-- `status` matches 0070 exactly, including the partial unique index: two
|
|
-- approved plans would be two answers to "what is this mission", and the phase
|
|
-- table holds one.
|
|
CREATE TABLE IF NOT EXISTS mission_plan_proposals (
|
|
id UUID PRIMARY KEY,
|
|
mission_id UUID NOT NULL REFERENCES missions (id) ON DELETE CASCADE,
|
|
workspace_id UUID NOT NULL,
|
|
-- {"phases": [{kind, task, done_when?, done_when_check?, allow_empty?}, ...]}
|
|
-- Order is the array's own order; a model that also emits `order_idx` would
|
|
-- give two sources for one fact.
|
|
plan JSONB NOT NULL,
|
|
author_model TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'proposed'
|
|
CHECK (status IN ('proposed', 'approved', 'rejected')),
|
|
note TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
decided_at TIMESTAMPTZ,
|
|
decided_by UUID
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS mission_plan_proposals_mission_idx
|
|
ON mission_plan_proposals (mission_id, created_at DESC);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS mission_plan_proposals_one_approved
|
|
ON mission_plan_proposals (mission_id)
|
|
WHERE status = 'approved';
|