@miller-tech/uap 1.40.1 → 1.41.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.
Files changed (59) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/cli/deliver-defaults.d.ts +23 -0
  3. package/dist/cli/deliver-defaults.d.ts.map +1 -0
  4. package/dist/cli/deliver-defaults.js +121 -0
  5. package/dist/cli/deliver-defaults.js.map +1 -0
  6. package/dist/cli/init.d.ts.map +1 -1
  7. package/dist/cli/init.js +29 -0
  8. package/dist/cli/init.js.map +1 -1
  9. package/dist/cli/setup.d.ts.map +1 -1
  10. package/dist/cli/setup.js +19 -0
  11. package/dist/cli/setup.js.map +1 -1
  12. package/dist/policies/policy-tools.d.ts +7 -0
  13. package/dist/policies/policy-tools.d.ts.map +1 -1
  14. package/dist/policies/policy-tools.js +24 -2
  15. package/dist/policies/policy-tools.js.map +1 -1
  16. package/package.json +3 -1
  17. package/src/policies/enforcers/7ebbc721-7540-4e9f-879a-770e0213a09b_architecture_review.py +101 -0
  18. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  19. package/src/policies/enforcers/_common.py +100 -0
  20. package/src/policies/enforcers/artifact_hygiene.py +52 -0
  21. package/src/policies/enforcers/cluster_routing.py +63 -0
  22. package/src/policies/enforcers/codebase_read_before_plan.py +52 -0
  23. package/src/policies/enforcers/coord_overlap.py +81 -0
  24. package/src/policies/enforcers/delivery_enforcement.py +97 -0
  25. package/src/policies/enforcers/doc_live_over_report.py +50 -0
  26. package/src/policies/enforcers/expert_review_required.py +135 -0
  27. package/src/policies/enforcers/iac_parity.py +53 -0
  28. package/src/policies/enforcers/mcp_router_first.py +37 -0
  29. package/src/policies/enforcers/memory_before_plan.py +61 -0
  30. package/src/policies/enforcers/parallel_reads.py +50 -0
  31. package/src/policies/enforcers/rtk_wrap.py +44 -0
  32. package/src/policies/enforcers/schema_diff_gate.py +80 -0
  33. package/src/policies/enforcers/session_memory_write.py +52 -0
  34. package/src/policies/enforcers/task_required.py +131 -0
  35. package/src/policies/enforcers/test_gate.py +58 -0
  36. package/src/policies/enforcers/validate_plan_before_build.py +75 -0
  37. package/src/policies/enforcers/worktree_required.py +57 -0
  38. package/src/policies/schemas/policies/architecture-review.md +51 -0
  39. package/src/policies/schemas/policies/artifact-hygiene.md +29 -0
  40. package/src/policies/schemas/policies/cluster-routing.md +31 -0
  41. package/src/policies/schemas/policies/codebase-read-before-plan.md +30 -0
  42. package/src/policies/schemas/policies/coord-overlap.md +24 -0
  43. package/src/policies/schemas/policies/delivery-enforcement.md +45 -0
  44. package/src/policies/schemas/policies/doc-live-over-report.md +32 -0
  45. package/src/policies/schemas/policies/expert-review-required.md +60 -0
  46. package/src/policies/schemas/policies/iac-parity.md +31 -0
  47. package/src/policies/schemas/policies/mandatory-testing-deployment.md +147 -0
  48. package/src/policies/schemas/policies/mcp-router-first.md +24 -0
  49. package/src/policies/schemas/policies/memory-before-plan.md +24 -0
  50. package/src/policies/schemas/policies/merge-deploy-monitor-verify.md +145 -0
  51. package/src/policies/schemas/policies/parallel-reads.md +24 -0
  52. package/src/policies/schemas/policies/rtk-wrap.md +26 -0
  53. package/src/policies/schemas/policies/schema-diff-gate.md +30 -0
  54. package/src/policies/schemas/policies/session-memory-write.md +24 -0
  55. package/src/policies/schemas/policies/task-required.md +49 -0
  56. package/src/policies/schemas/policies/test-gate.md +24 -0
  57. package/src/policies/schemas/policies/validate-plan-before-build.md +28 -0
  58. package/src/policies/schemas/policies/worktree-required.md +28 -0
  59. package/templates/hooks/uap-policy-gate.sh +5 -0
