@pilotspace/add 1.11.0 → 1.13.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/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): across ALL active milestones, the NOT-done tasks
169
- whose owner OR assignee is `me`. Returns ordered rows {slug, milestone, phase, role} with
170
- role in {owner, assignee, both}, sorted by active-milestone order then slug. PURE · no I/O."""
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 t.get("milestone") not in active_set or _task_done(t):
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)
@@ -235,6 +239,112 @@ def _stamp_gate_record(root: Path, state: dict, slug: str, outcome: str) -> None
235
239
  _atomic_write(f, new)
236
240
 
237
241
 
242
+ def _stamp_adr_record(root: Path, state: dict, slug: str) -> None:
243
+ """Write-back (adr-at-observe): HARVEST a §7 `### Decisions (ADR)` block from the actor-stamps
244
+ ALREADY in the task — §1 framing (AI) · §3 freeze (human) · §5 strategy-actually-used (AI) · §6
245
+ gate (human|AI by autonomy). HARVEST-not-author: every rendered line is sourced from an existing
246
+ stamp; the engine invents no decision content (NO-EXEC). GRANDFATHER like _stamp_gate_record:
247
+ fills ONLY while the block still holds its `<harvested…>` placeholder line; a resolved/absent
248
+ block or an unreadable file -> a byte-identical no-op (legacy + fast tasks untouched). NEVER
249
+ raises: any per-source parse fault renders "<unrecorded>". Called from cmd_gate AFTER
250
+ _stamp_gate_record (so §6 is already mirrored) and AFTER save_state (state is the source of
251
+ truth; the file only mirrors it, so a write fault never loses the verdict).
252
+
253
+ §7-OBSERVE-scoped (INV-7): the placeholder is matched ONLY inside the "## 7 · OBSERVE" section,
254
+ so a "<harvested at done…>" line elsewhere — e.g. a §3 contract that ILLUSTRATES this very
255
+ feature — is never touched (a file-wide first-match would corrupt the frozen contract; caught
256
+ by dogfooding adr-harvest on itself)."""
257
+ f = root / "tasks" / slug / "TASK.md"
258
+ try:
259
+ text = f.read_text(encoding="utf-8")
260
+ except OSError:
261
+ return # unreadable -> no-op
262
+ sec7 = re.search(r"(?ms)^## 7 · OBSERVE\b.*?(?=\n## \d+ ·|\Z)", text)
263
+ if not sec7:
264
+ return # no §7 OBSERVE (fast / legacy) -> no-op
265
+ m = re.search(r"(?m)^<harvested at done[^\n]*>$", sec7.group(0))
266
+ if not m:
267
+ return # resolved (hand-edited) or absent -> grandfather no-op
268
+ ph_start, ph_end = sec7.start() + m.start(), sec7.start() + m.end()
269
+ UN = "<unrecorded>"
270
+ bodies = _raw_phase_bodies(root, slug)
271
+
272
+ def _framing(): # §1 -> [AI]: chosen + rejected
273
+ try:
274
+ m = re.search(r"(?m)^Framings weighed:[ \t]*(.+)$", bodies.get(1, ""))
275
+ if not m:
276
+ return UN, ""
277
+ chosen, rejected = UN, []
278
+ for p in (s.strip() for s in m.group(1).split("·") if s.strip()):
279
+ cm = re.match(r"(.*?)\s*\(chosen\b.*\)\s*$", p) # "(chosen)" OR "(chosen — rationale)"
280
+ if cm:
281
+ chosen = cm.group(1).strip() or UN
282
+ else:
283
+ rejected.append(p)
284
+ # an UNFILLED §1 is a "<chosen>" placeholder token — degrade to <unrecorded>, but a
285
+ # real framing that merely CONTAINS a "<" (e.g. quoting a type) is kept (faithful capture)
286
+ if chosen is UN or chosen.startswith("<"):
287
+ return UN, ""
288
+ return chosen, " · ".join(rejected)
289
+ except Exception:
290
+ return UN, ""
291
+
292
+ def _freeze(): # §3 -> [human]: "FROZEN @ vN — approved by NAME"
293
+ try:
294
+ m = re.search(r"(?m)^.*FROZEN @ (v\d+).*?approved by ([^\n<]+?)\s*$", bodies.get(3, ""))
295
+ if m:
296
+ return m.group(1), m.group(2).strip()
297
+ except Exception:
298
+ pass
299
+ t = ((state.get("tasks") or {}).get(slug) or {})
300
+ fr = t.get("freeze") or {}
301
+ ver = fr.get("version") or t.get("contract_version")
302
+ return (f"v{ver}" if ver else UN), (fr.get("by") or fr.get("actor") or UN)
303
+
304
+ def _strategy(): # §5 -> [AI]: the value, default "as planned"
305
+ try:
306
+ m = re.search(r"(?m)^Strategy actually used:[ \t]*(.+)$", bodies.get(5, ""))
307
+ if m:
308
+ # UNFILLED is the "<fill at …>" template token; a real value may legitimately
309
+ # contain "<" (quoting `<tag>`, "x < y") and must NOT degrade to the default
310
+ val = m.group(1).strip()
311
+ if val and not val.startswith("<fill"):
312
+ return val
313
+ except Exception:
314
+ pass
315
+ return "as planned"
316
+
317
+ def _gate(): # §6 -> [human|AI]: outcome + reviewer
318
+ try:
319
+ mo = re.search(r"(?m)^Outcome:[ \t]*(\S+)", bodies.get(6, ""))
320
+ outcome = mo.group(1) if mo else (((state.get("tasks") or {}).get(slug) or {}).get("gate") or UN)
321
+ mr = re.search(r"(?m)^Reviewed by:[ \t]*([^·\n<]+)", bodies.get(6, ""))
322
+ rev = mr.group(1).strip() if mr else UN
323
+ except Exception:
324
+ outcome, rev = UN, UN
325
+ am = re.search(r"(?m)^autonomy:[ \t]*(\w+)", text)
326
+ actor = "AI" if (am and am.group(1) == "auto") else "human"
327
+ return outcome, rev, actor
328
+
329
+ try:
330
+ chosen, rejected = _framing()
331
+ fver, fby = _freeze()
332
+ strat = _strategy()
333
+ outcome, rev, gate_actor = _gate()
334
+ except Exception:
335
+ return # never block the gate
336
+ rej = f"; rejected {rejected}" if rejected else ""
337
+ lines = [
338
+ f"- [AI] specify — chose {chosen}{rej}",
339
+ f"- [human] freeze — froze §3 @ {fver} (approved by {fby})",
340
+ f"- [AI] build — strategy used: {strat}",
341
+ f"- [{gate_actor}] verify — gate {outcome} (reviewed by {rev})",
342
+ ]
343
+ new = text[:ph_start] + "\n".join(lines) + text[ph_end:]
344
+ if new != text:
345
+ _atomic_write(f, new)
346
+
347
+
238
348
  # --- guidelines / CLAUDE.md-injection subsystem (moved to add_engine/guidelines.py) -
239
349
  from add_engine.guidelines import (
240
350
  _guideline_block, _inject_block, _rule_file_mode, _strip_inline_block,
@@ -461,6 +571,151 @@ def cmd_drop_delta(args: argparse.Namespace) -> None:
461
571
  print(_next_footer(root, state))
462
572
 
463
573
 
574
+ def _open_spec_delta_indices(text: str) -> list[int]:
575
+ """Every splitlines(keepends=True) index of an `[SPEC · open]` line (carry-delta --all). PURE.
576
+ A SPEC flip preserves line count + position, so these indices stay valid across sequential
577
+ flips."""
578
+ return [i for i, ln in enumerate(text.splitlines(keepends=True))
579
+ if (m := _SPEC_DELTA_RE.match(ln.rstrip("\n"))) and m.group(2) == "open"]
580
+
581
+
582
+ def cmd_carry_delta(args: argparse.Namespace) -> None:
583
+ """DEFER a task's open SPEC delta(s) non-lossily — `[SPEC · open]` -> `[SPEC · carried]`
584
+ + a ` [carried: <reason>]` stamp (delta-drain). A carried delta clears the release floor and
585
+ the `status` staleness count but SURVIVES on disk: retrievable via `add.py deltas --carried`
586
+ and re-activatable via `reopen-delta`. `--reason` is REQUIRED (no silent carry); `--all` carries
587
+ every open delta in the task; `--match` targets the unique one. Validate-then-write."""
588
+ root = _require_root()
589
+ state = load_state(root)
590
+ slug = _resolve_task(state, args.slug) # unknown task -> _die
591
+ reason = (getattr(args, "reason", None) or "").strip()
592
+ if not reason:
593
+ _die("carry_reason_required: carry-delta needs a --reason — a deferral must say why "
594
+ "(it is the breadcrumb a future loop reads)")
595
+ task_md = root / "tasks" / slug / "TASK.md"
596
+ text = task_md.read_text(encoding="utf-8")
597
+ stamp = f"[carried: {reason}]"
598
+ if getattr(args, "all", False):
599
+ idxs = _open_spec_delta_indices(text)
600
+ if not idxs:
601
+ _die(f"no_open_spec_delta: task '{slug}' has no open SPEC delta to carry")
602
+ for idx in idxs: # indices stay valid (flip is in-place)
603
+ text = _resolve_spec_delta(text, "carried", line_index=idx, stamp=stamp)
604
+ _atomic_write(task_md, text)
605
+ print(f"carried {len(idxs)} open SPEC delta(s) in '{slug}' -> [SPEC · carried] ({reason})")
606
+ print(_next_footer(root, state))
607
+ return
608
+ match = getattr(args, "match", None)
609
+ status, idx, _disp = _select_spec_delta(text, match)
610
+ if status == "no_open":
611
+ _die(f"no_open_spec_delta: task '{slug}' has no open SPEC delta to carry")
612
+ if status == "no_match": # a --match miss is DISTINCT from no-open
613
+ _die(f"no_matching_spec_delta: no open SPEC delta in '{slug}' matches --match '{match}'")
614
+ if status == "ambiguous":
615
+ _die(f"ambiguous_spec_match: --match '{match}' matches multiple open SPEC deltas in "
616
+ f"'{slug}' — narrow it, or use --all")
617
+ new_text = _resolve_spec_delta(text, "carried", line_index=idx, stamp=stamp)
618
+ _atomic_write(task_md, new_text)
619
+ print(f"carried the {'matched' if match else 'first'} open SPEC delta in '{slug}' -> "
620
+ f"[SPEC · carried] ({reason})")
621
+ print(_next_footer(root, state))
622
+
623
+
624
+ def cmd_reopen_delta(args: argparse.Namespace) -> None:
625
+ """RE-ACTIVATE a carried SPEC delta — `[SPEC · carried]` -> `[SPEC · open]` (delta-drain).
626
+ The inverse of carry: a deferred delta re-enters the open count + release floor + staleness.
627
+ The ` [carried: …]` breadcrumb is stripped so a re-activated delta reads clean. `--match`
628
+ targets the unique carried delta. Validate-then-write; refuse `no_carried_spec_delta`."""
629
+ root = _require_root()
630
+ state = load_state(root)
631
+ slug = _resolve_task(state, args.slug)
632
+ task_md = root / "tasks" / slug / "TASK.md"
633
+ text = task_md.read_text(encoding="utf-8")
634
+ match = getattr(args, "match", None)
635
+ status, idx, _disp = _select_spec_delta(text, match, status="carried")
636
+ if status in ("no_open", "no_match"):
637
+ _die(f"no_carried_spec_delta: task '{slug}' has no carried SPEC delta to reopen"
638
+ + (f" matching --match '{match}'" if status == "no_match" else ""))
639
+ if status == "ambiguous":
640
+ _die(f"ambiguous_spec_match: --match '{match}' matches multiple carried SPEC deltas in "
641
+ f"'{slug}' — narrow it")
642
+ new_text = _resolve_spec_delta(text, "open", line_index=idx, from_status="carried")
643
+ lines = new_text.splitlines(keepends=True) # drop the carried breadcrumb (no accretion)
644
+ eol = lines[idx][len(lines[idx].rstrip("\n")):]
645
+ lines[idx] = re.sub(r"\s*\[carried:[^\]]*\]\s*$", "", lines[idx].rstrip("\n")) + eol
646
+ _atomic_write(task_md, "".join(lines))
647
+ print(f"reopened the {'matched' if match else 'first'} carried SPEC delta in '{slug}' -> [SPEC · open]")
648
+ print(_next_footer(root, state))
649
+
650
+
651
+ # a §3 still carrying this template placeholder is NOT a drafted contract yet
652
+ _CONTRACT_TEMPLATE_RE = re.compile(r"<METHOD>")
653
+
654
+
655
+ def _next_freeze_version(state: dict, slug: str) -> str:
656
+ """v1 on the first freeze; N+1 of the highest prior freeze version recorded on the
657
+ task's state record on a re-freeze (after a change request). PURE — reads state only."""
658
+ prior = ((state.get("tasks") or {}).get(slug) or {}).get("freeze") or {}
659
+ m = re.fullmatch(r"v(\d+)", str(prior.get("version", "")))
660
+ return f"v{int(m.group(1)) + 1}" if m else "v1"
661
+
662
+
663
+ def cmd_freeze(args: argparse.Namespace) -> None:
664
+ """The §3 contract-freeze write command — the 5th engine-WRITTEN human approval (task
665
+ freeze-actor-stamp), joining lock · gate · milestone-done · release. Flips the target
666
+ task's §3 `Status: DRAFT` -> `FROZEN @ vN — approved by <name>` AND records a structured
667
+ actor on the task's state record (mirrors cmd_lock's `setup.actor`), so the audit trail
668
+ has no hole at freeze. The human RUNS it as their approval — never pre-stamped.
669
+
670
+ validate-then-write: every refusal fires before any write. Writes TASK.md first, then
671
+ state; a crash between degrades to today's legacy text-only freeze (never corrupt state),
672
+ design-for-failure."""
673
+ root = _require_root()
674
+ state = load_state(root)
675
+ raw_slug = getattr(args, "slug", None)
676
+ if not raw_slug and not _active_task(state):
677
+ _die("no_active_task: no task given and no active task is set")
678
+ slug = _resolve_task(state, raw_slug) # unknown slug -> _die
679
+ task_md = root / "tasks" / slug / "TASK.md"
680
+ text = task_md.read_text(encoding="utf-8")
681
+ raw3 = _phase_spans(text).get(3, "")
682
+ phase = (state["tasks"].get(slug) or {}).get("phase", "specify")
683
+ # --- validate (no writes); error precedence: frozen -> not-drafted -> unflagged ---
684
+ if _contract_frozen(raw3):
685
+ _die(f"already_frozen: {slug}'s §3 is already FROZEN — re-freeze only via a change "
686
+ f"request back to SPECIFY")
687
+ if _phase_index(phase) < _phase_index("contract") or _CONTRACT_TEMPLATE_RE.search(raw3):
688
+ _die(f"contract_not_drafted: {slug}'s §3 is not a drafted contract yet — reach the "
689
+ f"`contract` phase and replace the template before freezing")
690
+ if not _flag_well_formed(raw3):
691
+ _die(f"unflagged_freeze: {slug}'s §3 must surface a well-formed lowest-confidence flag "
692
+ f"('Least-sure flag surfaced at freeze:' + a [part] tag) before it freezes")
693
+ # --- write ---
694
+ ver = _next_freeze_version(state, slug)
695
+ who = args.by or identity._actor_stamp(state)["name"]
696
+ # flip the `Status: DRAFT` line WITHIN the §3 region only — a bare `Status: DRAFT` in
697
+ # §1/§2 prose must never be frozen by mistake (refute-read finding). §3 span runs from
698
+ # its `## 3 ·` heading to the next `## `/`---`/EOF (same boundary as _phase_spans).
699
+ h3 = re.search(r"(?m)^##\s*3\s*·.*$", text)
700
+ if not h3:
701
+ _die(f"contract_not_drafted: {slug}'s TASK.md has no §3 CONTRACT section")
702
+ seg_start = h3.end()
703
+ nxt = re.search(r"(?m)^(?:##\s|---\s*$)", text[seg_start:])
704
+ seg_end = seg_start + (nxt.start() if nxt else len(text) - seg_start)
705
+ new_seg, n = re.subn(r"(?m)^(\s*)Status:\s*DRAFT\s*$",
706
+ lambda m: f"{m.group(1)}Status: FROZEN @ {ver} — approved by {who}",
707
+ text[seg_start:seg_end], count=1)
708
+ if n == 0:
709
+ _die(f"contract_not_drafted: {slug}'s §3 has no 'Status: DRAFT' line to freeze")
710
+ new_text = text[:seg_start] + new_seg + text[seg_end:]
711
+ _atomic_write(task_md, new_text) # TASK.md first (audit source of truth)
712
+ state["tasks"][slug]["freeze"] = {"version": ver, "frozen_at": _now(),
713
+ "approved_by": who, "actor": identity._actor_stamp(state)}
714
+ save_state(root, state)
715
+ print(f"froze §3 of {slug} @ {ver} — approved by {who}")
716
+ print(_next_footer(root, state))
717
+
718
+
464
719
  def _parse_deps(raw: str | None) -> list[str]:
