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/cli/tasks/crud.py
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CRUD commands for task management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from gobby.cli.tasks._utils import (
|
|
11
|
+
collect_ancestors,
|
|
12
|
+
compute_tree_prefixes,
|
|
13
|
+
format_task_header,
|
|
14
|
+
format_task_row,
|
|
15
|
+
get_claimed_task_ids,
|
|
16
|
+
get_task_manager,
|
|
17
|
+
normalize_status,
|
|
18
|
+
resolve_task_id,
|
|
19
|
+
sort_tasks_for_tree,
|
|
20
|
+
)
|
|
21
|
+
from gobby.cli.utils import resolve_project_ref
|
|
22
|
+
from gobby.utils.project_context import get_project_context
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.command("list")
|
|
26
|
+
@click.option(
|
|
27
|
+
"--status",
|
|
28
|
+
"-s",
|
|
29
|
+
help="Filter by status (open, in_progress, review, closed, blocked). Comma-separated for multiple.",
|
|
30
|
+
)
|
|
31
|
+
@click.option(
|
|
32
|
+
"--active",
|
|
33
|
+
is_flag=True,
|
|
34
|
+
help="Shorthand for --status open,in_progress (all active work)",
|
|
35
|
+
)
|
|
36
|
+
@click.option("--project", "-p", "project_ref", help="Filter by project (name or UUID)")
|
|
37
|
+
@click.option("--assignee", "-a", help="Filter by assignee")
|
|
38
|
+
@click.option(
|
|
39
|
+
"--ready", is_flag=True, help="Show only ready tasks (open/in_progress with no blocking deps)"
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--blocked", is_flag=True, help="Show only blocked tasks (open with unresolved blockers)"
|
|
43
|
+
)
|
|
44
|
+
@click.option("--limit", "-l", default=50, help="Max tasks to show")
|
|
45
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
46
|
+
def list_tasks(
|
|
47
|
+
status: str | None,
|
|
48
|
+
active: bool,
|
|
49
|
+
project_ref: str | None,
|
|
50
|
+
assignee: str | None,
|
|
51
|
+
ready: bool,
|
|
52
|
+
blocked: bool,
|
|
53
|
+
limit: int,
|
|
54
|
+
json_format: bool,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""List tasks."""
|
|
57
|
+
if ready and blocked:
|
|
58
|
+
click.echo("Error: --ready and --blocked are mutually exclusive.", err=True)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
if active and status:
|
|
62
|
+
click.echo("Error: --active and --status are mutually exclusive.", err=True)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
# Parse comma-separated statuses or use --active shorthand
|
|
66
|
+
# Normalize hyphen-separated status names (e.g., in-progress -> in_progress)
|
|
67
|
+
status_filter: str | list[str] | None = None
|
|
68
|
+
if active:
|
|
69
|
+
status_filter = ["open", "in_progress"]
|
|
70
|
+
elif status:
|
|
71
|
+
if "," in status:
|
|
72
|
+
status_filter = [normalize_status(s.strip()) for s in status.split(",")]
|
|
73
|
+
else:
|
|
74
|
+
status_filter = normalize_status(status)
|
|
75
|
+
|
|
76
|
+
project_id = resolve_project_ref(project_ref)
|
|
77
|
+
|
|
78
|
+
manager = get_task_manager()
|
|
79
|
+
|
|
80
|
+
if ready:
|
|
81
|
+
# Use ready task detection (open/in_progress tasks with no unresolved blocking dependencies)
|
|
82
|
+
tasks_list = manager.list_ready_tasks(
|
|
83
|
+
project_id=project_id,
|
|
84
|
+
assignee=assignee,
|
|
85
|
+
limit=limit,
|
|
86
|
+
)
|
|
87
|
+
label = "ready tasks"
|
|
88
|
+
empty_msg = "No ready tasks found."
|
|
89
|
+
elif blocked:
|
|
90
|
+
# Show tasks that are blocked by unresolved dependencies
|
|
91
|
+
tasks_list = manager.list_blocked_tasks(
|
|
92
|
+
project_id=project_id,
|
|
93
|
+
limit=limit,
|
|
94
|
+
)
|
|
95
|
+
label = "blocked tasks"
|
|
96
|
+
empty_msg = "No blocked tasks found."
|
|
97
|
+
else:
|
|
98
|
+
tasks_list = manager.list_tasks(
|
|
99
|
+
project_id=project_id,
|
|
100
|
+
status=status_filter,
|
|
101
|
+
assignee=assignee,
|
|
102
|
+
limit=limit,
|
|
103
|
+
)
|
|
104
|
+
label = "tasks"
|
|
105
|
+
empty_msg = "No tasks found."
|
|
106
|
+
|
|
107
|
+
if json_format:
|
|
108
|
+
click.echo(json.dumps([t.to_dict() for t in tasks_list], indent=2, default=str))
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if not tasks_list:
|
|
112
|
+
click.echo(empty_msg)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# For filtered views, include ancestors for proper tree hierarchy
|
|
116
|
+
primary_ids: set[str] | None = None
|
|
117
|
+
display_tasks = tasks_list
|
|
118
|
+
if ready or blocked or status_filter:
|
|
119
|
+
display_tasks, primary_ids = collect_ancestors(tasks_list, manager)
|
|
120
|
+
|
|
121
|
+
# Sort for proper tree display order
|
|
122
|
+
display_tasks = sort_tasks_for_tree(display_tasks)
|
|
123
|
+
|
|
124
|
+
# Get tasks claimed by active sessions for indicator display
|
|
125
|
+
claimed_ids = get_claimed_task_ids()
|
|
126
|
+
|
|
127
|
+
click.echo(f"Found {len(tasks_list)} {label}:")
|
|
128
|
+
click.echo(format_task_header())
|
|
129
|
+
prefixes = compute_tree_prefixes(display_tasks, primary_ids)
|
|
130
|
+
for task in display_tasks:
|
|
131
|
+
prefix_info = prefixes.get(task.id, ("", True))
|
|
132
|
+
tree_prefix, is_primary = prefix_info
|
|
133
|
+
click.echo(
|
|
134
|
+
format_task_row(
|
|
135
|
+
task,
|
|
136
|
+
tree_prefix=tree_prefix,
|
|
137
|
+
is_primary=is_primary,
|
|
138
|
+
claimed_task_ids=claimed_ids,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@click.command("ready")
|
|
144
|
+
@click.option("--limit", "-n", default=10, help="Max results")
|
|
145
|
+
@click.option("--project", "-p", "project_ref", help="Filter by project (name or UUID)")
|
|
146
|
+
@click.option("--priority", type=int, help="Filter by priority")
|
|
147
|
+
@click.option("--type", "-t", "task_type", help="Filter by type")
|
|
148
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
149
|
+
@click.option("--flat", is_flag=True, help="Flat list without tree hierarchy")
|
|
150
|
+
def ready_tasks(
|
|
151
|
+
limit: int,
|
|
152
|
+
project_ref: str | None,
|
|
153
|
+
priority: int | None,
|
|
154
|
+
task_type: str | None,
|
|
155
|
+
json_format: bool,
|
|
156
|
+
flat: bool,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""List tasks with no unresolved blocking dependencies."""
|
|
159
|
+
project_id = resolve_project_ref(project_ref)
|
|
160
|
+
manager = get_task_manager()
|
|
161
|
+
tasks_list = manager.list_ready_tasks(
|
|
162
|
+
project_id=project_id,
|
|
163
|
+
priority=priority,
|
|
164
|
+
task_type=task_type,
|
|
165
|
+
limit=limit,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if json_format:
|
|
169
|
+
click.echo(json.dumps([t.to_dict() for t in tasks_list], indent=2, default=str))
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
if not tasks_list:
|
|
173
|
+
click.echo("No ready tasks found.")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
# Get tasks claimed by active sessions for indicator display
|
|
177
|
+
claimed_ids = get_claimed_task_ids()
|
|
178
|
+
|
|
179
|
+
click.echo(f"Found {len(tasks_list)} ready tasks:")
|
|
180
|
+
click.echo(format_task_header())
|
|
181
|
+
|
|
182
|
+
if flat:
|
|
183
|
+
# Simple flat list without tree structure
|
|
184
|
+
for task in tasks_list:
|
|
185
|
+
click.echo(format_task_row(task, claimed_task_ids=claimed_ids))
|
|
186
|
+
else:
|
|
187
|
+
# Include ancestors for proper tree hierarchy
|
|
188
|
+
display_tasks, primary_ids = collect_ancestors(tasks_list, manager)
|
|
189
|
+
display_tasks = sort_tasks_for_tree(display_tasks)
|
|
190
|
+
prefixes = compute_tree_prefixes(display_tasks, primary_ids)
|
|
191
|
+
for task in display_tasks:
|
|
192
|
+
prefix_info = prefixes.get(task.id, ("", True))
|
|
193
|
+
tree_prefix, is_primary = prefix_info
|
|
194
|
+
click.echo(
|
|
195
|
+
format_task_row(
|
|
196
|
+
task,
|
|
197
|
+
tree_prefix=tree_prefix,
|
|
198
|
+
is_primary=is_primary,
|
|
199
|
+
claimed_task_ids=claimed_ids,
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@click.command("blocked")
|
|
205
|
+
@click.option("--limit", "-n", default=20, help="Max results")
|
|
206
|
+
@click.option("--project", "-p", "project_ref", help="Filter by project (name or UUID)")
|
|
207
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
208
|
+
def blocked_tasks(limit: int, project_ref: str | None, json_format: bool) -> None:
|
|
209
|
+
"""List blocked tasks with what blocks them."""
|
|
210
|
+
from gobby.storage.task_dependencies import TaskDependencyManager
|
|
211
|
+
|
|
212
|
+
project_id = resolve_project_ref(project_ref)
|
|
213
|
+
manager = get_task_manager()
|
|
214
|
+
dep_manager = TaskDependencyManager(manager.db)
|
|
215
|
+
blocked_list = manager.list_blocked_tasks(project_id=project_id, limit=limit)
|
|
216
|
+
|
|
217
|
+
if json_format:
|
|
218
|
+
# Build detailed structure for JSON output
|
|
219
|
+
result = []
|
|
220
|
+
for task in blocked_list:
|
|
221
|
+
tree = dep_manager.get_dependency_tree(task.id)
|
|
222
|
+
result.append(
|
|
223
|
+
{
|
|
224
|
+
"task": task.to_dict(),
|
|
225
|
+
"blocked_by": tree.get("blockers", []),
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
if not blocked_list:
|
|
232
|
+
click.echo("No blocked tasks found.")
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
click.echo(f"Found {len(blocked_list)} blocked tasks:")
|
|
236
|
+
for task in blocked_list:
|
|
237
|
+
tree = dep_manager.get_dependency_tree(task.id)
|
|
238
|
+
blocker_ids = tree.get("blockers", [])
|
|
239
|
+
click.echo(f"\n○ {task.id[:8]}: {task.title}")
|
|
240
|
+
if blocker_ids:
|
|
241
|
+
click.echo(" Blocked by:")
|
|
242
|
+
for b in blocker_ids:
|
|
243
|
+
blocker_id = b.get("id") if isinstance(b, dict) else b
|
|
244
|
+
if not blocker_id or not isinstance(blocker_id, str):
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
# Explicit cast to satisfy linter
|
|
248
|
+
bid: str = blocker_id
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
blocker_task = manager.get_task(bid)
|
|
252
|
+
status_icon = "✓" if blocker_task.status == "closed" else "○"
|
|
253
|
+
click.echo(f" {status_icon} {bid[:8]}: {blocker_task.title}")
|
|
254
|
+
except Exception:
|
|
255
|
+
click.echo(f" ? {bid[:8]}: (not found)")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@click.command("stats")
|
|
259
|
+
@click.option("--project", "-p", "project_ref", help="Filter by project (name or UUID)")
|
|
260
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
261
|
+
def task_stats(project_ref: str | None, json_format: bool) -> None:
|
|
262
|
+
"""Show task statistics."""
|
|
263
|
+
project_id = resolve_project_ref(project_ref)
|
|
264
|
+
manager = get_task_manager()
|
|
265
|
+
|
|
266
|
+
# Get counts by status
|
|
267
|
+
all_tasks = manager.list_tasks(project_id=project_id, limit=10000)
|
|
268
|
+
total = len(all_tasks)
|
|
269
|
+
by_status = {"open": 0, "in_progress": 0, "review": 0, "closed": 0}
|
|
270
|
+
by_priority = {1: 0, 2: 0, 3: 0}
|
|
271
|
+
by_type: dict[str, int] = {}
|
|
272
|
+
|
|
273
|
+
for task in all_tasks:
|
|
274
|
+
by_status[task.status] = by_status.get(task.status, 0) + 1
|
|
275
|
+
if task.priority:
|
|
276
|
+
by_priority[task.priority] = by_priority.get(task.priority, 0) + 1
|
|
277
|
+
if task.task_type:
|
|
278
|
+
by_type[task.task_type] = by_type.get(task.task_type, 0) + 1
|
|
279
|
+
|
|
280
|
+
# Get ready and blocked counts
|
|
281
|
+
ready_count = len(manager.list_ready_tasks(project_id=project_id, limit=10000))
|
|
282
|
+
blocked_count = len(manager.list_blocked_tasks(project_id=project_id, limit=10000))
|
|
283
|
+
|
|
284
|
+
stats = {
|
|
285
|
+
"total": total,
|
|
286
|
+
"by_status": by_status,
|
|
287
|
+
"by_priority": {
|
|
288
|
+
"high": by_priority.get(1, 0),
|
|
289
|
+
"medium": by_priority.get(2, 0),
|
|
290
|
+
"low": by_priority.get(3, 0),
|
|
291
|
+
},
|
|
292
|
+
"by_type": by_type,
|
|
293
|
+
"ready": ready_count,
|
|
294
|
+
"blocked": blocked_count,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if json_format:
|
|
298
|
+
click.echo(json.dumps(stats, indent=2))
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
click.echo("Task Statistics:")
|
|
302
|
+
click.echo(f" Total: {total}")
|
|
303
|
+
click.echo(f" Open: {by_status.get('open', 0)}")
|
|
304
|
+
click.echo(f" In Progress: {by_status.get('in_progress', 0)}")
|
|
305
|
+
click.echo(f" Review: {by_status.get('review', 0)}")
|
|
306
|
+
click.echo(f" Closed: {by_status.get('closed', 0)}")
|
|
307
|
+
click.echo(f"\n Ready (no blockers): {ready_count}")
|
|
308
|
+
click.echo(f" Blocked: {blocked_count}")
|
|
309
|
+
click.echo(f"\n High Priority: {by_priority.get(1, 0)}")
|
|
310
|
+
click.echo(f" Medium Priority: {by_priority.get(2, 0)}")
|
|
311
|
+
click.echo(f" Low Priority: {by_priority.get(3, 0)}")
|
|
312
|
+
if by_type:
|
|
313
|
+
click.echo("\n By Type:")
|
|
314
|
+
for t, count in sorted(by_type.items(), key=lambda x: -x[1]):
|
|
315
|
+
click.echo(f" {t}: {count}")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@click.command("create")
|
|
319
|
+
@click.argument("title")
|
|
320
|
+
@click.option("--description", "-d", help="Task description")
|
|
321
|
+
@click.option("--priority", "-p", type=int, default=2, help="Priority (1=High, 2=Med, 3=Low)")
|
|
322
|
+
@click.option("--type", "-t", "task_type", default="task", help="Task type")
|
|
323
|
+
def create_task(title: str, description: str | None, priority: int, task_type: str) -> None:
|
|
324
|
+
"""Create a new task."""
|
|
325
|
+
project_ctx = get_project_context()
|
|
326
|
+
if not project_ctx or "id" not in project_ctx:
|
|
327
|
+
click.echo("Error: Not in a gobby project or project.json missing 'id'.", err=True)
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
manager = get_task_manager()
|
|
331
|
+
task = manager.create_task(
|
|
332
|
+
project_id=project_ctx["id"],
|
|
333
|
+
title=title,
|
|
334
|
+
description=description,
|
|
335
|
+
priority=priority,
|
|
336
|
+
task_type=task_type,
|
|
337
|
+
)
|
|
338
|
+
task_ref = f"#{task.seq_num}" if task.seq_num else task.id[:8]
|
|
339
|
+
project_name = project_ctx.get("name") if project_ctx else None
|
|
340
|
+
|
|
341
|
+
if project_name and task.seq_num:
|
|
342
|
+
click.echo(f"Created task {project_name}-#{task.seq_num}: {task.title}")
|
|
343
|
+
else:
|
|
344
|
+
click.echo(f"Created task {task_ref}: {task.title}")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@click.command("show")
|
|
348
|
+
@click.argument("task_id", metavar="TASK")
|
|
349
|
+
def show_task(task_id: str) -> None:
|
|
350
|
+
"""Show details for a task.
|
|
351
|
+
|
|
352
|
+
TASK can be: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID.
|
|
353
|
+
"""
|
|
354
|
+
manager = get_task_manager()
|
|
355
|
+
task = resolve_task_id(manager, task_id)
|
|
356
|
+
|
|
357
|
+
if not task:
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
click.echo(f"Task: {task.title}")
|
|
361
|
+
click.echo(f"ID: {task.id}")
|
|
362
|
+
if task.seq_num:
|
|
363
|
+
click.echo(f"Ref: #{task.seq_num}")
|
|
364
|
+
click.echo(f"Status: {task.status}")
|
|
365
|
+
click.echo(f"Priority: {task.priority}")
|
|
366
|
+
click.echo(f"Type: {task.task_type}")
|
|
367
|
+
click.echo(f"Created: {task.created_at}")
|
|
368
|
+
click.echo(f"Updated: {task.updated_at}")
|
|
369
|
+
if task.assignee:
|
|
370
|
+
click.echo(f"Assignee: {task.assignee}")
|
|
371
|
+
if task.labels:
|
|
372
|
+
click.echo(f"Labels: {', '.join(task.labels)}")
|
|
373
|
+
if task.description:
|
|
374
|
+
click.echo(f"\n{task.description}")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@click.command("update")
|
|
378
|
+
@click.argument("task_id", metavar="TASK")
|
|
379
|
+
@click.option("--title", "-T", help="New title")
|
|
380
|
+
@click.option("--status", "-s", help="New status")
|
|
381
|
+
@click.option("--priority", type=int, help="New priority")
|
|
382
|
+
@click.option("--assignee", "-a", help="New assignee")
|
|
383
|
+
@click.option("--parent", "parent_task_id", help="Parent task (#N, path, or UUID)")
|
|
384
|
+
def update_task(
|
|
385
|
+
task_id: str,
|
|
386
|
+
title: str | None,
|
|
387
|
+
status: str | None,
|
|
388
|
+
priority: int | None,
|
|
389
|
+
assignee: str | None,
|
|
390
|
+
parent_task_id: str | None,
|
|
391
|
+
) -> None:
|
|
392
|
+
"""Update a task.
|
|
393
|
+
|
|
394
|
+
TASK can be: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID.
|
|
395
|
+
"""
|
|
396
|
+
manager = get_task_manager()
|
|
397
|
+
resolved = resolve_task_id(manager, task_id)
|
|
398
|
+
if not resolved:
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
# Resolve parent task ID if provided
|
|
402
|
+
resolved_parent_id = None
|
|
403
|
+
if parent_task_id:
|
|
404
|
+
resolved_parent = resolve_task_id(manager, parent_task_id)
|
|
405
|
+
if not resolved_parent:
|
|
406
|
+
return
|
|
407
|
+
resolved_parent_id = resolved_parent.id
|
|
408
|
+
|
|
409
|
+
# Only pass parameters that were explicitly provided (not None)
|
|
410
|
+
# to avoid setting NOT NULL fields to NULL
|
|
411
|
+
kwargs: dict[str, Any] = {}
|
|
412
|
+
if title is not None:
|
|
413
|
+
kwargs["title"] = title
|
|
414
|
+
if status is not None:
|
|
415
|
+
kwargs["status"] = status
|
|
416
|
+
if priority is not None:
|
|
417
|
+
kwargs["priority"] = priority
|
|
418
|
+
if assignee is not None:
|
|
419
|
+
kwargs["assignee"] = assignee
|
|
420
|
+
if resolved_parent_id is not None:
|
|
421
|
+
kwargs["parent_task_id"] = resolved_parent_id
|
|
422
|
+
|
|
423
|
+
task = manager.update_task(resolved.id, **kwargs)
|
|
424
|
+
|
|
425
|
+
# Use standardized ref
|
|
426
|
+
task_ref = f"#{task.seq_num}" if task.seq_num else task.id[:8]
|
|
427
|
+
click.echo(f"Updated task {task_ref}")
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@click.command("close")
|
|
431
|
+
@click.argument("task_ids", metavar="TASK", nargs=-1, required=True)
|
|
432
|
+
@click.option("--reason", "-r", default="completed", help="Reason for closing")
|
|
433
|
+
@click.option("--skip-validation", is_flag=True, help="Skip validation checks")
|
|
434
|
+
@click.option("--force", "-f", is_flag=True, help="Alias for --skip-validation")
|
|
435
|
+
def close_task_cmd(
|
|
436
|
+
task_ids: tuple[str, ...], reason: str, skip_validation: bool, force: bool
|
|
437
|
+
) -> None:
|
|
438
|
+
"""Close one or more tasks.
|
|
439
|
+
|
|
440
|
+
TASK can be: #N (e.g., #1, #47), seq_num (e.g., 47), path (e.g., 1.2.3), or UUID.
|
|
441
|
+
Multiple tasks can be specified separated by spaces or commas.
|
|
442
|
+
|
|
443
|
+
Examples:
|
|
444
|
+
gobby tasks close #42
|
|
445
|
+
gobby tasks close 42 43 44
|
|
446
|
+
gobby tasks close abc123,#45,46
|
|
447
|
+
|
|
448
|
+
Parent tasks require all children to be closed first.
|
|
449
|
+
Use --skip-validation or --force for wont_fix, duplicate, etc.
|
|
450
|
+
"""
|
|
451
|
+
manager = get_task_manager()
|
|
452
|
+
skip = skip_validation or force
|
|
453
|
+
|
|
454
|
+
# Expand comma-separated values into individual IDs
|
|
455
|
+
expanded_ids: list[str] = []
|
|
456
|
+
for task_id in task_ids:
|
|
457
|
+
if "," in task_id:
|
|
458
|
+
expanded_ids.extend(part.strip() for part in task_id.split(",") if part.strip())
|
|
459
|
+
else:
|
|
460
|
+
expanded_ids.append(task_id)
|
|
461
|
+
|
|
462
|
+
closed_count = 0
|
|
463
|
+
failed_count = 0
|
|
464
|
+
|
|
465
|
+
for task_id in expanded_ids:
|
|
466
|
+
resolved = resolve_task_id(manager, task_id)
|
|
467
|
+
if not resolved:
|
|
468
|
+
failed_count += 1
|
|
469
|
+
continue
|
|
470
|
+
|
|
471
|
+
if not skip:
|
|
472
|
+
# Check if task has children (is a parent task)
|
|
473
|
+
children = manager.list_tasks(parent_task_id=resolved.id, limit=1000)
|
|
474
|
+
|
|
475
|
+
if children:
|
|
476
|
+
# Parent task: must have all children closed
|
|
477
|
+
open_children = [c for c in children if c.status != "closed"]
|
|
478
|
+
if open_children:
|
|
479
|
+
task_ref = f"#{resolved.seq_num}" if resolved.seq_num else resolved.id[:8]
|
|
480
|
+
click.echo(
|
|
481
|
+
f"Cannot close {task_ref}: {len(open_children)} child tasks still open",
|
|
482
|
+
err=True,
|
|
483
|
+
)
|
|
484
|
+
failed_count += 1
|
|
485
|
+
continue
|
|
486
|
+
|
|
487
|
+
task = manager.close_task(resolved.id, reason=reason)
|
|
488
|
+
|
|
489
|
+
# Use standardized ref
|
|
490
|
+
task_ref = f"#{task.seq_num}" if task.seq_num else task.id[:8]
|
|
491
|
+
click.echo(f"Closed task {task_ref} ({reason})")
|
|
492
|
+
closed_count += 1
|
|
493
|
+
|
|
494
|
+
# Summary if multiple tasks were processed
|
|
495
|
+
if len(expanded_ids) > 1:
|
|
496
|
+
if failed_count > 0:
|
|
497
|
+
click.echo(f"\nClosed {closed_count}/{len(expanded_ids)} tasks ({failed_count} failed)")
|
|
498
|
+
else:
|
|
499
|
+
click.echo(f"\nClosed {closed_count} tasks")
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@click.command("reopen")
|
|
503
|
+
@click.argument("task_id", metavar="TASK")
|
|
504
|
+
@click.option("--reason", "-r", default=None, help="Reason for reopening")
|
|
505
|
+
def reopen_task_cmd(task_id: str, reason: str | None) -> None:
|
|
506
|
+
"""Reopen a closed or review task.
|
|
507
|
+
|
|
508
|
+
TASK can be: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID.
|
|
509
|
+
|
|
510
|
+
Sets status back to 'open', clears closed_at/closed_reason, and resets
|
|
511
|
+
accepted_by_user to false.
|
|
512
|
+
"""
|
|
513
|
+
manager = get_task_manager()
|
|
514
|
+
resolved = resolve_task_id(manager, task_id)
|
|
515
|
+
if not resolved:
|
|
516
|
+
return
|
|
517
|
+
|
|
518
|
+
# Use standardized ref for errors
|
|
519
|
+
resolved_ref = f"#{resolved.seq_num}" if resolved.seq_num else resolved.id[:8]
|
|
520
|
+
|
|
521
|
+
if resolved.status not in ("closed", "review"):
|
|
522
|
+
click.echo(
|
|
523
|
+
f"Task {resolved_ref} is not closed or in review (status: {resolved.status})", err=True
|
|
524
|
+
)
|
|
525
|
+
return
|
|
526
|
+
|
|
527
|
+
task = manager.reopen_task(resolved.id, reason=reason)
|
|
528
|
+
|
|
529
|
+
# Use standardized ref
|
|
530
|
+
task_ref = f"#{task.seq_num}" if task.seq_num else task.id[:8]
|
|
531
|
+
|
|
532
|
+
if reason:
|
|
533
|
+
click.echo(f"Reopened task {task_ref} ({reason})")
|
|
534
|
+
else:
|
|
535
|
+
click.echo(f"Reopened task {task_ref}")
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@click.command("delete")
|
|
539
|
+
@click.argument("task_refs", nargs=-1, required=True, metavar="TASKS...")
|
|
540
|
+
@click.option("--cascade", "-c", is_flag=True, help="Delete child tasks")
|
|
541
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
542
|
+
def delete_task(task_refs: tuple[str, ...], cascade: bool, yes: bool) -> None:
|
|
543
|
+
"""Delete one or more tasks.
|
|
544
|
+
|
|
545
|
+
TASKS can be: #N (e.g., #1, #47), comma-separated (#1,#2,#3), or UUIDs.
|
|
546
|
+
Multiple tasks can be specified separated by spaces or commas.
|
|
547
|
+
|
|
548
|
+
Examples:
|
|
549
|
+
gobby tasks delete #42
|
|
550
|
+
gobby tasks delete #42,#43,#44 --cascade
|
|
551
|
+
gobby tasks delete #42 #43 #44 --yes
|
|
552
|
+
"""
|
|
553
|
+
from gobby.cli.tasks._utils import parse_task_refs
|
|
554
|
+
|
|
555
|
+
manager = get_task_manager()
|
|
556
|
+
|
|
557
|
+
# Parse and resolve all task refs
|
|
558
|
+
all_refs = parse_task_refs(task_refs)
|
|
559
|
+
resolved_tasks = []
|
|
560
|
+
for ref in all_refs:
|
|
561
|
+
resolved = resolve_task_id(manager, ref)
|
|
562
|
+
if resolved:
|
|
563
|
+
resolved_tasks.append((ref, resolved))
|
|
564
|
+
|
|
565
|
+
if not resolved_tasks:
|
|
566
|
+
return
|
|
567
|
+
|
|
568
|
+
# Confirm deletion
|
|
569
|
+
if not yes:
|
|
570
|
+
task_list = ", ".join(ref for ref, _ in resolved_tasks)
|
|
571
|
+
if not click.confirm(f"Delete {len(resolved_tasks)} task(s): {task_list}?"):
|
|
572
|
+
click.echo("Cancelled.")
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
# Delete tasks
|
|
576
|
+
deleted = 0
|
|
577
|
+
for ref, resolved in resolved_tasks:
|
|
578
|
+
try:
|
|
579
|
+
manager.delete_task(resolved.id, cascade=cascade)
|
|
580
|
+
click.echo(f"Deleted task {resolved.id}")
|
|
581
|
+
deleted += 1
|
|
582
|
+
except ValueError as e:
|
|
583
|
+
msg = str(e)
|
|
584
|
+
if "has children" in msg and "cascade=True" in msg:
|
|
585
|
+
msg = f"Task {ref} has children. Use --cascade to delete with all subtasks."
|
|
586
|
+
click.echo(f"Error: {msg}", err=True)
|
|
587
|
+
|
|
588
|
+
if len(resolved_tasks) > 1:
|
|
589
|
+
click.echo(f"\nDeleted {deleted}/{len(resolved_tasks)} tasks.")
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
@click.command("de-escalate")
|
|
593
|
+
@click.argument("task_id", metavar="TASK")
|
|
594
|
+
@click.option("--reason", "-r", required=True, help="Reason for de-escalation")
|
|
595
|
+
@click.option("--reset-validation", is_flag=True, help="Reset validation fail count")
|
|
596
|
+
def de_escalate_cmd(task_id: str, reason: str, reset_validation: bool) -> None:
|
|
597
|
+
"""Return an escalated task to open status.
|
|
598
|
+
|
|
599
|
+
TASK can be: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID.
|
|
600
|
+
|
|
601
|
+
Use after human intervention resolves the issue that caused escalation.
|
|
602
|
+
"""
|
|
603
|
+
manager = get_task_manager()
|
|
604
|
+
resolved = resolve_task_id(manager, task_id)
|
|
605
|
+
if not resolved:
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
if resolved.status != "escalated":
|
|
609
|
+
click.echo(
|
|
610
|
+
f"Task {resolved.id[:8]} is not escalated (status: {resolved.status})",
|
|
611
|
+
err=True,
|
|
612
|
+
)
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
# Build update kwargs
|
|
616
|
+
update_kwargs: dict[str, str | int | None] = {
|
|
617
|
+
"status": "open",
|
|
618
|
+
"escalated_at": None,
|
|
619
|
+
"escalation_reason": None,
|
|
620
|
+
}
|
|
621
|
+
if reset_validation:
|
|
622
|
+
update_kwargs["validation_fail_count"] = 0
|
|
623
|
+
|
|
624
|
+
manager.update_task(resolved.id, **update_kwargs)
|
|
625
|
+
click.echo(f"De-escalated task {resolved.id[:8]} ({reason})")
|
|
626
|
+
if reset_validation:
|
|
627
|
+
click.echo(" Validation fail count reset to 0")
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
@click.command("validation-history")
|
|
631
|
+
@click.argument("task_id", metavar="TASK")
|
|
632
|
+
@click.option("--clear", is_flag=True, help="Clear validation history")
|
|
633
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
634
|
+
def validation_history_cmd(task_id: str, clear: bool, json_format: bool) -> None:
|
|
635
|
+
"""View or clear validation history for a task.
|
|
636
|
+
|
|
637
|
+
TASK can be: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID.
|
|
638
|
+
"""
|
|
639
|
+
from gobby.tasks.validation_history import ValidationHistoryManager
|
|
640
|
+
|
|
641
|
+
manager = get_task_manager()
|
|
642
|
+
resolved = resolve_task_id(manager, task_id)
|
|
643
|
+
if not resolved:
|
|
644
|
+
return
|
|
645
|
+
|
|
646
|
+
history_manager = ValidationHistoryManager(manager.db)
|
|
647
|
+
|
|
648
|
+
if clear:
|
|
649
|
+
history_manager.clear_history(resolved.id)
|
|
650
|
+
manager.update_task(resolved.id, validation_fail_count=0)
|
|
651
|
+
click.echo(f"Cleared validation history for {resolved.id[:8]}")
|
|
652
|
+
return
|
|
653
|
+
|
|
654
|
+
iterations = history_manager.get_iteration_history(resolved.id)
|
|
655
|
+
|
|
656
|
+
if json_format:
|
|
657
|
+
result = {
|
|
658
|
+
"task_id": resolved.id,
|
|
659
|
+
"iterations": [
|
|
660
|
+
{
|
|
661
|
+
"iteration": it.iteration,
|
|
662
|
+
"status": it.status,
|
|
663
|
+
"feedback": it.feedback,
|
|
664
|
+
"issues": [i.to_dict() for i in (it.issues or [])],
|
|
665
|
+
"created_at": it.created_at,
|
|
666
|
+
}
|
|
667
|
+
for it in iterations
|
|
668
|
+
],
|
|
669
|
+
}
|
|
670
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
671
|
+
return
|
|
672
|
+
|
|
673
|
+
if not iterations:
|
|
674
|
+
click.echo(f"No validation history for task {resolved.id[:8]}")
|
|
675
|
+
return
|
|
676
|
+
|
|
677
|
+
click.echo(f"Validation history for {resolved.id[:8]}:")
|
|
678
|
+
for it in iterations:
|
|
679
|
+
click.echo(f"\n Iteration {it.iteration}: {it.status}")
|
|
680
|
+
if it.feedback:
|
|
681
|
+
feedback_preview = it.feedback[:100] + "..." if len(it.feedback) > 100 else it.feedback
|
|
682
|
+
click.echo(f" Feedback: {feedback_preview}")
|
|
683
|
+
if it.issues:
|
|
684
|
+
click.echo(f" Issues: {len(it.issues)}")
|
|
685
|
+
click.echo(f" Created: {it.created_at}")
|