htmlgraph 0.23.5__py3-none-any.whl → 0.24.0__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/__init__.py +5 -1
- htmlgraph/cigs/__init__.py +77 -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/tracker.py +317 -0
- htmlgraph/cli.py +325 -11
- htmlgraph/hooks/cigs_pretool_enforcer.py +350 -0
- htmlgraph/hooks/posttooluse.py +50 -2
- htmlgraph/hooks/task_enforcer.py +60 -4
- htmlgraph/models.py +14 -1
- htmlgraph/orchestration/headless_spawner.py +519 -21
- htmlgraph/orchestrator-system-prompt-optimized.txt +259 -53
- htmlgraph/reflection.py +442 -0
- htmlgraph/sdk.py +26 -9
- {htmlgraph-0.23.5.dist-info → htmlgraph-0.24.0.dist-info}/METADATA +2 -1
- {htmlgraph-0.23.5.dist-info → htmlgraph-0.24.0.dist-info}/RECORD +29 -17
- {htmlgraph-0.23.5.data → htmlgraph-0.24.0.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.23.5.data → htmlgraph-0.24.0.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.23.5.data → htmlgraph-0.24.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.23.5.data → htmlgraph-0.24.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.23.5.data → htmlgraph-0.24.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.23.5.dist-info → htmlgraph-0.24.0.dist-info}/WHEEL +0 -0
- {htmlgraph-0.23.5.dist-info → htmlgraph-0.24.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CIGS PreToolUse Enforcer - Enhanced Orchestrator Enforcement with Escalation
|
|
3
|
+
|
|
4
|
+
Integrates the Computational Imperative Guidance System (CIGS) into the PreToolUse
|
|
5
|
+
hook for intelligent delegation enforcement with escalating guidance.
|
|
6
|
+
|
|
7
|
+
Architecture:
|
|
8
|
+
1. Uses existing OrchestratorValidator for base classification
|
|
9
|
+
2. Loads session violation count from ViolationTracker
|
|
10
|
+
3. Classifies operation using CostCalculator
|
|
11
|
+
4. Generates imperative message with escalation via ImperativeMessageGenerator
|
|
12
|
+
5. Records violation if should_delegate=True
|
|
13
|
+
6. Returns hookSpecificOutput with imperative message
|
|
14
|
+
|
|
15
|
+
Escalation Levels:
|
|
16
|
+
- Level 0 (0 violations): Guidance - informative, no cost shown
|
|
17
|
+
- Level 1 (1 violation): Imperative - commanding, includes cost
|
|
18
|
+
- Level 2 (2 violations): Final Warning - urgent, includes consequences
|
|
19
|
+
- Level 3 (3+ violations): Circuit Breaker - blocking, requires acknowledgment
|
|
20
|
+
|
|
21
|
+
Design Reference:
|
|
22
|
+
.htmlgraph/spikes/computational-imperative-guidance-system-design.md
|
|
23
|
+
Part 2: CIGS PreToolUse Hook Integration
|
|
24
|
+
Part 4: Imperative Message Generation
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from htmlgraph.cigs.cost import CostCalculator
|
|
34
|
+
from htmlgraph.cigs.messaging import ImperativeMessageGenerator
|
|
35
|
+
from htmlgraph.cigs.tracker import ViolationTracker
|
|
36
|
+
from htmlgraph.hooks.orchestrator import is_allowed_orchestrator_operation
|
|
37
|
+
from htmlgraph.orchestrator_mode import OrchestratorModeManager
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CIGSPreToolEnforcer:
|
|
41
|
+
"""
|
|
42
|
+
CIGS-enhanced PreToolUse enforcement with escalating imperative messages.
|
|
43
|
+
|
|
44
|
+
Integrates all CIGS components for comprehensive delegation enforcement.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# Tools that are ALWAYS allowed (orchestrator core)
|
|
48
|
+
ALWAYS_ALLOWED = {"Task", "AskUserQuestion", "TodoWrite"}
|
|
49
|
+
|
|
50
|
+
# Exploration tools that require delegation after first use
|
|
51
|
+
EXPLORATION_TOOLS = {"Read", "Grep", "Glob"}
|
|
52
|
+
|
|
53
|
+
# Implementation tools that always require delegation
|
|
54
|
+
IMPLEMENTATION_TOOLS = {"Edit", "Write", "NotebookEdit", "Delete"}
|
|
55
|
+
|
|
56
|
+
def __init__(self, graph_dir: Path | None = None):
|
|
57
|
+
"""
|
|
58
|
+
Initialize CIGS PreToolUse enforcer.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
graph_dir: Root directory for HtmlGraph (defaults to .htmlgraph)
|
|
62
|
+
"""
|
|
63
|
+
if graph_dir is None:
|
|
64
|
+
graph_dir = self._find_graph_dir()
|
|
65
|
+
|
|
66
|
+
self.graph_dir = graph_dir
|
|
67
|
+
self.manager = OrchestratorModeManager(graph_dir)
|
|
68
|
+
self.cost_calculator = CostCalculator()
|
|
69
|
+
self.message_generator = ImperativeMessageGenerator()
|
|
70
|
+
self.tracker = ViolationTracker(graph_dir)
|
|
71
|
+
|
|
72
|
+
# Ensure session ID is set (detect from environment or use current session)
|
|
73
|
+
if self.tracker._session_id is None:
|
|
74
|
+
self.tracker.set_session_id(self._get_or_create_session_id())
|
|
75
|
+
|
|
76
|
+
def _find_graph_dir(self) -> Path:
|
|
77
|
+
"""Find .htmlgraph directory starting from cwd."""
|
|
78
|
+
cwd = Path.cwd()
|
|
79
|
+
graph_dir = cwd / ".htmlgraph"
|
|
80
|
+
|
|
81
|
+
if not graph_dir.exists():
|
|
82
|
+
for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
|
|
83
|
+
candidate = parent / ".htmlgraph"
|
|
84
|
+
if candidate.exists():
|
|
85
|
+
graph_dir = candidate
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
return graph_dir
|
|
89
|
+
|
|
90
|
+
def enforce(self, tool: str, params: dict) -> dict[str, Any]:
|
|
91
|
+
"""
|
|
92
|
+
Enforce CIGS delegation rules with escalating guidance.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
tool: Tool name (Read, Edit, Bash, etc.)
|
|
96
|
+
params: Tool parameters
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Hook response dict in Claude Code standard format:
|
|
100
|
+
{
|
|
101
|
+
"hookSpecificOutput": {
|
|
102
|
+
"hookEventName": "PreToolUse",
|
|
103
|
+
"permissionDecision": "allow" | "deny",
|
|
104
|
+
"additionalContext": "...", # If allow with guidance
|
|
105
|
+
"permissionDecisionReason": "...", # If deny
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
"""
|
|
109
|
+
# Check if orchestrator mode is enabled
|
|
110
|
+
if not self.manager.is_enabled():
|
|
111
|
+
return self._allow()
|
|
112
|
+
|
|
113
|
+
enforcement_level = self.manager.get_enforcement_level()
|
|
114
|
+
|
|
115
|
+
# ALWAYS ALLOWED tools pass through
|
|
116
|
+
if tool in self.ALWAYS_ALLOWED:
|
|
117
|
+
return self._allow()
|
|
118
|
+
|
|
119
|
+
# Check if SDK operation (always allowed)
|
|
120
|
+
if self._is_sdk_operation(tool, params):
|
|
121
|
+
return self._allow()
|
|
122
|
+
|
|
123
|
+
# Get session violation summary
|
|
124
|
+
summary = self.tracker.get_session_violations()
|
|
125
|
+
violation_count = summary.total_violations
|
|
126
|
+
|
|
127
|
+
# Check circuit breaker (3+ violations)
|
|
128
|
+
if violation_count >= 3 and enforcement_level == "strict":
|
|
129
|
+
return self._circuit_breaker(violation_count)
|
|
130
|
+
|
|
131
|
+
# Classify operation using existing orchestrator logic
|
|
132
|
+
is_allowed, reason, category = is_allowed_orchestrator_operation(tool, params)
|
|
133
|
+
|
|
134
|
+
# CIGS enforces stricter rules in strict mode:
|
|
135
|
+
# - Even "single lookups" should be delegated (exploration tools)
|
|
136
|
+
# - All implementation tools should be delegated
|
|
137
|
+
should_delegate = False
|
|
138
|
+
if enforcement_level == "strict":
|
|
139
|
+
if tool in self.EXPLORATION_TOOLS or tool in self.IMPLEMENTATION_TOOLS:
|
|
140
|
+
should_delegate = True
|
|
141
|
+
# Override is_allowed - CIGS wants delegation even for first use
|
|
142
|
+
is_allowed = False
|
|
143
|
+
|
|
144
|
+
# If orchestrator allows and CIGS doesn't override, proceed
|
|
145
|
+
if is_allowed and not should_delegate:
|
|
146
|
+
return self._allow()
|
|
147
|
+
|
|
148
|
+
# Operation should be delegated - classify with cost analysis
|
|
149
|
+
classification = self.cost_calculator.classify_operation(
|
|
150
|
+
tool=tool,
|
|
151
|
+
params=params,
|
|
152
|
+
is_exploration_sequence=self._is_exploration_sequence(tool),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Generate imperative message with escalation
|
|
156
|
+
imperative_message = self.message_generator.generate(
|
|
157
|
+
tool=tool,
|
|
158
|
+
classification=classification,
|
|
159
|
+
violation_count=violation_count,
|
|
160
|
+
autonomy_level=enforcement_level,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Record violation for session tracking
|
|
164
|
+
predicted_waste = classification.predicted_cost - classification.optimal_cost
|
|
165
|
+
self.tracker.record_violation(
|
|
166
|
+
tool=tool,
|
|
167
|
+
params=params,
|
|
168
|
+
classification=classification,
|
|
169
|
+
predicted_waste=predicted_waste,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Return response based on enforcement level and escalation
|
|
173
|
+
if enforcement_level == "strict":
|
|
174
|
+
# STRICT mode - deny with imperative message
|
|
175
|
+
return {
|
|
176
|
+
"hookSpecificOutput": {
|
|
177
|
+
"hookEventName": "PreToolUse",
|
|
178
|
+
"permissionDecision": "deny",
|
|
179
|
+
"permissionDecisionReason": imperative_message,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else:
|
|
183
|
+
# GUIDANCE mode - allow but with strong message
|
|
184
|
+
return {
|
|
185
|
+
"hookSpecificOutput": {
|
|
186
|
+
"hookEventName": "PreToolUse",
|
|
187
|
+
"permissionDecision": "allow",
|
|
188
|
+
"additionalContext": imperative_message,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
def _allow(self) -> dict[str, Any]:
|
|
193
|
+
"""Return allow response."""
|
|
194
|
+
return {
|
|
195
|
+
"hookSpecificOutput": {
|
|
196
|
+
"hookEventName": "PreToolUse",
|
|
197
|
+
"permissionDecision": "allow",
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
def _circuit_breaker(self, violation_count: int) -> dict[str, Any]:
|
|
202
|
+
"""Return circuit breaker blocking response."""
|
|
203
|
+
message = (
|
|
204
|
+
"🚨 CIRCUIT BREAKER TRIGGERED\n\n"
|
|
205
|
+
f"You have violated delegation rules {violation_count} times this session.\n\n"
|
|
206
|
+
"**Violations detected:**\n"
|
|
207
|
+
"- Direct execution instead of delegation\n"
|
|
208
|
+
"- Context waste on tactical operations\n"
|
|
209
|
+
"- Ignored imperative guidance messages\n\n"
|
|
210
|
+
"**REQUIRED:** Acknowledge violations before proceeding:\n"
|
|
211
|
+
"`uv run htmlgraph orchestrator acknowledge-violation`\n\n"
|
|
212
|
+
"**OR** Change enforcement settings:\n"
|
|
213
|
+
"- Disable: `uv run htmlgraph orchestrator disable`\n"
|
|
214
|
+
"- Guidance mode: `uv run htmlgraph orchestrator set-level guidance`\n"
|
|
215
|
+
"- Reset violations: `uv run htmlgraph orchestrator reset-violations`"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
"hookSpecificOutput": {
|
|
220
|
+
"hookEventName": "PreToolUse",
|
|
221
|
+
"permissionDecision": "deny",
|
|
222
|
+
"permissionDecisionReason": message,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
def _is_sdk_operation(self, tool: str, params: dict) -> bool:
|
|
227
|
+
"""Check if operation is an SDK operation (always allowed)."""
|
|
228
|
+
if tool != "Bash":
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
command = params.get("command", "")
|
|
232
|
+
|
|
233
|
+
# Allow htmlgraph SDK commands
|
|
234
|
+
if command.startswith("uv run htmlgraph ") or command.startswith("htmlgraph "):
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
# Allow git read-only commands
|
|
238
|
+
if command.startswith(("git status", "git diff", "git log")):
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
# Allow SDK inline usage
|
|
242
|
+
if "from htmlgraph import" in command or "import htmlgraph" in command:
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
def _is_exploration_sequence(self, tool: str) -> bool:
|
|
248
|
+
"""Check if this is part of an exploration sequence."""
|
|
249
|
+
if tool not in self.EXPLORATION_TOOLS:
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
# Check recent history for exploration pattern
|
|
253
|
+
# This is simplified - could use tool_history from orchestrator.py
|
|
254
|
+
summary = self.tracker.get_session_violations()
|
|
255
|
+
|
|
256
|
+
# If we've already had exploration violations, this is a sequence
|
|
257
|
+
exploration_violations = [
|
|
258
|
+
v for v in summary.violations if v.tool in self.EXPLORATION_TOOLS
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
return len(exploration_violations) >= 1
|
|
262
|
+
|
|
263
|
+
def _get_or_create_session_id(self) -> str:
|
|
264
|
+
"""Get or create a session ID for tracking."""
|
|
265
|
+
# Try to get from environment
|
|
266
|
+
if "HTMLGRAPH_SESSION_ID" in os.environ:
|
|
267
|
+
return os.environ["HTMLGRAPH_SESSION_ID"]
|
|
268
|
+
|
|
269
|
+
# Try to get from session manager
|
|
270
|
+
try:
|
|
271
|
+
from htmlgraph.session_manager import SessionManager
|
|
272
|
+
|
|
273
|
+
sm = SessionManager(self.graph_dir)
|
|
274
|
+
current = sm.get_active_session()
|
|
275
|
+
if current:
|
|
276
|
+
return str(current.id)
|
|
277
|
+
except Exception:
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
# Fallback: create a session ID for this test/run
|
|
281
|
+
# Use a consistent ID for the process
|
|
282
|
+
if not hasattr(self.__class__, "_fallback_session_id"):
|
|
283
|
+
from uuid import uuid4
|
|
284
|
+
|
|
285
|
+
fallback_id: str = f"test-session-{uuid4().hex[:8]}"
|
|
286
|
+
setattr(self.__class__, "_fallback_session_id", fallback_id)
|
|
287
|
+
return fallback_id
|
|
288
|
+
|
|
289
|
+
return str(getattr(self.__class__, "_fallback_session_id"))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def enforce_cigs_pretool(tool_input: dict[str, Any]) -> dict[str, Any]:
|
|
293
|
+
"""
|
|
294
|
+
Main entry point for CIGS PreToolUse enforcement.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
tool_input: Hook input with tool name and parameters
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Hook response dict in Claude Code standard format
|
|
301
|
+
"""
|
|
302
|
+
# Extract tool and params from input
|
|
303
|
+
tool = tool_input.get("name", "") or tool_input.get("tool_name", "")
|
|
304
|
+
params = tool_input.get("input", {}) or tool_input.get("tool_input", {})
|
|
305
|
+
|
|
306
|
+
# Create enforcer and run
|
|
307
|
+
try:
|
|
308
|
+
enforcer = CIGSPreToolEnforcer()
|
|
309
|
+
return enforcer.enforce(tool, params)
|
|
310
|
+
except Exception as e:
|
|
311
|
+
# Graceful degradation - allow on error
|
|
312
|
+
print(f"Warning: CIGS enforcement error: {e}", file=sys.stderr)
|
|
313
|
+
return {
|
|
314
|
+
"hookSpecificOutput": {
|
|
315
|
+
"hookEventName": "PreToolUse",
|
|
316
|
+
"permissionDecision": "allow",
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def main() -> None:
|
|
322
|
+
"""Hook entry point for script wrapper."""
|
|
323
|
+
# Check environment overrides
|
|
324
|
+
if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
|
|
325
|
+
print(json.dumps({"hookSpecificOutput": {"permissionDecision": "allow"}}))
|
|
326
|
+
sys.exit(0)
|
|
327
|
+
|
|
328
|
+
if os.environ.get("HTMLGRAPH_ORCHESTRATOR_DISABLED") == "1":
|
|
329
|
+
print(json.dumps({"hookSpecificOutput": {"permissionDecision": "allow"}}))
|
|
330
|
+
sys.exit(0)
|
|
331
|
+
|
|
332
|
+
# Read tool input from stdin
|
|
333
|
+
try:
|
|
334
|
+
tool_input = json.load(sys.stdin)
|
|
335
|
+
except json.JSONDecodeError:
|
|
336
|
+
tool_input = {}
|
|
337
|
+
|
|
338
|
+
# Run CIGS enforcement
|
|
339
|
+
result = enforce_cigs_pretool(tool_input)
|
|
340
|
+
|
|
341
|
+
# Output response
|
|
342
|
+
print(json.dumps(result))
|
|
343
|
+
|
|
344
|
+
# Exit code based on permission decision
|
|
345
|
+
permission = result.get("hookSpecificOutput", {}).get("permissionDecision", "allow")
|
|
346
|
+
sys.exit(0 if permission == "allow" else 1)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
main()
|
htmlgraph/hooks/posttooluse.py
CHANGED
|
@@ -8,6 +8,7 @@ in parallel using asyncio:
|
|
|
8
8
|
3. Task validation - validates task results
|
|
9
9
|
4. Error tracking - logs errors and auto-creates debug spikes
|
|
10
10
|
5. Debugging suggestions - suggests resources when errors detected
|
|
11
|
+
6. CIGS analysis - cost accounting and reinforcement for delegation
|
|
11
12
|
|
|
12
13
|
Architecture:
|
|
13
14
|
- All tasks run simultaneously via asyncio.gather()
|
|
@@ -25,8 +26,10 @@ import asyncio
|
|
|
25
26
|
import json
|
|
26
27
|
import os
|
|
27
28
|
import sys
|
|
29
|
+
from pathlib import Path
|
|
28
30
|
from typing import Any
|
|
29
31
|
|
|
32
|
+
from htmlgraph.cigs import CIGSPostToolAnalyzer
|
|
30
33
|
from htmlgraph.hooks.event_tracker import track_event
|
|
31
34
|
from htmlgraph.hooks.orchestrator_reflector import orchestrator_reflect
|
|
32
35
|
from htmlgraph.hooks.post_tool_use_failure import run as track_error
|
|
@@ -233,11 +236,48 @@ async def suggest_debugging_resources(hook_input: dict[str, Any]) -> dict[str, A
|
|
|
233
236
|
return {}
|
|
234
237
|
|
|
235
238
|
|
|
239
|
+
async def run_cigs_analysis(hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
240
|
+
"""
|
|
241
|
+
Run CIGS cost accounting and reinforcement analysis.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
hook_input: Hook input with tool execution details
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
CIGS analysis response: {"hookSpecificOutput": {...}}
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
loop = asyncio.get_event_loop()
|
|
251
|
+
|
|
252
|
+
# Extract tool info
|
|
253
|
+
tool_name = hook_input.get("name", "") or hook_input.get("tool_name", "")
|
|
254
|
+
tool_params = hook_input.get("input", {}) or hook_input.get("tool_input", {})
|
|
255
|
+
tool_response = hook_input.get("result", {}) or hook_input.get(
|
|
256
|
+
"tool_response", {}
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Initialize CIGS analyzer
|
|
260
|
+
graph_dir = Path.cwd() / ".htmlgraph"
|
|
261
|
+
analyzer = CIGSPostToolAnalyzer(graph_dir)
|
|
262
|
+
|
|
263
|
+
# Run analysis in executor (may involve I/O)
|
|
264
|
+
return await loop.run_in_executor(
|
|
265
|
+
None,
|
|
266
|
+
analyzer.analyze,
|
|
267
|
+
tool_name,
|
|
268
|
+
tool_params,
|
|
269
|
+
tool_response,
|
|
270
|
+
)
|
|
271
|
+
except Exception:
|
|
272
|
+
# Graceful degradation - allow on error
|
|
273
|
+
return {}
|
|
274
|
+
|
|
275
|
+
|
|
236
276
|
async def posttooluse_hook(
|
|
237
277
|
hook_type: str, hook_input: dict[str, Any]
|
|
238
278
|
) -> dict[str, Any]:
|
|
239
279
|
"""
|
|
240
|
-
Unified PostToolUse hook - runs tracking, reflection, validation, error tracking,
|
|
280
|
+
Unified PostToolUse hook - runs tracking, reflection, validation, error tracking, debugging suggestions, and CIGS analysis in parallel.
|
|
241
281
|
|
|
242
282
|
Args:
|
|
243
283
|
hook_type: "PostToolUse" or "Stop"
|
|
@@ -254,19 +294,21 @@ async def posttooluse_hook(
|
|
|
254
294
|
}
|
|
255
295
|
}
|
|
256
296
|
"""
|
|
257
|
-
# Run all
|
|
297
|
+
# Run all six in parallel using asyncio.gather
|
|
258
298
|
(
|
|
259
299
|
event_response,
|
|
260
300
|
reflection_response,
|
|
261
301
|
validation_response,
|
|
262
302
|
error_tracking_response,
|
|
263
303
|
debug_suggestions,
|
|
304
|
+
cigs_response,
|
|
264
305
|
) = await asyncio.gather(
|
|
265
306
|
run_event_tracking(hook_type, hook_input),
|
|
266
307
|
run_orchestrator_reflection(hook_input),
|
|
267
308
|
run_task_validation(hook_input),
|
|
268
309
|
run_error_tracking(hook_input),
|
|
269
310
|
suggest_debugging_resources(hook_input),
|
|
311
|
+
run_cigs_analysis(hook_input),
|
|
270
312
|
)
|
|
271
313
|
|
|
272
314
|
# Combine responses (all should return continue=True)
|
|
@@ -307,6 +349,12 @@ async def posttooluse_hook(
|
|
|
307
349
|
if ctx:
|
|
308
350
|
guidance_parts.append(ctx)
|
|
309
351
|
|
|
352
|
+
# CIGS analysis (cost accounting and reinforcement)
|
|
353
|
+
if "hookSpecificOutput" in cigs_response:
|
|
354
|
+
ctx = cigs_response["hookSpecificOutput"].get("additionalContext", "")
|
|
355
|
+
if ctx:
|
|
356
|
+
guidance_parts.append(ctx)
|
|
357
|
+
|
|
310
358
|
# Build unified response
|
|
311
359
|
response: dict[str, Any] = {"continue": True} # PostToolUse never blocks
|
|
312
360
|
|
htmlgraph/hooks/task_enforcer.py
CHANGED
|
@@ -9,6 +9,7 @@ Architecture:
|
|
|
9
9
|
- Checks if prompt already includes save instructions
|
|
10
10
|
- Auto-injects SDK save template if missing
|
|
11
11
|
- Returns updatedInput with modified prompt
|
|
12
|
+
- Tracks parent session context and nesting depth (Phase 2)
|
|
12
13
|
|
|
13
14
|
Usage:
|
|
14
15
|
from htmlgraph.hooks.task_enforcer import enforce_task_saving
|
|
@@ -17,6 +18,7 @@ Usage:
|
|
|
17
18
|
# Returns: {"continue": True, "hookSpecificOutput": {"updatedInput": {...}}}
|
|
18
19
|
"""
|
|
19
20
|
|
|
21
|
+
import os
|
|
20
22
|
from typing import Any
|
|
21
23
|
|
|
22
24
|
|
|
@@ -124,8 +126,56 @@ def enforce_task_saving(tool_name: str, tool_params: dict[str, Any]) -> dict[str
|
|
|
124
126
|
if not prompt:
|
|
125
127
|
return {"continue": True}
|
|
126
128
|
|
|
129
|
+
# Phase 2: Track parent session context and increment nesting depth
|
|
130
|
+
parent_session = os.environ.get("HTMLGRAPH_PARENT_SESSION")
|
|
131
|
+
parent_agent = os.environ.get("HTMLGRAPH_PARENT_AGENT", "claude-code")
|
|
132
|
+
nesting_depth = int(os.environ.get("HTMLGRAPH_NESTING_DEPTH", "0"))
|
|
133
|
+
|
|
134
|
+
# Track Task invocation as activity (if parent session exists)
|
|
135
|
+
task_activity_id = None
|
|
136
|
+
if parent_session:
|
|
137
|
+
try:
|
|
138
|
+
from htmlgraph import SDK
|
|
139
|
+
|
|
140
|
+
sdk = SDK(agent=parent_agent, parent_session=parent_session)
|
|
141
|
+
|
|
142
|
+
# Track Task invocation
|
|
143
|
+
entry = sdk.track_activity(
|
|
144
|
+
tool="Task",
|
|
145
|
+
summary=f"Task invoked: {tool_params.get('description', 'Unnamed task')[:100]}",
|
|
146
|
+
payload={
|
|
147
|
+
"subagent_type": tool_params.get("subagent_type"),
|
|
148
|
+
"description": tool_params.get("description"),
|
|
149
|
+
"prompt_preview": prompt[:200] if prompt else "",
|
|
150
|
+
"nesting_depth": nesting_depth,
|
|
151
|
+
},
|
|
152
|
+
success=True,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if entry:
|
|
156
|
+
task_activity_id = entry.id
|
|
157
|
+
|
|
158
|
+
except Exception:
|
|
159
|
+
# Graceful degradation - continue even if tracking fails
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
# Increment nesting depth for child
|
|
163
|
+
new_depth = nesting_depth + 1
|
|
164
|
+
|
|
165
|
+
# Set parent activity and increment depth in environment
|
|
166
|
+
if task_activity_id:
|
|
167
|
+
os.environ["HTMLGRAPH_PARENT_ACTIVITY"] = task_activity_id
|
|
168
|
+
|
|
169
|
+
os.environ["HTMLGRAPH_NESTING_DEPTH"] = str(new_depth)
|
|
170
|
+
|
|
171
|
+
# Warn about runaway recursion
|
|
172
|
+
warning = ""
|
|
173
|
+
if new_depth > 3:
|
|
174
|
+
warning = f"\n⚠️ Warning: Nesting depth exceeds 3 levels (depth={new_depth}). Consider flattening task hierarchy."
|
|
175
|
+
|
|
127
176
|
# Check if save instructions already present
|
|
128
177
|
if has_save_instructions(prompt):
|
|
178
|
+
# Even if save instructions exist, we still need to update environment
|
|
129
179
|
return {"continue": True}
|
|
130
180
|
|
|
131
181
|
# Detect subagent type from prompt context
|
|
@@ -146,15 +196,21 @@ def enforce_task_saving(tool_name: str, tool_params: dict[str, Any]) -> dict[str
|
|
|
146
196
|
updated_params = tool_params.copy()
|
|
147
197
|
updated_params["prompt"] = modified_prompt
|
|
148
198
|
|
|
199
|
+
# Build context message
|
|
200
|
+
context_msg = (
|
|
201
|
+
f"📝 Auto-injected HtmlGraph save instructions into Task prompt. "
|
|
202
|
+
f"Subagent will be reminded to save findings using SDK.spikes. "
|
|
203
|
+
f"(depth={new_depth}, parent={parent_session[:12] if parent_session else 'none'})"
|
|
204
|
+
)
|
|
205
|
+
if warning:
|
|
206
|
+
context_msg += warning
|
|
207
|
+
|
|
149
208
|
# Return response with updatedInput
|
|
150
209
|
return {
|
|
151
210
|
"continue": True,
|
|
152
211
|
"hookSpecificOutput": {
|
|
153
212
|
"hookEventName": "PreToolUse",
|
|
154
213
|
"updatedInput": updated_params,
|
|
155
|
-
"additionalContext":
|
|
156
|
-
"📝 Auto-injected HtmlGraph save instructions into Task prompt. "
|
|
157
|
-
"Subagent will be reminded to save findings using SDK.spikes."
|
|
158
|
-
),
|
|
214
|
+
"additionalContext": context_msg,
|
|
159
215
|
},
|
|
160
216
|
}
|
htmlgraph/models.py
CHANGED
|
@@ -925,6 +925,11 @@ class Session(BaseModel):
|
|
|
925
925
|
worked_on: list[str] = Field(default_factory=list) # Feature IDs
|
|
926
926
|
continued_from: str | None = None # Previous session ID
|
|
927
927
|
|
|
928
|
+
# Parent session context (for nested Task() calls)
|
|
929
|
+
parent_session: str | None = None # Parent session ID
|
|
930
|
+
parent_activity: str | None = None # Parent activity ID
|
|
931
|
+
nesting_depth: int = 0 # Depth of nesting (0 = top-level)
|
|
932
|
+
|
|
928
933
|
# Handoff context
|
|
929
934
|
handoff_notes: str | None = None
|
|
930
935
|
recommended_next: str | None = None
|
|
@@ -1357,6 +1362,14 @@ class Session(BaseModel):
|
|
|
1357
1362
|
if self.primary_work_type
|
|
1358
1363
|
else ""
|
|
1359
1364
|
)
|
|
1365
|
+
# Parent session attributes
|
|
1366
|
+
parent_session_attrs = ""
|
|
1367
|
+
if self.parent_session:
|
|
1368
|
+
parent_session_attrs += f' data-parent-session="{self.parent_session}"'
|
|
1369
|
+
if self.parent_activity:
|
|
1370
|
+
parent_session_attrs += f' data-parent-activity="{self.parent_activity}"'
|
|
1371
|
+
if self.nesting_depth > 0:
|
|
1372
|
+
parent_session_attrs += f' data-nesting-depth="{self.nesting_depth}"'
|
|
1360
1373
|
|
|
1361
1374
|
# Serialize work_breakdown as JSON if present
|
|
1362
1375
|
import json
|
|
@@ -1466,7 +1479,7 @@ class Session(BaseModel):
|
|
|
1466
1479
|
data-agent="{self.agent}"
|
|
1467
1480
|
data-started-at="{self.started_at.isoformat()}"
|
|
1468
1481
|
data-last-activity="{self.last_activity.isoformat()}"
|
|
1469
|
-
data-event-count="{self.event_count}"{subagent_attr}{commit_attr}{ended_attr}{primary_work_type_attr}{work_breakdown_attr}{context_attrs}{transcript_attrs}>
|
|
1482
|
+
data-event-count="{self.event_count}"{subagent_attr}{commit_attr}{ended_attr}{primary_work_type_attr}{work_breakdown_attr}{context_attrs}{transcript_attrs}{parent_session_attrs}>
|
|
1470
1483
|
|
|
1471
1484
|
<header>
|
|
1472
1485
|
<h1>{title}</h1>
|