465
720
  if not raw:
466
721
  return []
@@ -490,7 +745,7 @@ def _resolve_task(state: dict, slug: str | None) -> str:
490
745
  return slug
491
746
 
492
747
 
493
- def _build_entry(root: Path, state: dict, slug: str) -> None:
748
+ def _build_entry(root: Path, state: dict, slug: str, skip_freeze: bool = False) -> None:
494
749
  """The shared tests->build entry guards + snapshots (task phase-build-guard, F4).
495
750
 
496
751
  Extracted VERBATIM from cmd_advance's `nxt == "build"` block so BOTH `advance` and the
@@ -500,22 +755,28 @@ def _build_entry(root: Path, state: dict, slug: str) -> None:
500
755
  so a refused entry leaves the task byte-unchanged. The heal loop sets phase=build DIRECTLY
501
756
  and never routes here, so it stays exempt.
502
757
  """
503
- # the OPTED-IN crossing guards (fast-lane + flow-enforcement): a task whose PARENT
504
- # milestone opted into --await-confirm is held to the trust floor at tests->build. A
505
- # task under a plain / no milestone is never gated (every existing advance-to-build flow
506
- # stays green). validate-then-write — every refusal runs BEFORE the tripwire/scope
507
- # snapshots below, writing nothing; the task stays at `tests`.
758
+ # the crossing guards. validate-then-write every refusal runs BEFORE the tripwire/scope
759
+ # snapshots below, writing nothing; the task stays at `tests` (the lone exception is the
760
+ # recorded freeze_skipped marker on the explicit --skip-freeze path).
508
761
  _ms = state["tasks"][slug].get("milestone")
509
762
  _optin = bool(_ms) and (state.get("milestones") or {}).get(_ms, {}).get("await_confirm") is True
510
763
  raw3 = _raw_phase_bodies(root, slug).get(3, "")
