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,354 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
CIGS PreToolUse Enforcer - Enhanced Orchestrator Enforcement with Escalation
|
|
7
|
+
|
|
8
|
+
Integrates the Computational Imperative Guidance System (CIGS) into the PreToolUse
|
|
9
|
+
hook for intelligent delegation enforcement with escalating guidance.
|
|
10
|
+
|
|
11
|
+
Architecture:
|
|
12
|
+
1. Uses existing OrchestratorValidator for base classification
|
|
13
|
+
2. Loads session violation count from ViolationTracker
|
|
14
|
+
3. Classifies operation using CostCalculator
|
|
15
|
+
4. Generates imperative message with escalation via ImperativeMessageGenerator
|
|
16
|
+
5. Records violation if should_delegate=True
|
|
17
|
+
6. Returns hookSpecificOutput with imperative message
|
|
18
|
+
|
|
19
|
+
Escalation Levels:
|
|
20
|
+
- Level 0 (0 violations): Guidance - informative, no cost shown
|
|
21
|
+
- Level 1 (1 violation): Imperative - commanding, includes cost
|
|
22
|
+
- Level 2 (2 violations): Final Warning - urgent, includes consequences
|
|
23
|
+
- Level 3 (3+ violations): Circuit Breaker - blocking, requires acknowledgment
|
|
24
|
+
|
|
25
|
+
Design Reference:
|
|
26
|
+
.htmlgraph/spikes/computational-imperative-guidance-system-design.md
|
|
27
|
+
Part 2: CIGS PreToolUse Hook Integration
|
|
28
|
+
Part 4: Imperative Message Generation
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import sys
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from htmlgraph.cigs.cost import CostCalculator
|
|
38
|
+
from htmlgraph.cigs.messaging import ImperativeMessageGenerator
|
|
39
|
+
from htmlgraph.cigs.tracker import ViolationTracker
|
|
40
|
+
from htmlgraph.hooks.orchestrator import is_allowed_orchestrator_operation
|
|
41
|
+
from htmlgraph.orchestrator_mode import OrchestratorModeManager
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CIGSPreToolEnforcer:
|
|
45
|
+
"""
|
|
46
|
+
CIGS-enhanced PreToolUse enforcement with escalating imperative messages.
|
|
47
|
+
|
|
48
|
+
Integrates all CIGS components for comprehensive delegation enforcement.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# Tools that are ALWAYS allowed (orchestrator core)
|
|
52
|
+
ALWAYS_ALLOWED = {"Task", "AskUserQuestion", "TodoWrite"}
|
|
53
|
+
|
|
54
|
+
# Exploration tools that require delegation after first use
|
|
55
|
+
EXPLORATION_TOOLS = {"Read", "Grep", "Glob"}
|
|
56
|
+
|
|
57
|
+
# Implementation tools that always require delegation
|
|
58
|
+
IMPLEMENTATION_TOOLS = {"Edit", "Write", "NotebookEdit", "Delete"}
|
|
59
|
+
|
|
60
|
+
def __init__(self, graph_dir: Path | None = None):
|
|
61
|
+
"""
|
|
62
|
+
Initialize CIGS PreToolUse enforcer.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
graph_dir: Root directory for HtmlGraph (defaults to .htmlgraph)
|
|
66
|
+
"""
|
|
67
|
+
if graph_dir is None:
|
|
68
|
+
graph_dir = self._find_graph_dir()
|
|
69
|
+
|
|
70
|
+
self.graph_dir = graph_dir
|
|
71
|
+
self.manager = OrchestratorModeManager(graph_dir)
|
|
72
|
+
self.cost_calculator = CostCalculator()
|
|
73
|
+
self.message_generator = ImperativeMessageGenerator()
|
|
74
|
+
self.tracker = ViolationTracker(graph_dir)
|
|
75
|
+
|
|
76
|
+
# Ensure session ID is set (detect from environment or use current session)
|
|
77
|
+
if self.tracker._session_id is None:
|
|
78
|
+
self.tracker.set_session_id(self._get_or_create_session_id())
|
|
79
|
+
|
|
80
|
+
def _find_graph_dir(self) -> Path:
|
|
81
|
+
"""Find .htmlgraph directory starting from cwd."""
|
|
82
|
+
cwd = Path.cwd()
|
|
83
|
+
graph_dir = cwd / ".htmlgraph"
|
|
84
|
+
|
|
85
|
+
if not graph_dir.exists():
|
|
86
|
+
for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
|
|
87
|
+
candidate = parent / ".htmlgraph"
|
|
88
|
+
if candidate.exists():
|
|
89
|
+
graph_dir = candidate
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
return graph_dir
|
|
93
|
+
|
|
94
|
+
def enforce(self, tool: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
95
|
+
"""
|
|
96
|
+
Enforce CIGS delegation rules with escalating guidance.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
tool: Tool name (Read, Edit, Bash, etc.)
|
|
100
|
+
params: Tool parameters
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Hook response dict in Claude Code standard format:
|
|
104
|
+
{
|
|
105
|
+
"hookSpecificOutput": {
|
|
106
|
+
"hookEventName": "PreToolUse",
|
|
107
|
+
"permissionDecision": "allow" | "deny",
|
|
108
|
+
"additionalContext": "...", # If allow with guidance
|
|
109
|
+
"permissionDecisionReason": "...", # If deny
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
"""
|
|
113
|
+
# Check if orchestrator mode is enabled
|
|
114
|
+
if not self.manager.is_enabled():
|
|
115
|
+
return self._allow()
|
|
116
|
+
|
|
117
|
+
enforcement_level = self.manager.get_enforcement_level()
|
|
118
|
+
|
|
119
|
+
# ALWAYS ALLOWED tools pass through
|
|
120
|
+
if tool in self.ALWAYS_ALLOWED:
|
|
121
|
+
return self._allow()
|
|
122
|
+
|
|
123
|
+
# Check if SDK operation (always allowed)
|
|
124
|
+
if self._is_sdk_operation(tool, params):
|
|
125
|
+
return self._allow()
|
|
126
|
+
|
|
127
|
+
# Get session violation summary
|
|
128
|
+
summary = self.tracker.get_session_violations()
|
|
129
|
+
violation_count = summary.total_violations
|
|
130
|
+
|
|
131
|
+
# Check circuit breaker (3+ violations)
|
|
132
|
+
if violation_count >= 3 and enforcement_level == "strict":
|
|
133
|
+
return self._circuit_breaker(violation_count)
|
|
134
|
+
|
|
135
|
+
# Classify operation using existing orchestrator logic
|
|
136
|
+
is_allowed, reason, category = is_allowed_orchestrator_operation(tool, params)
|
|
137
|
+
|
|
138
|
+
# CIGS enforces stricter rules in strict mode:
|
|
139
|
+
# - Even "single lookups" should be delegated (exploration tools)
|
|
140
|
+
# - All implementation tools should be delegated
|
|
141
|
+
should_delegate = False
|
|
142
|
+
if enforcement_level == "strict":
|
|
143
|
+
if tool in self.EXPLORATION_TOOLS or tool in self.IMPLEMENTATION_TOOLS:
|
|
144
|
+
should_delegate = True
|
|
145
|
+
# Override is_allowed - CIGS wants delegation even for first use
|
|
146
|
+
is_allowed = False
|
|
147
|
+
|
|
148
|
+
# If orchestrator allows and CIGS doesn't override, proceed
|
|
149
|
+
if is_allowed and not should_delegate:
|
|
150
|
+
return self._allow()
|
|
151
|
+
|
|
152
|
+
# Operation should be delegated - classify with cost analysis
|
|
153
|
+
classification = self.cost_calculator.classify_operation(
|
|
154
|
+
tool=tool,
|
|
155
|
+
params=params,
|
|
156
|
+
is_exploration_sequence=self._is_exploration_sequence(tool),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Generate imperative message with escalation
|
|
160
|
+
imperative_message = self.message_generator.generate(
|
|
161
|
+
tool=tool,
|
|
162
|
+
classification=classification,
|
|
163
|
+
violation_count=violation_count,
|
|
164
|
+
autonomy_level=enforcement_level,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Record violation for session tracking
|
|
168
|
+
predicted_waste = classification.predicted_cost - classification.optimal_cost
|
|
169
|
+
self.tracker.record_violation(
|
|
170
|
+
tool=tool,
|
|
171
|
+
params=params,
|
|
172
|
+
classification=classification,
|
|
173
|
+
predicted_waste=predicted_waste,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Return response based on enforcement level and escalation
|
|
177
|
+
if enforcement_level == "strict":
|
|
178
|
+
# STRICT mode - deny with imperative message
|
|
179
|
+
return {
|
|
180
|
+
"hookSpecificOutput": {
|
|
181
|
+
"hookEventName": "PreToolUse",
|
|
182
|
+
"permissionDecision": "deny",
|
|
183
|
+
"permissionDecisionReason": imperative_message,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else:
|
|
187
|
+
# GUIDANCE mode - allow but with strong message
|
|
188
|
+
return {
|
|
189
|
+
"hookSpecificOutput": {
|
|
190
|
+
"hookEventName": "PreToolUse",
|
|
191
|
+
"permissionDecision": "allow",
|
|
192
|
+
"additionalContext": imperative_message,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
def _allow(self) -> dict[str, Any]:
|
|
197
|
+
"""Return allow response."""
|
|
198
|
+
return {
|
|
199
|
+
"hookSpecificOutput": {
|
|
200
|
+
"hookEventName": "PreToolUse",
|
|
201
|
+
"permissionDecision": "allow",
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
def _circuit_breaker(self, violation_count: int) -> dict[str, Any]:
|
|
206
|
+
"""Return circuit breaker blocking response."""
|
|
207
|
+
message = (
|
|
208
|
+
"🚨 CIRCUIT BREAKER TRIGGERED\n\n"
|
|
209
|
+
f"You have violated delegation rules {violation_count} times this session.\n\n"
|
|
210
|
+
"**Violations detected:**\n"
|
|
211
|
+
"- Direct execution instead of delegation\n"
|
|
212
|
+
"- Context waste on tactical operations\n"
|
|
213
|
+
"- Ignored imperative guidance messages\n\n"
|
|
214
|
+
"**REQUIRED:** Acknowledge violations before proceeding:\n"
|
|
215
|
+
"`uv run htmlgraph orchestrator acknowledge-violation`\n\n"
|
|
216
|
+
"**OR** Change enforcement settings:\n"
|
|
217
|
+
"- Disable: `uv run htmlgraph orchestrator disable`\n"
|
|
218
|
+
"- Guidance mode: `uv run htmlgraph orchestrator set-level guidance`\n"
|
|
219
|
+
"- Reset violations: `uv run htmlgraph orchestrator reset-violations`"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
"hookSpecificOutput": {
|
|
224
|
+
"hookEventName": "PreToolUse",
|
|
225
|
+
"permissionDecision": "deny",
|
|
226
|
+
"permissionDecisionReason": message,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
def _is_sdk_operation(self, tool: str, params: dict[str, Any]) -> bool:
|
|
231
|
+
"""Check if operation is an SDK operation (always allowed)."""
|
|
232
|
+
if tool != "Bash":
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
command = params.get("command", "")
|
|
236
|
+
|
|
237
|
+
# Allow htmlgraph SDK commands
|
|
238
|
+
if command.startswith("uv run htmlgraph ") or command.startswith("htmlgraph "):
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
# Allow git read-only commands
|
|
242
|
+
if command.startswith(("git status", "git diff", "git log")):
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
# Allow SDK inline usage
|
|
246
|
+
if "from htmlgraph import" in command or "import htmlgraph" in command:
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
def _is_exploration_sequence(self, tool: str) -> bool:
|
|
252
|
+
"""Check if this is part of an exploration sequence."""
|
|
253
|
+
if tool not in self.EXPLORATION_TOOLS:
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
# Check recent history for exploration pattern
|
|
257
|
+
# This is simplified - could use tool_history from orchestrator.py
|
|
258
|
+
summary = self.tracker.get_session_violations()
|
|
259
|
+
|
|
260
|
+
# If we've already had exploration violations, this is a sequence
|
|
261
|
+
exploration_violations = [
|
|
262
|
+
v for v in summary.violations if v.tool in self.EXPLORATION_TOOLS
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
return len(exploration_violations) >= 1
|
|
266
|
+
|
|
267
|
+
def _get_or_create_session_id(self) -> str:
|
|
268
|
+
"""Get or create a session ID for tracking."""
|
|
269
|
+
# Try to get from environment
|
|
270
|
+
if "HTMLGRAPH_SESSION_ID" in os.environ:
|
|
271
|
+
return os.environ["HTMLGRAPH_SESSION_ID"]
|
|
272
|
+
|
|
273
|
+
# Try to get from session manager
|
|
274
|
+
try:
|
|
275
|
+
from htmlgraph.session_manager import SessionManager
|
|
276
|
+
|
|
277
|
+
sm = SessionManager(self.graph_dir)
|
|
278
|
+
current = sm.get_active_session()
|
|
279
|
+
if current:
|
|
280
|
+
return str(current.id)
|
|
281
|
+
except Exception:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
# Fallback: create a session ID for this test/run
|
|
285
|
+
# Use a consistent ID for the process
|
|
286
|
+
if not hasattr(self.__class__, "_fallback_session_id"):
|
|
287
|
+
from uuid import uuid4
|
|
288
|
+
|
|
289
|
+
fallback_id: str = f"test-session-{uuid4().hex[:8]}"
|
|
290
|
+
setattr(self.__class__, "_fallback_session_id", fallback_id)
|
|
291
|
+
return fallback_id
|
|
292
|
+
|
|
293
|
+
return str(getattr(self.__class__, "_fallback_session_id"))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def enforce_cigs_pretool(tool_input: dict[str, Any]) -> dict[str, Any]:
|
|
297
|
+
"""
|
|
298
|
+
Main entry point for CIGS PreToolUse enforcement.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
tool_input: Hook input with tool name and parameters
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Hook response dict in Claude Code standard format
|
|
305
|
+
"""
|
|
306
|
+
# Extract tool and params from input
|
|
307
|
+
tool = tool_input.get("name", "") or tool_input.get("tool_name", "")
|
|
308
|
+
params = tool_input.get("input", {}) or tool_input.get("tool_input", {})
|
|
309
|
+
|
|
310
|
+
# Create enforcer and run
|
|
311
|
+
try:
|
|
312
|
+
enforcer = CIGSPreToolEnforcer()
|
|
313
|
+
return enforcer.enforce(tool, params)
|
|
314
|
+
except Exception as e:
|
|
315
|
+
# Graceful degradation - allow on error
|
|
316
|
+
logger.warning(f"Warning: CIGS enforcement error: {e}")
|
|
317
|
+
return {
|
|
318
|
+
"hookSpecificOutput": {
|
|
319
|
+
"hookEventName": "PreToolUse",
|
|
320
|
+
"permissionDecision": "allow",
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def main() -> None:
|
|
326
|
+
"""Hook entry point for script wrapper."""
|
|
327
|
+
# Check environment overrides
|
|
328
|
+
if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
|
|
329
|
+
print(json.dumps({"hookSpecificOutput": {"permissionDecision": "allow"}}))
|
|
330
|
+
sys.exit(0)
|
|
331
|
+
|
|
332
|
+
if os.environ.get("HTMLGRAPH_ORCHESTRATOR_DISABLED") == "1":
|
|
333
|
+
print(json.dumps({"hookSpecificOutput": {"permissionDecision": "allow"}}))
|
|
334
|
+
sys.exit(0)
|
|
335
|
+
|
|
336
|
+
# Read tool input from stdin
|
|
337
|
+
try:
|
|
338
|
+
tool_input = json.load(sys.stdin)
|
|
339
|
+
except json.JSONDecodeError:
|
|
340
|
+
tool_input = {}
|
|
341
|
+
|
|
342
|
+
# Run CIGS enforcement
|
|
343
|
+
result = enforce_cigs_pretool(tool_input)
|
|
344
|
+
|
|
345
|
+
# Output response
|
|
346
|
+
print(json.dumps(result))
|
|
347
|
+
|
|
348
|
+
# Exit code based on permission decision
|
|
349
|
+
permission = result.get("hookSpecificOutput", {}).get("permissionDecision", "allow")
|
|
350
|
+
sys.exit(0 if permission == "allow" else 1)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
if __name__ == "__main__":
|
|
354
|
+
main()
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Concurrent Session Detection and Formatting.
|
|
3
|
+
|
|
4
|
+
Provides utilities to detect other active sessions and format them
|
|
5
|
+
for injection into the orchestrator's context at session start.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_concurrent_sessions(
|
|
15
|
+
db: HtmlGraphDB,
|
|
16
|
+
current_session_id: str,
|
|
17
|
+
minutes: int = 30,
|
|
18
|
+
) -> list[dict[str, Any]]:
|
|
19
|
+
"""
|
|
20
|
+
Get other sessions that are currently active.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
db: Database connection
|
|
24
|
+
current_session_id: Current session to exclude
|
|
25
|
+
minutes: Look back window for activity
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
List of concurrent session dicts with id, agent_id, last_user_query, etc.
|
|
29
|
+
"""
|
|
30
|
+
if not db.connection:
|
|
31
|
+
db.connect()
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
cursor = db.connection.cursor() # type: ignore[union-attr]
|
|
35
|
+
# Use datetime format that matches database (without timezone)
|
|
36
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(minutes=minutes)).strftime(
|
|
37
|
+
"%Y-%m-%d %H:%M:%S"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
cursor.execute(
|
|
41
|
+
"""
|
|
42
|
+
SELECT
|
|
43
|
+
session_id as id,
|
|
44
|
+
agent_assigned as agent_id,
|
|
45
|
+
created_at,
|
|
46
|
+
status,
|
|
47
|
+
(SELECT input_summary FROM agent_events
|
|
48
|
+
WHERE session_id = sessions.session_id
|
|
49
|
+
ORDER BY timestamp DESC LIMIT 1) as last_user_query,
|
|
50
|
+
(SELECT timestamp FROM agent_events
|
|
51
|
+
WHERE session_id = sessions.session_id
|
|
52
|
+
ORDER BY timestamp DESC LIMIT 1) as last_user_query_at
|
|
53
|
+
FROM sessions
|
|
54
|
+
WHERE status = 'active'
|
|
55
|
+
AND session_id != ?
|
|
56
|
+
AND created_at > ?
|
|
57
|
+
ORDER BY created_at DESC
|
|
58
|
+
""",
|
|
59
|
+
(current_session_id, cutoff),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
rows = cursor.fetchall()
|
|
63
|
+
return [dict(row) for row in rows]
|
|
64
|
+
except Exception: # pragma: no cover
|
|
65
|
+
# Gracefully handle database errors
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def format_concurrent_sessions_markdown(sessions: list[dict[str, Any]]) -> str:
|
|
70
|
+
"""
|
|
71
|
+
Format concurrent sessions as markdown for context injection.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
sessions: List of session dicts from get_concurrent_sessions
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Markdown formatted string for system prompt injection
|
|
78
|
+
"""
|
|
79
|
+
if not sessions:
|
|
80
|
+
return ""
|
|
81
|
+
|
|
82
|
+
lines = ["## Concurrent Sessions (Active Now)", ""]
|
|
83
|
+
|
|
84
|
+
for session in sessions:
|
|
85
|
+
session_id = session.get("id", "unknown")
|
|
86
|
+
session_id = session_id[:12] if len(session_id) > 12 else session_id
|
|
87
|
+
agent = session.get("agent_id", "unknown")
|
|
88
|
+
query = session.get("last_user_query", "No recent query")
|
|
89
|
+
last_active = session.get("last_user_query_at")
|
|
90
|
+
|
|
91
|
+
# Calculate time ago
|
|
92
|
+
time_ago = "unknown"
|
|
93
|
+
if last_active:
|
|
94
|
+
try:
|
|
95
|
+
last_dt = datetime.fromisoformat(
|
|
96
|
+
last_active.replace("Z", "+00:00")
|
|
97
|
+
if isinstance(last_active, str)
|
|
98
|
+
else last_active
|
|
99
|
+
)
|
|
100
|
+
delta = datetime.now(timezone.utc) - last_dt
|
|
101
|
+
if delta.total_seconds() < 60:
|
|
102
|
+
time_ago = "just now"
|
|
103
|
+
elif delta.total_seconds() < 3600:
|
|
104
|
+
time_ago = f"{int(delta.total_seconds() // 60)} min ago"
|
|
105
|
+
else:
|
|
106
|
+
time_ago = f"{int(delta.total_seconds() // 3600)} hours ago"
|
|
107
|
+
except (ValueError, TypeError, AttributeError):
|
|
108
|
+
time_ago = "unknown"
|
|
109
|
+
|
|
110
|
+
# Truncate query for display
|
|
111
|
+
query_display = (
|
|
112
|
+
query[:50] + "..." if query and len(query) > 50 else (query or "Unknown")
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
lines.append(f'- **{session_id}** ({agent}): "{query_display}" - {time_ago}')
|
|
116
|
+
|
|
117
|
+
lines.append("")
|
|
118
|
+
lines.append("*Coordinate with concurrent sessions to avoid duplicate work.*")
|
|
119
|
+
lines.append("")
|
|
120
|
+
|
|
121
|
+
return "\n".join(lines)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_recent_completed_sessions(
|
|
125
|
+
db: HtmlGraphDB,
|
|
126
|
+
hours: int = 24,
|
|
127
|
+
limit: int = 5,
|
|
128
|
+
) -> list[dict[str, Any]]:
|
|
129
|
+
"""
|
|
130
|
+
Get recently completed sessions for handoff context.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
db: Database connection
|
|
134
|
+
hours: Look back window
|
|
135
|
+
limit: Maximum sessions to return
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of recently completed session dicts
|
|
139
|
+
"""
|
|
140
|
+
if not db.connection:
|
|
141
|
+
db.connect()
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
cursor = db.connection.cursor() # type: ignore[union-attr]
|
|
145
|
+
# Use datetime format that matches database (without timezone)
|
|
146
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(hours=hours)).strftime(
|
|
147
|
+
"%Y-%m-%d %H:%M:%S"
|
|
148
|
+
)
|
|
149
|
+
cursor.execute(
|
|
150
|
+
"""
|
|
151
|
+
SELECT session_id as id, agent_assigned as agent_id, created_at as started_at,
|
|
152
|
+
completed_at, total_events,
|
|
153
|
+
(SELECT input_summary FROM agent_events
|
|
154
|
+
WHERE session_id = sessions.session_id
|
|
155
|
+
ORDER BY timestamp DESC LIMIT 1) as last_user_query
|
|
156
|
+
FROM sessions
|
|
157
|
+
WHERE status = 'completed'
|
|
158
|
+
AND completed_at > ?
|
|
159
|
+
ORDER BY completed_at DESC
|
|
160
|
+
LIMIT ?
|
|
161
|
+
""",
|
|
162
|
+
(cutoff, limit),
|
|
163
|
+
)
|
|
164
|
+
rows = cursor.fetchall()
|
|
165
|
+
return [dict(row) for row in rows]
|
|
166
|
+
except Exception: # pragma: no cover
|
|
167
|
+
# Gracefully handle database errors
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def format_recent_work_markdown(sessions: list[dict[str, Any]]) -> str:
|
|
172
|
+
"""
|
|
173
|
+
Format recently completed sessions as markdown.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
sessions: List of completed session dicts
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Markdown formatted string
|
|
180
|
+
"""
|
|
181
|
+
if not sessions:
|
|
182
|
+
return ""
|
|
183
|
+
|
|
184
|
+
lines = ["## Recent Work (Last 24 Hours)", ""]
|
|
185
|
+
|
|
186
|
+
for session in sessions:
|
|
187
|
+
session_id = session.get("id", "unknown")
|
|
188
|
+
session_id = session_id[:12] if len(session_id) > 12 else session_id
|
|
189
|
+
query = session.get("last_user_query", "No query recorded")
|
|
190
|
+
total_events = session.get("total_events") or 0
|
|
191
|
+
|
|
192
|
+
query_display = (
|
|
193
|
+
query[:60] + "..." if query and len(query) > 60 else (query or "Unknown")
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
lines.append(f"- `{session_id}`: {query_display} ({total_events} events)")
|
|
197
|
+
|
|
198
|
+
lines.append("")
|
|
199
|
+
|
|
200
|
+
return "\n".join(lines)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
__all__ = [
|
|
204
|
+
"get_concurrent_sessions",
|
|
205
|
+
"format_concurrent_sessions_markdown",
|
|
206
|
+
"get_recent_completed_sessions",
|
|
207
|
+
"format_recent_work_markdown",
|
|
208
|
+
]
|