ktl 0.64.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.
- ktl/.claude-plugin/marketplace.json +16 -0
- ktl/.claude-plugin/plugin.json +10 -0
- ktl/README.md +59 -0
- ktl/__init__.py +1 -0
- ktl/__main__.py +5 -0
- ktl/cli.py +390 -0
- ktl/gh.py +1746 -0
- ktl/skills/ktl-curator/SKILL.md +117 -0
- ktl/skills/ktl-curator/references/domain-bundle.md +98 -0
- ktl/skills/ktl-curator/references/domain-vocabularies.md +60 -0
- ktl/skills/ktl-curator/references/portability.md +38 -0
- ktl/skills/ktl-curator/references/queries.md +103 -0
- ktl/skills/ktl-curator/references/review-session.md +288 -0
- ktl/skills/ktl-curator/references/trust-fields.md +138 -0
- ktl/skills/ktl-docent/SKILL.md +108 -0
- ktl/skills/ktl-docent/references/answering.md +110 -0
- ktl/skills/ktl-docent/references/feedback.md +78 -0
- ktl/skills/ktl-docent/references/obsidian.md +38 -0
- ktl/skills/ktl-docent/scripts/knowledge-feedback.sh +199 -0
- ktl/skills/ktl-docent/scripts/knowledge-report.sh +2085 -0
- ktl/skills/ktl-librarian/SKILL.md +145 -0
- ktl/skills/ktl-librarian/references/agent-skills.md +41 -0
- ktl/skills/ktl-librarian/references/audit.md +31 -0
- ktl/skills/ktl-librarian/references/bootstrap.md +23 -0
- ktl/skills/ktl-librarian/references/domain-vocabulary.md +154 -0
- ktl/skills/ktl-librarian/references/excerpts.md +105 -0
- ktl/skills/ktl-librarian/references/golden-rules.md +107 -0
- ktl/skills/ktl-librarian/references/ingest.md +46 -0
- ktl/skills/ktl-librarian/references/patch.md +143 -0
- ktl/skills/ktl-librarian/references/portability.md +24 -0
- ktl/skills/ktl-librarian/references/retellings.md +62 -0
- ktl/skills/ktl-librarian/references/scheduled-task.md +61 -0
- ktl/skills/ktl-prose/SKILL.md +190 -0
- ktl/skills/ktl-prose/assets/voa-word-book.txt +1642 -0
- ktl/skills/ktl-prose/references/limits.md +70 -0
- ktl/skills/ktl-prose/references/other-files.md +83 -0
- ktl/skills/ktl-prose/references/rules.md +359 -0
- ktl/skills/ktl-prose/references/sources.md +108 -0
- ktl/skills/ktl-prose/references/young-readers.md +167 -0
- ktl/skills/ktl-prose/scripts/prose-check.py +2597 -0
- ktl/skills/ktl-sidecar/SKILL.md +185 -0
- ktl/skills/ktl-sidecar/references/automation.md +473 -0
- ktl/skills/ktl-sidecar/references/gate.md +154 -0
- ktl/skills/ktl-sidecar/references/m365.md +50 -0
- ktl/skills/ktl-sidecar/references/portability.md +122 -0
- ktl/skills/ktl-sidecar/references/prerequisites.md +32 -0
- ktl/skills/ktl-sidecar/references/repository-settings.json +457 -0
- ktl/skills/ktl-sidecar/references/repository-settings.md +66 -0
- ktl/skills/ktl-sidecar/templates/README.md +76 -0
- ktl/skills/ktl-sidecar/templates/domain.yaml +129 -0
- ktl/skills/ktl-sidecar/templates/gitattributes +7 -0
- ktl/skills/ktl-sidecar/templates/github/knowledge-federation.yaml +559 -0
- ktl/skills/ktl-sidecar/templates/github/knowledge-ingest-pull-request.md +12 -0
- ktl/skills/ktl-sidecar/templates/github/knowledge-ingest.yaml +330 -0
- ktl/skills/ktl-sidecar/templates/github/knowledge-librarian.yaml +778 -0
- ktl/skills/ktl-sidecar/templates/github/knowledge-registrar.yaml +584 -0
- ktl/skills/ktl-sidecar/templates/github/knowledge-release.yaml +622 -0
- ktl/skills/ktl-sidecar/templates/github/knowledge-sync-pull-request.md +9 -0
- ktl/skills/ktl-sidecar/templates/github/knowledge-sync.yaml +692 -0
- ktl/skills/ktl-sidecar/templates/gitignore +37 -0
- ktl/skills/ktl-sidecar/templates/ingest.yaml +75 -0
- ktl/skills/ktl-sidecar/templates/justfile +72 -0
- ktl/skills/ktl-sidecar/templates/knowledge/index.md +22 -0
- ktl/skills/ktl-sidecar/templates/knowledge/log.md +6 -0
- ktl/skills/ktl-sidecar/templates/knowledge/services/example-service-a.md +13 -0
- ktl/skills/ktl-sidecar/templates/knowledge/services/example-service-b.md +14 -0
- ktl/skills/ktl-sidecar/templates/knowledge/services/index.md +4 -0
- ktl/skills/ktl-sidecar/templates/knowledge-sync.manifest +91 -0
- ktl/skills/ktl-sidecar/templates/llms.txt +24 -0
- ktl/skills/ktl-sidecar/templates/m365/knowledge-m365.sh +238 -0
- ktl/skills/ktl-sidecar/templates/m365/ktl-docent-m365.md +112 -0
- ktl/skills/ktl-sidecar/templates/pyproject.toml +22 -0
- ktl/skills/ktl-sidecar/templates/queries.http +54 -0
- ktl/skills/ktl-sidecar/templates/readme-for-ai-agents.md +13 -0
- ktl/skills/ktl-sidecar/templates/scripts/knowledge-apply.py +1600 -0
- ktl/skills/ktl-sidecar/templates/scripts/knowledge-apply.sh +93 -0
- ktl/skills/ktl-sidecar/templates/scripts/knowledge-conventions.py +898 -0
- ktl/skills/ktl-sidecar/templates/scripts/knowledge-conventions.sh +466 -0
- ktl/skills/ktl-sidecar/templates/scripts/knowledge-federate.py +4010 -0
- ktl/skills/ktl-sidecar/templates/scripts/knowledge-feedback.sh +199 -0
- ktl/skills/ktl-sidecar/templates/scripts/knowledge-ingest.py +2970 -0
- ktl/skills/ktl-sidecar/templates/scripts/knowledge-ingest.sh +87 -0
- ktl/skills/ktl-sidecar/templates/scripts/knowledge-librarian.sh +550 -0
- ktl/skills/ktl-sidecar/templates/scripts/knowledge-preflight.sh +553 -0
- ktl/skills/ktl-sidecar/templates/scripts/knowledge-provenance.sh +518 -0
- ktl/skills/ktl-sidecar/templates/scripts/knowledge-report.sh +2085 -0
- ktl/skills/ktl-sidecar/templates/scripts/knowledge-sync.sh +853 -0
- ktl/skills/ktl-website/SKILL.md +138 -0
- ktl/skills/ktl-website/references/customising.md +53 -0
- ktl/skills/ktl-website/references/deploy.md +93 -0
- ktl/skills/ktl-website/references/inputs.md +94 -0
- ktl/skills/ktl-website/references/prerequisites.md +28 -0
- ktl/skills/ktl-website/references/repository-settings.json +50 -0
- ktl/skills/ktl-website/scripts/website-preflight.sh +312 -0
- ktl/skills/ktl-website/templates/github/knowledge-website.yaml +176 -0
- ktl/skills/ktl-website/templates/website/README.md +57 -0
- ktl/skills/ktl-website/templates/website/assets/card.png +0 -0
- ktl/skills/ktl-website/templates/website/assets/css/custom.css +3 -0
- ktl/skills/ktl-website/templates/website/assets/css/site.css +356 -0
- ktl/skills/ktl-website/templates/website/assets/js/graph.js +299 -0
- ktl/skills/ktl-website/templates/website/assets/js/site.js +235 -0
- ktl/skills/ktl-website/templates/website/content/_content.gotmpl +41 -0
- ktl/skills/ktl-website/templates/website/content/browse/_content.gotmpl +15 -0
- ktl/skills/ktl-website/templates/website/content/changelog/_content.gotmpl +40 -0
- ktl/skills/ktl-website/templates/website/content/docs/_content.gotmpl +49 -0
- ktl/skills/ktl-website/templates/website/content/graph.md +4 -0
- ktl/skills/ktl-website/templates/website/content/health.md +4 -0
- ktl/skills/ktl-website/templates/website/content/knowledge/_content.gotmpl +66 -0
- ktl/skills/ktl-website/templates/website/content/schema/_content.gotmpl +34 -0
- ktl/skills/ktl-website/templates/website/gitignore +9 -0
- ktl/skills/ktl-website/templates/website/hugo.toml +158 -0
- ktl/skills/ktl-website/templates/website/layouts/404.html +6 -0
- ktl/skills/ktl-website/templates/website/layouts/_markup/render-blockquote.html +8 -0
- ktl/skills/ktl-website/templates/website/layouts/_markup/render-codeblock-mermaid.html +2 -0
- ktl/skills/ktl-website/templates/website/layouts/_markup/render-heading.html +4 -0
- ktl/skills/ktl-website/templates/website/layouts/_markup/render-image.html +3 -0
- ktl/skills/ktl-website/templates/website/layouts/_markup/render-link.html +5 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/actor.html +4 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/card.html +15 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/concept-card.html +97 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/concept-link.html +6 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/concepts.html +17 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/content.html +2 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/docs-groups.html +62 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/full-url.html +16 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/gendoc.html +30 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/health-tiles.html +21 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/heroes.html +43 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/icon.html +8 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/inline.html +2 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/jsonld.html +8 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/labels.html +9 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/levels.html +40 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/licence.html +9 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/literal.html +8 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/markdown-file.html +23 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/markdown.html +59 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/mirrors.html +28 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/nav-key.html +21 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/neighbourhood.html +51 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/pager.html +15 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/preview.html +3 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/raw.html +26 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/repo-path.html +6 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/route.html +39 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/routes.html +9 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/schema-file.html +17 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/sidebar.html +83 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/slot-name.html +2 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/title.html +9 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/tones.html +8 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/tools.html +27 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/trust.html +16 -0
- ktl/skills/ktl-website/templates/website/layouts/_partials/vendor.html +16 -0
- ktl/skills/ktl-website/templates/website/layouts/baseof.html +119 -0
- ktl/skills/ktl-website/templates/website/layouts/browse/section.html +20 -0
- ktl/skills/ktl-website/templates/website/layouts/changelog/page.html +15 -0
- ktl/skills/ktl-website/templates/website/layouts/changelog/section.html +12 -0
- ktl/skills/ktl-website/templates/website/layouts/graph.html +54 -0
- ktl/skills/ktl-website/templates/website/layouts/health.html +39 -0
- ktl/skills/ktl-website/templates/website/layouts/home.html +36 -0
- ktl/skills/ktl-website/templates/website/layouts/home.llms.txt +32 -0
- ktl/skills/ktl-website/templates/website/layouts/home.llmsfull.txt +20 -0
- ktl/skills/ktl-website/templates/website/layouts/page.atom.xml +28 -0
- ktl/skills/ktl-website/templates/website/layouts/page.html +10 -0
- ktl/skills/ktl-website/templates/website/layouts/page.jsonld.jsonld +2 -0
- ktl/skills/ktl-website/templates/website/layouts/redirect.html +14 -0
- ktl/skills/ktl-website/templates/website/layouts/schema/page.html +10 -0
- ktl/skills/ktl-website/templates/website/layouts/schema/section.html +29 -0
- ktl/skills/ktl-website/templates/website/layouts/section.html +51 -0
- ktl/skills/ktl-website/templates/website/pyproject.toml +12 -0
- ktl/skills/ktl-website/templates/website/scripts/build.sh +69 -0
- ktl/skills/ktl-website/templates/website/scripts/check-links.py +107 -0
- ktl/skills/ktl-website/templates/website/scripts/clean-html.py +167 -0
- ktl/skills/ktl-website/templates/website/scripts/prepare.sh +141 -0
- ktl/skills/ktl-website/templates/website/site.toml +55 -0
- ktl/skills/ktl-website/templates/website/static/brand/knowledge-trust-ladder-logo-dark.svg +22 -0
- ktl/skills/ktl-website/templates/website/static/brand/knowledge-trust-ladder-logo.svg +22 -0
- ktl/skills/ktl-website/templates/website/static/fonts/OFL-atkinson-hyperlegible-next.txt +93 -0
- ktl/skills/ktl-website/templates/website/static/fonts/OFL-source-serif-4.txt +93 -0
- ktl/skills/ktl-website/templates/website/static/fonts/atkinson-hyperlegible-next-latin-wght-italic.woff2 +0 -0
- ktl/skills/ktl-website/templates/website/static/fonts/atkinson-hyperlegible-next-latin-wght-normal.woff2 +0 -0
- ktl/skills/ktl-website/templates/website/static/fonts/source-serif-4-latin-wght-normal.woff2 +0 -0
- ktl/skills/ktl-website/templates/website/uv.lock +62 -0
- ktl-0.64.0.dist-info/METADATA +81 -0
- ktl-0.64.0.dist-info/RECORD +189 -0
- ktl-0.64.0.dist-info/WHEEL +4 -0
- ktl-0.64.0.dist-info/entry_points.txt +2 -0
- ktl-0.64.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "knowledge-trust-ladder",
|
|
3
|
+
"owner": { "name": "Noel McLoughlin", "url": "https://github.com/noelmcloughlin" },
|
|
4
|
+
"metadata": {
|
|
5
|
+
"description": "Agent skills that keep a LOKF knowledge bundle true, and record who checked each claim."
|
|
6
|
+
},
|
|
7
|
+
"plugins": [
|
|
8
|
+
{
|
|
9
|
+
"name": "knowledge-trust-ladder",
|
|
10
|
+
"source": "./",
|
|
11
|
+
"description": "Four agent skills that keep a LOKF knowledge bundle current beside any repository, and record who checked each claim. A fifth skill, ktl-prose, rewords a bundle's agent-written concepts in plain English, and a sixth, ktl-website, builds a website for the bundle.",
|
|
12
|
+
"category": "knowledge-management",
|
|
13
|
+
"keywords": ["okf", "open-knowledge-format", "lokf", "linkml", "knowledge-graph", "linked-data", "json-ld", "rdf", "sparql", "provenance", "trust-ladder", "human-review", "agent-skills", "wikiskill", "website"]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "knowledge-trust-ladder",
|
|
3
|
+
"displayName": "Knowledge Trust Ladder",
|
|
4
|
+
"description": "Four agent skills that keep a LOKF knowledge bundle (Linked Open Knowledge Format, a semantic profile of OKF) current beside any repository, and record who checked each claim: a sidecar, a librarian, a curator and a docent. A fifth skill, ktl-prose, rewords a bundle's agent-written concepts in plain English, and a sixth, ktl-website, builds a website for the bundle.",
|
|
5
|
+
"author": { "name": "Noel McLoughlin", "url": "https://github.com/noelmcloughlin" },
|
|
6
|
+
"homepage": "https://w3id.org/ktl/",
|
|
7
|
+
"repository": "https://github.com/noelmcloughlin/knowledge-trust-ladder",
|
|
8
|
+
"license": "Apache-2.0",
|
|
9
|
+
"keywords": ["okf", "open-knowledge-format", "lokf", "linkml", "knowledge-graph", "linked-data", "json-ld", "rdf", "sparql", "provenance", "trust-ladder", "human-review", "agent-skills", "wikiskill", "website"]
|
|
10
|
+
}
|
ktl/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# ktl
|
|
2
|
+
|
|
3
|
+
The six Knowledge Trust Ladder (KTL) agent skills, and a command that installs them for GitHub Copilot, Claude Code, Codex or any other agent that reads skill folders, and sets the GitHub settings their workflows need. The skills keep a LOKF knowledge bundle beside a repository true, and record who checked each claim. [The KTL site](https://w3id.org/ktl/) says what each skill does.
|
|
4
|
+
|
|
5
|
+
The package has no dependencies, needs Python 3.9 or later, and installs from PyPI or any mirror of it, so it serves a machine that cannot reach github.com.
|
|
6
|
+
|
|
7
|
+
## Install the skills
|
|
8
|
+
|
|
9
|
+
Install all six for your user:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uvx ktl skills install --scope user
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That installs for GitHub Copilot, as `gh skill install` does. For Claude Code or Codex, add `--agent claude-code` or `--agent codex`. `--scope` takes `user` or `project`, and `project` means the top folder of the repository you are in. For any other agent, give the folder it reads with `--dest`. To install one skill, add `--skill ktl-docent`. To pin a version, write `ktl@X.Y.Z` in place of `ktl`, or `ktl==X.Y.Z` to pip. The six skills are released together, so one version pins them all. To update, run the command again as `uvx ktl@latest`, or `uv tool upgrade ktl` where `uv tool install` put the package.
|
|
16
|
+
|
|
17
|
+
Without uv, pipx does the same:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pipx run ktl skills install --scope user
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`python3 -m pip install --user ktl`, then `python3 -m ktl skills install --scope user`, works where the system lets pip install for a user. Debian, Ubuntu and Homebrew refuse it.
|
|
24
|
+
|
|
25
|
+
## Check what is installed
|
|
26
|
+
|
|
27
|
+
`ktl skills status` says, for each folder an agent reads at project and user scope, whether each KTL skill there is the same as this package's, and names each file that differs. A copy `gh skill install` made counts as the same when only gh's own rewrite of its `SKILL.md` differs. It exits 1 when a copy differs, so a script can act on it. `--dest` checks one folder.
|
|
28
|
+
|
|
29
|
+
## Set the GitHub settings
|
|
30
|
+
|
|
31
|
+
The workflows the skills install read repository variables, secrets kept in Environments, and a few repository settings. `ktl gh` compares what each skill's catalogue asks for with a repository's settings, through the GitHub CLI and your own login, and sets what is missing:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uvx ktl gh plan # what is missing, and why; changes nothing
|
|
35
|
+
uvx ktl gh apply # the Environments, their branch rules, and the values a catalogue gives
|
|
36
|
+
uvx ktl gh secrets # each secret, asked for without an echo
|
|
37
|
+
uvx ktl gh enable librarian # one switch, once what it needs is in place
|
|
38
|
+
uvx ktl gh prune # the variables a release retired
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Run it in a clone, or name a repository with `--repo owner/name`. It needs `gh auth login` and admin rights on the repository. `apply` never loosens a setting and never turns a switch on, and each verb that changes something prints the change and asks first. [Turn on the workflows](https://w3id.org/ktl/docs/install/#turn-on-the-workflows) says what each workflow needs, and links the page on each verb.
|
|
42
|
+
|
|
43
|
+
## Other installers
|
|
44
|
+
|
|
45
|
+
`ktl skills path` prints the folder that holds the skills. It is the argument for the installers that take a local folder:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
gh skill install "$(ktl skills path)" --from-local --all --scope user --agent github-copilot
|
|
49
|
+
npx skills add "$(ktl skills path)" --global --yes
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The package folder is also a Claude Code plugin marketplace. Install the package with `uv tool install ktl` first, so that the folder stays in place, then:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
claude plugin marketplace add "$(ktl skills path --marketplace)"
|
|
56
|
+
claude plugin install knowledge-trust-ladder@knowledge-trust-ladder
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`ktl skills` names the six skills. The package also goes by the repository's name: `knowledge-trust-ladder` installs `ktl` at the same version. Each release's files can be rebuilt from its tag and compared byte for byte, as [the release page](https://w3id.org/ktl/docs/releasing/#the-ktl-package-on-pypi) says. [The install page](https://w3id.org/ktl/docs/install/) gives every other route, and what each skill needs on the machine.
|
ktl/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Knowledge Trust Ladder (KTL): the six agent skills, a command that installs them, and one that sets the GitHub settings their workflows need."""
|
ktl/__main__.py
ADDED
ktl/cli.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""The `ktl` command: list, locate, install and check the Knowledge Trust
|
|
2
|
+
Ladder skills that this package carries, and, with `ktl gh`, set the GitHub
|
|
3
|
+
settings the workflows those skills install need (src/ktl/gh.py).
|
|
4
|
+
|
|
5
|
+
The package holds the six skills under `ktl/skills/` and the Claude Code
|
|
6
|
+
plugin manifest under `ktl/.claude-plugin/`, each byte for byte as the
|
|
7
|
+
release tag holds them. `ktl skills install` copies skill folders into the
|
|
8
|
+
folder an agent reads, and calls no other tool, so it works the same on
|
|
9
|
+
every operating system and forge, and with no network once the package is
|
|
10
|
+
installed. `gh skill install --from-local` and `npx skills add` take the
|
|
11
|
+
folder `ktl skills path` prints, where either is at hand. `ktl skills
|
|
12
|
+
status` says, for each folder an agent reads, whether the KTL skills there
|
|
13
|
+
are this package's.
|
|
14
|
+
|
|
15
|
+
The command uses the standard library alone and runs on Python 3.9 or
|
|
16
|
+
later, the python3 that RHEL 9 includes. The verbs are those of the lokf
|
|
17
|
+
toolkit's `lokf skills` command, so a person who uses both meets one shape.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import os
|
|
24
|
+
import shutil
|
|
25
|
+
import stat
|
|
26
|
+
import sys
|
|
27
|
+
import tempfile
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
# Where each agent reads skills, at project scope (relative to the
|
|
31
|
+
# repository's top folder) and at user scope (relative to the home folder).
|
|
32
|
+
# Each placement was checked against `gh skill install` 2.101. An agent not
|
|
33
|
+
# listed here takes --dest. No one folder serves them all: Claude Code reads
|
|
34
|
+
# only its own, and Copilot and Codex differ at user scope. So, as with `gh
|
|
35
|
+
# skill install`, GitHub Copilot is the agent when none is named.
|
|
36
|
+
AGENTS = {
|
|
37
|
+
"claude-code": (".claude/skills", ".claude/skills"),
|
|
38
|
+
"codex": (".agents/skills", ".agents/skills"),
|
|
39
|
+
"github-copilot": (".agents/skills", ".copilot/skills"),
|
|
40
|
+
}
|
|
41
|
+
DEFAULT_AGENT = "github-copilot"
|
|
42
|
+
|
|
43
|
+
# A project folder Copilot-style agents also read, which no installer here
|
|
44
|
+
# writes. `status` checks it, as the sidecar's preflight does.
|
|
45
|
+
OTHER_PROJECT_FOLDERS = {".github/skills": "github-copilot"}
|
|
46
|
+
|
|
47
|
+
# Files a copy never needs: bytecode a Python run may have left beside a
|
|
48
|
+
# script.
|
|
49
|
+
IGNORED = shutil.ignore_patterns("__pycache__", "*.pyc")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def package_root() -> Path:
|
|
53
|
+
"""Return the folder that holds `skills/` and `.claude-plugin/`.
|
|
54
|
+
|
|
55
|
+
In an installed package that is the `ktl` package folder. In a checkout
|
|
56
|
+
of the repository, where the build has not copied the skills in, it is
|
|
57
|
+
the repository root, two folders above `src/ktl/`.
|
|
58
|
+
"""
|
|
59
|
+
here = Path(__file__).resolve().parent
|
|
60
|
+
if (here / "skills").is_dir():
|
|
61
|
+
return here
|
|
62
|
+
checkout = here.parent.parent
|
|
63
|
+
if (checkout / "skills" / "ktl-docent" / "SKILL.md").is_file():
|
|
64
|
+
return checkout
|
|
65
|
+
raise SystemExit(f"ktl: no skills folder in {here} or in a checkout above it; reinstall the package")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def skills_dir() -> Path:
|
|
69
|
+
return package_root() / "skills"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def skill_names() -> list[str]:
|
|
73
|
+
return sorted(p.parent.name for p in skills_dir().glob("*/SKILL.md"))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def version() -> str:
|
|
77
|
+
try:
|
|
78
|
+
from importlib.metadata import PackageNotFoundError, version as dist_version
|
|
79
|
+
except ImportError: # pragma: no cover - every supported Python has it
|
|
80
|
+
return "unknown"
|
|
81
|
+
try:
|
|
82
|
+
return dist_version("ktl")
|
|
83
|
+
except PackageNotFoundError:
|
|
84
|
+
return "0.0.0 (a checkout, not an installed package)"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def scalar(text: str) -> str:
|
|
88
|
+
"""One YAML scalar written on one line: plain, single-quoted or double-quoted."""
|
|
89
|
+
text = text.strip()
|
|
90
|
+
if len(text) >= 2 and text[0] == text[-1] == "'":
|
|
91
|
+
return text[1:-1].replace("''", "'")
|
|
92
|
+
if len(text) >= 2 and text[0] == text[-1] == '"':
|
|
93
|
+
return text[1:-1].replace('\\"', '"').replace("\\\\", "\\")
|
|
94
|
+
return text
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def read_skill_md(path: Path) -> tuple[dict[str, str], dict[str, str], str]:
|
|
98
|
+
"""Split a SKILL.md into its frontmatter values, its metadata block and its body.
|
|
99
|
+
|
|
100
|
+
The skills write each value on one line, plain or quoted, so a line
|
|
101
|
+
reader serves and the package needs no YAML parser. `gh skill install`
|
|
102
|
+
rewrites the file as it installs it: the keys come out sorted, a quoted
|
|
103
|
+
value can come out plain, a metadata block names the source, and the
|
|
104
|
+
blank line after the frontmatter goes. Reading each value, and the body
|
|
105
|
+
without its leading blank lines, undoes all four, so two copies of one
|
|
106
|
+
release read the same however each was installed.
|
|
107
|
+
"""
|
|
108
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
109
|
+
values: dict[str, str] = {}
|
|
110
|
+
metadata: dict[str, str] = {}
|
|
111
|
+
if not lines or lines[0].strip() != "---":
|
|
112
|
+
return values, metadata, "\n".join(lines).lstrip("\n")
|
|
113
|
+
key = ""
|
|
114
|
+
for number, line in enumerate(lines[1:], start=1):
|
|
115
|
+
if line.strip() == "---":
|
|
116
|
+
return values, metadata, "\n".join(lines[number + 1 :]).lstrip("\n")
|
|
117
|
+
if line[:1] in (" ", "\t"):
|
|
118
|
+
if key == "metadata" and ":" in line:
|
|
119
|
+
name, value = line.split(":", 1)
|
|
120
|
+
metadata[name.strip()] = scalar(value)
|
|
121
|
+
continue
|
|
122
|
+
if ":" in line:
|
|
123
|
+
key, value = line.split(":", 1)
|
|
124
|
+
key = key.strip()
|
|
125
|
+
if key != "metadata":
|
|
126
|
+
values[key] = scalar(value)
|
|
127
|
+
return values, metadata, ""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def first_sentence(text: str) -> str:
|
|
131
|
+
end = text.find(". ")
|
|
132
|
+
return text if end < 0 else text[: end + 1]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def project_root(start: Path) -> Path:
|
|
136
|
+
"""Return the top folder of the repository that holds start: the nearest
|
|
137
|
+
folder at or above it with a `.git` entry, which is a folder in a clone
|
|
138
|
+
and a file in a worktree or a submodule. With none, start itself."""
|
|
139
|
+
for folder in (start, *start.parents):
|
|
140
|
+
if (folder / ".git").exists():
|
|
141
|
+
return folder
|
|
142
|
+
return start
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def target_folder(agent: str, scope: str) -> Path:
|
|
146
|
+
project, user = AGENTS[agent]
|
|
147
|
+
return (Path.home() / user) if scope == "user" else (project_root(Path.cwd()) / project)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def install(names: list[str], dest: Path) -> None:
|
|
151
|
+
"""Copy each named skill folder into dest, replacing a folder of the
|
|
152
|
+
same name.
|
|
153
|
+
|
|
154
|
+
Every skill is copied into one hidden staging folder in dest first, and
|
|
155
|
+
the old folders are swapped for the new ones only once all are copied.
|
|
156
|
+
So a copy that fails, for want of space, say, leaves the earlier release
|
|
157
|
+
whole rather than half replaced. The staging folder holds each skill a
|
|
158
|
+
level down, where no agent looks for one.
|
|
159
|
+
"""
|
|
160
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
source = skills_dir()
|
|
162
|
+
staging = Path(tempfile.mkdtemp(prefix=".ktl-", dir=dest))
|
|
163
|
+
try:
|
|
164
|
+
for name in names:
|
|
165
|
+
shutil.copytree(source / name, staging / name, ignore=IGNORED)
|
|
166
|
+
for name in names:
|
|
167
|
+
target = dest / name
|
|
168
|
+
if target.is_symlink() or target.is_file():
|
|
169
|
+
target.unlink()
|
|
170
|
+
elif target.exists():
|
|
171
|
+
shutil.rmtree(target)
|
|
172
|
+
(staging / name).rename(target)
|
|
173
|
+
finally:
|
|
174
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def files_of(folder: Path) -> dict[str, Path]:
|
|
178
|
+
"""Every file in a skill folder by its path inside it, bytecode aside."""
|
|
179
|
+
return {
|
|
180
|
+
p.relative_to(folder).as_posix(): p
|
|
181
|
+
for p in sorted(folder.rglob("*"))
|
|
182
|
+
if p.is_file() and "__pycache__" not in p.parts and p.suffix != ".pyc"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def executable(path: Path) -> bool:
|
|
187
|
+
return bool(path.stat().st_mode & stat.S_IXUSR)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def same_skill_md(a: Path, b: Path) -> bool:
|
|
191
|
+
"""Whether two SKILL.md files read the same, gh's metadata block aside."""
|
|
192
|
+
values_a, _, body_a = read_skill_md(a)
|
|
193
|
+
values_b, _, body_b = read_skill_md(b)
|
|
194
|
+
return values_a == values_b and body_a == body_b
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def compare(installed: Path, carried: Path) -> tuple[list[str], list[str]]:
|
|
198
|
+
"""Compare an installed skill with this package's copy of it.
|
|
199
|
+
|
|
200
|
+
Return the files whose content differs, or that only one copy holds,
|
|
201
|
+
and the files this package marks executable that the installed copy
|
|
202
|
+
does not. A SKILL.md counts as the same when it reads the same through
|
|
203
|
+
read_skill_md. The skills run their scripts through bash, so a lost
|
|
204
|
+
executable bit is reported apart from a change of content.
|
|
205
|
+
"""
|
|
206
|
+
mine, theirs = files_of(carried), files_of(installed)
|
|
207
|
+
changed = sorted(set(mine) ^ set(theirs))
|
|
208
|
+
not_executable = []
|
|
209
|
+
for rel in sorted(set(mine) & set(theirs)):
|
|
210
|
+
a, b = mine[rel], theirs[rel]
|
|
211
|
+
if a.read_bytes() != b.read_bytes() and not (rel == "SKILL.md" and same_skill_md(a, b)):
|
|
212
|
+
changed.append(rel)
|
|
213
|
+
if os.name != "nt" and executable(a) and not executable(b):
|
|
214
|
+
not_executable.append(rel)
|
|
215
|
+
return sorted(changed), not_executable
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def where(folder: Path) -> str:
|
|
219
|
+
try:
|
|
220
|
+
return "~/" + folder.relative_to(Path.home()).as_posix()
|
|
221
|
+
except ValueError:
|
|
222
|
+
return str(folder)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def folders_to_check(dest: Path | None) -> dict[Path, list[str]]:
|
|
226
|
+
"""Each folder an agent reads skills from, with who reads it: the
|
|
227
|
+
project's folders first, then the user's."""
|
|
228
|
+
if dest is not None:
|
|
229
|
+
return {dest: []}
|
|
230
|
+
root = project_root(Path.cwd())
|
|
231
|
+
readers: dict[Path, list[str]] = {}
|
|
232
|
+
for agent, (project, _) in sorted(AGENTS.items()):
|
|
233
|
+
readers.setdefault(root / project, []).append(f"{agent}, project")
|
|
234
|
+
for folder, agent in OTHER_PROJECT_FOLDERS.items():
|
|
235
|
+
readers.setdefault(root / folder, []).append(f"{agent}, project")
|
|
236
|
+
for agent, (_, user) in sorted(AGENTS.items()):
|
|
237
|
+
readers.setdefault(Path.home() / user, []).append(f"{agent}, user")
|
|
238
|
+
return readers
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def state(folder: Path, name: str, carried: list[str]) -> tuple[str, bool]:
|
|
242
|
+
"""One installed skill's line in `status`, and whether it matches this package."""
|
|
243
|
+
skill = folder / name
|
|
244
|
+
if name not in carried:
|
|
245
|
+
return "not in this package: an older release's, or another's", False
|
|
246
|
+
changed, not_executable = compare(skill, skills_dir() / name)
|
|
247
|
+
ref = read_skill_md(skill / "SKILL.md")[1].get("github-ref", "")
|
|
248
|
+
source = f"; installed by gh skill from {ref.rsplit('/', 1)[-1]}" if ref else ""
|
|
249
|
+
if changed:
|
|
250
|
+
shown = ", ".join(changed[:3]) + (", ..." if len(changed) > 3 else "")
|
|
251
|
+
count = f"{len(changed)} file{'s' if len(changed) != 1 else ''}"
|
|
252
|
+
return f"differs from this package in {count} ({shown}){source}", False
|
|
253
|
+
lost = f", though {len(not_executable)} script{'s' if len(not_executable) != 1 else ''} lost the executable bit" if not_executable else ""
|
|
254
|
+
return f"same as this package{lost}{source}", True
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def cmd_list(args: argparse.Namespace) -> int:
|
|
258
|
+
for name in skill_names():
|
|
259
|
+
description = read_skill_md(skills_dir() / name / "SKILL.md")[0].get("description", "")
|
|
260
|
+
print(f"{name}\t{first_sentence(description)}")
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def cmd_path(args: argparse.Namespace) -> int:
|
|
265
|
+
print(package_root() if args.marketplace else skills_dir())
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def cmd_install(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
|
|
270
|
+
if args.dest is not None and (args.agent or args.scope):
|
|
271
|
+
parser.error("give --dest, or --scope with an optional --agent, not both")
|
|
272
|
+
if args.dest is None and not args.scope:
|
|
273
|
+
parser.error("name the scope (--scope user|project), or a folder with --dest")
|
|
274
|
+
available = skill_names()
|
|
275
|
+
names = sorted(set(args.skill)) if args.skill else available
|
|
276
|
+
unknown = [n for n in names if n not in available]
|
|
277
|
+
if unknown:
|
|
278
|
+
parser.error(f"no skill named {', '.join(unknown)}; this package carries {', '.join(available)}")
|
|
279
|
+
dest = args.dest if args.dest is not None else target_folder(args.agent or DEFAULT_AGENT, args.scope)
|
|
280
|
+
install(names, dest)
|
|
281
|
+
print(f"Installed {len(names)} skill{'s' if len(names) != 1 else ''} from ktl {version()} into {dest}: {', '.join(names)}")
|
|
282
|
+
return 0
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def cmd_status(args: argparse.Namespace) -> int:
|
|
286
|
+
carried = skill_names()
|
|
287
|
+
print(f"ktl {version()} carries {', '.join(carried)}.")
|
|
288
|
+
in_step, empty = True, []
|
|
289
|
+
for folder, readers in folders_to_check(args.dest).items():
|
|
290
|
+
present = sorted(p.parent.name for p in folder.glob("ktl-*/SKILL.md")) if folder.is_dir() else []
|
|
291
|
+
if not present:
|
|
292
|
+
empty.append(where(folder))
|
|
293
|
+
continue
|
|
294
|
+
print(where(folder) + (f" (read by {'; '.join(readers)})" if readers else ""))
|
|
295
|
+
for name in present:
|
|
296
|
+
line, same = state(folder, name, carried)
|
|
297
|
+
in_step = in_step and same
|
|
298
|
+
print(f" {name:<14} {line}")
|
|
299
|
+
absent = [name for name in carried if name not in present]
|
|
300
|
+
if absent:
|
|
301
|
+
print(f" {'not here':<14} {', '.join(absent)}")
|
|
302
|
+
if empty:
|
|
303
|
+
print(f"No KTL skill in {', '.join(empty)}.")
|
|
304
|
+
return 0 if in_step else 1
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
308
|
+
parser = argparse.ArgumentParser(
|
|
309
|
+
prog="ktl",
|
|
310
|
+
description="List, locate, install and check the Knowledge Trust Ladder (KTL) skills this package carries, "
|
|
311
|
+
"and set the GitHub settings the workflows they install need.",
|
|
312
|
+
)
|
|
313
|
+
parser.add_argument("--version", action="version", version=f"ktl {version()}")
|
|
314
|
+
groups = parser.add_subparsers(dest="group", metavar="{skills,gh}")
|
|
315
|
+
skills = groups.add_parser("skills", help="the six skills: list (the default), path, install, status")
|
|
316
|
+
actions = skills.add_subparsers(dest="action", metavar="{list,path,install,status}")
|
|
317
|
+
|
|
318
|
+
actions.add_parser("list", help="name each skill, with the first sentence of its description")
|
|
319
|
+
|
|
320
|
+
path = actions.add_parser("path", help="print the folder that holds the skills")
|
|
321
|
+
path.add_argument(
|
|
322
|
+
"--marketplace",
|
|
323
|
+
action="store_true",
|
|
324
|
+
help="print the folder that holds .claude-plugin/ instead, for `claude plugin marketplace add`",
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
table = "\n".join(
|
|
328
|
+
f" {agent:<15} project: {project}/ user: ~/{user}/" for agent, (project, user) in sorted(AGENTS.items())
|
|
329
|
+
)
|
|
330
|
+
install_parser = actions.add_parser(
|
|
331
|
+
"install",
|
|
332
|
+
help="copy the skills into the folder an agent reads",
|
|
333
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
334
|
+
description="Copy the skills into the folder an agent reads, replacing any earlier copy of each.",
|
|
335
|
+
epilog=f"Where each agent reads skills (project scope is relative to the repository's top folder):\n{table}\n\n"
|
|
336
|
+
f"With no --agent, the agent is {DEFAULT_AGENT}. Give --dest for any other agent or folder.",
|
|
337
|
+
)
|
|
338
|
+
install_parser.add_argument(
|
|
339
|
+
"--agent", choices=sorted(AGENTS), help=f"the agent whose folder to use (default: {DEFAULT_AGENT})"
|
|
340
|
+
)
|
|
341
|
+
install_parser.add_argument("--scope", choices=["user", "project"], help="the user's folder, or this project's")
|
|
342
|
+
install_parser.add_argument("--dest", type=Path, help="install into this folder instead")
|
|
343
|
+
install_parser.add_argument(
|
|
344
|
+
"--skill",
|
|
345
|
+
action="append",
|
|
346
|
+
metavar="NAME",
|
|
347
|
+
help="install only this skill; repeat for more (default: all six)",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
status = actions.add_parser(
|
|
351
|
+
"status",
|
|
352
|
+
help="say whether the KTL skills in each folder an agent reads are this package's",
|
|
353
|
+
description="For each folder an agent reads skills from, at project and user scope, say whether each KTL "
|
|
354
|
+
"skill there is the same as this package's copy. A copy gh skill installed counts as the same when only "
|
|
355
|
+
"gh's own rewrite of its SKILL.md differs. Exits 1 when an installed copy differs, so a script can act on it.",
|
|
356
|
+
)
|
|
357
|
+
status.add_argument("--dest", type=Path, help="check this folder only")
|
|
358
|
+
|
|
359
|
+
from ktl import gh
|
|
360
|
+
|
|
361
|
+
gh.add_parser(groups)
|
|
362
|
+
return parser
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def main(argv: list[str] | None = None) -> int:
|
|
366
|
+
parser = build_parser()
|
|
367
|
+
args = parser.parse_args(argv)
|
|
368
|
+
if args.group == "gh":
|
|
369
|
+
from ktl import gh
|
|
370
|
+
|
|
371
|
+
return gh.main(args)
|
|
372
|
+
if args.group != "skills":
|
|
373
|
+
parser.print_help()
|
|
374
|
+
return 2
|
|
375
|
+
try:
|
|
376
|
+
if args.action in (None, "list"):
|
|
377
|
+
return cmd_list(args)
|
|
378
|
+
if args.action == "path":
|
|
379
|
+
return cmd_path(args)
|
|
380
|
+
if args.action == "status":
|
|
381
|
+
return cmd_status(args)
|
|
382
|
+
return cmd_install(args, parser)
|
|
383
|
+
except OSError as error:
|
|
384
|
+
reason = error.strerror or str(error)
|
|
385
|
+
print(f"ktl: {reason}: {error.filename}" if error.filename else f"ktl: {reason}", file=sys.stderr)
|
|
386
|
+
return 1
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
if __name__ == "__main__": # pragma: no cover
|
|
390
|
+
sys.exit(main())
|