511
- # freeze-before-build gate (fast-lane): "collapse-never-skip" made REAL — the freeze is
512
- # engine-enforced for an opted-in milestone OR any --fast task (the fast arm, fast-new-task-flag:
513
- # a fast task is held to the floor under ANY milestone, so the lighter lane never drops the
514
- # trust seam). PRECEDES build-expectations: you freeze §3 before pre-declaring §6's outcomes.
515
- _freeze_gated = _optin or state["tasks"][slug].get("fast") is True
516
- if _freeze_gated and not _contract_frozen(raw3):
517
- _die("contract_not_frozen: freeze §3 before crossing into build — approve "
518
- f"the contract in {slug}'s TASK.md (Status: FROZEN @ vN)")
764
+ # freeze gate — UNIVERSAL (freeze-gate-universal, flow-honesty): closes audit finding H1.
765
+ # The gate used to be opt-in (`_optin or fast`), so a plain-milestone task could cross
766
+ # tests->build on a DRAFT §3 the method's decision point was engine-enforced for only a
767
+ # subset. It now fires for EVERY task. The ONLY bypass is the RECORDED `--skip-freeze` escape:
768
+ # it stamps an auditable `freeze_skipped` marker (never silent) and never auto-freezes §3
769
+ # (Status stays DRAFT). Still PRECEDES build-expectations: freeze §3 before pre-declaring §6.
770
+ if not _contract_frozen(raw3):
771
+ if not skip_freeze:
772
+ _die("contract_not_frozen: freeze §3 before crossing into build — approve "
773
+ f"the contract in {slug}'s TASK.md (Status: FROZEN @ vN), or pass "
774
+ "--skip-freeze to cross with a recorded skip")
775
+ state["tasks"][slug]["freeze_skipped"] = {
776
+ "by": identity._actor_stamp(state)["name"],
777
+ "at": _now(),
778
+ "from_phase": state["tasks"][slug].get("phase", "tests"),
779
+ }
519
780
  # build-expectations gate (flow-enforcement): an opted-in task may not enter build until
520
781
  # its §6 `### Build expectations` are pre-declared — so verify checks the build is RIGHT,
521
782
  # not just green. Same opt-in switch as the contract-fill gate, one level out.
@@ -573,7 +834,7 @@ def cmd_phase(args: argparse.Namespace) -> None:
573
834
  # validate-then-write: a refusal raises BEFORE the phase is set, so nothing moves. The heal
574
835
  # loop sets phase=build directly (never via cmd_phase) and so stays exempt.
575
836
  if args.phase == "build":
576
- _build_entry(root, state, slug)
837
+ _build_entry(root, state, slug, skip_freeze=getattr(args, "skip_freeze", False))
577
838
  state["tasks"][slug]["phase"] = args.phase
578
839
  state["tasks"][slug]["updated"] = _now()
579
840
  save_state(root, state) # F12: durable state FIRST (source of truth) — may _die
@@ -616,8 +877,9 @@ def cmd_advance(args: argparse.Namespace) -> None:
616
877
  if nxt == "build":
617
878
  # the tests->build entry guards + snapshots now live in the shared _build_entry helper
618
879
  # (task phase-build-guard, F4) so `advance` and the `phase build` admin override run the
619
- # IDENTICAL gate stack. Behavior here is byte-unchanged this is a pure extraction.
620
- _build_entry(root, state, slug)
880
+ # IDENTICAL gate stack. `--skip-freeze` (freeze-gate-universal) threads through to the
881
+ # universal freeze gate — the only recorded bypass of the now-mandatory §3 freeze.
882
+ _build_entry(root, state, slug, skip_freeze=getattr(args, "skip_freeze", False))
621
883
  # cross-component contract artifact (cross-component-contract): the contract->tests crossing
622
884
  # is the producer's freeze-approval moment. A `produces:` task WRITES the immutable snapshot;
623
885
  # a `consumes:` task PINS the live hash — a missing/unreadable snapshot HARD-STOPS here (the
@@ -800,6 +1062,7 @@ def cmd_gate(args: argparse.Namespace) -> None:
800
1062
  if completing:
801
1063
  _sync_task_marker(root, slug, "done") # then mirror the phase into TASK.md — no split-brain
802
1064
  _stamp_gate_record(root, state, slug, args.outcome) # mirror the verdict into §6 (Finding C)
1065
+ _stamp_adr_record(root, state, slug) # adr-at-observe: harvest §7 Decisions (ADR) — AFTER §6 is mirrored
803
1066
  print(f"task '{slug}' gate -> {args.outcome}")
804
1067
  _gbar = _task_green_bar(root, slug) # per-component-verify: surface the bound bar
805
1068
  if _gbar:
@@ -1303,6 +1566,14 @@ def cmd_status(args: argparse.Namespace) -> None:
1303
1566
  _so = _fmt_actor(_owner_rec) if _owner_rec.get("name") else ""
1304
1567
  _own_frag = f" · owner: {_so}" if _so else ""
1305
1568
  print(f" {_mk} {_m:<20} task={_tk or '(none)'} phase={_ph}{_tag}{_own_frag}")
1569
+ # queued backlog (queue-resume-surface): surface milestones awaiting promotion so a
1570
+ # multi-milestone session resumes cleanly — `active` is what you're on, `queued` is what's
1571
+ # next. ADDITIVE + present-only: silent when zero queued (byte-identical), exactly like the
1572
+ # release/loose/streams cues; reads state, writes nothing, changes no command decision.
1573
+ _queued = [ms for ms, m in milestones.items() if m.get("status") == "queued"]
1574
+ if _queued:
1575
+ print(f"queued : {len(_queued)} milestone(s) next — {', '.join(_queued)}")
1576
+ print(f" promote next: add.py activate {_queued[0]}")
1306
1577
  # surface the active task's autonomy level (task explicit-autonomy-dial) so the human
1307
1578
  # reads the throttle every session; "unset" when no explicit `autonomy:` line is present.
1308
1579
  if active and active in tasks:
@@ -1347,12 +1618,16 @@ def cmd_status(args: argparse.Namespace) -> None:
1347
1618
  open_deltas = sum(len(v) for v in _collect_open_deltas(root).values())
1348
1619
  if open_deltas:
1349
1620
  print(f"deltas : {open_deltas} open — consolidate at milestone close (add.py deltas)")
1350
- # SPEC-delta nudge (project-wide): surface unresolved forward hand-offs so a seed/drop
1351
- # can't be silently skipped (read-only; silent when none). Sibling of the fold nudge.
1621
+ # SPEC-delta staleness nudge (project-wide): surface unresolved forward hand-offs as STALE
1622
+ # backpressure so they drain instead of silently accumulating (delta-drain). Read-only;
1623
+ # PRESENT-ONLY (silent when none → byte-identical). The prefix stays `spec :` (the cue the
1624
+ # spec-delta guards pin); the wording now names the staleness + the drain surface, which now
1625
+ # includes carry-delta (defer non-lossily) beside seed/drop.
1352
1626
  open_spec = len(_collect_open_spec_deltas(root))
1353
1627
  if open_spec:
1354
1628
  noun = "delta" if open_spec == 1 else "deltas"
1355
- print(f"spec : {open_spec} open SPEC {noun} — resolve: new-task --from-delta / drop-delta")
1629
+ print(f"spec : {open_spec} open SPEC {noun} — stale; drain via add.py deltas "
1630
+ "(carry-delta / new-task --from-delta / drop-delta)")
1356
1631
  # When the setup is unlocked, the only terminal guidance that matters is
1357
1632
  # review+lock; suppress the generic resume block so it does not compete.
1358
1633
  if unlocked:
@@ -2239,6 +2514,39 @@ def _doctor_findings(root: Path) -> list[str]:
2239
2514
  if m is not None and m not in milestones:
2240
2515
  findings.append(f"task '{slug}' references missing milestone '{m}' — fix: set its "
2241
2516
  "milestone to a real one (or none)")
2517
+ # value-domain checks (doctor-value-checks): gate/phase enum (required) · owner/assignee
2518
+ # shape (optional) · archived consistency. Present-but-invalid + missing-required only;
2519
+ # absent owner/assignee is fine. Pure/total — .get-guarded, isinstance-checked, never raises.
2520
+ for slug, t in tasks.items():
2521
+ if not isinstance(t, dict):
2522
+ continue
2523
+ g = t.get("gate")
2524
+ if g is None:
2525
+ findings.append(f"task '{slug}' is missing its gate — fix: one of {', '.join(GATES)}")
2526
+ elif g not in GATES:
2527
+ findings.append(f"task '{slug}' has invalid gate '{g}' — fix: one of {', '.join(GATES)}")
2528
+ p = t.get("phase")
2529
+ if p is None:
2530
+ findings.append(f"task '{slug}' is missing its phase — fix: one of {', '.join(PHASES)}")
2531
+ elif p not in PHASES:
2532
+ findings.append(f"task '{slug}' has invalid phase '{p}' — fix: one of {', '.join(PHASES)}")
2533
+ for role in ("owner", "assignee"):
2534
+ v = t.get(role)
2535
+ if v is not None and not (isinstance(v, dict) and isinstance(v.get("name"), str) and v.get("name")):
2536
+ findings.append(f"task '{slug}' has a malformed {role} — fix: an actor object "
2537
+ "{name, email, source} or remove it")
2538
+ archived = state.get("archived") if isinstance(state.get("archived"), list) else []
2539
+ for a in archived:
2540
+ if not isinstance(a, dict):
2541
+ continue
2542
+ aslug = a.get("slug")
2543
+ if aslug is not None and aslug in milestones:
2544
+ findings.append(f"archived milestone '{aslug}' is also a live milestone — fix: remove "
2545
+ "the live duplicate or the archived entry")
2546
+ ts = a.get("task_slugs")
2547
+ if isinstance(ts, list) and isinstance(a.get("tasks"), int) and a.get("tasks") != len(ts):
2548
+ findings.append(f"archived milestone '{aslug}' task count {a.get('tasks')} ≠ {len(ts)} "
2549
+ "listed — fix: reconcile its task_slugs")
2242
2550
  return findings
