@miller-tech/uap 1.146.1 → 1.147.1
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/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +10 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/plan.d.ts +8 -0
- package/dist/cli/plan.d.ts.map +1 -0
- package/dist/cli/plan.js +64 -0
- package/dist/cli/plan.js.map +1 -0
- package/dist/delivery/spec-imports.d.ts.map +1 -1
- package/dist/delivery/spec-imports.js +11 -3
- package/dist/delivery/spec-imports.js.map +1 -1
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/expert_review_required.py +6 -2
- package/src/policies/enforcers/validate_plan_on_change.py +104 -0
- package/src/policies/schemas/policies/validate-plan-on-change.md +40 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
|
@@ -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`.
|