htmlgraph 0.26.24__py3-none-any.whl → 0.27.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.
Files changed (155) hide show
  1. htmlgraph/__init__.py +23 -1
  2. htmlgraph/__init__.pyi +123 -0
  3. htmlgraph/agent_registry.py +2 -1
  4. htmlgraph/analytics/cli.py +3 -3
  5. htmlgraph/analytics/cost_analyzer.py +5 -1
  6. htmlgraph/analytics/cross_session.py +13 -9
  7. htmlgraph/analytics/dependency.py +10 -6
  8. htmlgraph/analytics/work_type.py +15 -11
  9. htmlgraph/analytics_index.py +2 -1
  10. htmlgraph/api/main.py +114 -51
  11. htmlgraph/api/templates/dashboard-redesign.html +3 -3
  12. htmlgraph/api/templates/dashboard.html +3 -3
  13. htmlgraph/api/templates/partials/work-items.html +613 -0
  14. htmlgraph/attribute_index.py +2 -1
  15. htmlgraph/builders/base.py +2 -1
  16. htmlgraph/builders/bug.py +2 -1
  17. htmlgraph/builders/chore.py +2 -1
  18. htmlgraph/builders/epic.py +2 -1
  19. htmlgraph/builders/feature.py +2 -1
  20. htmlgraph/builders/insight.py +2 -1
  21. htmlgraph/builders/metric.py +2 -1
  22. htmlgraph/builders/pattern.py +2 -1
  23. htmlgraph/builders/phase.py +2 -1
  24. htmlgraph/builders/spike.py +2 -1
  25. htmlgraph/builders/track.py +28 -1
  26. htmlgraph/cli/analytics.py +2 -1
  27. htmlgraph/cli/base.py +33 -8
  28. htmlgraph/cli/core.py +2 -1
  29. htmlgraph/cli/main.py +2 -1
  30. htmlgraph/cli/models.py +2 -1
  31. htmlgraph/cli/templates/cost_dashboard.py +2 -1
  32. htmlgraph/cli/work/__init__.py +76 -1
  33. htmlgraph/cli/work/browse.py +115 -0
  34. htmlgraph/cli/work/features.py +2 -1
  35. htmlgraph/cli/work/orchestration.py +2 -1
  36. htmlgraph/cli/work/report.py +2 -1
  37. htmlgraph/cli/work/sessions.py +2 -1
  38. htmlgraph/cli/work/snapshot.py +559 -0
  39. htmlgraph/cli/work/tracks.py +2 -1
  40. htmlgraph/collections/base.py +43 -4
  41. htmlgraph/collections/bug.py +2 -1
  42. htmlgraph/collections/chore.py +2 -1
  43. htmlgraph/collections/epic.py +2 -1
  44. htmlgraph/collections/feature.py +2 -1
  45. htmlgraph/collections/insight.py +2 -1
  46. htmlgraph/collections/metric.py +2 -1
  47. htmlgraph/collections/pattern.py +2 -1
  48. htmlgraph/collections/phase.py +2 -1
  49. htmlgraph/collections/session.py +12 -7
  50. htmlgraph/collections/spike.py +6 -1
  51. htmlgraph/collections/task_delegation.py +7 -2
  52. htmlgraph/collections/todo.py +14 -1
  53. htmlgraph/collections/traces.py +15 -10
  54. htmlgraph/context_analytics.py +2 -1
  55. htmlgraph/converter.py +11 -0
  56. htmlgraph/dependency_models.py +2 -1
  57. htmlgraph/edge_index.py +2 -1
  58. htmlgraph/event_log.py +81 -66
  59. htmlgraph/event_migration.py +2 -1
  60. htmlgraph/file_watcher.py +12 -8
  61. htmlgraph/find_api.py +2 -1
  62. htmlgraph/git_events.py +6 -2
  63. htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
  64. htmlgraph/hooks/drift_handler.py +3 -3
  65. htmlgraph/hooks/event_tracker.py +40 -61
  66. htmlgraph/hooks/installer.py +5 -1
  67. htmlgraph/hooks/orchestrator.py +92 -14
  68. htmlgraph/hooks/orchestrator_reflector.py +4 -0
  69. htmlgraph/hooks/post_tool_use_failure.py +7 -3
  70. htmlgraph/hooks/posttooluse.py +4 -0
  71. htmlgraph/hooks/prompt_analyzer.py +5 -5
  72. htmlgraph/hooks/session_handler.py +5 -2
  73. htmlgraph/hooks/session_summary.py +6 -2
  74. htmlgraph/hooks/validator.py +8 -4
  75. htmlgraph/ids.py +2 -1
  76. htmlgraph/learning.py +2 -1
  77. htmlgraph/mcp_server.py +2 -1
  78. htmlgraph/models.py +18 -1
  79. htmlgraph/operations/analytics.py +2 -1
  80. htmlgraph/operations/bootstrap.py +2 -1
  81. htmlgraph/operations/events.py +2 -1
  82. htmlgraph/operations/fastapi_server.py +2 -1
  83. htmlgraph/operations/hooks.py +2 -1
  84. htmlgraph/operations/initialization.py +2 -1
  85. htmlgraph/operations/server.py +2 -1
  86. htmlgraph/orchestration/__init__.py +4 -0
  87. htmlgraph/orchestration/claude_launcher.py +23 -20
  88. htmlgraph/orchestration/command_builder.py +2 -1
  89. htmlgraph/orchestration/headless_spawner.py +6 -2
  90. htmlgraph/orchestration/model_selection.py +7 -3
  91. htmlgraph/orchestration/plugin_manager.py +25 -21
  92. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  93. htmlgraph/orchestration/spawners/claude.py +5 -2
  94. htmlgraph/orchestration/spawners/codex.py +12 -19
  95. htmlgraph/orchestration/spawners/copilot.py +13 -18
  96. htmlgraph/orchestration/spawners/gemini.py +12 -19
  97. htmlgraph/orchestration/subprocess_runner.py +6 -3
  98. htmlgraph/orchestration/task_coordination.py +16 -8
  99. htmlgraph/orchestrator.py +2 -1
  100. htmlgraph/parallel.py +2 -1
  101. htmlgraph/query_builder.py +2 -1
  102. htmlgraph/reflection.py +2 -1
  103. htmlgraph/refs.py +344 -0
  104. htmlgraph/repo_hash.py +2 -1
  105. htmlgraph/sdk/__init__.py +398 -0
  106. htmlgraph/sdk/__init__.pyi +14 -0
  107. htmlgraph/sdk/analytics/__init__.py +19 -0
  108. htmlgraph/sdk/analytics/engine.py +155 -0
  109. htmlgraph/sdk/analytics/helpers.py +178 -0
  110. htmlgraph/sdk/analytics/registry.py +109 -0
  111. htmlgraph/sdk/base.py +484 -0
  112. htmlgraph/sdk/constants.py +216 -0
  113. htmlgraph/sdk/core.pyi +308 -0
  114. htmlgraph/sdk/discovery.py +120 -0
  115. htmlgraph/sdk/help/__init__.py +12 -0
  116. htmlgraph/sdk/help/mixin.py +699 -0
  117. htmlgraph/sdk/mixins/__init__.py +15 -0
  118. htmlgraph/sdk/mixins/attribution.py +113 -0
  119. htmlgraph/sdk/mixins/mixin.py +410 -0
  120. htmlgraph/sdk/operations/__init__.py +12 -0
  121. htmlgraph/sdk/operations/mixin.py +427 -0
  122. htmlgraph/sdk/orchestration/__init__.py +17 -0
  123. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  124. htmlgraph/sdk/orchestration/spawner.py +204 -0
  125. htmlgraph/sdk/planning/__init__.py +19 -0
  126. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  127. htmlgraph/sdk/planning/mixin.py +211 -0
  128. htmlgraph/sdk/planning/parallel.py +186 -0
  129. htmlgraph/sdk/planning/queue.py +210 -0
  130. htmlgraph/sdk/planning/recommendations.py +87 -0
  131. htmlgraph/sdk/planning/smart_planning.py +319 -0
  132. htmlgraph/sdk/session/__init__.py +19 -0
  133. htmlgraph/sdk/session/continuity.py +57 -0
  134. htmlgraph/sdk/session/handoff.py +110 -0
  135. htmlgraph/sdk/session/info.py +309 -0
  136. htmlgraph/sdk/session/manager.py +103 -0
  137. htmlgraph/server.py +21 -17
  138. htmlgraph/session_manager.py +1 -7
  139. htmlgraph/session_warning.py +2 -1
  140. htmlgraph/sessions/handoff.py +10 -3
  141. htmlgraph/system_prompts.py +2 -1
  142. htmlgraph/track_builder.py +14 -1
  143. htmlgraph/transcript.py +2 -1
  144. htmlgraph/watch.py +2 -1
  145. htmlgraph/work_type_utils.py +2 -1
  146. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/METADATA +15 -1
  147. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/RECORD +154 -117
  148. htmlgraph/sdk.py +0 -3430
  149. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/dashboard.html +0 -0
  150. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/styles.css +0 -0
  151. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  152. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  153. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  154. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/WHEEL +0 -0
  155. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/entry_points.txt +0 -0
