@miller-tech/uap 1.40.0 → 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.
- package/README.md +109 -642
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli/deliver-defaults.d.ts +23 -0
- package/dist/cli/deliver-defaults.d.ts.map +1 -0
- package/dist/cli/deliver-defaults.js +121 -0
- package/dist/cli/deliver-defaults.js.map +1 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +29 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +19 -0
- package/dist/cli/setup.js.map +1 -1
- package/dist/policies/policy-tools.d.ts +7 -0
- package/dist/policies/policy-tools.d.ts.map +1 -1
- package/dist/policies/policy-tools.js +24 -2
- package/dist/policies/policy-tools.js.map +1 -1
- package/docs/INDEX.md +48 -286
- package/docs/architecture/OVERVIEW.md +328 -0
- package/docs/architecture/PROTOCOL.md +204 -0
- package/docs/benchmarks/README.md +17 -192
- package/docs/getting-started/CONFIGURATION.md +237 -0
- package/docs/getting-started/INSTALLATION.md +125 -0
- package/docs/getting-started/QUICKSTART.md +115 -0
- package/docs/guides/COORDINATION.md +162 -0
- package/docs/guides/DELIVER.md +115 -0
- package/docs/guides/DEPLOY_BATCHING.md +212 -0
- package/docs/guides/DROIDS_AND_SKILLS.md +202 -0
- package/docs/guides/LOCAL_MODELS.md +148 -0
- package/docs/guides/MCP_ROUTER.md +195 -0
- package/docs/guides/MEMORY.md +235 -0
- package/docs/guides/MULTI_MODEL.md +223 -0
- package/docs/guides/POLICIES.md +190 -0
- package/docs/guides/WORKTREE_WORKFLOW.md +185 -0
- package/docs/integrations/MCP_ROUTER.md +147 -0
- package/docs/integrations/RTK.md +102 -0
- package/docs/reference/API.md +485 -0
- package/docs/reference/CLI.md +719 -0
- package/docs/reference/CONFIGURATION.md +90 -193
- package/docs/reference/DATABASE_SCHEMA.md +110 -344
- package/docs/reference/FEATURES.md +176 -472
- package/docs/reference/PATTERNS.md +102 -0
- package/docs/reference/PLATFORMS.md +83 -0
- package/package.json +3 -1
- package/src/policies/enforcers/7ebbc721-7540-4e9f-879a-770e0213a09b_architecture_review.py +101 -0
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/_common.py +100 -0
- package/src/policies/enforcers/artifact_hygiene.py +52 -0
- package/src/policies/enforcers/cluster_routing.py +63 -0
- package/src/policies/enforcers/codebase_read_before_plan.py +52 -0
- package/src/policies/enforcers/coord_overlap.py +81 -0
- package/src/policies/enforcers/delivery_enforcement.py +97 -0
- package/src/policies/enforcers/doc_live_over_report.py +50 -0
- package/src/policies/enforcers/expert_review_required.py +135 -0
- package/src/policies/enforcers/iac_parity.py +53 -0
- package/src/policies/enforcers/mcp_router_first.py +37 -0
- package/src/policies/enforcers/memory_before_plan.py +61 -0
- package/src/policies/enforcers/parallel_reads.py +50 -0
- package/src/policies/enforcers/rtk_wrap.py +44 -0
- package/src/policies/enforcers/schema_diff_gate.py +80 -0
- package/src/policies/enforcers/session_memory_write.py +52 -0
- package/src/policies/enforcers/task_required.py +131 -0
- package/src/policies/enforcers/test_gate.py +58 -0
- package/src/policies/enforcers/validate_plan_before_build.py +75 -0
- package/src/policies/enforcers/worktree_required.py +57 -0
- package/src/policies/schemas/policies/architecture-review.md +51 -0
- package/src/policies/schemas/policies/artifact-hygiene.md +29 -0
- package/src/policies/schemas/policies/cluster-routing.md +31 -0
- package/src/policies/schemas/policies/codebase-read-before-plan.md +30 -0
- package/src/policies/schemas/policies/coord-overlap.md +24 -0
- package/src/policies/schemas/policies/delivery-enforcement.md +45 -0
- package/src/policies/schemas/policies/doc-live-over-report.md +32 -0
- package/src/policies/schemas/policies/expert-review-required.md +60 -0
- package/src/policies/schemas/policies/iac-parity.md +31 -0
- package/src/policies/schemas/policies/mandatory-testing-deployment.md +147 -0
- package/src/policies/schemas/policies/mcp-router-first.md +24 -0
- package/src/policies/schemas/policies/memory-before-plan.md +24 -0
- package/src/policies/schemas/policies/merge-deploy-monitor-verify.md +145 -0
- package/src/policies/schemas/policies/parallel-reads.md +24 -0
- package/src/policies/schemas/policies/rtk-wrap.md +26 -0
- package/src/policies/schemas/policies/schema-diff-gate.md +30 -0
- package/src/policies/schemas/policies/session-memory-write.md +24 -0
- package/src/policies/schemas/policies/task-required.md +49 -0
- package/src/policies/schemas/policies/test-gate.md +24 -0
- package/src/policies/schemas/policies/validate-plan-before-build.md +28 -0
- package/src/policies/schemas/policies/worktree-required.md +28 -0
- package/templates/hooks/uap-policy-gate.sh +5 -0
- package/docs/AGENTS.md +0 -423
- package/docs/DOCUMENTATION_AUDIT_REPORT.md +0 -131
- package/docs/GETTING_STARTED.md +0 -288
- package/docs/PROJECT_ANALYSIS_REPORT.md +0 -510
- package/docs/architecture/COMPLETE_ARCHITECTURE.md +0 -748
- package/docs/architecture/EXPERT_STACK.md +0 -137
- package/docs/architecture/MULTI_MODEL.md +0 -224
- package/docs/architecture/PLATFORM_GATING.md +0 -68
- package/docs/architecture/SYSTEM_ANALYSIS.md +0 -334
- package/docs/architecture/UAP_COMPLIANCE.md +0 -217
- package/docs/architecture/UAP_PROTOCOL.md +0 -339
- package/docs/architecture/UAP_STRICT_DROIDS.md +0 -172
- package/docs/archive/BALLS_MODE_SELF_ANALYSIS.md +0 -260
- package/docs/archive/BENCHMARK_GAPS_AND_PLAN.md +0 -146
- package/docs/archive/FAILING_TASKS_SOLUTION_PLAN.md +0 -668
- package/docs/archive/JINJA2-SYSTEM-MESSAGE-FIX.md +0 -209
- package/docs/archive/MODEL_ROUTING_IMPLEMENTATION_SUMMARY.md +0 -281
- package/docs/archive/MODEL_ROUTING_OPTIMIZATION_PLAN.md +0 -320
- package/docs/archive/NPM-PUBLISH-V0.9.1.md +0 -240
- package/docs/archive/OPTIMIZATION_OPTIONS.md +0 -334
- package/docs/archive/PARALLELISM_GAPS_AND_OPTIONS.md +0 -422
- package/docs/archive/POLICY_GATE_IMPLEMENTATION.md +0 -245
- package/docs/archive/SETUP_IMPROVEMENTS.md +0 -213
- package/docs/archive/UAP_GENERIC_OPTIMIZATION_PLAN.md +0 -270
- package/docs/archive/UAP_OPTIMIZATION_PLAN.md +0 -701
- package/docs/archive/UAP_V103_PATTERN_DESIGN.md +0 -315
- package/docs/archive/UAP_V104_COMPLIANCE_DESIGN.md +0 -223
- package/docs/archive/changelog/2026-03-10_uap-100-compliance.md +0 -77
- package/docs/archive/changelog/2026-03-10_uap-full-system-verification.md +0 -109
- package/docs/archive/opencode-integration-guide.md +0 -740
- package/docs/archive/opencode-integration-quickref.md +0 -180
- package/docs/benchmarks/OVERNIGHT_RUNNER.md +0 -341
- package/docs/benchmarks/SPECULATIVE_DECODING_JOURNEY_2026-03.md +0 -221
- package/docs/benchmarks/VALIDATION_PLAN.md +0 -568
- package/docs/blog/SPECULATIVE_DECODING_PRODUCTION_PLAYBOOK.md +0 -139
- package/docs/blog/local-coding-agents.md +0 -266
- package/docs/blog/x-thread.md +0 -254
- package/docs/deployment/DEPLOYMENT.md +0 -895
- package/docs/deployment/DEPLOYMENT_STRATEGIES.md +0 -518
- package/docs/deployment/DEPLOY_BATCHER_ANALYSIS.md +0 -224
- package/docs/deployment/DEPLOY_BATCHING.md +0 -273
- package/docs/deployment/DEPLOY_BUCKETING_ANALYSIS.md +0 -420
- package/docs/deployment/QWEN35_LLAMA_CPP.md +0 -426
- package/docs/deployment/UAP_LLAMA_ANTHROPIC_PROXY_BOOTSTRAP.md +0 -279
- package/docs/getting-started/INTEGRATION.md +0 -628
- package/docs/getting-started/OVERVIEW.md +0 -324
- package/docs/getting-started/SETUP.md +0 -377
- package/docs/integrations/MCP_ROUTER_SETUP.md +0 -445
- package/docs/integrations/RTK_INTEGRATION.md +0 -468
- package/docs/operations/TROUBLESHOOTING.md +0 -660
- package/docs/pr/PR_SPECULATIVE_DOCS_TEMPLATE.md +0 -146
- package/docs/pr/UPSTREAM_PRS.md +0 -424
- package/docs/reference/API_REFERENCE.md +0 -903
- package/docs/reference/EXPERT_DROIDS.md +0 -219
- package/docs/reference/HARNESS-MATRIX.md +0 -318
- package/docs/reference/PATTERN_LIBRARY.md +0 -636
- package/docs/reference/UAP_CLI_REFERENCE.md +0 -620
- package/docs/research/BEHAVIORAL_PATTERNS.md +0 -228
- package/docs/research/DOMAIN_STRATEGIES.md +0 -316
- package/docs/research/MEMORY_SYSTEMS_COMPARISON.md +0 -812
- package/docs/research/PATTERN_ANALYSIS_2026-01-18.md +0 -436
- package/docs/research/PERFORMANCE_ANALYSIS_2026-01-18.md +0 -209
- package/docs/research/PERFORMANCE_TEST_PLAN.md +0 -383
- package/docs/research/TERMINAL_BENCH_LEARNINGS.md +0 -217
|
@@ -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()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""mcp-router-first enforcer: MCP tools must be loaded on demand."""
|
|
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 # noqa: E402
|
|
10
|
+
|
|
11
|
+
BULK_PATTERNS = re.compile(r"(load[-_ ]all|bulk[-_ ]load|all[-_ ]tools|eager)", re.I)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
op, args = parse_cli()
|
|
16
|
+
blob = f"{op} {arg_str(args)}"
|
|
17
|
+
|
|
18
|
+
if op not in {"ToolSearch", "tool_search", "mcp-router", "mcp_router"}:
|
|
19
|
+
emit(True, "not an MCP-router op")
|
|
20
|
+
|
|
21
|
+
query = (args.get("query") or "").strip()
|
|
22
|
+
max_results = int(args.get("max_results") or 5)
|
|
23
|
+
|
|
24
|
+
if BULK_PATTERNS.search(blob):
|
|
25
|
+
emit(False, "mcp-router-first: bulk/eager MCP tool load detected; query by specific tool name instead")
|
|
26
|
+
|
|
27
|
+
if not query or max_results > 20:
|
|
28
|
+
emit(
|
|
29
|
+
False,
|
|
30
|
+
"mcp-router-first: ToolSearch must use a specific query and max_results<=20",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
emit(True, "scoped MCP router query")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""memory-before-plan enforcer: plans require a recent uap memory query."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import re
|
|
5
|
+
import sqlite3
|
|
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, repo_root # noqa: E402
|
|
12
|
+
|
|
13
|
+
PLAN_OPS = {"ExitPlanMode", "Plan", "TodoWrite", "plan", "design"}
|
|
14
|
+
# Only match standalone words, not compounds like 'validate-plan-before-build'
|
|
15
|
+
PLAN_WORD_RE = re.compile(r"(?<![-\w/])(plan the|design the|architect the|propose a plan|roadmap for)", re.I)
|
|
16
|
+
RECENT_SEC = 300
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def recent_memory_query(root: Path) -> bool:
|
|
20
|
+
db = root / "agents" / "data" / "memory" / "short_term.db"
|
|
21
|
+
if not db.exists():
|
|
22
|
+
return False
|
|
23
|
+
try:
|
|
24
|
+
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True, timeout=1.0)
|
|
25
|
+
cur = con.execute(
|
|
26
|
+
"SELECT timestamp FROM session_memories "
|
|
27
|
+
"WHERE content LIKE '%uap memory query%' OR type='memory_query' "
|
|
28
|
+
"ORDER BY id DESC LIMIT 1"
|
|
29
|
+
)
|
|
30
|
+
row = cur.fetchone()
|
|
31
|
+
con.close()
|
|
32
|
+
if not row:
|
|
33
|
+
return False
|
|
34
|
+
raw = row[0][:19].replace("T", " ")
|
|
35
|
+
try:
|
|
36
|
+
ts = time.mktime(time.strptime(raw, "%Y-%m-%d %H:%M:%S"))
|
|
37
|
+
except Exception: # noqa: BLE001
|
|
38
|
+
return False
|
|
39
|
+
# Memory is stored as UTC from datetime('now'); compare with UTC now
|
|
40
|
+
return (time.time() - (ts - time.timezone)) < RECENT_SEC
|
|
41
|
+
except sqlite3.Error:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main() -> None:
|
|
46
|
+
op, args = parse_cli()
|
|
47
|
+
blob = f"{op} {arg_str(args)}"
|
|
48
|
+
if op not in PLAN_OPS and not PLAN_WORD_RE.search(blob):
|
|
49
|
+
emit(True, "not a plan operation")
|
|
50
|
+
|
|
51
|
+
if recent_memory_query(repo_root()):
|
|
52
|
+
emit(True, "recent uap memory query on record")
|
|
53
|
+
|
|
54
|
+
emit(
|
|
55
|
+
False,
|
|
56
|
+
"memory-before-plan: run `uap memory query <topic>` before planning to surface prior context",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
main()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""parallel-reads enforcer: nudge when serial read fan-out is detected."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
10
|
+
from _common import emit, parse_cli # noqa: E402
|
|
11
|
+
|
|
12
|
+
READ_OPS = {"Read", "Grep", "Glob", "WebFetch", "read", "grep", "glob", "webfetch"}
|
|
13
|
+
STATE = Path(os.environ.get("UAP_STATE_DIR", ".uap")) / "parallel_reads.state"
|
|
14
|
+
WINDOW_SEC = 4.0
|
|
15
|
+
THRESHOLD = 2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> None:
|
|
19
|
+
op, _args = parse_cli()
|
|
20
|
+
if op not in READ_OPS:
|
|
21
|
+
emit(True, "not a read op")
|
|
22
|
+
|
|
23
|
+
STATE.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
now = time.time()
|
|
25
|
+
history: list[float] = []
|
|
26
|
+
if STATE.exists():
|
|
27
|
+
try:
|
|
28
|
+
history = [
|
|
29
|
+
float(l) for l in STATE.read_text().splitlines() if l.strip()
|
|
30
|
+
]
|
|
31
|
+
except ValueError:
|
|
32
|
+
history = []
|
|
33
|
+
|
|
34
|
+
history = [t for t in history if now - t < WINDOW_SEC]
|
|
35
|
+
history.append(now)
|
|
36
|
+
STATE.write_text("\n".join(f"{t}" for t in history[-10:]))
|
|
37
|
+
|
|
38
|
+
if len(history) > THRESHOLD:
|
|
39
|
+
emit(
|
|
40
|
+
True,
|
|
41
|
+
f"parallel-reads: {len(history)} serial read ops in {WINDOW_SEC}s "
|
|
42
|
+
"— batch independent reads in a single tool-call message for 2-5x speed-up",
|
|
43
|
+
warning=True,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
emit(True, "read cadence within batch window")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
main()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""rtk-wrap enforcer: heavy CLIs must be invoked via rtk."""
|
|
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
|
+
WRAPPED = ("git", "kubectl", "docker", "docker-compose", "npm", "pnpm", "yarn", "helm", "terraform")
|
|
12
|
+
RTK_META = re.compile(r"^\s*rtk\s+(gain|discover|proxy|--version|-V|--help)\b")
|
|
13
|
+
ALREADY_WRAPPED = re.compile(r"^\s*rtk\s+\S+")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
op, args = parse_cli()
|
|
18
|
+
cmd = (args.get("command") or args.get("cmd") or "").strip()
|
|
19
|
+
if not cmd or op.lower() != "bash":
|
|
20
|
+
emit(True, "not a Bash command")
|
|
21
|
+
|
|
22
|
+
first = cmd.split(maxsplit=1)[0].lstrip("(").lstrip("{")
|
|
23
|
+
if first == "rtk" and RTK_META.search(cmd):
|
|
24
|
+
emit(True, "rtk meta command")
|
|
25
|
+
if ALREADY_WRAPPED.match(cmd):
|
|
26
|
+
emit(True, "already wrapped")
|
|
27
|
+
|
|
28
|
+
# Inspect tokens (ignore env assignments like FOO=bar cmd)
|
|
29
|
+
tokens = [t for t in cmd.split() if "=" not in t.split("/")[0]]
|
|
30
|
+
for tok in tokens[:3]:
|
|
31
|
+
bin_name = tok.split("/")[-1]
|
|
32
|
+
if bin_name in WRAPPED:
|
|
33
|
+
emit(
|
|
34
|
+
False,
|
|
35
|
+
f"rtk-wrap: '{bin_name}' must be invoked via rtk. "
|
|
36
|
+
f"Use: rtk {cmd}",
|
|
37
|
+
bin=bin_name,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
emit(True, "no wrapped CLI in command")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
main()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""schema-diff-gate enforcer: schema/pool changes must pass uap schema-diff."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import re
|
|
5
|
+
import sqlite3
|
|
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 emit, parse_cli, repo_root, run, worktree_root # noqa: E402
|
|
12
|
+
|
|
13
|
+
WATCHED_RE = re.compile(
|
|
14
|
+
r"(migrations/.*\.sql|infra/postgres-spock/|infra/helm_charts/[^/]*pgdog|"
|
|
15
|
+
r"infra/helm_charts/[^/]*cnpg|infra/helm_charts/[^/]*redis|"
|
|
16
|
+
r"infra/helm_charts/[^/]*envoy|infra/helm_charts/[^/]*sentinel)",
|
|
17
|
+
re.I,
|
|
18
|
+
)
|
|
19
|
+
COMMIT_OPS = {"git-commit", "git commit", "Bash"}
|
|
20
|
+
RECENT_SEC = 3600
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def touched_watched_paths(root: Path) -> list[str]:
|
|
24
|
+
rc, out, _ = run(["git", "diff", "--name-only", "HEAD"], cwd=root)
|
|
25
|
+
if rc != 0:
|
|
26
|
+
return []
|
|
27
|
+
rc2, staged, _ = run(["git", "diff", "--name-only", "--cached"], cwd=root)
|
|
28
|
+
all_files = (out + "\n" + (staged if rc2 == 0 else "")).splitlines()
|
|
29
|
+
return [f for f in all_files if f and WATCHED_RE.search(f)]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def schema_diff_ok(root: Path) -> bool:
|
|
33
|
+
db = root / "agents" / "data" / "memory" / "short_term.db"
|
|
34
|
+
if not db.exists():
|
|
35
|
+
return False
|
|
36
|
+
try:
|
|
37
|
+
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True, timeout=1.0)
|
|
38
|
+
cur = con.execute(
|
|
39
|
+
"SELECT timestamp FROM session_memories "
|
|
40
|
+
"WHERE content LIKE '%schema-diff%pass%' "
|
|
41
|
+
"ORDER BY id DESC LIMIT 1"
|
|
42
|
+
)
|
|
43
|
+
row = cur.fetchone()
|
|
44
|
+
con.close()
|
|
45
|
+
if not row:
|
|
46
|
+
return False
|
|
47
|
+
try:
|
|
48
|
+
ts = time.mktime(time.strptime(row[0][:19], "%Y-%m-%dT%H:%M:%S"))
|
|
49
|
+
except Exception: # noqa: BLE001
|
|
50
|
+
return False
|
|
51
|
+
return (time.time() - ts) < RECENT_SEC
|
|
52
|
+
except sqlite3.Error:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main() -> None:
|
|
57
|
+
op, args = parse_cli()
|
|
58
|
+
cmd = (args.get("command") or "").lower()
|
|
59
|
+
is_commit = op in COMMIT_OPS or "git commit" in cmd or "git push" in cmd
|
|
60
|
+
if not is_commit:
|
|
61
|
+
emit(True, "not a commit/push gate point")
|
|
62
|
+
|
|
63
|
+
# git diff runs against the working tree; short_term.db lives in MAIN_ROOT
|
|
64
|
+
watched = touched_watched_paths(worktree_root())
|
|
65
|
+
if not watched:
|
|
66
|
+
emit(True, "no watched schema/pool paths in diff")
|
|
67
|
+
|
|
68
|
+
if schema_diff_ok(repo_root()):
|
|
69
|
+
emit(True, f"recent schema-diff pass covers: {', '.join(watched[:5])}")
|
|
70
|
+
|
|
71
|
+
emit(
|
|
72
|
+
False,
|
|
73
|
+
"schema-diff-gate: changes to "
|
|
74
|
+
+ ", ".join(watched[:5])
|
|
75
|
+
+ " require `uap schema-diff` to pass (within 1h). Run it and re-commit.",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""session-memory-write enforcer: code-changing sessions must write a lesson."""
|
|
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
|
+
END_OPS = {"session-end", "stop", "terminate", "SessionEnd"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def recent_lesson(root: Path) -> bool:
|
|
15
|
+
db = root / "agents" / "data" / "memory" / "short_term.db"
|
|
16
|
+
if not db.exists():
|
|
17
|
+
return False
|
|
18
|
+
try:
|
|
19
|
+
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True, timeout=1.0)
|
|
20
|
+
cur = con.execute(
|
|
21
|
+
"SELECT COUNT(*) FROM session_memories "
|
|
22
|
+
"WHERE type IN ('decision','lesson','pattern') "
|
|
23
|
+
"AND session_id='current'"
|
|
24
|
+
)
|
|
25
|
+
n = cur.fetchone()[0]
|
|
26
|
+
con.close()
|
|
27
|
+
return n > 0
|
|
28
|
+
except sqlite3.Error:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main() -> None:
|
|
33
|
+
op, args = parse_cli()
|
|
34
|
+
if op not in END_OPS:
|
|
35
|
+
emit(True, "not a session-end op")
|
|
36
|
+
|
|
37
|
+
code_changed = bool(args.get("code_changed"))
|
|
38
|
+
if not code_changed:
|
|
39
|
+
emit(True, "no code changes this session")
|
|
40
|
+
|
|
41
|
+
if recent_lesson(repo_root()):
|
|
42
|
+
emit(True, "lesson/decision recorded this session")
|
|
43
|
+
|
|
44
|
+
emit(
|
|
45
|
+
False,
|
|
46
|
+
"session-memory-write: code changed but no decision/lesson/pattern row in short_term.db. "
|
|
47
|
+
"Insert one before terminating.",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
main()
|