2243
2551
 
2244
2552
 
@@ -2259,25 +2567,29 @@ def cmd_doctor(args: argparse.Namespace) -> None:
2259
2567
 
2260
2568
 
2261
2569
  def cmd_mine(args: argparse.Namespace) -> None:
2262
- """Read-only `add.py mine`: across all active milestones, the not-done tasks owned-by or
2263
- assigned-to the resolved actor (`_whoami`, or `--actor "Name <email>"`). Text or `--json`.
2264
- An empty queue is a plain exit-0 line, not an error. NEVER writes state."""
2570
+ """Read-only `add.py mine`: the not-done tasks owned-by or assigned-to the resolved actor
2571
+ (`_whoami`, or `--actor "Name <email>"`). Default lens is the active SET; `--all` widens it
2572
+ to EVERY milestone plus loose (milestone-less) tasks. Text or `--json`. An empty queue is a
2573
+ plain exit-0 line, not an error. NEVER writes state."""
2265
2574
  root = find_root()
2266
2575
  if root is None:
2267
2576
  _die("no_project")
2268
2577
  state = load_state(root)
2269
2578
  me = identity._parse_actor_arg(args.actor) if getattr(args, "actor", None) else identity._whoami(state)
2270
- rows = _my_work(state, me)
2579
+ scope_all = getattr(args, "all", False)
2580
+ rows = _my_work(state, me, scope_all=scope_all)
2271
2581
  if getattr(args, "json", False):
2272
2582
  print(json.dumps({"actor": me, "tasks": rows}))
2273
2583
  return
2584
+ scope = "all" if scope_all else "active"
2274
2585
  who = _fmt_actor(me) or me.get("name", "you")
2275
2586
  if not rows:
2276
- print(f"mine: no open tasks for {who} across active milestones")
2587
+ print(f"mine: no open tasks for {who} across {scope} milestones")
2277
2588
  return
2278
- print(f"mine: {who} — {len(rows)} open task(s) across active milestones:")
2589
+ print(f"mine: {who} — {len(rows)} open task(s) across {scope} milestones:")
2279
2590
  for r in rows:
2280
- print(f" {r['slug']:<24} [{r['milestone']}] phase={r['phase']} ({r['role']})")
2591
+ loc = f"[{r['milestone']}]" if r["milestone"] else "[loose]"
2592
+ print(f" {r['slug']:<24} {loc} phase={r['phase']} ({r['role']})")
2281
2593
 
2282
2594
 
2283
2595
  # ---------------------------------------------------------------------------
@@ -2424,6 +2736,12 @@ def cmd_new_milestone(args: argparse.Namespace) -> None:
2424
2736
  slug = args.slug
2425
2737
  if not slug.replace("-", "").replace("_", "").isalnum():
2426
2738
  _die("bad_slug")
2739
+ # Prefer a short DESCRIPTIVE slug over a bare version (v2, v1-1, 1.2): a descriptive
2740
+ # name keeps the milestones list legible. Advisory only — never blocks (matches the
2741
+ # engine's `note:` convention); a deliberate version slug still creates.
2742
+ if re.match(r"^v?\d+([._-]\d+)*$", slug, re.IGNORECASE):
2743
+ print(f"note: slug '{slug}' looks like a bare version — prefer a short "
2744
+ f"descriptive name (e.g. 'payment-retries'). Creating anyway.")
2427
2745
  state.setdefault("milestones", {})
2428
2746
  mdir = root / "milestones" / slug
2429
2747
  mfile = mdir / MILESTONE_FILE
@@ -2431,17 +2749,24 @@ def cmd_new_milestone(args: argparse.Namespace) -> None:
2431
2749
  _die("milestone_exists")
2432
2750
  mdir.mkdir(parents=True, exist_ok=True)
2433
2751
  title = args.title or slug.replace("-", " ").replace("_", " ").title()
2752
+ # One _now() instant feeds BOTH the MILESTONE.md render and the state record, so the
2753
+ # human-facing `created:` is a full ISO timestamp provably equal to state.json.
2754
+ now = _now()
2434
2755
  _atomic_write(mfile, _render_template(
2435
2756
  "MILESTONE.md", title=title, goal=args.goal or "<goal>",
2436
- stage=args.stage, date=date.today().isoformat()))
2757
+ stage=args.stage, date=now))
2437
2758
  # confirm-parent gate (OPT-IN, mirrors `init --await-lock`): `--await-confirm` seeds the
2438
2759
  # milestone UNCONFIRMED so new-task is held until `add.py milestone-confirm`. WITHOUT the flag
2439
2760
  # NO `confirmed` key is written → grandfathered-confirmed → no gate (so the existing engine
2440
2761
  # tests stay byte-green). The guided skill flow passes the flag at the human-review point.
2441
2762
  await_confirm = bool(getattr(args, "await_confirm", False))
2763
+ # --queued (OPT-IN): create the milestone non-active (status=queued) without stealing focus.
2764
+ # The active set is left UNCHANGED so the default path (no flag) stays byte-identical. Promote
2765
+ # later with `activate` (queued→active). Foundation for roadmap intake (1 active + N queued).
2766
+ queued = bool(getattr(args, "queued", False))
2442
2767
  record = {
2443
2768
  "title": title, "goal": args.goal or "", "stage": args.stage,
2444
- "status": "active", "created": _now(), "updated": _now(),
2769
+ "status": "queued" if queued else "active", "created": now, "updated": now,
2445
2770
  }
2446
2771
  if await_confirm:
2447
2772
  # `await_confirm` is the STABLE opt-in marker (set ONLY here, at creation). `confirmed`
@@ -2449,11 +2774,22 @@ def cmd_new_milestone(args: argparse.Namespace) -> None:
2449
2774
  # milestone too, so a later build-entry gate must key on `await_confirm`, not `confirmed`.
2450
2775
  record.update(confirmed=False, confirmed_at=None, confirmed_by=None, await_confirm=True)
2451
2776
  state["milestones"][slug] = record
2452
- _set_active_milestone(state, slug)
2777
+ if not queued:
2778
+ # PRESERVE the active SET (new-milestone-add-focus): ADD this milestone + focus it, rather
2779
+ # than REPLACING the set and evicting the others. Single-active is identical ([] -> [slug]);
2780
+ # a user who already had P active now keeps P active alongside the new primary.
2781
+ _activate_milestone(state, slug)
2453
2782
  save_state(root, state)
2454
2783
  print(f"created milestone '{slug}' -> {mfile}")
2455
- print("active milestone set." + ("" if not await_confirm else
2456
- " (unconfirmedshow the MILESTONE.md, then: add.py milestone-confirm " + slug + ")"))
2784
+ if queued:
2785
+ print(f"queued (not active) promote it with: add.py activate {slug}")
2786
+ # surface the recorded confirm gate for a queued+await_confirm milestone (queued-await-confirm-hint):
2787
+ # additive — prints ONLY when await_confirm, so plain `--queued` output stays byte-identical.
2788
+ if await_confirm:
2789
+ print(f" (unconfirmed — after promote: add.py milestone-confirm {slug})")
2790
+ else:
2791
+ print("active milestone set." + ("" if not await_confirm else
2792
+ " (unconfirmed — show the MILESTONE.md, then: add.py milestone-confirm " + slug + ")"))
2457
2793
  print(_next_footer(root, state)) # converges the old "Decompose it into tasks: …" hint
2458
2794
 
2459
2795
 
@@ -2542,25 +2878,34 @@ def cmd_ready(args: argparse.Namespace) -> None:
2542
2878
 
2543
2879
 
2544
2880
  def _wave_schedule(state: dict, mslug: str) -> dict:
2545
- """Pure, total: derive the DAG schedule for milestone `mslug` from state — never
2546
- mutates, never raises on dict input. Returns one of:
2881
+ """One-element wrapper: a single milestone's schedule is the merge over just [mslug].
2882
+ Output is byte-identical to the historical per-milestone scheduler (the suite is the oracle)."""
2883
+ return _wave_schedule_merged(state, [mslug])
2884
+
2885
+
2886
+ def _wave_schedule_merged(state: dict, mslugs: list[str]) -> dict:
2887
+ """Pure, total: derive ONE DAG schedule over the UNION of open members across `mslugs`
2888
+ (a single-element list is the historical per-milestone case) — never mutates, never raises
2889
+ on dict input. Returns one of:
2547
2890
  {"cycle": [slug, ...]} — unschedulable cycle
2548
2891
  {"waves", "critical_path", "critical_path_len", "tiers", "blocked"} — a schedule
2549
2892
 
2550
2893
  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 this milestone
2552
- forces a later wave. A not-done dep that is NOT an open member (external/unknown)
2553
- is UNSATISFIABLE here -> the task is `blocked`, never scheduled. Critical path is the
2554
- longest chain (most tasks) through the scheduled sub-DAG; ties break by sorted slug.
2555
- Tier is advisory: `top` on the critical path, `mid` elsewhere (scheduled tasks only)."""
2894
+ predicate cmd_ready uses. A not-done dep that is an OPEN MEMBER of ANY target milestone
2895
+ forces a later wave (so a cross-milestone dep ORDERS, it does not block). A not-done dep
2896
+ that is NOT an open member of any target (external/unknown) is UNSATISFIABLE here -> the
2897
+ task is `blocked`, never scheduled. Critical path is the longest chain (most tasks) through
2898
+ the scheduled sub-DAG; ties break by sorted slug. Tier is advisory: `top` on the critical
2899
+ path, `mid` elsewhere (scheduled tasks only)."""
2556
2900
  tasks = state.get("tasks") or {}
