@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.
Files changed (118) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/.codex-plugin/plugin.json +13 -0
  3. package/.skills.json +19 -0
  4. package/AGENTS.md +1344 -0
  5. package/CLAUDE.md +178 -0
  6. package/GEMINI.md +102 -0
  7. package/LICENSE +21 -0
  8. package/README.md +350 -0
  9. package/SKILL.md +132 -0
  10. package/bin/agentic-hooks.sh +79 -0
  11. package/bin/cli.js +1060 -0
  12. package/core/__init__.py +52 -0
  13. package/core/ai_evaluator.py +117 -0
  14. package/core/autopilot_engine.py +368 -0
  15. package/core/clean_code_guard.py +188 -0
  16. package/core/engine_py/__init__.py +29 -0
  17. package/core/engine_py/agent_worker.py +136 -0
  18. package/core/engine_py/decider.py +150 -0
  19. package/core/engine_py/energy.py +45 -0
  20. package/core/engine_py/event_bus.py +63 -0
  21. package/core/engine_py/executor.py +186 -0
  22. package/core/engine_py/models.py +193 -0
  23. package/core/engine_py/queue.py +314 -0
  24. package/core/engine_py/runner.py +116 -0
  25. package/core/engine_py/system_workers.py +70 -0
  26. package/core/engine_py/toon_adapter.py +586 -0
  27. package/core/engine_py/verification_controller.py +208 -0
  28. package/core/engine_py/worker.py +167 -0
  29. package/core/engine_spec/event_schema.json +65 -0
  30. package/core/engine_spec/example_workflow.yaml +73 -0
  31. package/core/engine_spec/workflow_schema.json +127 -0
  32. package/core/hooks/__init__.py +29 -0
  33. package/core/hooks/adapters/__init__.py +25 -0
  34. package/core/hooks/adapters/claude_adapter.py +83 -0
  35. package/core/hooks/adapters/cli_agent_adapter.py +82 -0
  36. package/core/hooks/adapters/codex_adapter.py +78 -0
  37. package/core/hooks/adapters/cursor_adapter.py +73 -0
  38. package/core/hooks/adapters/gemini_adapter.py +93 -0
  39. package/core/hooks/adapters/homebrew_adapter.py +69 -0
  40. package/core/hooks/adapters/mcp_proxy.py +133 -0
  41. package/core/hooks/adapters/shell_adapter.py +65 -0
  42. package/core/hooks/dispatcher.py +118 -0
  43. package/core/hooks/policy_engine.py +375 -0
  44. package/core/hooks/session_end.py +141 -0
  45. package/core/hooks/types.py +147 -0
  46. package/core/integrations/__init__.py +28 -0
  47. package/core/integrations/installer.py +225 -0
  48. package/core/integrations/lifecycle_director.py +175 -0
  49. package/core/integrations/registry.py +105 -0
  50. package/core/multi_agent_system.py +164 -0
  51. package/core/skills_indexer.py +742 -0
  52. package/core/system/__init__.py +25 -0
  53. package/core/system/announcements.py +72 -0
  54. package/core/system/dependencies.py +69 -0
  55. package/core/system/doctor.py +171 -0
  56. package/core/system/health.py +144 -0
  57. package/core/system/installer.py +137 -0
  58. package/core/system/notifications.py +97 -0
  59. package/core/system/refresher.py +110 -0
  60. package/core/system/updater.py +167 -0
  61. package/core/system/version_tracker.py +65 -0
  62. package/docs/architecture_plan.md +7 -0
  63. package/docs/guides/failure-recovery.md +714 -0
  64. package/docs/implementation_summary.md +10 -0
  65. package/docs/protocols/autopilot-execution.md +148 -0
  66. package/docs/protocols/code-change-protocol.md +49 -0
  67. package/docs/protocols/context-preservation-detail.md +114 -0
  68. package/docs/protocols/quality-gates.md +110 -0
  69. package/docs/protocols/ulw-mode.md +60 -0
  70. package/docs/research_findings.md +10 -0
  71. package/docs/solutions/autonomous-autopilot-engine-architecture.md +38 -0
  72. package/install.sh +111 -0
  73. package/marketplace.json +37 -0
  74. package/package.json +81 -0
  75. package/skills/agentic-workflow/SKILL.md +132 -0
  76. package/skills/agentic-workflow/skill-spec.json +100 -0
  77. package/soul.md +445 -0
  78. package/src/engine_ts/decider.ts +186 -0
  79. package/src/engine_ts/event-bus.ts +57 -0
  80. package/src/engine_ts/executor.ts +262 -0
  81. package/src/engine_ts/index.ts +12 -0
  82. package/src/engine_ts/queue.ts +93 -0
  83. package/src/engine_ts/runner.ts +108 -0
  84. package/src/engine_ts/skills-indexer.ts +264 -0
  85. package/src/engine_ts/toon-adapter.ts +91 -0
  86. package/src/engine_ts/types.ts +134 -0
  87. package/src/engine_ts/verification-controller.ts +204 -0
  88. package/src/engine_ts/worker.ts +280 -0
  89. package/src/hooks/adapters/claude-adapter.ts +54 -0
  90. package/src/hooks/adapters/cli-agent-adapter.ts +46 -0
  91. package/src/hooks/adapters/codex-adapter.ts +69 -0
  92. package/src/hooks/adapters/cursor-adapter.ts +60 -0
  93. package/src/hooks/adapters/gemini-adapter.ts +71 -0
  94. package/src/hooks/adapters/homebrew-adapter.ts +36 -0
  95. package/src/hooks/adapters/mcp-proxy.ts +66 -0
  96. package/src/hooks/adapters/shell-adapter.ts +42 -0
  97. package/src/hooks/dispatcher.ts +113 -0
  98. package/src/hooks/index.ts +16 -0
  99. package/src/hooks/policy-engine.ts +376 -0
  100. package/src/hooks/session-end.ts +125 -0
  101. package/src/hooks/types.ts +61 -0
  102. package/src/index.d.ts +34 -0
  103. package/src/index.ts +23 -0
  104. package/src/integrations/index.ts +7 -0
  105. package/src/integrations/installer.ts +208 -0
  106. package/src/integrations/lifecycle-director.ts +139 -0
  107. package/src/integrations/registry.ts +82 -0
  108. package/src/system/announcements.ts +143 -0
  109. package/src/system/dependencies.ts +176 -0
  110. package/src/system/doctor.ts +374 -0
  111. package/src/system/health.ts +270 -0
  112. package/src/system/index.ts +14 -0
  113. package/src/system/installer.ts +262 -0
  114. package/src/system/notifications.ts +180 -0
  115. package/src/system/refresher.ts +207 -0
  116. package/src/system/types.ts +268 -0
  117. package/src/system/updater.ts +219 -0
  118. 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()