kollabor 0.4.9__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.
- core/__init__.py +18 -0
- core/application.py +578 -0
- core/cli.py +193 -0
- core/commands/__init__.py +43 -0
- core/commands/executor.py +277 -0
- core/commands/menu_renderer.py +319 -0
- core/commands/parser.py +186 -0
- core/commands/registry.py +331 -0
- core/commands/system_commands.py +479 -0
- core/config/__init__.py +7 -0
- core/config/llm_task_config.py +110 -0
- core/config/loader.py +501 -0
- core/config/manager.py +112 -0
- core/config/plugin_config_manager.py +346 -0
- core/config/plugin_schema.py +424 -0
- core/config/service.py +399 -0
- core/effects/__init__.py +1 -0
- core/events/__init__.py +12 -0
- core/events/bus.py +129 -0
- core/events/executor.py +154 -0
- core/events/models.py +258 -0
- core/events/processor.py +176 -0
- core/events/registry.py +289 -0
- core/fullscreen/__init__.py +19 -0
- core/fullscreen/command_integration.py +290 -0
- core/fullscreen/components/__init__.py +12 -0
- core/fullscreen/components/animation.py +258 -0
- core/fullscreen/components/drawing.py +160 -0
- core/fullscreen/components/matrix_components.py +177 -0
- core/fullscreen/manager.py +302 -0
- core/fullscreen/plugin.py +204 -0
- core/fullscreen/renderer.py +282 -0
- core/fullscreen/session.py +324 -0
- core/io/__init__.py +52 -0
- core/io/buffer_manager.py +362 -0
- core/io/config_status_view.py +272 -0
- core/io/core_status_views.py +410 -0
- core/io/input_errors.py +313 -0
- core/io/input_handler.py +2655 -0
- core/io/input_mode_manager.py +402 -0
- core/io/key_parser.py +344 -0
- core/io/layout.py +587 -0
- core/io/message_coordinator.py +204 -0
- core/io/message_renderer.py +601 -0
- core/io/modal_interaction_handler.py +315 -0
- core/io/raw_input_processor.py +946 -0
- core/io/status_renderer.py +845 -0
- core/io/terminal_renderer.py +586 -0
- core/io/terminal_state.py +551 -0
- core/io/visual_effects.py +734 -0
- core/llm/__init__.py +26 -0
- core/llm/api_communication_service.py +863 -0
- core/llm/conversation_logger.py +473 -0
- core/llm/conversation_manager.py +414 -0
- core/llm/file_operations_executor.py +1401 -0
- core/llm/hook_system.py +402 -0
- core/llm/llm_service.py +1629 -0
- core/llm/mcp_integration.py +386 -0
- core/llm/message_display_service.py +450 -0
- core/llm/model_router.py +214 -0
- core/llm/plugin_sdk.py +396 -0
- core/llm/response_parser.py +848 -0
- core/llm/response_processor.py +364 -0
- core/llm/tool_executor.py +520 -0
- core/logging/__init__.py +19 -0
- core/logging/setup.py +208 -0
- core/models/__init__.py +5 -0
- core/models/base.py +23 -0
- core/plugins/__init__.py +13 -0
- core/plugins/collector.py +212 -0
- core/plugins/discovery.py +386 -0
- core/plugins/factory.py +263 -0
- core/plugins/registry.py +152 -0
- core/storage/__init__.py +5 -0
- core/storage/state_manager.py +84 -0
- core/ui/__init__.py +6 -0
- core/ui/config_merger.py +176 -0
- core/ui/config_widgets.py +369 -0
- core/ui/live_modal_renderer.py +276 -0
- core/ui/modal_actions.py +162 -0
- core/ui/modal_overlay_renderer.py +373 -0
- core/ui/modal_renderer.py +591 -0
- core/ui/modal_state_manager.py +443 -0
- core/ui/widget_integration.py +222 -0
- core/ui/widgets/__init__.py +27 -0
- core/ui/widgets/base_widget.py +136 -0
- core/ui/widgets/checkbox.py +85 -0
- core/ui/widgets/dropdown.py +140 -0
- core/ui/widgets/label.py +78 -0
- core/ui/widgets/slider.py +185 -0
- core/ui/widgets/text_input.py +224 -0
- core/utils/__init__.py +11 -0
- core/utils/config_utils.py +656 -0
- core/utils/dict_utils.py +212 -0
- core/utils/error_utils.py +275 -0
- core/utils/key_reader.py +171 -0
- core/utils/plugin_utils.py +267 -0
- core/utils/prompt_renderer.py +151 -0
- kollabor-0.4.9.dist-info/METADATA +298 -0
- kollabor-0.4.9.dist-info/RECORD +128 -0
- kollabor-0.4.9.dist-info/WHEEL +5 -0
- kollabor-0.4.9.dist-info/entry_points.txt +2 -0
- kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
- kollabor-0.4.9.dist-info/top_level.txt +4 -0
- kollabor_cli_main.py +20 -0
- plugins/__init__.py +1 -0
- plugins/enhanced_input/__init__.py +18 -0
- plugins/enhanced_input/box_renderer.py +103 -0
- plugins/enhanced_input/box_styles.py +142 -0
- plugins/enhanced_input/color_engine.py +165 -0
- plugins/enhanced_input/config.py +150 -0
- plugins/enhanced_input/cursor_manager.py +72 -0
- plugins/enhanced_input/geometry.py +81 -0
- plugins/enhanced_input/state.py +130 -0
- plugins/enhanced_input/text_processor.py +115 -0
- plugins/enhanced_input_plugin.py +385 -0
- plugins/fullscreen/__init__.py +9 -0
- plugins/fullscreen/example_plugin.py +327 -0
- plugins/fullscreen/matrix_plugin.py +132 -0
- plugins/hook_monitoring_plugin.py +1299 -0
- plugins/query_enhancer_plugin.py +350 -0
- plugins/save_conversation_plugin.py +502 -0
- plugins/system_commands_plugin.py +93 -0
- plugins/tmux_plugin.py +795 -0
- plugins/workflow_enforcement_plugin.py +629 -0
- system_prompt/default.md +1286 -0
- system_prompt/default_win.md +265 -0
- system_prompt/example_with_trender.md +47 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
"""Workflow Enforcement Plugin for Kollabor CLI.
|
|
2
|
+
|
|
3
|
+
This plugin detects todo lists in LLM responses and enforces sequential completion
|
|
4
|
+
with tool calling verification and confirmation requirements.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Dict, List, Optional, Any
|
|
14
|
+
|
|
15
|
+
from core.events.models import EventType, HookPriority
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WorkflowState(Enum):
|
|
21
|
+
"""Workflow enforcement states."""
|
|
22
|
+
INACTIVE = "inactive" # No active workflow
|
|
23
|
+
TODO_DETECTED = "todo_detected" # Todo list found, waiting for user confirmation
|
|
24
|
+
ENFORCING = "enforcing" # Actively enforcing todo completion
|
|
25
|
+
WAITING_CONFIRMATION = "waiting_confirmation" # Waiting for completion confirmation
|
|
26
|
+
BLOCKED = "blocked" # User requested bypass or hit issue
|
|
27
|
+
COMPLETED = "completed" # Workflow successfully completed
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class TodoItem:
|
|
32
|
+
"""Represents a single todo item."""
|
|
33
|
+
index: int
|
|
34
|
+
text: str
|
|
35
|
+
terminal_command: Optional[str] = None
|
|
36
|
+
completed: bool = False
|
|
37
|
+
confirmed: bool = False
|
|
38
|
+
attempted: bool = False
|
|
39
|
+
failure_reason: Optional[str] = None
|
|
40
|
+
timestamp_started: Optional[datetime] = None
|
|
41
|
+
timestamp_completed: Optional[datetime] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class WorkflowContext:
|
|
46
|
+
"""Maintains context for active workflow."""
|
|
47
|
+
original_request: str = ""
|
|
48
|
+
todo_items: List[TodoItem] = field(default_factory=list)
|
|
49
|
+
current_todo_index: int = 0
|
|
50
|
+
state: WorkflowState = WorkflowState.INACTIVE
|
|
51
|
+
llm_response_with_todos: str = ""
|
|
52
|
+
bypass_requested: bool = False
|
|
53
|
+
bypass_reason: str = ""
|
|
54
|
+
started_at: Optional[datetime] = None
|
|
55
|
+
completed_at: Optional[datetime] = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class WorkflowEnforcementPlugin:
|
|
59
|
+
"""Plugin that enforces todo completion with tool calling verification."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, state_manager, event_bus, renderer, config):
|
|
62
|
+
"""Initialize workflow enforcement plugin."""
|
|
63
|
+
self.state_manager = state_manager
|
|
64
|
+
self.event_bus = event_bus
|
|
65
|
+
self.renderer = renderer
|
|
66
|
+
self.config = config
|
|
67
|
+
|
|
68
|
+
# Workflow state
|
|
69
|
+
self.workflow_context = WorkflowContext()
|
|
70
|
+
|
|
71
|
+
# Configuration
|
|
72
|
+
self.enabled = config.get("workflow_enforcement.enabled", True)
|
|
73
|
+
self.require_tool_calls = config.get("workflow_enforcement.require_tool_calls", True)
|
|
74
|
+
self.confirmation_timeout = config.get("workflow_enforcement.confirmation_timeout", 300) # 5 minutes
|
|
75
|
+
self.bypass_keywords = config.get("workflow_enforcement.bypass_keywords",
|
|
76
|
+
["bypass", "skip", "blocked", "issue", "problem"])
|
|
77
|
+
|
|
78
|
+
logger.info("Workflow Enforcement Plugin initialized")
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def get_default_config():
|
|
82
|
+
"""Return default configuration for the plugin."""
|
|
83
|
+
return {
|
|
84
|
+
"workflow_enforcement": {
|
|
85
|
+
"enabled": False,
|
|
86
|
+
"require_tool_calls": True,
|
|
87
|
+
"confirmation_timeout": 300,
|
|
88
|
+
"bypass_keywords": ["bypass", "skip", "blocked", "issue", "problem"],
|
|
89
|
+
"auto_start_workflows": True,
|
|
90
|
+
"show_progress_in_status": True
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def get_config_widgets() -> Dict[str, Any]:
|
|
96
|
+
"""Get configuration widgets for this plugin."""
|
|
97
|
+
return {
|
|
98
|
+
"title": "Workflow Enforcement",
|
|
99
|
+
"widgets": [
|
|
100
|
+
{"type": "checkbox", "label": "Require Tool Calls", "config_path": "workflow_enforcement.require_tool_calls", "help": "Require workflows to include tool calls"},
|
|
101
|
+
{"type": "slider", "label": "Confirmation Timeout", "config_path": "workflow_enforcement.confirmation_timeout", "min_value": 30, "max_value": 600, "step": 30, "help": "Workflow confirmation timeout (seconds)"},
|
|
102
|
+
{"type": "checkbox", "label": "Auto Start Workflows", "config_path": "workflow_enforcement.auto_start_workflows", "help": "Automatically start detected workflows"},
|
|
103
|
+
{"type": "checkbox", "label": "Show Progress in Status", "config_path": "workflow_enforcement.show_progress_in_status", "help": "Display workflow progress in status bar"}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async def initialize(self):
|
|
108
|
+
"""Initialize the plugin."""
|
|
109
|
+
# Load any persistent workflow state
|
|
110
|
+
await self._load_workflow_state()
|
|
111
|
+
logger.info("Workflow enforcement plugin initialized")
|
|
112
|
+
|
|
113
|
+
async def register_hooks(self):
|
|
114
|
+
"""Register hooks for workflow enforcement."""
|
|
115
|
+
if not self.enabled:
|
|
116
|
+
logger.info("Workflow enforcement plugin disabled")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# Hook into LLM responses to detect todos
|
|
120
|
+
await self.event_bus.register_hook(
|
|
121
|
+
EventType.LLM_RESPONSE_POST,
|
|
122
|
+
"workflow_todo_detector",
|
|
123
|
+
self._detect_and_process_todos,
|
|
124
|
+
HookPriority.PREPROCESSING
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Hook into user input to handle confirmations and bypass
|
|
128
|
+
await self.event_bus.register_hook(
|
|
129
|
+
EventType.USER_INPUT_PRE,
|
|
130
|
+
"workflow_input_processor",
|
|
131
|
+
self._process_user_input,
|
|
132
|
+
HookPriority.PREPROCESSING
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Hook into LLM requests to inject workflow context
|
|
136
|
+
await self.event_bus.register_hook(
|
|
137
|
+
EventType.LLM_REQUEST_PRE,
|
|
138
|
+
"workflow_context_injector",
|
|
139
|
+
self._inject_workflow_context,
|
|
140
|
+
HookPriority.PREPROCESSING
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
logger.info("Workflow enforcement hooks registered")
|
|
144
|
+
|
|
145
|
+
async def _detect_and_process_todos(self, event_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
146
|
+
"""Detect todo lists in LLM responses and initiate workflow enforcement."""
|
|
147
|
+
if self.workflow_context.state == WorkflowState.ENFORCING:
|
|
148
|
+
# Already in workflow - check if this is a completion response
|
|
149
|
+
return await self._handle_workflow_response(event_data)
|
|
150
|
+
|
|
151
|
+
response_content = event_data.get("response", "")
|
|
152
|
+
todos = self._extract_todo_list(response_content)
|
|
153
|
+
|
|
154
|
+
if todos and len(todos) > 0:
|
|
155
|
+
logger.info(f"Detected {len(todos)} todo items, initiating workflow enforcement")
|
|
156
|
+
|
|
157
|
+
# Initialize workflow context
|
|
158
|
+
self.workflow_context = WorkflowContext(
|
|
159
|
+
original_request=event_data.get("original_request", ""),
|
|
160
|
+
todo_items=todos,
|
|
161
|
+
state=WorkflowState.TODO_DETECTED,
|
|
162
|
+
llm_response_with_todos=response_content,
|
|
163
|
+
started_at=datetime.now()
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Save workflow state
|
|
167
|
+
await self._save_workflow_state()
|
|
168
|
+
|
|
169
|
+
# Modify the response to include workflow activation message
|
|
170
|
+
activation_msg = self._create_workflow_activation_message()
|
|
171
|
+
event_data["response"] = f"{response_content}\n\n{activation_msg}"
|
|
172
|
+
|
|
173
|
+
# Display workflow activation via hook message
|
|
174
|
+
self.renderer.write_hook_message(
|
|
175
|
+
f"[*] Workflow Enforcement Activated - {len(todos)} todos detected"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return event_data
|
|
179
|
+
|
|
180
|
+
async def _process_user_input(self, event_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
181
|
+
"""Process user input for workflow commands and confirmations."""
|
|
182
|
+
if self.workflow_context.state == WorkflowState.INACTIVE:
|
|
183
|
+
return event_data
|
|
184
|
+
|
|
185
|
+
user_input = event_data.get("message", "").strip().lower()
|
|
186
|
+
|
|
187
|
+
# Check for bypass request
|
|
188
|
+
if any(keyword in user_input for keyword in self.bypass_keywords):
|
|
189
|
+
return await self._handle_bypass_request(event_data, user_input)
|
|
190
|
+
|
|
191
|
+
# Handle workflow state transitions
|
|
192
|
+
if self.workflow_context.state == WorkflowState.TODO_DETECTED:
|
|
193
|
+
if "start workflow" in user_input or "yes" in user_input or "confirm" in user_input:
|
|
194
|
+
return await self._start_workflow_enforcement(event_data)
|
|
195
|
+
elif "no" in user_input or "cancel" in user_input:
|
|
196
|
+
return await self._cancel_workflow(event_data)
|
|
197
|
+
|
|
198
|
+
elif self.workflow_context.state == WorkflowState.WAITING_CONFIRMATION:
|
|
199
|
+
if "completed" in user_input or "done" in user_input or "finished" in user_input:
|
|
200
|
+
return await self._confirm_todo_completion(event_data)
|
|
201
|
+
elif "failed" in user_input or "error" in user_input:
|
|
202
|
+
return await self._handle_todo_failure(event_data, user_input)
|
|
203
|
+
|
|
204
|
+
return event_data
|
|
205
|
+
|
|
206
|
+
async def _inject_workflow_context(self, event_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
207
|
+
"""Inject workflow context into LLM requests."""
|
|
208
|
+
if self.workflow_context.state == WorkflowState.ENFORCING:
|
|
209
|
+
current_todo = self._get_current_todo()
|
|
210
|
+
if current_todo:
|
|
211
|
+
context_injection = f"""
|
|
212
|
+
|
|
213
|
+
WORKFLOW ENFORCEMENT ACTIVE:
|
|
214
|
+
- Original Request: {self.workflow_context.original_request}
|
|
215
|
+
- Current Todo ({current_todo.index + 1}/{len(self.workflow_context.todo_items)}): {current_todo.text}
|
|
216
|
+
- Required: Use <terminal> tags for all commands as shown in examples
|
|
217
|
+
- Status: {'ATTEMPTED' if current_todo.attempted else 'PENDING'}
|
|
218
|
+
|
|
219
|
+
You MUST complete this todo item using proper tool calling before proceeding.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
# Prepend workflow context to the message
|
|
223
|
+
original_message = event_data.get("message", "")
|
|
224
|
+
event_data["message"] = f"{context_injection}\n\n{original_message}"
|
|
225
|
+
|
|
226
|
+
return event_data
|
|
227
|
+
|
|
228
|
+
def _extract_todo_list(self, text: str) -> List[TodoItem]:
|
|
229
|
+
"""Extract todo items from markdown text."""
|
|
230
|
+
todos = []
|
|
231
|
+
|
|
232
|
+
# Pattern to match markdown todo items with optional terminal commands
|
|
233
|
+
todo_pattern = r'^\s*-\s*\[\s*\]\s*(.+?)(?:\s*<terminal>(.+?)</terminal>)?$'
|
|
234
|
+
|
|
235
|
+
lines = text.split('\n')
|
|
236
|
+
todo_index = 0
|
|
237
|
+
|
|
238
|
+
for line in lines:
|
|
239
|
+
match = re.match(todo_pattern, line, re.MULTILINE)
|
|
240
|
+
if match:
|
|
241
|
+
todo_text = match.group(1).strip()
|
|
242
|
+
terminal_command = match.group(2).strip() if match.group(2) else None
|
|
243
|
+
|
|
244
|
+
todos.append(TodoItem(
|
|
245
|
+
index=todo_index,
|
|
246
|
+
text=todo_text,
|
|
247
|
+
terminal_command=terminal_command
|
|
248
|
+
))
|
|
249
|
+
todo_index += 1
|
|
250
|
+
|
|
251
|
+
return todos
|
|
252
|
+
|
|
253
|
+
def _create_workflow_activation_message(self) -> str:
|
|
254
|
+
"""Create the workflow activation message."""
|
|
255
|
+
todo_count = len(self.workflow_context.todo_items)
|
|
256
|
+
|
|
257
|
+
msg = f"""
|
|
258
|
+
[*] **WORKFLOW ENFORCEMENT ACTIVATED**
|
|
259
|
+
|
|
260
|
+
I've detected {todo_count} todo items that require completion. The workflow system will:
|
|
261
|
+
|
|
262
|
+
1.**Enforce Sequential Completion** - Each todo must be completed in order
|
|
263
|
+
2.**Require Tool Calling** - All commands must use <terminal> tags
|
|
264
|
+
3.**Wait for Confirmation** - You must confirm each completion
|
|
265
|
+
4.**Track Progress** - Monitor completion status
|
|
266
|
+
5.**Allow Bypass** - Use keywords: {', '.join(self.bypass_keywords)}
|
|
267
|
+
|
|
268
|
+
**Next Steps:**
|
|
269
|
+
- Reply "**start workflow**" to begin enforcement
|
|
270
|
+
- Reply "**cancel**" to proceed without workflow
|
|
271
|
+
- Each todo will be presented individually for completion
|
|
272
|
+
|
|
273
|
+
**Current Todo Queue:**
|
|
274
|
+
{self._format_todo_queue()}
|
|
275
|
+
"""
|
|
276
|
+
return msg
|
|
277
|
+
|
|
278
|
+
def _format_todo_queue(self) -> str:
|
|
279
|
+
"""Format the todo queue for display."""
|
|
280
|
+
lines = []
|
|
281
|
+
for i, todo in enumerate(self.workflow_context.todo_items):
|
|
282
|
+
status = "[DONE]" if todo.completed else "[ACTIVE]" if i == self.workflow_context.current_todo_index else "[PENDING]"
|
|
283
|
+
command_info = f" `{todo.terminal_command}`" if todo.terminal_command else ""
|
|
284
|
+
lines.append(f"{status} **{i+1}.** {todo.text}{command_info}")
|
|
285
|
+
return '\n'.join(lines)
|
|
286
|
+
|
|
287
|
+
async def _start_workflow_enforcement(self, event_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
288
|
+
"""Start enforcing workflow completion."""
|
|
289
|
+
self.workflow_context.state = WorkflowState.ENFORCING
|
|
290
|
+
await self._save_workflow_state()
|
|
291
|
+
|
|
292
|
+
# Present first todo
|
|
293
|
+
first_todo = self._get_current_todo()
|
|
294
|
+
if first_todo:
|
|
295
|
+
first_todo.attempted = True
|
|
296
|
+
first_todo.timestamp_started = datetime.now()
|
|
297
|
+
|
|
298
|
+
enforcement_msg = self._create_todo_enforcement_message(first_todo)
|
|
299
|
+
|
|
300
|
+
# Replace user message with workflow enforcement
|
|
301
|
+
event_data["message"] = enforcement_msg
|
|
302
|
+
|
|
303
|
+
self.renderer.write_hook_message(
|
|
304
|
+
f"Workflow Started - Todo 1/{len(self.workflow_context.todo_items)}: {first_todo.text[:50]}..."
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return event_data
|
|
308
|
+
|
|
309
|
+
async def _cancel_workflow(self, event_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
310
|
+
"""Cancel workflow enforcement."""
|
|
311
|
+
self.workflow_context.state = WorkflowState.INACTIVE
|
|
312
|
+
await self._save_workflow_state()
|
|
313
|
+
|
|
314
|
+
event_data["message"] = "Workflow enforcement cancelled. Proceeding with normal operation."
|
|
315
|
+
|
|
316
|
+
self.renderer.write_hook_message("Workflow Enforcement Cancelled")
|
|
317
|
+
return event_data
|
|
318
|
+
|
|
319
|
+
def _create_todo_enforcement_message(self, todo_item: TodoItem) -> str:
|
|
320
|
+
"""Create enforcement message for a specific todo."""
|
|
321
|
+
progress = f"{todo_item.index + 1}/{len(self.workflow_context.todo_items)}"
|
|
322
|
+
|
|
323
|
+
msg = f"""
|
|
324
|
+
**WORKFLOW ENFORCEMENT - TODO {progress}**
|
|
325
|
+
|
|
326
|
+
**Original Request:** {self.workflow_context.original_request}
|
|
327
|
+
|
|
328
|
+
**Current Todo:** {todo_item.text}
|
|
329
|
+
|
|
330
|
+
**Requirements:**
|
|
331
|
+
- Complete this todo item fully
|
|
332
|
+
- Use <terminal> tags for all commands (required!)
|
|
333
|
+
- Show your work with actual tool execution
|
|
334
|
+
- Reply "**completed**" when finished
|
|
335
|
+
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
if todo_item.terminal_command:
|
|
339
|
+
msg += f"**Suggested Command:** `{todo_item.terminal_command}`\n\n"
|
|
340
|
+
|
|
341
|
+
msg += f"""**Bypass Options:**
|
|
342
|
+
- Reply "**bypass [reason]**" if blocked
|
|
343
|
+
- Reply "**failed [reason]**" if unable to complete
|
|
344
|
+
|
|
345
|
+
**Progress:** {self._format_todo_queue()}
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
**Now complete this todo using proper <terminal> tags and confirm when done.**
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
return msg
|
|
353
|
+
|
|
354
|
+
async def _confirm_todo_completion(self, event_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
355
|
+
"""Handle todo completion confirmation."""
|
|
356
|
+
current_todo = self._get_current_todo()
|
|
357
|
+
if not current_todo:
|
|
358
|
+
return event_data
|
|
359
|
+
|
|
360
|
+
# Mark current todo as completed
|
|
361
|
+
current_todo.completed = True
|
|
362
|
+
current_todo.confirmed = True
|
|
363
|
+
current_todo.timestamp_completed = datetime.now()
|
|
364
|
+
|
|
365
|
+
# Move to next todo or complete workflow
|
|
366
|
+
self.workflow_context.current_todo_index += 1
|
|
367
|
+
|
|
368
|
+
if self.workflow_context.current_todo_index >= len(self.workflow_context.todo_items):
|
|
369
|
+
# Workflow completed!
|
|
370
|
+
return await self._complete_workflow(event_data)
|
|
371
|
+
else:
|
|
372
|
+
# Present next todo
|
|
373
|
+
next_todo = self._get_current_todo()
|
|
374
|
+
next_todo.attempted = True
|
|
375
|
+
next_todo.timestamp_started = datetime.now()
|
|
376
|
+
|
|
377
|
+
next_msg = self._create_todo_enforcement_message(next_todo)
|
|
378
|
+
event_data["message"] = next_msg
|
|
379
|
+
|
|
380
|
+
self.renderer.write_hook_message(
|
|
381
|
+
f"Todo {current_todo.index + 1} completed! Moving to Todo {next_todo.index + 1}/{len(self.workflow_context.todo_items)}"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
await self._save_workflow_state()
|
|
385
|
+
return event_data
|
|
386
|
+
|
|
387
|
+
async def _complete_workflow(self, event_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
388
|
+
"""Complete the workflow successfully."""
|
|
389
|
+
self.workflow_context.state = WorkflowState.COMPLETED
|
|
390
|
+
self.workflow_context.completed_at = datetime.now()
|
|
391
|
+
|
|
392
|
+
completion_stats = self._generate_completion_stats()
|
|
393
|
+
|
|
394
|
+
completion_msg = f"""
|
|
395
|
+
**WORKFLOW ENFORCEMENT COMPLETED!**
|
|
396
|
+
|
|
397
|
+
**Original Request:** {self.workflow_context.original_request}
|
|
398
|
+
|
|
399
|
+
**Results:**
|
|
400
|
+
- **All {len(self.workflow_context.todo_items)} todos completed successfully**
|
|
401
|
+
- **Tool calling enforced throughout**
|
|
402
|
+
- **Total time:** {completion_stats['duration']}
|
|
403
|
+
- **Success rate:** {completion_stats['success_rate']}%
|
|
404
|
+
|
|
405
|
+
{completion_stats['summary']}
|
|
406
|
+
|
|
407
|
+
**Workflow enforcement is now deactivated.** You can continue with normal operation.
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
event_data["message"] = completion_msg
|
|
411
|
+
|
|
412
|
+
self.renderer.write_hook_message("Workflow Enforcement Completed Successfully!")
|
|
413
|
+
|
|
414
|
+
# Reset workflow state
|
|
415
|
+
self.workflow_context = WorkflowContext()
|
|
416
|
+
await self._save_workflow_state()
|
|
417
|
+
|
|
418
|
+
return event_data
|
|
419
|
+
|
|
420
|
+
async def _handle_bypass_request(self, event_data: Dict[str, Any], user_input: str) -> Dict[str, Any]:
|
|
421
|
+
"""Handle workflow bypass requests."""
|
|
422
|
+
bypass_reason = user_input.replace("bypass", "").replace("skip", "").strip()
|
|
423
|
+
|
|
424
|
+
self.workflow_context.bypass_requested = True
|
|
425
|
+
self.workflow_context.bypass_reason = bypass_reason
|
|
426
|
+
self.workflow_context.state = WorkflowState.BLOCKED
|
|
427
|
+
|
|
428
|
+
bypass_msg = f"""
|
|
429
|
+
[!] **WORKFLOW BYPASS ACTIVATED**
|
|
430
|
+
|
|
431
|
+
**Reason:** {bypass_reason or "No reason provided"}
|
|
432
|
+
|
|
433
|
+
**Options:**
|
|
434
|
+
1. Reply "**resume**" to continue workflow from current todo
|
|
435
|
+
2. Reply "**abort**" to completely cancel workflow enforcement
|
|
436
|
+
3. Continue with normal operation - workflow remains paused
|
|
437
|
+
|
|
438
|
+
**Current Progress:** {self._format_todo_queue()}
|
|
439
|
+
"""
|
|
440
|
+
|
|
441
|
+
event_data["message"] = bypass_msg
|
|
442
|
+
|
|
443
|
+
self.renderer.write_hook_message(f"[!] Workflow Bypassed: {bypass_reason}")
|
|
444
|
+
|
|
445
|
+
await self._save_workflow_state()
|
|
446
|
+
return event_data
|
|
447
|
+
|
|
448
|
+
async def _handle_todo_failure(self, event_data: Dict[str, Any], user_input: str) -> Dict[str, Any]:
|
|
449
|
+
"""Handle todo failure reports."""
|
|
450
|
+
current_todo = self._get_current_todo()
|
|
451
|
+
if current_todo:
|
|
452
|
+
failure_reason = user_input.replace("failed", "").replace("error", "").strip()
|
|
453
|
+
current_todo.failure_reason = failure_reason
|
|
454
|
+
|
|
455
|
+
failure_msg = f"""
|
|
456
|
+
**TODO FAILURE REPORTED**
|
|
457
|
+
|
|
458
|
+
**Failed Todo:** {current_todo.text}
|
|
459
|
+
**Reason:** {failure_reason or "No reason provided"}
|
|
460
|
+
|
|
461
|
+
**Options:**
|
|
462
|
+
1. Reply "**retry**" to attempt this todo again
|
|
463
|
+
2. Reply "**skip**" to mark as failed and move to next todo
|
|
464
|
+
3. Reply "**bypass workflow**" to exit workflow enforcement
|
|
465
|
+
|
|
466
|
+
Would you like to retry this todo or skip it?
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
event_data["message"] = failure_msg
|
|
470
|
+
|
|
471
|
+
self.renderer.write_hook_message(f"Todo Failed: {current_todo.text[:50]}...")
|
|
472
|
+
|
|
473
|
+
return event_data
|
|
474
|
+
|
|
475
|
+
def _get_current_todo(self) -> Optional[TodoItem]:
|
|
476
|
+
"""Get the current todo item being worked on."""
|
|
477
|
+
if (0 <= self.workflow_context.current_todo_index < len(self.workflow_context.todo_items)):
|
|
478
|
+
return self.workflow_context.todo_items[self.workflow_context.current_todo_index]
|
|
479
|
+
return None
|
|
480
|
+
|
|
481
|
+
def _generate_completion_stats(self) -> Dict[str, Any]:
|
|
482
|
+
"""Generate workflow completion statistics."""
|
|
483
|
+
completed_todos = [todo for todo in self.workflow_context.todo_items if todo.completed]
|
|
484
|
+
failed_todos = [todo for todo in self.workflow_context.todo_items if todo.failure_reason]
|
|
485
|
+
|
|
486
|
+
duration = "N/A"
|
|
487
|
+
if self.workflow_context.started_at and self.workflow_context.completed_at:
|
|
488
|
+
delta = self.workflow_context.completed_at - self.workflow_context.started_at
|
|
489
|
+
duration = f"{delta.total_seconds():.1f} seconds"
|
|
490
|
+
|
|
491
|
+
success_rate = (len(completed_todos) / len(self.workflow_context.todo_items)) * 100 if self.workflow_context.todo_items else 0
|
|
492
|
+
|
|
493
|
+
summary_lines = []
|
|
494
|
+
for i, todo in enumerate(self.workflow_context.todo_items):
|
|
495
|
+
status = "COMPLETED" if todo.completed else "FAILED" if todo.failure_reason else "SKIPPED"
|
|
496
|
+
summary_lines.append(f" {i+1}. {todo.text[:60]}... - {status}")
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
"duration": duration,
|
|
500
|
+
"success_rate": int(success_rate),
|
|
501
|
+
"completed_count": len(completed_todos),
|
|
502
|
+
"failed_count": len(failed_todos),
|
|
503
|
+
"summary": "\n".join(summary_lines)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async def _save_workflow_state(self):
|
|
507
|
+
"""Save workflow state to persistent storage."""
|
|
508
|
+
try:
|
|
509
|
+
await self.state_manager.set_state("workflow_enforcement", {
|
|
510
|
+
"workflow_context": {
|
|
511
|
+
"original_request": self.workflow_context.original_request,
|
|
512
|
+
"current_todo_index": self.workflow_context.current_todo_index,
|
|
513
|
+
"state": self.workflow_context.state.value,
|
|
514
|
+
"bypass_requested": self.workflow_context.bypass_requested,
|
|
515
|
+
"bypass_reason": self.workflow_context.bypass_reason,
|
|
516
|
+
"todo_items": [
|
|
517
|
+
{
|
|
518
|
+
"index": todo.index,
|
|
519
|
+
"text": todo.text,
|
|
520
|
+
"terminal_command": todo.terminal_command,
|
|
521
|
+
"completed": todo.completed,
|
|
522
|
+
"confirmed": todo.confirmed,
|
|
523
|
+
"attempted": todo.attempted,
|
|
524
|
+
"failure_reason": todo.failure_reason
|
|
525
|
+
}
|
|
526
|
+
for todo in self.workflow_context.todo_items
|
|
527
|
+
]
|
|
528
|
+
}
|
|
529
|
+
})
|
|
530
|
+
except Exception as e:
|
|
531
|
+
logger.warning(f"Failed to save workflow state: {e}")
|
|
532
|
+
|
|
533
|
+
async def _load_workflow_state(self):
|
|
534
|
+
"""Load workflow state from persistent storage."""
|
|
535
|
+
try:
|
|
536
|
+
state_data = await self.state_manager.get_state("workflow_enforcement")
|
|
537
|
+
if state_data and "workflow_context" in state_data:
|
|
538
|
+
context_data = state_data["workflow_context"]
|
|
539
|
+
|
|
540
|
+
# Reconstruct workflow context
|
|
541
|
+
self.workflow_context.original_request = context_data.get("original_request", "")
|
|
542
|
+
self.workflow_context.current_todo_index = context_data.get("current_todo_index", 0)
|
|
543
|
+
self.workflow_context.state = WorkflowState(context_data.get("state", WorkflowState.INACTIVE.value))
|
|
544
|
+
self.workflow_context.bypass_requested = context_data.get("bypass_requested", False)
|
|
545
|
+
self.workflow_context.bypass_reason = context_data.get("bypass_reason", "")
|
|
546
|
+
|
|
547
|
+
# Reconstruct todo items
|
|
548
|
+
todo_items_data = context_data.get("todo_items", [])
|
|
549
|
+
self.workflow_context.todo_items = [
|
|
550
|
+
TodoItem(
|
|
551
|
+
index=item["index"],
|
|
552
|
+
text=item["text"],
|
|
553
|
+
terminal_command=item.get("terminal_command"),
|
|
554
|
+
completed=item.get("completed", False),
|
|
555
|
+
confirmed=item.get("confirmed", False),
|
|
556
|
+
attempted=item.get("attempted", False),
|
|
557
|
+
failure_reason=item.get("failure_reason")
|
|
558
|
+
)
|
|
559
|
+
for item in todo_items_data
|
|
560
|
+
]
|
|
561
|
+
|
|
562
|
+
if self.workflow_context.state != WorkflowState.INACTIVE:
|
|
563
|
+
logger.info(f"Restored workflow state: {self.workflow_context.state.value}")
|
|
564
|
+
except Exception as e:
|
|
565
|
+
logger.warning(f"Failed to load workflow state: {e}")
|
|
566
|
+
|
|
567
|
+
def get_status_line(self) -> Dict[str, List[str]]:
|
|
568
|
+
"""Return status information for display."""
|
|
569
|
+
if self.workflow_context.state == WorkflowState.INACTIVE:
|
|
570
|
+
return {"A": [], "B": [], "C": []}
|
|
571
|
+
|
|
572
|
+
status_text = f"Workflow: {self.workflow_context.state.value.title()}"
|
|
573
|
+
|
|
574
|
+
if self.workflow_context.state == WorkflowState.ENFORCING:
|
|
575
|
+
current_todo = self._get_current_todo()
|
|
576
|
+
if current_todo:
|
|
577
|
+
progress = f"{current_todo.index + 1}/{len(self.workflow_context.todo_items)}"
|
|
578
|
+
status_text = f"Workflow: Todo {progress}"
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
"A": [],
|
|
582
|
+
"B": [status_text],
|
|
583
|
+
"C": []
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async def _handle_workflow_response(self, event_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
587
|
+
"""Handle LLM responses during active workflow enforcement."""
|
|
588
|
+
response_content = event_data.get("response", "")
|
|
589
|
+
|
|
590
|
+
# Check if response contains terminal commands (indicates compliance)
|
|
591
|
+
has_terminal_commands = "<terminal>" in response_content and "</terminal>" in response_content
|
|
592
|
+
|
|
593
|
+
current_todo = self._get_current_todo()
|
|
594
|
+
if current_todo and not current_todo.completed:
|
|
595
|
+
if has_terminal_commands:
|
|
596
|
+
# Good! LLM is using terminal commands
|
|
597
|
+
# Transition to waiting for confirmation
|
|
598
|
+
self.workflow_context.state = WorkflowState.WAITING_CONFIRMATION
|
|
599
|
+
|
|
600
|
+
confirmation_prompt = f"""
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
**WORKFLOW CHECK**: I can see you've used terminal commands to work on this todo.
|
|
604
|
+
|
|
605
|
+
**Todo**: {current_todo.text}
|
|
606
|
+
|
|
607
|
+
Please reply "**completed**" when you've finished this todo item, or "**failed [reason]**" if you encountered issues.
|
|
608
|
+
"""
|
|
609
|
+
event_data["response"] = f"{response_content}{confirmation_prompt}"
|
|
610
|
+
|
|
611
|
+
else:
|
|
612
|
+
# LLM not using terminal commands - enforce compliance
|
|
613
|
+
enforcement_reminder = f"""
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
**WORKFLOW VIOLATION**: You must use <terminal> tags for commands!
|
|
617
|
+
|
|
618
|
+
**Current Todo**: {current_todo.text}
|
|
619
|
+
|
|
620
|
+
Please redo this todo using proper <terminal>command</terminal> tags as shown in the examples.
|
|
621
|
+
"""
|
|
622
|
+
event_data["response"] = f"{response_content}{enforcement_reminder}"
|
|
623
|
+
|
|
624
|
+
return event_data
|
|
625
|
+
|
|
626
|
+
async def shutdown(self):
|
|
627
|
+
"""Cleanup when plugin shuts down."""
|
|
628
|
+
await self._save_workflow_state()
|
|
629
|
+
logger.info("Workflow enforcement plugin shutdown")
|