@pilotspace/add 1.16.1 → 1.17.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
@@ -101,6 +101,7 @@ from add_engine.io_state import ( # re-exported as module globals: callers use
101
101
  _load_state_for_json, # --json state loader
102
102
  _md5_text, _md5_file, # md5 hashing helpers
103
103
  _personas_unseeded, # persona-seed-nudge predicate
104
+ _real_persona_slugs, # persona-fit-nudge slug listing
104
105
  )
105
106
 
106
107
 
@@ -280,10 +281,14 @@ _BLANK_RUN_RE = re.compile(r"\n{3,}")
280
281
  def _strip_live_scaffold(text: str) -> str:
281
282
  """Remove `<!-- … -->` instruction comments from a TASK.md — fences untouched, idempotent.
282
283
 
283
- Splits on fenced code blocks so a comment inside a ``` fence (e.g. the frozen §3) is never
284
- touched; in the non-fence segments it drops comment spans, trims the trailing whitespace a
285
- removal leaves on a line, and collapses 3+ consecutive newlines to one blank line."""
286
- segs = re.split(r"(```.*?```)", text, flags=re.DOTALL)
284
+ Splits on fenced code blocks AND an inline single-backtick span that IS itself a whole
285
+ `` `<!--...-->` `` (literal comment syntax quoted as an example in prose) so neither is
286
+ touched; a live comment that merely CONTAINS unrelated backtick-quoted code (e.g. this very
287
+ template's own `` `add.py autonomy set` ``-style asides) is untouched by this exception and
288
+ still stripped whole, exactly as before. In the remaining segments it drops comment spans,
289
+ trims the trailing whitespace a removal leaves on a line, and collapses 3+ consecutive
290
+ newlines to one blank line."""
291
+ segs = re.split(r"(```.*?```|`<!--.*?-->`)", text, flags=re.DOTALL)
287
292
  for i in range(0, len(segs), 2): # even indices = OUTSIDE any fence
288
293
  s = _HTML_COMMENT_RE.sub("", segs[i])
289
294
  s = _TRAILING_WS_RE.sub("", s)
@@ -395,6 +400,28 @@ def _stamp_gate_record(root: Path, state: dict, slug: str, outcome: str) -> None
395
400
  _atomic_write(f, new)
396
401
 
397
402
 
403
+ def _capture_wrapped(label: str, body: str):
404
+ """Capture a `<label>: value` field that may WRAP onto continuation lines (a human writing
405
+ prose in a TASK.md field routinely wraps past one line). Matches the label's first line, then
406
+ consumes subsequent physical lines while each is non-blank AND does not itself start a new
407
+ field label — `Word Word:` or `Word Word (parenthetical):` (the real template places labels
408
+ like `Safety rule (feature-specific):`/`Persona (optional):` immediately after a wrapped field
409
+ with no blank line; a parenthetical-blind boundary would silently swallow them) — so a wrapped
410
+ value is captured in full without ever bleeding into the next field or past a blank-line
411
+ paragraph break. Returns None if the label is absent, matching the single-line behavior it
412
+ replaces."""
413
+ m = re.search(rf"(?m)^{re.escape(label)}:[ \t]*(.+)$", body)
414
+ if not m:
415
+ return None
416
+ lines = [m.group(1).strip()]
417
+ rest = body[m.end():].split("\n")[1:]
418
+ for line in rest:
419
+ if not line.strip() or re.match(r"^[A-Z][A-Za-z ]*(\([^)]*\))?[ \t]*:", line):
420
+ break
421
+ lines.append(line.strip())
422
+ return " ".join(lines)
423
+
424
+
398
425
  def _stamp_adr_record(root: Path, state: dict, slug: str) -> None:
399
426
  """Write-back (adr-at-observe): HARVEST a §7 `### Decisions (ADR)` block from the actor-stamps
400
427
  ALREADY in the task — §1 framing (AI) · §3 freeze (human) · §5 strategy-actually-used (AI) · §6
@@ -427,11 +454,11 @@ def _stamp_adr_record(root: Path, state: dict, slug: str) -> None:
427
454
 
428
455
  def _framing(): # §1 -> [AI]: chosen + rejected
429
456
  try:
430
- m = re.search(r"(?m)^Framings weighed:[ \t]*(.+)$", bodies.get(1, ""))
431
- if not m:
457
+ val = _capture_wrapped("Framings weighed", bodies.get(1, ""))
458
+ if val is None:
432
459
  return UN, ""
433
460
  chosen, rejected = UN, []
434
- for p in (s.strip() for s in m.group(1).split("·") if s.strip()):
461
+ for p in (s.strip() for s in val.split("·") if s.strip()):
435
462
  cm = re.match(r"(.*?)\s*\(chosen\b.*\)\s*$", p) # "(chosen)" OR "(chosen — rationale)"
436
463
  if cm:
437
464
  chosen = cm.group(1).strip() or UN
@@ -459,12 +486,11 @@ def _stamp_adr_record(root: Path, state: dict, slug: str) -> None:
459
486
 
460
487
  def _strategy(): # §5 -> [AI]: the value, default "as planned"
461
488
  try:
462
- m = re.search(r"(?m)^Strategy actually used:[ \t]*(.+)$", bodies.get(5, ""))
463
- if m:
489
+ val = _capture_wrapped("Strategy actually used", bodies.get(5, ""))
490
+ if val:
464
491
  # UNFILLED is the "<fill at …>" template token; a real value may legitimately
465
492
  # contain "<" (quoting `<tag>`, "x < y") and must NOT degrade to the default
466
- val = m.group(1).strip()
467
- if val and not val.startswith("<fill"):
493
+ if not val.startswith("<fill"):
468
494
  return val
469
495
  except Exception:
470
496
  pass
@@ -594,6 +620,26 @@ def cmd_sync_guidelines(args: argparse.Namespace) -> None:
594
620
  print(f"{action:>9} {name}")
595
621
 
596
622
 
623
+ # fastlane-intake-nudge: a frozen, blunt lexical heuristic — substring match only, no semantic
624
+ # read. Advisory-only (see _fastlane_nudge); never blocks, never selects the lane itself.
625
+ RISK_KEYWORDS = frozenset({
626
+ "milestone", "release", "security", "auth", "architecture", "migration",
627
+ "schema", "protocol", "engine", "breaking", "concurrency", "compliance", "payment",
628
+ })
629
+
630
+
631
+ def _fastlane_nudge(title: str, slug: str) -> str | None:
632
+ """PURE. None if any RISK_KEYWORDS token appears in title.lower() or slug.lower();
633
+ else the one-line advisory recommending the fast lane or a direct edit."""
634
+ haystack = f"{title} {slug}".lower()
635
+ if any(word in haystack for word in RISK_KEYWORDS):
636
+ return None
637
+ return ("heuristic: this looks like a fast-lane or direct-edit candidate (no --fast, "
638
+ "no risk keyword in title/slug) — consider `add.py new-task <slug> --fast`, or "
639
+ "just edit directly for a single-file change. Recommendation only — the lane "
640
+ "is yours to pick.")
641
+
642
+
597
643
  def cmd_new_task(args: argparse.Namespace) -> None:
598
644
  root = _require_root()
599
645
  state = load_state(root)
@@ -718,10 +764,31 @@ def cmd_new_task(args: argparse.Namespace) -> None:
718
764
  if from_delta:
719
765
  print(f"seeded from '{from_delta}' — its open SPEC delta is now "
720
766
  f"[SPEC · seeded] … [→ {slug}]; §1 Feature pre-filled.")
767
+ if not fast:
768
+ note = _fastlane_nudge(title, slug)
769
+ if note:
770
+ print(note)
721
771
  print("active task set. phase: ground. Gather the real codebase (section 0 GROUND).")
722
772
  print(_next_footer(root, state)) # converges the old "then: add.py advance" hint
723
773
 
724
774
 
775
+ def _delta_task_md(root: Path, state: dict, raw_slug: str | None) -> tuple[str, Path, bool]:
776
+ """Resolve a delta verb's target to its on-disk TASK.md — ACTIVE (state-tracked) or
777
+ light-ARCHIVED (state entry dropped at archive-milestone, file kept). The SPEC-delta
778
+ lifecycle lives in the FILE and `deltas` already lists archived ones, so the write verbs
779
+ reach them too (delta-drain reach-back) — previously that needed a hand edit. An archived
780
+ target must be named EXPLICITLY: the no-slug active-task fallback never resolves to one.
781
+ A slug neither in state nor on disk still dies `unknown task` (a compacted bundle under
782
+ .add/archive/ stays out of reach — recover it first). Returns (slug, task_md, archived)."""
783
+ if raw_slug and raw_slug not in state.get("tasks", {}):
784
+ task_md = root / "tasks" / raw_slug / "TASK.md"
785
+ if task_md.exists():
786
+ return raw_slug, task_md, True
787
+ _die(f"unknown task '{raw_slug}'")
788
+ slug = _resolve_task(state, raw_slug) # active path, unchanged semantics
789
+ return slug, root / "tasks" / slug / "TASK.md", False
790
+
791
+
725
792
  def cmd_drop_delta(args: argparse.Namespace) -> None:
726
793
  """DISMISS a task's first open SPEC delta — `[SPEC · open]` -> `[SPEC · dropped]`.
727
794
 
@@ -730,8 +797,7 @@ def cmd_drop_delta(args: argparse.Namespace) -> None:
730
797
  text + `(evidence: …)` are byte-preserved by the pure `_resolve_spec_delta`."""
731
798
  root = _require_root()
732
799
  state = load_state(root)
733
- slug = _resolve_task(state, args.slug) # unknown task -> _die
734
- task_md = root / "tasks" / slug / "TASK.md"
800
+ slug, task_md, _arch = _delta_task_md(root, state, args.slug)
735
801
  text = task_md.read_text(encoding="utf-8")
736
802
  match = getattr(args, "match", None)
737
803
  status, idx, _disp = _select_spec_delta(text, match)
@@ -744,7 +810,8 @@ def cmd_drop_delta(args: argparse.Namespace) -> None:
744
810
  f"'{slug}' — narrow it")
