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,343 @@
|
|
|
1
|
+
"""Task query operations.
|
|
2
|
+
|
|
3
|
+
This module provides query operations for listing and filtering tasks:
|
|
4
|
+
- list_tasks: General task listing with filters
|
|
5
|
+
- list_ready_tasks: Tasks ready to work on (not blocked)
|
|
6
|
+
- list_blocked_tasks: Tasks blocked by dependencies
|
|
7
|
+
- list_workflow_tasks: Tasks associated with a workflow
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from gobby.storage.database import DatabaseProtocol
|
|
13
|
+
from gobby.storage.tasks._models import Task
|
|
14
|
+
from gobby.storage.tasks._ordering import order_tasks_hierarchically
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def list_tasks(
|
|
18
|
+
db: DatabaseProtocol,
|
|
19
|
+
project_id: str | None = None,
|
|
20
|
+
status: str | list[str] | None = None,
|
|
21
|
+
priority: int | None = None,
|
|
22
|
+
assignee: str | None = None,
|
|
23
|
+
task_type: str | None = None,
|
|
24
|
+
label: str | None = None,
|
|
25
|
+
parent_task_id: str | None = None,
|
|
26
|
+
title_like: str | None = None,
|
|
27
|
+
limit: int = 50,
|
|
28
|
+
offset: int = 0,
|
|
29
|
+
) -> list[Task]:
|
|
30
|
+
"""List tasks with filtering.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
db: Database protocol instance
|
|
34
|
+
project_id: Filter by project
|
|
35
|
+
status: Filter by status. Can be a single status string, a list of statuses,
|
|
36
|
+
or None to include all statuses.
|
|
37
|
+
priority: Filter by priority
|
|
38
|
+
assignee: Filter by assignee
|
|
39
|
+
task_type: Filter by task type
|
|
40
|
+
label: Filter by label
|
|
41
|
+
parent_task_id: Filter by parent task
|
|
42
|
+
title_like: Filter by title (partial match)
|
|
43
|
+
limit: Maximum tasks to return
|
|
44
|
+
offset: Pagination offset
|
|
45
|
+
|
|
46
|
+
Results are ordered hierarchically: parents appear before their children,
|
|
47
|
+
with siblings sorted by priority ASC, then created_at ASC.
|
|
48
|
+
"""
|
|
49
|
+
query = "SELECT * FROM tasks WHERE 1=1"
|
|
50
|
+
params: list[Any] = []
|
|
51
|
+
|
|
52
|
+
if project_id:
|
|
53
|
+
query += " AND project_id = ?"
|
|
54
|
+
params.append(project_id)
|
|
55
|
+
if status:
|
|
56
|
+
if isinstance(status, list):
|
|
57
|
+
placeholders = ", ".join("?" for _ in status)
|
|
58
|
+
query += f" AND status IN ({placeholders})"
|
|
59
|
+
params.extend(status)
|
|
60
|
+
else:
|
|
61
|
+
query += " AND status = ?"
|
|
62
|
+
params.append(status)
|
|
63
|
+
if priority:
|
|
64
|
+
query += " AND priority = ?"
|
|
65
|
+
params.append(priority)
|
|
66
|
+
if assignee:
|
|
67
|
+
query += " AND assignee = ?"
|
|
68
|
+
params.append(assignee)
|
|
69
|
+
if task_type:
|
|
70
|
+
query += " AND task_type = ?"
|
|
71
|
+
params.append(task_type)
|
|
72
|
+
if label:
|
|
73
|
+
# tasks.labels is a JSON list. We use json_each to find if the label is in the list.
|
|
74
|
+
query += " AND EXISTS (SELECT 1 FROM json_each(tasks.labels) WHERE value = ?)"
|
|
75
|
+
params.append(label)
|
|
76
|
+
if parent_task_id:
|
|
77
|
+
query += " AND parent_task_id = ?"
|
|
78
|
+
params.append(parent_task_id)
|
|
79
|
+
if title_like:
|
|
80
|
+
query += " AND title LIKE ?"
|
|
81
|
+
params.append(f"%{title_like}%")
|
|
82
|
+
|
|
83
|
+
# Fetch with base ordering, then apply hierarchical sort in Python
|
|
84
|
+
query += " ORDER BY priority ASC, created_at ASC LIMIT ? OFFSET ?"
|
|
85
|
+
params.extend([limit, offset])
|
|
86
|
+
|
|
87
|
+
rows = db.fetchall(query, tuple(params))
|
|
88
|
+
tasks = [Task.from_row(row) for row in rows]
|
|
89
|
+
|
|
90
|
+
# Bulk fetch dependencies for these tasks to support topological sort
|
|
91
|
+
if tasks:
|
|
92
|
+
task_ids = [t.id for t in tasks]
|
|
93
|
+
placeholders = ", ".join("?" for _ in task_ids)
|
|
94
|
+
# nosec B608: placeholders are just '?' characters, values parameterized
|
|
95
|
+
dep_rows = db.fetchall(
|
|
96
|
+
f"SELECT task_id, depends_on FROM task_dependencies WHERE dep_type = 'blocks' AND task_id IN ({placeholders})", # nosec B608
|
|
97
|
+
tuple(task_ids),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Map by task_id -> set of blockers
|
|
101
|
+
blockers_map: dict[str, set[str]] = {}
|
|
102
|
+
for row in dep_rows:
|
|
103
|
+
tid = row["task_id"]
|
|
104
|
+
blocker = row["depends_on"]
|
|
105
|
+
if tid not in blockers_map:
|
|
106
|
+
blockers_map[tid] = set()
|
|
107
|
+
blockers_map[tid].add(blocker)
|
|
108
|
+
|
|
109
|
+
# Populate task objects
|
|
110
|
+
for task in tasks:
|
|
111
|
+
if task.id in blockers_map:
|
|
112
|
+
task.blocked_by = blockers_map[task.id]
|
|
113
|
+
|
|
114
|
+
return order_tasks_hierarchically(tasks)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def list_ready_tasks(
|
|
118
|
+
db: DatabaseProtocol,
|
|
119
|
+
project_id: str | None = None,
|
|
120
|
+
priority: int | None = None,
|
|
121
|
+
task_type: str | None = None,
|
|
122
|
+
assignee: str | None = None,
|
|
123
|
+
parent_task_id: str | None = None,
|
|
124
|
+
limit: int = 50,
|
|
125
|
+
offset: int = 0,
|
|
126
|
+
) -> list[Task]:
|
|
127
|
+
"""List tasks that are ready to work on (open or in_progress) and not blocked.
|
|
128
|
+
|
|
129
|
+
A task is ready if:
|
|
130
|
+
1. It is open or in_progress
|
|
131
|
+
2. It has no open blocking dependencies
|
|
132
|
+
3. Its parent (if any) is also ready (recursive check up the chain)
|
|
133
|
+
|
|
134
|
+
Note: in_progress tasks are included because they represent active work
|
|
135
|
+
that should remain visible in the ready queue.
|
|
136
|
+
|
|
137
|
+
Results are ordered hierarchically: parents appear before their children,
|
|
138
|
+
with siblings sorted by priority ASC, then created_at ASC.
|
|
139
|
+
|
|
140
|
+
Note: The limit is applied AFTER hierarchical ordering to ensure coherent
|
|
141
|
+
tree structures. We fetch all ready tasks, order them hierarchically,
|
|
142
|
+
then return the first N tasks in tree traversal order.
|
|
143
|
+
"""
|
|
144
|
+
# Use recursive CTE to find tasks with ready parent chains
|
|
145
|
+
query = """
|
|
146
|
+
WITH RECURSIVE ready_tasks AS (
|
|
147
|
+
-- Base case: open/in_progress tasks with no parent and no external blocking deps
|
|
148
|
+
SELECT t.id FROM tasks t
|
|
149
|
+
WHERE t.status IN ('open', 'in_progress')
|
|
150
|
+
AND t.parent_task_id IS NULL
|
|
151
|
+
AND NOT EXISTS (
|
|
152
|
+
SELECT 1 FROM task_dependencies d
|
|
153
|
+
JOIN tasks blocker ON d.depends_on = blocker.id
|
|
154
|
+
WHERE d.task_id = t.id
|
|
155
|
+
AND d.dep_type = 'blocks'
|
|
156
|
+
-- Blocker is unresolved if not closed AND not in review without requiring user review
|
|
157
|
+
AND NOT (
|
|
158
|
+
blocker.status = 'closed'
|
|
159
|
+
OR (blocker.status = 'review' AND blocker.requires_user_review = 0)
|
|
160
|
+
)
|
|
161
|
+
-- Exclude ancestor blocked by any descendant (completion block, not work block)
|
|
162
|
+
AND NOT EXISTS (
|
|
163
|
+
WITH RECURSIVE ancestors AS (
|
|
164
|
+
SELECT blocker.parent_task_id AS ancestor_id
|
|
165
|
+
UNION ALL
|
|
166
|
+
SELECT p.parent_task_id
|
|
167
|
+
FROM tasks p
|
|
168
|
+
JOIN ancestors a ON p.id = a.ancestor_id
|
|
169
|
+
WHERE p.parent_task_id IS NOT NULL
|
|
170
|
+
)
|
|
171
|
+
SELECT 1 FROM ancestors WHERE ancestor_id = t.id
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
UNION ALL
|
|
176
|
+
|
|
177
|
+
-- Recursive case: open/in_progress tasks whose parent is ready and no external blocking deps
|
|
178
|
+
SELECT t.id FROM tasks t
|
|
179
|
+
JOIN ready_tasks rt ON t.parent_task_id = rt.id
|
|
180
|
+
WHERE t.status IN ('open', 'in_progress')
|
|
181
|
+
AND NOT EXISTS (
|
|
182
|
+
SELECT 1 FROM task_dependencies d
|
|
183
|
+
JOIN tasks blocker ON d.depends_on = blocker.id
|
|
184
|
+
WHERE d.task_id = t.id
|
|
185
|
+
AND d.dep_type = 'blocks'
|
|
186
|
+
-- Blocker is unresolved if not closed AND not in review without requiring user review
|
|
187
|
+
AND NOT (
|
|
188
|
+
blocker.status = 'closed'
|
|
189
|
+
OR (blocker.status = 'review' AND blocker.requires_user_review = 0)
|
|
190
|
+
)
|
|
191
|
+
-- Exclude ancestor blocked by any descendant (completion block, not work block)
|
|
192
|
+
AND NOT EXISTS (
|
|
193
|
+
WITH RECURSIVE ancestors AS (
|
|
194
|
+
SELECT blocker.parent_task_id AS ancestor_id
|
|
195
|
+
UNION ALL
|
|
196
|
+
SELECT p.parent_task_id
|
|
197
|
+
FROM tasks p
|
|
198
|
+
JOIN ancestors a ON p.id = a.ancestor_id
|
|
199
|
+
WHERE p.parent_task_id IS NOT NULL
|
|
200
|
+
)
|
|
201
|
+
SELECT 1 FROM ancestors WHERE ancestor_id = t.id
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
SELECT t.* FROM tasks t
|
|
206
|
+
JOIN ready_tasks rt ON t.id = rt.id
|
|
207
|
+
WHERE 1=1
|
|
208
|
+
"""
|
|
209
|
+
params: list[Any] = []
|
|
210
|
+
|
|
211
|
+
if project_id:
|
|
212
|
+
query += " AND t.project_id = ?"
|
|
213
|
+
params.append(project_id)
|
|
214
|
+
if priority:
|
|
215
|
+
query += " AND t.priority = ?"
|
|
216
|
+
params.append(priority)
|
|
217
|
+
if task_type:
|
|
218
|
+
query += " AND t.task_type = ?"
|
|
219
|
+
params.append(task_type)
|
|
220
|
+
if assignee:
|
|
221
|
+
query += " AND t.assignee = ?"
|
|
222
|
+
params.append(assignee)
|
|
223
|
+
if parent_task_id:
|
|
224
|
+
query += " AND t.parent_task_id = ?"
|
|
225
|
+
params.append(parent_task_id)
|
|
226
|
+
|
|
227
|
+
# Fetch all matching tasks (no SQL limit) so we can order hierarchically first
|
|
228
|
+
internal_limit = 1000
|
|
229
|
+
query += " ORDER BY t.priority ASC, t.created_at ASC LIMIT ?"
|
|
230
|
+
params.append(internal_limit)
|
|
231
|
+
|
|
232
|
+
rows = db.fetchall(query, tuple(params))
|
|
233
|
+
tasks = [Task.from_row(row) for row in rows]
|
|
234
|
+
|
|
235
|
+
# Order hierarchically, then apply user's limit/offset
|
|
236
|
+
ordered = order_tasks_hierarchically(tasks)
|
|
237
|
+
return ordered[offset : offset + limit] if limit else ordered
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def list_blocked_tasks(
|
|
241
|
+
db: DatabaseProtocol,
|
|
242
|
+
project_id: str | None = None,
|
|
243
|
+
parent_task_id: str | None = None,
|
|
244
|
+
limit: int = 50,
|
|
245
|
+
offset: int = 0,
|
|
246
|
+
) -> list[Task]:
|
|
247
|
+
"""List tasks that are blocked by at least one open blocking dependency.
|
|
248
|
+
|
|
249
|
+
Only considers "external" blockers - excludes parent tasks being blocked
|
|
250
|
+
by their own descendants (which is a "completion" block, not a "work" block).
|
|
251
|
+
|
|
252
|
+
Results are ordered hierarchically: parents appear before their children,
|
|
253
|
+
with siblings sorted by priority ASC, then created_at ASC.
|
|
254
|
+
|
|
255
|
+
Note: The limit is applied AFTER hierarchical ordering to ensure coherent
|
|
256
|
+
tree structures.
|
|
257
|
+
"""
|
|
258
|
+
query = """
|
|
259
|
+
SELECT t.* FROM tasks t
|
|
260
|
+
WHERE t.status = 'open'
|
|
261
|
+
AND EXISTS (
|
|
262
|
+
SELECT 1 FROM task_dependencies d
|
|
263
|
+
JOIN tasks blocker ON d.depends_on = blocker.id
|
|
264
|
+
WHERE d.task_id = t.id
|
|
265
|
+
AND d.dep_type = 'blocks'
|
|
266
|
+
-- Blocker is unresolved if not closed AND not in review without requiring user review
|
|
267
|
+
AND NOT (
|
|
268
|
+
blocker.status = 'closed'
|
|
269
|
+
OR (blocker.status = 'review' AND blocker.requires_user_review = 0)
|
|
270
|
+
)
|
|
271
|
+
-- Exclude ancestor blocked by any descendant (completion block, not work block)
|
|
272
|
+
AND NOT EXISTS (
|
|
273
|
+
WITH RECURSIVE ancestors AS (
|
|
274
|
+
SELECT blocker.parent_task_id AS ancestor_id
|
|
275
|
+
UNION ALL
|
|
276
|
+
SELECT p.parent_task_id
|
|
277
|
+
FROM tasks p
|
|
278
|
+
JOIN ancestors a ON p.id = a.ancestor_id
|
|
279
|
+
WHERE p.parent_task_id IS NOT NULL
|
|
280
|
+
)
|
|
281
|
+
SELECT 1 FROM ancestors WHERE ancestor_id = t.id
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
"""
|
|
285
|
+
params: list[Any] = []
|
|
286
|
+
|
|
287
|
+
if project_id:
|
|
288
|
+
query += " AND t.project_id = ?"
|
|
289
|
+
params.append(project_id)
|
|
290
|
+
if parent_task_id:
|
|
291
|
+
query += " AND t.parent_task_id = ?"
|
|
292
|
+
params.append(parent_task_id)
|
|
293
|
+
|
|
294
|
+
# Fetch all matching tasks (no SQL limit) so we can order hierarchically first
|
|
295
|
+
internal_limit = 1000
|
|
296
|
+
query += " ORDER BY t.priority ASC, t.created_at ASC LIMIT ?"
|
|
297
|
+
params.append(internal_limit)
|
|
298
|
+
|
|
299
|
+
rows = db.fetchall(query, tuple(params))
|
|
300
|
+
tasks = [Task.from_row(row) for row in rows]
|
|
301
|
+
|
|
302
|
+
# Order hierarchically, then apply user's limit/offset
|
|
303
|
+
ordered = order_tasks_hierarchically(tasks)
|
|
304
|
+
return ordered[offset : offset + limit] if limit else ordered
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def list_workflow_tasks(
|
|
308
|
+
db: DatabaseProtocol,
|
|
309
|
+
workflow_name: str,
|
|
310
|
+
project_id: str | None = None,
|
|
311
|
+
status: str | None = None,
|
|
312
|
+
limit: int = 100,
|
|
313
|
+
offset: int = 0,
|
|
314
|
+
) -> list[Task]:
|
|
315
|
+
"""List tasks associated with a workflow, ordered by sequence_order.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
db: Database protocol instance
|
|
319
|
+
workflow_name: The workflow name to filter by
|
|
320
|
+
project_id: Optional project ID filter
|
|
321
|
+
status: Optional status filter ('open', 'in_progress', 'closed')
|
|
322
|
+
limit: Maximum tasks to return
|
|
323
|
+
offset: Pagination offset
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
List of tasks ordered by sequence_order (nulls last), then created_at
|
|
327
|
+
"""
|
|
328
|
+
query = "SELECT * FROM tasks WHERE workflow_name = ?"
|
|
329
|
+
params: list[Any] = [workflow_name]
|
|
330
|
+
|
|
331
|
+
if project_id:
|
|
332
|
+
query += " AND project_id = ?"
|
|
333
|
+
params.append(project_id)
|
|
334
|
+
if status:
|
|
335
|
+
query += " AND status = ?"
|
|
336
|
+
params.append(status)
|
|
337
|
+
|
|
338
|
+
# Order by sequence_order (nulls last), then created_at
|
|
339
|
+
query += " ORDER BY COALESCE(sequence_order, 999999) ASC, created_at ASC LIMIT ? OFFSET ?"
|
|
340
|
+
params.extend([limit, offset])
|
|
341
|
+
|
|
342
|
+
rows = db.fetchall(query, tuple(params))
|
|
343
|
+
return [Task.from_row(row) for row in rows]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Task search module using TF-IDF.
|
|
2
|
+
|
|
3
|
+
Provides semantic search capabilities for tasks using the shared TF-IDF backend.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from gobby.search import TFIDFSearcher
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from gobby.storage.tasks._models import Task
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_searchable_content(task: Task) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Build searchable text content from a task.
|
|
22
|
+
|
|
23
|
+
Combines title + description + labels into a single searchable string.
|
|
24
|
+
This ensures all relevant text is indexed for search.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
task: Task object to extract content from
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Concatenated searchable text
|
|
31
|
+
"""
|
|
32
|
+
parts: list[str] = []
|
|
33
|
+
|
|
34
|
+
# Title is always present and most important
|
|
35
|
+
if task.title:
|
|
36
|
+
parts.append(task.title)
|
|
37
|
+
|
|
38
|
+
# Description provides additional context
|
|
39
|
+
if task.description:
|
|
40
|
+
parts.append(task.description)
|
|
41
|
+
|
|
42
|
+
# Labels can contain useful keywords
|
|
43
|
+
if task.labels:
|
|
44
|
+
parts.append(" ".join(task.labels))
|
|
45
|
+
|
|
46
|
+
# Task type can be useful for filtering
|
|
47
|
+
if task.task_type:
|
|
48
|
+
parts.append(task.task_type)
|
|
49
|
+
|
|
50
|
+
# Category can help with domain filtering
|
|
51
|
+
if task.category:
|
|
52
|
+
parts.append(task.category)
|
|
53
|
+
|
|
54
|
+
return " ".join(parts)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TaskSearcher:
|
|
58
|
+
"""
|
|
59
|
+
TF-IDF based search for tasks.
|
|
60
|
+
|
|
61
|
+
Wraps the generic TFIDFSearcher with task-specific content building.
|
|
62
|
+
Supports lazy fitting and dirty tracking for efficient reindexing.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
ngram_range: tuple[int, int] = (1, 2),
|
|
68
|
+
max_features: int = 10000,
|
|
69
|
+
min_df: int = 1,
|
|
70
|
+
stop_words: str | None = "english",
|
|
71
|
+
refit_threshold: int = 10,
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Initialize task searcher.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
ngram_range: Min/max n-gram sizes for tokenization
|
|
78
|
+
max_features: Maximum vocabulary size
|
|
79
|
+
min_df: Minimum document frequency for inclusion
|
|
80
|
+
stop_words: Language for stop words (None to disable)
|
|
81
|
+
refit_threshold: Number of updates before automatic refit
|
|
82
|
+
"""
|
|
83
|
+
self._searcher = TFIDFSearcher(
|
|
84
|
+
ngram_range=ngram_range,
|
|
85
|
+
max_features=max_features,
|
|
86
|
+
min_df=min_df,
|
|
87
|
+
stop_words=stop_words,
|
|
88
|
+
refit_threshold=refit_threshold,
|
|
89
|
+
)
|
|
90
|
+
self._dirty = True
|
|
91
|
+
|
|
92
|
+
def fit(self, tasks: list[Task]) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Build the search index from tasks.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
tasks: List of Task objects to index
|
|
98
|
+
"""
|
|
99
|
+
if not tasks:
|
|
100
|
+
self._searcher.fit([])
|
|
101
|
+
self._dirty = False
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
# Build (task_id, content) tuples
|
|
105
|
+
items = [(task.id, build_searchable_content(task)) for task in tasks]
|
|
106
|
+
|
|
107
|
+
self._searcher.fit(items)
|
|
108
|
+
self._dirty = False
|
|
109
|
+
logger.info(f"Task search index built with {len(tasks)} tasks")
|
|
110
|
+
|
|
111
|
+
def search(self, query: str, top_k: int = 20) -> list[tuple[str, float]]:
|
|
112
|
+
"""
|
|
113
|
+
Search for tasks matching the query.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
query: Search query text
|
|
117
|
+
top_k: Maximum number of results to return
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
List of (task_id, similarity_score) tuples, sorted by
|
|
121
|
+
similarity descending.
|
|
122
|
+
"""
|
|
123
|
+
return self._searcher.search(query, top_k)
|
|
124
|
+
|
|
125
|
+
def needs_refit(self) -> bool:
|
|
126
|
+
"""Check if the index needs rebuilding."""
|
|
127
|
+
return self._dirty or self._searcher.needs_refit()
|
|
128
|
+
|
|
129
|
+
def mark_dirty(self) -> None:
|
|
130
|
+
"""Mark the index as needing a refit."""
|
|
131
|
+
self._dirty = True
|
|
132
|
+
self._searcher.mark_update()
|
|
133
|
+
|
|
134
|
+
def get_stats(self) -> dict[str, Any]:
|
|
135
|
+
"""Get statistics about the search index."""
|
|
136
|
+
stats = self._searcher.get_stats()
|
|
137
|
+
stats["dirty"] = self._dirty
|
|
138
|
+
return stats
|
|
139
|
+
|
|
140
|
+
def clear(self) -> None:
|
|
141
|
+
"""Clear the search index."""
|
|
142
|
+
self._searcher.clear()
|
|
143
|
+
self._dirty = True
|