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.
Files changed (122) hide show
  1. {gemcode-0.3.48/src/gemcode.egg-info → gemcode-0.3.50}/PKG-INFO +1 -1
  2. {gemcode-0.3.48 → gemcode-0.3.50}/pyproject.toml +1 -1
  3. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/agent.py +126 -31
  4. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/bash.py +54 -13
  5. gemcode-0.3.50/src/gemcode/tools/edit.py +82 -0
  6. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/filesystem.py +27 -7
  7. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/search.py +18 -10
  8. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/todo.py +42 -8
  9. {gemcode-0.3.48 → gemcode-0.3.50/src/gemcode.egg-info}/PKG-INFO +1 -1
  10. gemcode-0.3.48/src/gemcode/tools/edit.py +0 -53
  11. {gemcode-0.3.48 → gemcode-0.3.50}/LICENSE +0 -0
  12. {gemcode-0.3.48 → gemcode-0.3.50}/MANIFEST.in +0 -0
  13. {gemcode-0.3.48 → gemcode-0.3.50}/README.md +0 -0
  14. {gemcode-0.3.48 → gemcode-0.3.50}/setup.cfg +0 -0
  15. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/__init__.py +0 -0
  16. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/__main__.py +0 -0
  17. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/audit.py +0 -0
  18. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/autocompact.py +0 -0
  19. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/callbacks.py +0 -0
  20. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/capability_routing.py +0 -0
  21. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/cli.py +0 -0
  22. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/compaction.py +0 -0
  23. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/computer_use/__init__.py +0 -0
  24. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/computer_use/browser_computer.py +0 -0
  25. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/config.py +0 -0
  26. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/context_budget.py +0 -0
  27. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/context_warning.py +0 -0
  28. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/credentials.py +0 -0
  29. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/hitl_session.py +0 -0
  30. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/hooks.py +0 -0
  31. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/intent_classifier.py +0 -0
  32. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/interactions.py +0 -0
  33. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/invoke.py +0 -0
  34. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/kairos_daemon.py +0 -0
  35. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/limits.py +0 -0
  36. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/live_audio_engine.py +0 -0
  37. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/logging_config.py +0 -0
  38. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/mcp_loader.py +0 -0
  39. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/memory/__init__.py +0 -0
  40. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/memory/embedding_memory_service.py +0 -0
  41. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/memory/file_memory_service.py +0 -0
  42. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/modality_tools.py +0 -0
  43. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/model_errors.py +0 -0
  44. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/model_routing.py +0 -0
  45. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/openapi_loader.py +0 -0
  46. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/paths.py +0 -0
  47. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/permissions.py +0 -0
  48. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/plugins/__init__.py +0 -0
  49. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  50. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  51. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/pricing.py +0 -0
  52. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/prompt_suggestions.py +0 -0
  53. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/query/__init__.py +0 -0
  54. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/query/config.py +0 -0
  55. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/query/deps.py +0 -0
  56. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/query/engine.py +0 -0
  57. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/query/stop_hooks.py +0 -0
  58. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/query/token_budget.py +0 -0
  59. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/query/transitions.py +0 -0
  60. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/refine.py +0 -0
  61. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/repl_commands.py +0 -0
  62. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/repl_slash.py +0 -0
  63. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/review_agent.py +0 -0
  64. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/session_runtime.py +0 -0
  65. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/session_store.py +0 -0
  66. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/slash_commands.py +0 -0
  67. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/thinking.py +0 -0
  68. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tool_prompt_manifest.py +0 -0
  69. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tool_registry.py +0 -0
  70. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/__init__.py +0 -0
  71. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/browser.py +0 -0
  72. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/notes.py +0 -0
  73. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/shell.py +0 -0
  74. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/shell_gate.py +0 -0
  75. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/subtask.py +0 -0
  76. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/think.py +0 -0
  77. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools/web.py +0 -0
  78. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tools_inspector.py +0 -0
  79. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/trust.py +0 -0
  80. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tui/input_handler.py +0 -0
  81. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tui/scrollback.py +0 -0
  82. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tui/spinner.py +0 -0
  83. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tui/welcome_banner.py +0 -0
  84. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/tui/welcome_rich.py +0 -0
  85. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/version.py +0 -0
  86. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/vertex.py +0 -0
  87. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/web/__init__.py +0 -0
  88. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/web/claude_sse_adapter.py +0 -0
  89. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/web/terminal_repl.py +0 -0
  90. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode/workspace_hints.py +0 -0
  91. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode.egg-info/SOURCES.txt +0 -0
  92. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode.egg-info/dependency_links.txt +0 -0
  93. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode.egg-info/entry_points.txt +0 -0
  94. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode.egg-info/requires.txt +0 -0
  95. {gemcode-0.3.48 → gemcode-0.3.50}/src/gemcode.egg-info/top_level.txt +0 -0
  96. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_agent_instruction.py +0 -0
  97. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_autocompact.py +0 -0
  98. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_capability_routing.py +0 -0
  99. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_claude_web_adapter_sse.py +0 -0
  100. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_cli_init.py +0 -0
  101. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_computer_use_permissions.py +0 -0
  102. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_context_budget.py +0 -0
  103. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_context_warning.py +0 -0
  104. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_credentials.py +0 -0
  105. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_interactive_permission_ask.py +0 -0
  106. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_kairos_scheduler.py +0 -0
  107. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_modality_tools.py +0 -0
  108. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_model_error_retry.py +0 -0
  109. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_model_errors.py +0 -0
  110. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_model_routing.py +0 -0
  111. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_paths.py +0 -0
  112. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_permissions.py +0 -0
  113. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_prompt_suggestions.py +0 -0
  114. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_repl_commands.py +0 -0
  115. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_repl_slash.py +0 -0
  116. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_slash_commands.py +0 -0
  117. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_thinking_config.py +0 -0
  118. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_token_budget.py +0 -0
  119. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_tool_context_circulation.py +0 -0
  120. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_tools.py +0 -0
  121. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_tools_inspector.py +0 -0
  122. {gemcode-0.3.48 → gemcode-0.3.50}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.48