745
811
  new_text = _resolve_spec_delta(text, "dropped", line_index=idx)
746
812
  _atomic_write(task_md, new_text)
747
- print(f"dropped the {'matched' if match else 'first'} open SPEC delta in '{slug}' -> [SPEC · dropped]")
813
+ print(f"dropped the {'matched' if match else 'first'} open SPEC delta in "
814
+ f"'{slug}'{' (archived — on-disk record)' if _arch else ''} -> [SPEC · dropped]")
748
815
  print(_next_footer(root, state))
749
816
 
750
817
 
@@ -764,12 +831,12 @@ def cmd_carry_delta(args: argparse.Namespace) -> None:
764
831
  every open delta in the task; `--match` targets the unique one. Validate-then-write."""
765
832
  root = _require_root()
766
833
  state = load_state(root)
767
- slug = _resolve_task(state, args.slug) # unknown task -> _die
834
+ slug, task_md, _arch = _delta_task_md(root, state, args.slug)
835
+ _arch_note = " (archived — on-disk record)" if _arch else ""
768
836
  reason = (getattr(args, "reason", None) or "").strip()
769
837
  if not reason:
770
838
  _die("carry_reason_required: carry-delta needs a --reason — a deferral must say why "
771
839
  "(it is the breadcrumb a future loop reads)")
772
- task_md = root / "tasks" / slug / "TASK.md"
773
840
  text = task_md.read_text(encoding="utf-8")
774
841
  stamp = f"[carried: {reason}]"
775
842
  if getattr(args, "all", False):
@@ -779,7 +846,7 @@ def cmd_carry_delta(args: argparse.Namespace) -> None:
779
846
  for idx in idxs: # indices stay valid (flip is in-place)
780
847
  text = _resolve_spec_delta(text, "carried", line_index=idx, stamp=stamp)
781
848
  _atomic_write(task_md, text)
782
- print(f"carried {len(idxs)} open SPEC delta(s) in '{slug}' -> [SPEC · carried] ({reason})")
849
+ print(f"carried {len(idxs)} open SPEC delta(s) in '{slug}'{_arch_note} -> [SPEC · carried] ({reason})")
783
850
  print(_next_footer(root, state))
784
851
  return
785
852
  match = getattr(args, "match", None)
@@ -793,7 +860,7 @@ def cmd_carry_delta(args: argparse.Namespace) -> None:
793
860
  f"'{slug}' — narrow it, or use --all")
794
861
  new_text = _resolve_spec_delta(text, "carried", line_index=idx, stamp=stamp)
795
862
  _atomic_write(task_md, new_text)
796
- print(f"carried the {'matched' if match else 'first'} open SPEC delta in '{slug}' -> "
863
+ print(f"carried the {'matched' if match else 'first'} open SPEC delta in '{slug}'{_arch_note} -> "
797
864
  f"[SPEC · carried] ({reason})")
798
865
  print(_next_footer(root, state))
799
866
 
@@ -805,8 +872,7 @@ def cmd_reopen_delta(args: argparse.Namespace) -> None:
805
872
  targets the unique carried delta. Validate-then-write; refuse `no_carried_spec_delta`."""
806
873
  root = _require_root()
807
874
  state = load_state(root)
808
- slug = _resolve_task(state, args.slug)
809
- task_md = root / "tasks" / slug / "TASK.md"
875
+ slug, task_md, _arch = _delta_task_md(root, state, args.slug)
810
876
  text = task_md.read_text(encoding="utf-8")
811
877
  match = getattr(args, "match", None)
812
878
  status, idx, _disp = _select_spec_delta(text, match, status="carried")
