hermes-loop-plugin 1.0.0__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.
@@ -0,0 +1,290 @@
1
+ """Hermes Loop plugin — registration.
2
+
3
+ Provides continuous task execution loop with state persistence and completion promises.
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ from pathlib import Path
9
+
10
+ from . import commands, schemas, tools
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def _on_post_tool_call(tool_name: str, args: dict, result: str, task_id: str, **kwargs):
16
+ """Hook: runs after every tool call to check loop continuation."""
17
+
18
+ # Only process if this is a loop-related tool or we're in a loop session
19
+ loop_tools = ['loop_status', 'set_completion_promise', 'init_loop',
20
+ 'complete_task', 'add_blocking_issue', 'reset_loop']
21
+
22
+ if not any(t in tool_name for t in loop_tools):
23
+ return
24
+
25
+ cwd = kwargs.get('cwd', str(Path.cwd()))
26
+ state_file_path = Path(cwd) / '.hermes-loop-state.json'
27
+
28
+ # Check if we have an active state file
29
+ if not state_file_path.exists():
30
+ return
31
+
32
+ try:
33
+ with open(state_file_path) as f:
34
+ state = json.load(f)
35
+
36
+ total_tasks = state.get('total_tasks', 0)
37
+ completed_tasks = state.get('completed_tasks', 0)
38
+ blocking_issues = state.get('blocking_issues', [])
39
+
40
+ # If all tasks complete, signal loop end
41
+ if completed_tasks >= total_tasks and not blocking_issues:
42
+ logger.info(f"[hermes-loop] All {total_tasks} tasks completed at session {task_id}")
43
+
44
+ # Add completion marker to transcript for detection by stop hook
45
+ transcript_path = kwargs.get('transcript_path')
46
+ if transcript_path:
47
+ try:
48
+ with open(transcript_path, 'a') as f:
49
+ f.write(f"\n# [HERMES-LOOP] ALL_TASKS_COMPLETE at {task_id}\n")
50
+ except Exception as e:
51
+ logger.error(f"Failed to write completion marker: {e}")
52
+
53
+ except Exception as e:
54
+ logger.error(f"[hermes-loop] Error in post_tool_call hook: {e}")
55
+
56
+
57
+ def _on_session_start(session_id: str, platform: str, **kwargs):
58
+ """Hook: runs when a new session starts."""
59
+
60
+ cwd = kwargs.get('cwd', str(Path.cwd()))
61
+ state_file_path = Path(cwd) / '.hermes-loop-state.json'
62
+
63
+ if not state_file_path.exists():
64
+ return
65
+
66
+ try:
67
+ with open(state_file_path) as f:
68
+ state = json.load(f)
69
+
70
+ total_tasks = state.get('total_tasks', 0)
71
+ completed_tasks = state.get('completed_tasks', 0)
72
+
73
+ if total_tasks > 0 and completed_tasks < total_tasks:
74
+ logger.info(
75
+ f"[hermes-loop] Resuming loop from session {session_id}: "
76
+ f"{completed_tasks}/{total_tasks} tasks complete"
77
+ )
78
+
79
+ except Exception as e:
80
+ logger.error(f"[hermes-loop] Error in on_session_start hook: {e}")
81
+
82
+
83
+ def _on_stop_hook(input_data: dict, **kwargs) -> int:
84
+ """Hook: determines if loop should continue or stop.
85
+
86
+ Returns 0 to continue, non-zero to stop.
87
+ This is the main loop controller."""
88
+
89
+ cwd = input_data.get('cwd', str(Path.cwd()))
90
+ transcript_path = input_data.get('transcript_path')
91
+
92
+ state_file_path = Path(cwd) / '.hermes-loop-state.json'
93
+
94
+ # If no state file, nothing to continue
95
+ if not state_file_path.exists():
96
+ return 0
97
+
98
+ try:
99
+ with open(state_file_path) as f:
100
+ state = json.load(f)
101
+
102
+ total_tasks = state.get('total_tasks', 0)
103
+ completed_tasks = state.get('completed_tasks', 0)
104
+ blocking_issues = state.get('blocking_issues', [])
105
+ completion_promise = state.get('completion_promise')
106
+
107
+ # Check for explicit ALL_TASKS_COMPLETE in transcript (backup detection)
108
+ if transcript_path and Path(transcript_path).exists():
109
+ with open(transcript_path) as f:
110
+ content = f.read()
111
+ if 'ALL_TASKS_COMPLETE' in content:
112
+ logger.info("[hermes-loop] Detected ALL_TASKS_COMPLETE marker")
113
+ return 1 # Stop
114
+
115
+ # Check blocking issues first
116
+ if blocking_issues:
117
+ logger.warning(
118
+ f"[hermes-loop] Blocking issues detected, stopping loop: {blocking_issues}"
119
+ )
120
+ return 1
121
+
122
+ # Check completion promise
123
+ if completion_promise and not completion_promise.get('fulfilled', False):
124
+ promise_type = completion_promise.get('promise_type', '')
125
+ condition = completion_promise.get('condition', '')
126
+
127
+ # Evaluate the promise condition
128
+ promise_fulfilled = False
129
+
130
+ if promise_type == 'task_count':
131
+ expected = int(completion_promise.get('expected_value', 0))
132
+ promise_fulfilled = completed_tasks >= expected
133
+
134
+ elif promise_type == 'file_exists':
135
+ check_path = Path(cwd) / condition
136
+ promise_fulfilled = check_path.exists()
137
+
138
+ elif promise_type == 'content_match':
139
+ check_path = Path(cwd) / condition
140
+ if check_path.exists():
141
+ content = check_path.read_text()
142
+ pattern = completion_promise.get('expected_value', '')
143
+ promise_fulfilled = pattern in content
144
+
145
+ # Update fulfilled status
146
+ if promise_fulfilled:
147
+ completion_promise['fulfilled'] = True
148
+ with open(state_file_path, 'w') as f:
149
+ json.dump(state, f, indent=2)
150
+
151
+ if not promise_fulfilled:
152
+ logger.info(
153
+ f"[hermes-loop] Completion promise not yet fulfilled "
154
+ f"({promise_type}: {condition}), continuing loop"
155
+ )
156
+ return 0
157
+
158
+ # Check if all tasks complete
159
+ if completed_tasks >= total_tasks:
160
+ logger.info(f"[hermes-loop] All {total_tasks} tasks completed, stopping loop")
161
+ return 1
162
+
163
+ # Continue the loop
164
+ logger.debug(
165
+ f"[hermes-loop] Continuing loop: {completed_tasks}/{total_tasks} tasks complete"
166
+ )
167
+
168
+ except Exception as e:
169
+ logger.error(f"[hermes-loop] Error in stop hook: {e}")
170
+
171
+ return 0 # Default to continue on error
172
+
173
+
174
+ def register(ctx):
175
+ """Register tools and hooks with Hermes Agent."""
176
+
177
+ # Core loop status tool
178
+ ctx.register_tool(
179
+ name="loop_status",
180
+ toolset="hermes-loop",
181
+ schema=schemas.LOOP_STATUS,
182
+ handler=tools.loop_status
183
+ )
184
+
185
+ # Completion promise tool
186
+ ctx.register_tool(
187
+ name="set_completion_promise",
188
+ toolset="hermes-loop",
189
+ schema=schemas.COMPLETE_PROMISE,
190
+ handler=tools.set_completion_promise
191
+ )
192
+
193
+ # Command-style tools for easier usage
194
+ ctx.register_tool(
195
+ name="init_loop",
196
+ toolset="hermes-loop",
197
+ schema={
198
+ "name": "init_loop",
199
+ "description": (
200
+ "Initialize a new loop state. Creates the state file and sets up task tracking."
201
+ ),
202
+ "parameters": {
203
+ "type": "object",
204
+ "properties": {
205
+ "total_tasks": {
206
+ "type": "integer",
207
+ "description": "Total number of tasks in the loop"
208
+ },
209
+ "promise_type": {
210
+ "type": "string",
211
+ "description": "Optional completion promise type (task_count, file_exists, content_match)"
212
+ },
213
+ "condition": {
214
+ "type": "string",
215
+ "description": "Path/pattern for the promise"
216
+ },
217
+ "expected_value": {
218
+ "type": "string",
219
+ "description": "Expected value for content_match promises"
220
+ }
221
+ },
222
+ "required": ["total_tasks"]
223
+ }
224
+ },
225
+ handler=tools.init_loop
226
+ )
227
+
228
+ # Command-style tools (aliases for easier usage)
229
+ ctx.register_tool(
230
+ name="complete_task",
231
+ toolset="hermes-loop",
232
+ schema={
233
+ "name": "complete_task",
234
+ "description": (
235
+ "Mark next task as completed. Increments the completed tasks counter."
236
+ ),
237
+ "parameters": {
238
+ "type": "object",
239
+ "properties": {},
240
+ "required": []
241
+ }
242
+ },
243
+ handler=tools.complete_task
244
+ )
245
+
246
+ ctx.register_tool(
247
+ name="add_blocking_issue",
248
+ toolset="hermes-loop",
249
+ schema={
250
+ "name": "add_blocking_issue",
251
+ "description": (
252
+ "Add a blocking issue. When issues block progress, the loop will stop automatically."
253
+ ),
254
+ "parameters": {
255
+ "type": "object",
256
+ "properties": {
257
+ "issue": {
258
+ "type": "string",
259
+ "description": "Description of the blocking issue"
260
+ }
261
+ },
262
+ "required": ["issue"]
263
+ }
264
+ },
265
+ handler=tools.add_blocking_issue
266
+ )
267
+
268
+ ctx.register_tool(
269
+ name="reset_loop",
270
+ toolset="hermes-loop",
271
+ schema={
272
+ "name": "reset_loop",
273
+ "description": (
274
+ "Reset loop state. Clears completed task count but keeps total tasks and promise."
275
+ ),
276
+ "parameters": {
277
+ "type": "object",
278
+ "properties": {},
279
+ "required": []
280
+ }
281
+ },
282
+ handler=tools.reset_loop
283
+ )
284
+
285
+ # Register hooks for loop continuation control
286
+ ctx.register_hook("post_tool_call", _on_post_tool_call)
287
+ ctx.register_hook("on_session_start", _on_session_start)
288
+ ctx.register_hook("stop_hook", _on_stop_hook)
289
+
290
+ logger.info("[hermes-loop] Plugin registered with 6 tools and 3 hooks")
@@ -0,0 +1,328 @@
1
+ """Command-style wrappers for hermes-loop tools.
2
+
3
+ These provide more intuitive, slash-command-like interfaces to the loop functionality."""
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+
9
+ class LoopCommands:
10
+ """Convenience class providing command-style access to loop operations."""
11
+
12
+ def __init__(self):
13
+ self.state_file = Path.cwd() / '.hermes-loop-state.json'
14
+
15
+ def init_loop(self, total_tasks: int, promise_type: str = None,
16
+ condition: str = None, expected_value: str = None) -> dict:
17
+ """Initialize a new loop state.
18
+
19
+ Args:
20
+ total_tasks: Total number of tasks in the loop
21
+ promise_type: Optional completion promise type (task_count, file_exists, content_match)
22
+ condition: Path/pattern for the promise
23
+ expected_value: Expected value for content_match promises
24
+
25
+ Returns:
26
+ dict with initialization status
27
+ """
28
+ state = {
29
+ "total_tasks": total_tasks,
30
+ "completed_tasks": 0,
31
+ "blocking_issues": [],
32
+ "created_at": None
33
+ }
34
+
35
+ if promise_type:
36
+ state["completion_promise"] = {
37
+ "promise_type": promise_type,
38
+ "condition": condition,
39
+ "expected_value": expected_value,
40
+ "fulfilled": False
41
+ }
42
+ else:
43
+ state["completion_promise"] = None
44
+
45
+ with open(self.state_file, 'w') as f:
46
+ json.dump(state, f, indent=2)
47
+
48
+ return {
49
+ "success": True,
50
+ "message": f"Loop initialized with {total_tasks} tasks",
51
+ "state_file": str(self.state_file),
52
+ "promise_set": bool(promise_type)
53
+ }
54
+
55
+ def complete_task(self) -> dict:
56
+ """Mark one task as completed.
57
+
58
+ Returns:
59
+ dict with updated state
60
+ """
61
+ if not self.state_file.exists():
62
+ return {
63
+ "success": False,
64
+ "error": "No active loop state found"
65
+ }
66
+
67
+ try:
68
+ with open(self.state_file) as f:
69
+ state = json.load(f)
70
+
71
+ old_count = state['completed_tasks']
72
+ state['completed_tasks'] += 1
73
+
74
+ # Track completion history if this is the first task
75
+ if state['completed_tasks'] == 1 and 'completion_history' not in state:
76
+ state['completion_history'] = []
77
+
78
+ if 'completion_history' in state:
79
+ from datetime import datetime
80
+ state['completion_history'].append({
81
+ "task_index": state['completed_tasks'],
82
+ "timestamp": datetime.now().isoformat()
83
+ })
84
+
85
+ with open(self.state_file, 'w') as f:
86
+ json.dump(state, f, indent=2)
87
+
88
+ return {
89
+ "success": True,
90
+ "previous_count": old_count,
91
+ "new_count": state['completed_tasks'],
92
+ "total_tasks": state['total_tasks'],
93
+ "remaining": state['total_tasks'] - state['completed_tasks']
94
+ }
95
+ except Exception as e:
96
+ return {
97
+ "success": False,
98
+ "error": str(e)
99
+ }
100
+
101
+ def set_promise(self, promise_type: str, condition: str = None,
102
+ expected_value: str = None) -> dict:
103
+ """Set or update the completion promise.
104
+
105
+ Args:
106
+ promise_type: Type of promise (task_count, file_exists, content_match)
107
+ condition: Path/pattern for the promise
108
+ expected_value: Expected value for content_match promises
109
+
110
+ Returns:
111
+ dict with promise status
112
+ """
113
+ if not self.state_file.exists():
114
+ return {
115
+ "success": False,
116
+ "error": "No active loop state found"
117
+ }
118
+
119
+ try:
120
+ with open(self.state_file) as f:
121
+ state = json.load(f)
122
+
123
+ promise = {
124
+ "promise_type": promise_type,
125
+ "condition": condition,
126
+ "expected_value": expected_value,
127
+ "fulfilled": False
128
+ }
129
+
130
+ state['completion_promise'] = promise
131
+
132
+ with open(self.state_file, 'w') as f:
133
+ json.dump(state, f, indent=2)
134
+
135
+ return {
136
+ "success": True,
137
+ "promise_type": promise_type,
138
+ "message": f"Promise set via {promise_type}"
139
+ }
140
+ except Exception as e:
141
+ return {
142
+ "success": False,
143
+ "error": str(e)
144
+ }
145
+
146
+ def add_blocking_issue(self, issue: str) -> dict:
147
+ """Add a blocking issue that will stop the loop.
148
+
149
+ Args:
150
+ issue: Description of the blocking issue
151
+
152
+ Returns:
153
+ dict with update status
154
+ """
155
+ if not self.state_file.exists():
156
+ return {
157
+ "success": False,
158
+ "error": "No active loop state found"
159
+ }
160
+
161
+ try:
162
+ with open(self.state_file) as f:
163
+ state = json.load(f)
164
+
165
+ if issue not in state['blocking_issues']:
166
+ state['blocking_issues'].append(issue)
167
+
168
+ # Add timestamp for tracking
169
+ if 'issues' not in state:
170
+ from datetime import datetime
171
+ state['issues'] = []
172
+ state['issues'].append({
173
+ "issue": issue,
174
+ "timestamp": datetime.now().isoformat()
175
+ })
176
+
177
+ with open(self.state_file, 'w') as f:
178
+ json.dump(state, f, indent=2)
179
+
180
+ return {
181
+ "success": True,
182
+ "message": f"Blocking issue added: '{issue}'",
183
+ "total_issues": len(state['blocking_issues'])
184
+ }
185
+ except Exception as e:
186
+ return {
187
+ "success": False,
188
+ "error": str(e)
189
+ }
190
+
191
+ def status(self) -> dict:
192
+ """Get current loop status.
193
+
194
+ Returns:
195
+ dict with full status information
196
+ """
197
+ if not self.state_file.exists():
198
+ return {
199
+ "active": False,
200
+ "message": "No active loop state found"
201
+ }
202
+
203
+ try:
204
+ with open(self.state_file) as f:
205
+ state = json.load(f)
206
+
207
+ total = state.get('total_tasks', 0)
208
+ completed = state.get('completed_tasks', 0)
209
+ blocking = state.get('blocking_issues', [])
210
+ promise = state.get('completion_promise')
211
+
212
+ # Check if promise is fulfilled
213
+ promise_fulfilled = False
214
+ if promise:
215
+ promise_type = promise.get('promise_type', '')
216
+
217
+ if promise_type == 'task_count':
218
+ expected = int(promise.get('expected_value', 0))
219
+ promise_fulfilled = completed >= expected
220
+
221
+ elif promise_type == 'file_exists':
222
+ check_path = Path.cwd() / promise.get('condition', '')
223
+ promise_fulfilled = check_path.exists()
224
+
225
+ elif promise_type == 'content_match':
226
+ check_path = Path.cwd() / promise.get('condition', '')
227
+ if check_path.exists():
228
+ content = check_path.read_text()
229
+ pattern = promise.get('expected_value', '')
230
+ promise_fulfilled = pattern in content
231
+
232
+ has_remaining = completed < total and not promise_fulfilled
233
+
234
+ return {
235
+ "active": True,
236
+ "has_remaining_tasks": has_remaining,
237
+ "tasks_completed": completed,
238
+ "total_tasks": total,
239
+ "remaining_tasks": total - completed,
240
+ "completion_reached": (not has_remaining) or promise_fulfilled,
241
+ "blocking_issues": blocking,
242
+ "promise_status": {
243
+ "has_promise": bool(promise),
244
+ "type": promise.get('promise_type') if promise else None,
245
+ "fulfilled": promise_fulfilled and promise # Only mark fulfilled if not already done
246
+ } if promise else None
247
+ }
248
+ except Exception as e:
249
+ return {
250
+ "active": False,
251
+ "error": str(e)
252
+ }
253
+
254
+ def reset(self) -> dict:
255
+ """Reset the loop state (clears completed tasks but keeps total).
256
+
257
+ Returns:
258
+ dict with reset status
259
+ """
260
+ if not self.state_file.exists():
261
+ return {
262
+ "success": False,
263
+ "error": "No active loop state found"
264
+ }
265
+
266
+ try:
267
+ with open(self.state_file) as f:
268
+ state = json.load(f)
269
+
270
+ old_completed = state['completed_tasks']
271
+ state['completed_tasks'] = 0
272
+
273
+ # Clear completion history on reset
274
+ if 'completion_history' in state:
275
+ del state['completion_history']
276
+
277
+ with open(self.state_file, 'w') as f:
278
+ json.dump(state, f, indent=2)
279
+
280
+ return {
281
+ "success": True,
282
+ "message": f"Loop reset - cleared {old_completed} completed tasks",
283
+ "total_tasks": state['total_tasks']
284
+ }
285
+ except Exception as e:
286
+ return {
287
+ "success": False,
288
+ "error": str(e)
289
+ }
290
+
291
+
292
+ # Convenience functions for tool handlers to use
293
+
294
+ def init_loop_command(total_tasks: int, promise_type: str = None,
295
+ condition: str = None, expected_value: str = None) -> str:
296
+ """Command-style loop initialization."""
297
+ cmds = LoopCommands()
298
+ result = cmds.init_loop(total_tasks, promise_type, condition, expected_value)
299
+ return json.dumps(result)
300
+
301
+
302
+ def complete_task_command() -> str:
303
+ """Mark next task as completed."""
304
+ cmds = LoopCommands()
305
+ result = cmds.complete_task()
306
+ return json.dumps(result)
307
+
308
+
309
+ def set_promise_command(promise_type: str, condition: str = None,
310
+ expected_value: str = None) -> str:
311
+ """Set completion promise."""
312
+ cmds = LoopCommands()
313
+ result = cmds.set_promise(promise_type, condition, expected_value)
314
+ return json.dumps(result)
315
+
316
+
317
+ def add_blocking_issue_command(issue: str) -> str:
318
+ """Add a blocking issue."""
319
+ cmds = LoopCommands()
320
+ result = cmds.add_blocking_issue(issue)
321
+ return json.dumps(result)
322
+
323
+
324
+ def loop_status_command() -> str:
325
+ """Get current loop status."""
326
+ cmds = LoopCommands()
327
+ result = cmds.status()
328
+ return json.dumps(result, indent=2)
@@ -0,0 +1,43 @@
1
+ """Tool schemas — what the LLM sees for the hermes-loop plugin."""
2
+
3
+ LOOP_STATUS = {
4
+ "name": "loop_status",
5
+ "description": (
6
+ "Check the current status of the task execution loop. "
7
+ "Returns whether tasks remain, if completion is reached, and any blocking issues. "
8
+ "Use this when you need to understand the loop state or debug continuation logic."
9
+ ),
10
+ "parameters": {
11
+ "type": "object",
12
+ "properties": {},
13
+ "required": []
14
+ }
15
+ }
16
+
17
+ COMPLETE_PROMISE = {
18
+ "name": "set_completion_promise",
19
+ "description": (
20
+ "Set a completion promise that defines when the loop should stop. "
21
+ "Use this to specify custom termination conditions beyond automatic task detection. "
22
+ "The loop will continue until either all tasks complete OR this promise is fulfilled."
23
+ ),
24
+ "parameters": {
25
+ "type": "object",
26
+ "properties": {
27
+ "promise_type": {
28
+ "type": "string",
29
+ "description": "Type of completion condition: 'task_count', 'file_exists', 'content_match', or 'custom'",
30
+ "enum": ["task_count", "file_exists", "content_match", "custom"]
31
+ },
32
+ "condition": {
33
+ "type": "string",
34
+ "description": "The specific condition to check (e.g., file path, content pattern, task index)"
35
+ },
36
+ "expected_value": {
37
+ "type": "string",
38
+ "description": "Expected value for the condition (optional)"
39
+ }
40
+ },
41
+ "required": ["promise_type", "condition"]
42
+ }
43
+ }
@@ -0,0 +1,126 @@
1
+ """Tool handlers for hermes-loop plugin.
2
+
3
+ These provide both direct JSON responses and command-style interfaces."""
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+
9
+ def loop_status(args: dict, **kwargs) -> str:
10
+ """Check current loop status (alias for /loop_status).
11
+
12
+ Returns detailed status including completion progress and promise state.
13
+ """
14
+ from .commands import LoopCommands
15
+
16
+ cmds = LoopCommands()
17
+ result = cmds.status()
18
+ return json.dumps(result, indent=2)
19
+
20
+
21
+ def set_completion_promise(args: dict, **kwargs) -> str:
22
+ """Set completion promise (alias for /set_promise).
23
+
24
+ Defines custom termination conditions for the loop.
25
+ """
26
+ from .commands import LoopCommands
27
+
28
+ cmds = LoopCommands()
29
+ result = cmds.set_promise(
30
+ promise_type=args.get('promise_type'),
31
+ condition=args.get('condition'),
32
+ expected_value=args.get('expected_value')
33
+ )
34
+ return json.dumps(result)
35
+
36
+
37
+ def init_loop(args: dict, **kwargs) -> str:
38
+ """Initialize a new loop state (alias for /init_loop).
39
+
40
+ Creates the state file and sets up task tracking.
41
+
42
+ Args:
43
+ total_tasks: Total number of tasks in the loop
44
+ promise_type: Optional completion promise type
45
+ condition: Path/pattern for the promise
46
+ expected_value: Expected value for content_match promises
47
+ """
48
+ from .commands import LoopCommands
49
+
50
+ cmds = LoopCommands()
51
+ result = cmds.init_loop(
52
+ total_tasks=args.get('total_tasks', 0),
53
+ promise_type=args.get('promise_type'),
54
+ condition=args.get('condition'),
55
+ expected_value=args.get('expected_value')
56
+ )
57
+ return json.dumps(result)
58
+
59
+
60
+ def complete_task(args: dict, **kwargs) -> str:
61
+ """Complete next task (alias for /complete_task).
62
+
63
+ Increments the completed tasks counter.
64
+ Use this after each subagent or implementation step.
65
+ """
66
+ from .commands import LoopCommands
67
+
68
+ cmds = LoopCommands()
69
+ result = cmds.complete_task()
70
+ return json.dumps(result)
71
+
72
+
73
+ def add_blocking_issue(args: dict, **kwargs) -> str:
74
+ """Add a blocking issue (alias for /add_blocking_issue).
75
+
76
+ When issues block progress, the loop will stop automatically.
77
+ Use this when you hit an error that requires manual intervention.
78
+
79
+ Args:
80
+ issue: Description of the blocking issue
81
+ """
82
+ from .commands import LoopCommands
83
+
84
+ cmds = LoopCommands()
85
+ result = cmds.add_blocking_issue(args.get('issue', ''))
86
+ return json.dumps(result)
87
+
88
+
89
+ def reset_loop(args: dict, **kwargs) -> str:
90
+ """Reset loop state (alias for /reset_loop).
91
+
92
+ Clears completed task count but keeps total tasks and promise.
93
+ Use this to restart iteration from the beginning.
94
+ """
95
+ from .commands import LoopCommands
96
+
97
+ cmds = LoopCommands()
98
+ result = cmds.reset()
99
+ return json.dumps(result)
100
+
101
+
102
+ # Command-style aliases for more intuitive usage
103
+
104
+ def command_init_loop(args: dict, **kwargs) -> str:
105
+ """Command-style loop initialization."""
106
+ return init_loop(args, **kwargs)
107
+
108
+
109
+ def command_complete_task(args: dict, **kwargs) -> str:
110
+ """Mark next task as completed."""
111
+ return complete_task(args, **kwargs)
112
+
113
+
114
+ def command_set_promise(args: dict, **kwargs) -> str:
115
+ """Set completion promise."""
116
+ return set_completion_promise(args, **kwargs)
117
+
118
+
119
+ def command_add_blocking_issue(args: dict, **kwargs) -> str:
120
+ """Add a blocking issue."""
121
+ return add_blocking_issue(args, **kwargs)
122
+
123
+
124
+ def command_loop_status(args: dict, **kwargs) -> str:
125
+ """Get current loop status."""
126
+ return loop_status(args, **kwargs)
@@ -0,0 +1,471 @@
1
+ Metadata-Version: 2.4
2
+ Name: hermes-loop-plugin
3
+ Version: 1.0.0
4
+ Summary: Continuous task execution loop plugin for Hermes Agent
5
+ Author-email: Hermes Agent Community <community@hermes-agent.dev>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/NousResearch/hermes-loop-plugin
8
+ Project-URL: Documentation, https://github.com/NousResearch/hermes-loop-plugin#readme
9
+ Project-URL: Repository, https://github.com/NousResearch/hermes-loop-plugin.git
10
+ Project-URL: Issues, https://github.com/NousResearch/hermes-loop-plugin/issues
11
+ Keywords: hermes-agent,plugin,loop,iteration,automation
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Dynamic: license-file
24
+
25
+ # Hermes Loop Plugin
26
+
27
+ Continuous task execution loop that keeps the agent running until goals are completed via state file monitoring and optional completion promises.
28
+
29
+ ## Overview
30
+
31
+ This plugin enables **persistent multi-step workflows** where tasks span multiple sessions or require iterative refinement. It works similarly to Claude Code's ralph-wiggum-loop but is designed specifically for Hermes Agent's plugin architecture.
32
+
33
+ ## Features
34
+
35
+ - **State Persistence**: Tracks task progress via `.hermes-loop-state.json`
36
+ - **Completion Promises**: Define custom termination conditions (file exists, content match, task count)
37
+ - **Automatic Resumption**: Continues from where it left off across sessions
38
+ - **Blocking Detection**: Stops loop when critical issues prevent progress
39
+ - **Command-style Tools**: Intuitive tool names that work like slash commands!
40
+
41
+ ### Available Commands
42
+
43
+ | Command | Description |
44
+ |---------|-------------|
45
+ | `init_loop` | Initialize a new loop state |
46
+ | `loop_status` | Check current loop status |
47
+ | `complete_task` | Mark next task as completed |
48
+ | `set_completion_promise` | Define custom termination condition |
49
+ | `add_blocking_issue` | Add blocker that stops loop |
50
+ | `reset_loop` | Reset completed count |
51
+
52
+ ## Installation
53
+
54
+ ### From GitHub (Current) 🚀
55
+
56
+ The plugin is available for direct installation from our GitHub repository:
57
+
58
+ ```bash
59
+ pip install git+https://github.com/AshMartian/hermes-loop-plugin.git@v1.0.0
60
+ ```
61
+
62
+ After installation, the plugin will be automatically discovered when you start Hermes Agent. No additional configuration required!
63
+
64
+ ### Verify Installation
65
+
66
+ Check that the plugin is installed correctly:
67
+
68
+ ```bash
69
+ # Check if package is installed
70
+ pip show hermes-loop-plugin
71
+
72
+ # Or verify plugin location (if using local development)
73
+ ls ~/.hermes/plugins/hermes-loop/
74
+ ```
75
+
76
+ You should see:
77
+ - `plugin.yaml` - Plugin manifest
78
+ - `__init__.py` - Registration and hooks
79
+ - `schemas.py` - Tool schemas (what LLM sees)
80
+ - `tools.py` - Tool handlers (actual implementation)
81
+ - `SKILL.md` - Comprehensive usage documentation
82
+
83
+ ### Local Development Installation
84
+
85
+ For development or custom builds, install from source:
86
+
87
+ ```bash
88
+ # Clone the repository
89
+ git clone https://github.com/AshMartian/hermes-loop-plugin.git
90
+ cd hermes-loop-plugin
91
+
92
+ # Install in editable mode for development
93
+ pip install -e .
94
+
95
+ # Or build and install manually
96
+ python -m build
97
+ pip install dist/hermes_loop_plugin-*.whl
98
+ ```
99
+
100
+ ### Manual Installation (for advanced users)
101
+
102
+ If you need to install the plugin manually:
103
+
104
+ 1. Download the package from [GitHub Releases](https://github.com/AshMartian/hermes-loop-plugin/releases) or clone the repository
105
+ 2. Install using pip: `pip install dist/hermes_loop_plugin-X.X.X.tar.gz`
106
+ 3. Restart Hermes Agent to load the plugin
107
+
108
+ ### 📦 PyPI Installation (Coming Soon!)
109
+
110
+ The plugin will be available on [PyPI](https://pypi.org/) for easier installation:
111
+
112
+ ```bash
113
+ # Coming soon!
114
+ pip install hermes-loop-plugin
115
+ ```
116
+
117
+ **Track progress:** Check our [GitHub Releases](https://github.com/AshMartian/hermes-loop-plugin/releases) for updates when we publish to PyPI.
118
+
119
+ ## Quick Start
120
+
121
+ ### 1. Initialize a loop state
122
+
123
+ Create a `.hermes-loop-state.json` file in your working directory:
124
+
125
+ ```python
126
+ from pathlib import Path
127
+ import json
128
+
129
+ state_file = Path.cwd() / '.hermes-loop-state.json'
130
+
131
+ with open(state_file, 'w') as f:
132
+ json.dump({
133
+ "total_tasks": 5,
134
+ "completed_tasks": 0,
135
+ "blocking_issues": [],
136
+ "completion_promise": {
137
+ "promise_type": "file_exists",
138
+ "condition": "src/features/new-feature.tsx"
139
+ }
140
+ }, f)
141
+ ```
142
+
143
+ ### 2. Set completion promise (optional)
144
+
145
+ Use the tool to define custom termination conditions:
146
+
147
+ ```python
148
+ set_completion_promise(
149
+ promise_type="content_match",
150
+ condition="tests/feature.test.ts",
151
+ expected_value="describe('Feature'"
152
+ )
153
+ ```
154
+
155
+ ### 3. Execute tasks
156
+
157
+ Run your tasks via subagents or direct implementation, updating the state after each completion:
158
+
159
+ ```python
160
+ from pathlib import Path
161
+
162
+ # Task 1
163
+ delegate_task(goal="Implement feature step 1")
164
+
165
+ # Update state
166
+ state_file = Path.cwd() / '.hermes-loop-state.json'
167
+ with open(state_file) as f:
168
+ state = json.load(f)
169
+ state['completed_tasks'] += 1
170
+ with open(state_file, 'w') as f:
171
+ json.dump(state, f, indent=2)
172
+
173
+ # Repeat for remaining tasks...
174
+ ```
175
+
176
+ ### 4. Loop continues automatically
177
+
178
+ The plugin's stop hook monitors the state file and determines whether to continue or stop execution based on:
179
+ - Task completion count vs total
180
+ - Completion promise fulfillment status
181
+ - Blocking issues present
182
+
183
+ ## State File Format
184
+
185
+ ```json
186
+ {
187
+ "total_tasks": 5,
188
+ "completed_tasks": 2,
189
+ "blocking_issues": [],
190
+ "completion_promise": {
191
+ "promise_type": "file_exists",
192
+ "condition": "src/features/new-feature.tsx",
193
+ "expected_value": null,
194
+ "fulfilled": false
195
+ },
196
+ "created_at": "2026-03-17T09:38:00Z"
197
+ }
198
+ ```
199
+
200
+ **Fields:**
201
+ - `total_tasks`: Total number of tasks in the loop
202
+ - `completed_tasks`: Number completed so far
203
+ - `blocking_issues`: List of issues that should stop the loop
204
+ - `completion_promise`: Optional custom termination condition
205
+ - `created_at`: Timestamp when loop started
206
+
207
+ ## Completion Promise Types
208
+
209
+ ### 1. Task Count
210
+
211
+ Stop after completing a specific number of tasks:
212
+
213
+ ```json
214
+ {
215
+ "promise_type": "task_count",
216
+ "condition": null,
217
+ "expected_value": "3"
218
+ }
219
+ ```
220
+
221
+ **Use case:** Continue until I've tried at least 3 debugging approaches
222
+
223
+ ### 2. File Exists
224
+
225
+ Stop when a specific file is created:
226
+
227
+ ```json
228
+ {
229
+ "promise_type": "file_exists",
230
+ "condition": "src/features/new-feature.tsx"
231
+ }
232
+ ```
233
+
234
+ **Use case:** Keep implementing until the feature file exists
235
+
236
+ ### 3. Content Match
237
+
238
+ Stop when a file contains specific content:
239
+
240
+ ```json
241
+ {
242
+ "promise_type": "content_match",
243
+ "condition": "src/utils/validation.ts",
244
+ "expected_value": "export function validateInput("
245
+ }
246
+ ```
247
+
248
+ **Use case:** Continue until validation logic is implemented
249
+
250
+ ### 4. Custom
251
+
252
+ Define your own condition via tool implementation:
253
+
254
+ ```json
255
+ {
256
+ "promise_type": "custom",
257
+ "condition": "my_custom_condition"
258
+ }
259
+ ```
260
+
261
+ **Use case:** Complex conditions requiring custom logic (extend the plugin)
262
+
263
+ ## Tools Provided
264
+
265
+ ### `loop_status`
266
+
267
+ Check current loop state and whether continuation is needed.
268
+
269
+ **Parameters:** None
270
+
271
+ **Returns:** JSON with:
272
+ - `has_remaining_tasks`: true if tasks remain
273
+ - `tasks_completed`: number completed so far
274
+ - `total_tasks`: total tasks in loop
275
+ - `completion_reached`: true if all conditions met
276
+ - `blocking_issues`: list of blocking issues (if any)
277
+
278
+ ### `set_completion_promise`
279
+
280
+ Set custom termination condition for the loop.
281
+
282
+ **Parameters:**
283
+ - `promise_type`: Type of condition (`task_count`, `file_exists`, `content_match`, `custom`)
284
+ - `condition`: Specific condition to check (e.g., file path, content pattern)
285
+ - `expected_value`: Expected value for condition (optional)
286
+
287
+ ## Integration Patterns
288
+
289
+ ### With subagent-driven-development
290
+
291
+ Use the loop to keep subagents running across sessions:
292
+
293
+ ```python
294
+ # Initialize state
295
+ state_file = Path.cwd() / '.hermes-loop-state.json'
296
+ with open(state_file, 'w') as f:
297
+ json.dump({"total_tasks": 5, "completed_tasks": 0}, f)
298
+
299
+ # Dispatch tasks via subagents (loop continues automatically)
300
+ delegate_task(goal="Implement Task 1")
301
+ mark_task_complete(state_file)
302
+
303
+ delegate_task(goal="Implement Task 2")
304
+ mark_task_complete(state_file)
305
+
306
+ # Loop will continue until all 5 tasks complete
307
+ ```
308
+
309
+ ### With writing-plans
310
+
311
+ Use loop for plans requiring iteration:
312
+
313
+ ```python
314
+ plan = read_file("docs/plans/feature-plan.md")
315
+ tasks = parse_plan_tasks(plan)
316
+
317
+ state_file = Path.cwd() / '.hermes-loop-state.json'
318
+ with open(state_file, 'w') as f:
319
+ json.dump({"total_tasks": len(tasks), "completed_tasks": 0}, f)
320
+
321
+ # Execute tasks (loop continues automatically across sessions)
322
+ ```
323
+
324
+ ### With systematic-debugging
325
+
326
+ Loop works seamlessly with debugging workflows:
327
+
328
+ ```python
329
+ state_file = Path.cwd() / '.hermes-loop-state.json'
330
+ with open(state_file, 'w') as f:
331
+ json.dump({
332
+ "total_tasks": 10,
333
+ "completed_tasks": 0,
334
+ "blocking_issues": [],
335
+ "completion_promise": {
336
+ "promise_type": "content_match",
337
+ "condition": "src/app.tsx",
338
+ "expected_value": "// Bug fixed"
339
+ }
340
+ }, f)
341
+
342
+ # Follow systematic-debugging process:
343
+ # 1. Identify symptoms
344
+ # 2. Form hypothesis
345
+ # 3. Test hypothesis
346
+ # 4. Update state if test fails
347
+ # 5. Loop continues until bug fixed or max iterations
348
+ ```
349
+
350
+ ## Debugging
351
+
352
+ ### Check loop status
353
+
354
+ ```python
355
+ status = loop_status()
356
+ print(f"Remaining: {status['has_remaining_tasks']}")
357
+ print(f"Completed: {status['tasks_completed']}/{status['total_tasks']}")
358
+ ```
359
+
360
+ ### Inspect state file directly
361
+
362
+ ```bash
363
+ cat .hermes-loop-state.json
364
+ ```
365
+
366
+ ### Force stop the loop
367
+
368
+ If loop is stuck, remove or rename the state file:
369
+
370
+ ```bash
371
+ mv .hermes-loop-state.json .hermes-loop-state.json.backup
372
+ # Loop will detect missing state and stop
373
+ ```
374
+
375
+ ## Extending the Plugin
376
+
377
+ To add new promise types or customize behavior:
378
+
379
+ 1. **Update `schemas.py`** - Add new enum values to tool parameters
380
+ 2. **Modify `tools.py`** - Implement handler logic for new features
381
+ 3. **Edit `__init__.py`** - Update `_on_stop_hook()` to evaluate new conditions
382
+
383
+ Example: Add HTTP status promise type:
384
+
385
+ ```python
386
+ # In __init__.py, add to _on_stop_hook:
387
+ elif promise_type == 'http_status':
388
+ import requests
389
+ url = completion_promise.get('condition', '')
390
+ response = requests.get(url)
391
+ expected_status = int(completion_promise.get('expected_value', 200))
392
+ promise_fulfilled = (response.status_code == expected_status)
393
+ ```
394
+
395
+ ## Security Considerations
396
+
397
+ ### State file permissions
398
+
399
+ Ensure state files are not world-readable if they contain sensitive data:
400
+
401
+ ```bash
402
+ chmod 600 .hermes-loop-state.json
403
+ ```
404
+
405
+ ### Validate promise conditions
406
+
407
+ Prevent path traversal attacks by validating promise conditions:
408
+
409
+ ```python
410
+ def validate_promise_condition(condition: str) -> bool:
411
+ """Prevent path traversal attacks."""
412
+ if '..' in condition or condition.startswith('/'):
413
+ return False # Reject absolute paths and path traversal
414
+ return True
415
+ ```
416
+
417
+ ## Comparison with Claude Code's ralph-wiggum-loop
418
+
419
+ | Feature | Hermes Loop | Ralph Wiggum Loop |
420
+ |---------|-------------|-------------------|
421
+ | Platform | Hermes Agent | Claude Code |
422
+ | State file format | JSON | JSON (similar) |
423
+ | Promise types | 4 built-in (task_count, file_exists, content_match, custom) | Similar |
424
+ | Hook integration | post_tool_call, on_session_start, stop_hook | Similar pattern |
425
+ | Tool interface | Explicit tools (loop_status, set_completion_promise) | Implicit via commands |
426
+ | Installation | ~/.hermes/plugins/ | .claude-plugin/ |
427
+
428
+ The Hermes Loop plugin follows the same conceptual model as ralph-wiggum but is adapted for Hermes Agent's tool-based architecture.
429
+
430
+ ## License
431
+
432
+ MIT License - see LICENSE file in repository root
433
+
434
+ **Repository:** [https://github.com/AshMartian/hermes-loop-plugin](https://github.com/AshMartian/hermes-loop-plugin)
435
+
436
+ ## Contributing
437
+
438
+ Contributions welcome! To contribute:
439
+
440
+ 1. Fork the repository
441
+ 2. Create a feature branch
442
+ 3. Make your changes
443
+ 4. Update documentation
444
+ 5. Submit a pull request
445
+
446
+ ### Plugin structure for contributors
447
+
448
+ ```
449
+ hermes-loop/
450
+ ├── plugin.yaml # Plugin manifest
451
+ ├── __init__.py # Registration and hooks
452
+ ├── schemas.py # Tool schemas (LLM-facing)
453
+ ├── tools.py # Tool handlers (implementation)
454
+ ├── SKILL.md # Usage documentation
455
+ └── README.md # This file
456
+ ```
457
+
458
+ ## Resources
459
+
460
+ - [Hermes Agent Plugin Documentation](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/guides/build-a-hermes-plugin.md)
461
+ - [Subagent-Driven Development Skill](~/.hermes/skills/software-development/subagent-driven-development/SKILL.md)
462
+ - [Writing Plans Skill](~/.hermes/skills/software-development/writing-plans/SKILL.md)
463
+
464
+ ## Version History
465
+
466
+ ### v1.0.0 (2026-03-17)
467
+
468
+ - Initial release with core loop functionality
469
+ - Support for task_count, file_exists, content_match promise types
470
+ - Automatic state persistence and resumption
471
+ - Stop hook integration for loop continuation control
@@ -0,0 +1,10 @@
1
+ hermes_loop_plugin/__init__.py,sha256=SgNmDb0Uhun_yXSszooVSG41DzHs-ZnWsKSrcJ5ObIA,10171
2
+ hermes_loop_plugin/commands.py,sha256=6iMnLpeCBiK99S4yqSymSB38fKhxhhj5rAkbyOuhN_w,11186
3
+ hermes_loop_plugin/schemas.py,sha256=WaFNvQsZaS65C_fj2HuE9d7E94MA1EskJuJ4b1YA5Go,1610
4
+ hermes_loop_plugin/tools.py,sha256=WSDdI0VVHlvakWOKvYbD818ew7FLBwJA57o-xnJHrrc,3611
5
+ hermes_loop_plugin-1.0.0.dist-info/licenses/LICENSE,sha256=0-Ba0Itudgl1DfFJZM0eTtHpYyOIywhwDm7EFZHPVCo,1079
6
+ hermes_loop_plugin-1.0.0.dist-info/METADATA,sha256=dzrfK_MpFWQglAIPeHXs5dvOSn7Y_yvQ4SBcdwudwAw,13220
7
+ hermes_loop_plugin-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ hermes_loop_plugin-1.0.0.dist-info/entry_points.txt,sha256=FFWtkXHYnVLUyNo5r6_QVauw90iQdP3gJ3zMGiGH_mg,65
9
+ hermes_loop_plugin-1.0.0.dist-info/top_level.txt,sha256=Sr9EdLfHHxs06hjHwbP44kon4k6oNaTPk2ALL7r3lts,19
10
+ hermes_loop_plugin-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [hermes_agent.plugins]
2
+ hermes-loop = hermes_loop_plugin:register
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hermes Agent Community
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ hermes_loop_plugin