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.
Files changed (32) hide show
  1. htmlgraph/__init__.py +1 -1
  2. htmlgraph/api/main.py +193 -45
  3. htmlgraph/api/templates/dashboard.html +11 -0
  4. htmlgraph/api/templates/partials/activity-feed.html +458 -8
  5. htmlgraph/dashboard.html +41 -0
  6. htmlgraph/db/schema.py +254 -4
  7. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  8. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  9. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  10. htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
  11. htmlgraph/hooks/concurrent_sessions.py +208 -0
  12. htmlgraph/hooks/context.py +57 -10
  13. htmlgraph/hooks/drift_handler.py +24 -20
  14. htmlgraph/hooks/event_tracker.py +204 -177
  15. htmlgraph/hooks/orchestrator.py +6 -4
  16. htmlgraph/hooks/orchestrator_reflector.py +4 -4
  17. htmlgraph/hooks/pretooluse.py +3 -6
  18. htmlgraph/hooks/prompt_analyzer.py +14 -25
  19. htmlgraph/hooks/session_handler.py +123 -69
  20. htmlgraph/hooks/state_manager.py +7 -4
  21. htmlgraph/hooks/validator.py +15 -11
  22. htmlgraph/orchestration/headless_spawner.py +322 -15
  23. htmlgraph/orchestration/live_events.py +377 -0
  24. {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/dashboard.html +41 -0
  25. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/METADATA +1 -1
  26. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/RECORD +32 -27
  27. {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/styles.css +0 -0
  28. {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  29. {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  30. {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  31. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/WHEEL +0 -0
  32. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/entry_points.txt +0 -0
@@ -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 load_user_query_event
256
+ from htmlgraph.hooks.event_tracker import get_parent_user_query
260
257
 
261
- user_query_event_id = load_user_query_event(graph_dir, session_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 pathlib import Path
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) -> Optional[dict[str, Any]]:
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
- return work_item if isinstance(work_item, dict) else None
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: Optional[dict[str, Any]], prompt: str
310
- ) -> Optional[str]:
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) -> Optional[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. The event ID is saved to a
530
- session-scoped file for retrieval by PreToolUse hook.
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
- return output
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
- # Capture handoff context if provided
239
- handoff_notes = context.hook_input.get("handoff_notes") or os.environ.get(
240
- "HTMLGRAPH_HANDOFF_NOTES"
241
- )
242
- recommended_next = context.hook_input.get("recommended_next") or os.environ.get(
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
- blockers = None
250
- if isinstance(blockers_raw, str):
251
- blockers = [b.strip() for b in blockers_raw.split(",") if b.strip()]
252
- elif isinstance(blockers_raw, list):
253
- blockers = [str(b).strip() for b in blockers_raw if str(b).strip()]
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
- if handoff_notes or recommended_next or blockers:
347
+ # Record session end activity
256
348
  try:
257
- context.session_manager.set_session_handoff(
349
+ context.session_manager.track_activity(
258
350
  session_id=session.id,
259
- handoff_notes=handoff_notes,
260
- recommended_next=recommended_next,
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 set handoff: {e}")
355
+ context.log("warning", f"Could not track session end: {e}")
266
356
  output["status"] = "partial"
267
357
 
268
- # Link transcript if external session ID provided
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
- return data.get("info", {}).get("version")
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
- path = graph_dir / pattern
622
+ file_path: Path = graph_dir / pattern
569
623
  try:
570
- if path.exists():
571
- path.unlink()
572
- logger.debug(f"Cleaned up: {path}")
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 {path}: {e}")
628
+ logger.debug(f"Could not clean up {file_path}: {e}")
575
629
 
576
630
 
577
631
  __all__ = [
@@ -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(self, activity: dict, timestamp: datetime | None = None) -> None:
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
 
@@ -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(tool: str, params: dict, config: dict) -> bool:
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(tool: str, params: dict, config: dict) -> bool:
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: