htmlgraph 0.9.3__py3-none-any.whl → 0.27.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -1,3 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
1
7
  """
2
8
  Base collection class for managing nodes.
3
9
 
@@ -5,16 +11,20 @@ Provides common collection functionality for all node types
5
11
  with lazy-loading, filtering, and batch operations.
6
12
  """
7
13
 
8
- from __future__ import annotations
9
- from typing import TYPE_CHECKING, TypeVar, Generic, Any, Iterator
14
+
15
+ from collections.abc import Callable, Iterator
10
16
  from contextlib import contextmanager
11
17
  from datetime import datetime
18
+ from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, cast
19
+
20
+ from htmlgraph.exceptions import ClaimConflictError, NodeNotFoundError
12
21
 
13
22
  if TYPE_CHECKING:
14
- from htmlgraph.sdk import SDK
23
+ from htmlgraph.graph import HtmlGraph
15
24
  from htmlgraph.models import Node
25
+ from htmlgraph.sdk import SDK
16
26
 
17
- CollectionT = TypeVar('CollectionT', bound='BaseCollection')
27
+ CollectionT = TypeVar("CollectionT", bound="BaseCollection")
18
28
 
19
29
 
20
30
  class BaseCollection(Generic[CollectionT]):
@@ -40,8 +50,16 @@ class BaseCollection(Generic[CollectionT]):
40
50
 
41
51
  _collection_name: str = "nodes" # Override in subclasses
42
52
  _node_type: str = "node" # Override in subclasses
53
+ _builder_class: type | None = (
54
+ None # Override in subclasses to enable builder pattern
55
+ )
43
56
 
44
- def __init__(self, sdk: 'SDK', collection_name: str | None = None, node_type: str | None = None):
57
+ def __init__(
58
+ self,
59
+ sdk: SDK,
60
+ collection_name: str | None = None,
61
+ node_type: str | None = None,
62
+ ):
45
63
  """
46
64
  Initialize a collection.
47
65
 
@@ -55,26 +73,181 @@ class BaseCollection(Generic[CollectionT]):
55
73
  self._sdk = sdk
56
74
  self._collection_name = collection_name or self._collection_name
57
75
  self._node_type = node_type or self._node_type
58
- self._graph = None # Lazy-loaded
76
+ self._graph: HtmlGraph | None = None # Lazy-loaded
77
+ self._ref_manager: Any = None # Set by SDK during initialization
78
+
79
+ def _ensure_graph(self) -> HtmlGraph:
80
+ """
81
+ Get or initialize the graph for this collection.
59
82
 
