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.
Files changed (90) hide show
  1. obey_cli/__init__.py +3 -0
  2. obey_cli/__main__.py +3 -0
  3. obey_cli/_data/demo/README.md +12 -0
  4. obey_cli/_data/demo/obey.yaml +287 -0
  5. obey_cli/_data/demo/repo/.gitignore +8 -0
  6. obey_cli/_data/demo/repo/AGENTS.md +38 -0
  7. obey_cli/_data/demo/repo/README.md +36 -0
  8. obey_cli/_data/demo/repo/eslint.config.js +24 -0
  9. obey_cli/_data/demo/repo/index.html +21 -0
  10. obey_cli/_data/demo/repo/openapi.yaml +86 -0
  11. obey_cli/_data/demo/repo/package.json +26 -0
  12. obey_cli/_data/demo/repo/pnpm-lock.yaml +1439 -0
  13. obey_cli/_data/demo/repo/scripts/gen-types.mjs +109 -0
  14. obey_cli/_data/demo/repo/src/api/client.test.ts +60 -0
  15. obey_cli/_data/demo/repo/src/api/client.ts +71 -0
  16. obey_cli/_data/demo/repo/src/api/response-guards.test.ts +21 -0
  17. obey_cli/_data/demo/repo/src/api/response-guards.ts +38 -0
  18. obey_cli/_data/demo/repo/src/date-utils.test.ts +51 -0
  19. obey_cli/_data/demo/repo/src/date-utils.ts +60 -0
  20. obey_cli/_data/demo/repo/src/generated/api-types.ts +35 -0
  21. obey_cli/_data/demo/repo/src/logger.test.ts +24 -0
  22. obey_cli/_data/demo/repo/src/logger.ts +36 -0
  23. obey_cli/_data/demo/repo/src/main.ts +30 -0
  24. obey_cli/_data/demo/repo/src/render-table.test.ts +34 -0
  25. obey_cli/_data/demo/repo/src/render-table.ts +48 -0
  26. obey_cli/_data/demo/repo/src/shift-planner.test.ts +66 -0
  27. obey_cli/_data/demo/repo/src/shift-planner.ts +101 -0
  28. obey_cli/_data/demo/repo/tsconfig.json +20 -0
  29. obey_cli/_data/demo/repo/vitest.config.ts +8 -0
  30. obey_cli/_data/demo/runs/after.jsonl.gz +0 -0
  31. obey_cli/_data/demo/runs/before.jsonl.gz +0 -0
  32. obey_cli/_data/demo/runs/meta.json +10 -0
  33. obey_cli/_data/schemas/obey-config-v1.json +116 -0
  34. obey_cli/_data/schemas/report-v1.json +55 -0
  35. obey_cli/_data/schemas/survey-record-v1.json +30 -0
  36. obey_cli/checkers/__init__.py +30 -0
  37. obey_cli/checkers/samples.py +95 -0
  38. obey_cli/cli.py +201 -0
  39. obey_cli/commands.py +676 -0
  40. obey_cli/config.py +90 -0
  41. obey_cli/discover.py +232 -0
  42. obey_cli/enforce/__init__.py +114 -0
  43. obey_cli/enforce/engine/__init__.py +0 -0
  44. obey_cli/enforce/engine/obey_engine.py +2005 -0
  45. obey_cli/enforce/merge.py +196 -0
  46. obey_cli/enforce/targets/__init__.py +354 -0
  47. obey_cli/enforce/templates/obey_hook.py +452 -0
  48. obey_cli/enforce/templates/pre_commit.py +116 -0
  49. obey_cli/extract.py +1342 -0
  50. obey_cli/lint.py +125 -0
  51. obey_cli/llm.py +129 -0
  52. obey_cli/parse_md.py +260 -0
  53. obey_cli/probes/__init__.py +0 -0
  54. obey_cli/probes/facts.py +162 -0
  55. obey_cli/probes/templates.py +326 -0
  56. obey_cli/py.typed +0 -0
  57. obey_cli/report/__init__.py +314 -0
  58. obey_cli/resources.py +35 -0
  59. obey_cli/rules.py +75 -0
  60. obey_cli/runner/__init__.py +0 -0
  61. obey_cli/runner/agents/__init__.py +22 -0
  62. obey_cli/runner/agents/base.py +96 -0
  63. obey_cli/runner/agents/claude_code.py +203 -0
  64. obey_cli/runner/agents/codex.py +152 -0
  65. obey_cli/runner/agents/copilot.py +142 -0
  66. obey_cli/runner/agents/gemini.py +125 -0
  67. obey_cli/runner/agents/opencode.py +156 -0
  68. obey_cli/runner/core.py +140 -0
  69. obey_cli/runner/egress_proxy.py +166 -0
  70. obey_cli/runner/gateway.py +529 -0
  71. obey_cli/runner/ledger.py +132 -0
  72. obey_cli/runner/local.py +176 -0
  73. obey_cli/runner/orchestrate.py +227 -0
  74. obey_cli/runner/sandbox/Dockerfile +41 -0
  75. obey_cli/runner/sandbox/entrypoint.sh +38 -0
  76. obey_cli/runner/sandbox.py +341 -0
  77. obey_cli/runner/sidecar.py +38 -0
  78. obey_cli/runner/workspace.py +187 -0
  79. obey_cli/score.py +150 -0
  80. obey_cli/study/__init__.py +0 -0
  81. obey_cli/study/analysis.py +277 -0
  82. obey_cli/study/design.py +98 -0
  83. obey_cli/study/experiment.py +198 -0
  84. obey_cli/study/report.py +519 -0
  85. obey_cli/study/survey.py +438 -0
  86. obey_cli-0.1.0.dist-info/METADATA +393 -0
  87. obey_cli-0.1.0.dist-info/RECORD +90 -0
  88. obey_cli-0.1.0.dist-info/WHEEL +4 -0
  89. obey_cli-0.1.0.dist-info/entry_points.txt +2 -0
  90. obey_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
