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
gobby/tasks/tdd.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TDD sandwich pattern utilities for task expansion.
|
|
3
|
+
|
|
4
|
+
This module provides shared logic for applying the TDD (Test-Driven Development)
|
|
5
|
+
sandwich pattern to task expansions. The sandwich wraps implementation tasks:
|
|
6
|
+
- ONE [TDD] task at the start (RED phase - write failing tests)
|
|
7
|
+
- Original tasks renamed with [IMPL] prefix (GREEN phase - make tests pass)
|
|
8
|
+
- ONE [REF] task at the end (BLUE phase - refactor while keeping tests green)
|
|
9
|
+
|
|
10
|
+
Used by both MCP expand_task tool and CLI expand command.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from gobby.storage.task_dependencies import TaskDependencyManager
|
|
19
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"TDD_PREFIXES",
|
|
25
|
+
"TDD_SKIP_PATTERNS",
|
|
26
|
+
"TDD_CRITERIA_RED",
|
|
27
|
+
"TDD_CRITERIA_BLUE",
|
|
28
|
+
"TDD_PARENT_CRITERIA",
|
|
29
|
+
"TDD_CATEGORIES",
|
|
30
|
+
"should_skip_tdd",
|
|
31
|
+
"should_skip_expansion",
|
|
32
|
+
"apply_tdd_sandwich",
|
|
33
|
+
"build_expansion_context",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# TDD triplet prefixes - used for both skip detection and triplet creation
|
|
37
|
+
TDD_PREFIXES = ("[TDD]", "[IMPL]", "[REF]")
|
|
38
|
+
|
|
39
|
+
# Task categories that should get TDD treatment
|
|
40
|
+
TDD_CATEGORIES = ("code", "config")
|
|
41
|
+
|
|
42
|
+
# Patterns for tasks that should skip TDD transformation (case-insensitive)
|
|
43
|
+
TDD_SKIP_PATTERNS = (
|
|
44
|
+
# New TDD prefixes (already in triplet form)
|
|
45
|
+
r"^\[TDD\]",
|
|
46
|
+
r"^\[IMPL\]",
|
|
47
|
+
r"^\[REF\]",
|
|
48
|
+
# Legacy TDD prefixes (backwards compatibility)
|
|
49
|
+
r"^Write tests for:",
|
|
50
|
+
r"^Implement:",
|
|
51
|
+
r"^Refactor:",
|
|
52
|
+
# Deletion tasks (simple operations, no tests needed)
|
|
53
|
+
r"^Delete\b",
|
|
54
|
+
r"^Remove\b",
|
|
55
|
+
# Documentation updates
|
|
56
|
+
r"^Update.*README",
|
|
57
|
+
r"^Update.*documentation",
|
|
58
|
+
r"^Update.*docs\b",
|
|
59
|
+
# Config file updates
|
|
60
|
+
r"^Update.*\.toml\b",
|
|
61
|
+
r"^Update.*\.yaml\b",
|
|
62
|
+
r"^Update.*\.yml\b",
|
|
63
|
+
r"^Update.*\.json\b",
|
|
64
|
+
r"^Update.*\.env\b",
|
|
65
|
+
r"^Update.*config",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# TDD validation criteria templates per phase
|
|
69
|
+
TDD_CRITERIA_RED = """## Deliverable
|
|
70
|
+
- [ ] Tests written that define expected behavior
|
|
71
|
+
- [ ] Tests fail when run (no implementation yet)
|
|
72
|
+
- [ ] Test coverage addresses acceptance criteria from parent task
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
TDD_CRITERIA_BLUE = """## Deliverable
|
|
76
|
+
- [ ] All tests continue to pass
|
|
77
|
+
- [ ] Code refactored for clarity and maintainability
|
|
78
|
+
- [ ] No new functionality added (refactor only)
|
|
79
|
+
- [ ] Unrelated bugs discovered during refactor logged as new bug tasks
|
|
80
|
+
|
|
81
|
+
**Note:** If you discover bugs outside your scope during refactoring, create bug tasks
|
|
82
|
+
for them rather than fixing them now.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
TDD_PARENT_CRITERIA = """## Deliverable
|
|
86
|
+
- [ ] All child tasks completed
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def should_skip_tdd(title: str) -> bool:
|
|
91
|
+
"""
|
|
92
|
+
Check if a task should skip TDD transformation based on its title.
|
|
93
|
+
|
|
94
|
+
Tasks are skipped if they match any TDD_SKIP_PATTERNS:
|
|
95
|
+
- Already TDD triplet tasks ([TDD], [IMPL], [REF] prefixes)
|
|
96
|
+
- Legacy TDD prefixes (Write tests for:, Implement:, Refactor:)
|
|
97
|
+
- Deletion tasks (Delete X, Remove Y)
|
|
98
|
+
- Documentation updates (Update README, Update docs)
|
|
99
|
+
- Config file updates (Update pyproject.toml, Update .env)
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
title: The task title to check
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
True if the task should skip TDD transformation, False otherwise
|
|
106
|
+
"""
|
|
107
|
+
for pattern in TDD_SKIP_PATTERNS:
|
|
108
|
+
if re.search(pattern, title, re.IGNORECASE):
|
|
109
|
+
return True
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def should_skip_expansion(title: str, is_expanded: bool, force: bool = False) -> tuple[bool, str]:
|
|
114
|
+
"""
|
|
115
|
+
Check if a task should be skipped from expansion.
|
|
116
|
+
|
|
117
|
+
Tasks are skipped if:
|
|
118
|
+
- Already expanded (is_expanded=True) unless force=True
|
|
119
|
+
- Title starts with TDD prefixes ([TDD], [IMPL], [REF]) - these are atomic tasks
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
title: The task title to check
|
|
123
|
+
is_expanded: Whether the task's is_expanded flag is set
|
|
124
|
+
force: Whether to force expansion even if already expanded
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Tuple of (should_skip: bool, reason: str)
|
|
128
|
+
reason is empty string if should_skip is False
|
|
129
|
+
"""
|
|
130
|
+
# Check for TDD prefixes - these tasks should never be expanded
|
|
131
|
+
for prefix in TDD_PREFIXES:
|
|
132
|
+
if title.startswith(prefix):
|
|
133
|
+
return True, f"TDD task ({prefix})"
|
|
134
|
+
|
|
135
|
+
# Check if already expanded
|
|
136
|
+
if is_expanded and not force:
|
|
137
|
+
return True, "already expanded"
|
|
138
|
+
|
|
139
|
+
return False, ""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def apply_tdd_sandwich(
|
|
143
|
+
task_manager: "LocalTaskManager",
|
|
144
|
+
dep_manager: "TaskDependencyManager",
|
|
145
|
+
parent_task_id: str,
|
|
146
|
+
impl_task_ids: list[str],
|
|
147
|
+
refactor_task_ids: list[str] | None = None,
|
|
148
|
+
) -> dict[str, Any]:
|
|
149
|
+
"""Apply TDD sandwich pattern to a parent task's children.
|
|
150
|
+
|
|
151
|
+
Creates a "sandwich" structure where implementation tasks are wrapped:
|
|
152
|
+
- ONE [TDD] task at the start (RED phase - write failing tests for all impls)
|
|
153
|
+
- Original child tasks renamed with [IMPL] prefix (GREEN phase)
|
|
154
|
+
- Intermediate [REF] tasks (refactor-category subtasks) depend on all [IMPL]s
|
|
155
|
+
- ONE final [REF] task at the end (BLUE phase - refactor everything)
|
|
156
|
+
|
|
157
|
+
Dependencies are wired:
|
|
158
|
+
- All [IMPL] tasks are blocked by the [TDD] task
|
|
159
|
+
- Intermediate [REF] tasks are blocked by all [IMPL] tasks
|
|
160
|
+
- Final [REF] task is blocked by [TDD], all [IMPL]s, and intermediate [REF]s
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
task_manager: LocalTaskManager instance for task CRUD
|
|
164
|
+
dep_manager: TaskDependencyManager instance for dependency wiring
|
|
165
|
+
parent_task_id: The parent task ID being expanded
|
|
166
|
+
impl_task_ids: List of implementation task IDs (the original children)
|
|
167
|
+
refactor_task_ids: List of refactor-category task IDs (optional)
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Dict with:
|
|
171
|
+
- success: True if sandwich was applied
|
|
172
|
+
- tasks_created: Number of tasks created (2: TDD + REF)
|
|
173
|
+
- test_task_id: ID of the created TDD task
|
|
174
|
+
- refactor_task_id: ID of the created REF task
|
|
175
|
+
- impl_task_count: Number of impl tasks wrapped
|
|
176
|
+
- intermediate_refactor_count: Number of intermediate refactor tasks processed
|
|
177
|
+
Or error info if failed
|
|
178
|
+
"""
|
|
179
|
+
parent = task_manager.get_task(parent_task_id)
|
|
180
|
+
if not parent:
|
|
181
|
+
return {"success": False, "error": f"Parent task not found: {parent_task_id}"}
|
|
182
|
+
|
|
183
|
+
if not impl_task_ids:
|
|
184
|
+
return {"success": False, "error": "No implementation tasks to wrap"}
|
|
185
|
+
|
|
186
|
+
# Skip if already TDD-applied
|
|
187
|
+
if parent.is_tdd_applied:
|
|
188
|
+
return {"success": False, "skipped": True, "reason": "already_applied"}
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
# Build list of impl task titles for TDD task context
|
|
192
|
+
impl_titles = []
|
|
193
|
+
for impl_id in impl_task_ids:
|
|
194
|
+
impl_task = task_manager.get_task(impl_id)
|
|
195
|
+
if impl_task:
|
|
196
|
+
impl_titles.append(f"- {impl_task.title}")
|
|
197
|
+
impl_list = "\n".join(impl_titles) if impl_titles else "- (implementation tasks)"
|
|
198
|
+
|
|
199
|
+
# 1. Create ONE Test Task (Red phase) - tests for all implementations
|
|
200
|
+
test_task = task_manager.create_task(
|
|
201
|
+
title=f"[TDD] Write failing tests for {parent.title}",
|
|
202
|
+
description=(
|
|
203
|
+
f"Write failing tests for: {parent.title}\n\n"
|
|
204
|
+
"## Implementation tasks to cover:\n"
|
|
205
|
+
f"{impl_list}\n\n"
|
|
206
|
+
"RED phase of TDD - define expected behavior before implementation."
|
|
207
|
+
),
|
|
208
|
+
project_id=parent.project_id,
|
|
209
|
+
parent_task_id=parent.id,
|
|
210
|
+
task_type="task",
|
|
211
|
+
priority=parent.priority,
|
|
212
|
+
validation_criteria=TDD_CRITERIA_RED,
|
|
213
|
+
category="test",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# 2. Add [IMPL] prefix to all implementation tasks and wire dependencies
|
|
217
|
+
for impl_id in impl_task_ids:
|
|
218
|
+
impl_task = task_manager.get_task(impl_id)
|
|
219
|
+
if impl_task and not impl_task.title.startswith("[IMPL]"):
|
|
220
|
+
task_manager.update_task(impl_id, title=f"[IMPL] {impl_task.title}")
|
|
221
|
+
try:
|
|
222
|
+
dep_manager.add_dependency(impl_id, test_task.id, "blocks")
|
|
223
|
+
except ValueError:
|
|
224
|
+
pass # Dependency already exists
|
|
225
|
+
|
|
226
|
+
# 3. Add [REF] prefix to intermediate refactor tasks and wire dependencies
|
|
227
|
+
# These are refactor-category subtasks from the expansion (not the final [REF])
|
|
228
|
+
intermediate_refactor_ids: list[str] = []
|
|
229
|
+
for ref_id in refactor_task_ids or []:
|
|
230
|
+
ref_task = task_manager.get_task(ref_id)
|
|
231
|
+
if ref_task and not ref_task.title.startswith("[REF]"):
|
|
232
|
+
task_manager.update_task(ref_id, title=f"[REF] {ref_task.title}")
|
|
233
|
+
intermediate_refactor_ids.append(ref_id)
|
|
234
|
+
# Each intermediate [REF] depends on all [IMPL] tasks
|
|
235
|
+
for impl_id in impl_task_ids:
|
|
236
|
+
try:
|
|
237
|
+
dep_manager.add_dependency(ref_id, impl_id, "blocks")
|
|
238
|
+
except ValueError:
|
|
239
|
+
pass # Dependency already exists
|
|
240
|
+
|
|
241
|
+
# 5. Create ONE final Refactor Task (Blue phase) - refactor after all impls done
|
|
242
|
+
refactor_task = task_manager.create_task(
|
|
243
|
+
title=f"[REF] Refactor and verify {parent.title}",
|
|
244
|
+
description=(
|
|
245
|
+
f"Refactor implementations in: {parent.title}\n\n"
|
|
246
|
+
"BLUE phase of TDD - clean up while keeping tests green."
|
|
247
|
+
),
|
|
248
|
+
project_id=parent.project_id,
|
|
249
|
+
parent_task_id=parent.id,
|
|
250
|
+
task_type="task",
|
|
251
|
+
priority=parent.priority,
|
|
252
|
+
validation_criteria=TDD_CRITERIA_BLUE,
|
|
253
|
+
category="code",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# 6. Wire final refactor to be blocked by TDD, all impls, and intermediate refs
|
|
257
|
+
# REFACTOR depends on TDD (must complete testing before refactoring)
|
|
258
|
+
try:
|
|
259
|
+
dep_manager.add_dependency(refactor_task.id, test_task.id, "blocks")
|
|
260
|
+
except ValueError:
|
|
261
|
+
pass # Dependency already exists
|
|
262
|
+
|
|
263
|
+
# REFACTOR depends on all impl tasks
|
|
264
|
+
for impl_id in impl_task_ids:
|
|
265
|
+
try:
|
|
266
|
+
dep_manager.add_dependency(refactor_task.id, impl_id, "blocks")
|
|
267
|
+
except ValueError:
|
|
268
|
+
pass # Dependency already exists
|
|
269
|
+
|
|
270
|
+
# REFACTOR depends on all intermediate refactor tasks (if any)
|
|
271
|
+
for ref_id in intermediate_refactor_ids:
|
|
272
|
+
try:
|
|
273
|
+
dep_manager.add_dependency(refactor_task.id, ref_id, "blocks")
|
|
274
|
+
except ValueError:
|
|
275
|
+
pass # Dependency already exists
|
|
276
|
+
|
|
277
|
+
# Mark parent as TDD-applied
|
|
278
|
+
task_manager.update_task(
|
|
279
|
+
parent.id,
|
|
280
|
+
is_tdd_applied=True,
|
|
281
|
+
validation_criteria=TDD_PARENT_CRITERIA,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
"success": True,
|
|
286
|
+
"tasks_created": 2, # TDD + final REF (impl tasks already existed)
|
|
287
|
+
"test_task_id": test_task.id,
|
|
288
|
+
"refactor_task_id": refactor_task.id,
|
|
289
|
+
"impl_task_count": len(impl_task_ids),
|
|
290
|
+
"intermediate_refactor_count": len(intermediate_refactor_ids),
|
|
291
|
+
}
|
|
292
|
+
except Exception as e:
|
|
293
|
+
return {"success": False, "error": str(e)}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def build_expansion_context(
|
|
297
|
+
expansion_context_json: str | None,
|
|
298
|
+
user_context: str | None,
|
|
299
|
+
) -> str | None:
|
|
300
|
+
"""
|
|
301
|
+
Build context for expansion by merging stored data with user context.
|
|
302
|
+
|
|
303
|
+
If the task has expansion_context (legacy enrichment data), parse it and
|
|
304
|
+
include research findings, validation criteria, and complexity info.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
expansion_context_json: JSON string from task.expansion_context (may be None)
|
|
308
|
+
user_context: User-provided context string (may be None)
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Merged context string, or None if no context available
|
|
312
|
+
"""
|
|
313
|
+
import json
|
|
314
|
+
|
|
315
|
+
enrichment_parts: list[str] = []
|
|
316
|
+
|
|
317
|
+
# Parse stored expansion_context (legacy enrichment data)
|
|
318
|
+
if expansion_context_json:
|
|
319
|
+
try:
|
|
320
|
+
enrichment_data = json.loads(expansion_context_json)
|
|
321
|
+
|
|
322
|
+
# Include research findings
|
|
323
|
+
if research := enrichment_data.get("research_findings"):
|
|
324
|
+
enrichment_parts.append(f"## Research Findings\n{research}")
|
|
325
|
+
|
|
326
|
+
# Include validation criteria
|
|
327
|
+
if validation := enrichment_data.get("validation_criteria"):
|
|
328
|
+
enrichment_parts.append(f"## Validation Criteria\n{validation}")
|
|
329
|
+
|
|
330
|
+
# Include complexity info
|
|
331
|
+
complexity_level = enrichment_data.get("complexity_level")
|
|
332
|
+
subtask_count = enrichment_data.get("suggested_subtask_count")
|
|
333
|
+
if complexity_level or subtask_count:
|
|
334
|
+
complexity_info = []
|
|
335
|
+
if complexity_level:
|
|
336
|
+
complexity_info.append(f"Complexity level: {complexity_level}")
|
|
337
|
+
if subtask_count:
|
|
338
|
+
complexity_info.append(f"Suggested subtask count: {subtask_count}")
|
|
339
|
+
enrichment_parts.append("## Complexity Analysis\n" + "\n".join(complexity_info))
|
|
340
|
+
|
|
341
|
+
except (json.JSONDecodeError, TypeError):
|
|
342
|
+
# Legacy or plain text context - preserve it as raw text
|
|
343
|
+
enrichment_parts.append(f"## Legacy Expansion Context\n{expansion_context_json}")
|
|
344
|
+
|
|
345
|
+
# Add user-provided context
|
|
346
|
+
if user_context:
|
|
347
|
+
enrichment_parts.append(f"## Additional Context\n{user_context}")
|
|
348
|
+
|
|
349
|
+
# Return merged context or None
|
|
350
|
+
if enrichment_parts:
|
|
351
|
+
return "\n\n".join(enrichment_parts)
|
|
352
|
+
return None
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task tree builder module.
|
|
3
|
+
|
|
4
|
+
Creates task hierarchies from JSON tree structures. Simpler alternative
|
|
5
|
+
to TaskHierarchyBuilder which parses markdown.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class TreeBuildResult:
|
|
22
|
+
"""Result of building a task tree."""
|
|
23
|
+
|
|
24
|
+
tasks_created: int
|
|
25
|
+
epic_ref: str | None # Short ref for the root task (e.g., "#42")
|
|
26
|
+
task_refs: list[str] # All created task refs
|
|
27
|
+
errors: list[str] # Any errors encountered
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TaskTreeBuilder:
|
|
31
|
+
"""Builds task trees from JSON structures.
|
|
32
|
+
|
|
33
|
+
Creates tasks with parent-child relationships and wires dependencies
|
|
34
|
+
between siblings based on `depends_on` references.
|
|
35
|
+
|
|
36
|
+
Example tree:
|
|
37
|
+
{
|
|
38
|
+
"title": "Epic Title",
|
|
39
|
+
"task_type": "epic",
|
|
40
|
+
"children": [
|
|
41
|
+
{
|
|
42
|
+
"title": "Phase 1",
|
|
43
|
+
"children": [
|
|
44
|
+
{"title": "Task A", "category": "code"},
|
|
45
|
+
{"title": "Task B", "category": "code", "depends_on": ["Task A"]}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
task_manager: LocalTaskManager,
|
|
55
|
+
project_id: str,
|
|
56
|
+
session_id: str | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Initialize the builder.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
task_manager: LocalTaskManager instance for creating tasks
|
|
62
|
+
project_id: Project ID for created tasks
|
|
63
|
+
session_id: Optional session ID for tracking
|
|
64
|
+
"""
|
|
65
|
+
self.task_manager = task_manager
|
|
66
|
+
self.project_id = project_id
|
|
67
|
+
self.session_id = session_id
|
|
68
|
+
self._title_to_id: dict[str, str] = {} # Map title -> task_id for dependency resolution
|
|
69
|
+
self._sibling_index_map: dict[
|
|
70
|
+
str | None, dict[int, str]
|
|
71
|
+
] = {} # parent_id -> {sibling_index -> task_id}
|
|
72
|
+
self._created_tasks: list[str] = []
|
|
73
|
+
self._errors: list[str] = []
|
|
74
|
+
|
|
75
|
+
def build(self, tree: dict[str, Any]) -> TreeBuildResult:
|
|
76
|
+
"""Build task tree from JSON structure.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
tree: JSON tree with title, task_type, children, etc.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
TreeBuildResult with created task refs
|
|
83
|
+
"""
|
|
84
|
+
self._title_to_id = {}
|
|
85
|
+
self._sibling_index_map = {}
|
|
86
|
+
self._created_tasks = []
|
|
87
|
+
self._errors = []
|
|
88
|
+
|
|
89
|
+
# Create the root task
|
|
90
|
+
root_id = self._create_node(tree, parent_task_id=None, sibling_index=0)
|
|
91
|
+
|
|
92
|
+
# Wire dependencies after all tasks are created
|
|
93
|
+
self._wire_dependencies(tree)
|
|
94
|
+
|
|
95
|
+
# Get short refs for all created tasks
|
|
96
|
+
task_refs = []
|
|
97
|
+
epic_ref = None
|
|
98
|
+
for task_id in self._created_tasks:
|
|
99
|
+
task = self.task_manager.get_task(task_id)
|
|
100
|
+
if task and task.seq_num:
|
|
101
|
+
ref = f"#{task.seq_num}"
|
|
102
|
+
task_refs.append(ref)
|
|
103
|
+
if task_id == root_id:
|
|
104
|
+
epic_ref = ref
|
|
105
|
+
|
|
106
|
+
return TreeBuildResult(
|
|
107
|
+
tasks_created=len(self._created_tasks),
|
|
108
|
+
epic_ref=epic_ref,
|
|
109
|
+
task_refs=task_refs,
|
|
110
|
+
errors=self._errors,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def get_id_for_title(self, title: str) -> str | None:
|
|
114
|
+
"""Get the task ID for a given title.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
title: The task title to look up
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The task ID if found, None otherwise
|
|
121
|
+
"""
|
|
122
|
+
return self._title_to_id.get(title)
|
|
123
|
+
|
|
124
|
+
def _create_node(
|
|
125
|
+
self,
|
|
126
|
+
node: dict[str, Any],
|
|
127
|
+
parent_task_id: str | None,
|
|
128
|
+
sibling_index: int = 0,
|
|
129
|
+
) -> str | None:
|
|
130
|
+
"""Create a task node and its children recursively.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
node: Task node dict with title, children, etc.
|
|
134
|
+
parent_task_id: ID of parent task (None for root)
|
|
135
|
+
sibling_index: Index of this node among its siblings (for numeric dependency refs)
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Created task ID, or None if creation failed
|
|
139
|
+
"""
|
|
140
|
+
title = node.get("title")
|
|
141
|
+
if not title:
|
|
142
|
+
self._errors.append("Node missing required 'title' field")
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
# Extract task fields
|
|
146
|
+
task_type = node.get("task_type", "task")
|
|
147
|
+
description = node.get("description")
|
|
148
|
+
priority = node.get("priority", 2)
|
|
149
|
+
category = node.get("category")
|
|
150
|
+
labels = node.get("labels", [])
|
|
151
|
+
validation_criteria = node.get("validation_criteria")
|
|
152
|
+
requires_user_review = node.get("requires_user_review", False)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
# Create the task
|
|
156
|
+
task = self.task_manager.create_task(
|
|
157
|
+
title=title,
|
|
158
|
+
project_id=self.project_id,
|
|
159
|
+
task_type=task_type,
|
|
160
|
+
parent_task_id=parent_task_id,
|
|
161
|
+
description=description,
|
|
162
|
+
priority=priority,
|
|
163
|
+
category=category,
|
|
164
|
+
labels=labels,
|
|
165
|
+
validation_criteria=validation_criteria,
|
|
166
|
+
requires_user_review=requires_user_review,
|
|
167
|
+
created_in_session_id=self.session_id,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
self._created_tasks.append(task.id)
|
|
171
|
+
|
|
172
|
+
# Check for duplicate titles (warn but continue for partial functionality)
|
|
173
|
+
if title in self._title_to_id:
|
|
174
|
+
existing_id = self._title_to_id[title]
|
|
175
|
+
self._errors.append(
|
|
176
|
+
f"Duplicate task title '{title}': conflicts with existing task {existing_id}"
|
|
177
|
+
)
|
|
178
|
+
self._title_to_id[title] = task.id
|
|
179
|
+
|
|
180
|
+
# Track sibling index for numeric dependency references
|
|
181
|
+
if parent_task_id not in self._sibling_index_map:
|
|
182
|
+
self._sibling_index_map[parent_task_id] = {}
|
|
183
|
+
self._sibling_index_map[parent_task_id][sibling_index] = task.id
|
|
184
|
+
|
|
185
|
+
logger.debug(f"Created task {task.id} (#{task.seq_num}): {title}")
|
|
186
|
+
|
|
187
|
+
# Create children with their sibling indices
|
|
188
|
+
children = node.get("children", [])
|
|
189
|
+
for i, child in enumerate(children):
|
|
190
|
+
self._create_node(child, parent_task_id=task.id, sibling_index=i)
|
|
191
|
+
|
|
192
|
+
return task.id
|
|
193
|
+
|
|
194
|
+
except Exception as e:
|
|
195
|
+
self._errors.append(f"Failed to create task '{title}': {e}")
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
def _wire_dependencies(self, tree: dict[str, Any]) -> None:
|
|
199
|
+
"""Wire dependencies after all tasks are created.
|
|
200
|
+
|
|
201
|
+
Resolves `depends_on` references to task IDs. Supports:
|
|
202
|
+
- Title strings: `"depends_on": ["Task A"]` → lookup in _title_to_id
|
|
203
|
+
- Numeric indices: `"depends_on": [0]` → lookup sibling by index
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
tree: The original tree structure
|
|
207
|
+
"""
|
|
208
|
+
from gobby.storage.task_dependencies import TaskDependencyManager
|
|
209
|
+
|
|
210
|
+
dep_manager = TaskDependencyManager(self.task_manager.db)
|
|
211
|
+
|
|
212
|
+
def process_node(node: dict[str, Any], parent_task_id: str | None) -> None:
|
|
213
|
+
title = node.get("title")
|
|
214
|
+
depends_on = node.get("depends_on", [])
|
|
215
|
+
|
|
216
|
+
if title and depends_on and title in self._title_to_id:
|
|
217
|
+
task_id = self._title_to_id[title]
|
|
218
|
+
|
|
219
|
+
for dep in depends_on:
|
|
220
|
+
blocker_id: str | None = None
|
|
221
|
+
dep_display: str = str(dep) # For error messages
|
|
222
|
+
|
|
223
|
+
if isinstance(dep, int):
|
|
224
|
+
# Numeric index - look up sibling by index
|
|
225
|
+
sibling_map = self._sibling_index_map.get(parent_task_id, {})
|
|
226
|
+
blocker_id = sibling_map.get(dep)
|
|
227
|
+
if blocker_id is None:
|
|
228
|
+
self._errors.append(f"Sibling index {dep} not found for task '{title}'")
|
|
229
|
+
continue
|
|
230
|
+
elif isinstance(dep, str):
|
|
231
|
+
# Title string - look up by title
|
|
232
|
+
blocker_id = self._title_to_id.get(dep)
|
|
233
|
+
if blocker_id is None:
|
|
234
|
+
self._errors.append(f"Dependency not found: '{dep}' for task '{title}'")
|
|
235
|
+
continue
|
|
236
|
+
else:
|
|
237
|
+
self._errors.append(
|
|
238
|
+
f"Invalid dependency type {type(dep).__name__} for task '{title}'"
|
|
239
|
+
)
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
dep_manager.add_dependency(
|
|
244
|
+
task_id=task_id,
|
|
245
|
+
depends_on=blocker_id,
|
|
246
|
+
dep_type="blocks",
|
|
247
|
+
)
|
|
248
|
+
logger.debug(f"Added dependency: {title} depends on {dep_display}")
|
|
249
|
+
except ValueError as e:
|
|
250
|
+
# Ignore duplicate dependency errors
|
|
251
|
+
if "already exists" not in str(e):
|
|
252
|
+
self._errors.append(
|
|
253
|
+
f"Failed to add dependency {title} -> {dep_display}: {e}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Get this node's task_id to pass as parent for children
|
|
257
|
+
node_task_id = self._title_to_id.get(title) if title else None
|
|
258
|
+
|
|
259
|
+
# Process children
|
|
260
|
+
for child in node.get("children", []):
|
|
261
|
+
process_node(child, parent_task_id=node_task_id)
|
|
262
|
+
|
|
263
|
+
process_node(tree, parent_task_id=None)
|