Skip to content

Commit 10cb928

Browse files
committed
chore: install repo-local dp runtime
1 parent 441fb0a commit 10cb928

2 files changed

Lines changed: 251 additions & 0 deletions

File tree

dp-policy.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"version": 1,
3+
"description": "Bootstrap governance pipelines for rlm-claude-code using currently stable local checks.",
4+
"pipelines": {
5+
"review": [
6+
{
7+
"name": "tooling-sanity",
8+
"cmd": ["uv", "run", "--extra", "dev", "pytest", "--version"]
9+
},
10+
{
11+
"name": "representative-unit-slice",
12+
"cmd": ["uv", "run", "--extra", "dev", "pytest", "-q", "tests/unit/test_repl_environment.py"]
13+
}
14+
],
15+
"verify": [
16+
{
17+
"name": "representative-unit-slice",
18+
"cmd": ["uv", "run", "--extra", "dev", "pytest", "-q", "tests/unit/test_repl_environment.py"]
19+
}
20+
],
21+
"pre-commit": [
22+
{
23+
"name": "representative-unit-slice",
24+
"cmd": ["uv", "run", "--extra", "dev", "pytest", "-q", "tests/unit/test_repl_environment.py"]
25+
}
26+
],
27+
"pre-push": [
28+
{
29+
"name": "representative-unit-slice",
30+
"cmd": ["uv", "run", "--extra", "dev", "pytest", "-q", "tests/unit/test_repl_environment.py"]
31+
}
32+
]
33+
}
34+
}

