skillchain-compiler 0.3.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.
skillchain.py
ADDED
|
@@ -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,6 @@
|
|
|
1
|
+
skillchain.py,sha256=0s3rYB4jj-2a19xBIL1bLZxp_HRHW_DgOGC5wG9KQnc,10104
|
|
2
|
+
skillchain_compiler-0.3.0.dist-info/METADATA,sha256=b1-teA2VdVE9g7DE90JL9XUfVX0qcS7rqYnL9Q7e1jQ,156
|
|
3
|
+
skillchain_compiler-0.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
4
|
+
skillchain_compiler-0.3.0.dist-info/entry_points.txt,sha256=aCDLR8zjr_e4fVb55jcJiDSaIMEJt6Y2545Y_fcol04,47
|
|
5
|
+
skillchain_compiler-0.3.0.dist-info/top_level.txt,sha256=ykPtWIjL1rXi5-VNQHWVb5YtpvegKp-SUosF0dj7sIE,11
|
|
6
|
+
skillchain_compiler-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
skillchain
|