htmlgraph/sdk.py DELETED
@@ -1,3430 +0,0 @@
1
- """
2
- HtmlGraph SDK - AI-Friendly Interface
3
-
4
- Provides a fluent, ergonomic API for AI agents with:
5
- - Auto-discovery of .htmlgraph directory
6
- - Method chaining for all operations
7
- - Context managers for auto-save
8
- - Batch operations
9
- - Minimal boilerplate
10
-
11
- Example:
12
- from htmlgraph import SDK
13
-
14
- # Auto-discovers .htmlgraph directory
15
- sdk = SDK(agent="claude")
16
-
17
- # Fluent feature creation
18
- feature = sdk.features.create(
19
- title="User Authentication",
20
- track="auth"
21
- ).add_steps([
22
- "Create login endpoint",
23
- "Add JWT middleware",
24
- "Write tests"
25
- ]).set_priority("high").save()
26
-
27
- # Work on a feature
28
- with sdk.features.get("feature-001") as feature:
29
- feature.start()
30
- feature.complete_step(0)
31
- # Auto-saves on exit
32
-
33
- # Query
34
- todos = sdk.features.where(status="todo", priority="high")
35
-
36
- # Batch operations
37
- sdk.features.mark_done(["feat-001", "feat-002", "feat-003"])
38
- """
39
-
40
- from __future__ import annotations
41
-
42
- import os
43
- from pathlib import Path
44
- from typing import Any
45
-
46
- from htmlgraph.agent_detection import detect_agent_name
47
- from htmlgraph.agents import AgentInterface
48
- from htmlgraph.analytics import Analytics, CrossSessionAnalytics, DependencyAnalytics
49
- from htmlgraph.collections import (
50
- BaseCollection,
51
- BugCollection,
52
- ChoreCollection,
53
- EpicCollection,
54
- FeatureCollection,
55
- PhaseCollection,
56
- SpikeCollection,
57
- TaskDelegationCollection,
58
- TodoCollection,
59
- )
60
- from htmlgraph.collections.insight import InsightCollection
61
- from htmlgraph.collections.metric import MetricCollection
62
- from htmlgraph.collections.pattern import PatternCollection
63
- from htmlgraph.collections.session import SessionCollection
64
- from htmlgraph.context_analytics import ContextAnalytics
65
- from htmlgraph.db.schema import HtmlGraphDB
66
- from htmlgraph.graph import HtmlGraph
67
- from htmlgraph.models import Node, Step
68
- from htmlgraph.session_manager import SessionManager
69
- from htmlgraph.session_warning import check_and_show_warning
70
- from htmlgraph.system_prompts import SystemPromptManager
71
- from htmlgraph.track_builder import TrackCollection
72
- from htmlgraph.types import (
73
- ActiveWorkItem,
74
- BottleneckDict,
75
- SessionStartInfo,
76
- )
77
-
78
-
79
- class SDK:
80
- """
81
- Main SDK interface for AI agents.
82
-
83
- Auto-discovers .htmlgraph directory and provides fluent API for all collections.
84
-
85
- Available Collections:
86
- - features: Feature work items with builder support
87
- - bugs: Bug reports
88
- - chores: Maintenance and chore tasks
89
- - spikes: Investigation and research spikes
90
- - epics: Large bodies of work
91
- - phases: Project phases
92
- - sessions: Agent sessions
93
- - tracks: Work tracks
94
- - agents: Agent information
95
- - todos: Persistent task tracking (mirrors TodoWrite API)
96
- - patterns: Workflow patterns (optimal/anti-pattern)
97
- - insights: Session health insights
98
- - metrics: Aggregated time-series metrics
99
-
100
- System Prompt Management:
101
- sdk.system_prompts - Manage system prompts
102
- .get_active() - Get active prompt (project override OR plugin default)
103
- .get_default() - Get plugin default system prompt
104
- .get_project() - Get project-level override if exists
105
- .create(template) - Create project-level override
106
- .validate() - Validate prompt token count
107
- .delete() - Delete project override (fall back to default)
108
- .get_stats() - Get prompt statistics
109
-
110
- Analytics & Decision Support:
111
- sdk.dep_analytics - Dependency analysis
112
- .find_bottlenecks(top_n=5) - Find blocking tasks
113
- .get_parallel_work(max_agents=5) - Find parallelizable work
114
- .recommend_next_tasks(agent_count=1) - Smart recommendations
115
- .assess_dependency_risk() - Check for circular deps
116
- .impact_analysis(node_id) - See what task unlocks
117
-
118
- sdk.analytics - Work analytics
119
- .get_work_type_distribution() - Breakdown by type
120
- .get_spike_to_feature_ratio() - Investigation vs implementation
121
- .get_maintenance_burden() - Chore vs feature ratio
122
-
123
- sdk.context - Context tracking
124
- .get_context_usage() - Session context metrics
125
- .get_context_efficiency() - Efficiency score
126
-
127
- Discovery & Help:
128
- sdk.help() - Get structured help for all operations
129
- sdk.help('analytics') - Get analytics-specific help
130
- sdk.help('features') - Get feature collection help
131
-
132
- Error Handling Patterns
133
- =======================
134
-
135
- SDK methods follow consistent error handling patterns by operation type:
136
-
137
- LOOKUP OPERATIONS (Return None):
138
- Single-item lookups return None when not found.
139
- Always check the result before using.
140
-
141
- >>> feature = sdk.features.get("nonexistent")
142
- >>> if feature:
143
- ... print(feature.title)
144
- ... else:
145
- ... print("Not found")
146
-
147
- QUERY OPERATIONS (Return Empty List):
148
- Queries return empty list when no matches or on error.
149
- Safe to iterate without checking.
150
-
151
- >>> results = sdk.features.where(status="impossible")
152
- >>> for r in results: # Empty iteration is safe
153
- ... print(r.title)
154
-
155
- EDIT OPERATIONS (Raise Exception):
156
- Edit operations raise NodeNotFoundError when target missing.
157
- Use try/except to handle gracefully.
158
-
159
- >>> from htmlgraph.exceptions import NodeNotFoundError
160
- >>> try:
161
- ... with sdk.features.edit("nonexistent") as f:
162
- ... f.status = "done"
163
- ... except NodeNotFoundError:
164
- ... print("Feature not found")
165
-
166
- CREATE OPERATIONS (Raise on Validation):
167
- Create operations raise ValidationError on invalid input.
168
-
169
- >>> try:
170
- ... sdk.features.create("") # Empty title
171
- ... except ValidationError:
172
- ... print("Title required")
173
-
174
- BATCH OPERATIONS (Return Results Dict):
175
- Batch operations return dict with success_count, failed_ids, and warnings.
176
- Provides detailed feedback for partial failures.
177
-
178
- >>> result = sdk.features.mark_done(["feat-1", "missing", "feat-2"])
179
- >>> print(f"Completed {result['success_count']} of 3")
180
- >>> if result['failed_ids']:
181
- ... print(f"Failed: {result['failed_ids']}")
182
- ... print(f"Reasons: {result['warnings']}")
183
-
184
- Pattern Summary:
185
- | Operation Type | Error Behavior | Example Method |
186
- |----------------|--------------------|-----------------------|
187
- | Lookup | Return None | .get(id) |
188
- | Query | Return [] | .where(), .all() |
189
- | Edit | Raise Exception | .edit(id) |
190
- | Create | Raise on Invalid | .create(title) |
191
- | Batch | Return Results Dict| .mark_done([ids]) |
192
- | Delete | Return Bool | .delete(id) |
193
-
194
- Available Exceptions:
195
- - NodeNotFoundError: Node with ID not found
196
- - ValidationError: Invalid input parameters
197
- - ClaimConflictError: Node already claimed by another agent
198
-
199
- Example:
200
- sdk = SDK(agent="claude")
201
-
202
- # Work with features (has builder support)
203
- feature = sdk.features.create("User Auth")
204
- .set_priority("high")
205
- .add_steps(["Login", "Logout"])
206
- .save()
207
-
208
- # Work with bugs
209
- high_bugs = sdk.bugs.where(status="todo", priority="high")
210
- with sdk.bugs.edit("bug-001") as bug:
211
- bug.status = "in-progress"
212
-
213
- # Work with any collection
214
- all_spikes = sdk.spikes.all()
215
- sdk.chores.mark_done(["chore-001", "chore-002"])
216
- sdk.epics.assign(["epic-001"], agent="claude")
217
- """
218
-
219
- def __init__(
220
- self,
221
- directory: Path | str | None = None,
222
- agent: str | None = None,
223
- parent_session: str | None = None,
224
- db_path: str | None = None,
225
- ):
226
- """
227
- Initialize SDK.
228
-
229
- Args:
230
- directory: Path to .htmlgraph directory (auto-discovered if not provided)
231
- agent: REQUIRED - Agent identifier for operations.
232
- Used to attribute work items (features, spikes, bugs, etc) to the agent.
233
- Examples: agent='explorer', agent='coder', agent='tester'
234
- Critical for: Work attribution, result retrieval, orchestrator tracking
235
- Falls back to: CLAUDE_AGENT_NAME env var, then detect_agent_name()
236
- Raises ValueError if not provided and cannot be detected
237
- parent_session: Parent session ID to log activities to (for nested contexts)
238
- db_path: Path to SQLite database file (optional, defaults to ~/.htmlgraph/htmlgraph.db)
239
- """
240
- if directory is None:
241
- directory = self._discover_htmlgraph()
242
-
243
- if agent is None:
244
- # Try environment variable fallback
245
- agent = os.getenv("CLAUDE_AGENT_NAME")
246
-
247
- if agent is None:
248
- # Try automatic detection
249
- detected = detect_agent_name()
250
- if detected and detected != "cli":
251
- # Only accept detected if it's not the default fallback
252
- agent = detected
253
- else:
254
- # No valid agent found - fail fast with helpful error message
255
- raise ValueError(
256
- "Agent identifier is required for work attribution. "
257
- "Pass agent='name' to SDK() initialization. "
258
- "Examples: SDK(agent='explorer'), SDK(agent='coder'), SDK(agent='tester')\n"
259
- "Alternatively, set CLAUDE_AGENT_NAME environment variable.\n"
260
- "Critical for: Work attribution, result retrieval, orchestrator tracking"
261
- )
262
-
263
- self._directory = Path(directory)
264
- self._agent_id = agent
265
- self._parent_session = parent_session or os.getenv("HTMLGRAPH_PARENT_SESSION")
266
-
267
- # Initialize SQLite database (Phase 2)
268
- self._db = HtmlGraphDB(
269
- db_path or str(Path.home() / ".htmlgraph" / "htmlgraph.db")
270
- )
271
- self._db.connect()
272
- self._db.create_tables()
273
-
274
- # Initialize underlying HtmlGraphs first (for backward compatibility and sharing)
275
- # These are shared with SessionManager to avoid double-loading features
276
- self._graph = HtmlGraph(self._directory / "features")
277
- self._bugs_graph = HtmlGraph(self._directory / "bugs")
278
-
279
- # Initialize SessionManager with shared graph instances to avoid double-loading
280
- self.session_manager = SessionManager(
281
- self._directory,
282
- features_graph=self._graph,
283
- bugs_graph=self._bugs_graph,
284
- )
285
-
286
- # Agent interface (for backward compatibility)
287
- self._agent_interface = AgentInterface(
288
- self._directory / "features", agent_id=agent
289
- )
290
-
291
- # Collection interfaces - all work item types (all with builder support)
292
- self.features = FeatureCollection(self)
293
- self.bugs = BugCollection(self)
294
- self.chores = ChoreCollection(self)
295
- self.spikes = SpikeCollection(self)
296
- self.epics = EpicCollection(self)
297
- self.phases = PhaseCollection(self)
298
-
299
- # Non-work collections
300
- self.sessions: SessionCollection = SessionCollection(self)
301
- self.tracks: TrackCollection = TrackCollection(
302
- self
303
- ) # Use specialized collection with builder support
304
- self.agents: BaseCollection = BaseCollection(self, "agents", "agent")
305
-
306
- # Learning collections (Active Learning Persistence)
307
- self.patterns = PatternCollection(self)
308
- self.insights = InsightCollection(self)
309
- self.metrics = MetricCollection(self)
310
-
311
- # Todo collection (persistent task tracking)
312
- self.todos = TodoCollection(self)
313
-
314
- # Task delegation collection (observability for spawned agents)
315
- self.task_delegations = TaskDelegationCollection(self)
316
-
317
- # Create learning directories if needed
318
- (self._directory / "patterns").mkdir(exist_ok=True)
319
- (self._directory / "insights").mkdir(exist_ok=True)
320
- (self._directory / "metrics").mkdir(exist_ok=True)
321
- (self._directory / "todos").mkdir(exist_ok=True)
322
- (self._directory / "task-delegations").mkdir(exist_ok=True)
323
-
324
- # Analytics interface (Phase 2: Work Type Analytics)
325
- self.analytics = Analytics(self)
326
-
327
- # Dependency analytics interface (Advanced graph analytics)
328
- self.dep_analytics = DependencyAnalytics(self._graph)
329
-
330
- # Cross-session analytics interface (Git commit-based analytics)
331
- self.cross_session_analytics = CrossSessionAnalytics(self)
332
-
333
- # Context analytics interface (Context usage tracking)
334
- self.context = ContextAnalytics(self)
335
-
336
- # Pattern learning interface (Phase 2: Behavior Pattern Learning)
337
- from htmlgraph.analytics.pattern_learning import PatternLearner
338
-
339
- self.pattern_learning = PatternLearner(self._directory)
340
-
341
- # Lazy-loaded orchestrator for subagent management
342
- self._orchestrator: Any = None
343
-
344
- # System prompt manager (lazy-loaded)
345
- self._system_prompts: SystemPromptManager | None = None
346
-
347
- # Session warning system (workaround for Claude Code hook bug #10373)
348
- # Shows orchestrator instructions on first SDK usage per session
349
- self._session_warning = check_and_show_warning(
350
- self._directory,
351
- agent=self._agent_id,
352
- session_id=None, # Will be set by session manager if available
353
- )
354
-
355
- @staticmethod
356
- def _discover_htmlgraph() -> Path:
357
- """
358
- Auto-discover .htmlgraph directory.
359
-
360
- Searches current directory and parents.
361
- """
362
- current = Path.cwd()
363
-
364
- # Check current directory
365
- if (current / ".htmlgraph").exists():
366
- return current / ".htmlgraph"
367
-
368
- # Check parent directories
369
- for parent in current.parents:
370
- if (parent / ".htmlgraph").exists():
371
- return parent / ".htmlgraph"
372
-
373
- # Default to current directory
374
- return current / ".htmlgraph"
375
-
376
- @property
377
- def agent(self) -> str | None:
378
- """Get current agent ID."""
379
- return self._agent_id
380
-
381
- @property
382
- def system_prompts(self) -> SystemPromptManager:
383
- """
384
- Access system prompt management.
385
-
386
- Provides methods to:
387
- - Get active prompt (project override OR plugin default)
388
- - Create/delete project-level overrides
389
- - Validate token counts
390
- - Get prompt statistics
391
-
392
- Lazy-loaded on first access.
393
-
394
- Returns:
395
- SystemPromptManager instance
396
-
397
- Example:
398
- >>> sdk = SDK(agent="claude")
399
-
400
- # Get active prompt
401
- >>> prompt = sdk.system_prompts.get_active()
402
-
403
- # Create project override
404
- >>> sdk.system_prompts.create("## Custom prompt\\n...")
405
-
406
- # Validate token count
407
- >>> result = sdk.system_prompts.validate()
408
- >>> print(result['message'])
409
-
410
- # Get statistics
411
- >>> stats = sdk.system_prompts.get_stats()
412
- >>> print(f"Source: {stats['source']}")
413
- """
414
- if self._system_prompts is None:
415
- self._system_prompts = SystemPromptManager(self._directory)
416
- return self._system_prompts
417
-
418
- def dismiss_session_warning(self) -> bool:
419
- """
420
- Dismiss the session warning after reading it.
421
-
422
- IMPORTANT: Call this as your FIRST action after seeing the orchestrator
423
- warning. This confirms you've read the instructions.
424
-
425
- Returns:
426
- True if warning was dismissed, False if already dismissed
427
-
428
- Example:
429
- sdk = SDK(agent="claude")
430
- # Warning shown automatically...
431
-
432
- # First action: dismiss to confirm you read it
433
- sdk.dismiss_session_warning()
434
-
435
- # Now proceed with orchestration
436
- sdk.spawn_coder(feature_id="feat-123", ...)
437
- """
438
- if self._session_warning:
439
- return self._session_warning.dismiss(
440
- agent=self._agent_id,
441
- session_id=None,
442
- )
443
- return False
444
-
445
- def get_warning_status(self) -> dict[str, Any]:
446
- """
447
- Get current session warning status.
448
-
449
- Returns:
450
- Dict with dismissed status, timestamp, and show count
451
- """
452
- if self._session_warning:
453
- return self._session_warning.get_status()
454
- return {"dismissed": True, "show_count": 0}
455
-
456
- # =========================================================================
457
- # SQLite Database Integration (Phase 2)
458
- # =========================================================================
459
-
460
- def db(self) -> HtmlGraphDB:
461
- """
462
- Get the SQLite database instance.
463
-
464
- Returns:
465
- HtmlGraphDB instance for executing queries
466
-
467
- Example:
468
- >>> sdk = SDK(agent="claude")
469
- >>> db = sdk.db()
470
- >>> events = db.get_session_events("sess-123")
471
- >>> features = db.get_features_by_status("todo")
472
- """
473
- return self._db
474
-
475
- def query(self, sql: str, params: tuple = ()) -> list[dict[str, Any]]:
476
- """
477
- Execute a raw SQL query on the SQLite database.
478
-
479
- Args:
480
- sql: SQL query string
481
- params: Query parameters (for safe parameterized queries)
482
-
483
- Returns:
484
- List of result dictionaries
485
-
486
- Example:
487
- >>> sdk = SDK(agent="claude")
488
- >>> results = sdk.query(
489
- ... "SELECT * FROM features WHERE status = ? AND priority = ?",
490
- ... ("todo", "high")
491
- ... )
492
- >>> for row in results:
493
- ... print(row["title"])
494
- """
495
- if not self._db.connection:
496
- self._db.connect()
497
-
498
- cursor = self._db.connection.cursor() # type: ignore[union-attr]
499
- cursor.execute(sql, params)
500
- rows = cursor.fetchall()
501
- return [dict(row) for row in rows]
502
-
503
- def execute_query_builder(
504
- self, sql: str, params: tuple = ()
505
- ) -> list[dict[str, Any]]:
506
- """
507
- Execute a query using the Queries builder.
508
-
509
- Args:
510
- sql: SQL query from Queries builder
511
- params: Parameters from Queries builder
512
-
513
- Returns:
514
- List of result dictionaries
515
-
516
- Example:
517
- >>> sdk = SDK(agent="claude")
518
- >>> sql, params = Queries.get_features_by_status("todo", limit=5)
519
- >>> results = sdk.execute_query_builder(sql, params)
520
- """
521
- return self.query(sql, params)
522
-
523
- def export_to_html(
524
- self,
525
- output_dir: str | None = None,
526
- include_features: bool = True,
527
- include_sessions: bool = True,
528
- include_events: bool = False,
529
- ) -> dict[str, int]:
530
- """
531
- Export SQLite data to HTML files for backward compatibility.
532
-
533
- Args:
534
- output_dir: Directory to export to (defaults to .htmlgraph)
535
- include_features: Export features
536
- include_sessions: Export sessions
537
- include_events: Export events (detailed, use with care)
538
-
539
- Returns:
540
- Dict with export counts: {"features": int, "sessions": int, "events": int}
541
-
542
- Example:
543
- >>> sdk = SDK(agent="claude")
544
- >>> result = sdk.export_to_html()
545
- >>> print(f"Exported {result['features']} features")
546
- """
547
- if output_dir is None:
548
- output_dir = str(self._directory)
549
-
550
- output_path = Path(output_dir)
551
- counts = {"features": 0, "sessions": 0, "events": 0}
552
-
553
- if include_features:
554
- # Export all features from SQLite to HTML
555
- features_dir = output_path / "features"
556
- features_dir.mkdir(parents=True, exist_ok=True)
557
-
558
- try:
559
- cursor = self._db.connection.cursor() # type: ignore[union-attr]
560
- cursor.execute("SELECT * FROM features")
561
- rows = cursor.fetchall()
562
-
563
- for row in rows:
564
- feature_dict = dict(row)
565
- feature_id = feature_dict["id"]
566
- # Write HTML file (simplified export)
567
- html_file = features_dir / f"{feature_id}.html"
568
- html_file.write_text(
569
- f"<h1>{feature_dict['title']}</h1>"
570
- f"<p>Status: {feature_dict['status']}</p>"
571
- f"<p>Type: {feature_dict['type']}</p>"
572
- )
573
- counts["features"] += 1
574
- except Exception as e:
575
- import logging
576
-
577
- logging.error(f"Error exporting features: {e}")
578
-
579
- if include_sessions:
580
- # Export all sessions from SQLite to HTML
581
- sessions_dir = output_path / "sessions"
582
- sessions_dir.mkdir(parents=True, exist_ok=True)
583
-
584
- try:
585
- cursor = self._db.connection.cursor() # type: ignore[union-attr]
586
- cursor.execute("SELECT * FROM sessions")
587
- rows = cursor.fetchall()
588
-
589
- for row in rows:
590
- session_dict = dict(row)
591
- session_id = session_dict["session_id"]
592
- # Write HTML file (simplified export)
593
- html_file = sessions_dir / f"{session_id}.html"
594
- html_file.write_text(
595
- f"<h1>Session {session_id}</h1>"
596
- f"<p>Agent: {session_dict['agent_assigned']}</p>"
597
- f"<p>Status: {session_dict['status']}</p>"
598
- )
599
- counts["sessions"] += 1
600
- except Exception as e:
601
- import logging
602
-
603
- logging.error(f"Error exporting sessions: {e}")
604
-
605
- return counts
606
-
607
- def _log_event(
608
- self,
609
- event_type: str,
610
- tool_name: str | None = None,
611
- input_summary: str | None = None,
612
- output_summary: str | None = None,
613
- context: dict[str, Any] | None = None,
614
- cost_tokens: int = 0,
615
- ) -> bool:
616
- """
617
- Log an event to the SQLite database with parent-child linking.
618
-
619
- Internal method used by collections to track operations.
620
- Automatically creates a session if one doesn't exist.
621
- Reads parent event ID from HTMLGRAPH_PARENT_ACTIVITY env var for hierarchical tracking.
622
-
623
- Args:
624
- event_type: Type of event (tool_call, completion, error, etc.)
625
- tool_name: Tool that was called
626
- input_summary: Summary of input
627
- output_summary: Summary of output
628
- context: Additional context metadata
629
- cost_tokens: Token cost estimate
630
-
631
- Returns:
632
- True if logged successfully, False otherwise
633
-
634
- Example (internal use):
635
- >>> sdk._log_event(
636
- ... event_type="tool_call",
637
- ... tool_name="Edit",
638
- ... input_summary="Edit file.py",
639
- ... cost_tokens=100
640
- ... )
641
- """
642
- from uuid import uuid4
643
-
644
- event_id = f"evt-{uuid4().hex[:12]}"
645
- session_id = self._parent_session or "cli-session"
646
-
647
- # Read parent event ID from environment variable for hierarchical linking
648
- parent_event_id = os.getenv("HTMLGRAPH_PARENT_ACTIVITY")
649
-
650
- # Ensure session exists before logging event
651
- try:
652
- self._ensure_session_exists(session_id, parent_event_id=parent_event_id)
653
- except Exception as e:
654
- import logging
655
-
656
- logging.debug(f"Failed to ensure session exists: {e}")
657
- # Continue anyway - session creation failure shouldn't block event logging
658
-
659
- return self._db.insert_event(
660
- event_id=event_id,
661
- agent_id=self._agent_id,
662
- event_type=event_type,
663
- session_id=session_id,
664
- tool_name=tool_name,
665
- input_summary=input_summary,
666
- output_summary=output_summary,
667
- context=context,
668
- parent_event_id=parent_event_id,
669
- cost_tokens=cost_tokens,
670
- )
671
-
672
- def _ensure_session_exists(
673
- self, session_id: str, parent_event_id: str | None = None
674
- ) -> None:
675
- """
676
- Create a session record if it doesn't exist.
677
-
678
- Args:
679
- session_id: Session ID to ensure exists
680
- parent_event_id: Event that spawned this session (optional)
681
- """
682
- if not self._db.connection:
683
- self._db.connect()
684
-
685
- cursor = self._db.connection.cursor() # type: ignore[union-attr]
686
- cursor.execute(
687
- "SELECT COUNT(*) FROM sessions WHERE session_id = ?", (session_id,)
688
- )
689
- exists = cursor.fetchone()[0] > 0
690
-
691
- if not exists:
692
- # Create session record
693
- self._db.insert_session(
694
- session_id=session_id,
695
- agent_assigned=self._agent_id,
696
- is_subagent=self._parent_session is not None,
697
- parent_session_id=self._parent_session,
698
- parent_event_id=parent_event_id,
699
- )
700
-
701
- def reload(self) -> None:
702
- """Reload all data from disk."""
703
- self._graph.reload()
704
- self._agent_interface.reload()
705
- # SessionManager reloads implicitly on access via its converters/graphs
706
-
707
- def summary(self, max_items: int = 10) -> str:
708
- """
709
- Get project summary.
710
-
711
- Returns:
712
- Compact overview for AI agent orientation
713
- """
714
- return self._agent_interface.get_summary(max_items)
715
-
716
- def my_work(self) -> dict[str, Any]:
717
- """
718
- Get current agent's workload.
719
-
720
- Returns:
721
- Dict with in_progress, completed counts
722
- """
723
- if not self._agent_id:
724
- raise ValueError("No agent ID set")
725
- return self._agent_interface.get_workload(self._agent_id)
726
-
727
- def next_task(
728
- self, priority: str | None = None, auto_claim: bool = True
729
- ) -> Node | None:
730
- """
731
- Get next available task for this agent.
732
-
733
- Args:
734
- priority: Optional priority filter
735
- auto_claim: Automatically claim the task
736
-
737
- Returns:
738
- Next available Node or None
739
- """
740
- return self._agent_interface.get_next_task(
741
- agent_id=self._agent_id,
742
- priority=priority,
743
- node_type="feature",
744
- auto_claim=auto_claim,
745
- )
746
-
747
- def set_session_handoff(
748
- self,
749
- handoff_notes: str | None = None,
750
- recommended_next: str | None = None,
751
- blockers: list[str] | None = None,
752
- session_id: str | None = None,
753
- ) -> Any:
754
- """
755
- Set handoff context on a session.
756
-
757
- Args:
758
- handoff_notes: Notes for next session/agent
759
- recommended_next: Suggested next steps
760
- blockers: List of blockers
761
- session_id: Specific session ID (defaults to active session)
762
-
763
- Returns:
764
- Updated Session or None if not found
765
- """
766
- if not session_id:
767
- if self._agent_id:
768
- active = self.session_manager.get_active_session_for_agent(
769
- self._agent_id
770
- )
771
- else:
772
- active = self.session_manager.get_active_session()
773
- if not active:
774
- return None
775
- session_id = active.id
776
-
777
- return self.session_manager.set_session_handoff(
778
- session_id=session_id,
779
- handoff_notes=handoff_notes,
780
- recommended_next=recommended_next,
781
- blockers=blockers,
782
- )
783
-
784
- def continue_from_last(
785
- self,
786
- agent: str | None = None,
787
- auto_create_session: bool = True,
788
- ) -> tuple[Any, Any]:
789
- """
790
- Continue work from the last completed session.
791
-
792
- Loads context from previous session including handoff notes,
793
- recommended files, blockers, and recent commits.
794
-
795
- Args:
796
- agent: Filter by agent (None = current SDK agent)
797
- auto_create_session: Create new session if True
798
-
799
- Returns:
800
- Tuple of (new_session, resume_info) or (None, None)
801
-
802
- Example:
803
- >>> sdk = SDK(agent="claude")
804
- >>> session, resume = sdk.continue_from_last()
805
- >>> if resume:
806
- ... print(resume.summary)
807
- ... print(resume.next_focus)
808
- ... for file in resume.recommended_files:
809
- ... print(f" - {file}")
810
- """
811
- if not agent:
812
- agent = self._agent_id
813
-
814
- return self.session_manager.continue_from_last(
815
- agent=agent,
816
- auto_create_session=auto_create_session,
817
- )
818
-
819
- def end_session_with_handoff(
820
- self,
821
- session_id: str | None = None,
822
- summary: str | None = None,
823
- next_focus: str | None = None,
824
- blockers: list[str] | None = None,
825
- keep_context: list[str] | None = None,
826
- auto_recommend_context: bool = True,
827
- ) -> Any:
828
- """
829
- End session with handoff information for next session.
830
-
831
- Args:
832
- session_id: Session to end (None = active session)
833
- summary: What was accomplished
834
- next_focus: What should be done next
835
- blockers: List of blockers
836
- keep_context: List of files to keep context for
837
- auto_recommend_context: Auto-recommend files from git
838
-
839
- Returns:
840
- Updated Session or None
841
-
842
- Example:
843
- >>> sdk.end_session_with_handoff(
844
- ... summary="Completed OAuth integration",
845
- ... next_focus="Implement JWT token refresh",
846
- ... blockers=["Waiting for security review"],
847
- ... keep_context=["src/auth/oauth.py"]
848
- ... )
849
- """
850
- if not session_id:
851
- if self._agent_id:
852
- active = self.session_manager.get_active_session_for_agent(
853
- self._agent_id
854
- )
855
- else:
856
- active = self.session_manager.get_active_session()
857
- if not active:
858
- return None
859
- session_id = active.id
860
-
861
- return self.session_manager.end_session_with_handoff(
862
- session_id=session_id,
863
- summary=summary,
864
- next_focus=next_focus,
865
- blockers=blockers,
866
- keep_context=keep_context,
867
- auto_recommend_context=auto_recommend_context,
868
- )
869
-
870
- def start_session(
871
- self,
872
- session_id: str | None = None,
873
- title: str | None = None,
874
- agent: str | None = None,
875
- ) -> Any:
876
- """
877
- Start a new session.
878
-
879
- Args:
880
- session_id: Optional session ID
881
- title: Optional session title
882
- agent: Optional agent override (defaults to SDK agent)
883
-
884
- Returns:
885
- New Session instance
886
- """
887
- return self.session_manager.start_session(
888
- session_id=session_id,
889
- agent=agent or self._agent_id or "cli",
890
- title=title,
891
- parent_session_id=self._parent_session,
892
- )
893
-
894
- def end_session(
895
- self,
896
- session_id: str,
897
- handoff_notes: str | None = None,
898
- recommended_next: str | None = None,
899
- blockers: list[str] | None = None,
900
- ) -> Any:
901
- """
902
- End a session.
903
-
904
- Args:
905
- session_id: Session ID to end
906
- handoff_notes: Optional handoff notes
907
- recommended_next: Optional recommendations
908
- blockers: Optional blockers
909
-
910
- Returns:
911
- Ended Session instance
912
- """
913
- return self.session_manager.end_session(
914
- session_id=session_id,
915
- handoff_notes=handoff_notes,
916
- recommended_next=recommended_next,
917
- blockers=blockers,
918
- )
919
-
920
- def get_status(self) -> dict[str, Any]:
921
- """
922
- Get project status.
923
-
924
- Returns:
925
- Dict with status metrics (WIP, counts, etc.)
926
- """
927
- return self.session_manager.get_status()
928
-
929
- def dedupe_sessions(
930
- self,
931
- max_events: int = 1,
932
- move_dir_name: str = "_orphans",
933
- dry_run: bool = False,
934
- stale_extra_active: bool = True,
935
- ) -> dict[str, int]:
936
- """
937
- Move low-signal sessions (e.g. SessionStart-only) out of the main sessions dir.
938
-
939
- Args:
940
- max_events: Maximum events threshold (sessions with <= this many events are moved)
941
- move_dir_name: Directory name to move orphaned sessions to
942
- dry_run: If True, only report what would be done without actually moving files
943
- stale_extra_active: If True, also mark extra active sessions as stale
944
-
945
- Returns:
946
- Dict with counts: {"scanned": int, "moved": int, "missing": int, "staled_active": int, "kept_active": int}
947
-
948
- Example:
949
- >>> sdk = SDK(agent="claude")
950
- >>> result = sdk.dedupe_sessions(max_events=1, dry_run=False)
951
- >>> print(f"Scanned: {result['scanned']}, Moved: {result['moved']}")
952
- """
953
- return self.session_manager.dedupe_orphan_sessions(
954
- max_events=max_events,
955
- move_dir_name=move_dir_name,
956
- dry_run=dry_run,
957
- stale_extra_active=stale_extra_active,
958
- )
959
-
960
- def track_activity(
961
- self,
962
- tool: str,
963
- summary: str,
964
- file_paths: list[str] | None = None,
965
- success: bool = True,
966
- feature_id: str | None = None,
967
- session_id: str | None = None,
968
- parent_activity_id: str | None = None,
969
- payload: dict[str, Any] | None = None,
970
- ) -> Any:
971
- """
972
- Track an activity in the current or specified session.
973
-
974
- Args:
975
- tool: Tool name (Edit, Bash, Read, etc.)
976
- summary: Human-readable summary of the activity
977
- file_paths: Files involved in this activity
978
- success: Whether the tool call succeeded
979
- feature_id: Explicit feature ID (skips attribution if provided)
980
- session_id: Session ID (defaults to parent session if available, then active session)
981
- parent_activity_id: ID of parent activity (e.g., Skill/Task invocation)
982
- payload: Optional rich payload data
983
-
984
- Returns:
985
- Created ActivityEntry with attribution
986
-
987
- Example:
988
- >>> sdk = SDK(agent="claude")
989
- >>> entry = sdk.track_activity(
990
- ... tool="CustomTool",
991
- ... summary="Performed custom analysis",
992
- ... file_paths=["src/main.py"],
993
- ... success=True
994
- ... )
995
- >>> print(f"Tracked: [{entry.tool}] {entry.summary}")
996
- """
997
- # Determine target session: explicit parameter > parent_session > active > none
998
- if not session_id:
999
- # Priority 1: Parent session (explicitly provided or from env var)
1000
- if self._parent_session:
1001
- session_id = self._parent_session
1002
- else:
1003
- # Priority 2: Active session for this agent
1004
- active = self.session_manager.get_active_session(agent=self._agent_id)
1005
- if active:
1006
- session_id = active.id
1007
- else:
1008
- raise ValueError(
1009
- "No active session. Start one with sdk.start_session()"
1010
- )
1011
-
1012
- # Get parent activity ID from environment if not provided
1013
- if not parent_activity_id:
1014
- parent_activity_id = os.getenv("HTMLGRAPH_PARENT_ACTIVITY")
1015
-
1016
- return self.session_manager.track_activity(
1017
- session_id=session_id,
1018
- tool=tool,
1019
- summary=summary,
1020
- file_paths=file_paths,
1021
- success=success,
1022
- feature_id=feature_id,
1023
- parent_activity_id=parent_activity_id,
1024
- payload=payload,
1025
- )
1026
-
1027
- # =========================================================================
1028
- # Strategic Planning & Analytics (Agent-Friendly Interface)
1029
- # =========================================================================
1030
-
1031
- def find_bottlenecks(self, top_n: int = 5) -> list[BottleneckDict]:
1032
- """
1033
- Identify tasks blocking the most downstream work.
1034
-
1035
- Note: Prefer using sdk.dep_analytics.find_bottlenecks() directly.
1036
- This method exists for backward compatibility.
1037
-
1038
- Args:
1039
- top_n: Maximum number of bottlenecks to return
1040
-
1041
- Returns:
1042
- List of bottleneck tasks with impact metrics
1043
-
1044
- Example:
1045
- >>> sdk = SDK(agent="claude")
1046
- >>> # Preferred approach
1047
- >>> bottlenecks = sdk.dep_analytics.find_bottlenecks(top_n=3)
1048
- >>> # Or via SDK (backward compatibility)
1049
- >>> bottlenecks = sdk.find_bottlenecks(top_n=3)
1050
- >>> for bn in bottlenecks:
1051
- ... print(f"{bn['title']} blocks {bn['blocks_count']} tasks")
1052
- """
1053
- bottlenecks = self.dep_analytics.find_bottlenecks(top_n=top_n)
1054
-
1055
- # Convert to agent-friendly dict format for backward compatibility
1056
- return [
1057
- {
1058
- "id": bn.id,
1059
- "title": bn.title,
1060
- "status": bn.status,
1061
- "priority": bn.priority,
1062
- "blocks_count": bn.transitive_blocking,
1063
- "impact_score": bn.weighted_impact,
1064
- "blocked_tasks": bn.blocked_nodes[:5],
1065
- }
1066
- for bn in bottlenecks
1067
- ]
1068
-
1069
- def get_parallel_work(self, max_agents: int = 5) -> dict[str, Any]:
1070
- """
1071
- Find tasks that can be worked on simultaneously.
1072
-
1073
- Note: Prefer using sdk.dep_analytics.find_parallelizable_work() directly.
1074
- This method exists for backward compatibility.
1075
-
1076
- Args:
1077
- max_agents: Maximum number of parallel agents to plan for
1078
-
1079
- Returns:
1080
- Dict with parallelization opportunities
1081
-
1082
- Example:
1083
- >>> sdk = SDK(agent="claude")
1084
- >>> # Preferred approach
1085
- >>> report = sdk.dep_analytics.find_parallelizable_work(status="todo")
1086
- >>> # Or via SDK (backward compatibility)
1087
- >>> parallel = sdk.get_parallel_work(max_agents=3)
1088
- >>> print(f"Can work on {parallel['max_parallelism']} tasks at once")
1089
- >>> print(f"Ready now: {parallel['ready_now']}")
1090
- """
1091
- report = self.dep_analytics.find_parallelizable_work(status="todo")
1092
-
1093
- ready_now = (
1094
- report.dependency_levels[0].nodes if report.dependency_levels else []
1095
- )
1096
-
1097
- return {
1098
- "max_parallelism": report.max_parallelism,
1099
- "ready_now": ready_now[:max_agents],
1100
- "total_ready": len(ready_now),
1101
- "level_count": len(report.dependency_levels),
1102
- "next_level": report.dependency_levels[1].nodes
1103
- if len(report.dependency_levels) > 1
1104
- else [],
1105
- }
1106
-
1107
- def recommend_next_work(self, agent_count: int = 1) -> list[dict[str, Any]]:
1108
- """
1109
- Get smart recommendations for what to work on next.
1110
-
1111
- Note: Prefer using sdk.dep_analytics.recommend_next_tasks() directly.
1112
- This method exists for backward compatibility.
1113
-
1114
- Considers priority, dependencies, and transitive impact.
1115
-
1116
- Args:
1117
- agent_count: Number of agents/tasks to recommend
1118
-
1119
- Returns:
1120
- List of recommended tasks with reasoning
1121
-
1122
- Example:
1123
- >>> sdk = SDK(agent="claude")
1124
- >>> # Preferred approach
1125
- >>> recs = sdk.dep_analytics.recommend_next_tasks(agent_count=3)
1126
- >>> # Or via SDK (backward compatibility)
1127
- >>> recs = sdk.recommend_next_work(agent_count=3)
1128
- >>> for rec in recs:
1129
- ... print(f"{rec['title']} (score: {rec['score']})")
1130
- ... print(f" Reasons: {rec['reasons']}")
1131
- """
1132
- recommendations = self.dep_analytics.recommend_next_tasks(
1133
- agent_count=agent_count, lookahead=5
1134
- )
1135
-
1136
- return [
1137
- {
1138
- "id": rec.id,
1139
- "title": rec.title,
1140
- "priority": rec.priority,
1141
- "score": rec.score,
1142
- "reasons": rec.reasons,
1143
- "estimated_hours": rec.estimated_effort,
1144
- "unlocks_count": len(rec.unlocks),
1145
- "unlocks": rec.unlocks[:3],
1146
- }
1147
- for rec in recommendations.recommendations
1148
- ]
1149
-
1150
- def assess_risks(self) -> dict[str, Any]:
1151
- """
1152
- Assess dependency-related risks in the project.
1153
-
1154
- Note: Prefer using sdk.dep_analytics.assess_dependency_risk() directly.
1155
- This method exists for backward compatibility.
1156
-
1157
- Identifies single points of failure, circular dependencies,
1158
- and orphaned tasks.
1159
-
1160
- Returns:
1161
- Dict with risk assessment results
1162
-
1163
- Example:
1164
- >>> sdk = SDK(agent="claude")
1165
- >>> # Preferred approach
1166
- >>> risk = sdk.dep_analytics.assess_dependency_risk()
1167
- >>> # Or via SDK (backward compatibility)
1168
- >>> risks = sdk.assess_risks()
1169
- >>> if risks['high_risk_count'] > 0:
1170
- ... print(f"Warning: {risks['high_risk_count']} high-risk tasks")
1171
- """
1172
- risk = self.dep_analytics.assess_dependency_risk()
1173
-
1174
- return {
1175
- "high_risk_count": len(risk.high_risk),
1176
- "high_risk_tasks": [
1177
- {
1178
- "id": node.id,
1179
- "title": node.title,
1180
- "risk_score": node.risk_score,
1181
- "risk_factors": [f.description for f in node.risk_factors],
1182
- }
1183
- for node in risk.high_risk
1184
- ],
1185
- "circular_dependencies": risk.circular_dependencies,
1186
- "orphaned_count": len(risk.orphaned_nodes),
1187
- "orphaned_tasks": risk.orphaned_nodes[:5],
1188
- "recommendations": risk.recommendations,
1189
- }
1190
-
1191
- def analyze_impact(self, node_id: str) -> dict[str, Any]:
1192
- """
1193
- Analyze the impact of completing a specific task.
1194
-
1195
- Note: Prefer using sdk.dep_analytics.impact_analysis() directly.
1196
- This method exists for backward compatibility.
1197
-
1198
- Args:
1199
- node_id: Task to analyze
1200
-
1201
- Returns:
1202
- Dict with impact analysis
1203
-
1204
- Example:
1205
- >>> sdk = SDK(agent="claude")
1206
- >>> # Preferred approach
1207
- >>> impact = sdk.dep_analytics.impact_analysis("feature-001")
1208
- >>> # Or via SDK (backward compatibility)
1209
- >>> impact = sdk.analyze_impact("feature-001")
1210
- >>> print(f"Completing this unlocks {impact['unlocks_count']} tasks")
1211
- """
1212
- impact = self.dep_analytics.impact_analysis(node_id)
1213
-
1214
- return {
1215
- "node_id": node_id,
1216
- "direct_dependents": impact.direct_dependents,
1217
- "total_impact": impact.transitive_dependents,
1218
- "completion_impact": impact.completion_impact,
1219
- "unlocks_count": len(impact.affected_nodes),
1220
- "affected_tasks": impact.affected_nodes[:10],
1221
- }
1222
-
1223
- def get_work_queue(
1224
- self, agent_id: str | None = None, limit: int = 10, min_score: float = 0.0
1225
- ) -> list[dict[str, Any]]:
1226
- """
1227
- Get prioritized work queue showing recommended work, active work, and dependencies.
1228
-
1229
- This method provides a comprehensive view of:
1230
- 1. Recommended next work (using smart analytics)
1231
- 2. Active work by all agents
1232
- 3. Blocked items and what's blocking them
1233
- 4. Priority-based scoring
1234
-
1235
- Args:
1236
- agent_id: Agent to get queue for (defaults to SDK agent)
1237
- limit: Maximum number of items to return (default: 10)
1238
- min_score: Minimum score threshold (default: 0.0)
1239
-
1240
- Returns:
1241
- List of work queue items with scoring and metadata:
1242
- - task_id: Work item ID
1243
- - title: Work item title
1244
- - status: Current status
1245
- - priority: Priority level
1246
- - score: Routing score
1247
- - complexity: Complexity level (if set)
1248
- - effort: Estimated effort (if set)
1249
- - blocks_count: Number of tasks this blocks (if any)
1250
- - blocked_by: List of blocking task IDs (if blocked)
1251
- - agent_assigned: Current assignee (if any)
1252
- - type: Work item type (feature, bug, spike, etc.)
1253
-
1254
- Example:
1255
- >>> sdk = SDK(agent="claude")
1256
- >>> queue = sdk.get_work_queue(limit=5)
1257
- >>> for item in queue:
1258
- ... print(f"{item['score']:.1f} - {item['title']}")
1259
- ... if item.get('blocked_by'):
1260
- ... print(f" ⚠️ Blocked by: {', '.join(item['blocked_by'])}")
1261
- """
1262
- from htmlgraph.routing import AgentCapabilityRegistry, CapabilityMatcher
1263
-
1264
- agent = agent_id or self._agent_id or "cli"
1265
-
1266
- # Get all work item types
1267
- all_work = []
1268
- for collection_name in ["features", "bugs", "spikes", "chores", "epics"]:
1269
- collection = getattr(self, collection_name, None)
1270
- if collection:
1271
- # Get todo and blocked items
1272
- for item in collection.where(status="todo"):
1273
- all_work.append(item)
1274
- for item in collection.where(status="blocked"):
1275
- all_work.append(item)
1276
-
1277
- if not all_work:
1278
- return []
1279
-
1280
- # Get recommendations from analytics (uses strategic scoring)
1281
- recommendations = self.recommend_next_work(agent_count=limit * 2)
1282
- rec_scores = {rec["id"]: rec["score"] for rec in recommendations}
1283
-
1284
- # Build routing registry
1285
- registry = AgentCapabilityRegistry()
1286
-
1287
- # Register current agent
1288
- registry.register_agent(agent, capabilities=[], wip_limit=5)
1289
-
1290
- # Get current WIP count for agent
1291
- wip_count = len(self.features.where(status="in-progress", agent_assigned=agent))
1292
- registry.set_wip(agent, wip_count)
1293
-
1294
- # Score each work item
1295
- queue_items = []
1296
- for item in all_work:
1297
- # Use strategic score if available, otherwise use routing score
1298
- if item.id in rec_scores:
1299
- score = rec_scores[item.id]
1300
- else:
1301
- # Fallback to routing score
1302
- agent_profile = registry.get_agent(agent)
1303
- if agent_profile:
1304
- score = CapabilityMatcher.score_agent_task_fit(agent_profile, item)
1305
- else:
1306
- score = 0.0
1307
-
1308
- # Apply minimum score filter
1309
- if score < min_score:
1310
- continue
1311
-
1312
- # Build queue item
1313
- queue_item = {
1314
- "task_id": item.id,
1315
- "title": item.title,
1316
- "status": item.status,
1317
- "priority": item.priority,
1318
- "score": score,
1319
- "type": item.type,
1320
- "complexity": getattr(item, "complexity", None),
1321
- "effort": getattr(item, "estimated_effort", None),
1322
- "agent_assigned": getattr(item, "agent_assigned", None),
1323
- "blocks_count": 0,
1324
- "blocked_by": [],
1325
- }
1326
-
1327
- # Add dependency information
1328
- if hasattr(item, "edges"):
1329
- # Check if this item blocks others
1330
- blocks = item.edges.get("blocks", [])
1331
- queue_item["blocks_count"] = len(blocks)
1332
-
1333
- # Check if this item is blocked
1334
- blocked_by = item.edges.get("blocked_by", [])
1335
- queue_item["blocked_by"] = blocked_by
1336
-
1337
- queue_items.append(queue_item)
1338
-
1339
- # Sort by score (descending)
1340
- queue_items.sort(key=lambda x: x["score"], reverse=True)
1341
-
1342
- # Limit results
1343
- return queue_items[:limit]
1344
-
1345
- def work_next(
1346
- self,
1347
- agent_id: str | None = None,
1348
- auto_claim: bool = False,
1349
- min_score: float = 0.0,
1350
- ) -> Node | None:
1351
- """
1352
- Get the next best task for an agent using smart routing.
1353
-
1354
- Uses both strategic analytics and capability-based routing to find
1355
- the optimal next task.
1356
-
1357
- Args:
1358
- agent_id: Agent to get task for (defaults to SDK agent)
1359
- auto_claim: Automatically claim the task (default: False)
1360
- min_score: Minimum score threshold (default: 0.0)
1361
-
1362
- Returns:
1363
- Next best Node or None if no suitable task found
1364
-
1365
- Example:
1366
- >>> sdk = SDK(agent="claude")
1367
- >>> task = sdk.work_next(auto_claim=True)
1368
- >>> if task:
1369
- ... print(f"Working on: {task.title}")
1370
- ... # Task is automatically claimed and assigned
1371
- """
1372
- agent = agent_id or self._agent_id or "cli"
1373
-
1374
- # Get work queue - get more items since we filter for actionable (todo) only
1375
- queue = self.get_work_queue(agent_id=agent, limit=20, min_score=min_score)
1376
-
1377
- if not queue:
1378
- return None
1379
-
1380
- # Find the first actionable (todo) task - blocked tasks are not actionable
1381
- top_item = None
1382
- for item in queue:
1383
- if item["status"] == "todo":
1384
- top_item = item
1385
- break
1386
-
1387
- if top_item is None:
1388
- return None
1389
-
1390
- # Fetch the actual node
1391
- task = None
1392
- for collection_name in ["features", "bugs", "spikes", "chores", "epics"]:
1393
- collection = getattr(self, collection_name, None)
1394
- if collection:
1395
- try:
1396
- task = collection.get(top_item["task_id"])
1397
- if task:
1398
- break
1399
- except (ValueError, FileNotFoundError):
1400
- continue
1401
-
1402
- if not task:
1403
- return None
1404
-
1405
- # Auto-claim if requested
1406
- if auto_claim and task.status == "todo" and collection is not None:
1407
- # Claim the task
1408
- # collection.edit returns context manager or None
1409
- task_editor: Any = collection.edit(task.id)
1410
- if task_editor is not None:
1411
- # collection.edit returns context manager
1412
- with task_editor as t:
1413
- t.status = "in-progress"
1414
- t.agent_assigned = agent
1415
-
1416
- result: Node | None = task
1417
- return result
1418
-
1419
- # =========================================================================
1420
- # Planning Workflow Integration
1421
- # =========================================================================
1422
-
1423
- def start_planning_spike(
1424
- self,
1425
- title: str,
1426
- context: str = "",
1427
- timebox_hours: float = 4.0,
1428
- auto_start: bool = True,
1429
- ) -> Node:
1430
- """
1431
- Create a planning spike to research and design before implementation.
1432
-
1433
- This is for timeboxed investigation before creating a full track.
1434
-
1435
- Args:
1436
- title: Spike title (e.g., "Plan User Authentication System")
1437
- context: Background information
1438
- timebox_hours: Time limit for spike (default: 4 hours)
1439
- auto_start: Automatically start the spike (default: True)
1440
-
1441
- Returns:
1442
- Created spike Node
1443
-
1444
- Example:
1445
- >>> sdk = SDK(agent="claude")
1446
- >>> spike = sdk.start_planning_spike(
1447
- ... "Plan Real-time Notifications",
1448
- ... context="Users need live updates. Research options.",
1449
- ... timebox_hours=3.0
1450
- ... )
1451
- """
1452
- from htmlgraph.ids import generate_id
1453
- from htmlgraph.models import Spike, SpikeType
1454
-
1455
- # Create spike directly (SpikeBuilder doesn't exist yet)
1456
- spike_id = generate_id(node_type="spike", title=title)
1457
- spike = Spike(
1458
- id=spike_id,
1459
- title=title,
1460
- type="spike",
1461
- status="in-progress" if auto_start and self._agent_id else "todo",
1462
- spike_type=SpikeType.ARCHITECTURAL,
1463
- timebox_hours=int(timebox_hours),
1464
- agent_assigned=self._agent_id if auto_start and self._agent_id else None,
1465
- steps=[
1466
- Step(description="Research existing solutions and patterns"),
1467
- Step(description="Define requirements and constraints"),
1468
- Step(description="Design high-level architecture"),
1469
- Step(description="Identify dependencies and risks"),
1470
- Step(description="Create implementation plan"),
1471
- ],
1472
- content=f"<p>{context}</p>" if context else "",
1473
- edges={},
1474
- properties={},
1475
- )
1476
-
1477
- self._graph.add(spike)
1478
- return spike
1479
-
1480
- def create_track_from_plan(
1481
- self,
1482
- title: str,
1483
- description: str,
1484
- spike_id: str | None = None,
1485
- priority: str = "high",
1486
- requirements: list[str | tuple[str, str]] | None = None,
1487
- phases: list[tuple[str, list[str]]] | None = None,
1488
- ) -> dict[str, Any]:
1489
- """
1490
- Create a track with spec and plan from planning results.
1491
-
1492
- Args:
1493
- title: Track title
1494
- description: Track description
1495
- spike_id: Optional spike ID that led to this track
1496
- priority: Track priority (default: "high")
1497
- requirements: List of requirements (strings or (req, priority) tuples)
1498
- phases: List of (phase_name, tasks) tuples for the plan
1499
-
1500
- Returns:
1501
- Dict with track, spec, and plan details
1502
-
1503
- Example:
1504
- >>> sdk = SDK(agent="claude")
1505
- >>> track_info = sdk.create_track_from_plan(
1506
- ... title="User Authentication System",
1507
- ... description="OAuth 2.0 with JWT tokens",
1508
- ... requirements=[
1509
- ... ("OAuth 2.0 integration", "must-have"),
1510
- ... ("JWT token management", "must-have"),
1511
- ... "Password reset flow"
1512
- ... ],
1513
- ... phases=[
1514
- ... ("Phase 1: OAuth", ["Setup providers (2h)", "Callback (2h)"]),
1515
- ... ("Phase 2: JWT", ["Token signing (2h)", "Refresh (1.5h)"])
1516
- ... ]
1517
- ... )
1518
- """
1519
-
1520
- builder = (
1521
- self.tracks.builder()
1522
- .title(title)
1523
- .description(description)
1524
- .priority(priority)
1525
- )
1526
-
1527
- # Add reference to planning spike if provided
1528
- if spike_id:
1529
- # Access internal data for track builder
1530
- data: dict[str, Any] = builder._data # type: ignore[attr-defined]
1531
- data["properties"]["planning_spike"] = spike_id
1532
-
1533
- # Add spec if requirements provided
1534
- if requirements:
1535
- # Convert simple strings to (requirement, "must-have") tuples
1536
- req_list = []
1537
- for req in requirements:
1538
- if isinstance(req, str):
1539
- req_list.append((req, "must-have"))
1540
- else:
1541
- req_list.append(req)
1542
-
1543
- builder.with_spec(
1544
- overview=description,
1545
- context=f"Track created from planning spike: {spike_id}"
1546
- if spike_id
1547
- else "",
1548
- requirements=req_list,
1549
- acceptance_criteria=[],
1550
- )
1551
-
1552
- # Add plan if phases provided
1553
- if phases:
1554
- builder.with_plan_phases(phases)
1555
-
1556
- track = builder.create()
1557
-
1558
- return {
1559
- "track_id": track.id,
1560
- "title": track.title,
1561
- "has_spec": bool(requirements),
1562
- "has_plan": bool(phases),
1563
- "spike_id": spike_id,
1564
- "priority": priority,
1565
- }
1566
-
1567
- def smart_plan(
1568
- self,
1569
- description: str,
1570
- create_spike: bool = True,
1571
- timebox_hours: float = 4.0,
1572
- research_completed: bool = False,
1573
- research_findings: dict[str, Any] | None = None,
1574
- ) -> dict[str, Any]:
1575
- """
1576
- Smart planning workflow: analyzes project context and creates spike or track.
1577
-
1578
- This is the main entry point for planning new work. It:
1579
- 1. Checks current project state
1580
- 2. Provides context from strategic analytics
1581
- 3. Creates a planning spike or track as appropriate
1582
-
1583
- **IMPORTANT: Research Phase Required**
1584
- For complex features, you should complete research BEFORE planning:
1585
- 1. Use /htmlgraph:research or WebSearch to gather best practices
1586
- 2. Document findings (libraries, patterns, anti-patterns)
1587
- 3. Pass research_completed=True and research_findings to this method
1588
- 4. This ensures planning is informed by industry best practices
1589
-
1590
- Research-first workflow:
1591
- 1. /htmlgraph:research "{topic}" → Gather external knowledge
1592
- 2. sdk.smart_plan(..., research_completed=True) → Plan with context
1593
- 3. Complete spike steps → Design solution
1594
- 4. Create track from plan → Structure implementation
1595
-
1596
- Args:
1597
- description: What you want to plan (e.g., "User authentication system")
1598
- create_spike: Create a spike for research (default: True)
1599
- timebox_hours: If creating spike, time limit (default: 4 hours)
1600
- research_completed: Whether research was performed (default: False)
1601
- research_findings: Structured research findings (optional)
1602
-
1603
- Returns:
1604
- Dict with planning context and created spike/track info
1605
-
1606
- Example:
1607
- >>> sdk = SDK(agent="claude")
1608
- >>> # WITH research (recommended for complex work)
1609
- >>> research = {
1610
- ... "topic": "OAuth 2.0 best practices",
1611
- ... "sources_count": 5,
1612
- ... "recommended_library": "authlib",
1613
- ... "key_insights": ["Use PKCE", "Implement token rotation"]
1614
- ... }
1615
- >>> plan = sdk.smart_plan(
1616
- ... "User authentication system",
1617
- ... create_spike=True,
1618
- ... research_completed=True,
1619
- ... research_findings=research
1620
- ... )
1621
- >>> print(f"Created: {plan['spike_id']}")
1622
- >>> print(f"Research informed: {plan['research_informed']}")
1623
- """
1624
- # Get project context from strategic analytics
1625
- bottlenecks = self.find_bottlenecks(top_n=3)
1626
- risks = self.assess_risks()
1627
- parallel = self.get_parallel_work(max_agents=5)
1628
-
1629
- context = {
1630
- "bottlenecks_count": len(bottlenecks),
1631
- "high_risk_count": risks["high_risk_count"],
1632
- "parallel_capacity": parallel["max_parallelism"],
1633
- "description": description,
1634
- }
1635
-
1636
- # Build context string with research info
1637
- context_str = f"Project context:\n- {len(bottlenecks)} bottlenecks\n- {risks['high_risk_count']} high-risk items\n- {parallel['max_parallelism']} parallel capacity"
1638
-
1639
- if research_completed and research_findings:
1640
- context_str += f"\n\nResearch completed:\n- Topic: {research_findings.get('topic', description)}"
1641
- if "sources_count" in research_findings:
1642
- context_str += f"\n- Sources: {research_findings['sources_count']}"
1643
- if "recommended_library" in research_findings:
1644
- context_str += (
1645
- f"\n- Recommended: {research_findings['recommended_library']}"
1646
- )
1647
-
1648
- # Validation: warn if complex work planned without research
1649
- is_complex = any(
1650
- [
1651
- "auth" in description.lower(),
1652
- "security" in description.lower(),
1653
- "real-time" in description.lower(),
1654
- "websocket" in description.lower(),
1655
- "oauth" in description.lower(),
1656
- "performance" in description.lower(),
1657
- "integration" in description.lower(),
1658
- ]
1659
- )
1660
-
1661
- warnings = []
1662
- if is_complex and not research_completed:
1663
- warnings.append(
1664
- "⚠️ Complex feature detected without research. "
1665
- "Consider using /htmlgraph:research first to gather best practices."
1666
- )
1667
-
1668
- if create_spike:
1669
- spike = self.start_planning_spike(
1670
- title=f"Plan: {description}",
1671
- context=context_str,
1672
- timebox_hours=timebox_hours,
1673
- )
1674
-
1675
- # Store research metadata in spike properties if provided
1676
- if research_completed and research_findings:
1677
- spike.properties["research_completed"] = True
1678
- spike.properties["research_findings"] = research_findings
1679
- self._graph.update(spike)
1680
-
1681
- result = {
1682
- "type": "spike",
1683
- "spike_id": spike.id,
1684
- "title": spike.title,
1685
- "status": spike.status,
1686
- "timebox_hours": timebox_hours,
1687
- "project_context": context,
1688
- "research_informed": research_completed,
1689
- "next_steps": [
1690
- "Research and design the solution"
1691
- if not research_completed
1692
- else "Design solution using research findings",
1693
- "Complete spike steps",
1694
- "Use SDK.create_track_from_plan() to create track",
1695
- ],
1696
- }
1697
-
1698
- if warnings:
1699
- result["warnings"] = warnings
1700
-
1701
- return result
1702
- else:
1703
- # Direct track creation (for when you already know what to do)
1704
- track_info = self.create_track_from_plan(
1705
- title=description, description=f"Planned with context: {context}"
1706
- )
1707
-
1708
- result = {
1709
- "type": "track",
1710
- **track_info,
1711
- "project_context": context,
1712
- "research_informed": research_completed,
1713
- "next_steps": [
1714
- "Create features from track plan",
1715
- "Link features to track",
1716
- "Start implementation",
1717
- ],
1718
- }
1719
-
1720
- if warnings:
1721
- result["warnings"] = warnings
1722
-
1723
- return result
1724
-
1725
- def plan_parallel_work(
1726
- self,
1727
- max_agents: int = 5,
1728
- shared_files: list[str] | None = None,
1729
- ) -> dict[str, Any]:
1730
- """
1731
- Plan and prepare parallel work execution.
1732
-
1733
- This integrates with smart_plan to enable parallel agent dispatch.
1734
- Uses the 6-phase ParallelWorkflow:
1735
- 1. Pre-flight analysis (dependencies, risks)
1736
- 2. Context preparation (shared file caching)
1737
- 3. Prompt generation (for Task tool)
1738
-
1739
- Args:
1740
- max_agents: Maximum parallel agents (default: 5)
1741
- shared_files: Files to pre-cache for all agents
1742
-
1743
- Returns:
1744
- Dict with parallel execution plan:
1745
- - can_parallelize: Whether parallelization is recommended
1746
- - analysis: Pre-flight analysis results
1747
- - prompts: Ready-to-use Task tool prompts
1748
- - recommendations: Optimization suggestions
1749
-
1750
- Example:
1751
- >>> sdk = SDK(agent="orchestrator")
1752
- >>> plan = sdk.plan_parallel_work(max_agents=3)
1753
- >>> if plan["can_parallelize"]:
1754
- ... # Use prompts with Task tool
1755
- ... for p in plan["prompts"]:
1756
- ... Task(prompt=p["prompt"], description=p["description"])
1757
- """
1758
- from htmlgraph.parallel import ParallelWorkflow
1759
-
1760
- workflow = ParallelWorkflow(self)
1761
-
1762
- # Phase 1: Pre-flight analysis
1763
- analysis = workflow.analyze(max_agents=max_agents)
1764
-
1765
- result = {
1766
- "can_parallelize": analysis.can_parallelize,
1767
- "max_parallelism": analysis.max_parallelism,
1768
- "ready_tasks": analysis.ready_tasks,
1769
- "blocked_tasks": analysis.blocked_tasks,
1770
- "speedup_factor": analysis.speedup_factor,
1771
- "recommendation": analysis.recommendation,
1772
- "warnings": analysis.warnings,
1773
- "prompts": [],
1774
- }
1775
-
1776
- if not analysis.can_parallelize:
1777
- result["reason"] = analysis.recommendation
1778
- return result
1779
-
1780
- # Phase 2 & 3: Prepare tasks and generate prompts
1781
- tasks = workflow.prepare_tasks(
1782
- analysis.ready_tasks[:max_agents],
1783
- shared_files=shared_files,
1784
- )
1785
- prompts = workflow.generate_prompts(tasks)
1786
-
1787
- result["prompts"] = prompts
1788
- result["task_count"] = len(prompts)
1789
-
1790
- # Add efficiency guidelines
1791
- result["guidelines"] = {
1792
- "dispatch": "Send ALL Task calls in a SINGLE message for true parallelism",
1793
- "patterns": [
1794
- "Grep → Read (search before reading)",
1795
- "Read → Edit → Bash (read, modify, test)",
1796
- "Glob → Read (find files first)",
1797
- ],
1798
- "avoid": [
1799
- "Sequential Task calls (loses parallelism)",
1800
- "Read → Read → Read (cache instead)",
1801
- "Edit → Edit → Edit (batch edits)",
1802
- ],
1803
- }
1804
-
1805
- return result
1806
-
1807
- def aggregate_parallel_results(
1808
- self,
1809
- agent_ids: list[str],
1810
- ) -> dict[str, Any]:
1811
- """
1812
- Aggregate results from parallel agent execution.
1813
-
1814
- Call this after parallel agents complete to:
1815
- - Collect health metrics
1816
- - Detect anti-patterns
1817
- - Identify conflicts
1818
- - Generate recommendations
1819
-
1820
- Args:
1821
- agent_ids: List of agent/transcript IDs to analyze
1822
-
1823
- Returns:
1824
- Dict with aggregated results and validation
1825
-
1826
- Example:
1827
- >>> # After parallel work completes
1828
- >>> results = sdk.aggregate_parallel_results([
1829
- ... "agent-abc123",
1830
- ... "agent-def456",
1831
- ... "agent-ghi789",
1832
- ... ])
1833
- >>> print(f"Health: {results['avg_health_score']:.0%}")
1834
- >>> print(f"Conflicts: {results['conflicts']}")
1835
- """
1836
- from htmlgraph.parallel import ParallelWorkflow
1837
-
1838
- workflow = ParallelWorkflow(self)
1839
-
1840
- # Phase 5: Aggregate
1841
- aggregate = workflow.aggregate(agent_ids)
1842
-
1843
- # Phase 6: Validate
1844
- validation = workflow.validate(aggregate)
1845
-
1846
- return {
1847
- "total_agents": aggregate.total_agents,
1848
- "successful": aggregate.successful,
1849
- "failed": aggregate.failed,
1850
- "total_duration_seconds": aggregate.total_duration_seconds,
1851
- "parallel_speedup": aggregate.parallel_speedup,
1852
- "avg_health_score": aggregate.avg_health_score,
1853
- "total_anti_patterns": aggregate.total_anti_patterns,
1854
- "files_modified": aggregate.files_modified,
1855
- "conflicts": aggregate.conflicts,
1856
- "recommendations": aggregate.recommendations,
1857
- "validation": validation,
1858
- "all_passed": all(validation.values()),
1859
- }
1860
-
1861
- # =========================================================================
1862
- # Subagent Orchestration
1863
- # =========================================================================
1864
-
1865
- @property
1866
- def orchestrator(self) -> Any:
1867
- """
1868
- Get the subagent orchestrator for spawning explorer/coder agents.
1869
-
1870
- Lazy-loaded on first access.
1871
-
1872
- Returns:
1873
- SubagentOrchestrator instance
1874
-
1875
- Example:
1876
- >>> sdk = SDK(agent="claude")
1877
- >>> explorer = sdk.orchestrator.spawn_explorer(
1878
- ... task="Find all API endpoints",
1879
- ... scope="src/"
1880
- ... )
1881
- """
1882
- if self._orchestrator is None:
1883
- from htmlgraph.orchestrator import SubagentOrchestrator
1884
-
1885
- self._orchestrator = SubagentOrchestrator(self) # type: ignore[assignment]
1886
- return self._orchestrator
1887
-
1888
- def spawn_explorer(
1889
- self,
1890
- task: str,
1891
- scope: str | None = None,
1892
- patterns: list[str] | None = None,
1893
- questions: list[str] | None = None,
1894
- ) -> dict[str, Any]:
1895
- """
1896
- Spawn an explorer subagent for codebase discovery.
1897
-
1898
- Explorer agents are optimized for finding files, searching patterns,
1899
- and mapping code without modifying anything.
1900
-
1901
- Args:
1902
- task: What to explore/discover
1903
- scope: Directory scope (e.g., "src/")
1904
- patterns: Glob patterns to focus on
1905
- questions: Specific questions to answer
1906
-
1907
- Returns:
1908
- Dict with prompt ready for Task tool
1909
-
1910
- Note:
1911
- Returns dict with 'prompt', 'description', 'subagent_type' keys.
1912
- Returns empty dict if spawning fails.
1913
-
1914
- Example:
1915
- >>> prompt = sdk.spawn_explorer(
1916
- ... task="Find all database models",
1917
- ... scope="src/models/",
1918
- ... questions=["What ORM is used?"]
1919
- ... )
1920
- >>> # Execute with Task tool
1921
- >>> Task(prompt=prompt["prompt"], description=prompt["description"])
1922
-
1923
- See also:
1924
- spawn_coder: Spawn implementation agent with feature context
1925
- orchestrate: Full exploration + implementation workflow
1926
- """
1927
- subagent_prompt = self.orchestrator.spawn_explorer(
1928
- task=task,
1929
- scope=scope,
1930
- patterns=patterns,
1931
- questions=questions,
1932
- )
1933
- result: dict[str, Any] = subagent_prompt.to_task_kwargs()
1934
- return result
1935
-
1936
- def spawn_coder(
1937
- self,
1938
- feature_id: str,
1939
- context: str | None = None,
1940
- files_to_modify: list[str] | None = None,
1941
- test_command: str | None = None,
1942
- ) -> dict[str, Any]:
1943
- """
1944
- Spawn a coder subagent for implementing changes.
1945
-
1946
- Coder agents are optimized for reading, modifying, and testing code.
1947
-
1948
- Args:
1949
- feature_id: Feature being implemented
1950
- context: Results from explorer (string summary)
1951
- files_to_modify: Specific files to change
1952
- test_command: Command to verify changes
1953
-
1954
- Returns:
1955
- Dict with prompt ready for Task tool
1956
-
1957
- Note:
1958
- Returns dict with 'prompt', 'description', 'subagent_type' keys.
1959
- Requires valid feature_id. Returns empty dict if feature not found.
1960
-
1961
- Example:
1962
- >>> prompt = sdk.spawn_coder(
1963
- ... feature_id="feat-add-auth",
1964
- ... context=explorer_results,
1965
- ... test_command="uv run pytest tests/auth/"
1966
- ... )
1967
- >>> Task(prompt=prompt["prompt"], description=prompt["description"])
1968
-
1969
- See also:
1970
- spawn_explorer: Explore codebase before implementation
1971
- orchestrate: Full exploration + implementation workflow
1972
- """
1973
- subagent_prompt = self.orchestrator.spawn_coder(
1974
- feature_id=feature_id,
1975
- context=context,
1976
- files_to_modify=files_to_modify,
1977
- test_command=test_command,
1978
- )
1979
- result: dict[str, Any] = subagent_prompt.to_task_kwargs()
1980
- return result
1981
-
1982
- def orchestrate(
1983
- self,
1984
- feature_id: str,
1985
- exploration_scope: str | None = None,
1986
- test_command: str | None = None,
1987
- ) -> dict[str, Any]:
1988
- """
1989
- Orchestrate full feature implementation with explorer and coder.
1990
-
1991
- Generates prompts for a two-phase workflow:
1992
- 1. Explorer discovers relevant code and patterns
1993
- 2. Coder implements the feature based on explorer findings
1994
-
1995
- Args:
1996
- feature_id: Feature to implement
1997
- exploration_scope: Directory to explore
1998
- test_command: Test command for verification
1999
-
2000
- Returns:
2001
- Dict with explorer and coder prompts
2002
-
2003
- Example:
2004
- >>> prompts = sdk.orchestrate(
2005
- ... "feat-add-caching",
2006
- ... exploration_scope="src/cache/",
2007
- ... test_command="uv run pytest tests/cache/"
2008
- ... )
2009
- >>> # Phase 1: Run explorer
2010
- >>> Task(prompt=prompts["explorer"]["prompt"], ...)
2011
- >>> # Phase 2: Run coder with explorer results
2012
- >>> Task(prompt=prompts["coder"]["prompt"], ...)
2013
-
2014
- See also:
2015
- spawn_explorer: Just the exploration phase
2016
- spawn_coder: Just the implementation phase
2017
- """
2018
- prompts = self.orchestrator.orchestrate_feature(
2019
- feature_id=feature_id,
2020
- exploration_scope=exploration_scope,
2021
- test_command=test_command,
2022
- )
2023
- return {
2024
- "explorer": prompts["explorer"].to_task_kwargs(),
2025
- "coder": prompts["coder"].to_task_kwargs(),
2026
- "workflow": [
2027
- "1. Execute explorer Task and collect results",
2028
- "2. Parse explorer results for files and patterns",
2029
- "3. Execute coder Task with explorer context",
2030
- "4. Verify coder results and update feature status",
2031
- ],
2032
- }
2033
-
2034
- # =========================================================================
2035
- # Session Management Optimization
2036
- # =========================================================================
2037
-
2038
- def get_session_start_info(
2039
- self,
2040
- include_git_log: bool = True,
2041
- git_log_count: int = 5,
2042
- analytics_top_n: int = 3,
2043
- analytics_max_agents: int = 3,
2044
- ) -> SessionStartInfo:
2045
- """
2046
- Get comprehensive session start information in a single call.
2047
-
2048
- Consolidates all information needed for session start into one method,
2049
- reducing context usage from 6+ tool calls to 1.
2050
-
2051
- Args:
2052
- include_git_log: Include recent git commits (default: True)
2053
- git_log_count: Number of recent commits to include (default: 5)
2054
- analytics_top_n: Number of bottlenecks/recommendations (default: 3)
2055
- analytics_max_agents: Max agents for parallel work analysis (default: 3)
2056
-
2057
- Returns:
2058
- Dict with comprehensive session start context:
2059
- - status: Project status (nodes, collections, WIP)
2060
- - active_work: Current active work item (if any)
2061
- - features: List of features with status
2062
- - sessions: Recent sessions
2063
- - git_log: Recent commits (if include_git_log=True)
2064
- - analytics: Strategic insights (bottlenecks, recommendations, parallel)
2065
-
2066
- Note:
2067
- Returns empty dict {} if session context unavailable.
2068
- Always check for expected keys before accessing.
2069
-
2070
- Example:
2071
- >>> sdk = SDK(agent="claude")
2072
- >>> info = sdk.get_session_start_info()
2073
- >>> print(f"Project: {info['status']['total_nodes']} nodes")
2074
- >>> print(f"WIP: {info['status']['in_progress_count']}")
2075
- >>> if info.get('active_work'):
2076
- ... print(f"Active: {info['active_work']['title']}")
2077
- >>> for bn in info['analytics']['bottlenecks']:
2078
- ... print(f"Bottleneck: {bn['title']}")
2079
- """
2080
- import subprocess
2081
-
2082
- result = {}
2083
-
2084
- # 1. Project status
2085
- result["status"] = self.get_status()
2086
-
2087
- # 2. Active work item (validation status) - always include, even if None
2088
- result["active_work"] = self.get_active_work_item() # type: ignore[assignment]
2089
-
2090
- # 3. Features list (simplified)
2091
- features_list: list[dict[str, object]] = []
2092
- for feature in self.features.all():
2093
- features_list.append(
2094
- {
2095
- "id": feature.id,
2096
- "title": feature.title,
2097
- "status": feature.status,
2098
- "priority": feature.priority,
2099
- "steps_total": len(feature.steps),
2100
- "steps_completed": sum(1 for s in feature.steps if s.completed),
2101
- }
2102
- )
2103
- result["features"] = features_list # type: ignore[assignment]
2104
-
2105
- # 4. Sessions list (recent 20)
2106
- sessions_list: list[dict[str, Any]] = []
2107
- for session in self.sessions.all()[:20]:
2108
- sessions_list.append(
2109
- {
2110
- "id": session.id,
2111
- "status": session.status,
2112
- "agent": session.properties.get("agent", "unknown"),
2113
- "event_count": session.properties.get("event_count", 0),
2114
- "started": session.created.isoformat()
2115
- if hasattr(session, "created")
2116
- else None,
2117
- }
2118
- )
2119
- result["sessions"] = sessions_list # type: ignore[assignment]
2120
-
2121
- # 5. Git log (if requested)
2122
- if include_git_log:
2123
- try:
2124
- git_result = subprocess.run(
2125
- ["git", "log", "--oneline", f"-{git_log_count}"],
2126
- capture_output=True,
2127
- text=True,
2128
- check=True,
2129
- cwd=self._directory.parent,
2130
- )
2131
- git_lines: list[str] = git_result.stdout.strip().split("\n")
2132
- result["git_log"] = git_lines # type: ignore[assignment]
2133
- except (subprocess.CalledProcessError, FileNotFoundError):
2134
- empty_list: list[str] = []
2135
- result["git_log"] = empty_list # type: ignore[assignment]
2136
-
2137
- # 6. Strategic analytics
2138
- result["analytics"] = {
2139
- "bottlenecks": self.find_bottlenecks(top_n=analytics_top_n),
2140
- "recommendations": self.recommend_next_work(agent_count=analytics_top_n),
2141
- "parallel": self.get_parallel_work(max_agents=analytics_max_agents),
2142
- }
2143
-
2144
- return result # type: ignore[return-value]
2145
-
2146
- def get_active_work_item(
2147
- self,
2148
- agent: str | None = None,
2149
- filter_by_agent: bool = False,
2150
- work_types: list[str] | None = None,
2151
- ) -> ActiveWorkItem | None:
2152
- """
2153
- Get the currently active work item (in-progress status).
2154
-
2155
- This is used by the PreToolUse validation hook to check if code changes
2156
- have an active work item for attribution.
2157
-
2158
- Args:
2159
- agent: Agent ID for filtering (optional)
2160
- filter_by_agent: If True, filter by agent. If False (default), return any active work item
2161
- work_types: Work item types to check (defaults to all: features, bugs, spikes, chores, epics)
2162
-
2163
- Returns:
2164
- Dict with work item details or None if no active work item found:
2165
- - id: Work item ID
2166
- - title: Work item title
2167
- - type: Work item type (feature, bug, spike, chore, epic)
2168
- - status: Should be "in-progress"
2169
- - agent: Assigned agent
2170
- - steps_total: Total steps
2171
- - steps_completed: Completed steps
2172
- - auto_generated: (spikes only) True if auto-generated spike
2173
- - spike_subtype: (spikes only) "session-init" or "transition"
2174
-
2175
- Example:
2176
- >>> sdk = SDK(agent="claude")
2177
- >>> # Get any active work item
2178
- >>> active = sdk.get_active_work_item()
2179
- >>> if active:
2180
- ... print(f"Working on: {active['title']}")
2181
- ...
2182
- >>> # Get only this agent's active work item
2183
- >>> active = sdk.get_active_work_item(filter_by_agent=True)
2184
- """
2185
- # Default to all work item types
2186
- if work_types is None:
2187
- work_types = ["features", "bugs", "spikes", "chores", "epics"]
2188
-
2189
- # Search across all work item types
2190
- # Separate real work items from auto-generated spikes
2191
- real_work_items = []
2192
- auto_spikes = []
2193
-
2194
- for work_type in work_types:
2195
- collection = getattr(self, work_type, None)
2196
- if collection is None:
2197
- continue
2198
-
2199
- # Query for in-progress items
2200
- in_progress = collection.where(status="in-progress")
2201
-
2202
- for item in in_progress:
2203
- # Filter by agent if requested
2204
- if filter_by_agent:
2205
- agent_id = agent or self._agent_id
2206
- if agent_id and hasattr(item, "agent_assigned"):
2207
- if item.agent_assigned != agent_id:
2208
- continue
2209
-
2210
- item_dict = {
2211
- "id": item.id,
2212
- "title": item.title,
2213
- "type": item.type,
2214
- "status": item.status,
2215
- "agent": getattr(item, "agent_assigned", None),
2216
- "steps_total": len(item.steps) if hasattr(item, "steps") else 0,
2217
- "steps_completed": sum(1 for s in item.steps if s.completed)
2218
- if hasattr(item, "steps")
2219
- else 0,
2220
- }
2221
-
2222
- # Add spike-specific fields for auto-spike detection
2223
- if item.type == "spike":
2224
- item_dict["auto_generated"] = getattr(item, "auto_generated", False)
2225
- item_dict["spike_subtype"] = getattr(item, "spike_subtype", None)
2226
-
2227
- # Separate auto-spikes from real work
2228
- # Auto-spikes are temporary tracking items (session-init, transition, conversation-init)
2229
- is_auto_spike = item_dict["auto_generated"] and item_dict[
2230
- "spike_subtype"
2231
- ] in ("session-init", "transition", "conversation-init")
2232
-
2233
- if is_auto_spike:
2234
- auto_spikes.append(item_dict)
2235
- else:
2236
- # Real user-created spike
2237
- real_work_items.append(item_dict)
2238
- else:
2239
- # Features, bugs, chores, epics are always real work
2240
- real_work_items.append(item_dict)
2241
-
2242
- # Prioritize real work items over auto-spikes
2243
- # Auto-spikes should only show if there's NO other active work item
2244
- if real_work_items:
2245
- return real_work_items[0] # type: ignore[return-value]
2246
-
2247
- if auto_spikes:
2248
- return auto_spikes[0] # type: ignore[return-value]
2249
-
2250
- return None
2251
-
2252
- # =========================================================================
2253
- # Operations Layer - Server, Hooks, Events, Analytics
2254
- # =========================================================================
2255
-
2256
- def start_server(
2257
- self,
2258
- port: int = 8080,
2259
- host: str = "localhost",
2260
- watch: bool = True,
2261
- auto_port: bool = False,
2262
- ) -> Any:
2263
- """
2264
- Start HtmlGraph server for browsing graph via web UI.
2265
-
2266
- Args:
2267
- port: Port to listen on (default: 8080)
2268
- host: Host to bind to (default: "localhost")
2269
- watch: Enable file watching for auto-reload (default: True)
2270
- auto_port: Automatically find available port if specified port is in use (default: False)
2271
-
2272
- Returns:
2273
- ServerStartResult with handle, warnings, and config used
2274
-
2275
- Raises:
2276
- PortInUseError: If port is in use and auto_port=False
2277
- ServerStartError: If server fails to start
2278
-
2279
- Example:
2280
- >>> sdk = SDK(agent="claude")
2281
- >>> result = sdk.start_server(port=8080, watch=True)
2282
- >>> print(f"Server running at {result.handle.url}")
2283
- >>> # Open browser to http://localhost:8080
2284
- >>>
2285
- >>> # Stop server when done
2286
- >>> sdk.stop_server(result.handle)
2287
-
2288
- See also:
2289
- stop_server: Stop running server
2290
- get_server_status: Check if server is running
2291
- """
2292
- from htmlgraph.operations import server
2293
-
2294
- return server.start_server(
2295
- port=port,
2296
- graph_dir=self._directory,
2297
- static_dir=self._directory.parent, # Project root for index.html
2298
- host=host,
2299
- watch=watch,
2300
- auto_port=auto_port,
2301
- )
2302
-
2303
- def stop_server(self, handle: Any) -> None:
2304
- """
2305
- Stop a running HtmlGraph server.
2306
-
2307
- Args:
2308
- handle: ServerHandle returned from start_server()
2309
-
2310
- Raises:
2311
- ServerStartError: If shutdown fails
2312
-
2313
- Example:
2314
- >>> sdk = SDK(agent="claude")
2315
- >>> result = sdk.start_server()
2316
- >>> # Work with server...
2317
- >>> sdk.stop_server(result.handle)
2318
- """
2319
- from htmlgraph.operations import server
2320
-
2321
- server.stop_server(handle)
2322
-
2323
- def get_server_status(self, handle: Any | None = None) -> Any:
2324
- """
2325
- Check server status.
2326
-
2327
- Args:
2328
- handle: Optional ServerHandle to check
2329
-
2330
- Returns:
2331
- ServerStatus indicating whether server is running
2332
-
2333
- Example:
2334
- >>> sdk = SDK(agent="claude")
2335
- >>> result = sdk.start_server()
2336
- >>> status = sdk.get_server_status(result.handle)
2337
- >>> print(f"Running: {status.running}")
2338
- """
2339
- from htmlgraph.operations import server
2340
-
2341
- return server.get_server_status(handle)
2342
-
2343
- def install_hooks(self, use_copy: bool = False) -> Any:
2344
- """
2345
- Install Git hooks for automatic tracking.
2346
-
2347
- Installs hooks that automatically track sessions, activities, and features
2348
- as you work.
2349
-
2350
- Args:
2351
- use_copy: Force copy instead of symlink (default: False)
2352
-
2353
- Returns:
2354
- HookInstallResult with installation details
2355
-
2356
- Raises:
2357
- HookInstallError: If installation fails
2358
- HookConfigError: If configuration is invalid
2359
-
2360
- Example:
2361
- >>> sdk = SDK(agent="claude")
2362
- >>> result = sdk.install_hooks()
2363
- >>> print(f"Installed: {result.installed}")
2364
- >>> print(f"Skipped: {result.skipped}")
2365
- >>> if result.warnings:
2366
- ... print(f"Warnings: {result.warnings}")
2367
-
2368
- See also:
2369
- list_hooks: List installed hooks
2370
- validate_hook_config: Validate hook configuration
2371
- """
2372
- from htmlgraph.operations import hooks
2373
-
2374
- return hooks.install_hooks(
2375
- project_dir=self._directory.parent,
2376
- use_copy=use_copy,
2377
- )
2378
-
2379
- def list_hooks(self) -> Any:
2380
- """
2381
- List Git hooks status (enabled/disabled/missing).
2382
-
2383
- Returns:
2384
- HookListResult with enabled, disabled, and missing hooks
2385
-
2386
- Example:
2387
- >>> sdk = SDK(agent="claude")
2388
- >>> result = sdk.list_hooks()
2389
- >>> print(f"Enabled: {result.enabled}")
2390
- >>> print(f"Disabled: {result.disabled}")
2391
- >>> print(f"Missing: {result.missing}")
2392
- """
2393
- from htmlgraph.operations import hooks
2394
-
2395
- return hooks.list_hooks(project_dir=self._directory.parent)
2396
-
2397
- def validate_hook_config(self) -> Any:
2398
- """
2399
- Validate hook configuration.
2400
-
2401
- Returns:
2402
- HookValidationResult with validation status
2403
-
2404
- Example:
2405
- >>> sdk = SDK(agent="claude")
2406
- >>> result = sdk.validate_hook_config()
2407
- >>> if not result.valid:
2408
- ... print(f"Errors: {result.errors}")
2409
- >>> if result.warnings:
2410
- ... print(f"Warnings: {result.warnings}")
2411
- """
2412
- from htmlgraph.operations import hooks
2413
-
2414
- return hooks.validate_hook_config(project_dir=self._directory.parent)
2415
-
2416
- def export_sessions(self, overwrite: bool = False) -> Any:
2417
- """
2418
- Export legacy session HTML logs to JSONL events.
2419
-
2420
- Converts HTML session files to JSONL format for efficient querying.
2421
-
2422
- Args:
2423
- overwrite: Whether to overwrite existing JSONL files (default: False)
2424
-
2425
- Returns:
2426
- EventExportResult with counts of written, skipped, failed files
2427
-
2428
- Raises:
2429
- EventOperationError: If export fails
2430
-
2431
- Example:
2432
- >>> sdk = SDK(agent="claude")
2433
- >>> result = sdk.export_sessions()
2434
- >>> print(f"Exported {result.written} sessions")
2435
- >>> print(f"Skipped {result.skipped} (already exist)")
2436
- >>> if result.failed > 0:
2437
- ... print(f"Failed {result.failed} sessions")
2438
-
2439
- See also:
2440
- rebuild_event_index: Rebuild SQLite index from JSONL
2441
- query_events: Query exported events
2442
- """
2443
- from htmlgraph.operations import events
2444
-
2445
- return events.export_sessions(
2446
- graph_dir=self._directory,
2447
- overwrite=overwrite,
2448
- )
2449
-
2450
- def rebuild_event_index(self) -> Any:
2451
- """
2452
- Rebuild SQLite analytics index from JSONL events.
2453
-
2454
- Creates an optimized SQLite index for fast analytics queries.
2455
-
2456
- Returns:
2457
- EventRebuildResult with db_path and counts of inserted/skipped events
2458
-
2459
- Raises:
2460
- EventOperationError: If rebuild fails
2461
-
2462
- Example:
2463
- >>> sdk = SDK(agent="claude")
2464
- >>> result = sdk.rebuild_event_index()
2465
- >>> print(f"Rebuilt index: {result.db_path}")
2466
- >>> print(f"Inserted {result.inserted} events")
2467
- >>> print(f"Skipped {result.skipped} (duplicates)")
2468
-
2469
- See also:
2470
- export_sessions: Export HTML sessions to JSONL first
2471
- """
2472
- from htmlgraph.operations import events
2473
-
2474
- return events.rebuild_index(graph_dir=self._directory)
2475
-
2476
- def query_events(
2477
- self,
2478
- session_id: str | None = None,
2479
- tool: str | None = None,
2480
- feature_id: str | None = None,
2481
- since: str | None = None,
2482
- limit: int | None = None,
2483
- ) -> Any:
2484
- """
2485
- Query events from JSONL logs with optional filters.
2486
-
2487
- Args:
2488
- session_id: Filter by session ID (None = all sessions)
2489
- tool: Filter by tool name (e.g., 'Bash', 'Edit')
2490
- feature_id: Filter by attributed feature ID
2491
- since: Only events after this timestamp (ISO string)
2492
- limit: Maximum number of events to return
2493
-
2494
- Returns:
2495
- EventQueryResult with matching events and total count
2496
-
2497
- Raises:
2498
- EventOperationError: If query fails
2499
-
2500
- Example:
2501
- >>> sdk = SDK(agent="claude")
2502
- >>> # Get all events for a session
2503
- >>> result = sdk.query_events(session_id="sess-123")
2504
- >>> print(f"Found {result.total} events")
2505
- >>>
2506
- >>> # Get recent Bash events
2507
- >>> result = sdk.query_events(
2508
- ... tool="Bash",
2509
- ... since="2025-01-01T00:00:00Z",
2510
- ... limit=10
2511
- ... )
2512
- >>> for event in result.events:
2513
- ... print(f"{event['timestamp']}: {event['summary']}")
2514
-
2515
- See also:
2516
- export_sessions: Export sessions to JSONL first
2517
- get_event_stats: Get event statistics
2518
- """
2519
- from htmlgraph.operations import events
2520
-
2521
- return events.query_events(
2522
- graph_dir=self._directory,
2523
- session_id=session_id,
2524
- tool=tool,
2525
- feature_id=feature_id,
2526
- since=since,
2527
- limit=limit,
2528
- )
2529
-
2530
- def get_event_stats(self) -> Any:
2531
- """
2532
- Get statistics about events in the system.
2533
-
2534
- Returns:
2535
- EventStats with counts of total events, sessions, and files
2536
-
2537
- Example:
2538
- >>> sdk = SDK(agent="claude")
2539
- >>> stats = sdk.get_event_stats()
2540
- >>> print(f"Total events: {stats.total_events}")
2541
- >>> print(f"Sessions: {stats.session_count}")
2542
- >>> print(f"JSONL files: {stats.file_count}")
2543
- """
2544
- from htmlgraph.operations import events
2545
-
2546
- return events.get_event_stats(graph_dir=self._directory)
2547
-
2548
- def analyze_session(self, session_id: str) -> Any:
2549
- """
2550
- Compute detailed analytics for a single session.
2551
-
2552
- Analyzes work distribution, spike-to-feature ratio, maintenance burden,
2553
- transition metrics, and more.
2554
-
2555
- Args:
2556
- session_id: ID of the session to analyze
2557
-
2558
- Returns:
2559
- AnalyticsSessionResult with session metrics and warnings
2560
-
2561
- Raises:
2562
- AnalyticsOperationError: If session cannot be analyzed
2563
-
2564
- Example:
2565
- >>> sdk = SDK(agent="claude")
2566
- >>> result = sdk.analyze_session("sess-123")
2567
- >>> print(f"Primary work type: {result.metrics['primary_work_type']}")
2568
- >>> print(f"Total events: {result.metrics['total_events']}")
2569
- >>> print(f"Work distribution: {result.metrics['work_distribution']}")
2570
- >>> if result.warnings:
2571
- ... print(f"Warnings: {result.warnings}")
2572
-
2573
- See also:
2574
- analyze_project: Analyze entire project
2575
- """
2576
- from htmlgraph.operations import analytics
2577
-
2578
- return analytics.analyze_session(
2579
- graph_dir=self._directory,
2580
- session_id=session_id,
2581
- )
2582
-
2583
- def analyze_project(self) -> Any:
2584
- """
2585
- Compute project-wide analytics.
2586
-
2587
- Analyzes all sessions, work distribution, spike-to-feature ratios,
2588
- maintenance burden, and session types across the entire project.
2589
-
2590
- Returns:
2591
- AnalyticsProjectResult with project metrics and warnings
2592
-
2593
- Raises:
2594
- AnalyticsOperationError: If project cannot be analyzed
2595
-
2596
- Example:
2597
- >>> sdk = SDK(agent="claude")
2598
- >>> result = sdk.analyze_project()
2599
- >>> print(f"Total sessions: {result.metrics['total_sessions']}")
2600
- >>> print(f"Work distribution: {result.metrics['work_distribution']}")
2601
- >>> print(f"Spike-to-feature ratio: {result.metrics['spike_to_feature_ratio']}")
2602
- >>> print(f"Session types: {result.metrics['session_types']}")
2603
- >>> for session in result.metrics['recent_sessions']:
2604
- ... print(f" {session['session_id']}: {session['primary_work_type']}")
2605
-
2606
- See also:
2607
- analyze_session: Analyze a single session
2608
- get_work_recommendations: Get work recommendations
2609
- """
2610
- from htmlgraph.operations import analytics
2611
-
2612
- return analytics.analyze_project(graph_dir=self._directory)
2613
-
2614
- def get_work_recommendations(self) -> Any:
2615
- """
2616
- Get smart work recommendations based on project state.
2617
-
2618
- Uses dependency analytics to recommend next tasks based on priority,
2619
- dependencies, and impact.
2620
-
2621
- Returns:
2622
- RecommendationsResult with recommendations, reasoning, and warnings
2623
-
2624
- Raises:
2625
- AnalyticsOperationError: If recommendations cannot be generated
2626
-
2627
- Example:
2628
- >>> sdk = SDK(agent="claude")
2629
- >>> result = sdk.get_work_recommendations()
2630
- >>> for rec in result.recommendations:
2631
- ... print(f"{rec['title']} (score: {rec['score']})")
2632
- ... print(f" Reasons: {rec['reasons']}")
2633
- ... print(f" Unlocks: {rec['unlocks']}")
2634
- >>> print(f"Reasoning: {result.reasoning}")
2635
-
2636
- See also:
2637
- recommend_next_work: Legacy method (backward compatibility)
2638
- get_work_queue: Get prioritized work queue
2639
- """
2640
- from htmlgraph.operations import analytics
2641
-
2642
- return analytics.get_recommendations(graph_dir=self._directory)
2643
-
2644
- # =========================================================================
2645
- # Task Attribution - Subagent Work Tracking
2646
- # =========================================================================
2647
-
2648
- def get_task_attribution(self, task_id: str) -> dict[str, Any]:
2649
- """
2650
- Get attribution - which subagent did what work in this task?
2651
-
2652
- Queries the database to find all events associated with a Claude Code task,
2653
- showing which subagent executed each tool call.
2654
-
2655
- Args:
2656
- task_id: Claude Code's internal task ID (available from Task() response)
2657
-
2658
- Returns:
2659
- Dictionary with task_id, by_subagent mapping, and total_events count
2660
-
2661
- Example:
2662
- >>> sdk = SDK(agent="claude")
2663
- >>> result = sdk.get_task_attribution("task-abc123-xyz789")
2664
- >>> for subagent, events in result['by_subagent'].items():
2665
- ... print(f"{subagent}:")
2666
- ... for event in events:
2667
- ... print(f" - {event['tool']}: {event['summary']}")
2668
- >>> print(f"Total events: {result['total_events']}")
2669
-
2670
- See also:
2671
- get_subagent_work: Get all work grouped by subagent in a session
2672
- """
2673
- from htmlgraph.config import get_database_path
2674
- from htmlgraph.db.schema import HtmlGraphDB
2675
-
2676
- try:
2677
- db = HtmlGraphDB(str(get_database_path()))
2678
- events = db.get_events_for_task(task_id)
2679
-
2680
- # Group by subagent_type
2681
- by_subagent: dict[str, list[dict[str, Any]]] = {}
2682
- for event in events:
2683
- agent = event.get("subagent_type", "orchestrator")
2684
- if agent not in by_subagent:
2685
- by_subagent[agent] = []
2686
- by_subagent[agent].append(
2687
- {
2688
- "tool": event.get("tool_name"),
2689
- "summary": event.get("input_summary"),
2690
- "timestamp": event.get("created_at"),
2691
- "event_id": event.get("event_id"),
2692
- "success": not event.get("is_error", False),
2693
- }
2694
- )
2695
-
2696
- return {
2697
- "task_id": task_id,
2698
- "by_subagent": by_subagent,
2699
- "total_events": len(events),
2700
- }
2701
- except Exception as e:
2702
- return {
2703
- "task_id": task_id,
2704
- "by_subagent": {},
2705
- "total_events": 0,
2706
- "error": str(e),
2707
- }
2708
-
2709
- def get_subagent_work(self, session_id: str) -> dict[str, list[dict[str, Any]]]:
2710
- """
2711
- Get all work grouped by which subagent did it in a session.
2712
-
2713
- Shows which subagent (researcher, general-purpose, etc.) executed each
2714
- tool call within a session.
2715
-
2716
- Args:
2717
- session_id: Session ID to analyze
2718
-
2719
- Returns:
2720
- Dictionary mapping subagent_type to list of events they executed.
2721
- Each event includes: tool_name, input_summary, output_summary, created_at, event_id
2722
-
2723
- Example:
2724
- >>> sdk = SDK(agent="claude")
2725
- >>> work = sdk.get_subagent_work("sess-123")
2726
- >>> for subagent, events in work.items():
2727
- ... print(f"{subagent} ({len(events)} events):")
2728
- ... for event in events:
2729
- ... print(f" - {event['tool_name']}: {event['input_summary']}")
2730
-
2731
- See also:
2732
- get_task_attribution: Get work for a specific Claude Code task
2733
- analyze_session: Get session metrics and analytics
2734
- """
2735
- from htmlgraph.config import get_database_path
2736
- from htmlgraph.db.schema import HtmlGraphDB
2737
-
2738
- try:
2739
- db = HtmlGraphDB(str(get_database_path()))
2740
- return db.get_subagent_work(session_id)
2741
- except Exception:
2742
- return {}
2743
-
2744
- # =========================================================================
2745
- # Help & Documentation
2746
- # =========================================================================
2747
-
2748
- def help(self, topic: str | None = None) -> str:
2749
- """
2750
- Get help on SDK usage.
2751
-
2752
- Args:
2753
- topic: Optional topic (e.g., 'features', 'sessions', 'analytics', 'orchestration')
2754
-
2755
- Returns:
2756
- Formatted help text
2757
-
2758
- Example:
2759
- >>> sdk = SDK(agent="claude")
2760
- >>> print(sdk.help()) # List all topics
2761
- >>> print(sdk.help('features')) # Feature collection help
2762
- >>> print(sdk.help('analytics')) # Analytics help
2763
-
2764
- See also:
2765
- Python's built-in help(sdk) for full API documentation
2766
- sdk.features, sdk.bugs, sdk.spikes for work item managers
2767
- """
2768
- if topic is None:
2769
- return self._help_index()
2770
- return self._help_topic(topic)
2771
-
2772
- def _help_index(self) -> str:
2773
- """Return overview of all available methods/collections."""
2774
- return """HtmlGraph SDK - Quick Reference
2775
-
2776
- COLLECTIONS (Work Items):
2777
- sdk.features - Feature work items with builder support
2778
- sdk.bugs - Bug reports
2779
- sdk.spikes - Investigation and research spikes
2780
- sdk.chores - Maintenance and chore tasks
2781
- sdk.epics - Large bodies of work
2782
- sdk.phases - Project phases
2783
-
2784
- COLLECTIONS (Non-Work):
2785
- sdk.sessions - Agent sessions
2786
- sdk.tracks - Work tracks with builder support
2787
- sdk.agents - Agent information
2788
-
2789
- LEARNING (Active Learning):
2790
- sdk.patterns - Workflow patterns (optimal/anti-pattern)
2791
- sdk.insights - Session health insights
2792
- sdk.metrics - Aggregated time-series metrics
2793
-
2794
- CORE METHODS:
2795
- sdk.summary() - Get project summary
2796
- sdk.my_work() - Get current agent's workload
2797
- sdk.next_task() - Get next available task
2798
- sdk.reload() - Reload all data from disk
2799
-
2800
- SESSION MANAGEMENT:
2801
- sdk.start_session() - Start a new session
2802
- sdk.end_session() - End a session
2803
- sdk.track_activity() - Track activity in session
2804
- sdk.dedupe_sessions() - Clean up low-signal sessions
2805
- sdk.get_status() - Get project status
2806
-
2807
- STRATEGIC ANALYTICS:
2808
- sdk.find_bottlenecks() - Identify blocking tasks
2809
- sdk.recommend_next_work() - Get smart recommendations
2810
- sdk.get_parallel_work() - Find parallelizable work
2811
- sdk.assess_risks() - Assess dependency risks
2812
- sdk.analyze_impact() - Analyze task impact
2813
-
2814
- WORK QUEUE:
2815
- sdk.get_work_queue() - Get prioritized work queue
2816
- sdk.work_next() - Get next best task (smart routing)
2817
-
2818
- PLANNING WORKFLOW:
2819
- sdk.smart_plan() - Smart planning with research
2820
- sdk.start_planning_spike() - Create planning spike
2821
- sdk.create_track_from_plan() - Create track from plan
2822
- sdk.plan_parallel_work() - Plan parallel execution
2823
- sdk.aggregate_parallel_results() - Aggregate parallel results
2824
-
2825
- ORCHESTRATION:
2826
- sdk.spawn_explorer() - Spawn explorer subagent
2827
- sdk.spawn_coder() - Spawn coder subagent
2828
- sdk.orchestrate() - Orchestrate feature implementation
2829
-
2830
- SESSION OPTIMIZATION:
2831
- sdk.get_session_start_info() - Get comprehensive session start info
2832
- sdk.get_active_work_item() - Get currently active work item
2833
-
2834
- ANALYTICS INTERFACES:
2835
- sdk.analytics - Work type analytics
2836
- sdk.dep_analytics - Dependency analytics
2837
- sdk.context - Context analytics
2838
-
2839
- OPERATIONS (Server, Hooks, Events):
2840
- sdk.start_server() - Start web server for graph browsing
2841
- sdk.stop_server() - Stop running server
2842
- sdk.install_hooks() - Install Git hooks for tracking
2843
- sdk.list_hooks() - List Git hooks status
2844
- sdk.export_sessions() - Export HTML sessions to JSONL
2845
- sdk.rebuild_event_index() - Rebuild SQLite index from events
2846
- sdk.query_events() - Query JSONL event logs
2847
- sdk.get_event_stats() - Get event statistics
2848
- sdk.analyze_session() - Analyze single session metrics
2849
- sdk.analyze_project() - Analyze project-wide metrics
2850
- sdk.get_work_recommendations() - Get work recommendations
2851
-
2852
- ERROR HANDLING:
2853
- Lookup (.get) - Returns None if not found
2854
- Query (.where) - Returns empty list on no matches
2855
- Edit (.edit) - Raises NodeNotFoundError if missing
2856
- Batch (.mark_done) - Returns dict with success_count, failed_ids, warnings
2857
-
2858
- For detailed help on a topic:
2859
- sdk.help('features') - Feature collection methods
2860
- sdk.help('analytics') - Analytics methods
2861
- sdk.help('sessions') - Session management
2862
- sdk.help('orchestration') - Subagent orchestration
2863
- sdk.help('planning') - Planning workflow
2864
- sdk.help('operations') - Server, hooks, events operations
2865
- """
2866
-
2867
- def __dir__(self) -> list[str]:
2868
- """Return attributes with most useful ones first for discoverability."""
2869
- priority = [
2870
- # Work item managers
2871
- "features",
2872
- "bugs",
2873
- "spikes",
2874
- "chores",
2875
- "epics",
2876
- "phases",
2877
- # Non-work collections
2878
- "tracks",
2879
- "sessions",
2880
- "agents",
2881
- # Learning collections
2882
- "patterns",
2883
- "insights",
2884
- "metrics",
2885
- # Orchestration
2886
- "spawn_explorer",
2887
- "spawn_coder",
2888
- "orchestrate",
2889
- # Session management
2890
- "get_session_start_info",
2891
- "start_session",
2892
- "end_session",
2893
- # Strategic analytics
2894
- "find_bottlenecks",
2895
- "recommend_next_work",
2896
- "get_parallel_work",
2897
- # Work queue
2898
- "get_work_queue",
2899
- "work_next",
2900
- # Operations
2901
- "start_server",
2902
- "install_hooks",
2903
- "export_sessions",
2904
- "analyze_project",
2905
- # Help
2906
- "help",
2907
- ]
2908
- # Get all attributes
2909
- all_attrs = object.__dir__(self)
2910
- # Separate into priority, regular, and dunder attributes
2911
- regular = [a for a in all_attrs if not a.startswith("_") and a not in priority]
2912
- dunder = [a for a in all_attrs if a.startswith("_")]
2913
- # Return priority items first, then regular, then dunder
2914
- return priority + regular + dunder
2915
-
2916
- def _help_topic(self, topic: str) -> str:
2917
- """Return specific help for topic."""
2918
- topic = topic.lower()
2919
-
2920
- if topic in ["feature", "features"]:
2921
- return """FEATURES COLLECTION
2922
-
2923
- Create and manage feature work items with builder support.
2924
-
2925
- COMMON METHODS:
2926
- sdk.features.create(title) - Create new feature (returns builder)
2927
- sdk.features.get(id) - Get feature by ID
2928
- sdk.features.all() - Get all features
2929
- sdk.features.where(**filters) - Query features
2930
- sdk.features.edit(id) - Edit feature (context manager)
2931
- sdk.features.mark_done(ids) - Mark features as done
2932
- sdk.features.assign(ids, agent) - Assign features to agent
2933
-
2934
- BUILDER PATTERN:
2935
- feature = (sdk.features.create("User Auth")
2936
- .set_priority("high")
2937
- .add_steps(["Login", "Logout", "Reset password"])
2938
- .add_edge("blocked_by", "feat-database")
2939
- .save())
2940
-
2941
- QUERIES:
2942
- high_priority = sdk.features.where(status="todo", priority="high")
2943
- my_features = sdk.features.where(agent_assigned="claude")
2944
- blocked = sdk.features.where(status="blocked")
2945
-
2946
- CONTEXT MANAGER:
2947
- with sdk.features.edit("feat-001") as f:
2948
- f.status = "in-progress"
2949
- f.complete_step(0)
2950
- # Auto-saves on exit
2951
-
2952
- BATCH OPERATIONS:
2953
- result = sdk.features.mark_done(["feat-001", "feat-002"])
2954
- print(f"Completed {result['success_count']} features")
2955
- if result['failed_ids']:
2956
- print(f"Failed: {result['failed_ids']}")
2957
-
2958
- COMMON MISTAKES:
2959
- ❌ sdk.features.mark_complete([ids]) → ✅ sdk.features.mark_done([ids])
2960
- ❌ sdk.feature.create(...) → ✅ sdk.features.create(...)
2961
- ❌ claim(id, agent_id=...) → ✅ claim(id, agent=...)
2962
- ❌ builder.status = "done" → ✅ builder.save(); then edit()
2963
-
2964
- See also: sdk.help('bugs'), sdk.help('spikes'), sdk.help('chores')
2965
- """
2966
-
2967
- elif topic in ["bug", "bugs"]:
2968
- return """BUGS COLLECTION
2969
-
2970
- Create and manage bug reports.
2971
-
2972
- COMMON METHODS:
2973
- sdk.bugs.create(title) - Create new bug (returns builder)
2974
- sdk.bugs.get(id) - Get bug by ID
2975
- sdk.bugs.all() - Get all bugs
2976
- sdk.bugs.where(**filters) - Query bugs
2977
- sdk.bugs.edit(id) - Edit bug (context manager)
2978
-
2979
- BUILDER PATTERN:
2980
- bug = (sdk.bugs.create("Login fails on Safari")
2981
- .set_priority("critical")
2982
- .add_steps(["Reproduce", "Fix", "Test"])
2983
- .save())
2984
-
2985
- QUERIES:
2986
- critical = sdk.bugs.where(priority="critical", status="todo")
2987
- my_bugs = sdk.bugs.where(agent_assigned="claude")
2988
-
2989
- See also: sdk.help('features'), sdk.help('spikes')
2990
- """
2991
-
2992
- elif topic in ["spike", "spikes"]:
2993
- return """SPIKES COLLECTION
2994
-
2995
- Create and manage investigation/research spikes.
2996
-
2997
- COMMON METHODS:
2998
- sdk.spikes.create(title) - Create new spike (returns builder)
2999
- sdk.spikes.get(id) - Get spike by ID
3000
- sdk.spikes.all() - Get all spikes
3001
- sdk.spikes.where(**filters) - Query spikes
3002
-
3003
- BUILDER PATTERN:
3004
- spike = (sdk.spikes.create("Research OAuth providers")
3005
- .set_priority("high")
3006
- .add_steps(["Research", "Document findings"])
3007
- .save())
3008
-
3009
- PLANNING SPIKES:
3010
- spike = sdk.start_planning_spike(
3011
- "Plan User Auth",
3012
- context="Users need login",
3013
- timebox_hours=4.0
3014
- )
3015
-
3016
- See also: sdk.help('planning'), sdk.help('features')
3017
- """
3018
-
3019
- elif topic in ["chore", "chores"]:
3020
- return """CHORES COLLECTION
3021
-
3022
- Create and manage maintenance and chore tasks.
3023
-
3024
- COMMON METHODS:
3025
- sdk.chores.create(title) - Create new chore (returns builder)
3026
- sdk.chores.get(id) - Get chore by ID
3027
- sdk.chores.all() - Get all chores
3028
- sdk.chores.where(**filters) - Query chores
3029
-
3030
- BUILDER PATTERN:
3031
- chore = (sdk.chores.create("Update dependencies")
3032
- .set_priority("medium")
3033
- .add_steps(["Run uv update", "Test", "Commit"])
3034
- .save())
3035
-
3036
- See also: sdk.help('features'), sdk.help('bugs')
3037
- """
3038
-
3039
- elif topic in ["epic", "epics"]:
3040
- return """EPICS COLLECTION
3041
-
3042
- Create and manage large bodies of work.
3043
-
3044
- COMMON METHODS:
3045
- sdk.epics.create(title) - Create new epic (returns builder)
3046
- sdk.epics.get(id) - Get epic by ID
3047
- sdk.epics.all() - Get all epics
3048
- sdk.epics.where(**filters) - Query epics
3049
-
3050
- BUILDER PATTERN:
3051
- epic = (sdk.epics.create("Authentication System")
3052
- .set_priority("critical")
3053
- .add_steps(["Design", "Implement", "Test", "Deploy"])
3054
- .save())
3055
-
3056
- See also: sdk.help('features'), sdk.help('tracks')
3057
- """
3058
-
3059
- elif topic in ["track", "tracks"]:
3060
- return """TRACKS COLLECTION
3061
-
3062
- Create and manage work tracks with builder support.
3063
-
3064
- COMMON METHODS:
3065
- sdk.tracks.create(title) - Create new track (returns builder)
3066
- sdk.tracks.builder() - Get track builder
3067
- sdk.tracks.get(id) - Get track by ID
3068
- sdk.tracks.all() - Get all tracks
3069
- sdk.tracks.where(**filters) - Query tracks
3070
-
3071
- BUILDER PATTERN:
3072
- track = (sdk.tracks.builder()
3073
- .title("User Authentication")
3074
- .description("OAuth 2.0 system")
3075
- .priority("high")
3076
- .with_spec(
3077
- overview="OAuth integration",
3078
- requirements=[("OAuth 2.0", "must-have")],
3079
- acceptance_criteria=["Login works"]
3080
- )
3081
- .with_plan_phases([
3082
- ("Phase 1", ["Setup (2h)", "Config (1h)"]),
3083
- ("Phase 2", ["Testing (2h)"])
3084
- ])
3085
- .create())
3086
-
3087
- FROM PLANNING:
3088
- track_info = sdk.create_track_from_plan(
3089
- title="User Auth",
3090
- description="OAuth system",
3091
- requirements=[("OAuth", "must-have")],
3092
- phases=[("Phase 1", ["Setup", "Config"])]
3093
- )
3094
-
3095
- See also: sdk.help('planning'), sdk.help('features')
3096
- """
3097
-
3098
- elif topic in ["session", "sessions"]:
3099
- return """SESSION MANAGEMENT
3100
-
3101
- Create and manage agent sessions.
3102
-
3103
- SESSION METHODS:
3104
- sdk.start_session(title=...) - Start new session
3105
- sdk.end_session(id) - End session
3106
- sdk.track_activity(...) - Track activity in session
3107
- sdk.dedupe_sessions(...) - Clean up low-signal sessions
3108
- sdk.get_status() - Get project status
3109
-
3110
- SESSION COLLECTION:
3111
- sdk.sessions.get(id) - Get session by ID
3112
- sdk.sessions.all() - Get all sessions
3113
- sdk.sessions.where(**filters) - Query sessions
3114
-
3115
- TYPICAL WORKFLOW:
3116
- # Session start hook handles this automatically
3117
- session = sdk.start_session(title="Fix login bug")
3118
-
3119
- # Track activities (handled by hooks)
3120
- sdk.track_activity(
3121
- tool="Edit",
3122
- summary="Fixed auth logic",
3123
- file_paths=["src/auth.py"],
3124
- success=True
3125
- )
3126
-
3127
- # End session
3128
- sdk.end_session(
3129
- session.id,
3130
- handoff_notes="Login bug fixed, needs testing"
3131
- )
3132
-
3133
- CLEANUP:
3134
- # Remove orphaned sessions (<=1 event)
3135
- result = sdk.dedupe_sessions(max_events=1, dry_run=False)
3136
-
3137
- See also: sdk.help('analytics')
3138
- """
3139
-
3140
- elif topic in ["analytic", "analytics", "strategic"]:
3141
- return """STRATEGIC ANALYTICS
3142
-
3143
- Find bottlenecks, recommend work, and assess risks.
3144
-
3145
- DEPENDENCY ANALYTICS:
3146
- bottlenecks = sdk.find_bottlenecks(top_n=5)
3147
- # Returns tasks blocking the most work
3148
-
3149
- parallel = sdk.get_parallel_work(max_agents=5)
3150
- # Returns tasks that can run simultaneously
3151
-
3152
- recs = sdk.recommend_next_work(agent_count=3)
3153
- # Returns smart recommendations with scoring
3154
-
3155
- risks = sdk.assess_risks()
3156
- # Returns high-risk tasks and circular deps
3157
-
3158
- impact = sdk.analyze_impact("feat-001")
3159
- # Returns what unlocks if you complete this task
3160
-
3161
- DIRECT ACCESS (preferred):
3162
- sdk.dep_analytics.find_bottlenecks(top_n=5)
3163
- sdk.dep_analytics.recommend_next_tasks(agent_count=3)
3164
- sdk.dep_analytics.find_parallelizable_work(status="todo")
3165
- sdk.dep_analytics.assess_dependency_risk()
3166
- sdk.dep_analytics.impact_analysis("feat-001")
3167
-
3168
- WORK TYPE ANALYTICS:
3169
- sdk.analytics.get_wip_by_type()
3170
- sdk.analytics.get_completion_rates()
3171
- sdk.analytics.get_agent_workload()
3172
-
3173
- CONTEXT ANALYTICS:
3174
- sdk.context.track_usage(...)
3175
- sdk.context.get_usage_report()
3176
-
3177
- See also: sdk.help('planning'), sdk.help('work_queue')
3178
- """
3179
-
3180
- elif topic in ["queue", "work_queue", "routing"]:
3181
- return """WORK QUEUE & ROUTING
3182
-
3183
- Get prioritized work using smart routing.
3184
-
3185
- WORK QUEUE:
3186
- queue = sdk.get_work_queue(limit=10, min_score=0.0)
3187
- # Returns prioritized list with scores
3188
-
3189
- for item in queue:
3190
- print(f"{item['score']:.1f} - {item['title']}")
3191
- if item.get('blocked_by'):
3192
- print(f" Blocked by: {item['blocked_by']}")
3193
-
3194
- SMART ROUTING:
3195
- task = sdk.work_next(auto_claim=True, min_score=0.5)
3196
- # Returns next best task using analytics + capabilities
3197
-
3198
- if task:
3199
- print(f"Working on: {task.title}")
3200
- # Task is auto-claimed and assigned
3201
-
3202
- SIMPLE NEXT TASK:
3203
- task = sdk.next_task(priority="high", auto_claim=True)
3204
- # Simpler version without smart routing
3205
-
3206
- See also: sdk.help('analytics')
3207
- """
3208
-
3209
- elif topic in ["plan", "planning", "workflow"]:
3210
- return """PLANNING WORKFLOW
3211
-
3212
- Research, plan, and create tracks for new work.
3213
-
3214
- SMART PLANNING:
3215
- plan = sdk.smart_plan(
3216
- "User authentication system",
3217
- create_spike=True,
3218
- timebox_hours=4.0,
3219
- research_completed=True, # IMPORTANT: Do research first!
3220
- research_findings={
3221
- "topic": "OAuth 2.0 best practices",
3222
- "recommended_library": "authlib",
3223
- "key_insights": ["Use PKCE", "Token rotation"]
3224
- }
3225
- )
3226
-
3227
- PLANNING SPIKE:
3228
- spike = sdk.start_planning_spike(
3229
- "Plan Real-time Notifications",
3230
- context="Users need live updates",
3231
- timebox_hours=3.0
3232
- )
3233
-
3234
- CREATE TRACK FROM PLAN:
3235
- track_info = sdk.create_track_from_plan(
3236
- title="User Authentication",
3237
- description="OAuth 2.0 with JWT",
3238
- requirements=[
3239
- ("OAuth 2.0 integration", "must-have"),
3240
- ("JWT token management", "must-have")
3241
- ],
3242
- phases=[
3243
- ("Phase 1: OAuth", ["Setup (2h)", "Callback (2h)"]),
3244
- ("Phase 2: JWT", ["Token signing (2h)"])
3245
- ]
3246
- )
3247
-
3248
- PARALLEL PLANNING:
3249
- plan = sdk.plan_parallel_work(max_agents=3)
3250
- if plan["can_parallelize"]:
3251
- for p in plan["prompts"]:
3252
- Task(prompt=p["prompt"])
3253
-
3254
- # After parallel work completes
3255
- results = sdk.aggregate_parallel_results([
3256
- "agent-1", "agent-2", "agent-3"
3257
- ])
3258
-
3259
- See also: sdk.help('tracks'), sdk.help('spikes')
3260
- """
3261
-
3262
- elif topic in ["orchestration", "orchestrate", "subagent", "subagents"]:
3263
- return """SUBAGENT ORCHESTRATION
3264
-
3265
- Spawn explorer and coder subagents for complex work.
3266
-
3267
- EXPLORER (Discovery):
3268
- prompt = sdk.spawn_explorer(
3269
- task="Find all API endpoints",
3270
- scope="src/api/",
3271
- patterns=["*.py"],
3272
- questions=["What framework is used?"]
3273
- )
3274
- # Execute with Task tool
3275
- Task(prompt=prompt["prompt"], description=prompt["description"])
3276
-
3277
- CODER (Implementation):
3278
- prompt = sdk.spawn_coder(
3279
- feature_id="feat-add-auth",
3280
- context=explorer_results,
3281
- files_to_modify=["src/auth.py"],
3282
- test_command="uv run pytest tests/auth/"
3283
- )
3284
- Task(prompt=prompt["prompt"], description=prompt["description"])
3285
-
3286
- FULL ORCHESTRATION:
3287
- prompts = sdk.orchestrate(
3288
- "feat-add-caching",
3289
- exploration_scope="src/cache/",
3290
- test_command="uv run pytest tests/cache/"
3291
- )
3292
-
3293
- # Phase 1: Explorer
3294
- Task(prompt=prompts["explorer"]["prompt"])
3295
-
3296
- # Phase 2: Coder (with explorer results)
3297
- Task(prompt=prompts["coder"]["prompt"])
3298
-
3299
- WORKFLOW:
3300
- 1. Explorer discovers code patterns and files
3301
- 2. Coder implements changes using explorer findings
3302
- 3. Both agents auto-track in sessions
3303
- 4. Feature gets updated with progress
3304
-
3305
- See also: sdk.help('planning')
3306
- """
3307
-
3308
- elif topic in ["optimization", "session_start", "active_work"]:
3309
- return """SESSION OPTIMIZATION
3310
-
3311
- Reduce context usage with optimized methods.
3312
-
3313
- SESSION START INFO:
3314
- info = sdk.get_session_start_info(
3315
- include_git_log=True,
3316
- git_log_count=5,
3317
- analytics_top_n=3
3318
- )
3319
-
3320
- # Single call returns:
3321
- # - status: Project status
3322
- # - active_work: Current work item
3323
- # - features: All features
3324
- # - sessions: Recent sessions
3325
- # - git_log: Recent commits
3326
- # - analytics: Bottlenecks, recommendations, parallel
3327
-
3328
- ACTIVE WORK ITEM:
3329
- active = sdk.get_active_work_item()
3330
- if active:
3331
- print(f"Working on: {active['title']}")
3332
- print(f"Progress: {active['steps_completed']}/{active['steps_total']}")
3333
-
3334
- # Filter by agent
3335
- active = sdk.get_active_work_item(filter_by_agent=True)
3336
-
3337
- BENEFITS:
3338
- - 6+ tool calls → 1 method call
3339
- - Reduced token usage
3340
- - Faster session initialization
3341
- - All context in one place
3342
-
3343
- See also: sdk.help('sessions')
3344
- """
3345
-
3346
- elif topic in ["operation", "operations", "server", "hooks", "events"]:
3347
- return """OPERATIONS - Server, Hooks, Events
3348
-
3349
- Infrastructure operations for running HtmlGraph.
3350
-
3351
- SERVER OPERATIONS:
3352
- # Start server for web UI
3353
- result = sdk.start_server(port=8080, watch=True)
3354
- print(f"Server at {result.handle.url}")
3355
-
3356
- # Stop server
3357
- sdk.stop_server(result.handle)
3358
-
3359
- # Check status
3360
- status = sdk.get_server_status(result.handle)
3361
-
3362
- HOOK OPERATIONS:
3363
- # Install Git hooks for automatic tracking
3364
- result = sdk.install_hooks()
3365
- print(f"Installed: {result.installed}")
3366
-
3367
- # List hook status
3368
- result = sdk.list_hooks()
3369
- print(f"Enabled: {result.enabled}")
3370
- print(f"Missing: {result.missing}")
3371
-
3372
- # Validate configuration
3373
- result = sdk.validate_hook_config()
3374
- if not result.valid:
3375
- print(f"Errors: {result.errors}")
3376
-
3377
- EVENT OPERATIONS:
3378
- # Export HTML sessions to JSONL
3379
- result = sdk.export_sessions()
3380
- print(f"Exported {result.written} sessions")
3381
-
3382
- # Rebuild SQLite index
3383
- result = sdk.rebuild_event_index()
3384
- print(f"Inserted {result.inserted} events")
3385
-
3386
- # Query events
3387
- result = sdk.query_events(
3388
- session_id="sess-123",
3389
- tool="Bash",
3390
- limit=10
3391
- )
3392
- for event in result.events:
3393
- print(f"{event['timestamp']}: {event['summary']}")
3394
-
3395
- # Get statistics
3396
- stats = sdk.get_event_stats()
3397
- print(f"Total events: {stats.total_events}")
3398
-
3399
- ANALYTICS OPERATIONS:
3400
- # Analyze session
3401
- result = sdk.analyze_session("sess-123")
3402
- print(f"Primary work: {result.metrics['primary_work_type']}")
3403
-
3404
- # Analyze project
3405
- result = sdk.analyze_project()
3406
- print(f"Total sessions: {result.metrics['total_sessions']}")
3407
- print(f"Work distribution: {result.metrics['work_distribution']}")
3408
-
3409
- # Get recommendations
3410
- result = sdk.get_work_recommendations()
3411
- for rec in result.recommendations:
3412
- print(f"{rec['title']} (score: {rec['score']})")
3413
-
3414
- See also: sdk.help('analytics'), sdk.help('sessions')
3415
- """
3416
-
3417
- else:
3418
- return f"""Unknown topic: '{topic}'
3419
-
3420
- Available topics:
3421
- - features, bugs, spikes, chores, epics (work collections)
3422
- - tracks, sessions, agents (non-work collections)
3423
- - analytics, strategic (dependency and work analytics)
3424
- - work_queue, routing (smart task routing)
3425
- - planning, workflow (planning and track creation)
3426
- - orchestration, subagents (explorer/coder spawning)
3427
- - optimization, session_start (context optimization)
3428
-
3429
- Try: sdk.help() for full overview
3430
- """