htmlgraph 0.26.4__py3-none-any.whl → 0.26.6__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.
Files changed (69) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +1 -1
  2. htmlgraph/__init__.py +1 -1
  3. htmlgraph/api/main.py +50 -10
  4. htmlgraph/api/templates/dashboard-redesign.html +608 -54
  5. htmlgraph/api/templates/partials/activity-feed.html +21 -0
  6. htmlgraph/api/templates/partials/features.html +81 -12
  7. htmlgraph/api/templates/partials/orchestration.html +35 -0
  8. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  9. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  10. htmlgraph/cli/__init__.py +42 -0
  11. htmlgraph/cli/__main__.py +6 -0
  12. htmlgraph/cli/analytics.py +939 -0
  13. htmlgraph/cli/base.py +660 -0
  14. htmlgraph/cli/constants.py +206 -0
  15. htmlgraph/cli/core.py +856 -0
  16. htmlgraph/cli/main.py +143 -0
  17. htmlgraph/cli/models.py +462 -0
  18. htmlgraph/cli/templates/__init__.py +1 -0
  19. htmlgraph/cli/templates/cost_dashboard.py +398 -0
  20. htmlgraph/cli/work/__init__.py +159 -0
  21. htmlgraph/cli/work/features.py +567 -0
  22. htmlgraph/cli/work/orchestration.py +675 -0
  23. htmlgraph/cli/work/sessions.py +465 -0
  24. htmlgraph/cli/work/tracks.py +485 -0
  25. htmlgraph/dashboard.html +6414 -634
  26. htmlgraph/db/schema.py +8 -3
  27. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +20 -13
  28. htmlgraph/docs/README.md +2 -3
  29. htmlgraph/hooks/event_tracker.py +189 -35
  30. htmlgraph/hooks/git_commands.py +175 -0
  31. htmlgraph/hooks/orchestrator.py +137 -71
  32. htmlgraph/hooks/orchestrator_reflector.py +23 -0
  33. htmlgraph/hooks/pretooluse.py +29 -6
  34. htmlgraph/hooks/session_handler.py +28 -0
  35. htmlgraph/hooks/session_summary.py +391 -0
  36. htmlgraph/hooks/subagent_detection.py +202 -0
  37. htmlgraph/hooks/validator.py +192 -79
  38. htmlgraph/operations/__init__.py +18 -0
  39. htmlgraph/operations/initialization.py +596 -0
  40. htmlgraph/operations/initialization.py.backup +228 -0
  41. htmlgraph/orchestration/__init__.py +16 -1
  42. htmlgraph/orchestration/claude_launcher.py +185 -0
  43. htmlgraph/orchestration/command_builder.py +71 -0
  44. htmlgraph/orchestration/headless_spawner.py +72 -1332
  45. htmlgraph/orchestration/plugin_manager.py +136 -0
  46. htmlgraph/orchestration/prompts.py +137 -0
  47. htmlgraph/orchestration/spawners/__init__.py +16 -0
  48. htmlgraph/orchestration/spawners/base.py +194 -0
  49. htmlgraph/orchestration/spawners/claude.py +170 -0
  50. htmlgraph/orchestration/spawners/codex.py +442 -0
  51. htmlgraph/orchestration/spawners/copilot.py +299 -0
  52. htmlgraph/orchestration/spawners/gemini.py +478 -0
  53. htmlgraph/orchestration/subprocess_runner.py +33 -0
  54. htmlgraph/orchestration.md +563 -0
  55. htmlgraph/orchestrator-system-prompt-optimized.txt +620 -55
  56. htmlgraph/orchestrator_config.py +357 -0
  57. htmlgraph/orchestrator_mode.py +45 -12
  58. htmlgraph/transcript.py +16 -4
  59. htmlgraph-0.26.6.data/data/htmlgraph/dashboard.html +6592 -0
  60. {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/METADATA +1 -1
  61. {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/RECORD +67 -33
  62. {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/entry_points.txt +1 -1
  63. htmlgraph/cli.py +0 -7256
  64. htmlgraph-0.26.4.data/data/htmlgraph/dashboard.html +0 -812
  65. {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/styles.css +0 -0
  66. {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  67. {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  68. {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  69. {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/WHEEL +0 -0
@@ -0,0 +1,442 @@
1
+ """Codex spawner implementation."""
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ import time
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from .base import AIResult, BaseSpawner
10
+
11
+ if TYPE_CHECKING:
12
+ from htmlgraph.sdk import SDK
13
+
14
+
15
+ class CodexSpawner(BaseSpawner):
16
+ """Spawner for OpenAI Codex CLI."""
17
+
18
+ def _parse_and_track_events(self, jsonl_output: str, sdk: "SDK") -> list[dict]:
19
+ """
20
+ Parse Codex JSONL events and track in HtmlGraph.
21
+
22
+ Args:
23
+ jsonl_output: JSONL output from Codex CLI
24
+ sdk: HtmlGraph SDK instance for tracking
25
+
26
+ Returns:
27
+ Parsed events list
28
+ """
29
+ events = []
30
+ parse_errors = []
31
+
32
+ for line_num, line in enumerate(jsonl_output.splitlines(), start=1):
33
+ if not line.strip():
34
+ continue
35
+
36
+ try:
37
+ event = json.loads(line)
38
+ events.append(event)
39
+
40
+ event_type = event.get("type")
41
+
42
+ # Track item.started events
43
+ if event_type == "item.started":
44
+ item = event.get("item", {})
45
+ item_type = item.get("type")
46
+
47
+ if item_type == "command_execution":
48
+ command = item.get("command", "")
49
+ self._track_activity(
50
+ sdk,
51
+ tool="codex_command",
52
+ summary=f"Codex executing: {command[:80]}",
53
+ payload={"command": command},
54
+ )
55
+
56
+ # Track item.completed events
57
+ elif event_type == "item.completed":
58
+ item = event.get("item", {})
59
+ item_type = item.get("type")
60
+
61
+ if item_type == "file_change":
62
+ path = item.get("path", "unknown")
63
+ self._track_activity(
64
+ sdk,
65
+ tool="codex_file_change",
66
+ summary=f"Codex modified: {path}",
67
+ file_paths=[path],
68
+ payload={"path": path},
69
+ )
70
+
71
+ elif item_type == "agent_message":
72
+ text = item.get("text", "")
73
+ summary = text[:100] + "..." if len(text) > 100 else text
74
+ self._track_activity(
75
+ sdk,
76
+ tool="codex_message",
77
+ summary=f"Codex: {summary}",
78
+ payload={"text_length": len(text)},
79
+ )
80
+
81
+ # Track turn.completed for token usage
82
+ elif event_type == "turn.completed":
83
+ usage = event.get("usage", {})
84
+ total_tokens = sum(usage.values())
85
+ self._track_activity(
86
+ sdk,
87
+ tool="codex_completion",
88
+ summary=f"Codex turn completed ({total_tokens} tokens)",
89
+ payload={"usage": usage},
90
+ )
91
+
92
+ except json.JSONDecodeError as e:
93
+ parse_errors.append(
94
+ {
95
+ "line_number": line_num,
96
+ "error": str(e),
97
+ "content": line[:100],
98
+ }
99
+ )
100
+ continue
101
+
102
+ return events
103
+
104
+ def spawn(
105
+ self,
106
+ prompt: str,
107
+ output_json: bool = True,
108
+ model: str | None = None,
109
+ sandbox: str | None = None,
110
+ full_auto: bool = True,
111
+ images: list[str] | None = None,
112
+ output_last_message: str | None = None,
113
+ output_schema: str | None = None,
114
+ skip_git_check: bool = False,
115
+ working_directory: str | None = None,
116
+ use_oss: bool = False,
117
+ bypass_approvals: bool = False,
118
+ track_in_htmlgraph: bool = True,
119
+ timeout: int = 120,
120
+ tracker: Any = None,
121
+ parent_event_id: str | None = None,
122
+ ) -> AIResult:
123
+ """
124
+ Spawn Codex in headless mode.
125
+
126
+ Args:
127
+ prompt: Task description for Codex
128
+ output_json: JSONL output flag (enables real-time tracking)
129
+ model: Model selection (e.g., "gpt-4-turbo"). Default: None
130
+ sandbox: Sandbox mode ("read-only", "workspace-write", or full)
131
+ full_auto: Enable full auto mode. Default: True (required headless)
132
+ images: List of image paths (--image). Default: None
133
+ output_last_message: Write last message to file. Default: None
134
+ output_schema: JSON schema for validation. Default: None
135
+ skip_git_check: Skip git repo check. Default: False
136
+ working_directory: Workspace directory (--cd). Default: None
137
+ use_oss: Use local Ollama provider (--oss). Default: False
138
+ bypass_approvals: Bypass approval checks. Default: False
139
+ track_in_htmlgraph: Enable HtmlGraph activity tracking. Default: True
140
+ timeout: Max seconds to wait
141
+ tracker: Optional SpawnerEventTracker for recording subprocess invocation
142
+ parent_event_id: Optional parent event ID for event hierarchy
143
+
144
+ Returns:
145
+ AIResult with response, error, and tracked events if tracking enabled
146
+ """
147
+ # Initialize tracking if enabled
148
+ sdk: SDK | None = None
149
+ tracked_events: list[dict] = []
150
+ if track_in_htmlgraph and output_json:
151
+ sdk = self._get_sdk()
152
+
153
+ # Publish live event: spawner starting
154
+ self._publish_live_event(
155
+ "spawner_start",
156
+ "codex",
157
+ prompt=prompt,
158
+ model=model,
159
+ )
160
+ start_time = time.time()
161
+
162
+ cmd = ["codex", "exec"]
163
+
164
+ if output_json:
165
+ cmd.append("--json")
166
+
167
+ # Add model if specified
168
+ if model:
169
+ cmd.extend(["--model", model])
170
+
171
+ # Add sandbox mode if specified
172
+ if sandbox:
173
+ cmd.extend(["--sandbox", sandbox])
174
+
175
+ # Add full auto flag
176
+ if full_auto:
177
+ cmd.append("--full-auto")
178
+
179
+ # Add images
180
+ if images:
181
+ for image in images:
182
+ cmd.extend(["--image", image])
183
+
184
+ # Add output last message file if specified
185
+ if output_last_message:
186
+ cmd.extend(["--output-last-message", output_last_message])
187
+
188
+ # Add output schema if specified
189
+ if output_schema:
190
+ cmd.extend(["--output-schema", output_schema])
191
+
192
+ # Add skip git check flag
193
+ if skip_git_check:
194
+ cmd.append("--skip-git-repo-check")
195
+
196
+ # Add working directory if specified
197
+ if working_directory:
198
+ cmd.extend(["--cd", working_directory])
199
+
200
+ # Add OSS flag
201
+ if use_oss:
202
+ cmd.append("--oss")
203
+
204
+ # Add bypass approvals flag
205
+ if bypass_approvals:
206
+ cmd.append("--dangerously-bypass-approvals-and-sandbox")
207
+
208
+ # Add prompt as final argument
209
+ cmd.append(prompt)
210
+
211
+ # Track spawner start if SDK available
212
+ if sdk:
213
+ self._track_activity(
214
+ sdk,
215
+ tool="codex_spawn_start",
216
+ summary=f"Spawning Codex: {prompt[:80]}",
217
+ payload={
218
+ "prompt_length": len(prompt),
219
+ "model": model,
220
+ "sandbox": sandbox,
221
+ },
222
+ )
223
+
224
+ try:
225
+ # Publish live event: executing
226
+ self._publish_live_event(
227
+ "spawner_phase",
228
+ "codex",
229
+ phase="executing",
230
+ details="Running Codex CLI",
231
+ )
232
+
233
+ # Record subprocess invocation if tracker is available
234
+ subprocess_event_id = None
235
+ print(
236
+ f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}",
237
+ file=sys.stderr,
238
+ )
239
+ if tracker and parent_event_id:
240
+ print(
241
+ "DEBUG: Recording subprocess invocation for Codex...",
242
+ file=sys.stderr,
243
+ )
244
+ try:
245
+ subprocess_event = tracker.record_tool_call(
246
+ tool_name="subprocess.codex",
247
+ tool_input={"cmd": cmd},
248
+ phase_event_id=parent_event_id,
249
+ spawned_agent="gpt-4",
250
+ )
251
+ if subprocess_event:
252
+ subprocess_event_id = subprocess_event.get("event_id")
253
+ print(
254
+ f"DEBUG: Subprocess event created for Codex: {subprocess_event_id}",
255
+ file=sys.stderr,
256
+ )
257
+ else:
258
+ print("DEBUG: subprocess_event was None", file=sys.stderr)
259
+ except Exception as e:
260
+ # Tracking failure should not break execution
261
+ print(
262
+ f"DEBUG: Exception recording Codex subprocess: {e}",
263
+ file=sys.stderr,
264
+ )
265
+ pass
266
+ else:
267
+ print(
268
+ f"DEBUG: Skipping Codex subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}",
269
+ file=sys.stderr,
270
+ )
271
+
272
+ result = subprocess.run(
273
+ cmd,
274
+ stdout=subprocess.PIPE,
275
+ stderr=subprocess.DEVNULL,
276
+ text=True,
277
+ timeout=timeout,
278
+ )
279
+
280
+ # Complete subprocess invocation tracking
281
+ if tracker and subprocess_event_id:
282
+ try:
283
+ tracker.complete_tool_call(
284
+ event_id=subprocess_event_id,
285
+ output_summary=result.stdout[:500] if result.stdout else "",
286
+ success=result.returncode == 0,
287
+ )
288
+ except Exception:
289
+ # Tracking failure should not break execution
290
+ pass
291
+
292
+ # Publish live event: processing
293
+ self._publish_live_event(
294
+ "spawner_phase",
295
+ "codex",
296
+ phase="processing",
297
+ details="Parsing Codex response",
298
+ )
299
+
300
+ if not output_json:
301
+ # Plain text mode - return as-is
302
+ duration = time.time() - start_time
303
+ success = result.returncode == 0
304
+ self._publish_live_event(
305
+ "spawner_complete",
306
+ "codex",
307
+ success=success,
308
+ duration=duration,
309
+ response=result.stdout.strip()[:200] if success else None,
310
+ error="Command failed" if not success else None,
311
+ )
312
+ return AIResult(
313
+ success=success,
314
+ response=result.stdout.strip(),
315
+ tokens_used=None,
316
+ error=None if success else "Command failed",
317
+ raw_output=result.stdout,
318
+ tracked_events=tracked_events,
319
+ )
320
+
321
+ # Parse JSONL output
322
+ events = []
323
+ parse_errors = []
324
+
325
+ # Use tracking parser if SDK is available
326
+ if sdk:
327
+ tracked_events = self._parse_and_track_events(result.stdout, sdk)
328
+ events = tracked_events
329
+ else:
330
+ # Fallback to regular parsing without tracking
331
+ for line_num, line in enumerate(result.stdout.splitlines(), start=1):
332
+ if line.strip():
333
+ try:
334
+ events.append(json.loads(line))
335
+ except json.JSONDecodeError as e:
336
+ parse_errors.append(
337
+ {
338
+ "line_number": line_num,
339
+ "error": str(e),
340
+ "content": line[
341
+ :100
342
+ ], # First 100 chars for debugging
343
+ }
344
+ )
345
+ continue
346
+
347
+ # Extract agent message
348
+ response = None
349
+ for event in events:
350
+ if event.get("type") == "item.completed":
351
+ item = event.get("item", {})
352
+ if item.get("type") == "agent_message":
353
+ response = item.get("text")
354
+
355
+ # Extract token usage from turn.completed event
356
+ tokens = None
357
+ for event in events:
358
+ if event.get("type") == "turn.completed":
359
+ usage = event.get("usage", {})
360
+ # Sum all token types
361
+ tokens = sum(usage.values())
362
+
363
+ # Publish live event: complete
364
+ duration = time.time() - start_time
365
+ success = result.returncode == 0
366
+ self._publish_live_event(
367
+ "spawner_complete",
368
+ "codex",
369
+ success=success,
370
+ duration=duration,
371
+ response=response[:200] if response else None,
372
+ tokens=tokens,
373
+ error="Command failed" if not success else None,
374
+ )
375
+ return AIResult(
376
+ success=success,
377
+ response=response or "",
378
+ tokens_used=tokens,
379
+ error=None if success else "Command failed",
380
+ raw_output={
381
+ "events": events,
382
+ "parse_errors": parse_errors if parse_errors else None,
383
+ },
384
+ tracked_events=tracked_events,
385
+ )
386
+
387
+ except FileNotFoundError:
388
+ duration = time.time() - start_time
389
+ self._publish_live_event(
390
+ "spawner_complete",
391
+ "codex",
392
+ success=False,
393
+ duration=duration,
394
+ error="CLI not found",
395
+ )
396
+ return AIResult(
397
+ success=False,
398
+ response="",
399
+ tokens_used=None,
400
+ error="Codex CLI not found. Install from: https://github.com/openai/codex",
401
+ raw_output=None,
402
+ tracked_events=tracked_events,
403
+ )
404
+ except subprocess.TimeoutExpired as e:
405
+ duration = time.time() - start_time
406
+ self._publish_live_event(
407
+ "spawner_complete",
408
+ "codex",
409
+ success=False,
410
+ duration=duration,
411
+ error=f"Timed out after {timeout} seconds",
412
+ )
413
+ return AIResult(
414
+ success=False,
415
+ response="",
416
+ tokens_used=None,
417
+ error=f"Timed out after {timeout} seconds",
418
+ raw_output={
419
+ "partial_stdout": e.stdout.decode() if e.stdout else None,
420
+ "partial_stderr": e.stderr.decode() if e.stderr else None,
421
+ }
422
+ if e.stdout or e.stderr
423
+ else None,
424
+ tracked_events=tracked_events,
425
+ )
426
+ except Exception as e:
427
+ duration = time.time() - start_time
428
+ self._publish_live_event(
429
+ "spawner_complete",
430
+ "codex",
431
+ success=False,
432
+ duration=duration,
433
+ error=str(e),
434
+ )
435
+ return AIResult(
436
+ success=False,
437
+ response="",
438
+ tokens_used=None,
439
+ error=f"Unexpected error: {type(e).__name__}: {e}",
440
+ raw_output=None,
441
+ tracked_events=tracked_events,
442
+ )