2557
2901
  archived = _archived_task_slugs(state)
2902
+ targetset = set(mslugs)
2558
2903
 
2559
2904
  def _ok(d: str) -> bool: # satisfied externally / already done
2560
2905
  return d in archived or (d in tasks and _task_done(tasks[d]))
2561
2906
 
2562
2907
  open_members = {s: t for s, t in tasks.items()
2563
- if t.get("milestone") == mslug and not _task_done(t)}
2908
+ if t.get("milestone") in targetset and not _task_done(t)}
2564
2909
 
2565
2910
  # partition open members into blocked vs schedulable — to a FIXED POINT, so blocking
2566
2911
  # propagates transitively: a task is blocked if any dep is unsatisfiable here, where
@@ -2661,16 +3006,71 @@ def _wave_block_lines(state: dict, mslug: str, sched: dict) -> list[str]:
2661
3006
  return lines
2662
3007
 
2663
3008
 
3009
+ def _wave_block_lines_merged(state: dict, mslugs: list[str], sched: dict) -> list[str]:
3010
+ """The text lines `waves --merge` renders for ONE unified schedule over the milestone SET:
3011
+ a `merged: …` header naming the set + each scheduled task tagged with its `[milestone]` so
3012
+ cross-milestone tasks are unambiguous. Critical-path / tier-hint / blocked lines mirror
3013
+ `_wave_block_lines`."""
3014
+ n = len(mslugs)
3015
+ lines = [f"merged: {' + '.join(mslugs)} ({n} milestone{'s' if n != 1 else ''})"]
3016
+ if not sched["waves"]:
3017
+ if sched["blocked"]:
3018
+ for s in sched["blocked"]:
3019
+ lines.append(f"blocked: {s} (waiting on {', '.join(sched['blocked'][s])})")
3020
+ else:
3021
+ lines.append("all tasks done — nothing to schedule")
3022
+ return lines
3023
+ scheduled_set = {x for w in sched["waves"] for x in w}
3024
+ for i, wave in enumerate(sched["waves"], start=1):
3025
+ parts = []
3026
+ for s in wave:
3027
+ label = f"{s} [{state['tasks'][s].get('milestone')}]"
3028
+ md = sorted(d for d in (state["tasks"][s].get("depends_on") or [])
3029
+ if d in scheduled_set)
3030
+ parts.append(f"{label} (deps: {', '.join(md)})" if md else label)
3031
+ lines.append(f"wave {i}: {', '.join(parts)}")
3032
+ crit = sched["critical_path"]
3033
+ lines.append(f"critical path: {' → '.join(crit)} ({sched['critical_path_len']} tasks)")
3034
+ tops = [s for s, tier in sched["tiers"].items() if tier == "top"]
3035
+ mids = [s for s, tier in sched["tiers"].items() if tier == "mid"]
3036
+ lines.append(f"tier hint: top → {', '.join(tops)}; mid → {', '.join(mids) or '(none)'}")
3037
+ for s in sched["blocked"]:
3038
+ lines.append(f"blocked: {s} (waiting on {', '.join(sched['blocked'][s])})")
3039
+ return lines
3040
+
3041
+
2664
3042
  def cmd_waves(args: argparse.Namespace) -> None:
2665
3043
  """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. Writes nothing; no `next:` footer."""
3044
+ and blocked set. With no --milestone it spans EVERY active milestone (cross-active-waves)
3045
+ as SEPARATE streams; a single target / --milestone renders byte-identically. With --merge it
3046
+ unifies the active SET into ONE schedule so cross-milestone deps order, not block.
3047
+ Writes nothing; no `next:` footer."""
2668
3048
  is_json = getattr(args, "json", False)
2669
3049
  if is_json:
2670
3050
  _, state = _load_state_for_json()
2671
3051
  else:
2672
3052
  state = load_state(_require_root())
2673
3053
  mslug_arg = getattr(args, "milestone", None)
3054
+ if getattr(args, "merge", False):
3055
+ if mslug_arg: # explicit target → a 1-milestone merge (NOT a conflict)
3056
+ if mslug_arg not in (state.get("milestones") or {}):
3057
+ _die(f"unknown_milestone: '{mslug_arg}' is not a milestone in this project")
3058
+ targets = [mslug_arg]
3059
+ else:
3060
+ primary = _active_milestone(state)
3061
+ if not primary:
3062
+ _die("no_active_milestone: no active milestone and no --milestone given")
3063
+ targets = [primary] + [m for m in (state.get("active_milestones") or [])
3064
+ if m != primary]
3065
+ sched = _wave_schedule_merged(state, targets)
3066
+ if "cycle" in sched:
3067
+ _die(f"dependency_cycle: not-done deps form a cycle "
3068
+ f"({' -> '.join(sched['cycle'])}) — no valid schedule")
3069
+ if is_json:
3070
+ print(json.dumps({"merged": targets, **sched}))
3071
+ else:
3072
+ print("\n".join(_wave_block_lines_merged(state, targets, sched)))
3073
+ return
2674
3074
  if mslug_arg:
2675
3075
  targets = [mslug_arg] # explicit single target — unchanged
2676
3076
  else:
@@ -2938,6 +3338,11 @@ def cmd_activate(args: argparse.Namespace) -> None:
2938
3338
  _die("unknown_milestone")
2939
3339
  if state["milestones"][slug].get("status") == "done":
2940
3340
  _die("milestone_done")
3341
+ # PROMOTE a queued milestone: activating it flips queued→active (human-gated promotion —
3342
+ # the chosen verb, reusing `activate` rather than a separate `promote`). An already-active
3343
+ # milestone is just refocused (status unchanged), keeping the default path byte-identical.
3344
+ if state["milestones"][slug].get("status") == "queued":
3345
+ state["milestones"][slug]["status"] = "active"
2941
3346
  _activate_milestone(state, slug)
2942
3347
  save_state(root, state)
2943
3348
  print(f"activated '{slug}' — active: {', '.join(state['active_milestones'])}")
@@ -4143,7 +4548,7 @@ _DELTA_STATUSES = ("open", "folded", "rejected")
4143
4548
  # dismissed (dropped) — never consolidated into the foundation. _STATUS_SETS keys each
4144
4549
  # tag to its legal status set so the ONE lint can reject a cross-set pairing
4145
4550
  # ([SPEC · folded], [SDD · seeded]) without a parallel grammar.
4146
- _SPEC_STATUSES = ("open", "seeded", "dropped")
4551
+ _SPEC_STATUSES = ("open", "seeded", "dropped", "carried")
4147
4552
  _STATUS_SETS = {**{c: _DELTA_STATUSES for c in _COMPETENCY_ORDER}, "SPEC": _SPEC_STATUSES}
4148
4553
 
4149
4554
  # Broad structural tag detector: finds ANY "- [tok · tok]" line (valid OR malformed).
@@ -4299,13 +4704,13 @@ def _collect_open_deltas(root: Path) -> dict[str, list[dict]]:
4299
4704
  return by_comp
4300
4705
 
4301
4706
 
4302
- def _collect_open_spec_deltas(root: Path) -> list[dict]:
4303
- """Scan every .add/tasks/*/TASK.md "### Spec delta" block for OPEN SPEC deltas.
4707
+ def _collect_spec_deltas(root: Path, status: str = "open") -> list[dict]:
4708
+ """Scan every .add/tasks/*/TASK.md "### Spec delta" block for SPEC deltas of `status`.
4304
4709
 
4305
- Returns a FLAT list of {task, text, evidence} dicts (SPEC is one tag, never
4306
- bucketed by competency). A SPEC delta is a forward hand-off that resolves into
4307
- a TASK never consolidated into the foundation so it is collected SEPARATELY from
4308
- _collect_open_deltas. READ-ONLY; never mutates any file."""
4710
+ Returns a FLAT list of {task, text, evidence} dicts (SPEC is one tag, never bucketed by
4711
+ competency). A SPEC delta is a forward hand-off that resolves into a TASK (seeded), is
4712
+ dismissed (dropped), or is DEFERRED non-lossily (carried)the open/carried VIEWS are this
4713
+ one scan keyed on `status`. READ-ONLY; never mutates any file."""
4309
4714
  out: list[dict] = []
4310
4715
  tasks_dir = root / "tasks"
4311
4716
  if not tasks_dir.is_dir():
@@ -4318,7 +4723,7 @@ def _collect_open_spec_deltas(root: Path) -> list[dict]:
4318
4723
  continue
4319
4724
  for unit in _spec_delta_entries(text):
4320
4725
  m = _SPEC_DELTA_RE.match(unit[0])
4321
- if m.group(2) != "open": # seeded / dropped are resolved excluded
4726
+ if m.group(2) != status: # other statuses are excluded from this view
4322
4727
  continue
4323
4728
  tail = " ".join([m.group(3).strip(), *unit[1:]]).strip()
4324
4729
  em = _EVIDENCE_RE.match(tail)
@@ -4330,51 +4735,69 @@ def _collect_open_spec_deltas(root: Path) -> list[dict]:
4330
4735
  return out
4331
4736
 
4332
4737
 
