@pilotspace/add 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/docs/02-the-flow.md +10 -0
- package/docs/14-foundation.md +6 -4
- package/docs/appendix-c-glossary.md +7 -1
- package/package.json +1 -1
- package/skill/add/SKILL.md +2 -2
- package/skill/add/design.md +19 -2
- package/skill/add/intake.md +16 -0
- package/tooling/add.py +253 -27
- package/tooling/templates/DESIGN.md.tmpl +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,68 @@ All notable changes to the ADD method (`@pilotspace/add` on npm,
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.12.0] — 2026-06-26
|
|
10
|
+
|
|
11
|
+
Multi-milestone polish release — the intake/roadmap front grows a real **queue**,
|
|
12
|
+
the parallel-front residuals close, and the UDD loop gains a design-intake beat.
|
|
13
|
+
Backward-compatible throughout; a single-active project sees no behavior change.
|
|
14
|
+
|
|
15
|
+
### Added (multi-milestone-intake — a request that is several milestones)
|
|
16
|
+
- **`queued` milestone status.** The milestone status enum becomes
|
|
17
|
+
**active · queued · done** (was active · done). A milestone can now exist
|
|
18
|
+
non-active, awaiting promotion — created with `new-milestone --queued` and made
|
|
19
|
+
active with an explicit `activate`. Migration-safe: an old state with no `queued`
|
|
20
|
+
milestones reads byte-identically.
|
|
21
|
+
- **Roadmap intake.** `intake.md` now guides decomposing a request that is *several*
|
|
22
|
+
milestones into a roadmap that creates **all N — 1 active + N−1 queued** — instead
|
|
23
|
+
of only the first. Promotion stays human-gated; the whole set is never auto-activated.
|
|
24
|
+
- **Queued-backlog resume cue.** `status` surfaces the queued backlog (active
|
|
25
|
+
milestone + what's queued next), so a multi-milestone session resumes cleanly. The
|
|
26
|
+
cue is present-only — byte-identical output when nothing is queued.
|
|
27
|
+
|
|
28
|
+
### Added (multi-active-polish — close the parallel-front residuals)
|
|
29
|
+
- **`waves --merge`** folds the active SET into one cross-milestone DAG and critical
|
|
30
|
+
path (today's `waves` is per-milestone only).
|
|
31
|
+
- **`mine --all`** widens the ownership lens past the active milestones — every
|
|
32
|
+
not-done task you own or are assigned, across all milestones plus loose, with an
|
|
33
|
+
email-OR-name match. Plain `mine` (active-only) stays byte-identical.
|
|
34
|
+
- **`doctor` value-domain checks.** `doctor` now flags a bad `gate` enum
|
|
35
|
+
(∉ {PASS, RISK-ACCEPTED, HARD-STOP}), a bad `phase`, archived inconsistency, or a
|
|
36
|
+
malformed owner/assignee — beyond the existing referential pointers. It **adds
|
|
37
|
+
findings only**; it never auto-fails or retro-reds grandfathered history.
|
|
38
|
+
- **`new-milestone` add-and-focus.** Creating a milestone while one is active now
|
|
39
|
+
**preserves the active SET** (adds the new one and focuses it) instead of replacing
|
|
40
|
+
the set and evicting the others. `--queued` and single-active stay byte-identical.
|
|
41
|
+
|
|
42
|
+
### Added (udd-design-intake)
|
|
43
|
+
- **`design-intake` beat.** The UDD design loop opens with a new front beat that
|
|
44
|
+
interviews the human on four axes — **FIDELITY · CONCEPT · LAYOUT · VISUAL DESIGN**
|
|
45
|
+
— recorded in `DESIGN.md` before review-capture-confirm. Convention-only (the engine
|
|
46
|
+
never renders); identity values (brand color/type) stay human-owned — surfaced to
|
|
47
|
+
decide, never auto-picked.
|
|
48
|
+
|
|
49
|
+
### Added / Changed (loose tasks since 1.11.0)
|
|
50
|
+
- **Descriptive-slug nudge (`new-milestone`).** A milestone slug that is a bare
|
|
51
|
+
version number (`v2`, `v1-1`, `1.2` — matched by `^v?\d+([._-]\d+)*$`) prints an
|
|
52
|
+
advisory `note:` suggesting a short **descriptive** name (e.g. `payment-retries`) and
|
|
53
|
+
**still creates the milestone** — never blocks.
|
|
54
|
+
- **Full-ISO `created:` stamp.** MILESTONE.md's `created:` line is the full UTC ISO
|
|
55
|
+
timestamp (`2026-06-26T03:47:28+00:00`); a single `_now()` instant feeds both the
|
|
56
|
+
rendered file and the `state.json` record.
|
|
57
|
+
- **`add.py freeze`** — an engine-stamped human-approval write at the §3 contract
|
|
58
|
+
freeze (DRAFT → FROZEN @ vN with a structured actor line). A security finding remains
|
|
59
|
+
an un-forceable HARD-STOP.
|
|
60
|
+
- **Queued + await-confirm reminder.** A milestone that is both `queued` and
|
|
61
|
+
`--await-confirm` now surfaces its `milestone-confirm` reminder at resume.
|
|
62
|
+
- **Flow docs.** Book ch.02 gains a subsection explaining the milestone-scale
|
|
63
|
+
composition rule — tasks are **listed breadth-first up front** (the DAG), then each
|
|
64
|
+
**specified just-in-time**.
|
|
65
|
+
|
|
66
|
+
This release bundles **3 closed milestones** (`udd-design-intake`,
|
|
67
|
+
`multi-milestone-intake`, `multi-active-polish`) and the loose tasks above since
|
|
68
|
+
1.11.0. Every milestone was built end-to-end through ADD's own
|
|
69
|
+
spec→tests→build→verify flow.
|
|
70
|
+
|
|
9
71
|
## [1.11.0] — 2026-06-26
|
|
10
72
|
|
|
11
73
|
Engine-modularization + audit-hardening release — the 7k-line monolith becomes a
|
package/docs/02-the-flow.md
CHANGED
|
@@ -52,6 +52,16 @@ The shape is deliberate: the human-led steps establish direction, a frozen contr
|
|
|
52
52
|
|
|
53
53
|
> **What changed in v7 (the diagrams above show the structural flow, which is unchanged).** The *steps* and their order are exactly as drawn — only **who resolves them** moved. The AI now drafts the whole specification bundle (steps 1–4) and a person approves it **once**, at the contract freeze (not a sign-off at each step); and **Verify is auto-gated on evidence** under `autonomy: auto` (the default), escalating security — always a `HARD-STOP` — and other residue to a person. Lower the autonomy level to `conservative` to keep a human at the Verify gate. See [11 Governance](./11-governance.md).
|
|
54
54
|
|
|
55
|
+
## Many features, one at a time — listed up front, specified just-in-time
|
|
56
|
+
|
|
57
|
+
The flow above runs *one* feature. A milestone holds many, and they compose by one rule: **list every task up front, specify each just-in-time.**
|
|
58
|
+
|
|
59
|
+
- **Listed up front (breadth-first).** When a milestone is created it is decomposed **breadth-first** into a task *list* — `slug · depends-on · one line` each. That is the whole plan and its dependency DAG (`add.py waves` views the order and the critical path). The milestone living-doc (`MILESTONE.md`) holds this list plus the shared decisions and the exit criteria, and it stays **thin** — no per-task detail lives there.
|
|
60
|
+
- **Specified just-in-time.** Each listed task runs the full seven-step flow (0 Ground … 6 Verify) only when work reaches it. Its specification bundle — §1–§4 — is written then, **not** bundled for every task before any build begins.
|
|
61
|
+
- **Why just-in-time.** A later task's spec absorbs what earlier tasks' Observe step taught (a sharper contract, a convention that emerged, a delta fed back), and a bundle written too early rots before you arrive at it. This is the same backward-correction principle — Observe → Specify — applied at milestone scale rather than within one feature. So: list all the tasks, then specify each in turn.
|
|
62
|
+
|
|
63
|
+
The sequence is therefore **decompose the milestone → schedule the task DAG → run each task's seven-step flow just-in-time → close the milestone** — breadth planned once, depth earned one task at a time.
|
|
64
|
+
|
|
55
65
|
## Why the order is the order
|
|
56
66
|
|
|
57
67
|
Each step produces exactly one artifact, and each artifact is the input to the next step. The order is not a preference; it is a dependency chain.
|
package/docs/14-foundation.md
CHANGED
|
@@ -49,10 +49,12 @@ the method already names, made explicit as three concerns.
|
|
|
49
49
|
|
|
50
50
|
When a milestone has screens, UDD is not only a static `DESIGN.md` — it runs a
|
|
51
51
|
**design-definition loop** that turns the domain into a screen the human has *seen
|
|
52
|
-
and confirmed before build*. Its
|
|
53
|
-
→ wireframe → render-capture-confirm`:
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
and confirmed before build*. Its five beats are `design-intake → review-domain →
|
|
53
|
+
research-components → wireframe → render-capture-confirm`: interview the human on the
|
|
54
|
+
four design axes (fidelity · concept · layout · visual design) so the look is directed
|
|
55
|
+
not guessed, read the domain into screens and regions, research and reuse components
|
|
56
|
+
before inventing them, wireframe the structure low-fi, then render a real screen and
|
|
57
|
+
**capture** it. That capture is the **design-confirm**
|
|
56
58
|
evidence — a real image the person approves *before* implementation, so the build
|
|
57
59
|
matches the layout instead of discovering it. The book keeps the *why*; the
|
|
58
60
|
operational recipe (the wireframe format, the token-bound mock, the capture engines)
|
|
@@ -84,6 +84,10 @@
|
|
|
84
84
|
|
|
85
85
|
**Baseline approval** (formerly "the lock-down") — the single human gate ending autonomous setup: an explicit yes that freezes the foundation, first scope, and first contract together; runs as `add.py lock --by <name>`.
|
|
86
86
|
|
|
87
|
+
**Queued milestone** — a milestone created non-active: its status is `queued` (the milestone status enum is **active · queued · done**). It is recorded with its `MILESTONE.md` on disk but does not hold the active focus; `add.py new-milestone --queued <slug>` creates it, and `add.py activate <slug>` promotes it queued→active one at a time. A queued milestone is not `done`, so it does not satisfy the graduation gate.
|
|
88
|
+
|
|
89
|
+
**Roadmap** — the multi-milestone intake artifact: when a request decomposes into N>1 milestones of the same line, the AI proposes the ordered roadmap and, on human confirm, creates all N — the first active, the rest **queued** — then promotes each with `activate` as it is started. Distinct from `split_required` (which spans different buckets). Lives in the `add` skill's `intake.md`.
|
|
90
|
+
|
|
87
91
|
**Scope level** (formerly "altitude") — the granularity a decision lives at, as one ordered ladder of five: **setup/foundation** · **intake** (request → versioned scope) · **the milestone loop** (the task is its inner unit) · **stage graduation** (changes rigor, not version; see **Stage graduation**) · **release** (≥1 closed milestone → a versioned, watched cut; see **Release scope level**). One ⚠-assumption notation is shared across every scope level.
|
|
88
92
|
|
|
89
93
|
**Autonomy level** (formerly "autonomy dial") — the explicit per-task setting (`autonomy: manual | conservative | auto`, an ordered ladder manual < conservative < auto) choosing who resolves Verify: `auto` auto-PASSes on complete evidence, `conservative` keeps a human at the gate, `manual` is the strict floor (the human owns the gate; nothing auto-resolves). A high-risk scope refuses an unguarded `auto` — it must be lowered to `manual` or `conservative`. New tasks seed a visible, overridable `autonomy: auto`; a live task with no level warns (`implicit_autonomy`), a token outside the set is rejected (`unknown_autonomy_level`).
|
|
@@ -134,13 +138,15 @@
|
|
|
134
138
|
|
|
135
139
|
**Newest-first append-only** — every append-only foundation sequence prepends the newest record at the top; the rolled-up settled line anchors at the bottom (the oldest end), so compaction collapses upward.
|
|
136
140
|
|
|
141
|
+
**Design intake** — beat 0 of the UDD **design-definition loop**: the agent interviews the human on four design **axes** *before* reading the domain, so the look is directed not guessed. **Fidelity** (lo-fi wireframe · hi-fi mockup · production — how far the design renders), **Concept** (idea · mood · direction), **Layout** (structure · grid · hierarchy), and **Visual design** (color · type · spacing · imagery — surfaced for the human, never auto-picked). Recorded as project defaults in `DESIGN.md`'s `## Design intake` and per-screen overrides in the prototype note. Fidelity is recorded intent, not an engine gate.
|
|
142
|
+
|
|
137
143
|
**Wireframe** — the Stage-A low-fidelity, *structural* map of one screen: its regions and the component **slots** inside them, derived from `prototypes/<name>.json` *before* any color, type, or spacing — it answers "what goes where", not "what it looks like". Beat 3 of the UDD **design-definition loop**; the low-fi half of the two-stage fidelity that ends in a confirmed capture. See the `udd-wireframe.md` template (`tooling/templates/`, Stage A).
|
|
138
144
|
|
|
139
145
|
**Design mock** — the Stage-B high-fidelity, **self-contained** HTML render of a screen: the `catalog.json` components as a reusable token-bound kit, bound to `tokens.json` and populated with mock data, openable offline and screenshot-able. The human-facing *visible* evidence the human confirms (the frozen `prototypes/<name>.json` tree is its machine-checkable twin). Beat 4's hi-fi artifact; the recipe lives in the `udd-wireframe.md` template (`tooling/templates/`, Stage B).
|
|
140
146
|
|
|
141
147
|
**Capture** — the real rendered image (PNG/SVG) of a design mock: the **design-confirm evidence** artifact. Captures live at `.add/design/captures/<name>.<ext>` (one per prototype) and are attached or mentioned in the feature's `TASK.md`; `@json-render/image` (Satori → PNG/SVG, no browser) is the named default capture engine, otherwise the self-contained mock is screenshot headless. The engine never renders — it only MEASURES presence: `add.py check` raises a never-red `missing_capture` WARN for a prototype with no capture.
|
|
142
148
|
|
|
143
|
-
**Design-confirm** — the human touchpoint closing the UDD **design-definition loop** (`review-domain → research-components → wireframe → render-capture-confirm`, beat
|
|
149
|
+
**Design-confirm** — the human touchpoint closing the UDD **design-definition loop** (`design-intake → review-domain → research-components → wireframe → render-capture-confirm`, the final beat of the `add` skill's `design.md`): approving the captured screen image **before build**, show-before-ask, so the implementation matches the layout the human has already seen instead of discovering it.
|
|
144
150
|
|
|
145
151
|
---
|
|
146
152
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pilotspace/add",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "ADD (AI-Driven Development). One skill. Eight steps. Five disciplines. Every feature ships through the loop — a minimal, state-tracked Claude Code skill that ships the AIDD book as its trust layer.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"add": "bin/cli.js"
|
package/skill/add/SKILL.md
CHANGED
|
@@ -123,8 +123,8 @@ once the human confirms, rewrites `SOUL.md` (the human is the only writer) — `
|
|
|
123
123
|
- **Small, low-risk task**, less ceremony → the **fast lane**: `new-task --fast` scaffolds the minimal
|
|
124
124
|
`TASK.fast.md`, bundle approved in one freeze — `phases/fast-lane.md`. Floor held (frozen contract ·
|
|
125
125
|
red test · verify gate; `--fast` is freeze-gated under any milestone). Collapse, never skip; opt-in.
|
|
126
|
-
- **UI feature** at specify → the **design-definition loop** (UDD):
|
|
127
|
-
reuse components → wireframe → a
|
|
126
|
+
- **UI feature** at specify → the **design-definition loop** (UDD): intake the design axes → review the
|
|
127
|
+
domain → research and reuse components → wireframe → a captured screen the human confirms **before** build — `design.md`.
|
|
128
128
|
Tool-agnostic; the engine never renders.
|
|
129
129
|
- Tasks all done but the milestone **goal** unmet → `milestone-done` holds it open; the loop turns open
|
|
130
130
|
deltas + extras into the next tasks (you propose, the human confirms) until the goal is met — `loop.md`.
|
package/skill/add/design.md
CHANGED
|
@@ -10,14 +10,29 @@ draws from already exists — `tokens.json` (the compact-DTCG dialect, `udd-toke
|
|
|
10
10
|
`catalog.json` + `prototypes/<name>.json` (`udd-catalog.md`). This loop fills that foundation
|
|
11
11
|
for a feature and earns the human's sign-off on the look before build.
|
|
12
12
|
|
|
13
|
-
## The loop —
|
|
13
|
+
## The loop — five beats
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
review-domain → research-components → wireframe → render-capture-confirm
|
|
16
|
+
design-intake → review-domain → research-components → wireframe → render-capture-confirm
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Run the beats in order. Each feeds the next; the last ends at a human design-confirm.
|
|
20
20
|
|
|
21
|
+
### 0 · design-intake
|
|
22
|
+
Before you read the domain, interview the human on **four design axes** — so the look is
|
|
23
|
+
*directed*, not guessed. Ask each, show the options, record the pick:
|
|
24
|
+
|
|
25
|
+
- **FIDELITY** — how far this design goes: *lo-fi wireframe* · *hi-fi mockup* · *production*.
|
|
26
|
+
Recorded **intent** that **informs** how far the later beats render (it is not an engine gate).
|
|
27
|
+
- **CONCEPT** — the design *idea / mood / direction* in a line.
|
|
28
|
+
- **LAYOUT** — the *structure / grid / hierarchy* the screens lean on.
|
|
29
|
+
- **VISUAL DESIGN** — *color · type · spacing · imagery*. **Surface** identity values for the
|
|
30
|
+
human to choose — never auto-pick a brand value (identity stays **human-owned**, `udd-tokens.md`).
|
|
31
|
+
|
|
32
|
+
Record the answers **before** review-domain, at both levels: project **defaults** in DESIGN.md's
|
|
33
|
+
`## Design intake` section; per-screen **overrides** (only the deltas) in the per-feature design
|
|
34
|
+
note (the `prototypes/<name>.json` companion). Show-before-ask — confirm the picks, never assume.
|
|
35
|
+
|
|
21
36
|
### 1 · review-domain
|
|
22
37
|
Start from the **domain**, not a blank canvas. Read the domain model — entities, flows, the
|
|
23
38
|
ubiquitous language in `PROJECT.md` / `GLOSSARY.md` — and derive **which screens** the feature
|
|
@@ -70,6 +85,8 @@ human to decide, never auto-pick a brand value (`udd-tokens.md`).
|
|
|
70
85
|
## The hard rules
|
|
71
86
|
|
|
72
87
|
<constraints>
|
|
88
|
+
- **Intake before domain.** The four axes (FIDELITY · CONCEPT · LAYOUT · VISUAL DESIGN) are
|
|
89
|
+
interviewed and recorded — DESIGN.md defaults + per-screen overrides — before beat 1.
|
|
73
90
|
- **Domain first.** A screen is derived from the domain (beat 1), never sketched blind.
|
|
74
91
|
- **Reuse before invent.** Beat 2 checks the catalog first; a new component is a justified,
|
|
75
92
|
cited exception — never the reflex.
|
package/skill/add/intake.md
CHANGED
|
@@ -56,6 +56,22 @@ request, emit ONE of:
|
|
|
56
56
|
When confirmed, record the `rationale` in the artifact you create or affect — the new MILESTONE.md
|
|
57
57
|
goal/body, the new TASK.md, or a note in the affected TASK.md — never in state.json.
|
|
58
58
|
|
|
59
|
+
## Roadmap — a request that is several milestones
|
|
60
|
+
|
|
61
|
+
Some requests decompose into **N>1 milestones of the same line** — a roadmap, not one milestone.
|
|
62
|
+
Don't create only the first and lose the rest. Instead:
|
|
63
|
+
|
|
64
|
+
1. **Propose** the roadmap — the ordered milestone list, each with a one-line goal. (AI proposes.)
|
|
65
|
+
2. **Confirm** — the human confirms the roadmap before anything is created. Never auto-create N
|
|
66
|
+
milestones unprompted — the intake floor (`ask_human`) still holds.
|
|
67
|
+
3. **Create** all N on confirm — the first with `add.py new-milestone <slug>` (active), the rest
|
|
68
|
+
with `add.py new-milestone --queued <slug>` (status `queued`, not focused).
|
|
69
|
+
4. **Promote** one at a time — as each is started, `add.py activate <slug>` flips it queued→active.
|
|
70
|
+
You work one active milestone at a time; the queue is the agreed backlog, surfaced at resume.
|
|
71
|
+
|
|
72
|
+
This is NOT `split_required`: that code is for a request spanning **different buckets** (propose the
|
|
73
|
+
smallest correctly-sized set). A roadmap is several milestones of the **same line**, created queued.
|
|
74
|
+
|
|
59
75
|
## Worked examples (from this project's own history)
|
|
60
76
|
|
|
61
77
|
| request | bucket | rationale |
|
package/tooling/add.py
CHANGED
|
@@ -164,16 +164,20 @@ from add_engine.identity import ( # re-exported for `add.<name>` attr c
|
|
|
164
164
|
)
|
|
165
165
|
|
|
166
166
|
|
|
167
|
-
def _my_work(state: dict, me: dict) -> list[dict]:
|
|
168
|
-
"""The "my work" lens (multi-active-UX):
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
def _my_work(state: dict, me: dict, scope_all: bool = False) -> list[dict]:
|
|
168
|
+
"""The "my work" lens (multi-active-UX): the NOT-done tasks whose owner OR assignee is `me`.
|
|
169
|
+
By default the lens is the active SET; `scope_all=True` (mine-all-lens) widens it to EVERY
|
|
170
|
+
milestone plus loose (milestone-less) tasks. Returns ordered rows {slug, milestone, phase,
|
|
171
|
+
role} with role in {owner, assignee, both}, sorted by active-milestone order then slug
|
|
172
|
+
(non-active/loose sort after the active block, then by slug). PURE · no I/O."""
|
|
171
173
|
active = list(state.get("active_milestones") or [])
|
|
172
174
|
active_set = set(active)
|
|
173
175
|
tasks = state.get("tasks") if isinstance(state.get("tasks"), dict) else {}
|
|
174
176
|
rows: list[dict] = []
|
|
175
177
|
for slug, t in tasks.items():
|
|
176
|
-
if not isinstance(t, dict) or
|
|
178
|
+
if not isinstance(t, dict) or _task_done(t):
|
|
179
|
+
continue
|
|
180
|
+
if not scope_all and t.get("milestone") not in active_set:
|
|
177
181
|
continue
|
|
178
182
|
owns = identity._actor_matches(t.get("owner"), me)
|
|
179
183
|
assigned = identity._actor_matches(t.get("assignee"), me)
|
|
@@ -461,6 +465,74 @@ def cmd_drop_delta(args: argparse.Namespace) -> None:
|
|
|
461
465
|
print(_next_footer(root, state))
|
|
462
466
|
|
|
463
467
|
|
|
468
|
+
# a §3 still carrying this template placeholder is NOT a drafted contract yet
|
|
469
|
+
_CONTRACT_TEMPLATE_RE = re.compile(r"<METHOD>")
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _next_freeze_version(state: dict, slug: str) -> str:
|
|
473
|
+
"""v1 on the first freeze; N+1 of the highest prior freeze version recorded on the
|
|
474
|
+
task's state record on a re-freeze (after a change request). PURE — reads state only."""
|
|
475
|
+
prior = ((state.get("tasks") or {}).get(slug) or {}).get("freeze") or {}
|
|
476
|
+
m = re.fullmatch(r"v(\d+)", str(prior.get("version", "")))
|
|
477
|
+
return f"v{int(m.group(1)) + 1}" if m else "v1"
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def cmd_freeze(args: argparse.Namespace) -> None:
|
|
481
|
+
"""The §3 contract-freeze write command — the 5th engine-WRITTEN human approval (task
|
|
482
|
+
freeze-actor-stamp), joining lock · gate · milestone-done · release. Flips the target
|
|
483
|
+
task's §3 `Status: DRAFT` -> `FROZEN @ vN — approved by <name>` AND records a structured
|
|
484
|
+
actor on the task's state record (mirrors cmd_lock's `setup.actor`), so the audit trail
|
|
485
|
+
has no hole at freeze. The human RUNS it as their approval — never pre-stamped.
|
|
486
|
+
|
|
487
|
+
validate-then-write: every refusal fires before any write. Writes TASK.md first, then
|
|
488
|
+
state; a crash between degrades to today's legacy text-only freeze (never corrupt state),
|
|
489
|
+
design-for-failure."""
|
|
490
|
+
root = _require_root()
|
|
491
|
+
state = load_state(root)
|
|
492
|
+
raw_slug = getattr(args, "slug", None)
|
|
493
|
+
if not raw_slug and not _active_task(state):
|
|
494
|
+
_die("no_active_task: no task given and no active task is set")
|
|
495
|
+
slug = _resolve_task(state, raw_slug) # unknown slug -> _die
|
|
496
|
+
task_md = root / "tasks" / slug / "TASK.md"
|
|
497
|
+
text = task_md.read_text(encoding="utf-8")
|
|
498
|
+
raw3 = _phase_spans(text).get(3, "")
|
|
499
|
+
phase = (state["tasks"].get(slug) or {}).get("phase", "specify")
|
|
500
|
+
# --- validate (no writes); error precedence: frozen -> not-drafted -> unflagged ---
|
|
501
|
+
if _contract_frozen(raw3):
|
|
502
|
+
_die(f"already_frozen: {slug}'s §3 is already FROZEN — re-freeze only via a change "
|
|
503
|
+
f"request back to SPECIFY")
|
|
504
|
+
if _phase_index(phase) < _phase_index("contract") or _CONTRACT_TEMPLATE_RE.search(raw3):
|
|
505
|
+
_die(f"contract_not_drafted: {slug}'s §3 is not a drafted contract yet — reach the "
|
|
506
|
+
f"`contract` phase and replace the template before freezing")
|
|
507
|
+
if not _flag_well_formed(raw3):
|
|
508
|
+
_die(f"unflagged_freeze: {slug}'s §3 must surface a well-formed lowest-confidence flag "
|
|
509
|
+
f"('Least-sure flag surfaced at freeze:' + a [part] tag) before it freezes")
|
|
510
|
+
# --- write ---
|
|
511
|
+
ver = _next_freeze_version(state, slug)
|
|
512
|
+
who = args.by or identity._actor_stamp(state)["name"]
|
|
513
|
+
# flip the `Status: DRAFT` line WITHIN the §3 region only — a bare `Status: DRAFT` in
|
|
514
|
+
# §1/§2 prose must never be frozen by mistake (refute-read finding). §3 span runs from
|
|
515
|
+
# its `## 3 ·` heading to the next `## `/`---`/EOF (same boundary as _phase_spans).
|
|
516
|
+
h3 = re.search(r"(?m)^##\s*3\s*·.*$", text)
|
|
517
|
+
if not h3:
|
|
518
|
+
_die(f"contract_not_drafted: {slug}'s TASK.md has no §3 CONTRACT section")
|
|
519
|
+
seg_start = h3.end()
|
|
520
|
+
nxt = re.search(r"(?m)^(?:##\s|---\s*$)", text[seg_start:])
|
|
521
|
+
seg_end = seg_start + (nxt.start() if nxt else len(text) - seg_start)
|
|
522
|
+
new_seg, n = re.subn(r"(?m)^(\s*)Status:\s*DRAFT\s*$",
|
|
523
|
+
lambda m: f"{m.group(1)}Status: FROZEN @ {ver} — approved by {who}",
|
|
524
|
+
text[seg_start:seg_end], count=1)
|
|
525
|
+
if n == 0:
|
|
526
|
+
_die(f"contract_not_drafted: {slug}'s §3 has no 'Status: DRAFT' line to freeze")
|
|
527
|
+
new_text = text[:seg_start] + new_seg + text[seg_end:]
|
|
528
|
+
_atomic_write(task_md, new_text) # TASK.md first (audit source of truth)
|
|
529
|
+
state["tasks"][slug]["freeze"] = {"version": ver, "frozen_at": _now(),
|
|
530
|
+
"approved_by": who, "actor": identity._actor_stamp(state)}
|
|
531
|
+
save_state(root, state)
|
|
532
|
+
print(f"froze §3 of {slug} @ {ver} — approved by {who}")
|
|
533
|
+
print(_next_footer(root, state))
|
|
534
|
+
|
|
535
|
+
|
|
464
536
|
def _parse_deps(raw: str | None) -> list[str]:
|
|
465
537
|
if not raw:
|
|
466
538
|
return []
|
|
@@ -1303,6 +1375,14 @@ def cmd_status(args: argparse.Namespace) -> None:
|
|
|
1303
1375
|
_so = _fmt_actor(_owner_rec) if _owner_rec.get("name") else ""
|
|
1304
1376
|
_own_frag = f" · owner: {_so}" if _so else ""
|
|
1305
1377
|
print(f" {_mk} {_m:<20} task={_tk or '(none)'} phase={_ph}{_tag}{_own_frag}")
|
|
1378
|
+
# queued backlog (queue-resume-surface): surface milestones awaiting promotion so a
|
|
1379
|
+
# multi-milestone session resumes cleanly — `active` is what you're on, `queued` is what's
|
|
1380
|
+
# next. ADDITIVE + present-only: silent when zero queued (byte-identical), exactly like the
|
|
1381
|
+
# release/loose/streams cues; reads state, writes nothing, changes no command decision.
|
|
1382
|
+
_queued = [ms for ms, m in milestones.items() if m.get("status") == "queued"]
|
|
1383
|
+
if _queued:
|
|
1384
|
+
print(f"queued : {len(_queued)} milestone(s) next — {', '.join(_queued)}")
|
|
1385
|
+
print(f" promote next: add.py activate {_queued[0]}")
|
|
1306
1386
|
# surface the active task's autonomy level (task explicit-autonomy-dial) so the human
|
|
1307
1387
|
# reads the throttle every session; "unset" when no explicit `autonomy:` line is present.
|
|
1308
1388
|
if active and active in tasks:
|
|
@@ -2239,6 +2319,39 @@ def _doctor_findings(root: Path) -> list[str]:
|
|
|
2239
2319
|
if m is not None and m not in milestones:
|
|
2240
2320
|
findings.append(f"task '{slug}' references missing milestone '{m}' — fix: set its "
|
|
2241
2321
|
"milestone to a real one (or none)")
|
|
2322
|
+
# value-domain checks (doctor-value-checks): gate/phase enum (required) · owner/assignee
|
|
2323
|
+
# shape (optional) · archived consistency. Present-but-invalid + missing-required only;
|
|
2324
|
+
# absent owner/assignee is fine. Pure/total — .get-guarded, isinstance-checked, never raises.
|
|
2325
|
+
for slug, t in tasks.items():
|
|
2326
|
+
if not isinstance(t, dict):
|
|
2327
|
+
continue
|
|
2328
|
+
g = t.get("gate")
|
|
2329
|
+
if g is None:
|
|
2330
|
+
findings.append(f"task '{slug}' is missing its gate — fix: one of {', '.join(GATES)}")
|
|
2331
|
+
elif g not in GATES:
|
|
2332
|
+
findings.append(f"task '{slug}' has invalid gate '{g}' — fix: one of {', '.join(GATES)}")
|
|
2333
|
+
p = t.get("phase")
|
|
2334
|
+
if p is None:
|
|
2335
|
+
findings.append(f"task '{slug}' is missing its phase — fix: one of {', '.join(PHASES)}")
|
|
2336
|
+
elif p not in PHASES:
|
|
2337
|
+
findings.append(f"task '{slug}' has invalid phase '{p}' — fix: one of {', '.join(PHASES)}")
|
|
2338
|
+
for role in ("owner", "assignee"):
|
|
2339
|
+
v = t.get(role)
|
|
2340
|
+
if v is not None and not (isinstance(v, dict) and isinstance(v.get("name"), str) and v.get("name")):
|
|
2341
|
+
findings.append(f"task '{slug}' has a malformed {role} — fix: an actor object "
|
|
2342
|
+
"{name, email, source} or remove it")
|
|
2343
|
+
archived = state.get("archived") if isinstance(state.get("archived"), list) else []
|
|
2344
|
+
for a in archived:
|
|
2345
|
+
if not isinstance(a, dict):
|
|
2346
|
+
continue
|
|
2347
|
+
aslug = a.get("slug")
|
|
2348
|
+
if aslug is not None and aslug in milestones:
|
|
2349
|
+
findings.append(f"archived milestone '{aslug}' is also a live milestone — fix: remove "
|
|
2350
|
+
"the live duplicate or the archived entry")
|
|
2351
|
+
ts = a.get("task_slugs")
|
|
2352
|
+
if isinstance(ts, list) and isinstance(a.get("tasks"), int) and a.get("tasks") != len(ts):
|
|
2353
|
+
findings.append(f"archived milestone '{aslug}' task count {a.get('tasks')} ≠ {len(ts)} "
|
|
2354
|
+
"listed — fix: reconcile its task_slugs")
|
|
2242
2355
|
return findings
|
|
2243
2356
|
|
|
2244
2357
|
|
|
@@ -2259,25 +2372,29 @@ def cmd_doctor(args: argparse.Namespace) -> None:
|
|
|
2259
2372
|
|
|
2260
2373
|
|
|
2261
2374
|
def cmd_mine(args: argparse.Namespace) -> None:
|
|
2262
|
-
"""Read-only `add.py mine`:
|
|
2263
|
-
|
|
2264
|
-
|
|
2375
|
+
"""Read-only `add.py mine`: the not-done tasks owned-by or assigned-to the resolved actor
|
|
2376
|
+
(`_whoami`, or `--actor "Name <email>"`). Default lens is the active SET; `--all` widens it
|
|
2377
|
+
to EVERY milestone plus loose (milestone-less) tasks. Text or `--json`. An empty queue is a
|
|
2378
|
+
plain exit-0 line, not an error. NEVER writes state."""
|
|
2265
2379
|
root = find_root()
|
|
2266
2380
|
if root is None:
|
|
2267
2381
|
_die("no_project")
|
|
2268
2382
|
state = load_state(root)
|
|
2269
2383
|
me = identity._parse_actor_arg(args.actor) if getattr(args, "actor", None) else identity._whoami(state)
|
|
2270
|
-
|
|
2384
|
+
scope_all = getattr(args, "all", False)
|
|
2385
|
+
rows = _my_work(state, me, scope_all=scope_all)
|
|
2271
2386
|
if getattr(args, "json", False):
|
|
2272
2387
|
print(json.dumps({"actor": me, "tasks": rows}))
|
|
2273
2388
|
return
|
|
2389
|
+
scope = "all" if scope_all else "active"
|
|
2274
2390
|
who = _fmt_actor(me) or me.get("name", "you")
|
|
2275
2391
|
if not rows:
|
|
2276
|
-
print(f"mine: no open tasks for {who} across
|
|
2392
|
+
print(f"mine: no open tasks for {who} across {scope} milestones")
|
|
2277
2393
|
return
|
|
2278
|
-
print(f"mine: {who} — {len(rows)} open task(s) across
|
|
2394
|
+
print(f"mine: {who} — {len(rows)} open task(s) across {scope} milestones:")
|
|
2279
2395
|
for r in rows:
|
|
2280
|
-
|
|
2396
|
+
loc = f"[{r['milestone']}]" if r["milestone"] else "[loose]"
|
|
2397
|
+
print(f" {r['slug']:<24} {loc} phase={r['phase']} ({r['role']})")
|
|
2281
2398
|
|
|
2282
2399
|
|
|
2283
2400
|
# ---------------------------------------------------------------------------
|
|
@@ -2424,6 +2541,12 @@ def cmd_new_milestone(args: argparse.Namespace) -> None:
|
|
|
2424
2541
|
slug = args.slug
|
|
2425
2542
|
if not slug.replace("-", "").replace("_", "").isalnum():
|
|
2426
2543
|
_die("bad_slug")
|
|
2544
|
+
# Prefer a short DESCRIPTIVE slug over a bare version (v2, v1-1, 1.2): a descriptive
|
|
2545
|
+
# name keeps the milestones list legible. Advisory only — never blocks (matches the
|
|
2546
|
+
# engine's `note:` convention); a deliberate version slug still creates.
|
|
2547
|
+
if re.match(r"^v?\d+([._-]\d+)*$", slug, re.IGNORECASE):
|
|
2548
|
+
print(f"note: slug '{slug}' looks like a bare version — prefer a short "
|
|
2549
|
+
f"descriptive name (e.g. 'payment-retries'). Creating anyway.")
|
|
2427
2550
|
state.setdefault("milestones", {})
|
|
2428
2551
|
mdir = root / "milestones" / slug
|
|
2429
2552
|
mfile = mdir / MILESTONE_FILE
|
|
@@ -2431,17 +2554,24 @@ def cmd_new_milestone(args: argparse.Namespace) -> None:
|
|
|
2431
2554
|
_die("milestone_exists")
|
|
2432
2555
|
mdir.mkdir(parents=True, exist_ok=True)
|
|
2433
2556
|
title = args.title or slug.replace("-", " ").replace("_", " ").title()
|
|
2557
|
+
# One _now() instant feeds BOTH the MILESTONE.md render and the state record, so the
|
|
2558
|
+
# human-facing `created:` is a full ISO timestamp provably equal to state.json.
|
|
2559
|
+
now = _now()
|
|
2434
2560
|
_atomic_write(mfile, _render_template(
|
|
2435
2561
|
"MILESTONE.md", title=title, goal=args.goal or "<goal>",
|
|
2436
|
-
stage=args.stage, date=
|
|
2562
|
+
stage=args.stage, date=now))
|
|
2437
2563
|
# confirm-parent gate (OPT-IN, mirrors `init --await-lock`): `--await-confirm` seeds the
|
|
2438
2564
|
# milestone UNCONFIRMED so new-task is held until `add.py milestone-confirm`. WITHOUT the flag
|
|
2439
2565
|
# NO `confirmed` key is written → grandfathered-confirmed → no gate (so the existing engine
|
|
2440
2566
|
# tests stay byte-green). The guided skill flow passes the flag at the human-review point.
|
|
2441
2567
|
await_confirm = bool(getattr(args, "await_confirm", False))
|
|
2568
|
+
# --queued (OPT-IN): create the milestone non-active (status=queued) without stealing focus.
|
|
2569
|
+
# The active set is left UNCHANGED so the default path (no flag) stays byte-identical. Promote
|
|
2570
|
+
# later with `activate` (queued→active). Foundation for roadmap intake (1 active + N queued).
|
|
2571
|
+
queued = bool(getattr(args, "queued", False))
|
|
2442
2572
|
record = {
|
|
2443
2573
|
"title": title, "goal": args.goal or "", "stage": args.stage,
|
|
2444
|
-
"status": "active", "created":
|
|
2574
|
+
"status": "queued" if queued else "active", "created": now, "updated": now,
|
|
2445
2575
|
}
|
|
2446
2576
|
if await_confirm:
|
|
2447
2577
|
# `await_confirm` is the STABLE opt-in marker (set ONLY here, at creation). `confirmed`
|
|
@@ -2449,11 +2579,22 @@ def cmd_new_milestone(args: argparse.Namespace) -> None:
|
|
|
2449
2579
|
# milestone too, so a later build-entry gate must key on `await_confirm`, not `confirmed`.
|
|
2450
2580
|
record.update(confirmed=False, confirmed_at=None, confirmed_by=None, await_confirm=True)
|
|
2451
2581
|
state["milestones"][slug] = record
|
|
2452
|
-
|
|
2582
|
+
if not queued:
|
|
2583
|
+
# PRESERVE the active SET (new-milestone-add-focus): ADD this milestone + focus it, rather
|
|
2584
|
+
# than REPLACING the set and evicting the others. Single-active is identical ([] -> [slug]);
|
|
2585
|
+
# a user who already had P active now keeps P active alongside the new primary.
|
|
2586
|
+
_activate_milestone(state, slug)
|
|
2453
2587
|
save_state(root, state)
|
|
2454
2588
|
print(f"created milestone '{slug}' -> {mfile}")
|
|
2455
|
-
|
|
2456
|
-
|
|
2589
|
+
if queued:
|
|
2590
|
+
print(f"queued (not active) — promote it with: add.py activate {slug}")
|
|
2591
|
+
# surface the recorded confirm gate for a queued+await_confirm milestone (queued-await-confirm-hint):
|
|
2592
|
+
# additive — prints ONLY when await_confirm, so plain `--queued` output stays byte-identical.
|
|
2593
|
+
if await_confirm:
|
|
2594
|
+
print(f" (unconfirmed — after promote: add.py milestone-confirm {slug})")
|
|
2595
|
+
else:
|
|
2596
|
+
print("active milestone set." + ("" if not await_confirm else
|
|
2597
|
+
" (unconfirmed — show the MILESTONE.md, then: add.py milestone-confirm " + slug + ")"))
|
|
2457
2598
|
print(_next_footer(root, state)) # converges the old "Decompose it into tasks: …" hint
|
|
2458
2599
|
|
|
2459
2600
|
|
|
@@ -2542,25 +2683,34 @@ def cmd_ready(args: argparse.Namespace) -> None:
|
|
|
2542
2683
|
|
|
2543
2684
|
|
|
2544
2685
|
def _wave_schedule(state: dict, mslug: str) -> dict:
|
|
2545
|
-
"""
|
|
2546
|
-
|
|
2686
|
+
"""One-element wrapper: a single milestone's schedule is the merge over just [mslug].
|
|
2687
|
+
Output is byte-identical to the historical per-milestone scheduler (the suite is the oracle)."""
|
|
2688
|
+
return _wave_schedule_merged(state, [mslug])
|
|
2689
|
+
|
|
2690
|
+
|
|
2691
|
+
def _wave_schedule_merged(state: dict, mslugs: list[str]) -> dict:
|
|
2692
|
+
"""Pure, total: derive ONE DAG schedule over the UNION of open members across `mslugs`
|
|
2693
|
+
(a single-element list is the historical per-milestone case) — never mutates, never raises
|
|
2694
|
+
on dict input. Returns one of:
|
|
2547
2695
|
{"cycle": [slug, ...]} — unschedulable cycle
|
|
2548
2696
|
{"waves", "critical_path", "critical_path_len", "tiers", "blocked"} — a schedule
|
|
2549
2697
|
|
|
2550
2698
|
A dep is SATISFIED (does not block) if it is archived or `_task_done` — the SAME
|
|
2551
|
-
predicate cmd_ready uses. A not-done dep that is an OPEN MEMBER of
|
|
2552
|
-
forces a later wave
|
|
2553
|
-
is
|
|
2554
|
-
|
|
2555
|
-
Tier is advisory: `top` on the critical
|
|
2699
|
+
predicate cmd_ready uses. A not-done dep that is an OPEN MEMBER of ANY target milestone
|
|
2700
|
+
forces a later wave (so a cross-milestone dep ORDERS, it does not block). A not-done dep
|
|
2701
|
+
that is NOT an open member of any target (external/unknown) is UNSATISFIABLE here -> the
|
|
2702
|
+
task is `blocked`, never scheduled. Critical path is the longest chain (most tasks) through
|
|
2703
|
+
the scheduled sub-DAG; ties break by sorted slug. Tier is advisory: `top` on the critical
|
|
2704
|
+
path, `mid` elsewhere (scheduled tasks only)."""
|
|
2556
2705
|
tasks = state.get("tasks") or {}
|
|
2557
2706
|
archived = _archived_task_slugs(state)
|
|
2707
|
+
targetset = set(mslugs)
|
|
2558
2708
|
|
|
2559
2709
|
def _ok(d: str) -> bool: # satisfied externally / already done
|
|
2560
2710
|
return d in archived or (d in tasks and _task_done(tasks[d]))
|
|
2561
2711
|
|
|
2562
2712
|
open_members = {s: t for s, t in tasks.items()
|
|
2563
|
-
if t.get("milestone")
|
|
2713
|
+
if t.get("milestone") in targetset and not _task_done(t)}
|
|
2564
2714
|
|
|
2565
2715
|
# partition open members into blocked vs schedulable — to a FIXED POINT, so blocking
|
|
2566
2716
|
# propagates transitively: a task is blocked if any dep is unsatisfiable here, where
|
|
@@ -2661,16 +2811,71 @@ def _wave_block_lines(state: dict, mslug: str, sched: dict) -> list[str]:
|
|
|
2661
2811
|
return lines
|
|
2662
2812
|
|
|
2663
2813
|
|
|
2814
|
+
def _wave_block_lines_merged(state: dict, mslugs: list[str], sched: dict) -> list[str]:
|
|
2815
|
+
"""The text lines `waves --merge` renders for ONE unified schedule over the milestone SET:
|
|
2816
|
+
a `merged: …` header naming the set + each scheduled task tagged with its `[milestone]` so
|
|
2817
|
+
cross-milestone tasks are unambiguous. Critical-path / tier-hint / blocked lines mirror
|
|
2818
|
+
`_wave_block_lines`."""
|
|
2819
|
+
n = len(mslugs)
|
|
2820
|
+
lines = [f"merged: {' + '.join(mslugs)} ({n} milestone{'s' if n != 1 else ''})"]
|
|
2821
|
+
if not sched["waves"]:
|
|
2822
|
+
if sched["blocked"]:
|
|
2823
|
+
for s in sched["blocked"]:
|
|
2824
|
+
lines.append(f"blocked: {s} (waiting on {', '.join(sched['blocked'][s])})")
|
|
2825
|
+
else:
|
|
2826
|
+
lines.append("all tasks done — nothing to schedule")
|
|
2827
|
+
return lines
|
|
2828
|
+
scheduled_set = {x for w in sched["waves"] for x in w}
|
|
2829
|
+
for i, wave in enumerate(sched["waves"], start=1):
|
|
2830
|
+
parts = []
|
|
2831
|
+
for s in wave:
|
|
2832
|
+
label = f"{s} [{state['tasks'][s].get('milestone')}]"
|
|
2833
|
+
md = sorted(d for d in (state["tasks"][s].get("depends_on") or [])
|
|
2834
|
+
if d in scheduled_set)
|
|
2835
|
+
parts.append(f"{label} (deps: {', '.join(md)})" if md else label)
|
|
2836
|
+
lines.append(f"wave {i}: {', '.join(parts)}")
|
|
2837
|
+
crit = sched["critical_path"]
|
|
2838
|
+
lines.append(f"critical path: {' → '.join(crit)} ({sched['critical_path_len']} tasks)")
|
|
2839
|
+
tops = [s for s, tier in sched["tiers"].items() if tier == "top"]
|
|
2840
|
+
mids = [s for s, tier in sched["tiers"].items() if tier == "mid"]
|
|
2841
|
+
lines.append(f"tier hint: top → {', '.join(tops)}; mid → {', '.join(mids) or '(none)'}")
|
|
2842
|
+
for s in sched["blocked"]:
|
|
2843
|
+
lines.append(f"blocked: {s} (waiting on {', '.join(sched['blocked'][s])})")
|
|
2844
|
+
return lines
|
|
2845
|
+
|
|
2846
|
+
|
|
2664
2847
|
def cmd_waves(args: argparse.Namespace) -> None:
|
|
2665
2848
|
"""READ-ONLY DAG scheduler: print the topological waves, critical path, advisory tier hint,
|
|
2666
|
-
and blocked set. With no --milestone it spans EVERY active milestone (cross-active-waves)
|
|
2667
|
-
a single target / --milestone renders byte-identically.
|
|
2849
|
+
and blocked set. With no --milestone it spans EVERY active milestone (cross-active-waves)
|
|
2850
|
+
as SEPARATE streams; a single target / --milestone renders byte-identically. With --merge it
|
|
2851
|
+
unifies the active SET into ONE schedule so cross-milestone deps order, not block.
|
|
2852
|
+
Writes nothing; no `next:` footer."""
|
|
2668
2853
|
is_json = getattr(args, "json", False)
|
|
2669
2854
|
if is_json:
|
|
2670
2855
|
_, state = _load_state_for_json()
|
|
2671
2856
|
else:
|
|
2672
2857
|
state = load_state(_require_root())
|
|
2673
2858
|
mslug_arg = getattr(args, "milestone", None)
|
|
2859
|
+
if getattr(args, "merge", False):
|
|
2860
|
+
if mslug_arg: # explicit target → a 1-milestone merge (NOT a conflict)
|
|
2861
|
+
if mslug_arg not in (state.get("milestones") or {}):
|
|
2862
|
+
_die(f"unknown_milestone: '{mslug_arg}' is not a milestone in this project")
|
|
2863
|
+
targets = [mslug_arg]
|
|
2864
|
+
else:
|
|
2865
|
+
primary = _active_milestone(state)
|
|
2866
|
+
if not primary:
|
|
2867
|
+
_die("no_active_milestone: no active milestone and no --milestone given")
|
|
2868
|
+
targets = [primary] + [m for m in (state.get("active_milestones") or [])
|
|
2869
|
+
if m != primary]
|
|
2870
|
+
sched = _wave_schedule_merged(state, targets)
|
|
2871
|
+
if "cycle" in sched:
|
|
2872
|
+
_die(f"dependency_cycle: not-done deps form a cycle "
|
|
2873
|
+
f"({' -> '.join(sched['cycle'])}) — no valid schedule")
|
|
2874
|
+
if is_json:
|
|
2875
|
+
print(json.dumps({"merged": targets, **sched}))
|
|
2876
|
+
else:
|
|
2877
|
+
print("\n".join(_wave_block_lines_merged(state, targets, sched)))
|
|
2878
|
+
return
|
|
2674
2879
|
if mslug_arg:
|
|
2675
2880
|
targets = [mslug_arg] # explicit single target — unchanged
|
|
2676
2881
|
else:
|
|
@@ -2938,6 +3143,11 @@ def cmd_activate(args: argparse.Namespace) -> None:
|
|
|
2938
3143
|
_die("unknown_milestone")
|
|
2939
3144
|
if state["milestones"][slug].get("status") == "done":
|
|
2940
3145
|
_die("milestone_done")
|
|
3146
|
+
# PROMOTE a queued milestone: activating it flips queued→active (human-gated promotion —
|
|
3147
|
+
# the chosen verb, reusing `activate` rather than a separate `promote`). An already-active
|
|
3148
|
+
# milestone is just refocused (status unchanged), keeping the default path byte-identical.
|
|
3149
|
+
if state["milestones"][slug].get("status") == "queued":
|
|
3150
|
+
state["milestones"][slug]["status"] = "active"
|
|
2941
3151
|
_activate_milestone(state, slug)
|
|
2942
3152
|
save_state(root, state)
|
|
2943
3153
|
print(f"activated '{slug}' — active: {', '.join(state['active_milestones'])}")
|
|
@@ -5242,6 +5452,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
5242
5452
|
pl.add_argument("--json", action="store_true", help="emit one JSON object instead of text")
|
|
5243
5453
|
pl.set_defaults(func=cmd_lock)
|
|
5244
5454
|
|
|
5455
|
+
pfz = sub.add_parser("freeze",
|
|
5456
|
+
help="freeze a task's §3 contract (the human approval) — stamps "
|
|
5457
|
+
"FROZEN @ vN + a structured actor on the task record")
|
|
5458
|
+
pfz.add_argument("slug", nargs="?", default=None,
|
|
5459
|
+
help="task to freeze (default: the active task)")
|
|
5460
|
+
pfz.add_argument("--by", default=None, help="approver name (default: the resolved actor)")
|
|
5461
|
+
pfz.set_defaults(func=cmd_freeze)
|
|
5462
|
+
|
|
5245
5463
|
pwho = sub.add_parser("whoami",
|
|
5246
5464
|
help="show / set the git-native actor (git config -> OS user; --name to override)")
|
|
5247
5465
|
# --name (set) and --unset (clear) are mutually exclusive — argparse rejects the
|
|
@@ -5301,6 +5519,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
5301
5519
|
pm.add_argument("--goal", default=None, help="one-sentence outcome")
|
|
5302
5520
|
pm.add_argument("--stage", default="mvp", choices=STAGES)
|
|
5303
5521
|
pm.add_argument("--force", action="store_true", help="overwrite MILESTONE.md if present")
|
|
5522
|
+
pm.add_argument("--queued", action="store_true",
|
|
5523
|
+
help="create the milestone QUEUED (status=queued), not active: it is recorded "
|
|
5524
|
+
"and its MILESTONE.md written, but the active focus is unchanged. Promote it "
|
|
5525
|
+
"later with `activate <slug>`. Foundation for roadmap intake (1 active + N queued).")
|
|
5304
5526
|
pm.add_argument("--await-confirm", action="store_true",
|
|
5305
5527
|
help="opt into the confirm-parent gate: seed the milestone unconfirmed so "
|
|
5306
5528
|
"new-task is held until `milestone-confirm` (mirrors `init --await-lock`); "
|
|
@@ -5321,6 +5543,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
5321
5543
|
"waves + critical path + advisory tier hint")
|
|
5322
5544
|
pwa.add_argument("--milestone", default=None,
|
|
5323
5545
|
help="milestone slug to schedule (default: the active milestone)")
|
|
5546
|
+
pwa.add_argument("--merge", action="store_true",
|
|
5547
|
+
help="unify the active SET into ONE schedule (cross-milestone deps order, not block)")
|
|
5324
5548
|
pwa.add_argument("--json", action="store_true", help="machine-readable JSON output")
|
|
5325
5549
|
pwa.set_defaults(func=cmd_waves)
|
|
5326
5550
|
|
|
@@ -5441,6 +5665,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
5441
5665
|
pmine.add_argument("--actor", default=None, metavar="\"Name <email>\"",
|
|
5442
5666
|
help="inspect another actor's queue instead of your own")
|
|
5443
5667
|
pmine.add_argument("--json", action="store_true", help="emit one JSON object instead of text")
|
|
5668
|
+
pmine.add_argument("--all", action="store_true",
|
|
5669
|
+
help="widen past the active SET: every milestone + loose tasks, not just active")
|
|
5444
5670
|
pmine.set_defaults(func=cmd_mine)
|
|
5445
5671
|
|
|
5446
5672
|
pwv = sub.add_parser("wave-verify",
|
|
@@ -31,6 +31,17 @@ The design intent the AI drafts UI against — the persona, the one job, the fee
|
|
|
31
31
|
- **Design principles** — <!-- 3–5 lines, e.g. "one primary action per screen" · "never hide state" -->
|
|
32
32
|
- **Accessibility floor** — <!-- e.g. WCAG AA contrast · focus-visible · hit-target ≥ 44px -->
|
|
33
33
|
|
|
34
|
+
## Design intake
|
|
35
|
+
|
|
36
|
+
The per-design picks the `design-intake` beat collects (see the `add` skill's `design.md`).
|
|
37
|
+
These are project **defaults**; a screen records only its **overrides** in its prototype note.
|
|
38
|
+
VISUAL DESIGN draws its color/type from **## Identity** above — don't restate the brand here.
|
|
39
|
+
|
|
40
|
+
- **Fidelity** — <!-- lo-fi wireframe | hi-fi mockup | production — how far this design renders -->
|
|
41
|
+
- **Concept** — <!-- the design idea / mood / direction, in a line -->
|
|
42
|
+
- **Layout** — <!-- the structure / grid / hierarchy the screens lean on -->
|
|
43
|
+
- **Visual design** — <!-- spacing scale · imagery style (color · type come from ## Identity) -->
|
|
44
|
+
|
|
34
45
|
## Screens
|
|
35
46
|
|
|
36
47
|
An index of the app's screens — one `design/prototypes/<name>.json` per screen (a flat
|