gemcode 0.3.77__py3-none-any.whl → 0.3.78__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gemcode/agent.py CHANGED
@@ -27,11 +27,49 @@ from gemcode.limits import make_before_model_limits_callback, make_before_model_
27
27
  from gemcode.thinking import build_thinking_config
28
28
  from gemcode.tools import build_function_tools
29
29
  from gemcode.tool_prompt_manifest import build_tool_manifest
30
- from gemcode.skills import build_skill_manifest_text
30
+ from gemcode.skills import (
31
+ build_skill_manifest_text,
32
+ expand_skill_text,
33
+ list_supporting_files,
34
+ load_skill,
35
+ )
31
36
  from gemcode.output_styles import build_output_style_section
32
37
  from gemcode.rules import build_rules_section
33
38
 
34
39
 
40
+ def _build_session_loaded_skills_section(cfg: GemCodeConfig) -> str:
41
+ """Full bodies for GemSkills the user loaded with /gemskill (session-scoped)."""
42
+ names = list(getattr(cfg, "session_loaded_skill_names", None) or [])
43
+ if not names:
44
+ return ""
45
+ sid = getattr(cfg, "session_skill_expand_session_id", None) or ""
46
+ chunks: list[str] = []
47
+ seen: set[str] = set()
48
+ for raw in names:
49
+ sk_name = (raw or "").strip().lower()
50
+ if not sk_name or sk_name in seen:
51
+ continue
52
+ seen.add(sk_name)
53
+ s = load_skill(cfg.project_root, sk_name)
54
+ if s is None:
55
+ continue
56
+ expanded = expand_skill_text(s, arguments="", session_id=sid)
57
+ files = list_supporting_files(s)
58
+ head = f"### GemSkill: `/{s.meta.name}` (loaded for this session)\n\n"
59
+ chunk = head + expanded
60
+ if files:
61
+ chunk += f"\n\nSupporting files: {', '.join(files)}"
62
+ chunks.append(chunk)
63
+ if not chunks:
64
+ return ""
65
+ return (
66
+ "## Loaded GemSkills (this session)\n"
67
+ "The user explicitly loaded these skills with `/gemskill`. Follow their workflows "
68
+ "when the task matches their purpose; do not force them on unrelated requests.\n\n"
69
+ + "\n\n---\n\n".join(chunks)
70
+ )
71
+
72
+
35
73
  def build_global_instruction() -> str:
36
74
  """Global instruction applied to the entire agent tree (via ADK plugin)."""
37
75
  return (
@@ -888,6 +926,9 @@ You have two tools to persist project insights across sessions (auto-memory styl
888
926
  skill_manifest = build_skill_manifest_text(cfg.project_root)
889
927
  if skill_manifest:
890
928
  base = f"{base}\n\n{skill_manifest}"
929
+ loaded_skills = _build_session_loaded_skills_section(cfg)
930
+ if loaded_skills:
931
+ base = f"{base}\n\n{loaded_skills}"
891
932
  extra = _load_gemini_md(cfg.project_root)
892
933
  if extra.strip():
893
934
  return f"{base}\n\n## Project instructions (GEMINI.md)\n{extra}"
gemcode/autotune.py CHANGED
@@ -3,8 +3,9 @@ from __future__ import annotations
3
3
  import subprocess
4
4
  import time
5
5
  from pathlib import Path
6
- from typing import Any
6
+ from typing import Any, Iterable
7
7
 
8
+ from gemcode.config import GemCodeConfig
8
9
  from gemcode.evals.harness import run_eval_suite, write_eval_record
9
10
 
10
11
 
@@ -47,11 +48,24 @@ def init_autotune(*, project_root: Path, tag: str) -> dict[str, Any]:
47
48
  return {"status": "created", "branch": branch}
48
49
 
49
50
 
50
- def run_autotune_eval(*, project_root: Path, include_llm: bool, model: str | None = None) -> dict[str, Any]:
51
+ def run_autotune_eval(
52
+ *,
53
+ project_root: Path,
54
+ include_llm: bool,
55
+ model: str | None = None,
56
+ session_cfg: GemCodeConfig | None = None,
57
+ extra_tools: Iterable[Any] | None = None,
58
+ ) -> dict[str, Any]:
51
59
  """
52
60
  Run eval suite and persist last result to .gemcode/evals/last_eval.json.
53
61
  """
54
- res = run_eval_suite(project_root=project_root, include_llm=include_llm, model=model)
62
+ res = run_eval_suite(
63
+ project_root=project_root,
64
+ include_llm=include_llm,
65
+ model=model,
66
+ session_cfg=session_cfg,
67
+ extra_tools=extra_tools,
68
+ )
55
69
  meta = {
56
70
  "ts": time.time(),
57
71
  "git_sha": _git_head_sha(project_root),
gemcode/cli.py CHANGED
@@ -245,6 +245,13 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
245
245
  file=sys.stderr,
246
246
  )
247
247
 
248
+ try:
249
+ from gemcode.repl_commands import install_readline_slash_completion
250
+
251
+ install_readline_slash_completion()
252
+ except Exception:
253
+ pass
254
+
248
255
  print(
249
256
  "GemCode CLI is running. Type your prompt and press Enter. (Ctrl+D to exit)",
250
257
  file=sys.stderr,
@@ -261,6 +268,7 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
261
268
  if prompt_text in (":q", "quit", "exit", "/exit"):
262
269
  break
263
270
 
271
+ cfg.session_skill_expand_session_id = session_id
264
272
  slash = await process_repl_slash(
265
273
  cfg=cfg,
266
274
  runner=runner,
@@ -273,7 +281,16 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
273
281
  break
274
282
  if slash.new_session_id is not None:
275
283
  session_id = slash.new_session_id
284
+ cfg.session_skill_expand_session_id = session_id
276
285
  if slash.skip_model_turn:
286
+ if slash.force_rebuild_runner:
287
+ try:
288
+ _c = runner.close()
289
+ if asyncio.iscoroutine(_c):
290
+ await _c
291
+ except Exception:
292
+ pass
293
+ runner = create_runner(cfg, extra_tools=None)
277
294
  continue
278
295
  prompt_text = slash.model_prompt or prompt_text
279
296
 
gemcode/config.py CHANGED
@@ -212,6 +212,12 @@ class GemCodeConfig:
212
212
  default_factory=lambda: os.environ.get("GEMCODE_OUTPUT_STYLE") or None
213
213
  )
214
214
 
215
+ # GemSkills explicitly loaded via /gemskill — full bodies injected into the
216
+ # system instruction until cleared or the session is reset/resumed.
217
+ session_loaded_skill_names: list[str] = field(default_factory=list)
218
+ # Substitutes ${GEMCODE_SESSION_ID} when expanding loaded skills for prompts.
219
+ session_skill_expand_session_id: str | None = None
220
+
215
221
  # Modality toggles (tool injection + routing).
216
222
  enable_deep_research: bool = field(
217
223
  default_factory=lambda: _truthy_env("GEMCODE_ENABLE_DEEP_RESEARCH", default=False)
gemcode/evals/harness.py CHANGED
@@ -3,14 +3,17 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import json
5
5
  import os
6
+ import subprocess
7
+ import sys
6
8
  import time
7
- from dataclasses import dataclass
9
+ from dataclasses import dataclass, replace
8
10
  from pathlib import Path
9
- from typing import Any, Callable
11
+ from typing import Any, Iterable
10
12
 
11
13
  from gemcode.config import GemCodeConfig, load_cli_environment
12
14
  from gemcode.invoke import run_turn
13
15
  from gemcode.session_runtime import create_runner
16
+ from gemcode.tools_inspector import inspect_tools, smoke_tools
14
17
 
15
18
 
16
19
  @dataclass
@@ -21,11 +24,25 @@ class EvalResult:
21
24
  details: str = ""
22
25
 
23
26
 
24
- def _run_cmd(cmd: str, *, cwd: Path) -> tuple[int, str]:
25
- import subprocess
26
- p = subprocess.run(cmd, cwd=str(cwd), shell=True, capture_output=True, text=True)
27
- out = (p.stdout or "") + (p.stderr or "")
28
- return int(p.returncode), out
27
+ def _discover_pytest_cwd(project_root: Path) -> tuple[Path, dict[str, str] | None] | None:
28
+ """
29
+ Return (cwd, env) for running pytest, or None if no tests tree found.
30
+
31
+ ``env`` is ``None`` to inherit the process environment; otherwise a full env dict.
32
+
33
+ Supports:
34
+ - Monorepo layout: <root>/gemcode/tests → cwd gemcode, PYTHONPATH=src
35
+ - Single-package layout: <root>/tests → cwd root
36
+ """
37
+ if (project_root / "tests").is_dir():
38
+ return project_root, None
39
+ gc = project_root / "gemcode"
40
+ if (gc / "tests").is_dir():
41
+ env = os.environ.copy()
42
+ prev = env.get("PYTHONPATH", "")
43
+ env["PYTHONPATH"] = "src" + (os.pathsep + prev if prev else "")
44
+ return gc, env
45
+ return None
29
46
 
30
47
 
31
48
  def _events_to_text(events: list) -> str:
@@ -72,28 +89,65 @@ def run_eval_suite(
72
89
  project_root: Path,
73
90
  include_llm: bool,
74
91
  model: str | None = None,
92
+ session_cfg: GemCodeConfig | None = None,
93
+ extra_tools: Iterable[Any] | None = None,
75
94
  ) -> dict[str, Any]:
76
95
  """
77
96
  Fixed evaluation harness (AutoResearch-style): deterministic gates + optional LLM golden prompts.
97
+
98
+ When ``session_cfg`` is set (e.g. from the REPL), tool smoke uses that config so flags match the live session.
78
99
  """
79
100
  t0 = time.time()
80
101
  load_cli_environment()
81
- cfg = GemCodeConfig(project_root=project_root)
82
- if model:
83
- cfg.model = model
84
- cfg.model_overridden = True
102
+ root = session_cfg.project_root if session_cfg is not None else project_root.resolve()
103
+ if session_cfg is not None:
104
+ cfg = replace(session_cfg, model=model, model_overridden=True) if model else session_cfg
105
+ else:
106
+ cfg = GemCodeConfig(project_root=project_root.resolve())
107
+ if model:
108
+ cfg.model = model
109
+ cfg.model_overridden = True
85
110
 
86
111
  results: list[EvalResult] = []
87
112
 
88
- # Gate 1: tool schema smoke
89
- rc, out = _run_cmd("PYTHONPATH=src python3 -m gemcode tools smoke", cwd=project_root / "gemcode")
90
- results.append(EvalResult(name="tools_smoke", ok=(rc == 0), score=1.0 if rc == 0 else 0.0, details=out[-800:]))
91
-
92
- # Gate 2: pytest if present
93
- tests_dir = project_root / "gemcode" / "tests"
94
- if tests_dir.is_dir():
95
- rc2, out2 = _run_cmd("PYTHONPATH=src python3 -m pytest -q", cwd=project_root / "gemcode")
96
- results.append(EvalResult(name="pytest", ok=(rc2 == 0), score=1.0 if rc2 == 0 else 0.0, details=out2[-1200:]))
113
+ # Gate 1: tool declaration smoke (in-process; matches REPL config when session_cfg is passed)
114
+ inspections = inspect_tools(cfg, extra_tools=extra_tools)
115
+ failures = smoke_tools(inspections)
116
+ ok_smoke = len(failures) == 0
117
+ smoke_details = ""
118
+ if failures:
119
+ smoke_details = "\n".join(
120
+ f"{f.name}: {f.declaration_error}" for f in failures[:40]
121
+ )
122
+ results.append(
123
+ EvalResult(
124
+ name="tools_smoke",
125
+ ok=ok_smoke,
126
+ score=1.0 if ok_smoke else 0.0,
127
+ details=smoke_details[-1200:],
128
+ )
129
+ )
130
+
131
+ # Gate 2: pytest if a tests/ tree exists under root or root/gemcode
132
+ pytest_target = _discover_pytest_cwd(root)
133
+ if pytest_target is not None:
134
+ cwd, env = pytest_target
135
+ p = subprocess.run(
136
+ [sys.executable, "-m", "pytest", "-q"],
137
+ cwd=str(cwd),
138
+ env=env,
139
+ capture_output=True,
140
+ text=True,
141
+ )
142
+ out2 = (p.stdout or "") + (p.stderr or "")
143
+ results.append(
144
+ EvalResult(
145
+ name="pytest",
146
+ ok=(p.returncode == 0),
147
+ score=1.0 if p.returncode == 0 else 0.0,
148
+ details=out2[-1200:],
149
+ )
150
+ )
97
151
 
98
152
  if include_llm:
99
153
  goldens = [
gemcode/repl_commands.py CHANGED
@@ -196,6 +196,123 @@ def format_tools_lines(
196
196
  return lines
197
197
 
198
198
 
199
+ # ---------------------------------------------------------------------------
200
+ # Slash command registry for TUI (prompt_toolkit) + plain REPL (readline Tab).
201
+ # One canonical name per feature where possible; a few shortcuts (e.g.
202
+ # ``gemskill``) stay as their own row so Tab/TUI shows them. Other aliases
203
+ # still work in ``process_repl_slash`` but are omitted here (see descriptions).
204
+ # ---------------------------------------------------------------------------
205
+ SLASH_COMMANDS: list[tuple[str, str]] = [
206
+ ("add-dir", "Extra read/search roots · /add_dir works too"),
207
+ ("append", "Iterate a file · /append gemskill <name> <request>"),
208
+ ("audit", "Tail audit.log · /logs same"),
209
+ ("autotune", "Branch + eval ledger · /autotune init <tag> · /autotune eval"),
210
+ ("batch", "Built-in batch GemSkill (large parallel changes)"),
211
+ ("budget", "Per-turn token budget · /token-budget same"),
212
+ ("caps", "Capabilities · /capabilities /capability same"),
213
+ ("clear", "Fresh session · same as /session new"),
214
+ ("code", "Toggle ADK BuiltInCodeExecutor (sandboxed Python)"),
215
+ ("compact", "Context compaction / summarization"),
216
+ ("computer", "Browser automation · /browser same"),
217
+ ("config", "Dump active configuration"),
218
+ ("context", "Context pressure + token breakdown"),
219
+ ("cost", "Session token usage + estimated cost"),
220
+ ("create", "New GemSkill file · /create gemskill <name> [description]"),
221
+ ("gemskill", "Load skill into session prompt · /gemskill <name> · list · clear"),
222
+ ("curated", "Curated memory snapshot · /memory-files /memoryfiles same"),
223
+ ("diff", "Git diff or checkpoint diff"),
224
+ ("doctor", "Environment sanity check"),
225
+ ("embeddings", "Semantic file search · /embed same"),
226
+ ("eval", "Eval gates (tools + pytest) · /eval llm optional"),
227
+ ("exit", "Leave the REPL · /quit same"),
228
+ ("help", "Short help · /? same"),
229
+ ("hooks", "Post-turn hook configuration"),
230
+ ("init", "Generate GEMINI.md project instructions"),
231
+ ("kaira", "Background job scheduler — how to run gemcode kaira"),
232
+ ("limits", "Execution limits (calls, context, …)"),
233
+ ("live-audio", "How to run gemcode live-audio · /liveaudio same"),
234
+ ("login", "How to run gemcode login (API key)"),
235
+ ("maps", "Maps grounding · /maps on|off · /map same"),
236
+ ("memory", "Persistent memory · /memory on|off"),
237
+ ("mode", "Model mode: fast|balanced|quality|auto"),
238
+ ("model", "Model info / override · /models same"),
239
+ ("notes", ".gemcode/notes.md · /notes clear · /notes edit"),
240
+ ("permissions", "Permission + HITL · /perm /permission same"),
241
+ ("plan", "Plan-before-act mode"),
242
+ ("research", "Deep research tools · /research on|off"),
243
+ ("review", "Parallel code review"),
244
+ ("rewind", "Checkpoints · /checkpoint same"),
245
+ ("rules", "Rule files from .gemcode/rules/"),
246
+ ("session", "Session id / list / resume / new"),
247
+ ("skill", "Load or show a GemSkill"),
248
+ ("skills", "List GemSkills"),
249
+ ("status", "Model, capabilities, thinking, limits"),
250
+ ("style", "Output styles · /style <name>|off"),
251
+ ("thinking", "Thinking verbose/brief/off, budget, level"),
252
+ ("tools", "Tool inventory · /tools smoke"),
253
+ ("trust", "Workspace trust · /trust on|off"),
254
+ ("version", "GemCode version"),
255
+ ]
256
+
257
+
258
+ def install_readline_slash_completion() -> bool:
259
+ """
260
+ Enable Tab completion for slash commands in the plain REPL (``input("> ")``).
261
+
262
+ Returns False if readline is unavailable or stdin is not a TTY.
263
+ """
264
+ try:
265
+ import readline
266
+ except ImportError:
267
+ return False
268
+ if not hasattr(sys.stdin, "isatty") or not sys.stdin.isatty():
269
+ return False
270
+
271
+ # Treat ``/foo`` as one word so Tab completes after ``/``.
272
+ try:
273
+ delims = readline.get_completer_delims()
274
+ if "/" in delims:
275
+ readline.set_completer_delims(delims.replace("/", ""))
276
+ except Exception:
277
+ pass
278
+
279
+ ordered = [name for name, _ in SLASH_COMMANDS]
280
+ _matches: list[str] = []
281
+
282
+ def completer(text: str, state: int) -> str | None:
283
+ nonlocal _matches
284
+ if state == 0:
285
+ _matches = []
286
+ if not text.startswith("/"):
287
+ return None
288
+ # Only complete the first token (/command …); skip when typing args.
289
+ if " " in text[1:]:
290
+ return None
291
+ frag = text[1:].lower()
292
+ for n in ordered:
293
+ if not frag or n.startswith(frag) or frag in n:
294
+ _matches.append(f"/{n} ")
295
+ if not _matches:
296
+ return None
297
+ try:
298
+ return _matches[state]
299
+ except IndexError:
300
+ return None
301
+
302
+ readline.set_completer(completer)
303
+ # GNU readline: double-Tab lists matches. libedit (macOS) may ignore unknown "set" directives.
304
+ for spec in (
305
+ "set show-all-if-ambiguous on",
306
+ "set completion-ignore-case on",
307
+ "tab: complete",
308
+ ):
309
+ try:
310
+ readline.parse_and_bind(spec)
311
+ except Exception:
312
+ pass
313
+ return True
314
+
315
+
199
316
  def slash_help_lines() -> list[str]:
200
317
  return [
201
318
  "Slash commands:",
@@ -203,13 +320,18 @@ def slash_help_lines() -> list[str]:
203
320
  " (CLI) gemcode login Save or change API key (~/.gemcode/credentials.json)",
204
321
  "",
205
322
  " Project setup:",
323
+ " /trust Show workspace trust status (file/shell tools)",
324
+ " /trust on|off Trust or revoke trust for this project root (~/.gemcode/trust.json)",
206
325
  " /init Analyze project structure and generate GEMINI.md",
207
326
  " /init force Regenerate GEMINI.md even if it already exists",
208
327
  " /cost Show token usage and estimated cost for this session",
209
328
  " /notes Show agent auto-notes (.gemcode/notes.md)",
210
329
  " /notes clear Delete all notes",
211
330
  " /notes edit Open notes in $EDITOR",
212
- " /create gemskill <name> [description] Create a GemSkill scaffold under .gemcode/skills/",
331
+ " /create gemskill <name> [description] Create a new GemSkill (SKILL.md scaffold)",
332
+ " /gemskill <name> Load an existing GemSkill into this session (system prompt)",
333
+ " /gemskill list|clear List skills or unload all session-loaded skills",
334
+ " /append gemskill <name> <request> Ask the agent to edit that skill file",
213
335
  " /style List available output styles",
214
336
  " /style <name>|off Activate an output style for this session",
215
337
  " /rules Show loaded rule files (from .gemcode/rules/)",
@@ -237,6 +359,13 @@ def slash_help_lines() -> list[str]:
237
359
  " /context Show context pressure + last prompt tokens",
238
360
  " /audit [N] Tail of .gemcode/audit.log (default 40 lines)",
239
361
  " /tools List tool inventory for this config",
362
+ " /tools smoke Declaration compile check only (failures listed)",
363
+ " /eval [llm] Run tools_smoke (+ pytest if tests/ exist); optional LLM goldens",
364
+ " /autotune init <tag> Git branch autotune/<tag> for experiment tracking",
365
+ " /autotune eval [llm] Eval + append .gemcode/evals/autotune_ledger.jsonl",
366
+ " /curated Show GEMCODE_MEMORY.md / GEMCODE_USER.md snapshot",
367
+ " /login How to run gemcode login (API key outside REPL)",
368
+ " /live-audio How to run gemcode live-audio (mic → Gemini Live)",
240
369
  " /doctor Environment sanity check",
241
370
  " /version Print GemCode version hint",
242
371
  " /exit Exit the REPL",
@@ -254,6 +383,7 @@ def slash_help_lines() -> list[str]:
254
383
  " /computer url Show current browser URL",
255
384
  " /research Show deep-research status",
256
385
  " /research on|off Enable/disable Google Search + URL Context tools",
386
+ " /maps on|off Enable/disable Maps grounding (runner rebuild)",
257
387
  " /embeddings on|off Enable/disable semantic file search (Embeddings API)",
258
388
  " /caps View all capability flags",
259
389
  " /caps <research|embeddings|all|reset> Bulk capability control",
gemcode/repl_slash.py CHANGED
@@ -28,10 +28,12 @@ from gemcode.repl_commands import (
28
28
  format_tools_lines,
29
29
  slash_help_lines,
30
30
  )
31
+ from gemcode.curated_memory import load_snapshot as _curated_load_snapshot
31
32
  from gemcode.slash_commands import parse_slash_command
32
33
  from gemcode.skills import discover_skill_metas, expand_skill_text, list_supporting_files, load_skill
33
34
  from gemcode.output_styles import discover_output_styles, load_output_style
34
35
  from gemcode.rules import load_rules as _load_rules
36
+ from gemcode.trust import is_trusted_root, trust_json_path, trust_root
35
37
 
36
38
 
37
39
  @dataclass
@@ -45,6 +47,12 @@ class ReplSlashResult:
45
47
  force_rebuild_runner: bool = False # True when agent config changed (thinking, etc.)
46
48
 
47
49
 
50
+ def _clear_session_loaded_skills(cfg: GemCodeConfig) -> None:
51
+ raw = getattr(cfg, "session_loaded_skill_names", None)
52
+ if isinstance(raw, list):
53
+ raw.clear()
54
+
55
+
48
56
  def _parse_tail_n(args: str, *, default: int = 40) -> int:
49
57
  parts = (args or "").strip().split()
50
58
  if not parts:
@@ -116,6 +124,61 @@ async def process_repl_slash(
116
124
  )
117
125
  return ReplSlashResult(model_prompt=prompt)
118
126
 
127
+ # ── /gemskill (load full skill into session system prompt) ────────────────
128
+ if name == "gemskill":
129
+ args_gs = (sc.args or "").strip()
130
+ if not args_gs or args_gs.lower() in ("help", "?"):
131
+ out("Usage:")
132
+ out(" /gemskill <name> Load an existing GemSkill into this session (system prompt).")
133
+ out(" /gemskill list List skills you can load")
134
+ out(" /gemskill clear Unload all session-loaded skills")
135
+ out()
136
+ out("Create a new skill: /create gemskill <name> [description]")
137
+ out("Edit an existing one: /append gemskill <name> <what to change>")
138
+ out()
139
+ return ReplSlashResult(skip_model_turn=True)
140
+ al = args_gs.lower()
141
+ if al in ("list", "ls", "show"):
142
+ metas_gs = discover_skill_metas(cfg.project_root)
143
+ if not metas_gs:
144
+ out("No GemSkills found.")
145
+ out("Create one: /create gemskill <name> [description]")
146
+ out()
147
+ return ReplSlashResult(skip_model_turn=True)
148
+ out("GemSkills (load with /gemskill <name>):")
149
+ for k in sorted(metas_gs.keys()):
150
+ m, _ = metas_gs[k]
151
+ inv = "manual-only" if m.disable_model_invocation else "auto-eligible"
152
+ out(f" {m.name} ({inv}) — {m.description}")
153
+ out()
154
+ return ReplSlashResult(skip_model_turn=True)
155
+ if al == "clear":
156
+ _clear_session_loaded_skills(cfg)
157
+ cfg.session_skill_expand_session_id = session_id
158
+ out("Session-loaded GemSkills cleared.")
159
+ out("Runner will rebuild on the next turn.")
160
+ out()
161
+ return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
162
+ sk_part = args_gs.split()[0].strip().lower()
163
+ s_gs = load_skill(cfg.project_root, sk_part)
164
+ if s_gs is None:
165
+ out(f"Unknown skill: {sk_part}")
166
+ out("Tip: /gemskill list · Create: /create gemskill <name>")
167
+ out()
168
+ return ReplSlashResult(skip_model_turn=True)
169
+ loaded = cfg.session_loaded_skill_names
170
+ if sk_part in loaded:
171
+ out(f"GemSkill `/{sk_part}` is already loaded for this session.")
172
+ out()
173
+ return ReplSlashResult(skip_model_turn=True)
174
+ loaded.append(sk_part)
175
+ cfg.session_skill_expand_session_id = session_id
176
+ out(f"Loaded GemSkill into session: /{sk_part}")
177
+ out("Full skill body is now in the system prompt until /gemskill clear or a new session.")
178
+ out("Runner will rebuild on the next turn.")
179
+ out()
180
+ return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
181
+
119
182
  # ── /batch (built-in GemSkill) ─────────────────────────────────────────────
120
183
  if name == "batch":
121
184
  goal = (sc.args or "").strip()
@@ -230,10 +293,42 @@ async def process_repl_slash(
230
293
 
231
294
  out(f"Created GemSkill: /{skill_name}")
232
295
  out(f"Path: {skill_md}")
233
- out("Try: /skills then /" + skill_name + " <args>")
296
+ out("Try: /gemskill " + skill_name + " or /" + skill_name + " <args> for a one-shot turn")
234
297
  out()
235
298
  return ReplSlashResult(skip_model_turn=True)
236
299
 
300
+ # ── /append gemskill (iterate existing SKILL.md) ───────────────────────────
301
+ if name == "append":
302
+ raw_ap = (sc.args or "").strip()
303
+ parts_ap = raw_ap.split(None, 2)
304
+ if len(parts_ap) < 3 or parts_ap[0].lower() != "gemskill":
305
+ out("Usage: /append gemskill <name> <what to add or change>")
306
+ out("Example: /append gemskill review-code Add a checklist for API security")
307
+ out()
308
+ return ReplSlashResult(skip_model_turn=True)
309
+ sk_ap = parts_ap[1].strip().lower()
310
+ instruction_ap = parts_ap[2].strip()
311
+ s_ap = load_skill(cfg.project_root, sk_ap)
312
+ if s_ap is None:
313
+ out(f"Unknown skill: {sk_ap}")
314
+ out("Tip: /gemskill list")
315
+ out()
316
+ return ReplSlashResult(skip_model_turn=True)
317
+ files_ap = list_supporting_files(s_ap)
318
+ prompt_ap = (
319
+ f"The user wants to **iterate** on an existing GemSkill ({sk_ap!r}).\n\n"
320
+ f"## Skill file (primary edit target)\n`{s_ap.skill_md}`\n\n"
321
+ f"## User request\n{instruction_ap}\n\n"
322
+ "## Instructions\n"
323
+ "1. Read the full SKILL.md (and supporting files only if needed).\n"
324
+ "2. Apply the user's request: clarify steps, add sections, improve examples, fix mistakes.\n"
325
+ "3. Preserve valid YAML frontmatter (`name`, `description`, etc.) unless the user asked to rename.\n"
326
+ "4. Save changes with `search_replace` or `write_file`.\n"
327
+ "5. Summarize what you changed.\n\n"
328
+ + (f"## Supporting files (optional)\n{', '.join(files_ap)}\n\n" if files_ap else "")
329
+ )
330
+ return ReplSlashResult(model_prompt=prompt_ap)
331
+
237
332
  # ── /style ────────────────────────────────────────────────────────────────
238
333
  if name == "style":
239
334
  args = (sc.args or "").strip()
@@ -293,6 +388,44 @@ async def process_repl_slash(
293
388
  out()
294
389
  return ReplSlashResult(skip_model_turn=True)
295
390
 
391
+ # ── /trust (workspace permission for tools) ─────────────────────────────────
392
+ if name == "trust":
393
+ args_s = (sc.args or "").strip().lower()
394
+ root = cfg.project_root.resolve()
395
+ tpath = trust_json_path()
396
+
397
+ if args_s in ("", "status", "show"):
398
+ if is_trusted_root(root):
399
+ out(f"Workspace is trusted:\n {root}")
400
+ else:
401
+ out(f"Workspace is NOT trusted:\n {root}")
402
+ out("File, shell, and git tools require trust. Use: /trust on")
403
+ out(f"Trust database: {tpath}")
404
+ out()
405
+ return ReplSlashResult(skip_model_turn=True)
406
+
407
+ if args_s in ("on", "yes", "y", "1", "true", "enable"):
408
+ trust_root(root, trusted=True)
409
+ out(f"Trusted:\n {root}")
410
+ out(f"Saved to {tpath}")
411
+ out()
412
+ return ReplSlashResult(skip_model_turn=True)
413
+
414
+ if args_s in ("off", "no", "n", "0", "false", "disable", "revoke"):
415
+ trust_root(root, trusted=False)
416
+ out(f"Removed trust for:\n {root}")
417
+ out("Tools will refuse until you run /trust on (or approve on next CLI start).")
418
+ out()
419
+ return ReplSlashResult(skip_model_turn=True)
420
+
421
+ out("Usage:")
422
+ out(" /trust Show whether this project root is trusted")
423
+ out(" /trust on Trust this workspace (required for file/shell/git tools)")
424
+ out(" /trust off Stop trusting this workspace")
425
+ out(f" (stored under {tpath.parent}/)")
426
+ out()
427
+ return ReplSlashResult(skip_model_turn=True)
428
+
296
429
  # ── /add-dir (safe multi-root access) ──────────────────────────────────────
297
430
  if name in ("add-dir", "add_dir", "adddir"):
298
431
  import os
@@ -539,6 +672,141 @@ async def process_repl_slash(
539
672
  out()
540
673
  return ReplSlashResult(skip_model_turn=True)
541
674
 
675
+ # ── /eval ─────────────────────────────────────────────────────────────────
676
+ if name == "eval":
677
+ raw_args = (sc.args or "").strip().lower()
678
+ include_llm = "llm" in raw_args or "--llm" in raw_args
679
+ from gemcode.evals.harness import run_eval_suite, write_eval_record
680
+
681
+ try:
682
+ res = run_eval_suite(
683
+ project_root=cfg.project_root,
684
+ include_llm=include_llm,
685
+ model=None,
686
+ session_cfg=cfg,
687
+ extra_tools=extra_tools,
688
+ )
689
+ except Exception as e:
690
+ out(f"eval failed: {type(e).__name__}: {e}")
691
+ out()
692
+ return ReplSlashResult(skip_model_turn=True)
693
+ try:
694
+ rec_path = write_eval_record(cfg.project_root, res)
695
+ out(f"eval: ok={res.get('ok')} score={float(res.get('score', 0)):.2f} elapsed_s={float(res.get('elapsed_s', 0)):.1f} → {rec_path}")
696
+ except OSError:
697
+ out(f"eval: ok={res.get('ok')} score={float(res.get('score', 0)):.2f} elapsed_s={float(res.get('elapsed_s', 0)):.1f}")
698
+ for row in res.get("results") or []:
699
+ nm = row.get("name", "?")
700
+ ok = row.get("ok", False)
701
+ out(f" {nm}: {'ok' if ok else 'FAIL'}")
702
+ if not ok and row.get("details"):
703
+ det = str(row["details"]).strip()
704
+ if det:
705
+ snippet = det[-600:] if len(det) > 600 else det
706
+ out(f" {snippet}")
707
+ out()
708
+ return ReplSlashResult(skip_model_turn=True)
709
+
710
+ # ── /autotune ───────────────────────────────────────────────────────────────
711
+ if name == "autotune":
712
+ parts = (sc.args or "").strip().split()
713
+ if not parts:
714
+ out("Usage:")
715
+ out(" /autotune init <tag> Create git branch autotune/<tag> (requires git repo)")
716
+ out(" /autotune eval [llm] Run eval suite and append .gemcode/evals/autotune_ledger.jsonl")
717
+ out()
718
+ return ReplSlashResult(skip_model_turn=True)
719
+ sub = parts[0].lower()
720
+ if sub == "init":
721
+ if len(parts) < 2:
722
+ out("Usage: /autotune init <tag>")
723
+ out()
724
+ return ReplSlashResult(skip_model_turn=True)
725
+ tag = parts[1].strip()
726
+ from gemcode.autotune import init_autotune
727
+
728
+ r = init_autotune(project_root=cfg.project_root, tag=tag)
729
+ if r.get("error"):
730
+ out(f"autotune init: {r.get('error')}")
731
+ if r.get("output"):
732
+ out(str(r["output"])[-800:])
733
+ else:
734
+ out(f"autotune init: {r.get('status')} branch={r.get('branch')}")
735
+ out()
736
+ return ReplSlashResult(skip_model_turn=True)
737
+ if sub == "eval":
738
+ include_llm = any(p.lower() in ("llm", "--llm") for p in parts[1:])
739
+ from gemcode.autotune import run_autotune_eval
740
+
741
+ try:
742
+ r = run_autotune_eval(
743
+ project_root=cfg.project_root,
744
+ include_llm=include_llm,
745
+ model=None,
746
+ session_cfg=cfg,
747
+ extra_tools=extra_tools,
748
+ )
749
+ except Exception as e:
750
+ out(f"autotune eval failed: {type(e).__name__}: {e}")
751
+ out()
752
+ return ReplSlashResult(skip_model_turn=True)
753
+ out(f"autotune eval: ok={r.get('ok')} score={float(r.get('score', 0)):.2f}")
754
+ if r.get("record_path"):
755
+ out(f" record: {r['record_path']}")
756
+ if r.get("ledger_path"):
757
+ out(f" ledger: {r['ledger_path']}")
758
+ out()
759
+ return ReplSlashResult(skip_model_turn=True)
760
+ out(f"Unknown /autotune subcommand: {sub}")
761
+ out("Usage: /autotune init <tag> · /autotune eval [llm]")
762
+ out()
763
+ return ReplSlashResult(skip_model_turn=True)
764
+
765
+ # ── /curated (curated memory files) ────────────────────────────────────────
766
+ if name in ("curated", "memory-files", "memoryfiles"):
767
+ snap = _curated_load_snapshot(cfg.project_root, max_chars=8000)
768
+ out("Curated memory (injected when memory is on):")
769
+ out(f" project: {snap.get('memory_path')}")
770
+ out(f" user: {snap.get('user_path')}")
771
+ out(f" loaded: {snap.get('chars', 0)} chars exists={snap.get('exists')}")
772
+ out()
773
+ txt = (snap.get("text") or "").strip()
774
+ if txt:
775
+ out("--- snapshot ---")
776
+ out(txt)
777
+ out("--- end ---")
778
+ else:
779
+ out("(empty — create .gemcode/GEMCODE_MEMORY.md and GEMCODE_USER.md)")
780
+ out()
781
+ return ReplSlashResult(skip_model_turn=True)
782
+
783
+ # ── /login ─────────────────────────────────────────────────────────────────
784
+ if name == "login":
785
+ from gemcode.credentials import credentials_path
786
+
787
+ out("API keys are not stored inside the REPL. Use a separate terminal:")
788
+ out()
789
+ out(" gemcode login")
790
+ out()
791
+ out("Creates or updates your key at:")
792
+ out(f" {credentials_path()}")
793
+ out()
794
+ out("Get a key: https://aistudio.google.com/app/apikey")
795
+ out()
796
+ return ReplSlashResult(skip_model_turn=True)
797
+
798
+ # ── /live-audio ────────────────────────────────────────────────────────────
799
+ if name in ("live-audio", "liveaudio"):
800
+ out("Live audio (microphone → Gemini Live) runs as a dedicated CLI, not inside this REPL.")
801
+ out()
802
+ out("Example:")
803
+ out(f" gemcode live-audio -C {cfg.project_root}")
804
+ out()
805
+ out("Flags: --seconds N --rate 24000 --language en-US --model <id>")
806
+ out(" --yes --deep-research --embeddings --session <uuid>")
807
+ out()
808
+ return ReplSlashResult(skip_model_turn=True)
809
+
542
810
  if name in ("model", "models"):
543
811
  args = (sc.args or "").strip()
544
812
  if not args:
@@ -772,6 +1040,22 @@ async def process_repl_slash(
772
1040
  return ReplSlashResult(skip_model_turn=True)
773
1041
 
774
1042
  if name == "tools":
1043
+ args_t = (sc.args or "").strip().lower()
1044
+ if args_t in ("smoke", "decl", "declarations"):
1045
+ from gemcode.tools_inspector import inspect_tools, smoke_tools
1046
+
1047
+ inspections = inspect_tools(cfg, extra_tools=extra_tools)
1048
+ bad = smoke_tools(inspections)
1049
+ if not bad:
1050
+ out(f"tools smoke: OK ({len(inspections)} tools, declarations compile)")
1051
+ else:
1052
+ out(f"tools smoke: {len(bad)} failure(s) of {len(inspections)} tools")
1053
+ for i in bad[:60]:
1054
+ out(f" {i.name}: {i.declaration_error}")
1055
+ if len(bad) > 60:
1056
+ out(f" … ({len(bad) - 60} more)")
1057
+ out()
1058
+ return ReplSlashResult(skip_model_turn=True)
775
1059
  out("\n".join(format_tools_lines(cfg, extra_tools=extra_tools)))
776
1060
  out()
777
1061
  return ReplSlashResult(skip_model_turn=True)
@@ -787,6 +1071,8 @@ async def process_repl_slash(
787
1071
  out(f"model_mode: {cfg.model_mode}")
788
1072
  out(f"session_id: {session_id}")
789
1073
  out(f"project_root: {cfg.project_root}")
1074
+ _lg = getattr(cfg, "session_loaded_skill_names", None) or []
1075
+ out(f"loaded_skills: {', '.join(_lg) if _lg else '(none)'} (/gemskill)")
790
1076
  out()
791
1077
  out("Capabilities:")
792
1078
  out(f" deep_research: {'on ✓' if cfg.enable_deep_research else 'off'}")
@@ -891,10 +1177,15 @@ async def process_repl_slash(
891
1177
 
892
1178
  # /clear or /session new — start fresh session
893
1179
  if name == "clear" or args_lower in ("new", "reset"):
1180
+ _clear_session_loaded_skills(cfg)
894
1181
  new_id = str(uuid.uuid4())
895
1182
  out(f"new session_id: {new_id}")
896
1183
  out()
897
- return ReplSlashResult(skip_model_turn=True, new_session_id=new_id)
1184
+ return ReplSlashResult(
1185
+ skip_model_turn=True,
1186
+ new_session_id=new_id,
1187
+ force_rebuild_runner=True,
1188
+ )
898
1189
 
899
1190
  # /session list — show recent sessions
900
1191
  if args_lower in ("list", "ls", "history"):
@@ -940,9 +1231,14 @@ async def process_repl_slash(
940
1231
  out(f"Already in session {found[:8]}.")
941
1232
  out()
942
1233
  return ReplSlashResult(skip_model_turn=True)
1234
+ _clear_session_loaded_skills(cfg)
943
1235
  out(f"Resuming session {found[:8]}…")
944
1236
  out()
945
- return ReplSlashResult(skip_model_turn=True, new_session_id=found)
1237
+ return ReplSlashResult(
1238
+ skip_model_turn=True,
1239
+ new_session_id=found,
1240
+ force_rebuild_runner=True,
1241
+ )
946
1242
 
947
1243
  # Default: show current session info
948
1244
  from gemcode.session_store import get_session_name, touch_session
@@ -1327,6 +1623,33 @@ async def process_repl_slash(
1327
1623
  out()
1328
1624
  return ReplSlashResult(skip_model_turn=True)
1329
1625
 
1626
+ # ── /maps (Maps grounding, deep-research stack) ────────────────────────────
1627
+ if name in ("maps", "map"):
1628
+ args_s = (sc.args or "").strip().lower()
1629
+ if not args_s or args_s in ("status", "show"):
1630
+ status = "on ✓" if cfg.enable_maps_grounding else "off"
1631
+ out(f"maps_grounding: {status}")
1632
+ out()
1633
+ out("Commands: /maps on · /maps off")
1634
+ out("When on: Maps-backed grounding is available alongside deep-research tools (if enabled).")
1635
+ out()
1636
+ return ReplSlashResult(skip_model_turn=True)
1637
+ if args_s == "on":
1638
+ cfg.enable_maps_grounding = True
1639
+ out("maps_grounding: on")
1640
+ out(" Runner will rebuild on next turn.")
1641
+ out()
1642
+ return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
1643
+ if args_s == "off":
1644
+ cfg.enable_maps_grounding = False
1645
+ out("maps_grounding: off")
1646
+ out()
1647
+ return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
1648
+ out(f"Unknown /maps subcommand: '{args_s}'")
1649
+ out("Usage: /maps [on|off]")
1650
+ out()
1651
+ return ReplSlashResult(skip_model_turn=True)
1652
+
1330
1653
  # ── /mode ─────────────────────────────────────────────────────────────────
1331
1654
  if name == "mode":
1332
1655
  args_s = (sc.args or "").strip().lower()
gemcode/trust.py CHANGED
@@ -10,6 +10,11 @@ def _trust_file_path() -> Path:
10
10
  return base / "trust.json"
11
11
 
12
12
 
13
+ def trust_json_path() -> Path:
14
+ """Path to the trust database (respects GEMCODE_HOME)."""
15
+ return _trust_file_path()
16
+
17
+
13
18
  def load_trusted_roots() -> set[str]:
14
19
  """
15
20
  Returns a set of resolved absolute paths (as strings) that are trusted.
@@ -36,48 +36,7 @@ try:
36
36
  except ImportError:
37
37
  pass
38
38
 
39
- # ---------------------------------------------------------------------------
40
- # Slash command registry
41
- # (name without /, description shown in the autocomplete popup)
42
- # ---------------------------------------------------------------------------
43
- SLASH_COMMANDS: list[tuple[str, str]] = [
44
- ("help", "List all available commands"),
45
- ("init", "Analyze project and generate GEMINI.md project instructions"),
46
- ("cost", "Show session token usage and estimated USD cost breakdown"),
47
- ("notes", "View agent auto-generated project notes (.gemcode/notes.md)"),
48
- ("diff", "Show git diff (or checkpoint diff fallback)"),
49
- ("rewind", "Restore files to a previous checkpoint · alias: /checkpoint"),
50
- ("add-dir", "Add extra directory for read/search access · /add-dir list"),
51
- ("batch", "Parallel large-change orchestrator (built-in GemSkill)"),
52
- ("review", "Parallel code review: security + style + correctness simultaneously"),
53
- ("compact", "Compact conversation history to free context window"),
54
- ("clear", "Start a fresh session (clears history) · alias: /session new"),
55
- ("model", "View or switch model · /model use <id> · /model list"),
56
- ("mode", "Set model mode · /mode fast|balanced|quality|auto"),
57
- ("thinking", "Thinking config · /thinking verbose · /thinking brief · /thinking budget <N>"),
58
- ("status", "Show full session status: model, capabilities, thinking, limits"),
59
- ("context", "Show context window usage and token counts"),
60
- ("compact", "Summarise conversation history to reclaim context space"),
61
- ("research", "Toggle Google Search + URL Context · /research on|off"),
62
- ("embeddings", "Toggle semantic file search via Embeddings API · /embeddings on|off"),
63
- ("computer", "Browser automation (Playwright Chromium) · /computer on|off|url"),
64
- ("caps", "View/toggle all capabilities · /caps · /caps all · /caps reset"),
65
- ("memory", "Toggle persistent memory · /memory on|off"),
66
- ("budget", "Set per-turn token budget · /budget <N> · /budget off"),
67
- ("limits", "Show/set execution limits (max_llm_calls, context, etc.)"),
68
- ("kaira", "Background parallel job scheduler — how to run gemcode kaira"),
69
- ("code", "Toggle sandboxed Python code executor (ADK BuiltInCodeExecutor)"),
70
- ("plan", "Toggle plan mode — agent writes explicit plan before executing tools"),
71
- ("tools", "List all tools and their permission categories"),
72
- ("config", "Show full active configuration (all fields)"),
73
- ("permissions", "Show current permission mode (default / strict / yes)"),
74
- ("session", "Show current session ID · /session new to reset"),
75
- ("audit", "Show recent audit log · /audit [N lines]"),
76
- ("doctor", "Run diagnostics and validate the environment"),
77
- ("hooks", "Show post-turn hook configuration"),
78
- ("version", "Show installed GemCode version"),
79
- ("exit", "Exit GemCode"),
80
- ]
39
+ from gemcode.repl_commands import SLASH_COMMANDS
81
40
 
82
41
 
83
42
  # ---------------------------------------------------------------------------
@@ -136,6 +95,13 @@ class GemCodeInputHandler:
136
95
 
137
96
  if _PT_AVAILABLE and sys.stdin.isatty() and sys.stdout.isatty():
138
97
  self._build_session()
98
+ elif sys.stdin.isatty():
99
+ try:
100
+ from gemcode.repl_commands import install_readline_slash_completion
101
+
102
+ install_readline_slash_completion()
103
+ except Exception:
104
+ pass
139
105
 
140
106
  def _build_session(self) -> None:
141
107
  style = Style.from_dict(
gemcode/tui/scrollback.py CHANGED
@@ -530,6 +530,7 @@ async def run_gemcode_scrollback_tui(
530
530
  old_model = getattr(cfg, "model", "")
531
531
  old_model_overridden = bool(getattr(cfg, "model_overridden", False))
532
532
 
533
+ cfg.session_skill_expand_session_id = current_session_id
533
534
  slash = await process_repl_slash(
534
535
  cfg=cfg,
535
536
  runner=runner,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.77
3
+ Version: 0.3.78
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -197,32 +197,82 @@ This document is the **authoritative reference** for CLI behavior, configuration
197
197
 
198
198
  ## Table of contents
199
199
 
200
- 1. [Architecture](#architecture)
201
- 2. [Requirements and install](#requirements-and-install)
202
- 3. [First run](#first-run)
203
- 4. [CLI commands](#cli-commands)
204
- 5. [Main CLI flags](#main-cli-flags)
205
- 6. [The `.gemcode/` directory](#the-gemcode-directory)
206
- 7. [Project context: `GEMINI.md`](#project-context-geminimd)
207
- 8. [Function tools (catalog)](#function-tools-catalog)
208
- 9. [REPL: slash commands](#repl-slash-commands)
209
- 10. [GemSkills](#gemskills)
210
- 11. [Output styles and rules](#output-styles-and-rules)
211
- 12. [Checkpoints, diff, and rewind](#checkpoints-diff-and-rewind)
212
- 13. [Multi-root workspaces (`/add-dir`)](#multi-root-workspaces-add-dir)
213
- 14. [Model routing and thinking](#model-routing-and-thinking)
214
- 15. [Capabilities](#capabilities)
215
- 16. [Permissions and interactive approval](#permissions-and-interactive-approval)
216
- 17. [Hooks](#hooks)
217
- 18. [Token budget, context, and compaction](#token-budget-context-and-compaction)
218
- 19. [MCP](#mcp)
219
- 20. [IDE bridge: `gemcode ide --stdio`](#ide-bridge-gemcode-ide-stdio)
220
- 21. [Eval harness and autotune](#eval-harness-and-autotune)
221
- 22. [Kaira scheduler](#kaira-scheduler)
222
- 23. [Live audio](#live-audio)
223
- 24. [Related components](#related-components)
224
- 25. [Environment variables](#environment-variables)
225
- 26. [Development and release](#development-and-release)
200
+ 1. [What GemCode is (detailed)](#what-gemcode-is-detailed)
201
+ 2. [Architecture](#architecture)
202
+ 3. [Requirements and install](#requirements-and-install)
203
+ 4. [First run](#first-run)
204
+ 5. [CLI commands](#cli-commands)
205
+ 6. [Main CLI flags](#main-cli-flags)
206
+ 7. [The `.gemcode/` directory](#the-gemcode-directory)
207
+ 8. [Project context: `GEMINI.md`](#project-context-geminimd)
208
+ 9. [Function tools (catalog)](#function-tools-catalog)
209
+ 10. [REPL: slash commands](#repl-slash-commands)
210
+ 11. [GemSkills](#gemskills)
211
+ 12. [Curated memory](#curated-memory)
212
+ 13. [Workspace trust](#workspace-trust)
213
+ 14. [Output styles and rules](#output-styles-and-rules)
214
+ 15. [Checkpoints, diff, and rewind](#checkpoints-diff-and-rewind)
215
+ 16. [Multi-root workspaces (`/add-dir`)](#multi-root-workspaces-add-dir)
216
+ 17. [Model routing and thinking](#model-routing-and-thinking)
217
+ 18. [Capabilities](#capabilities)
218
+ 19. [Permissions and interactive approval](#permissions-and-interactive-approval)
219
+ 20. [Hooks](#hooks)
220
+ 21. [Token budget, context, and compaction](#token-budget-context-and-compaction)
221
+ 22. [MCP](#mcp)
222
+ 23. [IDE bridge: `gemcode ide --stdio`](#ide-bridge-gemcode-ide-stdio)
223
+ 24. [Eval harness and autotune](#eval-harness-and-autotune)
224
+ 25. [Kaira scheduler](#kaira-scheduler)
225
+ 26. [Live audio](#live-audio)
226
+ 27. [Related components](#related-components)
227
+ 28. [Environment variables](#environment-variables)
228
+ 29. [Development and release](#development-and-release)
229
+
230
+ ---
231
+
232
+ ## What GemCode is (detailed)
233
+
234
+ **GemCode** is a single Python package that exposes a **`gemcode`** CLI. Every invocation is anchored to a **project root** (`-C`, default current directory). From there it:
235
+
236
+ - Builds a **GemCodeConfig** (model, capabilities, permissions, limits, paths).
237
+ - Optionally loads **MCP** toolsets from `.gemcode/mcp.json`.
238
+ - Constructs an ADK **Runner** with a root **LlmAgent** whose **system instruction** aggregates: global behavior, `GEMINI.md`, capability sections, tool manifest, optional **output style** and **rules**, the **skills manifest** (metadata only), and any **session-loaded GemSkills** (full bodies after `/gemskill`).
239
+ - Persists conversation state in **SQLite** (`.gemcode/sessions.sqlite`) keyed by **`--session`** / REPL session id.
240
+
241
+ ### Modes of use
242
+
243
+ | Mode | How | Best for |
244
+ |------|-----|----------|
245
+ | **One-shot CLI** | `gemcode -C repo "prompt"` | Scripts, CI-style runs, quick questions. |
246
+ | **REPL** | `gemcode -C repo` (TTY, no prompt arg) | Long sessions, slash commands, exploration. |
247
+ | **TUI** | Same as REPL with `GEMCODE_TUI=1` (default when supported) | Scrollback, styled output, slash **completion menu**. |
248
+ | **IDE** | VS Code extension → `gemcode ide --stdio` | Editor-native Chat, diff apply, proposals. |
249
+ | **Kaira** | `gemcode kaira` | Background queue of independent agent jobs. |
250
+ | **Live audio** | `gemcode live-audio` | Voice → Gemini Live (separate from file-editing REPL). |
251
+
252
+ ### “Memory” in GemCode (three different things)
253
+
254
+ | Mechanism | Where | Purpose |
255
+ |-----------|--------|---------|
256
+ | **Session history** | `sessions.sqlite` | Full turn-by-turn chat for a session id. |
257
+ | **Curated memory** | `GEMCODE_MEMORY.md`, `GEMCODE_USER.md` (under `.gemcode/`) | Small, **human-approved** facts injected when memory features are on; distinct from noisy auto-memory. |
258
+ | **Embedding memory** | `memories.jsonl` + ADK hooks (when enabled) | Retrieval-oriented storage when **`GEMCODE_ENABLE_MEMORY`** (and related) are on. |
259
+
260
+ Use **`/curated`** in the REPL to preview the curated snapshot; tools such as **`read_curated_memory`** / **`remember_fact`** operate on that layer.
261
+
262
+ ### GemSkills at a glance (four ways)
263
+
264
+ | Mechanism | Effect |
265
+ |-----------|--------|
266
+ | **`/create gemskill <name> [description]`** | Creates **`.gemcode/skills/<name>/SKILL.md`** scaffold (new skill on disk). |
267
+ | **`/gemskill <name>`** | **Pins** the skill’s **full body** into the **system instruction** for the **current session** (until `/gemskill clear`, new session, or resume). Rebuilds the runner. |
268
+ | **`/append gemskill <name> <request>`** | One model turn instructed to **edit** that skill file according to `<request>`. |
269
+ | **`/skill <name>`**, **`/<name>`**, or tools **`load_skill`** | **One-shot** turn: inject expanded skill into the **user message** for that turn only (does not pin to system prompt). |
270
+
271
+ The built-in **`batch`** skill is manual-only (`disable_model_invocation`); use **`/batch <goal>`** to run it.
272
+
273
+ ### Slash completion
274
+
275
+ The REPL and TUI use a **canonical** command list for Tab completion (`repl_commands.SLASH_COMMANDS`). Some spellings (e.g. `/quit`, `/logs`, `/embed`) still work in **`process_repl_slash`** but may not appear as separate menu rows—see descriptions in **`/help`**.
226
276
 
227
277
  ---
228
278
 
@@ -311,6 +361,7 @@ State is **project-local** (unless noted).
311
361
  | Path / artifact | Purpose |
312
362
  |-----------------|---------|
313
363
  | `sessions.sqlite` | ADK session service: conversation history for `--session` ids. |
364
+ | `GEMCODE_MEMORY.md`, `GEMCODE_USER.md` | **Curated memory** (see [Curated memory](#curated-memory)). |
314
365
  | `audit.log` | JSONL audit: tool usage, model usage, terminal reasons, optional tool-use summaries. |
315
366
  | `tool-results/` | Offloaded large tool outputs; references like `tool_result:<sha256>`. |
316
367
  | `artifacts/` | File artifacts (ADK `FileArtifactService`). |
@@ -421,10 +472,15 @@ In interactive mode, lines starting with `/` are **slash commands** (see `repl_c
421
472
 
422
473
  | Command | Purpose |
423
474
  |---------|---------|
475
+ | `/trust`, `/trust on`, `/trust off` | Show, grant, or revoke **workspace trust** for the project root (stored in `~/.gemcode/trust.json`; required for file/shell/git tools). |
424
476
  | `/init` \| `/init force` | Analyze the repo and generate or overwrite `GEMINI.md`. |
425
477
  | `/cost` | Token usage and estimated cost for the session. |
426
478
  | `/notes`, `/notes clear`, `/notes edit` | View, clear, or edit `.gemcode/notes.md`. |
427
- | `/create gemskill <name> [description]` | Scaffold `.gemcode/skills/<name>/SKILL.md`. |
479
+ | `/create gemskill <name> [description]` | **Create** a new GemSkill (scaffold `SKILL.md`). |
480
+ | `/gemskill <name>` | **Load** an existing skill into the **session system prompt** (until `/gemskill clear` or new session). |
481
+ | `/gemskill list` \| `/gemskill clear` | List skills or unload all session-loaded skills. |
482
+ | `/append gemskill <name> <request>` | **Iterate** the skill file: model edits `SKILL.md` per your request. |
483
+ | `/curated` | Show **curated memory** snapshot (`.gemcode/GEMCODE_MEMORY.md`, `GEMCODE_USER.md`). |
428
484
  | `/style`, `/style <name>\|off` | List or activate **output styles** (`.gemcode/output-styles/*.md`). |
429
485
  | `/rules` | Show **rule** files from `.gemcode/rules/` (with path gating). |
430
486
  | `/diff`, `/diff last`, `/diff cp_…` | Git diff, or **checkpoint → workspace** diff. |
@@ -437,7 +493,7 @@ In interactive mode, lines starting with `/` are **slash commands** (see `repl_c
437
493
  | Command | Purpose |
438
494
  |---------|---------|
439
495
  | `/help` | Short help. |
440
- | `/status` | Model, capabilities, thinking, limits, risk/context telemetry. |
496
+ | `/status` | Model, capabilities, thinking, limits, risk/context telemetry; **`loaded_skills`** (session-pinned GemSkills from `/gemskill`). |
441
497
  | `/config` | Dump active config fields. |
442
498
  | `/session`, `/session list`, `/session name`, `/session resume`, `/session new` | Session management; `/clear` aliases `/session new`. |
443
499
  | `/compact`, `/compact <focus>` | Force context compaction / summarization. |
@@ -445,6 +501,12 @@ In interactive mode, lines starting with `/` are **slash commands** (see `repl_c
445
501
  | `/context` | Context pressure and token breakdown (includes styles, rules, skills manifest, touched paths). |
446
502
  | `/audit [N]` | Tail of `audit.log`. |
447
503
  | `/tools` | Tool inventory for current config. |
504
+ | `/tools smoke` | Declaration compile check only (lists failures). |
505
+ | `/eval`, `/eval llm` | **Eval harness**: tool smoke + `pytest` if a `tests/` tree exists; optional LLM golden prompts (costs tokens). Writes `.gemcode/evals/last_eval.json`. |
506
+ | `/autotune init <tag>` | Create git branch `autotune/<tag>` (requires a git repo). |
507
+ | `/autotune eval`, `… llm` | Run eval and append `.gemcode/evals/autotune_ledger.jsonl`. |
508
+ | `/login` | How to run **`gemcode login`** (API key is set outside the REPL). |
509
+ | `/live-audio` | How to run **`gemcode live-audio`** (mic → Gemini Live). |
448
510
  | `/doctor` | Environment sanity check. |
449
511
  | `/version` | Version string. |
450
512
  | `/exit` | Leave the REPL. |
@@ -462,6 +524,7 @@ In interactive mode, lines starting with `/` are **slash commands** (see `repl_c
462
524
  |---------|---------|
463
525
  | `/computer`, `/computer on\|off`, `/computer url` | Browser automation (Playwright). |
464
526
  | `/research`, `/research on\|off` | Deep research tools. |
527
+ | `/maps`, `/maps on\|off` | Maps **grounding** toggle (runner rebuild). |
465
528
  | `/embeddings on\|off` | Semantic search tool. |
466
529
  | `/caps`, `/caps …` | View or bulk-toggle capabilities. |
467
530
  | `/memory`, `/memory on\|off` | Persistent memory. |
@@ -496,7 +559,33 @@ The TUI (when `GEMCODE_TUI=1` and terminal supports it) provides **slash complet
496
559
  - **Discovery:** Only **metadata** (name + description) is preloaded into context for token efficiency. Full body loads **on demand** via `/skill <name>`, `/<name>`, or tools `load_skill` / `list_skills`.
497
560
  - **Built-in:** **`batch`** — parallel large-change workflow (map → units → `run_subtask` → verify). Exposed as `/batch <goal>`; not auto-invoked by the model (`disable_model_invocation`).
498
561
 
499
- Use **`/create gemskill <name>`** to scaffold a new skill directory.
562
+ - **`/create gemskill <name>`** create a new skill directory.
563
+ - **`/gemskill <name>`** — pin the full skill body into the system prompt for this session.
564
+ - **`/append gemskill <name> <text>`** — one-shot turn for the model to revise that skill on disk.
565
+
566
+ ---
567
+
568
+ ## Curated memory
569
+
570
+ **Curated memory** is a small, **intentional** text layer separate from raw session logs or embedding retrieval:
571
+
572
+ - **Project facts:** `.gemcode/GEMCODE_MEMORY.md` (conventions, commands, architecture notes the team wants the agent to see).
573
+ - **User preferences (per project):** `.gemcode/GEMCODE_USER.md`.
574
+
575
+ Legacy filenames **`.gemcode/MEMORY.md`** and **`.gemcode/USER.md`** are still read if the new names are absent.
576
+
577
+ Content is **sanitized** before append (length and sensitivity heuristics). The REPL command **`/curated`** prints a bounded snapshot of what would be injected. When the **memory** capability is enabled, curated material can be combined with broader memory tools—see **`remember_fact`**, **`read_curated_memory`** in the [function tools](#function-tools-catalog) table.
578
+
579
+ ---
580
+
581
+ ## Workspace trust
582
+
583
+ File, shell, and related tools require the **project root** to be **trusted**. Trust is stored in **`~/.gemcode/trust.json`** (path configurable via **`GEMCODE_HOME`**), not inside the repo.
584
+
585
+ - **`/trust`** — show whether the current root is trusted.
586
+ - **`/trust on`** / **`/trust off`** — grant or revoke trust for **`cfg.project_root`**.
587
+
588
+ Without trust, mutating and shell tools remain blocked even if **`--yes`** is set—this reduces accidental execution when the cwd is wrong (e.g. home directory).
500
589
 
501
590
  ---
502
591
 
@@ -1,15 +1,15 @@
1
1
  gemcode/__init__.py,sha256=l0DCRYqK7KM7Fb7u49fqh-5_SlpeIL7r3LjMeJWMgSg,112
2
2
  gemcode/__main__.py,sha256=EX2s1hxq2Yvli_-tnBN3w5Qv4bOjsBBbjyISF0pDIQw,37
3
- gemcode/agent.py,sha256=r20SL_FNI-45rK3DjlecoqOR4MqEH69UpDfRF0trSzc,55480
3
+ gemcode/agent.py,sha256=_v00pQSOzAI9nyaaKC1wn-aTalD4SV52KDzwlyIHJAo,56867
4
4
  gemcode/audit.py,sha256=bh9uhXaeh8wqxqoZtz3ZAowd8Ndk1ss-mw9993Vlrgo,469
5
5
  gemcode/autocompact.py,sha256=OE3QbGx2gWN2WVXy28Sd0xyfAJgVR1x6ml9HrX2CB7I,6719
6
- gemcode/autotune.py,sha256=3Oz3HLjqgrMyCivYa0kOLZWJ3iRV3oAFZYbMKOyZZ70,2479
6
+ gemcode/autotune.py,sha256=zcTGDKC8LSnw0fHuoOcnnh1rz0by8K6MTUKl0_GhT9s,2704
7
7
  gemcode/callbacks.py,sha256=QR98bz-FeK2kp9N9JdzaeHRD2Ga2_rgz0XAPiYFafU0,31038
8
8
  gemcode/capability_routing.py,sha256=ZScRb2Bn41ZJUEmw_QaYYDVUPaNnbGBbK216tm5G4mI,3456
9
9
  gemcode/checkpoints.py,sha256=ptz2YW7P56mkqOrAf7-MXA0_Op3y4fgAg5ly_etuKvw,3936
10
- gemcode/cli.py,sha256=DowN0qGnjtl5AUOB5h65w7kGLkxWH2CRHly_cozUn84,27889
10
+ gemcode/cli.py,sha256=2R5pvpx3T8sUnfgbBRmkFjv5-uxCPeo_HuXMsaz23s8,28429
11
11
  gemcode/compaction.py,sha256=9YtA_qa23_8dHWVHx7AJwUduuI7jJQtq-m6sT8jgPWI,1186
12
- gemcode/config.py,sha256=Xqox35fLqGaJtRiXEM_v7INTSg4sGe-Ym9JwOwyJVBY,17688
12
+ gemcode/config.py,sha256=OTJrLFIX-sr6kMWVI4l8rM2kJqkt3X2NcNFBEg6Nfdg,18042
13
13
  gemcode/context_budget.py,sha256=nctVwzpUg6kyBL0FHM7xcrvVvZpX9qj7cZK4IczfKAg,10534
14
14
  gemcode/context_warning.py,sha256=YwkkNx6AM2ugOckg8QhRWYcU6D3UJXUOHiBwjKQT2rs,4199
15
15
  gemcode/credentials.py,sha256=o1gQ4oQXAjjA1IANXgCalM3WjTkfVkbkXsf-lNVQ55I,1300
@@ -39,8 +39,8 @@ gemcode/policy_profile.py,sha256=kcaKJQwLxAo3RjqfJJHl_G7B5GgTYKco0z3k5QcXsVY,386
39
39
  gemcode/pricing.py,sha256=lftp0SwyDqOzHqC2-6XzgZZhjif5PLdCe1Q3wY-p6kQ,3558
40
40
  gemcode/prompt_suggestions.py,sha256=RNEclxtoorRqu-wUlzuyUJ7OLFVOOryGOZBpbaCducI,2544
41
41
  gemcode/refine.py,sha256=BijEZ4Z32wGa9aK_WottyAhZF-j0xEqRg5UpjedNv2A,7653
42
- gemcode/repl_commands.py,sha256=wbrsn1aPW0bF2XVDqwiPqDZBW1qFCuQONASOUu0MKj0,12169
43
- gemcode/repl_slash.py,sha256=_I-kvZxi8j3ZCXPBI9cmJZJV0yLiz_-zDgoLKpBrTA8,69803
42
+ gemcode/repl_commands.py,sha256=iGF68JdJiwryoZ69EvLSNAg7BN-ivopctJqNWe4UjWk,18467
43
+ gemcode/repl_slash.py,sha256=zRBFJYnaNfO3yAfHQ5S9X-OyiZSWDePoR62OV5LbG5M,83743
44
44
  gemcode/review_agent.py,sha256=4t7_5-aE60b4-EheJ_eSB_H2eQYf9GppKoui6jw0TME,5264
45
45
  gemcode/rules.py,sha256=Itg02VpifOo6jqGj5xwna_ahaPPb0OVtaeR2cNI0pLE,3018
46
46
  gemcode/session_runtime.py,sha256=MQr34s6dw4bmk_VPSgBvkJTMwUBZQhvWDXl_iC5Pp6s,20867
@@ -52,13 +52,13 @@ gemcode/tool_prompt_manifest.py,sha256=BbM88yj6hN5CTDBzyHa7DZ1S6gpzIZQe9G7dFqkMm
52
52
  gemcode/tool_registry.py,sha256=DqHshoOG_8U5Ijey407DFxp3xYq3bVBLwUVxFgGgELs,1799
53
53
  gemcode/tool_result_store.py,sha256=50ufSP03F1D8mWFX7HFimTVh-fMWpshOtw4AP-qf-cU,6351
54
54
  gemcode/tools_inspector.py,sha256=ZAuD1oVIsZvyD_vrzaWBws2ezpFT6cIsU_w4yc_1PPA,4086
55
- gemcode/trust.py,sha256=fxe57Xg6aL_KU24bQDUtD-rXjsNpaq7g-eQTInZnudE,1336
55
+ gemcode/trust.py,sha256=kZi1dll1T8RUZtSY42WbsEh-ZIkMRup_IZPfYlJMbw4,1457
56
56
  gemcode/version.py,sha256=uwynYS-RmK8CDoqGtt8976kFkJv0zELkEAlwebnp_io,380
57
57
  gemcode/vertex.py,sha256=Fy8zxuU8jWkObt0WDRI0XmgnjNznILXVLVwKjImNz9Q,643
58
58
  gemcode/workspace_hints.py,sha256=WQFwyoGnVrzzYSl0s5MNcd1-UP11Ao8KGgrdFIB5m9g,621
59
59
  gemcode/computer_use/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
60
  gemcode/computer_use/browser_computer.py,sha256=bWXXgj7K6ZrDqT49iPEcnBd_ZJBwRxSgKhzKaJpn4cQ,13212
61
- gemcode/evals/harness.py,sha256=nn6luhFf5EzIGbTIFRvac36yVlLQRhXoKnfyKbcCaM4,3695
61
+ gemcode/evals/harness.py,sha256=qf2L77cdghBE6uPUTkQQxL9TmgDpN54NwTmyJUFcF14,5232
62
62
  gemcode/memory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
63
  gemcode/memory/embedding_memory_service.py,sha256=4iMZUw80GY8SPrJcuT4CwOsTmZ600SOuYkm-nv2c634,9422
64
64
  gemcode/memory/file_memory_service.py,sha256=yAXCspfSPBfQXDrwPX8ZZsqaprnC_CKvgmN_GiQVzuo,6092
@@ -91,8 +91,8 @@ gemcode/tools/think.py,sha256=bch9bsz1bs24uia5l3utnNSkT84mIyU_EzMgi82e60Y,1624
91
91
  gemcode/tools/todo.py,sha256=dlGfcNce1WsJ5Y9txrDL3SoF6Hv2rms9r1cvGPs6qIs,5798
92
92
  gemcode/tools/web.py,sha256=I-6-GgCVKblc9zVFfilWLHoJZfri7_pC2MpT52ZZarE,5078
93
93
  gemcode/tools/web_search.py,sha256=YqIvwAoOXK3TMqrrMVeSrg5Nyt7Ou5nRljrYQ4784_4,8816
94
- gemcode/tui/input_handler.py,sha256=3bW98qgVeJdGeREqhH4U1jH_QWRM6YSZYeyFd2TunUw,11090
95
- gemcode/tui/scrollback.py,sha256=NLD4YYgya_ouGZSxca_ts42YVK9vp3SL3Ll6tuUiTUA,33076
94
+ gemcode/tui/input_handler.py,sha256=Az8SbPaPHksIoibjph8gevMnfjagR1b-34_wpKbEhgQ,8259
95
+ gemcode/tui/scrollback.py,sha256=BA_hjqdEggnpbq5TZtJbojB7sRRgZ7evQMEQEfeCKoU,33137
96
96
  gemcode/tui/spinner.py,sha256=dExs_enBPOWjkmRtodDzRw3E-MYh-xgtqDo54Q82sco,4892
97
97
  gemcode/tui/welcome_banner.py,sha256=aocl1lnoyLIM6RN4f65g3i0wRA71RqUlgPrGsXeVLW4,4387
98
98
  gemcode/tui/welcome_rich.py,sha256=VERTr2PB8hCdxNftJ827OdilFnnJMMDnqkaqzLne_lQ,4059
@@ -100,9 +100,9 @@ gemcode/web/__init__.py,sha256=EysmUAWs6g-lmMk4VFljKfaHVrEgb_FiIzwQmBdORJc,40
100
100
  gemcode/web/sse_adapter.py,sha256=fXhKxn_bdJJUGqlmvkxLNSYL-ZiIZDaLHtQCF_BheRc,7108
101
101
  gemcode/web/terminal_repl.py,sha256=fQt895g0qcr6VBhXfv_5b_bsC5zHT5-MO0ysBdgi2Fg,3886
102
102
  gemcode/web/web_sse_compat.py,sha256=9A2s-GI7El7AotJqhO263FrLwppCXXkdydZ5EiOQbao,504
103
- gemcode-0.3.77.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
104
- gemcode-0.3.77.dist-info/METADATA,sha256=d0GwZUIL62wHtRBxK1U5lHYxNJdd_h6yakBqZM_5SdU,33688
105
- gemcode-0.3.77.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
106
- gemcode-0.3.77.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
107
- gemcode-0.3.77.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
108
- gemcode-0.3.77.dist-info/RECORD,,
103
+ gemcode-0.3.78.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
104
+ gemcode-0.3.78.dist-info/METADATA,sha256=LEGVRi0Lfgz12djaXYe11PAzIEpce21QAIzpPQx3tvM,40109
105
+ gemcode-0.3.78.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
106
+ gemcode-0.3.78.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
107
+ gemcode-0.3.78.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
108
+ gemcode-0.3.78.dist-info/RECORD,,