localcode 0.4.0__py3-none-macosx_13_0_arm64.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 (173) hide show
  1. localcode/__init__.py +11 -0
  2. localcode/__main__.py +5 -0
  3. localcode/_subproc_env.py +54 -0
  4. localcode/agent/__init__.py +177 -0
  5. localcode/agent/app_tasks.py +157 -0
  6. localcode/agent/constants.py +275 -0
  7. localcode/agent/context.py +1152 -0
  8. localcode/agent/goal.py +244 -0
  9. localcode/agent/helpers.py +1036 -0
  10. localcode/agent/hollow_module.py +230 -0
  11. localcode/agent/hooks.py +414 -0
  12. localcode/agent/loop.py +2855 -0
  13. localcode/agent/plateau.py +297 -0
  14. localcode/agent/project_check_gate.py +91 -0
  15. localcode/agent/prompt_context.py +264 -0
  16. localcode/agent/prompts.py +368 -0
  17. localcode/agent/reasoning_loop.py +79 -0
  18. localcode/agent/recovery.py +606 -0
  19. localcode/agent/recovery_controller.py +25 -0
  20. localcode/agent/round_policy.py +23 -0
  21. localcode/agent/sections.py +241 -0
  22. localcode/agent/state_machine.py +100 -0
  23. localcode/agent/streaming.py +324 -0
  24. localcode/agent/tool_execution.py +368 -0
  25. localcode/agent/tool_orchestration.py +54 -0
  26. localcode/agent/turn_finalization.py +135 -0
  27. localcode/app.py +1595 -0
  28. localcode/approvals.py +17 -0
  29. localcode/auto_compact.py +176 -0
  30. localcode/autonomy.py +103 -0
  31. localcode/bin/__init__.py +0 -0
  32. localcode/bin/llama-server +0 -0
  33. localcode/bin/localcode-ui +4 -0
  34. localcode/bootstrap.py +1191 -0
  35. localcode/cache.py +194 -0
  36. localcode/checkpoint.py +375 -0
  37. localcode/compact.py +73 -0
  38. localcode/compaction.py +341 -0
  39. localcode/composer.py +72 -0
  40. localcode/config.py +521 -0
  41. localcode/context.py +161 -0
  42. localcode/display.py +58 -0
  43. localcode/embeddings.py +480 -0
  44. localcode/entrypoint.py +536 -0
  45. localcode/errors.py +350 -0
  46. localcode/events.py +409 -0
  47. localcode/evidence.py +102 -0
  48. localcode/execution_policy.py +48 -0
  49. localcode/features.py +308 -0
  50. localcode/formatting.py +91 -0
  51. localcode/headless_json.py +265 -0
  52. localcode/health.py +288 -0
  53. localcode/hf_quants.py +203 -0
  54. localcode/history.py +594 -0
  55. localcode/indexer.py +144 -0
  56. localcode/injection_defense.py +204 -0
  57. localcode/launcher.py +373 -0
  58. localcode/logging_utils.py +28 -0
  59. localcode/lsp.py +153 -0
  60. localcode/mcp/__init__.py +88 -0
  61. localcode/mcp/_bridge.py +61 -0
  62. localcode/mcp/_config.py +33 -0
  63. localcode/mcp/_transports.py +128 -0
  64. localcode/mcp/client.py +395 -0
  65. localcode/memory_guard.py +291 -0
  66. localcode/model_config.py +288 -0
  67. localcode/model_delete.py +444 -0
  68. localcode/model_families.py +279 -0
  69. localcode/models.py +236 -0
  70. localcode/models_catalog.py +901 -0
  71. localcode/notebook.py +123 -0
  72. localcode/output.py +403 -0
  73. localcode/patching.py +65 -0
  74. localcode/paths.py +303 -0
  75. localcode/performance.py +623 -0
  76. localcode/permissions.py +79 -0
  77. localcode/permissions_v2.py +310 -0
  78. localcode/plans.py +198 -0
  79. localcode/process_registry.py +271 -0
  80. localcode/protocol/__init__.py +56 -0
  81. localcode/protocol/events.py +116 -0
  82. localcode/protocol/jsonl.py +153 -0
  83. localcode/protocol/outcomes.py +231 -0
  84. localcode/reasoning_capabilities.py +59 -0
  85. localcode/recommendations.py +87 -0
  86. localcode/recovery.py +134 -0
  87. localcode/redaction.py +164 -0
  88. localcode/runtime.py +2331 -0
  89. localcode/server_manager.py +1069 -0
  90. localcode/session.py +296 -0
  91. localcode/shell.py +83 -0
  92. localcode/skills/debug.md +21 -0
  93. localcode/skills/edit-verified.md +30 -0
  94. localcode/skills/explain.md +19 -0
  95. localcode/skills/finish-verified.md +11 -0
  96. localcode/skills/git-commit-safely.md +30 -0
  97. localcode/skills/locate.md +21 -0
  98. localcode/skills/plan-task.md +51 -0
  99. localcode/skills/review.md +22 -0
  100. localcode/skills/run-tests.md +24 -0
  101. localcode/skills.py +728 -0
  102. localcode/snapshots.py +195 -0
  103. localcode/sounds.py +50 -0
  104. localcode/telemetry.py +273 -0
  105. localcode/theme.py +137 -0
  106. localcode/thermal.py +160 -0
  107. localcode/thinking.py +114 -0
  108. localcode/tool_router.py +246 -0
  109. localcode/toolkit.py +1241 -0
  110. localcode/tools/__init__.py +400 -0
  111. localcode/tools/agent.py +259 -0
  112. localcode/tools/append_file.py +55 -0
  113. localcode/tools/background_process.py +115 -0
  114. localcode/tools/base.py +168 -0
  115. localcode/tools/bash.py +1160 -0
  116. localcode/tools/code_navigation.py +97 -0
  117. localcode/tools/edit_diff.py +80 -0
  118. localcode/tools/edit_file.py +477 -0
  119. localcode/tools/facts.py +149 -0
  120. localcode/tools/glob_tool.py +60 -0
  121. localcode/tools/grep.py +65 -0
  122. localcode/tools/inspect_symbol.py +266 -0
  123. localcode/tools/launch_app.py +93 -0
  124. localcode/tools/list_files.py +42 -0
  125. localcode/tools/multi_edit.py +167 -0
  126. localcode/tools/plan_mode.py +86 -0
  127. localcode/tools/project_check.py +685 -0
  128. localcode/tools/read_file.py +261 -0
  129. localcode/tools/read_state.py +223 -0
  130. localcode/tools/skill_tool.py +41 -0
  131. localcode/tools/syntax_check.py +216 -0
  132. localcode/tools/todo_write.py +200 -0
  133. localcode/tools/tool_call_repair.py +157 -0
  134. localcode/tools/web_fetch.py +182 -0
  135. localcode/tools/web_search.py +45 -0
  136. localcode/tools/write_file.py +281 -0
  137. localcode/tui/__init__.py +1 -0
  138. localcode/tui/app.py +485 -0
  139. localcode/tui/bridge.py +78 -0
  140. localcode/tui/clipboard_image.py +153 -0
  141. localcode/tui/paste_collapse.py +95 -0
  142. localcode/tui/screens/__init__.py +0 -0
  143. localcode/tui/screens/chat.py +5052 -0
  144. localcode/tui/screens/confirm.py +123 -0
  145. localcode/tui/screens/mode_picker.py +79 -0
  146. localcode/tui/screens/model_picker.py +962 -0
  147. localcode/tui/screens/setup.py +891 -0
  148. localcode/tui/styles/__init__.py +0 -0
  149. localcode/tui/styles/app.tcss +197 -0
  150. localcode/tui/widgets/__init__.py +0 -0
  151. localcode/tui/widgets/approval.py +2 -0
  152. localcode/tui/widgets/chat_log.py +2063 -0
  153. localcode/tui/widgets/messages/__init__.py +21 -0
  154. localcode/tui/widgets/messages/diff.py +287 -0
  155. localcode/tui/widgets/voice_visualizer.py +122 -0
  156. localcode/turn_diff.py +180 -0
  157. localcode/ui/FORK_COMMIT +1 -0
  158. localcode/ui/__init__.py +58 -0
  159. localcode/ui/launch.py +204 -0
  160. localcode/ui/picker_cli.py +165 -0
  161. localcode/ui/plugin/localcode.ts +626 -0
  162. localcode/ui/ports.py +42 -0
  163. localcode/ui/server_cmd.py +89 -0
  164. localcode/ui/supervisor.py +736 -0
  165. localcode/undo.py +121 -0
  166. localcode/verification.py +87 -0
  167. localcode/voice.py +948 -0
  168. localcode-0.4.0.dist-info/METADATA +160 -0
  169. localcode-0.4.0.dist-info/RECORD +173 -0
  170. localcode-0.4.0.dist-info/WHEEL +5 -0
  171. localcode-0.4.0.dist-info/entry_points.txt +3 -0
  172. localcode-0.4.0.dist-info/licenses/LICENSE +201 -0
  173. localcode-0.4.0.dist-info/top_level.txt +1 -0