3
+ Version: 0.3.50
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.48"
7
+ version = "0.3.50"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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
- Use your built-in thinking to read the user's intent before acting. Match the response style to what was actually asked:
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
- | Message type | Examples | Right action |
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
- **Never call `list_directory`, `read_project_notes`, or any tool in response to a greeting or a general knowledge question that needs no project context.**
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
- ### Engineering task workflow
414
- 1. **Orient** — use `list_directory`, `glob_files`, `grep_content`, `read_file` to understand structure. These tools need **no permission** and are instant.
415
- 2. **Plan** for complex tasks, call `todo_write` upfront to map out the work.
416
- 3. **Execute** — make the changes, run the checks, iterate.
417
- 4. **Verify** confirm the result is correct before reporting done.
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
- Never stop mid-task just because the first tool call succeeded. Keep going until the full task is complete or you hit a genuine blocker.
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
- - **Think first.** Read the message intent before touching any tool. The intent table above is your guide.
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
- - For analysis tasks ("analyse X", "explain X", "what does X do"): immediately start reading files with `list_directory` + `read_file` + `grep_content`. Produce concrete findings, not hypotheses.
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
- - Reading multiple files simultaneously
541
- - Grepping for different patterns at once
542
- - `list_directory` + `glob_files` in parallel ✓
543
- - Multiple `run_subtask` calls in one turn for parallel sub-agent exploration ✓
544
- Sequential: when step B needs step A's result.
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 through the user's intent before acting. "
765
- "For greetings or general questions, reply directly without calling tools. "
766
- "For engineering tasks, act fully and autonomously orient, plan, execute, verify. "
767
- "Use read-only tools before shell/write tools."
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
- Use this for:
44
- - Git operations: bash("git log --oneline -20")
45
- - Pipelines: bash("cat package.json | python3 -m json.tool")
46
- - Finding files: bash("find . -name '*.py' -newer setup.py | head -20")
47
- - Complex builds: bash("cd frontend && npm ci && npm run build")
48
- - Inspecting output: bash("ls -la | grep '.py'")
49
- - Running tests with flags: bash("pytest -x -q --tb=short 2>&1 | head -100")
50
-
51
- For long-running servers use background=True. cwd_subdir sets working
52
- directory relative to the project root.
53
-
54
- IMPORTANT: This runs real shell commands. Be precise and avoid destructive
55
- operations (rm -rf, force-push, etc.) without explicit user approval.
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. Large files are truncated.
24
+ Read a text file relative to the project root.
25
25
 
26
- Use start_line / end_line to read a specific line range (1-indexed, inclusive).
27
- This is efficient for large files e.g. read_file("app.py", start_line=100, end_line=200)
28
- reads only lines 100–200 without loading the whole file into context.
29
- Omit end_line to read from start_line to end of file (still subject to max_bytes).
26
+ IMPORTANT: ALWAYS use read_file before editing a file. Never propose changes
27
+ to code you haven't readthe 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
- """List files and directories under path (relative to project root)."""
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
- """Glob file paths relative to project root (e.g. 'src/**/*.py')."""
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
- Scans files matching path_glob (glob relative to project root). Binary files skipped.
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
- Options:
46
- - context_lines: show N lines before and after each match (like grep -C N). Very useful for
47
- understanding surrounding code. Example: grep_content("def foo", context_lines=3)
48
- - case_sensitive: set False for case-insensitive search (like grep -i)
49
- - max_matches: cap on returned matches (1–500, default 80)
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("TODO", "**/*.py") # find all TODOs in Python files
53
- grep_content("useState", "**/*.tsx", context_lines=2) # React hooks with context
54
- grep_content("error", "**/*.log", case_sensitive=False) # case-insensitive log search
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
- Maintain a task list for this session. Use for multi-step work: plan, then
25
- mark items completed as you go.
26
-
27
- Args:
28
- merge: If true, upsert by task id and keep prior order for existing ids;
29
- if false, replace the entire list.
30
- todos: Each item must have id (str), content (str), and status
31
- (pending | in_progress | completed | cancelled).
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.48
3
+ Version: 0.3.50
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -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