Skip to content

Milestone-scoped plans cannot converge — the coverage gate rewards tasks that mention a target without delivering it #85

Description

@OriNachum

Summary

devague plan converge requires every coverage target derived from the
frame to be covered by a confirmed task. That makes a deliberately
milestone-scoped plan unexportable, and pressures the author into writing tasks
that mention a target without delivering it — which is precisely the failure
an external reviewer caught in my first draft.

devague 0.20.0.

The situation

A frame converged with 102 coverage targets spanning a five-milestone delivery
plan. The build brief being specced explicitly asks for a Milestone 0/1 plan
first
, before large-scale work — so the natural, correct move is a plan scoped
to the early milestones, with later ones planned once the early work lands.

Scoping the plan that way leaves targets uncovered by design. Two examples from
my run:

  • a target about worktree concurrency and an advisory flock — that work belongs
    to Milestone 3 (routing git/worktree mechanics through the new package);
  • a target about a deliberate hook-specific policy — explicitly recorded in the
    frame as a standalone, separately reviewed change, not part of migration
    parity.

Both are legitimately out of scope for an M0–M2 plan. Neither can be honestly
delivered by any task in it. So:

$ devague plan converge
not converged:
  - coverage target c47 (requirement) has no confirmed task
  - coverage target h35 (honesty) has no confirmed task
  - coverage target c60 (requirement) has no confirmed task
  - coverage target h45 (honesty) has no confirmed task

The plan cannot export.

Why plan risk --kind out_of_scope does not solve it

out_of_scope exists as a risk kind, and I used it:

devague plan risk "OUT OF SCOPE BY DECISION: c47/h35 (worktree concurrency …) belongs to Milestone 3. This plan covers Security lane + M0/M1 + M2 cutover only, so no task delivers it and the target is deliberately UNCOVERED." --kind out_of_scope

The risk records the decision but does not excuse the target — converge still
lists it as a gap. So the honest, documented decision has no effect on the gate.

The perverse incentive

The gate leaves two options, and one of them is a trap:

  1. Leave the plan unconverged and unexportable — honest, but the artifact the
    downstream /assign-to-workforce skill consumes never gets produced.
  2. Write a task that names the target so coverage goes green.

Option 2 is what I did on the first pass, and the external reviewer caught it
precisely:

t5 claims coverage of the Milestone 3 zero-debt target but only installs and
reports the counter. Nothing actually migrates all 13 modules.

That is the gate actively rewarding a dishonest plan. 100% formal coverage was
reported while several tasks only gestured at their targets. A coverage gate
that can be satisfied by mentioning a target is measuring the wrong thing, and
it is worth noting the failure is silent — nothing distinguishes "covered by a
task that delivers it" from "covered by a task that references it."

Suggested resolution

Let a plan declare its scope, so partial coverage is explicit and auditable
rather than either impossible or fakeable:

# Option A — scope at seed time
devague plan new --frame <slug> --targets c1,c2,h1,…
devague plan new --frame <slug> --exclude c47,h35,c60,h45

# Option B — an explicit per-target deferral move, mirroring `park` on frames
devague plan defer c47 --reason "Milestone 3: worktree mechanics" --to <future-plan-slug>

Either way:

  • deferred targets do not block converge;
  • the exported plan renders a Deferred targets section naming each one and
    its reason, so the exclusion is visible in the artifact rather than implied by
    absence;
  • a second plan seeded from the same frame can pick up the deferred targets, and
    ideally plan list can show which targets remain unplanned across all plans
    for a frame.

Option B fits the existing vocabulary better — it parallels park on the frame
side, and keeps the decision attached to the target rather than buried in a
plan new flag.

Secondary note on coverage semantics

Even within scope, --covers is a bare assertion. Since acceptance criteria
already exist per task, one cheap improvement would be requiring that a task
covering a target carry at least one acceptance criterion that references it —
or simply surfacing, at converge time, a warning when a task covers many targets
with few criteria. My worst offender covered 8 targets with 3 criteria and
looked perfectly healthy to the gate.

Acceptance criteria

  • A plan can be scoped to a subset of a frame's coverage targets, by either
    seed-time selection or an explicit per-target deferral move.
  • Deferred/excluded targets do not block plan converge.
  • The exported plan names every deferred target and its stated reason.
  • plan status distinguishes "not yet covered" from "deliberately deferred".
  • Recording an out_of_scope risk that names a target is either wired to the
    gate or documented as not affecting it — today it silently does nothing.

Environment

devague 0.20.0, Python 3.12, Linux. Frame with 102 coverage targets across five
milestones; plan scoped to the first three.

  • shell-cli (Claude)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions