org-knowledge-layer 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.
- okl/__init__.py +12 -0
- okl/__main__.py +8 -0
- okl/bootstrap.py +83 -0
- okl/cli.py +484 -0
- okl/client.py +160 -0
- okl/core.py +223 -0
- okl/drift.py +119 -0
- okl/mcp_server.py +75 -0
- okl/scaffold/MANIFEST.md +59 -0
- okl/scaffold/ci/method-gates.yml +32 -0
- okl/scaffold/ci/okl-verify.yml +59 -0
- okl/scaffold/claude/agents/architecture-reviewer.md +41 -0
- okl/scaffold/claude/commands/check-rules.md +24 -0
- okl/scaffold/claude/commands/feature-spec.md +37 -0
- okl/scaffold/claude/rules/example-area.md +22 -0
- okl/scaffold/claude/skills/RECOMMENDED-COMPANIONS.md +40 -0
- okl/scaffold/claude/skills/encoding-loop/SKILL.md +48 -0
- okl/scaffold/claude/skills/verify-before-claiming/SKILL.md +56 -0
- okl/scaffold/evals/README.md +32 -0
- okl/scaffold/evals/cases.jsonl +1 -0
- okl/scaffold/evals/run_evals.py +109 -0
- okl/scaffold/gates/check-canon-size.sh +11 -0
- okl/scaffold/gates/check-doc-orphans.sh +19 -0
- okl/scaffold/gates/check-retractions.sh +22 -0
- okl/scaffold/gates/check-tombstones.sh +22 -0
- okl/scaffold/gates/run-gates.sh +31 -0
- okl/scaffold/hooks/hooks.json +16 -0
- okl/scaffold/hooks/stop-okl-encode.sh +78 -0
- okl/scaffold/hooks/userpromptsubmit-okl-check.sh +68 -0
- okl/scaffold/plugin/plugin.json +10 -0
- okl/scaffold/profiles/dotnet/README.md +12 -0
- okl/scaffold/profiles/dotnet/rules/architecture.md +55 -0
- okl/scaffold/profiles/dotnet/rules/messaging.md +31 -0
- okl/scaffold/profiles/dotnet/rules/performance-and-data.md +36 -0
- okl/scaffold/profiles/dotnet/rules/security.md +42 -0
- okl/scaffold/profiles/geospatial/README.md +6 -0
- okl/scaffold/profiles/geospatial/rules/geospatial-ml.md +38 -0
- okl/scaffold/profiles/python-rag/README.md +13 -0
- okl/scaffold/profiles/python-rag/rules/fastapi-backend.md +37 -0
- okl/scaffold/profiles/python-rag/rules/project-structure.md +28 -0
- okl/scaffold/profiles/python-rag/rules/rag-pipeline.md +73 -0
- okl/scaffold/profiles/react/README.md +18 -0
- okl/scaffold/profiles/react/rules/frontend.md +57 -0
- okl/scaffold/registries/RETRACTIONS.md +19 -0
- okl/scaffold/registries/tombstones.txt +7 -0
- okl/scaffold/root/CLAUDE.md +55 -0
- okl/scaffold/root/METHOD.md +64 -0
- okl/scaffold_cmd.py +110 -0
- okl/seed/dotnet-canon.json +489 -0
- okl/seed/dotnet-decisions.json +328 -0
- okl/seed/dotnet-defects.json +133 -0
- okl/seed/dotnet-review-surfaces.json +147 -0
- okl/seed/frontend-canon.json +116 -0
- okl/seed/geospatial-deeptime-defects.json +59 -0
- okl/seed/geospatial-defects.json +154 -0
- okl/seed/geospatial-enforcement-defects.json +121 -0
- okl/seed/geospatial-eval-defects.json +25 -0
- okl/seed/rag-defects.json +120 -0
- okl/seed/react-defects.json +45 -0
- okl/seed.py +55 -0
- okl/service.py +137 -0
- okl/store.py +432 -0
- org_knowledge_layer-0.1.0.dist-info/METADATA +475 -0
- org_knowledge_layer-0.1.0.dist-info/RECORD +67 -0
- org_knowledge_layer-0.1.0.dist-info/WHEEL +4 -0
- org_knowledge_layer-0.1.0.dist-info/entry_points.txt +2 -0
- org_knowledge_layer-0.1.0.dist-info/licenses/LICENSE +21 -0
okl/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""okl — the Org Knowledge Layer (the sixth surface).
|
|
2
|
+
|
|
3
|
+
Install into any repo; read lessons curated by your other repos; contribute back.
|
|
4
|
+
See the design rationale in the-sixth-surface.md.
|
|
5
|
+
"""
|
|
6
|
+
from . import core
|
|
7
|
+
from .client import Client, OKLUnreachable
|
|
8
|
+
from .store import EDGE_RELS, NODE_TYPES, Edge, Node, Store
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__all__ = ["Store", "Node", "Edge", "Client", "OKLUnreachable", "core",
|
|
12
|
+
"NODE_TYPES", "EDGE_RELS", "__version__"]
|
okl/__main__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""`python3 -m okl` — the PATH-independent entry point.
|
|
2
|
+
|
|
3
|
+
Hooks run in whatever environment the agent harness spawns, which often lacks the
|
|
4
|
+
console-script directory on PATH; any python3 that can import okl can still run it.
|
|
5
|
+
"""
|
|
6
|
+
from .cli import main
|
|
7
|
+
|
|
8
|
+
raise SystemExit(main())
|
okl/bootstrap.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Factory bootstrapping (Codified Context §5.1 — factory agents that generate each tier).
|
|
2
|
+
|
|
3
|
+
Their companion repo ships agents that generate the initial infrastructure for a new
|
|
4
|
+
project. OKL's analogue: instead of stamping only static templates, *read the repo's own
|
|
5
|
+
signals* — git history, existing docs, an existing `.claude/` — and propose a starter set
|
|
6
|
+
of okl nodes for a human to review and seed.
|
|
7
|
+
|
|
8
|
+
This is deliberately NOT an autonomous LLM interview at install time (the kit can't assume
|
|
9
|
+
network/model access when `okl init` runs). It emits a reviewable `okl-bootstrap.json` in
|
|
10
|
+
the same format `okl seed` consumes; the human edits it, then `okl seed okl-bootstrap.json`.
|
|
11
|
+
The point is to lower the empty-store cold-start, not to fabricate lessons.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import subprocess
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _git(args: list[str], repo_dir: str) -> str:
|
|
21
|
+
try:
|
|
22
|
+
out = subprocess.run(["git", "-C", repo_dir, *args],
|
|
23
|
+
capture_output=True, text=True, timeout=20)
|
|
24
|
+
return out.stdout if out.returncode == 0 else ""
|
|
25
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
26
|
+
return ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def propose_nodes(repo: str, repo_dir: str = ".") -> dict[str, Any]:
|
|
30
|
+
"""Return a seed-format dict of PROPOSED starter nodes derived from repo signals.
|
|
31
|
+
|
|
32
|
+
Every proposed node is marked unverified and tagged in its body as a bootstrap
|
|
33
|
+
proposal, so a human must confirm it (and choose scope) before it becomes canon.
|
|
34
|
+
"""
|
|
35
|
+
nodes: list[dict] = []
|
|
36
|
+
|
|
37
|
+
# 1) Fix/bug commits → candidate Defect nodes (the repo already told us what broke).
|
|
38
|
+
log = _git(["log", "--no-merges", "-n", "400", "--format=%h%x09%s"], repo_dir)
|
|
39
|
+
fix_kw = ("fix", "bug", "revert", "hotfix", "regression", "broke", "incorrect", "wrong")
|
|
40
|
+
seen = set()
|
|
41
|
+
for line in log.splitlines():
|
|
42
|
+
if "\t" not in line:
|
|
43
|
+
continue
|
|
44
|
+
sha, subj = line.split("\t", 1)
|
|
45
|
+
low = subj.lower()
|
|
46
|
+
if any(k in low for k in fix_kw):
|
|
47
|
+
key = subj.strip().lower()[:60]
|
|
48
|
+
if key in seen:
|
|
49
|
+
continue
|
|
50
|
+
seen.add(key)
|
|
51
|
+
nodes.append({
|
|
52
|
+
"key": f"boot_fix_{sha}",
|
|
53
|
+
"type": "Defect",
|
|
54
|
+
"title": subj.strip()[:120],
|
|
55
|
+
"scope": f"repo:{repo}",
|
|
56
|
+
"body": f"[BOOTSTRAP PROPOSAL from commit {sha}] Review: is this a recurring "
|
|
57
|
+
f"class worth encoding? If so, write the symptom/cause/fix and set scope.",
|
|
58
|
+
"found_by": "bootstrap: git fix-commit scan",
|
|
59
|
+
})
|
|
60
|
+
if len(nodes) >= 25:
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
# 2) Existing docs → candidate Rule/Deep-reference pointers (don't ingest text, point at it).
|
|
64
|
+
docs = []
|
|
65
|
+
for pat in ("docs", "doc"):
|
|
66
|
+
dd = Path(repo_dir) / pat
|
|
67
|
+
if dd.is_dir():
|
|
68
|
+
docs += [p for p in dd.rglob("*.md")]
|
|
69
|
+
for p in docs[:15]:
|
|
70
|
+
rel = p.relative_to(repo_dir)
|
|
71
|
+
nodes.append({
|
|
72
|
+
"key": f"boot_doc_{abs(hash(str(rel))) % 10**8}",
|
|
73
|
+
"type": "Rule",
|
|
74
|
+
"title": f"See {rel}",
|
|
75
|
+
"scope": f"repo:{repo}",
|
|
76
|
+
"body": "[BOOTSTRAP PROPOSAL] Existing doc — extract any durable rule and encode it, "
|
|
77
|
+
"or leave as a deep-reference pointer.",
|
|
78
|
+
"files": str(rel),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
note = ("PROPOSED nodes from repo signals — NOT yet canon. Review titles/scope, add "
|
|
82
|
+
"symptom/cause/fix, delete noise, then `okl seed okl-bootstrap.json`.")
|
|
83
|
+
return {"_comment": note, "nodes": nodes, "edges": []}
|
okl/cli.py
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""okl CLI — init / connect / check / record / search / seed / metric / serve.
|
|
2
|
+
|
|
3
|
+
Stdlib argparse only, so the package installs with zero required deps for the
|
|
4
|
+
local + client path. `serve` and `mcp` import their extras lazily.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from . import core
|
|
14
|
+
from .client import Client, OKLUnreachable, load_config, save_config
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _print_json(obj) -> None:
|
|
18
|
+
json.dump(obj, sys.stdout, indent=2)
|
|
19
|
+
sys.stdout.write("\n")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _merge_hook_settings(claude: Path) -> bool:
|
|
23
|
+
"""Register both okl hooks in .claude/settings.json. Idempotent: existing settings and
|
|
24
|
+
unrelated hooks are preserved; an already-registered okl hook is left alone. A hook
|
|
25
|
+
that is installed but unregistered is a surface nobody runs."""
|
|
26
|
+
settings_path = claude / "settings.json"
|
|
27
|
+
try:
|
|
28
|
+
settings = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
|
29
|
+
except json.JSONDecodeError:
|
|
30
|
+
print(f"! {settings_path} is not valid JSON — not touching it; register the hooks manually.")
|
|
31
|
+
return False
|
|
32
|
+
hooks_cfg = settings.setdefault("hooks", {})
|
|
33
|
+
changed = False
|
|
34
|
+
# UserPromptSubmit, not PreToolUse: only UserPromptSubmit/SessionStart stdout reaches the
|
|
35
|
+
# model's context. A PreToolUse briefing fires but is never read (found by E2E test).
|
|
36
|
+
wanted = [
|
|
37
|
+
("UserPromptSubmit", None, '"$CLAUDE_PROJECT_DIR"/.claude/hooks/userpromptsubmit-okl-check.sh'),
|
|
38
|
+
("Stop", None, '"$CLAUDE_PROJECT_DIR"/.claude/hooks/stop-okl-encode.sh'),
|
|
39
|
+
]
|
|
40
|
+
for event, matcher, command in wanted:
|
|
41
|
+
entries = hooks_cfg.setdefault(event, [])
|
|
42
|
+
if any(h.get("command", "").endswith(Path(command).name)
|
|
43
|
+
for e in entries for h in e.get("hooks", [])):
|
|
44
|
+
continue
|
|
45
|
+
entry: dict = {"hooks": [{"type": "command", "command": command}]}
|
|
46
|
+
if matcher:
|
|
47
|
+
entry["matcher"] = matcher
|
|
48
|
+
entries.append(entry)
|
|
49
|
+
changed = True
|
|
50
|
+
if changed:
|
|
51
|
+
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
|
|
52
|
+
return changed
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def cmd_init(args) -> int:
|
|
56
|
+
"""Wire the current repo so the loop runs without manual follow-up steps:
|
|
57
|
+
config, hooks (installed AND registered), CI verifier, MCP registration."""
|
|
58
|
+
import shutil
|
|
59
|
+
repo = args.repo or Path.cwd().name
|
|
60
|
+
cfg = load_config()
|
|
61
|
+
cfg["repo"] = repo
|
|
62
|
+
if args.service:
|
|
63
|
+
cfg["service_url"] = args.service
|
|
64
|
+
if args.interests:
|
|
65
|
+
cfg["interests"] = [t.strip().lower() for t in args.interests.split(",") if t.strip()]
|
|
66
|
+
# Pin how to invoke okl on THIS machine, for hooks running outside the dev shell
|
|
67
|
+
# (agent harnesses don't inherit venv/pipx PATH entries). Machine-local by design —
|
|
68
|
+
# .okl/ is gitignored; hooks fall back to PATH and `python3 -m okl` regardless.
|
|
69
|
+
cfg["okl_bin"] = shutil.which("okl") or f"{sys.executable} -m okl"
|
|
70
|
+
path = save_config(cfg)
|
|
71
|
+
print(f"✓ wrote {path} (repo={repo}, mode={'remote' if cfg.get('service_url') else 'local'}"
|
|
72
|
+
+ (f", interests={','.join(cfg['interests'])}" if cfg.get("interests") else "") + ")")
|
|
73
|
+
|
|
74
|
+
claude = Path(".claude")
|
|
75
|
+
if claude.exists():
|
|
76
|
+
_install_claude_wiring(claude)
|
|
77
|
+
else:
|
|
78
|
+
print("• no .claude/ dir found — see README to wire the hook for your agent (AGENTS.md/.cursor).")
|
|
79
|
+
_install_ci_verifier()
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _install_claude_wiring(claude: Path) -> None:
|
|
84
|
+
"""Install AND register the hooks: the PreToolUse check (the enforced read) and the
|
|
85
|
+
Stop encode reminder (the write-side catch). Scripts come from the packaged scaffold —
|
|
86
|
+
one canonical source, no drift. Also registers the MCP server when the extra exists."""
|
|
87
|
+
hooks = claude / "hooks"
|
|
88
|
+
hooks.mkdir(exist_ok=True)
|
|
89
|
+
scaffold_hooks = Path(__file__).parent / "scaffold" / "hooks"
|
|
90
|
+
for name, label in [("userpromptsubmit-okl-check.sh", "pre-task check hook (UserPromptSubmit)"),
|
|
91
|
+
("stop-okl-encode.sh", "encode reminder (Stop hook)")]:
|
|
92
|
+
dst = hooks / name
|
|
93
|
+
dst.write_text((scaffold_hooks / name).read_text())
|
|
94
|
+
dst.chmod(0o755)
|
|
95
|
+
print(f"✓ installed {label} → {dst}")
|
|
96
|
+
if _merge_hook_settings(claude):
|
|
97
|
+
print("✓ registered both hooks in .claude/settings.json (UserPromptSubmit + Stop)")
|
|
98
|
+
else:
|
|
99
|
+
print("• hooks already registered in .claude/settings.json")
|
|
100
|
+
# MCP: register the okl server only if the extra is importable (a registration whose
|
|
101
|
+
# dependency is missing would be a broken tool, worse than none).
|
|
102
|
+
try:
|
|
103
|
+
import mcp # noqa: F401
|
|
104
|
+
except ImportError:
|
|
105
|
+
print("• MCP extra not installed — `pip install okl[mcp]` then re-run init to register the agent tools.")
|
|
106
|
+
return
|
|
107
|
+
mcp_path = Path(".mcp.json")
|
|
108
|
+
mcp_cfg = json.loads(mcp_path.read_text()) if mcp_path.exists() else {}
|
|
109
|
+
servers = mcp_cfg.setdefault("mcpServers", {})
|
|
110
|
+
if "okl" not in servers:
|
|
111
|
+
servers["okl"] = {"command": "okl", "args": ["mcp"]}
|
|
112
|
+
mcp_path.write_text(json.dumps(mcp_cfg, indent=2) + "\n")
|
|
113
|
+
print("✓ registered okl MCP server → .mcp.json (okl_check / okl_record / okl_search)")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _install_ci_verifier() -> None:
|
|
117
|
+
"""Install the CI verifier workflow instead of printing a copy instruction; warn
|
|
118
|
+
loudly when git is absent, because the drift layer is dead without history."""
|
|
119
|
+
if not Path(".git").exists():
|
|
120
|
+
print("⚠ not a git repository — the drift verifier (okl drift) and the CI gate are DISABLED")
|
|
121
|
+
print(" until `git init`: drift compares governed files against their last-verified commit.")
|
|
122
|
+
return
|
|
123
|
+
wf = Path(".github") / "workflows" / "okl-verify.yml"
|
|
124
|
+
if wf.exists():
|
|
125
|
+
print(f"• CI verifier already present → {wf}")
|
|
126
|
+
return
|
|
127
|
+
wf.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
src = Path(__file__).parent / "scaffold" / "ci" / "okl-verify.yml"
|
|
129
|
+
wf.write_text(src.read_text())
|
|
130
|
+
print(f"✓ installed CI verifier → {wf} (drift gate + repo gates on every PR)")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def cmd_connect(args) -> int:
|
|
134
|
+
cfg = load_config()
|
|
135
|
+
cfg["service_url"] = args.url
|
|
136
|
+
if args.token:
|
|
137
|
+
cfg["token"] = args.token
|
|
138
|
+
path = save_config(cfg)
|
|
139
|
+
print(f"✓ connected → {args.url} ({path})")
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def cmd_check(args) -> int:
|
|
144
|
+
client = Client()
|
|
145
|
+
try:
|
|
146
|
+
result = client.check(args.task, repo=args.repo)
|
|
147
|
+
except OKLUnreachable as e:
|
|
148
|
+
# FAIL CLOSED — loud, non-zero, no reassuring empty result.
|
|
149
|
+
print(f"OKL UNREACHABLE — refusing to report a clean check.\n{e}", file=sys.stderr)
|
|
150
|
+
return 2
|
|
151
|
+
if args.format == "json":
|
|
152
|
+
_print_json(result)
|
|
153
|
+
else:
|
|
154
|
+
print(core.render_check_for_agent(result))
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def cmd_record(args) -> int:
|
|
159
|
+
client = Client()
|
|
160
|
+
kwargs = dict(type=args.type, title=args.title, scope=args.scope,
|
|
161
|
+
body=args.body, status=args.status, found_by=args.found_by,
|
|
162
|
+
ttl_days=args.ttl_days, owner=args.owner, verified=args.verified,
|
|
163
|
+
files=args.files, symptom=args.symptom, fix=args.fix, tags=args.tags,
|
|
164
|
+
id=args.id)
|
|
165
|
+
if args.repo:
|
|
166
|
+
kwargs["repo"] = args.repo
|
|
167
|
+
node_id = client.record(**{k: v for k, v in kwargs.items() if v is not None})
|
|
168
|
+
print(node_id)
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def cmd_link(args) -> int:
|
|
173
|
+
Client().link(args.src, args.rel, args.dst)
|
|
174
|
+
print(f"✓ {args.src} -[{args.rel}]-> {args.dst}")
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def cmd_verify(args) -> int:
|
|
179
|
+
"""Run the named check, and stamp the node verified ONLY on an observed pass.
|
|
180
|
+
|
|
181
|
+
The evidence trail (command, expect-match, timestamp) is stored on the node —
|
|
182
|
+
the store-side mechanization of verify-before-claiming: no run, no stamp."""
|
|
183
|
+
import subprocess
|
|
184
|
+
from datetime import datetime, timezone
|
|
185
|
+
r = subprocess.run(args.run, shell=True, capture_output=True, text=True,
|
|
186
|
+
timeout=args.timeout)
|
|
187
|
+
output = (r.stdout or "") + (r.stderr or "")
|
|
188
|
+
tail = "\n".join(output.strip().splitlines()[-5:])
|
|
189
|
+
if r.returncode != 0:
|
|
190
|
+
print(f"✗ check FAILED (exit {r.returncode}) — NOT stamping verification.\n{tail}",
|
|
191
|
+
file=sys.stderr)
|
|
192
|
+
return 1
|
|
193
|
+
if args.expect and args.expect not in output:
|
|
194
|
+
# exit 0 alone is a step grading itself — require the positive success signal
|
|
195
|
+
# when the caller names one (the exit-0-zero-files lesson).
|
|
196
|
+
print(f"✗ check exited 0 but expected signal {args.expect!r} NOT in output — NOT stamping.\n{tail}",
|
|
197
|
+
file=sys.stderr)
|
|
198
|
+
return 1
|
|
199
|
+
stamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%MZ")
|
|
200
|
+
evidence = f"`{args.run}` exit 0" + (f", matched {args.expect!r}" if args.expect else "") + f" @ {stamp}"
|
|
201
|
+
try:
|
|
202
|
+
node = Client().verify(args.node_id, evidence)
|
|
203
|
+
except OKLUnreachable as e:
|
|
204
|
+
print(f"OKL UNREACHABLE — check passed but the stamp was NOT recorded.\n{e}", file=sys.stderr)
|
|
205
|
+
return 2
|
|
206
|
+
print(f"✓ verified {node['id']} — {node['title']}\n evidence: {node['verified_by']}")
|
|
207
|
+
return 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def cmd_search(args) -> int:
|
|
211
|
+
results = Client().search(args.query, scope=args.scope,
|
|
212
|
+
node_types=args.type, limit=args.limit)
|
|
213
|
+
if args.format == "json":
|
|
214
|
+
_print_json(results)
|
|
215
|
+
else:
|
|
216
|
+
for r in results:
|
|
217
|
+
tag = " (STALE)" if r.get("stale") else ""
|
|
218
|
+
print(f"[{r['type']:10}] {r['scope']:16} {r['title']}{tag}")
|
|
219
|
+
return 0
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def cmd_metric(args) -> int:
|
|
223
|
+
"""Recurrence-after-arming — the quantification the method says it lacks."""
|
|
224
|
+
try:
|
|
225
|
+
rows = Client().recurrence()
|
|
226
|
+
except OKLUnreachable as e:
|
|
227
|
+
print(f"OKL UNREACHABLE — cannot compute metric.\n{e}", file=sys.stderr)
|
|
228
|
+
return 2
|
|
229
|
+
if args.format == "json":
|
|
230
|
+
_print_json({"recurrence_after_arming": rows, "count": len(rows)})
|
|
231
|
+
else:
|
|
232
|
+
if not rows:
|
|
233
|
+
print("recurrence-after-arming: 0 — no known defect class has recurred where a gate should have armed. ✓")
|
|
234
|
+
else:
|
|
235
|
+
print(f"recurrence-after-arming: {len(rows)}")
|
|
236
|
+
for r in rows:
|
|
237
|
+
print(f" {r['defect_class']} recurred in {r['recurred_in']} (gate: {r['gate']})")
|
|
238
|
+
return 0
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def cmd_drift(args) -> int:
|
|
242
|
+
"""Source-vs-spec drift: rules whose governed code changed after last verification."""
|
|
243
|
+
from . import drift
|
|
244
|
+
client = Client()
|
|
245
|
+
try:
|
|
246
|
+
nodes = client.all_nodes()
|
|
247
|
+
except OKLUnreachable as e:
|
|
248
|
+
print(f"OKL UNREACHABLE — cannot check drift.\n{e}", file=sys.stderr)
|
|
249
|
+
return 2
|
|
250
|
+
repo = args.repo or client.repo
|
|
251
|
+
hits = drift.detect_drift(nodes, repo, repo_dir=args.repo_dir)
|
|
252
|
+
if args.format == "json":
|
|
253
|
+
_print_json({"drift": [h.as_dict() for h in hits], "count": len(hits)})
|
|
254
|
+
return 0
|
|
255
|
+
print(drift.render_drift(hits))
|
|
256
|
+
# Fail closed when asked to gate (CI): drift is a defect to surface, exit 1.
|
|
257
|
+
return 1 if (hits and args.gate) else 0
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def cmd_coverage(args) -> int:
|
|
261
|
+
"""Knowledge-to-code ratio — a health signal, not a target (Codified Context §4.2)."""
|
|
262
|
+
import subprocess
|
|
263
|
+
from pathlib import Path
|
|
264
|
+
client = Client()
|
|
265
|
+
try:
|
|
266
|
+
nodes = client.all_nodes()
|
|
267
|
+
except OKLUnreachable as e:
|
|
268
|
+
print(f"OKL UNREACHABLE — cannot compute coverage.\n{e}", file=sys.stderr)
|
|
269
|
+
return 2
|
|
270
|
+
repo = args.repo or client.repo
|
|
271
|
+
in_scope = [n for n in nodes if n.scope == "org" or n.scope == f"repo:{repo}"]
|
|
272
|
+
knowledge_lines = sum(len((n.body or "").splitlines()) + 1 for n in in_scope)
|
|
273
|
+
# code lines: git ls-files line count, or None if not a repo
|
|
274
|
+
code_lines = None
|
|
275
|
+
try:
|
|
276
|
+
files = subprocess.run(["git", "-C", args.repo_dir, "ls-files"],
|
|
277
|
+
capture_output=True, text=True, timeout=15)
|
|
278
|
+
if files.returncode == 0:
|
|
279
|
+
code_lines = 0
|
|
280
|
+
for f in files.stdout.splitlines():
|
|
281
|
+
fp = Path(args.repo_dir) / f
|
|
282
|
+
if fp.suffix.lower() in {".py",".cs",".ts",".tsx",".js",".jsx",".go",".rs",".java",".rb"}:
|
|
283
|
+
try:
|
|
284
|
+
code_lines += sum(1 for _ in fp.open("rb"))
|
|
285
|
+
except OSError:
|
|
286
|
+
pass
|
|
287
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
288
|
+
pass
|
|
289
|
+
ratio = (knowledge_lines / code_lines) if code_lines else None
|
|
290
|
+
out = {"nodes_in_scope": len(in_scope), "knowledge_lines": knowledge_lines,
|
|
291
|
+
"code_lines": code_lines,
|
|
292
|
+
"knowledge_to_code": round(ratio, 4) if ratio is not None else None}
|
|
293
|
+
if args.format == "json":
|
|
294
|
+
_print_json(out); return 0
|
|
295
|
+
print(f"OKL coverage for {repo}:")
|
|
296
|
+
print(f" encoded nodes in scope : {out['nodes_in_scope']}")
|
|
297
|
+
print(f" knowledge lines : {out['knowledge_lines']}")
|
|
298
|
+
print(f" code lines : {out['code_lines'] if out['code_lines'] is not None else '(not a git repo)'}")
|
|
299
|
+
if ratio is not None:
|
|
300
|
+
print(f" knowledge-to-code : {ratio:.1%} (health signal — a sudden spike in agent confusion "
|
|
301
|
+
"means a relevant node is missing or stale, not that this number is wrong)")
|
|
302
|
+
return 0
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def cmd_bootstrap(args) -> int:
|
|
306
|
+
"""Propose starter nodes from repo signals into a reviewable okl-bootstrap.json."""
|
|
307
|
+
from pathlib import Path
|
|
308
|
+
|
|
309
|
+
from . import bootstrap
|
|
310
|
+
repo = args.repo or Client().repo
|
|
311
|
+
proposal = bootstrap.propose_nodes(repo, repo_dir=args.repo_dir)
|
|
312
|
+
out = Path(args.out)
|
|
313
|
+
out.write_text(json.dumps(proposal, indent=1))
|
|
314
|
+
n = len(proposal["nodes"])
|
|
315
|
+
print(f"✓ proposed {n} starter node(s) → {out}")
|
|
316
|
+
print(" Review + edit (set scope, add symptom/cause/fix, delete noise), then:")
|
|
317
|
+
print(f" okl seed {out}")
|
|
318
|
+
return 0
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def cmd_seed(args) -> int:
|
|
322
|
+
import glob
|
|
323
|
+
from pathlib import Path
|
|
324
|
+
|
|
325
|
+
from .seed import seed_from_file
|
|
326
|
+
path = args.path or str(Path(__file__).parent.parent.parent / "seed")
|
|
327
|
+
if path == str(Path(__file__).parent.parent.parent / "seed") and not Path(path).exists():
|
|
328
|
+
# installed package: seeds ship under the package dir
|
|
329
|
+
path = str(Path(__file__).parent / "seed")
|
|
330
|
+
targets = (sorted(glob.glob(str(Path(path) / "*-defects.json")))
|
|
331
|
+
if Path(path).is_dir() else [path])
|
|
332
|
+
if not targets:
|
|
333
|
+
print(f"no *-defects.json found under {path}")
|
|
334
|
+
return 1
|
|
335
|
+
client, total = Client(), 0
|
|
336
|
+
for t in targets:
|
|
337
|
+
n = seed_from_file(client, t)
|
|
338
|
+
total += n
|
|
339
|
+
print(f" ✓ {n} node(s) from {Path(t).name}")
|
|
340
|
+
print(f"✓ seeded {total} node(s) from {len(targets)} file(s)")
|
|
341
|
+
return 0
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def cmd_scaffold(args) -> int:
|
|
345
|
+
"""Stamp the portable method kit (canon, skills, agent, commands, gates, evals, hook) into a repo."""
|
|
346
|
+
from .scaffold_cmd import scaffold
|
|
347
|
+
res = scaffold(target=args.target, repo=args.repo, force=args.force, plugin=args.plugin,
|
|
348
|
+
profile=args.profile)
|
|
349
|
+
print(f"✓ scaffolded method kit into {res['root']} (repo={res['repo']}"
|
|
350
|
+
+ (f", profiles={'+'.join(args.profile)}" if args.profile else "")
|
|
351
|
+
+ (", as Claude Code plugin" if res['plugin'] else "") + ")")
|
|
352
|
+
print(f" {len(res['written'])} file(s) written, {len(res['skipped'])} skipped (already existed).")
|
|
353
|
+
if args.verbose:
|
|
354
|
+
for f in res["written"]:
|
|
355
|
+
print(f" + {f}")
|
|
356
|
+
if res["skipped"] and not args.force:
|
|
357
|
+
print(f" skipped (use --force to overwrite): {', '.join(res['skipped'][:8])}"
|
|
358
|
+
+ (" …" if len(res['skipped']) > 8 else ""))
|
|
359
|
+
if res["fills"]:
|
|
360
|
+
print(f"\n {len(res['fills'])} <<FILL>> slot(s) to complete (stack-specific rules):")
|
|
361
|
+
for f in res["fills"]:
|
|
362
|
+
print(f" • {f}")
|
|
363
|
+
print(" grep -rn '<<FILL' . to find them all later.")
|
|
364
|
+
print("\nNext: `okl init` to wire the knowledge layer, then `/feature-spec` before your first change.")
|
|
365
|
+
return 0
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def cmd_serve(args) -> int:
|
|
369
|
+
from .service import run
|
|
370
|
+
run(host=args.host, port=args.port)
|
|
371
|
+
return 0
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def cmd_mcp(args) -> int:
|
|
375
|
+
from .mcp_server import run_stdio
|
|
376
|
+
run_stdio()
|
|
377
|
+
return 0
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
381
|
+
p = argparse.ArgumentParser(prog="okl", description="Org Knowledge Layer — the sixth surface.")
|
|
382
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
383
|
+
|
|
384
|
+
pi = sub.add_parser("init", help="wire the current repo (config + hook + CI pointer)")
|
|
385
|
+
pi.add_argument("--repo"); pi.add_argument("--service")
|
|
386
|
+
pi.add_argument("--interests", help="comma-sep subject tags this repo cares about "
|
|
387
|
+
"(filters org-scope lessons in `check`; see store.KNOWN_TAGS)")
|
|
388
|
+
pi.set_defaults(func=cmd_init)
|
|
389
|
+
|
|
390
|
+
pc = sub.add_parser("connect", help="point this repo at a shared OKL service URL")
|
|
391
|
+
pc.add_argument("url"); pc.add_argument("--token")
|
|
392
|
+
pc.set_defaults(func=cmd_connect)
|
|
393
|
+
|
|
394
|
+
pk = sub.add_parser("check", help="pre-task read: relevant lessons for a task")
|
|
395
|
+
pk.add_argument("--task", required=True); pk.add_argument("--repo")
|
|
396
|
+
pk.add_argument("--format", choices=["agent", "json"], default="agent")
|
|
397
|
+
pk.set_defaults(func=cmd_check)
|
|
398
|
+
|
|
399
|
+
pr = sub.add_parser("record", help="record a node (defect/gate/claim/...)")
|
|
400
|
+
pr.add_argument("--type", required=True); pr.add_argument("--title", required=True)
|
|
401
|
+
pr.add_argument("--scope", required=True, help="'org' or 'repo:<name>' or 'repo'")
|
|
402
|
+
pr.add_argument("--repo"); pr.add_argument("--body"); pr.add_argument("--status")
|
|
403
|
+
pr.add_argument("--found-by", dest="found_by")
|
|
404
|
+
pr.add_argument("--ttl-days", dest="ttl_days", type=int); pr.add_argument("--owner")
|
|
405
|
+
pr.add_argument("--files", help="comma-sep path globs this node governs (enrolls it in drift detection)")
|
|
406
|
+
pr.add_argument("--symptom", help="Symptom→Cause→Fix: the observable symptom (cause goes in --body)")
|
|
407
|
+
pr.add_argument("--fix", help="Symptom→Cause→Fix: the fix to apply")
|
|
408
|
+
pr.add_argument("--tags", help="comma-sep subject tags from the controlled vocabulary "
|
|
409
|
+
"(store.KNOWN_TAGS), e.g. 'react,security'")
|
|
410
|
+
pr.add_argument("--id", help="explicit stable id (makes the write idempotent — re-records replace)")
|
|
411
|
+
pr.add_argument("--verified", action="store_true")
|
|
412
|
+
pr.set_defaults(func=cmd_record)
|
|
413
|
+
|
|
414
|
+
pl = sub.add_parser("link", help="add an edge between two nodes")
|
|
415
|
+
pl.add_argument("src"); pl.add_argument("rel"); pl.add_argument("dst")
|
|
416
|
+
pl.set_defaults(func=cmd_link)
|
|
417
|
+
|
|
418
|
+
pvf = sub.add_parser("verify", help="run a check and stamp a node verified only on an observed pass")
|
|
419
|
+
pvf.add_argument("node_id")
|
|
420
|
+
pvf.add_argument("--run", required=True, help="the check command; exit 0 required to stamp")
|
|
421
|
+
pvf.add_argument("--expect", help="substring that must appear in the output — a positive success "
|
|
422
|
+
"signal, so exit 0 alone can't self-certify (the exit-0-zero-files lesson)")
|
|
423
|
+
pvf.add_argument("--timeout", type=int, default=600, help="seconds before the check is killed (default 600)")
|
|
424
|
+
pvf.set_defaults(func=cmd_verify)
|
|
425
|
+
|
|
426
|
+
ps = sub.add_parser("search", help="full-text search over the encoded body")
|
|
427
|
+
ps.add_argument("query"); ps.add_argument("--scope")
|
|
428
|
+
ps.add_argument("--type", nargs="*"); ps.add_argument("--limit", type=int, default=25)
|
|
429
|
+
ps.add_argument("--format", choices=["text", "json"], default="text")
|
|
430
|
+
ps.set_defaults(func=cmd_search)
|
|
431
|
+
|
|
432
|
+
pm = sub.add_parser("metric", help="recurrence-after-arming metric")
|
|
433
|
+
pm.add_argument("--format", choices=["text", "json"], default="text")
|
|
434
|
+
pm.set_defaults(func=cmd_metric)
|
|
435
|
+
|
|
436
|
+
pdr = sub.add_parser("drift", help="source-vs-spec drift: rules whose governed code changed after verification")
|
|
437
|
+
pdr.add_argument("--repo"); pdr.add_argument("--repo-dir", dest="repo_dir", default=".")
|
|
438
|
+
pdr.add_argument("--gate", action="store_true", help="exit 1 if drift found (for CI)")
|
|
439
|
+
pdr.add_argument("--format", choices=["text", "json"], default="text")
|
|
440
|
+
pdr.set_defaults(func=cmd_drift)
|
|
441
|
+
|
|
442
|
+
pcv = sub.add_parser("coverage", help="knowledge-to-code ratio (health signal)")
|
|
443
|
+
pcv.add_argument("--repo"); pcv.add_argument("--repo-dir", dest="repo_dir", default=".")
|
|
444
|
+
pcv.add_argument("--format", choices=["text", "json"], default="text")
|
|
445
|
+
pcv.set_defaults(func=cmd_coverage)
|
|
446
|
+
|
|
447
|
+
pb = sub.add_parser("bootstrap", help="propose starter nodes from repo signals (git log, docs)")
|
|
448
|
+
pb.add_argument("--repo"); pb.add_argument("--repo-dir", dest="repo_dir", default=".")
|
|
449
|
+
pb.add_argument("--out", default="okl-bootstrap.json")
|
|
450
|
+
pb.set_defaults(func=cmd_bootstrap)
|
|
451
|
+
|
|
452
|
+
pd = sub.add_parser("seed", help="ingest seed file(s) as nodes (a *-defects.json, or a dir of them)")
|
|
453
|
+
pd.add_argument("path", nargs="?", default=None,
|
|
454
|
+
help="a seed JSON, or a directory of *-defects.json (default: the bundled seed dir — all three repos)")
|
|
455
|
+
pd.set_defaults(func=cmd_seed)
|
|
456
|
+
|
|
457
|
+
psc = sub.add_parser("scaffold", help="stamp the portable method kit into a repo")
|
|
458
|
+
psc.add_argument("target", nargs="?", default=".", help="target repo dir (default: cwd)")
|
|
459
|
+
psc.add_argument("--repo", help="repo name (default: dir name)")
|
|
460
|
+
psc.add_argument("--plugin", action="store_true", help="also write plugin.json (Claude Code plugin)")
|
|
461
|
+
from .scaffold_cmd import list_profiles
|
|
462
|
+
psc.add_argument("--profile", action="append", choices=list_profiles(), metavar="PROFILE",
|
|
463
|
+
help="drop a stack's verbatim canon into .claude/rules/; repeatable and composable, "
|
|
464
|
+
f"e.g. --profile dotnet --profile react (available: {', '.join(list_profiles())})")
|
|
465
|
+
psc.add_argument("--force", action="store_true", help="overwrite existing files")
|
|
466
|
+
psc.add_argument("--verbose", "-v", action="store_true")
|
|
467
|
+
psc.set_defaults(func=cmd_scaffold)
|
|
468
|
+
|
|
469
|
+
pv = sub.add_parser("serve", help="run the shared FastAPI service")
|
|
470
|
+
pv.add_argument("--host", default="0.0.0.0"); pv.add_argument("--port", type=int, default=8080)
|
|
471
|
+
pv.set_defaults(func=cmd_serve)
|
|
472
|
+
|
|
473
|
+
pmcp = sub.add_parser("mcp", help="run the MCP server (stdio) for agent tools")
|
|
474
|
+
pmcp.set_defaults(func=cmd_mcp)
|
|
475
|
+
return p
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def main(argv: list[str] | None = None) -> int:
|
|
479
|
+
args = build_parser().parse_args(argv)
|
|
480
|
+
return args.func(args)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
if __name__ == "__main__":
|
|
484
|
+
raise SystemExit(main())
|