htmlgraph 0.25.0__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 +1 -1
- htmlgraph/api/main.py +193 -45
- htmlgraph/api/templates/dashboard.html +11 -0
- htmlgraph/api/templates/partials/activity-feed.html +458 -8
- htmlgraph/dashboard.html +41 -0
- htmlgraph/db/schema.py +254 -4
- 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/cigs_pretool_enforcer.py +2 -2
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +57 -10
- htmlgraph/hooks/drift_handler.py +24 -20
- htmlgraph/hooks/event_tracker.py +204 -177
- htmlgraph/hooks/orchestrator.py +6 -4
- htmlgraph/hooks/orchestrator_reflector.py +4 -4
- htmlgraph/hooks/pretooluse.py +3 -6
- htmlgraph/hooks/prompt_analyzer.py +14 -25
- htmlgraph/hooks/session_handler.py +123 -69
- htmlgraph/hooks/state_manager.py +7 -4
- htmlgraph/hooks/validator.py +15 -11
- htmlgraph/orchestration/headless_spawner.py +322 -15
- htmlgraph/orchestration/live_events.py +377 -0
- {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/dashboard.html +41 -0
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/METADATA +1 -1
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/RECORD +32 -27
- {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/WHEEL +0 -0
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/entry_points.txt +0 -0
htmlgraph/hooks/pretooluse.py
CHANGED
|
@@ -243,8 +243,6 @@ def create_task_parent_event(
|
|
|
243
243
|
Parent event_id if successful, None otherwise
|
|
244
244
|
"""
|
|
245
245
|
try:
|
|
246
|
-
from pathlib import Path
|
|
247
|
-
|
|
248
246
|
if not db.connection:
|
|
249
247
|
db.connect()
|
|
250
248
|
|
|
@@ -252,13 +250,12 @@ def create_task_parent_event(
|
|
|
252
250
|
subagent_type = extract_subagent_type(tool_input)
|
|
253
251
|
prompt = str(tool_input.get("prompt", ""))[:200]
|
|
254
252
|
|
|
255
|
-
# Load UserQuery event ID for parent-child linking
|
|
256
|
-
graph_dir = Path.cwd() / ".htmlgraph"
|
|
253
|
+
# Load UserQuery event ID for parent-child linking from database
|
|
257
254
|
user_query_event_id = None
|
|
258
255
|
try:
|
|
259
|
-
from htmlgraph.hooks.event_tracker import
|
|
256
|
+
from htmlgraph.hooks.event_tracker import get_parent_user_query
|
|
260
257
|
|
|
261
|
-
user_query_event_id =
|
|
258
|
+
user_query_event_id = get_parent_user_query(db, session_id)
|
|
262
259
|
except Exception:
|
|
263
260
|
pass
|
|
264
261
|
|
|
@@ -15,14 +15,11 @@ The module is designed to be reusable across different hook implementations,
|
|
|
15
15
|
with graceful degradation if dependencies are unavailable.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
import json
|
|
19
18
|
import logging
|
|
20
|
-
import os
|
|
21
19
|
import re
|
|
22
20
|
import uuid
|
|
23
21
|
from datetime import datetime, timezone
|
|
24
|
-
from
|
|
25
|
-
from typing import Any, Optional
|
|
22
|
+
from typing import Any
|
|
26
23
|
|
|
27
24
|
from htmlgraph.hooks.context import HookContext
|
|
28
25
|
|
|
@@ -276,7 +273,7 @@ def get_session_violation_count(context: HookContext) -> tuple[int, int]:
|
|
|
276
273
|
return 0, 0
|
|
277
274
|
|
|
278
275
|
|
|
279
|
-
def get_active_work_item(context: HookContext) ->
|
|
276
|
+
def get_active_work_item(context: HookContext) -> dict[str, Any] | None:
|
|
280
277
|
"""
|
|
281
278
|
Query HtmlGraph for active feature/spike.
|
|
282
279
|
|
|
@@ -299,15 +296,18 @@ def get_active_work_item(context: HookContext) -> Optional[dict[str, Any]]:
|
|
|
299
296
|
|
|
300
297
|
sdk = SDK()
|
|
301
298
|
work_item = sdk.get_active_work_item()
|
|
302
|
-
|
|
299
|
+
if work_item is None:
|
|
300
|
+
return None
|
|
301
|
+
# Convert ActiveWorkItem TypedDict to dict
|
|
302
|
+
return dict(work_item) if hasattr(work_item, "__iter__") else work_item # type: ignore
|
|
303
303
|
except Exception as e:
|
|
304
304
|
logger.debug(f"Could not get active work item: {e}")
|
|
305
305
|
return None
|
|
306
306
|
|
|
307
307
|
|
|
308
308
|
def generate_guidance(
|
|
309
|
-
classification: dict[str, Any], active_work:
|
|
310
|
-
) ->
|
|
309
|
+
classification: dict[str, Any], active_work: dict[str, Any] | None, prompt: str
|
|
310
|
+
) -> str | None:
|
|
311
311
|
"""
|
|
312
312
|
Generate workflow guidance based on classification and context.
|
|
313
313
|
|
|
@@ -447,7 +447,7 @@ def generate_guidance(
|
|
|
447
447
|
|
|
448
448
|
|
|
449
449
|
def generate_cigs_guidance(
|
|
450
|
-
cigs_intent: dict, violation_count: int, waste_tokens: int
|
|
450
|
+
cigs_intent: dict[str, Any], violation_count: int, waste_tokens: int
|
|
451
451
|
) -> str:
|
|
452
452
|
"""
|
|
453
453
|
Generate CIGS-specific guidance for detected violations.
|
|
@@ -521,13 +521,13 @@ def generate_cigs_guidance(
|
|
|
521
521
|
return "\n".join(guidance_parts)
|
|
522
522
|
|
|
523
523
|
|
|
524
|
-
def create_user_query_event(context: HookContext, prompt: str) ->
|
|
524
|
+
def create_user_query_event(context: HookContext, prompt: str) -> str | None:
|
|
525
525
|
"""
|
|
526
526
|
Create UserQuery event in HtmlGraph database.
|
|
527
527
|
|
|
528
528
|
Records the user prompt as a UserQuery event that serves as the parent
|
|
529
|
-
for subsequent tool calls and delegations.
|
|
530
|
-
|
|
529
|
+
for subsequent tool calls and delegations. Database is the single source
|
|
530
|
+
of truth - no file-based state is used.
|
|
531
531
|
|
|
532
532
|
Args:
|
|
533
533
|
context: HookContext with session and database access
|
|
@@ -548,7 +548,6 @@ def create_user_query_event(context: HookContext, prompt: str) -> Optional[str]:
|
|
|
548
548
|
"""
|
|
549
549
|
try:
|
|
550
550
|
session_id = context.session_id
|
|
551
|
-
graph_dir = context.graph_dir
|
|
552
551
|
|
|
553
552
|
if not session_id or session_id == "unknown":
|
|
554
553
|
logger.debug("No valid session ID for UserQuery event")
|
|
@@ -556,9 +555,6 @@ def create_user_query_event(context: HookContext, prompt: str) -> Optional[str]:
|
|
|
556
555
|
|
|
557
556
|
# Create UserQuery event in database
|
|
558
557
|
try:
|
|
559
|
-
from htmlgraph.db.schema import HtmlGraphDB
|
|
560
|
-
from htmlgraph.hooks.event_tracker import save_user_query_event
|
|
561
|
-
|
|
562
558
|
db = context.database
|
|
563
559
|
|
|
564
560
|
# Ensure session exists in database before creating event
|
|
@@ -589,6 +585,8 @@ def create_user_query_event(context: HookContext, prompt: str) -> Optional[str]:
|
|
|
589
585
|
input_summary = prompt[:200]
|
|
590
586
|
|
|
591
587
|
# Insert UserQuery event into agent_events
|
|
588
|
+
# Database is the single source of truth for parent-child linking
|
|
589
|
+
# Subsequent tool calls query database via get_parent_user_query()
|
|
592
590
|
success = db.insert_event(
|
|
593
591
|
event_id=user_query_event_id,
|
|
594
592
|
agent_id="user",
|
|
@@ -606,15 +604,6 @@ def create_user_query_event(context: HookContext, prompt: str) -> Optional[str]:
|
|
|
606
604
|
logger.warning("Failed to insert UserQuery event into database")
|
|
607
605
|
return None
|
|
608
606
|
|
|
609
|
-
# Save event ID to session-scoped file for subsequent tool calls
|
|
610
|
-
# This is used by PreToolUse hook to link Task delegations
|
|
611
|
-
try:
|
|
612
|
-
save_user_query_event(graph_dir, session_id, user_query_event_id)
|
|
613
|
-
logger.debug(f"Saved UserQuery event file: {user_query_event_id}")
|
|
614
|
-
except Exception as e:
|
|
615
|
-
# If saving to file fails, still continue (database insert succeeded)
|
|
616
|
-
logger.warning(f"Could not save UserQuery event file: {e}")
|
|
617
|
-
|
|
618
607
|
logger.info(f"Created UserQuery event: {user_query_event_id}")
|
|
619
608
|
return user_query_event_id
|
|
620
609
|
|
|
@@ -89,7 +89,7 @@ def init_or_get_session(context: HookContext) -> Any | None:
|
|
|
89
89
|
return None
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def handle_session_start(context: HookContext, session: Any | None) -> dict:
|
|
92
|
+
def handle_session_start(context: HookContext, session: Any | None) -> dict[str, Any]:
|
|
93
93
|
"""
|
|
94
94
|
Initialize HtmlGraph tracking for the session.
|
|
95
95
|
|
|
@@ -99,6 +99,7 @@ def handle_session_start(context: HookContext, session: Any | None) -> dict:
|
|
|
99
99
|
- Builds feature context string
|
|
100
100
|
- Records session start event
|
|
101
101
|
- Creates conversation-init spike if new conversation
|
|
102
|
+
- Injects concurrent session and recent work context
|
|
102
103
|
|
|
103
104
|
Args:
|
|
104
105
|
context: HookContext with project and graph directory information
|
|
@@ -110,6 +111,7 @@ def handle_session_start(context: HookContext, session: Any | None) -> dict:
|
|
|
110
111
|
"continue": True,
|
|
111
112
|
"hookSpecificOutput": {
|
|
112
113
|
"sessionFeatureContext": str with feature context,
|
|
114
|
+
"sessionContext": optional concurrent/recent work context,
|
|
113
115
|
"versionInfo": optional version check result
|
|
114
116
|
}
|
|
115
117
|
}
|
|
@@ -118,6 +120,7 @@ def handle_session_start(context: HookContext, session: Any | None) -> dict:
|
|
|
118
120
|
"continue": True,
|
|
119
121
|
"hookSpecificOutput": {
|
|
120
122
|
"sessionFeatureContext": "",
|
|
123
|
+
"sessionContext": "",
|
|
121
124
|
"versionInfo": None,
|
|
122
125
|
},
|
|
123
126
|
}
|
|
@@ -201,10 +204,61 @@ Activity will be attributed to these features based on file patterns and keyword
|
|
|
201
204
|
except Exception as e:
|
|
202
205
|
context.log("debug", f"Could not check version: {e}")
|
|
203
206
|
|
|
207
|
+
# Build concurrent session context
|
|
208
|
+
try:
|
|
209
|
+
from htmlgraph.hooks.concurrent_sessions import (
|
|
210
|
+
format_concurrent_sessions_markdown,
|
|
211
|
+
format_recent_work_markdown,
|
|
212
|
+
get_concurrent_sessions,
|
|
213
|
+
get_recent_completed_sessions,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
db = context.database
|
|
217
|
+
|
|
218
|
+
# Get concurrent sessions (other active windows)
|
|
219
|
+
concurrent = get_concurrent_sessions(db, context.session_id, minutes=30)
|
|
220
|
+
concurrent_md = format_concurrent_sessions_markdown(concurrent)
|
|
221
|
+
|
|
222
|
+
# Get recent completed work
|
|
223
|
+
recent = get_recent_completed_sessions(db, hours=24, limit=5)
|
|
224
|
+
recent_md = format_recent_work_markdown(recent)
|
|
225
|
+
|
|
226
|
+
# Build session context
|
|
227
|
+
session_context = ""
|
|
228
|
+
if concurrent_md:
|
|
229
|
+
session_context += concurrent_md + "\n"
|
|
230
|
+
if recent_md:
|
|
231
|
+
session_context += recent_md + "\n"
|
|
232
|
+
|
|
233
|
+
if session_context:
|
|
234
|
+
output["hookSpecificOutput"]["sessionContext"] = session_context.strip()
|
|
235
|
+
context.log(
|
|
236
|
+
"info",
|
|
237
|
+
f"Injected context: {len(concurrent)} concurrent, {len(recent)} recent",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
except ImportError:
|
|
241
|
+
context.log("debug", "Concurrent session module not available")
|
|
242
|
+
except Exception as e:
|
|
243
|
+
context.log("warning", f"Failed to get concurrent session context: {e}")
|
|
244
|
+
|
|
245
|
+
# Update session with user's current query (if available from hook input)
|
|
246
|
+
try:
|
|
247
|
+
user_query = context.hook_input.get("prompt", "")
|
|
248
|
+
if user_query and session:
|
|
249
|
+
context.session_manager.track_activity(
|
|
250
|
+
session_id=session.id,
|
|
251
|
+
tool="UserQuery",
|
|
252
|
+
summary=user_query[:100],
|
|
253
|
+
payload={"query_length": len(user_query)},
|
|
254
|
+
)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
context.log("warning", f"Failed to update session activity: {e}")
|
|
257
|
+
|
|
204
258
|
return output
|
|
205
259
|
|
|
206
260
|
|
|
207
|
-
def handle_session_end(context: HookContext) -> dict:
|
|
261
|
+
def handle_session_end(context: HookContext) -> dict[str, Any]:
|
|
208
262
|
"""
|
|
209
263
|
Close session gracefully and record final metrics.
|
|
210
264
|
|
|
@@ -233,76 +287,75 @@ def handle_session_end(context: HookContext) -> dict:
|
|
|
233
287
|
session = context.session_manager.get_active_session()
|
|
234
288
|
if not session:
|
|
235
289
|
context.log("debug", "No active session to close")
|
|
236
|
-
|
|
290
|
+
else:
|
|
291
|
+
# Capture handoff context if provided
|
|
292
|
+
handoff_notes = context.hook_input.get("handoff_notes") or os.environ.get(
|
|
293
|
+
"HTMLGRAPH_HANDOFF_NOTES"
|
|
294
|
+
)
|
|
295
|
+
recommended_next = context.hook_input.get(
|
|
296
|
+
"recommended_next"
|
|
297
|
+
) or os.environ.get("HTMLGRAPH_HANDOFF_RECOMMEND")
|
|
298
|
+
blockers_raw = context.hook_input.get("blockers") or os.environ.get(
|
|
299
|
+
"HTMLGRAPH_HANDOFF_BLOCKERS"
|
|
300
|
+
)
|
|
237
301
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
"HTMLGRAPH_HANDOFF_RECOMMEND"
|
|
244
|
-
)
|
|
245
|
-
blockers_raw = context.hook_input.get("blockers") or os.environ.get(
|
|
246
|
-
"HTMLGRAPH_HANDOFF_BLOCKERS"
|
|
247
|
-
)
|
|
302
|
+
blockers = None
|
|
303
|
+
if isinstance(blockers_raw, str):
|
|
304
|
+
blockers = [b.strip() for b in blockers_raw.split(",") if b.strip()]
|
|
305
|
+
elif isinstance(blockers_raw, list):
|
|
306
|
+
blockers = [str(b).strip() for b in blockers_raw if str(b).strip()]
|
|
248
307
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
308
|
+
if handoff_notes or recommended_next or blockers:
|
|
309
|
+
try:
|
|
310
|
+
context.session_manager.set_session_handoff(
|
|
311
|
+
session_id=session.id,
|
|
312
|
+
handoff_notes=handoff_notes,
|
|
313
|
+
recommended_next=recommended_next,
|
|
314
|
+
blockers=blockers,
|
|
315
|
+
)
|
|
316
|
+
context.log("info", "Session handoff recorded")
|
|
317
|
+
except Exception as e:
|
|
318
|
+
context.log("warning", f"Could not set handoff: {e}")
|
|
319
|
+
output["status"] = "partial"
|
|
320
|
+
|
|
321
|
+
# Link transcript if external session ID provided
|
|
322
|
+
external_session_id = context.hook_input.get(
|
|
323
|
+
"session_id"
|
|
324
|
+
) or os.environ.get("CLAUDE_SESSION_ID")
|
|
325
|
+
if external_session_id:
|
|
326
|
+
try:
|
|
327
|
+
from htmlgraph.transcript import TranscriptReader
|
|
328
|
+
|
|
329
|
+
reader = TranscriptReader()
|
|
330
|
+
transcript = reader.read_session(external_session_id)
|
|
331
|
+
if transcript:
|
|
332
|
+
context.session_manager.link_transcript(
|
|
333
|
+
session_id=session.id,
|
|
334
|
+
transcript_id=external_session_id,
|
|
335
|
+
transcript_path=str(transcript.path),
|
|
336
|
+
git_branch=transcript.git_branch
|
|
337
|
+
if hasattr(transcript, "git_branch")
|
|
338
|
+
else None,
|
|
339
|
+
)
|
|
340
|
+
context.log("info", "Transcript linked to session")
|
|
341
|
+
except ImportError:
|
|
342
|
+
context.log("debug", "Transcript reader not available")
|
|
343
|
+
except Exception as e:
|
|
344
|
+
context.log("warning", f"Could not link transcript: {e}")
|
|
345
|
+
output["status"] = "partial"
|
|
254
346
|
|
|
255
|
-
|
|
347
|
+
# Record session end activity
|
|
256
348
|
try:
|
|
257
|
-
context.session_manager.
|
|
349
|
+
context.session_manager.track_activity(
|
|
258
350
|
session_id=session.id,
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
blockers=blockers,
|
|
351
|
+
tool="SessionEnd",
|
|
352
|
+
summary="Session ended",
|
|
262
353
|
)
|
|
263
|
-
context.log("info", "Session handoff recorded")
|
|
264
354
|
except Exception as e:
|
|
265
|
-
context.log("warning", f"Could not
|
|
355
|
+
context.log("warning", f"Could not track session end: {e}")
|
|
266
356
|
output["status"] = "partial"
|
|
267
357
|
|
|
268
|
-
|
|
269
|
-
external_session_id = context.hook_input.get("session_id") or os.environ.get(
|
|
270
|
-
"CLAUDE_SESSION_ID"
|
|
271
|
-
)
|
|
272
|
-
if external_session_id:
|
|
273
|
-
try:
|
|
274
|
-
from htmlgraph.transcript import TranscriptReader
|
|
275
|
-
|
|
276
|
-
reader = TranscriptReader()
|
|
277
|
-
transcript = reader.read_session(external_session_id)
|
|
278
|
-
if transcript:
|
|
279
|
-
context.session_manager.link_transcript(
|
|
280
|
-
session_id=session.id,
|
|
281
|
-
transcript_id=external_session_id,
|
|
282
|
-
transcript_path=str(transcript.path),
|
|
283
|
-
git_branch=transcript.git_branch
|
|
284
|
-
if hasattr(transcript, "git_branch")
|
|
285
|
-
else None,
|
|
286
|
-
)
|
|
287
|
-
context.log("info", "Transcript linked to session")
|
|
288
|
-
except ImportError:
|
|
289
|
-
context.log("debug", "Transcript reader not available")
|
|
290
|
-
except Exception as e:
|
|
291
|
-
context.log("warning", f"Could not link transcript: {e}")
|
|
292
|
-
output["status"] = "partial"
|
|
293
|
-
|
|
294
|
-
# Record session end activity
|
|
295
|
-
try:
|
|
296
|
-
context.session_manager.track_activity(
|
|
297
|
-
session_id=session.id,
|
|
298
|
-
tool="SessionEnd",
|
|
299
|
-
summary="Session ended",
|
|
300
|
-
)
|
|
301
|
-
except Exception as e:
|
|
302
|
-
context.log("warning", f"Could not track session end: {e}")
|
|
303
|
-
output["status"] = "partial"
|
|
304
|
-
|
|
305
|
-
context.log("info", f"Session closed: {session.id}")
|
|
358
|
+
context.log("info", f"Session closed: {session.id}")
|
|
306
359
|
|
|
307
360
|
except ImportError:
|
|
308
361
|
context.log("error", "SessionManager not available")
|
|
@@ -507,7 +560,8 @@ def _get_latest_pypi_version() -> str | None:
|
|
|
507
560
|
)
|
|
508
561
|
with urllib.request.urlopen(req, timeout=5) as response:
|
|
509
562
|
data = json.loads(response.read().decode())
|
|
510
|
-
|
|
563
|
+
version: str | None = data.get("info", {}).get("version")
|
|
564
|
+
return version
|
|
511
565
|
except Exception:
|
|
512
566
|
return None
|
|
513
567
|
|
|
@@ -565,13 +619,13 @@ def _cleanup_temp_files(graph_dir: Path) -> None:
|
|
|
565
619
|
logger.debug(f"Could not clean up {path}: {e}")
|
|
566
620
|
else:
|
|
567
621
|
# Handle single file
|
|
568
|
-
|
|
622
|
+
file_path: Path = graph_dir / pattern
|
|
569
623
|
try:
|
|
570
|
-
if
|
|
571
|
-
|
|
572
|
-
logger.debug(f"Cleaned up: {
|
|
624
|
+
if file_path.exists():
|
|
625
|
+
file_path.unlink()
|
|
626
|
+
logger.debug(f"Cleaned up: {file_path}")
|
|
573
627
|
except Exception as e:
|
|
574
|
-
logger.debug(f"Could not clean up {
|
|
628
|
+
logger.debug(f"Could not clean up {file_path}: {e}")
|
|
575
629
|
|
|
576
630
|
|
|
577
631
|
__all__ = [
|
htmlgraph/hooks/state_manager.py
CHANGED
|
@@ -25,6 +25,7 @@ import os
|
|
|
25
25
|
import tempfile
|
|
26
26
|
from datetime import datetime, timedelta
|
|
27
27
|
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
28
29
|
|
|
29
30
|
logger = logging.getLogger(__name__)
|
|
30
31
|
|
|
@@ -61,7 +62,7 @@ class ParentActivityTracker:
|
|
|
61
62
|
"""Ensure .htmlgraph directory exists."""
|
|
62
63
|
self.graph_dir.mkdir(parents=True, exist_ok=True)
|
|
63
64
|
|
|
64
|
-
def load(self, max_age_minutes: int = 5) -> dict:
|
|
65
|
+
def load(self, max_age_minutes: int = 5) -> dict[str, Any]:
|
|
65
66
|
"""
|
|
66
67
|
Load parent activity state.
|
|
67
68
|
|
|
@@ -334,7 +335,7 @@ class DriftQueueManager:
|
|
|
334
335
|
"""Ensure .htmlgraph directory exists."""
|
|
335
336
|
self.graph_dir.mkdir(parents=True, exist_ok=True)
|
|
336
337
|
|
|
337
|
-
def load(self, max_age_hours: int = 48) -> dict:
|
|
338
|
+
def load(self, max_age_hours: int = 48) -> dict[str, Any]:
|
|
338
339
|
"""
|
|
339
340
|
Load drift queue and filter by age.
|
|
340
341
|
|
|
@@ -393,7 +394,7 @@ class DriftQueueManager:
|
|
|
393
394
|
logger.warning(f"Error loading drift queue: {e}")
|
|
394
395
|
return {"activities": [], "last_classification": None}
|
|
395
396
|
|
|
396
|
-
def save(self, queue: dict) -> None:
|
|
397
|
+
def save(self, queue: dict[str, Any]) -> None:
|
|
397
398
|
"""
|
|
398
399
|
Save drift queue to file.
|
|
399
400
|
|
|
@@ -425,7 +426,9 @@ class DriftQueueManager:
|
|
|
425
426
|
except Exception as e:
|
|
426
427
|
logger.error(f"Unexpected error saving drift queue: {e}")
|
|
427
428
|
|
|
428
|
-
def add_activity(
|
|
429
|
+
def add_activity(
|
|
430
|
+
self, activity: dict[str, Any], timestamp: datetime | None = None
|
|
431
|
+
) -> None:
|
|
429
432
|
"""
|
|
430
433
|
Add activity to drift queue.
|
|
431
434
|
|
htmlgraph/hooks/validator.py
CHANGED
|
@@ -79,7 +79,7 @@ def load_tool_history() -> list[dict]:
|
|
|
79
79
|
data = json.loads(TOOL_HISTORY_FILE.read_text())
|
|
80
80
|
|
|
81
81
|
# Handle both formats: {"history": [...]} and [...] (legacy)
|
|
82
|
-
if isinstance(data, dict):
|
|
82
|
+
if isinstance(data, dict): # type: ignore[arg-type]
|
|
83
83
|
data = data.get("history", [])
|
|
84
84
|
|
|
85
85
|
# Filter to last hour only
|
|
@@ -149,7 +149,7 @@ def detect_optimal_pattern(tool: str, history: list[dict]) -> str | None:
|
|
|
149
149
|
return OPTIMAL_PATTERNS.get(pair)
|
|
150
150
|
|
|
151
151
|
|
|
152
|
-
def get_pattern_guidance(tool: str, history: list[dict]) -> dict:
|
|
152
|
+
def get_pattern_guidance(tool: str, history: list[dict]) -> dict[str, Any]:
|
|
153
153
|
"""Get guidance based on tool patterns."""
|
|
154
154
|
# Check for anti-patterns first
|
|
155
155
|
anti_pattern = detect_anti_pattern(tool, history)
|
|
@@ -192,7 +192,7 @@ def get_session_health_hint(history: list[dict]) -> str | None:
|
|
|
192
192
|
return None
|
|
193
193
|
|
|
194
194
|
|
|
195
|
-
def load_validation_config() -> dict:
|
|
195
|
+
def load_validation_config() -> dict[str, Any]:
|
|
196
196
|
"""Load validation config with defaults."""
|
|
197
197
|
config_path = (
|
|
198
198
|
Path(__file__).parent.parent.parent.parent.parent
|
|
@@ -218,7 +218,9 @@ def load_validation_config() -> dict:
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
|
|
221
|
-
def is_always_allowed(
|
|
221
|
+
def is_always_allowed(
|
|
222
|
+
tool: str, params: dict[str, Any], config: dict[str, Any]
|
|
223
|
+
) -> bool:
|
|
222
224
|
"""Check if tool is always allowed (read-only operations)."""
|
|
223
225
|
# Always-allow tools
|
|
224
226
|
if tool in config.get("always_allow", {}).get("tools", []):
|
|
@@ -234,7 +236,7 @@ def is_always_allowed(tool: str, params: dict, config: dict) -> bool:
|
|
|
234
236
|
return False
|
|
235
237
|
|
|
236
238
|
|
|
237
|
-
def is_direct_htmlgraph_write(tool: str, params: dict) -> tuple[bool, str]:
|
|
239
|
+
def is_direct_htmlgraph_write(tool: str, params: dict[str, Any]) -> tuple[bool, str]:
|
|
238
240
|
"""Check if attempting direct write to .htmlgraph/ (always denied)."""
|
|
239
241
|
if tool not in ["Write", "Edit", "Delete", "NotebookEdit"]:
|
|
240
242
|
return False, ""
|
|
@@ -246,7 +248,7 @@ def is_direct_htmlgraph_write(tool: str, params: dict) -> tuple[bool, str]:
|
|
|
246
248
|
return False, ""
|
|
247
249
|
|
|
248
250
|
|
|
249
|
-
def is_sdk_command(tool: str, params: dict, config: dict) -> bool:
|
|
251
|
+
def is_sdk_command(tool: str, params: dict[str, Any], config: dict[str, Any]) -> bool:
|
|
250
252
|
"""Check if Bash command is an SDK command."""
|
|
251
253
|
if tool != "Bash":
|
|
252
254
|
return False
|
|
@@ -259,7 +261,9 @@ def is_sdk_command(tool: str, params: dict, config: dict) -> bool:
|
|
|
259
261
|
return False
|
|
260
262
|
|
|
261
263
|
|
|
262
|
-
def is_code_operation(
|
|
264
|
+
def is_code_operation(
|
|
265
|
+
tool: str, params: dict[str, Any], config: dict[str, Any]
|
|
266
|
+
) -> bool:
|
|
263
267
|
"""Check if operation modifies code."""
|
|
264
268
|
# Direct file operations
|
|
265
269
|
if tool in config.get("code_operations", {}).get("tools", []):
|
|
@@ -288,7 +292,7 @@ def get_active_work_item() -> dict | None:
|
|
|
288
292
|
return None
|
|
289
293
|
|
|
290
294
|
|
|
291
|
-
def check_orchestrator_violation(tool: str, params: dict) -> dict | None:
|
|
295
|
+
def check_orchestrator_violation(tool: str, params: dict[str, Any]) -> dict | None:
|
|
292
296
|
"""
|
|
293
297
|
Check if operation violates orchestrator mode rules.
|
|
294
298
|
|
|
@@ -363,8 +367,8 @@ def check_orchestrator_violation(tool: str, params: dict) -> dict | None:
|
|
|
363
367
|
|
|
364
368
|
|
|
365
369
|
def validate_tool_call(
|
|
366
|
-
tool: str, params: dict, config: dict, history: list[dict]
|
|
367
|
-
) -> dict:
|
|
370
|
+
tool: str, params: dict[str, Any], config: dict[str, Any], history: list[dict]
|
|
371
|
+
) -> dict[str, Any]:
|
|
368
372
|
"""
|
|
369
373
|
Validate tool call and return GUIDANCE with active learning.
|
|
370
374
|
|
|
@@ -375,7 +379,7 @@ def validate_tool_call(
|
|
|
375
379
|
history: Tool usage history (from load_tool_history())
|
|
376
380
|
|
|
377
381
|
Returns:
|
|
378
|
-
dict: {"decision": "allow" | "block", "guidance": "...", "suggestion": "...", ...}
|
|
382
|
+
dict[str, Any]: {"decision": "allow" | "block", "guidance": "...", "suggestion": "...", ...}
|
|
379
383
|
All operations are ALLOWED unless blocked for safety reasons.
|
|
380
384
|
|
|
381
385
|
Example:
|