@jaguilar87/gaia-ops 3.9.8 → 3.9.9

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.
@@ -33,10 +33,22 @@ from pathlib import Path
33
33
  from typing import Dict, List, Any, Optional
34
34
  import hashlib
35
35
 
36
- # Configure structured logging
36
+ # Configure structured logging with file handler (matching pre_tool_use.py pattern)
37
+ try:
38
+ from modules.core.paths import get_logs_dir
39
+ _log_dir = get_logs_dir()
40
+ except ImportError:
41
+ _log_dir = Path.cwd() / ".claude" / "logs"
42
+ _log_dir.mkdir(parents=True, exist_ok=True)
43
+
44
+ _log_file = _log_dir / f"subagent_stop-{os.getenv('USER', 'unknown')}.log"
37
45
  logging.basicConfig(
38
46
  level=logging.INFO,
39
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
47
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
48
+ handlers=[
49
+ logging.FileHandler(_log_file),
50
+ logging.StreamHandler()
51
+ ]
40
52
  )
41
53
  logger = logging.getLogger(__name__)
42
54
 
@@ -668,41 +680,52 @@ def subagent_stop_hook(task_info: Dict[str, Any], agent_output: str) -> Dict[str
668
680
  def _read_transcript(transcript_path: str) -> str:
669
681
  """Read agent transcript from file path provided by Claude Code.
670
682
 
671
- Claude Code provides ``agent_transcript_path`` pointing to a JSONL file
672
- with the subagent's conversation messages. We extract text content from
673
- assistant messages to reconstruct the agent output.
683
+ Claude Code provides ``agent_transcript_path`` pointing to a JSONL file.
684
+ Each line has the structure:
685
+ {"type": "assistant", "message": {"role": "assistant", "content": [...]}, ...}
686
+ The role/content are nested inside the ``message`` field.
674
687
 
675
688
  Falls back to empty string on any error so the hook never crashes.
676
689
  """
677
690
  try:
678
- path = Path(transcript_path)
691
+ # Expand ~ to home directory (Claude Code may use ~ in paths)
692
+ path = Path(transcript_path).expanduser()
693
+ logger.debug("Reading transcript from: %s", path)
694
+
679
695
  if not path.exists():
680
- logger.debug("Transcript file not found: %s", transcript_path)
696
+ logger.warning("Transcript file not found: %s", path)
681
697
  return ""
682
698
 
683
699
  lines = path.read_text().strip().splitlines()
700
+
684
701
  text_parts: List[str] = []
685
702
  for line in lines:
686
703
  if not line.strip():
687
704
  continue
688
705
  try:
689
- msg = json.loads(line)
690
- # Extract text from assistant messages
706
+ entry = json.loads(line)
707
+
708
+ # Claude Code transcript format: content is inside entry["message"]
709
+ msg = entry.get("message", entry) # fallback to entry itself for simple format
691
710
  role = msg.get("role", "")
692
- if role == "assistant":
693
- content = msg.get("content", "")
694
- if isinstance(content, str):
695
- text_parts.append(content)
696
- elif isinstance(content, list):
697
- for block in content:
698
- if isinstance(block, dict) and block.get("type") == "text":
699
- text_parts.append(block.get("text", ""))
700
- elif isinstance(block, str):
701
- text_parts.append(block)
711
+ if role != "assistant":
712
+ continue
713
+
714
+ content = msg.get("content", "")
715
+ if isinstance(content, str):
716
+ text_parts.append(content)
717
+ elif isinstance(content, list):
718
+ for block in content:
719
+ if isinstance(block, dict) and block.get("type") == "text":
720
+ text_parts.append(block.get("text", ""))
721
+ elif isinstance(block, str):
722
+ text_parts.append(block)
702
723
  except (json.JSONDecodeError, TypeError):
703
724
  continue
704
725
 
705
- return "\n".join(text_parts)
726
+ result = "\n".join(text_parts)
727
+ logger.debug("Extracted %d text parts, total length: %d chars", len(text_parts), len(result))
728
+ return result
706
729
 
707
730
  except Exception as e:
708
731
  logger.debug("Failed to read transcript from %s: %s", transcript_path, e)
@@ -803,7 +826,7 @@ if __name__ == "__main__":
803
826
 
804
827
  hook_data = json.loads(stdin_data)
805
828
 
806
- logger.info(f"Hook event: {hook_data.get('hook_event_name')}")
829
+ logger.info(f"Hook event: {hook_data.get('hook_event_name')}, agent: {hook_data.get('agent_type', 'unknown')}")
807
830
 
808
831
  # Build task_info from Claude Code SubagentStop payload
809
832
  task_info = _build_task_info_from_hook_data(hook_data)
@@ -811,6 +834,7 @@ if __name__ == "__main__":
811
834
  # Extract agent output from transcript file
812
835
  transcript_path = hook_data.get("agent_transcript_path", "")
813
836
  agent_output = _read_transcript(transcript_path) if transcript_path else ""
837
+ logger.info(f"Agent output: {len(agent_output)} chars from transcript")
814
838
 
815
839
  # Run the full processing chain
816
840
  result = subagent_stop_hook(task_info, agent_output)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaguilar87/gaia-ops",
3
- "version": "3.9.8",
3
+ "version": "3.9.9",
4
4
  "description": "Multi-agent orchestration system for Claude Code - DevOps automation toolkit",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -339,12 +339,15 @@ class TestStdinHandler:
339
339
  """Simulate Claude Code SubagentStop: pipe JSON via stdin with transcript."""
340
340
  context_file = project_env["context_file"]
341
341
 
342
- # Create a fake transcript JSONL file
342
+ # Create a fake transcript JSONL file (Claude Code format: content inside "message")
343
343
  transcript_path = tmp_path / "agent_transcript.jsonl"
344
344
  transcript_lines = [
345
345
  json.dumps({
346
- "role": "assistant",
347
- "content": AGENT_OUTPUT_WITH_CONTEXT_UPDATE,
346
+ "type": "assistant",
347
+ "message": {
348
+ "role": "assistant",
349
+ "content": [{"type": "text", "text": AGENT_OUTPUT_WITH_CONTEXT_UPDATE}],
350
+ },
348
351
  }),
349
352
  ]
350
353
  transcript_path.write_text("\n".join(transcript_lines))
@@ -463,23 +466,26 @@ class TestStdinHandler:
463
466
  """Verify handling of transcript with content as list of blocks."""
464
467
  context_file = project_env["context_file"]
465
468
 
466
- # Create transcript with content as list (Claude API format)
469
+ # Create transcript with content as list (Claude Code transcript format)
467
470
  transcript_path = tmp_path / "agent_transcript_blocks.jsonl"
468
471
  transcript_lines = [
469
472
  json.dumps({
470
- "role": "assistant",
471
- "content": [
472
- {"type": "text", "text": "## Namespace Validation Report\n\n20 namespaces found.\n\n"},
473
- {"type": "text", "text": "CONTEXT_UPDATE:\n"},
474
- {"type": "text", "text": json.dumps({
475
- "cluster_details": {
476
- "namespaces": {
477
- "application": ["adm", "dev"],
478
- "system": ["kube-system"],
473
+ "type": "assistant",
474
+ "message": {
475
+ "role": "assistant",
476
+ "content": [
477
+ {"type": "text", "text": "## Namespace Validation Report\n\n20 namespaces found.\n\n"},
478
+ {"type": "text", "text": "CONTEXT_UPDATE:\n"},
479
+ {"type": "text", "text": json.dumps({
480
+ "cluster_details": {
481
+ "namespaces": {
482
+ "application": ["adm", "dev"],
483
+ "system": ["kube-system"],
484
+ }
479
485
  }
480
- }
481
- })},
482
- ],
486
+ })},
487
+ ],
488
+ },
483
489
  }),
484
490
  ]
485
491
  transcript_path.write_text("\n".join(transcript_lines))
@@ -528,25 +534,33 @@ class TestReadTranscript:
528
534
  """Unit tests for the _read_transcript helper."""
529
535
 
530
536
  def test_read_string_content(self, tmp_path):
537
+ """Claude Code transcript format with string content inside message."""
531
538
  mod = _import_subagent_stop()
532
539
  transcript = tmp_path / "transcript.jsonl"
533
540
  transcript.write_text(json.dumps({
534
- "role": "assistant",
535
- "content": "Hello world",
541
+ "type": "assistant",
542
+ "message": {
543
+ "role": "assistant",
544
+ "content": "Hello world",
545
+ },
536
546
  }))
537
547
 
538
548
  result = mod._read_transcript(str(transcript))
539
549
  assert "Hello world" in result
540
550
 
541
551
  def test_read_list_content(self, tmp_path):
552
+ """Claude Code transcript format with list content blocks."""
542
553
  mod = _import_subagent_stop()
543
554
  transcript = tmp_path / "transcript.jsonl"
544
555
  transcript.write_text(json.dumps({
545
- "role": "assistant",
546
- "content": [
547
- {"type": "text", "text": "Part 1"},
548
- {"type": "text", "text": "Part 2"},
549
- ],
556
+ "type": "assistant",
557
+ "message": {
558
+ "role": "assistant",
559
+ "content": [
560
+ {"type": "text", "text": "Part 1"},
561
+ {"type": "text", "text": "Part 2"},
562
+ ],
563
+ },
550
564
  }))
551
565
 
552
566
  result = mod._read_transcript(str(transcript))
@@ -554,11 +568,13 @@ class TestReadTranscript:
554
568
  assert "Part 2" in result
555
569
 
556
570
  def test_skips_user_messages(self, tmp_path):
571
+ """Only assistant messages are extracted, user/progress entries are skipped."""
557
572
  mod = _import_subagent_stop()
558
573
  transcript = tmp_path / "transcript.jsonl"
559
574
  lines = [
560
- json.dumps({"role": "user", "content": "user message"}),
561
- json.dumps({"role": "assistant", "content": "assistant message"}),
575
+ json.dumps({"type": "user", "message": {"role": "user", "content": "user message"}}),
576
+ json.dumps({"type": "assistant", "message": {"role": "assistant", "content": "assistant message"}}),
577
+ json.dumps({"type": "progress", "message": {}}),
562
578
  ]
563
579
  transcript.write_text("\n".join(lines))
564
580
 
@@ -566,6 +582,18 @@ class TestReadTranscript:
566
582
  assert "user message" not in result
567
583
  assert "assistant message" in result
568
584
 
585
+ def test_fallback_simple_format(self, tmp_path):
586
+ """Fallback: if no 'message' key, treat entry itself as the message."""
587
+ mod = _import_subagent_stop()
588
+ transcript = tmp_path / "transcript.jsonl"
589
+ transcript.write_text(json.dumps({
590
+ "role": "assistant",
591
+ "content": "simple format",
592
+ }))
593
+
594
+ result = mod._read_transcript(str(transcript))
595
+ assert "simple format" in result
596
+
569
597
  def test_missing_file_returns_empty(self, tmp_path):
570
598
  mod = _import_subagent_stop()
571
599
  result = mod._read_transcript(str(tmp_path / "nonexistent.jsonl"))