Commit Graph
3 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
Omar SobhandClaude Opus 5 da3731d753 fix(missions): a coding phase that delivers nothing is a failure
The last open item in the silent-success class: a coding phase that
changed no files reported `completed` — the same status a phase gets for
delivering tested, reviewed, pushed work. Mission `019fcf62` completed
that way with its agents silently unpinned from the repo, and nothing in
the platform disagreed; it was found by a script diffing the forge.

The verdict is applied at capture rather than at completion, because
capture selects on `status = 'completed'` — the platform does not know
whether a phase produced anything until after it has already finished.

Three conditions must hold before failing a phase, because a false
positive here fails honest work: the phase is a coding phase (research
phases legitimately write nothing to the tree), the diff was actually
computed (an uncomputable diff also reports zero files — blaming the
agent for a platform fault is the same defect wearing different
clothes), and `allow_empty` is not set. Only an explicit `true` opts
out, so a typo leaves the check armed. Registered in phase_config with
its reader named, per the seam-2 rule.

Also closes an ordering hazard this exposed: capture is batched and runs
after a phase completes, so a backlogged mission could close as
'completed' and only then have capture discover an empty phase — leaving
a 'completed' mission holding a 'failed' phase, unfixable because the
mission-close CASE only touches 'running' rows. A repo-bearing mission
now waits for its work to be captured before closing.

Adds a `noop` scenario to the harness: a phase told to change nothing,
which PASSES only when the phase comes back `failed`. Same discipline as
the uid self-test — a check that has never been seen to fire has not
been shown to work.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-04 19:39:14 -07:00
Omar SobhandClaude Opus 5 ec85f6c8da fix(missions): close the three seams behind this run of failures
ci / gates (push) Failing after 6s
ci / rust (push) Skipped
ci / frontend (push) Skipped
ci / e2e (push) Skipped
ci / publish (push) Skipped
Seam 1 — delivery inferred checkout state from the tree, so whether work
survived depended on what the agent happened to do. 019fc444 committed
and left a clean tree; 019fc476 had its base advanced to match HEAD;
019fc450 survived only because a phase FAILED to commit and left the tree
dirty. Same code, opposite outcomes, decided by the agent.

mark_phase_started records the fact at phase launch, before the agent
acts, so every one of those states answers identically. The tree checks
remain as a second line of defence for pre-existing checkouts.

Seam 2 — phase config was accepted, stored and read by nobody. That was
`task`: every phase of every mission got identical instructions. The new
phase_config registry names the reader for each live key and lists the
eight that are declared-but-unimplemented, reporting both at mission
creation so an author sees what will not happen. Its CI test found one I
had missed: security_hardening.toml sets phase-level mcp_bundles asking
for gitea_forge + security_scan, but bundles come from the TEAM template
and the phase gets neither.

Seam 4 — push_url_for collapsed a failed query, an unbound repo and a
missing clone_url into one None, so a database fault was recorded as
"nothing to push to" and metadata read `pushed: null, push_error: null` —
the same ambiguity commit_error already fixed. Each case now carries its
reason into the artifact, and a local git failure during publish is
recorded rather than dropped by .ok().

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-02 18:58:45 -07:00