htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__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 +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +51 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/__init__.py +8 -1
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +67 -27
- htmlgraph/analytics_index.py +53 -20
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/base.py +57 -2
- htmlgraph/builders/bug.py +19 -3
- htmlgraph/builders/chore.py +19 -3
- htmlgraph/builders/epic.py +19 -3
- htmlgraph/builders/feature.py +27 -3
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +19 -3
- htmlgraph/builders/spike.py +29 -3
- htmlgraph/builders/track.py +42 -1
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +197 -14
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +13 -2
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +116 -7
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2246 -248
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +2 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +86 -37
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +67 -9
- 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/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +790 -99
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +327 -76
- htmlgraph/hooks/orchestrator_reflector.py +31 -4
- htmlgraph/hooks/post_tool_use_failure.py +32 -7
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +92 -19
- htmlgraph/hooks/pretooluse.py +527 -7
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +99 -4
- htmlgraph/hooks/validator.py +212 -91
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +125 -100
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +217 -18
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +115 -4
- htmlgraph/parallel.py +2 -1
- htmlgraph/parser.py +86 -6
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +2 -1
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +295 -107
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +285 -3
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +33 -1
- htmlgraph/track_manager.py +38 -0
- htmlgraph/transcript.py +18 -5
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -4839
- htmlgraph/sdk.py +0 -2359
- htmlgraph-0.20.1.dist-info/RECORD +0 -118
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pattern Detection for CIGS - Identify behavioral anti-patterns from tool usage history.
|
|
3
|
+
|
|
4
|
+
Detects anti-patterns that violate HtmlGraph delegation principles:
|
|
5
|
+
1. exploration_sequence: 3+ Read/Grep/Glob in sequence (should use spawn_gemini)
|
|
6
|
+
2. edit_without_test: Edit operations without subsequent test delegation
|
|
7
|
+
3. direct_git_commit: git commit via Bash instead of spawn_copilot
|
|
8
|
+
4. repeated_read_same_file: Same file read multiple times in short window
|
|
9
|
+
|
|
10
|
+
Reference: .htmlgraph/spikes/computational-imperative-guidance-system-design.md (Part 5, Section 5.3)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .models import PatternRecord
|
|
17
|
+
|
|
18
|
+
# Define exploration and implementation tool categories
|
|
19
|
+
EXPLORATION_TOOLS = {"Read", "Grep", "Glob"}
|
|
20
|
+
IMPLEMENTATION_TOOLS = {"Edit", "Write", "NotebookEdit"}
|
|
21
|
+
TESTING_TOOLS = {"Bash"} # pytest, npm test, yarn test
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class DetectionResult:
|
|
26
|
+
"""Result of pattern detection."""
|
|
27
|
+
|
|
28
|
+
pattern_type: str # "anti-pattern" or "good-pattern"
|
|
29
|
+
name: str
|
|
30
|
+
description: str
|
|
31
|
+
detected: bool
|
|
32
|
+
trigger_conditions: list[str] = field(default_factory=list)
|
|
33
|
+
example_sequence: list[str] = field(default_factory=list)
|
|
34
|
+
remediation: str | None = None
|
|
35
|
+
confidence: float = 0.0 # 0.0 to 1.0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PatternDetector:
|
|
39
|
+
"""
|
|
40
|
+
Detect behavioral patterns from tool usage history.
|
|
41
|
+
|
|
42
|
+
Uses a sliding window approach to identify anti-patterns that violate
|
|
43
|
+
HtmlGraph delegation principles.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
# Window size for analysis (last N tool calls)
|
|
47
|
+
DEFAULT_WINDOW_SIZE = 10
|
|
48
|
+
|
|
49
|
+
def __init__(self, window_size: int = DEFAULT_WINDOW_SIZE):
|
|
50
|
+
"""Initialize pattern detector.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
window_size: Number of recent tool calls to analyze
|
|
54
|
+
"""
|
|
55
|
+
self.window_size = window_size
|
|
56
|
+
self._anti_patterns = self._init_anti_patterns()
|
|
57
|
+
|
|
58
|
+
def _init_anti_patterns(self) -> dict[str, Any]:
|
|
59
|
+
"""Initialize anti-pattern definitions."""
|
|
60
|
+
return {
|
|
61
|
+
"exploration_sequence": {
|
|
62
|
+
"description": "Multiple exploration tools in sequence",
|
|
63
|
+
"trigger_conditions": [
|
|
64
|
+
"3+ exploration tools (Read/Grep/Glob) in last N calls",
|
|
65
|
+
"No delegation (spawn_gemini/Task) between them",
|
|
66
|
+
],
|
|
67
|
+
"remediation": "Use spawn_gemini() for comprehensive exploration",
|
|
68
|
+
"min_occurrences": 3,
|
|
69
|
+
"detector": self._detect_exploration_sequence,
|
|
70
|
+
},
|
|
71
|
+
"edit_without_test": {
|
|
72
|
+
"description": "Edit operations without subsequent test delegation",
|
|
73
|
+
"trigger_conditions": [
|
|
74
|
+
"Edit/Write operation detected",
|
|
75
|
+
"No Task() with 'test' in prompt within next 3 calls",
|
|
76
|
+
],
|
|
77
|
+
"remediation": "Include testing in Task() prompt for code changes",
|
|
78
|
+
"detector": self._detect_edit_without_test,
|
|
79
|
+
},
|
|
80
|
+
"direct_git_commit": {
|
|
81
|
+
"description": "Git commit executed directly instead of via spawn_copilot",
|
|
82
|
+
"trigger_conditions": [
|
|
83
|
+
"Bash tool with 'git commit' command",
|
|
84
|
+
"No spawn_copilot() delegation",
|
|
85
|
+
],
|
|
86
|
+
"remediation": "Use spawn_copilot() for git operations",
|
|
87
|
+
"detector": self._detect_direct_git_commit,
|
|
88
|
+
},
|
|
89
|
+
"repeated_read_same_file": {
|
|
90
|
+
"description": "Same file read multiple times in short window",
|
|
91
|
+
"trigger_conditions": [
|
|
92
|
+
"Same file_path in 2+ Read operations",
|
|
93
|
+
"Within last 10 tool calls",
|
|
94
|
+
"No delegation between reads",
|
|
95
|
+
],
|
|
96
|
+
"remediation": "Delegate to Explorer (spawn_gemini) for comprehensive file analysis",
|
|
97
|
+
"detector": self._detect_repeated_read_same_file,
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def detect_all_patterns(self, history: list[dict[str, Any]]) -> list[PatternRecord]:
|
|
102
|
+
"""
|
|
103
|
+
Detect all anti-patterns in tool usage history.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
history: List of tool call records with structure:
|
|
107
|
+
{
|
|
108
|
+
"tool": str,
|
|
109
|
+
"command": str (for Bash),
|
|
110
|
+
"file_path": str (for Read),
|
|
111
|
+
"prompt": str (for Task),
|
|
112
|
+
"timestamp": datetime,
|
|
113
|
+
...
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of detected PatternRecord instances
|
|
118
|
+
"""
|
|
119
|
+
detected = []
|
|
120
|
+
history_window = (
|
|
121
|
+
history[-self.window_size :] if len(history) > self.window_size else history
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
for pattern_name, pattern_def in self._anti_patterns.items():
|
|
125
|
+
detector_func: Any = pattern_def["detector"]
|
|
126
|
+
result = detector_func(history_window)
|
|
127
|
+
if result.detected:
|
|
128
|
+
detected.append(
|
|
129
|
+
PatternRecord(
|
|
130
|
+
id=f"pattern-{pattern_name}",
|
|
131
|
+
pattern_type="anti-pattern",
|
|
132
|
+
name=pattern_name,
|
|
133
|
+
description=result.description,
|
|
134
|
+
trigger_conditions=result.trigger_conditions,
|
|
135
|
+
example_sequence=result.example_sequence,
|
|
136
|
+
occurrence_count=1,
|
|
137
|
+
sessions_affected=[],
|
|
138
|
+
correct_approach=result.remediation,
|
|
139
|
+
delegation_suggestion=self._get_delegation_suggestion(
|
|
140
|
+
pattern_name
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return detected
|
|
146
|
+
|
|
147
|
+
def detect_pattern(
|
|
148
|
+
self, pattern_name: str, history: list[dict[str, Any]]
|
|
149
|
+
) -> DetectionResult:
|
|
150
|
+
"""
|
|
151
|
+
Detect a specific anti-pattern.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
pattern_name: Name of pattern to detect
|
|
155
|
+
history: Tool usage history
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
DetectionResult with detection details
|
|
159
|
+
"""
|
|
160
|
+
if pattern_name not in self._anti_patterns:
|
|
161
|
+
raise ValueError(f"Unknown pattern: {pattern_name}")
|
|
162
|
+
|
|
163
|
+
pattern_def = self._anti_patterns[pattern_name]
|
|
164
|
+
history_window = (
|
|
165
|
+
history[-self.window_size :] if len(history) > self.window_size else history
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
detector_func: Any = pattern_def["detector"]
|
|
169
|
+
return detector_func(history_window) # type: ignore[no-any-return]
|
|
170
|
+
|
|
171
|
+
def _detect_exploration_sequence(
|
|
172
|
+
self, history: list[dict[str, Any]]
|
|
173
|
+
) -> DetectionResult:
|
|
174
|
+
"""
|
|
175
|
+
Detect exploration_sequence anti-pattern.
|
|
176
|
+
|
|
177
|
+
Triggers when 3+ exploration tools (Read/Grep/Glob) appear in sequence
|
|
178
|
+
without delegation to spawn_gemini() or Task().
|
|
179
|
+
"""
|
|
180
|
+
pattern_name = "exploration_sequence"
|
|
181
|
+
pattern_def = self._anti_patterns[pattern_name]
|
|
182
|
+
|
|
183
|
+
if not history:
|
|
184
|
+
return DetectionResult(
|
|
185
|
+
pattern_type="anti-pattern",
|
|
186
|
+
name=pattern_name,
|
|
187
|
+
description=pattern_def["description"],
|
|
188
|
+
detected=False,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Count exploration tools in history
|
|
192
|
+
exploration_count = 0
|
|
193
|
+
exploration_tools_found = []
|
|
194
|
+
|
|
195
|
+
for call in history:
|
|
196
|
+
tool = call.get("tool", "")
|
|
197
|
+
|
|
198
|
+
if tool in EXPLORATION_TOOLS:
|
|
199
|
+
exploration_count += 1
|
|
200
|
+
exploration_tools_found.append(tool)
|
|
201
|
+
elif tool in {"Task", "Bash"} and "spawn_gemini" in call.get(
|
|
202
|
+
"prompt", ""
|
|
203
|
+
) + call.get("command", ""):
|
|
204
|
+
# Delegation found, reset
|
|
205
|
+
exploration_count = 0
|
|
206
|
+
exploration_tools_found = []
|
|
207
|
+
elif tool not in EXPLORATION_TOOLS:
|
|
208
|
+
# Non-exploration tool breaks the sequence (unless it's delegation)
|
|
209
|
+
if tool not in {"Task"} or not any(
|
|
210
|
+
d in call.get("prompt", "") for d in ["spawn_", "gemini"]
|
|
211
|
+
):
|
|
212
|
+
pass # Don't reset, continue counting
|
|
213
|
+
|
|
214
|
+
detected = exploration_count >= 3
|
|
215
|
+
confidence = min(1.0, exploration_count / 3) if detected else 0.0
|
|
216
|
+
|
|
217
|
+
return DetectionResult(
|
|
218
|
+
pattern_type="anti-pattern",
|
|
219
|
+
name=pattern_name,
|
|
220
|
+
description=pattern_def["description"],
|
|
221
|
+
detected=detected,
|
|
222
|
+
trigger_conditions=pattern_def["trigger_conditions"],
|
|
223
|
+
example_sequence=exploration_tools_found[-3:]
|
|
224
|
+
if len(exploration_tools_found) >= 3
|
|
225
|
+
else [],
|
|
226
|
+
remediation=pattern_def["remediation"],
|
|
227
|
+
confidence=confidence,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def _detect_edit_without_test(
|
|
231
|
+
self, history: list[dict[str, Any]]
|
|
232
|
+
) -> DetectionResult:
|
|
233
|
+
"""
|
|
234
|
+
Detect edit_without_test anti-pattern.
|
|
235
|
+
|
|
236
|
+
Triggers when Edit/Write operations exist without subsequent Task()
|
|
237
|
+
delegation containing test keywords within next 3 calls.
|
|
238
|
+
"""
|
|
239
|
+
pattern_name = "edit_without_test"
|
|
240
|
+
pattern_def = self._anti_patterns[pattern_name]
|
|
241
|
+
|
|
242
|
+
if not history:
|
|
243
|
+
return DetectionResult(
|
|
244
|
+
pattern_type="anti-pattern",
|
|
245
|
+
name=pattern_name,
|
|
246
|
+
description=pattern_def["description"],
|
|
247
|
+
detected=False,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
test_keywords = {
|
|
251
|
+
"test",
|
|
252
|
+
"pytest",
|
|
253
|
+
"unittest",
|
|
254
|
+
"vitest",
|
|
255
|
+
"jest",
|
|
256
|
+
"mocha",
|
|
257
|
+
"assert",
|
|
258
|
+
"verify",
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# Check for Edit/Write without subsequent test delegation
|
|
262
|
+
for i, call in enumerate(history):
|
|
263
|
+
tool = call.get("tool", "")
|
|
264
|
+
|
|
265
|
+
if tool in IMPLEMENTATION_TOOLS:
|
|
266
|
+
# Found an edit, check next 3 calls for test delegation
|
|
267
|
+
test_found = False
|
|
268
|
+
remaining_calls = history[i + 1 : i + 4]
|
|
269
|
+
|
|
270
|
+
for next_call in remaining_calls:
|
|
271
|
+
next_tool = next_call.get("tool", "")
|
|
272
|
+
prompt = next_call.get("prompt", "").lower()
|
|
273
|
+
|
|
274
|
+
# Check if this is a test delegation
|
|
275
|
+
if next_tool == "Task" and any(
|
|
276
|
+
kw in prompt for kw in test_keywords
|
|
277
|
+
):
|
|
278
|
+
test_found = True
|
|
279
|
+
break
|
|
280
|
+
|
|
281
|
+
if not test_found and len(history) > i + 1:
|
|
282
|
+
# Edit found without subsequent test delegation
|
|
283
|
+
example_seq = [call.get("tool", "")] + [
|
|
284
|
+
c.get("tool", "") for c in remaining_calls[:3]
|
|
285
|
+
]
|
|
286
|
+
return DetectionResult(
|
|
287
|
+
pattern_type="anti-pattern",
|
|
288
|
+
name=pattern_name,
|
|
289
|
+
description=pattern_def["description"],
|
|
290
|
+
detected=True,
|
|
291
|
+
trigger_conditions=pattern_def["trigger_conditions"],
|
|
292
|
+
example_sequence=example_seq,
|
|
293
|
+
remediation=pattern_def["remediation"],
|
|
294
|
+
confidence=0.8,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return DetectionResult(
|
|
298
|
+
pattern_type="anti-pattern",
|
|
299
|
+
name=pattern_name,
|
|
300
|
+
description=pattern_def["description"],
|
|
301
|
+
detected=False,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def _detect_direct_git_commit(
|
|
305
|
+
self, history: list[dict[str, Any]]
|
|
306
|
+
) -> DetectionResult:
|
|
307
|
+
"""
|
|
308
|
+
Detect direct_git_commit anti-pattern.
|
|
309
|
+
|
|
310
|
+
Triggers when git commit is executed via Bash directly instead of
|
|
311
|
+
delegating to spawn_copilot().
|
|
312
|
+
"""
|
|
313
|
+
pattern_name = "direct_git_commit"
|
|
314
|
+
pattern_def = self._anti_patterns[pattern_name]
|
|
315
|
+
|
|
316
|
+
if not history:
|
|
317
|
+
return DetectionResult(
|
|
318
|
+
pattern_type="anti-pattern",
|
|
319
|
+
name=pattern_name,
|
|
320
|
+
description=pattern_def["description"],
|
|
321
|
+
detected=False,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
git_commit_commands = [
|
|
325
|
+
"git commit",
|
|
326
|
+
"git push",
|
|
327
|
+
"git add",
|
|
328
|
+
"git merge",
|
|
329
|
+
"git rebase",
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
for i, call in enumerate(history):
|
|
333
|
+
tool = call.get("tool", "")
|
|
334
|
+
|
|
335
|
+
if tool == "Bash":
|
|
336
|
+
command = call.get("command", "").lower()
|
|
337
|
+
|
|
338
|
+
# Check if this is a git commit operation
|
|
339
|
+
is_git_commit = any(cmd in command for cmd in git_commit_commands)
|
|
340
|
+
|
|
341
|
+
if is_git_commit:
|
|
342
|
+
# Check if this was preceded by spawn_copilot delegation
|
|
343
|
+
was_delegated = False
|
|
344
|
+
if i > 0:
|
|
345
|
+
prev_call = history[i - 1]
|
|
346
|
+
if prev_call.get("tool", "") == "Task":
|
|
347
|
+
prompt = prev_call.get("prompt", "").lower()
|
|
348
|
+
if "copilot" in prompt or "git" in prompt:
|
|
349
|
+
was_delegated = True
|
|
350
|
+
|
|
351
|
+
if not was_delegated:
|
|
352
|
+
return DetectionResult(
|
|
353
|
+
pattern_type="anti-pattern",
|
|
354
|
+
name=pattern_name,
|
|
355
|
+
description=pattern_def["description"],
|
|
356
|
+
detected=True,
|
|
357
|
+
trigger_conditions=pattern_def["trigger_conditions"],
|
|
358
|
+
example_sequence=["Bash", command.split()[0:2]],
|
|
359
|
+
remediation=pattern_def["remediation"],
|
|
360
|
+
confidence=0.95,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
return DetectionResult(
|
|
364
|
+
pattern_type="anti-pattern",
|
|
365
|
+
name=pattern_name,
|
|
366
|
+
description=pattern_def["description"],
|
|
367
|
+
detected=False,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
def _detect_repeated_read_same_file(
|
|
371
|
+
self, history: list[dict[str, Any]]
|
|
372
|
+
) -> DetectionResult:
|
|
373
|
+
"""
|
|
374
|
+
Detect repeated_read_same_file anti-pattern.
|
|
375
|
+
|
|
376
|
+
Triggers when the same file is read multiple times within the window
|
|
377
|
+
without delegation to explore comprehensively.
|
|
378
|
+
"""
|
|
379
|
+
pattern_name = "repeated_read_same_file"
|
|
380
|
+
pattern_def = self._anti_patterns[pattern_name]
|
|
381
|
+
|
|
382
|
+
if not history:
|
|
383
|
+
return DetectionResult(
|
|
384
|
+
pattern_type="anti-pattern",
|
|
385
|
+
name=pattern_name,
|
|
386
|
+
description=pattern_def["description"],
|
|
387
|
+
detected=False,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Track file reads
|
|
391
|
+
file_read_count: dict[str, int] = {}
|
|
392
|
+
file_read_sequence: list[tuple[str, str]] = []
|
|
393
|
+
|
|
394
|
+
for call in history:
|
|
395
|
+
tool = call.get("tool", "")
|
|
396
|
+
|
|
397
|
+
if tool == "Read":
|
|
398
|
+
file_path = call.get("file_path", "")
|
|
399
|
+
if file_path:
|
|
400
|
+
file_read_count[file_path] = file_read_count.get(file_path, 0) + 1
|
|
401
|
+
file_read_sequence.append(("Read", file_path))
|
|
402
|
+
|
|
403
|
+
# Check for repeated reads of the same file
|
|
404
|
+
repeated_files = {
|
|
405
|
+
f: count for f, count in file_read_count.items() if count >= 2
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if repeated_files:
|
|
409
|
+
# File was read multiple times
|
|
410
|
+
most_repeated = max(repeated_files.items(), key=lambda x: x[1])
|
|
411
|
+
example_seq = [tool for tool, _ in file_read_sequence if tool == "Read"][
|
|
412
|
+
-3:
|
|
413
|
+
]
|
|
414
|
+
|
|
415
|
+
return DetectionResult(
|
|
416
|
+
pattern_type="anti-pattern",
|
|
417
|
+
name=pattern_name,
|
|
418
|
+
description=pattern_def["description"],
|
|
419
|
+
detected=True,
|
|
420
|
+
trigger_conditions=[f"{most_repeated[1]}x reads of: {most_repeated[0]}"]
|
|
421
|
+
+ pattern_def["trigger_conditions"],
|
|
422
|
+
example_sequence=example_seq,
|
|
423
|
+
remediation=pattern_def["remediation"],
|
|
424
|
+
confidence=min(1.0, most_repeated[1] / 3),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
return DetectionResult(
|
|
428
|
+
pattern_type="anti-pattern",
|
|
429
|
+
name=pattern_name,
|
|
430
|
+
description=pattern_def["description"],
|
|
431
|
+
detected=False,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
def _get_delegation_suggestion(self, pattern_name: str) -> str:
|
|
435
|
+
"""Get delegation suggestion for a detected anti-pattern."""
|
|
436
|
+
suggestions = {
|
|
437
|
+
"exploration_sequence": (
|
|
438
|
+
"spawn_gemini(prompt='Comprehensive search and analysis of codebase for...')"
|
|
439
|
+
),
|
|
440
|
+
"edit_without_test": (
|
|
441
|
+
"Task(prompt='Make the following changes AND run tests to verify: ...')"
|
|
442
|
+
),
|
|
443
|
+
"direct_git_commit": (
|
|
444
|
+
"spawn_copilot(prompt='Commit changes with message: ...')"
|
|
445
|
+
),
|
|
446
|
+
"repeated_read_same_file": (
|
|
447
|
+
"spawn_gemini(prompt='Analyze the entire file and extract all relevant sections: ...')"
|
|
448
|
+
),
|
|
449
|
+
}
|
|
450
|
+
return suggestions.get(
|
|
451
|
+
pattern_name,
|
|
452
|
+
"Delegate this operation to an appropriate subagent",
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
def get_pattern_statistics(
|
|
456
|
+
self, all_history: list[dict[str, Any]]
|
|
457
|
+
) -> dict[str, Any]:
|
|
458
|
+
"""
|
|
459
|
+
Analyze pattern statistics across entire history.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
all_history: Complete tool usage history (not just window)
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Dictionary with pattern statistics
|
|
466
|
+
"""
|
|
467
|
+
stats = {}
|
|
468
|
+
|
|
469
|
+
# Check each anti-pattern with sliding windows across history
|
|
470
|
+
for i in range(max(0, len(all_history) - 50), len(all_history)):
|
|
471
|
+
window = all_history[i : i + self.window_size]
|
|
472
|
+
if not window:
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
detected = self.detect_all_patterns(window)
|
|
476
|
+
for pattern in detected:
|
|
477
|
+
if pattern.name not in stats:
|
|
478
|
+
stats[pattern.name] = {"count": 0, "sessions": set()}
|
|
479
|
+
stats[pattern.name]["count"] = stats[pattern.name]["count"] + 1 # type: ignore[operator]
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
name: {"occurrence_count": data["count"]} for name, data in stats.items()
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# Helper function for external use
|
|
487
|
+
def detect_patterns(
|
|
488
|
+
history: list[dict[str, Any]], window_size: int = 10
|
|
489
|
+
) -> list[PatternRecord]:
|
|
490
|
+
"""
|
|
491
|
+
Detect anti-patterns from tool usage history.
|
|
492
|
+
|
|
493
|
+
Convenience function that creates a detector and finds all patterns.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
history: Tool usage history
|
|
497
|
+
window_size: Window size for pattern detection
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
List of detected PatternRecord instances
|
|
501
|
+
"""
|
|
502
|
+
detector = PatternDetector(window_size=window_size)
|
|
503
|
+
return detector.detect_all_patterns(history)
|