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.
- htmlgraph/.htmlgraph/.session-warning-state.json +1 -1
- htmlgraph/__init__.py +1 -1
- htmlgraph/api/main.py +50 -10
- htmlgraph/api/templates/dashboard-redesign.html +608 -54
- htmlgraph/api/templates/partials/activity-feed.html +21 -0
- htmlgraph/api/templates/partials/features.html +81 -12
- htmlgraph/api/templates/partials/orchestration.html +35 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +939 -0
- htmlgraph/cli/base.py +660 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +856 -0
- htmlgraph/cli/main.py +143 -0
- htmlgraph/cli/models.py +462 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +398 -0
- htmlgraph/cli/work/__init__.py +159 -0
- htmlgraph/cli/work/features.py +567 -0
- htmlgraph/cli/work/orchestration.py +675 -0
- htmlgraph/cli/work/sessions.py +465 -0
- htmlgraph/cli/work/tracks.py +485 -0
- htmlgraph/dashboard.html +6414 -634
- htmlgraph/db/schema.py +8 -3
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +20 -13
- htmlgraph/docs/README.md +2 -3
- htmlgraph/hooks/event_tracker.py +189 -35
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/orchestrator.py +137 -71
- htmlgraph/hooks/orchestrator_reflector.py +23 -0
- htmlgraph/hooks/pretooluse.py +29 -6
- htmlgraph/hooks/session_handler.py +28 -0
- htmlgraph/hooks/session_summary.py +391 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/validator.py +192 -79
- htmlgraph/operations/__init__.py +18 -0
- htmlgraph/operations/initialization.py +596 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/orchestration/__init__.py +16 -1
- htmlgraph/orchestration/claude_launcher.py +185 -0
- htmlgraph/orchestration/command_builder.py +71 -0
- htmlgraph/orchestration/headless_spawner.py +72 -1332
- htmlgraph/orchestration/plugin_manager.py +136 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +170 -0
- htmlgraph/orchestration/spawners/codex.py +442 -0
- htmlgraph/orchestration/spawners/copilot.py +299 -0
- htmlgraph/orchestration/spawners/gemini.py +478 -0
- htmlgraph/orchestration/subprocess_runner.py +33 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +620 -55
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +45 -12
- htmlgraph/transcript.py +16 -4
- htmlgraph-0.26.6.data/data/htmlgraph/dashboard.html +6592 -0
- {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/METADATA +1 -1
- {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/RECORD +67 -33
- {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -7256
- htmlgraph-0.26.4.data/data/htmlgraph/dashboard.html +0 -812
- {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/WHEEL +0 -0
htmlgraph/db/schema.py
CHANGED
|
@@ -96,6 +96,7 @@ class HtmlGraphDB:
|
|
|
96
96
|
|
|
97
97
|
# Add missing columns with defaults
|
|
98
98
|
migrations = [
|
|
99
|
+
("feature_id", "TEXT"),
|
|
99
100
|
("subagent_type", "TEXT"),
|
|
100
101
|
("child_spike_count", "INTEGER DEFAULT 0"),
|
|
101
102
|
("cost_tokens", "INTEGER DEFAULT 0"),
|
|
@@ -216,6 +217,7 @@ class HtmlGraphDB:
|
|
|
216
217
|
output_summary TEXT,
|
|
217
218
|
context JSON,
|
|
218
219
|
session_id TEXT NOT NULL,
|
|
220
|
+
feature_id TEXT,
|
|
219
221
|
parent_agent_id TEXT,
|
|
220
222
|
parent_event_id TEXT,
|
|
221
223
|
subagent_type TEXT,
|
|
@@ -227,7 +229,8 @@ class HtmlGraphDB:
|
|
|
227
229
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
228
230
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
229
231
|
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE ON UPDATE CASCADE,
|
|
230
|
-
FOREIGN KEY (parent_event_id) REFERENCES agent_events(event_id) ON DELETE SET NULL ON UPDATE CASCADE
|
|
232
|
+
FOREIGN KEY (parent_event_id) REFERENCES agent_events(event_id) ON DELETE SET NULL ON UPDATE CASCADE,
|
|
233
|
+
FOREIGN KEY (feature_id) REFERENCES features(id) ON DELETE SET NULL ON UPDATE CASCADE
|
|
231
234
|
)
|
|
232
235
|
""")
|
|
233
236
|
|
|
@@ -513,6 +516,7 @@ class HtmlGraphDB:
|
|
|
513
516
|
execution_duration_seconds: float = 0.0,
|
|
514
517
|
subagent_type: str | None = None,
|
|
515
518
|
model: str | None = None,
|
|
519
|
+
feature_id: str | None = None,
|
|
516
520
|
) -> bool:
|
|
517
521
|
"""
|
|
518
522
|
Insert an agent event into the database.
|
|
@@ -553,16 +557,17 @@ class HtmlGraphDB:
|
|
|
553
557
|
cursor.execute(
|
|
554
558
|
"""
|
|
555
559
|
INSERT INTO agent_events
|
|
556
|
-
(event_id, agent_id, event_type, session_id, tool_name,
|
|
560
|
+
(event_id, agent_id, event_type, session_id, feature_id, tool_name,
|
|
557
561
|
input_summary, output_summary, context, parent_agent_id,
|
|
558
562
|
parent_event_id, cost_tokens, execution_duration_seconds, subagent_type, model)
|
|
559
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
563
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
560
564
|
""",
|
|
561
565
|
(
|
|
562
566
|
event_id,
|
|
563
567
|
agent_id,
|
|
564
568
|
event_type,
|
|
565
569
|
session_id,
|
|
570
|
+
feature_id,
|
|
566
571
|
tool_name,
|
|
567
572
|
input_summary,
|
|
568
573
|
output_summary,
|
|
@@ -32,8 +32,8 @@ result = spawner.spawn_claude(
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
result = spawner.spawn_gemini(
|
|
35
|
-
prompt="Analyze codebase for performance issues"
|
|
36
|
-
model=
|
|
35
|
+
prompt="Analyze codebase for performance issues"
|
|
36
|
+
# model=None (default) - uses latest Gemini models including Gemini 3 preview
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
result = spawner.spawn_codex(
|
|
@@ -88,7 +88,7 @@ else:
|
|
|
88
88
|
```python
|
|
89
89
|
result = spawner.spawn_gemini(
|
|
90
90
|
prompt="Analyze this code for security issues",
|
|
91
|
-
model=
|
|
91
|
+
# model=None (default) - RECOMMENDED: uses latest Gemini models
|
|
92
92
|
temperature=0.7, # Creativity (0-1)
|
|
93
93
|
output_json=False
|
|
94
94
|
)
|
|
@@ -99,10 +99,17 @@ else:
|
|
|
99
99
|
print(f"Error: {result.error}")
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
**Gemini
|
|
103
|
-
- `gemini-2.
|
|
104
|
-
-
|
|
105
|
-
|
|
102
|
+
**Gemini Model Selection:**
|
|
103
|
+
- `None` (default) - **RECOMMENDED**: CLI chooses best available model (gemini-2.5-flash-lite, gemini-3-flash-preview)
|
|
104
|
+
- Explicit model specification is DISCOURAGED - older models may fail with newer CLI versions
|
|
105
|
+
|
|
106
|
+
**Available models (if you must specify):**
|
|
107
|
+
- `gemini-2.5-flash-lite` - Fast, efficient
|
|
108
|
+
- `gemini-3-flash-preview` - Gemini 3 with enhanced capabilities
|
|
109
|
+
- `gemini-2.5-pro` - More capable, slower
|
|
110
|
+
|
|
111
|
+
**DEPRECATED models (may cause errors):**
|
|
112
|
+
- `gemini-2.0-flash`, `gemini-1.5-pro`, `gemini-1.5-flash` - Use `None` instead
|
|
106
113
|
|
|
107
114
|
---
|
|
108
115
|
|
|
@@ -174,8 +181,8 @@ if model == "gpt-4-turbo":
|
|
|
174
181
|
result = spawner.spawn_codex(prompt="...", model=model)
|
|
175
182
|
elif model == "claude-opus":
|
|
176
183
|
result = spawner.spawn_claude(prompt="...", model=model)
|
|
177
|
-
elif model
|
|
178
|
-
result = spawner.spawn_gemini(prompt="..."
|
|
184
|
+
elif model.startswith("gemini"):
|
|
185
|
+
result = spawner.spawn_gemini(prompt="...") # model=None recommended
|
|
179
186
|
```
|
|
180
187
|
|
|
181
188
|
**Task Types:**
|
|
@@ -427,8 +434,8 @@ Decompose complex tasks into subtasks:
|
|
|
427
434
|
```python
|
|
428
435
|
# Step 1: Analysis (cheap/fast)
|
|
429
436
|
analysis_result = spawner.spawn_gemini(
|
|
430
|
-
prompt="Analyze the codebase structure"
|
|
431
|
-
model=
|
|
437
|
+
prompt="Analyze the codebase structure"
|
|
438
|
+
# model=None - uses latest Gemini models (Gemini 3 preview)
|
|
432
439
|
)
|
|
433
440
|
|
|
434
441
|
# Step 2: Design (more capable)
|
|
@@ -559,8 +566,8 @@ def get_consensus(prompt, num_agents=3):
|
|
|
559
566
|
prompt=prompt, model="claude-opus"
|
|
560
567
|
)))
|
|
561
568
|
|
|
562
|
-
results.append(("gemini
|
|
563
|
-
prompt=prompt
|
|
569
|
+
results.append(("gemini", spawner.spawn_gemini(
|
|
570
|
+
prompt=prompt # model=None uses latest Gemini models
|
|
564
571
|
)))
|
|
565
572
|
|
|
566
573
|
results.append(("gpt-4-turbo", spawner.spawn_codex(
|
htmlgraph/docs/README.md
CHANGED
|
@@ -198,10 +198,9 @@ result = spawner.spawn_claude(
|
|
|
198
198
|
approval="auto"
|
|
199
199
|
)
|
|
200
200
|
|
|
201
|
-
# Spawn Gemini
|
|
201
|
+
# Spawn Gemini (model=None uses latest models including Gemini 3 preview)
|
|
202
202
|
result = spawner.spawn_gemini(
|
|
203
|
-
prompt="Analyze performance"
|
|
204
|
-
model="gemini-2.0-flash"
|
|
203
|
+
prompt="Analyze performance"
|
|
205
204
|
)
|
|
206
205
|
|
|
207
206
|
# Spawn Codex
|
htmlgraph/hooks/event_tracker.py
CHANGED
|
@@ -446,6 +446,77 @@ def detect_agent_from_environment() -> tuple[str, str | None]:
|
|
|
446
446
|
return agent_id, model_name
|
|
447
447
|
|
|
448
448
|
|
|
449
|
+
def detect_subagent_context_from_database(
|
|
450
|
+
db: HtmlGraphDB, current_session_id: str
|
|
451
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
452
|
+
"""
|
|
453
|
+
Detect if we're in a subagent context by checking for active task_delegation events.
|
|
454
|
+
|
|
455
|
+
This is the DATABASE-BASED approach to subagent detection, which is necessary because
|
|
456
|
+
environment variables set by PreToolUse hooks in the parent process do NOT propagate
|
|
457
|
+
to subagent processes spawned by Claude Code's Task() tool.
|
|
458
|
+
|
|
459
|
+
Strategy:
|
|
460
|
+
1. Query for task_delegation events with status='started' (not yet completed)
|
|
461
|
+
2. If found within the last 5 minutes, we're likely in a subagent context
|
|
462
|
+
3. Return the subagent_type and parent session info
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
db: HtmlGraphDB instance
|
|
466
|
+
current_session_id: The session_id from hook_input (Claude Code's session ID)
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
Tuple of (subagent_type, parent_session_id, parent_event_id)
|
|
470
|
+
All None if not in subagent context
|
|
471
|
+
"""
|
|
472
|
+
try:
|
|
473
|
+
if db.connection is None:
|
|
474
|
+
return None, None, None
|
|
475
|
+
|
|
476
|
+
cursor = db.connection.cursor()
|
|
477
|
+
|
|
478
|
+
# Query for active task_delegation events (started but not completed)
|
|
479
|
+
# that were created in the last 5 minutes
|
|
480
|
+
# Exclude events that belong to the current session (those are parent context, not subagent)
|
|
481
|
+
cursor.execute(
|
|
482
|
+
"""
|
|
483
|
+
SELECT event_id, session_id, subagent_type, timestamp
|
|
484
|
+
FROM agent_events
|
|
485
|
+
WHERE event_type = 'task_delegation'
|
|
486
|
+
AND status = 'started'
|
|
487
|
+
AND timestamp >= datetime('now', '-5 minutes')
|
|
488
|
+
AND session_id != ?
|
|
489
|
+
ORDER BY timestamp DESC
|
|
490
|
+
LIMIT 1
|
|
491
|
+
""",
|
|
492
|
+
(current_session_id,),
|
|
493
|
+
)
|
|
494
|
+
row = cursor.fetchone()
|
|
495
|
+
|
|
496
|
+
if row:
|
|
497
|
+
parent_event_id = row[0]
|
|
498
|
+
parent_session_id = row[1]
|
|
499
|
+
subagent_type = row[2] or "general-purpose"
|
|
500
|
+
|
|
501
|
+
print(
|
|
502
|
+
f"Debug: Detected subagent context from database: "
|
|
503
|
+
f"type={subagent_type}, parent_session={parent_session_id}, "
|
|
504
|
+
f"parent_event={parent_event_id}",
|
|
505
|
+
file=sys.stderr,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
return subagent_type, parent_session_id, parent_event_id
|
|
509
|
+
|
|
510
|
+
return None, None, None
|
|
511
|
+
|
|
512
|
+
except Exception as e:
|
|
513
|
+
print(
|
|
514
|
+
f"Debug: Error detecting subagent context from database: {e}",
|
|
515
|
+
file=sys.stderr,
|
|
516
|
+
)
|
|
517
|
+
return None, None, None
|
|
518
|
+
|
|
519
|
+
|
|
449
520
|
def extract_file_paths(tool_input: dict[str, Any], tool_name: str) -> list[str]:
|
|
450
521
|
"""Extract file paths from tool input based on tool type."""
|
|
451
522
|
paths = []
|
|
@@ -543,6 +614,7 @@ def record_event_to_sqlite(
|
|
|
543
614
|
agent_id: str | None = None,
|
|
544
615
|
subagent_type: str | None = None,
|
|
545
616
|
model: str | None = None,
|
|
617
|
+
feature_id: str | None = None,
|
|
546
618
|
) -> str | None:
|
|
547
619
|
"""
|
|
548
620
|
Record a tool call event to SQLite database for dashboard queries.
|
|
@@ -559,6 +631,7 @@ def record_event_to_sqlite(
|
|
|
559
631
|
agent_id: Agent identifier (optional)
|
|
560
632
|
subagent_type: Subagent type for Task delegations (optional)
|
|
561
633
|
model: Claude model name (e.g., claude-haiku, claude-opus) (optional)
|
|
634
|
+
feature_id: Feature ID for attribution (optional)
|
|
562
635
|
|
|
563
636
|
Returns:
|
|
564
637
|
event_id if successful, None otherwise
|
|
@@ -603,6 +676,7 @@ def record_event_to_sqlite(
|
|
|
603
676
|
cost_tokens=0,
|
|
604
677
|
subagent_type=subagent_type,
|
|
605
678
|
model=model,
|
|
679
|
+
feature_id=feature_id,
|
|
606
680
|
)
|
|
607
681
|
|
|
608
682
|
if success:
|
|
@@ -710,19 +784,43 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
710
784
|
detected_model = model_from_input
|
|
711
785
|
|
|
712
786
|
active_session = None
|
|
787
|
+
is_subagent_session = False
|
|
788
|
+
parent_event_id_for_session = None
|
|
789
|
+
|
|
790
|
+
# Get session_id from hook_input first (Claude Code provides this)
|
|
791
|
+
hook_session_id = hook_input.get("session_id") or hook_input.get("sessionId")
|
|
713
792
|
|
|
714
|
-
# Check if we're in a subagent context
|
|
715
|
-
#
|
|
716
|
-
# subagent events to the parent orchestrator session
|
|
793
|
+
# Check if we're in a subagent context using multiple methods:
|
|
794
|
+
# Method 1: Environment variables (works if Task() spawner sets them)
|
|
717
795
|
subagent_type = os.environ.get("HTMLGRAPH_SUBAGENT_TYPE")
|
|
718
796
|
parent_session_id = os.environ.get("HTMLGRAPH_PARENT_SESSION")
|
|
719
797
|
|
|
798
|
+
# Method 2: Database-based detection (CRITICAL for Claude Code Task() tool)
|
|
799
|
+
# Environment variables don't propagate to subagent processes, so we check
|
|
800
|
+
# the database for active task_delegation events
|
|
801
|
+
if not subagent_type and db and hook_session_id:
|
|
802
|
+
db_subagent_type, db_parent_session_id, db_parent_event_id = (
|
|
803
|
+
detect_subagent_context_from_database(db, hook_session_id)
|
|
804
|
+
)
|
|
805
|
+
if db_subagent_type:
|
|
806
|
+
subagent_type = db_subagent_type
|
|
807
|
+
parent_session_id = db_parent_session_id
|
|
808
|
+
parent_event_id_for_session = db_parent_event_id
|
|
809
|
+
print(
|
|
810
|
+
f"Debug: Using database-based subagent detection: "
|
|
811
|
+
f"type={subagent_type}, parent={parent_session_id}",
|
|
812
|
+
file=sys.stderr,
|
|
813
|
+
)
|
|
814
|
+
|
|
720
815
|
if subagent_type and parent_session_id:
|
|
721
|
-
# We're in a subagent
|
|
722
|
-
|
|
723
|
-
subagent_session_id = f"{parent_session_id}-{subagent_type}"
|
|
816
|
+
# We're in a subagent context
|
|
817
|
+
is_subagent_session = True
|
|
724
818
|
|
|
725
|
-
#
|
|
819
|
+
# Use Claude's session_id (hook_session_id) as the subagent session ID
|
|
820
|
+
# This ensures events are properly tracked to this session
|
|
821
|
+
subagent_session_id = hook_session_id or f"{parent_session_id}-{subagent_type}"
|
|
822
|
+
|
|
823
|
+
# Check if session already exists in our system
|
|
726
824
|
existing = manager.session_converter.load(subagent_session_id)
|
|
727
825
|
if existing:
|
|
728
826
|
active_session = existing
|
|
@@ -742,7 +840,7 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
742
840
|
)
|
|
743
841
|
print(
|
|
744
842
|
f"Debug: Created subagent session: {subagent_session_id} "
|
|
745
|
-
f"(parent: {parent_session_id})",
|
|
843
|
+
f"(parent: {parent_session_id}, is_subagent=True)",
|
|
746
844
|
file=sys.stderr,
|
|
747
845
|
)
|
|
748
846
|
except Exception as e:
|
|
@@ -755,18 +853,38 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
755
853
|
# Override detected agent for subagent context
|
|
756
854
|
detected_agent = f"{subagent_type}-spawner"
|
|
757
855
|
else:
|
|
758
|
-
# Normal orchestrator/parent context
|
|
759
|
-
|
|
760
|
-
if not
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
856
|
+
# Normal orchestrator/parent context
|
|
857
|
+
# CRITICAL: Use session_id from hook_input (Claude Code provides this)
|
|
858
|
+
# Only fall back to manager.get_active_session() if not in hook_input
|
|
859
|
+
if hook_session_id:
|
|
860
|
+
# Claude Code provided session_id - use it directly
|
|
861
|
+
# Check if session already exists
|
|
862
|
+
existing = manager.session_converter.load(hook_session_id)
|
|
863
|
+
if existing:
|
|
864
|
+
active_session = existing
|
|
865
|
+
else:
|
|
866
|
+
# Create new session with Claude's session_id
|
|
867
|
+
try:
|
|
868
|
+
active_session = manager.start_session(
|
|
869
|
+
session_id=hook_session_id,
|
|
870
|
+
agent=detected_agent,
|
|
871
|
+
title=f"Session {datetime.now().strftime('%Y-%m-%d %H:%M')}",
|
|
872
|
+
)
|
|
873
|
+
except Exception:
|
|
874
|
+
return {"continue": True}
|
|
875
|
+
else:
|
|
876
|
+
# Fallback: No session_id in hook_input - use global session cache
|
|
877
|
+
active_session = manager.get_active_session()
|
|
878
|
+
if not active_session:
|
|
879
|
+
# No active HtmlGraph session yet; start one
|
|
880
|
+
try:
|
|
881
|
+
active_session = manager.start_session(
|
|
882
|
+
session_id=None,
|
|
883
|
+
agent=detected_agent,
|
|
884
|
+
title=f"Session {datetime.now().strftime('%Y-%m-%d %H:%M')}",
|
|
885
|
+
)
|
|
886
|
+
except Exception:
|
|
887
|
+
return {"continue": True}
|
|
770
888
|
|
|
771
889
|
active_session_id = active_session.id
|
|
772
890
|
|
|
@@ -786,10 +904,13 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
786
904
|
except Exception:
|
|
787
905
|
return default
|
|
788
906
|
|
|
907
|
+
# Use is_subagent_session flag from our detection, not just from session object
|
|
789
908
|
is_subagent_raw = safe_getattr(active_session, "is_subagent", False)
|
|
790
|
-
|
|
909
|
+
is_subagent_from_obj = (
|
|
791
910
|
bool(is_subagent_raw) if isinstance(is_subagent_raw, bool) else False
|
|
792
911
|
)
|
|
912
|
+
# Prefer our detection (is_subagent_session) over object attribute
|
|
913
|
+
final_is_subagent = is_subagent_session or is_subagent_from_obj
|
|
793
914
|
|
|
794
915
|
transcript_id = safe_getattr(active_session, "transcript_id", None)
|
|
795
916
|
transcript_path = safe_getattr(active_session, "transcript_path", None)
|
|
@@ -799,14 +920,35 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
799
920
|
if transcript_path is not None and not isinstance(transcript_path, str):
|
|
800
921
|
transcript_path = None
|
|
801
922
|
|
|
923
|
+
# Get parent_session_id from our detection or from session object
|
|
924
|
+
final_parent_session_id = parent_session_id or safe_getattr(
|
|
925
|
+
active_session, "parent_session_id", None
|
|
926
|
+
)
|
|
927
|
+
if final_parent_session_id is not None and not isinstance(
|
|
928
|
+
final_parent_session_id, str
|
|
929
|
+
):
|
|
930
|
+
final_parent_session_id = None
|
|
931
|
+
|
|
802
932
|
db.insert_session(
|
|
803
933
|
session_id=active_session_id,
|
|
804
934
|
agent_assigned=safe_getattr(active_session, "agent", None)
|
|
805
935
|
or detected_agent,
|
|
806
|
-
|
|
936
|
+
parent_session_id=final_parent_session_id,
|
|
937
|
+
parent_event_id=parent_event_id_for_session,
|
|
938
|
+
is_subagent=final_is_subagent,
|
|
807
939
|
transcript_id=transcript_id,
|
|
808
940
|
transcript_path=transcript_path,
|
|
809
941
|
)
|
|
942
|
+
|
|
943
|
+
# Log subagent session creation for debugging
|
|
944
|
+
if final_is_subagent:
|
|
945
|
+
print(
|
|
946
|
+
f"Debug: Inserted subagent session to SQLite: "
|
|
947
|
+
f"session_id={active_session_id}, is_subagent=True, "
|
|
948
|
+
f"parent_session={final_parent_session_id}, "
|
|
949
|
+
f"parent_event={parent_event_id_for_session}",
|
|
950
|
+
file=sys.stderr,
|
|
951
|
+
)
|
|
810
952
|
except Exception as e:
|
|
811
953
|
# Session may already exist, that's OK - continue
|
|
812
954
|
print(
|
|
@@ -818,7 +960,7 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
818
960
|
if hook_type == "Stop":
|
|
819
961
|
# Session is ending - track stop event
|
|
820
962
|
try:
|
|
821
|
-
manager.track_activity(
|
|
963
|
+
result = manager.track_activity(
|
|
822
964
|
session_id=active_session_id, tool="Stop", summary="Agent stopped"
|
|
823
965
|
)
|
|
824
966
|
|
|
@@ -833,6 +975,7 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
833
975
|
is_error=False,
|
|
834
976
|
agent_id=detected_agent,
|
|
835
977
|
model=detected_model,
|
|
978
|
+
feature_id=result.feature_id if result else None,
|
|
836
979
|
)
|
|
837
980
|
except Exception as e:
|
|
838
981
|
print(f"Warning: Could not track stop: {e}", file=sys.stderr)
|
|
@@ -846,7 +989,7 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
846
989
|
preview += "..."
|
|
847
990
|
|
|
848
991
|
try:
|
|
849
|
-
manager.track_activity(
|
|
992
|
+
result = manager.track_activity(
|
|
850
993
|
session_id=active_session_id, tool="UserQuery", summary=f'"{preview}"'
|
|
851
994
|
)
|
|
852
995
|
|
|
@@ -863,6 +1006,7 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
863
1006
|
is_error=False,
|
|
864
1007
|
agent_id=detected_agent,
|
|
865
1008
|
model=detected_model,
|
|
1009
|
+
feature_id=result.feature_id if result else None,
|
|
866
1010
|
)
|
|
867
1011
|
|
|
868
1012
|
except Exception as e:
|
|
@@ -915,20 +1059,29 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
915
1059
|
warning_threshold = drift_settings.get("warning_threshold") or 0.7
|
|
916
1060
|
auto_classify_threshold = drift_settings.get("auto_classify_threshold") or 0.85
|
|
917
1061
|
|
|
918
|
-
# Determine parent activity context using
|
|
1062
|
+
# Determine parent activity context using multiple sources
|
|
919
1063
|
parent_activity_id = None
|
|
920
1064
|
|
|
921
|
-
#
|
|
1065
|
+
# Priority 1: If we're in a subagent context, use the parent event from
|
|
1066
|
+
# the task_delegation that spawned us (detected from database)
|
|
1067
|
+
if is_subagent_session and parent_event_id_for_session:
|
|
1068
|
+
parent_activity_id = parent_event_id_for_session
|
|
1069
|
+
print(
|
|
1070
|
+
f"Debug: Using parent_event from subagent detection: {parent_activity_id}",
|
|
1071
|
+
file=sys.stderr,
|
|
1072
|
+
)
|
|
1073
|
+
# Priority 2: Check environment variable for cross-process parent linking
|
|
922
1074
|
# This is set by PreToolUse hook when Task() spawns a subagent
|
|
923
|
-
|
|
924
|
-
"
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1075
|
+
else:
|
|
1076
|
+
env_parent = os.environ.get("HTMLGRAPH_PARENT_EVENT") or os.environ.get(
|
|
1077
|
+
"HTMLGRAPH_PARENT_QUERY_EVENT"
|
|
1078
|
+
)
|
|
1079
|
+
if env_parent:
|
|
1080
|
+
parent_activity_id = env_parent
|
|
1081
|
+
# Priority 3: Query database for most recent UserQuery event as parent
|
|
1082
|
+
# Database is the single source of truth for parent-child linking
|
|
1083
|
+
elif db:
|
|
1084
|
+
parent_activity_id = get_parent_user_query(db, active_session_id)
|
|
932
1085
|
|
|
933
1086
|
# Track the activity
|
|
934
1087
|
nudge = None
|
|
@@ -963,6 +1116,7 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
963
1116
|
agent_id=detected_agent,
|
|
964
1117
|
subagent_type=task_subagent_type,
|
|
965
1118
|
model=detected_model,
|
|
1119
|
+
feature_id=result.feature_id if result else None,
|
|
966
1120
|
)
|
|
967
1121
|
|
|
968
1122
|
# If this was a Task() delegation, also record to agent_collaboration
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared git command classification for hooks.
|
|
3
|
+
|
|
4
|
+
Provides consistent rules for which git operations are allowed vs require delegation.
|
|
5
|
+
Used by both validator.py and orchestrator.py to ensure consistent behavior.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
GitCommandType = Literal["read", "write", "unknown"]
|
|
11
|
+
|
|
12
|
+
# Read-only git commands (safe to allow)
|
|
13
|
+
GIT_READ_ONLY = {
|
|
14
|
+
"status",
|
|
15
|
+
"log",
|
|
16
|
+
"diff",
|
|
17
|
+
"show",
|
|
18
|
+
"branch", # When used with -l or --list or no args
|
|
19
|
+
"reflog",
|
|
20
|
+
"ls-files",
|
|
21
|
+
"ls-remote",
|
|
22
|
+
"rev-parse",
|
|
23
|
+
"describe",
|
|
24
|
+
"tag", # When used without -a/-d or with -l
|
|
25
|
+
"remote", # When used with -v or show
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Write operations (require delegation)
|
|
29
|
+
GIT_WRITE_OPS = {
|
|
30
|
+
"add",
|
|
31
|
+
"commit",
|
|
32
|
+
"push",
|
|
33
|
+
"pull",
|
|
34
|
+
"fetch",
|
|
35
|
+
"merge",
|
|
36
|
+
"rebase",
|
|
37
|
+
"cherry-pick",
|
|
38
|
+
"reset",
|
|
39
|
+
"checkout", # Can modify working tree
|
|
40
|
+
"switch",
|
|
41
|
+
"restore",
|
|
42
|
+
"rm",
|
|
43
|
+
"mv",
|
|
44
|
+
"clean",
|
|
45
|
+
"stash",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def classify_git_command(command: str) -> GitCommandType:
|
|
50
|
+
"""
|
|
51
|
+
Classify a git command as read, write, or unknown.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
command: Full command string (e.g., "git status" or "git add .")
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
"read", "write", or "unknown"
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
>>> classify_git_command("git status")
|
|
61
|
+
"read"
|
|
62
|
+
>>> classify_git_command("git commit -m 'msg'")
|
|
63
|
+
"write"
|
|
64
|
+
>>> classify_git_command("git log --oneline")
|
|
65
|
+
"read"
|
|
66
|
+
>>> classify_git_command("git add .")
|
|
67
|
+
"write"
|
|
68
|
+
"""
|
|
69
|
+
# Strip "git" prefix and get subcommand
|
|
70
|
+
parts = command.strip().split()
|
|
71
|
+
if not parts or parts[0] != "git":
|
|
72
|
+
return "unknown"
|
|
73
|
+
|
|
74
|
+
if len(parts) < 2:
|
|
75
|
+
return "unknown"
|
|
76
|
+
|
|
77
|
+
subcommand = parts[1]
|
|
78
|
+
|
|
79
|
+
# Check write operations first (more critical)
|
|
80
|
+
if subcommand in GIT_WRITE_OPS:
|
|
81
|
+
return "write"
|
|
82
|
+
|
|
83
|
+
# Special handling for branch (flag-based classification)
|
|
84
|
+
if subcommand == "branch":
|
|
85
|
+
# branch with -d or -D flags is write, otherwise read
|
|
86
|
+
if len(parts) > 2:
|
|
87
|
+
flags = " ".join(parts[2:])
|
|
88
|
+
if (
|
|
89
|
+
" -d " in flags
|
|
90
|
+
or " -D " in flags
|
|
91
|
+
or flags.startswith("-d ")
|
|
92
|
+
or flags.startswith("-D ")
|
|
93
|
+
):
|
|
94
|
+
return "write"
|
|
95
|
+
return "read"
|
|
96
|
+
|
|
97
|
+
# Special handling for tag (flag-based classification)
|
|
98
|
+
if subcommand == "tag":
|
|
99
|
+
# tag with -a (annotated) or -d (delete) flags is write, otherwise read
|
|
100
|
+
if len(parts) > 2:
|
|
101
|
+
flags = " ".join(parts[2:])
|
|
102
|
+
if (
|
|
103
|
+
" -a " in flags
|
|
104
|
+
or " -d " in flags
|
|
105
|
+
or flags.startswith("-a ")
|
|
106
|
+
or flags.startswith("-d ")
|
|
107
|
+
):
|
|
108
|
+
return "write"
|
|
109
|
+
return "read"
|
|
110
|
+
|
|
111
|
+
# Then check read-only
|
|
112
|
+
if subcommand in GIT_READ_ONLY:
|
|
113
|
+
return "read"
|
|
114
|
+
|
|
115
|
+
# Unknown git command
|
|
116
|
+
return "unknown"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def should_allow_git_command(command: str) -> bool:
|
|
120
|
+
"""
|
|
121
|
+
Check if a git command should be allowed without delegation.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
True if command is read-only (safe), False if write (delegate)
|
|
125
|
+
|
|
126
|
+
Examples:
|
|
127
|
+
>>> should_allow_git_command("git status")
|
|
128
|
+
True
|
|
129
|
+
>>> should_allow_git_command("git commit -m 'msg'")
|
|
130
|
+
False
|
|
131
|
+
>>> should_allow_git_command("git diff HEAD~1")
|
|
132
|
+
True
|
|
133
|
+
>>> should_allow_git_command("git push origin main")
|
|
134
|
+
False
|
|
135
|
+
"""
|
|
136
|
+
cmd_type = classify_git_command(command)
|
|
137
|
+
return cmd_type == "read"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_git_delegation_reason(command: str) -> str:
|
|
141
|
+
"""
|
|
142
|
+
Get delegation reason for git write operations.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
command: Git command that requires delegation
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Human-readable reason explaining why delegation is required
|
|
149
|
+
"""
|
|
150
|
+
parts = command.strip().split()
|
|
151
|
+
if len(parts) < 2:
|
|
152
|
+
return "Git write operations should be delegated to Skill('.claude-plugin:copilot')"
|
|
153
|
+
|
|
154
|
+
subcommand = parts[1]
|
|
155
|
+
|
|
156
|
+
if subcommand in ["commit", "add", "push"]:
|
|
157
|
+
return (
|
|
158
|
+
f"Git {subcommand} is a write operation and should be delegated to "
|
|
159
|
+
f"Skill('.claude-plugin:copilot') for proper Git workflow management"
|
|
160
|
+
)
|
|
161
|
+
elif subcommand in ["merge", "rebase", "cherry-pick"]:
|
|
162
|
+
return (
|
|
163
|
+
f"Git {subcommand} is a complex merge operation and should be delegated to "
|
|
164
|
+
f"Skill('.claude-plugin:copilot') for safe execution"
|
|
165
|
+
)
|
|
166
|
+
elif subcommand in ["reset", "checkout", "restore"]:
|
|
167
|
+
return (
|
|
168
|
+
f"Git {subcommand} can modify working tree and should be delegated to "
|
|
169
|
+
f"Skill('.claude-plugin:copilot') for safe execution"
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
return (
|
|
173
|
+
f"Git {subcommand} is a write operation and should be delegated to "
|
|
174
|
+
f"Skill('.claude-plugin:copilot')"
|
|
175
|
+
)
|