@miller-tech/uap 1.146.1 → 1.147.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,104 @@
1
+ #!/usr/bin/env python3
2
+ """validate-plan-on-change enforcer: ALWAYS validate a plan after creating or
3
+ modifying it.
4
+
5
+ A Write/Edit to a PLAN artifact (anything under a `plans/` directory, or a file
6
+ whose name is plan-like: `PLAN.md`, `*-plan.md`, `plan-*.md`, `implementation-plan.md`,
7
+ `*.plan.md`) is BLOCKED unless the agent has run the `validate the plan` prompt
8
+ recently and recorded it with `uap plan validate`. The block message is the
9
+ self-prompt: it tells the agent to validate the plan first, then retry.
10
+
11
+ "Recently" = within UAP_PLAN_VALIDATE_WINDOW seconds (default 300) of the last
12
+ `uap plan validate`, so a burst of edits in one planning session shares a single
13
+ validation, but a plan touched after a gap re-validates.
14
+
15
+ State: `.uap/plan_state.json` {"validated_at": <epoch>}. Escape hatch (justify in
16
+ the plan/PR): UAP_PLAN_VALIDATE_OFF=1.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import os
22
+ import re
23
+ import time
24
+ from pathlib import Path
25
+ import sys
26
+
27
+ sys.path.insert(0, str(Path(__file__).parent))
28
+ from _common import emit, parse_cli # noqa: E402
29
+
30
+ EDIT_OPS = {"Write", "Edit", "MultiEdit", "write", "edit", "multiedit"}
31
+ STATE = Path(os.environ.get("UAP_STATE_DIR", ".uap")) / "plan_state.json"
32
+ DEFAULT_WINDOW = 300
33
+
34
+ # A plan-like filename STEM (the part before `.md`): `plan`, `foo-plan`, `plan-v2`,
35
+ # `implementation_plan`, `foo.plan`. Deliberately does NOT match `planning`, `plans`
36
+ # (plural handled by the plans/ dir rule), or `explanation`.
37
+ PLAN_STEM_RE = re.compile(r"(^|[-_. ])plans?([-_. ]|$)", re.IGNORECASE)
38
+
39
+
40
+ def _target(args: dict) -> str:
41
+ for k in ("file_path", "path", "notebook_path", "filePath", "target"):
42
+ if args.get(k):
43
+ return str(args[k])
44
+ return ""
45
+
46
+
47
+ def _is_plan_file(target: str) -> bool:
48
+ if not target:
49
+ return False
50
+ posix = target.replace(os.sep, "/")
51
+ # Any file inside a plans/ directory.
52
+ if re.search(r"(^|/)plans?/", posix, re.IGNORECASE):
53
+ return True
54
+ name = posix.rsplit("/", 1)[-1]
55
+ if not name.lower().endswith(".md"):
56
+ return False
57
+ stem = name[:-3]
58
+ return bool(PLAN_STEM_RE.search(stem))
59
+
60
+
61
+ def _validated_at() -> float:
62
+ try:
63
+ return float(json.loads(STATE.read_text()).get("validated_at", 0))
64
+ except Exception: # noqa: BLE001
65
+ return 0.0
66
+
67
+
68
+ def main() -> None:
69
+ op, args = parse_cli()
70
+ if os.environ.get("UAP_PLAN_VALIDATE_OFF") == "1":
71
+ emit(True, "UAP_PLAN_VALIDATE_OFF override set")
72
+ if op not in EDIT_OPS:
73
+ emit(True, "not a file-write operation")
74
+
75
+ target = _target(args)
76
+ if not _is_plan_file(target):
77
+ emit(True, "not a plan artifact")
78
+
79
+ try:
80
+ window = int(os.environ.get("UAP_PLAN_VALIDATE_WINDOW", DEFAULT_WINDOW))
81
+ except ValueError:
82
+ window = DEFAULT_WINDOW
83
+
84
+ age = time.time() - _validated_at()
85
+ if age <= window:
86
+ emit(True, f"plan validated {int(age)}s ago (within {window}s)")
87
+
88
+ emit(
89
+ False,
90
+ "validate-plan-on-change: you are creating or modifying a plan "
91
+ f"('{target}') but have NOT validated it. ALWAYS validate a plan after "
92
+ "creating or modifying it.\n"
93
+ " 1. Run the prompt `validate the plan` — review assumptions, gaps, "
94
+ "risks, and whether it still matches the request.\n"
95
+ " 2. Record it: `uap plan validate`.\n"
96
+ " 3. Retry this write.\n"
97
+ "(One validation covers a burst of edits for "
98
+ f"{window}s; override with UAP_PLAN_VALIDATE_OFF=1.)",
99
+ inject_prompt="validate the plan",
100
+ )
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()
@@ -0,0 +1,40 @@
1
+ # validate-plan-on-change
2
+
3
+ **Category**: workflow
4
+ **Level**: REQUIRED
5
+ **Enforcement Stage**: pre-exec
6
+ **Tags**: planning, validation, accuracy, self-prompt
7
+
8
+ ## Rule
9
+
10
+ A `Write` or `Edit` to a **plan artifact** is BLOCKED unless the plan has been
11
+ validated recently. A plan artifact is any file under a `plans/` directory or a
12
+ file with a plan-like name (`PLAN.md`, `*-plan.md`, `plan-*.md`,
13
+ `implementation-plan.md`, `*.plan.md`). `planning.md`, `explanation.md`, and
14
+ plural-but-unrelated names are NOT plan artifacts.
15
+
16
+ On a blocked write the agent MUST:
17
+
18
+ 1. Run the prompt `validate the plan` — review assumptions, gaps, risks, and
19
+ whether the plan still matches the request.
20
+ 2. Record the validation: `uap plan validate`.
21
+ 3. Retry the write.
22
+
23
+ One validation covers a burst of edits for `UAP_PLAN_VALIDATE_WINDOW` seconds
24
+ (default 300), so iterative edits in a single planning session don't re-prompt;
25
+ a plan touched after that gap re-validates.
26
+
27
+ ## Why
28
+
29
+ User directive: "force yourself to prompt `validate the plan` after any plan is
30
+ created or modified — you should ALWAYS validate the plan." Plans drift from the
31
+ request as they grow; an unvalidated plan ships last-mile gaps into code. Making
32
+ validation a hard gate at the moment the plan changes catches those gaps while
33
+ they are cheapest to fix. (Supersedes the older `validate-plan-before-build`,
34
+ which never fired because nothing set its `plan_ready` marker.)
35
+
36
+ ## Enforcement
37
+
38
+ Python enforcer `validate_plan_on_change.py`. State in `.uap/plan_state.json`
39
+ (`{"validated_at": <epoch>}`, written by `uap plan validate`). Escape hatch,
40
+ justify in the plan/PR: `UAP_PLAN_VALIDATE_OFF=1`.