htmlgraph 0.26.25__py3-none-any.whl → 0.27.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 +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/cost_monitor.py +664 -0
- htmlgraph/analytics/cross_session.py +13 -9
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +15 -11
- htmlgraph/analytics_index.py +2 -1
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +167 -62
- htmlgraph/api/websocket.py +538 -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 +2 -1
- htmlgraph/cli/analytics.py +2 -1
- htmlgraph/cli/base.py +2 -1
- 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 +2 -1
- htmlgraph/cli/work/browse.py +2 -1
- 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 +2 -1
- htmlgraph/cli/work/tracks.py +2 -1
- htmlgraph/collections/base.py +10 -5
- 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 +2 -1
- htmlgraph/collections/traces.py +15 -10
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/db/schema.py +67 -6
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/edge_index.py +2 -1
- htmlgraph/event_log.py +83 -64
- 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 +4 -0
- 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 +2 -1
- 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/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/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 +24 -19
- 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 +2 -1
- htmlgraph/repo_hash.py +2 -1
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- 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/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +21 -17
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/handoff.py +4 -3
- htmlgraph/system_prompts.py +2 -1
- htmlgraph/track_builder.py +2 -1
- htmlgraph/transcript.py +2 -1
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/METADATA +1 -1
- htmlgraph-0.27.1.dist-info/RECORD +332 -0
- htmlgraph/sdk.py +0 -3500
- htmlgraph-0.26.25.dist-info/RECORD +0 -274
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/WHEEL +0 -0
- {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/entry_points.txt +0 -0
htmlgraph/db/schema.py
CHANGED
|
@@ -141,8 +141,15 @@ class HtmlGraphDB:
|
|
|
141
141
|
|
|
142
142
|
# Migration: rename 'agent' to 'agent_assigned' if needed
|
|
143
143
|
if "agent" in columns and "agent_assigned" not in columns:
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
try:
|
|
145
|
+
cursor.execute(
|
|
146
|
+
"ALTER TABLE sessions RENAME COLUMN agent TO agent_assigned"
|
|
147
|
+
)
|
|
148
|
+
logger.info("Migrated sessions.agent -> sessions.agent_assigned")
|
|
149
|
+
except sqlite3.OperationalError as e:
|
|
150
|
+
logger.debug(f"Could not rename column: {e}")
|
|
151
|
+
# Column may already exist
|
|
152
|
+
pass
|
|
146
153
|
|
|
147
154
|
# Add missing columns with defaults
|
|
148
155
|
# Note: SQLite doesn't allow CURRENT_TIMESTAMP in ALTER TABLE, so we use NULL
|
|
@@ -169,6 +176,10 @@ class HtmlGraphDB:
|
|
|
169
176
|
("blockers", "TEXT"), # JSON array of blocker strings
|
|
170
177
|
("recommended_context", "TEXT"), # JSON array of file paths
|
|
171
178
|
("continued_from", "TEXT"), # Previous session ID
|
|
179
|
+
# Phase 3.1: Real-time cost monitoring
|
|
180
|
+
("cost_budget", "REAL"), # Budget in USD for this session
|
|
181
|
+
("cost_threshold_breached", "INTEGER DEFAULT 0"), # Whether budget exceeded
|
|
182
|
+
("predicted_cost", "REAL DEFAULT 0.0"), # Predicted final cost
|
|
172
183
|
]
|
|
173
184
|
|
|
174
185
|
# Refresh columns after potential rename
|
|
@@ -252,7 +263,7 @@ class HtmlGraphDB:
|
|
|
252
263
|
title TEXT NOT NULL,
|
|
253
264
|
description TEXT,
|
|
254
265
|
status TEXT NOT NULL DEFAULT 'todo' CHECK(
|
|
255
|
-
status IN ('todo', '
|
|
266
|
+
status IN ('todo', 'in-progress', 'blocked', 'done', 'active', 'ended', 'stale')
|
|
256
267
|
),
|
|
257
268
|
priority TEXT DEFAULT 'medium' CHECK(
|
|
258
269
|
priority IN ('low', 'medium', 'high', 'critical')
|
|
@@ -267,7 +278,7 @@ class HtmlGraphDB:
|
|
|
267
278
|
parent_feature_id TEXT,
|
|
268
279
|
tags JSON,
|
|
269
280
|
metadata JSON,
|
|
270
|
-
FOREIGN KEY (track_id) REFERENCES tracks(
|
|
281
|
+
FOREIGN KEY (track_id) REFERENCES tracks(id),
|
|
271
282
|
FOREIGN KEY (parent_feature_id) REFERENCES features(id)
|
|
272
283
|
)
|
|
273
284
|
""")
|
|
@@ -302,6 +313,9 @@ class HtmlGraphDB:
|
|
|
302
313
|
blockers JSON,
|
|
303
314
|
recommended_context JSON,
|
|
304
315
|
continued_from TEXT,
|
|
316
|
+
cost_budget REAL,
|
|
317
|
+
cost_threshold_breached INTEGER DEFAULT 0,
|
|
318
|
+
predicted_cost REAL DEFAULT 0.0,
|
|
305
319
|
FOREIGN KEY (parent_session_id) REFERENCES sessions(session_id) ON DELETE SET NULL ON UPDATE CASCADE,
|
|
306
320
|
FOREIGN KEY (parent_event_id) REFERENCES agent_events(event_id) ON DELETE SET NULL ON UPDATE CASCADE,
|
|
307
321
|
FOREIGN KEY (continued_from) REFERENCES sessions(session_id) ON DELETE SET NULL ON UPDATE CASCADE
|
|
@@ -311,14 +325,15 @@ class HtmlGraphDB:
|
|
|
311
325
|
# 4. TRACKS TABLE - Multi-feature initiatives
|
|
312
326
|
cursor.execute("""
|
|
313
327
|
CREATE TABLE IF NOT EXISTS tracks (
|
|
314
|
-
|
|
328
|
+
id TEXT PRIMARY KEY,
|
|
329
|
+
type TEXT DEFAULT 'track',
|
|
315
330
|
title TEXT NOT NULL,
|
|
316
331
|
description TEXT,
|
|
317
332
|
priority TEXT DEFAULT 'medium' CHECK(
|
|
318
333
|
priority IN ('low', 'medium', 'high', 'critical')
|
|
319
334
|
),
|
|
320
335
|
status TEXT NOT NULL DEFAULT 'todo' CHECK(
|
|
321
|
-
status IN ('todo', '
|
|
336
|
+
status IN ('todo', 'in-progress', 'blocked', 'done', 'active', 'ended', 'stale')
|
|
322
337
|
),
|
|
323
338
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
324
339
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
@@ -436,6 +451,39 @@ class HtmlGraphDB:
|
|
|
436
451
|
)
|
|
437
452
|
""")
|
|
438
453
|
|
|
454
|
+
# 11. COST_EVENTS TABLE - Phase 3.1: Real-time cost monitoring & alerts
|
|
455
|
+
cursor.execute("""
|
|
456
|
+
CREATE TABLE IF NOT EXISTS cost_events (
|
|
457
|
+
event_id TEXT PRIMARY KEY,
|
|
458
|
+
session_id TEXT NOT NULL,
|
|
459
|
+
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
460
|
+
|
|
461
|
+
-- Token tracking
|
|
462
|
+
tool_name TEXT,
|
|
463
|
+
model TEXT,
|
|
464
|
+
input_tokens INTEGER DEFAULT 0,
|
|
465
|
+
output_tokens INTEGER DEFAULT 0,
|
|
466
|
+
total_tokens INTEGER DEFAULT 0,
|
|
467
|
+
cost_usd REAL DEFAULT 0.0,
|
|
468
|
+
|
|
469
|
+
-- Agent tracking
|
|
470
|
+
agent_id TEXT,
|
|
471
|
+
subagent_type TEXT,
|
|
472
|
+
|
|
473
|
+
-- Alert tracking
|
|
474
|
+
alert_type TEXT,
|
|
475
|
+
message TEXT,
|
|
476
|
+
current_cost_usd REAL,
|
|
477
|
+
budget_usd REAL,
|
|
478
|
+
predicted_cost_usd REAL,
|
|
479
|
+
severity TEXT,
|
|
480
|
+
acknowledged BOOLEAN DEFAULT 0,
|
|
481
|
+
|
|
482
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
483
|
+
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
484
|
+
)
|
|
485
|
+
""")
|
|
486
|
+
|
|
439
487
|
# 9. Create indexes for performance
|
|
440
488
|
self._create_indexes(cursor)
|
|
441
489
|
|
|
@@ -529,6 +577,19 @@ class HtmlGraphDB:
|
|
|
529
577
|
"CREATE INDEX IF NOT EXISTS idx_handoff_from_session ON handoff_tracking(from_session_id, created_at DESC)",
|
|
530
578
|
"CREATE INDEX IF NOT EXISTS idx_handoff_to_session ON handoff_tracking(to_session_id, resumed_at DESC)",
|
|
531
579
|
"CREATE INDEX IF NOT EXISTS idx_handoff_rating ON handoff_tracking(user_rating, created_at DESC)",
|
|
580
|
+
# cost_events indexes - optimized for real-time cost monitoring & alerts
|
|
581
|
+
# Pattern: WHERE session_id ORDER BY timestamp DESC (cost timeline)
|
|
582
|
+
"CREATE INDEX IF NOT EXISTS idx_cost_events_session_ts ON cost_events(session_id, timestamp DESC)",
|
|
583
|
+
# Pattern: WHERE alert_type (alert filtering)
|
|
584
|
+
"CREATE INDEX IF NOT EXISTS idx_cost_events_alert_type ON cost_events(alert_type, timestamp DESC)",
|
|
585
|
+
# Pattern: WHERE model GROUP BY (cost breakdown)
|
|
586
|
+
"CREATE INDEX IF NOT EXISTS idx_cost_events_model ON cost_events(model, session_id)",
|
|
587
|
+
# Pattern: WHERE tool_name GROUP BY (tool cost analysis)
|
|
588
|
+
"CREATE INDEX IF NOT EXISTS idx_cost_events_tool ON cost_events(tool_name, session_id)",
|
|
589
|
+
# Pattern: WHERE severity (alert severity filtering)
|
|
590
|
+
"CREATE INDEX IF NOT EXISTS idx_cost_events_severity ON cost_events(severity, timestamp DESC)",
|
|
591
|
+
# Pattern: Timestamp range queries for predictions
|
|
592
|
+
"CREATE INDEX IF NOT EXISTS idx_cost_events_timestamp ON cost_events(timestamp DESC)",
|
|
532
593
|
]
|
|
533
594
|
|
|
534
595
|
for index_sql in indexes:
|
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,98 @@ 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
|
-
|
|
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 []
|
|
58
102
|
|
|
59
103
|
def to_json(self) -> dict[str, Any]:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"timestamp": self.timestamp.isoformat(),
|
|
63
|
-
"session_id": self.session_id,
|
|
64
|
-
"agent": self.agent,
|
|
65
|
-
"tool": self.tool,
|
|
66
|
-
"summary": self.summary,
|
|
67
|
-
"success": self.success,
|
|
68
|
-
"feature_id": self.feature_id,
|
|
69
|
-
"work_type": self.work_type,
|
|
70
|
-
"drift_score": self.drift_score,
|
|
71
|
-
"start_commit": self.start_commit,
|
|
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
|
-
}
|
|
104
|
+
"""Convert EventRecord to JSON-serializable dictionary."""
|
|
105
|
+
return self.model_dump(mode="json")
|
|
90
106
|
|
|
91
107
|
|
|
92
108
|
class JsonlEventLog:
|
|
@@ -104,7 +120,10 @@ class JsonlEventLog:
|
|
|
104
120
|
|
|
105
121
|
def append(self, record: EventRecord) -> Path:
|
|
106
122
|
path = self.path_for_session(record.session_id)
|
|
107
|
-
line =
|
|
123
|
+
line = (
|
|
124
|
+
json.dumps(record.model_dump(mode="json"), ensure_ascii=False, default=str)
|
|
125
|
+
+ "\n"
|
|
126
|
+
)
|
|
108
127
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
109
128
|
|
|
110
129
|
# 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",
|
htmlgraph/hooks/drift_handler.py
CHANGED
|
@@ -133,7 +133,7 @@ def load_drift_config(graph_dir: Path) -> dict[str, Any]:
|
|
|
133
133
|
Example:
|
|
134
134
|
```python
|
|
135
135
|
config = load_drift_config(Path(".htmlgraph"))
|
|
136
|
-
|
|
136
|
+
logger.info(f"Auto-classify threshold: {config['drift_detection']['auto_classify_threshold']}")
|
|
137
137
|
```
|
|
138
138
|
"""
|
|
139
139
|
graph_dir = Path(graph_dir)
|
|
@@ -196,7 +196,7 @@ def detect_drift(
|
|
|
196
196
|
```python
|
|
197
197
|
score, feature_id = detect_drift(activity_result, config)
|
|
198
198
|
if score > config['drift_detection']['auto_classify_threshold']:
|
|
199
|
-
|
|
199
|
+
logger.info(f"HIGH DRIFT: {score:.2f}")
|
|
200
200
|
```
|
|
201
201
|
"""
|
|
202
202
|
drift_score = getattr(activity_result, "drift_score", 0.0) or 0.0
|
|
@@ -242,7 +242,7 @@ def handle_high_drift(
|
|
|
242
242
|
```python
|
|
243
243
|
nudge = handle_high_drift(context, 0.87, queue, config)
|
|
244
244
|
if nudge:
|
|
245
|
-
|
|
245
|
+
logger.info("%s", nudge) # "HIGH DRIFT (0.87): Activity queued for classification..."
|
|
246
246
|
```
|
|
247
247
|
"""
|
|
248
248
|
drift_config = config.get("drift_detection", {})
|