Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ jobs:
similarity-integration:
name: "Similarity precision/recall"
needs: classify
if: ${{ github.event_name != 'pull_request' || (needs.classify.outputs.docs_only != 'true' && needs.classify.outputs.graph_only != 'true' && needs.classify.outputs.similarity_changed == 'true') }}
if: ${{ github.event_name != 'pull_request' || (needs.classify.outputs.docs_only != 'true' && needs.classify.outputs.similarity_changed == 'true') }}
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/Python-3.11+-green.svg)](https://python.org)
[![PyPI](https://img.shields.io/pypi/v/claude-ctx.svg)](https://pypi.org/project/claude-ctx/)
[![Tests](https://img.shields.io/badge/Tests-3872_collected-brightgreen.svg)](#)
[![Tests](https://img.shields.io/badge/Tests-3886_collected-brightgreen.svg)](#)
[![Graph](https://img.shields.io/badge/Graph-102%2C928_nodes_/_2.9M_edges-red.svg)](graph/)
[![Docs](https://img.shields.io/badge/docs-MkDocs_Material-blue.svg)](https://stevesolun.github.io/ctx/)
[![Repo views](https://hits.sh/github.com/stevesolun/ctx.svg?label=repo%20views)](https://hits.sh/github.com/stevesolun/ctx/)
Expand Down Expand Up @@ -62,7 +62,7 @@ ctx-init --graph --graph-install-mode full
```

The full `wiki-graph.tar.gz` includes the shipped skill index,
91,463 skill entity pages under `entities/skills/`, 89,465 hydrated
91,464 skill entity pages under `entities/skills/`, 89,465 hydrated
installable `SKILL.md` files under `converted/`,
and 207 harness pages under
`entities/harnesses/`.
Expand Down
21 changes: 9 additions & 12 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ hide:

[![Repo views](https://hits.sh/github.com/stevesolun/ctx.svg?label=repo%20views)](https://hits.sh/github.com/stevesolun/ctx/)

Watches what you develop, walks a knowledge graph of **91,463 skill pages, 467
agents, 10,788 MCP servers, and 207 harnesses**, and recommends the
Watches what you develop, walks a knowledge graph of **91,464 skill pages, 467 agents, 10,790 MCP servers, and 207 cataloged harnesses**, and recommends the
right execution bundle on the fly. The live execution bundle is skills,
agents, and MCP servers only; custom/API/local model users get a separate
harness recommendation based on model choice and task goal. You decide
Expand Down Expand Up @@ -91,12 +90,12 @@ graph-based discovery:

- A Karpathy 3-layer wiki at `~/.claude/skill-wiki/` is the single source
of truth.
- **102,925 graph nodes** for the shipped skill/agent/MCP/harness
inventory, including 91,463 skill pages
- **102,928 graph nodes** for the shipped skill/agent/MCP/harness
inventory, including 91,464 skill pages
and 207 harness pages under `entities/harnesses/`.
Each page tracks tags, status, provenance, and usage where it applies.
- A **knowledge graph** (102,925 nodes, 2,913,930 edges) built from a
13,460-node core plus 89,465 body-backed skill nodes.
- A **knowledge graph** (102,928 nodes, 2,913,960 edges) built from a
13,463-node core plus 89,465 body-backed skill nodes.
The graph has 52 Louvain communities and blends semantic cosine,
tag overlap, and slug-token overlap; 89,465 skill bodies are
shipped as installable `SKILL.md` files. Entries over the configured line
Expand Down Expand Up @@ -132,9 +131,8 @@ ones are flagged. New ones self-ingest.

---

102,925 shipped graph nodes: 13,460 curated skill/agent/MCP/harness
nodes plus 89,465 body-backed skill nodes. The graph has
2,913,930 weighted edges and 52 Louvain communities.
102,928 shipped graph nodes: 13,463 curated skill/agent/MCP/harness nodes plus 89,465 body-backed skill nodes. The graph has
2,913,960 weighted edges and 52 Louvain communities.
Ships pre-built in `graph/wiki-graph.tar.gz` and powers the
graph-aware recommendations + the pre-ship `ctx-dedup-check` gate.

Expand Down Expand Up @@ -204,14 +202,13 @@ ones are flagged. New ones self-ingest.
---

**v1.0.9** — MIT, CI-matrixed (Ubuntu 3.12 plus Windows/macOS 3.11/3.12),
3,872 tests collected. Ships console scripts including `ctx-init`,
3,886 tests collected. Ships console scripts including `ctx-init`,
`ctx-monitor` (local dashboard with graph + wiki + load/unload for
skills, agents, and MCP servers, plus Harness Setup for user-owned LLMs),
`ctx-incremental-attach`, `ctx-incremental-shadow`, `ctx-dedup-check`
(pre-ship near-duplicate gate), and
`ctx-tag-backfill` (entity hygiene), plus a fast runtime graph artifact
and the full ~439 MiB wiki tarball with **102,925 nodes / 2,913,930
edges / 52 Louvain communities**.
and the full ~439 MiB wiki tarball with **102,928 nodes / 2,913,960 edges / 52 Louvain communities**.

[:octicons-arrow-right-24: CHANGELOG](https://github.com/stevesolun/ctx/blob/main/CHANGELOG.md) ·
[Repository](https://github.com/stevesolun/ctx)
Expand Down
23 changes: 11 additions & 12 deletions docs/knowledge-graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ agents, and MCP servers.
## What's in it

Authoritative numbers from the shipped tarball. The curated-core snapshot
is **13,460 nodes** (1,998 curated skills + 467 agents + 10,788 MCP servers
+ 207 harnesses). Harness pages under `entities/harnesses/` are ingested into
is **13,463 nodes** (1,999 curated skills + 467 agents + 10,790 MCP servers + 207 harnesses). Harness pages under `entities/harnesses/` are ingested into
local rebuilds and the separate harness recommendation path. The
tarball also carries **91,463 skill pages**; **89,465**
tarball also carries **91,464 skill pages**; **89,465**
skill bodies are hydrated as installable `SKILL.md` files under
`converted/`; the **28,612** entries over the configured line
limit were converted to gated micro-skill orchestrators. Full original bodies
Expand All @@ -26,18 +25,18 @@ are omitted from the shipped tarball.

| | Count |
|---|---:|
| Total nodes | **102,925** |
| Curated core nodes | **13,460** (1,998 skills + 467 agents + 10,788 MCP servers + 207 harnesses) |
| Total nodes | **102,928** |
| Curated core nodes | **13,463** (1,999 skills + 467 agents + 10,790 MCP servers + 207 harnesses) |
| Body-backed skill nodes | **89,465** hydrated installable skill entries |
| Total edges | **2,913,930** |
| Total edges | **2,913,960** |
| Hydrated skill incident edges | **2,605,721** |
| Hydrated skill semantic incident edges | **1,500,648** |
| Communities | **52** (Louvain) |
| Edge sources (overlap-deduped) | semantic 1,683,163 - tag 897,754 - token 433,245 |
| Cross-type edges (skill <-> agent) | ~67K |
| Cross-type edges (skill <-> MCP) | ~41K |
| Cross-type edges (agent <-> MCP) | ~223 |
| Harness edges | **6,571** |
| Edge sources (overlap-deduped) | semantic 1,683,193 - tag 897,784 - token 433,245 |
| Cross-type edges (skill <-> agent) | ~66,799 |
| Cross-type edges (skill <-> MCP) | ~41,521 |
| Cross-type edges (agent <-> MCP) | ~229 |
| Harness edges | **6,576** |
| Shipped skill index | **89,465** observed body-backed skill entries |

## Install
Expand Down Expand Up @@ -169,7 +168,7 @@ raw = json.loads(
edges_key = "links" if "links" in raw else "edges"
G = node_link_graph(raw, edges=edges_key)

# 102,925 nodes, 2,913,930 edges
# 102,928 nodes, 2,913,960 edges
print(G.number_of_nodes(), G.number_of_edges())

# Find entities related to 'fastapi-pro' by edge weight
Expand Down
11 changes: 10 additions & 1 deletion scripts/ci_required.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ def failed_required_jobs(
event_name == "pull_request"
and _job_output(needs, "classify", "graph_artifact_changed") == "true"
)
similarity_changed_pr = (
event_name == "pull_request"
and _job_output(needs, "classify", "similarity_changed") == "true"
)
cheap_pr = docs_only_pr or graph_only_pr
for name, details in sorted(needs.items()):
result = details.get("result")
Expand All @@ -86,7 +90,12 @@ def failed_required_jobs(
and result == "skipped"
):
continue
if cheap_pr and name in CHEAP_PR_SKIPPABLE_JOBS and result == "skipped":
if (
cheap_pr
and name in CHEAP_PR_SKIPPABLE_JOBS
and not (name == "similarity-integration" and similarity_changed_pr)
and result == "skipped"
):
continue
if (
event_name == "pull_request"
Expand Down
9 changes: 9 additions & 0 deletions src/agent_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ def install_agent(source: Path, agents_dir: Path, name: str) -> Path:
return dest


def mirror_agent_body(installed_path: Path, wiki_path: Path, name: str) -> Path:
"""Mirror the installed agent body into the wiki install source tree."""
mirror_root = wiki_path / "converted-agents"
dest = mirror_root / f"{name}.md"
safe_copy_file(installed_path, dest, dest_root=mirror_root)
return dest


def write_entity_page(wiki_path: Path, name: str, content: str) -> bool:
"""Write agent entity page. Returns True if newly created."""
page = wiki_path / "entities" / "agents" / f"{name}.md"
Expand Down Expand Up @@ -147,6 +155,7 @@ def add_agent(

# 1. Install into agents-dir.
installed_path = install_agent(source_path, agents_dir, name)
mirror_agent_body(installed_path, wiki_path, name)

# 2. Record embedding. Non-fatal on failure — install already
# succeeded and a missing vector only weakens the next check.
Expand Down
4 changes: 2 additions & 2 deletions src/ctx/adapters/generic/ctx_core_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,8 @@ def _dispatch_recommend(self, args: dict[str, Any]) -> str:
)

tags = _query_to_tags(query)
if not tags:
use_semantic_query = bool(args.get("use_semantic_query"))
if not tags and not use_semantic_query:
return json.dumps({
"error": "query produced no usable tags",
"results": [],
Expand All @@ -335,7 +336,6 @@ def _dispatch_recommend(self, args: dict[str, Any]) -> str:

from ctx.core.resolve.recommendations import recommend_by_tags # noqa: PLC0415

use_semantic_query = bool(args.get("use_semantic_query"))
if use_semantic_query:
self._refresh_semantic_cache_signature()
raw = recommend_by_tags(
Expand Down
19 changes: 16 additions & 3 deletions src/ctx/core/graph/incremental_attach.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from ctx.core.graph.vector_index import load_vector_index

_PERCENTILES = (50, 60, 75, 90, 95)
_DEFAULT_MIN_SEMANTIC_SCORE = 0.75
_DEFAULT_MIN_SEMANTIC_SCORE = 0.80
_DEFAULT_MIN_FINAL_WEIGHT = 0.03
_ATTACH_METHOD = "ann_attach_v1"

Expand Down Expand Up @@ -218,7 +218,7 @@ def main(argv: list[str] | None = None) -> int:
attach.add_argument("--embedding-backend", default="sentence-transformers")
attach.add_argument("--embedding-model")
attach.add_argument("--top-k", type=int, default=20)
attach.add_argument("--min-score", type=float, default=_DEFAULT_MIN_SEMANTIC_SCORE)
attach.add_argument("--min-score", type=float)
attach.add_argument("--min-final-weight", type=float, default=_DEFAULT_MIN_FINAL_WEIGHT)
attach.add_argument("--dry-run", action="store_true", help="Print the overlay record without writing")
attach.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
Expand Down Expand Up @@ -246,7 +246,11 @@ def main(argv: list[str] | None = None) -> int:
vector_json=args.vector_json,
model_id=args.model_id,
top_k=args.top_k,
min_score=args.min_score,
min_score=(
args.min_score
if args.min_score is not None
else _default_min_semantic_score()
),
min_final_weight=args.min_final_weight,
dry_run=args.dry_run,
embedding_backend=args.embedding_backend,
Expand Down Expand Up @@ -275,6 +279,15 @@ def _resolve_text_input(text: str | None, text_file: str | None) -> str | None:
raise ValueError(f"cannot read --text-file {path}") from exc


def _default_min_semantic_score() -> float:
try:
from ctx_config import cfg # noqa: PLC0415

return float(cfg.graph_semantic_min_cosine)
except Exception: # pragma: no cover - standalone CLI fallback.
return _DEFAULT_MIN_SEMANTIC_SCORE


def _read_index_meta(index_dir: Path) -> dict[str, Any]:
meta_path = index_dir / "vector-index.meta.json"
try:
Expand Down
25 changes: 24 additions & 1 deletion src/ctx_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,20 @@ def _upsert_wiki_entity(payload: dict[str, Any]) -> tuple[bool, str]:
return True, f"saved {entity_type}:{slug} and queued graph refresh"


def _entity_live_in_manifest(slug: str, entity_type: str) -> bool:
manifest = _read_manifest()
for entry in manifest.get("load", []):
if not isinstance(entry, dict):
continue
entry_slug = str(entry.get("skill") or entry.get("slug") or "")
entry_type = _normalize_dashboard_entity_type(
str(entry.get("entity_type") or entry.get("type") or "skill"),
)
if entry_slug == slug and entry_type == entity_type:
return True
return False


def _delete_wiki_entity(slug: str, entity_type: str) -> tuple[bool, str]:
try:
normalized = _normalize_dashboard_entity_type(entity_type)
Expand All @@ -538,6 +552,14 @@ def _delete_wiki_entity(slug: str, entity_type: str) -> tuple[bool, str]:
path = _wiki_entity_path(slug, entity_type=normalized)
if path is None:
return False, f"no wiki entity found for {normalized}:{slug}"
if _entity_live_in_manifest(slug, normalized):
unloaded, unload_detail = _perform_unload(slug, normalized)
if not unloaded:
return (
False,
f"{normalized}:{slug} is loaded; unload before delete failed: "
f"{unload_detail}",
)
with file_lock(path):
path.unlink()
_queue_entity_refresh(
Expand Down Expand Up @@ -6409,8 +6431,9 @@ def _api_reads_enabled(self) -> bool:
return self._mutations_enabled()

def _read_authorized(self, qs: dict[str, str]) -> bool:
request_host = _request_host_name(self.headers.get("Host", ""))
if self._mutations_enabled():
return True
return _host_allows_mutations(request_host)
token = self.headers.get("X-CTX-Monitor-Token") or qs.get("token", "")
return bool(_MONITOR_TOKEN) and secrets.compare_digest(token, _MONITOR_TOKEN)

Expand Down
9 changes: 9 additions & 0 deletions src/skill_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ def install_skill(source: Path, skills_dir: Path, name: str) -> Path:
return dest


def mirror_install_body(installed_path: Path, name: str, converted_root: Path) -> Path:
"""Mirror a short installed SKILL.md into the wiki install source tree."""
dest = converted_root / name / "SKILL.md"
safe_copy_file(installed_path, dest, dest_root=converted_root)
return dest


# ── Conversion ────────────────────────────────────────────────────────────────

def maybe_convert(
Expand Down Expand Up @@ -368,6 +375,8 @@ def add_skill(
# 2. Convert if above threshold
converted_root = wiki_path / "converted"
converted, pipeline_path = maybe_convert(installed_path, name, converted_root, line_count)
if not converted:
mirror_install_body(installed_path, name, converted_root)

# 3. Detect related skills and scan sources (before writing new page)
related = find_related_skills(wiki_path, name, tags)
Expand Down
50 changes: 50 additions & 0 deletions src/tests/test_agent_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,56 @@ def test_existing_agent_update_existing_applies_change(
)


def test_new_agent_add_writes_converted_agent_mirror(
tmp_path: Path,
monkeypatch: Any,
) -> None:
wiki, agents_dir, source = _setup_paths(tmp_path)
source_text = _agent_text(description="Installable mirrored agent.")
source.write_text(source_text, encoding="utf-8")
_patch_side_effects(monkeypatch)

result = agent_add.add_agent(
source_path=source,
name="reviewer-agent",
wiki_path=wiki,
agents_dir=agents_dir,
)

mirror = wiki / "converted-agents" / "reviewer-agent.md"
assert result["is_new_page"] is True
assert mirror.read_text(encoding="utf-8") == source_text


def test_existing_agent_update_refreshes_converted_agent_mirror(
tmp_path: Path,
monkeypatch: Any,
) -> None:
wiki, agents_dir, source = _setup_paths(tmp_path)
installed = agents_dir / "reviewer-agent.md"
installed.write_text(_agent_text(), encoding="utf-8")
mirror = wiki / "converted-agents" / "reviewer-agent.md"
mirror.parent.mkdir(parents=True)
mirror.write_text("old mirror\n", encoding="utf-8")
entity = wiki / "entities" / "agents" / "reviewer-agent.md"
entity.write_text("# existing entity\n", encoding="utf-8")
updated_text = _agent_text(description="Updated mirrored agent.")
source.write_text(updated_text, encoding="utf-8")
_patch_side_effects(monkeypatch)

result = agent_add.add_agent(
source_path=source,
name="reviewer-agent",
wiki_path=wiki,
agents_dir=agents_dir,
review_existing=True,
update_existing=True,
)

assert result["is_new_page"] is False
assert mirror.read_text(encoding="utf-8") == updated_text


def test_main_existing_agent_prints_update_review(
tmp_path: Path,
monkeypatch: Any,
Expand Down
19 changes: 19 additions & 0 deletions src/tests/test_ci_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,25 @@ def test_ci_required_rejects_missing_similarity_gate_on_source_pr() -> None:
}


def test_ci_required_rejects_missing_similarity_gate_on_graph_only_pr() -> None:
needs = _required_needs(
classify={
"result": "success",
"outputs": {
"docs_only": "false",
"graph_only": "true",
"graph_artifact_changed": "true",
"similarity_changed": "true",
},
},
**{"similarity-integration": {"result": "skipped"}},
)

assert failed_required_jobs(needs, event_name="pull_request") == {
"similarity-integration": "skipped",
}


def test_ci_required_allows_similarity_skip_for_unrelated_source_pr() -> None:
needs = _required_needs(
classify={
Expand Down
Loading
Loading