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 +6 -0
- kibsu/__main__.py +139 -0
- kibsu/audit.py +468 -0
- kibsu/check.py +534 -0
- kibsu/config.py +63 -0
- kibsu/discover.py +323 -0
- kibsu/gate.py +720 -0
- kibsu/guide.py +281 -0
- kibsu/index.py +235 -0
- kibsu/install.py +317 -0
- kibsu/learn.py +313 -0
- kibsu/report.py +325 -0
- kibsu/survey.py +173 -0
- kibsu/tokens.py +344 -0
- kibsu-0.1.0.dist-info/METADATA +250 -0
- kibsu-0.1.0.dist-info/RECORD +19 -0
- kibsu-0.1.0.dist-info/WHEEL +5 -0
- kibsu-0.1.0.dist-info/licenses/LICENSE +21 -0
- kibsu-0.1.0.dist-info/top_level.txt +1 -0
kibsu/__init__.py
ADDED
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())
|