scripts/dp

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#!/usr/bin/env python3
2+
"""Repo-local dp governance runtime.
3+
4+
This wrapper provides deterministic `review`, `verify`, and `enforce`
5+
pipelines from `dp-policy.json` so governance commands do not depend on
6+
ambient global tooling.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import argparse
12+
import json
13+
import os
14+
import subprocess
15+
import sys
16+
import time
17+
from dataclasses import asdict, dataclass
18+
from pathlib import Path
19+
from typing import Any
20+
21+
22+
@dataclass
23+
class StepResult:
24+
name: str
25+
command: list[str]
26+
exit_code: int
27+
duration_ms: int
28+
stdout: str
29+
stderr: str
30+
31+
@property
32+
def ok(self) -> bool:
33+
return self.exit_code == 0
34+
35+
36+
def _repo_root() -> Path:
37+
return Path(__file__).resolve().parents[1]
38+
39+
40+
def _load_policy(path: Path) -> dict[str, Any]:
41+
if not path.exists():
42+
raise RuntimeError(
43+
f"Policy file not found: {path}. Create it or pass --policy with a valid path."
44+
)
45+
try:
46+
data = json.loads(path.read_text(encoding="utf-8"))
47+
except json.JSONDecodeError as exc:
48+
raise RuntimeError(f"Policy JSON parse failure at {path}: {exc}") from exc
49+
50+
pipelines = data.get("pipelines")
51+
if not isinstance(pipelines, dict):
52+
raise RuntimeError(
53+
f"Policy {path} is missing object key 'pipelines'."
54+
)
55+
return data
56+
57+
58+
def _normalize_step(step: Any) -> tuple[str, list[str]]:
59+
if isinstance(step, dict):
60+
name = str(step.get("name") or "unnamed-step")
61+
cmd = step.get("cmd")
62+
else:
63+
name = "step"
64+
cmd = step
65+
66+
if not isinstance(cmd, list) or not cmd or not all(isinstance(v, str) for v in cmd):
67+
raise RuntimeError(
68+
f"Invalid policy step {step!r}; expected list[str] in key 'cmd'."
69+
)
70+
return name, list(cmd)
71+
72+
73+
def _run_step(name: str, cmd: list[str], cwd: Path) -> StepResult:
74+
started = time.perf_counter()
75+
try:
76+
proc = subprocess.run(
77+
cmd,
78+
cwd=str(cwd),
79+
text=True,
80+
capture_output=True,
81+
check=False,
82+
)
83+
exit_code = proc.returncode
84+
stdout = proc.stdout
85+
stderr = proc.stderr
86+
except FileNotFoundError as exc:
87+
exit_code = 127
88+
stdout = ""
89+
stderr = (
90+
f"Command not found: {cmd[0]!r}. "
91+
"Ensure required toolchains are installed and available on PATH."
92+
)
93+
if exc.strerror:
94+
stderr = f"{stderr} ({exc.strerror})"
95+
96+
duration_ms = int((time.perf_counter() - started) * 1000)
97+
return StepResult(
98+
name=name,
99+
command=cmd,
100+
exit_code=exit_code,
101+
duration_ms=duration_ms,
102+
stdout=stdout,
103+
stderr=stderr,
104+
)
105+
106+
107+
def _run_pipeline(
108+
stage: str,
109+
policy_path: Path,
110+
json_output: bool,
111+
cwd: Path,
112+
) -> int:
113+
policy = _load_policy(policy_path)
114+
steps_raw = policy["pipelines"].get(stage)
115+
if not isinstance(steps_raw, list) or not steps_raw:
116+
raise RuntimeError(
117+
f"No pipeline steps configured for stage '{stage}' in {policy_path}."
118+
)
119+
120+
steps: list[StepResult] = []
121+
for raw in steps_raw:
122+
name, cmd = _normalize_step(raw)
123+
result = _run_step(name, cmd, cwd)
124+
steps.append(result)
125+
if not result.ok:
126+
break
127+
128+
ok = all(step.ok for step in steps) and len(steps) == len(steps_raw)
129+
payload = {
130+
"stage": stage,
131+
"policy": str(policy_path),
132+
"cwd": str(cwd),
133+
"ok": ok,
134+
"steps": [
135+
{
136+
**asdict(step),
137+
"ok": step.ok,
138+
}
139+
for step in steps
140+
],
141+
}
142+
143+
if json_output:
144+
print(json.dumps(payload, indent=2))
145+
else:
146+
print(f"dp {stage}: {'PASS' if ok else 'FAIL'}")
147+
for step in steps:
148+
cmd = " ".join(step.command)
149+
print(
150+
f"- {step.name}: exit={step.exit_code} duration_ms={step.duration_ms} cmd={cmd}"
151+
)
152+
if step.stdout.strip():
153+
print(step.stdout.rstrip())
154+
if step.stderr.strip():
155+
print(step.stderr.rstrip(), file=sys.stderr)
156+
157+
if ok:
158+
return 0
159+
failed = next((step for step in steps if not step.ok), None)
160+
return failed.exit_code if failed is not None and failed.exit_code != 0 else 1
161+
162+
163+
def _parse_args(argv: list[str]) -> argparse.Namespace:
164+
parser = argparse.ArgumentParser(prog="dp")
165+
sub = parser.add_subparsers(dest="command", required=True)
166+
167+
review = sub.add_parser("review", help="Run review pipeline")
168+
review.add_argument("--policy", default="dp-policy.json")
169+
review.add_argument("--json", action="store_true")
170+
171+
verify = sub.add_parser("verify", help="Run verify pipeline")
172+
verify.add_argument("--policy", default="dp-policy.json")
173+
verify.add_argument("--json", action="store_true")
174+
175+
enforce = sub.add_parser("enforce", help="Run enforcement pipeline")
176+
enforce.add_argument("stage", choices=["pre-commit", "pre-push"])
177+
enforce.add_argument("--policy", default="dp-policy.json")
178+
enforce.add_argument("--json", action="store_true")
179+
180+
return parser.parse_args(argv)
181+
182+
183+
def main(argv: list[str] | None = None) -> int:
184+
args = _parse_args(argv or sys.argv[1:])
185+
repo = _repo_root()
186+
cwd = Path(os.environ.get("LOOP_REPO_ROOT", str(repo))).resolve()
187+
policy = Path(args.policy)
188+
if not policy.is_absolute():
189+
policy = cwd / policy
190+
191+
try:
192+
if args.command == "review":
193+
return _run_pipeline("review", policy, args.json, cwd)
194+
if args.command == "verify":
195+
return _run_pipeline("verify", policy, args.json, cwd)
196+
if args.command == "enforce":
197+
return _run_pipeline(args.stage, policy, args.json, cwd)
198+
raise RuntimeError(f"Unsupported command: {args.command}")
199+
except RuntimeError as exc:
200+
if getattr(args, "json", False):
201+
print(
202+
json.dumps(
203+
{
204+
"ok": False,
205+
"error": str(exc),
206+
"command": args.command,
207+
},
208+
indent=2,
209+
)
210+
)
211+
else:
212+
print(f"dp error: {exc}", file=sys.stderr)
213+
return 2
214+
215+
216+
if __name__ == "__main__":
217+
raise SystemExit(main())

0 commit comments

Comments
 (0)