data-olympus 0.3.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 (73) hide show
  1. data_olympus/__init__.py +14 -0
  2. data_olympus/_bin/_kb_detect_workspace.sh +57 -0
  3. data_olympus/_bin/_kb_enforce.py +619 -0
  4. data_olympus/_bin/_kb_fallback.py +361 -0
  5. data_olympus/_bin/kb +957 -0
  6. data_olympus/_bin/kb-enforce-hook +337 -0
  7. data_olympus/_bin/opencode/data-olympus-gate.ts +102 -0
  8. data_olympus/audit_log.py +275 -0
  9. data_olympus/audit_trailers.py +42 -0
  10. data_olympus/auth.py +139 -0
  11. data_olympus/cli/__init__.py +1 -0
  12. data_olympus/cli/import_cmd.py +115 -0
  13. data_olympus/cli/indexgen.py +60 -0
  14. data_olympus/cli/main.py +151 -0
  15. data_olympus/cli/report_cmd.py +181 -0
  16. data_olympus/config.py +261 -0
  17. data_olympus/cooccurrence.py +393 -0
  18. data_olympus/dedup.py +57 -0
  19. data_olympus/durable.py +51 -0
  20. data_olympus/embeddings.py +317 -0
  21. data_olympus/enforce_policy.py +297 -0
  22. data_olympus/format/__init__.py +16 -0
  23. data_olympus/format/document.py +39 -0
  24. data_olympus/format/frontmatter.py +35 -0
  25. data_olympus/format/lint.py +92 -0
  26. data_olympus/format/validate.py +71 -0
  27. data_olympus/git_ops.py +397 -0
  28. data_olympus/health.py +114 -0
  29. data_olympus/importer/__init__.py +13 -0
  30. data_olympus/importer/adr.py +286 -0
  31. data_olympus/importer/flat.py +170 -0
  32. data_olympus/importer/model.py +106 -0
  33. data_olympus/importer/okf.py +192 -0
  34. data_olympus/importer/run.py +416 -0
  35. data_olympus/importer/stamp.py +227 -0
  36. data_olympus/index.py +1745 -0
  37. data_olympus/markdown_parse.py +103 -0
  38. data_olympus/models.py +480 -0
  39. data_olympus/onboarding.py +131 -0
  40. data_olympus/onboarding_inflight.py +137 -0
  41. data_olympus/onboarding_playbook.py +99 -0
  42. data_olympus/pending.py +533 -0
  43. data_olympus/principals.py +168 -0
  44. data_olympus/prompts.py +35 -0
  45. data_olympus/push_queue.py +261 -0
  46. data_olympus/query_expansion.py +200 -0
  47. data_olympus/rate_limit.py +81 -0
  48. data_olympus/refresh.py +329 -0
  49. data_olympus/report.py +133 -0
  50. data_olympus/rest_api.py +845 -0
  51. data_olympus/safe_id.py +36 -0
  52. data_olympus/search_gate.py +64 -0
  53. data_olympus/search_shortcut.py +146 -0
  54. data_olympus/server.py +1115 -0
  55. data_olympus/session_metrics.py +303 -0
  56. data_olympus/setup_wizard.py +751 -0
  57. data_olympus/thin_pointer.py +20 -0
  58. data_olympus/tools_audit.py +41 -0
  59. data_olympus/tools_enforce.py +198 -0
  60. data_olympus/tools_onboarding.py +585 -0
  61. data_olympus/tools_read.py +230 -0
  62. data_olympus/tools_write.py +878 -0
  63. data_olympus/trigram.py +126 -0
  64. data_olympus/viewer/__init__.py +1 -0
  65. data_olympus/viewer/generator.py +375 -0
  66. data_olympus/worktrees.py +147 -0
  67. data_olympus/write_gate.py +382 -0
  68. data_olympus-0.3.0.dist-info/METADATA +97 -0
  69. data_olympus-0.3.0.dist-info/RECORD +73 -0
  70. data_olympus-0.3.0.dist-info/WHEEL +4 -0
  71. data_olympus-0.3.0.dist-info/entry_points.txt +3 -0
  72. data_olympus-0.3.0.dist-info/licenses/LICENSE +202 -0
  73. data_olympus-0.3.0.dist-info/licenses/NOTICE +8 -0