60
- def _ensure_graph(self):
61
- """Lazy-load the graph for this collection."""
83
+ Uses SDK's shared graph instances where available to avoid creating
84
+ multiple graph objects for the same collection. Creates a new instance
85
+ for unrecognized collections.
86
+
87
+ Returns:
88
+ HtmlGraph instance for this collection
89
+
90
+ Note:
91
+ This method is lazy - the graph is only loaded on first access.
92
+ """
62
93
  if self._graph is None:
63
- from htmlgraph.graph import HtmlGraph
64
- collection_path = self._sdk._directory / self._collection_name
65
- self._graph = HtmlGraph(collection_path, auto_load=True)
94
+ # Use SDK's shared graph instances to avoid multiple graph objects
95
+ if self._collection_name == "features" and hasattr(self._sdk, "_graph"):
96
+ self._graph = self._sdk._graph
97
+ elif self._collection_name == "bugs" and hasattr(self._sdk, "_bugs_graph"):
98
+ self._graph = self._sdk._bugs_graph
99
+ else:
100
+ # For other collections, create a new graph instance
101
+ from htmlgraph.graph import HtmlGraph
102
+
103
+ collection_path = self._sdk._directory / self._collection_name
104
+ self._graph = HtmlGraph(collection_path, auto_load=True)
105
+
106
+ # Ensure graph is loaded
107
+ if not self._graph._nodes:
108
+ self._graph.reload()
109
+
66
110
  return self._graph
67
111
 
112
+ def __getattribute__(self, name: str) -> Any:
113
+ """
114
+ Override attribute access to provide helpful error messages.
115
+
116
+ When an attribute doesn't exist, provides suggestions for common
117
+ mistakes and similar method names to improve discoverability.
118
+
119
+ Args:
120
+ name: Attribute name being accessed
121
+
122
+ Returns:
123
+ The requested attribute
124
+
125
+ Raises:
126
+ AttributeError: With helpful suggestions if attribute not found
127
+ """
128
+ try:
129
+ return object.__getattribute__(self, name)
130
+ except AttributeError as e:
131
+ # Get available methods
132
+ available = [m for m in dir(self) if not m.startswith("_")]
133
+
134
+ # Common mistakes mapping
135
+ common_mistakes = {
136
+ "mark_complete": "mark_done",
137
+ "complete": "Use complete(node_id) for single item or mark_done([ids]) for batch",
138
+ "finish": "mark_done",
139
+ "end": "mark_done",
140
+ "update_status": "edit() context manager or batch_update()",
141
+ "mark_as_done": "mark_done",
142
+ "set_done": "mark_done",
143
+ "complete_all": "mark_done",
144
+ }
145
+
146
+ suggestions = []
147
+ if name in common_mistakes:
148
+ suggestions.append(f"Did you mean: {common_mistakes[name]}")
149
+
150
+ # Find similar method names
151
+ similar = [
152
+ m
153
+ for m in available
154
+ if name.lower() in m.lower() or m.lower() in name.lower()
155
+ ]
156
+ if similar:
157
+ suggestions.append(f"Similar methods: {', '.join(similar[:5])}")
158
+
159
+ # Build helpful error message
160
+ error_msg = f"'{type(self).__name__}' has no attribute '{name}'."
161
+ if suggestions:
162
+ error_msg += "\n\n" + "\n".join(suggestions)
163
+ error_msg += f"\n\nAvailable methods: {', '.join(available[:15])}"
164
+ error_msg += "\n\nTip: Use sdk.help() to see all available operations."
165
+
166
+ raise AttributeError(error_msg) from e
167
+
168
+ def __dir__(self) -> list[str]:
169
+ """
170
+ Return attributes with most useful ones first.
171
+
172
+ Orders attributes to show commonly-used methods first in auto-complete
173
+ and help() output, improving discoverability for new users.
174
+
175
+ Returns:
176
+ List of attribute names, ordered by priority then alphabetically
177
+ """
178
+ priority = [
179
+ # Creation and retrieval
180
+ "create",
181
+ "get",
182
+ "all",
183
+ "where",
184
+ "filter",
185
+ # Work management
186
+ "start",
187
+ "complete",
188
+ "claim",
189
+ "release",
190
+ # Editing
191
+ "edit",
192
+ "update",
193
+ # Batch operations
194
+ "mark_done",
195
+ "assign",
196
+ "batch_update",
197
+ # Deletion
198
+ "delete",
199
+ "batch_delete",
200
+ ]
201
+ # Get all attributes
202
+ all_attrs = object.__dir__(self)
203
+ # Separate into priority, regular, and dunder attributes
204
+ regular = [a for a in all_attrs if not a.startswith("_") and a not in priority]
205
+ dunder = [a for a in all_attrs if a.startswith("_")]
206
+ # Return priority items first, then regular, then dunder
207
+ return priority + regular + dunder
208
+
209
+ def set_ref_manager(self, ref_manager: Any) -> None:
210
+ """
211
+ Set the ref manager for this collection.
212
+
213
+ Called by SDK during initialization to enable short ref support.
214
+
215
+ Args:
216
+ ref_manager: RefManager instance from SDK
217
+ """
218
+ self._ref_manager = ref_manager
219
+
220
+ def get_ref(self, node_id: str) -> str | None:
221
+ """
222
+ Get short ref for a node in this collection.
223
+
224
+ Convenience method to get ref without accessing SDK directly.
225
+
226
+ Args:
227
+ node_id: Full node ID like "feat-a1b2c3d4"
228
+
229
+ Returns:
230
+ Short ref like "@f1", or None if ref manager not available
231
+
232
+ Example:
233
+ >>> feature = sdk.features.get("feat-abc123")
234
+ >>> ref = sdk.features.get_ref(feature.id)
235
+ >>> logger.info("%s", ref) # "@f1"
236
+ """
237
+ if self._ref_manager:
238
+ result = self._ref_manager.get_ref(node_id)
239
+ return cast(str | None, result)
240
+ return None
241
+
68
242
  def create(
69
- self,
70
- title: str,
71
- priority: str = "medium",
72
- status: str = "todo",
73
- **kwargs
74
- ) -> Node:
243
+ self, title: str, priority: str = "medium", status: str = "todo", **kwargs: Any
244
+ ) -> Any:
75
245
  """