@@ -821,7 +887,8 @@ def cmd_reopen_delta(args: argparse.Namespace) -> None:
821
887
  eol = lines[idx][len(lines[idx].rstrip("\n")):]
822
888
  lines[idx] = re.sub(r"\s*\[carried:[^\]]*\]\s*$", "", lines[idx].rstrip("\n")) + eol
823
889
  _atomic_write(task_md, "".join(lines))
824
- print(f"reopened the {'matched' if match else 'first'} carried SPEC delta in '{slug}' -> [SPEC · open]")
890
+ print(f"reopened the {'matched' if match else 'first'} carried SPEC delta in "
891
+ f"'{slug}'{' (archived — on-disk record)' if _arch else ''} -> [SPEC · open]")
825
892
  print(_next_footer(root, state))
826
893
 
827
894
 
@@ -1033,6 +1100,35 @@ def cmd_phase(args: argparse.Namespace) -> None:
1033
1100
  print(_next_footer(root, state))
1034
1101
 
1035
1102
 
1103
+ def cmd_recross(args: argparse.Namespace) -> None:
1104
+ """The RECORDED post-freeze re-cross (bundle-advance): a HUMAN-APPROVED test change after
1105
+ the tests->build crossing (e.g. a test added at review) re-arms the tamper tripwire + §5
1106
+ scope snapshot by re-running the IDENTICAL _build_entry gate stack — never a freeze bypass
1107
+ (a DRAFT §3 still refuses contract_not_frozen). The approver is recorded in state
1108
+ (tasks[slug]["recross"] = {by, at, from_phase}) so an audit can tell a signed re-cross
1109
+ from silent tampering. Replaces the undocumented `phase tests` + `advance` dance."""
1110
+ root = _require_root()
1111
+ state = load_state(root)
1112
+ slug = _resolve_task(state, args.slug)
1113
+ cur = state["tasks"][slug]["phase"]
1114
+ if cur not in ("build", "verify"):
1115
+ _die(f"recross_wrong_phase: re-cross re-arms the tests->build snapshots — only a "
1116
+ f"task at build or verify can re-cross (task '{slug}' is at {cur})")
1117
+ if not (getattr(args, "by", "") or "").strip():
1118
+ _die("recross_unsigned: a post-freeze test change is human-approved — record the "
1119
+ "approver with --by <name>")
1120
+ _build_entry(root, state, slug) # full gate stack; validate-then-write
1121
+ state["tasks"][slug]["recross"] = {"by": args.by.strip(), "at": _now(),
1122
+ "from_phase": cur}
1123
+ state["tasks"][slug]["phase"] = "build"
1124
+ state["tasks"][slug]["updated"] = _now()
1125
+ save_state(root, state) # durable state FIRST (source of truth)
1126
+ _sync_task_marker(root, slug, "build") # then mirror into TASK.md — no split-brain
1127
+ print(f"task '{slug}' re-crossed tests->build — tripwire + scope re-snapshotted "
1128
+ f"(approved by {args.by.strip()})")
1129
+ print(_next_footer(root, state))
1130
+
1131
+
1036
1132
  def cmd_advance(args: argparse.Namespace) -> None:
1037
1133
  root = _require_root()
1038
1134
  state = load_state(root)
@@ -1041,6 +1137,18 @@ def cmd_advance(args: argparse.Namespace) -> None:
1041
1137
  idx = PHASES.index(cur)
1042
1138
  if idx >= len(PHASES) - 1:
1043
1139
  _die(f"task '{slug}' already at final phase ({cur})")
1140
+ # bundle fast-forward (bundle-advance): --to repeats the SINGLE-STEP advance — every
1141
+ # crossing guard below runs per step — and stops hard at `tests`: the tests->build
1142
+ # crossing carries the gate stack (_build_entry) and is never fast-forwarded.
1143
+ _to = getattr(args, "to", None)
1144
+ if _to is not None:
1145
+ if _to not in PHASES:
1146
+ _die(f"advance_to_invalid: --to must be one of: {', '.join(PHASES)}")
1147
+ if PHASES.index(_to) > PHASES.index("tests"):
1148
+ _die("advance_to_stops_at_tests: --to fast-forwards the bundle bookkeeping only — "
1149
+ "the tests->build crossing carries the gate stack; cross it with a plain advance")
1150
+ if PHASES.index(_to) <= idx:
1151
+ _die(f"advance_to_not_forward: task '{slug}' is already at {cur}")
1044
1152
  nxt = PHASES[idx + 1]
1045
1153
  # build-boundary gate: pre-lock the front (specify..tests) is allowed, but crossing
1046
1154
  # into build/verify/observe/done is refused until `add.py lock`.
@@ -1120,6 +1228,11 @@ def cmd_advance(args: argparse.Namespace) -> None:
1120
1228
  print(" note: record the lessons this loop taught the foundation in §7 "
1121
1229
  "OBSERVE, then update PROJECT.md when ready:")
1122
1230
  print(f" add.py {_FOLD_VERB} --task {slug} (review first: add.py deltas)")
1231
+ # bundle fast-forward: keep stepping (each pass re-loads state and re-runs every
1232
+ # crossing guard) until the validated --to target is reached.
1233
+ if _to is not None and PHASES.index(nxt) < PHASES.index(_to):
1234
+ cmd_advance(args)
1235
+ return
1123
1236
  print(_next_footer(root, state))
1124
1237
 
1125
1238
 
@@ -1303,9 +1416,47 @@ def _driver_marker(stop: bool) -> str:
1303
1416
  return " [human gate]" if stop else " [you drive]"
1304
1417
 
1305
1418
 
1419
+ def _gate_explain(root: Path, state: dict, slug: str) -> None:
1420
+ """gate-explain (method-ergonomics): compose the gate decision from the SAME predicates
1421
+ cmd_gate enforces, as a READ-ONLY answer — the agent asks instead of recalling run.md.
1422
+ PURE: prints, writes nothing."""
1423
+ hdr = _task_header(root, slug)
1424
+ body6 = _raw_phase_bodies(root, slug).get(6, "")
1425
+ from add_engine.autonomy import _autonomy_level
1426
+ level = _autonomy_level(hdr) or "auto"
1427
+ high = bool(_RISK_HIGH_RE.search(hdr))
1428
+ sens = _task_sensitivity(hdr, valid=_project_sensitivity_values(root)) or "unset"
1429
+ adv_pass = _advisor_verdict_is_pass(body6)
1430
+ adv_clean = adv_pass and _advisor_no_residue(body6)
1431
+ relaxed = sens == "mechanical" and adv_clean
1432
+ print(f"gate-explain {slug}")
1433
+ print(f" phase: {state['tasks'][slug].get('phase', '?')}")
1434
+ print(f" autonomy: {level} · risk: {'high' if high else 'unset/low'} · sensitivity: {sens}")
1435
+ print(f" advisor 3-lens: {'PASS, residue none' if adv_clean else ('PASS with residue' if adv_pass else 'unrecorded or non-PASS')}")
1436
+ print(f" advisor-gate-relax: {'applies (mechanical + clean advisor verdict)' if relaxed else 'not applicable'}")
1437
+ if _autonomy_lowered(hdr):
1438
+ print(" path: HUMAN — the lowered autonomy level puts a person at this verify gate")
1439
+ elif high and not relaxed:
1440
+ print(" path: REFUSED at completion (unguarded_high_risk_auto) — lower the autonomy "
1441
+ "level (`add.py autonomy set conservative`), or for a mechanical task record a "
1442
+ "clean Advisor 3-lens verdict")
1443
+ elif relaxed:
1444
+ print(" path: RELAX — mechanical sensitivity + advisor PASS/none may complete "
1445
+ "without a lowered level (advisor-gate-relax)")
1446
+ else:
1447
+ print(" path: AUTO — may auto-PASS on complete evidence (tests green · no tamper · "
1448
+ "loops dry · deep check + refute-read + 3-lens recorded) with no residue")
1449
+ print(" floor: a security finding is always HARD-STOP — never auto-passed, on every path")
1450
+
1451
+
1306
1452
  def cmd_gate(args: argparse.Namespace) -> None:
1307
1453
  root = _require_root()
1308
1454
  state = load_state(root)
1455
+ if getattr(args, "explain", False):
1456
+ # slug may have landed in the outcome slot (`gate --explain <slug>`)
1457
+ cand = args.slug or (args.outcome if args.outcome not in GATES else None)
1458
+ _gate_explain(root, state, _resolve_task(state, cand))
1459
+ return
1309
1460
  slug = _resolve_task(state, args.slug)
1310
1461
  # build-boundary gate: no verdict may be recorded before the setup is locked.
1311
1462
  if not _setup_locked(state):
@@ -1493,6 +1644,55 @@ def cmd_autonomy(args: argparse.Namespace) -> None:
1493
1644
  _print_autonomy(root, state, slug)
1494
1645
 
1495
1646
 
1647
+ def cmd_worktree_prep(args: argparse.Namespace) -> None:
1648
+ """Mechanize streams.md's manual worktree recipe (worktree-prep): cut an isolated git
1649
+ worktree at HEAD for a spawned worker, materialize the gitignored engine content a
1650
+ tracked-only checkout lacks (.add/tooling · .add/docs — confirmed absent 3-for-3 when
1651
+ done by hand), and echo the fork base for the WAVE.md ledger. Workspace-only: never
1652
+ writes state.json (prep is not a state transition). validate-then-act — every refusal
1653
+ precedes the first filesystem write; a dirty tree WARNS (streams.md: cut AFTER the
1654
+ bundle commit) but proceeds. Runs git only (identity.py precedent) — the NO-EXEC floor
1655
+ (never run a verify suite) is untouched."""
1656
+ root = _require_root()
1657
+ state = load_state(root)
1658
+ slug = _resolve_task(state, args.slug) # unknown task -> _die
1659
+ project = root.parent
1660
+ try:
1661
+ r = subprocess.run(["git", "-C", str(project), "rev-parse", "--short", "HEAD"],
1662
+ capture_output=True, text=True, timeout=30)
1663
+ except (OSError, subprocess.SubprocessError):
1664
+ r = None
1665
+ if r is None or r.returncode != 0:
1666
+ _die("worktree_prep_no_git: the project root is not a git repository with a commit — "
1667
+ "worktree isolation needs git (commit the frozen bundle first)")
1668
+ base = r.stdout.strip()
1669
+ dest = (Path(args.dir).expanduser().resolve() if getattr(args, "dir", None)
1670
+ else project.parent / f"{project.name}-wt-{slug}")
1671
+ if dest.exists():
1672
+ _die(f"worktree_prep_exists: {dest} already exists — remove it or pass --dir <path>")
1673
+ dirty = subprocess.run(["git", "-C", str(project), "status", "--porcelain"],
1674
+ capture_output=True, text=True, timeout=30).stdout.strip()
1675
+ if dirty:
1676
+ print("warning: worktree_prep_dirty_tree — uncommitted changes will NOT ride into the "
1677
+ "worktree; streams.md cuts AFTER committing the frozen bundle", file=sys.stderr)
1678
+ r = subprocess.run(["git", "-C", str(project), "worktree", "add", str(dest), "HEAD"],
1679
+ capture_output=True, text=True, timeout=120)
1680
+ if r.returncode != 0:
1681
+ _die(f"worktree_prep_git_failed: git worktree add refused — {r.stderr.strip()[:300]}")
1682
+ copied = [] # tracked-only checkout lacks these
1683
+ for name in ("tooling", "docs"):
1684
+ src = root / name
1685
+ if src.is_dir():
1686
+ shutil.copytree(src, dest / ".add" / name, dirs_exist_ok=True)
1687
+ copied.append(f".add/{name}")
1688
+ print(f"worktree ready: {dest}")
1689
+ print(f"fork base: {base} (record it in the WAVE.md ledger; the worker's step-0 "
1690
+ "re-echoes `git rev-parse HEAD` and wave-verify checks the match)")
1691
+ print(f"materialized (gitignored, absent from a bare checkout): "
1692
+ f"{', '.join(copied) if copied else 'none found'}")
1693
+ print(f"cleanup when merged: git -C {project} worktree remove {dest}")
1694
+
1695
+
1496
1696
  def cmd_streams(args: argparse.Namespace) -> None:
