docs: design spec for milestone-close gate (cycle ≠ milestone)

Separate `cycle` (a pipeline-graph round) from `milestone` (a
Gitea tracker container that closes only when complete AND
functional). `audit` proves drift-clean, never functional, so no
audit signal — and no `/boss` done-state — may close a milestone.

Introduces a milestone-close gate (complete ∧ functional) defined
once in docs/pipeline.md, and a milestone-wide fieldtest as a
carrier-scope variant of the existing fieldtest skill. The
milestone fieldtest runs curated end-to-end scenarios derived
top-down from the milestone's promise — proving the shipped
implementation delivers what the milestone as a whole promised,
not a mechanical union of per-cycle axes. The actual tracker
close stays a manual act; this spec only defines when a milestone
is closeable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 11:57:41 +02:00
parent 2d0de46fea
commit c2e7f55651
@@ -0,0 +1,241 @@
# Milestone-close gate: separating `cycle` from `milestone`
**Date:** 2026-05-31
**Status:** approved (design)
**Topic:** make `audit` unable to imply a milestone is finished;
add a milestone-wide fieldtest as the closing gate.
## Problem
`audit` runs at cycle close and emits a "clean" signal whose
commit body reads `cycle <X> tidy (clean)`. `audit` only proves
*drift-clean* — the code matches the design ledger. It is blind
to whether the work is *functional* from a downstream
consumer's point of view; that is exactly what `fieldtest`
measures, and `fieldtest` runs *after* `audit`.
Two defects follow:
1. **Conflation of `cycle` and `milestone`.** `README.md`
("AILang calls a cycle a *milestone*") and
`templates/project-profile.yml:41` (`cycle: # what a
top-level work unit is called`) treat a cycle as the
top-level work unit. It is not. A **milestone** is a tracker
(Gitea) container that closes only when the work is
*completely implemented and functional*. A **cycle** is one
round in the pipeline graph — a step in the loop. A milestone
spans potentially many cycles.
2. **An audit-clean signal can leak into a milestone close.**
Because `audit`'s "clean" reads like "done", an orchestrator
can be led to close a Gitea milestone on the strength of an
audit, before any fieldtest has demonstrated the work is
functional. No skill closes a milestone today, so this is a
conceptual hazard rather than a code path — but the wording
actively invites the mistake.
The fix is **not** to swap the `audit → fieldtest` order:
`fieldtest` needs the drift-clean state to test against, so
`audit` must stay first. The fix is to **decouple the gate**
`audit`'s "clean" never closes anything, and a separate,
explicitly defined milestone-close gate requires both
completeness and functional validation.
## Conceptual model
### Two distinct axes
- **Cycle** — one round in the pipeline graph
(`brainstorm → planner → implement → audit → [per-cycle
fieldtest]`). A cycle close is an internal loop step. Many
cycles per milestone.
- **Milestone** — a tracker (Gitea) container. Closes **only**
when *complete* (every cycle drift-clean / ratified, no open
iterations) **and** *functional* (milestone fieldtest green).
### Three disentangled "clean" terms
| Term | Owner | Means | Closes a milestone? |
|------|-------|-------|---------------------|
| `drift-clean` | `audit` | code matches the ledger | never |
| cycle functionally validated | per-cycle `fieldtest` | cycle-local surface usable | never |
| milestone closeable | milestone-close gate | complete ∧ functional | yes (manual act) |
### Two fieldtest scopes
The milestone fieldtest is a **scope variant** of the existing
`fieldtest` skill — same `fieldtester` agent, a different
carrier. No new skill; keeps the surface DRY.
- **Per-cycle fieldtest** (existing, unchanged): carrier carries
`cycle_id` / `cycle_scope`; dispatched after a
surface-touching cycle's audit; catches cycle-local friction
early; optional / orchestrator-dispatched as today.
- **Milestone fieldtest** (new variant): carrier carries
`milestone_id` and `milestone_promise` — the milestone's
stated goal / acceptance in the tracker (and its spec, if one
exists). The fieldtester derives a small set of **curated
end-to-end scenarios** that, run as a downstream consumer,
prove the shipped implementation delivers **what the milestone
as a whole promised**. Scenarios are chosen *top-down from the
milestone's promise*, not assembled bottom-up as the
mechanical union of per-cycle axes — the per-cycle fieldtests
already cover axis-local surface; the milestone fieldtest's
job is to demonstrate the promise end to end. Its green status
roll-up is the closing gate.
## The milestone-close gate (authoritative)
A milestone may be closed in the tracker only when **both** legs
hold:
1. **Complete** — every cycle filed under the milestone is
`audit` drift-clean (or its drift explicitly ratified), and
the milestone container has no open iterations / issues
remaining.
2. **Functional** — the milestone fieldtest has run its curated
end-to-end scenarios against the milestone's promise and its
status roll-up is `clean`: every scenario demonstrably
delivers what the milestone promised; no open `bug` findings;
`friction` / `spec_gap` findings either resolved or
explicitly ratified into the design ledger.
No `audit` result, and no `/boss` done-state, closes a milestone
on its own. `/boss` done-state means "this `/boss` run has
nothing left to dispatch" — it is **not** a milestone close.
The actual close (`tea milestone close`, or the tracker
equivalent) stays a **deliberate human / orchestrator act**,
performed only after the gate reads green. This design defines
*when* a milestone is closeable; it does **not** mechanise the
close. (`issue/SKILL.md` is out of scope here.)
### Edge cases
- **Non-surface milestone.** A milestone whose entire scope is
internal (refactor, infra, docs) touches no user-visible
surface. The milestone fieldtest is then *not applicable*
(same skip logic as the per-cycle fieldtest, lifted to
milestone level). The "functional" leg is satisfied vacuously;
the "complete" leg (audit-clean across cycles) suffices.
- **Single-cycle milestone.** Nothing special: the milestone
fieldtest still runs (if surface-touching), proving the
milestone's promise end to end even though that promise
happened to ship in a single cycle. The per-cycle and
milestone fieldtests may coincide in surface but remain
conceptually distinct gates — axis-local validation versus
promise-level proof.
- **Ratified drift.** A cycle whose drift was ratified
(`--update-baseline` + ratify paragraph) counts as satisfying
the "complete" leg for that cycle — ratification is the
documented form of "complete".
## Per-file changes
Single-source discipline (consistent with the recent
carrier/handoff single-sourcing): the gate is **defined once**
in `docs/pipeline.md`; every other file **references** it rather
than restating it, so the files cannot drift.
### `docs/pipeline.md` (single source)
- ASCII graph: rename `audit --(clean)` to `audit
--(drift-clean)`; annotate `[cycle close]` explicitly as a
loop step, not a milestone end.
- New subsection **"Cycle vs. milestone"** — defines the two
axes authoritatively (a milestone spans many cycles; a cycle
is one loop round).
- New subsection **"Milestone-close gate"** — the authoritative
gate definition above (complete ∧ functional), names the
milestone fieldtest as a mandatory precondition for
surface-touching milestones, and states plainly that no
`audit` and no `/boss` done-state closes a milestone.
### `audit/SKILL.md`
- Step 2 table: "Audit can close" → drift-gate wording. `audit`
clears the *cycle drift check*; it does not close the cycle or
the milestone.
- Close-commit wording (`cycle <X> tidy (clean)`): kept, but
annotated explicitly as *drift-clean, not a milestone signal*.
- New **Red Flag**: "Declaring a milestone closeable on the
strength of an audit-clean, while the milestone fieldtest has
not run green." Cross-reference the gate in `docs/pipeline.md`.
- Overview: clarify `audit` ↔ cycle close (never milestone).
### `fieldtest/SKILL.md`
- Document the second trigger variant — the **milestone
fieldtest** (milestone-wide, end-to-end): carrier carries
`milestone_id` and `milestone_promise`; the fieldtester
derives curated end-to-end scenarios from the milestone's
promise (top-down, not a union of per-cycle axes) and proves
the shipped implementation delivers it; its green status
roll-up is the closing gate. Reference the gate in
`docs/pipeline.md` rather than restating it.
- The "audit first" precondition stays, for both variants.
- The milestone-fieldtest skip logic mirrors the per-cycle one,
lifted to milestone level (non-surface milestone → exempt).
### `boss/SKILL.md`
- Done-state: reference the `docs/pipeline.md` gate instead of
restating the formula. Clarify that a `/boss` done-state is
**not** an automatic milestone close — the milestone close
needs the milestone fieldtest green and is a manual act.
### `README.md`
- **Root fix**: correct the `cycle ≡ milestone` conflation
(the "AILang calls a cycle a *milestone*" lines). Cycle = a
pipeline round; milestone = a tracker container spanning many
cycles, closed only when complete ∧ functional.
- `fieldtest` row: per-cycle fieldtest stays optional; note the
milestone fieldtest as the mandatory gate before a
surface-touching milestone closes.
### `templates/project-profile.yml`
- Fix the `cycle:` comment ("top-level work unit" is wrong — a
cycle is the pipeline round).
- Add a `milestone:` vocabulary field for the tracker container.
- Add the milestone-fieldtest phase to the pipeline config (so a
project can name / gate it), consistent with the existing
`pipeline:` configuration model.
### `docs/profile-schema.md`
- Same root conflation lives here: the `vocabulary` table
(`cycle` = "top-level work unit", with `milestone` as an
example value) and the minimal-profile example (`cycle:
milestone`). Correct both — document `cycle` as the pipeline
round and add the `milestone` vocabulary field as the tracker
container.
- Document the milestone-fieldtest phase in the `pipeline`
section alongside the existing `fieldtest` phase.
## Scope boundaries
**In scope:** the gate definition and the wording/decoupling
across `docs/pipeline.md`, `audit`, `fieldtest`, `boss`,
`README.md`, `templates/project-profile.yml`; the milestone
fieldtest as a carrier-scope variant of the existing skill.
**Out of scope:** mechanising the tracker close (no
`issue/SKILL.md` change, no automated `tea milestone close`);
reordering `audit → fieldtest`; any change to the per-cycle
fieldtest's existing behaviour beyond naming it "per-cycle".
## Verification
- Grep sweep: no remaining file equates `cycle` with
`milestone`; the only authoritative gate definition lives in
`docs/pipeline.md`, and `audit` / `fieldtest` / `boss`
reference it without restating it.
- `audit/SKILL.md` carries the new Red Flag and drift-gate
wording; its close-commit example is annotated as drift-clean.
- `fieldtest/SKILL.md` documents both carrier scopes and the
milestone-level skip rule.
- `templates/project-profile.yml` parses (YAML) with the new
`milestone:` vocabulary field and the milestone-fieldtest
phase, and `docs/profile-schema.md` stays consistent with it.