76
246
  Create a new node in this collection.
77
247
 
248
+ If `_builder_class` is set, returns a builder instance for fluent interface.
249
+ Otherwise, creates and saves a simple Node directly.
250
+
78
251
  Args:
79
252
  title: Node title
80
253
  priority: Priority level (low, medium, high, critical)
@@ -82,12 +255,29 @@ class BaseCollection(Generic[CollectionT]):
82
255
  **kwargs: Additional node properties
83
256
 
84
257
  Returns:
85
- Created Node instance
258
+ Builder instance if `_builder_class` is set, else created Node instance
259
+
260
+ Raises:
261
+ ValueError: If node with same ID already exists (when using simple creation)
262
+ ValidationError: If invalid node properties provided
86
263
 
87
264
  Example:
88
- >>> bug = sdk.bugs.create("Login fails", priority="critical")
89
- >>> chore = sdk.chores.create("Update dependencies", priority="medium")
90
- """
265
+ >>> # With builder (FeatureCollection, BugCollection, etc.)
266
+ >>> feature = sdk.features.create("User Auth") \\
267
+ ... .set_priority("high") \\
268
+ ... .save()
269
+ >>>
270
+ >>> # Without builder (simple collections)
271
+ >>> node = sdk.nodes.create("Simple task", priority="medium")
272
+ """
273
+ # If builder class is configured, use it
274
+ if self._builder_class is not None:
275
+ # Pass priority and status to builder via kwargs
276
+ return self._builder_class(
277
+ self._sdk, title, priority=priority, status=status, **kwargs
278
+ )
279
+
280
+ # Fallback to simple node creation
91
281
  from htmlgraph.ids import generate_id
92
282
  from htmlgraph.models import Node
93
283
 
@@ -99,9 +289,14 @@ class BaseCollection(Generic[CollectionT]):
99
289
  id=node_id,
100
290
  title=title,
101
291
  type=self._node_type,
102
- priority=priority,
103
- status=status,
104
- **kwargs
292
+ priority=cast(Literal["low", "medium", "high", "critical"], priority),
293
+ status=cast(
294
+ Literal[
295
+ "todo", "in-progress", "blocked", "done", "active", "ended", "stale"
296
+ ],
297
+ status,
298
+ ),
299
+ **kwargs,
105
300
  )
106
301
 
107
302
  # Add to graph
@@ -123,7 +318,7 @@ class BaseCollection(Generic[CollectionT]):
123
318
  Example:
124
319
  >>> feature = sdk.features.get("feat-001")
125
320
  """
126
- return self._ensure_graph().get(node_id)
321
+ return cast("Node | None", self._ensure_graph().get(node_id))
127
322
 
128
323
  @contextmanager
129
324
  def edit(self, node_id: str) -> Iterator[Node]:
@@ -139,7 +334,7 @@ class BaseCollection(Generic[CollectionT]):
139
334
  The node to edit
140
335
 
141
336
  Raises:
142
- ValueError: If node not found
337
+ NodeNotFoundError: If node not found
143
338
 
144
339
  Example:
145
340
  >>> with sdk.features.edit("feat-001") as feature:
@@ -148,7 +343,7 @@ class BaseCollection(Generic[CollectionT]):
148
343
  graph = self._ensure_graph()
149
344
  node = graph.get(node_id)
150
345
  if not node:
151
- raise ValueError(f"{self._node_type.capitalize()} {node_id} not found")
346
+ raise NodeNotFoundError(self._node_type, node_id)
152
347
 
153
348
  yield node
154
349
 
@@ -161,7 +356,7 @@ class BaseCollection(Generic[CollectionT]):
161
356
  priority: str | None = None,
162
357
  track: str | None = None,
163
358
  assigned_to: str | None = None,
164
- **extra_filters
359
+ **extra_filters: Any,
165
360
  ) -> list[Node]:
166
361
  """
167
362
  Query nodes with filters.
@@ -180,16 +375,17 @@ class BaseCollection(Generic[CollectionT]):
180
375
  >>> high_priority = sdk.features.where(status="todo", priority="high")
181
376
  >>> assigned = sdk.features.where(assigned_to="claude")
