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
|
@@ -4,6 +4,7 @@ Extracted from actions.py as part of strangler fig decomposition.
|
|
|
4
4
|
These functions handle file artifact capture and reading.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import asyncio
|
|
7
8
|
import glob
|
|
8
9
|
import logging
|
|
9
10
|
import os
|
|
@@ -101,3 +102,33 @@ def read_artifact(
|
|
|
101
102
|
except Exception as e:
|
|
102
103
|
logger.error(f"read_artifact: Failed to read {filepath}: {e}")
|
|
103
104
|
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# --- ActionHandler-compatible wrappers ---
|
|
108
|
+
# These match the ActionHandler protocol: (context: ActionContext, **kwargs) -> dict | None
|
|
109
|
+
|
|
110
|
+
if __name__ != "__main__":
|
|
111
|
+
from typing import TYPE_CHECKING
|
|
112
|
+
|
|
113
|
+
if TYPE_CHECKING:
|
|
114
|
+
from gobby.workflows.actions import ActionContext
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def handle_capture_artifact(context: "ActionContext", **kwargs: Any) -> dict[str, Any] | None:
|
|
118
|
+
"""ActionHandler wrapper for capture_artifact."""
|
|
119
|
+
return await asyncio.to_thread(
|
|
120
|
+
capture_artifact,
|
|
121
|
+
state=context.state,
|
|
122
|
+
pattern=kwargs.get("pattern"),
|
|
123
|
+
save_as=kwargs.get("as"),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def handle_read_artifact(context: "ActionContext", **kwargs: Any) -> dict[str, Any] | None:
|
|
128
|
+
"""ActionHandler wrapper for read_artifact."""
|
|
129
|
+
return await asyncio.to_thread(
|
|
130
|
+
read_artifact,
|
|
131
|
+
state=context.state,
|
|
132
|
+
pattern=kwargs.get("pattern"),
|
|
133
|
+
variable_name=kwargs.get("as"),
|
|
134
|
+
)
|
|
@@ -284,3 +284,14 @@ def get_progress_summary(
|
|
|
284
284
|
"last_event_at": (summary.last_event_at.isoformat() if summary.last_event_at else None),
|
|
285
285
|
"events_by_type": {k.value: v for k, v in summary.events_by_type.items()},
|
|
286
286
|
}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# --- ActionHandler-compatible wrappers ---
|
|
290
|
+
# These match the ActionHandler protocol: (context: ActionContext, **kwargs) -> dict | None
|
|
291
|
+
# Note: These handlers require executor access for progress_tracker and stuck_detector,
|
|
292
|
+
# so they are created as closures inside ActionExecutor._register_defaults().
|
|
293
|
+
|
|
294
|
+
# No wrapper functions are defined in this file. The actual handler implementations
|
|
295
|
+
# are closures created in ActionExecutor._register_defaults() which capture the
|
|
296
|
+
# executor's self.progress_tracker and self.stuck_detector references. See that
|
|
297
|
+
# method for the actual implementations and where these components are hooked up.
|
|
@@ -6,10 +6,14 @@ These functions handle context injection, message injection, and handoff extract
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import asyncio
|
|
9
10
|
import json
|
|
10
11
|
import logging
|
|
11
12
|
from pathlib import Path
|
|
12
|
-
from typing import Any
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from gobby.workflows.actions import ActionContext
|
|
13
17
|
|
|
14
18
|
from gobby.workflows.git_utils import get_git_status, get_recent_git_commits
|
|
15
19
|
|
|
@@ -304,6 +308,17 @@ def extract_handoff_context(
|
|
|
304
308
|
except Exception as wt_err:
|
|
305
309
|
logger.debug(f"Failed to get worktree context: {wt_err}")
|
|
306
310
|
|
|
311
|
+
# Add active skills from HookSkillManager
|
|
312
|
+
try:
|
|
313
|
+
from gobby.hooks.skill_manager import HookSkillManager
|
|
314
|
+
|
|
315
|
+
skill_manager = HookSkillManager()
|
|
316
|
+
core_skills = skill_manager.discover_core_skills()
|
|
317
|
+
always_apply_skills = [s.name for s in core_skills if s.is_always_apply()]
|
|
318
|
+
handoff_ctx.active_skills = always_apply_skills
|
|
319
|
+
except Exception as skill_err:
|
|
320
|
+
logger.debug(f"Failed to get active skills: {skill_err}")
|
|
321
|
+
|
|
307
322
|
# Format as markdown (like /clear stores formatted summary)
|
|
308
323
|
markdown = format_handoff_as_markdown(handoff_ctx)
|
|
309
324
|
|
|
@@ -320,6 +335,32 @@ def extract_handoff_context(
|
|
|
320
335
|
return {"error": str(e)}
|
|
321
336
|
|
|
322
337
|
|
|
338
|
+
def recommend_skills_for_task(task: dict[str, Any] | None) -> list[str]:
|
|
339
|
+
"""Recommend relevant skills based on task category.
|
|
340
|
+
|
|
341
|
+
Uses HookSkillManager to get skill recommendations based on the task's
|
|
342
|
+
category field. Returns always-apply skills if no category is set.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
task: Task dict with optional 'category' field, or None.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
List of recommended skill names for this task.
|
|
349
|
+
"""
|
|
350
|
+
if task is None:
|
|
351
|
+
return []
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
from gobby.hooks.skill_manager import HookSkillManager
|
|
355
|
+
|
|
356
|
+
manager = HookSkillManager()
|
|
357
|
+
category = task.get("category")
|
|
358
|
+
return manager.recommend_skills(category=category)
|
|
359
|
+
except Exception as e:
|
|
360
|
+
logger.debug(f"Failed to recommend skills: {e}")
|
|
361
|
+
return []
|
|
362
|
+
|
|
363
|
+
|
|
323
364
|
def format_handoff_as_markdown(ctx: Any, prompt_template: str | None = None) -> str:
|
|
324
365
|
"""Format HandoffContext as markdown for storage.
|
|
325
366
|
|
|
@@ -391,4 +432,55 @@ def format_handoff_as_markdown(ctx: Any, prompt_template: str | None = None) ->
|
|
|
391
432
|
lines.append(f"- {activity}")
|
|
392
433
|
sections.append("\n".join(lines))
|
|
393
434
|
|
|
435
|
+
# Active skills section
|
|
436
|
+
if hasattr(ctx, "active_skills") and ctx.active_skills:
|
|
437
|
+
lines = ["### Active Skills"]
|
|
438
|
+
lines.append(f"Skills available: {', '.join(ctx.active_skills)}")
|
|
439
|
+
sections.append("\n".join(lines))
|
|
440
|
+
|
|
394
441
|
return "\n\n".join(sections)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# --- ActionHandler-compatible wrappers ---
|
|
445
|
+
# These match the ActionHandler protocol: (context: ActionContext, **kwargs) -> dict | None
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
async def handle_inject_context(context: ActionContext, **kwargs: Any) -> dict[str, Any] | None:
|
|
449
|
+
"""ActionHandler wrapper for inject_context."""
|
|
450
|
+
return await asyncio.to_thread(
|
|
451
|
+
inject_context,
|
|
452
|
+
session_manager=context.session_manager,
|
|
453
|
+
session_id=context.session_id,
|
|
454
|
+
state=context.state,
|
|
455
|
+
template_engine=context.template_engine,
|
|
456
|
+
source=kwargs.get("source"),
|
|
457
|
+
template=kwargs.get("template"),
|
|
458
|
+
require=kwargs.get("require", False),
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
async def handle_inject_message(context: ActionContext, **kwargs: Any) -> dict[str, Any] | None:
|
|
463
|
+
"""ActionHandler wrapper for inject_message."""
|
|
464
|
+
return await asyncio.to_thread(
|
|
465
|
+
inject_message,
|
|
466
|
+
session_manager=context.session_manager,
|
|
467
|
+
session_id=context.session_id,
|
|
468
|
+
state=context.state,
|
|
469
|
+
template_engine=context.template_engine,
|
|
470
|
+
content=kwargs.get("content"),
|
|
471
|
+
**{k: v for k, v in kwargs.items() if k != "content"},
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
async def handle_extract_handoff_context(
|
|
476
|
+
context: ActionContext, **kwargs: Any
|
|
477
|
+
) -> dict[str, Any] | None:
|
|
478
|
+
"""ActionHandler wrapper for extract_handoff_context."""
|
|
479
|
+
return await asyncio.to_thread(
|
|
480
|
+
extract_handoff_context,
|
|
481
|
+
session_manager=context.session_manager,
|
|
482
|
+
session_id=context.session_id,
|
|
483
|
+
config=context.config,
|
|
484
|
+
db=context.db,
|
|
485
|
+
worktree_manager=kwargs.get("worktree_manager"),
|
|
486
|
+
)
|
|
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING
|
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from gobby.hooks.events import HookEvent
|
|
14
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
14
15
|
from gobby.tasks.session_tasks import SessionTaskManager
|
|
15
16
|
|
|
16
17
|
from .definitions import WorkflowState
|
|
@@ -22,6 +23,7 @@ def detect_task_claim(
|
|
|
22
23
|
event: "HookEvent",
|
|
23
24
|
state: "WorkflowState",
|
|
24
25
|
session_task_manager: "SessionTaskManager | None" = None,
|
|
26
|
+
task_manager: "LocalTaskManager | None" = None,
|
|
25
27
|
) -> None:
|
|
26
28
|
"""Detect gobby-tasks calls that claim or release a task for this session.
|
|
27
29
|
|
|
@@ -44,7 +46,8 @@ def detect_task_claim(
|
|
|
44
46
|
|
|
45
47
|
tool_name = event.data.get("tool_name", "")
|
|
46
48
|
tool_input = event.data.get("tool_input", {}) or {}
|
|
47
|
-
|
|
49
|
+
# Claude Code sends "tool_result", but we also check "tool_output" for compatibility
|
|
50
|
+
tool_output = event.data.get("tool_result") or event.data.get("tool_output") or {}
|
|
48
51
|
|
|
49
52
|
# Check if this is a gobby-tasks call via MCP proxy
|
|
50
53
|
# Tool name could be "call_tool" (from legacy) or "mcp__gobby__call_tool" (direct)
|
|
@@ -58,7 +61,34 @@ def detect_task_claim(
|
|
|
58
61
|
|
|
59
62
|
# Check inner tool name
|
|
60
63
|
inner_tool_name = tool_input.get("tool_name", "")
|
|
61
|
-
|
|
64
|
+
|
|
65
|
+
# Handle close_task - clears task_claimed when task is closed
|
|
66
|
+
# Note: Claude Code doesn't include tool_result in post-tool-use hooks, so for CC
|
|
67
|
+
# the workflow state is updated directly in the MCP proxy's close_task function.
|
|
68
|
+
# This detection provides a fallback for CLIs that do report tool results (Gemini/Codex).
|
|
69
|
+
if inner_tool_name == "close_task":
|
|
70
|
+
tool_output = event.data.get("tool_result") or event.data.get("tool_output") or {}
|
|
71
|
+
|
|
72
|
+
# If no tool output, skip - can't verify success
|
|
73
|
+
# The MCP proxy's close_task handles state clearing for successful closes
|
|
74
|
+
if not tool_output:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
# Check if close succeeded (not an error)
|
|
78
|
+
if isinstance(tool_output, dict):
|
|
79
|
+
if tool_output.get("error") or tool_output.get("status") == "error":
|
|
80
|
+
return
|
|
81
|
+
result = tool_output.get("result", {})
|
|
82
|
+
if isinstance(result, dict) and result.get("error"):
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# Clear task_claimed on successful close
|
|
86
|
+
state.variables["task_claimed"] = False
|
|
87
|
+
state.variables["claimed_task_id"] = None
|
|
88
|
+
logger.info(f"Session {state.session_id}: task_claimed=False (detected close_task success)")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
if inner_tool_name not in ("create_task", "update_task", "claim_task"):
|
|
62
92
|
return
|
|
63
93
|
|
|
64
94
|
# For update_task, only count if status is being set to in_progress
|
|
@@ -66,11 +96,9 @@ def detect_task_claim(
|
|
|
66
96
|
arguments = tool_input.get("arguments", {}) or {}
|
|
67
97
|
if arguments.get("status") != "in_progress":
|
|
68
98
|
return
|
|
99
|
+
# claim_task always counts (it sets status to in_progress internally)
|
|
69
100
|
|
|
70
|
-
#
|
|
71
|
-
is_close_task = inner_tool_name == "close_task"
|
|
72
|
-
|
|
73
|
-
# Check if the call succeeded (not an error)
|
|
101
|
+
# Check if the call succeeded (not an error) - for non-close_task operations
|
|
74
102
|
# tool_output structure varies, but errors typically have "error" key
|
|
75
103
|
# or the MCP response has "status": "error"
|
|
76
104
|
if isinstance(tool_output, dict):
|
|
@@ -81,35 +109,26 @@ def detect_task_claim(
|
|
|
81
109
|
if isinstance(result, dict) and result.get("error"):
|
|
82
110
|
return
|
|
83
111
|
|
|
84
|
-
# Handle close_task - clear the claim only if closing the claimed task
|
|
85
|
-
if is_close_task:
|
|
86
|
-
arguments = tool_input.get("arguments", {}) or {}
|
|
87
|
-
closed_task_id = arguments.get("task_id")
|
|
88
|
-
claimed_task_id = state.variables.get("claimed_task_id")
|
|
89
|
-
|
|
90
|
-
# Only clear task_claimed if we're closing the task that was claimed
|
|
91
|
-
if closed_task_id and claimed_task_id and closed_task_id == claimed_task_id:
|
|
92
|
-
state.variables["task_claimed"] = False
|
|
93
|
-
state.variables["claimed_task_id"] = None
|
|
94
|
-
logger.info(
|
|
95
|
-
f"Session {state.session_id}: task_claimed=False "
|
|
96
|
-
f"(claimed task {closed_task_id} closed via close_task)"
|
|
97
|
-
)
|
|
98
|
-
else:
|
|
99
|
-
logger.debug(
|
|
100
|
-
f"Session {state.session_id}: close_task for {closed_task_id} "
|
|
101
|
-
f"(claimed: {claimed_task_id}) - not clearing task_claimed"
|
|
102
|
-
)
|
|
103
|
-
return
|
|
104
|
-
|
|
105
112
|
# Extract task_id based on tool type
|
|
106
113
|
arguments = tool_input.get("arguments", {}) or {}
|
|
107
|
-
if inner_tool_name
|
|
114
|
+
if inner_tool_name in ("update_task", "claim_task"):
|
|
108
115
|
task_id = arguments.get("task_id")
|
|
116
|
+
# Resolve to UUID for consistent comparison with close_task
|
|
117
|
+
if task_id and task_manager:
|
|
118
|
+
try:
|
|
119
|
+
task = task_manager.get_task(task_id)
|
|
120
|
+
if task:
|
|
121
|
+
task_id = task.id # Use UUID
|
|
122
|
+
except Exception: # nosec B110 - best effort resolution, keep original if fails
|
|
123
|
+
pass
|
|
109
124
|
elif inner_tool_name == "create_task":
|
|
110
125
|
# For create_task, the id is in the result
|
|
111
126
|
result = tool_output.get("result", {}) if isinstance(tool_output, dict) else {}
|
|
112
127
|
task_id = result.get("id") if isinstance(result, dict) else None
|
|
128
|
+
# Skip if we can't get the task ID (e.g., Claude Code doesn't include tool results)
|
|
129
|
+
# The MCP tool itself handles state updates in this case via _crud.py
|
|
130
|
+
if not task_id:
|
|
131
|
+
return
|
|
113
132
|
else:
|
|
114
133
|
task_id = None
|
|
115
134
|
|
|
@@ -121,8 +140,8 @@ def detect_task_claim(
|
|
|
121
140
|
f"(via {inner_tool_name})"
|
|
122
141
|
)
|
|
123
142
|
|
|
124
|
-
# Auto-link task to session when
|
|
125
|
-
if inner_tool_name
|
|
143
|
+
# Auto-link task to session when claiming a task
|
|
144
|
+
if inner_tool_name in ("update_task", "claim_task"):
|
|
126
145
|
arguments = tool_input.get("arguments", {}) or {}
|
|
127
146
|
task_id = arguments.get("task_id")
|
|
128
147
|
if task_id and session_task_manager:
|
|
@@ -159,6 +178,70 @@ def detect_plan_mode(event: "HookEvent", state: "WorkflowState") -> None:
|
|
|
159
178
|
logger.info(f"Session {state.session_id}: plan_mode=False (exited plan mode)")
|
|
160
179
|
|
|
161
180
|
|
|
181
|
+
def detect_plan_mode_from_context(event: "HookEvent", state: "WorkflowState") -> None:
|
|
182
|
+
"""Detect plan mode from system reminders injected by Claude Code.
|
|
183
|
+
|
|
184
|
+
Claude Code injects system reminders like "Plan mode is active" when the user
|
|
185
|
+
enters plan mode via the UI (not via the EnterPlanMode tool). This function
|
|
186
|
+
detects those reminders and sets the plan_mode variable accordingly.
|
|
187
|
+
|
|
188
|
+
IMPORTANT: Only matches indicators within <system-reminder> tags to avoid
|
|
189
|
+
false positives from handoff context or user messages that mention plan mode.
|
|
190
|
+
|
|
191
|
+
This complements detect_plan_mode() which only catches programmatic tool calls.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
event: The BEFORE_AGENT hook event (contains user prompt with system reminders)
|
|
195
|
+
state: Current workflow state (modified in place)
|
|
196
|
+
"""
|
|
197
|
+
if not event.data:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
# Check for plan mode system reminder in the prompt
|
|
201
|
+
prompt = event.data.get("prompt", "") or ""
|
|
202
|
+
|
|
203
|
+
# Extract only content within <system-reminder> tags to avoid false positives
|
|
204
|
+
# from handoff context or user messages mentioning plan mode
|
|
205
|
+
import re
|
|
206
|
+
|
|
207
|
+
system_reminders = re.findall(r"<system-reminder>(.*?)</system-reminder>", prompt, re.DOTALL)
|
|
208
|
+
reminder_text = " ".join(system_reminders)
|
|
209
|
+
|
|
210
|
+
# Claude Code injects these phrases in system reminders when plan mode is active
|
|
211
|
+
plan_mode_indicators = [
|
|
212
|
+
"Plan mode is active",
|
|
213
|
+
"Plan mode still active",
|
|
214
|
+
"You are in plan mode",
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
# Check if plan mode is indicated in system reminders only
|
|
218
|
+
for indicator in plan_mode_indicators:
|
|
219
|
+
if indicator in reminder_text:
|
|
220
|
+
if not state.variables.get("plan_mode"):
|
|
221
|
+
state.variables["plan_mode"] = True
|
|
222
|
+
logger.info(
|
|
223
|
+
f"Session {state.session_id}: plan_mode=True "
|
|
224
|
+
f"(detected from system reminder: '{indicator}')"
|
|
225
|
+
)
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
# Detect exit from plan mode (also only in system reminders)
|
|
229
|
+
exit_indicators = [
|
|
230
|
+
"Exited Plan Mode",
|
|
231
|
+
"Plan mode exited",
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
for indicator in exit_indicators:
|
|
235
|
+
if indicator in reminder_text:
|
|
236
|
+
if state.variables.get("plan_mode"):
|
|
237
|
+
state.variables["plan_mode"] = False
|
|
238
|
+
logger.info(
|
|
239
|
+
f"Session {state.session_id}: plan_mode=False "
|
|
240
|
+
f"(detected from system reminder: '{indicator}')"
|
|
241
|
+
)
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
|
|
162
245
|
def detect_mcp_call(event: "HookEvent", state: "WorkflowState") -> None:
|
|
163
246
|
"""Track MCP tool calls by server/tool for workflow conditions.
|
|
164
247
|
|
|
@@ -180,7 +263,8 @@ def detect_mcp_call(event: "HookEvent", state: "WorkflowState") -> None:
|
|
|
180
263
|
|
|
181
264
|
tool_name = event.data.get("tool_name", "")
|
|
182
265
|
tool_input = event.data.get("tool_input", {}) or {}
|
|
183
|
-
|
|
266
|
+
# Claude Code sends "tool_result", but we also check "tool_output" for compatibility
|
|
267
|
+
tool_output = event.data.get("tool_result") or event.data.get("tool_output") or {}
|
|
184
268
|
|
|
185
269
|
# Check for MCP proxy call
|
|
186
270
|
if tool_name not in ("call_tool", "mcp__gobby__call_tool"):
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Task enforcement actions for workflow engine.
|
|
2
|
+
|
|
3
|
+
This package provides actions that enforce task tracking before allowing
|
|
4
|
+
certain tools, and enforce task completion before allowing agent to stop.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from gobby.workflows.enforcement.blocking import block_tools
|
|
8
|
+
from gobby.workflows.enforcement.commit_policy import (
|
|
9
|
+
capture_baseline_dirty_files,
|
|
10
|
+
require_commit_before_stop,
|
|
11
|
+
require_task_review_or_close_before_stop,
|
|
12
|
+
)
|
|
13
|
+
from gobby.workflows.enforcement.handlers import (
|
|
14
|
+
handle_block_tools,
|
|
15
|
+
handle_capture_baseline_dirty_files,
|
|
16
|
+
handle_require_active_task,
|
|
17
|
+
handle_require_commit_before_stop,
|
|
18
|
+
handle_require_task_complete,
|
|
19
|
+
handle_require_task_review_or_close_before_stop,
|
|
20
|
+
handle_validate_session_task_scope,
|
|
21
|
+
)
|
|
22
|
+
from gobby.workflows.enforcement.task_policy import (
|
|
23
|
+
require_active_task,
|
|
24
|
+
require_task_complete,
|
|
25
|
+
validate_session_task_scope,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
# Blocking
|
|
30
|
+
"block_tools",
|
|
31
|
+
# Commit policy
|
|
32
|
+
"capture_baseline_dirty_files",
|
|
33
|
+
"require_commit_before_stop",
|
|
34
|
+
"require_task_review_or_close_before_stop",
|
|
35
|
+
# Task policy
|
|
36
|
+
"require_active_task",
|
|
37
|
+
"require_task_complete",
|
|
38
|
+
"validate_session_task_scope",
|
|
39
|
+
# Handlers
|
|
40
|
+
"handle_block_tools",
|
|
41
|
+
"handle_capture_baseline_dirty_files",
|
|
42
|
+
"handle_require_active_task",
|
|
43
|
+
"handle_require_commit_before_stop",
|
|
44
|
+
"handle_require_task_complete",
|
|
45
|
+
"handle_require_task_review_or_close_before_stop",
|
|
46
|
+
"handle_validate_session_task_scope",
|
|
47
|
+
]
|