htmlgraph 0.24.2__py3-none-any.whl → 0.26.1__py3-none-any.whl
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.
- htmlgraph/__init__.py +20 -1
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/analytics/cross_session.py +4 -3
- htmlgraph/analytics/work_type.py +52 -16
- htmlgraph/analytics_index.py +51 -19
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/main.py +2263 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +812 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1020 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +509 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +163 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/builders/base.py +55 -1
- htmlgraph/builders/bug.py +17 -2
- htmlgraph/builders/chore.py +17 -2
- htmlgraph/builders/epic.py +17 -2
- htmlgraph/builders/feature.py +25 -2
- htmlgraph/builders/phase.py +17 -2
- htmlgraph/builders/spike.py +27 -2
- htmlgraph/builders/track.py +14 -0
- htmlgraph/cigs/__init__.py +4 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cli.py +1427 -401
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +21 -0
- htmlgraph/collections/session.py +189 -0
- htmlgraph/collections/spike.py +7 -1
- htmlgraph/collections/task_delegation.py +236 -0
- htmlgraph/collections/traces.py +482 -0
- htmlgraph/config.py +113 -0
- htmlgraph/converter.py +41 -0
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +3356 -492
- htmlgraph-0.24.2.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1584 -0
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +710 -0
- htmlgraph/docs/README.md +533 -0
- htmlgraph/docs/version_check.py +3 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +2 -0
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +318 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +496 -79
- htmlgraph/hooks/orchestrator.py +6 -4
- htmlgraph/hooks/orchestrator_reflector.py +4 -4
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/pretooluse.py +473 -6
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +637 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_stop.py +309 -0
- htmlgraph/hooks/task_enforcer.py +39 -0
- htmlgraph/hooks/validator.py +15 -11
- htmlgraph/models.py +111 -15
- htmlgraph/operations/fastapi_server.py +230 -0
- htmlgraph/orchestration/headless_spawner.py +344 -29
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/repo_hash.py +511 -0
- htmlgraph/sdk.py +348 -10
- htmlgraph/server.py +194 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +131 -1
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/system_prompts.py +449 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +19 -0
- htmlgraph/validation.py +115 -0
- htmlgraph-0.26.1.data/data/htmlgraph/dashboard.html +7458 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/METADATA +91 -64
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/RECORD +112 -46
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/WHEEL +0 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/entry_points.txt +0 -0
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import subprocess
|
|
6
|
+
import time
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from typing import TYPE_CHECKING
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
11
|
+
from htmlgraph.orchestration.live_events import LiveEventPublisher
|
|
10
12
|
from htmlgraph.sdk import SDK
|
|
11
13
|
|
|
12
14
|
|
|
@@ -63,7 +65,92 @@ class HeadlessSpawner:
|
|
|
63
65
|
|
|
64
66
|
def __init__(self) -> None:
|
|
65
67
|
"""Initialize spawner."""
|
|
66
|
-
|
|
68
|
+
self._live_publisher: LiveEventPublisher | None = None
|
|
69
|
+
|
|
70
|
+
def _get_live_publisher(self) -> "LiveEventPublisher | None":
|
|
71
|
+
"""
|
|
72
|
+
Get LiveEventPublisher instance for real-time WebSocket streaming.
|
|
73
|
+
|
|
74
|
+
Returns None if publisher unavailable (optional dependency).
|
|
75
|
+
"""
|
|
76
|
+
if self._live_publisher is None:
|
|
77
|
+
try:
|
|
78
|
+
from htmlgraph.orchestration.live_events import LiveEventPublisher
|
|
79
|
+
|
|
80
|
+
self._live_publisher = LiveEventPublisher()
|
|
81
|
+
except Exception:
|
|
82
|
+
# Live events are optional
|
|
83
|
+
pass
|
|
84
|
+
return self._live_publisher
|
|
85
|
+
|
|
86
|
+
def _publish_live_event(
|
|
87
|
+
self,
|
|
88
|
+
event_type: str,
|
|
89
|
+
spawner_type: str,
|
|
90
|
+
**kwargs: str | int | float | bool | None,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Publish a live event for WebSocket streaming.
|
|
94
|
+
|
|
95
|
+
Silently fails if publisher unavailable (optional feature).
|
|
96
|
+
"""
|
|
97
|
+
publisher = self._get_live_publisher()
|
|
98
|
+
if publisher is None:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
parent_event_id = os.getenv("HTMLGRAPH_PARENT_EVENT")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
if event_type == "spawner_start":
|
|
105
|
+
publisher.spawner_start(
|
|
106
|
+
spawner_type=spawner_type,
|
|
107
|
+
prompt=str(kwargs.get("prompt", "")),
|
|
108
|
+
parent_event_id=parent_event_id,
|
|
109
|
+
model=str(kwargs.get("model", "")) if kwargs.get("model") else None,
|
|
110
|
+
)
|
|
111
|
+
elif event_type == "spawner_phase":
|
|
112
|
+
progress_val = kwargs.get("progress")
|
|
113
|
+
publisher.spawner_phase(
|
|
114
|
+
spawner_type=spawner_type,
|
|
115
|
+
phase=str(kwargs.get("phase", "executing")),
|
|
116
|
+
progress=int(progress_val) if progress_val is not None else None,
|
|
117
|
+
details=str(kwargs.get("details", ""))
|
|
118
|
+
if kwargs.get("details")
|
|
119
|
+
else None,
|
|
120
|
+
parent_event_id=parent_event_id,
|
|
121
|
+
)
|
|
122
|
+
elif event_type == "spawner_complete":
|
|
123
|
+
duration_val = kwargs.get("duration")
|
|
124
|
+
tokens_val = kwargs.get("tokens")
|
|
125
|
+
publisher.spawner_complete(
|
|
126
|
+
spawner_type=spawner_type,
|
|
127
|
+
success=bool(kwargs.get("success", False)),
|
|
128
|
+
duration_seconds=float(duration_val)
|
|
129
|
+
if duration_val is not None
|
|
130
|
+
else None,
|
|
131
|
+
response_preview=str(kwargs.get("response", ""))[:200]
|
|
132
|
+
if kwargs.get("response")
|
|
133
|
+
else None,
|
|
134
|
+
tokens_used=int(tokens_val) if tokens_val is not None else None,
|
|
135
|
+
error=str(kwargs.get("error", "")) if kwargs.get("error") else None,
|
|
136
|
+
parent_event_id=parent_event_id,
|
|
137
|
+
)
|
|
138
|
+
elif event_type == "spawner_tool_use":
|
|
139
|
+
publisher.spawner_tool_use(
|
|
140
|
+
spawner_type=spawner_type,
|
|
141
|
+
tool_name=str(kwargs.get("tool_name", "unknown")),
|
|
142
|
+
parent_event_id=parent_event_id,
|
|
143
|
+
)
|
|
144
|
+
elif event_type == "spawner_message":
|
|
145
|
+
publisher.spawner_message(
|
|
146
|
+
spawner_type=spawner_type,
|
|
147
|
+
message=str(kwargs.get("message", "")),
|
|
148
|
+
role=str(kwargs.get("role", "assistant")),
|
|
149
|
+
parent_event_id=parent_event_id,
|
|
150
|
+
)
|
|
151
|
+
except Exception:
|
|
152
|
+
# Live events should never break spawner execution
|
|
153
|
+
pass
|
|
67
154
|
|
|
68
155
|
def _get_sdk(self) -> "SDK | None":
|
|
69
156
|
"""
|
|
@@ -382,14 +469,14 @@ class HeadlessSpawner:
|
|
|
382
469
|
|
|
383
470
|
Args:
|
|
384
471
|
prompt: Task description for Gemini
|
|
385
|
-
output_format: "json" or "stream-json" (
|
|
386
|
-
model: Model selection (e.g., "gemini-2.0-flash"). Default: None
|
|
387
|
-
include_directories:
|
|
472
|
+
output_format: "json" or "stream-json" (enables real-time tracking)
|
|
473
|
+
model: Model selection (e.g., "gemini-2.0-flash"). Default: None
|
|
474
|
+
include_directories: Directories to include for context. Default: None
|
|
388
475
|
track_in_htmlgraph: Enable HtmlGraph activity tracking. Default: True
|
|
389
476
|
timeout: Max seconds to wait
|
|
390
477
|
|
|
391
478
|
Returns:
|
|
392
|
-
AIResult with response
|
|
479
|
+
AIResult with response, error, and tracked events if tracking enabled
|
|
393
480
|
"""
|
|
394
481
|
# Initialize tracking if enabled
|
|
395
482
|
sdk: SDK | None = None
|
|
@@ -397,6 +484,15 @@ class HeadlessSpawner:
|
|
|
397
484
|
if track_in_htmlgraph:
|
|
398
485
|
sdk = self._get_sdk()
|
|
399
486
|
|
|
487
|
+
# Publish live event: spawner starting
|
|
488
|
+
self._publish_live_event(
|
|
489
|
+
"spawner_start",
|
|
490
|
+
"gemini",
|
|
491
|
+
prompt=prompt,
|
|
492
|
+
model=model,
|
|
493
|
+
)
|
|
494
|
+
start_time = time.time()
|
|
495
|
+
|
|
400
496
|
try:
|
|
401
497
|
# Build command based on tested pattern from spike spk-4029eef3
|
|
402
498
|
cmd = ["gemini", "-p", prompt, "--output-format", output_format]
|
|
@@ -425,6 +521,14 @@ class HeadlessSpawner:
|
|
|
425
521
|
# Tracking failure should not break execution
|
|
426
522
|
pass
|
|
427
523
|
|
|
524
|
+
# Publish live event: executing
|
|
525
|
+
self._publish_live_event(
|
|
526
|
+
"spawner_phase",
|
|
527
|
+
"gemini",
|
|
528
|
+
phase="executing",
|
|
529
|
+
details="Running Gemini CLI",
|
|
530
|
+
)
|
|
531
|
+
|
|
428
532
|
# Execute with timeout and stderr redirection
|
|
429
533
|
# Note: Cannot use capture_output with stderr parameter
|
|
430
534
|
result = subprocess.run(
|
|
@@ -435,8 +539,24 @@ class HeadlessSpawner:
|
|
|
435
539
|
timeout=timeout,
|
|
436
540
|
)
|
|
437
541
|
|
|
542
|
+
# Publish live event: processing response
|
|
543
|
+
self._publish_live_event(
|
|
544
|
+
"spawner_phase",
|
|
545
|
+
"gemini",
|
|
546
|
+
phase="processing",
|
|
547
|
+
details="Parsing Gemini response",
|
|
548
|
+
)
|
|
549
|
+
|
|
438
550
|
# Check for command execution errors
|
|
439
551
|
if result.returncode != 0:
|
|
552
|
+
duration = time.time() - start_time
|
|
553
|
+
self._publish_live_event(
|
|
554
|
+
"spawner_complete",
|
|
555
|
+
"gemini",
|
|
556
|
+
success=False,
|
|
557
|
+
duration=duration,
|
|
558
|
+
error=f"CLI failed with exit code {result.returncode}",
|
|
559
|
+
)
|
|
440
560
|
return AIResult(
|
|
441
561
|
success=False,
|
|
442
562
|
response="",
|
|
@@ -455,16 +575,20 @@ class HeadlessSpawner:
|
|
|
455
575
|
# Only use stream-json parsing if we got valid events
|
|
456
576
|
if tracked_events:
|
|
457
577
|
# For stream-json, we need to extract response differently
|
|
458
|
-
#
|
|
578
|
+
# Collect all assistant message content, then check result
|
|
459
579
|
response_text = ""
|
|
460
580
|
for event in tracked_events:
|
|
461
|
-
if event.get("type") == "
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
581
|
+
if event.get("type") == "message":
|
|
582
|
+
# Only collect assistant messages
|
|
583
|
+
if event.get("role") == "assistant":
|
|
584
|
+
content = event.get("content", "")
|
|
585
|
+
if content:
|
|
586
|
+
response_text += content
|
|
587
|
+
elif event.get("type") == "result":
|
|
588
|
+
# Result event may have response field (override if present)
|
|
589
|
+
if "response" in event and event["response"]:
|
|
590
|
+
response_text = event["response"]
|
|
591
|
+
# Don't break - we've already collected messages
|
|
468
592
|
|
|
469
593
|
# Token usage from stats in result event
|
|
470
594
|
tokens = None
|
|
@@ -481,6 +605,16 @@ class HeadlessSpawner:
|
|
|
481
605
|
tokens = total_tokens if total_tokens > 0 else None
|
|
482
606
|
break
|
|
483
607
|
|
|
608
|
+
# Publish live event: complete
|
|
609
|
+
duration = time.time() - start_time
|
|
610
|
+
self._publish_live_event(
|
|
611
|
+
"spawner_complete",
|
|
612
|
+
"gemini",
|
|
613
|
+
success=True,
|
|
614
|
+
duration=duration,
|
|
615
|
+
response=response_text,
|
|
616
|
+
tokens=tokens,
|
|
617
|
+
)
|
|
484
618
|
return AIResult(
|
|
485
619
|
success=True,
|
|
486
620
|
response=response_text,
|
|
@@ -498,6 +632,14 @@ class HeadlessSpawner:
|
|
|
498
632
|
try:
|
|
499
633
|
output = json.loads(result.stdout)
|
|
500
634
|
except json.JSONDecodeError as e:
|
|
635
|
+
duration = time.time() - start_time
|
|
636
|
+
self._publish_live_event(
|
|
637
|
+
"spawner_complete",
|
|
638
|
+
"gemini",
|
|
639
|
+
success=False,
|
|
640
|
+
duration=duration,
|
|
641
|
+
error=f"Failed to parse JSON: {e}",
|
|
642
|
+
)
|
|
501
643
|
return AIResult(
|
|
502
644
|
success=False,
|
|
503
645
|
response="",
|
|
@@ -521,6 +663,16 @@ class HeadlessSpawner:
|
|
|
521
663
|
total_tokens += model_tokens
|
|
522
664
|
tokens = total_tokens if total_tokens > 0 else None
|
|
523
665
|
|
|
666
|
+
# Publish live event: complete
|
|
667
|
+
duration = time.time() - start_time
|
|
668
|
+
self._publish_live_event(
|
|
669
|
+
"spawner_complete",
|
|
670
|
+
"gemini",
|
|
671
|
+
success=True,
|
|
672
|
+
duration=duration,
|
|
673
|
+
response=response_text,
|
|
674
|
+
tokens=tokens,
|
|
675
|
+
)
|
|
524
676
|
return AIResult(
|
|
525
677
|
success=True,
|
|
526
678
|
response=response_text,
|
|
@@ -531,6 +683,14 @@ class HeadlessSpawner:
|
|
|
531
683
|
)
|
|
532
684
|
|
|
533
685
|
except subprocess.TimeoutExpired as e:
|
|
686
|
+
duration = time.time() - start_time
|
|
687
|
+
self._publish_live_event(
|
|
688
|
+
"spawner_complete",
|
|
689
|
+
"gemini",
|
|
690
|
+
success=False,
|
|
691
|
+
duration=duration,
|
|
692
|
+
error=f"Timed out after {timeout} seconds",
|
|
693
|
+
)
|
|
534
694
|
return AIResult(
|
|
535
695
|
success=False,
|
|
536
696
|
response="",
|
|
@@ -545,6 +705,14 @@ class HeadlessSpawner:
|
|
|
545
705
|
tracked_events=tracked_events,
|
|
546
706
|
)
|
|
547
707
|
except FileNotFoundError:
|
|
708
|
+
duration = time.time() - start_time
|
|
709
|
+
self._publish_live_event(
|
|
710
|
+
"spawner_complete",
|
|
711
|
+
"gemini",
|
|
712
|
+
success=False,
|
|
713
|
+
duration=duration,
|
|
714
|
+
error="CLI not found",
|
|
715
|
+
)
|
|
548
716
|
return AIResult(
|
|
549
717
|
success=False,
|
|
550
718
|
response="",
|
|
@@ -554,6 +722,14 @@ class HeadlessSpawner:
|
|
|
554
722
|
tracked_events=tracked_events,
|
|
555
723
|
)
|
|
556
724
|
except Exception as e:
|
|
725
|
+
duration = time.time() - start_time
|
|
726
|
+
self._publish_live_event(
|
|
727
|
+
"spawner_complete",
|
|
728
|
+
"gemini",
|
|
729
|
+
success=False,
|
|
730
|
+
duration=duration,
|
|
731
|
+
error=str(e),
|
|
732
|
+
)
|
|
557
733
|
return AIResult(
|
|
558
734
|
success=False,
|
|
559
735
|
response="",
|
|
@@ -585,17 +761,17 @@ class HeadlessSpawner:
|
|
|
585
761
|
|
|
586
762
|
Args:
|
|
587
763
|
prompt: Task description for Codex
|
|
588
|
-
output_json:
|
|
764
|
+
output_json: JSONL output flag (enables real-time tracking)
|
|
589
765
|
model: Model selection (e.g., "gpt-4-turbo"). Default: None
|
|
590
|
-
sandbox: Sandbox mode ("read-only", "workspace-write",
|
|
591
|
-
full_auto: Enable full auto mode
|
|
766
|
+
sandbox: Sandbox mode ("read-only", "workspace-write", or full)
|
|
767
|
+
full_auto: Enable full auto mode. Default: True (required headless)
|
|
592
768
|
images: List of image paths (--image). Default: None
|
|
593
|
-
output_last_message: Write last message to file
|
|
594
|
-
output_schema: JSON schema for validation
|
|
595
|
-
skip_git_check: Skip git repo check
|
|
769
|
+
output_last_message: Write last message to file. Default: None
|
|
770
|
+
output_schema: JSON schema for validation. Default: None
|
|
771
|
+
skip_git_check: Skip git repo check. Default: False
|
|
596
772
|
working_directory: Workspace directory (--cd). Default: None
|
|
597
773
|
use_oss: Use local Ollama provider (--oss). Default: False
|
|
598
|
-
bypass_approvals:
|
|
774
|
+
bypass_approvals: Bypass approval checks. Default: False
|
|
599
775
|
track_in_htmlgraph: Enable HtmlGraph activity tracking. Default: True
|
|
600
776
|
timeout: Max seconds to wait
|
|
601
777
|
|
|
@@ -608,6 +784,15 @@ class HeadlessSpawner:
|
|
|
608
784
|
if track_in_htmlgraph and output_json:
|
|
609
785
|
sdk = self._get_sdk()
|
|
610
786
|
|
|
787
|
+
# Publish live event: spawner starting
|
|
788
|
+
self._publish_live_event(
|
|
789
|
+
"spawner_start",
|
|
790
|
+
"codex",
|
|
791
|
+
prompt=prompt,
|
|
792
|
+
model=model,
|
|
793
|
+
)
|
|
794
|
+
start_time = time.time()
|
|
795
|
+
|
|
611
796
|
cmd = ["codex", "exec"]
|
|
612
797
|
|
|
613
798
|
if output_json:
|
|
@@ -674,6 +859,14 @@ class HeadlessSpawner:
|
|
|
674
859
|
pass
|
|
675
860
|
|
|
676
861
|
try:
|
|
862
|
+
# Publish live event: executing
|
|
863
|
+
self._publish_live_event(
|
|
864
|
+
"spawner_phase",
|
|
865
|
+
"codex",
|
|
866
|
+
phase="executing",
|
|
867
|
+
details="Running Codex CLI",
|
|
868
|
+
)
|
|
869
|
+
|
|
677
870
|
result = subprocess.run(
|
|
678
871
|
cmd,
|
|
679
872
|
stdout=subprocess.PIPE,
|
|
@@ -682,13 +875,31 @@ class HeadlessSpawner:
|
|
|
682
875
|
timeout=timeout,
|
|
683
876
|
)
|
|
684
877
|
|
|
878
|
+
# Publish live event: processing
|
|
879
|
+
self._publish_live_event(
|
|
880
|
+
"spawner_phase",
|
|
881
|
+
"codex",
|
|
882
|
+
phase="processing",
|
|
883
|
+
details="Parsing Codex response",
|
|
884
|
+
)
|
|
885
|
+
|
|
685
886
|
if not output_json:
|
|
686
887
|
# Plain text mode - return as-is
|
|
888
|
+
duration = time.time() - start_time
|
|
889
|
+
success = result.returncode == 0
|
|
890
|
+
self._publish_live_event(
|
|
891
|
+
"spawner_complete",
|
|
892
|
+
"codex",
|
|
893
|
+
success=success,
|
|
894
|
+
duration=duration,
|
|
895
|
+
response=result.stdout.strip()[:200] if success else None,
|
|
896
|
+
error="Command failed" if not success else None,
|
|
897
|
+
)
|
|
687
898
|
return AIResult(
|
|
688
|
-
success=
|
|
899
|
+
success=success,
|
|
689
900
|
response=result.stdout.strip(),
|
|
690
901
|
tokens_used=None,
|
|
691
|
-
error=None if
|
|
902
|
+
error=None if success else "Command failed",
|
|
692
903
|
raw_output=result.stdout,
|
|
693
904
|
tracked_events=tracked_events,
|
|
694
905
|
)
|
|
@@ -735,11 +946,23 @@ class HeadlessSpawner:
|
|
|
735
946
|
# Sum all token types
|
|
736
947
|
tokens = sum(usage.values())
|
|
737
948
|
|
|
949
|
+
# Publish live event: complete
|
|
950
|
+
duration = time.time() - start_time
|
|
951
|
+
success = result.returncode == 0
|
|
952
|
+
self._publish_live_event(
|
|
953
|
+
"spawner_complete",
|
|
954
|
+
"codex",
|
|
955
|
+
success=success,
|
|
956
|
+
duration=duration,
|
|
957
|
+
response=response[:200] if response else None,
|
|
958
|
+
tokens=tokens,
|
|
959
|
+
error="Command failed" if not success else None,
|
|
960
|
+
)
|
|
738
961
|
return AIResult(
|
|
739
|
-
success=
|
|
962
|
+
success=success,
|
|
740
963
|
response=response or "",
|
|
741
964
|
tokens_used=tokens,
|
|
742
|
-
error=None if
|
|
965
|
+
error=None if success else "Command failed",
|
|
743
966
|
raw_output={
|
|
744
967
|
"events": events,
|
|
745
968
|
"parse_errors": parse_errors if parse_errors else None,
|
|
@@ -748,6 +971,14 @@ class HeadlessSpawner:
|
|
|
748
971
|
)
|
|
749
972
|
|
|
750
973
|
except FileNotFoundError:
|
|
974
|
+
duration = time.time() - start_time
|
|
975
|
+
self._publish_live_event(
|
|
976
|
+
"spawner_complete",
|
|
977
|
+
"codex",
|
|
978
|
+
success=False,
|
|
979
|
+
duration=duration,
|
|
980
|
+
error="CLI not found",
|
|
981
|
+
)
|
|
751
982
|
return AIResult(
|
|
752
983
|
success=False,
|
|
753
984
|
response="",
|
|
@@ -757,6 +988,14 @@ class HeadlessSpawner:
|
|
|
757
988
|
tracked_events=tracked_events,
|
|
758
989
|
)
|
|
759
990
|
except subprocess.TimeoutExpired as e:
|
|
991
|
+
duration = time.time() - start_time
|
|
992
|
+
self._publish_live_event(
|
|
993
|
+
"spawner_complete",
|
|
994
|
+
"codex",
|
|
995
|
+
success=False,
|
|
996
|
+
duration=duration,
|
|
997
|
+
error=f"Timed out after {timeout} seconds",
|
|
998
|
+
)
|
|
760
999
|
return AIResult(
|
|
761
1000
|
success=False,
|
|
762
1001
|
response="",
|
|
@@ -771,6 +1010,14 @@ class HeadlessSpawner:
|
|
|
771
1010
|
tracked_events=tracked_events,
|
|
772
1011
|
)
|
|
773
1012
|
except Exception as e:
|
|
1013
|
+
duration = time.time() - start_time
|
|
1014
|
+
self._publish_live_event(
|
|
1015
|
+
"spawner_complete",
|
|
1016
|
+
"codex",
|
|
1017
|
+
success=False,
|
|
1018
|
+
duration=duration,
|
|
1019
|
+
error=str(e),
|
|
1020
|
+
)
|
|
774
1021
|
return AIResult(
|
|
775
1022
|
success=False,
|
|
776
1023
|
response="",
|
|
@@ -794,9 +1041,9 @@ class HeadlessSpawner:
|
|
|
794
1041
|
|
|
795
1042
|
Args:
|
|
796
1043
|
prompt: Task description for Copilot
|
|
797
|
-
allow_tools:
|
|
798
|
-
allow_all_tools: Auto-approve all tools
|
|
799
|
-
deny_tools:
|
|
1044
|
+
allow_tools: Tools to auto-approve (e.g., ["shell(git)"])
|
|
1045
|
+
allow_all_tools: Auto-approve all tools. Default: False
|
|
1046
|
+
deny_tools: Tools to deny (--deny-tool). Default: None
|
|
800
1047
|
track_in_htmlgraph: Enable HtmlGraph activity tracking. Default: True
|
|
801
1048
|
timeout: Max seconds to wait
|
|
802
1049
|
|
|
@@ -809,6 +1056,14 @@ class HeadlessSpawner:
|
|
|
809
1056
|
if track_in_htmlgraph:
|
|
810
1057
|
sdk = self._get_sdk()
|
|
811
1058
|
|
|
1059
|
+
# Publish live event: spawner starting
|
|
1060
|
+
self._publish_live_event(
|
|
1061
|
+
"spawner_start",
|
|
1062
|
+
"copilot",
|
|
1063
|
+
prompt=prompt,
|
|
1064
|
+
)
|
|
1065
|
+
start_time = time.time()
|
|
1066
|
+
|
|
812
1067
|
cmd = ["copilot", "-p", prompt]
|
|
813
1068
|
|
|
814
1069
|
# Add allow all tools flag
|
|
@@ -838,6 +1093,14 @@ class HeadlessSpawner:
|
|
|
838
1093
|
pass
|
|
839
1094
|
|
|
840
1095
|
try:
|
|
1096
|
+
# Publish live event: executing
|
|
1097
|
+
self._publish_live_event(
|
|
1098
|
+
"spawner_phase",
|
|
1099
|
+
"copilot",
|
|
1100
|
+
phase="executing",
|
|
1101
|
+
details="Running Copilot CLI",
|
|
1102
|
+
)
|
|
1103
|
+
|
|
841
1104
|
result = subprocess.run(
|
|
842
1105
|
cmd,
|
|
843
1106
|
capture_output=True,
|
|
@@ -845,6 +1108,14 @@ class HeadlessSpawner:
|
|
|
845
1108
|
timeout=timeout,
|
|
846
1109
|
)
|
|
847
1110
|
|
|
1111
|
+
# Publish live event: processing
|
|
1112
|
+
self._publish_live_event(
|
|
1113
|
+
"spawner_phase",
|
|
1114
|
+
"copilot",
|
|
1115
|
+
phase="processing",
|
|
1116
|
+
details="Parsing Copilot response",
|
|
1117
|
+
)
|
|
1118
|
+
|
|
848
1119
|
# Parse output: response is before stats block
|
|
849
1120
|
lines = result.stdout.split("\n")
|
|
850
1121
|
|
|
@@ -874,16 +1145,36 @@ class HeadlessSpawner:
|
|
|
874
1145
|
prompt, response, sdk
|
|
875
1146
|
)
|
|
876
1147
|
|
|
1148
|
+
# Publish live event: complete
|
|
1149
|
+
duration = time.time() - start_time
|
|
1150
|
+
success = result.returncode == 0
|
|
1151
|
+
self._publish_live_event(
|
|
1152
|
+
"spawner_complete",
|
|
1153
|
+
"copilot",
|
|
1154
|
+
success=success,
|
|
1155
|
+
duration=duration,
|
|
1156
|
+
response=response[:200] if response else None,
|
|
1157
|
+
tokens=tokens,
|
|
1158
|
+
error=result.stderr if not success else None,
|
|
1159
|
+
)
|
|
877
1160
|
return AIResult(
|
|
878
|
-
success=
|
|
1161
|
+
success=success,
|
|
879
1162
|
response=response,
|
|
880
1163
|
tokens_used=tokens,
|
|
881
|
-
error=None if
|
|
1164
|
+
error=None if success else result.stderr,
|
|
882
1165
|
raw_output=result.stdout,
|
|
883
1166
|
tracked_events=tracked_events,
|
|
884
1167
|
)
|
|
885
1168
|
|
|
886
1169
|
except FileNotFoundError:
|
|
1170
|
+
duration = time.time() - start_time
|
|
1171
|
+
self._publish_live_event(
|
|
1172
|
+
"spawner_complete",
|
|
1173
|
+
"copilot",
|
|
1174
|
+
success=False,
|
|
1175
|
+
duration=duration,
|
|
1176
|
+
error="CLI not found",
|
|
1177
|
+
)
|
|
887
1178
|
return AIResult(
|
|
888
1179
|
success=False,
|
|
889
1180
|
response="",
|
|
@@ -893,6 +1184,14 @@ class HeadlessSpawner:
|
|
|
893
1184
|
tracked_events=tracked_events,
|
|
894
1185
|
)
|
|
895
1186
|
except subprocess.TimeoutExpired as e:
|
|
1187
|
+
duration = time.time() - start_time
|
|
1188
|
+
self._publish_live_event(
|
|
1189
|
+
"spawner_complete",
|
|
1190
|
+
"copilot",
|
|
1191
|
+
success=False,
|
|
1192
|
+
duration=duration,
|
|
1193
|
+
error=f"Timed out after {timeout} seconds",
|
|
1194
|
+
)
|
|
896
1195
|
return AIResult(
|
|
897
1196
|
success=False,
|
|
898
1197
|
response="",
|
|
@@ -907,6 +1206,14 @@ class HeadlessSpawner:
|
|
|
907
1206
|
tracked_events=tracked_events,
|
|
908
1207
|
)
|
|
909
1208
|
except Exception as e:
|
|
1209
|
+
duration = time.time() - start_time
|
|
1210
|
+
self._publish_live_event(
|
|
1211
|
+
"spawner_complete",
|
|
1212
|
+
"copilot",
|
|
1213
|
+
success=False,
|
|
1214
|
+
duration=duration,
|
|
1215
|
+
error=str(e),
|
|
1216
|
+
)
|
|
910
1217
|
return AIResult(
|
|
911
1218
|
success=False,
|
|
912
1219
|
response="",
|
|
@@ -924,6 +1231,7 @@ class HeadlessSpawner:
|
|
|
924
1231
|
resume: str | None = None,
|
|
925
1232
|
verbose: bool = False,
|
|
926
1233
|
timeout: int = 300,
|
|
1234
|
+
extra_args: list[str] | None = None,
|
|
927
1235
|
) -> AIResult:
|
|
928
1236
|
"""
|
|
929
1237
|
Spawn Claude in headless mode.
|
|
@@ -948,6 +1256,7 @@ class HeadlessSpawner:
|
|
|
948
1256
|
resume: Resume from previous session (--resume). Default: None
|
|
949
1257
|
verbose: Enable verbose output (--verbose). Default: False
|
|
950
1258
|
timeout: Max seconds (default: 300, Claude can be slow with initialization)
|
|
1259
|
+
extra_args: Additional arguments to pass to Claude CLI
|
|
951
1260
|
|
|
952
1261
|
Returns:
|
|
953
1262
|
AIResult with response or error
|
|
@@ -975,6 +1284,12 @@ class HeadlessSpawner:
|
|
|
975
1284
|
if verbose:
|
|
976
1285
|
cmd.append("--verbose")
|
|
977
1286
|
|
|
1287
|
+
# Add extra args
|
|
1288
|
+
if extra_args:
|
|
1289
|
+
cmd.extend(extra_args)
|
|
1290
|
+
|
|
1291
|
+
# Use -- separator to ensure prompt isn't consumed by variadic args
|
|
1292
|
+
cmd.append("--")
|
|
978
1293
|
cmd.append(prompt)
|
|
979
1294
|
|
|
980
1295
|
try:
|