182
377
  """
378
+
183
379
  def matches(node: Node) -> bool:
184
380
  if node.type != self._node_type:
185
381
  return False
186
- if status and getattr(node, 'status', None) != status:
382
+ if status and getattr(node, "status", None) != status:
187
383
  return False
188
- if priority and getattr(node, 'priority', None) != priority:
384
+ if priority and getattr(node, "priority", None) != priority:
189
385
  return False
190
386
  if track and getattr(node, "track_id", None) != track:
191
387
  return False
192
- if assigned_to and getattr(node, 'agent_assigned', None) != assigned_to:
388
+ if assigned_to and getattr(node, "agent_assigned", None) != assigned_to:
193
389
  return False
194
390
 
195
391
  # Check extra filters
@@ -199,7 +395,41 @@ class BaseCollection(Generic[CollectionT]):
199
395
 
200
396
  return True
201
397
 
202
- return self._ensure_graph().filter(matches)
398
+ return cast("list[Node]", self._ensure_graph().filter(matches))
399
+
400
+ def filter(self, predicate: Callable[[Node], bool]) -> list[Node]:
401
+ """
402
+ Filter nodes using a custom predicate function.
403
+
404
+ Args:
405
+ predicate: A callable that takes a Node and returns True if it matches
406
+
407
+ Returns:
408
+ List of nodes that match the predicate
409
+
410
+ Example:
411
+ >>> # Find features with "High" in title
412
+ >>> high_priority = sdk.features.filter(lambda f: "High" in f.title)
413
+ >>>
414
+ >>> # Find features created in the last week
415
+ >>> from datetime import datetime, timedelta
416
+ >>> recent = sdk.features.filter(
417
+ ... lambda f: f.created > datetime.now() - timedelta(days=7)
418
+ ... )
419
+ >>>
420
+ >>> # Complex multi-condition filter
421
+ >>> urgent = sdk.features.filter(
422
+ ... lambda f: f.priority == "high" and f.status == "todo"
423
+ ... )
424
+ """
425
+
426
+ def matches(node: Node) -> bool:
427
+ # First filter by type, then apply user predicate
428
+ if node.type != self._node_type:
429
+ return False
430
+ return predicate(node)
431
+
432
+ return cast("list[Node]", self._ensure_graph().filter(matches))
203
433
 
204
434
  def all(self) -> list[Node]:
205
435
  """
@@ -227,7 +457,7 @@ class BaseCollection(Generic[CollectionT]):
227
457
  >>> sdk.features.delete("feat-001")
228
458
  """
229
459
  graph = self._ensure_graph()
230
- return graph.delete(node_id)
460
+ return cast(bool, graph.delete(node_id))
231
461
 
232
462
  def batch_delete(self, node_ids: list[str]) -> int:
233
463
  """
@@ -241,10 +471,10 @@ class BaseCollection(Generic[CollectionT]):
241
471
 
242
472
  Example:
243
473
  >>> count = sdk.features.batch_delete(["feat-001", "feat-002", "feat-003"])
244
- >>> print(f"Deleted {count} features")
474
+ >>> logger.info(f"Deleted {count} features")
245
475
  """
246
476
  graph = self._ensure_graph()
247
- return graph.batch_delete(node_ids)
477
+ return cast(int, graph.batch_delete(node_ids))
248
478
 
249
479
  def update(self, node: Node) -> Node:
250
480
  """
@@ -256,6 +486,9 @@ class BaseCollection(Generic[CollectionT]):
256
486
  Returns:
257
487
  Updated node
258
488
 
489
+ Raises:
490
+ NodeNotFoundError: If node doesn't exist in the graph
491
+
259
492
  Example:
260
493
  >>> feature.status = "done"
261
494
  >>> sdk.features.update(feature)
@@ -264,11 +497,7 @@ class BaseCollection(Generic[CollectionT]):
264
497
  self._ensure_graph().update(node)
265
498
  return node
266
499
 
267
- def batch_update(
268
- self,
269
- node_ids: list[str],
270
- updates: dict[str, Any]
271
- ) -> int:
500
+ def batch_update(self, node_ids: list[str], updates: dict[str, Any]) -> int:
272
501
  """
273
502
  Vectorized batch update operation.
274
503
 
@@ -304,7 +533,7 @@ class BaseCollection(Generic[CollectionT]):
304
533
 
305
534
  return count
306
535
 
307
- def mark_done(self, node_ids: list[str]) -> int:
536
+ def mark_done(self, node_ids: list[str]) -> dict[str, Any]:
308
537
  """
