chaincompiler-si 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.
- chaincompiler_si-0.3.0/PKG-INFO +12 -0
- chaincompiler_si-0.3.0/pyproject.toml +19 -0
- chaincompiler_si-0.3.0/setup.cfg +4 -0
- chaincompiler_si-0.3.0/src/chaincompiler_si.egg-info/PKG-INFO +12 -0
- chaincompiler_si-0.3.0/src/chaincompiler_si.egg-info/SOURCES.txt +17 -0
- chaincompiler_si-0.3.0/src/chaincompiler_si.egg-info/dependency_links.txt +1 -0
- chaincompiler_si-0.3.0/src/chaincompiler_si.egg-info/entry_points.txt +2 -0
- chaincompiler_si-0.3.0/src/chaincompiler_si.egg-info/requires.txt +7 -0
- chaincompiler_si-0.3.0/src/chaincompiler_si.egg-info/top_level.txt +1 -0
- chaincompiler_si-0.3.0/src/si/__init__.py +20 -0
- chaincompiler_si-0.3.0/src/si/interp.py +58 -0
- chaincompiler_si-0.3.0/src/si/mcp_tree.py +57 -0
- chaincompiler_si-0.3.0/src/si/runtime.py +62 -0
- chaincompiler_si-0.3.0/src/si/scaffold.py +71 -0
- chaincompiler_si-0.3.0/src/si/server.py +77 -0
- chaincompiler_si-0.3.0/src/si/skill/SKILL.md +28 -0
- chaincompiler_si-0.3.0/tests/test_scaffold.py +40 -0
- chaincompiler_si-0.3.0/tests/test_si.py +85 -0
- chaincompiler_si-0.3.0/tests/test_si_regressions.py +32 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chaincompiler-si
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Self-Interpreter for the ChainCompiler chain dialect — an MCP that is also a skill
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: chaincompiler==0.3.0
|
|
7
|
+
Requires-Dist: chainaios==0.3.0
|
|
8
|
+
Requires-Dist: chaincompiler-accc==0.3.0
|
|
9
|
+
Requires-Dist: corcc==0.3.0
|
|
10
|
+
Requires-Dist: sccc==0.3.0
|
|
11
|
+
Requires-Dist: agent-skilltree>=0.4.0
|
|
12
|
+
Requires-Dist: fastmcp
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chaincompiler-si"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Self-Interpreter for the ChainCompiler chain dialect — an MCP that is also a skill"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = ["chaincompiler==0.3.0", "chainaios==0.3.0", "chaincompiler-accc==0.3.0", "corcc==0.3.0", "sccc==0.3.0", "agent-skilltree>=0.4.0", "fastmcp"]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
si-server = "si.server:main"
|
|
14
|
+
|
|
15
|
+
[tool.setuptools.packages.find]
|
|
16
|
+
where = ["src"]
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.package-data]
|
|
19
|
+
si = ["skill/SKILL.md"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chaincompiler-si
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Self-Interpreter for the ChainCompiler chain dialect — an MCP that is also a skill
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: chaincompiler==0.3.0
|
|
7
|
+
Requires-Dist: chainaios==0.3.0
|
|
8
|
+
Requires-Dist: chaincompiler-accc==0.3.0
|
|
9
|
+
Requires-Dist: corcc==0.3.0
|
|
10
|
+
Requires-Dist: sccc==0.3.0
|
|
11
|
+
Requires-Dist: agent-skilltree>=0.4.0
|
|
12
|
+
Requires-Dist: fastmcp
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
src/chaincompiler_si.egg-info/PKG-INFO
|
|
3
|
+
src/chaincompiler_si.egg-info/SOURCES.txt
|
|
4
|
+
src/chaincompiler_si.egg-info/dependency_links.txt
|
|
5
|
+
src/chaincompiler_si.egg-info/entry_points.txt
|
|
6
|
+
src/chaincompiler_si.egg-info/requires.txt
|
|
7
|
+
src/chaincompiler_si.egg-info/top_level.txt
|
|
8
|
+
src/si/__init__.py
|
|
9
|
+
src/si/interp.py
|
|
10
|
+
src/si/mcp_tree.py
|
|
11
|
+
src/si/runtime.py
|
|
12
|
+
src/si/scaffold.py
|
|
13
|
+
src/si/server.py
|
|
14
|
+
src/si/skill/SKILL.md
|
|
15
|
+
tests/test_scaffold.py
|
|
16
|
+
tests/test_si.py
|
|
17
|
+
tests/test_si_regressions.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
si
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""si — the Self-Interpreter for the ChainCompiler chain dialect.
|
|
2
|
+
|
|
3
|
+
A Python dialect: the interpreter defers to native Python and only injects the
|
|
4
|
+
ChainCompiler vocabulary. Ships as an MCP server (si.server) that is ALSO a skill
|
|
5
|
+
(si/skill/SKILL.md), and can turn skill trees / exchange masters into MCPs.
|
|
6
|
+
The execute arm of Compilero Bandito; the self-hosting capstone.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__version__ = "0.3.0"
|
|
11
|
+
|
|
12
|
+
from .interp import interpret, make_env
|
|
13
|
+
from .mcp_tree import build_tree_server, tree_to_mcp
|
|
14
|
+
from .runtime import reachable, read_skill, walk
|
|
15
|
+
from .scaffold import scaffold_repo, validate_node
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"interpret", "make_env", "walk", "reachable", "read_skill",
|
|
19
|
+
"build_tree_server", "tree_to_mcp", "scaffold_repo", "validate_node", "__version__",
|
|
20
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""The Self-Interpreter for the chain dialect.
|
|
2
|
+
|
|
3
|
+
The dialect is **a Python dialect** — so the interpreter does NOT re-wrap Python's
|
|
4
|
+
native ops (arithmetic, control flow, data). It runs the program with Python, and
|
|
5
|
+
only *injects the ChainCompiler vocabulary* as builtins: forge/gate/package the
|
|
6
|
+
chains, build/walk skill trees, build exchanges, turn trees into MCPs. A program
|
|
7
|
+
sets `result` (or the namespace is returned) — that's the value.
|
|
8
|
+
|
|
9
|
+
Self-interpreter: programs can themselves forge languages, build trees, and emit
|
|
10
|
+
MCPs — including ones that interpret further. ChainCompiler thereby runs itself.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def make_env(extra: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
18
|
+
"""The dialect's vocabulary — ChainCompiler primitives, injected as names."""
|
|
19
|
+
import accc
|
|
20
|
+
import chaincompiler
|
|
21
|
+
import chainaios
|
|
22
|
+
import corcc
|
|
23
|
+
import sccc
|
|
24
|
+
import skilltree
|
|
25
|
+
|
|
26
|
+
from . import runtime
|
|
27
|
+
from .mcp_tree import tree_to_mcp
|
|
28
|
+
|
|
29
|
+
env: dict[str, Any] = {
|
|
30
|
+
# construct (the AIOS top-door now lives in chainaios; write_skill stays in the CC base)
|
|
31
|
+
"construct_language": chainaios.construct_language,
|
|
32
|
+
"write_skill": chaincompiler.write_skill,
|
|
33
|
+
"forge_ac": accc.forge, "package_ac": accc.package, "gate_ac": accc.gate,
|
|
34
|
+
"forge_cor": corcc.forge_persona, "package_cor": corcc.package, "lint_cor": corcc.lint,
|
|
35
|
+
"forge_sc": sccc.forge, "package_sc": sccc.package, "lint_sc": sccc.lint,
|
|
36
|
+
# organize
|
|
37
|
+
"SkillTree": skilltree.SkillTree, "TreeNode": skilltree.TreeNode,
|
|
38
|
+
"materialize": skilltree.materialize, "validate_tree": skilltree.validate,
|
|
39
|
+
"build_exchange": skilltree.build_exchange, "load_exchange": skilltree.load_exchange,
|
|
40
|
+
# execute / search / emit
|
|
41
|
+
"walk": runtime.walk, "reachable": runtime.reachable, "read_skill": runtime.read_skill,
|
|
42
|
+
"tree_to_mcp": tree_to_mcp,
|
|
43
|
+
"result": None,
|
|
44
|
+
}
|
|
45
|
+
if extra:
|
|
46
|
+
env.update(extra)
|
|
47
|
+
return env
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def interpret(source: str, *, env: dict[str, Any] | None = None) -> Any:
|
|
51
|
+
"""Interpret a chain-dialect program. Returns `result` (or the eval of a single expr)."""
|
|
52
|
+
ns = make_env(env)
|
|
53
|
+
try:
|
|
54
|
+
code = compile(source, "<si>", "eval") # single expression?
|
|
55
|
+
return eval(code, ns)
|
|
56
|
+
except SyntaxError:
|
|
57
|
+
exec(compile(source, "<si>", "exec"), ns) # full program
|
|
58
|
+
return ns.get("result")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Turn a skill tree (or exchange master) into an MCP server.
|
|
2
|
+
|
|
3
|
+
`build_tree_server` makes a FastMCP server that exposes a materialized tree's
|
|
4
|
+
skills as tools (list / read / walk). `tree_to_mcp` writes a tiny launcher so any
|
|
5
|
+
tree or master becomes a runnable MCP — that's "make trees and masters into MCPs".
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from . import runtime
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_tree_server(root_dir: str | Path, root_name: str, *, name: str | None = None):
|
|
15
|
+
"""A FastMCP server exposing the tree's skills. (Run with .run() for stdio.)"""
|
|
16
|
+
from fastmcp import FastMCP
|
|
17
|
+
|
|
18
|
+
root_dir = str(root_dir)
|
|
19
|
+
server = FastMCP(name or f"skilltree:{root_name}")
|
|
20
|
+
|
|
21
|
+
@server.tool()
|
|
22
|
+
def list_skills() -> list[str]:
|
|
23
|
+
"""List every skill reachable from this tree's root."""
|
|
24
|
+
return runtime.reachable(root_dir, root_name)
|
|
25
|
+
|
|
26
|
+
@server.tool()
|
|
27
|
+
def read_skill(skill: str) -> str:
|
|
28
|
+
"""Return the body of a skill in this tree by name."""
|
|
29
|
+
body = runtime.read_skill(root_dir, skill)
|
|
30
|
+
return body if body is not None else f"(no skill named {skill!r} in this tree)"
|
|
31
|
+
|
|
32
|
+
@server.tool()
|
|
33
|
+
def walk() -> dict:
|
|
34
|
+
"""Return the full tree, traversed by following its breadcrumbs."""
|
|
35
|
+
return runtime.walk(root_dir, root_name)
|
|
36
|
+
|
|
37
|
+
return server
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_LAUNCHER = '''#!/usr/bin/env python3
|
|
41
|
+
"""Auto-generated MCP launcher for skill tree {name!r} (root {root!r})."""
|
|
42
|
+
from si.mcp_tree import build_tree_server
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
build_tree_server({root_dir!r}, {root!r}, name={name!r}).run()
|
|
46
|
+
'''
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def tree_to_mcp(root_dir: str | Path, root_name: str, out_path: str | Path,
|
|
50
|
+
*, name: str | None = None) -> Path:
|
|
51
|
+
"""Write a runnable MCP launcher that serves this tree/master. Returns its path."""
|
|
52
|
+
out = Path(out_path)
|
|
53
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
out.write_text(_LAUNCHER.format(root_dir=str(Path(root_dir).resolve()),
|
|
55
|
+
root=root_name, name=name or f"skilltree:{root_name}"),
|
|
56
|
+
encoding="utf-8")
|
|
57
|
+
return out
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""The execute arm — walk a SkillTree by following its Read-breadcrumbs.
|
|
2
|
+
|
|
3
|
+
This is what "running" a tree means: start at the root SKILL.md, parse the
|
|
4
|
+
breadcrumb paths, and recurse. No new mechanism — the same breadcrumbs the
|
|
5
|
+
validator checks are the ones the runtime follows. (The breadcrumb verb is the
|
|
6
|
+
Read tool, not `cat`; the path is what we follow.)
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
_CRUMB = re.compile(r"`([^`]+/SKILL\.md)`") # the backticked SKILL.md path, verb-agnostic
|
|
14
|
+
_COORD = re.compile(r"^\d+(?:\.\d+)*-") # a coordinate-addressed folder: `0.1-<name>`
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _skill_md(node_dir: Path, name: str) -> Path:
|
|
18
|
+
return Path(node_dir) / ".claude" / "skills" / name / "SKILL.md"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _strip_front(text: str) -> str:
|
|
22
|
+
return text.split("---", 2)[-1].strip() if text.lstrip().startswith("---") else text.strip()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _walk_md(md: Path) -> dict:
|
|
26
|
+
node = {"name": md.parent.name, "skill": str(md), "children": []}
|
|
27
|
+
if not md.is_file():
|
|
28
|
+
node["error"] = "missing"
|
|
29
|
+
return node
|
|
30
|
+
node_dir = md.parents[3] # <node>/.claude/skills/<name>/SKILL.md → <node>
|
|
31
|
+
for p in _CRUMB.findall(md.read_text(encoding="utf-8")):
|
|
32
|
+
child = Path(p)
|
|
33
|
+
if not child.is_absolute(): # a portable crumb is relative to the node's folder
|
|
34
|
+
child = node_dir / child # (SkillTree ≥0.4.0); a localized one is absolute
|
|
35
|
+
node["children"].append(_walk_md(child))
|
|
36
|
+
return node
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def walk(root_dir: str | Path, root_name: str) -> dict:
|
|
40
|
+
"""Traverse the tree from its root by following breadcrumbs. Returns a nested dict."""
|
|
41
|
+
return _walk_md(_skill_md(Path(root_dir), root_name))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def reachable(root_dir: str | Path, root_name: str) -> list[str]:
|
|
45
|
+
"""Flat list of every skill name reachable from the root (the execute frontier)."""
|
|
46
|
+
out: list[str] = []
|
|
47
|
+
def rec(n):
|
|
48
|
+
out.append(n["name"])
|
|
49
|
+
for c in n["children"]:
|
|
50
|
+
rec(c)
|
|
51
|
+
rec(walk(root_dir, root_name))
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def read_skill(root_dir: str | Path, name: str) -> str | None:
|
|
56
|
+
"""Search the tree for a skill by name; return its body (the search arm). A coordinate-addressed
|
|
57
|
+
tree names each skill's folder `<coord>-<name>`; the name matches with or without the coordinate."""
|
|
58
|
+
for md in Path(root_dir).rglob("SKILL.md"):
|
|
59
|
+
folder = md.parent.name
|
|
60
|
+
if folder == name or _COORD.sub("", folder) == name:
|
|
61
|
+
return _strip_front(md.read_text(encoding="utf-8"))
|
|
62
|
+
return None
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""P5.1 — scaffold a federated node repo from a skill tree.
|
|
2
|
+
|
|
3
|
+
When the SI emits an MCP, it can also stamp a ChainCompiler-SHAPED node repo: the
|
|
4
|
+
skill tree (breadcrumbs), a marketplace registry that **federates under a
|
|
5
|
+
parent**, a runnable MCP launcher, and a README. The fractal made real — each
|
|
6
|
+
emitted MCP becomes a repo like ours, its marketplace joining under ours.
|
|
7
|
+
|
|
8
|
+
Full CI mechanics (deploy workflow, site generator) are P5.2; this stamps the
|
|
9
|
+
load-bearing shape: tree + registry + MCP + README, all validatable.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from skilltree import SkillTree, Violation, materialize
|
|
17
|
+
from skilltree import validate as validate_tree
|
|
18
|
+
|
|
19
|
+
from .mcp_tree import tree_to_mcp
|
|
20
|
+
|
|
21
|
+
_NODE_README = """# {name} — a ChainCompiler node
|
|
22
|
+
|
|
23
|
+
A self-contained skill tree, served as an MCP, federated under `{parent}`.
|
|
24
|
+
|
|
25
|
+
- **Walk it** — start at `.claude/skills/{root}/SKILL.md` and Read each breadcrumb to descend.
|
|
26
|
+
- **Serve it** — `python serve_mcp.py` (an MCP exposing this tree's skills).
|
|
27
|
+
- **Marketplace** — see `registry.json`; this node registers under its parent.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def scaffold_repo(name: str, tree: SkillTree | str | Path, out_dir: str | Path,
|
|
32
|
+
*, parent: str | None = None) -> Path:
|
|
33
|
+
"""Stamp a federated node repo at <out_dir>/<name>. Returns the repo path."""
|
|
34
|
+
if isinstance(tree, (str, Path)):
|
|
35
|
+
tree = SkillTree.load(tree)
|
|
36
|
+
repo = Path(out_dir) / name
|
|
37
|
+
repo.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
materialize(tree, repo) # the node's skill tree (breadcrumbs)
|
|
40
|
+
root = tree.root.name
|
|
41
|
+
|
|
42
|
+
(repo / "registry.json").write_text(json.dumps({ # marketplace, federated
|
|
43
|
+
"name": name,
|
|
44
|
+
"parent": parent, # null at the root; else a repo ref
|
|
45
|
+
"entries": [{"name": root, "kind": "tree", "manifest": "skilltree.json",
|
|
46
|
+
"trust": "unverified"}],
|
|
47
|
+
}, indent=2), encoding="utf-8")
|
|
48
|
+
|
|
49
|
+
tree_to_mcp(repo, root, repo / "serve_mcp.py", name=f"node:{name}") # the runnable MCP
|
|
50
|
+
(repo / "README.md").write_text(
|
|
51
|
+
_NODE_README.format(name=name, parent=parent or "(root)", root=root), encoding="utf-8")
|
|
52
|
+
return repo
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def validate_node(repo: str | Path) -> list[Violation]:
|
|
56
|
+
"""Validate a scaffolded node: the tree + the registry + the MCP launcher."""
|
|
57
|
+
repo = Path(repo)
|
|
58
|
+
out = list(validate_tree(repo))
|
|
59
|
+
reg = repo / "registry.json"
|
|
60
|
+
if not reg.is_file():
|
|
61
|
+
out.append(Violation("error", repo.name, "missing registry.json"))
|
|
62
|
+
else:
|
|
63
|
+
try:
|
|
64
|
+
data = json.loads(reg.read_text(encoding="utf-8"))
|
|
65
|
+
if "parent" not in data or "entries" not in data:
|
|
66
|
+
out.append(Violation("error", repo.name, "registry.json missing parent/entries"))
|
|
67
|
+
except json.JSONDecodeError:
|
|
68
|
+
out.append(Violation("error", repo.name, "registry.json is not valid JSON"))
|
|
69
|
+
if not (repo / "serve_mcp.py").is_file():
|
|
70
|
+
out.append(Violation("error", repo.name, "missing serve_mcp.py launcher"))
|
|
71
|
+
return out
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""The Self-Interpreter as an MCP server (and it is also a skill — see si/skill/).
|
|
2
|
+
|
|
3
|
+
Exposes the chain dialect to any MCP client:
|
|
4
|
+
- si_interpret(source) run a chain-dialect program
|
|
5
|
+
- si_walk(root_dir, root_name) traverse a materialized tree
|
|
6
|
+
- si_reachable(root_dir, root_name) list reachable skills
|
|
7
|
+
- si_read_skill(root_dir, skill) fetch a skill body by name
|
|
8
|
+
- si_tree_to_mcp(root_dir, name, out) turn a tree/master into its own MCP
|
|
9
|
+
- si_search(root_dir, query) BM25 over a tree, optionally coordinate-scoped
|
|
10
|
+
|
|
11
|
+
Run: python -m si.server (or the `si-server` console script)
|
|
12
|
+
|
|
13
|
+
The tool functions stay plain callables (registered, not rebound) so they're
|
|
14
|
+
usable directly and in tests, not only over the wire.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
from fastmcp import FastMCP
|
|
21
|
+
|
|
22
|
+
from . import runtime
|
|
23
|
+
from .interp import interpret
|
|
24
|
+
from .mcp_tree import tree_to_mcp
|
|
25
|
+
|
|
26
|
+
mcp = FastMCP("self-interpreter")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def si_interpret(source: str) -> str:
|
|
30
|
+
"""Interpret a ChainCompiler chain-dialect program; returns repr(result)."""
|
|
31
|
+
return repr(interpret(source))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def si_walk(root_dir: str, root_name: str) -> dict:
|
|
35
|
+
"""Traverse a materialized skill tree by following its breadcrumbs."""
|
|
36
|
+
return runtime.walk(root_dir, root_name)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def si_reachable(root_dir: str, root_name: str) -> list[str]:
|
|
40
|
+
"""List every skill reachable from a tree's root."""
|
|
41
|
+
return runtime.reachable(root_dir, root_name)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def si_read_skill(root_dir: str, skill: str) -> str:
|
|
45
|
+
"""Search a tree for a skill by name and return its body."""
|
|
46
|
+
body = runtime.read_skill(root_dir, skill)
|
|
47
|
+
return body if body is not None else f"(no skill named {skill!r})"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def si_tree_to_mcp(root_dir: str, root_name: str, out_path: str) -> str:
|
|
51
|
+
"""Turn a skill tree (or exchange master) into a runnable MCP launcher."""
|
|
52
|
+
return str(tree_to_mcp(root_dir, root_name, out_path))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def si_search(root_dir: str, query: str, scope_coord: str | None = None, limit: int = 10) -> list[dict]:
|
|
56
|
+
"""Search the skill tree (BM25), optionally scoped to a coordinate subtree (e.g. '0.1'). SkillTree's search hands a
|
|
57
|
+
query's words to FTS5 unquoted, so it raises on a query with no word and on an operator word (`NOT`, `AND`); this
|
|
58
|
+
door sends it the query's words alone, lowercased — FTS5's operators are capitals and its index folds case, so the
|
|
59
|
+
words match as before — and answers a query with no word with nothing."""
|
|
60
|
+
words = re.findall(r"\w+", query.lower())
|
|
61
|
+
if not words:
|
|
62
|
+
return []
|
|
63
|
+
from skilltree import search_tree
|
|
64
|
+
return search_tree(root_dir, " ".join(words), scope_coord=scope_coord, limit=limit)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Register as MCP tools without rebinding the names (keep them plain callables).
|
|
68
|
+
for _fn in (si_interpret, si_walk, si_reachable, si_read_skill, si_tree_to_mcp, si_search):
|
|
69
|
+
mcp.tool()(_fn)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main() -> None:
|
|
73
|
+
mcp.run()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
main()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: self-interpreter
|
|
3
|
+
description: Run ChainCompiler chain-dialect programs and turn skill trees into MCPs. Use to forge languages, build/walk skill trees, or execute a cognition chain.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Self-Interpreter (SI)
|
|
7
|
+
|
|
8
|
+
The SI runs the **chain dialect** — a Python dialect whose vocabulary is the
|
|
9
|
+
ChainCompiler primitives (`forge_ac`, `forge_cor`, `forge_sc`, `construct_language`,
|
|
10
|
+
`SkillTree`, `TreeNode`, `materialize`, `walk`, `build_exchange`, `tree_to_mcp`, …).
|
|
11
|
+
It defers to native Python for everything else.
|
|
12
|
+
|
|
13
|
+
## As an MCP
|
|
14
|
+
|
|
15
|
+
Start it: `python -m si.server`. Tools:
|
|
16
|
+
|
|
17
|
+
- `si_interpret(source)` — run a chain-dialect program; returns `repr(result)`.
|
|
18
|
+
- `si_walk(root_dir, root_name)` / `si_reachable(...)` — traverse a skill tree.
|
|
19
|
+
- `si_read_skill(root_dir, skill)` — fetch a skill body by name.
|
|
20
|
+
- `si_tree_to_mcp(root_dir, root_name, out)` — turn a tree/master into its own MCP.
|
|
21
|
+
|
|
22
|
+
## Example program
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
tree = SkillTree(TreeNode("root", "sc", children=[TreeNode("a", "ac"), TreeNode("b", "cor")]))
|
|
26
|
+
materialize(tree, OUT)
|
|
27
|
+
result = reachable(OUT, "root") # the execute arm: walk the tree
|
|
28
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""P5.1 tests: scaffold a federated node repo and validate it."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from si import scaffold_repo, validate_node
|
|
8
|
+
from skilltree import SkillTree, TreeNode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _tree() -> SkillTree:
|
|
12
|
+
return SkillTree(TreeNode("cc-skill-tree", "sc", children=[
|
|
13
|
+
TreeNode("reason", "cor", children=[TreeNode("simplify", "ac")]),
|
|
14
|
+
]))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_scaffold_stamps_a_chaincompiler_shaped_node(tmp_path: Path):
|
|
18
|
+
repo = scaffold_repo("debug-node", _tree(), tmp_path, parent="github:sancovp/chaincompiler")
|
|
19
|
+
# the load-bearing shape: tree + registry + MCP launcher + README
|
|
20
|
+
assert (repo / ".claude" / "skills" / "cc-skill-tree" / "SKILL.md").is_file()
|
|
21
|
+
assert (repo / "skilltree.json").is_file()
|
|
22
|
+
assert (repo / "registry.json").is_file()
|
|
23
|
+
assert (repo / "serve_mcp.py").is_file()
|
|
24
|
+
assert (repo / "README.md").is_file()
|
|
25
|
+
assert validate_node(repo) == []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_registry_federates_under_parent(tmp_path: Path):
|
|
29
|
+
repo = scaffold_repo("node-x", _tree(), tmp_path, parent="github:sancovp/chaincompiler")
|
|
30
|
+
reg = json.loads((repo / "registry.json").read_text())
|
|
31
|
+
assert reg["parent"] == "github:sancovp/chaincompiler" # joins under ours
|
|
32
|
+
assert reg["entries"][0]["name"] == "cc-skill-tree"
|
|
33
|
+
assert reg["entries"][0]["trust"] == "unverified" # untrusted by default
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_validate_node_catches_a_broken_node(tmp_path: Path):
|
|
37
|
+
repo = scaffold_repo("node-y", _tree(), tmp_path)
|
|
38
|
+
(repo / "serve_mcp.py").unlink() # remove the MCP launcher
|
|
39
|
+
issues = [v for v in validate_node(repo) if v.severity == "error"]
|
|
40
|
+
assert any("serve_mcp.py" in v.message for v in issues)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""SI tests: the dialect interpreter, the walk runtime, and tree→MCP."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from si import build_tree_server, interpret, reachable, read_skill, tree_to_mcp
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_interpret_single_expression():
|
|
10
|
+
assert interpret("2 + 3") == 5 # native Python op, not re-wrapped
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_interpret_builds_and_walks_a_tree(tmp_path: Path):
|
|
14
|
+
# a chain-dialect program: build a tree with injected primitives, walk it
|
|
15
|
+
src = """
|
|
16
|
+
tree = SkillTree(TreeNode("root", "sc", children=[TreeNode("a", "ac"), TreeNode("b", "cor")]))
|
|
17
|
+
materialize(tree, OUT)
|
|
18
|
+
result = reachable(OUT, "root")
|
|
19
|
+
"""
|
|
20
|
+
out = str(tmp_path / "tree")
|
|
21
|
+
result = interpret(src, env={"OUT": out})
|
|
22
|
+
assert result == ["root", "a", "b"] # the execute arm followed the breadcrumbs
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_interpret_constructs_a_language(tmp_path: Path):
|
|
26
|
+
from corcc.notation import EINSTEIN
|
|
27
|
+
src = """
|
|
28
|
+
result = construct_language("triage", ac_chain="[A] ⇒ [B] ⇒ |C|",
|
|
29
|
+
cor_persona=PERSONA, db=DB, skills_dir=SK, out_dir=OUT)
|
|
30
|
+
"""
|
|
31
|
+
bundle = interpret(src, env={
|
|
32
|
+
"PERSONA": EINSTEIN, "DB": str(tmp_path / "x.db"),
|
|
33
|
+
"SK": str(tmp_path / "sk"), "OUT": str(tmp_path / "out"),
|
|
34
|
+
})
|
|
35
|
+
assert bundle.ac.exists() and bundle.cor.exists() and bundle.sc.exists()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_tree_to_mcp_writes_launcher(tmp_path: Path):
|
|
39
|
+
src = 'materialize(SkillTree(TreeNode("root","sc",children=[TreeNode("a","ac")])), OUT)'
|
|
40
|
+
out = str(tmp_path / "tree")
|
|
41
|
+
interpret(src, env={"OUT": out})
|
|
42
|
+
launcher = tree_to_mcp(out, "root", tmp_path / "serve_tree.py")
|
|
43
|
+
assert launcher.is_file()
|
|
44
|
+
text = launcher.read_text()
|
|
45
|
+
assert "build_tree_server" in text and "root" in text
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_build_tree_server_constructs(tmp_path: Path):
|
|
49
|
+
src = 'materialize(SkillTree(TreeNode("root","sc",children=[TreeNode("a","ac")])), OUT)'
|
|
50
|
+
out = str(tmp_path / "tree")
|
|
51
|
+
interpret(src, env={"OUT": out})
|
|
52
|
+
server = build_tree_server(out, "root")
|
|
53
|
+
assert server is not None
|
|
54
|
+
# the runtime the server wraps works
|
|
55
|
+
assert reachable(out, "root") == ["root", "a"]
|
|
56
|
+
assert read_skill(out, "a") is not None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_walk_reaches_grandchildren_of_a_portable_tree(tmp_path: Path, monkeypatch):
|
|
60
|
+
# SkillTree ≥0.4.0 writes each breadcrumb relative to its node's folder; the walk must
|
|
61
|
+
# resolve it there, not against the working directory. A one-level tree hides the
|
|
62
|
+
# difference (a missing child still records its name), so this tree has two levels.
|
|
63
|
+
from si import walk
|
|
64
|
+
src = ('materialize(SkillTree(TreeNode("root","sc",children=['
|
|
65
|
+
'TreeNode("a","cor",children=[TreeNode("a1","ac")])])), OUT)')
|
|
66
|
+
out = tmp_path / "tree"
|
|
67
|
+
interpret(src, env={"OUT": str(out)})
|
|
68
|
+
monkeypatch.chdir(tmp_path) # any working directory but the tree's own
|
|
69
|
+
assert reachable(out, "root") == ["root", "a", "a1"]
|
|
70
|
+
|
|
71
|
+
def errors(node):
|
|
72
|
+
own = [node["name"]] if "error" in node else []
|
|
73
|
+
return own + [e for child in node["children"] for e in errors(child)]
|
|
74
|
+
|
|
75
|
+
assert errors(walk(out, "root")) == []
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_read_skill_finds_a_skill_in_a_coordinate_tree(tmp_path: Path):
|
|
79
|
+
# materialize(coords=True) names each folder `<coord>-<name>`; read_skill finds it by its name
|
|
80
|
+
from skilltree import SkillTree, TreeNode, materialize
|
|
81
|
+
out = tmp_path / "tree"
|
|
82
|
+
materialize(SkillTree(TreeNode("root", "sc", children=[TreeNode("a", "ac")])), out, coords=True)
|
|
83
|
+
assert read_skill(out, "a") is not None
|
|
84
|
+
assert read_skill(out, "missing") is None
|
|
85
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""The self-interpreter's search door never crashes on what a query holds: one with no word in it finds nothing, and
|
|
2
|
+
an operator word (`NOT`, `AND`) is read as a word.
|
|
3
|
+
|
|
4
|
+
A query with no word crashes glyphsteer's index (the public repository's bug sweeps, pull requests #1 and #2), and both
|
|
5
|
+
crash SkillTree's search behind this door; the door sends SkillTree the query's words alone.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from si.server import si_search
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _tree(root: Path) -> Path:
|
|
15
|
+
skill = root / "triage" / "SKILL.md"
|
|
16
|
+
skill.parent.mkdir(parents=True)
|
|
17
|
+
skill.write_text("---\nname: triage\ndescription: triage some text\n---\nbody text\n", encoding="utf-8")
|
|
18
|
+
return root
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_si_search_answers_a_query_with_no_word_with_nothing(tmp_path):
|
|
22
|
+
root = _tree(tmp_path)
|
|
23
|
+
for query in ('"', '?"!', '""', " "):
|
|
24
|
+
assert si_search(str(root), query) == []
|
|
25
|
+
assert [h["name"] for h in si_search(str(root), "triage")] == ["triage"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_si_search_reads_operator_words_as_words(tmp_path):
|
|
29
|
+
root = _tree(tmp_path)
|
|
30
|
+
assert si_search(str(root), "NOT") == [] # FTS5's NOT, unquoted, was a syntax error
|
|
31
|
+
for query in ("triage AND", "Triage OR NOT", "NEAR triage", "triage*"):
|
|
32
|
+
assert [h["name"] for h in si_search(str(root), query)] == ["triage"], query
|