@jaguilar87/gaia-ops 1.0.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/CHANGELOG.md +315 -0
- package/CLAUDE.md +154 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/agents/aws-troubleshooter.md +50 -0
- package/agents/claude-architect.md +821 -0
- package/agents/devops-developer.md +92 -0
- package/agents/gcp-troubleshooter.md +50 -0
- package/agents/gitops-operator.md +360 -0
- package/agents/terraform-architect.md +289 -0
- package/bin/gaia-init.js +620 -0
- package/commands/architect.md +97 -0
- package/commands/restore-session.md +87 -0
- package/commands/save-session.md +88 -0
- package/commands/session-status.md +61 -0
- package/commands/speckit.add-task.md +144 -0
- package/commands/speckit.analyze-task.md +65 -0
- package/commands/speckit.implement.md +96 -0
- package/commands/speckit.init.md +237 -0
- package/commands/speckit.plan.md +88 -0
- package/commands/speckit.specify.md +161 -0
- package/commands/speckit.tasks.md +188 -0
- package/config/AGENTS.md +162 -0
- package/config/agent-catalog.md +604 -0
- package/config/context-contracts.md +682 -0
- package/config/git-standards.md +674 -0
- package/config/git_standards.json +69 -0
- package/config/orchestration-workflow.md +735 -0
- package/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
- package/hooks/__pycache__/pre_kubectl_security.cpython-312.pyc +0 -0
- package/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
- package/hooks/__pycache__/subagent_stop.cpython-312.pyc +0 -0
- package/hooks/post_tool_use.py +463 -0
- package/hooks/pre_kubectl_security.py +205 -0
- package/hooks/pre_tool_use.py +530 -0
- package/hooks/session_start.py +315 -0
- package/hooks/subagent_stop.py +549 -0
- package/index.js +92 -0
- package/package.json +59 -0
- package/speckit/README.en.md +648 -0
- package/speckit/README.md +353 -0
- package/speckit/governance.md +169 -0
- package/speckit/scripts/check-prerequisites.sh +194 -0
- package/speckit/scripts/common.sh +126 -0
- package/speckit/scripts/create-new-feature.sh +131 -0
- package/speckit/scripts/init.sh +42 -0
- package/speckit/scripts/setup-plan.sh +95 -0
- package/speckit/scripts/update-agent-context.sh +718 -0
- package/speckit/templates/adr-template.md +118 -0
- package/speckit/templates/agent-file-template.md +23 -0
- package/speckit/templates/plan-template.md +233 -0
- package/speckit/templates/spec-template.md +116 -0
- package/speckit/templates/tasks-template-bkp.md +136 -0
- package/speckit/templates/tasks-template.md +345 -0
- package/templates/CLAUDE.template.md +170 -0
- package/templates/code-examples/approval_gate_workflow.py +141 -0
- package/templates/code-examples/clarification_workflow.py +94 -0
- package/templates/code-examples/commit_validation.py +86 -0
- package/templates/project-context.template.json +126 -0
- package/templates/settings.template.json +307 -0
- package/tools/__pycache__/agent_router.cpython-312.pyc +0 -0
- package/tools/__pycache__/approval_gate.cpython-312.pyc +0 -0
- package/tools/__pycache__/clarify_engine.cpython-312.pyc +0 -0
- package/tools/__pycache__/clarify_patterns.cpython-312.pyc +0 -0
- package/tools/__pycache__/commit_validator.cpython-312.pyc +0 -0
- package/tools/__pycache__/context_section_reader.cpython-312.pyc +0 -0
- package/tools/__pycache__/routing_dashboard.cpython-312.pyc +0 -0
- package/tools/__pycache__/routing_feedback.cpython-312.pyc +0 -0
- package/tools/__pycache__/semantic_matcher.cpython-312.pyc +0 -0
- package/tools/__pycache__/task_manager.cpython-312.pyc +0 -0
- package/tools/agent_capabilities.json +231 -0
- package/tools/agent_invoker_helper.py +239 -0
- package/tools/agent_router.py +730 -0
- package/tools/approval_gate.py +318 -0
- package/tools/clarify_engine.py +511 -0
- package/tools/clarify_patterns.py +356 -0
- package/tools/commit_validator.py +338 -0
- package/tools/context_provider.py +181 -0
- package/tools/context_section_reader.py +301 -0
- package/tools/demo_clarify.py +104 -0
- package/tools/generate_embeddings.py +168 -0
- package/tools/quicktriage_aws_troubleshooter.sh +45 -0
- package/tools/quicktriage_devops_developer.sh +38 -0
- package/tools/quicktriage_gcp_troubleshooter.sh +51 -0
- package/tools/quicktriage_gitops_operator.sh +47 -0
- package/tools/quicktriage_terraform_architect.sh +40 -0
- package/tools/semantic_matcher.py +222 -0
- package/tools/task_manager.py +547 -0
- package/tools/task_manager_README.md +395 -0
- package/tools/task_manager_example.py +215 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Subagent stop hook for Claude Code Agent System
|
|
4
|
+
|
|
5
|
+
Handles session state persistence when specialized agents complete execution.
|
|
6
|
+
|
|
7
|
+
Responsibilities:
|
|
8
|
+
1. Update .claude/session/active/ with current session state
|
|
9
|
+
2. Collect and index artifacts generated during agent execution
|
|
10
|
+
3. Persist metadata for session continuity
|
|
11
|
+
4. Prepare state for potential bundle creation (via /save-session)
|
|
12
|
+
|
|
13
|
+
Architecture:
|
|
14
|
+
- Part of the hybrid session management system
|
|
15
|
+
- Updates live session context (.claude/session/active/)
|
|
16
|
+
- Does NOT create bundles directly (bundles are on-demand via /save-session)
|
|
17
|
+
- Enables artifact collection for later bundling
|
|
18
|
+
|
|
19
|
+
Integration:
|
|
20
|
+
- Executed automatically after agent tool completes
|
|
21
|
+
- Works with session_startup_check.py for initialization
|
|
22
|
+
- Feeds data to /save-session for explicit bundling
|
|
23
|
+
- Maintains artifact inventory for historical reference
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
import json
|
|
29
|
+
import shutil
|
|
30
|
+
import logging
|
|
31
|
+
from datetime import datetime
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Dict, List, Any, Optional
|
|
34
|
+
import hashlib
|
|
35
|
+
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
|
|
36
|
+
|
|
37
|
+
# Configure structured logging
|
|
38
|
+
logging.basicConfig(
|
|
39
|
+
level=logging.INFO,
|
|
40
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
41
|
+
)
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ArtifactCopyError(Exception):
|
|
46
|
+
"""Custom exception for artifact copy failures"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BundleCreationError(Exception):
|
|
51
|
+
"""Custom exception for bundle creation failures"""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
class SessionManager:
|
|
55
|
+
"""Manages session state and bundle creation"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, bundle_dir: str = None):
|
|
58
|
+
if bundle_dir is None:
|
|
59
|
+
# Find the .claude directory by looking upward from current location
|
|
60
|
+
claude_dir = self._find_claude_dir()
|
|
61
|
+
bundle_dir = claude_dir / "session" / "bundles"
|
|
62
|
+
|
|
63
|
+
self.bundle_dir = Path(bundle_dir)
|
|
64
|
+
self.bundle_dir.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
self.session_id = self._get_or_create_session_id()
|
|
66
|
+
self.current_bundle_path = self.bundle_dir / f"{datetime.now().strftime('%Y-%m-%d')}-{self.session_id}"
|
|
67
|
+
logger.info(f"SessionManager initialized with bundle_dir={self.bundle_dir}, session_id={self.session_id}")
|
|
68
|
+
|
|
69
|
+
def _find_claude_dir(self) -> Path:
|
|
70
|
+
"""Find the .claude directory by searching upward from current location"""
|
|
71
|
+
current = Path.cwd()
|
|
72
|
+
|
|
73
|
+
# If we're already in a .claude directory, go up one level
|
|
74
|
+
if current.name == ".claude":
|
|
75
|
+
logger.debug("Already in .claude directory")
|
|
76
|
+
return current
|
|
77
|
+
|
|
78
|
+
# Look for .claude in current directory
|
|
79
|
+
claude_dir = current / ".claude"
|
|
80
|
+
if claude_dir.exists():
|
|
81
|
+
logger.debug(f"Found .claude at {claude_dir}")
|
|
82
|
+
return claude_dir
|
|
83
|
+
|
|
84
|
+
# Search upward through parent directories
|
|
85
|
+
for parent in current.parents:
|
|
86
|
+
claude_dir = parent / ".claude"
|
|
87
|
+
if claude_dir.exists():
|
|
88
|
+
logger.debug(f"Found .claude at {claude_dir}")
|
|
89
|
+
return claude_dir
|
|
90
|
+
|
|
91
|
+
# Default fallback - create .claude in current directory
|
|
92
|
+
logger.warning("No .claude directory found, creating in current directory")
|
|
93
|
+
claude_dir = current / ".claude"
|
|
94
|
+
claude_dir.mkdir(exist_ok=True)
|
|
95
|
+
return claude_dir
|
|
96
|
+
|
|
97
|
+
def _get_or_create_session_id(self) -> str:
|
|
98
|
+
"""Get existing session ID or create new one"""
|
|
99
|
+
session_id = os.environ.get("CLAUDE_SESSION_ID")
|
|
100
|
+
if not session_id:
|
|
101
|
+
# Generate session ID based on timestamp and some randomness
|
|
102
|
+
timestamp = datetime.now().strftime("%H%M%S")
|
|
103
|
+
hash_input = f"{timestamp}-{os.getpid()}"
|
|
104
|
+
session_hash = hashlib.sha256(hash_input.encode()).hexdigest()[:8]
|
|
105
|
+
session_id = f"session-{timestamp}-{session_hash}"
|
|
106
|
+
os.environ["CLAUDE_SESSION_ID"] = session_id
|
|
107
|
+
logger.info(f"Generated new session_id: {session_id}")
|
|
108
|
+
else:
|
|
109
|
+
logger.info(f"Using existing session_id: {session_id}")
|
|
110
|
+
return session_id
|
|
111
|
+
|
|
112
|
+
class BundleManager:
|
|
113
|
+
"""Manages artifact bundling and session persistence with enhanced error handling"""
|
|
114
|
+
|
|
115
|
+
def __init__(self, session_manager: SessionManager):
|
|
116
|
+
self.session_manager = session_manager
|
|
117
|
+
self.bundle_path = session_manager.current_bundle_path
|
|
118
|
+
logger.info(f"BundleManager initialized with bundle_path={self.bundle_path}")
|
|
119
|
+
|
|
120
|
+
def create_bundle(self, task_info: Dict[str, Any], artifacts: List[str],
|
|
121
|
+
agent_transcript: str) -> Path:
|
|
122
|
+
"""
|
|
123
|
+
Create a bundle with task artifacts and metadata
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
BundleCreationError: If bundle creation fails critically
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
# Create bundle directory
|
|
130
|
+
self.bundle_path.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
logger.info(f"Created bundle directory: {self.bundle_path}")
|
|
132
|
+
|
|
133
|
+
# Bundle metadata
|
|
134
|
+
bundle_metadata = {
|
|
135
|
+
"session_id": self.session_manager.session_id,
|
|
136
|
+
"timestamp": datetime.now().isoformat(),
|
|
137
|
+
"task_info": task_info,
|
|
138
|
+
"artifacts": artifacts,
|
|
139
|
+
"bundle_version": "1.1",
|
|
140
|
+
"errors": [] # Track any non-critical errors
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Save metadata
|
|
144
|
+
self._save_metadata(bundle_metadata)
|
|
145
|
+
|
|
146
|
+
# Save transcript
|
|
147
|
+
self._save_transcript(agent_transcript)
|
|
148
|
+
|
|
149
|
+
# Copy artifacts (with error tracking)
|
|
150
|
+
copy_errors = self._copy_artifacts(artifacts)
|
|
151
|
+
if copy_errors:
|
|
152
|
+
bundle_metadata["errors"].extend(copy_errors)
|
|
153
|
+
# Re-save metadata with error info
|
|
154
|
+
self._save_metadata(bundle_metadata)
|
|
155
|
+
|
|
156
|
+
# Generate summary
|
|
157
|
+
self._generate_summary(task_info, artifacts, copy_errors)
|
|
158
|
+
|
|
159
|
+
# Update latest symlink
|
|
160
|
+
self._update_latest_link()
|
|
161
|
+
|
|
162
|
+
logger.info(f"Bundle created successfully: {self.bundle_path}")
|
|
163
|
+
return self.bundle_path
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error(f"Critical error creating bundle: {e}", exc_info=True)
|
|
167
|
+
raise BundleCreationError(f"Failed to create bundle: {e}") from e
|
|
168
|
+
|
|
169
|
+
def _save_metadata(self, metadata: Dict[str, Any]):
|
|
170
|
+
"""Save metadata with error handling"""
|
|
171
|
+
try:
|
|
172
|
+
metadata_file = self.bundle_path / "metadata.json"
|
|
173
|
+
with open(metadata_file, "w") as f:
|
|
174
|
+
json.dump(metadata, f, indent=2)
|
|
175
|
+
logger.debug(f"Saved metadata to {metadata_file}")
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"Failed to save metadata: {e}")
|
|
178
|
+
raise
|
|
179
|
+
|
|
180
|
+
def _save_transcript(self, transcript: str):
|
|
181
|
+
"""Save transcript with error handling"""
|
|
182
|
+
try:
|
|
183
|
+
transcript_file = self.bundle_path / "transcript.md"
|
|
184
|
+
with open(transcript_file, "w") as f:
|
|
185
|
+
f.write(transcript)
|
|
186
|
+
logger.debug(f"Saved transcript to {transcript_file}")
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error(f"Failed to save transcript: {e}")
|
|
189
|
+
raise
|
|
190
|
+
|
|
191
|
+
@retry(
|
|
192
|
+
stop=stop_after_attempt(3),
|
|
193
|
+
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
194
|
+
retry=retry_if_exception_type(ArtifactCopyError)
|
|
195
|
+
)
|
|
196
|
+
def _copy_single_artifact(self, artifact_path: Path, dest_path: Path):
|
|
197
|
+
"""
|
|
198
|
+
Copy a single artifact with retry logic
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
ArtifactCopyError: If copy fails after retries
|
|
202
|
+
"""
|
|
203
|
+
try:
|
|
204
|
+
shutil.copy2(artifact_path, dest_path)
|
|
205
|
+
|
|
206
|
+
# Verify copy
|
|
207
|
+
if not dest_path.exists():
|
|
208
|
+
raise ArtifactCopyError(f"Destination file not created: {dest_path}")
|
|
209
|
+
|
|
210
|
+
if dest_path.stat().st_size == 0 and artifact_path.stat().st_size > 0:
|
|
211
|
+
raise ArtifactCopyError(f"Destination file empty: {dest_path}")
|
|
212
|
+
|
|
213
|
+
logger.debug(f"Successfully copied artifact: {artifact_path.name}")
|
|
214
|
+
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.warning(f"Failed to copy {artifact_path}: {e}")
|
|
217
|
+
raise ArtifactCopyError(f"Copy failed: {e}") from e
|
|
218
|
+
|
|
219
|
+
def _copy_artifacts(self, artifacts: List[str]) -> List[str]:
|
|
220
|
+
"""
|
|
221
|
+
Copy artifact files to bundle directory with graceful degradation
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
List of error messages for artifacts that failed to copy
|
|
225
|
+
"""
|
|
226
|
+
artifacts_dir = self.bundle_path / "artifacts"
|
|
227
|
+
artifacts_dir.mkdir(exist_ok=True)
|
|
228
|
+
|
|
229
|
+
errors = []
|
|
230
|
+
successful_copies = 0
|
|
231
|
+
|
|
232
|
+
for artifact_path_str in artifacts:
|
|
233
|
+
artifact_file = Path(artifact_path_str)
|
|
234
|
+
|
|
235
|
+
if not artifact_file.exists():
|
|
236
|
+
error_msg = f"Artifact not found: {artifact_path_str}"
|
|
237
|
+
logger.warning(error_msg)
|
|
238
|
+
errors.append(error_msg)
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
dest_path = artifacts_dir / artifact_file.name
|
|
243
|
+
self._copy_single_artifact(artifact_file, dest_path)
|
|
244
|
+
successful_copies += 1
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
error_msg = f"Failed to copy {artifact_path_str}: {str(e)}"
|
|
248
|
+
logger.error(error_msg)
|
|
249
|
+
errors.append(error_msg)
|
|
250
|
+
# Continue with other artifacts
|
|
251
|
+
|
|
252
|
+
logger.info(f"Copied {successful_copies}/{len(artifacts)} artifacts successfully")
|
|
253
|
+
|
|
254
|
+
if errors:
|
|
255
|
+
logger.warning(f"Encountered {len(errors)} errors during artifact copy")
|
|
256
|
+
|
|
257
|
+
return errors
|
|
258
|
+
|
|
259
|
+
def _generate_summary(self, task_info: Dict[str, Any], artifacts: List[str],
|
|
260
|
+
copy_errors: List[str]):
|
|
261
|
+
"""Generate bundle summary with error information"""
|
|
262
|
+
try:
|
|
263
|
+
summary_content = [
|
|
264
|
+
f"# Bundle Summary - {self.session_manager.session_id}",
|
|
265
|
+
f"",
|
|
266
|
+
f"**Date**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
|
267
|
+
f"**Task**: {task_info.get('task_id', 'Unknown')} - {task_info.get('description', 'Unknown')}",
|
|
268
|
+
f"**Agent**: {task_info.get('agent', 'Unknown')}",
|
|
269
|
+
f"**Security Tier**: {task_info.get('tier', 'Unknown')}",
|
|
270
|
+
f"",
|
|
271
|
+
f"## Artifacts Generated",
|
|
272
|
+
f"",
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
for artifact in artifacts:
|
|
276
|
+
artifact_name = Path(artifact).name
|
|
277
|
+
status = "❌" if any(artifact in err for err in copy_errors) else "✅"
|
|
278
|
+
summary_content.append(f"{status} `{artifact_name}`: {artifact}")
|
|
279
|
+
|
|
280
|
+
if copy_errors:
|
|
281
|
+
summary_content.extend([
|
|
282
|
+
f"",
|
|
283
|
+
f"## ⚠️ Errors Encountered",
|
|
284
|
+
f"",
|
|
285
|
+
])
|
|
286
|
+
for error in copy_errors:
|
|
287
|
+
summary_content.append(f"- {error}")
|
|
288
|
+
|
|
289
|
+
summary_content.extend([
|
|
290
|
+
f"",
|
|
291
|
+
f"## Task Context",
|
|
292
|
+
f"",
|
|
293
|
+
f"- **Working Directory**: {task_info.get('working_dir', 'Unknown')}",
|
|
294
|
+
f"- **Tags**: {', '.join(task_info.get('tags', []))}",
|
|
295
|
+
f"- **Project Context**: {task_info.get('project_context', 'None')}",
|
|
296
|
+
f"",
|
|
297
|
+
f"## Next Steps",
|
|
298
|
+
f"",
|
|
299
|
+
f"Review artifacts in `artifacts/` directory and check transcript for detailed execution log.",
|
|
300
|
+
f"",
|
|
301
|
+
])
|
|
302
|
+
|
|
303
|
+
summary_file = self.bundle_path / "summary.md"
|
|
304
|
+
with open(summary_file, "w") as f:
|
|
305
|
+
f.write("\n".join(summary_content))
|
|
306
|
+
|
|
307
|
+
logger.debug(f"Generated summary at {summary_file}")
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(f"Failed to generate summary: {e}")
|
|
311
|
+
# Non-critical, don't raise
|
|
312
|
+
|
|
313
|
+
def _update_latest_link(self):
|
|
314
|
+
"""Update latest bundle symlink"""
|
|
315
|
+
latest_link = self.bundle_path.parent / "latest"
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
# Remove existing symlink
|
|
319
|
+
if latest_link.is_symlink():
|
|
320
|
+
latest_link.unlink()
|
|
321
|
+
|
|
322
|
+
# Create new symlink
|
|
323
|
+
latest_link.symlink_to(self.bundle_path.name)
|
|
324
|
+
logger.debug(f"Updated latest symlink to {self.bundle_path.name}")
|
|
325
|
+
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.warning(f"Could not create latest symlink: {e}")
|
|
328
|
+
# Non-critical, don't raise
|
|
329
|
+
|
|
330
|
+
class ArtifactCollector:
|
|
331
|
+
"""Collects artifacts generated during agent execution"""
|
|
332
|
+
|
|
333
|
+
def __init__(self, working_dir: str = "."):
|
|
334
|
+
self.working_dir = Path(working_dir)
|
|
335
|
+
self.common_artifact_patterns = [
|
|
336
|
+
"*-validation-report.md",
|
|
337
|
+
"*-results-summary.md",
|
|
338
|
+
"*.tfplan",
|
|
339
|
+
"*-analysis.md",
|
|
340
|
+
"*-recommendations.md",
|
|
341
|
+
"security-scan-results.md",
|
|
342
|
+
"terraform-validation-report.md",
|
|
343
|
+
"gitops-validation-report.md",
|
|
344
|
+
"gcp-health-queries.md",
|
|
345
|
+
"gcp-health-results-summary.md",
|
|
346
|
+
]
|
|
347
|
+
logger.info(f"ArtifactCollector initialized with working_dir={self.working_dir}")
|
|
348
|
+
|
|
349
|
+
def collect_artifacts(self, task_id: str = None) -> List[str]:
|
|
350
|
+
"""
|
|
351
|
+
Collect all relevant artifacts from working directory with error handling
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
List of artifact paths
|
|
355
|
+
"""
|
|
356
|
+
artifacts = []
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
# Collect by pattern
|
|
360
|
+
for pattern in self.common_artifact_patterns:
|
|
361
|
+
try:
|
|
362
|
+
matching_files = list(self.working_dir.glob(pattern))
|
|
363
|
+
artifacts.extend([str(f) for f in matching_files])
|
|
364
|
+
except Exception as e:
|
|
365
|
+
logger.warning(f"Pattern {pattern} failed: {e}")
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
# Collect task-specific artifacts
|
|
369
|
+
if task_id:
|
|
370
|
+
try:
|
|
371
|
+
task_pattern = f"*{task_id.lower()}*"
|
|
372
|
+
task_files = list(self.working_dir.glob(task_pattern))
|
|
373
|
+
artifacts.extend([str(f) for f in task_files if f.suffix in ['.md', '.txt', '.json', '.yaml']])
|
|
374
|
+
except Exception as e:
|
|
375
|
+
logger.warning(f"Task-specific collection failed: {e}")
|
|
376
|
+
|
|
377
|
+
# Collect recent agent-generated files (created in last hour)
|
|
378
|
+
try:
|
|
379
|
+
import time
|
|
380
|
+
current_time = time.time()
|
|
381
|
+
agent_keywords = ['report', 'analysis', 'validation', 'summary', 'results', 'recommendations']
|
|
382
|
+
for file_path in self.working_dir.rglob("*"):
|
|
383
|
+
if file_path.is_file():
|
|
384
|
+
file_age = current_time - file_path.stat().st_mtime
|
|
385
|
+
if file_age < 3600 and file_path.suffix in ['.md', '.txt', '.json', '.yaml']:
|
|
386
|
+
if any(keyword in file_path.name.lower() for keyword in agent_keywords):
|
|
387
|
+
artifacts.append(str(file_path))
|
|
388
|
+
except Exception as e:
|
|
389
|
+
logger.warning(f"Recent file collection failed: {e}")
|
|
390
|
+
|
|
391
|
+
# Remove duplicates
|
|
392
|
+
artifacts = list(set(artifacts))
|
|
393
|
+
|
|
394
|
+
# Filter out system files
|
|
395
|
+
artifacts = [a for a in artifacts if not any(exclude in a for exclude in ['.git', '__pycache__', '.claude'])]
|
|
396
|
+
|
|
397
|
+
logger.info(f"Collected {len(artifacts)} artifacts")
|
|
398
|
+
return artifacts
|
|
399
|
+
|
|
400
|
+
except Exception as e:
|
|
401
|
+
logger.error(f"Error collecting artifacts: {e}")
|
|
402
|
+
return [] # Return empty list on error, don't crash
|
|
403
|
+
|
|
404
|
+
def subagent_stop_hook(task_info: Dict[str, Any], agent_output: str) -> Dict[str, Any]:
|
|
405
|
+
"""
|
|
406
|
+
Main subagent stop hook - OPTION A: SILENT MODE
|
|
407
|
+
|
|
408
|
+
Philosophy:
|
|
409
|
+
- Only updates .claude/session/active/ with session state
|
|
410
|
+
- Does NOT create bundles automatically
|
|
411
|
+
- Bundles are created on-demand via /save-session
|
|
412
|
+
- Keeps everything silent and non-intrusive
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
task_info: Task information including ID, description, agent, etc.
|
|
416
|
+
agent_output: Complete output from agent execution
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Success confirmation (no bundle creation)
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
# Initialize session manager (NOT bundle manager)
|
|
424
|
+
session_manager = SessionManager()
|
|
425
|
+
|
|
426
|
+
# Update .claude/session/active/ with current session state
|
|
427
|
+
active_dir = Path(session_manager._find_claude_dir()) / "session" / "active"
|
|
428
|
+
active_dir.mkdir(parents=True, exist_ok=True)
|
|
429
|
+
|
|
430
|
+
# Save session context for this agent execution
|
|
431
|
+
session_context = {
|
|
432
|
+
"timestamp": datetime.now().isoformat(),
|
|
433
|
+
"session_id": session_manager.session_id,
|
|
434
|
+
"task_id": task_info.get('task_id', 'unknown'),
|
|
435
|
+
"agent": task_info.get('agent', 'unknown'),
|
|
436
|
+
"description": task_info.get('description', ''),
|
|
437
|
+
"tier": task_info.get('tier', 'unknown'),
|
|
438
|
+
"tags": task_info.get('tags', []),
|
|
439
|
+
"working_dir": str(task_info.get('working_dir', '.')),
|
|
440
|
+
"project_context": task_info.get('project_context', ''),
|
|
441
|
+
# Store agent output for later retrieval
|
|
442
|
+
"agent_output_available": len(agent_output) > 0
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# Save context as JSON
|
|
446
|
+
context_file = active_dir / "context.json"
|
|
447
|
+
with open(context_file, "w") as f:
|
|
448
|
+
json.dump(session_context, f, indent=2)
|
|
449
|
+
|
|
450
|
+
logger.debug(f"✅ Updated session context: {context_file}")
|
|
451
|
+
|
|
452
|
+
# Silently collect artifacts (for later use by /save-session)
|
|
453
|
+
artifact_collector = ArtifactCollector()
|
|
454
|
+
artifacts = artifact_collector.collect_artifacts(task_info.get('task_id'))
|
|
455
|
+
logger.debug(f"📊 Artifacts available for bundling: {len(artifacts)}")
|
|
456
|
+
|
|
457
|
+
# Return success (silent - no bundle created yet)
|
|
458
|
+
return {
|
|
459
|
+
"success": True,
|
|
460
|
+
"session_id": session_manager.session_id,
|
|
461
|
+
"status": "active_updated",
|
|
462
|
+
"artifacts_available": len(artifacts),
|
|
463
|
+
"note": "Bundle creation deferred to /save-session"
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
except Exception as e:
|
|
467
|
+
logger.debug(f"⚠️ Error updating session context: {e}")
|
|
468
|
+
# Don't fail completely - just log and continue
|
|
469
|
+
return {
|
|
470
|
+
"success": False,
|
|
471
|
+
"error": str(e),
|
|
472
|
+
"status": "partial_update",
|
|
473
|
+
"note": "Session context update failed, but agent execution may have succeeded"
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
def main():
|
|
477
|
+
"""CLI interface for testing bundle creation"""
|
|
478
|
+
|
|
479
|
+
if len(sys.argv) < 2:
|
|
480
|
+
print("Usage: python subagent_stop.py <task_id>")
|
|
481
|
+
print(" python subagent_stop.py --test")
|
|
482
|
+
sys.exit(1)
|
|
483
|
+
|
|
484
|
+
if sys.argv[1] == "--test":
|
|
485
|
+
# Test bundle creation
|
|
486
|
+
test_task_info = {
|
|
487
|
+
"task_id": "T006",
|
|
488
|
+
"description": "Terraform plan for infrastructure",
|
|
489
|
+
"agent": "terraform-specialist",
|
|
490
|
+
"tier": "T1",
|
|
491
|
+
"tags": ["#terraform", "#infrastructure"],
|
|
492
|
+
"working_dir": os.getcwd(),
|
|
493
|
+
"project_context": "Multi-project repository with TCM application"
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
test_output = """
|
|
497
|
+
# Terraform Specialist Execution Log
|
|
498
|
+
|
|
499
|
+
## Task: T006 - Terraform plan for infrastructure
|
|
500
|
+
|
|
501
|
+
### Actions Performed:
|
|
502
|
+
1. ✅ Format check: terraform fmt -check
|
|
503
|
+
2. ✅ Initialization: terraform init -backend=false
|
|
504
|
+
3. ✅ Validation: terraform validate
|
|
505
|
+
4. ✅ Planning: terraform plan -out=tfplan
|
|
506
|
+
|
|
507
|
+
### Results:
|
|
508
|
+
- Configuration is properly formatted
|
|
509
|
+
- All modules validated successfully
|
|
510
|
+
- Plan generated with 12 resources to create
|
|
511
|
+
- No security issues detected
|
|
512
|
+
|
|
513
|
+
### Artifacts Generated:
|
|
514
|
+
- terraform-validation-report.md
|
|
515
|
+
- infrastructure.tfplan
|
|
516
|
+
- security-scan-results.md
|
|
517
|
+
|
|
518
|
+
### Recommendations:
|
|
519
|
+
- Review tfplan before any apply operations
|
|
520
|
+
- Consider implementing resource tagging
|
|
521
|
+
- Enable state locking for production
|
|
522
|
+
"""
|
|
523
|
+
|
|
524
|
+
result = subagent_stop_hook(test_task_info, test_output)
|
|
525
|
+
|
|
526
|
+
if result["success"]:
|
|
527
|
+
print("✅ Test bundle creation successful!")
|
|
528
|
+
print(f"📁 Bundle path: {result['bundle_path']}")
|
|
529
|
+
print(f"📊 Artifacts: {result['artifacts_count']}")
|
|
530
|
+
|
|
531
|
+
# Show bundle contents
|
|
532
|
+
bundle_path = Path(result["bundle_path"])
|
|
533
|
+
if bundle_path.exists():
|
|
534
|
+
print("\n📋 Bundle contents:")
|
|
535
|
+
for item in sorted(bundle_path.rglob("*")):
|
|
536
|
+
if item.is_file():
|
|
537
|
+
relative_path = item.relative_to(bundle_path)
|
|
538
|
+
print(f" {relative_path}")
|
|
539
|
+
else:
|
|
540
|
+
print(f"❌ Test failed: {result['error']}")
|
|
541
|
+
|
|
542
|
+
else:
|
|
543
|
+
task_id = sys.argv[1]
|
|
544
|
+
print(f"Creating bundle for task: {task_id}")
|
|
545
|
+
# In real usage, this would be called by the agent system
|
|
546
|
+
print("Note: This would normally be called automatically by the agent system")
|
|
547
|
+
|
|
548
|
+
if __name__ == "__main__":
|
|
549
|
+
main()
|
package/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @aaxis/claude-agents
|
|
3
|
+
*
|
|
4
|
+
* Shared Claude Code agent system for Aaxis DevOps workflows
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { getAgentPath, getToolPath } from '@aaxis/claude-agents';
|
|
8
|
+
* const agentPath = getAgentPath('gitops-operator');
|
|
9
|
+
* const toolPath = getToolPath('context_provider.py');
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { dirname, join } from 'path';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
|
|
18
|
+
export const PACKAGE_ROOT = __dirname;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get absolute path to an agent definition
|
|
22
|
+
* @param {string} agentName - Name of the agent (e.g., 'gitops-operator')
|
|
23
|
+
* @returns {string} Absolute path to agent file
|
|
24
|
+
*/
|
|
25
|
+
export function getAgentPath(agentName) {
|
|
26
|
+
return join(PACKAGE_ROOT, 'agents', `${agentName}.md`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get absolute path to a tool
|
|
31
|
+
* @param {string} toolName - Name of the tool (e.g., 'context_provider.py')
|
|
32
|
+
* @returns {string} Absolute path to tool file
|
|
33
|
+
*/
|
|
34
|
+
export function getToolPath(toolName) {
|
|
35
|
+
return join(PACKAGE_ROOT, 'tools', toolName);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get absolute path to a hook
|
|
40
|
+
* @param {string} hookName - Name of the hook (e.g., 'pre-commit')
|
|
41
|
+
* @returns {string} Absolute path to hook file
|
|
42
|
+
*/
|
|
43
|
+
export function getHookPath(hookName) {
|
|
44
|
+
return join(PACKAGE_ROOT, 'hooks', hookName);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get absolute path to a command
|
|
49
|
+
* @param {string} commandName - Name of the command (e.g., 'architect.md')
|
|
50
|
+
* @returns {string} Absolute path to command file
|
|
51
|
+
*/
|
|
52
|
+
export function getCommandPath(commandName) {
|
|
53
|
+
return join(PACKAGE_ROOT, 'commands', commandName);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get absolute path to documentation
|
|
58
|
+
* @param {string} docName - Name of the doc (e.g., 'orchestration-workflow.md')
|
|
59
|
+
* @returns {string} Absolute path to doc file
|
|
60
|
+
*/
|
|
61
|
+
export function getDocPath(docName) {
|
|
62
|
+
return join(PACKAGE_ROOT, 'docs', docName);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get absolute path to a template
|
|
67
|
+
* @param {string} templateName - Name of the template (e.g., 'CLAUDE.template.md')
|
|
68
|
+
* @returns {string} Absolute path to template file
|
|
69
|
+
*/
|
|
70
|
+
export function getTemplatePath(templateName) {
|
|
71
|
+
return join(PACKAGE_ROOT, 'templates', templateName);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get absolute path to config file
|
|
76
|
+
* @param {string} configName - Name of the config (e.g., 'git_standards.json')
|
|
77
|
+
* @returns {string} Absolute path to config file
|
|
78
|
+
*/
|
|
79
|
+
export function getConfigPath(configName) {
|
|
80
|
+
return join(PACKAGE_ROOT, 'config', configName);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default {
|
|
84
|
+
PACKAGE_ROOT,
|
|
85
|
+
getAgentPath,
|
|
86
|
+
getToolPath,
|
|
87
|
+
getHookPath,
|
|
88
|
+
getCommandPath,
|
|
89
|
+
getDocPath,
|
|
90
|
+
getTemplatePath,
|
|
91
|
+
getConfigPath
|
|
92
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jaguilar87/gaia-ops",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Multi-agent orchestration system for Claude Code - DevOps automation toolkit",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"gaia-init": "./bin/gaia-init.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"claude-code",
|
|
12
|
+
"devops",
|
|
13
|
+
"gitops",
|
|
14
|
+
"terraform",
|
|
15
|
+
"kubernetes",
|
|
16
|
+
"ai-agents",
|
|
17
|
+
"gaia-ops",
|
|
18
|
+
"orchestration",
|
|
19
|
+
"automation"
|
|
20
|
+
],
|
|
21
|
+
"author": "Jorge Aguilar <jaguilar1897@gmail.com>",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"private": false,
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/metraton/gaia-ops.git"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin/",
|
|
30
|
+
"agents/",
|
|
31
|
+
"tools/",
|
|
32
|
+
"hooks/",
|
|
33
|
+
"commands/",
|
|
34
|
+
"templates/",
|
|
35
|
+
"config/",
|
|
36
|
+
"speckit/",
|
|
37
|
+
"CLAUDE.md",
|
|
38
|
+
"CHANGELOG.md",
|
|
39
|
+
"index.js"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "pytest tests/ -v",
|
|
43
|
+
"validate": "python3 tools/commit_validator.py",
|
|
44
|
+
"lint": "eslint ."
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"prompts": "^2.4.2",
|
|
48
|
+
"chalk": "^5.3.0",
|
|
49
|
+
"ora": "^7.0.1",
|
|
50
|
+
"yargs": "^17.7.2"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"eslint": "^8.50.0"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=18.0.0",
|
|
57
|
+
"python": ">=3.9"
|
|
58
|
+
}
|
|
59
|
+
}
|