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.
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: skillchain-compiler
3
+ Version: 0.3.0
4
+ Summary: Compose Claude Code skills into a validated, packaged chain
5
+ Requires-Python: >=3.11
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: skillchain-compiler
3
+ Version: 0.3.0
4
+ Summary: Compose Claude Code skills into a validated, packaged chain
5
+ Requires-Python: >=3.11
@@ -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,2 @@
1
+ [console_scripts]
2
+ skillchain = skillchain:main