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,889 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from gobby.storage.database import DatabaseProtocol
|
|
7
|
+
from gobby.storage.tasks._aggregates import (
|
|
8
|
+
count_blocked_tasks as _count_blocked_tasks,
|
|
9
|
+
)
|
|
10
|
+
from gobby.storage.tasks._aggregates import (
|
|
11
|
+
count_by_status as _count_by_status,
|
|
12
|
+
)
|
|
13
|
+
from gobby.storage.tasks._aggregates import (
|
|
14
|
+
count_ready_tasks as _count_ready_tasks,
|
|
15
|
+
)
|
|
16
|
+
from gobby.storage.tasks._aggregates import (
|
|
17
|
+
count_tasks as _count_tasks,
|
|
18
|
+
)
|
|
19
|
+
from gobby.storage.tasks._crud import (
|
|
20
|
+
create_task as _create_task,
|
|
21
|
+
)
|
|
22
|
+
from gobby.storage.tasks._crud import (
|
|
23
|
+
find_task_by_prefix as _find_task_by_prefix,
|
|
24
|
+
)
|
|
25
|
+
from gobby.storage.tasks._crud import (
|
|
26
|
+
find_tasks_by_prefix as _find_tasks_by_prefix,
|
|
27
|
+
)
|
|
28
|
+
from gobby.storage.tasks._crud import (
|
|
29
|
+
get_task as _get_task,
|
|
30
|
+
)
|
|
31
|
+
from gobby.storage.tasks._crud import (
|
|
32
|
+
update_task as _update_task,
|
|
33
|
+
)
|
|
34
|
+
from gobby.storage.tasks._id import generate_task_id, resolve_task_reference
|
|
35
|
+
from gobby.storage.tasks._lifecycle import (
|
|
36
|
+
add_label as _add_label,
|
|
37
|
+
)
|
|
38
|
+
from gobby.storage.tasks._lifecycle import (
|
|
39
|
+
close_task as _close_task,
|
|
40
|
+
)
|
|
41
|
+
from gobby.storage.tasks._lifecycle import (
|
|
42
|
+
delete_task as _delete_task,
|
|
43
|
+
)
|
|
44
|
+
from gobby.storage.tasks._lifecycle import (
|
|
45
|
+
link_commit as _link_commit,
|
|
46
|
+
)
|
|
47
|
+
from gobby.storage.tasks._lifecycle import (
|
|
48
|
+
remove_label as _remove_label,
|
|
49
|
+
)
|
|
50
|
+
from gobby.storage.tasks._lifecycle import (
|
|
51
|
+
reopen_task as _reopen_task,
|
|
52
|
+
)
|
|
53
|
+
from gobby.storage.tasks._lifecycle import (
|
|
54
|
+
unlink_commit as _unlink_commit,
|
|
55
|
+
)
|
|
56
|
+
from gobby.storage.tasks._models import (
|
|
57
|
+
PRIORITY_MAP,
|
|
58
|
+
UNSET,
|
|
59
|
+
VALID_CATEGORIES,
|
|
60
|
+
Task,
|
|
61
|
+
TaskIDCollisionError,
|
|
62
|
+
TaskNotFoundError,
|
|
63
|
+
normalize_priority,
|
|
64
|
+
validate_category,
|
|
65
|
+
)
|
|
66
|
+
from gobby.storage.tasks._ordering import order_tasks_hierarchically
|
|
67
|
+
from gobby.storage.tasks._path_cache import (
|
|
68
|
+
compute_path_cache,
|
|
69
|
+
update_descendant_paths,
|
|
70
|
+
update_path_cache,
|
|
71
|
+
)
|
|
72
|
+
from gobby.storage.tasks._queries import (
|
|
73
|
+
list_blocked_tasks as _list_blocked_tasks,
|
|
74
|
+
)
|
|
75
|
+
from gobby.storage.tasks._queries import (
|
|
76
|
+
list_ready_tasks as _list_ready_tasks,
|
|
77
|
+
)
|
|
78
|
+
from gobby.storage.tasks._queries import (
|
|
79
|
+
list_tasks as _list_tasks,
|
|
80
|
+
)
|
|
81
|
+
from gobby.storage.tasks._queries import (
|
|
82
|
+
list_workflow_tasks as _list_workflow_tasks,
|
|
83
|
+
)
|
|
84
|
+
from gobby.storage.tasks._search import TaskSearcher
|
|
85
|
+
|
|
86
|
+
logger = logging.getLogger(__name__)
|
|
87
|
+
|
|
88
|
+
# Re-export for backward compatibility
|
|
89
|
+
__all__ = [
|
|
90
|
+
"PRIORITY_MAP",
|
|
91
|
+
"UNSET",
|
|
92
|
+
"VALID_CATEGORIES",
|
|
93
|
+
"Task",
|
|
94
|
+
"TaskIDCollisionError",
|
|
95
|
+
"TaskNotFoundError",
|
|
96
|
+
"normalize_priority",
|
|
97
|
+
"validate_category",
|
|
98
|
+
"generate_task_id",
|
|
99
|
+
"order_tasks_hierarchically",
|
|
100
|
+
"LocalTaskManager",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class LocalTaskManager:
|
|
105
|
+
def __init__(self, db: DatabaseProtocol):
|
|
106
|
+
self.db = db
|
|
107
|
+
self._change_listeners: list[Callable[[], Any]] = []
|
|
108
|
+
self._searcher: TaskSearcher | None = None
|
|
109
|
+
|
|
110
|
+
def add_change_listener(self, listener: Callable[[], Any]) -> None:
|
|
111
|
+
"""Add a listener to be called when tasks change."""
|
|
112
|
+
self._change_listeners.append(listener)
|
|
113
|
+
|
|
114
|
+
def _notify_listeners(self) -> None:
|
|
115
|
+
"""Notify all listeners of a change and mark search index dirty."""
|
|
116
|
+
# Mark search index as needing refit
|
|
117
|
+
if self._searcher is not None:
|
|
118
|
+
self._searcher.mark_dirty()
|
|
119
|
+
|
|
120
|
+
for listener in self._change_listeners:
|
|
121
|
+
try:
|
|
122
|
+
listener()
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error(f"Error in task change listener: {e}")
|
|
125
|
+
|
|
126
|
+
def compute_path_cache(self, task_id: str) -> str | None:
|
|
127
|
+
"""Compute the hierarchical path for a task.
|
|
128
|
+
|
|
129
|
+
Traverses up the parent chain to build a dotted path from seq_nums.
|
|
130
|
+
Format: 'ancestor_seq.parent_seq.task_seq' (e.g., '1.3.47')
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
task_id: The task ID to compute path for
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Dotted path string (e.g., '1.3.47'), or None if task not found
|
|
137
|
+
or any task in the chain is missing a seq_num.
|
|
138
|
+
"""
|
|
139
|
+
return compute_path_cache(self.db, task_id)
|
|
140
|
+
|
|
141
|
+
def update_path_cache(self, task_id: str) -> str | None:
|
|
142
|
+
"""Compute and store the path_cache for a task.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
task_id: The task ID to update
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
The computed path, or None if computation failed
|
|
149
|
+
"""
|
|
150
|
+
return update_path_cache(self.db, task_id)
|
|
151
|
+
|
|
152
|
+
def update_descendant_paths(self, task_id: str) -> int:
|
|
153
|
+
"""Update path_cache for a task and all its descendants.
|
|
154
|
+
|
|
155
|
+
Use this after reparenting a task to cascade path updates.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
task_id: The root task ID to start updating from
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Number of tasks updated
|
|
162
|
+
"""
|
|
163
|
+
return update_descendant_paths(self.db, task_id)
|
|
164
|
+
|
|
165
|
+
def create_task(
|
|
166
|
+
self,
|
|
167
|
+
project_id: str,
|
|
168
|
+
title: str,
|
|
169
|
+
description: str | None = None,
|
|
170
|
+
parent_task_id: str | None = None,
|
|
171
|
+
created_in_session_id: str | None = None,
|
|
172
|
+
priority: int = 2,
|
|
173
|
+
task_type: str = "task",
|
|
174
|
+
assignee: str | None = None,
|
|
175
|
+
labels: list[str] | None = None,
|
|
176
|
+
category: str | None = None,
|
|
177
|
+
complexity_score: int | None = None,
|
|
178
|
+
estimated_subtasks: int | None = None,
|
|
179
|
+
expansion_context: str | None = None,
|
|
180
|
+
validation_criteria: str | None = None,
|
|
181
|
+
use_external_validator: bool = False,
|
|
182
|
+
workflow_name: str | None = None,
|
|
183
|
+
verification: str | None = None,
|
|
184
|
+
sequence_order: int | None = None,
|
|
185
|
+
github_issue_number: int | None = None,
|
|
186
|
+
github_pr_number: int | None = None,
|
|
187
|
+
github_repo: str | None = None,
|
|
188
|
+
linear_issue_id: str | None = None,
|
|
189
|
+
linear_team_id: str | None = None,
|
|
190
|
+
agent_name: str | None = None,
|
|
191
|
+
reference_doc: str | None = None,
|
|
192
|
+
requires_user_review: bool = False,
|
|
193
|
+
) -> Task:
|
|
194
|
+
"""Create a new task with collision handling."""
|
|
195
|
+
task_id = _create_task(
|
|
196
|
+
self.db,
|
|
197
|
+
project_id=project_id,
|
|
198
|
+
title=title,
|
|
199
|
+
description=description,
|
|
200
|
+
parent_task_id=parent_task_id,
|
|
201
|
+
created_in_session_id=created_in_session_id,
|
|
202
|
+
priority=priority,
|
|
203
|
+
task_type=task_type,
|
|
204
|
+
assignee=assignee,
|
|
205
|
+
labels=labels,
|
|
206
|
+
category=category,
|
|
207
|
+
complexity_score=complexity_score,
|
|
208
|
+
estimated_subtasks=estimated_subtasks,
|
|
209
|
+
expansion_context=expansion_context,
|
|
210
|
+
validation_criteria=validation_criteria,
|
|
211
|
+
use_external_validator=use_external_validator,
|
|
212
|
+
workflow_name=workflow_name,
|
|
213
|
+
verification=verification,
|
|
214
|
+
sequence_order=sequence_order,
|
|
215
|
+
github_issue_number=github_issue_number,
|
|
216
|
+
github_pr_number=github_pr_number,
|
|
217
|
+
github_repo=github_repo,
|
|
218
|
+
linear_issue_id=linear_issue_id,
|
|
219
|
+
linear_team_id=linear_team_id,
|
|
220
|
+
agent_name=agent_name,
|
|
221
|
+
reference_doc=reference_doc,
|
|
222
|
+
requires_user_review=requires_user_review,
|
|
223
|
+
)
|
|
224
|
+
self._notify_listeners()
|
|
225
|
+
return self.get_task(task_id)
|
|
226
|
+
|
|
227
|
+
def get_task(self, task_id: str, project_id: str | None = None) -> Task:
|
|
228
|
+
"""Get a task by ID or reference.
|
|
229
|
+
|
|
230
|
+
Accepts multiple formats:
|
|
231
|
+
- UUID: Direct lookup
|
|
232
|
+
- #N: Project-scoped seq_num (requires project_id)
|
|
233
|
+
- N: Plain seq_num (requires project_id)
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
task_id: Task identifier in any supported format
|
|
237
|
+
project_id: Required for #N and N formats
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
The Task object
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
ValueError: If task not found or format requires project_id
|
|
244
|
+
"""
|
|
245
|
+
return _get_task(self.db, task_id, project_id)
|
|
246
|
+
|
|
247
|
+
def find_task_by_prefix(self, prefix: str) -> Task | None:
|
|
248
|
+
"""Find a task by ID prefix. Returns None if no match or multiple matches."""
|
|
249
|
+
return _find_task_by_prefix(self.db, prefix)
|
|
250
|
+
|
|
251
|
+
def find_tasks_by_prefix(self, prefix: str) -> list[Task]:
|
|
252
|
+
"""Find all tasks matching an ID prefix."""
|
|
253
|
+
return _find_tasks_by_prefix(self.db, prefix)
|
|
254
|
+
|
|
255
|
+
def resolve_task_reference(self, ref: str, project_id: str) -> str:
|
|
256
|
+
"""Resolve a task reference to its UUID.
|
|
257
|
+
|
|
258
|
+
Accepts multiple reference formats:
|
|
259
|
+
- N: Plain seq_num (e.g., 47)
|
|
260
|
+
- #N: Project-scoped seq_num (e.g., #47)
|
|
261
|
+
- 1.2.3: Path cache format
|
|
262
|
+
- UUID: Direct UUID (validated to exist)
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
ref: Task reference in any supported format
|
|
266
|
+
project_id: Project ID for scoped lookups
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
The task's UUID
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
TaskNotFoundError: If the reference cannot be resolved
|
|
273
|
+
"""
|
|
274
|
+
return resolve_task_reference(self.db, ref, project_id)
|
|
275
|
+
|
|
276
|
+
def update_task(
|
|
277
|
+
self,
|
|
278
|
+
task_id: str,
|
|
279
|
+
title: str | None | Any = UNSET,
|
|
280
|
+
description: str | None | Any = UNSET,
|
|
281
|
+
status: str | None | Any = UNSET,
|
|
282
|
+
priority: int | None | Any = UNSET,
|
|
283
|
+
task_type: str | None | Any = UNSET,
|
|
284
|
+
assignee: str | None | Any = UNSET,
|
|
285
|
+
labels: list[str] | None | Any = UNSET,
|
|
286
|
+
parent_task_id: str | None | Any = UNSET,
|
|
287
|
+
validation_status: str | None | Any = UNSET,
|
|
288
|
+
validation_feedback: str | None | Any = UNSET,
|
|
289
|
+
category: str | None | Any = UNSET,
|
|
290
|
+
complexity_score: int | None | Any = UNSET,
|
|
291
|
+
estimated_subtasks: int | None | Any = UNSET,
|
|
292
|
+
expansion_context: str | None | Any = UNSET,
|
|
293
|
+
validation_criteria: str | None | Any = UNSET,
|
|
294
|
+
use_external_validator: bool | None | Any = UNSET,
|
|
295
|
+
validation_fail_count: int | None | Any = UNSET,
|
|
296
|
+
workflow_name: str | None | Any = UNSET,
|
|
297
|
+
verification: str | None | Any = UNSET,
|
|
298
|
+
sequence_order: int | None | Any = UNSET,
|
|
299
|
+
escalated_at: str | None | Any = UNSET,
|
|
300
|
+
escalation_reason: str | None | Any = UNSET,
|
|
301
|
+
github_issue_number: int | None | Any = UNSET,
|
|
302
|
+
github_pr_number: int | None | Any = UNSET,
|
|
303
|
+
github_repo: str | None | Any = UNSET,
|
|
304
|
+
linear_issue_id: str | None | Any = UNSET,
|
|
305
|
+
linear_team_id: str | None | Any = UNSET,
|
|
306
|
+
agent_name: str | None | Any = UNSET,
|
|
307
|
+
reference_doc: str | None | Any = UNSET,
|
|
308
|
+
is_expanded: bool | None | Any = UNSET,
|
|
309
|
+
is_tdd_applied: bool | None | Any = UNSET,
|
|
310
|
+
validation_override_reason: str | None | Any = UNSET,
|
|
311
|
+
requires_user_review: bool | None | Any = UNSET,
|
|
312
|
+
) -> Task:
|
|
313
|
+
"""Update task fields."""
|
|
314
|
+
parent_changed = _update_task(
|
|
315
|
+
self.db,
|
|
316
|
+
task_id=task_id,
|
|
317
|
+
title=title,
|
|
318
|
+
description=description,
|
|
319
|
+
status=status,
|
|
320
|
+
priority=priority,
|
|
321
|
+
task_type=task_type,
|
|
322
|
+
assignee=assignee,
|
|
323
|
+
labels=labels,
|
|
324
|
+
parent_task_id=parent_task_id,
|
|
325
|
+
validation_status=validation_status,
|
|
326
|
+
validation_feedback=validation_feedback,
|
|
327
|
+
category=category,
|
|
328
|
+
complexity_score=complexity_score,
|
|
329
|
+
estimated_subtasks=estimated_subtasks,
|
|
330
|
+
expansion_context=expansion_context,
|
|
331
|
+
validation_criteria=validation_criteria,
|
|
332
|
+
use_external_validator=use_external_validator,
|
|
333
|
+
validation_fail_count=validation_fail_count,
|
|
334
|
+
workflow_name=workflow_name,
|
|
335
|
+
verification=verification,
|
|
336
|
+
sequence_order=sequence_order,
|
|
337
|
+
escalated_at=escalated_at,
|
|
338
|
+
escalation_reason=escalation_reason,
|
|
339
|
+
github_issue_number=github_issue_number,
|
|
340
|
+
github_pr_number=github_pr_number,
|
|
341
|
+
github_repo=github_repo,
|
|
342
|
+
linear_issue_id=linear_issue_id,
|
|
343
|
+
linear_team_id=linear_team_id,
|
|
344
|
+
agent_name=agent_name,
|
|
345
|
+
reference_doc=reference_doc,
|
|
346
|
+
is_expanded=is_expanded,
|
|
347
|
+
is_tdd_applied=is_tdd_applied,
|
|
348
|
+
validation_override_reason=validation_override_reason,
|
|
349
|
+
requires_user_review=requires_user_review,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# If parent_task_id was changed, update path_cache for this task and all descendants
|
|
353
|
+
if parent_changed:
|
|
354
|
+
self.update_descendant_paths(task_id)
|
|
355
|
+
|
|
356
|
+
self._notify_listeners()
|
|
357
|
+
return self.get_task(task_id)
|
|
358
|
+
|
|
359
|
+
def close_task(
|
|
360
|
+
self,
|
|
361
|
+
task_id: str,
|
|
362
|
+
reason: str | None = None,
|
|
363
|
+
force: bool = False,
|
|
364
|
+
closed_in_session_id: str | None = None,
|
|
365
|
+
closed_commit_sha: str | None = None,
|
|
366
|
+
validation_override_reason: str | None = None,
|
|
367
|
+
) -> Task:
|
|
368
|
+
"""Close a task.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
task_id: The task ID to close
|
|
372
|
+
reason: Optional reason for closing
|
|
373
|
+
force: If True, close even if there are open children (default: False)
|
|
374
|
+
closed_in_session_id: Session ID where task was closed
|
|
375
|
+
closed_commit_sha: Git commit SHA at time of closing
|
|
376
|
+
validation_override_reason: Why agent bypassed validation (if applicable)
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
ValueError: If task not found or has open children (and force=False)
|
|
380
|
+
"""
|
|
381
|
+
_close_task(
|
|
382
|
+
self.db,
|
|
383
|
+
task_id=task_id,
|
|
384
|
+
reason=reason,
|
|
385
|
+
force=force,
|
|
386
|
+
closed_in_session_id=closed_in_session_id,
|
|
387
|
+
closed_commit_sha=closed_commit_sha,
|
|
388
|
+
validation_override_reason=validation_override_reason,
|
|
389
|
+
)
|
|
390
|
+
self._notify_listeners()
|
|
391
|
+
return self.get_task(task_id)
|
|
392
|
+
|
|
393
|
+
def reopen_task(
|
|
394
|
+
self,
|
|
395
|
+
task_id: str,
|
|
396
|
+
reason: str | None = None,
|
|
397
|
+
) -> Task:
|
|
398
|
+
"""Reopen a closed or review task.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
task_id: The task ID to reopen
|
|
402
|
+
reason: Optional reason for reopening
|
|
403
|
+
|
|
404
|
+
Raises:
|
|
405
|
+
ValueError: If task not found or not closed/review
|
|
406
|
+
"""
|
|
407
|
+
_reopen_task(self.db, task_id=task_id, reason=reason)
|
|
408
|
+
self._notify_listeners()
|
|
409
|
+
return self.get_task(task_id)
|
|
410
|
+
|
|
411
|
+
def add_label(self, task_id: str, label: str) -> Task:
|
|
412
|
+
"""Add a label to a task if not present."""
|
|
413
|
+
result = _add_label(self.db, task_id, label)
|
|
414
|
+
self._notify_listeners()
|
|
415
|
+
return result
|
|
416
|
+
|
|
417
|
+
def remove_label(self, task_id: str, label: str) -> Task:
|
|
418
|
+
"""Remove a label from a task if present."""
|
|
419
|
+
result = _remove_label(self.db, task_id, label)
|
|
420
|
+
self._notify_listeners()
|
|
421
|
+
return result
|
|
422
|
+
|
|
423
|
+
def link_commit(self, task_id: str, commit_sha: str, cwd: str | Path | None = None) -> Task:
|
|
424
|
+
"""Link a commit SHA to a task.
|
|
425
|
+
|
|
426
|
+
Adds the commit SHA to the task's commits array if not already present.
|
|
427
|
+
The SHA is normalized to dynamic short format for consistency.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
task_id: The task ID to link the commit to.
|
|
431
|
+
commit_sha: The git commit SHA to link (short or full).
|
|
432
|
+
cwd: Working directory for git operations (defaults to current directory).
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Updated Task object.
|
|
436
|
+
|
|
437
|
+
Raises:
|
|
438
|
+
ValueError: If task not found or SHA cannot be resolved.
|
|
439
|
+
"""
|
|
440
|
+
if _link_commit(self.db, task_id, commit_sha, cwd):
|
|
441
|
+
self._notify_listeners()
|
|
442
|
+
return self.get_task(task_id)
|
|
443
|
+
|
|
444
|
+
def unlink_commit(self, task_id: str, commit_sha: str, cwd: str | Path | None = None) -> Task:
|
|
445
|
+
"""Unlink a commit SHA from a task.
|
|
446
|
+
|
|
447
|
+
Removes the commit SHA from the task's commits array if present.
|
|
448
|
+
Handles both normalized and legacy SHA formats via prefix matching.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
task_id: The task ID to unlink the commit from.
|
|
452
|
+
commit_sha: The git commit SHA to unlink (short or full).
|
|
453
|
+
cwd: Working directory for git operations (defaults to current directory).
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Updated Task object.
|
|
457
|
+
|
|
458
|
+
Raises:
|
|
459
|
+
ValueError: If task not found.
|
|
460
|
+
"""
|
|
461
|
+
if _unlink_commit(self.db, task_id, commit_sha, cwd):
|
|
462
|
+
self._notify_listeners()
|
|
463
|
+
return self.get_task(task_id)
|
|
464
|
+
|
|
465
|
+
def delete_task(self, task_id: str, cascade: bool = False) -> bool:
|
|
466
|
+
"""Delete a task. If cascade is True, delete children recursively.
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
True if task was deleted, False if task not found.
|
|
470
|
+
"""
|
|
471
|
+
result = _delete_task(self.db, task_id, cascade)
|
|
472
|
+
if result:
|
|
473
|
+
self._notify_listeners()
|
|
474
|
+
return result
|
|
475
|
+
|
|
476
|
+
def list_tasks(
|
|
477
|
+
self,
|
|
478
|
+
project_id: str | None = None,
|
|
479
|
+
status: str | list[str] | None = None,
|
|
480
|
+
priority: int | None = None,
|
|
481
|
+
assignee: str | None = None,
|
|
482
|
+
task_type: str | None = None,
|
|
483
|
+
label: str | None = None,
|
|
484
|
+
parent_task_id: str | None = None,
|
|
485
|
+
title_like: str | None = None,
|
|
486
|
+
limit: int = 50,
|
|
487
|
+
offset: int = 0,
|
|
488
|
+
) -> list[Task]:
|
|
489
|
+
"""List tasks with filtering.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
status: Filter by status. Can be a single status string, a list of statuses,
|
|
493
|
+
or None to include all statuses.
|
|
494
|
+
|
|
495
|
+
Results are ordered hierarchically: parents appear before their children,
|
|
496
|
+
with siblings sorted by priority ASC, then created_at ASC.
|
|
497
|
+
"""
|
|
498
|
+
return _list_tasks(
|
|
499
|
+
self.db,
|
|
500
|
+
project_id=project_id,
|
|
501
|
+
status=status,
|
|
502
|
+
priority=priority,
|
|
503
|
+
assignee=assignee,
|
|
504
|
+
task_type=task_type,
|
|
505
|
+
label=label,
|
|
506
|
+
parent_task_id=parent_task_id,
|
|
507
|
+
title_like=title_like,
|
|
508
|
+
limit=limit,
|
|
509
|
+
offset=offset,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
def list_ready_tasks(
|
|
513
|
+
self,
|
|
514
|
+
project_id: str | None = None,
|
|
515
|
+
priority: int | None = None,
|
|
516
|
+
task_type: str | None = None,
|
|
517
|
+
assignee: str | None = None,
|
|
518
|
+
parent_task_id: str | None = None,
|
|
519
|
+
limit: int = 50,
|
|
520
|
+
offset: int = 0,
|
|
521
|
+
) -> list[Task]:
|
|
522
|
+
"""List tasks that are ready to work on (open or in_progress) and not blocked.
|
|
523
|
+
|
|
524
|
+
A task is ready if:
|
|
525
|
+
1. It is open or in_progress
|
|
526
|
+
2. It has no open blocking dependencies
|
|
527
|
+
3. Its parent (if any) is also ready (recursive check up the chain)
|
|
528
|
+
|
|
529
|
+
Note: in_progress tasks are included because they represent active work
|
|
530
|
+
that should remain visible in the ready queue.
|
|
531
|
+
|
|
532
|
+
Results are ordered hierarchically: parents appear before their children,
|
|
533
|
+
with siblings sorted by priority ASC, then created_at ASC.
|
|
534
|
+
"""
|
|
535
|
+
return _list_ready_tasks(
|
|
536
|
+
self.db,
|
|
537
|
+
project_id=project_id,
|
|
538
|
+
priority=priority,
|
|
539
|
+
task_type=task_type,
|
|
540
|
+
assignee=assignee,
|
|
541
|
+
parent_task_id=parent_task_id,
|
|
542
|
+
limit=limit,
|
|
543
|
+
offset=offset,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
def list_blocked_tasks(
|
|
547
|
+
self,
|
|
548
|
+
project_id: str | None = None,
|
|
549
|
+
parent_task_id: str | None = None,
|
|
550
|
+
limit: int = 50,
|
|
551
|
+
offset: int = 0,
|
|
552
|
+
) -> list[Task]:
|
|
553
|
+
"""List tasks that are blocked by at least one open blocking dependency.
|
|
554
|
+
|
|
555
|
+
Only considers "external" blockers - excludes parent tasks being blocked
|
|
556
|
+
by their own descendants (which is a "completion" block, not a "work" block).
|
|
557
|
+
|
|
558
|
+
Results are ordered hierarchically: parents appear before their children,
|
|
559
|
+
with siblings sorted by priority ASC, then created_at ASC.
|
|
560
|
+
"""
|
|
561
|
+
return _list_blocked_tasks(
|
|
562
|
+
self.db,
|
|
563
|
+
project_id=project_id,
|
|
564
|
+
parent_task_id=parent_task_id,
|
|
565
|
+
limit=limit,
|
|
566
|
+
offset=offset,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
def list_workflow_tasks(
|
|
570
|
+
self,
|
|
571
|
+
workflow_name: str,
|
|
572
|
+
project_id: str | None = None,
|
|
573
|
+
status: str | None = None,
|
|
574
|
+
limit: int = 100,
|
|
575
|
+
offset: int = 0,
|
|
576
|
+
) -> list[Task]:
|
|
577
|
+
"""List tasks associated with a workflow, ordered by sequence_order.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
workflow_name: The workflow name to filter by
|
|
581
|
+
project_id: Optional project ID filter
|
|
582
|
+
status: Optional status filter ('open', 'in_progress', 'closed')
|
|
583
|
+
limit: Maximum tasks to return
|
|
584
|
+
offset: Pagination offset
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
List of tasks ordered by sequence_order (nulls last), then created_at
|
|
588
|
+
"""
|
|
589
|
+
return _list_workflow_tasks(
|
|
590
|
+
self.db,
|
|
591
|
+
workflow_name=workflow_name,
|
|
592
|
+
project_id=project_id,
|
|
593
|
+
status=status,
|
|
594
|
+
limit=limit,
|
|
595
|
+
offset=offset,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
def count_tasks(
|
|
599
|
+
self,
|
|
600
|
+
project_id: str | None = None,
|
|
601
|
+
status: str | None = None,
|
|
602
|
+
) -> int:
|
|
603
|
+
"""Count tasks with optional filters.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
project_id: Filter by project
|
|
607
|
+
status: Filter by status
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
Count of matching tasks
|
|
611
|
+
"""
|
|
612
|
+
return _count_tasks(self.db, project_id=project_id, status=status)
|
|
613
|
+
|
|
614
|
+
def count_by_status(self, project_id: str | None = None) -> dict[str, int]:
|
|
615
|
+
"""Count tasks grouped by status.
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
project_id: Optional project filter
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
Dictionary mapping status to count
|
|
622
|
+
"""
|
|
623
|
+
return _count_by_status(self.db, project_id=project_id)
|
|
624
|
+
|
|
625
|
+
def count_ready_tasks(self, project_id: str | None = None) -> int:
|
|
626
|
+
"""Count tasks that are ready (open or in_progress) and not blocked.
|
|
627
|
+
|
|
628
|
+
A task is ready if it has no external blocking dependencies.
|
|
629
|
+
Excludes parent tasks blocked by their own descendants (completion block, not work block).
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
project_id: Optional project filter
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
Count of ready tasks
|
|
636
|
+
"""
|
|
637
|
+
return _count_ready_tasks(self.db, project_id=project_id)
|
|
638
|
+
|
|
639
|
+
def count_blocked_tasks(self, project_id: str | None = None) -> int:
|
|
640
|
+
"""Count tasks that are blocked by at least one external blocking dependency.
|
|
641
|
+
|
|
642
|
+
Excludes parent tasks blocked by their own descendants (completion block, not work block).
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
project_id: Optional project filter
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
Count of blocked tasks
|
|
649
|
+
"""
|
|
650
|
+
return _count_blocked_tasks(self.db, project_id=project_id)
|
|
651
|
+
|
|
652
|
+
def create_task_with_decomposition(
|
|
653
|
+
self,
|
|
654
|
+
project_id: str,
|
|
655
|
+
title: str,
|
|
656
|
+
description: str | None = None,
|
|
657
|
+
parent_task_id: str | None = None,
|
|
658
|
+
created_in_session_id: str | None = None,
|
|
659
|
+
priority: int = 2,
|
|
660
|
+
task_type: str = "task",
|
|
661
|
+
assignee: str | None = None,
|
|
662
|
+
labels: list[str] | None = None,
|
|
663
|
+
category: str | None = None,
|
|
664
|
+
complexity_score: int | None = None,
|
|
665
|
+
estimated_subtasks: int | None = None,
|
|
666
|
+
expansion_context: str | None = None,
|
|
667
|
+
validation_criteria: str | None = None,
|
|
668
|
+
use_external_validator: bool = False,
|
|
669
|
+
workflow_name: str | None = None,
|
|
670
|
+
verification: str | None = None,
|
|
671
|
+
sequence_order: int | None = None,
|
|
672
|
+
) -> dict[str, Any]:
|
|
673
|
+
"""Create a task and return result dict.
|
|
674
|
+
|
|
675
|
+
Args:
|
|
676
|
+
project_id: Project ID
|
|
677
|
+
title: Task title
|
|
678
|
+
description: Task description
|
|
679
|
+
parent_task_id: Optional parent task ID
|
|
680
|
+
created_in_session_id: Session ID where task was created
|
|
681
|
+
priority: Task priority
|
|
682
|
+
task_type: Task type
|
|
683
|
+
assignee: Optional assignee
|
|
684
|
+
labels: Optional labels list
|
|
685
|
+
category: Task domain category
|
|
686
|
+
complexity_score: Complexity score
|
|
687
|
+
estimated_subtasks: Estimated number of subtasks
|
|
688
|
+
expansion_context: Additional context for expansion
|
|
689
|
+
validation_criteria: Validation criteria for completion
|
|
690
|
+
use_external_validator: Whether to use external validator
|
|
691
|
+
workflow_name: Workflow name
|
|
692
|
+
verification: Verification steps
|
|
693
|
+
sequence_order: Sequence order in parent
|
|
694
|
+
|
|
695
|
+
Returns:
|
|
696
|
+
Dict with task details.
|
|
697
|
+
"""
|
|
698
|
+
task = self.create_task(
|
|
699
|
+
project_id=project_id,
|
|
700
|
+
title=title,
|
|
701
|
+
description=description,
|
|
702
|
+
parent_task_id=parent_task_id,
|
|
703
|
+
created_in_session_id=created_in_session_id,
|
|
704
|
+
priority=priority,
|
|
705
|
+
task_type=task_type,
|
|
706
|
+
assignee=assignee,
|
|
707
|
+
labels=labels,
|
|
708
|
+
category=category,
|
|
709
|
+
complexity_score=complexity_score,
|
|
710
|
+
estimated_subtasks=estimated_subtasks,
|
|
711
|
+
expansion_context=expansion_context,
|
|
712
|
+
validation_criteria=validation_criteria,
|
|
713
|
+
use_external_validator=use_external_validator,
|
|
714
|
+
workflow_name=workflow_name,
|
|
715
|
+
verification=verification,
|
|
716
|
+
sequence_order=sequence_order,
|
|
717
|
+
)
|
|
718
|
+
return {"task": task.to_dict()}
|
|
719
|
+
|
|
720
|
+
def update_task_with_result(
|
|
721
|
+
self,
|
|
722
|
+
task_id: str,
|
|
723
|
+
description: str | None = None,
|
|
724
|
+
) -> dict[str, Any]:
|
|
725
|
+
"""Update a task's description and return result dict.
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
task_id: Task ID
|
|
729
|
+
description: New description
|
|
730
|
+
|
|
731
|
+
Returns:
|
|
732
|
+
Dict with task details.
|
|
733
|
+
"""
|
|
734
|
+
updated = self.update_task(task_id, description=description)
|
|
735
|
+
return {"task": updated.to_dict()}
|
|
736
|
+
|
|
737
|
+
# --- Search Methods ---
|
|
738
|
+
|
|
739
|
+
def _ensure_searcher(self) -> TaskSearcher:
|
|
740
|
+
"""Get or create the task searcher instance."""
|
|
741
|
+
if self._searcher is None:
|
|
742
|
+
self._searcher = TaskSearcher()
|
|
743
|
+
return self._searcher
|
|
744
|
+
|
|
745
|
+
def _ensure_search_fitted(self, project_id: str | None = None) -> None:
|
|
746
|
+
"""Ensure the search index is fitted with current tasks.
|
|
747
|
+
|
|
748
|
+
Note: The index is always built from ALL tasks (not project-scoped) to ensure
|
|
749
|
+
the index remains valid for searches against any project. Project filtering
|
|
750
|
+
is applied in search_tasks() after TF-IDF ranking.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
project_id: Unused - kept for API compatibility. Index always includes all tasks.
|
|
754
|
+
"""
|
|
755
|
+
_ = project_id # Unused - index is always global
|
|
756
|
+
searcher = self._ensure_searcher()
|
|
757
|
+
|
|
758
|
+
if not searcher.needs_refit():
|
|
759
|
+
return
|
|
760
|
+
|
|
761
|
+
# Always fetch ALL tasks to build a global index
|
|
762
|
+
# Project-scoped filtering happens in search_tasks() after ranking
|
|
763
|
+
index_limit = 10000
|
|
764
|
+
tasks = _list_tasks(
|
|
765
|
+
self.db,
|
|
766
|
+
project_id=None, # Always global
|
|
767
|
+
limit=index_limit,
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
if len(tasks) == index_limit:
|
|
771
|
+
logger.warning(
|
|
772
|
+
f"Task search index may be incomplete: fetched exactly {index_limit} tasks. "
|
|
773
|
+
"Consider increasing the index limit or implementing pagination."
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
searcher.fit(tasks)
|
|
777
|
+
logger.info(f"Task search index fitted with {len(tasks)} tasks")
|
|
778
|
+
|
|
779
|
+
def mark_search_refit_needed(self) -> None:
|
|
780
|
+
"""Mark that the search index needs to be rebuilt."""
|
|
781
|
+
if self._searcher is not None:
|
|
782
|
+
self._searcher.mark_dirty()
|
|
783
|
+
|
|
784
|
+
def search_tasks(
|
|
785
|
+
self,
|
|
786
|
+
query: str,
|
|
787
|
+
project_id: str | None = None,
|
|
788
|
+
status: str | list[str] | None = None,
|
|
789
|
+
task_type: str | None = None,
|
|
790
|
+
priority: int | None = None,
|
|
791
|
+
parent_task_id: str | None = None,
|
|
792
|
+
category: str | None = None,
|
|
793
|
+
limit: int = 20,
|
|
794
|
+
min_score: float = 0.0,
|
|
795
|
+
) -> list[tuple[Task, float]]:
|
|
796
|
+
"""Search tasks using TF-IDF semantic search.
|
|
797
|
+
|
|
798
|
+
Two-phase search: TF-IDF ranking first, then apply SQL filters.
|
|
799
|
+
|
|
800
|
+
Args:
|
|
801
|
+
query: Search query text
|
|
802
|
+
project_id: Filter by project
|
|
803
|
+
status: Filter by status (string or list of strings)
|
|
804
|
+
task_type: Filter by task type
|
|
805
|
+
priority: Filter by priority
|
|
806
|
+
parent_task_id: Filter by parent task ID (UUID)
|
|
807
|
+
category: Filter by task category
|
|
808
|
+
limit: Maximum results to return
|
|
809
|
+
min_score: Minimum similarity score threshold (0.0-1.0)
|
|
810
|
+
|
|
811
|
+
Returns:
|
|
812
|
+
List of (Task, similarity_score) tuples, sorted by score descending
|
|
813
|
+
"""
|
|
814
|
+
# Ensure the search index is fitted
|
|
815
|
+
self._ensure_search_fitted(project_id)
|
|
816
|
+
|
|
817
|
+
searcher = self._ensure_searcher()
|
|
818
|
+
|
|
819
|
+
# Phase 1: TF-IDF search to get candidate task IDs
|
|
820
|
+
# Get more candidates than limit to allow for filtering
|
|
821
|
+
search_results = searcher.search(query, top_k=limit * 3)
|
|
822
|
+
|
|
823
|
+
if not search_results:
|
|
824
|
+
return []
|
|
825
|
+
|
|
826
|
+
# Phase 2: Fetch tasks and apply filters
|
|
827
|
+
results: list[tuple[Task, float]] = []
|
|
828
|
+
|
|
829
|
+
for task_id, score in search_results:
|
|
830
|
+
if score < min_score:
|
|
831
|
+
continue
|
|
832
|
+
|
|
833
|
+
try:
|
|
834
|
+
task = self.get_task(task_id)
|
|
835
|
+
except (ValueError, TaskNotFoundError):
|
|
836
|
+
# Task may have been deleted since indexing
|
|
837
|
+
continue
|
|
838
|
+
|
|
839
|
+
# Apply filters
|
|
840
|
+
if project_id and task.project_id != project_id:
|
|
841
|
+
continue
|
|
842
|
+
|
|
843
|
+
if status:
|
|
844
|
+
if isinstance(status, list):
|
|
845
|
+
if task.status not in status:
|
|
846
|
+
continue
|
|
847
|
+
elif task.status != status:
|
|
848
|
+
continue
|
|
849
|
+
|
|
850
|
+
if task_type and task.task_type != task_type:
|
|
851
|
+
continue
|
|
852
|
+
|
|
853
|
+
if priority is not None and task.priority != priority:
|
|
854
|
+
continue
|
|
855
|
+
|
|
856
|
+
if parent_task_id and task.parent_task_id != parent_task_id:
|
|
857
|
+
continue
|
|
858
|
+
|
|
859
|
+
if category and task.category != category:
|
|
860
|
+
continue
|
|
861
|
+
|
|
862
|
+
results.append((task, score))
|
|
863
|
+
|
|
864
|
+
if len(results) >= limit:
|
|
865
|
+
break
|
|
866
|
+
|
|
867
|
+
return results
|
|
868
|
+
|
|
869
|
+
def reindex_search(self, project_id: str | None = None) -> dict[str, Any]:
|
|
870
|
+
"""Force rebuild of the task search index.
|
|
871
|
+
|
|
872
|
+
Note: The index is always global (includes all tasks). Project-scoped
|
|
873
|
+
filtering is applied at search time in search_tasks().
|
|
874
|
+
|
|
875
|
+
Args:
|
|
876
|
+
project_id: Unused - kept for API compatibility. Index always rebuilds globally.
|
|
877
|
+
|
|
878
|
+
Returns:
|
|
879
|
+
Dict with index statistics
|
|
880
|
+
"""
|
|
881
|
+
searcher = self._ensure_searcher()
|
|
882
|
+
|
|
883
|
+
# Force refit by marking dirty
|
|
884
|
+
searcher.mark_dirty()
|
|
885
|
+
|
|
886
|
+
# Ensure fitted will rebuild the index
|
|
887
|
+
self._ensure_search_fitted(project_id)
|
|
888
|
+
|
|
889
|
+
return searcher.get_stats()
|