4333
- # The FIRST writer of the seeded/dropped statuses (task 1 only TOLERATED them on read).
4334
- # seed-and-drop's resolution verbs both route through here.
4335
- _SPEC_OPEN_TOKEN_RE = re.compile(r"(\[\s*SPEC\s*·\s*)open(\s*\])")
4738
+ def _collect_open_spec_deltas(root: Path) -> list[dict]:
4739
+ """Open SPEC deltas — the release-floor + `deltas` + `status` count source (a thin view)."""
4740
+ return _collect_spec_deltas(root, "open")
4741
+
4742
+
4743
+ def _collect_carried_spec_deltas(root: Path) -> list[dict]:
4744
+ """Carried (deferred, non-lossy) SPEC deltas — the `deltas --carried` retrieval surface."""
4745
+ return _collect_spec_deltas(root, "carried")
4746
+
4747
+
4748
+ # The FIRST writer of the seeded/dropped/carried statuses (task 1 only TOLERATED them on read).
4749
+ # seed-and-drop's resolution verbs AND delta-drain's carry/reopen all route through here. The token
4750
+ # regex matches ANY current SPEC status, so the flip works in either direction (open->carried,
4751
+ # carried->open) — not only away from open.
4752
+ _SPEC_STATUS_TOKEN_RE = re.compile(r"(\[\s*SPEC\s*·\s*)(?:open|seeded|dropped|carried)(\s*\])")
4336
4753
 
4337
4754
 
4338
4755
  def _resolve_spec_delta(text: str, new_status: str, pointer: str | None = None,
4339
- line_index: int | None = None) -> str | None:
4340
- """Flip ONE `[SPEC · open]` line in `text` to `new_status`; return the new text.
4756
+ line_index: int | None = None, *, from_status: str = "open",
4757
+ stamp: str | None = None) -> str | None:
4758
+ """Flip ONE `[SPEC · <from_status>]` line in `text` to `new_status`; return the new text.
4341
4759
 
4342
4760
  PURE — no IO. With `line_index` (a splitlines(keepends=True) index, as `_select_spec_delta`
4343
- returns) flip THAT line; without it, flip the FIRST open delta (back-compat). Only the status
4344
- token changes (+ a trailing ` [→ <pointer>]` provenance stamp when seeding); the entry's text
4345
- and `(evidence: …)` are byte-preserved. Returns None when there is NO open SPEC delta to flip —
4761
+ returns) flip THAT line; without it, flip the FIRST `from_status` delta (back-compat;
4762
+ `from_status` defaults to "open"). Only the status token changes (+ a trailing ` [→ <pointer>]`
4763
+ seed stamp, or a free-form ` <stamp>` e.g. `[carried: <reason>]`); the entry's text and
4764
+ `(evidence: …)` are byte-preserved. Returns None when there is NO matching delta to flip —
4346
4765
  the caller then refuses and writes nothing. Mirrors the `_autonomy_decl_line` pure-transform."""
4347
4766
  lines = text.splitlines(keepends=True)
4348
4767
  target = line_index
4349
- if target is None: # back-compat: the FIRST open delta
4768
+ if target is None: # back-compat: the FIRST from_status delta
4350
4769
  for i, ln in enumerate(lines):
4351
4770
  m = _SPEC_DELTA_RE.match(ln.rstrip("\n"))
4352
- if m and m.group(2) == "open":
4771
+ if m and m.group(2) == from_status:
4353
4772
  target = i
4354
4773
  break
4355
4774
  if target is None:
4356
4775
  return None
4357
4776
  ln = lines[target]
4358
4777
  eol = ln[len(ln.rstrip("\n")):] # preserve the exact line ending
4359
- body = _SPEC_OPEN_TOKEN_RE.sub(rf"\g<1>{new_status}\g<2>", ln.rstrip("\n"), count=1)
4778
+ body = _SPEC_STATUS_TOKEN_RE.sub(rf"\g<1>{new_status}\g<2>", ln.rstrip("\n"), count=1)
4360
4779
  if pointer:
4361
4780
  body = f"{body} [→ {pointer}]"
4781
+ if stamp:
4782
+ body = f"{body} {stamp}"
4362
4783
  lines[target] = body + eol
4363
4784
  return "".join(lines)
4364
4785
 
4365
4786
 
4366
- def _select_spec_delta(text: str, match: str | None = None) -> tuple[str, int | None, str | None]:
4367
- """Pick the open SPEC delta to resolve (delta-match-selector). PURE no IO.
4787
+ def _select_spec_delta(text: str, match: str | None = None,
4788
+ status: str = "open") -> tuple[str, int | None, str | None]:
4789
+ """Pick the SPEC delta (of `status`, default "open") to resolve (delta-match-selector). PURE.
4368
4790
 
4369
- `match=None` -> the FIRST open delta. `match=<substr>` -> the UNIQUE open delta whose display
4370
- text (status token + `(evidence: …)` excluded) contains <substr>, case-insensitive. Returns
4371
- (status, line_index, display_text) where status is one of: "ok" (line_index/display set),
4372
- "no_open" (no open delta at all), "no_match" (--match hit zero), "ambiguous" (--match hit >1).
4373
- line_index is a splitlines(keepends=True) index, the same `_resolve_spec_delta` flips."""
4791
+ `match=None` -> the FIRST such delta. `match=<substr>` -> the UNIQUE one whose display text
4792
+ (status token + `(evidence: …)` excluded) contains <substr>, case-insensitive. Returns
4793
+ (result, line_index, display_text) where result is one of: "ok" (line_index/display set),
4794
+ "no_open" (none of `status` at all), "no_match" (--match hit zero), "ambiguous" (--match hit >1).
4795
+ line_index is a splitlines(keepends=True) index, the same `_resolve_spec_delta` flips. (The
4796
+ "no_open" token is status-agnostic; the carried-track caller maps it to `no_carried_spec_delta`.)"""
4374
4797
  opens: list[tuple[int, str]] = []
4375
4798
  for i, ln in enumerate(text.splitlines(keepends=True)):
4376
4799
  m = _SPEC_DELTA_RE.match(ln.rstrip("\n"))
4377
- if not m or m.group(2) != "open":
4800
+ if not m or m.group(2) != status:
4378
4801
  continue
4379
4802
  tail = m.group(3).strip()
4380
4803
  cut = tail.find("(evidence:") # exclude the evidence tail even if its paren is unclosed
@@ -4660,24 +5083,96 @@ def _audit_findings(root: Path, state: dict) -> tuple[int, list[dict]]:
4660
5083
  for k in ("owner", "ticket", "expires")):
4661
5084
  f(slug, "waiver_incomplete",
4662
5085
  "RISK-ACCEPTED needs owner · ticket · expires")
5086
+ # adr-at-observe: a gated/done task whose §7 Decisions (ADR) block STILL holds its bare
5087
+ # <harvested…> placeholder never harvested its decision record. GRANDFATHER (INV-1): a §7
5088
+ # with NO block is legacy → skipped. BARE-LINE probe (INV-2, the same regex _stamp_adr_record
5089
+ # writes through) → harvested prose that merely quotes "<harvested at done" is not a false hit.
5090
+ s7 = raw.get(7, "")
5091
+ if "### Decisions (ADR)" in s7 and re.search(r"(?m)^<harvested at done[^\n]*>$", s7):
5092
+ f(slug, "adr_record_missing",
5093
+ "§7 Decisions (ADR) block still holds its <harvested…> placeholder (never harvested at gate)")
4663
5094
  return checked, findings
4664
5095
 
4665
5096
 
5097
+ def _freeze_skip_notices(state: dict) -> list[dict]:
5098
+ """The recorded `--skip-freeze` crossings (freeze-gate-universal): tasks that crossed
5099
+ tests->build on a DRAFT §3 via the explicit escape. SURFACED by `add.py audit` so no skip is
5100
+ silent — but NOT a finding: a recorded, sanctioned bypass never fails CI; the human judges it.
5101
+ PURE — reads only."""
5102
+ out = []
5103
+ for slug in sorted(state.get("tasks") or {}):
5104
+ mark = (state["tasks"][slug] or {}).get("freeze_skipped")
5105
+ if isinstance(mark, dict):
5106
+ out.append({"task": slug, "by": mark.get("by", "?"),
5107
+ "at": mark.get("at", "?"), "from_phase": mark.get("from_phase", "?")})
5108
+ return out
5109
+
5110
+
5111
+ # Any `risk:` declaration in the header (high|normal|low|…) — read from the `·`-delimited header
5112
+ # region only (mirrors _RISK_HIGH_RE's anchoring so a title substring can never look like one).
5113
+ _RISK_ANY_RE = re.compile(r"(?:^|·)[ \t]*risk:[ \t]*\S", re.MULTILINE)
5114
+
5115
+
5116
+ def _guarantee_lint_notices(root: Path, state: dict) -> dict:
5117
+ """PRESENCE-ONLY, MEASURE-NOT-BLOCK lints SURFACED (never failed-on) by `add.py audit`
5118
+ (guarantee-audit-lints). For tasks that reached verify (phase ∈ {verify, observe, done}):
5119
+ shallow[] = §6 '### Deep checks' block present-but-unfilled (_section_unfilled; an
5120
+ ABSENT block grandfathers a legacy task — never retro-flagged);
5121
+ risk_unset[] = the header carries NO `risk:` token (an undeclared risk level at verify);
5122
+ refute_unrecorded[]= §6 '### Refute-read verdict' block present-but-unfilled (self-grading-
5123
+ refute-record, M4) — the earned-green verdict the AI must record under
5124
+ `auto`; ABSENT block grandfathers exactly like shallow. MEASURE-NOT-BLOCK:
5125
+ never auto-blocks a gate, only surfaced here for review + a human spot-audit.
5126
+ Honest visibility for three verify guarantees; NEVER a finding (audit stays exit 0). PURE — reads
5127
+ TASK.md + state only, writes nothing."""
5128
+ shallow, risk_unset, refute_unrecorded = [], [], []
5129
+ for slug in sorted(state.get("tasks") or {}):
5130
+ if (state["tasks"][slug] or {}).get("phase") not in ("verify", "observe", "done"):
5131
+ continue
5132
+ body6 = _raw_phase_bodies(root, slug).get(6, "")
5133
+ if _section_unfilled(body6, "### Deep checks"):
5134
+ shallow.append(slug)
5135
+ if _section_unfilled(body6, "### Refute-read verdict"):
5136
+ refute_unrecorded.append(slug)
5137
+ if not _RISK_ANY_RE.search(_task_header(root, slug)):
5138
+ risk_unset.append(slug)
5139
+ return {"shallow": shallow, "risk_unset": risk_unset, "refute_unrecorded": refute_unrecorded}
5140
+
5141
+
4666
5142
  def cmd_audit(args: argparse.Namespace) -> None:
4667
5143
  """Read-only: audit recorded human decision points for well-formedness. Exit 0 clean,