309
538
  Batch mark nodes as done.
310
539
 
@@ -312,12 +541,55 @@ class BaseCollection(Generic[CollectionT]):
312
541
  node_ids: List of node IDs to mark as done
313
542
 
314
543
  Returns:
315
- Number of nodes updated
544
+ Dict with 'success_count', 'failed_ids', and 'warnings'
316
545
 
317
546
  Example:
318
- >>> sdk.features.mark_done(["feat-001", "feat-002"])
547
+ >>> result = sdk.features.mark_done(["feat-001", "feat-002"])
548
+ >>> logger.info(f"Completed {result['success_count']} of {len(node_ids)}")
549
+ >>> if result['failed_ids']:
550
+ ... logger.info(f"Failed: {result['failed_ids']}")
319
551
  """
320
- return self.batch_update(node_ids, {"status": "done"})
552
+ graph = self._ensure_graph()
553
+ results: dict[str, Any] = {"success_count": 0, "failed_ids": [], "warnings": []}
554
+
555
+ for node_id in node_ids:
556
+ try:
557
+ node = graph.get(node_id)
558
+ if not node:
559
+ results["failed_ids"].append(node_id)
560
+ results["warnings"].append(f"Node {node_id} not found")
561
+ continue
562
+
563
+ node.status = "done"
564
+ node.updated = datetime.now()
565
+ graph.update(node)
566
+ results["success_count"] += 1
567
+
568
+ # Log completion event to SQLite
569
+ try:
570
+ self._sdk._log_event(
571
+ event_type="tool_call",
572
+ tool_name="SDK.mark_done",
573
+ input_summary=f"Mark {self._node_type} done: {node_id}",
574
+ output_summary=f"Marked {node_id} as done",
575
+ context={
576
+ "collection": self._collection_name,
577
+ "node_id": node_id,
578
+ "node_type": self._node_type,
579
+ "title": node.title,
580
+ },
581
+ cost_tokens=25,
582
+ )
583
+ except Exception as e:
584
+ import logging
585
+
586
+ logging.debug(f"Event logging failed for mark_done: {e}")
587
+
588
+ except Exception as e:
589
+ results["failed_ids"].append(node_id)
590
+ results["warnings"].append(f"Failed to mark {node_id}: {str(e)}")
591
+
592
+ return results
321
593
 
322
594
  def assign(self, node_ids: list[str], agent: str) -> int:
323
595
  """
@@ -333,129 +605,167 @@ class BaseCollection(Generic[CollectionT]):
333
605
  Example:
334
606
  >>> sdk.features.assign(["feat-001", "feat-002"], "claude")
335
607
  """
336
- updates = {
337
- "agent_assigned": agent,
338
- "status": "in-progress"
339
- }
608
+ updates = {"agent_assigned": agent, "status": "in-progress"}
340
609
  return self.batch_update(node_ids, updates)
341
610
 
342
611
  def start(self, node_id: str, agent: str | None = None) -> Node | None:
343
612
  """
344
613
  Start working on a node (feature/bug/etc).
345
614
 
346
- Delegates to SessionManager to:
615
+ Delegates to SessionManager if available for smart tracking:
347
616
  1. Check WIP limits
348
617
  2. Ensure not claimed by others
349
618
  3. Auto-claim for agent
350
619
  4. Link to active session
351
620
  5. Log 'FeatureStart' event
352
621
 
622
+ Falls back to simple status update if SessionManager not available.
623
+
353
624
  Args:
354
625
  node_id: Node ID to start
355
626
  agent: Agent ID (defaults to SDK agent)
356
627
 
357
628
  Returns:
