gemcode 0.3.48__tar.gz → 0.3.50__tar.gz
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.
- {gemcode-0.3.48/src/gemcode.egg-info → gemcode-0.3.50}/PKG-INFO +1 -1
- {gemcode-0.3.48 → gemcode-0.3.50}/pyproject.toml +1 -1
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/agent.py +126 -31
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/bash.py +54 -13
- gemcode-0.3.50/src/gemcode/tools/edit.py +82 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/filesystem.py +27 -7
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/search.py +18 -10
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/todo.py +42 -8
- {gemcode-0.3.48 → gemcode-0.3.50/src/gemcode.egg-info}/PKG-INFO +1 -1
- gemcode-0.3.48/src/gemcode/tools/edit.py +0 -53
- {gemcode-0.3.48 → gemcode-0.3.50}/LICENSE +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/MANIFEST.in +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/README.md +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/setup.cfg +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/cli.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/config.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/hooks.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/pricing.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/refine.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/repl_slash.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/session_store.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/version.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode.egg-info/SOURCES.txt +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_credentials.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_paths.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_permissions.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_tools.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_workspace_hints.py +0 -0
|
@@ -122,13 +122,68 @@ def _load_gemini_md(project_root: Path) -> str:
|
|
|
122
122
|
return combined[:_TOTAL_CAP]
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
def _get_git_context(root) -> str:
|
|
126
|
+
"""
|
|
127
|
+
Run a quick git snapshot at session start — branch, recent commits, diff-stat.
|
|
128
|
+
Returns a formatted string or empty string if not a git repo.
|
|
129
|
+
Mirrors OpenClaude's getGitStatus() pattern.
|
|
130
|
+
"""
|
|
131
|
+
import subprocess
|
|
132
|
+
import shutil
|
|
133
|
+
|
|
134
|
+
git = shutil.which("git")
|
|
135
|
+
if not git:
|
|
136
|
+
return ""
|
|
137
|
+
try:
|
|
138
|
+
def _run(*args, cwd=root):
|
|
139
|
+
r = subprocess.run(
|
|
140
|
+
[git, "--no-optional-locks"] + list(args),
|
|
141
|
+
cwd=str(cwd),
|
|
142
|
+
capture_output=True,
|
|
143
|
+
text=True,
|
|
144
|
+
timeout=5,
|
|
145
|
+
)
|
|
146
|
+
return r.stdout.strip() if r.returncode == 0 else ""
|
|
147
|
+
|
|
148
|
+
# Check it's a git repo
|
|
149
|
+
if not _run("rev-parse", "--is-inside-work-tree"):
|
|
150
|
+
return ""
|
|
151
|
+
|
|
152
|
+
branch = _run("rev-parse", "--abbrev-ref", "HEAD") or "HEAD"
|
|
153
|
+
log = _run("log", "--oneline", "-5")
|
|
154
|
+
status = _run("status", "--short")
|
|
155
|
+
username = _run("config", "user.name")
|
|
156
|
+
|
|
157
|
+
if not log: # empty repo
|
|
158
|
+
return ""
|
|
159
|
+
|
|
160
|
+
status_trunc = status[:2000] + "\n(truncated)" if len(status) > 2000 else status
|
|
161
|
+
|
|
162
|
+
lines = [
|
|
163
|
+
"This is the git state at session start — it is a snapshot and will NOT update automatically.",
|
|
164
|
+
f"Current branch: {branch}",
|
|
165
|
+
]
|
|
166
|
+
if username:
|
|
167
|
+
lines.append(f"Git user: {username}")
|
|
168
|
+
lines.append(f"Recent commits:\n{log}")
|
|
169
|
+
if status_trunc:
|
|
170
|
+
lines.append(f"Working tree status:\n{status_trunc}")
|
|
171
|
+
else:
|
|
172
|
+
lines.append("Working tree: clean")
|
|
173
|
+
return "\n\n".join(lines)
|
|
174
|
+
except Exception:
|
|
175
|
+
return ""
|
|
176
|
+
|
|
177
|
+
|
|
125
178
|
def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
126
179
|
"""
|
|
127
180
|
Injected every session so the model is fully self-aware of its own capabilities,
|
|
128
181
|
limits, and the environment — not just generic defaults.
|
|
129
182
|
"""
|
|
183
|
+
import datetime
|
|
130
184
|
root = cfg.project_root.resolve()
|
|
131
185
|
model = (getattr(cfg, "model", None) or "").strip() or "(default)"
|
|
186
|
+
today = datetime.date.today().strftime("%A, %B %d, %Y")
|
|
132
187
|
|
|
133
188
|
# ── Active capabilities ──────────────────────────────────────────────────
|
|
134
189
|
caps: list[str] = []
|
|
@@ -178,7 +233,12 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
|
178
233
|
"would benefit from background parallelism."
|
|
179
234
|
)
|
|
180
235
|
|
|
236
|
+
# ── Git context ───────────────────────────────────────────────────────────
|
|
237
|
+
git_ctx = _get_git_context(root)
|
|
238
|
+
git_section = f"\n\n## Git context (snapshot at session start)\n{git_ctx}" if git_ctx else ""
|
|
239
|
+
|
|
181
240
|
return f"""## Runtime facts (authoritative for this session)
|
|
241
|
+
- **Today's date:** {today}
|
|
182
242
|
- **Project root** — every filesystem tool path is relative to: `{root}`
|
|
183
243
|
- **Model id in use:** `{model}`. Override mid-session with `/model use <id>` or `/mode fast|balanced|quality|auto`.
|
|
184
244
|
- **Execution budget:** {budget_line}.
|
|
@@ -190,7 +250,7 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
|
190
250
|
{kairos_section}
|
|
191
251
|
- **UI banner** phrases like "GemCode Pro" are terminal marketing, not a separate API tier.
|
|
192
252
|
- **Env toggles** (`GEMCODE_ENABLE_COMPUTER_USE`, `GEMCODE_MODEL`, etc.) affect only the OS process that launched gemcode. Pasting `VAR=1` in chat does NOT reconfigure a running session—tell the user to export in their shell, use project `.env`, or restart the CLI.
|
|
193
|
-
- **Working in subfolders** — call `list_directory("Desktop")`, `glob_files("**/query.ts")`, `read_file("testing/ai-edtech-app/src/app/page.tsx")` directly. Never claim access is blocked unless a tool returned an explicit error."""
|
|
253
|
+
- **Working in subfolders** — call `list_directory("Desktop")`, `glob_files("**/query.ts")`, `read_file("testing/ai-edtech-app/src/app/page.tsx")` directly. Never claim access is blocked unless a tool returned an explicit error.{git_section}"""
|
|
194
254
|
|
|
195
255
|
|
|
196
256
|
def _build_memory_section(cfg: GemCodeConfig) -> str:
|
|
@@ -398,25 +458,19 @@ You run locally via the GemCode CLI. You are the same agent the user launched
|
|
|
398
458
|
|
|
399
459
|
## Core identity and approach
|
|
400
460
|
|
|
401
|
-
|
|
461
|
+
Before you respond to anything, **think through what the person is actually trying to achieve**. Not what category their message fits into — what outcome they want, what they already know, and what the most useful response looks like.
|
|
402
462
|
|
|
403
|
-
|
|
404
|
-
|---|---|---|
|
|
405
|
-
| **Greeting / chitchat** | "hi", "thanks", "cool" | Reply warmly in one sentence. No tools. |
|
|
406
|
-
| **General knowledge** | "what is a closure?", "explain OAuth" | Answer from knowledge. No tools unless this specific repo is needed. |
|
|
407
|
-
| **Project question** | "how does auth work here?", "what's in this folder?" | 1–2 read-only tools, then a focused answer. |
|
|
408
|
-
| **Engineering task** | "fix the bug", "add pagination", "refactor X" | Orient → Plan → Execute → Verify. |
|
|
409
|
-
| **Analysis / audit** | "analyse the whole backend", "summarise all endpoints" | Thorough tool sweep, then synthesise. |
|
|
463
|
+
That thinking should drive everything: how much you use tools, how deep you go, how long you reply, what tone you take. A one-line social message deserves a one-line reply. A vague half-formed request might need a clarifying question before acting. A complex multi-file task needs systematic exploration first. A debugging session mid-error needs a hypothesis, not a search. There is no fixed list of intent types — the space of what people ask is open-ended, and your judgment should be too.
|
|
410
464
|
|
|
411
|
-
**
|
|
465
|
+
The one hard rule: **only reach for tools when they genuinely serve the response**. If the answer is in your knowledge, give it. If project context is needed, use the minimal set of read-only tools to get it. If you need to execute something, do it. But never open a tool call just to appear busy.
|
|
412
466
|
|
|
413
|
-
|
|
414
|
-
1. **
|
|
415
|
-
2. **Plan
|
|
416
|
-
3. **Execute** —
|
|
417
|
-
4. **Verify
|
|
467
|
+
When you do need to act on the codebase:
|
|
468
|
+
1. **Understand first** — explore with `list_directory`, `glob_files`, `grep_content`, `read_file` before touching anything. These are instant and need no permission.
|
|
469
|
+
2. **Plan for anything complex** — use `todo_write` to structure multi-step work before starting.
|
|
470
|
+
3. **Execute completely** — don't stop at the first success. Finish the whole task.
|
|
471
|
+
4. **Verify before you call it done** — check your own work.
|
|
418
472
|
|
|
419
|
-
|
|
473
|
+
The depth of each step should match the complexity of what was asked. Don't run a four-step engineering workflow for a one-sentence question.
|
|
420
474
|
|
|
421
475
|
## CRITICAL: Read-only tools first — never bash for exploration
|
|
422
476
|
`bash` and `run_command` require permission confirmation by default. Always start with the **zero-permission** read-only tools:
|
|
@@ -439,12 +493,10 @@ You have native deep thinking capability — use it actively:
|
|
|
439
493
|
- **For trade-off decisions** (which library, which pattern, which approach): reason through the pros/cons given this specific codebase.
|
|
440
494
|
|
|
441
495
|
## Interpreting requests
|
|
442
|
-
-
|
|
443
|
-
- **Engineering tasks** ("fix", "add", "refactor", "analyse", "debug"): infer from the repo — search, read, then act. Do not give abstract advice when concrete files exist.
|
|
444
|
-
- If the user refers to symbols or behaviors, **find them** with `glob_files`/`grep_content`/`list_directory` — never ask them to paste paths you can discover yourself.
|
|
496
|
+
- If the user refers to symbols, files, or behaviors they expect you to know — **find them** with `glob_files`/`grep_content`/`list_directory`. Never ask them to paste paths you can discover yourself.
|
|
445
497
|
- **Never propose edits to files you haven't read.** Read first, then edit.
|
|
446
498
|
- When something fails, diagnose (re-read the error, check assumptions) before switching strategy. Do not repeat the same failed call.
|
|
447
|
-
-
|
|
499
|
+
- When asked to analyse or explain something: read the actual files, produce concrete findings, not hypotheses.
|
|
448
500
|
|
|
449
501
|
## Tool selection guide
|
|
450
502
|
|
|
@@ -535,13 +587,16 @@ One user message = many model↔tool rounds (up to 256 LLM calls by default). Th
|
|
|
535
587
|
|
|
536
588
|
**Do not stop after step 2 or 3** — complete the full task.
|
|
537
589
|
|
|
538
|
-
## Parallelism
|
|
539
|
-
Issue independent tool calls in the same turn when outputs don't depend on each other
|
|
540
|
-
|
|
541
|
-
-
|
|
542
|
-
-
|
|
543
|
-
-
|
|
544
|
-
|
|
590
|
+
## Parallelism — batch independent work
|
|
591
|
+
Issue independent tool calls **in the same turn** when outputs don't depend on each other.
|
|
592
|
+
This is faster and costs fewer turns. Concrete examples:
|
|
593
|
+
- Reading multiple files → send all `read_file` calls together
|
|
594
|
+
- Grepping different patterns → one message, multiple `grep_content` calls
|
|
595
|
+
- `list_directory` + `glob_files` → issue both at once
|
|
596
|
+
- Exploring multiple subsystems → one `run_subtask` per subsystem in one turn
|
|
597
|
+
- `git status` and `git log` → chain with `&&` or issue in parallel
|
|
598
|
+
|
|
599
|
+
Sequential only when step B genuinely needs step A's output.
|
|
545
600
|
|
|
546
601
|
## Sub-agent delegation (orchestrator-worker pattern)
|
|
547
602
|
Use `run_subtask` when the work is better done in an isolated context:
|
|
@@ -603,11 +658,51 @@ For tasks where quality matters:
|
|
|
603
658
|
- **Unexpected file content**: re-read the actual file rather than assuming your mental model is correct.
|
|
604
659
|
- **Compiler / linter errors pasted by the user**: extract the file path and line from the error, read that file, apply the minimal fix, and re-run the check. Never explain without fixing.
|
|
605
660
|
|
|
661
|
+
## Git Safety Protocol
|
|
662
|
+
Follow these rules on every turn, no exceptions:
|
|
663
|
+
- **NEVER** update git config
|
|
664
|
+
- **NEVER** run destructive git commands (`push --force`, `reset --hard`, `checkout .`, `restore .`, `clean -f`, `branch -D`) unless the user *explicitly* asks for it
|
|
665
|
+
- **NEVER** skip hooks (`--no-verify`, `--no-gpg-sign`) unless the user explicitly requests it
|
|
666
|
+
- **NEVER** force-push to main/master — warn the user if they ask for this
|
|
667
|
+
- **Prefer NEW commits over amending.** Only amend when all three conditions hold: (a) user explicitly asked, (b) the commit was created in this session, (c) it has NOT been pushed to remote. If a pre-commit hook rejects a commit, the commit did NOT happen — fix the problem and create a NEW commit, never amend.
|
|
668
|
+
- **Stage selectively** — prefer `git add <specific-file>` over `git add -A` or `git add .` to avoid accidentally including `.env`, credentials, or large binaries
|
|
669
|
+
- **Never commit unless the user explicitly asks.** It is very important to only commit when asked.
|
|
670
|
+
|
|
671
|
+
## Committing changes
|
|
672
|
+
When the user asks for a git commit:
|
|
673
|
+
1. Run in parallel: `git status`, `git diff`, `git log --oneline -5` (to match their style)
|
|
674
|
+
2. Analyze all staged changes and draft a concise commit message (1-2 sentences, focus on *why* not *what*)
|
|
675
|
+
3. Check for sensitive files (.env, credentials) — warn if they're staged
|
|
676
|
+
4. Stage specific files, then commit via HEREDOC:
|
|
677
|
+
```
|
|
678
|
+
git commit -m "$(cat <<'EOF'
|
|
679
|
+
Your message here.
|
|
680
|
+
EOF
|
|
681
|
+
)"
|
|
682
|
+
```
|
|
683
|
+
5. Run `git status` after to confirm success
|
|
684
|
+
6. Do NOT push unless explicitly asked
|
|
685
|
+
|
|
686
|
+
## Creating pull requests
|
|
687
|
+
Use `gh pr create` via `bash`. When asked to create a PR:
|
|
688
|
+
1. Run in parallel: `git status`, `git diff`, `git log [base]...HEAD`, check remote tracking
|
|
689
|
+
2. Look at ALL commits in the PR (not just the latest)
|
|
690
|
+
3. Push branch if needed: `git push -u origin HEAD`
|
|
691
|
+
4. Create with: `gh pr create --title "..." --body "$(cat <<'EOF'\n## Summary\n...\n## Test plan\n...\nEOF\n)"`
|
|
692
|
+
5. Return the PR URL
|
|
693
|
+
|
|
606
694
|
## Risk and permissions
|
|
607
695
|
- State destructive operations clearly before doing them (deletes, force-push, data truncation).
|
|
608
696
|
- For `bash` commands that could be destructive (`rm -rf`, `git push --force`), confirm with the user first.
|
|
609
697
|
- If a tool is denied, adjust the plan — don't retry the same gated call.
|
|
610
698
|
|
|
699
|
+
## Avoid unnecessary sleep / polling
|
|
700
|
+
- Do NOT `sleep` between commands that can run immediately — just run them
|
|
701
|
+
- Do NOT poll a process in a sleep loop — check its status directly or start it with `background=True`
|
|
702
|
+
- If you're waiting for a background process you started, do not poll — it will complete on its own
|
|
703
|
+
- If you must wait (e.g. for a server to start), use a one-shot check: `bash("sleep 2 && curl -s http://localhost:3000")`
|
|
704
|
+
- Do NOT retry failing commands in a sleep loop — diagnose the root cause first
|
|
705
|
+
|
|
611
706
|
## Communication
|
|
612
707
|
- One short line before the first tool call in a turn (e.g. "Reading the auth module and checking the test suite...").
|
|
613
708
|
- Summarize tool results in plain language — the user doesn't see raw tool internals.
|
|
@@ -761,10 +856,10 @@ def build_root_agent(
|
|
|
761
856
|
# prepended to every agent's effective instruction.
|
|
762
857
|
global_instr = (
|
|
763
858
|
"You are GemCode, an expert software engineering agent powered by Google Gemini. "
|
|
764
|
-
"Think
|
|
765
|
-
"
|
|
766
|
-
"
|
|
767
|
-
"
|
|
859
|
+
"Think deeply about what the person actually wants before you do anything. "
|
|
860
|
+
"Use exactly as many tools as the task genuinely requires — no more. "
|
|
861
|
+
"Act fully and autonomously when action is needed. "
|
|
862
|
+
"Always use read-only tools before shell or write tools."
|
|
768
863
|
)
|
|
769
864
|
|
|
770
865
|
agent_kwargs: dict = dict(
|
|
@@ -40,19 +40,60 @@ def make_bash_tool(cfg: GemCodeConfig):
|
|
|
40
40
|
Run an arbitrary shell command via bash. Supports pipelines, redirects,
|
|
41
41
|
subshells, and multi-step workflows that run_command cannot express.
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
## Common usage patterns
|
|
44
|
+
|
|
45
|
+
Git / version control:
|
|
46
|
+
bash("git log --oneline -20")
|
|
47
|
+
bash("git diff HEAD~1 -- src/api/")
|
|
48
|
+
bash("git status && git diff --stat")
|
|
49
|
+
bash("git stash list")
|
|
50
|
+
|
|
51
|
+
Builds / tests:
|
|
52
|
+
bash("npm run build 2>&1 | tail -50")
|
|
53
|
+
bash("pytest tests/ -x -q --tb=short 2>&1 | head -150")
|
|
54
|
+
bash("cargo build --release 2>&1 | tail -30")
|
|
55
|
+
bash("go test ./... 2>&1 | tail -50")
|
|
56
|
+
|
|
57
|
+
Pipelines and inspection:
|
|
58
|
+
bash("find . -name '*.py' | xargs grep -l 'SomeClass' | head -20")
|
|
59
|
+
bash("cat package.json | python3 -m json.tool")
|
|
60
|
+
bash("wc -l $(find . -name '*.py') | sort -n | tail -20")
|
|
61
|
+
|
|
62
|
+
Long-running servers — ALWAYS use background=True:
|
|
63
|
+
bash("npm run dev", background=True)
|
|
64
|
+
bash("python manage.py runserver", background=True)
|
|
65
|
+
bash("tail -f logs/app.log", background=True)
|
|
66
|
+
NEVER call bash("npm run dev") without background=True — it blocks forever.
|
|
67
|
+
|
|
68
|
+
## Issuing multiple commands
|
|
69
|
+
When commands are independent, issue them in separate parallel tool calls
|
|
70
|
+
in the same turn. When they depend on each other, chain with && in one call.
|
|
71
|
+
Use ; only when you don't care if earlier commands fail.
|
|
72
|
+
DO NOT use newlines to separate commands (newlines are ok inside quoted strings).
|
|
73
|
+
|
|
74
|
+
## Git Safety Protocol — always follow these rules
|
|
75
|
+
- NEVER update git config
|
|
76
|
+
- NEVER run destructive git commands (push --force, reset --hard, checkout .,
|
|
77
|
+
restore ., clean -f, branch -D) without explicit user instruction
|
|
78
|
+
- NEVER skip hooks (--no-verify, --no-gpg-sign) unless the user explicitly asks
|
|
79
|
+
- NEVER force-push to main/master — warn the user if they request it
|
|
80
|
+
- ALWAYS prefer creating a NEW commit over amending an existing one.
|
|
81
|
+
Amending is ONLY appropriate when: (a) the user explicitly asks for it AND
|
|
82
|
+
(b) the commit has not been pushed to a remote yet
|
|
83
|
+
- When staging files, prefer adding specific files by name rather than
|
|
84
|
+
"git add -A" or "git add ." which can include .env or credentials
|
|
85
|
+
- Do NOT commit unless the user explicitly asks you to
|
|
86
|
+
|
|
87
|
+
## Avoid unnecessary sleep
|
|
88
|
+
- Do NOT sleep between commands that can run immediately — just run them
|
|
89
|
+
- Do NOT poll in a sleep loop — check process status directly or use background=True
|
|
90
|
+
- If waiting for a background task, do not sleep-poll; it was started and will finish
|
|
91
|
+
- If you must wait (rate limits, deliberate pacing), keep it under 5 seconds
|
|
92
|
+
|
|
93
|
+
## Security
|
|
94
|
+
Be precise. Avoid destructive operations (rm -rf, force-push) without
|
|
95
|
+
explicit user approval. Quote file paths that contain spaces.
|
|
96
|
+
cwd_subdir is relative to the project root.
|
|
56
97
|
"""
|
|
57
98
|
if not trusted:
|
|
58
99
|
return {"error": "Project folder is not trusted. Re-run GemCode and approve folder trust."}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Write and search_replace tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from gemcode.config import GemCodeConfig
|
|
8
|
+
from gemcode.paths import PathEscapeError, resolve_under_root
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def make_edit_tools(cfg: GemCodeConfig):
|
|
12
|
+
root = cfg.project_root
|
|
13
|
+
|
|
14
|
+
def write_file(path: str, content: str) -> dict:
|
|
15
|
+
"""
|
|
16
|
+
Create or overwrite a file with the given content.
|
|
17
|
+
|
|
18
|
+
Path is relative to the project root.
|
|
19
|
+
|
|
20
|
+
IMPORTANT: If the file already exists, READ it first with read_file() before
|
|
21
|
+
calling write_file(). Overwriting without reading risks losing existing content.
|
|
22
|
+
Only use write_file for NEW files or when you intend a complete replacement.
|
|
23
|
+
For targeted in-place edits, use search_replace() instead.
|
|
24
|
+
|
|
25
|
+
Never write non-textual content (binary, base64 blobs) — those belong in
|
|
26
|
+
artifacts, not in source files.
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
p = resolve_under_root(root, path)
|
|
30
|
+
except PathEscapeError as e:
|
|
31
|
+
return {"error": str(e)}
|
|
32
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
p.write_text(content, encoding="utf-8")
|
|
34
|
+
return {"path": path, "bytes_written": len(content.encode("utf-8"))}
|
|
35
|
+
|
|
36
|
+
def search_replace(
|
|
37
|
+
path: str,
|
|
38
|
+
old_string: str,
|
|
39
|
+
new_string: str,
|
|
40
|
+
replace_all: bool = False,
|
|
41
|
+
) -> dict:
|
|
42
|
+
"""
|
|
43
|
+
Perform an exact string replacement in a file.
|
|
44
|
+
|
|
45
|
+
IMPORTANT: You MUST call read_file() on the file at least once before calling
|
|
46
|
+
search_replace(). Editing a file you haven't read leads to wrong context
|
|
47
|
+
and broken changes.
|
|
48
|
+
|
|
49
|
+
Usage rules:
|
|
50
|
+
- old_string must match the file EXACTLY (whitespace, indentation, line endings).
|
|
51
|
+
The edit FAILS if old_string is not found, or if it appears more than once
|
|
52
|
+
and replace_all is False.
|
|
53
|
+
- Use the smallest old_string that is clearly unique — typically 3-5 lines of
|
|
54
|
+
surrounding context is enough. Do not include 20+ lines of context when 4
|
|
55
|
+
lines would uniquely identify the target location.
|
|
56
|
+
- Set replace_all=True to rename a variable or rename a string across the whole file.
|
|
57
|
+
- Always prefer search_replace over write_file for targeted edits — it preserves
|
|
58
|
+
the rest of the file and makes the change reviewable.
|
|
59
|
+
- Do NOT add emojis or comments that just explain what the code does. Only add
|
|
60
|
+
comments that explain non-obvious intent or trade-offs.
|
|
61
|
+
- NEVER propose edits before reading. Read first. Edit second.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
p = resolve_under_root(root, path)
|
|
65
|
+
except PathEscapeError as e:
|
|
66
|
+
return {"error": str(e)}
|
|
67
|
+
if not p.is_file():
|
|
68
|
+
return {"error": f"Not a file: {path}"}
|
|
69
|
+
text = p.read_text(encoding="utf-8", errors="strict")
|
|
70
|
+
count = text.count(old_string)
|
|
71
|
+
if count == 0:
|
|
72
|
+
return {"error": "old_string not found"}
|
|
73
|
+
if count > 1 and not replace_all:
|
|
74
|
+
return {"error": f"old_string appears {count} times; set replace_all=true or narrow snippet"}
|
|
75
|
+
if replace_all:
|
|
76
|
+
new_text = text.replace(old_string, new_string)
|
|
77
|
+
else:
|
|
78
|
+
new_text = text.replace(old_string, new_string, 1)
|
|
79
|
+
p.write_text(new_text, encoding="utf-8")
|
|
80
|
+
return {"path": path, "replacements": count if replace_all else 1}
|
|
81
|
+
|
|
82
|
+
return write_file, search_replace
|
|
@@ -21,12 +21,19 @@ def make_filesystem_tools(cfg: GemCodeConfig):
|
|
|
21
21
|
end_line: int | None = None,
|
|
22
22
|
) -> dict:
|
|
23
23
|
"""
|
|
24
|
-
Read a text file relative to the project root.
|
|
24
|
+
Read a text file relative to the project root.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
IMPORTANT: ALWAYS use read_file before editing a file. Never propose changes
|
|
27
|
+
to code you haven't read — the mental model is always wrong without reading.
|
|
28
|
+
Never use bash("cat file") or bash("head file") — use read_file instead.
|
|
29
|
+
|
|
30
|
+
For large files, use start_line / end_line to read a specific range (1-indexed, inclusive):
|
|
31
|
+
read_file("app.py", start_line=100, end_line=200) — lines 100-200
|
|
32
|
+
read_file("app.py", start_line=500) — line 500 to end
|
|
33
|
+
This is efficient — loads only the needed slice into context.
|
|
34
|
+
|
|
35
|
+
When multiple files are needed, issue all read_file calls in the same turn
|
|
36
|
+
(parallel reads) rather than sequentially.
|
|
30
37
|
"""
|
|
31
38
|
if not trusted:
|
|
32
39
|
return {"error": "Project folder is not trusted. Re-run GemCode and approve folder trust."}
|
|
@@ -96,7 +103,13 @@ def make_filesystem_tools(cfg: GemCodeConfig):
|
|
|
96
103
|
return {"src": src, "dest": dest, "moved": True}
|
|
97
104
|
|
|
98
105
|
def list_directory(path: str = ".") -> dict:
|
|
99
|
-
"""
|
|
106
|
+
"""
|
|
107
|
+
List files and directories under a path (relative to project root).
|
|
108
|
+
|
|
109
|
+
Use this instead of bash("ls ...") — it needs no permission and is instant.
|
|
110
|
+
Prefer for directory exploration before any editing or execution.
|
|
111
|
+
Issue in parallel with glob_files when you need both structure and file matches.
|
|
112
|
+
"""
|
|
100
113
|
if not trusted:
|
|
101
114
|
return {"error": "Project folder is not trusted. Re-run GemCode and approve folder trust."}
|
|
102
115
|
try:
|
|
@@ -116,7 +129,14 @@ def make_filesystem_tools(cfg: GemCodeConfig):
|
|
|
116
129
|
return {"path": path, "entries": entries[:500]}
|
|
117
130
|
|
|
118
131
|
def glob_files(pattern: str) -> dict:
|
|
119
|
-
"""
|
|
132
|
+
"""
|
|
133
|
+
Find files by glob pattern relative to project root (e.g. 'src/**/*.py').
|
|
134
|
+
|
|
135
|
+
Use this instead of bash("find . -name '*.py'") — it needs no permission.
|
|
136
|
+
Supports recursive patterns: '**/*.ts', 'src/**/test_*.py', '**/config*.json'.
|
|
137
|
+
Can be issued in parallel with list_directory and grep_content in the same turn.
|
|
138
|
+
Returns up to 200 matches.
|
|
139
|
+
"""
|
|
120
140
|
if not trusted:
|
|
121
141
|
return {"error": "Project folder is not trusted. Re-run GemCode and approve folder trust."}
|
|
122
142
|
if ".." in pattern or pattern.startswith("/"):
|
|
@@ -38,20 +38,28 @@ def make_grep_tool(cfg: GemCodeConfig):
|
|
|
38
38
|
case_sensitive: bool = True,
|
|
39
39
|
) -> dict:
|
|
40
40
|
"""
|
|
41
|
-
Search file contents with a regex pattern.
|
|
41
|
+
Search file contents with a regex pattern (backed by ripgrep when available).
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Use this instead of bash("grep -r pattern .") — it needs no permission
|
|
44
|
+
and is instant. Binary files are skipped automatically.
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
-
|
|
49
|
-
|
|
46
|
+
Parameters:
|
|
47
|
+
- pattern: Regex pattern (Python/ripgrep syntax). Use | for alternation.
|
|
48
|
+
- path_glob: File glob relative to project root (default: all files).
|
|
49
|
+
- context_lines: Lines before+after each match (like grep -C). Use to see
|
|
50
|
+
surrounding code — e.g. context_lines=4 shows a function's body.
|
|
51
|
+
- case_sensitive: False for case-insensitive search.
|
|
52
|
+
- max_matches: Cap on returned results (1–500, default 80).
|
|
50
53
|
|
|
51
54
|
Examples:
|
|
52
|
-
grep_content("
|
|
53
|
-
grep_content("
|
|
54
|
-
grep_content("
|
|
55
|
+
grep_content("def authenticate", "**/*.py", context_lines=4)
|
|
56
|
+
grep_content("TODO|FIXME|HACK", "**/*.ts")
|
|
57
|
+
grep_content("import React", "**/*.tsx", case_sensitive=False)
|
|
58
|
+
grep_content("class.*Error", "**/*.py", context_lines=2)
|
|
59
|
+
grep_content("useState", "src/**/*.tsx", context_lines=3)
|
|
60
|
+
|
|
61
|
+
Issue multiple grep_content calls in the same turn when searching for
|
|
62
|
+
different patterns — they run in parallel.
|
|
55
63
|
"""
|
|
56
64
|
if max_matches < 1:
|
|
57
65
|
max_matches = 1
|
|
@@ -21,14 +21,48 @@ def make_todo_tool(cfg: GemCodeConfig):
|
|
|
21
21
|
tool_context: ToolContext,
|
|
22
22
|
) -> dict[str, Any]:
|
|
23
23
|
"""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
Create and maintain a structured task list for the current session.
|
|
25
|
+
Tracks progress, organises complex tasks, and makes multi-step work
|
|
26
|
+
visible to the user.
|
|
27
|
+
|
|
28
|
+
## When to use
|
|
29
|
+
Use proactively when:
|
|
30
|
+
1. Task has 3 or more distinct steps
|
|
31
|
+
2. Task is non-trivial and requires careful planning
|
|
32
|
+
3. User provides a list of things to do (numbered or comma-separated)
|
|
33
|
+
4. After receiving new instructions — capture them as todos immediately
|
|
34
|
+
5. When you start working on a sub-task — mark it in_progress BEFORE beginning.
|
|
35
|
+
Only one task should be in_progress at a time.
|
|
36
|
+
6. After completing a sub-task — mark it completed and add any discovered follow-ups
|
|
37
|
+
|
|
38
|
+
## When NOT to use
|
|
39
|
+
Skip this tool when:
|
|
40
|
+
1. There is only one simple, straightforward task
|
|
41
|
+
2. The task is trivial (can be done in 1-2 steps)
|
|
42
|
+
3. The task is purely conversational or informational
|
|
43
|
+
4. Answering a question that requires no planning
|
|
44
|
+
|
|
45
|
+
## Verification
|
|
46
|
+
After completing a list of 3 or more tasks, if none of them was a verification
|
|
47
|
+
step, add a final verification task: "Verify all changes are correct and
|
|
48
|
+
consistent" — then actually do it (re-read key files, run tests, check imports).
|
|
49
|
+
|
|
50
|
+
## Args
|
|
51
|
+
- merge: True = upsert by id (update specific items). False = replace entire list.
|
|
52
|
+
- todos: list of {id: str, content: str, status: pending|in_progress|completed|cancelled}
|
|
53
|
+
|
|
54
|
+
## Examples
|
|
55
|
+
Good use (complex multi-step task):
|
|
56
|
+
todo_write(merge=False, todos=[
|
|
57
|
+
{"id":"1","content":"Read current auth.ts","status":"in_progress"},
|
|
58
|
+
{"id":"2","content":"Add JWT refresh logic","status":"pending"},
|
|
59
|
+
{"id":"3","content":"Update tests","status":"pending"},
|
|
60
|
+
{"id":"4","content":"Run npm run build to verify","status":"pending"},
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
Bad use (single trivial task — just do it directly):
|
|
64
|
+
todo_write(merge=False, todos=[{"id":"1","content":"Fix typo in README","status":"pending"}])
|
|
65
|
+
# Don't do this. Just fix the typo.
|
|
32
66
|
"""
|
|
33
67
|
if not isinstance(todos, list):
|
|
34
68
|
return {"error": "todos must be a list"}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
"""Write and search_replace tools."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from gemcode.config import GemCodeConfig
|
|
8
|
-
from gemcode.paths import PathEscapeError, resolve_under_root
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def make_edit_tools(cfg: GemCodeConfig):
|
|
12
|
-
root = cfg.project_root
|
|
13
|
-
|
|
14
|
-
def write_file(path: str, content: str) -> dict:
|
|
15
|
-
"""Create or overwrite a file relative to the project root."""
|
|
16
|
-
try:
|
|
17
|
-
p = resolve_under_root(root, path)
|
|
18
|
-
except PathEscapeError as e:
|
|
19
|
-
return {"error": str(e)}
|
|
20
|
-
p.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
-
p.write_text(content, encoding="utf-8")
|
|
22
|
-
return {"path": path, "bytes_written": len(content.encode("utf-8"))}
|
|
23
|
-
|
|
24
|
-
def search_replace(
|
|
25
|
-
path: str,
|
|
26
|
-
old_string: str,
|
|
27
|
-
new_string: str,
|
|
28
|
-
replace_all: bool = False,
|
|
29
|
-
) -> dict:
|
|
30
|
-
"""
|
|
31
|
-
Replace old_string with new_string in a text file. Fails if old_string
|
|
32
|
-
is missing or duplicate (unless replace_all=True).
|
|
33
|
-
"""
|
|
34
|
-
try:
|
|
35
|
-
p = resolve_under_root(root, path)
|
|
36
|
-
except PathEscapeError as e:
|
|
37
|
-
return {"error": str(e)}
|
|
38
|
-
if not p.is_file():
|
|
39
|
-
return {"error": f"Not a file: {path}"}
|
|
40
|
-
text = p.read_text(encoding="utf-8", errors="strict")
|
|
41
|
-
count = text.count(old_string)
|
|
42
|
-
if count == 0:
|
|
43
|
-
return {"error": "old_string not found"}
|
|
44
|
-
if count > 1 and not replace_all:
|
|
45
|
-
return {"error": f"old_string appears {count} times; set replace_all=true or narrow snippet"}
|
|
46
|
-
if replace_all:
|
|
47
|
-
new_text = text.replace(old_string, new_string)
|
|
48
|
-
else:
|
|
49
|
-
new_text = text.replace(old_string, new_string, 1)
|
|
50
|
-
p.write_text(new_text, encoding="utf-8")
|
|
51
|
-
return {"path": path, "replacements": count if replace_all else 1}
|
|
52
|
-
|
|
53
|
-
return write_file, search_replace
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|