@@ -0,0 +1,100 @@
1
+ """Shared helpers for UAP policy enforcers."""
2
+ from __future__ import annotations
3
+ import argparse
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ def parse_cli() -> tuple[str, dict[str, Any]]:
13
+ p = argparse.ArgumentParser()
14
+ p.add_argument("--operation", required=True)
15
+ p.add_argument("--args", default="{}")
16
+ ns = p.parse_args()
17
+ try:
18
+ args = json.loads(ns.args)
19
+ except json.JSONDecodeError:
20
+ args = {}
21
+ return ns.operation, args
22
+
23
+
24
+ def emit(allowed: bool, reason: str, **extra: Any) -> None:
25
+ payload: dict[str, Any] = {"allowed": allowed, "reason": reason}
26
+ payload.update(extra)
27
+ json.dump(payload, sys.stdout)
28
+ sys.exit(0 if allowed else 2)
29
+
30
+
31
+ def repo_root() -> Path:
32
+ env = os.environ.get("UAP_REPO_ROOT")
33
+ if env:
34
+ return Path(env)
35
+ cwd = Path.cwd()
36
+ for p in [cwd, *cwd.parents]:
37
+ if (p / ".git").exists():
38
+ return p
39
+ return cwd
40
+
41
+
42
+ def worktree_root() -> Path:
43
+ """Root of the current WORKING TREE for git operations.
44
+
45
+ Distinct from repo_root() (the main checkout, where runtime data like
46
+ policies.db lives). git-diff based enforcers must run against the working
47
+ tree — which is the worktree when an operation runs from inside one. The
48
+ policy gate exports UAP_WORKTREE_ROOT; fall back to `git rev-parse` from cwd,
49
+ then to repo_root().
50
+ """
51
+ env = os.environ.get("UAP_WORKTREE_ROOT")
52
+ if env:
53
+ return Path(env)
54
+ try:
55
+ r = subprocess.run(
56
+ ["git", "rev-parse", "--show-toplevel"],
57
+ capture_output=True, text=True, timeout=3, env=_clean_env(),
58
+ )
59
+ if r.returncode == 0 and r.stdout.strip():
60
+ return Path(r.stdout.strip())
61
+ except Exception: # noqa: BLE001
62
+ pass
63
+ return repo_root()
64
+
65
+
66
+ # git exports repo-context vars (GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, ...)
67
+ # into hook environments. An enforcer spawned during a hook would then run its
68
+ # own git calls against the HOOK'S repo instead of cwd — silently no-op'ing
69
+ # every git-diff based check. Strip them so cwd decides the repo.
70
+ _GIT_CONTEXT_VARS = (
71
+ "GIT_DIR",
72
+ "GIT_WORK_TREE",
73
+ "GIT_INDEX_FILE",
74
+ "GIT_COMMON_DIR",
75
+ "GIT_OBJECT_DIRECTORY",
76
+ "GIT_PREFIX",
77
+ )
78
+
79
+
80
+ def _clean_env() -> dict[str, str]:
81
+ return {k: v for k, v in os.environ.items() if k not in _GIT_CONTEXT_VARS}
82
+
83
+
84
+ def run(cmd: list[str], cwd: Path | None = None, timeout: int = 5) -> tuple[int, str, str]:
85
+ try:
86
+ r = subprocess.run(
87
+ cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout,
88
+ env=_clean_env(),
89
+ )
90
+ return r.returncode, r.stdout, r.stderr
91
+ except Exception as e: # noqa: BLE001
92
+ return 1, "", str(e)
93
+
94
+
95
+ def arg_str(args: dict[str, Any]) -> str:
96
+ """Flatten args to a single lowercase string for substring checks."""
97
+ try:
98
+ return json.dumps(args, default=str).lower()
99
+ except Exception: # noqa: BLE001
100
+ return str(args).lower()
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python3
2
+ """artifact-hygiene enforcer: block binary artifacts outside curated dirs."""
3
+ from __future__ import annotations
4
+ import re
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).parent))
9
+ from _common import emit, parse_cli # noqa: E402
10
+
11
+ BINARY_RE = re.compile(r"\.(png|jpe?g|gif|pdf|zip|tar\.gz|tgz|db|sqlite\d?)$", re.I)
12
+ ALLOWED_PREFIXES = (
13
+ "docs/",
14
+ "tests/",
15
+ "apps/",
16
+ "agents/data/memory/",
17
+ ".playwright-mcp/",
18
+ "observability/",
19
+ )
20
+ ALLOWED_SUBSTRINGS = ("/__screenshots__/", "/public/", "/static/", "/assets/")
21
+ WRITE_OPS = {"Write", "write", "create-file"}
22
+
23
+
24
+ def main() -> None:
25
+ op, args = parse_cli()
26
+ if op not in WRITE_OPS:
27
+ emit(True, "not a write op")
28
+
29
+ path = (args.get("file_path") or args.get("path") or "").replace("\\", "/")
30
+ if not BINARY_RE.search(path):
31
+ emit(True, "not a binary artifact")
32
+
33
+ rel = path
34
+ for marker in ("/pay2u/", "/miller-tech/"):
35
+ if marker in rel:
36
+ rel = rel.split(marker, 1)[1]
37
+ break
38
+
39
+ if any(rel.startswith(p) for p in ALLOWED_PREFIXES):
40
+ emit(True, f"allowed prefix: {rel}")
41
+ if any(s in rel for s in ALLOWED_SUBSTRINGS):
42
+ emit(True, f"allowed subdir: {rel}")
43
+
44
+ emit(
45
+ False,
46
+ f"artifact-hygiene: binary '{rel}' must live under docs/, tests/, or apps/**/public|static|assets. "
47
+ "Do not litter repo root.",
48
+ )
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python3
2
+ """cluster-routing enforcer: kubectl/helm context must match component domain."""
3
+ from __future__ import annotations
4
+ import re
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).parent))
9
+ from _common import arg_str, emit, parse_cli, run # noqa: E402
10
+
11
+ DOMAINS = {
12
+ "openobserve": re.compile(
13
+ r"grafana|prometheus|openobserve|fluent-?bit|servicemonitor|alertmanager|loki|tempo|jaeger",
14
+ re.I,
15
+ ),
16
+ "zitadel": re.compile(r"zitadel|oidc|keycloak|iam-crd", re.I),
17
+ }
18
+ CONTEXTS = {
19
+ "openobserve": "do-syd1-pay2u-openobserve",
20
+ "zitadel": "do-syd1-zitadel",
21
+ "main": "do-syd1-pay2u",
22
+ }
23
+
24
+
25
+ def pick_domain(blob: str) -> str:
26
+ for d, rx in DOMAINS.items():
27
+ if rx.search(blob):
28
+ return d
29
+ return "main"
30
+
31
+
32
+ def main() -> None:
33
+ op, args = parse_cli()
34
+ blob = f"{op} {arg_str(args)}"
35
+
36
+ if not re.search(r"\b(kubectl|helm)\b", blob):
37
+ emit(True, "not a kubectl/helm call")
38
+
39
+ if not re.search(
40
+ r"\b(apply|patch|create|edit|delete|install|upgrade|uninstall|rollout)\b", blob
41
+ ):
42
+ emit(True, "read-only kubectl/helm call")
43
+
44
+ rc, out, _ = run(["kubectl", "config", "current-context"])
45
+ ctx = out.strip() if rc == 0 else ""
46
+
47
+ wanted_domain = pick_domain(blob)
48
+ wanted_ctx = CONTEXTS[wanted_domain]
49
+
50
+ if ctx != wanted_ctx:
51
+ emit(
52
+ False,
53
+ f"cluster-routing: context '{ctx}' does not match domain "
54
+ f"'{wanted_domain}'. Run: kubectl config use-context {wanted_ctx}",
55
+ wanted_context=wanted_ctx,
56
+ current_context=ctx,
57
+ )
58
+
59
+ emit(True, f"context '{ctx}' matches domain '{wanted_domain}'")
60
+
61
+
62
+ if __name__ == "__main__":
63
+ main()
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python3
2
+ """codebase-read-before-plan enforcer: plans require prior reads of target paths."""
3
+ from __future__ import annotations
4
+ import os
5
+ import re
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+
10
+ sys.path.insert(0, str(Path(__file__).parent))
11
+ from _common import arg_str, emit, parse_cli # noqa: E402
12
+
13
+ PLAN_OPS = {"ExitPlanMode", "Plan", "TodoWrite"}
14
+ PLAN_WORD_RE = re.compile(r"(?<![-\w/])(plan the|design the|architect the|propose a plan|spec the)", re.I)
15
+ READ_LOG = Path(os.environ.get("UAP_STATE_DIR", ".uap")) / "read_log.state"
16
+ RECENT_SEC = 1800
17
+
18
+
19
+ def recent_reads() -> set[str]:
20
+ if not READ_LOG.exists():
21
+ return set()
22
+ out: set[str] = set()
23
+ now = time.time()
24
+ for line in READ_LOG.read_text().splitlines():
25
+ try:
26
+ ts, path = line.split("\t", 1)
27
+ if now - float(ts) < RECENT_SEC:
28
+ out.add(path)
29
+ except ValueError:
30
+ continue
31
+ return out
32
+
33
+
34
+ def main() -> None:
35
+ op, args = parse_cli()
36
+ blob = f"{op} {arg_str(args)}"
37
+ if op not in PLAN_OPS and not PLAN_WORD_RE.search(blob):
38
+ emit(True, "not a plan op")
39
+
40
+ reads = recent_reads()
41
+ if reads:
42
+ emit(True, f"{len(reads)} recent codebase reads on record")
43
+
44
+ emit(
45
+ False,
46
+ "codebase-read-before-plan: no Read/Grep/Glob within the last 30 min. "
47
+ "Read the existing codebase in the target scope before emitting a plan.",
48
+ )
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env python3
2
+ """coord-overlap enforcer: check for in-flight agent path reservations."""
3
+ from __future__ import annotations
4
+ import sqlite3
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).parent))
9
+ from _common import emit, parse_cli, repo_root # noqa: E402
10
+
11
+ AGENT_OPS = {"Agent", "spawn-agent", "subagent", "delegate"}
12
+
13
+
14
+ def overlapping_reservations(root: Path, paths: list[str]) -> list[str]:
15
+ db = root / "agents" / "data" / "coordination" / "coordination.db"
16
+ if not db.exists() or not paths:
17
+ return []
18
+ try:
19
+ con = sqlite3.connect(f"file:{db}?mode=ro", uri=True, timeout=1.0)
20
+ # Best-effort: look for any reservations table with path-like column
21
+ cur = con.execute(
22
+ "SELECT name FROM sqlite_master WHERE type='table'"
23
+ )
24
+ tables = [r[0] for r in cur.fetchall()]
25
+ hits: list[str] = []
26
+ for t in tables:
27
+ try:
28
+ cols = [r[1] for r in con.execute(f"PRAGMA table_info({t})")]
29
+ path_col = next(
30
+ (c for c in cols if c.lower() in ("path", "paths", "file", "scope")),
31
+ None,
32
+ )
33
+ status_col = next(
34
+ (c for c in cols if c.lower() in ("status", "state", "active")), None
35
+ )
36
+ if not path_col:
37
+ continue
38
+ where = f" WHERE {status_col} IN ('active','in_progress',1)" if status_col else ""
39
+ rows = con.execute(f"SELECT {path_col} FROM {t}{where}").fetchall()
40
+ for (v,) in rows:
41
+ if not v:
42
+ continue
43
+ for p in paths:
44
+ if p and p in str(v):
45
+ hits.append(f"{t}:{v}")
46
+ except sqlite3.Error:
47
+ continue
48
+ con.close()
49
+ return hits
50
+ except sqlite3.Error:
51
+ return []
52
+
53
+
54
+ def main() -> None:
55
+ op, args = parse_cli()
56
+ if op not in AGENT_OPS:
57
+ emit(True, "not an agent-spawn op")
58
+
59
+ paths_raw = (
60
+ args.get("paths")
61
+ or args.get("scope")
62
+ or args.get("prompt", "")
63
+ )
64
+ if isinstance(paths_raw, list):
65
+ paths = [str(p) for p in paths_raw]
66
+ else:
67
+ paths = [p for p in str(paths_raw).split() if "/" in p]
68
+
69
+ hits = overlapping_reservations(repo_root(), paths)
70
+ if hits:
71
+ emit(
72
+ False,
73
+ f"coord-overlap: active reservations on: {', '.join(hits[:5])}. "
74
+ "Run `uap coordination check` before spawning.",
75
+ )
76
+
77
+ emit(True, "no overlapping reservations")
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python3
2
+ """delivery-enforcement enforcer: route substantive coding through `uap deliver`.
3
+
4
+ Fires on Edit/Write/MultiEdit to source-code files. The intent is that
5
+ non-trivial coding work goes through the `uap deliver` convergence loop (which
6
+ drives a model to verified completion against the real gates) rather than
7
+ ad-hoc hand edits.
8
+
9
+ SAFETY — default mode is ADVISORY (always allows, logs a nudge), so installing
10
+ this policy never breaks editing. Strict enforcement is opt-in:
11
+
12
+ UAP_ENFORCE_DELIVERY=block # direct source edits outside a deliver context
13
+ # are blocked (exit 2)
14
+
15
+ Escape hatches (always honored, even in block mode):
16
+ - UAP_DELIVER_ACTIVE=1 set by the deliver loop for its own subprocesses
17
+ - UAP_DELIVER_BYPASS=1 explicit operator override for a sanctioned manual edit
18
+
19
+ Exempt by construction: non-source files, docs/configs/scripts/policies, test
20
+ files (deliver protects those itself), and tooling dot-dirs.
21
+ """
22
+ from __future__ import annotations
23
+ import os
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ sys.path.insert(0, str(Path(__file__).parent))
28
+ from _common import emit, parse_cli, repo_root # noqa: E402
29
+
30
+ EDIT_OPS = {"Edit", "Write", "MultiEdit", "edit", "write", "multiedit"}
31
+
32
+ # Only real implementation code is gated.
33
+ SOURCE_EXTS = (
34
+ ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs",
35
+ ".py", ".go", ".rs", ".java", ".rb", ".php", ".cs", ".swift", ".kt",
36
+ ".c", ".cc", ".cpp", ".h", ".hpp",
37
+ )
38
+
39
+ EXEMPT_PREFIXES = (
40
+ ".claude/", ".cursor/", ".opencode/", ".codex/", ".forge/", ".omp/",
41
+ ".uap/", ".policy-tools/", ".worktrees/",
42
+ "src/policies/", "scripts/", "docs/", "policies/", "test/", "tests/",
43
+ )
44
+
45
+ # Test files are protected by deliver itself; never gate them here.
46
+ TEST_MARKERS = (".test.", ".spec.", "_test.", "/test/", "/tests/", "/__tests__/")
47
+
48
+
49
+ def main() -> None:
50
+ op, args = parse_cli()
51
+ if op not in EDIT_OPS:
52
+ emit(True, "not a file-edit operation")
53
+
54
+ target = args.get("file_path") or args.get("path") or args.get("target") or ""
55
+ if not target:
56
+ emit(True, "no file path in args")
57
+
58
+ root = repo_root()
59
+ try:
60
+ rel = str(Path(target).resolve().relative_to(root))
61
+ except ValueError:
62
+ emit(True, "target outside repo")
63
+
64
+ rel_posix = rel.replace(os.sep, "/")
65
+ low = rel_posix.lower()
66
+
67
+ if not low.endswith(SOURCE_EXTS):
68
+ emit(True, "not source code")
69
+ if any(rel_posix.startswith(p) for p in EXEMPT_PREFIXES):
70
+ emit(True, f"exempt path: {rel_posix}")
71
+ if any(m in "/" + low for m in TEST_MARKERS):
72
+ emit(True, "test file (protected by deliver itself)")
73
+
74
+ # Escape hatches.
75
+ if os.environ.get("UAP_DELIVER_ACTIVE") == "1":
76
+ emit(True, "inside a deliver-driven run")
77
+ if os.environ.get("UAP_DELIVER_BYPASS") == "1":
78
+ emit(True, "UAP_DELIVER_BYPASS override set")
79
+
80
+ msg = (
81
+ f"delivery-enforcement: '{rel_posix}' is source code being edited directly. "
82
+ "Route substantive coding through `uap deliver` (drives a model to verified "
83
+ "completion against the gates), or set UAP_DELIVER_BYPASS=1 for a sanctioned "
84
+ "manual edit."
85
+ )
86
+
87
+ mode = os.environ.get("UAP_ENFORCE_DELIVERY", "advisory").lower()
88
+ if mode == "block":
89
+ emit(False, msg)
90
+
91
+ # Advisory (default): never blocks. Surface the nudge, then allow.
92
+ print(f"[delivery-enforcement advisory] {msg}", file=sys.stderr)
93
+ emit(True, "advisory: nudge logged (set UAP_ENFORCE_DELIVERY=block to enforce)")
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env python3
2
+ """doc-live-over-report enforcer: block new *_REPORT/*_COMPLETE/*_SUMMARY/*_PLAN md files."""
3
+ from __future__ import annotations
4
+ import re
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).parent))
9
+ from _common import emit, parse_cli # noqa: E402
10
+
11
+ BLOCKED_RE = re.compile(
12
+ r"(_REPORT|_COMPLETE|_SUMMARY|_PLAN|_FIX_\d|_\d{4}-\d{2}-\d{2})\.md$",
13
+ re.I,
14
+ )
15
+ SCOPED_DIRS = ("infra/", "docs/", "")
16
+ WRITE_OPS = {"Write", "write", "create-file"}
17
+
18
+
19
+ def main() -> None:
20
+ op, args = parse_cli()
21
+ if op not in WRITE_OPS:
22
+ emit(True, "not a write op")
23
+
24
+ path = args.get("file_path") or args.get("path") or ""
25
+ if not path.endswith(".md"):
26
+ emit(True, "not a markdown file")
27
+
28
+ rel = path.replace("\\", "/")
29
+ # Strip leading abs path prefix if present
30
+ for marker in ("/pay2u/", "/miller-tech/"):
31
+ if marker in rel:
32
+ rel = rel.split(marker, 1)[1]
33
+ break
34
+
35
+ is_scoped = any(rel.startswith(d) for d in SCOPED_DIRS if d) or "/" not in rel
36
+ if not is_scoped:
37
+ emit(True, "out-of-scope path")
38
+
39
+ if BLOCKED_RE.search(rel):
40
+ emit(
41
+ False,
42
+ f"doc-live-over-report: '{rel}' is a retrospective/dated doc pattern. "
43
+ "Update canonical README/runbook instead.",
44
+ )
45
+
46
+ emit(True, "not a blocked doc pattern")
47
+
48
+
49
+ if __name__ == "__main__":
50
+ main()
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python3
2
+ """expert-review-required enforcer: a parallel expert review must precede ship.
3
+
4
+ Blocks ship actions (git commit / git push / gh pr create / merge / pr-ready /
5
+ signoff) unless a review artifact exists for the current branch AND covers the
6
+ current HEAD. This makes the `parallel-expert-review` skill's "REQUIRED by
7
+ policy" claim real rather than advisory.
8
+
9
+ Review artifact: .uap/reviews/<branch-slug>.json, written by the
10
+ parallel-expert-review flow on consolidation. Recognised shape:
11
+ { "head": "<sha>", "verdict": "approve|...", "reviewers": [...] }
12
+ If the artifact carries a `head` that differs from the current HEAD, the review
13
+ is stale and the op is blocked.
14
+
15
+ Fail-open: if branch/HEAD cannot be resolved, the op is allowed — non-UAP repos
16
+ and detached states are unaffected. Override: set UAP_NO_REVIEW=1 to bypass.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import os
22
+ import re
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ sys.path.insert(0, str(Path(__file__).parent))
27
+ from _common import emit, parse_cli, repo_root, run # noqa: E402
28
+
29
+ # Ship verbs are anchored to their tool prefix so that the bare tokens "merge"
30
+ # or "signoff" inside read-only commands (git diff --merge-base, rg merge,
31
+ # cat docs/merge-strategy.md) do not trip the gate.
32
+ SHIP_PATTERNS = (
33
+ re.compile(r"\bgit\s+(commit|push|merge)\b"),
34
+ re.compile(r"\bgh\s+pr\s+(create|merge|ready)\b"),
35
+ re.compile(r"\b(pr[-_ ]?ready|sign[-_ ]?off|ready[-_ ]for[-_ ]review)\b", re.I),
36
+ )
37
+
38
+
39
+ def current_branch(root: Path) -> str | None:
40
+ # symbolic-ref resolves the branch name even on an unborn branch (no commits
41
+ # yet); rev-parse --abbrev-ref returns "HEAD" in that state.
42
+ rc, out, _ = run(["git", "symbolic-ref", "--short", "HEAD"], cwd=root)
43
+ if rc == 0 and out.strip():
44
+ return out.strip()
45
+ rc, out, _ = run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=root)
46
+ if rc == 0 and out.strip() and out.strip() != "HEAD":
47
+ return out.strip()
48
+ return None
49
+
50
+
51
+ def slug_for(branch: str) -> str:
52
+ """Injective filename slug for a branch ref.
53
+
54
+ A naive `/`->`-` substitution collapses distinct refs (`feature/foo` and
55
+ `feature-foo`, which can coexist) onto the same artifact, silently bypassing
56
+ the gate. Percent-encode `%` first, then `/`, so the mapping is reversible
57
+ and collision-free: `feature/foo` -> `feature%2Ffoo`, `feature-foo` stays
58
+ `feature-foo`.
59
+ """
60
+ return branch.replace("%", "%25").replace("/", "%2F")
61
+
62
+
63
+ def head_sha(root: Path) -> str | None:
64
+ rc, out, _ = run(["git", "rev-parse", "HEAD"], cwd=root)
65
+ return out.strip() if rc == 0 and out.strip() else None
66
+
67
+
68
+ def main() -> None:
69
+ op, args = parse_cli()
70
+
71
+ if os.environ.get("UAP_NO_REVIEW") == "1":
72
+ emit(True, "UAP_NO_REVIEW override set")
73
+
74
+ op_l = op.lower()
75
+ if op_l != "bash":
76
+ emit(True, "not a ship operation")
77
+
78
+ cmd = args.get("command") or args.get("cmd") or ""
79
+ if not any(p.search(cmd) for p in SHIP_PATTERNS):
80
+ emit(True, "not a ship action")
81
+
82
+ root = repo_root()
83
+ branch = current_branch(root)
84
+ if branch is None:
85
+ emit(True, "branch not resolvable (detached/non-git) — fail-open")
86
+ slug = slug_for(branch)
87
+
88
+ review = root / ".uap" / "reviews" / f"{slug}.json"
89
+ if not review.exists():
90
+ emit(
91
+ False,
92
+ f"expert-review-required: no review artifact at .uap/reviews/{slug}.json. "
93
+ "Run the parallel-expert-review skill (code-quality, security, performance, "
94
+ "docs, test-coverage reviewers) and record the consolidated verdict before "
95
+ "shipping. Override for one-off meta-work: UAP_NO_REVIEW=1.",
96
+ )
97
+
98
+ head = head_sha(root)
99
+ try:
100
+ data = json.loads(review.read_text())
101
+ except Exception: # noqa: BLE001
102
+ data = {}
103
+
104
+ # Defense-in-depth against artifact reuse across branches: if the artifact
105
+ # records the branch it covers, it must match the current branch.
106
+ artifact_branch = data.get("branch") if isinstance(data, dict) else None
107
+ if artifact_branch and artifact_branch != branch:
108
+ emit(
109
+ False,
110
+ f"expert-review-required: review at .uap/reviews/{slug}.json covers branch "
111
+ f"'{artifact_branch}', not '{branch}'. Re-run the parallel expert review on "
112
+ "this branch. Override: UAP_NO_REVIEW=1.",
113
+ )
114
+
115
+ # Stale check relative to current HEAD.
116
+ reviewed_head = data.get("head") if isinstance(data, dict) else None
117
+ if reviewed_head and head and reviewed_head != head:
118
+ emit(
119
+ False,
120
+ f"expert-review-required: review at .uap/reviews/{slug}.json covers "
121
+ f"{reviewed_head[:8]} but HEAD is {head[:8]} — the review is stale. "
122
+ "Re-run the parallel expert review for the current changes. "
123
+ "Override: UAP_NO_REVIEW=1.",
124
+ )
125
+
126
+ emit(
127
+ True,
128
+ f"expert-review satisfied (.uap/reviews/{slug}.json"
129
+ + (f", head {reviewed_head[:8]}" if reviewed_head else "")
130
+ + ")",
131
+ )
132
+
133
+
134
+ if __name__ == "__main__":
135
+ main()
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env python3
2
+ """iac-parity enforcer: live-state changes must have matching IaC diff."""
3
+ from __future__ import annotations
4
+ import re
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).parent))
9
+ from _common import arg_str, emit, parse_cli, run, worktree_root # noqa: E402
10
+
11
+ MUTATING_RE = re.compile(
12
+ r"\b(kubectl|helm|doctl|aws|gcloud)\b.*?\b(apply|patch|create|edit|delete|install|upgrade|rollout|scale|set)\b",
13
+ re.I,
14
+ )
15
+ IAC_PATHS = (
16
+ "infra/terraform/",
17
+ "infra/helm_charts/",
18
+ "infra/kubernetes/",
19
+ "infra/k8s/",
20
+ "infra/policies/",
21
+ )
22
+
23
+
24
+ def main() -> None:
25
+ op, args = parse_cli()
26
+ blob = f"{op} {arg_str(args)}"
27
+
28
+ if not MUTATING_RE.search(blob):
29
+ emit(True, "not a mutating IaC-scope command")
30
+
31
+ root = worktree_root() # git status must run against the working tree, not MAIN_ROOT
32
+ rc, out, _ = run(["git", "status", "--porcelain"], cwd=root)
33
+ if rc != 0:
34
+ emit(True, "git status unavailable; deferring to post-commit check")
35
+
36
+ has_iac = any(
37
+ line[3:].startswith(IAC_PATHS) or line[3:].lstrip().startswith(IAC_PATHS)
38
+ for line in out.splitlines()
39
+ )
40
+
41
+ if not has_iac:
42
+ emit(
43
+ False,
44
+ "iac-parity: live-state mutation without matching IaC diff under "
45
+ + ", ".join(IAC_PATHS)
46
+ + ". Update Terraform/Helm/K8s manifests in the same worktree.",
47
+ )
48
+
49
+ emit(True, "IaC diff present in worktree")
50
+
51
+
52
+ if __name__ == "__main__":
53
+ main()