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,300 @@
|
|
|
1
|
+
"""Task models, exceptions, and constants.
|
|
2
|
+
|
|
3
|
+
This module contains:
|
|
4
|
+
- Task dataclass with serialization methods
|
|
5
|
+
- Task-related exceptions
|
|
6
|
+
- Priority and category constants
|
|
7
|
+
- Validation and normalization helpers
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sqlite3
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
# Priority name to numeric value mapping
|
|
16
|
+
PRIORITY_MAP = {"backlog": 4, "low": 3, "medium": 2, "high": 1, "critical": 0}
|
|
17
|
+
|
|
18
|
+
# Valid task categories (enum-like constraint)
|
|
19
|
+
VALID_CATEGORIES: frozenset[str] = frozenset(
|
|
20
|
+
{
|
|
21
|
+
"code", # Implementation tasks
|
|
22
|
+
"config", # Configuration file changes
|
|
23
|
+
"docs", # Documentation tasks
|
|
24
|
+
"test", # Test infrastructure tasks (fixtures, helpers)
|
|
25
|
+
"refactor", # Refactoring tasks (including updating existing tests)
|
|
26
|
+
"research", # Investigation/exploration tasks
|
|
27
|
+
"planning", # Design/architecture tasks
|
|
28
|
+
"manual", # Manual functional testing (observe output)
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Sentinel for unset optional parameters
|
|
33
|
+
UNSET: Any = object()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def validate_category(category: str | None) -> str | None:
|
|
37
|
+
"""Validate and normalize a category value.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
category: Category string to validate (case-insensitive)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Normalized lowercase category if valid, None otherwise
|
|
44
|
+
"""
|
|
45
|
+
if category is None:
|
|
46
|
+
return None
|
|
47
|
+
normalized = category.lower().strip()
|
|
48
|
+
return normalized if normalized in VALID_CATEGORIES else None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def normalize_priority(priority: int | str | None) -> int:
|
|
52
|
+
"""Convert priority to numeric value for sorting."""
|
|
53
|
+
if priority is None:
|
|
54
|
+
return 999
|
|
55
|
+
if isinstance(priority, str):
|
|
56
|
+
# Check if it's a named priority
|
|
57
|
+
if priority.lower() in PRIORITY_MAP:
|
|
58
|
+
return PRIORITY_MAP[priority.lower()]
|
|
59
|
+
# Try to parse as int
|
|
60
|
+
try:
|
|
61
|
+
return int(priority)
|
|
62
|
+
except ValueError:
|
|
63
|
+
return 999
|
|
64
|
+
return int(priority)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TaskIDCollisionError(Exception):
|
|
68
|
+
"""Raised when a unique task ID cannot be generated."""
|
|
69
|
+
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TaskNotFoundError(Exception):
|
|
74
|
+
"""Raised when a task reference cannot be resolved to an existing task."""
|
|
75
|
+
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class Task:
|
|
81
|
+
id: str
|
|
82
|
+
project_id: str
|
|
83
|
+
title: str
|
|
84
|
+
status: Literal[
|
|
85
|
+
"open", "in_progress", "review", "closed", "failed", "escalated", "needs_decomposition"
|
|
86
|
+
]
|
|
87
|
+
priority: int
|
|
88
|
+
task_type: str # bug, feature, task, epic, chore
|
|
89
|
+
created_at: str
|
|
90
|
+
updated_at: str
|
|
91
|
+
# Optional fields
|
|
92
|
+
description: str | None = None
|
|
93
|
+
parent_task_id: str | None = None
|
|
94
|
+
created_in_session_id: str | None = None
|
|
95
|
+
closed_in_session_id: str | None = None
|
|
96
|
+
closed_commit_sha: str | None = None
|
|
97
|
+
closed_at: str | None = None
|
|
98
|
+
assignee: str | None = None
|
|
99
|
+
labels: list[str] | None = None
|
|
100
|
+
closed_reason: str | None = None
|
|
101
|
+
validation_status: Literal["pending", "valid", "invalid"] | None = None
|
|
102
|
+
validation_feedback: str | None = None
|
|
103
|
+
category: str | None = None
|
|
104
|
+
complexity_score: int | None = None
|
|
105
|
+
estimated_subtasks: int | None = None
|
|
106
|
+
expansion_context: str | None = None
|
|
107
|
+
validation_criteria: str | None = None
|
|
108
|
+
use_external_validator: bool = False
|
|
109
|
+
validation_fail_count: int = 0
|
|
110
|
+
validation_override_reason: str | None = None # Why agent bypassed validation
|
|
111
|
+
# Workflow integration fields
|
|
112
|
+
workflow_name: str | None = None
|
|
113
|
+
verification: str | None = None
|
|
114
|
+
sequence_order: int | None = None
|
|
115
|
+
# Commit linking
|
|
116
|
+
commits: list[str] | None = None
|
|
117
|
+
# Escalation fields
|
|
118
|
+
escalated_at: str | None = None
|
|
119
|
+
escalation_reason: str | None = None
|
|
120
|
+
# GitHub integration fields
|
|
121
|
+
github_issue_number: int | None = None
|
|
122
|
+
github_pr_number: int | None = None
|
|
123
|
+
github_repo: str | None = None
|
|
124
|
+
# Linear integration fields
|
|
125
|
+
linear_issue_id: str | None = None
|
|
126
|
+
linear_team_id: str | None = None
|
|
127
|
+
# Human-friendly ID fields (task renumbering)
|
|
128
|
+
seq_num: int | None = None
|
|
129
|
+
path_cache: str | None = None
|
|
130
|
+
# Agent configuration
|
|
131
|
+
agent_name: str | None = None # Subagent config file to use for this task
|
|
132
|
+
# Spec traceability
|
|
133
|
+
reference_doc: str | None = None # Path to source specification document
|
|
134
|
+
# Processing flags for idempotent operations
|
|
135
|
+
is_expanded: bool = False # Subtasks have been created
|
|
136
|
+
is_tdd_applied: bool = False # TDD pairs have been generated
|
|
137
|
+
# Review status fields (HITL support)
|
|
138
|
+
requires_user_review: bool = False # Task requires user sign-off before closing
|
|
139
|
+
accepted_by_user: bool = False # Set True when user moves review → closed
|
|
140
|
+
# Dependency fields (populated on demand, not stored in tasks table)
|
|
141
|
+
blocked_by: set[str] = field(default_factory=set)
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_row(cls, row: sqlite3.Row) -> "Task":
|
|
145
|
+
"""Convert database row to Task object."""
|
|
146
|
+
labels_json = row["labels"]
|
|
147
|
+
labels = json.loads(labels_json) if labels_json else []
|
|
148
|
+
|
|
149
|
+
# Handle optional columns that might not exist yet if migration pending
|
|
150
|
+
keys = row.keys()
|
|
151
|
+
|
|
152
|
+
return cls(
|
|
153
|
+
id=row["id"],
|
|
154
|
+
project_id=row["project_id"],
|
|
155
|
+
title=row["title"],
|
|
156
|
+
status=row["status"],
|
|
157
|
+
priority=normalize_priority(row["priority"]),
|
|
158
|
+
task_type=row["task_type"],
|
|
159
|
+
created_at=row["created_at"],
|
|
160
|
+
updated_at=row["updated_at"],
|
|
161
|
+
description=row["description"],
|
|
162
|
+
parent_task_id=row["parent_task_id"],
|
|
163
|
+
created_in_session_id=(
|
|
164
|
+
row["created_in_session_id"]
|
|
165
|
+
if "created_in_session_id" in keys
|
|
166
|
+
else (
|
|
167
|
+
row["discovered_in_session_id"] if "discovered_in_session_id" in keys else None
|
|
168
|
+
)
|
|
169
|
+
),
|
|
170
|
+
closed_in_session_id=(
|
|
171
|
+
row["closed_in_session_id"] if "closed_in_session_id" in keys else None
|
|
172
|
+
),
|
|
173
|
+
closed_commit_sha=row["closed_commit_sha"] if "closed_commit_sha" in keys else None,
|
|
174
|
+
closed_at=row["closed_at"] if "closed_at" in keys else None,
|
|
175
|
+
assignee=row["assignee"],
|
|
176
|
+
labels=labels,
|
|
177
|
+
closed_reason=row["closed_reason"],
|
|
178
|
+
validation_status=row["validation_status"] if "validation_status" in keys else None,
|
|
179
|
+
validation_feedback=(
|
|
180
|
+
row["validation_feedback"] if "validation_feedback" in keys else None
|
|
181
|
+
),
|
|
182
|
+
category=row["category"] if "category" in keys else None,
|
|
183
|
+
complexity_score=row["complexity_score"] if "complexity_score" in keys else None,
|
|
184
|
+
estimated_subtasks=row["estimated_subtasks"] if "estimated_subtasks" in keys else None,
|
|
185
|
+
expansion_context=row["expansion_context"] if "expansion_context" in keys else None,
|
|
186
|
+
validation_criteria=(
|
|
187
|
+
row["validation_criteria"] if "validation_criteria" in keys else None
|
|
188
|
+
),
|
|
189
|
+
use_external_validator=(
|
|
190
|
+
bool(row["use_external_validator"]) if "use_external_validator" in keys else False
|
|
191
|
+
),
|
|
192
|
+
validation_fail_count=(
|
|
193
|
+
row["validation_fail_count"] if "validation_fail_count" in keys else 0
|
|
194
|
+
),
|
|
195
|
+
validation_override_reason=(
|
|
196
|
+
row["validation_override_reason"] if "validation_override_reason" in keys else None
|
|
197
|
+
),
|
|
198
|
+
workflow_name=row["workflow_name"] if "workflow_name" in keys else None,
|
|
199
|
+
verification=row["verification"] if "verification" in keys else None,
|
|
200
|
+
sequence_order=row["sequence_order"] if "sequence_order" in keys else None,
|
|
201
|
+
commits=json.loads(row["commits"]) if "commits" in keys and row["commits"] else None,
|
|
202
|
+
escalated_at=row["escalated_at"] if "escalated_at" in keys else None,
|
|
203
|
+
escalation_reason=row["escalation_reason"] if "escalation_reason" in keys else None,
|
|
204
|
+
github_issue_number=(
|
|
205
|
+
row["github_issue_number"] if "github_issue_number" in keys else None
|
|
206
|
+
),
|
|
207
|
+
github_pr_number=row["github_pr_number"] if "github_pr_number" in keys else None,
|
|
208
|
+
github_repo=row["github_repo"] if "github_repo" in keys else None,
|
|
209
|
+
linear_issue_id=row["linear_issue_id"] if "linear_issue_id" in keys else None,
|
|
210
|
+
linear_team_id=row["linear_team_id"] if "linear_team_id" in keys else None,
|
|
211
|
+
seq_num=row["seq_num"] if "seq_num" in keys else None,
|
|
212
|
+
path_cache=row["path_cache"] if "path_cache" in keys else None,
|
|
213
|
+
agent_name=row["agent_name"] if "agent_name" in keys else None,
|
|
214
|
+
reference_doc=row["reference_doc"] if "reference_doc" in keys else None,
|
|
215
|
+
is_expanded=bool(row["is_expanded"]) if "is_expanded" in keys else False,
|
|
216
|
+
is_tdd_applied=bool(row["is_tdd_applied"]) if "is_tdd_applied" in keys else False,
|
|
217
|
+
requires_user_review=(
|
|
218
|
+
bool(row["requires_user_review"]) if "requires_user_review" in keys else False
|
|
219
|
+
),
|
|
220
|
+
accepted_by_user=(
|
|
221
|
+
bool(row["accepted_by_user"]) if "accepted_by_user" in keys else False
|
|
222
|
+
),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def to_dict(self) -> dict[str, Any]:
|
|
226
|
+
"""Convert Task to dictionary."""
|
|
227
|
+
return {
|
|
228
|
+
"ref": f"#{self.seq_num}" if self.seq_num else self.id[:8],
|
|
229
|
+
"project_id": self.project_id,
|
|
230
|
+
"title": self.title,
|
|
231
|
+
"status": self.status,
|
|
232
|
+
"priority": self.priority,
|
|
233
|
+
"type": self.task_type, # Use 'type' for API compatibility
|
|
234
|
+
"created_at": self.created_at,
|
|
235
|
+
"updated_at": self.updated_at,
|
|
236
|
+
"description": self.description,
|
|
237
|
+
"parent_task_id": self.parent_task_id,
|
|
238
|
+
"created_in_session_id": self.created_in_session_id,
|
|
239
|
+
"closed_in_session_id": self.closed_in_session_id,
|
|
240
|
+
"closed_commit_sha": self.closed_commit_sha,
|
|
241
|
+
"closed_at": self.closed_at,
|
|
242
|
+
"assignee": self.assignee,
|
|
243
|
+
"labels": self.labels,
|
|
244
|
+
"closed_reason": self.closed_reason,
|
|
245
|
+
"validation_status": self.validation_status,
|
|
246
|
+
"validation_feedback": self.validation_feedback,
|
|
247
|
+
"category": self.category,
|
|
248
|
+
"complexity_score": self.complexity_score,
|
|
249
|
+
"estimated_subtasks": self.estimated_subtasks,
|
|
250
|
+
"expansion_context": self.expansion_context,
|
|
251
|
+
"validation_criteria": self.validation_criteria,
|
|
252
|
+
"use_external_validator": self.use_external_validator,
|
|
253
|
+
"validation_fail_count": self.validation_fail_count,
|
|
254
|
+
"validation_override_reason": self.validation_override_reason,
|
|
255
|
+
"workflow_name": self.workflow_name,
|
|
256
|
+
"verification": self.verification,
|
|
257
|
+
"sequence_order": self.sequence_order,
|
|
258
|
+
"commits": self.commits,
|
|
259
|
+
"escalated_at": self.escalated_at,
|
|
260
|
+
"escalation_reason": self.escalation_reason,
|
|
261
|
+
"github_issue_number": self.github_issue_number,
|
|
262
|
+
"github_pr_number": self.github_pr_number,
|
|
263
|
+
"github_repo": self.github_repo,
|
|
264
|
+
"linear_issue_id": self.linear_issue_id,
|
|
265
|
+
"linear_team_id": self.linear_team_id,
|
|
266
|
+
"seq_num": self.seq_num,
|
|
267
|
+
"path_cache": self.path_cache,
|
|
268
|
+
"agent_name": self.agent_name,
|
|
269
|
+
"reference_doc": self.reference_doc,
|
|
270
|
+
"is_expanded": self.is_expanded,
|
|
271
|
+
"is_tdd_applied": self.is_tdd_applied,
|
|
272
|
+
"requires_user_review": self.requires_user_review,
|
|
273
|
+
"accepted_by_user": self.accepted_by_user,
|
|
274
|
+
"id": self.id, # UUID at end for backwards compat
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
def to_brief(self) -> dict[str, Any]:
|
|
278
|
+
"""Convert Task to brief discovery format for list operations.
|
|
279
|
+
|
|
280
|
+
Returns only essential fields needed for task discovery.
|
|
281
|
+
Use get_task() with to_dict() for full task details.
|
|
282
|
+
|
|
283
|
+
This follows the progressive disclosure pattern used for MCP tools:
|
|
284
|
+
- list_tasks() returns brief format (8 fields)
|
|
285
|
+
- get_task() returns full format (33 fields)
|
|
286
|
+
"""
|
|
287
|
+
return {
|
|
288
|
+
"ref": f"#{self.seq_num}" if self.seq_num else self.id[:8],
|
|
289
|
+
"title": self.title,
|
|
290
|
+
"status": self.status,
|
|
291
|
+
"priority": self.priority,
|
|
292
|
+
"type": self.task_type,
|
|
293
|
+
"parent_task_id": self.parent_task_id,
|
|
294
|
+
"created_at": self.created_at,
|
|
295
|
+
"updated_at": self.updated_at,
|
|
296
|
+
"seq_num": self.seq_num,
|
|
297
|
+
"path_cache": self.path_cache,
|
|
298
|
+
"requires_user_review": self.requires_user_review,
|
|
299
|
+
"id": self.id, # UUID at end for backwards compat
|
|
300
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Hierarchical task ordering utilities.
|
|
2
|
+
|
|
3
|
+
This module provides functions for ordering tasks hierarchically,
|
|
4
|
+
with parents appearing before their children and siblings sorted
|
|
5
|
+
topologically by dependencies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from gobby.storage.tasks._models import Task, normalize_priority
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def order_tasks_hierarchically(tasks: list[Task]) -> list[Task]:
|
|
12
|
+
"""
|
|
13
|
+
Reorder tasks so parents appear before their children.
|
|
14
|
+
|
|
15
|
+
The ordering is: parent -> children (recursively), then next parent -> children, etc.
|
|
16
|
+
Root tasks (no parent) are sorted by priority ASC, then created_at ASC.
|
|
17
|
+
Children are sorted by priority ASC, then created_at ASC within their parent.
|
|
18
|
+
|
|
19
|
+
Returns a new list with tasks ordered hierarchically.
|
|
20
|
+
"""
|
|
21
|
+
if not tasks:
|
|
22
|
+
return []
|
|
23
|
+
|
|
24
|
+
# Build lookup structures
|
|
25
|
+
task_by_id: dict[str, Task] = {t.id: t for t in tasks}
|
|
26
|
+
children_by_parent: dict[str | None, list[Task]] = {}
|
|
27
|
+
|
|
28
|
+
for task in tasks:
|
|
29
|
+
parent_id = task.parent_task_id
|
|
30
|
+
# Only group under parent if parent is in the result set
|
|
31
|
+
if parent_id and parent_id not in task_by_id:
|
|
32
|
+
parent_id = None
|
|
33
|
+
if parent_id not in children_by_parent:
|
|
34
|
+
children_by_parent[parent_id] = []
|
|
35
|
+
children_by_parent[parent_id].append(task)
|
|
36
|
+
|
|
37
|
+
def sort_siblings(siblings: list[Task]) -> list[Task]:
|
|
38
|
+
"""Sort siblings topologically with priority tie-breaking."""
|
|
39
|
+
if not siblings:
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
# 1. Build local dependency graph for these siblings
|
|
43
|
+
sibling_ids = {t.id for t in siblings}
|
|
44
|
+
graph: dict[str, list[str]] = {t.id: [] for t in siblings}
|
|
45
|
+
in_degree: dict[str, int] = {t.id: 0 for t in siblings}
|
|
46
|
+
|
|
47
|
+
for task in siblings:
|
|
48
|
+
# Check who blocks this task (Local dependencies only)
|
|
49
|
+
# task.blocked_by contains IDs of tasks that block 'task'
|
|
50
|
+
# If A blocks B, we want A -> B order.
|
|
51
|
+
# So graph edge is A -> B.
|
|
52
|
+
# task.blocked_by = {A} means B depends on A.
|
|
53
|
+
|
|
54
|
+
for blocker_id in task.blocked_by:
|
|
55
|
+
if blocker_id in sibling_ids:
|
|
56
|
+
graph[blocker_id].append(task.id)
|
|
57
|
+
in_degree[task.id] += 1
|
|
58
|
+
|
|
59
|
+
# 2. Initialize queue with tasks having 0 in-degree (no local blockers)
|
|
60
|
+
# We want to process high priority tasks first among available ones.
|
|
61
|
+
# Priority 0 is highest, so valid sort key is (priority, created_at).
|
|
62
|
+
# We sort the initial list to ensure deterministic order for stable sort
|
|
63
|
+
queue = [t for t in siblings if in_degree[t.id] == 0]
|
|
64
|
+
# Sort queue by priority/created_at so we pop high priority first
|
|
65
|
+
queue.sort(key=lambda t: (normalize_priority(t.priority), t.created_at))
|
|
66
|
+
|
|
67
|
+
sorted_siblings: list[Task] = []
|
|
68
|
+
|
|
69
|
+
while queue:
|
|
70
|
+
# Pop the first (highest priority available)
|
|
71
|
+
current = queue.pop(0)
|
|
72
|
+
sorted_siblings.append(current)
|
|
73
|
+
|
|
74
|
+
# Decrease in-degree of neighbors
|
|
75
|
+
neighbors = graph[current.id]
|
|
76
|
+
# Neighbors might become available. Collect them.
|
|
77
|
+
newly_available = []
|
|
78
|
+
for neighbor_id in neighbors:
|
|
79
|
+
in_degree[neighbor_id] -= 1
|
|
80
|
+
if in_degree[neighbor_id] == 0:
|
|
81
|
+
newly_available.append(task_by_id[neighbor_id])
|
|
82
|
+
|
|
83
|
+
# Sort newly available nodes by priority and add to queue
|
|
84
|
+
# We need to re-sort queue every time or insert in order.
|
|
85
|
+
# Since N is small (siblings usually < 50), simple re-sort of queue is fine.
|
|
86
|
+
newly_available.sort(key=lambda t: (normalize_priority(t.priority), t.created_at))
|
|
87
|
+
|
|
88
|
+
# Add newly available to queue. We want to maintain global order in queue
|
|
89
|
+
# based on priority.
|
|
90
|
+
# Merging two sorted lists is O(N).
|
|
91
|
+
queue.extend(newly_available)
|
|
92
|
+
queue.sort(key=lambda t: (normalize_priority(t.priority), t.created_at))
|
|
93
|
+
|
|
94
|
+
# Check for cycles (remaining nodes with >0 in-degree)
|
|
95
|
+
if len(sorted_siblings) < len(siblings):
|
|
96
|
+
# Cycle detected. Append remaining nodes sorted by priority.
|
|
97
|
+
remaining = [t for t in siblings if t not in sorted_siblings]
|
|
98
|
+
remaining.sort(key=lambda t: (normalize_priority(t.priority), t.created_at))
|
|
99
|
+
sorted_siblings.extend(remaining)
|
|
100
|
+
|
|
101
|
+
return sorted_siblings
|
|
102
|
+
|
|
103
|
+
# Sort children within each parent group
|
|
104
|
+
for parent_id, children in children_by_parent.items():
|
|
105
|
+
children_by_parent[parent_id] = sort_siblings(children)
|
|
106
|
+
|
|
107
|
+
# Build result with DFS traversal
|
|
108
|
+
result: list[Task] = []
|
|
109
|
+
|
|
110
|
+
def add_with_children(task: Task) -> None:
|
|
111
|
+
result.append(task)
|
|
112
|
+
for child in children_by_parent.get(task.id, []):
|
|
113
|
+
add_with_children(child)
|
|
114
|
+
|
|
115
|
+
# Start with root tasks (no parent or parent not in result set)
|
|
116
|
+
for root_task in children_by_parent.get(None, []):
|
|
117
|
+
add_with_children(root_task)
|
|
118
|
+
|
|
119
|
+
return result
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Path cache computation and management utilities.
|
|
2
|
+
|
|
3
|
+
This module provides functions for computing and updating task path caches,
|
|
4
|
+
which represent the hierarchical position of a task as a dotted seq_num path.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
|
|
10
|
+
from gobby.storage.database import DatabaseProtocol
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compute_path_cache(db: DatabaseProtocol, task_id: str) -> str | None:
|
|
16
|
+
"""Compute the hierarchical path for a task.
|
|
17
|
+
|
|
18
|
+
Traverses up the parent chain to build a dotted path from seq_nums.
|
|
19
|
+
Format: 'ancestor_seq.parent_seq.task_seq' (e.g., '1.3.47')
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
db: Database protocol instance
|
|
23
|
+
task_id: The task ID to compute path for
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Dotted path string (e.g., '1.3.47'), or None if task not found
|
|
27
|
+
or any task in the chain is missing a seq_num.
|
|
28
|
+
"""
|
|
29
|
+
# Build path by walking up parent chain
|
|
30
|
+
path_parts: list[str] = []
|
|
31
|
+
current_id: str | None = task_id
|
|
32
|
+
|
|
33
|
+
# Safety limit to prevent infinite loops (max 100 levels deep)
|
|
34
|
+
max_depth = 100
|
|
35
|
+
depth = 0
|
|
36
|
+
|
|
37
|
+
while current_id and depth < max_depth:
|
|
38
|
+
row = db.fetchone(
|
|
39
|
+
"SELECT seq_num, parent_task_id FROM tasks WHERE id = ?",
|
|
40
|
+
(current_id,),
|
|
41
|
+
)
|
|
42
|
+
if not row:
|
|
43
|
+
# Task not found
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
seq_num = row["seq_num"]
|
|
47
|
+
if seq_num is None:
|
|
48
|
+
# seq_num not yet assigned
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
path_parts.append(str(seq_num))
|
|
52
|
+
current_id = row["parent_task_id"]
|
|
53
|
+
depth += 1
|
|
54
|
+
|
|
55
|
+
if depth >= max_depth:
|
|
56
|
+
logger.warning(f"Task {task_id} exceeded max depth ({max_depth}) when computing path")
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
# Reverse to get root-to-leaf order
|
|
60
|
+
path_parts.reverse()
|
|
61
|
+
return ".".join(path_parts)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def update_path_cache(db: DatabaseProtocol, task_id: str) -> str | None:
|
|
65
|
+
"""Compute and store the path_cache for a task.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
db: Database protocol instance
|
|
69
|
+
task_id: The task ID to update
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The computed path, or None if computation failed
|
|
73
|
+
"""
|
|
74
|
+
path = compute_path_cache(db, task_id)
|
|
75
|
+
if path is not None:
|
|
76
|
+
now = datetime.now(UTC).isoformat()
|
|
77
|
+
db.execute(
|
|
78
|
+
"UPDATE tasks SET path_cache = ?, updated_at = ? WHERE id = ?",
|
|
79
|
+
(path, now, task_id),
|
|
80
|
+
)
|
|
81
|
+
return path
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def update_descendant_paths(db: DatabaseProtocol, task_id: str) -> int:
|
|
85
|
+
"""Update path_cache for a task and all its descendants.
|
|
86
|
+
|
|
87
|
+
Use this after reparenting a task to cascade path updates.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
db: Database protocol instance
|
|
91
|
+
task_id: The root task ID to start updating from
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Number of tasks updated
|
|
95
|
+
"""
|
|
96
|
+
updated_count = 0
|
|
97
|
+
|
|
98
|
+
# Update the task itself
|
|
99
|
+
if update_path_cache(db, task_id):
|
|
100
|
+
updated_count += 1
|
|
101
|
+
|
|
102
|
+
# Find and update all descendants (recursive)
|
|
103
|
+
children = db.fetchall(
|
|
104
|
+
"SELECT id FROM tasks WHERE parent_task_id = ?",
|
|
105
|
+
(task_id,),
|
|
106
|
+
)
|
|
107
|
+
for child in children:
|
|
108
|
+
updated_count += update_descendant_paths(db, child["id"])
|
|
109
|
+
|
|
110
|
+
return updated_count
|