htmlgraph 0.26.4__py3-none-any.whl → 0.26.6__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 (69) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +1 -1
  2. htmlgraph/__init__.py +1 -1
  3. htmlgraph/api/main.py +50 -10
  4. htmlgraph/api/templates/dashboard-redesign.html +608 -54
  5. htmlgraph/api/templates/partials/activity-feed.html +21 -0
  6. htmlgraph/api/templates/partials/features.html +81 -12
  7. htmlgraph/api/templates/partials/orchestration.html +35 -0
  8. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  9. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  10. htmlgraph/cli/__init__.py +42 -0
  11. htmlgraph/cli/__main__.py +6 -0
  12. htmlgraph/cli/analytics.py +939 -0
  13. htmlgraph/cli/base.py +660 -0
  14. htmlgraph/cli/constants.py +206 -0
  15. htmlgraph/cli/core.py +856 -0
  16. htmlgraph/cli/main.py +143 -0
  17. htmlgraph/cli/models.py +462 -0
  18. htmlgraph/cli/templates/__init__.py +1 -0
  19. htmlgraph/cli/templates/cost_dashboard.py +398 -0
  20. htmlgraph/cli/work/__init__.py +159 -0
  21. htmlgraph/cli/work/features.py +567 -0
  22. htmlgraph/cli/work/orchestration.py +675 -0
  23. htmlgraph/cli/work/sessions.py +465 -0
  24. htmlgraph/cli/work/tracks.py +485 -0
  25. htmlgraph/dashboard.html +6414 -634
  26. htmlgraph/db/schema.py +8 -3
  27. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +20 -13
  28. htmlgraph/docs/README.md +2 -3
  29. htmlgraph/hooks/event_tracker.py +189 -35
  30. htmlgraph/hooks/git_commands.py +175 -0
  31. htmlgraph/hooks/orchestrator.py +137 -71
  32. htmlgraph/hooks/orchestrator_reflector.py +23 -0
  33. htmlgraph/hooks/pretooluse.py +29 -6
  34. htmlgraph/hooks/session_handler.py +28 -0
  35. htmlgraph/hooks/session_summary.py +391 -0
  36. htmlgraph/hooks/subagent_detection.py +202 -0
  37. htmlgraph/hooks/validator.py +192 -79
  38. htmlgraph/operations/__init__.py +18 -0
  39. htmlgraph/operations/initialization.py +596 -0
  40. htmlgraph/operations/initialization.py.backup +228 -0
  41. htmlgraph/orchestration/__init__.py +16 -1
  42. htmlgraph/orchestration/claude_launcher.py +185 -0
  43. htmlgraph/orchestration/command_builder.py +71 -0
  44. htmlgraph/orchestration/headless_spawner.py +72 -1332
  45. htmlgraph/orchestration/plugin_manager.py +136 -0
  46. htmlgraph/orchestration/prompts.py +137 -0
  47. htmlgraph/orchestration/spawners/__init__.py +16 -0
  48. htmlgraph/orchestration/spawners/base.py +194 -0
  49. htmlgraph/orchestration/spawners/claude.py +170 -0
  50. htmlgraph/orchestration/spawners/codex.py +442 -0
  51. htmlgraph/orchestration/spawners/copilot.py +299 -0
  52. htmlgraph/orchestration/spawners/gemini.py +478 -0
  53. htmlgraph/orchestration/subprocess_runner.py +33 -0
  54. htmlgraph/orchestration.md +563 -0
  55. htmlgraph/orchestrator-system-prompt-optimized.txt +620 -55
  56. htmlgraph/orchestrator_config.py +357 -0
  57. htmlgraph/orchestrator_mode.py +45 -12
  58. htmlgraph/transcript.py +16 -4
  59. htmlgraph-0.26.6.data/data/htmlgraph/dashboard.html +6592 -0
  60. {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/METADATA +1 -1
  61. {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/RECORD +67 -33
  62. {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/entry_points.txt +1 -1
  63. htmlgraph/cli.py +0 -7256
  64. htmlgraph-0.26.4.data/data/htmlgraph/dashboard.html +0 -812
  65. {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/styles.css +0 -0
  66. {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  67. {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  68. {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  69. {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/WHEEL +0 -0
@@ -0,0 +1,391 @@
1
+ """
2
+ Session Summary Module - CIGS Integration
3
+
4
+ Generates comprehensive session summaries with CIGS analytics at session end.
5
+ This module is loaded by the Stop hook.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+
16
+ def _resolve_project_dir(cwd: str | None = None) -> str:
17
+ """Resolve project directory (git root or cwd)."""
18
+ env_dir = os.environ.get("CLAUDE_PROJECT_DIR")
19
+ if env_dir:
20
+ return env_dir
21
+ start_dir = cwd or os.getcwd()
22
+ try:
23
+ result = subprocess.run(
24
+ ["git", "rev-parse", "--show-toplevel"],
25
+ capture_output=True,
26
+ text=True,
27
+ cwd=start_dir,
28
+ timeout=5,
29
+ )
30
+ if result.returncode == 0:
31
+ return result.stdout.strip()
32
+ except Exception:
33
+ pass
34
+ return start_dir
35
+
36
+
37
+ def _bootstrap_pythonpath(project_dir: str) -> None:
38
+ """Add local src/python to PYTHONPATH for CIGS imports."""
39
+ repo_src = Path(project_dir) / "src" / "python"
40
+ if repo_src.exists():
41
+ sys.path.insert(0, str(repo_src))
42
+
43
+
44
+ # Try to import CIGS modules
45
+ try:
46
+ project_dir_for_import = _resolve_project_dir()
47
+ _bootstrap_pythonpath(project_dir_for_import)
48
+
49
+ from htmlgraph.cigs.autonomy import AutonomyRecommender
50
+ from htmlgraph.cigs.cost import CostCalculator
51
+ from htmlgraph.cigs.patterns import PatternDetector
52
+ from htmlgraph.cigs.tracker import ViolationTracker
53
+
54
+ CIGS_AVAILABLE = True
55
+ except Exception:
56
+ CIGS_AVAILABLE = False
57
+
58
+
59
+ class CIGSSessionSummarizer:
60
+ """
61
+ Generate comprehensive session summary with CIGS analytics.
62
+
63
+ Implements section 2.5 of CIGS design document:
64
+ - Load session violations from ViolationTracker
65
+ - Analyze session patterns using PatternDetector
66
+ - Calculate session costs
67
+ - Generate autonomy recommendation for next session
68
+ - Build comprehensive session summary
69
+ - Persist summary to .htmlgraph/cigs/session-summaries/{session_id}.json
70
+ """
71
+
72
+ def __init__(self, graph_dir: Path):
73
+ """Initialize session summarizer.
74
+
75
+ Args:
76
+ graph_dir: Path to .htmlgraph directory
77
+ """
78
+ if not CIGS_AVAILABLE:
79
+ raise RuntimeError("CIGS modules not available")
80
+
81
+ self.graph_dir = Path(graph_dir)
82
+ self.cigs_dir = self.graph_dir / "cigs"
83
+ self.summaries_dir = self.cigs_dir / "session-summaries"
84
+
85
+ # Ensure directories exist
86
+ self.summaries_dir.mkdir(parents=True, exist_ok=True)
87
+
88
+ # Initialize CIGS components
89
+ self.tracker = ViolationTracker(graph_dir)
90
+ self.pattern_detector = PatternDetector()
91
+ self.cost_calculator = CostCalculator()
92
+ self.autonomy_recommender = AutonomyRecommender()
93
+
94
+ def summarize(self, session_id: str | None = None) -> dict:
95
+ """
96
+ Generate comprehensive session summary.
97
+
98
+ Args:
99
+ session_id: Session ID (defaults to current/active session)
100
+
101
+ Returns:
102
+ Hook response with session summary
103
+ """
104
+ # Get session violations
105
+ violations = self.tracker.get_session_violations(session_id)
106
+
107
+ # Detect patterns from recent violations
108
+ patterns = self._detect_patterns(violations.violations)
109
+
110
+ # Calculate cost metrics
111
+ costs = self._calculate_costs(violations)
112
+
113
+ # Generate autonomy recommendation for next session
114
+ autonomy_rec = self.autonomy_recommender.recommend(
115
+ violations=violations,
116
+ patterns=patterns,
117
+ compliance_history=self._get_compliance_history(),
118
+ )
119
+
120
+ # Build summary text
121
+ summary_text = self._build_summary_text(
122
+ violations, patterns, costs, autonomy_rec
123
+ )
124
+
125
+ # Persist summary for future reference
126
+ self._persist_summary(
127
+ violations.session_id,
128
+ {
129
+ "session_id": violations.session_id,
130
+ "violations": violations.to_dict(),
131
+ "patterns": [p.to_dict() for p in patterns],
132
+ "costs": costs,
133
+ "autonomy_recommendation": autonomy_rec.to_dict(),
134
+ },
135
+ )
136
+
137
+ return {
138
+ "hookSpecificOutput": {
139
+ "hookEventName": "Stop",
140
+ "additionalContext": summary_text,
141
+ }
142
+ }
143
+
144
+ def _detect_patterns(self, violation_records: list) -> list:
145
+ """
146
+ Detect behavioral patterns from violation records.
147
+
148
+ Args:
149
+ violation_records: List of ViolationRecord instances
150
+
151
+ Returns:
152
+ List of detected PatternRecord instances
153
+ """
154
+ if not violation_records:
155
+ return []
156
+
157
+ # Convert violations to tool history format
158
+ history: list[dict[str, Any]] = []
159
+ for v in violation_records:
160
+ history.append(
161
+ {
162
+ "tool": v.tool,
163
+ "command": v.tool_params.get("command", ""),
164
+ "file_path": v.tool_params.get("file_path", ""),
165
+ "prompt": v.tool_params.get("prompt", ""),
166
+ "timestamp": v.timestamp,
167
+ }
168
+ )
169
+
170
+ # Detect all patterns
171
+ patterns = self.pattern_detector.detect_all_patterns(history)
172
+ return patterns # type: ignore[no-any-return]
173
+
174
+ def _calculate_costs(self, violations: Any) -> dict:
175
+ """
176
+ Calculate cost metrics from violations.
177
+
178
+ Args:
179
+ violations: SessionViolationSummary
180
+
181
+ Returns:
182
+ Dictionary with cost metrics
183
+ """
184
+ total_tokens = sum(v.actual_cost_tokens for v in violations.violations)
185
+ optimal_tokens = sum(v.optimal_cost_tokens for v in violations.violations)
186
+ waste_tokens = violations.total_waste_tokens
187
+
188
+ if total_tokens > 0:
189
+ waste_percentage = (waste_tokens / total_tokens) * 100
190
+ efficiency_score = (optimal_tokens / total_tokens) * 100
191
+ else:
192
+ waste_percentage = 0.0
193
+ efficiency_score = 100.0
194
+
195
+ return {
196
+ "total_tokens": total_tokens,
197
+ "optimal_tokens": optimal_tokens,
198
+ "waste_tokens": waste_tokens,
199
+ "waste_percentage": waste_percentage,
200
+ "efficiency_score": efficiency_score,
201
+ }
202
+
203
+ def _get_compliance_history(self) -> list[float]:
204
+ """
205
+ Get compliance history from last 5 sessions.
206
+
207
+ Returns:
208
+ List of compliance rates (0.0-1.0)
209
+ """
210
+ # Get recent violations (last 5 sessions)
211
+ recent = self.tracker.get_recent_violations(sessions=5)
212
+
213
+ # Group by session and calculate compliance rates
214
+ sessions: dict[str, list] = {}
215
+ for v in recent:
216
+ if v.session_id not in sessions:
217
+ sessions[v.session_id] = []
218
+ sessions[v.session_id].append(v)
219
+
220
+ # Calculate compliance rate per session
221
+ compliance_rates = []
222
+ for session_id, session_violations in sessions.items():
223
+ total_violations = len(session_violations)
224
+ # Compliance rate: 1.0 = no violations, decreases with more violations
225
+ compliance_rate = max(0.0, 1.0 - (total_violations / 5.0))
226
+ compliance_rates.append(compliance_rate)
227
+
228
+ return compliance_rates[-5:] if compliance_rates else [1.0]
229
+
230
+ def _build_summary_text(
231
+ self, violations: Any, patterns: Any, costs: Any, autonomy_rec: Any
232
+ ) -> str:
233
+ """
234
+ Build human-readable session summary.
235
+
236
+ Args:
237
+ violations: SessionViolationSummary
238
+ patterns: List of PatternRecord instances
239
+ costs: Cost metrics dictionary
240
+ autonomy_rec: AutonomyLevel recommendation
241
+
242
+ Returns:
243
+ Formatted markdown summary
244
+ """
245
+ # Compliance rate
246
+ compliance_pct = violations.compliance_rate * 100
247
+
248
+ # Circuit breaker status
249
+ breaker_status = (
250
+ "🚨 TRIGGERED" if violations.circuit_breaker_triggered else "✅ OK"
251
+ )
252
+
253
+ # Format violations by type
254
+ violations_detail = ""
255
+ if violations.violations_by_type:
256
+ for vtype, count in violations.violations_by_type.items():
257
+ violations_detail += f" - {vtype}: {count}\n"
258
+ else:
259
+ violations_detail = " - No violations detected\n"
260
+
261
+ # Format detected patterns
262
+ patterns_text = self._format_patterns(patterns)
263
+
264
+ # Format anti-patterns
265
+ anti_patterns_text = self._format_anti_patterns(patterns)
266
+
267
+ # Build summary
268
+ summary = f"""## 📊 CIGS Session Summary
269
+
270
+ ### Delegation Metrics
271
+ - **Compliance Rate:** {compliance_pct:.0f}%
272
+ - **Violations:** {violations.total_violations} (circuit breaker threshold: 3)
273
+ - **Circuit Breaker:** {breaker_status}
274
+
275
+ ### Violation Breakdown
276
+ {violations_detail}
277
+
278
+ ### Cost Analysis
279
+ - **Total Context Used:** {costs["total_tokens"]} tokens
280
+ - **Estimated Waste:** {costs["waste_tokens"]} tokens ({costs["waste_percentage"]:.1f}%)
281
+ - **Optimal Path Cost:** {costs["optimal_tokens"]} tokens
282
+ - **Efficiency Score:** {costs["efficiency_score"]:.0f}/100
283
+
284
+ {patterns_text}
285
+
286
+ {anti_patterns_text}
287
+
288
+ ### Autonomy Recommendation
289
+ **Next Session Level:** {autonomy_rec.level.upper()}
290
+ **Messaging Intensity:** {autonomy_rec.messaging_intensity}
291
+ **Enforcement Mode:** {autonomy_rec.enforcement_mode}
292
+
293
+ **Reason:** {autonomy_rec.reason}
294
+
295
+ ### Learning Applied
296
+ - ✅ Violation patterns added to detection model
297
+ - ✅ Cost predictions updated with actual session data
298
+ - ✅ Messaging intensity adjusted for next session: {autonomy_rec.messaging_intensity}
299
+ - ✅ Session summary persisted to `.htmlgraph/cigs/session-summaries/`
300
+
301
+ ---
302
+
303
+ **Next Steps:**
304
+ 1. Review detected anti-patterns (if any) and adjust workflow
305
+ 2. Your autonomy level for next session: **{autonomy_rec.level.upper()}**
306
+ 3. Guidance intensity: **{autonomy_rec.messaging_intensity}**
307
+ """
308
+
309
+ return summary
310
+
311
+ def _format_patterns(self, patterns: list) -> str:
312
+ """Format detected good patterns."""
313
+ good_patterns = [p for p in patterns if p.pattern_type == "good-pattern"]
314
+
315
+ if not good_patterns:
316
+ return "### Detected Patterns\n- No significant patterns detected"
317
+
318
+ text = "### Detected Patterns\n"
319
+ for p in good_patterns:
320
+ text += f"- ✅ **{p.name}**: {p.description}\n"
321
+ text += f" - Occurrences: {p.occurrence_count}\n"
322
+
323
+ return text
324
+
325
+ def _format_anti_patterns(self, patterns: list) -> str:
326
+ """Format detected anti-patterns with remediation."""
327
+ anti_patterns = [p for p in patterns if p.pattern_type == "anti-pattern"]
328
+
329
+ if not anti_patterns:
330
+ return "### Anti-Patterns Identified\n- ✅ No anti-patterns detected"
331
+
332
+ text = "### Anti-Patterns Identified\n"
333
+ for p in anti_patterns:
334
+ text += f"- ⚠️ **{p.name}**: {p.description}\n"
335
+ text += f" - Occurrences: {p.occurrence_count}\n"
336
+ if p.correct_approach:
337
+ text += f" - **Correct Approach:** {p.correct_approach}\n"
338
+ if p.delegation_suggestion:
339
+ text += f" - **Suggested Delegation:** {p.delegation_suggestion}\n"
340
+
341
+ return text
342
+
343
+ def _persist_summary(self, session_id: str, summary_data: dict) -> None:
344
+ """
345
+ Persist session summary to file for future reference.
346
+
347
+ Args:
348
+ session_id: Session identifier
349
+ summary_data: Summary dictionary to persist
350
+ """
351
+ try:
352
+ summary_file = self.summaries_dir / f"{session_id}.json"
353
+ with open(summary_file, "w") as f:
354
+ json.dump(summary_data, f, indent=2, default=str)
355
+ except Exception as e:
356
+ print(f"Warning: Failed to persist summary: {e}", file=sys.stderr)
357
+
358
+
359
+ def main() -> None:
360
+ """Hook entry point for script wrapper."""
361
+ # Check if tracking is disabled
362
+ if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
363
+ print(json.dumps({"continue": True}))
364
+ sys.exit(0)
365
+
366
+ try:
367
+ hook_input = json.load(sys.stdin)
368
+ except json.JSONDecodeError:
369
+ hook_input = {}
370
+
371
+ session_id = hook_input.get("session_id") or os.environ.get("CLAUDE_SESSION_ID")
372
+ cwd = hook_input.get("cwd")
373
+ project_dir = _resolve_project_dir(cwd if cwd else None)
374
+ graph_dir = Path(project_dir) / ".htmlgraph"
375
+
376
+ # Check if CIGS is enabled (disabled by default for now)
377
+ cigs_enabled = os.environ.get("HTMLGRAPH_CIGS_ENABLED") == "1"
378
+
379
+ if not cigs_enabled or not CIGS_AVAILABLE:
380
+ # CIGS not enabled or not available, just output empty response
381
+ print(json.dumps({"continue": True}))
382
+ return
383
+
384
+ # Generate CIGS session summary
385
+ try:
386
+ summarizer = CIGSSessionSummarizer(graph_dir)
387
+ result = summarizer.summarize(session_id)
388
+ print(json.dumps(result))
389
+ except Exception as e:
390
+ print(f"Warning: Could not generate CIGS summary: {e}", file=sys.stderr)
391
+ print(json.dumps({"continue": True}))
@@ -0,0 +1,202 @@
1
+ """
2
+ Subagent Context Detection for Orchestrator Mode
3
+
4
+ This module provides utilities to detect when code is executing within a
5
+ delegated subagent context (spawned via Task() tool) vs. the main orchestrator.
6
+
7
+ Key Problem:
8
+ PreToolUse hooks (orchestrator-enforce.py, validator.py) enforce delegation
9
+ rules that block direct tool use in strict mode. However, subagents MUST use
10
+ tools directly - that's the delegated work. Without context detection, subagents
11
+ get blocked, making strict orchestrator mode unusable.
12
+
13
+ Solution:
14
+ Detect subagent context via multiple signals:
15
+ 1. Environment variables set by Claude Code when spawning Task() subagents
16
+ 2. Session state markers in database
17
+ 3. Parent session tracking
18
+
19
+ Usage:
20
+ from htmlgraph.hooks.subagent_detection import is_subagent_context
21
+
22
+ if is_subagent_context():
23
+ # Allow direct tool use - this is delegated work
24
+ return {"continue": True}
25
+ else:
26
+ # Enforce delegation rules - this is orchestrator
27
+ return enforce_delegation(tool, params)
28
+ """
29
+
30
+ import os
31
+ from pathlib import Path
32
+ from typing import Any
33
+
34
+
35
+ def is_subagent_context() -> bool:
36
+ """
37
+ Check if we're executing within a delegated subagent (spawned via Task()).
38
+
39
+ Detection Strategy (in priority order):
40
+ 1. CLAUDE_SUBAGENT_ID environment variable (set by Task() spawner)
41
+ 2. CLAUDE_PARENT_SESSION_ID environment variable (set by Task() spawner)
42
+ 3. Session state marker in database (is_subagent flag)
43
+ 4. Active session has parent_session_id set
44
+
45
+ Returns:
46
+ True if executing in subagent context, False if orchestrator context
47
+
48
+ Note:
49
+ - Gracefully degrades if detection mechanisms fail (returns False)
50
+ - False positives are safe (allow direct tool use)
51
+ - False negatives would break subagents (must be avoided)
52
+ """
53
+ # Check 1: Direct environment variable from Task() spawner
54
+ if os.getenv("CLAUDE_SUBAGENT_ID"):
55
+ return True
56
+
57
+ # Check 2: Parent session ID indicates we're a subagent
58
+ if os.getenv("CLAUDE_PARENT_SESSION_ID"):
59
+ return True
60
+
61
+ # Check 3: Session state marker in database
62
+ try:
63
+ session_state = _load_session_state()
64
+ if session_state.get("is_subagent", False):
65
+ return True
66
+
67
+ # Check 4: Session has parent_session_id
68
+ if session_state.get("parent_session_id"):
69
+ return True
70
+ except Exception:
71
+ # Graceful degradation - if we can't check, assume NOT subagent
72
+ # This is safe because it only allows stricter enforcement
73
+ pass
74
+
75
+ # Check 5: Query database for active session with parent_session_id
76
+ try:
77
+ if _has_parent_session_in_db():
78
+ return True
79
+ except Exception:
80
+ pass
81
+
82
+ return False
83
+
84
+
85
+ def _load_session_state() -> dict[str, Any]:
86
+ """
87
+ Load session state from .htmlgraph/session-state.json.
88
+
89
+ Returns:
90
+ Session state dict, or empty dict if not found
91
+ """
92
+ try:
93
+ # Find .htmlgraph directory
94
+ graph_dir = _find_graph_dir()
95
+ if not graph_dir:
96
+ return {}
97
+
98
+ state_file = graph_dir / "session-state.json"
99
+ if not state_file.exists():
100
+ return {}
101
+
102
+ import json
103
+
104
+ result: dict[str, Any] = json.loads(state_file.read_text())
105
+ return result
106
+ except Exception:
107
+ return {}
108
+
109
+
110
+ def _has_parent_session_in_db() -> bool:
111
+ """
112
+ Check if current session has a parent_session_id in database.
113
+
114
+ Returns:
115
+ True if session is a subagent (has parent), False otherwise
116
+ """
117
+ try:
118
+ graph_dir = _find_graph_dir()
119
+ if not graph_dir:
120
+ return False
121
+
122
+ db_path = graph_dir / "htmlgraph.db"
123
+ if not db_path.exists():
124
+ return False
125
+
126
+ import sqlite3
127
+
128
+ # Get current session ID from environment or database
129
+
130
+ # We need hook_input to create context, but we don't have it here
131
+ # Fall back to environment check
132
+ session_id = os.getenv("HTMLGRAPH_SESSION_ID") or os.getenv("CLAUDE_SESSION_ID")
133
+
134
+ if not session_id:
135
+ # Try to get most recent session from database
136
+ conn = sqlite3.connect(str(db_path), timeout=1.0)
137
+ cursor = conn.cursor()
138
+ cursor.execute("""
139
+ SELECT session_id FROM sessions
140
+ WHERE status = 'active'
141
+ ORDER BY created_at DESC
142
+ LIMIT 1
143
+ """)
144
+ row = cursor.fetchone()
145
+ if row:
146
+ session_id = row[0]
147
+ conn.close()
148
+
149
+ if not session_id:
150
+ return False
151
+
152
+ # Check if this session has a parent
153
+ conn = sqlite3.connect(str(db_path), timeout=1.0)
154
+ cursor = conn.cursor()
155
+ cursor.execute(
156
+ """
157
+ SELECT parent_session_id FROM sessions
158
+ WHERE session_id = ?
159
+ """,
160
+ (session_id,),
161
+ )
162
+ row = cursor.fetchone()
163
+ conn.close()
164
+
165
+ if row and row[0]:
166
+ return True
167
+
168
+ except Exception:
169
+ pass
170
+
171
+ return False
172
+
173
+
174
+ def _find_graph_dir() -> Path | None:
175
+ """
176
+ Find .htmlgraph directory starting from current working directory.
177
+
178
+ Returns:
179
+ Path to .htmlgraph directory, or None if not found
180
+ """
181
+ try:
182
+ cwd = Path.cwd()
183
+ graph_dir = cwd / ".htmlgraph"
184
+
185
+ if graph_dir.exists():
186
+ return graph_dir
187
+
188
+ # Search up to 3 parent directories
189
+ for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
190
+ candidate = parent / ".htmlgraph"
191
+ if candidate.exists():
192
+ return candidate
193
+
194
+ except Exception:
195
+ pass
196
+
197
+ return None
198
+
199
+
200
+ __all__ = [
201
+ "is_subagent_context",
202
+ ]