obey-cli 0.1.0__py3-none-any.whl
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.
- obey_cli/__init__.py +3 -0
- obey_cli/__main__.py +3 -0
- obey_cli/_data/demo/README.md +12 -0
- obey_cli/_data/demo/obey.yaml +287 -0
- obey_cli/_data/demo/repo/.gitignore +8 -0
- obey_cli/_data/demo/repo/AGENTS.md +38 -0
- obey_cli/_data/demo/repo/README.md +36 -0
- obey_cli/_data/demo/repo/eslint.config.js +24 -0
- obey_cli/_data/demo/repo/index.html +21 -0
- obey_cli/_data/demo/repo/openapi.yaml +86 -0
- obey_cli/_data/demo/repo/package.json +26 -0
- obey_cli/_data/demo/repo/pnpm-lock.yaml +1439 -0
- obey_cli/_data/demo/repo/scripts/gen-types.mjs +109 -0
- obey_cli/_data/demo/repo/src/api/client.test.ts +60 -0
- obey_cli/_data/demo/repo/src/api/client.ts +71 -0
- obey_cli/_data/demo/repo/src/api/response-guards.test.ts +21 -0
- obey_cli/_data/demo/repo/src/api/response-guards.ts +38 -0
- obey_cli/_data/demo/repo/src/date-utils.test.ts +51 -0
- obey_cli/_data/demo/repo/src/date-utils.ts +60 -0
- obey_cli/_data/demo/repo/src/generated/api-types.ts +35 -0
- obey_cli/_data/demo/repo/src/logger.test.ts +24 -0
- obey_cli/_data/demo/repo/src/logger.ts +36 -0
- obey_cli/_data/demo/repo/src/main.ts +30 -0
- obey_cli/_data/demo/repo/src/render-table.test.ts +34 -0
- obey_cli/_data/demo/repo/src/render-table.ts +48 -0
- obey_cli/_data/demo/repo/src/shift-planner.test.ts +66 -0
- obey_cli/_data/demo/repo/src/shift-planner.ts +101 -0
- obey_cli/_data/demo/repo/tsconfig.json +20 -0
- obey_cli/_data/demo/repo/vitest.config.ts +8 -0
- obey_cli/_data/demo/runs/after.jsonl.gz +0 -0
- obey_cli/_data/demo/runs/before.jsonl.gz +0 -0
- obey_cli/_data/demo/runs/meta.json +10 -0
- obey_cli/_data/schemas/obey-config-v1.json +116 -0
- obey_cli/_data/schemas/report-v1.json +55 -0
- obey_cli/_data/schemas/survey-record-v1.json +30 -0
- obey_cli/checkers/__init__.py +30 -0
- obey_cli/checkers/samples.py +95 -0
- obey_cli/cli.py +201 -0
- obey_cli/commands.py +676 -0
- obey_cli/config.py +90 -0
- obey_cli/discover.py +232 -0
- obey_cli/enforce/__init__.py +114 -0
- obey_cli/enforce/engine/__init__.py +0 -0
- obey_cli/enforce/engine/obey_engine.py +2005 -0
- obey_cli/enforce/merge.py +196 -0
- obey_cli/enforce/targets/__init__.py +354 -0
- obey_cli/enforce/templates/obey_hook.py +452 -0
- obey_cli/enforce/templates/pre_commit.py +116 -0
- obey_cli/extract.py +1342 -0
- obey_cli/lint.py +125 -0
- obey_cli/llm.py +129 -0
- obey_cli/parse_md.py +260 -0
- obey_cli/probes/__init__.py +0 -0
- obey_cli/probes/facts.py +162 -0
- obey_cli/probes/templates.py +326 -0
- obey_cli/py.typed +0 -0
- obey_cli/report/__init__.py +314 -0
- obey_cli/resources.py +35 -0
- obey_cli/rules.py +75 -0
- obey_cli/runner/__init__.py +0 -0
- obey_cli/runner/agents/__init__.py +22 -0
- obey_cli/runner/agents/base.py +96 -0
- obey_cli/runner/agents/claude_code.py +203 -0
- obey_cli/runner/agents/codex.py +152 -0
- obey_cli/runner/agents/copilot.py +142 -0
- obey_cli/runner/agents/gemini.py +125 -0
- obey_cli/runner/agents/opencode.py +156 -0
- obey_cli/runner/core.py +140 -0
- obey_cli/runner/egress_proxy.py +166 -0
- obey_cli/runner/gateway.py +529 -0
- obey_cli/runner/ledger.py +132 -0
- obey_cli/runner/local.py +176 -0
- obey_cli/runner/orchestrate.py +227 -0
- obey_cli/runner/sandbox/Dockerfile +41 -0
- obey_cli/runner/sandbox/entrypoint.sh +38 -0
- obey_cli/runner/sandbox.py +341 -0
- obey_cli/runner/sidecar.py +38 -0
- obey_cli/runner/workspace.py +187 -0
- obey_cli/score.py +150 -0
- obey_cli/study/__init__.py +0 -0
- obey_cli/study/analysis.py +277 -0
- obey_cli/study/design.py +98 -0
- obey_cli/study/experiment.py +198 -0
- obey_cli/study/report.py +519 -0
- obey_cli/study/survey.py +438 -0
- obey_cli-0.1.0.dist-info/METADATA +393 -0
- obey_cli-0.1.0.dist-info/RECORD +90 -0
- obey_cli-0.1.0.dist-info/WHEEL +4 -0
- obey_cli-0.1.0.dist-info/entry_points.txt +2 -0
- obey_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Run the phrasing experiment (pilot, main grid, enforcement) and keep per-run summaries.
|
|
2
|
+
|
|
3
|
+
Per-run summaries go to study/results/<phase>.jsonl (committed). Full run records with streams-derived events and
|
|
4
|
+
diffs go to study/results/raw/<phase>.jsonl (published as a release archive after a scan, never committed).
|
|
5
|
+
Interrupted batches resume: runs whose key is already in the summaries are skipped.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from collections.abc import Callable, Sequence
|
|
15
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from obey_cli.study import design as d
|
|
21
|
+
|
|
22
|
+
HARNESS_TARGET = {
|
|
23
|
+
"claude-code": "claude",
|
|
24
|
+
"opencode": "opencode",
|
|
25
|
+
"codex": "codex",
|
|
26
|
+
"copilot": "copilot",
|
|
27
|
+
"gemini": "gemini",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class StudyRun:
|
|
33
|
+
phase: str
|
|
34
|
+
cell: d.Cell
|
|
35
|
+
trial: int
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def key(self) -> str:
|
|
39
|
+
c = self.cell
|
|
40
|
+
return f"{self.phase}|{c.repo}|{c.slot}|{c.condition}|{c.harness}|{self.trial}"
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def run_id(self) -> str:
|
|
44
|
+
return f"{self.phase}-{hashlib.sha1(self.key.encode()).hexdigest()[:12]}" # noqa: S324 - an identifier
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def plan(design: dict[str, Any], phase: str, trials: int, **filters: Any) -> list[StudyRun]:
|
|
48
|
+
runs = [StudyRun(phase, cell, t) for cell in d.cells(design, **filters) for t in range(trials)]
|
|
49
|
+
# Interleave cells so an early stop leaves a balanced sample rather than whole missing conditions.
|
|
50
|
+
runs.sort(key=lambda r: (r.trial, hashlib.sha1(r.key.encode()).hexdigest())) # noqa: S324
|
|
51
|
+
return runs
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def done_keys(summaries: Path) -> set[str]:
|
|
55
|
+
if not summaries.exists():
|
|
56
|
+
return set()
|
|
57
|
+
keys = set()
|
|
58
|
+
for line in summaries.read_text(encoding="utf-8").splitlines():
|
|
59
|
+
if line.strip():
|
|
60
|
+
row = json.loads(line)
|
|
61
|
+
if row.get("status") == "ok":
|
|
62
|
+
keys.add(row["key"])
|
|
63
|
+
return keys
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def summarize(run: StudyRun, record: dict[str, Any], verdict: Any) -> dict[str, Any]:
|
|
67
|
+
denied = sum(1 for h in record.get("hook_log") or [] if h.get("decision") in ("deny", "block"))
|
|
68
|
+
return {
|
|
69
|
+
"key": run.key,
|
|
70
|
+
"run_id": record["run_id"],
|
|
71
|
+
"phase": run.phase,
|
|
72
|
+
"repo": run.cell.repo,
|
|
73
|
+
"slot": run.cell.slot,
|
|
74
|
+
"condition": run.cell.condition,
|
|
75
|
+
"harness": run.cell.harness,
|
|
76
|
+
"trial": run.trial,
|
|
77
|
+
"agent_version": record.get("agent_version"),
|
|
78
|
+
"model": record.get("model"),
|
|
79
|
+
"status": record["status"],
|
|
80
|
+
"verdict": verdict.verdict,
|
|
81
|
+
"evidence_kind": (verdict.evidence[0]["kind"] if verdict.evidence else None),
|
|
82
|
+
"denied": denied,
|
|
83
|
+
"cost_usd": record["usage"]["cost_usd"],
|
|
84
|
+
"input_tokens": record["usage"]["input_tokens"],
|
|
85
|
+
"output_tokens": record["usage"]["output_tokens"],
|
|
86
|
+
"duration_s": record["duration_s"],
|
|
87
|
+
"events": len(record.get("events") or []),
|
|
88
|
+
"commits": len(record.get("commits") or []),
|
|
89
|
+
"started_at": record.get("started_at"),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def execute(
|
|
94
|
+
design: dict[str, Any],
|
|
95
|
+
runs: Sequence[StudyRun],
|
|
96
|
+
*,
|
|
97
|
+
root: Path,
|
|
98
|
+
key: str,
|
|
99
|
+
image: str,
|
|
100
|
+
budget: float,
|
|
101
|
+
spent_before: float,
|
|
102
|
+
concurrency: int,
|
|
103
|
+
results: Path,
|
|
104
|
+
enforce_rules: bool = False,
|
|
105
|
+
progress: Callable[[str], None] = print,
|
|
106
|
+
) -> dict[str, Any]:
|
|
107
|
+
"""Run `runs` grouped by harness (one gateway batch each), appending summaries and raw records as they finish."""
|
|
108
|
+
from obey_cli.checkers import check
|
|
109
|
+
from obey_cli.commands import _enforcement_files
|
|
110
|
+
from obey_cli.runner import agents
|
|
111
|
+
from obey_cli.runner.core import RunSpec
|
|
112
|
+
from obey_cli.runner.core import execute as execute_run
|
|
113
|
+
from obey_cli.runner.sandbox import DockerSandbox
|
|
114
|
+
|
|
115
|
+
phase = runs[0].phase if runs else "none"
|
|
116
|
+
summaries = results / f"{phase}.jsonl"
|
|
117
|
+
raw = results / "raw" / f"{phase}.jsonl"
|
|
118
|
+
raw.parent.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
skip = done_keys(summaries)
|
|
120
|
+
todo = [r for r in runs if r.key not in skip]
|
|
121
|
+
progress(f"{phase}: {len(todo)} runs to do ({len(runs) - len(todo)} already done)")
|
|
122
|
+
box = DockerSandbox(image, cpus="2", memory="4g")
|
|
123
|
+
lock = threading.Lock()
|
|
124
|
+
spent = [spent_before]
|
|
125
|
+
stopped: list[str] = []
|
|
126
|
+
started = time.monotonic()
|
|
127
|
+
model = design["model"]
|
|
128
|
+
for harness in sorted({r.cell.harness for r in todo}):
|
|
129
|
+
adapter = agents.get(harness)
|
|
130
|
+
mine = [r for r in todo if r.cell.harness == harness]
|
|
131
|
+
batch = box.start_batch(
|
|
132
|
+
key=key,
|
|
133
|
+
model=model,
|
|
134
|
+
price={},
|
|
135
|
+
allow_hosts=design.get("allow_hosts") or ["registry.npmjs.org", "pypi.org", "files.pythonhosted.org"],
|
|
136
|
+
run_cap=0.5,
|
|
137
|
+
budget=None,
|
|
138
|
+
run_ids=[r.run_id for r in mine],
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def one(run: StudyRun, adapter: Any = adapter, batch: Any = batch, harness: str = harness) -> None:
|
|
142
|
+
if stopped:
|
|
143
|
+
return
|
|
144
|
+
with lock:
|
|
145
|
+
if spent[0] >= budget:
|
|
146
|
+
stopped.append(f"budget ${budget:.2f} reached (spent ${spent[0]:.2f})")
|
|
147
|
+
return
|
|
148
|
+
repo = design["repos"][run.cell.repo]
|
|
149
|
+
text, _ = d.render(repo, run.cell.slot, run.cell.condition)
|
|
150
|
+
rule = d.rule(design, run.cell.repo, run.cell.slot, run.cell.condition)
|
|
151
|
+
extra: dict[str, str] = {}
|
|
152
|
+
hooks: dict[str, str] = {}
|
|
153
|
+
if enforce_rules:
|
|
154
|
+
extra, hooks = _enforcement_files([rule], [HARNESS_TARGET[harness], "precommit"])
|
|
155
|
+
spec = RunSpec(
|
|
156
|
+
run_id=run.run_id,
|
|
157
|
+
prompt=rule["probes"][0]["instruction"],
|
|
158
|
+
source=root / repo["path"],
|
|
159
|
+
probe_id=rule["probes"][0]["id"],
|
|
160
|
+
rule_id=rule["id"],
|
|
161
|
+
arm="enforced" if enforce_rules else ("baseline" if run.cell.condition == "baseline" else "rule"),
|
|
162
|
+
condition=run.cell.condition,
|
|
163
|
+
trial=run.trial,
|
|
164
|
+
overrides={"AGENTS.md": text},
|
|
165
|
+
extra_files=extra,
|
|
166
|
+
git_hooks=hooks,
|
|
167
|
+
timeout_s=600,
|
|
168
|
+
)
|
|
169
|
+
record = execute_run(adapter, spec, box=box, batch=batch, model=model)
|
|
170
|
+
verdict = check(rule["checker"], record)
|
|
171
|
+
summary = summarize(run, record, verdict)
|
|
172
|
+
with lock:
|
|
173
|
+
spent[0] += record["usage"]["cost_usd"]
|
|
174
|
+
with summaries.open("a", encoding="utf-8") as fh:
|
|
175
|
+
fh.write(json.dumps(summary, sort_keys=True) + "\n")
|
|
176
|
+
with raw.open("a", encoding="utf-8") as fh:
|
|
177
|
+
fh.write(json.dumps({**record, "study_key": run.key}) + "\n")
|
|
178
|
+
elapsed = time.monotonic() - started
|
|
179
|
+
progress(
|
|
180
|
+
f"{summary['phase']} {harness:11s} {run.cell.repo:10s} {run.cell.slot:24s} "
|
|
181
|
+
f"{run.cell.condition:11s} t{run.trial} {summary['status']:7s} {summary['verdict']:13s} "
|
|
182
|
+
f"${summary['cost_usd']:.4f} total ${spent[0]:.2f} ({elapsed:.0f}s)"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
with ThreadPoolExecutor(max_workers=max(1, concurrency)) as pool:
|
|
187
|
+
for future in as_completed([pool.submit(one, r) for r in mine]):
|
|
188
|
+
future.result()
|
|
189
|
+
finally:
|
|
190
|
+
box.stop_batch(batch)
|
|
191
|
+
if stopped:
|
|
192
|
+
break
|
|
193
|
+
return {
|
|
194
|
+
"phase": phase,
|
|
195
|
+
"done": len(done_keys(summaries)),
|
|
196
|
+
"spent": spent[0],
|
|
197
|
+
"stopped": stopped[0] if stopped else None,
|
|
198
|
+
}
|