localcode/session.py ADDED
@@ -0,0 +1,296 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
5
+ import json
6
+ from pathlib import Path
7
+ import uuid
8
+
9
+ from .config import ensure_home_dirs
10
+ from .paths import write_private
11
+
12
+
13
+ Message = dict[str, str]
14
+
15
+
16
+ def utc_now() -> str:
17
+ return datetime.now(timezone.utc).isoformat()
18
+
19
+
20
+ @dataclass
21
+ class TaskState:
22
+ task_id: str
23
+ user_request: str
24
+ goal_type: str
25
+ task_kind: str
26
+ task_slug: str
27
+ goal_summary: str
28
+ success_criteria: list[str] = field(default_factory=list)
29
+ status: str = "in_progress"
30
+ current_stage: str = ""
31
+ active_port: int = 0
32
+ completion_status: str = ""
33
+ blocked_reason: str = ""
34
+ turn_count: int = 0
35
+ created_at: str = field(default_factory=utc_now)
36
+ updated_at: str = field(default_factory=utc_now)
37
+ final_response: str = ""
38
+
39
+
40
+ @dataclass
41
+ class SessionState:
42
+ session_id: str
43
+ repo_root: Path
44
+ created_at: str
45
+ profile: str = "e4b"
46
+ model: str = ""
47
+ messages: list[Message] = field(default_factory=list)
48
+ pinned_files: list[str] = field(default_factory=list)
49
+ last_assistant_text: str = ""
50
+ events: list[dict[str, str]] = field(default_factory=list)
51
+ current_task: TaskState | None = None
52
+ recent_tasks: list[TaskState] = field(default_factory=list)
53
+ # Working-memory checklist written by the todo_write tool: a list of
54
+ # {content, status, activeForm} dicts. Persisted so a resumed session
55
+ # keeps its plan; injected into context each round by the agent loop.
56
+ todos: list[dict] = field(default_factory=list)
57
+
58
+
59
+ class SessionStore:
60
+ def __init__(self) -> None:
61
+ home = ensure_home_dirs()
62
+ self.sessions_dir = home / "sessions"
63
+
64
+ def create(self, repo_root: Path, profile: str = "e4b", model: str = "") -> SessionState:
65
+ session = SessionState(
66
+ session_id=uuid.uuid4().hex[:12],
67
+ repo_root=repo_root,
68
+ created_at=utc_now(),
69
+ profile=profile,
70
+ model=model,
71
+ )
72
+ self.save(session)
73
+ return session
74
+
75
+ def create_task(self, session: SessionState, *, user_request: str, goal_type: str,
76
+ task_kind: str, task_slug: str,
77
+ goal_summary: str, success_criteria: list[str]) -> TaskState:
78
+ task = TaskState(
79
+ task_id=uuid.uuid4().hex[:12],
80
+ user_request=user_request,
81
+ goal_type=goal_type,
82
+ task_kind=task_kind,
83
+ task_slug=task_slug,
84
+ goal_summary=goal_summary,
85
+ success_criteria=list(success_criteria),
86
+ turn_count=1,
87
+ current_stage="planning" if goal_type == "build_app" else "",
88
+ )
89
+ session.current_task = task
90
+ session.recent_tasks.append(task)
91
+ session.recent_tasks = session.recent_tasks[-20:]
92
+ self.save(session)
93
+ return task
94
+
95
+ def continue_task(self, session: SessionState, *, user_request: str,
96
+ goal_type: str | None = None,
97
+ goal_summary: str | None = None,
98
+ success_criteria: list[str] | None = None) -> TaskState | None:
99
+ """Attach a follow-up user turn to the current task.
100
+
101
+ Coding agents should treat short corrections as continuations
102
+ of the task that just ran, not as fresh general chat tasks.
103
+ Keep the original task identity and only update turn-specific
104
+ goal details.
105
+ """
106
+ task = session.current_task
107
+ if task is None:
108
+ return None
109
+ task.user_request = f"{task.user_request}\n\nFollow-up: {user_request}".strip()
110
+ if goal_type is not None:
111
+ task.goal_type = goal_type
112
+ if goal_summary is not None:
113
+ task.goal_summary = goal_summary
114
+ if success_criteria is not None:
115
+ task.success_criteria = list(success_criteria)
116
+ task.status = "in_progress"
117
+ task.completion_status = ""
118
+ task.blocked_reason = ""
119
+ task.final_response = ""
120
+ task.turn_count += 1
121
+ task.updated_at = utc_now()
122
+ self.save(session)
123
+ return task
124
+
125
+ def update_task(self, session: SessionState, *, status: str | None = None,
126
+ current_stage: str | None = None,
127
+ active_port: int | None = None,
128
+ completion_status: str | None = None,
129
+ blocked_reason: str | None = None,
130
+ final_response: str | None = None,
131
+ increment_turn: bool = False) -> None:
132
+ task = session.current_task
133
+ if task is None:
134
+ return
135
+ if increment_turn:
136
+ task.turn_count += 1
137
+ if status is not None:
138
+ task.status = status
139
+ if current_stage is not None:
140
+ task.current_stage = current_stage
141
+ if active_port is not None:
142
+ task.active_port = active_port
143
+ if completion_status is not None:
144
+ task.completion_status = completion_status
145
+ if blocked_reason is not None:
146
+ task.blocked_reason = blocked_reason
147
+ if final_response is not None:
148
+ task.final_response = final_response
149
+ task.updated_at = utc_now()
150
+ self.save(session)
151
+
152
+ def load(self, session_id: str) -> SessionState:
153
+ path = self.sessions_dir / f"{session_id}.json"
154
+ data = json.loads(path.read_text())
155
+ current_task = data.get("current_task")
156
+ recent_tasks = data.get("recent_tasks", [])
157
+ def _load_task(payload: dict | None) -> TaskState | None:
158
+ if not isinstance(payload, dict):
159
+ return None
160
+ payload = dict(payload)
161
+ if "task_kind" not in payload:
162
+ payload.setdefault("task_kind", payload.get("goal_type", ""))
163
+ payload.setdefault("task_slug", payload.get("task_id", "task"))
164
+ payload.pop("large_write_escalations", None)
165
+ return TaskState(**payload)
166
+ return SessionState(
167
+ session_id=data["session_id"],
168
+ repo_root=Path(data["repo_root"]),
169
+ created_at=data["created_at"],
170
+ profile=data.get("profile", "e4b"),
171
+ model=data.get("model", ""),
172
+ messages=list(data.get("messages", [])),
173
+ pinned_files=list(data.get("pinned_files", [])),
174
+ last_assistant_text=data.get("last_assistant_text", ""),
175
+ events=list(data.get("events", [])),
176
+ current_task=_load_task(current_task),
177
+ recent_tasks=[task for task in (_load_task(t) for t in recent_tasks) if task is not None],
178
+ todos=list(data.get("todos", [])),
179
+ )
180
+
181
+ def save(self, session: SessionState) -> Path:
182
+ path = self.sessions_dir / f"{session.session_id}.json"
183
+ payload = {
184
+ "session_id": session.session_id,
185
+ "repo_root": str(session.repo_root),
186
+ "created_at": session.created_at,
187
+ "profile": session.profile,
188
+ "model": session.model,
189
+ "messages": session.messages,
190
+ "pinned_files": session.pinned_files,
191
+ "last_assistant_text": session.last_assistant_text,
192
+ "events": session.events,
193
+ "todos": session.todos,
194
+ "current_task": None if session.current_task is None else {
195
+ "task_id": session.current_task.task_id,
196
+ "user_request": session.current_task.user_request,
197
+ "goal_type": session.current_task.goal_type,
198
+ "task_kind": session.current_task.task_kind,
199
+ "task_slug": session.current_task.task_slug,
200
+ "goal_summary": session.current_task.goal_summary,
201
+ "success_criteria": session.current_task.success_criteria,
202
+ "status": session.current_task.status,
203
+ "current_stage": session.current_task.current_stage,
204
+ "active_port": session.current_task.active_port,
205
+ "completion_status": session.current_task.completion_status,
206
+ "blocked_reason": session.current_task.blocked_reason,
207
+ "turn_count": session.current_task.turn_count,
208
+ "created_at": session.current_task.created_at,
209
+ "updated_at": session.current_task.updated_at,
210
+ "final_response": session.current_task.final_response,
211
+ },
212
+ "recent_tasks": [
213
+ {
214
+ "task_id": task.task_id,
215
+ "user_request": task.user_request,
216
+ "goal_type": task.goal_type,
217
+ "task_kind": task.task_kind,
218
+ "task_slug": task.task_slug,
219
+ "goal_summary": task.goal_summary,
220
+ "success_criteria": task.success_criteria,
221
+ "status": task.status,
222
+ "current_stage": task.current_stage,
223
+ "active_port": task.active_port,
224
+ "completion_status": task.completion_status,
225
+ "blocked_reason": task.blocked_reason,
226
+ "turn_count": task.turn_count,
227
+ "created_at": task.created_at,
228
+ "updated_at": task.updated_at,
229
+ "final_response": task.final_response,
230
+ }
231
+ for task in session.recent_tasks[-20:]
232
+ ],
233
+ }
234
+ # Secret redaction chokepoint. The transcript is verbatim: a key
235
+ # the user pasted, or one a tool result echoed, would persist in
236
+ # cleartext across restarts. Scrub credential VALUES out of every
237
+ # string in the payload before it hits disk.
238
+ try:
239
+ from .redaction import scrub_obj
240
+ payload = scrub_obj(payload)
241
+ except Exception:
242
+ pass
243
+ # Session JSON is a verbatim transcript (every user message, every
244
+ # model response, every tool result) — owner-read only.
245
+ write_private(path, json.dumps(payload, indent=2))
246
+ return path
247
+
248
+ def list_sessions(self) -> list[tuple[str, str, str]]:
249
+ rows: list[tuple[str, str, str]] = []
250
+ for path in sorted(self.sessions_dir.glob("*.json"), reverse=True):
251
+ data = json.loads(path.read_text())
252
+ rows.append((data["session_id"], data["created_at"], data["repo_root"]))
253
+ return rows
254
+
255
+ def latest_for_repo(self, repo_root: Path) -> SessionState | None:
256
+ matches: list[SessionState] = []
257
+ for path in sorted(self.sessions_dir.glob("*.json"), reverse=True):
258
+ data = json.loads(path.read_text())
259
+ if Path(data["repo_root"]).resolve() == repo_root.resolve():
260
+ def _load_task(payload: dict | None) -> TaskState | None:
261
+ if not isinstance(payload, dict):
262
+ return None
263
+ payload = dict(payload)
264
+ if "task_kind" not in payload:
265
+ payload.setdefault("task_kind", payload.get("goal_type", ""))
266
+ payload.setdefault("task_slug", payload.get("task_id", "task"))
267
+ payload.pop("large_write_escalations", None)
268
+ return TaskState(**payload)
269
+ matches.append(
270
+ SessionState(
271
+ session_id=data["session_id"],
272
+ repo_root=Path(data["repo_root"]),
273
+ created_at=data["created_at"],
274
+ profile=data.get("profile", "e4b"),
275
+ model=data.get("model", ""),
276
+ messages=list(data.get("messages", [])),
277
+ pinned_files=list(data.get("pinned_files", [])),
278
+ last_assistant_text=data.get("last_assistant_text", ""),
279
+ events=list(data.get("events", [])),
280
+ current_task=_load_task(data.get("current_task")),
281
+ recent_tasks=[task for task in (_load_task(t) for t in data.get("recent_tasks", [])) if task is not None],
282
+ )
283
+ )
284
+ return matches[0] if matches else None
285
+
286
+ def append_event(self, session: SessionState, event_type: str, detail: str, **metadata: str) -> None:
287
+ event = {
288
+ "time": utc_now(),
289
+ "type": event_type,
290
+ "detail": detail,
291
+ }
292
+ if metadata:
293
+ event.update({k: str(v) for k, v in metadata.items()})
294
+ session.events.append(event)
295
+ session.events = session.events[-200:]
296
+ self.save(session)
localcode/shell.py ADDED
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import signal
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from typing import Callable
8
+
9
+ from ._subproc_env import clean_env
10
+
11
+ DEFAULT_TIMEOUT = 120 # seconds
12
+ MAX_OUTPUT_CHARS = 100_000
13
+
14
+
15
+ @dataclass
16
+ class ShellResult:
17
+ command: str
18
+ returncode: int
19
+ output: str
20
+ timed_out: bool = False
21
+
22
+
23
+ def run_shell(
24
+ command: str,
25
+ cwd: str,
26
+ timeout: int = DEFAULT_TIMEOUT,
27
+ max_output: int = MAX_OUTPUT_CHARS,
28
+ on_output: Callable[[str], None] | None = None,
29
+ ) -> ShellResult:
30
+ env = clean_env()
31
+ env["LOCALCODE_SHELL"] = "1" # let child processes know they're inside localcode
32
+ try:
33
+ process = subprocess.Popen(
34
+ command,
35
+ cwd=cwd,
36
+ shell=True,
37
+ stdout=subprocess.PIPE,
38
+ stderr=subprocess.STDOUT,
39
+ text=True,
40
+ env=env,
41
+ preexec_fn=os.setsid if hasattr(os, "setsid") else None,
42
+ )
43
+ except Exception as exc:
44
+ return ShellResult(command=command, returncode=1, output=str(exc))
45
+
46
+ output_parts: list[str] = []
47
+ total_chars = 0
48
+ timed_out = False
49
+ assert process.stdout is not None
50
+
51
+ try:
52
+ for line in process.stdout:
53
+ output_parts.append(line)
54
+ total_chars += len(line)
55
+ if on_output is not None:
56
+ on_output(line.rstrip("\n"))
57
+ if total_chars >= max_output:
58
+ output_parts.append(f"\n...[output truncated at {max_output} chars]")
59
+ break
60
+ returncode = process.wait(timeout=timeout)
61
+ except subprocess.TimeoutExpired:
62
+ timed_out = True
63
+ # kill the entire process group
64
+ try:
65
+ if hasattr(os, "killpg"):
66
+ os.killpg(os.getpgid(process.pid), signal.SIGKILL)
67
+ else:
68
+ process.kill()
69
+ except Exception:
70
+ pass
71
+ process.wait(timeout=5)
72
+ output_parts.append(f"\n...[command timed out after {timeout}s]")
73
+ returncode = 124
74
+ except Exception as exc:
75
+ output_parts.append(f"\n...[error: {exc}]")
76
+ returncode = process.wait()
77
+
78
+ return ShellResult(
79
+ command=command,
80
+ returncode=returncode,
81
+ output="".join(output_parts),
82
+ timed_out=timed_out,
83
+ )
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: debug
3
+ description: Diagnose a bug — reproduce, read, hypothesize, fix, verify. Don't guess.
4
+ when_to_use: User reports an error, a test is failing, unexpected output, or "this doesn't work".
5
+ ---
6
+
7
+ Debug the reported issue. Follow in order, don't skip.
8
+
9
+ 1. **Reproduce.** Run the exact command or code path that surfaced the bug. Capture the actual error text — don't paraphrase it.
10
+ 2. **Read the relevant code.** Use `grep` / `read_file` to find the failing location. Read enough surrounding context to understand what was expected. (Use the `locate` skill if the target isn't obvious.)
11
+ 3. **Form ONE hypothesis** about the root cause. Write it in your reply before acting. "I think X because Y."
12
+ 4. **Make the minimal fix.** If you're wrong, the error will still be there — that's fine, iterate. (Rule 16: diagnose before retrying.)
13
+ 5. **Verify.** Re-run the reproduction command. If it now passes, use `edit-verified` / `run-tests` to confirm no regressions.
14
+
15
+ Don't:
16
+ - Guess fixes before reading the code.
17
+ - "Fix" by disabling the failing assertion / test.
18
+ - Add try/except that swallows the error.
19
+ - Declare victory without re-running the repro.
20
+
21
+ If you can't reproduce → ask for the exact command, environment, and full error output. Guessing wastes the user's time.
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: edit-verified
3
+ description: Make a code edit, then IMMEDIATELY verify it with syntax check + targeted test run before declaring done.
4
+ when_to_use: Any time you call write_file or edit_file. This is the SWE-agent linter-gate pattern — the ablation evidence for small-model edit reliability.
5
+ ---
6
+
7
+ Edit + verify in one atomic flow. Based on the SWE-agent linter-gate
8
+ ablation (Yang et al. 2024) — guarded edits were the single largest ACI
9
+ delta for small models.
10
+
11
+ After every `edit_file` or `write_file`, run in this order:
12
+
13
+ 1. **Syntax check** (language-appropriate, non-blocking on stdout):
14
+ - Python: `python -c "import ast; ast.parse(open('<path>').read())"`
15
+ - TS/JS: `node --check <path>` (JS only) or `npx tsc --noEmit <path>`
16
+ - Go: `gofmt -e <path>` (errors if syntax bad)
17
+ - Rust: covered by `cargo check` in step 2
18
+
19
+ 2. **Targeted test run** — only tests that could plausibly exercise this change:
20
+ - Python: `pytest tests/test_<same-basename>.py -q` if it exists; else scope by keyword `pytest -k <function-name>`
21
+ - JS: `npm test -- <path-to-test>` or `vitest run <path>`
22
+ - Rust: `cargo test <test-name>`
23
+ - Go: `go test ./<pkg> -run <TestName>`
24
+
25
+ 3. **If step 1 or 2 fails**: report the error verbatim, fix, re-run. Do NOT claim done.
26
+ If step 1 passes and step 2 has no matching test: say "syntax OK, no test covers this change" — don't imply tests ran when they didn't (rule 17).
27
+
28
+ 4. **If no tests exist anywhere**: run the smallest reproducer that exercises the change (import the module, call the function with a trivial input). Report what you ran.
29
+
30
+ Don't: claim "should work", "this is correct", or "the fix is straightforward" without the verify step. Don't re-run identical failing commands (rule 16).
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: explain
3
+ description: Walk through a file or function for a newcomer — purpose, flow, gotchas, concise.
4
+ when_to_use: User asks "explain X" / "how does Y work" / "I don't understand this". Also as a pre-edit orientation step before a non-trivial change.
5
+ ---
6
+
7
+ Explain the target code.
8
+
9
+ Structure:
10
+ 1. **Purpose** — one sentence. What problem does this solve?
11
+ 2. **Flow** — walk through the key logic in order of execution. Skip boilerplate (imports, trivial setters).
12
+ 3. **Gotchas** — 1-3 things a reader might miss:
13
+ - Non-obvious state mutations
14
+ - Side effects (I/O, globals, env vars)
15
+ - Subtle ordering dependencies
16
+ - Comments or naming that's misleading
17
+ 4. **What it does NOT do** — common misconceptions.
18
+
19
+ Concise. Reference `path:line` for key chunks. End with "Ask follow-ups if anything is unclear" — don't re-summarize. Small models tend to repeat themselves; resist.
@@ -0,0 +1,11 @@
1
+ ---
2
+ name: finish-verified
3
+ description: Finish a coding task once each requested outcome has concrete evidence.
4
+ when_to_use: Before declaring a coding task complete or when work is drifting into optional polish.
5
+ ---
6
+
7
+ 1. Restate the requested outcomes as a short checklist.
8
+ 2. Attach one concrete proof to each outcome: a focused test, build, runtime probe, or exact file inspection.
9
+ 3. Treat exit code zero as shell success, not automatically task success. Check output for masked errors, fallbacks, skipped checks, and pipelines that hide an upstream failure.
10
+ 4. Resolve only missing requested outcomes. Do not add unrelated cleanup or polish.
11
+ 5. When every outcome has evidence, stop and report the evidence. If one cannot be proven, name that gap precisely.
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: git-commit-safely
3
+ description: Stage and commit safely — new commit not amend, no --no-verify, no `git add -A`, match project message style.
4
+ when_to_use: User asks to commit. ALSO when they say "ship" / "save progress" / "wrap this up".
5
+ ---
6
+
7
+ Create a git commit safely.
8
+
9
+ Pre-commit checks (run in parallel):
10
+ - `git status` — see everything that's changed. NEVER use `-uall` (memory issue on large repos).
11
+ - `git diff --cached` — what's already staged.
12
+ - `git log --oneline -5` — the project's message style. Match it.
13
+
14
+ Rules:
15
+ - **NEW commits only.** Don't `--amend` unless the user explicitly says "amend". A failed pre-commit hook means the commit did NOT happen — amending would rewrite the PREVIOUS commit and potentially destroy work.
16
+ - **Never `git add -A` or `git add .`** — stage specific paths by name. Prevents accidentally committing `.env`, credentials, build output.
17
+ - **Never skip hooks** (`--no-verify`, `--no-gpg-sign`). Fix the hook failure; don't bypass.
18
+ - **Never force-push to main/master.** Warn before any force-push.
19
+
20
+ Use a HEREDOC for multi-line messages:
21
+ ```
22
+ git commit -m "$(cat <<'EOF'
23
+ Summary line here.
24
+
25
+ Optional body explaining why.
26
+ EOF
27
+ )"
28
+ ```
29
+
30
+ If there's nothing staged, don't create an empty commit. Tell the user.
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: locate
3
+ description: Find the exact file → class/function → line range to edit BEFORE reading or changing anything. Prevents aimless exploration.
4
+ when_to_use: User asks to change/fix/review a specific behavior without naming the file, or the task is "where does X happen?". First step for any non-trivial edit on an unfamiliar repo.
5
+ ---
6
+
7
+ Locate the target before acting. Based on the Agentless pattern
8
+ (Xia et al. 2024) — the strongest small-model SWE-bench delta came
9
+ from structured localization, not better planning.
10
+
11
+ Process:
12
+ 1. **Surface search** — `grep -rn "<the name-ish thing from the request>" src/ --include='*.py'`. Start wide, don't assume the exact symbol spelling.
13
+ 2. **Narrow to a file** — pick the most-referenced or most-obvious match. If 3+ files look equally relevant, say so and ask the user which one.
14
+ 3. **Inside that file, find the region** — `grep -En 'def <name>|class <name>' <file>` to get the line number of the specific function/class. (`-E` uses extended regex so `|` alternates without backslash.)
15
+ 4. **Read a bounded window** — `read_file(path=<file>, offset=<start-10>, limit=80)`. Don't read the whole file; 2-3 bit models choke on 1500-line blobs.
16
+ 5. **State what you found BEFORE editing**: "Target: `path/to/file.py:123` — `def foo(...)`. Making change because <reason>."
17
+
18
+ Anti-patterns (don't do):
19
+ - `list_files(.)` as first move when the user named a specific behavior.
20
+ - Reading a full file when a grep would pinpoint 40 lines.
21
+ - Editing before stating what you found.
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: plan-task
3
+ description: Decompose a multi-step task into a written checklist BEFORE coding. Read-and-update the checklist between steps.
4
+ when_to_use: Task will likely take >4 tool calls — new feature, multi-file change, migration, debugging an unknown. Skip for trivial edits.
5
+ disable-model-invocation: true
6
+ ---
7
+
8
+ Externalize the plan. Small quantized models (2-3 bit) drift on long
9
+ horizon tasks; a written plan becomes durable state they re-read.
10
+ Evidence: Agentless externalized-plan beat agent loops at comparable
11
+ compute (Xia et al. 2024); Voyager skill composition relies on the
12
+ same principle.
13
+
14
+ Format (output this as a markdown block BEFORE your first tool call):
15
+
16
+ ```
17
+ Goal: <one sentence — what "done" looks like>
18
+
19
+ Steps:
20
+ 1. [ ] <step 1 — small enough to verify independently>
21
+ verify: <command or assertion that proves it worked>
22
+ 2. [ ] <step 2>
23
+ verify: <...>
24
+ 3. [ ] <step 3>
25
+ verify: <...>
26
+
27
+ Risks: <1-2 things that could go wrong>
28
+ ```
29
+
30
+ Then execute ALL steps end-to-end in the SAME turn. After each step
31
+ completes, immediately move to the next step's tool call — do NOT
32
+ write a "progress so far / next steps" summary mid-task and stop. The
33
+ plan is a checklist for YOU to follow continuously, not a milestone
34
+ report for the user.
35
+
36
+ Only stop before finishing all steps if:
37
+ - A step's verify FAILS (then surface the failure and ask the user)
38
+ - The user explicitly asked you to stop after step N
39
+ - Tool execution returns REJECTED with a hard error you cannot recover from
40
+
41
+ Otherwise: keep going until every step is done. Save the final
42
+ summary for the very end, after the last step's verify passes.
43
+
44
+ Keep the plan ≤6 steps. If it's longer, the task is probably two
45
+ separate tasks — surface that to the user.
46
+
47
+ Anti-patterns:
48
+ - Re-planning after every step (wastes tokens).
49
+ - A 12-step plan that covers every imaginable branch.
50
+ - Writing "Current Progress / Next Steps" summary while steps remain.
51
+ - Treating completed-this-turn as completed-the-whole-task.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: review
3
+ description: Review code changes for correctness, security, performance, style — specific line-numbered feedback.
4
+ when_to_use: User asks for a review. Also as a second-pass sanity check after a non-trivial diff. Prefer this over informal "does this look right?"
5
+ ---
6
+
7
+ Review the diff (or scope the user named).
8
+
9
+ Focus in priority order:
10
+ 1. **Correctness** — bugs, off-by-one, wrong conditions, unhandled edge cases.
11
+ 2. **Security** — injection (SQL/command/XSS), auth gaps, data exposure, unsanitized input crossing a trust boundary.
12
+ 3. **Performance** — obvious N+1 queries, unbounded loops, blocking I/O on hot paths.
13
+ 4. **Style** — matches project conventions? Naming consistent?
14
+
15
+ Process:
16
+ - `git diff` (or the user-named scope) first. Don't review what you haven't seen.
17
+ - For each issue, cite `path:line` and explain why it matters.
18
+ - Suggest fixes as `edit_file` calls when confident. Describe and defer when unsure.
19
+ - Don't flag stylistic preferences that aren't in the project's style. Don't nitpick.
20
+ - End with 1-line verdict: ship / fix-first / blocked.
21
+
22
+ If the diff is clean, say so briefly. A 2-line "LGTM, one tiny suggestion" review beats a 40-line manufactured-concerns one.
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: run-tests
3
+ description: Run the project's AUTOMATED TEST SUITE (pytest / jest / cargo test / go test) to verify correctness. DO NOT use this to launch or start a user-facing app — for running an app, execute its entry point directly via the bash tool.
4
+ when_to_use: User explicitly says "run tests", "run the tests", "are tests passing", "check the tests". NOT when they say "run it" or "run the app" — that means launch the app, not the test suite.
5
+ ---
6
+
7
+ Detect and run the test suite.
8
+
9
+ Framework detection — check in this order:
10
+ 1. `pyproject.toml` has `[tool.pytest]` or `pytest` in dependencies → `pytest -q`
11
+ 2. `package.json` has a `test` script → `npm test` (or `pnpm`/`yarn` based on lockfile)
12
+ 3. `Cargo.toml` exists → `cargo test`
13
+ 4. `go.mod` exists → `go test ./...`
14
+ 5. Fallback: look for `tests/`, `test/`, `__tests__/`. Ask the user if unclear.
15
+
16
+ Scope to affected files when possible (faster):
17
+ - `pytest tests/test_foo.py` for a change to `src/foo.py`
18
+ - `pytest -k <function-name>` for a change to a specific function
19
+ - `cargo test <mod>::` for a single Rust module
20
+
21
+ Report faithfully per rule 17:
22
+ - **Passing**: "N tests ran, all passed."
23
+ - **Failing**: quote the exact failure output (first 20 lines). Don't hide.
24
+ - **Crashed** (didn't run): "Tests crashed during collection: <error>. I did NOT run them."