gobby 0.2.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/tasks/expansion.py
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task expansion module.
|
|
3
|
+
|
|
4
|
+
Handles breaking down high-level tasks into smaller, actionable subtasks
|
|
5
|
+
using LLM providers with structured JSON output.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from gobby.config.app import ProjectVerificationConfig, TaskExpansionConfig
|
|
16
|
+
from gobby.llm import LLMService
|
|
17
|
+
from gobby.storage.task_dependencies import TaskDependencyManager
|
|
18
|
+
from gobby.storage.tasks import LocalTaskManager, Task
|
|
19
|
+
from gobby.tasks.context import ExpansionContext, ExpansionContextGatherer
|
|
20
|
+
from gobby.tasks.criteria import PatternCriteriaInjector
|
|
21
|
+
from gobby.tasks.prompts.expand import ExpansionPromptBuilder
|
|
22
|
+
from gobby.utils.json_helpers import extract_json_from_text
|
|
23
|
+
from gobby.utils.project_context import get_verification_config
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class SubtaskSpec:
|
|
30
|
+
"""Parsed subtask specification from LLM output."""
|
|
31
|
+
|
|
32
|
+
title: str
|
|
33
|
+
description: str | None = None
|
|
34
|
+
priority: int = 2
|
|
35
|
+
task_type: str = "task"
|
|
36
|
+
category: str | None = None
|
|
37
|
+
validation: str | None = None # Acceptance criteria from LLM
|
|
38
|
+
depends_on: list[int] | None = None
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
"""Validate and normalize category after initialization."""
|
|
42
|
+
if self.category:
|
|
43
|
+
from gobby.storage.tasks import validate_category
|
|
44
|
+
|
|
45
|
+
self.category = validate_category(self.category)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TaskExpander:
|
|
49
|
+
"""Expands tasks into subtasks using LLM and context."""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
config: TaskExpansionConfig,
|
|
54
|
+
llm_service: LLMService,
|
|
55
|
+
task_manager: LocalTaskManager,
|
|
56
|
+
mcp_manager: Any | None = None,
|
|
57
|
+
verification_config: ProjectVerificationConfig | None = None,
|
|
58
|
+
):
|
|
59
|
+
self.config = config
|
|
60
|
+
self.llm_service = llm_service
|
|
61
|
+
self.task_manager = task_manager
|
|
62
|
+
self.mcp_manager = mcp_manager
|
|
63
|
+
self.context_gatherer = ExpansionContextGatherer(
|
|
64
|
+
task_manager=task_manager,
|
|
65
|
+
llm_service=llm_service,
|
|
66
|
+
config=config,
|
|
67
|
+
mcp_manager=mcp_manager,
|
|
68
|
+
)
|
|
69
|
+
self.prompt_builder = ExpansionPromptBuilder(config)
|
|
70
|
+
|
|
71
|
+
# Initialize pattern criteria injector
|
|
72
|
+
# Try to get verification config from project if not provided
|
|
73
|
+
if verification_config is None:
|
|
74
|
+
verification_config = get_verification_config()
|
|
75
|
+
self.criteria_injector = PatternCriteriaInjector(
|
|
76
|
+
pattern_config=config.pattern_criteria,
|
|
77
|
+
verification_config=verification_config,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def _resolve_tdd_mode(self, session_id: str | None, task_type: str | None = None) -> bool:
|
|
81
|
+
"""Resolve tdd_mode with cascading precedence.
|
|
82
|
+
|
|
83
|
+
Order: task_type override > step workflow > lifecycle workflow > config.yaml > pydantic default
|
|
84
|
+
|
|
85
|
+
Epic tasks never use TDD mode since their closing condition is
|
|
86
|
+
'all children are closed', not test-based verification.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
session_id: Session ID to resolve TDD mode from workflow state
|
|
90
|
+
task_type: Task type - epics always disable TDD mode
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if TDD mode is enabled, False otherwise
|
|
94
|
+
"""
|
|
95
|
+
# Epics never use TDD mode
|
|
96
|
+
if task_type == "epic":
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
if session_id:
|
|
100
|
+
try:
|
|
101
|
+
from gobby.workflows.state_manager import WorkflowStateManager
|
|
102
|
+
|
|
103
|
+
state_manager = WorkflowStateManager(self.task_manager.db)
|
|
104
|
+
state = state_manager.get_state(session_id)
|
|
105
|
+
if state and state.variables and "tdd_mode" in state.variables:
|
|
106
|
+
return bool(state.variables["tdd_mode"])
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.debug(f"Failed to resolve tdd_mode from workflow state: {e}")
|
|
109
|
+
|
|
110
|
+
# Fall back to config (includes pydantic default)
|
|
111
|
+
return self.config.tdd_mode
|
|
112
|
+
|
|
113
|
+
async def expand_task(
|
|
114
|
+
self,
|
|
115
|
+
task_id: str,
|
|
116
|
+
title: str,
|
|
117
|
+
description: str | None = None,
|
|
118
|
+
context: str | None = None,
|
|
119
|
+
enable_web_research: bool = False,
|
|
120
|
+
enable_code_context: bool = True,
|
|
121
|
+
session_id: str | None = None,
|
|
122
|
+
) -> dict[str, Any]:
|
|
123
|
+
"""
|
|
124
|
+
Expand a task into subtasks using structured JSON output.
|
|
125
|
+
|
|
126
|
+
The LLM returns a JSON object with subtask specifications, which are
|
|
127
|
+
then parsed and created as tasks with proper dependency wiring.
|
|
128
|
+
|
|
129
|
+
Note: This creates plain subtasks only. To apply TDD structure
|
|
130
|
+
(test/implement/refactor triplets), use the apply_tdd command
|
|
131
|
+
separately after expansion.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
task_id: ID of the task to expand
|
|
135
|
+
title: Task title
|
|
136
|
+
description: Task description
|
|
137
|
+
context: Additional context for expansion
|
|
138
|
+
enable_web_research: Whether to enable web research (default: False)
|
|
139
|
+
enable_code_context: Whether to enable code context gathering (default: True)
|
|
140
|
+
session_id: Session ID for TDD mode resolution (optional)
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Dictionary with:
|
|
144
|
+
- subtask_ids: List of created subtask IDs
|
|
145
|
+
- subtask_count: Number of subtasks created
|
|
146
|
+
- raw_response: The raw LLM response (for debugging)
|
|
147
|
+
"""
|
|
148
|
+
if not self.config.enabled:
|
|
149
|
+
logger.info("Task expansion disabled, skipping")
|
|
150
|
+
return {
|
|
151
|
+
"subtask_ids": [],
|
|
152
|
+
"subtask_count": 0,
|
|
153
|
+
"raw_response": "Expansion disabled",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
logger.info(f"Expanding task {task_id}: {title}")
|
|
157
|
+
|
|
158
|
+
# Apply overall timeout for entire expansion
|
|
159
|
+
timeout_seconds = self.config.timeout
|
|
160
|
+
try:
|
|
161
|
+
async with asyncio.timeout(timeout_seconds):
|
|
162
|
+
return await self._expand_task_impl(
|
|
163
|
+
task_id=task_id,
|
|
164
|
+
title=title,
|
|
165
|
+
description=description,
|
|
166
|
+
context=context,
|
|
167
|
+
enable_web_research=enable_web_research,
|
|
168
|
+
enable_code_context=enable_code_context,
|
|
169
|
+
session_id=session_id,
|
|
170
|
+
)
|
|
171
|
+
except TimeoutError:
|
|
172
|
+
error_msg = (
|
|
173
|
+
f"Task expansion timed out after {timeout_seconds} seconds. "
|
|
174
|
+
f"Consider increasing task_expansion.timeout in config or simplifying the task."
|
|
175
|
+
)
|
|
176
|
+
logger.error(f"Expansion timeout for {task_id}: {error_msg}")
|
|
177
|
+
return {
|
|
178
|
+
"error": error_msg,
|
|
179
|
+
"subtask_ids": [],
|
|
180
|
+
"subtask_count": 0,
|
|
181
|
+
"timeout": True,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async def _expand_task_impl(
|
|
185
|
+
self,
|
|
186
|
+
task_id: str,
|
|
187
|
+
title: str,
|
|
188
|
+
description: str | None = None,
|
|
189
|
+
context: str | None = None,
|
|
190
|
+
enable_web_research: bool = False,
|
|
191
|
+
enable_code_context: bool = True,
|
|
192
|
+
session_id: str | None = None,
|
|
193
|
+
) -> dict[str, Any]:
|
|
194
|
+
"""Internal implementation of expand_task (called within timeout context)."""
|
|
195
|
+
# Gather enhanced context
|
|
196
|
+
task_obj = self.task_manager.get_task(task_id)
|
|
197
|
+
if not task_obj:
|
|
198
|
+
logger.warning(f"Task {task_id} not found for context gathering, using basic info")
|
|
199
|
+
task_obj = Task(
|
|
200
|
+
id=task_id,
|
|
201
|
+
project_id="unknown",
|
|
202
|
+
title=title,
|
|
203
|
+
status="open",
|
|
204
|
+
priority=2,
|
|
205
|
+
task_type="task",
|
|
206
|
+
created_at="",
|
|
207
|
+
updated_at="",
|
|
208
|
+
description=description,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
expansion_ctx = await self.context_gatherer.gather_context(
|
|
212
|
+
task_obj,
|
|
213
|
+
enable_web_research=enable_web_research,
|
|
214
|
+
enable_code_context=enable_code_context,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Inject pattern-specific criteria based on task labels and description
|
|
218
|
+
pattern_criteria = self.criteria_injector.inject(
|
|
219
|
+
task=task_obj,
|
|
220
|
+
context=expansion_ctx,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Combine user context with pattern criteria if detected
|
|
224
|
+
combined_instructions = context or ""
|
|
225
|
+
if pattern_criteria:
|
|
226
|
+
logger.info(f"Detected patterns for {task_id}, adding pattern-specific criteria")
|
|
227
|
+
if combined_instructions:
|
|
228
|
+
combined_instructions += f"\n\n{pattern_criteria}"
|
|
229
|
+
else:
|
|
230
|
+
combined_instructions = pattern_criteria
|
|
231
|
+
|
|
232
|
+
# Build prompt using builder
|
|
233
|
+
prompt = self.prompt_builder.build_user_prompt(
|
|
234
|
+
task=task_obj,
|
|
235
|
+
context=expansion_ctx,
|
|
236
|
+
user_instructions=combined_instructions if combined_instructions else None,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
# Get provider and generate text response
|
|
241
|
+
provider = self.llm_service.get_provider(self.config.provider)
|
|
242
|
+
|
|
243
|
+
# Resolve TDD mode from session workflow state or config
|
|
244
|
+
# Epics never use TDD mode
|
|
245
|
+
tdd_mode = self._resolve_tdd_mode(session_id, task_obj.task_type)
|
|
246
|
+
|
|
247
|
+
# Note: TDD transformation is applied separately via apply_tdd command.
|
|
248
|
+
# The expand_task only creates plain subtasks.
|
|
249
|
+
response = await provider.generate_text(
|
|
250
|
+
prompt=prompt,
|
|
251
|
+
system_prompt=self.prompt_builder.get_system_prompt(tdd_mode=tdd_mode),
|
|
252
|
+
model=self.config.model,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
logger.debug(f"LLM response (first 500 chars): {response[:500]}")
|
|
256
|
+
|
|
257
|
+
# Parse JSON from response
|
|
258
|
+
subtask_specs = self._parse_subtasks(response)
|
|
259
|
+
logger.debug(f"Parsed {len(subtask_specs)} subtask specs")
|
|
260
|
+
|
|
261
|
+
if not subtask_specs:
|
|
262
|
+
logger.warning(f"No subtasks parsed from response for {task_id}")
|
|
263
|
+
return {
|
|
264
|
+
"subtask_ids": [],
|
|
265
|
+
"subtask_count": 0,
|
|
266
|
+
"raw_response": response,
|
|
267
|
+
"error": "No subtasks found in response",
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# Create tasks with dependency wiring and precise criteria
|
|
271
|
+
# Note: TDD transformation is done separately via apply_tdd command
|
|
272
|
+
subtask_ids = await self._create_subtasks(
|
|
273
|
+
parent_task_id=task_id,
|
|
274
|
+
project_id=task_obj.project_id,
|
|
275
|
+
subtask_specs=subtask_specs,
|
|
276
|
+
expansion_context=expansion_ctx,
|
|
277
|
+
parent_labels=task_obj.labels or [],
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Save expansion context to the parent task for audit/reuse
|
|
281
|
+
self._save_expansion_context(task_id, expansion_ctx)
|
|
282
|
+
|
|
283
|
+
logger.info(f"Expansion complete for {task_id}: created {len(subtask_ids)} subtasks")
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
"subtask_ids": subtask_ids,
|
|
287
|
+
"subtask_count": len(subtask_ids),
|
|
288
|
+
"raw_response": response,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
error_msg = str(e) or f"{type(e).__name__}: (no message)"
|
|
293
|
+
logger.error(f"Failed to expand task {task_id}: {error_msg}", exc_info=True)
|
|
294
|
+
return {"error": error_msg, "subtask_ids": [], "subtask_count": 0}
|
|
295
|
+
|
|
296
|
+
# Patterns that indicate a test task (case-insensitive)
|
|
297
|
+
TEST_TASK_PATTERNS = (
|
|
298
|
+
r"^write\s+tests?\s+for",
|
|
299
|
+
r"^add\s+(?:unit\s+)?tests?\s+for",
|
|
300
|
+
r"^create\s+(?:unit\s+)?tests?",
|
|
301
|
+
r"^unit\s+tests?\s+for",
|
|
302
|
+
r"^integration\s+tests?\s+for",
|
|
303
|
+
r"^test\s+(?:the\s+)?",
|
|
304
|
+
r"^verify\s+with\s+tests?",
|
|
305
|
+
r"tests?\s+for\s+.*(?:class|function|method|module)",
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def _is_test_task(self, title: str, category: str | None) -> bool:
|
|
309
|
+
"""Check if a subtask is a test task that should be filtered out.
|
|
310
|
+
|
|
311
|
+
TDD sandwich pattern creates test tasks automatically. LLM-generated
|
|
312
|
+
test tasks would cause duplicates and should be filtered.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
title: The subtask title
|
|
316
|
+
category: The subtask category (if provided)
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
True if this is a test task that should be filtered
|
|
320
|
+
"""
|
|
321
|
+
# Don't filter refactor tasks - they may legitimately update existing tests
|
|
322
|
+
if category and category.lower() == "refactor":
|
|
323
|
+
return False
|
|
324
|
+
|
|
325
|
+
# Filter by category=test
|
|
326
|
+
if category and category.lower() == "test":
|
|
327
|
+
return True
|
|
328
|
+
|
|
329
|
+
# Filter by title patterns (only when category is not explicitly set to refactor)
|
|
330
|
+
title_lower = title.lower().strip()
|
|
331
|
+
for pattern in self.TEST_TASK_PATTERNS:
|
|
332
|
+
if re.search(pattern, title_lower, re.IGNORECASE):
|
|
333
|
+
return True
|
|
334
|
+
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
def _parse_subtasks(self, response: str) -> list[SubtaskSpec]:
|
|
338
|
+
"""
|
|
339
|
+
Parse subtask specifications from LLM JSON response.
|
|
340
|
+
|
|
341
|
+
Filters out test tasks since TDD sandwich creates them automatically.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
response: Raw LLM response text (should be JSON)
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
List of SubtaskSpec objects parsed from the response
|
|
348
|
+
"""
|
|
349
|
+
# Try to extract JSON from the response
|
|
350
|
+
json_str = self._extract_json(response)
|
|
351
|
+
if not json_str:
|
|
352
|
+
logger.warning("No JSON found in response")
|
|
353
|
+
return []
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
data = json.loads(json_str)
|
|
357
|
+
except json.JSONDecodeError as e:
|
|
358
|
+
logger.error(f"Failed to parse JSON: {e}")
|
|
359
|
+
return []
|
|
360
|
+
|
|
361
|
+
# Extract subtasks array
|
|
362
|
+
subtasks_data = data.get("subtasks", [])
|
|
363
|
+
if not isinstance(subtasks_data, list):
|
|
364
|
+
logger.warning(f"Expected 'subtasks' to be a list, got {type(subtasks_data)}")
|
|
365
|
+
return []
|
|
366
|
+
|
|
367
|
+
# Parse each subtask, filtering out test tasks
|
|
368
|
+
subtask_specs = []
|
|
369
|
+
filtered_count = 0
|
|
370
|
+
for i, item in enumerate(subtasks_data):
|
|
371
|
+
if not isinstance(item, dict):
|
|
372
|
+
logger.warning(f"Subtask {i} is not a dict, skipping")
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
if "title" not in item:
|
|
376
|
+
logger.warning(f"Subtask {i} missing title, skipping")
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
title = item["title"]
|
|
380
|
+
category = item.get("category")
|
|
381
|
+
|
|
382
|
+
# Filter out test tasks - TDD sandwich creates them automatically
|
|
383
|
+
if self._is_test_task(title, category):
|
|
384
|
+
logger.debug(f"Filtered test task: '{title}' (category={category})")
|
|
385
|
+
filtered_count += 1
|
|
386
|
+
continue
|
|
387
|
+
|
|
388
|
+
spec = SubtaskSpec(
|
|
389
|
+
title=title,
|
|
390
|
+
description=item.get("description"),
|
|
391
|
+
priority=item.get("priority", 2),
|
|
392
|
+
task_type=item.get("task_type", "task"),
|
|
393
|
+
category=category,
|
|
394
|
+
validation=item.get("validation"),
|
|
395
|
+
depends_on=item.get("depends_on"),
|
|
396
|
+
)
|
|
397
|
+
subtask_specs.append(spec)
|
|
398
|
+
|
|
399
|
+
if filtered_count > 0:
|
|
400
|
+
logger.debug(f"Filtered {filtered_count} test tasks from LLM output")
|
|
401
|
+
|
|
402
|
+
return subtask_specs
|
|
403
|
+
|
|
404
|
+
def _extract_json(self, text: str) -> str | None:
|
|
405
|
+
"""Extract JSON from text. Delegates to shared utility."""
|
|
406
|
+
return extract_json_from_text(text)
|
|
407
|
+
|
|
408
|
+
async def _create_subtasks(
|
|
409
|
+
self,
|
|
410
|
+
parent_task_id: str,
|
|
411
|
+
project_id: str,
|
|
412
|
+
subtask_specs: list[SubtaskSpec],
|
|
413
|
+
expansion_context: ExpansionContext | None = None,
|
|
414
|
+
parent_labels: list[str] | None = None,
|
|
415
|
+
) -> list[str]:
|
|
416
|
+
"""
|
|
417
|
+
Create tasks from parsed subtask specifications.
|
|
418
|
+
|
|
419
|
+
Handles dependency wiring by mapping depends_on indices to task IDs.
|
|
420
|
+
Generates precise validation criteria using expansion context.
|
|
421
|
+
|
|
422
|
+
Note: TDD transformation is NOT done here. Use apply_tdd separately
|
|
423
|
+
to transform code tasks into test/implement/refactor triplets.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
parent_task_id: ID of the parent task
|
|
427
|
+
project_id: Project ID for the new tasks
|
|
428
|
+
subtask_specs: List of parsed subtask specifications
|
|
429
|
+
expansion_context: Context gathered during expansion (for criteria generation)
|
|
430
|
+
parent_labels: Labels from the parent task (for pattern detection)
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
List of created task IDs
|
|
434
|
+
"""
|
|
435
|
+
created_ids: list[str] = []
|
|
436
|
+
dep_manager = TaskDependencyManager(self.task_manager.db)
|
|
437
|
+
|
|
438
|
+
# Map subtask_spec index to task ID for dependency wiring
|
|
439
|
+
spec_index_to_id: dict[int, str] = {}
|
|
440
|
+
|
|
441
|
+
for i, spec in enumerate(subtask_specs):
|
|
442
|
+
# Build description
|
|
443
|
+
description = spec.description or ""
|
|
444
|
+
|
|
445
|
+
# Use validation from LLM output directly as validation_criteria
|
|
446
|
+
# This replaces the post-expansion generate_criteria() loop
|
|
447
|
+
validation_criteria = spec.validation
|
|
448
|
+
|
|
449
|
+
# If no validation from LLM and context available, generate precise criteria
|
|
450
|
+
if not validation_criteria and expansion_context:
|
|
451
|
+
precise_criteria = await self._generate_precise_criteria(
|
|
452
|
+
spec=spec,
|
|
453
|
+
context=expansion_context,
|
|
454
|
+
parent_labels=parent_labels or [],
|
|
455
|
+
)
|
|
456
|
+
if precise_criteria:
|
|
457
|
+
validation_criteria = precise_criteria
|
|
458
|
+
|
|
459
|
+
# Create the task with validation_criteria from LLM output
|
|
460
|
+
task = self.task_manager.create_task(
|
|
461
|
+
title=spec.title,
|
|
462
|
+
description=description if description else None,
|
|
463
|
+
project_id=project_id,
|
|
464
|
+
priority=spec.priority,
|
|
465
|
+
task_type=spec.task_type,
|
|
466
|
+
parent_task_id=parent_task_id,
|
|
467
|
+
category=spec.category,
|
|
468
|
+
validation_criteria=validation_criteria,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
created_ids.append(task.id)
|
|
472
|
+
logger.debug(f"Created subtask {task.id}: {spec.title}")
|
|
473
|
+
|
|
474
|
+
spec_index_to_id[i] = task.id
|
|
475
|
+
|
|
476
|
+
# Add dependencies
|
|
477
|
+
if spec.depends_on:
|
|
478
|
+
for dep_idx in spec.depends_on:
|
|
479
|
+
if dep_idx in spec_index_to_id:
|
|
480
|
+
blocker_id = spec_index_to_id[dep_idx]
|
|
481
|
+
try:
|
|
482
|
+
dep_manager.add_dependency(task.id, blocker_id, "blocks")
|
|
483
|
+
logger.debug(f"Added dependency: {task.id} blocked by {blocker_id}")
|
|
484
|
+
except Exception as e:
|
|
485
|
+
logger.warning(f"Failed to add dependency: {e}")
|
|
486
|
+
else:
|
|
487
|
+
logger.warning(
|
|
488
|
+
f"Subtask {i} references invalid or forward index {dep_idx}, skipping dependency"
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
return created_ids
|
|
492
|
+
|
|
493
|
+
def _save_expansion_context(
|
|
494
|
+
self,
|
|
495
|
+
task_id: str,
|
|
496
|
+
context: "ExpansionContext",
|
|
497
|
+
) -> None:
|
|
498
|
+
"""
|
|
499
|
+
Save expansion context to the task for audit and reuse.
|
|
500
|
+
|
|
501
|
+
Stores web research results and other context in the task's
|
|
502
|
+
expansion_context field as JSON.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
task_id: ID of the task to update
|
|
506
|
+
context: The expansion context to save
|
|
507
|
+
"""
|
|
508
|
+
try:
|
|
509
|
+
# Build a slim context dict focused on web research
|
|
510
|
+
context_data: dict[str, Any] = {}
|
|
511
|
+
|
|
512
|
+
if context.web_research:
|
|
513
|
+
context_data["web_research"] = context.web_research
|
|
514
|
+
|
|
515
|
+
if context.agent_findings:
|
|
516
|
+
context_data["agent_findings"] = context.agent_findings
|
|
517
|
+
|
|
518
|
+
if context.relevant_files:
|
|
519
|
+
context_data["relevant_files"] = context.relevant_files
|
|
520
|
+
|
|
521
|
+
if not context_data:
|
|
522
|
+
logger.debug(f"No expansion context to save for {task_id}")
|
|
523
|
+
return
|
|
524
|
+
|
|
525
|
+
# Serialize and update the task
|
|
526
|
+
context_json = json.dumps(context_data)
|
|
527
|
+
self.task_manager.update_task(task_id, expansion_context=context_json)
|
|
528
|
+
logger.debug(f"Saved expansion context for {task_id} ({len(context_json)} bytes)")
|
|
529
|
+
|
|
530
|
+
except Exception as e:
|
|
531
|
+
logger.warning(f"Failed to save expansion context for {task_id}: {e}")
|
|
532
|
+
|
|
533
|
+
async def _generate_precise_criteria(
|
|
534
|
+
self,
|
|
535
|
+
spec: SubtaskSpec,
|
|
536
|
+
context: ExpansionContext,
|
|
537
|
+
parent_labels: list[str],
|
|
538
|
+
) -> str:
|
|
539
|
+
"""
|
|
540
|
+
Generate precise validation criteria for a subtask using full expansion context.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
spec: The subtask specification
|
|
544
|
+
context: Full expansion context with verification commands, signatures, etc.
|
|
545
|
+
parent_labels: Labels from the parent task (for pattern detection)
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
Markdown-formatted validation criteria string
|
|
549
|
+
"""
|
|
550
|
+
criteria_parts: list[str] = []
|
|
551
|
+
|
|
552
|
+
# 1. Start with pattern-specific criteria from parent labels
|
|
553
|
+
pattern_criteria = self.criteria_injector.inject_for_labels(
|
|
554
|
+
labels=parent_labels,
|
|
555
|
+
extra_placeholders=context.verification_commands,
|
|
556
|
+
)
|
|
557
|
+
if pattern_criteria:
|
|
558
|
+
criteria_parts.append(pattern_criteria)
|
|
559
|
+
|
|
560
|
+
# 2. Add base criteria from category if present
|
|
561
|
+
if spec.category:
|
|
562
|
+
# Substitute verification commands into category
|
|
563
|
+
strategy = spec.category
|
|
564
|
+
if context.verification_commands:
|
|
565
|
+
for name, cmd in context.verification_commands.items():
|
|
566
|
+
strategy = strategy.replace(f"{{{name}}}", f"`{cmd}`")
|
|
567
|
+
criteria_parts.append(f"## Test Strategy\n\n- [ ] {strategy}")
|
|
568
|
+
|
|
569
|
+
# 3. Add file-specific criteria if relevant files are mentioned
|
|
570
|
+
if context.relevant_files and spec.description:
|
|
571
|
+
relevant_for_subtask = [
|
|
572
|
+
f
|
|
573
|
+
for f in context.relevant_files
|
|
574
|
+
if f.lower() in (spec.title + (spec.description or "")).lower()
|
|
575
|
+
]
|
|
576
|
+
if relevant_for_subtask:
|
|
577
|
+
file_criteria = ["## File Requirements", ""]
|
|
578
|
+
for f in relevant_for_subtask:
|
|
579
|
+
file_criteria.append(f"- [ ] `{f}` is correctly modified/created")
|
|
580
|
+
criteria_parts.append("\n".join(file_criteria))
|
|
581
|
+
|
|
582
|
+
# 4. Add function signature criteria if applicable
|
|
583
|
+
if context.function_signatures and spec.description:
|
|
584
|
+
desc_lower = (spec.description or "").lower()
|
|
585
|
+
for _file_path, signatures in context.function_signatures.items():
|
|
586
|
+
for sig in signatures:
|
|
587
|
+
if not sig:
|
|
588
|
+
continue
|
|
589
|
+
# Extract function name robustly using regex
|
|
590
|
+
# Handles: "def func_name(", "async def func_name(", "func_name("
|
|
591
|
+
func_name = None
|
|
592
|
+
# Try regex patterns first
|
|
593
|
+
match = re.search(r"(?:async\s+)?def\s+(\w+)", sig)
|
|
594
|
+
if match:
|
|
595
|
+
func_name = match.group(1)
|
|
596
|
+
else:
|
|
597
|
+
# Fallback: try to get name before first paren
|
|
598
|
+
match = re.search(r"(\w+)\s*\(", sig)
|
|
599
|
+
if match:
|
|
600
|
+
func_name = match.group(1)
|
|
601
|
+
else:
|
|
602
|
+
# Last resort: use existing split logic
|
|
603
|
+
try:
|
|
604
|
+
func_name = (
|
|
605
|
+
sig.split("(")[0].split()[-1] if "(" in sig else sig.split()[-1]
|
|
606
|
+
)
|
|
607
|
+
except (IndexError, AttributeError):
|
|
608
|
+
continue
|
|
609
|
+
|
|
610
|
+
if func_name and func_name.lower() in desc_lower:
|
|
611
|
+
criteria_parts.append(
|
|
612
|
+
f"## Function Integrity\n\n"
|
|
613
|
+
f"- [ ] `{func_name}` signature preserved or updated as intended"
|
|
614
|
+
)
|
|
615
|
+
break
|
|
616
|
+
|
|
617
|
+
# 5. Add verification command criteria
|
|
618
|
+
if context.verification_commands:
|
|
619
|
+
verification_criteria = ["## Verification", ""]
|
|
620
|
+
for name, cmd in context.verification_commands.items():
|
|
621
|
+
if name in ["unit_tests", "type_check", "lint"]:
|
|
622
|
+
verification_criteria.append(f"- [ ] `{cmd}` passes")
|
|
623
|
+
if len(verification_criteria) > 2: # Has items beyond header
|
|
624
|
+
criteria_parts.append("\n".join(verification_criteria))
|
|
625
|
+
|
|
626
|
+
return "\n\n".join(criteria_parts) if criteria_parts else ""
|