@pilotspace/add 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ """add_engine — the ADD engine, modularized.
2
+
3
+ add.py is the runnable entry; the engine body lives here as focused modules.
4
+ Started by the engine-modularization milestone (first module: constants).
5
+ """
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python3
2
+ """add_engine.accessors — pure active-task / active-milestone state-dict accessors.
3
+
4
+ In-memory readers/mutators over the state dict (no file IO, no imports beyond
5
+ __future__). Extracted from add.py (engine-modularization 4/N); add.py re-exports
6
+ them as module globals so `add._active_task` etc. resolve unchanged.
7
+ """
8
+ from __future__ import annotations
9
+
10
+
11
+ def _active_milestone(state: dict) -> str | None:
12
+ """The primary active milestone — the N<=1 scalar mirror (== active_milestones[0])."""
13
+ return state.get("active_milestone")
14
+
15
+ def _active_task(state: dict, milestone: str | None = None) -> str | None:
16
+ """The active task: per-milestone when `milestone` is given (partial-state -> None),
17
+ else the global/primary scalar active task. Total — never raises."""
18
+ if milestone is None:
19
+ return state.get("active_task")
20
+ return (state.get("active_tasks") or {}).get(milestone)
21
+
22
+ def _set_active_milestone(state: dict, slug: str | None) -> None:
23
+ """Set the primary active milestone, keeping `active_milestones` consistent (N<=1 sync)."""
24
+ state["active_milestone"] = slug
25
+ state["active_milestones"] = [] if slug is None else [slug]
26
+
27
+ def _set_active_task(state: dict, slug: str | None, milestone: str | None = None) -> None:
28
+ """Set the active task, keeping the scalar mirror AND the per-milestone map in sync.
29
+ With no owning active milestone the active task is scalar-only (the migration's orphan
30
+ rule); clearing (slug is None) pops the milestone's entry."""
31
+ state["active_task"] = slug
32
+ ms = milestone if milestone is not None else _active_milestone(state)
33
+ tasks_map = state.setdefault("active_tasks", {})
34
+ if ms is None:
35
+ return
36
+ if slug is None:
37
+ tasks_map.pop(ms, None)
38
+ else:
39
+ tasks_map[ms] = slug
40
+
41
+ def _activate_milestone(state: dict, slug: str) -> None:
42
+ """Add a milestone to the active SET (idempotent) and make it the primary focus,
43
+ syncing the scalar active_task to that milestone's entry. Does NOT remove other members
44
+ (this is how a user reaches N>=2 active milestones)."""
45
+ ms_list = state.setdefault("active_milestones", [])
46
+ if slug not in ms_list:
47
+ ms_list.append(slug)
48
+ state["active_milestone"] = slug
49
+ state["active_task"] = (state.get("active_tasks") or {}).get(slug)
50
+
51
+ def _deactivate_milestone(state: dict, slug: str) -> None:
52
+ """Remove a milestone from the active SET, pop its active-task entry, and (if it was the
53
+ primary) repoint the primary to the most-recent remaining member (or None when empty)."""
54
+ ms_list = state.setdefault("active_milestones", [])
55
+ if slug in ms_list:
56
+ ms_list.remove(slug)
57
+ (state.setdefault("active_tasks", {})).pop(slug, None)
58
+ if state.get("active_milestone") == slug:
59
+ new = ms_list[-1] if ms_list else None
60
+ state["active_milestone"] = new
61
+ state["active_task"] = (state.get("active_tasks") or {}).get(new) if new else None
@@ -0,0 +1,58 @@
1
+ """add_engine.autonomy — autonomy-level read & resolve (engine-modularization 16/N).
2
+
3
+ Read the declared level off a task header, resolve the effective level (declared else the
4
+ project default), and read the project default token. A closed, unpatched cluster
5
+ (transitive-closure AST = zero outbound). The cluster-private _AUTONOMY_LINE_RE lives here;
6
+ the SHARED _AUTONOMY_LEVELS lives in constants.py. Deps: constants + taskdoc (_task_header)
7
+ + stdlib; no add.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from pathlib import Path
13
+
14
+ from add_engine.constants import _AUTONOMY_LEVELS
15
+ from add_engine.taskdoc import _task_header
16
+
17
+ _AUTONOMY_LINE_RE = re.compile(r"(?:^|·)[ \t]*autonomy:[ \t]*([^\s<#|]+)", re.MULTILINE)
18
+
19
+
20
+ def _autonomy_level(hdr: str):
21
+ """The declared autonomy rung from a TASK.md header region (HTML comments
22
+ already stripped by _task_header). Returns a member of _AUTONOMY_LEVELS, or
23
+ None when no `autonomy:` line is present (UNSET — an unfilled `<…>` placeholder,
24
+ whose value the regex declines, counts as unset), or "?" when a REAL token outside
25
+ the set was written (unknown). PURE."""
26
+ m = _AUTONOMY_LINE_RE.search(hdr)
27
+ if not m:
28
+ return None
29
+ tok = m.group(1).strip().lower()
30
+ return tok if tok in _AUTONOMY_LEVELS else "?"
31
+
32
+ def _effective_autonomy(root: Path, state: dict, slug: str) -> str:
33
+ """The autonomy rung that governs `slug` right now: the task's own declared rung,
34
+ falling back to the project default when the task line is UNSET (None) or an
35
+ unrecognized token ("?") — the same fail-safe chain cmd_new_task seeds from
36
+ (_project_autonomy: absent -> auto, garbled -> conservative). PURE. `state` is unused
37
+ today; it is kept in the signature beside _driver_stop for symmetry."""
38
+ lvl = _autonomy_level(_task_header(root, slug))
39
+ return lvl if lvl in _AUTONOMY_LEVELS else _project_autonomy(root)
40
+
41
+ def _project_autonomy_token(root: Path):
42
+ """The RAW autonomy declaration in PROJECT.md — a recognized rung, None when no
43
+ declaration line is present, or "?" for a real-but-unrecognized token. Uses the
44
+ anchored _autonomy_level (a title/prose substring is never a declaration) with
45
+ HTML comments stripped. Unreadable foundation -> None. Read-only and PURE."""
46
+ try:
47
+ text = (root / "PROJECT.md").read_text(encoding="utf-8")
48
+ except OSError:
49
+ return None
50
+ return _autonomy_level(re.sub(r"<!--.*?-->", "", text, flags=re.S))
51
+
52
+ def _project_autonomy(root: Path) -> str:
53
+ """The autonomy rung a new task INHERITS from the project default. Fail-SAFE:
54
+ no declaration -> "auto" (the method default; v7: absent = auto); an unrecognized
55
+ token -> "conservative" (NEVER silently "auto"); an unreadable foundation -> "auto".
56
+ Read-only and PURE — mirrors _project_goal; the seed source for cmd_new_task."""
57
+ tok = _project_autonomy_token(root)
58
+ return "auto" if tok is None else ("conservative" if tok == "?" else tok)
@@ -0,0 +1,117 @@
1
+ """add_engine.components — the component-aware-add subsystem (engine-modularization 11/N).
2
+
3
+ Registry (.add/components.toml) + produced/consumed contracts + cross-repo federation +
4
+ scope confinement. A closed, unpatched cluster (transitive-closure AST = zero outbound).
5
+ Replicates add.py's degrade-safe tomllib guard so `import` stays safe on Python < 3.11
6
+ (where the registry degrades to opt-out). Opt-in: no components.toml -> every reader is
7
+ byte-identical to single-component. Deps: stdlib only (no add, no add_engine).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from pathlib import Path
13
+
14
+ try: # component registry parse (Python 3.11+ stdlib); degrade-safe
15
+ import tomllib
16
+ except ModuleNotFoundError: # < 3.11: registry unsupported -> tomllib None -> opt-out
17
+ tomllib = None
18
+
19
+
20
+ def _confined(p: Path, rootp: Path) -> bool:
21
+ """True only if p resolves (symlinks followed) inside rootp; errors -> False.
22
+ The v2 confinement check — no read is attempted on a path that fails it."""
23
+ try:
24
+ return p.resolve().is_relative_to(rootp)
25
+ except OSError:
26
+ return False
27
+
28
+ def _components(root: Path) -> dict[str, dict]:
29
+ """The registry from .add/components.toml → {name: {root, verify, green_bar,
30
+ language}}. `root` required per entry; an entry missing it is skipped (the finding
31
+ surface reports it). `verify` is stored OPAQUE — parsed as data, NEVER executed. PURE."""
32
+ if tomllib is None:
33
+ return {}
34
+ try:
35
+ raw = (root / "components.toml").read_bytes()
36
+ except OSError:
37
+ return {}
38
+ try:
39
+ data = tomllib.loads(raw.decode("utf-8"))
40
+ except (tomllib.TOMLDecodeError, UnicodeDecodeError, ValueError):
41
+ return {}
42
+ out: dict[str, dict] = {}
43
+ for name, spec in (data.get("component") or {}).items():
44
+ # "?" is the reserved unknown-binding sentinel (_task_component) — a component
45
+ # named "?" would collide and silently drop cover, so it never registers.
46
+ if name == "?" or not isinstance(spec, dict) or not isinstance(spec.get("root"), str):
47
+ continue
48
+ out[name] = {"root": spec["root"], "verify": spec.get("verify"),
49
+ "green_bar": spec.get("green_bar"), "language": spec.get("language")}
50
+ return out
51
+
52
+ def _cite_region(body: str) -> str:
53
+ """The user-authored "Build expectations" evidence region of a §6 body, stamp-stripped —
54
+ the only place a per-component green-bar cite counts (per-component-verify, v3). PURE.
55
+
56
+ The marker matches BOTH template shapes: the standard "### Build expectations …" heading AND
57
+ the fast-lane bare "Build expectations (from …):" line, running up to the GATE RECORD sub-block.
58
+ So the top-of-§6 checklist ("- [ ] all tests pass") and the "Outcome: <PASS|…>" placeholder are
59
+ excluded, and a component-bound FAST task is still citable. The trailing strip removes the
60
+ engine's own "component: … · expected green-bar: …" stamp wherever it landed, so a stamp that
61
+ fell inside the region can never self-satisfy the gate. No marker -> "" (fail-closed for a bound
62
+ task: it must declare its evidence)."""
63
+ m = re.search(r"(?im)^#*[ \t]*Build expectations\b.*?(?=\n#+[ \t]*GATE RECORD\b|\Z)", body, re.DOTALL)
64
+ region = m.group(0) if m else ""
65
+ return re.sub(r"(?m)^component:.*·.*expected green-bar:.*$", "", region)
66
+
67
+ def _contracts(root: Path) -> dict[str, dict]:
68
+ """[contract.<id>] from .add/components.toml -> {id: {producer: str, consumers: list[str]}}.
69
+ A malformed entry (producer not a str) is skipped (the finding surface reports it). PURE."""
70
+ if tomllib is None:
71
+ return {}
72
+ try:
73
+ data = tomllib.loads((root / "components.toml").read_bytes().decode("utf-8"))
74
+ except (OSError, tomllib.TOMLDecodeError, UnicodeDecodeError, ValueError):
75
+ return {}
76
+ out: dict[str, dict] = {}
77
+ for cid, spec in (data.get("contract") or {}).items():
78
+ if not isinstance(spec, dict) or not isinstance(spec.get("producer"), str):
79
+ continue
80
+ cons = spec.get("consumers")
81
+ out[cid] = {"producer": spec["producer"],
82
+ "consumers": [c for c in cons if isinstance(c, str)] if isinstance(cons, list) else []}
83
+ return out
84
+
85
+ def _federation(root: Path) -> dict[str, dict]:
86
+ """[federation.<id>] from .add/components.toml -> {id: {source: str, pin: str|None}}.
87
+ The cross-REPO join: a consumer repo names where a producer repo's published snapshot lives.
88
+ A malformed entry (no string source) is skipped; a non-string `pin` degrades to None. Degrade-safe
89
+ — never raises. PURE. On Python < 3.11 (no tomllib) this returns {} like the other component
90
+ readers, so `federate` reports federation_unknown — components.toml needs a 3.11+ runtime."""
91
+ if tomllib is None:
92
+ return {}
93
+ try:
94
+ data = tomllib.loads((root / "components.toml").read_bytes().decode("utf-8"))
95
+ except (OSError, tomllib.TOMLDecodeError, UnicodeDecodeError, ValueError):
96
+ return {}
97
+ out: dict[str, dict] = {}
98
+ for fid, spec in (data.get("federation") or {}).items():
99
+ if not isinstance(spec, dict) or not isinstance(spec.get("source"), str):
100
+ continue
101
+ pin = spec.get("pin")
102
+ out[fid] = {"source": spec["source"], "pin": pin if isinstance(pin, str) else None}
103
+ return out
104
+
105
+ def _contract_snapshot(root: Path, cid: str) -> Path:
106
+ return root / "contracts" / f"{cid}.json"
107
+
108
+ def _in_scope(rel: str, declared: list[str]) -> bool:
109
+ """True when rel falls under any declared token — exact match for a file
110
+ token, whole-subtree prefix containment for a directory token ('…/')."""
111
+ for tok in declared:
112
+ if tok.endswith("/"):
113
+ if rel.startswith(tok) or rel == tok.rstrip("/"):
114
+ return True
115
+ elif rel == tok:
116
+ return True
117
+ return False
@@ -0,0 +1,221 @@
1
+ """add_engine.constants — engine constants (moved verbatim from add.py).
2
+
3
+ Pure module-level constants. add.py re-exports these so `import add; add.STAGES`
4
+ (and the 6 _-prefixed names) still resolve. `__all__` lists the public names so
5
+ `from add_engine.constants import *` brings exactly them (not the Path import).
6
+ """
7
+ import re
8
+ from pathlib import Path
9
+
10
+ __all__ = [
11
+ "ROOT_DIRNAME",
12
+ "STATE_FILE",
13
+ "MILESTONE_FILE",
14
+ "GOAL_UNSET",
15
+ "STAGES",
16
+ "GRADUATION_CUE",
17
+ "RELEASABLE_CUE",
18
+ "RELEASES_FILE",
19
+ "PHASES",
20
+ "GATES",
21
+ "HEAL_CAP",
22
+ "PHASE_GUIDE",
23
+ "PHASE_OWNER",
24
+ "SETUP_FILES",
25
+ "GUIDELINE_FILES",
26
+ "RULES_FILE_REL",
27
+ "WORKFLOW_HEADINGS",
28
+ ]
29
+
30
+ ROOT_DIRNAME = ".add"
31
+ STATE_FILE = "state.json"
32
+ MILESTONE_FILE = "MILESTONE.md"
33
+ # The project GOAL (v20) is read live from PROJECT.md — never copied into state.json
34
+ # (single-source; the foundation is the truth). A missing/blank source degrades to
35
+ # this sentinel so the read-only orientation surfaces never blank or crash.
36
+ GOAL_UNSET = "(unset — add a 'goal:' line to PROJECT.md)"
37
+ STAGES = ("prototype", "poc", "mvp", "production")
38
+ # v22 stage-graduation: the read-only cue `status` shows when the MVP is covered.
39
+ # Worded as the ACTION (never a file) so it stands before graduate.md exists.
40
+ GRADUATION_CUE = "MVP covered → propose graduation"
41
+ # release-altitude: the read-only cue `status` shows when ≥1 closed milestone is
42
+ # unreleased. The 5th scope level (release.md). `{n}` is filled at print time; the
43
+ # wording matches SKILL.md's "Beyond the bundle" cross-ref byte-for-byte.
44
+ RELEASABLE_CUE = "releasable: {n} milestone(s) closed since last release"
45
+ # the append-only release ledger lives at the PROJECT ROOT (the dir containing .add/),
46
+ # a sibling of CHANGELOG.md — NOT inside .add/. The ledger IS the attribution source:
47
+ # a milestone is "released" iff its slug appears on a `milestones:` row.
48
+ RELEASES_FILE = "RELEASES.md"
49
+ PHASES = ("ground", "specify", "scenarios", "contract", "tests", "build", "verify", "observe", "done")
50
+ GATES = ("none", "PASS", "RISK-ACCEPTED", "HARD-STOP")
51
+ # heal-then-escalate (verify-integrity): the bounded self-heal loop cap. A CONFIRMED cheat
52
+ # (mechanical tripwire divergence, or an agent-reported semantic refute-read finding) returns
53
+ # the task to BUILD for an honest redo; after HEAL_CAP such attempts the next confirmed cheat
54
+ # forces a HARD-STOP escalation to the human. MONOTONIC — attempts never auto-resets (a gamed
55
+ # green is never auto-passed; the loop is never unbounded).
56
+ HEAL_CAP = 3
57
+
58
+
59
+
60
+ # `add.py guide` copy: per-phase (concrete next action, book chapter to read).
61
+ # Keep the action wording aligned with each phase's EXIT line in the TASK template.
62
+ PHASE_GUIDE = {
63
+ "ground": ("gather the real codebase the task touches — files, symbols, signatures, conventions, and the anchor points the contract will cite; defer to PROJECT.md/CONVENTIONS.md and gather only the task delta",
64
+ "02-the-flow.md"),
65
+ "specify": ("state every rule — Must / Reject (+ named code) / After; rank assumptions lowest-confidence first and flag the biggest risk",
66
+ "03-step-1-specify.md"),
67
+ "scenarios": ("write one Given/When/Then per Must AND per Reject; every result observable",
68
+ "04-step-2-scenarios.md"),
69
+ "contract": ("freeze the shape — signature, fields, error codes; names match the glossary",
70
+ "05-step-3-contract.md"),
71
+ "tests": ("write one failing test per scenario; run them RED for the right reason",
72
+ "06-step-4-tests.md"),
73
+ "build": ("write the minimum code to pass the tests; change no test and no contract",
74
+ "07-step-5-build.md"),
75
+ "verify": ("run the suite + non-functional checks, then record the gate",
76
+ "08-step-6-verify.md"),
77
+ "observe": ("note what to watch + the spec delta for the next loop",
78
+ "09-the-loop.md"),
79
+ "done": ("this task is done — pick the next feature",
80
+ "02-the-flow.md"),
81
+ }
82
+ # Phase -> who owns it, for the `--json` autonomy signal. An autonomous harness may run a
83
+ # phase only when owner=="ai" (stop is false); every other phase is a checkpoint. The map
84
+ # follows the book's who-does-what table (Verify is "human only"); `tests`/`build`/`observe`
85
+ # are AI-led. A phase missing here is `unmapped_phase` (fail closed) — never defaulted.
86
+ PHASE_OWNER = {
87
+ "ground": "ai",
88
+ "specify": "human", "scenarios": "human", "contract": "seam",
89
+ "tests": "ai", "build": "ai", "verify": "human", "observe": "ai", "done": "human",
90
+ }
91
+ SETUP_FILES = ("PROJECT.md", "CONVENTIONS.md", "GLOSSARY.md", "MODEL_REGISTRY.md", "dependencies.allowlist", "DESIGN.md", "SOUL.md")
92
+
93
+ # Scaffolded into .add/.gitignore at init so the engine's transient LOCAL artifacts
94
+ # never reach git. Bare-filename patterns match at any depth under .add/ (tasks/,
95
+ # milestones/, archive/). These are working state, not records: scope-snapshot.json
96
+ # is the tests->build touch baseline the verify scope-gate reads from disk (the
97
+ # durable scope declaration is the state.json anchor); pre-archive-state.bak.json is
98
+ # archive-milestone's pre-delete recovery net — needed on disk, never in history;
99
+ # pre-update-state.bak.json is the installer update's pre-write state backup (cli.js
100
+ # cmdUpdate / pip _installer.update) — same "on disk, never in git" need; .update-cache.json
101
+ # is the update-nudge's once-a-day registry throttle. All stay on disk; git-ignoring them
102
+ # is hygiene, never deletion. SINGLE-SOURCED: this constant is kept byte-identical to
103
+ # tooling/templates/gitignore.tmpl (test_gitignore_bak_seed parity) so the installers,
104
+ # which seed/refresh .add/.gitignore from that template on update, never drift from init.
105
+ _GITIGNORE_BODY = """\
106
+ # ADD engine transient artifacts — local working state, never committed.
107
+ # (Scaffolded by `add.py init`; refreshed additively by the installer on update.)
108
+ scope-snapshot.json
109
+ pre-archive-state.bak.json
110
+ pre-update-state.bak.json
111
+ .update-cache.json
112
+ """
113
+
114
+ # Guideline-injection targets + version-stable markers. NEVER change these marker
115
+ # strings: a re-run finds the old block by exact match, so changing them would
116
+ # orphan every block written by a prior version (see TASK guideline-inject).
117
+ GUIDELINE_FILES = ("AGENTS.md", "CLAUDE.md")
118
+ _GUIDE_BEGIN = "<!-- ADD:BEGIN — managed by `add.py sync-guidelines`; do not edit inside -->"
119
+ _GUIDE_END = "<!-- ADD:END -->"
120
+
121
+ # Rule-file mode (ccsk-style projects): instead of inlining the block into CLAUDE.md,
122
+ # write it to a dedicated rule file under .claude/rules/ and leave a one-line reference
123
+ # in CLAUDE.md's Workflows section. .claude/rules/ is a CLAUDE-only convention, so this
124
+ # mode only ever relocates CLAUDE.md — AGENTS.md/.clinerules keep the inline block.
125
+ RULES_FILE_REL = Path(".claude") / "rules" / "add-workflows.md"
126
+ # Headings (most→least specific) a project may already use to group rule/workflow links.
127
+ # Match is case-insensitive on the heading TEXT, at any `#` level.
128
+ WORKFLOW_HEADINGS = ("Rules & Workflows", "Workflows", "Rules")
129
+ _RULE_REF_LINE = "- ADD (AI-Driven Development) Workflows rules: ./.claude/rules/add-workflows.md"
130
+
131
+ # Minimal embedded fallback so the tool still works if templates/ is missing
132
+ # (circuit breaker: never hard-fail just because a template file was deleted).
133
+ _FALLBACK_TASK = """# TASK: {title}
134
+
135
+ slug: {slug} · created: {date} · stage: {stage}
136
+ autonomy: auto
137
+ phase: ground
138
+
139
+ ## 0 · GROUND
140
+ Touches (files · symbols · signatures):
141
+ Honors (patterns / conventions):
142
+ Anchors the contract cites:
143
+
144
+ ## 1 · SPECIFY
145
+ Feature:
146
+ Framings weighed:
147
+ Must:
148
+ Reject:
149
+ After:
150
+ Assumptions — lowest-confidence first:
151
+ ⚠ <most likely wrong> — lowest confidence because <why>; if wrong: <cost>
152
+
153
+ ## 2 · SCENARIOS
154
+ ## 3 · CONTRACT
155
+ Status: DRAFT
156
+ ## 4 · TESTS
157
+ ## 5 · BUILD
158
+ ## 6 · VERIFY
159
+ ### GATE RECORD
160
+ Outcome:
161
+ ## 7 · OBSERVE
162
+ ### Spec delta
163
+ ### Competency deltas
164
+ """
165
+
166
+
167
+ # Fast-lane fallback: the minimal TASK.md variant (sections {0,1,3,4,5,6}; §2 + §7 dropped).
168
+ # Mirrors templates/TASK.fast.md.tmpl's section set (circuit-breaker parity); a deleted
169
+ # templates/ never hard-fails the fast lane. Keeps the trust floor: §3 freeze-flag + Status,
170
+ # §6 GATE RECORD/Outcome, §0 Anchors, §4 red-before-build, §5 Scope.
171
+ _FALLBACK_TASK_FAST = """# TASK: {title}
172
+
173
+ slug: {slug} · created: {date} · stage: {stage}
174
+ autonomy: auto
175
+ phase: ground
176
+ fast: true
177
+
178
+ ## 0 · GROUND
179
+ Touches (files · symbols):
180
+ Anchors the contract cites:
181
+
182
+ ## 1 · SPECIFY
183
+ Feature:
184
+ Must:
185
+ Reject:
186
+ Accept:
187
+ Assumptions: ⚠ <most likely wrong> — why; if wrong: <cost>
188
+
189
+ ## 3 · CONTRACT
190
+ Least-sure flag surfaced at freeze:
191
+ Status: DRAFT
192
+
193
+ ## 4 · TESTS
194
+ Plan:
195
+ Tests live in: `./tests/` · MUST run red before Build.
196
+
197
+ ## 5 · BUILD
198
+ Scope (may touch): `./src/`
199
+
200
+ ## 6 · VERIFY
201
+ Build expectations (from §1 Accept + §3 CONTRACT):
202
+ ### GATE RECORD
203
+ Outcome:
204
+ Reviewed by:
205
+ """
206
+
207
+ _DEFAULT_WIDTH = 72 # fixed width for the persisted/canonical render (RETRO.md)
208
+
209
+
210
+ # --- shared delta-parsing regexes (used by taskdoc readers AND the deltas-web lint) ---
211
+ _DELTA_RE = re.compile(
212
+ r"\s*-\s*\[\s*(DDD|SDD|UDD|TDD|ADD)\s*·\s*(open|folded|rejected)\s*\]\s*(.+)$"
213
+ )
214
+ _EVIDENCE_RE = re.compile(r"^(.*?)\s*\(evidence:\s*(.*?)\)\s*$")
215
+ _SPEC_DELTA_RE = re.compile(
216
+ r"\s*-\s*\[\s*(SPEC)\s*·\s*(open|seeded|dropped)\s*\]\s*(.+)$"
217
+ )
218
+
219
+
220
+ # --- autonomy levels (shared: autonomy resolvers + _AUTONOMY_ORDER/cmd_autonomy) ---
221
+ _AUTONOMY_LEVELS = ("manual", "conservative", "auto")