htmlgraph 0.26.24__py3-none-any.whl → 0.27.0__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 +23 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/cli.py +3 -3
- htmlgraph/analytics/cost_analyzer.py +5 -1
- htmlgraph/analytics/cross_session.py +13 -9
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/work_type.py +15 -11
- htmlgraph/analytics_index.py +2 -1
- htmlgraph/api/main.py +114 -51
- htmlgraph/api/templates/dashboard-redesign.html +3 -3
- htmlgraph/api/templates/dashboard.html +3 -3
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/builders/base.py +2 -1
- htmlgraph/builders/bug.py +2 -1
- htmlgraph/builders/chore.py +2 -1
- htmlgraph/builders/epic.py +2 -1
- htmlgraph/builders/feature.py +2 -1
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +2 -1
- htmlgraph/builders/spike.py +2 -1
- htmlgraph/builders/track.py +28 -1
- htmlgraph/cli/analytics.py +2 -1
- htmlgraph/cli/base.py +33 -8
- htmlgraph/cli/core.py +2 -1
- htmlgraph/cli/main.py +2 -1
- htmlgraph/cli/models.py +2 -1
- htmlgraph/cli/templates/cost_dashboard.py +2 -1
- htmlgraph/cli/work/__init__.py +76 -1
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +2 -1
- htmlgraph/cli/work/orchestration.py +2 -1
- htmlgraph/cli/work/report.py +2 -1
- htmlgraph/cli/work/sessions.py +2 -1
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +2 -1
- htmlgraph/collections/base.py +43 -4
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +12 -7
- htmlgraph/collections/spike.py +6 -1
- htmlgraph/collections/task_delegation.py +7 -2
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +15 -10
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +11 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/edge_index.py +2 -1
- htmlgraph/event_log.py +81 -66
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +6 -2
- htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
- htmlgraph/hooks/drift_handler.py +3 -3
- htmlgraph/hooks/event_tracker.py +40 -61
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +92 -14
- htmlgraph/hooks/orchestrator_reflector.py +4 -0
- htmlgraph/hooks/post_tool_use_failure.py +7 -3
- htmlgraph/hooks/posttooluse.py +4 -0
- htmlgraph/hooks/prompt_analyzer.py +5 -5
- htmlgraph/hooks/session_handler.py +5 -2
- htmlgraph/hooks/session_summary.py +6 -2
- htmlgraph/hooks/validator.py +8 -4
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +2 -1
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +18 -1
- htmlgraph/operations/analytics.py +2 -1
- htmlgraph/operations/bootstrap.py +2 -1
- htmlgraph/operations/events.py +2 -1
- htmlgraph/operations/fastapi_server.py +2 -1
- htmlgraph/operations/hooks.py +2 -1
- htmlgraph/operations/initialization.py +2 -1
- htmlgraph/operations/server.py +2 -1
- htmlgraph/orchestration/__init__.py +4 -0
- htmlgraph/orchestration/claude_launcher.py +23 -20
- htmlgraph/orchestration/command_builder.py +2 -1
- htmlgraph/orchestration/headless_spawner.py +6 -2
- htmlgraph/orchestration/model_selection.py +7 -3
- htmlgraph/orchestration/plugin_manager.py +25 -21
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/claude.py +5 -2
- htmlgraph/orchestration/spawners/codex.py +12 -19
- htmlgraph/orchestration/spawners/copilot.py +13 -18
- htmlgraph/orchestration/spawners/gemini.py +12 -19
- htmlgraph/orchestration/subprocess_runner.py +6 -3
- htmlgraph/orchestration/task_coordination.py +16 -8
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/parallel.py +2 -1
- htmlgraph/query_builder.py +2 -1
- htmlgraph/reflection.py +2 -1
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +2 -1
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/server.py +21 -17
- htmlgraph/session_manager.py +1 -7
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/handoff.py +10 -3
- htmlgraph/system_prompts.py +2 -1
- htmlgraph/track_builder.py +14 -1
- htmlgraph/transcript.py +2 -1
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/METADATA +15 -1
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/RECORD +154 -117
- htmlgraph/sdk.py +0 -3430
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/WHEEL +0 -0
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/entry_points.txt +0 -0
htmlgraph/collections/feature.py
CHANGED
htmlgraph/collections/insight.py
CHANGED
htmlgraph/collections/metric.py
CHANGED
htmlgraph/collections/pattern.py
CHANGED
htmlgraph/collections/phase.py
CHANGED
htmlgraph/collections/session.py
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
1
7
|
"""
|
|
2
8
|
SessionCollection - Session state and management interface.
|
|
3
9
|
|
|
@@ -13,7 +19,6 @@ Integration with SessionStart hook:
|
|
|
13
19
|
sdk.sessions.setup_environment_variables(state)
|
|
14
20
|
"""
|
|
15
21
|
|
|
16
|
-
from __future__ import annotations
|
|
17
22
|
|
|
18
23
|
from typing import TYPE_CHECKING
|
|
19
24
|
|
|
@@ -82,9 +87,9 @@ class SessionCollection(BaseCollection):
|
|
|
82
87
|
Example:
|
|
83
88
|
>>> sdk = SDK()
|
|
84
89
|
>>> state = sdk.sessions.get_current_state()
|
|
85
|
-
>>>
|
|
86
|
-
>>>
|
|
87
|
-
>>>
|
|
90
|
+
>>> logger.info(f"Session: {state['session_id']}")
|
|
91
|
+
>>> logger.info(f"Post-compact: {state['is_post_compact']}")
|
|
92
|
+
>>> logger.info(f"Delegation enabled: {state['delegation_enabled']}")
|
|
88
93
|
"""
|
|
89
94
|
return self._state_manager.get_current_state()
|
|
90
95
|
|
|
@@ -116,8 +121,8 @@ class SessionCollection(BaseCollection):
|
|
|
116
121
|
>>> sdk = SDK()
|
|
117
122
|
>>> state = sdk.sessions.get_current_state()
|
|
118
123
|
>>> env_vars = sdk.sessions.setup_environment_variables(state)
|
|
119
|
-
>>>
|
|
120
|
-
>>>
|
|
124
|
+
>>> logger.info(f"CLAUDE_SESSION_ID: {env_vars['CLAUDE_SESSION_ID']}")
|
|
125
|
+
>>> logger.info(f"CLAUDE_DELEGATION_ENABLED: {env_vars['CLAUDE_DELEGATION_ENABLED']}")
|
|
121
126
|
"""
|
|
122
127
|
return self._state_manager.setup_environment_variables(
|
|
123
128
|
session_state=session_state, auto_detect_compact=auto_detect_compact
|
|
@@ -168,7 +173,7 @@ class SessionCollection(BaseCollection):
|
|
|
168
173
|
Example:
|
|
169
174
|
>>> sdk = SDK()
|
|
170
175
|
>>> if sdk.sessions.detect_compact_automatically():
|
|
171
|
-
...
|
|
176
|
+
... logger.info("This is a post-compact session")
|
|
172
177
|
"""
|
|
173
178
|
return self._state_manager.detect_compact_automatically()
|
|
174
179
|
|
htmlgraph/collections/spike.py
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
1
7
|
"""
|
|
2
8
|
Spike collection for managing investigation and research spikes.
|
|
3
9
|
|
|
4
10
|
Extends BaseCollection with spike-specific builder support.
|
|
5
11
|
"""
|
|
6
12
|
|
|
7
|
-
from __future__ import annotations
|
|
8
13
|
|
|
9
14
|
from datetime import datetime
|
|
10
15
|
from typing import TYPE_CHECKING
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
1
7
|
"""
|
|
2
8
|
Task delegation collection for tracking spawned agent work.
|
|
3
9
|
|
|
@@ -12,7 +18,6 @@ Captures observability data for Task() calls:
|
|
|
12
18
|
This data proves multi-agent orchestration works and enables dashboard attribution.
|
|
13
19
|
"""
|
|
14
20
|
|
|
15
|
-
from __future__ import annotations
|
|
16
21
|
|
|
17
22
|
from datetime import datetime
|
|
18
23
|
from typing import TYPE_CHECKING, Any
|
|
@@ -41,7 +46,7 @@ class TaskDelegationCollection(BaseCollection["TaskDelegationCollection"]):
|
|
|
41
46
|
>>> sdk = SDK(agent="orchestrator")
|
|
42
47
|
>>> delegations = sdk.task_delegations.where(agent_type="codex-spawner")
|
|
43
48
|
>>> for d in delegations:
|
|
44
|
-
...
|
|
49
|
+
... logger.info(f"{d.agent_type}: {d.task_description} ({d.duration_seconds}s)")
|
|
45
50
|
>>>
|
|
46
51
|
>>> # Get all delegations for a specific agent
|
|
47
52
|
>>> gemini_work = sdk.task_delegations.where(agent_type="gemini-spawner")
|
htmlgraph/collections/todo.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Todo collection for managing persistent todo items.
|
|
3
5
|
|
|
@@ -8,7 +10,6 @@ Unlike other collections, TodoCollection provides:
|
|
|
8
10
|
- Bulk sync from TodoWrite format
|
|
9
11
|
"""
|
|
10
12
|
|
|
11
|
-
from __future__ import annotations
|
|
12
13
|
|
|
13
14
|
from datetime import datetime
|
|
14
15
|
from typing import TYPE_CHECKING, Any
|
|
@@ -64,6 +65,18 @@ class TodoCollection:
|
|
|
64
65
|
|
|
65
66
|
# Cache for loaded todos (lazy-loaded)
|
|
66
67
|
self._todos: dict[str, Todo] | None = None
|
|
68
|
+
self._ref_manager: Any = None # Set by SDK during initialization
|
|
69
|
+
|
|
70
|
+
def set_ref_manager(self, ref_manager: Any) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Set the ref manager for this collection.
|
|
73
|
+
|
|
74
|
+
Called by SDK during initialization to enable short ref support.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
ref_manager: RefManager instance from SDK
|
|
78
|
+
"""
|
|
79
|
+
self._ref_manager = ref_manager
|
|
67
80
|
|
|
68
81
|
def _ensure_loaded(self) -> dict[str, Todo]:
|
|
69
82
|
"""Load todos from disk if not cached."""
|
htmlgraph/collections/traces.py
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
1
7
|
"""
|
|
2
8
|
Tool Execution Traces Collection
|
|
3
9
|
|
|
@@ -11,21 +17,20 @@ Example:
|
|
|
11
17
|
>>> # Get traces for current session
|
|
12
18
|
>>> traces = sdk.traces.get_traces(session_id="sess-abc123")
|
|
13
19
|
>>> for trace in traces:
|
|
14
|
-
...
|
|
20
|
+
... logger.info(f"{trace.tool_name}: {trace.duration_ms}ms")
|
|
15
21
|
>>>
|
|
16
22
|
>>> # Find slow tool calls
|
|
17
23
|
>>> slow = sdk.traces.get_slow_traces(threshold_ms=1000)
|
|
18
24
|
>>>
|
|
19
25
|
>>> # Get hierarchical view (parent-child relationships)
|
|
20
26
|
>>> tree = sdk.traces.get_trace_tree(trace_id="trace-xyz")
|
|
21
|
-
>>>
|
|
22
|
-
>>>
|
|
27
|
+
>>> logger.info(f"Root: {tree.root.tool_name}")
|
|
28
|
+
>>> logger.info(f"Children: {len(tree.children)}")
|
|
23
29
|
>>>
|
|
24
30
|
>>> # Get error traces for debugging
|
|
25
31
|
>>> errors = sdk.traces.get_error_traces(session_id="sess-abc123")
|
|
26
32
|
"""
|
|
27
33
|
|
|
28
|
-
from __future__ import annotations
|
|
29
34
|
|
|
30
35
|
from dataclasses import dataclass
|
|
31
36
|
from datetime import datetime, timezone
|
|
@@ -245,7 +250,7 @@ class TraceCollection:
|
|
|
245
250
|
|
|
246
251
|
return None
|
|
247
252
|
except Exception as e:
|
|
248
|
-
|
|
253
|
+
logger.info(f"Error getting trace {tool_use_id}: {e}")
|
|
249
254
|
return None
|
|
250
255
|
|
|
251
256
|
def get_traces(
|
|
@@ -302,7 +307,7 @@ class TraceCollection:
|
|
|
302
307
|
rows = cursor.fetchall()
|
|
303
308
|
return [self._row_to_trace(row) for row in rows]
|
|
304
309
|
except Exception as e:
|
|
305
|
-
|
|
310
|
+
logger.info(f"Error getting traces for session {session_id}: {e}")
|
|
306
311
|
return []
|
|
307
312
|
|
|
308
313
|
def get_traces_by_tool(self, tool_name: str, limit: int = 100) -> list[TraceRecord]:
|
|
@@ -337,7 +342,7 @@ class TraceCollection:
|
|
|
337
342
|
rows = cursor.fetchall()
|
|
338
343
|
return [self._row_to_trace(row) for row in rows]
|
|
339
344
|
except Exception as e:
|
|
340
|
-
|
|
345
|
+
logger.info(f"Error getting traces for tool {tool_name}: {e}")
|
|
341
346
|
return []
|
|
342
347
|
|
|
343
348
|
def get_trace_tree(self, trace_id: str) -> TraceTree | None:
|
|
@@ -403,7 +408,7 @@ class TraceCollection:
|
|
|
403
408
|
|
|
404
409
|
return build_tree(root)
|
|
405
410
|
except Exception as e:
|
|
406
|
-
|
|
411
|
+
logger.info(f"Error getting trace tree for {trace_id}: {e}")
|
|
407
412
|
return None
|
|
408
413
|
|
|
409
414
|
def get_slow_traces(self, threshold_ms: int, limit: int = 100) -> list[TraceRecord]:
|
|
@@ -440,7 +445,7 @@ class TraceCollection:
|
|
|
440
445
|
rows = cursor.fetchall()
|
|
441
446
|
return [self._row_to_trace(row) for row in rows]
|
|
442
447
|
except Exception as e:
|
|
443
|
-
|
|
448
|
+
logger.info(f"Error getting slow traces: {e}")
|
|
444
449
|
return []
|
|
445
450
|
|
|
446
451
|
def get_error_traces(self, session_id: str, limit: int = 100) -> list[TraceRecord]:
|
|
@@ -478,5 +483,5 @@ class TraceCollection:
|
|
|
478
483
|
rows = cursor.fetchall()
|
|
479
484
|
return [self._row_to_trace(row) for row in rows]
|
|
480
485
|
except Exception as e:
|
|
481
|
-
|
|
486
|
+
logger.info(f"Error getting error traces: {e}")
|
|
482
487
|
return []
|
htmlgraph/context_analytics.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Context Analytics for HtmlGraph
|
|
3
5
|
|
|
@@ -7,7 +9,6 @@ Provides hierarchical context usage tracking and analytics:
|
|
|
7
9
|
Enables drill-down analysis of where context was consumed.
|
|
8
10
|
"""
|
|
9
11
|
|
|
10
|
-
from __future__ import annotations
|
|
11
12
|
|
|
12
13
|
from dataclasses import dataclass, field
|
|
13
14
|
from typing import TYPE_CHECKING, Any
|
htmlgraph/converter.py
CHANGED
|
@@ -520,6 +520,17 @@ def html_to_session(filepath: Path | str) -> Session:
|
|
|
520
520
|
if blockers:
|
|
521
521
|
data["blockers"] = blockers
|
|
522
522
|
|
|
523
|
+
# Parse recommended context files
|
|
524
|
+
recommended_context = []
|
|
525
|
+
for li in parser.query(
|
|
526
|
+
"section[data-handoff] div[data-recommended-context] li"
|
|
527
|
+
):
|
|
528
|
+
file_path = li.to_text().strip()
|
|
529
|
+
if file_path:
|
|
530
|
+
recommended_context.append(file_path)
|
|
531
|
+
if recommended_context:
|
|
532
|
+
data["recommended_context"] = recommended_context
|
|
533
|
+
|
|
523
534
|
# Parse activity log
|
|
524
535
|
activity_log = []
|
|
525
536
|
for li in parser.query("section[data-activity-log] ol li"):
|
htmlgraph/dependency_models.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Data models for dependency analytics.
|
|
3
5
|
|
|
@@ -9,7 +11,6 @@ Provides Pydantic models for dependency-aware analytics results including:
|
|
|
9
11
|
- Work prioritization
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
|
-
from __future__ import annotations
|
|
13
14
|
|
|
14
15
|
from typing import Literal
|
|
15
16
|
|
htmlgraph/edge_index.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Edge Index for O(1) reverse edge lookups.
|
|
3
5
|
|
|
@@ -10,7 +12,6 @@ Without this index, finding incoming edges requires scanning all nodes
|
|
|
10
12
|
in the graph - O(V×E) complexity.
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
|
-
from __future__ import annotations
|
|
14
15
|
|
|
15
16
|
from collections import defaultdict
|
|
16
17
|
from collections.abc import Iterator
|
htmlgraph/event_log.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Event logging for HtmlGraph.
|
|
3
5
|
|
|
@@ -9,84 +11,94 @@ Design goals:
|
|
|
9
11
|
- Deterministic serialization for rebuildable analytics indexes
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
|
-
from __future__ import annotations
|
|
13
14
|
|
|
14
15
|
import json
|
|
15
|
-
from dataclasses import dataclass
|
|
16
16
|
from datetime import datetime
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import TYPE_CHECKING, Any
|
|
19
19
|
|
|
20
|
+
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
|
|
21
|
+
|
|
20
22
|
if TYPE_CHECKING:
|
|
21
23
|
pass
|
|
22
24
|
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
26
|
+
class EventRecord(BaseModel):
|
|
27
|
+
"""
|
|
28
|
+
Event record for HtmlGraph tracking.
|
|
29
|
+
|
|
30
|
+
Uses Pydantic for automatic validation and serialization.
|
|
31
|
+
Immutable via ConfigDict(frozen=True).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(frozen=True)
|
|
35
|
+
|
|
36
|
+
event_id: str = Field(..., min_length=1, description="Unique event identifier")
|
|
37
|
+
timestamp: datetime = Field(..., description="Event timestamp")
|
|
38
|
+
session_id: str = Field(..., min_length=1, description="Session identifier")
|
|
39
|
+
agent: str = Field(..., description="Agent name (e.g., 'claude', 'gemini')")
|
|
40
|
+
tool: str = Field(..., description="Tool used (e.g., 'Bash', 'Edit', 'Read')")
|
|
41
|
+
summary: str = Field(..., description="Human-readable event summary")
|
|
42
|
+
success: bool = Field(..., description="Whether the operation succeeded")
|
|
43
|
+
feature_id: str | None = Field(None, description="Associated feature ID")
|
|
44
|
+
drift_score: float | None = Field(None, description="Context drift score")
|
|
45
|
+
start_commit: str | None = Field(None, description="Starting git commit hash")
|
|
46
|
+
continued_from: str | None = Field(
|
|
47
|
+
None, description="Previous session ID if continued"
|
|
48
|
+
)
|
|
49
|
+
work_type: str | None = Field(None, description="WorkType enum value")
|
|
50
|
+
session_status: str | None = Field(None, description="Session status")
|
|
51
|
+
file_paths: list[str] | None = Field(None, description="Files involved in event")
|
|
52
|
+
payload: dict[str, Any] | None = Field(None, description="Additional event data")
|
|
53
|
+
parent_session_id: str | None = Field(
|
|
54
|
+
None, description="Parent session ID for subagents"
|
|
55
|
+
)
|
|
56
|
+
|
|
42
57
|
# Phase 1: Enhanced Event Data Schema for multi-AI delegation tracking
|
|
43
|
-
delegated_to_ai: str | None = (
|
|
44
|
-
None
|
|
58
|
+
delegated_to_ai: str | None = Field(
|
|
59
|
+
None, description="AI delegate: 'gemini', 'codex', 'copilot', 'claude', or None"
|
|
60
|
+
)
|
|
61
|
+
task_id: str | None = Field(
|
|
62
|
+
None, description="Unique task ID for parallel tracking"
|
|
63
|
+
)
|
|
64
|
+
task_status: str | None = Field(
|
|
65
|
+
None,
|
|
66
|
+
description="Task status: 'pending', 'running', 'completed', 'failed', 'timeout'",
|
|
67
|
+
)
|
|
68
|
+
model_selected: str | None = Field(
|
|
69
|
+
None, description="Specific model (e.g., 'gemini-2.0-flash')"
|
|
70
|
+
)
|
|
71
|
+
complexity_level: str | None = Field(
|
|
72
|
+
None, description="Complexity: 'low', 'medium', 'high', 'very-high'"
|
|
73
|
+
)
|
|
74
|
+
budget_mode: str | None = Field(
|
|
75
|
+
None, description="Budget mode: 'free', 'balanced', 'performance'"
|
|
45
76
|
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
None # "pending", "running", "completed", "failed", "timeout"
|
|
77
|
+
execution_duration_seconds: float | None = Field(
|
|
78
|
+
None, description="Delegation execution time"
|
|
49
79
|
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"continued_from": self.continued_from,
|
|
73
|
-
"session_status": self.session_status,
|
|
74
|
-
"file_paths": self.file_paths or [],
|
|
75
|
-
"payload": self.payload,
|
|
76
|
-
"parent_session_id": self.parent_session_id,
|
|
77
|
-
# Delegation fields
|
|
78
|
-
"delegated_to_ai": self.delegated_to_ai,
|
|
79
|
-
"task_id": self.task_id,
|
|
80
|
-
"task_status": self.task_status,
|
|
81
|
-
"model_selected": self.model_selected,
|
|
82
|
-
"complexity_level": self.complexity_level,
|
|
83
|
-
"budget_mode": self.budget_mode,
|
|
84
|
-
"execution_duration_seconds": self.execution_duration_seconds,
|
|
85
|
-
"tokens_estimated": self.tokens_estimated,
|
|
86
|
-
"tokens_actual": self.tokens_actual,
|
|
87
|
-
"cost_usd": self.cost_usd,
|
|
88
|
-
"task_findings": self.task_findings,
|
|
89
|
-
}
|
|
80
|
+
tokens_estimated: int | None = Field(None, description="Estimated token usage")
|
|
81
|
+
tokens_actual: int | None = Field(None, description="Actual token usage")
|
|
82
|
+
cost_usd: float | None = Field(None, description="Calculated cost in USD")
|
|
83
|
+
task_findings: str | None = Field(None, description="Results from delegated task")
|
|
84
|
+
|
|
85
|
+
@field_validator("event_id", "session_id")
|
|
86
|
+
@classmethod
|
|
87
|
+
def validate_non_empty_string(cls, v: str) -> str:
|
|
88
|
+
"""Ensure event_id and session_id are non-empty."""
|
|
89
|
+
if not v or not v.strip():
|
|
90
|
+
raise ValueError("Field must be a non-empty string")
|
|
91
|
+
return v
|
|
92
|
+
|
|
93
|
+
@field_serializer("timestamp")
|
|
94
|
+
def serialize_timestamp(self, timestamp: datetime) -> str:
|
|
95
|
+
"""Serialize timestamp to ISO format string."""
|
|
96
|
+
return timestamp.isoformat()
|
|
97
|
+
|
|
98
|
+
@field_serializer("file_paths")
|
|
99
|
+
def serialize_file_paths(self, file_paths: list[str] | None) -> list[str]:
|
|
100
|
+
"""Ensure file_paths is always a list (never None) in JSON output."""
|
|
101
|
+
return file_paths or []
|
|
90
102
|
|
|
91
103
|
|
|
92
104
|
class JsonlEventLog:
|
|
@@ -104,7 +116,10 @@ class JsonlEventLog:
|
|
|
104
116
|
|
|
105
117
|
def append(self, record: EventRecord) -> Path:
|
|
106
118
|
path = self.path_for_session(record.session_id)
|
|
107
|
-
line =
|
|
119
|
+
line = (
|
|
120
|
+
json.dumps(record.model_dump(mode="json"), ensure_ascii=False, default=str)
|
|
121
|
+
+ "\n"
|
|
122
|
+
)
|
|
108
123
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
109
124
|
|
|
110
125
|
# Best-effort dedupe: some producers (e.g. git hooks) may retry or be chained.
|
htmlgraph/event_migration.py
CHANGED
htmlgraph/file_watcher.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
1
5
|
"""
|
|
2
6
|
File watcher for automatic graph reloading.
|
|
3
7
|
|
|
@@ -67,7 +71,7 @@ class GraphFileHandler(FileSystemEventHandler):
|
|
|
67
71
|
|
|
68
72
|
def _trigger_reload(self) -> None:
|
|
69
73
|
"""Trigger a reload after debounce delay."""
|
|
70
|
-
|
|
74
|
+
logger.info(f"[FileWatcher] Reloading collection: {self.collection}")
|
|
71
75
|
self.reload_callback()
|
|
72
76
|
|
|
73
77
|
def _debounced_reload(self) -> None:
|
|
@@ -87,7 +91,7 @@ class GraphFileHandler(FileSystemEventHandler):
|
|
|
87
91
|
if not self._is_relevant_file(str(event.src_path)):
|
|
88
92
|
return
|
|
89
93
|
|
|
90
|
-
|
|
94
|
+
logger.debug(
|
|
91
95
|
f"[FileWatcher] {self.collection}: File created - {Path(str(event.src_path)).name}"
|
|
92
96
|
)
|
|
93
97
|
self._debounced_reload()
|
|
@@ -101,7 +105,7 @@ class GraphFileHandler(FileSystemEventHandler):
|
|
|
101
105
|
if not self._is_relevant_file(str(event.src_path)):
|
|
102
106
|
return
|
|
103
107
|
|
|
104
|
-
|
|
108
|
+
logger.debug(
|
|
105
109
|
f"[FileWatcher] {self.collection}: File modified - {Path(str(event.src_path)).name}"
|
|
106
110
|
)
|
|
107
111
|
self._debounced_reload()
|
|
@@ -115,7 +119,7 @@ class GraphFileHandler(FileSystemEventHandler):
|
|
|
115
119
|
if not self._is_relevant_file(str(event.src_path)):
|
|
116
120
|
return
|
|
117
121
|
|
|
118
|
-
|
|
122
|
+
logger.debug(
|
|
119
123
|
f"[FileWatcher] {self.collection}: File deleted - {Path(str(event.src_path)).name}"
|
|
120
124
|
)
|
|
121
125
|
self._debounced_reload()
|
|
@@ -146,7 +150,7 @@ class GraphWatcher:
|
|
|
146
150
|
|
|
147
151
|
def start(self) -> None:
|
|
148
152
|
"""Start watching for file changes."""
|
|
149
|
-
|
|
153
|
+
logger.info(
|
|
150
154
|
f"[FileWatcher] Starting file watcher for {len(self.collections)} collections..."
|
|
151
155
|
)
|
|
152
156
|
|
|
@@ -160,7 +164,7 @@ class GraphWatcher:
|
|
|
160
164
|
def reload() -> None:
|
|
161
165
|
graph = self.get_graph_callback(coll)
|
|
162
166
|
count = graph.reload()
|
|
163
|
-
|
|
167
|
+
logger.info(f"[FileWatcher] Reloaded {count} nodes in {coll}")
|
|
164
168
|
|
|
165
169
|
return reload
|
|
166
170
|
|
|
@@ -173,11 +177,11 @@ class GraphWatcher:
|
|
|
173
177
|
self.observer.schedule(handler, str(collection_dir), recursive=recursive)
|
|
174
178
|
|
|
175
179
|
self.observer.start()
|
|
176
|
-
|
|
180
|
+
logger.info(f"[FileWatcher] Watching {self.graph_dir} for changes...")
|
|
177
181
|
|
|
178
182
|
def stop(self) -> None:
|
|
179
183
|
"""Stop watching for file changes."""
|
|
180
|
-
|
|
184
|
+
logger.info("[FileWatcher] Stopping file watcher...")
|
|
181
185
|
self.observer.stop()
|
|
182
186
|
self.observer.join()
|
|
183
187
|
|
htmlgraph/find_api.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
BeautifulSoup-style Find API for HtmlGraph.
|
|
3
5
|
|
|
@@ -22,7 +24,6 @@ Example:
|
|
|
22
24
|
nodes = graph.find_all(properties__effort__gt=8)
|
|
23
25
|
"""
|
|
24
26
|
|
|
25
|
-
from __future__ import annotations
|
|
26
27
|
|
|
27
28
|
import re
|
|
28
29
|
from collections.abc import Callable
|
htmlgraph/git_events.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Git event logging for HtmlGraph.
|
|
3
5
|
|
|
@@ -10,7 +12,6 @@ Design goals:
|
|
|
10
12
|
- Analytics-friendly: schema compatible with EventRecord/AnalyticsIndex
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
|
-
from __future__ import annotations
|
|
14
15
|
|
|
15
16
|
import os
|
|
16
17
|
import re
|
|
@@ -277,7 +278,10 @@ def _append_event(
|
|
|
277
278
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
278
279
|
with p.open("a", encoding="utf-8") as f:
|
|
279
280
|
f.write(
|
|
280
|
-
json.dumps(
|
|
281
|
+
json.dumps(
|
|
282
|
+
record.model_dump(mode="json"), ensure_ascii=False, default=str
|
|
283
|
+
)
|
|
284
|
+
+ "\n"
|
|
281
285
|
)
|
|
282
286
|
return
|
|
283
287
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
1
5
|
"""
|
|
2
6
|
CIGS PreToolUse Enforcer - Enhanced Orchestrator Enforcement with Escalation
|
|
3
7
|
|
|
@@ -309,7 +313,7 @@ def enforce_cigs_pretool(tool_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
309
313
|
return enforcer.enforce(tool, params)
|
|
310
314
|
except Exception as e:
|
|
311
315
|
# Graceful degradation - allow on error
|
|
312
|
-
|
|
316
|
+
logger.warning(f"Warning: CIGS enforcement error: {e}")
|
|
313
317
|
return {
|
|
314
318
|
"hookSpecificOutput": {
|
|
315
319
|
"hookEventName": "PreToolUse",
|