skillchain-compiler 0.3.0__tar.gz
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.
- skillchain_compiler-0.3.0/PKG-INFO +5 -0
- skillchain_compiler-0.3.0/README.md +59 -0
- skillchain_compiler-0.3.0/pyproject.toml +15 -0
- skillchain_compiler-0.3.0/setup.cfg +4 -0
- skillchain_compiler-0.3.0/skillchain.py +255 -0
- skillchain_compiler-0.3.0/skillchain_compiler.egg-info/PKG-INFO +5 -0
- skillchain_compiler-0.3.0/skillchain_compiler.egg-info/SOURCES.txt +8 -0
- skillchain_compiler-0.3.0/skillchain_compiler.egg-info/dependency_links.txt +1 -0
- skillchain_compiler-0.3.0/skillchain_compiler.egg-info/entry_points.txt +2 -0
- skillchain_compiler-0.3.0/skillchain_compiler.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# skillchain — a skillchain compiler
|
|
2
|
+
|
|
3
|
+
Compose Claude Code **skills** into a validated, packaged **chain**.
|
|
4
|
+
|
|
5
|
+
A skillchain is an ordered list of steps of two kinds:
|
|
6
|
+
- **skill** — invoke a named skill (the compiler validates it exists on disk).
|
|
7
|
+
- **cor** — an **Attention-Chain / Chain-of-Reasoning** bridge between skills: an explicit
|
|
8
|
+
reasoning step that routes one skill's output into the next.
|
|
9
|
+
|
|
10
|
+
The compiler **(1)** indexes every skill under `~/.claude/skills` + `~/.claude/plugins`,
|
|
11
|
+
**(2)** validates every referenced skill exists (errors listing any missing), **(3)** wires
|
|
12
|
+
the CoR bridges, **(4)** exports a **skill package** (`<out>/<name>/SKILL.md` + `chain.json`)
|
|
13
|
+
that, when invoked, executes the chain in order.
|
|
14
|
+
|
|
15
|
+
## Use
|
|
16
|
+
```bash
|
|
17
|
+
python3 skillchain.py list # list indexed skills
|
|
18
|
+
python3 skillchain.py validate examples/offer_builder.chain
|
|
19
|
+
python3 skillchain.py compile examples/offer_builder.chain --out dist
|
|
20
|
+
python3 skillchain.py compile examples/offer_builder.chain --install # -> ~/.claude/skills
|
|
21
|
+
```
|
|
22
|
+
`--install` writes the compiled package straight into `~/.claude/skills/<name>/` so it becomes
|
|
23
|
+
an invokable skill (after a reload). Otherwise it lands in `./dist/<name>/`.
|
|
24
|
+
|
|
25
|
+
## Spec formats
|
|
26
|
+
Two interchangeable formats; both parse to the same internal spec.
|
|
27
|
+
|
|
28
|
+
### `.chain` text DSL (ergonomic)
|
|
29
|
+
```
|
|
30
|
+
name: my_chain
|
|
31
|
+
description: one line
|
|
32
|
+
> skill-a : args for skill-a # '>' = skill step (skill : args)
|
|
33
|
+
= result_a # '=' = capture last result as a var
|
|
34
|
+
~ Given {result_a}, reason about X, then produce Y. # '~' = CoR / AttentionChain bridge
|
|
35
|
+
> skill-b : use {result_a} and Y
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### `.json` (canonical)
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"name": "my_chain",
|
|
42
|
+
"description": "one line",
|
|
43
|
+
"steps": [
|
|
44
|
+
{"skill": "skill-a", "args": "args for skill-a", "capture": "result_a"},
|
|
45
|
+
{"cor": "Given {result_a}, reason about X, then produce Y."},
|
|
46
|
+
{"skill": "skill-b", "args": "use {result_a} and Y"}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## What "compile" produces
|
|
52
|
+
`<out>/<name>/SKILL.md` — a real skill whose body is the ordered orchestration: each skill
|
|
53
|
+
step says *"invoke skill X with …, capture as …"*, each CoR step is an explicit reason-then-carry
|
|
54
|
+
bridge, and the header lists the validated dependencies. Plus `chain.json`, the canonical spec.
|
|
55
|
+
|
|
56
|
+
## Validation guarantees
|
|
57
|
+
`compile` refuses to emit a package if **any** referenced skill is missing — so a compiled
|
|
58
|
+
chain only ever composes skills that actually exist. `validate` exits non-zero on any problem
|
|
59
|
+
(missing skill, no name, malformed step), so it can gate CI.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "skillchain-compiler"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Compose Claude Code skills into a validated, packaged chain"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
|
|
11
|
+
[project.scripts]
|
|
12
|
+
skillchain = "skillchain:main"
|
|
13
|
+
|
|
14
|
+
[tool.setuptools]
|
|
15
|
+
py-modules = ["skillchain"]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
skillchain — compose Claude Code skills into a validated, packaged chain.
|
|
4
|
+
|
|
5
|
+
A skillchain is an ordered list of steps. Two step kinds:
|
|
6
|
+
• skill — invoke a named skill (validated to exist on disk)
|
|
7
|
+
• cor — an Attention-Chain / Chain-of-Reasoning bridge between skills
|
|
8
|
+
(an explicit reasoning step that routes one skill's output into the next)
|
|
9
|
+
|
|
10
|
+
The compiler:
|
|
11
|
+
1. indexes every skill on disk (~/.claude/skills + ~/.claude/plugins),
|
|
12
|
+
2. validates every referenced skill EXISTS (errors listing any missing),
|
|
13
|
+
3. wires the CoR bridges between steps,
|
|
14
|
+
4. exports a skill PACKAGE (<out>/<name>/SKILL.md + chain.json) that, when
|
|
15
|
+
invoked, executes the chain in order.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
skillchain.py list
|
|
19
|
+
skillchain.py validate <spec.json|spec.chain>
|
|
20
|
+
skillchain.py compile <spec.json|spec.chain> [--out DIR] [--install]
|
|
21
|
+
|
|
22
|
+
Spec formats — see README.md. JSON is canonical; `.chain` is a light text DSL:
|
|
23
|
+
name: research_to_paper
|
|
24
|
+
description: research a question then write it up
|
|
25
|
+
> deep-research : <the question> # '>' = skill step (skill : args)
|
|
26
|
+
= research # '=' = capture last result as a var
|
|
27
|
+
~ Given {research}, extract the 3 strongest claims and the gaps, then write an outline brief. # '~' = CoR bridge
|
|
28
|
+
> academic-paper : write the paper from the outline brief
|
|
29
|
+
"""
|
|
30
|
+
import argparse, json, os, re, sys, pathlib
|
|
31
|
+
|
|
32
|
+
HOME = pathlib.Path.home()
|
|
33
|
+
SKILL_ROOTS = [HOME / ".claude" / "skills", HOME / ".claude" / "plugins"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ----------------------------------------------------------------------------- skill index
|
|
37
|
+
def _read(path):
|
|
38
|
+
try:
|
|
39
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
40
|
+
except Exception:
|
|
41
|
+
return ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _frontmatter(path):
|
|
45
|
+
txt = _read(path)
|
|
46
|
+
name = None
|
|
47
|
+
desc = None
|
|
48
|
+
m = re.search(r"^\s*name:\s*(.+?)\s*$", txt, re.MULTILINE)
|
|
49
|
+
if m:
|
|
50
|
+
name = m.group(1).strip().strip("\"'")
|
|
51
|
+
m = re.search(r"^\s*description:\s*(.+?)\s*$", txt, re.MULTILINE)
|
|
52
|
+
if m:
|
|
53
|
+
desc = m.group(1).strip().strip("\"'")
|
|
54
|
+
return name, desc
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def index_skills(roots=SKILL_ROOTS):
|
|
58
|
+
"""Return {alias -> {'path':Path,'name':str,'desc':str}}. A skill is reachable
|
|
59
|
+
by its frontmatter name, its directory name, and (after a ':') the colon-suffix."""
|
|
60
|
+
idx = {}
|
|
61
|
+
for root in roots:
|
|
62
|
+
if not root.exists():
|
|
63
|
+
continue
|
|
64
|
+
for sk in root.rglob("SKILL.md"):
|
|
65
|
+
name, desc = _frontmatter(sk)
|
|
66
|
+
dirname = sk.parent.name
|
|
67
|
+
canonical = name or dirname
|
|
68
|
+
rec = {"path": sk, "name": canonical, "desc": desc or ""}
|
|
69
|
+
for alias in {name, dirname, (name or "").split(":")[-1], dirname.split(":")[-1]}:
|
|
70
|
+
if alias:
|
|
71
|
+
idx.setdefault(alias, rec)
|
|
72
|
+
return idx
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def resolve(ref, idx):
|
|
76
|
+
"""Resolve a skill reference against the index. Try exact, then colon-suffix."""
|
|
77
|
+
if ref in idx:
|
|
78
|
+
return idx[ref]
|
|
79
|
+
if ":" in ref and ref.split(":")[-1] in idx:
|
|
80
|
+
return idx[ref.split(":")[-1]]
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ----------------------------------------------------------------------------- spec parsing
|
|
85
|
+
def parse_spec(path):
|
|
86
|
+
p = pathlib.Path(path)
|
|
87
|
+
text = _read(p)
|
|
88
|
+
if p.suffix == ".json" or text.lstrip().startswith("{"):
|
|
89
|
+
return json.loads(text)
|
|
90
|
+
return parse_chain_dsl(text)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def parse_chain_dsl(text):
|
|
94
|
+
spec = {"name": None, "description": "", "steps": []}
|
|
95
|
+
for raw in text.splitlines():
|
|
96
|
+
line = raw.strip()
|
|
97
|
+
if not line or line.startswith("#"):
|
|
98
|
+
continue
|
|
99
|
+
if line.lower().startswith("name:"):
|
|
100
|
+
spec["name"] = line.split(":", 1)[1].strip()
|
|
101
|
+
elif line.lower().startswith("description:"):
|
|
102
|
+
spec["description"] = line.split(":", 1)[1].strip()
|
|
103
|
+
elif line.startswith(">"): # skill step: > skill : args
|
|
104
|
+
body = line[1:].strip()
|
|
105
|
+
if ":" in body:
|
|
106
|
+
skill, args = body.split(":", 1)
|
|
107
|
+
spec["steps"].append({"skill": skill.strip(), "args": args.strip()})
|
|
108
|
+
else:
|
|
109
|
+
spec["steps"].append({"skill": body.strip(), "args": ""})
|
|
110
|
+
elif line.startswith("="): # capture last result: = varname
|
|
111
|
+
var = line[1:].strip()
|
|
112
|
+
if spec["steps"]:
|
|
113
|
+
spec["steps"][-1]["capture"] = var
|
|
114
|
+
elif line.startswith("~"): # CoR bridge
|
|
115
|
+
spec["steps"].append({"cor": line[1:].strip()})
|
|
116
|
+
return spec
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ----------------------------------------------------------------------------- validate
|
|
120
|
+
def validate(spec, idx):
|
|
121
|
+
"""Return (ok, report). report lists every step with resolved path or MISSING."""
|
|
122
|
+
rows, missing = [], []
|
|
123
|
+
if not spec.get("name"):
|
|
124
|
+
missing.append("(spec has no 'name')")
|
|
125
|
+
for i, step in enumerate(spec.get("steps", []), 1):
|
|
126
|
+
if "skill" in step:
|
|
127
|
+
rec = resolve(step["skill"], idx)
|
|
128
|
+
if rec:
|
|
129
|
+
rows.append((i, "skill", step["skill"], "ok", str(rec["path"])))
|
|
130
|
+
else:
|
|
131
|
+
rows.append((i, "skill", step["skill"], "MISSING", ""))
|
|
132
|
+
missing.append(step["skill"])
|
|
133
|
+
elif "cor" in step:
|
|
134
|
+
rows.append((i, "cor", (step["cor"][:48] + "…") if len(step["cor"]) > 48 else step["cor"], "ok", ""))
|
|
135
|
+
else:
|
|
136
|
+
rows.append((i, "?", str(step), "BAD-STEP", ""))
|
|
137
|
+
missing.append(f"step {i} (unknown kind)")
|
|
138
|
+
return (len(missing) == 0), rows, missing
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ----------------------------------------------------------------------------- compile
|
|
142
|
+
def compile_package(spec, idx, out_dir):
|
|
143
|
+
name = spec["name"]
|
|
144
|
+
desc = spec.get("description", "") or f"Compiled skillchain: {name}"
|
|
145
|
+
steps = spec.get("steps", [])
|
|
146
|
+
deps = sorted({resolve(s["skill"], idx)["name"] for s in steps if "skill" in s})
|
|
147
|
+
|
|
148
|
+
body = []
|
|
149
|
+
body.append(f"# {name} — compiled skillchain\n")
|
|
150
|
+
body.append(
|
|
151
|
+
f"> Compiled by `skillchain.py`. "
|
|
152
|
+
f"Orchestrates {len(steps)} step(s) composing {len(deps)} skill(s).\n"
|
|
153
|
+
f"> **Dependencies (validated present at compile time):** "
|
|
154
|
+
+ (", ".join(f"`{d}`" for d in deps) if deps else "none")
|
|
155
|
+
+ "\n"
|
|
156
|
+
)
|
|
157
|
+
body.append(
|
|
158
|
+
"**Execute the steps below in order.** Carry each step's result forward; where a step "
|
|
159
|
+
"references `{var}`, substitute the captured value from the named earlier step.\n"
|
|
160
|
+
)
|
|
161
|
+
sidx = 0
|
|
162
|
+
for step in steps:
|
|
163
|
+
sidx += 1
|
|
164
|
+
if "skill" in step:
|
|
165
|
+
rec = resolve(step["skill"], idx)
|
|
166
|
+
args = step.get("args", "").strip()
|
|
167
|
+
cap = step.get("capture")
|
|
168
|
+
body.append(f"## Step {sidx} — invoke skill `{rec['name']}`")
|
|
169
|
+
if rec.get("desc"):
|
|
170
|
+
body.append(f"*{rec['desc']}*\n")
|
|
171
|
+
body.append(
|
|
172
|
+
f"Invoke the **{rec['name']}** skill"
|
|
173
|
+
+ (f" with input: {args}" if args else " (no extra input)")
|
|
174
|
+
+ (f"\n\n→ Capture its result as `{cap}`." if cap else "")
|
|
175
|
+
+ "\n"
|
|
176
|
+
)
|
|
177
|
+
elif "cor" in step:
|
|
178
|
+
body.append(f"## Step {sidx} — reasoning bridge (CoR / AttentionChain)")
|
|
179
|
+
body.append(
|
|
180
|
+
"Reason explicitly, step by step:\n\n"
|
|
181
|
+
f"> {step['cor']}\n\n"
|
|
182
|
+
"State the conclusion, then carry it forward as the input to the next step.\n"
|
|
183
|
+
)
|
|
184
|
+
body.append("## Done")
|
|
185
|
+
body.append("Return the final step's output as the result of this chain.\n")
|
|
186
|
+
|
|
187
|
+
skill_md = f"---\nname: {name}\ndescription: {desc}\n---\n\n" + "\n".join(body)
|
|
188
|
+
|
|
189
|
+
pkg = pathlib.Path(out_dir) / name
|
|
190
|
+
pkg.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
(pkg / "SKILL.md").write_text(skill_md, encoding="utf-8")
|
|
192
|
+
(pkg / "chain.json").write_text(json.dumps(spec, indent=2), encoding="utf-8")
|
|
193
|
+
return pkg, deps
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ----------------------------------------------------------------------------- cli
|
|
197
|
+
def _print_rows(rows):
|
|
198
|
+
for i, kind, what, status, path in rows:
|
|
199
|
+
mark = "✓" if status == "ok" else "✗"
|
|
200
|
+
line = f" {mark} [{i}] {kind:5} {what}"
|
|
201
|
+
if status != "ok":
|
|
202
|
+
line += f" <{status}>"
|
|
203
|
+
elif path:
|
|
204
|
+
line += f" → {path}"
|
|
205
|
+
print(line)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main():
|
|
209
|
+
ap = argparse.ArgumentParser(prog="skillchain", description="compose skills into a validated, packaged chain")
|
|
210
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
211
|
+
sub.add_parser("list", help="list indexed skills")
|
|
212
|
+
v = sub.add_parser("validate", help="check a chain spec's skills all exist")
|
|
213
|
+
v.add_argument("spec")
|
|
214
|
+
c = sub.add_parser("compile", help="validate + emit a skill package")
|
|
215
|
+
c.add_argument("spec")
|
|
216
|
+
c.add_argument("--out", default="dist", help="output dir (default: ./dist)")
|
|
217
|
+
c.add_argument("--install", action="store_true", help="emit into ~/.claude/skills instead of --out")
|
|
218
|
+
a = ap.parse_args()
|
|
219
|
+
|
|
220
|
+
idx = index_skills()
|
|
221
|
+
|
|
222
|
+
if a.cmd == "list":
|
|
223
|
+
names = sorted({rec["name"] for rec in idx.values()})
|
|
224
|
+
print(f"{len(names)} skills indexed under {', '.join(str(r) for r in SKILL_ROOTS if r.exists())}:")
|
|
225
|
+
for n in names:
|
|
226
|
+
print(f" - {n}")
|
|
227
|
+
return 0
|
|
228
|
+
|
|
229
|
+
spec = parse_spec(a.spec)
|
|
230
|
+
ok, rows, missing = validate(spec, idx)
|
|
231
|
+
print(f"chain: {spec.get('name')!r} ({len(spec.get('steps', []))} steps)")
|
|
232
|
+
_print_rows(rows)
|
|
233
|
+
|
|
234
|
+
if a.cmd == "validate":
|
|
235
|
+
if ok:
|
|
236
|
+
print("✓ valid — all skills resolve.")
|
|
237
|
+
return 0
|
|
238
|
+
print(f"✗ INVALID — {len(missing)} problem(s): {', '.join(missing)}")
|
|
239
|
+
return 1
|
|
240
|
+
|
|
241
|
+
# compile
|
|
242
|
+
if not ok:
|
|
243
|
+
print(f"✗ refusing to compile — {len(missing)} missing/bad: {', '.join(missing)}")
|
|
244
|
+
return 1
|
|
245
|
+
out = (HOME / ".claude" / "skills") if a.install else pathlib.Path(a.out)
|
|
246
|
+
pkg, deps = compile_package(spec, idx, out)
|
|
247
|
+
print(f"✓ compiled → {pkg}/SKILL.md (+ chain.json)")
|
|
248
|
+
print(f" deps: {', '.join(deps) if deps else 'none'}")
|
|
249
|
+
if a.install:
|
|
250
|
+
print(" installed into ~/.claude/skills — invokable as a skill after reload.")
|
|
251
|
+
return 0
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
if __name__ == "__main__":
|
|
255
|
+
sys.exit(main())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
skillchain.py
|
|
4
|
+
skillchain_compiler.egg-info/PKG-INFO
|
|
5
|
+
skillchain_compiler.egg-info/SOURCES.txt
|
|
6
|
+
skillchain_compiler.egg-info/dependency_links.txt
|
|
7
|
+
skillchain_compiler.egg-info/entry_points.txt
|
|
8
|
+
skillchain_compiler.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
skillchain
|