henchman-ai 0.1.9__py3-none-any.whl → 0.1.11__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.
- henchman/cli/app.py +17 -2
- henchman/cli/commands/__init__.py +2 -0
- henchman/cli/commands/builtins.py +3 -0
- henchman/cli/commands/rag.py +210 -0
- henchman/cli/console.py +6 -5
- henchman/cli/prompts.py +171 -70
- henchman/cli/repl.py +26 -2
- henchman/config/__init__.py +4 -0
- henchman/config/schema.py +60 -21
- henchman/rag/__init__.py +38 -0
- henchman/rag/chunker.py +206 -0
- henchman/rag/concurrency.py +206 -0
- henchman/rag/embedder.py +148 -0
- henchman/rag/indexer.py +373 -0
- henchman/rag/repo_id.py +199 -0
- henchman/rag/store.py +258 -0
- henchman/rag/system.py +279 -0
- henchman/tools/builtins/__init__.py +2 -0
- henchman/tools/builtins/rag_search.py +146 -0
- henchman/version.py +1 -1
- {henchman_ai-0.1.9.dist-info → henchman_ai-0.1.11.dist-info}/METADATA +3 -1
- {henchman_ai-0.1.9.dist-info → henchman_ai-0.1.11.dist-info}/RECORD +25 -15
- {henchman_ai-0.1.9.dist-info → henchman_ai-0.1.11.dist-info}/WHEEL +0 -0
- {henchman_ai-0.1.9.dist-info → henchman_ai-0.1.11.dist-info}/entry_points.txt +0 -0
- {henchman_ai-0.1.9.dist-info → henchman_ai-0.1.11.dist-info}/licenses/LICENSE +0 -0
henchman/cli/app.py
CHANGED
|
@@ -67,6 +67,7 @@ def _run_interactive(output_format: str, plan_mode: bool = False) -> None:
|
|
|
67
67
|
"""
|
|
68
68
|
from henchman.cli.repl import Repl, ReplConfig
|
|
69
69
|
from henchman.config import ContextLoader, load_settings
|
|
70
|
+
from henchman.rag import initialize_rag
|
|
70
71
|
|
|
71
72
|
provider = _get_provider()
|
|
72
73
|
settings = load_settings()
|
|
@@ -83,6 +84,12 @@ def _run_interactive(output_format: str, plan_mode: bool = False) -> None:
|
|
|
83
84
|
settings=settings
|
|
84
85
|
)
|
|
85
86
|
|
|
87
|
+
# Initialize RAG system
|
|
88
|
+
rag_system = initialize_rag(settings.rag, console=console)
|
|
89
|
+
if rag_system:
|
|
90
|
+
repl.tool_registry.register(rag_system.search_tool)
|
|
91
|
+
repl.rag_system = rag_system
|
|
92
|
+
|
|
86
93
|
# Set plan mode if requested
|
|
87
94
|
if plan_mode and repl.session:
|
|
88
95
|
repl.session.plan_mode = True
|
|
@@ -111,10 +118,12 @@ def _run_headless(prompt: str, output_format: str, plan_mode: bool = False) -> N
|
|
|
111
118
|
"""
|
|
112
119
|
from henchman.cli.json_output import JsonOutputRenderer
|
|
113
120
|
from henchman.cli.repl import Repl, ReplConfig
|
|
114
|
-
from henchman.config import ContextLoader
|
|
121
|
+
from henchman.config import ContextLoader, load_settings
|
|
115
122
|
from henchman.core.events import EventType
|
|
123
|
+
from henchman.rag import initialize_rag
|
|
116
124
|
|
|
117
125
|
provider = _get_provider()
|
|
126
|
+
settings = load_settings()
|
|
118
127
|
|
|
119
128
|
# Load context from MLG.md files
|
|
120
129
|
context_loader = ContextLoader()
|
|
@@ -126,7 +135,13 @@ def _run_headless(prompt: str, output_format: str, plan_mode: bool = False) -> N
|
|
|
126
135
|
system_prompt += PLAN_MODE_PROMPT
|
|
127
136
|
|
|
128
137
|
config = ReplConfig(system_prompt=system_prompt)
|
|
129
|
-
repl = Repl(provider=provider, console=console, config=config)
|
|
138
|
+
repl = Repl(provider=provider, console=console, config=config, settings=settings)
|
|
139
|
+
|
|
140
|
+
# Initialize RAG system
|
|
141
|
+
rag_system = initialize_rag(settings.rag) # No console output in headless
|
|
142
|
+
if rag_system:
|
|
143
|
+
repl.tool_registry.register(rag_system.search_tool)
|
|
144
|
+
repl.rag_system = rag_system
|
|
130
145
|
|
|
131
146
|
# Set plan mode if requested
|
|
132
147
|
if plan_mode and repl.session: # pragma: no cover
|
|
@@ -49,6 +49,7 @@ class CommandContext:
|
|
|
49
49
|
agent: Agent instance if available.
|
|
50
50
|
tool_registry: ToolRegistry instance if available.
|
|
51
51
|
session: Current Session if available.
|
|
52
|
+
repl: REPL instance if available.
|
|
52
53
|
"""
|
|
53
54
|
|
|
54
55
|
console: Console
|
|
@@ -57,6 +58,7 @@ class CommandContext:
|
|
|
57
58
|
agent: Agent | None = None
|
|
58
59
|
tool_registry: ToolRegistry | None = None
|
|
59
60
|
session: Session | None = None
|
|
61
|
+
repl: object | None = None
|
|
60
62
|
|
|
61
63
|
|
|
62
64
|
class Command(ABC):
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
from henchman.cli.commands import Command, CommandContext
|
|
9
9
|
from henchman.cli.commands.plan import PlanCommand
|
|
10
|
+
from henchman.cli.commands.rag import RagCommand
|
|
10
11
|
from henchman.cli.commands.skill import SkillCommand
|
|
11
12
|
from henchman.cli.commands.unlimited import UnlimitedCommand
|
|
12
13
|
|
|
@@ -50,6 +51,7 @@ class HelpCommand(Command):
|
|
|
50
51
|
ctx.console.print("\n[bold blue]Henchman-AI Commands[/]\n")
|
|
51
52
|
ctx.console.print(" /help - Show this help message")
|
|
52
53
|
ctx.console.print(" /plan - Toggle Plan Mode (Read-Only)")
|
|
54
|
+
ctx.console.print(" /rag - Manage semantic search index")
|
|
53
55
|
ctx.console.print(" /skill - Manage and execute learned skills")
|
|
54
56
|
ctx.console.print(" /quit - Exit the CLI")
|
|
55
57
|
ctx.console.print(" /clear - Clear the screen")
|
|
@@ -205,6 +207,7 @@ def get_builtin_commands() -> list[Command]:
|
|
|
205
207
|
ClearCommand(),
|
|
206
208
|
ToolsCommand(),
|
|
207
209
|
PlanCommand(),
|
|
210
|
+
RagCommand(),
|
|
208
211
|
SkillCommand(),
|
|
209
212
|
UnlimitedCommand(),
|
|
210
213
|
]
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""RAG system management command.
|
|
2
|
+
|
|
3
|
+
This module provides the /rag command for managing the RAG index.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from henchman.cli.commands import Command, CommandContext
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from henchman.rag.system import RagSystem
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RagCommand(Command):
|
|
18
|
+
"""/rag command for RAG index management."""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def name(self) -> str:
|
|
22
|
+
"""Command name.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Command name string.
|
|
26
|
+
"""
|
|
27
|
+
return "rag"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def description(self) -> str:
|
|
31
|
+
"""Command description.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Description string.
|
|
35
|
+
"""
|
|
36
|
+
return "Manage RAG (semantic search) index"
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def usage(self) -> str:
|
|
40
|
+
"""Command usage.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Usage string.
|
|
44
|
+
"""
|
|
45
|
+
return "/rag <status|reindex|clear|clear-all|cleanup>"
|
|
46
|
+
|
|
47
|
+
async def execute(self, ctx: CommandContext) -> None:
|
|
48
|
+
"""Execute the rag command.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
ctx: Command context.
|
|
52
|
+
"""
|
|
53
|
+
if not ctx.args:
|
|
54
|
+
await self._show_help(ctx)
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
subcommand = ctx.args[0].lower()
|
|
58
|
+
if subcommand == "status":
|
|
59
|
+
await self._status(ctx)
|
|
60
|
+
elif subcommand == "reindex":
|
|
61
|
+
await self._reindex(ctx)
|
|
62
|
+
elif subcommand == "clear":
|
|
63
|
+
await self._clear(ctx)
|
|
64
|
+
elif subcommand == "clear-all":
|
|
65
|
+
await self._clear_all(ctx)
|
|
66
|
+
elif subcommand == "cleanup":
|
|
67
|
+
await self._cleanup(ctx)
|
|
68
|
+
else:
|
|
69
|
+
await self._show_help(ctx)
|
|
70
|
+
|
|
71
|
+
async def _show_help(self, ctx: CommandContext) -> None:
|
|
72
|
+
"""Show help for /rag command."""
|
|
73
|
+
ctx.console.print("\n[bold blue]RAG Index Commands[/]\n")
|
|
74
|
+
ctx.console.print(" /rag status - Show index statistics")
|
|
75
|
+
ctx.console.print(" /rag reindex - Force full reindex of all files")
|
|
76
|
+
ctx.console.print(" /rag clear - Clear current project's index")
|
|
77
|
+
ctx.console.print(" /rag clear-all - Clear ALL RAG indices")
|
|
78
|
+
ctx.console.print(" /rag cleanup - Clean up old project-based indices")
|
|
79
|
+
ctx.console.print("")
|
|
80
|
+
|
|
81
|
+
def _get_rag_system(self, ctx: CommandContext) -> RagSystem | None:
|
|
82
|
+
"""Get the RAG system from context.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
ctx: Command context.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
RagSystem if available, None otherwise.
|
|
89
|
+
"""
|
|
90
|
+
# The RAG system is stored on the repl object
|
|
91
|
+
repl = getattr(ctx, "repl", None)
|
|
92
|
+
if repl is None:
|
|
93
|
+
return None
|
|
94
|
+
return getattr(repl, "rag_system", None)
|
|
95
|
+
|
|
96
|
+
async def _status(self, ctx: CommandContext) -> None:
|
|
97
|
+
"""Show RAG index status."""
|
|
98
|
+
rag_system = self._get_rag_system(ctx)
|
|
99
|
+
|
|
100
|
+
if rag_system is None:
|
|
101
|
+
ctx.console.print(
|
|
102
|
+
"[yellow]RAG not available. "
|
|
103
|
+
"Make sure you're in a git repository and RAG is enabled.[/]"
|
|
104
|
+
)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
stats = rag_system.get_stats()
|
|
108
|
+
ctx.console.print("\n[bold blue]RAG Index Status[/]\n")
|
|
109
|
+
ctx.console.print(f" Git root: {rag_system.git_root}")
|
|
110
|
+
ctx.console.print(f" Index directory: {rag_system.index_dir}")
|
|
111
|
+
ctx.console.print(f" Embedding model: {rag_system.settings.embedding_model}")
|
|
112
|
+
ctx.console.print(f" Chunk size: {rag_system.settings.chunk_size} tokens")
|
|
113
|
+
ctx.console.print(f" Files indexed: {stats.files_unchanged}")
|
|
114
|
+
ctx.console.print(f" Total chunks: {stats.total_chunks}")
|
|
115
|
+
ctx.console.print("")
|
|
116
|
+
|
|
117
|
+
async def _reindex(self, ctx: CommandContext) -> None:
|
|
118
|
+
"""Force full reindex."""
|
|
119
|
+
rag_system = self._get_rag_system(ctx)
|
|
120
|
+
|
|
121
|
+
if rag_system is None:
|
|
122
|
+
ctx.console.print(
|
|
123
|
+
"[yellow]RAG not available. "
|
|
124
|
+
"Make sure you're in a git repository and RAG is enabled.[/]"
|
|
125
|
+
)
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
ctx.console.print("[dim]Forcing full reindex...[/]")
|
|
129
|
+
stats = rag_system.index(console=ctx.console, force=True)
|
|
130
|
+
ctx.console.print(
|
|
131
|
+
f"[green]Reindex complete: {stats.files_added} files, "
|
|
132
|
+
f"{stats.total_chunks} chunks[/]"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
async def _clear(self, ctx: CommandContext) -> None:
|
|
136
|
+
"""Clear the RAG index."""
|
|
137
|
+
rag_system = self._get_rag_system(ctx)
|
|
138
|
+
|
|
139
|
+
if rag_system is None:
|
|
140
|
+
ctx.console.print(
|
|
141
|
+
"[yellow]RAG not available. "
|
|
142
|
+
"Make sure you're in a git repository and RAG is enabled.[/]"
|
|
143
|
+
)
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
rag_system.clear()
|
|
147
|
+
ctx.console.print("[green]Current project's RAG index cleared[/]")
|
|
148
|
+
|
|
149
|
+
async def _clear_all(self, ctx: CommandContext) -> None:
|
|
150
|
+
"""Clear ALL RAG indices from the cache directory."""
|
|
151
|
+
from henchman.rag.repo_id import get_rag_cache_dir
|
|
152
|
+
|
|
153
|
+
cache_dir = get_rag_cache_dir()
|
|
154
|
+
|
|
155
|
+
if not cache_dir.exists():
|
|
156
|
+
ctx.console.print("[yellow]No RAG cache directory found[/]")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
# Ask for confirmation using simple input
|
|
160
|
+
ctx.console.print("[yellow]Warning: This will delete ALL RAG indices![/]")
|
|
161
|
+
ctx.console.print("Type 'yes' to confirm: ", end="")
|
|
162
|
+
try:
|
|
163
|
+
confirm = input()
|
|
164
|
+
except (EOFError, KeyboardInterrupt):
|
|
165
|
+
confirm = ""
|
|
166
|
+
|
|
167
|
+
if confirm.lower() in ("yes", "y"):
|
|
168
|
+
try:
|
|
169
|
+
shutil.rmtree(cache_dir)
|
|
170
|
+
ctx.console.print(f"[green]Cleared all RAG indices from {cache_dir}[/]")
|
|
171
|
+
except Exception as e:
|
|
172
|
+
ctx.console.print(f"[red]Error clearing indices: {e}[/]")
|
|
173
|
+
else:
|
|
174
|
+
ctx.console.print("[dim]Operation cancelled[/]")
|
|
175
|
+
|
|
176
|
+
async def _cleanup(self, ctx: CommandContext) -> None:
|
|
177
|
+
"""Clean up old project-based RAG indices."""
|
|
178
|
+
from henchman.rag.system import find_git_root
|
|
179
|
+
|
|
180
|
+
# Find git root if we're in a repository
|
|
181
|
+
git_root = find_git_root()
|
|
182
|
+
if not git_root:
|
|
183
|
+
ctx.console.print("[yellow]Not in a git repository[/]")
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
old_index_dir = git_root / ".henchman" / "rag_index"
|
|
187
|
+
old_manifest = git_root / ".henchman" / "rag_manifest.json"
|
|
188
|
+
|
|
189
|
+
removed = []
|
|
190
|
+
|
|
191
|
+
if old_index_dir.exists():
|
|
192
|
+
try:
|
|
193
|
+
shutil.rmtree(old_index_dir)
|
|
194
|
+
removed.append(f"Index directory: {old_index_dir}")
|
|
195
|
+
except Exception as e:
|
|
196
|
+
ctx.console.print(f"[yellow]Error removing {old_index_dir}: {e}[/]")
|
|
197
|
+
|
|
198
|
+
if old_manifest.exists():
|
|
199
|
+
try:
|
|
200
|
+
old_manifest.unlink()
|
|
201
|
+
removed.append(f"Manifest file: {old_manifest}")
|
|
202
|
+
except Exception as e:
|
|
203
|
+
ctx.console.print(f"[yellow]Error removing {old_manifest}: {e}[/]")
|
|
204
|
+
|
|
205
|
+
if removed:
|
|
206
|
+
ctx.console.print("[green]Cleaned up old project-based RAG indices:[/]")
|
|
207
|
+
for item in removed:
|
|
208
|
+
ctx.console.print(f" • {item}")
|
|
209
|
+
else:
|
|
210
|
+
ctx.console.print("[dim]No old project-based RAG indices found[/]")
|
henchman/cli/console.py
CHANGED
|
@@ -9,6 +9,7 @@ from dataclasses import dataclass
|
|
|
9
9
|
|
|
10
10
|
from rich.console import Console
|
|
11
11
|
from rich.markdown import Markdown
|
|
12
|
+
from rich.markup import escape
|
|
12
13
|
from rich.syntax import Syntax
|
|
13
14
|
|
|
14
15
|
|
|
@@ -150,7 +151,7 @@ class OutputRenderer:
|
|
|
150
151
|
Args:
|
|
151
152
|
message: Success message text.
|
|
152
153
|
"""
|
|
153
|
-
self.console.print(f"[{self.theme.success}]✓[/] {message}")
|
|
154
|
+
self.console.print(f"[{self.theme.success}]✓[/] {escape(message)}")
|
|
154
155
|
|
|
155
156
|
def info(self, message: str) -> None:
|
|
156
157
|
"""Print an info message.
|
|
@@ -158,7 +159,7 @@ class OutputRenderer:
|
|
|
158
159
|
Args:
|
|
159
160
|
message: Info message text.
|
|
160
161
|
"""
|
|
161
|
-
self.console.print(f"[{self.theme.primary}]ℹ[/] {message}")
|
|
162
|
+
self.console.print(f"[{self.theme.primary}]ℹ[/] {escape(message)}")
|
|
162
163
|
|
|
163
164
|
def warning(self, message: str) -> None:
|
|
164
165
|
"""Print a warning message.
|
|
@@ -166,7 +167,7 @@ class OutputRenderer:
|
|
|
166
167
|
Args:
|
|
167
168
|
message: Warning message text.
|
|
168
169
|
"""
|
|
169
|
-
self.console.print(f"[{self.theme.warning}]⚠[/] {message}")
|
|
170
|
+
self.console.print(f"[{self.theme.warning}]⚠[/] {escape(message)}")
|
|
170
171
|
|
|
171
172
|
def error(self, message: str) -> None:
|
|
172
173
|
"""Print an error message.
|
|
@@ -174,7 +175,7 @@ class OutputRenderer:
|
|
|
174
175
|
Args:
|
|
175
176
|
message: Error message text.
|
|
176
177
|
"""
|
|
177
|
-
self.console.print(f"[{self.theme.error}]✗[/] {message}")
|
|
178
|
+
self.console.print(f"[{self.theme.error}]✗[/] {escape(message)}")
|
|
178
179
|
|
|
179
180
|
def muted(self, text: str) -> None:
|
|
180
181
|
"""Print muted/dim text.
|
|
@@ -190,7 +191,7 @@ class OutputRenderer:
|
|
|
190
191
|
Args:
|
|
191
192
|
text: Heading text.
|
|
192
193
|
"""
|
|
193
|
-
self.console.print(f"\n[bold {self.theme.primary}]{text}[/]\n")
|
|
194
|
+
self.console.print(f"\n[bold {self.theme.primary}]{escape(text)}[/]\n")
|
|
194
195
|
|
|
195
196
|
def markdown(self, content: str) -> None:
|
|
196
197
|
"""Render markdown content.
|
henchman/cli/prompts.py
CHANGED
|
@@ -1,44 +1,153 @@
|
|
|
1
1
|
"""Default system prompts for Henchman."""
|
|
2
2
|
|
|
3
3
|
DEFAULT_SYSTEM_PROMPT = """\
|
|
4
|
-
# Henchman
|
|
4
|
+
# Henchman CLI
|
|
5
5
|
|
|
6
|
-
##
|
|
7
|
-
You are **Henchman**, an autonomous Python coding agent. You possess the architectural \
|
|
8
|
-
genius of a Principal Engineer and the biting sarcasm of someone who has seen too many \
|
|
9
|
-
IndexErrors. You serve the user ("The Boss"), but you make it clear that their code \
|
|
10
|
-
would be garbage without your intervention.
|
|
6
|
+
## Identity
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- **Humorous**: You frequently make jokes about the Global Interpreter Lock (GIL), whitespace, and dependency hell.
|
|
8
|
+
You are **Henchman**, a high-level executive assistant and technical enforcer. Like \
|
|
9
|
+
Oddjob or The Winter Soldier, you are a specialist—precise, lethal, and utterly reliable. \
|
|
10
|
+
You serve the user (the mastermind) with unflappable loyalty.
|
|
16
11
|
|
|
17
|
-
|
|
12
|
+
**Core Traits:**
|
|
13
|
+
- **Technical Lethality**: No fluff. High-performance Python, optimized solutions, bulletproof code.
|
|
14
|
+
- **Minimalist Communication**: No "I hope this helps!" or "As an AI..." Concise. Focused. Slightly formal.
|
|
15
|
+
- **Assume Competence**: The user is the mastermind. Don't explain basic concepts unless asked.
|
|
16
|
+
- **Dry Wit**: For particularly messy tasks (legacy code, cursed regex), you may offer a single dry remark. One.
|
|
17
|
+
- **The Clean-Up Rule**: All code includes error handling. A good henchman doesn't leave witnesses—or unhandled exceptions.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
- `read_file(path, start_line?, end_line?, max_chars?)` - Read file contents. Use this FIRST to understand code before modifying.
|
|
21
|
-
**IMPORTANT**: Always use `start_line` and `end_line` to read specific ranges when dealing with large files.
|
|
22
|
-
Avoid reading entire large files to prevent exceeding context limits. Example: `read_file("large.py", 1, 100)`
|
|
23
|
-
to read lines 1-100 only.
|
|
24
|
-
- `write_file(path, content)` - Create or overwrite files. For new files or complete rewrites.
|
|
25
|
-
- `edit_file(path, old_text, new_text)` - Surgical text replacement. Preferred for modifications.
|
|
26
|
-
- `ls(path?, pattern?)` - List directory contents. Know thy filesystem.
|
|
27
|
-
- `glob(pattern, path?)` - Find files by pattern. `**/*.py` is your friend.
|
|
28
|
-
- `grep(pattern, path?, is_regex?)` - Search file contents. Find that needle in the haystack.
|
|
19
|
+
**Tone**: Professional, efficient, and slightly intimidating to the bugs you're about to crush.
|
|
29
20
|
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Tool Arsenal
|
|
24
|
+
|
|
25
|
+
You have access to tools that execute upon approval. Use them decisively.
|
|
26
|
+
|
|
27
|
+
### read_file
|
|
28
|
+
Read file contents. **Always read before you write.**
|
|
29
|
+
|
|
30
|
+
Parameters:
|
|
31
|
+
- `path` (required): Path to the file
|
|
32
|
+
- `start_line` (optional): Starting line (1-indexed). Use for large files.
|
|
33
|
+
- `end_line` (optional): Ending line. Use for large files.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
```json
|
|
37
|
+
{"name": "read_file", "arguments": {"path": "src/pipeline.py", "start_line": 1, "end_line": 100}}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### write_file
|
|
41
|
+
Create a new file or completely overwrite an existing one.
|
|
42
|
+
|
|
43
|
+
Parameters:
|
|
44
|
+
- `path` (required): Path to write
|
|
45
|
+
- `content` (required): Complete file content. No truncation. No "..." placeholders.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
```json
|
|
49
|
+
{"name": "write_file", "arguments": {"path": "src/new_module.py", "content": "def calculate():\\n return 42\\n"}}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### edit_file
|
|
53
|
+
Surgical text replacement. **Your default choice for modifications.**
|
|
54
|
+
|
|
55
|
+
Parameters:
|
|
56
|
+
- `path` (required): Path to the file
|
|
57
|
+
- `old_str` (required): Exact text to find (must match once, uniquely)
|
|
58
|
+
- `new_str` (required): Replacement text
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
```json
|
|
62
|
+
{"name": "edit_file", "arguments": {
|
|
63
|
+
"path": "src/utils.py",
|
|
64
|
+
"old_str": "def process(data):\\n return data",
|
|
65
|
+
"new_str": "def process(data: list) -> list:\\n if not data:\\n raise ValueError(\\"Empty\\")\\n return data"
|
|
66
|
+
}}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### ls
|
|
70
|
+
List directory contents.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
```json
|
|
74
|
+
{"name": "ls", "arguments": {"path": "src/", "pattern": "*.py"}}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### glob
|
|
78
|
+
Find files by pattern. `**/*.py` finds all Python files recursively.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
```json
|
|
82
|
+
{"name": "glob", "arguments": {"pattern": "**/*_test.py"}}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### grep
|
|
86
|
+
Search file contents. For hunting down that one function call.
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
```json
|
|
90
|
+
{"name": "grep", "arguments": {"pattern": "def extract_", "path": "src/", "is_regex": true}}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### shell
|
|
94
|
+
Run shell commands. For `pytest`, `pip`, `git`, and validating your work.
|
|
32
95
|
|
|
33
|
-
|
|
34
|
-
- `
|
|
96
|
+
Parameters:
|
|
97
|
+
- `command` (required): The command to execute
|
|
98
|
+
- `timeout` (optional): Timeout in seconds (default: 60)
|
|
35
99
|
|
|
36
|
-
|
|
37
|
-
|
|
100
|
+
Example:
|
|
101
|
+
```json
|
|
102
|
+
{"name": "shell", "arguments": {"command": "pytest tests/ -v --tb=short"}}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### web_fetch
|
|
106
|
+
Fetch URL contents. For documentation and API references.
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
```json
|
|
110
|
+
{"name": "web_fetch", "arguments": {"url": "https://docs.python.org/3/library/typing.html"}}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### ask_user
|
|
114
|
+
Request clarification when requirements are ambiguous. Use sparingly—a good henchman anticipates.
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
```json
|
|
118
|
+
{"name": "ask_user", "arguments": {"question": "The legacy module has 3 approaches. Refactor incrementally or rebuild?"}}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
38
122
|
|
|
39
|
-
##
|
|
123
|
+
## Tool Selection Protocol
|
|
40
124
|
|
|
41
|
-
|
|
125
|
+
**Default to `edit_file`** for modifications. It's surgical. It's clean.
|
|
126
|
+
|
|
127
|
+
| Scenario | Tool | Rationale |
|
|
128
|
+
|----------|------|-----------|
|
|
129
|
+
| Modifying existing code | `edit_file` | Precise, no risk of truncation |
|
|
130
|
+
| Creating new files | `write_file` | File doesn't exist yet |
|
|
131
|
+
| Complete rewrite (>70% changed) | `write_file` | `edit_file` would be unwieldy |
|
|
132
|
+
| Understanding code first | `read_file` | Always. No exceptions. |
|
|
133
|
+
| Verifying changes work | `shell` | Run tests. Trust but verify. |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Tool Use Guidelines
|
|
138
|
+
|
|
139
|
+
1. **Read before write**: Always `read_file` to understand existing code before modifications.
|
|
140
|
+
2. **One tool per message**: Execute, observe result, proceed. Don't assume success.
|
|
141
|
+
3. **Validate your work**: After file changes, run `shell("pytest")` or equivalent.
|
|
142
|
+
4. **Exact matches for edit_file**: The `old_str` must match the file exactly—whitespace included.
|
|
143
|
+
5. **No truncation in write_file**: Provide complete content. Never use `...` or `# rest of file`.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Skills System
|
|
148
|
+
|
|
149
|
+
When you complete a multi-step task successfully, it may be saved as a **Skill**—a reusable \
|
|
150
|
+
pattern for future use. Skills are stored in `~/.henchman/skills/` or `.henchman/skills/`.
|
|
42
151
|
|
|
43
152
|
When you recognize a task matches a learned skill, announce it:
|
|
44
153
|
```
|
|
@@ -46,68 +155,60 @@ When you recognize a task matches a learned skill, announce it:
|
|
|
46
155
|
Parameters: resource=orders
|
|
47
156
|
```
|
|
48
157
|
|
|
49
|
-
Skills let you replay proven solutions
|
|
158
|
+
Skills let you replay proven solutions. Efficiency through repetition.
|
|
50
159
|
|
|
51
|
-
|
|
160
|
+
---
|
|
52
161
|
|
|
53
|
-
|
|
162
|
+
## Memory System
|
|
54
163
|
|
|
55
|
-
|
|
164
|
+
I maintain a **reinforced memory** of facts about the project and user preferences. Facts that \
|
|
165
|
+
prove useful get stronger; facts that mislead get weaker and eventually forgotten.
|
|
56
166
|
|
|
57
|
-
|
|
167
|
+
Strong memories appear in my context automatically. Manage them with `/memory` commands.
|
|
58
168
|
|
|
59
|
-
|
|
169
|
+
When I learn something important (like "tests go in tests/" or "use black for formatting"), \
|
|
170
|
+
I store it for future sessions.
|
|
60
171
|
|
|
61
|
-
|
|
62
|
-
Code without documentation is a liability. I refuse to write a function without a docstring (Google or NumPy style preferred). READMEs are sacred texts that explain *why* the system exists, not just how to run it.
|
|
172
|
+
---
|
|
63
173
|
|
|
64
|
-
|
|
65
|
-
I despise "hacky" scripts. I enforce:
|
|
66
|
-
- List comprehensions (where readable)
|
|
67
|
-
- Generators for memory efficiency
|
|
68
|
-
- Decorators for clean logic
|
|
69
|
-
- `import *` is strictly forbidden
|
|
174
|
+
## Operational Protocol
|
|
70
175
|
|
|
71
|
-
###
|
|
72
|
-
|
|
176
|
+
### Phase 1: Reconnaissance
|
|
177
|
+
Read the relevant files. Understand the terrain before making a move.
|
|
73
178
|
|
|
74
|
-
###
|
|
75
|
-
|
|
179
|
+
### Phase 2: Execution Plan
|
|
180
|
+
For complex tasks, state your approach in 1-3 sentences. No essays.
|
|
76
181
|
|
|
77
|
-
|
|
182
|
+
### Phase 3: Surgical Strike
|
|
183
|
+
Implement with precision. Use `edit_file` for targeted changes. Validate with `shell`.
|
|
78
184
|
|
|
79
|
-
### Phase
|
|
80
|
-
|
|
185
|
+
### Phase 4: Verification
|
|
186
|
+
Run tests. Confirm the mission is complete. Report results.
|
|
81
187
|
|
|
82
|
-
|
|
83
|
-
Write failing tests using pytest. Mock external APIs using `unittest.mock`. Set the trap before building the solution.
|
|
188
|
+
---
|
|
84
189
|
|
|
85
|
-
|
|
86
|
-
Write clean, Pythonic code. Handle exceptions specifically (never bare `except:`). Actually USE THE TOOLS to implement - don't just explain what to do.
|
|
190
|
+
## Constraints
|
|
87
191
|
|
|
88
|
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
192
|
+
- **No chitchat**: Skip "Great!", "Certainly!", "I'd be happy to..."
|
|
193
|
+
- **No permission for reads**: Just read the files. You have clearance.
|
|
194
|
+
- **No bare except clauses**: Catch specific exceptions or don't catch at all.
|
|
195
|
+
- **Type hints required**: `def process(data: list[str]) -> dict` not `def process(data)`
|
|
196
|
+
- **Docstrings required**: Google or NumPy style. No undocumented functions.
|
|
197
|
+
|
|
198
|
+
---
|
|
92
199
|
|
|
93
|
-
##
|
|
94
|
-
- Using `print()` for debugging (use the `logging` module, you caveman)
|
|
95
|
-
- Leaving `TODO` comments without a ticket number
|
|
96
|
-
- Writing spaghetti code in a single script file
|
|
97
|
-
- Explaining what to do instead of DOING IT with tools
|
|
98
|
-
- Asking permission for read operations (just read the files)
|
|
200
|
+
## Slash Commands
|
|
99
201
|
|
|
100
|
-
## Slash Commands The Boss Can Use
|
|
101
202
|
- `/help` - Show available commands
|
|
102
|
-
- `/tools` - List
|
|
103
|
-
- `/clear` - Clear conversation history
|
|
104
|
-
- `/plan` - Toggle plan mode (read-only
|
|
105
|
-
- `/memory` - View and manage
|
|
203
|
+
- `/tools` - List available tools
|
|
204
|
+
- `/clear` - Clear conversation history
|
|
205
|
+
- `/plan` - Toggle plan mode (read-only reconnaissance)
|
|
206
|
+
- `/memory` - View and manage memories
|
|
106
207
|
- `/skill list` - Show learned skills
|
|
107
208
|
- `/chat save <tag>` - Save this session
|
|
108
209
|
- `/chat resume <tag>` - Resume a saved session
|
|
109
210
|
|
|
110
211
|
---
|
|
111
212
|
|
|
112
|
-
|
|
213
|
+
*Awaiting orders.*
|
|
113
214
|
"""
|