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,394 @@
|
|
|
1
|
+
"""Context injection and handoff workflow actions.
|
|
2
|
+
|
|
3
|
+
Extracted from actions.py as part of strangler fig decomposition.
|
|
4
|
+
These functions handle context injection, message injection, and handoff extraction.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from gobby.workflows.git_utils import get_git_status, get_recent_git_commits
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def inject_context(
|
|
20
|
+
session_manager: Any,
|
|
21
|
+
session_id: str,
|
|
22
|
+
state: Any,
|
|
23
|
+
template_engine: Any,
|
|
24
|
+
source: str | None = None,
|
|
25
|
+
template: str | None = None,
|
|
26
|
+
require: bool = False,
|
|
27
|
+
) -> dict[str, Any] | None:
|
|
28
|
+
"""Inject context from a source.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
session_manager: The session manager instance
|
|
32
|
+
session_id: Current session ID
|
|
33
|
+
state: WorkflowState instance
|
|
34
|
+
template_engine: Template engine for rendering
|
|
35
|
+
source: Source type (previous_session_summary, handoff, artifacts, etc.)
|
|
36
|
+
template: Optional template for rendering
|
|
37
|
+
require: If True, block session when no content found (default: False)
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Dict with inject_context key, blocking decision, or None
|
|
41
|
+
"""
|
|
42
|
+
# Validate required parameters
|
|
43
|
+
if session_manager is None:
|
|
44
|
+
logger.warning(f"inject_context: session_manager is None (session_id={session_id})")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
if state is None:
|
|
48
|
+
logger.warning(f"inject_context: state is None (session_id={session_id})")
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
if template_engine is None:
|
|
52
|
+
logger.warning(f"inject_context: template_engine is None (session_id={session_id})")
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
if not session_id:
|
|
56
|
+
logger.warning("inject_context: session_id is empty or None")
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
# Debug logging for troubleshooting
|
|
60
|
+
logger.debug(
|
|
61
|
+
f"inject_context called: source={source!r}, "
|
|
62
|
+
f"template_present={template is not None}, "
|
|
63
|
+
f"template_len={len(template) if template else 0}, "
|
|
64
|
+
f"session_id={session_id}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Support template-only injection (no source lookup needed)
|
|
68
|
+
condition_result = (not source) and bool(template)
|
|
69
|
+
logger.debug(
|
|
70
|
+
f"inject_context: not source={not source}, bool(template)={bool(template)}, "
|
|
71
|
+
f"condition_result={condition_result}"
|
|
72
|
+
)
|
|
73
|
+
if not source and template:
|
|
74
|
+
# Render static template directly
|
|
75
|
+
logger.debug("inject_context: entering template-only path")
|
|
76
|
+
render_context: dict[str, Any] = {
|
|
77
|
+
"session": session_manager.get(session_id),
|
|
78
|
+
"state": state,
|
|
79
|
+
"artifacts": state.artifacts if state else {},
|
|
80
|
+
"observations": state.observations if state else {},
|
|
81
|
+
}
|
|
82
|
+
rendered = template_engine.render(template, render_context)
|
|
83
|
+
logger.debug(f"inject_context: rendered template, len={len(rendered) if rendered else 0}")
|
|
84
|
+
if state:
|
|
85
|
+
state.context_injected = True
|
|
86
|
+
return {"inject_context": rendered}
|
|
87
|
+
|
|
88
|
+
if not source:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
content = ""
|
|
92
|
+
|
|
93
|
+
if source in ["previous_session_summary", "handoff"]:
|
|
94
|
+
current_session = session_manager.get(session_id)
|
|
95
|
+
if not current_session:
|
|
96
|
+
logger.warning(f"Session {session_id} not found")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
if current_session.parent_session_id:
|
|
100
|
+
parent = session_manager.get(current_session.parent_session_id)
|
|
101
|
+
if parent:
|
|
102
|
+
content = parent.summary_markdown
|
|
103
|
+
# Failback: try reading from file if database summary is empty
|
|
104
|
+
# This handles cases where daemon was unavailable during /clear
|
|
105
|
+
if not content and hasattr(parent, "external_id") and parent.external_id:
|
|
106
|
+
summary_dir = Path.home() / ".gobby" / "session_summaries"
|
|
107
|
+
if summary_dir.exists():
|
|
108
|
+
for summary_file in summary_dir.glob(f"session_*_{parent.external_id}.md"):
|
|
109
|
+
try:
|
|
110
|
+
content = summary_file.read_text()
|
|
111
|
+
logger.info(
|
|
112
|
+
f"Recovered summary from failback file for {parent.external_id}"
|
|
113
|
+
)
|
|
114
|
+
break
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.warning(f"Failed to read failback file {summary_file}: {e}")
|
|
117
|
+
|
|
118
|
+
elif source == "artifacts":
|
|
119
|
+
if state.artifacts:
|
|
120
|
+
lines = ["## Captured Artifacts"]
|
|
121
|
+
for name, path in state.artifacts.items():
|
|
122
|
+
lines.append(f"- {name}: {path}")
|
|
123
|
+
content = "\n".join(lines)
|
|
124
|
+
|
|
125
|
+
elif source == "observations":
|
|
126
|
+
if state.observations:
|
|
127
|
+
content = "## Observations\n" + json.dumps(state.observations, indent=2)
|
|
128
|
+
|
|
129
|
+
elif source == "workflow_state":
|
|
130
|
+
try:
|
|
131
|
+
state_dict = state.model_dump(exclude={"observations", "artifacts"})
|
|
132
|
+
except AttributeError:
|
|
133
|
+
state_dict = state.dict(exclude={"observations", "artifacts"})
|
|
134
|
+
content = "## Workflow State\n" + json.dumps(state_dict, indent=2, default=str)
|
|
135
|
+
|
|
136
|
+
elif source == "compact_handoff":
|
|
137
|
+
# Look at CURRENT session's compact_markdown (not parent)
|
|
138
|
+
# On compact, the same session continues - compact_markdown was saved to this session
|
|
139
|
+
# during pre_compact, so we read it from the current session itself.
|
|
140
|
+
current_session = session_manager.get(session_id)
|
|
141
|
+
logger.debug(
|
|
142
|
+
f"compact_handoff lookup: session_id={session_id}, "
|
|
143
|
+
f"compact_markdown exists: {bool(getattr(current_session, 'compact_markdown', None)) if current_session else False}"
|
|
144
|
+
)
|
|
145
|
+
if current_session and current_session.compact_markdown:
|
|
146
|
+
content = current_session.compact_markdown
|
|
147
|
+
logger.debug(
|
|
148
|
+
f"Loaded compact_markdown ({len(content)} chars) from current session {session_id}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if content:
|
|
152
|
+
if template:
|
|
153
|
+
render_context = {
|
|
154
|
+
"session": session_manager.get(session_id),
|
|
155
|
+
"state": state,
|
|
156
|
+
"artifacts": state.artifacts,
|
|
157
|
+
"observations": state.observations,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if source in ["previous_session_summary", "handoff"]:
|
|
161
|
+
render_context["summary"] = content
|
|
162
|
+
render_context["handoff"] = {"notes": content}
|
|
163
|
+
elif source == "artifacts":
|
|
164
|
+
render_context["artifacts_list"] = content
|
|
165
|
+
elif source == "observations":
|
|
166
|
+
render_context["observations_text"] = content
|
|
167
|
+
elif source == "workflow_state":
|
|
168
|
+
render_context["workflow_state_text"] = content
|
|
169
|
+
elif source == "compact_handoff":
|
|
170
|
+
# Pass content to template (like /clear does with summary)
|
|
171
|
+
render_context["handoff"] = content
|
|
172
|
+
|
|
173
|
+
content = template_engine.render(template, render_context)
|
|
174
|
+
|
|
175
|
+
state.context_injected = True
|
|
176
|
+
return {"inject_context": content}
|
|
177
|
+
|
|
178
|
+
# No content found - block if required
|
|
179
|
+
if require:
|
|
180
|
+
reason = f"Required handoff context not found (source={source})"
|
|
181
|
+
logger.warning(f"inject_context: {reason}")
|
|
182
|
+
return {"decision": "block", "reason": reason}
|
|
183
|
+
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def inject_message(
|
|
188
|
+
session_manager: Any,
|
|
189
|
+
session_id: str,
|
|
190
|
+
state: Any,
|
|
191
|
+
template_engine: Any,
|
|
192
|
+
content: str | None = None,
|
|
193
|
+
**extra_kwargs: Any,
|
|
194
|
+
) -> dict[str, Any] | None:
|
|
195
|
+
"""Inject a message to the user/assistant, rendering it as a template.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
session_manager: The session manager instance
|
|
199
|
+
session_id: Current session ID
|
|
200
|
+
state: WorkflowState instance
|
|
201
|
+
template_engine: Template engine for rendering
|
|
202
|
+
content: Template content to render
|
|
203
|
+
**extra_kwargs: Additional context for rendering
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Dict with inject_message key, or None
|
|
207
|
+
"""
|
|
208
|
+
if not content:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
render_context: dict[str, Any] = {
|
|
212
|
+
"session": session_manager.get(session_id),
|
|
213
|
+
"state": state,
|
|
214
|
+
"artifacts": state.artifacts,
|
|
215
|
+
"step_action_count": state.step_action_count,
|
|
216
|
+
"variables": state.variables or {},
|
|
217
|
+
}
|
|
218
|
+
render_context.update(extra_kwargs)
|
|
219
|
+
|
|
220
|
+
rendered_content = template_engine.render(content, render_context)
|
|
221
|
+
return {"inject_message": rendered_content}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def extract_handoff_context(
|
|
225
|
+
session_manager: Any,
|
|
226
|
+
session_id: str,
|
|
227
|
+
config: Any | None = None,
|
|
228
|
+
db: Any | None = None,
|
|
229
|
+
worktree_manager: Any | None = None,
|
|
230
|
+
) -> dict[str, Any] | None:
|
|
231
|
+
"""Extract handoff context from transcript and save to session.compact_markdown.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
session_manager: The session manager instance
|
|
235
|
+
session_id: Current session ID
|
|
236
|
+
config: Optional config with compact_handoff settings
|
|
237
|
+
db: Optional LocalDatabase instance for dependency injection
|
|
238
|
+
worktree_manager: Optional LocalWorktreeManager instance for dependency injection
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Dict with extraction result or error
|
|
242
|
+
"""
|
|
243
|
+
if config:
|
|
244
|
+
compact_config = getattr(config, "compact_handoff", None)
|
|
245
|
+
if compact_config and not compact_config.enabled:
|
|
246
|
+
return {"skipped": True, "reason": "compact_handoff disabled"}
|
|
247
|
+
|
|
248
|
+
current_session = session_manager.get(session_id)
|
|
249
|
+
if not current_session:
|
|
250
|
+
return {"error": "Session not found"}
|
|
251
|
+
|
|
252
|
+
transcript_path = getattr(current_session, "jsonl_path", None)
|
|
253
|
+
if not transcript_path:
|
|
254
|
+
return {"error": "No transcript path"}
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
from gobby.sessions.analyzer import TranscriptAnalyzer
|
|
258
|
+
|
|
259
|
+
path = Path(transcript_path)
|
|
260
|
+
if not path.exists():
|
|
261
|
+
return {"error": "Transcript file not found"}
|
|
262
|
+
|
|
263
|
+
turns = []
|
|
264
|
+
with open(path) as f:
|
|
265
|
+
for line in f:
|
|
266
|
+
if line.strip():
|
|
267
|
+
turns.append(json.loads(line))
|
|
268
|
+
|
|
269
|
+
analyzer = TranscriptAnalyzer()
|
|
270
|
+
handoff_ctx = analyzer.extract_handoff_context(turns, max_turns=100)
|
|
271
|
+
|
|
272
|
+
# Enrich with real-time git status
|
|
273
|
+
if not handoff_ctx.git_status:
|
|
274
|
+
handoff_ctx.git_status = get_git_status()
|
|
275
|
+
|
|
276
|
+
# Enrich with real git commits
|
|
277
|
+
real_commits = get_recent_git_commits()
|
|
278
|
+
if real_commits:
|
|
279
|
+
handoff_ctx.git_commits = real_commits
|
|
280
|
+
|
|
281
|
+
# Enrich with worktree context if session is in a worktree
|
|
282
|
+
try:
|
|
283
|
+
# Use injected worktree_manager, or create one from injected db
|
|
284
|
+
wt_manager = worktree_manager
|
|
285
|
+
if wt_manager is None and db is not None:
|
|
286
|
+
from gobby.storage.worktrees import LocalWorktreeManager
|
|
287
|
+
|
|
288
|
+
wt_manager = LocalWorktreeManager(db)
|
|
289
|
+
|
|
290
|
+
if wt_manager is not None:
|
|
291
|
+
worktrees = wt_manager.list(agent_session_id=session_id, limit=1)
|
|
292
|
+
if worktrees:
|
|
293
|
+
wt = worktrees[0]
|
|
294
|
+
handoff_ctx.active_worktree = {
|
|
295
|
+
"id": wt.id,
|
|
296
|
+
"branch_name": wt.branch_name,
|
|
297
|
+
"worktree_path": wt.worktree_path,
|
|
298
|
+
"base_branch": wt.base_branch,
|
|
299
|
+
"task_id": wt.task_id,
|
|
300
|
+
"status": wt.status,
|
|
301
|
+
}
|
|
302
|
+
else:
|
|
303
|
+
logger.debug("Skipping worktree enrichment: no worktree_manager or db provided")
|
|
304
|
+
except Exception as wt_err:
|
|
305
|
+
logger.debug(f"Failed to get worktree context: {wt_err}")
|
|
306
|
+
|
|
307
|
+
# Format as markdown (like /clear stores formatted summary)
|
|
308
|
+
markdown = format_handoff_as_markdown(handoff_ctx)
|
|
309
|
+
|
|
310
|
+
# Save to session.compact_markdown
|
|
311
|
+
session_manager.update_compact_markdown(session_id, markdown)
|
|
312
|
+
|
|
313
|
+
logger.debug(
|
|
314
|
+
f"Saved compact handoff markdown ({len(markdown)} chars) to session {session_id}"
|
|
315
|
+
)
|
|
316
|
+
return {"handoff_context_extracted": True, "markdown_length": len(markdown)}
|
|
317
|
+
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.error(f"extract_handoff_context: Failed: {e}")
|
|
320
|
+
return {"error": str(e)}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def format_handoff_as_markdown(ctx: Any, prompt_template: str | None = None) -> str:
|
|
324
|
+
"""Format HandoffContext as markdown for storage.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
ctx: HandoffContext with extracted session data
|
|
328
|
+
prompt_template: Optional custom template (unused, reserved for future)
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Formatted markdown string with all sections
|
|
332
|
+
"""
|
|
333
|
+
_ = prompt_template # Reserved for future template support
|
|
334
|
+
sections: list[str] = []
|
|
335
|
+
|
|
336
|
+
# Active task section
|
|
337
|
+
if ctx.active_gobby_task:
|
|
338
|
+
task = ctx.active_gobby_task
|
|
339
|
+
sections.append(
|
|
340
|
+
f"### Active Task\n"
|
|
341
|
+
f"**{task.get('title', 'Untitled')}** ({task.get('id', 'unknown')})\n"
|
|
342
|
+
f"Status: {task.get('status', 'unknown')}"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Worktree context section
|
|
346
|
+
if ctx.active_worktree:
|
|
347
|
+
wt = ctx.active_worktree
|
|
348
|
+
lines = ["### Worktree Context"]
|
|
349
|
+
lines.append(f"- **Branch**: `{wt.get('branch_name', 'unknown')}`")
|
|
350
|
+
lines.append(f"- **Path**: `{wt.get('worktree_path', 'unknown')}`")
|
|
351
|
+
lines.append(f"- **Base**: `{wt.get('base_branch', 'main')}`")
|
|
352
|
+
if wt.get("task_id"):
|
|
353
|
+
lines.append(f"- **Task**: {wt.get('task_id')}")
|
|
354
|
+
sections.append("\n".join(lines))
|
|
355
|
+
|
|
356
|
+
# Todo state section
|
|
357
|
+
if ctx.todo_state:
|
|
358
|
+
lines = ["### In-Progress Work"]
|
|
359
|
+
for todo in ctx.todo_state:
|
|
360
|
+
status = todo.get("status", "pending")
|
|
361
|
+
marker = "x" if status == "completed" else ">" if status == "in_progress" else " "
|
|
362
|
+
lines.append(f"- [{marker}] {todo.get('content', '')}")
|
|
363
|
+
sections.append("\n".join(lines))
|
|
364
|
+
|
|
365
|
+
# Git commits section
|
|
366
|
+
if ctx.git_commits:
|
|
367
|
+
lines = ["### Commits This Session"]
|
|
368
|
+
for commit in ctx.git_commits:
|
|
369
|
+
lines.append(f"- `{commit.get('hash', '')[:7]}` {commit.get('message', '')}")
|
|
370
|
+
sections.append("\n".join(lines))
|
|
371
|
+
|
|
372
|
+
# Git status section
|
|
373
|
+
if ctx.git_status:
|
|
374
|
+
sections.append(f"### Uncommitted Changes\n```\n{ctx.git_status}\n```")
|
|
375
|
+
|
|
376
|
+
# Files modified section
|
|
377
|
+
if ctx.files_modified:
|
|
378
|
+
lines = ["### Files Being Modified"]
|
|
379
|
+
for f in ctx.files_modified:
|
|
380
|
+
lines.append(f"- {f}")
|
|
381
|
+
sections.append("\n".join(lines))
|
|
382
|
+
|
|
383
|
+
# Initial goal section
|
|
384
|
+
if ctx.initial_goal:
|
|
385
|
+
sections.append(f"### Original Goal\n{ctx.initial_goal}")
|
|
386
|
+
|
|
387
|
+
# Recent activity section
|
|
388
|
+
if ctx.recent_activity:
|
|
389
|
+
lines = ["### Recent Activity"]
|
|
390
|
+
for activity in ctx.recent_activity[-5:]:
|
|
391
|
+
lines.append(f"- {activity}")
|
|
392
|
+
sections.append("\n".join(lines))
|
|
393
|
+
|
|
394
|
+
return "\n\n".join(sections)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from datetime import UTC, datetime
|
|
2
|
+
from typing import Any, Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
5
|
+
|
|
6
|
+
# --- Workflow Definition Models (YAML) ---
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WorkflowRule(BaseModel):
|
|
10
|
+
name: str | None = None
|
|
11
|
+
when: str
|
|
12
|
+
action: Literal["block", "allow", "require_approval", "warn"]
|
|
13
|
+
message: str | None = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WorkflowTransition(BaseModel):
|
|
17
|
+
to: str
|
|
18
|
+
when: str
|
|
19
|
+
on_transition: list[dict[str, Any]] = Field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WorkflowExitCondition(BaseModel):
|
|
23
|
+
type: str
|
|
24
|
+
|
|
25
|
+
# Other fields depend on type (e.g. pattern, prompt, variable)
|
|
26
|
+
model_config = ConfigDict(extra="allow")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PrematureStopHandler(BaseModel):
|
|
30
|
+
"""Handler for when an agent attempts to stop before task completion."""
|
|
31
|
+
|
|
32
|
+
action: Literal["guide_continuation", "block", "warn"] = "guide_continuation"
|
|
33
|
+
message: str = "Task has incomplete subtasks. Use suggest_next_task() to continue."
|
|
34
|
+
condition: str | None = None # Optional condition to check (e.g., task_tree_complete)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WorkflowStep(BaseModel):
|
|
38
|
+
name: str
|
|
39
|
+
description: str | None = None
|
|
40
|
+
|
|
41
|
+
on_enter: list[dict[str, Any]] = Field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
# "all" or list of tool names
|
|
44
|
+
allowed_tools: list[str] | Literal["all"] = Field(default="all")
|
|
45
|
+
blocked_tools: list[str] = Field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
rules: list[WorkflowRule] = Field(default_factory=list)
|
|
48
|
+
transitions: list[WorkflowTransition] = Field(default_factory=list)
|
|
49
|
+
exit_conditions: list[dict[str, Any]] = Field(default_factory=list) # flexible for now
|
|
50
|
+
|
|
51
|
+
on_exit: list[dict[str, Any]] = Field(default_factory=list)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class WorkflowDefinition(BaseModel):
|
|
55
|
+
name: str
|
|
56
|
+
description: str | None = None
|
|
57
|
+
version: str = "1.0"
|
|
58
|
+
type: Literal["lifecycle", "step"] = "step"
|
|
59
|
+
extends: str | None = None
|
|
60
|
+
|
|
61
|
+
@field_validator("version", mode="before")
|
|
62
|
+
@classmethod
|
|
63
|
+
def coerce_version_to_string(cls, v: Any) -> str:
|
|
64
|
+
"""Accept numeric versions (1.0, 2) and coerce to string."""
|
|
65
|
+
return str(v) if v is not None else "1.0"
|
|
66
|
+
|
|
67
|
+
settings: dict[str, Any] = Field(default_factory=dict)
|
|
68
|
+
variables: dict[str, Any] = Field(default_factory=dict)
|
|
69
|
+
|
|
70
|
+
steps: list[WorkflowStep] = Field(default_factory=list)
|
|
71
|
+
|
|
72
|
+
# Global triggers (on_session_start, etc.)
|
|
73
|
+
triggers: dict[str, list[dict[str, Any]]] = Field(default_factory=dict)
|
|
74
|
+
|
|
75
|
+
on_error: list[dict[str, Any]] = Field(default_factory=list)
|
|
76
|
+
|
|
77
|
+
# Handler for premature stop attempts (step workflows only)
|
|
78
|
+
# Triggered when agent tries to stop but exit_condition is not met
|
|
79
|
+
on_premature_stop: PrematureStopHandler | None = None
|
|
80
|
+
|
|
81
|
+
# Exit condition for the entire workflow (when this is true, workflow can end)
|
|
82
|
+
exit_condition: str | None = None
|
|
83
|
+
|
|
84
|
+
def get_step(self, step_name: str) -> WorkflowStep | None:
|
|
85
|
+
for s in self.steps:
|
|
86
|
+
if s.name == step_name:
|
|
87
|
+
return s
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# --- Workflow State Models (Runtime) ---
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class WorkflowState(BaseModel):
|
|
95
|
+
session_id: str
|
|
96
|
+
workflow_name: str
|
|
97
|
+
step: str
|
|
98
|
+
step_entered_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
99
|
+
step_action_count: int = 0
|
|
100
|
+
total_action_count: int = 0
|
|
101
|
+
|
|
102
|
+
artifacts: dict[str, str] = Field(default_factory=dict)
|
|
103
|
+
observations: list[dict[str, Any]] = Field(default_factory=list)
|
|
104
|
+
|
|
105
|
+
reflection_pending: bool = False
|
|
106
|
+
context_injected: bool = False
|
|
107
|
+
|
|
108
|
+
variables: dict[str, Any] = Field(default_factory=dict)
|
|
109
|
+
|
|
110
|
+
# Task decomposition state
|
|
111
|
+
task_list: list[dict[str, Any]] | None = None
|
|
112
|
+
current_task_index: int = 0
|
|
113
|
+
files_modified_this_task: int = 0
|
|
114
|
+
|
|
115
|
+
# Approval state for user_approval exit conditions
|
|
116
|
+
approval_pending: bool = False
|
|
117
|
+
approval_condition_id: str | None = None # Which condition is awaiting approval
|
|
118
|
+
approval_prompt: str | None = None # The prompt shown to user
|
|
119
|
+
approval_requested_at: datetime | None = None
|
|
120
|
+
approval_timeout_seconds: int | None = None # None = no timeout
|
|
121
|
+
|
|
122
|
+
# Escape hatch: temporarily disable enforcement
|
|
123
|
+
disabled: bool = False
|
|
124
|
+
disabled_reason: str | None = None
|
|
125
|
+
|
|
126
|
+
# Track initial step for reset functionality
|
|
127
|
+
initial_step: str | None = None
|
|
128
|
+
|
|
129
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
130
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|