gemcode 0.4.16__tar.gz → 0.4.18__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.4.16/src/gemcode.egg-info → gemcode-0.4.18}/PKG-INFO +7 -1
- {gemcode-0.4.16 → gemcode-0.4.18}/README.md +6 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/pyproject.toml +1 -1
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/agent_habits.py +26 -6
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/agent_mesh.py +71 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/repl_commands.py +1 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/repl_slash.py +60 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/__init__.py +28 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/bash.py +25 -1
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/shell.py +41 -1
- {gemcode-0.4.16 → gemcode-0.4.18/src/gemcode.egg-info}/PKG-INFO +7 -1
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_agent_mesh.py +26 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_tools.py +19 -12
- {gemcode-0.4.16 → gemcode-0.4.18}/LICENSE +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/MANIFEST.in +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/setup.cfg +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/__init__.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/__main__.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/a2a_bridge.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/agent.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/agent_intelligence.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/agent_triggers.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/audit.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/automations.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/autotune.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/cli.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/codebase_awareness.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/compaction.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/config.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/credentials.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/delegation_learning.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/event_bus.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/fleet_reports.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/hooks.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/ide_protocol.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/ide_stdio.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/interactions.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/invoke.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/kaira_client.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/kaira_daemon.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/kaira_ipc.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/kaira_job_store.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/learning.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/limits.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/multimodal_input.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/org.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/paths.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/permissions.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/pricing.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query/config.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query_sanitizer.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/refine.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/rules.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/self_healing.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/session_store.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/session_summariser.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/skills.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/thinking.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tool_synthesis.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/automations_tools.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/compress_memory.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/org_tools.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/user_choice.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/veomem_tools.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/trust.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/veomem_bridge.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/version.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/vertex.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/wal.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/web/sse_adapter.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode.egg-info/SOURCES.txt +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_add_dir.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_agent_habits.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_autocompact.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_automations.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_capability_routing.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_cli_init.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_compress_memory_tool.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_context_budget.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_context_warning.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_credentials.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_event_bus.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_fleet_reports.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_ide_stdio_attachments.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_kaira_ipc_paths.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_modality_tools.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_model_errors.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_model_routing.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_multimodal_input.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_paths.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_permissions.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_repl_commands.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_repl_slash.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_session_runtime_cache.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_skills.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_slash_commands.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_slash_completion_registry.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_thinking_config.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_token_budget.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_web_sse_adapter.py +0 -0
- {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_workspace_hints.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gemcode
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.18
|
|
4
4
|
Summary: Local-first coding agent on Google Gemini + ADK
|
|
5
5
|
Author: GemCode Contributors
|
|
6
6
|
License: Apache License
|
|
@@ -292,6 +292,12 @@ Every GemCode run is anchored to a project root. This determines:
|
|
|
292
292
|
- what instruction files are loaded
|
|
293
293
|
- which repo-local assets are active
|
|
294
294
|
|
|
295
|
+
### Multi-agent habits, skills, and mesh runtime
|
|
296
|
+
|
|
297
|
+
- **Habits** for the whole fleet live in **one** file: `.gemcode/habits.json` at the **fleet root**. Each row names which **org member** runs (`agent` field), on what schedule—different members can have different prompts and intervals at once.
|
|
298
|
+
- **Skills** and **per-turn runtime** are **per member**: org `skill_name`, member skill under `.gemcode/skills/`, and optional **agent workspace** `.gemcode/agents/<id>-<slug>/` (own session DB, memory, local skills). Mesh jobs use that context automatically.
|
|
299
|
+
- **Stopping automation:** removing a habit only stops *new* work. To cancel **queued** or **running** mesh jobs, use **`/mesh halt`** or the **`mesh_halt`** tool (see [`orchestration.md`](../docs/orchestration.md#stopping-background-work-habits-removed-but-jobs-still-finishing)).
|
|
300
|
+
|
|
295
301
|
### `.gemcode/`
|
|
296
302
|
GemCode stores project-local state under `.gemcode/`, including:
|
|
297
303
|
- sessions
|
|
@@ -99,6 +99,12 @@ Every GemCode run is anchored to a project root. This determines:
|
|
|
99
99
|
- what instruction files are loaded
|
|
100
100
|
- which repo-local assets are active
|
|
101
101
|
|
|
102
|
+
### Multi-agent habits, skills, and mesh runtime
|
|
103
|
+
|
|
104
|
+
- **Habits** for the whole fleet live in **one** file: `.gemcode/habits.json` at the **fleet root**. Each row names which **org member** runs (`agent` field), on what schedule—different members can have different prompts and intervals at once.
|
|
105
|
+
- **Skills** and **per-turn runtime** are **per member**: org `skill_name`, member skill under `.gemcode/skills/`, and optional **agent workspace** `.gemcode/agents/<id>-<slug>/` (own session DB, memory, local skills). Mesh jobs use that context automatically.
|
|
106
|
+
- **Stopping automation:** removing a habit only stops *new* work. To cancel **queued** or **running** mesh jobs, use **`/mesh halt`** or the **`mesh_halt`** tool (see [`orchestration.md`](../docs/orchestration.md#stopping-background-work-habits-removed-but-jobs-still-finishing)).
|
|
107
|
+
|
|
102
108
|
### `.gemcode/`
|
|
103
109
|
GemCode stores project-local state under `.gemcode/`, including:
|
|
104
110
|
- sessions
|
|
@@ -10,9 +10,18 @@ Examples:
|
|
|
10
10
|
- "Nightly at 2am, run a full security audit"
|
|
11
11
|
- "Every 5 minutes, check for new issues in the tracker"
|
|
12
12
|
|
|
13
|
-
Habits are stored in `.gemcode/habits.json`
|
|
14
|
-
or the REPL.
|
|
15
|
-
|
|
13
|
+
Habits are stored in **one** fleet file, `.gemcode/habits.json` (next to `org.json`),
|
|
14
|
+
and can be managed via tools or the REPL. That file holds **many** rows; each row is
|
|
15
|
+
one schedule and names **which org member** runs it via the ``agent`` field—so
|
|
16
|
+
different agents can have different prompts, intervals, and enablement at the same time.
|
|
17
|
+
|
|
18
|
+
**Not** stored in habits: each member’s **skills** (org `skill_name`, workspace-local
|
|
19
|
+
skills under `.gemcode/agents/<id>-<slug>/.gemcode/skills/`) and **runtime/session**
|
|
20
|
+
(SQLite session, memory, routing). Those come from org membership and the mesh worker
|
|
21
|
+
context when the habit fires.
|
|
22
|
+
|
|
23
|
+
Each habit specifies:
|
|
24
|
+
- Which agent runs it (org member name)
|
|
16
25
|
- What they do (prompt)
|
|
17
26
|
- When they do it (interval, cron, or daily)
|
|
18
27
|
- Whether they're enabled
|
|
@@ -296,7 +305,7 @@ def make_habits_tools(cfg: GemCodeConfig) -> list:
|
|
|
296
305
|
"""Build tools for managing agent habits."""
|
|
297
306
|
|
|
298
307
|
def habits_list() -> dict:
|
|
299
|
-
"""List all
|
|
308
|
+
"""List all fleet habits. Each entry targets one org member (`agent`); members differ by skills/workspace when the job runs."""
|
|
300
309
|
habits = load_habits(cfg.project_root)
|
|
301
310
|
return {
|
|
302
311
|
"ok": True,
|
|
@@ -330,9 +339,14 @@ def make_habits_tools(cfg: GemCodeConfig) -> list:
|
|
|
330
339
|
or send any message; the TUI also prints a throttled hint when mesh jobs finish
|
|
331
340
|
(GEMCODE_FLEET_TUI_NOTIFY).
|
|
332
341
|
|
|
342
|
+
Removing a habit only stops **future** enqueues. Jobs already queued or running (including
|
|
343
|
+
verifier/trigger follow-ups) keep going until they finish unless you call **`mesh_halt`**
|
|
344
|
+
or **`/mesh halt`**.
|
|
345
|
+
|
|
333
346
|
Args:
|
|
334
347
|
name: Unique name for this habit (e.g., "test-watch", "nightly-audit").
|
|
335
|
-
agent: Org member name to run this (e.g., "kaira", "verifier").
|
|
348
|
+
agent: Org **member** name to run this (e.g., "kaira", "verifier", "tcs_analyst").
|
|
349
|
+
That member’s own skills, workspace, and ADK session apply when the habit runs—not the manager’s.
|
|
336
350
|
Use "self" or "main" to run as the main GemCode agent.
|
|
337
351
|
prompt: What the agent should do each time it wakes up.
|
|
338
352
|
every_minutes: Run every N minutes (e.g., 30 = every half hour).
|
|
@@ -391,6 +405,11 @@ def make_habits_tools(cfg: GemCodeConfig) -> list:
|
|
|
391
405
|
save_habits(cfg.project_root, habits)
|
|
392
406
|
return {"ok": True, "removed": before - len(habits)}
|
|
393
407
|
|
|
408
|
+
def habits_clear_all() -> dict:
|
|
409
|
+
"""Remove every habit from `.gemcode/habits.json` (nothing will re-enqueue until you add new habits)."""
|
|
410
|
+
save_habits(cfg.project_root, [])
|
|
411
|
+
return {"ok": True, "cleared": True}
|
|
412
|
+
|
|
394
413
|
def habits_pause(name: str) -> dict:
|
|
395
414
|
"""Pause a habit (stop it from firing until resumed)."""
|
|
396
415
|
habits = load_habits(cfg.project_root)
|
|
@@ -414,7 +433,8 @@ def make_habits_tools(cfg: GemCodeConfig) -> list:
|
|
|
414
433
|
habits_list.__name__ = "habits_list"
|
|
415
434
|
habits_add.__name__ = "habits_add"
|
|
416
435
|
habits_remove.__name__ = "habits_remove"
|
|
436
|
+
habits_clear_all.__name__ = "habits_clear_all"
|
|
417
437
|
habits_pause.__name__ = "habits_pause"
|
|
418
438
|
habits_resume.__name__ = "habits_resume"
|
|
419
439
|
|
|
420
|
-
return [habits_list, habits_add, habits_remove, habits_pause, habits_resume]
|
|
440
|
+
return [habits_list, habits_add, habits_remove, habits_clear_all, habits_pause, habits_resume]
|
|
@@ -9,6 +9,12 @@ that does not require **`gemcode runtime`** for **`org_delegate`**. It manages:
|
|
|
9
9
|
3. Event routing (via the in-memory EventBus)
|
|
10
10
|
4. Automatic result reporting (fleet reports + bus messages)
|
|
11
11
|
|
|
12
|
+
Each queued job is bound to an **org member**. When it runs, that member gets their own
|
|
13
|
+
effective ``project_root`` (agent workspace under ``.gemcode/agents/…`` when configured),
|
|
14
|
+
their **skills** (member skill + workspace-local skills), **memory**, **SQLite session**,
|
|
15
|
+
and capability/model routing—so different agents do not share one generic runtime even
|
|
16
|
+
when habits or triggers are defined in the same fleet ``habits.json``.
|
|
17
|
+
|
|
12
18
|
Optional **`gemcode runtime`** is a separate fleet-manager process (IPC, automations, stdin queue).
|
|
13
19
|
Slash **`/agent assign`** / **`trigger`** publish `org.assign` over IPC when the socket is up; otherwise the REPL falls back to **`org_delegate`** (this mesh). The mesh also subscribes to **`org.assign`** on the in-process bus for the same payload shape.
|
|
14
20
|
"""
|
|
@@ -24,6 +30,7 @@ import time
|
|
|
24
30
|
import uuid
|
|
25
31
|
from dataclasses import dataclass, field
|
|
26
32
|
from pathlib import Path
|
|
33
|
+
from collections.abc import Callable
|
|
27
34
|
from typing import Any
|
|
28
35
|
|
|
29
36
|
from gemcode.config import GemCodeConfig, _truthy_env
|
|
@@ -263,6 +270,67 @@ class AgentMesh:
|
|
|
263
270
|
self._bg_loop.call_soon_threadsafe(_poke)
|
|
264
271
|
fut.result(timeout=timeout)
|
|
265
272
|
|
|
273
|
+
def _call_on_mesh_loop(self, fn: Callable[[], Any], *, timeout: float = 30.0) -> Any:
|
|
274
|
+
"""Run a sync callable on the mesh asyncio loop (safe from any thread)."""
|
|
275
|
+
if self._bg_thread is None or not self._bg_thread.is_alive():
|
|
276
|
+
self.start()
|
|
277
|
+
if not self._wait_bg_loop_ready():
|
|
278
|
+
raise RuntimeError("mesh background loop not available")
|
|
279
|
+
assert self._bg_loop is not None
|
|
280
|
+
fut: concurrent.futures.Future[Any] = concurrent.futures.Future()
|
|
281
|
+
|
|
282
|
+
def _wrap() -> None:
|
|
283
|
+
try:
|
|
284
|
+
fut.set_result(fn())
|
|
285
|
+
except Exception as e:
|
|
286
|
+
fut.set_exception(e)
|
|
287
|
+
|
|
288
|
+
self._bg_loop.call_soon_threadsafe(_wrap)
|
|
289
|
+
return fut.result(timeout=timeout)
|
|
290
|
+
|
|
291
|
+
def clear_pending_jobs(self) -> int:
|
|
292
|
+
"""Remove jobs not yet started from the mesh queue. Returns how many were dropped."""
|
|
293
|
+
|
|
294
|
+
def _drain() -> int:
|
|
295
|
+
n = 0
|
|
296
|
+
while True:
|
|
297
|
+
try:
|
|
298
|
+
self._queue.get_nowait()
|
|
299
|
+
n += 1
|
|
300
|
+
except asyncio.QueueEmpty:
|
|
301
|
+
break
|
|
302
|
+
return n
|
|
303
|
+
|
|
304
|
+
return int(self._call_on_mesh_loop(_drain))
|
|
305
|
+
|
|
306
|
+
def cancel_running_jobs(self) -> int:
|
|
307
|
+
"""Cancel in-flight mesh job tasks (habits, delegates, triggers). Returns cancel count."""
|
|
308
|
+
|
|
309
|
+
def _cancel() -> int:
|
|
310
|
+
n = 0
|
|
311
|
+
for _jid, task in list(self._running.items()):
|
|
312
|
+
if not task.done():
|
|
313
|
+
task.cancel()
|
|
314
|
+
n += 1
|
|
315
|
+
return n
|
|
316
|
+
|
|
317
|
+
return int(self._call_on_mesh_loop(_cancel))
|
|
318
|
+
|
|
319
|
+
def halt_jobs(
|
|
320
|
+
self,
|
|
321
|
+
*,
|
|
322
|
+
clear_queue: bool = True,
|
|
323
|
+
cancel_running: bool = True,
|
|
324
|
+
) -> dict[str, Any]:
|
|
325
|
+
"""Stop queued and/or running mesh work (does not edit ``habits.json``)."""
|
|
326
|
+
cleared = self.clear_pending_jobs() if clear_queue else 0
|
|
327
|
+
cancelled = self.cancel_running_jobs() if cancel_running else 0
|
|
328
|
+
return {
|
|
329
|
+
"ok": True,
|
|
330
|
+
"cleared_queued": cleared,
|
|
331
|
+
"cancelled_running": cancelled,
|
|
332
|
+
}
|
|
333
|
+
|
|
266
334
|
def enqueue(
|
|
267
335
|
self,
|
|
268
336
|
*,
|
|
@@ -603,6 +671,9 @@ class AgentMesh:
|
|
|
603
671
|
pass
|
|
604
672
|
|
|
605
673
|
finally:
|
|
674
|
+
if job.status == "running":
|
|
675
|
+
job.status = "cancelled"
|
|
676
|
+
job.error = "cancelled"
|
|
606
677
|
cf = job.completion_future
|
|
607
678
|
if cf is not None and not cf.done():
|
|
608
679
|
try:
|
|
@@ -237,6 +237,7 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
|
|
|
237
237
|
("bus", "Runtime bus — send/publish lightweight messages over IPC"),
|
|
238
238
|
("inbox", "Bus inbox filters for this UI (to/topics)"),
|
|
239
239
|
("fleet", "Fleet inbox — /fleet show | digest (habits / mesh reports)"),
|
|
240
|
+
("mesh", "In-process mesh — /mesh halt | status (stop queued/running jobs)"),
|
|
240
241
|
("agent", "Create/manage a child agent workspace (folder + registry)"),
|
|
241
242
|
# NOTE: /org and /delegate are deprecated aliases; keep working but do not list.
|
|
242
243
|
("limits", "Execution limits (calls, context, …)"),
|
|
@@ -146,6 +146,66 @@ async def process_repl_slash(
|
|
|
146
146
|
return ReplSlashResult(skip_model_turn=True)
|
|
147
147
|
return ReplSlashResult(model_prompt=fleet_digest_prompt())
|
|
148
148
|
|
|
149
|
+
# ── /mesh (in-process agent mesh — cancel queued/running work) ─────────────
|
|
150
|
+
if name == "mesh":
|
|
151
|
+
from gemcode.agent_mesh import get_mesh
|
|
152
|
+
|
|
153
|
+
raw_m = (sc.args or "").strip()
|
|
154
|
+
parts_m = raw_m.lower().split()
|
|
155
|
+
first_m = parts_m[0] if parts_m else ""
|
|
156
|
+
|
|
157
|
+
m = get_mesh(cfg)
|
|
158
|
+
if m is None:
|
|
159
|
+
out("[mesh] not initialized.")
|
|
160
|
+
out()
|
|
161
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
162
|
+
|
|
163
|
+
if first_m in ("help", "?"):
|
|
164
|
+
out("In-process agent mesh (habits, org_delegate, triggers):")
|
|
165
|
+
out(" /mesh Show counts + short tip.")
|
|
166
|
+
out(" /mesh status Queued / running job counts.")
|
|
167
|
+
out(" /mesh halt Drop queued jobs and cancel running mesh tasks.")
|
|
168
|
+
out(" /mesh halt --habits Same, and clear `.gemcode/habits.json` entirely.")
|
|
169
|
+
out("Note: `habits_remove` only stops *new* enqueues; queued or running jobs continue")
|
|
170
|
+
out("until they finish unless you `/mesh halt`. Orchestration is in this process")
|
|
171
|
+
out("(no separate daemon required for the mesh).")
|
|
172
|
+
out()
|
|
173
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
174
|
+
|
|
175
|
+
if first_m in ("halt", "stop", "kill"):
|
|
176
|
+
and_habits = "--habits" in parts_m
|
|
177
|
+
try:
|
|
178
|
+
h = m.halt_jobs(clear_queue=True, cancel_running=True)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
out(f"[mesh] halt failed: {type(e).__name__}: {e}")
|
|
181
|
+
out()
|
|
182
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
183
|
+
out(
|
|
184
|
+
f"[mesh] halted: cleared {h.get('cleared_queued', 0)} queued job(s), "
|
|
185
|
+
f"cancelled {h.get('cancelled_running', 0)} running task(s).",
|
|
186
|
+
)
|
|
187
|
+
if and_habits:
|
|
188
|
+
from gemcode.agent_habits import save_habits
|
|
189
|
+
save_habits(cfg.project_root, [])
|
|
190
|
+
out("[mesh] also cleared `.gemcode/habits.json` (all habits removed).")
|
|
191
|
+
out()
|
|
192
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
193
|
+
|
|
194
|
+
if first_m in ("status", "") or not parts_m:
|
|
195
|
+
st = m.status()
|
|
196
|
+
out(
|
|
197
|
+
f"[mesh] queued={st['queued_jobs']} running={st['running_jobs']} "
|
|
198
|
+
f"completed_total={st['completed_jobs']} max_concurrency={st['max_concurrency']}",
|
|
199
|
+
)
|
|
200
|
+
if not parts_m or first_m == "":
|
|
201
|
+
out("Tip: `/mesh halt` stops queued + running work · `/mesh help`")
|
|
202
|
+
out()
|
|
203
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
204
|
+
|
|
205
|
+
out("[mesh] unknown subcommand. Try `/mesh help`.")
|
|
206
|
+
out()
|
|
207
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
208
|
+
|
|
149
209
|
# ── /attach (queue files for the next user message: PDF, images, audio, …) ─
|
|
150
210
|
if name in ("attach", "file", "image", "img"):
|
|
151
211
|
raw_i = (sc.args or "").strip()
|
|
@@ -206,8 +206,36 @@ def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) ->
|
|
|
206
206
|
return {"ok": False, "error": "mesh not initialized"}
|
|
207
207
|
return {"ok": True, **m.status()}
|
|
208
208
|
|
|
209
|
+
def mesh_halt(
|
|
210
|
+
clear_queued_jobs: bool = True,
|
|
211
|
+
cancel_running_jobs: bool = True,
|
|
212
|
+
remove_all_habits: bool = False,
|
|
213
|
+
) -> dict:
|
|
214
|
+
"""
|
|
215
|
+
Stop background mesh work: drop jobs waiting in the queue, cancel jobs currently running,
|
|
216
|
+
and optionally wipe all habits. Use when habits were removed but work keeps finishing.
|
|
217
|
+
|
|
218
|
+
This is the in-process mesh (same GemCode session), not a separate ``gemcode runtime`` daemon.
|
|
219
|
+
"""
|
|
220
|
+
m = get_mesh(cfg)
|
|
221
|
+
if m is None:
|
|
222
|
+
return {"ok": False, "error": "mesh not initialized"}
|
|
223
|
+
try:
|
|
224
|
+
out = m.halt_jobs(clear_queue=clear_queued_jobs, cancel_running=cancel_running_jobs)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
return {"ok": False, "error": f"{type(e).__name__}: {e}"}
|
|
227
|
+
if remove_all_habits:
|
|
228
|
+
from gemcode.agent_habits import save_habits
|
|
229
|
+
save_habits(cfg.project_root, [])
|
|
230
|
+
out["habits_cleared"] = True
|
|
231
|
+
else:
|
|
232
|
+
out["habits_cleared"] = False
|
|
233
|
+
return out
|
|
234
|
+
|
|
209
235
|
mesh_status.__name__ = "mesh_status"
|
|
236
|
+
mesh_halt.__name__ = "mesh_halt"
|
|
210
237
|
tools.append(mesh_status)
|
|
238
|
+
tools.append(mesh_halt)
|
|
211
239
|
except Exception:
|
|
212
240
|
pass
|
|
213
241
|
|
|
@@ -38,7 +38,7 @@ def make_bash_tool(cfg: GemCodeConfig):
|
|
|
38
38
|
except Exception:
|
|
39
39
|
pass
|
|
40
40
|
|
|
41
|
-
def
|
|
41
|
+
def _bash_sync(
|
|
42
42
|
command: str,
|
|
43
43
|
timeout_seconds: int = 120,
|
|
44
44
|
cwd_subdir: str = ".",
|
|
@@ -232,6 +232,30 @@ def make_bash_tool(cfg: GemCodeConfig):
|
|
|
232
232
|
except subprocess.TimeoutExpired:
|
|
233
233
|
return {"error": f"Timeout after {timeout_seconds}s", "command": command}
|
|
234
234
|
|
|
235
|
+
async def bash(
|
|
236
|
+
command: str,
|
|
237
|
+
timeout_seconds: int = 120,
|
|
238
|
+
cwd_subdir: str = ".",
|
|
239
|
+
background: bool = False,
|
|
240
|
+
) -> dict:
|
|
241
|
+
"""
|
|
242
|
+
Async wrapper for bash execution.
|
|
243
|
+
|
|
244
|
+
Why: synchronous subprocess calls block the TUI event loop, freezing the
|
|
245
|
+
live spinner timers ("Running…", "Querying…"). We run blocking shell work
|
|
246
|
+
on a worker thread so the UI keeps updating.
|
|
247
|
+
"""
|
|
248
|
+
if background:
|
|
249
|
+
# Background start is quick; keep it synchronous for simplicity.
|
|
250
|
+
return _bash_sync(command, timeout_seconds=timeout_seconds, cwd_subdir=cwd_subdir, background=True)
|
|
251
|
+
return await asyncio.to_thread(
|
|
252
|
+
_bash_sync,
|
|
253
|
+
command,
|
|
254
|
+
timeout_seconds,
|
|
255
|
+
cwd_subdir,
|
|
256
|
+
False,
|
|
257
|
+
)
|
|
258
|
+
|
|
235
259
|
return bash
|
|
236
260
|
|
|
237
261
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import shutil
|
|
@@ -45,7 +46,7 @@ def make_run_command(cfg: GemCodeConfig):
|
|
|
45
46
|
except Exception:
|
|
46
47
|
pass
|
|
47
48
|
|
|
48
|
-
def
|
|
49
|
+
def _run_command_sync(
|
|
49
50
|
command: str,
|
|
50
51
|
args: list[str] | None = None,
|
|
51
52
|
timeout_seconds: int = 120,
|
|
@@ -200,4 +201,43 @@ def make_run_command(cfg: GemCodeConfig):
|
|
|
200
201
|
except subprocess.TimeoutExpired:
|
|
201
202
|
return {"error": f"Timeout after {timeout_seconds}s"}
|
|
202
203
|
|
|
204
|
+
async def run_command(
|
|
205
|
+
command: str,
|
|
206
|
+
args: list[str] | None = None,
|
|
207
|
+
timeout_seconds: int = 120,
|
|
208
|
+
tool_context: ToolContext | None = None,
|
|
209
|
+
cwd_subdir: str = ".",
|
|
210
|
+
background: bool = False,
|
|
211
|
+
extra_env_keys: list[str] | None = None,
|
|
212
|
+
extra_env_values: list[str] | None = None,
|
|
213
|
+
) -> dict:
|
|
214
|
+
"""
|
|
215
|
+
Async wrapper for allowlisted subprocess execution.
|
|
216
|
+
|
|
217
|
+
Why: synchronous subprocess calls block the TUI event loop, freezing the live
|
|
218
|
+
spinner timers while tools run. We offload blocking work to a thread.
|
|
219
|
+
"""
|
|
220
|
+
if background:
|
|
221
|
+
return _run_command_sync(
|
|
222
|
+
command,
|
|
223
|
+
args=args,
|
|
224
|
+
timeout_seconds=timeout_seconds,
|
|
225
|
+
tool_context=tool_context,
|
|
226
|
+
cwd_subdir=cwd_subdir,
|
|
227
|
+
background=True,
|
|
228
|
+
extra_env_keys=extra_env_keys,
|
|
229
|
+
extra_env_values=extra_env_values,
|
|
230
|
+
)
|
|
231
|
+
return await asyncio.to_thread(
|
|
232
|
+
_run_command_sync,
|
|
233
|
+
command,
|
|
234
|
+
args,
|
|
235
|
+
timeout_seconds,
|
|
236
|
+
tool_context,
|
|
237
|
+
cwd_subdir,
|
|
238
|
+
False,
|
|
239
|
+
extra_env_keys,
|
|
240
|
+
extra_env_values,
|
|
241
|
+
)
|
|
242
|
+
|
|
203
243
|
return run_command
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gemcode
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.18
|
|
4
4
|
Summary: Local-first coding agent on Google Gemini + ADK
|
|
5
5
|
Author: GemCode Contributors
|
|
6
6
|
License: Apache License
|
|
@@ -292,6 +292,12 @@ Every GemCode run is anchored to a project root. This determines:
|
|
|
292
292
|
- what instruction files are loaded
|
|
293
293
|
- which repo-local assets are active
|
|
294
294
|
|
|
295
|
+
### Multi-agent habits, skills, and mesh runtime
|
|
296
|
+
|
|
297
|
+
- **Habits** for the whole fleet live in **one** file: `.gemcode/habits.json` at the **fleet root**. Each row names which **org member** runs (`agent` field), on what schedule—different members can have different prompts and intervals at once.
|
|
298
|
+
- **Skills** and **per-turn runtime** are **per member**: org `skill_name`, member skill under `.gemcode/skills/`, and optional **agent workspace** `.gemcode/agents/<id>-<slug>/` (own session DB, memory, local skills). Mesh jobs use that context automatically.
|
|
299
|
+
- **Stopping automation:** removing a habit only stops *new* work. To cancel **queued** or **running** mesh jobs, use **`/mesh halt`** or the **`mesh_halt`** tool (see [`orchestration.md`](../docs/orchestration.md#stopping-background-work-habits-removed-but-jobs-still-finishing)).
|
|
300
|
+
|
|
295
301
|
### `.gemcode/`
|
|
296
302
|
GemCode stores project-local state under `.gemcode/`, including:
|
|
297
303
|
- sessions
|
|
@@ -173,6 +173,32 @@ def test_apply_mesh_worker_unattended_off_inherits_manager(tmp_path: Path) -> No
|
|
|
173
173
|
assert cfg.interactive_permission_ask is True
|
|
174
174
|
|
|
175
175
|
|
|
176
|
+
def test_mesh_halt_clears_pending_queue(tmp_path: Path) -> None:
|
|
177
|
+
old_s = os.environ.get("PYTEST_GEMCODE_MESH_SCHEDULER")
|
|
178
|
+
old_h = os.environ.get("GEMCODE_AGENT_HABITS")
|
|
179
|
+
os.environ["PYTEST_GEMCODE_MESH_SCHEDULER"] = "0"
|
|
180
|
+
os.environ["GEMCODE_AGENT_HABITS"] = "0"
|
|
181
|
+
try:
|
|
182
|
+
cfg = GemCodeConfig(project_root=tmp_path)
|
|
183
|
+
mesh = AgentMesh(cfg, max_concurrency=2)
|
|
184
|
+
mesh.enqueue(prompt="a", priority=1, member_name="x")
|
|
185
|
+
mesh.enqueue(prompt="b", priority=1, member_name="y")
|
|
186
|
+
mesh.wait_for_pending_enqueues()
|
|
187
|
+
assert mesh._queue.qsize() == 2
|
|
188
|
+
h = mesh.halt_jobs(clear_queue=True, cancel_running=False)
|
|
189
|
+
assert h["cleared_queued"] == 2
|
|
190
|
+
assert mesh._queue.qsize() == 0
|
|
191
|
+
finally:
|
|
192
|
+
if old_s is None:
|
|
193
|
+
os.environ.pop("PYTEST_GEMCODE_MESH_SCHEDULER", None)
|
|
194
|
+
else:
|
|
195
|
+
os.environ["PYTEST_GEMCODE_MESH_SCHEDULER"] = old_s
|
|
196
|
+
if old_h is None:
|
|
197
|
+
os.environ.pop("GEMCODE_AGENT_HABITS", None)
|
|
198
|
+
else:
|
|
199
|
+
os.environ["GEMCODE_AGENT_HABITS"] = old_h
|
|
200
|
+
|
|
201
|
+
|
|
176
202
|
def test_mesh_priority_ordering(tmp_path: Path) -> None:
|
|
177
203
|
"""Higher priority jobs should be dequeued first."""
|
|
178
204
|
old_s = os.environ.get("PYTEST_GEMCODE_MESH_SCHEDULER")
|
|
@@ -2,6 +2,8 @@ from pathlib import Path
|
|
|
2
2
|
import sys
|
|
3
3
|
from unittest.mock import MagicMock
|
|
4
4
|
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
5
7
|
from gemcode.hitl_session import HITL_STICKY_SESSION_KEY
|
|
6
8
|
|
|
7
9
|
from gemcode.config import GemCodeConfig
|
|
@@ -43,7 +45,8 @@ def test_read_file(tmp_path: Path, monkeypatch) -> None:
|
|
|
43
45
|
assert out["content"] == "hello"
|
|
44
46
|
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
@pytest.mark.asyncio
|
|
49
|
+
async def test_run_command_allowlist_bypass_after_shell_gate(tmp_path: Path, monkeypatch) -> None:
|
|
47
50
|
"""Interactive approval arms one shot: rm works without being on GEMCODE_ALLOW_COMMANDS."""
|
|
48
51
|
monkeypatch.setenv("GEMCODE_HOME", str(tmp_path / ".gemstate"))
|
|
49
52
|
trust_root(tmp_path, trusted=True)
|
|
@@ -53,22 +56,23 @@ def test_run_command_allowlist_bypass_after_shell_gate(tmp_path: Path, monkeypat
|
|
|
53
56
|
tgt.write_text("hi", encoding="utf-8")
|
|
54
57
|
arm_confirmed_shell_basename("rm")
|
|
55
58
|
run_command = make_run_command(cfg)
|
|
56
|
-
out = run_command("rm", ["x.txt"])
|
|
59
|
+
out = await run_command("rm", ["x.txt"])
|
|
57
60
|
assert out.get("exit_code") == 0
|
|
58
61
|
assert not tgt.exists()
|
|
59
62
|
# Gate is not consumed when a different executable runs first (still non-allowlisted).
|
|
60
63
|
arm_confirmed_shell_basename("rm")
|
|
61
64
|
assert "uname" not in cfg.allow_commands
|
|
62
|
-
wrong = run_command("uname", ["-a"])
|
|
65
|
+
wrong = await run_command("uname", ["-a"])
|
|
63
66
|
assert "not in allowlist" in str(wrong.get("error", ""))
|
|
64
67
|
tgt2 = tmp_path / "y.txt"
|
|
65
68
|
tgt2.write_text("z", encoding="utf-8")
|
|
66
|
-
ok2 = run_command("rm", ["y.txt"])
|
|
69
|
+
ok2 = await run_command("rm", ["y.txt"])
|
|
67
70
|
assert ok2.get("exit_code") == 0
|
|
68
71
|
assert not tgt2.exists()
|
|
69
72
|
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
@pytest.mark.asyncio
|
|
75
|
+
async def test_run_command_sticky_session_bypasses_allowlist(tmp_path: Path, monkeypatch) -> None:
|
|
72
76
|
monkeypatch.setenv("GEMCODE_HOME", str(tmp_path / ".gemstate"))
|
|
73
77
|
trust_root(tmp_path, trusted=True)
|
|
74
78
|
cfg = GemCodeConfig(project_root=tmp_path)
|
|
@@ -76,12 +80,13 @@ def test_run_command_sticky_session_bypasses_allowlist(tmp_path: Path, monkeypat
|
|
|
76
80
|
run_command = make_run_command(cfg)
|
|
77
81
|
ctx = MagicMock()
|
|
78
82
|
ctx.state = {HITL_STICKY_SESSION_KEY: True}
|
|
79
|
-
out = run_command("uname", ["-a"], tool_context=ctx)
|
|
83
|
+
out = await run_command("uname", ["-a"], tool_context=ctx)
|
|
80
84
|
assert out.get("exit_code") == 0
|
|
81
85
|
assert (out.get("stdout") or out.get("stderr") or "").strip()
|
|
82
86
|
|
|
83
87
|
|
|
84
|
-
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
async def test_run_command_cwd_subdir(tmp_path: Path, monkeypatch) -> None:
|
|
85
90
|
monkeypatch.setenv("GEMCODE_HOME", str(tmp_path / ".gemstate"))
|
|
86
91
|
trust_root(tmp_path, trusted=True)
|
|
87
92
|
cfg = GemCodeConfig(project_root=tmp_path)
|
|
@@ -91,7 +96,7 @@ def test_run_command_cwd_subdir(tmp_path: Path, monkeypatch) -> None:
|
|
|
91
96
|
run_command = make_run_command(cfg)
|
|
92
97
|
ctx = MagicMock()
|
|
93
98
|
ctx.state = {HITL_STICKY_SESSION_KEY: True}
|
|
94
|
-
out = run_command(
|
|
99
|
+
out = await run_command(
|
|
95
100
|
"python3",
|
|
96
101
|
["-c", "print(open('marker.txt').read())"],
|
|
97
102
|
cwd_subdir="nest",
|
|
@@ -101,14 +106,15 @@ def test_run_command_cwd_subdir(tmp_path: Path, monkeypatch) -> None:
|
|
|
101
106
|
assert "in-nest" in (out.get("stdout") or "")
|
|
102
107
|
|
|
103
108
|
|
|
104
|
-
|
|
109
|
+
@pytest.mark.asyncio
|
|
110
|
+
async def test_run_command_background_returns_pid(tmp_path: Path, monkeypatch) -> None:
|
|
105
111
|
monkeypatch.setenv("GEMCODE_HOME", str(tmp_path / ".gemstate"))
|
|
106
112
|
trust_root(tmp_path, trusted=True)
|
|
107
113
|
cfg = GemCodeConfig(project_root=tmp_path)
|
|
108
114
|
run_command = make_run_command(cfg)
|
|
109
115
|
ctx = MagicMock()
|
|
110
116
|
ctx.state = {HITL_STICKY_SESSION_KEY: True}
|
|
111
|
-
out = run_command(
|
|
117
|
+
out = await run_command(
|
|
112
118
|
"python3",
|
|
113
119
|
["-c", "print(1)"],
|
|
114
120
|
background=True,
|
|
@@ -118,14 +124,15 @@ def test_run_command_background_returns_pid(tmp_path: Path, monkeypatch) -> None
|
|
|
118
124
|
assert isinstance(out.get("pid"), int)
|
|
119
125
|
|
|
120
126
|
|
|
121
|
-
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_run_command_extra_env_merges(tmp_path: Path, monkeypatch) -> None:
|
|
122
129
|
monkeypatch.setenv("GEMCODE_HOME", str(tmp_path / ".gemstate"))
|
|
123
130
|
trust_root(tmp_path, trusted=True)
|
|
124
131
|
cfg = GemCodeConfig(project_root=tmp_path)
|
|
125
132
|
run_command = make_run_command(cfg)
|
|
126
133
|
ctx = MagicMock()
|
|
127
134
|
ctx.state = {HITL_STICKY_SESSION_KEY: True}
|
|
128
|
-
out = run_command(
|
|
135
|
+
out = await run_command(
|
|
129
136
|
"python3",
|
|
130
137
|
["-c", "import os; print(os.environ.get('GEMCODE_TEST_EXTRA', ''))"],
|
|
131
138
|
extra_env_keys=["GEMCODE_TEST_EXTRA"],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|