4668
- exit 1 with findings — the enforcement gate CI consumes (audit-ci). Writes
4669
- NOTHING; every other command is byte-identical."""
5144
+ exit 1 with findings — the enforcement gate CI consumes (audit-ci). Also SURFACES (never
5145
+ fails on) recorded `--skip-freeze` crossings, so a skipped freeze is visible in review.
5146
+ Writes NOTHING; every other command is byte-identical."""
4670
5147
  root = _require_root()
4671
- checked, findings = _audit_findings(root, load_state(root))
5148
+ state = load_state(root)
5149
+ checked, findings = _audit_findings(root, state)
5150
+ skips = _freeze_skip_notices(state)
5151
+ glints = _guarantee_lint_notices(root, state)
4672
5152
  if getattr(args, "json", False):
4673
- print(json.dumps({"checked": checked, "findings": findings},
4674
- ensure_ascii=False, indent=2))
5153
+ print(json.dumps({"checked": checked, "findings": findings, "freeze_skips": skips,
5154
+ "guarantee_lints": glints}, ensure_ascii=False, indent=2))
4675
5155
  else:
4676
- if findings:
4677
- for x in findings:
4678
- print(f"audit: {x['code']} {x['task']} — {x['detail']}")
4679
- else:
5156
+ for x in findings:
5157
+ print(f"audit: {x['code']} {x['task']} — {x['detail']}")
5158
+ for s in skips:
5159
+ print(f"audit: freeze_skipped {s['task']} — crossed tests->build with a DRAFT §3 "
5160
+ f"(by {s['by']} at {s['at']})")
5161
+ for slug in glints["shallow"]:
5162
+ print(f"audit: shallow_deep_check {slug} — §6 Deep-checks block unfilled "
5163
+ f"(a shallow verify, not a pass)")
5164
+ if glints["risk_unset"]:
5165
+ rs = glints["risk_unset"]
5166
+ print(f"audit: risk_unset — {len(rs)} task(s) reached verify with no risk: "
5167
+ f"declaration: {', '.join(rs)}")
5168
+ if glints["refute_unrecorded"]:
5169
+ ru = glints["refute_unrecorded"]
5170
+ print(f"audit: refute_unrecorded — {len(ru)} task(s): {', '.join(ru)} "
5171
+ f"— record the earned-green refute verdict (§6); a spot-audit is the backstop")
5172
+ if not findings and not skips and not glints["shallow"] and not glints["risk_unset"] \
5173
+ and not glints["refute_unrecorded"]:
4680
5174
  print(f"audit: clean ({checked} tasks checked)")
5175
+ # MEASURE-NOT-BLOCK: only real findings raise the exit code; notices never do.
4681
5176
  if findings:
4682
5177
  sys.exit(1)
4683
5178
 
@@ -4946,6 +5441,15 @@ def release_data(root: Path, state: dict) -> dict:
4946
5441
  # loose — done milestone-free tasks not yet attributed (the cut's loose bundle, peer to releasable)
4947
5442
  loose = _releasable_loose_tasks(root, state)
4948
5443
 
5444
+ # open_spec_deltas — unresolved SPEC deltas riding into the cut (the forceable floor's count
5445
+ # source; one record-set so the floor + release-report can never disagree). GATHER, never judge.
5446
+ # LIVE-only: a SPEC delta blocks the cut only while its task is in active state. Archived tasks
5447
+ # are PASS-done history (their deltas stay preserved + visible PROJECT-WIDE in `add.py deltas` /
5448
+ # the `status` cue, but never block a fresh release — they cannot be cleared by the live-scoped
5449
+ # carry/drop verbs). This makes the floor count == the set the CLI can actually drain.
5450
+ live_tasks = state.get("tasks") or {}
5451
+ open_deltas = [d for d in _collect_open_spec_deltas(root) if d["task"] in live_tasks]
5452
+
4949
5453
  return {
4950
5454
  "releasable": releasable,
4951
5455
  "changed": changed,
@@ -4953,9 +5457,12 @@ def release_data(root: Path, state: dict) -> dict:
4953
5457
  "blockers": blockers,
4954
5458
  "monitors": monitors,
4955
5459
  "loose": loose,
5460
+ "open_spec_deltas": {"count": len(open_deltas),
5461
+ "items": [{"task": d["task"], "text": d["text"]} for d in open_deltas]},
4956
5462
  "summary": {
4957
5463
  "releasable": len(releasable), "changed": len(changed), "waivers": len(waivers),
4958
5464
  "blockers": len(blockers), "monitors": len(monitors), "loose": len(loose),
5465
+ "open_spec_deltas": len(open_deltas),
4959
5466
  },
4960
5467
  }
4961
5468
 
@@ -5031,8 +5538,8 @@ def cmd_release(args: argparse.Namespace) -> None:
5031
5538
  "never shipped. Resolve it (a change request back to Specify) before releasing. "
5032
5539
  "--force does NOT override this.")
5033
5540
  if not forced and _build_in_flight(state):
5034
- _die("release_tests_red: a build is in flight without a recorded green gate — finish and "
5035
- "gate it first, or pass --force to override.")
5541
+ _die("release_build_in_flight: a build is in flight without a recorded green gate — finish "
5542
+ "and gate it first, or pass --force to override.")
5036
5543
  bundle = _releasable(root, state)
5037
5544
  loose_bundle = _releasable_loose_tasks(root, state)
5038
5545
  if not forced and not bundle and not loose_bundle:
@@ -5042,6 +5549,12 @@ def cmd_release(args: argparse.Namespace) -> None:
5042
5549
  if not forced and d["waivers"] and not disclosed:
5043
5550
  _die("release_undisclosed_waiver: a RISK-ACCEPTED waiver rides into this release — pass "
5044
5551
  "--with-waivers to disclose it in the notes, or --force to override.")
5552
+ open_spec = d["open_spec_deltas"]["count"]
5553
+ if not forced and open_spec > 0:
5554
+ _die(f"release_open_spec_deltas: {open_spec} open SPEC delta(s) unresolved — drain them "
5555
+ "first (carry-delta / new-task --from-delta / drop-delta; see `add.py deltas`), or "
5556
+ "pass --force to cut anyway (they ride into the release unresolved). Unlike "
5557
+ "release_security_open, this floor IS forceable.")
5045
5558
 
5046
5559
  # ── RECORD — build both contents in memory, then write CHANGELOG, then RELEASES (commit) ──
5047
5560
  day = date.today().isoformat()
@@ -5076,16 +5589,22 @@ def cmd_release(args: argparse.Namespace) -> None:
5076
5589
 
5077
5590
 
5078
5591
  def cmd_deltas(args: argparse.Namespace) -> None:
5079
- """Read-only: report open competency lessons AND open SPEC deltas, SEPARATELY.
5080
-
5081
- Scans every .add/tasks/*/TASK.md: '### Competency deltas' → open lessons grouped
5082
- by competency (DDD·SDD·UDD·TDD·ADD), and '### Spec delta' → open forward hand-offs
5083
- in their own section (a SPEC delta resolves into a task, never consolidates). --json emits
5084
- one JSON object with both under separate keys. Exit 0 ALWAYS. Writes NOTHING."""
5592
+ """Read-only: report open competency lessons AND open SPEC deltas, SEPARATELY — plus, with
5593
+ `--carried`/`--all`, the carried (deferred, non-lossy) SPEC deltas as a RETRIEVAL surface.
5594
+
5595
+ Scans every .add/tasks/*/TASK.md: '### Competency deltas' → open lessons grouped by competency
5596
+ (DDD·SDD·UDD·TDD·ADD), and '### Spec delta' open forward hand-offs in their own section (a SPEC
5597
+ delta resolves into a task, never consolidates). `--carried` lists the carried deltas (re-activate
5598
+ via `reopen-delta`); `--all` shows open + carried. Bare output is BYTE-IDENTICAL to before.
5599
+ --json emits one object (carried keys only when requested). Exit 0 ALWAYS. Writes NOTHING."""
5085
5600
  root = _require_root()
5086
5601
  by_comp = _collect_open_deltas(root)
5087
5602
  total = sum(len(v) for v in by_comp.values())
5088
5603
  spec = _collect_open_spec_deltas(root)
5604
+ only_carried = getattr(args, "carried", False)
5605
+ want_carried = only_carried or getattr(args, "all", False)
5606
+ want_open = not only_carried # --carried hides open; --all keeps it
5607
+ carried = _collect_carried_spec_deltas(root) if want_carried else []
5089
5608
 