1497
1697
  """show / set the project STREAMS posture — the parallel-vs-sequential half of the run mode
1498
1698
  (persist-run-mode). Project-scoped (parallelism is ACROSS tasks, so there is no per-task posture)
@@ -1945,6 +2145,20 @@ def cmd_status(args: argparse.Namespace) -> None:
1945
2145
  if _loose:
1946
2146
  print(f" → releasable: {len(_loose)} loose task(s) since last release")
1947
2147
 
2148
+ # loop-surfacing-nudges: two ADDITIVE cues so the observe loop surfaces its own accumulation
2149
+ # (cues COUNT, never judge; a clean project's output is byte-identical). carried = the deferred
2150
+ # spec-delta backlog (write-only memory otherwise); compaction = the folded tail the
2151
+ # compact-foundation.md ritual exists to roll (threshold 25 keeps young projects quiet).
2152
+ _carried_n = len(_collect_carried_spec_deltas(root))
2153
+ if _carried_n:
2154
+ print(f" → carried: {_carried_n} deferred spec delta(s) — add.py deltas --carried")
2155
+ _tail = _foundation_tail(root)
2156
+ if _tail["bullets"] >= 25:
2157
+ _rolled = f"fv{_tail['last_settled_fv']}" if _tail["last_settled_fv"] else "never"
2158
+ _now = f"fv{_tail['fv']}" if _tail["fv"] else "fv?"
2159
+ print(f" → compaction: {_tail['bullets']} consolidated lesson(s) above the settled line "
2160
+ f"(last rolled {_rolled}, now {_now}) — compact-foundation.md")
2161
+
1948
2162
  # fast-lane marker (fast-new-task-flag): tag an ACTIVE fast task so the lane is visible at a
1949
2163
  # glance. Presentation-only, existence-gated — a plain/absent active task is byte-unchanged.
1950
2164
  _fast_mark = " · fast" if active and tasks.get(active, {}).get("fast") is True else ""
@@ -3448,6 +3662,13 @@ def cmd_new_milestone(args: argparse.Namespace) -> None:
3448
3662
  # (a queued milestone isn't yet in flight, so the nudge would be premature there).
3449
3663
  if _personas_unseeded(root):
3450
3664
  print(f"note: {PERSONA_HINT}")
3665
+ else:
3666
+ # persona-fit-nudge: the opposite branch — ≥1 real persona already exists, so nudge
3667
+ # the AI to confirm domain fit (or draft a new one) rather than silently assuming an
3668
+ # existing persona covers this brand-new milestone. Existence-only, mutually
3669
+ # exclusive with the note above (same predicate, opposite branch — never both).
3670
+ slugs = ", ".join(_real_persona_slugs(root))
3671
+ print(f"persona-fit: {PERSONA_FIT_HINT_TEMPLATE.format(slugs=slugs)}")
3451
3672
  print(_next_footer(root, state)) # converges the old "Decompose it into tasks: …" hint
3452
3673
 
3453
3674
 
@@ -5640,6 +5861,32 @@ def _collect_open_spec_deltas(root: Path) -> list[dict]:
5640
5861
  return _collect_spec_deltas(root, "open")
5641
5862
 
5642
5863
 
5864
+ def _foundation_tail(root: Path) -> dict:
5865
+ """Count the un-compacted foundation tail — READ-ONLY facts for the status compaction cue.
5866
+
5867
+ Returns {bullets, last_settled_fv, fv}: live `[folded foundation-version N]` stamps across
5868
+ PROJECT.md + CONVENTIONS.md, the highest fv a `settled …fvK–fvM` rolled line reaches (None
5869
+ when never rolled), and the current header fv (None when unparseable). Judges nothing."""
5870
+ bullets, settled, fv = 0, None, None
5871
+ for name in ("PROJECT.md", "CONVENTIONS.md"):
5872
+ f = root / name
5873
+ if not f.is_file():
5874
+ continue
5875
+ try:
5876
+ s = f.read_text(encoding="utf-8")
5877
+ except OSError:
5878
+ continue
5879
+ bullets += s.count("[folded foundation-version")
5880
+ for m in re.finditer(r"settled[^\n]*?fv\d+[–-]fv(\d+)", s):
5881
+ n = int(m.group(1))
5882
+ settled = n if settled is None else max(settled, n)
5883
+ if fv is None:
5884
+ hm = re.search(r"foundation-version:\s*(\d+)", s)
5885
+ if hm:
5886
+ fv = int(hm.group(1))
5887
+ return {"bullets": bullets, "last_settled_fv": settled, "fv": fv}
5888
+
5889
+
5643
5890
  def _collect_carried_spec_deltas(root: Path) -> list[dict]:
5644
5891
  """Carried (deferred, non-lossy) SPEC deltas — the `deltas --carried` retrieval surface."""
5645
5892
  return _collect_spec_deltas(root, "carried")
@@ -5745,12 +5992,136 @@ _KEY_DECISIONS_HEADING = "## Key Decisions" # the universal audit-trail sectio
5745
5992
  _TABLE_SEP_RE = re.compile(r"\s*\|[-\s|]+\|\s*$")
5746
5993
 
5747
5994
  # persona-self-improve: a `persona:<slug>` lesson routes into `.add/personas/<slug>.md` instead of a
5748
- # foundation file. The section HINT picks the growable section; only these two are routable.
5995
+ # foundation file. The section HINT picks the growable section; only these four are routable
5996
+ # (fold-persona-sections widened the pair to the 1.16.1 schema's behavioral sections).
5749
5997
  _PERSONA_FOLD_SECTIONS = {
5750
5998
  "critical-rule": "## Critical Rules",
5751
5999
  "success-metric": "## Success Metrics",
6000
+ "anti-pattern": "## Anti-patterns",
6001
+ "ability": "## Abilities",
5752
6002
  }
5753
6003
 
6004
+ # fold-glossary-deltas: a 6th pseudo-competency, `GLOSSARY`, folding a DONE task's own §3
6005
+ # `Glossary deltas: <term>: <definition>` line into `.add/GLOSSARY.md` — unstamped, matching
6006
+ # that file's own existing convention (provenance lives in the task's OWN stamped line instead).
6007
+ _GLOSSARY_LINE_RE = re.compile(r"^Glossary deltas:\s*(.*)$")
6008
+ _GLOSSARY_STAMP_RE = re.compile(r"\[" + re.escape(_FOLDED) + r" foundation-version \d+\]\s*$")
6009
+ _GLOSSARY_EMBEDDED_TERM_RE = re.compile(r"`[^`\n]{1,80}`\s*:") # a 2nd `term`: span -> multi-term, reject
6010
+ _TASK_ATTR_LINE_RE = re.compile(r"^[A-Z][A-Za-z0-9 /()'-]*:\s") # a new top-level "Key: value" line ends a wrap
6011
+ # (real corpus has hyphenated/apostrophe'd labels too, e.g. "Least-sure flag surfaced at freeze:",
6012
+ # "BIND-DON'T-BREAK:" — a narrower class silently over-joins the NEXT field into a Glossary delta)
6013
+
6014
+
6015
+ def _parse_glossary_delta(full_text: str) -> tuple[str, str] | None:
6016
+ """Parse a joined Glossary-deltas value into (term, definition), PURE, or None when it does
6017
+ NOT cleanly parse as a single `<term>: <definition>` pair — "none", already resolved (the
6018
+ `[<resolved> foundation-version N]` stamp), no top-level colon, an unmatched backtick before
6019
+ the split (the `roster-portable-shape`-shaped hazard), or a second embedded `` `term`: ``
6020
+ span further in the definition (the `search-index`-shaped two-terms-in-one-line hazard) all
6021
+ reject."""
6022
+ text = full_text.strip()
6023
+ if not text or text.lower().startswith("none"):
6024
+ return None
6025
+ if _GLOSSARY_STAMP_RE.search(text):
6026
+ return None
6027
+ colon = text.find(":")
6028
+ if colon == -1:
6029
+ return None
6030
+ term_candidate, definition = text[:colon], text[colon + 1:].strip()
6031
+ if not definition or term_candidate.count("`") % 2 != 0:
6032
+ return None
6033
+ term = term_candidate.strip()
6034
+ if term.startswith("`") and term.endswith("`") and len(term) >= 2:
6035
+ term = term[1:-1].strip()
6036
+ if not term or _GLOSSARY_EMBEDDED_TERM_RE.search(definition):
6037
+ return None
6038
+ return term, definition
6039
+
6040
+
6041
+ def _fold_glossary_delta(text: str, version: int) -> str | None:
6042
+ """Append the resolved-stamp (` [<resolved> foundation-version N]`) to the LAST physical line
6043
+ of the FIRST clean, not-yet-resolved `Glossary deltas:` entry in `text` (joining any wrapped
6044
+ continuation lines the same way `_collect_open_deltas` groups a multi-line delta). PURE — no
6045
+ IO. Returns None when there is no clean, not-yet-resolved candidate (the caller then refuses
6046
+ — validate-all-then-write)."""
6047
+ lines = text.splitlines(keepends=True)
6048
+ start = next((i for i, ln in enumerate(lines) if _GLOSSARY_LINE_RE.match(ln.rstrip("\n"))), None)
6049
+ if start is None:
6050
+ return None
6051
+ end = start
6052
+ unit = [_GLOSSARY_LINE_RE.match(lines[start].rstrip("\n")).group(1).strip()]
6053
+ for j in range(start + 1, len(lines)):
6054
+ stripped = lines[j].strip()
6055
+ if not stripped or _TASK_ATTR_LINE_RE.match(stripped) or stripped.startswith("<!--"):
6056
+ break
6057
+ unit.append(stripped)
6058
+ end = j
6059
+ if _parse_glossary_delta(" ".join(unit)) is None:
6060
+ return None
6061
+ last = lines[end]
6062
+ eol = last[len(last.rstrip("\n")):]
6063
+ lines[end] = last.rstrip("\n") + f" [{_FOLDED} foundation-version {version}]" + eol
6064
+ return "".join(lines)
6065
+
6066
+
6067
+ def _collect_glossary_deltas(root: Path) -> tuple[list[dict], int]:
6068
+ """Scan every DONE task's own `Glossary deltas:` line for a clean, not-yet-resolved
6069
+ consolidation candidate.
6070
+
6071
+ Returns (candidates, skipped_count): candidates are {task, term, definition} dicts, READ-ONLY.
6072
+ skipped_count tallies DONE tasks whose line is non-"none", not already resolved-stamped, but
6073
+ does NOT cleanly parse — measured and reported (never silently invisible), per this
6074
+ mechanism's own freeze decision: "count and report skipped/unparseable lines"."""
6075
+ candidates: list[dict] = []
6076
+ skipped = 0
6077
+ tasks_dir = root / "tasks"
6078
+ if not tasks_dir.is_dir():
6079
+ return candidates, skipped
6080
+ for task_md in sorted(tasks_dir.glob("*/TASK.md")):
6081
+ slug = task_md.parent.name
6082
+ if _read_task_phase(root, slug) != "done":
6083
+ continue
6084
+ try:
6085
+ text = task_md.read_text(encoding="utf-8")
6086
+ except OSError:
6087
+ continue
6088
+ lines = text.splitlines()
6089
+ collected = None
6090
+ for i, line in enumerate(lines):
6091
+ m = _GLOSSARY_LINE_RE.match(line)
6092
+ if not m:
6093
+ continue
6094
+ unit = [m.group(1).strip()]
6095
+ for cont in lines[i + 1:]:
6096
+ stripped = cont.strip()
6097
+ if not stripped or _TASK_ATTR_LINE_RE.match(stripped) or stripped.startswith("<!--"):
6098
+ break
6099
+ unit.append(stripped)
6100
+ collected = " ".join(unit).strip()
6101
+ break
6102
+ if collected is None:
6103
+ continue
6104
+ if not collected or collected.lower().startswith("none") or _GLOSSARY_STAMP_RE.search(collected):
6105
+ continue # not a candidate — already handled or none
6106
+ parsed = _parse_glossary_delta(collected)
6107
+ if parsed is None:
6108
+ skipped += 1
6109
+ continue
6110
+ term, definition = parsed
6111
+ candidates.append({"task": slug, "term": term, "definition": definition})
6112
+ return candidates, skipped
6113
+
6114
+
6115
+ def _glossary_has_term(glossary_text: str, term: str) -> bool:
6116
+ """Case-insensitive match of `term` against the text before GLOSSARY.md's own first ': '
6117
+ on each line — the existing-term dup check (`_FOLD_ROUTES`-style, GLOSSARY.md-specific)."""
6118
+ needle = term.strip().lower()
6119
+ for line in glossary_text.splitlines():
6120
+ idx = line.find(": ")
6121
+ if idx != -1 and line[:idx].strip().lower() == needle:
6122
+ return True
6123
+ return False
6124
+
5754
6125
 