obey_cli/cli.py ADDED
@@ -0,0 +1,201 @@
1
+ """Command-line entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ from collections.abc import Callable, Sequence
9
+ from pathlib import Path
10
+
11
+ from obey_cli import __version__
12
+ from obey_cli.config import CONFIG_NAME, ConfigError
13
+
14
+ Handler = Callable[[argparse.Namespace], int]
15
+
16
+
17
+ def _out(text: str) -> None:
18
+ print(text, flush=True)
19
+
20
+
21
+ def _err(text: str) -> None:
22
+ print(f"obey: {text}", file=sys.stderr, flush=True)
23
+
24
+
25
+ def cmd_extract(args: argparse.Namespace) -> int:
26
+ from obey_cli import config as cfg
27
+ from obey_cli.rules import build_config, counts
28
+
29
+ root = Path(args.repo).resolve()
30
+ path = root / args.config
31
+ previous = cfg.load(path) if path.exists() else None
32
+ proposals = []
33
+ if args.llm:
34
+ from obey_cli.discover import discover
35
+ from obey_cli.extract import extract, llm_propose
36
+ from obey_cli.llm import LLMConfig
37
+ from obey_cli.runner.ledger import Ledger
38
+
39
+ ledger = Ledger(root / ".obey" / "ledger.jsonl", budget_usd=args.budget)
40
+ proposals = llm_propose(
41
+ extract(discover(root, args.include_local), root), LLMConfig.from_env(args.llm_model), ledger
42
+ )
43
+ config, files = build_config(root, previous, args.include_local, proposals)
44
+ if not files:
45
+ _err(f"no agent instruction files found under {root}")
46
+ return 1
47
+ if args.dry_run:
48
+ _out(cfg.dump(config))
49
+ else:
50
+ cfg.save(path, config)
51
+ c = counts(config["rules"])
52
+ _out(
53
+ f"{len(files)} instruction file(s), {len(config['rules'])} rules: {c.get('active', 0)} active, "
54
+ f"{c.get('uncheckable', 0)} uncheckable, {c.get('proposed', 0)} proposed"
55
+ + ("" if args.dry_run else f" -> {path.relative_to(root) if path.is_relative_to(root) else path}")
56
+ )
57
+ if c.get("proposed"):
58
+ _out("Review the proposed rules and set `status: active` on the ones you want measured.")
59
+ return 0
60
+
61
+
62
+ def build_parser() -> argparse.ArgumentParser:
63
+ parser = argparse.ArgumentParser(
64
+ prog="obey",
65
+ description="Unit tests for AGENTS.md: measure which rules your coding agent follows, then enforce them.",
66
+ )
67
+ parser.add_argument("--version", action="version", version=f"obey {__version__}")
68
+ sub = parser.add_subparsers(dest="command", metavar="<command>")
69
+
70
+ def add(name: str, help_text: str, handler: Handler) -> argparse.ArgumentParser:
71
+ p = sub.add_parser(name, help=help_text, description=help_text)
72
+ p.set_defaults(handler=handler)
73
+ p.add_argument("--repo", default=".", help="repository root (default: current directory)")
74
+ p.add_argument(
75
+ "--config", default=CONFIG_NAME, help="config file relative to the repository (default: obey.yaml)"
76
+ )
77
+ return p
78
+
79
+ p = add("extract", "turn instruction files into checkable rules in obey.yaml", cmd_extract)
80
+ p.add_argument("--llm", action="store_true", help="propose checkers for unmapped rules with an LLM (reviewed)")
81
+ p.add_argument("--llm-model", default=None, help="model for --llm (default: $OBEY_LLM_MODEL)")
82
+ p.add_argument("--budget", type=float, default=1.0, help="USD cap for --llm calls (default: 1.0)")
83
+ p.add_argument("--include-local", action="store_true", help="also read CLAUDE.local.md")
84
+ p.add_argument("--dry-run", action="store_true", help="print obey.yaml instead of writing it")
85
+
86
+ from obey_cli import commands as c
87
+
88
+ p = add("probe", "create a tempting task for each active rule (written into obey.yaml)", c.cmd_probe)
89
+ p.add_argument("--regenerate", action="store_true", help="replace existing probes")
90
+
91
+ p = add("run", "run your coding agent on the probes in a sandbox and score each rule", c.cmd_run)
92
+ p.add_argument("--agent", default=None, help="claude-code | codex | opencode | copilot | gemini")
93
+ p.add_argument("--model", default=None, help="model id at your provider, e.g. anthropic/claude-haiku-5.5")
94
+ p.add_argument("--trials", type=int, default=5, help="runs per probe (default: 5)")
95
+ p.add_argument("--baseline-trials", type=int, default=3, help="runs per probe with the rule removed (default: 3)")
96
+ p.add_argument("--no-baseline", action="store_true", help="skip the no-rule baseline arm")
97
+ p.add_argument("--rules", default=None, help="comma-separated rule ids (default: all active rules with probes)")
98
+ p.add_argument("--budget", type=float, default=5.0, help="USD cap enforced by the ledger (default: 5)")
99
+ p.add_argument("--run-cap", type=float, default=0.5, help="USD cap per run, enforced by the gateway (default: 0.5)")
100
+ p.add_argument("--estimate", action="store_true", help="print runs × estimated cost and exit")
101
+ p.add_argument(
102
+ "--compare",
103
+ default=None,
104
+ metavar="BEFORE.json",
105
+ help="re-run broken rules with enforcement on and report before/after and evasions",
106
+ )
107
+ p.add_argument(
108
+ "--targets",
109
+ default="claude,codex,cursor,gemini,copilot,opencode,precommit",
110
+ help="enforcement targets for --compare",
111
+ )
112
+ p.add_argument("--concurrency", type=int, default=4)
113
+ p.add_argument(
114
+ "--image", default=None, help="sandbox image (default: sandbox.image in obey.yaml, then obey-sandbox:0.1.0)"
115
+ )
116
+ p.add_argument("--unsafe-local", action="store_true", help="run without a container (see the README first)")
117
+ p.add_argument("--out", default=None, help="output directory (default: .obey/runs/<timestamp>)")
118
+ p.add_argument("--offline-price", action="store_true", help="do not fetch model prices for --estimate")
119
+
120
+ p = add("report", "re-score recorded runs with the current checkers and render the report", c.cmd_report)
121
+ p.add_argument("runs", help="runs.jsonl written by `obey run`")
122
+ p.add_argument("--format", choices=["files", "html", "md", "json"], default="files")
123
+
124
+ p = add("enforce", "compile rules into hooks for your agents and a git pre-commit check", c.cmd_enforce)
125
+ p.add_argument(
126
+ "--targets",
127
+ default="claude,codex,cursor,gemini,copilot,opencode,precommit",
128
+ help="comma-separated: claude, codex, cursor, gemini, copilot, opencode, precommit",
129
+ )
130
+ p.add_argument(
131
+ "--from",
132
+ dest="from_report",
133
+ default=None,
134
+ metavar="REPORT.json",
135
+ help="only rules broken in this report (for example .obey/report.json)",
136
+ )
137
+ p.add_argument("--rules", default=None, help="comma-separated rule ids")
138
+ p.add_argument("--dry-run", action="store_true", help="print the diff and write nothing")
139
+ p.add_argument("--remove", action="store_true", help="remove obey's entries and files")
140
+
141
+ p = add("lint", "check obey.yaml, instruction-file hashes and enforcement sync; write the badge", c.cmd_lint)
142
+ p.add_argument("--no-badge", action="store_true", help="check for obey-badge.json without rewriting it")
143
+
144
+ p = add("doctor", "check git, Docker, the sandbox image, the agent CLIs and the API key", c.cmd_doctor)
145
+ p.add_argument("--image", default=None)
146
+ p.add_argument("--build-image", action="store_true", help="build the default sandbox image (a few GB)")
147
+
148
+ p = add("init", "extract rules and create probes in one step", c.cmd_init)
149
+
150
+ p = add("demo", "replay recorded runs on the bundled demo repository (no API key, no Docker)", c.cmd_demo)
151
+ p.add_argument("--html", default=None, metavar="PATH", help="also write the HTML report here")
152
+
153
+ p = add("study", "reproduce the study: survey, run (pilot, main, enforce), plan, report", c.cmd_study)
154
+ p.add_argument("study_command", choices=["survey", "run", "plan", "report"])
155
+ p.add_argument("--collect", action="store_true", help="survey: sample repositories (needs GITHUB_TOKEN)")
156
+ p.add_argument("--analyse", action="store_true", help="survey: compute features at the pinned commits")
157
+ p.add_argument("--llm", action="store_true", help="survey: classify unmapped rules with an LLM")
158
+ p.add_argument("--llm-model", default=None)
159
+ p.add_argument("--target", type=int, default=600)
160
+ p.add_argument("--phase", choices=["pilot", "main", "enforce"], default="pilot")
161
+ p.add_argument("--trials", type=int, default=1)
162
+ p.add_argument("--budget", type=float, default=3.0)
163
+ p.add_argument("--concurrency", type=int, default=6)
164
+ p.add_argument("--image", default=None)
165
+ p.add_argument("--repos", default=None)
166
+ p.add_argument("--slots", default=None)
167
+ p.add_argument("--harnesses", default=None)
168
+ p.add_argument("--estimate", action="store_true")
169
+ p.add_argument(
170
+ "--from-condition",
171
+ choices=["imperative", "prohibition", "reason", "baseline"],
172
+ default="imperative",
173
+ help="enforce: re-run the cells violated in this condition, in this condition, with hooks",
174
+ )
175
+ p.add_argument("--verdicts", default=None, help="enforce: main-grid summaries that select the cells")
176
+
177
+ p = add("precommit", "run .obey/pre-commit (for the pre-commit framework)", c.cmd_precommit)
178
+ p.add_argument("--commit-msg", action="store_true", help="check the commit message file instead of staged files")
179
+ p.add_argument("files", nargs="*")
180
+ return parser
181
+
182
+
183
+ def main(argv: Sequence[str] | None = None) -> int:
184
+ parser = build_parser()
185
+ args = parser.parse_args(argv)
186
+ if args.command is None:
187
+ parser.print_help(sys.stderr)
188
+ return 2
189
+ try:
190
+ code: int = args.handler(args)
191
+ return code
192
+ except ConfigError as exc:
193
+ _err(str(exc))
194
+ return 1
195
+ except KeyboardInterrupt:
196
+ _err("interrupted")
197
+ return 130
198
+ except BrokenPipeError:
199
+ # `obey enforce --dry-run | head`: the reader went away; stop quietly, as other command-line tools do.
200
+ os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
201
+ return 141