5090
5609
  if getattr(args, "json", False):
5091
5610
  payload: dict = {
@@ -5094,26 +5613,40 @@ def cmd_deltas(args: argparse.Namespace) -> None:
5094
5613
  "spec": spec,
5095
5614
  "spec_total": len(spec),
5096
5615
  }
5616
+ if want_carried:
5617
+ payload["carried"] = carried
5618
+ payload["carried_total"] = len(carried)
5097
5619
  print(json.dumps(payload, ensure_ascii=False))
5098
5620
  return
5099
5621
 
5100
- if total == 0 and not spec:
5101
- print("no open deltas.")
5102
- return
5103
-
5104
- if total:
5105
- print(f"open lessons learned ({total} total):")
5106
- for comp in _COMPETENCY_ORDER:
5107
- entries = by_comp[comp]
5108
- if not entries:
5109
- continue
5110
- print(f" {comp} ({len(entries)}):")
5111
- for e in entries:
5622
+ printed = False
5623
+ if want_open:
5624
+ if total:
5625
+ print(f"open lessons learned ({total} total):")
5626
+ for comp in _COMPETENCY_ORDER:
5627
+ entries = by_comp[comp]
5628
+ if not entries:
5629
+ continue
5630
+ print(f" {comp} ({len(entries)}):")
5631
+ for e in entries:
5632
+ print(f" - {e['text']} [{e['task']}]")
5633
+ printed = True
5634
+ if spec:
5635
+ print(f"open spec deltas ({len(spec)} total):")
5636
+ for e in spec:
5637
+ print(f" - {e['text']} [{e['task']}]")
5638
+ printed = True
5639
+ if want_carried:
5640
+ if carried:
5641
+ print(f"carried spec deltas ({len(carried)} total — reopen via add.py reopen-delta):")
5642
+ for e in carried:
5112
5643
  print(f" - {e['text']} [{e['task']}]")
5113
- if spec:
5114
- print(f"open spec deltas ({len(spec)} total):")
5115
- for e in spec:
5116
- print(f" - {e['text']} [{e['task']}]")
5644
+ printed = True
5645
+ elif only_carried: # --carried alone, nothing carried
5646
+ print("no carried spec deltas.")
5647
+ printed = True
5648
+ if not printed:
5649
+ print("no open deltas.")
5117
5650
 
5118
5651
 
5119
5652
  def cmd_project(args: argparse.Namespace) -> None:
@@ -5242,6 +5775,14 @@ def build_parser() -> argparse.ArgumentParser:
5242
5775
  pl.add_argument("--json", action="store_true", help="emit one JSON object instead of text")
5243
5776
  pl.set_defaults(func=cmd_lock)
5244
5777
 
5778
+ pfz = sub.add_parser("freeze",
5779
+ help="freeze a task's §3 contract (the human approval) — stamps "
5780
+ "FROZEN @ vN + a structured actor on the task record")
5781
+ pfz.add_argument("slug", nargs="?", default=None,
5782
+ help="task to freeze (default: the active task)")
5783
+ pfz.add_argument("--by", default=None, help="approver name (default: the resolved actor)")
5784
+ pfz.set_defaults(func=cmd_freeze)
5785
+
5245
5786
  pwho = sub.add_parser("whoami",
5246
5787
  help="show / set the git-native actor (git config -> OS user; --name to override)")
5247
5788
  # --name (set) and --unset (clear) are mutually exclusive — argparse rejects the
@@ -5295,12 +5836,37 @@ def build_parser() -> argparse.ArgumentParser:
5295
5836
  "(case-insensitive) instead of the first")
5296
5837
  pdd.set_defaults(func=cmd_drop_delta)
5297
5838
 
5839
+ pcd = sub.add_parser("carry-delta",
5840
+ help="defer a task's open SPEC delta non-lossily -> [SPEC · carried]")
5841
+ pcd.add_argument("slug", help="task whose open SPEC delta to carry (defer)")
5842
+ pcd.add_argument("--reason", default=None, metavar="TEXT",
5843
+ help="REQUIRED — why it is deferred (the breadcrumb a future loop reads)")
5844
+ grp = pcd.add_mutually_exclusive_group()
5845
+ grp.add_argument("--match", default=None, metavar="SUBSTR",
5846
+ help="target the UNIQUE open SPEC delta whose text contains SUBSTR "
5847
+ "(case-insensitive) instead of the first")
5848
+ grp.add_argument("--all", action="store_true",
5849
+ help="carry EVERY open SPEC delta in the task")
5850
+ pcd.set_defaults(func=cmd_carry_delta)
5851
+
5852
+ prd = sub.add_parser("reopen-delta",
5853
+ help="re-activate a carried SPEC delta -> [SPEC · open]")
5854
+ prd.add_argument("slug", help="task whose carried SPEC delta to reopen")
5855
+ prd.add_argument("--match", default=None, metavar="SUBSTR",
5856
+ help="target the UNIQUE carried SPEC delta whose text contains SUBSTR "
5857
+ "(case-insensitive) instead of the first")
5858
+ prd.set_defaults(func=cmd_reopen_delta)
5859
+
5298
5860
  pm = sub.add_parser("new-milestone", help="scaffold a milestone (SDD living doc)")
5299
5861
  pm.add_argument("slug")
5300
5862
  pm.add_argument("--title", default=None)
5301
5863
  pm.add_argument("--goal", default=None, help="one-sentence outcome")
5302
5864
  pm.add_argument("--stage", default="mvp", choices=STAGES)
5303
5865
  pm.add_argument("--force", action="store_true", help="overwrite MILESTONE.md if present")
5866
+ pm.add_argument("--queued", action="store_true",
5867
+ help="create the milestone QUEUED (status=queued), not active: it is recorded "
5868
+ "and its MILESTONE.md written, but the active focus is unchanged. Promote it "
5869
+ "later with `activate <slug>`. Foundation for roadmap intake (1 active + N queued).")
5304
5870
  pm.add_argument("--await-confirm", action="store_true",
5305
5871
  help="opt into the confirm-parent gate: seed the milestone unconfirmed so "
5306
5872
  "new-task is held until `milestone-confirm` (mirrors `init --await-lock`); "
@@ -5321,6 +5887,8 @@ def build_parser() -> argparse.ArgumentParser:
5321
5887
  "waves + critical path + advisory tier hint")
5322
5888
  pwa.add_argument("--milestone", default=None,
5323
5889
  help="milestone slug to schedule (default: the active milestone)")
5890
+ pwa.add_argument("--merge", action="store_true",
5891
+ help="unify the active SET into ONE schedule (cross-milestone deps order, not block)")
5324
5892
  pwa.add_argument("--json", action="store_true", help="machine-readable JSON output")
5325
5893
  pwa.set_defaults(func=cmd_waves)
5326
5894
 
@@ -5364,10 +5932,16 @@ def build_parser() -> argparse.ArgumentParser:
5364
5932
  pp = sub.add_parser("phase", help="set a task's phase explicitly")
5365
5933
  pp.add_argument("phase", choices=PHASES)
5366
5934
  pp.add_argument("slug", nargs="?", default=None)
5935
+ pp.add_argument("--skip-freeze", action="store_true",
5936
+ help="cross tests->build on a DRAFT §3, recording an auditable freeze_skipped "
5937
+ "marker (the universal freeze gate's only bypass; never auto-freezes §3)")
5367
5938
  pp.set_defaults(func=cmd_phase, _opt_positionals=("slug",))
5368
5939
 
5369
5940
  pa = sub.add_parser("advance", help="move a task to the next phase")
5370
5941
  pa.add_argument("slug", nargs="?", default=None)
5942
+ pa.add_argument("--skip-freeze", action="store_true",
5943
+ help="cross tests->build on a DRAFT §3, recording an auditable freeze_skipped "
5944
+ "marker (the universal freeze gate's only bypass; never auto-freezes §3)")
5371
5945
  pa.set_defaults(func=cmd_advance, _opt_positionals=("slug",))
5372
5946
 
5373
5947
  pg = sub.add_parser("gate", help="record a verify gate outcome")
@@ -5441,6 +6015,8 @@ def build_parser() -> argparse.ArgumentParser:
5441
6015
  pmine.add_argument("--actor", default=None, metavar="\"Name <email>\"",
5442
6016
  help="inspect another actor's queue instead of your own")
5443
6017
  pmine.add_argument("--json", action="store_true", help="emit one JSON object instead of text")
6018
+ pmine.add_argument("--all", action="store_true",
6019
+ help="widen past the active SET: every milestone + loose tasks, not just active")
5444
6020
  pmine.set_defaults(func=cmd_mine)
5445
6021
 
5446
6022
  pwv = sub.add_parser("wave-verify",
@@ -5485,6 +6061,11 @@ def build_parser() -> argparse.ArgumentParser:
5485
6061
  pdt = sub.add_parser("deltas",
5486
6062
  help="read-only report: open lessons learned grouped by competency")
5487
6063
  pdt.add_argument("--json", action="store_true", help="machine-readable JSON output")
6064
+ pdt_g = pdt.add_mutually_exclusive_group()
6065
+ pdt_g.add_argument("--carried", action="store_true",
6066
+ help="list the carried (deferred) SPEC deltas instead of the open ones")
6067
+ pdt_g.add_argument("--all", action="store_true",
6068
+ help="list open AND carried SPEC deltas")
5488
6069
  pdt.set_defaults(func=cmd_deltas)
5489
6070
 
5490
6071
  pfo = sub.add_parser(_FOLD_VERB,