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,311 @@
|
|
|
1
|
+
"""Task lifecycle operations.
|
|
2
|
+
|
|
3
|
+
This module provides operations for managing task lifecycle:
|
|
4
|
+
- close_task: Close a task
|
|
5
|
+
- reopen_task: Reopen a closed/review task
|
|
6
|
+
- add_label, remove_label: Manage task labels
|
|
7
|
+
- link_commit, unlink_commit: Manage task-commit associations
|
|
8
|
+
- delete_task: Delete a task
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from gobby.storage.database import DatabaseProtocol
|
|
17
|
+
from gobby.storage.tasks._crud import get_task, update_task
|
|
18
|
+
from gobby.storage.tasks._models import Task
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def close_task(
|
|
24
|
+
db: DatabaseProtocol,
|
|
25
|
+
task_id: str,
|
|
26
|
+
reason: str | None = None,
|
|
27
|
+
force: bool = False,
|
|
28
|
+
closed_in_session_id: str | None = None,
|
|
29
|
+
closed_commit_sha: str | None = None,
|
|
30
|
+
validation_override_reason: str | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Close a task.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
db: Database protocol instance
|
|
36
|
+
task_id: The task ID to close
|
|
37
|
+
reason: Optional reason for closing
|
|
38
|
+
force: If True, close even if there are open children (default: False)
|
|
39
|
+
closed_in_session_id: Session ID where task was closed
|
|
40
|
+
closed_commit_sha: Git commit SHA at time of closing
|
|
41
|
+
validation_override_reason: Why agent bypassed validation (if applicable)
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If task not found or has open children (and force=False)
|
|
45
|
+
"""
|
|
46
|
+
# Check for open children unless force=True
|
|
47
|
+
if not force:
|
|
48
|
+
open_children = db.fetchall(
|
|
49
|
+
"SELECT id, title FROM tasks WHERE parent_task_id = ? AND status != 'closed'",
|
|
50
|
+
(task_id,),
|
|
51
|
+
)
|
|
52
|
+
if open_children:
|
|
53
|
+
child_list = ", ".join(f"{c['id']} ({c['title']})" for c in open_children[:3])
|
|
54
|
+
if len(open_children) > 3:
|
|
55
|
+
child_list += f" and {len(open_children) - 3} more"
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Cannot close task {task_id}: has {len(open_children)} open child task(s): {child_list}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Check if task is being closed from review state (user acceptance)
|
|
61
|
+
current_task = get_task(db, task_id)
|
|
62
|
+
accepted_by_user = current_task.status == "review" if current_task else False
|
|
63
|
+
|
|
64
|
+
now = datetime.now(UTC).isoformat()
|
|
65
|
+
with db.transaction() as conn:
|
|
66
|
+
cursor = conn.execute(
|
|
67
|
+
"""UPDATE tasks SET
|
|
68
|
+
status = 'closed',
|
|
69
|
+
closed_reason = ?,
|
|
70
|
+
closed_at = ?,
|
|
71
|
+
closed_in_session_id = ?,
|
|
72
|
+
closed_commit_sha = ?,
|
|
73
|
+
validation_override_reason = ?,
|
|
74
|
+
accepted_by_user = ?,
|
|
75
|
+
updated_at = ?
|
|
76
|
+
WHERE id = ?""",
|
|
77
|
+
(
|
|
78
|
+
reason,
|
|
79
|
+
now,
|
|
80
|
+
closed_in_session_id,
|
|
81
|
+
closed_commit_sha,
|
|
82
|
+
validation_override_reason,
|
|
83
|
+
1 if accepted_by_user else 0,
|
|
84
|
+
now,
|
|
85
|
+
task_id,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
if cursor.rowcount == 0:
|
|
89
|
+
raise ValueError(f"Task {task_id} not found")
|
|
90
|
+
|
|
91
|
+
# Update any associated worktrees to merged status (outside transaction)
|
|
92
|
+
# This is best-effort and should not roll back the task close
|
|
93
|
+
try:
|
|
94
|
+
db.execute(
|
|
95
|
+
"""UPDATE worktrees SET status = 'merged', updated_at = ?
|
|
96
|
+
WHERE task_id = ? AND status = 'active'""",
|
|
97
|
+
(now, task_id),
|
|
98
|
+
)
|
|
99
|
+
except Exception as wt_err:
|
|
100
|
+
# Worktree update is best-effort, don't fail task close
|
|
101
|
+
logger.debug(f"Failed to update worktree status for task {task_id}: {wt_err}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def reopen_task(
|
|
105
|
+
db: DatabaseProtocol,
|
|
106
|
+
task_id: str,
|
|
107
|
+
reason: str | None = None,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Reopen a closed or review task.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
db: Database protocol instance
|
|
113
|
+
task_id: The task ID to reopen
|
|
114
|
+
reason: Optional reason for reopening
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If task not found or not closed/review
|
|
118
|
+
"""
|
|
119
|
+
task = get_task(db, task_id)
|
|
120
|
+
if task.status not in ("closed", "review"):
|
|
121
|
+
raise ValueError(f"Task {task_id} is not closed or in review (status: {task.status})")
|
|
122
|
+
|
|
123
|
+
now = datetime.now(UTC).isoformat()
|
|
124
|
+
|
|
125
|
+
# Build description update if reason provided
|
|
126
|
+
new_description = task.description or ""
|
|
127
|
+
if reason:
|
|
128
|
+
reopen_note = f"\n\n[Reopened: {reason}]"
|
|
129
|
+
new_description = new_description + reopen_note
|
|
130
|
+
|
|
131
|
+
with db.transaction() as conn:
|
|
132
|
+
conn.execute(
|
|
133
|
+
"""UPDATE tasks SET
|
|
134
|
+
status = 'open',
|
|
135
|
+
closed_reason = NULL,
|
|
136
|
+
closed_at = NULL,
|
|
137
|
+
closed_in_session_id = NULL,
|
|
138
|
+
closed_commit_sha = NULL,
|
|
139
|
+
accepted_by_user = 0,
|
|
140
|
+
description = ?,
|
|
141
|
+
updated_at = ?
|
|
142
|
+
WHERE id = ?""",
|
|
143
|
+
(new_description if reason else task.description, now, task_id),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Reactivate any merged or abandoned worktrees for this task (outside transaction)
|
|
147
|
+
# This is best-effort and should not roll back the task reopen
|
|
148
|
+
try:
|
|
149
|
+
db.execute(
|
|
150
|
+
"""UPDATE worktrees SET status = 'active', updated_at = ?
|
|
151
|
+
WHERE task_id = ? AND status IN ('merged', 'abandoned')""",
|
|
152
|
+
(now, task_id),
|
|
153
|
+
)
|
|
154
|
+
except Exception as wt_err:
|
|
155
|
+
# Worktree update is best-effort, don't fail task reopen
|
|
156
|
+
logger.debug(f"Failed to reactivate worktree for task {task_id}: {wt_err}")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def add_label(db: DatabaseProtocol, task_id: str, label: str) -> Task:
|
|
160
|
+
"""Add a label to a task if not present."""
|
|
161
|
+
task = get_task(db, task_id)
|
|
162
|
+
labels = task.labels or []
|
|
163
|
+
if label not in labels:
|
|
164
|
+
labels.append(label)
|
|
165
|
+
update_task(db, task_id, labels=labels)
|
|
166
|
+
return get_task(db, task_id)
|
|
167
|
+
return task
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def remove_label(db: DatabaseProtocol, task_id: str, label: str) -> Task:
|
|
171
|
+
"""Remove a label from a task if present."""
|
|
172
|
+
task = get_task(db, task_id)
|
|
173
|
+
labels = task.labels or []
|
|
174
|
+
if label in labels:
|
|
175
|
+
labels.remove(label)
|
|
176
|
+
update_task(db, task_id, labels=labels)
|
|
177
|
+
return get_task(db, task_id)
|
|
178
|
+
return task
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def link_commit(
|
|
182
|
+
db: DatabaseProtocol, task_id: str, commit_sha: str, cwd: str | Path | None = None
|
|
183
|
+
) -> bool:
|
|
184
|
+
"""Link a commit SHA to a task.
|
|
185
|
+
|
|
186
|
+
Adds the commit SHA to the task's commits array if not already present.
|
|
187
|
+
The SHA is normalized to dynamic short format for consistency.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
db: Database protocol instance
|
|
191
|
+
task_id: The task ID to link the commit to.
|
|
192
|
+
commit_sha: The git commit SHA to link (short or full).
|
|
193
|
+
cwd: Working directory for git operations (defaults to current directory).
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
True if commit was added, False if already present.
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
ValueError: If task not found or SHA cannot be resolved.
|
|
200
|
+
"""
|
|
201
|
+
from gobby.utils.git import normalize_commit_sha
|
|
202
|
+
|
|
203
|
+
# Normalize SHA to dynamic short format
|
|
204
|
+
normalized_sha = normalize_commit_sha(commit_sha, cwd=cwd)
|
|
205
|
+
if not normalized_sha:
|
|
206
|
+
raise ValueError(f"Invalid or unresolved commit SHA: {commit_sha}")
|
|
207
|
+
|
|
208
|
+
task = get_task(db, task_id) # Raises if not found
|
|
209
|
+
commits = task.commits or []
|
|
210
|
+
if normalized_sha not in commits:
|
|
211
|
+
commits.append(normalized_sha)
|
|
212
|
+
# Update the commits column in the database
|
|
213
|
+
now = datetime.now(UTC).isoformat()
|
|
214
|
+
with db.transaction() as conn:
|
|
215
|
+
conn.execute(
|
|
216
|
+
"UPDATE tasks SET commits = ?, updated_at = ? WHERE id = ?",
|
|
217
|
+
(json.dumps(commits), now, task_id),
|
|
218
|
+
)
|
|
219
|
+
return True
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def unlink_commit(
|
|
224
|
+
db: DatabaseProtocol, task_id: str, commit_sha: str, cwd: str | Path | None = None
|
|
225
|
+
) -> bool:
|
|
226
|
+
"""Unlink a commit SHA from a task.
|
|
227
|
+
|
|
228
|
+
Removes the commit SHA from the task's commits array if present.
|
|
229
|
+
Handles both normalized and legacy SHA formats via prefix matching.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
db: Database protocol instance
|
|
233
|
+
task_id: The task ID to unlink the commit from.
|
|
234
|
+
commit_sha: The git commit SHA to unlink (short or full).
|
|
235
|
+
cwd: Working directory for git operations (defaults to current directory).
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
True if commit was removed, False if not found.
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
ValueError: If task not found.
|
|
242
|
+
"""
|
|
243
|
+
from gobby.utils.git import normalize_commit_sha
|
|
244
|
+
|
|
245
|
+
# Try to normalize - if it fails, fall back to prefix matching
|
|
246
|
+
normalized_sha = normalize_commit_sha(commit_sha, cwd=cwd)
|
|
247
|
+
|
|
248
|
+
task = get_task(db, task_id) # Raises if not found
|
|
249
|
+
commits = task.commits or []
|
|
250
|
+
|
|
251
|
+
# Find matching commit (handle both normalized and legacy SHAs)
|
|
252
|
+
sha_to_remove = None
|
|
253
|
+
for stored_sha in commits:
|
|
254
|
+
# Exact match with normalized SHA
|
|
255
|
+
if normalized_sha and stored_sha == normalized_sha:
|
|
256
|
+
sha_to_remove = stored_sha
|
|
257
|
+
break
|
|
258
|
+
# Prefix matching for legacy mixed-format data
|
|
259
|
+
if stored_sha.startswith(commit_sha) or commit_sha.startswith(stored_sha):
|
|
260
|
+
sha_to_remove = stored_sha
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
if sha_to_remove:
|
|
264
|
+
commits.remove(sha_to_remove)
|
|
265
|
+
# Update the commits column in the database
|
|
266
|
+
now = datetime.now(UTC).isoformat()
|
|
267
|
+
commits_json = json.dumps(commits) if commits else None
|
|
268
|
+
with db.transaction() as conn:
|
|
269
|
+
conn.execute(
|
|
270
|
+
"UPDATE tasks SET commits = ?, updated_at = ? WHERE id = ?",
|
|
271
|
+
(commits_json, now, task_id),
|
|
272
|
+
)
|
|
273
|
+
return True
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def delete_task(db: DatabaseProtocol, task_id: str, cascade: bool = False) -> bool:
|
|
278
|
+
"""Delete a task. If cascade is True, delete children recursively.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
db: Database protocol instance
|
|
282
|
+
task_id: The task ID to delete
|
|
283
|
+
cascade: If True, delete children recursively (default: False)
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
True if task was deleted, False if task not found.
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
ValueError: If task has children and cascade is False.
|
|
290
|
+
"""
|
|
291
|
+
# Check if task exists first
|
|
292
|
+
existing = db.fetchone("SELECT 1 FROM tasks WHERE id = ?", (task_id,))
|
|
293
|
+
if not existing:
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
if not cascade:
|
|
297
|
+
# Check for children
|
|
298
|
+
row = db.fetchone("SELECT 1 FROM tasks WHERE parent_task_id = ?", (task_id,))
|
|
299
|
+
if row:
|
|
300
|
+
raise ValueError(f"Task {task_id} has children. Use cascade=True to delete.")
|
|
301
|
+
|
|
302
|
+
if cascade:
|
|
303
|
+
# Recursive delete
|
|
304
|
+
# Find all children
|
|
305
|
+
children = db.fetchall("SELECT id FROM tasks WHERE parent_task_id = ?", (task_id,))
|
|
306
|
+
for child in children:
|
|
307
|
+
delete_task(db, child["id"], cascade=True)
|
|
308
|
+
|
|
309
|
+
with db.transaction() as conn:
|
|
310
|
+
conn.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
|
|
311
|
+
return True
|