@pilotspace/add 1.17.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +144 -0
- package/GETTING-STARTED.md +20 -64
- package/README.md +128 -56
- package/agents/add.md +68 -0
- package/bin/cli.js +86 -21
- package/package.json +2 -3
- package/personas-teacher/VENDOR.md +6 -0
- package/skill/add/SKILL.md +100 -134
- package/skill/add/adopt.md +28 -0
- package/skill/add/beyond.md +36 -0
- package/skill/add/deltas.md +25 -2
- package/skill/add/design.md +56 -74
- package/skill/add/{report-template.md → gate-udd.md} +28 -38
- package/skill/add/intake.md +56 -25
- package/skill/add/loop.md +32 -19
- package/skill/add/phases/build.md +78 -0
- package/skill/add/phases/direction.md +280 -0
- package/skill/add/phases/verify.md +160 -0
- package/skill/add/run.md +11 -18
- package/skill/add/terms.md +18 -0
- package/tooling/add.py +2383 -3261
- package/tooling/add_engine/autonomy.py +2 -37
- package/tooling/add_engine/components.py +4 -87
- package/tooling/add_engine/constants.py +145 -91
- package/tooling/add_engine/guidelines.py +9 -11
- package/tooling/add_engine/io_state.py +69 -2
- package/tooling/add_engine/predicates.py +90 -2
- package/tooling/add_engine/search.py +6 -6
- package/tooling/add_engine/taskdoc.py +15 -10
- package/tooling/templates/CONVENTIONS.md.tmpl +1 -1
- package/tooling/templates/GLOSSARY.md.tmpl +10 -4
- package/tooling/templates/MILESTONE.md.tmpl +20 -16
- package/tooling/templates/PLAN.md.tmpl +128 -0
- package/tooling/templates/PROJECT.md.tmpl +6 -1
- package/tooling/templates/PROMPT.persona.md.tmpl +17 -21
- package/tooling/templates/personas/_template.md.tmpl +33 -29
- package/tooling/templates/personas/build-engineer.md.tmpl +45 -0
- package/tooling/templates/personas/data-steward.md.tmpl +45 -0
- package/tooling/templates/personas/evidence-verifier.md.tmpl +45 -0
- package/tooling/templates/personas/platform-engineer.md.tmpl +45 -0
- package/tooling/templates/personas/product-lead.md.tmpl +51 -0
- package/tooling/templates/personas/quality-auditor.md.tmpl +47 -0
- package/tooling/templates/personas/release-manager.md.tmpl +52 -0
- package/tooling/templates/personas/security-gatekeeper.md.tmpl +45 -0
- package/tooling/templates/personas/software-architect.md.tmpl +45 -0
- package/tooling/templates/personas/stream-orchestrator.md.tmpl +44 -0
- package/tooling/templates/personas/technical-writer.md.tmpl +49 -0
- package/tooling/templates/personas/ux-experience-lead.md.tmpl +48 -0
- package/tooling/templates/specs/SPEC.md.tmpl +18 -0
- package/tooling/templates/udd-wireframe.md +1 -1
- package/agents/add-advisor.md +0 -31
- package/agents/add-build.md +0 -29
- package/agents/add-design.md +0 -32
- package/agents/add-persona.md +0 -33
- package/agents/add-verify.md +0 -36
- package/docs/00-introduction.md +0 -46
- package/docs/01-principles.md +0 -71
- package/docs/02-the-flow.md +0 -113
- package/docs/03-step-1-specify.md +0 -123
- package/docs/04-step-2-scenarios.md +0 -90
- package/docs/05-step-3-contract.md +0 -80
- package/docs/06-step-4-tests.md +0 -85
- package/docs/07-step-5-build.md +0 -82
- package/docs/08-step-6-verify.md +0 -105
- package/docs/09-the-loop.md +0 -75
- package/docs/10-setup-and-stages.md +0 -118
- package/docs/11-governance.md +0 -91
- package/docs/12-roles.md +0 -99
- package/docs/13-adoption.md +0 -67
- package/docs/14-foundation.md +0 -142
- package/docs/15-foundations-and-lineage.md +0 -106
- package/docs/16-releasing.md +0 -182
- package/docs/17-components.md +0 -143
- package/docs/18-personas.md +0 -80
- package/docs/README.md +0 -77
- package/docs/add-competencies.png +0 -0
- package/docs/add-flow.png +0 -0
- package/docs/add-foundation.png +0 -0
- package/docs/add-hierarchy.png +0 -0
- package/docs/add-milestone-task-lifecycle.png +0 -0
- package/docs/add-task-growth-wheel.png +0 -0
- package/docs/appendix-a-templates.md +0 -88
- package/docs/appendix-b-prompts.md +0 -154
- package/docs/appendix-c-glossary.md +0 -176
- package/docs/appendix-d-worked-example.md +0 -231
- package/docs/appendix-e-checklists.md +0 -92
- package/docs/appendix-f-requirements-matrix.md +0 -171
- package/docs/appendix-g-references.md +0 -106
- package/skill/add/advisor.md +0 -81
- package/skill/add/compact-foundation.md +0 -40
- package/skill/add/components.md +0 -52
- package/skill/add/confidence.md +0 -32
- package/skill/add/fold.md +0 -63
- package/skill/add/graduate.md +0 -39
- package/skill/add/phases/0-ground.md +0 -64
- package/skill/add/phases/0-setup.md +0 -103
- package/skill/add/phases/1-specify.md +0 -61
- package/skill/add/phases/2-scenarios.md +0 -48
- package/skill/add/phases/3-contract.md +0 -60
- package/skill/add/phases/4-tests.md +0 -61
- package/skill/add/phases/5-build.md +0 -62
- package/skill/add/phases/6-verify.md +0 -69
- package/skill/add/phases/7-observe.md +0 -45
- package/skill/add/phases/fast-lane.md +0 -34
- package/skill/add/release.md +0 -61
- package/skill/add/scope.md +0 -72
- package/skill/add/self-improve.md +0 -20
- package/skill/add/sensitivity.md +0 -51
- package/skill/add/setup-review.md +0 -48
- package/skill/add/soul.md +0 -58
- package/skill/add/streams.md +0 -269
- package/tooling/add_engine/release.py +0 -86
- package/tooling/templates/TASK.fast.md.tmpl +0 -78
- package/tooling/templates/TASK.md.tmpl +0 -181
|
@@ -11,15 +11,14 @@ from __future__ import annotations
|
|
|
11
11
|
import re
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
from add_engine.constants import _AUTONOMY_LEVELS
|
|
14
|
+
from add_engine.constants import _AUTONOMY_LEVELS
|
|
15
15
|
from add_engine.taskdoc import _task_header
|
|
16
16
|
|
|
17
17
|
_AUTONOMY_LINE_RE = re.compile(r"(?:^|·)[ \t]*autonomy:[ \t]*([^\s<#|]+)", re.MULTILINE)
|
|
18
|
-
_STREAMS_LINE_RE = re.compile(r"(?:^|·)[ \t]*streams:[ \t]*([^\s<#|]+)", re.MULTILINE)
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
def _autonomy_level(hdr: str):
|
|
22
|
-
"""The declared autonomy rung from a
|
|
21
|
+
"""The declared autonomy rung from a PLAN.md header region (HTML comments
|
|
23
22
|
already stripped by _task_header). Returns a member of _AUTONOMY_LEVELS, or
|
|
24
23
|
None when no `autonomy:` line is present (UNSET — an unfilled `<…>` placeholder,
|
|
25
24
|
whose value the regex declines, counts as unset), or "?" when a REAL token outside
|
|
@@ -57,37 +56,3 @@ def _project_autonomy(root: Path) -> str:
|
|
|
57
56
|
Read-only and PURE — mirrors _project_goal; the seed source for cmd_new_task."""
|
|
58
57
|
tok = _project_autonomy_token(root)
|
|
59
58
|
return "auto" if tok is None else ("conservative" if tok == "?" else tok)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# --- streams posture: the parallel-vs-sequential half of the run mode (persist-run-mode) ---
|
|
63
|
-
# A project-scoped declaration in PROJECT.md beside autonomy. Mirrors the autonomy resolvers:
|
|
64
|
-
# anchored read (a title/prose substring is never a declaration), fail-safe defaults.
|
|
65
|
-
|
|
66
|
-
def _streams_posture(text: str):
|
|
67
|
-
"""The declared streams posture from a foundation region (HTML comments already stripped
|
|
68
|
-
by the caller). A member of _STREAMS_POSTURES, None when no `streams:` line is present, or
|
|
69
|
-
"?" when a REAL token outside the set was written (unknown). PURE."""
|
|
70
|
-
m = _STREAMS_LINE_RE.search(text)
|
|
71
|
-
if not m:
|
|
72
|
-
return None
|
|
73
|
-
tok = m.group(1).strip().lower()
|
|
74
|
-
return tok if tok in _STREAMS_POSTURES else "?"
|
|
75
|
-
|
|
76
|
-
def _project_streams_token(root: Path):
|
|
77
|
-
"""The RAW streams declaration in PROJECT.md — a recognized posture, None when no
|
|
78
|
-
declaration line is present, or "?" for a real-but-unrecognized token. Uses the anchored
|
|
79
|
-
_streams_posture (a title/prose substring is never a declaration) with HTML comments
|
|
80
|
-
stripped. Unreadable foundation -> None. Read-only and PURE."""
|
|
81
|
-
try:
|
|
82
|
-
text = (root / "PROJECT.md").read_text(encoding="utf-8")
|
|
83
|
-
except OSError:
|
|
84
|
-
return None
|
|
85
|
-
return _streams_posture(re.sub(r"<!--.*?-->", "", text, flags=re.S))
|
|
86
|
-
|
|
87
|
-
def _project_streams(root: Path) -> str:
|
|
88
|
-
"""The streams posture the project runs under. Fail-SAFE: no declaration -> "parallel"
|
|
89
|
-
(the documented project default: parallel+auto); an unrecognized token -> "sequential"
|
|
90
|
-
(the conservative fallback, NEVER silently parallel); an unreadable foundation -> "parallel".
|
|
91
|
-
Read-only and PURE — mirrors _project_autonomy."""
|
|
92
|
-
tok = _project_streams_token(root)
|
|
93
|
-
return "parallel" if tok is None else ("sequential" if tok == "?" else tok)
|
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
"""add_engine.components —
|
|
1
|
+
"""add_engine.components — scope-confinement utilities.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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).
|
|
3
|
+
kernel-trim (ADD 2.0 M5): the components pillar (registry · cross-component contracts ·
|
|
4
|
+
federation) died with its verbs; only the two GENERIC path/scope utilities every scope
|
|
5
|
+
read shares survive here. Deps: stdlib only (no add, no add_engine).
|
|
8
6
|
"""
|
|
9
7
|
from __future__ import annotations
|
|
10
8
|
|
|
11
|
-
import re
|
|
12
9
|
from pathlib import Path
|
|
13
10
|
|
|
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
11
|
|
|
20
12
|
def _confined(p: Path, rootp: Path) -> bool:
|
|
21
13
|
"""True only if p resolves (symlinks followed) inside rootp; errors -> False.
|
|
@@ -25,85 +17,10 @@ def _confined(p: Path, rootp: Path) -> bool:
|
|
|
25
17
|
except OSError:
|
|
26
18
|
return False
|
|
27
19
|
|
|
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
20
|
|
|
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
21
|
|
|
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
22
|
|
|
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
23
|
|
|
105
|
-
def _contract_snapshot(root: Path, cid: str) -> Path:
|
|
106
|
-
return root / "contracts" / f"{cid}.json"
|
|
107
24
|
|
|
108
25
|
def _in_scope(rel: str, declared: list[str]) -> bool:
|
|
109
26
|
"""True when rel falls under any declared token — exact match for a file
|
|
@@ -8,27 +8,35 @@ import re
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
__all__ = [
|
|
11
|
+
"BOOK_URL",
|
|
12
|
+
"book_url",
|
|
11
13
|
"ROOT_DIRNAME",
|
|
12
14
|
"STATE_FILE",
|
|
13
15
|
"MILESTONE_FILE",
|
|
14
16
|
"GOAL_UNSET",
|
|
15
17
|
"STAGES",
|
|
16
|
-
"GRADUATION_CUE",
|
|
17
|
-
"RELEASABLE_CUE",
|
|
18
|
-
"RELEASES_FILE",
|
|
19
18
|
"PHASES",
|
|
19
|
+
"LEGACY_PHASES",
|
|
20
20
|
"GATES",
|
|
21
21
|
"HEAL_CAP",
|
|
22
22
|
"PHASE_GUIDE",
|
|
23
23
|
"PHASE_OWNER",
|
|
24
|
+
"PHASE_GROUPS",
|
|
25
|
+
"PHASE_AGENT",
|
|
24
26
|
"SETUP_FILES",
|
|
25
27
|
"PERSONA_FRONTMATTER_KEYS",
|
|
26
28
|
"PERSONA_REQUIRED_SECTIONS",
|
|
29
|
+
"PERSONA_FLOW_VALUES",
|
|
30
|
+
"TASK_KINDS",
|
|
31
|
+
"SPEC_DDS",
|
|
27
32
|
"PERSONA_HINT",
|
|
28
33
|
"PERSONA_FIT_HINT_TEMPLATE",
|
|
29
34
|
"GUIDELINE_FILES",
|
|
30
35
|
"RULES_FILE_REL",
|
|
31
36
|
"WORKFLOW_HEADINGS",
|
|
37
|
+
"_GATE_MODES",
|
|
38
|
+
"_SKIPPABLE_PHASES",
|
|
39
|
+
"_DIALECT_CLASSES",
|
|
32
40
|
]
|
|
33
41
|
|
|
34
42
|
ROOT_DIRNAME = ".add"
|
|
@@ -39,18 +47,19 @@ MILESTONE_FILE = "MILESTONE.md"
|
|
|
39
47
|
# this sentinel so the read-only orientation surfaces never blank or crash.
|
|
40
48
|
GOAL_UNSET = "(unset — add a 'goal:' line to PROJECT.md)"
|
|
41
49
|
STAGES = ("prototype", "poc", "mvp", "production")
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
# kernel-trim (ADD 2.0 M5): GRADUATION_CUE / RELEASABLE_CUE / RELEASES_FILE died with the
|
|
51
|
+
# graduate/release verbs — the release-manager persona owns that judgment now.
|
|
52
|
+
PHASES = ("direction", "build", "verify", "done")
|
|
53
|
+
# phase-collapse-3 (thin-engine-loop W2): the 6-phase walk collapsed to 3 work phases.
|
|
54
|
+
# `direction` is the whole front span (the old specify+plan+tests — §1–§4 drafted
|
|
55
|
+
# top-to-bottom, ONE freeze approval crosses it into build). Legacy tokens normalize to
|
|
56
|
+
# their 3-phase home at READ time (load_state) — 473 pre-collapse task records are never
|
|
57
|
+
# bulk-rewritten; the map below is the single source both load_state and _phase_index use.
|
|
58
|
+
LEGACY_PHASES = {
|
|
59
|
+
"ground": "direction", "specify": "direction", "scenarios": "direction",
|
|
60
|
+
"contract": "direction", "plan": "direction", "tests": "direction",
|
|
61
|
+
"observe": "verify",
|
|
62
|
+
}
|
|
54
63
|
GATES = ("none", "PASS", "RISK-ACCEPTED", "HARD-STOP")
|
|
55
64
|
# heal-then-escalate (verify-integrity): the bounded self-heal loop cap. A CONFIRMED cheat
|
|
56
65
|
# (mechanical tripwire divergence, or an agent-reported semantic refute-read finding) returns
|
|
@@ -61,36 +70,61 @@ HEAL_CAP = 3
|
|
|
61
70
|
|
|
62
71
|
|
|
63
72
|
|
|
73
|
+
# The AIDD book's published home (book-stops-shipping, ADD 2.0 M6b): the book no
|
|
74
|
+
# longer installs into projects as .add/docs/ — every engine chapter pointer deep-
|
|
75
|
+
# links here instead. mkdocs pretty-URLs: docs/<stem>.md renders at <BOOK_URL>/<stem>/.
|
|
76
|
+
BOOK_URL = "https://pilotspace.github.io/ADD"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def book_url(chapter: str) -> str:
|
|
80
|
+
"""Deep link for a book chapter filename ('02-the-flow.md' -> …/02-the-flow/)."""
|
|
81
|
+
stem = chapter[:-3] if chapter.endswith(".md") else chapter
|
|
82
|
+
return f"{BOOK_URL}/{stem}/"
|
|
83
|
+
|
|
84
|
+
|
|
64
85
|
# `add.py guide` copy: per-phase (concrete next action, book chapter to read).
|
|
65
86
|
# Keep the action wording aligned with each phase's EXIT line in the TASK template.
|
|
66
87
|
PHASE_GUIDE = {
|
|
67
|
-
"
|
|
68
|
-
"02-the-flow.md"),
|
|
69
|
-
"specify": ("state every rule — Must / Reject (+ named code) / After; rank assumptions lowest-confidence first and flag the biggest risk",
|
|
88
|
+
"direction": ("draft the Direction bundle top-to-bottom — §1 rules (Must / Reject + named codes / After, assumptions ranked lowest-confidence first) · §2 one scenario per rule · §3 the change PLAN: ground the real code, draft the contract, and DESCRIBE what this task will do (scope · ordered batches · approach — the plan-of-action the freeze report shows the human) · §4 red suite failing for the right reason; then the ONE approval: freeze --by <name> --cross",
|
|
70
89
|
"03-step-1-specify.md"),
|
|
71
|
-
"scenarios": ("write one Given/When/Then per Must AND per Reject; every result observable",
|
|
72
|
-
"04-step-2-scenarios.md"),
|
|
73
|
-
"contract": ("freeze the shape — signature, fields, error codes; names match the glossary",
|
|
74
|
-
"05-step-3-contract.md"),
|
|
75
|
-
"tests": ("write one failing test per scenario; run them RED for the right reason",
|
|
76
|
-
"06-step-4-tests.md"),
|
|
77
90
|
"build": ("write the minimum code to pass the tests; change no test and no contract",
|
|
78
91
|
"07-step-5-build.md"),
|
|
79
|
-
"verify": ("run the suite + non-functional checks, then record the gate",
|
|
92
|
+
"verify": ("run the suite + non-functional checks, then record the gate; then note what to watch + the spec delta for the next loop (§7)",
|
|
80
93
|
"08-step-6-verify.md"),
|
|
81
|
-
"observe": ("note what to watch + the spec delta for the next loop",
|
|
82
|
-
"09-the-loop.md"),
|
|
83
94
|
"done": ("this task is done — pick the next feature",
|
|
84
95
|
"02-the-flow.md"),
|
|
85
96
|
}
|
|
86
97
|
# Phase -> who owns it, for the `--json` autonomy signal. An autonomous harness may run a
|
|
87
98
|
# phase only when owner=="ai" (stop is false); every other phase is a checkpoint. The map
|
|
88
|
-
# follows the book's who-does-what table (Verify is "human only"); `tests`/`build
|
|
99
|
+
# follows the book's who-does-what table (Verify is "human only"); `tests`/`build`
|
|
89
100
|
# are AI-led. A phase missing here is `unmapped_phase` (fail closed) — never defaulted.
|
|
90
101
|
PHASE_OWNER = {
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
|
|
102
|
+
"direction": "seam",
|
|
103
|
+
"build": "ai", "verify": "human", "done": "human",
|
|
104
|
+
}
|
|
105
|
+
# phase-bundles: the work phases (PHASES minus the terminal "done") group into 3
|
|
106
|
+
# agent-owned bundles surfaced at `status`/`guide` — DIRECTION fixes the shape (through
|
|
107
|
+
# the frozen change plan — grounding + contract + build-strategy — AND the red suite; the
|
|
108
|
+
# method thesis is "fix spec/scenarios/plan/failing-tests BEFORE the build"), BUILD makes
|
|
109
|
+
# it green, VERIFY earns trust and feeds the next loop. A grouping OVER PHASES, never a
|
|
110
|
+
# reorder; "done" (terminal, human-led) deliberately has no bundle — see PHASE_AGENT/
|
|
111
|
+
# _phase_bundle below. Union == set(PHASES) - {"done"}, pairwise disjoint (test_phase_bundles.py).
|
|
112
|
+
PHASE_GROUPS = {
|
|
113
|
+
"DIRECTION": ("direction",),
|
|
114
|
+
"BUILD": ("build",),
|
|
115
|
+
"VERIFY": ("verify",),
|
|
116
|
+
}
|
|
117
|
+
# phase-bundles: the roster agent PREFERRED for each phase (per-PHASE, not per-bundle —
|
|
118
|
+
# roster-distill (ADD 2.0 M1): ONE execution shell — the `add` agent — serves every
|
|
119
|
+
# phase; the spawn prompt names the mode (direction·build·verify·advise·persona) and
|
|
120
|
+
# the agent loads that beat's guide + the fitting persona (personas carry the
|
|
121
|
+
# expertise, the agent carries the discipline). A phase missing here is a bug (PHASE_GROUPS'
|
|
122
|
+
# own union covers every key); `_phase_bundle` is the fail-closed resolver for an
|
|
123
|
+
# unmapped/corrupted phase token, not this map directly.
|
|
124
|
+
PHASE_AGENT = {
|
|
125
|
+
"direction": "add",
|
|
126
|
+
"build": "add",
|
|
127
|
+
"verify": "add",
|
|
94
128
|
}
|
|
95
129
|
SETUP_FILES = ("PROJECT.md", "CONVENTIONS.md", "GLOSSARY.md", "MODEL_REGISTRY.md", "dependencies.allowlist", "DESIGN.md", "SOUL.md", "personas/_template.md")
|
|
96
130
|
|
|
@@ -100,23 +134,52 @@ SETUP_FILES = ("PROJECT.md", "CONVENTIONS.md", "GLOSSARY.md", "MODEL_REGISTRY.md
|
|
|
100
134
|
# content quality is the AI's authoring concern, not the engine gate. NO-EXEC: validation is pure.
|
|
101
135
|
PERSONA_FRONTMATTER_KEYS = ("name", "vibe")
|
|
102
136
|
PERSONA_REQUIRED_SECTIONS = ("## Identity", "## Critical Rules", "## Default Requirement", "## Success Metrics")
|
|
137
|
+
# persona-schema-hardening: the closed set of apply-surfaces a `flow:` value may name — the
|
|
138
|
+
# single source the quality predicate reads (a value outside this set is loaded by NO surface,
|
|
139
|
+
# so a typo would otherwise fail silently). Findings are WARN-only (measure-not-block).
|
|
140
|
+
PERSONA_FLOW_VALUES = ("design", "build", "advisor", "verify")
|
|
141
|
+
# persona-task-kinds (ADD 2.0 M1 persona-core): the closed task-kind taxonomy — the join key
|
|
142
|
+
# between a persona's routing claim (`task-kinds:` frontmatter) and a task's declared kind
|
|
143
|
+
# (`kind:` header line). Route-outcome traces record it, the persona scoreboard groups by it.
|
|
144
|
+
# Closed on purpose: a free-text kind can't be scored across tasks. Measure-not-block —
|
|
145
|
+
# an unknown kind is a named WARN (quality predicate Finding C), never a refusal.
|
|
146
|
+
TASK_KINDS = ("feature", "refactor", "test", "docs", "ui",
|
|
147
|
+
"security", "data", "infra", "release", "integration")
|
|
148
|
+
|
|
149
|
+
# specs-5dd (ADD 2.0 M3): the closed 5-DD map — dd tag -> (spec file under .add/specs/,
|
|
150
|
+
# title, lens). init renders ONE template (templates/specs/SPEC.md.tmpl) five ways;
|
|
151
|
+
# `delta-append <dd>` routes a lesson to its file. Closed on purpose: the five lenses ARE
|
|
152
|
+
# the method's competency model (DDD·SDD·UDD·TDD·ADD) — an unknown dd is a refusal
|
|
153
|
+
# (delta_dd_unknown), because a delta filed under a sixth ad-hoc lens is a delta lost.
|
|
154
|
+
SPEC_DDS = {
|
|
155
|
+
"ddd": ("domain.md", "Domain",
|
|
156
|
+
"what the system IS: entities, rules, ubiquitous language (DDD)"),
|
|
157
|
+
"sdd": ("system.md", "System",
|
|
158
|
+
"how it is built: architecture, contracts, data shapes (SDD)"),
|
|
159
|
+
"udd": ("experience.md", "Experience",
|
|
160
|
+
"how it feels to use: flows, surfaces, the humans served (UDD)"),
|
|
161
|
+
"tdd": ("quality.md", "Quality",
|
|
162
|
+
"how we know it works: test strategy, floors, evidence (TDD)"),
|
|
163
|
+
"add": ("method.md", "Method",
|
|
164
|
+
"how we work: the loop, autonomy, ceremony budget (ADD)"),
|
|
165
|
+
}
|
|
103
166
|
|
|
104
167
|
# persona-seed-nudge v2: ONE hint, single-sourced — `new-milestone`/`check`/`status` all print
|
|
105
168
|
# THIS constant (not their own copy) so the wording can never drift across the three surfaces.
|
|
106
169
|
# Project-scoped (not "this milestone's domain") per the confirmed v2 amendment: the AI should
|
|
107
170
|
# catch up ALL of a project's missing personas, not draft a single milestone-fit one.
|
|
108
|
-
PERSONA_HINT = ("no project-fit persona seeded yet under .add/personas/ — spawn the add
|
|
109
|
-
"
|
|
110
|
-
"PROJECT.md's domain")
|
|
171
|
+
PERSONA_HINT = ("no project-fit persona seeded yet under .add/personas/ — spawn the add agent "
|
|
172
|
+
"in persona mode (or read docs/18-personas.md) to seed the project's persona(s) "
|
|
173
|
+
"from PROJECT.md's domain + the seed templates (tooling/templates/personas/)")
|
|
111
174
|
|
|
112
175
|
# persona-fit-nudge: the OPPOSITE-branch, mutually-exclusive sibling of PERSONA_HINT — fires only
|
|
113
176
|
# when ≥1 real persona ALREADY exists, so a brand-new milestone doesn't silently assume one of
|
|
114
177
|
# them fits its domain. Existence-only (names the persona slugs already seeded); the AI still
|
|
115
|
-
# owns the actual fit judgment
|
|
116
|
-
# {slugs} is filled at call time from `.add/personas/*.md` (excluding `_template`).
|
|
178
|
+
# owns the actual fit judgment (the add agent's persona mode) — the engine never scores content
|
|
179
|
+
# similarity. {slugs} is filled at call time from `.add/personas/*.md` (excluding `_template`).
|
|
117
180
|
PERSONA_FIT_HINT_TEMPLATE = (
|
|
118
181
|
"existing persona(s) seeded — {slugs} — confirm one fits this milestone's domain, or spawn "
|
|
119
|
-
"the add
|
|
182
|
+
"the add agent in persona mode (or read docs/18-personas.md) to draft a better-fit one"
|
|
120
183
|
)
|
|
121
184
|
|
|
122
185
|
# Scaffolded into .add/.gitignore at init so the engine's transient LOCAL artifacts
|
|
@@ -171,16 +234,11 @@ _RULE_REF_LINE = "- ADD (AI-Driven Development) Workflows rules: ./.claude/rules
|
|
|
171
234
|
|
|
172
235
|
# Minimal embedded fallback so the tool still works if templates/ is missing
|
|
173
236
|
# (circuit breaker: never hard-fail just because a template file was deleted).
|
|
174
|
-
_FALLBACK_TASK = """#
|
|
237
|
+
_FALLBACK_TASK = """# PLAN: {title}
|
|
175
238
|
|
|
176
239
|
slug: {slug} · created: {date} · stage: {stage}
|
|
177
240
|
autonomy: auto
|
|
178
|
-
phase:
|
|
179
|
-
|
|
180
|
-
## 0 · GROUND
|
|
181
|
-
Touches (files · symbols · signatures):
|
|
182
|
-
Honors (patterns / conventions):
|
|
183
|
-
Anchors the contract cites:
|
|
241
|
+
phase: direction
|
|
184
242
|
|
|
185
243
|
## 1 · SPECIFY
|
|
186
244
|
Feature:
|
|
@@ -188,12 +246,18 @@ Framings weighed:
|
|
|
188
246
|
Must:
|
|
189
247
|
Reject:
|
|
190
248
|
After:
|
|
191
|
-
|
|
192
|
-
|
|
249
|
+
Boundary:
|
|
250
|
+
<assumptions>
|
|
251
|
+
⚠ <the ONE assumption most likely to be wrong — if wrong: <cost>>
|
|
252
|
+
</assumptions>
|
|
193
253
|
|
|
194
254
|
## 2 · SCENARIOS
|
|
195
|
-
## 3 ·
|
|
255
|
+
## 3 · PLAN
|
|
256
|
+
### Contract
|
|
196
257
|
Status: DRAFT
|
|
258
|
+
### Build-strategy
|
|
259
|
+
Scope (may touch):
|
|
260
|
+
Regression floor:
|
|
197
261
|
## 4 · TESTS
|
|
198
262
|
## 5 · BUILD
|
|
199
263
|
## 6 · VERIFY
|
|
@@ -205,46 +269,9 @@ Outcome:
|
|
|
205
269
|
"""
|
|
206
270
|
|
|
207
271
|
|
|
208
|
-
#
|
|
209
|
-
#
|
|
210
|
-
# templates/ never hard-fails the fast lane. Keeps the trust floor: §3 freeze-flag + Status,
|
|
211
|
-
# §6 GATE RECORD/Outcome, §0 Anchors, §4 red-before-build, §5 Scope.
|
|
212
|
-
_FALLBACK_TASK_FAST = """# TASK: {title}
|
|
213
|
-
|
|
214
|
-
slug: {slug} · created: {date} · stage: {stage}
|
|
215
|
-
autonomy: auto
|
|
216
|
-
phase: ground
|
|
217
|
-
fast: true
|
|
218
|
-
|
|
219
|
-
## 0 · GROUND
|
|
220
|
-
Touches (files · symbols):
|
|
221
|
-
Anchors the contract cites:
|
|
222
|
-
Ground SHA:
|
|
223
|
-
|
|
224
|
-
## 1 · SPECIFY
|
|
225
|
-
Feature:
|
|
226
|
-
Must:
|
|
227
|
-
Reject:
|
|
228
|
-
Accept:
|
|
229
|
-
Assumptions: ⚠ <most likely wrong> — why; if wrong: <cost>
|
|
230
|
-
|
|
231
|
-
## 3 · CONTRACT
|
|
232
|
-
Least-sure flag surfaced at freeze:
|
|
233
|
-
Status: DRAFT
|
|
234
|
-
|
|
235
|
-
## 4 · TESTS
|
|
236
|
-
Plan:
|
|
237
|
-
Tests live in: `./tests/` · MUST run red before Build.
|
|
238
|
-
|
|
239
|
-
## 5 · BUILD
|
|
240
|
-
Scope (may touch): `./src/`
|
|
272
|
+
# atomic-node: the ONE template IS the lean render — _FAST_SECTIONS retired with
|
|
273
|
+
# the lane scaffolds (the stripped blocks no longer exist in the template).
|
|
241
274
|
|
|
242
|
-
## 6 · VERIFY
|
|
243
|
-
Build expectations (from §1 Accept + §3 CONTRACT):
|
|
244
|
-
### GATE RECORD
|
|
245
|
-
Outcome:
|
|
246
|
-
Reviewed by:
|
|
247
|
-
"""
|
|
248
275
|
|
|
249
276
|
_DEFAULT_WIDTH = 72 # fixed width for the persisted/canonical render (RETRO.md)
|
|
250
277
|
|
|
@@ -284,12 +311,39 @@ _TAG_TOKEN_RE = re.compile(r"(M\d+|R:[A-Za-z0-9_]+)")
|
|
|
284
311
|
# --- autonomy levels (shared: autonomy resolvers + _AUTONOMY_ORDER/cmd_autonomy) ---
|
|
285
312
|
_AUTONOMY_LEVELS = ("manual", "conservative", "auto")
|
|
286
313
|
|
|
287
|
-
# --- streams posture (shared: streams resolvers + cmd_streams) — the parallel-vs-sequential
|
|
288
|
-
# half of the run mode (persist-run-mode); project-scoped, persisted in PROJECT.md beside autonomy ---
|
|
289
|
-
_STREAMS_POSTURES = ("parallel", "sequential")
|
|
290
|
-
|
|
291
314
|
# --- sensitivity taxonomy (shared: _task_sensitivity reader + cmd_freeze/status/audit) — the
|
|
292
315
|
# risk-CLASS the human declares in the TASK header at freeze (risk-sensitivity-taxonomy). The
|
|
293
316
|
# engine validates + surfaces a HUMAN-declared token; it NEVER classifies. A closed enum, sibling
|
|
294
|
-
# of _AUTONOMY_LEVELS
|
|
317
|
+
# of _AUTONOMY_LEVELS. Consumed downstream by advisor-gate-relax (mechanical). ---
|
|
295
318
|
_SENSITIVITY_VALUES = ("security", "data", "architecture", "mechanical")
|
|
319
|
+
|
|
320
|
+
# --- gate mode (shared: _task_gate_mode reader + cmd_freeze's --ai-plan-verify path) — the
|
|
321
|
+
# two-way DIRECTION-freeze declaration (ai-plan-verify-gate): human (default) | ai-plan-verify.
|
|
322
|
+
# A closed 2-tuple, sibling of _AUTONOMY_LEVELS/_SENSITIVITY_VALUES — but,
|
|
323
|
+
# unlike them, listed in __all__: a NEW trust-loosening capability is deliberately surfaced via
|
|
324
|
+
# `from add_engine.constants import *`, not tucked into the _-prefixed sibling import list.
|
|
325
|
+
# Absent header line -> None from the resolver, treated as "human" by every caller (fail-closed
|
|
326
|
+
# default — never silently upgrades to the loosened path). ---
|
|
327
|
+
_GATE_MODES = ("human", "ai-plan-verify")
|
|
328
|
+
|
|
329
|
+
# --- skippable phases (shared: _task_skip_set reader + cmd_advance's skip pre-pass) — the
|
|
330
|
+
# fast-lane-skips closed tuple: the ONLY set cmd_advance's skip pre-pass ever tests `nxt`
|
|
331
|
+
# against (scenarios left it at phase-merge-specify: merged into specify, the retired
|
|
332
|
+
# header token is tolerated-and-ignored). specify/plan/tests/build/verify can NEVER be
|
|
333
|
+
# skipped — a structural
|
|
334
|
+
# exclusion (this tuple never names them), not a runtime-checked policy. Same relative order
|
|
335
|
+
# as PHASES. Listed in __all__ (mirrors _GATE_MODES): a new trust-loosening capability is
|
|
336
|
+
# deliberately surfaced via `from add_engine.constants import *`, not tucked away. ---
|
|
337
|
+
_SKIPPABLE_PHASES = ()
|
|
338
|
+
|
|
339
|
+
# --- format-dialect registry (shared: _dialect_gaps + the tests->build crossing warning +
|
|
340
|
+
# cmd_check's dialect_gap lint) — quality-floors floor 1. Closed (name, regex) pairs: a
|
|
341
|
+
# class matches only its FULL value shape, never prose fragments (a bare `2026-07-10`
|
|
342
|
+
# date must not match — the aware class requires the T separator + a zone suffix). Born
|
|
343
|
+
# from benchmark WV1 wm2: an arm's own suite stayed green on naive timestamps while the
|
|
344
|
+
# spec's own examples were Z-suffixed; the aware/naive crash shipped green. Listed in
|
|
345
|
+
# __all__ (mirrors _GATE_MODES): a new trust surface is deliberately surfaced. ---
|
|
346
|
+
_DIALECT_CLASSES = (
|
|
347
|
+
("aware-iso-timestamp",
|
|
348
|
+
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})"),
|
|
349
|
+
)
|
|
@@ -41,19 +41,17 @@ def _guideline_block() -> str:
|
|
|
41
41
|
"\n"
|
|
42
42
|
"The flow: INTAKE sizes a request into a milestone; each task runs the **specification bundle**\n"
|
|
43
43
|
"(Spec+Scenarios+Contract+Tests) — ONE human approval at the frozen contract — then a\n"
|
|
44
|
-
"self-driving build→verify run. Non-negotiable for every agent
|
|
45
|
-
"
|
|
46
|
-
"
|
|
44
|
+
"self-driving build→verify run. Non-negotiable for every agent: Never weaken a test or edit a frozen contract\n"
|
|
45
|
+
"to make a build pass; a security finding is always HARD-STOP — never auto-passed.\n"
|
|
46
|
+
"PROJECT.md `invariants:` (run/entry contracts) bind EVERY task: the artifact must hold under\n"
|
|
47
|
+
"the BARE declared runtime — a dependency that breaks it is a defect, never \"expected\".\n"
|
|
47
48
|
"\n"
|
|
48
|
-
"Roster (from `add-method/agents
|
|
49
|
-
"
|
|
50
|
-
"-
|
|
51
|
-
"
|
|
52
|
-
"- persona · any phase, cross-cutting service · mid\n"
|
|
53
|
-
"- advisor · any phase, cross-cutting service · top\n"
|
|
49
|
+
"Roster (from `add-method/agents/add.md`): ONE `add` agent — the spawn names the mode\n"
|
|
50
|
+
"(direction · build · verify · advise · persona); the agent loads that beat's phase guide\n"
|
|
51
|
+
"plus the best-fit `.add/personas/` persona (personas carry the expertise, the agent\n"
|
|
52
|
+
"carries the discipline; the floor above binds every mode).\n"
|
|
54
53
|
"\n"
|
|
55
|
-
"On Claude Code the `add` skill drives this loop; other agents follow the three steps.
|
|
56
|
-
"book is in `.add/docs/`. This block is generated by `add.py sync-guidelines` — edit outside the markers.\n"
|
|
54
|
+
"On Claude Code the `add` skill drives this loop; other agents follow the three steps. Book: https://pilotspace.github.io/ADD/. This block is generated by `add.py sync-guidelines` — edit outside the markers.\n"
|
|
57
55
|
f"{_GUIDE_END}"
|
|
58
56
|
)
|
|
59
57
|
|
|
@@ -117,17 +117,32 @@ def _atomic_write_many(writes: list[tuple[Path, str]]) -> None:
|
|
|
117
117
|
# --- root finding + state load/save + the shared error primitive ------------
|
|
118
118
|
|
|
119
119
|
def find_root(start: Path | None = None) -> Path | None:
|
|
120
|
-
"""Walk up from cwd to find a .add/ project root.
|
|
120
|
+
"""Walk up from cwd to find a .add/ project root.
|
|
121
|
+
|
|
122
|
+
Opt-in boundary: when `ADD_ROOT_CEILING` is set, the walk stops at that dir
|
|
123
|
+
(inclusive) and never ascends above it — so a workspace nested under an
|
|
124
|
+
ancestor project resolves ONLY its own root (or None before init), never the
|
|
125
|
+
parent's. Unset (every normal invocation) is the legacy walk to fs-root,
|
|
126
|
+
byte-identical. Both paths are resolved so a symlinked ceiling still matches.
|
|
127
|
+
"""
|
|
121
128
|
cur = (start or Path.cwd()).resolve()
|
|
129
|
+
_ceil = os.environ.get("ADD_ROOT_CEILING")
|
|
130
|
+
ceil = Path(_ceil).resolve() if _ceil else None
|
|
122
131
|
for d in (cur, *cur.parents):
|
|
123
132
|
if (d / ROOT_DIRNAME / STATE_FILE).exists():
|
|
124
133
|
return d / ROOT_DIRNAME
|
|
134
|
+
if ceil is not None and d == ceil:
|
|
135
|
+
break
|
|
125
136
|
return None
|
|
126
137
|
|
|
127
138
|
def _require_root() -> Path:
|
|
128
139
|
root = find_root()
|
|
129
140
|
if root is None:
|
|
130
|
-
|
|
141
|
+
# skip-error-ergonomics M2: hand the exact command with its flags — the
|
|
142
|
+
# bare "run init first" hint made every fresh headless agent read
|
|
143
|
+
# `init --help` before its first real call (LOOP-2 re-measure, all reps).
|
|
144
|
+
_die('no .add/ project found — run: add.py init --name "<project>" '
|
|
145
|
+
"--stage <prototype|poc|mvp|production>")
|
|
131
146
|
return root
|
|
132
147
|
|
|
133
148
|
def _migrate_state(state: dict) -> dict:
|
|
@@ -170,8 +185,60 @@ def _state_text_or_die(root: Path) -> str:
|
|
|
170
185
|
f"`git checkout --ours/--theirs {STATE_FILE}`), then run `add.py doctor` to verify")
|
|
171
186
|
return text
|
|
172
187
|
|
|
188
|
+
# kickoff-truth M3 (contract v2): the dup-failure short-circuit. The 2026-07-13
|
|
189
|
+
# transcript audit measured 12-21% of all engine calls as byte-identical repeats of
|
|
190
|
+
# a call that had just failed the same way — the agent re-issues the command
|
|
191
|
+
# unchanged instead of reading the error. The sig sidecar lives OUTSIDE the project
|
|
192
|
+
# tree (OS tmp dir, keyed by md5(root path)): the engine's reject-writes-nothing
|
|
193
|
+
# floor pins that a REJECTED command leaves the .add tree byte-identical, so the
|
|
194
|
+
# hint state is ephemeral per-project cache, never a tree write. add.main()
|
|
195
|
+
# registers each invocation and clears the sidecar on any successful exit, so only
|
|
196
|
+
# CONSECUTIVE identical failures short-circuit. Fail-open everywhere: no root /
|
|
197
|
+
# unreadable sidecar / any OSError -> no hint, never a block — a hint helper must
|
|
198
|
+
# never change an error's outcome.
|
|
199
|
+
_INVOCATION: list[str] | None = None # set by add.main() per dispatch
|
|
200
|
+
|
|
201
|
+
def _register_invocation(argv: list[str]) -> None:
|
|
202
|
+
global _INVOCATION
|
|
203
|
+
_INVOCATION = list(argv)
|
|
204
|
+
|
|
205
|
+
def _last_fail_path() -> Path | None:
|
|
206
|
+
root = find_root()
|
|
207
|
+
if root is None:
|
|
208
|
+
return None
|
|
209
|
+
key = hashlib.md5(str(root.resolve()).encode("utf-8")).hexdigest()[:12]
|
|
210
|
+
return Path(tempfile.gettempdir()) / f"add-last-fail-{key}.json"
|
|
211
|
+
|
|
212
|
+
def _clear_last_fail() -> None:
|
|
213
|
+
try:
|
|
214
|
+
side = _last_fail_path()
|
|
215
|
+
if side is not None:
|
|
216
|
+
side.unlink(missing_ok=True)
|
|
217
|
+
except OSError:
|
|
218
|
+
pass # hygiene only — success must never fail on it
|
|
219
|
+
|
|
220
|
+
def _dup_fail_hint(msg: str) -> str | None:
|
|
221
|
+
try:
|
|
222
|
+
side = _last_fail_path()
|
|
223
|
+
if side is None:
|
|
224
|
+
return None
|
|
225
|
+
argv = _INVOCATION if _INVOCATION is not None else sys.argv[1:]
|
|
226
|
+
code_head = msg.split(":", 1)[0].strip()
|
|
227
|
+
sig = hashlib.md5((" ".join(argv) + "\n" + code_head).encode("utf-8")).hexdigest()
|
|
228
|
+
hint = None
|
|
229
|
+
if side.exists() and json.loads(side.read_text(encoding="utf-8")).get("sig") == sig:
|
|
230
|
+
hint = ("hint: this exact call already failed with the same error — change "
|
|
231
|
+
"the command or input before retrying (add.py status shows the true state)")
|
|
232
|
+
side.write_text(json.dumps({"sig": sig}), encoding="utf-8")
|
|
233
|
+
return hint
|
|
234
|
+
except (OSError, ValueError):
|
|
235
|
+
return None
|
|
236
|
+
|
|
173
237
|
def _die(msg: str, code: int = 1) -> None:
|
|
238
|
+
hint = _dup_fail_hint(msg) # kickoff-truth M3 — fail-open, never raises
|
|
174
239
|
print(f"add: error: {msg}", file=sys.stderr)
|
|
240
|
+
if hint:
|
|
241
|
+
print(hint, file=sys.stderr)
|
|
175
242
|
raise SystemExit(code)
|
|
176
243
|
|
|
177
244
|
|