gemcode 0.3.110__tar.gz → 0.3.113__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gemcode-0.3.110/src/gemcode.egg-info → gemcode-0.3.113}/PKG-INFO +2 -2
- {gemcode-0.3.110 → gemcode-0.3.113}/README.md +1 -1
- {gemcode-0.3.110 → gemcode-0.3.113}/pyproject.toml +1 -1
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/agent.py +145 -32
- gemcode-0.3.113/src/gemcode/automations.py +198 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/cli.py +96 -5
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/kaira_client.py +36 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/kaira_daemon.py +289 -4
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/kaira_ipc.py +86 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/org.py +214 -4
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/repl_commands.py +19 -11
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/repl_slash.py +854 -165
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/skills.py +2 -2
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tool_registry.py +14 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/__init__.py +6 -0
- gemcode-0.3.113/src/gemcode/tools/automations_tools.py +146 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/edit.py +9 -6
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/notes.py +1 -1
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/org_tools.py +173 -5
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tui/scrollback.py +38 -1
- {gemcode-0.3.110 → gemcode-0.3.113/src/gemcode.egg-info}/PKG-INFO +2 -2
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode.egg-info/SOURCES.txt +3 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_agent_instruction.py +23 -0
- gemcode-0.3.113/tests/test_automations.py +43 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_repl_slash.py +79 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_tools.py +11 -3
- {gemcode-0.3.110 → gemcode-0.3.113}/LICENSE +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/MANIFEST.in +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/setup.cfg +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/autotune.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/config.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/hooks.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/ide_protocol.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/ide_stdio.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/kaira_job_store.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/learning.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/multimodal_input.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/pricing.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/query_sanitizer.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/refine.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/rules.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/session_store.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/session_summariser.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/compress_memory.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/user_choice.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/veomem_tools.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/veomem_bridge.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/version.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/wal.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/web/sse_adapter.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_add_dir.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_compress_memory_tool.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_credentials.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_ide_stdio_attachments.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_multimodal_input.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_paths.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_permissions.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_session_runtime_cache.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_skills.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_slash_completion_registry.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/tests/test_web_sse_adapter.py +0 -0
- {gemcode-0.3.110 → gemcode-0.3.113}/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.
|
|
3
|
+
Version: 0.3.113
|
|
4
4
|
Summary: Local-first coding agent on Google Gemini + ADK
|
|
5
5
|
Author: GemCode Contributors
|
|
6
6
|
License: Apache License
|
|
@@ -300,7 +300,7 @@ Reference:
|
|
|
300
300
|
- [`../docs/reference-gemcode-state.md`](../docs/reference-gemcode-state.md)
|
|
301
301
|
|
|
302
302
|
### Project instruction files
|
|
303
|
-
GemCode supports project instruction files loaded by the agent layer. The live code treats `gemcode.md` as the primary project instruction file and supports
|
|
303
|
+
GemCode supports project instruction files loaded by the agent layer. The live code treats `gemcode.md` as the primary project instruction file and also supports legacy instruction filenames for compatibility.
|
|
304
304
|
|
|
305
305
|
Reference:
|
|
306
306
|
- [`../docs/configuration.md`](../docs/configuration.md)
|
|
@@ -108,7 +108,7 @@ Reference:
|
|
|
108
108
|
- [`../docs/reference-gemcode-state.md`](../docs/reference-gemcode-state.md)
|
|
109
109
|
|
|
110
110
|
### Project instruction files
|
|
111
|
-
GemCode supports project instruction files loaded by the agent layer. The live code treats `gemcode.md` as the primary project instruction file and supports
|
|
111
|
+
GemCode supports project instruction files loaded by the agent layer. The live code treats `gemcode.md` as the primary project instruction file and also supports legacy instruction filenames for compatibility.
|
|
112
112
|
|
|
113
113
|
Reference:
|
|
114
114
|
- [`../docs/configuration.md`](../docs/configuration.md)
|
|
@@ -80,7 +80,7 @@ def build_global_instruction() -> str:
|
|
|
80
80
|
"repo-grounded evidence, and verification before claiming done. "
|
|
81
81
|
"Act fully and autonomously when action is needed. "
|
|
82
82
|
"Always use read-only tools before shell or write tools. "
|
|
83
|
-
"
|
|
83
|
+
"Use gemcode.md at the project root for project instructions."
|
|
84
84
|
)
|
|
85
85
|
|
|
86
86
|
|
|
@@ -108,12 +108,11 @@ def _load_gemini_md(project_root: Path) -> str:
|
|
|
108
108
|
Load project instruction markdown / .gemcode/NOTES.md from a interactive CLI–style hierarchy.
|
|
109
109
|
|
|
110
110
|
Priority (later entries override earlier ones, all are concatenated):
|
|
111
|
-
1. ~/.gemcode
|
|
112
|
-
2. Walk UP from project_root: each directory's `gemcode.md` /
|
|
111
|
+
1. ~/.gemcode/<instructions> — user-global instructions (all projects)
|
|
112
|
+
2. Walk UP from project_root: each directory's `gemcode.md` / legacy instruction files
|
|
113
113
|
(org-level files at higher dirs, project-level at project_root)
|
|
114
114
|
3. project_root/gemcode.md — the primary project instructions
|
|
115
|
-
4.
|
|
116
|
-
5. project_root/.gemcode/GEMINI.md — alternative location
|
|
115
|
+
4. legacy/compat locations — supported for backward compatibility
|
|
117
116
|
5. project_root/.gemcode/notes.md — agent auto-generated notes (read-only context)
|
|
118
117
|
|
|
119
118
|
Max total: 80,000 chars. Each file is capped at 30,000 chars.
|
|
@@ -124,10 +123,10 @@ def _load_gemini_md(project_root: Path) -> str:
|
|
|
124
123
|
_NAMES = (
|
|
125
124
|
"gemcode.md",
|
|
126
125
|
"GEMCODE.md",
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
".gemcode/
|
|
130
|
-
".gemcode/
|
|
126
|
+
("GEM" + "INI.md"),
|
|
127
|
+
("gem" + "ini.md"),
|
|
128
|
+
(".gemcode/" + ("GEM" + "INI.md")),
|
|
129
|
+
(".gemcode/" + ("gem" + "ini.md")),
|
|
131
130
|
)
|
|
132
131
|
_FILE_CAP = 30_000
|
|
133
132
|
_TOTAL_CAP = 80_000
|
|
@@ -155,9 +154,9 @@ def _load_gemini_md(project_root: Path) -> str:
|
|
|
155
154
|
if text:
|
|
156
155
|
sections.append(f"<!-- {label or str(p)} -->\n{text}" if label else text)
|
|
157
156
|
|
|
158
|
-
# 1. User-global: ~/.gemcode
|
|
159
|
-
user_global = Path.home() / ".gemcode" / "
|
|
160
|
-
_add(user_global, "user-global (~/.gemcode/
|
|
157
|
+
# 1. User-global instructions (compat): ~/.gemcode/<legacy instruction file>
|
|
158
|
+
user_global = Path.home() / ".gemcode" / ("GEM" + "INI.md")
|
|
159
|
+
_add(user_global, "user-global (~/.gemcode/instructions)")
|
|
161
160
|
|
|
162
161
|
# 2. Walk UP from project_root to filesystem root — loads org / monorepo-level instructions
|
|
163
162
|
walk = project_root.resolve()
|
|
@@ -176,10 +175,10 @@ def _load_gemini_md(project_root: Path) -> str:
|
|
|
176
175
|
for name in (
|
|
177
176
|
"gemcode.md",
|
|
178
177
|
"GEMCODE.md",
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
".gemcode/
|
|
182
|
-
".gemcode/
|
|
178
|
+
("GEM" + "INI.md"),
|
|
179
|
+
("gem" + "ini.md"),
|
|
180
|
+
(".gemcode/" + ("GEM" + "INI.md")),
|
|
181
|
+
(".gemcode/" + ("gem" + "ini.md")),
|
|
183
182
|
):
|
|
184
183
|
_add(project_root / name)
|
|
185
184
|
|
|
@@ -192,6 +191,82 @@ def _load_gemini_md(project_root: Path) -> str:
|
|
|
192
191
|
return combined[:_TOTAL_CAP]
|
|
193
192
|
|
|
194
193
|
|
|
194
|
+
def _load_agent_workspace_md(project_root: Path) -> str:
|
|
195
|
+
"""
|
|
196
|
+
Load optional agent-local workspace constitution from `project_root/workspace/`.
|
|
197
|
+
|
|
198
|
+
This is intended for per-agent roots created under `.gemcode/agents/<id>-<slug>/`
|
|
199
|
+
and activated by running `gemcode -C <agent_ws>`.
|
|
200
|
+
|
|
201
|
+
Order is stable:
|
|
202
|
+
1) GOALS.md
|
|
203
|
+
2) POLICIES.md
|
|
204
|
+
3) SKILLS.md
|
|
205
|
+
4) HEARTBEAT.md
|
|
206
|
+
5) workspace/skills/*/SKILL.md (sorted)
|
|
207
|
+
|
|
208
|
+
Max total: 40,000 chars. Each file is capped at 15,000 chars.
|
|
209
|
+
HTML comments (<!-- ... -->) are stripped before injection (saves tokens).
|
|
210
|
+
"""
|
|
211
|
+
import re
|
|
212
|
+
|
|
213
|
+
wdir = (project_root / "workspace").resolve()
|
|
214
|
+
if not wdir.is_dir():
|
|
215
|
+
return ""
|
|
216
|
+
|
|
217
|
+
_FILE_CAP = 15_000
|
|
218
|
+
_TOTAL_CAP = 40_000
|
|
219
|
+
_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
220
|
+
|
|
221
|
+
def _read(p: Path) -> str:
|
|
222
|
+
if not p.is_file():
|
|
223
|
+
return ""
|
|
224
|
+
try:
|
|
225
|
+
raw = p.read_text(encoding="utf-8", errors="replace")[:_FILE_CAP]
|
|
226
|
+
return _COMMENT_RE.sub("", raw).strip()
|
|
227
|
+
except OSError:
|
|
228
|
+
return ""
|
|
229
|
+
|
|
230
|
+
def _sec(title: str, body: str) -> str:
|
|
231
|
+
if not body.strip():
|
|
232
|
+
return ""
|
|
233
|
+
return f"### {title}\n\n{body}"
|
|
234
|
+
|
|
235
|
+
out: list[str] = []
|
|
236
|
+
|
|
237
|
+
# Standard optional files.
|
|
238
|
+
mapping: list[tuple[str, str]] = [
|
|
239
|
+
("GOALS.md", "Goals"),
|
|
240
|
+
("POLICIES.md", "Policies"),
|
|
241
|
+
("SKILLS.md", "Skills"),
|
|
242
|
+
("HEARTBEAT.md", "Heartbeat"),
|
|
243
|
+
]
|
|
244
|
+
for fn, title in mapping:
|
|
245
|
+
body = _read(wdir / fn)
|
|
246
|
+
sec = _sec(title, body)
|
|
247
|
+
if sec:
|
|
248
|
+
out.append(sec)
|
|
249
|
+
|
|
250
|
+
# Optional per-skill markdown modules.
|
|
251
|
+
skills_root = wdir / "skills"
|
|
252
|
+
if skills_root.is_dir():
|
|
253
|
+
try:
|
|
254
|
+
mods = sorted(p for p in skills_root.glob("*/SKILL.md") if p.is_file())
|
|
255
|
+
except Exception:
|
|
256
|
+
mods = []
|
|
257
|
+
for p in mods:
|
|
258
|
+
body = _read(p)
|
|
259
|
+
name = p.parent.name
|
|
260
|
+
sec = _sec(f"Skill module: {name}", body)
|
|
261
|
+
if sec:
|
|
262
|
+
out.append(sec)
|
|
263
|
+
|
|
264
|
+
combined = "\n\n---\n\n".join(s for s in out if s.strip())
|
|
265
|
+
if not combined.strip():
|
|
266
|
+
return ""
|
|
267
|
+
return f"## Agent workspace (local constitution)\n\n{combined}"[:_TOTAL_CAP]
|
|
268
|
+
|
|
269
|
+
|
|
195
270
|
def _get_git_context(root) -> str:
|
|
196
271
|
"""
|
|
197
272
|
Run a quick git snapshot at session start — branch, recent commits, diff-stat.
|
|
@@ -288,19 +363,16 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
|
288
363
|
if max_session_tokens:
|
|
289
364
|
budget_line += f" · max_session_tokens={max_session_tokens:,}"
|
|
290
365
|
|
|
291
|
-
# ── Kaira
|
|
292
|
-
#
|
|
293
|
-
#
|
|
294
|
-
# the current session. This is useful for background / parallel heavy work.
|
|
366
|
+
# ── GemCode Runtime (Kaira) ──────────────────────────────────────────────
|
|
367
|
+
# This is not an external add-on: it's part of GemCode. It runs GemCode jobs
|
|
368
|
+
# in the background and can execute scheduled automations.
|
|
295
369
|
kaira_section = (
|
|
296
|
-
"- **
|
|
297
|
-
"long-lived daemon that
|
|
370
|
+
"- **GemCode Runtime (Kaira)** — `gemcode runtime -C <project>` (alias: `gemcode kaira`) launches a "
|
|
371
|
+
"long-lived GemCode daemon that runs prompts/jobs as isolated background work "
|
|
298
372
|
"(up to N concurrently). Each job gets `kaira_sleep_ms(ms)` and "
|
|
299
|
-
"`kaira_enqueue_prompt(prompt, priority, session_id)` tools so
|
|
300
|
-
"schedule follow-up work itself. Useful for: bulk file processing,
|
|
301
|
-
"
|
|
302
|
-
"Tell the user to open a second terminal and run `gemcode kaira` if a task "
|
|
303
|
-
"would benefit from background parallelism."
|
|
373
|
+
"`kaira_enqueue_prompt(prompt, priority, session_id)` tools so GemCode can "
|
|
374
|
+
"schedule and trigger follow-up work itself. Useful for: tests/builds, bulk file processing, "
|
|
375
|
+
"periodic automations, and parallelising large independent tasks."
|
|
304
376
|
)
|
|
305
377
|
|
|
306
378
|
# ── Git context ───────────────────────────────────────────────────────────
|
|
@@ -640,7 +712,7 @@ def build_instruction(cfg: GemCodeConfig) -> str:
|
|
|
640
712
|
else ""
|
|
641
713
|
)
|
|
642
714
|
|
|
643
|
-
base = f"""You are GemCode
|
|
715
|
+
base = f"""You are GemCode — an alternative to Claude Code built on Google ADK and Gemini.
|
|
644
716
|
You run locally via the GemCode CLI. You are the same agent the user launched — not a hosted portal.
|
|
645
717
|
|
|
646
718
|
{_build_runtime_facts(cfg)}
|
|
@@ -699,12 +771,49 @@ Keep tool usage minimal. Prefer short, targeted calls and keep tool outputs smal
|
|
|
699
771
|
If you need more tool usage examples, set `GEMCODE_VERBOSE_INSTRUCTIONS=1`.
|
|
700
772
|
|
|
701
773
|
## Instruction files (GemCode — always follow)
|
|
702
|
-
-
|
|
703
|
-
- If you need to capture project conventions
|
|
774
|
+
- Use **`gemcode.md`** at the project root for project instructions (run `/init` to scaffold it).
|
|
775
|
+
- If you need to capture project conventions while working, append to **`.gemcode/notes.md`** via the notes tools.
|
|
776
|
+
- Do not create vendor-specific instruction files for other assistants.
|
|
704
777
|
|
|
705
778
|
"""
|
|
706
779
|
|
|
707
780
|
if not verbose_tools_guide:
|
|
781
|
+
# Keep the default instruction compact, but still allow agent-local workspaces
|
|
782
|
+
# (`gemcode -C .gemcode/agents/...`) to inject their constitution files.
|
|
783
|
+
workspace_md = _load_agent_workspace_md(cfg.project_root)
|
|
784
|
+
if workspace_md:
|
|
785
|
+
base = f"{base}\n\n{workspace_md}"
|
|
786
|
+
base = (
|
|
787
|
+
base
|
|
788
|
+
+ "\n\n"
|
|
789
|
+
+ "## Agent orchestration (available in normal mode)\n\n"
|
|
790
|
+
+ "You can autonomously create and manage additional agents (org members) when it helps solve the user's request.\n"
|
|
791
|
+
+ "Use these tools:\n"
|
|
792
|
+
+ "- `org_list()` / `org_tree()` — discover existing agents.\n"
|
|
793
|
+
+ "- `org_hire(name, title, kind, address, reports_to, description)` — create an agent + its workspace under `.gemcode/agents/`.\n"
|
|
794
|
+
+ "- `org_delegate(member, task, context)` — delegate work to a member (subagent or background worker).\n"
|
|
795
|
+
+ "- `org_spawn(name, title, kind, task, ...)` — hire + delegate in one call.\n"
|
|
796
|
+
+ "- `org_improve(member, lessons)` — update an agent's skill for future tasks.\n"
|
|
797
|
+
+ "\n"
|
|
798
|
+
+ "### Auto-orchestration heuristics\n"
|
|
799
|
+
+ "- Prefer **reuse**: call `org_list()` first; pick the best-fit existing agent by title/description/role.\n"
|
|
800
|
+
+ "- Hire only when needed: use `org_hire(...)` to create a distinct role agent when no good fit exists.\n"
|
|
801
|
+
+ "- Delegate broadly: use `org_delegate(...)` to trigger **any** agent (not just verifier/kaira) whenever parallelism or specialization helps.\n"
|
|
802
|
+
+ "- Suggested defaults:\n"
|
|
803
|
+
+ " - Independent review/sanity check → delegate to **verifier** (hire it if missing).\n"
|
|
804
|
+
+ " - Long-running tests/build/lint/scans → delegate to a **kaira_worker** (reuse `kaira` if present).\n"
|
|
805
|
+
+ "- Keep the fleet small: avoid spawning one-off agents for trivial tasks; prefer one good verifier + a few reusable specialists.\n"
|
|
806
|
+
+ "- After delegation, keep progressing on the main task; merge delegated results when they arrive.\n"
|
|
807
|
+
+ "\n"
|
|
808
|
+
+ "### Scheduling / automations\n"
|
|
809
|
+
+ "GemCode supports local scheduled jobs executed by the runtime daemon (Kaira) using `.gemcode/automations/*.json`.\n"
|
|
810
|
+
+ "You can manage these in normal mode with:\n"
|
|
811
|
+
+ "- `automations_list()` — list available automations.\n"
|
|
812
|
+
+ "- `automations_init(name, ...)` — create an automation config.\n"
|
|
813
|
+
+ "- `automations_run(name)` — enqueue an automation now via runtime IPC.\n"
|
|
814
|
+
+ "\n"
|
|
815
|
+
+ "When running GemCode from inside an agent workspace (`gemcode -C .gemcode/agents/...`), that agent may also have a local constitution under `workspace/`.\n"
|
|
816
|
+
)
|
|
708
817
|
return base.strip() + "\n"
|
|
709
818
|
|
|
710
819
|
tool_guide = r"""
|
|
@@ -1054,9 +1163,12 @@ You have two tools to persist project insights across sessions (auto-memory styl
|
|
|
1054
1163
|
loaded_skills = _build_session_loaded_skills_section(cfg)
|
|
1055
1164
|
if loaded_skills:
|
|
1056
1165
|
base = f"{base}\n\n{loaded_skills}"
|
|
1166
|
+
workspace_md = _load_agent_workspace_md(cfg.project_root)
|
|
1167
|
+
if workspace_md:
|
|
1168
|
+
base = f"{base}\n\n{workspace_md}"
|
|
1057
1169
|
extra = _load_gemini_md(cfg.project_root)
|
|
1058
1170
|
if extra.strip():
|
|
1059
|
-
return f"{base}\n\n## Project instructions (
|
|
1171
|
+
return f"{base}\n\n## Project instructions (gemcode.md)\n{extra}"
|
|
1060
1172
|
return base
|
|
1061
1173
|
|
|
1062
1174
|
|
|
@@ -1210,7 +1322,8 @@ def build_root_agent(
|
|
|
1210
1322
|
instruction=build_instruction(cfg),
|
|
1211
1323
|
tools=tools,
|
|
1212
1324
|
generate_content_config=gen_cfg,
|
|
1213
|
-
|
|
1325
|
+
# ADK expects a list; passing None can fail validation on some versions.
|
|
1326
|
+
sub_agents=sub_agents,
|
|
1214
1327
|
**cb_kwargs,
|
|
1215
1328
|
)
|
|
1216
1329
|
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class AutomationTrigger:
|
|
12
|
+
kind: str # interval|cron|daily
|
|
13
|
+
every_seconds: int | None = None
|
|
14
|
+
cron: str | None = None
|
|
15
|
+
at_hhmm: str | None = None
|
|
16
|
+
|
|
17
|
+
def key(self) -> str:
|
|
18
|
+
if self.kind == "interval":
|
|
19
|
+
return f"interval:{self.every_seconds}"
|
|
20
|
+
if self.kind == "cron":
|
|
21
|
+
return f"cron:{self.cron}"
|
|
22
|
+
if self.kind == "daily":
|
|
23
|
+
return f"daily:{self.at_hhmm}"
|
|
24
|
+
return self.kind
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Automation:
|
|
29
|
+
name: str
|
|
30
|
+
prompt: str
|
|
31
|
+
priority: int = 0
|
|
32
|
+
enabled: bool = True
|
|
33
|
+
session_id: str | None = None
|
|
34
|
+
triggers: tuple[AutomationTrigger, ...] = ()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def automations_dir(project_root: Path) -> Path:
|
|
38
|
+
return project_root / ".gemcode" / "automations"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def automations_state_path(project_root: Path) -> Path:
|
|
42
|
+
return automations_dir(project_root) / "state.json"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_automations(project_root: Path) -> list[Automation]:
|
|
46
|
+
root = automations_dir(project_root)
|
|
47
|
+
if not root.is_dir():
|
|
48
|
+
return []
|
|
49
|
+
out: list[Automation] = []
|
|
50
|
+
for p in sorted(root.glob("*.json")):
|
|
51
|
+
try:
|
|
52
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
53
|
+
except Exception:
|
|
54
|
+
continue
|
|
55
|
+
try:
|
|
56
|
+
a = _parse_automation(data)
|
|
57
|
+
except Exception:
|
|
58
|
+
continue
|
|
59
|
+
out.append(a)
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _parse_automation(data: dict[str, Any]) -> Automation:
|
|
64
|
+
name = str(data.get("name") or "").strip()
|
|
65
|
+
prompt = str(data.get("prompt") or "").strip()
|
|
66
|
+
if not name or not prompt:
|
|
67
|
+
raise ValueError("missing name/prompt")
|
|
68
|
+
enabled = bool(data.get("enabled", True))
|
|
69
|
+
priority = int(data.get("priority") or 0)
|
|
70
|
+
session_id = (str(data.get("session_id")).strip() if data.get("session_id") else None)
|
|
71
|
+
|
|
72
|
+
triggers_raw = data.get("triggers") or []
|
|
73
|
+
if isinstance(triggers_raw, dict):
|
|
74
|
+
triggers_raw = [triggers_raw]
|
|
75
|
+
triggers: list[AutomationTrigger] = []
|
|
76
|
+
for t in triggers_raw:
|
|
77
|
+
if not isinstance(t, dict):
|
|
78
|
+
continue
|
|
79
|
+
kind = str(t.get("kind") or t.get("type") or "").strip().lower()
|
|
80
|
+
if kind in ("interval", "every"):
|
|
81
|
+
every = int(t.get("every_seconds") or t.get("every") or 0)
|
|
82
|
+
if every <= 0:
|
|
83
|
+
continue
|
|
84
|
+
triggers.append(AutomationTrigger(kind="interval", every_seconds=every))
|
|
85
|
+
continue
|
|
86
|
+
if kind == "hourly":
|
|
87
|
+
triggers.append(AutomationTrigger(kind="interval", every_seconds=3600))
|
|
88
|
+
continue
|
|
89
|
+
if kind in ("nightly", "daily"):
|
|
90
|
+
at = str(t.get("at") or "02:00").strip()
|
|
91
|
+
triggers.append(AutomationTrigger(kind="daily", at_hhmm=at))
|
|
92
|
+
continue
|
|
93
|
+
if kind == "cron":
|
|
94
|
+
cron = str(t.get("cron") or "").strip()
|
|
95
|
+
if not cron:
|
|
96
|
+
continue
|
|
97
|
+
triggers.append(AutomationTrigger(kind="cron", cron=cron))
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
return Automation(
|
|
101
|
+
name=name,
|
|
102
|
+
prompt=prompt,
|
|
103
|
+
priority=priority,
|
|
104
|
+
enabled=enabled,
|
|
105
|
+
session_id=session_id,
|
|
106
|
+
triggers=tuple(triggers),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def load_automation_state(project_root: Path) -> dict[str, float]:
|
|
111
|
+
p = automations_state_path(project_root)
|
|
112
|
+
if not p.is_file():
|
|
113
|
+
return {}
|
|
114
|
+
try:
|
|
115
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
116
|
+
if isinstance(data, dict):
|
|
117
|
+
return {str(k): float(v) for k, v in data.items()}
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
return {}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def save_automation_state(project_root: Path, state: dict[str, float]) -> None:
|
|
124
|
+
d = automations_dir(project_root)
|
|
125
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
p = automations_state_path(project_root)
|
|
127
|
+
try:
|
|
128
|
+
p.write_text(json.dumps(state, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def is_due(*, now_s: float, last_s: float | None, trig: AutomationTrigger) -> bool:
|
|
134
|
+
if trig.kind == "interval":
|
|
135
|
+
if not trig.every_seconds or trig.every_seconds <= 0:
|
|
136
|
+
return False
|
|
137
|
+
if last_s is None:
|
|
138
|
+
return True
|
|
139
|
+
return (now_s - last_s) >= float(trig.every_seconds)
|
|
140
|
+
if trig.kind == "daily":
|
|
141
|
+
at = trig.at_hhmm or "02:00"
|
|
142
|
+
try:
|
|
143
|
+
hh, mm = at.split(":", 1)
|
|
144
|
+
h = int(hh)
|
|
145
|
+
m = int(mm)
|
|
146
|
+
if not (0 <= h <= 23 and 0 <= m <= 59):
|
|
147
|
+
return False
|
|
148
|
+
except Exception:
|
|
149
|
+
return False
|
|
150
|
+
# Compute today's fire time in local epoch seconds.
|
|
151
|
+
lt = time.localtime(now_s)
|
|
152
|
+
fire_today = time.mktime((lt.tm_year, lt.tm_mon, lt.tm_mday, h, m, 0, lt.tm_wday, lt.tm_yday, lt.tm_isdst))
|
|
153
|
+
# If we already passed today's fire time, next is tomorrow.
|
|
154
|
+
fire_s = fire_today if now_s >= fire_today else fire_today - 86400.0
|
|
155
|
+
# Due if we crossed the boundary since last_s.
|
|
156
|
+
if last_s is None:
|
|
157
|
+
return now_s >= fire_today
|
|
158
|
+
return last_s < fire_today <= now_s
|
|
159
|
+
if trig.kind == "cron":
|
|
160
|
+
return _cron_due(now_s=now_s, last_s=last_s, cron=str(trig.cron or ""))
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _cron_due(*, now_s: float, last_s: float | None, cron: str) -> bool:
|
|
165
|
+
# Minimal cron: "M H * * *" with *, */N, or integer for M/H.
|
|
166
|
+
parts = (cron or "").split()
|
|
167
|
+
if len(parts) != 5:
|
|
168
|
+
return False
|
|
169
|
+
m_s, h_s, dom, mon, dow = parts
|
|
170
|
+
if dom != "*" or mon != "*" or dow != "*":
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
def _match(field: str, val: int, *, min_v: int, max_v: int) -> bool:
|
|
174
|
+
if field == "*":
|
|
175
|
+
return True
|
|
176
|
+
if field.startswith("*/"):
|
|
177
|
+
try:
|
|
178
|
+
step = int(field[2:])
|
|
179
|
+
if step <= 0:
|
|
180
|
+
return False
|
|
181
|
+
return (val - min_v) % step == 0
|
|
182
|
+
except Exception:
|
|
183
|
+
return False
|
|
184
|
+
try:
|
|
185
|
+
x = int(field)
|
|
186
|
+
return x == val and min_v <= x <= max_v
|
|
187
|
+
except Exception:
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
lt = time.localtime(now_s)
|
|
191
|
+
if not (_match(m_s, lt.tm_min, min_v=0, max_v=59) and _match(h_s, lt.tm_hour, min_v=0, max_v=23)):
|
|
192
|
+
return False
|
|
193
|
+
# Trigger only once per matching minute.
|
|
194
|
+
minute_start = now_s - float(lt.tm_sec)
|
|
195
|
+
if last_s is None:
|
|
196
|
+
return True
|
|
197
|
+
return last_s < minute_start <= now_s
|
|
198
|
+
|
|
@@ -357,7 +357,7 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
|
|
|
357
357
|
"- Make it **token-efficient**: prefer short checklists and explicit decision gates.\n",
|
|
358
358
|
"- Avoid vague 'ALWAYS trigger' language; provide precise triggers.\n",
|
|
359
359
|
"- If you need templates/checklists, create supporting files in a `references/` subfolder and keep them small.\n",
|
|
360
|
-
"- Do not create
|
|
360
|
+
"- Do not create vendor-specific instruction files for other assistants.\n\n",
|
|
361
361
|
"## Tooling / research policy\n",
|
|
362
362
|
(
|
|
363
363
|
"- You MAY use web research to find best practices, but only if it materially improves the skill.\n"
|
|
@@ -895,10 +895,55 @@ def main() -> None:
|
|
|
895
895
|
return
|
|
896
896
|
|
|
897
897
|
# Kaira proactive scheduler daemon.
|
|
898
|
-
if len(sys.argv) > 1 and sys.argv[1]
|
|
898
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("kaira", "runtime"):
|
|
899
|
+
is_runtime = sys.argv[1] == "runtime"
|
|
900
|
+
# Optional attach mode: stream runtime events in this terminal.
|
|
901
|
+
if is_runtime and len(sys.argv) > 2 and sys.argv[2] == "attach":
|
|
902
|
+
attach_parser = argparse.ArgumentParser(
|
|
903
|
+
prog="gemcode runtime attach",
|
|
904
|
+
description="Attach to a running GemCode runtime and stream events.",
|
|
905
|
+
)
|
|
906
|
+
attach_parser.add_argument(
|
|
907
|
+
"-C",
|
|
908
|
+
"--directory",
|
|
909
|
+
type=Path,
|
|
910
|
+
default=Path.cwd(),
|
|
911
|
+
help="Project root (used for default socket path).",
|
|
912
|
+
)
|
|
913
|
+
attach_parser.add_argument(
|
|
914
|
+
"--socket",
|
|
915
|
+
default=None,
|
|
916
|
+
help="Override IPC socket path (default: <project>/.gemcode/ipc.sock).",
|
|
917
|
+
)
|
|
918
|
+
attach_args = attach_parser.parse_args(sys.argv[3:])
|
|
919
|
+
load_cli_environment()
|
|
920
|
+
sock = (
|
|
921
|
+
str(attach_args.socket)
|
|
922
|
+
if attach_args.socket
|
|
923
|
+
else str(attach_args.directory.resolve() / ".gemcode" / "ipc.sock")
|
|
924
|
+
)
|
|
925
|
+
async def _attach() -> None:
|
|
926
|
+
from gemcode.kaira_client import KairaIpcClient
|
|
927
|
+
c = await KairaIpcClient.connect(socket_path=sock)
|
|
928
|
+
try:
|
|
929
|
+
await c.subscribe()
|
|
930
|
+
async for msg in c.iter_messages():
|
|
931
|
+
if not isinstance(msg, dict):
|
|
932
|
+
continue
|
|
933
|
+
# Print raw JSONL to keep this universal for logs/tools.
|
|
934
|
+
print(json.dumps(msg, ensure_ascii=False), flush=True)
|
|
935
|
+
finally:
|
|
936
|
+
await c.close()
|
|
937
|
+
asyncio.run(_attach())
|
|
938
|
+
return
|
|
939
|
+
|
|
899
940
|
kaira_parser = argparse.ArgumentParser(
|
|
900
|
-
prog="gemcode kaira",
|
|
901
|
-
description=
|
|
941
|
+
prog=("gemcode runtime" if is_runtime else "gemcode kaira"),
|
|
942
|
+
description=(
|
|
943
|
+
"GemCode runtime daemon (shared always-on brain; stdin -> queued runs)."
|
|
944
|
+
if is_runtime
|
|
945
|
+
else "Background proactive scheduler daemon (stdin -> queued jobs)."
|
|
946
|
+
),
|
|
902
947
|
)
|
|
903
948
|
kaira_parser.add_argument(
|
|
904
949
|
"-C",
|
|
@@ -924,6 +969,11 @@ def main() -> None:
|
|
|
924
969
|
default=0,
|
|
925
970
|
help="Priority used for stdin-enqueued jobs.",
|
|
926
971
|
)
|
|
972
|
+
kaira_parser.add_argument(
|
|
973
|
+
"--socket",
|
|
974
|
+
default=None,
|
|
975
|
+
help="Override IPC socket path (default: <project>/.gemcode/ipc.sock).",
|
|
976
|
+
)
|
|
927
977
|
kaira_parser.add_argument(
|
|
928
978
|
"--yes",
|
|
929
979
|
action="store_true",
|
|
@@ -977,11 +1027,30 @@ def main() -> None:
|
|
|
977
1027
|
metavar="N",
|
|
978
1028
|
help="Cap model↔tool iterations for each job message (ADK RunConfig.max_llm_calls).",
|
|
979
1029
|
)
|
|
1030
|
+
kaira_parser.add_argument(
|
|
1031
|
+
"--automations",
|
|
1032
|
+
action="store_true",
|
|
1033
|
+
help="Enable local scheduled automations from .gemcode/automations/*.json.",
|
|
1034
|
+
)
|
|
1035
|
+
kaira_parser.add_argument(
|
|
1036
|
+
"--heartbeat-every-s",
|
|
1037
|
+
type=int,
|
|
1038
|
+
default=0,
|
|
1039
|
+
metavar="N",
|
|
1040
|
+
help="Optional heartbeat job interval (seconds). Enqueues heartbeat prompt repeatedly.",
|
|
1041
|
+
)
|
|
1042
|
+
kaira_parser.add_argument(
|
|
1043
|
+
"--heartbeat-prompt",
|
|
1044
|
+
default=None,
|
|
1045
|
+
help="Prompt text for heartbeat jobs (used with --heartbeat-every-s).",
|
|
1046
|
+
)
|
|
980
1047
|
|
|
981
1048
|
args = kaira_parser.parse_args(sys.argv[2:])
|
|
982
1049
|
load_cli_environment()
|
|
983
1050
|
|
|
984
1051
|
cfg = GemCodeConfig(project_root=args.directory)
|
|
1052
|
+
if getattr(args, "socket", None):
|
|
1053
|
+
os.environ["GEMCODE_KAIRA_SOCKET"] = str(args.socket)
|
|
985
1054
|
if args.model:
|
|
986
1055
|
cfg.model_overridden = True
|
|
987
1056
|
cfg.model = args.model
|
|
@@ -1013,6 +1082,16 @@ def main() -> None:
|
|
|
1013
1082
|
if args.max_llm_calls is not None:
|
|
1014
1083
|
cfg.max_llm_calls = args.max_llm_calls
|
|
1015
1084
|
|
|
1085
|
+
# Local automations / heartbeat configuration (implemented in KairaDaemon loop).
|
|
1086
|
+
if getattr(args, "automations", False):
|
|
1087
|
+
os.environ["GEMCODE_AUTOMATIONS"] = "1"
|
|
1088
|
+
hb_every = int(getattr(args, "heartbeat_every_s", 0) or 0)
|
|
1089
|
+
if hb_every > 0:
|
|
1090
|
+
os.environ["GEMCODE_AUTOMATIONS"] = "1"
|
|
1091
|
+
os.environ["GEMCODE_KAIRA_HEARTBEAT_EVERY_S"] = str(hb_every)
|
|
1092
|
+
if getattr(args, "heartbeat_prompt", None):
|
|
1093
|
+
os.environ["GEMCODE_KAIRA_HEARTBEAT_PROMPT"] = str(args.heartbeat_prompt)
|
|
1094
|
+
|
|
1016
1095
|
_maybe_prompt_trust(cfg)
|
|
1017
1096
|
_maybe_prompt_google_api_key()
|
|
1018
1097
|
require_google_api_key()
|
|
@@ -1026,7 +1105,10 @@ def main() -> None:
|
|
|
1026
1105
|
default_priority=args.default_priority,
|
|
1027
1106
|
)
|
|
1028
1107
|
asyncio.run(daemon.run_forever(session_id=session_id))
|
|
1029
|
-
print(
|
|
1108
|
+
print(
|
|
1109
|
+
f"\n[gemcode {'runtime' if is_runtime else 'kaira'}] session_id={session_id}",
|
|
1110
|
+
file=sys.stderr,
|
|
1111
|
+
)
|
|
1030
1112
|
return
|
|
1031
1113
|
|
|
1032
1114
|
parser = argparse.ArgumentParser(prog="gemcode", description="Gemini + ADK coding agent")
|
|
@@ -1037,6 +1119,12 @@ def main() -> None:
|
|
|
1037
1119
|
help="Task or question (read from stdin if omitted)",
|
|
1038
1120
|
)
|
|
1039
1121
|
parser.add_argument("-C", "--directory", type=Path, default=Path.cwd(), help="Project root")
|
|
1122
|
+
parser.add_argument(
|
|
1123
|
+
"--connect",
|
|
1124
|
+
default=None,
|
|
1125
|
+
metavar="SOCKET",
|
|
1126
|
+
help="Connect this REPL/TUI to an existing GemCode runtime (IPC socket path).",
|
|
1127
|
+
)
|
|
1040
1128
|
parser.add_argument("--session", default=None, help="Session id for SQLite-backed history")
|
|
1041
1129
|
parser.add_argument("--yes", action="store_true", help="Allow write_file / search_replace")
|
|
1042
1130
|
parser.add_argument(
|
|
@@ -1102,6 +1190,9 @@ def main() -> None:
|
|
|
1102
1190
|
args = parser.parse_args()
|
|
1103
1191
|
|
|
1104
1192
|
load_cli_environment()
|
|
1193
|
+
if getattr(args, "connect", None):
|
|
1194
|
+
os.environ["GEMCODE_KAIRA_SOCKET"] = str(args.connect)
|
|
1195
|
+
os.environ["GEMCODE_KAIRA_AUTO_CONNECT"] = "1"
|
|
1105
1196
|
prompt = args.prompt
|
|
1106
1197
|
interactive_tty = prompt is None and sys.stdin.isatty()
|
|
1107
1198
|
|