358
- Updated Node
629
+ Updated Node, or None if not found
630
+
631
+ Raises:
632
+ NodeNotFoundError: If node not found
633
+
634
+ Example:
635
+ >>> sdk.features.start('feat-abc123')
636
+ >>> sdk.features.start('feat-xyz', agent='claude')
359
637
  """
360
638
  agent = agent or self._sdk.agent
361
-
639
+
362
640
  # Use SessionManager if available (smart tracking)
363
- if hasattr(self._sdk, 'session_manager'):
364
- return self._sdk.session_manager.start_feature(
365
- feature_id=node_id,
366
- collection=self._collection_name,
367
- agent=agent,
368
- log_activity=True
641
+ if hasattr(self._sdk, "session_manager"):
642
+ return cast(
643
+ "Node | None",
644
+ self._sdk.session_manager.start_feature(
645
+ feature_id=node_id,
646
+ collection=self._collection_name,
647
+ agent=agent,
648
+ log_activity=True,
649
+ ),
369
650
  )
370
-
651
+
371
652
  # Fallback to simple update (no session/events)
372
653
  node = self.get(node_id)
373
654
  if not node:
374
- raise ValueError(f"Node {node_id} not found")
375
-
655
+ raise NodeNotFoundError(self._node_type, node_id)
656
+
376
657
  node.status = "in-progress"
377
658
  node.updated = datetime.now()
378
659
  self._ensure_graph().update(node)
379
660
  return node
380
661
 
381
- def complete(self, node_id: str, agent: str | None = None) -> Node | None:
662
+ def complete(
663
+ self,
664
+ node_id: str,
665
+ agent: str | None = None,
666
+ transcript_id: str | None = None,
667
+ ) -> Node | None:
382
668
  """
383
- Complete a node.
669
+ Mark a node as complete.
384
670
 
385
- Delegates to SessionManager to:
386
- 1. Update status
671
+ Delegates to SessionManager if available for event logging and
672
+ transcript linking:
673
+ 1. Update status to 'done'
387
674
  2. Log 'FeatureComplete' event
388
675
  3. Release claim (optional behavior)
676
+ 4. Link transcript if provided (for parallel agent tracking)
677
+
678
+ Falls back to simple status update if SessionManager not available.
389
679
 
390
680
  Args:
391
681
  node_id: Node ID to complete
392
682
  agent: Agent ID (defaults to SDK agent)
683
+ transcript_id: Optional transcript ID (agent session) that implemented
684
+ this feature. Used for parallel agent tracking.
393
685
 
394
686
  Returns:
395
- Updated Node
687
+ Updated Node, or None if not found
688
+
689
+ Raises:
690
+ NodeNotFoundError: If node not found
691
+
692
+ Example:
693
+ >>> sdk.features.complete('feat-abc123')
694
+ >>> sdk.features.complete('feat-xyz', agent='claude', transcript_id='trans-123')
396
695
  """
397
696
  agent = agent or self._sdk.agent
398
697
 
399
698
  # Use SessionManager if available
400
- if hasattr(self._sdk, 'session_manager'):
401
- return self._sdk.session_manager.complete_feature(
402
- feature_id=node_id,
403
- collection=self._collection_name,
404
- agent=agent,
405
- log_activity=True
699
+ if hasattr(self._sdk, "session_manager"):
700
+ return cast(
701
+ "Node | None",
702
+ self._sdk.session_manager.complete_feature(
703
+ feature_id=node_id,
704
+ collection=self._collection_name,
705
+ agent=agent,
706
+ log_activity=True,
707
+ transcript_id=transcript_id,
708
+ ),
406
709
  )
407
710
 
408
711
  # Fallback
409
712
  node = self.get(node_id)
410
713
  if not node:
411
- raise ValueError(f"Node {node_id} not found")
714
+ raise NodeNotFoundError(self._node_type, node_id)
412
715
 
413
716
  node.status = "done"
414
717
  node.updated = datetime.now()
415
718
  self._ensure_graph().update(node)
416
719
  return node
417
720
 
418
- def claim(self, node_id: str, agent: str | None = None) -> Node:
721
+ def claim(self, node_id: str, agent: str | None = None) -> Node | None:
419
722
  """
420
723
  Claim a node for an agent.
421
724
 
422
- Delegates to SessionManager to:
725
+ Delegates to SessionManager if available for ownership tracking:
423
726
  1. Check ownership rules
424
727
  2. Update assignment
425
728
  3. Log 'FeatureClaim' event
426
729
 
730
+ Falls back to simple assignment if SessionManager not available.
731
+
427
732
  Args:
428
733
  node_id: Node ID to claim
429
734
  agent: Agent ID (defaults to SDK agent)
430
735
 
431
736
  Returns:
432
- The claimed Node
737
+ The claimed Node, or None if not found
433
738
 
434
739
  Raises:
435
740
  ValueError: If agent not provided and SDK has no agent
