@mamdouh-aboammar/agentic-workflow 1.2.0
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.
- package/.claude-plugin/plugin.json +10 -0
- package/.codex-plugin/plugin.json +13 -0
- package/.skills.json +19 -0
- package/AGENTS.md +1344 -0
- package/CLAUDE.md +178 -0
- package/GEMINI.md +102 -0
- package/LICENSE +21 -0
- package/README.md +350 -0
- package/SKILL.md +132 -0
- package/bin/agentic-hooks.sh +79 -0
- package/bin/cli.js +1060 -0
- package/core/__init__.py +52 -0
- package/core/ai_evaluator.py +117 -0
- package/core/autopilot_engine.py +368 -0
- package/core/clean_code_guard.py +188 -0
- package/core/engine_py/__init__.py +29 -0
- package/core/engine_py/agent_worker.py +136 -0
- package/core/engine_py/decider.py +150 -0
- package/core/engine_py/energy.py +45 -0
- package/core/engine_py/event_bus.py +63 -0
- package/core/engine_py/executor.py +186 -0
- package/core/engine_py/models.py +193 -0
- package/core/engine_py/queue.py +314 -0
- package/core/engine_py/runner.py +116 -0
- package/core/engine_py/system_workers.py +70 -0
- package/core/engine_py/toon_adapter.py +586 -0
- package/core/engine_py/verification_controller.py +208 -0
- package/core/engine_py/worker.py +167 -0
- package/core/engine_spec/event_schema.json +65 -0
- package/core/engine_spec/example_workflow.yaml +73 -0
- package/core/engine_spec/workflow_schema.json +127 -0
- package/core/hooks/__init__.py +29 -0
- package/core/hooks/adapters/__init__.py +25 -0
- package/core/hooks/adapters/claude_adapter.py +83 -0
- package/core/hooks/adapters/cli_agent_adapter.py +82 -0
- package/core/hooks/adapters/codex_adapter.py +78 -0
- package/core/hooks/adapters/cursor_adapter.py +73 -0
- package/core/hooks/adapters/gemini_adapter.py +93 -0
- package/core/hooks/adapters/homebrew_adapter.py +69 -0
- package/core/hooks/adapters/mcp_proxy.py +133 -0
- package/core/hooks/adapters/shell_adapter.py +65 -0
- package/core/hooks/dispatcher.py +118 -0
- package/core/hooks/policy_engine.py +375 -0
- package/core/hooks/session_end.py +141 -0
- package/core/hooks/types.py +147 -0
- package/core/integrations/__init__.py +28 -0
- package/core/integrations/installer.py +225 -0
- package/core/integrations/lifecycle_director.py +175 -0
- package/core/integrations/registry.py +105 -0
- package/core/multi_agent_system.py +164 -0
- package/core/skills_indexer.py +742 -0
- package/core/system/__init__.py +25 -0
- package/core/system/announcements.py +72 -0
- package/core/system/dependencies.py +69 -0
- package/core/system/doctor.py +171 -0
- package/core/system/health.py +144 -0
- package/core/system/installer.py +137 -0
- package/core/system/notifications.py +97 -0
- package/core/system/refresher.py +110 -0
- package/core/system/updater.py +167 -0
- package/core/system/version_tracker.py +65 -0
- package/docs/architecture_plan.md +7 -0
- package/docs/guides/failure-recovery.md +714 -0
- package/docs/implementation_summary.md +10 -0
- package/docs/protocols/autopilot-execution.md +148 -0
- package/docs/protocols/code-change-protocol.md +49 -0
- package/docs/protocols/context-preservation-detail.md +114 -0
- package/docs/protocols/quality-gates.md +110 -0
- package/docs/protocols/ulw-mode.md +60 -0
- package/docs/research_findings.md +10 -0
- package/docs/solutions/autonomous-autopilot-engine-architecture.md +38 -0
- package/install.sh +111 -0
- package/marketplace.json +37 -0
- package/package.json +81 -0
- package/skills/agentic-workflow/SKILL.md +132 -0
- package/skills/agentic-workflow/skill-spec.json +100 -0
- package/soul.md +445 -0
- package/src/engine_ts/decider.ts +186 -0
- package/src/engine_ts/event-bus.ts +57 -0
- package/src/engine_ts/executor.ts +262 -0
- package/src/engine_ts/index.ts +12 -0
- package/src/engine_ts/queue.ts +93 -0
- package/src/engine_ts/runner.ts +108 -0
- package/src/engine_ts/skills-indexer.ts +264 -0
- package/src/engine_ts/toon-adapter.ts +91 -0
- package/src/engine_ts/types.ts +134 -0
- package/src/engine_ts/verification-controller.ts +204 -0
- package/src/engine_ts/worker.ts +280 -0
- package/src/hooks/adapters/claude-adapter.ts +54 -0
- package/src/hooks/adapters/cli-agent-adapter.ts +46 -0
- package/src/hooks/adapters/codex-adapter.ts +69 -0
- package/src/hooks/adapters/cursor-adapter.ts +60 -0
- package/src/hooks/adapters/gemini-adapter.ts +71 -0
- package/src/hooks/adapters/homebrew-adapter.ts +36 -0
- package/src/hooks/adapters/mcp-proxy.ts +66 -0
- package/src/hooks/adapters/shell-adapter.ts +42 -0
- package/src/hooks/dispatcher.ts +113 -0
- package/src/hooks/index.ts +16 -0
- package/src/hooks/policy-engine.ts +376 -0
- package/src/hooks/session-end.ts +125 -0
- package/src/hooks/types.ts +61 -0
- package/src/index.d.ts +34 -0
- package/src/index.ts +23 -0
- package/src/integrations/index.ts +7 -0
- package/src/integrations/installer.ts +208 -0
- package/src/integrations/lifecycle-director.ts +139 -0
- package/src/integrations/registry.ts +82 -0
- package/src/system/announcements.ts +143 -0
- package/src/system/dependencies.ts +176 -0
- package/src/system/doctor.ts +374 -0
- package/src/system/health.ts +270 -0
- package/src/system/index.ts +14 -0
- package/src/system/installer.ts +262 -0
- package/src/system/notifications.ts +180 -0
- package/src/system/refresher.ts +207 -0
- package/src/system/types.ts +268 -0
- package/src/system/updater.ts +219 -0
- package/src/system/version-tracker.ts +137 -0
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
skills_indexer.py — Agentic Skills Mesh & Universal Auto-Indexer
|
|
4
|
+
|
|
5
|
+
Implements:
|
|
6
|
+
1. Multi-path Auto-Discovery across local user environments (~/.gemini, ~/.claude, ~/.agents, ~/.agent-kernel, workspace)
|
|
7
|
+
2. Semantic Agentic Node Extraction:
|
|
8
|
+
- Types: skill, workflow, agent, playbook, rule, guard
|
|
9
|
+
- Metadata: id, name, version, author, description, tags
|
|
10
|
+
- Triggers: slash commands, intent keywords, file patterns
|
|
11
|
+
- Execution: tools required, rules & guards, graph edges (links)
|
|
12
|
+
3. Dual-Format Synchronized Indexing:
|
|
13
|
+
- skills-index.json: Full typed graph representation
|
|
14
|
+
- skills-index.toon: High-density Token-Optimized Object Notation for LLM context injection (~65% token savings)
|
|
15
|
+
4. Runtime Skills Mesh & Intent Resolver for Autopilot and Coding Agents.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import time
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from dataclasses import dataclass, field, asdict
|
|
25
|
+
from typing import Dict, List, Optional, Set, Tuple, Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NodeType(str, Enum):
|
|
29
|
+
SKILL = "skill"
|
|
30
|
+
WORKFLOW = "workflow"
|
|
31
|
+
AGENT = "agent"
|
|
32
|
+
PLAYBOOK = "playbook"
|
|
33
|
+
RULE = "rule"
|
|
34
|
+
GUARD = "guard"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class AgenticNode:
|
|
39
|
+
id: str
|
|
40
|
+
name: str
|
|
41
|
+
type: NodeType
|
|
42
|
+
path: str
|
|
43
|
+
description: str = ""
|
|
44
|
+
version: str = "1.0.0"
|
|
45
|
+
author: str = ""
|
|
46
|
+
tags: List[str] = field(default_factory=list)
|
|
47
|
+
triggers: List[str] = field(default_factory=list)
|
|
48
|
+
tools_required: List[str] = field(default_factory=list)
|
|
49
|
+
rules_and_guards: List[str] = field(default_factory=list)
|
|
50
|
+
edges: List[str] = field(default_factory=list) # Related or dependent node IDs
|
|
51
|
+
source_dir: str = ""
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
54
|
+
d = asdict(self)
|
|
55
|
+
d["type"] = self.type.value
|
|
56
|
+
return d
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_dict(cls, data: Dict[str, Any]) -> "AgenticNode":
|
|
60
|
+
data_copy = dict(data)
|
|
61
|
+
if "type" in data_copy and isinstance(data_copy["type"], str):
|
|
62
|
+
try:
|
|
63
|
+
data_copy["type"] = NodeType(data_copy["type"])
|
|
64
|
+
except ValueError:
|
|
65
|
+
data_copy["type"] = NodeType.SKILL
|
|
66
|
+
return cls(**data_copy)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class FrontmatterParser:
|
|
70
|
+
"""Extracts and parses YAML or JSON frontmatter and markdown sections."""
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def parse_markdown(content: str) -> Tuple[Dict[str, Any], str]:
|
|
74
|
+
frontmatter: Dict[str, Any] = {}
|
|
75
|
+
body = content
|
|
76
|
+
|
|
77
|
+
# Match YAML frontmatter between ---
|
|
78
|
+
yaml_match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)$", content, re.DOTALL)
|
|
79
|
+
if yaml_match:
|
|
80
|
+
raw_yaml = yaml_match.group(1)
|
|
81
|
+
body = yaml_match.group(2)
|
|
82
|
+
for line in raw_yaml.split("\n"):
|
|
83
|
+
line = line.strip()
|
|
84
|
+
if not line or line.startswith("#"):
|
|
85
|
+
continue
|
|
86
|
+
if ":" in line:
|
|
87
|
+
key, val = line.split(":", 1)
|
|
88
|
+
key = key.strip()
|
|
89
|
+
val = val.strip().strip("\"'")
|
|
90
|
+
if val.startswith("[") and val.endswith("]"):
|
|
91
|
+
# Simple list parse
|
|
92
|
+
items = [x.strip().strip("\"'") for x in val[1:-1].split(",") if x.strip()]
|
|
93
|
+
frontmatter[key] = items
|
|
94
|
+
else:
|
|
95
|
+
frontmatter[key] = val
|
|
96
|
+
|
|
97
|
+
return frontmatter, body
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class NodeClassifier:
|
|
101
|
+
"""Classifies a skill into an Agentic Node type based on semantics and naming."""
|
|
102
|
+
|
|
103
|
+
GUARD_PATTERNS = ["guard", "security", "protect", "block", "secret-filter", "safety", "tdd"]
|
|
104
|
+
WORKFLOW_PATTERNS = ["workflow", "pipeline", "orchestrat", "gsd-", "phase", "loop", "lifecycle"]
|
|
105
|
+
AGENT_PATTERNS = ["agency-", "engineer", "specialist", "architect", "reviewer", "tester", "guardian", "developer", "lead"]
|
|
106
|
+
PLAYBOOK_PATTERNS = ["playbook", "guide", "handbook", "best-practice", "convention", "patterns", "how-to"]
|
|
107
|
+
RULE_PATTERNS = ["rule", "constitution", "invariant", "directive", "governance", "standard"]
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def classify(cls, node_id: str, name: str, description: str, body: str) -> NodeType:
|
|
111
|
+
text = f"{node_id} {name} {description}".lower()
|
|
112
|
+
|
|
113
|
+
for g in cls.GUARD_PATTERNS:
|
|
114
|
+
if g in text:
|
|
115
|
+
return NodeType.GUARD
|
|
116
|
+
|
|
117
|
+
for r in cls.RULE_PATTERNS:
|
|
118
|
+
if r in text and ("must" in body.lower() or "never" in body.lower() or "rule" in text):
|
|
119
|
+
return NodeType.RULE
|
|
120
|
+
|
|
121
|
+
for a in cls.AGENT_PATTERNS:
|
|
122
|
+
if a in text or node_id.startswith("agency-"):
|
|
123
|
+
return NodeType.AGENT
|
|
124
|
+
|
|
125
|
+
for w in cls.WORKFLOW_PATTERNS:
|
|
126
|
+
if w in text or node_id.startswith("gsd-"):
|
|
127
|
+
return NodeType.WORKFLOW
|
|
128
|
+
|
|
129
|
+
for p in cls.PLAYBOOK_PATTERNS:
|
|
130
|
+
if p in text or "step 1" in body.lower() or "procedure" in body.lower():
|
|
131
|
+
return NodeType.PLAYBOOK
|
|
132
|
+
|
|
133
|
+
return NodeType.SKILL
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TriggerExtractor:
|
|
137
|
+
"""Extracts slash commands, trigger keywords, and tool requirements."""
|
|
138
|
+
|
|
139
|
+
SLASH_CMD_RE = re.compile(r"(/(?:[a-zA-Z0-9_\-]+))")
|
|
140
|
+
TOOL_RE = re.compile(r"\b(view_file|write_to_file|replace_file_content|run_command|read_url_content|search_web|find_by_name|grep_search|list_dir|ask_question|generate_image|bash)\b", re.IGNORECASE)
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def extract_triggers(cls, node_id: str, name: str, description: str, body: str, tags: List[str]) -> List[str]:
|
|
144
|
+
triggers: Set[str] = set()
|
|
145
|
+
|
|
146
|
+
# Add node id and words from name
|
|
147
|
+
triggers.add(node_id.lower())
|
|
148
|
+
for word in re.split(r"[\s\-_]+", name.lower()):
|
|
149
|
+
if len(word) > 2 and word not in ["the", "and", "for", "with", "app"]:
|
|
150
|
+
triggers.add(word)
|
|
151
|
+
|
|
152
|
+
# Add tags
|
|
153
|
+
for t in tags:
|
|
154
|
+
triggers.add(t.lower())
|
|
155
|
+
|
|
156
|
+
# Slash commands mentioned in text
|
|
157
|
+
slash_cmds = cls.SLASH_CMD_RE.findall(body[:2000])
|
|
158
|
+
for sc in slash_cmds:
|
|
159
|
+
if len(sc) > 2 and not sc.startswith("/Users") and not sc.startswith("/var"):
|
|
160
|
+
triggers.add(sc.lower())
|
|
161
|
+
|
|
162
|
+
# Keywords from description
|
|
163
|
+
desc_words = re.split(r"[\s\-_,.:;]+", description.lower())
|
|
164
|
+
for w in desc_words:
|
|
165
|
+
if len(w) > 3 and w not in ["this", "that", "with", "when", "using", "from"]:
|
|
166
|
+
triggers.add(w)
|
|
167
|
+
|
|
168
|
+
return sorted(list(triggers))[:15]
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def extract_tools(cls, body: str) -> List[str]:
|
|
172
|
+
matches = cls.TOOL_RE.findall(body)
|
|
173
|
+
unique_tools = set()
|
|
174
|
+
for m in matches:
|
|
175
|
+
norm = m.lower()
|
|
176
|
+
if norm == "bash":
|
|
177
|
+
norm = "run_command"
|
|
178
|
+
unique_tools.add(norm)
|
|
179
|
+
return sorted(list(unique_tools))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class EdgeResolver:
|
|
183
|
+
"""Discovers relationships, prerequisites, and complementary links between nodes."""
|
|
184
|
+
|
|
185
|
+
COMPLEMENTARY_MAP = {
|
|
186
|
+
"omni-skill": ["skill-conductor", "skill-architect", "skill-portability-compiler", "skill-evaluator", "agentic-workflow"],
|
|
187
|
+
"agentic-workflow": ["omni-skill", "workflow-generator", "clean-code-guard", "fable-tdd"],
|
|
188
|
+
"test-driven-development": ["test-guard", "clean-code-guard", "fable-tdd"],
|
|
189
|
+
"fable-tdd": ["test-guard", "clean-code-guard", "test-driven-development"],
|
|
190
|
+
"clean-code-guard": ["test-guard", "autoreview"],
|
|
191
|
+
"workflow-generator": ["gsd-plan-phase", "architecture-guardian"],
|
|
192
|
+
"agency-senior-developer": ["agency-code-reviewer", "agency-reality-checker", "clean-code-guard"],
|
|
193
|
+
"agency-code-reviewer": ["agency-reality-checker", "clean-code-guard", "test-guard"],
|
|
194
|
+
"agency-api-tester": ["test-guard", "clean-code-guard"],
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def resolve_edges(cls, node_id: str, all_node_ids: Set[str], body: str) -> List[str]:
|
|
199
|
+
edges: Set[str] = set()
|
|
200
|
+
|
|
201
|
+
# Check pre-defined complementaries
|
|
202
|
+
if node_id in cls.COMPLEMENTARY_MAP:
|
|
203
|
+
for c in cls.COMPLEMENTARY_MAP[node_id]:
|
|
204
|
+
if c in all_node_ids:
|
|
205
|
+
edges.add(c)
|
|
206
|
+
|
|
207
|
+
# Check body text mentions of other skill IDs
|
|
208
|
+
for other_id in all_node_ids:
|
|
209
|
+
if other_id != node_id and len(other_id) > 4:
|
|
210
|
+
if other_id in body:
|
|
211
|
+
edges.add(other_id)
|
|
212
|
+
|
|
213
|
+
# Prefix clustering (e.g. gsd-* or fable-* or 21st-*)
|
|
214
|
+
prefix = node_id.split("-")[0] if "-" in node_id else ""
|
|
215
|
+
if prefix in ["gsd", "fable", "agency", "ce"]:
|
|
216
|
+
related_in_cluster = [
|
|
217
|
+
n for n in all_node_ids
|
|
218
|
+
if n.startswith(prefix + "-") and n != node_id
|
|
219
|
+
]
|
|
220
|
+
for r in related_in_cluster[:3]:
|
|
221
|
+
edges.add(r)
|
|
222
|
+
|
|
223
|
+
return sorted(list(edges))[:8]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class ToonFormatter:
|
|
227
|
+
"""
|
|
228
|
+
Token-Optimized Object Notation (TOON) Serializer.
|
|
229
|
+
|
|
230
|
+
A compact, human-readable, token-dense serialization format that cuts LLM
|
|
231
|
+
context token cost by ~65% compared to JSON.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def format_node(node: AgenticNode) -> str:
|
|
236
|
+
tools_str = ",".join(sorted(node.tools_required)) if node.tools_required else "none"
|
|
237
|
+
triggers_str = ",".join(sorted(node.triggers[:8])) if node.triggers else ""
|
|
238
|
+
links_str = ",".join(sorted(node.edges)) if node.edges else ""
|
|
239
|
+
rules_str = ",".join(sorted(node.rules_and_guards)) if node.rules_and_guards else ""
|
|
240
|
+
|
|
241
|
+
lines = [
|
|
242
|
+
f"@node:{node.id} [type:{node.type.value}, name:\"{node.name}\", tools:{tools_str}]",
|
|
243
|
+
f"path:{node.path}",
|
|
244
|
+
f"summary:{node.description.strip()}"
|
|
245
|
+
]
|
|
246
|
+
if triggers_str:
|
|
247
|
+
lines.append(f"triggers:{triggers_str}")
|
|
248
|
+
if links_str:
|
|
249
|
+
lines.append(f"links:{links_str}")
|
|
250
|
+
if rules_str:
|
|
251
|
+
lines.append(f"rules:{rules_str}")
|
|
252
|
+
|
|
253
|
+
return "\n".join(lines)
|
|
254
|
+
|
|
255
|
+
@classmethod
|
|
256
|
+
def serialize_mesh(cls, nodes: List[AgenticNode]) -> str:
|
|
257
|
+
timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
258
|
+
header = [
|
|
259
|
+
f"# ════════════════════════════════════════════════════════════════",
|
|
260
|
+
f"# AGENTIC SKILLS MESH — TOKEN-OPTIMIZED OBJECT NOTATION (TOON)",
|
|
261
|
+
f"# Generated: {timestamp} | Active Nodes: {len(nodes)}",
|
|
262
|
+
f"# ════════════════════════════════════════════════════════════════\n"
|
|
263
|
+
]
|
|
264
|
+
body_blocks = [cls.format_node(n) for n in nodes]
|
|
265
|
+
return "\n".join(header) + "\n\n".join(body_blocks) + "\n"
|
|
266
|
+
|
|
267
|
+
@classmethod
|
|
268
|
+
def parse_toon(cls, content: str) -> List[Dict[str, Any]]:
|
|
269
|
+
"""Parses a TOON file back into node dictionaries."""
|
|
270
|
+
nodes: List[Dict[str, Any]] = []
|
|
271
|
+
current: Dict[str, Any] = {}
|
|
272
|
+
|
|
273
|
+
for line in content.split("\n"):
|
|
274
|
+
line = line.strip()
|
|
275
|
+
if not line or line.startswith("#"):
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
if line.startswith("@node:"):
|
|
279
|
+
if current and "id" in current:
|
|
280
|
+
nodes.append(current)
|
|
281
|
+
current = {"id": "", "name": "", "type": "skill", "tools_required": [], "triggers": [], "edges": [], "rules_and_guards": []}
|
|
282
|
+
# Parse header: @node:<id> [type:<type>, name:"<name>", tools:<tools>]
|
|
283
|
+
match = re.match(r"^@node:([^\s\[]+)(?:\s*\[(.*)\])?", line)
|
|
284
|
+
if match:
|
|
285
|
+
current["id"] = match.group(1)
|
|
286
|
+
attrs = match.group(2)
|
|
287
|
+
if attrs:
|
|
288
|
+
# Split by comma only when followed by a key:
|
|
289
|
+
for part in re.split(r",\s*(?=[a-zA-Z_]+:)", attrs):
|
|
290
|
+
if ":" in part:
|
|
291
|
+
k, v = part.split(":", 1)
|
|
292
|
+
k = k.strip()
|
|
293
|
+
v = v.strip().strip("\"'")
|
|
294
|
+
if k == "type":
|
|
295
|
+
current["type"] = v
|
|
296
|
+
elif k == "name":
|
|
297
|
+
current["name"] = v
|
|
298
|
+
elif k == "tools":
|
|
299
|
+
current["tools_required"] = [x.strip() for x in v.split(",") if x.strip() and x.strip() != "none"]
|
|
300
|
+
|
|
301
|
+
elif current:
|
|
302
|
+
if line.startswith("path:"):
|
|
303
|
+
current["path"] = line.split("path:", 1)[1].strip()
|
|
304
|
+
elif line.startswith("summary:"):
|
|
305
|
+
current["description"] = line.split("summary:", 1)[1].strip()
|
|
306
|
+
elif line.startswith("triggers:"):
|
|
307
|
+
current["triggers"] = [x.strip() for x in line.split("triggers:", 1)[1].split(",") if x.strip()]
|
|
308
|
+
elif line.startswith("links:"):
|
|
309
|
+
current["edges"] = [x.strip() for x in line.split("links:", 1)[1].split(",") if x.strip()]
|
|
310
|
+
elif line.startswith("rules:"):
|
|
311
|
+
current["rules_and_guards"] = [x.strip() for x in line.split("rules:", 1)[1].split(",") if x.strip()]
|
|
312
|
+
|
|
313
|
+
if current and "id" in current:
|
|
314
|
+
nodes.append(current)
|
|
315
|
+
|
|
316
|
+
return nodes
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class SkillScanner:
|
|
320
|
+
"""Discovers and scans all skills directories across the system and project."""
|
|
321
|
+
|
|
322
|
+
def __init__(self, project_dir: str = ".", candidate_dirs: Optional[List[str]] = None):
|
|
323
|
+
self.project_dir = os.path.abspath(project_dir)
|
|
324
|
+
self.explicit_candidate_dirs = [os.path.abspath(d) for d in candidate_dirs] if candidate_dirs is not None else None
|
|
325
|
+
|
|
326
|
+
def get_candidate_directories(self) -> List[str]:
|
|
327
|
+
"""Returns ordered list of directories to scan for skills."""
|
|
328
|
+
if self.explicit_candidate_dirs is not None:
|
|
329
|
+
return self.explicit_candidate_dirs
|
|
330
|
+
|
|
331
|
+
dirs: List[str] = []
|
|
332
|
+
|
|
333
|
+
# 1. Environment variable overrides
|
|
334
|
+
env_paths = os.getenv("AGENT_SKILLS_PATH", "") or os.getenv("GEMINI_SKILLS_PATH", "")
|
|
335
|
+
if env_paths:
|
|
336
|
+
for p in re.split(r"[:;]", env_paths):
|
|
337
|
+
if p and os.path.isdir(p):
|
|
338
|
+
dirs.append(os.path.abspath(p))
|
|
339
|
+
|
|
340
|
+
if os.getenv("AGENT_SKILLS_ISOLATED") == "1":
|
|
341
|
+
return dirs
|
|
342
|
+
|
|
343
|
+
# 2. User Home Skill Directories
|
|
344
|
+
home = os.path.expanduser("~")
|
|
345
|
+
global_candidates = [
|
|
346
|
+
os.path.join(home, ".gemini", "config", "skills"),
|
|
347
|
+
os.path.join(home, ".gemini", "config", "plugins"),
|
|
348
|
+
os.path.join(home, ".claude", "skills"),
|
|
349
|
+
os.path.join(home, ".agents", "skills"),
|
|
350
|
+
os.path.join(home, ".agent-kernel", "skills"),
|
|
351
|
+
os.path.join(home, ".agent-kernel"),
|
|
352
|
+
]
|
|
353
|
+
for gc in global_candidates:
|
|
354
|
+
if os.path.isdir(gc):
|
|
355
|
+
dirs.append(os.path.abspath(gc))
|
|
356
|
+
|
|
357
|
+
# 3. Project Workspace Directories
|
|
358
|
+
local_candidates = [
|
|
359
|
+
os.path.join(self.project_dir, ".claude", "skills"),
|
|
360
|
+
os.path.join(self.project_dir, ".cursor", "skills"),
|
|
361
|
+
os.path.join(self.project_dir, ".gemini", "skills"),
|
|
362
|
+
os.path.join(self.project_dir, ".gemini", "config", "skills"),
|
|
363
|
+
os.path.join(self.project_dir, ".agents", "skills"),
|
|
364
|
+
os.path.join(self.project_dir, "skills"),
|
|
365
|
+
]
|
|
366
|
+
for lc in local_candidates:
|
|
367
|
+
if os.path.isdir(lc):
|
|
368
|
+
dirs.append(os.path.abspath(lc))
|
|
369
|
+
|
|
370
|
+
# Deduplicate while preserving order
|
|
371
|
+
seen = set()
|
|
372
|
+
deduped = []
|
|
373
|
+
for d in dirs:
|
|
374
|
+
if d not in seen:
|
|
375
|
+
seen.add(d)
|
|
376
|
+
deduped.append(d)
|
|
377
|
+
|
|
378
|
+
return deduped
|
|
379
|
+
|
|
380
|
+
def scan_directory(self, target_dir: str) -> List[Tuple[str, str]]:
|
|
381
|
+
"""
|
|
382
|
+
Scans a directory for skill files (SKILL.md, .skills.json, README.md).
|
|
383
|
+
Returns list of (skill_dir_path, main_skill_file_path).
|
|
384
|
+
"""
|
|
385
|
+
results: List[Tuple[str, str]] = []
|
|
386
|
+
if not os.path.isdir(target_dir):
|
|
387
|
+
return results
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
entries = os.listdir(target_dir)
|
|
391
|
+
except (PermissionError, OSError):
|
|
392
|
+
return results
|
|
393
|
+
|
|
394
|
+
# Check if target_dir itself is a skill directory (has SKILL.md or .skills.json)
|
|
395
|
+
self_skill_md = os.path.join(target_dir, "SKILL.md")
|
|
396
|
+
if os.path.isfile(self_skill_md):
|
|
397
|
+
results.append((target_dir, self_skill_md))
|
|
398
|
+
|
|
399
|
+
# Check direct subdirectories
|
|
400
|
+
for entry in sorted(entries):
|
|
401
|
+
if entry.startswith(".") or entry in ["node_modules", "dist", "build", "__pycache__"]:
|
|
402
|
+
continue
|
|
403
|
+
entry_path = os.path.join(target_dir, entry)
|
|
404
|
+
if os.path.isdir(entry_path):
|
|
405
|
+
skill_file = None
|
|
406
|
+
for fname in ["SKILL.md", "skill.md", "README.md"]:
|
|
407
|
+
candidate = os.path.join(entry_path, fname)
|
|
408
|
+
if os.path.isfile(candidate):
|
|
409
|
+
skill_file = candidate
|
|
410
|
+
break
|
|
411
|
+
if skill_file:
|
|
412
|
+
results.append((entry_path, skill_file))
|
|
413
|
+
else:
|
|
414
|
+
# Check 1 level deeper for plugin structures like plugins/firebase/skills/abc
|
|
415
|
+
try:
|
|
416
|
+
sub_entries = os.listdir(entry_path)
|
|
417
|
+
for sub in sub_entries:
|
|
418
|
+
sub_path = os.path.join(entry_path, sub)
|
|
419
|
+
if os.path.isdir(sub_path):
|
|
420
|
+
sub_skill = os.path.join(sub_path, "SKILL.md")
|
|
421
|
+
if os.path.isfile(sub_skill):
|
|
422
|
+
results.append((sub_path, sub_skill))
|
|
423
|
+
except (PermissionError, OSError) as e:
|
|
424
|
+
import logging
|
|
425
|
+
logging.warning("skills_indexer: cannot scan %s: %s", entry_path, e)
|
|
426
|
+
|
|
427
|
+
return results
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class AgenticSkillsMesh:
|
|
431
|
+
"""
|
|
432
|
+
Universal Agentic Skills Mesh and Auto-Indexer.
|
|
433
|
+
|
|
434
|
+
Acts as the single source of discovery, graph modeling, and resolution
|
|
435
|
+
for skills, workflows, agents, playbooks, and rules across the system.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
def __init__(self, project_dir: str = ".", candidate_dirs: Optional[List[str]] = None):
|
|
439
|
+
self.project_dir = os.path.abspath(project_dir)
|
|
440
|
+
self.scanner = SkillScanner(self.project_dir, candidate_dirs=candidate_dirs)
|
|
441
|
+
self.nodes: Dict[str, AgenticNode] = {}
|
|
442
|
+
self.json_index_path = os.path.join(self.project_dir, "skills-index.json")
|
|
443
|
+
self.toon_index_path = os.path.join(self.project_dir, "skills-index.toon")
|
|
444
|
+
|
|
445
|
+
def scan(self) -> Dict[str, AgenticNode]:
|
|
446
|
+
"""Discovers, parses, and resolves all skills across the system."""
|
|
447
|
+
candidate_dirs = self.scanner.get_candidate_directories()
|
|
448
|
+
discovered_skills: List[Tuple[str, str, str]] = [] # (source_dir, skill_dir, file_path)
|
|
449
|
+
|
|
450
|
+
for cdir in candidate_dirs:
|
|
451
|
+
found = self.scanner.scan_directory(cdir)
|
|
452
|
+
for skill_dir, file_path in found:
|
|
453
|
+
discovered_skills.append((cdir, skill_dir, file_path))
|
|
454
|
+
|
|
455
|
+
# Check project root SKILL.md
|
|
456
|
+
root_skill = os.path.join(self.project_dir, "SKILL.md")
|
|
457
|
+
if os.path.isfile(root_skill):
|
|
458
|
+
discovered_skills.append((self.project_dir, self.project_dir, root_skill))
|
|
459
|
+
|
|
460
|
+
temp_nodes: Dict[str, Tuple[AgenticNode, str]] = {}
|
|
461
|
+
all_ids: Set[str] = set()
|
|
462
|
+
|
|
463
|
+
for source_dir, skill_dir, file_path in discovered_skills:
|
|
464
|
+
try:
|
|
465
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
466
|
+
content = f.read(100_000) # Read first 100k bytes max
|
|
467
|
+
except Exception:
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
frontmatter, body = FrontmatterParser.parse_markdown(content)
|
|
471
|
+
|
|
472
|
+
# Determine ID
|
|
473
|
+
node_id = frontmatter.get("name")
|
|
474
|
+
if not node_id:
|
|
475
|
+
node_id = os.path.basename(skill_dir)
|
|
476
|
+
node_id = re.sub(r"[^a-zA-Z0-9_\-]+", "-", node_id.strip()).lower().strip("-")
|
|
477
|
+
if not node_id:
|
|
478
|
+
node_id = f"skill-{len(temp_nodes)}"
|
|
479
|
+
|
|
480
|
+
name = frontmatter.get("name", os.path.basename(skill_dir))
|
|
481
|
+
description = frontmatter.get("description", "")
|
|
482
|
+
if not description:
|
|
483
|
+
# Extract first meaningful paragraph from body
|
|
484
|
+
for line in body.split("\n"):
|
|
485
|
+
line = line.strip()
|
|
486
|
+
if line and not line.startswith("#") and not line.startswith("-") and len(line) > 20:
|
|
487
|
+
description = line[:200]
|
|
488
|
+
break
|
|
489
|
+
if not description:
|
|
490
|
+
description = f"Autonomous agentic skill {name}"
|
|
491
|
+
|
|
492
|
+
version = str(frontmatter.get("version", "1.0.0"))
|
|
493
|
+
author = str(frontmatter.get("author", ""))
|
|
494
|
+
tags = frontmatter.get("tags", [])
|
|
495
|
+
if isinstance(tags, str):
|
|
496
|
+
tags = [tags]
|
|
497
|
+
|
|
498
|
+
node_type = NodeClassifier.classify(node_id, name, description, body)
|
|
499
|
+
triggers = TriggerExtractor.extract_triggers(node_id, name, description, body, tags)
|
|
500
|
+
tools = TriggerExtractor.extract_tools(body)
|
|
501
|
+
|
|
502
|
+
rules: List[str] = []
|
|
503
|
+
if node_type in [NodeType.GUARD, NodeType.RULE]:
|
|
504
|
+
rules.append(f"enforce-{node_id}")
|
|
505
|
+
if "tdd" in node_id or "test" in node_id:
|
|
506
|
+
rules.append("enforce-tdd-verification")
|
|
507
|
+
if "clean-code" in node_id:
|
|
508
|
+
rules.append("enforce-clean-code-solid")
|
|
509
|
+
|
|
510
|
+
node = AgenticNode(
|
|
511
|
+
id=node_id,
|
|
512
|
+
name=name,
|
|
513
|
+
type=node_type,
|
|
514
|
+
path=file_path,
|
|
515
|
+
description=description,
|
|
516
|
+
version=version,
|
|
517
|
+
author=author,
|
|
518
|
+
tags=tags,
|
|
519
|
+
triggers=triggers,
|
|
520
|
+
tools_required=tools,
|
|
521
|
+
rules_and_guards=rules,
|
|
522
|
+
edges=[],
|
|
523
|
+
source_dir=source_dir
|
|
524
|
+
)
|
|
525
|
+
temp_nodes[node_id] = (node, body)
|
|
526
|
+
all_ids.add(node_id)
|
|
527
|
+
|
|
528
|
+
# Second pass: resolve edges across all discovered nodes
|
|
529
|
+
resolved_nodes: Dict[str, AgenticNode] = {}
|
|
530
|
+
for nid, (node, body) in temp_nodes.items():
|
|
531
|
+
node.edges = EdgeResolver.resolve_edges(nid, all_ids, body)
|
|
532
|
+
resolved_nodes[nid] = node
|
|
533
|
+
|
|
534
|
+
self.nodes = resolved_nodes
|
|
535
|
+
return self.nodes
|
|
536
|
+
|
|
537
|
+
def build_index(self, output_dir: Optional[str] = None) -> Tuple[str, str]:
|
|
538
|
+
"""
|
|
539
|
+
Compiles and writes skills-index.json and skills-index.toon.
|
|
540
|
+
Returns paths to both files.
|
|
541
|
+
"""
|
|
542
|
+
if not self.nodes:
|
|
543
|
+
self.scan()
|
|
544
|
+
|
|
545
|
+
target_dir = os.path.abspath(output_dir) if output_dir else self.project_dir
|
|
546
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
547
|
+
|
|
548
|
+
json_path = os.path.join(target_dir, "skills-index.json")
|
|
549
|
+
toon_path = os.path.join(target_dir, "skills-index.toon")
|
|
550
|
+
|
|
551
|
+
# 1. Write JSON index
|
|
552
|
+
nodes_list = sorted(list(self.nodes.values()), key=lambda n: n.id)
|
|
553
|
+
index_payload = {
|
|
554
|
+
"version": "1.0.0",
|
|
555
|
+
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
556
|
+
"total_nodes": len(nodes_list),
|
|
557
|
+
"stats": {
|
|
558
|
+
"skills": sum(1 for n in nodes_list if n.type == NodeType.SKILL),
|
|
559
|
+
"workflows": sum(1 for n in nodes_list if n.type == NodeType.WORKFLOW),
|
|
560
|
+
"agents": sum(1 for n in nodes_list if n.type == NodeType.AGENT),
|
|
561
|
+
"playbooks": sum(1 for n in nodes_list if n.type == NodeType.PLAYBOOK),
|
|
562
|
+
"rules": sum(1 for n in nodes_list if n.type == NodeType.RULE),
|
|
563
|
+
"guards": sum(1 for n in nodes_list if n.type == NodeType.GUARD),
|
|
564
|
+
},
|
|
565
|
+
"nodes": [n.to_dict() for n in nodes_list]
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
with open(json_path, "w", encoding="utf-8") as f:
|
|
569
|
+
json.dump(index_payload, f, indent=2)
|
|
570
|
+
|
|
571
|
+
# 2. Write TOON index (high-density compact format)
|
|
572
|
+
toon_content = ToonFormatter.serialize_mesh(nodes_list)
|
|
573
|
+
with open(toon_path, "w", encoding="utf-8") as f:
|
|
574
|
+
f.write(toon_content)
|
|
575
|
+
|
|
576
|
+
self.json_index_path = json_path
|
|
577
|
+
self.toon_index_path = toon_path
|
|
578
|
+
return json_path, toon_path
|
|
579
|
+
|
|
580
|
+
def load_index(self) -> bool:
|
|
581
|
+
"""Loads index from skills-index.json if it exists."""
|
|
582
|
+
if os.path.isfile(self.json_index_path):
|
|
583
|
+
try:
|
|
584
|
+
with open(self.json_index_path, "r", encoding="utf-8") as f:
|
|
585
|
+
data = json.load(f)
|
|
586
|
+
nodes_data = data.get("nodes", [])
|
|
587
|
+
self.nodes = {d["id"]: AgenticNode.from_dict(d) for d in nodes_data}
|
|
588
|
+
return True
|
|
589
|
+
except Exception as e:
|
|
590
|
+
import logging
|
|
591
|
+
logging.warning("skills_indexer: failed to load index from %s: %s", self.json_index_path, e)
|
|
592
|
+
return False
|
|
593
|
+
|
|
594
|
+
def get_node(self, node_id: str) -> Optional[AgenticNode]:
|
|
595
|
+
"""Retrieves an agentic node by its unique ID."""
|
|
596
|
+
if not self.nodes:
|
|
597
|
+
if not self.load_index():
|
|
598
|
+
self.scan()
|
|
599
|
+
return self.nodes.get(node_id)
|
|
600
|
+
|
|
601
|
+
def search(self, query: str, limit: int = 10) -> List[AgenticNode]:
|
|
602
|
+
"""Fast keyword search across all indexed nodes."""
|
|
603
|
+
if not self.nodes:
|
|
604
|
+
if not self.load_index():
|
|
605
|
+
self.scan()
|
|
606
|
+
|
|
607
|
+
q_terms = [t.lower() for t in re.split(r"[\s\-_]+", query.strip()) if t]
|
|
608
|
+
if not q_terms:
|
|
609
|
+
return list(self.nodes.values())[:limit]
|
|
610
|
+
|
|
611
|
+
scored: List[Tuple[int, AgenticNode]] = []
|
|
612
|
+
for node in self.nodes.values():
|
|
613
|
+
score = 0
|
|
614
|
+
node_text = f"{node.id} {node.name} {node.description} {' '.join(node.tags)} {' '.join(node.triggers)}".lower()
|
|
615
|
+
|
|
616
|
+
for term in q_terms:
|
|
617
|
+
if term == node.id:
|
|
618
|
+
score += 20
|
|
619
|
+
elif term in node.name.lower():
|
|
620
|
+
score += 10
|
|
621
|
+
elif term in node.triggers:
|
|
622
|
+
score += 5
|
|
623
|
+
elif term in node_text:
|
|
624
|
+
score += 2
|
|
625
|
+
|
|
626
|
+
if score > 0:
|
|
627
|
+
scored.append((score, node))
|
|
628
|
+
|
|
629
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
630
|
+
return [node for _, node in scored[:limit]]
|
|
631
|
+
|
|
632
|
+
def resolve_for_intent(self, intent: str, active_files: Optional[List[str]] = None, top_k: int = 5) -> List[AgenticNode]:
|
|
633
|
+
"""
|
|
634
|
+
Agentic Intent Resolver:
|
|
635
|
+
Maps a user task, prompt, or stage description to the most appropriate
|
|
636
|
+
Agentic Nodes, specialized Agents, Playbooks, and Governance Guards.
|
|
637
|
+
"""
|
|
638
|
+
candidates = self.search(intent, limit=top_k * 2)
|
|
639
|
+
if not candidates:
|
|
640
|
+
return []
|
|
641
|
+
|
|
642
|
+
selected: List[AgenticNode] = []
|
|
643
|
+
seen_ids: Set[str] = set()
|
|
644
|
+
|
|
645
|
+
# Prioritize guards if intent implies testing, modification, or code changes
|
|
646
|
+
intent_lower = intent.lower()
|
|
647
|
+
needs_code_guard = any(w in intent_lower for w in ["write", "code", "implement", "refactor", "fix", "feature"])
|
|
648
|
+
needs_test_guard = any(w in intent_lower for w in ["test", "verify", "qa", "assert", "spec"])
|
|
649
|
+
|
|
650
|
+
if needs_code_guard:
|
|
651
|
+
for g_id in ["clean-code-guard", "autoreview"]:
|
|
652
|
+
if g_id in self.nodes and g_id not in seen_ids:
|
|
653
|
+
selected.append(self.nodes[g_id])
|
|
654
|
+
seen_ids.add(g_id)
|
|
655
|
+
|
|
656
|
+
if needs_test_guard:
|
|
657
|
+
for t_id in ["test-guard", "test-driven-development", "fable-tdd"]:
|
|
658
|
+
if t_id in self.nodes and t_id not in seen_ids:
|
|
659
|
+
selected.append(self.nodes[t_id])
|
|
660
|
+
seen_ids.add(t_id)
|
|
661
|
+
|
|
662
|
+
for c in candidates:
|
|
663
|
+
if c.id not in seen_ids and len(selected) < top_k:
|
|
664
|
+
selected.append(c)
|
|
665
|
+
seen_ids.add(c.id)
|
|
666
|
+
|
|
667
|
+
return selected
|
|
668
|
+
|
|
669
|
+
def get_toon_context(self, node_ids: Optional[List[str]] = None) -> str:
|
|
670
|
+
"""
|
|
671
|
+
Produces high-density TOON context string ready for LLM prompt injection.
|
|
672
|
+
"""
|
|
673
|
+
if not self.nodes:
|
|
674
|
+
if not self.load_index():
|
|
675
|
+
self.scan()
|
|
676
|
+
|
|
677
|
+
target_nodes: List[AgenticNode] = []
|
|
678
|
+
if node_ids:
|
|
679
|
+
for nid in node_ids:
|
|
680
|
+
if nid in self.nodes:
|
|
681
|
+
target_nodes.append(self.nodes[nid])
|
|
682
|
+
else:
|
|
683
|
+
target_nodes = list(self.nodes.values())
|
|
684
|
+
|
|
685
|
+
return ToonFormatter.serialize_mesh(target_nodes)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def main():
|
|
689
|
+
import argparse
|
|
690
|
+
parser = argparse.ArgumentParser(description="Agentic Skills Mesh & Universal Indexer")
|
|
691
|
+
parser.add_argument("command", choices=["scan", "index", "search", "resolve", "toon"], help="Command to run")
|
|
692
|
+
parser.add_argument("query", nargs="?", default="", help="Search query or intent")
|
|
693
|
+
parser.add_argument("--output", "-o", default=".", help="Output directory for index files")
|
|
694
|
+
parser.add_argument("--limit", "-n", type=int, default=10, help="Max results limit")
|
|
695
|
+
|
|
696
|
+
args = parser.parse_args()
|
|
697
|
+
mesh = AgenticSkillsMesh()
|
|
698
|
+
|
|
699
|
+
if args.command == "scan":
|
|
700
|
+
print("🔍 Scanning skills directories across user environment...")
|
|
701
|
+
nodes = mesh.scan()
|
|
702
|
+
print(f"✅ Discovered {len(nodes)} agentic nodes across:")
|
|
703
|
+
for d in mesh.scanner.get_candidate_directories():
|
|
704
|
+
print(f" - {d}")
|
|
705
|
+
|
|
706
|
+
elif args.command == "index":
|
|
707
|
+
print(f"⚡ Building synchronized JSON and TOON skills indexes in '{args.output}'...")
|
|
708
|
+
json_file, toon_file = mesh.build_index(output_dir=args.output)
|
|
709
|
+
json_sz = os.path.getsize(json_file)
|
|
710
|
+
toon_sz = os.path.getsize(toon_file)
|
|
711
|
+
saving = max(0, int((1.0 - (toon_sz / json_sz)) * 100)) if json_sz > 0 else 0
|
|
712
|
+
print(f"✅ Indexed {len(mesh.nodes)} Agentic Nodes:")
|
|
713
|
+
print(f" 📄 JSON: {json_file} ({json_sz:,} bytes)")
|
|
714
|
+
print(f" ⚡ TOON: {toon_file} ({toon_sz:,} bytes) -> [{saving}% token/size savings]")
|
|
715
|
+
|
|
716
|
+
elif args.command == "search":
|
|
717
|
+
results = mesh.search(args.query, limit=args.limit)
|
|
718
|
+
print(f"🔎 Found {len(results)} matches for '{args.query}':")
|
|
719
|
+
for r in results:
|
|
720
|
+
print(f" [{r.type.value.upper()}] {r.id}: {r.name} — {r.description[:80]}...")
|
|
721
|
+
|
|
722
|
+
elif args.command == "resolve":
|
|
723
|
+
results = mesh.resolve_for_intent(args.query, top_k=args.limit)
|
|
724
|
+
print(f"🎯 Resolved {len(results)} agentic node(s) for task: '{args.query}':")
|
|
725
|
+
for r in results:
|
|
726
|
+
print(f" [{r.type.value.upper()}] {r.id} ({r.path})")
|
|
727
|
+
if r.triggers:
|
|
728
|
+
print(f" Triggers: {', '.join(r.triggers[:5])}")
|
|
729
|
+
if r.edges:
|
|
730
|
+
print(f" Links: {', '.join(r.edges[:5])}")
|
|
731
|
+
|
|
732
|
+
elif args.command == "toon":
|
|
733
|
+
node = mesh.get_node(args.query)
|
|
734
|
+
if node:
|
|
735
|
+
print(ToonFormatter.format_node(node))
|
|
736
|
+
else:
|
|
737
|
+
print(f"❌ Node '{args.query}' not found.")
|
|
738
|
+
sys.exit(1)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
if __name__ == "__main__":
|
|
742
|
+
main()
|