htmlgraph 0.23.5__py3-none-any.whl → 0.24.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. htmlgraph/__init__.py +5 -1
  2. htmlgraph/cigs/__init__.py +77 -0
  3. htmlgraph/cigs/autonomy.py +385 -0
  4. htmlgraph/cigs/cost.py +475 -0
  5. htmlgraph/cigs/messages_basic.py +472 -0
  6. htmlgraph/cigs/messaging.py +365 -0
  7. htmlgraph/cigs/models.py +771 -0
  8. htmlgraph/cigs/pattern_storage.py +427 -0
  9. htmlgraph/cigs/patterns.py +503 -0
  10. htmlgraph/cigs/posttool_analyzer.py +234 -0
  11. htmlgraph/cigs/tracker.py +317 -0
  12. htmlgraph/cli.py +413 -11
  13. htmlgraph/hooks/cigs_pretool_enforcer.py +350 -0
  14. htmlgraph/hooks/posttooluse.py +50 -2
  15. htmlgraph/hooks/task_enforcer.py +60 -4
  16. htmlgraph/models.py +14 -1
  17. htmlgraph/orchestration/headless_spawner.py +519 -21
  18. htmlgraph/orchestrator-system-prompt-optimized.txt +259 -53
  19. htmlgraph/reflection.py +442 -0
  20. htmlgraph/sdk.py +26 -9
  21. {htmlgraph-0.23.5.dist-info → htmlgraph-0.24.1.dist-info}/METADATA +2 -1
  22. {htmlgraph-0.23.5.dist-info → htmlgraph-0.24.1.dist-info}/RECORD +29 -17
  23. {htmlgraph-0.23.5.data → htmlgraph-0.24.1.data}/data/htmlgraph/dashboard.html +0 -0
  24. {htmlgraph-0.23.5.data → htmlgraph-0.24.1.data}/data/htmlgraph/styles.css +0 -0
  25. {htmlgraph-0.23.5.data → htmlgraph-0.24.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  26. {htmlgraph-0.23.5.data → htmlgraph-0.24.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  27. {htmlgraph-0.23.5.data → htmlgraph-0.24.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  28. {htmlgraph-0.23.5.dist-info → htmlgraph-0.24.1.dist-info}/WHEEL +0 -0
  29. {htmlgraph-0.23.5.dist-info → htmlgraph-0.24.1.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()
@@ -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, and debugging suggestions in parallel.
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 five in parallel using asyncio.gather
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
 
@@ -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>