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.
Files changed (128) hide show
  1. core/__init__.py +18 -0
  2. core/application.py +578 -0
  3. core/cli.py +193 -0
  4. core/commands/__init__.py +43 -0
  5. core/commands/executor.py +277 -0
  6. core/commands/menu_renderer.py +319 -0
  7. core/commands/parser.py +186 -0
  8. core/commands/registry.py +331 -0
  9. core/commands/system_commands.py +479 -0
  10. core/config/__init__.py +7 -0
  11. core/config/llm_task_config.py +110 -0
  12. core/config/loader.py +501 -0
  13. core/config/manager.py +112 -0
  14. core/config/plugin_config_manager.py +346 -0
  15. core/config/plugin_schema.py +424 -0
  16. core/config/service.py +399 -0
  17. core/effects/__init__.py +1 -0
  18. core/events/__init__.py +12 -0
  19. core/events/bus.py +129 -0
  20. core/events/executor.py +154 -0
  21. core/events/models.py +258 -0
  22. core/events/processor.py +176 -0
  23. core/events/registry.py +289 -0
  24. core/fullscreen/__init__.py +19 -0
  25. core/fullscreen/command_integration.py +290 -0
  26. core/fullscreen/components/__init__.py +12 -0
  27. core/fullscreen/components/animation.py +258 -0
  28. core/fullscreen/components/drawing.py +160 -0
  29. core/fullscreen/components/matrix_components.py +177 -0
  30. core/fullscreen/manager.py +302 -0
  31. core/fullscreen/plugin.py +204 -0
  32. core/fullscreen/renderer.py +282 -0
  33. core/fullscreen/session.py +324 -0
  34. core/io/__init__.py +52 -0
  35. core/io/buffer_manager.py +362 -0
  36. core/io/config_status_view.py +272 -0
  37. core/io/core_status_views.py +410 -0
  38. core/io/input_errors.py +313 -0
  39. core/io/input_handler.py +2655 -0
  40. core/io/input_mode_manager.py +402 -0
  41. core/io/key_parser.py +344 -0
  42. core/io/layout.py +587 -0
  43. core/io/message_coordinator.py +204 -0
  44. core/io/message_renderer.py +601 -0
  45. core/io/modal_interaction_handler.py +315 -0
  46. core/io/raw_input_processor.py +946 -0
  47. core/io/status_renderer.py +845 -0
  48. core/io/terminal_renderer.py +586 -0
  49. core/io/terminal_state.py +551 -0
  50. core/io/visual_effects.py +734 -0
  51. core/llm/__init__.py +26 -0
  52. core/llm/api_communication_service.py +863 -0
  53. core/llm/conversation_logger.py +473 -0
  54. core/llm/conversation_manager.py +414 -0
  55. core/llm/file_operations_executor.py +1401 -0
  56. core/llm/hook_system.py +402 -0
  57. core/llm/llm_service.py +1629 -0
  58. core/llm/mcp_integration.py +386 -0
  59. core/llm/message_display_service.py +450 -0
  60. core/llm/model_router.py +214 -0
  61. core/llm/plugin_sdk.py +396 -0
  62. core/llm/response_parser.py +848 -0
  63. core/llm/response_processor.py +364 -0
  64. core/llm/tool_executor.py +520 -0
  65. core/logging/__init__.py +19 -0
  66. core/logging/setup.py +208 -0
  67. core/models/__init__.py +5 -0
  68. core/models/base.py +23 -0
  69. core/plugins/__init__.py +13 -0
  70. core/plugins/collector.py +212 -0
  71. core/plugins/discovery.py +386 -0
  72. core/plugins/factory.py +263 -0
  73. core/plugins/registry.py +152 -0
  74. core/storage/__init__.py +5 -0
  75. core/storage/state_manager.py +84 -0
  76. core/ui/__init__.py +6 -0
  77. core/ui/config_merger.py +176 -0
  78. core/ui/config_widgets.py +369 -0
  79. core/ui/live_modal_renderer.py +276 -0
  80. core/ui/modal_actions.py +162 -0
  81. core/ui/modal_overlay_renderer.py +373 -0
  82. core/ui/modal_renderer.py +591 -0
  83. core/ui/modal_state_manager.py +443 -0
  84. core/ui/widget_integration.py +222 -0
  85. core/ui/widgets/__init__.py +27 -0
  86. core/ui/widgets/base_widget.py +136 -0
  87. core/ui/widgets/checkbox.py +85 -0
  88. core/ui/widgets/dropdown.py +140 -0
  89. core/ui/widgets/label.py +78 -0
  90. core/ui/widgets/slider.py +185 -0
  91. core/ui/widgets/text_input.py +224 -0
  92. core/utils/__init__.py +11 -0
  93. core/utils/config_utils.py +656 -0
  94. core/utils/dict_utils.py +212 -0
  95. core/utils/error_utils.py +275 -0
  96. core/utils/key_reader.py +171 -0
  97. core/utils/plugin_utils.py +267 -0
  98. core/utils/prompt_renderer.py +151 -0
  99. kollabor-0.4.9.dist-info/METADATA +298 -0
  100. kollabor-0.4.9.dist-info/RECORD +128 -0
  101. kollabor-0.4.9.dist-info/WHEEL +5 -0
  102. kollabor-0.4.9.dist-info/entry_points.txt +2 -0
  103. kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
  104. kollabor-0.4.9.dist-info/top_level.txt +4 -0
  105. kollabor_cli_main.py +20 -0
  106. plugins/__init__.py +1 -0
  107. plugins/enhanced_input/__init__.py +18 -0
  108. plugins/enhanced_input/box_renderer.py +103 -0
  109. plugins/enhanced_input/box_styles.py +142 -0
  110. plugins/enhanced_input/color_engine.py +165 -0
  111. plugins/enhanced_input/config.py +150 -0
  112. plugins/enhanced_input/cursor_manager.py +72 -0
  113. plugins/enhanced_input/geometry.py +81 -0
  114. plugins/enhanced_input/state.py +130 -0
  115. plugins/enhanced_input/text_processor.py +115 -0
  116. plugins/enhanced_input_plugin.py +385 -0
  117. plugins/fullscreen/__init__.py +9 -0
  118. plugins/fullscreen/example_plugin.py +327 -0
  119. plugins/fullscreen/matrix_plugin.py +132 -0
  120. plugins/hook_monitoring_plugin.py +1299 -0
  121. plugins/query_enhancer_plugin.py +350 -0
  122. plugins/save_conversation_plugin.py +502 -0
  123. plugins/system_commands_plugin.py +93 -0
  124. plugins/tmux_plugin.py +795 -0
  125. plugins/workflow_enforcement_plugin.py +629 -0
  126. system_prompt/default.md +1286 -0
  127. system_prompt/default_win.md +265 -0
  128. 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")