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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +192 -100
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +190 -14
- htmlgraph/analytics_index.py +135 -51
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +45 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/orchestration/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
htmlgraph/collections/base.py
CHANGED
|
@@ -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
|
-
|
|
9
|
-
from
|
|
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.
|
|
23
|
+
from htmlgraph.graph import HtmlGraph
|
|
15
24
|
from htmlgraph.models import Node
|
|
25
|
+
from htmlgraph.sdk import SDK
|
|
16
26
|
|
|
17
|
-
CollectionT = TypeVar(
|
|
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__(
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
>>>
|
|
89
|
-
>>>
|
|
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=
|
|
104
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
382
|
+
if status and getattr(node, "status", None) != status:
|
|
187
383
|
return False
|
|
188
|
-
if priority and getattr(node,
|
|
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,
|
|
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
|
-
>>>
|
|
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]) ->
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
364
|
-
return
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
669
|
+
Mark a node as complete.
|
|
384
670
|
|
|
385
|
-
Delegates to SessionManager
|
|
386
|
-
|
|
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,
|
|
401
|
-
return
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
437
|
-
|
|
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,
|
|
445
|
-
return
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
|
765
|
+
raise NodeNotFoundError(self._node_type, node_id)
|
|
456
766
|
|
|
457
767
|
if node.agent_assigned and node.agent_assigned != agent:
|
|
458
|
-
raise
|
|
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
|
|
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
|
-
|
|
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,
|
|
491
|
-
return
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
|
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
|