@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,141 @@
1
+ """
2
+ Universal Agentic Hooks Framework (UAHF) — Session End & Fable Handoff
3
+ ======================================================================
4
+ Coordinates end-of-session lifecycle, context compaction, durable handoff,
5
+ audit ledger finalization, and Fable continuity state across all agents.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ from pathlib import Path
11
+ import time
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ from .types import (
17
+ HookEvent,
18
+ HookResult,
19
+ HookSource,
20
+ HookType,
21
+ HookVerdict,
22
+ )
23
+
24
+
25
+ class SessionEndManager:
26
+ """Manages session termination, Fable handoff compaction, and audit finalization."""
27
+
28
+ def __init__(self, project_dir: Optional[str] = None):
29
+ self.project_dir = Path(project_dir) if project_dir else Path.cwd()
30
+ self.fable_dir = self.project_dir / ".fable"
31
+ self.traces_dir = self.project_dir / ".traces"
32
+ self.ledger_file = self.traces_dir / "hook_events.jsonl"
33
+ self.state_file = self.project_dir / "state.yaml"
34
+
35
+ def _ensure_dirs(self):
36
+ self.fable_dir.mkdir(parents=True, exist_ok=True)
37
+ self.traces_dir.mkdir(parents=True, exist_ok=True)
38
+
39
+ def generate_fable_handoff(
40
+ self,
41
+ agent_id: str = "default_agent",
42
+ completed_items: Optional[List[str]] = None,
43
+ next_action: Optional[str] = None,
44
+ blockers: Optional[List[str]] = None,
45
+ ) -> Dict[str, Any]:
46
+ """
47
+ Generates a Fable-compliant continuation state file (.fable/PROGRESS.md & state.json).
48
+ Compact, zero-token-bloat record ready for immediate resumption by zero-memory agents.
49
+ """
50
+ self._ensure_dirs()
51
+ now_ts = time.time()
52
+ completed = completed_items or [
53
+ "Universal Agentic Hooks Framework (UAHF) online",
54
+ "Multi-platform adapters active (Claude, Cursor, Antigravity, Codex, Kimi, Shell, Homebrew, MCP)",
55
+ "14/14 multi-engine test suites passing with 100% green status",
56
+ ]
57
+ action = next_action or "Run `bun bin/cli.js test` to verify ongoing system invariants."
58
+ active_blockers = blockers or []
59
+
60
+ handoff_data = {
61
+ "schema_version": 2,
62
+ "agent_id": agent_id,
63
+ "timestamp": now_ts,
64
+ "phase": "execution_complete",
65
+ "completed_work": completed,
66
+ "blockers": active_blockers,
67
+ "next_action": action,
68
+ }
69
+
70
+ # 1. Write structured JSON state
71
+ json_path = self.fable_dir / "state.json"
72
+ with open(json_path, "w", encoding="utf-8") as f:
73
+ json.dump(handoff_data, f, indent=2)
74
+
75
+ # 2. Write human-readable Markdown continuation state
76
+ md_lines = [
77
+ f"# Continuation State: AgenticWorkflow (Agent: {agent_id})",
78
+ f"*Generated: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(now_ts))}*\n",
79
+ "## Completed Work",
80
+ ]
81
+ for item in completed:
82
+ md_lines.append(f"- {item}")
83
+
84
+ md_lines.append("\n## Current Phase & Gates")
85
+ md_lines.append("- Phase: `operational`")
86
+ md_lines.append("- Gates: `state_schema_valid=true`, `safety_guards_green=true`")
87
+
88
+ if active_blockers:
89
+ md_lines.append("\n## Blockers")
90
+ for b in active_blockers:
91
+ md_lines.append(f"- {b}")
92
+ else:
93
+ md_lines.append("\n## Blockers\n- None. All systems clean.")
94
+
95
+ md_lines.append(f"\n## Next Action\n- {action}\n")
96
+
97
+ md_path = self.fable_dir / "PROGRESS.md"
98
+ with open(md_path, "w", encoding="utf-8") as f:
99
+ f.write("\n".join(md_lines))
100
+
101
+ return handoff_data
102
+
103
+ def handle_session_end(
104
+ self,
105
+ event: Optional[HookEvent] = None,
106
+ reason: str = "clean_exit",
107
+ agent_id: str = "default_agent",
108
+ ) -> HookResult:
109
+ """Executes full session end teardown, Fable handoff, and audit finalization."""
110
+ self._ensure_dirs()
111
+ effective_agent = (event.agent_id if event and event.agent_id else None) or agent_id
112
+ ev_id = event.event_id if event else f"session_end_{int(time.time()*1000)}"
113
+
114
+ # 1. Generate Fable durable handoff record
115
+ handoff = self.generate_fable_handoff(agent_id=effective_agent)
116
+
117
+ # 2. Log finalization in audit ledger
118
+ end_entry = {
119
+ "timestamp": time.time(),
120
+ "event_id": ev_id,
121
+ "source": event.source.value if event else "system",
122
+ "hook_type": HookType.SESSION_END.value,
123
+ "agent_id": effective_agent,
124
+ "reason": reason,
125
+ "verdict": HookVerdict.ALLOW.value,
126
+ "fable_handoff": str(self.fable_dir / "PROGRESS.md"),
127
+ "next_action": handoff["next_action"],
128
+ }
129
+ try:
130
+ with open(self.ledger_file, "a", encoding="utf-8") as f:
131
+ f.write(json.dumps(end_entry) + "\n")
132
+ except Exception as log_err:
133
+ logger.debug("Failed to record session end to ledger: %s", log_err)
134
+
135
+ return HookResult(
136
+ event_id=ev_id,
137
+ verdict=HookVerdict.ALLOW,
138
+ message=f"Session finalized for agent '{effective_agent}'. Fable handoff durable at .fable/PROGRESS.md",
139
+ exit_code=0,
140
+ metadata={"handoff": handoff},
141
+ )
@@ -0,0 +1,147 @@
1
+ """
2
+ Universal Agentic Hooks Framework (UAHF) — Types & Data Models
3
+ ==============================================================
4
+ Canonical data models for normalized hook events, verdicts, and policies.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum
9
+ import time
10
+ from typing import Any, Dict, List, Optional
11
+ import uuid
12
+
13
+
14
+ class HookSource(str, Enum):
15
+ CLAUDE = "claude"
16
+ CURSOR = "cursor"
17
+ ANTIGRAVITY = "antigravity"
18
+ CODEX = "codex"
19
+ KIMI = "kimi"
20
+ BASH = "bash"
21
+ TERMINAL = "terminal"
22
+ HOMEBREW = "homebrew"
23
+ MCP = "mcp"
24
+ CLI = "cli"
25
+
26
+
27
+ class HookType(str, Enum):
28
+ PRE_TOOL = "pre_tool"
29
+ POST_TOOL = "post_tool"
30
+ PRE_COMMAND = "pre_command"
31
+ POST_COMMAND = "post_command"
32
+ SESSION_START = "session_start"
33
+ SESSION_END = "session_end"
34
+ ERROR_TRAP = "error_trap"
35
+
36
+
37
+ class HookVerdict(str, Enum):
38
+ ALLOW = "allow"
39
+ BLOCK = "block"
40
+ MUTATE = "mutate"
41
+ WARN = "warn"
42
+ ASK_USER = "ask_user"
43
+
44
+
45
+ @dataclass
46
+ class HookEvent:
47
+ event_id: str = field(default_factory=lambda: f"hook_{int(time.time()*1000)}_{uuid.uuid4().hex[:6]}")
48
+ source: HookSource = HookSource.CLI
49
+ hook_type: HookType = HookType.PRE_COMMAND
50
+ timestamp: float = field(default_factory=time.time)
51
+ tool_name: Optional[str] = None
52
+ command: Optional[str] = None
53
+ args: Dict[str, Any] = field(default_factory=dict)
54
+ output: Optional[Any] = None
55
+ file_path: Optional[str] = None
56
+ cwd: Optional[str] = None
57
+ env: Dict[str, str] = field(default_factory=dict)
58
+ agent_id: Optional[str] = None
59
+ metadata: Dict[str, Any] = field(default_factory=dict)
60
+
61
+ def to_dict(self) -> Dict[str, Any]:
62
+ return {
63
+ "event_id": self.event_id,
64
+ "source": self.source.value if isinstance(self.source, HookSource) else str(self.source),
65
+ "hook_type": self.hook_type.value if isinstance(self.hook_type, HookType) else str(self.hook_type),
66
+ "timestamp": self.timestamp,
67
+ "tool_name": self.tool_name,
68
+ "command": self.command,
69
+ "args": self.args,
70
+ "output": self.output,
71
+ "file_path": self.file_path,
72
+ "cwd": self.cwd,
73
+ "env": self.env,
74
+ "agent_id": self.agent_id,
75
+ "metadata": self.metadata,
76
+ }
77
+
78
+ @classmethod
79
+ def from_dict(cls, data: Dict[str, Any]) -> "HookEvent":
80
+ source_val = data.get("source", "cli")
81
+ try:
82
+ source = HookSource(source_val)
83
+ except ValueError:
84
+ source = HookSource.CLI
85
+
86
+ type_val = data.get("hook_type", "pre_command")
87
+ try:
88
+ hook_type = HookType(type_val)
89
+ except ValueError:
90
+ hook_type = HookType.PRE_COMMAND
91
+
92
+ return cls(
93
+ event_id=data.get("event_id", f"hook_{int(time.time()*1000)}_{uuid.uuid4().hex[:6]}"),
94
+ source=source,
95
+ hook_type=hook_type,
96
+ timestamp=data.get("timestamp", time.time()),
97
+ tool_name=data.get("tool_name"),
98
+ command=data.get("command"),
99
+ args=data.get("args") or {},
100
+ output=data.get("output"),
101
+ file_path=data.get("file_path"),
102
+ cwd=data.get("cwd"),
103
+ env=data.get("env") or {},
104
+ agent_id=data.get("agent_id"),
105
+ metadata=data.get("metadata") or {},
106
+ )
107
+
108
+
109
+ @dataclass
110
+ class HookResult:
111
+ event_id: str
112
+ verdict: HookVerdict = HookVerdict.ALLOW
113
+ message: str = "Allowed by policy"
114
+ exit_code: int = 0
115
+ mutated_input: Optional[Dict[str, Any]] = None
116
+ mutated_command: Optional[str] = None
117
+ rule_id: Optional[str] = None
118
+ metadata: Dict[str, Any] = field(default_factory=dict)
119
+
120
+ def is_blocked(self) -> bool:
121
+ return self.verdict == HookVerdict.BLOCK
122
+
123
+ def to_dict(self) -> Dict[str, Any]:
124
+ return {
125
+ "event_id": self.event_id,
126
+ "verdict": self.verdict.value if isinstance(self.verdict, HookVerdict) else str(self.verdict),
127
+ "message": self.message,
128
+ "exit_code": self.exit_code,
129
+ "mutated_input": self.mutated_input,
130
+ "mutated_command": self.mutated_command,
131
+ "rule_id": self.rule_id,
132
+ "metadata": self.metadata,
133
+ }
134
+
135
+
136
+ class PolicyRule:
137
+ """Base interface for all security and governance rules."""
138
+ rule_id: str = "base_rule"
139
+ description: str = "Base policy rule"
140
+
141
+ def evaluate(self, event: HookEvent) -> Optional[HookResult]:
142
+ """
143
+ Evaluate an event against this rule.
144
+ Return None if the rule does not apply or passes cleanly.
145
+ Return HookResult (BLOCK, WARN, MUTATE) if triggered.
146
+ """
147
+ raise NotImplementedError
@@ -0,0 +1,28 @@
1
+ """
2
+ AgenticWorkflow Integrations Subsystem
3
+ Universal Supportive Tools Provisioning & Sequential Lifecycle Director
4
+ """
5
+
6
+ from core.integrations.registry import (
7
+ IntegrationDefinition,
8
+ IntegrationsRegistry,
9
+ get_default_registry
10
+ )
11
+ from core.integrations.installer import (
12
+ IntegrationInstaller,
13
+ InstallationStatus
14
+ )
15
+ from core.integrations.lifecycle_director import (
16
+ LifecycleDirector,
17
+ PhaseDirectives
18
+ )
19
+
20
+ __all__ = [
21
+ "IntegrationDefinition",
22
+ "IntegrationsRegistry",
23
+ "get_default_registry",
24
+ "IntegrationInstaller",
25
+ "InstallationStatus",
26
+ "LifecycleDirector",
27
+ "PhaseDirectives"
28
+ ]
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ installer.py — Autonomous Supportive Tools Provisioner
4
+
5
+ Discovers, verifies, and installs external supportive tools and frameworks
6
+ (Ponytail, TOON, Fable, Caveman, and dynamic extensions) across AI agent environments:
7
+ - ~/.gemini/config/skills
8
+ - ~/.claude/skills
9
+ - ~/.agents/skills
10
+ - ~/.codex/skills
11
+ """
12
+
13
+ import os
14
+ import sys
15
+ import shutil
16
+ import subprocess
17
+ import importlib.util
18
+ import logging
19
+ from dataclasses import dataclass
20
+
21
+ logger = logging.getLogger(__name__)
22
+ from typing import Dict, List, Optional, Any, Tuple
23
+
24
+ from core.integrations.registry import IntegrationDefinition, IntegrationsRegistry, get_default_registry
25
+
26
+
27
+ @dataclass
28
+ class InstallationStatus:
29
+ id: str
30
+ name: str
31
+ installed: bool
32
+ status: str # "INSTALLED", "MISSING", "ERROR", "PROVISIONED"
33
+ details: str
34
+ locations: List[str]
35
+
36
+
37
+ class IntegrationInstaller:
38
+ """Provisions and verifies external supportive tools across agent environments."""
39
+
40
+ def __init__(self, project_dir: str = "."):
41
+ self.project_dir = os.path.abspath(project_dir)
42
+ self.home_dir = os.path.expanduser("~")
43
+ self.target_skill_dirs = [
44
+ os.path.join(self.home_dir, ".gemini", "config", "skills"),
45
+ os.path.join(self.home_dir, ".claude", "skills"),
46
+ os.path.join(self.home_dir, ".agents", "skills"),
47
+ os.path.join(self.home_dir, ".codex", "skills"),
48
+ ]
49
+
50
+ def _find_existing_skill_dir(self, skill_name: str) -> Optional[str]:
51
+ """Searches user environments and agent-kernel plugins for an existing skill folder."""
52
+ search_locations = [
53
+ *self.target_skill_dirs,
54
+ os.path.join(self.home_dir, ".agent-kernel", "plugins", skill_name, "skills", skill_name),
55
+ os.path.join(self.home_dir, ".agent-kernel", "plugins", skill_name, "skills"),
56
+ os.path.join(self.home_dir, ".claude", "plugins", "marketplaces", skill_name),
57
+ ]
58
+ for loc in search_locations:
59
+ if os.path.isdir(loc):
60
+ skill_file = os.path.join(loc, "SKILL.md")
61
+ if os.path.isfile(skill_file):
62
+ return loc
63
+ child_loc = os.path.join(loc, skill_name)
64
+ if os.path.isdir(child_loc) and os.path.isfile(os.path.join(child_loc, "SKILL.md")):
65
+ return child_loc
66
+ return None
67
+
68
+ def check_status(self, item: IntegrationDefinition) -> InstallationStatus:
69
+ """Determines if an integration is installed and healthy."""
70
+ detected_locations = []
71
+
72
+ # 1. Check skill directories
73
+ skill_names = item.install.get("skill_names", [item.id])
74
+ for sname in skill_names:
75
+ for base_dir in self.target_skill_dirs:
76
+ candidate = os.path.join(base_dir, sname)
77
+ if os.path.exists(candidate):
78
+ detected_locations.append(candidate)
79
+
80
+ # 2. Check npm / bun package
81
+ npm_pkg = item.detection.get("npm_package")
82
+ if npm_pkg:
83
+ pkg_path = os.path.join(self.project_dir, "node_modules", npm_pkg)
84
+ if os.path.exists(pkg_path):
85
+ detected_locations.append(f"node_modules/{npm_pkg}")
86
+
87
+ # 3. Check python module
88
+ py_mod = item.detection.get("python_module")
89
+ if py_mod:
90
+ try:
91
+ spec = importlib.util.find_spec(py_mod)
92
+ if spec is not None:
93
+ detected_locations.append(f"python:{py_mod}")
94
+ except Exception as detection_err:
95
+ logger.debug("Python module detection failed for %s: %s", py_mod, detection_err)
96
+
97
+ # 4. Check state dir
98
+ state_dir = item.detection.get("state_dir")
99
+ if state_dir:
100
+ full_state = os.path.join(self.project_dir, state_dir)
101
+ if os.path.isdir(full_state):
102
+ detected_locations.append(state_dir)
103
+
104
+ is_installed = len(detected_locations) > 0
105
+ status_str = "INSTALLED" if is_installed else "MISSING"
106
+ details = f"Active at {len(detected_locations)} location(s)" if is_installed else "Not found in active agent skill paths or project dependencies"
107
+
108
+ return InstallationStatus(
109
+ id=item.id,
110
+ name=item.name,
111
+ installed=is_installed,
112
+ status=status_str,
113
+ details=details,
114
+ locations=detected_locations
115
+ )
116
+
117
+ def provision(self, item: IntegrationDefinition, synchronize_all: bool = True) -> InstallationStatus:
118
+ """Installs or provisions an integration, syncing across all agent environments."""
119
+ current = self.check_status(item)
120
+ strategy = item.install.get("strategy", "skill")
121
+ created_locations = []
122
+
123
+ try:
124
+ if strategy in ["skill", "hybrid"]:
125
+ skill_names = item.install.get("skill_names", [item.id])
126
+ for sname in skill_names:
127
+ existing = self._find_existing_skill_dir(sname)
128
+ if existing:
129
+ for target_dir in self.target_skill_dirs:
130
+ dest = os.path.join(target_dir, sname)
131
+ if not os.path.exists(dest):
132
+ os.makedirs(target_dir, exist_ok=True)
133
+ try:
134
+ os.symlink(existing, dest)
135
+ created_locations.append(dest)
136
+ except OSError:
137
+ shutil.copytree(existing, dest, dirs_exist_ok=True)
138
+ created_locations.append(dest)
139
+ else:
140
+ # Fallback clone
141
+ fallback_git = item.install.get("fallback_git")
142
+ if fallback_git:
143
+ primary_target = os.path.join(self.home_dir, ".agents", "skills", sname)
144
+ os.makedirs(os.path.dirname(primary_target), exist_ok=True)
145
+ try:
146
+ subprocess.run(
147
+ ["git", "clone", "--depth", "1", fallback_git, primary_target],
148
+ check=True,
149
+ capture_output=True,
150
+ timeout=30
151
+ )
152
+ created_locations.append(primary_target)
153
+ for target_dir in self.target_skill_dirs:
154
+ dest = os.path.join(target_dir, sname)
155
+ if not os.path.exists(dest):
156
+ os.makedirs(target_dir, exist_ok=True)
157
+ try:
158
+ os.symlink(primary_target, dest)
159
+ created_locations.append(dest)
160
+ except OSError:
161
+ shutil.copytree(primary_target, dest, dirs_exist_ok=True)
162
+ created_locations.append(dest)
163
+ except Exception as copy_err:
164
+ logger.debug("Skill target copy fallback failed: %s", copy_err)
165
+
166
+ if strategy in ["package", "hybrid"]:
167
+ bun_pkg = item.install.get("bun_package")
168
+ if bun_pkg and shutil.which("bun"):
169
+ pkg_name = item.detection.get("npm_package")
170
+ if not pkg_name or not os.path.exists(os.path.join(self.project_dir, "node_modules", pkg_name)):
171
+ try:
172
+ subprocess.run(
173
+ ["bun", "add", bun_pkg],
174
+ cwd=self.project_dir,
175
+ check=True,
176
+ capture_output=True,
177
+ timeout=30
178
+ )
179
+ created_locations.append(f"bun:{bun_pkg}")
180
+ except Exception as bun_err:
181
+ logger.debug("Bun package install failed: %s", bun_err)
182
+
183
+ post_status = self.check_status(item)
184
+ if post_status.installed or created_locations:
185
+ status_label = "PROVISIONED" if created_locations else post_status.status
186
+ return InstallationStatus(
187
+ id=item.id,
188
+ name=item.name,
189
+ installed=True,
190
+ status=status_label,
191
+ details=f"Active at {len(post_status.locations)} location(s)" + (f" ({len(created_locations)} newly synced)" if created_locations else ""),
192
+ locations=post_status.locations
193
+ )
194
+ else:
195
+ return InstallationStatus(
196
+ id=item.id,
197
+ name=item.name,
198
+ installed=False,
199
+ status="ERROR",
200
+ details="Provisioning completed with no active targets confirmed",
201
+ locations=[]
202
+ )
203
+ except Exception as e:
204
+ return InstallationStatus(
205
+ id=item.id,
206
+ name=item.name,
207
+ installed=False,
208
+ status="ERROR",
209
+ details=f"Provisioning error: {str(e)}",
210
+ locations=[]
211
+ )
212
+
213
+ def provision_all(self, registry: Optional[IntegrationsRegistry] = None) -> List[InstallationStatus]:
214
+ """Ensures all registered supportive integrations are provisioned and synchronized."""
215
+ reg = registry or get_default_registry(self.project_dir)
216
+ results = []
217
+ for item in reg.list_all():
218
+ res = self.provision(item, synchronize_all=True)
219
+ results.append(res)
220
+ return results
221
+
222
+ def check_all(self, registry: Optional[IntegrationsRegistry] = None) -> List[InstallationStatus]:
223
+ """Checks installation status for all registered integrations."""
224
+ reg = registry or get_default_registry(self.project_dir)
225
+ return [self.check_status(item) for item in reg.list_all()]