htmlgraph 0.26.23__py3-none-any.whl → 0.26.25__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 +1 -1
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/api/main.py +56 -23
- 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/builders/track.py +26 -0
- htmlgraph/cli/base.py +31 -7
- htmlgraph/cli/work/__init__.py +74 -0
- htmlgraph/cli/work/browse.py +114 -0
- htmlgraph/cli/work/snapshot.py +558 -0
- htmlgraph/collections/base.py +34 -0
- htmlgraph/collections/todo.py +12 -0
- htmlgraph/converter.py +11 -0
- htmlgraph/db/schema.py +34 -1
- htmlgraph/hooks/orchestrator.py +88 -14
- htmlgraph/hooks/session_handler.py +3 -1
- htmlgraph/models.py +22 -2
- htmlgraph/orchestration/__init__.py +4 -0
- htmlgraph/orchestration/plugin_manager.py +1 -2
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/refs.py +343 -0
- htmlgraph/sdk.py +162 -1
- htmlgraph/session_manager.py +154 -2
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +755 -0
- htmlgraph/track_builder.py +12 -0
- {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/METADATA +1 -1
- {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/RECORD +36 -28
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/WHEEL +0 -0
- {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/entry_points.txt +0 -0
htmlgraph/db/schema.py
CHANGED
|
@@ -163,6 +163,12 @@ class HtmlGraphDB:
|
|
|
163
163
|
("completed_at", "DATETIME"),
|
|
164
164
|
("last_user_query_at", "DATETIME"),
|
|
165
165
|
("last_user_query", "TEXT"),
|
|
166
|
+
# Phase 2 Feature 3: Cross-Session Continuity handoff fields
|
|
167
|
+
("handoff_notes", "TEXT"),
|
|
168
|
+
("recommended_next", "TEXT"),
|
|
169
|
+
("blockers", "TEXT"), # JSON array of blocker strings
|
|
170
|
+
("recommended_context", "TEXT"), # JSON array of file paths
|
|
171
|
+
("continued_from", "TEXT"), # Previous session ID
|
|
166
172
|
]
|
|
167
173
|
|
|
168
174
|
# Refresh columns after potential rename
|
|
@@ -291,8 +297,14 @@ class HtmlGraphDB:
|
|
|
291
297
|
metadata JSON,
|
|
292
298
|
last_user_query_at DATETIME,
|
|
293
299
|
last_user_query TEXT,
|
|
300
|
+
handoff_notes TEXT,
|
|
301
|
+
recommended_next TEXT,
|
|
302
|
+
blockers JSON,
|
|
303
|
+
recommended_context JSON,
|
|
304
|
+
continued_from TEXT,
|
|
294
305
|
FOREIGN KEY (parent_session_id) REFERENCES sessions(session_id) ON DELETE SET NULL ON UPDATE CASCADE,
|
|
295
|
-
FOREIGN KEY (parent_event_id) REFERENCES agent_events(event_id) ON DELETE SET NULL ON UPDATE CASCADE
|
|
306
|
+
FOREIGN KEY (parent_event_id) REFERENCES agent_events(event_id) ON DELETE SET NULL ON UPDATE CASCADE,
|
|
307
|
+
FOREIGN KEY (continued_from) REFERENCES sessions(session_id) ON DELETE SET NULL ON UPDATE CASCADE
|
|
296
308
|
)
|
|
297
309
|
""")
|
|
298
310
|
|
|
@@ -407,6 +419,23 @@ class HtmlGraphDB:
|
|
|
407
419
|
)
|
|
408
420
|
""")
|
|
409
421
|
|
|
422
|
+
# 10. HANDOFF_TRACKING TABLE - Phase 2 Feature 3: Track handoff effectiveness
|
|
423
|
+
cursor.execute("""
|
|
424
|
+
CREATE TABLE IF NOT EXISTS handoff_tracking (
|
|
425
|
+
handoff_id TEXT PRIMARY KEY,
|
|
426
|
+
from_session_id TEXT NOT NULL,
|
|
427
|
+
to_session_id TEXT,
|
|
428
|
+
items_in_context INTEGER DEFAULT 0,
|
|
429
|
+
items_accessed INTEGER DEFAULT 0,
|
|
430
|
+
time_to_resume_seconds INTEGER DEFAULT 0,
|
|
431
|
+
user_rating INTEGER CHECK(user_rating BETWEEN 1 AND 5),
|
|
432
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
433
|
+
resumed_at DATETIME,
|
|
434
|
+
FOREIGN KEY (from_session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
|
|
435
|
+
FOREIGN KEY (to_session_id) REFERENCES sessions(session_id) ON DELETE SET NULL
|
|
436
|
+
)
|
|
437
|
+
""")
|
|
438
|
+
|
|
410
439
|
# 9. Create indexes for performance
|
|
411
440
|
self._create_indexes(cursor)
|
|
412
441
|
|
|
@@ -496,6 +525,10 @@ class HtmlGraphDB:
|
|
|
496
525
|
# live_events indexes - optimized for real-time WebSocket streaming
|
|
497
526
|
"CREATE INDEX IF NOT EXISTS idx_live_events_pending ON live_events(broadcast_at) WHERE broadcast_at IS NULL",
|
|
498
527
|
"CREATE INDEX IF NOT EXISTS idx_live_events_created ON live_events(created_at DESC)",
|
|
528
|
+
# handoff_tracking indexes - optimized for handoff effectiveness queries
|
|
529
|
+
"CREATE INDEX IF NOT EXISTS idx_handoff_from_session ON handoff_tracking(from_session_id, created_at DESC)",
|
|
530
|
+
"CREATE INDEX IF NOT EXISTS idx_handoff_to_session ON handoff_tracking(to_session_id, resumed_at DESC)",
|
|
531
|
+
"CREATE INDEX IF NOT EXISTS idx_handoff_rating ON handoff_tracking(user_rating, created_at DESC)",
|
|
499
532
|
]
|
|
500
533
|
|
|
501
534
|
for index_sql in indexes:
|
htmlgraph/hooks/orchestrator.py
CHANGED
|
@@ -94,6 +94,73 @@ def load_tool_history(session_id: str) -> list[dict]:
|
|
|
94
94
|
return []
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def record_tool_event(tool_name: str, session_id: str) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Record a tool event to the database for history tracking.
|
|
100
|
+
|
|
101
|
+
This is called at the end of PreToolUse hook execution to track
|
|
102
|
+
tool usage patterns for sequence detection.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
tool_name: Name of the tool being called
|
|
106
|
+
session_id: Session identifier for isolation
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
import datetime
|
|
110
|
+
import uuid
|
|
111
|
+
|
|
112
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
113
|
+
|
|
114
|
+
# Find database path
|
|
115
|
+
cwd = Path.cwd()
|
|
116
|
+
graph_dir = cwd / ".htmlgraph"
|
|
117
|
+
if not graph_dir.exists():
|
|
118
|
+
for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
|
|
119
|
+
candidate = parent / ".htmlgraph"
|
|
120
|
+
if candidate.exists():
|
|
121
|
+
graph_dir = candidate
|
|
122
|
+
break
|
|
123
|
+
|
|
124
|
+
if not graph_dir.exists():
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
db_path = graph_dir / "htmlgraph.db"
|
|
128
|
+
db = HtmlGraphDB(str(db_path))
|
|
129
|
+
if db.connection is None:
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
cursor = db.connection.cursor()
|
|
133
|
+
timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
134
|
+
|
|
135
|
+
# Ensure session exists (required by FK constraint)
|
|
136
|
+
cursor.execute(
|
|
137
|
+
"""
|
|
138
|
+
INSERT OR IGNORE INTO sessions (session_id, agent_assigned, created_at, status)
|
|
139
|
+
VALUES (?, ?, ?, ?)
|
|
140
|
+
""",
|
|
141
|
+
(session_id, "orchestrator-hook", timestamp, "active"),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Record the tool event using the actual schema
|
|
145
|
+
# Schema has: event_id, agent_id, event_type, timestamp, tool_name, session_id, etc.
|
|
146
|
+
event_id = str(uuid.uuid4())
|
|
147
|
+
agent_id = "orchestrator-hook" # Identifier for the hook
|
|
148
|
+
|
|
149
|
+
cursor.execute(
|
|
150
|
+
"""
|
|
151
|
+
INSERT INTO agent_events (event_id, agent_id, event_type, timestamp, tool_name, session_id)
|
|
152
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
153
|
+
""",
|
|
154
|
+
(event_id, agent_id, "tool_call", timestamp, tool_name, session_id),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
db.connection.commit()
|
|
158
|
+
db.disconnect()
|
|
159
|
+
except Exception:
|
|
160
|
+
# Graceful degradation - don't fail hook on recording error
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
|
|
97
164
|
def is_allowed_orchestrator_operation(
|
|
98
165
|
tool: str, params: dict[str, Any], session_id: str = "unknown"
|
|
99
166
|
) -> tuple[bool, str, str]:
|
|
@@ -402,6 +469,7 @@ def enforce_orchestrator_mode(
|
|
|
402
469
|
# Check if this is a subagent context - subagents have unrestricted tool access
|
|
403
470
|
if is_subagent_context():
|
|
404
471
|
return {
|
|
472
|
+
"continue": True,
|
|
405
473
|
"hookSpecificOutput": {
|
|
406
474
|
"hookEventName": "PreToolUse",
|
|
407
475
|
"permissionDecision": "allow",
|
|
@@ -425,18 +493,14 @@ def enforce_orchestrator_mode(
|
|
|
425
493
|
manager = OrchestratorModeManager(graph_dir)
|
|
426
494
|
|
|
427
495
|
if not manager.is_enabled():
|
|
428
|
-
# Mode not active, allow everything
|
|
429
|
-
return {
|
|
430
|
-
"hookSpecificOutput": {
|
|
431
|
-
"hookEventName": "PreToolUse",
|
|
432
|
-
"permissionDecision": "allow",
|
|
433
|
-
},
|
|
434
|
-
}
|
|
496
|
+
# Mode not active, allow everything with no additional output
|
|
497
|
+
return {"continue": True}
|
|
435
498
|
|
|
436
499
|
enforcement_level = manager.get_enforcement_level()
|
|
437
500
|
except Exception:
|
|
438
501
|
# If we can't check mode, fail open (allow)
|
|
439
502
|
return {
|
|
503
|
+
"continue": True,
|
|
440
504
|
"hookSpecificOutput": {
|
|
441
505
|
"hookEventName": "PreToolUse",
|
|
442
506
|
"permissionDecision": "allow",
|
|
@@ -467,6 +531,7 @@ def enforce_orchestrator_mode(
|
|
|
467
531
|
)
|
|
468
532
|
|
|
469
533
|
return {
|
|
534
|
+
"continue": False,
|
|
470
535
|
"hookSpecificOutput": {
|
|
471
536
|
"hookEventName": "PreToolUse",
|
|
472
537
|
"permissionDecision": "deny",
|
|
@@ -491,6 +556,7 @@ def enforce_orchestrator_mode(
|
|
|
491
556
|
):
|
|
492
557
|
# Provide guidance even when allowing
|
|
493
558
|
return {
|
|
559
|
+
"continue": True,
|
|
494
560
|
"hookSpecificOutput": {
|
|
495
561
|
"hookEventName": "PreToolUse",
|
|
496
562
|
"permissionDecision": "allow",
|
|
@@ -498,6 +564,7 @@ def enforce_orchestrator_mode(
|
|
|
498
564
|
},
|
|
499
565
|
}
|
|
500
566
|
return {
|
|
567
|
+
"continue": True,
|
|
501
568
|
"hookSpecificOutput": {
|
|
502
569
|
"hookEventName": "PreToolUse",
|
|
503
570
|
"permissionDecision": "allow",
|
|
@@ -513,8 +580,8 @@ def enforce_orchestrator_mode(
|
|
|
513
580
|
suggestion = create_task_suggestion(tool, params)
|
|
514
581
|
|
|
515
582
|
if enforcement_level == "strict":
|
|
516
|
-
# STRICT mode -
|
|
517
|
-
|
|
583
|
+
# STRICT mode - advisory warning with violation count (does not block)
|
|
584
|
+
warning_message = (
|
|
518
585
|
f"🚫 ORCHESTRATOR MODE VIOLATION ({violations}/{circuit_breaker_threshold}): {reason}\n\n"
|
|
519
586
|
f"⚠️ WARNING: Direct operations waste context and break delegation pattern!\n\n"
|
|
520
587
|
f"Suggested delegation:\n"
|
|
@@ -523,23 +590,25 @@ def enforce_orchestrator_mode(
|
|
|
523
590
|
|
|
524
591
|
# Add circuit breaker warning if approaching threshold
|
|
525
592
|
if violations >= circuit_breaker_threshold:
|
|
526
|
-
|
|
593
|
+
warning_message += (
|
|
527
594
|
"🚨 CIRCUIT BREAKER TRIGGERED - Further violations will be blocked!\n\n"
|
|
528
595
|
"Reset with: uv run htmlgraph orchestrator reset-violations\n"
|
|
529
596
|
)
|
|
530
597
|
elif violations == circuit_breaker_threshold - 1:
|
|
531
|
-
|
|
598
|
+
warning_message += "⚠️ Next violation will trigger circuit breaker!\n\n"
|
|
532
599
|
|
|
533
|
-
|
|
600
|
+
warning_message += (
|
|
534
601
|
"See ORCHESTRATOR_DIRECTIVES in session context for HtmlGraph delegation pattern.\n"
|
|
535
602
|
"To disable orchestrator mode: uv run htmlgraph orchestrator disable"
|
|
536
603
|
)
|
|
537
604
|
|
|
605
|
+
# Advisory-only: allow operation but provide warning
|
|
538
606
|
return {
|
|
607
|
+
"continue": True,
|
|
539
608
|
"hookSpecificOutput": {
|
|
540
609
|
"hookEventName": "PreToolUse",
|
|
541
|
-
"permissionDecision": "
|
|
542
|
-
"
|
|
610
|
+
"permissionDecision": "allow",
|
|
611
|
+
"additionalContext": warning_message,
|
|
543
612
|
},
|
|
544
613
|
}
|
|
545
614
|
else:
|
|
@@ -549,6 +618,7 @@ def enforce_orchestrator_mode(
|
|
|
549
618
|
)
|
|
550
619
|
|
|
551
620
|
return {
|
|
621
|
+
"continue": True,
|
|
552
622
|
"hookSpecificOutput": {
|
|
553
623
|
"hookEventName": "PreToolUse",
|
|
554
624
|
"permissionDecision": "allow",
|
|
@@ -592,5 +662,9 @@ def main() -> None:
|
|
|
592
662
|
# Enforce orchestrator mode with session_id for history lookup
|
|
593
663
|
response = enforce_orchestrator_mode(tool_name, tool_input, session_id)
|
|
594
664
|
|
|
665
|
+
# Record tool event to database for history tracking
|
|
666
|
+
# This allows subsequent calls to detect patterns (e.g., multiple Reads)
|
|
667
|
+
record_tool_event(tool_name, session_id)
|
|
668
|
+
|
|
595
669
|
# Output JSON response
|
|
596
670
|
print(json.dumps(response))
|
|
@@ -185,7 +185,9 @@ def handle_session_start(context: HookContext, session: Any | None) -> dict[str,
|
|
|
185
185
|
|
|
186
186
|
{feature_list}
|
|
187
187
|
|
|
188
|
-
Activity will be attributed to these features based on file patterns and keywords.
|
|
188
|
+
Activity will be attributed to these features based on file patterns and keywords.
|
|
189
|
+
|
|
190
|
+
**To view all work and progress:** `htmlgraph snapshot --summary`"""
|
|
189
191
|
output["hookSpecificOutput"]["sessionFeatureContext"] = context_str
|
|
190
192
|
context.log("info", f"Loaded {len(active_features)} active features")
|
|
191
193
|
|
htmlgraph/models.py
CHANGED
|
@@ -975,10 +975,13 @@ class Session(BaseModel):
|
|
|
975
975
|
parent_activity: str | None = None # Parent activity ID
|
|
976
976
|
nesting_depth: int = 0 # Depth of nesting (0 = top-level)
|
|
977
977
|
|
|
978
|
-
# Handoff context
|
|
978
|
+
# Handoff context (Phase 2 Feature 3: Cross-Session Continuity)
|
|
979
979
|
handoff_notes: str | None = None
|
|
980
980
|
recommended_next: str | None = None
|
|
981
981
|
blockers: list[str] = Field(default_factory=list)
|
|
982
|
+
recommended_context: list[str] = Field(
|
|
983
|
+
default_factory=list
|
|
984
|
+
) # File paths to keep context for
|
|
982
985
|
|
|
983
986
|
# High-frequency activity log
|
|
984
987
|
activity_log: list[ActivityEntry] = Field(default_factory=list)
|
|
@@ -1382,7 +1385,12 @@ class Session(BaseModel):
|
|
|
1382
1385
|
|
|
1383
1386
|
# Build handoff HTML
|
|
1384
1387
|
handoff_html = ""
|
|
1385
|
-
if
|
|
1388
|
+
if (
|
|
1389
|
+
self.handoff_notes
|
|
1390
|
+
or self.recommended_next
|
|
1391
|
+
or self.blockers
|
|
1392
|
+
or self.recommended_context
|
|
1393
|
+
):
|
|
1386
1394
|
handoff_section = """
|
|
1387
1395
|
<section data-handoff>
|
|
1388
1396
|
<h3>Handoff Context</h3>"""
|
|
@@ -1405,6 +1413,18 @@ class Session(BaseModel):
|
|
|
1405
1413
|
</ul>
|
|
1406
1414
|
</div>"""
|
|
1407
1415
|
|
|
1416
|
+
if self.recommended_context:
|
|
1417
|
+
context_items = "\n ".join(
|
|
1418
|
+
f"<li>{file_path}</li>" for file_path in self.recommended_context
|
|
1419
|
+
)
|
|
1420
|
+
handoff_section += f"""
|
|
1421
|
+
<div data-recommended-context>
|
|
1422
|
+
<strong>Recommended Context:</strong>
|
|
1423
|
+
<ul>
|
|
1424
|
+
{context_items}
|
|
1425
|
+
</ul>
|
|
1426
|
+
</div>"""
|
|
1427
|
+
|
|
1408
1428
|
handoff_section += "\n </section>"
|
|
1409
1429
|
handoff_html = handoff_section
|
|
1410
1430
|
|
|
@@ -9,6 +9,7 @@ from .model_selection import (
|
|
|
9
9
|
get_fallback_chain,
|
|
10
10
|
select_model,
|
|
11
11
|
)
|
|
12
|
+
from .spawner_event_tracker import SpawnerEventTracker, create_tracker_from_env
|
|
12
13
|
|
|
13
14
|
# Export modular spawners for advanced usage
|
|
14
15
|
from .spawners import (
|
|
@@ -37,6 +38,9 @@ __all__ = [
|
|
|
37
38
|
"CodexSpawner",
|
|
38
39
|
"CopilotSpawner",
|
|
39
40
|
"ClaudeSpawner",
|
|
41
|
+
# Spawner event tracking
|
|
42
|
+
"SpawnerEventTracker",
|
|
43
|
+
"create_tracker_from_env",
|
|
40
44
|
# Model selection
|
|
41
45
|
"ModelSelection",
|
|
42
46
|
"TaskType",
|
|
@@ -22,7 +22,7 @@ class PluginManager:
|
|
|
22
22
|
"""Get the plugin directory path.
|
|
23
23
|
|
|
24
24
|
Returns:
|
|
25
|
-
Path to packages/claude-plugin
|
|
25
|
+
Path to packages/claude-plugin (the plugin root, not .claude-plugin)
|
|
26
26
|
"""
|
|
27
27
|
# Path(__file__) = .../src/python/htmlgraph/orchestration/plugin_manager.py
|
|
28
28
|
# Need to go up 5 levels to reach project root
|
|
@@ -30,7 +30,6 @@ class PluginManager:
|
|
|
30
30
|
Path(__file__).parent.parent.parent.parent.parent
|
|
31
31
|
/ "packages"
|
|
32
32
|
/ "claude-plugin"
|
|
33
|
-
/ ".claude-plugin"
|
|
34
33
|
)
|
|
35
34
|
|
|
36
35
|
@staticmethod
|