5755
6126
  def _fold_competency_delta(text: str, version: int, comps=None) -> str | None:
5756
6127
  """Flip EVERY open competency lesson in `text` to resolved + append ` [<resolved> foundation-version N]`.
@@ -5831,7 +6202,16 @@ def cmd_fold(args: argparse.Namespace) -> None:
5831
6202
  if want_task and it["task"] != want_task:
5832
6203
  continue
5833
6204
  selected.append({**it, "comp": comp})
5834
- if not selected:
6205
+
6206
+ # fold-glossary-deltas: --comp GLOSSARY | (no --comp) ALSO folds glossary terms; a specific
6207
+ # competency (--comp DDD etc.) narrows AWAY from glossary, matching the frozen contract.
6208
+ glossary_selected: list[dict] = []
6209
+ glossary_skipped = 0
6210
+ if want_comp is None or want_comp == "GLOSSARY":
6211
+ glossary_candidates, glossary_skipped = _collect_glossary_deltas(root)
6212
+ glossary_selected = [it for it in glossary_candidates if not want_task or it["task"] == want_task]
6213
+
6214
+ if not selected and not glossary_selected:
5835
6215
  scope = (f"task '{want_task}'" if want_task else "the project") + \
5836
6216
  (f", competency {want_comp}" if want_comp else "")
5837
6217
  _die(f"no_open_deltas: no open lesson to consolidate in {scope} (see `add.py deltas`)")
@@ -5878,15 +6258,45 @@ def cmd_fold(args: argparse.Namespace) -> None:
5878
6258
  "seed the persona first (setup) or fix the slug")
5879
6259
  persona_paths[slug] = ppath
5880
6260
 
6261
+ # glossary routing — GLOSSARY.md must exist to fold into (this mechanism never creates it).
6262
+ glossary_path = root / "GLOSSARY.md"
6263
+ if glossary_selected and not glossary_path.exists():
6264
+ _die("missing_glossary_file: no .add/GLOSSARY.md to consolidate a glossary term into — "
6265
+ "create it first (or narrow away from --comp GLOSSARY) and re-run")
6266
+
5881
6267
  # ── build EVERY edit in memory before writing anything ──────────────────────────────────────
5882
6268
  comps_filter = {want_comp} if want_comp else None
6269
+ comp_task_set = {it["task"] for it in selected}
6270
+ glossary_task_set = {it["task"] for it in glossary_selected}
5883
6271
  task_new: dict[str, str] = {}
5884
- for slug in dict.fromkeys(it["task"] for it in selected):
6272
+ for slug in list(dict.fromkeys(it["task"] for it in selected)) + [
6273
+ s for s in dict.fromkeys(it["task"] for it in glossary_selected) if s not in comp_task_set]:
5885
6274
  tmd = root / "tasks" / slug / "TASK.md"
5886
- flipped = _fold_competency_delta(tmd.read_text(encoding="utf-8"), new_v, comps_filter)
5887
- if flipped is None: # defensive: selected ⇒ ≥1 open here
5888
- _die(f"no_open_deltas: task '{slug}' lost its open lesson mid-session")
5889
- task_new[slug] = flipped
6275
+ body = tmd.read_text(encoding="utf-8")
6276
+ if slug in comp_task_set:
6277
+ flipped = _fold_competency_delta(body, new_v, comps_filter)
6278
+ if flipped is None: # defensive: selected ⇒ ≥1 open here
6279
+ _die(f"no_open_deltas: task '{slug}' lost its open lesson mid-session")
6280
+ body = flipped
6281
+ if slug in glossary_task_set:
6282
+ gflipped = _fold_glossary_delta(body, new_v)
6283
+ if gflipped is None: # defensive: selected ⇒ ≥1 clean candidate here
6284
+ _die(f"no_open_deltas: task '{slug}' lost its glossary delta mid-session")
6285
+ body = gflipped
6286
+ task_new[slug] = body
6287
+
6288
+ # glossary transcription — append clean, UNSTAMPED "Term: definition" lines (GLOSSARY.md's own
6289
+ # existing convention); a case-insensitive-existing term is skipped, never duplicated.
6290
+ glossary_text = glossary_path.read_text(encoding="utf-8") if glossary_selected else ""
6291
+ glossary_new_lines: list[str] = []
6292
+ for it in glossary_selected:
6293
+ if _glossary_has_term(glossary_text, it["term"]):
6294
+ continue
6295
+ line = f"{it['term']}: {it['definition']}"
6296
+ if glossary_text and not glossary_text.endswith("\n"):
6297
+ glossary_text += "\n"
6298
+ glossary_text += line + "\n"
6299
+ glossary_new_lines.append(line)
5890
6300
 
5891
6301
  def _bullet(it):
5892
6302
  ev = f" (evidence: {it['evidence']})" if it["evidence"] else ""
@@ -5928,13 +6338,17 @@ def cmd_fold(args: argparse.Namespace) -> None:
5928
6338
  persona_new[slug] = after
5929
6339
 
5930
6340
  counts = {c: sum(1 for it in selected if it["comp"] == c) for c in _COMPETENCY_ORDER}
5931
- count_str = " · ".join(f"{c} {counts[c]}" for c in _COMPETENCY_ORDER if counts[c])
6341
+ count_parts = [f"{c} {counts[c]}" for c in _COMPETENCY_ORDER if counts[c]]
6342
+ if glossary_selected:
6343
+ count_parts.append(f"GLOSSARY {len(glossary_selected)}")
6344
+ count_str = " · ".join(count_parts)
5932
6345
  scope = "all" if not (want_task or want_comp) else " ".join(
5933
6346
  filter(None, [f"--task {want_task}" if want_task else "",
5934
6347
  f"--comp {want_comp}" if want_comp else ""]))
6348
+ glossary_row_note = f"; {len(glossary_new_lines)} glossary term(s) added" if glossary_selected else ""
5935
6349
  row = (f"| {date.today().isoformat()} | {_FOLD_VERB} {scope} → foundation-version {new_v} "
5936
6350
  f"({count_str}) | consolidate captured OBSERVE lessons into the versioned foundation "
5937
- f"| {len(selected)} lessons open→{_FOLDED}; +{len(selected)} routed bullets; {prev_v}→{new_v} |")
6351
+ f"| {len(selected)} lessons open→{_FOLDED}; +{len(selected)} routed bullets{glossary_row_note}; {prev_v}→{new_v} |")
5938
6352
  proj_text = _prepend_key_decision_row(proj_text, row)
5939
6353
  proj_text = re.sub(r"foundation-version:\s*\d+", f"foundation-version: {new_v}", proj_text, count=1)
5940
6354
 
@@ -5953,11 +6367,22 @@ def cmd_fold(args: argparse.Namespace) -> None:
5953
6367
  writes.append((persona_paths[slug], body))
5954
6368
  if persona_new:
5955
6369
  touched.append(f"{len(persona_new)} persona")
6370
+ if glossary_selected and glossary_new_lines:
6371
+ writes.append((glossary_path, glossary_text))
6372
+ touched.append("GLOSSARY.md")
5956
6373
  _atomic_write_many(writes)
5957
6374
 
5958
- print(f"{_FOLDED} {len(selected)} lessons -> foundation-version {new_v}")
5959
- print(f" {count_str}")
6375
+ print(f"{_FOLDED} {len(selected)} lesson(s) -> foundation-version {new_v}")
6376
+ if count_str:
6377
+ print(f" {count_str}")
5960
6378
  print(f" bumped PROJECT.md {prev_v} -> {new_v}")
6379
+ if glossary_selected:
6380
+ added = len(glossary_new_lines)
6381
+ dup = len(glossary_selected) - added
6382
+ dup_note = f"; {dup} duplicate term(s) skipped" if dup else ""
6383
+ print(f" glossary: {added} term(s) added{dup_note}")
6384
+ if glossary_skipped:
6385
+ print(f" glossary: {glossary_skipped} line(s) unparseable, skipped")
5961
6386
  print(f" files: {', '.join(touched)}")
5962
6387
  print(_next_footer(root, state))
5963
6388
 
@@ -6045,7 +6470,9 @@ def _audit_findings(root: Path, state: dict) -> tuple[int, list[dict]]:
6045
6470
  if marked:
6046
6471
  f(slug, "risk_accepted_security",
6047
6472
  "a waiver on a marked security item is never allowed")
6048
- if not all(re.search(rf"{k}:\s*(?!<)\S", s6)
6473
+ # case-insensitive: human-signed records write Owner:/Ticket:/Expires: —
6474
+ # the seam is the field EXISTING, casing is presentation (waiver-field-case)
6475
+ if not all(re.search(rf"{k}:\s*(?!<)\S", s6, re.IGNORECASE)
6049
6476
  for k in ("owner", "ticket", "expires")):
6050
6477
  f(slug, "waiver_incomplete",
6051
6478
  "RISK-ACCEPTED needs owner · ticket · expires")
@@ -6188,7 +6615,12 @@ def _guarantee_lint_notices(root: Path, state: dict) -> dict:
6188
6615
  "advisor_residue_on_mechanical_mis_tier": advisor_residue_on_mechanical_mis_tier,
6189
6616
  "rule_coverage_gap": rule_coverage_gap,
6190
6617
  "contract_report_unrecorded": contract_report_unrecorded,
6191
- "verify_report_unrecorded": verify_report_unrecorded}
6618
+ "verify_report_unrecorded": verify_report_unrecorded,
6619
+ # DERIVED rollup (verify-record-rollup): one summary list over the four §6-record
6620
+ # lists — additive; the per-code lists above stay the source of truth.
6621
+ "verify_record_incomplete": sorted(
6622
+ set(shallow) | set(refute_unrecorded)
6623
+ | set(advisor_verdict_unrecorded) | set(verify_report_unrecorded))}
6192
6624
 
6193
6625
 
6194
6626
  def cmd_audit(args: argparse.Namespace) -> None:
@@ -6251,6 +6683,11 @@ def cmd_audit(args: argparse.Namespace) -> None:
6251
6683
  vr = glints["verify_report_unrecorded"]
6252
6684
  print(f"audit: verify_report_unrecorded — {len(vr)} task(s): {', '.join(vr)} "
6253
6685
  f"— record the rendered gate report (§6 `Reported: yes`); a spot-audit is the backstop")
6686
+ if glints["verify_record_incomplete"]:
6687
+ vi = glints["verify_record_incomplete"]
6688
+ print(f"audit: verify_record_incomplete — {len(vi)} task(s): {', '.join(vi)} "
6689
+ f"— fill the §6 verify record (rollup of the four detail lines above: "
6690
+ f"deep-check · refute-read · 3-lens · gate-report)")
6254
6691
  if not findings and not skips and not glints["shallow"] and not glints["risk_unset"] \
6255
6692
  and not glints["refute_unrecorded"] and not glints["advisor_verdict_unrecorded"] \
6256
6693
  and not glints["sensitivity_unset"] \
@@ -6580,6 +7017,13 @@ def cmd_release_report(args: argparse.Namespace) -> None:
6580
7017
  L.append(f" - {c['milestone']}: {c['retro'] or '(no RETRO record)'} "
6581
7018
  f"({c['carried_deltas']} carried · {len(c['key_decisions'])} key decision(s))")
6582
7019
  L.append("")
7020
+ _carried = _collect_carried_spec_deltas(root)
7021
+ L.append(f"Carried ({len(_carried)}) — deferred spec deltas riding across releases (re-triage each cut):")
7022
+ for cd in _carried[:10]:
7023
+ L.append(f" - [{cd['task']}] {cd['text'][:100]}")
7024
+ if len(_carried) > 10:
7025
+ L.append(f" … and {len(_carried) - 10} more — add.py deltas --carried")
7026
+ L.append("")
6583
7027
  L.append(f"Waivers ({s['waivers']}) — open RISK-ACCEPTED riding into the cut, soonest expiry first:")
6584
7028
  for w in d["waivers"]:
6585
7029
  L.append(f" - {w['slug']}: {w['owner']} · {w['ticket']} · expires {w['expires']}")
@@ -7056,11 +7500,24 @@ def build_parser() -> argparse.ArgumentParser:
7056
7500
  pa.add_argument("--skip-freeze", action="store_true",
7057
7501
  help="cross tests->build on a DRAFT §3, recording an auditable freeze_skipped "
7058
7502
  "marker (the universal freeze gate's only bypass; never auto-freezes §3)")
7503
+ pa.add_argument("--to", default=None,
7504
+ help="fast-forward the bundle bookkeeping to this phase (at most `tests`); "
7505
+ "every crossing guard still runs per step")
7059
7506
  pa.set_defaults(func=cmd_advance, _opt_positionals=("slug",))
7060
7507
 
7508
+ prx = sub.add_parser("re-cross", help="re-arm the tests->build snapshots after a "
7509
+ "HUMAN-APPROVED post-freeze test change")
7510
+ prx.add_argument("slug", nargs="?", default=None)
7511
+ prx.add_argument("--by", default="",
7512
+ help="the human approver (required — a post-freeze test change is human-approved)")
7513
+ prx.set_defaults(func=cmd_recross, _opt_positionals=("slug",))
7514
+
7061
7515
  pg = sub.add_parser("gate", help="record a verify gate outcome")
7062
- pg.add_argument("outcome", choices=GATES)
7063
- pg.add_argument("slug", nargs="?", default=None)
7516
+ pg.add_argument("outcome", nargs="?", default=None) # validated in cmd_gate (gate-explain
7517
+ pg.add_argument("slug", nargs="?", default=None) # made it optional under --explain)
7518
+ pg.add_argument("--explain", action="store_true",
7519
+ help="READ-ONLY: print the composed auto-pass/escalation path for the task "
7520
+ "(autonomy · risk · sensitivity · advisor verdict), then exit")
7064
7521
  pg.add_argument("--owner", help="RISK-ACCEPTED waiver: accountable owner")
7065
7522
  pg.add_argument("--ticket", help="RISK-ACCEPTED waiver: tracking ticket/link")
7066
7523
  pg.add_argument("--expires", help="RISK-ACCEPTED waiver: expiry date")
@@ -7081,6 +7538,13 @@ def build_parser() -> argparse.ArgumentParser:
7081
7538
  pst.add_argument("posture", nargs="?", default=None, help="set: <parallel|sequential>")
7082
7539
  pst.set_defaults(func=cmd_streams, _opt_positionals=("posture",))
7083
7540
 
7541
+ pwt = sub.add_parser("worktree-prep",
7542
+ help="cut a worker worktree at HEAD + materialize gitignored "
7543
+ ".add/tooling + .add/docs + echo the fork base (streams.md recipe)")
7544
+ pwt.add_argument("slug", nargs="?", default=None, help="task the worktree is for (default: active)")
7545
+ pwt.add_argument("--dir", default=None, help="worktree path (default: ../<project>-wt-<slug>)")
7546
+ pwt.set_defaults(func=cmd_worktree_prep)
7547
+
7084
7548
  pto = sub.add_parser("todo", help="capture / list / close a lightweight backlog todo (jot an idea)")
7085
7549
  pto.add_argument("text", nargs="?", default=None,
7086
7550
  help="todo text to capture; omit to LIST open todos")
@@ -7208,7 +7672,8 @@ def build_parser() -> argparse.ArgumentParser:
7208
7672
  help="record one retrospective consolidation of open lessons into the "
7209
7673
  "versioned foundation (stamp + route + version-bump, atomic)")
7210
7674
  pfo.add_argument("--task", help="narrow to one task's open lessons")
7211
- pfo.add_argument("--comp", choices=_COMPETENCY_ORDER, help="narrow to one competency's open lessons")
7675
+ pfo.add_argument("--comp", choices=[*_COMPETENCY_ORDER, "GLOSSARY"],
7676
+ help="narrow to one competency's open lessons, or GLOSSARY for glossary-only")
7212
7677
  pfo.set_defaults(func=cmd_fold)
7213
7678
 
7214
7679
  pgr = sub.add_parser("graduation-report",