436
- ValueError: If node not found
437
- ValueError: If node already claimed by different agent
741
+ NodeNotFoundError: If node not found
742
+ ClaimConflictError: If node already claimed by different agent
743
+
744
+ Example:
745
+ >>> sdk.features.claim('feat-abc123')
746
+ >>> sdk.features.claim('feat-xyz', agent='claude')
438
747
  """
439
748
  agent = agent or self._sdk.agent
440
749
  if not agent:
441
750
  raise ValueError("Agent ID required for claiming")
442
751
 
443
752
  # Use SessionManager if available
444
- if hasattr(self._sdk, 'session_manager'):
445
- return self._sdk.session_manager.claim_feature(
446
- feature_id=node_id,
447
- collection=self._collection_name,
448
- agent=agent
753
+ if hasattr(self._sdk, "session_manager"):
754
+ return cast(
755
+ "Node | None",
756
+ self._sdk.session_manager.claim_feature(
757
+ feature_id=node_id, collection=self._collection_name, agent=agent
758
+ ),
449
759
  )
450
760
 
451
761
  # Fallback logic
452
762
  graph = self._ensure_graph()
453
- node = graph.get(node_id)
763
+ node = cast("Node | None", graph.get(node_id))
454
764
  if not node:
455
- raise ValueError(f"Node {node_id} not found")
765
+ raise NodeNotFoundError(self._node_type, node_id)
456
766
 
457
767
  if node.agent_assigned and node.agent_assigned != agent:
458
- raise ValueError(f"Node {node_id} is already claimed by {node.agent_assigned}")
768
+ raise ClaimConflictError(node_id, node.agent_assigned)
459
769
 
460
770
  node.agent_assigned = agent
461
771
  node.claimed_at = datetime.now()
@@ -464,41 +774,48 @@ class BaseCollection(Generic[CollectionT]):
464
774
  graph.update(node)
465
775
  return node
466
776
 
467
- def release(self, node_id: str, agent: str | None = None) -> Node:
777
+ def release(self, node_id: str, agent: str | None = None) -> Node | None:
468
778
  """
469
779
  Release a claimed node.
470
780
 
471
- Delegates to SessionManager to:
781
+ Delegates to SessionManager if available for ownership tracking:
472
782
  1. Verify ownership
473
783
  2. Clear assignment
474
784
  3. Log 'FeatureRelease' event
475
785
 
786
+ Falls back to simple assignment clearing if SessionManager not available.
787
+
476
788
  Args:
477
789
  node_id: Node ID to release
478
790
  agent: Agent ID (defaults to SDK agent)
479
791
 
480
792
  Returns:
481
- The released Node
793
+ The released Node, or None if not found
482
794
 
483
795
  Raises:
484
- ValueError: If node not found
796
+ NodeNotFoundError: If node not found
797
+
798
+ Example:
799
+ >>> sdk.features.release('feat-abc123')
800
+ >>> sdk.features.release('feat-xyz', agent='claude')
485
801
  """
486
802
  # SessionManager.release_feature requires an agent to verify ownership
487
803
  agent = agent or self._sdk.agent
488
-
804
+
489
805
  # Use SessionManager if available
490
- if hasattr(self._sdk, 'session_manager') and agent:
491
- return self._sdk.session_manager.release_feature(
492
- feature_id=node_id,
493
- collection=self._collection_name,
494
- agent=agent
806
+ if hasattr(self._sdk, "session_manager") and agent:
807
+ return cast(
808
+ "Node | None",
809
+ self._sdk.session_manager.release_feature(
810
+ feature_id=node_id, collection=self._collection_name, agent=agent
811
+ ),
495
812
  )
496
813
 
497
814
  # Fallback logic
498
815
  graph = self._ensure_graph()
499
- node = graph.get(node_id)
816
+ node = cast("Node | None", graph.get(node_id))
500
817
  if not node:
501
- raise ValueError(f"Node {node_id} not found")
818
+ raise NodeNotFoundError(self._node_type, node_id)
502
819
 
503
820
  node.agent_assigned = None
504
821
  node.claimed_at = None
@@ -506,4 +823,4 @@ class BaseCollection(Generic[CollectionT]):
506
823
  node.status = "todo"
507
824
  node.updated = datetime.now()
508
825
  graph.update(node)
509
- return node
826
+ return node