@@ -0,0 +1,14 @@
1
+ """data-olympus: governance-grade knowledge-base format, CLI, and MCP server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError
6
+ from importlib.metadata import version as _pkg_version
7
+
8
+ try:
9
+ # Single source of truth: the installed distribution's version (declared in
10
+ # pyproject [project].version). Reading it here keeps `data_olympus.__version__`
11
+ # from drifting out of sync with the packaging metadata the release chain tags on.
12
+ __version__ = _pkg_version("data-olympus")
13
+ except PackageNotFoundError: # pragma: no cover - only when running from a raw tree
14
+ __version__ = "0.0.0+unknown"
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # Sourced by: hooks (agent-hooks/*) and bin/kb onboarding-check.
3
+ # Resolves CWD to (WORKSPACE, COMPONENT?, WORKSPACE_REMOTE_URL?, COMPONENT_REMOTE_URL?)
4
+ # via upward path traversal.
5
+
6
+ detect_workspace_and_component() {
7
+ local start="${1:-$PWD}"
8
+ local cwd
9
+ cwd=$(cd "$start" && pwd -P) || return 1
10
+
11
+ # Walk up until we find a directory directly under the workspaces root.
12
+ # The root is configurable via KB_WORKSPACES_ROOT (default: ~/projects).
13
+ local workspaces_root="${KB_WORKSPACES_ROOT:-$HOME/projects}"
14
+ local workspace_root=""
15
+ local cur="$cwd"
16
+ while [ "$cur" != "/" ]; do
17
+ if [ "$(dirname "$cur")" = "$workspaces_root" ]; then
18
+ workspace_root="$cur"
19
+ break
20
+ fi
21
+ cur=$(dirname "$cur")
22
+ done
23
+
24
+ if [ -z "$workspace_root" ]; then
25
+ return 1
26
+ fi
27
+
28
+ WORKSPACE=$(basename "$workspace_root")
29
+ WORKSPACE_REMOTE_URL=""
30
+ COMPONENT=""
31
+ COMPONENT_REMOTE_URL=""
32
+
33
+ if [ -d "$workspace_root/.git" ]; then
34
+ WORKSPACE_REMOTE_URL=$(git -C "$workspace_root" remote get-url origin 2>/dev/null || true)
35
+ fi
36
+
37
+ if [ "$cwd" != "$workspace_root" ]; then
38
+ local rel="${cwd#$workspace_root/}"
39
+ local probe="$workspace_root"
40
+ local seg
41
+ local IFS_save="$IFS"
42
+ IFS=/
43
+ for seg in $rel; do
44
+ IFS="$IFS_save"
45
+ probe="$probe/$seg"
46
+ if [ -d "$probe/.git" ]; then
47
+ COMPONENT=$(basename "$probe")
48
+ COMPONENT_REMOTE_URL=$(git -C "$probe" remote get-url origin 2>/dev/null || true)
49
+ break
50
+ fi
51
+ done
52
+ IFS="$IFS_save"
53
+ fi
54
+
55
+ export WORKSPACE COMPONENT WORKSPACE_REMOTE_URL COMPONENT_REMOTE_URL
56
+ return 0
57
+ }
@@ -0,0 +1,619 @@
1
+ #!/usr/bin/env python3
2
+ """kb enforce installer: per-agent providers for the data-olympus enforcement gate.
3
+
4
+ Each provider idempotently installs/removes MARKER-tagged enforcement wiring for
5
+ one coding agent, backs up before editing, and reports status/doctor. Subcommands:
6
+ install | uninstall | status | doctor. Select an agent with --agent.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import os
13
+ import shutil
14
+ import sys
15
+ import time
16
+ import urllib.request
17
+ from pathlib import Path
18
+
19
+ MARKER = "data-olympus-enforce"
20
+ SHIM_VERSION = "1"
21
+ HOOK_BIN = str(Path(__file__).resolve().parent / "kb-enforce-hook")
22
+ PLUGIN_SRC = Path(__file__).resolve().parent / "opencode" / "data-olympus-gate.ts"
23
+ PLUGIN_NAME = "data-olympus-gate.ts"
24
+
25
+
26
+ def _home() -> str:
27
+ override = os.getenv("KB_ENFORCE_HOME")
28
+ return override if override else os.path.expanduser("~")
29
+
30
+
31
+ def _backup(target: Path) -> None:
32
+ if target.exists():
33
+ ts = time.strftime("%Y%m%d-%H%M%S")
34
+ shutil.copy2(target, target.with_suffix(target.suffix + f".kb-bak-{ts}"))
35
+
36
+
37
+ def _load_json(target: Path) -> dict:
38
+ if target.exists() and target.read_text().strip():
39
+ return json.loads(target.read_text())
40
+ return {}
41
+
42
+
43
+ def _doctor_endpoint() -> tuple[bool, str]:
44
+ endpoint = os.getenv("KB_ENDPOINT", "http://localhost:8080")
45
+ try:
46
+ with urllib.request.urlopen(f"{endpoint}/api/v1/health", timeout=5) as r:
47
+ ok = r.status == 200
48
+ except Exception as exc: # noqa: BLE001 - report any failure
49
+ return False, f"cannot reach {endpoint}: {exc}"
50
+ return ok, f"endpoint {endpoint} reachable={ok}"
51
+
52
+
53
+ def _hook_bin_in_worktree(hook_bin: str) -> bool:
54
+ """True when the dispatcher path resolves inside a git worktree checkout
55
+ (a `.worktrees/` or `.claude/worktrees/` segment). An install performed from
56
+ such a checkout dangles after the worktree is pruned and then silently fails
57
+ open, so doctor warns about it."""
58
+ parts = Path(hook_bin).resolve().parts
59
+ if ".worktrees" in parts:
60
+ return True
61
+ # `.claude/worktrees/<...>`: a `.claude` segment immediately followed by
62
+ # `worktrees`.
63
+ return any(
64
+ parts[i] == ".claude" and parts[i + 1] == "worktrees"
65
+ for i in range(len(parts) - 1)
66
+ )
67
+
68
+
69
+ def _doctor_hook_bin() -> tuple[bool, list[str]]:
70
+ """Verify the installed hook dispatcher exists and is executable, and warn
71
+ when it resolves inside a worktree. Returns (ok, messages)."""
72
+ msgs: list[str] = []
73
+ exists = os.path.isfile(HOOK_BIN)
74
+ executable = exists and os.access(HOOK_BIN, os.X_OK)
75
+ if not exists:
76
+ msgs.append(f"hook command MISSING at {HOOK_BIN}")
77
+ elif not executable:
78
+ msgs.append(f"hook command present but NOT executable at {HOOK_BIN}")
79
+ else:
80
+ msgs.append(f"hook command present and executable at {HOOK_BIN}")
81
+ if _hook_bin_in_worktree(HOOK_BIN):
82
+ msgs.append(
83
+ f"WARNING: hook command resolves inside a worktree ({HOOK_BIN}); this "
84
+ "install will dangle and silently fail open once the worktree is "
85
+ "pruned. Re-run `kb enforce install` from the main checkout."
86
+ )
87
+ return executable, msgs
88
+
89
+
90
+ def _managed_versions_in_hooks(data: dict) -> set:
91
+ """The set of managed-marker versions present in a JSON hooks map."""
92
+ return {
93
+ h[MARKER]
94
+ for blocks in data.get("hooks", {}).values()
95
+ for block in blocks
96
+ for h in block.get("hooks", [])
97
+ if MARKER in h
98
+ }
99
+
100
+
101
+ class HookFileProvider:
102
+ """A provider that writes MARKER-tagged hook entries into a JSON hooks map
103
+ inside a target file (the map lives under the top-level 'hooks' key).
104
+
105
+ events: list of (event_name, dispatcher_mode, matcher_or_None).
106
+ dialect: passed to kb-enforce-hook as '--dialect <dialect>'; omitted when
107
+ 'claude' so Claude's slice-1 command form ('<hook> <mode>') is preserved.
108
+ """
109
+
110
+ tier = "hard"
111
+
112
+ def __init__(self, name: str, default_target: Path, events: list,
113
+ dialect: str = "claude", note: str = "") -> None:
114
+ self.name = name
115
+ self._default_target = default_target
116
+ self._events = events
117
+ self._dialect = dialect
118
+ self._note = note
119
+ self.enforce_mode = "hard"
120
+
121
+ def default_target(self) -> Path:
122
+ return self._default_target
123
+
124
+ def _command(self, mode: str) -> str:
125
+ # Always thread --agent so the consult audit records the correct
126
+ # per-agent identity. --dialect is still suppressed for claude (default)
127
+ # to keep its slice-1 command form.
128
+ dialect = "" if self._dialect == "claude" else f" --dialect {self._dialect}"
129
+ return f"{HOOK_BIN} {mode}{dialect} --agent {self.name}"
130
+
131
+ def _managed_block(self, mode: str, matcher: str | None) -> dict:
132
+ entry = {"type": "command", "command": self._command(mode), MARKER: SHIM_VERSION}
133
+ block: dict = {"hooks": [entry]}
134
+ if matcher is not None:
135
+ block["matcher"] = matcher
136
+ return block
137
+
138
+ @staticmethod
139
+ def _strip_managed(hooks: dict) -> dict:
140
+ out: dict = {}
141
+ for event, blocks in hooks.items():
142
+ kept = []
143
+ for block in blocks:
144
+ kh = [h for h in block.get("hooks", []) if MARKER not in h]
145
+ if kh:
146
+ nb = dict(block)
147
+ nb["hooks"] = kh
148
+ kept.append(nb)
149
+ if kept:
150
+ out[event] = kept
151
+ return out
152
+
153
+ def install(self, target: Path) -> int:
154
+ data = _load_json(target)
155
+ _backup(target)
156
+ hooks = self._strip_managed(data.get("hooks", {}))
157
+ events = self._events
158
+ if self.enforce_mode == "soft":
159
+ events = [(e, m, mt) for (e, m, mt) in self._events if m != "pre-tool"]
160
+ for event, mode, matcher in events:
161
+ hooks.setdefault(event, []).append(self._managed_block(mode, matcher))
162
+ data["hooks"] = hooks
163
+ target.parent.mkdir(parents=True, exist_ok=True)
164
+ target.write_text(json.dumps(data, indent=2) + "\n")
165
+ print(
166
+ f"installed data-olympus enforcement (v{SHIM_VERSION}) into {target} "
167
+ f"[{self.name}, tier={self.tier}]"
168
+ )
169
+ if self._note:
170
+ print(self._note)
171
+ return 0
172
+
173
+ def uninstall(self, target: Path) -> int:
174
+ data = _load_json(target)
175
+ if "hooks" not in data:
176
+ print("nothing to uninstall")
177
+ return 0
178
+ _backup(target)
179
+ data["hooks"] = self._strip_managed(data["hooks"])
180
+ if not data["hooks"]:
181
+ del data["hooks"]
182
+ target.write_text(json.dumps(data, indent=2) + "\n")
183
+ print(f"uninstalled data-olympus enforcement from {target} [{self.name}]")
184
+ return 0
185
+
186
+ def status(self, target: Path) -> int:
187
+ data = _load_json(target)
188
+ versions = _managed_versions_in_hooks(data)
189
+ if not versions:
190
+ print(f"{self.name}: not installed")
191
+ return 0
192
+ stale = " (stale; run `kb enforce install`)" if SHIM_VERSION not in versions else ""
193
+ print(f"{self.name}: installed, tier={self.tier}, versions={sorted(versions)}{stale}")
194
+ return 0
195
+
196
+ def doctor(self, target: Path) -> int:
197
+ """Doctor now checks three things beyond endpoint reachability:
198
+ the managed marker/version is present in the LIVE settings file, the hook
199
+ dispatcher exists and is executable, and the dispatcher is not installed
200
+ from a worktree (which dangles after pruning and fails open)."""
201
+ ok_ep, msg_ep = _doctor_endpoint()
202
+ print(f"doctor [{self.name}]: {msg_ep}")
203
+
204
+ data = _load_json(target)
205
+ versions = _managed_versions_in_hooks(data)
206
+ if not versions:
207
+ print(f"doctor [{self.name}]: managed hook NOT installed in {target}")
208
+ ok_marker = False
209
+ elif SHIM_VERSION not in versions:
210
+ print(f"doctor [{self.name}]: managed hook in {target} is STALE "
211
+ f"(found {sorted(versions)}, want {SHIM_VERSION}); run "
212
+ "`kb enforce install`")
213
+ ok_marker = False
214
+ else:
215
+ print(f"doctor [{self.name}]: managed hook v{SHIM_VERSION} present in {target}")
216
+ ok_marker = True
217
+
218
+ ok_bin, bin_msgs = _doctor_hook_bin()
219
+ for m in bin_msgs:
220
+ print(f"doctor [{self.name}]: {m}")
221
+
222
+ # A worktree-installed hook is a real problem even if the file is currently
223
+ # present and executable, so fail doctor on it.
224
+ ok_worktree = not _hook_bin_in_worktree(HOOK_BIN)
225
+ return 0 if (ok_ep and ok_marker and ok_bin and ok_worktree) else 1
226
+
227
+
228
+ def _claude_provider() -> HookFileProvider:
229
+ return HookFileProvider(
230
+ name="claude-code",
231
+ default_target=Path(_home()) / ".claude" / "settings.json",
232
+ events=[
233
+ ("SessionStart", "session-start", None),
234
+ ("UserPromptSubmit", "user-prompt", None),
235
+ # Anchored: the matcher is a regex, so an un-anchored alternation
236
+ # substring-matches unrelated tools (e.g. "Bash" matches "BashOutput",
237
+ # "Edit" matches "NotebookEditOther"). ^(...)$ gates exactly these.
238
+ ("PreToolUse", "pre-tool", "^(Edit|Write|MultiEdit|NotebookEdit|Bash)$"),
239
+ ("Stop", "stop", None),
240
+ ],
241
+ dialect="claude",
242
+ )
243
+
244
+
245
+ CODEX_TRUST_NOTE = (
246
+ "NOTE (codex): Codex requires this hook to be trusted before it runs. On the "
247
+ "next `codex` start you will be prompted to trust it, or run codex with "
248
+ "`--dangerously-bypass-hook-trust` for vetted automation. The trust hash is "
249
+ "persisted under [hooks.state] in ~/.codex/config.toml."
250
+ )
251
+
252
+
253
+ def _codex_provider() -> HookFileProvider:
254
+ return HookFileProvider(
255
+ name="codex",
256
+ default_target=Path(_home()) / ".codex" / "hooks.json",
257
+ events=[
258
+ ("SessionStart", "session-start", None),
259
+ ("UserPromptSubmit", "user-prompt", None),
260
+ # Anchored (see the claude provider): keep the alternation exact so
261
+ # "Bash" does not also gate "BashOutput".
262
+ ("PreToolUse", "pre-tool", "^(Edit|Write|MultiEdit|Bash)$"),
263
+ ("Stop", "stop", None),
264
+ ],
265
+ dialect="claude", # Codex shares Claude's exit-2 deny contract
266
+ note=CODEX_TRUST_NOTE,
267
+ )
268
+
269
+
270
+ def _gemini_provider() -> HookFileProvider:
271
+ return HookFileProvider(
272
+ name="gemini",
273
+ default_target=Path(_home()) / ".gemini" / "settings.json",
274
+ events=[
275
+ ("SessionStart", "session-start", None),
276
+ ("BeforeAgent", "user-prompt", None), # BeforeAgent carries `prompt`
277
+ # Anchored to avoid substring matches against other Gemini tools.
278
+ ("BeforeTool", "pre-tool", "^(write_file|replace|run_shell_command)$"),
279
+ ("Stop", "stop", None),
280
+ ],
281
+ dialect="gemini",
282
+ )
283
+
284
+
285
+ class OpenCodeProvider:
286
+ """OpenCode provider. Unlike the hard shell-hook agents, OpenCode is wired by
287
+ dropping a managed TypeScript plugin file into the plugin directory. Install
288
+ copies the bundled template; uninstall removes it only if it still carries the
289
+ managed marker, so operator-authored plugins in the same dir are untouched.
290
+ """
291
+
292
+ name = "opencode"
293
+ tier = "hard"
294
+
295
+ def default_target(self) -> Path:
296
+ return Path(_home()) / ".config" / "opencode" / "plugin"
297
+
298
+ def install(self, target: Path) -> int:
299
+ target.mkdir(parents=True, exist_ok=True)
300
+ dest = target / PLUGIN_NAME
301
+ if dest.exists():
302
+ _backup(dest)
303
+ shutil.copy2(PLUGIN_SRC, dest)
304
+ print(
305
+ f"installed data-olympus enforcement (v{SHIM_VERSION}) into {dest} "
306
+ f"[opencode, tier=hard]"
307
+ )
308
+ return 0
309
+
310
+ def uninstall(self, target: Path) -> int:
311
+ dest = target / PLUGIN_NAME
312
+ if dest.exists() and "data-olympus-enforce (managed)" in dest.read_text():
313
+ dest.unlink()
314
+ print(f"uninstalled data-olympus enforcement from {dest} [opencode]")
315
+ else:
316
+ print("nothing to uninstall")
317
+ return 0
318
+
319
+ def status(self, target: Path) -> int:
320
+ import re
321
+ dest = target / PLUGIN_NAME
322
+ if not (dest.exists() and "data-olympus-enforce (managed)" in dest.read_text()):
323
+ print("opencode: not installed")
324
+ return 0
325
+ m = re.search(r"data-olympus-enforce \(managed\) v(\d+)", dest.read_text())
326
+ ver = m.group(1) if m else "?"
327
+ stale = " (stale; run `kb enforce install`)" if ver != SHIM_VERSION else ""
328
+ print(f"opencode: installed, tier=hard, versions=['{ver}']{stale}")
329
+ return 0
330
+
331
+ def doctor(self, target: Path) -> int:
332
+ ok_ep, msg = _doctor_endpoint()
333
+ print(f"doctor [opencode]: {msg}")
334
+ dest = target / PLUGIN_NAME
335
+ installed = dest.exists() and "data-olympus-enforce (managed)" in dest.read_text()
336
+ if installed:
337
+ print(f"doctor [opencode]: managed plugin present at {dest}")
338
+ else:
339
+ print(f"doctor [opencode]: managed plugin NOT installed at {dest}")
340
+ # The plugin source lives beside this installer; warn if it (and so the
341
+ # install source) resolves inside a worktree.
342
+ ok_worktree = not _hook_bin_in_worktree(str(PLUGIN_SRC))
343
+ if not ok_worktree:
344
+ print(f"doctor [opencode]: WARNING: plugin source resolves inside a "
345
+ f"worktree ({PLUGIN_SRC}); re-install from the main checkout.")
346
+ return 0 if (ok_ep and installed and ok_worktree) else 1
347
+
348
+
349
+ IBEGIN = "<!-- >>> data-olympus enforce (managed) >>> -->"
350
+ IEND = "<!-- <<< data-olympus enforce <<< -->"
351
+ INSTRUCTIONS_BODY = f"""{IBEGIN}
352
+ <!-- data-olympus-enforce v{SHIM_VERSION} -->
353
+ ## Governed decisions (data-olympus)
354
+
355
+ Before making a code or architectural decision (choosing a library, pattern,
356
+ migration, API design, schema, or auth approach), you MUST call the `kb_consult`
357
+ MCP tool for the current workspace and follow the governing rules it returns.
358
+ Treat those rules as authoritative, not advisory. This agent cannot be hard-gated
359
+ locally, so compliance is observed via the data-olympus audit log.
360
+ {IEND}"""
361
+
362
+
363
+ class InstructionsProvider:
364
+ """A soft provider that writes a MARKER-delimited managed block into a
365
+ Markdown instructions file. Unlike the hard hook providers, it cannot block a
366
+ tool call; it instructs the agent to consult the KB and relies on the
367
+ data-olympus audit log to observe compliance. The block is delimited by the
368
+ IBEGIN/IEND HTML comments so install is idempotent (one block, replaced on
369
+ re-install) and uninstall is surgical (operator content preserved).
370
+ """
371
+
372
+ tier = "soft"
373
+
374
+ def __init__(self, name: str, default_target: Path) -> None:
375
+ self.name = name
376
+ self._default_target = default_target
377
+
378
+ def default_target(self) -> Path:
379
+ return self._default_target
380
+
381
+ @staticmethod
382
+ def _strip_block(text: str) -> str:
383
+ if IBEGIN in text and IEND in text:
384
+ pre = text.split(IBEGIN, 1)[0].rstrip("\n")
385
+ post = text.split(IEND, 1)[1].lstrip("\n")
386
+ joined = "\n".join(p for p in (pre, post) if p)
387
+ return (joined + "\n") if joined else ""
388
+ return text
389
+
390
+ def install(self, target: Path) -> int:
391
+ existing = target.read_text() if target.exists() else ""
392
+ if target.exists():
393
+ _backup(target)
394
+ base = self._strip_block(existing).rstrip("\n")
395
+ new = (base + "\n\n" if base else "") + INSTRUCTIONS_BODY + "\n"
396
+ target.parent.mkdir(parents=True, exist_ok=True)
397
+ target.write_text(new)
398
+ print(
399
+ f"installed data-olympus enforcement (v{SHIM_VERSION}) into {target} "
400
+ f"[{self.name}, tier=soft]"
401
+ )
402
+ return 0
403
+
404
+ def uninstall(self, target: Path) -> int:
405
+ if not target.exists():
406
+ print("nothing to uninstall")
407
+ return 0
408
+ _backup(target)
409
+ target.write_text(self._strip_block(target.read_text()))
410
+ print(f"uninstalled data-olympus enforcement from {target} [{self.name}]")
411
+ return 0
412
+
413
+ def status(self, target: Path) -> int:
414
+ if target.exists() and IBEGIN in target.read_text():
415
+ print(f"{self.name}: installed, tier=soft, versions=['{SHIM_VERSION}']")
416
+ else:
417
+ print(f"{self.name}: not installed")
418
+ return 0
419
+
420
+ def doctor(self, _target: Path) -> int:
421
+ ok, msg = _doctor_endpoint()
422
+ print(f"doctor [{self.name}]: {msg}")
423
+ return 0 if ok else 1
424
+
425
+
426
+ class UnsupportedProvider:
427
+ """A documented-unsupported provider stub. The agent has no local hook,
428
+ instructions, or MCP surface we can wire, so install/uninstall/doctor report
429
+ the reason to stderr and exit non-zero (69 == EX_UNAVAILABLE); status reports
430
+ unsupported but exits 0 (querying state is not itself a failure).
431
+ """
432
+
433
+ tier = "unsupported"
434
+
435
+ def __init__(self, name: str, reason: str) -> None:
436
+ self.name = name
437
+ self._reason = reason
438
+
439
+ def default_target(self) -> Path:
440
+ return Path("/dev/null")
441
+
442
+ def _report(self) -> int:
443
+ print(f"{self.name}: unsupported -- {self._reason}", file=sys.stderr)
444
+ return 69 # EX_UNAVAILABLE
445
+
446
+ def install(self, _target: Path) -> int:
447
+ return self._report()
448
+
449
+ def uninstall(self, _target: Path) -> int:
450
+ return self._report()
451
+
452
+ def status(self, _target: Path) -> int:
453
+ print(f"{self.name}: unsupported -- {self._reason}")
454
+ return 0
455
+
456
+ def doctor(self, _target: Path) -> int:
457
+ return self._report()
458
+
459
+
460
+ HOOK_BEGIN = "# >>> data-olympus enforce (managed) >>>"
461
+ HOOK_END = "# <<< data-olympus enforce <<<"
462
+
463
+
464
+ def _git_managed_block(block: bool) -> str:
465
+ if block:
466
+ body = (
467
+ 'data-olympus report --staged --fail-on-unverified || {\n'
468
+ ' echo "[KB] commit blocked: governed change without a consultation '
469
+ '(set KB_ENFORCE_FAIL_MODE or run kb_consult)." >&2; exit 1;\n'
470
+ '}'
471
+ )
472
+ else:
473
+ body = (
474
+ 'data-olympus report --range HEAD~1..HEAD --emit-events || true'
475
+ )
476
+ return f"{HOOK_BEGIN}\n# data-olympus-enforce v{SHIM_VERSION}\n{body}\n{HOOK_END}\n"
477
+
478
+
479
+ class GitHookProvider:
480
+ name = "git"
481
+ tier = "detect"
482
+
483
+ def __init__(self, *, block: bool = False) -> None:
484
+ self._block = block
485
+
486
+ def default_target(self) -> Path:
487
+ hook = "pre-commit" if self._block else "post-commit"
488
+ return Path(".git") / "hooks" / hook
489
+
490
+ @staticmethod
491
+ def _strip(text: str) -> str:
492
+ if HOOK_BEGIN in text and HOOK_END in text:
493
+ pre = text.split(HOOK_BEGIN, 1)[0].rstrip("\n")
494
+ post = text.split(HOOK_END, 1)[1].lstrip("\n")
495
+ joined = "\n".join(p for p in (pre, post) if p)
496
+ return (joined + "\n") if joined else ""
497
+ return text
498
+
499
+ def install(self, target: Path) -> int:
500
+ existing = target.read_text() if target.exists() else ""
501
+ if target.exists():
502
+ _backup(target)
503
+ base = self._strip(existing).rstrip("\n")
504
+ if not base:
505
+ base = "#!/bin/sh"
506
+ new = base + "\n\n" + _git_managed_block(self._block) + "\n"
507
+ target.parent.mkdir(parents=True, exist_ok=True)
508
+ target.write_text(new)
509
+ target.chmod(0o755)
510
+ kind = "pre-commit (block)" if self._block else "post-commit (warn)"
511
+ print(f"installed data-olympus enforcement (v{SHIM_VERSION}) into {target} [git, {kind}]")
512
+ return 0
513
+
514
+ def uninstall(self, target: Path) -> int:
515
+ if not target.exists():
516
+ print("nothing to uninstall")
517
+ return 0
518
+ _backup(target)
519
+ target.write_text(self._strip(target.read_text()))
520
+ print(f"uninstalled data-olympus enforcement from {target} [git]")
521
+ return 0
522
+
523
+ def status(self, target: Path) -> int:
524
+ if target.exists() and HOOK_BEGIN in target.read_text():
525
+ print(f"git: installed, tier=detect, versions=['{SHIM_VERSION}']")
526
+ else:
527
+ print("git: not installed")
528
+ return 0
529
+
530
+ def doctor(self, target: Path) -> int:
531
+ ok = target.exists() and HOOK_BEGIN in target.read_text() and os.access(target, os.X_OK)
532
+ print(f"doctor [git]: hook {'present and executable' if ok else 'missing'} at {target}")
533
+ return 0 if ok else 1
534
+
535
+
536
+ def registry() -> dict:
537
+ return {
538
+ "claude-code": _claude_provider(),
539
+ "codex": _codex_provider(),
540
+ "gemini": _gemini_provider(),
541
+ "opencode": OpenCodeProvider(),
542
+ # copilot-cli: the GitHub Copilot CLI loads "custom instructions from
543
+ # AGENTS.md and related files" (verified via `copilot --help`:
544
+ # `--no-custom-instructions` disables exactly that). No instructions file
545
+ # exists in ~/.copilot/ on this laptop, so we default to the documented
546
+ # global custom-instructions path ~/.copilot/copilot-instructions.md (one
547
+ # of the "related files"). Operators can override with --settings.
548
+ "copilot-cli": InstructionsProvider(
549
+ "copilot-cli", Path(_home()) / ".copilot" / "copilot-instructions.md"),
550
+ "copilot-ide": InstructionsProvider(
551
+ "copilot-ide", Path(".github/copilot-instructions.md")),
552
+ "antigravity": UnsupportedProvider(
553
+ "antigravity",
554
+ "no documented local hook/instructions/MCP surface as of 2026-06; "
555
+ "revisit when Google publishes an extensibility API"),
556
+ }
557
+
558
+
559
+ def main(argv: list[str]) -> int:
560
+ p = argparse.ArgumentParser(prog="kb enforce")
561
+ p.add_argument("command", choices=["install", "uninstall", "status", "doctor"])
562
+ p.add_argument("--agent", default=None)
563
+ p.add_argument("--all", action="store_true")
564
+ p.add_argument("--settings", default=None)
565
+ p.add_argument("--block", action="store_true",
566
+ help="git provider: install a blocking pre-commit hook "
567
+ "instead of post-commit warn")
568
+ p.add_argument("--mode", choices=["off", "soft", "hard"], default="hard",
569
+ help="off uninstalls; soft installs inject-only (no blocking "
570
+ "gate); hard (default) installs the full gate")
571
+ args = p.parse_args(argv)
572
+ reg = registry()
573
+
574
+ if args.all or (args.command == "status" and not args.agent and not args.settings):
575
+ rc = 0
576
+ for name, provider in reg.items():
577
+ if args.all and getattr(provider, "tier", "") == "unsupported":
578
+ print(f"{name}: skipped (unsupported)")
579
+ continue
580
+ cmd = args.command
581
+ if args.command == "install" and args.mode == "off":
582
+ cmd = "uninstall"
583
+ elif args.command == "install" and args.mode == "soft":
584
+ if hasattr(provider, "enforce_mode"):
585
+ provider.enforce_mode = "soft"
586
+ else:
587
+ print(f"note: {name} has a fixed tier; --mode soft has no "
588
+ f"effect (use --mode off to uninstall)")
589
+ target = provider.default_target()
590
+ rc |= {
591
+ "install": provider.install, "uninstall": provider.uninstall,
592
+ "status": provider.status, "doctor": provider.doctor,
593
+ }[cmd](target)
594
+ return rc
595
+
596
+ agent = args.agent or "claude-code"
597
+ provider = GitHookProvider(block=args.block) if agent == "git" else reg.get(agent)
598
+ if provider is None:
599
+ print(f"kb enforce: unknown agent '{agent}' (known: {', '.join(sorted(reg))}, git)",
600
+ file=sys.stderr)
601
+ return 64
602
+ command = args.command
603
+ if args.command == "install" and args.mode == "off":
604
+ command = "uninstall"
605
+ elif args.command == "install" and args.mode == "soft":
606
+ if hasattr(provider, "enforce_mode"):
607
+ provider.enforce_mode = "soft"
608
+ else:
609
+ print(f"note: {agent} has a fixed tier; --mode soft has no effect "
610
+ f"(use --mode off to uninstall)")
611
+ target = Path(args.settings) if args.settings else provider.default_target()
612
+ return {
613
+ "install": provider.install, "uninstall": provider.uninstall,
614
+ "status": provider.status, "doctor": provider.doctor,
615
+ }[command](target)
616
+
617
+
618
+ if __name__ == "__main__":
619
+ raise SystemExit(main(sys.argv[1:]))