kibsu 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.
kibsu/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Kibsu — track what your coding-agent instructions can actually prove.
2
+
3
+ See README.md for the tool's purpose and PROVENANCE.md for how it was built.
4
+ """
5
+
6
+ __version__ = "0.1.0"
kibsu/__main__.py ADDED
@@ -0,0 +1,139 @@
1
+ """Command-line entry point for kibsu.
2
+
3
+ Run as `python -m kibsu <command> ...`. Tier A tools (discover, index, install, tokens,
4
+ survey) and Tier B tools (check, report, guide, audit) are ported and registered below;
5
+ Tier C (learn, gate) is now fully ported too.
6
+ """
7
+
8
+ import argparse
9
+ import importlib
10
+ import sys
11
+
12
+ from . import __version__
13
+
14
+
15
+ def _cmd_version(args):
16
+ print("kibsu %s" % __version__)
17
+ return 0
18
+
19
+
20
+ # Tools that carry their own argparse.ArgumentParser and handle their own -h/--help. Their
21
+ # argv is forwarded untouched (see _forward below) rather than re-parsed by argparse
22
+ # subparsers here: argparse.REMAINDER does not reliably swallow a bare "--help" as the very
23
+ # first token of a subparser with no other options defined, so dispatch for these is
24
+ # done by slicing sys.argv directly, before kibsu's own top-level parser ever runs.
25
+ _FORWARDED = ("discover", "index", "install", "tokens", "check", "report", "guide", "audit", "learn", "gate")
26
+
27
+
28
+ def _forward(subcommand, extra_args):
29
+ """Hand off to a ported tool's own main(), unmodified.
30
+
31
+ discover.py / index.py / install.py / tokens.py each carry their own
32
+ argparse.ArgumentParser and call parse_args() with no explicit argv, so it reads
33
+ sys.argv[1:]. To reuse that logic exactly as ported, sys.argv is swapped for the
34
+ duration of the call to `[subcommand] + extra_args`; each tool's own ArgumentParser has
35
+ prog= set to "python -m kibsu <subcommand>", so its own --help / usage / error output is
36
+ correct regardless of what sys.argv[0] happens to be under `python -m kibsu`.
37
+ """
38
+ module = importlib.import_module("." + subcommand, __package__)
39
+ old_argv = sys.argv
40
+ try:
41
+ sys.argv = [subcommand] + list(extra_args)
42
+ return module.main()
43
+ finally:
44
+ sys.argv = old_argv
45
+
46
+
47
+ def _cmd_survey(args):
48
+ # survey.py has no argparse of its own (see that file) - it reads sys.argv[1] directly
49
+ # as an optional local-repo path, and unconditionally clones + audits ten public repos
50
+ # on every real run regardless of that argument. So --help is handled entirely by THIS
51
+ # subparser (add_help defaults to True below) and never reaches survey.main() at all;
52
+ # that is deliberate, not an oversight, and keeps `--help` free of network side effects.
53
+ from . import survey
54
+ old_argv = sys.argv
55
+ try:
56
+ sys.argv = ["survey", args.local] if args.local else ["survey"]
57
+ survey.main()
58
+ finally:
59
+ sys.argv = old_argv
60
+ return 0
61
+
62
+
63
+ # Subcommand registry: name -> (help text, handler(args) -> exit code). Only used for
64
+ # commands NOT in _FORWARDED - those are dispatched earlier, in main(), by slicing argv.
65
+ _SUBCOMMANDS = {
66
+ "version": ("print the installed kibsu version", _cmd_version),
67
+ "discover": ("what is configured in a repo, and what actually runs", None),
68
+ "index": ("build a deterministic markdown index with a derived taxonomy", None),
69
+ "install": ("wire the check gate to git commit, reversibly (needs the check tool - "
70
+ "see its own --help)", None),
71
+ "tokens": ("model-tier subagent guard, cost ledger, and spend report", None),
72
+ "survey": ("clone public agent-instruction repos, audit each, print the distribution",
73
+ _cmd_survey),
74
+ "check": ("check the repo against its own index - the pre-commit gate (needs an index - "
75
+ "see the index tool)", None),
76
+ "report": ("read-only readiness report: what an agent cannot do here yet", None),
77
+ "guide": ("what an agent actually has to remember, vs. what a mechanism enforces", None),
78
+ "audit": ("measure the checkable:claimable ratio of an agent skill set", None),
79
+ "learn": ("does the shared knowledge base still tell the truth - dangling links, rotted "
80
+ "citations, orphans", None),
81
+ "gate": ("the commit gate: runs the commands configured under \"gates\" in .kibsu.json and "
82
+ "blocks only on a NEW finding (needs a baseline - see its own --help)", None),
83
+ }
84
+
85
+
86
+ def build_parser():
87
+ parser = argparse.ArgumentParser(
88
+ prog="kibsu",
89
+ description=(
90
+ "Kibsu reads your coding-agent instructions (AGENTS.md, CLAUDE.md, "
91
+ "skills, memory) and reports which of them can actually be "
92
+ "verified from the repository."
93
+ ),
94
+ )
95
+ parser.add_argument(
96
+ "--version",
97
+ action="version",
98
+ version="kibsu %s" % __version__,
99
+ )
100
+
101
+ subparsers = parser.add_subparsers(dest="command", metavar="<command>")
102
+
103
+ for name, (help_text, _handler) in _SUBCOMMANDS.items():
104
+ if name == "survey":
105
+ sp = subparsers.add_parser(name, help=help_text)
106
+ sp.add_argument(
107
+ "local",
108
+ nargs="?",
109
+ default=None,
110
+ help="optional local repo to audit for comparison (this still clones and "
111
+ "audits public repos over the network regardless)",
112
+ )
113
+ else:
114
+ # For _FORWARDED names this subparser only exists so `python -m kibsu --help`
115
+ # lists them; real invocations are intercepted in main() before parse_args runs.
116
+ subparsers.add_parser(name, help=help_text)
117
+
118
+ return parser
119
+
120
+
121
+ def main(argv=None):
122
+ raw = sys.argv[1:] if argv is None else list(argv)
123
+
124
+ if raw and raw[0] in _FORWARDED:
125
+ return _forward(raw[0], raw[1:])
126
+
127
+ parser = build_parser()
128
+ args = parser.parse_args(raw)
129
+
130
+ if not args.command:
131
+ parser.print_help()
132
+ return 0
133
+
134
+ _help_text, handler = _SUBCOMMANDS[args.command]
135
+ return handler(args)
136
+
137
+
138
+ if __name__ == "__main__":
139
+ sys.exit(main())
kibsu/audit.py ADDED
@@ -0,0 +1,468 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ skill-audit v0.3.0 - how much of an agent instruction set can actually be checked?
5
+
6
+ An instruction is CHECKABLE if a reviewer could tell from the repo alone whether it happened: it
7
+ runs a command, produces or edits a named file, or is a tick-box. It is CLAIMABLE if the only
8
+ evidence is the agent saying so.
9
+
10
+ v0.3.0 adds the DOCTRINE genre. v0.2.0 added the rest.
11
+ GENRE persona / procedure / reference / DOCTRINE / mixed. Only PROCEDURE units are fairly
12
+ judged on checkability. A role description promises nothing. A DOCTRINE - "restate the
13
+ intent", "name the hidden assumption" - produces better judgement, not a file, so it
14
+ scores 0% by construction and that says nothing about its worth. Without this genre
15
+ the tool defames the best-written skills in a collection: it rated one at 0/22 and
16
+ called it the most claimable unit in the set, while that skill's own transcripts
17
+ showed 47 uses across 13 sessions. Scoring a doctrine on checkability is a category
18
+ error, and it is the easiest rebuttal to this whole measurement - so the tool makes
19
+ the split itself.
20
+ --artifacts Extract the files a skill MANDATES, then look for them in the working tree and in
21
+ full git history. A mandated artifact with zero instances, ever, is a phantom: an
22
+ instruction no model has ever been caught skipping.
23
+
24
+ DESIGN BIAS: every ambiguous instruction counts as CHECKABLE. Reported ratios are CEILINGS.
25
+ Dependency-free, Python 3.8+. Reads only - never executes anything it scans.
26
+
27
+ EXIT CODES
28
+ 0 the audit ran to completion and printed its measurements (text or --json). A low
29
+ checkable:claimable ratio, however low, never changes this code - this tool measures, it
30
+ does not pass/fail.
31
+ 1 no .md files were found under <dir> - there was nothing to audit. The only other code this
32
+ tool returns.
33
+
34
+ python -m kibsu audit <dir> [--json] [--definitions] [--artifacts] [--limit N]
35
+ """
36
+ import argparse, io, json, os, re, subprocess, sys
37
+
38
+ if hasattr(sys.stdout, "reconfigure"):
39
+ sys.stdout.reconfigure(encoding="utf-8")
40
+
41
+ VERSION = "0.3.0"
42
+ INCLUDE_ARCHIVED = False
43
+
44
+ RUNNABLE_LANGS = {"bash", "sh", "shell", "console", "powershell", "ps1", "pwsh", "zsh",
45
+ "python", "py", "cmd", "bat", "sql", "javascript", "js", "node", "ruby", "go"}
46
+ BINARIES = r"(?:python3?|py|pip3?|git|npm|npx|pnpm|yarn|node|deno|bun|pytest|tox|make|cargo|go|dotnet|" \
47
+ r"docker|kubectl|terraform|aws|az|gcloud|gh|curl|wget|jq|rg|grep|sed|awk|find|ls|cat|" \
48
+ r"pwsh|powershell|bash|sh|sqlcmd|psql|mysql|ruby|rake|mvn|gradle|java|tsc|eslint|prettier|" \
49
+ r"ruff|black|mypy|vitest|jest|cypress|playwright|bundle|composer|php|dart|flutter|swift)"
50
+ INLINE_CMD = re.compile(r"`\s*" + BINARIES + r"\b[^`]*`")
51
+ BARE_CMD = re.compile(r"^\s*(?:[-*+]\s+|\d+[.)]\s+)?" + BINARIES + r"\b\s+\S")
52
+ CHECKBOX = re.compile(r"^\s*(?:[-*+]\s*)?(?:\[[ xX]\]|[□☐☑☒])")
53
+ PATHY = re.compile(r"[`\"']?[\w./\\*-]+\.(?:md|json|ya?ml|py|ps1|sh|js|ts|tsx|jsx|sql|toml|ini|cfg|txt|csv|lock)\b")
54
+ EXITY = re.compile(r"\b(exit code|exit 0|exit 1|non-zero|returns? 0|must pass|passes|green|fails? loud|"
55
+ r"assert|verify that|diff|git status|git log|numstat)\b", re.I)
56
+ MODALS = re.compile(r"\b(MUST|SHOULD|ALWAYS|NEVER|REQUIRED|DO NOT|DON'T|MANDATORY|"
57
+ r"must|never|always|do not|don't|ensure|make sure)\b")
58
+ VERBS = (r"add|append|apply|archive|ask|assert|bump|build|call|change|check|clean|clear|close|commit|"
59
+ r"compare|confirm|copy|create|declare|delete|deploy|describe|do|document|edit|enable|ensure|"
60
+ r"enumerate|execute|explain|export|extract|fetch|fill|find|finish|fix|follow|generate|get|give|"
61
+ r"go|grep|handle|identify|implement|include|insert|inspect|install|invoke|keep|list|load|log|"
62
+ r"look|maintain|make|mark|measure|merge|move|name|note|open|output|parse|pass|perform|pick|"
63
+ r"place|prefer|prepare|print|produce|prove|pull|push|put|read|record|refresh|regenerate|"
64
+ r"register|remove|rename|render|repeat|replace|report|require|reset|resolve|restate|restore|"
65
+ r"return|review|rewrite|run|save|scan|search|select|send|set|show|skip|sort|split|stamp|start|"
66
+ r"state|stop|store|summarise|summarize|surface|sweep|switch|sync|tag|take|tell|test|track|"
67
+ r"translate|treat|trigger|update|upgrade|use|validate|verify|walk|write")
68
+ IMPERATIVE = re.compile(r"^\s*(?:[-*+>]\s+|\d+[.)]\s+|\|\s*)?(?:" + VERBS + r")\b", re.I)
69
+
70
+ # ---- genre signals -------------------------------------------------------------------------
71
+ PERSONA_RE = [re.compile(p, re.I) for p in (
72
+ r"^\s*you are (a|an|the)\b", r"\byour expertise\b", r"\byou specialize\b", r"\byou specialise\b",
73
+ r"^\s*as an? [\w\s-]{3,30}(,|you)", r"\byour role is\b", r"\byou are responsible for\b",
74
+ r"\bexpert (in|at)\b", r"\byears of experience\b", r"\byou excel at\b", r"\byou have deep\b")]
75
+ # --- doctrine signals -------------------------------------------------------------------
76
+ # A DOCTRINE tells the agent how to THINK, not what to DO. Scoring it on checkability is a
77
+ # category error: "name the hidden assumption" produces better judgement, not a file, so it
78
+ # will always score 0% and that says nothing about its worth. Without this genre the tool
79
+ # systematically defames the best-written skills in any collection - it scored a skill whose
80
+ # own transcripts show 47 uses at 0/22 and called it the most claimable unit in the set.
81
+ DOCTRINE_RE = [re.compile(p, re.I) for p in (
82
+ r"\b(ask yourself|before you (act|build|answer|start|begin)|the temptation|resist the|"
83
+ r"instead of|rather than|do not assume|question the|challenge the|hidden assumption|"
84
+ r"think (about|through|twice)|notice (when|that)|judgement call|judgment call|"
85
+ r"when in doubt|it is not enough|the point is not|second-guess)\b",
86
+ r"\b(anti-?pattern|red flag|failure mode|smell test|rationalis\w*|rationaliz\w*|"
87
+ r"blind spot|assumption)\b",
88
+ r"\bnot\b[^.\n]{0,45}\bbut\b", # contrast construction: "not X, but Y"
89
+ )]
90
+ # The load-bearing discriminator: what does the INSTRUCTION ask for? An epistemic instruction
91
+ # asks the agent to think differently ("restate the intent", "name the hidden assumption").
92
+ # An action instruction asks it to change the world ("run the checker", "update the index").
93
+ # Document furniture - numbered headings, tables - does not distinguish the two: ten numbered
94
+ # PRINCIPLES look identical to ten numbered STEPS. The verb does distinguish them.
95
+ EPISTEMIC = re.compile(
96
+ r"^\s*(?:[-*+>]\s+|\d+[.)]\s+|\*\*)?(?:restate|question|challenge|assume|notice|consider|"
97
+ r"judge|doubt|think|understand|interpret|weigh|distinguish|recogni[sz]e|resist|avoid|prefer|"
98
+ r"name|surface|separate|reframe|suspect|verify before|ask)", re.I)
99
+ STEPY = re.compile(r"^\s*(?:#+\s*)?(?:step\s*)?\d+[.)]\s+\S", re.I)
100
+ SEQ = re.compile(r"^\s*(?:#+\s*)?(first|then|next|finally|afterwards|before you|after you|"
101
+ r"once you|begin by|start by)\b", re.I)
102
+ TABLE = re.compile(r"^\s*\|.*\|\s*$")
103
+ ARTIFACT_VERB = re.compile(
104
+ r"\b(creat|writ|wrote|append|produc|generat|sav|emit|updat|record|log|output|add|regenerat|"
105
+ r"stamp|bump|export)\w*\b", re.I)
106
+ FILE_TOKEN = re.compile(
107
+ r"`([^`\s]*?[\w*\[\]{}-]+\.(?:md|json|ya?ml|py|ps1|sh|js|ts|sql|toml|ini|cfg|txt|csv))`")
108
+
109
+ # --- phantom-scope filters (fix for the false-positive class) --------------------------------
110
+ # A mandated artifact only counts as a PHANTOM if the skill claims it is produced INSIDE the repo
111
+ # the skill lives in. Skills that scaffold the *user's* project legitimately name files that will
112
+ # never exist here, and scoring them was wrong.
113
+ SCAFFOLD_SKILL = re.compile(
114
+ r"\b(scaffold|boilerplate|starter|template|generator|generate a new|create a new project|"
115
+ r"new project|project init|bootstrap a)\w*\b", re.I)
116
+ USER_SCOPE_LINE = re.compile(
117
+ r"\b(your|the user'?s?|target|destination|output|new)\s+"
118
+ r"(project|repo(sitory)?|app|application|codebase|directory|folder)\b|"
119
+ r"\bin your\b|\bfor the user\b|\bthe generated\b", re.I)
120
+
121
+ DEFINITIONS = """
122
+ METRIC DEFINITIONS (contest them - that is the point)
123
+
124
+ instruction a non-heading, non-code line telling the agent to do something: opens with an
125
+ imperative verb, or carries a modal (MUST / NEVER / ALWAYS / DO NOT / ensure).
126
+
127
+ CHECKABLE a reviewer could confirm it happened from the repo alone:
128
+ tick-box | runnable command | names a concrete file | exit code / diff / assertion
129
+ CLAIMABLE everything else. Only evidence is the agent's own report.
130
+
131
+ GENRE persona - describes WHO the agent is ("You are a senior Rust engineer...").
132
+ Promises nothing, so checkability is NOT a fair measure of it.
133
+ procedure - describes WHAT TO DO, in order. Checkability applies fully.
134
+ doctrine - describes HOW TO THINK ("name the hidden assumption before building").
135
+ Produces judgement, not artifacts. Checkability does NOT apply, and a
136
+ 0% here is the genre working as intended, not a defect.
137
+ reference - lookup material: tables, definitions, options.
138
+ mixed - no signal dominates.
139
+ NOTE: procedure is weighted toward EXECUTABLE signals (commands, checkboxes), not
140
+ merely numbered ones - ten numbered principles are not a ten-step procedure.
141
+
142
+ phantom an artifact a skill mandates that has never existed in the working tree OR in git
143
+ history. Requires full history; a shallow clone reports UNKNOWN, never zero.
144
+
145
+ BIAS ambiguity resolves to CHECKABLE. Reported ratios are ceilings.
146
+ """
147
+
148
+
149
+ def strip_frontmatter(t):
150
+ if t.startswith("---"):
151
+ e = t.find("\n---", 3)
152
+ if e != -1:
153
+ return t[e + 4:], t[3:e]
154
+ return t, ""
155
+
156
+
157
+ def classify(sig, lines):
158
+ """Explicit, contestable. Densities per 100 lines so long files are not penalised.
159
+
160
+ PROCEDURE is weighted toward EXECUTABLE signals - commands and checkboxes - not merely
161
+ numbered ones. A doctrine with ten numbered principles is not a ten-step procedure, and
162
+ an earlier version of this function classified exactly that way.
163
+ """
164
+ k = 100.0 / max(1, lines)
165
+ persona = sig["persona_hits"] * k * 3.0
166
+ procedure = (sig["steps"] * 0.5 + sig["checkboxes"] * 2 + sig["runnable_fences"] * 3
167
+ + sig["seq"]) * k
168
+ reference = sig["tables"] * k * 0.7
169
+ doctrine = sig["doctrine_hits"] * k * 1.6
170
+ scores = dict(persona=round(persona, 2), procedure=round(procedure, 2),
171
+ reference=round(reference, 2), doctrine=round(doctrine, 2))
172
+ best = max(persona, procedure, reference, doctrine)
173
+ if best < 0.8:
174
+ return "mixed", scores
175
+ for name, val in (("doctrine", doctrine), ("procedure", procedure),
176
+ ("persona", persona), ("reference", reference)):
177
+ if val == best:
178
+ return name, scores
179
+ return "mixed", scores
180
+
181
+
182
+ def analyse(text):
183
+ body, fm = strip_frontmatter(text)
184
+ m = re.search(r"^\s*genre\s*:\s*([A-Za-z]+)\s*(?:#.*)?$", fm, re.M)
185
+ fm_genre = m.group(1) if m else None
186
+ lines = body.split("\n")
187
+ o = dict(lines=len(lines), fences=0, runnable_fences=0, checkboxes=0, instructions=0,
188
+ checkable=0, claimable=0, inline_cmds=0, steps=0, seq=0, tables=0,
189
+ persona_hits=0, doctrine_hits=0, epistemic=0, action=0, mandated=[])
190
+ if any(r.search(fm) for r in PERSONA_RE):
191
+ o["persona_hits"] += 2
192
+ in_fence, lang = False, ""
193
+ for ln in lines:
194
+ f = re.match(r"^\s*```+\s*([\w+-]*)", ln)
195
+ if f:
196
+ if not in_fence:
197
+ in_fence, lang = True, (f.group(1) or "").lower()
198
+ o["fences"] += 1
199
+ if lang in RUNNABLE_LANGS:
200
+ o["runnable_fences"] += 1
201
+ else:
202
+ in_fence, lang = False, ""
203
+ continue
204
+ if in_fence:
205
+ continue
206
+ if TABLE.match(ln):
207
+ o["tables"] += 1
208
+ if any(r.search(ln) for r in PERSONA_RE):
209
+ o["persona_hits"] += 1
210
+ for r in DOCTRINE_RE:
211
+ o["doctrine_hits"] += len(r.findall(ln))
212
+ if STEPY.match(ln):
213
+ o["steps"] += 1
214
+ if SEQ.match(ln):
215
+ o["seq"] += 1
216
+ if ln.lstrip().startswith("#"):
217
+ continue
218
+ n_inline = len(INLINE_CMD.findall(ln))
219
+ o["inline_cmds"] += n_inline
220
+ is_box = bool(CHECKBOX.match(ln))
221
+ if is_box:
222
+ o["checkboxes"] += 1
223
+ if not ln.strip():
224
+ continue
225
+ if not (is_box or IMPERATIVE.match(ln) or MODALS.search(ln)):
226
+ continue
227
+ o["instructions"] += 1
228
+ o["epistemic" if EPISTEMIC.match(ln) else "action"] += 1
229
+ checkable = (is_box or n_inline > 0 or bool(BARE_CMD.match(ln))
230
+ or bool(PATHY.search(ln)) or bool(EXITY.search(ln)))
231
+ o["checkable" if checkable else "claimable"] += 1
232
+ if ARTIFACT_VERB.search(ln):
233
+ for m in FILE_TOKEN.findall(ln):
234
+ # Strip a leading "./" as a PREFIX. lstrip("./\\") takes a character SET and
235
+ # eats the dot of ".agents/skills/x", turning a real path into one that resolves
236
+ # nowhere - so the artifact is silently dropped and reads as "not mandated".
237
+ # See memory/learnings/a-checker-that-guesses-the-base-path-cries-wolf.md rule 3.
238
+ tok = m.strip().replace("\\", "/")
239
+ while tok.startswith("./"):
240
+ tok = tok[2:]
241
+ if tok and len(tok) < 90:
242
+ o["mandated"].append({"tok": tok, "line": ln.strip()[:200]})
243
+ seen = set()
244
+ uniq = []
245
+ for m in o["mandated"]:
246
+ if m["tok"] not in seen:
247
+ seen.add(m["tok"]); uniq.append(m)
248
+ o["mandated"] = uniq
249
+ o["scaffolding"] = bool(SCAFFOLD_SKILL.search(fm) or SCAFFOLD_SKILL.search(body[:1500]))
250
+ detected, o["genre_scores"] = classify(o, o["lines"])
251
+ # DECLARATION BEATS DETECTION. Auto-detecting "doctrine" reliably proved beyond this tool:
252
+ # ten numbered PRINCIPLES are structurally identical to ten numbered STEPS, and every
253
+ # heuristic tried was really the author's prior belief in regex form. So the skill author
254
+ # states the genre and the tool reports it - while still detecting independently and
255
+ # FLAGGING disagreement, so a declaration cannot quietly buy a better score.
256
+ declared = (fm_genre or "").strip().lower()
257
+ if declared in ("procedure", "doctrine", "persona", "reference", "mixed"):
258
+ o["genre"], o["genre_source"] = declared, "declared"
259
+ o["genre_conflict"] = (declared != detected)
260
+ else:
261
+ o["genre"], o["genre_source"] = detected, "detected"
262
+ o["genre_conflict"] = False
263
+ o["genre_detected"] = detected
264
+ return o
265
+
266
+
267
+ META = {"readme", "contributing", "license", "licence", "changelog", "code_of_conduct", "security",
268
+ "index", "install", "installation", "faq", "authors", "notice", "roadmap", "support",
269
+ "governance", "history", "upgrading", "migration", "todo"}
270
+ INSTR_DIRS = {"skills", "agents", "subagents", "commands", "rules", "prompts", "plugins",
271
+ ".claude", ".cursor", "categories"}
272
+
273
+
274
+ def _walk(root):
275
+ for dp, dn, fn in os.walk(root):
276
+ dn[:] = [d for d in dn if d not in (".git", "node_modules", "__pycache__", ".venv", "dist", "build")
277
+ and (INCLUDE_ARCHIVED or not d.startswith("_"))]
278
+ yield dp, fn
279
+
280
+
281
+ def find_skills(root):
282
+ hits = [os.path.join(dp, f) for dp, fn in _walk(root) for f in fn if f.lower() == "skill.md"]
283
+ if hits:
284
+ return hits, "SKILL.md"
285
+ ok = lambda f: f.lower().endswith(".md") and os.path.splitext(f)[0].lower() not in META
286
+ parts = lambda dp: {p.lower() for p in os.path.relpath(dp, root).replace("\\", "/").split("/")}
287
+ hits = [os.path.join(dp, f) for dp, fn in _walk(root) for f in fn if ok(f) and (parts(dp) & INSTR_DIRS)]
288
+ if hits:
289
+ return hits, "instruction-dir/*.md"
290
+ return [os.path.join(dp, f) for dp, fn in _walk(root) for f in fn if ok(f)], "*.md (no instruction dir)"
291
+
292
+
293
+ # ---- artifacts ----------------------------------------------------------------------------
294
+ def git_root(path):
295
+ p = subprocess.run(["git", "rev-parse", "--show-toplevel"], cwd=path,
296
+ capture_output=True, text=True)
297
+ return p.stdout.strip() if p.returncode == 0 else None
298
+
299
+
300
+ def history_paths(root):
301
+ """All paths ever touched, plus whether history is shallow (which makes zero meaningless)."""
302
+ shallow = os.path.isfile(os.path.join(root, ".git", "shallow"))
303
+ p = subprocess.run(["git", "log", "--all", "--pretty=format:", "--name-only"], cwd=root,
304
+ capture_output=True, text=True, encoding="utf-8", errors="replace")
305
+ paths = {l.strip() for l in p.stdout.split("\n") if l.strip()} if p.returncode == 0 else set()
306
+ return paths, shallow
307
+
308
+
309
+ def tree_paths(root):
310
+ out = set()
311
+ for dp, dn, fn in os.walk(root):
312
+ dn[:] = [d for d in dn if d != ".git"]
313
+ for f in fn:
314
+ out.add(os.path.relpath(os.path.join(dp, f), root).replace("\\", "/"))
315
+ return out
316
+
317
+
318
+ def glob_re(tok):
319
+ esc = re.escape(tok).replace(r"\*", "[^/]*").replace(r"\?", ".")
320
+ return re.compile(r"(^|/)" + esc + r"$", re.I)
321
+
322
+
323
+ def check_artifacts(root, rows):
324
+ gr = git_root(root)
325
+ hist, shallow = (history_paths(gr) if gr else (set(), False))
326
+ tree = tree_paths(gr or root)
327
+ dirs = {os.path.dirname(p) for p in (tree | hist)}
328
+ dirs.discard("")
329
+ res = []
330
+ for r in rows:
331
+ for m in r["mandated"]:
332
+ tok, line = m["tok"], m["line"]
333
+ # --- scope filter: is this artifact claimed to live in THIS repo? ---
334
+ reason = None
335
+ if r.get("scaffolding"):
336
+ reason = "skill scaffolds the user's project"
337
+ elif USER_SCOPE_LINE.search(line):
338
+ reason = "line refers to the user's project, not this repo"
339
+ else:
340
+ pre = os.path.dirname(tok.replace("\\", "/"))
341
+ if pre and not any(d == pre or d.endswith("/" + pre) for d in dirs):
342
+ reason = "path prefix '%s/' does not exist in this repo" % pre
343
+ rx = glob_re(tok)
344
+ hit_tree = any(rx.search(p) for p in tree)
345
+ hit_hist = any(rx.search(p) for p in hist)
346
+ res.append(dict(skill=r["skill"], artifact=tok, in_tree=hit_tree, in_history=hit_hist,
347
+ in_scope=(reason is None), out_of_scope_reason=reason,
348
+ phantom=(reason is None and not hit_tree and not hit_hist)))
349
+ return res, shallow, bool(gr), len(tree | hist)
350
+
351
+
352
+ def main():
353
+ ap = argparse.ArgumentParser(prog="python -m kibsu audit", description="Measure the checkable:claimable ratio of an agent skill set.")
354
+ ap.add_argument("path")
355
+ ap.add_argument("--json", action="store_true")
356
+ ap.add_argument("--definitions", action="store_true")
357
+ ap.add_argument("--artifacts", action="store_true", help="find mandated artifacts and hunt for them")
358
+ ap.add_argument("--limit", type=int, default=12)
359
+ ap.add_argument("--include-archived", action="store_true")
360
+ a = ap.parse_args()
361
+ global INCLUDE_ARCHIVED
362
+ INCLUDE_ARCHIVED = a.include_archived
363
+ if a.definitions:
364
+ print(DEFINITIONS)
365
+
366
+ root = os.path.abspath(a.path)
367
+ files, mode = find_skills(root)
368
+ if not files:
369
+ print("no .md found under " + root)
370
+ return 1
371
+ rows = []
372
+ for p in sorted(files):
373
+ try:
374
+ t = io.open(p, encoding="utf-8", errors="replace").read()
375
+ except Exception:
376
+ continue
377
+ r = analyse(t)
378
+ rel = os.path.relpath(p, root).replace("\\", "/")
379
+ r["skill"] = (os.path.dirname(rel) or rel) if mode == "SKILL.md" else rel
380
+ rows.append(r)
381
+
382
+ def agg(rs):
383
+ t = {k: sum(r[k] for r in rs) for k in ("lines", "instructions", "checkable", "claimable",
384
+ "runnable_fences", "checkboxes")}
385
+ t["units"] = len(rs)
386
+ t["pct"] = (100.0 * t["checkable"] / t["instructions"]) if t["instructions"] else 0.0
387
+ t["zero"] = len([r for r in rs if r["checkable"] == 0 and r["instructions"] > 0])
388
+ return t
389
+
390
+ ALL, PROC = agg(rows), agg([r for r in rows if r["genre"] == "procedure"])
391
+ by_genre = {}
392
+ for g in ("procedure", "doctrine", "persona", "reference", "mixed"):
393
+ sub = [r for r in rows if r["genre"] == g]
394
+ if sub:
395
+ by_genre[g] = agg(sub)
396
+
397
+ arts, shallow, has_git, _ = ([], False, False, 0)
398
+ if a.artifacts:
399
+ arts, shallow, has_git, _ = check_artifacts(root, rows)
400
+
401
+ if a.json:
402
+ print(json.dumps(dict(version=VERSION, root=root, mode=mode, all=ALL, procedure_only=PROC,
403
+ by_genre=by_genre, artifacts=arts, history_shallow=shallow,
404
+ has_git=has_git, skills=rows), indent=2))
405
+ return 0
406
+
407
+ print("\nskill-audit v%s %s" % (VERSION, root))
408
+ print(" %d units (%s), %s lines\n" % (ALL["units"], mode, format(ALL["lines"], ",")))
409
+ print(" %-11s %6s %8s %8s %8s %9s" % ("genre", "units", "instr", "check", "CHECK%", "0-check"))
410
+ for g in ("procedure", "doctrine", "persona", "reference", "mixed"):
411
+ if g in by_genre:
412
+ t = by_genre[g]
413
+ print(" %-11s %6d %8s %8s %7.1f%% %6d/%-3d" % (g, t["units"], format(t["instructions"], ","),
414
+ format(t["checkable"], ","), t["pct"], t["zero"], t["units"]))
415
+ print(" " + "-" * 56)
416
+ print(" %-11s %6d %8s %8s %7.1f%% %6d/%-3d" % ("ALL", ALL["units"], format(ALL["instructions"], ","),
417
+ format(ALL["checkable"], ","), ALL["pct"], ALL["zero"], ALL["units"]))
418
+ print("\n >> HEADLINE (procedure units only - the fair comparison): %.1f%% checkable"
419
+ % PROC["pct"] if PROC["units"] else "\n >> no procedure-genre units found")
420
+
421
+ # A declared genre must never quietly buy a better score, so detection still runs and any
422
+ # disagreement is printed. Silent trust would make the declaration a loophole.
423
+ dec = [r for r in rows if r.get("genre_source") == "declared"]
424
+ conf = [r for r in dec if r.get("genre_conflict")]
425
+ if dec:
426
+ print("\n genre declared in frontmatter: %d unit(s), %d disagreeing with detection"
427
+ % (len(dec), len(conf)))
428
+ for r in conf[:6]:
429
+ print(" %-32s declared=%-9s detected=%s"
430
+ % (r["skill"][:32], r["genre"], r["genre_detected"]))
431
+
432
+ if a.artifacts:
433
+ print("\n --- mandated artifacts ---")
434
+ if not has_git:
435
+ print(" not a git repo: history check unavailable (tree only)")
436
+ elif shallow:
437
+ print(" SHALLOW CLONE: git history unavailable. 'never existed' cannot be established;")
438
+ print(" phantom counts below are UNKNOWN, not zero. Re-clone with full history to use this.")
439
+ inn = [x for x in arts if x["in_scope"]]
440
+ out = [x for x in arts if not x["in_scope"]]
441
+ ph = [x for x in inn if x["phantom"]]
442
+ print(" %d references, %d distinct" % (len(arts), len({x["artifact"] for x in arts})))
443
+ print(" in-scope (claimed to live in THIS repo): %d out-of-scope: %d" % (len(inn), len(out)))
444
+ if has_git and not shallow:
445
+ print(" PHANTOM, in-scope (never in tree, never in any commit): %d of %d (%.0f%%)"
446
+ % (len(ph), len(inn), 100.0 * len(ph) / max(1, len(inn))))
447
+ for x in ph[:a.limit]:
448
+ print(" %-34s mandated by %s" % (x["artifact"][:34], x["skill"][:40]))
449
+ if out:
450
+ print(" excluded as user-project scope (%d), sample:" % len(out))
451
+ for x in out[:3]:
452
+ print(" %-30s %s" % (x["artifact"][:30], x["out_of_scope_reason"][:56]))
453
+ elif arts:
454
+ print(" history unavailable - phantom status UNKNOWN, not zero")
455
+
456
+ worst = sorted([r for r in rows if r["instructions"] >= 5 and r["genre"] == "procedure"],
457
+ key=lambda r: (r["checkable"] / r["instructions"], -r["instructions"]))[:a.limit]
458
+ if worst:
459
+ print("\n most claimable PROCEDURE units (>=5 instructions):")
460
+ for r in worst:
461
+ print(" %-44s %5d instr %5d check %4.0f%%" % (r["skill"][:44], r["instructions"],
462
+ r["checkable"], 100.0 * r["checkable"] / r["instructions"]))
463
+ print()
464
+ return 0
465
+
466
+
467
+ if __name__ == "__main__":
468
+ sys.exit(main())