gobby 0.2.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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sqlite3
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from gobby.storage.database import DatabaseProtocol
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
DependencyType = Literal["blocks", "related", "discovered-from"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class TaskDependency:
|
|
16
|
+
id: int
|
|
17
|
+
task_id: str
|
|
18
|
+
depends_on: str
|
|
19
|
+
dep_type: DependencyType
|
|
20
|
+
created_at: str
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_row(cls, row: sqlite3.Row) -> "TaskDependency":
|
|
24
|
+
return cls(
|
|
25
|
+
id=row["id"],
|
|
26
|
+
task_id=row["task_id"],
|
|
27
|
+
depends_on=row["depends_on"],
|
|
28
|
+
dep_type=row["dep_type"],
|
|
29
|
+
created_at=row["created_at"],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict[str, Any]:
|
|
33
|
+
"""Convert TaskDependency to dictionary."""
|
|
34
|
+
return {
|
|
35
|
+
"id": self.id,
|
|
36
|
+
"task_id": self.task_id,
|
|
37
|
+
"depends_on": self.depends_on,
|
|
38
|
+
"dep_type": self.dep_type,
|
|
39
|
+
"created_at": self.created_at,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DependencyCycleError(Exception):
|
|
44
|
+
"""Raised when a dependency cycle is detected."""
|
|
45
|
+
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TaskDependencyManager:
|
|
50
|
+
def __init__(self, db: DatabaseProtocol):
|
|
51
|
+
self.db = db
|
|
52
|
+
|
|
53
|
+
def add_dependency(
|
|
54
|
+
self, task_id: str, depends_on: str, dep_type: DependencyType = "blocks"
|
|
55
|
+
) -> TaskDependency:
|
|
56
|
+
"""Add a dependency."""
|
|
57
|
+
if task_id == depends_on:
|
|
58
|
+
raise ValueError("Task cannot depend on itself")
|
|
59
|
+
|
|
60
|
+
# For 'blocks', prevent cycles
|
|
61
|
+
if dep_type == "blocks" and self._would_create_cycle(task_id, depends_on):
|
|
62
|
+
raise DependencyCycleError(
|
|
63
|
+
f"Adding dependency {task_id} blocks {depends_on} would create a cycle"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
now = datetime.now(UTC).isoformat()
|
|
67
|
+
|
|
68
|
+
with self.db.transaction() as conn:
|
|
69
|
+
cursor = conn.execute(
|
|
70
|
+
"INSERT INTO task_dependencies (task_id, depends_on, dep_type, created_at) VALUES (?, ?, ?, ?)",
|
|
71
|
+
(task_id, depends_on, dep_type, now),
|
|
72
|
+
)
|
|
73
|
+
dep_id = cursor.lastrowid
|
|
74
|
+
|
|
75
|
+
if dep_id is None:
|
|
76
|
+
raise ValueError("Failed to retrieve dependency ID")
|
|
77
|
+
|
|
78
|
+
return TaskDependency(dep_id, task_id, depends_on, dep_type, now)
|
|
79
|
+
|
|
80
|
+
def remove_dependency(self, task_id: str, depends_on: str) -> bool:
|
|
81
|
+
"""Remove a dependency."""
|
|
82
|
+
with self.db.transaction() as conn:
|
|
83
|
+
cursor = conn.execute(
|
|
84
|
+
"DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?",
|
|
85
|
+
(task_id, depends_on),
|
|
86
|
+
)
|
|
87
|
+
deleted: bool = cursor.rowcount > 0
|
|
88
|
+
return deleted
|
|
89
|
+
|
|
90
|
+
def get_blockers(self, task_id: str) -> list[TaskDependency]:
|
|
91
|
+
"""Get tasks that block this task (task_id depends on X)."""
|
|
92
|
+
rows = self.db.fetchall(
|
|
93
|
+
"SELECT * FROM task_dependencies WHERE task_id = ? AND dep_type = 'blocks'",
|
|
94
|
+
(task_id,),
|
|
95
|
+
)
|
|
96
|
+
return [TaskDependency.from_row(row) for row in rows]
|
|
97
|
+
|
|
98
|
+
def get_blocking(self, task_id: str) -> list[TaskDependency]:
|
|
99
|
+
"""Get tasks that this task blocks (X depends on task_id)."""
|
|
100
|
+
rows = self.db.fetchall(
|
|
101
|
+
"SELECT * FROM task_dependencies WHERE depends_on = ? AND dep_type = 'blocks'",
|
|
102
|
+
(task_id,),
|
|
103
|
+
)
|
|
104
|
+
return [TaskDependency.from_row(row) for row in rows]
|
|
105
|
+
|
|
106
|
+
def get_all_dependencies(self, task_id: str) -> list[TaskDependency]:
|
|
107
|
+
"""Get all dependencies for a task (outgoing edges)."""
|
|
108
|
+
rows = self.db.fetchall(
|
|
109
|
+
"SELECT * FROM task_dependencies WHERE task_id = ?",
|
|
110
|
+
(task_id,),
|
|
111
|
+
)
|
|
112
|
+
return [TaskDependency.from_row(row) for row in rows]
|
|
113
|
+
|
|
114
|
+
def _would_create_cycle(self, task_id: str, depends_on: str) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Check if adding edge task_id -> depends_on creates a cycle.
|
|
117
|
+
This implies exists path depends_on -> ... -> task_id.
|
|
118
|
+
"""
|
|
119
|
+
visited = set()
|
|
120
|
+
stack = [depends_on]
|
|
121
|
+
|
|
122
|
+
while stack:
|
|
123
|
+
current = stack.pop()
|
|
124
|
+
if current == task_id:
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
if current in visited:
|
|
128
|
+
continue
|
|
129
|
+
visited.add(current)
|
|
130
|
+
|
|
131
|
+
deps = self.db.fetchall(
|
|
132
|
+
"SELECT depends_on FROM task_dependencies WHERE task_id = ? AND dep_type = 'blocks'",
|
|
133
|
+
(current,),
|
|
134
|
+
)
|
|
135
|
+
for row in deps:
|
|
136
|
+
stack.append(row["depends_on"])
|
|
137
|
+
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
def get_dependency_tree(
|
|
141
|
+
self,
|
|
142
|
+
task_id: str,
|
|
143
|
+
direction: Literal["blockers", "blocking", "both"] = "both",
|
|
144
|
+
max_depth: int = 10,
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
"""
|
|
147
|
+
Get dependency tree.
|
|
148
|
+
direction:
|
|
149
|
+
- blockers: tasks that task_id depends on (upstream)
|
|
150
|
+
- blocking: tasks that depend on task_id (downstream)
|
|
151
|
+
- both: both
|
|
152
|
+
"""
|
|
153
|
+
result: dict[str, Any] = {"id": task_id}
|
|
154
|
+
|
|
155
|
+
if max_depth <= 0:
|
|
156
|
+
result["_truncated"] = True
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
if direction in ("blockers", "both"):
|
|
160
|
+
blockers = self.get_blockers(task_id)
|
|
161
|
+
if blockers:
|
|
162
|
+
result["blockers"] = [
|
|
163
|
+
self.get_dependency_tree(
|
|
164
|
+
b.depends_on, direction="blockers", max_depth=max_depth - 1
|
|
165
|
+
)
|
|
166
|
+
for b in blockers
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
if direction in ("blocking", "both"):
|
|
170
|
+
blocking = self.get_blocking(task_id)
|
|
171
|
+
if blocking:
|
|
172
|
+
# blocking contains deps where depends_on = task_id. The other end is task_id.
|
|
173
|
+
# Use b.task_id (the task that is blocked).
|
|
174
|
+
result["blocking"] = [
|
|
175
|
+
self.get_dependency_tree(
|
|
176
|
+
b.task_id, direction="blocking", max_depth=max_depth - 1
|
|
177
|
+
)
|
|
178
|
+
for b in blocking
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
def check_cycles(self) -> list[list[str]]:
|
|
184
|
+
"""Detect all cycles in 'blocks' dependencies. Returns list of cycles (list of task IDs)."""
|
|
185
|
+
rows = self.db.fetchall(
|
|
186
|
+
"SELECT task_id, depends_on FROM task_dependencies WHERE dep_type = 'blocks'"
|
|
187
|
+
)
|
|
188
|
+
graph: dict[str, list[str]] = {}
|
|
189
|
+
for row in rows:
|
|
190
|
+
u, v = row["task_id"], row["depends_on"]
|
|
191
|
+
graph.setdefault(u, []).append(v)
|
|
192
|
+
graph.setdefault(v, [])
|
|
193
|
+
|
|
194
|
+
cycles = []
|
|
195
|
+
visited = set()
|
|
196
|
+
path = []
|
|
197
|
+
path_set = set()
|
|
198
|
+
|
|
199
|
+
def dfs(u: str) -> None:
|
|
200
|
+
visited.add(u)
|
|
201
|
+
path.append(u)
|
|
202
|
+
path_set.add(u)
|
|
203
|
+
|
|
204
|
+
for v in graph.get(u, []):
|
|
205
|
+
if v in path_set:
|
|
206
|
+
# Cycle found
|
|
207
|
+
# cycle is from v to ... to u to v
|
|
208
|
+
try:
|
|
209
|
+
idx = path.index(v)
|
|
210
|
+
cycles.append(path[idx:].copy())
|
|
211
|
+
except ValueError:
|
|
212
|
+
pass
|
|
213
|
+
elif v not in visited:
|
|
214
|
+
dfs(v)
|
|
215
|
+
|
|
216
|
+
path_set.remove(u)
|
|
217
|
+
path.pop()
|
|
218
|
+
|
|
219
|
+
for node in list(graph.keys()):
|
|
220
|
+
if node not in visited:
|
|
221
|
+
dfs(node)
|
|
222
|
+
|
|
223
|
+
return cycles
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Task storage module.
|
|
2
|
+
|
|
3
|
+
This package provides task management functionality including:
|
|
4
|
+
- Task dataclass and serialization
|
|
5
|
+
- LocalTaskManager for CRUD operations
|
|
6
|
+
- Task ID generation and resolution
|
|
7
|
+
- Hierarchical ordering utilities
|
|
8
|
+
|
|
9
|
+
All public symbols are re-exported for backward compatibility.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from gobby.storage.tasks._id import generate_task_id
|
|
13
|
+
from gobby.storage.tasks._manager import LocalTaskManager
|
|
14
|
+
from gobby.storage.tasks._models import (
|
|
15
|
+
PRIORITY_MAP,
|
|
16
|
+
UNSET,
|
|
17
|
+
VALID_CATEGORIES,
|
|
18
|
+
Task,
|
|
19
|
+
TaskIDCollisionError,
|
|
20
|
+
TaskNotFoundError,
|
|
21
|
+
normalize_priority,
|
|
22
|
+
validate_category,
|
|
23
|
+
)
|
|
24
|
+
from gobby.storage.tasks._ordering import order_tasks_hierarchically
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Core classes
|
|
28
|
+
"Task",
|
|
29
|
+
"LocalTaskManager",
|
|
30
|
+
# Exceptions
|
|
31
|
+
"TaskIDCollisionError",
|
|
32
|
+
"TaskNotFoundError",
|
|
33
|
+
# Functions
|
|
34
|
+
"generate_task_id",
|
|
35
|
+
"validate_category",
|
|
36
|
+
"normalize_priority",
|
|
37
|
+
"order_tasks_hierarchically",
|
|
38
|
+
# Constants
|
|
39
|
+
"PRIORITY_MAP",
|
|
40
|
+
"VALID_CATEGORIES",
|
|
41
|
+
"UNSET",
|
|
42
|
+
]
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Task aggregate operations.
|
|
2
|
+
|
|
3
|
+
This module provides aggregate operations for task counts and statistics:
|
|
4
|
+
- count_tasks: Count tasks with optional filters
|
|
5
|
+
- count_by_status: Count tasks grouped by status
|
|
6
|
+
- count_ready_tasks: Count tasks ready to work on
|
|
7
|
+
- count_blocked_tasks: Count tasks blocked by dependencies
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from gobby.storage.database import DatabaseProtocol
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def count_tasks(
|
|
16
|
+
db: DatabaseProtocol,
|
|
17
|
+
project_id: str | None = None,
|
|
18
|
+
status: str | None = None,
|
|
19
|
+
) -> int:
|
|
20
|
+
"""Count tasks with optional filters.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
db: Database protocol instance
|
|
24
|
+
project_id: Filter by project
|
|
25
|
+
status: Filter by status
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Count of matching tasks
|
|
29
|
+
"""
|
|
30
|
+
query = "SELECT COUNT(*) as count FROM tasks WHERE 1=1"
|
|
31
|
+
params: list[Any] = []
|
|
32
|
+
|
|
33
|
+
if project_id:
|
|
34
|
+
query += " AND project_id = ?"
|
|
35
|
+
params.append(project_id)
|
|
36
|
+
if status:
|
|
37
|
+
query += " AND status = ?"
|
|
38
|
+
params.append(status)
|
|
39
|
+
|
|
40
|
+
result = db.fetchone(query, tuple(params))
|
|
41
|
+
return result["count"] if result else 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def count_by_status(
|
|
45
|
+
db: DatabaseProtocol,
|
|
46
|
+
project_id: str | None = None,
|
|
47
|
+
) -> dict[str, int]:
|
|
48
|
+
"""Count tasks grouped by status.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
db: Database protocol instance
|
|
52
|
+
project_id: Optional project filter
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dictionary mapping status to count
|
|
56
|
+
"""
|
|
57
|
+
query = "SELECT status, COUNT(*) as count FROM tasks"
|
|
58
|
+
params: list[Any] = []
|
|
59
|
+
|
|
60
|
+
if project_id:
|
|
61
|
+
query += " WHERE project_id = ?"
|
|
62
|
+
params.append(project_id)
|
|
63
|
+
|
|
64
|
+
query += " GROUP BY status"
|
|
65
|
+
|
|
66
|
+
rows = db.fetchall(query, tuple(params))
|
|
67
|
+
return {row["status"]: row["count"] for row in rows}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def count_ready_tasks(
|
|
71
|
+
db: DatabaseProtocol,
|
|
72
|
+
project_id: str | None = None,
|
|
73
|
+
) -> int:
|
|
74
|
+
"""Count tasks that are ready (open or in_progress) and not blocked.
|
|
75
|
+
|
|
76
|
+
A task is ready if it has no external blocking dependencies.
|
|
77
|
+
Excludes parent tasks blocked by their own descendants (completion block, not work block).
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
db: Database protocol instance
|
|
81
|
+
project_id: Optional project filter
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Count of ready tasks
|
|
85
|
+
"""
|
|
86
|
+
# Uses the same descendant-aware predicate as list_ready_tasks.
|
|
87
|
+
# The is_descendant_of check uses a recursive CTE to walk up the blocker's
|
|
88
|
+
# ancestor chain and check if the blocked task (t.id) appears anywhere.
|
|
89
|
+
query = """
|
|
90
|
+
SELECT COUNT(*) as count FROM tasks t
|
|
91
|
+
WHERE t.status IN ('open', 'in_progress')
|
|
92
|
+
AND NOT EXISTS (
|
|
93
|
+
SELECT 1 FROM task_dependencies d
|
|
94
|
+
JOIN tasks blocker ON d.depends_on = blocker.id
|
|
95
|
+
WHERE d.task_id = t.id
|
|
96
|
+
AND d.dep_type = 'blocks'
|
|
97
|
+
-- Blocker is unresolved if not closed AND not in review without requiring user review
|
|
98
|
+
AND NOT (
|
|
99
|
+
blocker.status = 'closed'
|
|
100
|
+
OR (blocker.status = 'review' AND blocker.requires_user_review = 0)
|
|
101
|
+
)
|
|
102
|
+
-- Exclude ancestor blocked by any descendant (completion block, not work block)
|
|
103
|
+
-- Check if t.id appears anywhere in blocker's ancestor chain
|
|
104
|
+
AND NOT EXISTS (
|
|
105
|
+
WITH RECURSIVE ancestors AS (
|
|
106
|
+
SELECT blocker.parent_task_id AS ancestor_id
|
|
107
|
+
UNION ALL
|
|
108
|
+
SELECT p.parent_task_id
|
|
109
|
+
FROM tasks p
|
|
110
|
+
JOIN ancestors a ON p.id = a.ancestor_id
|
|
111
|
+
WHERE p.parent_task_id IS NOT NULL
|
|
112
|
+
)
|
|
113
|
+
SELECT 1 FROM ancestors WHERE ancestor_id = t.id
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
"""
|
|
117
|
+
params: list[Any] = []
|
|
118
|
+
|
|
119
|
+
if project_id:
|
|
120
|
+
query += " AND t.project_id = ?"
|
|
121
|
+
params.append(project_id)
|
|
122
|
+
|
|
123
|
+
result = db.fetchone(query, tuple(params))
|
|
124
|
+
return result["count"] if result else 0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def count_blocked_tasks(
|
|
128
|
+
db: DatabaseProtocol,
|
|
129
|
+
project_id: str | None = None,
|
|
130
|
+
) -> int:
|
|
131
|
+
"""Count tasks that are blocked by at least one external blocking dependency.
|
|
132
|
+
|
|
133
|
+
Excludes parent tasks blocked by their own descendants (completion block, not work block).
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
db: Database protocol instance
|
|
137
|
+
project_id: Optional project filter
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Count of blocked tasks
|
|
141
|
+
"""
|
|
142
|
+
# Uses the same descendant-aware predicate as list_ready_tasks.
|
|
143
|
+
# The is_descendant_of check uses a recursive CTE to walk up the blocker's
|
|
144
|
+
# ancestor chain and check if the blocked task (t.id) appears anywhere.
|
|
145
|
+
query = """
|
|
146
|
+
SELECT COUNT(*) as count FROM tasks t
|
|
147
|
+
WHERE t.status = 'open'
|
|
148
|
+
AND EXISTS (
|
|
149
|
+
SELECT 1 FROM task_dependencies d
|
|
150
|
+
JOIN tasks blocker ON d.depends_on = blocker.id
|
|
151
|
+
WHERE d.task_id = t.id
|
|
152
|
+
AND d.dep_type = 'blocks'
|
|
153
|
+
-- Blocker is unresolved if not closed AND not in review without requiring user review
|
|
154
|
+
AND NOT (
|
|
155
|
+
blocker.status = 'closed'
|
|
156
|
+
OR (blocker.status = 'review' AND blocker.requires_user_review = 0)
|
|
157
|
+
)
|
|
158
|
+
-- Exclude ancestor blocked by any descendant (completion block, not work block)
|
|
159
|
+
-- Check if t.id appears anywhere in blocker's ancestor chain
|
|
160
|
+
AND NOT EXISTS (
|
|
161
|
+
WITH RECURSIVE ancestors AS (
|
|
162
|
+
SELECT blocker.parent_task_id AS ancestor_id
|
|
163
|
+
UNION ALL
|
|
164
|
+
SELECT p.parent_task_id
|
|
165
|
+
FROM tasks p
|
|
166
|
+
JOIN ancestors a ON p.id = a.ancestor_id
|
|
167
|
+
WHERE p.parent_task_id IS NOT NULL
|
|
168
|
+
)
|
|
169
|
+
SELECT 1 FROM ancestors WHERE ancestor_id = t.id
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
"""
|
|
173
|
+
params: list[Any] = []
|
|
174
|
+
|
|
175
|
+
if project_id:
|
|
176
|
+
query += " AND t.project_id = ?"
|
|
177
|
+
params.append(project_id)
|
|
178
|
+
|
|
179
|
+
result = db.fetchone(query, tuple(params))
|
|
180
|
+
return result["count"] if result else 0
|