gobby 0.2.5__py3-none-any.whl → 0.2.7__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 +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +13 -4
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/runner.py +8 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +15 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +8 -8
- gobby/cli/installers/shared.py +175 -13
- gobby/cli/sessions.py +1 -1
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +12 -5
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +69 -91
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +9 -41
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +188 -2
- gobby/hooks/hook_manager.py +50 -4
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/skill_manager.py +130 -0
- gobby/hooks/webhooks.py +1 -1
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +98 -35
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +56 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -8
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +518 -0
- gobby/mcp_proxy/tools/memory.py +3 -26
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +616 -0
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -338
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +73 -285
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +24 -12
- gobby/servers/routes/admin.py +294 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1317
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +2 -0
- gobby/sessions/transcripts/claude.py +79 -10
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +286 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +162 -201
- gobby/storage/sessions.py +116 -7
- gobby/storage/skills.py +782 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +57 -7
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +40 -5
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +46 -35
- gobby/tools/summarizer.py +91 -10
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1135
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +93 -1
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
- gobby/workflows/engine.py +13 -2
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/loader.py +19 -6
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +154 -0
- gobby/workflows/safe_evaluator.py +183 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +111 -1
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1292
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/session_messages.py +0 -1056
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
|
@@ -161,3 +161,58 @@ def clear_stop_signal(
|
|
|
161
161
|
|
|
162
162
|
cleared = stop_registry.clear(session_id)
|
|
163
163
|
return {"success": True, "cleared": cleared}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# --- ActionHandler factory functions ---
|
|
167
|
+
# These create ActionHandler-compatible wrappers that close over the stop_registry.
|
|
168
|
+
# The ActionExecutor calls these factories in _register_defaults() to create handlers
|
|
169
|
+
# that have access to the executor's stop_registry instance.
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def make_handle_check_stop_signal(
|
|
173
|
+
stop_registry: "StopRegistry | None",
|
|
174
|
+
) -> Any:
|
|
175
|
+
"""Factory that creates a check_stop_signal handler with access to stop_registry."""
|
|
176
|
+
|
|
177
|
+
async def handler(context: "Any", **kwargs: Any) -> dict[str, Any] | None:
|
|
178
|
+
"""ActionHandler for check_stop_signal."""
|
|
179
|
+
return check_stop_signal(
|
|
180
|
+
stop_registry=stop_registry,
|
|
181
|
+
session_id=context.session_id,
|
|
182
|
+
state=context.state,
|
|
183
|
+
acknowledge=kwargs.get("acknowledge", False),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return handler
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def make_handle_request_stop(
|
|
190
|
+
stop_registry: "StopRegistry | None",
|
|
191
|
+
) -> Any:
|
|
192
|
+
"""Factory that creates a request_stop handler with access to stop_registry."""
|
|
193
|
+
|
|
194
|
+
async def handler(context: "Any", **kwargs: Any) -> dict[str, Any] | None:
|
|
195
|
+
"""ActionHandler for request_stop."""
|
|
196
|
+
return request_stop(
|
|
197
|
+
stop_registry=stop_registry,
|
|
198
|
+
session_id=kwargs.get("session_id", context.session_id),
|
|
199
|
+
source=kwargs.get("source", "workflow"),
|
|
200
|
+
reason=kwargs.get("reason"),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return handler
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def make_handle_clear_stop_signal(
|
|
207
|
+
stop_registry: "StopRegistry | None",
|
|
208
|
+
) -> Any:
|
|
209
|
+
"""Factory that creates a clear_stop_signal handler with access to stop_registry."""
|
|
210
|
+
|
|
211
|
+
async def handler(context: "Any", **kwargs: Any) -> dict[str, Any] | None:
|
|
212
|
+
"""ActionHandler for clear_stop_signal."""
|
|
213
|
+
return clear_stop_signal(
|
|
214
|
+
stop_registry=stop_registry,
|
|
215
|
+
session_id=kwargs.get("session_id", context.session_id),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return handler
|
|
@@ -9,10 +9,13 @@ from __future__ import annotations
|
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import Any, Literal
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
13
13
|
|
|
14
14
|
from gobby.workflows.git_utils import get_file_changes, get_git_status
|
|
15
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from gobby.workflows.actions import ActionContext
|
|
18
|
+
|
|
16
19
|
logger = logging.getLogger(__name__)
|
|
17
20
|
|
|
18
21
|
|
|
@@ -42,6 +45,23 @@ def format_turns_for_llm(turns: list[dict[str, Any]]) -> str:
|
|
|
42
45
|
text_parts.append(f"[Thinking: {block.get('thinking', '')}]")
|
|
43
46
|
elif block.get("type") == "tool_use":
|
|
44
47
|
text_parts.append(f"[Tool: {block.get('name', 'unknown')}]")
|
|
48
|
+
elif block.get("type") == "tool_result":
|
|
49
|
+
result_content = block.get("content", "")
|
|
50
|
+
# Extract text from list of content blocks if needed
|
|
51
|
+
if isinstance(result_content, list):
|
|
52
|
+
extracted = []
|
|
53
|
+
for item in result_content:
|
|
54
|
+
if isinstance(item, dict):
|
|
55
|
+
extracted.append(
|
|
56
|
+
item.get("text", "") or item.get("content", "")
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
extracted.append(str(item))
|
|
60
|
+
result_content = " ".join(extracted)
|
|
61
|
+
content_str = str(result_content)
|
|
62
|
+
preview = content_str[:100]
|
|
63
|
+
suffix = "..." if len(content_str) > 100 else ""
|
|
64
|
+
text_parts.append(f"[Result: {preview}{suffix}]")
|
|
45
65
|
content = " ".join(text_parts)
|
|
46
66
|
|
|
47
67
|
formatted.append(f"[Turn {i + 1} - {role}]: {content}")
|
|
@@ -342,3 +362,93 @@ async def generate_handoff(
|
|
|
342
362
|
return {"error": "Failed to generate summary"}
|
|
343
363
|
|
|
344
364
|
return {"handoff_created": True, "summary_length": summary_result.get("summary_length", 0)}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# --- ActionHandler-compatible wrappers ---
|
|
368
|
+
# These match the ActionHandler protocol: (context: ActionContext, **kwargs) -> dict | None
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
async def handle_synthesize_title(context: ActionContext, **kwargs: Any) -> dict[str, Any] | None:
|
|
372
|
+
"""ActionHandler wrapper for synthesize_title."""
|
|
373
|
+
# Extract prompt from event data (UserPromptSubmit hook)
|
|
374
|
+
prompt = None
|
|
375
|
+
if context.event_data:
|
|
376
|
+
prompt = context.event_data.get("prompt")
|
|
377
|
+
|
|
378
|
+
return await synthesize_title(
|
|
379
|
+
session_manager=context.session_manager,
|
|
380
|
+
session_id=context.session_id,
|
|
381
|
+
llm_service=context.llm_service,
|
|
382
|
+
transcript_processor=context.transcript_processor,
|
|
383
|
+
template_engine=context.template_engine,
|
|
384
|
+
template=kwargs.get("template"),
|
|
385
|
+
prompt=prompt,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
async def handle_generate_summary(context: ActionContext, **kwargs: Any) -> dict[str, Any] | None:
|
|
390
|
+
"""ActionHandler wrapper for generate_summary."""
|
|
391
|
+
return await generate_summary(
|
|
392
|
+
session_manager=context.session_manager,
|
|
393
|
+
session_id=context.session_id,
|
|
394
|
+
llm_service=context.llm_service,
|
|
395
|
+
transcript_processor=context.transcript_processor,
|
|
396
|
+
template=kwargs.get("template"),
|
|
397
|
+
mode=kwargs.get("mode", "clear"),
|
|
398
|
+
previous_summary=kwargs.get("previous_summary"),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
async def handle_generate_handoff(context: ActionContext, **kwargs: Any) -> dict[str, Any] | None:
|
|
403
|
+
"""ActionHandler wrapper for generate_handoff.
|
|
404
|
+
|
|
405
|
+
Handles mode detection from event_data and previous summary fetching for compact mode.
|
|
406
|
+
Also supports loading templates from prompts collection via 'prompt' parameter.
|
|
407
|
+
"""
|
|
408
|
+
# Detect mode from kwargs or event data
|
|
409
|
+
mode = kwargs.get("mode", "clear")
|
|
410
|
+
|
|
411
|
+
# Check if this is a compact event based on event_data
|
|
412
|
+
COMPACT_EVENT_TYPES = {"pre_compact", "compact"}
|
|
413
|
+
if context.event_data:
|
|
414
|
+
raw_event_type = context.event_data.get("event_type") or ""
|
|
415
|
+
normalized_event_type = str(raw_event_type).strip().lower()
|
|
416
|
+
if normalized_event_type in COMPACT_EVENT_TYPES:
|
|
417
|
+
mode = "compact"
|
|
418
|
+
|
|
419
|
+
# For compact mode, fetch previous summary for cumulative compression
|
|
420
|
+
previous_summary = None
|
|
421
|
+
if mode == "compact":
|
|
422
|
+
current_session = context.session_manager.get(context.session_id)
|
|
423
|
+
if current_session:
|
|
424
|
+
previous_summary = getattr(current_session, "summary_markdown", None)
|
|
425
|
+
if previous_summary:
|
|
426
|
+
logger.debug(
|
|
427
|
+
f"Compact mode: using previous summary ({len(previous_summary)} chars) "
|
|
428
|
+
f"for cumulative compression"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Load template from prompts collection if 'prompt' parameter provided
|
|
432
|
+
template = kwargs.get("template")
|
|
433
|
+
prompt_path = kwargs.get("prompt")
|
|
434
|
+
if prompt_path and not template:
|
|
435
|
+
try:
|
|
436
|
+
from gobby.prompts.loader import PromptLoader
|
|
437
|
+
|
|
438
|
+
loader = PromptLoader()
|
|
439
|
+
prompt_template = loader.load(prompt_path)
|
|
440
|
+
template = prompt_template.content
|
|
441
|
+
logger.debug(f"Loaded prompt template from: {prompt_path}")
|
|
442
|
+
except Exception as e:
|
|
443
|
+
logger.warning(f"Failed to load prompt from {prompt_path}: {e}")
|
|
444
|
+
# Fall back to inline template or default
|
|
445
|
+
|
|
446
|
+
return await generate_handoff(
|
|
447
|
+
session_manager=context.session_manager,
|
|
448
|
+
session_id=context.session_id,
|
|
449
|
+
llm_service=context.llm_service,
|
|
450
|
+
transcript_processor=context.transcript_processor,
|
|
451
|
+
template=template,
|
|
452
|
+
previous_summary=previous_summary,
|
|
453
|
+
mode=mode,
|
|
454
|
+
)
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""Task sync workflow actions.
|
|
2
|
+
|
|
3
|
+
Extracted from actions.py as part of strangler fig decomposition.
|
|
4
|
+
These functions handle task sync import/export and workflow task operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from gobby.storage.database import DatabaseProtocol
|
|
13
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
14
|
+
from gobby.workflows.definitions import WorkflowState
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def task_sync_import(
|
|
20
|
+
task_sync_manager: Any,
|
|
21
|
+
session_manager: "LocalSessionManager",
|
|
22
|
+
session_id: str,
|
|
23
|
+
) -> dict[str, Any]:
|
|
24
|
+
"""Import tasks from JSONL file.
|
|
25
|
+
|
|
26
|
+
Reads .gobby/tasks.jsonl and imports tasks into SQLite using
|
|
27
|
+
Last-Write-Wins conflict resolution based on updated_at.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
task_sync_manager: TaskSyncManager instance
|
|
31
|
+
session_manager: Session manager for project lookup
|
|
32
|
+
session_id: Current session ID
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dict with imported status or error
|
|
36
|
+
"""
|
|
37
|
+
if not task_sync_manager:
|
|
38
|
+
logger.debug("task_sync_import: No task_sync_manager available")
|
|
39
|
+
return {"error": "Task Sync Manager not available"}
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# Get project_id from session for project-scoped sync
|
|
43
|
+
project_id = None
|
|
44
|
+
session = await asyncio.to_thread(session_manager.get, session_id)
|
|
45
|
+
if session:
|
|
46
|
+
project_id = session.project_id
|
|
47
|
+
|
|
48
|
+
await asyncio.to_thread(task_sync_manager.import_from_jsonl, project_id=project_id)
|
|
49
|
+
logger.info("Task sync import completed")
|
|
50
|
+
return {"imported": True}
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.error(f"task_sync_import failed: {e}", exc_info=True)
|
|
53
|
+
return {"error": str(e)}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def task_sync_export(
|
|
57
|
+
task_sync_manager: Any,
|
|
58
|
+
session_manager: "LocalSessionManager",
|
|
59
|
+
session_id: str,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
"""Export tasks to JSONL file.
|
|
62
|
+
|
|
63
|
+
Writes tasks and dependencies to .gobby/tasks.jsonl for Git persistence.
|
|
64
|
+
Uses content hashing to skip writes if nothing changed.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
task_sync_manager: TaskSyncManager instance
|
|
68
|
+
session_manager: Session manager for project lookup
|
|
69
|
+
session_id: Current session ID
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Dict with exported status or error
|
|
73
|
+
"""
|
|
74
|
+
if not task_sync_manager:
|
|
75
|
+
logger.debug("task_sync_export: No task_sync_manager available")
|
|
76
|
+
return {"error": "Task Sync Manager not available"}
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
# Get project_id from session for project-scoped sync
|
|
80
|
+
project_id = None
|
|
81
|
+
session = await asyncio.to_thread(session_manager.get, session_id)
|
|
82
|
+
if session:
|
|
83
|
+
project_id = session.project_id
|
|
84
|
+
|
|
85
|
+
await asyncio.to_thread(task_sync_manager.export_to_jsonl, project_id=project_id)
|
|
86
|
+
logger.info("Task sync export completed")
|
|
87
|
+
return {"exported": True}
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"task_sync_export failed: {e}", exc_info=True)
|
|
90
|
+
return {"error": str(e)}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def persist_tasks(
|
|
94
|
+
db: "DatabaseProtocol",
|
|
95
|
+
session_manager: "LocalSessionManager",
|
|
96
|
+
session_id: str,
|
|
97
|
+
state: "WorkflowState",
|
|
98
|
+
tasks: list[dict[str, Any]] | None = None,
|
|
99
|
+
source: str | None = None,
|
|
100
|
+
workflow_name: str | None = None,
|
|
101
|
+
parent_task_id: str | None = None,
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
|
+
"""Persist a list of task dicts to Gobby task system.
|
|
104
|
+
|
|
105
|
+
Enhanced to support workflow integration with ID mapping.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
db: Database instance
|
|
109
|
+
session_manager: Session manager
|
|
110
|
+
session_id: Current session ID
|
|
111
|
+
state: WorkflowState for variables access
|
|
112
|
+
tasks: List of task dicts
|
|
113
|
+
source: Variable name containing task list (alternative to tasks)
|
|
114
|
+
workflow_name: Associate tasks with this workflow
|
|
115
|
+
parent_task_id: Optional parent task for all created tasks
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dict with tasks_persisted count, ids list, and id_mapping dict
|
|
119
|
+
"""
|
|
120
|
+
# Get tasks from either 'tasks' kwarg or 'source' variable
|
|
121
|
+
task_list = tasks or []
|
|
122
|
+
|
|
123
|
+
if source and state.variables:
|
|
124
|
+
source_data = state.variables.get(source)
|
|
125
|
+
if source_data:
|
|
126
|
+
# Handle nested structure like task_list.tasks
|
|
127
|
+
if isinstance(source_data, dict) and "tasks" in source_data:
|
|
128
|
+
task_list = source_data["tasks"]
|
|
129
|
+
elif isinstance(source_data, list):
|
|
130
|
+
task_list = source_data
|
|
131
|
+
|
|
132
|
+
if not task_list:
|
|
133
|
+
return {"tasks_persisted": 0, "ids": [], "id_mapping": {}}
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
from gobby.workflows.task_actions import persist_decomposed_tasks
|
|
137
|
+
|
|
138
|
+
current_session = await asyncio.to_thread(session_manager.get, session_id)
|
|
139
|
+
project_id = current_session.project_id if current_session else "default"
|
|
140
|
+
|
|
141
|
+
# Get workflow name from kwargs or state
|
|
142
|
+
wf_name = workflow_name
|
|
143
|
+
if not wf_name and state.workflow_name:
|
|
144
|
+
wf_name = state.workflow_name
|
|
145
|
+
|
|
146
|
+
id_mapping = await asyncio.to_thread(
|
|
147
|
+
persist_decomposed_tasks,
|
|
148
|
+
db=db,
|
|
149
|
+
project_id=project_id,
|
|
150
|
+
tasks_data=task_list,
|
|
151
|
+
workflow_name=wf_name or "unnamed",
|
|
152
|
+
parent_task_id=parent_task_id,
|
|
153
|
+
created_in_session_id=session_id,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Store ID mapping in workflow state for reference
|
|
157
|
+
if not state.variables:
|
|
158
|
+
state.variables = {}
|
|
159
|
+
state.variables["task_id_mapping"] = id_mapping
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
"tasks_persisted": len(id_mapping),
|
|
163
|
+
"ids": list(id_mapping.values()),
|
|
164
|
+
"id_mapping": id_mapping,
|
|
165
|
+
}
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.error(f"persist_tasks: Failed: {e}", exc_info=True)
|
|
168
|
+
return {"error": str(e)}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
async def get_workflow_tasks(
|
|
172
|
+
db: "DatabaseProtocol",
|
|
173
|
+
session_manager: "LocalSessionManager",
|
|
174
|
+
session_id: str,
|
|
175
|
+
state: "WorkflowState",
|
|
176
|
+
workflow_name: str | None = None,
|
|
177
|
+
include_closed: bool = False,
|
|
178
|
+
output_as: str | None = None,
|
|
179
|
+
) -> dict[str, Any]:
|
|
180
|
+
"""Get tasks associated with the current workflow.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
db: Database instance
|
|
184
|
+
session_manager: Session manager
|
|
185
|
+
session_id: Current session ID
|
|
186
|
+
state: WorkflowState for variables access
|
|
187
|
+
workflow_name: Override workflow name (defaults to current)
|
|
188
|
+
include_closed: Include closed tasks (default: False)
|
|
189
|
+
output_as: Variable name to store result in
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Dict with tasks list and count
|
|
193
|
+
"""
|
|
194
|
+
from gobby.workflows.task_actions import get_workflow_tasks as _get_workflow_tasks
|
|
195
|
+
|
|
196
|
+
wf_name = workflow_name
|
|
197
|
+
if not wf_name and state.workflow_name:
|
|
198
|
+
wf_name = state.workflow_name
|
|
199
|
+
|
|
200
|
+
if not wf_name:
|
|
201
|
+
return {"error": "No workflow name specified"}
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
current_session = await asyncio.to_thread(session_manager.get, session_id)
|
|
205
|
+
project_id = current_session.project_id if current_session else None
|
|
206
|
+
|
|
207
|
+
tasks = await asyncio.to_thread(
|
|
208
|
+
_get_workflow_tasks,
|
|
209
|
+
db=db,
|
|
210
|
+
workflow_name=wf_name,
|
|
211
|
+
project_id=project_id,
|
|
212
|
+
include_closed=include_closed,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Convert to dicts for YAML/JSON serialization
|
|
216
|
+
tasks_data = [t.to_dict() for t in tasks]
|
|
217
|
+
|
|
218
|
+
# Store in variable if requested
|
|
219
|
+
if output_as:
|
|
220
|
+
if not state.variables:
|
|
221
|
+
state.variables = {}
|
|
222
|
+
state.variables[output_as] = tasks_data
|
|
223
|
+
|
|
224
|
+
# Also update task_list in state for workflow engine use
|
|
225
|
+
state.task_list = [{"id": t.id, "title": t.title, "status": t.status} for t in tasks]
|
|
226
|
+
|
|
227
|
+
return {"tasks": tasks_data, "count": len(tasks)}
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.error(f"get_workflow_tasks: Failed: {e}", exc_info=True)
|
|
230
|
+
return {"error": str(e)}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
async def update_workflow_task(
|
|
234
|
+
db: "DatabaseProtocol",
|
|
235
|
+
state: "WorkflowState",
|
|
236
|
+
task_id: str | None = None,
|
|
237
|
+
status: str | None = None,
|
|
238
|
+
verification: str | None = None,
|
|
239
|
+
validation_status: str | None = None,
|
|
240
|
+
validation_feedback: str | None = None,
|
|
241
|
+
) -> dict[str, Any]:
|
|
242
|
+
"""Update a task from workflow context.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
db: Database instance
|
|
246
|
+
state: WorkflowState for task_list access
|
|
247
|
+
task_id: ID of task to update (required)
|
|
248
|
+
status: New status
|
|
249
|
+
verification: Verification result
|
|
250
|
+
validation_status: Validation status
|
|
251
|
+
validation_feedback: Validation feedback
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Dict with updated task data
|
|
255
|
+
"""
|
|
256
|
+
from gobby.workflows.task_actions import update_task_from_workflow
|
|
257
|
+
|
|
258
|
+
tid = task_id
|
|
259
|
+
if not tid:
|
|
260
|
+
# Try to get from current_task_index in state
|
|
261
|
+
if state.task_list and state.current_task_index is not None:
|
|
262
|
+
idx = state.current_task_index
|
|
263
|
+
if 0 <= idx < len(state.task_list):
|
|
264
|
+
tid = state.task_list[idx].get("id")
|
|
265
|
+
|
|
266
|
+
if not tid:
|
|
267
|
+
return {"error": "No task_id specified"}
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
task = await asyncio.to_thread(
|
|
271
|
+
update_task_from_workflow,
|
|
272
|
+
db=db,
|
|
273
|
+
task_id=tid,
|
|
274
|
+
status=status,
|
|
275
|
+
verification=verification,
|
|
276
|
+
validation_status=validation_status,
|
|
277
|
+
validation_feedback=validation_feedback,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if task:
|
|
281
|
+
return {"updated": True, "task": task.to_dict()}
|
|
282
|
+
return {"updated": False, "error": "Task not found"}
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.error(f"update_workflow_task: Failed for task {tid}: {e}", exc_info=True)
|
|
285
|
+
return {"updated": False, "error": str(e)}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# --- ActionHandler-compatible wrappers ---
|
|
289
|
+
# These match the ActionHandler protocol: (context: ActionContext, **kwargs) -> dict | None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
async def handle_task_sync_import(context: Any, **kwargs: Any) -> dict[str, Any] | None:
|
|
293
|
+
"""ActionHandler wrapper for task_sync_import."""
|
|
294
|
+
return await task_sync_import(
|
|
295
|
+
task_sync_manager=context.task_sync_manager,
|
|
296
|
+
session_manager=context.session_manager,
|
|
297
|
+
session_id=context.session_id,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
async def handle_task_sync_export(context: Any, **kwargs: Any) -> dict[str, Any] | None:
|
|
302
|
+
"""ActionHandler wrapper for task_sync_export."""
|
|
303
|
+
return await task_sync_export(
|
|
304
|
+
task_sync_manager=context.task_sync_manager,
|
|
305
|
+
session_manager=context.session_manager,
|
|
306
|
+
session_id=context.session_id,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
async def handle_persist_tasks(context: Any, **kwargs: Any) -> dict[str, Any] | None:
|
|
311
|
+
"""ActionHandler wrapper for persist_tasks."""
|
|
312
|
+
return await persist_tasks(
|
|
313
|
+
db=context.db,
|
|
314
|
+
session_manager=context.session_manager,
|
|
315
|
+
session_id=context.session_id,
|
|
316
|
+
state=context.state,
|
|
317
|
+
tasks=kwargs.get("tasks"),
|
|
318
|
+
source=kwargs.get("source"),
|
|
319
|
+
workflow_name=kwargs.get("workflow_name"),
|
|
320
|
+
parent_task_id=kwargs.get("parent_task_id"),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
async def handle_get_workflow_tasks(context: Any, **kwargs: Any) -> dict[str, Any] | None:
|
|
325
|
+
"""ActionHandler wrapper for get_workflow_tasks."""
|
|
326
|
+
return await get_workflow_tasks(
|
|
327
|
+
db=context.db,
|
|
328
|
+
session_manager=context.session_manager,
|
|
329
|
+
session_id=context.session_id,
|
|
330
|
+
state=context.state,
|
|
331
|
+
workflow_name=kwargs.get("workflow_name"),
|
|
332
|
+
include_closed=kwargs.get("include_closed", False),
|
|
333
|
+
output_as=kwargs.get("as"),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
async def handle_update_workflow_task(context: Any, **kwargs: Any) -> dict[str, Any] | None:
|
|
338
|
+
"""ActionHandler wrapper for update_workflow_task."""
|
|
339
|
+
return await update_workflow_task(
|
|
340
|
+
db=context.db,
|
|
341
|
+
state=context.state,
|
|
342
|
+
task_id=kwargs.get("task_id"),
|
|
343
|
+
status=kwargs.get("status"),
|
|
344
|
+
verification=kwargs.get("verification"),
|
|
345
|
+
validation_status=kwargs.get("validation_status"),
|
|
346
|
+
validation_feedback=kwargs.get("validation_feedback"),
|
|
347
|
+
)
|
gobby/workflows/todo_actions.py
CHANGED
|
@@ -4,9 +4,13 @@ Extracted from actions.py as part of strangler fig decomposition.
|
|
|
4
4
|
These functions handle TODO.md file operations.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import asyncio
|
|
7
8
|
import logging
|
|
8
9
|
import os
|
|
9
|
-
from typing import Any
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from gobby.workflows.actions import ActionContext
|
|
10
14
|
|
|
11
15
|
logger = logging.getLogger(__name__)
|
|
12
16
|
|
|
@@ -82,3 +86,32 @@ def mark_todo_complete(
|
|
|
82
86
|
except Exception as e:
|
|
83
87
|
logger.error(f"mark_todo_complete: Failed: {e}")
|
|
84
88
|
return {"error": str(e)}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# --- ActionHandler-compatible wrappers ---
|
|
92
|
+
# These match the ActionHandler protocol: (context: ActionContext, **kwargs) -> dict | None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def handle_write_todos(context: "ActionContext", **kwargs: Any) -> dict[str, Any] | None:
|
|
96
|
+
"""ActionHandler wrapper for write_todos."""
|
|
97
|
+
return await asyncio.to_thread(
|
|
98
|
+
write_todos,
|
|
99
|
+
todos=kwargs.get("todos", []),
|
|
100
|
+
filename=kwargs.get("filename", "TODO.md"),
|
|
101
|
+
mode=kwargs.get("mode", "w"),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def handle_mark_todo_complete(
|
|
106
|
+
context: "ActionContext", **kwargs: Any
|
|
107
|
+
) -> dict[str, Any] | None:
|
|
108
|
+
"""ActionHandler wrapper for mark_todo_complete."""
|
|
109
|
+
todo_text = kwargs.get("todo_text")
|
|
110
|
+
if not todo_text:
|
|
111
|
+
return {"error": "Missing required parameter: todo_text"}
|
|
112
|
+
|
|
113
|
+
return await asyncio.to_thread(
|
|
114
|
+
mark_todo_complete,
|
|
115
|
+
todo_text,
|
|
116
|
+
kwargs.get("filename", "TODO.md"),
|
|
117
|
+
)
|